UNPKG

node-credstasher

Version:

A TypeScript implementation of credstash for storing and retrieving secrets using AWS KMS and DynamoDB.

301 lines (300 loc) 12.3 kB
"use strict"; var __webpack_require__ = {}; (()=>{ __webpack_require__.n = (module)=>{ var getter = module && module.__esModule ? ()=>module['default'] : ()=>module; __webpack_require__.d(getter, { a: getter }); return getter; }; })(); (()=>{ __webpack_require__.d = (exports1, definition)=>{ for(var key in definition)if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports1, key)) Object.defineProperty(exports1, key, { enumerable: true, get: definition[key] }); }; })(); (()=>{ __webpack_require__.o = (obj, prop)=>Object.prototype.hasOwnProperty.call(obj, prop); })(); (()=>{ __webpack_require__.r = (exports1)=>{ if ('undefined' != typeof Symbol && Symbol.toStringTag) Object.defineProperty(exports1, Symbol.toStringTag, { value: 'Module' }); Object.defineProperty(exports1, '__esModule', { value: true }); }; })(); var __webpack_exports__ = {}; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { default: ()=>CredstashClient, CredstashClient: ()=>CredstashClient }); const client_dynamodb_namespaceObject = require("@aws-sdk/client-dynamodb"); const client_kms_namespaceObject = require("@aws-sdk/client-kms"); const lib_dynamodb_namespaceObject = require("@aws-sdk/lib-dynamodb"); const external_aes_js_namespaceObject = require("aes-js"); var external_aes_js_default = /*#__PURE__*/ __webpack_require__.n(external_aes_js_namespaceObject); const external_crypto_namespaceObject = require("crypto"); var external_crypto_default = /*#__PURE__*/ __webpack_require__.n(external_crypto_namespaceObject); const DEFAULT_DIGEST = "sha256"; const decryptItem = async (dbItem, kmsClient, options = {})=>{ const item = { contents: dbItem.contents, hmac: Buffer.from(dbItem.hmac).toString("utf-8"), key: dbItem.key, digest: dbItem.digest ?? DEFAULT_DIGEST }; const encryptionContext = options.context ? { EncryptionContext: { ...options.context } } : {}; const decryptedItem = await kmsClient.send(new client_kms_namespaceObject.DecryptCommand({ CiphertextBlob: Buffer.from(item.key, "base64"), ...encryptionContext })); if (!decryptedItem.Plaintext) throw new Error("Failed to decrypt item key"); const key = decryptedItem.Plaintext.slice(0, 32); const hmacKey = decryptedItem.Plaintext.slice(32); const contents = Buffer.from(item.contents, "base64"); const hmac = external_crypto_default().createHmac(item.digest, hmacKey); hmac.update(contents); const hmacHex = hmac.digest("hex"); if (hmacHex !== item.hmac) throw new Error("HMAC verification failed"); const decrypt = new (external_aes_js_default()).ModeOfOperation.ctr(key); const decryptedBytes = decrypt.decrypt(contents); const plaintext = external_aes_js_default().utils.utf8.fromBytes(decryptedBytes); return plaintext; }; const encryptItem = async (secret, kmsKeyId, kmsClient, options = {})=>{ const encryptionContext = options.context ? { EncryptionContext: options.context } : {}; const dataKeyResponse = await kmsClient.send(new client_kms_namespaceObject.GenerateDataKeyCommand({ KeyId: kmsKeyId, NumberOfBytes: 64, ...encryptionContext })); if (!dataKeyResponse.Plaintext || !dataKeyResponse.CiphertextBlob) throw new Error("Failed to generate data key"); const key = dataKeyResponse.Plaintext.slice(0, 32); const hmacKey = dataKeyResponse.Plaintext.slice(32); const encryptedKey = dataKeyResponse.CiphertextBlob; const encrypt = new (external_aes_js_default()).ModeOfOperation.ctr(key); const valueBytes = external_aes_js_default().utils.utf8.toBytes(secret); const encryptedValue = encrypt.encrypt(valueBytes); const hmac = external_crypto_default().createHmac(DEFAULT_DIGEST, hmacKey); hmac.update(encryptedValue); const hmacHex = hmac.digest("hex"); return { contents: Buffer.from(encryptedValue).toString("base64"), hmac: hmacHex, key: Buffer.from(encryptedKey).toString("base64"), digest: DEFAULT_DIGEST }; }; const DEFAULT_REGION = "us-east-1"; const DEFAULT_TABLE = "credential-store"; const DEFAULT_KMS_KEY_ID = "alias/credstash"; const DEFAULT_KMS_REGION = "us-east-1"; const DEFAULT_PROFILE = "default"; class CredstashClient { config; dynamoClient; docClient; kmsClient; constructor(config = {}){ const region = config.region || process.env.AWS_REGION || DEFAULT_REGION; const kmsRegion = config.kmsRegion || process.env.KMS_REGION || config.region || DEFAULT_KMS_REGION; this.config = { region, kmsRegion, table: config.table || process.env.CREDSTASH_TABLE || DEFAULT_TABLE, kmsKeyId: config.kmsKeyId || process.env.CREDSTASH_KMS_KEY_ID || DEFAULT_KMS_KEY_ID, profile: config.profile || process.env.AWS_PROFILE || DEFAULT_PROFILE, dynamodbEndpoint: config.dynamodbEndpoint || process.env.DYNAMODB_ENDPOINT || "", kmsEndpoint: config.kmsEndpoint || process.env.KMS_ENDPOINT || "" }; this.dynamoClient = new client_dynamodb_namespaceObject.DynamoDBClient({ region: this.config.region, endpoint: this.config.dynamodbEndpoint || void 0 }); this.kmsClient = new client_kms_namespaceObject.KMSClient({ region: this.config.kmsRegion, endpoint: this.config.kmsEndpoint || void 0 }); this.docClient = lib_dynamodb_namespaceObject.DynamoDBDocumentClient.from(this.dynamoClient); } async checkForTable() { try { await this.dynamoClient.send(new client_dynamodb_namespaceObject.DescribeTableCommand({ TableName: this.config.table })); } catch (error) { if (error instanceof client_dynamodb_namespaceObject.ResourceNotFoundException) throw new Error(`Credstash table '${this.config.table}' not found. Please create it first. You can run 'credstasher setup' to create it.`); throw error; } } async ensureTableExists() { try { await this.dynamoClient.send(new client_dynamodb_namespaceObject.DescribeTableCommand({ TableName: this.config.table })); } catch (error) { if (error instanceof client_dynamodb_namespaceObject.ResourceNotFoundException) await this.createTable(); else throw error; } } async createTable() { const createTableCommand = new client_dynamodb_namespaceObject.CreateTableCommand({ TableName: this.config.table, KeySchema: [ { AttributeName: "name", KeyType: "HASH" }, { AttributeName: "version", KeyType: "RANGE" } ], AttributeDefinitions: [ { AttributeName: "name", AttributeType: "S" }, { AttributeName: "version", AttributeType: "S" } ], BillingMode: "PAY_PER_REQUEST" }); await this.dynamoClient.send(createTableCommand); } async putSecret(name, secret, options = {}) { const version = options.version || await this.getNextVersion(name); const kmsKeyId = options.kmsKeyId || this.config.kmsKeyId; const encryptedItem = await encryptItem(secret, kmsKeyId, this.kmsClient, options); const itemRow = { name, version, ...encryptedItem }; await this.docClient.send(new lib_dynamodb_namespaceObject.PutCommand({ TableName: this.config.table, Item: itemRow })); } async getSecret(name, options = {}) { const response = await this.docClient.send(new lib_dynamodb_namespaceObject.QueryCommand({ TableName: this.config.table, ConsistentRead: true, ScanIndexForward: false, KeyConditionExpression: "#name = :name", ExpressionAttributeNames: { "#name": "name" }, ExpressionAttributeValues: { ":name": name } })); if (!response.Items || 0 === response.Items.length) throw new Error(`Secret '${name}' not found`); let item; if (void 0 !== options.version) { item = response.Items.find((item)=>item.version === options.version); if (!item) throw new Error(`Version '${options.version}' of secret '${name}' not found`); } else item = response.Items[0]; return await decryptItem(item, this.kmsClient, options); } async deleteSecret(name, options = {}) { if (options.all) { const versions = await this.listVersions(name); for (const version of versions)await this.docClient.send(new lib_dynamodb_namespaceObject.DeleteCommand({ TableName: this.config.table, Key: { name, version: version.version } })); } else { const version = options.version || await this.getLatestVersion(name); await this.docClient.send(new lib_dynamodb_namespaceObject.DeleteCommand({ TableName: this.config.table, Key: { name, version } })); } } async listSecrets() { const items = []; let lastEvaluatedKey; do { const response = await this.docClient.send(new lib_dynamodb_namespaceObject.ScanCommand({ TableName: this.config.table, ProjectionExpression: "#name, version, #comment", ExpressionAttributeNames: { "#name": "name", "#comment": "comment" }, ...lastEvaluatedKey && { ExclusiveStartKey: lastEvaluatedKey } })); if (response.Items) items.push(...response.Items); lastEvaluatedKey = response.LastEvaluatedKey; }while (lastEvaluatedKey); return items.map((item)=>({ name: item.name, version: item.version })); } async listVersions(name) { const response = await this.docClient.send(new lib_dynamodb_namespaceObject.QueryCommand({ TableName: this.config.table, KeyConditionExpression: "#name = :name", ExpressionAttributeNames: { "#name": "name" }, ExpressionAttributeValues: { ":name": name }, ProjectionExpression: "#name, version" })); return (response.Items || []).map((item)=>({ name: item.name, version: item.version })); } async getLatestVersion(name) { const versions = await this.listVersions(name); if (0 === versions.length) throw new Error(`Secret '${name}' not found`); const numericVersions = versions.map((v)=>Number.parseInt(v.version, 10)).filter((v)=>!Number.isNaN(v)).sort((a, b)=>b - a); return numericVersions.length > 0 ? numericVersions[0]?.toString() ?? "1" : "1"; } async getNextVersion(name) { try { const latestVersion = await this.getLatestVersion(name); return (Number.parseInt(latestVersion, 10) + 1).toString(); } catch (_error) { return "1"; } } } exports.CredstashClient = __webpack_exports__.CredstashClient; exports["default"] = __webpack_exports__["default"]; for(var __webpack_i__ in __webpack_exports__)if (-1 === [ "CredstashClient", "default" ].indexOf(__webpack_i__)) exports[__webpack_i__] = __webpack_exports__[__webpack_i__]; Object.defineProperty(exports, '__esModule', { value: true });