Skip to content

Commit

Permalink
#276 - added methods for AES encryption and decryption
Browse files Browse the repository at this point in the history
  • Loading branch information
thehenrytsai authored Mar 29, 2023
1 parent ebab08a commit 9f63ad0
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Decentralized Web Node (DWN) SDK

Code Coverage
![Statements](https://img.shields.io/badge/statements-95.1%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-93.05%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-94.09%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-95.1%25-brightgreen.svg?style=flat)
![Statements](https://img.shields.io/badge/statements-93.73%25-brightgreen.svg?style=flat) ![Branches](https://img.shields.io/badge/branches-92.9%25-brightgreen.svg?style=flat) ![Functions](https://img.shields.io/badge/functions-90.55%25-brightgreen.svg?style=flat) ![Lines](https://img.shields.io/badge/lines-93.73%25-brightgreen.svg?style=flat)

## Introduction

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tbd54566975/dwn-sdk-js",
"version": "0.0.27",
"version": "0.0.28",
"description": "A reference implementation of https://identity.foundation/decentralized-web-node/spec/",
"type": "module",
"types": "./dist/esm/src/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export { Dwn } from './dwn.js';
export { DwnConstant } from './core/dwn-constant.js';
export { DwnInterfaceName, DwnMethodName } from './core/message.js';
export { Encoder } from './utils/encoder.js';
export { Encryption } from './utils/encryption.js';
export { HooksWrite, HooksWriteOptions } from './interfaces/hooks/messages/hooks-write.js';
export { Jws } from './utils/jws.js';
export { KeyMaterial, PrivateJwk, PublicJwk } from './jose/types.js';
Expand Down
63 changes: 63 additions & 0 deletions src/utils/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as crypto from 'crypto';
import { Readable } from 'readable-stream';

/**
* Utility class for performing common encryption operations.
*/
export class Encryption {
/**
* Encrypts the given plaintext stream using AES-256-CTR algorithm.
*/
public static async aes256CtrEncrypt(key: Uint8Array, initializationVector: Uint8Array, plaintextStream: Readable): Promise<Readable> {
const cipher = crypto.createCipheriv('aes-256-ctr', key, initializationVector);

const cipherStream = new Readable({
read(): void { }
});

plaintextStream.on('data', (chunk) => {
const encryptedChunk = cipher.update(chunk);
cipherStream.push(encryptedChunk);
});

plaintextStream.on('end', () => {
const finalChunk = cipher.final();
cipherStream.push(finalChunk);
cipherStream.push(null);
});

plaintextStream.on('error', (err) => {
cipherStream.emit('error', err);
});

return cipherStream;
}

/**
* Decrypts the given cipher stream using AES-256-CTR algorithm.
*/
public static async aes256CtrDecrypt(key: Uint8Array, initializationVector: Uint8Array, cipherStream: Readable): Promise<Readable> {
const decipher = crypto.createDecipheriv('aes-256-ctr', key, initializationVector);

const plaintextStream = new Readable({
read(): void { }
});

cipherStream.on('data', (chunk) => {
const decryptedChunk = decipher.update(chunk);
plaintextStream.push(decryptedChunk);
});

cipherStream.on('end', () => {
const finalChunk = decipher.final();
plaintextStream.push(finalChunk);
plaintextStream.push(null);
});

cipherStream.on('error', (err) => {
plaintextStream.emit('error', err);
});

return plaintextStream;
}
}
120 changes: 120 additions & 0 deletions tests/utils/encryption.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Comparer } from '../utils/comparer.js';
import { DataStream } from '../../src/index.js';
import { Encryption } from '../../src/utils/encryption.js';
import { expect } from 'chai';
import { Readable } from 'readable-stream';
import { TestDataGenerator } from './test-data-generator.js';

describe('Encryption', () => {
describe('AES-256-CTR', () => {
it('should be able to encrypt and decrypt a data stream correctly', async () => {
const key = TestDataGenerator.randomBytes(32);
const initializationVector = TestDataGenerator.randomBytes(16);

const inputBytes = TestDataGenerator.randomBytes(1_000_000);
const inputStream = DataStream.fromBytes(inputBytes);

const cipherStream = await Encryption.aes256CtrEncrypt(key, initializationVector, inputStream);

const plaintextStream = await Encryption.aes256CtrDecrypt(key, initializationVector, cipherStream);
const plaintextBytes = await DataStream.toBytes(plaintextStream);

expect(Comparer.byteArraysEqual(inputBytes, plaintextBytes)).to.be.true;
});

it('should emit error on encrypt if the plaintext data stream emits an error', async () => {
const key = TestDataGenerator.randomBytes(32);
const initializationVector = TestDataGenerator.randomBytes(16);

let errorOccurred = false;

// a mock plaintext stream
const randomByteGenerator = asyncRandomByteGenerator({ totalIterations: 10, bytesPerIteration: 1 });
const mockPlaintextStream = new Readable({
async read(): Promise<void> {
if (errorOccurred) {
return;
}

// MUST use async generator/iterator, else caller will repeatedly call `read()` in a blocking manner until `null` is returned
const { value } = await randomByteGenerator.next();
this.push(value);
}
});

const cipherStream = await Encryption.aes256CtrEncrypt(key, initializationVector, mockPlaintextStream);

const simulatedErrorMessage = 'Simulated error';

// test that the `error` event from plaintext stream will propagate to the cipher stream
const eventPromise = new Promise<void>((resolve, _reject) => {
cipherStream.on('error', (error) => {
expect(error).to.equal(simulatedErrorMessage);
errorOccurred = true;
resolve();
});
});

// trigger the `error` in the plaintext stream
mockPlaintextStream.emit('error', simulatedErrorMessage);

await eventPromise;

expect(errorOccurred).to.be.true;
});
});

it('should emit error on decrypt if the plaintext data stream emits an error', async () => {
const key = TestDataGenerator.randomBytes(32);
const initializationVector = TestDataGenerator.randomBytes(16);

let errorOccurred = false;

// a mock cipher stream
const randomByteGenerator = asyncRandomByteGenerator({ totalIterations: 10, bytesPerIteration: 1 });
const mockCipherStream = new Readable({
async read(): Promise<void> {
if (errorOccurred) {
return;
}

// MUST use async generator/iterator, else caller will repeatedly call `read()` in a blocking manner until `null` is returned
const { value } = await randomByteGenerator.next();
this.push(value);
}
});

const plaintextStream = await Encryption.aes256CtrDecrypt(key, initializationVector, mockCipherStream);

const simulatedErrorMessage = 'Simulated error';

// test that the `error` event from cipher stream will propagate to the plaintext stream
const eventPromise = new Promise<void>((resolve, _reject) => {
plaintextStream.on('error', (error) => {
expect(error).to.equal(simulatedErrorMessage);
errorOccurred = true;
resolve();
});
});

// trigger the `error` in the cipher stream
mockCipherStream.emit('error', simulatedErrorMessage);

await eventPromise;

expect(errorOccurred).to.be.true;
});
});

/**
* Generates iterations of random bytes
*/
async function* asyncRandomByteGenerator(input: { totalIterations: number, bytesPerIteration: number }): AsyncGenerator<Uint8Array | null> {
let i = 0;
while (i < input.totalIterations) {
yield TestDataGenerator.randomBytes(input.bytesPerIteration);
i++;
}

yield null;
}

0 comments on commit 9f63ad0

Please sign in to comment.