Skip to content

Commit

Permalink
feat: add encapsulated cbd encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
piotr-roslaniec committed Aug 7, 2023
1 parent ae56829 commit 7e19676
Show file tree
Hide file tree
Showing 5 changed files with 462 additions and 266 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@
},
"dependencies": {
"@nucypher/nucypher-core": "^0.10.0",
"@types/crypto-js": "^4.1.1",
"axios": "^0.21.1",
"crypto-js": "^4.1.1",
"deep-equal": "^2.2.1",
"ethers": "^5.4.1",
"joi": "^17.7.0",
Expand Down
133 changes: 131 additions & 2 deletions src/characters/enrico.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,18 @@ import {
PublicKey,
SecretKey,
} from '@nucypher/nucypher-core';
import { AES } from 'crypto-js';

import { ConditionExpression } from '../conditions';
import { ConditionExpression, ConditionExpressionJSON } from '../conditions';
import { Keyring } from '../keyring';
import { toBytes } from '../utils';
import {
bytesEquals,
fromBase64,
fromBytes,
objectEquals,
toBase64,
toBytes,
} from '../utils';

export class Enrico {
public readonly encryptingKey: PublicKey | DkgPublicKey;
Expand Down Expand Up @@ -73,4 +81,125 @@ export class Enrico {
);
return { ciphertext, aad };
}

public async encapsulateCbd(
message: string,
passphrase: string,
withConditions?: ConditionExpression
): Promise<ThresholdMessageKit> {
const bulkCiphertext = AES.encrypt(message, passphrase).toString();

const { ciphertext, aad } = this.encryptMessageCbd(
passphrase,
withConditions
);

return new ThresholdMessageKit(
ciphertext,
aad,
bulkCiphertext,
withConditions || this.conditions
);
}
}

export class ThresholdMessageKit {
private static readonly SEPARATOR = ';';

constructor(
public keyCiphertext: Ciphertext,
public keyAad: Uint8Array,
public bulkCiphertext: string, // OpenSSL-compatible string
public conditions?: ConditionExpression,
public metadata: Record<string, string> = {}
) {}

public toObj(): ThresholdMessageKitJSON {
return {
keyCiphertext: toBase64(this.keyCiphertext.toBytes()),
keyAad: toBase64(this.keyAad),
bulkCiphertext: this.bulkCiphertext,
conditions: this.conditions ? this.conditions.toObj() : undefined,
metadata: this.metadata,
};
}

public toJson(): string {
return JSON.stringify(this.toObj());
}

public toBytes(): Uint8Array {
const bulkCiphertextBytes = toBytes(this.bulkCiphertext);
const separator = toBytes(ThresholdMessageKit.SEPARATOR);
// bulkCiphertext will be serialized separately
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { bulkCiphertext: _, ...tmkMeta } = this.toObj();
const tmkMetaBytes = toBytes(JSON.stringify(tmkMeta));

return new Uint8Array([
...tmkMetaBytes,
...separator,
...bulkCiphertextBytes,
]);
}

public static fromObj({
keyCiphertext,
keyAad,
bulkCiphertext,
conditions,
metadata,
}: ThresholdMessageKitJSON): ThresholdMessageKit {
return new ThresholdMessageKit(
Ciphertext.fromBytes(fromBase64(keyCiphertext)),
fromBase64(keyAad),
bulkCiphertext,
conditions ? ConditionExpression.fromObj(conditions) : undefined,
metadata
);
}

public static fromJson(json: string): ThresholdMessageKit {
return ThresholdMessageKit.fromObj(JSON.parse(json));
}

public static fromBytes(bytes: Uint8Array): ThresholdMessageKit {
const separator = toBytes(ThresholdMessageKit.SEPARATOR);
const separatorIndex = bytes.indexOf(separator[0]);
const tmkMetaBytes = bytes.slice(0, separatorIndex);
const bulkCiphertextBytes = bytes.slice(separatorIndex + separator.length);

const tmkMeta: ThresholdMessageKitJSON = JSON.parse(
fromBytes(tmkMetaBytes)
);
const { keyCiphertext, keyAad, conditions, metadata } = tmkMeta;

return new ThresholdMessageKit(
Ciphertext.fromBytes(fromBase64(keyCiphertext)),
fromBase64(keyAad),
fromBytes(bulkCiphertextBytes),
conditions ? ConditionExpression.fromObj(conditions) : undefined,
metadata
);
}

public equals(other: ThresholdMessageKit): boolean {
return [
this.keyCiphertext.equals(other.keyCiphertext),
bytesEquals(this.keyAad, other.keyAad),
this.bulkCiphertext === other.bulkCiphertext,
other.conditions
? this.conditions?.equals(other.conditions)
: !this.conditions,
objectEquals(this.metadata, other.metadata),
].every(Boolean);
}
}

export type ThresholdMessageKitJSON = {
keyCiphertext: string;
keyAad: string;
bulkCiphertext: string;
conditions?: ConditionExpressionJSON;
metadata: Record<string, string>;
};
3 changes: 3 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import deepEqual from 'deep-equal';
export const toBytes = (str: string): Uint8Array =>
new TextEncoder().encode(str);

export const fromBytes = (bytes: Uint8Array): string =>
new TextDecoder().decode(bytes);

export const fromHexString = (hexString: string): Uint8Array => {
if (hexString.startsWith('0x')) {
hexString = hexString.slice(2);
Expand Down
32 changes: 32 additions & 0 deletions test/integration/enrico.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Disabling because we want to access Alice.keyring which is a private property
/* eslint-disable @typescript-eslint/no-explicit-any */
import { DkgPublicKey } from '@nucypher/nucypher-core';

import { conditions, Enrico, PolicyMessageKit } from '../../src';
import { ThresholdMessageKit } from '../../src/characters/enrico';
import { RetrievalResult } from '../../src/kits/retrieval';
import { toBytes } from '../../src/utils';
import {
Expand Down Expand Up @@ -150,3 +153,32 @@ describe('enrico', () => {
expect(alicePlaintext).toEqual(alicePlaintext);
});
});

describe('enrico with cbd encapsulation', () => {
it('encapsulates a plaintext message', async () => {
const message = 'fake-message';
const encryptingKey = DkgPublicKey.random();
const ownsBufficornNFT = ERC721Ownership.fromObj({
contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
parameters: [3591],
chain: 5,
});
const conditions = new ConditionExpression(ownsBufficornNFT);
const enrico = new Enrico(encryptingKey, undefined, conditions);

const passphrase = "I'm a passphrase";
const tmk = await enrico.encapsulateCbd(message, passphrase, conditions);

const asObject = tmk.toObj();
const fromObject = ThresholdMessageKit.fromObj(asObject);
expect(fromObject.equals(tmk)).toBeTruthy();

const asJson = tmk.toJson();
const fromJson = ThresholdMessageKit.fromJson(asJson);
expect(fromJson.equals(tmk)).toBeTruthy();

const asBytes = tmk.toBytes();
const fromBytes = ThresholdMessageKit.fromBytes(asBytes);
expect(fromBytes.equals(tmk)).toBeTruthy();
});
});
Loading

0 comments on commit 7e19676

Please sign in to comment.