├── .gitignore ├── lib ├── encoder.js ├── hmac.js ├── decrypter.js ├── keys.js └── secrets.js ├── circle.yml ├── test ├── encoder.js └── index.js ├── examples └── all.js ├── LICENSE ├── package.json ├── index.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /lib/encoder.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | encode: (s) => { 4 | return new Buffer(s).toString('base64'); 5 | }, 6 | decode: (s) => { 7 | return new Buffer(s, 'base64'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 4.0 4 | 5 | test: 6 | override: 7 | - npm run security 8 | - npm test 9 | - npm run coverage 10 | 11 | deployment: 12 | production: 13 | branch: master 14 | commands: 15 | - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/.npmrc 16 | - npm version "1.0.$CIRCLE_BUILD_NUM" --no-git-tag-version 17 | - npm publish 18 | -------------------------------------------------------------------------------- /test/encoder.js: -------------------------------------------------------------------------------- 1 | const encoder = require('../lib/encoder.js'); 2 | const should = require('chai').should(); 3 | 4 | describe('encoder', () => { 5 | it('should encode correctly', () => { 6 | encoder.encode('TEST').toString().should.equal('VEVTVA=='); 7 | }); 8 | it('should encode and decode', () => { 9 | encoder.decode(encoder.encode('TEST')).toString('utf8').should.equal('TEST'); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /examples/all.js: -------------------------------------------------------------------------------- 1 | const Credstash = require('../index.js'); 2 | var credstash = new Credstash(); 3 | 4 | return credstash.list((e, secrets) => { 5 | if (e) { 6 | console.error('error listing secrets', e); 7 | } 8 | return credstash.get('test', (e, secret) => { 9 | if (e) { 10 | console.error('error getting secret', e); 11 | } 12 | console.log('do not share the secret', secret); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /lib/hmac.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const encoder = require('./encoder'); 3 | 4 | module.exports = { 5 | check: (stashes, done) => { 6 | const wrongMacs = stashes.filter(stash => { 7 | const hmac = crypto.createHmac('sha256', stash.hmacPlaintext); 8 | const contents = encoder.decode(stash.contents); 9 | hmac.update(contents); 10 | return hmac.digest('hex') !== stash.hmac; 11 | }); 12 | 13 | if (wrongMacs.length > 0) { 14 | return done(new Error('Computed HMAC does not match store HMAC')); 15 | } 16 | 17 | return done(null, stashes); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/decrypter.js: -------------------------------------------------------------------------------- 1 | const aesjs = require('aes-js'); 2 | const encoder = require('./encoder'); 3 | 4 | function decryptedString(stash) { 5 | const key = stash.keyPlaintext; 6 | const value = encoder.decode(stash.contents); 7 | const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(1)); 8 | const decryptedBytes = aesCtr.decrypt(value); 9 | return decryptedBytes.toString(); 10 | } 11 | 12 | module.exports = { 13 | decryptedList: (stashes, done) => { 14 | const decrypted = stashes.map(stash => { 15 | return decryptedString(stash); 16 | }); 17 | return done(null, decrypted); 18 | }, 19 | decryptedObject: (stashes, done) => { 20 | const decrypted = stashes.reduce((acc, stash) => { 21 | acc[stash.name] = decryptedString(stash); 22 | return acc; 23 | }, {}); 24 | return done(null, decrypted); 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/keys.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const async = require('async'); 3 | const encoder = require('./encoder'); 4 | 5 | if (typeof process.env.AWS_DEFAULT_REGION !== 'undefined') { 6 | AWS.config.update({region: process.env.AWS_DEFAULT_REGION}); 7 | } 8 | 9 | function decrypt(key, done) { 10 | var params = { 11 | CiphertextBlob: encoder.decode(key) 12 | }; 13 | 14 | return new AWS.KMS().decrypt(params, done); 15 | } 16 | 17 | function split(stashes, decryptedKeys, done) { 18 | var result = stashes.map((stash, index) => { 19 | stash.keyPlaintext = new Buffer(32); 20 | stash.hmacPlaintext = new Buffer(32); 21 | decryptedKeys[index].Plaintext.copy(stash.keyPlaintext, 0, 0, 32); 22 | decryptedKeys[index].Plaintext.copy(stash.hmacPlaintext, 0, 32); 23 | return stash; 24 | }); 25 | return done(null, result); 26 | } 27 | 28 | module.exports = { 29 | decrypt: (stashes, done) => { 30 | return async.waterfall([ 31 | async.apply(async.map, stashes.map(s => s.key), decrypt), 32 | async.apply(split, stashes) 33 | ], done); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Roy Lines 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "credstash", 3 | "version": "1.0.1", 4 | "description": "Module for reading credstash secrets", 5 | "main": "index.js", 6 | "scripts": { 7 | "security": "nsp check", 8 | "test": "istanbul cover _mocha -dir $CIRCLE_ARTIFACTS", 9 | "coverage": "istanbul check-coverage --statement 80 $CIRCLE_ARTIFACTS/coverage.json", 10 | "coveralls": "cat $CIRCLE_ARTIFACTS/lcov.info | coveralls" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/roylines/node-credstash.git" 15 | }, 16 | "keywords": [ 17 | "credstash", 18 | "secrets" 19 | ], 20 | "author": "Roy Lines", 21 | "license": "ISC", 22 | "bugs": { 23 | "url": "https://github.com/roylines/node-credstash/issues" 24 | }, 25 | "homepage": "https://github.com/roylines/node-credstash#readme", 26 | "devDependencies": { 27 | "aws-sdk-mock": "1.0.5", 28 | "chai": "3.5.0", 29 | "coveralls": "2.11.6", 30 | "istanbul": "0.4.2", 31 | "mocha": "2.4.5", 32 | "nsp": "2.6.2" 33 | }, 34 | "dependencies": { 35 | "aes-js": "0.2.2", 36 | "async": "1.5.2", 37 | "aws-sdk": "2.2.35", 38 | "xtend": "4.0.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const async = require('async'); 2 | const decrypter = require('./lib/decrypter'); 3 | const encoder = require('./lib/encoder'); 4 | const hmac = require('./lib/hmac'); 5 | const keys = require('./lib/keys'); 6 | const secrets = require('./lib/secrets'); 7 | const xtend = require('xtend'); 8 | 9 | const defaults = { 10 | limit: 1 11 | }; 12 | 13 | function Credstash(config) { 14 | this.table = config ? config.table : undefined; 15 | } 16 | 17 | Credstash.prototype.list = function(options, done) { 18 | if (typeof options === 'function') { 19 | done = options; 20 | options = defaults; 21 | } else { 22 | options = xtend(defaults, options); 23 | } 24 | 25 | return async.waterfall([ 26 | async.apply(secrets.list, this.table, options), 27 | async.apply(keys.decrypt), 28 | async.apply(hmac.check), 29 | async.apply(decrypter.decryptedObject) 30 | ], function (err, secrets) { 31 | if (err) { 32 | return done(err); 33 | } 34 | 35 | done(null, secrets); 36 | }); 37 | }; 38 | 39 | Credstash.prototype.get = function(name, options, done) { 40 | if (typeof options === 'function') { 41 | done = options; 42 | options = defaults; 43 | } else { 44 | options = xtend(defaults, options); 45 | } 46 | 47 | return async.waterfall([ 48 | async.apply(secrets.get, this.table, name, options), 49 | async.apply(keys.decrypt), 50 | async.apply(hmac.check), 51 | async.apply(decrypter.decryptedList) 52 | ], function (err, secrets) { 53 | if (err) { 54 | return done(err); 55 | } 56 | 57 | if (options.limit === 1) { 58 | return done(null, secrets && secrets[0]); 59 | } 60 | 61 | done(null, secrets); 62 | }); 63 | }; 64 | 65 | module.exports = Credstash; 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-credstash 2 | [![Circle CI](https://circleci.com/gh/roylines/node-credstash.svg?style=svg)](https://circleci.com/gh/roylines/node-credstash) 3 | 4 | Node.js module for reading [credstash](https://github.com/fugue/credstash) secrets without needing snakes 5 | 6 | ```js 7 | const Credstash = require('credstash'); 8 | const credstash = new Credstash(); 9 | 10 | // .get method for one key (table query) 11 | credstash.get('secret', (e, secret) => { 12 | console.log('do not share the secret', secret); 13 | }); 14 | 15 | // .list method for multiple keys (table scan) 16 | credstash.list((e, secrets) => { 17 | console.log('do not share the secrets', secrets); 18 | }); 19 | ``` 20 | 21 | ## Installation 22 | Ensure you have [AWS credentials configured](http://docs.aws.amazon.com/AWSJavaScriptSDK/guide/node-configuring.html). 23 | The credentials should be set up as a [secret reader](https://github.com/fugue/credstash#secret-reader) 24 | 25 | ```bash 26 | $ npm install credstash 27 | ``` 28 | 29 | ## What is credstash? 30 | [Credstash](https://github.com/fugue/credstash) is a little utility for managing credentials in the cloud 31 | 32 | ## Who this module for? 33 | This module is for environments where you are using [credstash](https://github.com/fugue/credstash) to store secrets, 34 | and you want to read secrets within node without installing python. 35 | The module could be used within your node module to retrieve, for instance, database connection credentials from credstash. 36 | 37 | ## Retrieving the last N versions of a secret 38 | Credstash support [versioning of secrets](https://github.com/fugue/credstash#versioning-secrets) which allows to easily rotate secrets. 39 | 40 | By default node-credstash will return the latest (most recent version of a secret). 41 | You can also retrieve the latest N versions of a secret as follows: 42 | 43 | ```js 44 | const Credstash = require('credstash'); 45 | const credstash = new Credstash(); 46 | 47 | credstash.get('secret', {limit: 3}, (e, secrets) => { 48 | console.log('this is the last version', secrets[0]); 49 | console.log('this is the second-last', secrets[1]); 50 | console.log('this is the third-last', secrets[2]); 51 | }); 52 | ``` 53 | -------------------------------------------------------------------------------- /lib/secrets.js: -------------------------------------------------------------------------------- 1 | const AWS = require('aws-sdk'); 2 | const async = require('async'); 3 | 4 | if (typeof process.env.AWS_DEFAULT_REGION !== 'undefined') { 5 | AWS.config.update({region: process.env.AWS_DEFAULT_REGION}); 6 | } 7 | 8 | // Blatantly borrowed from https://www.electrictoolbox.com/pad-number-zeroes-javascript/ 9 | function pad(number, length) { 10 | var str = '' + number; 11 | while (str.length < length) { 12 | str = '0' + str; 13 | } 14 | return str; 15 | } 16 | 17 | function makeVersion(version) { 18 | return { 19 | ComparisonOperator: 'EQ', 20 | AttributeValueList: [{ 21 | S: pad(version, 19) 22 | }] 23 | }; 24 | } 25 | 26 | function scan(table, options, done) { 27 | var params = { 28 | TableName: table || 'credential-store', 29 | ConsistentRead: true, 30 | ScanFilter: {} 31 | }; 32 | 33 | if (options.version != undefined) { 34 | params.ScanFilter.version = makeVersion(options.version); 35 | } 36 | 37 | return new AWS.DynamoDB().scan(params, done); 38 | } 39 | 40 | function find(table, name, options, done) { 41 | var params = { 42 | TableName: table || 'credential-store', 43 | ConsistentRead: true, 44 | Limit: options.limit, 45 | ScanIndexForward: false, 46 | KeyConditions: { 47 | name: { 48 | ComparisonOperator: 'EQ', 49 | AttributeValueList: [{ 50 | S: name 51 | }] 52 | } 53 | } 54 | }; 55 | 56 | if (options.version != undefined) { 57 | params.KeyConditions.version = makeVersion(options.version); 58 | } 59 | 60 | return new AWS.DynamoDB().query(params, done); 61 | } 62 | 63 | function map(name, data, done) { 64 | if (!data.Items || data.Items.length === 0) { 65 | return done(new Error('secret not found: ' + name)); 66 | } 67 | 68 | var result = data.Items.map(item => ({ 69 | name: item.name.S, 70 | key: item.key.S, 71 | hmac: ('S' in item.hmac) ? item.hmac.S : item.hmac.B.toString('utf8'), 72 | contents: item.contents.S 73 | })); 74 | 75 | return done(null, result); 76 | } 77 | 78 | module.exports = { 79 | get: (table, name, options, done) => { 80 | return async.waterfall([ 81 | async.apply(find, table, name, options), 82 | async.apply(map, name), 83 | ], done); 84 | }, 85 | list: (table, options, done) => { 86 | return async.waterfall([ 87 | async.apply(scan, table, options), 88 | async.apply(map, 'all secrets'), 89 | ], done); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const Credstash = require('../index.js'); 2 | const should = require('chai').should(); 3 | const AWS = require('aws-sdk-mock'); 4 | 5 | describe('credstash', () => { 6 | afterEach(() => { 7 | AWS.restore(); 8 | }); 9 | 10 | it('can get a list secrets', (done) => { 11 | AWS.mock('DynamoDB', 'scan', mockScanWithTake('credential-store')); 12 | AWS.mock('KMS', 'decrypt', mockKMS); 13 | var credstash = new Credstash(); 14 | return credstash.list({ limit: 3 }, (e, secrets) => { 15 | should.not.exist(e); 16 | secrets['name1'].should.equal('magic'); 17 | secrets['name2'].should.equal('magic'); 18 | secrets['name3'].should.equal('magic'); 19 | return done(); 20 | }); 21 | }); 22 | 23 | it('can get secret', (done) => { 24 | AWS.mock('DynamoDB', 'query', mockQuery('credential-store')); 25 | AWS.mock('KMS', 'decrypt', mockKMS); 26 | var credstash = new Credstash(); 27 | return credstash.get('secret', (e, secret) => { 28 | should.not.exist(e); 29 | secret.should.equal('magic'); 30 | return done(); 31 | }); 32 | }); 33 | 34 | it('can get N versions of a secret', (done) => { 35 | AWS.mock('DynamoDB', 'query', mockQueryWithTake('credential-store')); 36 | AWS.mock('KMS', 'decrypt', mockKMS); 37 | var credstash = new Credstash(); 38 | return credstash.get('secret', { limit: 3 }, (e, secrets) => { 39 | should.not.exist(e); 40 | secrets[0].should.equal('magic'); 41 | secrets[1].should.equal('magic'); 42 | secrets[2].should.equal('magic'); 43 | return done(); 44 | }); 45 | }); 46 | 47 | it('can get secret from alternative table', (done) => { 48 | AWS.mock('DynamoDB', 'query', mockQuery('another-table')); 49 | AWS.mock('KMS', 'decrypt', mockKMS); 50 | var credstash = new Credstash({ 51 | table: 'another-table' 52 | }); 53 | return credstash.get('secret', (e, secret) => { 54 | should.not.exist(e); 55 | secret.should.equal('magic'); 56 | return done(); 57 | }); 58 | }); 59 | }); 60 | 61 | var mockKMS = (params, done) => { 62 | var ret = { 63 | Plaintext: new Buffer('KvQ7FPrc2uYXHjW8n+Y63HHCvyRjujeaIZepV/eUkfkz8ZbM9oymmzC69+XLTlbtvRV1MNmo3ngqE+7dJHoDMw==', 'base64') 64 | } 65 | 66 | return done(null, ret); 67 | }; 68 | 69 | var mockQuery = (expectedTable) => { 70 | return (params, done) => { 71 | params.TableName.should.equal(expectedTable); 72 | var ret = { 73 | Items: [{ 74 | name: { 75 | S: 'name' 76 | }, 77 | key: { 78 | S: 'CiBzvX0zBm6hGu0EnbpRJ+eO+HfPOIsG5oq1UDiK+pi/vBLLAQEBAQB4c719MwZuoRrtBJ26USfnjvh3zziLBuaKtVA4ivqYv7wAAACiMIGfBgkqhkiG9w0BBwaggZEwgY4CAQAwgYgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKNQYv5K9wPp+EvLQAgEQgFsITbvzf75MiY6aeIG2v/OzH2ThW5EJrfgNSekCGXONJSs3R8qkOlxFOfnoISvCXylMwBr+iAZFydgZiSyudPE+qocnYi++aVsv+iV9rR7o+FGQtSWKj2UH9PHm' 79 | }, 80 | hmac: { 81 | S: 'ada335c27410033b16887d083aba629c17ad8f88b7982f331e4f6f8df92c41a9' 82 | }, 83 | contents: { 84 | S: 'H2T+k+c=' 85 | } 86 | }] 87 | }; 88 | 89 | return done(null, ret); 90 | }; 91 | }; 92 | 93 | var mockQueryWithTake = (expectedTable) => { 94 | return (params, done) => { 95 | params.Limit.should.equal(3); 96 | params.TableName.should.equal(expectedTable); 97 | var ret = { 98 | Items: [{ 99 | name: { 100 | S: 'name' 101 | }, 102 | key: { 103 | S: 'CiBzvX0zBm6hGu0EnbpRJ+eO+HfPOIsG5oq1UDiK+pi/vBLLAQEBAQB4c719MwZuoRrtBJ26USfnjvh3zziLBuaKtVA4ivqYv7wAAACiMIGfBgkqhkiG9w0BBwaggZEwgY4CAQAwgYgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKNQYv5K9wPp+EvLQAgEQgFsITbvzf75MiY6aeIG2v/OzH2ThW5EJrfgNSekCGXONJSs3R8qkOlxFOfnoISvCXylMwBr+iAZFydgZiSyudPE+qocnYi++aVsv+iV9rR7o+FGQtSWKj2UH9PHm' 104 | }, 105 | hmac: { 106 | S: 'ada335c27410033b16887d083aba629c17ad8f88b7982f331e4f6f8df92c41a9' 107 | }, 108 | contents: { 109 | S: 'H2T+k+c=' 110 | } 111 | },{ 112 | name: { 113 | S: 'name' 114 | }, 115 | key: { 116 | S: 'CiBzvX0zBm6hGu0EnbpRJ+eO+HfPOIsG5oq1UDiK+pi/vBLLAQEBAQB4c719MwZuoRrtBJ26USfnjvh3zziLBuaKtVA4ivqYv7wAAACiMIGfBgkqhkiG9w0BBwaggZEwgY4CAQAwgYgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKNQYv5K9wPp+EvLQAgEQgFsITbvzf75MiY6aeIG2v/OzH2ThW5EJrfgNSekCGXONJSs3R8qkOlxFOfnoISvCXylMwBr+iAZFydgZiSyudPE+qocnYi++aVsv+iV9rR7o+FGQtSWKj2UH9PHm' 117 | }, 118 | hmac: { 119 | S: 'ada335c27410033b16887d083aba629c17ad8f88b7982f331e4f6f8df92c41a9' 120 | }, 121 | contents: { 122 | S: 'H2T+k+c=' 123 | } 124 | },{ 125 | name: { 126 | S: 'name' 127 | }, 128 | key: { 129 | S: 'CiBzvX0zBm6hGu0EnbpRJ+eO+HfPOIsG5oq1UDiK+pi/vBLLAQEBAQB4c719MwZuoRrtBJ26USfnjvh3zziLBuaKtVA4ivqYv7wAAACiMIGfBgkqhkiG9w0BBwaggZEwgY4CAQAwgYgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKNQYv5K9wPp+EvLQAgEQgFsITbvzf75MiY6aeIG2v/OzH2ThW5EJrfgNSekCGXONJSs3R8qkOlxFOfnoISvCXylMwBr+iAZFydgZiSyudPE+qocnYi++aVsv+iV9rR7o+FGQtSWKj2UH9PHm' 130 | }, 131 | hmac: { 132 | S: 'ada335c27410033b16887d083aba629c17ad8f88b7982f331e4f6f8df92c41a9' 133 | }, 134 | contents: { 135 | S: 'H2T+k+c=' 136 | } 137 | }] 138 | }; 139 | 140 | return done(null, ret); 141 | }; 142 | }; 143 | var mockScanWithTake = (expectedTable) => { 144 | return (params, done) => { 145 | params.TableName.should.equal(expectedTable); 146 | var ret = { 147 | Items: [{ 148 | name: { 149 | S: 'name1' 150 | }, 151 | key: { 152 | S: 'CiBzvX0zBm6hGu0EnbpRJ+eO+HfPOIsG5oq1UDiK+pi/vBLLAQEBAQB4c719MwZuoRrtBJ26USfnjvh3zziLBuaKtVA4ivqYv7wAAACiMIGfBgkqhkiG9w0BBwaggZEwgY4CAQAwgYgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKNQYv5K9wPp+EvLQAgEQgFsITbvzf75MiY6aeIG2v/OzH2ThW5EJrfgNSekCGXONJSs3R8qkOlxFOfnoISvCXylMwBr+iAZFydgZiSyudPE+qocnYi++aVsv+iV9rR7o+FGQtSWKj2UH9PHm' 153 | }, 154 | hmac: { 155 | S: 'ada335c27410033b16887d083aba629c17ad8f88b7982f331e4f6f8df92c41a9' 156 | }, 157 | contents: { 158 | S: 'H2T+k+c=' 159 | } 160 | },{ 161 | name: { 162 | S: 'name2' 163 | }, 164 | key: { 165 | S: 'CiBzvX0zBm6hGu0EnbpRJ+eO+HfPOIsG5oq1UDiK+pi/vBLLAQEBAQB4c719MwZuoRrtBJ26USfnjvh3zziLBuaKtVA4ivqYv7wAAACiMIGfBgkqhkiG9w0BBwaggZEwgY4CAQAwgYgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKNQYv5K9wPp+EvLQAgEQgFsITbvzf75MiY6aeIG2v/OzH2ThW5EJrfgNSekCGXONJSs3R8qkOlxFOfnoISvCXylMwBr+iAZFydgZiSyudPE+qocnYi++aVsv+iV9rR7o+FGQtSWKj2UH9PHm' 166 | }, 167 | hmac: { 168 | S: 'ada335c27410033b16887d083aba629c17ad8f88b7982f331e4f6f8df92c41a9' 169 | }, 170 | contents: { 171 | S: 'H2T+k+c=' 172 | } 173 | },{ 174 | name: { 175 | S: 'name3' 176 | }, 177 | key: { 178 | S: 'CiBzvX0zBm6hGu0EnbpRJ+eO+HfPOIsG5oq1UDiK+pi/vBLLAQEBAQB4c719MwZuoRrtBJ26USfnjvh3zziLBuaKtVA4ivqYv7wAAACiMIGfBgkqhkiG9w0BBwaggZEwgY4CAQAwgYgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMKNQYv5K9wPp+EvLQAgEQgFsITbvzf75MiY6aeIG2v/OzH2ThW5EJrfgNSekCGXONJSs3R8qkOlxFOfnoISvCXylMwBr+iAZFydgZiSyudPE+qocnYi++aVsv+iV9rR7o+FGQtSWKj2UH9PHm' 179 | }, 180 | hmac: { 181 | S: 'ada335c27410033b16887d083aba629c17ad8f88b7982f331e4f6f8df92c41a9' 182 | }, 183 | contents: { 184 | S: 'H2T+k+c=' 185 | } 186 | }] 187 | }; 188 | 189 | return done(null, ret); 190 | }; 191 | }; 192 | --------------------------------------------------------------------------------