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
JavaScript
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
});
;