@bryopsida/crypto
Version:
A crypto library utilizing @bryopsida/key-store to protect data using data encryption keys
212 lines • 8.36 kB
JavaScript
import { Stream } from 'stream';
import { readFile } from 'fs/promises';
import { randomBytes, createCipheriv, randomUUID, createDecipheriv, createHmac, } from 'node:crypto';
import { resolveHome } from './resolve.js';
export class Crypto {
masterKeyPath;
masterKeyContext;
keyStore;
constructor(keyStore, masterKeyPath, masterKeyContext) {
this.masterKeyPath = masterKeyPath;
this.masterKeyContext = masterKeyContext;
this.keyStore = keyStore;
}
encodeCipherText(cipherTxt) {
const concatenatedBuffer = Buffer.concat([
Buffer.from(cipherTxt.rootKeyId),
Buffer.from(cipherTxt.keyId),
cipherTxt.iv,
cipherTxt.authTag,
cipherTxt.ciphertext,
]);
return Promise.resolve(concatenatedBuffer.toString('base64'));
}
async encryptAndEncode(encryptOpts) {
const cipherText = await this.encrypt(encryptOpts);
return this.encodeCipherText(cipherText);
}
decryptEncoded(encodedCipherText, rootKeyContext, dekContext, context) {
const concatenatedBuffer = Buffer.from(encodedCipherText, 'base64');
const rootKeyId = concatenatedBuffer.slice(0, 36).toString('utf8');
const keyId = concatenatedBuffer.slice(36, 72).toString('utf8');
const iv = concatenatedBuffer.slice(72, 88);
const authTag = concatenatedBuffer.slice(88, 104);
const ciphertext = concatenatedBuffer.slice(104);
return this.decrypt({
algorithm: 'aes-256-gcm',
rootKeyId,
keyId,
iv,
authTag,
ciphertext,
rootKeyContext,
dekContext,
context,
});
}
async readFileFromPath(path) {
const buffer = await readFile(resolveHome(path), 'utf-8');
return Buffer.from(buffer, 'base64');
}
async unsealRootKey(keyId, keyContext) {
const sealedKey = await this.keyStore.fetchSealedRootKey(keyId);
const iv = sealedKey.slice(0, 16);
const authTag = sealedKey.slice(sealedKey.length - 16);
const encryptedKey = sealedKey.slice(16, sealedKey.length - 16);
const aead = Buffer.from(keyContext || (await this.readFileFromPath(this.masterKeyContext)));
const rootKeyDecipher = createDecipheriv('aes-256-gcm', await this.readFileFromPath(this.masterKeyPath), iv, {
authTagLength: 16,
});
rootKeyDecipher.setAuthTag(authTag);
rootKeyDecipher.setAAD(aead);
const key = rootKeyDecipher.update(encryptedKey);
return Buffer.concat([key, rootKeyDecipher.final()]);
}
async unsealDekKey(keyId, rootKeyId, keyContext, rootKeyContext) {
const rootKey = await this.unsealRootKey(rootKeyId, rootKeyContext);
const sealedKey = await this.keyStore.fetchSealedDataEncKey(keyId);
const iv = sealedKey.slice(0, 16);
const authTag = sealedKey.slice(sealedKey.length - 16);
const encryptedKey = sealedKey.slice(16, sealedKey.length - 16);
const keyDecipher = createDecipheriv('aes-256-gcm', rootKey, iv, {
authTagLength: 16,
});
keyDecipher.setAuthTag(authTag);
if (keyContext) {
keyDecipher.setAAD(Buffer.from(keyContext));
}
const key = keyDecipher.update(encryptedKey);
return Buffer.concat([key, keyDecipher.final()]);
}
async seal(data, key, context) {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', key, iv, {
authTagLength: 16,
});
let aead;
if (context) {
aead = Buffer.from(context);
}
else {
aead = await this.readFileFromPath(this.masterKeyContext);
}
cipher.setAAD(aead);
const ciphertext = Buffer.concat([cipher.update(data), cipher.final()]);
const authTag = cipher.getAuthTag();
return {
keyId: randomUUID(),
rootKeyId: 'master',
keyCipherText: ciphertext,
iv,
authTag,
};
}
async saveSealedKey(sealedKey) {
await this.keyStore.saveSealedDataEncKey(sealedKey.keyId, Buffer.concat([
sealedKey.iv,
sealedKey.keyCipherText,
sealedKey.authTag || Buffer.alloc(0),
]));
}
async saveSealedRootKey(sealedKey) {
await this.keyStore.saveSealedRootKey(sealedKey.keyId, Buffer.concat([
sealedKey.iv,
sealedKey.keyCipherText,
sealedKey.authTag || Buffer.alloc(0),
]));
}
async generateRootKey(size, context) {
const key = randomBytes(size);
if (!context) {
context = (await this.readFileFromPath(this.masterKeyContext)).toString('utf-8');
}
const sealedKey = await this.seal(key, await this.readFileFromPath(this.masterKeyPath), context);
await this.saveSealedRootKey(sealedKey);
return sealedKey.keyId;
}
async generateDataEncKey(size, rootKeyId, rootKeyContext, dekContext) {
const key = randomBytes(size);
const rootKey = await this.unsealRootKey(rootKeyId, rootKeyContext);
const sealedKey = await this.seal(key, rootKey, dekContext);
await this.saveSealedKey(sealedKey);
return sealedKey.keyId;
}
async destroyDataEncKey(keyId) {
await this.keyStore.destroySealedDataEncKey(keyId);
}
async destroyRootKey(rootKeyId) {
await this.keyStore.destroySealedRootKey(rootKeyId);
}
async encrypt(encryptRequest) {
const dek = await this.unsealDekKey(encryptRequest.keyId, encryptRequest.rootKeyId, encryptRequest.dekContext, encryptRequest.rootKeyContext);
const iv = encryptRequest.iv || randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', dek, iv, {
authTagLength: 16,
});
if (encryptRequest.context) {
cipher.setAAD(Buffer.from(encryptRequest.context));
}
if (encryptRequest.plaintext instanceof Stream) {
const retText = {
keyId: encryptRequest.keyId,
rootKeyId: encryptRequest.rootKeyId,
iv,
algorithm: 'aes-256-gcm',
ciphertext: encryptRequest.plaintext.pipe(cipher).on('finish', () => {
retText.authTag = cipher.getAuthTag();
}),
};
return retText;
}
return {
keyId: encryptRequest.keyId,
rootKeyId: encryptRequest.rootKeyId,
iv,
algorithm: 'aes-256-gcm',
ciphertext: Buffer.concat([
cipher.update(encryptRequest.plaintext),
cipher.final(),
]),
authTag: cipher.getAuthTag(),
};
}
async decrypt(decryptOpts) {
const dek = await this.unsealDekKey(decryptOpts.keyId, decryptOpts.rootKeyId, decryptOpts.dekContext, decryptOpts.rootKeyContext);
const decipher = createDecipheriv('aes-256-gcm', dek, decryptOpts.iv, {
authTagLength: 16,
});
if (decryptOpts.authTag) {
decipher.setAuthTag(decryptOpts.authTag);
}
if (decryptOpts.context) {
decipher.setAAD(Buffer.from(decryptOpts.context));
}
if (decryptOpts.ciphertext instanceof Stream) {
return decryptOpts.ciphertext.pipe(decipher);
}
return Buffer.concat([
decipher.update(decryptOpts.ciphertext),
decipher.final(),
]);
}
async close() {
await this.keyStore.close();
}
hasDataEncKey(keyId) {
return this.keyStore.hasSealedDataEncKey(keyId);
}
hasRootKey(rootKeyId) {
return this.keyStore.hasSealedRootKey(rootKeyId);
}
async mac(keyOpts, message) {
const dek = await this.unsealDekKey(keyOpts.keyId, keyOpts.rootKeyId, keyOpts.dekContext, keyOpts.rootKeyContext);
const hmac = createHmac('sha256', dek);
hmac.update(message);
const result = hmac.digest();
return Promise.resolve(result);
}
async validate(opts, message, digest) {
return (await this.mac(opts, message)).equals(digest);
}
}
//# sourceMappingURL=crypto.js.map