From e03d951ad17e63b4aee498394b1892ff29c21cdf Mon Sep 17 00:00:00 2001 From: Robert Cronin Date: Mon, 24 Aug 2020 14:08:05 +0800 Subject: [PATCH] Expand Testing - Source Files --- .gitlab-ci.yml | 1 + package.json | 2 +- proto/Agent.proto | 3 +- src/cli/Agent.ts | 9 - src/cli/Crypto.ts | 12 +- src/cli/Keys.ts | 10 +- src/cli/Secrets.ts | 128 ++++---- src/cli/Vaults.ts | 1 - src/lib/agent/PolykeyAgent.ts | 55 ++-- src/lib/agent/PolykeyClient.ts | 66 +++-- src/lib/git/GitBackend.ts | 124 ++++---- src/lib/git/GitFrontend.ts | 62 ++++ src/lib/git/{GitClient.ts => GitRequest.ts} | 65 +---- src/lib/keys/KeyManager.ts | 38 ++- src/lib/keys/KeyManagerWorker.ts | 5 +- src/lib/peers/PeerManager.ts | 30 +- src/lib/vaults/Vault.ts | 4 +- src/lib/vaults/VaultManager.ts | 8 +- tests/cli/CLI.test.ts | 303 +++++++++++++++++++ tests/lib/agent/Agent.test.ts | 202 +++++++++++-- tests/lib/git/Git.test.ts | 194 +++++++++++++ tests/lib/keys/KeyManager.test.ts | 84 ++++++ tests/lib/peers/PKI.test.ts | 47 +++ tests/lib/peers/PeerManager.test.ts | 90 ++++++ tests/lib/vaults/Vaults.test.ts | 307 ++++++++++++++++++++ 25 files changed, 1517 insertions(+), 333 deletions(-) create mode 100644 src/lib/git/GitFrontend.ts rename src/lib/git/{GitClient.ts => GitRequest.ts} (52%) create mode 100644 tests/cli/CLI.test.ts create mode 100644 tests/lib/git/Git.test.ts create mode 100644 tests/lib/keys/KeyManager.test.ts create mode 100644 tests/lib/peers/PKI.test.ts create mode 100644 tests/lib/peers/PeerManager.test.ts create mode 100644 tests/lib/vaults/Vaults.test.ts diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6fdb078739..7256bacf84 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -28,6 +28,7 @@ jest: stage: check image: node:12 script: + - npm run build:all - npm run test build_all: diff --git a/package.json b/package.json index 1fd87c5b70..a75cd479b6 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "futoin-hkdf": "^1.3.2", "google-auth-library": "^6.0.5", "google-protobuf": "^4.0.0-rc.2", - "isomorphic-git": "^1.5.0", + "isomorphic-git": "^1.7.4", "kbpgp": "^2.0.82", "keybase-bot": "^3.6.1", "node-forge": "^0.9.1", diff --git a/proto/Agent.proto b/proto/Agent.proto index ab5433f2a9..eba2dae2c5 100644 --- a/proto/Agent.proto +++ b/proto/Agent.proto @@ -77,8 +77,7 @@ message SignFileResponseMessage { // ==== VerifyFile ==== // message VerifyFileRequestMessage { string file_path = 1; - string signature_path = 2; - string public_key_path = 3; + string public_key_path = 2; } message VerifyFileResponseMessage { bool verified = 1; diff --git a/src/cli/Agent.ts b/src/cli/Agent.ts index f4aa0f023a..24558b512a 100644 --- a/src/cli/Agent.ts +++ b/src/cli/Agent.ts @@ -19,7 +19,6 @@ function makeStartAgentCommand() { const pid = await PolykeyAgent.startAgent(daemon); pkLogger(`agent has started with pid of ${pid}`, PKMessageType.SUCCESS); } - process.exit(); }), ); } @@ -36,7 +35,6 @@ function makeRestartAgentCommand() { const daemon: boolean = options.daemon; const pid = await PolykeyAgent.startAgent(daemon); pkLogger(`agent has restarted with pid of ${pid}`, PKMessageType.SUCCESS); - process.exit(); }), ); } @@ -47,7 +45,6 @@ function makeAgentStatusCommand() { const client = PolykeyAgent.connectToAgent(); const status = await client.getAgentStatus(); pkLogger(`agent status is: '${status}'`, PKMessageType.INFO); - process.exit(); }), ); } @@ -76,7 +73,6 @@ function makeStopAgentCommand() { throw Error('agent failed to stop'); } } - process.exit(); }), ); } @@ -98,7 +94,6 @@ function makeListNodesCommand() { pkLogger(node, PKMessageType.INFO); } } - process.exit(); }), ); } @@ -127,8 +122,6 @@ function makeNewNodeCommand() { } else { throw Error('something went wrong with node creation'); } - - process.exit(); }), ); } @@ -150,8 +143,6 @@ function makeLoadNodeCommand() { } else { throw Error('something went wrong when loading node'); } - - process.exit(); }), ); } diff --git a/src/cli/Crypto.ts b/src/cli/Crypto.ts index 8263db454e..4b39d3cdae 100644 --- a/src/cli/Crypto.ts +++ b/src/cli/Crypto.ts @@ -47,8 +47,7 @@ function makeVerifyCommand() { '-k, --public-key ', 'path to public key that will be used to verify files, defaults to primary key', ) - .option('-s, --detach-sig ', 'path to detached signature for file, defaults to [filename].sig') - .requiredOption('-f, --verified-file ', 'file to be verified') + .requiredOption('-f, --signed-file ', 'file to be verified') .action( actionRunner(async (options) => { const client = PolykeyAgent.connectToAgent(); @@ -60,9 +59,8 @@ function makeVerifyCommand() { const nodePath = determineNodePath(options); const publicKey = options.publicKey; const filePath = options.signedFile; - const signaturePath = options.detachSig ?? filePath + '.sig'; - const verified = await client.verifyFile(nodePath, filePath, signaturePath); + const verified = await client.verifyFile(nodePath, filePath, publicKey); if (verified) { pkLogger(`file '${filePath}' was successfully verified`, PKMessageType.SUCCESS); } else { @@ -91,7 +89,7 @@ function makeEncryptCommand() { const nodePath = determineNodePath(options); const publicKey = options.publicKey; - const filePath = options.filePath + const filePath = options.filePath; try { const encryptedPath = await client.encryptFile(nodePath, filePath, publicKey); @@ -124,7 +122,7 @@ function makeDecryptCommand() { const privateKey = options.privateKey; const keyPassphrase = options.keyPassphrase; - const filePath = options.filePath + const filePath = options.filePath; try { const decryptedPath = await client.decryptFile(nodePath, filePath, privateKey, keyPassphrase); @@ -142,7 +140,7 @@ function makeCryptoCommand() { .addCommand(makeVerifyCommand()) .addCommand(makeSignCommand()) .addCommand(makeEncryptCommand()) - .addCommand(makeDecryptCommand()) + .addCommand(makeDecryptCommand()); } export default makeCryptoCommand; diff --git a/src/cli/Keys.ts b/src/cli/Keys.ts index 46fad5025f..ea50f1d87a 100644 --- a/src/cli/Keys.ts +++ b/src/cli/Keys.ts @@ -34,7 +34,7 @@ function makeDeleteKeyCommand() { const successful = await client.deleteKey(nodePath, keyName); pkLogger( - `key '${keyName}' was ${successful ? '' : 'un-'}sucessfully deleted`, + `key '${keyName}' was ${successful ? '' : 'un-'}successfully deleted`, successful ? PKMessageType.SUCCESS : PKMessageType.INFO, ); }), @@ -93,14 +93,14 @@ function makeListPrimaryKeyPairCommand() { if (outputJson) { pkLogger(JSON.stringify(keypair), PKMessageType.INFO); } else { - pkLogger("Public Key:", PKMessageType.SUCCESS); + pkLogger('Public Key:', PKMessageType.SUCCESS); pkLogger(keypair.publicKey, PKMessageType.INFO); if (privateKey) { - pkLogger("Private Key:", PKMessageType.SUCCESS); + pkLogger('Private Key:', PKMessageType.SUCCESS); pkLogger(keypair.privateKey, PKMessageType.INFO); } } - }) + }), ); } @@ -111,7 +111,7 @@ function makeKeyManagerCommand() { .addCommand(makeDeleteKeyCommand()) .addCommand(makeListKeysCommand()) .addCommand(makeGetKeyCommand()) - .addCommand(makeListPrimaryKeyPairCommand()) + .addCommand(makeListPrimaryKeyPairCommand()); } export default makeKeyManagerCommand; diff --git a/src/cli/Secrets.ts b/src/cli/Secrets.ts index 6637984b36..608e492f8f 100644 --- a/src/cli/Secrets.ts +++ b/src/cli/Secrets.ts @@ -4,8 +4,7 @@ import { spawn, ChildProcess, exec } from 'child_process'; import { PolykeyAgent } from '../lib/Polykey'; import { actionRunner, pkLogger, PKMessageType, determineNodePath } from '.'; - -const pathRegex = /^([a-zA-Z0-9_ -]+)(?::)([a-zA-Z0-9_ -]+)(?:=)?([a-zA-Z_][a-zA-Z0-9_]+)?$/ +const pathRegex = /^([a-zA-Z0-9_ -]+)(?::)([a-zA-Z0-9_ -]+)(?:=)?([a-zA-Z_][a-zA-Z0-9_]+)?$/; function makeListSecretsCommand() { return new commander.Command('list') @@ -23,10 +22,9 @@ function makeListSecretsCommand() { const vaultNames: string[] = Array.from(options.args.values()); if (!vaultNames.length) { - throw Error('no vault names provided') + throw Error('no vault names provided'); } - for (const vaultName of vaultNames) { // Get list of secrets from pk const secretNames = await client.listSecrets(nodePath, vaultName); @@ -49,7 +47,7 @@ function makeListSecretsCommand() { function makeNewSecretCommand() { return new commander.Command('new') - .description('create a secret within a given vault') + .description("create a secret within a given vault, specify an secret path with ':'") .option('--node-path ', 'node path') .arguments("secret path of the format ':'") .requiredOption('-f, --file-path ', 'path to the secret to be added') @@ -60,20 +58,20 @@ function makeNewSecretCommand() { const nodePath = determineNodePath(options); const isVerbose: boolean = options.verbose ?? false; - const secretPath: string[] = Array.from(options.args.values()) + const secretPath: string[] = Array.from(options.args.values()); if (secretPath.length < 1 || (secretPath.length == 1 && !pathRegex.test(secretPath[0]))) { - throw Error("please specify a new secret name using the format: ':'") + throw Error("please specify a new secret name using the format: ':'"); } else if (secretPath.length > 1) { - throw Error('you can only add one secret at a time') + throw Error('you can only add one secret at a time'); } - const firstEntry = secretPath[0] - const [_, vaultName, secretName] = firstEntry.match(pathRegex)! + const firstEntry = secretPath[0]; + const [_, vaultName, secretName] = firstEntry.match(pathRegex)!; const filePath: string = options.filePath; try { // Add the secret const successful = await client.createSecret(nodePath, vaultName, secretName, filePath); pkLogger( - `secret '${secretName}' was ${successful ? '' : 'un-'}sucessfully added to vault '${vaultName}'`, + `secret '${secretName}' was ${successful ? '' : 'un-'}successfully added to vault '${vaultName}'`, PKMessageType.SUCCESS, ); } catch (err) { @@ -85,7 +83,7 @@ function makeNewSecretCommand() { function makeUpdateSecretCommand() { return new commander.Command('update') - .description('update a secret within a given vault') + .description("update a secret within a given vault, specify an secret path with ':'") .option('--node-path ', 'node path') .arguments("secret path of the format ':'") .requiredOption('-f, --file-path ', 'path to the new secret') @@ -96,20 +94,20 @@ function makeUpdateSecretCommand() { const nodePath = determineNodePath(options); const isVerbose: boolean = options.verbose ?? false; - const secretPath: string[] = Array.from(options.args.values()) + const secretPath: string[] = Array.from(options.args.values()); if (secretPath.length < 1 || (secretPath.length == 1 && !pathRegex.test(secretPath[0]))) { - throw Error("please specify the secret using the format: ':'") + throw Error("please specify the secret using the format: ':'"); } else if (secretPath.length > 1) { - throw Error('you can only update one secret at a time') + throw Error('you can only update one secret at a time'); } - const firstEntry = secretPath[0] - const [_, vaultName, secretName] = firstEntry.match(pathRegex)! + const firstEntry = secretPath[0]; + const [_, vaultName, secretName] = firstEntry.match(pathRegex)!; const filePath: string = options.filePath; try { // Update the secret const successful = await client.updateSecret(nodePath, vaultName, secretName, filePath); pkLogger( - `secret '${secretName}' was ${successful ? '' : 'un-'}sucessfully updated in vault '${vaultName}'`, + `secret '${secretName}' was ${successful ? '' : 'un-'}successfully updated in vault '${vaultName}'`, successful ? PKMessageType.SUCCESS : PKMessageType.WARNING, ); } catch (err) { @@ -122,7 +120,7 @@ function makeUpdateSecretCommand() { function makeDeleteSecretCommand() { return new commander.Command('delete') .alias('del') - .description('delete a secret from a given vault') + .description("delete a secret from a given vault, specify an secret path with ':'") .arguments("secret path of the format ':'") .option('--verbose', 'increase verbosity level by one') .action( @@ -131,19 +129,19 @@ function makeDeleteSecretCommand() { const nodePath = determineNodePath(options); const isVerbose: boolean = options.verbose ?? false; - const secretPath: string[] = Array.from(options.args.values()) + const secretPath: string[] = Array.from(options.args.values()); if (secretPath.length < 1 || (secretPath.length == 1 && !pathRegex.test(secretPath[0]))) { - throw Error("please specify the secret using the format: ':'") + throw Error("please specify the secret using the format: ':'"); } else if (secretPath.length > 1) { - throw Error('you can only delete one secret at a time') + throw Error('you can only delete one secret at a time'); } - const firstEntry = secretPath[0] - const [_, vaultName, secretName] = firstEntry.match(pathRegex)! + const firstEntry = secretPath[0]; + const [_, vaultName, secretName] = firstEntry.match(pathRegex)!; try { // Remove secret const successful = await client.destroySecret(nodePath, vaultName, secretName); pkLogger( - `secret '${secretName}' was ${successful ? '' : 'un-'}sucessfully removed from vault '${vaultName}'`, + `secret '${secretName}' was ${successful ? '' : 'un-'}successfully removed from vault '${vaultName}'`, PKMessageType.SUCCESS, ); } catch (err) { @@ -155,7 +153,7 @@ function makeDeleteSecretCommand() { function makeGetSecretCommand() { return new commander.Command('get') - .description('retrieve a secret from a given vault') + .description("retrieve a secret from a given vault, specify an secret path with ':'") .arguments("secret path of the format ':'") .option('-e, --env', 'wrap the secret in an environment variable declaration') .action( @@ -165,22 +163,19 @@ function makeGetSecretCommand() { const isEnv: boolean = options.env ?? false; const isVerbose: boolean = options.verbose ?? false; - const secretPath: string[] = Array.from(options.args.values()) + const secretPath: string[] = Array.from(options.args.values()); if (secretPath.length < 1 || (secretPath.length == 1 && !pathRegex.test(secretPath[0]))) { - throw Error("please specify the secret using the format: ':'") + throw Error("please specify the secret using the format: ':'"); } else if (secretPath.length > 1) { - throw Error('you can only get one secret at a time') + throw Error('you can only get one secret at a time'); } - const firstEntry = secretPath[0] - const [_, vaultName, secretName] = firstEntry.match(pathRegex)! + const firstEntry = secretPath[0]; + const [_, vaultName, secretName] = firstEntry.match(pathRegex)!; try { // Retrieve secret const secret = await client.getSecret(nodePath, vaultName, secretName); if (isEnv) { - pkLogger( - `export ${secretName.toUpperCase().replace('-', '_')}='${secret.toString()}'`, - PKMessageType.none - ); + pkLogger(`export ${secretName.toUpperCase().replace('-', '_')}='${secret.toString()}'`, PKMessageType.none); } else { pkLogger(secret.toString(), PKMessageType.none); } @@ -194,13 +189,23 @@ function makeGetSecretCommand() { function makeSecretEnvCommand() { return new commander.Command('env') .storeOptionsAsProperties(false) - .description('run a modified environment with injected secrets') - .option('--command ', 'In the environment of the derivation, run the shell command cmd. This command is executed in an interactive shell. (Use --run to use a non-interactive shell instead.)') - .option('--run ', 'Like --command, but executes the command in a non-interactive shell. This means (among other things) that if you hit Ctrl-C while the command is running, the shell exits.') - .arguments("secrets to inject into env, of the format ':'. you can also control what the environment variable will be called using ':=', defaults to upper, snake case of the original secret name.") + .description( + "run a modified environment with injected secrets, specify an secret path with ':'", + ) + .option( + '--command ', + 'In the environment of the derivation, run the shell command cmd. This command is executed in an interactive shell. (Use --run to use a non-interactive shell instead.)', + ) + .option( + '--run ', + 'Like --command, but executes the command in a non-interactive shell. This means (among other things) that if you hit Ctrl-C while the command is running, the shell exits.', + ) + .arguments( + "secrets to inject into env, of the format ':'. you can also control what the environment variable will be called using ':=', defaults to upper, snake case of the original secret name.", + ) .action( actionRunner(async (cmd) => { - const options = cmd.opts() + const options = cmd.opts(); const client = PolykeyAgent.connectToAgent(); const nodePath = determineNodePath(options); @@ -209,60 +214,59 @@ function makeSecretEnvCommand() { const command: string | undefined = options.command; const run: string | undefined = options.run; - const secretPathList: string[] = Array.from(cmd.args.values()) - + const secretPathList: string[] = Array.from(cmd.args.values()); if (secretPathList.length < 1) { - throw Error("please specify at least one secret") + throw Error('please specify at least one secret'); } // Parse secret paths in list - const parsedPathList: { vaultName: string, secretName: string, variableName: string }[] = [] + const parsedPathList: { vaultName: string; secretName: string; variableName: string }[] = []; for (const path of secretPathList) { if (!pathRegex.test(path)) { - throw Error(`secret path was not of the format ':[=]': ${path}`) + throw Error(`secret path was not of the format ':[=]': ${path}`); } - const [_, vaultName, secretName, variableName] = path.match(pathRegex)! + const [_, vaultName, secretName, variableName] = path.match(pathRegex)!; parsedPathList.push({ vaultName, secretName, - variableName: variableName ?? secretName.toUpperCase().replace('-', '_') - }) + variableName: variableName ?? secretName.toUpperCase().replace('-', '_'), + }); } - const secretEnv = { ...process.env } + const secretEnv = { ...process.env }; try { // Get all the secrets for (const obj of parsedPathList) { const secret = await client.getSecret(nodePath, obj.vaultName, obj.secretName); - secretEnv[obj.variableName] = secret.toString() + secretEnv[obj.variableName] = secret.toString(); } } catch (err) { throw Error(`Error when retrieving secret: ${err.message}`); } try { - const shellPath = process.env.SHELL! - const args: string[] = [] + const shellPath = process.env.SHELL!; + const args: string[] = []; if (command && run) { - throw Error('only one of --command or --run can be specified') + throw Error('only one of --command or --run can be specified'); } else if (command) { - args.push('-i') - args.push('-c') - args.push(`"${command}"`) + args.push('-i'); + args.push('-c'); + args.push(`"${command}"`); } else if (run) { - args.push('-c') - args.push(`"${run}"`) + args.push('-c'); + args.push(`"${run}"`); } const shell = spawn(shellPath, args, { stdio: 'inherit', env: secretEnv, - shell: true - }) + shell: true, + }); shell.on('close', (code) => { if (code != 0) { - pkLogger(`polykey: environment terminated with code: ${code}`, PKMessageType.WARNING) + pkLogger(`polykey: environment terminated with code: ${code}`, PKMessageType.WARNING); } - }) + }); } catch (err) { throw Error(`Error when running environment: ${err.message}`); } diff --git a/src/cli/Vaults.ts b/src/cli/Vaults.ts index 28f1659ffe..17760b4a83 100644 --- a/src/cli/Vaults.ts +++ b/src/cli/Vaults.ts @@ -69,7 +69,6 @@ function makeDeleteVaultCommand() { const successful = await client.destroyVault(nodePath, vaultName); pkLogger(`vault '${vaultName}' destroyed ${successful ? 'un-' : ''}successfully`, PKMessageType.SUCCESS); } - }), ); } diff --git a/src/lib/agent/PolykeyAgent.ts b/src/lib/agent/PolykeyAgent.ts index 2aa5b7a014..a89f65c813 100644 --- a/src/lib/agent/PolykeyAgent.ts +++ b/src/lib/agent/PolykeyAgent.ts @@ -75,22 +75,23 @@ class PolykeyAgent { nodePathSet.delete(nodePath); this.persistentStore.set('nodePaths', Array.from(nodePathSet.values())); } + private getPolyKey(nodePath: string, failOnLocked: boolean = true): Polykey { - const pk = this.polykeyMap.get(nodePath) + const pk = this.polykeyMap.get(nodePath); if (this.polykeyMap.has(nodePath) && pk) { if (fs.existsSync(nodePath)) { if (failOnLocked && !pk.keyManager.identityLoaded) { - throw Error(`node path exists in memory but is locked: ${nodePath}`) + throw Error(`node path exists in memory but is locked: ${nodePath}`); } else { - return pk + return pk; } } else { - this.removeNodePath(nodePath) - throw Error(`node path exists in memory but does not exist on file system: ${nodePath}`) + this.removeNodePath(nodePath); + throw Error(`node path exists in memory but does not exist on file system: ${nodePath}`); } } else { - this.removeNodePath(nodePath) - throw Error(`node path does not exist in memory: ${nodePath}`) + this.removeNodePath(nodePath); + throw Error(`node path does not exist in memory: ${nodePath}`); } } @@ -260,8 +261,9 @@ class PolykeyAgent { private async registerNode(nodePath: string, request: Uint8Array) { const { passphrase } = RegisterNodeRequestMessage.decode(request); - let pk: Polykey | undefined = this.getPolyKey(nodePath, false); - if (pk) { + let pk: Polykey; + if (this.polykeyMap.has(nodePath)) { + pk = this.getPolyKey(nodePath, false); if (pk.keyManager.identityLoaded) { throw Error(`node path is already loaded and unlocked: '${nodePath}'`); } @@ -273,9 +275,9 @@ class PolykeyAgent { pk = new Polykey(nodePath, fs, km); } // Load all metadata - await pk.keyManager.loadMetadata() - pk.peerManager.loadMetadata() - await pk.vaultManager.loadMetadata() + await pk.keyManager.loadMetadata(); + pk.peerManager.loadMetadata(); + await pk.vaultManager.loadMetadata(); // Set polykey class this.setPolyKey(nodePath, pk); @@ -350,7 +352,10 @@ class PolykeyAgent { const { includePrivateKey } = GetPrimaryKeyPairRequestMessage.decode(request); const pk = this.getPolyKey(nodePath); const keypair = pk.keyManager.getKeyPair(); - return GetPrimaryKeyPairResponseMessage.encode({ publicKey: keypair.public, privateKey: includePrivateKey ? keypair.private : undefined }).finish(); + return GetPrimaryKeyPairResponseMessage.encode({ + publicKey: keypair.public, + privateKey: includePrivateKey ? keypair.private : undefined, + }).finish(); } private async deleteKey(nodePath: string, request: Uint8Array) { const { keyName } = DeleteKeyRequestMessage.decode(request); @@ -369,9 +374,9 @@ class PolykeyAgent { return SignFileResponseMessage.encode({ signaturePath }).finish(); } private async verifyFile(nodePath: string, request: Uint8Array) { - const { filePath, signaturePath } = VerifyFileRequestMessage.decode(request); + const { filePath, publicKeyPath } = VerifyFileRequestMessage.decode(request); const pk = this.getPolyKey(nodePath); - const verified = await pk.keyManager.verifyFile(filePath, signaturePath); + const verified = await pk.keyManager.verifyFile(filePath, publicKeyPath); return VerifyFileResponseMessage.encode({ verified }).finish(); } private async encryptFile(nodePath: string, request: Uint8Array) { @@ -422,11 +427,11 @@ class PolykeyAgent { const { vaultName, secretName, secretPath, secretContent } = CreateSecretRequestMessage.decode(request); const pk = this.getPolyKey(nodePath); const vault = pk.vaultManager.getVault(vaultName); - let secretBuffer: Buffer + let secretBuffer: Buffer; if (secretPath) { secretBuffer = await fs.promises.readFile(secretPath); } else { - secretBuffer = Buffer.from(secretContent) + secretBuffer = Buffer.from(secretContent); } await vault.addSecret(secretName, secretBuffer); return CreateSecretResponseMessage.encode({ successful: true }).finish(); @@ -449,11 +454,11 @@ class PolykeyAgent { const { vaultName, secretName, secretPath, secretContent } = UpdateSecretRequestMessage.decode(request); const pk = this.getPolyKey(nodePath); const vault = pk.vaultManager.getVault(vaultName); - let secretBuffer: Buffer + let secretBuffer: Buffer; if (secretPath) { secretBuffer = await fs.promises.readFile(secretPath); } else { - secretBuffer = Buffer.from(secretContent) + secretBuffer = Buffer.from(secretContent); } await vault.updateSecret(secretName, secretBuffer); return UpdateSecretResponseMessage.encode({ successful: true }).finish(); @@ -477,7 +482,9 @@ class PolykeyAgent { static get SocketPath(): string { const platform = os.platform(); const userInfo = os.userInfo(); - if (platform == 'win32') { + if (process.env.PK_SOCKET_PATH) { + return process.env.PK_SOCKET_PATH; + } else if (platform == 'win32') { return path.join('\\\\?\\pipe', process.cwd(), 'polykey-agent'); } else { return `/run/user/${userInfo.uid}/polykey/S.polykey-agent`; @@ -487,7 +494,9 @@ class PolykeyAgent { public static get LogPath(): string { const platform = os.platform(); const userInfo = os.userInfo(); - if (platform == 'win32') { + if (process.env.PK_LOG_PATH) { + return process.env.PK_LOG_PATH; + } else if (platform == 'win32') { return path.join(os.tmpdir(), 'polykey', 'log'); } else { return `/run/user/${userInfo.uid}/polykey/log`; @@ -512,12 +521,14 @@ class PolykeyAgent { 'ipc', fs.openSync(path.join(PolykeyAgent.LogPath, 'output.log'), 'a'), fs.openSync(path.join(PolykeyAgent.LogPath, 'error.log'), 'a'), - ] + ], + silent: true, }; const agentProcess = fork(PolykeyAgent.DAEMON_SCRIPT_PATH, undefined, options); const pid = agentProcess.pid; agentProcess.unref(); + agentProcess.disconnect(); resolve(pid); } catch (err) { reject(err); diff --git a/src/lib/agent/PolykeyClient.ts b/src/lib/agent/PolykeyClient.ts index a3e85d8897..eee20a816e 100644 --- a/src/lib/agent/PolykeyClient.ts +++ b/src/lib/agent/PolykeyClient.ts @@ -63,7 +63,7 @@ class PolykeyClient { if (data instanceof Uint8Array) { responseList.push(data); } else { - responseList.push(...data) + responseList.push(...data); } }); stream.on('error', (err) => { @@ -115,9 +115,13 @@ class PolykeyClient { async registerNode(path: string, passphrase: string) { const registerNodeRequest = RegisterNodeRequestMessage.encode({ passphrase }).finish(); - const encodedResponse = await this.handleAgentCommunication(AgentMessageType.REGISTER_NODE, path, registerNodeRequest); + const encodedResponse = await this.handleAgentCommunication( + AgentMessageType.REGISTER_NODE, + path, + registerNodeRequest, + ); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.REGISTER_NODE)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.REGISTER_NODE)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -132,7 +136,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.NEW_NODE, path, newNodeRequest); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.NEW_NODE)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.NEW_NODE)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -147,7 +151,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.LIST_NODES, undefined, newNodeRequest); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.LIST_NODES)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.LIST_NODES)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -164,7 +168,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.DERIVE_KEY, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DERIVE_KEY)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DERIVE_KEY)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -177,7 +181,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.DELETE_KEY, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DELETE_KEY)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DELETE_KEY)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -190,7 +194,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.LIST_KEYS, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.LIST_KEYS)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.LIST_KEYS)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -203,7 +207,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.GET_KEY, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.GET_KEY)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.GET_KEY)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -214,9 +218,13 @@ class PolykeyClient { async getPrimaryKeyPair(nodePath: string, includePrivateKey: boolean = false) { const request = GetPrimaryKeyPairRequestMessage.encode({ includePrivateKey }).finish(); - const encodedResponse = await this.handleAgentCommunication(AgentMessageType.GET_PRIMARY_KEYPAIR, nodePath, request); + const encodedResponse = await this.handleAgentCommunication( + AgentMessageType.GET_PRIMARY_KEYPAIR, + nodePath, + request, + ); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.GET_PRIMARY_KEYPAIR)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.GET_PRIMARY_KEYPAIR)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -233,7 +241,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.SIGN_FILE, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.SIGN_FILE)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.SIGN_FILE)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -241,12 +249,12 @@ class PolykeyClient { const { signaturePath } = SignFileResponseMessage.decode(subMessage); return signaturePath; } - async verifyFile(nodePath: string, filePath: string, signaturePath?: string) { - const request = VerifyFileRequestMessage.encode({ filePath, signaturePath }).finish(); + async verifyFile(nodePath: string, filePath: string, publicKeyPath?: string) { + const request = VerifyFileRequestMessage.encode({ filePath, publicKeyPath }).finish(); const encodedResponse = await this.handleAgentCommunication(AgentMessageType.VERIFY_FILE, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.VERIFY_FILE)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.VERIFY_FILE)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -259,7 +267,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.ENCRYPT_FILE, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.ENCRYPT_FILE)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.ENCRYPT_FILE)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -272,7 +280,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.DECRYPT_FILE, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DECRYPT_FILE)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DECRYPT_FILE)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -287,7 +295,7 @@ class PolykeyClient { async listVaults(nodePath: string) { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.LIST_VAULTS, nodePath); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.LIST_VAULTS)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.LIST_VAULTS)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -300,7 +308,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.NEW_VAULT, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.NEW_VAULT)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.NEW_VAULT)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -313,7 +321,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.DESTROY_VAULT, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DESTROY_VAULT)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DESTROY_VAULT)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -329,7 +337,7 @@ class PolykeyClient { const request = ListSecretsRequestMessage.encode({ vaultName }).finish(); const encodedResponse = await this.handleAgentCommunication(AgentMessageType.LIST_SECRETS, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.LIST_SECRETS)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.LIST_SECRETS)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -338,7 +346,7 @@ class PolykeyClient { return secretNames; } async createSecret(nodePath: string, vaultName: string, secretName: string, secret: string | Buffer) { - let request: Uint8Array + let request: Uint8Array; if (typeof secret == 'string') { request = CreateSecretRequestMessage.encode({ vaultName, secretName, secretPath: secret }).finish(); } else { @@ -347,7 +355,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.CREATE_SECRET, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.CREATE_SECRET)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.CREATE_SECRET)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -360,7 +368,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.DESTROY_SECRET, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DESTROY_SECRET)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.DESTROY_SECRET)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -373,7 +381,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.GET_SECRET, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.GET_SECRET)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.GET_SECRET)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -382,7 +390,7 @@ class PolykeyClient { return Buffer.from(secret); } async updateSecret(nodePath: string, vaultName: string, secretName: string, secret: string | Buffer) { - let request: Uint8Array + let request: Uint8Array; if (typeof secret == 'string') { request = UpdateSecretRequestMessage.encode({ vaultName, secretName, secretPath: secret }).finish(); } else { @@ -391,7 +399,7 @@ class PolykeyClient { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.UPDATE_SECRET, nodePath, request); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.UPDATE_SECRET)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.UPDATE_SECRET)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -407,7 +415,7 @@ class PolykeyClient { try { const encodedResponse = await this.handleAgentCommunication(AgentMessageType.STATUS); - const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.STATUS)?.subMessage + const subMessage = encodedResponse.find((r) => r.type == AgentMessageType.STATUS)?.subMessage; if (!subMessage) { throw Error('agent did not respond'); } @@ -418,7 +426,7 @@ class PolykeyClient { if ((err).toString().match(/ECONNRESET|ENOENT|ECONNRESET/)) { return 'stopped'; } - throw err + throw err; } } async stopAgent(): Promise { diff --git a/src/lib/git/GitBackend.ts b/src/lib/git/GitBackend.ts index 03e7b198b8..cb48f23ed6 100644 --- a/src/lib/git/GitBackend.ts +++ b/src/lib/git/GitBackend.ts @@ -1,4 +1,5 @@ -import Path from 'path'; +import path from 'path'; +import { EncryptedFS } from 'encryptedfs'; import { PassThrough } from 'readable-stream'; import VaultManager from '../vaults/VaultManager'; import uploadPack from './upload-pack/uploadPack'; @@ -16,97 +17,76 @@ import packObjects from './pack-objects/packObjects'; // We need someway to notify other agents about what vaults we have based on some type of authorisation because they don't explicitly know about them class GitBackend { - private polykeyPath: string; - private vaultManager: VaultManager; - constructor(polykeyPath: string, vaultManager: VaultManager) { - this.polykeyPath = polykeyPath; - this.vaultManager = vaultManager; - } + private repoDirectoryPath: string; + private getFileSystem: (repoName: string) => EncryptedFS; - /** - * Find out whether vault exists. - * @param vaultName Name of vault to check - * @param publicKey Public key of peer trying to access vault - */ - private exists(vaultName: string, publicKey: string) { - try { - const vault = this.vaultManager.getVault(vaultName); - if (vault) { - return vault.peerCanAccess(publicKey); - } - return false; - } catch (error) { - return false; - } + constructor(repoDirectoryPath: string, getFileSystem: (repoName: string) => any) { + this.repoDirectoryPath = repoDirectoryPath; + this.getFileSystem = getFileSystem; } - async handleInfoRequest(vaultName: string): Promise { + async handleInfoRequest(repoName: string): Promise { // Only handle upload-pack for now const service = 'upload-pack'; - const connectingPublicKey = ''; + const fileSystem = this.getFileSystem(repoName); const responseBuffers: Buffer[] = []; - if (!this.exists(vaultName, connectingPublicKey)) { - throw Error(`vault does not exist: '${vaultName}'`); - } else { - responseBuffers.push(Buffer.from(this.createGitPacketLine('# service=git-' + service + '\n'))); - responseBuffers.push(Buffer.from('0000')); + if (!fileSystem.existsSync(path.join(this.repoDirectoryPath, repoName))) { + throw Error(`repository does not exist: '${repoName}'`); + } - const fileSystem = this.vaultManager.getVault(vaultName)?.EncryptedFS; + responseBuffers.push(Buffer.from(this.createGitPacketLine('# service=git-' + service + '\n'))); + responseBuffers.push(Buffer.from('0000')); - const buffers = await uploadPack(fileSystem, Path.join(this.polykeyPath, vaultName), undefined, true); - const buffersToWrite = buffers ?? []; - responseBuffers.push(...buffersToWrite); - } + const buffers = await uploadPack(fileSystem, path.join(this.repoDirectoryPath, repoName), undefined, true); + const buffersToWrite = buffers ?? []; + responseBuffers.push(...buffersToWrite); return Buffer.concat(responseBuffers); } - async handlePackRequest(vaultName: string, body: Buffer): Promise { + async handlePackRequest(repoName: string, body: Buffer): Promise { // eslint-disable-next-line return new Promise(async (resolve, reject) => { const responseBuffers: Buffer[] = []; - // Check if vault exists - const connectingPublicKey = ''; - if (!this.exists(vaultName, connectingPublicKey)) { - throw Error(`vault does not exist: '${vaultName}'`); + const fileSystem = this.getFileSystem(repoName); + + // Check if repo exists + if (!fileSystem.existsSync(path.join(this.repoDirectoryPath, repoName))) { + throw Error(`repository does not exist: '${repoName}'`); } - const fileSystem = this.vaultManager.getVault(vaultName)?.EncryptedFS; - - if (fileSystem) { - if (body.toString().slice(4, 8) == 'want') { - const wantedObjectId = body.toString().slice(9, 49); - const packResult = await packObjects( - fileSystem, - Path.join(this.polykeyPath, vaultName), - [wantedObjectId], - undefined, - ); - - // This the 'wait for more data' line as I understand it - responseBuffers.push(Buffer.from('0008NAK\n')); - - // This is to get the side band stuff working - const readable = new PassThrough(); - const progressStream = new PassThrough(); - const sideBand = GitSideBand.mux('side-band-64', readable, packResult.packstream, progressStream, []); - sideBand.on('data', (data: Buffer) => { - responseBuffers.push(data); - }); - sideBand.on('end', () => { - resolve(Buffer.concat(responseBuffers)); - }); - sideBand.on('error', (err) => { - reject(err); - }); - - // Write progress to the client - progressStream.write(Buffer.from('0014progress is at 50%\n')); - progressStream.end(); - } + if (body.toString().slice(4, 8) == 'want') { + const wantedObjectId = body.toString().slice(9, 49); + const packResult = await packObjects( + fileSystem, + path.join(this.repoDirectoryPath, repoName), + [wantedObjectId], + undefined, + ); + + // This the 'wait for more data' line as I understand it + responseBuffers.push(Buffer.from('0008NAK\n')); + + // This is to get the side band stuff working + const readable = new PassThrough(); + const progressStream = new PassThrough(); + const sideBand = GitSideBand.mux('side-band-64', readable, packResult.packstream, progressStream, []); + sideBand.on('data', (data: Buffer) => { + responseBuffers.push(data); + }); + sideBand.on('end', () => { + resolve(Buffer.concat(responseBuffers)); + }); + sideBand.on('error', (err) => { + reject(err); + }); + + // Write progress to the client + progressStream.write(Buffer.from('0014progress is at 50%\n')); + progressStream.end(); } }); } diff --git a/src/lib/git/GitFrontend.ts b/src/lib/git/GitFrontend.ts new file mode 100644 index 0000000000..fdcea689cc --- /dev/null +++ b/src/lib/git/GitFrontend.ts @@ -0,0 +1,62 @@ +import * as grpc from '@grpc/grpc-js'; +import { Address } from '../peers/PeerInfo'; +import KeyManager from '../keys/KeyManager'; +import { GitServerClient } from '../../../proto/compiled/Git_grpc_pb'; +import { InfoRequest, PackRequest } from '../../../proto/compiled/Git_pb'; + +/** + * Responsible for converting HTTP messages from isomorphic-git into requests and sending them to a specific peer. + */ +class GitFrontend { + private client: GitServerClient; + private credentials: grpc.ChannelCredentials; + + constructor(address: Address, keyManager: KeyManager) { + const pkiInfo = keyManager.PKIInfo; + if (pkiInfo.caCert && pkiInfo.cert && pkiInfo.key) { + this.credentials = grpc.credentials.createSsl(pkiInfo.caCert, pkiInfo.key, pkiInfo.cert); + } else { + this.credentials = grpc.credentials.createInsecure(); + } + this.client = new GitServerClient(address.toString(), this.credentials); + } + + /** + * Requests remote info from the connected peer for the named vault. + * @param vaultName Name of the desired vault + */ + async requestInfo(vaultName: string): Promise { + return new Promise((resolve, reject) => { + const request = new InfoRequest(); + request.setVaultname(vaultName); + this.client.requestInfo(request, function (err, response) { + if (err) { + reject(err); + } else { + resolve(Buffer.from(response.getBody_asB64(), 'base64')); + } + }); + }); + } + + /** + * Requests a pack from the connected peer for the named vault. + * @param vaultName Name of the desired vault + */ + async requestPack(vaultName: string, body: Uint8Array): Promise { + return new Promise((resolve, reject) => { + const request = new PackRequest(); + request.setVaultname(vaultName); + request.setBody(body); + this.client.requestPack(request, function (err, response) { + if (err) { + reject(err); + } else { + resolve(Buffer.from(response.getBody_asB64(), 'base64')); + } + }); + }); + } +} + +export default GitFrontend; diff --git a/src/lib/git/GitClient.ts b/src/lib/git/GitRequest.ts similarity index 52% rename from src/lib/git/GitClient.ts rename to src/lib/git/GitRequest.ts index 26887c0eaf..8c83a657ca 100644 --- a/src/lib/git/GitClient.ts +++ b/src/lib/git/GitRequest.ts @@ -1,24 +1,16 @@ -import * as grpc from '@grpc/grpc-js'; -import { Address } from '../peers/PeerInfo'; -import KeyManager from '../keys/KeyManager'; -import { GitServerClient } from '../../../proto/compiled/Git_grpc_pb'; -import { InfoRequest, PackRequest } from '../../../proto/compiled/Git_pb'; - /** * Responsible for converting HTTP messages from isomorphic-git into requests and sending them to a specific peer. */ -class GitClient { - private client: GitServerClient; - private credentials: grpc.ChannelCredentials; +class GitRequest { + private requestInfo: (vaultName: string) => Promise; + private requestPack: (vaultName: string, body: Buffer) => Promise; - constructor(address: Address, keyManager: KeyManager) { - const pkiInfo = keyManager.PKIInfo; - if (pkiInfo.caCert && pkiInfo.cert && pkiInfo.key) { - this.credentials = grpc.credentials.createSsl(pkiInfo.caCert, pkiInfo.key, pkiInfo.cert); - } else { - this.credentials = grpc.credentials.createInsecure(); - } - this.client = new GitServerClient(address.toString(), this.credentials); + constructor( + requestInfo: (vaultName: string) => Promise, + requestPack: (vaultName: string, body: Buffer) => Promise, + ) { + this.requestInfo = requestInfo; + this.requestPack = requestPack; } /** @@ -75,43 +67,6 @@ class GitClient { } // ==== HELPER METHODS ==== // - /** - * Requests remote info from the connected peer for the named vault. - * @param vaultName Name of the desired vault - */ - private async requestInfo(vaultName: string): Promise { - return new Promise((resolve, reject) => { - const request = new InfoRequest(); - request.setVaultname(vaultName); - this.client.requestInfo(request, function (err, response) { - if (err) { - reject(err); - } else { - resolve(Buffer.from(response.getBody_asB64(), 'base64')); - } - }); - }); - } - - /** - * Requests a pack from the connected peer for the named vault. - * @param vaultName Name of the desired vault - */ - private async requestPack(vaultName: string, body: Uint8Array): Promise { - return new Promise((resolve, reject) => { - const request = new PackRequest(); - request.setVaultname(vaultName); - request.setBody(body); - this.client.requestPack(request, function (err, response) { - if (err) { - reject(err); - } else { - resolve(Buffer.from(response.getBody_asB64(), 'base64')); - } - }); - }); - } - /** * Converts a buffer into an iterator expected by isomorphic git. * @param data Data to be turned into an iterator @@ -133,4 +88,4 @@ class GitClient { } } -export default GitClient; +export default GitRequest; diff --git a/src/lib/keys/KeyManager.ts b/src/lib/keys/KeyManager.ts index 1bf559128d..5e00b62c90 100644 --- a/src/lib/keys/KeyManager.ts +++ b/src/lib/keys/KeyManager.ts @@ -40,7 +40,7 @@ class KeyManager { publicKeyPath: null, pkiKeyPath: null, pkiCertPath: null, - caCertPath: null + caCertPath: null, }; ///////// @@ -285,10 +285,10 @@ class KeyManager { const salt = crypto.randomBytes(32); const key = await promisify(crypto.pbkdf2)(passphrase, salt, 10000, 256 / 8, 'sha256'); if (storeKey) { - this.derivedKeys[name] = key - await this.writeMetadata() + this.derivedKeys[name] = key; + await this.writeMetadata(); } - return key + return key; } /** @@ -296,16 +296,16 @@ class KeyManager { * @param name Name of the key to be deleted */ async deleteKey(name: string): Promise { - const successful = delete this.derivedKeys[name] - await this.writeMetadata() - return successful + const successful = delete this.derivedKeys[name]; + await this.writeMetadata(); + return successful; } /** * List all keys in the current keymanager */ listKeys(): string[] { - return Object.keys(this.derivedKeys) + return Object.keys(this.derivedKeys); } /** @@ -382,7 +382,7 @@ class KeyManager { async getIdentityFromPrivateKey(privateKey: Buffer, passphrase: string): Promise { const identity = await promisify(kbpgp.KeyManager.import_from_armored_pgp)({ armored: privateKey }); if (identity.is_pgp_locked()) { - await promisify(identity.unlock_pgp)({ passphrase: passphrase }); + await promisify(identity.unlock_pgp.bind(identity))({ passphrase }); } return identity; } @@ -454,10 +454,9 @@ class KeyManager { /** * Verifies the given data with the provided key or the primary key if none is specified * @param data Buffer or file containing the data to be verified - * @param signature The PGP signature * @param publicKey Buffer containing the key to verify with. Defaults to primary public key if no key is given. */ - async verifyData(data: Buffer | string, signature: Buffer, publicKey?: Buffer): Promise { + async verifyData(data: Buffer | string, publicKey?: Buffer): Promise { const ring = new kbpgp.keyring.KeyRing(); let resolvedIdentity: Object; if (publicKey) { @@ -471,13 +470,12 @@ class KeyManager { if (this.useWebWorkers && this.workerPool) { const workerResponse = await this.workerPool.queue(async (workerCrypto) => { - return await workerCrypto.verifyData(data, signature, resolvedIdentity); + return await workerCrypto.verifyData(data, resolvedIdentity); }); return workerResponse; } else { const params = { - armored: signature, - data: data, + armored: data, keyfetch: ring, }; const literals = await promisify(kbpgp.unbox)(params); @@ -505,10 +503,9 @@ class KeyManager { /** * Verifies the given file with the provided key or the primary key if none is specified * @param filePath Path to file containing the data to be verified - * @param signaturePath The path to the file containing the PGP signature * @param publicKey Buffer containing the key to verify with. Defaults to primary public key if no key is given. */ - async verifyFile(filePath: string, signaturePath: string, publicKey?: string | Buffer): Promise { + async verifyFile(filePath: string, publicKey?: string | Buffer): Promise { // Get key if provided let keyBuffer: Buffer; if (publicKey) { @@ -523,8 +520,7 @@ class KeyManager { } // Read in file buffer and signature const fileBuffer = this.fileSystem.readFileSync(filePath); - const signatureBuffer = this.fileSystem.readFileSync(signaturePath); - const isVerified = await this.verifyData(fileBuffer, signatureBuffer, keyBuffer!); + const isVerified = await this.verifyData(fileBuffer, keyBuffer!); return isVerified; } @@ -715,7 +711,7 @@ class KeyManager { this.fileSystem.writeFileSync(this.metadataPath, metadata); // Store the keys if identity is loaded if (this.identityLoaded) { - const derivedKeys = JSON.stringify(this.derivedKeys) + const derivedKeys = JSON.stringify(this.derivedKeys); const encryptedMetadata = await this.encryptData(Buffer.from(derivedKeys)); await this.fileSystem.promises.writeFile(this.derivedKeysPath, encryptedMetadata); } @@ -725,12 +721,12 @@ class KeyManager { if (this.fileSystem.existsSync(this.metadataPath)) { const metadata = this.fileSystem.readFileSync(this.metadataPath).toString(); this.metadata = JSON.parse(metadata); - if (this.identityLoaded) { + if (this.identityLoaded && this.fileSystem.existsSync(this.derivedKeysPath)) { const encryptedMetadata = this.fileSystem.readFileSync(this.derivedKeysPath); const metadata = (await this.decryptData(encryptedMetadata)).toString(); const derivedKeys = JSON.parse(metadata); for (const key of Object.keys(derivedKeys)) { - this.derivedKeys[key] = Buffer.from(derivedKeys[key]) + this.derivedKeys[key] = Buffer.from(derivedKeys[key]); } } } diff --git a/src/lib/keys/KeyManagerWorker.ts b/src/lib/keys/KeyManagerWorker.ts index 7998bdbeb5..ac0aa7ceaa 100644 --- a/src/lib/keys/KeyManagerWorker.ts +++ b/src/lib/keys/KeyManagerWorker.ts @@ -22,13 +22,12 @@ const keyManagerWorker = { * @param signature The PGP signature * @param identity Identity with which to verify with. */ - async verifyData(data: Buffer | string, signature: Buffer, identity: any): Promise { + async verifyData(data: Buffer | string, identity: any): Promise { const ring = new kbpgp.keyring.KeyRing(); ring.add_key_manager(identity); const params = { - armored: signature, - data: data, + armored: data, keyfetch: ring, }; const literals = await promisify(kbpgp.unbox)(params); diff --git a/src/lib/peers/PeerManager.ts b/src/lib/peers/PeerManager.ts index da741ab354..3f43ee5961 100644 --- a/src/lib/peers/PeerManager.ts +++ b/src/lib/peers/PeerManager.ts @@ -2,7 +2,8 @@ import os from 'os'; import fs from 'fs'; import path from 'path'; import * as grpc from '@grpc/grpc-js'; -import GitClient from '../git/GitClient'; +import GitRequest from '../git/GitRequest'; +import GitFrontend from '../git/GitFrontend'; import GitBackend from '../git/GitBackend'; import KeyManager from '../keys/KeyManager'; import { peer } from '../../../proto/js/Peer'; @@ -57,7 +58,7 @@ class PeerManager { serverStarted: boolean = false; private gitBackend: GitBackend; private credentials: grpc.ServerCredentials; - private peerConnections: Map; + private peerConnections: Map; constructor( polykeyPath: string = `${os.homedir()}/.polykey`, @@ -102,7 +103,10 @@ class PeerManager { ///////////////// // GRPC Server // ///////////////// - this.gitBackend = new GitBackend(polykeyPath, vaultManager); + this.gitBackend = new GitBackend( + polykeyPath, + ((vaultName: string) => vaultManager.getVault(vaultName).EncryptedFS).bind(vaultManager), + ); this.server = new grpc.Server(); // Add service @@ -113,7 +117,6 @@ class PeerManager { // Create the server credentials. SSL only if ca cert exists const pkiInfo = this.keyManager.PKIInfo; - if (pkiInfo.caCert && pkiInfo.cert && pkiInfo.key) { this.credentials = grpc.ServerCredentials.createSsl( pkiInfo.caCert, @@ -128,13 +131,15 @@ class PeerManager { } else { this.credentials = grpc.ServerCredentials.createInsecure(); } - - this.server.bindAsync(`0.0.0.0:${process.env.PK_PORT ?? 0}`, this.credentials, (err, boundPort) => { + this.server.bindAsync(`0.0.0.0:${process.env.PK_PORT ?? 0}`, this.credentials, async (err, boundPort) => { if (err) { throw err; } else { const address = new Address('localhost', boundPort.toString()); this.server.start(); + while (!this.localPeerInfo) { + await new Promise((r, _) => setTimeout(() => r(), 1000)); + } this.localPeerInfo.connect(address); this.serverStarted = true; } @@ -143,9 +148,7 @@ class PeerManager { private async requestInfo(call, callback) { const infoRequest: InfoRequest = call.request; - const vaultName = infoRequest.getVaultname(); - const infoReply = new InfoReply(); infoReply.setVaultname(vaultName); infoReply.setBody(await this.gitBackend.handleInfoRequest(vaultName)); @@ -156,7 +159,6 @@ class PeerManager { const packRequest: PackRequest = call.request; const vaultName = packRequest.getVaultname(); const body = Buffer.from(packRequest.getBody_asB64(), 'base64'); - const reply = new PackReply(); reply.setVaultname(vaultName); reply.setBody(await this.gitBackend.handlePackRequest(vaultName, body)); @@ -261,16 +263,16 @@ class PeerManager { * Get a secure connection to the peer * @param peer Public key of an existing peer or address of new peer */ - connectToPeer(peer: string | Address): GitClient { + connectToPeer(peer: string | Address): GitFrontend { // Throw error if trying to connect to self if (peer == this.localPeerInfo.connectedAddr || peer == this.localPeerInfo.publicKey) { throw Error('Cannot connect to self'); } let address: Address; if (typeof peer == 'string') { - const existingSocket = this.peerConnections.get(peer); - if (existingSocket) { - return existingSocket; + const existingConnection = this.peerConnections.get(peer); + if (existingConnection) { + return existingConnection; } const peerAddress = this.getPeer(peer)?.connectedAddr; @@ -283,7 +285,7 @@ class PeerManager { address = peer; } - const conn = new GitClient(address, this.keyManager); + const conn = new GitFrontend(address, this.keyManager); if (typeof peer == 'string') { this.peerConnections.set(peer, conn); diff --git a/src/lib/vaults/Vault.ts b/src/lib/vaults/Vault.ts index 7fa9d38793..a0329dfb78 100644 --- a/src/lib/vaults/Vault.ts +++ b/src/lib/vaults/Vault.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import Path from 'path'; import git from 'isomorphic-git'; -import GitClient from '../git/GitClient'; +import GitRequest from '../git/GitRequest'; import { EncryptedFS } from 'encryptedfs'; import { Mutex } from 'async-mutex'; @@ -219,7 +219,7 @@ class Vault { * @param address Address of polykey node that owns vault to be pulled * @param getSocket Function to get an active connection to provided address */ - async pullVault(gitClient: GitClient) { + async pullVault(gitClient: GitRequest) { const release = await this.mutex.acquire(); try { // Strangely enough this is needed for pulls along with ref set to 'HEAD' diff --git a/src/lib/vaults/VaultManager.ts b/src/lib/vaults/VaultManager.ts index cf41b47abc..2c99cf0fad 100644 --- a/src/lib/vaults/VaultManager.ts +++ b/src/lib/vaults/VaultManager.ts @@ -3,7 +3,7 @@ import os from 'os'; import Path from 'path'; import git from 'isomorphic-git'; import Vault from '../vaults/Vault'; -import GitClient from '../git/GitClient'; +import GitRequest from '../git/GitRequest'; import { EncryptedFS } from 'encryptedfs'; import KeyManager from '../keys/KeyManager'; @@ -121,7 +121,7 @@ class VaultManager { * @param address Address of polykey node that owns vault to be cloned * @param getSocket Function to get an active connection to provided address */ - async cloneVault(vaultName: string, gitClient: GitClient): Promise { + async cloneVault(vaultName: string, gitRequest: GitRequest): Promise { // Confirm it doesn't exist locally already if (this.vaultExists(vaultName)) { throw Error('Vault name already exists locally, try pulling instead'); @@ -131,7 +131,7 @@ class VaultManager { // First check if it exists on remote const info = await git.getRemoteInfo({ - http: gitClient, + http: gitRequest, url: vaultUrl, }); @@ -151,7 +151,7 @@ class VaultManager { // Clone vault from address await git.clone({ fs: { promises: newEfs.promises }, - http: gitClient, + http: gitRequest, dir: Path.join(this.polykeyPath, vaultName), url: vaultUrl, ref: 'master', diff --git a/tests/cli/CLI.test.ts b/tests/cli/CLI.test.ts new file mode 100644 index 0000000000..3457b88d92 --- /dev/null +++ b/tests/cli/CLI.test.ts @@ -0,0 +1,303 @@ +import fs from 'fs' +import os from 'os' +import path from 'path' +import { spawnSync } from 'child_process'; +import { randomString } from '../../src/lib/utils'; + +const cliPath = require.resolve('../../bin/polykey') + +describe('Polykey CLI', () => { + let pkCliEnv: {} + let tempPkAgentDir: string + + const validateCli = async function validateCli(cliOptions: { + args: string[], + expectedOutput?: string[], + expectedError?: Error, + ignoreOutput?: boolean, + ignoreError?: boolean, + env?: {} + }) { + const args = cliOptions.args + const expectedOutput = cliOptions.expectedOutput ?? [] + const expectedError = cliOptions.expectedError ?? undefined + const ignoreOutput = cliOptions.ignoreOutput ?? false + const ignoreError = cliOptions.ignoreError ?? false + const env = cliOptions.env ?? {} + const { output, error, stdout } = spawnSync(cliPath, args, { env: { ...process.env, ...pkCliEnv, ...env } }) + + const receivedOutput = output + .filter((b) => (b ?? '').toString() != '') + .map((b) => b.toString()) + + if (!ignoreOutput) { + for (const [index, value] of expectedOutput.entries()) { + expect(receivedOutput[index]).toEqual(expect.stringContaining(value)) + } + if (expectedOutput.length == 0) { + expect(receivedOutput).toEqual([]) + } + } + if (!ignoreError) { + expect(error).toEqual(expectedError) + } + // wait a couple of milli-seconds + // this should not normally be needed but there is an issue with + // nodes not being immediately loaded after the agent starts/restarts + await new Promise((r, _) => setTimeout(() => r(), 100)) + } + + beforeAll(() => { + tempPkAgentDir = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + pkCliEnv = { + PK_LOG_PATH: path.join(tempPkAgentDir, 'log'), + PK_SOCKET_PATH: path.join(tempPkAgentDir, 'S.testing-socket') + } + }) + afterAll(() => { + fs.rmdirSync(tempPkAgentDir, { recursive: true }) + }) + describe('With Agent Stopped', () => { + beforeEach(async () => { + // stop agent + await validateCli({ args: ['agent', 'stop'], ignoreOutput: true }) + }) + + test('agent status returns correctly if agent is stopped', async () => { + await validateCli({ args: ['agent', 'status'], expectedOutput: ["agent status is: 'stopped'"] }) + }) + + test('can start agent', async () => { + await validateCli({ args: ['agent', 'start'], expectedOutput: ["agent has started"] }) + }) + + test('can start agent as a daemon', async () => { + await validateCli({ args: ['agent', 'start', '-d'], expectedOutput: ["agent has started"] }) + }) + }) + + describe('With Agent Started', () => { + beforeEach(async () => { + await validateCli({ args: ['agent', 'stop'], ignoreOutput: true }) + await validateCli({ args: ['agent', 'start'], ignoreOutput: true }) + }) + + test('agent status returns correctly if agent is started', async () => { + await validateCli({ args: ['agent', 'status'], expectedOutput: ["agent status is: 'online'"] }) + }) + + test('can forcibly stop the agent without errors', async () => { + await validateCli({ args: ['agent', 'stop', '-f'], ignoreOutput: true }) + }) + + test('can restart agent without errors', async () => { + await validateCli({ args: ['agent', 'restart'], expectedOutput: ["agent has restarted"] }) + }) + + test('can restart agent as daemon without errors', async () => { + await validateCli({ args: ['agent', 'restart', '-d'], expectedOutput: ["agent has restarted"] }) + }) + }) + + describe('With New Node Created', () => { + let nodePath: string + let env: {} + beforeAll(async () => { + nodePath = path.join(`${os.tmpdir()}`, `pktest${randomString()}`) + + await validateCli({ args: ['agent', 'stop'], ignoreOutput: true }) + await validateCli({ args: ['agent', 'start'], ignoreOutput: true }) + env = { KEYNODE_PATH: nodePath } + await validateCli({ + args: [ + 'agent', + 'create', + '-n', + 'John Smith', + '-e', + 'john@email.com', + '-p', + 'passphrase', + '-b', + '1024', + ], + ignoreOutput: true, + env + }) + }) + afterAll(() => { + fs.rmdirSync(nodePath, { recursive: true }) + }) + + test('new node shows up in agent list', async () => { + await validateCli({ args: ['agent', 'list'], expectedOutput: [nodePath] }) + }) + + test('cannot operate on a locked node', async () => { + await validateCli({ args: ['agent', 'restart'], expectedOutput: ["agent has restarted"] }) + await validateCli({ args: ['vaults', 'list'], expectedOutput: ['Error: node path exists in memory but is locked'], env }) + }) + + test('can load exisitng node after agent restart', async () => { + await validateCli({ args: ['agent', 'restart'], expectedOutput: ["agent has restarted"] }) + await validateCli({ args: ['agent', 'load', '-p', 'passphrase'], expectedOutput: [nodePath], env }) + }) + + describe('Vault Operations', () => { + let vaultName: string + beforeEach(async () => { + vaultName = `Vault-${randomString()}` + // create a new vault + await validateCli({ args: ['vaults', 'new', vaultName], expectedOutput: ["vault created at"], env }) + }) + + test('existing vault shows up in vault list', async () => { + await validateCli({ args: ['vaults', 'list'], expectedOutput: [vaultName], env }) + }) + + test('can delete vault', async () => { + await validateCli({ args: ['vaults', 'list'], expectedOutput: [vaultName], env }) + }) + + describe('Secret Operations', () => { + let secretName: string + let secretContent: string + beforeEach(async () => { + secretName = `Secret-${randomString()}` + secretContent = `some secret content with random string: ${randomString()}` + // write to temporary file + const secretPath = path.join(os.tmpdir(), secretName) + fs.writeFileSync(secretPath, Buffer.from(secretContent)) + // create a new secret + await validateCli({ + args: ['secrets', 'new', `${vaultName}:${secretName}`, '-f', secretPath], + expectedOutput: [`secret '${secretName}' was successfully added to vault '${vaultName}'`], + env + }) + // delete temporary file + fs.unlinkSync(secretPath) + }) + + test('existing secret shows up in secret list', async () => { + await validateCli({ args: ['secrets', 'list', vaultName], expectedOutput: [secretName], env }) + }) + + test('deleted secret does not show up in secret list', async () => { + await validateCli({ + args: ['secrets', 'delete', `${vaultName}:${secretName}`], + expectedOutput: [`secret '${secretName}' was successfully removed from vault '${vaultName}'`], + env + }) + await validateCli({ args: ['secrets', 'list', vaultName], expectedOutput: [], env }) + }) + + test('can get secret from vault', async () => { + await validateCli({ args: ['secrets', 'get', `${vaultName}:${secretName}`], expectedOutput: [secretContent], env }) + }) + + test('can update secret content', async () => { + const newSecretContent = `very new secret content with another random string: ${randomString()}` + // write to temporary file + const secretPath = path.join(os.tmpdir(), secretName) + fs.writeFileSync(secretPath, Buffer.from(newSecretContent)) + await validateCli({ + args: ['secrets', 'update', `${vaultName}:${secretName}`, '-f', secretPath], + expectedOutput: [`secret '${secretName}' was successfully updated in vault '${vaultName}'`], + env + }) + // delete temporary file + fs.unlinkSync(secretPath) + + // make sure get secret returns correct new content + await validateCli({ args: ['secrets', 'get', `${vaultName}:${secretName}`], expectedOutput: [newSecretContent], env }) + }) + + test('can enter env with secret from vault', async () => { + const envVarName = secretName.toUpperCase().replace('-', '_') + await validateCli({ + args: ['secrets', 'env', `${vaultName}:${secretName}=${envVarName}`, '--command', `echo $${envVarName}`], + expectedOutput: [secretContent], + env + }) + }) + }) + }) + + describe('Key Operations', () => { + let keyName: string + let keyPassphrase: string + + let primaryPublicKey: string + let primaryPrivateKey: string + beforeEach(async () => { + keyName = `Key-${randomString()}` + keyPassphrase = `passphrase-${randomString()}` + // create a new key + await validateCli({ args: ['keys', 'new', '-n', keyName, '-p', keyPassphrase], expectedOutput: [`'${keyName}' was added to the Key Manager`], env }) + + // read in public and private keys + primaryPublicKey = fs.readFileSync(path.join(nodePath, '.keys', 'public_key')).toString() + primaryPrivateKey = fs.readFileSync(path.join(nodePath, '.keys', 'private_key')).toString() + }) + + test('existing key shows up in key list', async () => { + await validateCli({ args: ['keys', 'list'], expectedOutput: [keyName], env }) + }) + + test('can get existing key', async () => { + await validateCli({ args: ['keys', 'get', '-n', keyName], ignoreOutput: true, env }) + }) + + test('can delete existing key', async () => { + await validateCli({ args: ['keys', 'delete', '-n', keyName], expectedOutput: [`key '${keyName}' was successfully deleted`], env }) + }) + + test('can retreive primary keypair', async () => { + await validateCli({ + args: ['keys', 'primary', '-p', '-j'], + expectedOutput: [ + JSON.stringify({ publicKey: primaryPublicKey, privateKey: primaryPrivateKey }) + ], + env + }) + }) + }) + + describe('Crypto Operations', () => { + let tempDir: string + let filePath: string + let fileContent: Buffer + beforeEach(async () => { + tempDir = path.join(os.tmpdir(), `pktest${randomString()}`) + fs.mkdirSync(tempDir) + + filePath = path.join(tempDir, `random-file-${randomString()}`) + fileContent = Buffer.from(`file content: ${randomString()}`) + fs.writeFileSync(filePath, fileContent) + }) + + test('can encrypt and decrypt file', async () => { + await validateCli({ args: ['crypto', 'encrypt', '-f', filePath], expectedOutput: [`file successfully encrypted: '${filePath}'`], env }) + const encryptedData = fs.readFileSync(filePath) + expect(encryptedData).not.toEqual(undefined) + + await validateCli({ args: ['crypto', 'decrypt', '-f', filePath], expectedOutput: [`file successfully decrypted: '${filePath}'`], env }) + const decryptedData = fs.readFileSync(filePath) + expect(decryptedData).toEqual(fileContent) + }) + + test('can sign and verify file', async () => { + const signedPath = `${filePath}.sig` + await validateCli({ args: ['crypto', 'sign', filePath], expectedOutput: [`file '${filePath}' successfully signed at '${signedPath}'`], env }) + const signedData = fs.readFileSync(signedPath) + expect(signedData).not.toEqual(undefined) + + await validateCli({ args: ['crypto', 'verify', '-f', signedPath], expectedOutput: [`file '${signedPath}' was successfully verified`], env }) + const verifiedData = fs.readFileSync(filePath) + expect(verifiedData).toEqual(fileContent) + }) + + + }) + }) +}) diff --git a/tests/lib/agent/Agent.test.ts b/tests/lib/agent/Agent.test.ts index 086af6d0d7..247b71112d 100644 --- a/tests/lib/agent/Agent.test.ts +++ b/tests/lib/agent/Agent.test.ts @@ -2,42 +2,196 @@ import os from 'os'; import fs from 'fs'; import path from 'path'; import { randomString } from '../../../src/lib/utils'; -import { PolykeyAgent, PolykeyClient } from '../../../src/lib/Polykey'; +import Polykey, { PolykeyAgent, PolykeyClient } from '../../../src/lib/Polykey'; // TODO add tests as part of testing PR describe('Agent class', () => { + let pkCliEnv: {} + let tempPkAgentDir: string + let agent: PolykeyAgent let client: PolykeyClient let tempDir: string - beforeEach(async () => { - // // Start the agent running - // fs.mkdirSync(`/run/user/${process.getuid()}/polykey`) - // // This has issues in the gitlab ci/cd pipeline since there is - // // no /run/user//polykey directory - // agent = new PolykeyAgent() - // PolykeyAgent.startAgent() - // client = PolykeyAgent.connectToAgent() - // tempDir = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + beforeAll(async () => { + tempPkAgentDir = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + pkCliEnv = { + PK_LOG_PATH: path.join(tempPkAgentDir, 'log'), + PK_SOCKET_PATH: path.join(tempPkAgentDir, 'S.testing-socket') + } + // Start the agent running + agent = new PolykeyAgent() + await PolykeyAgent.startAgent() + + // connect to agent + client = PolykeyAgent.connectToAgent() + tempDir = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) }) - afterEach(() => { - // agent.stop() - // fs.rmdirSync(tempDir, { recursive: true }) + afterAll(() => { + agent.stop() + fs.rmdirSync(tempPkAgentDir, { recursive: true }) + fs.rmdirSync(tempDir, { recursive: true }) }) - test('can add new node', async () => { - // const nodePath = path.join(tempDir, 'SomePolykey') - // const response = await client.newNode(nodePath, 'Robert Cronin', 'robert.cronin@email.com', 'very password', 1024) - // expect(response).toEqual(true) + test('can get agent status', async () => { + expect(await client.getAgentStatus()).toEqual('online') }) - test('can list nodes', async () => { - // const nodePath = path.join(tempDir, 'SomePolykey') - // const newNodeResponse = await client.newNode(nodePath, 'Robert Cronin', 'robert.cronin@email.com', 'very password', 1024) - // expect(newNodeResponse).toEqual(true) - // // Listed nodes should contain our newly created node - // const listNodesResponse = await client.listNodes() - // expect(listNodesResponse).toEqual([nodePath]) + describe('Node Specific Operations', () => { + let nodePath: string + + beforeAll(async () => { + nodePath = path.join(tempDir, `PolykeyNode-${randomString()}`) + const successful = await client.newNode(nodePath, 'John Smith', 'john@email.com', 'very password', 1024) + expect(successful).toEqual(true) + }) + + test('added nodes turn up in node list', async () => { + expect(await client.listNodes()).toContainEqual(nodePath) + }) + + test('can register node', async () => { + const nonAgentNodePath = path.join(tempDir, `SomePolykey-${randomString()}`) + const pk = new Polykey(nonAgentNodePath, fs) + await pk.keyManager.generateKeyPair('John Smith', 'john@email.com', 'very password', 1024, true) + + expect(await client.registerNode(nonAgentNodePath, 'very password')).toEqual(true) + expect(await client.listNodes()).toContainEqual(nonAgentNodePath) + }) + + describe('Crypto Specific Operations', () => { + let filePath: string + let fileContent: Buffer + beforeEach(async () => { + filePath = path.join(tempDir, `random-file-${randomString()}`) + fileContent = Buffer.from(`file content: ${randomString()}`) + fs.writeFileSync(filePath, fileContent) + }) + + test('can encrypt and decrypt file', async () => { + const encryptedFilePath = await client.encryptFile(nodePath, filePath) + const encryptedData = fs.readFileSync(encryptedFilePath) + expect(encryptedData).not.toEqual(undefined) + + const decryptedFilePath = await client.decryptFile(nodePath, encryptedFilePath) + const decryptedData = fs.readFileSync(decryptedFilePath) + expect(decryptedData).toEqual(fileContent) + }) + + test('can sign and verify file', async () => { + const signedFilePath = await client.signFile(nodePath, filePath) + const signedData = fs.readFileSync(signedFilePath) + expect(signedData).not.toEqual(undefined) + + const verified = await client.verifyFile(nodePath, signedFilePath) + expect(verified).toEqual(true) + }) + }) + + describe('Key Specific Operations', () => { + let keyName: string + + beforeEach(async () => { + keyName = `random-key-${randomString()}` + const successful = await client.deriveKey(nodePath, keyName, 'passphrase') + expect(successful).toEqual(true) + }) + + test('can retreive keypair', async () => { + const keypair = await client.getPrimaryKeyPair(nodePath, true) + expect(keypair).not.toEqual(undefined) + }) + + test('derived key shows up in key list', async () => { + const keyList = await client.listKeys(nodePath) + expect(keyList).toContainEqual(keyName) + }) + + test('can retreived derived key', async () => { + const keyContent = await client.getKey(nodePath, keyName) + expect(keyContent).not.toEqual(undefined) + }) + + test('can delete derived key', async () => { + const successful = await client.deleteKey(nodePath, keyName) + expect(successful).toEqual(true) + + const keyList = await client.listKeys(nodePath) + expect(keyList).not.toContainEqual(keyName) + + expect(client.getKey(nodePath, keyName)).rejects.toThrow() + }) + }) + + describe('Vault Specific Operations', () => { + let vaultName: string + beforeEach(async () => { + vaultName = `Vault-${randomString()}` + await client.newVault(nodePath, vaultName) + }) + + test('created vault turns up in vault list', async () => { + expect(await client.listVaults(nodePath)).toContainEqual(vaultName) + }) + + test('can destroy vault', async () => { + const successful = await client.destroyVault(nodePath, vaultName) + expect(successful).toEqual(true) + expect(await client.listVaults(nodePath)).not.toContainEqual(vaultName) + }) + + describe('Secret Specific Operations', () => { + let secretName: string + let secretContent: Buffer + + beforeEach(async () => { + secretName = `Secret-${randomString()}` + secretContent = Buffer.from(`some random secret: ${randomString()}`) + const successful = await client.createSecret(nodePath, vaultName, secretName, secretContent) + expect(successful).toEqual(true) + }) + + test('can list secrets', async () => { + const secretList = await client.listSecrets(nodePath, vaultName) + expect(secretList).toContainEqual(secretName) + }) + + test('can retreive secret', async () => { + const retreivedSecretContent = await client.getSecret(nodePath, vaultName, secretName) + expect(retreivedSecretContent).toEqual(secretContent) + }) + + test('can remove secret', async () => { + const successful = await client.destroySecret(nodePath, vaultName, secretName) + expect(successful).toEqual(true) + }) + + test('retreiving a removed secret throws an error', async () => { + const successful = await client.destroySecret(nodePath, vaultName, secretName) + expect(successful).toEqual(true) + + const retreivedSecretContentPromise = client.getSecret(nodePath, vaultName, secretName) + expect(retreivedSecretContentPromise).rejects.toThrow() + }) + + test('removed secret is not listed in secret list', async () => { + const successful = await client.destroySecret(nodePath, vaultName, secretName) + expect(successful).toEqual(true) + + const secretList = await client.listSecrets(nodePath, vaultName) + expect(secretList).not.toContainEqual(secretName) + }) + + test('can update secret content', async () => { + const newContent = Buffer.from(`new secret content: ${randomString()}`) + const successful = await client.updateSecret(nodePath, vaultName, secretName, newContent) + expect(successful).toEqual(true) + + const updatedSecretContent = await client.getSecret(nodePath, vaultName, secretName) + expect(updatedSecretContent).toEqual(newContent) + }) + }) + }) }) }) diff --git a/tests/lib/git/Git.test.ts b/tests/lib/git/Git.test.ts new file mode 100644 index 0000000000..20657ba5f8 --- /dev/null +++ b/tests/lib/git/Git.test.ts @@ -0,0 +1,194 @@ +import os from 'os'; +import fs from 'fs'; +import path from 'path'; +import git from 'isomorphic-git' +import { randomString } from '../../../src/lib/utils'; +import GitBackend from '../../../src/lib/git/GitBackend'; +import GitRequest from '../../../src/lib/git/GitRequest'; + +describe('GitBackend and GitRequest classes', () => { + let sourceDir: string + let targetDir: string + + let repoName: string + let sourceRepoPath: string + let targetRepoPath: string + + let fileName: string + let fileContents: Buffer + + let gitBackend: GitBackend + let gitRequest: GitRequest + + beforeEach(async () => { + + sourceDir = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + targetDir = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + repoName = `repo-${randomString()}` + sourceRepoPath = path.join(sourceDir, repoName) + targetRepoPath = path.join(targetDir, repoName) + fs.mkdirSync(sourceRepoPath, { recursive: true }) + // Initialize a git repo in sourceDir + await git.init({ + fs, + dir: sourceRepoPath, + }); + + // initial commit + await git.commit({ + fs, + dir: sourceRepoPath, + author: { + name: repoName, + }, + message: 'init commit' + }); + // Write packed-refs file because isomorphic git goes searching for it + // and apparently its not autogenerated + fs.writeFileSync(path.join(sourceRepoPath, '.git', 'packed-refs'), '# pack-refs with: peeled fully-peeled sorted'); + + // create first file + fileName = `some-file-${randomString()}` + fileContents = Buffer.from(`some-file-contents-${randomString()}`) + fs.writeFileSync(path.join(sourceRepoPath, fileName), fileContents) + // commit that file + await git.add({ + fs, + dir: sourceRepoPath, + filepath: fileName + }) + await git.commit({ + fs, + dir: sourceRepoPath, + author: { + name: repoName, + }, + message: 'added first file' + }); + + // set up git backend + gitBackend = new GitBackend(sourceDir, (_) => fs) + // create git request object + gitRequest = new GitRequest( + gitBackend.handleInfoRequest.bind(gitBackend), + gitBackend.handlePackRequest.bind(gitBackend), + ) + }) + + afterEach(() => { + fs.rmdirSync(sourceDir, { recursive: true }) + fs.rmdirSync(targetDir, { recursive: true }) + }) + + test('can clone a repository', async () => { + await git.clone({ + fs: { promises: fs.promises }, + http: gitRequest, + dir: targetRepoPath, + url: `http://0.0.0.0/${repoName}`, + }); + + // check file has been cloned too + expect(fs.readFileSync(path.join(sourceRepoPath, fileName))).toEqual(fileContents) + }) + + describe('Push Operations', () => { + // todo add in testing when push functionaltiy is implemented + }) + + describe('Pull Operations', () => { + beforeEach(async () => { + // clone to the targetDir + await git.clone({ + fs: { promises: fs.promises }, + http: gitRequest, + dir: targetRepoPath, + url: `http://0.0.0.0/${repoName}` + }); + + // set git config + await git.setConfig({ + fs, + dir: targetRepoPath, + path: 'user.name', + value: repoName + }) + }) + + test('file changes are reflected in pulled changes', async () => { + const newFileContents = Buffer.from(`some new random change: ${randomString()}`) + // change file in source repo + fs.writeFileSync(path.join(sourceRepoPath, fileName), newFileContents) + + // add and commit + await git.add({ + fs, + dir: sourceRepoPath, + filepath: fileName + }) + await git.commit({ + fs, + dir: sourceRepoPath, + author: { + name: repoName, + }, + message: 'modified first file' + }); + + // pull in target repo + await git.pull({ + fs, + http: gitRequest, + dir: targetRepoPath + }) + + // expect that the file contents have changed in target repo + expect(fs.readFileSync(path.join(targetRepoPath, fileName))).toEqual(newFileContents) + }) + }) + + describe('Branch Operations', () => { + let branchName: string + beforeEach(async () => { + // clone to the targetDir + await git.clone({ + fs, + http: gitRequest, + dir: targetRepoPath, + url: `http://0.0.0.0/${repoName}` + }); + + // set git config + await git.setConfig({ + fs, + dir: targetRepoPath, + path: 'user.name', + value: repoName + }) + + // add a new branch to the sourceDir + branchName = `random-branch-name-${randomString()}` + await git.branch({ fs, dir: sourceRepoPath, ref: branchName }) + expect(await git.listBranches({ fs, dir: sourceRepoPath })).toContainEqual(branchName) + }) + + test('can pull and checkout new remote branch', async () => { + // pull from sourceDir to get new branch + await git.pull({ + fs, + http: gitRequest, + dir: targetRepoPath, + }) + + // switch to new branch + await git.checkout({ + fs, + dir: targetRepoPath, + ref: branchName + }) + + // branch is listed + expect(await git.listBranches({ fs, dir: targetRepoPath })).toContainEqual(branchName) + }) + }) +}) diff --git a/tests/lib/keys/KeyManager.test.ts b/tests/lib/keys/KeyManager.test.ts new file mode 100644 index 0000000000..b2b0dbd0ab --- /dev/null +++ b/tests/lib/keys/KeyManager.test.ts @@ -0,0 +1,84 @@ +import fs from 'fs'; +import os from 'os'; +import { randomString } from '../../../src/lib/utils'; +import KeyManager from '../../../src/lib/keys/KeyManager'; + +describe('KeyManager class', () => { + + let tempDir: string + let km: KeyManager + + beforeAll(async done => { + // Define temp directory + tempDir = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + + // Create keyManager + km = new KeyManager(tempDir, fs) + await km.generateKeyPair('John Smith', 'john.smith@email.com', 'passphrase', 1024, true) + + done() + }) + + afterAll(() => { + fs.rmdirSync(tempDir, { recursive: true }) + }) + + test('can create keypairs', async done => { + // Create private keys (async) + expect(km.generateKeyPair('John Smith', 'john.smith@gmail.com', 'passphrase', 1024)).resolves.not.toThrow() + + done() + }) + + test('can create symmetric keys', async done => { + const generatedKey = await km.generateKey('new-key', 'passphrase', true) + + const retreivedKey = km.getKey('new-key') + + expect(retreivedKey).toEqual(generatedKey) + + done() + }) + + test('can load an identity from a public key', async done => { + + const keypair = await km.generateKeyPair('John Smith', 'john@email.com', 'passphrase', 1024) + + const identity = await km.getIdentityFromPublicKey(Buffer.from(keypair.public!)) + + expect(identity).not.toEqual(undefined) + + done() + }) + + test('can load an identity from a private key', async done => { + + const keypair = await km.generateKeyPair('John Smith', 'john@email.com', 'passphrase', 1024) + + const identity = await km.getIdentityFromPrivateKey(Buffer.from(keypair.private!), 'passphrase') + + expect(identity).not.toEqual(undefined) + + done() + }) + + test('can sign and verify data', async done => { + const originalData = Buffer.from('I am to be signed') + const signedData = await km.signData(originalData) + const isVerified = await km.verifyData(signedData) + expect(isVerified).toEqual(true) + done() + }) + + test('can sign and verify files', async done => { + const originalData = Buffer.from('I am to be signed') + const filePath = `${tempDir}/file` + fs.writeFileSync(filePath, originalData) + // Sign file + const signedFilePath = await km.signFile(filePath) + // Verify file + const isVerified = await km.verifyFile(signedFilePath) + expect(isVerified).toEqual(true) + done() + }) +}) diff --git a/tests/lib/peers/PKI.test.ts b/tests/lib/peers/PKI.test.ts new file mode 100644 index 0000000000..de362a734b --- /dev/null +++ b/tests/lib/peers/PKI.test.ts @@ -0,0 +1,47 @@ +import fs from 'fs' +import os from 'os' +import Polykey from "../../../src/lib/Polykey" +import { randomString } from '../../../src/lib/utils' +import KeyManager from '../../../src/lib/keys/KeyManager' + +// TODO: part of adding PKI functionality to polykey +describe('PKI', () => { + let tempDirPeerA: string + let peerA: Polykey + + let tempDirPeerB: string + let peerB: Polykey + + beforeAll(async () => { + // ======== PEER A ======== // + // Define temp directory + tempDirPeerA = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + + // Create keyManager + const keyManagerA = new KeyManager(tempDirPeerA, fs) + await keyManagerA.generateKeyPair('John Smith', 'john.smith@email.com', 'some passphrase', 1024, true) + + // Initialize polykey + peerA = new Polykey( + tempDirPeerA, + fs, + keyManagerA + ) + while (!peerA.peerManager.serverStarted) { + await new Promise((resolve, reject) => { + setTimeout(() => resolve(), 500) + }) + } + }) + + afterAll(() => { + fs.rmdirSync(tempDirPeerA, { recursive: true }) + fs.rmdirSync(tempDirPeerB, { recursive: true }) + }) + + describe('Peer Connections', () => { + test('can connect securely to another peer and send data back and forth', async done => { + done() + }) + }) +}) diff --git a/tests/lib/peers/PeerManager.test.ts b/tests/lib/peers/PeerManager.test.ts new file mode 100644 index 0000000000..2a2a1aa18d --- /dev/null +++ b/tests/lib/peers/PeerManager.test.ts @@ -0,0 +1,90 @@ +import fs from 'fs' +import os from 'os' +import Polykey from "../../../src/lib/Polykey" +import { randomString } from '../../../src/lib/utils' +import KeyManager from '../../../src/lib/keys/KeyManager' + +describe('PeerManager class', () => { + let tempDirPeerA: string + let peerA: Polykey + + let tempDirPeerB: string + let peerB: Polykey + + beforeAll(async () => { + // ======== PEER A ======== // + // Define temp directory + tempDirPeerA = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + + // Create keyManager + const keyManagerA = new KeyManager(tempDirPeerA, fs) + await keyManagerA.generateKeyPair('John Smith', 'john.smith@email.com', 'some passphrase', 1024, true) + + // Initialize polykey + peerA = new Polykey( + tempDirPeerA, + fs, + keyManagerA + ) + while (!peerA.peerManager.serverStarted) { + await new Promise((resolve, reject) => { + setTimeout(() => resolve(), 500) + }) + } + + // ======== PEER A ======== // + // Define temp directory + tempDirPeerB = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + + // Create keyManager + const keyManagerB = new KeyManager(tempDirPeerB, fs) + await keyManagerB.generateKeyPair('Jane Doe', 'jane.doe@email.com', 'some different passphrase', 1024, true) + + // Initialize polykey + peerB = new Polykey( + tempDirPeerB, + fs, + keyManagerB + ) + while (!peerB.peerManager.serverStarted) { + await new Promise((resolve, reject) => { + setTimeout(() => resolve(), 500) + }) + } + + }) + + afterAll(() => { + fs.rmdirSync(tempDirPeerA, { recursive: true }) + fs.rmdirSync(tempDirPeerB, { recursive: true }) + }) + + describe('Peer Connections', () => { + test('can connect securely to another peer and send data back and forth', async done => { + peerA.peerManager.addPeer(peerB.peerManager.getLocalPeerInfo()) + peerB.peerManager.addPeer(peerA.peerManager.getLocalPeerInfo()) + // ==== A to B ==== // + const gitClient = peerA.peerManager.connectToPeer(peerB.peerManager.getLocalPeerInfo().publicKey) + expect(gitClient).not.toEqual(undefined) + + done() + }) + }) + + // describe('Peer Discovery', () => { + // test('find a peer via public key', async done => { + // // TODO: try to find a way to test this, currently its untestable because keybase login integration hasn't been completed + // const peerInfo = await peerA.peerManager.findPubKey(peerB.peerManager.getLocalPeerInfo().publicKey) + // console.log(peerInfo); + + // done() + // }) + + // test('find a user on github', async () => { + // // TODO: try to find a way to test this, currently its untestable because keybase login integration hasn't been completed + // await peerA.peerManager.findSocialUser('robert-cronin', 'github') + // }) + // }) + + +}) diff --git a/tests/lib/vaults/Vaults.test.ts b/tests/lib/vaults/Vaults.test.ts new file mode 100644 index 0000000000..acbf1eaa6b --- /dev/null +++ b/tests/lib/vaults/Vaults.test.ts @@ -0,0 +1,307 @@ +import fs, { write } from 'fs'; +import os from 'os'; +import path from 'path'; +import Polykey from "../../../src/lib/Polykey"; +import { randomString } from '../../../src/lib/utils'; +import KeyManager from '../../../src/lib/keys/KeyManager'; +import VaultManager from '../../../src/lib/vaults/VaultManager'; +import crypto from 'crypto'; +import GitRequest from '../../../src/lib/git/GitRequest'; + +describe('VaultManager class', () => { + let randomVaultName: string + + let tempDir: string + let pk: Polykey + let vm: VaultManager + + beforeAll(async done => { + // Define temp directory + tempDir = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + + // Create keyManager + const km = new KeyManager(tempDir, fs) + + // Generate keypair + await km.generateKeyPair('John Smith', 'john.smith@email.com', 'passphrase', 1024, true) + + // Load pki info + const cwd = process.cwd() + const peer1Path = path.join(cwd, 'tmp', 'secrets', 'peer1') + const caPath = path.join(cwd, 'tmp', 'secrets', 'CA') + km.loadPKIInfo( + fs.readFileSync(path.join(peer1Path, 'server.key')), + fs.readFileSync(path.join(peer1Path, 'server.crt')), + fs.readFileSync(path.join(caPath, 'root_ca.crt')), + true + ) + + // Initialize polykey + pk = new Polykey( + tempDir, + fs, + km + ) + vm = pk.vaultManager + done() + }) + + afterAll(() => { + fs.rmdirSync(tempDir, { recursive: true }) + }) + + beforeEach(() => { + // Reset the vault name for each test + randomVaultName = `Vault-${randomString()}` + }) + + test('can create vault', async () => { + // Create vault + await vm.createVault(randomVaultName) + const vaultExists = vm.vaultExists(randomVaultName) + expect(vaultExists).toEqual(true) + }) + + test('cannot create same vault twice', async () => { + // Create vault + await vm.createVault(randomVaultName) + const vaultExists = vm.vaultExists(randomVaultName) + expect(vaultExists).toEqual(true) + // Create vault a second time + expect(vm.createVault(randomVaultName)).rejects.toThrow('Vault already exists!') + }) + test('can destroy vaults', async () => { + // Create vault + await vm.createVault(randomVaultName) + expect(vm.vaultExists(randomVaultName)).toStrictEqual(true) + // Destroy the vault + vm.destroyVault(randomVaultName) + expect(vm.vaultExists(randomVaultName)).toStrictEqual(false) + }) + + /////////////////// + // Vault Secrets // + /////////////////// + describe('secrets within vaults', () => { + test('can create secrets and read them back', async () => { + // Create vault + const vault = await vm.createVault(randomVaultName) + + // Run test + const initialSecretName = 'ASecret' + const initialSecret = 'super confidential information' + // Add secret + await vault.addSecret(initialSecretName, Buffer.from(initialSecret)) + + // Read secret + const readBuffer = vault.getSecret(initialSecretName) + const readSecret = readBuffer.toString() + + expect(readSecret).toStrictEqual(initialSecret) + }) + }) + + //////////////////// + // Sharing Vaults // + //////////////////// + describe('sharing vaults', () => { + let tempDir2: string + let peerPk: Polykey + let peerVm: VaultManager + + beforeAll(async done => { + // Define temp directory + tempDir2 = fs.mkdtempSync(`${os.tmpdir}/pktest${randomString()}`) + // Create keyManager + const km2 = new KeyManager(tempDir2, fs) + + // Generate keypair + await km2.generateKeyPair('Jane Doe', 'jane.doe@email.com', 'passphrase', 1024, true) + + // Load pki info + const cwd = process.cwd() + const peer2Path = path.join(cwd, 'tmp', 'secrets', 'peer2') + const caPath = path.join(cwd, 'tmp', 'secrets', 'CA') + km2.loadPKIInfo( + fs.readFileSync(path.join(peer2Path, 'server.key')), + fs.readFileSync(path.join(peer2Path, 'server.crt')), + fs.readFileSync(path.join(caPath, 'root_ca.crt')), + true + ) + + // Initialize polykey + peerPk = new Polykey( + tempDir2, + fs, + km2 + ) + peerVm = peerPk.vaultManager + done() + }) + + afterAll(() => { + // Remove temp directory + fs.rmdirSync(tempDir2, { recursive: true }) + }) + + test('can clone vault', async done => { + // Create vault + const vault = await vm.createVault(randomVaultName) + // Add secret + const initialSecretName = 'ASecret' + const initialSecret = 'super confidential information' + await vault.addSecret(initialSecretName, Buffer.from(initialSecret)) + + // Pull from pk in peerPk + + const gitFrontend = peerPk.peerManager.connectToPeer(pk.peerManager.getLocalPeerInfo().connectedAddr!) + const gitRequest = new GitRequest( + gitFrontend.requestInfo.bind(gitFrontend), + gitFrontend.requestPack.bind(gitFrontend) + ) + const clonedVault = await peerVm.cloneVault(randomVaultName, gitRequest) + + const pkSecret = vault.getSecret(initialSecretName).toString() + + await clonedVault.pullVault(gitRequest) + + const peerPkSecret = clonedVault.getSecret(initialSecretName).toString() + + expect(peerPkSecret).toStrictEqual(pkSecret) + expect(peerPkSecret).toStrictEqual(initialSecret) + + + done() + }) + + test('stress test - can clone many vaults concurrently', async done => { + + const vaultNameList = [...Array(10).keys()].map((_) => { + return `Vault-${randomString()}` + }) + + for (const vaultName of vaultNameList) { + const vault = await vm.createVault(vaultName) + // Add secret + const initialSecretName = 'ASecret' + const initialSecret = 'super confidential information' + await vault.addSecret(initialSecretName, Buffer.from(initialSecret)) + } + + const gitFrontend = peerPk.peerManager.connectToPeer(pk.peerManager.getLocalPeerInfo().connectedAddr!) + const gitRequest = new GitRequest( + gitFrontend.requestInfo.bind(gitFrontend), + gitFrontend.requestPack.bind(gitFrontend) + ) + + // clone all vaults asynchronously + const clonedVaults = await Promise.all(vaultNameList.map(async (v) => { + return peerVm.cloneVault(v, gitRequest) + })) + const clonedVaultNameList = clonedVaults.map((v) => { + return v.name + }) + + expect(clonedVaultNameList).toEqual(vaultNameList) + + done() + }, 20000) + + test('can pull changes', async done => { + // Create vault + const vault = await vm.createVault(randomVaultName) + // Add secret + const initialSecretName = 'InitialSecret' + const initialSecret = 'super confidential information' + await vault.addSecret(initialSecretName, Buffer.from(initialSecret)) + + // First clone from pk in peerPk + const gitFrontend = peerPk.peerManager.connectToPeer(pk.peerManager.getLocalPeerInfo().connectedAddr!) + const gitRequest = new GitRequest( + gitFrontend.requestInfo.bind(gitFrontend), + gitFrontend.requestPack.bind(gitFrontend) + ) + const clonedVault = await peerVm.cloneVault(randomVaultName, gitRequest) + + // Add secret to pk + await vault.addSecret('NewSecret', Buffer.from('some other secret information')) + + // Pull from vault + await clonedVault.pullVault(gitRequest) + + // Compare new secret + const pkNewSecret = vault.getSecret(initialSecretName).toString() + const peerPkNewSecret = clonedVault.getSecret(initialSecretName).toString() + expect(pkNewSecret).toStrictEqual(peerPkNewSecret) + done() + }) + + test('removing secret is reflected in peer vault', async done => { + // Create vault + const vault = await vm.createVault(randomVaultName) + // Add secret + const initialSecretName = 'InitialSecret' + const initialSecret = 'super confidential information' + await vault.addSecret(initialSecretName, Buffer.from(initialSecret)) + + // First clone from pk in peerPk + const gitFrontend = peerPk.peerManager.connectToPeer(pk.peerManager.getLocalPeerInfo().connectedAddr!) + const gitRequest = new GitRequest( + gitFrontend.requestInfo.bind(gitFrontend), + gitFrontend.requestPack.bind(gitFrontend) + ) + const clonedVault = await peerVm.cloneVault(randomVaultName, gitRequest) + + // Confirm secrets list only contains InitialSecret + const secretList = vault.listSecrets() + const clonedSecretList = clonedVault.listSecrets() + expect(secretList).toEqual(clonedSecretList) + expect(clonedSecretList).toEqual([initialSecretName]) + + // Remove secret from pk vault + await vault.removeSecret(initialSecretName) + + // Pull clonedVault + await clonedVault.pullVault(gitRequest) + + // Confirm secrets list is now empty + const removedSecretList = vault.listSecrets() + const removedClonedSecretList = clonedVault.listSecrets() + expect(removedSecretList).toEqual(removedClonedSecretList) + expect(removedClonedSecretList).toEqual([]) + + done() + }) + }) + + ///////////////// + // Concurrency // + ///////////////// + describe('concurrency', () => { + test('parallel write operations are sequentially executed', async done => { + const vault = await pk.vaultManager.createVault(randomVaultName) + const writeOps: Promise[] = [] + const expectedHistory: number[] = [] + for (const n of Array(50).keys()) { + // Get a random number of bytes so each operation might finish earlier than the others + const randomNumber = 1 + Math.round(Math.random() * 5000) + const secretBuffer = crypto.randomBytes(randomNumber) + const writeOp = vault.addSecret(`${n + 1}`, secretBuffer) + writeOps.push(writeOp) + expectedHistory.push(n + 1) + } + await Promise.all(writeOps) + + const history = (await vault.getVaultHistory()).reverse() + .map((commit) => { + const match = commit.match(/([0-9]+)/) + return (match) ? parseInt(match[0]) : undefined + }) + .filter((n) => n != undefined) + + expect(history).toEqual(expectedHistory) + + done() + }, 20000) + }) +})