Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions example/src/tests/cipher/cipher_tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,59 @@ test(SUITE, 'Buffer concat vs string concat produce same result', () => {
expect(bufResult).to.equal(strResult);
});

test(SUITE, 'base64 string encoding with multi-block plaintext', () => {
const testKey = Buffer.from(
'KTnGEDonslhj/qGvf6rj4HSnO32T7dvjAs5PntTDB0s=',
'base64',
);
const testIv = Buffer.from('2pXx2krk1wU8RI6AQjuPUg==', 'base64');
// 32 bytes = 2 AES blocks; update() returns 32 bytes, 32 % 3 = 2 remainder
const text = 'A'.repeat(32);

const cipher1 = createCipheriv('aes-256-cbc', testKey, testIv);
const bufResult = Buffer.concat([
cipher1.update(Buffer.from(text, 'utf8')),
cipher1.final(),
]).toString('base64');

const cipher2 = createCipheriv('aes-256-cbc', testKey, testIv);
const strResult =
cipher2.update(text, 'utf8', 'base64') + cipher2.final('base64');

expect(bufResult).to.equal(strResult);
});

test(SUITE, 'base64 encoding at exactly one block boundary', () => {
// 16 bytes = exactly one AES block; update() returns 16 bytes, 16 % 3 = 1
const text = 'A'.repeat(16);

const cipher1 = createCipheriv('aes-128-cbc', key16, iv);
const bufResult = Buffer.concat([
cipher1.update(Buffer.from(text, 'utf8')),
cipher1.final(),
]).toString('base64');

const cipher2 = createCipheriv('aes-128-cbc', key16, iv);
const strResult =
cipher2.update(text, 'utf8', 'base64') + cipher2.final('base64');

expect(bufResult).to.equal(strResult);
});

test(SUITE, 'base64 encoding encrypt/decrypt roundtrip with long input', () => {
const longText = 'The quick brown fox jumps over the lazy dog. '.repeat(5);

const cipher = createCipheriv('aes-256-cbc', key32, iv);
const encrypted =
cipher.update(longText, 'utf8', 'base64') + cipher.final('base64');

const decipher = createDecipheriv('aes-256-cbc', key32, iv);
const decrypted =
decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8');

expect(decrypted).to.equal(longText);
});

test(SUITE, 'update with hex input and output encoding', () => {
const cipher1 = createCipheriv('aes-128-cbc', key16, iv);
const bufResult = Buffer.concat([
Expand Down
1 change: 1 addition & 0 deletions packages/react-native-quick-crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"events": "3.3.0",
"readable-stream": "4.5.2",
"safe-buffer": "^5.2.1",
"string_decoder": "^1.3.0",
"util": "0.12.5"
},
"devDependencies": {
Expand Down
20 changes: 17 additions & 3 deletions packages/react-native-quick-crypto/src/cipher.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { NitroModules } from 'react-native-nitro-modules';
import Stream, { type TransformOptions } from 'readable-stream';
import { StringDecoder } from 'string_decoder';
import { Buffer } from '@craftzdog/react-native-buffer';
import type { BinaryLike, BinaryLikeNode, Encoding } from './utils';
import type {
Expand All @@ -14,7 +15,7 @@ import type {
Cipher as NativeCipher,
CipherFactory,
} from './specs/cipher.nitro';
import { ab2str, binaryLikeToArrayBuffer } from './utils';
import { binaryLikeToArrayBuffer } from './utils';
import {
getDefaultEncoding,
getUIntOption,
Expand Down Expand Up @@ -74,6 +75,8 @@ interface CipherArgs {

class CipherCommon extends Stream.Transform {
private native: NativeCipher;
private _decoder: StringDecoder | null = null;
private _decoderEncoding: string | undefined = undefined;

constructor({ isCipher, cipherType, cipherKey, iv, options }: CipherArgs) {
// Explicitly create TransformOptions for super()
Expand Down Expand Up @@ -120,6 +123,17 @@ class CipherCommon extends Stream.Transform {
});
}

private getDecoder(encoding: string): StringDecoder {
const normalized = normalizeEncoding(encoding);
if (!this._decoder) {
this._decoder = new StringDecoder(encoding as BufferEncoding);
this._decoderEncoding = normalized;
} else if (this._decoderEncoding !== normalized) {
throw new Error('Cannot change encoding');
}
return this._decoder;
}

update(data: Buffer): Buffer;
update(data: BinaryLike, inputEncoding?: Encoding): Buffer;
update(
Expand Down Expand Up @@ -147,7 +161,7 @@ class CipherCommon extends Stream.Transform {
);

if (outputEncoding && outputEncoding !== 'buffer') {
return ab2str(ret, outputEncoding);
return this.getDecoder(outputEncoding).write(Buffer.from(ret));
}

return Buffer.from(ret);
Expand All @@ -159,7 +173,7 @@ class CipherCommon extends Stream.Transform {
const ret = this.native.final();

if (outputEncoding && outputEncoding !== 'buffer') {
return ab2str(ret, outputEncoding);
return this.getDecoder(outputEncoding).end(Buffer.from(ret));
}

return Buffer.from(ret);
Expand Down
Loading