Skip to content

Commit

Permalink
Refactor crypto usage
Browse files Browse the repository at this point in the history
  • Loading branch information
hectorgomezv committed Nov 18, 2024
1 parent cdb2cf8 commit dc11111
Show file tree
Hide file tree
Showing 8 changed files with 51 additions and 32 deletions.
7 changes: 5 additions & 2 deletions src/config/entities/configuration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { randomBytes } from 'crypto';

// Custom configuration for the application

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
Expand Down Expand Up @@ -34,8 +36,9 @@ export default () => ({
},
local: {
algorithm: process.env.LOCAL_ENCRYPTION_ALGORITHM || 'aes-256-cbc',
key: process.env.LOCAL_ENCRYPTION_KEY || 'a'.repeat(64),
iv: process.env.LOCAL_ENCRYPTION_IV || 'b'.repeat(32),
key:
process.env.LOCAL_ENCRYPTION_KEY || randomBytes(32).toString('hex'),
iv: process.env.LOCAL_ENCRYPTION_IV || randomBytes(16).toString('hex'),
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { createAccountDtoBuilder } from '@/domain/accounts/entities/__tests__/cr
import type { Account } from '@/domain/accounts/entities/account.entity';
import type { ILoggingService } from '@/logging/logging.interface';
import { faker } from '@faker-js/faker/.';
import { randomBytes } from 'crypto';
import type postgres from 'postgres';

const mockLoggingService = {
Expand Down Expand Up @@ -45,8 +46,10 @@ describe('AddressBooksDataSource', () => {
if (key === 'expirationTimeInSeconds.default') return faker.number.int();
if (key === 'application.isProduction') return false;
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc';
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64);
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32);
if (key === 'accounts.encryption.local.key')
return randomBytes(32).toString('hex');
if (key === 'accounts.encryption.local.iv')
return randomBytes(16).toString('hex');
});
mockEncryptionApiManager.getApi.mockResolvedValue(
new LocalEncryptionApiService(mockConfigurationService),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@aws-sdk/client-kms';
import { faker } from '@faker-js/faker/.';
import { mockClient } from 'aws-sdk-client-mock';
import * as crypto from 'crypto';
import { randomBytes } from 'crypto';

const mockConfigurationService = {
get: jest.fn(),
Expand Down Expand Up @@ -93,7 +93,7 @@ describe('AwsEncryptionApiService', () => {
describe('encryptBlob/decryptBlob', () => {
it('should encrypt and decrypt arrays of objects correctly', async () => {
const data = [JSON.parse(fakeJson()), JSON.parse(fakeJson())];
const key = new Uint8Array(crypto.randomBytes(32).buffer);
const key = randomBytes(32);
const encryptedBlob = encryptedBlobBuilder().build();
kmsMock.on(GenerateDataKeyCommand).resolves({
Plaintext: Buffer.from(key),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
KMSClient,
} from '@aws-sdk/client-kms';
import { Inject, Injectable } from '@nestjs/common';
import * as crypto from 'crypto';
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';

@Injectable()
export class AwsEncryptionApiService implements IEncryptionApi {
Expand Down Expand Up @@ -52,7 +52,7 @@ export class AwsEncryptionApiService implements IEncryptionApi {
return Buffer.from(decryptedData.Plaintext).toString('utf8');
}

async encryptBlob(data: unknown): Promise<EncryptedBlob> {
async encryptBlob<T>(data: T): Promise<EncryptedBlob> {
if ((typeof data !== 'object' && !Array.isArray(data)) || data === null) {
throw new Error('Data must be an object or array');
}
Expand All @@ -65,8 +65,8 @@ export class AwsEncryptionApiService implements IEncryptionApi {
if (!Plaintext || !CiphertextBlob) {
throw new Error('Failed to generate data key');
}
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(this.algorithm, Plaintext, iv);
const iv = randomBytes(16);
const cipher = createCipheriv(this.algorithm, Plaintext, iv);
return {
encryptedData: Buffer.concat([
cipher.update(JSON.stringify(data), 'utf8'),
Expand All @@ -85,7 +85,7 @@ export class AwsEncryptionApiService implements IEncryptionApi {
if (!Plaintext) {
throw new Error('Failed to decrypt data key');
}
const decipher = crypto.createDecipheriv(this.algorithm, Plaintext, iv);
const decipher = createDecipheriv(this.algorithm, Plaintext, iv);
const decryptedData = Buffer.concat([
decipher.update(encryptedData),
decipher.final(),
Expand Down
19 changes: 13 additions & 6 deletions src/datasources/accounts/encryption/encryption-api.manager.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { IConfigurationService } from '@/config/configuration.service.inter
import { AwsEncryptionApiService } from '@/datasources/accounts/encryption/aws-encryption-api.service';
import { EncryptionApiManager } from '@/datasources/accounts/encryption/encryption-api.manager';
import { LocalEncryptionApiService } from '@/datasources/accounts/encryption/local-encryption-api.service';
import { randomBytes } from 'crypto';

const mockConfigurationService = {
get: jest.fn(),
Expand All @@ -20,8 +21,10 @@ describe('EncryptionApiManager', () => {
if (key === 'accounts.encryption.type') return 'local';
if (key === 'application.isProduction') return false;
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc';
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64);
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32);
if (key === 'accounts.encryption.local.key')
return randomBytes(32).toString('hex');
if (key === 'accounts.encryption.local.iv')
return randomBytes(16).toString('hex');
throw new Error(`Unexpected key: ${key}`);
});
target = new EncryptionApiManager(mockConfigurationService);
Expand All @@ -36,8 +39,10 @@ describe('EncryptionApiManager', () => {
if (key === 'accounts.encryption.type') return 'local';
if (key === 'application.isProduction') return false;
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc';
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64);
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32);
if (key === 'accounts.encryption.local.key')
return randomBytes(32).toString('hex');
if (key === 'accounts.encryption.local.iv')
return randomBytes(16).toString('hex');
throw new Error(`Unexpected key: ${key}`);
});
target = new EncryptionApiManager(mockConfigurationService);
Expand All @@ -52,7 +57,8 @@ describe('EncryptionApiManager', () => {
it('should get a AwsEncryptionApiService', async () => {
mockConfigurationService.getOrThrow.mockImplementation((key) => {
if (key === 'accounts.encryption.type') return 'aws';
if (key === 'accounts.encryption.awsKms.keyId') return 'a'.repeat(64);
if (key === 'accounts.encryption.awsKms.keyId')
return randomBytes(32).toString('hex');
if (key === 'accounts.encryption.awsKms.algorithm') return 'aes-256-cbc';
throw new Error(`Unexpected key: ${key}`);
});
Expand All @@ -66,7 +72,8 @@ describe('EncryptionApiManager', () => {
it('should return the same instance of AwsEncryptionApiService on a second call', async () => {
mockConfigurationService.getOrThrow.mockImplementation((key) => {
if (key === 'accounts.encryption.type') return 'aws';
if (key === 'accounts.encryption.awsKms.keyId') return 'a'.repeat(64);
if (key === 'accounts.encryption.awsKms.keyId')
return randomBytes(32).toString('hex');
if (key === 'accounts.encryption.awsKms.algorithm') return 'aes-256-cbc';
throw new Error(`Unexpected key: ${key}`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { IConfigurationService } from '@/config/configuration.service.inter
import { encryptedBlobBuilder } from '@/datasources/accounts/encryption/entities/__tests__/encrypted-blob.builder';
import { LocalEncryptionApiService } from '@/datasources/accounts/encryption/local-encryption-api.service';
import { faker } from '@faker-js/faker/.';
import { randomBytes } from 'crypto';

const mockConfigurationService = {
get: jest.fn(),
Expand All @@ -17,8 +18,9 @@ describe('LocalEncryptionApiService', () => {
mockConfigurationService.getOrThrow.mockImplementation((key) => {
if (key === 'application.isProduction') return false;
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc';
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64);
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32);
if (key === 'accounts.encryption.local.key')
return new Uint8Array(randomBytes(32).buffer);
if (key === 'accounts.encryption.local.iv') return randomBytes(16);
throw new Error(`Unexpected key: ${key}`);
});
target = new LocalEncryptionApiService(mockConfigurationService);
Expand All @@ -29,8 +31,9 @@ describe('LocalEncryptionApiService', () => {
mockConfigurationService.getOrThrow.mockImplementation((key) => {
if (key === 'application.isProduction') return true;
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc';
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64);
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32);
if (key === 'accounts.encryption.local.key')
return new Uint8Array(randomBytes(32).buffer);
if (key === 'accounts.encryption.local.iv') return randomBytes(16);
throw new Error(`Unexpected key: ${key}`);
});
target = new LocalEncryptionApiService(mockConfigurationService);
Expand All @@ -44,8 +47,9 @@ describe('LocalEncryptionApiService', () => {
mockConfigurationService.getOrThrow.mockImplementation((key) => {
if (key === 'application.isProduction') return true;
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc';
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64);
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32);
if (key === 'accounts.encryption.local.key')
return new Uint8Array(randomBytes(32).buffer);
if (key === 'accounts.encryption.local.iv') return randomBytes(16);
throw new Error(`Unexpected key: ${key}`);
});
target = new LocalEncryptionApiService(mockConfigurationService);
Expand Down Expand Up @@ -77,8 +81,9 @@ describe('LocalEncryptionApiService', () => {
mockConfigurationService.getOrThrow.mockImplementation((key) => {
if (key === 'application.isProduction') return true;
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc';
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64);
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32);
if (key === 'accounts.encryption.local.key')
return new Uint8Array(randomBytes(32).buffer);
if (key === 'accounts.encryption.local.iv') return randomBytes(16);
throw new Error(`Unexpected key: ${key}`);
});
target = new LocalEncryptionApiService(mockConfigurationService);
Expand All @@ -94,8 +99,9 @@ describe('LocalEncryptionApiService', () => {
mockConfigurationService.getOrThrow.mockImplementation((key) => {
if (key === 'application.isProduction') return true;
if (key === 'accounts.encryption.local.algorithm') return 'aes-256-cbc';
if (key === 'accounts.encryption.local.key') return 'a'.repeat(64);
if (key === 'accounts.encryption.local.iv') return 'b'.repeat(32);
if (key === 'accounts.encryption.local.key')
return new Uint8Array(randomBytes(32).buffer);
if (key === 'accounts.encryption.local.iv') return randomBytes(16);
throw new Error(`Unexpected key: ${key}`);
});
target = new LocalEncryptionApiService(mockConfigurationService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { IConfigurationService } from '@/config/configuration.service.interface'
import { EncryptedBlob } from '@/datasources/accounts/encryption/entities/encrypted-blob.entity';
import type { IEncryptionApi } from '@/domain/interfaces/encryption-api.interface';
import { Injectable } from '@nestjs/common';
import * as crypto from 'crypto';
import { createCipheriv, createDecipheriv } from 'crypto';

@Injectable()
export class LocalEncryptionApiService implements IEncryptionApi {
Expand Down Expand Up @@ -36,7 +36,7 @@ export class LocalEncryptionApiService implements IEncryptionApi {
if (this.isProduction) {
throw new Error('Local encryption is not suitable for production usage');
}
const cipher = crypto.createCipheriv(this.algorithm, this.key, this.iv);
const cipher = createCipheriv(this.algorithm, this.key, this.iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
return Promise.resolve(encrypted);
Expand All @@ -46,7 +46,7 @@ export class LocalEncryptionApiService implements IEncryptionApi {
if (this.isProduction) {
throw new Error('Local encryption is not suitable for production usage');
}
const decipher = crypto.createDecipheriv(this.algorithm, this.key, this.iv);
const decipher = createDecipheriv(this.algorithm, this.key, this.iv);
let decrypted = decipher.update(data, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return Promise.resolve(decrypted);
Expand Down Expand Up @@ -81,7 +81,7 @@ export class LocalEncryptionApiService implements IEncryptionApi {
const decryptedKey = await this.decrypt(
encryptedBlob.encryptedDataKey.toString('hex'),
);
const decipher = crypto.createDecipheriv(
const decipher = createDecipheriv(
this.algorithm,
Buffer.from(decryptedKey, 'hex'),
encryptedBlob.iv,
Expand Down
2 changes: 1 addition & 1 deletion src/domain/interfaces/encryption-api.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface IEncryptionApi {

decrypt(data: string): Promise<string>;

encryptBlob(data: unknown): Promise<EncryptedBlob>;
encryptBlob<T>(data: T): Promise<EncryptedBlob>;

decryptBlob<T>(data: EncryptedBlob): Promise<T>;
}

0 comments on commit dc11111

Please sign in to comment.