Skip to content

Commit

Permalink
Refactor @web5/crypto to replace Web Crypto CryptoKey with JWK (#318
Browse files Browse the repository at this point in the history
)

* Refactor Ed25519 to generateKey instead of generateKeyPair
* Refactor Secp256k1 to generateKey instead of generateKeyPair and simplify sign/verify
* Refactor X25519 to generateKey instead of generateKeyPair
* Refactor PBKDF2 to use JWKs
* Remove CryptoKeyToJwkMixin
* Improve test coverage for PBKDF2
* Refactor Ed25519, Secp256k1, and X25519 to use JWKs
* Refactor EcdhAlgorithm to use JWK
* Refactor EcdsaAlgorithm to use JWK
* Refactor EdDsaAlgorithm to use JWK
* Refactor AesCtrAlgorithm to use JWK
* Refactor AesCtrAlgorithm to JWK
* Refactor AesGcm to use JWK
* Bump @noble ciphers, curves, and hashes dependencies

---------

Signed-off-by: Frank Hinek <[email protected]>
  • Loading branch information
frankhinek authored Nov 28, 2023
1 parent c417ba0 commit 590a5fc
Show file tree
Hide file tree
Showing 70 changed files with 8,001 additions and 5,255 deletions.
7 changes: 3 additions & 4 deletions .web5-spec/credentials.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response } from 'express';
import { VerifiableCredential, SignOptions } from '@web5/credentials';
import { DidKeyMethod, PortableDid } from '@web5/dids';
import { Ed25519, Jose } from '@web5/crypto';
import { Ed25519, PrivateKeyJwk } from '@web5/crypto';
import { paths } from './openapi.js';

