Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for bidirectional encryption. #16

Merged
merged 6 commits into from
May 9, 2022
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
2 changes: 2 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ overrides:
"@typescript-eslint/require-await": "off"
"@typescript-eslint/no-unsafe-argument": "off"
"@typescript-eslint/no-unsafe-assignment": "off"
"@typescript-eslint/no-unused-vars": "off"

- files: src/**/*
env:
node: false
Expand Down
61 changes: 48 additions & 13 deletions src/encryptionContext.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,74 @@
import type { AeadParams } from './interfaces/aeadParams';
import type { KeyInfo } from './interfaces/keyInfo';
import type { KdfContext } from './kdfContext';

import { ExporterContext } from './exporterContext';
import * as errors from './errors';
import { i2Osp, xor } from './utils';

import * as consts from './consts';
import * as errors from './errors';

export class EncryptionContext extends ExporterContext {

protected readonly key: CryptoKey;
protected readonly baseNonce: Uint8Array;
protected seq: number;
/// AEAD algorithm identifier.
protected _alg: string;
/// The length in bytes of a key for the algorithm.
protected _nK: number;
/// The length in bytes of a nonce for the algorithm.
protected _nN: number;
/// The length in bytes of an authentication tag for the algorithm.
protected _nT: number;
/// Forward key information.
protected _f: KeyInfo;
/// Reverse key information.
protected _r: KeyInfo;

public constructor(crypto: SubtleCrypto, kdf: KdfContext, params: AeadParams) {
super(crypto, kdf, params.exporterSecret);

if (params.key === undefined || params.baseNonce === undefined || params.seq === undefined) {
throw new errors.ValidationError('Required parameters are missing');
}
this.key = params.key;
this.baseNonce = params.baseNonce;
this.seq = params.seq;
this._alg = params.alg;
this._nK = params.nK;
this._nN = params.nN;
this._nT = params.nT;
this._f = {
key: params.key,
baseNonce: params.baseNonce,
seq: params.seq,
};
this._r = {
key: params.key,
baseNonce: consts.EMPTY,
seq: 0,
};
return;
}

protected computeNonce(): ArrayBuffer {
const seqBytes = i2Osp(this.seq, this.baseNonce.byteLength);
return xor(this.baseNonce, seqBytes);
protected computeNonce(k: KeyInfo): ArrayBuffer {
const seqBytes = i2Osp(k.seq, k.baseNonce.byteLength);
return xor(k.baseNonce, seqBytes);
}

protected incrementSeq() {
protected incrementSeq(k: KeyInfo) {
// if (this.seq >= (1 << (8 * this.baseNonce.byteLength)) - 1) {
if (this.seq >= Number.MAX_SAFE_INTEGER) {
if (k.seq >= Number.MAX_SAFE_INTEGER) {
throw new errors.MessageLimitReachedError('Message limit reached');
}
this.seq += 1;
k.seq += 1;
return;
}

public async setupBidirectional(keySeed: ArrayBuffer, nonceSeed: ArrayBuffer): Promise<void> {
try {
this._r.baseNonce = new Uint8Array(await this.export(nonceSeed, this._nN));
const key = await this.export(keySeed, this._nK);
this._r.key = await this._crypto.importKey('raw', key, { name: this._alg }, true, consts.AEAD_USAGES);
this._r.seq = 0;
} catch (e: unknown) {
this._r.baseNonce = consts.EMPTY;
throw e;
}
}
}
12 changes: 8 additions & 4 deletions src/exporterContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ export class ExporterContext extends WebCrypto implements EncryptionContextInter
return;
}

public async seal(data: ArrayBuffer, aad: ArrayBuffer): Promise<ArrayBuffer> {
throw new errors.SealError('Not available on export-only mode');
public async seal(_data: ArrayBuffer, _aad: ArrayBuffer): Promise<ArrayBuffer> {
throw new errors.NotSupportedError('Not available on export-only mode');
}

public async open(data: ArrayBuffer, aad: ArrayBuffer): Promise<ArrayBuffer> {
throw new errors.OpenError('Not available on export-only mode');
public async open(_data: ArrayBuffer, _aad: ArrayBuffer): Promise<ArrayBuffer> {
throw new errors.NotSupportedError('Not available on export-only mode');
}

public async setupBidirectional(_keySeed: ArrayBuffer, _nonceSeed: ArrayBuffer): Promise<void> {
throw new errors.NotSupportedError('Not available on export-only mode');
}

public async export(info: ArrayBuffer, len: number): Promise<ArrayBuffer> {
Expand Down
18 changes: 15 additions & 3 deletions src/interfaces/aeadParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@
*/
export interface AeadParams {

/** The algorithm identifier */
alg: string;

/** The length in bytes of a key for the algorithm */
nK: number;

/** The length in bytes of a nonce for the algorithm */
nN: number;

/** The length in bytes of an authentication tag for the algorithm */
nT: number;

/** A secret used for the secret export interface */
exporterSecret: ArrayBuffer;

/** A secret key */
key?: CryptoKey;

Expand All @@ -11,7 +26,4 @@ export interface AeadParams {

/** A sequence number */
seq?: number;

/** A secret used for the secret export interface */
exporterSecret: ArrayBuffer;
}
7 changes: 7 additions & 0 deletions src/interfaces/encryptionContextInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ export interface EncryptionContextInterface extends Exporter {
* data using a symmetric key and a nonce.
*/
open(data: ArrayBuffer, aad?: ArrayBuffer): Promise<ArrayBuffer>;

/**
* Sets up bi-directional encryption to allow a recipient to send
* encrypted messages to a sender.
*/
setupBidirectional(keySeed: ArrayBuffer, nonceSeed: ArrayBuffer): Promise<void>;

}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/interfaces/keyInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface KeyInfo {
key: CryptoKey;
baseNonce: Uint8Array;
seq: number;
}
1 change: 0 additions & 1 deletion src/kdfCommon.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { WebCrypto } from './webCrypto';
import { concat } from './utils';

import * as consts from './consts';

Expand Down
10 changes: 9 additions & 1 deletion src/kdfContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ export class KdfContext extends KdfCommon {

if (this._algAead === '') {
return {
alg: this._algAead,
nK: this._nK,
nN: this._nN,
nT: this._nT,
exporterSecret: exporterSecret,
};
}
Expand All @@ -111,10 +115,14 @@ export class KdfContext extends KdfCommon {
const baseNonce = await this.extractAndExpand(sharedSecret, ikm, baseNonceInfo, this._nN);

return {
alg: this._algAead,
nK: this._nK,
nN: this._nN,
nT: this._nT,
exporterSecret: exporterSecret,
key: await this._crypto.importKey('raw', key, { name: this._algAead }, true, consts.AEAD_USAGES),
baseNonce: new Uint8Array(baseNonce),
seq: 0,
exporterSecret: exporterSecret,
};
}
}
25 changes: 20 additions & 5 deletions src/recipientContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,37 @@ import * as errors from './errors';
export class RecipientContext extends EncryptionContext {

public async seal(data: ArrayBuffer, aad: ArrayBuffer = EMPTY): Promise<ArrayBuffer> {
throw new errors.NotSupportedError('Bidirectional encryption not supported');
if (this._r.baseNonce.length === 0) {
throw new errors.SealError("Bidirectional encryption is not setup");
}
let ct: ArrayBuffer;
try {
const alg = {
name: this._alg,
iv: this.computeNonce(this._r),
additionalData: aad,
};
ct = await this._crypto.encrypt(alg, this._r.key, data);
} catch (e: unknown) {
throw new errors.SealError(e);
}
this.incrementSeq(this._r);
return ct;
}

public async open(data: ArrayBuffer, aad: ArrayBuffer = EMPTY): Promise<ArrayBuffer> {
let pt: ArrayBuffer;
try {
const alg = {
name: 'AES-GCM',
iv: this.computeNonce(),
name: this._alg,
iv: this.computeNonce(this._f),
additionalData: aad,
};
pt = await this._crypto.decrypt(alg, this.key, data);
pt = await this._crypto.decrypt(alg, this._f.key, data);
} catch (e: unknown) {
throw new errors.OpenError(e);
}
this.incrementSeq();
this.incrementSeq(this._f);
return pt;
}
}
25 changes: 20 additions & 5 deletions src/senderContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,34 @@ export class SenderContext extends EncryptionContext implements Encapsulator {
let ct: ArrayBuffer;
try {
const alg = {
name: 'AES-GCM',
iv: this.computeNonce(),
name: this._alg,
iv: this.computeNonce(this._f),
additionalData: aad,
};
ct = await this._crypto.encrypt(alg, this.key, data);
ct = await this._crypto.encrypt(alg, this._f.key, data);
} catch (e: unknown) {
throw new errors.SealError(e);
}
this.incrementSeq();
this.incrementSeq(this._f);
return ct;
}

public async open(data: ArrayBuffer, aad: ArrayBuffer = EMPTY): Promise<ArrayBuffer> {
throw new errors.NotSupportedError('Bidirectional encryption not supported');
if (this._r.baseNonce.length === 0) {
throw new errors.SealError('Bidirectional encryption is not setup');
}
let pt: ArrayBuffer;
try {
const alg = {
name: this._alg,
iv: this.computeNonce(this._r),
additionalData: aad,
};
pt = await this._crypto.decrypt(alg, this._r.key, data);
} catch (e: unknown) {
throw new errors.OpenError(e);
}
this.incrementSeq(this._r);
return pt;
}
}
44 changes: 44 additions & 0 deletions test/cipherSuite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,48 @@ describe('CipherSuite', () => {
});
});

describe('bidirectional seal and open', () => {
it('should work normally', async () => {

const te = new TextEncoder();

// setup
const suite = new CipherSuite({
kem: Kem.DhkemP256HkdfSha256,
kdf: Kdf.HkdfSha256,
aead: Aead.Aes128Gcm,
});
const rkp = await suite.generateKeyPair();

const sender = await suite.createSenderContext({
recipientPublicKey: rkp.publicKey,
});

const recipient = await suite.createRecipientContext({
recipientKey: rkp,
enc: sender.enc,
});

// setup bidirectional encryption
await sender.setupBidirectional(te.encode('seed-for-key'), te.encode('seed-for-nonce'));
await recipient.setupBidirectional(te.encode('seed-for-key'), te.encode('seed-for-nonce'));

// encrypt
const ct = await sender.seal(te.encode('my-secret-message'));

// decrypt
const pt = await recipient.open(ct);

// encrypt reversely
const rct = await recipient.seal(te.encode('my-secret-message'));

// decrypt reversely
const rpt = await sender.open(rct);

// assert
expect(new TextDecoder().decode(pt)).toEqual('my-secret-message');
expect(new TextDecoder().decode(rpt)).toEqual('my-secret-message');
});
});

});
6 changes: 4 additions & 2 deletions test/conformanceTester.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ export class ConformanceTester extends WebCrypto {
const suite = new CipherSuite({ kem: v.kem_id, kdf: v.kdf_id, aead: v.aead_id });

// deriveKeyPair
const derived = await suite.deriveKey(ikmR.buffer);
expect(new Uint8Array(derived)).toEqual(skRm);
const derivedR = await suite.deriveKey(ikmR.buffer);
expect(new Uint8Array(derivedR)).toEqual(skRm);
const derivedE = await suite.deriveKey(ikmE.buffer);
expect(new Uint8Array(derivedE)).toEqual(skEm);

const sender = await suite.createSenderContext({
info: info,
Expand Down