|
| 1 | +/** |
| 2 | + * XChaCha20-Poly1305 tests |
| 3 | + * |
| 4 | + * Test vectors from IETF draft-irtf-cfrg-xchacha and libsodium test suite. |
| 5 | + * XChaCha20-Poly1305 is an AEAD cipher with: |
| 6 | + * - 32-byte key |
| 7 | + * - 24-byte nonce (extended nonce) |
| 8 | + * - 16-byte authentication tag |
| 9 | + * - AAD (Additional Authenticated Data) support |
| 10 | + */ |
| 11 | + |
| 12 | +import { |
| 13 | + Buffer, |
| 14 | + createCipheriv, |
| 15 | + createDecipheriv, |
| 16 | +} from 'react-native-quick-crypto'; |
| 17 | +import { expect } from 'chai'; |
| 18 | +import { test } from '../util'; |
| 19 | +import { roundTripAuth } from './roundTrip'; |
| 20 | + |
| 21 | +const SUITE = 'cipher'; |
| 22 | + |
| 23 | +function fromHex(h: string | Buffer): Buffer { |
| 24 | + if (typeof h === 'string') { |
| 25 | + h = h.replace(/([^0-9a-f])/gi, ''); |
| 26 | + return Buffer.from(h, 'hex'); |
| 27 | + } |
| 28 | + return h; |
| 29 | +} |
| 30 | + |
| 31 | +interface XChaCha20Poly1305TestVector { |
| 32 | + key: string; |
| 33 | + nonce: string; |
| 34 | + plaintext: string; |
| 35 | + aad: string | Buffer; |
| 36 | + ciphertext: string; |
| 37 | + tag: string; |
| 38 | +} |
| 39 | + |
| 40 | +// Test vector from IETF draft-irtf-cfrg-xchacha (Appendix A.3.1) |
| 41 | +const ietfA31Vector: XChaCha20Poly1305TestVector = { |
| 42 | + key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', |
| 43 | + nonce: '404142434445464748494a4b4c4d4e4f5051525354555657', |
| 44 | + plaintext: |
| 45 | + '4c616469657320616e642047656e746c656d656e206f662074686520636c6173' + |
| 46 | + '73206f66202739393a204966204920636f756c64206f6666657220796f75206f' + |
| 47 | + '6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73' + |
| 48 | + '637265656e20776f756c642062652069742e', |
| 49 | + aad: '50515253c0c1c2c3c4c5c6c7', |
| 50 | + ciphertext: |
| 51 | + 'bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb' + |
| 52 | + '731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b452' + |
| 53 | + '2f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff9' + |
| 54 | + '21f9664c97637da9768812f615c68b13b52e', |
| 55 | + tag: 'c0875924c1c7987947deafd8780acf49', |
| 56 | +}; |
| 57 | + |
| 58 | +function testXChaCha20Poly1305Vector( |
| 59 | + vector: XChaCha20Poly1305TestVector, |
| 60 | + description: string, |
| 61 | +) { |
| 62 | + test(SUITE, `xchacha20-poly1305 ${description}`, () => { |
| 63 | + const key = fromHex(vector.key); |
| 64 | + const nonce = fromHex(vector.nonce); |
| 65 | + const plaintext = fromHex(vector.plaintext); |
| 66 | + const aad = fromHex(vector.aad); |
| 67 | + const expectedCiphertext = fromHex(vector.ciphertext); |
| 68 | + const expectedTag = fromHex(vector.tag); |
| 69 | + |
| 70 | + // First test round trip |
| 71 | + roundTripAuth('xchacha20-poly1305', key, nonce, plaintext, aad); |
| 72 | + |
| 73 | + // Then test against expected values |
| 74 | + const cipher = createCipheriv('xchacha20-poly1305', key, nonce); |
| 75 | + cipher.setAAD(aad); |
| 76 | + const actualCiphertext = Buffer.concat([ |
| 77 | + cipher.update(plaintext), |
| 78 | + cipher.final(), |
| 79 | + ]); |
| 80 | + const actualTag = cipher.getAuthTag(); |
| 81 | + |
| 82 | + expect(actualCiphertext).to.deep.equal(expectedCiphertext); |
| 83 | + expect(actualTag).to.deep.equal(expectedTag); |
| 84 | + }); |
| 85 | +} |
| 86 | + |
| 87 | +testXChaCha20Poly1305Vector(ietfA31Vector, 'IETF draft A.3.1 vector'); |
| 88 | + |
| 89 | +// Basic round-trip tests |
| 90 | +test(SUITE, 'xchacha20-poly1305 basic round trip', () => { |
| 91 | + const key = Buffer.alloc(32, 0x42); |
| 92 | + const nonce = Buffer.alloc(24, 0x24); |
| 93 | + const plaintext = Buffer.from('Hello, XChaCha20-Poly1305!', 'utf8'); |
| 94 | + const aad = Buffer.from('additional data', 'utf8'); |
| 95 | + |
| 96 | + roundTripAuth('xchacha20-poly1305', key, nonce, plaintext, aad); |
| 97 | +}); |
| 98 | + |
| 99 | +test(SUITE, 'xchacha20-poly1305 without AAD', () => { |
| 100 | + const key = Buffer.alloc(32, 0x42); |
| 101 | + const nonce = Buffer.alloc(24, 0x24); |
| 102 | + const plaintext = Buffer.from('Hello, XChaCha20-Poly1305!', 'utf8'); |
| 103 | + |
| 104 | + roundTripAuth('xchacha20-poly1305', key, nonce, plaintext); |
| 105 | +}); |
| 106 | + |
| 107 | +test(SUITE, 'xchacha20-poly1305 empty plaintext', () => { |
| 108 | + const key = Buffer.alloc(32, 0x42); |
| 109 | + const nonce = Buffer.alloc(24, 0x24); |
| 110 | + const plaintext = Buffer.alloc(0); |
| 111 | + const aad = Buffer.from('aad only', 'utf8'); |
| 112 | + |
| 113 | + roundTripAuth('xchacha20-poly1305', key, nonce, plaintext, aad); |
| 114 | +}); |
| 115 | + |
| 116 | +test(SUITE, 'xchacha20-poly1305 large plaintext', () => { |
| 117 | + const key = Buffer.alloc(32, 0x42); |
| 118 | + const nonce = Buffer.alloc(24, 0x24); |
| 119 | + const plaintext = Buffer.alloc(4096, 0x55); |
| 120 | + const aad = Buffer.from('large data test', 'utf8'); |
| 121 | + |
| 122 | + roundTripAuth('xchacha20-poly1305', key, nonce, plaintext, aad); |
| 123 | +}); |
| 124 | + |
| 125 | +// Error case tests |
| 126 | +test(SUITE, 'xchacha20-poly1305 wrong key size throws', () => { |
| 127 | + const key = Buffer.alloc(16, 0x42); // Wrong size: should be 32 |
| 128 | + const nonce = Buffer.alloc(24, 0x24); |
| 129 | + |
| 130 | + expect(() => { |
| 131 | + createCipheriv('xchacha20-poly1305', key, nonce); |
| 132 | + }).to.throw(/key must be 32 bytes/i); |
| 133 | +}); |
| 134 | + |
| 135 | +test(SUITE, 'xchacha20-poly1305 wrong nonce size throws', () => { |
| 136 | + const key = Buffer.alloc(32, 0x42); |
| 137 | + const nonce = Buffer.alloc(12, 0x24); // Wrong size: should be 24 |
| 138 | + |
| 139 | + expect(() => { |
| 140 | + createCipheriv('xchacha20-poly1305', key, nonce); |
| 141 | + }).to.throw(/nonce must be 24 bytes/i); |
| 142 | +}); |
| 143 | + |
| 144 | +test(SUITE, 'xchacha20-poly1305 tag mismatch throws', () => { |
| 145 | + const key = Buffer.alloc(32, 0x42); |
| 146 | + const nonce = Buffer.alloc(24, 0x24); |
| 147 | + const plaintext = Buffer.from('test message', 'utf8'); |
| 148 | + |
| 149 | + // Encrypt |
| 150 | + const cipher = createCipheriv('xchacha20-poly1305', key, nonce); |
| 151 | + const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); |
| 152 | + |
| 153 | + // Try to decrypt with wrong tag |
| 154 | + const decipher = createDecipheriv('xchacha20-poly1305', key, nonce); |
| 155 | + const wrongTag = Buffer.alloc(16, 0xff); // Wrong tag |
| 156 | + decipher.setAuthTag(wrongTag); |
| 157 | + decipher.update(ciphertext); |
| 158 | + |
| 159 | + expect(() => { |
| 160 | + decipher.final(); |
| 161 | + }).to.throw(/authentication tag mismatch/i); |
| 162 | +}); |
0 commit comments