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..ce802125d 100644 --- a/src/bin/agent/CommandStart.ts +++ b/src/bin/agent/CommandStart.ts @@ -1,35 +1,164 @@ +import type { StdioOptions } from 'child_process'; +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.rootKeyPairBits); + 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.addOption(binOptions.fresh); 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: { + rootKeyPairBits: options.rootKeyPairBits, + 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) { + 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 != null) { + const agentErrFile = await this.fs.promises.open( + options.backgroundErrFile, + 'w', + ); + stdio[2] = agentErrFile.fd; + } + const agentProcess = child_process.fork( + path.join(__dirname, '../polykey-agent'), + [], + { + cwd: process.cwd(), + env: process.env, + detached: true, + serialization: 'advanced', + stdio, + }, + ); + 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 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( + new binErrors.ErrorCLIPolykeyAgentProcess( + 'Agent process closed during fork', + { + code, + signal, + }, + ), + ); + }); + const messageIn: AgentChildProcessInput = { + logLevel: this.logger.getEffectiveLevel(), + 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, + // 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 () => { + 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..93387465c 100644 --- a/src/bin/bootstrap/CommandBootstrap.ts +++ b/src/bin/bootstrap/CommandBootstrap.ts @@ -1,48 +1,39 @@ -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.addOption(binOptions.rootKeyPairBits); + this.addOption(binOptions.fresh); 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({ + password, + nodePath: options.nodePath, + keysConfig: { + rootKeyPairBits: options.rootKeyPairBits, + 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-agent.ts b/src/bin/polykey-agent.ts new file mode 100644 index 000000000..7e06e3a6f --- /dev/null +++ b/src/bin/polykey-agent.ts @@ -0,0 +1,132 @@ +#!/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'; + +process.title = 'polykey-agent'; + +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!.bind(process)); + const { p: messageInP, resolveP: resolveMessageInP } = + promise(); + process.once('message', (data) => { + resolveMessageInP(data); + }); + const messageIn = await messageInP; + logger.setLevel(messageIn.logLevel); + 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/polykey.ts b/src/bin/polykey.ts index 28f978564..ac40d5373 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,37 @@ 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'; +process.title = 'polykey'; + 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/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..79c281887 --- /dev/null +++ b/src/bin/types.ts @@ -0,0 +1,66 @@ +import type { LogLevel } from '@matrixai/logger'; +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 = { + logLevel: LogLevel; + 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..983722998 --- /dev/null +++ b/src/bin/utils/options.ts @@ -0,0 +1,132 @@ +/** + * Options and Arguments used by commands + * Use PolykeyCommand.addOption or PolykeyCommand.addArgument + * @module + */ +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', +) + .env('PK_NODE_PATH') + .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); + +/** + * 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 + */ +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', +); + +const backgroundErrFile = new commander.Option( + '-bef, --background-err-file ', + '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, + verbose, + fresh, + nodeId, + clientHost, + clientPort, + ingressHost, + ingressPort, + recoveryCodeFile, + passwordFile, + background, + backgroundOutFile, + backgroundErrFile, + rootKeyPairBits, +}; 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..1e24566a2 100644 --- a/src/bootstrap/errors.ts +++ b/src/bootstrap/errors.ts @@ -1,15 +1,10 @@ -import { ErrorPolykey } from '../errors'; +import { ErrorPolykey, sysexits } from '../errors'; class ErrorBootstrap extends ErrorPolykey {} -class ErrorExistingState extends ErrorBootstrap { - description: string = 'Files already exist at node path'; - exitCode: number = 64; +class ErrorBootstrapExistingState extends ErrorBootstrap { + description = 'Node path is occupied with existing state'; + exitCode = sysexits.USAGE; } -class ErrorMalformedKeynode extends ErrorBootstrap { - description: string = 'Malformed Polykey state exists at node path'; - exitCode: number = 64; -} - -export { ErrorExistingState, ErrorMalformedKeynode }; +export { ErrorBootstrap, ErrorBootstrapExistingState }; 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..5d09a11ee --- /dev/null +++ b/src/bootstrap/utils.ts @@ -0,0 +1,193 @@ +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 * as bootstrapErrors from './errors'; +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'; +import * as utils from '../utils'; +import * as errors from '../errors'; + +/** + * Bootstraps the Node Path + */ +async function bootstrapState({ + password, + nodePath = config.defaults.nodePath, + keysConfig = {}, + fresh = false, + fs = require('fs'), + logger = new Logger(bootstrapState.name), +}: { + password: string; + nodePath?: string; + keysConfig?: { + rootKeyPairBits?: number; + rootCertDuration?: number; + dbKeyBits?: number; + 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}`); + 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); + 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 }); + 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 + // 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({ + ...keysConfig_, + keysPath, + password, + fs, + logger: logger.getChild(KeyManager.name), + 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..f96650dde 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,79 @@ 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) { + 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 = recoveredKeyPair; + await this.writeRootKeyPair(recoveredKeyPair, 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 +696,45 @@ class KeyManager { } } + /** + * 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) { + throw new keysErrors.ErrorRootKeysRead(e.message, { + errno: e.errno, + syscall: e.syscall, + code: e.code, + path: e.path, + }); + } + 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( keyPath: string, bits: number = 256, @@ -679,33 +817,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..216958b42 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 { + description = 'Status is locked by another process'; + exitCode = sysexits.TEMPFAIL; +} -export { ErrorStatus, ErrorStatusNotRunning, ErrorStatusLockFailed }; +class ErrorStatusRead extends ErrorStatus { + description = 'Failed to read status info'; + exitCode = sysexits.IOERR; +} + +class ErrorStatusWrite extends ErrorStatus { + description = 'Failed to write status info'; + exitCode = sysexits.IOERR; +} + +class ErrorStatusParse extends ErrorStatus { + description = '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..f823eb4c5 --- /dev/null +++ b/tests/bin/agent/agent.test.ts @@ -0,0 +1,386 @@ +// 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('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..fcce5f0e3 --- /dev/null +++ b/tests/bin/agent/start.test.ts @@ -0,0 +1,536 @@ +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 * as binUtils from '@/bin/utils'; +import config from '@/config'; +import * as testBinUtils 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', + async () => { + const password = 'abc123'; + const agentProcess = await testBinUtils.pkSpawn( + [ + 'agent', + 'start', + '--node-path', + path.join(dataDir, 'polykey'), + '--root-key-pair-bits', + '1024', + '--verbose', + ], + { + 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 testBinUtils.processExit(agentProcess); + 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, + ); + test( + 'start in background', + async () => { + const password = 'abc123'; + const passwordPath = path.join(dataDir, 'password'); + await fs.promises.writeFile(passwordPath, password); + const agentProcess = await testBinUtils.pkSpawn( + [ + 'agent', + 'start', + '--password-file', + passwordPath, + '--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'), + }, + 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, 'SIGINT'); + // Check for graceful exit + 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([ + testBinUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('agentProcess1'), + ), + testBinUtils.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('SIGQUIT'); + 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 testBinUtils.processExit( + agentProcess1, + ); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGQUIT'); + } + }, + global.defaultTimeout * 2, + ); + test( + 'concurrent bootstrap is coalesced', + async () => { + const password = 'abc123'; + // One of these processes is blocked + const [agentProcess, bootstrapProcess] = await Promise.all([ + testBinUtils.pkSpawn( + ['agent', 'start', '--root-key-pair-bits', '1024', '--verbose'], + { + PK_NODE_PATH: path.join(dataDir, 'polykey'), + PK_PASSWORD: password, + }, + dataDir, + logger.getChild('agentProcess'), + ), + testBinUtils.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 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 testBinUtils.processExit(agentProcess); + expect(exitCode).toBe(null); + expect(signal).toBe('SIGTERM'); + } + }, + global.defaultTimeout * 2, + ); + test( + 'start with existing state', + async () => { + const password = 'abc123'; + 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, + ); + const rlOut = readline.createInterface(agentProcess1.stdout!); + await new Promise((resolve, reject) => { + rlOut.once('line', resolve); + rlOut.once('close', reject); + }); + agentProcess1.kill('SIGHUP'); + const [exitCode1, signal1] = await testBinUtils.processExit( + agentProcess1, + ); + 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'), + 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('SIGHUP'); + const [exitCode2, signal2] = await testBinUtils.processExit( + agentProcess2, + ); + expect(exitCode2).toBe(null); + expect(signal2).toBe('SIGHUP'); + // Check for graceful exit + const statusInfo = (await status.readStatus())!; + expect(statusInfo.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 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', + ], + { + 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 testBinUtils.processExit(agentProcess2); + // 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, + ); + 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 f0f41503f..beae34f75 100644 --- a/tests/bin/bootstrap.test.ts +++ b/tests/bin/bootstrap.test.ts @@ -1,23 +1,22 @@ import os from 'os'; import path from 'path'; import fs from 'fs'; +import readline from 'readline'; +import Logger, { LogLevel, StreamHandler } from '@matrixai/logger'; +import { errors as statusErrors } from '@/status'; +import { errors as bootstrapErrors } from '@/bootstrap'; +import * as binUtils from '@/bin/utils'; +import * as testBinUtils from './utils'; -import { checkKeynodeState } from '@/bootstrap'; - -import * as utils from './utils'; - -describe.skip('CLI bootstrap', () => { +describe('bootstrap', () => { + const logger = new Logger('bootstrap test', LogLevel.WARN, [ + 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, { @@ -25,62 +24,229 @@ describe.skip('CLI bootstrap', () => { recursive: true, }); }); - - 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 fail to create keynode state if keynode exists.', async () => { - const result = await utils.pkStdio( - ['bootstrap', '-np', nodePath, '--password-file', passwordFile], - {}, + 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, - ); - 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], - {}, + )); + 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(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(exitCode).toBe(0); + const recoveryCode = stdout.trim(); + expect( + recoveryCode.split(' ').length === 12 || + recoveryCode.split(' ').length === 24, + ).toBe(true); }); - test('Should fail to create keynode state if other files exists.', async () => { - await fs.promises.mkdir(path.join(nodePath, 'NOTAKEYNODEDIR'), { - recursive: 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; }); - const result = await utils.pkStdio( - ['bootstrap', '-np', nodePath, '--password-file', passwordFile], - {}, - dataDir, - ); - 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'); + 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 () => { + 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 testBinUtils.processExit( + bootstrapProcess1, + ); + 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 * 2, + ); }); 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..c56cf5a0e 100644 --- a/tests/bin/utils.ts +++ b/tests/bin/utils.ts @@ -1,12 +1,17 @@ +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 main from '../../src/bin/polykey'; +import Logger from '@matrixai/logger'; +import main from '@/bin/polykey'; +import * as binUtils from '@/bin/utils'; +import * as statusErrors from '@/status/errors'; /** * Runs pk command functionally @@ -17,6 +22,9 @@ 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 */ @@ -45,6 +53,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 +92,11 @@ async function pkStdio( mockedStdout.mockRestore(); envRestore(); mockCwd.mockRestore(); + mockProcessRemoveListener.mockRestore(); + mockProcessOff.mockRestore(); + mockProcessAddListener.mockRestore(); + mockProcessOnce.mockRestore(); + mockProcessOn.mockRestore(); return { exitCode, stdout, @@ -68,6 +107,8 @@ 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 */ @@ -105,6 +146,7 @@ async function pkExec( { env, cwd, + windowsHide: true, }, (error, stdout, stderr) => { if (error != null && error.code === undefined) { @@ -123,6 +165,57 @@ 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) => { + // The readline library will trim newlines + logger.info(l); + }); + return subprocess; +} + /** * Runs pk command through subprocess expect wrapper * @throws assert.AssertionError when expectations fail @@ -191,4 +284,104 @@ async function pkExpect({ }); } -export { pk, pkStdio, pkExec, 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, + ); + }; +} + +/** + * 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 }; 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..7bad74e73 100644 --- a/tests/bootstrap/bootstrap.test.ts +++ b/tests/bootstrap/bootstrap.test.ts @@ -2,35 +2,25 @@ import fs from 'fs'; 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 bootstrapUtils from '@/bootstrap/utils'; +import { Status } from '@/status'; -import * as bootstrapErrors from '@/bootstrap/errors'; -import * as agentUtils from '@/agent/utils'; +jest.mock('@/keys/utils', () => ({ + ...jest.requireActual('@/keys/utils'), + generateDeterministicKeyPair: + jest.requireActual('@/keys/utils').generateKeyPair, +})); -describe.skip('Bootstrap', () => { - const password = 'password'; +describe('Bootstrap', () => { const logger = new Logger('AgentServerTest', LogLevel.WARN, [ new StreamHandler(), ]); 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(), '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 +31,51 @@ 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'); - }); - - 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); - }); - - test('should throw error if keynode already exists.', async () => { - await fakeKeynode(nodePath); - await expect(() => - bootstrapPolykeyState(nodePath, password), - ).rejects.toThrow(bootstrapErrors.ErrorMalformedKeynode); - }); - - test('should be able to start agent on created state.', async () => { - await bootstrapPolykeyState(nodePath, password); - const polykeyAgent = await PolykeyAgent.createPolykeyAgent({ + await bootstrapUtils.bootstrapState({ + nodePath, password, - nodePath: nodePath, - logger: 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/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; 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(