66 ReadVaultResponse ,
77} from './api' ;
88import { generateRandomString } from './random' ;
9- import { sha512 } from './hash ' ;
9+ import { KEY_VERSION_2_PREFIX , deriveVerificationHash , parseKey } from './verification ' ;
1010import { encryptionRegistry , compressionRegistry , validateMetadata } from './encryption/registry' ;
1111import { ProcessingMetadata } from './vault' ;
1212import { gcm } from './encryption' ;
@@ -63,6 +63,25 @@ export class Client {
6363 return processed ;
6464 }
6565
66+ // Metadata for the inner (URL key) layer: carries compression + the primary
67+ // encryption algorithm.
68+ private keyLayerMetadata ( metadata : ProcessingMetadata ) : ProcessingMetadata {
69+ return {
70+ compression : metadata . compression ,
71+ encryption : { algorithm : metadata . encryption . algorithm } ,
72+ } ;
73+ }
74+
75+ // Metadata for the outer (user password) layer: encryption only, no
76+ // compression (compression happens once, on the inner layer).
77+ private passwordLayerMetadata ( metadata : ProcessingMetadata ) : ProcessingMetadata {
78+ return {
79+ encryption : {
80+ algorithm : metadata . encryption . passwordAlgorithm ?? metadata . encryption . algorithm ,
81+ } ,
82+ } ;
83+ }
84+
6685 private async recoverContent (
6786 encoded : string ,
6887 key : string ,
@@ -103,21 +122,45 @@ export class Client {
103122 async create (
104123 input : Omit < CreateVaultRequest , 'h' | 'm' > & { p ?: string ; m ?: ProcessingMetadata } ,
105124 ) : Promise < CreateVaultResponse & { key : string ; hash : string } > {
106- const key = await generateRandomString ( this . keyLength ) ;
125+ const rawKey = await generateRandomString ( this . keyLength ) ;
126+ const hasPassword = input . p !== undefined && input . p !== '' ;
107127
128+ // Fast KDF for the high-entropy URL key layer; memory-hard Argon2id only for
129+ // the user-password layer, and only when a password is actually set.
108130 const metadata : ProcessingMetadata = input . m ?? {
109131 compression : {
110132 algorithm : 'zlib:pako' ,
111133 } ,
112134 encryption : {
113135 algorithm : 'ml-kem-768-2' ,
136+ passwordAlgorithm : hasPassword ? 'ml-kem-768-argon2' : undefined ,
114137 } ,
115138 } ;
116139
117- const processed = await this . processContent ( input . c , metadata , key ) . then ( ( r ) =>
118- ! input . p ? r : this . processContent ( r , metadata , input . p ) ,
119- ) ;
120- const hash = sha512 ( key + ( input . p ?? '' ) ) ;
140+ let processed : string ;
141+ if ( ! hasPassword ) {
142+ processed = await this . processContent ( input . c , metadata , rawKey ) ;
143+ } else if ( metadata . encryption . passwordAlgorithm ) {
144+ // Asymmetric: compress + key-encrypt inner, then password-encrypt outer.
145+ const inner = await this . processContent ( input . c , this . keyLayerMetadata ( metadata ) , rawKey ) ;
146+ processed = await this . processContent (
147+ inner ,
148+ this . passwordLayerMetadata ( metadata ) ,
149+ input . p as string ,
150+ ) ;
151+ } else {
152+ // Legacy symmetric pipeline (custom metadata without passwordAlgorithm).
153+ const inner = await this . processContent ( input . c , metadata , rawKey ) ;
154+ processed = await this . processContent ( inner , metadata , input . p as string ) ;
155+ }
156+
157+ // Only password-protected secrets need the Argon2id (v2) verification hash;
158+ // a bare high-entropy key is not brute-forceable, so it keeps the fast hash.
159+ // The version prefix on the shared key tells the reader which scheme to use.
160+ const key = hasPassword ? `${ KEY_VERSION_2_PREFIX } ${ rawKey } ` : rawKey ;
161+ const hash = hasPassword
162+ ? await deriveVerificationHash ( 'v2' , rawKey , input . p )
163+ : await deriveVerificationHash ( 'legacy' , rawKey ) ;
121164
122165 const response = await fetch ( `${ this . apiUrl } /vault` , {
123166 method : 'POST' ,
@@ -156,7 +199,8 @@ export class Client {
156199 }
157200
158201 async read ( id : string , key : string , password ?: string ) {
159- const h = sha512 ( key + ( password ?? '' ) ) ;
202+ const { scheme, rawKey } = parseKey ( key ) ;
203+ const h = await deriveVerificationHash ( scheme , rawKey , password ) ;
160204 const res = await fetch ( `${ this . apiUrl } /vault/${ id } ?h=${ h } ` , {
161205 headers : this . getHeaders ( ) ,
162206 } ) ;
@@ -170,11 +214,18 @@ export class Client {
170214 }
171215
172216 const data = await ( res . json ( ) as Promise < ReadVaultResponse > ) ;
173- const decrypted = password
174- ? await this . recoverContent ( data . c , password , data . m ) . then ( ( d ) =>
175- this . recoverContent ( d , key , data . m ) ,
176- )
177- : await this . recoverContent ( data . c , key , data . m ) ;
217+ let decrypted : string ;
218+ if ( ! password ) {
219+ decrypted = await this . recoverContent ( data . c , rawKey , data . m ) ;
220+ } else if ( data . m ?. encryption ?. passwordAlgorithm ) {
221+ // Asymmetric: password-decrypt outer, then key-decrypt + decompress inner.
222+ const tmp = await this . recoverContent ( data . c , password , this . passwordLayerMetadata ( data . m ) ) ;
223+ decrypted = await this . recoverContent ( tmp , rawKey , this . keyLayerMetadata ( data . m ) ) ;
224+ } else {
225+ // Legacy symmetric pipeline.
226+ const tmp = await this . recoverContent ( data . c , password , data . m ) ;
227+ decrypted = await this . recoverContent ( tmp , rawKey , data . m ) ;
228+ }
178229
179230 return {
180231 c : decrypted ,
0 commit comments