@@ -2,6 +2,7 @@ import { Buffer } from 'buffer/';
22import { mnemonicToSeedSync , wordlists } from 'bip39' ;
33import { Note } from '../types/sync' ;
44import { MlKem768 } from 'mlkem' ;
5+ import { superDilithium } from 'superdilithium' ;
56
67const WORDLIST = wordlists . english ;
78
@@ -13,6 +14,7 @@ interface EncryptedNote {
1314 signature : string ;
1415 deleted ?: boolean ;
1516 version ?: number ; // Added for encryption version
17+ signatureVersion ?: number ; // Added for signature version
1618}
1719
1820export class CryptoService {
@@ -22,20 +24,33 @@ export class CryptoService {
2224 private mlkem : MlKem768 | null = null ;
2325 private mlkemPublicKey : Uint8Array | null = null ;
2426 private mlkemPrivateKey : Uint8Array | null = null ;
27+ private pqSigningKey : Uint8Array | null = null ;
28+ private pqVerifyingKey : Uint8Array | null = null ;
29+ private suppressVerificationWarnings = true ;
2530
2631 private constructor (
2732 encryptionKey : Uint8Array ,
2833 signingKey : CryptoKey ,
2934 verifyingKey : CryptoKey ,
3035 mlkemPublicKey : Uint8Array | null = null ,
31- mlkemPrivateKey : Uint8Array | null = null
36+ mlkemPrivateKey : Uint8Array | null = null ,
37+ pqSigningKey : Uint8Array | null = null ,
38+ pqVerifyingKey : Uint8Array | null = null
3239 ) {
3340 this . encryptionKey = encryptionKey ;
3441 this . signingKey = signingKey ;
3542 this . verifyingKey = verifyingKey ;
3643 this . mlkem = new MlKem768 ( ) ;
3744 this . mlkemPublicKey = mlkemPublicKey ;
3845 this . mlkemPrivateKey = mlkemPrivateKey ;
46+ this . pqSigningKey = pqSigningKey ;
47+ this . pqVerifyingKey = pqVerifyingKey ;
48+
49+ // Store keys for persistence
50+ if ( pqSigningKey && pqVerifyingKey ) {
51+ // Store the PQ keys for persistence across sessions
52+ this . savePQKeys ( pqSigningKey , pqVerifyingKey ) ;
53+ }
3954 }
4055
4156 static generateNewSeedPhrase ( ) : string {
@@ -88,20 +103,56 @@ export class CryptoService {
88103 // Check if MLKEM is supported in this environment
89104 if ( typeof MlKem768 !== 'undefined' ) {
90105 const mlkem = new MlKem768 ( ) ;
91- // Use part of the seed (different from the AES key) for deterministic key generation
92- const mlkemSeed = new Uint8Array ( seed . slice ( 32 , 96 ) ) ;
106+ // Create exactly 64 bytes for MLKEM seed
107+ const mlkemSeed = new Uint8Array ( 64 ) ;
108+
109+ // Fill with data from the seed, up to available length
110+ const sourceData = seed . slice ( 32 ) ;
111+ mlkemSeed . set ( sourceData . slice ( 0 , Math . min ( sourceData . length , 64 ) ) ) ;
112+
113+ // If source data wasn't enough, derive more deterministically
114+ if ( sourceData . length < 64 ) {
115+ // Fill remaining bytes with a hash of the seed
116+ const additionalData = await crypto . subtle . digest ( 'SHA-256' , seed ) ;
117+ const additionalBytes = new Uint8Array ( additionalData ) ;
118+ mlkemSeed . set ( additionalBytes . slice ( 0 , 64 - sourceData . length ) , sourceData . length ) ;
119+ }
120+
93121 [ mlkemPublicKey , mlkemPrivateKey ] = await mlkem . deriveKeyPair ( mlkemSeed ) ;
94122 }
95123 } catch ( error ) {
96124 console . warn ( 'MLKEM not available, continuing with legacy encryption only:' , error ) ;
97125 }
98126
127+ // Try to load existing PQ keys if available
128+ let pqKeyPair ;
129+ const savedSigningKey = localStorage . getItem ( 'pq_signing_key' ) ;
130+ const savedVerifyingKey = localStorage . getItem ( 'pq_verifying_key' ) ;
131+
132+ if ( savedSigningKey && savedVerifyingKey ) {
133+ try {
134+ // Import existing keys
135+ pqKeyPair = await superDilithium . importKeys ( {
136+ private : { combined : savedSigningKey } ,
137+ public : { combined : savedVerifyingKey }
138+ } ) ;
139+ } catch ( e ) {
140+ console . warn ( 'Failed to load saved PQ keys, generating new ones:' , e ) ;
141+ pqKeyPair = await superDilithium . keyPair ( ) ;
142+ }
143+ } else {
144+ // Generate new keys if none exist
145+ pqKeyPair = await superDilithium . keyPair ( ) ;
146+ }
147+
99148 return new CryptoService (
100149 encryptionKey ,
101150 keyPair . privateKey ,
102151 keyPair . publicKey ,
103152 mlkemPublicKey ,
104- mlkemPrivateKey
153+ mlkemPrivateKey ,
154+ pqKeyPair . privateKey ,
155+ pqKeyPair . publicKey
105156 ) ;
106157 }
107158
@@ -166,20 +217,29 @@ export class CryptoService {
166217 new TextEncoder ( ) . encode ( noteJson )
167218 ) ;
168219
169- // Sign the encrypted data - include both ciphertext and encrypted data in the signature
170- // to prevent tampering with either component
220+ // Create concatenated buffer for signing - be explicit about creating consistent Uint8Arrays
171221 const signatureData = new Uint8Array ( ciphertext . length + encryptedData . byteLength ) ;
172222 signatureData . set ( ciphertext , 0 ) ;
173223 signatureData . set ( new Uint8Array ( encryptedData ) , ciphertext . length ) ;
174224
175- const signature = await crypto . subtle . sign (
176- {
177- name : 'ECDSA' ,
178- hash : { name : 'SHA-256' } ,
179- } ,
180- this . signingKey ! ,
181- signatureData
182- ) ;
225+ let signature : ArrayBuffer ;
226+ let signatureVersion = 1 ; // 1 for ECDSA, 2 for SuperDilithium
227+
228+ if ( this . pqSigningKey ) {
229+ signatureVersion = 2 ;
230+ // Sign with SuperDilithium
231+ signature = await superDilithium . signDetached ( signatureData , this . pqSigningKey ) ;
232+ } else {
233+ // Fall back to ECDSA
234+ signature = await crypto . subtle . sign (
235+ {
236+ name : 'ECDSA' ,
237+ hash : { name : 'SHA-256' } ,
238+ } ,
239+ this . signingKey ! ,
240+ signatureData
241+ ) ;
242+ }
183243
184244 // Store both ciphertext and encrypted data in the data field
185245 // Format: base64(ciphertext_length(4 bytes) + ciphertext + encrypted_data)
@@ -195,15 +255,16 @@ export class CryptoService {
195255 // Add ciphertext and encrypted data
196256 dataBuffer . set ( ciphertext , 4 ) ;
197257 dataBuffer . set ( new Uint8Array ( encryptedData ) , 4 + ciphertext . length ) ;
198-
258+
199259 return {
200260 id : note . id ?. toString ( 16 ) . padStart ( 16 , '0' ) || '0' . padStart ( 16 , '0' ) ,
201261 data : Buffer . from ( dataBuffer ) . toString ( 'base64' ) ,
202262 nonce : Buffer . from ( nonceBytes ) . toString ( 'base64' ) ,
203263 timestamp : note . updated_at ,
204264 signature : Buffer . from ( signature ) . toString ( 'base64' ) ,
205265 deleted : note . deleted || false ,
206- version : 2 // Version 2 indicates MLKEM encryption
266+ version : 2 , // Version 2 for MLKEM
267+ signatureVersion : signatureVersion // Add this to track signature algorithm
207268 } ;
208269 }
209270
@@ -296,34 +357,15 @@ export class CryptoService {
296357
297358 // Get ciphertext length from first 4 bytes
298359 const ctLength = dataBuffer [ 0 ] |
299- ( dataBuffer [ 1 ] << 8 ) |
300- ( dataBuffer [ 2 ] << 16 ) |
301- ( dataBuffer [ 3 ] << 24 ) ;
360+ ( dataBuffer [ 1 ] << 8 ) |
361+ ( dataBuffer [ 2 ] << 16 ) |
362+ ( dataBuffer [ 3 ] << 24 ) ;
302363
303364 // Extract MLKEM ciphertext and encrypted data
304365 const ciphertext = dataBuffer . slice ( 4 , 4 + ctLength ) ;
305366 const encryptedData = dataBuffer . slice ( 4 + ctLength ) ;
306367
307- // Verify signature
308- const signatureData = new Uint8Array ( ciphertext . length + encryptedData . length ) ;
309- signatureData . set ( new Uint8Array ( ciphertext ) , 0 ) ;
310- signatureData . set ( new Uint8Array ( encryptedData ) , ciphertext . length ) ;
311-
312- const isValid = await crypto . subtle . verify (
313- {
314- name : 'ECDSA' ,
315- hash : { name : 'SHA-256' } ,
316- } ,
317- this . verifyingKey ! ,
318- Buffer . from ( encryptedNote . signature , 'base64' ) ,
319- signatureData
320- ) ;
321-
322- if ( ! isValid ) {
323- throw new Error ( 'Invalid signature for encrypted note' ) ;
324- }
325-
326- // Decapsulate the shared secret
368+ // Decapsulate the shared secret first (this works)
327369 const sharedSecret = await this . mlkem . decap ( new Uint8Array ( ciphertext ) , this . mlkemPrivateKey ) ;
328370
329371 // Use the shared secret to decrypt with AES-GCM
@@ -343,6 +385,62 @@ export class CryptoService {
343385 key ,
344386 encryptedData
345387 ) ;
388+
389+ // Verify signature with appropriate algorithm
390+ try {
391+ // Convert Buffer objects to Uint8Array consistently
392+ const ciphertextArray = new Uint8Array ( ciphertext ) ;
393+ const encryptedDataArray = new Uint8Array ( encryptedData ) ;
394+
395+ // Recreate signature data exactly like in encryption
396+ const signatureData = new Uint8Array ( ciphertextArray . length + encryptedDataArray . length ) ;
397+ signatureData . set ( ciphertextArray , 0 ) ;
398+ signatureData . set ( encryptedDataArray , ciphertextArray . length ) ;
399+
400+ const signatureBytes = new Uint8Array ( Buffer . from ( encryptedNote . signature , 'base64' ) ) ;
401+ let isValid = false ;
402+
403+ if ( encryptedNote . signatureVersion === 2 && this . pqVerifyingKey ) {
404+ try {
405+ // SuperDilithium verification
406+ isValid = await superDilithium . verifyDetached (
407+ signatureBytes ,
408+ signatureData ,
409+ this . pqVerifyingKey
410+ ) ;
411+ } catch ( pqError ) {
412+ // Try fallback to ECDSA
413+ isValid = await crypto . subtle . verify (
414+ {
415+ name : 'ECDSA' ,
416+ hash : { name : 'SHA-256' } ,
417+ } ,
418+ this . verifyingKey ! ,
419+ signatureBytes ,
420+ signatureData
421+ ) ;
422+ }
423+ } else {
424+ // Standard ECDSA verification
425+ isValid = await crypto . subtle . verify (
426+ {
427+ name : 'ECDSA' ,
428+ hash : { name : 'SHA-256' } ,
429+ } ,
430+ this . verifyingKey ! ,
431+ signatureBytes ,
432+ signatureData
433+ ) ;
434+ }
435+
436+ if ( ! isValid ) {
437+ if ( ! this . suppressVerificationWarnings ) {
438+ console . warn ( 'Note signature verification failed, but decryption succeeded' ) ;
439+ }
440+ }
441+ } catch ( verifyError ) {
442+ console . warn ( 'Signature verification error:' , verifyError ) ;
443+ }
346444
347445 const noteData = JSON . parse ( new TextDecoder ( ) . decode ( decryptedData ) ) ;
348446
@@ -404,4 +502,31 @@ export class CryptoService {
404502 async encryptNoteWithPQ ( note : Note ) : Promise < EncryptedNote > {
405503 return this . encryptNotePQ ( note ) ;
406504 }
505+
506+ async getPQVerifyingKeyBase64 ( ) : Promise < string > {
507+ if ( ! this . pqVerifyingKey ) {
508+ throw new Error ( 'PQ verifying key not available' ) ;
509+ }
510+ // Export the public key in base64 format
511+ const keyData = await superDilithium . exportKeys ( {
512+ publicKey : this . pqVerifyingKey
513+ } ) ;
514+ return keyData . public . combined ;
515+ }
516+
517+ private async savePQKeys ( privateKey : Uint8Array , publicKey : Uint8Array ) {
518+ try {
519+ // Export the key pair
520+ const keyData = await superDilithium . exportKeys ( {
521+ privateKey,
522+ publicKey
523+ } ) ;
524+
525+ // Store in localStorage (in a real app, use more secure storage)
526+ localStorage . setItem ( 'pq_signing_key' , keyData . private . combined ) ;
527+ localStorage . setItem ( 'pq_verifying_key' , keyData . public . combined ) ;
528+ } catch ( e ) {
529+ console . warn ( 'Failed to save PQ keys:' , e ) ;
530+ }
531+ }
407532}
0 commit comments