Skip to content

Commit

Permalink
Merge pull request #43 from NitorCreations/node-cli
Browse files Browse the repository at this point in the history
Add Node.js CLI
  • Loading branch information
psiniemi authored Dec 1, 2019
2 parents 59818b0 + e7eba50 commit 2152f46
Show file tree
Hide file tree
Showing 8 changed files with 237 additions and 1,106 deletions.
28 changes: 28 additions & 0 deletions nodejs/cli/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"name": "@nitor/aws-vault-cli",
"version": "0.0.1",
"description": "Command-line interface for AWS vault",
"author": {
"name": "Nitor",
"email": "",
"url": "http://nitor.com"
},
"contributors": [
"Eetu Huisman <[email protected]>"
],
"main": "index.js",
"repository": {
"type": "git",
"url": "git://github.com/NitorCreations/vault"
},
"bugs": {
"url": "http://github.com/NitorCreations/vault/issues"
},
"license": "Apache-2.0",
"peerDependencies": {
"@nitor/aws-vault": "0.1.1"
},
"dependencies": {
"sade": "^1.4.2"
}
}
64 changes: 64 additions & 0 deletions nodejs/cli/vault.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env node
const sade = require('sade');
const loadOptions = require('../lib/loadOptions');
const client = require('../lib/vaultClient');

const DEFAULT_STACK_NAME = 'vault';

const handleRejection = err => {
console.error(err);
process.exit(1);
};

const prog = sade('vault');

prog.option('--vaultstack', 'Optional CloudFormation stack to lookup key and bucket.', DEFAULT_STACK_NAME);
prog.option('-p, --prefix', 'Optional prefix to store values under. Empty by default');
prog.option('-b, --bucket', 'Override the bucket name either for initialization or storing and looking up values');
prog.option('-k, --key-arn', 'Override the KMS key arn for storing or looking up values');
prog.option('--id', 'Give an IAM access key id to override those defined by the environment');
prog.option('--secret', 'Give an IAM secret access key to override those defined by the environment');
prog.option('-r, --region', 'Give a region for the stack and the bucket');

prog
.command('store <name> <value>')
.describe('Store data in the vault')
.option('-w, --overwrite', 'Overwrite the current value if it already exists', false)
.action((name, value, options) => {
loadOptions(options)
.then(options => client.store(name, value, options))
.catch(handleRejection);
})
.command('lookup <name>')
.describe('Look up data from the vault')
.action((name, options) => {
loadOptions(options)
.then(options => client.lookup(name, options))
.then(console.log)
.catch(handleRejection);
})
.command('delete <name>')
.describe('Delete data from the vault')
.action((name, options) => {
loadOptions(options)
.then(options => client.delete(name, options))
.catch(handleRejection)
})
.command('exists <name>')
.describe('Check if the vault contains data')
.action((name, options) => {
loadOptions(options)
.then(options => client.exists(name, options))
.then(console.log)
.catch(handleRejection)
})
.command('all')
.describe('List all keys the vault contains')
.action(options => {
loadOptions(options)
.then(options => client.all(options))
.then(console.log)
.catch(handleRejection)
});

prog.parse(process.argv);
15 changes: 15 additions & 0 deletions nodejs/cli/yarn.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1


mri@^1.1.0:
version "1.1.4"
resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a"
integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w==

sade@^1.4.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/sade/-/sade-1.6.1.tgz#aba16655e998b2b68beb9f13938af010f42eddd2"
integrity sha512-USHm9quYNmJwFwhOnEuJohdnBhUOKV1mhL0koHSJMLJaesRX0nuDuzbWmtUBbUmXkwTalLtUBzDlEnU940BiQA==
dependencies:
mri "^1.1.0"
17 changes: 17 additions & 0 deletions nodejs/lib/loadOptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const awscred = require('awscred');
const { promisify } = require('util');
const { CloudFormation } = require('aws-sdk');

const loadCredentialsAndRegion = promisify(awscred.loadCredentialsAndRegion);

module.exports = (options) => loadCredentialsAndRegion()
.then(({ region }) => new CloudFormation({ region }).describeStacks({ StackName: options.vaultstack }).promise()
.then((describeStackOutput) => Promise.resolve({ describeStackOutput, region })))
.then(({ describeStackOutput, region }) => {
const stack = describeStackOutput.Stacks[0];
return Promise.resolve({
vaultKey: options.k || stack.Outputs.find(output => output.OutputKey === 'kmsKeyArn').OutputValue,
bucketName: options.b || stack.Outputs.find(output => output.OutputKey === 'vaultBucketName').OutputValue,
region,
});
});
194 changes: 98 additions & 96 deletions nodejs/lib/vaultClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const ALGORITHMS = Object.freeze({
authCrypto: 'id-aes256-GCM',
kms: 'AES_256'
});
const STATIC_IV = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1337 / 256, 1337 % 256]);
const STATIC_IV = Buffer.from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1337 / 256, 1337 % 256]);
const ENCODING = 'UTF-8';

