From 7adf614a1a92a3856eb21e9df49f0f94f7e988c3 Mon Sep 17 00:00:00 2001 From: Brian Botha Date: Mon, 1 Nov 2021 14:38:15 +1100 Subject: [PATCH 1/8] Unattended `pk bootstrap` and `pk agent start` 1. Updated Status and removed lockfile 2. Use recovery code to bootstrap or start agent 3. NodeConnection: removing sends hole-punch message test 4. Created `resetPassword` untility function for respecting lock when resetting the password. 5. Slow keypair generation is mocked for testing 6. Signal handling for all commands in ExitHandlers 7. binProcessors utilities for processing CLI options 8. Common defaults for CLI, PolykeyAgent, PolykeyClient in src/config.ts 9. New terminating and non-terminating process testing utilities --- .eslintrc | 1 + package-lock.json | 134 ++-- package.json | 8 +- src/ErrorPolykey.ts | 4 +- src/PolykeyAgent.ts | 132 ++-- src/PolykeyClient.ts | 181 ++--- src/acl/ACL.ts | 1 - src/agent/backgroundAgent.ts | 33 - src/agent/utils.ts | 117 ---- src/bin/.eslintrc | 21 + src/bin/CommandPolykey.ts | 16 +- src/bin/agent/CommandAgent.ts | 12 +- src/bin/agent/CommandLock.ts | 30 +- src/bin/agent/CommandLockAll.ts | 48 +- src/bin/agent/CommandStart.ts | 157 ++++- src/bin/agent/CommandStatus.ts | 108 +-- src/bin/agent/CommandStop.ts | 48 +- src/bin/agent/CommandUnlock.ts | 44 +- src/bin/bootstrap/CommandBootstrap.ts | 58 +- src/bin/errors.ts | 65 +- src/bin/identities/CommandAllow.ts | 46 +- src/bin/identities/CommandAuthenticate.ts | 43 +- src/bin/identities/CommandClaim.ts | 43 +- src/bin/identities/CommandDisallow.ts | 45 +- src/bin/identities/CommandDiscover.ts | 45 +- src/bin/identities/CommandGet.ts | 45 +- src/bin/identities/CommandIdentities.ts | 22 +- src/bin/identities/CommandList.ts | 114 ++-- src/bin/identities/CommandPermissions.ts | 45 +- src/bin/identities/CommandSearch.ts | 44 +- src/bin/identities/CommandTrust.ts | 45 +- src/bin/identities/CommandUntrust.ts | 45 +- src/bin/keys/CommandCert.ts | 43 +- src/bin/keys/CommandCertchain.ts | 60 +- src/bin/keys/CommandDecrypt.ts | 61 +- src/bin/keys/CommandEncrypt.ts | 61 +- src/bin/keys/CommandKeys.ts | 20 +- src/bin/keys/CommandPassword.ts | 49 +- src/bin/keys/CommandRenew.ts | 49 +- src/bin/keys/CommandReset.ts | 49 +- src/bin/keys/CommandRoot.ts | 42 +- src/bin/keys/CommandSign.ts | 57 +- src/bin/keys/CommandVerify.ts | 65 +- src/bin/nodes/CommandAdd.ts | 44 +- src/bin/nodes/CommandClaim.ts | 42 +- src/bin/nodes/CommandFind.ts | 42 +- src/bin/nodes/CommandNodes.ts | 8 +- src/bin/nodes/CommandPing.ts | 42 +- src/bin/notifications/CommandClear.ts | 43 +- src/bin/notifications/CommandNotifications.ts | 6 +- src/bin/notifications/CommandRead.ts | 42 +- src/bin/notifications/CommandSend.ts | 42 +- src/bin/options.ts | 75 --- src/bin/polykey.ts | 39 +- src/bin/polykeyAgent.ts | 129 ++++ src/bin/secrets/CommandCreate.ts | 57 +- src/bin/secrets/CommandDelete.ts | 43 +- src/bin/secrets/CommandDir.ts | 42 +- src/bin/secrets/CommandEdit.ts | 57 +- src/bin/secrets/CommandEnv.ts | 2 +- src/bin/secrets/CommandGet.ts | 43 +- src/bin/secrets/CommandList.ts | 59 +- src/bin/secrets/CommandMkdir.ts | 43 +- src/bin/secrets/CommandRename.ts | 43 +- src/bin/secrets/CommandSecrets.ts | 20 +- src/bin/secrets/CommandUpdate.ts | 56 +- src/bin/types.ts | 64 ++ src/bin/utils/ExitHandlers.ts | 135 ++++ src/bin/utils/index.ts | 4 + src/bin/utils/options.ts | 124 ++++ src/bin/{ => utils}/parsers.ts | 78 +-- src/bin/utils/processors.ts | 248 +++++++ src/bin/{ => utils}/utils.ts | 66 +- src/bin/vaults/CommandClone.ts | 42 +- src/bin/vaults/CommandCreate.ts | 42 +- src/bin/vaults/CommandDelete.ts | 43 +- src/bin/vaults/CommandList.ts | 58 +- src/bin/vaults/CommandLog.ts | 69 +- src/bin/vaults/CommandPermissions.ts | 2 +- src/bin/vaults/CommandPull.ts | 42 +- src/bin/vaults/CommandRename.ts | 42 +- src/bin/vaults/CommandScan.ts | 2 +- src/bin/vaults/CommandShare.ts | 42 +- src/bin/vaults/CommandStat.ts | 2 +- src/bin/vaults/CommandUnshare.ts | 42 +- src/bin/vaults/CommandVaults.ts | 26 +- src/bin/vaults/CommandVersion.ts | 42 +- src/bootstrap/bootstrap.ts | 106 --- src/bootstrap/errors.ts | 12 +- src/bootstrap/index.ts | 3 +- src/bootstrap/types.ts | 8 - src/bootstrap/utils.ts | 172 +++++ src/client/clientService.ts | 4 + src/client/rpcKeys.ts | 2 +- src/client/rpcVaults.ts | 5 +- src/config.ts | 44 ++ src/errors.ts | 17 +- src/grpc/GRPCServer.ts | 19 +- src/grpc/utils.ts | 4 +- src/keys/KeyManager.ts | 205 ++++-- src/keys/errors.ts | 21 +- src/keys/types.ts | 3 + src/keys/utils.ts | 39 +- src/schema/Schema.ts | 2 +- src/sessions/Session.ts | 5 +- src/sessions/errors.ts | 16 +- src/status/Status.ts | 278 ++++---- src/status/StatusSchema.json | 67 ++ src/status/errors.ts | 31 +- src/status/index.ts | 4 +- src/status/types.ts | 46 ++ src/status/utils.ts | 11 + src/types.ts | 15 +- src/utils/errors.ts | 31 + src/utils/index.ts | 4 + src/utils/locks.ts | 36 + src/utils/sysexits.ts | 27 + src/{ => utils}/utils.ts | 109 +-- src/vaults/VaultOps.ts | 3 +- src/workers/polykeyWorkerModule.ts | 10 + tests/PolykeyAgent.test.ts | 53 +- tests/acl/ACL.test.ts | 15 +- tests/agent/GRPCClientAgent.test.ts | 275 ++++---- tests/agent/utils.test.ts | 115 ---- tests/bin/agent.test.ts | 497 -------------- tests/bin/agent/agent.test.ts | 633 ++++++++++++++++++ tests/bin/agent/start.test.ts | 68 ++ tests/bin/bootstrap.test.ts | 153 +++-- tests/bin/identities.test.ts | 8 +- tests/bin/keys.test.ts | 95 +-- tests/bin/nodes.test.ts | 26 +- tests/bin/notifications.test.ts | 13 +- tests/bin/secret.test.ts | 60 +- tests/bin/sessions.test.ts | 10 +- tests/bin/utils.retryAuth.test.ts | 22 +- tests/bin/utils.test.ts | 3 +- tests/bin/utils.ts | 88 ++- tests/bin/vaults.test.ts | 10 +- tests/bootstrap/bootstrap.test.ts | 117 ++-- tests/claims/utils.test.ts | 9 +- tests/client/GRPCClientClient.test.ts | 17 +- tests/client/PolykeyClient.test.ts | 51 +- tests/client/rpcAgent.test.ts | 19 +- tests/client/rpcGestalts.test.ts | 7 + tests/client/rpcIdentities.test.ts | 7 + tests/client/rpcKeys.test.ts | 195 +++--- tests/client/rpcNodes.test.ts | 31 +- tests/client/rpcNotifications.test.ts | 7 + tests/client/rpcSessions.test.ts | 7 + tests/client/rpcVaults.test.ts | 7 + tests/client/utils.ts | 17 +- tests/discovery/Discovery.test.ts | 7 + tests/gestalts/GestaltGraph.test.ts | 14 +- tests/grpc/GRPCClient.test.ts | 15 +- tests/grpc/GRPCServer.test.ts | 10 +- tests/identities/IdentitiesManager.test.ts | 14 +- tests/index.test.ts | 7 + tests/keys/KeyManager.test.ts | 307 ++++++--- tests/keys/utils.test.ts | 27 + tests/nodes/NodeConnection.test.ts | 53 +- tests/nodes/NodeGraph.test.ts | 125 ++-- tests/nodes/NodeManager.test.ts | 16 +- .../NotificationsManager.test.ts | 11 +- tests/sessions/SessionManager.test.ts | 9 +- tests/sigchain/Sigchain.test.ts | 13 +- tests/status/Status.test.ts | 208 ++++-- tests/utils.ts | 24 +- tests/vaults/VaultInternal.test.ts | 7 + tests/vaults/VaultManager.test.ts | 13 +- tests/vaults/VaultOps.test.ts | 21 +- 170 files changed, 5983 insertions(+), 3502 deletions(-) delete mode 100644 src/agent/backgroundAgent.ts delete mode 100644 src/agent/utils.ts create mode 100644 src/bin/.eslintrc delete mode 100644 src/bin/options.ts create mode 100644 src/bin/polykeyAgent.ts create mode 100644 src/bin/types.ts create mode 100644 src/bin/utils/ExitHandlers.ts create mode 100644 src/bin/utils/index.ts create mode 100644 src/bin/utils/options.ts rename src/bin/{ => utils}/parsers.ts (50%) create mode 100644 src/bin/utils/processors.ts rename src/bin/{ => utils}/utils.ts (66%) delete mode 100644 src/bootstrap/bootstrap.ts delete mode 100644 src/bootstrap/types.ts create mode 100644 src/bootstrap/utils.ts create mode 100644 src/status/StatusSchema.json create mode 100644 src/status/types.ts create mode 100644 src/status/utils.ts create mode 100644 src/utils/errors.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/locks.ts create mode 100644 src/utils/sysexits.ts rename src/{ => utils}/utils.ts (67%) delete mode 100644 tests/agent/utils.test.ts delete mode 100644 tests/bin/agent.test.ts create mode 100644 tests/bin/agent/agent.test.ts create mode 100644 tests/bin/agent/start.test.ts diff --git a/.eslintrc b/.eslintrc index 7d8a72091..57b176925 100644 --- a/.eslintrc +++ b/.eslintrc @@ -98,6 +98,7 @@ "@typescript-eslint/no-empty-function": 0, "@typescript-eslint/no-empty-interface": 0, "@typescript-eslint/consistent-type-imports": ["error"], + "@typescript-eslint/consistent-type-exports": ["error"], "no-throw-literal": "off", "@typescript-eslint/no-throw-literal": ["error"], "@typescript-eslint/no-floating-promises": ["error", { diff --git a/package-lock.json b/package-lock.json index a36b7bec9..f82faf934 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1574,9 +1574,9 @@ } }, "@matrixai/async-init": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.1.0.tgz", - "integrity": "sha512-IdecCtnkgkjyaWBTeOmTunlpeAkaokfghxWgZQnpVjCKmJ+37gxtYfnJ7GyuPldmLMj9OSNc1LdA9K8ufpinEQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@matrixai/async-init/-/async-init-1.2.0.tgz", + "integrity": "sha512-JM8bEvE9v5woWS2FohgWi66CV3cCD/j1cQvNPIBxAiKCoVPlJC/8geROinx3DGO5Wj7jTXkfzI9Ldu0tf8aPbg==", "requires": { "ts-custom-error": "^3.2.0" } @@ -1877,15 +1877,6 @@ "@types/node": "*" } }, - "@types/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@types/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-kd4LMvcnpYkspDcp7rmXKedn8iJSCoa331zRRamUp5oanKt/CefbEGPQP7G89enz7sKD4bvsr8mHSsC8j5WOvA==", - "dev": true, - "requires": { - "@types/retry": "*" - } - }, "@types/readable-stream": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-2.3.11.tgz", @@ -1896,12 +1887,6 @@ "safe-buffer": "*" } }, - "@types/retry": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", - "dev": true - }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -1930,17 +1915,17 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", - "integrity": "sha512-aINiAxGVdOl1eJyVjaWn/YcVAq4Gi/Yo35qHGCnqbWVz61g39D0h23veY/MA0rFFGfxK7TySg2uwDeNv+JgVpg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.4.0.tgz", + "integrity": "sha512-9/yPSBlwzsetCsGEn9j24D8vGQgJkOTr4oMLas/w886ZtzKIs1iyoqFrwsX2fqYEeUwsdBpC21gcjRGo57u0eg==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.33.0", - "@typescript-eslint/scope-manager": "4.33.0", - "debug": "^4.3.1", + "@typescript-eslint/experimental-utils": "5.4.0", + "@typescript-eslint/scope-manager": "5.4.0", + "debug": "^4.3.2", "functional-red-black-tree": "^1.0.1", "ignore": "^5.1.8", - "regexpp": "^3.1.0", + "regexpp": "^3.2.0", "semver": "^7.3.5", "tsutils": "^3.21.0" }, @@ -1957,58 +1942,58 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", - "integrity": "sha512-zeQjOoES5JFjTnAhI5QY7ZviczMzDptls15GFsI6jyUOq0kOf9+WonkhtlIhh0RgHRnqj5gdNxW5j1EvAyYg6Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.4.0.tgz", + "integrity": "sha512-Nz2JDIQUdmIGd6p33A+naQmwfkU5KVTLb/5lTk+tLVTDacZKoGQisj8UCxk7onJcrgjIvr8xWqkYI+DbI3TfXg==", "dev": true, "requires": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.4.0", + "@typescript-eslint/types": "5.4.0", + "@typescript-eslint/typescript-estree": "5.4.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0" } }, "@typescript-eslint/parser": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.33.0.tgz", - "integrity": "sha512-ZohdsbXadjGBSK0/r+d87X0SBmKzOq4/S5nzK6SBgJspFo9/CUDJ7hjayuze+JK7CZQLDMroqytp7pOcFKTxZA==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.4.0.tgz", + "integrity": "sha512-JoB41EmxiYpaEsRwpZEYAJ9XQURPFer8hpkIW9GiaspVLX8oqbqNM8P4EP8HOZg96yaALiLEVWllA2E8vwsIKw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.33.0", - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/typescript-estree": "4.33.0", - "debug": "^4.3.1" + "@typescript-eslint/scope-manager": "5.4.0", + "@typescript-eslint/types": "5.4.0", + "@typescript-eslint/typescript-estree": "5.4.0", + "debug": "^4.3.2" } }, "@typescript-eslint/scope-manager": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.33.0.tgz", - "integrity": "sha512-5IfJHpgTsTZuONKbODctL4kKuQje/bzBRkwHE8UOZ4f89Zeddg+EGZs8PD8NcN4LdM3ygHWYB3ukPAYjvl/qbQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.4.0.tgz", + "integrity": "sha512-pRxFjYwoi8R+n+sibjgF9iUiAELU9ihPBtHzocyW8v8D8G8KeQvXTsW7+CBYIyTYsmhtNk50QPGLE3vrvhM5KA==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0" + "@typescript-eslint/types": "5.4.0", + "@typescript-eslint/visitor-keys": "5.4.0" } }, "@typescript-eslint/types": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.33.0.tgz", - "integrity": "sha512-zKp7CjQzLQImXEpLt2BUw1tvOMPfNoTAfb8l51evhYbOEEzdWyQNmHWWGPR6hwKJDAi+1VXSBmnhL9kyVTTOuQ==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.4.0.tgz", + "integrity": "sha512-GjXNpmn+n1LvnttarX+sPD6+S7giO+9LxDIGlRl4wK3a7qMWALOHYuVSZpPTfEIklYjaWuMtfKdeByx0AcaThA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.33.0.tgz", - "integrity": "sha512-rkWRY1MPFzjwnEVHsxGemDzqqddw2QbTJlICPD9p9I9LfsO8fdmfQPOX3uKfUaGRDFJbfrtm/sXhVXN4E+bzCA==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.4.0.tgz", + "integrity": "sha512-nhlNoBdhKuwiLMx6GrybPT3SFILm5Gij2YBdPEPFlYNFAXUJWX6QRgvi/lwVoadaQEFsizohs6aFRMqsXI2ewA==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "@typescript-eslint/visitor-keys": "4.33.0", - "debug": "^4.3.1", - "globby": "^11.0.3", - "is-glob": "^4.0.1", + "@typescript-eslint/types": "5.4.0", + "@typescript-eslint/visitor-keys": "5.4.0", + "debug": "^4.3.2", + "globby": "^11.0.4", + "is-glob": "^4.0.3", "semver": "^7.3.5", "tsutils": "^3.21.0" }, @@ -2025,13 +2010,21 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.33.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", - "integrity": "sha512-uqi/2aSz9g2ftcHWf8uLPJA70rUv6yuMW5Bohw+bwcuzaxQIHaKFZCKGoGXIrc9vkTJ3+0txM73K0Hq3d5wgIg==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.4.0.tgz", + "integrity": "sha512-PVbax7MeE7tdLfW5SA0fs8NGVVr+buMPrcj+CWYWPXsZCH8qZ1THufDzbXm1xrZ2b2PA1iENJ0sRq5fuUtvsJg==", "dev": true, "requires": { - "@typescript-eslint/types": "4.33.0", - "eslint-visitor-keys": "^2.0.0" + "@typescript-eslint/types": "5.4.0", + "eslint-visitor-keys": "^3.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true + } } }, "abab": { @@ -4662,7 +4655,8 @@ "graceful-fs": { "version": "4.2.8", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", - "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==", + "dev": true }, "graphemesplit": { "version": "2.4.4", @@ -7816,16 +7810,6 @@ "sisteransi": "^1.0.5" } }, - "proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "requires": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", @@ -8114,11 +8098,6 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -8422,7 +8401,8 @@ "signal-exit": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.5.tgz", - "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==" + "integrity": "sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==", + "dev": true }, "simple-concat": { "version": "1.0.1", diff --git a/package.json b/package.json index 713f79451..084fcc48f 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ }, "dependencies": { "@grpc/grpc-js": "1.3.7", - "@matrixai/async-init": "^1.1.0", + "@matrixai/async-init": "^1.2.0", "@matrixai/db": "^1.1.0", "@matrixai/id": "^2.1.0", "@matrixai/logger": "^2.1.0", @@ -94,7 +94,6 @@ "node-forge": "^0.10.0", "pako": "^1.0.11", "prompts": "^2.4.1", - "proper-lockfile": "^4.1.2", "readable-stream": "^3.6.0", "threads": "^1.6.5", "ts-custom-error": "^3.2.0", @@ -111,11 +110,10 @@ "@types/node-forge": "^0.9.7", "@types/pako": "^1.0.2", "@types/prompts": "^2.0.13", - "@types/proper-lockfile": "^4.1.1", "@types/readable-stream": "^2.3.11", "@types/uuid": "^8.3.0", - "@typescript-eslint/eslint-plugin": "^4.12.0", - "@typescript-eslint/parser": "^4.12.0", + "@typescript-eslint/eslint-plugin": "^5.4.0", + "@typescript-eslint/parser": "^5.4.0", "babel-jest": "^26.6.3", "eslint": "^7.17.0", "eslint-config-prettier": "^7.1.0", diff --git a/src/ErrorPolykey.ts b/src/ErrorPolykey.ts index 1608ed9a9..414b95732 100644 --- a/src/ErrorPolykey.ts +++ b/src/ErrorPolykey.ts @@ -1,11 +1,11 @@ import type { POJO } from './types'; - import { CustomError } from 'ts-custom-error'; +import sysexits from './utils/sysexits'; class ErrorPolykey extends CustomError { data: POJO; description: string = 'Polykey error'; - exitCode: number = 1; + exitCode: number = sysexits.GENERAL; constructor(message: string = '', data: POJO = {}) { super(message); this.data = data; diff --git a/src/PolykeyAgent.ts b/src/PolykeyAgent.ts index d1b0c0134..70b518108 100644 --- a/src/PolykeyAgent.ts +++ b/src/PolykeyAgent.ts @@ -51,14 +51,15 @@ interface PolykeyAgent extends CreateDestroyStartStop {} class PolykeyAgent { public static async createPolykeyAgent({ // Required parameters - nodePath, password, // Optional configuration + nodePath = config.defaults.nodePath, keysConfig = {}, networkConfig = {}, forwardProxyConfig = {}, reverseProxyConfig = {}, // Optional dependencies + status, schema, keyManager, db, @@ -79,12 +80,13 @@ class PolykeyAgent { logger = new Logger(this.name), fresh = false, }: { - nodePath: string; password: string; + nodePath?: string; keysConfig?: { rootKeyPairBits?: number; rootCertDuration?: number; dbKeyBits?: number; + recoveryCode?: string; }; forwardProxyConfig?: { authToken?: string; @@ -97,6 +99,7 @@ class PolykeyAgent { connTimeoutTime?: number; }; networkConfig?: NetworkConfig; + status?: Status; schema?: Schema; keyManager?: KeyManager; db?: DB; @@ -121,56 +124,38 @@ class PolykeyAgent { const umask = 0o077; logger.info(`Setting umask to ${umask.toString(8).padStart(3, '0')}`); process.umask(umask); + if (nodePath == null) { + throw new errors.ErrorUtilsNodePath(); + } logger.info(`Setting node path to ${nodePath}`); const keysConfig_ = { - rootKeyPairBits: 4096, - rootCertDuration: 31536000, - dbKeyBits: 256, + ...config.defaults.keysConfig, ...utils.filterEmptyObject(keysConfig), }; const forwardProxyConfig_ = { authToken: (await keysUtils.getRandomBytes(10)).toString(), - connConnectTime: 20000, - connTimeoutTime: 20000, - connPingIntervalTime: 1000, + ...config.defaults.forwardProxyConfig, ...utils.filterEmptyObject(forwardProxyConfig), }; const reverseProxyConfig_ = { - connConnectTime: 20000, - connTimeoutTime: 20000, + ...config.defaults.reverseProxyConfig, ...utils.filterEmptyObject(reverseProxyConfig), }; - const networkConfig_ = { - // ForwardProxy - proxyHost: '127.0.0.1' as Host, - proxyPort: 0 as Port, - egressHost: '0.0.0.0' as Host, - egressPort: 0 as Port, - // ReverseProxy - ingressHost: '0.0.0.0' as Host, - ingressPort: 0 as Port, - // GRPCServer for agent service - agentHost: '127.0.0.1' as Host, - agentPort: 0 as Port, - // GRPCServer for client service - clientHost: '127.0.0.1' as Host, - clientPort: 0 as Port, - ...networkConfig, - }; - await utils.mkdirExists(fs, nodePath); - const statePath = path.join(nodePath, 'state'); - const dbPath = path.join(statePath, 'db'); - const keysPath = path.join(statePath, 'keys'); - const vaultsPath = path.join(statePath, 'vaults'); - - const status = await Status.createStatus({ - nodePath: nodePath, - fs: fs, - logger: logger.getChild('Lockfile'), - }); - await status.start(); - + const statusPath = path.join(nodePath, config.defaults.statusBase); + const statePath = path.join(nodePath, config.defaults.stateBase); + const dbPath = path.join(statePath, config.defaults.dbBase); + const keysPath = path.join(statePath, config.defaults.keysBase); + const vaultsPath = path.join(statePath, config.defaults.vaultsBase); + status = + status ?? + new Status({ + statusPath, + fs: fs, + logger: logger.getChild(Status.name), + }); + // Start locking the status + await status.start({ pid: process.pid }); schema = schema ?? (await Schema.createSchema({ @@ -179,7 +164,6 @@ class PolykeyAgent { logger: logger.getChild(Schema.name), fresh, })); - keyManager = keyManager ?? (await KeyManager.createKeyManager({ @@ -190,8 +174,6 @@ class PolykeyAgent { logger: logger.getChild(KeyManager.name), fresh, })); - await status.updateStatus('nodeId', keyManager.getNodeId()); - db = db ?? (await DB.createDB({ @@ -207,7 +189,6 @@ class PolykeyAgent { logger: logger.getChild(DB.name), fresh, })); - identitiesManager = identitiesManager ?? (await IdentitiesManager.createIdentitiesManager({ @@ -215,14 +196,12 @@ class PolykeyAgent { logger: logger.getChild(IdentitiesManager.name), fresh, })); - // Registering providers const githubProvider = new providers.GithubProvider({ clientId: config.providers['github.com'].clientId, logger: logger.getChild(providers.GithubProvider.name), }); identitiesManager.registerProvider(githubProvider); - sigchain = sigchain ?? (await Sigchain.createSigchain({ @@ -231,7 +210,6 @@ class PolykeyAgent { logger: logger.getChild(Sigchain.name), fresh, })); - acl = acl ?? (await ACL.createACL({ @@ -239,7 +217,6 @@ class PolykeyAgent { logger: logger.getChild(ACL.name), fresh, })); - gestaltGraph = gestaltGraph ?? (await GestaltGraph.createGestaltGraph({ @@ -248,21 +225,18 @@ class PolykeyAgent { logger: logger.getChild(GestaltGraph.name), fresh, })); - fwdProxy = fwdProxy ?? new ForwardProxy({ ...forwardProxyConfig_, logger: logger.getChild(ForwardProxy.name), }); - revProxy = revProxy ?? new ReverseProxy({ ...reverseProxyConfig_, logger: logger.getChild(ReverseProxy.name), }); - nodeManager = nodeManager ?? (await NodeManager.createNodeManager({ @@ -274,7 +248,6 @@ class PolykeyAgent { logger: logger.getChild(NodeManager.name), fresh, })); - discovery = discovery ?? (await Discovery.createDiscovery({ @@ -283,7 +256,6 @@ class PolykeyAgent { nodeManager, logger: logger.getChild(Discovery.name), })); - vaultManager = vaultManager ?? (await VaultManager.createVaultManager({ @@ -298,7 +270,6 @@ class PolykeyAgent { logger: logger.getChild(VaultManager.name), fresh, })); - notificationsManager = notificationsManager ?? (await NotificationsManager.createNotificationsManager({ @@ -309,27 +280,24 @@ class PolykeyAgent { logger: logger.getChild(NotificationsManager.name), fresh, })); - sessionManager = sessionManager ?? (await SessionManager.createSessionManager({ db, keyManager, logger: logger.getChild(SessionManager.name), + fresh, })); - grpcServerClient = grpcServerClient ?? new GRPCServer({ logger: logger.getChild(GRPCServer.name + 'Client'), }); - grpcServerAgent = grpcServerAgent ?? new GRPCServer({ logger: logger.getChild(GRPCServer.name + 'Agent'), }); - const polykeyAgent = new PolykeyAgent({ nodePath, status, @@ -352,13 +320,11 @@ class PolykeyAgent { fs, logger, }); - await polykeyAgent.start({ password, + networkConfig, fresh, - networkConfig: networkConfig_, }); - // Finished the start process. logger.info(`Created ${this.name}`); return polykeyAgent; } @@ -381,8 +347,8 @@ class PolykeyAgent { public readonly sessionManager: SessionManager; public readonly grpcServerAgent: GRPCServer; public readonly grpcServerClient: GRPCServer; + public readonly fs: FileSystem; - protected fs: FileSystem; protected logger: Logger; constructor({ @@ -459,28 +425,13 @@ class PolykeyAgent { networkConfig?: NetworkConfig; fresh?: boolean; }) { + this.logger.info(`Starting ${this.constructor.name}`); const networkConfig_ = { - // ForwardProxy - proxyHost: '127.0.0.1' as Host, - proxyPort: 0 as Port, - egressHost: '0.0.0.0' as Host, - egressPort: 0 as Port, - // ReverseProxy - ingressHost: '0.0.0.0' as Host, - ingressPort: 0 as Port, - // GRPCServer for agent service - agentHost: '127.0.0.1' as Host, - agentPort: 0 as Port, - // GRPCServer for client service - clientHost: '127.0.0.1' as Host, - clientPort: 0 as Port, - ...networkConfig, + ...config.defaults.networkConfig, + ...utils.filterEmptyObject(networkConfig), }; - - this.logger.info(`Starting ${this.constructor.name}`); - await this.status.start(); + await this.status.start({ pid: process.pid }); await this.schema.start({ fresh }); - const agentService = createAgentService({ keyManager: this.keyManager, vaultManager: this.vaultManager, @@ -488,7 +439,6 @@ class PolykeyAgent { sigchain: this.sigchain, notificationsManager: this.notificationsManager, }); - const clientService = createClientService({ polykeyAgent: this, discovery: this.discovery, @@ -502,6 +452,7 @@ class PolykeyAgent { fwdProxy: this.fwdProxy, revProxy: this.revProxy, clientGrpcServer: this.grpcServerClient, + fs: this.fs, }); // Starting modules @@ -554,13 +505,12 @@ class PolykeyAgent { await this.notificationsManager.start({ fresh }); await this.sessionManager.start({ fresh }); - await this.status.updateStatus('host', this.grpcServerClient.host); - await this.status.updateStatus('port', this.grpcServerClient.port); - await this.status.updateStatus('ingressHost', this.revProxy.ingressHost); - await this.status.updateStatus('ingressPort', this.revProxy.ingressPort); - await this.status.updateStatus('fwdProxyHost', this.fwdProxy.proxyHost); - await this.status.updateStatus('fwdProxyPort', this.fwdProxy.proxyPort); - await this.status.finishStart(); + await this.status.finishStart({ + pid: process.pid, + nodeId: this.keyManager.getNodeId(), + clientHost: this.grpcServerClient.host, + clientPort: this.grpcServerClient.port, + }); this.logger.info(`Started ${this.constructor.name}`); } @@ -570,7 +520,7 @@ class PolykeyAgent { */ public async stop() { this.logger.info(`Stopping ${this.constructor.name}`); - await this.status.beginStop(); + await this.status.beginStop({ pid: process.pid }); await this.sessionManager.stop(); await this.notificationsManager.stop(); await this.vaultManager.stop(); @@ -586,7 +536,7 @@ class PolykeyAgent { await this.db.stop(); await this.keyManager.stop(); await this.schema.stop(); - await this.status.stop(); + await this.status.stop({}); this.logger.info(`Stopped ${this.constructor.name}`); } diff --git a/src/PolykeyClient.ts b/src/PolykeyClient.ts index 9171acf7a..d39259bfe 100644 --- a/src/PolykeyClient.ts +++ b/src/PolykeyClient.ts @@ -1,30 +1,20 @@ -import type { FileSystem, LockConfig } from './types'; +import type { FileSystem } from './types'; import type { NodeId } from './nodes/types'; +import type { Host, Port } from './network/types'; import path from 'path'; import Logger from '@matrixai/logger'; import { CreateDestroyStartStop } from '@matrixai/async-init/dist/CreateDestroyStartStop'; -import * as utils from './utils'; import { Session } from './sessions'; -import { Status } from './status'; -import * as errors from './errors'; import { GRPCClientClient } from './client'; -import { sleep } from './utils'; - -// 2. client path should be an independent property -// 3. You should be able to start a PK client without actually having access to the node path -// 4. You will need access to all connection properties though +import * as errors from './errors'; +import * as utils from './utils'; +import config from './config'; /** * This PolykeyClient would create a new PolykeyClient object that constructs * a new GRPCClientClient which attempts to connect to an existing PolykeyAgent's * grpc server. - * - * The grpcClient is accessible, and so should be possible to perform tasks like: - * grpcClient.echo or whatever functions exist. - * - * It should read from some Status file in the nodePath, - * which is usually the default polykey path */ interface PolykeyClient extends CreateDestroyStartStop {} @CreateDestroyStartStop( @@ -33,183 +23,108 @@ interface PolykeyClient extends CreateDestroyStartStop {} ) class PolykeyClient { static async createPolykeyClient({ - nodePath, - clientPath = path.join(nodePath, 'client'), + nodeId, + host, + port, + nodePath = config.defaults.nodePath, session, + grpcClient, + timeout, fs = require('fs'), logger = new Logger(this.name), - // Optional start - timeout, - host, - port, + fresh = false, }: { - nodePath: string; - clientPath?: string; + nodeId: NodeId; + host: Host; + port: Port; + nodePath?: string; + timeout?: number; session?: Session; + grpcClient?: GRPCClientClient; fs?: FileSystem; logger?: Logger; - timeout?: number; - host?: string; - port?: number; + fresh?: boolean; }): Promise { logger.info(`Creating ${this.name}`); - nodePath = path.resolve(nodePath); - clientPath = path.resolve(clientPath); - const sessionTokenPath = path.join(clientPath, 'token'); + if (nodePath == null) { + throw new errors.ErrorUtilsNodePath(); + } + await utils.mkdirExists(fs, nodePath); + const sessionTokenPath = path.join(nodePath, config.defaults.tokenBase); session = session ?? (await Session.createSession({ sessionTokenPath, logger: logger.getChild(Session.name), + fresh, + })); + grpcClient = + grpcClient ?? + (await GRPCClientClient.createGRPCClientClient({ + nodeId, + host: host, + port: port, + tlsConfig: { keyPrivatePem: undefined, certChainPem: undefined }, + session, + timeout, + logger: logger.getChild(GRPCClientClient.name), })); const pkClient = new PolykeyClient({ nodePath, - clientPath, + grpcClient, session, fs, logger, }); - await pkClient.start({ - timeout, - host, - port, - }); + await pkClient.start(); logger.info(`Created ${this.name}`); return pkClient; } - // Optional parameters are encapsulated parameters - // the node path is optional? - // are we sure about this? - - public readonly clientPath: string; public readonly nodePath: string; - public readonly fs: FileSystem; - public readonly logger: Logger; - - public readonly grpcHost: string; - public readonly grpcPort: number; public readonly session: Session; - protected _grpcClient: GRPCClientClient; + public readonly grpcClient: GRPCClientClient; + + protected fs: FileSystem; + protected logger: Logger; constructor({ nodePath, - clientPath, session, + grpcClient, fs, logger, }: { nodePath: string; - clientPath: string; + grpcClient: GRPCClientClient; session: Session; fs: FileSystem; logger: Logger; }) { this.logger = logger; - this.fs = fs; this.nodePath = nodePath; - this.clientPath = clientPath; this.session = session; + this.grpcClient = grpcClient; + this.fs = fs; } - public async start({ - timeout, - host, - port, - }: { - timeout?: number; - host?: string; - port?: number; - } = {}): Promise { + public async start(): Promise { this.logger.info(`Starting ${this.constructor.name}`); - - const status = await Status.createStatus({ - nodePath: this.nodePath, - fs: this.fs, - logger: this.logger.getChild('Lockfile'), - }); - let starting = true; - for (let i = 0; i < 8 && starting; i++) { - switch (await status.checkStatus()) { - case 'STARTING': - await sleep(250); - continue; - case 'RUNNING': - starting = false; - break; - case 'STOPPING': - case 'UNLOCKED': - default: { - throw new errors.ErrorPolykey( - 'Polykey Status file not locked. Is the PolykeyAgent started?', - ); - } - } - } - - let lock: LockConfig; - try { - lock = await status.parseStatus(); - } catch (err) { - throw new errors.ErrorPolykey('Could not parse Polykey Lockfile.'); - } - - // Attempt to read token from fs and start session. - await this.session.start(); - - // Create a new GRPCClientClient - this._grpcClient = await GRPCClientClient.createGRPCClientClient({ - nodeId: lock.nodeId as NodeId, - host: host ?? lock.host ?? 'localhost', - port: port ?? lock.port ?? 0, - timeout: timeout ?? 30000, - tlsConfig: { keyPrivatePem: undefined, certChainPem: undefined }, - session: this.session, - logger: this.logger.getChild(GRPCClientClient.name), - }); - - if (!host && !lock.host) { - this.logger.warn('PolykeyClient started with default host: localhost'); - } - if (!port && !lock.port) { - this.logger.warn('PolykeyClient started with default port: 0'); - } - - await utils.mkdirExists(this.fs, this.clientPath); this.logger.info(`Started ${this.constructor.name}`); } public async stop() { this.logger.info(`Stopping ${this.constructor.name}`); - if (this.grpcClient) { - await this.grpcClient.destroy(); - } + await this.grpcClient.destroy(); await this.session.stop(); this.logger.info(`Stopped ${this.constructor.name}`); } public async destroy() { this.logger.info(`Destroying ${this.constructor.name}`); - - // What is a "Session" again - // it's an object that you start and maintain locks on it - // you can CreateDestroy it - // but do you start it? - - // should wipe out the actual session token and client-related data - // you must call all encapsulated properties - // - if (this.grpcClient) { - await this.grpcClient.destroy(); - } await this.session.destroy(); - this.logger.info(`Destroyed ${this.constructor.name}`); } - - public get grpcClient(): GRPCClientClient { - return this._grpcClient; - } } export default PolykeyClient; diff --git a/src/acl/ACL.ts b/src/acl/ACL.ts index 6de5d918f..ee337f194 100644 --- a/src/acl/ACL.ts +++ b/src/acl/ACL.ts @@ -1,4 +1,3 @@ -// Import type { Buffer } from 'buffer'; import type { Permission, VaultActions, PermissionIdString } from './types'; import type { DB, DBLevel, DBOp } from '@matrixai/db'; import type { NodeId } from '../nodes/types'; diff --git a/src/agent/backgroundAgent.ts b/src/agent/backgroundAgent.ts deleted file mode 100644 index 4bd5a5b10..000000000 --- a/src/agent/backgroundAgent.ts +++ /dev/null @@ -1,33 +0,0 @@ -import PolykeyAgent from '../PolykeyAgent'; - -let polykeyAgent: PolykeyAgent; - -async function handle(signal) { - process.stdout.write(`Caught ${signal}...`); - process.stdout.write('stopping polykeyAgent...'); - try { - await polykeyAgent.stop(); - } catch (e) { - process.stderr.write('Failed to stop agent.', e); - } - process.stdout.write('exiting...'); - process.exit(1); -} - -process.on('message', async (startOptions: string) => { - // Split the message into password and string - const ops = JSON.parse(startOptions); - try { - polykeyAgent = await PolykeyAgent.createPolykeyAgent({ - password: ops.password, - nodePath: ops.nodePath, - }); - // Catching kill signals. - process.send && process.send('started'); - process.on('SIGINT', handle); - process.on('SIGTERM', handle); - } catch (e) { - process.send && process.send(e.message); - process.exit(1); // Force an exit in the case of improper start. - } -}); diff --git a/src/agent/utils.ts b/src/agent/utils.ts deleted file mode 100644 index 436e35631..000000000 --- a/src/agent/utils.ts +++ /dev/null @@ -1,117 +0,0 @@ -import type { SpawnOptions } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { spawn } from 'cross-spawn'; -import { Status } from '../status'; -import * as agentErrors from '../errors'; - -async function checkAgentRunning(nodePath: string): Promise { - const status = await Status.createStatus({ - nodePath, - fs, - }); - - switch (await status.checkStatus()) { - case 'RUNNING': - return true; - case 'STARTING': - case 'STOPPING': - case 'UNLOCKED': - default: - return false; - } -} - -async function spawnBackgroundAgent( // FIXME, this is broken. - nodePath: string, - password: string, -): Promise { - // Checking agent running. - if (await checkAgentRunning(nodePath)) { - throw new agentErrors.ErrorAgentRunning( - `Unable to spawn Agent, already running at: ${nodePath}`, - ); - } - - const logPath = path.join(nodePath, 'agent', 'log'); - - try { - await fs.promises.mkdir(logPath, { recursive: true }); - } catch (err) { - if (err.code !== 'EEXIST') { - throw err; - } else { - await fs.promises.rmdir(logPath, { recursive: true }); - await fs.promises.mkdir(logPath, { recursive: true }); - } - } - - const options: SpawnOptions = { - detached: true, - stdio: [ - 'ignore', - fs.openSync(path.join(logPath, 'output.log'), 'a'), - fs.openSync(path.join(logPath, 'error.log'), 'a'), - 'ipc', - ], - uid: process.getuid(), - }; - - let spawnPath: string; - - const isElectron = false; - - const prefix = path.resolve(__dirname, 'backgroundAgent.'); - const suffix = fs.existsSync(prefix + 'js') ? 'js' : 'ts'; - - const DAEMON_SCRIPT_PATH = prefix + suffix; - - if (isElectron) { - options['env'] = { - ELECTRON_RUN_AS_NODE: '1', - }; - spawnPath = process.execPath; - } else { - spawnPath = DAEMON_SCRIPT_PATH.includes('.js') ? 'node' : 'ts-node'; - } - - // Spawning the process. - const agentProcess = spawn(spawnPath, [DAEMON_SCRIPT_PATH], options); - - const startOptions = { - nodePath: nodePath, - password: password, - }; - - let pid; - - let externalResolve; - let externalReject; - const promise = new Promise((resolve, reject) => { - externalResolve = resolve; - externalReject = reject; - }); - - agentProcess.send(JSON.stringify(startOptions), (err: Error) => { - if (err != null) { - agentProcess.kill('SIGTERM'); - } else { - pid = agentProcess.pid; - agentProcess.on('message', (msg) => { - agentProcess.unref(); - agentProcess.disconnect(); - if (msg !== 'started') { - externalReject( - 'something went wrong, child process did not start polykey agent', - ); - } - externalResolve(); - }); - } - }); - - await promise; - return pid; -} - -export { spawnBackgroundAgent, checkAgentRunning }; diff --git a/src/bin/.eslintrc b/src/bin/.eslintrc new file mode 100644 index 000000000..22f32604b --- /dev/null +++ b/src/bin/.eslintrc @@ -0,0 +1,21 @@ +{ + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "@", + "message": "Replace with relative path" + } + ], + "patterns": [ + { + "group": ["@/**"], + "message": "Replace with relative path" + } + ] + } + ] + } +} diff --git a/src/bin/CommandPolykey.ts b/src/bin/CommandPolykey.ts index a1d9c11ff..96d8d25fd 100644 --- a/src/bin/CommandPolykey.ts +++ b/src/bin/CommandPolykey.ts @@ -3,7 +3,7 @@ import type { FileSystem } from '../types'; import commander from 'commander'; import Logger, { StreamHandler } from '@matrixai/logger'; import * as binUtils from './utils'; -import * as binOptions from './options'; +import * as binOptions from './utils/options'; import * as binErrors from './errors'; /** @@ -15,18 +15,20 @@ const logger = new Logger('polykey', undefined, [new StreamHandler()]); * Base class for all commands */ class CommandPolykey extends commander.Command { - logger: Logger = logger; - fs: FileSystem; + protected logger: Logger = logger; + protected fs: FileSystem; + protected exitHandlers: binUtils.ExitHandlers; public constructor({ - args = [], + exitHandlers, fs = require('fs'), }: { - args?: ConstructorParameters; + exitHandlers: binUtils.ExitHandlers; fs?: FileSystem; - } = {}) { - super(...args); + }) { + super(); this.fs = fs; + this.exitHandlers = exitHandlers; // All commands must not exit upon error this.exitOverride(); // On usage error, show the help info diff --git a/src/bin/agent/CommandAgent.ts b/src/bin/agent/CommandAgent.ts index ea4b8bca5..8e83ac63e 100644 --- a/src/bin/agent/CommandAgent.ts +++ b/src/bin/agent/CommandAgent.ts @@ -11,12 +11,12 @@ class CommandAgent extends CommandPolykey { super(...args); this.name('agent'); this.description('Agent Operations'); - this.addCommand(new CommandLock()); - this.addCommand(new CommandLockAll()); - this.addCommand(new CommandStart()); - this.addCommand(new CommandStatus()); - this.addCommand(new CommandStop()); - this.addCommand(new CommandUnlock()); + this.addCommand(new CommandLock(...args)); + this.addCommand(new CommandLockAll(...args)); + this.addCommand(new CommandStart(...args)); + this.addCommand(new CommandStatus(...args)); + this.addCommand(new CommandStop(...args)); + this.addCommand(new CommandUnlock(...args)); } } diff --git a/src/bin/agent/CommandLock.ts b/src/bin/agent/CommandLock.ts index b950bbcdf..5c408fd8e 100644 --- a/src/bin/agent/CommandLock.ts +++ b/src/bin/agent/CommandLock.ts @@ -1,30 +1,24 @@ +import path from 'path'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import config from '../../config'; class CommandLock extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); this.name('lock'); this.description('Lock the Client and Clear the Existing Token'); - this.addOption(binOptions.nodeId); - this.addOption(binOptions.clientHost); - this.addOption(binOptions.clientPort); this.action(async (options) => { - const { default: PolykeyClient } = await import('../../PolykeyClient'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, + const { default: Session } = await import('../../sessions/Session'); + // Just delete the session token + const session = new Session({ + sessionTokenPath: path.join( + options.nodePath, + config.defaults.tokenBase, + ), + fs: this.fs, + logger: this.logger.getChild(Session.name), }); - - try { - // Clear token from memory - await client.session.stop(); - // Remove token from fs - await client.session.destroy(); - process.stdout.write('Client session stopped'); - } finally { - await client.stop(); - } + await session.destroy(); }); } } diff --git a/src/bin/agent/CommandLockAll.ts b/src/bin/agent/CommandLockAll.ts index f944e206e..d0c532049 100644 --- a/src/bin/agent/CommandLockAll.ts +++ b/src/bin/agent/CommandLockAll.ts @@ -1,9 +1,8 @@ -import type { Metadata } from '@grpc/grpc-js'; - +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandLockAll extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -16,27 +15,38 @@ class CommandLockAll extends CommandPolykey { this.action(async (options) => { const { default: PolykeyClient } = await import('../../PolykeyClient'); const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); - - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); - - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const emptyMessage = new utilsPB.EmptyMessage(); - await binUtils.retryAuth( - (auth?: Metadata) => grpcClient.sessionsLockAll(emptyMessage, auth), + await binUtils.retryAuthentication( + (auth) => grpcClient.sessionsLockAll(emptyMessage, auth), meta, ); - process.stdout.write('Locked all clients'); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/agent/CommandStart.ts b/src/bin/agent/CommandStart.ts index 0bf98fac1..f2024769e 100644 --- a/src/bin/agent/CommandStart.ts +++ b/src/bin/agent/CommandStart.ts @@ -1,35 +1,152 @@ +import type { AgentChildProcessInput, AgentChildProcessOutput } from '../types'; +import type PolykeyAgent from '../../PolykeyAgent'; +import type { RecoveryCode } from '../../keys/types'; +import path from 'path'; +import child_process from 'child_process'; +import process from 'process'; import CommandPolykey from '../CommandPolykey'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; +import * as binErrors from '../errors'; +import { promise } from '../../utils'; +import config from '../../config'; class CommandStart extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); this.name('start'); this.description('Start the Polykey Agent'); - this.option('-b, --background', 'Starts the agent as a background process'); + this.addOption(binOptions.recoveryCodeFile); + this.addOption(binOptions.clientHost); + this.addOption(binOptions.clientPort); + this.addOption(binOptions.ingressHost); + this.addOption(binOptions.ingressPort); + this.addOption(binOptions.background); + this.addOption(binOptions.backgroundOutFile); + this.addOption(binOptions.backgroundErrFile); this.action(async (options) => { - const agentUtils = await import('../../agent/utils'); + options.clientHost = + options.clientHost ?? config.defaults.networkConfig.clientHost; + options.clientPort = + options.clientPort ?? config.defaults.networkConfig.clientPort; const { default: PolykeyAgent } = await import('../../PolykeyAgent'); - const background = options.background; - const password = await this.fs.promises.readFile(options.passwordFile, { - encoding: 'utf-8', - }); - - if (background) { - await agentUtils.spawnBackgroundAgent(options.nodePath, password); + // Password is necessary + // If recovery code is supplied, then this is the new password + const password = await binProcessors.processPassword( + options.passwordFile, + this.fs, + ); + const recoveryCodeIn = await binProcessors.processRecoveryCode( + options.recoveryCodeFile, + this.fs, + ); + const agentConfig = { + password, + nodePath: options.nodePath, + keysConfig: { + recoveryCode: recoveryCodeIn, + }, + networkConfig: { + clientHost: options.clientHost, + clientPort: options.clientPort, + ingressHost: options.ingressHost, + ingressPort: options.ingressPort, + }, + fresh: options.fresh, + }; + let recoveryCodeOut: RecoveryCode | undefined; + if (options.background) { + let agentOutPath = path.join(options.nodePath, 'out.log'); + let agentErrPath = path.join(options.nodePath, 'err.log'); + if (options.backgroundOutFile) { + agentOutPath = options.backgroundOutFile; + } + if (options.backgroundErrFile) { + agentErrPath = options.backgroundErrFile; + } + const agentOutFile = await this.fs.promises.open(agentOutPath, 'w'); + const agentErrFile = await this.fs.promises.open(agentErrPath, 'w'); + const agentProcess = child_process.fork( + path.join(__dirname, '../polykeyAgent.ts'), + [], + { + cwd: process.cwd(), + env: process.env, + detached: true, + serialization: 'advanced', + stdio: ['ignore', agentOutFile.fd, agentErrFile.fd, 'ipc'], + }, + ); + const { + p: agentProcessP, + resolveP: resolveAgentProcessP, + rejectP: rejectAgentProcessP, + } = promise(); + // Once the agent responds with message, it considered ok to go-ahead + agentProcess.once('message', (messageOut: AgentChildProcessOutput) => { + if (messageOut.status === 'SUCCESS') { + agentProcess.unref(); + agentProcess.disconnect(); + recoveryCodeOut = messageOut.recoveryCode; + resolveAgentProcessP(); + return; + } else { + rejectAgentProcessP( + new binErrors.ErrorCLIPolykeyAgentProcess( + 'Agent process responded with error', + messageOut.error, + ), + ); + return; + } + }); + // Handle error event during abnormal spawning, this is rare + agentProcess.once('error', (e) => { + rejectAgentProcessP( + new binErrors.ErrorCLIPolykeyAgentProcess(e.message), + ); + }); + // If the process exits during initial execution of polykeyAgent script + // Then it is an exception, because the agent process is meant to be a long-running daemon + agentProcess.once('close', (code, signal) => { + rejectAgentProcessP( + new binErrors.ErrorCLIPolykeyAgentProcess( + 'Agent process closed during fork', + { + code, + signal, + }, + ), + ); + }); + const messageIn: AgentChildProcessInput = { + agentConfig, + }; + agentProcess.send(messageIn, (e) => { + if (e != null) + rejectAgentProcessP( + new binErrors.ErrorCLIPolykeyAgentProcess( + 'Failed sending agent process message', + ), + ); + }); + await agentProcessP; } else { - const agent = await PolykeyAgent.createPolykeyAgent({ - password, + // eslint-disable-next-line prefer-const + let pkAgent: PolykeyAgent | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkAgent != null) await pkAgent.stop(); + }); + pkAgent = await PolykeyAgent.createPolykeyAgent({ + fs: this.fs, logger: this.logger.getChild(PolykeyAgent.name), - nodePath: options.nodePath, + ...agentConfig, }); - - // If started add handlers for terminating. - const termHandler = async () => { - await agent.stop(); - }; - process.on('SIGTERM', termHandler); // For kill command. - process.on('SIGHUP', termHandler); // Edge case if remote terminal closes. like someone runs agent start in ssh. - process.on('SIGINT', termHandler); // For ctrl+C + recoveryCodeOut = pkAgent.keyManager.getRecoveryCode(); + } + // Recovery code is only available if it was newly generated + if (recoveryCodeOut != null) { + process.stdout.write(recoveryCodeOut + '\n'); } }); } diff --git a/src/bin/agent/CommandStatus.ts b/src/bin/agent/CommandStatus.ts index 0a5a4e373..1b718bf1a 100644 --- a/src/bin/agent/CommandStatus.ts +++ b/src/bin/agent/CommandStatus.ts @@ -1,11 +1,8 @@ -import type { Metadata } from '@grpc/grpc-js'; - -import type PolykeyClient from '../../PolykeyClient'; +import type { StatusInfo } from '../../status/types'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; -import * as errors from '../../errors'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandStatus extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -18,59 +15,76 @@ class CommandStatus extends CommandPolykey { this.action(async (options) => { const { default: PolykeyClient } = await import('../../PolykeyClient'); const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); - let client: PolykeyClient; - try { - client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), + const clientStatus = await binProcessors.processClientStatus( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + // Undefined statusInfo is the equivalent of a DEAD status + const statusInfo: StatusInfo = + clientStatus.statusInfo != null + ? clientStatus.statusInfo + : { status: 'DEAD', data: {} }; + // If status is not LIVE, we return what we have in the status info + // If status is LIVE, then we connect and acquire agent information + if (statusInfo.status !== 'LIVE') { + process.stdout.write( + binUtils.outputFormatter({ + type: options.format === 'json' ? 'json' : 'dict', + data: { + status: statusInfo.status, + ...statusInfo.data, + }, + }), + ); + } else { + let pkClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - } catch (err) { - if (err instanceof errors.ErrorPolykey) { - process.stdout.write( - binUtils.outputFormatter({ - type: options.format === 'json' ? 'json' : 'list', - data: [`Agent is offline.`], - }), + let response; + try { + pkClient = await PolykeyClient.createPolykeyClient({ + nodeId: clientStatus.nodeId!, + host: clientStatus.clientHost!, + port: clientStatus.clientPort!, + nodePath: options.nodePath, + logger: this.logger.getChild(PolykeyClient.name), + }); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const emptyMessage = new utilsPB.EmptyMessage(); + response = await binUtils.retryAuthentication( + (auth) => pkClient.grpcClient.agentStatus(emptyMessage, auth), + meta, ); + } finally { + if (pkClient != null) await pkClient.stop(); } - throw err; - } - - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, - }); - - try { - const grpcClient = client.grpcClient; - const emptyMessage = new utilsPB.EmptyMessage(); - - const response = await binUtils.retryAuth( - (auth?: Metadata) => grpcClient.agentStatus(emptyMessage, auth), - meta, - ); const nodeMessage = response.getNodeId()!; const addressMessage = response.getAddress()!; const certMessage = response.getCert()!; - const nodeId = nodeMessage.getNodeId(); - const host = addressMessage.getHost(); - const port = addressMessage.getPort(); + const clientHost = addressMessage.getHost(); + const clientPort = addressMessage.getPort(); const certChain = certMessage.getCert(); process.stdout.write( binUtils.outputFormatter({ - type: options.format === 'json' ? 'json' : 'list', - data: [ - `Agent is online.`, - `Node ID: ${nodeId}`, - `Host: ${host}`, - `Port: ${port}`, - `Root Certificate Chain: ${certChain}`, - ], + type: options.format === 'json' ? 'json' : 'dict', + data: { + status: statusInfo.status, + nodeId, + clientHost, + clientPort, + certChain, + }, }), ); - } finally { - await client.stop(); } }); } diff --git a/src/bin/agent/CommandStop.ts b/src/bin/agent/CommandStop.ts index eefe1850f..92b6ee7b4 100644 --- a/src/bin/agent/CommandStop.ts +++ b/src/bin/agent/CommandStop.ts @@ -1,9 +1,8 @@ -import type { Metadata } from '@grpc/grpc-js'; - +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandStop extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -16,25 +15,36 @@ class CommandStop extends CommandPolykey { this.action(async (options) => { const { default: PolykeyClient } = await import('../../PolykeyClient'); const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); - - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); - - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); + let pkClient: PolykeyClient; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); const emptyMessage = new utilsPB.EmptyMessage(); - await binUtils.retryAuth( - (auth?: Metadata) => grpcClient.agentStop(emptyMessage, auth), + const grpcClient = pkClient.grpcClient; + await binUtils.retryAuthentication( + (auth) => grpcClient.agentStop(emptyMessage, auth), meta, ); - process.stdout.write( binUtils.outputFormatter({ type: options.format === 'json' ? 'json' : 'list', @@ -42,7 +52,7 @@ class CommandStop extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient! != null) await pkClient.stop(); } }); } diff --git a/src/bin/agent/CommandUnlock.ts b/src/bin/agent/CommandUnlock.ts index de688f859..c4467804e 100644 --- a/src/bin/agent/CommandUnlock.ts +++ b/src/bin/agent/CommandUnlock.ts @@ -1,10 +1,11 @@ import type { SessionToken } from '../../sessions/types'; import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandUnlock extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -20,21 +21,36 @@ class CommandUnlock extends CommandPolykey { '../../proto/js/polykey/v1/sessions/sessions_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const password = await parsers.parsePassword({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const password = await binProcessors.processPassword( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const passwordMessage = new sessionsPB.Password(); passwordMessage.setPassword(password); - const responseMessage = await binUtils.retryAuth( + const responseMessage = await binUtils.retryAuthentication( (metaRetried?: Metadata) => { return metaRetried != null ? grpcClient.sessionsUnlock(passwordMessage, metaRetried) @@ -44,10 +60,10 @@ class CommandUnlock extends CommandPolykey { const token: SessionToken = responseMessage.getToken() as SessionToken; // Write token to file - await client.session.writeToken(token); + await pkClient.session.writeToken(token); process.stdout.write('Client session started'); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/bootstrap/CommandBootstrap.ts b/src/bin/bootstrap/CommandBootstrap.ts index 90b57a82f..b1db103b2 100644 --- a/src/bin/bootstrap/CommandBootstrap.ts +++ b/src/bin/bootstrap/CommandBootstrap.ts @@ -1,48 +1,34 @@ -import prompts from 'prompts'; +import process from 'process'; import CommandPolykey from '../CommandPolykey'; -import * as binUtils from '../utils'; -import { bootstrapPolykeyState } from '../../bootstrap'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandBootstrap extends CommandPolykey { constructor(...args: ConstructorParameters) { super(...args); this.name('bootstrap'); this.description('Bootstrap Keynode State'); + this.addOption(binOptions.recoveryCodeFile); this.action(async (options) => { - let password; - if (options.passwordFile != null) { - password = await this.fs.promises.readFile(options.passwordFile, { - encoding: 'utf-8', - }); - } else { - let success = false; - while (!success) { - const response = await prompts({ - type: 'text', - name: 'password', - message: 'Please enter a password for your Polykey Node:', - }); - password = response.password; - const confirm = await prompts({ - type: 'text', - name: 'confirm', - message: 'Please re-enter your password:', - }); - const passwordConfirm = confirm.confirm; - if (password === passwordConfirm) { - success = true; - } else { - this.logger.warn('Passwords did not match, please try again'); - } - } - } - await bootstrapPolykeyState(options.nodePath, password); - process.stdout.write( - binUtils.outputFormatter({ - type: options.format === 'json' ? 'json' : 'list', - data: [`Polykey bootstrapped at Node Path: ${options.nodePath}`], - }), + const bootstrapUtils = await import('../../bootstrap/utils'); + const password = await binProcessors.processPassword( + options.passwordFile, + this.fs, ); + const recoveryCodeIn = await binProcessors.processRecoveryCode( + options.recoveryCodeFile, + this.fs, + ); + const recoveryCodeOut = await bootstrapUtils.bootstrapState({ + nodePath: options.nodePath, + password, + recoveryCode: recoveryCodeIn, + fresh: options.fresh, + fs: this.fs, + logger: this.logger, + }); + this.logger.info(`Bootstrapped ${options.nodePath}`); + process.stdout.write(recoveryCodeOut + '\n'); }); } } diff --git a/src/bin/errors.ts b/src/bin/errors.ts index c259c77ea..fe6a81ceb 100644 --- a/src/bin/errors.ts +++ b/src/bin/errors.ts @@ -1,54 +1,83 @@ import ErrorPolykey from '../ErrorPolykey'; +import sysexits from '../utils/sysexits'; class ErrorCLI extends ErrorPolykey {} -class ErrorCLINodePath extends ErrorPolykey { +class ErrorCLINodePath extends ErrorCLI { description = 'Cannot derive default node path from unknown platform'; - exitCode = 64; + exitCode = sysexits.USAGE; +} + +class ErrorCLIStatusNotLive extends ErrorCLI { + description = + 'Could not resolve nodeId, clientHost or clientPort from Status'; + exitCode = sysexits.USAGE; +} + +class ErrorCLIPolykeyAgentProcess extends ErrorCLI { + description = 'PolykeyAgent process could not be started'; + exitCode = sysexits.OSERR; +} + +class ErrorCLIPasswordMissing extends ErrorCLI { + description = + 'Password is necessary, provide it via PK_PASSWORD, --password-file or when prompted'; + exitCode = sysexits.USAGE; } -class ErrorInvalidArguments extends ErrorCLI { - description: string = 'An invalid combination of arguments was supplied'; - exitCode: number = 64; +class ErrorCLIPasswordFileRead extends ErrorCLI { + description = 'Failed to read password file'; + exitCode = sysexits.NOINPUT; } -class ErrorGRPCNotStarted extends ErrorCLI {} +class ErrorCLIRecoveryCodeFileRead extends ErrorCLI { + description = 'Failed to read recovery code file'; + exitCode = sysexits.NOINPUT; +} + +class ErrorCLIFileRead extends ErrorCLI { + description = 'Failed to read file'; + exitCode = sysexits.NOINPUT; +} class ErrorSecretPathFormat extends ErrorCLI { - description: string = - "Secret name needs to be of format: ':'"; - exitCode: number = 64; + description = "Secret name needs to be of format: ':'"; + exitCode = 64; } class ErrorVaultNameAmbiguous extends ErrorCLI { - description: string = + description = 'There is more than 1 Vault with this name. Please specify a Vault ID'; exitCode = 1; } class ErrorSecretsUndefined extends ErrorCLI { - description: string = 'At least one secret must be specified as an argument'; - exitCode: number = 64; + description = 'At least one secret must be specified as an argument'; + exitCode = 64; } class ErrorNodeFindFailed extends ErrorCLI { - description: string = 'Failed to find the node in the DHT'; - exitCode: number = 1; + description = 'Failed to find the node in the DHT'; + exitCode = 1; } class ErrorNodePingFailed extends ErrorCLI { - description: string = 'Node was not online or not found.'; - exitCode: number = 1; + description = 'Node was not online or not found.'; + exitCode = 1; } export { ErrorCLI, ErrorCLINodePath, - ErrorGRPCNotStarted, + ErrorCLIPasswordMissing, + ErrorCLIStatusNotLive, + ErrorCLIPolykeyAgentProcess, + ErrorCLIPasswordFileRead, + ErrorCLIRecoveryCodeFileRead, + ErrorCLIFileRead, ErrorSecretPathFormat, ErrorVaultNameAmbiguous, ErrorSecretsUndefined, - ErrorInvalidArguments, ErrorNodeFindFailed, ErrorNodePingFailed, }; diff --git a/src/bin/identities/CommandAllow.ts b/src/bin/identities/CommandAllow.ts index 0bc9688d4..ae1ba6b8a 100644 --- a/src/bin/identities/CommandAllow.ts +++ b/src/bin/identities/CommandAllow.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandAllow extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -29,18 +31,34 @@ class CommandAllow extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const setActionMessage = new permissionsPB.ActionSet(); setActionMessage.setAction(permissions); let name: string; @@ -51,7 +69,7 @@ class CommandAllow extends CommandPolykey { setActionMessage.setNode(nodeMessage); name = `${gestaltId.nodeId}`; // Trusting - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsSetByNode(setActionMessage, auth), meta, @@ -66,7 +84,7 @@ class CommandAllow extends CommandPolykey { gestaltId.providerId, gestaltId.identityId, )}`; - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsSetByIdentity(setActionMessage, auth), meta, @@ -79,7 +97,7 @@ class CommandAllow extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandAuthenticate.ts b/src/bin/identities/CommandAuthenticate.ts index d3ac76b6a..5902c76dc 100644 --- a/src/bin/identities/CommandAuthenticate.ts +++ b/src/bin/identities/CommandAuthenticate.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandAuthenticate extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,24 +22,40 @@ class CommandAuthenticate extends CommandPolykey { '../../proto/js/polykey/v1/identities/identities_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; // Constructing message. const providerMessage = new identitiesPB.Provider(); providerMessage.setProviderId(providerId); providerMessage.setMessage(identityId); // Sending message. - const successMessage = await binUtils.retryAuth( + const successMessage = await binUtils.retryAuthentication( async (meta: Metadata) => { const stream = grpcClient.identitiesAuthenticate( providerMessage, @@ -65,7 +82,7 @@ class CommandAuthenticate extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandClaim.ts b/src/bin/identities/CommandClaim.ts index 12ead0c80..d3c4aec4b 100644 --- a/src/bin/identities/CommandClaim.ts +++ b/src/bin/identities/CommandClaim.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandClaim extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,18 +22,34 @@ class CommandClaim extends CommandPolykey { '../../proto/js/polykey/v1/identities/identities_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; // Constructing message. const providerMessage = new identitiesPB.Provider(); @@ -40,13 +57,13 @@ class CommandClaim extends CommandPolykey { providerMessage.setMessage(identityId); // Sending message. - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.identitiesClaim(providerMessage, auth), meta, ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandDisallow.ts b/src/bin/identities/CommandDisallow.ts index 3d6fdd29d..40b79379f 100644 --- a/src/bin/identities/CommandDisallow.ts +++ b/src/bin/identities/CommandDisallow.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandDisallow extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -29,18 +31,33 @@ class CommandDisallow extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; let name: string; const setActionMessage = new permissionsPB.ActionSet(); setActionMessage.setAction(permissions); @@ -52,7 +69,7 @@ class CommandDisallow extends CommandPolykey { setActionMessage.setNode(nodeMessage); name = `${gestaltId.nodeId}`; // Trusting - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsUnsetByNode(setActionMessage, auth), meta, @@ -68,7 +85,7 @@ class CommandDisallow extends CommandPolykey { gestaltId.identityId, )}`; // Trusting. - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsUnsetByIdentity(setActionMessage, auth), meta, @@ -83,7 +100,7 @@ class CommandDisallow extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandDiscover.ts b/src/bin/identities/CommandDiscover.ts index f6f934c6a..49b7ed7e9 100644 --- a/src/bin/identities/CommandDiscover.ts +++ b/src/bin/identities/CommandDiscover.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandDiscover extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -27,18 +29,33 @@ class CommandDiscover extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; let name: string; if (gestaltId.nodeId) { @@ -46,7 +63,7 @@ class CommandDiscover extends CommandPolykey { const nodeMessage = new nodesPB.Node(); nodeMessage.setNodeId(gestaltId.nodeId); name = `${gestaltId.nodeId}`; - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsDiscoveryByNode(nodeMessage, auth), meta, @@ -60,7 +77,7 @@ class CommandDiscover extends CommandPolykey { gestaltId.providerId, gestaltId.identityId, )}`; - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsDiscoveryByIdentity(providerMessage, auth), meta, @@ -74,7 +91,7 @@ class CommandDiscover extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandGet.ts b/src/bin/identities/CommandGet.ts index b1eb7c57d..958c3ddfd 100644 --- a/src/bin/identities/CommandGet.ts +++ b/src/bin/identities/CommandGet.ts @@ -1,10 +1,12 @@ import type { Metadata } from '@grpc/grpc-js'; import type gestaltsPB from '../../proto/js/polykey/v1/gestalts/gestalts_pb'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandGet extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -28,25 +30,40 @@ class CommandGet extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; let res: gestaltsPB.Graph; if (gestaltId.nodeId) { // Getting from node. const nodeMessage = new nodesPB.Node(); nodeMessage.setNodeId(gestaltId.nodeId); - res = await binUtils.retryAuth( + res = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsGestaltGetByNode(nodeMessage, auth), meta, @@ -56,7 +73,7 @@ class CommandGet extends CommandPolykey { const providerMessage = new identitiesPB.Provider(); providerMessage.setProviderId(gestaltId.providerId); providerMessage.setMessage(gestaltId.identityId); - res = await binUtils.retryAuth( + res = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsGestaltGetByIdentity(providerMessage, auth), meta, @@ -92,7 +109,7 @@ class CommandGet extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandIdentities.ts b/src/bin/identities/CommandIdentities.ts index 83435d7e7..7854513a6 100644 --- a/src/bin/identities/CommandIdentities.ts +++ b/src/bin/identities/CommandIdentities.ts @@ -16,17 +16,17 @@ class CommandIdentities extends CommandPolykey { super(...args); this.name('identities'); this.description('Identities Operations'); - this.addCommand(new CommandAllow()); - this.addCommand(new CommandAuthenticate()); - this.addCommand(new CommandClaim()); - this.addCommand(new CommandDisallow()); - this.addCommand(new CommandDiscover()); - this.addCommand(new CommandGet()); - this.addCommand(new CommandList()); - this.addCommand(new CommandPermissions()); - this.addCommand(new CommandSearch()); - this.addCommand(new CommandTrust()); - this.addCommand(new CommandUntrust()); + this.addCommand(new CommandAllow(...args)); + this.addCommand(new CommandAuthenticate(...args)); + this.addCommand(new CommandClaim(...args)); + this.addCommand(new CommandDisallow(...args)); + this.addCommand(new CommandDiscover(...args)); + this.addCommand(new CommandGet(...args)); + this.addCommand(new CommandList(...args)); + this.addCommand(new CommandPermissions(...args)); + this.addCommand(new CommandSearch(...args)); + this.addCommand(new CommandTrust(...args)); + this.addCommand(new CommandUntrust(...args)); } } diff --git a/src/bin/identities/CommandList.ts b/src/bin/identities/CommandList.ts index 8a23073d5..13a93aa69 100644 --- a/src/bin/identities/CommandList.ts +++ b/src/bin/identities/CommandList.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandList extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -18,56 +20,74 @@ class CommandList extends CommandPolykey { const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const emptyMessage = new utilsPB.EmptyMessage(); let output: any; - const gestalts = await binUtils.retryAuth(async (meta: Metadata) => { - const gestalts: Array = []; - const stream = grpcClient.gestaltsGestaltList(emptyMessage, meta); - for await (const val of stream) { - const gestalt = JSON.parse(val.getName()); - const newGestalt: any = { - permissions: [], - nodes: [], - identities: [], - }; - for (const node of Object.keys(gestalt.nodes)) { - const nodeInfo = gestalt.nodes[node]; - newGestalt.nodes.push({ id: nodeInfo.id }); - } - for (const identity of Object.keys(gestalt.identities)) { - const identityInfo = gestalt.identities[identity]; - newGestalt.identities.push({ - providerId: identityInfo.providerId, - identityId: identityInfo.identityId, - }); + const gestalts = await binUtils.retryAuthentication( + async (meta: Metadata) => { + const gestalts: Array = []; + const stream = grpcClient.gestaltsGestaltList(emptyMessage, meta); + for await (const val of stream) { + const gestalt = JSON.parse(val.getName()); + const newGestalt: any = { + permissions: [], + nodes: [], + identities: [], + }; + for (const node of Object.keys(gestalt.nodes)) { + const nodeInfo = gestalt.nodes[node]; + newGestalt.nodes.push({ id: nodeInfo.id }); + } + for (const identity of Object.keys(gestalt.identities)) { + const identityInfo = gestalt.identities[identity]; + newGestalt.identities.push({ + providerId: identityInfo.providerId, + identityId: identityInfo.identityId, + }); + } + // Getting the permissions for the gestalt. + const nodeMessage = new nodesPB.Node(); + nodeMessage.setNodeId(newGestalt.nodes[0].id); + const actionsMessage = await binUtils.retryAuthentication( + (auth?: Metadata) => + grpcClient.gestaltsActionsGetByNode(nodeMessage, auth), + meta, + ); + const actionList = actionsMessage.getActionList(); + if (actionList.length === 0) newGestalt.permissions = null; + else newGestalt.permissions = actionList; + gestalts.push(newGestalt); } - // Getting the permissions for the gestalt. - const nodeMessage = new nodesPB.Node(); - nodeMessage.setNodeId(newGestalt.nodes[0].id); - const actionsMessage = await binUtils.retryAuth( - (auth?: Metadata) => - grpcClient.gestaltsActionsGetByNode(nodeMessage, auth), - meta, - ); - const actionList = actionsMessage.getActionList(); - if (actionList.length === 0) newGestalt.permissions = null; - else newGestalt.permissions = actionList; - gestalts.push(newGestalt); - } - return gestalts; - }, meta); + return gestalts; + }, + meta, + ); output = gestalts; if (options.format !== 'json') { @@ -103,7 +123,7 @@ class CommandList extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandPermissions.ts b/src/bin/identities/CommandPermissions.ts index 1ccb46b21..5985ee698 100644 --- a/src/bin/identities/CommandPermissions.ts +++ b/src/bin/identities/CommandPermissions.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandPermissions extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -25,24 +27,39 @@ class CommandPermissions extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; let actions; if (gestaltId.nodeId) { // Getting by Node. const nodeMessage = new nodesPB.Node(); nodeMessage.setNodeId(gestaltId.nodeId); - const res = await binUtils.retryAuth( + const res = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsGetByNode(nodeMessage, auth), meta, @@ -53,7 +70,7 @@ class CommandPermissions extends CommandPolykey { const providerMessage = new identitiesPB.Provider(); providerMessage.setProviderId(gestaltId.providerId); providerMessage.setMessage(gestaltId.identityId); - const res = await binUtils.retryAuth( + const res = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsGetByIdentity(providerMessage, auth), meta, @@ -68,7 +85,7 @@ class CommandPermissions extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandSearch.ts b/src/bin/identities/CommandSearch.ts index b81d0221e..5b12c7cfc 100644 --- a/src/bin/identities/CommandSearch.ts +++ b/src/bin/identities/CommandSearch.ts @@ -1,10 +1,12 @@ import type { Metadata } from '@grpc/grpc-js'; import type { ProviderId, IdentityId } from '../../identities/types'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandSearch extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -24,21 +26,37 @@ class CommandSearch extends CommandPolykey { '../../proto/js/polykey/v1/identities/identities_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const providerMessage = new identitiesPB.Provider(); providerMessage.setProviderId(providerId); - const res = await binUtils.retryAuth( + const res = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.identitiesInfoGet(providerMessage, auth), meta, @@ -56,7 +74,7 @@ class CommandSearch extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandTrust.ts b/src/bin/identities/CommandTrust.ts index 24c5a4926..b19142c8b 100644 --- a/src/bin/identities/CommandTrust.ts +++ b/src/bin/identities/CommandTrust.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandTrust extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -28,18 +30,33 @@ class CommandTrust extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const action = 'notify'; const setActionMessage = new permissionsPB.ActionSet(); setActionMessage.setAction(action); @@ -51,7 +68,7 @@ class CommandTrust extends CommandPolykey { nodeMessage.setNodeId(gestaltId.nodeId); setActionMessage.setNode(nodeMessage); name = `${gestaltId.nodeId}`; - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsSetByNode(setActionMessage, auth), meta, @@ -66,7 +83,7 @@ class CommandTrust extends CommandPolykey { gestaltId.providerId, gestaltId.identityId, )}`; - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsSetByIdentity(setActionMessage, auth), meta, @@ -80,7 +97,7 @@ class CommandTrust extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/identities/CommandUntrust.ts b/src/bin/identities/CommandUntrust.ts index 7d5587e8b..79b4c03aa 100644 --- a/src/bin/identities/CommandUntrust.ts +++ b/src/bin/identities/CommandUntrust.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binOptions from '../options'; +import * as binOptions from '../utils/options'; import * as binUtils from '../utils'; -import * as parsers from '../parsers'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandUntrust extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -28,18 +30,33 @@ class CommandUntrust extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const action = 'notify'; const setActionMessage = new permissionsPB.ActionSet(); setActionMessage.setAction(action); @@ -51,7 +68,7 @@ class CommandUntrust extends CommandPolykey { nodeMessage.setNodeId(gestaltId.nodeId); setActionMessage.setNode(nodeMessage); name = `${gestaltId.nodeId}`; - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsUnsetByNode(setActionMessage, auth), meta, @@ -66,7 +83,7 @@ class CommandUntrust extends CommandPolykey { gestaltId.providerId, gestaltId.identityId, )}`; - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.gestaltsActionsUnsetByIdentity(setActionMessage, auth), meta, @@ -80,7 +97,7 @@ class CommandUntrust extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandCert.ts b/src/bin/keys/CommandCert.ts index 8e8815001..a10bc47e0 100644 --- a/src/bin/keys/CommandCert.ts +++ b/src/bin/keys/CommandCert.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandCert extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -17,20 +18,36 @@ class CommandCert extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const emptyMessage = new utilsPB.EmptyMessage(); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysCertsGet(emptyMessage, auth), meta, ); @@ -42,7 +59,7 @@ class CommandCert extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandCertchain.ts b/src/bin/keys/CommandCertchain.ts index 8850fae6f..4aedc0fde 100644 --- a/src/bin/keys/CommandCertchain.ts +++ b/src/bin/keys/CommandCertchain.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandsCertchain extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -17,27 +18,46 @@ class CommandsCertchain extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const emptyMessage = new utilsPB.EmptyMessage(); - const data = await binUtils.retryAuth(async (meta: Metadata) => { - const data: Array = []; - const stream = grpcClient.keysCertsChainGet(emptyMessage, meta); - for await (const cert of stream) { - data.push(`Certificate:\t\t${cert.getCert()}`); - } - return data; - }, meta); + const data = await binUtils.retryAuthentication( + async (meta: Metadata) => { + const data: Array = []; + const stream = grpcClient.keysCertsChainGet(emptyMessage, meta); + for await (const cert of stream) { + data.push(`Certificate:\t\t${cert.getCert()}`); + } + return data; + }, + meta, + ); process.stdout.write( binUtils.outputFormatter({ @@ -46,7 +66,7 @@ class CommandsCertchain extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandDecrypt.ts b/src/bin/keys/CommandDecrypt.ts index a987f0512..7410e9486 100644 --- a/src/bin/keys/CommandDecrypt.ts +++ b/src/bin/keys/CommandDecrypt.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; +import * as binErrors from '../errors'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandDecrypt extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,26 +23,51 @@ class CommandDecrypt extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const keysPB = await import('../../proto/js/polykey/v1/keys/keys_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; - const cryptoMessage = new keysPB.Crypto(); - const cipherText = await parsers.parseFilePath({ - filePath, - fs: this.fs, + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), }); + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; + const cryptoMessage = new keysPB.Crypto(); + let cipherText: string; + try { + cipherText = await this.fs.promises.readFile(filePath, { + encoding: 'binary', + }); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } + cryptoMessage.setData(cipherText); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysDecrypt(cryptoMessage, auth), meta, ); @@ -52,7 +79,7 @@ class CommandDecrypt extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandEncrypt.ts b/src/bin/keys/CommandEncrypt.ts index 891dddce1..02ad669bf 100644 --- a/src/bin/keys/CommandEncrypt.ts +++ b/src/bin/keys/CommandEncrypt.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; +import * as binErrors from '../errors'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandEncypt extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,27 +23,52 @@ class CommandEncypt extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const keysPB = await import('../../proto/js/polykey/v1/keys/keys_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const cryptoMessage = new keysPB.Crypto(); - const plainText = await parsers.parseFilePath({ - filePath, - fs: this.fs, - }); + let plainText: string; + try { + plainText = await this.fs.promises.readFile(filePath, { + encoding: 'binary', + }); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } cryptoMessage.setData(plainText); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysEncrypt(cryptoMessage, auth), meta, ); @@ -53,7 +80,7 @@ class CommandEncypt extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandKeys.ts b/src/bin/keys/CommandKeys.ts index 919d67cde..4145efe66 100644 --- a/src/bin/keys/CommandKeys.ts +++ b/src/bin/keys/CommandKeys.ts @@ -15,16 +15,16 @@ class CommandKeys extends CommandPolykey { super(...args); this.name('keys'); this.description('Keys Operations'); - this.addCommand(new CommandCert()); - this.addCommand(new CommandCertchain()); - this.addCommand(new CommandDecrypt()); - this.addCommand(new CommandEncrypt()); - this.addCommand(new CommandPassword()); - this.addCommand(new CommandRenew()); - this.addCommand(new CommandReset()); - this.addCommand(new CommandRoot()); - this.addCommand(new CommandSign()); - this.addCommand(new CommandVerify()); + this.addCommand(new CommandCert(...args)); + this.addCommand(new CommandCertchain(...args)); + this.addCommand(new CommandDecrypt(...args)); + this.addCommand(new CommandEncrypt(...args)); + this.addCommand(new CommandPassword(...args)); + this.addCommand(new CommandRenew(...args)); + this.addCommand(new CommandReset(...args)); + this.addCommand(new CommandRoot(...args)); + this.addCommand(new CommandSign(...args)); + this.addCommand(new CommandVerify(...args)); } } diff --git a/src/bin/keys/CommandPassword.ts b/src/bin/keys/CommandPassword.ts index 88753e0af..3532f8fa5 100644 --- a/src/bin/keys/CommandPassword.ts +++ b/src/bin/keys/CommandPassword.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandPassword extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -23,26 +24,42 @@ class CommandPassword extends CommandPolykey { '../../proto/js/polykey/v1/sessions/sessions_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const passwordMessage = new sessionsPB.Password(); - const password = await this.fs.promises.readFile(passwordPath, { - encoding: 'utf-8', - }); + const password = await binProcessors.processPassword( + passwordPath, + this.fs, + ); passwordMessage.setPassword(password); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysPasswordChange(passwordMessage, auth), meta, @@ -55,7 +72,7 @@ class CommandPassword extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandRenew.ts b/src/bin/keys/CommandRenew.ts index 35c5922f8..d3472f51e 100644 --- a/src/bin/keys/CommandRenew.ts +++ b/src/bin/keys/CommandRenew.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandRenew extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,26 +22,42 @@ class CommandRenew extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const keysPB = await import('../../proto/js/polykey/v1/keys/keys_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const keyMessage = new keysPB.Key(); - const password = await this.fs.promises.readFile(passwordPath, { - encoding: 'utf-8', - }); + const password = await binProcessors.processPassword( + passwordPath, + this.fs, + ); keyMessage.setName(password); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysKeyPairRenew(keyMessage, auth), meta, ); @@ -52,7 +69,7 @@ class CommandRenew extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandReset.ts b/src/bin/keys/CommandReset.ts index 8466c461c..d8b4b0f47 100644 --- a/src/bin/keys/CommandReset.ts +++ b/src/bin/keys/CommandReset.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandReset extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,26 +22,42 @@ class CommandReset extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const keysPB = await import('../../proto/js/polykey/v1/keys/keys_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const keyMessage = new keysPB.Key(); - const password = await this.fs.promises.readFile(passwordPath, { - encoding: 'utf-8', - }); + const password = await binProcessors.processPassword( + passwordPath, + this.fs, + ); keyMessage.setName(password); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysKeyPairReset(keyMessage, auth), meta, ); @@ -52,7 +69,7 @@ class CommandReset extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandRoot.ts b/src/bin/keys/CommandRoot.ts index 04ffa3bf0..9af8b359c 100644 --- a/src/bin/keys/CommandRoot.ts +++ b/src/bin/keys/CommandRoot.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandRoot extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -18,21 +19,36 @@ class CommandRoot extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const emptyMessage = new utilsPB.EmptyMessage(); - const keyPair = await binUtils.retryAuth( + const keyPair = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysKeyPairRoot(emptyMessage, auth), meta, ); @@ -53,7 +69,7 @@ class CommandRoot extends CommandPolykey { ); } } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandSign.ts b/src/bin/keys/CommandSign.ts index 2a9e516b3..fd5e4ab68 100644 --- a/src/bin/keys/CommandSign.ts +++ b/src/bin/keys/CommandSign.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; +import * as binErrors from '../errors'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandSign extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,24 +23,51 @@ class CommandSign extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const keysPB = await import('../../proto/js/polykey/v1/keys/keys_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const cryptoMessage = new keysPB.Crypto(); - const data = await parsers.parseFilePath({ filePath, fs: this.fs }); + let data: string; + try { + data = await this.fs.promises.readFile(filePath, { + encoding: 'binary', + }); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } cryptoMessage.setData(data); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysSign(cryptoMessage, auth), meta, ); @@ -50,7 +79,7 @@ class CommandSign extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/keys/CommandVerify.ts b/src/bin/keys/CommandVerify.ts index 6f8b22388..4569a274f 100644 --- a/src/bin/keys/CommandVerify.ts +++ b/src/bin/keys/CommandVerify.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; +import * as binErrors from '../errors'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandVerify extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -25,30 +27,57 @@ class CommandVerify extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const keysPB = await import('../../proto/js/polykey/v1/keys/keys_pb'); - const client = await PolykeyClient.createPolykeyClient({ - logger: this.logger.getChild(PolykeyClient.name), - nodePath: options.nodePath, - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const cryptoMessage = new keysPB.Crypto(); - const data = await parsers.parseFilePath({ filePath, fs: this.fs }); - const signature = await parsers.parseFilePath({ - filePath: signaturePath, - fs: this.fs, - }); + let data: string; + let signature: string; + try { + data = await this.fs.promises.readFile(filePath, { + encoding: 'binary', + }); + signature = await this.fs.promises.readFile(signaturePath, { + encoding: 'binary', + }); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } cryptoMessage.setData(data); cryptoMessage.setSignature(signature); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.keysVerify(cryptoMessage, auth), meta, ); @@ -60,7 +89,7 @@ class CommandVerify extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/nodes/CommandAdd.ts b/src/bin/nodes/CommandAdd.ts index 906d89763..b079783d3 100644 --- a/src/bin/nodes/CommandAdd.ts +++ b/src/bin/nodes/CommandAdd.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; -import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binUtils from '../utils/utils'; +import * as binProcessors from '../utils/processors'; +import * as binOptions from '../utils/options'; class CommandAdd extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -20,25 +21,40 @@ class CommandAdd extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const nodeAddressMessage = new nodesPB.NodeAddress(); nodeAddressMessage.setNodeId(nodeId); nodeAddressMessage.setAddress( new nodesPB.Address().setHost(host).setPort(port), ); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.nodesAdd(nodeAddressMessage, auth), meta, ); @@ -50,7 +66,7 @@ class CommandAdd extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/nodes/CommandClaim.ts b/src/bin/nodes/CommandClaim.ts index ee3346087..fd0e783ef 100644 --- a/src/bin/nodes/CommandClaim.ts +++ b/src/bin/nodes/CommandClaim.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandClaim extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -22,18 +23,33 @@ class CommandClaim extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const nodeClaimMessage = new nodesPB.Claim(); nodeClaimMessage.setNodeId(nodeId); if (options.forceInvite) { @@ -42,7 +58,7 @@ class CommandClaim extends CommandPolykey { nodeClaimMessage.setForceInvite(false); } - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.nodesClaim(nodeClaimMessage, auth), meta, ); @@ -68,7 +84,7 @@ class CommandClaim extends CommandPolykey { ); } } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/nodes/CommandFind.ts b/src/bin/nodes/CommandFind.ts index b274bb5b9..68f0e22fe 100644 --- a/src/bin/nodes/CommandFind.ts +++ b/src/bin/nodes/CommandFind.ts @@ -1,10 +1,11 @@ import type { Host, Port } from '../../network/types'; import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandFind extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -22,18 +23,33 @@ class CommandFind extends CommandPolykey { const CLIErrors = await import('../errors'); const nodesErrors = await import('../../nodes/errors'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const nodeMessage = new nodesPB.Node(); nodeMessage.setNodeId(nodeId); const result = { @@ -44,7 +60,7 @@ class CommandFind extends CommandPolykey { port: 0, }; try { - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.nodesFind(nodeMessage, auth), meta, ); @@ -81,7 +97,7 @@ class CommandFind extends CommandPolykey { if (!result.success) throw new CLIErrors.ErrorNodeFindFailed(result.message); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/nodes/CommandNodes.ts b/src/bin/nodes/CommandNodes.ts index 71d21efb3..6827d01f3 100644 --- a/src/bin/nodes/CommandNodes.ts +++ b/src/bin/nodes/CommandNodes.ts @@ -9,10 +9,10 @@ class CommandNodes extends CommandPolykey { super(...args); this.name('nodes'); this.description('Nodes Operations'); - this.addCommand(new CommandAdd()); - this.addCommand(new CommandClaim()); - this.addCommand(new CommandFind()); - this.addCommand(new CommandPing()); + this.addCommand(new CommandAdd(...args)); + this.addCommand(new CommandClaim(...args)); + this.addCommand(new CommandFind(...args)); + this.addCommand(new CommandPing(...args)); } } diff --git a/src/bin/nodes/CommandPing.ts b/src/bin/nodes/CommandPing.ts index 796788133..8d32f339f 100644 --- a/src/bin/nodes/CommandPing.ts +++ b/src/bin/nodes/CommandPing.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandPing extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -20,24 +21,39 @@ class CommandPing extends CommandPolykey { const CLIErrors = await import('../errors'); const nodesErrors = await import('../../nodes/errors'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const nodeMessage = new nodesPB.Node(); nodeMessage.setNodeId(nodeId); let statusMessage; let error; try { - statusMessage = await binUtils.retryAuth( + statusMessage = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.nodesPing(nodeMessage, auth), meta, ); @@ -71,7 +87,7 @@ class CommandPing extends CommandPolykey { if (error != null) throw error; } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/notifications/CommandClear.ts b/src/bin/notifications/CommandClear.ts index d98df482b..563a5ccb1 100644 --- a/src/bin/notifications/CommandClear.ts +++ b/src/bin/notifications/CommandClear.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandClear extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -17,21 +18,37 @@ class CommandClear extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const emptyMessage = new utilsPB.EmptyMessage(); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.notificationsClear(emptyMessage, auth), meta, @@ -44,7 +61,7 @@ class CommandClear extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/notifications/CommandNotifications.ts b/src/bin/notifications/CommandNotifications.ts index 138520134..aff48da12 100644 --- a/src/bin/notifications/CommandNotifications.ts +++ b/src/bin/notifications/CommandNotifications.ts @@ -8,9 +8,9 @@ class CommandNotifications extends CommandPolykey { super(...args); this.name('notifications'); this.description('Notifications Operations'); - this.addCommand(new CommandClear()); - this.addCommand(new CommandRead()); - this.addCommand(new CommandSend()); + this.addCommand(new CommandClear(...args)); + this.addCommand(new CommandRead(...args)); + this.addCommand(new CommandSend(...args)); } } diff --git a/src/bin/notifications/CommandRead.ts b/src/bin/notifications/CommandRead.ts index 5143a7718..b3d174f60 100644 --- a/src/bin/notifications/CommandRead.ts +++ b/src/bin/notifications/CommandRead.ts @@ -1,10 +1,11 @@ import type { Notification } from '../../notifications/types'; import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandRead extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -35,18 +36,33 @@ class CommandRead extends CommandPolykey { ); const notificationsUtils = await import('../../notifications/utils'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const notificationsReadMessage = new notificationsPB.Read(); if (options.unread) { @@ -57,7 +73,7 @@ class CommandRead extends CommandPolykey { notificationsReadMessage.setNumber(options.number); notificationsReadMessage.setOrder(options.order); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.notificationsRead(notificationsReadMessage, auth), meta, @@ -151,7 +167,7 @@ class CommandRead extends CommandPolykey { ); } } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/notifications/CommandSend.ts b/src/bin/notifications/CommandSend.ts index 83674d61e..0375c0100 100644 --- a/src/bin/notifications/CommandSend.ts +++ b/src/bin/notifications/CommandSend.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandSend extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,25 +22,40 @@ class CommandSend extends CommandPolykey { '../../proto/js/polykey/v1/notifications/notifications_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const notificationsSendMessage = new notificationsPB.Send(); const generalMessage = new notificationsPB.General(); generalMessage.setMessage(message); notificationsSendMessage.setReceiverId(node); notificationsSendMessage.setData(generalMessage); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.notificationsSend(notificationsSendMessage, auth), meta, @@ -56,7 +72,7 @@ class CommandSend extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/options.ts b/src/bin/options.ts deleted file mode 100644 index 7519c3ed7..000000000 --- a/src/bin/options.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Options and Arguments used by commands - * Use PolykeyCommand.addOption or PolykeyCommand.addArgument - * @module - */ - -import commander from 'commander'; -import * as parsers from './parsers'; -import * as binUtils from './utils'; - -/** - * Node path is the path to node state - * This is a directory on the filesystem - * This is optional, if it is not specified, we will derive - * platform-specific default node path - * On unknown platforms the the default is undefined - */ -const nodePath = new commander.Option( - '-np, --node-path ', - 'Path to Node State', -).default(binUtils.getDefaultNodePath()); - -/** - * Formatting choice of human, json, defaults to human - */ -const format = new commander.Option('-f, --format ', 'Output Format') - .choices(['human', 'json']) - .default('human'); - -/** - * Sets log level, defaults to 0, multiple uses will increase verbosity level - */ -const verbose = new commander.Option('-v, --verbose', 'Log Verbose Messages') - .argParser((_, p: number) => { - return p + 1; - }) - .default(0); - -const nodeId = new commander.Option('-ni', '--node-id ').env('PK_NODE_ID'); - -const clientHost = new commander.Option( - '-ch, --client-host
', - 'Client Host Address', -) - .env('PK_CLIENT_HOST') - .default('127.0.0.1'); - -const clientPort = new commander.Option( - '-cp, --client-port ', - 'Client Port', -) - .argParser(parsers.parseNumber) - .env('PK_CLIENT_PORT') - .default(0); - -const recoveryCodeFile = new commander.Option( - '-rcf, --recovery-code-file ', - 'Path to Recovery Code', -); - -const passwordFile = new commander.Option( - '-pf, --password-file ', - 'Path to Password', -); - -export { - nodePath, - format, - verbose, - nodeId, - clientHost, - clientPort, - recoveryCodeFile, - passwordFile, -}; diff --git a/src/bin/polykey.ts b/src/bin/polykey.ts index 28f978564..489a5ead7 100644 --- a/src/bin/polykey.ts +++ b/src/bin/polykey.ts @@ -1,6 +1,18 @@ #!/usr/bin/env node +import fs from 'fs'; import process from 'process'; +/** + * Hack for wiping out the threads signal handlers + * See: https://github.com/andywer/threads.js/issues/388 + * This is done statically during this import + * It is essential that the threads import here is very first import of threads module + * in the entire codebase for this hack to work + * If the worker manager is used, it must be stopped gracefully with the PolykeyAgent + */ +import 'threads'; +process.removeAllListeners('SIGINT'); +process.removeAllListeners('SIGTERM'); import commander from 'commander'; import CommandBootstrap from './bootstrap'; import CommandAgent from './agent'; @@ -10,30 +22,35 @@ import CommandKeys from './keys'; import CommandNodes from './nodes'; import CommandIdentities from './identities'; import CommandNotifications from './notifications'; - import CommandPolykey from './CommandPolykey'; import * as binUtils from './utils'; import ErrorPolykey from '../ErrorPolykey'; import config from '../config'; async function main(argv = process.argv): Promise { - const rootCommand = new CommandPolykey(); + // Registers signal and process error handler + // Any resource cleanup must be resolved within their try-catch block + // Leaf commands may register exit handlers in case of signal exits + // Process error handler should only be used by non-terminating commands + // When testing, this entire must be mocked to be a noop + const exitHandlers = new binUtils.ExitHandlers(); + const rootCommand = new CommandPolykey({ exitHandlers, fs }); rootCommand.name('polykey'); rootCommand.version(config.sourceVersion); rootCommand.description('Polykey CLI'); - rootCommand.addCommand(new CommandBootstrap()); - rootCommand.addCommand(new CommandAgent()); - rootCommand.addCommand(new CommandNodes()); - rootCommand.addCommand(new CommandSecrets()); - rootCommand.addCommand(new CommandKeys()); - rootCommand.addCommand(new CommandVaults()); - rootCommand.addCommand(new CommandIdentities()); - rootCommand.addCommand(new CommandNotifications()); + rootCommand.addCommand(new CommandBootstrap({ exitHandlers, fs })); + rootCommand.addCommand(new CommandAgent({ exitHandlers, fs })); + rootCommand.addCommand(new CommandNodes({ exitHandlers, fs })); + rootCommand.addCommand(new CommandSecrets({ exitHandlers, fs })); + rootCommand.addCommand(new CommandKeys({ exitHandlers, fs })); + rootCommand.addCommand(new CommandVaults({ exitHandlers, fs })); + rootCommand.addCommand(new CommandIdentities({ exitHandlers, fs })); + rootCommand.addCommand(new CommandNotifications({ exitHandlers, fs })); try { // `argv` will have node path and the script path as the first 2 parameters // navigates and executes the subcommand await rootCommand.parseAsync(argv); - // Successful execution + // Successful execution (even if the command was non-terminating) process.exitCode = 0; } catch (e) { if (e instanceof commander.CommanderError) { diff --git a/src/bin/polykeyAgent.ts b/src/bin/polykeyAgent.ts new file mode 100644 index 000000000..2ad0a2892 --- /dev/null +++ b/src/bin/polykeyAgent.ts @@ -0,0 +1,129 @@ +#!/usr/bin/env node +/** + * The is an internal script for running the PolykeyAgent as a child process + * This is not to be exported for external execution + * @module + */ +import type { AgentChildProcessInput, AgentChildProcessOutput } from './types'; +import fs from 'fs'; +import process from 'process'; +/** + * Hack for wiping out the threads signal handlers + * See: https://github.com/andywer/threads.js/issues/388 + * This is done statically during this import + * It is essential that the threads import here is very first import of threads module + * in the entire codebase for this hack to work + * If the worker manager is used, it must be stopped gracefully with the PolykeyAgent + */ +import 'threads'; +process.removeAllListeners('SIGINT'); +process.removeAllListeners('SIGTERM'); +import Logger, { StreamHandler } from '@matrixai/logger'; +import * as binUtils from './utils'; +import PolykeyAgent from '../PolykeyAgent'; +import ErrorPolykey from '../ErrorPolykey'; +import { promisify, promise } from '../utils'; + +const logger = new Logger('polykey', undefined, [new StreamHandler()]); + +/** + * Starts the agent process + */ +async function main(_argv = process.argv): Promise { + const exitHandlers = new binUtils.ExitHandlers(); + const processSend = promisify(process.send!); + const { p: messageInP, resolveP: resolveMessageInP } = + promise(); + process.once('message', (data) => { + resolveMessageInP(data); + }); + const messageIn = await messageInP; + let pkAgent: PolykeyAgent; + exitHandlers.handlers.push(async () => { + if (pkAgent != null) await pkAgent.stop(); + }); + try { + pkAgent = await PolykeyAgent.createPolykeyAgent({ + fs, + logger: logger.getChild(PolykeyAgent.name), + ...messageIn.agentConfig, + }); + } catch (e) { + if (e instanceof ErrorPolykey) { + process.stderr.write( + binUtils.outputFormatter({ + type: 'error', + name: e.name, + description: e.description, + message: e.message, + }), + ); + process.exitCode = e.exitCode; + } else { + // Unknown error, this should not happen + process.stderr.write( + binUtils.outputFormatter({ + type: 'error', + name: e.name, + description: e.message, + }), + ); + process.exitCode = 255; + } + const messageOut: AgentChildProcessOutput = { + status: 'FAILURE', + error: { + name: e.name, + description: e.description, + message: e.message, + exitCode: e.exitCode, + data: e.data, + stack: e.stack, + }, + }; + try { + await processSend(messageOut); + } catch (e) { + // If processSend itself failed here + // There's no point attempting to propagate the error to the parent + process.stderr.write( + binUtils.outputFormatter({ + type: 'error', + name: e.name, + description: e.message, + }), + ); + process.exitCode = 255; + } + return process.exitCode; + } + const messageOut: AgentChildProcessOutput = { + status: 'SUCCESS', + recoveryCode: pkAgent.keyManager.getRecoveryCode(), + }; + try { + await processSend(messageOut); + } catch (e) { + // If processSend itself failed here + // There's no point attempting to propagate the error to the parent + process.stderr.write( + binUtils.outputFormatter({ + type: 'error', + name: e.name, + description: e.message, + }), + ); + process.exitCode = 255; + return process.exitCode; + } + process.exitCode = 0; + return process.exitCode; +} + +if (require.main === module) { + (async () => { + await main(); + })(); +} + +export default main; diff --git a/src/bin/secrets/CommandCreate.ts b/src/bin/secrets/CommandCreate.ts index 260f8f5d3..1a6a08d3c 100644 --- a/src/bin/secrets/CommandCreate.ts +++ b/src/bin/secrets/CommandCreate.ts @@ -1,9 +1,12 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; +import * as binErrors from '../errors'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandCreate extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -31,28 +34,54 @@ class CommandCreate extends CommandPolykey { '../../proto/js/polykey/v1/secrets/secrets_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const secretMessage = new secretsPB.Secret(); const vaultMessage = new vaultsPB.Vault(); secretMessage.setVault(vaultMessage); vaultMessage.setNameOrId(secretPath[0]); secretMessage.setSecretName(secretPath[1]); - const content = await this.fs.promises.readFile(directoryPath); + let content: Buffer; + try { + content = await this.fs.promises.readFile(directoryPath); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } secretMessage.setSecretContent(content); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsSecretsNew(secretMessage, auth), meta, ); @@ -66,7 +95,7 @@ class CommandCreate extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/secrets/CommandDelete.ts b/src/bin/secrets/CommandDelete.ts index 77b64ca9b..b987fe8aa 100644 --- a/src/bin/secrets/CommandDelete.ts +++ b/src/bin/secrets/CommandDelete.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandDelete extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -28,25 +30,40 @@ class CommandDelete extends CommandPolykey { '../../proto/js/polykey/v1/secrets/secrets_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); const secretMessage = new secretsPB.Secret(); vaultMessage.setNameOrId(secretPath[0]); secretMessage.setVault(vaultMessage); secretMessage.setSecretName(secretPath[1]); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsSecretsDelete(secretMessage, auth), meta, @@ -61,7 +78,7 @@ class CommandDelete extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/secrets/CommandDir.ts b/src/bin/secrets/CommandDir.ts index 6c8c96784..1c5b93868 100644 --- a/src/bin/secrets/CommandDir.ts +++ b/src/bin/secrets/CommandDir.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandDir extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -27,25 +28,40 @@ class CommandDir extends CommandPolykey { '../../proto/js/polykey/v1/secrets/secrets_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + const pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const secretDirectoryMessage = new secretsPB.Directory(); const vaultMessage = new vaultsPB.Vault(); vaultMessage.setNameOrId(vaultName); secretDirectoryMessage.setVault(vaultMessage); secretDirectoryMessage.setSecretDirectory(directoryPath); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsSecretsNewDir(secretDirectoryMessage, auth), meta, @@ -60,7 +76,7 @@ class CommandDir extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/secrets/CommandEdit.ts b/src/bin/secrets/CommandEdit.ts index f3d9eb73d..2d1bdda26 100644 --- a/src/bin/secrets/CommandEdit.ts +++ b/src/bin/secrets/CommandEdit.ts @@ -1,9 +1,12 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; +import * as binErrors from '../errors'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandEdit extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -29,25 +32,41 @@ class CommandEdit extends CommandPolykey { '../../proto/js/polykey/v1/secrets/secrets_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const secretMessage = new secretsPB.Secret(); const vaultMessage = new vaultsPB.Vault(); vaultMessage.setNameOrId(secretPath[0]); secretMessage.setVault(vaultMessage); secretMessage.setSecretName(secretPath[1]); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsSecretsGet(secretMessage, auth), meta, ); @@ -63,7 +82,17 @@ class CommandEdit extends CommandPolykey { execSync(`$EDITOR \"${tmpFile}\"`, { stdio: 'inherit' }); - const content = await this.fs.promises.readFile(tmpFile); + let content: Buffer; + try { + content = await this.fs.promises.readFile(tmpFile); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } secretMessage.setVault(vaultMessage); secretMessage.setSecretContent(content); @@ -83,7 +112,7 @@ class CommandEdit extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/secrets/CommandEnv.ts b/src/bin/secrets/CommandEnv.ts index 09e2770cb..cab6a350f 100644 --- a/src/bin/secrets/CommandEnv.ts +++ b/src/bin/secrets/CommandEnv.ts @@ -9,7 +9,7 @@ // import * as grpcErrors from '../../grpc/errors'; // import CommandPolykey from '../CommandPolykey'; -// import * as binOptions from '../options'; +// import * as binOptions from '../utils/options'; // class CommandEnv extends CommandPolykey { // constructor(...args: ConstructorParameters) { diff --git a/src/bin/secrets/CommandGet.ts b/src/bin/secrets/CommandGet.ts index 0f0d5f940..21e412643 100644 --- a/src/bin/secrets/CommandGet.ts +++ b/src/bin/secrets/CommandGet.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandGet extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -31,18 +33,33 @@ class CommandGet extends CommandPolykey { '../../proto/js/polykey/v1/secrets/secrets_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const isEnv: boolean = options.env ?? false; const secretMessage = new secretsPB.Secret(); const vaultMessage = new vaultsPB.Vault(); @@ -50,7 +67,7 @@ class CommandGet extends CommandPolykey { secretMessage.setVault(vaultMessage); secretMessage.setSecretName(secretPath[1]); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsSecretsGet(secretMessage, auth), meta, ); @@ -78,7 +95,7 @@ class CommandGet extends CommandPolykey { ); } } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/secrets/CommandList.ts b/src/bin/secrets/CommandList.ts index 3b822e76c..f43448e6d 100644 --- a/src/bin/secrets/CommandList.ts +++ b/src/bin/secrets/CommandList.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandList extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,29 +22,47 @@ class CommandList extends CommandPolykey { '../../proto/js/polykey/v1/vaults/vaults_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); vaultMessage.setNameOrId(vaultName); - const data = await binUtils.retryAuth(async (meta: Metadata) => { - const data: Array = []; - const stream = grpcClient.vaultsSecretsList(vaultMessage, meta); - for await (const secret of stream) { - data.push(`${secret.getSecretName()}`); - } - return data; - }, meta); + const data = await binUtils.retryAuthentication( + async (meta: Metadata) => { + const data: Array = []; + const stream = grpcClient.vaultsSecretsList(vaultMessage, meta); + for await (const secret of stream) { + data.push(`${secret.getSecretName()}`); + } + return data; + }, + meta, + ); process.stdout.write( binUtils.outputFormatter({ type: options.format === 'json' ? 'json' : 'list', @@ -51,7 +70,7 @@ class CommandList extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/secrets/CommandMkdir.ts b/src/bin/secrets/CommandMkdir.ts index 0193e3b44..9df108740 100644 --- a/src/bin/secrets/CommandMkdir.ts +++ b/src/bin/secrets/CommandMkdir.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandMkdir extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -25,18 +27,33 @@ class CommandMkdir extends CommandPolykey { '../../proto/js/polykey/v1/vaults/vaults_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMkdirMessage = new vaultsPB.Mkdir(); const vaultMessage = new vaultsPB.Vault(); vaultMessage.setNameOrId(secretPath[0]); @@ -44,7 +61,7 @@ class CommandMkdir extends CommandPolykey { vaultMkdirMessage.setDirName(secretPath[1]); vaultMkdirMessage.setRecursive(options.recursive); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsSecretsMkdir(vaultMkdirMessage, auth), meta, @@ -59,7 +76,7 @@ class CommandMkdir extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/secrets/CommandRename.ts b/src/bin/secrets/CommandRename.ts index 4e4cc89cf..2a0a33713 100644 --- a/src/bin/secrets/CommandRename.ts +++ b/src/bin/secrets/CommandRename.ts @@ -1,9 +1,11 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandRename extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -28,18 +30,33 @@ class CommandRename extends CommandPolykey { '../../proto/js/polykey/v1/secrets/secrets_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); const secretMessage = new secretsPB.Secret(); const secretRenameMessage = new secretsPB.Rename(); @@ -49,7 +66,7 @@ class CommandRename extends CommandPolykey { secretMessage.setSecretName(secretPath[1]); secretRenameMessage.setNewName(newSecretName); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsSecretsRename(secretRenameMessage, auth), meta, @@ -64,7 +81,7 @@ class CommandRename extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/secrets/CommandSecrets.ts b/src/bin/secrets/CommandSecrets.ts index 18eaff823..904592b93 100644 --- a/src/bin/secrets/CommandSecrets.ts +++ b/src/bin/secrets/CommandSecrets.ts @@ -15,16 +15,16 @@ class CommandSecrets extends CommandPolykey { super(...args); this.name('secrets'); this.description('Secrets Operations'); - this.addCommand(new CommandCreate()); - this.addCommand(new CommandDelete()); - this.addCommand(new CommandDir()); - this.addCommand(new CommandEdit()); - // This.addCommand(new CommandEnv); - this.addCommand(new CommandGet()); - this.addCommand(new CommandList()); - this.addCommand(new CommandMkdir()); - this.addCommand(new CommandRename()); - this.addCommand(new CommandUpdate()); + this.addCommand(new CommandCreate(...args)); + this.addCommand(new CommandDelete(...args)); + this.addCommand(new CommandDir(...args)); + this.addCommand(new CommandEdit(...args)); + // This.addCommand(new CommandEnv(...args)); + this.addCommand(new CommandGet(...args)); + this.addCommand(new CommandList(...args)); + this.addCommand(new CommandMkdir(...args)); + this.addCommand(new CommandRename(...args)); + this.addCommand(new CommandUpdate(...args)); } } diff --git a/src/bin/secrets/CommandUpdate.ts b/src/bin/secrets/CommandUpdate.ts index 337efca8a..ef1df6806 100644 --- a/src/bin/secrets/CommandUpdate.ts +++ b/src/bin/secrets/CommandUpdate.ts @@ -1,9 +1,12 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; +import * as binErrors from '../errors'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as parsers from '../utils/parsers'; +import * as binProcessors from '../utils/processors'; class CommandUpdate extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -31,28 +34,53 @@ class CommandUpdate extends CommandPolykey { '../../proto/js/polykey/v1/secrets/secrets_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); const secretMessage = new secretsPB.Secret(); secretMessage.setVault(vaultMessage); vaultMessage.setNameOrId(secretPath[0]); secretMessage.setSecretName(secretPath[1]); - const content = await this.fs.promises.readFile(directoryPath); + let content: Buffer; + try { + content = await this.fs.promises.readFile(directoryPath); + } catch (e) { + throw new binErrors.ErrorCLIFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } secretMessage.setSecretContent(content); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsSecretsEdit(secretMessage, auth), meta, @@ -67,7 +95,7 @@ class CommandUpdate extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/types.ts b/src/bin/types.ts new file mode 100644 index 000000000..9e23a6307 --- /dev/null +++ b/src/bin/types.ts @@ -0,0 +1,64 @@ +import type { POJO } from '../types'; +import type { RecoveryCode } from '../keys/types'; +import type { Host, Port } from '../network/types'; + +/** + * PolykeyAgent Starting Input when Backgrounded + * When using advanced serialization, rich structures like + * Map, Set and more can be passed over IPC + * However traditional classes cannot be + */ +type AgentChildProcessInput = { + agentConfig: { + password: string; + nodePath?: string; + keysConfig?: { + rootKeyPairBits?: number; + rootCertDuration?: number; + dbKeyBits?: number; + recoveryCode?: RecoveryCode; + }; + forwardProxyConfig?: { + authToken?: string; + connConnectTime?: number; + connTimeoutTime?: number; + connPingIntervalTime?: number; + }; + reverseProxyConfig?: { + connConnectTime?: number; + connTimeoutTime?: number; + }; + networkConfig?: { + proxyHost?: Host; + proxyPort?: Port; + egressHost?: Host; + egressPort?: Port; + // ReverseProxy + ingressHost?: Host; + ingressPort?: Port; + // GRPCServer for agent service + agentHost?: Host; + agentPort?: Port; + // GRPCServer for client service + clientHost?: Host; + clientPort?: Port; + }; + fresh?: boolean; + }; +}; + +/** + * PolykeyAgent Starting Output when Backgrounded + * The error property contains arbitrary error properties + */ +type AgentChildProcessOutput = + | { + status: 'SUCCESS'; + recoveryCode?: RecoveryCode; + } + | { + status: 'FAILURE'; + error: POJO; + }; + +export type { AgentChildProcessInput, AgentChildProcessOutput }; diff --git a/src/bin/utils/ExitHandlers.ts b/src/bin/utils/ExitHandlers.ts new file mode 100644 index 000000000..84b981d80 --- /dev/null +++ b/src/bin/utils/ExitHandlers.ts @@ -0,0 +1,135 @@ +import process from 'process'; +import * as binUtils from './utils'; +import ErrorPolykey from '../../ErrorPolykey'; + +class ExitHandlers { + /** + * Mutate this array to control handlers + * Handlers will be executed in reverse order + */ + public handlers: Array<(signal?: NodeJS.Signals) => Promise>; + protected _exiting: boolean = false; + /** + * Handles synchronous and asynchronous exceptions + * This prints out appropriate error message on STDERR + * It sets the exit code according to the error + * 255 is set for unknown errors + */ + protected errorHandler = async (e: Error) => { + if (this._exiting) { + return; + } + this._exiting = true; + if (e instanceof ErrorPolykey) { + process.stderr.write( + binUtils.outputFormatter({ + type: 'error', + name: e.name, + description: e.description, + message: e.message, + }), + ); + process.exitCode = e.exitCode; + } else { + // Unknown error, this should not happen + process.stderr.write( + binUtils.outputFormatter({ + type: 'error', + name: e.name, + description: e.message, + }), + ); + process.exitCode = 255; + } + // Fail fast pattern + process.exit(); + }; + /** + * Handles termination signals + * This is idempotent + * After executing handlers, it will re-signal the process group + * This effectively runs the default signal handler in the NodeJS VM + */ + protected signalHandler = async (signal: NodeJS.Signals) => { + if (this._exiting) { + return; + } + this._exiting = true; + try { + await this.executeHandlers(signal); + } catch (e) { + // Due to finally clause, exceptions are caught here + // Signal handling will use signal-based exit codes + // https://nodejs.org/api/process.html#exit-codes + // Therefore `process.exitCode` is not set + if (e instanceof ErrorPolykey) { + process.stderr.write( + binUtils.outputFormatter({ + type: 'error', + name: e.name, + description: e.description, + message: e.message, + }), + ); + } else { + // Unknown error, this should not happen + process.stderr.write( + binUtils.outputFormatter({ + type: 'error', + name: e.name, + description: e.message, + }), + ); + } + } finally { + // Uninstall all handlers to prevent signal loop + this.uninstall(); + // Propagate signal to NodeJS VM handlers + process.kill(process.pid, signal); + } + }; + + /** + * Automatically installs all handlers + */ + public constructor( + handlers: Array<(signal?: NodeJS.Signals) => Promise> = [], + ) { + this.handlers = handlers; + this.install(); + } + + get exiting(): boolean { + return this._exiting; + } + + public install() { + process.on('SIGINT', this.signalHandler); + process.on('SIGTERM', this.signalHandler); + process.on('SIGQUIT', this.signalHandler); + process.on('SIGHUP', this.signalHandler); + // Both synchronous and asynchronous errors are handled + process.once('unhandledRejection', this.errorHandler); + process.once('uncaughtException', this.errorHandler); + } + + public uninstall() { + process.removeListener('SIGINT', this.signalHandler); + process.removeListener('SIGTERM', this.signalHandler); + process.removeListener('SIGQUIT', this.signalHandler); + process.removeListener('SIGHUP', this.signalHandler); + process.removeListener('unhandledRejection', this.errorHandler); + process.removeListener('uncaughtException', this.errorHandler); + } + + /** + * Execute handlers in reverse-order to match matroska model + */ + protected async executeHandlers(signal?: NodeJS.Signals) { + for (let i = this.handlers.length - 1, f = this.handlers[i]; i >= 0; i--) { + await f(signal); + } + } +} + +export default ExitHandlers; diff --git a/src/bin/utils/index.ts b/src/bin/utils/index.ts new file mode 100644 index 000000000..e875e06bb --- /dev/null +++ b/src/bin/utils/index.ts @@ -0,0 +1,4 @@ +export * from './utils'; +export * as options from './options'; +export * as parsers from './parsers'; +export { default as ExitHandlers } from './ExitHandlers'; diff --git a/src/bin/utils/options.ts b/src/bin/utils/options.ts new file mode 100644 index 000000000..04fb13d2f --- /dev/null +++ b/src/bin/utils/options.ts @@ -0,0 +1,124 @@ +/** + * Options and Arguments used by commands + * Use PolykeyCommand.addOption or PolykeyCommand.addArgument + * @module + */ +import path from 'path'; +import commander from 'commander'; +import * as binParsers from './parsers'; +import config from '../../config'; + +/** + * Node path is the path to node state + * This is a directory on the filesystem + * This is optional, if it is not specified, we will derive + * platform-specific default node path + * On unknown platforms the the default is undefined + */ +const nodePath = new commander.Option( + '-np, --node-path ', + 'Path to Node State', +).default(config.defaults.nodePath); + +/** + * Formatting choice of human, json, defaults to human + */ +const format = new commander.Option('-f, --format ', 'Output Format') + .choices(['human', 'json']) + .default('human'); + +/** + * Sets log level, defaults to 0, multiple uses will increase verbosity level + */ +const verbose = new commander.Option('-v, --verbose', 'Log Verbose Messages') + .argParser((_, p: number) => { + return p + 1; + }) + .default(0); + +/** + * Node ID used for connecting to a remote agent + */ +const nodeId = new commander.Option('-ni', '--node-id ').env('PK_NODE_ID'); + +/** + * Client host used for connecting to remote agent + */ +const clientHost = new commander.Option( + '-ch, --client-host ', + 'Client Host Address', +).env('PK_CLIENT_HOST'); + +/** + * Client port used for connecting to remote agent + */ +const clientPort = new commander.Option( + '-cp, --client-port ', + 'Client Port', +) + .argParser(binParsers.parseNumber) + .env('PK_CLIENT_PORT'); + +const ingressHost = new commander.Option( + '-ih, --ingress-host ', + 'Ingress host', +) + .env('PK_INGRESS_HOST') + .default(config.defaults.networkConfig.ingressHost); + +const ingressPort = new commander.Option( + '-ip, --ingress-port ', + 'Ingress Port', +) + .argParser(binParsers.parseNumber) + .env('PK_INGRESS_PORT') + .default(config.defaults.networkConfig.ingressPort); + +const passwordFile = new commander.Option( + '-pf, --password-file ', + 'Path to Password', +); + +const recoveryCodeFile = new commander.Option( + '-rcf, --recovery-code-file ', + 'Path to Recovery Code', +); + +const background = new commander.Option( + '-b, --background', + 'Starts the agent as a background process', +); + +const backgroundOutFile = new commander.Option( + '-bof, --background-out-file ', + 'Path to STDOUT for agent process', +).default( + config.defaults.nodePath != null + ? path.join(config.defaults.nodePath!, 'out.log') + : undefined, +); + +const backgroundErrFile = new commander.Option( + '-bef, --background-err-file ', + 'Path to STDERR for agent process', +).default( + config.defaults.nodePath != null + ? path.join(config.defaults.nodePath!, 'err.log') + : undefined, +); + +export { + nodePath, + format, + verbose, + nodeId, + clientHost, + clientPort, + ingressHost, + ingressPort, + recoveryCodeFile, + passwordFile, + background, + backgroundOutFile, + backgroundErrFile, +}; diff --git a/src/bin/parsers.ts b/src/bin/utils/parsers.ts similarity index 50% rename from src/bin/parsers.ts rename to src/bin/utils/parsers.ts index 6ffe2e18f..01b10cc6e 100644 --- a/src/bin/parsers.ts +++ b/src/bin/utils/parsers.ts @@ -1,14 +1,6 @@ -import type { FileSystem } from '../types'; -import type { IdentityId, ProviderId } from '../identities/types'; -import type { SessionToken } from '../sessions/types'; - -import { env } from 'process'; -import * as grpc from '@grpc/grpc-js'; +import type { IdentityId, ProviderId } from '../../identities/types'; import commander from 'commander'; - -import * as binUtils from './utils'; -import * as nodesUtils from '../nodes/utils'; -import * as clientUtils from '../client/utils'; +import * as nodesUtils from '../../nodes/utils'; function parseNumber(v: string): number { const num = parseInt(v); @@ -18,48 +10,6 @@ function parseNumber(v: string): number { return num; } -async function parseAuth({ - passwordFile, - fs = require('fs'), -}: { - passwordFile?: string; - fs?: FileSystem; -}): Promise { - let meta = new grpc.Metadata(); - if (passwordFile !== undefined) { - const password = await fs.promises.readFile(passwordFile, { - encoding: 'utf-8', - }); - meta = clientUtils.encodeAuthFromPassword(password); - } else if (env.PK_PASSWORD !== undefined) { - meta = clientUtils.encodeAuthFromPassword(env.PK_PASSWORD); - } else if (env.PK_TOKEN !== undefined) { - meta = clientUtils.encodeAuthFromSession(env.PK_TOKEN as SessionToken); - } - return meta; -} - -async function parsePassword({ - passwordFile, - fs = require('fs'), -}: { - passwordFile?: string; - fs?: FileSystem; -}): Promise { - let password: string | undefined = undefined; - if (passwordFile !== undefined) { - password = await fs.promises.readFile(passwordFile, { - encoding: 'utf-8', - }); - } else if (env['PK_PASSWORD'] !== undefined) { - password = env['PK_PASSWORD']; - } - while (password === undefined) { - password = await binUtils.requestPassword(); - } - return password; -} - function parseSecretPath( secretPath: string, ): [string, string, string | undefined] { @@ -96,19 +46,6 @@ function parseGestaltId(gestaltId: string) { return { providerId, identityId, nodeId }; } -async function parseFilePath({ - filePath, - fs = require('fs'), -}: { - filePath: string; - fs?: FileSystem; -}): Promise { - const cipherText = await fs.promises.readFile(filePath, { - encoding: 'binary', - }); - return cipherText; -} - function parseIdentityString(identityString: string): { providerId: ProviderId; identityId: IdentityId; @@ -125,13 +62,4 @@ function formatIdentityString( ): string { return `${providerId}:${identityId}`; } - -export { - parseNumber, - parseAuth, - parsePassword, - parseSecretPath, - parseGestaltId, - parseFilePath, - formatIdentityString, -}; +export { parseNumber, parseSecretPath, parseGestaltId, formatIdentityString }; diff --git a/src/bin/utils/processors.ts b/src/bin/utils/processors.ts new file mode 100644 index 000000000..b65333ffe --- /dev/null +++ b/src/bin/utils/processors.ts @@ -0,0 +1,248 @@ +import type { FileSystem } from '../../types'; +import type { RecoveryCode } from '../../keys/types'; +import type { NodeId } from '../../nodes/types'; +import type { Host, Port } from '../../network/types'; +import type { + StatusStarting, + StatusLive, + StatusStopping, + StatusDead, +} from '../../status/types'; +import type { SessionToken } from '../../sessions/types'; +import path from 'path'; +import prompts from 'prompts'; +import * as grpc from '@grpc/grpc-js'; +import Logger from '@matrixai/logger'; +import * as binErrors from '../errors'; +import * as clientUtils from '../../client/utils'; +import { Status } from '../../status'; +import config from '../../config'; + +/** + * Prompts for password + * This masks SIGINT handling + * When SIGINT is received this will return undefined + */ +async function promptPassword(): Promise { + const response = await prompts({ + type: 'password', + name: 'password', + message: 'Please enter your password', + }); + return response.password; +} + +/** + * Processes password + * Use this when password is necessary + * Order of operations are: + * 1. Reads --password-file + * 2. Reads PK_PASSWORD + * 3. Prompts for password + * This may return an empty string + */ +async function processPassword( + passwordFile?: string, + fs: FileSystem = require('fs'), +): Promise { + let password: string | undefined; + if (passwordFile != null) { + try { + password = (await fs.promises.readFile(passwordFile, 'utf-8')).trim(); + } catch (e) { + throw new binErrors.ErrorCLIPasswordFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } + } else if (typeof process.env['PK_PASSWORD'] === 'string') { + password = process.env['PK_PASSWORD']; + } else { + password = await promptPassword(); + if (password === undefined) { + throw new binErrors.ErrorCLIPasswordMissing(); + } + } + return password; +} + +/** + * Process recovery code + * Order of operations are: + * 1. Reads --recovery-code-file + * 2. Reads PK_RECOVERY_CODE + * This may return an empty string + */ +async function processRecoveryCode( + recoveryCodeFile?: string, + fs: FileSystem = require('fs'), +): Promise { + let recoveryCode: string | undefined; + if (recoveryCodeFile != null) { + try { + recoveryCode = ( + await fs.promises.readFile(recoveryCodeFile, 'utf-8') + ).trim(); + } catch (e) { + throw new binErrors.ErrorCLIRecoveryCodeFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } + } else if (typeof process.env['PK_RECOVERY_CODE'] === 'string') { + recoveryCode = process.env['PK_RECOVERY_CODE']; + } + return recoveryCode as RecoveryCode | undefined; +} + +/** + * Process client options + * Options are used for connecting PolykeyClient + * Order of operations are: + * 1. Reads --node-id, --client-host, --client-port + * 2. Reads PK_NODE_ID, PK_CLIENT_HOST, PK_CLIENT_PORT + * 3. Command-specific defaults + * 4. Reads Status + * Step 2 is done during option construction + * Step 3 is done in CommandPolykey classes + */ +async function processClientOptions( + nodePath: string, + nodeId?: NodeId, + clientHost?: Host, + clientPort?: Port, + fs = require('fs'), + logger = new Logger(processClientOptions.name), +): Promise<{ + nodeId: NodeId; + clientHost: Host; + clientPort: Port; +}> { + if (nodeId == null || clientHost == null || clientPort == null) { + const statusPath = path.join(nodePath, config.defaults.statusBase); + const status = new Status({ + statusPath, + fs, + logger: logger.getChild(Status.name), + }); + const statusInfo = await status.readStatus(); + if (statusInfo === undefined || statusInfo.status !== 'LIVE') { + throw new binErrors.ErrorCLIStatusNotLive(); + } + if (nodeId == null) nodeId = statusInfo.data.nodeId; + if (clientHost == null) clientHost = statusInfo.data.clientHost; + if (clientPort == null) clientPort = statusInfo.data.clientPort; + } + return { + nodeId, + clientHost: clientHost, + clientPort: clientPort, + }; +} + +/** + * Process client status + * Options are used for connecting PolykeyClient + * Variant of processClientOptions + * Use this when you need always need the status info + */ +async function processClientStatus( + nodePath: string, + nodeId?: NodeId, + clientHost?: Host, + clientPort?: Port, + fs = require('fs'), + logger = new Logger(processClientStatus.name), +): Promise< + | { + statusInfo: StatusStarting | StatusStopping | StatusDead | undefined; + nodeId: NodeId | undefined; + clientHost: Host | undefined; + clientPort: Port | undefined; + } + | { + statusInfo: StatusLive; + nodeId: NodeId; + clientHost: Host; + clientPort: Port; + } +> { + const statusPath = path.join(nodePath, config.defaults.statusBase); + const status = new Status({ + statusPath, + fs, + logger: logger.getChild(Status.name), + }); + const statusInfo = await status.readStatus(); + if (statusInfo?.status === 'LIVE') { + if (nodeId == null) nodeId = statusInfo.data.nodeId; + if (clientHost == null) clientHost = statusInfo.data.clientHost; + if (clientPort == null) clientPort = statusInfo.data.clientPort; + return { + nodeId, + clientHost, + clientPort, + statusInfo, + }; + } + return { + nodeId, + clientHost, + clientPort, + statusInfo, + }; +} + +/** + * Processes authentication metadata + * Use when authentication is necessary + * Order of operations are: + * 1. Reads --password-file + * 2. Reads PK_PASSWORD + * 3. Reads PK_TOKEN + * 4. Reads Session + * Step 4 is expected to be done during session interception + * This may return an empty metadata + */ +async function processAuthentication( + passwordFile?: string, + fs: FileSystem = require('fs'), +): Promise { + let meta; + if (passwordFile != null) { + let password; + try { + password = (await fs.promises.readFile(passwordFile, 'utf-8')).trim(); + } catch (e) { + throw new binErrors.ErrorCLIPasswordFileRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } + meta = clientUtils.encodeAuthFromPassword(password); + } else if (typeof process.env['PK_PASSWORD'] === 'string') { + meta = clientUtils.encodeAuthFromPassword(process.env['PK_PASSWORD']); + } else if (typeof process.env['PK_TOKEN'] === 'string') { + meta = clientUtils.encodeAuthFromSession( + process.env['PK_TOKEN'] as SessionToken, + ); + } else { + meta = new grpc.Metadata(); + } + return meta; +} + +export { + promptPassword, + processPassword, + processRecoveryCode, + processClientOptions, + processClientStatus, + processAuthentication, +}; diff --git a/src/bin/utils.ts b/src/bin/utils/utils.ts similarity index 66% rename from src/bin/utils.ts rename to src/bin/utils/utils.ts index 86a0df17b..eea41ed3e 100644 --- a/src/bin/utils.ts +++ b/src/bin/utils/utils.ts @@ -1,41 +1,12 @@ -import type { POJO } from '../types'; +import type { POJO } from '../../types'; -import os from 'os'; import process from 'process'; import { LogLevel } from '@matrixai/logger'; -import prompts from 'prompts'; import * as grpc from '@grpc/grpc-js'; -import * as clientUtils from '../client/utils'; -import * as clientErrors from '../client/errors'; - -function getDefaultNodePath(): string | undefined { - const prefix = 'polykey'; - const platform = os.platform(); - let p: string; - if (platform === 'linux') { - const homeDir = os.homedir(); - const dataDir = process.env.XDG_DATA_HOME; - if (dataDir != null) { - p = `${dataDir}/${prefix}`; - } else { - p = `${homeDir}/.local/share/${prefix}`; - } - } else if (platform === 'darwin') { - const homeDir = os.homedir(); - p = `${homeDir}/Library/Application Support/${prefix}`; - } else if (platform === 'win32') { - const homeDir = os.homedir(); - const appDataDir = process.env.LOCALAPPDATA; - if (appDataDir != null) { - p = `${appDataDir}/${prefix}`; - } else { - p = `${homeDir}/AppData/Local/${prefix}`; - } - } else { - return; - } - return p; -} +import * as binProcessors from './processors'; +import * as binErrors from '../errors'; +import * as clientUtils from '../../client/utils'; +import * as clientErrors from '../../client/errors'; /** * Convert verbosity to LogLevel @@ -107,21 +78,12 @@ function outputFormatter(msg: OutputObject): string { return output; } -async function requestPassword(): Promise { - const response = await prompts({ - type: 'text', - name: 'password', - message: 'Please enter your password', - }); - return response.password; -} - /** * CLI Authentication Retry Loop * Retries unary calls on attended authentication errors * Known as "privilege elevation" */ -async function retryAuth( +async function retryAuthentication( f: (meta: grpc.Metadata) => Promise, meta: grpc.Metadata = new grpc.Metadata(), ): Promise { @@ -142,7 +104,10 @@ async function retryAuth( // Now enter the retry loop while (true) { // Prompt the user for password - const password = await requestPassword(); + const password = await binProcessors.promptPassword(); + if (password == null) { + throw new binErrors.ErrorCLIPasswordMissing(); + } // Augment existing metadata clientUtils.encodeAuthFromPassword(password, meta); try { @@ -155,11 +120,6 @@ async function retryAuth( } } -export { - getDefaultNodePath, - verboseToLogLevel, - OutputObject, - outputFormatter, - requestPassword, - retryAuth, -}; +export { verboseToLogLevel, outputFormatter, retryAuthentication }; + +export type { OutputObject }; diff --git a/src/bin/vaults/CommandClone.ts b/src/bin/vaults/CommandClone.ts index f78a58f0c..e5867dcf9 100644 --- a/src/bin/vaults/CommandClone.ts +++ b/src/bin/vaults/CommandClone.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandClone extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -22,18 +23,33 @@ class CommandClone extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + const pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); const nodeMessage = new nodesPB.Node(); const vaultCloneMessage = new vaultsPB.Clone(); @@ -42,7 +58,7 @@ class CommandClone extends CommandPolykey { nodeMessage.setNodeId(nodeId); vaultMessage.setNameOrId(vaultNameOrId); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsClone(vaultCloneMessage, auth), meta, ); @@ -56,7 +72,7 @@ class CommandClone extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandCreate.ts b/src/bin/vaults/CommandCreate.ts index f0b4dcad8..3cd40cf0f 100644 --- a/src/bin/vaults/CommandCreate.ts +++ b/src/bin/vaults/CommandCreate.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandCreate extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,22 +22,37 @@ class CommandCreate extends CommandPolykey { '../../proto/js/polykey/v1/vaults/vaults_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); vaultMessage.setNameOrId(vaultName); - const response = await binUtils.retryAuth( + const response = await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsCreate(vaultMessage, auth), meta, ); @@ -48,7 +64,7 @@ class CommandCreate extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandDelete.ts b/src/bin/vaults/CommandDelete.ts index 23715fa5d..50134c1fa 100644 --- a/src/bin/vaults/CommandDelete.ts +++ b/src/bin/vaults/CommandDelete.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandDelete extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -20,21 +21,37 @@ class CommandDelete extends CommandPolykey { '../../proto/js/polykey/v1/vaults/vaults_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); vaultMessage.setNameOrId(vaultName); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsDelete(vaultMessage, auth), meta, ); @@ -46,7 +63,7 @@ class CommandDelete extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandList.ts b/src/bin/vaults/CommandList.ts index 9f481b5d9..f947f5839 100644 --- a/src/bin/vaults/CommandList.ts +++ b/src/bin/vaults/CommandList.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandList extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -17,26 +18,45 @@ class CommandList extends CommandPolykey { const { default: PolykeyClient } = await import('../../PolykeyClient'); const utilsPB = await import('../../proto/js/polykey/v1/utils/utils_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const emptyMessage = new utilsPB.EmptyMessage(); - const data = await binUtils.retryAuth(async (meta: Metadata) => { - const data: Array = []; - const stream = grpcClient.vaultsList(emptyMessage, meta); - for await (const vault of stream) { - data.push(`${vault.getVaultName()}:\t\t${vault.getVaultId()}`); - } - return data; - }, meta); + const data = await binUtils.retryAuthentication( + async (meta: Metadata) => { + const data: Array = []; + const stream = grpcClient.vaultsList(emptyMessage, meta); + for await (const vault of stream) { + data.push(`${vault.getVaultName()}:\t\t${vault.getVaultId()}`); + } + return data; + }, + meta, + ); process.stdout.write( binUtils.outputFormatter({ type: options.format === 'json' ? 'json' : 'list', @@ -44,7 +64,7 @@ class CommandList extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandLog.ts b/src/bin/vaults/CommandLog.ts index 27def01dd..6d0829f88 100644 --- a/src/bin/vaults/CommandLog.ts +++ b/src/bin/vaults/CommandLog.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandLog extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -25,18 +26,33 @@ class CommandLog extends CommandPolykey { '../../proto/js/polykey/v1/vaults/vaults_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); vaultMessage.setNameOrId(vault); const vaultsLogMessage = new vaultsPB.Log(); @@ -44,19 +60,22 @@ class CommandLog extends CommandPolykey { vaultsLogMessage.setLogDepth(options.depth); vaultsLogMessage.setCommitId(options.commitId ?? ''); - const data = await binUtils.retryAuth(async (meta: Metadata) => { - const data: Array = []; - const stream = grpcClient.vaultsLog(vaultsLogMessage, meta); - for await (const commit of stream) { - const timeStamp = commit.getTimeStamp(); - const date = new Date(timeStamp); - data.push(`commit ${commit.getOid()}`); - data.push(`committer ${commit.getCommitter()}`); - data.push(`Date: ${date.toDateString()}`); - data.push(`${commit.getMessage()}`); - } - return data; - }, meta); + const data = await binUtils.retryAuthentication( + async (meta: Metadata) => { + const data: Array = []; + const stream = grpcClient.vaultsLog(vaultsLogMessage, meta); + for await (const commit of stream) { + const timeStamp = commit.getTimeStamp(); + const date = new Date(timeStamp); + data.push(`commit ${commit.getOid()}`); + data.push(`committer ${commit.getCommitter()}`); + data.push(`Date: ${date.toDateString()}`); + data.push(`${commit.getMessage()}`); + } + return data; + }, + meta, + ); process.stdout.write( binUtils.outputFormatter({ type: options.format === 'json' ? 'json' : 'list', @@ -64,7 +83,7 @@ class CommandLog extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandPermissions.ts b/src/bin/vaults/CommandPermissions.ts index 5860c0058..96ab965f6 100644 --- a/src/bin/vaults/CommandPermissions.ts +++ b/src/bin/vaults/CommandPermissions.ts @@ -7,7 +7,7 @@ // import * as grpcErrors from '../../grpc/errors'; // import CommandPolykey from '../CommandPolykey'; -// import * as binOptions from '../options'; +// import * as binOptions from '../utils/options'; // class CommandPermissions extends CommandPolykey { // constructor(...args: ConstructorParameters) { diff --git a/src/bin/vaults/CommandPull.ts b/src/bin/vaults/CommandPull.ts index 9c2636d51..6c195817e 100644 --- a/src/bin/vaults/CommandPull.ts +++ b/src/bin/vaults/CommandPull.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandPull extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -22,18 +23,33 @@ class CommandPull extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); const nodeMessage = new nodesPB.Node(); const vaultPullMessage = new vaultsPB.Pull(); @@ -42,7 +58,7 @@ class CommandPull extends CommandPolykey { nodeMessage.setNodeId(nodeId); vaultMessage.setNameOrId(vaultName); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsPull(vaultPullMessage, auth), meta, ); @@ -56,7 +72,7 @@ class CommandPull extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandRename.ts b/src/bin/vaults/CommandRename.ts index 1f10ee785..97ed9fcdc 100644 --- a/src/bin/vaults/CommandRename.ts +++ b/src/bin/vaults/CommandRename.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandRename extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,25 +22,40 @@ class CommandRename extends CommandPolykey { '../../proto/js/polykey/v1/vaults/vaults_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); const vaultRenameMessage = new vaultsPB.Rename(); vaultRenameMessage.setVault(vaultMessage); vaultMessage.setNameOrId(vaultName); vaultRenameMessage.setNewName(newVaultName); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsRename(vaultRenameMessage, auth), meta, @@ -54,7 +70,7 @@ class CommandRename extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandScan.ts b/src/bin/vaults/CommandScan.ts index cc4ca4cc7..e8887d35b 100644 --- a/src/bin/vaults/CommandScan.ts +++ b/src/bin/vaults/CommandScan.ts @@ -7,7 +7,7 @@ // import * as grpcErrors from '../../grpc/errors'; // import CommandPolykey from '../CommandPolykey'; -// import * as binOptions from '../options'; +// import * as binOptions from '../utils/options'; // class CommandScan extends CommandPolykey { // constructor(...args: ConstructorParameters) { diff --git a/src/bin/vaults/CommandShare.ts b/src/bin/vaults/CommandShare.ts index 154a5abe0..0c88b11b7 100644 --- a/src/bin/vaults/CommandShare.ts +++ b/src/bin/vaults/CommandShare.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandShare extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -22,18 +23,33 @@ class CommandShare extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); const nodeMessage = new nodesPB.Node(); const setVaultPermsMessage = new vaultsPB.PermSet(); @@ -42,7 +58,7 @@ class CommandShare extends CommandPolykey { vaultMessage.setNameOrId(vaultName); nodeMessage.setNodeId(nodeId); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsPermissionsSet(setVaultPermsMessage, auth), meta, @@ -61,7 +77,7 @@ class CommandShare extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandStat.ts b/src/bin/vaults/CommandStat.ts index 17635b1d7..1b44f3c5d 100644 --- a/src/bin/vaults/CommandStat.ts +++ b/src/bin/vaults/CommandStat.ts @@ -7,7 +7,7 @@ // import * as grpcErrors from '../../grpc/errors'; // import CommandPolykey from '../CommandPolykey'; -// import * as binOptions from '../options'; +// import * as binOptions from '../utils/options'; // class CommandStat extends CommandPolykey { // constructor(...args: ConstructorParameters) { diff --git a/src/bin/vaults/CommandUnshare.ts b/src/bin/vaults/CommandUnshare.ts index f529ee5eb..671fc094a 100644 --- a/src/bin/vaults/CommandUnshare.ts +++ b/src/bin/vaults/CommandUnshare.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandUnshare extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -22,18 +23,33 @@ class CommandUnshare extends CommandPolykey { ); const nodesPB = await import('../../proto/js/polykey/v1/nodes/nodes_pb'); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const unsetVaultPermsMessage = new vaultsPB.PermUnset(); const vaultMessage = new vaultsPB.Vault(); const nodeMessage = new nodesPB.Node(); @@ -42,7 +58,7 @@ class CommandUnshare extends CommandPolykey { vaultMessage.setNameOrId(vaultName); nodeMessage.setNodeId(nodeId); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsPermissionsUnset(unsetVaultPermsMessage, auth), meta, @@ -61,7 +77,7 @@ class CommandUnshare extends CommandPolykey { }), ); } finally { - await client.start(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bin/vaults/CommandVaults.ts b/src/bin/vaults/CommandVaults.ts index d3ca252bc..e7ba102f5 100644 --- a/src/bin/vaults/CommandVaults.ts +++ b/src/bin/vaults/CommandVaults.ts @@ -18,19 +18,19 @@ class CommandVaults extends CommandPolykey { super(...args); this.name('vaults'); this.description('Vaults Operations'); - this.addCommand(new CommandClone()); - this.addCommand(new CommandCreate()); - this.addCommand(new CommandDelete()); - this.addCommand(new CommandList()); - this.addCommand(new CommandLog()); - // This.addCommand(new CommandPermissions); - this.addCommand(new CommandPull()); - this.addCommand(new CommandRename()); - // This.addCommand(new CommandScan); - this.addCommand(new CommandShare()); - // This.addCommand(new CommandStat); - this.addCommand(new CommandUnshare()); - this.addCommand(new CommandVersion()); + this.addCommand(new CommandClone(...args)); + this.addCommand(new CommandCreate(...args)); + this.addCommand(new CommandDelete(...args)); + this.addCommand(new CommandList(...args)); + this.addCommand(new CommandLog(...args)); + // This.addCommand(new CommandPermissions(...args)); + this.addCommand(new CommandPull(...args)); + this.addCommand(new CommandRename(...args)); + // This.addCommand(new CommandScan(...args)); + this.addCommand(new CommandShare(...args)); + // This.addCommand(new CommandStat(...args)); + this.addCommand(new CommandUnshare(...args)); + this.addCommand(new CommandVersion(...args)); } } diff --git a/src/bin/vaults/CommandVersion.ts b/src/bin/vaults/CommandVersion.ts index 715818781..051acecfd 100644 --- a/src/bin/vaults/CommandVersion.ts +++ b/src/bin/vaults/CommandVersion.ts @@ -1,9 +1,10 @@ import type { Metadata } from '@grpc/grpc-js'; +import type PolykeyClient from '../../PolykeyClient'; import CommandPolykey from '../CommandPolykey'; import * as binUtils from '../utils'; -import * as binOptions from '../options'; -import * as parsers from '../parsers'; +import * as binOptions from '../utils/options'; +import * as binProcessors from '../utils/processors'; class CommandVersion extends CommandPolykey { constructor(...args: ConstructorParameters) { @@ -21,25 +22,40 @@ class CommandVersion extends CommandPolykey { '../../proto/js/polykey/v1/vaults/vaults_pb' ); - const client = await PolykeyClient.createPolykeyClient({ - nodePath: options.nodePath, - logger: this.logger.getChild(PolykeyClient.name), - }); + const clientOptions = await binProcessors.processClientOptions( + options.nodePath, + options.nodeId, + options.clientHost, + options.clientPort, + this.fs, + this.logger.getChild(binProcessors.processClientOptions.name), + ); - const meta = await parsers.parseAuth({ - passwordFile: options.passwordFile, - fs: this.fs, + let pkClient: PolykeyClient | undefined; + this.exitHandlers.handlers.push(async () => { + if (pkClient != null) await pkClient.stop(); }); - try { - const grpcClient = client.grpcClient; + pkClient = await PolykeyClient.createPolykeyClient({ + nodePath: options.nodePath, + nodeId: clientOptions.nodeId, + host: clientOptions.clientHost, + port: clientOptions.clientPort, + logger: this.logger.getChild(PolykeyClient.name), + }); + + const meta = await binProcessors.processAuthentication( + options.passwordFile, + this.fs, + ); + const grpcClient = pkClient.grpcClient; const vaultMessage = new vaultsPB.Vault(); const vaultsVersionMessage = new vaultsPB.Version(); vaultMessage.setNameOrId(vault); vaultsVersionMessage.setVault(vaultMessage); vaultsVersionMessage.setVersionId(versionId); - await binUtils.retryAuth( + await binUtils.retryAuthentication( (auth?: Metadata) => grpcClient.vaultsVersion(vaultsVersionMessage, auth), meta, @@ -66,7 +82,7 @@ class CommandVersion extends CommandPolykey { }), ); } finally { - await client.stop(); + if (pkClient != null) await pkClient.stop(); } }); } diff --git a/src/bootstrap/bootstrap.ts b/src/bootstrap/bootstrap.ts deleted file mode 100644 index da2b8dc01..000000000 --- a/src/bootstrap/bootstrap.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { KeynodeState } from './types'; - -import path from 'path'; -import fs from 'fs'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - -import * as bootstrapErrors from './errors'; -import PolykeyAgent from '../PolykeyAgent'; - -import * as errors from '../errors'; -import * as utils from '../utils'; -import * as agentUtils from '../agent/utils'; - -async function bootstrapPolykeyState( - nodePath: string, - password: string, -): Promise { - const logger = new Logger('AgentServerTest', LogLevel.WARN, [ - new StreamHandler(), - ]); - - // Checks - // Checking for running agent. - if (await agentUtils.checkAgentRunning(nodePath)) { - throw new errors.ErrorAgentRunning('Agent currently running.'); - } - - // Checking keynode state. - switch (await checkKeynodeState(nodePath)) { - default: // Shouldn't be possible. - case 'MALFORMED_KEYNODE': - throw new bootstrapErrors.ErrorMalformedKeynode(); - case 'KEYNODE_EXISTS': - throw new bootstrapErrors.ErrorExistingState( - 'Polykey already exists at node path', - ); - case 'OTHER_EXISTS': - throw new bootstrapErrors.ErrorExistingState( - 'Files already exists at node path', - ); - case 'EMPTY_DIRECTORY': - case 'NO_DIRECTORY': - // This is fine. - break; - } - - const polykeyAgent = await PolykeyAgent.createPolykeyAgent({ - password, - nodePath: nodePath, - logger: logger, - }); - - // Setting FS editing mask. - const umaskNew = 0o077; - process.umask(umaskNew); - await utils.mkdirExists(fs, nodePath, { recursive: true }); - - // Starting and creating state (this will need to be changed with the new db stuff) - await polykeyAgent.nodeManager.start(); - - // Stopping - await polykeyAgent.nodeManager.stop(); - await polykeyAgent.db.stop(); - - await polykeyAgent.destroy(); -} - -async function checkKeynodeState(nodePath: string): Promise { - try { - const files = await fs.promises.readdir(nodePath); - // Checking if directory structure matches keynode structure. Possibly check the private and public key and the level db for keys) - if ( - files.includes('keys') && - files.includes('db') && - files.includes('versionFile') - ) { - const keysPath = path.join(nodePath, 'keys'); - const keysFiles = await fs.promises.readdir(keysPath); - if ( - !keysFiles.includes('db.key') || - !keysFiles.includes('root_certs') || - !keysFiles.includes('root.crt') || - !keysFiles.includes('root.key') || - !keysFiles.includes('root.pub') || - !keysFiles.includes('vault.key') - ) { - return 'MALFORMED_KEYNODE'; - } - return 'KEYNODE_EXISTS'; // Should be a good initilized keynode. - } else { - if (files.length !== 0) { - return 'OTHER_EXISTS'; // Bad structure, either malformed or not a keynode. - } else { - return 'EMPTY_DIRECTORY'; // Directy exists, but is empty, can make a keynode. - } - } - } catch (e) { - if (e.code === 'ENOENT') { - return 'NO_DIRECTORY'; // The directory does not exist, we can create a bootstrap a keynode. - } else { - throw e; - } - } -} - -export { bootstrapPolykeyState, KeynodeState, checkKeynodeState }; diff --git a/src/bootstrap/errors.ts b/src/bootstrap/errors.ts index f42043955..a9681841f 100644 --- a/src/bootstrap/errors.ts +++ b/src/bootstrap/errors.ts @@ -2,14 +2,4 @@ import { ErrorPolykey } from '../errors'; class ErrorBootstrap extends ErrorPolykey {} -class ErrorExistingState extends ErrorBootstrap { - description: string = 'Files already exist at node path'; - exitCode: number = 64; -} - -class ErrorMalformedKeynode extends ErrorBootstrap { - description: string = 'Malformed Polykey state exists at node path'; - exitCode: number = 64; -} - -export { ErrorExistingState, ErrorMalformedKeynode }; +export { ErrorBootstrap }; diff --git a/src/bootstrap/index.ts b/src/bootstrap/index.ts index 6102ec8cd..5b5826d2c 100644 --- a/src/bootstrap/index.ts +++ b/src/bootstrap/index.ts @@ -1 +1,2 @@ -export * from './bootstrap'; +export * as utils from './utils'; +export * as errors from './errors'; diff --git a/src/bootstrap/types.ts b/src/bootstrap/types.ts deleted file mode 100644 index f095d20ec..000000000 --- a/src/bootstrap/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -type KeynodeState = - | 'KEYNODE_EXISTS' - | 'OTHER_EXISTS' - | 'EMPTY_DIRECTORY' - | 'MALFORMED_KEYNODE' - | 'NO_DIRECTORY'; - -export type { KeynodeState }; diff --git a/src/bootstrap/utils.ts b/src/bootstrap/utils.ts new file mode 100644 index 000000000..3544bcb7e --- /dev/null +++ b/src/bootstrap/utils.ts @@ -0,0 +1,172 @@ +import type { FileSystem } from '../types'; +import type { RecoveryCode } from '../keys/types'; +import path from 'path'; +import Logger from '@matrixai/logger'; +import { DB } from '@matrixai/db'; +import { IdentitiesManager } from '../identities'; +import { SessionManager } from '../sessions'; +import { Status } from '../status'; +import { Schema } from '../schema'; +import { KeyManager, utils as keyUtils } from '../keys'; +import { Sigchain } from '../sigchain'; +import { ACL } from '../acl'; +import { GestaltGraph } from '../gestalts'; +import { ForwardProxy, ReverseProxy } from '../network'; +import { NodeManager } from '../nodes'; +import { VaultManager } from '../vaults'; +import { NotificationsManager } from '../notifications'; +import { mkdirExists } from '../utils'; +import config from '../config'; + +/** + * Bootstraps the Node Path + */ +async function bootstrapState({ + nodePath, + password, + recoveryCode, + fresh = false, + fs = require('fs'), + logger = new Logger(bootstrapState.name), +}: { + nodePath: string; + password: string; + recoveryCode?: RecoveryCode; + fresh?: boolean; + fs?: FileSystem; + logger?: Logger; +}): Promise { + const umask = 0o077; + logger.info(`Setting umask to ${umask.toString(8).padStart(3, '0')}`); + process.umask(umask); + logger.info(`Setting node path to ${nodePath}`); + await mkdirExists(fs, nodePath); + // Setup node path and sub paths + const statusPath = path.join(nodePath, config.defaults.statusBase); + const statePath = path.join(nodePath, config.defaults.stateBase); + const dbPath = path.join(statePath, config.defaults.dbBase); + const keysPath = path.join(statePath, config.defaults.keysBase); + const vaultsPath = path.join(statePath, config.defaults.vaultsBase); + const status = new Status({ + fs, + logger, + statusPath, + }); + try { + await status.start({ pid: process.pid }); + // Construction occurs here, fresh is propagated + // If any creations fail, then nodePath may be left with intermediate state + // Therefore the fresh parameter is expected to be true under normal usage + // Because it will work even if the node path is occupied + const schema = await Schema.createSchema({ + statePath, + fs, + logger: logger.getChild(Schema.name), + fresh, + }); + const keyManager = await KeyManager.createKeyManager({ + keysPath, + password, + fs, + logger: logger.getChild(KeyManager.name), + recoveryCode, + fresh, + }); + const db = await DB.createDB({ + dbPath, + fs, + logger: logger.getChild(DB.name), + crypto: { + key: keyManager.dbKey, + ops: { + encrypt: keyUtils.encryptWithKey, + decrypt: keyUtils.decryptWithKey, + }, + }, + fresh, + }); + const identitiesManager = await IdentitiesManager.createIdentitiesManager({ + db, + logger: logger.getChild(IdentitiesManager.name), + fresh, + }); + const sigchain = await Sigchain.createSigchain({ + db, + keyManager, + logger: logger.getChild(Sigchain.name), + fresh, + }); + const acl = await ACL.createACL({ + db, + logger: logger.getChild(ACL.name), + fresh, + }); + const gestaltGraph = await GestaltGraph.createGestaltGraph({ + acl, + db, + logger: logger.getChild(GestaltGraph.name), + fresh, + }); + // Proxies are constructed only, but not started + const fwdProxy = new ForwardProxy({ + authToken: '', + logger: logger.getChild(ForwardProxy.name), + }); + const revProxy = new ReverseProxy({ + logger: logger.getChild(ReverseProxy.name), + }); + const nodeManager = await NodeManager.createNodeManager({ + db, + keyManager, + sigchain, + fwdProxy, + revProxy, + logger: logger.getChild(NodeManager.name), + fresh, + }); + const vaultManager = await VaultManager.createVaultManager({ + acl, + db, + gestaltGraph, + keyManager, + nodeManager, + vaultsKey: keyManager.vaultKey, + vaultsPath, + logger: logger.getChild(VaultManager.name), + fresh, + }); + const notificationsManager = + await NotificationsManager.createNotificationsManager({ + acl, + db, + nodeManager, + keyManager, + logger: logger.getChild(NotificationsManager.name), + fresh, + }); + const sessionManager = await SessionManager.createSessionManager({ + db, + keyManager, + logger: logger.getChild(SessionManager.name), + fresh, + }); + const recoveryCodeNew = keyManager.getRecoveryCode()!; + await status.beginStop({ pid: process.pid }); + await sessionManager.stop(); + await notificationsManager.stop(); + await vaultManager.stop(); + await nodeManager.stop(); + await gestaltGraph.stop(); + await acl.stop(); + await sigchain.stop(); + await identitiesManager.stop(); + await db.stop(); + await keyManager.stop(); + await schema.stop(); + return recoveryCodeNew; + } finally { + await status.stop({}); + } +} + +export { bootstrapState }; diff --git a/src/client/clientService.ts b/src/client/clientService.ts index 5b5cb2278..384078f2c 100644 --- a/src/client/clientService.ts +++ b/src/client/clientService.ts @@ -9,6 +9,7 @@ import type { NotificationsManager } from '../notifications'; import type { Discovery } from '../discovery'; import type { ForwardProxy, ReverseProxy } from '../network'; import type { GRPCServer } from '../grpc'; +import type { FileSystem } from '../types'; import type * as grpc from '@grpc/grpc-js'; import type { IClientServiceServer } from '../proto/js/polykey/v1/client_service_grpc_pb'; @@ -45,6 +46,7 @@ function createClientService({ fwdProxy, revProxy, clientGrpcServer, + fs, }: { polykeyAgent: PolykeyAgent; keyManager: KeyManager; @@ -58,6 +60,7 @@ function createClientService({ fwdProxy: ForwardProxy; revProxy: ReverseProxy; clientGrpcServer: GRPCServer; + fs: FileSystem; }) { const authenticate = clientUtils.authenticator(sessionManager, keyManager); const clientService: IClientServiceServer = { @@ -73,6 +76,7 @@ function createClientService({ ...createVaultRPC({ vaultManager, authenticate, + fs, }), ...createKeysRPC({ keyManager, diff --git a/src/client/rpcKeys.ts b/src/client/rpcKeys.ts index 74259999c..8e03f4058 100644 --- a/src/client/rpcKeys.ts +++ b/src/client/rpcKeys.ts @@ -181,7 +181,7 @@ const createKeysRPC = ({ const metadata = await authenticate(call.metadata); call.sendMetadata(metadata); - await keyManager.changeRootKeyPassword(call.request.getPassword()); + await keyManager.changePassword(call.request.getPassword()); } catch (err) { callback(grpcUtils.fromError(err), response); } diff --git a/src/client/rpcVaults.ts b/src/client/rpcVaults.ts index f85cb9a37..15158edef 100644 --- a/src/client/rpcVaults.ts +++ b/src/client/rpcVaults.ts @@ -1,5 +1,6 @@ import type { Vault, VaultId, VaultName } from '../vaults/types'; import type { VaultManager } from '../vaults'; +import type { FileSystem } from '../types'; import type * as utils from './utils'; import type * as nodesPB from '../proto/js/polykey/v1/nodes/nodes_pb'; @@ -24,9 +25,11 @@ function decodeVaultId(input: string): VaultId | undefined { const createVaultRPC = ({ vaultManager, authenticate, + fs, }: { vaultManager: VaultManager; authenticate: utils.Authenticate; + fs: FileSystem; }) => { return { vaultsList: async ( @@ -450,7 +453,7 @@ const createVaultRPC = ({ if (!vaultId) throw new vaultsErrors.ErrorVaultUndefined(); const vault = await vaultManager.openVault(vaultId); const secretsPath = call.request.getSecretDirectory(); - await vaultOps.addSecretDirectory(vault, secretsPath); + await vaultOps.addSecretDirectory(vault, secretsPath, fs); response.setSuccess(true); callback(null, response); } catch (err) { diff --git a/src/config.ts b/src/config.ts index e9748e3b3..db95f07e9 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,3 +1,5 @@ +import type { Host, Port } from './network/types'; +import { getDefaultNodePath } from './utils'; // @ts-ignore package.json is outside rootDir import { version } from '../package.json'; @@ -48,6 +50,48 @@ const config = { nodeSignature: '1.3.6.1.4.1.57167.2.2.2', }, }, + /** + * Default configuration + */ + defaults: { + nodePath: getDefaultNodePath(), + statusBase: 'status.json', + stateBase: 'state', + dbBase: 'db', + keysBase: 'keys', + vaultsBase: 'vaults', + tokenBase: 'token', + keysConfig: { + rootKeyPairBits: 4096, + rootCertDuration: 31536000, + dbKeyBits: 256, + }, + networkConfig: { + // ForwardProxy + proxyHost: '127.0.0.1' as Host, + proxyPort: 0 as Port, + egressHost: '0.0.0.0' as Host, + egressPort: 0 as Port, + // ReverseProxy + ingressHost: '0.0.0.0' as Host, + ingressPort: 0 as Port, + // GRPCServer for agent service + agentHost: '127.0.0.1' as Host, + agentPort: 0 as Port, + // GRPCServer for client service + clientHost: '127.0.0.1' as Host, + clientPort: 0 as Port, + }, + forwardProxyConfig: { + connConnectTime: 20000, + connTimeoutTime: 20000, + connPingIntervalTime: 1000, + }, + reverseProxyConfig: { + connConnectTime: 20000, + connTimeoutTime: 20000, + }, + }, }; export default config; diff --git a/src/errors.ts b/src/errors.ts index 881e3268d..2096d10c9 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,18 +1,9 @@ import ErrorPolykey from './ErrorPolykey'; - -/** - * This is a special error that is only used for absurd situations - * Intended to placate typescript so that unreachable code type checks - * If this is thrown, this means there is a bug in the code - */ -class ErrorPolykeyUndefinedBehaviour extends ErrorPolykey { - description = 'You should never see this error'; - exitCode = 70; -} +import sysexits from './utils/sysexits'; class ErrorPolykeyUnimplemented extends ErrorPolykey { description = 'This is an unimplemented functionality'; - exitCode = 69; + exitCode = sysexits.UNAVAILABLE; } class ErrorPolykeyAgentRunning extends ErrorPolykey {} @@ -30,8 +21,8 @@ class ErrorPolykeyClientDestroyed extends ErrorPolykey {} class ErrorInvalidId extends ErrorPolykey {} export { + sysexits, ErrorPolykey, - ErrorPolykeyUndefinedBehaviour, ErrorPolykeyUnimplemented, ErrorPolykeyAgentRunning, ErrorPolykeyAgentNotRunning, @@ -64,3 +55,5 @@ export * from './claims/errors'; export * from './sigchain/errors'; export * from './bootstrap/errors'; export * from './notifications/errors'; +export * from './status/errors'; +export * from './utils/errors'; diff --git a/src/grpc/GRPCServer.ts b/src/grpc/GRPCServer.ts index 1179bcee2..cb7b59b13 100644 --- a/src/grpc/GRPCServer.ts +++ b/src/grpc/GRPCServer.ts @@ -28,10 +28,7 @@ class GRPCServer { protected _secured: boolean = false; constructor({ logger }: { logger?: Logger }) { - logger = logger ?? new Logger('GRPCServer'); - logger.info('Creating GRPC Server'); - this.logger = logger; - logger.info('Created GRPC Server'); + this.logger = logger ?? new Logger(this.constructor.name); } get secured(): boolean { @@ -53,7 +50,7 @@ class GRPCServer { this.tlsConfig = tlsConfig; this.services = services; let address = networkUtils.buildAddress(this._host, port); - this.logger.info(`Starting GRPC Server on ${address}`); + this.logger.info(`Starting ${this.constructor.name} on ${address}`); let serverCredentials: ServerCredentials; if (this.tlsConfig == null) { serverCredentials = grpcUtils.serverInsecureCredentials(); @@ -120,7 +117,7 @@ class GRPCServer { this._secured = true; } address = networkUtils.buildAddress(this._host, this._port); - this.logger.info(`Started GRPC Server on ${address}`); + this.logger.info(`Started ${this.constructor.name} on ${address}`); } /** @@ -133,7 +130,7 @@ class GRPCServer { }: { timeout?: number; } = {}): Promise { - this.logger.info('Stopping GRPC Server'); + this.logger.info(`Stopping ${this.constructor.name}`); const tryShutdown = promisify(this.server.tryShutdown).bind(this.server); const timer = timeout != null ? timerStart(timeout) : undefined; try { @@ -147,11 +144,13 @@ class GRPCServer { if (timer != null) timerStop(timer); } if (timer?.timedOut) { - this.logger.info('Timed out stopping GRPC Server, forcing shutdown'); + this.logger.info( + `Timed out stopping ${this.constructor.name}, forcing shutdown`, + ); this.server.forceShutdown(); } this._secured = false; - this.logger.info('Stopped GRPC Server'); + this.logger.info(`Stopped ${this.constructor.name}`); } @ready(new grpcErrors.ErrorGRPCServerNotRunning()) @@ -185,7 +184,7 @@ class GRPCServer { if (!this._secured) { throw new grpcErrors.ErrorGRPCServerNotSecured(); } - this.logger.info('Updating GRPC Server TLS Config'); + this.logger.info(`Updating ${this.constructor.name} TLS Config`); // @ts-ignore hack for private property const http2Servers = this.server.http2ServerList; for (const http2Server of http2Servers as Array) { diff --git a/src/grpc/utils.ts b/src/grpc/utils.ts index d5b161823..f14aeddb3 100644 --- a/src/grpc/utils.ts +++ b/src/grpc/utils.ts @@ -32,7 +32,7 @@ import { Buffer } from 'buffer'; import * as grpc from '@grpc/grpc-js'; import * as grpcErrors from './errors'; import * as errors from '../errors'; -import { promisify, promise } from '../utils'; +import { promisify, promise, never } from '../utils'; /** * GRPC insecure credentials for the client @@ -191,7 +191,7 @@ function toError(e: ServiceError): errors.ErrorPolykey { } } } - throw new errors.ErrorPolykeyUndefinedBehaviour(); + never(); } /** diff --git a/src/keys/KeyManager.ts b/src/keys/KeyManager.ts index 5fcbd3ab9..fc46b13d1 100644 --- a/src/keys/KeyManager.ts +++ b/src/keys/KeyManager.ts @@ -4,6 +4,7 @@ import type { KeyPairPem, CertificatePem, CertificatePemChain, + RecoveryCode, } from './types'; import type { FileSystem } from '../types'; import type { NodeId } from '../nodes/types'; @@ -11,6 +12,7 @@ import type { PolykeyWorkerManagerInterface } from '../workers/types'; import type { VaultKey } from '../vaults/types'; import path from 'path'; +import { Buffer } from 'buffer'; import Logger from '@matrixai/logger'; import { CreateDestroyStartStop, @@ -41,6 +43,7 @@ class KeyManager { protected fs: FileSystem; protected logger: Logger; protected rootKeyPair: KeyPair; + protected recoveryCode: RecoveryCode | undefined; protected _dbKey: Buffer; protected _vaultKey: Buffer; protected rootCert: Certificate; @@ -53,36 +56,40 @@ class KeyManager { static async createKeyManager({ keysPath, password, - fs = require('fs'), - logger = new Logger(this.name), rootKeyPairBits = 4096, rootCertDuration = 31536000, dbKeyBits = 256, vaultKeyBits = 256, + fs = require('fs'), + logger = new Logger(this.name), + recoveryCode, fresh = false, }: { keysPath: string; password: string; - fs?: FileSystem; - logger?: Logger; rootKeyPairBits?: number; rootCertDuration?: number; dbKeyBits?: number; vaultKeyBits?: number; + fs?: FileSystem; + logger?: Logger; + recoveryCode?: RecoveryCode; fresh?: boolean; }): Promise { logger.info(`Creating ${this.name}`); + logger.info(`Setting keys path to ${keysPath}`); const keyManager = new KeyManager({ - dbKeyBits, + keysPath, rootCertDuration, rootKeyPairBits, + dbKeyBits, vaultKeyBits, fs, logger, - keysPath, }); await keyManager.start({ password, + recoveryCode, fresh, }); logger.info(`Created ${this.name}`); @@ -91,24 +98,23 @@ class KeyManager { constructor({ keysPath, - fs, - logger, rootKeyPairBits, rootCertDuration, dbKeyBits, vaultKeyBits, + fs, + logger, }: { keysPath: string; - fs: FileSystem; - logger: Logger; rootKeyPairBits: number; rootCertDuration: number; dbKeyBits: number; vaultKeyBits: number; + fs: FileSystem; + logger: Logger; }) { this.logger = logger; this.keysPath = keysPath; - this.fs = fs; this.rootPubPath = path.join(keysPath, 'root.pub'); this.rootKeyPath = path.join(keysPath, 'root.key'); this.rootCertPath = path.join(keysPath, 'root.crt'); @@ -119,6 +125,7 @@ class KeyManager { this.rootCertDuration = rootCertDuration; this.dbKeyBits = dbKeyBits; this.vaultKeyBits = vaultKeyBits; + this.fs = fs; } public setWorkerManager(workerManager: PolykeyWorkerManagerInterface) { @@ -131,13 +138,20 @@ class KeyManager { public async start({ password, + recoveryCode, fresh = false, }: { password: string; + recoveryCode?: RecoveryCode; fresh?: boolean; - }) { + }): Promise { this.logger.info(`Starting ${this.constructor.name}`); - this.logger.info(`Setting keys path to ${this.keysPath}`); + if (password.length < 1) { + throw new keysErrors.ErrorKeysPasswordInvalid('Password cannot be empty'); + } + if (recoveryCode != null && !keysUtils.validateRecoveryCode(recoveryCode)) { + throw new keysErrors.ErrorKeysRecoveryCodeInvalid(); + } if (fresh) { await this.fs.promises.rm(this.keysPath, { force: true, @@ -146,15 +160,18 @@ class KeyManager { } await utils.mkdirExists(this.fs, this.keysPath); await utils.mkdirExists(this.fs, this.rootCertsPath); - const rootKeyPair = await this.setupRootKeyPair( + let rootKeyPair; + [rootKeyPair, recoveryCode] = await this.setupRootKeyPair( password, this.rootKeyPairBits, + recoveryCode, ); const rootCert = await this.setupRootCert( rootKeyPair, this.rootCertDuration, ); this.rootKeyPair = rootKeyPair; + this.recoveryCode = recoveryCode; this.rootCert = rootCert; this._dbKey = await this.setupKey(this.dbKeyPath, this.dbKeyBits); this._vaultKey = await this.setupKey(this.vaultKeyPath, this.vaultKeyBits); @@ -175,6 +192,16 @@ class KeyManager { this.logger.info(`Destroyed ${this.constructor.name}`); } + @ready(new keysErrors.ErrorKeyManagerNotRunning()) + get dbKey(): Buffer { + return this._dbKey; + } + + @ready(new keysErrors.ErrorKeyManagerNotRunning()) + get vaultKey(): VaultKey { + return this._vaultKey as VaultKey; + } + @ready(new keysErrors.ErrorKeyManagerNotRunning()) public getRootKeyPair(): KeyPair { return keysUtils.keyPairCopy(this.rootKeyPair); @@ -195,6 +222,14 @@ class KeyManager { return keysUtils.certToPem(this.rootCert); } + /** + * Gets the recovery code if it has been generated + */ + @ready(new keysErrors.ErrorKeyManagerNotRunning()) + public getRecoveryCode(): RecoveryCode | undefined { + return this.recoveryCode; + } + /** * Gets an array of certificates in order of leaf to root */ @@ -259,6 +294,12 @@ class KeyManager { return true; } + @ready(new keysErrors.ErrorKeyManagerNotRunning()) + public async changePassword(password: string): Promise { + this.logger.info('Changing root key pair password'); + await this.writeRootKeyPair(this.rootKeyPair, password); + } + @ready(new keysErrors.ErrorKeyManagerNotRunning()) public async encryptWithRootKeyPair(plainText: Buffer): Promise { const publicKey = this.rootKeyPair.publicKey; @@ -344,12 +385,6 @@ class KeyManager { return signed; } - @ready(new keysErrors.ErrorKeyManagerNotRunning()) - public async changeRootKeyPassword(password: string): Promise { - this.logger.info('Changing root key pair password'); - await this.writeRootKeyPair(this.rootKeyPair, password); - } - /** * Generates a new root key pair * Forces a generation of a leaf certificate as the new root certificate @@ -368,7 +403,8 @@ class KeyManager { this.logger.info('Renewing root key pair'); const keysDbKeyPlain = await this.readKey(this.dbKeyPath); const keysVaultKeyPlain = await this.readKey(this.vaultKeyPath); - const rootKeyPair = await this.generateKeyPair(bits); + const recoveryCodeNew = keysUtils.generateRecoveryCode(); + const rootKeyPair = await this.generateKeyPair(bits, recoveryCodeNew); const now = new Date(); const rootCert = keysUtils.generateCertificate( rootKeyPair.publicKey, @@ -404,6 +440,7 @@ class KeyManager { this.writeKey(keysVaultKeyPlain, this.vaultKeyPath, rootKeyPair), ]); this.rootKeyPair = rootKeyPair; + this.recoveryCode = recoveryCodeNew; this.rootCert = rootCert; } @@ -423,7 +460,8 @@ class KeyManager { this.logger.info('Resetting root key pair'); const keysDbKeyPlain = await this.readKey(this.dbKeyPath); const keysVaultKeyPlain = await this.readKey(this.vaultKeyPath); - const rootKeyPair = await this.generateKeyPair(bits); + const recoveryCodeNew = keysUtils.generateRecoveryCode(); + const rootKeyPair = await this.generateKeyPair(bits, recoveryCodeNew); const rootCert = keysUtils.generateCertificate( rootKeyPair.publicKey, rootKeyPair.privateKey, @@ -441,6 +479,7 @@ class KeyManager { this.writeKey(keysVaultKeyPlain, this.vaultKeyPath, rootKeyPair), ]); this.rootKeyPair = rootKeyPair; + this.recoveryCode = recoveryCodeNew; this.rootCert = rootCert; } @@ -499,19 +538,81 @@ class KeyManager { } } + /** + * Generates a key pair + * If recovery code is passed in, it is used as a deterministic seed + * Uses the worker manager if available + */ + protected async generateKeyPair( + bits: number, + recoveryCode?: RecoveryCode, + ): Promise { + let keyPair; + if (this.workerManager) { + keyPair = await this.workerManager.call(async (w) => { + let keyPair; + if (recoveryCode != null) { + keyPair = await w.generateDeterministicKeyPairAsn1( + bits, + recoveryCode, + ); + } else { + keyPair = await w.generateKeyPairAsn1(bits); + } + return keysUtils.keyPairFromAsn1(keyPair); + }); + } else { + if (recoveryCode != null) { + keyPair = await keysUtils.generateDeterministicKeyPair( + bits, + recoveryCode, + ); + } else { + keyPair = await keysUtils.generateKeyPair(bits); + } + } + return keyPair; + } + protected async setupRootKeyPair( password: string, bits: number = 4096, - ): Promise { - let rootKeyPair; + recoveryCode: RecoveryCode | undefined, + ): Promise<[KeyPair, RecoveryCode | undefined]> { + let rootKeyPair: KeyPair; + let recoveryCodeNew: RecoveryCode | undefined; if (await this.existsRootKeyPair()) { - rootKeyPair = await this.readRootKeyPair(password); + if (recoveryCode != null) { + // Recover the key pair with the recovery code + // Check if the generated key pair matches + const rootKeyPairCheck = await this.generateKeyPair(bits, recoveryCode); + if (!(await this.matchRootKeyPair(rootKeyPairCheck))) { + throw new keysErrors.ErrorKeysRecoveryCodeIncorrect(); + } + // Recovered key pair, write the key pair with the new password + rootKeyPair = rootKeyPairCheck; + await this.writeRootKeyPair(rootKeyPairCheck, password); + } else { + // Load key pair by decrypting with password + rootKeyPair = await this.readRootKeyPair(password); + } + return [rootKeyPair, undefined]; } else { this.logger.info('Generating root key pair'); - rootKeyPair = await this.generateKeyPair(bits); - await this.writeRootKeyPair(rootKeyPair, password); + if (recoveryCode != null) { + // Deterministic key pair generation from recovery code + // Recovery code is new by virtue of generating key pair + recoveryCodeNew = recoveryCode; + rootKeyPair = await this.generateKeyPair(bits, recoveryCode); + await this.writeRootKeyPair(rootKeyPair, password); + } else { + // Randomly generated recovery code + recoveryCodeNew = keysUtils.generateRecoveryCode(); + rootKeyPair = await this.generateKeyPair(bits, recoveryCodeNew); + await this.writeRootKeyPair(rootKeyPair, password); + } + return [rootKeyPair, recoveryCodeNew]; } - return rootKeyPair; } protected async existsRootKeyPair(): Promise { @@ -597,6 +698,27 @@ class KeyManager { } } + protected async matchRootKeyPair(keyPair: KeyPair): Promise { + let publicKeyPem: string; + try { + publicKeyPem = await this.fs.promises.readFile(this.rootPubPath, { + encoding: 'utf8', + }); + } catch (e) { + if (e.code === 'ENOENT') { + return false; + } + throw new keysErrors.ErrorRootKeysRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } + const publicKeyPemCheck = keysUtils.publicKeyToPem(keyPair.publicKey); + return publicKeyPemCheck === publicKeyPem; + } + protected async setupKey( keyPath: string, bits: number = 256, @@ -679,33 +801,6 @@ class KeyManager { } } - @ready(new keysErrors.ErrorKeyManagerNotRunning()) - get dbKey(): Buffer { - return this._dbKey; - } - - @ready(new keysErrors.ErrorKeyManagerNotRunning()) - get vaultKey(): VaultKey { - return this._vaultKey as VaultKey; - } - - /** - * Generates a key pair - * Uses the worker manager if available - */ - protected async generateKeyPair(bits: number): Promise { - let keyPair; - if (this.workerManager) { - keyPair = await this.workerManager.call(async (w) => { - const keyPair = await w.generateKeyPairAsn1(bits); - return keysUtils.keyPairFromAsn1(keyPair); - }); - } else { - keyPair = await keysUtils.generateKeyPair(bits); - } - return keyPair; - } - protected async setupRootCert( keyPair: KeyPair, duration: number = 31536000, diff --git a/src/keys/errors.ts b/src/keys/errors.ts index ee5e2d3fc..e239f3a4b 100644 --- a/src/keys/errors.ts +++ b/src/keys/errors.ts @@ -1,4 +1,4 @@ -import { ErrorPolykey } from '../errors'; +import { ErrorPolykey, sysexits } from '../errors'; class ErrorKeys extends ErrorPolykey {} @@ -8,6 +8,22 @@ class ErrorKeyManagerNotRunning extends ErrorKeys {} class ErrorKeyManagerDestroyed extends ErrorKeys {} +class ErrorKeysPasswordInvalid extends ErrorKeys { + description = 'Password has invalid format'; + exitCode = sysexits.USAGE; +} + +class ErrorKeysRecoveryCodeInvalid extends ErrorKeys { + description = 'Recovery code has invalid format'; + exitCode = sysexits.USAGE; +} + +class ErrorKeysRecoveryCodeIncorrect extends ErrorKeys { + description = + "Recovered key pair's public key does not match the root public key"; + exitCode = sysexits.USAGE; +} + class ErrorRootKeysRead extends ErrorKeys {} class ErrorRootKeysParse extends ErrorKeys {} @@ -35,6 +51,9 @@ export { ErrorKeyManagerRunning, ErrorKeyManagerNotRunning, ErrorKeyManagerDestroyed, + ErrorKeysPasswordInvalid, + ErrorKeysRecoveryCodeInvalid, + ErrorKeysRecoveryCodeIncorrect, ErrorRootKeysRead, ErrorRootKeysParse, ErrorRootKeysWrite, diff --git a/src/keys/types.ts b/src/keys/types.ts index 261e27acd..bad73403e 100644 --- a/src/keys/types.ts +++ b/src/keys/types.ts @@ -1,4 +1,5 @@ import type { asn1, pki } from 'node-forge'; +import type { Opaque } from '../types'; type PublicKey = pki.rsa.PublicKey; type PrivateKey = pki.rsa.PrivateKey; @@ -21,6 +22,7 @@ type Certificate = pki.Certificate; type CertificateAsn1 = asn1.Asn1; type CertificatePem = string; type CertificatePemChain = string; +type RecoveryCode = Opaque<'RecoveryCode', string>; export type { PublicKey, @@ -38,4 +40,5 @@ export type { CertificateAsn1, CertificatePem, CertificatePemChain, + RecoveryCode, }; diff --git a/src/keys/utils.ts b/src/keys/utils.ts index 0a231b5a9..2de8a60dd 100644 --- a/src/keys/utils.ts +++ b/src/keys/utils.ts @@ -13,6 +13,7 @@ import type { PublicKeyAsn1, PrivateKeyAsn1, PublicKeyPem, + RecoveryCode, } from './types'; import { Buffer } from 'buffer'; @@ -27,34 +28,60 @@ import { pkcs5, util as forgeUtil, } from 'node-forge'; +import * as bip39 from 'bip39'; import * as keysErrors from './errors'; import config from '../config'; import * as utils from '../utils'; -import { promisify } from '../utils'; +import { never } from '../utils'; // Using never as utils.never seems to cause a build error. function thinks it could return undefined. import { makeNodeId } from '../nodes/utils'; +bip39.setDefaultWordlist('english'); + const ivSize = 16; const authTagSize = 16; async function generateKeyPair(bits: number): Promise { - const generateKeyPair = promisify(pki.rsa.generateKeyPair).bind(pki.rsa); + const generateKeyPair = utils + .promisify(pki.rsa.generateKeyPair) + .bind(pki.rsa); return await generateKeyPair({ bits }); } async function generateDeterministicKeyPair( bits: number, - seed: string, + recoveryCode: string, ): Promise { const prng = random.createInstance(); prng.seedFileSync = (needed: number) => { // Using bip39 seed generation parameters // no passphrase is considered here - return pkcs5.pbkdf2(seed, 'mnemonic', 2048, needed, md.sha512.create()); + return pkcs5.pbkdf2( + recoveryCode, + 'mnemonic', + 2048, + needed, + md.sha512.create(), + ); }; - const generateKeyPair = promisify(pki.rsa.generateKeyPair).bind(pki.rsa); + const generateKeyPair = utils + .promisify(pki.rsa.generateKeyPair) + .bind(pki.rsa); return await generateKeyPair({ bits, prng }); } +function generateRecoveryCode(size: 12 | 24 = 24): RecoveryCode { + if (size === 12) { + return bip39.generateMnemonic(128, getRandomBytesSync) as RecoveryCode; + } else if (size === 24) { + return bip39.generateMnemonic(256, getRandomBytesSync) as RecoveryCode; + } + never(); +} + +function validateRecoveryCode(recoveryCode: string): boolean { + return bip39.validateMnemonic(recoveryCode); +} + function publicKeyToPem(publicKey: PublicKey): PublicKeyPem { return pki.publicKeyToPem(publicKey); } @@ -549,6 +576,8 @@ export { privateKeyFromAsn1, generateKeyPair, generateDeterministicKeyPair, + generateRecoveryCode, + validateRecoveryCode, keyPairToAsn1, keyPairFromAsn1, keyPairToPem, diff --git a/src/schema/Schema.ts b/src/schema/Schema.ts index aded8d741..82b8da2c5 100644 --- a/src/schema/Schema.ts +++ b/src/schema/Schema.ts @@ -159,7 +159,7 @@ class Schema { } const stateVersion = parseInt(stateVersionData.trim()); if (isNaN(stateVersion)) { - throw schemaErrors.ErrorSchemaVersionParse; + throw new schemaErrors.ErrorSchemaVersionParse(); } return stateVersion as StateVersion; }); diff --git a/src/sessions/Session.ts b/src/sessions/Session.ts index 536e871c2..c4f53091a 100644 --- a/src/sessions/Session.ts +++ b/src/sessions/Session.ts @@ -143,10 +143,7 @@ class Session { } await sessionTokenFile.truncate(); // Writes from the beginning - await sessionTokenFile.writeFile( - (sessionToken as string) + '\n', - 'utf-8', - ); + await sessionTokenFile.write((sessionToken as string) + '\n', 0, 'utf-8'); } finally { if (sessionTokenFile != null) { lock.unlock(sessionTokenFile.fd); diff --git a/src/sessions/errors.ts b/src/sessions/errors.ts index c850e6d8c..837020898 100644 --- a/src/sessions/errors.ts +++ b/src/sessions/errors.ts @@ -1,21 +1,21 @@ import { ErrorPolykey } from '../errors'; -class ErrorSession extends ErrorPolykey {} +class ErrorSessions extends ErrorPolykey {} -class ErrorSessionRunning extends ErrorSession {} +class ErrorSessionRunning extends ErrorSessions {} -class ErrorSessionNotRunning extends ErrorSession {} +class ErrorSessionNotRunning extends ErrorSessions {} -class ErrorSessionDestroyed extends ErrorSession {} +class ErrorSessionDestroyed extends ErrorSessions {} -class ErrorSessionManagerRunning extends ErrorSession {} +class ErrorSessionManagerRunning extends ErrorSessions {} -class ErrorSessionManagerNotRunning extends ErrorSession {} +class ErrorSessionManagerNotRunning extends ErrorSessions {} -class ErrorSessionManagerDestroyed extends ErrorSession {} +class ErrorSessionManagerDestroyed extends ErrorSessions {} export { - ErrorSession, + ErrorSessions, ErrorSessionRunning, ErrorSessionNotRunning, ErrorSessionDestroyed, diff --git a/src/status/Status.ts b/src/status/Status.ts index 89555d0b2..96d7aa1a9 100644 --- a/src/status/Status.ts +++ b/src/status/Status.ts @@ -1,180 +1,180 @@ -import type { FileSystem, LockConfig } from '../types'; -import type { LockStatus } from '../types'; - -import type { FileHandle } from 'fs/promises'; -import path from 'path'; +import type { + StatusInfo, + StatusStarting, + StatusLive, + StatusStopping, + StatusDead, +} from './types'; +import type { FileSystem, FileHandle } from '../types'; import Logger from '@matrixai/logger'; import lock from 'fd-lock'; import { StartStop, ready } from '@matrixai/async-init/dist/StartStop'; import * as statusErrors from './errors'; - -const STATUS_FILE_NAME = 'agent-status.json'; +import * as statusUtils from './utils'; +import { poll } from '../utils'; interface Status extends StartStop {} @StartStop() class Status { - public readonly nodePath: string; - public readonly lockPath: string; + public readonly statusPath: string; - protected config: LockConfig; - protected fs: FileSystem; protected logger: Logger; - protected fh: FileHandle; + protected fs: FileSystem; + protected statusFile: FileHandle; - public static async createStatus({ - nodePath, - fs, + public constructor({ + statusPath, + fs = require('fs'), logger, }: { - nodePath: string; + statusPath: string; fs?: FileSystem; logger?: Logger; - }): Promise { - const fs_ = fs ?? require('fs'); - const nodePath_ = nodePath; - const logger_ = logger ?? new Logger(this.name); - const lockPath = path.join(nodePath_, STATUS_FILE_NAME); - - // Creating lock - return new Status({ - nodePath: nodePath_, - fs: fs_, - logger: logger_, - lockPath, - }); - } - - constructor({ - nodePath, - fs, - logger, - lockPath, - }: { - nodePath: string; - fs: FileSystem; - logger: Logger; - lockPath: string; }) { + this.logger = logger ?? new Logger(this.constructor.name); + this.statusPath = statusPath; this.fs = fs; - this.nodePath = nodePath; - this.logger = logger; - this.lockPath = lockPath; } - /** - * Start the status, checks for any existing lock files, and writes - * the status. - * - * @throws ErrorPolykey if there is an existing status with a running pid - */ - public async start() { - this.fh = await this.fs.promises.open( - this.lockPath, - this.fs.constants.O_RDWR | this.fs.constants.O_CREAT, + public async start(data: StatusStarting['data']): Promise { + this.logger.info(`Starting ${this.constructor.name}`); + const statusFile = await this.fs.promises.open( + this.statusPath, + this.fs.constants.O_WRONLY | this.fs.constants.O_CREAT, ); - const stat = lock(this.fh.fd); - if (!stat) { - await this.fh.close(); - const lock = (await this.parseStatus())!; - this.logger.error( - `Lockfile being held by pid: ${lock.pid}. Is a PolykeyAgent already running?`, - ); - throw new statusErrors.ErrorStatusLockFailed( - `Lockfile being held by pid: ${lock.pid}`, - ); + if (!lock(statusFile.fd)) { + await statusFile.close(); + throw new statusErrors.ErrorStatusLocked(); } - - this.config = { - status: 'STARTING', - pid: process.pid, - }; - - await this.writeStatus(); - } - - public async finishStart() { - await this.updateStatus('status', 'RUNNING'); - } - - public async beginStop() { - await this.updateStatus('status', 'STOPPING'); + this.statusFile = statusFile; + try { + await this.writeStatus({ + status: 'STARTING', + data, + }); + } catch (e) { + lock.unlock(this.statusFile.fd); + await this.statusFile.close(); + throw e; + } + this.logger.info(`${this.constructor.name} is STARTING`); } - public async stop() { - this.logger.info( - `Releasing and deleting lockfile from ${path.join( - this.nodePath, - 'agent-status.json', - )}`, - ); - lock.unlock(this.fh.fd); - await this.fh.close(); - await this.fs.promises.rm(this.lockPath); + @ready(new statusErrors.ErrorStatusNotRunning()) + public async finishStart(data: StatusLive['data']): Promise { + this.logger.info(`Finish ${this.constructor.name} STARTING`); + await this.writeStatus({ + status: 'LIVE', + data, + }); + this.logger.info(`${this.constructor.name} is LIVE`); } - /** - * Updates the configuration stored in the status, then attempts - * to write the configuration to the lockPath - * @param key - * @param value - */ @ready(new statusErrors.ErrorStatusNotRunning()) - public async updateStatus(key: string, value: any): Promise { - this.config[key] = value; - await this.writeStatus(); + public async beginStop(data: StatusStopping['data']): Promise { + this.logger.info(`Begin ${this.constructor.name} STOPPING`); + await this.writeStatus({ + status: 'STOPPING', + data, + }); + this.logger.info(`${this.constructor.name} is STOPPING`); } - /** - * Writes the config to the lockPath - */ - private async writeStatus(): Promise { - this.logger.info(`Writing lockfile to ${this.lockPath}`); - await this.fs.promises.writeFile( - this.lockPath, - JSON.stringify(this.config), - ); + public async stop(data: StatusDead['data']): Promise { + this.logger.info(`Stopping ${this.constructor.name}`); + await this.writeStatus({ + status: 'DEAD', + data, + }); + lock.unlock(this.statusFile.fd); + await this.statusFile.close(); + this.logger.info(`${this.constructor.name} is DEAD`); } /** - * Attempts to parse the status given the current nodePath/lockPath. If it exists, - * returns the data within. Otherwise, returns false - * @returns config or false + * Read the status data + * This can be used without running Status */ - public async parseStatus(): Promise { - const data = await this.fs.promises.readFile(this.lockPath); - return JSON.parse(data.toString()); + public async readStatus(): Promise { + let statusData: string; + try { + statusData = await this.fs.promises.readFile(this.statusPath, 'utf-8'); + } catch (e) { + if (e.code === 'ENOENT') { + return; + } + throw new statusErrors.ErrorStatusRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } + let statusInfo; + try { + statusInfo = JSON.parse(statusData); + } catch (e) { + throw new statusErrors.ErrorStatusParse('JSON parsing failed'); + } + if (!statusUtils.statusValidate(statusInfo)) { + throw new statusErrors.ErrorStatusParse('StatusInfo validation failed', { + errors: statusUtils.statusValidate.errors, + }); + } + return statusInfo as StatusInfo; } - /** - * Checks the status of the status - * @returns - * 'UNLOCKED' - If the file was not locked. - * 'STARTING' - If the Agent is in the process of starting. - * 'RUNNING' - If the Agent is running. - * 'STOPPING' - If the Agent is in the process of stopping. - */ - public async checkStatus(): Promise { - let fh; + protected async writeStatus(statusInfo: StatusInfo): Promise { + this.logger.info(`Writing Status file to ${this.statusPath}`); try { - fh = await this.fs.promises.open(this.lockPath, 'r'); - if (lock(fh.fd)) { - // Was unlocked - lock.unlock(fh.fd); - await fh.close(); - return 'UNLOCKED'; - } else { - // Is locked, get status. - await fh.close(); - return (await this.parseStatus()).status; - } + await this.statusFile.truncate(); + await this.statusFile.write( + JSON.stringify(statusInfo, undefined, 2) + '\n', + 0, + 'utf-8', + ); } catch (e) { - if (e.code === 'ENOENT') return 'UNLOCKED'; - throw e; - } finally { - await fh?.close(); + throw new statusErrors.ErrorStatusWrite(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } + } + + public async waitFor( + status: StatusInfo['status'], + timeout?: number, + ): Promise { + const statusInfo = await poll( + async () => { + return await this.readStatus(); + }, + (e, statusInfo) => { + if (e != null) return true; + // DEAD status is a special case + // it is acceptable for the status file to not exist + if ( + status === 'DEAD' && + (statusInfo == null || statusInfo.status === 'DEAD') + ) { + return true; + } + if (statusInfo?.status === status) return true; + return false; + }, + 250, + timeout, + ); + if (statusInfo == null) { + return { + status: 'DEAD', + data: {}, + }; } + return statusInfo; } } -export { STATUS_FILE_NAME }; export default Status; diff --git a/src/status/StatusSchema.json b/src/status/StatusSchema.json new file mode 100644 index 000000000..59ac9bd1f --- /dev/null +++ b/src/status/StatusSchema.json @@ -0,0 +1,67 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "required": ["status", "data"], + "oneOf": [ + { + "properties": { + "status": { + "type": "string", + "const": "STARTING" + }, + "data": { + "type": "object", + "properties": { + "pid": { "type": "number" } + }, + "required": ["pid"] + } + } + }, + { + "properties": { + "status": { + "type": "string", + "const": "LIVE" + }, + "data": { + "type": "object", + "properties": { + "pid": { "type": "number" }, + "nodeId": { "type": "string" }, + "clientHost": { "type": "string" }, + "clientPort": { "type": "number" } + }, + "required": ["pid", "nodeId", "clientHost", "clientPort"] + } + } + }, + { + "properties": { + "status": { + "type": "string", + "const": "STOPPING" + }, + "data": { + "type": "object", + "properties": { + "pid": { "type": "number" } + }, + "required": ["pid"] + } + } + }, + { + "properties": { + "status": { + "type": "string", + "const": "DEAD" + }, + "data": { + "type": "object", + "required": [] + } + } + } + ] +} diff --git a/src/status/errors.ts b/src/status/errors.ts index 9705815f3..f2eccb171 100644 --- a/src/status/errors.ts +++ b/src/status/errors.ts @@ -1,9 +1,34 @@ -import { ErrorPolykey } from '../errors'; +import { ErrorPolykey, sysexits } from '../errors'; class ErrorStatus extends ErrorPolykey {} class ErrorStatusNotRunning extends ErrorStatus {} -class ErrorStatusLockFailed extends ErrorStatus {} +class ErrorStatusLocked extends ErrorStatus { + decription = 'Status is locked by another process'; + exitCode = sysexits.TEMPFAIL; +} -export { ErrorStatus, ErrorStatusNotRunning, ErrorStatusLockFailed }; +class ErrorStatusRead extends ErrorStatus { + decription = 'Failed to read status info'; + exitCode = sysexits.IOERR; +} + +class ErrorStatusWrite extends ErrorStatus { + decription = 'Failed to write status info'; + exitCode = sysexits.IOERR; +} + +class ErrorStatusParse extends ErrorStatus { + decription = 'Failed to parse status info'; + exitCode = sysexits.CONFIG; +} + +export { + ErrorStatus, + ErrorStatusNotRunning, + ErrorStatusLocked, + ErrorStatusRead, + ErrorStatusWrite, + ErrorStatusParse, +}; diff --git a/src/status/index.ts b/src/status/index.ts index 04e548f62..d80ffade1 100644 --- a/src/status/index.ts +++ b/src/status/index.ts @@ -1,2 +1,4 @@ export { default as Status } from './Status'; -export * from './Status'; +export * as types from './types'; +export * as errors from './errors'; +export * as utils from './utils'; diff --git a/src/status/types.ts b/src/status/types.ts new file mode 100644 index 000000000..000da195d --- /dev/null +++ b/src/status/types.ts @@ -0,0 +1,46 @@ +import type { NodeId } from '../nodes/types'; +import type { Host, Port } from '../network/types'; + +type StatusStarting = { + status: 'STARTING'; + data: { + pid: number; + [key: string]: any; + }; +}; + +type StatusLive = { + status: 'LIVE'; + data: { + pid: number; + nodeId: NodeId; + clientHost: Host; + clientPort: Port; + [key: string]: any; + }; +}; + +type StatusStopping = { + status: 'STOPPING'; + data: { + pid: number; + [key: string]: any; + }; +}; + +type StatusDead = { + status: 'DEAD'; + data: { + [key: string]: any; + }; +}; + +type StatusInfo = StatusStarting | StatusLive | StatusStopping | StatusDead; + +export type { + StatusInfo, + StatusStarting, + StatusLive, + StatusStopping, + StatusDead, +}; diff --git a/src/status/utils.ts b/src/status/utils.ts new file mode 100644 index 000000000..ab6e820ce --- /dev/null +++ b/src/status/utils.ts @@ -0,0 +1,11 @@ +import type { JSONSchemaType, ValidateFunction } from 'ajv'; +import type { StatusInfo } from './types'; +import Ajv from 'ajv'; +import StatusSchema from './StatusSchema.json'; + +const ajv = new Ajv(); + +const statusSchema = StatusSchema as JSONSchemaType; +const statusValidate: ValidateFunction = ajv.compile(statusSchema); + +export { statusSchema, statusValidate }; diff --git a/src/types.ts b/src/types.ts index 420dc8c9b..b09954b32 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,16 +69,10 @@ interface FileSystem { }; constants: typeof fs.constants; } -type LockStatus = 'STARTING' | 'RUNNING' | 'STOPPING' | 'UNLOCKED'; -type LockConfig = { - status: LockStatus; - pid: number; - nodeId?: string; - clientHost?: string; - clientPort?: number | undefined; -} & POJO; -export { +type FileHandle = fs.promises.FileHandle; + +export type { POJO, Opaque, AbstractConstructorParameters, @@ -88,6 +82,5 @@ export { Ref, Timer, FileSystem, - LockStatus, - LockConfig, + FileHandle, }; diff --git a/src/utils/errors.ts b/src/utils/errors.ts new file mode 100644 index 000000000..af54ffa86 --- /dev/null +++ b/src/utils/errors.ts @@ -0,0 +1,31 @@ +import sysexits from './sysexits'; +import ErrorPolykey from '../ErrorPolykey'; + +class ErrorUtils extends ErrorPolykey {} + +/** + * This is a special error that is only used for absurd situations + * Intended to placate typescript so that unreachable code type checks + * If this is thrown, this means there is a bug in the code + */ +class ErrorUtilsUndefinedBehaviour extends ErrorUtils { + description = 'You should never see this error'; + exitCode = sysexits.SOFTWARE; +} + +class ErrorUtilsPollTimeout extends ErrorUtils { + description = 'Poll timed out'; + exitCode = sysexits.TEMPFAIL; +} + +class ErrorUtilsNodePath extends ErrorUtils { + description = 'Cannot derive default node path from unknown platform'; + exitCode = sysexits.USAGE; +} + +export { + ErrorUtils, + ErrorUtilsUndefinedBehaviour, + ErrorUtilsPollTimeout, + ErrorUtilsNodePath, +}; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 000000000..d0bfeb4c0 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,4 @@ +export { default as sysexits } from './sysexits'; +export * from './locks'; +export * from './utils'; +export * as errors from './errors'; diff --git a/src/utils/locks.ts b/src/utils/locks.ts new file mode 100644 index 000000000..9ae553c6f --- /dev/null +++ b/src/utils/locks.ts @@ -0,0 +1,36 @@ +import type { MutexInterface } from 'async-mutex'; +import { Mutex } from 'async-mutex'; + +class RWLock { + protected readerCount: number = 0; + protected lock: Mutex = new Mutex(); + protected release: MutexInterface.Releaser; + + public async read(f: () => Promise): Promise { + let readerCount = ++this.readerCount; + // The first reader locks + if (readerCount === 1) { + this.release = await this.lock.acquire(); + } + try { + return await f(); + } finally { + readerCount = --this.readerCount; + // The last reader unlocks + if (readerCount === 0) { + this.release(); + } + } + } + + public async write(f: () => Promise): Promise { + this.release = await this.lock.acquire(); + try { + return await f(); + } finally { + this.release(); + } + } +} + +export { RWLock }; diff --git a/src/utils/sysexits.ts b/src/utils/sysexits.ts new file mode 100644 index 000000000..d48e6dacf --- /dev/null +++ b/src/utils/sysexits.ts @@ -0,0 +1,27 @@ +const sysexits = Object.freeze({ + OK: 0, + GENERAL: 1, + // Sysexit standard starts at 64 to avoid conflicts + USAGE: 64, + DATAERR: 65, + NOINPUT: 66, + NOUSER: 67, + NOHOST: 68, + UNAVAILABLE: 69, + SOFTWARE: 70, + OSERR: 71, + OSFILE: 72, + CANTCREAT: 73, + IOERR: 74, + TEMPFAIL: 75, + PROTOCOL: 76, + NOPERM: 77, + CONFIG: 78, + CANNOT_EXEC: 126, + COMMAND_NOT_FOUND: 127, + INVALID_EXIT_ARG: 128, + // 128+ are reserved for signal exits + UNKNOWN: 255, +}); + +export default sysexits; diff --git a/src/utils.ts b/src/utils/utils.ts similarity index 67% rename from src/utils.ts rename to src/utils/utils.ts index 4ab7a1ff8..2e53f3fc6 100644 --- a/src/utils.ts +++ b/src/utils/utils.ts @@ -1,9 +1,41 @@ -import type { MutexInterface } from 'async-mutex'; - -import type { FileSystem, Timer } from './types'; +import type { FileSystem, Timer } from '../types'; +import os from 'os'; import process from 'process'; import path from 'path'; -import { Mutex } from 'async-mutex'; +import * as utilsErrors from './errors'; + +function getDefaultNodePath(): string | undefined { + const prefix = 'polykey'; + const platform = os.platform(); + let p: string; + if (platform === 'linux') { + const homeDir = os.homedir(); + const dataDir = process.env.XDG_DATA_HOME; + if (dataDir != null) { + p = `${dataDir}/${prefix}`; + } else { + p = `${homeDir}/.local/share/${prefix}`; + } + } else if (platform === 'darwin') { + const homeDir = os.homedir(); + p = `${homeDir}/Library/Application Support/${prefix}`; + } else if (platform === 'win32') { + const homeDir = os.homedir(); + const appDataDir = process.env.LOCALAPPDATA; + if (appDataDir != null) { + p = `${appDataDir}/${prefix}`; + } else { + p = `${homeDir}/AppData/Local/${prefix}`; + } + } else { + return; + } + return p; +} + +function never(): never { + throw new utilsErrors.ErrorUtilsUndefinedBehaviour(); +} async function mkdirExists(fs: FileSystem, path, ...args) { try { @@ -46,6 +78,9 @@ function isEmptyObject(o) { return true; } +/** + * Filters out all undefined properties recursively + */ function filterEmptyObject(o) { return Object.fromEntries( Object.entries(o) @@ -72,20 +107,29 @@ async function poll( (e: null, result: T): boolean; }, interval = 1000, -) { - let result: T; - while (true) { - try { - result = await f(); - if (condition(null, result)) { - return result; + timeout?: number, +): Promise { + const timer = timeout != null ? timerStart(timeout) : undefined; + try { + let result: T; + while (true) { + if (timer?.timedOut) { + throw new utilsErrors.ErrorUtilsPollTimeout(); } - } catch (e) { - if (condition(e)) { - throw e; + try { + result = await f(); + if (condition(null, result)) { + return result; + } + } catch (e) { + if (condition(e)) { + throw e; + } } + await sleep(interval); } - await sleep(interval); + } finally { + if (timer != null) timerStop(timer); } } @@ -156,39 +200,9 @@ function arrayUnset(items: Array, item: T) { } } -class RWLock { - protected readerCount: number = 0; - protected lock: Mutex = new Mutex(); - protected release: MutexInterface.Releaser; - - public async read(f: () => Promise): Promise { - let readerCount = ++this.readerCount; - // The first reader locks - if (readerCount === 1) { - this.release = await this.lock.acquire(); - } - try { - return await f(); - } finally { - readerCount = --this.readerCount; - // The last reader unlocks - if (readerCount === 0) { - this.release(); - } - } - } - - public async write(f: () => Promise): Promise { - this.release = await this.lock.acquire(); - try { - return await f(); - } finally { - this.release(); - } - } -} - export { + getDefaultNodePath, + never, mkdirExists, pathIncludes, pidIsRunning, @@ -204,5 +218,4 @@ export { timerStop, arraySet, arrayUnset, - RWLock, }; diff --git a/src/vaults/VaultOps.ts b/src/vaults/VaultOps.ts index 6e1c50da7..a2d90921f 100644 --- a/src/vaults/VaultOps.ts +++ b/src/vaults/VaultOps.ts @@ -9,8 +9,8 @@ import type { SecretName, Vault, } from './types'; +import type { FileSystem } from '../types'; import path from 'path'; -import * as fs from 'fs'; import * as vaultsErrors from './errors'; import * as vaultsUtils from './utils'; @@ -185,6 +185,7 @@ async function mkdir( async function addSecretDirectory( vault: Vault, secretDirectory: SecretName, + fs: FileSystem, logger?: Logger, ): Promise { const absoluteDirPath = path.resolve(secretDirectory); diff --git a/src/workers/polykeyWorkerModule.ts b/src/workers/polykeyWorkerModule.ts index 882b95ec8..068896428 100644 --- a/src/workers/polykeyWorkerModule.ts +++ b/src/workers/polykeyWorkerModule.ts @@ -40,6 +40,16 @@ const polykeyWorker = { const keyPair = await keysUtils.generateKeyPair(bits); return keysUtils.keyPairToAsn1(keyPair); }, + async generateDeterministicKeyPairAsn1( + bits: number, + recoveryCode: string, + ): Promise { + const keyPair = await keysUtils.generateDeterministicKeyPair( + bits, + recoveryCode, + ); + return keysUtils.keyPairToAsn1(keyPair); + }, encryptWithPublicKeyAsn1( publicKeyAsn1: PublicKeyAsn1, plainText: string, diff --git a/tests/PolykeyAgent.test.ts b/tests/PolykeyAgent.test.ts index fac132ca2..5fd643a08 100644 --- a/tests/PolykeyAgent.test.ts +++ b/tests/PolykeyAgent.test.ts @@ -4,8 +4,15 @@ import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import PolykeyAgent from '@/PolykeyAgent'; import config from '@/config'; -// Import { ErrorStateVersionMismatch } from '@/errors'; -import { checkAgentRunning } from '@/agent/utils'; +import { Status } from '@/status'; +import * as schemaErrors from '@/schema/errors'; + +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); describe('Polykey', () => { const password = 'password'; @@ -131,8 +138,7 @@ describe('Polykey', () => { nodePath, logger, }); - }).rejects.toThrow(); // FIXME, use proper error here. - // ErrorStateVersionMismatch + }).rejects.toThrow(schemaErrors.ErrorSchemaVersionParse); }, global.polykeyStartupTimeout, ); @@ -157,19 +163,28 @@ describe('Polykey', () => { }, global.polykeyStartupTimeout, ); - test('Stopping and destroying properly stops Polykey', async () => { - // Starting. - const nodePath = `${dataDir}/polykey`; - pk = await PolykeyAgent.createPolykeyAgent({ - password, - nodePath, - logger, - }); - expect(await checkAgentRunning(nodePath)).toBeTruthy(); - - await pk.stop(); - expect(await checkAgentRunning(nodePath)).toBeFalsy(); - await pk.destroy(); - expect(await checkAgentRunning(nodePath)).toBeFalsy(); - }); + test( + 'Stopping and destroying properly stops Polykey', + async () => { + // Starting. + const nodePath = `${dataDir}/polykey`; + pk = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + logger, + }); + const statusPath = path.join(nodePath, 'status.json'); + const status = new Status({ + statusPath, + fs, + logger, + }); + await status.waitFor('LIVE', 2000); + await pk.stop(); + await status.waitFor('DEAD', 2000); + await pk.destroy(); + await status.waitFor('DEAD', 2000); + }, + global.polykeyStartupTimeout * 2, + ); }); diff --git a/tests/acl/ACL.test.ts b/tests/acl/ACL.test.ts index d3264bee3..117babd30 100644 --- a/tests/acl/ACL.test.ts +++ b/tests/acl/ACL.test.ts @@ -8,19 +8,16 @@ import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; import { utils as idUtils } from '@matrixai/id'; - import { ACL, errors as aclErrors } from '@/acl'; -import { KeyManager } from '@/keys'; import { makeVaultId } from '@/vaults/utils'; +import * as keysUtils from '@/keys/utils'; import { makeCrypto } from '../utils'; describe('ACL', () => { - const password = 'password'; const logger = new Logger(`${ACL.name} Test`, LogLevel.WARN, [ new StreamHandler(), ]); let dataDir: string; - let keyManager: KeyManager; let db: DB; let vaultId1: VaultId; let vaultId2: VaultId; @@ -31,17 +28,12 @@ describe('ACL', () => { dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); - const keysPath = `${dataDir}/keys`; - keyManager = await KeyManager.createKeyManager({ - password, - keysPath, - logger, - }); + const dbKey = await keysUtils.generateKey(); const dbPath = `${dataDir}/db`; db = await DB.createDB({ dbPath, logger, - crypto: makeCrypto(keyManager), + crypto: makeCrypto(dbKey), }); vaultId1 = makeVaultId(idUtils.fromString('vault1xxxxxxxxxx')); vaultId2 = makeVaultId(idUtils.fromString('vault2xxxxxxxxxx')); @@ -50,7 +42,6 @@ describe('ACL', () => { }); afterEach(async () => { await db.stop(); - await keyManager.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, diff --git a/tests/agent/GRPCClientAgent.test.ts b/tests/agent/GRPCClientAgent.test.ts index c8089de40..264400932 100644 --- a/tests/agent/GRPCClientAgent.test.ts +++ b/tests/agent/GRPCClientAgent.test.ts @@ -26,9 +26,15 @@ import { utils as claimsUtils, errors as claimsErrors } from '@/claims'; import { makeNodeId } from '@/nodes/utils'; import * as testUtils from './utils'; import TestNodeConnection from '../nodes/TestNodeConnection'; - import { makeCrypto } from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('GRPC agent', () => { const password = 'password'; const logger = new Logger('AgentServerTest', LogLevel.WARN, [ @@ -95,7 +101,7 @@ describe('GRPC agent', () => { dbPath: dbPath, fs: fs, logger: logger, - crypto: makeCrypto(keyManager), + crypto: makeCrypto(keyManager.dbKey), }); acl = await ACL.createACL({ @@ -161,22 +167,14 @@ describe('GRPC agent', () => { await testUtils.closeTestAgentServer(server); await vaultManager.stop(); - await vaultManager.destroy(); await notificationsManager.stop(); - await notificationsManager.destroy(); await sigchain.stop(); - await sigchain.destroy(); await nodeManager.stop(); - await nodeManager.destroy(); await gestaltGraph.stop(); - await gestaltGraph.destroy(); await acl.stop(); - await acl.destroy(); await fwdProxy.stop(); await db.stop(); - await db.destroy(); await keyManager.stop(); - await keyManager.destroy(); await fs.promises.rm(dataDir, { force: true, @@ -302,126 +300,141 @@ describe('GRPC agent', () => { await yKeyManager.destroy(); }); - test('can successfully cross sign a claim', async () => { - const genClaims = client.nodesCrossSignClaim(); - expect(genClaims.stream.destroyed).toBe(false); - // 2. X <- sends its intermediary signed claim <- Y - // Create a dummy intermediary claim to "receive" - const claim = await claimsUtils.createClaim({ - privateKey: yKeyManager.getRootKeyPairPem().privateKey, - hPrev: null, - seq: 1, - data: { - type: 'node', - node1: nodeIdY, - node2: nodeIdX, - }, - kid: nodeIdY, - }); - const intermediary: ClaimIntermediary = { - payload: claim.payload, - signature: claim.signatures[0], - }; - const crossSignMessage = claimsUtils.createCrossSignMessage({ - singlySignedClaim: intermediary, - }); - await genClaims.write(crossSignMessage); - - // 3. X -> sends doubly signed claim (Y's intermediary) + its own intermediary claim -> Y - // X reads this intermediary signed claim, and is expected to send back: - // 1. Doubly signed claim - // 2. Singly signed intermediary claim - const response = await genClaims.read(); - // Check X's sigchain is locked at start - expect(sigchain.locked).toBe(true); - expect(response.done).toBe(false); - expect(response.value).toBeInstanceOf(nodesPB.CrossSign); - const receivedMessage = response.value as nodesPB.CrossSign; - expect(receivedMessage.getSinglySignedClaim()).toBeDefined(); - expect(receivedMessage.getDoublySignedClaim()).toBeDefined(); - const constructedIntermediary = claimsUtils.reconstructClaimIntermediary( - receivedMessage.getSinglySignedClaim()!, - ); - const constructedDoubly = claimsUtils.reconstructClaimEncoded( - receivedMessage.getDoublySignedClaim()!, - ); - // Verify the intermediary claim with X's public key - const verifiedSingly = await claimsUtils.verifyIntermediaryClaimSignature( - constructedIntermediary, - keyManager.getRootKeyPairPem().publicKey, - ); - expect(verifiedSingly).toBe(true); - // Verify the doubly signed claim with both public keys - const verifiedDoubly = - (await claimsUtils.verifyClaimSignature( - constructedDoubly, - yKeyManager.getRootKeyPairPem().publicKey, - )) && - (await claimsUtils.verifyClaimSignature( - constructedDoubly, - keyManager.getRootKeyPairPem().publicKey, - )); - expect(verifiedDoubly).toBe(true); - - // 4. X <- sends doubly signed claim (X's intermediary) <- Y - const doublyResponse = await claimsUtils.signIntermediaryClaim({ - claim: constructedIntermediary, - privateKey: yKeyManager.getRootKeyPairPem().privateKey, - signeeNodeId: nodeIdY, - }); - const doublyMessage = claimsUtils.createCrossSignMessage({ - doublySignedClaim: doublyResponse, - }); - // Just before we complete the last step, check X's sigchain is still locked - expect(sigchain.locked).toBe(true); - await genClaims.write(doublyMessage); - - // Expect the stream to be closed. - const finalResponse = await genClaims.read(); - expect(finalResponse.done).toBe(true); - expect(genClaims.stream.destroyed).toBe(true); - - // Check X's sigchain is released at end. - expect(sigchain.locked).toBe(false); - // Check claim is in both node's sigchains - // Rather, check it's in X's sigchain - const chain = await sigchain.getChainData(); - expect(Object.keys(chain).length).toBe(1); - // Iterate just to be safe, but expected to only have this single claim - for (const c of Object.keys(chain)) { - const claimId = c as ClaimIdString; - expect(chain[claimId]).toStrictEqual(doublyResponse); - } - }); - test('fails after receiving undefined singly signed claim', async () => { - const genClaims = client.nodesCrossSignClaim(); - expect(genClaims.stream.destroyed).toBe(false); - // 2. X <- sends its intermediary signed claim <- Y - const crossSignMessageUndefinedSingly = new nodesPB.CrossSign(); - await genClaims.write(crossSignMessageUndefinedSingly); - await expect(() => genClaims.read()).rejects.toThrow( - claimsErrors.ErrorUndefinedSinglySignedClaim, - ); - expect(genClaims.stream.destroyed).toBe(true); - // Check sigchain's lock is released - expect(sigchain.locked).toBe(false); - }); - test('fails after receiving singly signed claim with no signature', async () => { - const genClaims = client.nodesCrossSignClaim(); - expect(genClaims.stream.destroyed).toBe(false); - // 2. X <- sends its intermediary signed claim <- Y - const crossSignMessageUndefinedSinglySignature = new nodesPB.CrossSign(); - const intermediaryNoSignature = new nodesPB.ClaimIntermediary(); - crossSignMessageUndefinedSinglySignature.setSinglySignedClaim( - intermediaryNoSignature, - ); - await genClaims.write(crossSignMessageUndefinedSinglySignature); - await expect(() => genClaims.read()).rejects.toThrow( - claimsErrors.ErrorUndefinedSignature, - ); - expect(genClaims.stream.destroyed).toBe(true); - // Check sigchain's lock is released - expect(sigchain.locked).toBe(false); - }); + test( + 'can successfully cross sign a claim', + async () => { + const genClaims = client.nodesCrossSignClaim(); + expect(genClaims.stream.destroyed).toBe(false); + // 2. X <- sends its intermediary signed claim <- Y + // Create a dummy intermediary claim to "receive" + const claim = await claimsUtils.createClaim({ + privateKey: yKeyManager.getRootKeyPairPem().privateKey, + hPrev: null, + seq: 1, + data: { + type: 'node', + node1: nodeIdY, + node2: nodeIdX, + }, + kid: nodeIdY, + }); + const intermediary: ClaimIntermediary = { + payload: claim.payload, + signature: claim.signatures[0], + }; + const crossSignMessage = claimsUtils.createCrossSignMessage({ + singlySignedClaim: intermediary, + }); + await genClaims.write(crossSignMessage); + + // 3. X -> sends doubly signed claim (Y's intermediary) + its own intermediary claim -> Y + // X reads this intermediary signed claim, and is expected to send back: + // 1. Doubly signed claim + // 2. Singly signed intermediary claim + const response = await genClaims.read(); + // Check X's sigchain is locked at start + expect(sigchain.locked).toBe(true); + expect(response.done).toBe(false); + expect(response.value).toBeInstanceOf(nodesPB.CrossSign); + const receivedMessage = response.value as nodesPB.CrossSign; + expect(receivedMessage.getSinglySignedClaim()).toBeDefined(); + expect(receivedMessage.getDoublySignedClaim()).toBeDefined(); + const constructedIntermediary = + claimsUtils.reconstructClaimIntermediary( + receivedMessage.getSinglySignedClaim()!, + ); + const constructedDoubly = claimsUtils.reconstructClaimEncoded( + receivedMessage.getDoublySignedClaim()!, + ); + // Verify the intermediary claim with X's public key + const verifiedSingly = + await claimsUtils.verifyIntermediaryClaimSignature( + constructedIntermediary, + keyManager.getRootKeyPairPem().publicKey, + ); + expect(verifiedSingly).toBe(true); + // Verify the doubly signed claim with both public keys + const verifiedDoubly = + (await claimsUtils.verifyClaimSignature( + constructedDoubly, + yKeyManager.getRootKeyPairPem().publicKey, + )) && + (await claimsUtils.verifyClaimSignature( + constructedDoubly, + keyManager.getRootKeyPairPem().publicKey, + )); + expect(verifiedDoubly).toBe(true); + + // 4. X <- sends doubly signed claim (X's intermediary) <- Y + const doublyResponse = await claimsUtils.signIntermediaryClaim({ + claim: constructedIntermediary, + privateKey: yKeyManager.getRootKeyPairPem().privateKey, + signeeNodeId: nodeIdY, + }); + const doublyMessage = claimsUtils.createCrossSignMessage({ + doublySignedClaim: doublyResponse, + }); + // Just before we complete the last step, check X's sigchain is still locked + expect(sigchain.locked).toBe(true); + await genClaims.write(doublyMessage); + + // Expect the stream to be closed. + const finalResponse = await genClaims.read(); + expect(finalResponse.done).toBe(true); + expect(genClaims.stream.destroyed).toBe(true); + + // Check X's sigchain is released at end. + expect(sigchain.locked).toBe(false); + // Check claim is in both node's sigchains + // Rather, check it's in X's sigchain + const chain = await sigchain.getChainData(); + expect(Object.keys(chain).length).toBe(1); + // Iterate just to be safe, but expected to only have this single claim + for (const c of Object.keys(chain)) { + const claimId = c as ClaimIdString; + expect(chain[claimId]).toStrictEqual(doublyResponse); + } + }, + global.defaultTimeout * 4, + ); + test( + 'fails after receiving undefined singly signed claim', + async () => { + const genClaims = client.nodesCrossSignClaim(); + expect(genClaims.stream.destroyed).toBe(false); + // 2. X <- sends its intermediary signed claim <- Y + const crossSignMessageUndefinedSingly = new nodesPB.CrossSign(); + await genClaims.write(crossSignMessageUndefinedSingly); + await expect(() => genClaims.read()).rejects.toThrow( + claimsErrors.ErrorUndefinedSinglySignedClaim, + ); + expect(genClaims.stream.destroyed).toBe(true); + // Check sigchain's lock is released + expect(sigchain.locked).toBe(false); + }, + global.defaultTimeout * 4, + ); + test( + 'fails after receiving singly signed claim with no signature', + async () => { + const genClaims = client.nodesCrossSignClaim(); + expect(genClaims.stream.destroyed).toBe(false); + // 2. X <- sends its intermediary signed claim <- Y + const crossSignMessageUndefinedSinglySignature = + new nodesPB.CrossSign(); + const intermediaryNoSignature = new nodesPB.ClaimIntermediary(); + crossSignMessageUndefinedSinglySignature.setSinglySignedClaim( + intermediaryNoSignature, + ); + await genClaims.write(crossSignMessageUndefinedSinglySignature); + await expect(() => genClaims.read()).rejects.toThrow( + claimsErrors.ErrorUndefinedSignature, + ); + expect(genClaims.stream.destroyed).toBe(true); + // Check sigchain's lock is released + expect(sigchain.locked).toBe(false); + }, + global.defaultTimeout * 4, + ); }); }); diff --git a/tests/agent/utils.test.ts b/tests/agent/utils.test.ts deleted file mode 100644 index 8b3ba2106..000000000 --- a/tests/agent/utils.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - -import PolykeyAgent from '@/PolykeyAgent'; - -import * as agentUtils from '@/agent/utils'; -import { poll } from '../utils'; - -describe('agent utils', () => { - const logger = new Logger('AgentServerTest', LogLevel.WARN, [ - new StreamHandler(), - ]); - const password = 'password123'; - let dataDir: string; - let nodePath: string; - - describe('checkAgentRunning', () => { - beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-'), - ); - nodePath = path.join(dataDir, 'keyNode'); - }); - afterEach(async () => { - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - - test('False if agent not running.', async () => { - await expect(agentUtils.checkAgentRunning(nodePath)).resolves.toBeFalsy(); - }); - - test('True if agent running.', async () => { - const agent = await PolykeyAgent.createPolykeyAgent({ - password, - nodePath: nodePath, - logger: logger, - }); - await expect( - agentUtils.checkAgentRunning(nodePath), - ).resolves.toBeTruthy(); - await agent.stop(); - await agent.destroy(); - }); - }); - describe('spawnBackgroundAgent', () => { - beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-'), - ); - nodePath = path.join(dataDir, 'keyNode'); - }); - afterEach(async () => { - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - - test( - 'Should spawn an agent in the background.', - async () => { - await expect( - agentUtils.checkAgentRunning(nodePath), - ).resolves.toBeFalsy(); - const pid = await agentUtils.spawnBackgroundAgent(nodePath, password); - expect(typeof pid).toBe('number'); // Returns a number. - expect(pid > 0).toBeTruthy(); // Non-zero - await poll(global.polykeyStartupTimeout * 1.5, async () => { - return await agentUtils.checkAgentRunning(nodePath); - }); - // Killing the agent. - process.kill(pid); - - // Polling for agent to stop. - await poll(global.polykeyStartupTimeout, async () => { - const test = await agentUtils.checkAgentRunning(nodePath); - return !test; - }); - // Polling for removed lockfile. - // FIXME: It is not removing the lockfile propely. - // await poll(global.polykeyStartupTimeout, async () => { - // const agentLock = await fs.promises.readdir(nodePath); - // const test = agentLock.includes('agent-lock.json'); - // return !test; - // }); - }, - global.polykeyStartupTimeout * 3.5, - ); - test('Should throw error if agent already running.', async () => { - const agent = await PolykeyAgent.createPolykeyAgent({ - password, - nodePath: nodePath, - logger: logger, - }); - await expect( - agentUtils.checkAgentRunning(nodePath), - ).resolves.toBeTruthy(); - - await expect(() => - agentUtils.spawnBackgroundAgent(nodePath, password), - ).rejects.toThrow('running'); - await expect( - agentUtils.checkAgentRunning(nodePath), - ).resolves.toBeTruthy(); // Check that it is running. - - await agent.stop(); - await agent.destroy(); - }); - }); -}); diff --git a/tests/bin/agent.test.ts b/tests/bin/agent.test.ts deleted file mode 100644 index 15030fa11..000000000 --- a/tests/bin/agent.test.ts +++ /dev/null @@ -1,497 +0,0 @@ -import type { SessionToken } from '@/sessions/types'; -import os from 'os'; -import path from 'path'; -import fs from 'fs'; -import { env } from 'process'; -import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - -import * as agentUtils from '@/agent/utils'; - -import PolykeyAgent from '@/PolykeyAgent'; - -import { Status } from '@/status'; -import { makeNodeId } from '@/nodes/utils'; -import * as testUtils from './utils'; -import { poll } from '../utils'; - -describe('CLI agent', () => { - const noJWTFailCode = 77; - const password = 'password'; - const logger = new Logger('AgentServerTest', LogLevel.WARN, [ - new StreamHandler(), - ]); - - describe('Agent start, status and stop', () => { - let dataDir: string; - let foregroundNodePath: string; - let backgroundNodePath: string; - - let inactiveNodePath: string; - let activeNodePath: string; - let activeNode: PolykeyAgent; - let passwordFile: string; - - beforeAll(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-'), - ); - passwordFile = path.join(dataDir, 'passwordFile'); - foregroundNodePath = path.join(dataDir, 'foreground'); - backgroundNodePath = path.join(dataDir, 'background'); - activeNodePath = path.join(dataDir, 'foregroundActive'); - inactiveNodePath = path.join(dataDir, 'inactiveNode'); - - await fs.promises.writeFile(passwordFile, password); - const logger = new Logger('Agent Test', LogLevel.WARN, [ - new StreamHandler(), - ]); - - activeNode = await PolykeyAgent.createPolykeyAgent({ - password, - nodePath: activeNodePath, - logger, - }); - }); - afterAll(async () => { - await activeNode.stop(); - await activeNode.destroy(); - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - describe('Starting the agent in the foreground', () => { - test( - 'should start the agent and clean up the lockfile when a kill signal is received', - async () => { - const agent = testUtils.pkExec( - [ - 'agent', - 'start', - '-np', - foregroundNodePath, - '--password-file', - passwordFile, - '-vvvv', - ], - {}, - dataDir, - ); - - await poll(global.polykeyStartupTimeout * 3, async () => { - return await agentUtils.checkAgentRunning(foregroundNodePath); - }); - - // Kill externally. - const status = await Status.createStatus({ - nodePath: foregroundNodePath, - fs, - logger, - }); - const lock = await status.parseStatus(); - process.kill(lock.pid); - await agent; // Waiting for agent to finish running. - await poll(global.polykeyStartupTimeout * 2, async () => { - const test = await agentUtils.checkAgentRunning(foregroundNodePath); - return !test; - }); - - // Checking that the lockfile was removed. - // FIXME: this is failing to be removed. seems like the stopping procedure isn't completing properly. - // await poll(global.polykeyStartupTimeout * 2, async () => { - // const files = await fs.promises.readdir(foregroundNodePath); - // const test = files.includes('agent-lock.json'); - // return !test; - // }); - }, - global.polykeyStartupTimeout * 5, - ); - test('should fail to start if an agent is already running at the path', async () => { - const result = await testUtils.pkStdio([ - 'agent', - 'start', - '-np', - activeNodePath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(1); - }); - }); - describe('Starting the agent in the background', () => { - test( - 'should start the agent and clean up the lockfile when a kill signal is received', - async () => { - const commands = [ - 'agent', - 'start', - '-b', - '-np', - backgroundNodePath, - '--password-file', - passwordFile, - ]; - - // We can await this since it should finish after spawning the background agent. - const result = await testUtils.pkStdio(commands); - expect(result.exitCode).toBe(0); - - await poll(global.polykeyStartupTimeout * 3, async () => { - return await agentUtils.checkAgentRunning(backgroundNodePath); - }); - - const status = await Status.createStatus({ - nodePath: backgroundNodePath, - fs, - logger, - }); - const lock = await status.parseStatus(); - process.kill(lock.pid); - await poll(global.polykeyStartupTimeout * 2, async () => { - const test = await agentUtils.checkAgentRunning(backgroundNodePath); - return !test; - }); - - // Checking that the lockfile was removed. - // FIXME: this is failing to be removed. seems like the stopping procedure isn't completing properly. - // await poll(global.polykeyStartupTimeout * 2, async () => { - // const files = await fs.promises.readdir(backgroundNodePath); - // const test = files.includes('agent-lock.json'); - // return !test; - // }); - }, - global.polykeyStartupTimeout * 5, - ); - test('Should fail to start if an agent is already running at the path', async () => { - const commands = [ - 'agent', - 'start', - '-b', - '-np', - activeNodePath, - '--password-file', - passwordFile, - ]; - // We can await this since it should finish after spawning the background agent. - const result = await testUtils.pkStdio(commands); - expect(result.exitCode).toBe(1); - }); - }); - - describe('getting agent status', () => { - test('should get the status of an online agent', async () => { - const result = await testUtils.pkStdio([ - 'agent', - 'status', - '-np', - activeNodePath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('online'); - }); - test('should get the status of an offline agent', async () => { - const result = await testUtils.pkStdio([ - 'agent', - 'status', - '-np', - inactiveNodePath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(1); - expect(result.stdout).toContain('offline'); - }); - // How should we handle the case of a path not being a keynode? - test.todo('should fail to get the status of an non-existent agent'); - }); - describe('Stopping the agent.', () => { - test('should fail to stop if agent not running', async () => { - // Stopping the agent. - const result = await testUtils.pkStdio([ - 'agent', - 'stop', - '-np', - inactiveNodePath, - ]); - expect(result.exitCode).toBe(1); - }); - test( - 'should clean up the lockfile and stop', - async () => { - // Starting session - await testUtils.pkStdio([ - 'agent', - 'unlock', - '-np', - activeNodePath, - '--password-file', - passwordFile, - ]); - - await poll(global.polykeyStartupTimeout * 3, async () => { - return await agentUtils.checkAgentRunning(activeNodePath); - }); - - // Stopping the agent. - const result = await testUtils.pkStdio([ - 'agent', - 'stop', - '-np', - activeNodePath, - ]); - expect(result.exitCode).toBe(0); - await poll(global.polykeyStartupTimeout * 2, async () => { - const test = await agentUtils.checkAgentRunning(backgroundNodePath); - return !test; - }); - - // Checking that the lockfile was removed. - // FIXME: this is failing to be removed. seems like the stopping procedure isn't completing properly. - // await poll(global.polykeyStartupTimeout * 2, async () => { - // const files = await fs.promises.readdir(backgroundNodePath); - // const test = files.includes('agent-lock.json'); - // return !test; - // }) - }, - global.polykeyStartupTimeout * 6, - ); - }); - }); - describe('Agent Sessions', () => { - let dataDir: string; - let passwordFile: string; - let activeAgentPath: string; - let inactiveAgentPath: string; - let activeAgent: PolykeyAgent; - - beforeAll(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'polykey-test-'), - ); - passwordFile = path.join(dataDir, 'passwordFile'); - activeAgentPath = path.join(dataDir, 'ActiveAgent'); - inactiveAgentPath = path.join(dataDir, 'InactiveAgent'); - await fs.promises.writeFile(passwordFile, password); - - activeAgent = await PolykeyAgent.createPolykeyAgent({ - password, - nodePath: activeAgentPath, - logger: logger, - }); - }, global.polykeyStartupTimeout); - afterAll(async () => { - await activeAgent.stop(); - await activeAgent.destroy(); - await fs.promises.rm(dataDir, { - force: true, - recursive: true, - }); - }); - - describe('Sessions should', () => { - afterEach(async () => { - await testUtils.pkStdio(['agent', 'lock', '-np', activeAgentPath]); - }); - - test('fail to unlock session if agent is not running', async () => { - const result = await testUtils.pkStdio([ - 'agent', - 'unlock', - '-np', - inactiveAgentPath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(1); - }); - test('provide the token to the client and store the token', async () => { - const result = await testUtils.pkStdio([ - 'agent', - 'unlock', - '-np', - activeAgentPath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(0); - - const content = await fs.promises.readFile( - path.join(activeAgentPath, 'client', 'token'), - { encoding: 'utf-8' }, - ); - - const verify = await activeAgent.sessionManager.verifyToken( - content as SessionToken, - ); - expect(verify).toBeTruthy(); - }); - test('fail to lock session if agent is not running', async () => { - const result = await testUtils.pkStdio([ - 'agent', - 'lock', - '-np', - inactiveAgentPath, - ]); - expect(result.exitCode).toBe(1); - }); - test('remove the token from the client and delete the token when locking session', async () => { - const result = await testUtils.pkStdio([ - 'agent', - 'lock', - '-np', - activeAgentPath, - ]); - expect(result.exitCode).toBe(0); - - await expect( - fs.promises.readdir(path.join(activeAgentPath, 'client')), - ).resolves.not.toContain('token'); - }); - test('fail to lock all sessions if agent not running', async () => { - const result = await testUtils.pkStdio([ - 'agent', - 'lockall', - '-np', - inactiveAgentPath, - ]); - expect(result.exitCode).toBe(1); - }); - test('cause old sessions to fail when locking all sessions', async () => { - const token = await activeAgent.sessionManager.createToken(); - - await testUtils.pkStdio([ - 'agent', - 'unlock', - '-np', - activeAgentPath, - '--password-file', - passwordFile, - ]); - - const result = await testUtils.pkStdio([ - 'agent', - 'lockall', - '-np', - activeAgentPath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(0); - - await expect( - activeAgent.sessionManager.verifyToken(token), - ).resolves.toBeFalsy(); - }); - }); - describe('Bin commands should retry with password when session is locked.', () => { - let dummyPath: string; - const identitiesCommands = [ - 'identities allow nodeId notify', - 'identities disallow nodeId notify', - 'identities perms nodeId', - 'identities trust nodeId', - 'identities untrust nodeId', - 'identities claim providerId identityId', - 'identities authenticate providerId identityId', - 'identities get nodeId', - 'identities list', - 'identities search providerId', - ]; - const keysCommands = [ - 'keys certchain', - 'keys cert', - 'keys root', - 'keys encrypt -fp filePath', // Fix this, filePath needs to be valid. - 'keys decrypt -fp filePath', - 'keys sign -fp filePath', - 'keys verify -fp filePath -sp sigPath', - 'keys renew -pp passPath', - 'keys reset -pp passPath', - 'keys password -pp passPath', - ]; - const nodesCommands = [ - 'node ping nodeId', - 'node find nodeId', - 'node claim nodeId', - 'node add nodeId 0.0.0.0 55555', - ]; - const notificationCommands = [ - 'notifications clear', - 'notifications read', - 'notifications send nodeId msg1', - ]; - const secretsCommands = [ - 'secrets create -sp vaultName:secretPath -fp filePath', - 'secrets rm -sp vaultName:secretPath', - 'secrets get -sp vaultName:secretPath', - 'secrets ls -vn vaultName', - 'secrets mkdir vaultName:secretPath', - 'secrets rename -sp vaultName:secretPath -sn secretName', - 'secrets update -sp vaultName:secretPath -fp secretPath', - 'secrets dir -vn vaultName -dp directory', - ]; - const vaultCommands = [ - 'vaults list', - 'vaults create -vn vaultName', - 'vaults rename -vn vaultName -nn vaultName', - 'vaults delete -vn vaultName', - 'vaults stat -vn vaultName', - 'vaults share vaultName nodeId', - 'vaults unshare vaultName nodeId', - 'vaults perms vaultName', - 'vaults clone -ni nodeId -vi vaultId', - 'vaults pull -vn vaultName -ni nodeId', - 'vaults scan -ni nodeId', - 'vaults version vaultName nodeId', - 'vaults log vaultName', - ]; - - const commands = [ - ['Identity', identitiesCommands], - ['Key', keysCommands], - ['Node', nodesCommands], - ['Notification', notificationCommands], - ['Secret', secretsCommands], - ['Vault', vaultCommands], - ]; - - const dummyVaultId = 'A'.repeat(44); - const dummyNodeId = makeNodeId( - 'vi3et1hrpv2m2lrplcm7cu913kr45v51cak54vm68anlbvuf83ra0', - ); - function generateCommand(commandString: string) { - const command = commandString - .replace(/filePath/g, dummyPath) - .replace(/sigPath/g, dummyPath) - .replace(/passPath/g, passwordFile) - .replace(/secretPath/g, dummyPath) - .replace(/nodeId/g, dummyNodeId) - .replace(/vaultId/g, dummyVaultId) - .split(' '); - const nodePath = ['-np', activeAgentPath]; - return [...command, ...nodePath]; - } - - describe.each(commands)('%s commands', (name, commands) => { - beforeAll(async () => { - env.PK_PASSWORD = password; - }); - beforeEach(async () => { - await testUtils.pkStdio(['agent', 'lock', '-np', activeAgentPath]); - dummyPath = path.join(dataDir, 'dummy'); - await fs.promises.writeFile(dummyPath, 'dummy'); - }); - afterAll(async () => { - delete env.PK_PASSWORD; - }); - test.each([...commands])('%p', async (commandString) => { - const command = generateCommand(commandString); - const result = await testUtils.pkStdio(command); - expect(result.exitCode).not.toBe(noJWTFailCode); - }); - }); - }); - }); -}); diff --git a/tests/bin/agent/agent.test.ts b/tests/bin/agent/agent.test.ts new file mode 100644 index 000000000..6b455f520 --- /dev/null +++ b/tests/bin/agent/agent.test.ts @@ -0,0 +1,633 @@ +// TODO: refactor into command-specific tests + +// import type { SessionToken } from '@/sessions/types'; +// import os from 'os'; +// import path from 'path'; +// import fs from 'fs'; +// import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +// import PolykeyAgent from '@/PolykeyAgent'; +// import { Status } from '@/status'; +// import { makeNodeId } from '@/nodes/utils'; +// import { sleep } from '@/utils'; +// import * as testUtils from './utils'; + +// jest.mock('@/keys/utils', () => ({ +// ...jest.requireActual('@/keys/utils'), +// generateDeterministicKeyPair: +// jest.requireActual('@/keys/utils').generateKeyPair, +// })); + +// describe('CLI agent', () => { +// const noJWTFailCode = 77; +// const password = 'password'; +// const logger = new Logger('AgentServerTest', LogLevel.WARN, [ +// new StreamHandler(), +// ]); +// const waitForTimeout = global.polykeyStartupTimeout * 2; + +// async function killAgent(nodePath: string, passwordFile: string) { +// await testUtils.pkStdio( +// ['agent', 'stop', '-np', nodePath, '--password-file', passwordFile], +// {}, +// '.', +// ); +// } + +// const statusPath = (nodePath: string): string => +// path.join(nodePath, 'status.json'); + +// describe('Agent start, status and stop', () => { +// let dataDir: string; +// let foregroundNodePath: string; +// let backgroundNodePath: string; + +// let inactiveNodePath: string; +// let activeNodePath: string; +// let activeNode: PolykeyAgent; +// let passwordFile: string; + +// beforeAll(async () => { +// dataDir = await fs.promises.mkdtemp( +// path.join(os.tmpdir(), 'polykey-test-'), +// ); +// passwordFile = path.join(dataDir, 'passwordFile'); +// foregroundNodePath = path.join(dataDir, 'foreground'); +// backgroundNodePath = path.join(dataDir, 'background'); +// activeNodePath = path.join(dataDir, 'foregroundActive'); +// inactiveNodePath = path.join(dataDir, 'inactiveNode'); + +// await fs.promises.writeFile(passwordFile, password); +// const logger = new Logger('Agent Test', LogLevel.WARN, [ +// new StreamHandler(), +// ]); + +// activeNode = await PolykeyAgent.createPolykeyAgent({ +// password, +// nodePath: activeNodePath, +// logger, +// }); +// }, global.defaultTimeout * 2); +// afterAll(async () => { +// await activeNode.stop(); +// await activeNode.destroy(); +// await fs.promises.rm(dataDir, { +// force: true, +// recursive: true, +// }); +// }); +// describe('Starting the agent in the foreground', () => { +// test( +// 'should start the agent and clean up the lockfile when a kill signal is received', +// async () => { +// const agent = testUtils.pkExec([ +// 'agent', +// 'start', +// '-np', +// foregroundNodePath, +// '--password-file', +// passwordFile, +// ]); +// const status = new Status({ +// statusPath: statusPath(foregroundNodePath), +// fs, +// logger, +// }); +// await status.waitFor('LIVE', waitForTimeout); + +// // Kill. +// await killAgent(foregroundNodePath, passwordFile); + +// const agentResult = await agent; // Waiting for agent to finish running. +// await status.waitFor('DEAD', waitForTimeout); + +// expect(agentResult.stdout.split(' ')).toHaveLength(24); +// }, +// global.polykeyStartupTimeout * 4, +// ); +// test( +// 'should start with port and host information as flags', +// async () => { +// const agent = testUtils.pkStdio([ +// 'agent', +// 'start', +// '-np', +// foregroundNodePath, +// '--password-file', +// passwordFile, +// '-ch', +// '127.0.0.1', +// '-cp', +// '55556', +// '-ih', +// '127.0.0.2', +// '127.0.0.3', +// '-ip', +// '55555', +// ]); +// const status = new Status({ +// statusPath: statusPath(foregroundNodePath), +// fs, +// logger, +// }); +// const statusConfig = await status.waitFor('LIVE', waitForTimeout); +// expect(statusConfig.data.clientHost).toEqual('127.0.0.1'); +// expect(statusConfig.data.clientPort).toEqual(55556); + +// // Kill externally. +// await killAgent(foregroundNodePath, passwordFile); +// await agent; // Waiting for agent to finish running. +// await status.waitFor('DEAD', waitForTimeout); +// }, +// global.polykeyStartupTimeout * 4, +// ); +// test( +// 'should start with port and host information from env', +// async () => { +// const agent = testUtils.pkExec( +// [ +// 'agent', +// 'start', +// '-np', +// foregroundNodePath, +// '--password-file', +// passwordFile, +// ], +// { +// PK_CLIENT_HOST: '127.0.0.1', +// PK_CLIENT_PORT: '55556', +// PK_INGRESS_HOST: '127.0.0.2', +// PK_INGRESS_PORT: '55555', +// }, +// ); + +// const status = new Status({ +// statusPath: statusPath(foregroundNodePath), +// fs, +// logger, +// }); +// const statusConfig = await status.waitFor('LIVE', waitForTimeout); +// expect(statusConfig.data.clientHost).toEqual('127.0.0.1'); +// expect(statusConfig.data.clientPort).toEqual(55556); + +// // Kill externally. +// await killAgent(foregroundNodePath, passwordFile); +// await agent; // Waiting for agent to finish running. +// await status.waitFor('DEAD', waitForTimeout); +// }, +// global.polykeyStartupTimeout * 4, +// ); +// test('should fail to start if an agent is already running at the path', async () => { +// const result = await testUtils.pkStdio([ +// 'agent', +// 'start', +// '-np', +// activeNodePath, +// '--password-file', +// passwordFile, +// ]); +// expect(result.exitCode).toBe(75); +// }); +// }); +// describe('Starting the agent in the background', () => { +// test( +// 'should start the agent and clean up the lockfile when a kill signal is received', +// async () => { +// const commands = [ +// 'agent', +// 'start', +// '-b', +// '-np', +// backgroundNodePath, +// '--password-file', +// passwordFile, +// ]; + +// // We can await this since it should finish after spawning the background agent. +// const result = await testUtils.pkStdio(commands); +// expect(result.exitCode).toBe(0); + +// const status = new Status({ +// statusPath: statusPath(backgroundNodePath), +// fs, +// logger, +// }); +// await status.waitFor('LIVE', waitForTimeout); + +// // Kill it (with fire) externally. +// await killAgent(backgroundNodePath, passwordFile); +// await sleep(100); +// await status.waitFor('DEAD', waitForTimeout); + +// expect(result.stdout).toContain('This is your recovery code'); +// const code = result.stdout.split('\n')[3]; // Getting the recovery code line. +// expect(code.split(' ')).toHaveLength(24); + +// // Checking that the status was removed. FIXME +// // await poll(global.polykeyStartupTimeout * 2, async () => { +// // const files = await fs.promises.readdir(backgroundNodePath); +// // const test = files.includes('agent-status.json'); +// // return !test; +// // }); +// }, +// global.polykeyStartupTimeout * 5, +// ); +// test('Should fail to start if an agent is already running at the path', async () => { +// const commands = [ +// 'agent', +// 'start', +// '-b', +// '-np', +// activeNodePath, +// '--password-file', +// passwordFile, +// ]; +// // We can await this since it should finish after spawning the background agent. +// const result = await testUtils.pkStdio(commands); +// expect(result.exitCode).toBe(75); +// }); +// }); +// test( +// 'concurrent agent start', +// async () => { +// const nodePath = path.join(dataDir, 'third'); +// const commands = [ +// 'agent', +// 'start', +// '-np', +// nodePath, +// '--password-file', +// passwordFile, +// ]; + +// // We can await this since it should finish after spawning the background agent. +// const prom1 = testUtils.pkExec(commands); +// const prom2 = testUtils.pkExec(commands); + +// const status = new Status({ +// statusPath: statusPath(nodePath), +// fs, +// logger, +// }); +// await status.waitFor('LIVE', waitForTimeout); + +// // Kill externally. +// const results = await Promise.all([prom1, prom2]); // Waiting for agent to finish running. +// const resultsString = JSON.stringify(results); +// expect(resultsString).toContain(':75'); +// expect(resultsString).toContain(':0'); +// }, +// global.defaultTimeout * 4, +// ); +// test( +// 'concurrent agent start and bootstrap.', +// async () => { +// const nodePath = path.join(dataDir, 'third'); +// const command1 = [ +// 'agent', +// 'start', +// '-np', +// nodePath, +// '--password-file', +// passwordFile, +// ]; +// const command2 = [ +// 'bootstrap', +// '-np', +// nodePath, +// '--password-file', +// passwordFile, +// ]; + +// // We can await this since it should finish after spawning the background agent. +// const prom1 = testUtils.pkExec(command1); +// const prom2 = testUtils.pkExec(command2); + +// const status = new Status({ +// statusPath: statusPath(nodePath), +// fs, +// logger, +// }); +// try { +// await status.waitFor('LIVE', 20000); +// } catch (e) { +// // Noop +// } +// // Kill externally. +// await killAgent(nodePath, passwordFile); + +// const results = await Promise.all([prom1, prom2]); // Waiting for agent to finish running. +// expect(results[0].exitCode).toBe(0); +// expect(results[1].exitCode).toBe(64); +// }, +// global.defaultTimeout * 5, +// ); + +// describe('getting agent status', () => { +// test('should get the status of an online agent', async () => { +// const result = await testUtils.pkStdio([ +// 'agent', +// 'status', +// '-np', +// activeNodePath, +// '--password-file', +// passwordFile, +// ]); +// expect(result.exitCode).toBe(0); +// expect(result.stdout).toContain('LIVE'); +// }); +// test( +// 'should get the status of an offline agent', +// async () => { +// const result = await testUtils.pkStdio([ +// 'agent', +// 'status', +// '-np', +// inactiveNodePath, +// '--password-file', +// passwordFile, +// ]); +// expect(result.exitCode).toBe(0); +// expect(result.stdout).toContain('DEAD'); +// }, +// global.failedConnectionTimeout, +// ); +// // How should we handle the case of a path not being a keynode? +// test.todo('should fail to get the status of an non-existent agent'); +// }); +// describe('Stopping the agent.', () => { +// test('should fail to stop if agent not running', async () => { +// // Stopping the agent. +// const result = await testUtils.pkStdio([ +// 'agent', +// 'stop', +// '-np', +// inactiveNodePath, +// ]); +// expect(result.exitCode).toBe(64); +// }); +// test( +// 'should clean up the status and stop', +// async () => { +// // Starting session +// await testUtils.pkStdio([ +// 'agent', +// 'unlock', +// '-np', +// activeNodePath, +// '--password-file', +// passwordFile, +// ]); + +// const status = new Status({ +// statusPath: statusPath(activeNodePath), +// fs, +// logger, +// }); +// await status.waitFor('LIVE', waitForTimeout); + +// // Stopping the agent. +// const result = await testUtils.pkStdio([ +// 'agent', +// 'stop', +// '-np', +// activeNodePath, +// ]); +// expect(result.exitCode).toBe(0); +// await sleep(100); +// await status.waitFor('DEAD', waitForTimeout); + +// // Checking that the lockfile was removed. +// // FIXME: this is failing to be removed. seems like the stopping procedure isn't completing properly. +// // await poll(global.polykeyStartupTimeout * 2, async () => { +// // const files = await fs.promises.readdir(backgroundNodePath); +// // const test = files.includes('agent-lock.json'); +// // return !test; +// // }) +// }, +// global.polykeyStartupTimeout * 6, +// ); +// }); +// }); +// describe('Agent Sessions', () => { +// let dataDir: string; +// let passwordFile: string; +// let activeAgentPath: string; +// let inactiveAgentPath: string; +// let activeAgent: PolykeyAgent; + +// beforeAll(async () => { +// dataDir = await fs.promises.mkdtemp( +// path.join(os.tmpdir(), 'polykey-test-'), +// ); +// passwordFile = path.join(dataDir, 'passwordFile'); +// activeAgentPath = path.join(dataDir, 'ActiveAgent'); +// inactiveAgentPath = path.join(dataDir, 'InactiveAgent'); +// await fs.promises.writeFile(passwordFile, password); + +// activeAgent = await PolykeyAgent.createPolykeyAgent({ +// password, +// nodePath: activeAgentPath, +// logger: logger, +// }); +// }, global.polykeyStartupTimeout); +// afterAll(async () => { +// await activeAgent.stop(); +// await activeAgent.destroy(); +// await fs.promises.rm(dataDir, { +// force: true, +// recursive: true, +// }); +// }); + +// describe('Sessions should', () => { +// afterEach(async () => { +// await testUtils.pkStdio(['agent', 'lock', '-np', activeAgentPath]); +// }); + +// test('fail to unlock session if agent is not running', async () => { +// const result = await testUtils.pkStdio([ +// 'agent', +// 'unlock', +// '-np', +// inactiveAgentPath, +// '--password-file', +// passwordFile, +// ]); +// expect(result.exitCode).toBe(64); +// }); +// test('provide the token to the client and store the token', async () => { +// const result = await testUtils.pkStdio([ +// 'agent', +// 'unlock', +// '-np', +// activeAgentPath, +// '--password-file', +// passwordFile, +// ]); +// expect(result.exitCode).toBe(0); + +// const content = await fs.promises.readFile( +// path.join(activeAgentPath, 'token'), +// { encoding: 'utf-8' }, +// ); + +// const verify = await activeAgent.sessionManager.verifyToken( +// content as SessionToken, +// ); +// expect(verify).toBeTruthy(); +// }); +// test('remove the token from the client and delete the token when locking session', async () => { +// const result = await testUtils.pkStdio([ +// 'agent', +// 'lock', +// '-np', +// activeAgentPath, +// ]); +// expect(result.exitCode).toBe(0); + +// await expect( +// fs.promises.readdir(path.join(activeAgentPath)), +// ).resolves.not.toContain('token'); +// }); +// test('fail to lock all sessions if agent not running', async () => { +// const result = await testUtils.pkStdio([ +// 'agent', +// 'lockall', +// '-np', +// inactiveAgentPath, +// ]); +// expect(result.exitCode).toBe(64); +// }); +// test('cause old sessions to fail when locking all sessions', async () => { +// const token = await activeAgent.sessionManager.createToken(); + +// await testUtils.pkStdio([ +// 'agent', +// 'unlock', +// '-np', +// activeAgentPath, +// '--password-file', +// passwordFile, +// ]); + +// const result = await testUtils.pkStdio([ +// 'agent', +// 'lockall', +// '-np', +// activeAgentPath, +// '--password-file', +// passwordFile, +// ]); +// expect(result.exitCode).toBe(0); + +// await expect( +// activeAgent.sessionManager.verifyToken(token), +// ).resolves.toBeFalsy(); +// }); +// }); +// describe('Bin commands should retry with password when session is locked.', () => { +// let dummyPath: string; +// const identitiesCommands = [ +// 'identities allow nodeId notify', +// 'identities disallow nodeId notify', +// 'identities perms nodeId', +// 'identities trust nodeId', +// 'identities untrust nodeId', +// 'identities claim providerId identityId', +// 'identities authenticate providerId identityId', +// 'identities get nodeId', +// 'identities list', +// 'identities search providerId', +// ]; +// const keysCommands = [ +// 'keys certchain', +// 'keys cert', +// 'keys root', +// 'keys encrypt -fp filePath', // Fix this, filePath needs to be valid. +// 'keys decrypt -fp filePath', +// 'keys sign -fp filePath', +// 'keys verify -fp filePath -sp sigPath', +// 'keys renew -pp passPath', +// 'keys reset -pp passPath', +// 'keys password -pp passPath', +// ]; +// const nodesCommands = [ +// 'node ping nodeId', +// 'node find nodeId', +// 'node claim nodeId', +// 'node add nodeId 0.0.0.0 55555', +// ]; +// const notificationCommands = [ +// 'notifications clear', +// 'notifications read', +// 'notifications send nodeId msg1', +// ]; +// const secretsCommands = [ +// 'secrets create -sp vaultName:secretPath -fp filePath', +// 'secrets rm -sp vaultName:secretPath', +// 'secrets get -sp vaultName:secretPath', +// 'secrets ls -vn vaultName', +// 'secrets mkdir vaultName:secretPath', +// 'secrets rename -sp vaultName:secretPath -sn secretName', +// 'secrets update -sp vaultName:secretPath -fp secretPath', +// 'secrets dir -vn vaultName -dp directory', +// ]; +// const vaultCommands = [ +// 'vaults list', +// 'vaults create -vn vaultName', +// 'vaults rename -vn vaultName -nn vaultName', +// 'vaults delete -vn vaultName', +// 'vaults stat -vn vaultName', +// 'vaults share vaultName nodeId', +// 'vaults unshare vaultName nodeId', +// 'vaults perms vaultName', +// 'vaults clone -ni nodeId -vi vaultId', +// 'vaults pull -vn vaultName -ni nodeId', +// 'vaults scan -ni nodeId', +// 'vaults version vaultName nodeId', +// 'vaults log vaultName', +// ]; + +// const commands = [ +// ['Identity', identitiesCommands], +// ['Key', keysCommands], +// ['Node', nodesCommands], +// ['Notification', notificationCommands], +// ['Secret', secretsCommands], +// ['Vault', vaultCommands], +// ]; + +// const dummyVaultId = 'A'.repeat(44); +// const dummyNodeId = makeNodeId( +// 'vi3et1hrpv2m2lrplcm7cu913kr45v51cak54vm68anlbvuf83ra0', +// ); +// function generateCommand(commandString: string) { +// const command = commandString +// .replace(/filePath/g, dummyPath) +// .replace(/sigPath/g, dummyPath) +// .replace(/passPath/g, passwordFile) +// .replace(/secretPath/g, dummyPath) +// .replace(/nodeId/g, dummyNodeId) +// .replace(/vaultId/g, dummyVaultId) +// .split(' '); +// const nodePath = ['-np', activeAgentPath]; +// return [...command, ...nodePath]; +// } + +// describe.each(commands)('%s commands', (name, commands) => { +// beforeEach(async () => { +// await testUtils.pkStdio(['agent', 'lock', '-np', activeAgentPath]); +// dummyPath = path.join(dataDir, 'dummy'); +// await fs.promises.writeFile(dummyPath, 'dummy'); +// }); +// test.each([...commands])('%p', async (commandString) => { +// const command = generateCommand(commandString); +// const result = await testUtils.pkStdio(command, { +// PK_PASSWORD: password, +// }); +// expect(result.exitCode).not.toBe(noJWTFailCode); +// }); +// }); +// }); +// }); +// }); diff --git a/tests/bin/agent/start.test.ts b/tests/bin/agent/start.test.ts new file mode 100644 index 000000000..8d4e61566 --- /dev/null +++ b/tests/bin/agent/start.test.ts @@ -0,0 +1,68 @@ +import type { RecoveryCode } from '@/keys/types'; +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import readline from 'readline'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import * as testUtils from '../utils'; + +describe('start', () => { + const logger = new Logger('start test', LogLevel.WARN, [new StreamHandler()]); + let dataDir: string; + beforeEach(async () => { + dataDir = await fs.promises.mkdtemp( + path.join(os.tmpdir(), 'polykey-test-'), + ); + }); + afterEach(async () => { + await fs.promises.rm(dataDir, { + force: true, + recursive: true, + }); + }); + test( + 'start in foreground with parameters', + async () => { + const password = 'abc123'; + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, password); + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'polykey'), + '--password-file', + passwordPath, + '--verbose', + ], + undefined, + dataDir, + logger, + ); + const rlOut = readline.createInterface(agentProcess.stdout!); + const recoveryCode = await new Promise( + (resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }, + ); + expect(typeof recoveryCode).toBe('string'); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + agentProcess.kill('SIGTERM'); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + }, + global.defaultTimeout * 4, + ); +}); diff --git a/tests/bin/bootstrap.test.ts b/tests/bin/bootstrap.test.ts index f0f41503f..976039fa7 100644 --- a/tests/bin/bootstrap.test.ts +++ b/tests/bin/bootstrap.test.ts @@ -1,12 +1,9 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; - -import { checkKeynodeState } from '@/bootstrap'; - import * as utils from './utils'; -describe.skip('CLI bootstrap', () => { +describe('CLI bootstrap', () => { let dataDir: string; let passwordFile: string; let nodePath: string; @@ -26,61 +23,119 @@ describe.skip('CLI bootstrap', () => { }); }); - test("Should create keynode state if directory doesn't exist.", async () => { - const result = await utils.pkStdio( - ['bootstrap', '-np', nodePath, '--password-file', passwordFile], - {}, - dataDir, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('Polykey bootstrapped at Node Path:'); - expect(result.stdout).toContain(nodePath); - expect(await checkKeynodeState(nodePath)).toBe('KEYNODE_EXISTS'); - }); - test('Should create keynode state if directory is empty.', async () => { - await fs.promises.mkdir(nodePath); - const result = await utils.pkStdio( - ['bootstrap', '-np', nodePath, '--password-file', passwordFile], - {}, - dataDir, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('Polykey bootstrapped at Node Path:'); - expect(result.stdout).toContain(nodePath); - expect(await checkKeynodeState(nodePath)).toBe('KEYNODE_EXISTS'); - }); + test( + "Should create keynode state if directory doesn't exist.", + async () => { + const result = await utils.pkStdio([ + 'bootstrap', + '-np', + nodePath, + '--password-file', + passwordFile, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout.split(' ')).toHaveLength(24); + }, + global.polykeyStartupTimeout * 2, + ); + test( + 'Should create keynode state if directory is empty.', + async () => { + await fs.promises.mkdir(nodePath); + const result = await utils.pkStdio([ + 'bootstrap', + '-np', + nodePath, + '--password-file', + passwordFile, + '-vvvv', + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout.split(' ')).toHaveLength(24); + }, + global.polykeyStartupTimeout * 2, + ); + test( + 'Should generate a recovery code when creating state.', + async () => { + await fs.promises.mkdir(nodePath); + const result = await utils.pkStdio([ + 'bootstrap', + '-np', + nodePath, + '--password-file', + passwordFile, + ]); + expect(result.exitCode).toBe(0); + expect(result.stdout.split(' ')).toHaveLength(24); + }, + global.polykeyStartupTimeout * 3, + ); test('Should fail to create keynode state if keynode exists.', async () => { - const result = await utils.pkStdio( - ['bootstrap', '-np', nodePath, '--password-file', passwordFile], - {}, - dataDir, - ); + const result = await utils.pkStdio([ + 'bootstrap', + '-np', + nodePath, + '--password-file', + passwordFile, + ]); expect(result.exitCode).toBe(0); - expect(await checkKeynodeState(nodePath)).toBe('KEYNODE_EXISTS'); // Should fail here. - const result2 = await utils.pkStdio( - ['bootstrap', '-np', nodePath, '--password-file', passwordFile], - {}, - dataDir, - ); + const result2 = await utils.pkStdio([ + 'bootstrap', + '-np', + nodePath, + '--password-file', + passwordFile, + ]); expect(result2.exitCode).not.toBe(0); - expect(result2.stdout).toContain('Error:'); - expect(result2.stdout).toContain('Files already exist at node path'); - expect(await checkKeynodeState(nodePath)).toBe('KEYNODE_EXISTS'); + expect(result2.stderr).toContain('ErrorBootstrapExistingState:'); + expect(result2.stderr).toContain('Node path is occupied'); }); test('Should fail to create keynode state if other files exists.', async () => { await fs.promises.mkdir(path.join(nodePath, 'NOTAKEYNODEDIR'), { recursive: true, }); - const result = await utils.pkStdio( - ['bootstrap', '-np', nodePath, '--password-file', passwordFile], - {}, - dataDir, - ); + const result = await utils.pkStdio([ + 'bootstrap', + '-np', + nodePath, + '--password-file', + passwordFile, + ]); expect(result.exitCode).not.toBe(0); - expect(result.stdout).toContain('Error:'); - expect(result.stdout).toContain('Files already exist at node path'); - expect(await checkKeynodeState(nodePath)).toBe('OTHER_EXISTS'); + expect(result.stderr).toContain('ErrorBootstrapExistingState:'); + expect(result.stderr).toContain('Node path is occupied'); }); + test( + 'concurrent bootstrapping', + async () => { + await fs.promises.mkdir(nodePath); + + // Bootstrapping two nodes at the same time. + const results = await Promise.all([ + utils.pkStdio([ + 'bootstrap', + '-np', + nodePath, + '--password-file', + passwordFile, + ]), + utils.pkStdio([ + 'bootstrap', + '-np', + nodePath, + '--password-file', + passwordFile, + ]), + ]); + + // 1 fails and 1 succeeds. + + expect(JSON.stringify(results)).toContain(':0'); + expect(JSON.stringify(results)).toContain(':75'); + }, + global.defaultTimeout * 4, + ); }); diff --git a/tests/bin/identities.test.ts b/tests/bin/identities.test.ts index 1a7b31d87..3a46523fe 100644 --- a/tests/bin/identities.test.ts +++ b/tests/bin/identities.test.ts @@ -20,6 +20,12 @@ import { } from '../utils'; import TestProvider from '../identities/TestProvider'; +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + function identityString( providerId: ProviderId, identityId: IdentityId, @@ -137,7 +143,7 @@ describe('CLI Identities', () => { {}, dataDir, ); - }, global.polykeyStartupTimeout); + }, global.polykeyStartupTimeout * 2); afterAll(async () => { await polykeyAgent.stop(); await polykeyAgent.destroy(); diff --git a/tests/bin/keys.test.ts b/tests/bin/keys.test.ts index ff245b9f4..0e79fe64b 100644 --- a/tests/bin/keys.test.ts +++ b/tests/bin/keys.test.ts @@ -2,10 +2,15 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - -import { PolykeyAgent } from '@'; +import PolykeyAgent from '@/PolykeyAgent'; import * as utils from './utils'; +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. @@ -140,56 +145,64 @@ describe('CLI keys', () => { }); describe('commandRenewKeypair', () => { - test('should renew the keypair', async () => { - // Starting new node. + test( + 'should renew the keypair', + async () => { + // Starting new node. - const rootKeypairOld = polykeyAgent.keyManager.getRootKeyPair(); - const passPath = path.join(dataDir, 'passwordNew'); - await fs.promises.writeFile(passPath, 'password-new'); + const rootKeypairOld = polykeyAgent.keyManager.getRootKeyPair(); + const passPath = path.join(dataDir, 'passwordNew'); + await fs.promises.writeFile(passPath, 'password-new'); - command = ['keys', 'renew', '-np', nodePath, passPath]; + command = ['keys', 'renew', '-np', nodePath, passPath]; - const result = await utils.pkStdio([...command], {}, dataDir); - expect(result.exitCode).toBe(0); + const result = await utils.pkStdio([...command], {}, dataDir); + expect(result.exitCode).toBe(0); - const rootKeypairNew = polykeyAgent.keyManager.getRootKeyPair(); - expect(rootKeypairNew.privateKey).not.toBe(rootKeypairOld.privateKey); - expect(rootKeypairNew.publicKey).not.toBe(rootKeypairOld.publicKey); + const rootKeypairNew = polykeyAgent.keyManager.getRootKeyPair(); + expect(rootKeypairNew.privateKey).not.toBe(rootKeypairOld.privateKey); + expect(rootKeypairNew.publicKey).not.toBe(rootKeypairOld.publicKey); - await polykeyAgent.stop(); + await polykeyAgent.stop(); - polykeyAgent = await PolykeyAgent.createPolykeyAgent({ - password: 'password-new', - nodePath: nodePath, - logger: logger, - }); - await polykeyAgent.keyManager.changeRootKeyPassword(password); - }); + polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password: 'password-new', + nodePath: nodePath, + logger: logger, + }); + await polykeyAgent.keyManager.changePassword(password); + }, + global.polykeyStartupTimeout * 4, + ); }); describe('commandResetKeyPair', () => { - test('should reset the keypair', async () => { - const rootKeypairOld = polykeyAgent.keyManager.getRootKeyPair(); - const passPath = path.join(dataDir, 'passwordNewNew'); - await fs.promises.writeFile(passPath, 'password-new-new'); + test( + 'should reset the keypair', + async () => { + const rootKeypairOld = polykeyAgent.keyManager.getRootKeyPair(); + const passPath = path.join(dataDir, 'passwordNewNew'); + await fs.promises.writeFile(passPath, 'password-new-new'); - command = ['keys', 'reset', '-np', nodePath, passPath]; + command = ['keys', 'reset', '-np', nodePath, passPath]; - const result = await utils.pkStdio([...command], {}, dataDir); - expect(result.exitCode).toBe(0); + const result = await utils.pkStdio([...command], {}, dataDir); + expect(result.exitCode).toBe(0); - const rootKeypairNew = polykeyAgent.keyManager.getRootKeyPair(); - expect(rootKeypairNew.privateKey).not.toBe(rootKeypairOld.privateKey); - expect(rootKeypairNew.publicKey).not.toBe(rootKeypairOld.publicKey); + const rootKeypairNew = polykeyAgent.keyManager.getRootKeyPair(); + expect(rootKeypairNew.privateKey).not.toBe(rootKeypairOld.privateKey); + expect(rootKeypairNew.publicKey).not.toBe(rootKeypairOld.publicKey); - await polykeyAgent.stop(); + await polykeyAgent.stop(); - polykeyAgent = await PolykeyAgent.createPolykeyAgent({ - password: 'password-new-new', - nodePath: nodePath, - logger: logger, - }); - await polykeyAgent.keyManager.changeRootKeyPassword(password); - }); + polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password: 'password-new-new', + nodePath: nodePath, + logger: logger, + }); + await polykeyAgent.keyManager.changePassword(password); + }, + global.polykeyStartupTimeout * 4, + ); }); describe('commandChangePassword', () => { test( @@ -210,9 +223,9 @@ describe('CLI keys', () => { nodePath: nodePath, logger: logger, }); - await polykeyAgent.keyManager.changeRootKeyPassword(password); + await polykeyAgent.keyManager.changePassword(password); }, - global.defaultTimeout * 2, + global.polykeyStartupTimeout * 4, ); }); }); diff --git a/tests/bin/nodes.test.ts b/tests/bin/nodes.test.ts index 8be14451a..3e969f458 100644 --- a/tests/bin/nodes.test.ts +++ b/tests/bin/nodes.test.ts @@ -4,11 +4,17 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { PolykeyAgent } from '@'; +import PolykeyAgent from '@/PolykeyAgent'; import { makeNodeId } from '@/nodes/utils'; import * as testUtils from './utils'; import * as testKeynodeUtils from '../utils'; +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. @@ -144,13 +150,17 @@ describe('CLI Nodes', () => { }); await remoteOnline.nodeManager.clearDB(); }); - test('Should send a gestalt invite', async () => { - const commands = genCommands(['claim', remoteOnlineNodeId]); - const result = await testUtils.pkStdio(commands, {}, dataDir); - expect(result.exitCode).toBe(0); // Succeeds. - expect(result.stdout).toContain('Gestalt Invite'); - expect(result.stdout).toContain(remoteOnlineNodeId); - }); + test( + 'Should send a gestalt invite', + async () => { + const commands = genCommands(['claim', remoteOnlineNodeId]); + const result = await testUtils.pkStdio(commands); + expect(result.exitCode).toBe(0); // Succeeds. + expect(result.stdout).toContain('Gestalt Invite'); + expect(result.stdout).toContain(remoteOnlineNodeId); + }, + global.polykeyStartupTimeout * 4, + ); test('Should send a gestalt invite (force invite)', async () => { await remoteOnline.notificationsManager.sendNotification(keynodeId, { type: 'GestaltInvite', diff --git a/tests/bin/notifications.test.ts b/tests/bin/notifications.test.ts index 6d032f33c..357f92e00 100644 --- a/tests/bin/notifications.test.ts +++ b/tests/bin/notifications.test.ts @@ -6,12 +6,16 @@ import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { utils as idUtils } from '@matrixai/id'; - -import { PolykeyAgent } from '@'; +import PolykeyAgent from '@/PolykeyAgent'; import { makeVaultId } from '@/vaults/utils'; import * as utils from './utils'; import * as testUtils from './utils'; -// Import { makeVaultId } from "@/vaults/utils"; + +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. @@ -35,7 +39,8 @@ describe('CLI Notifications', () => { let senderNodePath: string, receiverNodePath: string; let senderPasswordFile: string, receiverPasswordFile: string; let senderPolykeyAgent: PolykeyAgent, receiverPolykeyAgent: PolykeyAgent; - let senderNodeId: NodeId, receiverNodeId: NodeId; + let senderNodeId: NodeId; + let receiverNodeId: NodeId; // Helper functions function genCommandsSender(options: Array) { diff --git a/tests/bin/secret.test.ts b/tests/bin/secret.test.ts index 94db5dd1e..a9092d94c 100644 --- a/tests/bin/secret.test.ts +++ b/tests/bin/secret.test.ts @@ -3,10 +3,16 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { PolykeyAgent } from '@'; +import PolykeyAgent from '@/PolykeyAgent'; import { vaultOps } from '@/vaults'; import * as utils from './utils'; +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. @@ -56,30 +62,34 @@ describe('CLI secrets', () => { }); describe('commandCreateSecret', () => { - test('should create secrets', async () => { - const vaultName = 'Vault1' as VaultName; - const vault = await polykeyAgent.vaultManager.createVault(vaultName); - const secretPath = path.join(dataDir, 'secret'); - await fs.promises.writeFile(secretPath, 'this is a secret'); - - command = [ - 'secrets', - 'create', - '-np', - dataDir, - secretPath, - `${vaultName}:MySecret`, - ]; - - const result = await utils.pkStdio([...command], {}, dataDir); - expect(result.exitCode).toBe(0); - - const list = await vaultOps.listSecrets(vault); - expect(list.sort()).toStrictEqual(['MySecret']); - expect( - (await vaultOps.getSecret(vault, 'MySecret')).toString(), - ).toStrictEqual('this is a secret'); - }); + test( + 'should create secrets', + async () => { + const vaultName = 'Vault1' as VaultName; + const vault = await polykeyAgent.vaultManager.createVault(vaultName); + const secretPath = path.join(dataDir, 'secret'); + await fs.promises.writeFile(secretPath, 'this is a secret'); + + command = [ + 'secrets', + 'create', + '-np', + dataDir, + secretPath, + `${vaultName}:MySecret`, + ]; + + const result = await utils.pkStdio([...command], {}, dataDir); + expect(result.exitCode).toBe(0); + + const list = await vaultOps.listSecrets(vault); + expect(list.sort()).toStrictEqual(['MySecret']); + expect( + (await vaultOps.getSecret(vault, 'MySecret')).toString(), + ).toStrictEqual('this is a secret'); + }, + global.defaultTimeout * 2, + ); }); describe('commandDeleteSecret', () => { test('should delete secrets', async () => { diff --git a/tests/bin/sessions.test.ts b/tests/bin/sessions.test.ts index 0a9b2ce7d..96741f898 100644 --- a/tests/bin/sessions.test.ts +++ b/tests/bin/sessions.test.ts @@ -5,10 +5,16 @@ import path from 'path'; import fs from 'fs'; import lock from 'fd-lock'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { PolykeyAgent } from '@'; +import PolykeyAgent from '@/PolykeyAgent'; import { sleep } from '@/utils'; import * as testUtils from './utils'; +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. @@ -46,7 +52,7 @@ describe('Session Token Refreshing', () => { nodePath = path.join(dataDir, 'keynode'); command = ['vaults', 'list', '-np', nodePath]; passwordFile = path.join(dataDir, 'passwordFile'); - sessionFile = path.join(nodePath, 'client', 'token'); + sessionFile = path.join(nodePath, 'token'); await fs.promises.writeFile(passwordFile, 'password'); polykeyAgent = await PolykeyAgent.createPolykeyAgent({ password: 'password', diff --git a/tests/bin/utils.retryAuth.test.ts b/tests/bin/utils.retryAuth.test.ts index c2c0b1608..c313e1292 100644 --- a/tests/bin/utils.retryAuth.test.ts +++ b/tests/bin/utils.retryAuth.test.ts @@ -10,17 +10,19 @@ import * as binUtils from '@/bin/utils'; jest.mock('prompts'); const mockedPrompts = mocked(prompts); -describe('utils retryAuth', () => { +describe('utils retryAuthentication', () => { test('no retry on success', async () => { const mockCallSuccess = jest.fn().mockResolvedValue('hello world'); - const result = await binUtils.retryAuth(mockCallSuccess); + const result = await binUtils.retryAuthentication(mockCallSuccess); expect(mockCallSuccess.mock.calls.length).toBe(1); expect(result).toBe('hello world'); }); test('no retry on generic error', async () => { const error = new Error('oh no'); const mockCallFail = jest.fn().mockRejectedValue(error); - await expect(binUtils.retryAuth(mockCallFail)).rejects.toThrow(/oh no/); + await expect(binUtils.retryAuthentication(mockCallFail)).rejects.toThrow( + /oh no/, + ); expect(mockCallFail.mock.calls.length).toBe(1); }); test('no retry on unattended call with PK_TOKEN and PK_PASSWORD', async () => { @@ -31,7 +33,7 @@ describe('utils retryAuth', () => { PK_TOKEN: 'hello', PK_PASSWORD: 'world', }); - await expect(binUtils.retryAuth(mockCallFail)).rejects.toThrow( + await expect(binUtils.retryAuthentication(mockCallFail)).rejects.toThrow( clientErrors.ErrorClientAuthMissing, ); envRestore(); @@ -45,7 +47,7 @@ describe('utils retryAuth', () => { PK_TOKEN: 'hello', PK_PASSWORD: undefined, }); - await expect(binUtils.retryAuth(mockCallFail)).rejects.toThrow( + await expect(binUtils.retryAuthentication(mockCallFail)).rejects.toThrow( clientErrors.ErrorClientAuthMissing, ); envRestore(); @@ -59,7 +61,7 @@ describe('utils retryAuth', () => { PK_TOKEN: undefined, PK_PASSWORD: 'world', }); - await expect(binUtils.retryAuth(mockCallFail)).rejects.toThrow( + await expect(binUtils.retryAuthentication(mockCallFail)).rejects.toThrow( clientErrors.ErrorClientAuthMissing, ); envRestore(); @@ -82,7 +84,7 @@ describe('utils retryAuth', () => { PK_TOKEN: undefined, PK_PASSWORD: undefined, }); - const result = await binUtils.retryAuth(mockCall); + const result = await binUtils.retryAuthentication(mockCall); envRestore(); // Result is successful expect(result).toBe('hello world'); @@ -116,7 +118,7 @@ describe('utils retryAuth', () => { PK_TOKEN: undefined, PK_PASSWORD: undefined, }); - const result = await binUtils.retryAuth(mockCall); + const result = await binUtils.retryAuthentication(mockCall); envRestore(); // Result is successful expect(result).toBe('hello world'); @@ -153,7 +155,9 @@ describe('utils retryAuth', () => { PK_TOKEN: undefined, PK_PASSWORD: undefined, }); - await expect(binUtils.retryAuth(mockCall)).rejects.toThrow(/oh no/); + await expect(binUtils.retryAuthentication(mockCall)).rejects.toThrow( + /oh no/, + ); envRestore(); expect(mockCall.mock.calls.length).toBe(5); expect(mockedPrompts.mock.calls.length).toBe(4); diff --git a/tests/bin/utils.test.ts b/tests/bin/utils.test.ts index c9159b47f..ed0e83f48 100644 --- a/tests/bin/utils.test.ts +++ b/tests/bin/utils.test.ts @@ -1,10 +1,11 @@ import os from 'os'; +import * as utils from '@/utils'; import * as binUtils from '@/bin/utils'; describe('utils', () => { test('getting default node path', () => { const homeDir = os.homedir(); - const p = binUtils.getDefaultNodePath(); + const p = utils.getDefaultNodePath(); if (process.platform === 'linux') { expect(p).toBe(`${homeDir}/.local/share/polykey`); } else if (process.platform === 'darwin') { diff --git a/tests/bin/utils.ts b/tests/bin/utils.ts index 82e6712e4..1655569a9 100644 --- a/tests/bin/utils.ts +++ b/tests/bin/utils.ts @@ -1,11 +1,14 @@ +import type { ChildProcess } from 'child_process'; import os from 'os'; import fs from 'fs'; import path from 'path'; import process from 'process'; import child_process from 'child_process'; +import readline from 'readline'; import * as mockProcess from 'jest-mock-process'; import mockedEnv from 'mocked-env'; import nexpect from 'nexpect'; +import Logger from '@matrixai/logger'; import main from '../../src/bin/polykey'; /** @@ -45,6 +48,32 @@ async function pkStdio( return buffer.toString(encoding); } }; + // Process events are not allowed when testing + const mockProcessOn = mockProcess.spyOnImplementing( + process, + 'on', + () => process, + ); + const mockProcessOnce = mockProcess.spyOnImplementing( + process, + 'once', + () => process, + ); + const mockProcessAddListener = mockProcess.spyOnImplementing( + process, + 'addListener', + () => process, + ); + const mockProcessOff = mockProcess.spyOnImplementing( + process, + 'off', + () => process, + ); + const mockProcessRemoveListener = mockProcess.spyOnImplementing( + process, + 'removeListener', + () => process, + ); const mockCwd = mockProcess.spyOnImplementing(process, 'cwd', () => cwd!); const envRestore = mockedEnv(env); const mockedStdout = mockProcess.mockProcessStdout(); @@ -58,6 +87,11 @@ async function pkStdio( mockedStdout.mockRestore(); envRestore(); mockCwd.mockRestore(); + mockProcessRemoveListener.mockRestore(); + mockProcessOff.mockRestore(); + mockProcessAddListener.mockRestore(); + mockProcessOnce.mockRestore(); + mockProcessOn.mockRestore(); return { exitCode, stdout, @@ -68,6 +102,7 @@ async function pkStdio( /** * Runs pk command through subprocess * This is used when a subprocess functionality needs to be used + * This is intended for terminating subprocesses * @param env Augments env for command execution * @param cwd Defaults to temporary directory */ @@ -105,6 +140,7 @@ async function pkExec( { env, cwd, + windowsHide: true, }, (error, stdout, stderr) => { if (error != null && error.code === undefined) { @@ -123,6 +159,56 @@ async function pkExec( }); } +/** + * Launch pk command through subprocess + * This is used when a subprocess functionality needs to be used + * This is intended for non-terminating subprocesses + * @param env Augments env for command execution + * @param cwd Defaults to temporary directory + */ +async function pkSpawn( + args: Array = [], + env: Record = {}, + cwd?: string, + logger: Logger = new Logger(pkSpawn.name), +): Promise { + cwd = + cwd ?? (await fs.promises.mkdtemp(path.join(os.tmpdir(), 'polykey-test-'))); + env = { ...process.env, ...env }; + const tsConfigPath = path.resolve( + path.join(global.projectDir, 'tsconfig.json'), + ); + const tsConfigPathsRegisterPath = path.resolve( + path.join(global.projectDir, 'node_modules/tsconfig-paths/register'), + ); + const polykeyPath = path.resolve( + path.join(global.projectDir, 'src/bin/polykey.ts'), + ); + const subprocess = child_process.spawn( + 'ts-node', + [ + '--project', + tsConfigPath, + '--require', + tsConfigPathsRegisterPath, + '--transpile-only', + polykeyPath, + ...args, + ], + { + env, + cwd, + stdio: ['pipe', 'pipe', 'pipe'], + windowsHide: true, + }, + ); + const rlErr = readline.createInterface(subprocess.stderr!); + rlErr.on('line', (l) => { + logger.info(l); + }); + return subprocess; +} + /** * Runs pk command through subprocess expect wrapper * @throws assert.AssertionError when expectations fail @@ -191,4 +277,4 @@ async function pkExpect({ }); } -export { pk, pkStdio, pkExec, pkExpect }; +export { pk, pkStdio, pkExec, pkSpawn, pkExpect }; diff --git a/tests/bin/vaults.test.ts b/tests/bin/vaults.test.ts index 4c6abbdb5..eed8ca1ca 100644 --- a/tests/bin/vaults.test.ts +++ b/tests/bin/vaults.test.ts @@ -4,11 +4,17 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { PolykeyAgent } from '@'; +import PolykeyAgent from '@/PolykeyAgent'; import { makeNodeId } from '@/nodes/utils'; import { makeVaultIdPretty } from '@/vaults/utils'; import * as utils from './utils'; +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. @@ -105,7 +111,7 @@ describe('CLI vaults', () => { await polykeyAgent.vaultManager.createVault('Vault1' as VaultName); await polykeyAgent.vaultManager.createVault('Vault2' as VaultName); - const result = await utils.pkStdio([...command], {}, dataDir); + const result = await utils.pkStdio([...command]); expect(result.exitCode).toBe(0); }); }); diff --git a/tests/bootstrap/bootstrap.test.ts b/tests/bootstrap/bootstrap.test.ts index 1429734b1..f5ea0c65e 100644 --- a/tests/bootstrap/bootstrap.test.ts +++ b/tests/bootstrap/bootstrap.test.ts @@ -3,14 +3,20 @@ import path from 'path'; import os from 'os'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { bootstrapPolykeyState, checkKeynodeState } from '@/bootstrap'; import PolykeyAgent from '@/PolykeyAgent'; import * as bootstrapErrors from '@/bootstrap/errors'; -import * as agentUtils from '@/agent/utils'; +import * as bootstrapUtils from '@/bootstrap/utils'; +import { Status } from '@/status'; -describe.skip('Bootstrap', () => { - const password = 'password'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + +describe('Bootstrap', () => { const logger = new Logger('AgentServerTest', LogLevel.WARN, [ new StreamHandler(), ]); @@ -28,9 +34,7 @@ describe.skip('Bootstrap', () => { } beforeEach(async () => { - dataDir = await fs.promises.mkdtemp( - path.join(os.tmpdir(), 'lockfile-test-'), - ); + dataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'status-test-')); nodePath = path.join(dataDir, 'Node'); await fs.promises.mkdir(nodePath); }); @@ -41,80 +45,65 @@ describe.skip('Bootstrap', () => { }); }); - describe('checkKeynodeState should detect', () => { - test('no directory', async () => { - await fs.promises.rmdir(nodePath); - expect(await checkKeynodeState(nodePath)).toBe('NO_DIRECTORY'); - }); - - test('empty directory', async () => { - expect(await checkKeynodeState(nodePath)).toBe('EMPTY_DIRECTORY'); - }); - - test('other contents in directory', async () => { - await fs.promises.mkdir(path.join(nodePath, 'NotAnNodeDirectory')); - expect(await checkKeynodeState(nodePath)).toBe('OTHER_EXISTS'); - }); - - test('keynode without contents in directory', async () => { - await fakeKeynode(nodePath); - expect(await checkKeynodeState(nodePath)).toBe('MALFORMED_KEYNODE'); - }); - + describe('BootstrapPolykeyState', () => { + const password = 'password123'; test( - 'keynode with contents in directory', + 'should create state if no directory', async () => { - const pk = await PolykeyAgent.createPolykeyAgent({ - password, - nodePath: nodePath, - logger: logger, - }); - await pk.stop(); - await pk.destroy(); - expect(await checkKeynodeState(nodePath)).toBe('KEYNODE_EXISTS'); + // Await fs.promises.rmdir(nodePath); + await bootstrapUtils.bootstrapState({ nodePath, password, logger }); + // Should have keynode state; }, - global.polykeyStartupTimeout, + global.polykeyStartupTimeout * 4, ); - }); - describe('BootstrapPolykeyState', () => { - const password = 'password123'; - test('should create state if no directory', async () => { - // Await fs.promises.rmdir(nodePath); - await bootstrapPolykeyState(nodePath, password); - // Should have keynode state; - expect(await checkKeynodeState(nodePath)).toBe('KEYNODE_EXISTS'); - }); test('should create state if empty directory', async () => { - await bootstrapPolykeyState(nodePath, password); - expect(await checkKeynodeState(nodePath)).toBe('KEYNODE_EXISTS'); + await bootstrapUtils.bootstrapState({ + nodePath, + password, + logger, + }); }); test('Should throw error if other files exists.', async () => { await fs.promises.mkdir(path.join(nodePath, 'NotAnNodeDirectory')); await expect(() => - bootstrapPolykeyState(nodePath, password), - ).rejects.toThrow(bootstrapErrors.ErrorExistingState); + bootstrapUtils.bootstrapState({ nodePath, password, logger }), + ).rejects.toThrow(bootstrapErrors.ErrorBootstrapExistingState); }); test('should throw error if keynode already exists.', async () => { await fakeKeynode(nodePath); await expect(() => - bootstrapPolykeyState(nodePath, password), - ).rejects.toThrow(bootstrapErrors.ErrorMalformedKeynode); + bootstrapUtils.bootstrapState({ nodePath, password, logger }), + ).rejects.toThrow(bootstrapErrors.ErrorBootstrapExistingState); }); - test('should be able to start agent on created state.', async () => { - await bootstrapPolykeyState(nodePath, password); - const polykeyAgent = await PolykeyAgent.createPolykeyAgent({ - password, - nodePath: nodePath, - logger: logger, - }); - expect(await agentUtils.checkAgentRunning(nodePath)).toBeTruthy(); - await polykeyAgent.stop(); - await polykeyAgent.destroy(); - expect(await agentUtils.checkAgentRunning(nodePath)).toBeFalsy(); - }); + test( + 'should be able to start agent on created state.', + async () => { + await bootstrapUtils.bootstrapState({ + nodePath, + password, + logger, + }); + const polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + password, + nodePath, + logger, + }); + const statusPath = path.join(nodePath, 'status.json'); + const status = new Status({ + statusPath, + fs, + logger, + }); + await status.waitFor('LIVE', 10000); + await polykeyAgent.stop(); + await polykeyAgent.destroy(); + await status.waitFor('DEAD', 10000); + }, + global.polykeyStartupTimeout * 2, + ); }); }); diff --git a/tests/claims/utils.test.ts b/tests/claims/utils.test.ts index 4bff3da66..fccd00136 100644 --- a/tests/claims/utils.test.ts +++ b/tests/claims/utils.test.ts @@ -14,8 +14,15 @@ import { KeyManager } from '@/keys'; import { sleep } from '@/utils'; import * as claimsUtils from '@/claims/utils'; -import * as keysUtils from '@/keys/utils'; import * as claimsErrors from '@/claims/errors'; +import * as keysUtils from '@/keys/utils'; + +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); describe('Claims utils', () => { const password = 'password'; diff --git a/tests/client/GRPCClientClient.test.ts b/tests/client/GRPCClientClient.test.ts index 997b56216..76c7b3c06 100644 --- a/tests/client/GRPCClientClient.test.ts +++ b/tests/client/GRPCClientClient.test.ts @@ -9,11 +9,18 @@ import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { GRPCClientClient } from '@/client'; import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; import { PolykeyAgent } from '@'; -import * as parsers from '@/bin/parsers'; +import * as binProcessors from '@/bin/utils/processors'; import { Session } from '@/sessions'; import { errors as clientErrors } from '@/client'; import * as testUtils from './utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('GRPCClientClient', () => { const password = 'password'; const logger = new Logger('GRPCClientClientTest', LogLevel.WARN, [ @@ -81,10 +88,10 @@ describe('GRPCClientClient', () => { }); test('can get status', async () => { await fs.promises.writeFile(path.join(dataDir, 'password'), password); - const meta = await parsers.parseAuth({ - passwordFile: path.join(dataDir, 'password'), - fs: fs, - }); + const meta = await binProcessors.processAuthentication( + path.join(dataDir, 'password'), + fs, + ); const emptyMessage = new utilsPB.EmptyMessage(); const response = await client.agentStatus(emptyMessage, meta); expect(response.getAddress()).toBeTruthy(); diff --git a/tests/client/PolykeyClient.test.ts b/tests/client/PolykeyClient.test.ts index 4d0383b80..1381bbe36 100644 --- a/tests/client/PolykeyClient.test.ts +++ b/tests/client/PolykeyClient.test.ts @@ -4,8 +4,7 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - -import * as parsers from '@/bin/parsers'; +import * as binProcessors from '@/bin/utils/processors'; import { PolykeyClient } from '@'; import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; @@ -13,6 +12,13 @@ import { PolykeyAgent } from '@'; import * as testUtils from './utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('PolykeyClient', () => { const password = 'password'; const logger = new Logger('GRPCClientClientTest', LogLevel.WARN, [ @@ -26,6 +32,8 @@ describe('PolykeyClient', () => { let meta: grpc.Metadata; let dataDir: string; + let nodePath: string; + let clientPath: string; let polykeyAgent: PolykeyAgent; @@ -33,13 +41,15 @@ describe('PolykeyClient', () => { dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); + nodePath = path.join(dataDir, 'node'); + clientPath = path.join(dataDir, 'client'); passwordFile = path.join(dataDir, 'password'); await fs.promises.writeFile(passwordFile, password); - meta = await parsers.parseAuth({ passwordFile: passwordFile, fs: fs }); + meta = await binProcessors.processAuthentication(passwordFile, fs); polykeyAgent = await PolykeyAgent.createPolykeyAgent({ password, - nodePath: dataDir, + nodePath, logger: logger, }); @@ -49,7 +59,10 @@ describe('PolykeyClient', () => { }); pkClient = await PolykeyClient.createPolykeyClient({ - nodePath: dataDir, + nodeId: polykeyAgent.keyManager.getNodeId(), + host: polykeyAgent.grpcServerClient.host, + port: polykeyAgent.grpcServerClient.port, + nodePath: clientPath, fs: fs, logger: logger, }); @@ -84,8 +97,9 @@ describe('PolykeyClient', () => { new StreamHandler(), ]); let dataDir: string; - let nodePath: string; - let polykeyAgent: PolykeyAgent; + let nodePath2: string; + let clientPath2: string; + let polykeyAgent2: PolykeyAgent; let sessionToken; beforeAll(async () => { @@ -93,33 +107,34 @@ describe('PolykeyClient', () => { dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); - nodePath = path.join(dataDir, 'keynode'); + nodePath2 = path.join(dataDir, 'keynode'); + clientPath2 = path.join(dataDir, 'client2'); // Starting an agent. - polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + polykeyAgent2 = await PolykeyAgent.createPolykeyAgent({ password, - nodePath, + nodePath: nodePath2, logger: logger.getChild(PolykeyAgent.name), }); - sessionToken = await polykeyAgent.sessionManager.createToken(); + sessionToken = await polykeyAgent2.sessionManager.createToken(); }, global.defaultTimeout * 3); afterAll(async () => { - await polykeyAgent.stop(); - await polykeyAgent.destroy(); + await polykeyAgent2.stop(); + await polykeyAgent2.destroy(); }); test('can get status over TLS', async () => { // Starting client. const pkClient = await PolykeyClient.createPolykeyClient({ - nodePath, + nodeId: polykeyAgent2.keyManager.getNodeId(), + host: polykeyAgent2.grpcServerClient.host, + port: polykeyAgent2.grpcServerClient.port, + nodePath: clientPath2, fs: fs, logger: logger.getChild(PolykeyClient.name), }); await pkClient.session.start({ sessionToken }); - const meta = await parsers.parseAuth({ - passwordFile: passwordFile, - fs: fs, - }); + const meta = await binProcessors.processAuthentication(passwordFile, fs); const emptyMessage = new utilsPB.EmptyMessage(); const response = await pkClient.grpcClient.agentStatus( diff --git a/tests/client/rpcAgent.test.ts b/tests/client/rpcAgent.test.ts index 0df5251ca..68a623f5f 100644 --- a/tests/client/rpcAgent.test.ts +++ b/tests/client/rpcAgent.test.ts @@ -9,10 +9,16 @@ import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; import { KeyManager } from '@/keys'; import { ForwardProxy } from '@/network'; import * as grpcUtils from '@/grpc/utils'; -import * as agentUtils from '@/agent/utils'; -import { sleep } from '@/utils'; +import { Status } from '@/status'; import * as testUtils from './utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. @@ -104,8 +110,13 @@ describe('Agent client service', () => { const emptyMessage = new utilsPB.EmptyMessage(); await agentStop(emptyMessage, callCredentials); - await sleep(5000); - expect(await agentUtils.checkAgentRunning(dataDir)).toBeFalsy(); + const statusPath = path.join(polykeyAgent.nodePath, 'status'); + const status = new Status({ + statusPath, + fs, + logger, + }); + await status.waitFor('DEAD', 10000); }, global.polykeyStartupTimeout * 2, ); diff --git a/tests/client/rpcGestalts.test.ts b/tests/client/rpcGestalts.test.ts index 13c92de50..c1408658c 100644 --- a/tests/client/rpcGestalts.test.ts +++ b/tests/client/rpcGestalts.test.ts @@ -24,6 +24,13 @@ import * as nodesUtils from '@/nodes/utils'; import * as testUtils from './utils'; import TestProvider from '../identities/TestProvider'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. diff --git a/tests/client/rpcIdentities.test.ts b/tests/client/rpcIdentities.test.ts index 3ce8b05e9..3309dfdf4 100644 --- a/tests/client/rpcIdentities.test.ts +++ b/tests/client/rpcIdentities.test.ts @@ -17,6 +17,13 @@ import * as grpcUtils from '@/grpc/utils'; import * as testUtils from './utils'; import TestProvider from '../identities/TestProvider'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. diff --git a/tests/client/rpcKeys.test.ts b/tests/client/rpcKeys.test.ts index a88df4cc8..14a926fba 100644 --- a/tests/client/rpcKeys.test.ts +++ b/tests/client/rpcKeys.test.ts @@ -14,6 +14,13 @@ import { ForwardProxy } from '@/network'; import * as grpcUtils from '@/grpc/utils'; import * as testUtils from './utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. @@ -110,99 +117,107 @@ describe('Keys client service', () => { expect(key.getPrivate()).toBe(keyPair.privateKey); expect(key.getPublic()).toBe(keyPair.publicKey); }); - test('should reset root keypair', async () => { - const getRootKeyPair = grpcUtils.promisifyUnaryCall( - client, - client.keysKeyPairRoot, - ); + test( + 'should reset root keypair', + async () => { + const getRootKeyPair = grpcUtils.promisifyUnaryCall( + client, + client.keysKeyPairRoot, + ); - const resetKeyPair = grpcUtils.promisifyUnaryCall( - client, - client.keysKeyPairReset, - ); + const resetKeyPair = grpcUtils.promisifyUnaryCall( + client, + client.keysKeyPairReset, + ); - const keyPair = keyManager.getRootKeyPairPem(); - const nodeId1 = nodeManager.getNodeId(); - // @ts-ignore - get protected property - const fwdTLSConfig1 = polykeyAgent.fwdProxy.tlsConfig; - // @ts-ignore - get protected property - const revTLSConfig1 = polykeyAgent.revProxy.tlsConfig; - // @ts-ignore - get protected property - const serverTLSConfig1 = polykeyAgent.grpcServerClient.tlsConfig; - const expectedTLSConfig1: TLSConfig = { - keyPrivatePem: keyPair.privateKey, - certChainPem: await keyManager.getRootCertChainPem(), - }; - expect(fwdTLSConfig1).toEqual(expectedTLSConfig1); - expect(revTLSConfig1).toEqual(expectedTLSConfig1); - expect(serverTLSConfig1).toEqual(expectedTLSConfig1); - const keyMessage = new keysPB.Key(); - keyMessage.setName('somepassphrase'); - await resetKeyPair(keyMessage, callCredentials); - const emptyMessage = new utilsPB.EmptyMessage(); - await fs.promises.writeFile(passwordFile, 'somepassphrase'); - const key = await getRootKeyPair(emptyMessage, callCredentials); - const nodeId2 = nodeManager.getNodeId(); - // @ts-ignore - get protected property - const fwdTLSConfig2 = polykeyAgent.fwdProxy.tlsConfig; - // @ts-ignore - get protected property - const revTLSConfig2 = polykeyAgent.revProxy.tlsConfig; - // @ts-ignore - get protected property - const serverTLSConfig2 = polykeyAgent.grpcServerClient.tlsConfig; - const expectedTLSConfig2: TLSConfig = { - keyPrivatePem: key.getPrivate(), - certChainPem: await keyManager.getRootCertChainPem(), - }; - expect(fwdTLSConfig2).toEqual(expectedTLSConfig2); - expect(revTLSConfig2).toEqual(expectedTLSConfig2); - expect(serverTLSConfig2).toEqual(expectedTLSConfig2); - expect(key.getPrivate()).not.toBe(keyPair.privateKey); - expect(key.getPublic()).not.toBe(keyPair.publicKey); - expect(nodeId1).not.toBe(nodeId2); - }); - test('should renew root keypair', async () => { - const renewKeyPair = grpcUtils.promisifyUnaryCall( - client, - client.keysKeyPairRenew, - ); + const keyPair = keyManager.getRootKeyPairPem(); + const nodeId1 = nodeManager.getNodeId(); + // @ts-ignore - get protected property + const fwdTLSConfig1 = polykeyAgent.fwdProxy.tlsConfig; + // @ts-ignore - get protected property + const revTLSConfig1 = polykeyAgent.revProxy.tlsConfig; + // @ts-ignore - get protected property + const serverTLSConfig1 = polykeyAgent.grpcServerClient.tlsConfig; + const expectedTLSConfig1: TLSConfig = { + keyPrivatePem: keyPair.privateKey, + certChainPem: await keyManager.getRootCertChainPem(), + }; + expect(fwdTLSConfig1).toEqual(expectedTLSConfig1); + expect(revTLSConfig1).toEqual(expectedTLSConfig1); + expect(serverTLSConfig1).toEqual(expectedTLSConfig1); + const keyMessage = new keysPB.Key(); + keyMessage.setName('somepassphrase'); + await resetKeyPair(keyMessage, callCredentials); + const emptyMessage = new utilsPB.EmptyMessage(); + await fs.promises.writeFile(passwordFile, 'somepassphrase'); + const key = await getRootKeyPair(emptyMessage, callCredentials); + const nodeId2 = nodeManager.getNodeId(); + // @ts-ignore - get protected property + const fwdTLSConfig2 = polykeyAgent.fwdProxy.tlsConfig; + // @ts-ignore - get protected property + const revTLSConfig2 = polykeyAgent.revProxy.tlsConfig; + // @ts-ignore - get protected property + const serverTLSConfig2 = polykeyAgent.grpcServerClient.tlsConfig; + const expectedTLSConfig2: TLSConfig = { + keyPrivatePem: key.getPrivate(), + certChainPem: await keyManager.getRootCertChainPem(), + }; + expect(fwdTLSConfig2).toEqual(expectedTLSConfig2); + expect(revTLSConfig2).toEqual(expectedTLSConfig2); + expect(serverTLSConfig2).toEqual(expectedTLSConfig2); + expect(key.getPrivate()).not.toBe(keyPair.privateKey); + expect(key.getPublic()).not.toBe(keyPair.publicKey); + expect(nodeId1).not.toBe(nodeId2); + }, + global.defaultTimeout * 3, + ); + test( + 'should renew root keypair', + async () => { + const renewKeyPair = grpcUtils.promisifyUnaryCall( + client, + client.keysKeyPairRenew, + ); - const rootKeyPair1 = keyManager.getRootKeyPairPem(); - const nodeId1 = nodeManager.getNodeId(); - // @ts-ignore - get protected property - const fwdTLSConfig1 = polykeyAgent.fwdProxy.tlsConfig; - // @ts-ignore - get protected property - const revTLSConfig1 = polykeyAgent.revProxy.tlsConfig; - // @ts-ignore - get protected property - const serverTLSConfig1 = polykeyAgent.grpcServerClient.tlsConfig; - const expectedTLSConfig1: TLSConfig = { - keyPrivatePem: rootKeyPair1.privateKey, - certChainPem: await keyManager.getRootCertChainPem(), - }; - expect(fwdTLSConfig1).toEqual(expectedTLSConfig1); - expect(revTLSConfig1).toEqual(expectedTLSConfig1); - expect(serverTLSConfig1).toEqual(expectedTLSConfig1); - const keyMessage = new keysPB.Key(); - keyMessage.setName('somepassphrase'); - await renewKeyPair(keyMessage, callCredentials); - const rootKeyPair2 = keyManager.getRootKeyPairPem(); - const nodeId2 = nodeManager.getNodeId(); - // @ts-ignore - get protected property - const fwdTLSConfig2 = polykeyAgent.fwdProxy.tlsConfig; - // @ts-ignore - get protected property - const revTLSConfig2 = polykeyAgent.revProxy.tlsConfig; - // @ts-ignore - get protected property - const serverTLSConfig2 = polykeyAgent.grpcServerClient.tlsConfig; - const expectedTLSConfig2: TLSConfig = { - keyPrivatePem: rootKeyPair2.privateKey, - certChainPem: await keyManager.getRootCertChainPem(), - }; - expect(fwdTLSConfig2).toEqual(expectedTLSConfig2); - expect(revTLSConfig2).toEqual(expectedTLSConfig2); - expect(serverTLSConfig2).toEqual(expectedTLSConfig2); - expect(rootKeyPair2.privateKey).not.toBe(rootKeyPair1.privateKey); - expect(rootKeyPair2.publicKey).not.toBe(rootKeyPair1.publicKey); - expect(nodeId1).not.toBe(nodeId2); - }); + const rootKeyPair1 = keyManager.getRootKeyPairPem(); + const nodeId1 = nodeManager.getNodeId(); + // @ts-ignore - get protected property + const fwdTLSConfig1 = polykeyAgent.fwdProxy.tlsConfig; + // @ts-ignore - get protected property + const revTLSConfig1 = polykeyAgent.revProxy.tlsConfig; + // @ts-ignore - get protected property + const serverTLSConfig1 = polykeyAgent.grpcServerClient.tlsConfig; + const expectedTLSConfig1: TLSConfig = { + keyPrivatePem: rootKeyPair1.privateKey, + certChainPem: await keyManager.getRootCertChainPem(), + }; + expect(fwdTLSConfig1).toEqual(expectedTLSConfig1); + expect(revTLSConfig1).toEqual(expectedTLSConfig1); + expect(serverTLSConfig1).toEqual(expectedTLSConfig1); + const keyMessage = new keysPB.Key(); + keyMessage.setName('somepassphrase'); + await renewKeyPair(keyMessage, callCredentials); + const rootKeyPair2 = keyManager.getRootKeyPairPem(); + const nodeId2 = nodeManager.getNodeId(); + // @ts-ignore - get protected property + const fwdTLSConfig2 = polykeyAgent.fwdProxy.tlsConfig; + // @ts-ignore - get protected property + const revTLSConfig2 = polykeyAgent.revProxy.tlsConfig; + // @ts-ignore - get protected property + const serverTLSConfig2 = polykeyAgent.grpcServerClient.tlsConfig; + const expectedTLSConfig2: TLSConfig = { + keyPrivatePem: rootKeyPair2.privateKey, + certChainPem: await keyManager.getRootCertChainPem(), + }; + expect(fwdTLSConfig2).toEqual(expectedTLSConfig2); + expect(revTLSConfig2).toEqual(expectedTLSConfig2); + expect(serverTLSConfig2).toEqual(expectedTLSConfig2); + expect(rootKeyPair2.privateKey).not.toBe(rootKeyPair1.privateKey); + expect(rootKeyPair2.publicKey).not.toBe(rootKeyPair1.publicKey); + expect(nodeId1).not.toBe(nodeId2); + }, + global.defaultTimeout * 3, + ); test('should encrypt and decrypt with root keypair', async () => { const encryptWithKeyPair = grpcUtils.promisifyUnaryCall( client, diff --git a/tests/client/rpcNodes.test.ts b/tests/client/rpcNodes.test.ts index 4364e3a3d..fb5a616b0 100644 --- a/tests/client/rpcNodes.test.ts +++ b/tests/client/rpcNodes.test.ts @@ -16,12 +16,18 @@ import { ForwardProxy } from '@/network'; import * as grpcUtils from '@/grpc/utils'; import * as nodesErrors from '@/nodes/errors'; -import { sleep } from '@/utils'; -import { checkAgentRunning } from '@/agent/utils'; import { makeNodeId } from '@/nodes/utils'; +import { Status } from '@/status'; import * as testUtils from './utils'; import * as testKeynodeUtils from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. @@ -174,6 +180,13 @@ describe('Client service', () => { const serverNodeId = polykeyServer.nodeManager.getNodeId(); await testKeynodeUtils.addRemoteDetails(polykeyAgent, polykeyServer); await polykeyServer.stop(); + const statusPath = path.join(polykeyServer.nodePath, 'status'); + const status = new Status({ + statusPath, + fs, + logger, + }); + await status.waitFor('DEAD', 10000); // Case 1: cannot establish new connection, so offline const nodesPing = grpcUtils.promisifyUnaryCall( @@ -187,24 +200,14 @@ describe('Client service', () => { // Case 2: can establish new connection, so online await polykeyServer.start({ password: 'password' }); + await status.waitFor('LIVE', 10000); // Update the details (changed because we started again) await testKeynodeUtils.addRemoteDetails(polykeyAgent, polykeyServer); const res2 = await nodesPing(nodeMessage, callCredentials); expect(res2.getSuccess()).toEqual(true); // Case 3: pre-existing connection no longer active, so offline await polykeyServer.stop(); - await sleep(30000); - // TODO: Fix the polling to work. Currently the agent is not running, - // but the ping still returns true. Potentially lower-level connection - // hasn't timed out. - // const running = await poll( - // async () => checkAgentRunning(polykeyServer.nodePath), - // (e, result) => { - // if (result) return false; - // return true; - // } - // ); - expect(await checkAgentRunning(polykeyServer.nodePath)).toBeFalsy(); + await status.waitFor('DEAD', 10000); const res3 = await nodesPing(nodeMessage, callCredentials); expect(res3.getSuccess()).toEqual(false); }, diff --git a/tests/client/rpcNotifications.test.ts b/tests/client/rpcNotifications.test.ts index 15aa259a0..6c88724ac 100644 --- a/tests/client/rpcNotifications.test.ts +++ b/tests/client/rpcNotifications.test.ts @@ -19,6 +19,13 @@ import * as vaultsUtils from '@/vaults/utils'; import * as testUtils from './utils'; import * as testKeynodeUtils from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. diff --git a/tests/client/rpcSessions.test.ts b/tests/client/rpcSessions.test.ts index e2e4ddcf5..96a149a3a 100644 --- a/tests/client/rpcSessions.test.ts +++ b/tests/client/rpcSessions.test.ts @@ -15,6 +15,13 @@ import { sleep } from '@/utils'; import * as errors from '@/errors'; import * as testUtils from './utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. diff --git a/tests/client/rpcVaults.test.ts b/tests/client/rpcVaults.test.ts index fc4b4e988..9c1c28542 100644 --- a/tests/client/rpcVaults.test.ts +++ b/tests/client/rpcVaults.test.ts @@ -18,6 +18,13 @@ import * as vaultsUtils from '@/vaults/utils'; import { vaultOps } from '@/vaults'; import * as testUtils from './utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + /** * This test file has been optimised to use only one instance of PolykeyAgent where posible. * Setting up the PolykeyAgent has been done in a beforeAll block. diff --git a/tests/client/utils.ts b/tests/client/utils.ts index e36a2d5c8..6aa9f0c43 100644 --- a/tests/client/utils.ts +++ b/tests/client/utils.ts @@ -1,6 +1,8 @@ import type { IClientServiceServer } from '@/proto/js/polykey/v1/client_service_grpc_pb'; import type { SessionToken } from '@/sessions/types'; import type { PolykeyAgent } from '@'; +import type { NodeId } from '@/nodes/types'; +import type { Host, Port } from '@/network/types'; import * as grpc from '@grpc/grpc-js'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; @@ -34,6 +36,7 @@ async function openTestClientServer({ fwdProxy: polykeyAgent.fwdProxy, revProxy: polykeyAgent.revProxy, clientGrpcServer: polykeyAgent.grpcServerClient, + fs: polykeyAgent.fs, }); const callCredentials = _secure @@ -56,18 +59,24 @@ const closeTestClientServer = async (server) => { await tryShutdown(); }; -async function openTestClientClient(nodePath) { +async function openTestClientClient( + nodeId: NodeId, + host: Host, + port: Port, + clientPath: string, +) { const logger = new Logger('ClientClientTest', LogLevel.WARN, [ new StreamHandler(), ]); const fs = require('fs/promises'); const pkc: PolykeyClient = await PolykeyClient.createPolykeyClient({ - nodePath, + nodePath: clientPath, + host: host, + nodeId, + port: port, fs, logger, - }); - await pkc.start({ timeout: 30000, }); diff --git a/tests/discovery/Discovery.test.ts b/tests/discovery/Discovery.test.ts index 54652505a..e6d557371 100644 --- a/tests/discovery/Discovery.test.ts +++ b/tests/discovery/Discovery.test.ts @@ -14,6 +14,13 @@ import { setupRemoteKeynode, } from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('Discovery', () => { // Constants. const password = 'password'; diff --git a/tests/gestalts/GestaltGraph.test.ts b/tests/gestalts/GestaltGraph.test.ts index f997bff89..74739e2e9 100644 --- a/tests/gestalts/GestaltGraph.test.ts +++ b/tests/gestalts/GestaltGraph.test.ts @@ -21,16 +21,14 @@ import { errors as gestaltErrors, } from '@/gestalts'; import { ACL } from '@/acl'; -import { KeyManager } from '@/keys'; +import * as keysUtils from '@/keys/utils'; import { makeCrypto } from '../utils'; describe('GestaltGraph', () => { - const pass = 'password'; const logger = new Logger('GestaltGraph Test', LogLevel.WARN, [ new StreamHandler(), ]); let dataDir: string; - let keyManager: KeyManager; let db: DB; let acl: ACL; @@ -47,17 +45,11 @@ describe('GestaltGraph', () => { dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); - const keysPath = `${dataDir}/keys`; - keyManager = await KeyManager.createKeyManager({ - password: pass, - keysPath, - logger, - }); const dbPath = `${dataDir}/db`; db = await DB.createDB({ dbPath, logger, - crypto: makeCrypto(keyManager), + crypto: makeCrypto(await keysUtils.generateKey()), }); acl = await ACL.createACL({ db, logger }); @@ -131,8 +123,6 @@ describe('GestaltGraph', () => { await acl.destroy(); await db.stop(); await db.destroy(); - await keyManager.stop(); - await keyManager.destroy(); await fs.promises.rm(dataDir, { force: true, recursive: true, diff --git a/tests/grpc/GRPCClient.test.ts b/tests/grpc/GRPCClient.test.ts index 1f533e7e1..4fc3e763c 100644 --- a/tests/grpc/GRPCClient.test.ts +++ b/tests/grpc/GRPCClient.test.ts @@ -4,12 +4,13 @@ import type { SessionToken } from '@/sessions/types'; import type { NodeId } from '@/nodes/types'; import type { Host, Port } from '@/network/types'; import type { KeyPair, Certificate } from '@/keys/types'; +import type { KeyManager } from '@/keys'; import os from 'os'; import path from 'path'; import fs from 'fs'; import { DB } from '@matrixai/db'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { KeyManager, utils as keysUtils } from '@/keys'; +import { utils as keysUtils } from '@/keys'; import { Session, SessionManager } from '@/sessions'; import { utils as networkUtils } from '@/network'; import { errors as grpcErrors } from '@/grpc'; @@ -18,7 +19,6 @@ import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; import * as utils from './utils'; describe('GRPCClient', () => { - const password = 'password'; const logger = new Logger('GRPCClient Test', LogLevel.WARN, [ new StreamHandler(), ]); @@ -32,7 +32,6 @@ describe('GRPCClient', () => { let clientKeyPair: KeyPair; let clientCert: Certificate; - let keyManager: KeyManager; let db: DB; let sessionManager: SessionManager; @@ -48,24 +47,19 @@ describe('GRPCClient', () => { 31536000, ); nodeIdServer = networkUtils.certNodeId(serverCert); - const keysPath = path.join(dataDir, 'keys'); - keyManager = await KeyManager.createKeyManager({ - password, - keysPath, - logger, - }); const dbPath = path.join(dataDir, 'db'); db = await DB.createDB({ dbPath, logger, crypto: { - key: keyManager.dbKey, + key: await keysUtils.generateKey(), ops: { encrypt: keysUtils.encryptWithKey, decrypt: keysUtils.decryptWithKey, }, }, }); + const keyManager = { getNodeId: () => 'nodeID' as NodeId } as KeyManager; // Cheeky mocking. sessionManager = await SessionManager.createSessionManager({ db, keyManager, @@ -98,7 +92,6 @@ describe('GRPCClient', () => { await utils.closeTestServerSecure(server); await sessionManager.stop(); await db.stop(); - await keyManager.stop(); await fs.promises.rm(dataDir, { force: true, recursive: true, diff --git a/tests/grpc/GRPCServer.test.ts b/tests/grpc/GRPCServer.test.ts index 701cde0ff..ac7f18c69 100644 --- a/tests/grpc/GRPCServer.test.ts +++ b/tests/grpc/GRPCServer.test.ts @@ -6,14 +6,22 @@ import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; import { GRPCServer, utils as grpcUtils } from '@/grpc'; -import { KeyManager, utils as keysUtils } from '@/keys'; +import { KeyManager } from '@/keys'; import { utils as networkUtils } from '@/network'; import * as utilsPB from '@/proto/js/polykey/v1/utils/utils_pb'; import * as grpcErrors from '@/grpc/errors'; import { SessionManager } from '@/sessions'; import * as clientUtils from '@/client/utils'; +import * as keysUtils from '@/keys/utils'; import * as utils from './utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('GRPCServer', () => { const password = 'password'; let dataDir: string; diff --git a/tests/identities/IdentitiesManager.test.ts b/tests/identities/IdentitiesManager.test.ts index af1809fcb..32778e583 100644 --- a/tests/identities/IdentitiesManager.test.ts +++ b/tests/identities/IdentitiesManager.test.ts @@ -13,42 +13,32 @@ import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; -import { KeyManager } from '@/keys'; import { IdentitiesManager, providers } from '@/identities'; import * as identitiesErrors from '@/identities/errors'; +import * as keysUtils from '@/keys/utils'; import TestProvider from './TestProvider'; import { makeCrypto } from '../utils'; describe('IdentitiesManager', () => { - const password = 'password'; const logger = new Logger('IdentitiesManager Test', LogLevel.WARN, [ new StreamHandler(), ]); let dataDir: string; - let keyManager: KeyManager; let db: DB; beforeEach(async () => { dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); - const keysPath = `${dataDir}/keys`; - keyManager = await KeyManager.createKeyManager({ - password, - keysPath, - logger, - }); const dbPath = `${dataDir}/db`; db = await DB.createDB({ dbPath, logger, - crypto: makeCrypto(keyManager), + crypto: makeCrypto(await keysUtils.generateKey()), }); }); afterEach(async () => { await db.stop(); await db.destroy(); - await keyManager.stop(); - await keyManager.destroy(); await fs.promises.rm(dataDir, { force: true, recursive: true, diff --git a/tests/index.test.ts b/tests/index.test.ts index 1f19aea45..952f089a8 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -4,6 +4,13 @@ import os from 'os'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { PolykeyAgent } from '@'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('index', () => { const logger = new Logger('index test', LogLevel.WARN, [new StreamHandler()]); let dataDir; diff --git a/tests/keys/KeyManager.test.ts b/tests/keys/KeyManager.test.ts index afbea4057..a1771bcbb 100644 --- a/tests/keys/KeyManager.test.ts +++ b/tests/keys/KeyManager.test.ts @@ -1,17 +1,17 @@ import type { PolykeyWorkerManagerInterface } from '@/workers/types'; -import type { PublicKey } from '@/keys/types'; +import type { KeyPair, PublicKey } from '@/keys/types'; import fs from 'fs'; import os from 'os'; import path from 'path'; import { Buffer } from 'buffer'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - +import { DB } from '@matrixai/db'; import KeyManager from '@/keys/KeyManager'; -import { sleep } from '@/utils'; -import * as keysUtils from '@/keys/utils'; import * as keysErrors from '@/keys/errors'; -import { isNodeId, makeNodeId } from '@/nodes/utils'; -import { createWorkerManager } from '@/workers/utils'; +import * as workersUtils from '@/workers/utils'; +import * as keysUtils from '@/keys/utils'; +import { sleep } from '@/utils'; +import { makeCrypto } from '../utils'; describe('KeyManager', () => { const password = 'password'; @@ -19,11 +19,14 @@ describe('KeyManager', () => { new StreamHandler(), ]); let dataDir: string; - const cores = 1; + let keyPair: KeyPair; let workerManager: PolykeyWorkerManagerInterface; + let mockedGenerateDeterministicKeyPair; beforeAll(async () => { - workerManager = await createWorkerManager({ - cores, + // Key pair generated once for mocking + keyPair = await keysUtils.generateKeyPair(4096); + workerManager = await workersUtils.createWorkerManager({ + cores: 1, logger, }); }); @@ -31,17 +34,23 @@ describe('KeyManager', () => { await workerManager.destroy(); }); beforeEach(async () => { + // Use the mock for all tests + // Each test can individually restore the original implementation with mockRestore + // Has to be set in beforeEach as mockRestore removes the spyOn + mockedGenerateDeterministicKeyPair = jest + .spyOn(keysUtils, 'generateDeterministicKeyPair') + .mockResolvedValue(keyPair); dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); }); afterEach(async () => { + mockedGenerateDeterministicKeyPair.mockRestore(); await fs.promises.rm(dataDir, { force: true, recursive: true, }); }); - test('KeyManager readiness', async () => { const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ @@ -66,20 +75,6 @@ describe('KeyManager', () => { await keyManager.getRootCertChain(); }).rejects.toThrow(keysErrors.ErrorKeyManagerNotRunning); }); - // Test('construction constructs root key pair and root cert and root certs', async () => { - // const keysPath = `${dataDir}/keys`; - // const keyManager = await KeyManager.createKeyManager({ - // password, - // keysPath, - // logger, - // }); - // const keysPathContents = await fs.promises.readdir(keysPath); - // expect(keysPathContents).toContain('root.pub'); - // expect(keysPathContents).toContain('root.key'); - // expect(keysPathContents).toContain('root.crt'); - // expect(keysPathContents).toContain('root_certs'); - // await keyManager.stop(); - // }); test('constructs root key pair, root cert, root certs and db key', async () => { const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ @@ -104,6 +99,69 @@ describe('KeyManager', () => { expect(rootCertChainPem).not.toBeUndefined(); await keyManager.stop(); }); + test( + 'creates a recovery code and can recover from the same code', + async () => { + // Use the real generateDeterministicKeyPair + mockedGenerateDeterministicKeyPair.mockRestore(); + const keysPath = `${dataDir}/keys`; + // Minimum key pair size is 1024 + // Key pair generation can take 4 to 15 seconds + const keyManager = await KeyManager.createKeyManager({ + password, + keysPath, + rootKeyPairBits: 1024, + logger, + }); + const nodeId = keyManager.getNodeId(); + // Acquire the recovery code + const recoveryCode = keyManager.getRecoveryCode()!; + expect(recoveryCode).toBeDefined(); + await keyManager.stop(); + // Oops forgot the password + // Use the recovery code to recover and set the new password + await keyManager.start({ + password: 'newpassword', + recoveryCode, + }); + expect(await keyManager.checkPassword('newpassword')).toBe(true); + expect(keyManager.getNodeId()).toBe(nodeId); + await keyManager.stop(); + }, + global.defaultTimeout * 2, + ); + test( + 'create deterministic keypair with recovery code', + async () => { + // Use the real generateDeterministicKeyPair + mockedGenerateDeterministicKeyPair.mockRestore(); + const recoveryCode = keysUtils.generateRecoveryCode(); + const keysPath1 = `${dataDir}/keys1`; + const keyManager1 = await KeyManager.createKeyManager({ + password, + recoveryCode, + keysPath: keysPath1, + rootKeyPairBits: 1024, + logger, + }); + expect(keyManager1.getRecoveryCode()).toBe(recoveryCode); + const nodeId1 = keyManager1.getNodeId(); + await keyManager1.stop(); + const keysPath2 = `${dataDir}/keys2`; + const keyManager2 = await KeyManager.createKeyManager({ + password, + recoveryCode, + keysPath: keysPath2, + rootKeyPairBits: 1024, + logger, + }); + expect(keyManager2.getRecoveryCode()).toBe(recoveryCode); + const nodeId2 = keyManager2.getNodeId(); + await keyManager2.stop(); + expect(nodeId1).toBe(nodeId2); + }, + global.defaultTimeout * 2, + ); test('uses WorkerManager for generating root key pair', async () => { const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ @@ -119,10 +177,9 @@ describe('KeyManager', () => { keyManager.unsetWorkerManager(); }); test('encrypting and decrypting with root key', async () => { - const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ password, - keysPath, + keysPath: `${dataDir}/keys`, logger, }); const plainText = Buffer.from('abc'); @@ -132,10 +189,9 @@ describe('KeyManager', () => { await keyManager.stop(); }); test('uses WorkerManager for encryption and decryption with root key', async () => { - const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ password, - keysPath, + keysPath: `${dataDir}/keys`, logger, }); keyManager.setWorkerManager(workerManager); @@ -147,24 +203,22 @@ describe('KeyManager', () => { keyManager.unsetWorkerManager(); }); test('encrypting beyond maximum size', async () => { - const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ password, - keysPath, + keysPath: `${dataDir}/keys`, logger, }); // No way we can encrypt 1000 bytes without a ridiculous key size const plainText = Buffer.from(new Array(1000 + 1).join('A')); await expect(keyManager.encryptWithRootKeyPair(plainText)).rejects.toThrow( - keysErrors.ErrorEncryptSize, + 'Maximum plain text byte size is 446', ); await keyManager.stop(); }); test('signing and verifying with root key', async () => { - const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ password, - keysPath, + keysPath: `${dataDir}/keys`, logger, }); const data = Buffer.from('abc'); @@ -174,10 +228,9 @@ describe('KeyManager', () => { await keyManager.stop(); }); test('uses WorkerManager for signing and verifying with root key', async () => { - const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ password, - keysPath, + keysPath: `${dataDir}/keys`, logger, }); keyManager.setWorkerManager(workerManager); @@ -195,7 +248,7 @@ describe('KeyManager', () => { keysPath, logger, }); - await keyManager.changeRootKeyPassword('newpassword'); + await keyManager.changePassword('newpassword'); await keyManager.stop(); await expect(async () => { await KeyManager.createKeyManager({ @@ -247,11 +300,20 @@ describe('KeyManager', () => { keysPath, logger, }); + // KeyManager manages the db key + const dbPath = `${dataDir}/db`; + const db = await DB.createDB({ + dbPath, + logger, + crypto: makeCrypto(keyManager.dbKey), + }); const rootKeyPair1 = keyManager.getRootKeyPair(); const rootCert1 = keyManager.getRootCert(); await sleep(2000); // Let's just make sure there is time diff + await db.put(['test'], 'hello', 'world'); // Reset root key pair takes time await keyManager.resetRootKeyPair('password'); + expect(keyManager.getRecoveryCode()).toBeDefined(); const rootKeyPair2 = keyManager.getRootKeyPair(); const rootCert2 = keyManager.getRootCert(); expect(rootCert1.serialNumber).not.toBe(rootCert2.serialNumber); @@ -270,6 +332,9 @@ describe('KeyManager', () => { expect(keysUtils.publicKeyToPem(rootCert2.publicKey as PublicKey)).toBe( keysUtils.publicKeyToPem(rootKeyPair2.publicKey as PublicKey), ); + await db.stop(); + await db.start(); + expect(await db.get(['test'], 'hello')).toBe('world'); await keyManager.stop(); }); test('can renew root key pair', async () => { @@ -279,11 +344,18 @@ describe('KeyManager', () => { keysPath, logger, }); + const dbPath = `${dataDir}/db`; + const db = await DB.createDB({ + dbPath, + logger, + crypto: makeCrypto(keyManager.dbKey), + }); const rootKeyPair1 = keyManager.getRootKeyPair(); const rootCert1 = keyManager.getRootCert(); await sleep(2000); // Let's just make sure there is time diff - // renew root key pair takes time + await db.put(['test'], 'hello', 'world'); await keyManager.renewRootKeyPair('newpassword'); + expect(keyManager.getRecoveryCode()).toBeDefined(); const rootKeyPair2 = keyManager.getRootKeyPair(); const rootCert2 = keyManager.getRootCert(); expect(rootCert1.serialNumber).not.toBe(rootCert2.serialNumber); @@ -308,70 +380,125 @@ describe('KeyManager', () => { // Cert chain is ensured expect(keysUtils.certIssued(rootCert1, rootCert2)).toBe(true); expect(keysUtils.certVerified(rootCert1, rootCert2)).toBe(true); + await db.stop(); + await db.start(); + expect(await db.get(['test'], 'hello')).toBe('world'); await keyManager.stop(); }); - test( - 'order of certificate chain should be leaf to root', - async () => { + test('order of certificate chain should be leaf to root', async () => { + const keysPath = `${dataDir}/keys`; + const keyManager = await KeyManager.createKeyManager({ + password, + keysPath, + logger, + }); + const rootCertPem1 = keyManager.getRootCertPem(); + await sleep(2000); // Let's just make sure there is time diff + // renew root key pair takes time + await keyManager.renewRootKeyPair('newpassword'); + const rootCertPem2 = keyManager.getRootCertPem(); + await sleep(2000); // Let's just make sure there is time diff + // renew root key pair takes time + await keyManager.renewRootKeyPair('newnewpassword'); + const rootCertPem3 = keyManager.getRootCertPem(); + const rootCertChainPems = await keyManager.getRootCertChainPems(); + const rootCertChainPem = await keyManager.getRootCertChainPem(); + const rootCertChain = await keyManager.getRootCertChain(); + // The order should be from leaf to root + expect(rootCertChainPems).toStrictEqual([ + rootCertPem3, + rootCertPem2, + rootCertPem1, + ]); + expect(rootCertChainPem).toBe( + [rootCertPem3, rootCertPem2, rootCertPem1].join(''), + ); + const rootCertChainPems_ = rootCertChain.map((c) => { + return keysUtils.certToPem(c); + }); + expect(rootCertChainPems_).toStrictEqual([ + rootCertPem3, + rootCertPem2, + rootCertPem1, + ]); + await keyManager.stop(); + }); + describe('dbKey', () => { + test('Creates a key when started.', async () => { const keysPath = `${dataDir}/keys`; const keyManager = await KeyManager.createKeyManager({ password, keysPath, logger, }); - const rootCertPem1 = keyManager.getRootCertPem(); - await sleep(2000); // Let's just make sure there is time diff - // renew root key pair takes time - await keyManager.renewRootKeyPair('newpassword'); - const rootCertPem2 = keyManager.getRootCertPem(); - await sleep(2000); // Let's just make sure there is time diff - // renew root key pair takes time - await keyManager.renewRootKeyPair('newnewpassword'); - const rootCertPem3 = keyManager.getRootCertPem(); - const rootCertChainPems = await keyManager.getRootCertChainPems(); - const rootCertChainPem = await keyManager.getRootCertChainPem(); - const rootCertChain = await keyManager.getRootCertChain(); - // The order should be from leaf to root - expect(rootCertChainPems).toStrictEqual([ - rootCertPem3, - rootCertPem2, - rootCertPem1, - ]); - expect(rootCertChainPem).toBe( - [rootCertPem3, rootCertPem2, rootCertPem1].join(''), - ); - const rootCertChainPems_ = rootCertChain.map((c) => { - return keysUtils.certToPem(c); + expect(await fs.promises.readdir(keysPath)).toContain('db.key'); + expect(keyManager.dbKey.toString()).toBeTruthy(); + await keyManager.stop(); + }); + test('Throws an exception when it fails to parse the key.', async () => { + const keysPath = `${dataDir}/keys`; + const keyManager = await KeyManager.createKeyManager({ + password, + keysPath, + logger, }); - expect(rootCertChainPems_).toStrictEqual([ - rootCertPem3, - rootCertPem2, - rootCertPem1, - ]); + expect(await fs.promises.readdir(keysPath)).toContain('db.key'); + expect(keyManager.dbKey.toString()).toBeTruthy(); + await keyManager.stop(); + await expect( + KeyManager.createKeyManager({ + password: 'OtherPassword', + keysPath, + logger, + }), + ).rejects.toThrow(); await keyManager.stop(); - }, - global.defaultTimeout * 2 + 5000, - ); - test('generates a valid NodeId', async () => { - const keysPath = `${dataDir}/keys`; - const keyManager = await KeyManager.createKeyManager({ - password, - keysPath, - logger, }); - const nodeId = keyManager.getNodeId(); - isNodeId(nodeId); - makeNodeId(nodeId); - }); - test('destroyed prevents any further method calls', async () => { - const keysPath = `${dataDir}/keys`; - const keyManager = await KeyManager.createKeyManager({ - password: 'Password', - keysPath, - logger, + test('key remains unchanged when resetting keys.', async () => { + const keysPath = `${dataDir}/keys`; + const keyManager1 = await KeyManager.createKeyManager({ + password, + keysPath, + logger, + }); + expect(await fs.promises.readdir(keysPath)).toContain('db.key'); + expect(keyManager1.dbKey.toString()).toBeTruthy(); + const dbKey = keyManager1.dbKey; + + await keyManager1.resetRootKeyPair('NewPassword'); + expect(keyManager1.dbKey).toEqual(dbKey); + await keyManager1.stop(); + + const keyManager2 = await KeyManager.createKeyManager({ + password: 'NewPassword', + keysPath, + logger, + }); + expect(keyManager2.dbKey).toEqual(dbKey); + await keyManager2.stop(); + }); + test('key remains unchanged when renewing keys.', async () => { + const keysPath = `${dataDir}/keys`; + const keyManager1 = await KeyManager.createKeyManager({ + password, + keysPath, + logger, + }); + expect(await fs.promises.readdir(keysPath)).toContain('db.key'); + expect(keyManager1.dbKey.toString()).toBeTruthy(); + const dbKey = keyManager1.dbKey; + + await keyManager1.renewRootKeyPair('NewPassword'); + expect(keyManager1.dbKey).toEqual(dbKey); + await keyManager1.stop(); + + const keyManager2 = await KeyManager.createKeyManager({ + password: 'NewPassword', + keysPath, + logger, + }); + expect(keyManager2.dbKey).toEqual(dbKey); + await keyManager2.stop(); }); - await keyManager.stop(); - await keyManager.destroy(); - await expect(keyManager.renewRootKeyPair('NewPassword')).rejects.toThrow(); }); }); diff --git a/tests/keys/utils.test.ts b/tests/keys/utils.test.ts index 2508137a9..9d3e30a6e 100644 --- a/tests/keys/utils.test.ts +++ b/tests/keys/utils.test.ts @@ -70,4 +70,31 @@ describe('utils', () => { keysUtils.decryptPrivateKey(privateKeyPemEncrypted2, password3); }).toThrow(Error); }); + test('generates recovery code', async () => { + const recoveryCode = keysUtils.generateRecoveryCode(); + expect(recoveryCode.split(' ')).toHaveLength(24); + const recoveryCode24 = keysUtils.generateRecoveryCode(); + expect(recoveryCode24.split(' ')).toHaveLength(24); + const recoveryCode12 = keysUtils.generateRecoveryCode(12); + expect(recoveryCode12.split(' ')).toHaveLength(12); + }); + test( + 'generating key pair from recovery code is deterministic', + async () => { + const recoveryCode = keysUtils.generateRecoveryCode(12); + // Deterministic key pair generation can take between 4 to 10 seconds + const keyPair1 = await keysUtils.generateDeterministicKeyPair( + 256, + recoveryCode, + ); + const keyPair2 = await keysUtils.generateDeterministicKeyPair( + 256, + recoveryCode, + ); + const nodeId1 = keysUtils.publicKeyToFingerprint(keyPair1.publicKey); + const nodeId2 = keysUtils.publicKeyToFingerprint(keyPair2.publicKey); + expect(nodeId1).toBe(nodeId2); + }, + global.defaultTimeout * 2, + ); }); diff --git a/tests/nodes/NodeConnection.test.ts b/tests/nodes/NodeConnection.test.ts index bef0b62cd..97b96cc75 100644 --- a/tests/nodes/NodeConnection.test.ts +++ b/tests/nodes/NodeConnection.test.ts @@ -10,7 +10,6 @@ import { ForwardProxy, ReverseProxy } from '@/network'; import { NodeConnection, NodeManager } from '@/nodes'; import { VaultManager } from '@/vaults'; import { KeyManager } from '@/keys'; -import { utils as networkUtils } from '@/network'; import GRPCServer from '@/grpc/GRPCServer'; import { AgentServiceService, createAgentService } from '@/agent'; import { ACL } from '@/acl'; @@ -18,7 +17,6 @@ import { GestaltGraph } from '@/gestalts'; import { Sigchain } from '@/sigchain'; import { NotificationsManager } from '@/notifications'; -import * as grpcErrors from '@/grpc/errors'; import * as nodesUtils from '@/nodes/utils'; import * as nodesErrors from '@/nodes/errors'; import * as networkErrors from '@/network/errors'; @@ -26,7 +24,13 @@ import { makeNodeId } from '@/nodes/utils'; import { poll } from '@/utils'; import * as nodesTestUtils from './utils'; import { makeCrypto } from '../utils'; -// Import { poll } from '../utils'; + +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); describe('NodeConnection', () => { const password = 'password'; @@ -130,7 +134,7 @@ describe('NodeConnection', () => { dbPath: serverDbPath, fs: fs, logger: logger, - crypto: makeCrypto(serverKeyManager), + crypto: makeCrypto(serverKeyManager.dbKey), }); serverACL = await ACL.createACL({ db: serverDb, @@ -390,47 +394,6 @@ describe('NodeConnection', () => { ); await conn.destroy(); }); - test('sends hole punch message to connected target (expected to be broker, to relay further)', async () => { - const conn = await NodeConnection.createNodeConnection({ - targetNodeId: targetNodeId, - targetHost: targetHost, - targetPort: targetPort, - forwardProxy: clientFwdProxy, - keyManager: clientKeyManager, - logger: logger, - }); - await serverRevProxy.openConnection(sourceHost, sourcePort); - - const egressAddress = networkUtils.buildAddress( - clientFwdProxy.egressHost as Host, - clientFwdProxy.egressPort as Port, - ); - const signature = await clientKeyManager.signWithRootKeyPair( - Buffer.from(egressAddress), - ); - - // The targetNodeId ('NODEID') differs from the node ID of the connected target, - // indicating that this relay message is intended for another node. - // Expected to throw an error, as the connection to 1.1.1.1:11111 would not - // exist on the server's side. A broker is expected to have this pre-existing - // connection. - await expect( - async () => - await conn.sendHolePunchMessage( - sourceNodeId, - 'NODEID' as NodeId, - egressAddress, - signature, - ), - ).rejects.toThrow(grpcErrors.ErrorGRPCClientCall); - - await conn.stop(); - await serverRevProxy.closeConnection( - clientFwdProxy.egressHost, - clientFwdProxy.egressPort, - ); - await conn.destroy(); - }); test.skip('scans the servers vaults', async () => { // Const vault1 = await serverVaultManager.createVault('Vault1' as VaultName); // const vault2 = await serverVaultManager.createVault('Vault2' as VaultName); diff --git a/tests/nodes/NodeGraph.test.ts b/tests/nodes/NodeGraph.test.ts index 1b28dec63..7ddb4b6c0 100644 --- a/tests/nodes/NodeGraph.test.ts +++ b/tests/nodes/NodeGraph.test.ts @@ -15,6 +15,13 @@ import { makeNodeId } from '@/nodes/utils'; import * as nodesTestUtils from './utils'; import { makeCrypto } from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + // FIXME, some of these tests fail randomly. describe('NodeGraph', () => { const password = 'password'; @@ -120,7 +127,11 @@ describe('NodeGraph', () => { }, }); const dbPath = `${dataDir}/db`; - db = await DB.createDB({ dbPath, logger, crypto: makeCrypto(keyManager) }); + db = await DB.createDB({ + dbPath, + logger, + crypto: makeCrypto(keyManager.dbKey), + }); sigchain = await Sigchain.createSigchain({ keyManager: keyManager, db: db, @@ -469,63 +480,67 @@ describe('NodeGraph', () => { }, ]); }); - test('refreshes buckets', async () => { - const initialNodes: Record = {}; - // Generate and add some nodes - for (let i = 1; i < 255; i += 20) { - const newNodeId = nodesTestUtils.generateNodeIdForBucket( - nodeManager.getNodeId(), - i, - ); - const nodeAddress = { - ip: (i + '.' + i + '.' + i + '.' + i) as Host, - port: i as Port, - }; - await nodeGraph.setNode(newNodeId, nodeAddress); - initialNodes[newNodeId] = { - id: newNodeId, - address: nodeAddress, - distance: nodesUtils.calculateDistance( + test( + 'refreshes buckets', + async () => { + const initialNodes: Record = {}; + // Generate and add some nodes + for (let i = 1; i < 255; i += 20) { + const newNodeId = nodesTestUtils.generateNodeIdForBucket( nodeManager.getNodeId(), - newNodeId, - ), - }; - } - - // Renew the keypair - await keyManager.renewRootKeyPair('newPassword'); - // Reset the test's node ID state - nodeId = keyManager.getNodeId(); - // Refresh the buckets - await nodeGraph.refreshBuckets(); - - // Get all the new buckets, and expect that each node is in the correct bucket - const newBuckets = await nodeGraph.getAllBuckets(); - let nodeCount = 0; - for (const b of newBuckets) { - for (const n of Object.keys(b)) { - const nodeId = makeNodeId(n); - // Check that it was a node in the original DB - expect(initialNodes[nodeId]).toBeDefined(); - // Check it's in the correct bucket - const expectedIndex = nodesUtils.calculateBucketIndex( - nodeGraph.getNodeId(), - nodeId, - nodeGraph.nodeIdBits, + i, ); - const expectedBucket = await nodeGraph.getBucket(expectedIndex); - expect(expectedBucket).toBeDefined(); - expect(expectedBucket![nodeId]).toBeDefined(); - // Check it has the correct address - expect(b[nodeId].address).toEqual(initialNodes[nodeId].address); - nodeCount++; + const nodeAddress = { + ip: (i + '.' + i + '.' + i + '.' + i) as Host, + port: i as Port, + }; + await nodeGraph.setNode(newNodeId, nodeAddress); + initialNodes[newNodeId] = { + id: newNodeId, + address: nodeAddress, + distance: nodesUtils.calculateDistance( + nodeManager.getNodeId(), + newNodeId, + ), + }; } - } - // We had less than k (20) nodes, so we expect that all nodes will be re-added - // If we had more than k nodes, we may lose some of them (because the nodes - // may be re-added to newly full buckets) - expect(Object.keys(initialNodes).length).toEqual(nodeCount); - }); + + // Renew the keypair + await keyManager.renewRootKeyPair('newPassword'); + // Reset the test's node ID state + nodeId = keyManager.getNodeId(); + // Refresh the buckets + await nodeGraph.refreshBuckets(); + + // Get all the new buckets, and expect that each node is in the correct bucket + const newBuckets = await nodeGraph.getAllBuckets(); + let nodeCount = 0; + for (const b of newBuckets) { + for (const n of Object.keys(b)) { + const nodeId = makeNodeId(n); + // Check that it was a node in the original DB + expect(initialNodes[nodeId]).toBeDefined(); + // Check it's in the correct bucket + const expectedIndex = nodesUtils.calculateBucketIndex( + nodeGraph.getNodeId(), + nodeId, + nodeGraph.nodeIdBits, + ); + const expectedBucket = await nodeGraph.getBucket(expectedIndex); + expect(expectedBucket).toBeDefined(); + expect(expectedBucket![nodeId]).toBeDefined(); + // Check it has the correct address + expect(b[nodeId].address).toEqual(initialNodes[nodeId].address); + nodeCount++; + } + } + // We had less than k (20) nodes, so we expect that all nodes will be re-added + // If we had more than k nodes, we may lose some of them (because the nodes + // may be re-added to newly full buckets) + expect(Object.keys(initialNodes).length).toEqual(nodeCount); + }, + global.defaultTimeout * 4, + ); test('finds a single closest node', async () => { // New node added const newNode2Id = nodeId1; diff --git a/tests/nodes/NodeManager.test.ts b/tests/nodes/NodeManager.test.ts index 216f1393a..a7b41484e 100644 --- a/tests/nodes/NodeManager.test.ts +++ b/tests/nodes/NodeManager.test.ts @@ -9,7 +9,7 @@ import fs from 'fs'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { DB } from '@matrixai/db'; -import { KeyManager, utils as keysUtils } from '@/keys'; +import { KeyManager } from '@/keys'; import { NodeManager } from '@/nodes'; import { ForwardProxy, ReverseProxy } from '@/network'; import { Sigchain } from '@/sigchain'; @@ -17,9 +17,17 @@ import { sleep } from '@/utils'; import * as nodesErrors from '@/nodes/errors'; import * as claimsUtils from '@/claims/utils'; import { makeNodeId } from '@/nodes/utils'; +import * as keysUtils from '@/keys/utils'; import { makeCrypto } from '../utils'; import * as testUtils from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('NodeManager', () => { const password = 'password'; const logger = new Logger('NodeManagerTest', LogLevel.WARN, [ @@ -87,7 +95,11 @@ describe('NodeManager', () => { }, }); const dbPath = `${dataDir}/db`; - db = await DB.createDB({ dbPath, logger, crypto: makeCrypto(keyManager) }); + db = await DB.createDB({ + dbPath, + logger, + crypto: makeCrypto(keyManager.dbKey), + }); sigchain = await Sigchain.createSigchain({ keyManager, db, logger }); nodeManager = await NodeManager.createNodeManager({ diff --git a/tests/notifications/NotificationsManager.test.ts b/tests/notifications/NotificationsManager.test.ts index eea797dc2..025ef10ca 100644 --- a/tests/notifications/NotificationsManager.test.ts +++ b/tests/notifications/NotificationsManager.test.ts @@ -24,6 +24,13 @@ import * as networkUtils from '@/network/utils'; import { generateVaultId } from '@/vaults/utils'; import { makeCrypto } from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('NotificationsManager', () => { const password = 'password'; const node: NodeInfo = { @@ -123,7 +130,7 @@ describe('NotificationsManager', () => { dbPath: receiverDbPath, fs: fs, logger: logger, - crypto: makeCrypto(receiverKeyManager), + crypto: makeCrypto(receiverKeyManager.dbKey), }); receiverACL = await ACL.createACL({ db: receiverDb, @@ -215,7 +222,7 @@ describe('NotificationsManager', () => { dbPath: senderDbPath, fs, logger, - crypto: makeCrypto(senderKeyManager), + crypto: makeCrypto(senderKeyManager.dbKey), }); senderACL = await ACL.createACL({ db: senderDb, logger }); senderSigchain = await Sigchain.createSigchain({ diff --git a/tests/sessions/SessionManager.test.ts b/tests/sessions/SessionManager.test.ts index 90c54eae8..beaca9f0f 100644 --- a/tests/sessions/SessionManager.test.ts +++ b/tests/sessions/SessionManager.test.ts @@ -3,10 +3,17 @@ import os from 'os'; import path from 'path'; import { DB } from '@matrixai/db'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { KeyManager, utils as keysUtils } from '@/keys'; +import { KeyManager } from '@/keys'; import SessionManager from '@/sessions/SessionManager'; import * as sessionsErrors from '@/sessions/errors'; import { sleep } from '@/utils'; +import * as keysUtils from '@/keys/utils'; + +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); describe('SessionManager', () => { const password = 'password'; diff --git a/tests/sigchain/Sigchain.test.ts b/tests/sigchain/Sigchain.test.ts index 657481d98..c38a92333 100644 --- a/tests/sigchain/Sigchain.test.ts +++ b/tests/sigchain/Sigchain.test.ts @@ -12,6 +12,13 @@ import * as claimsUtils from '@/claims/utils'; import * as sigchainErrors from '@/sigchain/errors'; import { makeCrypto } from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('Sigchain', () => { const password = 'password'; const logger = new Logger('Sigchain Test', LogLevel.WARN, [ @@ -33,7 +40,11 @@ describe('Sigchain', () => { logger, }); const dbPath = `${dataDir}/db`; - db = await DB.createDB({ dbPath, logger, crypto: makeCrypto(keyManager) }); + db = await DB.createDB({ + dbPath, + logger, + crypto: makeCrypto(keyManager.dbKey), + }); }); afterEach(async () => { await db.stop(); diff --git a/tests/status/Status.test.ts b/tests/status/Status.test.ts index 41521b304..88f37d2ee 100644 --- a/tests/status/Status.test.ts +++ b/tests/status/Status.test.ts @@ -1,28 +1,37 @@ +import type { NodeId } from '@/nodes/types'; +import type { Host, Port } from '@/network/types'; + import fs from 'fs'; import os from 'os'; import path from 'path'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import * as statusErrors from '@/status/errors'; +import * as utilErrors from '@/utils/errors'; +import { sleep } from '@/utils'; import { Status } from '../../src/status'; describe('Lockfile is', () => { const logger = new Logger('Lockfile Test', LogLevel.WARN, [ new StreamHandler(), ]); + const waitForTimeout = 1000; let dataDir: string; let status: Status; + let statusPath: string; beforeEach(async () => { dataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'status-test-')); - status = await Status.createStatus({ - nodePath: dataDir, + statusPath = path.join(dataDir, 'status'); + status = new Status({ + statusPath, fs: fs, logger: logger, }); }); afterEach(async () => { + await status.stop({}); await fs.promises.rm(dataDir, { force: true, recursive: true, @@ -34,103 +43,200 @@ describe('Lockfile is', () => { }); test('starting and stopping with correct side effects', async () => { - await status.start(); - expect(fs.existsSync(status.lockPath)).toBe(true); - - await status.stop(); - expect(fs.existsSync(status.lockPath)).toBe(false); + await status.start({ pid: 0 }); + await status.readStatus(); + expect(fs.existsSync(status.statusPath)).toBe(true); + + await status.stop({ lol: 2 }); + await sleep(1000); + expect(fs.existsSync(status.statusPath)).toBe(true); + const state = await status.readStatus(); + expect(state?.status).toEqual('DEAD'); }); test('updating data and parsing it correctly', async () => { - let lock; - await status.start(); - lock = await status.parseStatus(); - expect(lock!.pid).toBeTruthy(); - - await status.updateStatus('grpcHost', 'localhost'); - await status.updateStatus('grpcPort', 12345); - await status.updateStatus('anything', 'something'); + await status.start({ pid: 0 }); + const lock1 = await status.readStatus(); + expect(lock1?.data.pid).toBeDefined(); + + await status.finishStart({ + pid: 0, + nodeId: 'node' as NodeId, + clientHost: '::1' as Host, + clientPort: 0 as Port, + grpcHost: 'localhost', + grpcPort: 12345, + anything: 'something', + }); - lock = await status.parseStatus(); - if (lock) { - expect(lock.pid).toBeTruthy(); - expect(lock.grpcHost).toBe('localhost'); - expect(lock.grpcPort).toBe(12345); - expect(lock.anything).toBe('something'); + const lock2 = await status.readStatus(); + if (lock2) { + expect(lock2.data.pid).toBeDefined(); + expect(lock2.data.grpcHost).toBe('localhost'); + expect(lock2.data.grpcPort).toBe(12345); + expect(lock2.data.anything).toBe('something'); } else { throw new Error('Lock should exist'); } - await status.stop(); + await status.stop({}); }); test('Working fine when a status already exists', async () => { await fs.promises.writeFile( - status.lockPath, + status.statusPath, JSON.stringify({ pid: 66666 }), ); - await status.start(); + await status.start({ pid: 0 }); let lock; - lock = await status.parseStatus(); + lock = await status.readStatus(); if (lock) { - expect(lock.pid).toBeTruthy(); + expect(lock.data.pid).toBeDefined(); } else { throw new Error('Lock should exist'); } - await status.updateStatus('grpcHost', 'localhost'); - await status.updateStatus('grpcPort', 12345); - await status.updateStatus('anything', 'something'); + await status.finishStart({ + pid: 0, + nodeId: 'node' as NodeId, + clientHost: '::1' as Host, + clientPort: 0 as Port, + grpcHost: 'localhost', + grpcPort: 12345, + anything: 'something', + }); - lock = await status.parseStatus(); + lock = await status.readStatus(); if (lock) { - expect(lock.pid).toBeTruthy(); - expect(lock.grpcHost).toBe('localhost'); - expect(lock.grpcPort).toBe(12345); - expect(lock.anything).toBe('something'); + expect(lock.data.pid).toBeDefined(); + expect(lock.data.grpcHost).toBe('localhost'); + expect(lock.data.grpcPort).toBe(12345); + expect(lock.data.anything).toBe('something'); } else { throw new Error('Lock should exist'); } - await status.stop(); + await status.stop({}); }); test('A running status holds a lock', async () => { // Make sure that the status is running - await status.start(); + await status.start({ pid: 0 }); // Try to start a new status. // Creation should succeed. - const status2 = await Status.createStatus({ - nodePath: dataDir, + const status2 = new Status({ + statusPath: path.join(dataDir, 'status'), fs: fs, logger: logger, }); // Should be able to read the lock info. - const info = await status2.parseStatus(); - expect(info).toBeTruthy(); - expect(info?.pid).toBeTruthy(); + const info = await status2.readStatus(); + expect(info).toBeDefined(); + expect(info?.data.pid).toBeDefined(); // Should fail to start a new lock. - await expect(() => status2.start()).rejects.toThrow( - statusErrors.ErrorStatusLockFailed, + await expect(() => status2.start({ pid: 0 })).rejects.toThrow( + statusErrors.ErrorStatusLocked, ); }); test('Lockfile has multiple states.', async () => { // Should be starting now. - await status.start(); - expect(await status.checkStatus()).toEqual('STARTING'); + await status.start({ pid: 0 }); + expect((await status.readStatus())?.status).toEqual('STARTING'); // Should be running. - await status.finishStart(); - expect(await status.checkStatus()).toEqual('RUNNING'); + await status.finishStart({ + clientHost: '' as Host, + clientPort: 0 as Port, + nodeId: '' as NodeId, + pid: 0, + }); + expect((await status.readStatus())?.status).toEqual('LIVE'); // Should be stopping. - await status.beginStop(); - expect(await status.checkStatus()).toEqual('STOPPING'); + await status.beginStop({ pid: 0 }); + expect((await status.readStatus())?.status).toEqual('STOPPING'); // Should be removed now. - await status.stop(); - expect(await status.checkStatus()).toEqual('UNLOCKED'); + await status.stop({}); + expect((await status.readStatus())?.status).toEqual('DEAD'); + }); + test('Status can wait for its status to be LIVE if started.', async () => { + // We want to mimic the startup procedure. + const delayedStart = async () => { + await status.start({ pid: 0 }); + await sleep(500); + await status.finishStart({ + clientHost: '' as Host, + clientPort: 0 as Port, + nodeId: '' as NodeId, + pid: 0, + }); + }; + const prom = delayedStart(); + + const test = await status.waitFor('LIVE', waitForTimeout); + expect(test.status).toEqual('LIVE'); + await prom; + + // Checking that we throw an error when we can't wait for RUNNING. + const delayedStop = async () => { + await status.beginStop({ pid: 0 }); + await sleep(500); + await status.stop({}); + }; + const prom2 = delayedStop(); + const test2 = status.waitFor('LIVE', waitForTimeout); + await expect(async () => { + await test2; + }).rejects.toThrow(utilErrors.ErrorUtilsPollTimeout); + await prom2; + + // Should throw if no file was found / unlocked. + const test3 = status.waitFor('LIVE', waitForTimeout); + await expect(async () => { + await test3; + }).rejects.toThrow(utilErrors.ErrorUtilsPollTimeout); + }); + test('Status can wait for its status to be DEAD if Stopping.', async () => { + // Should succeed if not started. + const test4 = await status.waitFor('DEAD', waitForTimeout); + expect(test4.status).toEqual('DEAD'); + + // Should throw an error when starting. + await status.start({ pid: 0 }); + const test = status.waitFor('LIVE', waitForTimeout); + await expect(async () => { + await test; + }).rejects.toThrow(utilErrors.ErrorUtilsPollTimeout); + + // Should throw an error whens started. + await status.start({ pid: 0 }); + const test2 = status.waitFor('DEAD', waitForTimeout); + await expect(async () => { + await test2; + }).rejects.toThrow(utilErrors.ErrorUtilsPollTimeout); + + // Should wait and succeed when stopping. + const delayedStart = async () => { + await status.beginStop({ pid: 0 }); + await sleep(500); + await status.stop({}); + }; + const prom2 = delayedStart(); + const test3 = await status.waitFor('DEAD', waitForTimeout); + expect(test3.status).toEqual('DEAD'); + await prom2; + }); + test('should throw an error when failing to parse.', async () => { + // Creating the status file. + await status.start({ pid: 0 }); + // Corrupting the status file. + await fs.promises.writeFile(statusPath, '{'); + // Should throw. + await expect(() => status.readStatus()).rejects.toThrow( + statusErrors.ErrorStatusParse, + ); }); }); diff --git a/tests/utils.ts b/tests/utils.ts index 9fd1ef1d4..debd93584 100644 --- a/tests/utils.ts +++ b/tests/utils.ts @@ -1,10 +1,8 @@ import type Logger from '@matrixai/logger'; import type { NodeAddress } from '@/nodes/types'; -import type { KeyManager } from '@/keys'; import os from 'os'; import path from 'path'; import fs from 'fs'; -import { sleep } from '@/utils'; import { utils as keyUtils } from '@/keys'; import { PolykeyAgent } from '../src'; @@ -31,11 +29,12 @@ async function setupRemoteKeynode({ path.join(os.tmpdir(), 'polykey-test-remote-'), ); } - return await PolykeyAgent.createPolykeyAgent({ + const agent = await PolykeyAgent.createPolykeyAgent({ password: 'password', nodePath: nodeDir, logger: logger, }); + return agent; } /** @@ -62,23 +61,9 @@ async function addRemoteDetails( } as NodeAddress); } -async function poll( - timeout: number, - condition: () => Promise, - delay: number = 1000, -) { - let timeProgress = 0; - while (timeProgress < timeout) { - if (await condition()) break; - await sleep(delay); - timeProgress += delay; - } - expect(await condition()).toBeTruthy(); -} - -function makeCrypto(keyManager: KeyManager) { +function makeCrypto(dbKey: Buffer) { return { - key: keyManager.dbKey, + key: dbKey, ops: { encrypt: keyUtils.encryptWithKey, decrypt: keyUtils.decryptWithKey, @@ -90,6 +75,5 @@ export { setupRemoteKeynode, cleanupRemoteKeynode, addRemoteDetails, - poll, makeCrypto, }; diff --git a/tests/vaults/VaultInternal.test.ts b/tests/vaults/VaultInternal.test.ts index f557a9681..8c0489e6b 100644 --- a/tests/vaults/VaultInternal.test.ts +++ b/tests/vaults/VaultInternal.test.ts @@ -11,6 +11,13 @@ import * as vaultsErrors from '@/vaults/errors'; import { sleep } from '@/utils'; import { KeyManager } from '@/keys'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('VaultInternal', () => { let dataDir: string; let dbPath: string; diff --git a/tests/vaults/VaultManager.test.ts b/tests/vaults/VaultManager.test.ts index b48c9f6e3..df9537c70 100644 --- a/tests/vaults/VaultManager.test.ts +++ b/tests/vaults/VaultManager.test.ts @@ -26,6 +26,13 @@ import { utils as vaultUtils } from '@/vaults'; import { makeVaultId } from '@/vaults/utils'; import { makeCrypto } from '../utils'; +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); + describe('VaultManager', () => { const password = 'password'; const logger = new Logger('VaultManager Test', LogLevel.WARN, [ @@ -100,7 +107,7 @@ describe('VaultManager', () => { db = await DB.createDB({ dbPath: dbPath, logger: logger, - crypto: makeCrypto(keyManager), + crypto: makeCrypto(keyManager.dbKey), }); sigchain = await Sigchain.createSigchain({ @@ -583,7 +590,7 @@ describe('VaultManager', () => { targetDb = await DB.createDB({ dbPath: path.join(targetDataDir, 'db'), logger: logger, - crypto: makeCrypto(keyManager), + crypto: makeCrypto(keyManager.dbKey), }); targetSigchain = await Sigchain.createSigchain({ keyManager: targetKeyManager, @@ -674,7 +681,7 @@ describe('VaultManager', () => { altDb = await DB.createDB({ dbPath: path.join(altDataDir, 'db'), logger: logger, - crypto: makeCrypto(keyManager), + crypto: makeCrypto(keyManager.dbKey), }); altSigchain = await Sigchain.createSigchain({ keyManager: altKeyManager, diff --git a/tests/vaults/VaultOps.test.ts b/tests/vaults/VaultOps.test.ts index f36027d84..f92839bf2 100644 --- a/tests/vaults/VaultOps.test.ts +++ b/tests/vaults/VaultOps.test.ts @@ -10,7 +10,14 @@ import * as errors from '@/vaults/errors'; import { VaultInternal, vaultOps } from '@/vaults'; import { KeyManager } from '@/keys'; import { generateVaultId } from '@/vaults/utils'; -import { getRandomBytes } from '@/keys/utils'; +import * as keysUtils from '@/keys/utils'; + +// Mocks. +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); describe('VaultOps', () => { const password = 'password'; @@ -292,7 +299,7 @@ describe('VaultOps', () => { await fs.promises.writeFile(path.join(secretDir, name), content); } - await vaultOps.addSecretDirectory(vault, secretDir); + await vaultOps.addSecretDirectory(vault, secretDir, fs); expect((await vaultOps.listSecrets(vault)).sort()).toStrictEqual( [ @@ -370,10 +377,10 @@ describe('VaultOps', () => { ); const secretDirName = path.basename(secretDir); const name = 'secret'; - const content = await getRandomBytes(5); + const content = await keysUtils.getRandomBytes(5); await fs.promises.writeFile(path.join(secretDir, name), content); - await vaultOps.addSecretDirectory(vault, secretDir); + await vaultOps.addSecretDirectory(vault, secretDir, fs); await expect( vault.access((efs) => efs.readdir(secretDirName)), ).resolves.toContain('secret'); @@ -410,7 +417,7 @@ describe('VaultOps', () => { 'secret5', ); - await vaultOps.addSecretDirectory(vault, path.join(secretDir)); + await vaultOps.addSecretDirectory(vault, path.join(secretDir), fs); const list = await vaultOps.listSecrets(vault); expect(list.sort()).toStrictEqual( [ @@ -459,7 +466,7 @@ describe('VaultOps', () => { path.join(secretDirName, 'secret1'), 'blocking-secret', ); - await vaultOps.addSecretDirectory(vault, secretDir); + await vaultOps.addSecretDirectory(vault, secretDir, fs); const list = await vaultOps.listSecrets(vault); expect(list.sort()).toStrictEqual( [ @@ -503,7 +510,7 @@ describe('VaultOps', () => { path.join(secretDirName, 'secret 9'), 'secret-content', ); - await vaultOps.addSecretDirectory(vault, secretDir); + await vaultOps.addSecretDirectory(vault, secretDir, fs); for (let j = 0; j < 8; j++) { await expect( From e7c57a780c217a0edf7cd60e667d1521cec89df9 Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Fri, 3 Dec 2021 17:29:18 +1100 Subject: [PATCH 2/8] Default background stdout and stderr is ignored now --- src/bin/agent/CommandStart.ts | 24 +++++++++++++++--------- src/bin/polykeyAgent.ts | 2 +- src/bin/utils/options.ts | 9 --------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/bin/agent/CommandStart.ts b/src/bin/agent/CommandStart.ts index f2024769e..3c6d12d1c 100644 --- a/src/bin/agent/CommandStart.ts +++ b/src/bin/agent/CommandStart.ts @@ -1,3 +1,4 @@ +import type { StdioOptions } from 'child_process'; import type { AgentChildProcessInput, AgentChildProcessOutput } from '../types'; import type PolykeyAgent from '../../PolykeyAgent'; import type { RecoveryCode } from '../../keys/types'; @@ -56,16 +57,21 @@ class CommandStart extends CommandPolykey { }; let recoveryCodeOut: RecoveryCode | undefined; if (options.background) { - let agentOutPath = path.join(options.nodePath, 'out.log'); - let agentErrPath = path.join(options.nodePath, 'err.log'); - if (options.backgroundOutFile) { - agentOutPath = options.backgroundOutFile; + const stdio: StdioOptions = ['ignore', 'ignore', 'ignore', 'ipc']; + if (options.backgroundOutFile != null) { + const agentOutFile = await this.fs.promises.open( + options.backgroundOutFile, + 'w', + ); + stdio[1] = agentOutFile.fd; } - if (options.backgroundErrFile) { - agentErrPath = options.backgroundErrFile; + if (options.backgroundErrFile != null) { + const agentErrFile = await this.fs.promises.open( + options.backgroundErrFile, + 'w', + ); + stdio[2] = agentErrFile.fd; } - const agentOutFile = await this.fs.promises.open(agentOutPath, 'w'); - const agentErrFile = await this.fs.promises.open(agentErrPath, 'w'); const agentProcess = child_process.fork( path.join(__dirname, '../polykeyAgent.ts'), [], @@ -74,7 +80,7 @@ class CommandStart extends CommandPolykey { env: process.env, detached: true, serialization: 'advanced', - stdio: ['ignore', agentOutFile.fd, agentErrFile.fd, 'ipc'], + stdio, }, ); const { diff --git a/src/bin/polykeyAgent.ts b/src/bin/polykeyAgent.ts index 2ad0a2892..3e67421dd 100644 --- a/src/bin/polykeyAgent.ts +++ b/src/bin/polykeyAgent.ts @@ -31,7 +31,7 @@ const logger = new Logger('polykey', undefined, [new StreamHandler()]); */ async function main(_argv = process.argv): Promise { const exitHandlers = new binUtils.ExitHandlers(); - const processSend = promisify(process.send!); + const processSend = promisify(process.send!.bind(process)); const { p: messageInP, resolveP: resolveMessageInP } = promise(); process.once('message', (data) => { diff --git a/src/bin/utils/options.ts b/src/bin/utils/options.ts index 04fb13d2f..02bbd939c 100644 --- a/src/bin/utils/options.ts +++ b/src/bin/utils/options.ts @@ -3,7 +3,6 @@ * Use PolykeyCommand.addOption or PolykeyCommand.addArgument * @module */ -import path from 'path'; import commander from 'commander'; import * as binParsers from './parsers'; import config from '../../config'; @@ -92,19 +91,11 @@ const background = new commander.Option( const backgroundOutFile = new commander.Option( '-bof, --background-out-file ', 'Path to STDOUT for agent process', -).default( - config.defaults.nodePath != null - ? path.join(config.defaults.nodePath!, 'out.log') - : undefined, ); const backgroundErrFile = new commander.Option( '-bef, --background-err-file ', 'Path to STDERR for agent process', -).default( - config.defaults.nodePath != null - ? path.join(config.defaults.nodePath!, 'err.log') - : undefined, ); export { From 55b804e23a4ebec6c48ec30bf02e977739bde763 Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Mon, 6 Dec 2021 16:05:34 +1100 Subject: [PATCH 3/8] Added --root-key-pair-bits, refactored tests for starting in background and concurrent coalescing, propagate log level to background agent --- src/bin/agent/CommandStart.ts | 9 +- src/bin/{polykeyAgent.ts => polykey-agent.ts} | 3 + src/bin/polykey.ts | 2 + src/bin/types.ts | 2 + src/bin/utils/options.ts | 10 +- src/status/errors.ts | 8 +- tests/bin/agent/agent.test.ts | 203 ----------------- tests/bin/agent/start.test.ts | 205 +++++++++++++++++- tests/bin/utils.ts | 88 +++++++- tests/setup.ts | 26 +++ 10 files changed, 343 insertions(+), 213 deletions(-) rename src/bin/{polykeyAgent.ts => polykey-agent.ts} (98%) diff --git a/src/bin/agent/CommandStart.ts b/src/bin/agent/CommandStart.ts index 3c6d12d1c..0143e384d 100644 --- a/src/bin/agent/CommandStart.ts +++ b/src/bin/agent/CommandStart.ts @@ -18,6 +18,7 @@ class CommandStart extends CommandPolykey { this.name('start'); this.description('Start the Polykey Agent'); this.addOption(binOptions.recoveryCodeFile); + this.addOption(binOptions.rootKeyPairBits); this.addOption(binOptions.clientHost); this.addOption(binOptions.clientPort); this.addOption(binOptions.ingressHost); @@ -45,6 +46,7 @@ class CommandStart extends CommandPolykey { password, nodePath: options.nodePath, keysConfig: { + rootKeyPairBits: options.rootKeyPairBits, recoveryCode: recoveryCodeIn, }, networkConfig: { @@ -73,7 +75,7 @@ class CommandStart extends CommandPolykey { stdio[2] = agentErrFile.fd; } const agentProcess = child_process.fork( - path.join(__dirname, '../polykeyAgent.ts'), + path.join(__dirname, '../polykey-agent'), [], { cwd: process.cwd(), @@ -112,7 +114,7 @@ class CommandStart extends CommandPolykey { new binErrors.ErrorCLIPolykeyAgentProcess(e.message), ); }); - // If the process exits during initial execution of polykeyAgent script + // If the process exits during initial execution of polykey-agent script // Then it is an exception, because the agent process is meant to be a long-running daemon agentProcess.once('close', (code, signal) => { rejectAgentProcessP( @@ -126,6 +128,7 @@ class CommandStart extends CommandPolykey { ); }); const messageIn: AgentChildProcessInput = { + logLevel: this.logger.getEffectiveLevel(), agentConfig, }; agentProcess.send(messageIn, (e) => { @@ -138,6 +141,8 @@ class CommandStart extends CommandPolykey { }); await agentProcessP; } else { + // Change process name to polykey-agent + process.title = 'polykey-agent'; // eslint-disable-next-line prefer-const let pkAgent: PolykeyAgent | undefined; this.exitHandlers.handlers.push(async () => { diff --git a/src/bin/polykeyAgent.ts b/src/bin/polykey-agent.ts similarity index 98% rename from src/bin/polykeyAgent.ts rename to src/bin/polykey-agent.ts index 3e67421dd..7e06e3a6f 100644 --- a/src/bin/polykeyAgent.ts +++ b/src/bin/polykey-agent.ts @@ -24,6 +24,8 @@ import PolykeyAgent from '../PolykeyAgent'; import ErrorPolykey from '../ErrorPolykey'; import { promisify, promise } from '../utils'; +process.title = 'polykey-agent'; + const logger = new Logger('polykey', undefined, [new StreamHandler()]); /** @@ -38,6 +40,7 @@ async function main(_argv = process.argv): Promise { resolveMessageInP(data); }); const messageIn = await messageInP; + logger.setLevel(messageIn.logLevel); let pkAgent: PolykeyAgent; exitHandlers.handlers.push(async () => { if (pkAgent != null) await pkAgent.stop(); diff --git a/src/bin/polykey.ts b/src/bin/polykey.ts index 489a5ead7..ac40d5373 100644 --- a/src/bin/polykey.ts +++ b/src/bin/polykey.ts @@ -27,6 +27,8 @@ import * as binUtils from './utils'; import ErrorPolykey from '../ErrorPolykey'; import config from '../config'; +process.title = 'polykey'; + async function main(argv = process.argv): Promise { // Registers signal and process error handler // Any resource cleanup must be resolved within their try-catch block diff --git a/src/bin/types.ts b/src/bin/types.ts index 9e23a6307..79c281887 100644 --- a/src/bin/types.ts +++ b/src/bin/types.ts @@ -1,3 +1,4 @@ +import type { LogLevel } from '@matrixai/logger'; import type { POJO } from '../types'; import type { RecoveryCode } from '../keys/types'; import type { Host, Port } from '../network/types'; @@ -9,6 +10,7 @@ import type { Host, Port } from '../network/types'; * However traditional classes cannot be */ type AgentChildProcessInput = { + logLevel: LogLevel; agentConfig: { password: string; nodePath?: string; diff --git a/src/bin/utils/options.ts b/src/bin/utils/options.ts index 02bbd939c..d0c70a0c4 100644 --- a/src/bin/utils/options.ts +++ b/src/bin/utils/options.ts @@ -17,7 +17,9 @@ import config from '../../config'; const nodePath = new commander.Option( '-np, --node-path ', 'Path to Node State', -).default(config.defaults.nodePath); +) + .env('PK_NODE_PATH') + .default(config.defaults.nodePath); /** * Formatting choice of human, json, defaults to human @@ -98,6 +100,11 @@ const backgroundErrFile = new commander.Option( 'Path to STDERR for agent process', ); +const rootKeyPairBits = new commander.Option( + '-rkpb --root-key-pair-bits ', + 'Bit size of root key pair', +).argParser(binParsers.parseNumber); + export { nodePath, format, @@ -112,4 +119,5 @@ export { background, backgroundOutFile, backgroundErrFile, + rootKeyPairBits, }; diff --git a/src/status/errors.ts b/src/status/errors.ts index f2eccb171..216958b42 100644 --- a/src/status/errors.ts +++ b/src/status/errors.ts @@ -5,22 +5,22 @@ class ErrorStatus extends ErrorPolykey {} class ErrorStatusNotRunning extends ErrorStatus {} class ErrorStatusLocked extends ErrorStatus { - decription = 'Status is locked by another process'; + description = 'Status is locked by another process'; exitCode = sysexits.TEMPFAIL; } class ErrorStatusRead extends ErrorStatus { - decription = 'Failed to read status info'; + description = 'Failed to read status info'; exitCode = sysexits.IOERR; } class ErrorStatusWrite extends ErrorStatus { - decription = 'Failed to write status info'; + description = 'Failed to write status info'; exitCode = sysexits.IOERR; } class ErrorStatusParse extends ErrorStatus { - decription = 'Failed to parse status info'; + description = 'Failed to parse status info'; exitCode = sysexits.CONFIG; } diff --git a/tests/bin/agent/agent.test.ts b/tests/bin/agent/agent.test.ts index 6b455f520..7e5ebbcf3 100644 --- a/tests/bin/agent/agent.test.ts +++ b/tests/bin/agent/agent.test.ts @@ -75,209 +75,6 @@ // recursive: true, // }); // }); -// describe('Starting the agent in the foreground', () => { -// test( -// 'should start the agent and clean up the lockfile when a kill signal is received', -// async () => { -// const agent = testUtils.pkExec([ -// 'agent', -// 'start', -// '-np', -// foregroundNodePath, -// '--password-file', -// passwordFile, -// ]); -// const status = new Status({ -// statusPath: statusPath(foregroundNodePath), -// fs, -// logger, -// }); -// await status.waitFor('LIVE', waitForTimeout); - -// // Kill. -// await killAgent(foregroundNodePath, passwordFile); - -// const agentResult = await agent; // Waiting for agent to finish running. -// await status.waitFor('DEAD', waitForTimeout); - -// expect(agentResult.stdout.split(' ')).toHaveLength(24); -// }, -// global.polykeyStartupTimeout * 4, -// ); -// test( -// 'should start with port and host information as flags', -// async () => { -// const agent = testUtils.pkStdio([ -// 'agent', -// 'start', -// '-np', -// foregroundNodePath, -// '--password-file', -// passwordFile, -// '-ch', -// '127.0.0.1', -// '-cp', -// '55556', -// '-ih', -// '127.0.0.2', -// '127.0.0.3', -// '-ip', -// '55555', -// ]); -// const status = new Status({ -// statusPath: statusPath(foregroundNodePath), -// fs, -// logger, -// }); -// const statusConfig = await status.waitFor('LIVE', waitForTimeout); -// expect(statusConfig.data.clientHost).toEqual('127.0.0.1'); -// expect(statusConfig.data.clientPort).toEqual(55556); - -// // Kill externally. -// await killAgent(foregroundNodePath, passwordFile); -// await agent; // Waiting for agent to finish running. -// await status.waitFor('DEAD', waitForTimeout); -// }, -// global.polykeyStartupTimeout * 4, -// ); -// test( -// 'should start with port and host information from env', -// async () => { -// const agent = testUtils.pkExec( -// [ -// 'agent', -// 'start', -// '-np', -// foregroundNodePath, -// '--password-file', -// passwordFile, -// ], -// { -// PK_CLIENT_HOST: '127.0.0.1', -// PK_CLIENT_PORT: '55556', -// PK_INGRESS_HOST: '127.0.0.2', -// PK_INGRESS_PORT: '55555', -// }, -// ); - -// const status = new Status({ -// statusPath: statusPath(foregroundNodePath), -// fs, -// logger, -// }); -// const statusConfig = await status.waitFor('LIVE', waitForTimeout); -// expect(statusConfig.data.clientHost).toEqual('127.0.0.1'); -// expect(statusConfig.data.clientPort).toEqual(55556); - -// // Kill externally. -// await killAgent(foregroundNodePath, passwordFile); -// await agent; // Waiting for agent to finish running. -// await status.waitFor('DEAD', waitForTimeout); -// }, -// global.polykeyStartupTimeout * 4, -// ); -// test('should fail to start if an agent is already running at the path', async () => { -// const result = await testUtils.pkStdio([ -// 'agent', -// 'start', -// '-np', -// activeNodePath, -// '--password-file', -// passwordFile, -// ]); -// expect(result.exitCode).toBe(75); -// }); -// }); -// describe('Starting the agent in the background', () => { -// test( -// 'should start the agent and clean up the lockfile when a kill signal is received', -// async () => { -// const commands = [ -// 'agent', -// 'start', -// '-b', -// '-np', -// backgroundNodePath, -// '--password-file', -// passwordFile, -// ]; - -// // We can await this since it should finish after spawning the background agent. -// const result = await testUtils.pkStdio(commands); -// expect(result.exitCode).toBe(0); - -// const status = new Status({ -// statusPath: statusPath(backgroundNodePath), -// fs, -// logger, -// }); -// await status.waitFor('LIVE', waitForTimeout); - -// // Kill it (with fire) externally. -// await killAgent(backgroundNodePath, passwordFile); -// await sleep(100); -// await status.waitFor('DEAD', waitForTimeout); - -// expect(result.stdout).toContain('This is your recovery code'); -// const code = result.stdout.split('\n')[3]; // Getting the recovery code line. -// expect(code.split(' ')).toHaveLength(24); - -// // Checking that the status was removed. FIXME -// // await poll(global.polykeyStartupTimeout * 2, async () => { -// // const files = await fs.promises.readdir(backgroundNodePath); -// // const test = files.includes('agent-status.json'); -// // return !test; -// // }); -// }, -// global.polykeyStartupTimeout * 5, -// ); -// test('Should fail to start if an agent is already running at the path', async () => { -// const commands = [ -// 'agent', -// 'start', -// '-b', -// '-np', -// activeNodePath, -// '--password-file', -// passwordFile, -// ]; -// // We can await this since it should finish after spawning the background agent. -// const result = await testUtils.pkStdio(commands); -// expect(result.exitCode).toBe(75); -// }); -// }); -// test( -// 'concurrent agent start', -// async () => { -// const nodePath = path.join(dataDir, 'third'); -// const commands = [ -// 'agent', -// 'start', -// '-np', -// nodePath, -// '--password-file', -// passwordFile, -// ]; - -// // We can await this since it should finish after spawning the background agent. -// const prom1 = testUtils.pkExec(commands); -// const prom2 = testUtils.pkExec(commands); - -// const status = new Status({ -// statusPath: statusPath(nodePath), -// fs, -// logger, -// }); -// await status.waitFor('LIVE', waitForTimeout); - -// // Kill externally. -// const results = await Promise.all([prom1, prom2]); // Waiting for agent to finish running. -// const resultsString = JSON.stringify(results); -// expect(resultsString).toContain(':75'); -// expect(resultsString).toContain(':0'); -// }, -// global.defaultTimeout * 4, -// ); // test( // 'concurrent agent start and bootstrap.', // async () => { diff --git a/tests/bin/agent/start.test.ts b/tests/bin/agent/start.test.ts index 8d4e61566..1e45fb1ad 100644 --- a/tests/bin/agent/start.test.ts +++ b/tests/bin/agent/start.test.ts @@ -4,6 +4,9 @@ import path from 'path'; import fs from 'fs'; import readline from 'readline'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { Status, errors as statusErrors } from '@/status'; +import * as binUtils from '@/bin/utils'; +import config from '@/config'; import * as testUtils from '../utils'; describe('start', () => { @@ -34,6 +37,8 @@ describe('start', () => { path.join(dataDir, 'polykey'), '--password-file', passwordPath, + '--root-key-pair-bits', + '1024', '--verbose', ], undefined, @@ -63,6 +68,204 @@ describe('start', () => { expect(exitCode).toBe(null); expect(signal).toBe('SIGTERM'); }, - global.defaultTimeout * 4, + global.defaultTimeout * 2, + ); + test( + 'start in foreground with environment variables', + async () => { + const password = 'abc123'; + const agentProcess = await testUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger, + ); + const rlOut = readline.createInterface(agentProcess.stdout!); + const recoveryCode = await new Promise( + (resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }, + ); + expect(typeof recoveryCode).toBe('string'); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + agentProcess.kill('SIGTERM'); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + }, + global.defaultTimeout * 2, + ); + test( + 'start in background with environment variables', + async () => { + const password = 'abc123'; + const agentProcess = await testUtils.pkSpawn( + [ + 'agent', + 'start', + '--root-key-pair-bits', + '1024', + '--background', + '--background-out-file', + path.join(dataDir, 'out.log'), + '--background-err-file', + path.join(dataDir, 'err.log'), + '--verbose', + ], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger, + ); + const agentProcessExit = new Promise((resolve, reject) => { + agentProcess.on('exit', (code, signal) => { + if (code === 0) { + resolve(); + } else { + reject( + new Error( + `Agent process exited with code: ${code} and signal: ${signal}`, + ), + ); + } + }); + }); + const rlOut = readline.createInterface(agentProcess.stdout!); + const recoveryCode = await new Promise( + (resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }, + ); + expect(typeof recoveryCode).toBe('string'); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + await agentProcessExit; + // Make sure that the daemon does output the recovery code + // The recovery code was already written out on agentProcess + const polykeyAgentOut = await fs.promises.readFile( + path.join(dataDir, 'out.log'), + 'utf-8', + ); + expect(polykeyAgentOut).toHaveLength(0); + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + fs, + logger, + }); + const statusInfo1 = (await status.readStatus())!; + expect(statusInfo1).toBeDefined(); + expect(statusInfo1.status).toBe('LIVE'); + process.kill(statusInfo1.data.pid, 'SIGTERM'); + const statusInfo2 = await status.waitFor('DEAD'); + expect(statusInfo2.status).toBe('DEAD'); + }, + global.defaultTimeout * 2, + ); + test( + 'concurrent starts are coalesced', + async () => { + const password = 'abc123'; + // One of these processes is blocked + const [agentProcess1, agentProcess2] = await Promise.all([ + testUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('agentProcess1'), + ), + testUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('agentProcess2'), + ), + ]); + // These will be the last line of STDERR + // The readline library will automatically trim off newlines + let stdErrLine1; + let stdErrLine2; + const rlErr1 = readline.createInterface(agentProcess1.stderr!); + const rlErr2 = readline.createInterface(agentProcess2.stderr!); + rlErr1.on('line', (l) => { + stdErrLine1 = l; + }); + rlErr2.on('line', (l) => { + stdErrLine2 = l; + }); + const [index, exitCode, signal] = await new Promise< + [number, number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess1.once('exit', (code, signal) => { + resolve([0, code, signal]); + }); + agentProcess2.once('exit', (code, signal) => { + resolve([1, code, signal]); + }); + }); + const errorStatusLocked = new statusErrors.ErrorStatusLocked(); + expect(exitCode).toBe(errorStatusLocked.exitCode); + expect(signal).toBe(null); + const eOutput = binUtils + .outputFormatter({ + type: 'error', + name: errorStatusLocked.name, + description: errorStatusLocked.description, + message: errorStatusLocked.message, + }) + .trim(); + // It's either the first or second process + if (index === 0) { + expect(stdErrLine1).toBeDefined(); + expect(stdErrLine1).toBe(eOutput); + agentProcess2.kill('SIGTERM'); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess2.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + } else if (index === 1) { + expect(stdErrLine2).toBeDefined(); + expect(stdErrLine2).toBe(eOutput); + agentProcess1.kill('SIGTERM'); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess1.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + } + }, + global.defaultTimeout * 2, ); }); diff --git a/tests/bin/utils.ts b/tests/bin/utils.ts index 1655569a9..7af49d24b 100644 --- a/tests/bin/utils.ts +++ b/tests/bin/utils.ts @@ -9,7 +9,9 @@ import * as mockProcess from 'jest-mock-process'; import mockedEnv from 'mocked-env'; import nexpect from 'nexpect'; import Logger from '@matrixai/logger'; -import main from '../../src/bin/polykey'; +import main from '@/bin/polykey'; +import * as binUtils from '@/bin/utils'; +import * as statusErrors from '@/status/errors'; /** * Runs pk command functionally @@ -277,4 +279,86 @@ async function pkExpect({ }); } -export { pk, pkStdio, pkExec, pkSpawn, pkExpect }; +/** + * Creates a PK agent running in the global path + * Use this in beforeAll, and use the result in afterAll + * Uses a references directory as a reference count + */ +async function pkAgent( + args: Array = [], + env: Record = {}, +) { + // The references directory will act like our reference count + try { + return await fs.promises.mkdir(path.join(global.binAgentDir, 'references')); + } catch (e) { + if (e.code !== 'EEXIST') { + throw e; + } + } + const reference = Math.floor(Math.random() * 1000).toString(); + // Plus 1 to the reference count + await fs.promises.writeFile( + path.join(global.binAgentDir, 'references', reference), + reference, + ); + const { exitCode, stderr } = await pkStdio( + [ + 'agent', + 'start', + // 1024 is the smallest size and is faster to start + '--root-key-pair-bits', + '1024', + ...args, + ], + { + PK_NODE_PATH: global.binAgentDir, + PK_PASSWORD: global.binAgentPassword, + ...env, + }, + global.binAgentDir, + ); + // If the status is locked, we can ignore the start call + if (exitCode !== 0) { + // Last line of STDERR + const stdErrLine = stderr.trim().split('\n').pop(); + const e = new statusErrors.ErrorStatusLocked(); + // Expected output for ErrorStatusLocked + const eOutput = binUtils + .outputFormatter({ + type: 'error', + name: e.name, + description: e.description, + message: e.message, + }) + .trim(); + if (exitCode !== e.exitCode || stdErrLine !== eOutput) { + // This should not happen + throw new Error('Failed to start Polykey Agent'); + } + } + return async () => { + await fs.promises.rm( + path.join(global.binAgentDir, 'references', reference), + ); + // If the pids directory is not empty, there are other processes still running + try { + await fs.promises.rmdir(path.join(global.binAgentDir, 'references')); + } catch (e) { + if (e.code === 'ENOTEMPTY') { + return; + } + throw e; + } + await pkStdio( + ['agent', 'stop', '--verbose'], + { + PK_NODE_PATH: global.binAgentDir, + PK_PASSWORD: global.binAgentPassword, + }, + global.binAgentDir, + ); + }; +} + +export { pk, pkStdio, pkExec, pkSpawn, pkExpect, pkAgent }; diff --git a/tests/setup.ts b/tests/setup.ts index 80ee90aae..ddd4f5693 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,3 +1,4 @@ +import os from 'os'; import path from 'path'; declare global { @@ -5,6 +6,8 @@ declare global { interface Global { projectDir: string; testDir: string; + binAgentDir: string; + binAgentPassword: string; defaultTimeout: number; polykeyStartupTimeout: number; failedConnectionTimeout: number; @@ -12,8 +15,31 @@ declare global { } } +/** + * Absolute directory to the project root + */ global.projectDir = path.join(__dirname, '../'); + +/** + * Absolute directory to the test root + */ global.testDir = __dirname; + +/** + * Absolute directory to a shared data directory used by bin tests + * This has to be a static path + * The setup.ts is copied into each test module + */ +global.binAgentDir = path.join(os.tmpdir(), 'polykey-test-bin'); + +/** + * Shared password for agent used by for bin tests + */ +global.binAgentPassword = 'hello world'; + +/** + * Default asynchronous test timeout + */ global.defaultTimeout = 20000; global.polykeyStartupTimeout = 30000; global.failedConnectionTimeout = 50000; From a692cc48db0ce2b6049bb55430bf2d4e56462bea Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Mon, 6 Dec 2021 16:26:00 +1100 Subject: [PATCH 4/8] Added concurrent bootstrap is coalesced test to agent start --- src/bin/bootstrap/CommandBootstrap.ts | 8 ++- src/bootstrap/utils.ts | 24 +++++-- tests/bin/agent/agent.test.ts | 44 ------------- tests/bin/agent/start.test.ts | 90 +++++++++++++++++++++++++++ tests/bin/bootstrap.test.ts | 37 ----------- tests/bootstrap/bootstrap.test.ts | 28 --------- 6 files changed, 115 insertions(+), 116 deletions(-) diff --git a/src/bin/bootstrap/CommandBootstrap.ts b/src/bin/bootstrap/CommandBootstrap.ts index b1db103b2..dde735aae 100644 --- a/src/bin/bootstrap/CommandBootstrap.ts +++ b/src/bin/bootstrap/CommandBootstrap.ts @@ -9,6 +9,7 @@ class CommandBootstrap extends CommandPolykey { this.name('bootstrap'); this.description('Bootstrap Keynode State'); this.addOption(binOptions.recoveryCodeFile); + this.addOption(binOptions.rootKeyPairBits); this.action(async (options) => { const bootstrapUtils = await import('../../bootstrap/utils'); const password = await binProcessors.processPassword( @@ -20,9 +21,12 @@ class CommandBootstrap extends CommandPolykey { this.fs, ); const recoveryCodeOut = await bootstrapUtils.bootstrapState({ - nodePath: options.nodePath, password, - recoveryCode: recoveryCodeIn, + nodePath: options.nodePath, + keysConfig: { + rootKeyPairBits: options.rootKeyPairBits, + recoveryCode: recoveryCodeIn, + }, fresh: options.fresh, fs: this.fs, logger: this.logger, diff --git a/src/bootstrap/utils.ts b/src/bootstrap/utils.ts index 3544bcb7e..abeeba02e 100644 --- a/src/bootstrap/utils.ts +++ b/src/bootstrap/utils.ts @@ -17,21 +17,28 @@ import { VaultManager } from '../vaults'; import { NotificationsManager } from '../notifications'; import { mkdirExists } from '../utils'; import config from '../config'; +import * as utils from '../utils'; +import * as errors from '../errors'; /** * Bootstraps the Node Path */ async function bootstrapState({ - nodePath, password, - recoveryCode, + nodePath = config.defaults.nodePath, + keysConfig = {}, fresh = false, fs = require('fs'), logger = new Logger(bootstrapState.name), }: { - nodePath: string; password: string; - recoveryCode?: RecoveryCode; + nodePath?: string; + keysConfig?: { + rootKeyPairBits?: number; + rootCertDuration?: number; + dbKeyBits?: number; + recoveryCode?: RecoveryCode; + }; fresh?: boolean; fs?: FileSystem; logger?: Logger; @@ -40,6 +47,13 @@ async function bootstrapState({ logger.info(`Setting umask to ${umask.toString(8).padStart(3, '0')}`); process.umask(umask); logger.info(`Setting node path to ${nodePath}`); + if (nodePath == null) { + throw new errors.ErrorUtilsNodePath(); + } + const keysConfig_ = { + ...config.defaults.keysConfig, + ...utils.filterEmptyObject(keysConfig), + }; await mkdirExists(fs, nodePath); // Setup node path and sub paths const statusPath = path.join(nodePath, config.defaults.statusBase); @@ -65,11 +79,11 @@ async function bootstrapState({ fresh, }); const keyManager = await KeyManager.createKeyManager({ + ...keysConfig_, keysPath, password, fs, logger: logger.getChild(KeyManager.name), - recoveryCode, fresh, }); const db = await DB.createDB({ diff --git a/tests/bin/agent/agent.test.ts b/tests/bin/agent/agent.test.ts index 7e5ebbcf3..f823eb4c5 100644 --- a/tests/bin/agent/agent.test.ts +++ b/tests/bin/agent/agent.test.ts @@ -75,50 +75,6 @@ // recursive: true, // }); // }); -// test( -// 'concurrent agent start and bootstrap.', -// async () => { -// const nodePath = path.join(dataDir, 'third'); -// const command1 = [ -// 'agent', -// 'start', -// '-np', -// nodePath, -// '--password-file', -// passwordFile, -// ]; -// const command2 = [ -// 'bootstrap', -// '-np', -// nodePath, -// '--password-file', -// passwordFile, -// ]; - -// // We can await this since it should finish after spawning the background agent. -// const prom1 = testUtils.pkExec(command1); -// const prom2 = testUtils.pkExec(command2); - -// const status = new Status({ -// statusPath: statusPath(nodePath), -// fs, -// logger, -// }); -// try { -// await status.waitFor('LIVE', 20000); -// } catch (e) { -// // Noop -// } -// // Kill externally. -// await killAgent(nodePath, passwordFile); - -// const results = await Promise.all([prom1, prom2]); // Waiting for agent to finish running. -// expect(results[0].exitCode).toBe(0); -// expect(results[1].exitCode).toBe(64); -// }, -// global.defaultTimeout * 5, -// ); - // describe('getting agent status', () => { // test('should get the status of an online agent', async () => { // const result = await testUtils.pkStdio([ diff --git a/tests/bin/agent/start.test.ts b/tests/bin/agent/start.test.ts index 1e45fb1ad..4df72eef4 100644 --- a/tests/bin/agent/start.test.ts +++ b/tests/bin/agent/start.test.ts @@ -268,4 +268,94 @@ describe('start', () => { }, global.defaultTimeout * 2, ); + test( + 'concurrent bootstrap is coalesced', + async () => { + const password = 'abc123'; + // One of these processes is blocked + const [agentProcess, bootstrapProcess] = await Promise.all([ + testUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('agentProcess'), + ), + testUtils.pkSpawn( + ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('bootstrapProcess'), + ), + ]); + // These will be the last line of STDERR + // The readline library will automatically trim off newlines + let stdErrLine1; + let stdErrLine2; + const rlErr1 = readline.createInterface(agentProcess.stderr!); + const rlErr2 = readline.createInterface(bootstrapProcess.stderr!); + rlErr1.on('line', (l) => { + stdErrLine1 = l; + }); + rlErr2.on('line', (l) => { + stdErrLine2 = l; + }); + const [index, exitCode, signal] = await new Promise< + [number, number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess.once('exit', (code, signal) => { + resolve([0, code, signal]); + }); + bootstrapProcess.once('exit', (code, signal) => { + resolve([1, code, signal]); + }); + }); + const errorStatusLocked = new statusErrors.ErrorStatusLocked(); + expect(exitCode).toBe(errorStatusLocked.exitCode); + expect(signal).toBe(null); + const eOutput = binUtils + .outputFormatter({ + type: 'error', + name: errorStatusLocked.name, + description: errorStatusLocked.description, + message: errorStatusLocked.message, + }) + .trim(); + + // It's either the first or second process + if (index === 0) { + expect(stdErrLine1).toBeDefined(); + expect(stdErrLine1).toBe(eOutput); + bootstrapProcess.kill('SIGTERM'); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + bootstrapProcess.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + } else if (index === 1) { + expect(stdErrLine2).toBeDefined(); + expect(stdErrLine2).toBe(eOutput); + agentProcess.kill('SIGTERM'); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + } + }, + global.defaultTimeout * 2, + ); }); diff --git a/tests/bin/bootstrap.test.ts b/tests/bin/bootstrap.test.ts index 976039fa7..122c3948e 100644 --- a/tests/bin/bootstrap.test.ts +++ b/tests/bin/bootstrap.test.ts @@ -71,43 +71,6 @@ describe('CLI bootstrap', () => { }, global.polykeyStartupTimeout * 3, ); - test('Should fail to create keynode state if keynode exists.', async () => { - const result = await utils.pkStdio([ - 'bootstrap', - '-np', - nodePath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(0); - - // Should fail here. - const result2 = await utils.pkStdio([ - 'bootstrap', - '-np', - nodePath, - '--password-file', - passwordFile, - ]); - expect(result2.exitCode).not.toBe(0); - expect(result2.stderr).toContain('ErrorBootstrapExistingState:'); - expect(result2.stderr).toContain('Node path is occupied'); - }); - test('Should fail to create keynode state if other files exists.', async () => { - await fs.promises.mkdir(path.join(nodePath, 'NOTAKEYNODEDIR'), { - recursive: true, - }); - const result = await utils.pkStdio([ - 'bootstrap', - '-np', - nodePath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain('ErrorBootstrapExistingState:'); - expect(result.stderr).toContain('Node path is occupied'); - }); test( 'concurrent bootstrapping', async () => { diff --git a/tests/bootstrap/bootstrap.test.ts b/tests/bootstrap/bootstrap.test.ts index f5ea0c65e..7bad74e73 100644 --- a/tests/bootstrap/bootstrap.test.ts +++ b/tests/bootstrap/bootstrap.test.ts @@ -2,14 +2,10 @@ import fs from 'fs'; import path from 'path'; import os from 'os'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; - import PolykeyAgent from '@/PolykeyAgent'; - -import * as bootstrapErrors from '@/bootstrap/errors'; import * as bootstrapUtils from '@/bootstrap/utils'; import { Status } from '@/status'; -// Mocks. jest.mock('@/keys/utils', () => ({ ...jest.requireActual('@/keys/utils'), generateDeterministicKeyPair: @@ -23,16 +19,6 @@ describe('Bootstrap', () => { let dataDir: string; let nodePath: string; - // Helper functions - async function fakeKeynode(nodePath) { - await fs.promises.mkdir(path.join(nodePath, 'keys')); - await fs.promises.mkdir(path.join(nodePath, 'db')); - await fs.promises.writeFile( - path.join(nodePath, 'versionFile'), - 'Versions or something IDK', - ); - } - beforeEach(async () => { dataDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'status-test-')); nodePath = path.join(dataDir, 'Node'); @@ -65,20 +51,6 @@ describe('Bootstrap', () => { }); }); - test('Should throw error if other files exists.', async () => { - await fs.promises.mkdir(path.join(nodePath, 'NotAnNodeDirectory')); - await expect(() => - bootstrapUtils.bootstrapState({ nodePath, password, logger }), - ).rejects.toThrow(bootstrapErrors.ErrorBootstrapExistingState); - }); - - test('should throw error if keynode already exists.', async () => { - await fakeKeynode(nodePath); - await expect(() => - bootstrapUtils.bootstrapState({ nodePath, password, logger }), - ).rejects.toThrow(bootstrapErrors.ErrorBootstrapExistingState); - }); - test( 'should be able to start agent on created state.', async () => { From 7e3ebe5cd82c6b14bba6ac4910596b023abeb64f Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Mon, 6 Dec 2021 16:45:40 +1100 Subject: [PATCH 5/8] Added start with existing state test for agent start --- tests/bin/agent/start.test.ts | 105 +++++++++++++++++++--------------- 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/tests/bin/agent/start.test.ts b/tests/bin/agent/start.test.ts index 4df72eef4..e2c430c64 100644 --- a/tests/bin/agent/start.test.ts +++ b/tests/bin/agent/start.test.ts @@ -10,7 +10,7 @@ import config from '@/config'; import * as testUtils from '../utils'; describe('start', () => { - const logger = new Logger('start test', LogLevel.WARN, [new StreamHandler()]); + const logger = new Logger('start test', LogLevel.INFO, [new StreamHandler()]); let dataDir: string; beforeEach(async () => { dataDir = await fs.promises.mkdtemp( @@ -24,60 +24,21 @@ describe('start', () => { }); }); test( - 'start in foreground with parameters', + 'start in foreground', async () => { const password = 'abc123'; - const passwordPath = path.join(dataDir, 'password'); - await fs.promises.writeFile(passwordPath, password); const agentProcess = await testUtils.pkSpawn( [ 'agent', 'start', '--node-path', path.join(dataDir, 'polykey'), - '--password-file', - passwordPath, '--root-key-pair-bits', '1024', + '--client-host', '--verbose', ], - undefined, - dataDir, - logger, - ); - const rlOut = readline.createInterface(agentProcess.stdout!); - const recoveryCode = await new Promise( - (resolve, reject) => { - rlOut.once('line', resolve); - rlOut.once('close', reject); - }, - ); - expect(typeof recoveryCode).toBe('string'); - expect( - recoveryCode.split(' ').length === 12 || - recoveryCode.split(' ').length === 24, - ).toBe(true); - agentProcess.kill('SIGTERM'); - const [exitCode, signal] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - agentProcess.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); - expect(exitCode).toBe(null); - expect(signal).toBe('SIGTERM'); - }, - global.defaultTimeout * 2, - ); - test( - 'start in foreground with environment variables', - async () => { - const password = 'abc123'; - const agentProcess = await testUtils.pkSpawn( - ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], { - PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: password, }, dataDir, @@ -109,13 +70,17 @@ describe('start', () => { global.defaultTimeout * 2, ); test( - 'start in background with environment variables', + 'start in background', async () => { const password = 'abc123'; + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, password); const agentProcess = await testUtils.pkSpawn( [ 'agent', 'start', + '--password-file', + passwordPath, '--root-key-pair-bits', '1024', '--background', @@ -127,7 +92,6 @@ describe('start', () => { ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), - PK_PASSWORD: password, }, dataDir, logger, @@ -326,7 +290,6 @@ describe('start', () => { message: errorStatusLocked.message, }) .trim(); - // It's either the first or second process if (index === 0) { expect(stdErrLine1).toBeDefined(); @@ -358,4 +321,56 @@ describe('start', () => { }, global.defaultTimeout * 2, ); + test( + 'start with existing state', + async () => { + const password = 'abc123'; + const agentProcess1 = await testUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger, + ); + const rlOut = readline.createInterface(agentProcess1.stdout!); + await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }); + agentProcess1.kill('SIGTERM'); + await new Promise<[number | null, NodeJS.Signals | null]>((resolve) => { + agentProcess1.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + const agentProcess2 = await testUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger, + ); + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + fs, + logger, + }); + await status.waitFor('LIVE'); + agentProcess2.kill('SIGTERM'); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess2.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + }, + global.defaultTimeout * 2, + ); }); From 037dd71276be905466d2c056b612673c41dc9cee Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Mon, 6 Dec 2021 21:15:24 +1100 Subject: [PATCH 6/8] Added fresh option and started refactoring bootstrap tests and interrupted start and bootstrap tests --- src/bin/agent/CommandStart.ts | 1 + src/bin/bootstrap/CommandBootstrap.ts | 1 + src/bin/utils/options.ts | 9 ++ src/bootstrap/errors.ts | 9 +- src/bootstrap/utils.ts | 7 ++ tests/bin/agent/start.test.ts | 143 ++++++++++++++++++---- tests/bin/bootstrap.test.ts | 164 +++++++++++++------------- tests/bin/utils.ts | 3 + 8 files changed, 229 insertions(+), 108 deletions(-) diff --git a/src/bin/agent/CommandStart.ts b/src/bin/agent/CommandStart.ts index 0143e384d..ce802125d 100644 --- a/src/bin/agent/CommandStart.ts +++ b/src/bin/agent/CommandStart.ts @@ -26,6 +26,7 @@ class CommandStart extends CommandPolykey { this.addOption(binOptions.background); this.addOption(binOptions.backgroundOutFile); this.addOption(binOptions.backgroundErrFile); + this.addOption(binOptions.fresh); this.action(async (options) => { options.clientHost = options.clientHost ?? config.defaults.networkConfig.clientHost; diff --git a/src/bin/bootstrap/CommandBootstrap.ts b/src/bin/bootstrap/CommandBootstrap.ts index dde735aae..93387465c 100644 --- a/src/bin/bootstrap/CommandBootstrap.ts +++ b/src/bin/bootstrap/CommandBootstrap.ts @@ -10,6 +10,7 @@ class CommandBootstrap extends CommandPolykey { this.description('Bootstrap Keynode State'); this.addOption(binOptions.recoveryCodeFile); this.addOption(binOptions.rootKeyPairBits); + this.addOption(binOptions.fresh); this.action(async (options) => { const bootstrapUtils = await import('../../bootstrap/utils'); const password = await binProcessors.processPassword( diff --git a/src/bin/utils/options.ts b/src/bin/utils/options.ts index d0c70a0c4..3dafddd02 100644 --- a/src/bin/utils/options.ts +++ b/src/bin/utils/options.ts @@ -37,6 +37,14 @@ const verbose = new commander.Option('-v, --verbose', 'Log Verbose Messages') }) .default(0); +/** + * Ignore any existing state during side-effectful construction + */ +const fresh = new commander.Option( + '--fresh', + 'Ignore existing state during construction' +).default(false); + /** * Node ID used for connecting to a remote agent */ @@ -109,6 +117,7 @@ export { nodePath, format, verbose, + fresh, nodeId, clientHost, clientPort, diff --git a/src/bootstrap/errors.ts b/src/bootstrap/errors.ts index a9681841f..1e24566a2 100644 --- a/src/bootstrap/errors.ts +++ b/src/bootstrap/errors.ts @@ -1,5 +1,10 @@ -import { ErrorPolykey } from '../errors'; +import { ErrorPolykey, sysexits } from '../errors'; class ErrorBootstrap extends ErrorPolykey {} -export { ErrorBootstrap }; +class ErrorBootstrapExistingState extends ErrorBootstrap { + description = 'Node path is occupied with existing state'; + exitCode = sysexits.USAGE; +} + +export { ErrorBootstrap, ErrorBootstrapExistingState }; diff --git a/src/bootstrap/utils.ts b/src/bootstrap/utils.ts index abeeba02e..5d09a11ee 100644 --- a/src/bootstrap/utils.ts +++ b/src/bootstrap/utils.ts @@ -3,6 +3,7 @@ import type { RecoveryCode } from '../keys/types'; import path from 'path'; import Logger from '@matrixai/logger'; import { DB } from '@matrixai/db'; +import * as bootstrapErrors from './errors'; import { IdentitiesManager } from '../identities'; import { SessionManager } from '../sessions'; import { Status } from '../status'; @@ -68,6 +69,12 @@ async function bootstrapState({ }); try { await status.start({ pid: process.pid }); + if (!fresh) { + // Check the if number of directory entries is greater than 1 due to status.json + if ((await fs.promises.readdir(nodePath)).length > 1) { + throw new bootstrapErrors.ErrorBootstrapExistingState(); + } + } // Construction occurs here, fresh is propagated // If any creations fail, then nodePath may be left with intermediate state // Therefore the fresh parameter is expected to be true under normal usage diff --git a/tests/bin/agent/start.test.ts b/tests/bin/agent/start.test.ts index e2c430c64..09a988d61 100644 --- a/tests/bin/agent/start.test.ts +++ b/tests/bin/agent/start.test.ts @@ -7,10 +7,10 @@ import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; import { Status, errors as statusErrors } from '@/status'; import * as binUtils from '@/bin/utils'; import config from '@/config'; -import * as testUtils from '../utils'; +import * as testBinUtils from '../utils'; describe('start', () => { - const logger = new Logger('start test', LogLevel.INFO, [new StreamHandler()]); + const logger = new Logger('start test', LogLevel.WARN, [new StreamHandler()]); let dataDir: string; beforeEach(async () => { dataDir = await fs.promises.mkdtemp( @@ -27,7 +27,7 @@ describe('start', () => { 'start in foreground', async () => { const password = 'abc123'; - const agentProcess = await testUtils.pkSpawn( + const agentProcess = await testBinUtils.pkSpawn( [ 'agent', 'start', @@ -35,7 +35,6 @@ describe('start', () => { path.join(dataDir, 'polykey'), '--root-key-pair-bits', '1024', - '--client-host', '--verbose', ], { @@ -66,6 +65,14 @@ describe('start', () => { }); expect(exitCode).toBe(null); expect(signal).toBe('SIGTERM'); + // Check for graceful exit + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + fs, + logger, + }); + const statusInfo = (await status.readStatus())!; + expect(statusInfo.status).toBe('DEAD'); }, global.defaultTimeout * 2, ); @@ -75,7 +82,7 @@ describe('start', () => { const password = 'abc123'; const passwordPath = path.join(dataDir, 'password'); await fs.promises.writeFile(passwordPath, password); - const agentProcess = await testUtils.pkSpawn( + const agentProcess = await testBinUtils.pkSpawn( [ 'agent', 'start', @@ -137,7 +144,8 @@ describe('start', () => { const statusInfo1 = (await status.readStatus())!; expect(statusInfo1).toBeDefined(); expect(statusInfo1.status).toBe('LIVE'); - process.kill(statusInfo1.data.pid, 'SIGTERM'); + process.kill(statusInfo1.data.pid, 'SIGINT'); + // Check for graceful exit const statusInfo2 = await status.waitFor('DEAD'); expect(statusInfo2.status).toBe('DEAD'); }, @@ -149,7 +157,7 @@ describe('start', () => { const password = 'abc123'; // One of these processes is blocked const [agentProcess1, agentProcess2] = await Promise.all([ - testUtils.pkSpawn( + testBinUtils.pkSpawn( ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -158,7 +166,7 @@ describe('start', () => { dataDir, logger.getChild('agentProcess1'), ), - testUtils.pkSpawn( + testBinUtils.pkSpawn( ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -205,7 +213,7 @@ describe('start', () => { if (index === 0) { expect(stdErrLine1).toBeDefined(); expect(stdErrLine1).toBe(eOutput); - agentProcess2.kill('SIGTERM'); + agentProcess2.kill('SIGQUIT'); const [exitCode, signal] = await new Promise< [number | null, NodeJS.Signals | null] >((resolve) => { @@ -214,11 +222,11 @@ describe('start', () => { }); }); expect(exitCode).toBe(null); - expect(signal).toBe('SIGTERM'); + expect(signal).toBe('SIGQUIT'); } else if (index === 1) { expect(stdErrLine2).toBeDefined(); expect(stdErrLine2).toBe(eOutput); - agentProcess1.kill('SIGTERM'); + agentProcess1.kill('SIGQUIT'); const [exitCode, signal] = await new Promise< [number | null, NodeJS.Signals | null] >((resolve) => { @@ -227,7 +235,7 @@ describe('start', () => { }); }); expect(exitCode).toBe(null); - expect(signal).toBe('SIGTERM'); + expect(signal).toBe('SIGQUIT'); } }, global.defaultTimeout * 2, @@ -238,7 +246,7 @@ describe('start', () => { const password = 'abc123'; // One of these processes is blocked const [agentProcess, bootstrapProcess] = await Promise.all([ - testUtils.pkSpawn( + testBinUtils.pkSpawn( ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -247,7 +255,7 @@ describe('start', () => { dataDir, logger.getChild('agentProcess'), ), - testUtils.pkSpawn( + testBinUtils.pkSpawn( ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -325,7 +333,7 @@ describe('start', () => { 'start with existing state', async () => { const password = 'abc123'; - const agentProcess1 = await testUtils.pkSpawn( + const agentProcess1 = await testBinUtils.pkSpawn( ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -339,13 +347,15 @@ describe('start', () => { rlOut.once('line', resolve); rlOut.once('close', reject); }); - agentProcess1.kill('SIGTERM'); - await new Promise<[number | null, NodeJS.Signals | null]>((resolve) => { + agentProcess1.kill('SIGHUP'); + const [exitCode1, signal1] = await new Promise<[number | null, NodeJS.Signals | null]>((resolve) => { agentProcess1.once('exit', (code, signal) => { resolve([code, signal]); }); }); - const agentProcess2 = await testUtils.pkSpawn( + expect(exitCode1).toBe(null); + expect(signal1).toBe('SIGHUP'); + const agentProcess2 = await testBinUtils.pkSpawn( ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], { PK_NODE_PATH: path.join(dataDir, 'polykey'), @@ -354,22 +364,109 @@ describe('start', () => { dataDir, logger, ); - const status = new Status({ + const status1 = new Status({ statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), fs, logger, }); - await status.waitFor('LIVE'); - agentProcess2.kill('SIGTERM'); - const [exitCode, signal] = await new Promise< + await status1.waitFor('LIVE'); + agentProcess2.kill('SIGHUP'); + const [exitCode2, signal2] = await new Promise< [number | null, NodeJS.Signals | null] >((resolve) => { agentProcess2.once('exit', (code, signal) => { resolve([code, signal]); }); }); + expect(exitCode2).toBe(null); + expect(signal2).toBe('SIGHUP'); + // Check for graceful exit + const status2 = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + fs, + logger, + }); + const statusInfo2 = (await status2.readStatus())!; + expect(statusInfo2.status).toBe('DEAD'); + }, + global.defaultTimeout * 2, + ); + test( + 'start when interrupted, requires fresh on next start', + async () => { + const password = 'password'; + const agentProcess1 = await testBinUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('agentProcess1'), + ); + const rlErr = readline.createInterface(agentProcess1.stderr!); + // Interrupt when generating the root key pair + await new Promise((resolve, reject) => { + rlErr.once('close', reject); + rlErr.on('line', (l) => { + // This line is brittle + // It may change if the log format changes + // Make sure to keep it updated at the exact point when the DB is created + if (l === 'INFO:DB:Created DB') { + agentProcess1.kill('SIGINT'); + resolve(); + } + }); + }); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess1.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); expect(exitCode).toBe(null); - expect(signal).toBe('SIGTERM'); + expect(signal).toBe('SIGINT'); + // Unlike bootstrapping, agent start can succeed under certain compatible partial state + // However in some cases, state will conflict, and the start will fail with various errors + // In such cases, the `--fresh` option must be used + const agentProcess2 = await testBinUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--fresh', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('agentProcess2'), + ); + const rlOut = readline.createInterface(agentProcess2.stdout!); + const recoveryCode = await new Promise( + (resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }, + ); + expect(typeof recoveryCode).toBe('string'); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + agentProcess2.kill('SIGQUIT'); + await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + agentProcess2.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + // Check for graceful exit + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + fs, + logger, + }); + const statusInfo = (await status.readStatus())!; + expect(statusInfo.status).toBe('DEAD'); }, global.defaultTimeout * 2, ); diff --git a/tests/bin/bootstrap.test.ts b/tests/bin/bootstrap.test.ts index 122c3948e..f16001ebc 100644 --- a/tests/bin/bootstrap.test.ts +++ b/tests/bin/bootstrap.test.ts @@ -1,20 +1,22 @@ +import type { RecoveryCode } from '@/keys/types'; import os from 'os'; import path from 'path'; import fs from 'fs'; -import * as utils from './utils'; +import readline from 'readline'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { Status, errors as statusErrors } from '@/status'; +import { errors as bootstrapErrors } from '@/bootstrap'; +import * as binUtils from '@/bin/utils'; +import config from '@/config'; +import * as testBinUtils from './utils'; -describe('CLI bootstrap', () => { +describe('bootstrap', () => { + const logger = new Logger('bootstrap test', LogLevel.INFO, [new StreamHandler()]); let dataDir: string; - let passwordFile: string; - let nodePath: string; - beforeEach(async () => { dataDir = await fs.promises.mkdtemp( path.join(os.tmpdir(), 'polykey-test-'), ); - passwordFile = path.join(dataDir, 'passwordFile'); - nodePath = path.join(dataDir, 'testnode'); - await fs.promises.writeFile(passwordFile, 'password'); }); afterEach(async () => { await fs.promises.rm(dataDir, { @@ -22,83 +24,79 @@ describe('CLI bootstrap', () => { recursive: true, }); }); - test( - "Should create keynode state if directory doesn't exist.", + 'bootstrap when interrupted, requires fresh on next bootstrap', async () => { - const result = await utils.pkStdio([ - 'bootstrap', - '-np', - nodePath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout.split(' ')).toHaveLength(24); - }, - global.polykeyStartupTimeout * 2, - ); - test( - 'Should create keynode state if directory is empty.', - async () => { - await fs.promises.mkdir(nodePath); - const result = await utils.pkStdio([ - 'bootstrap', - '-np', - nodePath, - '--password-file', - passwordFile, - '-vvvv', - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout.split(' ')).toHaveLength(24); - }, - global.polykeyStartupTimeout * 2, - ); - test( - 'Should generate a recovery code when creating state.', - async () => { - await fs.promises.mkdir(nodePath); - const result = await utils.pkStdio([ - 'bootstrap', - '-np', - nodePath, - '--password-file', - passwordFile, - ]); - expect(result.exitCode).toBe(0); - expect(result.stdout.split(' ')).toHaveLength(24); - }, - global.polykeyStartupTimeout * 3, - ); - test( - 'concurrent bootstrapping', - async () => { - await fs.promises.mkdir(nodePath); - - // Bootstrapping two nodes at the same time. - const results = await Promise.all([ - utils.pkStdio([ - 'bootstrap', - '-np', - nodePath, - '--password-file', - passwordFile, - ]), - utils.pkStdio([ - 'bootstrap', - '-np', - nodePath, - '--password-file', - passwordFile, - ]), - ]); - - // 1 fails and 1 succeeds. - - expect(JSON.stringify(results)).toContain(':0'); - expect(JSON.stringify(results)).toContain(':75'); + const password = 'password'; + const bootstrapProcess1 = await testBinUtils.pkSpawn( + ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('bootstrapProcess1'), + ); + const rlErr = readline.createInterface(bootstrapProcess1.stderr!); + // Interrupt when generating the root key pair + await new Promise((resolve, reject) => { + rlErr.once('close', reject); + rlErr.on('line', (l) => { + // This line is brittle + // It may change if the log format changes + // Make sure to keep it updated at the exact point when the root key pair is generated + if (l === 'INFO:KeyManager:Generating root key pair') { + bootstrapProcess1.kill('SIGINT'); + resolve(); + } + }); + }); + const [exitCode, signal] = await new Promise< + [number | null, NodeJS.Signals | null] + >((resolve) => { + bootstrapProcess1.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGINT'); + // Attempting to bootstrap should fail with existing state + const bootstrapProcess2 = await testBinUtils.pkStdio( + ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir + ); + const stdErrLine = bootstrapProcess2.stderr.trim().split('\n').pop(); + const errorBootstrapExistingState = new bootstrapErrors.ErrorBootstrapExistingState(); + expect(bootstrapProcess2.exitCode).toBe(errorBootstrapExistingState.exitCode); + const eOutput = binUtils + .outputFormatter({ + type: 'error', + name: errorBootstrapExistingState.name, + description: errorBootstrapExistingState.description, + message: errorBootstrapExistingState.message, + }) + .trim(); + expect(stdErrLine).toBe(eOutput); + // Attempting to bootstrap with --fresh should succeed + const bootstrapProcess3 = await testBinUtils.pkStdio( + ['bootstrap', '--root-key-pair-bits', '1024', '--fresh', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir + ); + expect(bootstrapProcess3.exitCode).toBe(0); + const recoveryCode = bootstrapProcess3.stdout.trim(); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); }, - global.defaultTimeout * 4, + global.defaultTimeout * 2, ); }); diff --git a/tests/bin/utils.ts b/tests/bin/utils.ts index 7af49d24b..ace09da5b 100644 --- a/tests/bin/utils.ts +++ b/tests/bin/utils.ts @@ -22,6 +22,7 @@ async function pk(args: Array): Promise { /** * Runs pk command functionally with mocked STDIO + * Both stdout and stderr are the entire output including newlines * @param env Augments env for command execution * @param cwd Defaults to temporary directory */ @@ -105,6 +106,7 @@ async function pkStdio( * Runs pk command through subprocess * This is used when a subprocess functionality needs to be used * This is intended for terminating subprocesses + * Both stdout and stderr are the entire output including newlines * @param env Augments env for command execution * @param cwd Defaults to temporary directory */ @@ -206,6 +208,7 @@ async function pkSpawn( ); const rlErr = readline.createInterface(subprocess.stderr!); rlErr.on('line', (l) => { + // The readline library will trim newlines logger.info(l); }); return subprocess; From 54f8be2a63be69b1eac01342a31f4299df14e01c Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Mon, 6 Dec 2021 23:15:22 +1100 Subject: [PATCH 7/8] Added tests for starting from recovery code and deriving public key bit size from existing key pair, and using testBinUtils.processExit --- src/bin/utils/options.ts | 2 +- src/keys/KeyManager.ts | 40 +++++-- tests/bin/agent/start.test.ts | 205 ++++++++++++++++++++++------------ tests/bin/bootstrap.test.ts | 25 +++-- tests/bin/utils.ts | 20 +++- 5 files changed, 195 insertions(+), 97 deletions(-) diff --git a/src/bin/utils/options.ts b/src/bin/utils/options.ts index 3dafddd02..983722998 100644 --- a/src/bin/utils/options.ts +++ b/src/bin/utils/options.ts @@ -42,7 +42,7 @@ const verbose = new commander.Option('-v, --verbose', 'Log Verbose Messages') */ const fresh = new commander.Option( '--fresh', - 'Ignore existing state during construction' + 'Ignore existing state during construction', ).default(false); /** diff --git a/src/keys/KeyManager.ts b/src/keys/KeyManager.ts index fc46b13d1..f96650dde 100644 --- a/src/keys/KeyManager.ts +++ b/src/keys/KeyManager.ts @@ -583,15 +583,13 @@ class KeyManager { let recoveryCodeNew: RecoveryCode | undefined; if (await this.existsRootKeyPair()) { if (recoveryCode != null) { - // Recover the key pair with the recovery code - // Check if the generated key pair matches - const rootKeyPairCheck = await this.generateKeyPair(bits, recoveryCode); - if (!(await this.matchRootKeyPair(rootKeyPairCheck))) { + const recoveredKeyPair = await this.recoverRootKeyPair(recoveryCode); + if (recoveredKeyPair == null) { throw new keysErrors.ErrorKeysRecoveryCodeIncorrect(); } // Recovered key pair, write the key pair with the new password - rootKeyPair = rootKeyPairCheck; - await this.writeRootKeyPair(rootKeyPairCheck, password); + rootKeyPair = recoveredKeyPair; + await this.writeRootKeyPair(recoveredKeyPair, password); } else { // Load key pair by decrypting with password rootKeyPair = await this.readRootKeyPair(password); @@ -698,16 +696,21 @@ class KeyManager { } } - protected async matchRootKeyPair(keyPair: KeyPair): Promise { + /** + * Recovers root key pair with recovery code + * Checks if the generated key pair public key matches + * Uses the existing key pair's public key bit size + * To generate the recovered key pair + */ + protected async recoverRootKeyPair( + recoveryCode: RecoveryCode, + ): Promise { let publicKeyPem: string; try { publicKeyPem = await this.fs.promises.readFile(this.rootPubPath, { encoding: 'utf8', }); } catch (e) { - if (e.code === 'ENOENT') { - return false; - } throw new keysErrors.ErrorRootKeysRead(e.message, { errno: e.errno, syscall: e.syscall, @@ -715,8 +718,21 @@ class KeyManager { path: e.path, }); } - const publicKeyPemCheck = keysUtils.publicKeyToPem(keyPair.publicKey); - return publicKeyPemCheck === publicKeyPem; + const rootKeyPairBits = keysUtils.publicKeyBitSize( + keysUtils.publicKeyFromPem(publicKeyPem), + ); + const recoveredKeyPair = await this.generateKeyPair( + rootKeyPairBits, + recoveryCode, + ); + const recoveredPublicKeyPem = keysUtils.publicKeyToPem( + recoveredKeyPair.publicKey, + ); + if (recoveredPublicKeyPem === publicKeyPem) { + return recoveredKeyPair; + } else { + return; + } } protected async setupKey( diff --git a/tests/bin/agent/start.test.ts b/tests/bin/agent/start.test.ts index 09a988d61..fcce5f0e3 100644 --- a/tests/bin/agent/start.test.ts +++ b/tests/bin/agent/start.test.ts @@ -56,13 +56,7 @@ describe('start', () => { recoveryCode.split(' ').length === 24, ).toBe(true); agentProcess.kill('SIGTERM'); - const [exitCode, signal] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - agentProcess.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode, signal] = await testBinUtils.processExit(agentProcess); expect(exitCode).toBe(null); expect(signal).toBe('SIGTERM'); // Check for graceful exit @@ -214,26 +208,18 @@ describe('start', () => { expect(stdErrLine1).toBeDefined(); expect(stdErrLine1).toBe(eOutput); agentProcess2.kill('SIGQUIT'); - const [exitCode, signal] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - agentProcess2.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode, signal] = await testBinUtils.processExit( + agentProcess2, + ); expect(exitCode).toBe(null); expect(signal).toBe('SIGQUIT'); } else if (index === 1) { expect(stdErrLine2).toBeDefined(); expect(stdErrLine2).toBe(eOutput); agentProcess1.kill('SIGQUIT'); - const [exitCode, signal] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - agentProcess1.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode, signal] = await testBinUtils.processExit( + agentProcess1, + ); expect(exitCode).toBe(null); expect(signal).toBe('SIGQUIT'); } @@ -303,26 +289,16 @@ describe('start', () => { expect(stdErrLine1).toBeDefined(); expect(stdErrLine1).toBe(eOutput); bootstrapProcess.kill('SIGTERM'); - const [exitCode, signal] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - bootstrapProcess.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode, signal] = await testBinUtils.processExit( + bootstrapProcess, + ); expect(exitCode).toBe(null); expect(signal).toBe('SIGTERM'); } else if (index === 1) { expect(stdErrLine2).toBeDefined(); expect(stdErrLine2).toBe(eOutput); agentProcess.kill('SIGTERM'); - const [exitCode, signal] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - agentProcess.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode, signal] = await testBinUtils.processExit(agentProcess); expect(exitCode).toBe(null); expect(signal).toBe('SIGTERM'); } @@ -348,11 +324,9 @@ describe('start', () => { rlOut.once('close', reject); }); agentProcess1.kill('SIGHUP'); - const [exitCode1, signal1] = await new Promise<[number | null, NodeJS.Signals | null]>((resolve) => { - agentProcess1.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode1, signal1] = await testBinUtils.processExit( + agentProcess1, + ); expect(exitCode1).toBe(null); expect(signal1).toBe('SIGHUP'); const agentProcess2 = await testBinUtils.pkSpawn( @@ -364,30 +338,21 @@ describe('start', () => { dataDir, logger, ); - const status1 = new Status({ + const status = new Status({ statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), fs, logger, }); - await status1.waitFor('LIVE'); + await status.waitFor('LIVE'); agentProcess2.kill('SIGHUP'); - const [exitCode2, signal2] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - agentProcess2.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode2, signal2] = await testBinUtils.processExit( + agentProcess2, + ); expect(exitCode2).toBe(null); expect(signal2).toBe('SIGHUP'); // Check for graceful exit - const status2 = new Status({ - statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), - fs, - logger, - }); - const statusInfo2 = (await status2.readStatus())!; - expect(statusInfo2.status).toBe('DEAD'); + const statusInfo = (await status.readStatus())!; + expect(statusInfo.status).toBe('DEAD'); }, global.defaultTimeout * 2, ); @@ -418,20 +383,21 @@ describe('start', () => { } }); }); - const [exitCode, signal] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - agentProcess1.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode, signal] = await testBinUtils.processExit(agentProcess1); expect(exitCode).toBe(null); expect(signal).toBe('SIGINT'); // Unlike bootstrapping, agent start can succeed under certain compatible partial state // However in some cases, state will conflict, and the start will fail with various errors // In such cases, the `--fresh` option must be used const agentProcess2 = await testBinUtils.pkSpawn( - ['agent', 'start', '--root-key-pair-bits', '1024', '--fresh', '--verbose'], + [ + 'agent', + 'start', + '--root-key-pair-bits', + '1024', + '--fresh', + '--verbose', + ], { PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: password, @@ -452,13 +418,7 @@ describe('start', () => { recoveryCode.split(' ').length === 24, ).toBe(true); agentProcess2.kill('SIGQUIT'); - await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - agentProcess2.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + await testBinUtils.processExit(agentProcess2); // Check for graceful exit const status = new Status({ statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), @@ -470,4 +430,107 @@ describe('start', () => { }, global.defaultTimeout * 2, ); + test( + 'start from recovery code', + async () => { + const password1 = 'abc123'; + const password2 = 'new password'; + const status = new Status({ + statusPath: path.join(dataDir, 'polykey', config.defaults.statusBase), + fs, + logger, + }); + const agentProcess1 = await testBinUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'polykey'), + '--root-key-pair-bits', + '1024', + '--verbose', + ], + { + PK_PASSWORD: password1, + }, + dataDir, + logger.getChild('agentProcess1'), + ); + const rlOut = readline.createInterface(agentProcess1.stdout!); + const recoveryCode = await new Promise( + (resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }, + ); + const statusInfo1 = (await status.readStatus())!; + agentProcess1.kill('SIGTERM'); + await testBinUtils.processExit(agentProcess1); + const recoveryCodePath = path.join(dataDir, 'recovery-code'); + await fs.promises.writeFile(recoveryCodePath, recoveryCode + '\n'); + // When recovering, having the wrong bit size is not a problem + const agentProcess2 = await testBinUtils.pkSpawn( + [ + 'agent', + 'start', + '--recovery-code-file', + recoveryCodePath, + '--root-key-pair-bits', + '2048', + '--verbose', + ], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password2, + }, + dataDir, + logger.getChild('agentProcess2'), + ); + const statusInfo2 = await status.waitFor('LIVE'); + expect(statusInfo2.status).toBe('LIVE'); + // Node Id hasn't changed + expect(statusInfo1.data.nodeId).toBe(statusInfo2.data.nodeId); + agentProcess2.kill('SIGTERM'); + await testBinUtils.processExit(agentProcess2); + // Check that the password has changed + const agentProcess3 = await testBinUtils.pkSpawn( + ['agent', 'start', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password2, + }, + dataDir, + logger.getChild('agentProcess3'), + ); + const statusInfo3 = await status.waitFor('LIVE'); + expect(statusInfo3.status).toBe('LIVE'); + // Node ID hasn't changed + expect(statusInfo1.data.nodeId).toBe(statusInfo3.data.nodeId); + agentProcess3.kill('SIGTERM'); + await testBinUtils.processExit(agentProcess3); + // Checks deterministic generation using the same recovery code + // First by deleting the polykey state + await fs.promises.rm(path.join(dataDir, 'polykey'), { + force: true, + recursive: true, + }); + const agentProcess4 = await testBinUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password2, + PK_RECOVERY_CODE: recoveryCode, + }, + dataDir, + logger.getChild('agentProcess4'), + ); + const statusInfo4 = await status.waitFor('LIVE'); + expect(statusInfo4.status).toBe('LIVE'); + // Same Node ID as before + expect(statusInfo1.data.nodeId).toBe(statusInfo4.data.nodeId); + agentProcess4.kill('SIGTERM'); + await testBinUtils.processExit(agentProcess4); + }, + global.defaultTimeout * 3, + ); }); diff --git a/tests/bin/bootstrap.test.ts b/tests/bin/bootstrap.test.ts index f16001ebc..150812220 100644 --- a/tests/bin/bootstrap.test.ts +++ b/tests/bin/bootstrap.test.ts @@ -11,7 +11,9 @@ import config from '@/config'; import * as testBinUtils from './utils'; describe('bootstrap', () => { - const logger = new Logger('bootstrap test', LogLevel.INFO, [new StreamHandler()]); + const logger = new Logger('bootstrap test', LogLevel.WARN, [ + new StreamHandler(), + ]); let dataDir: string; beforeEach(async () => { dataDir = await fs.promises.mkdtemp( @@ -51,13 +53,9 @@ describe('bootstrap', () => { } }); }); - const [exitCode, signal] = await new Promise< - [number | null, NodeJS.Signals | null] - >((resolve) => { - bootstrapProcess1.once('exit', (code, signal) => { - resolve([code, signal]); - }); - }); + const [exitCode, signal] = await testBinUtils.processExit( + bootstrapProcess1, + ); expect(exitCode).toBe(null); expect(signal).toBe('SIGINT'); // Attempting to bootstrap should fail with existing state @@ -67,11 +65,14 @@ describe('bootstrap', () => { PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: password, }, - dataDir + dataDir, ); const stdErrLine = bootstrapProcess2.stderr.trim().split('\n').pop(); - const errorBootstrapExistingState = new bootstrapErrors.ErrorBootstrapExistingState(); - expect(bootstrapProcess2.exitCode).toBe(errorBootstrapExistingState.exitCode); + const errorBootstrapExistingState = + new bootstrapErrors.ErrorBootstrapExistingState(); + expect(bootstrapProcess2.exitCode).toBe( + errorBootstrapExistingState.exitCode, + ); const eOutput = binUtils .outputFormatter({ type: 'error', @@ -88,7 +89,7 @@ describe('bootstrap', () => { PK_NODE_PATH: path.join(dataDir, 'polykey'), PK_PASSWORD: password, }, - dataDir + dataDir, ); expect(bootstrapProcess3.exitCode).toBe(0); const recoveryCode = bootstrapProcess3.stdout.trim(); diff --git a/tests/bin/utils.ts b/tests/bin/utils.ts index ace09da5b..893cbc522 100644 --- a/tests/bin/utils.ts +++ b/tests/bin/utils.ts @@ -364,4 +364,22 @@ async function pkAgent( }; } -export { pk, pkStdio, pkExec, pkSpawn, pkExpect, pkAgent }; +/** + * Waits for child process to exit + * When process is terminated with signal + * The code will be null + * When the process exits by itself, the signal will be null + */ +async function processExit( + process: ChildProcess, +): Promise<[number | null, NodeJS.Signals | null]> { + return await new Promise<[number | null, NodeJS.Signals | null]>( + (resolve) => { + process.once('exit', (code, signal) => { + resolve([code, signal]); + }); + }, + ); +} + +export { pk, pkStdio, pkExec, pkSpawn, pkExpect, pkAgent, processExit }; From ec6ad041b30d2ac8b7e59bb4b555533e5e9deb3f Mon Sep 17 00:00:00 2001 From: Roger Qiu Date: Mon, 6 Dec 2021 23:58:05 +1100 Subject: [PATCH 8/8] Added bootstrapping tests including occupied node path and concurrent bootstrapping --- tests/bin/bootstrap.test.ts | 155 +++++++++++++++++++++++++++++++++++- tests/bin/utils.ts | 2 + 2 files changed, 154 insertions(+), 3 deletions(-) diff --git a/tests/bin/bootstrap.test.ts b/tests/bin/bootstrap.test.ts index 150812220..beae34f75 100644 --- a/tests/bin/bootstrap.test.ts +++ b/tests/bin/bootstrap.test.ts @@ -1,13 +1,11 @@ -import type { RecoveryCode } from '@/keys/types'; import os from 'os'; import path from 'path'; import fs from 'fs'; import readline from 'readline'; import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; -import { Status, errors as statusErrors } from '@/status'; +import { errors as statusErrors } from '@/status'; import { errors as bootstrapErrors } from '@/bootstrap'; import * as binUtils from '@/bin/utils'; -import config from '@/config'; import * as testBinUtils from './utils'; describe('bootstrap', () => { @@ -26,6 +24,157 @@ describe('bootstrap', () => { recursive: true, }); }); + test( + 'bootstraps node state', + async () => { + const password = 'password'; + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, password); + const { exitCode, stdout } = await testBinUtils.pkStdio( + [ + 'bootstrap', + '--password-file', + passwordPath, + '--root-key-pair-bits', + '1024', + '--verbose', + ], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + }, + dataDir, + ); + expect(exitCode).toBe(0); + const recoveryCode = stdout.trim(); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + }, + global.defaultTimeout, + ); + test('bootstrapping occupied node state', async () => { + const password = 'password'; + await fs.promises.mkdir(path.join(dataDir, 'polykey')); + await fs.promises.writeFile(path.join(dataDir, 'polykey', 'test'), ''); + let exitCode, stdout, stderr; + ({ exitCode, stdout, stderr } = await testBinUtils.pkStdio( + [ + 'bootstrap', + '--node-path', + path.join(dataDir, 'polykey'), + '--root-key-pair-bits', + '1024', + '--verbose', + ], + { + PK_PASSWORD: password, + }, + dataDir, + )); + const errorBootstrapExistingState = + new bootstrapErrors.ErrorBootstrapExistingState(); + expect(exitCode).toBe(errorBootstrapExistingState.exitCode); + const stdErrLine = stderr.trim().split('\n').pop(); + const eOutput = binUtils + .outputFormatter({ + type: 'error', + name: errorBootstrapExistingState.name, + description: errorBootstrapExistingState.description, + message: errorBootstrapExistingState.message, + }) + .trim(); + expect(stdErrLine).toBe(eOutput); + ({ exitCode, stdout, stderr } = await testBinUtils.pkStdio( + [ + 'bootstrap', + '--node-path', + path.join(dataDir, 'polykey'), + '--root-key-pair-bits', + '1024', + '--fresh', + '--verbose', + ], + { + PK_PASSWORD: password, + }, + dataDir, + )); + expect(exitCode).toBe(0); + const recoveryCode = stdout.trim(); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); + }); + test('concurrent bootstrapping are coalesced', async () => { + const password = 'password'; + const [bootstrapProcess1, bootstrapProcess2] = await Promise.all([ + testBinUtils.pkSpawn( + ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('bootstrapProcess1'), + ), + testBinUtils.pkSpawn( + ['bootstrap', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('bootstrapProcess2'), + ), + ]); + // These will be the last line of STDERR + // The readline library will automatically trim off newlines + let stdErrLine1; + let stdErrLine2; + const rlErr1 = readline.createInterface(bootstrapProcess1.stderr!); + const rlErr2 = readline.createInterface(bootstrapProcess2.stderr!); + rlErr1.on('line', (l) => { + stdErrLine1 = l; + }); + rlErr2.on('line', (l) => { + stdErrLine2 = l; + }); + const [index, exitCode, signal] = await new Promise< + [number, number | null, NodeJS.Signals | null] + >((resolve) => { + bootstrapProcess1.once('exit', (code, signal) => { + resolve([0, code, signal]); + }); + bootstrapProcess2.once('exit', (code, signal) => { + resolve([1, code, signal]); + }); + }); + const errorStatusLocked = new statusErrors.ErrorStatusLocked(); + expect(exitCode).toBe(errorStatusLocked.exitCode); + expect(signal).toBe(null); + const eOutput = binUtils + .outputFormatter({ + type: 'error', + name: errorStatusLocked.name, + description: errorStatusLocked.description, + message: errorStatusLocked.message, + }) + .trim(); + // It's either the first or second process + if (index === 0) { + expect(stdErrLine1).toBeDefined(); + expect(stdErrLine1).toBe(eOutput); + const [exitCode] = await testBinUtils.processExit(bootstrapProcess2); + expect(exitCode).toBe(0); + } else if (index === 1) { + expect(stdErrLine2).toBeDefined(); + expect(stdErrLine2).toBe(eOutput); + const [exitCode] = await testBinUtils.processExit(bootstrapProcess1); + expect(exitCode).toBe(0); + } + }); test( 'bootstrap when interrupted, requires fresh on next bootstrap', async () => { diff --git a/tests/bin/utils.ts b/tests/bin/utils.ts index 893cbc522..c56cf5a0e 100644 --- a/tests/bin/utils.ts +++ b/tests/bin/utils.ts @@ -23,6 +23,8 @@ async function pk(args: Array): Promise { /** * Runs pk command functionally with mocked STDIO * Both stdout and stderr are the entire output including newlines + * This can only be used serially, because the mocks it relies on are global singletons + * If it is used concurrently, the mocking side-effects can conflict * @param env Augments env for command execution * @param cwd Defaults to temporary directory */