@@ -4,7 +4,7 @@ import { sha256 } from "@noble/hashes/sha256"
44import { randomBytes } from "@noble/hashes/utils"
55import { ScryptIdentity , ScryptRecipient , X25519Identity , X25519Recipient } from "./recipients.js"
66import { encodeHeader , encodeHeaderNoMAC , parseHeader , Stanza } from "./format.js"
7- import { decryptSTREAM , encryptSTREAM } from "./stream.js"
7+ import { ciphertextSize , decryptSTREAM , encryptSTREAM , plaintextSize } from "./stream.js"
88import { readAll , stream , read , readAllString , prepend } from "./io.js"
99
1010export * as armor from "./armor.js"
@@ -64,6 +64,27 @@ export interface Recipient {
6464 wrapFileKey ( fileKey : Uint8Array ) : Stanza [ ] | Promise < Stanza [ ] > ;
6565}
6666
67+ /**
68+ * A ReadableStream that can also report the expected output size based on the
69+ * input size.
70+ */
71+ export interface ReadableStreamWithSize extends ReadableStream < Uint8Array > {
72+ /**
73+ * Calculate the expected plaintext size from the given ciphertext size, or
74+ * vice versa.
75+ *
76+ * @param sourceSize - The size of the ciphertext or plaintext to calculate
77+ * the expected output size for.
78+ *
79+ * @returns The expected plaintext size if the input is a ciphertext, or the
80+ * expected ciphertext size if the input is a plaintext.
81+ *
82+ * @throws Only if the input is a ciphertext, and the ciphertext size is
83+ * too small or invalid. There are no invalid plaintext sizes.
84+ */
85+ size ( sourceSize : number ) : number
86+ }
87+
6788export { generateIdentity , identityToRecipient } from "./recipients.js"
6889
6990/**
@@ -139,11 +160,11 @@ export class Encrypter {
139160 * encoded as UTF-8.
140161 *
141162 * @returns A promise that resolves to the encrypted file as a Uint8Array,
142- * or as a ReadableStream if the input was a stream.
163+ * or as a { @link ReadableStreamWithSize} if the input was a stream.
143164 */
144165 async encrypt ( file : Uint8Array | string ) : Promise < Uint8Array >
145- async encrypt ( file : ReadableStream < Uint8Array > ) : Promise < ReadableStream < Uint8Array > >
146- async encrypt ( file : Uint8Array | string | ReadableStream < Uint8Array > ) : Promise < Uint8Array | ReadableStream < Uint8Array > > {
166+ async encrypt ( file : ReadableStream < Uint8Array > ) : Promise < ReadableStreamWithSize >
167+ async encrypt ( file : Uint8Array | string | ReadableStream < Uint8Array > ) : Promise < Uint8Array | ReadableStreamWithSize > {
147168 const fileKey = randomBytes ( 16 )
148169 const stanzas : Stanza [ ] = [ ]
149170
@@ -163,9 +184,13 @@ export class Encrypter {
163184 const streamKey = hkdf ( sha256 , fileKey , nonce , "payload" , 32 )
164185 const encrypter = encryptSTREAM ( streamKey )
165186
166- if ( file instanceof ReadableStream ) return prepend ( file . pipeThrough ( encrypter ) , header , nonce )
167- if ( typeof file === "string" ) file = new TextEncoder ( ) . encode ( file )
168- return await readAll ( prepend ( stream ( file ) . pipeThrough ( encrypter ) , header , nonce ) )
187+ if ( ! ( file instanceof ReadableStream ) ) {
188+ if ( typeof file === "string" ) file = new TextEncoder ( ) . encode ( file )
189+ return await readAll ( prepend ( stream ( file ) . pipeThrough ( encrypter ) , header , nonce ) )
190+ }
191+ return Object . assign ( prepend ( file . pipeThrough ( encrypter ) , header , nonce ) , {
192+ size : ( size : number ) : number => ciphertextSize ( size ) + header . length + nonce . length
193+ } )
169194 }
170195}
171196
@@ -230,22 +255,25 @@ export class Decrypter {
230255 * Ignored if the input is a stream.
231256 *
232257 * @returns A promise that resolves to the decrypted file, or to a
233- * ReadableStream of the decrypted file if the input was a stream.
234- * The header is processed before the promise resolves.
258+ * { @link ReadableStreamWithSize} of the decrypted file if the input was a
259+ * stream. The header is processed before the promise resolves.
235260 */
236261 async decrypt ( file : Uint8Array , outputFormat ?: "uint8array" ) : Promise < Uint8Array >
237262 async decrypt ( file : Uint8Array , outputFormat : "text" ) : Promise < string >
238- async decrypt ( file : ReadableStream < Uint8Array > ) : Promise < ReadableStream < Uint8Array > >
239- async decrypt ( file : Uint8Array | ReadableStream < Uint8Array > , outputFormat ?: "text" | "uint8array" ) : Promise < string | Uint8Array | ReadableStream < Uint8Array > > {
263+ async decrypt ( file : ReadableStream < Uint8Array > ) : Promise < ReadableStreamWithSize >
264+ async decrypt ( file : Uint8Array | ReadableStream < Uint8Array > , outputFormat ?: "text" | "uint8array" ) : Promise < string | Uint8Array | ReadableStreamWithSize > {
240265 const s = file instanceof ReadableStream ? file : stream ( file )
241- const { fileKey, rest } = await this . decryptHeaderInternal ( s )
266+ const { fileKey, headerSize , rest } = await this . decryptHeaderInternal ( s )
242267 const { data : nonce , rest : payload } = await read ( rest , 16 )
243268
244269 const streamKey = hkdf ( sha256 , fileKey , nonce , "payload" , 32 )
245270 const decrypter = decryptSTREAM ( streamKey )
246271 const out = payload . pipeThrough ( decrypter )
247272
248- if ( file instanceof ReadableStream ) return out
273+ const outWithSize = Object . assign ( out , {
274+ size : ( size : number ) : number => plaintextSize ( size - headerSize - nonce . length )
275+ } )
276+ if ( file instanceof ReadableStream ) return outWithSize
249277 if ( outputFormat === "text" ) return await readAllString ( out )
250278 return await readAll ( out )
251279 }
@@ -267,7 +295,7 @@ export class Decrypter {
267295 return ( await this . decryptHeaderInternal ( stream ( header ) ) ) . fileKey
268296 }
269297
270- private async decryptHeaderInternal ( file : ReadableStream < Uint8Array > ) : Promise < { fileKey : Uint8Array , rest : ReadableStream < Uint8Array > } > {
298+ private async decryptHeaderInternal ( file : ReadableStream < Uint8Array > ) : Promise < { fileKey : Uint8Array , headerSize : number , rest : ReadableStream < Uint8Array > } > {
271299 const h = await parseHeader ( file )
272300 const fileKey = await this . unwrapFileKey ( h . stanzas )
273301 if ( fileKey === null ) throw Error ( "no identity matched any of the file's recipients" )
@@ -276,7 +304,7 @@ export class Decrypter {
276304 const mac = hmac ( sha256 , hmacKey , h . headerNoMAC )
277305 if ( ! compareBytes ( h . MAC , mac ) ) throw Error ( "invalid header HMAC" )
278306
279- return { fileKey : fileKey , rest : h . rest }
307+ return { fileKey, headerSize : h . headerSize , rest : h . rest }
280308 }
281309
282310 private async unwrapFileKey ( stanzas : Stanza [ ] ) : Promise < Uint8Array | null > {
0 commit comments