refactor: type all of wormhole-crypto

This commit is contained in:
Jeremy Kahn 2023-02-18 11:56:58 -06:00
parent 7d73a5ca7f
commit 8d27c2239a
2 changed files with 146 additions and 10 deletions

149
src/react-app-env.d.ts vendored
View File

@ -1,16 +1,147 @@
/// <reference types="react-scripts" /> /// <reference types="react-scripts" />
// TODO: Type the rest of the API and contribute it to the wormhole-crypto project
declare module 'wormhole-crypto' { declare module 'wormhole-crypto' {
export class Keychain { /**
constructor(key: Uint8Array, salt: Uint8Array) * The encrypted byte range that is needed to decrypt the client's specified range.
*/
encryptStream(ReadableStream): Promise<ReadableStream> export type ByteRange = {
offset: number
decryptStream(ReadableStream): Promise<ReadableStream> 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<Uint8Array>
/**
* Returns a `Promise` that resolves to the authentication token as a base64-encoded string.
*/
authTokenB64(): Promise<string>
/**
* Returns a `Promise` that resolves to the HTTP header value to be provided to the Wormhole server. It contains the authentication token.
*/
authHeader(): Promise<string>
/**
* 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<EncryptedStream>
/**
* 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<ReadableStream>
/**
* 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<DecryptedStreamRange>
/**
* 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<Uint8Array>
/**
* @param encryptedMeta The encrypted metadata buffer to decrypt.
* @returns A `Promise` that resolves to a decrypted version of `encryptedMeta`.
*/
decryptMeta(encryptedMeta: Uint8Array): Promise<Uint8Array>
}
/**
* 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
} }

View File

@ -124,8 +124,13 @@ export class FileTransfer {
const encryptedFiles = await Promise.all( const encryptedFiles = await Promise.all(
filesToSeed.map(async file => { 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( const encryptedStream = await getKeychain(password).encryptStream(
file.stream() stream
) )
// WebTorrent internally opens the ReadableStream for file data twice. // WebTorrent internally opens the ReadableStream for file data twice.