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
37 changes: 37 additions & 0 deletions packages/core/src/encryption/__tests__/cipher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,43 @@ describe('Cipher', () => {
});
});

describe('encryptWithKey', () => {
it('should roundtrip with decryptWithKey using the same key', () => {
const encrypted = cipher.encryptWithKey('random-string', 'explicit-key', 'aes-256-cbc');
const decrypted = cipher.decryptWithKey(encrypted, 'explicit-key', 'aes-256-cbc');
expect(decrypted).toEqual('random-string');
});

it('should produce different ciphertexts for the same plaintext on successive calls', () => {
const first = cipher.encryptWithKey('random-string', 'explicit-key', 'aes-256-cbc');
const second = cipher.encryptWithKey('random-string', 'explicit-key', 'aes-256-cbc');
expect(first).not.toEqual(second);
});

it('should fail to decrypt with a different key', () => {
const encrypted = cipher.encryptWithKey('random-string', 'key-a', 'aes-256-cbc');
expect(() => cipher.decryptWithKey(encrypted, 'key-b', 'aes-256-cbc')).toThrow();
});

it('should throw on aes-256-gcm for encryptWithKey', () => {
expect(() => cipher.encryptWithKey('data', 'key', 'aes-256-gcm')).toThrow(
'GCM not yet implemented',
);
});

it('should throw on aes-256-gcm for decryptWithKey', () => {
expect(() => cipher.decryptWithKey('data', 'key', 'aes-256-gcm')).toThrow(
'GCM not yet implemented',
);
});

it('should be interoperable with legacy encrypt when given the same key', () => {
const encrypted = cipher.encrypt('random-string', 'shared-key');
const decrypted = cipher.decryptWithKey(encrypted, 'shared-key', 'aes-256-cbc');
expect(decrypted).toEqual('random-string');
});
});

describe('getKeyAndIv', () => {
it('should generate a key and iv using instance settings encryption key', () => {
const salt = Buffer.from('test-salt');
Expand Down
31 changes: 25 additions & 6 deletions packages/core/src/encryption/cipher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,44 @@ import { InstanceSettings } from '@/instance-settings';
// Data encrypted by CryptoJS always starts with these bytes
const RANDOM_BYTES = Buffer.from('53616c7465645f5f', 'hex');

export type CipherAlgorithm = 'aes-256-cbc' | 'aes-256-gcm';

@Service()
export class Cipher {
constructor(private readonly instanceSettings: InstanceSettings) {}

encrypt(data: string | object, customEncryptionKey?: string) {
const key = customEncryptionKey ?? this.instanceSettings.encryptionKey;
const plaintext = typeof data === 'string' ? data : JSON.stringify(data);
return this.encryptWithKey(plaintext, key, 'aes-256-cbc');
}

decrypt(data: string, customEncryptionKey?: string) {
const key = customEncryptionKey ?? this.instanceSettings.encryptionKey;
return this.decryptWithKey(data, key, 'aes-256-cbc');
}

encryptWithKey(data: string, key: string, algorithm: CipherAlgorithm): string {
if (algorithm === 'aes-256-gcm') {
throw new Error('GCM not yet implemented');
}
const salt = randomBytes(8);
const [key, iv] = this.getKeyAndIv(salt, customEncryptionKey);
const cipher = createCipheriv('aes-256-cbc', key, iv);
const encrypted = cipher.update(typeof data === 'string' ? data : JSON.stringify(data));
const [derivedKey, iv] = this.getKeyAndIv(salt, key);
const cipher = createCipheriv('aes-256-cbc', derivedKey, iv);
const encrypted = cipher.update(data);
return Buffer.concat([RANDOM_BYTES, salt, encrypted, cipher.final()]).toString('base64');
}

decrypt(data: string, customEncryptionKey?: string) {
decryptWithKey(data: string, key: string, algorithm: CipherAlgorithm): string {
if (algorithm === 'aes-256-gcm') {
throw new Error('GCM not yet implemented');
}
const input = Buffer.from(data, 'base64');
if (input.length < 16) return '';
const salt = input.subarray(8, 16);
const [key, iv] = this.getKeyAndIv(salt, customEncryptionKey);
const [derivedKey, iv] = this.getKeyAndIv(salt, key);
const contents = input.subarray(16);
const decipher = createDecipheriv('aes-256-cbc', key, iv);
const decipher = createDecipheriv('aes-256-cbc', derivedKey, iv);
return Buffer.concat([decipher.update(contents), decipher.final()]).toString('utf-8');
}

Expand Down
Loading