From 92023e433bc80c3d7c31cd1c36587e03b6986a9b Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Tue, 10 Dec 2024 13:49:05 -0500 Subject: [PATCH 01/10] Added encrypted wallet support to services --- docker-compose.yml | 3 +++ services/keymaster/server/src/config.js | 1 + .../keymaster/server/src/keymaster-api.js | 20 +++++++++++++++---- services/mediators/satoshi/src/config.js | 1 + .../mediators/satoshi/src/satoshi-mediator.js | 10 +++++++++- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 603f7b52..fc3497f1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,6 +44,7 @@ services: environment: - KC_KEYMASTER_PORT=4226 - KC_GATEKEEPER_URL=http://gatekeeper:4224 + - KC_ENCRYPTED_PASSPHRASE=${KC_ENCRYPTED_PASSPHRASE} volumes: - ./data:/app/keymaster/data user: "${KC_UID}:${KC_GID}" @@ -82,6 +83,7 @@ services: environment: - KC_GATEKEEPER_URL=http://gatekeeper:4224 - KC_NODE_ID=${KC_NODE_ID} + - KC_ENCRYPTED_PASSPHRASE=${KC_ENCRYPTED_PASSPHRASE} - KC_SAT_CHAIN=TFTC - KC_SAT_NETWORK=testnet - KC_SAT_START_BLOCK=0 @@ -116,6 +118,7 @@ services: environment: - KC_GATEKEEPER_URL=http://gatekeeper:4224 - KC_NODE_ID=${KC_NODE_ID} + - KC_ENCRYPTED_PASSPHRASE=${KC_ENCRYPTED_PASSPHRASE} - KC_SAT_CHAIN=TBTC - KC_SAT_NETWORK=testnet - KC_SAT_HOST=tbtc-node diff --git a/services/keymaster/server/src/config.js b/services/keymaster/server/src/config.js index 7a7ed1d8..50775c9c 100644 --- a/services/keymaster/server/src/config.js +++ b/services/keymaster/server/src/config.js @@ -5,6 +5,7 @@ dotenv.config(); const config = { gatekeeperURL: process.env.KC_GATEKEEPER_URL || 'http://localhost:4224', keymasterPort: process.env.KC_KEYMASTER_PORT ? parseInt(process.env.KC_KEYMASTER_PORT) : 4226, + keymasterPassphrase: process.env.KC_ENCRYPTED_PASSPHRASE, }; export default config; diff --git a/services/keymaster/server/src/keymaster-api.js b/services/keymaster/server/src/keymaster-api.js index 0b977bf1..affd01fb 100644 --- a/services/keymaster/server/src/keymaster-api.js +++ b/services/keymaster/server/src/keymaster-api.js @@ -4,9 +4,11 @@ import path from 'path'; import { fileURLToPath } from 'url'; import * as gatekeeper from '@mdip/gatekeeper/sdk'; import * as keymaster from '@mdip/keymaster/lib'; -import * as wallet from '@mdip/keymaster/db/json'; +import * as wallet_json from '@mdip/keymaster/db/json'; +import * as wallet_enc from '@mdip/keymaster/db/json/enc'; import * as cipher from '@mdip/cipher/node'; import config from './config.js'; +import { exit } from 'process'; const app = express(); const v1router = express.Router(); @@ -688,15 +690,25 @@ app.listen(port, async () => { intervalSeconds: 5, chatty: true, }); + + let wallet = wallet_json; + + if (config.keymasterPassphrase) { + wallet_enc.setPassphrase(config.keymasterPassphrase); + wallet = wallet_enc; + } + await keymaster.start({ gatekeeper, wallet, cipher }); console.log(`keymaster server running on port ${port}`); try { const currentId = await keymaster.getCurrentId(); - const doc = await keymaster.resolveId(); - console.log(`current ID: ${currentId}`); - console.log(JSON.stringify(doc, null, 4)); + if (currentId) { + console.log(`current ID: ${currentId}`); + const doc = await keymaster.resolveId(); + console.log(JSON.stringify(doc, null, 4)); + } serverReady = true; } catch (error) { diff --git a/services/mediators/satoshi/src/config.js b/services/mediators/satoshi/src/config.js index a74abd78..67366e43 100644 --- a/services/mediators/satoshi/src/config.js +++ b/services/mediators/satoshi/src/config.js @@ -5,6 +5,7 @@ dotenv.config(); const config = { nodeID: process.env.KC_NODE_ID, gatekeeperURL: process.env.KC_GATEKEEPER_URL || 'http://localhost:4224', + keymasterPassphrase: process.env.KC_ENCRYPTED_PASSPHRASE, chain: process.env.KC_SAT_CHAIN || 'BTC', network: process.env.KC_SAT_NETWORK || 'mainnet', host: process.env.KC_SAT_HOST || 'localhost', diff --git a/services/mediators/satoshi/src/satoshi-mediator.js b/services/mediators/satoshi/src/satoshi-mediator.js index e8f10e62..48e1a5e4 100644 --- a/services/mediators/satoshi/src/satoshi-mediator.js +++ b/services/mediators/satoshi/src/satoshi-mediator.js @@ -2,7 +2,8 @@ import fs from 'fs'; import BtcClient from 'bitcoin-core'; import * as gatekeeper from '@mdip/gatekeeper/sdk'; import * as keymaster from '@mdip/keymaster/lib'; -import * as wallet from '@mdip/keymaster/db/json'; +import * as wallet_json from '@mdip/keymaster/db/json'; +import * as wallet_enc from '@mdip/keymaster/db/json/enc'; import * as cipher from '@mdip/cipher/node'; import config from './config.js'; import { InvalidParameterError } from '@mdip/common/errors'; @@ -501,6 +502,13 @@ async function main() { chatty: true, }); + let wallet = wallet_json; + + if (config.keymasterPassphrase) { + wallet_enc.setPassphrase(config.keymasterPassphrase); + wallet = wallet_enc; + } + await keymaster.start({ gatekeeper, wallet, cipher }); await waitForNodeID(); From aeefe305a9a455ac131dcef3956ec193835b7e07 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Tue, 10 Dec 2024 15:07:13 -0500 Subject: [PATCH 02/10] Encrypt wallet in place --- packages/keymaster/src/db-wallet-json-enc.js | 2 +- packages/keymaster/src/db-wallet-json.js | 2 +- scripts/keychain-cli.js | 15 +++++++++------ services/keymaster/server/src/keymaster-api.js | 1 - tests/keymaster.test.js | 4 ++-- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/packages/keymaster/src/db-wallet-json-enc.js b/packages/keymaster/src/db-wallet-json-enc.js index 65422935..92bdef8f 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-enc.json`; +const walletName = `${dataFolder}/wallet.json`; const algorithm = 'aes-256-cbc'; // Algorithm const keyLength = 32; // 256 bit AES-256 diff --git a/packages/keymaster/src/db-wallet-json.js b/packages/keymaster/src/db-wallet-json.js index 196fb499..22391f9c 100644 --- a/packages/keymaster/src/db-wallet-json.js +++ b/packages/keymaster/src/db-wallet-json.js @@ -25,7 +25,7 @@ export function loadWallet() { const walletData = JSON.parse(walletJson); if (walletData && walletData.salt && walletData.iv && walletData.data) { - throw new Error('Wallet encrypted but KC_ENCRYPTED_PASSPHRASE not set'); + throw new Error('Wallet is encrypted'); } return walletData; diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index 05e289e3..858726fd 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -5,7 +5,7 @@ import dotenv from 'dotenv'; 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_json from '@mdip/keymaster/db/json'; import * as db_wallet_enc from '@mdip/keymaster/db/json/enc'; import * as cipher from '@mdip/cipher/node'; @@ -949,14 +949,17 @@ program } db_wallet_enc.setPassphrase(keymasterPassphrase); - const wallet = db_wallet.loadWallet(); + const wallet = db_wallet_json.loadWallet(); if (wallet === null) { await keymaster.newWallet(); } else { - const result = db_wallet_enc.saveWallet(wallet); - if (!result) { - console.error('Encrypted wallet file already exists'); + const ok = db_wallet_enc.saveWallet(wallet, true); + if (ok) { + console.log(UPDATE_OK); + } + else { + console.log(UPDATE_FAILED); } } } catch (error) { @@ -1007,7 +1010,7 @@ function getDBWallet() { db_wallet_enc.setPassphrase(keymasterPassphrase); return db_wallet_enc; } - return db_wallet; + return db_wallet_json; } async function run() { diff --git a/services/keymaster/server/src/keymaster-api.js b/services/keymaster/server/src/keymaster-api.js index affd01fb..8536fcab 100644 --- a/services/keymaster/server/src/keymaster-api.js +++ b/services/keymaster/server/src/keymaster-api.js @@ -8,7 +8,6 @@ import * as wallet_json from '@mdip/keymaster/db/json'; import * as wallet_enc from '@mdip/keymaster/db/json/enc'; import * as cipher from '@mdip/cipher/node'; import config from './config.js'; -import { exit } from 'process'; const app = express(); const v1router = express.Router(); diff --git a/tests/keymaster.test.js b/tests/keymaster.test.js index cb06a70a..4b267ece 100644 --- a/tests/keymaster.test.js +++ b/tests/keymaster.test.js @@ -138,7 +138,7 @@ describe('loadWallet', () => { throw new ExpectedExceptionError(); } catch (error) { expect(ok).toBe(true); - expect(error.message).toBe('Wallet encrypted but KC_ENCRYPTED_PASSPHRASE not set'); + expect(error.message).toBe('Wallet is encrypted'); } }); @@ -301,7 +301,7 @@ describe('saveWallet', () => { 'data': {} }); - const walletFile = 'data/wallet-enc.json'; + const walletFile = 'data/wallet.json'; const mockWallet = { mock: 1 }; fs.writeFileSync(walletFile, JSON.stringify(mockWallet, null, 4)); From 1318582c159a8f5735e070fa32f583500a8cfd8a Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Thu, 12 Dec 2024 14:57:58 +0000 Subject: [PATCH 03/10] feat: refactor wallet to use decorator pattern for optional encryption (#479) * feat: refactor wallet to use decorator pattern for optional encryption * Improve coverage --- packages/keymaster/src/db-wallet-json-enc.js | 33 +++++---------- packages/keymaster/src/db-wallet-json.js | 9 +--- sample.env | 2 +- scripts/keychain-cli.js | 43 ++++++++++++++----- tests/keymaster.test.js | 44 +++++++++++++------- 5 files changed, 74 insertions(+), 57 deletions(-) diff --git a/packages/keymaster/src/db-wallet-json-enc.js b/packages/keymaster/src/db-wallet-json-enc.js index 92bdef8f..1398b8c0 100644 --- a/packages/keymaster/src/db-wallet-json-enc.js +++ b/packages/keymaster/src/db-wallet-json-enc.js @@ -1,9 +1,5 @@ -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 @@ -11,21 +7,18 @@ const saltLength = 16; // 128-bit salt const iterations = 200000; // PBKDF2 iterations const digest = 'sha512'; // PBKDF2 hash function +let baseWallet; let passphrase; 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 }); - } +export function setWallet(wallet) { + baseWallet = wallet; +} +export function saveWallet(wallet, overwrite = false) { if (!passphrase) { throw new Error('KC_ENCRYPTED_PASSPHRASE not set'); } @@ -45,24 +38,20 @@ export function saveWallet(wallet, overwrite = false) { data: encrypted }; - fs.writeFileSync(walletName, JSON.stringify(encryptedData, null, 4)); - - return true; + return baseWallet.saveWallet(encryptedData, overwrite); } 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); + const encryptedData = baseWallet.loadWallet(); + if (!encryptedData) { + return null; + } - if (!encryptedData || !encryptedData.salt || !encryptedData.iv || !encryptedData.data) { + if (!encryptedData.salt || !encryptedData.iv || !encryptedData.data) { throw new Error('Wallet not encrypted'); } diff --git a/packages/keymaster/src/db-wallet-json.js b/packages/keymaster/src/db-wallet-json.js index 22391f9c..efc703ca 100644 --- a/packages/keymaster/src/db-wallet-json.js +++ b/packages/keymaster/src/db-wallet-json.js @@ -22,11 +22,6 @@ export function loadWallet() { } const walletJson = fs.readFileSync(walletName); - const walletData = JSON.parse(walletJson); - - if (walletData && walletData.salt && walletData.iv && walletData.data) { - throw new Error('Wallet is encrypted'); - } - - return walletData; + + return JSON.parse(walletJson); } diff --git a/sample.env b/sample.env index 93000ecb..9290e878 100644 --- a/sample.env +++ b/sample.env @@ -12,7 +12,7 @@ KC_GATEKEEPER_PORT=4224 KC_GATEKEEPER_GC_INTERVAL=60 # Wallet -KC_ENCRYPTED_WALLET=false +# KC_ENCRYPTED_PASSPHRASE= # CLI KC_GATEKEEPER_URL=http://localhost:4224 diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index 858726fd..41e712f9 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -948,20 +948,37 @@ program return; } - db_wallet_enc.setPassphrase(keymasterPassphrase); - const wallet = db_wallet_json.loadWallet(); + let wallet = db_wallet_json.loadWallet(); + if (wallet && (wallet.salt && wallet.iv && wallet.data)) { + console.error('Wallet already encrypted'); + return; + } if (wallet === null) { + await keymaster.start({ + gatekeeper: gatekeeper_sdk, + wallet: db_wallet_json, + cipher, + }); await keymaster.newWallet(); - } else { - const ok = db_wallet_enc.saveWallet(wallet, true); - if (ok) { - console.log(UPDATE_OK); - } - else { - console.log(UPDATE_FAILED); + wallet = db_wallet_json.loadWallet(); + + if (wallet === null) { + console.error('Failed to create new wallet'); + return; } } + + db_wallet_enc.setPassphrase(keymasterPassphrase); + db_wallet_enc.setWallet(db_wallet_json); + + const ok = db_wallet_enc.saveWallet(wallet, true); + if (ok) { + console.log(UPDATE_OK); + } + else { + console.log(UPDATE_FAILED); + } } catch (error) { console.error(error.message); } @@ -1006,11 +1023,15 @@ program }); function getDBWallet() { + let wallet = db_wallet_json; + if (keymasterPassphrase) { db_wallet_enc.setPassphrase(keymasterPassphrase); - return db_wallet_enc; + db_wallet_enc.setWallet(wallet); + wallet = db_wallet_enc; } - return db_wallet_json; + + return wallet; } async function run() { diff --git a/tests/keymaster.test.js b/tests/keymaster.test.js index 4b267ece..aafbb499 100644 --- a/tests/keymaster.test.js +++ b/tests/keymaster.test.js @@ -96,6 +96,7 @@ describe('loadWallet', () => { afterEach(async () => { mockFs.restore(); wallet_enc.setPassphrase(undefined); + wallet_enc.setWallet(undefined); await keymaster.start({ gatekeeper, wallet, cipher }); }); @@ -123,28 +124,17 @@ describe('loadWallet', () => { 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 }; - - const ok = await keymaster.saveWallet(mockWallet); + wallet_enc.setPassphrase('passphrase'); + wallet_enc.setWallet(wallet); - try { - await keymaster.loadWallet(); - throw new ExpectedExceptionError(); - } catch (error) { - expect(ok).toBe(true); - expect(error.message).toBe('Wallet is encrypted'); - } + const check_wallet = wallet_enc.loadWallet(); + expect(check_wallet).toBe(null); }); it('wallet should throw when passphrase not set', async () => { mockFs({}); const mockWallet = { mock: 1 }; + wallet_enc.setWallet(wallet); await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); @@ -167,6 +157,7 @@ describe('loadWallet', () => { await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); + wallet_enc.setWallet(wallet); wallet_enc.setPassphrase('passphrase'); const ok = await keymaster.saveWallet(mockWallet); @@ -191,6 +182,25 @@ describe('saveWallet', () => { await keymaster.start({ gatekeeper, wallet, cipher }); }); + it('test saving directly on the unencrypted wallet', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + const ok = wallet.saveWallet(mockWallet); + expect(ok).toBe(true); + }); + + it('test saving directly on the encrypted wallet', async () => { + mockFs({}); + const mockWallet = { mock: 0 }; + + wallet_enc.setWallet(wallet); + wallet_enc.setPassphrase('passphrase'); + + const ok = wallet_enc.saveWallet(mockWallet); + expect(ok).toBe(true); + }); + it('should save a wallet', async () => { mockFs({}); const mockWallet = { mock: 0 }; @@ -270,6 +280,7 @@ describe('saveWallet', () => { mockFs({}); await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); + wallet_enc.setWallet(wallet); wallet_enc.setPassphrase('passphrase'); const mockWallet1 = { mock: 1 }; @@ -306,6 +317,7 @@ describe('saveWallet', () => { fs.writeFileSync(walletFile, JSON.stringify(mockWallet, null, 4)); await keymaster.start({ gatekeeper, wallet: wallet_enc, cipher }); + wallet_enc.setWallet(wallet); wallet_enc.setPassphrase('passphrase'); try { From e52447d56f7ae27c78696f36af23a926578677c0 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Fri, 13 Dec 2024 05:27:26 +0000 Subject: [PATCH 04/10] feat: Python pip keymaster-sdk package (#469) * Initial work on a Python pip keymaster-sdk package * Update to the keymaster-sdk package * Restore performance test --- python/.flake8 | 8 + python/config.py | 1 - python/keymaster_sdk.py | 65 --- python/{ => keymaster_sdk}/.gitignore | 2 + python/keymaster_sdk/LICENSE | 21 + python/keymaster_sdk/README.md | 26 + python/keymaster_sdk/pyproject.toml | 20 + .../src/keymaster_sdk/__init__.py | 68 +++ .../src/keymaster_sdk/keymaster_sdk.py | 545 ++++++++++++++++++ .../tests/performance_test.py} | 13 +- .../keymaster_sdk/tests/test_keymaster_sdk.py | 375 ++++++++++++ python/requirements.txt | 1 - 12 files changed, 1072 insertions(+), 73 deletions(-) create mode 100644 python/.flake8 delete mode 100644 python/config.py delete mode 100644 python/keymaster_sdk.py rename python/{ => keymaster_sdk}/.gitignore (53%) create mode 100644 python/keymaster_sdk/LICENSE create mode 100644 python/keymaster_sdk/README.md create mode 100644 python/keymaster_sdk/pyproject.toml create mode 100644 python/keymaster_sdk/src/keymaster_sdk/__init__.py create mode 100644 python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py rename python/{test.py => keymaster_sdk/tests/performance_test.py} (76%) create mode 100644 python/keymaster_sdk/tests/test_keymaster_sdk.py delete mode 100644 python/requirements.txt diff --git a/python/.flake8 b/python/.flake8 new file mode 100644 index 00000000..64290313 --- /dev/null +++ b/python/.flake8 @@ -0,0 +1,8 @@ +[flake8] +exclude = + __init__.py, + .git, + */.venv/, + */build/, + */dist/ +max-line-length = 88 diff --git a/python/config.py b/python/config.py deleted file mode 100644 index ba0d3312..00000000 --- a/python/config.py +++ /dev/null @@ -1 +0,0 @@ -KEYMASTER_URL = 'http://localhost:4226' diff --git a/python/keymaster_sdk.py b/python/keymaster_sdk.py deleted file mode 100644 index 9e9fbf08..00000000 --- a/python/keymaster_sdk.py +++ /dev/null @@ -1,65 +0,0 @@ -import requests -from config import KEYMASTER_URL - -KEYMASTER_API = KEYMASTER_URL + "/api/v1" - -class KeymasterError(Exception): - """An error occurred while communicating with the Keymaster API.""" - -def proxy_request(method, url, **kwargs): - """ - Send a request to the specified URL and handle any HTTP errors. - - Args: - method (str): The HTTP method to use for the request. - url (str): The URL to send the request to. - **kwargs: Additional arguments to pass to `requests.request`. - - Returns: - dict: The JSON response from the server. - - Raises: - HTTPException: If the request fails, with the status code and response text from the server. - """ - try: - response = requests.request(method, url, **kwargs) - response.raise_for_status() - return response.json() - except requests.HTTPError as e: - raise KeymasterError(f"Error {e.response.status_code}: {e.response.text}") - -def isReady(): - response = proxy_request('GET', f'{KEYMASTER_API}/ready') - return response['ready'] - -def getCurrendId(): - response = proxy_request('GET', f'{KEYMASTER_API}/ids/current') - return response['current'] - -def listIds(): - response = proxy_request('GET', f'{KEYMASTER_API}/ids') - return response['ids'] - -def resolveId(id): - response = proxy_request('GET', f'{KEYMASTER_API}/ids/{id}') - return response['docs'] - -def createSchema(schema, options={}): - response = proxy_request('POST', f'{KEYMASTER_API}/schemas', json={"schema": schema, "options": options}) - return response['did'] - -def createTemplate(schema): - response = proxy_request('POST', f'{KEYMASTER_API}/schemas/did/template', json={"schema": schema}) - return response['template'] - -def bindCredential(schema, subject, options={}): - response = proxy_request('POST', f'{KEYMASTER_API}/credentials/bind', json={"schema": schema, "subject": subject, "options": options}) - return response['credential'] - -def issueCredential(credential, options={}): - response = proxy_request('POST', f'{KEYMASTER_API}/credentials/issued', json={"credential": credential, "options": options}) - return response['did'] - -def decryptJSON(did): - response = proxy_request('POST', f'{KEYMASTER_API}/keys/decrypt/json', json={"did": did}) - return response['json'] diff --git a/python/.gitignore b/python/keymaster_sdk/.gitignore similarity index 53% rename from python/.gitignore rename to python/keymaster_sdk/.gitignore index c85233c5..e6ec0834 100644 --- a/python/.gitignore +++ b/python/keymaster_sdk/.gitignore @@ -1,2 +1,4 @@ __pycache__ **/*.pyc +*.egg-info/ +dist/ diff --git a/python/keymaster_sdk/LICENSE b/python/keymaster_sdk/LICENSE new file mode 100644 index 00000000..5c08e122 --- /dev/null +++ b/python/keymaster_sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 MDIP + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/python/keymaster_sdk/README.md b/python/keymaster_sdk/README.md new file mode 100644 index 00000000..1cccb347 --- /dev/null +++ b/python/keymaster_sdk/README.md @@ -0,0 +1,26 @@ +# MDIP Keymaster + +Keymaster is a client library for the MDIP. It manages a wallet with any number of identities. + +### Installation + +```bash +pip install keymaster-sdk +``` + +### Requirements + +- Running keymaster instance + +### Usage + +```python +import keymaster_sdk as keymaster + +# Optional: URL defaults to http://localhost:4226 and can also +# be set using the environment variable KC_KEYMASTER_URL +keymaster.set_url('http://example.com:4226') + +ready = keymaster.is_ready() +print(f'Keymaster is ready: {ready}') +``` diff --git a/python/keymaster_sdk/pyproject.toml b/python/keymaster_sdk/pyproject.toml new file mode 100644 index 00000000..fb06186f --- /dev/null +++ b/python/keymaster_sdk/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "keymaster-sdk" +version = "0.1.0" +description = "Keymaster is a client library for the MDIP" +readme = "README.md" +license = { file = "LICENSE" } +authors = [ + { name="David McFadzean", email="david@selfid.com" }, + { name="Peter Bushnell", email="peter@selfid.com" } +] +dependencies = [ + "requests==2.32.0" +] + +[project.urls] +Homepage = "https://github.com/KeychainMDIP/kc/" diff --git a/python/keymaster_sdk/src/keymaster_sdk/__init__.py b/python/keymaster_sdk/src/keymaster_sdk/__init__.py new file mode 100644 index 00000000..dc8d31ba --- /dev/null +++ b/python/keymaster_sdk/src/keymaster_sdk/__init__.py @@ -0,0 +1,68 @@ +from .keymaster_sdk import ( + accept_credential, + add_group_member, + add_name, + add_signature, + backup_id, + backup_wallet, + bind_credential, + check_wallet, + create_id, + create_challenge, + create_group, + create_poll, + create_response, + create_schema, + create_template, + decrypt_json, + decrypt_message, + decrypt_mnemonic, + encrypt_json, + encrypt_message, + fix_wallet, + get_credential, + get_current_id, + get_group, + get_schema, + is_ready, + issue_credential, + KeymasterError, + list_credentials, + list_groups, + list_ids, + list_issued, + list_names, + list_registries, + list_schemas, + load_wallet, + new_wallet, + poll_template, + publish_credential, + publish_poll, + recover_id, + recover_wallet, + remove_credential, + remove_group_member, + remove_name, + remove_id, + resolve_asset, + resolve_did, + resolve_id, + revoke_credential, + rotate_keys, + set_current_id, + set_schema, + set_url, + save_wallet, + test_agent, + test_group, + test_schema, + update_credential, + update_poll, + unpublish_credential, + unpublish_poll, + verify_response, + verify_signature, + view_poll, + vote_poll, +) diff --git a/python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py b/python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py new file mode 100644 index 00000000..331c1674 --- /dev/null +++ b/python/keymaster_sdk/src/keymaster_sdk/keymaster_sdk.py @@ -0,0 +1,545 @@ +import os +import requests + +_base_url = os.environ.get("KC_KEYMASTER_URL", "http://localhost:4226") +_keymaster_api = _base_url + "/api/v1" + + +class KeymasterError(Exception): + """An error occurred while communicating with the Keymaster API.""" + + +def proxy_request(method, url, **kwargs): + """ + Send a request to the specified URL and handle any HTTP errors. + + Args: + method (str): The HTTP method to use for the request. + url (str): The URL to send the request to. + **kwargs: Additional arguments to pass to `requests.request`. + + Returns: + dict: The JSON response from the server. + + Raises: + HTTPException: If the request fails, with the status + code and response text from the server. + """ + try: + response = requests.request(method, url, **kwargs) + response.raise_for_status() + return response.json() + except requests.HTTPError as e: + raise KeymasterError(f"Error {e.response.status_code}: {e.response.text}") + + +def set_url(new_url: str): + global _base_url, _keymaster_api + _base_url = new_url + _keymaster_api = _base_url + "/api/v1" + + +def is_ready(): + response = proxy_request("GET", f"{_keymaster_api}/ready") + return response["ready"] + + +def create_id(name, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", f"{_keymaster_api}/ids", json={"name": name, "options": options} + ) + return response["did"] + + +def get_current_id(): + response = proxy_request("GET", f"{_keymaster_api}/ids/current") + return response["current"] + + +def set_current_id(name): + response = proxy_request( + "PUT", f"{_keymaster_api}/ids/current", json={"name": name} + ) + return response["ok"] + + +def remove_id(identifier): + response = proxy_request("DELETE", f"{_keymaster_api}/ids/{identifier}") + return response["ok"] + + +def backup_id(identifier): + response = proxy_request("POST", f"{_keymaster_api}/ids/{identifier}/backup") + return response["ok"] + + +def recover_id(did): + response = proxy_request( + "POST", f"{_keymaster_api}/ids/{did}/recover", json={"did": did} + ) + return response["recovered"] + + +def encrypt_message(msg, receiver, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/keys/encrypt/message", + json={"msg": msg, "receiver": receiver, "options": options}, + ) + return response["did"] + + +def decrypt_message(did): + response = proxy_request( + "POST", f"{_keymaster_api}/keys/decrypt/message", json={"did": did} + ) + return response["message"] + + +def list_ids(): + response = proxy_request("GET", f"{_keymaster_api}/ids") + return response["ids"] + + +def load_wallet(): + response = proxy_request("GET", f"{_keymaster_api}/wallet") + return response["wallet"] + + +def save_wallet(wallet): + response = proxy_request("PUT", f"{_keymaster_api}/wallet", json={"wallet": wallet}) + return response["ok"] + + +def backup_wallet(): + response = proxy_request( + "POST", + f"{_keymaster_api}/wallet/backup", + ) + return response["ok"] + + +def recover_wallet(): + response = proxy_request( + "POST", + f"{_keymaster_api}/wallet/recover", + ) + return response["wallet"] + + +def new_wallet(mnemonic, overwrite=False): + response = proxy_request( + "POST", + f"{_keymaster_api}/wallet/new", + json={"mnemonic": mnemonic, "overwrite": overwrite}, + ) + return response["wallet"] + + +def check_wallet(): + response = proxy_request( + "POST", + f"{_keymaster_api}/wallet/check", + ) + return response["check"] + + +def fix_wallet(): + response = proxy_request( + "POST", + f"{_keymaster_api}/wallet/fix", + ) + return response["fix"] + + +def decrypt_mnemonic(): + response = proxy_request( + "GET", + f"{_keymaster_api}/wallet/mnemonic", + ) + return response["mnemonic"] + + +def list_registries(): + response = proxy_request("GET", f"{_keymaster_api}/registries") + return response["registries"] + + +def resolve_id(identifier): + response = proxy_request("GET", f"{_keymaster_api}/ids/{identifier}") + return response["docs"] + + +def resolve_did(name): + response = proxy_request("GET", f"{_keymaster_api}/names/{name}") + return response["docs"] + + +def resolve_asset(name): + response = proxy_request("GET", f"{_keymaster_api}/assets/{name}") + return response["asset"] + + +def create_schema(schema, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", f"{_keymaster_api}/schemas", json={"schema": schema, "options": options} + ) + return response["did"] + + +def get_schema(identifier): + response = proxy_request("GET", f"{_keymaster_api}/schemas/{identifier}") + return response["schema"] + + +def set_schema(identifier, schema): + response = proxy_request( + "PUT", f"{_keymaster_api}/schemas/{identifier}", json={"schema": schema} + ) + return response["ok"] + + +def test_schema(identifier): + response = proxy_request("POST", f"{_keymaster_api}/schemas/{identifier}/test") + return response["test"] + + +def list_schemas(owner=None): + if owner is None: + owner = "" + response = proxy_request( + "GET", + f"{_keymaster_api}/schemas?owner={owner}", + ) + return response["schemas"] + + +def test_agent(identifier): + response = proxy_request("POST", f"{_keymaster_api}/agents/{identifier}/test") + return response["test"] + + +def create_template(schema): + response = proxy_request( + "POST", f"{_keymaster_api}/schemas/did/template", json={"schema": schema} + ) + return response["template"] + + +def bind_credential(schema, subject, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/credentials/bind", + json={"schema": schema, "subject": subject, "options": options}, + ) + return response["credential"] + + +def issue_credential(credential, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/credentials/issued", + json={"credential": credential, "options": options}, + ) + return response["did"] + + +def update_credential(did, credential): + response = proxy_request( + "POST", + f"{_keymaster_api}/credentials/issued/{did}", + json={"credential": credential}, + ) + return response["ok"] + + +def get_credential(did): + response = proxy_request( + "GET", + f"{_keymaster_api}/credentials/held/{did}", + ) + return response["credential"] + + +def list_credentials(): + response = proxy_request( + "GET", + f"{_keymaster_api}/credentials/held", + ) + return response["held"] + + +def publish_credential(did, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/credentials/held/{did}/publish", + json={"options": options}, + ) + return response["ok"] + + +def unpublish_credential(did): + response = proxy_request( + "POST", + f"{_keymaster_api}/credentials/held/{did}/unpublish", + ) + return response["ok"] + + +def remove_credential(did): + response = proxy_request( + "DELETE", + f"{_keymaster_api}/credentials/held/{did}", + ) + return response["ok"] + + +def revoke_credential(did): + response = proxy_request( + "DELETE", + f"{_keymaster_api}/credentials/issued/{did}", + ) + return response["ok"] + + +def list_issued(): + response = proxy_request( + "GET", + f"{_keymaster_api}/credentials/issued", + ) + return response["issued"] + + +def accept_credential(did): + response = proxy_request( + "POST", + f"{_keymaster_api}/credentials/held", + json={"did": did}, + ) + return response["ok"] + + +def decrypt_json(did): + response = proxy_request( + "POST", f"{_keymaster_api}/keys/decrypt/json", json={"did": did} + ) + return response["json"] + + +def encrypt_json(json, receiver, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/keys/encrypt/json", + json={"json": json, "receiver": receiver, "options": options}, + ) + return response["did"] + + +def list_names(): + response = proxy_request( + "GET", + f"{_keymaster_api}/names", + ) + return response["names"] + + +def add_name(name, did): + response = proxy_request( + "POST", f"{_keymaster_api}/names", json={"name": name, "did": did} + ) + return response["ok"] + + +def remove_name(name): + response = proxy_request("DELETE", f"{_keymaster_api}/names/{name}") + return response["ok"] + + +def create_challenge(challenge, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/challenge", + json={"challenge": challenge, "options": options}, + ) + return response["did"] + + +def create_response(challenge, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/response", + json={"challenge": challenge, "options": options}, + ) + return response["did"] + + +def verify_response(response, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/response/verify", + json={"response": response, "options": options}, + ) + return response["verify"] + + +def create_group(name, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/groups", + json={"name": name, "options": options}, + ) + return response["did"] + + +def get_group(group): + response = proxy_request( + "GET", + f"{_keymaster_api}/groups/{group}", + ) + return response["group"] + + +def add_group_member(group, member): + response = proxy_request( + "POST", + f"{_keymaster_api}/groups/{group}/add", + json={"group": group, "member": member}, + ) + return response["ok"] + + +def remove_group_member(group, member): + response = proxy_request( + "POST", + f"{_keymaster_api}/groups/{group}/remove", + json={"group": group, "member": member}, + ) + return response["ok"] + + +def test_group(group, member): + response = proxy_request( + "POST", + f"{_keymaster_api}/groups/{group}/test", + json={"group": group, "member": member}, + ) + return response["test"] + + +def list_groups(owner=None): + if owner is None: + owner = "" + response = proxy_request( + "GET", + f"{_keymaster_api}/groups?owner={owner}", + ) + return response["groups"] + + +def rotate_keys(): + response = proxy_request( + "POST", + f"{_keymaster_api}/keys/rotate", + ) + return response["ok"] + + +def add_signature(contents): + response = proxy_request( + "POST", + f"{_keymaster_api}/keys/sign", + json={"contents": contents}, + ) + return response["signed"] + + +def verify_signature(json): + response = proxy_request( + "POST", + f"{_keymaster_api}/keys/verify", + json={"json": json}, + ) + return response["ok"] + + +def poll_template(): + response = proxy_request( + "GET", + f"{_keymaster_api}/templates/poll", + ) + return response["template"] + + +def create_poll(poll, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/polls", + json={"poll": poll, "options": options}, + ) + return response["did"] + + +def view_poll(poll): + response = proxy_request( + "GET", + f"{_keymaster_api}/polls/{poll}/view", + ) + return response["poll"] + + +def vote_poll(poll, vote, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/polls/vote", + json={"poll": poll, "vote": vote, "options": options}, + ) + return response["did"] + + +def update_poll(ballot): + response = proxy_request( + "PUT", f"{_keymaster_api}/polls/update", json={"ballot": ballot} + ) + return response["ok"] + + +def publish_poll(poll, options=None): + if options is None: + options = {} + response = proxy_request( + "POST", + f"{_keymaster_api}/polls/{poll}/publish", + json={"poll": poll, "options": options}, + ) + return response["ok"] + + +def unpublish_poll(poll): + response = proxy_request("DELETE", f"{_keymaster_api}/polls/{poll}/unpublish") + return response["ok"] diff --git a/python/test.py b/python/keymaster_sdk/tests/performance_test.py similarity index 76% rename from python/test.py rename to python/keymaster_sdk/tests/performance_test.py index 2cf004cf..4dcd82be 100644 --- a/python/test.py +++ b/python/keymaster_sdk/tests/performance_test.py @@ -10,10 +10,10 @@ def main(): args = parser.parse_args() try: - ready = keymaster.isReady() + ready = keymaster.is_ready() print(f"Keymaster is ready: {ready}") - currentId = keymaster.getCurrendId() + currentId = keymaster.get_current_id() print(f"Current ID: {currentId}") expires = datetime.now(timezone.utc) + timedelta(minutes=1) @@ -23,19 +23,19 @@ def main(): 'validUntil': expires.isoformat() } - schema = keymaster.createSchema(None, test_options) + schema = keymaster.create_schema(None, test_options) - credential = keymaster.createTemplate(schema) + credential = keymaster.create_template(schema) print(json.dumps(credential, indent=4)) test_options['subject'] = currentId test_options['schema'] = schema for i in range(args.credentials): - vcDID = keymaster.issueCredential(credential, test_options) + vcDID = keymaster.issue_credential(credential, test_options) print(f"VC {i}: {vcDID}") - vc = keymaster.decryptJSON(vcDID) + vc = keymaster.decrypt_json(vcDID) print(json.dumps(vc, indent=4)) except Exception as e: @@ -43,3 +43,4 @@ def main(): if __name__ == "__main__": main() + diff --git a/python/keymaster_sdk/tests/test_keymaster_sdk.py b/python/keymaster_sdk/tests/test_keymaster_sdk.py new file mode 100644 index 00000000..6adfafde --- /dev/null +++ b/python/keymaster_sdk/tests/test_keymaster_sdk.py @@ -0,0 +1,375 @@ +import keymaster_sdk as keymaster +from datetime import datetime, timedelta, timezone +import random +import string + +# Test vars +expires = datetime.now(timezone.utc) + timedelta(minutes=1) +test_options = {"registry": "local", "validUntil": expires.isoformat()} +generated_ids = [] + + +# Tests +def test_isready(): + response = keymaster.is_ready() + assert_equal(response, True) + + +def test_ids(): + alice = generate_id() + alice_id = keymaster.create_id(alice) + + response = keymaster.test_agent(alice_id) + assert_equal(response, True) + + response = keymaster.set_current_id(alice) + assert_equal(response, True) + + response = keymaster.get_current_id() + assert_equal(response, alice) + + response = keymaster.resolve_id(alice) + assert_equal(response["didDocument"]["id"], alice_id) + + response = keymaster.list_ids() + assert alice in response, "expected ID not found in list_ids response" + + +def test_schemas(): + alice = generate_id() + alice_id = keymaster.create_id(alice) + keymaster.set_current_id(alice) + + did = keymaster.create_schema(None) + schema = keymaster.get_schema(did) + assert_equal(schema["$schema"], "http://json-schema.org/draft-07/schema#") + assert_equal(schema["type"], "object") + assert_equal(schema["properties"], {"propertyName": {"type": "string"}}) + assert_equal(schema["required"], ["propertyName"]) + + response = keymaster.list_schemas(alice_id) + assert_equal(response, [did]) + + response = keymaster.test_schema(did) + assert_equal(response, True) + + response = keymaster.set_schema(did, schema) + assert_equal(response, True) + + +def test_encrypt_decrypt_json(): + json = {"key": "value", "list": [1, 2, 3], "obj": {"name": "some object"}} + + alice = generate_id() + alice_id = keymaster.create_id(alice) + + did = keymaster.encrypt_json(json, alice_id) + data = keymaster.resolve_asset(did) + assert_equal(data["encrypted"]["sender"], alice_id) + + response = keymaster.decrypt_json(did) + assert_equal(response, json) + + +def test_issue_update_credentials(): + alice = generate_id() + keymaster.create_id(alice) + keymaster.set_current_id(alice) + + response = keymaster.list_credentials() + assert_equal(response, []) + + alice_id = keymaster.resolve_id(alice)["didDocument"]["id"] + schema = keymaster.create_schema(None, test_options) + credential = keymaster.create_template(schema) + assert_equal(credential["propertyName"], "TBD") + assert_equal(credential["$schema"], schema) + + options = { + **test_options, + "subject": alice, + "schema": schema, + } + + did = keymaster.issue_credential(credential, options) + vc = keymaster.get_credential(did) + assert_equal(vc["issuer"], alice_id) + assert_equal(vc["credentialSubject"]["id"], alice_id) + + response = keymaster.list_issued() + assert_equal(response, [did]) + + response = keymaster.decrypt_json(did) + assert_equal(response["type"], ["VerifiableCredential", schema]) + assert_equal(response["issuer"], alice_id) + assert_equal(response["credentialSubject"]["id"], alice_id) + + response = keymaster.update_credential(did, vc) + assert_equal(response, True) + + +def test_bind_credentials(): + alice = generate_id() + bob = generate_id() + keymaster.create_id(alice) + keymaster.create_id(bob) + keymaster.set_current_id(alice) + + alice_id = keymaster.resolve_id(alice)["didDocument"]["id"] + bob_id = keymaster.resolve_id(bob)["didDocument"]["id"] + schema = keymaster.create_schema(None, test_options) + + bc = keymaster.bind_credential(schema, bob, test_options) + assert_equal(bc["credentialSubject"]["id"], bob_id) + + did = keymaster.issue_credential(bc, test_options) + vc = keymaster.get_credential(did) + assert_equal(vc["issuer"], alice_id) + assert_equal(vc["credentialSubject"]["id"], bob_id) + + +def test_publish_credentials(): + bob = generate_id() + keymaster.create_id(bob) + bob_schema = keymaster.create_schema(None, test_options) + bc = keymaster.bind_credential(bob_schema, bob, test_options) + did = keymaster.issue_credential(bc, test_options) + identifier = keymaster.resolve_id(bob)["didDocument"]["id"] + + response = keymaster.publish_credential(did) + assert_equal(response["signature"]["signer"], identifier) + + response = keymaster.unpublish_credential(did) + assert_equal(response, f"OK credential {did} removed from manifest") + + +def test_accept_remove_revoke_credential(): + bob = generate_id() + keymaster.create_id(bob) + bob_schema = keymaster.create_schema(None, test_options) + bc = keymaster.bind_credential(bob_schema, bob, test_options) + did = keymaster.issue_credential(bc, test_options) + + response = keymaster.accept_credential(did) + assert_equal(response, True) + + response = keymaster.remove_credential(did) + assert_equal(response, True) + + response = keymaster.revoke_credential(did) + assert_equal(response, True) + + +def test_wallet(): + wallet = keymaster.load_wallet() + assert "seed" in wallet, "seed not present in wallet" + assert "mnemonic" in wallet["seed"], "mnemonic not present in wallet" + assert "hdkey" in wallet["seed"], "hdkey not present in wallet" + assert "xpriv" in wallet["seed"]["hdkey"], "xpriv not present in wallet" + + response = keymaster.save_wallet(wallet) + assert_equal(response, True) + + did = keymaster.backup_wallet() + doc = keymaster.resolve_did(did) + assert_equal(doc["didDocument"]["id"], did) + + mnemonic = keymaster.decrypt_mnemonic() + assert_equal(len(mnemonic.split()), 12) + + new_wallet = keymaster.new_wallet(mnemonic, True) + assert_equal(wallet["seed"]["hdkey"]["xpriv"], new_wallet["seed"]["hdkey"]["xpriv"]) + + recovered = keymaster.recover_wallet() + assert_equal(recovered, wallet) + + response = keymaster.check_wallet() + assert "checked" in response, "checked not present in check_wallet response" + + response = keymaster.fix_wallet() + assert "idsRemoved" in response, "idsRemoved not present in fix_wallet response" + + +def test_registeries(): + response = keymaster.list_registries() + assert ( + "hyperswarm" in response + ), "hyperswarm not present in list_registries response" + + +def test_backup_recover_id(): + alice = generate_id() + did = keymaster.create_id(alice) + + response = keymaster.backup_id(alice) + assert_equal(response, True) + + doc = keymaster.resolve_did(did) + vault = keymaster.resolve_did(doc["didDocumentData"]["vault"]) + assert len(vault["didDocumentData"]["backup"]) > 0, "backup not present in vault" + + keymaster.remove_id(generated_ids.pop()) + assert_equal(response, True) + + response = keymaster.list_ids() + assert alice not in response, "unexpected ID found in list_ids response" + + response = keymaster.recover_id(did) + assert_equal(response, alice) + + response = keymaster.list_ids() + assert alice in response, "expected ID not found in list_ids response" + + +def test_encrypt_decrypt_message(): + alice = generate_id() + bob = generate_id() + keymaster.create_id(alice) + bob_id = keymaster.create_id(bob) + keymaster.set_current_id(alice) + + msg = "Hi Bob" + + did = keymaster.encrypt_message(msg, bob_id) + response = keymaster.decrypt_message(did) + assert_equal(response, msg) + + keymaster.set_current_id(bob) + response = keymaster.decrypt_message(did) + assert_equal(response, msg) + + +def test_names(): + alice = generate_id() + alice_id = keymaster.create_id(alice) + + response = keymaster.remove_name("Bob") + + response = keymaster.add_name("Bob", alice_id) + assert_equal(response, True) + + response = keymaster.list_names() + assert "Bob" in response, "expected name not found in list_names response" + + response = keymaster.remove_name("Bob") + assert_equal(response, True) + + +def test_challenge_response(): + alice = generate_id() + alice_id = keymaster.create_id(alice) + keymaster.set_current_id(alice) + + challenge_did = keymaster.create_challenge({}) + doc = keymaster.resolve_did(challenge_did) + assert_equal(doc["didDocument"]["id"], challenge_did) + assert_equal(doc["didDocument"]["controller"], alice_id) + assert_equal(doc["didDocumentData"], {"challenge": {}}) + + bob = generate_id() + bob_id = keymaster.create_id(bob) + keymaster.set_current_id(bob) + + response_did = keymaster.create_response(challenge_did) + response = keymaster.decrypt_json(response_did) + assert_equal(response["response"]["challenge"], challenge_did) + assert_equal(response["response"]["credentials"], []) + + response = keymaster.verify_response(response_did) + assert_equal(response["challenge"], challenge_did) + assert_equal(response["responder"], bob_id) + + +def test_groups(): + alice = generate_id() + alice_id = keymaster.create_id(alice) + keymaster.set_current_id(alice) + + name = "test_group" + did = keymaster.create_group(name) + doc = keymaster.resolve_did(did) + assert_equal(doc["didDocument"]["id"], did) + assert_equal(doc["didDocument"]["controller"], alice_id) + + response = keymaster.list_groups() + assert did in response, "expected group not found in list_groups response" + + response = keymaster.add_group_member(did, alice_id) + assert_equal(response, True) + + response = keymaster.test_group(did, alice_id) + assert_equal(response, True) + + response = keymaster.get_group(did) + assert_equal(response["name"], name) + assert_equal(response["members"], [alice_id]) + + response = keymaster.remove_group_member(did, alice_id) + assert_equal(response, True) + + response = keymaster.get_group(did) + assert_equal(response["name"], name) + assert_equal(response["members"], []) + + +def test_rotate_keys(): + alice = generate_id() + keymaster.create_id(alice) + keymaster.set_current_id(alice) + + keymaster.rotate_keys() + wallet = keymaster.load_wallet() + assert_equal(wallet["ids"][alice]["index"], 1) + + +def test_signature(): + alice = generate_id() + keymaster.create_id(alice) + keymaster.set_current_id(alice) + + signed = keymaster.add_signature(str({})) + valid = keymaster.verify_signature(signed) + assert_equal(valid, True) + + +def test_polls(): + alice = generate_id() + alice_id = keymaster.create_id(alice) + keymaster.set_current_id(alice) + name = "test_group" + group = keymaster.create_group(name) + keymaster.add_group_member(group, alice_id) + + template = keymaster.poll_template() + template["roster"] = group + + poll = keymaster.create_poll(template) + ballot = keymaster.vote_poll(poll, 1) + response = keymaster.update_poll(ballot) + assert_equal(response, True) + response = keymaster.publish_poll(poll) + assert_equal(response, True) + response = keymaster.unpublish_poll(poll) + assert_equal(response, True) + response = keymaster.view_poll(poll) + assert_equal(response["results"]["ballots"][0]["voter"], alice_id) + + +def test_remove_ids(): + for identifier in generated_ids: + response = keymaster.remove_id(identifier) + assert_equal(response, True) + + +# Test and helper functions +def generate_id(): + + generated_ids.append( + "".join(random.choice(string.ascii_letters + string.digits) for _ in range(11)) + ) + return generated_ids[len(generated_ids) - 1] + + +def assert_equal(thing1, thing2): + if thing1 != thing2: + raise AssertionError(f"not({thing1} == {thing2})") diff --git a/python/requirements.txt b/python/requirements.txt deleted file mode 100644 index 2c24336e..00000000 --- a/python/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -requests==2.31.0 From 9c5a684d3ac468d7117875db1f02e70f92a032e3 Mon Sep 17 00:00:00 2001 From: Peter John Bushnell Date: Fri, 13 Dec 2024 14:13:28 +0000 Subject: [PATCH 05/10] fix: update api and mediator to set wallet when using encryption (#482) --- services/keymaster/server/src/keymaster-api.js | 1 + services/mediators/satoshi/src/satoshi-mediator.js | 1 + 2 files changed, 2 insertions(+) diff --git a/services/keymaster/server/src/keymaster-api.js b/services/keymaster/server/src/keymaster-api.js index 8536fcab..b813e9ad 100644 --- a/services/keymaster/server/src/keymaster-api.js +++ b/services/keymaster/server/src/keymaster-api.js @@ -694,6 +694,7 @@ app.listen(port, async () => { if (config.keymasterPassphrase) { wallet_enc.setPassphrase(config.keymasterPassphrase); + wallet_enc.setWallet(wallet); wallet = wallet_enc; } diff --git a/services/mediators/satoshi/src/satoshi-mediator.js b/services/mediators/satoshi/src/satoshi-mediator.js index 48e1a5e4..f2013092 100644 --- a/services/mediators/satoshi/src/satoshi-mediator.js +++ b/services/mediators/satoshi/src/satoshi-mediator.js @@ -506,6 +506,7 @@ async function main() { if (config.keymasterPassphrase) { wallet_enc.setPassphrase(config.keymasterPassphrase); + wallet_enc.setWallet(wallet); wallet = wallet_enc; } From e81f71c9d34933e5a9fd6b2a69f3e06531c08ae3 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Fri, 13 Dec 2024 09:45:04 -0500 Subject: [PATCH 06/10] feat: Added wallet cache (#477) * Added wallet cache * Added KC_WALLET_CACHE env var * Improved cache performance * Fix in admin script * Updated satoshi-mediator to use keymaster SDK * Added createAsset to keymaster SDK --- docker-compose.yml | 5 +++ packages/keymaster/package.json | 1 + packages/keymaster/src/db-wallet-cache.js | 19 +++++++++ packages/keymaster/src/keymaster-sdk.js | 10 +++++ sample.env | 5 ++- scripts/admin-cli.js | 2 +- scripts/keychain-cli.js | 7 ++++ services/keymaster/server/src/config.js | 1 + .../keymaster/server/src/keymaster-api.js | 10 ++++- services/mediators/satoshi/src/config.js | 1 + .../mediators/satoshi/src/satoshi-mediator.js | 39 ++++++++++++------- 11 files changed, 82 insertions(+), 18 deletions(-) create mode 100644 packages/keymaster/src/db-wallet-cache.js diff --git a/docker-compose.yml b/docker-compose.yml index fc3497f1..b5fca886 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -45,6 +45,7 @@ services: - KC_KEYMASTER_PORT=4226 - KC_GATEKEEPER_URL=http://gatekeeper:4224 - KC_ENCRYPTED_PASSPHRASE=${KC_ENCRYPTED_PASSPHRASE} + - KC_WALLET_CACHE=${KC_WALLET_CACHE} volumes: - ./data:/app/keymaster/data user: "${KC_UID}:${KC_GID}" @@ -82,6 +83,7 @@ services: image: keychainmdip/satoshi-mediator environment: - KC_GATEKEEPER_URL=http://gatekeeper:4224 + - KC_KEYMASTER_URL=http://keymaster:4226 - KC_NODE_ID=${KC_NODE_ID} - KC_ENCRYPTED_PASSPHRASE=${KC_ENCRYPTED_PASSPHRASE} - KC_SAT_CHAIN=TFTC @@ -104,6 +106,7 @@ services: depends_on: - tftc-node - gatekeeper + - keymaster tbtc-node: image: keychainmdip/bitcoin-core:v28.0 @@ -117,6 +120,7 @@ services: image: keychainmdip/satoshi-mediator environment: - KC_GATEKEEPER_URL=http://gatekeeper:4224 + - KC_KEYMASTER_URL=http://keymaster:4226 - KC_NODE_ID=${KC_NODE_ID} - KC_ENCRYPTED_PASSPHRASE=${KC_ENCRYPTED_PASSPHRASE} - KC_SAT_CHAIN=TBTC @@ -139,6 +143,7 @@ services: depends_on: - tbtc-node - gatekeeper + - keymaster ipfs-mediator: build: diff --git a/packages/keymaster/package.json b/packages/keymaster/package.json index dc556262..0d5d5b7d 100644 --- a/packages/keymaster/package.json +++ b/packages/keymaster/package.json @@ -8,6 +8,7 @@ "./sdk": "./src/keymaster-sdk.js", "./db/json": "./src/db-wallet-json.js", "./db/json/enc": "./src/db-wallet-json-enc.js", + "./db/cache": "./src/db-wallet-cache.js", "./db/web": "./src/db-wallet-web.js" }, "scripts": { diff --git a/packages/keymaster/src/db-wallet-cache.js b/packages/keymaster/src/db-wallet-cache.js new file mode 100644 index 00000000..247fd448 --- /dev/null +++ b/packages/keymaster/src/db-wallet-cache.js @@ -0,0 +1,19 @@ +let baseWallet; +let cachedWallet; + +export function setWallet(wallet) { + baseWallet = wallet; +} + +export function saveWallet(wallet, overwrite = false) { + cachedWallet = wallet; + return baseWallet.saveWallet(wallet, overwrite); +} + +export function loadWallet() { + if (!cachedWallet) { + cachedWallet = baseWallet.loadWallet(); + } + + return cachedWallet; +} diff --git a/packages/keymaster/src/keymaster-sdk.js b/packages/keymaster/src/keymaster-sdk.js index 2de66a52..bbbfa012 100644 --- a/packages/keymaster/src/keymaster-sdk.js +++ b/packages/keymaster/src/keymaster-sdk.js @@ -316,6 +316,16 @@ export async function resolveDID(name) { } } +export async function createAsset(data, options = {}) { + try { + const response = await axios.post(`${URL}/api/v1/assets`, { data, options }); + return response.data.did; + } + catch (error) { + throwError(error); + } +} + export async function resolveAsset(name) { try { const response = await axios.get(`${URL}/api/v1/assets/${name}`); diff --git a/sample.env b/sample.env index 9290e878..d59c42ee 100644 --- a/sample.env +++ b/sample.env @@ -11,8 +11,9 @@ KC_GATEKEEPER_REGISTRIES=hyperswarm,TBTC,TFTC KC_GATEKEEPER_PORT=4224 KC_GATEKEEPER_GC_INTERVAL=60 -# Wallet -# KC_ENCRYPTED_PASSPHRASE= +# Keymaster +KC_ENCRYPTED_PASSPHRASE= +KC_WALLET_CACHE=false # CLI KC_GATEKEEPER_URL=http://localhost:4224 diff --git a/scripts/admin-cli.js b/scripts/admin-cli.js index 5a7459ba..ccc4a7db 100644 --- a/scripts/admin-cli.js +++ b/scripts/admin-cli.js @@ -8,7 +8,7 @@ import * as wallet from '@mdip/keymaster/db/json'; import * as cipher from '@mdip/cipher/node'; dotenv.config(); -const gatekeeperURL = process.env.KC_CLI_GATEKEEPER_URL || 'http://localhost:4224'; +const gatekeeperURL = process.env.KC_GATEKEEPER_URL || 'http://localhost:4224'; program .version('1.0.0') diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index 41e712f9..c7d72b1d 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -7,6 +7,7 @@ import * as keymaster_lib from '@mdip/keymaster/lib'; import * as keymaster_sdk from '@mdip/keymaster/sdk'; import * as db_wallet_json from '@mdip/keymaster/db/json'; import * as db_wallet_enc from '@mdip/keymaster/db/json/enc'; +import * as db_wallet_cache from '@mdip/keymaster/db/cache'; import * as cipher from '@mdip/cipher/node'; dotenv.config(); @@ -16,6 +17,7 @@ const gatekeeperURL = process.env.KC_GATEKEEPER_URL || 'http://localhost:4224'; const keymasterURL = process.env.KC_KEYMASTER_URL; const keymasterPassphrase = process.env.KC_ENCRYPTED_PASSPHRASE; +const walletCache = process.env.KC_WALLET_CACHE ? process.env.KC_WALLET_CACHE === 'true' : false; const UPDATE_OK = "OK"; const UPDATE_FAILED = "Update failed"; @@ -1031,6 +1033,11 @@ function getDBWallet() { wallet = db_wallet_enc; } + if (walletCache) { + db_wallet_cache.setWallet(wallet); + wallet = db_wallet_cache; + } + return wallet; } diff --git a/services/keymaster/server/src/config.js b/services/keymaster/server/src/config.js index 50775c9c..ebc6cd9c 100644 --- a/services/keymaster/server/src/config.js +++ b/services/keymaster/server/src/config.js @@ -6,6 +6,7 @@ const config = { gatekeeperURL: process.env.KC_GATEKEEPER_URL || 'http://localhost:4224', keymasterPort: process.env.KC_KEYMASTER_PORT ? parseInt(process.env.KC_KEYMASTER_PORT) : 4226, keymasterPassphrase: process.env.KC_ENCRYPTED_PASSPHRASE, + walletCache: process.env.KC_WALLET_CACHE ? process.env.KC_WALLET_CACHE === 'true' : false, }; export default config; diff --git a/services/keymaster/server/src/keymaster-api.js b/services/keymaster/server/src/keymaster-api.js index b813e9ad..0d1d11be 100644 --- a/services/keymaster/server/src/keymaster-api.js +++ b/services/keymaster/server/src/keymaster-api.js @@ -6,6 +6,7 @@ import * as gatekeeper from '@mdip/gatekeeper/sdk'; import * as keymaster from '@mdip/keymaster/lib'; import * as wallet_json from '@mdip/keymaster/db/json'; import * as wallet_enc from '@mdip/keymaster/db/json/enc'; +import * as wallet_cache from '@mdip/keymaster/db/cache'; import * as cipher from '@mdip/cipher/node'; import config from './config.js'; const app = express(); @@ -574,8 +575,8 @@ v1router.post('/schemas/:id/template/', async (req, res) => { v1router.post('/assets/', async (req, res) => { try { - const { asset, options } = req.body; - const did = await keymaster.createAsset(asset, options); + const { data, options } = req.body; + const did = await keymaster.createAsset(data, options); res.json({ did }); } catch (error) { res.status(500).send({ error: error.toString() }); @@ -698,6 +699,11 @@ app.listen(port, async () => { wallet = wallet_enc; } + if (config.walletCache) { + wallet_cache.setWallet(wallet); + wallet = wallet_cache; + } + await keymaster.start({ gatekeeper, wallet, cipher }); console.log(`keymaster server running on port ${port}`); diff --git a/services/mediators/satoshi/src/config.js b/services/mediators/satoshi/src/config.js index 67366e43..567f8f2f 100644 --- a/services/mediators/satoshi/src/config.js +++ b/services/mediators/satoshi/src/config.js @@ -5,6 +5,7 @@ dotenv.config(); const config = { nodeID: process.env.KC_NODE_ID, gatekeeperURL: process.env.KC_GATEKEEPER_URL || 'http://localhost:4224', + keymasterURL: process.env.KC_KEYMASTER_URL, keymasterPassphrase: process.env.KC_ENCRYPTED_PASSPHRASE, chain: process.env.KC_SAT_CHAIN || 'BTC', network: process.env.KC_SAT_NETWORK || 'mainnet', diff --git a/services/mediators/satoshi/src/satoshi-mediator.js b/services/mediators/satoshi/src/satoshi-mediator.js index f2013092..63710ba1 100644 --- a/services/mediators/satoshi/src/satoshi-mediator.js +++ b/services/mediators/satoshi/src/satoshi-mediator.js @@ -1,7 +1,8 @@ import fs from 'fs'; import BtcClient from 'bitcoin-core'; import * as gatekeeper from '@mdip/gatekeeper/sdk'; -import * as keymaster from '@mdip/keymaster/lib'; +import * as keymaster_lib from '@mdip/keymaster/lib'; +import * as keymaster_sdk from '@mdip/keymaster/sdk'; import * as wallet_json from '@mdip/keymaster/db/json'; import * as wallet_enc from '@mdip/keymaster/db/json/enc'; import * as cipher from '@mdip/cipher/node'; @@ -9,6 +10,7 @@ import config from './config.js'; import { InvalidParameterError } from '@mdip/common/errors'; const REGISTRY = config.chain; +let keymaster; const client = new BtcClient({ network: config.network, @@ -132,8 +134,6 @@ async function importBatch(item) { return; } - console.log(JSON.stringify(item, null, 4)); - const batch = []; for (let i = 0; i < queue.length; i++) { @@ -151,8 +151,6 @@ async function importBatch(item) { }); } - // console.log(JSON.stringify(batch, null, 4)); - try { item.imported = await gatekeeper.importBatch(batch); item.processed = await gatekeeper.processEvents(); @@ -338,9 +336,10 @@ async function anchorBatch() { } const batch = await gatekeeper.getQueue(REGISTRY); - console.log(JSON.stringify(batch, null, 4)); if (batch.length > 0) { + console.log(JSON.stringify(batch, null, 4)); + const did = await keymaster.createAsset({ batch }, { registry: 'hyperswarm', controller: config.nodeID }); const txid = await createOpReturnTxn(did); @@ -367,7 +366,7 @@ async function anchorBatch() { } } else { - console.log('empty batch'); + console.log(`empty ${REGISTRY} queue`); } } @@ -502,15 +501,29 @@ async function main() { chatty: true, }); - let wallet = wallet_json; + if (config.keymasterURL) { + keymaster = keymaster_sdk; + await keymaster.start({ + url: config.keymasterURL, + waitUntilReady: true, + intervalSeconds: 5, + chatty: true, + }); + } + else { + keymaster = keymaster_lib; + + let wallet = wallet_json; + + if (config.keymasterPassphrase) { + wallet_enc.setPassphrase(config.keymasterPassphrase); + wallet_enc.setWallet(wallet); + wallet = wallet_enc; + } - if (config.keymasterPassphrase) { - wallet_enc.setPassphrase(config.keymasterPassphrase); - wallet_enc.setWallet(wallet); - wallet = wallet_enc; + await keymaster.start({ gatekeeper, wallet, cipher }); } - await keymaster.start({ gatekeeper, wallet, cipher }); await waitForNodeID(); if (config.importInterval > 0) { From 32d3cb593ce76cf29ef5883e6870aa7459d74f47 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Fri, 13 Dec 2024 11:39:03 -0500 Subject: [PATCH 07/10] fix: gatekeeper.start() handles multiple calls (#485) --- packages/gatekeeper/src/gatekeeper-sdk.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/gatekeeper/src/gatekeeper-sdk.js b/packages/gatekeeper/src/gatekeeper-sdk.js index 0140ea66..12e140c5 100644 --- a/packages/gatekeeper/src/gatekeeper-sdk.js +++ b/packages/gatekeeper/src/gatekeeper-sdk.js @@ -1,7 +1,7 @@ import axios from 'axios'; -let URL = ''; -let API = '/api/v1'; +const VERSION = '/api/v1'; +let API = VERSION; function throwError(error) { if (error.response) { @@ -13,8 +13,7 @@ function throwError(error) { export async function start(options = {}) { if (options.url) { - URL = options.url; - API = `${URL}${API}`; + API = `${options.url}${VERSION}`; } if (options.waitUntilReady) { From 47ccb56fc5127698009d82871237f3a69bf55150 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Fri, 13 Dec 2024 16:00:26 -0500 Subject: [PATCH 08/10] docs: Added CONTRIBUTING.md (#487) --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..a4f3ae05 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,16 @@ +# Contributing to the MDIP Keychain open source project + +1. [Create an issue](https://github.com/KeychainMDIP/kc/issues) first + - Requirements and acceptance criteria should be discussed in the issue +2. Create a development branch from the issue + - Branch name should start with issue number + - Github provides a link in the issue to [create the branch](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/creating-a-branch-for-an-issue) +3. Create a PR from the development branch + - All PRs must be linked to an issue +4. If the PR contains code changes then do a smoke test before merge + - Use `./start-node` to build and run the containers + - Make sure the node syncs OK + - Check the service logs for any errors + - An easy check is to run `./kc perf-test` (to create 100 local ephemeral credentials) followed by `./admin verify-db` (to garbage-collect the test credentials) +5. Merge the PR with squashed commits +6. Use [conventional commit](https://www.conventionalcommits.org/en/v1.0.0/) prefixes for the merge commit message From e0da84e8b421fa2112352bbb7857c491fb2006c6 Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Mon, 16 Dec 2024 15:18:56 -0500 Subject: [PATCH 09/10] feat: Added KC_MDIP_PROTOCOL to config (#489) --- docker-compose.yml | 1 + packages/gatekeeper/src/gatekeeper-sdk.js | 4 ++-- sample.env | 1 + services/mediators/hyperswarm/src/config.js | 1 + services/mediators/hyperswarm/src/hyperswarm-mediator.js | 5 ++--- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b5fca886..ed2e589b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,6 +62,7 @@ services: environment: - KC_GATEKEEPER_URL=http://gatekeeper:4224 - KC_NODE_NAME=${KC_NODE_NAME} + - KC_MDIP_PROTOCOL=${KC_MDIP_PROTOCOL} volumes: - ./data:/app/hyperswarm/data user: "${KC_UID}:${KC_GID}" diff --git a/packages/gatekeeper/src/gatekeeper-sdk.js b/packages/gatekeeper/src/gatekeeper-sdk.js index 12e140c5..979de106 100644 --- a/packages/gatekeeper/src/gatekeeper-sdk.js +++ b/packages/gatekeeper/src/gatekeeper-sdk.js @@ -27,7 +27,7 @@ async function waitUntilReady(options = {}) { let retries = 0; if (chatty) { - console.log(`Connecting to gatekeeper at ${URL}`); + console.log(`Connecting to gatekeeper at ${API}`); } while (!ready) { @@ -44,7 +44,7 @@ async function waitUntilReady(options = {}) { retries += 1; if (!chatty && becomeChattyAfter > 0 && retries > becomeChattyAfter) { - console.log(`Connecting to gatekeeper at ${URL}`); + console.log(`Connecting to gatekeeper at ${API}`); chatty = true; } } diff --git a/sample.env b/sample.env index d59c42ee..06c694a2 100644 --- a/sample.env +++ b/sample.env @@ -3,6 +3,7 @@ KC_UID=1000 KC_GID=1000 KC_NODE_NAME=mynodeName KC_NODE_ID=mynodeID +KC_MDIP_PROTOCOL= # Gatekeeper KC_DEBUG=false diff --git a/services/mediators/hyperswarm/src/config.js b/services/mediators/hyperswarm/src/config.js index 37e0463f..7382202b 100644 --- a/services/mediators/hyperswarm/src/config.js +++ b/services/mediators/hyperswarm/src/config.js @@ -6,6 +6,7 @@ const config = { debug: process.env.KC_DEBUG ? process.env.KC_DEBUG === 'true' : false, gatekeeperURL: process.env.KC_GATEKEEPER_URL || 'http://localhost:4224', nodeName: process.env.KC_NODE_NAME || 'anon', + protocol: process.env.KC_MDIP_PROTOCOL || '/MDIP/v24.11.19', }; export default config; diff --git a/services/mediators/hyperswarm/src/hyperswarm-mediator.js b/services/mediators/hyperswarm/src/hyperswarm-mediator.js index 1e3023b6..ca2d3371 100644 --- a/services/mediators/hyperswarm/src/hyperswarm-mediator.js +++ b/services/mediators/hyperswarm/src/hyperswarm-mediator.js @@ -14,7 +14,6 @@ EventEmitter.defaultMaxListeners = 100; const REGISTRY = 'hyperswarm'; const BATCH_SIZE = 100; -const PROTOCOL = '/MDIP/v24.11.19'; const nodes = {}; @@ -46,7 +45,7 @@ async function createSwarm() { await discovery.flushed(); const shortTopic = shortName(b4a.toString(topic, 'hex')); - console.log(`new hyperswarm peer id: ${shortName(peerName)} (${config.nodeName}) joined topic: ${shortTopic} using protocol: ${PROTOCOL}`); + console.log(`new hyperswarm peer id: ${shortName(peerName)} (${config.nodeName}) joined topic: ${shortTopic} using protocol: ${config.protocol}`); } async function addConnection(conn) { @@ -442,7 +441,7 @@ process.stdin.on('data', d => { }); // Join a common topic -const hash = sha256(PROTOCOL); +const hash = sha256(config.protocol); const networkID = Buffer.from(hash).toString('hex'); const topic = b4a.from(networkID, 'hex'); From 9ce8d7ff89581e00395e191db6bfa7d9ff39459e Mon Sep 17 00:00:00 2001 From: David McFadzean Date: Tue, 17 Dec 2024 11:59:36 -0500 Subject: [PATCH 10/10] fix: Confirmation delay on hyperswarm registry (#490) * Fix for create-challenge command * Test and fix for delayed confirm on hyperswarm * Fix lint errors --- packages/keymaster/src/keymaster-lib.js | 2 +- scripts/keychain-cli.js | 125 +++++++++--------- .../hyperswarm/src/hyperswarm-mediator.js | 2 +- tests/hypr-confirm.js | 58 ++++++++ tests/keymaster.test.js | 8 ++ 5 files changed, 130 insertions(+), 65 deletions(-) create mode 100644 tests/hypr-confirm.js diff --git a/packages/keymaster/src/keymaster-lib.js b/packages/keymaster/src/keymaster-lib.js index aeeae3a8..90039e01 100644 --- a/packages/keymaster/src/keymaster-lib.js +++ b/packages/keymaster/src/keymaster-lib.js @@ -1171,7 +1171,7 @@ export async function unpublishCredential(did) { export async function createChallenge(challenge = {}, options = {}) { - if (typeof challenge !== 'object' || Array.isArray(challenge)) { + if (!challenge || typeof challenge !== 'object' || Array.isArray(challenge)) { throw new InvalidParameterError('challenge'); } diff --git a/scripts/keychain-cli.js b/scripts/keychain-cli.js index c7d72b1d..bbb1abba 100644 --- a/scripts/keychain-cli.js +++ b/scripts/keychain-cli.js @@ -36,7 +36,7 @@ program console.log(JSON.stringify(wallet, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -55,7 +55,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -69,7 +69,7 @@ program console.log(`${idsRemoved} IDs and ${ownedRemoved} owned DIDs and ${heldRemoved} held DIDs and ${namesRemoved} names were removed`); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -82,7 +82,7 @@ program console.log(JSON.stringify(wallet, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -95,7 +95,7 @@ program console.log(JSON.stringify(wallet, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -108,7 +108,7 @@ program console.log(mnenomic); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -121,7 +121,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -134,7 +134,7 @@ program console.log(JSON.stringify(wallet, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -147,7 +147,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -161,7 +161,7 @@ program console.log(JSON.stringify(doc, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -179,7 +179,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -192,7 +192,7 @@ program console.log(response); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -205,7 +205,7 @@ program console.log(`ID ${name} removed`); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -227,7 +227,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -240,7 +240,7 @@ program console.log(UPDATE_OK); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -253,7 +253,7 @@ program console.log(JSON.stringify(doc, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -292,7 +292,7 @@ program console.log(cipherDid); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -306,7 +306,7 @@ program console.log(cipherDid); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -346,7 +346,7 @@ program console.log(JSON.stringify(json, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -360,7 +360,7 @@ program console.log(`signature in ${file}`, isValid ? 'is valid' : 'is NOT valid'); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -379,7 +379,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -388,7 +388,7 @@ program .description('Create challenge (optionally from a file)') .action(async (file, name) => { try { - const challenge = file ? JSON.parse(fs.readFileSync(file).toString()) : null; + const challenge = file ? JSON.parse(fs.readFileSync(file).toString()) : undefined; const did = await keymaster.createChallenge(challenge); if (name) { @@ -398,7 +398,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -407,8 +407,7 @@ program .description('Create challenge from a credential DID') .action(async (credentialDID, name) => { try { - const credential = await keymaster.lookupDID(credentialDID); - const challenge = { credentials: [{ schema: credential }] }; + const challenge = { credentials: [{ schema: credentialDID }] }; const did = await keymaster.createChallenge(challenge); if (name) { @@ -418,7 +417,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -431,7 +430,7 @@ program console.log(JSON.stringify(vc, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -450,7 +449,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -464,7 +463,7 @@ program console.log(JSON.stringify(response, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -482,7 +481,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -505,7 +504,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -518,7 +517,7 @@ program console.log(JSON.stringify(held, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -531,7 +530,7 @@ program console.log(JSON.stringify(credential, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -544,7 +543,7 @@ program console.log(JSON.stringify(response, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -557,7 +556,7 @@ program console.log(JSON.stringify(response, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -570,7 +569,7 @@ program console.log(response); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -583,7 +582,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -596,7 +595,7 @@ program console.log(JSON.stringify(vp, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -609,7 +608,7 @@ program console.log(UPDATE_OK); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -622,7 +621,7 @@ program console.log(UPDATE_OK); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -641,7 +640,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -655,7 +654,7 @@ program keymaster.addName(name, did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -668,7 +667,7 @@ program console.log(JSON.stringify(groups, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -681,7 +680,7 @@ program console.log(JSON.stringify(group, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -694,7 +693,7 @@ program console.log(response); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -707,7 +706,7 @@ program console.log(response); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -720,7 +719,7 @@ program console.log(response); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -739,7 +738,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -752,7 +751,7 @@ program console.log(JSON.stringify(schemas, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -766,7 +765,7 @@ program console.log(JSON.stringify(schema, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -779,7 +778,7 @@ program console.log(JSON.stringify(template, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -793,7 +792,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -806,7 +805,7 @@ program console.log(JSON.stringify(asset, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -819,7 +818,7 @@ program console.log(JSON.stringify(template, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -838,7 +837,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -851,7 +850,7 @@ program console.log(JSON.stringify(response, null, 4)); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -864,7 +863,7 @@ program console.log(did); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -882,7 +881,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -900,7 +899,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -918,7 +917,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -936,7 +935,7 @@ program } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -982,7 +981,7 @@ program console.log(UPDATE_FAILED); } } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); @@ -1020,7 +1019,7 @@ program console.timeEnd('total'); } catch (error) { - console.error(error.message); + console.error(error.message || error); } }); diff --git a/services/mediators/hyperswarm/src/hyperswarm-mediator.js b/services/mediators/hyperswarm/src/hyperswarm-mediator.js index ca2d3371..5c4125c7 100644 --- a/services/mediators/hyperswarm/src/hyperswarm-mediator.js +++ b/services/mediators/hyperswarm/src/hyperswarm-mediator.js @@ -363,7 +363,7 @@ async function flushQueue() { await gatekeeper.clearQueue(REGISTRY, batch); await relayMsg(msg); - await importBatch(batch); + await mergeBatch(batch); } } diff --git a/tests/hypr-confirm.js b/tests/hypr-confirm.js new file mode 100644 index 00000000..961bb83e --- /dev/null +++ b/tests/hypr-confirm.js @@ -0,0 +1,58 @@ +import * as keymaster from '@mdip/keymaster/lib'; +import * as wallet from '@mdip/keymaster/db/json'; +import * as gatekeeper from '@mdip/gatekeeper/sdk'; +import * as cipher from '@mdip/cipher/node'; + +async function runTest() { + const expires = new Date(); + expires.setMinutes(expires.getMinutes() + 1); + const testOptions = { registry: 'hyperswarm', validUntil: expires.toISOString() }; + + const alice = await keymaster.createId('Alice', testOptions); + console.log(`alice: ${alice}`); + + const asset = await keymaster.createAsset({ version: 1 }, testOptions); + const doc1 = await keymaster.resolveDID(asset); + console.log(JSON.stringify(doc1, null, 4)); + + await keymaster.updateAsset(asset, { version: 2 }); + let doc2 = await keymaster.resolveDID(asset); + console.log(JSON.stringify(doc2, null, 4)); + + while (doc2.didDocumentMetadata.confirmed === false) { + // wait for 1 second before checking again + await new Promise(resolve => setTimeout(resolve, 1000)); + + doc2 = await keymaster.resolveDID(asset); + console.log(JSON.stringify(doc2, null, 4)); + } +} + +async function main() { + await gatekeeper.start({ + url: 'http://localhost:4224', + waitUntilReady: true, + }); + + await keymaster.start({ + gatekeeper, + wallet, + cipher, + }); + + const backup = await keymaster.loadWallet(); + await keymaster.newWallet(null, true); + + try { + await runTest(); + } + catch (error) { + console.log(error); + } + + await keymaster.saveWallet(backup); + await keymaster.stop(); + process.exit(); +} + +main(); diff --git a/tests/keymaster.test.js b/tests/keymaster.test.js index aafbb499..f3d11db8 100644 --- a/tests/keymaster.test.js +++ b/tests/keymaster.test.js @@ -2048,6 +2048,14 @@ describe('createChallenge', () => { await keymaster.createId('Alice'); + try { + await keymaster.createChallenge(null); + throw new ExpectedExceptionError(); + } + catch (error) { + expect(error.message).toBe('Invalid parameter: challenge'); + } + try { await keymaster.createChallenge([]); throw new ExpectedExceptionError();