Skip to content
This repository has been archived by the owner on Jul 21, 2023. It is now read-only.

feat: swap js implementation of Ed25519 for native module or wasm in the browser #215

Closed
wants to merge 9 commits into from
6 changes: 6 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install -g @mapbox/node-pre-gyp
- run: npm install
- run: npm run lint
- run: npm run build
Expand All @@ -33,6 +34,7 @@ jobs:
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node }}
- run: npm install -g @mapbox/node-pre-gyp
- run: npm install
- run: npx nyc --reporter=lcov aegir test -t node -- --bail
- uses: codecov/codecov-action@v1
Expand All @@ -41,26 +43,30 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install -g @mapbox/node-pre-gyp
- run: npm install
- run: npm run test -- -t browser -t webworker --bail
test-firefox:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install -g @mapbox/node-pre-gyp
- run: npm install
- run: npm run test -- -t browser -t webworker --bail -- --browsers FirefoxHeadless
test-electron-main:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install -g @mapbox/node-pre-gyp
- run: npm install
- run: npx xvfb-maybe aegir test -t electron-main --bail
test-electron-renderer:
needs: check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: npm install -g @mapbox/node-pre-gyp
- run: npm install
- run: npx xvfb-maybe aegir test -t electron-renderer --bail
27 changes: 27 additions & 0 deletions benchmarks/ed25519/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ require('node-forge/lib/ed25519')
const forge = require('node-forge/lib/forge')
const stable = require('@stablelib/ed25519')
const supercopWasm = require('supercop.wasm')
const ed25519WasmPro = require('ed25519-wasm-pro')

