@ -1,9 +1,12 @@
// The core of the writer of phase 3: a .dkc of capsule format 2 and, when
// asked, a portable .dkk (spec §61, §62, §62.1), in the order and with the
// texts of capsule.Encrypt of the Go reference at spec-v0.9. Its random values
// come from the caller (plan of phase 3, decision 4): encrypt.ts passes those
// of crypto.getRandomValues, and only the tests of testing/ fix them.
// Internal: only encrypt.ts and testing/ import it, which a guard checks.
// texts of capsule.Encrypt of the Go reference at spec-v0.10, split as it
// is: newSealer checks the options that do not depend on the content, and
// seal writes a capsule of a format around a content given in pieces. Its
// random values come from the caller (plan of phase 3, decision 4):
// encrypt.ts passes those of crypto.getRandomValues, and only the tests of
// testing/ fix them. Internal: only encrypt.ts and testing/ import it, which
// a guard checks.
//
// Nothing is written before every check that can precede the content has
// passed, the header of PAYLOAD_AGE included. The output is closed only once
@ -21,7 +24,7 @@ import { compareInstants, type DateKey, formatRFC3339Nano, type Instant, isInsta
import { sha256Hasher } from './digest.ts' ;
import { DateKeysError } from './errors.ts' ;
import type { Extension } from './extension.ts' ;
import { DKC_PRELUDE_SIZE , FORMAT_2 , headerBinding , MAX_SEALED_CONTROL_LEN , preludeBytes } from './framing.ts' ;
import { DKC_PRELUDE_SIZE , FORMAT_2 , type FORMAT_3 , headerBinding , MAX_SEALED_CONTROL_LEN , preludeBytes } from './framing.ts' ;
import { decodeHeader , encodeHeader , type Policy , TIME_AND_KEY , TIME_ONLY } from './header.ts' ;
import { accessIdentity , payloadIdentity } from './open.ts' ;
import { sealedControlLength } from './lengths.ts' ;
@ -77,8 +80,8 @@ export interface Encrypted {
/** The effective unlock time: the time of the round, never before the requested instant. */
readonly unlockAt : Instant ;
readonly capsuleId : Uint8Array ;
/** The format written , always 2 . */
readonly format : typeof FORMAT_2 ;
/** The format written : 2 by encrypt, 3 by encryptFiles . */
readonly format : typeof FORMAT_2 | typeof FORMAT_3 ;
/** L, the padding rule and P = rule(L), the length of the plaintext of PAYLOAD_AGE (§29.1). */
readonly length : number ;
readonly padding : Padding ;
@ -114,12 +117,26 @@ const CHUNK = 64 << 10;
* of ` draws ` , as capsule . Encrypt . See encrypt .
* /
export async function writeCapsule ( src : EncryptSource , opts : EncryptOptions , draws : Draws ) : Promise < Encrypted > {
return guarded ( opts , async ( state ) = > {
// Step 1: the inputs, before anything is used.
checkOptions ( opts ) ;
const length = sourceLength ( src , opts . length ) ;
const s = await newSealer ( opts , length ) ;
return seal ( s , FORMAT_2 , length , draws , state , ( ) = > sourceContent ( src , length ) ) ;
} ) ;
}
interface WriteState {
writer? : WritableStreamDefaultWriter < Uint8Array > ;
}
// Runs a writer. Nothing partial is presented as a capsule: the output is
// aborted after any failure, even a TypeError of the options (§62.1 rule 9).
async function guarded ( opts : EncryptOptions , write : ( state : WriteState ) = > Promise < Encrypted > ) : Promise < Encrypted > {
const state : WriteState = { } ;
try {
return await write ( src , opts , draws , state ) ;
return await write ( s tate) ;
} catch ( err ) {
// Nothing partial is presented as a capsule: the output is aborted
// after any failure, even a TypeError of the options (§62.1 rule 9).
const output = ( opts as Partial < EncryptOptions > | null | undefined ) ? . output ;
try {
if ( state . writer !== undefined ) await state . writer . abort ( err ) ;
@ -131,12 +148,9 @@ export async function writeCapsule(src: EncryptSource, opts: EncryptOptions, dra
}
}
interface WriteState {
writer? : WritableStreamDefaultWriter < Uint8Array > ;
}
async function write ( src : EncryptSource , opts : EncryptOptions , draws : Draws , state : WriteState ) : Promise < Encrypted > {
// Step 1: the inputs, before anything is used.
// The options that every writer takes, checked as types before anything is
// used.
function checkOptions ( opts : EncryptOptions ) : void {
if ( typeof opts !== 'object' || opts === null ) throw new TypeError ( 'encrypt: options are required' ) ;
if ( opts . profile === undefined || opts . profile === null ) throw new TypeError ( 'capsule: EncryptOptions.Profile is required' ) ;
if ( typeof opts . now !== 'function' ) throw new TypeError ( 'capsule: EncryptOptions.Now is required' ) ;
@ -144,8 +158,33 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
if ( ! isInstant ( opts . unlockAt ) ) throw new TypeError ( 'encrypt: EncryptOptions.unlockAt is not an Instant' ) ;
if ( opts . output !== undefined && ! ( opts . output instanceof WritableStream ) ) throw new TypeError ( 'encrypt: EncryptOptions.output is not a WritableStream' ) ;
if ( opts . progress !== undefined && typeof opts . progress !== 'function' ) throw new TypeError ( 'encrypt: EncryptOptions.progress is not a function' ) ;
const length = sourceLength ( src , opts . length ) ;
}
// What the writers of both formats share, as the sealer of the reference:
// the options that do not depend on the content, copied and checked, and the
// DateKey resolved locally.
interface Sealer {
readonly profile : Profile ;
readonly policy : Policy ;
/** The recipients, checked; I_ACCESS is drawn when the capsule is sealed. */
readonly credentials : readonly Uint8Array [ ] ;
readonly portable : boolean ;
readonly code : Padding ;
readonly critical : Extension [ ] ;
readonly noncritical : Extension [ ] ;
readonly controlCritical : Extension [ ] ;
readonly controlNoncritical : Extension [ ] ;
readonly output : WritableStream < Uint8Array > | undefined ;
readonly progress : ( ( written : number , total : number ) = > void ) | undefined ;
readonly dateKey : DateKey ;
/** The time of the round, never before the requested instant. */
readonly unlock : Instant ;
}
// newSealer of the reference: copies of the inputs, then the profile, the
// clock, the padding rule with length, a first L, checked against L_MAX, the
// DateKey and the credentials (§15, §62.1 rules 2, 3 and 8).
async function newSealer ( opts : EncryptOptions , length : number ) : Promise < Sealer > {
// Step 2: copies, since the caller could change its inputs during an await.
const recipients = ( opts . recipients ? ? [ ] ) . map ( ( r , i ) = > {
if ( ! ( r instanceof Uint8Array ) || r . length !== 32 ) throw new TypeError ( ` encrypt: recipient ${ i } is not a 32-byte X25519 public key ` ) ;
@ -158,7 +197,7 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
const controlCritical = copyExtensions ( opts . controlCritical ) ;
const controlNoncritical = copyExtensions ( opts . controlNoncritical ) ;
const { policy , output , progress } = opts ;
const wantsPortableKey = opts . newPortableKey === true ;
const portable = opts . newPortableKey === true ;
const code = opts . padding === undefined ? REFORZADO : opts.padding ;
// Step 3: the profile.
@ -170,9 +209,7 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
if ( compareInstants ( unlockAt , now ) <= 0 ) throw new Error ( ` capsule: unlock time ${ formatRFC3339Nano ( unlockAt ) } is not in the future ` ) ;
// Step 5: the padding rule and P = rule(L), with the texts of PaddedLength.
if ( ! isPadding ( code ) ) throw new Error ( ` capsule: padding code ${ String ( code ) } is not defined ` ) ;
if ( length > MAX_PAYLOAD_LENGTH ) throw new Error ( ` capsule: content of ${ length } bytes exceeds L_MAX = ${ MAX_PAYLOAD_LENGTH } ` ) ;
const padded = paddedLength ( length , code ) ;
checkLength ( length , code ) ;
// Step 6: the DateKey, resolved locally (§15).
const dateKey = resolveDateKey ( profile , unlockAt ) ;
@ -182,12 +219,36 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
throw new DateKeysError ( 'ERR_ROUND_MISMATCH' , ` capsule: resolved round ${ dateKey . round } opens before the requested time ` ) ;
}
// Step 7: the credentials.
const credentials = accessRecipients ( policy , recipients , portable ) ;
return { profile , policy , credentials , portable , code , critical , noncritical , controlCritical , controlNoncritical , output , progress , dateKey , unlock } ;
}
// The padding rule and L, as PaddedLength checks them.
function checkLength ( length : number , code : Padding ) : void {
if ( ! isPadding ( code ) ) throw new Error ( ` capsule: padding code ${ String ( code ) } is not defined ` ) ;
if ( length > MAX_PAYLOAD_LENGTH ) throw new Error ( ` capsule: content of ${ length } bytes exceeds L_MAX = ${ MAX_PAYLOAD_LENGTH } ` ) ;
}
// The write of the sealer of the reference: a capsule of `format` whose
// content, `length` bytes that `content` gives, goes into the plaintext of
// PAYLOAD_AGE, followed by the zeros of its padding up to P (§29.1).
async function seal (
s : Sealer ,
format : typeof FORMAT_2 | typeof FORMAT_3 ,
length : number ,
draws : Draws ,
state : WriteState ,
content : ( ) = > Content ,
) : Promise < Encrypted > {
const { profile , policy , code , dateKey , output , progress } = s ;
const padded = paddedLength ( length , code ) ;
const wipe : Uint8Array [ ] = [ ] ;
try {
// Step 7: the credentials, and I_ACCESS when asked, the last of them.
const credentials = accessRecipients ( policy , recipients , wantsPortableKey ) ;
// Step 7: I_ACCESS when asked, the last of the credentials .
const credentials = [ . . . s . credentials ] ;
let portable : Uint8Array | undefined ;
if ( wantsPortableKey ) {
if ( s. portable ) {
portable = drawn ( draws . accessIdentity ( ) , 32 , 'I_ACCESS' ) ;
wipe . push ( portable ) ;
credentials . push ( x25519PublicKey ( portable ) ) ;
@ -216,7 +277,7 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
}
// Step 10: PUBLIC_HEADER, checked with the decoder of the reader.
const header = encodeHeader ( { capsuleId , dateKey , policy , critical , noncritical } ) ;
const header = encodeHeader ( { capsuleId , dateKey , policy , critical : s.critical , noncritical: s. noncritical } ) ;
selfCheck ( 'capsule: self-check: the reader rejects this PUBLIC_HEADER' , ( ) = > decodeHeader ( header ) ) ;
// Step 11: the length of CONTROL_CBOR, which does not depend on the
@ -224,8 +285,15 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
// SEALED_CONTROL_LEN. The provisional control holds L and the control
// extensions, hidden until the date (§55.2): it is wiped at once.
const provisional = encodeControl (
{ headerBinding : new Uint8Array ( 32 ) , payloadIdentity : new Uint8Array ( 32 ) , critical : controlCritical , noncritical : controlNoncritical , payloadLength : length , padding : code } ,
FORMAT_2 ,
{
headerBinding : new Uint8Array ( 32 ) ,
payloadIdentity : new Uint8Array ( 32 ) ,
critical : s.controlCritical ,
noncritical : s.controlNoncritical ,
payloadLength : length ,
padding : code ,
} ,
format ,
) ;
const controlLength = provisional . length ;
provisional . fill ( 0 ) ;
@ -239,17 +307,18 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
}
// Step 12: PRELUDE and header_binding over the exact bytes (§26).
const prelude = preludeBytes ( { format : FORMAT_2 , publicHeaderLen : header.length , sealedControlLen : sealedLength } ) ;
const prelude = preludeBytes ( { format , publicHeaderLen : header.length , sealedControlLen : sealedLength } ) ;
const binding = await headerBinding ( prelude , header ) ;
// Step 13: CONTROL_CBOR, schema version 2, checked with the decoder of
// the reader; the copy of I_PAYLOAD it decodes is wiped at once.
// Step 13: CONTROL_CBOR, of the schema version of the format, checked
// with the decoder of the reader; the copy of I_PAYLOAD it decodes is
// wiped at once.
const control = encodeControl (
{ headerBinding : binding , payloadIdentity : payloadId , critical : controlCritical, noncritical : controlNoncritical, payloadLength : length , padding : code } ,
FORMAT_2 ,
{ headerBinding : binding , payloadIdentity : payloadId , critical : s. controlCritical, noncritical : s. controlNoncritical, payloadLength : length , padding : code } ,
format ,
) ;
wipe . push ( control ) ;
selfCheck ( 'capsule: self-check: the reader rejects this CONTROL_CBOR' , ( ) = > decodeControl ( control , FORMAT_2 ) . payloadIdentity . fill ( 0 ) ) ;
selfCheck ( 'capsule: self-check: the reader rejects this CONTROL_CBOR' , ( ) = > decodeControl ( control , format ) . payloadIdentity . fill ( 0 ) ) ;
// Step 14: SEALED_CONTROL. INNER_ACCESS_AGE for the 16 slots in their
// order, then OUTER_TIME_AGE with the tlock recipient alone: each age
@ -273,10 +342,10 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
// Step 15: PAYLOAD_AGE for R_PAYLOAD, over the content and its padding.
// Its first piece is its age header alone, which I_PAYLOAD must open
// before anything is written; then I_PAYLOAD is wiped.
// The source is read through conten tStream, built first: a source that
// The content is read through plaintex tStream, built first: a source that
// cannot be read, such as a locked stream, fails as the caller's error.
const source : SourceState = { } ;
const input = contentStream( src , length , padded , source ) ;
const input = plaintextStream( content ( ) , length , padded , source ) ;
const pe = new Encrypter ( ) ;
pe . addRecipient ( formatX25519Recipient ( payloadRecipient ) ) ;
const payload = ( await sealWith ( ( ) = > pe . encrypt ( input ) ) ) as ReadableStreamWithSize ;
@ -333,9 +402,9 @@ async function write(src: EncryptSource, opts: EncryptOptions, draws: Draws, sta
}
return {
dateKey ,
unlockAt : unlock,
unlockAt : s. unlock,
capsuleId ,
format : FORMAT_2 ,
format ,
length ,
padding : code ,
paddedLength : padded ,
@ -503,45 +572,33 @@ async function readPiece(reader: ReadableStreamDefaultReader<Uint8Array>, source
}
}
// The plaintext of PAYLOAD_AGE: exactly L bytes of the source, in pieces of
// at most 64 KiB, then the P - L zeros of the padding in fresh pieces
// (§29.1). A source that ends before L, or delivers more than L, fails with
// the texts of writeContent of the Go reference; empty chunks count for
// nothing. The bytes are counted for every source, since the view of a
// resizable or transferred ArrayBuffer can shrink during an await.
function contentStream ( src : EncryptSource , length : number , padded : number , state : SourceState ) : ReadableStream < Uint8Array > {
const reader = src instanceof Uint8Array ? arrayReader ( src ) : ( src instanceof Blob ? src . stream ( ) : src ) . getReader ( ) ;
// The content of a capsule as a writer gives it: pieces of at most 64 KiB,
// exactly L bytes in all. A failure of next is the error of its source,
// which the writer rethrows as it is.
interface Content {
next ( ) : Promise < Uint8Array | undefined > ;
cancel ( reason : unknown ) : Promise < void > ;
}
// The plaintext of PAYLOAD_AGE: the L bytes of the content, then the P - L
// zeros of the padding in fresh pieces (§29.1).
function plaintextStream ( content : Content , length : number , padded : number , state : SourceState ) : ReadableStream < Uint8Array > {
let read = 0 ;
let pending : Uint8Array = new Uint8Array ( 0 ) ;
let ended = false ;
let padding = padded - length ;
const more = ( ) : Error = > new Error ( ` capsule: the source delivers more than the ${ length } bytes of EncryptOptions.Length ` ) ;
const next = async ( ) : Promise < Uint8Array | undefined > = > {
for ( ; ; ) {
const r = await reader . read ( ) ;
if ( r . done ) return undefined ;
if ( r . value . length > 0 ) return r . value ;
}
} ;
return new ReadableStream < Uint8Array > ( {
async pull ( controller ) {
try {
if ( read < length ) {
if ( pending . length === 0 ) {
const chunk = await next ( ) ;
if ( chunk === undefined ) throw new Error ( ` capsule: the source ended after ${ read } bytes, and EncryptOptions.Length is ${ length } ` ) ;
pending = chunk ;
}
const take = Math . min ( CHUNK , pending . length ) ;
if ( read + take > length ) throw more ( ) ;
controller . enqueue ( pending . subarray ( 0 , take ) ) ;
pending = pending . subarray ( take ) ;
read += take ;
return ;
}
if ( ! ended ) {
if ( pending . length > 0 || ( await next ( ) ) !== undefined ) throw more ( ) ;
const piece = await content . next ( ) ;
if ( piece !== undefined ) {
read += piece . length ;
controller . enqueue ( piece ) ;
return ;
}
ended = true ;
/* v8 ignore next -- @preserve: every content counts its bytes against L */
if ( read !== length ) throw new Error ( ` capsule: internal error: ${ read } bytes of content, L = ${ length } ` ) ;
}
if ( padding > 0 ) {
const zeros = Math . min ( CHUNK , padding ) ;
@ -553,15 +610,57 @@ function contentStream(src: EncryptSource, length: number, padded: number, state
} catch ( err ) {
state . error = err ;
controller . error ( err ) ;
await reader. cancel ( err ) . catch ( ( ) = > undefined ) ;
await content. cancel ( err ) ;
}
} ,
async cancel ( reason ) {
await reader. cancel ( reason ) . catch ( ( ) = > undefined ) ;
await content. cancel ( reason ) ;
} ,
} ) ;
}
// The content of format 2: exactly L bytes of the source, in pieces of at
// most 64 KiB. A source that ends before L, or delivers more than L, fails
// with the texts of writeContent of the Go reference; empty chunks count for
// nothing. The bytes are counted for every source, since the view of a
// resizable or transferred ArrayBuffer can shrink during an await.
function sourceContent ( src : EncryptSource , length : number ) : Content {
const reader = src instanceof Uint8Array ? arrayReader ( src ) : ( src instanceof Blob ? src . stream ( ) : src ) . getReader ( ) ;
let read = 0 ;
let pending : Uint8Array = new Uint8Array ( 0 ) ;
const more = ( ) : Error = > new Error ( ` capsule: the source delivers more than the ${ length } bytes of EncryptOptions.Length ` ) ;
const chunk = async ( ) : Promise < Uint8Array | undefined > = > {
for ( ; ; ) {
const r = await reader . read ( ) ;
if ( r . done ) return undefined ;
if ( r . value . length > 0 ) return r . value ;
}
} ;
return {
async next() {
if ( read < length ) {
if ( pending . length === 0 ) {
const c = await chunk ( ) ;
if ( c === undefined ) throw new Error ( ` capsule: the source ended after ${ read } bytes, and EncryptOptions.Length is ${ length } ` ) ;
pending = c ;
}
const take = Math . min ( CHUNK , pending . length ) ;
if ( read + take > length ) throw more ( ) ;
const piece = pending . subarray ( 0 , take ) ;
pending = pending . subarray ( take ) ;
read += take ;
return piece ;
}
// The end, which plaintextStream asks for once: nothing may follow L.
if ( pending . length > 0 || ( await chunk ( ) ) !== undefined ) throw more ( ) ;
return undefined ;
} ,
async cancel ( reason ) {
await reader . cancel ( reason ) . catch ( ( ) = > undefined ) ;
} ,
} ;
}
// A reader of a Uint8Array in pieces of 64 KiB.
function arrayReader ( b : Uint8Array ) : Pick < ReadableStreamDefaultReader < Uint8Array > , 'read' | 'cancel' > {
let at = 0 ;