From 8d27c2239a00ec81de2d0b207c32c4d9c3f12ede Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Sat, 18 Feb 2023 11:56:58 -0600 Subject: [PATCH] refactor: type all of wormhole-crypto --- src/react-app-env.d.ts | 149 ++++++++++++++++++++-- src/services/FileTransfer/FileTransfer.ts | 7 +- 2 files changed, 146 insertions(+), 10 deletions(-) diff --git a/src/react-app-env.d.ts b/src/react-app-env.d.ts index 12c6ef2..56c759b 100644 --- a/src/react-app-env.d.ts +++ b/src/react-app-env.d.ts @@ -1,16 +1,147 @@ /// -// TODO: Type the rest of the API and contribute it to the wormhole-crypto project declare module 'wormhole-crypto' { - export class Keychain { - constructor(key: Uint8Array, salt: Uint8Array) - - encryptStream(ReadableStream): Promise - - decryptStream(ReadableStream): Promise + /** + * The encrypted byte range that is needed to decrypt the client's specified range. + */ + export type ByteRange = { + offset: number + length: number } - export const plaintextSize = number => number + /** + * The metadata buffer to encrypt. + */ + export type Meta = Uint8Array - export const encryptedSize = number => number + /** + * A WHATWG readable stream used as a data source for the plaintext stream. + */ + export type EncryptedStream = ReadableStream + + /** + * @param streams An array of `ReadableStream` objects, one for each of the requested ranges. + * @returns Contains the plaintext data for the client's desired byte range. + */ + export type DecryptFn = (streams: ReadableStream[]) => ReadableStream + + export type DecryptedStreamRange = { + ranges: ByteRange[] + decrypt: DecryptFn + } + + export class Keychain { + /** + * Create a new keychain object. The keychain can be used to create encryption streams, decryption streams, and to encrypt or decrypt a "metadata" buffer. + * + * @param key The main key. This should be 16 bytes in length. If a string is given, then it should be a base64-encoded string. If the argument is null, then a key will be automatically generated. + * @param salt The salt. This should be 16 bytes in length. If a string is given, then it should be a base64-encoded string. If this argument is null, then a salt will be automatically generated. + */ + constructor( + key: Uint8Array | string | null = null, + salt: Uint8Array | string | null = null + ) + + /** + * The main key. + */ + key: Uint8Array + + /** + * The main key as a base64-encoded string. + */ + keyB64: string + + /** + * The salt. + * + * Implementation note: The salt is used to derive the (internal) metadata key and authentication token. + */ + salt: Uint8Array + + /** + * The salt as a base64-encoded string. + */ + saltB64: string + + /** + * Returns a `Promise` which resolves to the authentication token. By default, the authentication token is automatically derived from the main key using HKDF SHA-256. + * + * In Wormhole, the authentication token is used to communicate with the server and prove that the client has permission to fetch data for a room. Without a valid authentication token, the server will not return the encrypted room metadata or allow downloading the encrypted file data. + * + * Since the authentication token is derived from the main key, the client presents it to the Wormhole server as a "reader token" to prove that it is in possession of the main key without revealing the main key to the server. + * + * For destructive operations, like modifying the room, the client instead presents a "writer token", which is not derived from the main key but is provided by the server to the room creator who overrides the keychain authentication token by calling `keychain.setAuthToken(authToken)` with the "writer token". + */ + authToken(): Promise + + /** + * Returns a `Promise` that resolves to the authentication token as a base64-encoded string. + */ + authTokenB64(): Promise + + /** + * Returns a `Promise` that resolves to the HTTP header value to be provided to the Wormhole server. It contains the authentication token. + */ + authHeader(): Promise + + /** + * Update the keychain authentication token to `authToken`. + * + * @param authToken The authentication token. This should be 16 bytes in length. If a `string` is given, then it should be a base64-encoded string. If this argument is `null`, then an authentication token will be automatically generated. + */ + setAuthToken(authToken: Uint8Array | string | null = null): void + + /** + * @param stream A WHATWG readable stream used as a data source for the encrypted stream. + * + * @returns A `Promise` that resolves to a `ReadableStream` encryption stream that consumes the data in `stream` and returns an encrypted version. Data is encrypted with [Encrypted Content-Encoding for HTTP (RFC 8188)](https://tools.ietf.org/html/rfc8188). + */ + encryptStream(stream: ReadableStream): Promise + + /** + * Returns a `Promise` that resolves to a `ReadableStream` decryption stream that consumes the data in `encryptedStream` and returns a plaintext version. + * + * @param encryptedStream A WHATWG readable stream that was returned from encryptStream. + * @returns A `Promise` that resolves to a `ReadableStream` decryption stream that +consumes the data in `encryptedStream` and returns a plaintext version. + */ + decryptStream(encryptedStream: EncryptedStream): Promise + + /** + * Returns a `Promise` that resolves to a object containing `ranges`, which is an array of objects containing `offset` and `length` integers specifying the encrypted byte ranges that are needed to decrypt the client's specified range, and a `decrypt` function. + * + * Once the client has gathered a stream for each byte range in `ranges`, the client should call `decrypt(streams)`, where `streams` is an array of `ReadableStream` objects, one for each of the requested ranges. `decrypt` will then return a `ReadableStream` containing the plaintext data for the client's desired byte range. + */ + decryptStreamRange( + offset: number, + length: number, + totalEncryptedLength: number + ): Promise + + /** + * Implementation note: The metadata key is automatically derived from the main key using HKDF SHA-256. The value is not user-controlled. + * + * Implementation note: The initialization vector (IV) is automatically generated and included in the encrypted output. No need to generate it or to manage it separately from the encrypted output. + * + * @returns A `Promise` that resolves to an encrypted version of `meta`. The metadata is encrypted with AES-GCM. + */ + encryptMeta(meta: Meta): Promise + + /** + * @param encryptedMeta The encrypted metadata buffer to decrypt. + * @returns A `Promise` that resolves to a decrypted version of `encryptedMeta`. + */ + decryptMeta(encryptedMeta: Uint8Array): Promise + } + + /** + * Given an encrypted size, return the corresponding plaintext size. + */ + export function plaintextSize(encryptedSize: number): number + + /** + * Given a plaintext size, return the corresponding encrypted size. + */ + export function encryptedSize(plaintextSize: number): number } diff --git a/src/services/FileTransfer/FileTransfer.ts b/src/services/FileTransfer/FileTransfer.ts index 687233b..8bc56e5 100644 --- a/src/services/FileTransfer/FileTransfer.ts +++ b/src/services/FileTransfer/FileTransfer.ts @@ -124,8 +124,13 @@ export class FileTransfer { const encryptedFiles = await Promise.all( filesToSeed.map(async file => { + // Force a type conversion here to prevent stream from being typed as a + // NodeJS.ReadableStream, which is the default overloaded return type + // for file.stream(). + const stream = file.stream() as any as ReadableStream + const encryptedStream = await getKeychain(password).encryptStream( - file.stream() + stream ) // WebTorrent internally opens the ReadableStream for file data twice.