const createRequestObject = (bucketName, key) => Object.freeze({
Expand All @@ -16,67 +16,52 @@ const createRequestObject = (bucketName, key) => Object.freeze({

const createKeyRequestObject = (bucketName, name) => createRequestObject(bucketName, `${name}.key`);

const createEncryptedValueRequestObject = (bucketName, name) => createRequestObject(bucketName, `${name}.encrypted`);
const staticSuffix = `.encrypted`;
const createEncryptedValueRequestObject = (bucketName, name) => createRequestObject(bucketName, `${name}${staticSuffix}`);

const createAuthEncryptedValueRequestObject = (bucketName, name) => createRequestObject(bucketName, `${name}.aesgcm.encrypted`);
const aesgcmSuffix = `.aesgcm.encrypted`;
const createAuthEncryptedValueRequestObject = (bucketName, name) => createRequestObject(bucketName, `${name}${aesgcmSuffix}`);

const createMetaRequestObject = (bucketName, name) => createRequestObject(bucketName, `${name}.meta`);

const createDecipher = (meta, decryptedKey) => {
return meta === 'nometa' ?
const nometa = 'nometa';

const createDecipher = (meta, decryptedKey, authTag) => {
return meta === nometa ?
crypto.createDecipheriv(ALGORITHMS.crypto, decryptedKey, STATIC_IV) :
crypto.createDecipheriv(ALGORITHMS.authCrypto, decryptedKey, Buffer.from(JSON.parse(meta.Body).nonce, "base64")).setAAD(meta.Body);
crypto.createDecipheriv(ALGORITHMS.authCrypto, decryptedKey, Buffer.from(JSON.parse(meta.Body).nonce, "base64")).setAAD(meta.Body).setAuthTag(authTag);
};

const createVaultClient = (options) => {
const bucketName = options.bucketName;
const vaultKey = options.vaultKey;
const region = options.region || process.env.AWS_DEFAULT_REGION;

const s3 = new AWS.S3({
region: region
});
const kms = new AWS.KMS({
region: region
});

const writeObject = (base, value) => s3.putObject(Object.assign({
Body: value,
ACL: 'private'
}, base)).promise();

const ensureCredentials = () => {
if (AWS.config.credentials) {
return Promise.resolve();
}
return new AWS.CredentialProviderChain([
new AWS.SharedIniFileCredentials(),
new AWS.EnvironmentCredentials('AWS'),
new AWS.EC2MetadataCredentials({
httpOptions: { timeout: 5000 },
maxRetries: 10,
retryDelayOptions: { base: 200 }
})
]).resolvePromise();
};

return {
lookup: (name) => ensureCredentials()
.then(() =>
Promise.all([
s3.getObject(createKeyRequestObject(bucketName, name)).promise()
.then(encryptedKey => kms.decrypt({ CiphertextBlob: encryptedKey.Body }).promise()),
s3.getObject(createAuthEncryptedValueRequestObject(bucketName, name)).promise()
.catch(e => s3.getObject(createEncryptedValueRequestObject(bucketName, name)).promise()),
s3.getObject(createMetaRequestObject(bucketName, name)).promise().catch(e => "nometa")
])
)
const writeObject = (s3, base, value) => s3.putObject(Object.assign({
Body: value,
ACL: 'private'
}, base)).promise();

module.exports = {
lookup: (name, options) => {
const { region, bucketName } = options;

const s3 = new AWS.S3({
region,
});

const kms = new AWS.KMS({
region,
});

return Promise.all([
s3.getObject(createKeyRequestObject(bucketName, name)).promise()
.then(encryptedKey => kms.decrypt({ CiphertextBlob: encryptedKey.Body }).promise()),
s3.getObject(createAuthEncryptedValueRequestObject(bucketName, name)).promise()
.catch(() => s3.getObject(createEncryptedValueRequestObject(bucketName, name)).promise()),
s3.getObject(createMetaRequestObject(bucketName, name)).promise().catch(() => nometa)
])
.then(keyValueAndMeta => {
const decryptedKey = keyValueAndMeta[0].Plaintext;
const encryptedValue = keyValueAndMeta[1].Body.slice(0, -16);
const authTag = keyValueAndMeta[1].Body.slice(-16);
const meta = keyValueAndMeta[2];
const decipher = createDecipher(meta, decryptedKey).setAuthTag(authTag);
const decipher = createDecipher(meta, decryptedKey, authTag);
const value = decipher.update(encryptedValue, null, ENCODING);

try {
Expand All @@ -85,14 +70,21 @@ const createVaultClient = (options) => {
return Promise.reject(e);
}
return Promise.resolve(value);
}),

store: (name, data) => ensureCredentials()
.then(() =>
kms.generateDataKey({
KeyId: vaultKey,
KeySpec: ALGORITHMS.kms
}).promise())
})
},

store: (name, data, options) => {
const { region, vaultKey, bucketName } = options;
const kms = new AWS.KMS({
region,
});
const s3 = new AWS.S3({
region,
});
kms.generateDataKey({
KeyId: vaultKey,
KeySpec: ALGORITHMS.kms
}).promise()
.then((dataKey) => {
const nonce = crypto.randomBytes(12);
const aad = Buffer.from(JSON.stringify({
Expand All @@ -103,44 +95,54 @@ const createVaultClient = (options) => {
const authValue = cipher.update(data, ENCODING);
cipher.final(ENCODING);
return Promise.resolve({
key: dataKey.CiphertextBlob,
value: crypto.createCipheriv(ALGORITHMS.crypto, dataKey.Plaintext, STATIC_IV).update(data, ENCODING),
authValue: Buffer.concat([authValue, cipher.getAuthTag()]),
meta: aad
}
)})
key: dataKey.CiphertextBlob,
value: crypto.createCipheriv(ALGORITHMS.crypto, dataKey.Plaintext, STATIC_IV).update(data, ENCODING),
authValue: Buffer.concat([authValue, cipher.getAuthTag()]),
meta: aad
}
)
})
.then((keyAndValue) =>
Promise.all([
writeObject(createKeyRequestObject(bucketName, name), keyAndValue.key),
writeObject(createEncryptedValueRequestObject(bucketName, name), keyAndValue.value),
writeObject(createAuthEncryptedValueRequestObject(bucketName, name), keyAndValue.authValue),
writeObject(createMetaRequestObject(bucketName, name), keyAndValue.meta)
])),

delete: (name) => ensureCredentials()
.then(() =>
Promise.all([
s3.deleteObject(createEncryptedValueRequestObject(bucketName, name)).promise(),
s3.deleteObject(createKeyRequestObject(bucketName, name)).promise(),
s3.deleteObject(createAuthEncryptedValueRequestObject(bucketName, name)).promise().catch(e => e),
s3.deleteObject(createMetaRequestObject(bucketName, name)).promise().catch(e => e)
])),

exists: (name) => ensureCredentials()
.then(() =>
s3.headObject(createEncryptedValueRequestObject(bucketName, name)).promise())
.then(() => Promise.resolve(true), () => Promise.resolve(false)
),

all: () => ensureCredentials()
.then(() =>
s3.listObjectsV2({
Bucket: bucketName
}).promise())
.then((data) => Promise.resolve(data.Contents
.filter((object) => object.Key.endsWith('.encrypted'))
.map(object => object.Key.slice(0, -('.encrypted'.length)))))
};
writeObject(s3, createKeyRequestObject(bucketName, name), keyAndValue.key),
writeObject(s3, createEncryptedValueRequestObject(bucketName, name), keyAndValue.value),
writeObject(s3, createAuthEncryptedValueRequestObject(bucketName, name), keyAndValue.authValue),
writeObject(s3, createMetaRequestObject(bucketName, name), keyAndValue.meta)
]));
},

delete: (name, options) => {
const { region, bucketName } = options;
const s3 = new AWS.S3({
region,
});
return Promise.all([
s3.deleteObject(createEncryptedValueRequestObject(bucketName, name)).promise(),
s3.deleteObject(createKeyRequestObject(bucketName, name)).promise(),
s3.deleteObject(createAuthEncryptedValueRequestObject(bucketName, name)).promise().catch(e => e),
s3.deleteObject(createMetaRequestObject(bucketName, name)).promise().catch(e => e)
]);
},

exists: (name, options) => {
const { region, bucketName } = options;
const s3 = new AWS.S3({
region,
});
return s3.headObject(createEncryptedValueRequestObject(bucketName, name)).promise()
.then(() => Promise.resolve(true), () => Promise.resolve(false));
},

all: (options) => {
const { region, bucketName } = options;
const s3 = new AWS.S3({
region,
});
return s3.listObjectsV2({
Bucket: bucketName
}).promise()
.then(data => Promise.resolve([...new Set(data.Contents
.filter(object => object.Key.endsWith(aesgcmSuffix) || object.Key.endsWith(staticSuffix))
.map(object => object.Key.slice(0, -(object.Key.endsWith(aesgcmSuffix) ? aesgcmSuffix.length : staticSuffix.length))))]))
}
};

module.exports = createVaultClient;
12 changes: 3 additions & 9 deletions nodejs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nitor/aws-vault",
"version": "0.1.1",
"version": "1.0.0",
"author": {
"name": "Nitor Creations",
"email": "",
Expand All @@ -14,14 +14,8 @@
"test": "mocha"
},
"dependencies": {
"aws-sdk": "2.539.0"
},
"devDependencies": {
"aws-sdk-mock": "4.5.0",
"mocha": "6.2.1",
"should": "13.2.3",
"should-sinon": "0.0.6",
"sinon": "7.5.0"
"aws-sdk": "2.539.0",
"awscred": "1.5.0"
},
"engines": {
"node": ">= 0.8.0"
Expand Down
Loading

0 comments on commit 2152f46

Please sign in to comment.