diff --git a/benchmark/ed25519/index.cjs b/benchmark/ed25519/index.js similarity index 82% rename from benchmark/ed25519/index.cjs rename to benchmark/ed25519/index.js index 12876016..c0f554e8 100644 --- a/benchmark/ed25519/index.cjs +++ b/benchmark/ed25519/index.js @@ -1,18 +1,34 @@ /* eslint-disable no-console */ // @ts-expect-error types are missing -const forge = require('node-forge/lib/forge') -const Benchmark = require('benchmark') -const native = require('ed25519') -const noble = require('@noble/ed25519') +import forge from 'node-forge/lib/forge.js' +import Benchmark from 'benchmark' +import native from 'ed25519' +import * as noble from '@noble/ed25519' +import 'node-forge/lib/ed25519.js' +import stable from '@stablelib/ed25519' +import supercopWasm from 'supercop.wasm' +import ed25519WasmPro from 'ed25519-wasm-pro' +import * as libp2pCrypto from '../../dist/src/index.js' + const { randomBytes } = noble.utils -const { subtle } = require('crypto').webcrypto -require('node-forge/lib/ed25519') -const stable = require('@stablelib/ed25519') -const supercopWasm = require('supercop.wasm') -const ed25519WasmPro = require('ed25519-wasm-pro') const suite = new Benchmark.Suite('ed25519 implementations') +suite.add('@libp2p/crypto', async (d) => { + const message = Buffer.from('hello world ' + Math.random()) + + const key = await libp2pCrypto.keys.generateKeyPair('Ed25519') + + const signature = await key.sign(message) + const res = await key.public.verify(message, signature) + + if (!res) { + throw new Error('could not verify @libp2p/crypto signature') + } + + d.resolve() +}, { defer: true }) + suite.add('@noble/ed25519', async (d) => { const message = Buffer.from('hello world ' + Math.random()) const privateKey = noble.utils.randomPrivateKey() @@ -96,23 +112,6 @@ suite.add('ed25519 (native module)', async (d) => { d.resolve() }, { defer: true }) -suite.add('node.js web-crypto', async (d) => { - const message = Buffer.from('hello world ' + Math.random()) - - const key = await subtle.generateKey({ - name: 'NODE-ED25519', - namedCurve: 'NODE-ED25519' - }, true, ['sign', 'verify']) - const signature = await subtle.sign('NODE-ED25519', key.privateKey, message) - const res = await subtle.verify('NODE-ED25519', key.publicKey, signature, message) - - if (!res) { - throw new Error('could not verify node.js signature') - } - - d.resolve() -}, { defer: true }) - async function main () { await Promise.all([ new Promise((resolve) => { diff --git a/benchmark/ed25519/package.json b/benchmark/ed25519/package.json index 869564df..530a3d9c 100644 --- a/benchmark/ed25519/package.json +++ b/benchmark/ed25519/package.json @@ -2,6 +2,7 @@ "name": "libp2p-crypto-ed25519-benchmarks", "version": "0.0.0", "private": true, + "type": "module", "scripts": { "start": "node .", "compat": "node compat.js" diff --git a/package.json b/package.json index b4e1b019..37b51cc1 100644 --- a/package.json +++ b/package.json @@ -201,6 +201,7 @@ "./dist/src/ciphers/aes-gcm.js": "./dist/src/ciphers/aes-gcm.browser.js", "./dist/src/hmac/index.js": "./dist/src/hmac/index-browser.js", "./dist/src/keys/ecdh.js": "./dist/src/keys/ecdh-browser.js", + "./dist/src/keys/ed25519.js": "./dist/src/keys/ed25519-browser.js", "./dist/src/keys/rsa.js": "./dist/src/keys/rsa-browser.js" } } diff --git a/src/ciphers/aes-gcm.browser.ts b/src/ciphers/aes-gcm.browser.ts index 570c563b..658fdf16 100644 --- a/src/ciphers/aes-gcm.browser.ts +++ b/src/ciphers/aes-gcm.browser.ts @@ -46,9 +46,9 @@ export function create (opts?: CreateOptions) { * the encryption cipher. */ async function decrypt (data: Uint8Array, password: string | Uint8Array) { - const salt = data.slice(0, saltLength) - const nonce = data.slice(saltLength, saltLength + nonceLength) - const ciphertext = data.slice(saltLength + nonceLength) + const salt = data.subarray(0, saltLength) + const nonce = data.subarray(saltLength, saltLength + nonceLength) + const ciphertext = data.subarray(saltLength + nonceLength) const aesGcm = { name: algorithm, iv: nonce } if (typeof password === 'string') { diff --git a/src/ciphers/aes-gcm.ts b/src/ciphers/aes-gcm.ts index ce0fd9a5..ea408976 100644 --- a/src/ciphers/aes-gcm.ts +++ b/src/ciphers/aes-gcm.ts @@ -55,9 +55,9 @@ export function create (opts?: CreateOptions) { */ async function decryptWithKey (ciphertextAndNonce: Uint8Array, key: Uint8Array) { // eslint-disable-line require-await // Create Uint8Arrays of nonce, ciphertext and tag. - const nonce = ciphertextAndNonce.slice(0, nonceLength) - const ciphertext = ciphertextAndNonce.slice(nonceLength, ciphertextAndNonce.length - algorithmTagLength) - const tag = ciphertextAndNonce.slice(ciphertext.length + nonceLength) + const nonce = ciphertextAndNonce.subarray(0, nonceLength) + const ciphertext = ciphertextAndNonce.subarray(nonceLength, ciphertextAndNonce.length - algorithmTagLength) + const tag = ciphertextAndNonce.subarray(ciphertext.length + nonceLength) // Create the cipher instance. const cipher = crypto.createDecipheriv(algorithm, key, nonce) @@ -79,8 +79,8 @@ export function create (opts?: CreateOptions) { */ async function decrypt (data: Uint8Array, password: string | Uint8Array) { // eslint-disable-line require-await // Create Uint8Arrays of salt and ciphertextAndNonce. - const salt = data.slice(0, saltLength) - const ciphertextAndNonce = data.slice(saltLength) + const salt = data.subarray(0, saltLength) + const ciphertextAndNonce = data.subarray(saltLength) if (typeof password === 'string') { password = uint8ArrayFromString(password) diff --git a/src/keys/ecdh-browser.ts b/src/keys/ecdh-browser.ts index 7ec63fa2..e5fad8af 100644 --- a/src/keys/ecdh-browser.ts +++ b/src/keys/ecdh-browser.ts @@ -118,15 +118,15 @@ function unmarshalPublicKey (curve: string, key: Uint8Array) { const byteLen = curveLengths[curve] - if (!uint8ArrayEquals(key.slice(0, 1), Uint8Array.from([4]))) { + if (!uint8ArrayEquals(key.subarray(0, 1), Uint8Array.from([4]))) { throw errcode(new Error('Cannot unmarshal public key - invalid key format'), 'ERR_INVALID_KEY_FORMAT') } return { kty: 'EC', crv: curve, - x: uint8ArrayToString(key.slice(1, byteLen + 1), 'base64url'), - y: uint8ArrayToString(key.slice(1 + byteLen), 'base64url'), + x: uint8ArrayToString(key.subarray(1, byteLen + 1), 'base64url'), + y: uint8ArrayToString(key.subarray(1 + byteLen), 'base64url'), ext: true } } diff --git a/src/keys/ed25519-browser.ts b/src/keys/ed25519-browser.ts new file mode 100644 index 00000000..533421fd --- /dev/null +++ b/src/keys/ed25519-browser.ts @@ -0,0 +1,63 @@ +import * as ed from '@noble/ed25519' + +const PUBLIC_KEY_BYTE_LENGTH = 32 +const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys +const KEYS_BYTE_LENGTH = 32 + +export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength } +export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength } + +export async function generateKey () { + // the actual private key (32 bytes) + const privateKeyRaw = ed.utils.randomPrivateKey() + const publicKey = await ed.getPublicKey(privateKeyRaw) + + // concatenated the public key to the private key + const privateKey = concatKeys(privateKeyRaw, publicKey) + + return { + privateKey, + publicKey + } +} + +/** + * Generate keypair from a 32 byte uint8array + */ +export async function generateKeyFromSeed (seed: Uint8Array) { + if (seed.length !== KEYS_BYTE_LENGTH) { + throw new TypeError('"seed" must be 32 bytes in length.') + } else if (!(seed instanceof Uint8Array)) { + throw new TypeError('"seed" must be a node.js Buffer, or Uint8Array.') + } + + // based on node forges algorithm, the seed is used directly as private key + const privateKeyRaw = seed + const publicKey = await ed.getPublicKey(privateKeyRaw) + + const privateKey = concatKeys(privateKeyRaw, publicKey) + + return { + privateKey, + publicKey + } +} + +export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array) { + const privateKeyRaw = privateKey.subarray(0, KEYS_BYTE_LENGTH) + + return await ed.sign(msg, privateKeyRaw) +} + +export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array) { + return await ed.verify(sig, msg, publicKey) +} + +function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array) { + const privateKey = new Uint8Array(PRIVATE_KEY_BYTE_LENGTH) + for (let i = 0; i < KEYS_BYTE_LENGTH; i++) { + privateKey[i] = privateKeyRaw[i] + privateKey[KEYS_BYTE_LENGTH + i] = publicKey[i] + } + return privateKey +} diff --git a/src/keys/ed25519-class.ts b/src/keys/ed25519-class.ts index 99b7ab24..db8dddee 100644 --- a/src/keys/ed25519-class.ts +++ b/src/keys/ed25519-class.ts @@ -110,14 +110,14 @@ export function unmarshalEd25519PrivateKey (bytes: Uint8Array) { // Try the old, redundant public key version if (bytes.length > crypto.privateKeyLength) { bytes = ensureKey(bytes, crypto.privateKeyLength + crypto.publicKeyLength) - const privateKeyBytes = bytes.slice(0, crypto.privateKeyLength) - const publicKeyBytes = bytes.slice(crypto.privateKeyLength, bytes.length) + const privateKeyBytes = bytes.subarray(0, crypto.privateKeyLength) + const publicKeyBytes = bytes.subarray(crypto.privateKeyLength, bytes.length) return new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes) } bytes = ensureKey(bytes, crypto.privateKeyLength) - const privateKeyBytes = bytes.slice(0, crypto.privateKeyLength) - const publicKeyBytes = bytes.slice(crypto.publicKeyLength) + const privateKeyBytes = bytes.subarray(0, crypto.privateKeyLength) + const publicKeyBytes = bytes.subarray(crypto.publicKeyLength) return new Ed25519PrivateKey(privateKeyBytes, publicKeyBytes) } diff --git a/src/keys/ed25519.ts b/src/keys/ed25519.ts index 06bc9cad..5232b75a 100644 --- a/src/keys/ed25519.ts +++ b/src/keys/ed25519.ts @@ -1,23 +1,38 @@ -import * as ed from '@noble/ed25519' +import crypto from 'crypto' +import { promisify } from 'util' +import { toString as uint8arrayToString } from 'uint8arrays/to-string' +import { fromString as uint8arrayFromString } from 'uint8arrays/from-string' + +const keypair = promisify(crypto.generateKeyPair) const PUBLIC_KEY_BYTE_LENGTH = 32 const PRIVATE_KEY_BYTE_LENGTH = 64 // private key is actually 32 bytes but for historical reasons we concat private and public keys const KEYS_BYTE_LENGTH = 32 +const SIGNATURE_BYTE_LENGTH = 64 export { PUBLIC_KEY_BYTE_LENGTH as publicKeyLength } export { PRIVATE_KEY_BYTE_LENGTH as privateKeyLength } +function derivePublicKey (privateKey: Uint8Array) { + const hash = crypto.createHash('sha512') + hash.update(privateKey) + return hash.digest().subarray(32) +} + export async function generateKey () { - // the actual private key (32 bytes) - const privateKeyRaw = ed.utils.randomPrivateKey() - const publicKey = await ed.getPublicKey(privateKeyRaw) + const key = await keypair('ed25519', { + publicKeyEncoding: { type: 'spki', format: 'jwk' }, + privateKeyEncoding: { type: 'pkcs8', format: 'jwk' } + }) - // concatenated the public key to the private key - const privateKey = concatKeys(privateKeyRaw, publicKey) + // @ts-expect-error node types are missing jwk as a format + const privateKeyRaw = uint8arrayFromString(key.privateKey.d, 'base64url') + // @ts-expect-error node types are missing jwk as a format + const publicKeyRaw = uint8arrayFromString(key.privateKey.x, 'base64url') return { - privateKey, - publicKey + privateKey: concatKeys(privateKeyRaw, publicKeyRaw), + publicKey: publicKeyRaw } } @@ -32,25 +47,68 @@ export async function generateKeyFromSeed (seed: Uint8Array) { } // based on node forges algorithm, the seed is used directly as private key - const privateKeyRaw = seed - const publicKey = await ed.getPublicKey(privateKeyRaw) - - const privateKey = concatKeys(privateKeyRaw, publicKey) + const publicKeyRaw = derivePublicKey(seed) return { - privateKey, - publicKey + privateKey: concatKeys(seed, publicKeyRaw), + publicKey: publicKeyRaw } } -export async function hashAndSign (privateKey: Uint8Array, msg: Uint8Array) { - const privateKeyRaw = privateKey.slice(0, KEYS_BYTE_LENGTH) +export async function hashAndSign (key: Uint8Array, msg: Uint8Array) { + if (!(key instanceof Uint8Array)) { + throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.') + } + + let privateKey: Uint8Array + let publicKey: Uint8Array + + if (key.byteLength === PRIVATE_KEY_BYTE_LENGTH) { + privateKey = key.subarray(0, 32) + publicKey = key.subarray(32) + } else if (key.byteLength === KEYS_BYTE_LENGTH) { + privateKey = key.subarray(0, 32) + publicKey = derivePublicKey(privateKey) + } else { + throw new TypeError('"key" must be 64 or 32 bytes in length.') + } + + const obj = crypto.createPrivateKey({ + format: 'jwk', + key: { + crv: 'Ed25519', + d: uint8arrayToString(privateKey, 'base64url'), + x: uint8arrayToString(publicKey, 'base64url'), + kty: 'OKP' + } + }) - return await ed.sign(msg, privateKeyRaw) + return crypto.sign(null, msg, obj) } -export async function hashAndVerify (publicKey: Uint8Array, sig: Uint8Array, msg: Uint8Array) { - return await ed.verify(sig, msg, publicKey) +export async function hashAndVerify (key: Uint8Array, sig: Uint8Array, msg: Uint8Array) { + if (key.byteLength !== PUBLIC_KEY_BYTE_LENGTH) { + throw new TypeError('"key" must be 32 bytes in length.') + } else if (!(key instanceof Uint8Array)) { + throw new TypeError('"key" must be a node.js Buffer, or Uint8Array.') + } + + if (sig.byteLength !== SIGNATURE_BYTE_LENGTH) { + throw new TypeError('"sig" must be 64 bytes in length.') + } else if (!(sig instanceof Uint8Array)) { + throw new TypeError('"sig" must be a node.js Buffer, or Uint8Array.') + } + + const obj = crypto.createPublicKey({ + format: 'jwk', + key: { + crv: 'Ed25519', + x: uint8arrayToString(key, 'base64url'), + kty: 'OKP' + } + }) + + return crypto.verify(null, msg, obj, sig) } function concatKeys (privateKeyRaw: Uint8Array, publicKey: Uint8Array) { diff --git a/src/keys/key-stretcher.ts b/src/keys/key-stretcher.ts index 2420435d..7dfd98a8 100644 --- a/src/keys/key-stretcher.ts +++ b/src/keys/key-stretcher.ts @@ -61,13 +61,13 @@ export async function keyStretcher (cipherType: 'AES-128' | 'AES-256' | 'Blowfis const half = resultLength / 2 const resultBuffer = uint8ArrayConcat(result) - const r1 = resultBuffer.slice(0, half) - const r2 = resultBuffer.slice(half, resultLength) + const r1 = resultBuffer.subarray(0, half) + const r2 = resultBuffer.subarray(half, resultLength) const createKey = (res: Uint8Array) => ({ - iv: res.slice(0, ivSize), - cipherKey: res.slice(ivSize, ivSize + cipherKeySize), - macKey: res.slice(ivSize + cipherKeySize) + iv: res.subarray(0, ivSize), + cipherKey: res.subarray(ivSize, ivSize + cipherKeySize), + macKey: res.subarray(ivSize + cipherKeySize) }) return { diff --git a/src/util.ts b/src/util.ts index a7323d50..9d4a4fe9 100644 --- a/src/util.ts +++ b/src/util.ts @@ -14,7 +14,7 @@ export function bigIntegerToUintBase64url (num: { abs: () => any}, len?: number) // byte if the most significant bit of the number is 1: // https://docs.microsoft.com/en-us/windows/win32/seccertenroll/about-integer // Our number will always be positive so we should remove the leading padding. - buf = buf[0] === 0 ? buf.slice(1) : buf + buf = buf[0] === 0 ? buf.subarray(1) : buf if (len != null) { if (buf.length > len) throw new Error('byte array longer than desired length') diff --git a/test/keys/ed25519.spec.ts b/test/keys/ed25519.spec.ts index a007e395..7b9579a7 100644 --- a/test/keys/ed25519.spec.ts +++ b/test/keys/ed25519.spec.ts @@ -173,7 +173,7 @@ describe('ed25519', function () { const key = await crypto.keys.unmarshalPrivateKey(fixtures.redundantPubKey.privateKey) const bytes = key.marshal() expect(bytes.length).to.equal(64) - expect(bytes.slice(32)).to.eql(key.public.marshal()) + expect(bytes.subarray(32)).to.eql(key.public.marshal()) }) it('verifies with data from go with redundant public key', async () => {