diff --git a/.github/workflows/crypto-utils-tests.yml b/.github/workflows/crypto-utils-tests.yml new file mode 100644 index 000000000..b460a6d70 --- /dev/null +++ b/.github/workflows/crypto-utils-tests.yml @@ -0,0 +1,15 @@ +name: Crypto Utils tests + +on: [pull_request] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v4 + with: + node-version: "22.x" + - run: yarn install --frozen-lockfile --ignore-scripts + - run: npm install -g turbo@2.0.5 + - run: turbo telemetry disable + - run: turbo run build test --filter @docknetwork/crypto-utils diff --git a/examples/CHANGELOG.md b/examples/CHANGELOG.md index 335b983d7..425125266 100644 --- a/examples/CHANGELOG.md +++ b/examples/CHANGELOG.md @@ -1,5 +1,14 @@ # @docknetwork/sdk-examples +## 0.22.7 + +### Patch Changes + +- Updated dependencies + - @docknetwork/credential-sdk@0.56.0 + - @docknetwork/cheqd-blockchain-api@6.0.0 + - @docknetwork/cheqd-blockchain-modules@6.0.0 + ## 0.22.6 ### Minor Changes diff --git a/examples/package.json b/examples/package.json index 2efd52d11..764b6a6d6 100644 --- a/examples/package.json +++ b/examples/package.json @@ -2,7 +2,7 @@ "name": "@docknetwork/sdk-examples", "private": true, "type": "module", - "version": "0.22.6", + "version": "0.22.7", "scripts": { "bbs-dock-example": "babel-node ./src/bbs-dock.js", "claim-deduction-example": "babel-node ./src/claim-deduction.js", @@ -18,9 +18,9 @@ "lint": "eslint \"src/**/*.js\"" }, "dependencies": { - "@docknetwork/credential-sdk": "0.55.0", - "@docknetwork/cheqd-blockchain-api": "5.1.0", - "@docknetwork/cheqd-blockchain-modules": "5.0.0" + "@docknetwork/credential-sdk": "0.56.0", + "@docknetwork/cheqd-blockchain-api": "6.0.0", + "@docknetwork/cheqd-blockchain-modules": "6.0.0" }, "devDependencies": { "babel-eslint": "^10.1.0", diff --git a/packages/cheqd-blockchain-api/CHANGELOG.md b/packages/cheqd-blockchain-api/CHANGELOG.md index e4ef20c0f..fde7679c2 100644 --- a/packages/cheqd-blockchain-api/CHANGELOG.md +++ b/packages/cheqd-blockchain-api/CHANGELOG.md @@ -1,12 +1,18 @@ # @docknetwork/cheqd-blockchain-api +## 6.0.0 + +### Patch Changes + +- Updated dependencies + - @docknetwork/credential-sdk@0.56.0 + ## 5.1.0 ### Minor Changes - Upgrade Cheqd SDK - ## 5.0.0 ### Patch Changes diff --git a/packages/cheqd-blockchain-api/package.json b/packages/cheqd-blockchain-api/package.json index e958965c2..de6aec52b 100644 --- a/packages/cheqd-blockchain-api/package.json +++ b/packages/cheqd-blockchain-api/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/cheqd-blockchain-api", - "version": "5.1.0", + "version": "6.0.0", "license": "MIT", "main": "./dist/esm/index.js", "type": "module", @@ -32,7 +32,7 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@docknetwork/credential-sdk": "^0.55.0" + "@docknetwork/credential-sdk": "^0.56.0" }, "dependencies": { "@cheqd/sdk": "5.5.1", @@ -46,7 +46,7 @@ "@babel/plugin-transform-modules-commonjs": "^7.24.1", "@babel/preset-env": "^7.24.3", "@comunica/types": "^1.0.0", - "@docknetwork/credential-sdk": "^0.55.0", + "@docknetwork/credential-sdk": "^0.56.0", "@helia/strings": "^3.0.1", "@rollup/plugin-alias": "^4.0.2", "@rollup/plugin-commonjs": "^24.0.0", diff --git a/packages/cheqd-blockchain-modules/CHANGELOG.md b/packages/cheqd-blockchain-modules/CHANGELOG.md index 4065dc07f..871e96718 100644 --- a/packages/cheqd-blockchain-modules/CHANGELOG.md +++ b/packages/cheqd-blockchain-modules/CHANGELOG.md @@ -1,5 +1,12 @@ # @docknetwork/cheqd-blockchain-modules +## 6.0.0 + +### Patch Changes + +- Updated dependencies + - @docknetwork/credential-sdk@0.56.0 + ## 5.0.0 ### Patch Changes diff --git a/packages/cheqd-blockchain-modules/package.json b/packages/cheqd-blockchain-modules/package.json index 13fbffc2d..b07b2178c 100644 --- a/packages/cheqd-blockchain-modules/package.json +++ b/packages/cheqd-blockchain-modules/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/cheqd-blockchain-modules", - "version": "5.0.0", + "version": "6.0.0", "type": "module", "license": "MIT", "main": "./dist/esm/index.js", @@ -32,7 +32,7 @@ "node": ">=22.0.0" }, "peerDependencies": { - "@docknetwork/credential-sdk": "^0.55.0" + "@docknetwork/credential-sdk": "^0.56.0" }, "devDependencies": { "@babel/cli": "^7.24.1", @@ -41,8 +41,8 @@ "@babel/plugin-syntax-import-attributes": "^7.25.6", "@babel/plugin-transform-modules-commonjs": "^7.24.1", "@babel/preset-env": "^7.24.3", - "@docknetwork/cheqd-blockchain-api": "5.1.0", - "@docknetwork/credential-sdk": "^0.55.0", + "@docknetwork/cheqd-blockchain-api": "6.0.0", + "@docknetwork/credential-sdk": "^0.56.0", "@rollup/plugin-alias": "^4.0.2", "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-commonjs": "^24.0.0", diff --git a/packages/credential-sdk/CHANGELOG.md b/packages/credential-sdk/CHANGELOG.md index 4fedd68d8..ed483ca2f 100644 --- a/packages/credential-sdk/CHANGELOG.md +++ b/packages/credential-sdk/CHANGELOG.md @@ -1,5 +1,16 @@ # @docknetwork/credential-sdk +## 0.56.0 + +### Minor Changes + +- Create crypto-utils package + +### Patch Changes + +- Updated dependencies + - @docknetwork/crypto-utils@0.2.0 + ## 0.55.0 ### Minor Changes diff --git a/packages/credential-sdk/jest.config.js b/packages/credential-sdk/jest.config.js index 40abd6c4d..0707a6c23 100644 --- a/packages/credential-sdk/jest.config.js +++ b/packages/credential-sdk/jest.config.js @@ -1,6 +1,9 @@ export default { bail: true, - moduleNameMapper: {}, + moduleNameMapper: { + "^@docknetwork/crypto-utils$": "/../crypto-utils/src/index.js", + "^@docknetwork/crypto-utils/(.*)$": "/../crypto-utils/src/$1", + }, clearMocks: true, testTimeout: 30000, testEnvironment: "./tests/test-environment", diff --git a/packages/credential-sdk/package.json b/packages/credential-sdk/package.json index bcd0c02b8..3c79f36eb 100644 --- a/packages/credential-sdk/package.json +++ b/packages/credential-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@docknetwork/credential-sdk", - "version": "0.55.0", + "version": "0.56.0", "license": "MIT", "type": "module", "files": [ @@ -17,6 +17,11 @@ "require": "./dist/cjs/*/index.cjs", "default": "./dist/esm/*/index.js" }, + "./src/vc/helpers": { + "import": "./dist/esm/vc/helpers.js", + "require": "./dist/cjs/vc/helpers.cjs", + "default": "./dist/esm/vc/helpers.js" + }, "./src/*": { "import": "./dist/esm/*", "require": "./dist/cjs/*", @@ -38,18 +43,18 @@ "dependencies": { "@astronautlabs/jsonpath": "^1.1.2", "@digitalcredentials/vc-status-list": "^9.0.0", + "@docknetwork/crypto-utils": "0.2.0", "@docknetwork/crypto-wasm-ts": "^0.63.0", "@docknetwork/vc-delegation-engine": "1.0.4", "@juanelas/base64": "^1.1.5", "@sphereon/pex": "^5.0.0-unstable.28", "@sphereon/ssi-sdk-ext.did-resolver-jwk": "^0.26.0", - "@stablelib/ed25519": "^1.0.0", "@transmute/json-web-signature": "^0.7.0-unstable.82", + "base64url": "^3.0.1", "blake2b": "2.1.4", "blakejs": "^1.2.1", "bs58": "^5.0.0", "deep-equal": "^2.2.3", - "elliptic": "6.6.1", "js-sha256": "^0.11.0", "json-canonicalize": "1.0.4", "json-stringify-deterministic": "^1.0.12", diff --git a/packages/credential-sdk/src/keypairs/dock-keypair.js b/packages/credential-sdk/src/keypairs/dock-keypair.js index 0d8bd2c3c..b054d522f 100644 --- a/packages/credential-sdk/src/keypairs/dock-keypair.js +++ b/packages/credential-sdk/src/keypairs/dock-keypair.js @@ -1,123 +1 @@ -import { - withExtendedPrototypeProperties, - withExtendedStaticProperties, -} from '../utils'; -import { randomAsHex } from '../utils/types/bytes'; - -/** - * Abstract keypair used to sign byte sequences of arbitrary size. - */ -class DockKeypair { - /** - * Instantiates new `DockKeypair` with the provided underlying keypair. - * - * @param {*} keyPair - */ - constructor(keyPair) { - this.keyPair = keyPair; - } - - /** - * Generates `DockKeypair` from the supplied entropy. - * @param {Uint8Array} entropy - * @returns {DockKeypair} - */ - static fromEntropy(entropy) { - // eslint-disable-next-line no-underscore-dangle - return new this(entropy, 'entropy'); - } - - /** - * Generates `DockKeypair` from the supplied seed. - * @param {Uint8Array} seed - * @returns {DockKeypair} - */ - static fromSeed(seed) { - // eslint-disable-next-line no-underscore-dangle - return new this(seed, 'seed'); - } - - /** - * Generates `DockKeypair` from the supplied private key. - * @param {Uint8Array} privateKey - * @returns {DockKeypair} - */ - static fromPrivateKey(privateKey) { - // eslint-disable-next-line no-underscore-dangle - return new this(privateKey, 'private'); - } - - /** - * Generates random `DockKeypair`. - * @returns {DockKeypair} - */ - static random() { - return new this(randomAsHex(this.SeedSize)); - } - - /** - * Returns underlying verification key type. - */ - get verKeyType() { - return this.constructor.VerKeyType; - } - - /** - * Returns underlying public key. - */ - publicKey() { - // eslint-disable-next-line no-underscore-dangle - return new this.constructor.Signature.PublicKey(this._publicKey()); - } - - /** - * Returns underlying private key. - */ - privateKey() { - throw new Error('Unimplemented'); - } - - /** - * Signs supplied message using underlying keypair. - * @param {*} message - */ - sign(message) { - // eslint-disable-next-line no-underscore-dangle - return new this.constructor.Signature(this._sign(message)); - } - - /** - * Returns raw bytes of the underlying public key. - * @returns {Uint8Array} - */ - _publicKey() { - throw new Error('Unimplemented'); - } - - /** - * Signs supplied message and returns raw signature bytes. - * @returns {Uint8Array} - */ - _sign(_message) { - throw new Error('Unimplemented'); - } - - /** - * Verifies signature produced over supplied message's using given public key. - * @param {Uint8Array} _message - * @param {Uint8Array | Signature} _signature - * @param {Uint8Array | PublicKey} _publicKey - * @returns {boolean} - */ - static verify(_message, _signature, _publicKey) { - throw new Error('Unimplemented'); - } -} - -export default withExtendedPrototypeProperties( - ['privateKey', '_publicKey', '_sign'], - withExtendedStaticProperties( - ['Signature', 'VerKeyType', 'SeedSize', 'verify', 'constructor'], - DockKeypair, - ), -); +export { DockKeypair as default } from '@docknetwork/crypto-utils/keypairs'; diff --git a/packages/credential-sdk/src/keypairs/index.js b/packages/credential-sdk/src/keypairs/index.js index bc57c7ecd..a47f90912 100644 --- a/packages/credential-sdk/src/keypairs/index.js +++ b/packages/credential-sdk/src/keypairs/index.js @@ -1,5 +1,7 @@ -export { default as DockKeypair } from './dock-keypair'; -export { default as Ed25519Keypair } from './keypair-ed25519'; -export { default as Secp256k1Keypair } from './keypair-secp256k1'; -export { default as Secp256r1Keypair } from './keypair-secp256r1'; +export { + DockKeypair, + Ed25519Keypair, + Secp256k1Keypair, + Secp256r1Keypair, +} from '@docknetwork/crypto-utils/keypairs'; export { default as DidKeypair } from './did-keypair'; diff --git a/packages/credential-sdk/src/keypairs/keypair-ed25519.js b/packages/credential-sdk/src/keypairs/keypair-ed25519.js index d49ed2bfe..da7c89674 100644 --- a/packages/credential-sdk/src/keypairs/keypair-ed25519.js +++ b/packages/credential-sdk/src/keypairs/keypair-ed25519.js @@ -1,65 +1 @@ -import { - generateKeyPairFromSeed, - sign, - verify, - extractPublicKeyFromSecretKey, -} from '@stablelib/ed25519'; -import { SignatureEd25519 } from '../types/signatures'; -import { Ed25519VerKeyName } from '../vc/crypto/constants'; -import { normalizeToU8a, valueBytes } from '../utils'; -import DockKeypair from './dock-keypair'; - -export default class Ed25519Keypair extends DockKeypair { - static Signature = SignatureEd25519; - - static VerKeyType = Ed25519VerKeyName; - - static SeedSize = 32; - - /** - * - * Instantiates new `Ed25519Keypair` from the provided source. - * It can have one of two types: "seed" or "private". - * - * @param {Uint8Array} seedOrPrivate - * @param {"seed"|"private"} sourceType - */ - constructor(seedOrPrivate, sourceType = 'seed') { - let kp; - switch (sourceType) { - case 'seed': - kp = generateKeyPairFromSeed(normalizeToU8a(seedOrPrivate)); - break; - case 'private': { - const secretKey = normalizeToU8a(seedOrPrivate); - kp = { - secretKey, - publicKey: extractPublicKeyFromSecretKey(secretKey), - }; - break; - } - default: - throw new Error( - `Unsupported source type: \`${sourceType}\`, it must be either "seed" or "private"`, - ); - } - - super(kp); - } - - _publicKey() { - return this.keyPair.publicKey; - } - - privateKey() { - return this.keyPair.secretKey; - } - - _sign(message) { - return sign(this.keyPair.secretKey, message); - } - - static verify(message, signature, publicKey) { - return verify(valueBytes(publicKey), message, valueBytes(signature)); - } -} +export { Ed25519Keypair as default } from '@docknetwork/crypto-utils/keypairs'; diff --git a/packages/credential-sdk/src/keypairs/keypair-secp256k1.js b/packages/credential-sdk/src/keypairs/keypair-secp256k1.js index 43a5f59fe..2310aff4b 100644 --- a/packages/credential-sdk/src/keypairs/keypair-secp256k1.js +++ b/packages/credential-sdk/src/keypairs/keypair-secp256k1.js @@ -1,140 +1 @@ -import elliptic from 'elliptic'; -import { sha256 } from 'js-sha256'; - -import { SignatureSecp256k1 } from '../types/signatures'; -import { EcdsaSecp256k1VerKeyName } from '../vc/crypto/constants'; -import DockKeypair from './dock-keypair'; -import { hexToU8a, normalizeToU8a, valueBytes } from '../utils'; - -const EC = elliptic.ec; -const secp256k1Curve = new EC('secp256k1'); - -function encodeDERInt(intBytes) { - // Remove leading zeros - let i = 0; - while (i < intBytes.length && intBytes[i] === 0) { - i++; - } - - let trimmedBytes = intBytes.slice(i); - - // Ensure the integer is positive by adding a leading zero if needed - // eslint-disable-next-line no-bitwise - if (trimmedBytes[0] & 0x80) { - trimmedBytes = Uint8Array.of(0, ...trimmedBytes); - } - - const { length } = trimmedBytes; - return [0x02, length, ...trimmedBytes]; -} - -export default class Secp256k1Keypair extends DockKeypair { - static Signature = SignatureSecp256k1; - - static VerKeyType = EcdsaSecp256k1VerKeyName; - - static SeedSize = 32; - - /** - * - * Instantiates new `Secp256k1Keypair` from the provided source. - * It can have one of two types: "entropy" or "private". - * - * @param {Uint8Array} entropyOrPrivate - * @param {"entropy"|"private"} sourceType - */ - constructor(entropyOrPrivate, sourceType = 'entropy') { - let kp; - switch (sourceType) { - case 'entropy': - if (entropyOrPrivate == null) { - throw new Error('Entropy must be provided'); - } - - kp = secp256k1Curve.genKeyPair({ - entropy: entropyOrPrivate, - }); - break; - case 'private': - kp = secp256k1Curve.keyFromPrivate(normalizeToU8a(entropyOrPrivate)); - break; - default: - throw new Error( - `Unknown source type: \`${sourceType}\`, it must be either "entropy" or "private"`, - ); - } - - super(kp); - } - - _publicKey() { - // public key is in hex but doesn't contain a leading zero - return hexToU8a(`0x${this.keyPair.getPublic(true, 'hex')}`); - } - - privateKey() { - return hexToU8a(`0x${this.keyPair.getPrivate('hex')}`); - } - - _sign(message) { - const hash = this.constructor.hash(message); - const sig = this.keyPair.sign(hash, { - canonical: true, - }); - - // The signature is recoverable in 65-byte { R | S | index } format - const r = sig.r.toString('hex', 32); - const s = sig.s.toString('hex', 32); - const i = sig.recoveryParam.toString(16).padStart(2, '0'); - - // Make it proper hex - return hexToU8a(`0x${r}${s}${i}`); - } - - static signatureToDER(signature) { - const sigBytes = valueBytes(signature); - if (sigBytes.length !== 65) { - throw new Error( - `Invalid signature length. Expected is 65 bytes, received ${sigBytes.length}`, - ); - } - // Extract R, S components - const r = sigBytes.slice(0, 32); // First 32 bytes - const s = sigBytes.slice(32, 64); // Next 32 bytes - - // Encode R and S into DER format - const rEncoded = encodeDERInt(r); - const sEncoded = encodeDERInt(s); - - // Calculate total length - const totalLength = rEncoded.length + sEncoded.length; - - // Create Uint8Array for DER encoding - const der = new Uint8Array(2 + totalLength); // 2 bytes for tag and length + total payload - der[0] = 0x30; // DER sequence tag - der[1] = totalLength; // Total length of R + S encoded - - // Copy encoded R and S into DER array - der.set(rEncoded, 2); - der.set(sEncoded, 2 + rEncoded.length); - - return der; - } - - static hash(message) { - return sha256.digest(message); - } - - static verify(message, signature, publicKey) { - let bytes = valueBytes(signature); - if (bytes.length === 65) { - bytes = this.signatureToDER(bytes); - } - - return secp256k1Curve.verify( - this.hash(message), - bytes, - valueBytes(publicKey), - ); - } -} +export { Secp256k1Keypair as default } from '@docknetwork/crypto-utils/keypairs'; diff --git a/packages/credential-sdk/src/keypairs/keypair-secp256r1.js b/packages/credential-sdk/src/keypairs/keypair-secp256r1.js index 497404d24..c0e8f8ae0 100644 --- a/packages/credential-sdk/src/keypairs/keypair-secp256r1.js +++ b/packages/credential-sdk/src/keypairs/keypair-secp256r1.js @@ -1,123 +1 @@ -import elliptic from 'elliptic'; -import { sha256 } from 'js-sha256'; - -import { SignatureSecp256r1 } from '../types/signatures'; -import { EcdsaSecp256r1VerKeyName } from '../vc/crypto/constants'; -import DockKeypair from './dock-keypair'; -import { hexToU8a, normalizeToU8a, valueBytes } from '../utils'; - -const EC = elliptic.ec; -const secp256r1Curve = new EC('p256'); - -function encodeDERInt(intBytes) { - let i = 0; - while (i < intBytes.length && intBytes[i] === 0) { - i++; - } - - let trimmedBytes = intBytes.slice(i); - - // eslint-disable-next-line no-bitwise - if (trimmedBytes[0] & 0x80) { - trimmedBytes = Uint8Array.of(0, ...trimmedBytes); - } - - const { length } = trimmedBytes; - return [0x02, length, ...trimmedBytes]; -} - -export default class Secp256r1Keypair extends DockKeypair { - static Signature = SignatureSecp256r1; - - static VerKeyType = EcdsaSecp256r1VerKeyName; - - static SeedSize = 32; - - constructor(entropyOrPrivate, sourceType = 'entropy') { - let kp; - switch (sourceType) { - case 'entropy': - if (entropyOrPrivate == null) { - throw new Error('Entropy must be provided'); - } - - kp = secp256r1Curve.genKeyPair({ - entropy: entropyOrPrivate, - }); - break; - case 'private': - kp = secp256r1Curve.keyFromPrivate(normalizeToU8a(entropyOrPrivate)); - break; - default: - throw new Error( - `Unknown source type: \`${sourceType}\`, it must be either "entropy" or "private"`, - ); - } - - super(kp); - } - - _publicKey() { - return hexToU8a(`0x${this.keyPair.getPublic(true, 'hex')}`); - } - - privateKey() { - return hexToU8a(`0x${this.keyPair.getPrivate('hex')}`); - } - - _sign(message) { - const hash = this.constructor.hash(message); - const sig = this.keyPair.sign(hash, { - canonical: true, - }); - - const r = sig.r.toString('hex', 32); - const s = sig.s.toString('hex', 32); - const i = sig.recoveryParam.toString(16).padStart(2, '0'); - - return hexToU8a(`0x${r}${s}${i}`); - } - - static signatureToDER(signature) { - const sigBytes = valueBytes(signature); - if (sigBytes.length !== 65) { - throw new Error( - `Invalid signature length. Expected is 65 bytes, received ${sigBytes.length}`, - ); - } - - const r = sigBytes.slice(0, 32); - const s = sigBytes.slice(32, 64); - - const rEncoded = encodeDERInt(r); - const sEncoded = encodeDERInt(s); - - const totalLength = rEncoded.length + sEncoded.length; - - const der = new Uint8Array(2 + totalLength); - der[0] = 0x30; - der[1] = totalLength; - - der.set(rEncoded, 2); - der.set(sEncoded, 2 + rEncoded.length); - - return der; - } - - static hash(message) { - return sha256.digest(message); - } - - static verify(message, signature, publicKey) { - let bytes = valueBytes(signature); - if (bytes.length === 65) { - bytes = this.signatureToDER(bytes); - } - - return secp256r1Curve.verify( - this.hash(message), - bytes, - valueBytes(publicKey), - ); - } -} +export { Secp256r1Keypair as default } from '@docknetwork/crypto-utils/keypairs'; diff --git a/packages/credential-sdk/src/types/public-keys/index.js b/packages/credential-sdk/src/types/public-keys/index.js index 6b85914d4..61866ff7d 100644 --- a/packages/credential-sdk/src/types/public-keys/index.js +++ b/packages/credential-sdk/src/types/public-keys/index.js @@ -1,7 +1,13 @@ -export * from './public-key'; - -export { default as PublicKeySr25519Value } from './public-key-sr25519-value'; -export { default as PublicKeyEd25519Value } from './public-key-ed25519-value'; -export { default as PublicKeySecp256k1Value } from './public-key-secp256k1-value'; -export { default as PublicKeySecp256r1Value } from './public-key-secp256r1-value'; -export { default as PublicKeyX25519Value } from './public-key-x25519-value'; +export { + PublicKey, + PublicKeyEd25519, + PublicKeyEd25519Value, + PublicKeySecp256k1, + PublicKeySecp256k1Value, + PublicKeySecp256r1, + PublicKeySecp256r1Value, + PublicKeySr25519, + PublicKeySr25519Value, + PublicKeyX25519, + PublicKeyX25519Value, +} from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/public-keys/public-key-ed25519-value.js b/packages/credential-sdk/src/types/public-keys/public-key-ed25519-value.js index 02b2de8ff..529f1e8f5 100644 --- a/packages/credential-sdk/src/types/public-keys/public-key-ed25519-value.js +++ b/packages/credential-sdk/src/types/public-keys/public-key-ed25519-value.js @@ -1,11 +1 @@ -import { sized, TypedBytes } from '../generic'; -import { Ed25519VerKeyName } from '../../vc/crypto/constants'; - -/** Class representing value of a Ed25519 PublicKey */ -export default class PublicKeyEd25519Value extends sized(TypedBytes) { - static Type = 'ed25519'; - - static Size = 32; - - static VerKeyType = Ed25519VerKeyName; -} +export { PublicKeyEd25519Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/public-keys/public-key-secp256k1-value.js b/packages/credential-sdk/src/types/public-keys/public-key-secp256k1-value.js index 2f952fb18..23f038885 100644 --- a/packages/credential-sdk/src/types/public-keys/public-key-secp256k1-value.js +++ b/packages/credential-sdk/src/types/public-keys/public-key-secp256k1-value.js @@ -1,11 +1 @@ -import { sized, TypedBytes } from '../generic'; -import { EcdsaSecp256k1VerKeyName } from '../../vc/crypto/constants'; - -/** Class representing value of a compressed Secp256k1 PublicKey */ -export default class PublicKeySecp256k1Value extends sized(TypedBytes) { - static Type = 'secp256k1'; - - static Size = 33; - - static VerKeyType = EcdsaSecp256k1VerKeyName; -} +export { PublicKeySecp256k1Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/public-keys/public-key-secp256r1-value.js b/packages/credential-sdk/src/types/public-keys/public-key-secp256r1-value.js index d19a1e3ec..df86169d9 100644 --- a/packages/credential-sdk/src/types/public-keys/public-key-secp256r1-value.js +++ b/packages/credential-sdk/src/types/public-keys/public-key-secp256r1-value.js @@ -1,11 +1 @@ -import { sized, TypedBytes } from '../generic'; -import { EcdsaSecp256r1VerKeyName } from '../../vc/crypto/constants'; - -/** Class representing value of a compressed Secp256r1 PublicKey */ -export default class PublicKeySecp256r1Value extends sized(TypedBytes) { - static Type = 'secp256r1'; - - static Size = 33; - - static VerKeyType = EcdsaSecp256r1VerKeyName; -} +export { PublicKeySecp256r1Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/public-keys/public-key-sr25519-value.js b/packages/credential-sdk/src/types/public-keys/public-key-sr25519-value.js index c726d3286..0670de446 100644 --- a/packages/credential-sdk/src/types/public-keys/public-key-sr25519-value.js +++ b/packages/credential-sdk/src/types/public-keys/public-key-sr25519-value.js @@ -1,11 +1 @@ -import { sized, TypedBytes } from '../generic'; -import { Sr25519VerKeyName } from '../../vc/crypto/constants'; - -/** Class representing value of a Sr25519 PublicKey */ -export default class PublicKeySr25519Value extends sized(TypedBytes) { - static Type = 'sr25519'; - - static Size = 32; - - static VerKeyType = Sr25519VerKeyName; -} +export { PublicKeySr25519Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/public-keys/public-key-x25519-value.js b/packages/credential-sdk/src/types/public-keys/public-key-x25519-value.js index 3768f1416..583f60022 100644 --- a/packages/credential-sdk/src/types/public-keys/public-key-x25519-value.js +++ b/packages/credential-sdk/src/types/public-keys/public-key-x25519-value.js @@ -1,10 +1 @@ -import { sized, TypedBytes } from '../generic'; - -/** Class representing value of a X25519 PublicKey */ -export default class PublicKeyX25519Value extends sized(TypedBytes) { - static Type = 'x25519'; - - static Size = 32; - - static VerKeyType = 'X25519KeyAgreementKey2019'; -} +export { PublicKeyX25519Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/public-keys/public-key.js b/packages/credential-sdk/src/types/public-keys/public-key.js index 25eeb1227..d021c5e4a 100644 --- a/packages/credential-sdk/src/types/public-keys/public-key.js +++ b/packages/credential-sdk/src/types/public-keys/public-key.js @@ -1,65 +1,8 @@ -import { TypedEnum } from '../generic'; -import PublicKeyX25519Value from './public-key-x25519-value'; -import PublicKeyEd25519Value from './public-key-ed25519-value'; -import PublicKeySecp256k1Value from './public-key-secp256k1-value'; -import PublicKeySecp256r1Value from './public-key-secp256r1-value'; -import PublicKeySr25519Value from './public-key-sr25519-value'; - -/** - * Class representing either Ed25519 or Secp256k1 or Sr25519 or X25519 PublicKey - * @class - * @extends {TypedEnum} - */ -export class PublicKey extends TypedEnum { - static get VerKeyType() { - return this.Class.VerKeyType; - } -} -/** - * Class representing Ed25519 PublicKey - * @class - * @extends {PublicKey} - */ -export class PublicKeyEd25519 extends PublicKey { - static Class = PublicKeyEd25519Value; -} -/** - * Class representing Secp256k1 PublicKey - * @class - * @extends {PublicKey} - */ -export class PublicKeySecp256k1 extends PublicKey { - static Class = PublicKeySecp256k1Value; -} -/** - * Class representing Secp256r1 PublicKey - * @class - * @extends {PublicKey} - */ -export class PublicKeySecp256r1 extends PublicKey { - static Class = PublicKeySecp256r1Value; -} -/** - * Class representing Sr25519 PublicKey - * @class - * @extends {PublicKey} - */ -export class PublicKeySr25519 extends PublicKey { - static Class = PublicKeySr25519Value; -} -/** - * Class representing X25519 PublicKey - * @class - * @extends {PublicKey} - */ -export class PublicKeyX25519 extends PublicKey { - static Class = PublicKeyX25519Value; -} - -PublicKey.bindVariants( +export { + PublicKey, PublicKeyEd25519, PublicKeySecp256k1, PublicKeySecp256r1, PublicKeySr25519, PublicKeyX25519, -); +} from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/signatures/index.js b/packages/credential-sdk/src/types/signatures/index.js index e67215aea..43583e0ac 100644 --- a/packages/credential-sdk/src/types/signatures/index.js +++ b/packages/credential-sdk/src/types/signatures/index.js @@ -1,12 +1,11 @@ -import SignatureSr25519Value from './signature-sr25519-value'; -import SignatureEd25519Value from './signature-ed25519-value'; -import SignatureSecp256k1Value from './signature-secp256k1-value'; -import SignatureSecp256r1Value from './signature-secp256r1-value'; - export { - SignatureSr25519Value, + Signature, + SignatureEd25519, SignatureEd25519Value, + SignatureSecp256k1, SignatureSecp256k1Value, + SignatureSecp256r1, SignatureSecp256r1Value, -}; -export * from './signature'; + SignatureSr25519, + SignatureSr25519Value, +} from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/signatures/signature-ed25519-value.js b/packages/credential-sdk/src/types/signatures/signature-ed25519-value.js index 0e3631924..88020ec03 100644 --- a/packages/credential-sdk/src/types/signatures/signature-ed25519-value.js +++ b/packages/credential-sdk/src/types/signatures/signature-ed25519-value.js @@ -1,9 +1 @@ -import { PublicKeyEd25519 } from '../public-keys'; -import SignatureValue from './signature-value'; - -/** Class representing a Ed25519 Signature */ -export default class SignatureEd25519Value extends SignatureValue { - static PublicKey = PublicKeyEd25519; - - static Size = 64; -} +export { SignatureEd25519Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/signatures/signature-secp256k1-value.js b/packages/credential-sdk/src/types/signatures/signature-secp256k1-value.js index 87f852d06..2873e2322 100644 --- a/packages/credential-sdk/src/types/signatures/signature-secp256k1-value.js +++ b/packages/credential-sdk/src/types/signatures/signature-secp256k1-value.js @@ -1,9 +1 @@ -import SignatureValue from './signature-value'; -import { PublicKeySecp256k1 } from '../public-keys'; - -/** Class representing a Secp256k1 Signature */ -export default class SignatureSecp256k1Value extends SignatureValue { - static PublicKey = PublicKeySecp256k1; - - static Size = 65; -} +export { SignatureSecp256k1Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/signatures/signature-secp256r1-value.js b/packages/credential-sdk/src/types/signatures/signature-secp256r1-value.js index 7a84a6d07..d34a3a756 100644 --- a/packages/credential-sdk/src/types/signatures/signature-secp256r1-value.js +++ b/packages/credential-sdk/src/types/signatures/signature-secp256r1-value.js @@ -1,9 +1 @@ -import SignatureValue from './signature-value'; -import { PublicKeySecp256r1 } from '../public-keys'; - -/** Class representing a Secp256r1 Signature */ -export default class SignatureSecp256r1Value extends SignatureValue { - static PublicKey = PublicKeySecp256r1; - - static Size = 65; -} +export { SignatureSecp256r1Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/signatures/signature-sr25519-value.js b/packages/credential-sdk/src/types/signatures/signature-sr25519-value.js index bc033f14b..7f7661e2f 100644 --- a/packages/credential-sdk/src/types/signatures/signature-sr25519-value.js +++ b/packages/credential-sdk/src/types/signatures/signature-sr25519-value.js @@ -1,9 +1 @@ -import { PublicKeySr25519 } from '../public-keys'; -import SignatureValue from './signature-value'; - -/** Class representing a Sr25519 Signature */ -export default class SignatureSr25519Value extends SignatureValue { - static PublicKey = PublicKeySr25519; - - static Size = 64; -} +export { SignatureSr25519Value as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/signatures/signature-value.js b/packages/credential-sdk/src/types/signatures/signature-value.js index a3f074db4..4975bdf8c 100644 --- a/packages/credential-sdk/src/types/signatures/signature-value.js +++ b/packages/credential-sdk/src/types/signatures/signature-value.js @@ -1,25 +1 @@ -import { withExtendedStaticProperties } from '../../utils/inheritance'; -import { sized, TypedBytes } from '../generic'; - -/** Class representing a Signature. This export class should always be extended (abstract export class in some languages) */ -export default withExtendedStaticProperties( - ['PublicKey'], - class SignatureValue extends sized(TypedBytes) { - /** - * Public key associated with the signature. - * @type {typeof PublicKey} - */ - static PublicKey; - - static get Type() { - return this.PublicKey.Type; - } - - /** - * Returns the size of the underlying signature. - */ - get size() { - return this.constructor.Size; - } - }, -); +export { SignatureValue as default } from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/types/signatures/signature.js b/packages/credential-sdk/src/types/signatures/signature.js index 56e1f6f50..19a7aa3ef 100644 --- a/packages/credential-sdk/src/types/signatures/signature.js +++ b/packages/credential-sdk/src/types/signatures/signature.js @@ -1,59 +1,7 @@ -import { TypedEnum } from '../generic'; -import SignatureEd25519Value from './signature-ed25519-value'; -import SignatureSecp256k1Value from './signature-secp256k1-value'; -import SignatureSecp256r1Value from './signature-secp256r1-value'; -import SignatureSr25519Value from './signature-sr25519-value'; - -/** - * Class representing either Ed25519 or Secp256k1 or Sr25519 Signature - * @class - * @extends {TypedEnum} - */ -export class Signature extends TypedEnum { - static get PublicKey() { - return this.Class.PublicKey; - } - - get bytes() { - return this.value.bytes; - } -} -/** - * Class representing Ed25519 Signature - * @class - * @extends {Signature} - */ -export class SignatureEd25519 extends Signature { - static Class = SignatureEd25519Value; -} -/** - * Class representing Secp256k1 Signature - * @class - * @extends {Signature} - */ -export class SignatureSecp256k1 extends Signature { - static Class = SignatureSecp256k1Value; -} -/** - * Class representing Secp256r1 Signature - * @class - * @extends {Signature} - */ -export class SignatureSecp256r1 extends Signature { - static Class = SignatureSecp256r1Value; -} -/** - * Class representing Sr25519 Signature - * @class - * @extends {SignatureEd25519OrSignatureSecp256k1OrSignatureSr25519} - */ -export class SignatureSr25519 extends Signature { - static Class = SignatureSr25519Value; -} - -Signature.bindVariants( +export { + Signature, SignatureEd25519, SignatureSecp256k1, SignatureSecp256r1, SignatureSr25519, -); +} from '@docknetwork/crypto-utils/types'; diff --git a/packages/credential-sdk/src/utils/assert.js b/packages/credential-sdk/src/utils/assert.js index aab980d98..030e67f56 100644 --- a/packages/credential-sdk/src/utils/assert.js +++ b/packages/credential-sdk/src/utils/assert.js @@ -1,52 +1 @@ -import { panic } from './error'; - -/** - * Asserts that a condition is true; otherwise, throws an error using the panic function. - * @param {boolean} cond - The condition to check. - * @param {function | string | Error} error - The error message, function returning an error, or an Error object to throw. - */ -export const assert = (cond, error) => { - if (!cond) { - panic(error); - } -}; - -/** - * Error thrown when the provided function was executed more than once or wasn't executed at all. - */ -export class MustBeCalledOnce extends Error { - constructor(fn) { - super(`Function must be executed exactly once: \`${fn}\``); - } - - static ensure(fn, call) { - let executed = false; - - const name = `mustBeExecutedOnce(${fn.name})`; - const obj = { - [name](...args) { - let res; - let err; - - assert(!executed, () => new this(fn)); - try { - res = fn.apply(this, args); - } catch (e) { - err = e; - } finally { - executed = true; - } - - if (err != null) { - throw err; - } - return res; - }, - }; - - const res = call(obj[name]); - assert(executed, () => new this(fn)); - - return res; - } -} +export { MustBeCalledOnce, assert } from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/error.js b/packages/credential-sdk/src/utils/error.js index 814e33e1f..1d24953e4 100644 --- a/packages/credential-sdk/src/utils/error.js +++ b/packages/credential-sdk/src/utils/error.js @@ -1,52 +1,5 @@ -/** - * Throws an error with the provided message or generated by a function. - * @param {function | string | Error} err - The error message, function returning an error, or an Error object. - */ -export const panic = (error) => { - if (typeof error === 'function') { - return panic(error()); - } - - throw error instanceof Error ? error : new Error(error); -}; - -/** - * Wraps a function to prefix its error messages with a specified string. - * If an error is thrown by the wrapped function, this error will be caught, - * modified to include the provided prefix in its message, and then rethrown. - * - * @param {function(): T} fn - The function to wrap - * @param {string} prefix - Prefix to add before the original error message - * @returns {T} The return value of the wrapped function - * - * @example - * const wrappedFn = catchErrorWith('Error in component', () => { - * throw new Error('Something went wrong'); - * }); - * - * try { - * wrappedFn(); - * } catch (err) { - * console.error(err.message); // "Error in component: Something went wrong" - * } - */ -export const catchErrorWith = (prefix, fn) => { - try { - return fn(); - } catch (err) { - err.message = `${prefix}: ${err.message}`; - - throw err; - } -}; - -/** - * Creates a function that will prefix any error messages from the original - * function call with the specified string. This is useful for adding context - * to errors thrown by functions that don't directly handle their own error messaging. - * - * @param {function} fn - The function to wrap - * @param {string} prefix - Prefix to add before any error messages - * @returns {function(...args): T} A new function that will execute the original function with provided arguments and prefix its errors - */ -export const catchFnErrorWith = (prefix, fn) => (...args) => catchErrorWith(prefix, () => fn(...args)); +export { + catchErrorWith, + catchFnErrorWith, + panic, +} from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/inheritance.js b/packages/credential-sdk/src/utils/inheritance.js index 91fe24290..21c59346d 100644 --- a/packages/credential-sdk/src/utils/inheritance.js +++ b/packages/credential-sdk/src/utils/inheritance.js @@ -1,250 +1,12 @@ -import { ensureObjectWithAnyConstructor } from './types/object'; -import { fmtIterable } from './types/iterable'; -import { ensureNoIntersection } from './types/set'; - -/** - * Enhances the provided class with the given list of static properties to require - * in the inherited class. - * All properties will be checked for presence during the object constructor call. - * Each property on its own will be checked every time it is accessed. - * In case some property is missing, an error will be thrown. - * - * @template T - * @param {Array} properties - * @param {T} parentClass - * @returns {T} - */ -// eslint-disable-next-line sonarjs/cognitive-complexity -export function withExtendedStaticProperties(properties, parentClass) { - const name = `withExtStatics(${parentClass.name}, ${fmtIterable( - properties, - )})`; - - const extendedClass = { - [name]: class extends parentClass { - constructor(...args) { - super(...args); - - /* - * Ensures that properties are extended properly. - */ - for (const property of properties) { - try { - if (this.constructor[property] === parentClass[property]) { - throw new Error( - `Static property \`${property}\` of \`${this.constructor.name}\` isn't extended properly`, - ); - } - } catch (err) { - err.message = `Failed to check the prototype property: \n${err.message}`; - - throw err; - } - } - } - }, - }; - - for (const property of properties) { - const propertySymbol = Symbol(property); - - Object.defineProperty(extendedClass[name], property, { - get() { - if ( - this !== extendedClass[name] - && this[propertySymbol] === parentClass[property] - ) { - throw new Error( - `Property \`${property}\` of \`${this.name}\` isn't extended properly`, - ); - } - return this[propertySymbol]; - }, - set(newValue) { - if (Object.hasOwnProperty.call(this, propertySymbol)) { - throw new Error( - `Can't override the property \`${property}\` of \`${this.name}\``, - ); - } - - this[propertySymbol] = newValue; - }, - }); - } - - return extendedClass[name]; -} - -/** - * Enhances the provided class with the given list of prototype properties to require - * in the inherited class. - * All properties will be checked for presence during the object constructor call. - * In case some property is missing, an error will be thrown. - * - * @template T - * @param {Array} properties - * @param {T} parentClass - * @returns {T} - */ -export function withExtendedPrototypeProperties(properties, parentClass) { - const name = `withExtProto(${parentClass.name}, ${fmtIterable(properties)})`; - - const extendedClass = { - [name]: class extends parentClass { - constructor(...args) { - super(...args); - const proto = Object.getPrototypeOf(this); - - /* - * Ensures that properties are extended properly. - */ - for (const property of properties) { - try { - if (proto[property] === parentClass.prototype[property]) { - throw new Error( - `Property \`${property}\` of the object prototype of \`${this.constructor.name}\` isn't extended properly`, - ); - } - } catch (err) { - err.message = `Failed to check the static property: \n${err.message}`; - - throw err; - } - } - } - }, - }; - - return extendedClass[name]; -} - -const ObjectProperties = new WeakMap(); - -/** - * Returns a Set containing all properties of the given object and its prototype chain. - * - * @param {Object} obj - The object to retrieve properties from. - * @returns {Set} - A set of properties. - */ -export const allObjectPropertiesIncludingPrototypes = (obj) => { - if (obj == null) return new Set(); - - let props = ObjectProperties.get(obj); - if (props == null) { - props = new Set([ - ...Object.getOwnPropertyNames(obj), - ...allObjectPropertiesIncludingPrototypes(Object.getPrototypeOf(obj)), - ]); - ObjectProperties.set(obj, props); - } - - return props; -}; - -/** - * Validates that object properties don't intersect with prototype properties. - * - * @param {Object} obj - The object to validate. - */ -export const validateProperties = (obj) => ensureNoIntersection( - new Set(Object.getOwnPropertyNames(obj)), - allObjectPropertiesIncludingPrototypes(Object.getPrototypeOf(obj)), -); - -/** - * Checks if an object is the prototype of another object. - * - * @param {Object} proto - The prototype object. - * @param {Object} obj - The object to check against. - * @returns {boolean} - Whether the object is the prototype. - */ -export const isPrototypeOf = (proto, obj) => Object.isPrototypeOf.call(proto, obj); - -/** - * Checks if an object is equal to or a prototype of another object. - * - * @param {Object} proto - The prototype object. - * @param {Object} obj - The object to check. - * @returns {boolean} - Whether the object is equal to or a prototype of the other. - */ -export const isEqualToOrPrototypeOf = (proto, obj) => Object.is(proto, obj) || isPrototypeOf(proto, obj); - -/** - * Ensures an object is equal to or a prototype of another object. - * - * @param {Object} proto - The prototype object to check against. - * @param {Object} obj - The object to validate. - * @returns {Object} - The validated object if it meets the requirement. - * @throws {Error} - If the object doesn't match the prototype. - */ -export const ensureEqualToOrPrototypeOf = (proto, obj) => { - if (isEqualToOrPrototypeOf(proto, obj)) { - return obj; - } - - throw new Error( - `Expected \`${proto.name}\` to be equal to or a prototype of \`${obj.name}\``, - ); -}; - -/** - * Creates a new class that extends the given class but hides prototype functions from - * the original class on its own prototype. The resulting class will not have any - * enumerable functions from the original class except for those explicitly ignored. - * - * @param {Function} klass - The original class to extend - * @param {Set} ignore - Optional set of member names to keep visible - * @returns {Function} The new extended class without prototype functions - */ -export const withoutPrototypeFunctions = (klass, ignore = new Set()) => { - const name = `withoutPrototypeFunctions(${klass.name})`; - - const obj = { - [name]: class extends klass {}, - }; - - for (const member of Object.getOwnPropertyNames(klass.prototype)) { - if (!ignore.has(member) && typeof klass.prototype[member] === 'function') { - Object.defineProperty(obj[name].prototype, member, { - enumerable: false, - }); - } - } - - return obj[name]; -}; - -/** - * Ensures that the given value is an instance of the specified class. Throws an error with details if not. - * - * @param {*} value - The value to check. - * @param {Function} klass - The constructor or class to check against. - * @throws If value is not a Uint8Array. - * @returns {*} - The original value if it's an instance of the class. - */ -export const ensureInstanceOf = (value, klass) => { - if (ensureObjectWithAnyConstructor(value) instanceof klass) { - return value; - } - - throw new Error( - `Expected \`${value}\` with constructor \`${value?.constructor.name}\` to be an instance of \`${klass.name}\``, - ); -}; - -/** - * Ensures that the given prototype is that of the value. Throws an error with a message if not. - * - * @param {object} proto - The expected prototype. - * @param {*} value - The value to check. - * @returns {*} - The original value if it has the correct prototype. - */ -export const ensurePrototypeOf = (proto, value) => { - if (isPrototypeOf(proto, value)) { - return value; - } - - throw new Error( - `Expected \`${proto.name}\` to be a prototype of \`${value}\``, - ); -}; +export { + allObjectPropertiesIncludingPrototypes, + ensureEqualToOrPrototypeOf, + ensureInstanceOf, + ensurePrototypeOf, + isEqualToOrPrototypeOf, + isPrototypeOf, + validateProperties, + withExtendedPrototypeProperties, + withExtendedStaticProperties, + withoutPrototypeFunctions, +} from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/interfaces.js b/packages/credential-sdk/src/utils/interfaces.js index 58aed5e13..c6d728e9f 100644 --- a/packages/credential-sdk/src/utils/interfaces.js +++ b/packages/credential-sdk/src/utils/interfaces.js @@ -1,115 +1,13 @@ -import { MustBeCalledOnce } from './assert'; - -/** - * Attempts to call `value.toJSON()`, returns `JSON.parse(JSON.stringify(value))` if method doesn't exist. - * @template T - * @param {T} value - * @returns {object} - */ -export const maybeToJSON = (value) => (typeof value?.toJSON === 'function' - ? value.toJSON() - : JSON.parse(JSON.stringify(value))); - -/** - * Stringifies the provided value converted to JSON. - * @template T - * @param {T} value - * @returns {string} - */ -export const maybeToJSONString = (value) => JSON.stringify(maybeToJSON(value)); - -/** - * Attempts to convert provided value to the Cheqd Payload or JSON. - * @param {*} value - * @returns {object} - */ -export const maybeToCheqdPayloadOrJSON = (obj) => (typeof obj?.toCheqdPayload === 'function' // eslint-disable-line no-nested-ternary - ? obj.toCheqdPayload() - : typeof obj?.apply === 'function' // eslint-disable-line no-nested-ternary - ? obj.apply(maybeToCheqdPayloadOrJSON) - : typeof value !== 'object' && typeof value !== 'function' - ? obj - : maybeToJSON(obj)); - -/** - * Returns bytes of the value converted to a stringified JSON. - * @template T - * @param {T} value - * @returns {string} - */ -export const maybeToJSONStringBytes = (value) => (typeof value?.maybeToJSONStringBytes === 'function' - ? value.toJSONStringBytes() - : Uint8Array.from(Buffer.from(maybeToJSONString(value)))); - -/** - * Attempts to compare two values using `value.eq(other)`, returns `boolean`. - * @template T - * @param {T} value - * @param {T} other - * @returns {boolean} - */ -export const maybeEq = (value, other) => (typeof value?.eq === 'function' ? value.eq(other) : value === other); - -/** - * Attempts to call `value.toHuman()` or `value.toJSON`, returns `value` if methods don't exist. - * @template T - * @param {T} value - * @returns {object} - */ -export const maybeToHuman = (obj) => (obj && typeof obj.toHuman === 'function' ? obj.toHuman() : maybeToJSON(obj)); - -/** - * Attempts to call `value.toNumber()`, returns `+value` if method doesn't exist. - * @template T - * @param {T} value - * @returns {number} - */ -export const maybeToNumber = (value) => (typeof value?.toNumber === 'function' ? value.toNumber() : +value); - -/** - * Marks function that it can't be used as a constructor. - */ -export const NotAConstructor = Symbol.for( - '@docknetwork/credential-sdk/NotAConstructor', -); - -/** - * Attempts to intantiate new object of the supplied class using provided arguments. - * @param Class - * @param args - */ -export const maybeNew = (Class, args) => (!Class[NotAConstructor] ? new Class(...args) : Class.apply(Class, args)); - -/** - * Attempts to create new instance of the supplied class using `Class.from(obj)`, instantiates class if `from` doesn't exist. - * @param Class - * @param args - */ -export const maybeFrom = (klass, obj) => (typeof klass.from === 'function' ? klass.from(obj) : maybeNew(klass, [obj])); - -/** - * Applies function to the first value of the provided object that passes the check. - * @template T - * @template I - * @template O - * @param {function(I): boolean} check - * @param {function(I): O} fn - * @param {T} value - * @returns {O} - */ -export const applyToValue = (check, fn, value) => { - if (check(value)) { - return fn(value); - } else if (typeof value?.apply === 'function') { - let res; - MustBeCalledOnce.ensure( - (obj) => { - res = applyToValue(check, fn, obj); - }, - (wrapped) => value.apply(wrapped), - ); - return res; - } - - throw new Error("`fn` can't be applied because value didn't pass the check"); -}; +export { + NotAConstructor, + applyToValue, + maybeEq, + maybeFrom, + maybeNew, + maybeToCheqdPayloadOrJSON, + maybeToHuman, + maybeToJSON, + maybeToJSONString, + maybeToJSONStringBytes, + maybeToNumber, +} from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/types/array.js b/packages/credential-sdk/src/utils/types/array.js index d154ffc9b..d1959b31b 100644 --- a/packages/credential-sdk/src/utils/types/array.js +++ b/packages/credential-sdk/src/utils/types/array.js @@ -1,94 +1,8 @@ -import { withoutPrototypeFunctions } from '../inheritance'; - -/** - * Return true if the given value is an array. - * @param value - * @returns {boolean} - */ -export function isArray(value) { - return Array.isArray(value); -} - -/** - * Ensures that the given value is an array. Throws an error stating it must be an array if not. - * - * @param {*} value - The value to check. - * @returns {Array} - The original array value. - */ -export function ensureArray(value) { - if (Array.isArray(value)) { - return value; - } - - throw new TypeError('The provided value must be Array'); -} - -/** - * Ensures that the given value is array-like (an Array, Uint8Array, Uint16Array, Uint32Array, or Buffer). - * If not, throws an error indicating it must be array-like. - * - * @param {*} value - The value to check. - * @throws If value is not an instance of `Array|Uint8Array|Uint16Array|Uint32Array|Buffer` - * @returns {Array|Uint8Array|Uint16Array|Uint32Array|Buffer} - The original value if valid. - */ -export function ensureArrayLike(value) { - if ( - Array.isArray(value) - || value instanceof Uint8Array - || value instanceof Uint16Array - || value instanceof Uint32Array - || value instanceof Buffer - ) { - return value; - } - - throw new TypeError('Value needs to be an Array-like'); -} - -/** - * Get unique elements from an array based on the mapItem function. - * @param {Array} array - Array to check for duplicates. - * @param {Function} mapItem - Function to map elements before comparison. - * @returns {Array} - Array with unique elements. - */ -export function getUniqueElementsFromArray(array, mapItem) { - const seen = new Set(); - - return array.filter((item) => { - const key = mapItem(item); - - return seen.has(key) ? false : seen.add(key); - }); -} - -/** - * Splits array into chunks of the given size. - * - * @template T - * @param {Array} arr - * @param {number} chunkSize - * @returns {Array>} - */ -export const chunks = (arr, chunkSize) => arr.reduce((acc, item, idx) => { - if (idx % chunkSize === 0) acc.push([item]); - else acc[acc.length - 1].push(item); - - return acc; -}, []); - -/** - * Class extending `Array` with all prototype methods set to `undefined`. - */ -export class ArrayWithoutPrototypeMethods extends withoutPrototypeFunctions( - Array, - new Set([ - 'forEach', - 'filter', - 'every', - 'find', - 'findIndex', - 'splice', - 'some', - 'find', - ]), -) {} +export { + ArrayWithoutPrototypeMethods, + chunks, + ensureArray, + ensureArrayLike, + getUniqueElementsFromArray, + isArray, +} from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/types/bytes.js b/packages/credential-sdk/src/utils/types/bytes.js index 50b1c25dc..373dc4afb 100644 --- a/packages/credential-sdk/src/utils/types/bytes.js +++ b/packages/credential-sdk/src/utils/types/bytes.js @@ -1,268 +1,24 @@ -import crypto from 'crypto'; -import { applyToValue } from '../interfaces'; -import { ensureString } from './string'; -import { ensureIterable, isIterable } from './iterable'; -import { ensureInstanceOf } from '../inheritance'; - -/** - * Checks that the given value is a byte (an integer between 0 and 255). - * - * @param {*} byte - The value to check. - * @returns {boolean} - The validated byte value. - */ -export const isByte = (num) => Number.isInteger(num) && num >= 0 && num <= 255; - -/** - * Ensures that the given value is a byte (an integer between 0 and 255). - * - * @param {*} byte - The value to check. - * @throws If value is not a byte. - * @returns {number} - The validated byte value. - */ -export function ensureByte(num) { - if (isByte(num)) { - return num; - } - - throw new Error(`Expected \`${num}\` to be an integer in range 0-255`); -} - -/** - * Ensures that the given value is a list of bytes. If it's not a Uint8Array, converts it to one by mapping each element through `ensureByte`. - * - * @param {Uint8Array|Iterable} bytes - The bytes to validate. - * @throws If value is not a bytes. - * @returns {Uint8Array} - The validated Uint8Array containing the bytes. - */ -export const ensureBytes = (bytes) => { - if (bytes instanceof Uint8Array) { - return bytes; - } - - return Uint8Array.from([...ensureIterable(bytes)].map(ensureByte)); -}; - -/** - * Ensures that the given value is an instance of Uint8Array. Throws an error if not. - * - * @param {*} bytes - The value to check. - * @throws If value is not a Uint8Array. - * @returns {Uint8Array} - The original Uint8Array value. - */ -export const ensureUint8Array = (bytes) => { - if (bytes instanceof Uint8Array) { - return bytes; - } - - throw new TypeError('Expected instance of `Uint8Array`'); -}; - -/** - * Checks that the given value is a list of bytes. If it's not a Uint8Array, validates each item using `ensureByte`. - * - * @param {Uint8Array|Iterable} bytes - The bytes to validate. - * @throws If value is not a bytes. - * @returns {boolean} - The validated Uint8Array containing the bytes. - */ -export const isBytes = (bytes) => { - if (bytes instanceof Uint8Array) { - return true; - } else if (isIterable(bytes) && typeof bytes !== 'string') { - for (const byte of bytes) { - if (!isByte(byte)) { - return false; - } - } - - return true; - } else { - return false; - } -}; - -/** - * Check if the given input is hexadecimal or not. Optionally checks for the byte size of the hex. Case-insensitive on hex chars - * @param {string} value - Hexadecimal value - * @param {number} [byteSize] - Expected byte size of the input. - * @return {boolean} True if hex (with given size) else false - */ -export const isHexWithGivenByteSize = (value, byteSize) => { - if (typeof value !== 'string') { - return false; - } - const match = value.match(/^0x([0-9a-f]+$)/i); - - if (match && match.length > 1) { - if (byteSize !== undefined) { - // If `byteSize` is not a positive integer type, then check will fail - // 2 hex digits make a byte - return match[1].length === 2 * byteSize; - } - // Don't care about byte size of the match but it must be full byte - return match[1].length % 2 === 0; - } - - return false; -}; - -/** - * Returns `true` if provided value is a valid hex string. - * @param value - * @returns {boolean} - */ -export const isHex = (value) => isHexWithGivenByteSize(value); - -/** - * Converts supplied bytes to its hex representation. - * @param {Iterable} bytes - * @returns {string} - */ -export const u8aToHex = (bytes) => `0x${Buffer.from(ensureBytes(bytes)).toString('hex')}`; - -/** - * Creates `Uint8Array` from the supplied hex string. - * @param {string} str - * @returns {Uint8Array} - */ -export const hexToU8a = (str) => { - if (!isHex(str)) { - throw new Error( - `Expected valid hex string, received: \`${str}\` with type \`${typeof str}\``, - ); - } - - return Uint8Array.from(Buffer.from(str.slice(2), 'hex')); -}; - -/** - * Creates `Buffer` from the supplied string. - * @param {string} str - * @returns {Buffer} - */ -export const stringToBuffer = (str) => Buffer.from(ensureString(str)); - -/** - * Creates `Uint8Array` from the supplied string. - * @param {string} str - * @returns {Uint8Array} - */ -export const stringToU8a = (str) => Uint8Array.from(stringToBuffer(str)); - -/** - * Creates `Uint8Array` from the supplied buffer. - * @param {Buffer} buffer - * @returns {Uint8Array} - */ -export const bufferToU8a = (buffer) => Uint8Array.from(ensureInstanceOf(buffer, Buffer)); - -/** - * Converts supplied bytes to `Uint8Array` representation. - * @param {Iterable} bytes - * @returns {Uint8Array} - */ -export const u8aToU8a = (bytes) => bufferToU8a(Buffer.from(ensureBytes(bytes))); - -/** - * Converts supplied bytes buffer to its hex string representation. - * @param {Iterable} bytes - * @returns {string} - */ -export const u8aToString = (bytes) => Buffer.from(ensureBytes(bytes)).toString(); - -/** - * Converts supplied string containing any characters to its hex string representation. - * @param {string} str - * @returns {string} - */ -export const stringToHex = (str) => u8aToHex(stringToBuffer(str)); - -/** - * Attempts to convert supplied bytes to `Uint8Array`. - * @param {string | Uint8Array | Array} bytes - * @returns {Uint8Array} - */ -export const normalizeToU8a = (bytes) => { - if (bytes instanceof Uint8Array) { - return bytes; - } else if (isBytes(bytes)) { - return u8aToU8a(bytes); - } else if (isHex(bytes)) { - return hexToU8a(bytes); - } - - throw new Error("Can't convert supplied value to `Uint8Array`"); -}; - -/** - * Attempts to convert supplied bytes or a raw string to `Uint8Array`. - * @param {Uint8Array | string} bytesOrString - * @returns {Uint8Array} - */ -export const normalizeOrConvertStringToU8a = (bytesOrString) => (typeof bytesOrString === 'string' && !isHex(bytesOrString) - ? stringToU8a(bytesOrString) - : normalizeToU8a(bytesOrString)); - -/** - * Creates random `Uint8Array` array of supplied byte length. - * @param {number} length - * @returns {Uint8Array} - */ -export const randomAsU8a = (length) => u8aToU8a(crypto.randomBytes(length)); - -/** - * Creates random hex string of supplied byte length. - * @param {number} length - * @returns {string} - */ -export const randomAsHex = (length) => u8aToHex(randomAsU8a(length)); - -/** - * Attempts to get byte representation of the supplied object. - * Throws an error in case if it's not possible. - * @param {*} obj - * @returns {Uint8Array} - */ -export const valueBytes = (value) => applyToValue( - (inner) => Array.isArray(inner) - || inner instanceof Uint8Array - || (inner && typeof inner === 'object' && 'bytes' in inner), - (inner) => normalizeToU8a(inner.bytes ?? inner), - value, -); - -/** - * Attempts to get byte representation of the supplied object. - * In case if the number is found, it will be converted to bytes using `String(number)`. - * Throws an error in case if it's not possible. - * @param {*} value - * @returns {Uint8Array} - */ -export const valueNumberOrBytes = (value) => applyToValue( - (inner) => Array.isArray(inner) - || inner instanceof Uint8Array - || (inner && typeof inner === 'object' && 'bytes' in inner) - || typeof inner === 'number', - (inner) => (typeof inner === 'number' - ? stringToU8a(String(inner)) - : normalizeToU8a(inner.bytes ?? inner)), - value, -); - -/** - * Normalizes the given input to hex. Expects a Uint8Array or a hex string - * @param {Uint8Array|string} data - * @returns {string} - */ -export function normalizeToHex(data) { - if ( - data instanceof Uint8Array - || data instanceof Buffer - || Array.isArray(data) - ) { - return u8aToHex(data); - } else if (isHex(data)) { - return data; - } - - throw new Error('Expected a hex string or a byte array'); -} +export { + bufferToU8a, + ensureByte, + ensureBytes, + ensureUint8Array, + hexToU8a, + isByte, + isBytes, + isHex, + isHexWithGivenByteSize, + normalizeOrConvertStringToU8a, + normalizeToHex, + normalizeToU8a, + randomAsHex, + randomAsU8a, + stringToBuffer, + stringToHex, + stringToU8a, + u8aToHex, + u8aToString, + u8aToU8a, + valueBytes, + valueNumberOrBytes, +} from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/types/iterable.js b/packages/credential-sdk/src/utils/types/iterable.js index 828d08c64..e21db6f46 100644 --- a/packages/credential-sdk/src/utils/types/iterable.js +++ b/packages/credential-sdk/src/utils/types/iterable.js @@ -1,71 +1,7 @@ -import { isObject } from './object'; - -/** - * Return true if a value is iterable. - * @param value - * @returns {boolean} - */ -export function isIterable(value) { - // Check if it's an object and has Symbol.iterator property - return ( - value != null - && (typeof value === 'object' || typeof value === 'function') - && Symbol.iterator in Object(value) - ); -} - -/** - * Ensures that the given value is iterable. Throws an error with an optional context message if not. - * - * @param {*} value - The value to check for iterability. - * @throws If value is not an iterable. - * @returns {Iterable} - The original iterable value. - */ -export function ensureIterable(value) { - if (isIterable(value)) { - return value; - } - - throw new TypeError(`${value} needs to be an iterable.`); -} - -/** - * Returns string containing comma-separated items of the provided iterable. - * - * @template V - * @param {Iterable} iter - * @returns {string} - */ -export const fmtIterable = (iter) => `[${[...ensureIterable(iter)].map(String).join(', ')}]`; - -/** - * Converts the provided value into an iterable. If the value is already iterable, - * it's returned as-is. If it's an object (not null), it returns an array of its - * key-value pairs. Otherwise, it throws an error. - * - * @template T - The type of the input value - * @param {T} value - The value to convert into an iterable - * @returns {Iterable} - An iterable version of the input value - * @throws {Error} If the value is null, undefined, or neither an iterable nor an object - */ -export const toIterable = (value) => { - if (isIterable(value)) { - return value; - } else if (isObject(value) && value != null) { - return Object.entries(value); - } - - throw new Error(`\`${value}\` is not an iterable or object`); -}; - -/** - * Converts the provided value into an iterator. If the value is already iterable, - * it's returned as-is. If it's an object (not null), it returns an array of its - * key-value pairs. Otherwise, it throws an error. - * - * @template T - The type of the input value - * @param {T} value - The value to convert into an iterator - * @returns {IterableIterator} - An iterator version of the input value - * @throws {Error} If the value is null, undefined, or neither an iterable nor an object - */ -export const toIterator = (value) => toIterable(value)[Symbol.iterator](); +export { + ensureIterable, + fmtIterable, + isIterable, + toIterable, + toIterator, +} from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/types/number.js b/packages/credential-sdk/src/utils/types/number.js index 3fc689d58..d59deb518 100644 --- a/packages/credential-sdk/src/utils/types/number.js +++ b/packages/credential-sdk/src/utils/types/number.js @@ -1,25 +1 @@ -/** - * Return true if the given value is a number. - * @param value - * @returns {boolean} - */ -export function isNumber(value) { - return ( - (typeof value === 'number' && !Number.isNaN(value)) - || value instanceof Number - ); -} - -/** - * Ensures that the given value is a number. Throws an error with an optional context message if not. - * @param {any} value - The value to check. - * @param {string} [message] - Optional message for the error. - * @returns {number} - The value converted to a number. - */ -export function ensureNumber(value) { - if (isNumber(value)) { - return Number(value); - } - - throw new TypeError(`\`${value}\` needs to be a number.`); -} +export { ensureNumber, isNumber } from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/types/object.js b/packages/credential-sdk/src/utils/types/object.js index 46f74db2c..f2e152a48 100644 --- a/packages/credential-sdk/src/utils/types/object.js +++ b/packages/credential-sdk/src/utils/types/object.js @@ -1,133 +1,12 @@ -/** - * Return true if a value is an object with Object constructor. - * @param value - * @returns {boolean} - */ -export function isObject(value) { - return isObjectWithAnyConstructor(value) && value.constructor === Object; -} - -/** - * Return true if a value is an object. - * @param value - * @returns {boolean} - */ -export function isObjectWithAnyConstructor(value) { - return value && typeof value === 'object'; -} - -/** - * Ensures that the given value is an object. If not, throws an error explaining why. - * - * @template T - * @param {T} value - The value to check. - * @throws If value is not an object. - * @returns {T} - */ -export function ensureObject(value) { - if (isObject(value)) { - return value; - } - - throw new TypeError( - `${value} needs to be an object with Object constructor.`, - ); -} - -/** - * Ensures that the given value is an object. If not, throws an error explaining why. - * - * @template T - * @param {T} value - The value to check. - * @throws If value is not an object. - * @returns {T} - */ -export function ensureObjectWithAnyConstructor(value) { - if (isObjectWithAnyConstructor(value)) { - return value; - } - - throw new TypeError(`${value} needs to be an object.`); -} - -/** - * Ensures that the given object has a specific key as a property. Throws an error with a message if not. - * - * @param {object} value - The object to check. - * @param {string} key - The required property to look for. - * @param {string} name - Name of the object used in constructing the error message. - * @returns {object} - The original object if it contains the specified key. - */ -export function ensureObjectWithKey(value, key, name) { - if (key in ensureObject(value)) { - return value; - } - - throw new Error(`"${name}" must include the '${key}' property.`); -} - -/** - * Ensures that the given object has an 'id' property. Throws an error with a message if not. - * - * @param {object} value - The object to check. - * @param {string} name - Name of the object used in constructing the error message. - * @returns {object} - The original object if it contains the 'id' property. - */ -export function ensureObjectWithId(value, name) { - return ensureObjectWithKey(value, 'id', name); -} - -/** - * Clones supplied object using `JSON.parse(JSON.stringify(...))`. - * - * @param {Object} obj - * @returns {Object} - */ -export const deepClone = (obj) => JSON.parse(JSON.stringify(ensureObject(obj))); - -/** - * Filters properties of the object according to the supplied `filter`. - * - * @template K - * @template V - * @param {Object} obj - * @param {function(K, V): boolean} filter - * @returns {Object} - */ -export const filterObj = (obj, filter) => { - const res = Object.create(Object.getPrototypeOf(obj)); - for (const [key, value] of Object.entries(obj)) { - if (filter(key, value)) { - res[key] = value; - } - } - - return res; -}; - -/** - * Maps properties of the object according to the supplied `map`. - * - * @template K - * @template VI - * @template VO - * @param {Object} obj - * @param {function(VI): VO} map - * @returns {Object} - */ -export const mapObj = (obj, map) => { - const res = Object.create(Object.getPrototypeOf(obj)); - for (const [key, value] of Object.entries(obj)) { - res[key] = map(value); - } - - return res; -}; - -/** - * Sets prototype of the supplied object to `null`, returns the object. - * @template T - * @param {T} - * @returns {T} - */ -export const extendNull = (obj) => Object.setPrototypeOf(obj, null); +export { + deepClone, + ensureObject, + ensureObjectWithAnyConstructor, + ensureObjectWithId, + ensureObjectWithKey, + extendNull, + filterObj, + isObject, + isObjectWithAnyConstructor, + mapObj, +} from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/types/set.js b/packages/credential-sdk/src/utils/types/set.js index 99f31f036..8fcfce11e 100644 --- a/packages/credential-sdk/src/utils/types/set.js +++ b/packages/credential-sdk/src/utils/types/set.js @@ -1,40 +1,5 @@ -/** - * Return true if the given value is a Set. - * @param value - * @returns {boolean} - */ -export function isSet(value) { - return value instanceof Set; -} - -/** - * Ensures that the given value is a Set. Throws an error with an optional context message if not. - * @param {any} value - The value to check. - * @param {string} [message] - Optional message for the error. - * @returns {Set} - The value if it's a Set. - */ -export function ensureSet(value) { - if (isSet(value)) { - return value; - } - - throw new TypeError(`\`${value}\` needs to be a Set.`); -} - -/** - * Ensures that there is no intersection between two sets - * @param {Set} set1 - The first set to compare - * @param {Set} set2 - The second set to compare - * @throws {Error} - If there is an intersection between set1 and set2 - */ -export const ensureNoIntersection = (set1, set2) => { - const [min, max] = [set1, set2] - .map(ensureSet) - .sort((a, b) => a.size - b.size); - - for (const item of min) { - if (max.has(item)) { - throw new Error(`Item \`${item}\` exists in both sets`); - } - } -}; +export { + ensureNoIntersection, + ensureSet, + isSet, +} from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/utils/types/string.js b/packages/credential-sdk/src/utils/types/string.js index e30b1074e..c8151d161 100644 --- a/packages/credential-sdk/src/utils/types/string.js +++ b/packages/credential-sdk/src/utils/types/string.js @@ -1,22 +1 @@ -/** - * Return true if the given value is a string. - * @param value - * @returns {boolean} - */ -export function isString(value) { - return typeof value === 'string' || value instanceof String; -} - -/** - * Ensures that the given value is a string. If not, throws an error with an optional context message. - * - * @param {*} value - The value to check. - * @returns {string} - The value converted to a string. - */ -export function ensureString(value) { - if (isString(value)) { - return String(value); - } - - throw new TypeError(`${value} needs to be a string`); -} +export { ensureString, isString } from '@docknetwork/crypto-utils/utils'; diff --git a/packages/credential-sdk/src/vc/crypto/constants.js b/packages/credential-sdk/src/vc/crypto/constants.js index 426d8d494..709869cd4 100644 --- a/packages/credential-sdk/src/vc/crypto/constants.js +++ b/packages/credential-sdk/src/vc/crypto/constants.js @@ -1,23 +1,25 @@ -export const EcdsaSecp256k1VerKeyName = 'EcdsaSecp256k1VerificationKey2019'; -export const EcdsaSecp256k1SigName = 'EcdsaSecp256k1Signature2019'; -export const EcdsaSecp256r1VerKeyName = 'EcdsaSecp256r1VerificationKey2019'; -export const EcdsaSecp256r1SigName = 'EcdsaSecp256r1Signature2019'; -export const Ed25519VerKeyName = 'Ed25519VerificationKey2018'; -export const Ed255192020VerKeyName = 'Ed25519VerificationKey2020'; -export const Ed25519SigName = 'Ed25519Signature2018'; -export const Ed255192020SigName = 'Ed25519Signature2020'; -export const Sr25519VerKeyName = 'Sr25519VerificationKey2020'; -export const Sr25519SigName = 'Sr25519Signature2020'; -export const Bls12381BBSSigDockSigName = 'Bls12381BBS+SignatureDock2022'; -export const Bls12381BBSSigProofDockSigName = 'Bls12381BBS+SignatureProofDock2022'; -export const Bls12381BBSDockVerKeyName = 'Bls12381G2VerificationKeyDock2022'; -export const Bls12381BBS23SigDockSigName = 'Bls12381BBSSignatureDock2023'; -export const Bls12381BBDT16MacDockName = 'Bls12381BBDT16MACDock2024'; -export const Bls12381BBDT16MacProofDockName = 'Bls12381BBDT16MACProofDock2024'; -export const Bls12381BBS23SigProofDockSigName = 'Bls12381BBSSignatureProofDock2023'; -export const Bls12381BBS23DockVerKeyName = 'Bls12381BBSVerificationKeyDock2023'; -export const Bls12381BBDT16DockVerKeyName = 'Bls12381BBDT16VerificationKeyDock2024'; -export const Bls12381PSSigDockSigName = 'Bls12381PSSignatureDock2023'; -export const Bls12381PSSigProofDockSigName = 'Bls12381PSSignatureProofDock2023'; -export const Bls12381PSDockVerKeyName = 'Bls12381PSVerificationKeyDock2023'; -export const JsonWebKey2020VerKeyName = 'JsonWebKey2020'; +export { + Bls12381BBDT16DockVerKeyName, + Bls12381BBDT16MacDockName, + Bls12381BBDT16MacProofDockName, + Bls12381BBSDockVerKeyName, + Bls12381BBS23DockVerKeyName, + Bls12381BBS23SigDockSigName, + Bls12381BBS23SigProofDockSigName, + Bls12381BBSSigDockSigName, + Bls12381BBSSigProofDockSigName, + Bls12381PSDockVerKeyName, + Bls12381PSSigDockSigName, + Bls12381PSSigProofDockSigName, + EcdsaSecp256k1SigName, + EcdsaSecp256k1VerKeyName, + EcdsaSecp256r1SigName, + EcdsaSecp256r1VerKeyName, + Ed255192020SigName, + Ed255192020VerKeyName, + Ed25519SigName, + Ed25519VerKeyName, + JsonWebKey2020VerKeyName, + Sr25519SigName, + Sr25519VerKeyName, +} from '@docknetwork/crypto-utils/vc'; diff --git a/packages/credential-sdk/src/vc/jws.js b/packages/credential-sdk/src/vc/jws.js index db59c59fb..279aa2847 100644 --- a/packages/credential-sdk/src/vc/jws.js +++ b/packages/credential-sdk/src/vc/jws.js @@ -1,52 +1,7 @@ -import base64url from 'base64url'; - -const detachedHeaderParams = { - b64: false, - crit: ['b64'], -}; - -// Taken from https://github.com/transmute-industries/verifiable-data/blob/main/packages/jose-ld/src/JWS/createSigner.ts -export async function signJWS(signer, type, options, data) { - if (!type) { - return signer.sign({ data }); - } - - const header = { - alg: type, - ...options.header, - ...(options.detached ? detachedHeaderParams : undefined), - }; - const encodedHeader = base64url.encode(JSON.stringify(header)); - const encodedPayload = base64url.encode( - data instanceof Uint8Array - ? Buffer.from(data).toString('utf-8') - : JSON.stringify(data), - ); - - const toBeSigned = options.detached - ? new Uint8Array( - Buffer.concat([ - Buffer.from(encodedHeader, 'utf8'), - Buffer.from('.', 'utf-8'), - data, - ]), - ) - : new Uint8Array(Buffer.from(`${encodedHeader}.${encodedPayload}`)); - - const signature = await signer.sign({ data: toBeSigned }); - - // If not, encode it ourselves - return options.detached - ? `${encodedHeader}..${base64url.encode(Buffer.from(signature))}` - : `${encodedHeader}.${encodedPayload}.${base64url.encode( - Buffer.from(signature), - )}`; -} - -export function createJws({ encodedHeader, verifyData }) { - const buffer = Buffer.concat([ - Buffer.from(`${encodedHeader}.`, 'utf8'), - Buffer.from(verifyData.buffer, verifyData.byteOffset, verifyData.length), - ]); - return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.length); -} +export { + createJws, + createJwsSigner, + createRawSigner, + joseSignatureToDER, + signJWS, +} from '@docknetwork/crypto-utils/vc'; diff --git a/packages/credential-sdk/tests/crypto-utils-compatibility.test.js b/packages/credential-sdk/tests/crypto-utils-compatibility.test.js new file mode 100644 index 000000000..db002cf94 --- /dev/null +++ b/packages/credential-sdk/tests/crypto-utils-compatibility.test.js @@ -0,0 +1,91 @@ +import { + DockKeypair, + Ed25519Keypair, + Secp256k1Keypair, + Secp256r1Keypair, +} from '@docknetwork/crypto-utils/keypairs'; +import { + PublicKeyEd25519, + SignatureEd25519, +} from '@docknetwork/crypto-utils/types'; +import { + ensureBytes, + ensureInstanceOf, + maybeToJSON, +} from '@docknetwork/crypto-utils/utils'; +import { + createJws, + createJwsSigner, + createRawSigner, + joseSignatureToDER, + signJWS, +} from '@docknetwork/crypto-utils/vc'; + +import CredentialDockKeypair from '../src/keypairs/dock-keypair'; +import CredentialEd25519Keypair from '../src/keypairs/keypair-ed25519'; +import CredentialSecp256k1Keypair from '../src/keypairs/keypair-secp256k1'; +import CredentialSecp256r1Keypair from '../src/keypairs/keypair-secp256r1'; +import DidKeypair from '../src/keypairs/did-keypair'; +import { + PublicKeyEd25519 as CredentialPublicKeyEd25519, +} from '../src/types/public-keys'; +import { + SignatureEd25519 as CredentialSignatureEd25519, +} from '../src/types/signatures'; +import { + ensureBytes as credentialEnsureBytes, + ensureInstanceOf as credentialEnsureInstanceOf, + maybeToJSON as credentialMaybeToJSON, +} from '../src/utils'; +import { + createJws as credentialCreateJws, + createJwsSigner as credentialCreateJwsSigner, + createRawSigner as credentialCreateRawSigner, + joseSignatureToDER as credentialJoseSignatureToDER, + signJWS as credentialSignJWS, +} from '../src/vc/jws'; + +describe('crypto-utils compatibility shims', () => { + test('preserve keypair constructor identity', () => { + expect(CredentialDockKeypair).toBe(DockKeypair); + expect(CredentialEd25519Keypair).toBe(Ed25519Keypair); + expect(CredentialSecp256k1Keypair).toBe(Secp256k1Keypair); + expect(CredentialSecp256r1Keypair).toBe(Secp256r1Keypair); + }); + + test('preserve public key and signature constructor identity', () => { + expect(CredentialPublicKeyEd25519).toBe(PublicKeyEd25519); + expect(CredentialSignatureEd25519).toBe(SignatureEd25519); + }); + + test('preserve utility function identity', () => { + expect(credentialEnsureBytes).toBe(ensureBytes); + expect(credentialEnsureInstanceOf).toBe(ensureInstanceOf); + expect(credentialMaybeToJSON).toBe(maybeToJSON); + }); + + test('preserve JWS function identity', () => { + expect(credentialCreateJws).toBe(createJws); + expect(credentialCreateJwsSigner).toBe(createJwsSigner); + expect(credentialCreateRawSigner).toBe(createRawSigner); + expect(credentialJoseSignatureToDER).toBe(joseSignatureToDER); + expect(credentialSignJWS).toBe(signJWS); + }); + + test('adapt DidKeypair signatures for ES256 JWS', async () => { + const keypair = CredentialSecp256r1Keypair.fromEntropy( + new Uint8Array(CredentialSecp256r1Keypair.SeedSize).fill(1), + ); + const didKeypair = DidKeypair.didMethodKey(keypair); + const signature = await credentialCreateJwsSigner(didKeypair).sign({ + data: Uint8Array.of(1, 2, 3), + }); + + expect(signature).toHaveLength(64); + expect(CredentialSecp256r1Keypair.verify( + Uint8Array.of(1, 2, 3), + joseSignatureToDER(signature), + didKeypair.publicKey(), + )).toBe(true); + }); +}); diff --git a/packages/crypto-utils/.eslintrc.json b/packages/crypto-utils/.eslintrc.json new file mode 100644 index 000000000..79873d08a --- /dev/null +++ b/packages/crypto-utils/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "parser": "@babel/eslint-parser", + "parserOptions": { + "requireConfigFile": false, + "babelOptions": { + "babelrc": false, + "configFile": false, + "presets": ["@babel/preset-env"], + "sourceType": "module" + } + } +} diff --git a/packages/crypto-utils/CHANGELOG.md b/packages/crypto-utils/CHANGELOG.md new file mode 100644 index 000000000..c42a13d5b --- /dev/null +++ b/packages/crypto-utils/CHANGELOG.md @@ -0,0 +1,7 @@ +# @docknetwork/crypto-utils + +## 0.2.0 + +### Minor Changes + +- Create crypto-utils package diff --git a/packages/crypto-utils/LICENSE b/packages/crypto-utils/LICENSE new file mode 100644 index 000000000..f8d1edb3b --- /dev/null +++ b/packages/crypto-utils/LICENSE @@ -0,0 +1,9 @@ +The MIT License (MIT) + +Copyright (c) 2019 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/crypto-utils/README.md b/packages/crypto-utils/README.md new file mode 100644 index 000000000..dd0e8c6c5 --- /dev/null +++ b/packages/crypto-utils/README.md @@ -0,0 +1,34 @@ +# @docknetwork/crypto-utils + +Lightweight crypto primitives shared by Dock SDK packages. The package provides +Ed25519, secp256k1, and secp256r1 keypairs, signature and public-key value +types, byte utilities, verification-method constants, and JWS helpers. + +```js +import { + Ed25519Keypair, + createJwsSigner, + createRawSigner, + joseSignatureToDER, + signJWS, +} from '@docknetwork/crypto-utils'; + +const keypair = Ed25519Keypair.random(); +const signer = createRawSigner(keypair); +const signature = await signer.sign({ data: new Uint8Array([1, 2, 3]) }); +const jwsSigner = createJwsSigner(keypair); +const jws = await signJWS(jwsSigner, 'EdDSA', { detached: true, header: {} }, new Uint8Array([1, 2, 3])); +``` + +`createRawSigner` unwraps the SDK signature value returned by +`DockKeypair.sign`. It intentionally preserves Dock's existing wire formats: +64-byte Ed25519 signatures and 65-byte `r || s || recovery` ECDSA signatures. +It does not perform JOSE or IEEE P1363 conversion. + +Use `createJwsSigner` with `signJWS`. It unwraps `DockKeypair` and +`DidKeypair` signatures and converts Dock's 65-byte ECDSA representation to +the 64-byte `r || s` representation required by JOSE. To verify that JOSE +representation with a Dock ECDSA keypair, first convert it to DER with +`joseSignatureToDER`. + +Subpath exports are available at `keypairs`, `types`, `utils`, and `vc`. diff --git a/packages/crypto-utils/jest.config.js b/packages/crypto-utils/jest.config.js new file mode 100644 index 000000000..419b83e8b --- /dev/null +++ b/packages/crypto-utils/jest.config.js @@ -0,0 +1,12 @@ +export default { + bail: true, + clearMocks: true, + testEnvironment: 'node', + transform: { + '^.+\\.(ts|js)$': ['babel-jest', { rootMode: 'upward' }], + }, + transformIgnorePatterns: [ + '/node_modules/(?!@stablelib|uuid)', + ], + verbose: true, +}; diff --git a/packages/crypto-utils/package.json b/packages/crypto-utils/package.json new file mode 100644 index 000000000..a1ed29d06 --- /dev/null +++ b/packages/crypto-utils/package.json @@ -0,0 +1,78 @@ +{ + "name": "@docknetwork/crypto-utils", + "version": "0.2.0", + "description": "Lightweight Dock keypairs, crypto value types, byte utilities, and JWS helpers", + "license": "MIT", + "type": "module", + "files": [ + "dist", + "CHANGELOG.md", + "README.md", + "LICENSE" + ], + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.cjs", + "default": "./dist/esm/index.js" + }, + "./keypairs": { + "import": "./dist/esm/keypairs/index.js", + "require": "./dist/cjs/keypairs/index.cjs", + "default": "./dist/esm/keypairs/index.js" + }, + "./types": { + "import": "./dist/esm/types/index.js", + "require": "./dist/cjs/types/index.cjs", + "default": "./dist/esm/types/index.js" + }, + "./utils": { + "import": "./dist/esm/utils/index.js", + "require": "./dist/cjs/utils/index.cjs", + "default": "./dist/esm/utils/index.js" + }, + "./vc": { + "import": "./dist/esm/vc/index.js", + "require": "./dist/cjs/vc/index.cjs", + "default": "./dist/esm/vc/index.js" + }, + "./src/*": { + "import": "./dist/esm/*", + "require": "./dist/cjs/*", + "default": "./dist/esm/*" + } + }, + "repository": { + "type": "git", + "url": "https://github.com/docknetwork/sdk" + }, + "publishConfig": { + "access": "public" + }, + "engines": { + "node": ">=22.0.0" + }, + "dependencies": { + "@stablelib/ed25519": "^1.0.0", + "base64url": "^3.0.1", + "elliptic": "6.6.1", + "js-sha256": "^0.11.0" + }, + "devDependencies": { + "@babel/eslint-parser": "^7.25.9", + "@rollup/plugin-commonjs": "^28.0.1", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.3.1", + "babel-jest": "^29.7.0", + "eslint": "^8.0.0", + "jest": "^29.7.0", + "rollup": "^4.28.0", + "rollup-plugin-multi-input": "^1.4.1" + }, + "scripts": { + "build": "rollup -c", + "test": "jest --runInBand", + "lint": "eslint \"src/**/*.js\"", + "prepublishOnly": "yarn build" + } +} diff --git a/packages/crypto-utils/rollup.config.mjs b/packages/crypto-utils/rollup.config.mjs new file mode 100644 index 000000000..ac8dbb896 --- /dev/null +++ b/packages/crypto-utils/rollup.config.mjs @@ -0,0 +1,49 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import multiInput from 'rollup-plugin-multi-input'; + +const sep = path.sep; +const packageRoot = path.normalize(path.dirname(fileURLToPath(import.meta.url))); + +function isExternal(id) { + if (id.startsWith('\0')) return false; + if (id.startsWith('.') || id.startsWith(`src${sep}`) || id.startsWith('src/')) { + return false; + } + if (path.isAbsolute(id)) { + const normalized = path.normalize(id); + if (normalized.startsWith(packageRoot)) return false; + return id.includes(`${sep}node_modules${sep}`); + } + return true; +} + +export default [{ + input: ['src/**/*.js'], + external: isExternal, + plugins: [ + multiInput(), + nodeResolve({ preferBuiltins: true }), + json({ preferConst: true }), + commonjs(), + ], + output: [ + { + sourcemap: true, + dir: 'dist/esm', + format: 'esm', + entryFileNames: '[name].js', + }, + { + sourcemap: true, + dir: 'dist/cjs', + format: 'cjs', + entryFileNames: '[name].cjs', + interop: 'auto', + }, + ], +}]; diff --git a/packages/crypto-utils/src/index.js b/packages/crypto-utils/src/index.js new file mode 100644 index 000000000..bd20b12b6 --- /dev/null +++ b/packages/crypto-utils/src/index.js @@ -0,0 +1,4 @@ +export * from './keypairs'; +export * from './types'; +export * from './utils'; +export * from './vc'; diff --git a/packages/crypto-utils/src/keypairs/dock-keypair.js b/packages/crypto-utils/src/keypairs/dock-keypair.js new file mode 100644 index 000000000..0d8bd2c3c --- /dev/null +++ b/packages/crypto-utils/src/keypairs/dock-keypair.js @@ -0,0 +1,123 @@ +import { + withExtendedPrototypeProperties, + withExtendedStaticProperties, +} from '../utils'; +import { randomAsHex } from '../utils/types/bytes'; + +/** + * Abstract keypair used to sign byte sequences of arbitrary size. + */ +class DockKeypair { + /** + * Instantiates new `DockKeypair` with the provided underlying keypair. + * + * @param {*} keyPair + */ + constructor(keyPair) { + this.keyPair = keyPair; + } + + /** + * Generates `DockKeypair` from the supplied entropy. + * @param {Uint8Array} entropy + * @returns {DockKeypair} + */ + static fromEntropy(entropy) { + // eslint-disable-next-line no-underscore-dangle + return new this(entropy, 'entropy'); + } + + /** + * Generates `DockKeypair` from the supplied seed. + * @param {Uint8Array} seed + * @returns {DockKeypair} + */ + static fromSeed(seed) { + // eslint-disable-next-line no-underscore-dangle + return new this(seed, 'seed'); + } + + /** + * Generates `DockKeypair` from the supplied private key. + * @param {Uint8Array} privateKey + * @returns {DockKeypair} + */ + static fromPrivateKey(privateKey) { + // eslint-disable-next-line no-underscore-dangle + return new this(privateKey, 'private'); + } + + /** + * Generates random `DockKeypair`. + * @returns {DockKeypair} + */ + static random() { + return new this(randomAsHex(this.SeedSize)); + } + + /** + * Returns underlying verification key type. + */ + get verKeyType() { + return this.constructor.VerKeyType; + } + + /** + * Returns underlying public key. + */ + publicKey() { + // eslint-disable-next-line no-underscore-dangle + return new this.constructor.Signature.PublicKey(this._publicKey()); + } + + /** + * Returns underlying private key. + */ + privateKey() { + throw new Error('Unimplemented'); + } + + /** + * Signs supplied message using underlying keypair. + * @param {*} message + */ + sign(message) { + // eslint-disable-next-line no-underscore-dangle + return new this.constructor.Signature(this._sign(message)); + } + + /** + * Returns raw bytes of the underlying public key. + * @returns {Uint8Array} + */ + _publicKey() { + throw new Error('Unimplemented'); + } + + /** + * Signs supplied message and returns raw signature bytes. + * @returns {Uint8Array} + */ + _sign(_message) { + throw new Error('Unimplemented'); + } + + /** + * Verifies signature produced over supplied message's using given public key. + * @param {Uint8Array} _message + * @param {Uint8Array | Signature} _signature + * @param {Uint8Array | PublicKey} _publicKey + * @returns {boolean} + */ + static verify(_message, _signature, _publicKey) { + throw new Error('Unimplemented'); + } +} + +export default withExtendedPrototypeProperties( + ['privateKey', '_publicKey', '_sign'], + withExtendedStaticProperties( + ['Signature', 'VerKeyType', 'SeedSize', 'verify', 'constructor'], + DockKeypair, + ), +); diff --git a/packages/crypto-utils/src/keypairs/index.js b/packages/crypto-utils/src/keypairs/index.js new file mode 100644 index 000000000..197dadf7b --- /dev/null +++ b/packages/crypto-utils/src/keypairs/index.js @@ -0,0 +1,4 @@ +export { default as DockKeypair } from './dock-keypair'; +export { default as Ed25519Keypair } from './keypair-ed25519'; +export { default as Secp256k1Keypair } from './keypair-secp256k1'; +export { default as Secp256r1Keypair } from './keypair-secp256r1'; diff --git a/packages/crypto-utils/src/keypairs/keypair-ed25519.js b/packages/crypto-utils/src/keypairs/keypair-ed25519.js new file mode 100644 index 000000000..d49ed2bfe --- /dev/null +++ b/packages/crypto-utils/src/keypairs/keypair-ed25519.js @@ -0,0 +1,65 @@ +import { + generateKeyPairFromSeed, + sign, + verify, + extractPublicKeyFromSecretKey, +} from '@stablelib/ed25519'; +import { SignatureEd25519 } from '../types/signatures'; +import { Ed25519VerKeyName } from '../vc/crypto/constants'; +import { normalizeToU8a, valueBytes } from '../utils'; +import DockKeypair from './dock-keypair'; + +export default class Ed25519Keypair extends DockKeypair { + static Signature = SignatureEd25519; + + static VerKeyType = Ed25519VerKeyName; + + static SeedSize = 32; + + /** + * + * Instantiates new `Ed25519Keypair` from the provided source. + * It can have one of two types: "seed" or "private". + * + * @param {Uint8Array} seedOrPrivate + * @param {"seed"|"private"} sourceType + */ + constructor(seedOrPrivate, sourceType = 'seed') { + let kp; + switch (sourceType) { + case 'seed': + kp = generateKeyPairFromSeed(normalizeToU8a(seedOrPrivate)); + break; + case 'private': { + const secretKey = normalizeToU8a(seedOrPrivate); + kp = { + secretKey, + publicKey: extractPublicKeyFromSecretKey(secretKey), + }; + break; + } + default: + throw new Error( + `Unsupported source type: \`${sourceType}\`, it must be either "seed" or "private"`, + ); + } + + super(kp); + } + + _publicKey() { + return this.keyPair.publicKey; + } + + privateKey() { + return this.keyPair.secretKey; + } + + _sign(message) { + return sign(this.keyPair.secretKey, message); + } + + static verify(message, signature, publicKey) { + return verify(valueBytes(publicKey), message, valueBytes(signature)); + } +} diff --git a/packages/crypto-utils/src/keypairs/keypair-secp256k1.js b/packages/crypto-utils/src/keypairs/keypair-secp256k1.js new file mode 100644 index 000000000..43a5f59fe --- /dev/null +++ b/packages/crypto-utils/src/keypairs/keypair-secp256k1.js @@ -0,0 +1,140 @@ +import elliptic from 'elliptic'; +import { sha256 } from 'js-sha256'; + +import { SignatureSecp256k1 } from '../types/signatures'; +import { EcdsaSecp256k1VerKeyName } from '../vc/crypto/constants'; +import DockKeypair from './dock-keypair'; +import { hexToU8a, normalizeToU8a, valueBytes } from '../utils'; + +const EC = elliptic.ec; +const secp256k1Curve = new EC('secp256k1'); + +function encodeDERInt(intBytes) { + // Remove leading zeros + let i = 0; + while (i < intBytes.length && intBytes[i] === 0) { + i++; + } + + let trimmedBytes = intBytes.slice(i); + + // Ensure the integer is positive by adding a leading zero if needed + // eslint-disable-next-line no-bitwise + if (trimmedBytes[0] & 0x80) { + trimmedBytes = Uint8Array.of(0, ...trimmedBytes); + } + + const { length } = trimmedBytes; + return [0x02, length, ...trimmedBytes]; +} + +export default class Secp256k1Keypair extends DockKeypair { + static Signature = SignatureSecp256k1; + + static VerKeyType = EcdsaSecp256k1VerKeyName; + + static SeedSize = 32; + + /** + * + * Instantiates new `Secp256k1Keypair` from the provided source. + * It can have one of two types: "entropy" or "private". + * + * @param {Uint8Array} entropyOrPrivate + * @param {"entropy"|"private"} sourceType + */ + constructor(entropyOrPrivate, sourceType = 'entropy') { + let kp; + switch (sourceType) { + case 'entropy': + if (entropyOrPrivate == null) { + throw new Error('Entropy must be provided'); + } + + kp = secp256k1Curve.genKeyPair({ + entropy: entropyOrPrivate, + }); + break; + case 'private': + kp = secp256k1Curve.keyFromPrivate(normalizeToU8a(entropyOrPrivate)); + break; + default: + throw new Error( + `Unknown source type: \`${sourceType}\`, it must be either "entropy" or "private"`, + ); + } + + super(kp); + } + + _publicKey() { + // public key is in hex but doesn't contain a leading zero + return hexToU8a(`0x${this.keyPair.getPublic(true, 'hex')}`); + } + + privateKey() { + return hexToU8a(`0x${this.keyPair.getPrivate('hex')}`); + } + + _sign(message) { + const hash = this.constructor.hash(message); + const sig = this.keyPair.sign(hash, { + canonical: true, + }); + + // The signature is recoverable in 65-byte { R | S | index } format + const r = sig.r.toString('hex', 32); + const s = sig.s.toString('hex', 32); + const i = sig.recoveryParam.toString(16).padStart(2, '0'); + + // Make it proper hex + return hexToU8a(`0x${r}${s}${i}`); + } + + static signatureToDER(signature) { + const sigBytes = valueBytes(signature); + if (sigBytes.length !== 65) { + throw new Error( + `Invalid signature length. Expected is 65 bytes, received ${sigBytes.length}`, + ); + } + // Extract R, S components + const r = sigBytes.slice(0, 32); // First 32 bytes + const s = sigBytes.slice(32, 64); // Next 32 bytes + + // Encode R and S into DER format + const rEncoded = encodeDERInt(r); + const sEncoded = encodeDERInt(s); + + // Calculate total length + const totalLength = rEncoded.length + sEncoded.length; + + // Create Uint8Array for DER encoding + const der = new Uint8Array(2 + totalLength); // 2 bytes for tag and length + total payload + der[0] = 0x30; // DER sequence tag + der[1] = totalLength; // Total length of R + S encoded + + // Copy encoded R and S into DER array + der.set(rEncoded, 2); + der.set(sEncoded, 2 + rEncoded.length); + + return der; + } + + static hash(message) { + return sha256.digest(message); + } + + static verify(message, signature, publicKey) { + let bytes = valueBytes(signature); + if (bytes.length === 65) { + bytes = this.signatureToDER(bytes); + } + + return secp256k1Curve.verify( + this.hash(message), + bytes, + valueBytes(publicKey), + ); + } +} diff --git a/packages/crypto-utils/src/keypairs/keypair-secp256r1.js b/packages/crypto-utils/src/keypairs/keypair-secp256r1.js new file mode 100644 index 000000000..497404d24 --- /dev/null +++ b/packages/crypto-utils/src/keypairs/keypair-secp256r1.js @@ -0,0 +1,123 @@ +import elliptic from 'elliptic'; +import { sha256 } from 'js-sha256'; + +import { SignatureSecp256r1 } from '../types/signatures'; +import { EcdsaSecp256r1VerKeyName } from '../vc/crypto/constants'; +import DockKeypair from './dock-keypair'; +import { hexToU8a, normalizeToU8a, valueBytes } from '../utils'; + +const EC = elliptic.ec; +const secp256r1Curve = new EC('p256'); + +function encodeDERInt(intBytes) { + let i = 0; + while (i < intBytes.length && intBytes[i] === 0) { + i++; + } + + let trimmedBytes = intBytes.slice(i); + + // eslint-disable-next-line no-bitwise + if (trimmedBytes[0] & 0x80) { + trimmedBytes = Uint8Array.of(0, ...trimmedBytes); + } + + const { length } = trimmedBytes; + return [0x02, length, ...trimmedBytes]; +} + +export default class Secp256r1Keypair extends DockKeypair { + static Signature = SignatureSecp256r1; + + static VerKeyType = EcdsaSecp256r1VerKeyName; + + static SeedSize = 32; + + constructor(entropyOrPrivate, sourceType = 'entropy') { + let kp; + switch (sourceType) { + case 'entropy': + if (entropyOrPrivate == null) { + throw new Error('Entropy must be provided'); + } + + kp = secp256r1Curve.genKeyPair({ + entropy: entropyOrPrivate, + }); + break; + case 'private': + kp = secp256r1Curve.keyFromPrivate(normalizeToU8a(entropyOrPrivate)); + break; + default: + throw new Error( + `Unknown source type: \`${sourceType}\`, it must be either "entropy" or "private"`, + ); + } + + super(kp); + } + + _publicKey() { + return hexToU8a(`0x${this.keyPair.getPublic(true, 'hex')}`); + } + + privateKey() { + return hexToU8a(`0x${this.keyPair.getPrivate('hex')}`); + } + + _sign(message) { + const hash = this.constructor.hash(message); + const sig = this.keyPair.sign(hash, { + canonical: true, + }); + + const r = sig.r.toString('hex', 32); + const s = sig.s.toString('hex', 32); + const i = sig.recoveryParam.toString(16).padStart(2, '0'); + + return hexToU8a(`0x${r}${s}${i}`); + } + + static signatureToDER(signature) { + const sigBytes = valueBytes(signature); + if (sigBytes.length !== 65) { + throw new Error( + `Invalid signature length. Expected is 65 bytes, received ${sigBytes.length}`, + ); + } + + const r = sigBytes.slice(0, 32); + const s = sigBytes.slice(32, 64); + + const rEncoded = encodeDERInt(r); + const sEncoded = encodeDERInt(s); + + const totalLength = rEncoded.length + sEncoded.length; + + const der = new Uint8Array(2 + totalLength); + der[0] = 0x30; + der[1] = totalLength; + + der.set(rEncoded, 2); + der.set(sEncoded, 2 + rEncoded.length); + + return der; + } + + static hash(message) { + return sha256.digest(message); + } + + static verify(message, signature, publicKey) { + let bytes = valueBytes(signature); + if (bytes.length === 65) { + bytes = this.signatureToDER(bytes); + } + + return secp256r1Curve.verify( + this.hash(message), + bytes, + valueBytes(publicKey), + ); + } +} diff --git a/packages/crypto-utils/src/types/generic/any-of.js b/packages/crypto-utils/src/types/generic/any-of.js new file mode 100644 index 000000000..ed58d9c16 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/any-of.js @@ -0,0 +1,43 @@ +import { fmtIterable } from '../../utils'; +import withFrom from './with-from'; + +class NeverInstantiated { + constructor() { + throw new Error('Should never be instantiated'); + } +} + +/** + * Creates a new type that can be constructed from any of the provided types. + * The resulting type will attempt to parse a value using each provided type in order, + * returning the first successful construction or throwing an error if none succeed. + * + * @template T + * @param {...T[]} classes - Array of types to try constructing from + * @returns {anyOfTypes} A new type that can be constructed from any of the provided types + */ +export default function anyOf(...classes) { + const name = `anyOf(${fmtIterable(classes.map((klass) => klass.name))})`; + + const obj = { + [name]: class extends withFrom(NeverInstantiated, (value) => { + let err; + + for (const klass of classes) { + try { + return klass.from(value); + } catch (e) { + if (err != null) { + e.message = `${err.message}; ${e.message}`; + } + + err = e; + } + } + + throw err; + }) {}, + }; + + return obj[name]; +} diff --git a/packages/crypto-utils/src/types/generic/create-placeholder.js b/packages/crypto-utils/src/types/generic/create-placeholder.js new file mode 100644 index 000000000..a2568d095 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/create-placeholder.js @@ -0,0 +1,23 @@ +import { NotAConstructor } from '../../utils/interfaces'; + +/** + * Creates a typed placeholder that implements `from`/`fromJSON`/`fromApi` and creates a value from the supplied + * argument using `makePlacholder` function. + * + * @template P + * @param {function(*): P} makePlaceholder + * @returns {function(*): P} + */ +export default function createPlaceholder(makePlaceholder) { + const name = `placeholder(${makePlaceholder.name})`; + const obj = { + [name]: (...args) => makePlaceholder(...args), + }; + + obj[name][NotAConstructor] = true; + obj[name].from = obj[name]; + obj[name].fromApi = obj[name]; + obj[name].fromJSON = obj[name]; + + return obj[name]; +} diff --git a/packages/crypto-utils/src/types/generic/index.js b/packages/crypto-utils/src/types/generic/index.js new file mode 100644 index 000000000..dda1e40cf --- /dev/null +++ b/packages/crypto-utils/src/types/generic/index.js @@ -0,0 +1,9 @@ +export { default as TypedEnum } from './typed-enum'; +export { default as TypedBytes } from './typed-bytes'; +export { default as createPlaceholder } from './create-placeholder'; +export { default as Null } from './typed-null'; +export { default as sized } from './sized'; +export { default as withBase } from './with-base'; +export { default as anyOf } from './any-of'; +export { default as withFrom } from './with-from'; +export { default as withFromJSONBytes } from './with-from-json-bytes'; diff --git a/packages/crypto-utils/src/types/generic/sized.js b/packages/crypto-utils/src/types/generic/sized.js new file mode 100644 index 000000000..9799c2d88 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/sized.js @@ -0,0 +1,78 @@ +import { + allObjectPropertiesIncludingPrototypes, + ensureNumber, + withExtendedStaticProperties, +} from '../../utils'; + +/** + * Extends supplied class to check that its itstance lenght is always equal to the `static Size` property + * defined on the extended class. + * + * @template C + * @param {C} klass + * @param {function(*): number} getSize + * @returns {C} + */ +export default function sized(klass) { + const name = `Sized<${klass.name}>`; + const methods = [ + ...allObjectPropertiesIncludingPrototypes(klass.prototype), + ].filter( + (prop) => typeof klass.prototype[prop] === 'function' && prop !== 'constructor', + ); + + const obj = { + [name]: class extends klass { + /** + * @static + * @type {number} + * Size of the underlying data. + */ + static Size; + + constructor(...args) { + super(...args); + + this.ensureValidSize(); + } + + getSize() { + return ensureNumber(this.length); + } + + ensureValidSize() { + const { Size } = this.constructor; + + if (typeof Size !== 'number') { + throw new Error( + `Size must be a number, received \`${Size?.toString()}\``, + ); + } + + const size = this.getSize(); + + if (size !== Size) { + throw new Error( + `Received \`${size}\` items while expected \`${Size}\` by \`${this.constructor.name}\``, + ); + } + } + + static random() { + return super.random(this.Size); + } + }, + }; + + for (const method of methods) { + // eslint-disable-next-line func-names + obj[name].prototype[method] = function (...args) { + const res = klass.prototype[method].apply(this, args); + this.ensureValidSize(); + + return res; + }; + } + + return withExtendedStaticProperties(['Size'], obj[name]); +} diff --git a/packages/crypto-utils/src/types/generic/typed-bytes.js b/packages/crypto-utils/src/types/generic/typed-bytes.js new file mode 100644 index 000000000..0916d9a16 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/typed-bytes.js @@ -0,0 +1,156 @@ +import { + ensureBytes, + normalizeToU8a, + randomAsU8a, + u8aToHex, + u8aToU8a, +} from '../../utils/types/bytes'; +import { ArrayWithoutPrototypeMethods } from '../../utils'; +import withBase from './with-base'; +import withCatchNull from './with-catch-null'; +import withEq from './with-eq'; + +class TypedBytes extends withBase(ArrayWithoutPrototypeMethods) { + /** + * Creates an instance of TypedBytes from provided bytes. + * @param {Uint8Array|Array|string} bytes - Bytes in various formats + * @throws Will throw if bytes are invalid + */ + constructor(bytes) { + super(); + + this.set(normalizeToU8a(bytes)); + } + + /** + * Hex string representation of the bytes + * @type {string} + */ + get value() { + return this.toHex(); + } + + /** + * The raw Uint8Array representation of the bytes + * @type {Uint8Array} + */ + get bytes() { + return u8aToU8a(Array.prototype.values.call(this)); + } + + /** + * Sets the byte values for this instance + * @param {(Uint8Array|Array|string)} maybeBytes - Bytes in various formats + * @throws Will throw if invalid bytes are provided + */ + set(maybeBytes) { + const bytes = ensureBytes(maybeBytes); + this.length = bytes.length; + + for (let i = 0; i < bytes.length; i++) { + this[i] = bytes[i]; + } + } + + /** + * Creates a new TypedBytes instance from API data + * @param {*} value - API data to create from + */ + static fromApi(value) { + return new this(value); + } + + /** + * Generates random bytes of specified size + * @param {number} size - Number of bytes to generate (must be between 1-65535) + * @returns {TypedBytes} New instance with random bytes + * @throws Will throw if invalid size is provided + */ + static random(size) { + if (!Number.isInteger(size) || size < 0 || size > 65535) { + throw new Error( + `Expected a natural number between 1 and 65535, received: \`${size}\``, + ); + } + + return new this(randomAsU8a(size)); + } + + /** + * Converts bytes to hex string representation + * @returns {string} Hex string + */ + toHex() { + return u8aToHex(this); + } + + /** + * Returns JSON representation of the bytes (hex string) + * @returns {string} Hex string suitable for JSON + */ + toJSON() { + return this.toHex(); + } + + /** + * Applies a function to the value property + * @param {Function} fn - Function to apply + * @returns {*} Result of applying function + */ + apply(fn) { + return fn(this.value); + } + + /** + * Creates TypedBytes instance from JSON data + * @param {string} json - Hex string from JSON + */ + static fromJSON(json) { + return new this(json); + } + + /** + * Creates a new TypedBytes instance from various input types + * @param {*} obj - Input to create from + * @returns {TypedBytes} New instance + */ + static from(obj) { + if (obj instanceof this) { + return obj; + } else if (!Array.isArray(obj) && typeof obj !== 'string') { + return this.fromApi(obj); + } else { + return new this(obj); + } + } + + /** + * Checks equality with another TypedBytes instance + * @param {TypedBytes|string} other - Value to compare against + * @returns {boolean} True if equal, false otherwise + */ + eq(other) { + return String(this) === String(other); + } + + /** + * Returns string representation of the bytes (hex) + * @returns {string} Hex string representation + */ + toString() { + return this.toHex(); + } + + /** + * Returns locale-specific string representation (same as toString()) + * @returns {string} Hex string representation + */ + toLocaleString() { + return this.toString(); + } +} + +/** + * Class representing sequence of bytes. + */ +export default withEq(withCatchNull(TypedBytes)); diff --git a/packages/crypto-utils/src/types/generic/typed-enum.js b/packages/crypto-utils/src/types/generic/typed-enum.js new file mode 100644 index 000000000..ff747824d --- /dev/null +++ b/packages/crypto-utils/src/types/generic/typed-enum.js @@ -0,0 +1,335 @@ +/* eslint-disable max-classes-per-file */ +import { fmtIterable } from '../../utils'; +import { + isEqualToOrPrototypeOf, + withExtendedStaticProperties, +} from '../../utils/inheritance'; +import { + maybeEq, + maybeFrom, + maybeNew, + maybeToJSONString, +} from '../../utils/interfaces'; +import withBase from './with-base'; +import Null from './typed-null'; +import withCatchNull from './with-catch-null'; +import withEq from './with-eq'; +import withFromJSONBytes from './with-from-json-bytes'; + +/** + * @template V + */ +class TypedEnum extends withBase(class EnumBase {}) { + /** + * Possible subclasses. + * @type {Array} + * @static Variants + */ + static Variants; + + /** + * Class used to construct underlying value. + * @type {Function} + * @static Class + */ + static Class; + + /** + * The type of the enumeration. + * @type {string} + * @static + */ + static get Type() { + return this.Class.Type; + } + + /** + * Associates enum parameters with the base enum class and vice versa. + * @param {...this} classes + */ + /* eslint-disable */ + static bindVariants(...classes) { + class ClassIsBoundError extends Error { + constructor(klass) { + super(`Class \`${klass}\` is already bound`); + } + } + + if (Object.hasOwnProperty(this, "Variants")) { + throw new ClassIsBoundError(this); + } + + for (const klass of classes) { + if (klass === this) { + throw new Error(`\`${klass.name}\` can't be a variant of itself`); + } + if (!klass.Type) + throw new Error(`No \`Type\` specified in \`${klass.name}\``); + + const { Class, Type, isIdentifier, asIdentifier } = klass; + + Object.defineProperty(this.prototype, isIdentifier, { + get() { + return this instanceof klass; + }, + enumerable: false, + }); + + Object.defineProperty(this.prototype, asIdentifier, { + get() { + if (this[isIdentifier]) { + return this[Type]; + } else { + throw new Error( + `Not a \`${Type}\` by \`${this.constructor.name}\`` + ); + } + }, + enumerable: false, + }); + + const that = this; + + // TODO: revisit this + if (Class.Variants) { + const klassFrom = Class.from; + + for (const Variant of Class.Variants) { + Variant.from = function from(value) { + if (value instanceof that) { + return klassFrom.call(this, value[asIdentifier]); + } else { + return klassFrom.call(this, value); + } + }; + } + } + + const klassFrom = Class.from; + Class.from = function from(value) { + if (value instanceof that) { + return value[asIdentifier]; + } else { + return klassFrom.call(this, value); + } + }; + } + + this.Variants = classes; + } + /* eslint-enable */ + + static get isIdentifier() { + const { JsonType } = this; + + return `is${JsonType}`; + } + + static get asIdentifier() { + const { JsonType } = this; + + return `as${JsonType}`; + } + + static get JsonType() { + const { Type } = this; + + return `${Type[0].toUpperCase()}${Type.slice(1)}`; + } + + /** + * Create a new TypedEnum instance. + * @param {V} value - The value of the enumeration. + */ + constructor(value) { + super(); + + const { Class, name } = this.constructor; + + if (Class == null) { + throw new Error(`\`Class\` property is not specified for \`${name}\``); + } + + try { + this[this.type] = maybeFrom(Class, value); + } catch (error) { + error.message = `\nFailed to construct variant \`${Class.name}\` of \`${name}\`:\n${error}`; + + throw error; + } + } + + /** + * Get the type of the enumeration. + * @returns {string} The type of the enumeration. + */ + get type() { + return this.constructor.Type; + } + + get jsonType() { + return this.constructor.JsonType; + } + + /** + * Get the value of the enumeration. + * @returns {V} The value of the enumeration. + */ + get value() { + return this[this.type]; + } + + apply(fn) { + if (this.constructor.isNullish) { + return fn(this.constructor.JsonType); + } else { + const { type, value } = this; + + return { + [type]: fn(value), + }; + } + } + + // eslint-disable-next-line + static fromJSON(json) { + if (json == null) { + throw new Error( + `Received \`null\` while object was expected by \`${this.name}\``, + ); + } + if (typeof json === 'string' && this.isNullish) { + if (this.Class != null) { + if (this.JsonType === json || this.Type === json) { + return maybeNew(this, []); + } + + throw new Error( + `Unexpected json in \`${this}\`: \`${json}\`, expected \`${this.JsonType}\``, + ); + } else { + for (const Variant of this.Variants) { + if (Variant.JsonType === json || Variant.Type === json) { + return Variant.fromJSON(json); + } + } + + throw new Error( + `Unexpected json in \`${this}\`: \`${json}\`, expected one of ${fmtIterable( + new Set([...this.Variants].flatMap((v) => [v.JsonType, v.Type])), + )}`, + ); + } + } + + const keys = Object.keys(json); + if (keys.length !== 1) { + throw new Error( + `Expected object with 1 key, received \`${json}\` with keys: ${fmtIterable( + keys, + )} by ${this.name}`, + ); + } + const [key] = keys; + + if (this.Class != null) { + if (this.JsonType === key || this.Type === key) { + return maybeNew(this, [json[key]]); + } + + throw new Error( + `Unexpected key \`${key}\`, expected \`${this.JsonType}\` by \`${this.name}\``, + ); + } else { + for (const Variant of this.Variants) { + if (Variant.JsonType === key || Variant.Type === key) { + return Variant.fromJSON(json); + } + } + + throw new Error( + `Invalid key \`${key}\`, expected one of ${fmtIterable( + new Set([...this.Variants].flatMap((v) => [v.JsonType, v.Type])), + )} by \`${this.name}\``, + ); + } + } + + static fromApi(obj) { + if (this.Class != null) { + if (obj[this.isIdentifier]) { + return maybeNew(this, [obj[this.asIdentifier]]); + } else { + throw new Error( + `Incompatible value provided: \`${obj}\` to \`${this}\``, + ); + } + } else { + for (const Variant of this.Variants) { + if (obj[Variant.isIdentifier]) { + return Variant.fromApi(obj); + } + } + } + + throw new Error( + `Invalid object received: \`${maybeToJSONString( + obj, + )}\`, expected to build an instance of ${fmtIterable( + this.Variants.map((v) => v.Type), + )} by \`${this.name}\``, + ); + } + + static variant(obj) { + return this.Variants.find((variant) => isEqualToOrPrototypeOf(variant.Class, obj?.constructor ?? Null)); + } + + static directVariant(obj) { + return this.Variants.find((variant) => isEqualToOrPrototypeOf(variant, obj?.constructor ?? Null)); + } + + static get isNullish() { + return this.Class + ? this.Class === Null + : this.Variants.every((variant) => variant.Class === Null); + } + + static from(obj) { + if (obj instanceof this) { + return obj; + } else if (typeof obj === 'string' && this.isNullish) { + return this.fromJSON(obj); + } else if (Object.getPrototypeOf(obj) === Object.getPrototypeOf({})) { + return this.fromJSON(obj); + } else if (this.Class == null) { + const Variant = this.variant(obj); + + if (Variant) { + return new Variant(obj); + } else { + const DirectVariant = this.directVariant(obj); + + if (DirectVariant != null) { + return DirectVariant.from(obj); + } + } + } else if (obj instanceof this.Class) { + return new this(this.Class.from(obj)); + } + + return this.fromApi(obj); + } + + eq(other) { + return this.type === other.type && maybeEq(this.value, other.value); + } + + toString() { + return JSON.stringify(this.toJSON()); + } +} + +export default withFromJSONBytes( + withEq(withCatchNull(withExtendedStaticProperties(['Variants'], TypedEnum))), +); diff --git a/packages/crypto-utils/src/types/generic/typed-null.js b/packages/crypto-utils/src/types/generic/typed-null.js new file mode 100644 index 000000000..b18ced280 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/typed-null.js @@ -0,0 +1,11 @@ +import createPlaceholder from './create-placeholder'; + +const Null = (value) => { + if (value == null || !String(value).length) { + return null; + } else { + throw new Error(`Unexpected value received: \`${value}\``); + } +}; + +export default createPlaceholder(Null); diff --git a/packages/crypto-utils/src/types/generic/with-base.js b/packages/crypto-utils/src/types/generic/with-base.js new file mode 100644 index 000000000..3166566e7 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/with-base.js @@ -0,0 +1,115 @@ +import { stringToU8a } from '../../utils'; +import { + withExtendedStaticProperties, + withExtendedPrototypeProperties, +} from '../../utils/inheritance'; +import { maybeEq, maybeToJSON } from '../../utils/interfaces'; +import withCatchNull from './with-catch-null'; +import withEq from './with-eq'; +import withFromJSONBytes from './with-from-json-bytes'; + +/** + * Enhances provided class by adding prototype methods `toString`/`eq`, static `from` and requiring + * successor to implement static `fromJSON`/`fromApi` and prototype `toJSON`. + * + * @templace C + * @param {C} klass + * @returns {C} + */ +export default function withBase(klass) { + const name = `withBase(${klass.name})`; + + const classes = { + [name]: class extends klass { + /** + * Attempts to instantiate `this` from the provided JSON object. + * @param {object} json + */ + static fromJSON(_json) { + throw new Error('Unimplemented'); + } + + /** + * Attempts to instantiate `this` from the provided object received from the API side. + * @param {object} json + */ + static fromApi(_obj) { + throw new Error('Unimplemented'); + } + + /** + * Instantiates `this` from the supplied object. + * @param obj + */ + static from(obj) { + if (obj instanceof this) { + return obj; + } else if (Object.getPrototypeOf(obj) === Object.getPrototypeOf({})) { + return this.fromJSON(obj); + } else { + return this.fromApi(obj); + } + } + + /** + * Converts the instance to a JSON representation. + * @returns {object} The JSON representation of the instance. + */ + toJSON() { + return this.apply(maybeToJSON); + } + + /** + * Recursively applies supplied function to the underlying values. + * @template T + * @param {function(this): T} _fn + * @returns {T} + */ + apply(_fn) { + throw new Error('Unimplemented'); + } + + /** + * Converts the instance to a stringified JSON representation. + * @returns {string} The JSON representation of the instance. + */ + toJSONString() { + return JSON.stringify(this.toJSON()); + } + + /** + * Converts the instance to bytes of the stringified JSON representation. + * @returns {Uint8Array} The JSON representation of the instance. + */ + toJSONStringBytes() { + return stringToU8a(this.toJSONString()); + } + + /** + * Converts the instance to a string representation. + * @returns {string} + */ + toString() { + return JSON.stringify(this.toJSON()); + } + + /** + * Performs an equality check against other value. + * + * @param {*} other + * @returns {boolean} + */ + eq(other) { + return maybeEq(this.value, other.value); + } + }, + }; + + return withExtendedStaticProperties( + ['fromJSON', 'fromApi'], + withExtendedPrototypeProperties( + ['apply'], + withFromJSONBytes(withEq(withCatchNull(classes[name]))), + ), + ); +} diff --git a/packages/crypto-utils/src/types/generic/with-catch-null.js b/packages/crypto-utils/src/types/generic/with-catch-null.js new file mode 100644 index 000000000..6da82da4e --- /dev/null +++ b/packages/crypto-utils/src/types/generic/with-catch-null.js @@ -0,0 +1,45 @@ +/** + * Wraps supplied klass into a class that catches `null`ish values in `from`/`fromJSON`/`fromApi` methods. + * @template C + * @param {C} klass + * @returns {C} + */ +export default function withCatchNull(klass) { + const name = `withCatchNull(${klass.name})`; + + const obj = { + [name]: class extends klass { + static from(value) { + if (value == null) { + throw new Error( + `Unexpected \`null\`ish value received by \`${this.name}\``, + ); + } else { + return super.from(value); + } + } + + static fromJSON(value) { + if (value == null) { + throw new Error( + `Unexpected \`null\`ish value received by \`${this.name}\``, + ); + } else { + return super.fromJSON(value); + } + } + + static fromApi(value) { + if (value == null) { + throw new Error( + `Unexpected \`null\`ish value received by \`${this.name}\``, + ); + } else { + return super.fromApi(value); + } + } + }, + }; + + return obj[name]; +} diff --git a/packages/crypto-utils/src/types/generic/with-eq.js b/packages/crypto-utils/src/types/generic/with-eq.js new file mode 100644 index 000000000..d6484a094 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/with-eq.js @@ -0,0 +1,41 @@ +import anyOf from './any-of'; + +/** + * Enhances `prototype.eq` of the provided class to make it catch `null`ish values and + * attempt to instantiate a new value of the supplied class in case if constructor is different. + * @template C + * @param {C} klass + * @returns {C} + */ +export default function withEq(klass) { + const name = `withEq(${klass.name})`; + + const obj = { + [name]: class extends klass { + eq(other) { + if (other == null) { + return false; + } else if (Object.is(this, other)) { + return true; + } else { + let compareWith = other; + const { value, constructor } = this; + const tryFrom = [ + constructor, + value?.constructor, + value?.constructor?.Class, + ].filter((fn) => typeof fn?.from === 'function'); + try { + compareWith = constructor.from(anyOf(...tryFrom).from(compareWith)); + } catch (err) { + return false; + } + + return super.eq(compareWith); + } + } + }, + }; + + return obj[name]; +} diff --git a/packages/crypto-utils/src/types/generic/with-from-json-bytes.js b/packages/crypto-utils/src/types/generic/with-from-json-bytes.js new file mode 100644 index 000000000..71ccc8e01 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/with-from-json-bytes.js @@ -0,0 +1,19 @@ +import { u8aToString } from '../../utils'; + +export default function withFromJSONBytes(klass) { + const name = `withFromJSONBytes(${klass.name})`; + + const obj = { + [name]: class extends klass { + static from(value) { + if (value instanceof Uint8Array) { + return this.fromJSON(JSON.parse(u8aToString(value))); + } else { + return super.from(value); + } + } + }, + }; + + return obj[name]; +} diff --git a/packages/crypto-utils/src/types/generic/with-from.js b/packages/crypto-utils/src/types/generic/with-from.js new file mode 100644 index 000000000..9bc67dbd1 --- /dev/null +++ b/packages/crypto-utils/src/types/generic/with-from.js @@ -0,0 +1,25 @@ +export default function withFrom(klass, from, fromJSON = from, fromApi = from) { + const name = `withFrom(${klass.name})`; + + if (!from) { + throw new Error(`No \`from\` function provided for \`${klass.name}\``); + } + + const obj = { + [name]: class extends klass { + static from(value) { + return from.call(this, value, (v) => super.from(v)); + } + + static fromJSON(value) { + return fromJSON.call(this, value, (v) => super.fromJSON(v)); + } + + static fromApi(value) { + return fromApi.call(this, value, (v) => super.fromApi(v)); + } + }, + }; + + return obj[name]; +} diff --git a/packages/crypto-utils/src/types/index.js b/packages/crypto-utils/src/types/index.js new file mode 100644 index 000000000..a1170d8ee --- /dev/null +++ b/packages/crypto-utils/src/types/index.js @@ -0,0 +1,3 @@ +export * from './generic'; +export * from './public-keys'; +export * from './signatures'; diff --git a/packages/crypto-utils/src/types/public-keys/index.js b/packages/crypto-utils/src/types/public-keys/index.js new file mode 100644 index 000000000..6b85914d4 --- /dev/null +++ b/packages/crypto-utils/src/types/public-keys/index.js @@ -0,0 +1,7 @@ +export * from './public-key'; + +export { default as PublicKeySr25519Value } from './public-key-sr25519-value'; +export { default as PublicKeyEd25519Value } from './public-key-ed25519-value'; +export { default as PublicKeySecp256k1Value } from './public-key-secp256k1-value'; +export { default as PublicKeySecp256r1Value } from './public-key-secp256r1-value'; +export { default as PublicKeyX25519Value } from './public-key-x25519-value'; diff --git a/packages/crypto-utils/src/types/public-keys/public-key-ed25519-value.js b/packages/crypto-utils/src/types/public-keys/public-key-ed25519-value.js new file mode 100644 index 000000000..02b2de8ff --- /dev/null +++ b/packages/crypto-utils/src/types/public-keys/public-key-ed25519-value.js @@ -0,0 +1,11 @@ +import { sized, TypedBytes } from '../generic'; +import { Ed25519VerKeyName } from '../../vc/crypto/constants'; + +/** Class representing value of a Ed25519 PublicKey */ +export default class PublicKeyEd25519Value extends sized(TypedBytes) { + static Type = 'ed25519'; + + static Size = 32; + + static VerKeyType = Ed25519VerKeyName; +} diff --git a/packages/crypto-utils/src/types/public-keys/public-key-secp256k1-value.js b/packages/crypto-utils/src/types/public-keys/public-key-secp256k1-value.js new file mode 100644 index 000000000..2f952fb18 --- /dev/null +++ b/packages/crypto-utils/src/types/public-keys/public-key-secp256k1-value.js @@ -0,0 +1,11 @@ +import { sized, TypedBytes } from '../generic'; +import { EcdsaSecp256k1VerKeyName } from '../../vc/crypto/constants'; + +/** Class representing value of a compressed Secp256k1 PublicKey */ +export default class PublicKeySecp256k1Value extends sized(TypedBytes) { + static Type = 'secp256k1'; + + static Size = 33; + + static VerKeyType = EcdsaSecp256k1VerKeyName; +} diff --git a/packages/crypto-utils/src/types/public-keys/public-key-secp256r1-value.js b/packages/crypto-utils/src/types/public-keys/public-key-secp256r1-value.js new file mode 100644 index 000000000..d19a1e3ec --- /dev/null +++ b/packages/crypto-utils/src/types/public-keys/public-key-secp256r1-value.js @@ -0,0 +1,11 @@ +import { sized, TypedBytes } from '../generic'; +import { EcdsaSecp256r1VerKeyName } from '../../vc/crypto/constants'; + +/** Class representing value of a compressed Secp256r1 PublicKey */ +export default class PublicKeySecp256r1Value extends sized(TypedBytes) { + static Type = 'secp256r1'; + + static Size = 33; + + static VerKeyType = EcdsaSecp256r1VerKeyName; +} diff --git a/packages/crypto-utils/src/types/public-keys/public-key-sr25519-value.js b/packages/crypto-utils/src/types/public-keys/public-key-sr25519-value.js new file mode 100644 index 000000000..c726d3286 --- /dev/null +++ b/packages/crypto-utils/src/types/public-keys/public-key-sr25519-value.js @@ -0,0 +1,11 @@ +import { sized, TypedBytes } from '../generic'; +import { Sr25519VerKeyName } from '../../vc/crypto/constants'; + +/** Class representing value of a Sr25519 PublicKey */ +export default class PublicKeySr25519Value extends sized(TypedBytes) { + static Type = 'sr25519'; + + static Size = 32; + + static VerKeyType = Sr25519VerKeyName; +} diff --git a/packages/crypto-utils/src/types/public-keys/public-key-x25519-value.js b/packages/crypto-utils/src/types/public-keys/public-key-x25519-value.js new file mode 100644 index 000000000..3768f1416 --- /dev/null +++ b/packages/crypto-utils/src/types/public-keys/public-key-x25519-value.js @@ -0,0 +1,10 @@ +import { sized, TypedBytes } from '../generic'; + +/** Class representing value of a X25519 PublicKey */ +export default class PublicKeyX25519Value extends sized(TypedBytes) { + static Type = 'x25519'; + + static Size = 32; + + static VerKeyType = 'X25519KeyAgreementKey2019'; +} diff --git a/packages/crypto-utils/src/types/public-keys/public-key.js b/packages/crypto-utils/src/types/public-keys/public-key.js new file mode 100644 index 000000000..25eeb1227 --- /dev/null +++ b/packages/crypto-utils/src/types/public-keys/public-key.js @@ -0,0 +1,65 @@ +import { TypedEnum } from '../generic'; +import PublicKeyX25519Value from './public-key-x25519-value'; +import PublicKeyEd25519Value from './public-key-ed25519-value'; +import PublicKeySecp256k1Value from './public-key-secp256k1-value'; +import PublicKeySecp256r1Value from './public-key-secp256r1-value'; +import PublicKeySr25519Value from './public-key-sr25519-value'; + +/** + * Class representing either Ed25519 or Secp256k1 or Sr25519 or X25519 PublicKey + * @class + * @extends {TypedEnum} + */ +export class PublicKey extends TypedEnum { + static get VerKeyType() { + return this.Class.VerKeyType; + } +} +/** + * Class representing Ed25519 PublicKey + * @class + * @extends {PublicKey} + */ +export class PublicKeyEd25519 extends PublicKey { + static Class = PublicKeyEd25519Value; +} +/** + * Class representing Secp256k1 PublicKey + * @class + * @extends {PublicKey} + */ +export class PublicKeySecp256k1 extends PublicKey { + static Class = PublicKeySecp256k1Value; +} +/** + * Class representing Secp256r1 PublicKey + * @class + * @extends {PublicKey} + */ +export class PublicKeySecp256r1 extends PublicKey { + static Class = PublicKeySecp256r1Value; +} +/** + * Class representing Sr25519 PublicKey + * @class + * @extends {PublicKey} + */ +export class PublicKeySr25519 extends PublicKey { + static Class = PublicKeySr25519Value; +} +/** + * Class representing X25519 PublicKey + * @class + * @extends {PublicKey} + */ +export class PublicKeyX25519 extends PublicKey { + static Class = PublicKeyX25519Value; +} + +PublicKey.bindVariants( + PublicKeyEd25519, + PublicKeySecp256k1, + PublicKeySecp256r1, + PublicKeySr25519, + PublicKeyX25519, +); diff --git a/packages/crypto-utils/src/types/signatures/index.js b/packages/crypto-utils/src/types/signatures/index.js new file mode 100644 index 000000000..79f00cc3e --- /dev/null +++ b/packages/crypto-utils/src/types/signatures/index.js @@ -0,0 +1,14 @@ +import SignatureSr25519Value from './signature-sr25519-value'; +import SignatureEd25519Value from './signature-ed25519-value'; +import SignatureSecp256k1Value from './signature-secp256k1-value'; +import SignatureSecp256r1Value from './signature-secp256r1-value'; +import SignatureValue from './signature-value'; + +export { + SignatureSr25519Value, + SignatureEd25519Value, + SignatureSecp256k1Value, + SignatureSecp256r1Value, + SignatureValue, +}; +export * from './signature'; diff --git a/packages/crypto-utils/src/types/signatures/signature-ed25519-value.js b/packages/crypto-utils/src/types/signatures/signature-ed25519-value.js new file mode 100644 index 000000000..0e3631924 --- /dev/null +++ b/packages/crypto-utils/src/types/signatures/signature-ed25519-value.js @@ -0,0 +1,9 @@ +import { PublicKeyEd25519 } from '../public-keys'; +import SignatureValue from './signature-value'; + +/** Class representing a Ed25519 Signature */ +export default class SignatureEd25519Value extends SignatureValue { + static PublicKey = PublicKeyEd25519; + + static Size = 64; +} diff --git a/packages/crypto-utils/src/types/signatures/signature-secp256k1-value.js b/packages/crypto-utils/src/types/signatures/signature-secp256k1-value.js new file mode 100644 index 000000000..87f852d06 --- /dev/null +++ b/packages/crypto-utils/src/types/signatures/signature-secp256k1-value.js @@ -0,0 +1,9 @@ +import SignatureValue from './signature-value'; +import { PublicKeySecp256k1 } from '../public-keys'; + +/** Class representing a Secp256k1 Signature */ +export default class SignatureSecp256k1Value extends SignatureValue { + static PublicKey = PublicKeySecp256k1; + + static Size = 65; +} diff --git a/packages/crypto-utils/src/types/signatures/signature-secp256r1-value.js b/packages/crypto-utils/src/types/signatures/signature-secp256r1-value.js new file mode 100644 index 000000000..7a84a6d07 --- /dev/null +++ b/packages/crypto-utils/src/types/signatures/signature-secp256r1-value.js @@ -0,0 +1,9 @@ +import SignatureValue from './signature-value'; +import { PublicKeySecp256r1 } from '../public-keys'; + +/** Class representing a Secp256r1 Signature */ +export default class SignatureSecp256r1Value extends SignatureValue { + static PublicKey = PublicKeySecp256r1; + + static Size = 65; +} diff --git a/packages/crypto-utils/src/types/signatures/signature-sr25519-value.js b/packages/crypto-utils/src/types/signatures/signature-sr25519-value.js new file mode 100644 index 000000000..bc033f14b --- /dev/null +++ b/packages/crypto-utils/src/types/signatures/signature-sr25519-value.js @@ -0,0 +1,9 @@ +import { PublicKeySr25519 } from '../public-keys'; +import SignatureValue from './signature-value'; + +/** Class representing a Sr25519 Signature */ +export default class SignatureSr25519Value extends SignatureValue { + static PublicKey = PublicKeySr25519; + + static Size = 64; +} diff --git a/packages/crypto-utils/src/types/signatures/signature-value.js b/packages/crypto-utils/src/types/signatures/signature-value.js new file mode 100644 index 000000000..a3f074db4 --- /dev/null +++ b/packages/crypto-utils/src/types/signatures/signature-value.js @@ -0,0 +1,25 @@ +import { withExtendedStaticProperties } from '../../utils/inheritance'; +import { sized, TypedBytes } from '../generic'; + +/** Class representing a Signature. This export class should always be extended (abstract export class in some languages) */ +export default withExtendedStaticProperties( + ['PublicKey'], + class SignatureValue extends sized(TypedBytes) { + /** + * Public key associated with the signature. + * @type {typeof PublicKey} + */ + static PublicKey; + + static get Type() { + return this.PublicKey.Type; + } + + /** + * Returns the size of the underlying signature. + */ + get size() { + return this.constructor.Size; + } + }, +); diff --git a/packages/crypto-utils/src/types/signatures/signature.js b/packages/crypto-utils/src/types/signatures/signature.js new file mode 100644 index 000000000..56e1f6f50 --- /dev/null +++ b/packages/crypto-utils/src/types/signatures/signature.js @@ -0,0 +1,59 @@ +import { TypedEnum } from '../generic'; +import SignatureEd25519Value from './signature-ed25519-value'; +import SignatureSecp256k1Value from './signature-secp256k1-value'; +import SignatureSecp256r1Value from './signature-secp256r1-value'; +import SignatureSr25519Value from './signature-sr25519-value'; + +/** + * Class representing either Ed25519 or Secp256k1 or Sr25519 Signature + * @class + * @extends {TypedEnum} + */ +export class Signature extends TypedEnum { + static get PublicKey() { + return this.Class.PublicKey; + } + + get bytes() { + return this.value.bytes; + } +} +/** + * Class representing Ed25519 Signature + * @class + * @extends {Signature} + */ +export class SignatureEd25519 extends Signature { + static Class = SignatureEd25519Value; +} +/** + * Class representing Secp256k1 Signature + * @class + * @extends {Signature} + */ +export class SignatureSecp256k1 extends Signature { + static Class = SignatureSecp256k1Value; +} +/** + * Class representing Secp256r1 Signature + * @class + * @extends {Signature} + */ +export class SignatureSecp256r1 extends Signature { + static Class = SignatureSecp256r1Value; +} +/** + * Class representing Sr25519 Signature + * @class + * @extends {SignatureEd25519OrSignatureSecp256k1OrSignatureSr25519} + */ +export class SignatureSr25519 extends Signature { + static Class = SignatureSr25519Value; +} + +Signature.bindVariants( + SignatureEd25519, + SignatureSecp256k1, + SignatureSecp256r1, + SignatureSr25519, +); diff --git a/packages/crypto-utils/src/utils/assert.js b/packages/crypto-utils/src/utils/assert.js new file mode 100644 index 000000000..aab980d98 --- /dev/null +++ b/packages/crypto-utils/src/utils/assert.js @@ -0,0 +1,52 @@ +import { panic } from './error'; + +/** + * Asserts that a condition is true; otherwise, throws an error using the panic function. + * @param {boolean} cond - The condition to check. + * @param {function | string | Error} error - The error message, function returning an error, or an Error object to throw. + */ +export const assert = (cond, error) => { + if (!cond) { + panic(error); + } +}; + +/** + * Error thrown when the provided function was executed more than once or wasn't executed at all. + */ +export class MustBeCalledOnce extends Error { + constructor(fn) { + super(`Function must be executed exactly once: \`${fn}\``); + } + + static ensure(fn, call) { + let executed = false; + + const name = `mustBeExecutedOnce(${fn.name})`; + const obj = { + [name](...args) { + let res; + let err; + + assert(!executed, () => new this(fn)); + try { + res = fn.apply(this, args); + } catch (e) { + err = e; + } finally { + executed = true; + } + + if (err != null) { + throw err; + } + return res; + }, + }; + + const res = call(obj[name]); + assert(executed, () => new this(fn)); + + return res; + } +} diff --git a/packages/crypto-utils/src/utils/error.js b/packages/crypto-utils/src/utils/error.js new file mode 100644 index 000000000..814e33e1f --- /dev/null +++ b/packages/crypto-utils/src/utils/error.js @@ -0,0 +1,52 @@ +/** + * Throws an error with the provided message or generated by a function. + * @param {function | string | Error} err - The error message, function returning an error, or an Error object. + */ +export const panic = (error) => { + if (typeof error === 'function') { + return panic(error()); + } + + throw error instanceof Error ? error : new Error(error); +}; + +/** + * Wraps a function to prefix its error messages with a specified string. + * If an error is thrown by the wrapped function, this error will be caught, + * modified to include the provided prefix in its message, and then rethrown. + * + * @param {function(): T} fn - The function to wrap + * @param {string} prefix - Prefix to add before the original error message + * @returns {T} The return value of the wrapped function + * + * @example + * const wrappedFn = catchErrorWith('Error in component', () => { + * throw new Error('Something went wrong'); + * }); + * + * try { + * wrappedFn(); + * } catch (err) { + * console.error(err.message); // "Error in component: Something went wrong" + * } + */ +export const catchErrorWith = (prefix, fn) => { + try { + return fn(); + } catch (err) { + err.message = `${prefix}: ${err.message}`; + + throw err; + } +}; + +/** + * Creates a function that will prefix any error messages from the original + * function call with the specified string. This is useful for adding context + * to errors thrown by functions that don't directly handle their own error messaging. + * + * @param {function} fn - The function to wrap + * @param {string} prefix - Prefix to add before any error messages + * @returns {function(...args): T} A new function that will execute the original function with provided arguments and prefix its errors + */ +export const catchFnErrorWith = (prefix, fn) => (...args) => catchErrorWith(prefix, () => fn(...args)); diff --git a/packages/crypto-utils/src/utils/index.js b/packages/crypto-utils/src/utils/index.js new file mode 100644 index 000000000..157bf87a8 --- /dev/null +++ b/packages/crypto-utils/src/utils/index.js @@ -0,0 +1,5 @@ +export * from './types'; +export * from './interfaces'; +export * from './inheritance'; +export * from './error'; +export * from './assert'; diff --git a/packages/crypto-utils/src/utils/inheritance.js b/packages/crypto-utils/src/utils/inheritance.js new file mode 100644 index 000000000..91fe24290 --- /dev/null +++ b/packages/crypto-utils/src/utils/inheritance.js @@ -0,0 +1,250 @@ +import { ensureObjectWithAnyConstructor } from './types/object'; +import { fmtIterable } from './types/iterable'; +import { ensureNoIntersection } from './types/set'; + +/** + * Enhances the provided class with the given list of static properties to require + * in the inherited class. + * All properties will be checked for presence during the object constructor call. + * Each property on its own will be checked every time it is accessed. + * In case some property is missing, an error will be thrown. + * + * @template T + * @param {Array} properties + * @param {T} parentClass + * @returns {T} + */ +// eslint-disable-next-line sonarjs/cognitive-complexity +export function withExtendedStaticProperties(properties, parentClass) { + const name = `withExtStatics(${parentClass.name}, ${fmtIterable( + properties, + )})`; + + const extendedClass = { + [name]: class extends parentClass { + constructor(...args) { + super(...args); + + /* + * Ensures that properties are extended properly. + */ + for (const property of properties) { + try { + if (this.constructor[property] === parentClass[property]) { + throw new Error( + `Static property \`${property}\` of \`${this.constructor.name}\` isn't extended properly`, + ); + } + } catch (err) { + err.message = `Failed to check the prototype property: \n${err.message}`; + + throw err; + } + } + } + }, + }; + + for (const property of properties) { + const propertySymbol = Symbol(property); + + Object.defineProperty(extendedClass[name], property, { + get() { + if ( + this !== extendedClass[name] + && this[propertySymbol] === parentClass[property] + ) { + throw new Error( + `Property \`${property}\` of \`${this.name}\` isn't extended properly`, + ); + } + return this[propertySymbol]; + }, + set(newValue) { + if (Object.hasOwnProperty.call(this, propertySymbol)) { + throw new Error( + `Can't override the property \`${property}\` of \`${this.name}\``, + ); + } + + this[propertySymbol] = newValue; + }, + }); + } + + return extendedClass[name]; +} + +/** + * Enhances the provided class with the given list of prototype properties to require + * in the inherited class. + * All properties will be checked for presence during the object constructor call. + * In case some property is missing, an error will be thrown. + * + * @template T + * @param {Array} properties + * @param {T} parentClass + * @returns {T} + */ +export function withExtendedPrototypeProperties(properties, parentClass) { + const name = `withExtProto(${parentClass.name}, ${fmtIterable(properties)})`; + + const extendedClass = { + [name]: class extends parentClass { + constructor(...args) { + super(...args); + const proto = Object.getPrototypeOf(this); + + /* + * Ensures that properties are extended properly. + */ + for (const property of properties) { + try { + if (proto[property] === parentClass.prototype[property]) { + throw new Error( + `Property \`${property}\` of the object prototype of \`${this.constructor.name}\` isn't extended properly`, + ); + } + } catch (err) { + err.message = `Failed to check the static property: \n${err.message}`; + + throw err; + } + } + } + }, + }; + + return extendedClass[name]; +} + +const ObjectProperties = new WeakMap(); + +/** + * Returns a Set containing all properties of the given object and its prototype chain. + * + * @param {Object} obj - The object to retrieve properties from. + * @returns {Set} - A set of properties. + */ +export const allObjectPropertiesIncludingPrototypes = (obj) => { + if (obj == null) return new Set(); + + let props = ObjectProperties.get(obj); + if (props == null) { + props = new Set([ + ...Object.getOwnPropertyNames(obj), + ...allObjectPropertiesIncludingPrototypes(Object.getPrototypeOf(obj)), + ]); + ObjectProperties.set(obj, props); + } + + return props; +}; + +/** + * Validates that object properties don't intersect with prototype properties. + * + * @param {Object} obj - The object to validate. + */ +export const validateProperties = (obj) => ensureNoIntersection( + new Set(Object.getOwnPropertyNames(obj)), + allObjectPropertiesIncludingPrototypes(Object.getPrototypeOf(obj)), +); + +/** + * Checks if an object is the prototype of another object. + * + * @param {Object} proto - The prototype object. + * @param {Object} obj - The object to check against. + * @returns {boolean} - Whether the object is the prototype. + */ +export const isPrototypeOf = (proto, obj) => Object.isPrototypeOf.call(proto, obj); + +/** + * Checks if an object is equal to or a prototype of another object. + * + * @param {Object} proto - The prototype object. + * @param {Object} obj - The object to check. + * @returns {boolean} - Whether the object is equal to or a prototype of the other. + */ +export const isEqualToOrPrototypeOf = (proto, obj) => Object.is(proto, obj) || isPrototypeOf(proto, obj); + +/** + * Ensures an object is equal to or a prototype of another object. + * + * @param {Object} proto - The prototype object to check against. + * @param {Object} obj - The object to validate. + * @returns {Object} - The validated object if it meets the requirement. + * @throws {Error} - If the object doesn't match the prototype. + */ +export const ensureEqualToOrPrototypeOf = (proto, obj) => { + if (isEqualToOrPrototypeOf(proto, obj)) { + return obj; + } + + throw new Error( + `Expected \`${proto.name}\` to be equal to or a prototype of \`${obj.name}\``, + ); +}; + +/** + * Creates a new class that extends the given class but hides prototype functions from + * the original class on its own prototype. The resulting class will not have any + * enumerable functions from the original class except for those explicitly ignored. + * + * @param {Function} klass - The original class to extend + * @param {Set} ignore - Optional set of member names to keep visible + * @returns {Function} The new extended class without prototype functions + */ +export const withoutPrototypeFunctions = (klass, ignore = new Set()) => { + const name = `withoutPrototypeFunctions(${klass.name})`; + + const obj = { + [name]: class extends klass {}, + }; + + for (const member of Object.getOwnPropertyNames(klass.prototype)) { + if (!ignore.has(member) && typeof klass.prototype[member] === 'function') { + Object.defineProperty(obj[name].prototype, member, { + enumerable: false, + }); + } + } + + return obj[name]; +}; + +/** + * Ensures that the given value is an instance of the specified class. Throws an error with details if not. + * + * @param {*} value - The value to check. + * @param {Function} klass - The constructor or class to check against. + * @throws If value is not a Uint8Array. + * @returns {*} - The original value if it's an instance of the class. + */ +export const ensureInstanceOf = (value, klass) => { + if (ensureObjectWithAnyConstructor(value) instanceof klass) { + return value; + } + + throw new Error( + `Expected \`${value}\` with constructor \`${value?.constructor.name}\` to be an instance of \`${klass.name}\``, + ); +}; + +/** + * Ensures that the given prototype is that of the value. Throws an error with a message if not. + * + * @param {object} proto - The expected prototype. + * @param {*} value - The value to check. + * @returns {*} - The original value if it has the correct prototype. + */ +export const ensurePrototypeOf = (proto, value) => { + if (isPrototypeOf(proto, value)) { + return value; + } + + throw new Error( + `Expected \`${proto.name}\` to be a prototype of \`${value}\``, + ); +}; diff --git a/packages/crypto-utils/src/utils/interfaces.js b/packages/crypto-utils/src/utils/interfaces.js new file mode 100644 index 000000000..58aed5e13 --- /dev/null +++ b/packages/crypto-utils/src/utils/interfaces.js @@ -0,0 +1,115 @@ +import { MustBeCalledOnce } from './assert'; + +/** + * Attempts to call `value.toJSON()`, returns `JSON.parse(JSON.stringify(value))` if method doesn't exist. + * @template T + * @param {T} value + * @returns {object} + */ +export const maybeToJSON = (value) => (typeof value?.toJSON === 'function' + ? value.toJSON() + : JSON.parse(JSON.stringify(value))); + +/** + * Stringifies the provided value converted to JSON. + * @template T + * @param {T} value + * @returns {string} + */ +export const maybeToJSONString = (value) => JSON.stringify(maybeToJSON(value)); + +/** + * Attempts to convert provided value to the Cheqd Payload or JSON. + * @param {*} value + * @returns {object} + */ +export const maybeToCheqdPayloadOrJSON = (obj) => (typeof obj?.toCheqdPayload === 'function' // eslint-disable-line no-nested-ternary + ? obj.toCheqdPayload() + : typeof obj?.apply === 'function' // eslint-disable-line no-nested-ternary + ? obj.apply(maybeToCheqdPayloadOrJSON) + : typeof value !== 'object' && typeof value !== 'function' + ? obj + : maybeToJSON(obj)); + +/** + * Returns bytes of the value converted to a stringified JSON. + * @template T + * @param {T} value + * @returns {string} + */ +export const maybeToJSONStringBytes = (value) => (typeof value?.maybeToJSONStringBytes === 'function' + ? value.toJSONStringBytes() + : Uint8Array.from(Buffer.from(maybeToJSONString(value)))); + +/** + * Attempts to compare two values using `value.eq(other)`, returns `boolean`. + * @template T + * @param {T} value + * @param {T} other + * @returns {boolean} + */ +export const maybeEq = (value, other) => (typeof value?.eq === 'function' ? value.eq(other) : value === other); + +/** + * Attempts to call `value.toHuman()` or `value.toJSON`, returns `value` if methods don't exist. + * @template T + * @param {T} value + * @returns {object} + */ +export const maybeToHuman = (obj) => (obj && typeof obj.toHuman === 'function' ? obj.toHuman() : maybeToJSON(obj)); + +/** + * Attempts to call `value.toNumber()`, returns `+value` if method doesn't exist. + * @template T + * @param {T} value + * @returns {number} + */ +export const maybeToNumber = (value) => (typeof value?.toNumber === 'function' ? value.toNumber() : +value); + +/** + * Marks function that it can't be used as a constructor. + */ +export const NotAConstructor = Symbol.for( + '@docknetwork/credential-sdk/NotAConstructor', +); + +/** + * Attempts to intantiate new object of the supplied class using provided arguments. + * @param Class + * @param args + */ +export const maybeNew = (Class, args) => (!Class[NotAConstructor] ? new Class(...args) : Class.apply(Class, args)); + +/** + * Attempts to create new instance of the supplied class using `Class.from(obj)`, instantiates class if `from` doesn't exist. + * @param Class + * @param args + */ +export const maybeFrom = (klass, obj) => (typeof klass.from === 'function' ? klass.from(obj) : maybeNew(klass, [obj])); + +/** + * Applies function to the first value of the provided object that passes the check. + * @template T + * @template I + * @template O + * @param {function(I): boolean} check + * @param {function(I): O} fn + * @param {T} value + * @returns {O} + */ +export const applyToValue = (check, fn, value) => { + if (check(value)) { + return fn(value); + } else if (typeof value?.apply === 'function') { + let res; + MustBeCalledOnce.ensure( + (obj) => { + res = applyToValue(check, fn, obj); + }, + (wrapped) => value.apply(wrapped), + ); + return res; + } + + throw new Error("`fn` can't be applied because value didn't pass the check"); +}; diff --git a/packages/crypto-utils/src/utils/types/array.js b/packages/crypto-utils/src/utils/types/array.js new file mode 100644 index 000000000..d154ffc9b --- /dev/null +++ b/packages/crypto-utils/src/utils/types/array.js @@ -0,0 +1,94 @@ +import { withoutPrototypeFunctions } from '../inheritance'; + +/** + * Return true if the given value is an array. + * @param value + * @returns {boolean} + */ +export function isArray(value) { + return Array.isArray(value); +} + +/** + * Ensures that the given value is an array. Throws an error stating it must be an array if not. + * + * @param {*} value - The value to check. + * @returns {Array} - The original array value. + */ +export function ensureArray(value) { + if (Array.isArray(value)) { + return value; + } + + throw new TypeError('The provided value must be Array'); +} + +/** + * Ensures that the given value is array-like (an Array, Uint8Array, Uint16Array, Uint32Array, or Buffer). + * If not, throws an error indicating it must be array-like. + * + * @param {*} value - The value to check. + * @throws If value is not an instance of `Array|Uint8Array|Uint16Array|Uint32Array|Buffer` + * @returns {Array|Uint8Array|Uint16Array|Uint32Array|Buffer} - The original value if valid. + */ +export function ensureArrayLike(value) { + if ( + Array.isArray(value) + || value instanceof Uint8Array + || value instanceof Uint16Array + || value instanceof Uint32Array + || value instanceof Buffer + ) { + return value; + } + + throw new TypeError('Value needs to be an Array-like'); +} + +/** + * Get unique elements from an array based on the mapItem function. + * @param {Array} array - Array to check for duplicates. + * @param {Function} mapItem - Function to map elements before comparison. + * @returns {Array} - Array with unique elements. + */ +export function getUniqueElementsFromArray(array, mapItem) { + const seen = new Set(); + + return array.filter((item) => { + const key = mapItem(item); + + return seen.has(key) ? false : seen.add(key); + }); +} + +/** + * Splits array into chunks of the given size. + * + * @template T + * @param {Array} arr + * @param {number} chunkSize + * @returns {Array>} + */ +export const chunks = (arr, chunkSize) => arr.reduce((acc, item, idx) => { + if (idx % chunkSize === 0) acc.push([item]); + else acc[acc.length - 1].push(item); + + return acc; +}, []); + +/** + * Class extending `Array` with all prototype methods set to `undefined`. + */ +export class ArrayWithoutPrototypeMethods extends withoutPrototypeFunctions( + Array, + new Set([ + 'forEach', + 'filter', + 'every', + 'find', + 'findIndex', + 'splice', + 'some', + 'find', + ]), +) {} diff --git a/packages/crypto-utils/src/utils/types/bytes.js b/packages/crypto-utils/src/utils/types/bytes.js new file mode 100644 index 000000000..50b1c25dc --- /dev/null +++ b/packages/crypto-utils/src/utils/types/bytes.js @@ -0,0 +1,268 @@ +import crypto from 'crypto'; +import { applyToValue } from '../interfaces'; +import { ensureString } from './string'; +import { ensureIterable, isIterable } from './iterable'; +import { ensureInstanceOf } from '../inheritance'; + +/** + * Checks that the given value is a byte (an integer between 0 and 255). + * + * @param {*} byte - The value to check. + * @returns {boolean} - The validated byte value. + */ +export const isByte = (num) => Number.isInteger(num) && num >= 0 && num <= 255; + +/** + * Ensures that the given value is a byte (an integer between 0 and 255). + * + * @param {*} byte - The value to check. + * @throws If value is not a byte. + * @returns {number} - The validated byte value. + */ +export function ensureByte(num) { + if (isByte(num)) { + return num; + } + + throw new Error(`Expected \`${num}\` to be an integer in range 0-255`); +} + +/** + * Ensures that the given value is a list of bytes. If it's not a Uint8Array, converts it to one by mapping each element through `ensureByte`. + * + * @param {Uint8Array|Iterable} bytes - The bytes to validate. + * @throws If value is not a bytes. + * @returns {Uint8Array} - The validated Uint8Array containing the bytes. + */ +export const ensureBytes = (bytes) => { + if (bytes instanceof Uint8Array) { + return bytes; + } + + return Uint8Array.from([...ensureIterable(bytes)].map(ensureByte)); +}; + +/** + * Ensures that the given value is an instance of Uint8Array. Throws an error if not. + * + * @param {*} bytes - The value to check. + * @throws If value is not a Uint8Array. + * @returns {Uint8Array} - The original Uint8Array value. + */ +export const ensureUint8Array = (bytes) => { + if (bytes instanceof Uint8Array) { + return bytes; + } + + throw new TypeError('Expected instance of `Uint8Array`'); +}; + +/** + * Checks that the given value is a list of bytes. If it's not a Uint8Array, validates each item using `ensureByte`. + * + * @param {Uint8Array|Iterable} bytes - The bytes to validate. + * @throws If value is not a bytes. + * @returns {boolean} - The validated Uint8Array containing the bytes. + */ +export const isBytes = (bytes) => { + if (bytes instanceof Uint8Array) { + return true; + } else if (isIterable(bytes) && typeof bytes !== 'string') { + for (const byte of bytes) { + if (!isByte(byte)) { + return false; + } + } + + return true; + } else { + return false; + } +}; + +/** + * Check if the given input is hexadecimal or not. Optionally checks for the byte size of the hex. Case-insensitive on hex chars + * @param {string} value - Hexadecimal value + * @param {number} [byteSize] - Expected byte size of the input. + * @return {boolean} True if hex (with given size) else false + */ +export const isHexWithGivenByteSize = (value, byteSize) => { + if (typeof value !== 'string') { + return false; + } + const match = value.match(/^0x([0-9a-f]+$)/i); + + if (match && match.length > 1) { + if (byteSize !== undefined) { + // If `byteSize` is not a positive integer type, then check will fail + // 2 hex digits make a byte + return match[1].length === 2 * byteSize; + } + // Don't care about byte size of the match but it must be full byte + return match[1].length % 2 === 0; + } + + return false; +}; + +/** + * Returns `true` if provided value is a valid hex string. + * @param value + * @returns {boolean} + */ +export const isHex = (value) => isHexWithGivenByteSize(value); + +/** + * Converts supplied bytes to its hex representation. + * @param {Iterable} bytes + * @returns {string} + */ +export const u8aToHex = (bytes) => `0x${Buffer.from(ensureBytes(bytes)).toString('hex')}`; + +/** + * Creates `Uint8Array` from the supplied hex string. + * @param {string} str + * @returns {Uint8Array} + */ +export const hexToU8a = (str) => { + if (!isHex(str)) { + throw new Error( + `Expected valid hex string, received: \`${str}\` with type \`${typeof str}\``, + ); + } + + return Uint8Array.from(Buffer.from(str.slice(2), 'hex')); +}; + +/** + * Creates `Buffer` from the supplied string. + * @param {string} str + * @returns {Buffer} + */ +export const stringToBuffer = (str) => Buffer.from(ensureString(str)); + +/** + * Creates `Uint8Array` from the supplied string. + * @param {string} str + * @returns {Uint8Array} + */ +export const stringToU8a = (str) => Uint8Array.from(stringToBuffer(str)); + +/** + * Creates `Uint8Array` from the supplied buffer. + * @param {Buffer} buffer + * @returns {Uint8Array} + */ +export const bufferToU8a = (buffer) => Uint8Array.from(ensureInstanceOf(buffer, Buffer)); + +/** + * Converts supplied bytes to `Uint8Array` representation. + * @param {Iterable} bytes + * @returns {Uint8Array} + */ +export const u8aToU8a = (bytes) => bufferToU8a(Buffer.from(ensureBytes(bytes))); + +/** + * Converts supplied bytes buffer to its hex string representation. + * @param {Iterable} bytes + * @returns {string} + */ +export const u8aToString = (bytes) => Buffer.from(ensureBytes(bytes)).toString(); + +/** + * Converts supplied string containing any characters to its hex string representation. + * @param {string} str + * @returns {string} + */ +export const stringToHex = (str) => u8aToHex(stringToBuffer(str)); + +/** + * Attempts to convert supplied bytes to `Uint8Array`. + * @param {string | Uint8Array | Array} bytes + * @returns {Uint8Array} + */ +export const normalizeToU8a = (bytes) => { + if (bytes instanceof Uint8Array) { + return bytes; + } else if (isBytes(bytes)) { + return u8aToU8a(bytes); + } else if (isHex(bytes)) { + return hexToU8a(bytes); + } + + throw new Error("Can't convert supplied value to `Uint8Array`"); +}; + +/** + * Attempts to convert supplied bytes or a raw string to `Uint8Array`. + * @param {Uint8Array | string} bytesOrString + * @returns {Uint8Array} + */ +export const normalizeOrConvertStringToU8a = (bytesOrString) => (typeof bytesOrString === 'string' && !isHex(bytesOrString) + ? stringToU8a(bytesOrString) + : normalizeToU8a(bytesOrString)); + +/** + * Creates random `Uint8Array` array of supplied byte length. + * @param {number} length + * @returns {Uint8Array} + */ +export const randomAsU8a = (length) => u8aToU8a(crypto.randomBytes(length)); + +/** + * Creates random hex string of supplied byte length. + * @param {number} length + * @returns {string} + */ +export const randomAsHex = (length) => u8aToHex(randomAsU8a(length)); + +/** + * Attempts to get byte representation of the supplied object. + * Throws an error in case if it's not possible. + * @param {*} obj + * @returns {Uint8Array} + */ +export const valueBytes = (value) => applyToValue( + (inner) => Array.isArray(inner) + || inner instanceof Uint8Array + || (inner && typeof inner === 'object' && 'bytes' in inner), + (inner) => normalizeToU8a(inner.bytes ?? inner), + value, +); + +/** + * Attempts to get byte representation of the supplied object. + * In case if the number is found, it will be converted to bytes using `String(number)`. + * Throws an error in case if it's not possible. + * @param {*} value + * @returns {Uint8Array} + */ +export const valueNumberOrBytes = (value) => applyToValue( + (inner) => Array.isArray(inner) + || inner instanceof Uint8Array + || (inner && typeof inner === 'object' && 'bytes' in inner) + || typeof inner === 'number', + (inner) => (typeof inner === 'number' + ? stringToU8a(String(inner)) + : normalizeToU8a(inner.bytes ?? inner)), + value, +); + +/** + * Normalizes the given input to hex. Expects a Uint8Array or a hex string + * @param {Uint8Array|string} data + * @returns {string} + */ +export function normalizeToHex(data) { + if ( + data instanceof Uint8Array + || data instanceof Buffer + || Array.isArray(data) + ) { + return u8aToHex(data); + } else if (isHex(data)) { + return data; + } + + throw new Error('Expected a hex string or a byte array'); +} diff --git a/packages/crypto-utils/src/utils/types/index.js b/packages/crypto-utils/src/utils/types/index.js new file mode 100644 index 000000000..b62548adc --- /dev/null +++ b/packages/crypto-utils/src/utils/types/index.js @@ -0,0 +1,7 @@ +export * from './array'; +export * from './number'; +export * from './iterable'; +export * from './object'; +export * from './string'; +export * from './set'; +export * from './bytes'; diff --git a/packages/crypto-utils/src/utils/types/iterable.js b/packages/crypto-utils/src/utils/types/iterable.js new file mode 100644 index 000000000..828d08c64 --- /dev/null +++ b/packages/crypto-utils/src/utils/types/iterable.js @@ -0,0 +1,71 @@ +import { isObject } from './object'; + +/** + * Return true if a value is iterable. + * @param value + * @returns {boolean} + */ +export function isIterable(value) { + // Check if it's an object and has Symbol.iterator property + return ( + value != null + && (typeof value === 'object' || typeof value === 'function') + && Symbol.iterator in Object(value) + ); +} + +/** + * Ensures that the given value is iterable. Throws an error with an optional context message if not. + * + * @param {*} value - The value to check for iterability. + * @throws If value is not an iterable. + * @returns {Iterable} - The original iterable value. + */ +export function ensureIterable(value) { + if (isIterable(value)) { + return value; + } + + throw new TypeError(`${value} needs to be an iterable.`); +} + +/** + * Returns string containing comma-separated items of the provided iterable. + * + * @template V + * @param {Iterable} iter + * @returns {string} + */ +export const fmtIterable = (iter) => `[${[...ensureIterable(iter)].map(String).join(', ')}]`; + +/** + * Converts the provided value into an iterable. If the value is already iterable, + * it's returned as-is. If it's an object (not null), it returns an array of its + * key-value pairs. Otherwise, it throws an error. + * + * @template T - The type of the input value + * @param {T} value - The value to convert into an iterable + * @returns {Iterable} - An iterable version of the input value + * @throws {Error} If the value is null, undefined, or neither an iterable nor an object + */ +export const toIterable = (value) => { + if (isIterable(value)) { + return value; + } else if (isObject(value) && value != null) { + return Object.entries(value); + } + + throw new Error(`\`${value}\` is not an iterable or object`); +}; + +/** + * Converts the provided value into an iterator. If the value is already iterable, + * it's returned as-is. If it's an object (not null), it returns an array of its + * key-value pairs. Otherwise, it throws an error. + * + * @template T - The type of the input value + * @param {T} value - The value to convert into an iterator + * @returns {IterableIterator} - An iterator version of the input value + * @throws {Error} If the value is null, undefined, or neither an iterable nor an object + */ +export const toIterator = (value) => toIterable(value)[Symbol.iterator](); diff --git a/packages/crypto-utils/src/utils/types/number.js b/packages/crypto-utils/src/utils/types/number.js new file mode 100644 index 000000000..3fc689d58 --- /dev/null +++ b/packages/crypto-utils/src/utils/types/number.js @@ -0,0 +1,25 @@ +/** + * Return true if the given value is a number. + * @param value + * @returns {boolean} + */ +export function isNumber(value) { + return ( + (typeof value === 'number' && !Number.isNaN(value)) + || value instanceof Number + ); +} + +/** + * Ensures that the given value is a number. Throws an error with an optional context message if not. + * @param {any} value - The value to check. + * @param {string} [message] - Optional message for the error. + * @returns {number} - The value converted to a number. + */ +export function ensureNumber(value) { + if (isNumber(value)) { + return Number(value); + } + + throw new TypeError(`\`${value}\` needs to be a number.`); +} diff --git a/packages/crypto-utils/src/utils/types/object.js b/packages/crypto-utils/src/utils/types/object.js new file mode 100644 index 000000000..46f74db2c --- /dev/null +++ b/packages/crypto-utils/src/utils/types/object.js @@ -0,0 +1,133 @@ +/** + * Return true if a value is an object with Object constructor. + * @param value + * @returns {boolean} + */ +export function isObject(value) { + return isObjectWithAnyConstructor(value) && value.constructor === Object; +} + +/** + * Return true if a value is an object. + * @param value + * @returns {boolean} + */ +export function isObjectWithAnyConstructor(value) { + return value && typeof value === 'object'; +} + +/** + * Ensures that the given value is an object. If not, throws an error explaining why. + * + * @template T + * @param {T} value - The value to check. + * @throws If value is not an object. + * @returns {T} + */ +export function ensureObject(value) { + if (isObject(value)) { + return value; + } + + throw new TypeError( + `${value} needs to be an object with Object constructor.`, + ); +} + +/** + * Ensures that the given value is an object. If not, throws an error explaining why. + * + * @template T + * @param {T} value - The value to check. + * @throws If value is not an object. + * @returns {T} + */ +export function ensureObjectWithAnyConstructor(value) { + if (isObjectWithAnyConstructor(value)) { + return value; + } + + throw new TypeError(`${value} needs to be an object.`); +} + +/** + * Ensures that the given object has a specific key as a property. Throws an error with a message if not. + * + * @param {object} value - The object to check. + * @param {string} key - The required property to look for. + * @param {string} name - Name of the object used in constructing the error message. + * @returns {object} - The original object if it contains the specified key. + */ +export function ensureObjectWithKey(value, key, name) { + if (key in ensureObject(value)) { + return value; + } + + throw new Error(`"${name}" must include the '${key}' property.`); +} + +/** + * Ensures that the given object has an 'id' property. Throws an error with a message if not. + * + * @param {object} value - The object to check. + * @param {string} name - Name of the object used in constructing the error message. + * @returns {object} - The original object if it contains the 'id' property. + */ +export function ensureObjectWithId(value, name) { + return ensureObjectWithKey(value, 'id', name); +} + +/** + * Clones supplied object using `JSON.parse(JSON.stringify(...))`. + * + * @param {Object} obj + * @returns {Object} + */ +export const deepClone = (obj) => JSON.parse(JSON.stringify(ensureObject(obj))); + +/** + * Filters properties of the object according to the supplied `filter`. + * + * @template K + * @template V + * @param {Object} obj + * @param {function(K, V): boolean} filter + * @returns {Object} + */ +export const filterObj = (obj, filter) => { + const res = Object.create(Object.getPrototypeOf(obj)); + for (const [key, value] of Object.entries(obj)) { + if (filter(key, value)) { + res[key] = value; + } + } + + return res; +}; + +/** + * Maps properties of the object according to the supplied `map`. + * + * @template K + * @template VI + * @template VO + * @param {Object} obj + * @param {function(VI): VO} map + * @returns {Object} + */ +export const mapObj = (obj, map) => { + const res = Object.create(Object.getPrototypeOf(obj)); + for (const [key, value] of Object.entries(obj)) { + res[key] = map(value); + } + + return res; +}; + +/** + * Sets prototype of the supplied object to `null`, returns the object. + * @template T + * @param {T} + * @returns {T} + */ +export const extendNull = (obj) => Object.setPrototypeOf(obj, null); diff --git a/packages/crypto-utils/src/utils/types/set.js b/packages/crypto-utils/src/utils/types/set.js new file mode 100644 index 000000000..99f31f036 --- /dev/null +++ b/packages/crypto-utils/src/utils/types/set.js @@ -0,0 +1,40 @@ +/** + * Return true if the given value is a Set. + * @param value + * @returns {boolean} + */ +export function isSet(value) { + return value instanceof Set; +} + +/** + * Ensures that the given value is a Set. Throws an error with an optional context message if not. + * @param {any} value - The value to check. + * @param {string} [message] - Optional message for the error. + * @returns {Set} - The value if it's a Set. + */ +export function ensureSet(value) { + if (isSet(value)) { + return value; + } + + throw new TypeError(`\`${value}\` needs to be a Set.`); +} + +/** + * Ensures that there is no intersection between two sets + * @param {Set} set1 - The first set to compare + * @param {Set} set2 - The second set to compare + * @throws {Error} - If there is an intersection between set1 and set2 + */ +export const ensureNoIntersection = (set1, set2) => { + const [min, max] = [set1, set2] + .map(ensureSet) + .sort((a, b) => a.size - b.size); + + for (const item of min) { + if (max.has(item)) { + throw new Error(`Item \`${item}\` exists in both sets`); + } + } +}; diff --git a/packages/crypto-utils/src/utils/types/string.js b/packages/crypto-utils/src/utils/types/string.js new file mode 100644 index 000000000..e30b1074e --- /dev/null +++ b/packages/crypto-utils/src/utils/types/string.js @@ -0,0 +1,22 @@ +/** + * Return true if the given value is a string. + * @param value + * @returns {boolean} + */ +export function isString(value) { + return typeof value === 'string' || value instanceof String; +} + +/** + * Ensures that the given value is a string. If not, throws an error with an optional context message. + * + * @param {*} value - The value to check. + * @returns {string} - The value converted to a string. + */ +export function ensureString(value) { + if (isString(value)) { + return String(value); + } + + throw new TypeError(`${value} needs to be a string`); +} diff --git a/packages/crypto-utils/src/vc/crypto/constants.js b/packages/crypto-utils/src/vc/crypto/constants.js new file mode 100644 index 000000000..426d8d494 --- /dev/null +++ b/packages/crypto-utils/src/vc/crypto/constants.js @@ -0,0 +1,23 @@ +export const EcdsaSecp256k1VerKeyName = 'EcdsaSecp256k1VerificationKey2019'; +export const EcdsaSecp256k1SigName = 'EcdsaSecp256k1Signature2019'; +export const EcdsaSecp256r1VerKeyName = 'EcdsaSecp256r1VerificationKey2019'; +export const EcdsaSecp256r1SigName = 'EcdsaSecp256r1Signature2019'; +export const Ed25519VerKeyName = 'Ed25519VerificationKey2018'; +export const Ed255192020VerKeyName = 'Ed25519VerificationKey2020'; +export const Ed25519SigName = 'Ed25519Signature2018'; +export const Ed255192020SigName = 'Ed25519Signature2020'; +export const Sr25519VerKeyName = 'Sr25519VerificationKey2020'; +export const Sr25519SigName = 'Sr25519Signature2020'; +export const Bls12381BBSSigDockSigName = 'Bls12381BBS+SignatureDock2022'; +export const Bls12381BBSSigProofDockSigName = 'Bls12381BBS+SignatureProofDock2022'; +export const Bls12381BBSDockVerKeyName = 'Bls12381G2VerificationKeyDock2022'; +export const Bls12381BBS23SigDockSigName = 'Bls12381BBSSignatureDock2023'; +export const Bls12381BBDT16MacDockName = 'Bls12381BBDT16MACDock2024'; +export const Bls12381BBDT16MacProofDockName = 'Bls12381BBDT16MACProofDock2024'; +export const Bls12381BBS23SigProofDockSigName = 'Bls12381BBSSignatureProofDock2023'; +export const Bls12381BBS23DockVerKeyName = 'Bls12381BBSVerificationKeyDock2023'; +export const Bls12381BBDT16DockVerKeyName = 'Bls12381BBDT16VerificationKeyDock2024'; +export const Bls12381PSSigDockSigName = 'Bls12381PSSignatureDock2023'; +export const Bls12381PSSigProofDockSigName = 'Bls12381PSSignatureProofDock2023'; +export const Bls12381PSDockVerKeyName = 'Bls12381PSVerificationKeyDock2023'; +export const JsonWebKey2020VerKeyName = 'JsonWebKey2020'; diff --git a/packages/crypto-utils/src/vc/crypto/index.js b/packages/crypto-utils/src/vc/crypto/index.js new file mode 100644 index 000000000..c94f80f84 --- /dev/null +++ b/packages/crypto-utils/src/vc/crypto/index.js @@ -0,0 +1 @@ +export * from './constants'; diff --git a/packages/crypto-utils/src/vc/index.js b/packages/crypto-utils/src/vc/index.js new file mode 100644 index 000000000..0f4c53341 --- /dev/null +++ b/packages/crypto-utils/src/vc/index.js @@ -0,0 +1,3 @@ +export * from './crypto'; +export * from './jws'; +export * from './jws-signer'; diff --git a/packages/crypto-utils/src/vc/jws-signer.js b/packages/crypto-utils/src/vc/jws-signer.js new file mode 100644 index 000000000..7c6550100 --- /dev/null +++ b/packages/crypto-utils/src/vc/jws-signer.js @@ -0,0 +1,63 @@ +import { valueBytes } from '../utils'; + +/** + * Adapts a Dock keypair to the raw-byte signer contract used by JWS helpers. + * ECDSA output remains Dock's 65-byte r || s || recovery representation. + * + * @param {import('../keypairs/dock-keypair').default} keypair + * @returns {{sign: function({data: Uint8Array}): Uint8Array}} + */ +export function createRawSigner(keypair) { + return { + sign({ data }) { + return valueBytes(keypair.sign(data)); + }, + }; +} + +/** + * Adapts a Dock or DID keypair to the signer contract used by JWS helpers. + * Dock ECDSA signatures include a recovery byte; JOSE uses only r || s. + * + * @param {{sign: function(Uint8Array): *}} keypair + * @returns {{sign: function({data: Uint8Array}): Uint8Array}} + */ +export function createJwsSigner(keypair) { + return { + sign({ data }) { + const signature = valueBytes(keypair.sign(data)); + return signature.length === 65 ? signature.slice(0, 64) : signature; + }, + }; +} + +function encodeDERInteger(bytes) { + let offset = 0; + while (offset < bytes.length - 1 && bytes[offset] === 0) { + offset += 1; + } + + const value = bytes.slice(offset); + // eslint-disable-next-line no-bitwise + const encoded = value[0] & 0x80 ? Uint8Array.of(0, ...value) : value; + return Uint8Array.of(0x02, encoded.length, ...encoded); +} + +/** + * Converts a JOSE ECDSA signature from 64-byte r || s form to ASN.1 DER. + * + * @param {*} signature + * @returns {Uint8Array} + */ +export function joseSignatureToDER(signature) { + const bytes = valueBytes(signature); + if (bytes.length !== 64) { + throw new Error( + `Invalid JOSE signature length. Expected 64 bytes, received ${bytes.length}`, + ); + } + + const r = encodeDERInteger(bytes.slice(0, 32)); + const s = encodeDERInteger(bytes.slice(32)); + return Uint8Array.of(0x30, r.length + s.length, ...r, ...s); +} diff --git a/packages/crypto-utils/src/vc/jws.js b/packages/crypto-utils/src/vc/jws.js new file mode 100644 index 000000000..db59c59fb --- /dev/null +++ b/packages/crypto-utils/src/vc/jws.js @@ -0,0 +1,52 @@ +import base64url from 'base64url'; + +const detachedHeaderParams = { + b64: false, + crit: ['b64'], +}; + +// Taken from https://github.com/transmute-industries/verifiable-data/blob/main/packages/jose-ld/src/JWS/createSigner.ts +export async function signJWS(signer, type, options, data) { + if (!type) { + return signer.sign({ data }); + } + + const header = { + alg: type, + ...options.header, + ...(options.detached ? detachedHeaderParams : undefined), + }; + const encodedHeader = base64url.encode(JSON.stringify(header)); + const encodedPayload = base64url.encode( + data instanceof Uint8Array + ? Buffer.from(data).toString('utf-8') + : JSON.stringify(data), + ); + + const toBeSigned = options.detached + ? new Uint8Array( + Buffer.concat([ + Buffer.from(encodedHeader, 'utf8'), + Buffer.from('.', 'utf-8'), + data, + ]), + ) + : new Uint8Array(Buffer.from(`${encodedHeader}.${encodedPayload}`)); + + const signature = await signer.sign({ data: toBeSigned }); + + // If not, encode it ourselves + return options.detached + ? `${encodedHeader}..${base64url.encode(Buffer.from(signature))}` + : `${encodedHeader}.${encodedPayload}.${base64url.encode( + Buffer.from(signature), + )}`; +} + +export function createJws({ encodedHeader, verifyData }) { + const buffer = Buffer.concat([ + Buffer.from(`${encodedHeader}.`, 'utf8'), + Buffer.from(verifyData.buffer, verifyData.byteOffset, verifyData.length), + ]); + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.length); +} diff --git a/packages/crypto-utils/tests/crypto-utils.test.js b/packages/crypto-utils/tests/crypto-utils.test.js new file mode 100644 index 000000000..fc5eea010 --- /dev/null +++ b/packages/crypto-utils/tests/crypto-utils.test.js @@ -0,0 +1,244 @@ +import base64url from 'base64url'; + +import { + Ed25519Keypair, + Signature, + SignatureEd25519, + SignatureEd25519Value, + Secp256k1Keypair, + Secp256r1Keypair, + createJws, + createJwsSigner, + createRawSigner, + joseSignatureToDER, + signJWS, +} from '../src'; +import CredentialEd25519Keypair from '../../credential-sdk/src/keypairs/keypair-ed25519'; +import CredentialSecp256k1Keypair from '../../credential-sdk/src/keypairs/keypair-secp256k1'; +import CredentialSecp256r1Keypair from '../../credential-sdk/src/keypairs/keypair-secp256r1'; +import { + Signature as CredentialSignature, + SignatureEd25519 as CredentialSignatureEd25519, +} from '../../credential-sdk/src/types/signatures/signature'; +import CredentialSignatureEd25519Value from '../../credential-sdk/src/types/signatures/signature-ed25519-value'; +import * as cryptoBytes from '../src/utils/types/bytes'; +import * as credentialBytes from '../../credential-sdk/src/utils/types/bytes'; +import DeepSignatureEd25519Value from '../src/types/signatures/signature-ed25519-value'; + +const message = Uint8Array.from([0, 1, 2, 3, 127, 128, 254, 255]); + +describe.each([ + { + name: 'Ed25519Keypair', + Keypair: Ed25519Keypair, + sourceType: 'seed', + signatureLength: 64, + }, + { + name: 'Secp256k1Keypair', + Keypair: Secp256k1Keypair, + sourceType: 'entropy', + signatureLength: 65, + }, + { + name: 'Secp256r1Keypair', + Keypair: Secp256r1Keypair, + sourceType: 'entropy', + signatureLength: 65, + }, +])('$name', ({ Keypair, sourceType, signatureLength }) => { + const source = Uint8Array.from( + { length: Keypair.SeedSize }, + (_, index) => index + 1, + ); + + const createKeypair = () => ( + sourceType === 'seed' + ? Keypair.fromSeed(source) + : Keypair.fromEntropy(source) + ); + + test('signs and verifies arbitrary bytes', () => { + const keypair = createKeypair(); + const signature = keypair.sign(message); + + expect(signature.bytes).toHaveLength(signatureLength); + expect(Keypair.verify(message, signature, keypair.publicKey())).toBe(true); + expect(Keypair.verify(Uint8Array.of(9), signature, keypair.publicKey())).toBe(false); + if (signatureLength === 65) { + expect(Keypair.verify( + message, + joseSignatureToDER(signature.bytes.slice(0, 64)), + keypair.publicKey(), + )).toBe(true); + } + }); + + test('round-trips private keys', () => { + const keypair = createKeypair(); + const restored = Keypair.fromPrivateKey(keypair.privateKey()); + expect(restored.publicKey().bytes).toEqual(keypair.publicKey().bytes); + }); + + test('adapts signatures to raw Dock bytes', async () => { + const signature = await createRawSigner(createKeypair()).sign({ data: message }); + expect(signature).toBeInstanceOf(Uint8Array); + expect(signature).toHaveLength(signatureLength); + }); +}); + +describe('JWS helpers', () => { + const keypair = Ed25519Keypair.fromSeed(Uint8Array.from( + { length: Ed25519Keypair.SeedSize }, + (_, index) => index, + )); + const signer = createRawSigner(keypair); + + test('creates non-detached compact JWS', async () => { + const jws = await signJWS(signer, 'EdDSA', { detached: false, header: {} }, message); + const [header, payload, signature] = jws.split('.'); + + expect(JSON.parse(base64url.decode(header))).toEqual({ alg: 'EdDSA' }); + expect(payload).not.toBe(''); + expect(base64url.toBuffer(signature)).toHaveLength(64); + }); + + test('creates detached compact JWS', async () => { + const jws = await signJWS(signer, 'EdDSA', { detached: true, header: {} }, message); + const [header, payload, signature] = jws.split('.'); + + expect(JSON.parse(base64url.decode(header))).toEqual({ + alg: 'EdDSA', + b64: false, + crit: ['b64'], + }); + expect(payload).toBe(''); + expect(base64url.toBuffer(signature)).toHaveLength(64); + }); + + test('creates and verifies an ES256 JWS with a DID keypair adapter', async () => { + const ecdsaKeypair = Secp256r1Keypair.fromEntropy(Uint8Array.from( + { length: Secp256r1Keypair.SeedSize }, + (_, index) => index + 1, + )); + const didKeypair = { + sign(data) { + return ecdsaKeypair.sign(data); + }, + }; + const jws = await signJWS( + createJwsSigner(didKeypair), + 'ES256', + { detached: true, header: {} }, + message, + ); + const [header, payload, encodedSignature] = jws.split('.'); + const signature = new Uint8Array(base64url.toBuffer(encodedSignature)); + const signedData = createJws({ encodedHeader: header, verifyData: message }); + + expect(payload).toBe(''); + expect(signature).toHaveLength(64); + expect(Secp256r1Keypair.verify( + signedData, + joseSignatureToDER(signature), + ecdsaKeypair.publicKey(), + )).toBe(true); + }); + + test('creates JWS verification bytes without copying unrelated buffer data', () => { + const backing = Uint8Array.of(9, 1, 2, 3, 9); + const verifyData = backing.subarray(1, 4); + const result = createJws({ encodedHeader: 'header', verifyData }); + + expect(Buffer.from(result)).toEqual(Buffer.from('header.\u0001\u0002\u0003')); + }); +}); + +describe('credential-sdk behavior parity', () => { + test.each([ + { + name: 'Ed25519Keypair', + CryptoKeypair: Ed25519Keypair, + CredentialKeypair: CredentialEd25519Keypair, + sourceType: 'seed', + }, + { + name: 'Secp256k1Keypair', + CryptoKeypair: Secp256k1Keypair, + CredentialKeypair: CredentialSecp256k1Keypair, + sourceType: 'entropy', + }, + { + name: 'Secp256r1Keypair', + CryptoKeypair: Secp256r1Keypair, + CredentialKeypair: CredentialSecp256r1Keypair, + sourceType: 'entropy', + }, + ])('matches deterministic $name output', ({ + CryptoKeypair, + CredentialKeypair, + sourceType, + }) => { + const source = Uint8Array.from( + { length: CryptoKeypair.SeedSize }, + (_, index) => index + 1, + ); + const create = (Keypair) => ( + sourceType === 'seed' + ? Keypair.fromSeed(source) + : Keypair.fromEntropy(source) + ); + const cryptoKeypair = create(CryptoKeypair); + const credentialKeypair = create(CredentialKeypair); + + expect(cryptoKeypair.privateKey()).toEqual(credentialKeypair.privateKey()); + expect(cryptoKeypair.publicKey().toJSON()).toEqual( + credentialKeypair.publicKey().toJSON(), + ); + expect(cryptoKeypair.sign(message).toJSON()).toEqual( + credentialKeypair.sign(message).toJSON(), + ); + }); + + test('preserves TypedEnum conversion and variant APIs', () => { + const bytes = new Uint8Array(64); + const localValue = new SignatureEd25519Value(bytes); + const credentialValue = new CredentialSignatureEd25519Value(bytes); + const localSignature = new SignatureEd25519(localValue); + const credentialSignature = new CredentialSignatureEd25519(credentialValue); + const json = { ed25519: localValue.toHex() }; + + expect(Signature.variant(localValue).Type).toBe( + CredentialSignature.variant(credentialValue).Type, + ); + expect(Signature.directVariant(localSignature).Type).toBe( + CredentialSignature.directVariant(credentialSignature).Type, + ); + expect(Signature.isNullish).toBe(CredentialSignature.isNullish); + expect(Signature.from(localValue).toJSON()).toEqual( + CredentialSignature.from(credentialValue).toJSON(), + ); + expect(Signature.fromJSON(json).toJSON()).toEqual( + CredentialSignature.fromJSON(json).toJSON(), + ); + expect(Signature.fromApi(localSignature).toJSON()).toEqual( + CredentialSignature.fromApi(credentialSignature).toJSON(), + ); + }); + + test('preserves byte helper exports and representative conversions', () => { + expect(Object.keys(cryptoBytes).sort()).toEqual( + Object.keys(credentialBytes).sort(), + ); + expect(cryptoBytes.normalizeToHex([0, 127, 255])).toBe( + credentialBytes.normalizeToHex([0, 127, 255]), + ); + expect(cryptoBytes.valueNumberOrBytes(42)).toEqual( + credentialBytes.valueNumberOrBytes(42), + ); + }); + + test('provides individual-module default exports for deep shims', () => { + expect(DeepSignatureEd25519Value).toBe(SignatureEd25519Value); + }); +}); diff --git a/scripts/bench/CHANGELOG.md b/scripts/bench/CHANGELOG.md index 5262237c7..4aab95d4b 100644 --- a/scripts/bench/CHANGELOG.md +++ b/scripts/bench/CHANGELOG.md @@ -1,5 +1,14 @@ # @docknetwork/benchmarks +## 0.4.26 + +### Patch Changes + +- Updated dependencies + - @docknetwork/credential-sdk@0.56.0 + - @docknetwork/cheqd-blockchain-api@6.0.0 + - @docknetwork/cheqd-blockchain-modules@6.0.0 + ## 0.4.25 ### Patch Changes diff --git a/scripts/bench/package.json b/scripts/bench/package.json index f05ec9cd8..62c7df340 100644 --- a/scripts/bench/package.json +++ b/scripts/bench/package.json @@ -2,14 +2,14 @@ "name": "@docknetwork/benchmarks", "private": true, "type": "module", - "version": "0.4.25", + "version": "0.4.26", "scripts": { "bench": "babel-node src/main.js" }, "dependencies": { - "@docknetwork/cheqd-blockchain-api": "5.1.0", - "@docknetwork/cheqd-blockchain-modules": "5.0.0", - "@docknetwork/credential-sdk": "0.55.0", + "@docknetwork/cheqd-blockchain-api": "6.0.0", + "@docknetwork/cheqd-blockchain-modules": "6.0.0", + "@docknetwork/credential-sdk": "0.56.0", "@docknetwork/crypto-wasm-ts": "^0.63.0" }, "devDependencies": {