diff --git a/helpers/ed.js b/helpers/ed.js index 3f68c7c1..9a9c97cd 100644 --- a/helpers/ed.js +++ b/helpers/ed.js @@ -1,6 +1,6 @@ 'use strict'; -var sodium = require('sodium').api; +var sodium = require('sodium-native'); var mnemonic = require('bitcore-mnemonic'); @@ -51,11 +51,13 @@ ed.createPassPhraseHash = function (passPhrase) { * @return {Object} publicKey, privateKey */ ed.makeKeypair = function (hash) { - var keypair = sodium.crypto_sign_seed_keypair(hash); + const publicKey = Buffer.alloc(sodium.crypto_sign_PUBLICKEYBYTES); + const privateKey = Buffer.alloc(sodium.crypto_sign_SECRETKEYBYTES); + sodium.crypto_sign_seed_keypair(publicKey, privateKey, hash); return { - publicKey: keypair.publicKey, - privateKey: keypair.secretKey + publicKey, + privateKey }; }; @@ -67,7 +69,9 @@ ed.makeKeypair = function (hash) { * @return {signature} signature */ ed.sign = function (hash, keypair) { - return sodium.crypto_sign_detached(hash, Buffer.from(keypair.privateKey, 'hex')); + const signature = Buffer.alloc(sodium.crypto_sign_BYTES); + sodium.crypto_sign_detached(signature, hash, keypair.privateKey); + return signature; }; /** @@ -78,7 +82,7 @@ ed.sign = function (hash, keypair) { * @return {Boolean} true id verified */ ed.verify = function (hash, signatureBuffer, publicKeyBuffer) { - return sodium.crypto_sign_verify_detached(signatureBuffer, hash, publicKeyBuffer); + return sodium.crypto_sign_verify_detached(signatureBuffer, hash, publicKeyBuffer) }; module.exports = ed; diff --git a/package-lock.json b/package-lock.json index 995f4700..8d948bb2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,8 +43,8 @@ "semver": "=7.7.2", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", - "sodium": "^3.0.2", "sodium-browserify-tweetnacl": "*", + "sodium-native": "^5.0.9", "strftime": "=0.10.3", "unzipper": "^0.12.3", "valid-url": "=1.0.9", @@ -671,6 +671,7 @@ "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.6.0.tgz", "integrity": "sha512-wmP9kCFElCSr4MM4+1E4VckDuN4wLtiXSM/J0rKVQppajxQhowci89RGZr2OdLualowb8SRJ/R6OjsXrn9ZNFA==", "license": "MIT", + "peer": true, "dependencies": { "cluster-key-slot": "1.1.2" }, @@ -807,6 +808,7 @@ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", "dev": true, + "peer": true, "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" @@ -876,6 +878,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1132,6 +1135,75 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-addon-resolve": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/bare-addon-resolve/-/bare-addon-resolve-1.9.5.tgz", + "integrity": "sha512-XdqrG73zLK9LDfblOJwoAxmJ+7YdfRW4ex46+f4L+wPhk7H7LDrRMAbBw8s8jkxeEFpUenyB7QHnv0ErAWd3Yg==", + "license": "Apache-2.0", + "dependencies": { + "bare-module-resolve": "^1.10.0", + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-module-resolve": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/bare-module-resolve/-/bare-module-resolve-1.11.2.tgz", + "integrity": "sha512-HIBu9WacMejg3Dz4X1v6lJjp7ECnwpujvuLub+8I7JJLRwJaGxWMzGYvieOoS9R1n5iRByvTmLtIdPbwjfRgiQ==", + "license": "Apache-2.0", + "dependencies": { + "bare-semver": "^1.0.0" + }, + "peerDependencies": { + "bare-url": "*" + }, + "peerDependenciesMeta": { + "bare-url": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", + "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-semver": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bare-semver/-/bare-semver-1.0.2.tgz", + "integrity": "sha512-ESVaN2nzWhcI5tf3Zzcq9aqCZ676VWzqw07eEZ0qxAcEOAFYBa0pWq8sK34OQeHLY3JsfKXZS9mDyzyxGjeLzA==", + "license": "Apache-2.0" + }, + "node_modules/bare-url": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.1.tgz", + "integrity": "sha512-v2yl0TnaZTdEnelkKtXZGnotiV6qATBlnNuUMrHl6v9Lmmrh9mw9RYyImPU7/4RahumSwQS1k2oKXcRfXcbjJw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base-x": { "version": "3.0.11", "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.11.tgz", @@ -2241,6 +2313,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -3352,6 +3425,7 @@ "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", "dev": true, + "peer": true, "dependencies": { "dateformat": "~4.6.2", "eventemitter2": "~0.4.13", @@ -4844,6 +4918,7 @@ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "dev": true, + "peer": true, "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", @@ -5304,11 +5379,6 @@ "resolved": "https://registry.npmjs.org/neoip/-/neoip-3.0.1.tgz", "integrity": "sha512-yvMLOFvS7Tzthf9Ukl2/HrVzZqSjxm9PVOdAPLCt7pelDQ5WvJMIur1vNn3VXOL2tqrbzv6Vnal4PyoCrtRZPA==" }, - "node_modules/node-addon-api": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", - "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==" - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -7606,6 +7676,7 @@ "version": "4.0.2", "inBundle": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -8201,6 +8272,7 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", + "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -8240,7 +8312,6 @@ "resolved": "https://registry.npmjs.org/pg-cursor/-/pg-cursor-2.15.3.tgz", "integrity": "sha512-eHw63TsiGtFEfAd7tOTZ+TLy+i/2ePKS20H84qCQ+aQ60pve05Okon9tKMC+YN3j6XyeFoHnaim7Lt9WVafQsA==", "license": "MIT", - "peer": true, "peerDependencies": { "pg": "^8" } @@ -8280,6 +8351,7 @@ "resolved": "https://registry.npmjs.org/pg-native/-/pg-native-3.5.2.tgz", "integrity": "sha512-3oi+KVil86Vngo4H0IlhBaYSJWdcu8t2f1Y4TkQoQi5oZ9bNeYECGqW3oSGx69mjSZYHoC3h+3jYtqzRgndn5A==", "license": "MIT", + "peer": true, "dependencies": { "libpq": "^1.8.15", "pg-types": "2.2.0" @@ -8673,6 +8745,19 @@ "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", "dev": true }, + "node_modules/require-addon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/require-addon/-/require-addon-1.1.0.tgz", + "integrity": "sha512-KbXAD5q2+v1GJnkzd8zzbOxchTkStSyJZ9QwoCq3QwEXAaIlG3wDYRZGzVD357jmwaGY7hr5VaoEAL0BkF0Kvg==", + "license": "Apache-2.0", + "dependencies": { + "bare-addon-resolve": "^1.3.0", + "bare-url": "^2.1.0" + }, + "engines": { + "bare": ">=1.10.0" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -9295,15 +9380,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, - "node_modules/sodium": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/sodium/-/sodium-3.0.2.tgz", - "integrity": "sha512-IsTwTJeoNBU97km3XkrbCGC/n/9aUQejgD3QPr2YY2gtbSPru3TI6nhCqgoez9Mv88frF9oVZS/jrXFbd6WXyA==", - "hasInstallScript": true, - "dependencies": { - "node-addon-api": "*" - } - }, "node_modules/sodium-browserify-tweetnacl": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/sodium-browserify-tweetnacl/-/sodium-browserify-tweetnacl-0.2.6.tgz", @@ -9316,6 +9392,19 @@ "tweetnacl-auth": "^0.3.0" } }, + "node_modules/sodium-native": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-5.0.9.tgz", + "integrity": "sha512-6fpu3d6zdrRpLhuV3CDIBO5g90KkgaeR+c3xvDDz0ZnDkAlqbbPhFW7zhMJfsskfZ9SuC3SvBbqvxcECkXRyKw==", + "license": "MIT", + "dependencies": { + "require-addon": "^1.1.0", + "which-runtime": "^1.2.1" + }, + "engines": { + "bare": ">=1.16.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -9945,6 +10034,12 @@ "node": ">= 8" } }, + "node_modules/which-runtime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/which-runtime/-/which-runtime-1.3.2.tgz", + "integrity": "sha512-5kwCfWml7+b2NO7KrLMhYihjRx0teKkd3yGp1Xk5Vaf2JGdSh+rgVhEALAD9c/59dP+YwJHXoEO7e8QPy7gOkw==", + "license": "Apache-2.0" + }, "node_modules/which-typed-array": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", diff --git a/package.json b/package.json index c136d12d..add82a43 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,8 @@ "semver": "=7.7.2", "socket.io": "^4.8.1", "socket.io-client": "^4.8.1", - "sodium": "^3.0.2", "sodium-browserify-tweetnacl": "*", + "sodium-native": "=5.0.9", "strftime": "=0.10.3", "unzipper": "^0.12.3", "valid-url": "=1.0.9", diff --git a/test/unit/helpers/ed.js b/test/unit/helpers/ed.js new file mode 100644 index 00000000..f9e26cc4 --- /dev/null +++ b/test/unit/helpers/ed.js @@ -0,0 +1,101 @@ +'use strict'; + +const { expect } = require('chai'); +const crypto = require('crypto'); +const ed = require('../../../helpers/ed.js'); + +describe('ed', () => { + let passphrase; + let hash; + let keypair; + let message; + + beforeEach(() => { + passphrase = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + hash = ed.createPassPhraseHash(passphrase); + keypair = ed.makeKeypair(hash); + message = Buffer.from('adamant is good'); + }); + + describe('isValidPassphrase()', () => { + it('should return true for a valid BIP39 mnemonic', () => { + expect(ed.isValidPassphrase(passphrase)).to.be.true; + }); + + it('should return false for an invalid mnemonic', () => { + expect(ed.isValidPassphrase('not a real mnemonic phrase')).to.be.false; + }); + }); + + describe('generatePassphrase()', () => { + it('should generate a valid mnemonic', () => { + const generated = ed.generatePassphrase(); + expect(ed.isValidPassphrase(generated)).to.be.true; + }); + + it('should generate a different mnemonic each time', () => { + const a = ed.generatePassphrase(); + const b = ed.generatePassphrase(); + expect(a).to.not.equal(b); + }); + }); + + describe('createPassPhraseHash()', () => { + it('should create a SHA256 hash buffer', () => { + const h = ed.createPassPhraseHash(passphrase); + expect(Buffer.isBuffer(h)).to.be.true; + expect(h.length).to.equal(32); + }); + + it('should produce deterministic output for same input', () => { + const h1 = ed.createPassPhraseHash(passphrase); + const h2 = ed.createPassPhraseHash(passphrase); + expect(h1.equals(h2)).to.be.true; + }); + }); + + describe('makeKeypair()', () => { + it('should produce valid public and private key buffers', () => { + expect(Buffer.isBuffer(keypair.publicKey)).to.be.true; + expect(Buffer.isBuffer(keypair.privateKey)).to.be.true; + expect(keypair.publicKey.length).to.equal(32); + expect(keypair.privateKey.length).to.equal(64); + }); + + it('should produce deterministic keypair for same seed', () => { + const second = ed.makeKeypair(hash); + expect(second.publicKey.equals(keypair.publicKey)).to.be.true; + expect(second.privateKey.equals(keypair.privateKey)).to.be.true; + }); + }); + + describe('sign()', () => { + it('should return a valid signature buffer', () => { + const sig = ed.sign(message, keypair); + expect(Buffer.isBuffer(sig)).to.be.true; + expect(sig.length).to.equal(64); + }); + }); + + describe('verify()', () => { + it('should verify a valid signature', () => { + const sig = ed.sign(message, keypair); + const verified = ed.verify(message, sig, keypair.publicKey); + expect(verified).to.be.true; + }); + + it('should fail verification for modified message', () => { + const sig = ed.sign(message, keypair); + const tampered = Buffer.from('adamant is bad'); + const verified = ed.verify(tampered, sig, keypair.publicKey); + expect(verified).to.be.false; + }); + + it('should fail verification with wrong public key', () => { + const sig = ed.sign(message, keypair); + const other = ed.makeKeypair(crypto.randomBytes(32)); + const verified = ed.verify(message, sig, other.publicKey); + expect(verified).to.be.false; + }); + }); +});