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

Refactor @web5/crypto to replace Web Crypto CryptoKey with JWK #318

Merged
merged 18 commits into from
Nov 28, 2023
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
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
Loading