Skip to content

Commit

Permalink
Merge pull request #50 from dajiaji/add-x448
Browse files Browse the repository at this point in the history
Add support for DHKEM(X448, HKDF-SHA512).
  • Loading branch information
dajiaji authored May 28, 2022
2 parents 468d4eb + 6d51a56 commit b8903f1
Show file tree
Hide file tree
Showing 17 changed files with 6,777 additions and 37 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ This library works on both web browsers and Node.js (<b>currently, Deno is not s
| DHKEM (P-384, HKDF-SHA384) ||| |
| DHKEM (P-521, HKDF-SHA512) ||| |
| DHKEM (X25519, HKDF-SHA256) ||| |
| DHKEM (X448, HKDF-SHA512) | | | |
| DHKEM (X448, HKDF-SHA512) | | | |

### Key Derivation Functions (KDFs)

Expand Down
29 changes: 28 additions & 1 deletion package-lock.json

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

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
},
"dependencies": {
"@stablelib/chacha20poly1305": "^1.0.1",
"@stablelib/x25519": "^1.0.2"
"@stablelib/x25519": "^1.0.2",
"x448-js": "^1.0.3"
}
}
1 change: 1 addition & 0 deletions src/cipherSuite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class CipherSuite {
case Kem.DhkemP384HkdfSha384:
case Kem.DhkemP521HkdfSha512:
case Kem.DhkemX25519HkdfSha256:
case Kem.DhkemX448HkdfSha512:
break;
default:
throw new errors.InvalidParamError('Invalid KEM id');
Expand Down
4 changes: 2 additions & 2 deletions src/identifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export enum Kem {
DhkemP521HkdfSha512 = 0x0012,
/** DHKEM (X25519, HKDF-SHA256) */
DhkemX25519HkdfSha256 = 0x0020,
// /** DHKEM (X448, HKDF-SHA512) */
// DhkemX448HkdfSha512 = 0x0021,
/** DHKEM (X448, HKDF-SHA512) */
DhkemX448HkdfSha512 = 0x0021,
}

/**
Expand Down
16 changes: 12 additions & 4 deletions src/kemContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { RecipientContextParams } from './interfaces/recipientContextParams

import { Ec } from './kemPrimitives/ec';
import { X25519 } from './kemPrimitives/x25519';
import { X448 } from './kemPrimitives/x448';
import { Kem } from './identifiers';
import { KdfCommon } from './kdfCommon';
import { isCryptoKeyPair, i2Osp, concat, concat3 } from './utils/misc';
Expand Down Expand Up @@ -31,10 +32,13 @@ export class KemContext extends KdfCommon {
case Kem.DhkemP521HkdfSha512:
algHash = { name: 'HMAC', hash: 'SHA-512', length: 512 };
break;
default:
// case Kem.DhkemX25519HkdfSha256:
case Kem.DhkemX25519HkdfSha256:
algHash = { name: 'HMAC', hash: 'SHA-256', length: 256 };
break;
default:
// case Kem.DhkemX448HkdfSha512:
algHash = { name: 'HMAC', hash: 'SHA-512', length: 512 };
break;
}
super(api, suiteId, algHash);

Expand All @@ -51,11 +55,15 @@ export class KemContext extends KdfCommon {
this._prim = new Ec(kem, this, this._api);
this._nSecret = 64;
break;
default:
// case Kem.DhkemX25519HkdfSha256:
case Kem.DhkemX25519HkdfSha256:
this._prim = new X25519(this);
this._nSecret = 32;
break;
default:
// case Kem.DhkemX448HkdfSha512:
this._prim = new X448(this);
this._nSecret = 64;
break;
}
return;
}
Expand Down
27 changes: 8 additions & 19 deletions src/kemPrimitives/x25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,11 @@ import type { KdfCommon } from '../kdfCommon';

import { Kem } from '../identifiers';
import { i2Osp } from '../utils/misc';
import { XCryptoKey } from '../xCryptoKey';

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

export class XCryptoKey implements CryptoKey {

public readonly key: Uint8Array;
public readonly type: 'public' | 'private';
public readonly extractable: boolean = true;
public readonly algorithm: KeyAlgorithm = { name: 'X25519' };
public readonly usages: KeyUsage[] = consts.KEM_USAGES;

constructor(key: Uint8Array, type: 'public' | 'private') {
this.key = key;
this.type = type;
}
}
const ALG_NAME = 'X25519';

export class X25519 implements KemPrimitives {

Expand Down Expand Up @@ -60,7 +49,7 @@ export class X25519 implements KemPrimitives {
public async deriveKeyPair(ikm: ArrayBuffer): Promise<CryptoKeyPair> {
const dkpPrk = await this._hkdf.labeledExtract(consts.EMPTY, consts.LABEL_DKP_PRK, new Uint8Array(ikm));
const rawSk = await this._hkdf.labeledExpand(dkpPrk, consts.LABEL_SK, consts.EMPTY, this._nSk);
const sk = new XCryptoKey(new Uint8Array(rawSk), 'private');
const sk = new XCryptoKey(ALG_NAME, new Uint8Array(rawSk), 'private');
return {
privateKey: sk,
publicKey: await this.derivePublicKey(sk),
Expand All @@ -82,7 +71,7 @@ export class X25519 implements KemPrimitives {
if (k.byteLength !== this._nPk) {
reject(new Error('Invalid public key for the ciphersuite'));
} else {
resolve(new XCryptoKey(new Uint8Array(k), 'public'));
resolve(new XCryptoKey(ALG_NAME, new Uint8Array(k), 'public'));
}
});
}
Expand All @@ -95,22 +84,22 @@ export class X25519 implements KemPrimitives {
if (!isPublic && key.byteLength !== this._nSk) {
reject(new Error('Invalid private key for the ciphersuite'));
}
resolve(new XCryptoKey(new Uint8Array(key), isPublic ? 'public' : 'private'));
resolve(new XCryptoKey(ALG_NAME, new Uint8Array(key), isPublic ? 'public' : 'private'));
});
}

private _derivePublicKey(k: XCryptoKey): Promise<CryptoKey> {
return new Promise((resolve) => {
resolve(new XCryptoKey(scalarMultBase(k.key), 'public'));
resolve(new XCryptoKey(ALG_NAME, scalarMultBase(k.key), 'public'));
});
}

private _generateKeyPair(): Promise<CryptoKeyPair> {
return new Promise((resolve) => {
const kp = generateKeyPair();
resolve({
publicKey: new XCryptoKey(kp.publicKey, 'public'),
privateKey: new XCryptoKey(kp.secretKey, 'private'),
publicKey: new XCryptoKey(ALG_NAME, kp.publicKey, 'public'),
privateKey: new XCryptoKey(ALG_NAME, kp.secretKey, 'private'),
});
});
}
Expand Down
108 changes: 108 additions & 0 deletions src/kemPrimitives/x448.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { getPublicKey, getSharedSecret } from 'x448-js';

import type { KemPrimitives } from '../interfaces/kemPrimitives';
import type { KdfCommon } from '../kdfCommon';

import { loadCrypto } from '../webCrypto';
import { XCryptoKey } from '../xCryptoKey';

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

const ALG_NAME = 'X448';

export class X448 implements KemPrimitives {

private _hkdf: KdfCommon;
private _nPk: number;
private _nSk: number;

constructor(hkdf: KdfCommon) {
this._hkdf = hkdf;
this._nPk = 56;
this._nSk = 56;
}

public async serializePublicKey(key: CryptoKey): Promise<ArrayBuffer> {
return await this._serializePublicKey(key as XCryptoKey);
}

public async deserializePublicKey(key: ArrayBuffer): Promise<CryptoKey> {
return await this._deserializePublicKey(key);
}

public async importKey(format: 'raw', key: ArrayBuffer, isPublic: boolean): Promise<CryptoKey> {
if (format !== 'raw') {
throw new Error('Unsupported format');
}
return await this._importKey(key, isPublic);
}

public async derivePublicKey(key: CryptoKey): Promise<CryptoKey> {
return await this._derivePublicKey(key as XCryptoKey);
}

public async generateKeyPair(): Promise<CryptoKeyPair> {
const sk = new Uint8Array(56);
const cryptoApi = await loadCrypto();
cryptoApi.getRandomValues(sk);
return await this.deriveKeyPair(sk);
}

public async deriveKeyPair(ikm: ArrayBuffer): Promise<CryptoKeyPair> {
const dkpPrk = await this._hkdf.labeledExtract(consts.EMPTY, consts.LABEL_DKP_PRK, new Uint8Array(ikm));
const rawSk = await this._hkdf.labeledExpand(dkpPrk, consts.LABEL_SK, consts.EMPTY, this._nSk);
const sk = new XCryptoKey(ALG_NAME, new Uint8Array(rawSk), 'private');
return {
privateKey: sk,
publicKey: await this.derivePublicKey(sk),
};
}

public async dh(sk: CryptoKey, pk: CryptoKey): Promise<ArrayBuffer> {
return await this._dh(sk as XCryptoKey, pk as XCryptoKey);
}

private _serializePublicKey(k: XCryptoKey): Promise<ArrayBuffer> {
return new Promise((resolve) => {
resolve(k.key.buffer);
});
}

private _deserializePublicKey(k: ArrayBuffer): Promise<CryptoKey> {
return new Promise((resolve, reject) => {
if (k.byteLength !== this._nPk) {
reject(new Error('Invalid public key for the ciphersuite'));
} else {
resolve(new XCryptoKey(ALG_NAME, new Uint8Array(k), 'public'));
}
});
}

private _importKey(key: ArrayBuffer, isPublic: boolean): Promise<CryptoKey> {
return new Promise((resolve, reject) => {
if (isPublic && key.byteLength !== this._nPk) {
reject(new Error('Invalid public key for the ciphersuite'));
}
if (!isPublic && key.byteLength !== this._nSk) {
reject(new Error('Invalid private key for the ciphersuite'));
}
resolve(new XCryptoKey(ALG_NAME, new Uint8Array(key), isPublic ? 'public' : 'private'));
});
}

private _derivePublicKey(k: XCryptoKey): Promise<CryptoKey> {
return new Promise((resolve) => {
resolve(new XCryptoKey(ALG_NAME, Uint8Array.from(getPublicKey(k.key)), 'public'));
});
}

private _dh(sk: XCryptoKey, pk: XCryptoKey): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
try {
resolve(Uint8Array.from(getSharedSecret(sk.key, pk.key)));
} catch (e: unknown) {
reject(e);
}
});
}
}
27 changes: 19 additions & 8 deletions src/webCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,28 @@ export class WebCrypto {
}
}

export async function loadCrypto(): Promise<Crypto> {
if (isBrowser()) {
if (window.crypto !== undefined) {
return window.crypto;
}
// jsdom
}

try {
const { webcrypto } = await import('node:crypto');
return (webcrypto as unknown as Crypto);
} catch (e: unknown) {
throw new errors.NotSupportedError('Web Cryptograph API not supported');
}
}

export async function loadSubtleCrypto(): Promise<SubtleCrypto> {
if (isBrowser()) {
if (window.crypto === undefined) {
try {
const crypto = await import('crypto');
Object.defineProperty(global.self, 'crypto', { value: crypto.webcrypto });
} catch (e: unknown) {
throw new errors.NotSupportedError('Web Cryptograph API not supported');
}
if (window.crypto !== undefined) {
return window.crypto.subtle;
}
return window.crypto.subtle;
// jsdom
}

try {
Expand Down
16 changes: 16 additions & 0 deletions src/xCryptoKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as consts from './consts';

export class XCryptoKey implements CryptoKey {

public readonly key: Uint8Array;
public readonly type: 'public' | 'private';
public readonly extractable: boolean = true;
public readonly algorithm: KeyAlgorithm;
public readonly usages: KeyUsage[] = consts.KEM_USAGES;

constructor(name: string, key: Uint8Array, type: 'public' | 'private') {
this.key = key;
this.type = type;
this.algorithm = { name: name };
}
}
Loading

0 comments on commit b8903f1

Please sign in to comment.