1- import { cryptoUtils } from "./cryptoRSA" ;
2-
3- describe ( 'cryptoUtils' , ( ) => {
4- const mockBase64String = 'ZW5jcnlwdGVkLXRleHQ=' ; // decoded = encrypted-text
5- const subtle = {
6- // Generates a new key (for symmetric algorithms) or key pair (for public-key algorithms).
7- // Returns a promise that fulfills with a CryptoKey (for symmetric algorithms) or a CryptoKeyPair (for public-key algorithms).
8- generateKey : jest . fn ( ) . mockResolvedValue ( {
9- publicKey : 'public-key' ,
10- privateKey : 'private-key' ,
11- } ) ,
12- // Takes as input a CryptoKey object and gives you the key in an external, portable format.
13- // Returns a promise - If format was jwk, then the promise fulfills with a JSON object containing the key.
14- exportKey : jest . fn ( ) . mockImplementation ( ( type , str ) => str ) ,
15- // Takes as input a key in an external, portable format and gives you a CryptoKey object that you can use in the Web Crypto API.
16- // Returns a promise that fulfills with the imported key as a CryptoKey object.
17- importKey : jest . fn ( ) . mockImplementation ( ( type , str ) => str ) ,
18- // It takes as its arguments a key to encrypt with, some algorithm-specific parameters, and the data to encrypt (also known as "plaintext").
19- // Returns a promise that fulfills with an ArrayBuffer containing the "ciphertext".
20- encrypt : jest . fn ( ) . mockImplementation ( ( algorithm , publicKey , encoded ) => encoded ) ,
21- decrypt : jest . fn ( ) . mockImplementation ( ( algorithm , privateKey , ciphertext ) => ciphertext )
22- } ;
23-
24- // Receives a Uint8Array to encode to base-64
25- // Returns ASCII string containing the Base64 representation of stringToEncode.
26- const btoa = jest . fn ( ) . mockImplementation ( function ( str ) {
27- return mockBase64String ;
28- } ) ;
29- // Receives a base-64 encoded string to decode to Uint8Array
30- const atob = jest . fn ( ) . mockImplementation ( function ( str ) {
31- return "This is another message" ;
32- } ) ;
33-
34- let window = { } as any ;
35- window . crypto = {
36- subtle : subtle
37- } as any ;
38-
39- window . btoa = btoa ;
40- window . atob = atob ;
41-
42- beforeEach ( ( ) => {
43- global . window = window ;
44- jest . clearAllMocks ( ) ;
45- } ) ;
46-
47- describe ( 'generateKeypairs' , ( ) => {
48- it ( 'should generate an object with a private and a public key' , async ( ) => {
49- const keyPair = await cryptoUtils . generateKeypairs ( ) ;
50-
51- expect ( window . crypto . subtle . exportKey ) . toHaveBeenCalledWith ( 'jwk' , expect . any ( String ) ) ;
52- expect ( keyPair . privateKey ) . toBe ( JSON . stringify ( 'private-key' ) ) ;
53- expect ( keyPair . publicKey ) . toBe ( JSON . stringify ( 'public-key' ) ) ;
1+ /**
2+ * Crypto tests using the real Web Crypto API.
3+ *
4+ * Node 19+ exposes `globalThis.crypto.subtle` natively.
5+ * The production code accesses crypto via `window.crypto`, so we alias
6+ * `globalThis.window` to `globalThis` once before all tests so that
7+ * `window.crypto`, `window.btoa`, and `window.atob` resolve correctly
8+ * without any mocking.
9+ */
10+
11+ import { webcrypto } from 'crypto' ;
12+
13+ // Polyfill for Node versions < 19 that do not expose globalThis.crypto
14+ if ( ! globalThis . crypto ) {
15+ ( globalThis as any ) . crypto = webcrypto ;
16+ }
17+
18+ // cryptoRSA.ts accesses `window.crypto`, `window.btoa`, and `window.atob`.
19+ // In a Node (non-jsdom) environment `window` is undefined, so we point it at
20+ // globalThis which already has btoa/atob (Node 16+) and crypto (Node 19+).
21+ if ( typeof window === 'undefined' ) {
22+ ( globalThis as any ) . window = globalThis ;
23+ }
24+
25+ import { cryptoUtils } from './cryptoRSA' ;
26+ import { AesGcmEncryption } from './cryptoAES' ;
27+
28+ // ---------------------------------------------------------------------------
29+ // RSA round-trip tests
30+ // ---------------------------------------------------------------------------
31+ describe ( 'cryptoUtils (RSA-OAEP) – real Web Crypto' , ( ) => {
32+ it ( 'generateKeypairs() returns non-empty serialised public and private keys' , async ( ) => {
33+ const keyPair = await cryptoUtils . generateKeypairs ( ) ;
34+
35+ expect ( typeof keyPair . publicKey ) . toBe ( 'string' ) ;
36+ expect ( typeof keyPair . privateKey ) . toBe ( 'string' ) ;
37+ expect ( keyPair . publicKey . length ) . toBeGreaterThan ( 0 ) ;
38+ expect ( keyPair . privateKey . length ) . toBeGreaterThan ( 0 ) ;
39+
40+ // Keys should be valid JSON (JWK format wrapped in JSON.stringify)
41+ expect ( ( ) => JSON . parse ( keyPair . publicKey ) ) . not . toThrow ( ) ;
42+ expect ( ( ) => JSON . parse ( keyPair . privateKey ) ) . not . toThrow ( ) ;
5443 } ) ;
55- } ) ;
5644
57- describe ( 'encryptMessage' , ( ) => {
58- it ( 'should encrypt plaintext using a public key and return a base64-encoded string' , async ( ) => {
59- const plaintext = 'This is a message' ;
60- const { publicKey } = await cryptoUtils . generateKeypairs ( ) ;
61- const ciphertext = await cryptoUtils . encryptMessage ( plaintext , publicKey ) ;
45+ it ( 'encryptMessage() + decryptMessage() recover the original plaintext' , async ( ) => {
46+ const plaintext = 'Hello, end-to-end encryption!' ;
47+
48+ const { publicKey, privateKey } = await cryptoUtils . generateKeypairs ( ) ;
6249
63- expect ( window . crypto . subtle . importKey ) . toHaveBeenCalledWith ( 'jwk' , expect . any ( String ) , { name : 'RSA-OAEP' , hash : 'SHA-256' } , true , [ 'encrypt' ] ) ;
64- expect ( window . btoa ) . toHaveBeenCalledWith ( expect . any ( String ) ) ;
65- expect ( ciphertext ) . toBe ( 'ZW5jcnlwdGVkLXRleHQ=' ) ;
50+ const ciphertext = await cryptoUtils . encryptMessage ( plaintext , publicKey ) ;
51+ expect ( typeof ciphertext ) . toBe ( 'string' ) ;
52+ expect ( ciphertext ) . not . toBe ( plaintext ) ;
53+
54+ const recovered = await cryptoUtils . decryptMessage ( ciphertext , privateKey ) ;
55+ expect ( recovered ) . toBe ( plaintext ) ;
6656 } ) ;
67- } ) ;
68-
69- describe ( 'decryptMessage' , ( ) => {
70- it ( 'should decrypt a ciphertext using a private key' , async ( ) => {
71- const plaintext = 'This is another message' ;
72- const keyPair = await cryptoUtils . generateKeypairs ( ) ;
73- const ciphertext = await cryptoUtils . encryptMessage ( plaintext , keyPair . publicKey ) ;
74- const decryptedText = await cryptoUtils . decryptMessage ( ciphertext , keyPair . privateKey ) ;
75-
76- expect ( window . crypto . subtle . importKey ) . toHaveBeenCalledWith ( 'jwk' , expect . any ( String ) , { name : 'RSA-OAEP' , hash : 'SHA-256' } , true , [ 'encrypt' ] ) ;
77- expect ( window . atob ) . toHaveBeenCalledWith ( expect . any ( String ) ) ;
78- expect ( decryptedText ) . toBe ( "This is another message" ) ;
57+
58+ it ( 'decryptMessage() fails when using the wrong private key' , async ( ) => {
59+ const plaintext = 'Secret message' ;
60+
61+ const { publicKey } = await cryptoUtils . generateKeypairs ( ) ;
62+ const { privateKey : wrongPrivateKey } = await cryptoUtils . generateKeypairs ( ) ;
63+
64+ const ciphertext = await cryptoUtils . encryptMessage ( plaintext , publicKey ) ;
65+
66+ await expect (
67+ cryptoUtils . decryptMessage ( ciphertext , wrongPrivateKey )
68+ ) . rejects . toThrow ( ) ;
69+ } ) ;
70+ } ) ;
71+
72+ // ---------------------------------------------------------------------------
73+ // AES-GCM round-trip tests
74+ // ---------------------------------------------------------------------------
75+ describe ( 'AesGcmEncryption (AES-GCM) – real Web Crypto' , ( ) => {
76+ it ( 'int() generates a CryptoKey and returns it on subsequent calls' , async ( ) => {
77+ const aes = new AesGcmEncryption ( ) ;
78+ const key1 = await aes . int ( ) ;
79+ const key2 = await aes . int ( ) ;
80+
81+ expect ( key1 ) . toBeDefined ( ) ;
82+ // Second call should return the same cached instance
83+ expect ( key1 ) . toBe ( key2 ) ;
84+ } ) ;
85+
86+ it ( 'encryptData() + decryptData() recover the original data' , async ( ) => {
87+ const aes = new AesGcmEncryption ( ) ;
88+
89+ // Initialise the local key (used for encryption)
90+ await aes . int ( ) ;
91+
92+ // Export the local key and re-import it as the "remote" key so that
93+ // decryptData() (which uses aesKeyRemote) can decrypt what encryptData()
94+ // produced.
95+ const exportedKey = await aes . getRawAesKeyToExport ( ) ;
96+ await aes . setRemoteAesKey ( exportedKey ) ;
97+
98+ const originalText = 'AES test payload 🔒' ;
99+ const originalData = new TextEncoder ( ) . encode ( originalText ) . buffer ;
100+
101+ const { encryptedData, iv } = await aes . encryptData ( originalData ) ;
102+
103+ expect ( encryptedData ) . toBeInstanceOf ( Uint8Array ) ;
104+ expect ( iv ) . toBeInstanceOf ( Uint8Array ) ;
105+ expect ( iv . length ) . toBe ( 12 ) ; // AES-GCM IV is 12 bytes
106+
107+ const decryptedBuffer = await aes . decryptData ( encryptedData , iv ) ;
108+ const decryptedText = new TextDecoder ( ) . decode ( decryptedBuffer ) ;
109+
110+ expect ( decryptedText ) . toBe ( originalText ) ;
111+ } ) ;
112+
113+ it ( 'decryptData() throws when remote key has not been set' , async ( ) => {
114+ const aes = new AesGcmEncryption ( ) ;
115+ await aes . int ( ) ;
116+
117+ const { encryptedData, iv } = await aes . encryptData (
118+ new TextEncoder ( ) . encode ( 'data' ) . buffer
119+ ) ;
120+
121+ // No setRemoteAesKey() call → should throw
122+ await expect ( aes . decryptData ( encryptedData , iv ) ) . rejects . toThrow (
123+ 'Remote AES key not set.'
124+ ) ;
79125 } ) ;
80- } ) ;
81- } ) ;
126+ } ) ;
0 commit comments