type Signer = (data: Uint8Array) => Promise<Uint8Array>;
Expand All @@ -24,9 +24,8 @@ export async function credentialIssue(req: Request, res: Response) {

// build signing options
const [signingKeyPair] = ownDid.keySet.verificationMethodKeys!;
const privateKey = (await Jose.jwkToKey({ key: signingKeyPair.privateKeyJwk!})).keyMaterial;
const subjectIssuerDid = body.credential.credentialSubject["id"] as string;
const signer = EdDsaSigner(privateKey);
const signer = EdDsaSigner(signingKeyPair.privateKeyJwk as PrivateKeyJwk);
const signOptions: SignOptions = {
issuerDid : ownDid.did,
subjectDid : subjectIssuerDid,
Expand All @@ -51,7 +50,7 @@ export async function credentialIssue(req: Request, res: Response) {
res.json(resp);
}

function EdDsaSigner(privateKey: Uint8Array): Signer {
function EdDsaSigner(privateKey: PrivateKeyJwk): Signer {
return async (data: Uint8Array): Promise<Uint8Array> => {
const signature = await Ed25519.sign({ data, key: privateKey});
return signature;
Expand Down
531 changes: 406 additions & 125 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@web5/crypto",
"version": "0.2.2",
"version": "0.2.3",
"description": "TBD crypto library",
"type": "module",
"main": "./dist/cjs/index.js",
Expand Down Expand Up @@ -73,9 +73,9 @@
"node": ">=18.0.0"
},
"dependencies": {
"@noble/ciphers": "0.1.4",
"@noble/curves": "1.1.0",
"@noble/hashes": "1.3.1",
"@noble/ciphers": "0.4.0",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.2",
"@web5/common": "0.2.1"
},
"devDependencies": {
Expand Down
59 changes: 37 additions & 22 deletions packages/crypto/src/algorithms-api/aes/base.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,64 @@
import { universalTypeOf } from '@web5/common';

import type { Web5Crypto } from '../../types/web5-crypto.js';
import type { JwkOperation, PrivateKeyJwk } from '../../jose.js';

import { Jose } from '../../jose.js';
import { InvalidAccessError } from '../errors.js';
import { checkRequiredProperty } from '../../utils.js';
import { CryptoAlgorithm } from '../crypto-algorithm.js';
import { InvalidAccessError, OperationError } from '../errors.js';

export abstract class BaseAesAlgorithm extends CryptoAlgorithm {

public checkGenerateKey(options: {
public checkGenerateKeyOptions(options: {
algorithm: Web5Crypto.AesGenerateKeyOptions,
keyUsages: Web5Crypto.KeyUsage[]
keyOperations: JwkOperation[]
}): void {
const { algorithm, keyUsages } = options;
const { algorithm, keyOperations } = options;

// Algorithm specified in the operation must match the algorithm implementation processing the operation.
this.checkAlgorithmName({ algorithmName: algorithm.name });
// The algorithm object must contain a length property.
checkRequiredProperty({ property: 'length', inObject: algorithm });
// The length specified must be a number.
if (universalTypeOf(algorithm.length) !== 'Number') {
throw new TypeError(`Algorithm 'length' is not of type: Number.`);

// If specified, key operations must be permitted by the algorithm implementation processing the operation.
if (keyOperations) {
this.checkKeyOperations({ keyOperations, allowedKeyOperations: this.keyOperations });
}
// The length specified must be one of the allowed bit lengths for AES.
if (![128, 192, 256].includes(algorithm.length)) {
throw new OperationError(`Algorithm 'length' must be 128, 192, or 256.`);
}

public checkSecretKey(options: {
key: PrivateKeyJwk
}): void {
const { key } = options;

// The options object must contain a key property.
checkRequiredProperty({ property: 'key', inObject: options });

// The key object must be a JSON Web key (JWK).
this.checkJwk({ key });

// The key object must be an octet sequence (oct) private key in JWK format.
if (!Jose.isOctPrivateKeyJwk(key)) {
throw new InvalidAccessError('Requested operation is only valid for oct private keys.');
}

// If specified, the key's algorithm must match the algorithm implementation processing the operation.
if (key.alg) {
this.checkKeyAlgorithm({ keyAlgorithmName: key.alg });
}
// The key usages specified must be permitted by the algorithm implementation processing the operation.
this.checkKeyUsages({ keyUsages, allowedKeyUsages: this.keyUsages });
}

public abstract generateKey(options: {
algorithm: Web5Crypto.AesGenerateKeyOptions,
extractable: boolean,
keyUsages: Web5Crypto.KeyUsage[]
}): Promise<Web5Crypto.CryptoKey>;
keyOperations: JwkOperation[]
}): Promise<PrivateKeyJwk>;

public override async deriveBits(): Promise<Uint8Array> {
throw new InvalidAccessError(`Requested operation 'deriveBits' is not valid for ${this.name} keys.`);
throw new InvalidAccessError(`Requested operation 'deriveBits' is not valid for AES algorithm.`);
}

public override async sign(): Promise<Uint8Array> {
throw new InvalidAccessError(`Requested operation 'sign' is not valid for ${this.name} keys.`);
throw new InvalidAccessError(`Requested operation 'sign' is not valid for AES algorithm.`);
}

public override async verify(): Promise<boolean> {
throw new InvalidAccessError(`Requested operation 'verify' is not valid for ${this.name} keys.`);
throw new InvalidAccessError(`Requested operation 'verify' is not valid for AES algorithm.`);
}
}
73 changes: 59 additions & 14 deletions packages/crypto/src/algorithms-api/aes/ctr.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,96 @@
import { universalTypeOf } from '@web5/common';

import type { Web5Crypto } from '../../types/web5-crypto.js';
import type { JwkOperation, PrivateKeyJwk } from '../../jose.js';

import { BaseAesAlgorithm } from './base.js';
import { OperationError } from '../errors.js';
import { checkRequiredProperty } from '../../utils.js';

export abstract class BaseAesCtrAlgorithm extends BaseAesAlgorithm {

public readonly name = 'AES-CTR';

public readonly keyUsages: Web5Crypto.KeyUsage[] = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'];
public readonly keyOperations: JwkOperation[] = ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'];

public checkAlgorithmOptions(options: {
algorithm: Web5Crypto.AesCtrOptions,
key: Web5Crypto.CryptoKey
algorithm: Web5Crypto.AesCtrOptions
}): void {
const { algorithm, key } = options;
const { algorithm } = options;

// Algorithm specified in the operation must match the algorithm implementation processing the operation.
this.checkAlgorithmName({ algorithmName: algorithm.name });

// The algorithm object must contain a counter property.
checkRequiredProperty({ property: 'counter', inObject: algorithm });

// The counter must a Uint8Array.
if (!(universalTypeOf(algorithm.counter) === 'Uint8Array')) {
throw new TypeError(`Algorithm 'counter' is not of type: Uint8Array.`);
}

// The initial value of the counter block must be 16 bytes long (the AES block size).
if (algorithm.counter.byteLength !== 16) {
throw new OperationError(`Algorithm 'counter' must have length: 16 bytes.`);
}

// The algorithm object must contain a length property.
checkRequiredProperty({ property: 'length', inObject: algorithm });

// The length specified must be a number.
if (universalTypeOf(algorithm.length) !== 'Number') {
throw new TypeError(`Algorithm 'length' is not of type: Number.`);
}

// The length specified must be between 1 and 128.
if ((algorithm.length < 1 || algorithm.length > 128)) {
throw new OperationError(`Algorithm 'length' should be in the range: 1 to 128.`);
}
// The options object must contain a key property.
checkRequiredProperty({ property: 'key', inObject: options });
// The key object must be a CryptoKey.
this.checkCryptoKey({ key });
// The key algorithm must match the algorithm implementation processing the operation.
this.checkKeyAlgorithm({ keyAlgorithmName: key.algorithm.name });
// The CryptoKey object must be a secret key.
this.checkKeyType({ keyType: key.type, allowedKeyType: 'secret' });
}

public checkDecryptOptions(options: {
algorithm: Web5Crypto.AesCtrOptions,
key: PrivateKeyJwk,
data: Uint8Array
}): void {
const { algorithm, key, data } = options;

// Validate the algorithm input parameters.
this.checkAlgorithmOptions({ algorithm });

// Validate the secret key.
this.checkSecretKey({ key });

// If specified, the secret key must be allowed to be used for 'decrypt' operations.
if (key.key_ops) {
this.checkKeyOperations({ keyOperations: ['decrypt'], allowedKeyOperations: key.key_ops });
}

// The data must be a Uint8Array.
if (universalTypeOf(data) !== 'Uint8Array') {
throw new TypeError('The data must be of type Uint8Array.');
}
}

public checkEncryptOptions(options: {
algorithm: Web5Crypto.AesCtrOptions,
key: PrivateKeyJwk,
data: Uint8Array
}): void {
const { algorithm, key, data } = options;

// Validate the algorithm and key input parameters.
this.checkAlgorithmOptions({ algorithm });

// Validate the secret key.
this.checkSecretKey({ key });

// If specified, the secret key must be allowed to be used for 'encrypt' operations.
if (key.key_ops) {
this.checkKeyOperations({ keyOperations: ['encrypt'], allowedKeyOperations: key.key_ops });
}

// The data must be a Uint8Array.
if (universalTypeOf(data) !== 'Uint8Array') {
throw new TypeError('The data must be of type Uint8Array.');
}
}
}
Loading

0 comments on commit 590a5fc

Please sign in to comment.