const ALGORITHM = 'NODE-ED25519'
const ED25519_PKCS8_PREFIX = fromString('302e020100300506032b657004220420', 'hex')
Expand Down Expand Up @@ -97,6 +98,32 @@ const implementations = [{
verify: (message, signature, keyPair) => {
return supercopWasm.verify(signature, message, keyPair.publicKey)
}
}, {
name: 'ed25519-wasm-pro',
before: () => {
return new Promise(resolve => {
ed25519WasmPro.ready(() => {
resolve()
})
})
},
generateKeyPair: async () => {
const seed = ed25519WasmPro.createSeed()
const key = ed25519WasmPro.createKeyPair(seed)

return {
privateKey: seed,
publicKey: key.publicKey
}
},
sign: (message, keyPair) => {
const key = ed25519WasmPro.createKeyPair(keyPair.privateKey)

return ed25519WasmPro.sign(message, key.publicKey, key.secretKey)
},
verify: (message, signature, keyPair) => {
return ed25519WasmPro.verify(signature, message, keyPair.publicKey)
}
}, {
name: 'native Ed25519',
generateKeyPair: async () => {
Expand Down
40 changes: 31 additions & 9 deletions benchmarks/ed25519/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require('node-forge/lib/ed25519')
const forge = require('node-forge/lib/forge')
const stable = require('@stablelib/ed25519')
const supercopWasm = require('supercop.wasm')
const ed25519WasmPro = require('ed25519-wasm-pro')

const suite = new Benchmark.Suite('ed25519 implementations')

Expand Down Expand Up @@ -62,7 +63,21 @@ suite.add('supercop.wasm', async (d) => {
const isSigned = await supercopWasm.verify(signature, message, keys.publicKey)

if (!isSigned) {
throw new Error('could not verify noble signature')
throw new Error('could not verify supercop.wasm signature')
}

d.resolve()
}, { defer: true })

suite.add('ed25519-wasm-pro', async (d) => {
const message = Buffer.from('hello world ' + Math.random())
const seed = ed25519WasmPro.createSeed()
const keys = ed25519WasmPro.createKeyPair(seed)
const signature = ed25519WasmPro.sign(message, keys.publicKey, keys.secretKey)
const isSigned = await ed25519WasmPro.verify(signature, message, keys.publicKey)

if (!isSigned) {
throw new Error('could not verify ed25519-wasm-pro signature')
}

d.resolve()
Expand Down Expand Up @@ -100,14 +115,21 @@ suite.add('node.js web-crypto', async (d) => {
}, { defer: true })

async function main () {
supercopWasm.ready(() => {
suite
.on('cycle', (event) => console.log(String(event.target)))
.on('complete', function () {
console.log('fastest is ' + this.filter('fastest').map('name'))
})
.run({ async: true })
})
await Promise.all([
new Promise((resolve) => {
supercopWasm.ready(() => resolve())
}),
new Promise((resolve) => {
ed25519WasmPro.ready(() => resolve())
})
])

suite
.on('cycle', (event) => console.log(String(event.target)))
.on('complete', function () {
console.log('fastest is ' + this.filter('fastest').map('name'))
})
.run({ async: true })
}

main()
Expand Down
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
"./src/ciphers/aes-gcm.js": "./src/ciphers/aes-gcm.browser.js",
"./src/hmac/index.js": "./src/hmac/index-browser.js",
"./src/keys/ecdh.js": "./src/keys/ecdh-browser.js",
"./src/keys/rsa.js": "./src/keys/rsa-browser.js"
"./src/keys/rsa.js": "./src/keys/rsa-browser.js",
"./src/keys/ed25519/ed25519-native.js": "./src/keys/ed25519/ed25519-wasm.js",
"crypto": false,
"fs": false,
"path": false
},
"files": [
"src",
Expand Down Expand Up @@ -40,8 +44,8 @@
],
"license": "MIT",
"dependencies": {
"@noble/ed25519": "^1.3.0",
"@noble/secp256k1": "^1.3.0",
"ed25519-wasm-pro": "1.1.1",
"err-code": "^3.0.1",
"iso-random-stream": "^2.0.0",
"keypair": "^1.0.4",
Expand All @@ -52,6 +56,9 @@
"uint8arrays": "^3.0.0",
"ursa-optional": "^0.10.1"
},
"optionalDependencies": {
"ed25519": "^0.0.5"
},
"devDependencies": {
"@types/mocha": "^9.0.0",
"aegir": "^36.0.2",
Expand All @@ -61,7 +68,7 @@
},
"aegir": {
"build": {
"bundlesizeMax": "71kB"
"bundlesizeMax": "121kB"
}
},
"engines": {
Expand Down
9 changes: 5 additions & 4 deletions src/keys/ed25519-class.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const errcode = require('err-code')
const { equals: uint8ArrayEquals } = require('uint8arrays/equals')
const { concat: uint8ArrayConcat } = require('uint8arrays/concat')
const { sha256 } = require('multiformats/hashes/sha2')
const { base58btc } = require('multiformats/bases/base58')
const { identity } = require('multiformats/hashes/identity')
Expand Down Expand Up @@ -41,7 +42,7 @@ class Ed25519PublicKey {
}

class Ed25519PrivateKey {
// key - 64 byte Uint8Array containing private key
// key - 32 byte Uint8Array containing private key
// publicKey - 32 byte Uint8Array containing public key
constructor (key, publicKey) {
this._key = ensureKey(key, crypto.privateKeyLength)
Expand All @@ -57,7 +58,7 @@ class Ed25519PrivateKey {
}

marshal () {
return this._key
return uint8ArrayConcat([this._key, this._publicKey], crypto.privateKeyLength + crypto.publicKeyLength)
}

get bytes () {
Expand Down Expand Up @@ -139,10 +140,10 @@ async function generateKeyPairFromSeed (seed) {

function ensureKey (key, length) {
key = Uint8Array.from(key || [])
if (key.length !== length) {
if (key.length < length) {
throw errcode(new Error(`Key must be a Uint8Array of length ${length}, got ${key.length}`), 'ERR_INVALID_KEY_TYPE')
}
return key
return key.subarray(0, length)
}

module.exports = {
Expand Down
68 changes: 0 additions & 68 deletions src/keys/ed25519.js

This file was deleted.

40 changes: 40 additions & 0 deletions src/keys/ed25519/ed25519-native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
'use strict'

const native = require('ed25519')

/**
* Generate keypair from a seed
*
* @param {Uint8Array} seed - seed should be a 32 byte uint8array
*/
async function generateKeyFromSeed (seed) {
const key = native.MakeKeypair(seed)

return {
privateKey: key.privateKey.subarray(0, 32),
publicKey: key.publicKey
}
}

/**
* @param {Uint8Array} privateKey
* @param {Uint8Array} message
*/
async function hashAndSign (privateKey, message) {
return native.Sign(message, privateKey)
}

/**
* @param {Uint8Array} publicKey
* @param {Uint8Array} signature
* @param {Uint8Array} message
*/
async function hashAndVerify (publicKey, signature, message) {
return native.Verify(message, signature, publicKey)
}

module.exports = {
generateKeyFromSeed,
hashAndSign,
hashAndVerify
}
54 changes: 54 additions & 0 deletions src/keys/ed25519/ed25519-wasm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict'

const ed25519 = require('ed25519-wasm-pro')

const ready = new Promise((resolve) => {
ed25519.ready(() => {
resolve()
})
})

/**
* Generate keypair from a seed
*
* @param {Uint8Array} seed - seed should be a 32 byte uint8array
*/
async function generateKeyFromSeed (seed) {
await ready

const key = ed25519.createKeyPair(seed)

return {
privateKey: seed,
publicKey: key.publicKey
}
}

/**
* @param {Uint8Array} privateKey
* @param {Uint8Array} message
*/
async function hashAndSign (privateKey, message) {
await ready

const key = ed25519.createKeyPair(privateKey)

return ed25519.sign(message, key.publicKey, key.secretKey)
}

/**
* @param {Uint8Array} publicKey
* @param {Uint8Array} signature
* @param {Uint8Array} message
*/
async function hashAndVerify (publicKey, signature, message) {
await ready

return ed25519.verify(signature, message, publicKey)
}

module.exports = {
generateKeyFromSeed,
hashAndSign,
hashAndVerify
}
Loading