From 92d8b11a4920d84862356b16947c15e526dcd367 Mon Sep 17 00:00:00 2001 From: Bushstar Date: Wed, 4 Dec 2024 12:57:19 +0000 Subject: [PATCH 01/12] Add optional encrypted JSON wallet for CLI --- package-lock.json | 328 +++++++++++++++++- packages/common/src/errors.js | 8 + packages/keymaster/package.json | 2 + .../keymaster/src/db-wallet-encrypted-json.js | 75 ++++ packages/keymaster/src/keymaster-lib.js | 67 +++- sample.env | 3 + scripts/keychain-cli.js | 9 +- 7 files changed, 480 insertions(+), 12 deletions(-) create mode 100644 packages/keymaster/src/db-wallet-encrypted-json.js diff --git a/package-lock.json b/package-lock.json index 498a6f60..bb9aa0a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2477,6 +2477,253 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@inquirer/checkbox": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.2.tgz", + "integrity": "sha512-+gznPl8ip8P8HYHYecDtUtdsh1t2jvb+sWCD72GAiZ9m45RqwrLmReDaqdC0umQfamtFXVRoMVJ2/qINKGm9Tg==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.2.tgz", + "integrity": "sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.0.tgz", + "integrity": "sha512-I+ETk2AL+yAVbvuKx5AJpQmoaWhpiTFOg/UJb7ZkMAK4blmtG8ATh5ct+T/8xNld0CZG/2UhtkdMwpgvld92XQ==", + "dependencies": { + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.1.0.tgz", + "integrity": "sha512-K1gGWsxEqO23tVdp5MT3H799OZ4ER1za7Dlc8F4um0W7lwSv0KGR/YyrUEyimj0g7dXZd8XknM/5QA2/Uy+TbA==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.2.tgz", + "integrity": "sha512-WdgCX1cUtinz+syKyZdJomovULYlKUWZbVYZzhf+ZeeYf4htAQ3jLymoNs3koIAKfZZl3HUBb819ClCBfyznaw==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", + "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.0.2.tgz", + "integrity": "sha512-yCLCraigU085EcdpIVEDgyfGv4vBiE4I+k1qRkc9C5dMjWF42ADMGy1RFU94+eZlz4YlkmFsiyHZy0W1wdhaNg==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.2.tgz", + "integrity": "sha512-MKQhYofdUNk7eqJtz52KvM1dH6R93OMrqHduXCvuefKrsiMjHiMwjc3NZw5Imm2nqY7gWd9xdhYrtcHMJQZUxA==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.2.tgz", + "integrity": "sha512-tQXGSu7IO07gsYlGy3VgXRVsbOWqFBMbqAUrJSc1PDTQQ5Qdm+QVwkP0OC0jnUZ62D19iPgXOMO+tnWG+HhjNQ==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.1.0.tgz", + "integrity": "sha512-5U/XiVRH2pp1X6gpNAjWOglMf38/Ys522ncEHIKT1voRUvSj/DQnR22OVxHnwu5S+rCFaUiPQ57JOtMFQayqYA==", + "dependencies": { + "@inquirer/checkbox": "^4.0.2", + "@inquirer/confirm": "^5.0.2", + "@inquirer/editor": "^4.1.0", + "@inquirer/expand": "^4.0.2", + "@inquirer/input": "^4.0.2", + "@inquirer/number": "^3.0.2", + "@inquirer/password": "^4.0.2", + "@inquirer/rawlist": "^4.0.2", + "@inquirer/search": "^3.0.2", + "@inquirer/select": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.2.tgz", + "integrity": "sha512-3XGcskMoVF8H0Dl1S5TSZ3rMPPBWXRcM0VeNVsS4ByWeWjSeb0lPqfnBg6N7T0608I1B2bSVnbi2cwCrmOD1Yw==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.2.tgz", + "integrity": "sha512-Zv4FC7w4dJ13BOJfKRQCICQfShinGjb1bCEIHxTSnjj2telu3+3RHwHubPG9HyD4aix5s+lyAMEK/wSFD75HLA==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.2.tgz", + "integrity": "sha512-uSWUzaSYAEj0hlzxa1mUB6VqrKaYx0QxGBLZzU4xWFxaSyGaXxsSE4OSOwdU24j0xl8OajgayqFXW0l2bkl2kg==", + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/figures": "^1.0.8", + "@inquirer/type": "^3.0.1", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.1.tgz", + "integrity": "sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -4684,7 +4931,6 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", - "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -4698,7 +4944,6 @@ }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", - "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -5718,6 +5963,11 @@ "node": ">=10" } }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, "node_modules/chownr": { "version": "1.1.4", "license": "ISC" @@ -5800,6 +6050,14 @@ "node": ">=6" } }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "engines": { + "node": ">= 12" + } + }, "node_modules/cliui": { "version": "8.0.1", "license": "ISC", @@ -7303,6 +7561,19 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", "peer": true }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, @@ -8036,6 +8307,17 @@ "ms": "^2.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "funding": [ @@ -10923,6 +11205,14 @@ "npm": ">=6.0.0" } }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/napi-build-utils": { "version": "1.0.2", "license": "MIT" @@ -11350,6 +11640,14 @@ "node": ">= 0.8.0" } }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/p-defer": { "version": "4.0.0", "license": "MIT", @@ -12409,8 +12707,7 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/sanitize-filename": { "version": "1.6.3", @@ -13333,6 +13630,17 @@ "node": ">= 4.5.0" } }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tmpl": { "version": "1.0.5", "license": "BSD-3-Clause" @@ -14022,6 +14330,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "packages/cipher": { "name": "@mdip/cipher", "version": "0.4.0", @@ -14083,6 +14402,7 @@ "version": "0.4.1", "license": "MIT", "dependencies": { + "@inquirer/prompts": "^7.1.0", "@mdip/common": "*", "axios": "^1.7.7" } diff --git a/packages/common/src/errors.js b/packages/common/src/errors.js index bf775d09..3c0d2770 100644 --- a/packages/common/src/errors.js +++ b/packages/common/src/errors.js @@ -47,6 +47,14 @@ export class UnknownIDError extends MDIPError { } } +export class PassphraseError extends MDIPError { + static type = 'Passphrase'; + + constructor(detail) { + super(PassphraseError.type, detail); + } +} + // For unit tests export class ExpectedExceptionError extends MDIPError { static type = 'Expected to throw an exception'; diff --git a/packages/keymaster/package.json b/packages/keymaster/package.json index d4ceb9a5..90acc898 100644 --- a/packages/keymaster/package.json +++ b/packages/keymaster/package.json @@ -7,6 +7,7 @@ "./lib": "./src/keymaster-lib.js", "./sdk": "./src/keymaster-sdk.js", "./db/json": "./src/db-wallet-json.js", + "./db/encrypted/json": "./src/db-wallet-encrypted-json.js", "./db/web": "./src/db-wallet-web.js" }, "scripts": { @@ -16,6 +17,7 @@ "author": "David McFadzean ", "license": "MIT", "dependencies": { + "@inquirer/prompts": "^7.1.0", "@mdip/common": "*", "axios": "^1.7.7" }, diff --git a/packages/keymaster/src/db-wallet-encrypted-json.js b/packages/keymaster/src/db-wallet-encrypted-json.js new file mode 100644 index 00000000..a956fda5 --- /dev/null +++ b/packages/keymaster/src/db-wallet-encrypted-json.js @@ -0,0 +1,75 @@ +import fs from 'fs'; +import crypto from 'crypto'; + +const dataFolder = 'data'; +const walletName = `${dataFolder}/wallet_encrypted.json`; + +const algorithm = 'aes-256-cbc'; // Algorithm +const keyLength = 32; // 256 bit AES-256 +const ivLength = 16; // 128-bit AES block size +const saltLength = 16; // 128-bit salt +const iterations = 200000; // PBKDF2 iterations +const digest = 'sha512'; // PBKDF2 hash function + +let passphrase; + +export function setPassphrase(pw) { + passphrase = pw; +} + +export function getPassphrase(pw) { + return passphrase; +} + +export function saveWallet(wallet, overwrite = false) { + if (fs.existsSync(walletName) && !overwrite) { + return false; + } + + if (!fs.existsSync(dataFolder)) { + fs.mkdirSync(dataFolder, { recursive: true }); + } + + const walletJson = JSON.stringify(wallet, null, 4); + const salt = crypto.randomBytes(saltLength); + const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); + const iv = crypto.randomBytes(ivLength); + const cipher = crypto.createCipheriv(algorithm, key, iv); + + let encrypted = cipher.update(walletJson, 'utf8', 'base64'); + encrypted += cipher.final('base64'); + + const encryptedData = { + salt: salt.toString('base64'), + iv: iv.toString('base64'), + data: encrypted + }; + + fs.writeFileSync(walletName, JSON.stringify(encryptedData, null, 4)); + return true; +} + +export function loadWallet() { + if (fs.existsSync(walletName)) { + const encryptedJson = fs.readFileSync(walletName, 'utf8'); + const encryptedData = JSON.parse(encryptedJson); + + const salt = Buffer.from(encryptedData.salt, 'base64'); + const iv = Buffer.from(encryptedData.iv, 'base64'); + const encrypted = encryptedData.data; + const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + + let decrypted; + try { + decrypted = decipher.update(encrypted, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + } catch (err) { + throw new Error('Incorrect passphrase.'); + } + + return JSON.parse(decrypted); + } + + return null; +} diff --git a/packages/keymaster/src/keymaster-lib.js b/packages/keymaster/src/keymaster-lib.js index 05f2f40e..592943f7 100644 --- a/packages/keymaster/src/keymaster-lib.js +++ b/packages/keymaster/src/keymaster-lib.js @@ -1,4 +1,11 @@ -import { InvalidDIDError, InvalidParameterError, KeymasterError, UnknownIDError } from '@mdip/common/errors'; +import { password } from '@inquirer/prompts'; +import { + InvalidDIDError, + InvalidParameterError, + KeymasterError, + UnknownIDError, + PassphraseError, +} from '@mdip/common/errors'; let gatekeeper = null; let db = null; @@ -19,16 +26,30 @@ export async function start(options = {}) { throw new InvalidParameterError('options.gatekeeper'); } - if (options.wallet) { - db = options.wallet; + if (options.encrypted) { + if (options.encrypted_wallet) { + db = options.encrypted_wallet; - if (!db.loadWallet) { + if (!db.loadWallet || !db.saveWallet || !db.setPassphrase || !db.getPassphrase) { + throw new InvalidParameterError('options.encrypted_wallet'); + } + + db.setPassphrase(options.passphrase); + } else { + throw new InvalidParameterError('options.encrypted_wallet'); + } + } else { + if (options.wallet) { + db = options.wallet; + + if (!db.loadWallet || !db.saveWallet) { + throw new InvalidParameterError('options.wallet'); + } + } + else { throw new InvalidParameterError('options.wallet'); } } - else { - throw new InvalidParameterError('options.wallet'); - } if (options.cipher) { cipher = options.cipher; @@ -49,7 +70,18 @@ export async function listRegistries() { return gatekeeper.listRegistries(); } +async function checkAndSetPassphrase() { + if (!db.getPassphrase()) { + const passphrase = await promptPassphrase(); + db.setPassphrase(passphrase); + } +} + export async function loadWallet() { + if (db.setPassphrase) { + await checkAndSetPassphrase(); + } + let wallet = await db.loadWallet(); if (!wallet) { @@ -60,6 +92,10 @@ export async function loadWallet() { } export async function saveWallet(wallet, overwrite = true) { + if (db.setPassphrase) { + await checkAndSetPassphrase(); + } + // TBD validate wallet before saving return db.saveWallet(wallet, overwrite); } @@ -1949,3 +1985,20 @@ export async function unpublishPoll(pollId) { return updateAsset(pollId, { poll }); } + +async function promptPassphrase() { + try { + return await password({ + message: 'Please provide a wallet passphrase:', + mask: '*', + validate(value) { + if (value === '') { + return 'Passphrase cannot be empty'; + } + return true; + }, + }); + } catch (error) { + throw new PassphraseError('Exiting as no passphrase set'); + } +} diff --git a/sample.env b/sample.env index c1dff6ca..93000ecb 100644 --- a/sample.env +++ b/sample.env @@ -11,6 +11,9 @@ KC_GATEKEEPER_REGISTRIES=hyperswarm,TBTC,TFTC KC_GATEKEEPER_PORT=4224 KC_GATEKEEPER_GC_INTERVAL=60 +# Wallet +KC_ENCRYPTED_WALLET=false + # CLI KC_GATEKEEPER_URL=http://localhost:4224 # KC_KEYMASTER_URL=http://localhost:4226 diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index 038aede7..1e91ec23 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -6,6 +6,7 @@ import * as gatekeeper_sdk from '@mdip/gatekeeper/sdk'; import * as keymaster_lib from '@mdip/keymaster/lib'; import * as keymaster_sdk from '@mdip/keymaster/sdk'; import * as db_wallet from '@mdip/keymaster/db/json'; +import * as db_encrypted_wallet from '@mdip/keymaster/db/encrypted/json'; import * as cipher from '@mdip/cipher/node'; dotenv.config(); @@ -14,6 +15,9 @@ let keymaster; const gatekeeperURL = process.env.KC_GATEKEEPER_URL || 'http://localhost:4224'; const keymasterURL = process.env.KC_KEYMASTER_URL; +const keymasterEncypted = process.env.KC_ENCRYPTED_WALLET === 'true'; +const keymasterPassphrase = process.env.KC_ENCRYPTED_PASSPHRASE; + const UPDATE_OK = "OK"; const UPDATE_FAILED = "Update failed"; @@ -994,7 +998,10 @@ async function run() { await keymaster.start({ gatekeeper: gatekeeper_sdk, wallet: db_wallet, - cipher + encrypted_wallet: db_encrypted_wallet, + cipher, + encrypted: keymasterEncypted, + passphrase: keymasterPassphrase, }); program.parse(process.argv); await keymaster.stop(); From 15fbd7dd2fbeb8b4f6650a07708d9b8d439da9dd Mon Sep 17 00:00:00 2001 From: Bushstar Date: Wed, 4 Dec 2024 16:56:44 +0000 Subject: [PATCH 02/12] Do not prompt for passphrase in headless lib --- package-lock.json | 328 +----------------- packages/keymaster/package.json | 1 - .../keymaster/src/db-wallet-encrypted-json.js | 4 - packages/keymaster/src/keymaster-lib.js | 39 +-- 4 files changed, 9 insertions(+), 363 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb9aa0a1..498a6f60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2477,253 +2477,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@inquirer/checkbox": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.2.tgz", - "integrity": "sha512-+gznPl8ip8P8HYHYecDtUtdsh1t2jvb+sWCD72GAiZ9m45RqwrLmReDaqdC0umQfamtFXVRoMVJ2/qINKGm9Tg==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/figures": "^1.0.8", - "@inquirer/type": "^3.0.1", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/confirm": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.2.tgz", - "integrity": "sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/core": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.0.tgz", - "integrity": "sha512-I+ETk2AL+yAVbvuKx5AJpQmoaWhpiTFOg/UJb7ZkMAK4blmtG8ATh5ct+T/8xNld0CZG/2UhtkdMwpgvld92XQ==", - "dependencies": { - "@inquirer/figures": "^1.0.8", - "@inquirer/type": "^3.0.1", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "mute-stream": "^2.0.0", - "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/core/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@inquirer/core/node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@inquirer/editor": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.1.0.tgz", - "integrity": "sha512-K1gGWsxEqO23tVdp5MT3H799OZ4ER1za7Dlc8F4um0W7lwSv0KGR/YyrUEyimj0g7dXZd8XknM/5QA2/Uy+TbA==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1", - "external-editor": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/expand": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.2.tgz", - "integrity": "sha512-WdgCX1cUtinz+syKyZdJomovULYlKUWZbVYZzhf+ZeeYf4htAQ3jLymoNs3koIAKfZZl3HUBb819ClCBfyznaw==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.8.tgz", - "integrity": "sha512-tKd+jsmhq21AP1LhexC0pPwsCxEhGgAkg28byjJAd+xhmIs8LUX8JbUc3vBf3PhLxWiB5EvyBE5X7JSPAqMAqg==", - "engines": { - "node": ">=18" - } - }, - "node_modules/@inquirer/input": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.0.2.tgz", - "integrity": "sha512-yCLCraigU085EcdpIVEDgyfGv4vBiE4I+k1qRkc9C5dMjWF42ADMGy1RFU94+eZlz4YlkmFsiyHZy0W1wdhaNg==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/number": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.2.tgz", - "integrity": "sha512-MKQhYofdUNk7eqJtz52KvM1dH6R93OMrqHduXCvuefKrsiMjHiMwjc3NZw5Imm2nqY7gWd9xdhYrtcHMJQZUxA==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/password": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.2.tgz", - "integrity": "sha512-tQXGSu7IO07gsYlGy3VgXRVsbOWqFBMbqAUrJSc1PDTQQ5Qdm+QVwkP0OC0jnUZ62D19iPgXOMO+tnWG+HhjNQ==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1", - "ansi-escapes": "^4.3.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/prompts": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.1.0.tgz", - "integrity": "sha512-5U/XiVRH2pp1X6gpNAjWOglMf38/Ys522ncEHIKT1voRUvSj/DQnR22OVxHnwu5S+rCFaUiPQ57JOtMFQayqYA==", - "dependencies": { - "@inquirer/checkbox": "^4.0.2", - "@inquirer/confirm": "^5.0.2", - "@inquirer/editor": "^4.1.0", - "@inquirer/expand": "^4.0.2", - "@inquirer/input": "^4.0.2", - "@inquirer/number": "^3.0.2", - "@inquirer/password": "^4.0.2", - "@inquirer/rawlist": "^4.0.2", - "@inquirer/search": "^3.0.2", - "@inquirer/select": "^4.0.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/rawlist": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.2.tgz", - "integrity": "sha512-3XGcskMoVF8H0Dl1S5TSZ3rMPPBWXRcM0VeNVsS4ByWeWjSeb0lPqfnBg6N7T0608I1B2bSVnbi2cwCrmOD1Yw==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/type": "^3.0.1", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/search": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.2.tgz", - "integrity": "sha512-Zv4FC7w4dJ13BOJfKRQCICQfShinGjb1bCEIHxTSnjj2telu3+3RHwHubPG9HyD4aix5s+lyAMEK/wSFD75HLA==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/figures": "^1.0.8", - "@inquirer/type": "^3.0.1", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/select": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.2.tgz", - "integrity": "sha512-uSWUzaSYAEj0hlzxa1mUB6VqrKaYx0QxGBLZzU4xWFxaSyGaXxsSE4OSOwdU24j0xl8OajgayqFXW0l2bkl2kg==", - "dependencies": { - "@inquirer/core": "^10.1.0", - "@inquirer/figures": "^1.0.8", - "@inquirer/type": "^3.0.1", - "ansi-escapes": "^4.3.2", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, - "node_modules/@inquirer/type": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.1.tgz", - "integrity": "sha512-+ksJMIy92sOAiAccGpcKZUc3bYO07cADnscIxHBknEm3uNts3movSmBofc1908BNy5edKscxYeAdaX1NXkHS6A==", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - } - }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -4931,6 +4684,7 @@ }, "node_modules/ansi-escapes": { "version": "4.3.2", + "dev": true, "license": "MIT", "dependencies": { "type-fest": "^0.21.3" @@ -4944,6 +4698,7 @@ }, "node_modules/ansi-escapes/node_modules/type-fest": { "version": "0.21.3", + "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" @@ -5963,11 +5718,6 @@ "node": ">=10" } }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, "node_modules/chownr": { "version": "1.1.4", "license": "ISC" @@ -6050,14 +5800,6 @@ "node": ">=6" } }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "engines": { - "node": ">= 12" - } - }, "node_modules/cliui": { "version": "8.0.1", "license": "ISC", @@ -7561,19 +7303,6 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", "peer": true }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "dev": true, @@ -8307,17 +8036,6 @@ "ms": "^2.0.0" } }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "funding": [ @@ -11205,14 +10923,6 @@ "npm": ">=6.0.0" } }, - "node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/napi-build-utils": { "version": "1.0.2", "license": "MIT" @@ -11640,14 +11350,6 @@ "node": ">= 0.8.0" } }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/p-defer": { "version": "4.0.0", "license": "MIT", @@ -12707,7 +12409,8 @@ }, "node_modules/safer-buffer": { "version": "2.1.2", - "license": "MIT" + "license": "MIT", + "optional": true }, "node_modules/sanitize-filename": { "version": "1.6.3", @@ -13630,17 +13333,6 @@ "node": ">= 4.5.0" } }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "license": "BSD-3-Clause" @@ -14330,17 +14022,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "packages/cipher": { "name": "@mdip/cipher", "version": "0.4.0", @@ -14402,7 +14083,6 @@ "version": "0.4.1", "license": "MIT", "dependencies": { - "@inquirer/prompts": "^7.1.0", "@mdip/common": "*", "axios": "^1.7.7" } diff --git a/packages/keymaster/package.json b/packages/keymaster/package.json index 90acc898..14fcff28 100644 --- a/packages/keymaster/package.json +++ b/packages/keymaster/package.json @@ -17,7 +17,6 @@ "author": "David McFadzean ", "license": "MIT", "dependencies": { - "@inquirer/prompts": "^7.1.0", "@mdip/common": "*", "axios": "^1.7.7" }, diff --git a/packages/keymaster/src/db-wallet-encrypted-json.js b/packages/keymaster/src/db-wallet-encrypted-json.js index a956fda5..cb7809df 100644 --- a/packages/keymaster/src/db-wallet-encrypted-json.js +++ b/packages/keymaster/src/db-wallet-encrypted-json.js @@ -17,10 +17,6 @@ export function setPassphrase(pw) { passphrase = pw; } -export function getPassphrase(pw) { - return passphrase; -} - export function saveWallet(wallet, overwrite = false) { if (fs.existsSync(walletName) && !overwrite) { return false; diff --git a/packages/keymaster/src/keymaster-lib.js b/packages/keymaster/src/keymaster-lib.js index 592943f7..74b98cce 100644 --- a/packages/keymaster/src/keymaster-lib.js +++ b/packages/keymaster/src/keymaster-lib.js @@ -1,4 +1,3 @@ -import { password } from '@inquirer/prompts'; import { InvalidDIDError, InvalidParameterError, @@ -30,10 +29,14 @@ export async function start(options = {}) { if (options.encrypted_wallet) { db = options.encrypted_wallet; - if (!db.loadWallet || !db.saveWallet || !db.setPassphrase || !db.getPassphrase) { + if (!db.loadWallet || !db.saveWallet || !db.setPassphrase) { throw new InvalidParameterError('options.encrypted_wallet'); } + if (!options.passphrase) { + throw new PassphraseError('KC_ENCRYPTED_PASSPHRASE not set'); + } + db.setPassphrase(options.passphrase); } else { throw new InvalidParameterError('options.encrypted_wallet'); @@ -70,18 +73,7 @@ export async function listRegistries() { return gatekeeper.listRegistries(); } -async function checkAndSetPassphrase() { - if (!db.getPassphrase()) { - const passphrase = await promptPassphrase(); - db.setPassphrase(passphrase); - } -} - export async function loadWallet() { - if (db.setPassphrase) { - await checkAndSetPassphrase(); - } - let wallet = await db.loadWallet(); if (!wallet) { @@ -92,10 +84,6 @@ export async function loadWallet() { } export async function saveWallet(wallet, overwrite = true) { - if (db.setPassphrase) { - await checkAndSetPassphrase(); - } - // TBD validate wallet before saving return db.saveWallet(wallet, overwrite); } @@ -1985,20 +1973,3 @@ export async function unpublishPoll(pollId) { return updateAsset(pollId, { poll }); } - -async function promptPassphrase() { - try { - return await password({ - message: 'Please provide a wallet passphrase:', - mask: '*', - validate(value) { - if (value === '') { - return 'Passphrase cannot be empty'; - } - return true; - }, - }); - } catch (error) { - throw new PassphraseError('Exiting as no passphrase set'); - } -} From 27390647d81bcd57c75bcf952a7b5f878a94e4cc Mon Sep 17 00:00:00 2001 From: Bushstar Date: Thu, 5 Dec 2024 10:55:08 +0000 Subject: [PATCH 03/12] Combine wallet handling. Encrypt existing wallet. Change passphrase. --- .../keymaster/src/db-wallet-encrypted-json.js | 71 -------------- packages/keymaster/src/db-wallet-json.js | 97 ++++++++++++++++++- packages/keymaster/src/keymaster-lib.js | 33 ++----- scripts/keychain-cli.js | 4 +- 4 files changed, 104 insertions(+), 101 deletions(-) delete mode 100644 packages/keymaster/src/db-wallet-encrypted-json.js diff --git a/packages/keymaster/src/db-wallet-encrypted-json.js b/packages/keymaster/src/db-wallet-encrypted-json.js deleted file mode 100644 index cb7809df..00000000 --- a/packages/keymaster/src/db-wallet-encrypted-json.js +++ /dev/null @@ -1,71 +0,0 @@ -import fs from 'fs'; -import crypto from 'crypto'; - -const dataFolder = 'data'; -const walletName = `${dataFolder}/wallet_encrypted.json`; - -const algorithm = 'aes-256-cbc'; // Algorithm -const keyLength = 32; // 256 bit AES-256 -const ivLength = 16; // 128-bit AES block size -const saltLength = 16; // 128-bit salt -const iterations = 200000; // PBKDF2 iterations -const digest = 'sha512'; // PBKDF2 hash function - -let passphrase; - -export function setPassphrase(pw) { - passphrase = pw; -} - -export function saveWallet(wallet, overwrite = false) { - if (fs.existsSync(walletName) && !overwrite) { - return false; - } - - if (!fs.existsSync(dataFolder)) { - fs.mkdirSync(dataFolder, { recursive: true }); - } - - const walletJson = JSON.stringify(wallet, null, 4); - const salt = crypto.randomBytes(saltLength); - const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); - const iv = crypto.randomBytes(ivLength); - const cipher = crypto.createCipheriv(algorithm, key, iv); - - let encrypted = cipher.update(walletJson, 'utf8', 'base64'); - encrypted += cipher.final('base64'); - - const encryptedData = { - salt: salt.toString('base64'), - iv: iv.toString('base64'), - data: encrypted - }; - - fs.writeFileSync(walletName, JSON.stringify(encryptedData, null, 4)); - return true; -} - -export function loadWallet() { - if (fs.existsSync(walletName)) { - const encryptedJson = fs.readFileSync(walletName, 'utf8'); - const encryptedData = JSON.parse(encryptedJson); - - const salt = Buffer.from(encryptedData.salt, 'base64'); - const iv = Buffer.from(encryptedData.iv, 'base64'); - const encrypted = encryptedData.data; - const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); - const decipher = crypto.createDecipheriv(algorithm, key, iv); - - let decrypted; - try { - decrypted = decipher.update(encrypted, 'base64', 'utf8'); - decrypted += decipher.final('utf8'); - } catch (err) { - throw new Error('Incorrect passphrase.'); - } - - return JSON.parse(decrypted); - } - - return null; -} diff --git a/packages/keymaster/src/db-wallet-json.js b/packages/keymaster/src/db-wallet-json.js index 4316d19f..690ea59d 100644 --- a/packages/keymaster/src/db-wallet-json.js +++ b/packages/keymaster/src/db-wallet-json.js @@ -1,8 +1,32 @@ import fs from 'fs'; +import crypto from 'crypto'; const dataFolder = 'data'; const walletName = `${dataFolder}/wallet.json`; +const algorithm = 'aes-256-cbc'; // Algorithm +const keyLength = 32; // 256 bit AES-256 +const ivLength = 16; // 128-bit AES block size +const saltLength = 16; // 128-bit salt +const iterations = 200000; // PBKDF2 iterations +const digest = 'sha512'; // PBKDF2 hash function + +let passphrase; +let newPassphrase; +let encryptionEnabled; + +export function setEncryption(pp, ee, np) { + passphrase = pp; + encryptionEnabled = ee; + newPassphrase = np; +} + +function isWalletEncrypted() { + const fileContent = fs.readFileSync(walletName, 'utf8'); + const data = JSON.parse(fileContent); + return data && data.salt && data.iv && data.data; +} + export function saveWallet(wallet, overwrite = false) { if (fs.existsSync(walletName) && !overwrite) { return false; @@ -12,15 +36,78 @@ export function saveWallet(wallet, overwrite = false) { fs.mkdirSync(dataFolder, { recursive: true }); } - fs.writeFileSync(walletName, JSON.stringify(wallet, null, 4)); + if (encryptionEnabled) { + if (!passphrase) { + throw new Error('KC_ENCRYPTED_PASSPHRASE not set'); + } + + const walletJson = JSON.stringify(wallet, null, 4); + const salt = crypto.randomBytes(saltLength); + const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); + const iv = crypto.randomBytes(ivLength); + const cipher = crypto.createCipheriv(algorithm, key, iv); + + let encrypted = cipher.update(walletJson, 'utf8', 'base64'); + encrypted += cipher.final('base64'); + + const encryptedData = { + salt: salt.toString('base64'), + iv: iv.toString('base64'), + data: encrypted + }; + + fs.writeFileSync(walletName, JSON.stringify(encryptedData, null, 4)); + } else { + fs.writeFileSync(walletName, JSON.stringify(wallet, null, 4)); + } + return true; } export function loadWallet() { - if (fs.existsSync(walletName)) { - const walletJson = fs.readFileSync(walletName); - return JSON.parse(walletJson); + if (!fs.existsSync(walletName)) { + return null; + } + + let encrypted = isWalletEncrypted(); + let walletData; + + if (!encryptionEnabled && encrypted) { + throw new Error('Wallet encrypted but encryption is not enabled'); + } + + if (encrypted) { + if (!passphrase) { + throw new Error('KC_ENCRYPTED_PASSPHRASE not set'); + } + + const encryptedJson = fs.readFileSync(walletName, 'utf8'); + const encryptedData = JSON.parse(encryptedJson); + + const salt = Buffer.from(encryptedData.salt, 'base64'); + const iv = Buffer.from(encryptedData.iv, 'base64'); + const encryptedJSON = encryptedData.data; + const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + + let decrypted; + try { + decrypted = decipher.update(encryptedJSON, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + } catch (err) { + throw new Error('Incorrect passphrase.'); + } + walletData = JSON.parse(decrypted); + } else { + const walletJson = fs.readFileSync(walletName, 'utf8'); + walletData = JSON.parse(walletJson); + } + + if (encryptionEnabled && (!encrypted || newPassphrase)) { + passphrase = newPassphrase || passphrase; + saveWallet(walletData, true); + newPassphrase = undefined; } - return null; + return walletData; } diff --git a/packages/keymaster/src/keymaster-lib.js b/packages/keymaster/src/keymaster-lib.js index 74b98cce..55b041ad 100644 --- a/packages/keymaster/src/keymaster-lib.js +++ b/packages/keymaster/src/keymaster-lib.js @@ -25,33 +25,20 @@ export async function start(options = {}) { throw new InvalidParameterError('options.gatekeeper'); } - if (options.encrypted) { - if (options.encrypted_wallet) { - db = options.encrypted_wallet; + if (options.wallet) { + db = options.wallet; - if (!db.loadWallet || !db.saveWallet || !db.setPassphrase) { - throw new InvalidParameterError('options.encrypted_wallet'); - } - - if (!options.passphrase) { - throw new PassphraseError('KC_ENCRYPTED_PASSPHRASE not set'); - } - - db.setPassphrase(options.passphrase); - } else { - throw new InvalidParameterError('options.encrypted_wallet'); + if (!db.loadWallet || !db.saveWallet || !db.setEncryption) { + throw new InvalidParameterError('options.wallet'); } } else { - if (options.wallet) { - db = options.wallet; + throw new InvalidParameterError('options.wallet'); + } - if (!db.loadWallet || !db.saveWallet) { - throw new InvalidParameterError('options.wallet'); - } - } - else { - throw new InvalidParameterError('options.wallet'); - } + if (options.passphrase) { + db.setEncryption(options.passphrase, options.encrypted, options.newPassphrase); + } else if (options.encrypted) { + throw new PassphraseError('KC_ENCRYPTED_PASSPHRASE not set'); } if (options.cipher) { diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index 1e91ec23..4e187101 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -6,7 +6,6 @@ import * as gatekeeper_sdk from '@mdip/gatekeeper/sdk'; import * as keymaster_lib from '@mdip/keymaster/lib'; import * as keymaster_sdk from '@mdip/keymaster/sdk'; import * as db_wallet from '@mdip/keymaster/db/json'; -import * as db_encrypted_wallet from '@mdip/keymaster/db/encrypted/json'; import * as cipher from '@mdip/cipher/node'; dotenv.config(); @@ -17,6 +16,7 @@ const keymasterURL = process.env.KC_KEYMASTER_URL; const keymasterEncypted = process.env.KC_ENCRYPTED_WALLET === 'true'; const keymasterPassphrase = process.env.KC_ENCRYPTED_PASSPHRASE; +const keymasterNewPassphrase = process.env.KC_ENCRYPTED_NEW_PASSPHRASE; const UPDATE_OK = "OK"; const UPDATE_FAILED = "Update failed"; @@ -998,10 +998,10 @@ async function run() { await keymaster.start({ gatekeeper: gatekeeper_sdk, wallet: db_wallet, - encrypted_wallet: db_encrypted_wallet, cipher, encrypted: keymasterEncypted, passphrase: keymasterPassphrase, + newPassphrase: keymasterNewPassphrase, }); program.parse(process.argv); await keymaster.stop(); From 27d906852989c08227a61e6a8b80bdc4671672e0 Mon Sep 17 00:00:00 2001 From: Bushstar Date: Thu, 5 Dec 2024 12:16:46 +0000 Subject: [PATCH 04/12] Improve coverage --- tests/keymaster.test.js | 146 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 146 insertions(+) diff --git a/tests/keymaster.test.js b/tests/keymaster.test.js index 365c2206..d75b1a87 100644 --- a/tests/keymaster.test.js +++ b/tests/keymaster.test.js @@ -9,6 +9,8 @@ import * as wallet from '@mdip/keymaster/db/json'; import { copyJSON } from '@mdip/common/utils'; import { InvalidDIDError, ExpectedExceptionError, UnknownIDError } from '@mdip/common/errors'; +const passphraseNotSet = 'KC_ENCRYPTED_PASSPHRASE not set'; + beforeEach(async () => { await db_json.start('mdip'); await gatekeeper.start({ db: db_json }); @@ -86,6 +88,14 @@ describe('start', () => { catch (error) { expect(error.message).toBe('Invalid parameter: options.cipher'); } + + try { + await keymaster.start({ gatekeeper, wallet, cipher, encrypted: true }); + throw new ExpectedExceptionError(); + } + catch (error) { + expect(error.message).toBe('Passphrase: ' + passphraseNotSet); + } }); }); @@ -121,6 +131,7 @@ describe('saveWallet', () => { afterEach(() => { mockFs.restore(); + wallet.setEncryption(undefined, undefined, undefined); }); it('should save a wallet', async () => { @@ -197,6 +208,141 @@ describe('saveWallet', () => { expect(wallet).toStrictEqual(mockWallet); } }); + + it('load encrypted wallet', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + const passphrase = 'passphrase'; + const encryptionEnabled = true; + wallet.setEncryption(passphrase, encryptionEnabled, undefined); + + const ok = await keymaster.saveWallet(mockWallet); + const walletData = await keymaster.loadWallet(); + + expect(ok).toBe(true); + expect(walletData).toStrictEqual(mockWallet); + }); + + it('encrypt unencrypted wallet', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + const passphrase = 'passphrase'; + const encryptionEnabled = true; + + const ok = await keymaster.saveWallet(mockWallet); + + wallet.setEncryption(passphrase, encryptionEnabled, undefined); + + const walletData = await keymaster.loadWallet(); + + expect(ok).toBe(true); + expect(walletData).toStrictEqual(mockWallet); + }); + + it('update passphrase', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + const passphrase = 'passphrase'; + const newPassphrase = 'newPassphrase'; + const encryptionEnabled = true; + + wallet.setEncryption(passphrase, encryptionEnabled, undefined); + + const ok = await keymaster.saveWallet(mockWallet); + + wallet.setEncryption(passphrase, encryptionEnabled, newPassphrase); + + const walletData = await keymaster.loadWallet(); + + expect(ok).toBe(true); + expect(walletData).toStrictEqual(mockWallet); + }); + + it('check save encrypted with no passphrase throws', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + const encryptionEnabled = true; + wallet.setEncryption(undefined, encryptionEnabled, undefined); + + try { + await keymaster.saveWallet(mockWallet); + throw new ExpectedExceptionError(); + } catch (e) { + expect(e.message).toBe(passphraseNotSet); + } + }); + + it('check load encrypted with no passphrase throws', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + const passphrase = 'passphrase'; + const encryptionEnabled = true; + + wallet.setEncryption(passphrase, encryptionEnabled, undefined); + + const ok = await keymaster.saveWallet(mockWallet); + expect(ok).toBe(true); + + wallet.setEncryption(undefined, encryptionEnabled, undefined); + + try { + await keymaster.loadWallet(); + throw new ExpectedExceptionError(); + } catch (e) { + expect(e.message).toBe(passphraseNotSet); + } + }); + + it('check load unencrypted with encrypted wallet throws', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + const passphrase = 'passphrase'; + const encryptionEnabled = true; + + wallet.setEncryption(passphrase, encryptionEnabled, undefined); + + const ok = await keymaster.saveWallet(mockWallet); + expect(ok).toBe(true); + + wallet.setEncryption(undefined, undefined, undefined); + + try { + await keymaster.loadWallet(); + throw new ExpectedExceptionError(); + } catch (e) { + expect(e.message).toBe('Wallet encrypted but encryption is not enabled'); + } + }); + + it('check load with incorrect passphrase throws', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + let passphrase = 'passphrase'; + const encryptionEnabled = true; + + wallet.setEncryption(passphrase, encryptionEnabled, undefined); + + const ok = await keymaster.saveWallet(mockWallet); + expect(ok).toBe(true); + + passphrase = 'incorrect'; + + wallet.setEncryption(passphrase, encryptionEnabled, undefined); + + try { + await keymaster.loadWallet(); + throw new ExpectedExceptionError(); + } catch (e) { + expect(e.message).toBe('Incorrect passphrase.'); + } + }); }); describe('decryptMnemonic', () => { From 4c963c1cc81c336559963e9183eb4afcfef03b9e Mon Sep 17 00:00:00 2001 From: Bushstar Date: Thu, 5 Dec 2024 12:45:51 +0000 Subject: [PATCH 05/12] Remove unused line --- packages/keymaster/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/keymaster/package.json b/packages/keymaster/package.json index 14fcff28..d4ceb9a5 100644 --- a/packages/keymaster/package.json +++ b/packages/keymaster/package.json @@ -7,7 +7,6 @@ "./lib": "./src/keymaster-lib.js", "./sdk": "./src/keymaster-sdk.js", "./db/json": "./src/db-wallet-json.js", - "./db/encrypted/json": "./src/db-wallet-encrypted-json.js", "./db/web": "./src/db-wallet-web.js" }, "scripts": { From 3b08b8b5dcd558a0bcf0764b0929a825b1170494 Mon Sep 17 00:00:00 2001 From: Bushstar Date: Fri, 6 Dec 2024 14:19:08 +0000 Subject: [PATCH 06/12] Encrypt wallet via CLI. --- packages/common/src/errors.js | 8 - packages/keymaster/package.json | 1 + packages/keymaster/src/db-wallet-json-enc.js | 86 ++++++++ packages/keymaster/src/db-wallet-json.js | 91 +------- packages/keymaster/src/keymaster-lib.js | 16 +- scripts/keychain-cli.js | 37 +++- tests/keymaster.test.js | 206 ++++++++----------- 7 files changed, 208 insertions(+), 237 deletions(-) create mode 100644 packages/keymaster/src/db-wallet-json-enc.js diff --git a/packages/common/src/errors.js b/packages/common/src/errors.js index 3c0d2770..bf775d09 100644 --- a/packages/common/src/errors.js +++ b/packages/common/src/errors.js @@ -47,14 +47,6 @@ export class UnknownIDError extends MDIPError { } } -export class PassphraseError extends MDIPError { - static type = 'Passphrase'; - - constructor(detail) { - super(PassphraseError.type, detail); - } -} - // For unit tests export class ExpectedExceptionError extends MDIPError { static type = 'Expected to throw an exception'; diff --git a/packages/keymaster/package.json b/packages/keymaster/package.json index d4ceb9a5..dc556262 100644 --- a/packages/keymaster/package.json +++ b/packages/keymaster/package.json @@ -7,6 +7,7 @@ "./lib": "./src/keymaster-lib.js", "./sdk": "./src/keymaster-sdk.js", "./db/json": "./src/db-wallet-json.js", + "./db/json/enc": "./src/db-wallet-json-enc.js", "./db/web": "./src/db-wallet-web.js" }, "scripts": { diff --git a/packages/keymaster/src/db-wallet-json-enc.js b/packages/keymaster/src/db-wallet-json-enc.js new file mode 100644 index 00000000..ba761793 --- /dev/null +++ b/packages/keymaster/src/db-wallet-json-enc.js @@ -0,0 +1,86 @@ +import fs from 'fs'; +import crypto from 'crypto'; + +const dataFolder = 'data'; +const walletName = `${dataFolder}/wallet.json`; + +const algorithm = 'aes-256-cbc'; // Algorithm +const keyLength = 32; // 256 bit AES-256 +const ivLength = 16; // 128-bit AES block size +const saltLength = 16; // 128-bit salt +const iterations = 200000; // PBKDF2 iterations +const digest = 'sha512'; // PBKDF2 hash function + +let passphrase; +let newPassphrase; +let encryptionEnabled; + +export function setPassphrase(pp) { + passphrase = pp; +} + +export function saveWallet(wallet, overwrite = false) { + if (fs.existsSync(walletName) && !overwrite) { + return false; + } + + if (!fs.existsSync(dataFolder)) { + fs.mkdirSync(dataFolder, { recursive: true }); + } + + if (!passphrase) { + throw new Error('KC_ENCRYPTED_PASSPHRASE not set'); + } + + const walletJson = JSON.stringify(wallet, null, 4); + const salt = crypto.randomBytes(saltLength); + const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); + const iv = crypto.randomBytes(ivLength); + const cipher = crypto.createCipheriv(algorithm, key, iv); + + let encrypted = cipher.update(walletJson, 'utf8', 'base64'); + encrypted += cipher.final('base64'); + + const encryptedData = { + salt: salt.toString('base64'), + iv: iv.toString('base64'), + data: encrypted + }; + + fs.writeFileSync(walletName, JSON.stringify(encryptedData, null, 4)); + + return true; +} + +export function loadWallet() { + if (!fs.existsSync(walletName)) { + return null; + } + + if (!passphrase) { + throw new Error('KC_ENCRYPTED_PASSPHRASE not set'); + } + + const encryptedJson = fs.readFileSync(walletName, 'utf8'); + const encryptedData = JSON.parse(encryptedJson); + + if (!encryptedData || !encryptedData.salt || !encryptedData.iv || !encryptedData.data) { + throw new Error('Wallet not encrypted'); + } + + const salt = Buffer.from(encryptedData.salt, 'base64'); + const iv = Buffer.from(encryptedData.iv, 'base64'); + const encryptedJSON = encryptedData.data; + const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); + const decipher = crypto.createDecipheriv(algorithm, key, iv); + + let decrypted; + try { + decrypted = decipher.update(encryptedJSON, 'base64', 'utf8'); + decrypted += decipher.final('utf8'); + } catch (err) { + throw new Error('Incorrect passphrase.'); + } + + return JSON.parse(decrypted); +} diff --git a/packages/keymaster/src/db-wallet-json.js b/packages/keymaster/src/db-wallet-json.js index 690ea59d..196fb499 100644 --- a/packages/keymaster/src/db-wallet-json.js +++ b/packages/keymaster/src/db-wallet-json.js @@ -1,32 +1,8 @@ import fs from 'fs'; -import crypto from 'crypto'; const dataFolder = 'data'; const walletName = `${dataFolder}/wallet.json`; -const algorithm = 'aes-256-cbc'; // Algorithm -const keyLength = 32; // 256 bit AES-256 -const ivLength = 16; // 128-bit AES block size -const saltLength = 16; // 128-bit salt -const iterations = 200000; // PBKDF2 iterations -const digest = 'sha512'; // PBKDF2 hash function - -let passphrase; -let newPassphrase; -let encryptionEnabled; - -export function setEncryption(pp, ee, np) { - passphrase = pp; - encryptionEnabled = ee; - newPassphrase = np; -} - -function isWalletEncrypted() { - const fileContent = fs.readFileSync(walletName, 'utf8'); - const data = JSON.parse(fileContent); - return data && data.salt && data.iv && data.data; -} - export function saveWallet(wallet, overwrite = false) { if (fs.existsSync(walletName) && !overwrite) { return false; @@ -36,31 +12,7 @@ export function saveWallet(wallet, overwrite = false) { fs.mkdirSync(dataFolder, { recursive: true }); } - if (encryptionEnabled) { - if (!passphrase) { - throw new Error('KC_ENCRYPTED_PASSPHRASE not set'); - } - - const walletJson = JSON.stringify(wallet, null, 4); - const salt = crypto.randomBytes(saltLength); - const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); - const iv = crypto.randomBytes(ivLength); - const cipher = crypto.createCipheriv(algorithm, key, iv); - - let encrypted = cipher.update(walletJson, 'utf8', 'base64'); - encrypted += cipher.final('base64'); - - const encryptedData = { - salt: salt.toString('base64'), - iv: iv.toString('base64'), - data: encrypted - }; - - fs.writeFileSync(walletName, JSON.stringify(encryptedData, null, 4)); - } else { - fs.writeFileSync(walletName, JSON.stringify(wallet, null, 4)); - } - + fs.writeFileSync(walletName, JSON.stringify(wallet, null, 4)); return true; } @@ -69,44 +21,11 @@ export function loadWallet() { return null; } - let encrypted = isWalletEncrypted(); - let walletData; - - if (!encryptionEnabled && encrypted) { - throw new Error('Wallet encrypted but encryption is not enabled'); - } - - if (encrypted) { - if (!passphrase) { - throw new Error('KC_ENCRYPTED_PASSPHRASE not set'); - } - - const encryptedJson = fs.readFileSync(walletName, 'utf8'); - const encryptedData = JSON.parse(encryptedJson); - - const salt = Buffer.from(encryptedData.salt, 'base64'); - const iv = Buffer.from(encryptedData.iv, 'base64'); - const encryptedJSON = encryptedData.data; - const key = crypto.pbkdf2Sync(passphrase, salt, iterations, keyLength, digest); - const decipher = crypto.createDecipheriv(algorithm, key, iv); - - let decrypted; - try { - decrypted = decipher.update(encryptedJSON, 'base64', 'utf8'); - decrypted += decipher.final('utf8'); - } catch (err) { - throw new Error('Incorrect passphrase.'); - } - walletData = JSON.parse(decrypted); - } else { - const walletJson = fs.readFileSync(walletName, 'utf8'); - walletData = JSON.parse(walletJson); - } + const walletJson = fs.readFileSync(walletName); + const walletData = JSON.parse(walletJson); - if (encryptionEnabled && (!encrypted || newPassphrase)) { - passphrase = newPassphrase || passphrase; - saveWallet(walletData, true); - newPassphrase = undefined; + if (walletData && walletData.salt && walletData.iv && walletData.data) { + throw new Error('Wallet encrypted but KC_ENCRYPTED_PASSPHRASE not set'); } return walletData; diff --git a/packages/keymaster/src/keymaster-lib.js b/packages/keymaster/src/keymaster-lib.js index 55b041ad..2d37827f 100644 --- a/packages/keymaster/src/keymaster-lib.js +++ b/packages/keymaster/src/keymaster-lib.js @@ -1,10 +1,4 @@ -import { - InvalidDIDError, - InvalidParameterError, - KeymasterError, - UnknownIDError, - PassphraseError, -} from '@mdip/common/errors'; +import { InvalidDIDError, InvalidParameterError, KeymasterError, UnknownIDError } from '@mdip/common/errors'; let gatekeeper = null; let db = null; @@ -28,19 +22,13 @@ export async function start(options = {}) { if (options.wallet) { db = options.wallet; - if (!db.loadWallet || !db.saveWallet || !db.setEncryption) { + if (!db.loadWallet || !db.saveWallet) { throw new InvalidParameterError('options.wallet'); } } else { throw new InvalidParameterError('options.wallet'); } - if (options.passphrase) { - db.setEncryption(options.passphrase, options.encrypted, options.newPassphrase); - } else if (options.encrypted) { - throw new PassphraseError('KC_ENCRYPTED_PASSPHRASE not set'); - } - if (options.cipher) { cipher = options.cipher; diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index 4e187101..37ec8037 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -6,6 +6,7 @@ import * as gatekeeper_sdk from '@mdip/gatekeeper/sdk'; import * as keymaster_lib from '@mdip/keymaster/lib'; import * as keymaster_sdk from '@mdip/keymaster/sdk'; import * as db_wallet from '@mdip/keymaster/db/json'; +import * as db_wallet_enc from '@mdip/keymaster/db/json/enc'; import * as cipher from '@mdip/cipher/node'; dotenv.config(); @@ -14,9 +15,7 @@ let keymaster; const gatekeeperURL = process.env.KC_GATEKEEPER_URL || 'http://localhost:4224'; const keymasterURL = process.env.KC_KEYMASTER_URL; -const keymasterEncypted = process.env.KC_ENCRYPTED_WALLET === 'true'; const keymasterPassphrase = process.env.KC_ENCRYPTED_PASSPHRASE; -const keymasterNewPassphrase = process.env.KC_ENCRYPTED_NEW_PASSPHRASE; const UPDATE_OK = "OK"; const UPDATE_FAILED = "Update failed"; @@ -745,7 +744,7 @@ program program .command('list-schemas') .description('List schemas owned by current ID') - .action(async (file, name) => { + .action(async () => { try { const schemas = await keymaster.listSchemas(); console.log(JSON.stringify(schemas, null, 4)); @@ -939,6 +938,25 @@ program } }); +program + .command('encrypt-wallet [new-phrase]') + .description('Encrypt wallet or update passphrase') + .action(async (passphrase, newPassphrase) => { + try { + let wallet; + db_wallet_enc.setPassphrase(passphrase); + if (!newPassphrase) { + wallet = db_wallet.loadWallet(); + } else { + wallet = db_wallet_enc.loadWallet(); + db_wallet_enc.setPassphrase(newPassphrase); + } + db_wallet_enc.saveWallet(wallet, true); + } catch (error) { + console.error(error.message); + } + }); + program .command('perf-test [N]') .description('Performance test to create N credentials') @@ -977,6 +995,14 @@ program } }); +function getDBWallet() { + if (keymasterPassphrase) { + db_wallet_enc.setPassphrase(keymasterPassphrase); + return db_wallet_enc; + } + return db_wallet; +} + async function run() { if (keymasterURL) { keymaster = keymaster_sdk; @@ -997,11 +1023,8 @@ async function run() { }); await keymaster.start({ gatekeeper: gatekeeper_sdk, - wallet: db_wallet, + wallet: getDBWallet(), cipher, - encrypted: keymasterEncypted, - passphrase: keymasterPassphrase, - newPassphrase: keymasterNewPassphrase, }); program.parse(process.argv); await keymaster.stop(); diff --git a/tests/keymaster.test.js b/tests/keymaster.test.js index d75b1a87..4d9a3414 100644 --- a/tests/keymaster.test.js +++ b/tests/keymaster.test.js @@ -6,11 +6,10 @@ import * as gatekeeper from '@mdip/gatekeeper/lib'; import * as cipher from '@mdip/cipher/node'; import * as db_json from '@mdip/gatekeeper/db/json'; import * as wallet from '@mdip/keymaster/db/json'; +import * as wallet_enc from '@mdip/keymaster/db/json/enc'; import { copyJSON } from '@mdip/common/utils'; import { InvalidDIDError, ExpectedExceptionError, UnknownIDError } from '@mdip/common/errors'; -const passphraseNotSet = 'KC_ENCRYPTED_PASSPHRASE not set'; - beforeEach(async () => { await db_json.start('mdip'); await gatekeeper.start({ db: db_json }); @@ -88,21 +87,15 @@ describe('start', () => { catch (error) { expect(error.message).toBe('Invalid parameter: options.cipher'); } - - try { - await keymaster.start({ gatekeeper, wallet, cipher, encrypted: true }); - throw new ExpectedExceptionError(); - } - catch (error) { - expect(error.message).toBe('Passphrase: ' + passphraseNotSet); - } }); }); describe('loadWallet', () => { - afterEach(() => { + afterEach(async () => { mockFs.restore(); + wallet_enc.setPassphrase(undefined); + await keymaster.start({ gatekeeper, wallet, cipher }); }); it('should create a wallet on first load', async () => { @@ -125,13 +118,69 @@ describe('loadWallet', () => { expect(wallet2).toStrictEqual(wallet1); }); + + it('regular wallet should throw when loading encrypted wallet', async () => { + mockFs({}); + const mockWallet = { salt: 1, iv: 1, data: 1 }; + + const ok = await keymaster.saveWallet(mockWallet); + + try { + await keymaster.loadWallet(); + throw new ExpectedExceptionError(); + } catch (error) { + expect(ok).toBe(true); + expect(error.message).toBe('Wallet encrypted but KC_ENCRYPTED_PASSPHRASE not set'); + } + }); + + it('wallet should throw when passphrase not set', async () => { + mockFs({}); + const mockWallet = { mock: 1 }; + + await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); + + wallet_enc.setPassphrase('passphrase'); + const ok = await keymaster.saveWallet(mockWallet); + wallet_enc.setPassphrase(undefined); + + try { + await keymaster.loadWallet(); + throw new ExpectedExceptionError(); + } catch (error) { + expect(ok).toBe(true); + expect(error.message).toBe('KC_ENCRYPTED_PASSPHRASE not set'); + } + }); + + it('load with incorrect passphrase throws', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); + + wallet_enc.setPassphrase('passphrase'); + + const ok = await keymaster.saveWallet(mockWallet); + expect(ok).toBe(true); + + wallet_enc.setPassphrase('incorrect'); + + try { + await keymaster.loadWallet(); + throw new ExpectedExceptionError(); + } catch (e) { + expect(e.message).toBe('Incorrect passphrase.'); + } + }); }); describe('saveWallet', () => { - afterEach(() => { + afterEach(async () => { mockFs.restore(); - wallet.setEncryption(undefined, undefined, undefined); + wallet_enc.setPassphrase(undefined); + await keymaster.start({ gatekeeper, wallet, cipher }); }); it('should save a wallet', async () => { @@ -209,138 +258,51 @@ describe('saveWallet', () => { } }); - it('load encrypted wallet', async () => { - mockFs({}); - const mockWallet = { mock: 0 }; - - const passphrase = 'passphrase'; - const encryptionEnabled = true; - wallet.setEncryption(passphrase, encryptionEnabled, undefined); - - const ok = await keymaster.saveWallet(mockWallet); - const walletData = await keymaster.loadWallet(); - - expect(ok).toBe(true); - expect(walletData).toStrictEqual(mockWallet); - }); - - it('encrypt unencrypted wallet', async () => { - mockFs({}); - const mockWallet = { mock: 0 }; - - const passphrase = 'passphrase'; - const encryptionEnabled = true; - - const ok = await keymaster.saveWallet(mockWallet); - - wallet.setEncryption(passphrase, encryptionEnabled, undefined); - - const walletData = await keymaster.loadWallet(); - - expect(ok).toBe(true); - expect(walletData).toStrictEqual(mockWallet); - }); - - it('update passphrase', async () => { + it('should not overwrite an existing wallet if specified', async () => { mockFs({}); - const mockWallet = { mock: 0 }; - - const passphrase = 'passphrase'; - const newPassphrase = 'newPassphrase'; - const encryptionEnabled = true; - wallet.setEncryption(passphrase, encryptionEnabled, undefined); - - const ok = await keymaster.saveWallet(mockWallet); + await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); + wallet_enc.setPassphrase('passphrase'); - wallet.setEncryption(passphrase, encryptionEnabled, newPassphrase); + const mockWallet1 = { mock: 1 }; + const mockWallet2 = { mock: 2 }; + await keymaster.saveWallet(mockWallet1); + const ok = await keymaster.saveWallet(mockWallet2, false); const walletData = await keymaster.loadWallet(); - expect(ok).toBe(true); - expect(walletData).toStrictEqual(mockWallet); + expect(ok).toBe(false); + expect(walletData).toStrictEqual(mockWallet1); }); - it('check save encrypted with no passphrase throws', async () => { + it('wallet should throw when passphrase not set', async () => { mockFs({}); - const mockWallet = { mock: 0 }; - - const encryptionEnabled = true; - wallet.setEncryption(undefined, encryptionEnabled, undefined); + const mockWallet = { mock: 1 }; + await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); try { await keymaster.saveWallet(mockWallet); throw new ExpectedExceptionError(); - } catch (e) { - expect(e.message).toBe(passphraseNotSet); - } - }); - - it('check load encrypted with no passphrase throws', async () => { - mockFs({}); - const mockWallet = { mock: 0 }; - - const passphrase = 'passphrase'; - const encryptionEnabled = true; - - wallet.setEncryption(passphrase, encryptionEnabled, undefined); - - const ok = await keymaster.saveWallet(mockWallet); - expect(ok).toBe(true); - - wallet.setEncryption(undefined, encryptionEnabled, undefined); - - try { - await keymaster.loadWallet(); - throw new ExpectedExceptionError(); - } catch (e) { - expect(e.message).toBe(passphraseNotSet); - } - }); - - it('check load unencrypted with encrypted wallet throws', async () => { - mockFs({}); - const mockWallet = { mock: 0 }; - - const passphrase = 'passphrase'; - const encryptionEnabled = true; - - wallet.setEncryption(passphrase, encryptionEnabled, undefined); - - const ok = await keymaster.saveWallet(mockWallet); - expect(ok).toBe(true); - - wallet.setEncryption(undefined, undefined, undefined); - - try { - await keymaster.loadWallet(); - throw new ExpectedExceptionError(); - } catch (e) { - expect(e.message).toBe('Wallet encrypted but encryption is not enabled'); + } catch (error) { + expect(error.message).toBe('KC_ENCRYPTED_PASSPHRASE not set'); } }); - it('check load with incorrect passphrase throws', async () => { + it('encrypted wallet should throw when loading unencrypted wallet', async () => { mockFs({}); - const mockWallet = { mock: 0 }; - - let passphrase = 'passphrase'; - const encryptionEnabled = true; - - wallet.setEncryption(passphrase, encryptionEnabled, undefined); + const mockWallet = { mock: 1 }; const ok = await keymaster.saveWallet(mockWallet); - expect(ok).toBe(true); - passphrase = 'incorrect'; - - wallet.setEncryption(passphrase, encryptionEnabled, undefined); + await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); + wallet_enc.setPassphrase('passphrase'); try { - await keymaster.loadWallet(); + const result = await keymaster.loadWallet(); throw new ExpectedExceptionError(); - } catch (e) { - expect(e.message).toBe('Incorrect passphrase.'); + } catch (error) { + expect(ok).toBe(true); + expect(error.message).toBe('Wallet not encrypted'); } }); }); From 18bc2f9f6bddf4c59baa2005f9ac58844c51ffcf Mon Sep 17 00:00:00 2001 From: Bushstar Date: Fri, 6 Dec 2024 14:30:40 +0000 Subject: [PATCH 07/12] Remove unused vars --- packages/keymaster/src/db-wallet-json-enc.js | 2 -- tests/keymaster.test.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/keymaster/src/db-wallet-json-enc.js b/packages/keymaster/src/db-wallet-json-enc.js index ba761793..92bdef8f 100644 --- a/packages/keymaster/src/db-wallet-json-enc.js +++ b/packages/keymaster/src/db-wallet-json-enc.js @@ -12,8 +12,6 @@ const iterations = 200000; // PBKDF2 iterations const digest = 'sha512'; // PBKDF2 hash function let passphrase; -let newPassphrase; -let encryptionEnabled; export function setPassphrase(pp) { passphrase = pp; diff --git a/tests/keymaster.test.js b/tests/keymaster.test.js index 4d9a3414..86ee170c 100644 --- a/tests/keymaster.test.js +++ b/tests/keymaster.test.js @@ -298,7 +298,7 @@ describe('saveWallet', () => { wallet_enc.setPassphrase('passphrase'); try { - const result = await keymaster.loadWallet(); + await keymaster.loadWallet(); throw new ExpectedExceptionError(); } catch (error) { expect(ok).toBe(true); From ab64b832fc9d658318a1ef1611bba29342e111fc Mon Sep 17 00:00:00 2001 From: Bushstar Date: Fri, 6 Dec 2024 14:38:39 +0000 Subject: [PATCH 08/12] Loading non-existing wallet returns null --- tests/keymaster.test.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/keymaster.test.js b/tests/keymaster.test.js index 86ee170c..b4ec8553 100644 --- a/tests/keymaster.test.js +++ b/tests/keymaster.test.js @@ -119,6 +119,13 @@ describe('loadWallet', () => { expect(wallet2).toStrictEqual(wallet1); }); + it('loading non-existing encrypted wallet returns null', async () => { + mockFs({}); + + const wallet = wallet_enc.loadWallet(); + expect(wallet).toBe(null); + }); + it('regular wallet should throw when loading encrypted wallet', async () => { mockFs({}); const mockWallet = { salt: 1, iv: 1, data: 1 }; From 8353070d4c524aaaa71f5175b33034266a9a10a0 Mon Sep 17 00:00:00 2001 From: Bushstar Date: Sat, 7 Dec 2024 06:22:38 +0000 Subject: [PATCH 09/12] Separate encrypted and unencrypted wallets. Set env var automatically. --- packages/keymaster/src/db-wallet-json-enc.js | 2 +- scripts/keychain-cli.js | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/keymaster/src/db-wallet-json-enc.js b/packages/keymaster/src/db-wallet-json-enc.js index 92bdef8f..65422935 100644 --- a/packages/keymaster/src/db-wallet-json-enc.js +++ b/packages/keymaster/src/db-wallet-json-enc.js @@ -2,7 +2,7 @@ import fs from 'fs'; import crypto from 'crypto'; const dataFolder = 'data'; -const walletName = `${dataFolder}/wallet.json`; +const walletName = `${dataFolder}/wallet-enc.json`; const algorithm = 'aes-256-cbc'; // Algorithm const keyLength = 32; // 256 bit AES-256 diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index 37ec8037..d464046a 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -943,15 +943,27 @@ program .description('Encrypt wallet or update passphrase') .action(async (passphrase, newPassphrase) => { try { - let wallet; db_wallet_enc.setPassphrase(passphrase); + const unencryptedWallet = db_wallet.loadWallet(); + const encryptedWallet = db_wallet_enc.loadWallet(); if (!newPassphrase) { - wallet = db_wallet.loadWallet(); + if (unencryptedWallet === null) { + console.error('No unencrypted wallet exists to encrypt'); + return; + } else if (encryptedWallet !== null) { + console.error('Encrypted wallet-enc.json already exists'); + return; + } + db_wallet_enc.saveWallet(unencryptedWallet, true); } else { - wallet = db_wallet_enc.loadWallet(); + if (encryptedWallet === null) { + console.error('No encrypted wallet exists to change passphrase'); + return; + } db_wallet_enc.setPassphrase(newPassphrase); + db_wallet_enc.saveWallet(encryptedWallet, true); } - db_wallet_enc.saveWallet(wallet, true); + process.env.KC_ENCRYPTED_PASSPHRASE = newPassphrase ? newPassphrase : passphrase; } catch (error) { console.error(error.message); } From 31c8649fd0ed33b25d678e932732ceb537db0ccd Mon Sep 17 00:00:00 2001 From: Bushstar Date: Sat, 7 Dec 2024 06:46:03 +0000 Subject: [PATCH 10/12] Update failing test --- tests/keymaster.test.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/keymaster.test.js b/tests/keymaster.test.js index b4ec8553..cb06a70a 100644 --- a/tests/keymaster.test.js +++ b/tests/keymaster.test.js @@ -1,4 +1,5 @@ import mockFs from 'mock-fs'; +import fs from 'fs'; import canonicalize from 'canonicalize'; import * as keymaster from '@mdip/keymaster/lib'; @@ -296,10 +297,13 @@ describe('saveWallet', () => { }); it('encrypted wallet should throw when loading unencrypted wallet', async () => { - mockFs({}); + mockFs({ + 'data': {} + }); + const walletFile = 'data/wallet-enc.json'; const mockWallet = { mock: 1 }; - const ok = await keymaster.saveWallet(mockWallet); + fs.writeFileSync(walletFile, JSON.stringify(mockWallet, null, 4)); await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); wallet_enc.setPassphrase('passphrase'); @@ -308,7 +312,6 @@ describe('saveWallet', () => { await keymaster.loadWallet(); throw new ExpectedExceptionError(); } catch (error) { - expect(ok).toBe(true); expect(error.message).toBe('Wallet not encrypted'); } }); From 3577e55e2b293542fa172768a32a6b5aed818bd9 Mon Sep 17 00:00:00 2001 From: Bushstar Date: Mon, 9 Dec 2024 09:50:13 +0000 Subject: [PATCH 11/12] When encrypting wallet create one if it is not present --- scripts/keychain-cli.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index d464046a..e1b0794c 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -946,15 +946,20 @@ program db_wallet_enc.setPassphrase(passphrase); const unencryptedWallet = db_wallet.loadWallet(); const encryptedWallet = db_wallet_enc.loadWallet(); + if (!newPassphrase) { - if (unencryptedWallet === null) { - console.error('No unencrypted wallet exists to encrypt'); - return; - } else if (encryptedWallet !== null) { + if (encryptedWallet !== null) { console.error('Encrypted wallet-enc.json already exists'); - return; + } else if (unencryptedWallet === null) { + if (!keymasterPassphrase) { + await keymaster.start({ + gatekeeper: gatekeeper_sdk, + wallet: db_wallet_enc, + cipher, + }); + } + await keymaster.newWallet(); } - db_wallet_enc.saveWallet(unencryptedWallet, true); } else { if (encryptedWallet === null) { console.error('No encrypted wallet exists to change passphrase'); @@ -963,7 +968,6 @@ program db_wallet_enc.setPassphrase(newPassphrase); db_wallet_enc.saveWallet(encryptedWallet, true); } - process.env.KC_ENCRYPTED_PASSPHRASE = newPassphrase ? newPassphrase : passphrase; } catch (error) { console.error(error.message); } From 0f22b05cf639592ef7292a09c8fa469653bb4906 Mon Sep 17 00:00:00 2001 From: Bushstar Date: Mon, 9 Dec 2024 15:28:35 +0000 Subject: [PATCH 12/12] Set passphrase via env var. Remove change passphrase. --- scripts/keychain-cli.js | 43 ++++++++++++++++------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index e1b0794c..05e289e3 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -939,34 +939,25 @@ program }); program - .command('encrypt-wallet [new-phrase]') - .description('Encrypt wallet or update passphrase') - .action(async (passphrase, newPassphrase) => { - try { - db_wallet_enc.setPassphrase(passphrase); - const unencryptedWallet = db_wallet.loadWallet(); - const encryptedWallet = db_wallet_enc.loadWallet(); - - if (!newPassphrase) { - if (encryptedWallet !== null) { - console.error('Encrypted wallet-enc.json already exists'); - } else if (unencryptedWallet === null) { - if (!keymasterPassphrase) { - await keymaster.start({ - gatekeeper: gatekeeper_sdk, - wallet: db_wallet_enc, - cipher, - }); - } - await keymaster.newWallet(); - } + .command('encrypt-wallet') + .description('Encrypt wallet') + .action(async () => { + try { + if (!keymasterPassphrase) { + console.error('KC_ENCRYPTED_PASSPHRASE not set'); + return; + } + + db_wallet_enc.setPassphrase(keymasterPassphrase); + const wallet = db_wallet.loadWallet(); + + if (wallet === null) { + await keymaster.newWallet(); } else { - if (encryptedWallet === null) { - console.error('No encrypted wallet exists to change passphrase'); - return; + const result = db_wallet_enc.saveWallet(wallet); + if (!result) { + console.error('Encrypted wallet file already exists'); } - db_wallet_enc.setPassphrase(newPassphrase); - db_wallet_enc.saveWallet(encryptedWallet, true); } } catch (error) { console.error(error.message);