diff --git a/package.json b/package.json index 43f2e139..cb9e7415 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,9 @@ ], "scripts": { "lint": "aegir lint", - "build": "npm run build:proto && aegir build --no-types", + "build": "npm run build:proto && aegir build", "build:proto": "pbjs -t static-module -w commonjs -r libp2p-crypto-keys --force-number --no-verify --no-delimited --no-create --no-beautify --no-defaults --lint eslint-disable -o src/keys/keys.js ./src/keys/keys.proto", + "build:proto-types": "pbts -o src/keys/keys.d.ts src/keys/keys.js", "test": "aegir test", "test:node": "aegir test -t node", "test:browser": "aegir test -t browser -t webworker", @@ -30,6 +31,9 @@ "size": "aegir build --bundlesize", "test:types": "npx tsc" }, + "eslintConfig": { + "extends": "ipfs" + }, "keywords": [ "IPFS", "libp2p", @@ -52,14 +56,18 @@ "protobufjs": "^6.10.2", "secp256k1": "^4.0.0", "uint8arrays": "^2.1.4", - "ursa-optional": "^0.10.1" + "ursa-optional": "^0.10.1", + "libp2p-interfaces": "git://github.com/libp2p/js-libp2p-interfaces#feat/interfaces" }, "devDependencies": { "@types/chai": "^4.2.12", "@types/chai-string": "^1.4.2", "@types/dirty-chai": "^2.0.2", "@types/mocha": "^8.0.1", - "aegir": "^33.0.0", + "@types/pem-jwk": "^1.5.0", + "@types/node-forge": "^0.10.0", + "@types/secp256k1": "^4.0.2", + "aegir": "^33.2.2", "benchmark": "^2.1.4", "chai": "^4.2.0", "chai-string": "^1.5.0", diff --git a/src/aes/cipher-mode.js b/src/aes/cipher-mode.js index e475d193..e0f6be2c 100644 --- a/src/aes/cipher-mode.js +++ b/src/aes/cipher-mode.js @@ -7,7 +7,14 @@ const CIPHER_MODES = { 32: 'aes-256-ctr' } +/** + * @typedef {'aes-128-ctr'|'aes-256-ctr'} Mode + * + * @param {Uint8Array} key + * @returns {Mode} + */ module.exports = function (key) { + // @ts-expect-error const mode = CIPHER_MODES[key.length] if (!mode) { const modes = Object.entries(CIPHER_MODES).map(([k, v]) => `${k} (${v})`).join(' / ') diff --git a/src/aes/ciphers-browser.js b/src/aes/ciphers-browser.js index 4280bee2..db59c75a 100644 --- a/src/aes/ciphers-browser.js +++ b/src/aes/ciphers-browser.js @@ -1,25 +1,48 @@ 'use strict' +// @ts-ignore require('node-forge/lib/aes') +/** @type {import('node-forge')} */ +// @ts-ignore const forge = require('node-forge/lib/forge') const uint8ArrayToString = require('uint8arrays/to-string') const uint8ArrayFromString = require('uint8arrays/from-string') module.exports = { + /** + * + * @param {any} mode + * @param {Uint8Array} key + * @param {Uint8Array} iv + * @returns + */ createCipheriv: (mode, key, iv) => { const cipher2 = forge.cipher.createCipher('AES-CTR', uint8ArrayToString(key, 'ascii')) cipher2.start({ iv: uint8ArrayToString(iv, 'ascii') }) return { + /** + * @param {Uint8Array} data + */ update: (data) => { cipher2.update(forge.util.createBuffer(uint8ArrayToString(data, 'ascii'))) return uint8ArrayFromString(cipher2.output.getBytes(), 'ascii') } } }, + /** + * + * @param {any} mode + * @param {Uint8Array} key + * @param {Uint8Array} iv + * @returns + */ createDecipheriv: (mode, key, iv) => { const cipher2 = forge.cipher.createDecipher('AES-CTR', uint8ArrayToString(key, 'ascii')) cipher2.start({ iv: uint8ArrayToString(iv, 'ascii') }) return { + /** + * @param {Uint8Array} data + */ update: (data) => { cipher2.update(forge.util.createBuffer(uint8ArrayToString(data, 'ascii'))) return uint8ArrayFromString(cipher2.output.getBytes(), 'ascii') diff --git a/src/aes/index.js b/src/aes/index.js index 655b5478..413c6503 100644 --- a/src/aes/index.js +++ b/src/aes/index.js @@ -3,16 +3,31 @@ const ciphers = require('./ciphers') const cipherMode = require('./cipher-mode') +/** + * Create a new AES Cipher. + * + * @param {Uint8Array} key - The key, if length 16 then AES 128 is used. For length 32, AES 256 is used. + * @param {Uint8Array} iv - Must have length 16. + * @returns {Promise} + */ exports.create = async function (key, iv) { // eslint-disable-line require-await const mode = cipherMode(key) const cipher = ciphers.createCipheriv(mode, key, iv) const decipher = ciphers.createDecipheriv(mode, key, iv) const res = { + /** + * @param {Uint8Array} data + * @returns {Promise} + */ async encrypt (data) { // eslint-disable-line require-await return cipher.update(data) }, + /** + * @param {Uint8Array} data + * @returns {Promise} + */ async decrypt (data) { // eslint-disable-line require-await return decipher.update(data) } diff --git a/src/ciphers/aes-gcm.js b/src/ciphers/aes-gcm.js index 3ee2caa8..1a07a5be 100644 --- a/src/ciphers/aes-gcm.js +++ b/src/ciphers/aes-gcm.js @@ -15,7 +15,6 @@ const uint8ArrayFromString = require('uint8arrays/from-string') * @param {string} [options.digest=sha256] * @param {number} [options.saltLength=16] * @param {number} [options.iterations=32767] - * @returns {*} */ function create ({ algorithmTagLength = 16, @@ -57,12 +56,8 @@ function create ({ // Generate a 128-bit salt using a CSPRNG. const salt = crypto.randomBytes(saltLength) - if (typeof password === 'string' || password instanceof String) { - password = uint8ArrayFromString(password) - } - // Derive a key using PBKDF2. - const key = crypto.pbkdf2Sync(password, salt, iterations, keyLength, digest) + const key = crypto.pbkdf2Sync(decodePassword(password), salt, iterations, keyLength, digest) // Encrypt and prepend salt. return uint8ArrayConcat([salt, await encryptWithKey(Uint8Array.from(data), key)]) @@ -108,12 +103,8 @@ function create ({ const salt = data.slice(0, saltLength) const ciphertextAndNonce = data.slice(saltLength) - if (typeof password === 'string' || password instanceof String) { - password = uint8ArrayFromString(password) - } - // Derive the key using PBKDF2. - const key = crypto.pbkdf2Sync(password, salt, iterations, keyLength, digest) + const key = crypto.pbkdf2Sync(decodePassword(password), salt, iterations, keyLength, digest) // Decrypt and return result. return decryptWithKey(ciphertextAndNonce, key) @@ -124,6 +115,21 @@ function create ({ decrypt } } +/** + * + * @param {string | InstanceType | Uint8Array} password + * @returns {Uint8Array} + */ + +const decodePassword = (password) => { + if (typeof password === 'string') { + return uint8ArrayFromString(password) + } else if (password instanceof String) { + return uint8ArrayFromString(password.toString()) + } else { + return password + } +} module.exports = { create diff --git a/src/hmac/index-browser.js b/src/hmac/index-browser.js index f29476d1..caf18dc4 100644 --- a/src/hmac/index-browser.js +++ b/src/hmac/index-browser.js @@ -9,11 +9,24 @@ const hashTypes = { SHA512: 'SHA-512' } +/** + * + * @param {CryptoKey} key + * @param {Uint8Array} data + */ const sign = async (key, data) => { const buf = await webcrypto.get().subtle.sign({ name: 'HMAC' }, key, data) - return new Uint8Array(buf, buf.byteOffset, buf.byteLength) + return new Uint8Array(buf) } +/** + * @typedef {import('./index').HashType} HashType + * + * @param {HashType} hashType + * @param {Uint8Array} secret + * @returns {Promise} + */ + exports.create = async function (hashType, secret) { const hash = hashTypes[hashType] diff --git a/src/hmac/index.js b/src/hmac/index.js index ae4efe6e..d0cb14ca 100644 --- a/src/hmac/index.js +++ b/src/hmac/index.js @@ -3,8 +3,26 @@ const crypto = require('crypto') const lengths = require('./lengths') +/** + * Maps an IPFS hash name to its node-forge equivalent. + * See https://github.com/multiformats/multihash/blob/master/hashtable.csv + * + * @typedef {"SHA1" | "SHA256" | "SHA512"} HashType + */ + +/** + * Create a new HMAC Digest. + * + * @param {HashType} hash + * @param {Uint8Array} secret + * @returns {Promise} + */ exports.create = async function (hash, secret) { // eslint-disable-line require-await const res = { + /** + * + * @param {Uint8Array} data + */ async digest (data) { // eslint-disable-line require-await const hmac = crypto.createHmac(hash.toLowerCase(), secret) hmac.update(data) diff --git a/src/index.d.ts b/src/index.d.ts deleted file mode 100644 index dbcc9925..00000000 --- a/src/index.d.ts +++ /dev/null @@ -1,338 +0,0 @@ -/** - * Supported key types. - */ -export type KeyType = "Ed25519" | "RSA" | "secp256k1"; - -/** - * Maps an IPFS hash name to its node-forge equivalent. - * See https://github.com/multiformats/multihash/blob/master/hashtable.csv - */ -export type HashType = "SHA1" | "SHA256" | "SHA512"; - -/** - * Supported curve types. - */ -export type CurveType = "P-256" | "P-384" | "P-521"; - -/** - * Supported cipher types. - */ -export type CipherType = "AES-128" | "AES-256" | "Blowfish"; - -/** - * Exposes an interface to AES encryption (formerly Rijndael), - * as defined in U.S. Federal Information Processing Standards Publication 197. - * This uses CTR mode. - */ -export namespace aes { - /** - * AES Cipher in CTR mode. - */ - interface Cipher { - encrypt(data: Uint8Array): Promise; - decrypt(data: Uint8Array): Promise; - } - /** - * Create a new AES Cipher. - * @param key The key, if length 16 then AES 128 is used. For length 32, AES 256 is used. - * @param iv Must have length 16. - */ - function create(key: Uint8Array, iv: Uint8Array): Promise; -} - -/** - * Exposes an interface to the Keyed-Hash Message Authentication Code (HMAC) - * as defined in U.S. Federal Information Processing Standards Publication 198. - * An HMAC is a cryptographic hash that uses a key to sign a message. - * The receiver verifies the hash by recomputing it using the same key. - */ -export namespace hmac { - /** - * HMAC Digest. - */ - interface Digest { - digest(data: Uint8Array): Promise; - length: 20 | 32 | 64 | number; - } - /** - * Create a new HMAC Digest. - */ - function create( - hash: "SHA1" | "SHA256" | "SHA512" | string, - secret: Uint8Array - ): Promise; -} - -/** - * Generic public key interface. - */ -export interface PublicKey { - readonly bytes: Uint8Array; - verify(data: Uint8Array, sig: Uint8Array): Promise; - marshal(): Uint8Array; - equals(key: PublicKey): boolean; - hash(): Promise; -} - -/** - * Generic private key interface. - */ -export interface PrivateKey { - readonly public: PublicKey; - readonly bytes: Uint8Array; - sign(data: Uint8Array): Promise; - marshal(): Uint8Array; - equals(key: PrivateKey): boolean; - hash(): Promise; - /** - * Gets the ID of the key. - * - * The key id is the base58 encoding of the SHA-256 multihash of its public key. - * The public key is a protobuf encoding containing a type and the DER encoding - * of the PKCS SubjectPublicKeyInfo. - */ - id(): Promise; - /** - * Exports the password protected key in the format specified. - */ - export(password: string, format?: "pkcs-8" | string): Promise; -} - -export interface Keystretcher { - (res: Uint8Array): Keystretcher; - iv: Uint8Array; - cipherKey: Uint8Array; - macKey: Uint8Array; -} - -export interface StretchPair { - k1: Keystretcher; - k2: Keystretcher; -} - -/** - * Exposes an interface to various cryptographic key generation routines. - * Currently the 'RSA' and 'ed25519' types are supported, although ed25519 keys - * support only signing and verification of messages. For encryption / decryption - * support, RSA keys should be used. - * Installing the libp2p-crypto-secp256k1 module adds support for the 'secp256k1' - * type, which supports ECDSA signatures using the secp256k1 elliptic curve - * popularized by Bitcoin. This module is not installed by default, and should be - * explicitly depended on if your project requires secp256k1 support. - */ -export namespace keys { - export {}; - export namespace supportedKeys { - namespace rsa { - class RsaPublicKey implements PublicKey { - constructor(key: Uint8Array); - readonly bytes: Uint8Array; - verify(data: Uint8Array, sig: Uint8Array): Promise; - marshal(): Uint8Array; - encrypt(bytes: Uint8Array): Uint8Array; - equals(key: PublicKey): boolean; - hash(): Promise; - } - - class RsaPrivateKey implements PrivateKey { - constructor(key: any, publicKey: Uint8Array); - readonly public: RsaPublicKey; - readonly bytes: Uint8Array; - genSecret(): Uint8Array; - sign(data: Uint8Array): Promise; - decrypt(bytes: Uint8Array): Uint8Array; - marshal(): Uint8Array; - equals(key: PrivateKey): boolean; - hash(): Promise; - id(): Promise; - export(password: string, format?: string): Promise; - } - function unmarshalRsaPublicKey(buf: Uint8Array): RsaPublicKey; - function unmarshalRsaPrivateKey(buf: Uint8Array): Promise; - function generateKeyPair(bits: number): Promise; - function fromJwk(jwk: Uint8Array): Promise; - } - - namespace ed25519 { - class Ed25519PublicKey implements PublicKey { - constructor(key: Uint8Array); - readonly bytes: Uint8Array; - verify(data: Uint8Array, sig: Uint8Array): Promise; - marshal(): Uint8Array; - encrypt(bytes: Uint8Array): Uint8Array; - equals(key: PublicKey): boolean; - hash(): Promise; - } - - class Ed25519PrivateKey implements PrivateKey { - constructor(key: Uint8Array, publicKey: Uint8Array); - readonly public: Ed25519PublicKey; - readonly bytes: Uint8Array; - sign(data: Uint8Array): Promise; - marshal(): Uint8Array; - equals(key: PrivateKey): boolean; - hash(): Promise; - id(): Promise; - export(password: string, format?: string): Promise; - } - - function unmarshalEd25519PrivateKey( - buf: Uint8Array - ): Promise; - function unmarshalEd25519PublicKey(buf: Uint8Array): Ed25519PublicKey; - function generateKeyPair(): Promise; - function generateKeyPairFromSeed( - seed: Uint8Array - ): Promise; - } - - namespace secp256k1 { - class Secp256k1PublicKey implements PublicKey { - constructor(key: Uint8Array); - readonly bytes: Uint8Array; - verify(data: Uint8Array, sig: Uint8Array): Promise; - marshal(): Uint8Array; - encrypt(bytes: Uint8Array): Uint8Array; - equals(key: PublicKey): boolean; - hash(): Promise; - } - - class Secp256k1PrivateKey implements PrivateKey { - constructor(key: Uint8Array, publicKey: Uint8Array); - readonly public: Secp256k1PublicKey; - readonly bytes: Uint8Array; - sign(data: Uint8Array): Promise; - marshal(): Uint8Array; - equals(key: PrivateKey): boolean; - hash(): Promise; - id(): Promise; - export(password: string, format?: string): Promise; - } - - function unmarshalSecp256k1PrivateKey( - bytes: Uint8Array - ): Promise; - function unmarshalSecp256k1PublicKey(bytes: Uint8Array): Secp256k1PublicKey; - function generateKeyPair(): Promise; - } - } - - export const keysPBM: any; - - /** - * Generates a keypair of the given type and bitsize. - * @param type One of the supported key types. - * @param bits Number of bits. Minimum of 1024. - */ - export function generateKeyPair( - type: KeyType | string, - bits: number - ): Promise; - export function generateKeyPair( - type: "Ed25519" - ): Promise; - export function generateKeyPair( - type: "RSA", - bits: number - ): Promise; - export function generateKeyPair( - type: "secp256k1" - ): Promise; - - /** - * Generates a keypair of the given type and bitsize. - * @param type One of the supported key types. Currently only 'Ed25519' is supported. - * @param seed A 32 byte uint8array. - * @param bits Number of bits. Minimum of 1024. - */ - export function generateKeyPairFromSeed( - type: KeyType | string, - seed: Uint8Array, - bits: number - ): Promise; - export function generateKeyPairFromSeed( - type: "Ed25519", - seed: Uint8Array, - bits: number - ): Promise; - - /** - * Generates an ephemeral public key and returns a function that will compute the shared secret key. - * Focuses only on ECDH now, but can be made more general in the future. - * @param curve The curve to use. One of 'P-256', 'P-384', 'P-521' is currently supported. - */ - export function generateEphemeralKeyPair( - curve: CurveType | string - ): Promise<{ - key: Uint8Array; - genSharedKey: (theirPub: Uint8Array, forcePrivate?: any) => Promise; - }>; - - /** - * Generates a set of keys for each party by stretching the shared key. - * @param cipherType The cipher type to use. One of 'AES-128', 'AES-256', or 'Blowfish' - * @param hashType The hash type to use. One of 'SHA1', 'SHA2256', or 'SHA2512'. - * @param secret The shared key secret. - */ - export function keyStretcher( - cipherType: CipherType | string, - hashType: HashType | string, - secret: Uint8Array | string - ): Promise; - - /** - * Converts a protobuf serialized public key into its representative object. - * @param buf The protobuf serialized public key. - */ - export function unmarshalPublicKey(buf: Uint8Array): PublicKey; - - /** - * Converts a public key object into a protobuf serialized public key. - * @param key An RSA, Ed25519, or Secp256k1 public key object. - * @param type One of the supported key types. - */ - export function marshalPublicKey(key: PublicKey, type?: KeyType | string): Uint8Array; - - /** - * Converts a protobuf serialized private key into its representative object. - * @param buf The protobuf serialized private key. - */ - export function unmarshalPrivateKey(buf: Uint8Array): Promise; - - /** - * Converts a private key object into a protobuf serialized private key. - * @param key An RSA, Ed25519, or Secp256k1 private key object. - * @param type One of the supported key types. - */ - export function marshalPrivateKey(key: PrivateKey, type?: KeyType | string): Uint8Array; - - /** - * Converts a PEM password protected private key into its representative object. - * @param pem Password protected private key in PEM format. - * @param password The password used to protect the key. - */ - function _import(pem: string, password: string, format?: string): Promise; - export { _import as import }; -} - -/** - * Generates a Uint8Array populated by random bytes. - * @param The size of the random bytes Uint8Array. - */ -export function randomBytes(number: number): Uint8Array; - -/** - * Computes the Password-Based Key Derivation Function 2. - * @param password The password. - * @param salt The salt. - * @param iterations Number of iterations to use. - * @param keySize The size of the output key in bytes. - * @param hash The hash name ('sha1', 'sha2-512, ...) - */ -export function pbkdf2( - password: string | Uint8Array, - salt: string | Uint8Array, - iterations: number, - keySize: number, - hash: string -): Uint8Array; diff --git a/src/index.js b/src/index.js index 863625ed..26955c23 100644 --- a/src/index.js +++ b/src/index.js @@ -4,8 +4,40 @@ const hmac = require('./hmac') const aes = require('./aes') const keys = require('./keys') +/** + * Maps an IPFS hash name to its node-forge equivalent. + * See https://github.com/multiformats/multihash/blob/master/hashtable.csv + * + * @typedef {import('./hmac').HashType} HashType + * @typedef {import('./keys/ecdh').Curve} CurveType + * @typedef {import('./keys/key-stretcher').Cipher} CipherType + */ + +/** + * Exposes an interface to AES encryption (formerly Rijndael), + * as defined in U.S. Federal Information Processing Standards Publication 197. + * This uses CTR mode. + */ exports.aes = aes + +/** + * Exposes an interface to the Keyed-Hash Message Authentication Code (HMAC) + * as defined in U.S. Federal Information Processing Standards Publication 198. + * An HMAC is a cryptographic hash that uses a key to sign a message. + * The receiver verifies the hash by recomputing it using the same key. + */ exports.hmac = hmac + +/** + * Exposes an interface to various cryptographic key generation routines. + * Currently the 'RSA' and 'ed25519' types are supported, although ed25519 keys + * support only signing and verification of messages. For encryption / decryption + * support, RSA keys should be used. + * Installing the libp2p-crypto-secp256k1 module adds support for the 'secp256k1' + * type, which supports ECDSA signatures using the secp256k1 elliptic curve + * popularized by Bitcoin. This module is not installed by default, and should be + * explicitly depended on if your project requires secp256k1 support. + */ exports.keys = keys exports.randomBytes = require('./random-bytes') exports.pbkdf2 = require('./pbkdf2') diff --git a/src/keys/ecdh-browser.js b/src/keys/ecdh-browser.js index 8eade6a3..dc358bef 100644 --- a/src/keys/ecdh-browser.js +++ b/src/keys/ecdh-browser.js @@ -14,6 +14,12 @@ const bits = { 'P-521': 521 } +/** + * @typedef {keyof typeof bits} Curve + * + * @param {Curve} curve + */ + exports.generateEphmeralKeyPair = async function (curve) { validateCurveType(Object.keys(bits), curve) const pair = await webcrypto.get().subtle.generateKey( @@ -25,7 +31,12 @@ exports.generateEphmeralKeyPair = async function (curve) { ['deriveBits'] ) - // forcePrivate is used for testing only + /** + * forcePrivate is used for testing only + * + * @param {Uint8Array} theirPub + * @param {{ private: Uint8Array, public: Uint8Array }} [forcePrivate] + */ const genSharedKey = async (theirPub, forcePrivate) => { let privateKey @@ -61,14 +72,13 @@ exports.generateEphmeralKeyPair = async function (curve) { const buffer = await webcrypto.get().subtle.deriveBits( { name: 'ECDH', - namedCurve: curve, public: keys[0] }, keys[1], bits[curve] ) - return new Uint8Array(buffer, buffer.byteOffset, buffer.byteLength) + return new Uint8Array(buffer) } const publicKey = await webcrypto.get().subtle.exportKey('jwk', pair.publicKey) @@ -85,24 +95,34 @@ const curveLengths = { 'P-521': 66 } -// Marshal converts a jwk encodec ECDH public key into the -// form specified in section 4.3.6 of ANSI X9.62. (This is the format -// go-ipfs uses) +/** + * Marshal converts a jwk encodec ECDH public key into the + * form specified in section 4.3.6 of ANSI X9.62. (This is the format + * go-ipfs uses) + * + * @param {JsonWebKey} jwk + */ function marshalPublicKey (jwk) { - const byteLen = curveLengths[jwk.crv] + const { crv, x, y } = /** @type {{crv:Curve, x:string, y:string}} */ (jwk) + const byteLen = curveLengths[crv] return uint8ArrayConcat([ Uint8Array.from([4]), // uncompressed point - base64urlToBuffer(jwk.x, byteLen), - base64urlToBuffer(jwk.y, byteLen) + base64urlToBuffer(x, byteLen), + base64urlToBuffer(y, byteLen) ], 1 + byteLen * 2) } -// Unmarshal converts a point, serialized by Marshal, into an jwk encoded key +/** + * Unmarshal converts a point, serialized by Marshal, into an jwk encoded key + * + * @param {Curve} curve + * @param {Uint8Array} key + */ function unmarshalPublicKey (curve, key) { const byteLen = curveLengths[curve] - if (uint8ArrayEquals(!key.slice(0, 1), Uint8Array.from([4]))) { + if (!uint8ArrayEquals(key.slice(0, 1), Uint8Array.from([4]))) { throw errcode(new Error('Cannot unmarshal public key - invalid key format'), 'ERR_INVALID_KEY_FORMAT') } @@ -115,6 +135,11 @@ function unmarshalPublicKey (curve, key) { } } +/** + * + * @param {Curve} curve + * @param {{public: Uint8Array, private: Uint8Array}} key + */ const unmarshalPrivateKey = (curve, key) => ({ ...unmarshalPublicKey(curve, key.public), d: uint8ArrayToString(key.private, 'base64url') diff --git a/src/keys/ecdh.js b/src/keys/ecdh.js index 69651b6d..c4bdb0d1 100644 --- a/src/keys/ecdh.js +++ b/src/keys/ecdh.js @@ -8,6 +8,10 @@ const curves = { 'P-384': 'secp384r1', 'P-521': 'secp521r1' } +/** + * @typedef {keyof typeof curves} Curve + * @param {Curve} curve + */ exports.generateEphmeralKeyPair = async function (curve) { // eslint-disable-line require-await validateCurveType(Object.keys(curves), curve) @@ -16,7 +20,13 @@ exports.generateEphmeralKeyPair = async function (curve) { // eslint-disable-lin ecdh.generateKeys() return { + /** @type {Uint8Array} */ key: ecdh.getPublicKey(), + /** + * @param {Uint8Array} theirPub + * @param {{private: Uint8Array, public?:any}} [forcePrivate] + * @returns {Promise} + */ async genSharedKey (theirPub, forcePrivate) { // eslint-disable-line require-await if (forcePrivate) { ecdh.setPrivateKey(forcePrivate.private) diff --git a/src/keys/ed25519-class.js b/src/keys/ed25519-class.js index 4d324deb..f8ada5d4 100644 --- a/src/keys/ed25519-class.js +++ b/src/keys/ed25519-class.js @@ -1,5 +1,6 @@ 'use strict' +// @ts-ignore const sha = require('multihashing-async/src/sha') const errcode = require('err-code') const uint8ArrayEquals = require('uint8arrays/equals') @@ -8,11 +9,31 @@ const crypto = require('./ed25519') const pbm = require('./keys') const exporter = require('./exporter') +/** + * @typedef {import('libp2p-interfaces/src/crypto/types').PublicKey<'Ed25519'>} PublicKey + * @implements {PublicKey} + */ class Ed25519PublicKey { + /** + * @param {Uint8Array} key + */ constructor (key) { + /** @private */ this._key = ensureKey(key, crypto.publicKeyLength) } + /** + * @type {'Ed25519'} + */ + get algorithm () { + return 'Ed25519' + } + + /** + * + * @param {Uint8Array} data + * @param {Uint8Array} sig + */ async verify (data, sig) { // eslint-disable-line require-await return crypto.hashAndVerify(this._key, sig, data) } @@ -28,6 +49,10 @@ class Ed25519PublicKey { }).finish() } + /** + * @param {import('libp2p-interfaces/src/crypto/types').PublicKey} key + * @returns {key is this} + */ equals (key) { return uint8ArrayEquals(this.bytes, key.bytes) } @@ -37,14 +62,33 @@ class Ed25519PublicKey { } } +/** + * @typedef {import('libp2p-interfaces/src/crypto/types').PrivateKey<'Ed25519'>} PrivateKey + * @implements {PrivateKey} + */ class Ed25519PrivateKey { - // key - 64 byte Uint8Array containing private key - // publicKey - 32 byte Uint8Array containing public key + /** + * + * @param {Uint8Array} key - 64 byte Uint8Array containing private key + * @param {Uint8Array} publicKey - 32 byte Uint8Array containing public key + */ constructor (key, publicKey) { + /** @private */ this._key = ensureKey(key, crypto.privateKeyLength) + /** @private */ this._publicKey = ensureKey(publicKey, crypto.publicKeyLength) } + /** + * @type {'Ed25519'} + */ + get algorithm () { + return 'Ed25519' + } + + /** + * @param {Uint8Array} message + */ async sign (message) { // eslint-disable-line require-await return crypto.hashAndSign(this._key, message) } @@ -64,6 +108,10 @@ class Ed25519PrivateKey { }).finish() } + /** + * @param {import('libp2p-interfaces/src/crypto/types').PrivateKey} key + * @returns {key is this} + */ equals (key) { return uint8ArrayEquals(this.bytes, key.bytes) } @@ -91,7 +139,7 @@ class Ed25519PrivateKey { * * @param {string} password - The password to encrypt the key * @param {string} [format=libp2p-key] - The format in which to export as - * @returns {Promise} The encrypted private key + * @returns {Promise} The encrypted private key */ async export (password, format = 'libp2p-key') { // eslint-disable-line require-await if (format === 'libp2p-key') { @@ -102,6 +150,10 @@ class Ed25519PrivateKey { } } +/** + * @param {Uint8Array} bytes + * @returns {PrivateKey} + */ function unmarshalEd25519PrivateKey (bytes) { // Try the old, redundant public key version if (bytes.length > crypto.privateKeyLength) { @@ -117,21 +169,38 @@ function unmarshalEd25519PrivateKey (bytes) { return new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes) } +/** + * @param {Uint8Array} bytes + * @returns {PublicKey} + */ function unmarshalEd25519PublicKey (bytes) { bytes = ensureKey(bytes, crypto.publicKeyLength) return new Ed25519PublicKey(bytes) } +/** + * @returns {Promise} + */ async function generateKeyPair () { const { privateKey, publicKey } = await crypto.generateKey() return new Ed25519PrivateKey(privateKey, publicKey) } +/** + * + * @param {Uint8Array} seed + * @returns {Promise} + */ async function generateKeyPairFromSeed (seed) { const { privateKey, publicKey } = await crypto.generateKeyFromSeed(seed) return new Ed25519PrivateKey(privateKey, publicKey) } +/** + * + * @param {Uint8Array} key + * @param {number} length + */ function ensureKey (key, length) { key = Uint8Array.from(key || []) if (key.length !== length) { diff --git a/src/keys/ed25519.js b/src/keys/ed25519.js index 75b6671c..faec60eb 100644 --- a/src/keys/ed25519.js +++ b/src/keys/ed25519.js @@ -1,6 +1,9 @@ 'use strict' +// @ts-ignore require('node-forge/lib/ed25519') +/** @type {import('node-forge')} */ +// @ts-ignore const forge = require('node-forge/lib/forge') exports.publicKeyLength = forge.pki.ed25519.constants.PUBLIC_KEY_BYTE_LENGTH exports.privateKeyLength = forge.pki.ed25519.constants.PRIVATE_KEY_BYTE_LENGTH @@ -9,16 +12,29 @@ exports.generateKey = async function () { // eslint-disable-line require-await return forge.pki.ed25519.generateKeyPair() } -// seed should be a 32 byte uint8array +/** + * @param {Uint8Array} seed - should be a 32 byte uint8array + */ exports.generateKeyFromSeed = async function (seed) { // eslint-disable-line require-await return forge.pki.ed25519.generateKeyPair({ seed }) } +/** + * + * @param {Uint8Array} key + * @param {Uint8Array} msg + */ exports.hashAndSign = async function (key, msg) { // eslint-disable-line require-await return forge.pki.ed25519.sign({ message: msg, privateKey: key }) // return Uint8Array.from(nacl.sign.detached(msg, key)) } +/** + * + * @param {Uint8Array} key + * @param {Uint8Array} sig + * @param {Uint8Array} msg + */ exports.hashAndVerify = async function (key, sig, msg) { // eslint-disable-line require-await return forge.pki.ed25519.verify({ signature: sig, message: msg, publicKey: key }) } diff --git a/src/keys/ephemeral-keys.js b/src/keys/ephemeral-keys.js index 5a19819a..c491716c 100644 --- a/src/keys/ephemeral-keys.js +++ b/src/keys/ephemeral-keys.js @@ -2,8 +2,12 @@ const ecdh = require('./ecdh') -// Generates an ephemeral public key and returns a function that will compute -// the shared secret key. -// -// Focuses only on ECDH now, but can be made more general in the future. +/** + * Generates an ephemeral public key and returns a function that will compute + * the shared secret key. + * + * Focuses only on ECDH now, but can be made more general in the future. + * + * @param {'P-256'|'P-384'|'P-521'} curve + */ module.exports = async (curve) => ecdh.generateEphmeralKeyPair(curve) // eslint-disable-line require-await diff --git a/src/keys/index.js b/src/keys/index.js index 320a781f..83e19122 100644 --- a/src/keys/index.js +++ b/src/keys/index.js @@ -1,8 +1,12 @@ 'use strict' const keysPBM = require('./keys') +// @ts-ignore require('node-forge/lib/asn1') +// @ts-ignores require('node-forge/lib/pbe') +/** @type {import('node-forge')} */ +// @ts-ignore const forge = require('node-forge/lib/forge') const errcode = require('err-code') const uint8ArrayFromString = require('uint8arrays/from-string') @@ -15,12 +19,30 @@ const supportedKeys = { secp256k1: require('./secp256k1-class')(keysPBM, require('../random-bytes')) } +/** + * @template {KeyType} Type + * @typedef {Type extends 'RSA' ? import('./rsa-class').PrivateKey : + * Type extends 'Ed25519' ? import('./ed25519-class').PrivateKey : + * Type extends 'secp256k1' ? import('./secp256k1-class').PrivateKey : + * never} KeyByType + */ + +/** + * @typedef {"Ed25519" | "RSA" | "secp256k1"} KeyType + */ + const ErrMissingSecp256K1 = { message: 'secp256k1 support requires libp2p-crypto-secp256k1 package', code: 'ERR_MISSING_PACKAGE' } +/** + * @template {KeyType} Type + * @param {Type} type + * @returns {any} + */ function typeToKey (type) { + // @ts-expect-error - No index signature on supportedKeys const key = supportedKeys[type.toLowerCase()] if (!key) { const supported = Object.keys(supportedKeys).join(' / ') @@ -29,23 +51,44 @@ function typeToKey (type) { return key } -// Generates a keypair of the given type and bitsize +/** + * Generates a keypair of the given type and bitsize + * + * @deprecated - seems to abstract over key type but then bits are ignored by + * generateKeyPair + * + * @template {KeyType} Type + * @param {Type} type + * @param {number} [bits] + * @returns {Promise>} + */ const generateKeyPair = async (type, bits) => { // eslint-disable-line require-await return typeToKey(type).generateKeyPair(bits) } -// Generates a keypair of the given type and bitsize -// seed is a 32 byte uint8array -const generateKeyPairFromSeed = async (type, seed, bits) => { // eslint-disable-line require-await +/** + * Generates a keypair of the given type and bitsize seed is a 32 byte uint8array + * + * @deprecated - This is not generic and does not seems be of much use. + * @template {"Ed25519"} Type + * @param {Type} type + * @param {Uint8Array} seed + * @returns {Promise>} + */ +const generateKeyPairFromSeed = async (type, seed) => { // eslint-disable-line require-await const key = typeToKey(type) if (type.toLowerCase() !== 'ed25519') { throw errcode(new Error('Seed key derivation is unimplemented for RSA or secp256k1'), 'ERR_UNSUPPORTED_KEY_DERIVATION_TYPE') } - return key.generateKeyPairFromSeed(seed, bits) + + return key.generateKeyPairFromSeed(seed) } -// Converts a protobuf serialized public key into its -// representative object +/** + * Converts a protobuf serialized public key into its representative object + * + * @param {Uint8Array} buf + */ const unmarshalPublicKey = (buf) => { const decoded = keysPBM.PublicKey.decode(buf) const data = decoded.Data @@ -62,19 +105,28 @@ const unmarshalPublicKey = (buf) => { throw errcode(new Error(ErrMissingSecp256K1.message), ErrMissingSecp256K1.code) } default: - typeToKey(decoded.Type) // throws because type is not supported + throw typeToKey(decoded.Type) // throws because type is not supported } } -// Converts a public key object into a protobuf serialized public key +/** + * Converts a public key object into a protobuf serialized public key + * + * @template {KeyType} Type + * + * @param {import('libp2p-interfaces/src/crypto/types').PublicKey} key + * @param {Type} [type='RSA'] + */ const marshalPublicKey = (key, type) => { - type = (type || 'rsa').toLowerCase() - typeToKey(type) // check type + typeToKey(type || 'RSA') // check type return key.bytes } -// Converts a protobuf serialized private key into its -// representative object +/** + * Converts a protobuf serialized private key into its representative object + * + * @param {Uint8Array} buf + */ const unmarshalPrivateKey = async (buf) => { // eslint-disable-line require-await const decoded = keysPBM.PrivateKey.decode(buf) const data = decoded.Data @@ -91,14 +143,19 @@ const unmarshalPrivateKey = async (buf) => { // eslint-disable-line require-awai throw errcode(new Error(ErrMissingSecp256K1.message), ErrMissingSecp256K1.code) } default: - typeToKey(decoded.Type) // throws because type is not supported + throw typeToKey(decoded.Type) // throws because type is not supported } } -// Converts a private key object into a protobuf serialized private key +/** + * Converts a private key object into a protobuf serialized private key + * + * @template {KeyType} Type + * @param {import('libp2p-interfaces/src/crypto/types').PrivateKey} key + * @param {Type} [type='rsa'] + */ const marshalPrivateKey = (key, type) => { - type = (type || 'rsa').toLowerCase() - typeToKey(type) // check type + typeToKey((type || 'RSA')) // check type return key.bytes } @@ -120,9 +177,9 @@ const importKey = async (encryptedKey, password) => { // eslint-disable-line req if (key === null) { throw errcode(new Error('Cannot read the key, most likely the password is wrong or not a RSA key'), 'ERR_CANNOT_DECRYPT_PEM') } - let der = forge.asn1.toDer(forge.pki.privateKeyToAsn1(key)) - der = uint8ArrayFromString(der.getBytes(), 'ascii') - return supportedKeys.rsa.unmarshalRsaPrivateKey(der) + const der = forge.asn1.toDer(forge.pki.privateKeyToAsn1(key)) + const derb = uint8ArrayFromString(der.getBytes(), 'ascii') + return supportedKeys.rsa.unmarshalRsaPrivateKey(derb) } module.exports = { diff --git a/src/keys/jwk2pem.js b/src/keys/jwk2pem.js index 82345351..5c06577f 100644 --- a/src/keys/jwk2pem.js +++ b/src/keys/jwk2pem.js @@ -1,17 +1,36 @@ 'use strict' +// @ts-ignore require('node-forge/lib/rsa') +// @ts-ignore const forge = require('node-forge/lib/forge') + const { base64urlToBigInteger } = require('../util') +/** + * @typedef {import('pem-jwk').RSA_JWK} RSA_JWK + * + * @param {*} key + * @param {string[]} types + * @returns {any[]} + */ + function convert (key, types) { return types.map(t => base64urlToBigInteger(key[t])) } +/** + * @param {RSA_JWK} key + * @returns {import('node-forge').pki.rsa.PrivateKey} + */ function jwk2priv (key) { return forge.pki.setRsaPrivateKey(...convert(key, ['n', 'e', 'd', 'p', 'q', 'dp', 'dq', 'qi'])) } +/** + * @param {RSA_JWK} key + * @returns {import('node-forge').pki.rsa.PublicKey} + */ function jwk2pub (key) { return forge.pki.setRsaPublicKey(...convert(key, ['n', 'e'])) } diff --git a/src/keys/key-stretcher.js b/src/keys/key-stretcher.js index 51cca2b4..ddfb746c 100644 --- a/src/keys/key-stretcher.js +++ b/src/keys/key-stretcher.js @@ -20,8 +20,17 @@ const cipherMap = { } } -// Generates a set of keys for each party by stretching the shared key. -// (myIV, theirIV, myCipherKey, theirCipherKey, myMACKey, theirMACKey) +/** + * @typedef {keyof typeof cipherMap} Cipher + * + * Generates a set of keys for each party by stretching the shared key. + * (myIV, theirIV, myCipherKey, theirCipherKey, myMACKey, theirMACKey) + * + * @param {Cipher} cipherType + * @param {hmac.HashType} hash + * @param {Uint8Array} secret + * @returns {Promise} + */ module.exports = async (cipherType, hash, secret) => { const cipher = cipherMap[cipherType] @@ -34,6 +43,7 @@ module.exports = async (cipherType, hash, secret) => { throw errcode(new Error('missing hash type'), 'ERR_MISSING_HASH_TYPE') } + // @ts-expect-error - Blowfish has no keySize const cipherKeySize = cipher.keySize const ivSize = cipher.ivSize const hmacKeySize = 20 @@ -64,6 +74,10 @@ module.exports = async (cipherType, hash, secret) => { const r1 = resultBuffer.slice(0, half) const r2 = resultBuffer.slice(half, resultLength) + /** + * @param {Uint8Array} res + * @returns {import('libp2p-interfaces/src/crypto/types').Keystretcher} + */ const createKey = (res) => ({ iv: res.slice(0, ivSize), cipherKey: res.slice(ivSize, ivSize + cipherKeySize), diff --git a/src/keys/keys.d.ts b/src/keys/keys.d.ts new file mode 100644 index 00000000..e2baaccb --- /dev/null +++ b/src/keys/keys.d.ts @@ -0,0 +1,117 @@ +import * as $protobuf from "protobufjs"; +/** KeyType enum. */ +export enum KeyType { + RSA = 0, + Ed25519 = 1, + Secp256k1 = 2 +} + +/** Represents a PublicKey. */ +export class PublicKey implements IPublicKey { + + /** + * Constructs a new PublicKey. + * @param [p] Properties to set + */ + constructor(p?: IPublicKey); + + /** PublicKey Type. */ + public Type: KeyType; + + /** PublicKey Data. */ + public Data: Uint8Array; + + /** + * Encodes the specified PublicKey message. Does not implicitly {@link PublicKey.verify|verify} messages. + * @param m PublicKey message or plain object to encode + * @param [w] Writer to encode to + * @returns Writer + */ + public static encode(m: IPublicKey, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a PublicKey message from the specified reader or buffer. + * @param r Reader or buffer to decode from + * @param [l] Message length if known beforehand + * @returns PublicKey + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): PublicKey; + + /** + * Creates a PublicKey message from a plain object. Also converts values to their respective internal types. + * @param d Plain object + * @returns PublicKey + */ + public static fromObject(d: { [k: string]: any }): PublicKey; + + /** + * Creates a plain object from a PublicKey message. Also converts values to other types if specified. + * @param m PublicKey + * @param [o] Conversion options + * @returns Plain object + */ + public static toObject(m: PublicKey, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this PublicKey to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; +} + +/** Represents a PrivateKey. */ +export class PrivateKey implements IPrivateKey { + + /** + * Constructs a new PrivateKey. + * @param [p] Properties to set + */ + constructor(p?: IPrivateKey); + + /** PrivateKey Type. */ + public Type: KeyType; + + /** PrivateKey Data. */ + public Data: Uint8Array; + + /** + * Encodes the specified PrivateKey message. Does not implicitly {@link PrivateKey.verify|verify} messages. + * @param m PrivateKey message or plain object to encode + * @param [w] Writer to encode to + * @returns Writer + */ + public static encode(m: IPrivateKey, w?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a PrivateKey message from the specified reader or buffer. + * @param r Reader or buffer to decode from + * @param [l] Message length if known beforehand + * @returns PrivateKey + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(r: ($protobuf.Reader|Uint8Array), l?: number): PrivateKey; + + /** + * Creates a PrivateKey message from a plain object. Also converts values to their respective internal types. + * @param d Plain object + * @returns PrivateKey + */ + public static fromObject(d: { [k: string]: any }): PrivateKey; + + /** + * Creates a plain object from a PrivateKey message. Also converts values to other types if specified. + * @param m PrivateKey + * @param [o] Conversion options + * @returns Plain object + */ + public static toObject(m: PrivateKey, o?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this PrivateKey to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; +} diff --git a/src/keys/rsa-browser.js b/src/keys/rsa-browser.js index acb87237..81b0177a 100644 --- a/src/keys/rsa-browser.js +++ b/src/keys/rsa-browser.js @@ -7,6 +7,9 @@ const uint8ArrayFromString = require('uint8arrays/from-string') exports.utils = require('./rsa-utils') +/** + * @param {number} bits + */ exports.generateKey = async function (bits) { const pair = await webcrypto.get().subtle.generateKey( { @@ -27,7 +30,9 @@ exports.generateKey = async function (bits) { } } -// Takes a jwk key +/** + * @param {JsonWebKey} key + */ exports.unmarshalPrivateKey = async function (key) { const privateKey = await webcrypto.get().subtle.importKey( 'jwk', @@ -58,6 +63,10 @@ exports.unmarshalPrivateKey = async function (key) { exports.getRandomValues = randomBytes +/** + * @param {JsonWebKey} key + * @param {Uint8Array} msg + */ exports.hashAndSign = async function (key, msg) { const privateKey = await webcrypto.get().subtle.importKey( 'jwk', @@ -76,9 +85,16 @@ exports.hashAndSign = async function (key, msg) { Uint8Array.from(msg) ) - return new Uint8Array(sig, sig.byteOffset, sig.byteLength) + return new Uint8Array(sig) } +/** + * + * @param {JsonWebKey} key + * @param {Uint8Array} sig + * @param {Uint8Array} msg + * @returns + */ exports.hashAndVerify = async function (key, sig, msg) { const publicKey = await webcrypto.get().subtle.importKey( 'jwk', @@ -99,6 +115,10 @@ exports.hashAndVerify = async function (key, sig, msg) { ) } +/** + * + * @param {{privateKey: CryptoKey, publicKey: CryptoKey}} pair + */ function exportKey (pair) { return Promise.all([ webcrypto.get().subtle.exportKey('jwk', pair.privateKey), @@ -106,6 +126,9 @@ function exportKey (pair) { ]) } +/** + * @param {JsonWebKey} jwKey + */ function derivePublicFromPrivate (jwKey) { return webcrypto.get().subtle.importKey( 'jwk', @@ -137,17 +160,26 @@ Explanation: const { jwk2pub, jwk2priv } = require('./jwk2pem') -function convertKey (key, pub, msg, handle) { - const fkey = pub ? jwk2pub(key) : jwk2priv(key) - const fmsg = uint8ArrayToString(Uint8Array.from(msg), 'ascii') - const fomsg = handle(fmsg, fkey) - return uint8ArrayFromString(fomsg, 'ascii') -} - -exports.encrypt = function (key, msg) { - return convertKey(key, true, msg, (msg, key) => key.encrypt(msg)) -} - -exports.decrypt = function (key, msg) { - return convertKey(key, false, msg, (msg, key) => key.decrypt(msg)) -} +/** + * @param {Uint8Array} bytes + */ +const encodeAscii = bytes => uint8ArrayToString(Uint8Array.from(bytes), 'ascii') + +/** + * @param {string} text + */ +const decodeAscii = text => uint8ArrayFromString(text, 'ascii') + +/** + * @param {import('pem-jwk').RSA_JWK} key + * @param {Uint8Array} msg + */ +exports.encrypt = (key, msg) => + decodeAscii(jwk2pub(key).encrypt(encodeAscii(msg))) + +/** + * @param {import('pem-jwk').RSA_JWK} key + * @param {Uint8Array} msg + */ +exports.decrypt = (key, msg) => + decodeAscii(jwk2priv(key).decrypt(encodeAscii(msg))) diff --git a/src/keys/rsa-class.js b/src/keys/rsa-class.js index 38e14bce..9012489b 100644 --- a/src/keys/rsa-class.js +++ b/src/keys/rsa-class.js @@ -1,23 +1,51 @@ 'use strict' +// @ts-ignore const sha = require('multihashing-async/src/sha') const errcode = require('err-code') const uint8ArrayEquals = require('uint8arrays/equals') const uint8ArrayToString = require('uint8arrays/to-string') +// @ts-ignore require('node-forge/lib/sha512') +// @ts-ignore require('node-forge/lib/ed25519') +/** @type {import('node-forge')} */ +// @ts-ignore const forge = require('node-forge/lib/forge') const crypto = require('./rsa') const pbm = require('./keys') const exporter = require('./exporter') +/** + * @typedef {import('libp2p-interfaces/src/crypto/types').PublicKey<'RSA'>} VerificationKey + * @typedef {import('libp2p-interfaces/src/crypto/types').EncryptionKey} EncryptionKey + * @typedef {import('libp2p-interfaces/src/crypto/types').PrivateKey<'RSA'>} SigningKey + * @typedef {import('libp2p-interfaces/src/crypto/types').DecryptionKey} DecryptionKey + * @typedef {VerificationKey & EncryptionKey} PublicKey + * @typedef {SigningKey & DecryptionKey & { public: PublicKey }} PrivateKey + * @typedef {import('pem-jwk').RSA_JWK} JWK + * + * @implements {VerificationKey} + * @implements {EncryptionKey} + */ class RsaPublicKey { + /** + @param {JWK} key - Public key in JWK format + */ constructor (key) { this._key = key } + /** + * @returns {'RSA'} + */ + get algorithm () { + return 'RSA' + } + + // @ts-ignore async verify (data, sig) { // eslint-disable-line require-await return crypto.hashAndVerify(this._key, sig, data) } @@ -33,10 +61,18 @@ class RsaPublicKey { }).finish() } + /** + * @param {Uint8Array} bytes + */ + encrypt (bytes) { return crypto.encrypt(this._key, bytes) } + /** + * @param {import('libp2p-interfaces/src/crypto/types').PublicKey} key + * @returns {key is this} + */ equals (key) { return uint8ArrayEquals(this.bytes, key.bytes) } @@ -46,18 +82,32 @@ class RsaPublicKey { } } +/** + * @implements {SigningKey} + * @implements {DecryptionKey} + */ class RsaPrivateKey { - // key - Object of the jwk format - // publicKey - Uint8Array of the spki format + /** + * @param {JWK} key - Private key in JWT format + * @param {JWK} publicKey - Public key in JWT format + */ constructor (key, publicKey) { this._key = key this._publicKey = publicKey } + /** + * @type {'RSA'} + */ + get algorithm () { + return 'RSA' + } + genSecret () { return crypto.getRandomValues(16) } + // @ts-ignore async sign (message) { // eslint-disable-line require-await return crypto.hashAndSign(this._key, message) } @@ -70,6 +120,7 @@ class RsaPrivateKey { return new RsaPublicKey(this._publicKey) } + // @ts-ignore decrypt (bytes) { return crypto.decrypt(this._key, bytes) } @@ -85,6 +136,11 @@ class RsaPrivateKey { }).finish() } + /** + * + * @param {import('libp2p-interfaces/src/crypto/types').PrivateKey} key + * @returns {key is this} + */ equals (key) { return uint8ArrayEquals(this.bytes, key.bytes) } @@ -115,17 +171,17 @@ class RsaPrivateKey { */ async export (password, format = 'pkcs-8') { // eslint-disable-line require-await if (format === 'pkcs-8') { + // @ts-ignore - ByteBuffer isn't present in typedefs const buffer = new forge.util.ByteBuffer(this.marshal()) const asn1 = forge.asn1.fromDer(buffer) const privateKey = forge.pki.privateKeyFromAsn1(asn1) - const options = { + return forge.pki.encryptRsaPrivateKey(privateKey, password, { algorithm: 'aes256', count: 10000, saltSize: 128 / 8, prfAlgorithm: 'sha512' - } - return forge.pki.encryptRsaPrivateKey(privateKey, password, options) + }) } else if (format === 'libp2p-key') { return exporter.export(this.bytes, password) } else { @@ -134,32 +190,54 @@ class RsaPrivateKey { } } +/** + * + * @param {Uint8Array} bytes + * @returns {Promise} + */ async function unmarshalRsaPrivateKey (bytes) { const jwk = crypto.utils.pkcs1ToJwk(bytes) const keys = await crypto.unmarshalPrivateKey(jwk) return new RsaPrivateKey(keys.privateKey, keys.publicKey) } +/** + * @param {Uint8Array} bytes + * @returns {PublicKey} + */ function unmarshalRsaPublicKey (bytes) { const jwk = crypto.utils.pkixToJwk(bytes) return new RsaPublicKey(jwk) } +/** + * + * @param {JWK} jwk + * @returns {Promise} + */ async function fromJwk (jwk) { const keys = await crypto.unmarshalPrivateKey(jwk) return new RsaPrivateKey(keys.privateKey, keys.publicKey) } +/** + * + * @param {number} bits + * @returns {Promise} + */ async function generateKeyPair (bits) { const keys = await crypto.generateKey(bits) return new RsaPrivateKey(keys.privateKey, keys.publicKey) } +const genSecret = () => crypto.getRandomValues(16) + module.exports = { RsaPublicKey, RsaPrivateKey, unmarshalRsaPublicKey, unmarshalRsaPrivateKey, generateKeyPair, - fromJwk + fromJwk, + genSecret } diff --git a/src/keys/rsa-utils.js b/src/keys/rsa-utils.js index 871f89e0..43f50fb1 100644 --- a/src/keys/rsa-utils.js +++ b/src/keys/rsa-utils.js @@ -1,16 +1,25 @@ 'use strict' +// @ts-ignore require('node-forge/lib/asn1') +// @ts-ignore require('node-forge/lib/rsa') +/** @type {import('node-forge')} */ +// @ts-ignore const forge = require('node-forge/lib/forge') const { bigIntegerToUintBase64url, base64urlToBigInteger } = require('./../util') const uint8ArrayFromString = require('uint8arrays/from-string') const uint8ArrayToString = require('uint8arrays/to-string') -// Convert a PKCS#1 in ASN1 DER format to a JWK key +/** + * Convert a PKCS#1 in ASN1 DER format to a JWK key + * + * @param {Uint8Array} bytes + */ exports.pkcs1ToJwk = function (bytes) { const asn1 = forge.asn1.fromDer(uint8ArrayToString(bytes, 'ascii')) - const privateKey = forge.pki.privateKeyFromAsn1(asn1) + const privateKey = /** @type {import('node-forge').pki.rsa.PrivateKey} */ + (forge.pki.privateKeyFromAsn1(asn1)) // https://tools.ietf.org/html/rfc7518#section-6.3.1 return { @@ -28,26 +37,42 @@ exports.pkcs1ToJwk = function (bytes) { } } -// Convert a JWK key into PKCS#1 in ASN1 DER format +/** + * Convert a JWK key into PKCS#1 in ASN1 DER format + * + * @param {import('pem-jwk').RSA_JWK} jwk + */ exports.jwkToPkcs1 = function (jwk) { + // @ts-ignore - struct isn't PrivateKey const asn1 = forge.pki.privateKeyToAsn1({ n: base64urlToBigInteger(jwk.n), e: base64urlToBigInteger(jwk.e), + // @ts-ignore - d is optional d: base64urlToBigInteger(jwk.d), + // @ts-ignore - p is optional p: base64urlToBigInteger(jwk.p), + // @ts-ignore - q is optional q: base64urlToBigInteger(jwk.q), + // @ts-ignore - dp is optional dP: base64urlToBigInteger(jwk.dp), + // @ts-ignore - dq is optional dQ: base64urlToBigInteger(jwk.dq), + // @ts-ignore - qi is optional qInv: base64urlToBigInteger(jwk.qi) }) return uint8ArrayFromString(forge.asn1.toDer(asn1).getBytes(), 'ascii') } -// Convert a PKCIX in ASN1 DER format to a JWK key +/** + * Convert a PKCIX in ASN1 DER format to a JWK key + * + * @param {Uint8Array} bytes + */ exports.pkixToJwk = function (bytes) { const asn1 = forge.asn1.fromDer(uint8ArrayToString(bytes, 'ascii')) - const publicKey = forge.pki.publicKeyFromAsn1(asn1) + const publicKey = /** @type {import('node-forge').pki.rsa.PublicKey} */ + (forge.pki.publicKeyFromAsn1(asn1)) return { kty: 'RSA', @@ -58,8 +83,13 @@ exports.pkixToJwk = function (bytes) { } } -// Convert a JWK key to PKCIX in ASN1 DER format +/** + * Convert a JWK key to PKCIX in ASN1 DER format + * + * @param {import('pem-jwk').RSA_JWK} jwk + */ exports.jwkToPkix = function (jwk) { + // @ts-expect-error - public key has more stuff const asn1 = forge.pki.publicKeyToAsn1({ n: base64urlToBigInteger(jwk.n), e: base64urlToBigInteger(jwk.e) diff --git a/src/keys/rsa.js b/src/keys/rsa.js index f5704e8a..2683c710 100644 --- a/src/keys/rsa.js +++ b/src/keys/rsa.js @@ -3,18 +3,27 @@ const crypto = require('crypto') const errcode = require('err-code') const randomBytes = require('../random-bytes') + +/** + * @typedef {import('pem-jwk').RSA_JWK} RSA_JWK + */ + // @ts-check /** - * @type {PrivateKey} + * @type {typeof import('keypair').keypair} */ let keypair try { if (process.env.LP2P_FORCE_CRYPTO_LIB === 'keypair') { throw new Error('Force keypair usage') } + // @ts-ignore const ursa = require('ursa-optional') // throws if not compiled - keypair = ({ bits }) => { + /** + * @param {{bits?: number}} options + */ + keypair = ({ bits } = {}) => { const key = ursa.generatePrivateKey(bits) return { private: key.toPrivatePem(), @@ -26,6 +35,7 @@ try { throw e } + // @ts-ignore keypair = require('keypair') } const pemToJwk = require('pem-jwk').pem2jwk @@ -33,6 +43,9 @@ const jwkToPem = require('pem-jwk').jwk2pem exports.utils = require('./rsa-utils') +/** + * @param {number} bits + */ exports.generateKey = async function (bits) { // eslint-disable-line require-await const key = keypair({ bits }) return { @@ -41,7 +54,9 @@ exports.generateKey = async function (bits) { // eslint-disable-line require-awa } } -// Takes a jwk key +/** + * @param {RSA_JWK} key + */ exports.unmarshalPrivateKey = async function (key) { // eslint-disable-line require-await if (!key) { throw errcode(new Error('Missing key parameter'), 'ERR_MISSING_KEY') @@ -58,6 +73,12 @@ exports.unmarshalPrivateKey = async function (key) { // eslint-disable-line requ exports.getRandomValues = randomBytes +/** + * + * @param {RSA_JWK} key + * @param {Uint8Array} msg + * @returns {Promise} + */ exports.hashAndSign = async function (key, msg) { // eslint-disable-line require-await const sign = crypto.createSign('RSA-SHA256') sign.update(msg) @@ -65,6 +86,13 @@ exports.hashAndSign = async function (key, msg) { // eslint-disable-line require return sign.sign(pem) } +/** + * + * @param {RSA_JWK} key + * @param {Uint8Array} sig + * @param {Uint8Array} msg + * @returns {Promise} + */ exports.hashAndVerify = async function (key, sig, msg) { // eslint-disable-line require-await const verify = crypto.createVerify('RSA-SHA256') verify.update(msg) @@ -74,10 +102,20 @@ exports.hashAndVerify = async function (key, sig, msg) { // eslint-disable-line const padding = crypto.constants.RSA_PKCS1_PADDING +/** + * @param {RSA_JWK} key + * @param {Uint8Array} bytes + * @returns {Uint8Array} + */ exports.encrypt = function (key, bytes) { return crypto.publicEncrypt({ key: jwkToPem(key), padding }, bytes) } +/** + * @param {RSA_JWK} key + * @param {Uint8Array} bytes + * @returns {Uint8Array} + */ exports.decrypt = function (key, bytes) { return crypto.privateDecrypt({ key: jwkToPem(key), padding }, bytes) } diff --git a/src/keys/secp256k1-class.js b/src/keys/secp256k1-class.js index 35d735c3..dfe3ecb2 100644 --- a/src/keys/secp256k1-class.js +++ b/src/keys/secp256k1-class.js @@ -1,5 +1,6 @@ 'use strict' +// @ts-ignore const sha = require('multihashing-async/src/sha') const errcode = require('err-code') const uint8ArrayEquals = require('uint8arrays/equals') @@ -7,115 +8,200 @@ const uint8ArrayToString = require('uint8arrays/to-string') const exporter = require('./exporter') -module.exports = (keysProtobuf, randomBytes, crypto) => { - crypto = crypto || require('./secp256k1')(randomBytes) +/** + * @typedef {ReturnType} Secp256k1Crypto + * @typedef {import('libp2p-interfaces/src/crypto/types').PublicKey<'secp256k1'>} PublicKey + * @typedef {import('libp2p-interfaces/src/crypto/types').PrivateKey<'secp256k1'>} PrivateKey + * + * @typedef {{ + * keysProtobuf: import('./keys') + * crypto: Secp256k1Crypto + * }} Context + */ + +/** + * @implements {PublicKey} + */ +class Secp256k1PublicKey { + /** + * @param {Context} context + * @param {Uint8Array} key + */ + constructor (context, key) { + /** @private */ + this._context = context + context.crypto.validatePublicKey(key) + this._key = key + } - class Secp256k1PublicKey { - constructor (key) { - crypto.validatePublicKey(key) - this._key = key - } + /** + * @type {'secp256k1'} + */ + get algorithm () { + return 'secp256k1' + } - verify (data, sig) { - return crypto.hashAndVerify(this._key, sig, data) - } + /** + * @param {Uint8Array} data + * @param {Uint8Array} sig + */ + verify (data, sig) { + return this._context.crypto.hashAndVerify(this._key, sig, data) + } - marshal () { - return crypto.compressPublicKey(this._key) - } + marshal () { + return this._context.crypto.compressPublicKey(this._key) + } - get bytes () { - return keysProtobuf.PublicKey.encode({ - Type: keysProtobuf.KeyType.Secp256k1, - Data: this.marshal() - }).finish() - } + get bytes () { + const { keysProtobuf } = this._context + return keysProtobuf.PublicKey.encode({ + Type: keysProtobuf.KeyType.Secp256k1, + Data: this.marshal() + }).finish() + } - equals (key) { - return uint8ArrayEquals(this.bytes, key.bytes) - } + /** + * + * @param {import('libp2p-interfaces/src/crypto/types').PublicKey} key + * @returns {key is this} + */ + equals (key) { + return uint8ArrayEquals(this.bytes, key.bytes) + } - hash () { - return sha.multihashing(this.bytes, 'sha2-256') - } + hash () { + return sha.multihashing(this.bytes, 'sha2-256') } +} - class Secp256k1PrivateKey { - constructor (key, publicKey) { - this._key = key - this._publicKey = publicKey || crypto.computePublicKey(key) - crypto.validatePrivateKey(this._key) - crypto.validatePublicKey(this._publicKey) - } +/** + * @implements {PrivateKey} + */ +class Secp256k1PrivateKey { + /** + * @param {Context} context + * @param {Uint8Array} key + * @param {Uint8Array} [publicKey] + */ + constructor (context, key, publicKey) { + /** @private */ + this._context = context + this._key = key + this._publicKey = publicKey || context.crypto.computePublicKey(key) + context.crypto.validatePrivateKey(this._key) + context.crypto.validatePublicKey(this._publicKey) + } - sign (message) { - return crypto.hashAndSign(this._key, message) - } + /** + * @type {'secp256k1'} + */ + get algorithm () { + return 'secp256k1' + } - get public () { - return new Secp256k1PublicKey(this._publicKey) - } + /** + * @param {Uint8Array} message + */ - marshal () { - return this._key - } + sign (message) { + return this._context.crypto.hashAndSign(this._key, message) + } - get bytes () { - return keysProtobuf.PrivateKey.encode({ - Type: keysProtobuf.KeyType.Secp256k1, - Data: this.marshal() - }).finish() - } + /** + * @returns {PublicKey} + */ + get public () { + return new Secp256k1PublicKey(this._context, this._publicKey) + } - equals (key) { - return uint8ArrayEquals(this.bytes, key.bytes) - } + marshal () { + return this._key + } - hash () { - return sha.multihashing(this.bytes, 'sha2-256') - } + get bytes () { + const { keysProtobuf } = this._context + return keysProtobuf.PrivateKey.encode({ + Type: keysProtobuf.KeyType.Secp256k1, + Data: this.marshal() + }).finish() + } - /** - * Gets the ID of the key. - * - * The key id is the base58 encoding of the SHA-256 multihash of its public key. - * The public key is a protobuf encoding containing a type and the DER encoding - * of the PKCS SubjectPublicKeyInfo. - * - * @returns {Promise} - */ - async id () { - const hash = await this.public.hash() - return uint8ArrayToString(hash, 'base58btc') - } + /** + * @param {import('libp2p-interfaces/src/crypto/types').PrivateKey} key + * @returns {key is this} + */ + equals (key) { + return uint8ArrayEquals(this.bytes, key.bytes) + } + + hash () { + return sha.multihashing(this.bytes, 'sha2-256') + } + + /** + * Gets the ID of the key. + * + * The key id is the base58 encoding of the SHA-256 multihash of its public key. + * The public key is a protobuf encoding containing a type and the DER encoding + * of the PKCS SubjectPublicKeyInfo. + * + * @returns {Promise} + */ + async id () { + const hash = await this.public.hash() + return uint8ArrayToString(hash, 'base58btc') + } - /** - * Exports the key into a password protected `format` - * - * @param {string} password - The password to encrypt the key - * @param {string} [format=libp2p-key] - The format in which to export as - * @returns {Promise} The encrypted private key - */ - async export (password, format = 'libp2p-key') { // eslint-disable-line require-await - if (format === 'libp2p-key') { - return exporter.export(this.bytes, password) - } else { - throw errcode(new Error(`export format '${format}' is not supported`), 'ERR_INVALID_EXPORT_FORMAT') - } + /** + * Exports the key into a password protected `format` + * + * @param {string} password - The password to encrypt the key + * @param {string} [format=libp2p-key] - The format in which to export as + * @returns {Promise} The encrypted private key + */ + async export (password, format = 'libp2p-key') { // eslint-disable-line require-await + if (format === 'libp2p-key') { + return exporter.export(this.bytes, password) + } else { + throw errcode(new Error(`export format '${format}' is not supported`), 'ERR_INVALID_EXPORT_FORMAT') } } +} + +/** + * @param {import('./keys')} keysProtobuf + * @param {import('../random-bytes')} randomBytes + * @param {ReturnType} [cryptoAPI] + */ +module.exports = (keysProtobuf, randomBytes, cryptoAPI) => { + const crypto = cryptoAPI || require('./secp256k1')(randomBytes) + const context = { keysProtobuf, crypto } + + /** + * @param {Uint8Array} bytes + * @returns {PrivateKey} + */ function unmarshalSecp256k1PrivateKey (bytes) { - return new Secp256k1PrivateKey(bytes) + return new Secp256k1PrivateKey(context, bytes) } + /** + * + * @param {Uint8Array} bytes + * @returns {PublicKey} + */ function unmarshalSecp256k1PublicKey (bytes) { - return new Secp256k1PublicKey(bytes) + return new Secp256k1PublicKey(context, bytes) } + /** + * @returns {Promise} + */ async function generateKeyPair () { const privateKeyBytes = await crypto.generateKey() - return new Secp256k1PrivateKey(privateKeyBytes) + return new Secp256k1PrivateKey(context, privateKeyBytes) } return { @@ -126,3 +212,5 @@ module.exports = (keysProtobuf, randomBytes, crypto) => { generateKeyPair } } +module.exports.Secp256k1PublicKey = Secp256k1PublicKey +module.exports.Secp256k1PrivateKey = Secp256k1PrivateKey diff --git a/src/keys/secp256k1.js b/src/keys/secp256k1.js index 50d772e7..fe26f6f0 100644 --- a/src/keys/secp256k1.js +++ b/src/keys/secp256k1.js @@ -1,12 +1,19 @@ 'use strict' const secp256k1 = require('secp256k1') +// @ts-ignore const sha = require('multihashing-async/src/sha') const HASH_ALGORITHM = 'sha2-256' +/** + * @param {import('../random-bytes')} randomBytes + */ module.exports = (randomBytes) => { const privateKeyLength = 32 + /** + * @returns {Uint8Array} + */ function generateKey () { let privateKey do { @@ -15,18 +22,32 @@ module.exports = (randomBytes) => { return privateKey } + /** + * @param {Uint8Array} key + * @param {Uint8Array} msg + */ + async function hashAndSign (key, msg) { const digest = await sha.digest(msg, HASH_ALGORITHM) const sig = secp256k1.ecdsaSign(digest, key) return secp256k1.signatureExport(sig.signature) } + /** + * @param {Uint8Array} key + * @param {Uint8Array} sig + * @param {Uint8Array} msg + */ async function hashAndVerify (key, sig, msg) { const digest = await sha.digest(msg, HASH_ALGORITHM) sig = secp256k1.signatureImport(sig) return secp256k1.ecdsaVerify(sig, digest, key) } + /** + * @param {Uint8Array} key + * @returns {Uint8Array} + */ function compressPublicKey (key) { if (!secp256k1.publicKeyVerify(key)) { throw new Error('Invalid public key') @@ -34,22 +55,36 @@ module.exports = (randomBytes) => { return secp256k1.publicKeyConvert(key, true) } + /** + * @param {Uint8Array} key + * @returns {Uint8Array} + */ function decompressPublicKey (key) { return secp256k1.publicKeyConvert(key, false) } + /** + * @param {Uint8Array} key + */ function validatePrivateKey (key) { if (!secp256k1.privateKeyVerify(key)) { throw new Error('Invalid private key') } } + /** + * @param {Uint8Array} key + */ function validatePublicKey (key) { if (!secp256k1.publicKeyVerify(key)) { throw new Error('Invalid public key') } } + /** + * @param {Uint8Array} privateKey + * @returns {Uint8Array} + */ function computePublicKey (privateKey) { validatePrivateKey(privateKey) return secp256k1.publicKeyCreate(privateKey) diff --git a/src/keys/types.d.ts b/src/keys/types.d.ts new file mode 100644 index 00000000..f9044b81 --- /dev/null +++ b/src/keys/types.d.ts @@ -0,0 +1,6 @@ + +export type KeyByType = + Type extends 'RSA' ? import('./rsa-class').PrivateKey : + Type extends 'ED25519' ? import('./ed25519-class').PrivateKey : + Type extends 'secp256k1' ? import('./secp256k1-class').PrivateKey : + never diff --git a/src/keys/validate-curve-type.js b/src/keys/validate-curve-type.js index 81ccb2e3..6d16a5d9 100644 --- a/src/keys/validate-curve-type.js +++ b/src/keys/validate-curve-type.js @@ -2,6 +2,11 @@ const errcode = require('err-code') +/** + * + * @param {string[]} curveTypes + * @param {string} type + */ module.exports = function (curveTypes, type) { if (!curveTypes.includes(type)) { const names = curveTypes.join(' / ') diff --git a/src/pbkdf2.js b/src/pbkdf2.js index 4d2f0a2e..2d50b272 100644 --- a/src/pbkdf2.js +++ b/src/pbkdf2.js @@ -1,6 +1,8 @@ 'use strict' +// @ts-ignore const forgePbkdf2 = require('node-forge/lib/pbkdf2') +// @ts-ignore const forgeUtil = require('node-forge/lib/util') const errcode = require('err-code') @@ -20,11 +22,11 @@ const hashName = { /** * Computes the Password-Based Key Derivation Function 2. * - * @param {string} password - * @param {string} salt - * @param {number} iterations - * @param {number} keySize - (in bytes) - * @param {string} hash - The hash name ('sha1', 'sha2-512, ...) + * @param {string} password - The password. + * @param {string} salt - The salt. + * @param {number} iterations - Number of iterations to use. + * @param {number} keySize - The size of the output key in bytes. + * @param {keyof hashName} hash - The hash name ('sha1', 'sha2-512, ...) * @returns {string} - A new password */ function pbkdf2 (password, salt, iterations, keySize, hash) { diff --git a/src/random-bytes.js b/src/random-bytes.js index ef1edd51..e0c0d2d5 100644 --- a/src/random-bytes.js +++ b/src/random-bytes.js @@ -1,7 +1,16 @@ 'use strict' + +// @ts-ignore - not typed const randomBytes = require('iso-random-stream/src/random') const errcode = require('err-code') +/** + * + * Generates a Uint8Array populated by random bytes. + * + * @param {number} length - The size of the random bytes Uint8Array. + * @returns {Uint8Array} + */ module.exports = function (length) { if (isNaN(length) || length <= 0) { throw errcode(new Error('random bytes length must be a Number bigger than 0'), 'ERR_INVALID_LENGTH') diff --git a/src/util.js b/src/util.js index 7f686523..6a5da917 100644 --- a/src/util.js +++ b/src/util.js @@ -1,12 +1,21 @@ 'use strict' +// @ts-ignore require('node-forge/lib/util') +// @ts-ignore require('node-forge/lib/jsbn') +/** @type {import('node-forge')} */ +// @ts-ignore const forge = require('node-forge/lib/forge') const uint8ArrayFromString = require('uint8arrays/from-string') const uint8ArrayToString = require('uint8arrays/to-string') const uint8ArrayConcat = require('uint8arrays/concat') +/** + * + * @param {import('node-forge').jsbn.BigInteger} num + * @param {number} [len] + */ exports.bigIntegerToUintBase64url = (num, len) => { // Call `.abs()` to convert to unsigned let buf = Uint8Array.from(num.abs().toByteArray()) // toByteArray converts to big endian @@ -25,12 +34,21 @@ exports.bigIntegerToUintBase64url = (num, len) => { return uint8ArrayToString(buf, 'base64url') } -// Convert a base64url encoded string to a BigInteger +/** + * Convert a base64url encoded string to a BigInteger + * + * @param {string} str + */ exports.base64urlToBigInteger = str => { const buf = exports.base64urlToBuffer(str) return new forge.jsbn.BigInteger(uint8ArrayToString(buf, 'base16'), 16) } +/** + * + * @param {string} str + * @param {number} [len] + */ exports.base64urlToBuffer = (str, len) => { let buf = uint8ArrayFromString(str, 'base64urlpad') diff --git a/src/webcrypto.js b/src/webcrypto.js index 1663369a..a85da5c1 100644 --- a/src/webcrypto.js +++ b/src/webcrypto.js @@ -4,6 +4,11 @@ // Check native crypto exists and is enabled (In insecure context `self.crypto` // exists but `self.crypto.subtle` does not). +/** + * + * @param {Window & {msCrypto?: Crypto}} [win] + * @returns {Crypto} + */ exports.get = (win = self) => { const nativeCrypto = win.crypto || win.msCrypto diff --git a/test/aes/aes.spec.js b/test/aes/aes.spec.js index 275aa965..8a60786d 100644 --- a/test/aes/aes.spec.js +++ b/test/aes/aes.spec.js @@ -18,11 +18,11 @@ const bytes = { 32: 'AES-256' } -/** @typedef {import("libp2p-crypto").aes.Cipher} Cipher */ +/** @typedef {import("libp2p-interfaces/src/crypto/types").Cipher} Cipher */ describe('AES-CTR', () => { - Object.keys(bytes).forEach((byte) => { - it(`${bytes[byte]} - encrypt and decrypt`, async () => { + Object.entries(bytes).forEach(([byte, name]) => { + it(`${name} - encrypt and decrypt`, async () => { const key = new Uint8Array(parseInt(byte, 10)) key.fill(5) @@ -39,9 +39,10 @@ describe('AES-CTR', () => { }) }) - Object.keys(bytes).forEach((byte) => { - it(`${bytes[byte]} - fixed - encrypt and decrypt`, async () => { - const key = new Uint8Array(parseInt(byte, 10)) + Object.entries(bytes).forEach(([key, name]) => { + const byte = /** @type {16|32} */(parseInt(key, 10)) + it(`${name} - fixed - encrypt and decrypt`, async () => { + const key = new Uint8Array(byte) key.fill(5) const iv = new Uint8Array(16) @@ -62,13 +63,14 @@ describe('AES-CTR', () => { }) }) - Object.keys(bytes).forEach((byte) => { + Object.keys(bytes).forEach((key) => { + const byte = /** @type {16} */(parseInt(key, 10)) if (!goFixtures[byte]) { return } it(`${bytes[byte]} - go interop - encrypt and decrypt`, async () => { - const key = new Uint8Array(parseInt(byte, 10)) + const key = new Uint8Array(byte) key.fill(5) const iv = new Uint8Array(16) diff --git a/test/browser.js b/test/browser.js index e6f811c8..2c254b80 100644 --- a/test/browser.js +++ b/test/browser.js @@ -1,4 +1,5 @@ /* eslint-env mocha */ +// @ts-nocheck 'use strict' const chai = require('chai') diff --git a/test/crypto.spec.js b/test/crypto.spec.js index 61f9d47b..d9eade82 100644 --- a/test/crypto.spec.js +++ b/test/crypto.spec.js @@ -11,7 +11,7 @@ const fixtures = require('./fixtures/go-key-rsa') const { expectErrCode } = require('./util') const uint8ArrayEquals = require('uint8arrays/equals') -/** @typedef {import("libp2p-crypto").PrivateKey} PrivateKey */ +/** @typedef {import("libp2p-interfaces/src/crypto/types").PrivateKey} PrivateKey */ describe('libp2p-crypto', function () { this.timeout(20 * 1000) @@ -47,11 +47,13 @@ describe('libp2p-crypto', function () { }) it('generateKeyPair', () => { + // @ts-expect-error return expectErrCode(crypto.keys.generateKeyPair('invalid-key-type', 512), 'ERR_UNSUPPORTED_KEY_TYPE') }) it('generateKeyPairFromSeed', () => { const seed = crypto.randomBytes(32) + // @ts-expect-error return expectErrCode(crypto.keys.generateKeyPairFromSeed('invalid-key-type', seed, 512), 'ERR_UNSUPPORTED_KEY_TYPE') }) @@ -110,6 +112,7 @@ describe('libp2p-crypto', function () { }) it('throws on invalid hash name', () => { + // @ts-expect-error const fn = () => crypto.pbkdf2('password', 'at least 16 character salt', 500, 512 / 8, 'shaX-xxx') expect(fn).to.throw().with.property('code', 'ERR_UNSUPPORTED_HASH_TYPE') }) diff --git a/test/fixtures/go-elliptic-key.js b/test/fixtures/go-elliptic-key.js index 93830dda..1f9d952c 100644 --- a/test/fixtures/go-elliptic-key.js +++ b/test/fixtures/go-elliptic-key.js @@ -1,7 +1,7 @@ 'use strict' module.exports = { - curve: 'P-256', + curve: /** @type {'P-256'} */('P-256'), bob: { private: Uint8Array.from([ 181, 217, 162, 151, 225, 36, 53, 253, 107, 66, 27, 27, 232, 72, 0, 0, 103, 167, 84, 62, 203, 91, 97, 137, 131, 193, 230, 126, 98, 242, 216, 170 diff --git a/test/fixtures/go-stretch-key.js b/test/fixtures/go-stretch-key.js index 241e4d0e..eba6d5d8 100644 --- a/test/fixtures/go-stretch-key.js +++ b/test/fixtures/go-stretch-key.js @@ -1,8 +1,10 @@ 'use strict' +const { the } = require('../util/index') + module.exports = [{ - cipher: 'AES-256', - hash: 'SHA256', + cipher: the('AES-256'), + hash: the('SHA256'), secret: Uint8Array.from([ 195, 191, 209, 165, 209, 201, 127, 122, 136, 111, 31, 66, 111, 68, 38, 155, 216, 204, 46, 181, 200, 188, 170, 204, 104, 74, 239, 251, 173, 114, 222, 234 ]), diff --git a/test/helpers/test-garbage-error-handling.js b/test/helpers/test-garbage-error-handling.js index de892cc4..4663b457 100644 --- a/test/helpers/test-garbage-error-handling.js +++ b/test/helpers/test-garbage-error-handling.js @@ -1,4 +1,5 @@ /* eslint-env mocha */ +// @ts-nocheck 'use strict' const uint8ArrayFromString = require('uint8arrays/from-string') diff --git a/test/hmac/hmac.spec.js b/test/hmac/hmac.spec.js index ddc68676..8921f57c 100644 --- a/test/hmac/hmac.spec.js +++ b/test/hmac/hmac.spec.js @@ -10,6 +10,7 @@ const uint8ArrayFromString = require('uint8arrays/from-string') const crypto = require('../../src') +/** @type {import('../../src').HashType[]} */ const hashes = ['SHA1', 'SHA256', 'SHA512'] describe('HMAC', () => { diff --git a/test/keys/ed25519.spec.js b/test/keys/ed25519.spec.js index c0f317ef..b2164c53 100644 --- a/test/keys/ed25519.spec.js +++ b/test/keys/ed25519.spec.js @@ -13,7 +13,7 @@ const fixtures = require('../fixtures/go-key-ed25519') const testGarbage = require('../helpers/test-garbage-error-handling') -/** @typedef {import("libp2p-crypto").PrivateKey} PrivateKey */ +/** @typedef {import("libp2p-interfaces/src/crypto/types").PrivateKey<"Ed25519">} PrivateKey */ describe('ed25519', function () { this.timeout(20 * 1000) @@ -34,7 +34,7 @@ describe('ed25519', function () { it('generates a valid key from seed', async () => { const seed = crypto.randomBytes(32) - const seededkey = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) + const seededkey = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed) expect(seededkey).to.be.an.instanceof(ed25519.Ed25519PrivateKey) const digest = await seededkey.hash() expect(digest).to.have.length(34) @@ -42,17 +42,17 @@ describe('ed25519', function () { it('generates the same key from the same seed', async () => { const seed = crypto.randomBytes(32) - const seededkey1 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) - const seededkey2 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed, 512) + const seededkey1 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed) + const seededkey2 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed) expect(seededkey1.equals(seededkey2)).to.eql(true) expect(seededkey1.public.equals(seededkey2.public)).to.eql(true) }) it('generates different keys for different seeds', async () => { const seed1 = crypto.randomBytes(32) - const seededkey1 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed1, 512) + const seededkey1 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed1) const seed2 = crypto.randomBytes(32) - const seededkey2 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed2, 512) + const seededkey2 = await crypto.keys.generateKeyPairFromSeed('Ed25519', seed2) expect(seededkey1.equals(seededkey2)).to.eql(false) expect(seededkey1.public.equals(seededkey2.public)).to.eql(false) }) diff --git a/test/keys/ephemeral-keys.spec.js b/test/keys/ephemeral-keys.spec.js index a1396268..ebaf0db4 100644 --- a/test/keys/ephemeral-keys.spec.js +++ b/test/keys/ephemeral-keys.spec.js @@ -10,6 +10,7 @@ chai.use(dirtyChai) const fixtures = require('../fixtures/go-elliptic-key') const crypto = require('../../src') +/** @type {import('libp2p-crypto').CurveType[]} */ const curves = ['P-256', 'P-384'] // 'P-521' fails in tests :( no clue why // @ts-check /** @@ -67,6 +68,7 @@ describe('generateEphemeralKeyPair', () => { it('handles bad curve name', async () => { try { + // @ts-expect-error - invalid name await crypto.keys.generateEphemeralKeyPair('bad name') } catch (err) { expect(err.code).equals('ERR_INVALID_CURVE') diff --git a/test/keys/key-stretcher.spec.js b/test/keys/key-stretcher.spec.js index 80efc7f3..60cb92ba 100644 --- a/test/keys/key-stretcher.spec.js +++ b/test/keys/key-stretcher.spec.js @@ -12,7 +12,9 @@ const fixtures = require('../fixtures/go-stretch-key') describe('keyStretcher', () => { describe('generate', () => { + /** @type {import('libp2p-crypto').CipherType[]} */ const ciphers = ['AES-128', 'AES-256', 'Blowfish'] + /** @type {import('libp2p-crypto').HashType[]} */ const hashes = ['SHA1', 'SHA256', 'SHA512'] let res // @ts-check @@ -37,10 +39,12 @@ describe('keyStretcher', () => { }) it('handles invalid cipher type', () => { + // @ts-expect-error return expectErrCode(crypto.keys.keyStretcher('invalid-cipher', 'SHA256', 'secret'), 'ERR_INVALID_CIPHER_TYPE') }) it('handles missing hash type', () => { + // @ts-expect-error return expectErrCode(crypto.keys.keyStretcher('AES-128', '', 'secret'), 'ERR_MISSING_HASH_TYPE') }) }) diff --git a/test/keys/rsa-crypto-libs.js b/test/keys/rsa-crypto-libs.js index 1492397e..052e9eb6 100644 --- a/test/keys/rsa-crypto-libs.js +++ b/test/keys/rsa-crypto-libs.js @@ -16,7 +16,9 @@ describe('RSA crypto libs', function () { LIBS.forEach(lib => { describe(lib, () => { + /** @type {import('../../src')} */ let crypto + /** @type {typeof crypto.keys.supportedKeys.rsa} */ let rsa before(() => { diff --git a/test/keys/rsa.spec.js b/test/keys/rsa.spec.js index c5756d2d..b104abdd 100644 --- a/test/keys/rsa.spec.js +++ b/test/keys/rsa.spec.js @@ -16,14 +16,11 @@ const fixtures = require('../fixtures/go-key-rsa') const testGarbage = require('../helpers/test-garbage-error-handling') -/** @typedef {import('libp2p-crypto').keys.supportedKeys.rsa.RsaPrivateKey} RsaPrivateKey */ +/** @typedef {import('../../src/keys/rsa-class').PrivateKey} RsaPrivateKey */ describe('RSA', function () { this.timeout(20 * 1000) - // @ts-check - /** - * @type {RsaPrivateKey} - */ + /** @type {RsaPrivateKey} */ let key before(async () => { @@ -37,7 +34,7 @@ describe('RSA', function () { }) it('signs', async () => { - const text = key.genSecret() + const text = rsa.genSecret() const sig = await key.sign(text) const res = await key.public.verify(text, sig) expect(res).to.be.eql(true) @@ -164,6 +161,7 @@ describe('RSA', function () { }) it('handles invalid export type', () => { + // @ts-expect-error return expectErrCode(key.export('secret', 'invalid-type'), 'ERR_INVALID_EXPORT_FORMAT') }) }) diff --git a/test/keys/secp256k1.spec.js b/test/keys/secp256k1.spec.js index 80dcf01b..d517c1a5 100644 --- a/test/keys/secp256k1.spec.js +++ b/test/keys/secp256k1.spec.js @@ -14,6 +14,7 @@ const uint8ArrayFromString = require('uint8arrays/from-string') const fixtures = require('../fixtures/go-key-secp256k1') describe('secp256k1 keys', () => { + /** @type {import('../../src/keys/secp256k1-class').PrivateKey} */ let key before(async () => { @@ -117,7 +118,9 @@ describe('secp256k1 keys', () => { }) describe('key generation error', () => { + /** @type {typeof secp256k1Crypto.generateKey} */ let generateKey + /** @type {typeof crypto.keys.supportedKeys.secp256k1} */ let secp256k1 before(() => { @@ -141,7 +144,9 @@ describe('key generation error', () => { }) describe('handles generation of invalid key', () => { + /** @type {typeof secp256k1Crypto.generateKey} */ let generateKey + /** @type {typeof crypto.keys.supportedKeys.secp256k1} */ let secp256k1 before(() => { @@ -165,7 +170,9 @@ describe('handles generation of invalid key', () => { }) describe('crypto functions', () => { + /** @type {Uint8Array} */ let privKey + /** @type {Uint8Array} */ let pubKey before(async () => { @@ -193,6 +200,7 @@ describe('crypto functions', () => { it('errors if given a null Uint8Array to sign', async () => { try { + // @ts-expect-error await secp256k1Crypto.hashAndSign(privKey, null) } catch (err) { return // expected @@ -213,6 +221,7 @@ describe('crypto functions', () => { const sig = await secp256k1Crypto.hashAndSign(privKey, uint8ArrayFromString('hello')) try { + // @ts-expect-error await secp256k1Crypto.hashAndVerify(privKey, sig, null) } catch (err) { return // expected diff --git a/test/random-bytes.spec.js b/test/random-bytes.spec.js index 0eb95171..6de55837 100644 --- a/test/random-bytes.spec.js +++ b/test/random-bytes.spec.js @@ -22,6 +22,7 @@ describe('randomBytes', () => { }) it('throws if length is not a number', () => { + // @ts-expect-error - expects number expect(() => randomBytes('hi')).to.throw(Error).with.property('code', 'ERR_INVALID_LENGTH') }) }) diff --git a/test/util.spec.js b/test/util.spec.js index b2832d45..ed65f5f3 100644 --- a/test/util.spec.js +++ b/test/util.spec.js @@ -6,11 +6,16 @@ const chai = require('chai') const dirtyChai = require('dirty-chai') const expect = chai.expect chai.use(dirtyChai) +// @ts-ignore require('node-forge/lib/jsbn') +/** @type {import('node-forge')} */ +// @ts-ignore const forge = require('node-forge/lib/forge') + const util = require('../src/util') describe('Util', () => { + /** @type {import('node-forge').jsbn.BigInteger} */ let bn before(() => { diff --git a/test/util/index.js b/test/util/index.js index 209a54c3..ea244680 100644 --- a/test/util/index.js +++ b/test/util/index.js @@ -18,4 +18,12 @@ const expectErrCode = async (p, code) => { expect.fail(`Expected error with code ${code} but no error thrown`) } -module.exports = { expectErrCode } +/** + * @template {string|number|null|boolean} T + * @param {T} value + * @returns {T} + */ + +const the = value => value + +module.exports = { expectErrCode, the } diff --git a/tsconfig.json b/tsconfig.json index 5eb556d0..6ab0f52d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,30 +1,29 @@ { - "compilerOptions": { - "module": "commonjs", - "lib": [ - "es6" - ], - "target": "ES5", - "noImplicitAny": false, - "noImplicitThis": true, - "strictFunctionTypes": true, - "strictNullChecks": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "allowJs": true, - "checkJs": true, - "baseUrl": ".", - "paths": { - "libp2p-crypto": [ - "./src", - "../../src", - "../src" - ] - }, - "types": ["node", "mocha", "chai"], - "noEmit": true, - "forceConsistentCasingInFileNames": true - }, - "files": ["./src/index.d.ts",], - "include": ["./test/**/*.spec.js"] + "extends": "./node_modules/aegir/src/config/tsconfig.aegir.json", + "compilerOptions": { + "outDir": "dist", + "module": "commonjs", + "lib": [ + "es6", + "DOM" + ], + "resolveJsonModule": true, + "target": "ES5", + "baseUrl": ".", + "paths": { + "libp2p-crypto": [ + "./src", + "../../src", + "../src" + ] + } + }, + "exclude": [ + "./src/keys/keys.js" + ], + "include": [ + "test", + "src", + "test/fixtures/*.json" + ] }