@@ -73,27 +73,28 @@ describe('cryptoUtils (RSA-OAEP) – real Web Crypto', () => {
7373// AES-GCM round-trip tests
7474// ---------------------------------------------------------------------------
7575describe ( 'AesGcmEncryption (AES-GCM) – real Web Crypto' , ( ) => {
76- it ( 'int () generates a CryptoKey and returns it on subsequent calls' , async ( ) => {
76+ it ( 'init () generates the key and is idempotent on subsequent calls' , async ( ) => {
7777 const aes = new AesGcmEncryption ( ) ;
78- const key1 = await aes . int ( ) ;
79- const key2 = await aes . int ( ) ;
78+ await aes . init ( ) ;
79+ await aes . init ( ) ; // Should not throw or regenerate
8080
81- expect ( key1 ) . toBeDefined ( ) ;
82- // Second call should return the same cached instance
83- expect ( key1 ) . toBe ( key2 ) ;
81+ // Verify the key exists and is exportable
82+ const exported = await aes . exportKey ( ) ;
83+ expect ( typeof exported ) . toBe ( 'string' ) ;
84+ expect ( exported . length ) . toBeGreaterThan ( 0 ) ;
8485 } ) ;
8586
8687 it ( 'encryptData() + decryptData() recover the original data' , async ( ) => {
8788 const aes = new AesGcmEncryption ( ) ;
8889
8990 // Initialise the local key (used for encryption)
90- await aes . int ( ) ;
91+ await aes . init ( ) ;
9192
9293 // Export the local key and re-import it as the "remote" key so that
9394 // decryptData() (which uses aesKeyRemote) can decrypt what encryptData()
9495 // produced.
95- const exportedKey = await aes . getRawAesKeyToExport ( ) ;
96- await aes . setRemoteAesKey ( exportedKey ) ;
96+ const exportedKey = await aes . exportKey ( ) ;
97+ await aes . importRemoteKey ( exportedKey ) ;
9798
9899 const originalText = 'AES test payload 🔒' ;
99100 const originalData = new TextEncoder ( ) . encode ( originalText ) . buffer ;
@@ -112,13 +113,13 @@ describe('AesGcmEncryption (AES-GCM) – real Web Crypto', () => {
112113
113114 it ( 'decryptData() throws when remote key has not been set' , async ( ) => {
114115 const aes = new AesGcmEncryption ( ) ;
115- await aes . int ( ) ;
116+ await aes . init ( ) ;
116117
117118 const { encryptedData, iv } = await aes . encryptData (
118119 new TextEncoder ( ) . encode ( 'data' ) . buffer
119120 ) ;
120121
121- // No setRemoteAesKey () call → should throw
122+ // No importRemoteKey () call → should throw
122123 await expect ( aes . decryptData ( encryptedData , iv ) ) . rejects . toThrow (
123124 'Remote AES key not set.'
124125 ) ;
@@ -135,10 +136,10 @@ describe('AES key exchange via RSA-encrypted channel', () => {
135136
136137 // Simulate Alice (sender): generate an AES key
137138 const aliceAes = new AesGcmEncryption ( ) ;
138- await aliceAes . int ( ) ;
139+ await aliceAes . init ( ) ;
139140
140- // Alice exports her AES key as a JWK string (this is what getRawAesKeyToExport returns)
141- const aesKeyJwk = await aliceAes . getRawAesKeyToExport ( ) ;
141+ // Alice exports her AES key as a JWK string
142+ const aesKeyJwk = await aliceAes . exportKey ( ) ;
142143
143144 // Alice encrypts the AES key with Bob's RSA public key before sending it to the server
144145 const encryptedAesKey = await cryptoUtils . encryptMessage ( aesKeyJwk , bobPublicKey ) ;
@@ -151,7 +152,7 @@ describe('AES key exchange via RSA-encrypted channel', () => {
151152
152153 // Bob sets the decrypted AES key as his remote key
153154 const bobAes = new AesGcmEncryption ( ) ;
154- await bobAes . setRemoteAesKey ( decryptedAesKeyJwk ) ;
155+ await bobAes . importRemoteKey ( decryptedAesKeyJwk ) ;
155156
156157 // Alice encrypts some data with her local AES key
157158 const originalText = 'Secret message over AES-GCM 🔐' ;
@@ -171,8 +172,8 @@ describe('AES key exchange via RSA-encrypted channel', () => {
171172 const { privateKey : evePrivateKey } = await cryptoUtils . generateKeypairs ( ) ; // attacker's key
172173
173174 const aliceAes = new AesGcmEncryption ( ) ;
174- await aliceAes . int ( ) ;
175- const aesKeyJwk = await aliceAes . getRawAesKeyToExport ( ) ;
175+ await aliceAes . init ( ) ;
176+ const aesKeyJwk = await aliceAes . exportKey ( ) ;
176177
177178 // Alice encrypts AES key with Bob's public key
178179 const encryptedAesKey = await cryptoUtils . encryptMessage ( aesKeyJwk , bobPublicKey ) ;
@@ -183,3 +184,157 @@ describe('AES key exchange via RSA-encrypted channel', () => {
183184 ) . rejects . toThrow ( ) ;
184185 } ) ;
185186} ) ;
187+
188+ // ---------------------------------------------------------------------------
189+ // EncryptionFactory
190+ // ---------------------------------------------------------------------------
191+ describe ( 'EncryptionFactory' , ( ) => {
192+ // Import inside the describe so the window polyfill above is already set up
193+ const { EncryptionFactory } = require ( './encryptionFactory' ) ;
194+ const { AesGcmEncryption } = require ( './cryptoAES' ) ;
195+
196+ it ( 'create() with no args returns an object with symmetric and asymmetric strategies' , ( ) => {
197+ const strategy = EncryptionFactory . create ( ) ;
198+ expect ( strategy . symmetric ) . toBeDefined ( ) ;
199+ expect ( strategy . asymmetric ) . toBeDefined ( ) ;
200+ } ) ;
201+
202+ it ( 'create() returns a fresh symmetric instance on each call (stateful key material)' , ( ) => {
203+ const a = EncryptionFactory . create ( ) ;
204+ const b = EncryptionFactory . create ( ) ;
205+ // Symmetric strategy holds key state — must be a distinct instance each time
206+ expect ( a . symmetric ) . not . toBe ( b . symmetric ) ;
207+ } ) ;
208+
209+ it ( 'create({ symmetric: "AES-GCM" }) returns an AesGcmEncryption instance' , ( ) => {
210+ const { symmetric } = EncryptionFactory . create ( { symmetric : 'AES-GCM' } ) ;
211+ expect ( symmetric ) . toBeInstanceOf ( AesGcmEncryption ) ;
212+ } ) ;
213+
214+ it ( 'create() with an unknown symmetric name throws a descriptive error' , ( ) => {
215+ expect ( ( ) => EncryptionFactory . create ( { symmetric : 'UNKNOWN' } ) ) . toThrow (
216+ 'Unknown symmetric strategy: "UNKNOWN"'
217+ ) ;
218+ } ) ;
219+
220+ it ( 'create() with an unknown asymmetric name throws a descriptive error' , ( ) => {
221+ expect ( ( ) => EncryptionFactory . create ( { asymmetric : 'UNKNOWN' } ) ) . toThrow (
222+ 'Unknown asymmetric strategy: "UNKNOWN"'
223+ ) ;
224+ } ) ;
225+
226+ it ( 'registerSymmetric() makes a custom strategy available by name' , async ( ) => {
227+ const mockSymmetric = new AesGcmEncryption ( ) ;
228+ EncryptionFactory . registerSymmetric ( 'MOCK-SYM' , ( ) => mockSymmetric ) ;
229+
230+ const { symmetric } = EncryptionFactory . create ( { symmetric : 'MOCK-SYM' } ) ;
231+ expect ( symmetric ) . toBe ( mockSymmetric ) ;
232+ } ) ;
233+
234+ it ( 'registerAsymmetric() makes a custom strategy available by name' , ( ) => {
235+ const mockAsymmetric = { generateKeypairs : jest . fn ( ) , encryptMessage : jest . fn ( ) , decryptMessage : jest . fn ( ) } ;
236+ EncryptionFactory . registerAsymmetric ( 'MOCK-ASM' , ( ) => mockAsymmetric ) ;
237+
238+ const { asymmetric } = EncryptionFactory . create ( { asymmetric : 'MOCK-ASM' } ) ;
239+ expect ( asymmetric ) . toBe ( mockAsymmetric ) ;
240+ } ) ;
241+
242+ it ( 'registerSymmetric() supports chaining' , ( ) => {
243+ const result = EncryptionFactory . registerSymmetric ( 'CHAIN-TEST' , ( ) => new AesGcmEncryption ( ) ) ;
244+ expect ( result ) . toBe ( EncryptionFactory ) ;
245+ } ) ;
246+ } ) ;
247+
248+ // ---------------------------------------------------------------------------
249+ // Pluggable encryption – integration
250+ // ---------------------------------------------------------------------------
251+ describe ( 'Pluggable encryption (integration)' , ( ) => {
252+ const { EncryptionFactory } = require ( './encryptionFactory' ) ;
253+ const { AesGcmEncryption } = require ( './cryptoAES' ) ;
254+ const { cryptoUtils } = require ( './cryptoRSA' ) ;
255+
256+ /**
257+ * Thin tracking wrapper: delegates every call to a real AesGcmEncryption
258+ * while recording which methods were invoked.
259+ */
260+ class TrackingSymmetric {
261+ readonly calls : string [ ] = [ ] ;
262+ private inner = new AesGcmEncryption ( ) ;
263+
264+ async init ( ) { this . calls . push ( 'init' ) ; return this . inner . init ( ) ; }
265+ async encryptData ( data : ArrayBuffer ) { this . calls . push ( 'encryptData' ) ; return this . inner . encryptData ( data ) ; }
266+ async decryptData ( data : BufferSource , iv : BufferSource ) { this . calls . push ( 'decryptData' ) ; return this . inner . decryptData ( data , iv ) ; }
267+ async exportKey ( ) { this . calls . push ( 'exportKey' ) ; return this . inner . exportKey ( ) ; }
268+ async importRemoteKey ( key : string ) { this . calls . push ( 'importRemoteKey' ) ; return this . inner . importRemoteKey ( key ) ; }
269+ }
270+
271+ it ( 'custom symmetric strategy is called through the factory' , async ( ) => {
272+ const impl = new TrackingSymmetric ( ) ;
273+ EncryptionFactory . registerSymmetric ( 'TRACKING-SYM' , ( ) => impl ) ;
274+
275+ const { symmetric } = EncryptionFactory . create ( { symmetric : 'TRACKING-SYM' } ) ;
276+
277+ await symmetric . init ( ) ;
278+ const key = await symmetric . exportKey ( ) ;
279+ await symmetric . importRemoteKey ( key ) ;
280+ const { encryptedData, iv } = await symmetric . encryptData ( new TextEncoder ( ) . encode ( 'test' ) . buffer ) ;
281+ await symmetric . decryptData ( encryptedData , iv ) ;
282+
283+ expect ( impl . calls ) . toEqual ( [ 'init' , 'exportKey' , 'importRemoteKey' , 'encryptData' , 'decryptData' ] ) ;
284+ } ) ;
285+
286+ it ( 'custom asymmetric strategy is called through the factory' , async ( ) => {
287+ const calls : string [ ] = [ ] ;
288+ const impl = {
289+ generateKeypairs : async ( ) => { calls . push ( 'generateKeypairs' ) ; return cryptoUtils . generateKeypairs ( ) ; } ,
290+ encryptMessage : async ( p : string , k : string ) => { calls . push ( 'encryptMessage' ) ; return cryptoUtils . encryptMessage ( p , k ) ; } ,
291+ decryptMessage : async ( c : string , k : string ) => { calls . push ( 'decryptMessage' ) ; return cryptoUtils . decryptMessage ( c , k ) ; } ,
292+ } ;
293+ EncryptionFactory . registerAsymmetric ( 'TRACKING-ASM' , ( ) => impl ) ;
294+
295+ const { asymmetric } = EncryptionFactory . create ( { asymmetric : 'TRACKING-ASM' } ) ;
296+
297+ const { publicKey, privateKey } = await asymmetric . generateKeypairs ( ) ;
298+ const ciphertext = await asymmetric . encryptMessage ( 'hello' , publicKey ) ;
299+ const recovered = await asymmetric . decryptMessage ( ciphertext , privateKey ) ;
300+
301+ expect ( recovered ) . toBe ( 'hello' ) ;
302+ expect ( calls ) . toEqual ( [ 'generateKeypairs' , 'encryptMessage' , 'decryptMessage' ] ) ;
303+ } ) ;
304+
305+ it ( 'full SDK handshake protocol works end-to-end with factory-created strategies' , async ( ) => {
306+ // Mirrors exactly what sdk.ts does internally during setChannel()
307+ const aliceStrategy = EncryptionFactory . create ( ) ;
308+ const bobStrategy = EncryptionFactory . create ( ) ;
309+
310+ // Both peers initialise their symmetric keys
311+ await aliceStrategy . symmetric . init ( ) ;
312+ await bobStrategy . symmetric . init ( ) ;
313+
314+ // Both peers generate asymmetric key pairs
315+ const { publicKey : alicePub , privateKey : alicePriv } = await aliceStrategy . asymmetric . generateKeypairs ( ) ;
316+ const { publicKey : bobPub , privateKey : bobPriv } = await bobStrategy . asymmetric . generateKeypairs ( ) ;
317+
318+ // Alice wraps her symmetric key with Bob's public key and "sends" it
319+ const aliceExportedKey = await aliceStrategy . symmetric . exportKey ( ) ;
320+ const wrappedKey = await aliceStrategy . asymmetric . encryptMessage ( aliceExportedKey , bobPub ) ;
321+
322+ // Bob unwraps it with his private key and imports it as the remote key
323+ const unwrappedKey = await bobStrategy . asymmetric . decryptMessage ( wrappedKey , bobPriv ) ;
324+ await bobStrategy . symmetric . importRemoteKey ( unwrappedKey ) ;
325+
326+ // Alice encrypts a message; Bob decrypts it
327+ const plaintext = 'end-to-end via pluggable factory' ;
328+ const { encryptedData, iv } = await aliceStrategy . symmetric . encryptData (
329+ new TextEncoder ( ) . encode ( plaintext ) . buffer
330+ ) ;
331+ const decrypted = await bobStrategy . symmetric . decryptData ( encryptedData , iv ) ;
332+
333+ expect ( new TextDecoder ( ) . decode ( decrypted ) ) . toBe ( plaintext ) ;
334+
335+ // Confirm the asymmetric keys are independent (no cross-contamination)
336+ await expect (
337+ aliceStrategy . asymmetric . decryptMessage ( wrappedKey , alicePriv )
338+ ) . rejects . toThrow ( ) ;
339+ } ) ;
340+ } ) ;
0 commit comments