* feat: [#86] encrypt torrent data before upload * feat: [#86] decrypt torrent data after download * feat: [#86] use room ID as encryption key * feat: [#86] show alerts for encryption activity * feat: [#86] show progress indicator while encrypting files
This commit is contained in:
parent
8493ddade5
commit
d7287b5f6d
54
package-lock.json
generated
54
package-lock.json
generated
@ -36,6 +36,8 @@
|
|||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"readable-stream-node-to-web": "^1.0.1",
|
||||||
|
"readable-web-to-node-stream": "^3.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"sass": "^1.54.3",
|
"sass": "^1.54.3",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
@ -45,7 +47,8 @@
|
|||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"webtorrent": "^1.9.4"
|
"webtorrent": "^1.9.4",
|
||||||
|
"wormhole-crypto": "^0.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react-syntax-highlighter": "^15.5.5",
|
"@types/react-syntax-highlighter": "^15.5.5",
|
||||||
@ -22489,6 +22492,26 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readable-stream-node-to-web": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream-node-to-web/-/readable-stream-node-to-web-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-OGzi2VKLa8H259kAx7BIwuRrXHGcxeHj4RdASSgEGBP9Q2wowdPvBc65upF4Q9O05qWgKqBw1+9PiLTtObl7uQ=="
|
||||||
|
},
|
||||||
|
"node_modules/readable-web-to-node-stream": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "^3.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Borewit"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@ -26674,6 +26697,14 @@
|
|||||||
"workbox-core": "6.5.4"
|
"workbox-core": "6.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/wormhole-crypto": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/wormhole-crypto/-/wormhole-crypto-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-P6dsua8VTTbaLozTSbeDWBuXcoAJABH8a+XutfMg9Vb2a/roUPLFvkgtuJcXorp8hwLM5RguQEWtMZqgpJYohA==",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
@ -43199,6 +43230,19 @@
|
|||||||
"util-deprecate": "^1.0.1"
|
"util-deprecate": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"readable-stream-node-to-web": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream-node-to-web/-/readable-stream-node-to-web-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-OGzi2VKLa8H259kAx7BIwuRrXHGcxeHj4RdASSgEGBP9Q2wowdPvBc65upF4Q9O05qWgKqBw1+9PiLTtObl7uQ=="
|
||||||
|
},
|
||||||
|
"readable-web-to-node-stream": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-ePeK6cc1EcKLEhJFt/AebMCLL+GgSKhuygrZ/GLaKZYEecIgIECf4UaUuaByiGtzckwR4ain9VzUh95T1exYGw==",
|
||||||
|
"requires": {
|
||||||
|
"readable-stream": "^3.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"readdirp": {
|
"readdirp": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||||
@ -46280,6 +46324,14 @@
|
|||||||
"workbox-core": "6.5.4"
|
"workbox-core": "6.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"wormhole-crypto": {
|
||||||
|
"version": "0.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/wormhole-crypto/-/wormhole-crypto-0.3.1.tgz",
|
||||||
|
"integrity": "sha512-P6dsua8VTTbaLozTSbeDWBuXcoAJABH8a+XutfMg9Vb2a/roUPLFvkgtuJcXorp8hwLM5RguQEWtMZqgpJYohA==",
|
||||||
|
"requires": {
|
||||||
|
"base64-js": "^1.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"wrap-ansi": {
|
"wrap-ansi": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||||
|
@ -32,6 +32,8 @@
|
|||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"readable-stream-node-to-web": "^1.0.1",
|
||||||
|
"readable-web-to-node-stream": "^3.0.2",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"sass": "^1.54.3",
|
"sass": "^1.54.3",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
@ -41,7 +43,8 @@
|
|||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"webtorrent": "^1.9.4"
|
"webtorrent": "^1.9.4",
|
||||||
|
"wormhole-crypto": "^0.3.1"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useContext, useEffect, useRef, useState } from 'react'
|
||||||
|
import { TorrentFile } from 'webtorrent'
|
||||||
|
import { ReadableWebToNodeStream } from 'readable-web-to-node-stream'
|
||||||
import CircularProgress from '@mui/material/CircularProgress'
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
import { Typography } from '@mui/material'
|
||||||
|
|
||||||
import { fileTransfer } from 'services/FileTransfer'
|
import { fileTransfer } from 'services/FileTransfer'
|
||||||
import { Typography } from '@mui/material'
|
import { ShellContext } from 'contexts/ShellContext'
|
||||||
|
|
||||||
type TorrentFiles = Awaited<ReturnType<typeof fileTransfer.download>>
|
type TorrentFiles = Awaited<ReturnType<typeof fileTransfer.download>>
|
||||||
|
|
||||||
@ -17,18 +20,43 @@ interface InlineFileProps {
|
|||||||
export const InlineFile = ({ file }: InlineFileProps) => {
|
export const InlineFile = ({ file }: InlineFileProps) => {
|
||||||
const containerRef = useRef(null)
|
const containerRef = useRef(null)
|
||||||
const [didRenderingMediaFail, setDidRenderingMediaFail] = useState(false)
|
const [didRenderingMediaFail, setDidRenderingMediaFail] = useState(false)
|
||||||
|
const shellContext = useContext(ShellContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { current: container } = containerRef
|
;(async () => {
|
||||||
|
const { current: container } = containerRef
|
||||||
|
|
||||||
if (!container) return
|
if (!container) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
file.appendTo(container)
|
if (typeof shellContext.roomId !== 'string') {
|
||||||
} catch (e) {
|
throw new Error('shellContext.roomId is not a string')
|
||||||
setDidRenderingMediaFail(true)
|
}
|
||||||
}
|
|
||||||
}, [file, containerRef])
|
const readStream: NodeJS.ReadableStream = new ReadableWebToNodeStream(
|
||||||
|
await fileTransfer.getDecryptedFileReadStream(
|
||||||
|
file,
|
||||||
|
shellContext.roomId
|
||||||
|
)
|
||||||
|
// ReadableWebToNodeStream is the same as NodeJS.ReadableStream. The
|
||||||
|
// library's typing is wrong.
|
||||||
|
) as any
|
||||||
|
|
||||||
|
const decryptedFile: TorrentFile = {
|
||||||
|
...file,
|
||||||
|
createReadStream: () => {
|
||||||
|
return readStream
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.setPrototypeOf(decryptedFile, Object.getPrototypeOf(file))
|
||||||
|
decryptedFile.appendTo(container)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
setDidRenderingMediaFail(true)
|
||||||
|
}
|
||||||
|
})()
|
||||||
|
}, [file, containerRef, shellContext.roomId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef}>
|
<div ref={containerRef}>
|
||||||
@ -44,16 +72,20 @@ export const InlineFile = ({ file }: InlineFileProps) => {
|
|||||||
export const InlineMedia = ({ magnetURI }: InlineMediaProps) => {
|
export const InlineMedia = ({ magnetURI }: InlineMediaProps) => {
|
||||||
const [hasDownloadInitiated, setHasDownloadInitiated] = useState(false)
|
const [hasDownloadInitiated, setHasDownloadInitiated] = useState(false)
|
||||||
const [downloadedFiles, setDownloadedFiles] = useState<TorrentFiles>([])
|
const [downloadedFiles, setDownloadedFiles] = useState<TorrentFiles>([])
|
||||||
|
const shellContext = useContext(ShellContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
if (hasDownloadInitiated) return
|
if (hasDownloadInitiated) return
|
||||||
|
if (typeof shellContext.roomId !== 'string') {
|
||||||
|
throw new Error('shellContext.roomId is not a string')
|
||||||
|
}
|
||||||
|
|
||||||
setHasDownloadInitiated(true)
|
setHasDownloadInitiated(true)
|
||||||
const files = await fileTransfer.download(magnetURI)
|
const files = await fileTransfer.download(magnetURI, shellContext.roomId)
|
||||||
setDownloadedFiles(files)
|
setDownloadedFiles(files)
|
||||||
})()
|
})()
|
||||||
}, [hasDownloadInitiated, magnetURI])
|
}, [hasDownloadInitiated, magnetURI, shellContext.roomId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -4,6 +4,7 @@ import UploadFile from '@mui/icons-material/UploadFile'
|
|||||||
import Cancel from '@mui/icons-material/Cancel'
|
import Cancel from '@mui/icons-material/Cancel'
|
||||||
import Fab from '@mui/material/Fab'
|
import Fab from '@mui/material/Fab'
|
||||||
import Tooltip from '@mui/material/Tooltip'
|
import Tooltip from '@mui/material/Tooltip'
|
||||||
|
import CircularProgress from '@mui/material/CircularProgress'
|
||||||
|
|
||||||
import { RoomContext } from 'contexts/RoomContext'
|
import { RoomContext } from 'contexts/RoomContext'
|
||||||
import { PeerRoom } from 'services/PeerRoom/PeerRoom'
|
import { PeerRoom } from 'services/PeerRoom/PeerRoom'
|
||||||
@ -60,6 +61,8 @@ export function RoomFileUploadControls({
|
|||||||
|
|
||||||
const disableFileUpload = !isFileSharingEnabled || isMessageSending
|
const disableFileUpload = !isFileSharingEnabled || isMessageSending
|
||||||
|
|
||||||
|
const buttonIcon = isSharingFile ? <Cancel /> : <UploadFile />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -91,7 +94,11 @@ export function RoomFileUploadControls({
|
|||||||
onClick={handleToggleScreenShareButtonClick}
|
onClick={handleToggleScreenShareButtonClick}
|
||||||
disabled={disableFileUpload}
|
disabled={disableFileUpload}
|
||||||
>
|
>
|
||||||
{isSharingFile ? <Cancel /> : <UploadFile />}
|
{isFileSharingEnabled ? (
|
||||||
|
buttonIcon
|
||||||
|
) : (
|
||||||
|
<CircularProgress variant="indeterminate" color="inherit" />
|
||||||
|
)}
|
||||||
</Fab>
|
</Fab>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -322,7 +322,7 @@ export function useRoom(
|
|||||||
if (!showVideoDisplay && !isShowingMessages) setIsShowingMessages(true)
|
if (!showVideoDisplay && !isShowingMessages) setIsShowingMessages(true)
|
||||||
|
|
||||||
const handleInlineMediaUpload = async (files: File[]) => {
|
const handleInlineMediaUpload = async (files: File[]) => {
|
||||||
const fileOfferId = await fileTransfer.offer(files)
|
const fileOfferId = await fileTransfer.offer(files, roomId)
|
||||||
|
|
||||||
const unsentInlineMedia: UnsentInlineMedia = {
|
const unsentInlineMedia: UnsentInlineMedia = {
|
||||||
authorId: userId,
|
authorId: userId,
|
||||||
|
@ -32,7 +32,7 @@ export function useRoomFileShare({
|
|||||||
>(null)
|
>(null)
|
||||||
const [isFileSharingEnabled, setIsFileSharingEnabled] = useState(true)
|
const [isFileSharingEnabled, setIsFileSharingEnabled] = useState(true)
|
||||||
|
|
||||||
const { peerList, setPeerList } = shellContext
|
const { peerList, setPeerList, showAlert } = shellContext
|
||||||
const { peerOfferedFileMetadata, setPeerOfferedFileMetadata } = roomContext
|
const { peerOfferedFileMetadata, setPeerOfferedFileMetadata } = roomContext
|
||||||
|
|
||||||
const [sendFileOfferMetadata, receiveFileOfferMetadata] =
|
const [sendFileOfferMetadata, receiveFileOfferMetadata] =
|
||||||
@ -121,7 +121,19 @@ export function useRoomFileShare({
|
|||||||
setSharedFiles(files)
|
setSharedFiles(files)
|
||||||
setIsFileSharingEnabled(false)
|
setIsFileSharingEnabled(false)
|
||||||
|
|
||||||
const magnetURI = await fileTransfer.offer(files)
|
if (typeof shellContext.roomId !== 'string') {
|
||||||
|
throw new Error('shellContext.roomId is not a string')
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertText =
|
||||||
|
files.length > 1
|
||||||
|
? 'Encrypting a copy of the files...'
|
||||||
|
: 'Encrypting a copy of the file...'
|
||||||
|
showAlert(alertText, { severity: 'info' })
|
||||||
|
|
||||||
|
const magnetURI = await fileTransfer.offer(files, shellContext.roomId)
|
||||||
|
|
||||||
|
showAlert('Encryption complete', { severity: 'success' })
|
||||||
|
|
||||||
if (inlineMediaFiles.length > 0) {
|
if (inlineMediaFiles.length > 0) {
|
||||||
onInlineMediaUpload(inlineMediaFiles)
|
onInlineMediaUpload(inlineMediaFiles)
|
||||||
|
@ -38,7 +38,14 @@ export const PeerDownloadFileButton = ({
|
|||||||
setDownloadProgress(null)
|
setDownloadProgress(null)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fileTransfer.download(offeredFileId, { doSave: true, onProgress })
|
if (typeof shellContext.roomId !== 'string') {
|
||||||
|
throw new Error('shellContext.roomId is not a string')
|
||||||
|
}
|
||||||
|
|
||||||
|
await fileTransfer.download(offeredFileId, shellContext.roomId, {
|
||||||
|
doSave: true,
|
||||||
|
onProgress,
|
||||||
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (isError(e)) {
|
if (isError(e)) {
|
||||||
shellContext.showAlert(e.message, {
|
shellContext.showAlert(e.message, {
|
||||||
|
@ -1,14 +1,36 @@
|
|||||||
import WebTorrent, { Torrent } from 'webtorrent'
|
import WebTorrent, { Torrent, TorrentFile } from 'webtorrent'
|
||||||
import streamSaver from 'streamsaver'
|
import streamSaver from 'streamsaver'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
import { Keychain, plaintextSize } from 'wormhole-crypto'
|
||||||
|
// @ts-ignore
|
||||||
import idbChunkStore from 'idb-chunk-store'
|
import idbChunkStore from 'idb-chunk-store'
|
||||||
import { detectIncognito } from 'detectincognitojs'
|
import { detectIncognito } from 'detectincognitojs'
|
||||||
|
|
||||||
import { trackerUrls } from 'config/trackerUrls'
|
import { trackerUrls } from 'config/trackerUrls'
|
||||||
import { streamSaverUrl } from 'config/streamSaverUrl'
|
import { streamSaverUrl } from 'config/streamSaverUrl'
|
||||||
|
|
||||||
|
import { ReadableWebToNodeStream } from 'readable-web-to-node-stream'
|
||||||
|
// @ts-ignore
|
||||||
|
import nodeToWebStream from 'readable-stream-node-to-web'
|
||||||
|
|
||||||
streamSaver.mitm = streamSaverUrl
|
streamSaver.mitm = streamSaverUrl
|
||||||
|
|
||||||
|
interface NamedReadableWebToNodeStream extends NodeJS.ReadableStream {
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const getKeychain = (password: string) => {
|
||||||
|
const encoder = new TextEncoder()
|
||||||
|
const keyLength = 16
|
||||||
|
const padding = new Array(keyLength).join('0')
|
||||||
|
const key = password.concat(padding).slice(0, keyLength)
|
||||||
|
const salt = window.location.origin.concat(padding).slice(0, keyLength)
|
||||||
|
|
||||||
|
const keychain = new Keychain(encoder.encode(key), encoder.encode(salt))
|
||||||
|
|
||||||
|
return keychain
|
||||||
|
}
|
||||||
|
|
||||||
interface DownloadOpts {
|
interface DownloadOpts {
|
||||||
doSave?: boolean
|
doSave?: boolean
|
||||||
onProgress?: (progress: number) => void
|
onProgress?: (progress: number) => void
|
||||||
@ -19,51 +41,18 @@ export class FileTransfer {
|
|||||||
|
|
||||||
private torrents: Record<Torrent['magnetURI'], Torrent> = {}
|
private torrents: Record<Torrent['magnetURI'], Torrent> = {}
|
||||||
|
|
||||||
private async saveTorrentFiles(torrent: Torrent) {
|
private async saveTorrentFiles(torrent: Torrent, password: string) {
|
||||||
for (const file of torrent.files) {
|
for (const file of torrent.files) {
|
||||||
try {
|
try {
|
||||||
await new Promise<void>((resolve, reject) => {
|
const readStream = await this.getDecryptedFileReadStream(file, password)
|
||||||
const fileStream = streamSaver.createWriteStream(file.name, {
|
|
||||||
size: file.length,
|
|
||||||
})
|
|
||||||
|
|
||||||
const writeStream = fileStream.getWriter()
|
const writeStream = streamSaver.createWriteStream(file.name, {
|
||||||
const readStream = file.createReadStream()
|
size: plaintextSize(file.length),
|
||||||
let aborted = false
|
|
||||||
|
|
||||||
const handleData = async (data: ArrayBuffer) => {
|
|
||||||
try {
|
|
||||||
await writeStream.write(data)
|
|
||||||
} catch (e) {
|
|
||||||
writeStream.abort()
|
|
||||||
readStream.off('data', handleData)
|
|
||||||
aborted = true
|
|
||||||
reject()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBeforePageUnloadForFile = () => {
|
|
||||||
// Clean up any broken downloads
|
|
||||||
writeStream.abort()
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('beforeunload', handleBeforePageUnloadForFile)
|
|
||||||
|
|
||||||
const handleEnd = async () => {
|
|
||||||
window.removeEventListener(
|
|
||||||
'beforeunload',
|
|
||||||
handleBeforePageUnloadForFile
|
|
||||||
)
|
|
||||||
|
|
||||||
if (aborted) return
|
|
||||||
|
|
||||||
await writeStream.close()
|
|
||||||
resolve()
|
|
||||||
}
|
|
||||||
|
|
||||||
readStream.on('data', handleData).on('end', handleEnd)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await readStream.pipeTo(writeStream)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
throw new Error('Download aborted')
|
throw new Error('Download aborted')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +62,20 @@ export class FileTransfer {
|
|||||||
window.addEventListener('beforeunload', this.handleBeforePageUnload)
|
window.addEventListener('beforeunload', this.handleBeforePageUnload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async download(magnetURI: string, { onProgress, doSave }: DownloadOpts = {}) {
|
async getDecryptedFileReadStream(file: TorrentFile, password: string) {
|
||||||
|
const keychain = getKeychain(password)
|
||||||
|
const readStream: ReadableStream = await keychain.decryptStream(
|
||||||
|
nodeToWebStream(file.createReadStream())
|
||||||
|
)
|
||||||
|
|
||||||
|
return readStream
|
||||||
|
}
|
||||||
|
|
||||||
|
async download(
|
||||||
|
magnetURI: string,
|
||||||
|
password: string,
|
||||||
|
{ onProgress, doSave }: DownloadOpts = {}
|
||||||
|
) {
|
||||||
let torrent = this.torrents[magnetURI]
|
let torrent = this.torrents[magnetURI]
|
||||||
|
|
||||||
if (!torrent) {
|
if (!torrent) {
|
||||||
@ -107,7 +109,7 @@ export class FileTransfer {
|
|||||||
|
|
||||||
if (doSave) {
|
if (doSave) {
|
||||||
try {
|
try {
|
||||||
await this.saveTorrentFiles(torrent)
|
await this.saveTorrentFiles(torrent, password)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
torrent.off('download', handleDownload)
|
torrent.off('download', handleDownload)
|
||||||
|
|
||||||
@ -119,12 +121,35 @@ export class FileTransfer {
|
|||||||
return torrent.files
|
return torrent.files
|
||||||
}
|
}
|
||||||
|
|
||||||
async offer(files: Parameters<typeof this.webTorrentClient.seed>[0]) {
|
async offer(files: File[] | FileList, password: string) {
|
||||||
const { isPrivate } = await detectIncognito()
|
const { isPrivate } = await detectIncognito()
|
||||||
|
|
||||||
|
const filesToSeed: File[] =
|
||||||
|
files instanceof FileList ? Array.from(files) : files
|
||||||
|
|
||||||
|
const encryptedFiles = await Promise.all(
|
||||||
|
filesToSeed.map(async file => {
|
||||||
|
const encryptedStream = await getKeychain(password).encryptStream(
|
||||||
|
file.stream()
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebTorrent only accepts Node-style ReadableStreams
|
||||||
|
const nodeStream: NamedReadableWebToNodeStream =
|
||||||
|
new ReadableWebToNodeStream(
|
||||||
|
encryptedStream
|
||||||
|
// ReadableWebToNodeStream is the same as NodeJS.ReadableStream.
|
||||||
|
// The library's typing is wrong.
|
||||||
|
) as any as NodeJS.ReadableStream
|
||||||
|
|
||||||
|
nodeStream.name = file.name
|
||||||
|
|
||||||
|
return nodeStream
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
const torrent = await new Promise<Torrent>(res => {
|
const torrent = await new Promise<Torrent>(res => {
|
||||||
this.webTorrentClient.seed(
|
this.webTorrentClient.seed(
|
||||||
files,
|
encryptedFiles,
|
||||||
{
|
{
|
||||||
announce: trackerUrls,
|
announce: trackerUrls,
|
||||||
// If the user is using their browser's private mode, IndexedDB will
|
// If the user is using their browser's private mode, IndexedDB will
|
||||||
|
@ -12,3 +12,8 @@ jest.mock('webtorrent', () => ({
|
|||||||
__esModule: true,
|
__esModule: true,
|
||||||
default: class WebTorrent {},
|
default: class WebTorrent {},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
jest.mock('wormhole-crypto', () => ({
|
||||||
|
__esModule: true,
|
||||||
|
Keychain: class Keychain {},
|
||||||
|
}))
|
||||||
|
Loading…
Reference in New Issue
Block a user