feat: added key wrapping

This commit is contained in:
Daniel Hougaard
2024-11-11 21:45:06 +04:00
parent 3989646b80
commit 472f02e8b1
+80 -39
View File
@@ -118,8 +118,8 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe
let sessionManager: HsmSessionManager | null = null; let sessionManager: HsmSessionManager | null = null;
const $findKey = (session: grapheneLib.Session) => { const $findMasterKey = (session: grapheneLib.Session) => {
// Find the existing AES key // Find the master key (root key)
const template = { const template = {
class: graphene.ObjectClass.SECRET_KEY, class: graphene.ObjectClass.SECRET_KEY,
keyType: graphene.KeyType.AES, keyType: graphene.KeyType.AES,
@@ -129,15 +129,58 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe
const key = session.find(template).items(0); const key = session.find(template).items(0);
if (!key) { if (!key) {
throw new Error("Failed to encrypt data, AES key not found"); throw new Error("Failed to find master key");
} }
return key; return key;
}; };
const $generateAndWrapKey = (session: grapheneLib.Session) => {
const masterKey = $findMasterKey(session);
// Generate a new session key for encryption
const sessionKey = session.generateKey(graphene.KeyGenMechanism.AES, {
class: graphene.ObjectClass.SECRET_KEY,
keyType: graphene.KeyType.AES,
token: false, // Session-only key
sensitive: true,
extractable: true, // Must be true to allow wrapping
encrypt: true,
decrypt: true,
valueLen: 32 // 256-bit key
} as grapheneLib.ITemplate);
// Wrap the session key with master key
const wrappingMech = { name: "AES_KEY_WRAP", params: null };
const wrappedKey = session.wrapKey(
wrappingMech,
new graphene.Key(masterKey).toType(),
new graphene.Key(sessionKey).toType()
);
return { wrappedKey, sessionKey };
};
const $unwrapKey = (session: grapheneLib.Session, wrappedKey: Buffer) => {
const masterKey = $findMasterKey(session);
// Absolute minimal template - let HSM set most attributes
const unwrapTemplate = {
class: graphene.ObjectClass.SECRET_KEY,
keyType: graphene.KeyType.AES
} as grapheneLib.ITemplate;
const unwrappingMech = {
name: "AES_KEY_WRAP",
params: null
} as grapheneLib.MechanismType;
return session.unwrapKey(unwrappingMech, new graphene.Key(masterKey).toType(), wrappedKey, unwrapTemplate);
};
const $keyExists = (session: grapheneLib.Session): boolean => { const $keyExists = (session: grapheneLib.Session): boolean => {
try { try {
const key = $findKey(session); const key = $findMasterKey(session);
// items(0) will throw an error if no items are found // items(0) will throw an error if no items are found
// Return true only if we got a valid object with handle // Return true only if we got a valid object with handle
return key && typeof key.handle !== "undefined"; return key && typeof key.handle !== "undefined";
@@ -167,26 +210,24 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe
const session = sessionManager.getSession(); const session = sessionManager.getSession();
try { try {
// Check if key already exists // Check if master key exists, create if not
if ($keyExists(session)) { if (!$keyExists(session)) {
logger.info("Key already exists, skipping creation"); // Generate 256-bit AES master key with persistent storage
} else {
// Generate 256-bit AES key with persistent storage
session.generateKey(graphene.KeyGenMechanism.AES, { session.generateKey(graphene.KeyGenMechanism.AES, {
class: graphene.ObjectClass.SECRET_KEY, class: graphene.ObjectClass.SECRET_KEY,
token: true, // This ensures the key is stored persistently token: true,
valueLen: 256 / 8, valueLen: 256 / 8,
keyType: graphene.KeyType.AES, keyType: graphene.KeyType.AES,
label: appCfg.HSM_KEY_LABEL, label: appCfg.HSM_KEY_LABEL,
encrypt: true, derive: true, // Enable key derivation
decrypt: true, extractable: false,
extractable: false, // Prevent key export sensitive: true,
sensitive: true, // Mark as sensitive data private: true
private: true // Require login to access
}); });
logger.info(`Key created successfully with label: ${appCfg.HSM_KEY_LABEL}`); logger.info(`Master key created successfully with label: ${appCfg.HSM_KEY_LABEL}`);
} }
// Verify HSM supports required mechanisms
const mechs = session.slot.getMechanisms(); const mechs = session.slot.getMechanisms();
let gotAesGcmMechanism = false; let gotAesGcmMechanism = false;
@@ -200,10 +241,10 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe
} }
if (!gotAesGcmMechanism) { if (!gotAesGcmMechanism) {
throw new Error("Failed to initialize HSM. AES GCM encryption mechanism not supported by the HSM"); throw new Error("HSM does not support AES_GCM mechanism");
} }
} catch (error) { } catch (error) {
logger.error(error, "Error creating HSM key"); logger.error(error, "Error initializing HSM service");
throw error; throw error;
} }
}; };
@@ -217,55 +258,55 @@ export const hsmServiceFactory = ({ pkcs11Module: { module, graphene } }: THsmSe
throw new Error("HSM Session manager is not initialized"); throw new Error("HSM Session manager is not initialized");
} }
const session = sessionManager.getSession(); const session = sessionManager.getSession();
const key = $findKey(session);
// Generate IV // Generate IV for encryption
const iv = session.generateRandom(IV_LENGTH); const iv = session.generateRandom(IV_LENGTH);
// Generate and wrap a new session key
const { wrappedKey, sessionKey } = $generateAndWrapKey(session);
const alg = { const alg = {
name: appCfg.HSM_MECHANISM, name: appCfg.HSM_MECHANISM,
params: new graphene.AesGcm240Params(iv) params: new graphene.AesGcm240Params(iv)
} as grapheneLib.IAlgorithm; } as grapheneLib.IAlgorithm;
const cipher = session.createCipher(alg, new graphene.Key(key).toType()); const cipher = session.createCipher(alg, new graphene.Key(sessionKey).toType());
// Calculate the output buffer size based on input length // Calculate the output buffer size based on input length
// GCM adds a 16-byte auth tag, so we need input length + 16 // GCM adds a 16-byte auth tag, so we need input length + 16
const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH); const outputBuffer = Buffer.alloc(data.length + TAG_LENGTH);
const encryptedData = cipher.once(data, outputBuffer); const encryptedData = cipher.once(data, outputBuffer);
// Combine IV + encrypted data into a single buffer // Format: [Wrapped Key (40)][IV (16)][Encrypted Data + Tag]
// Format: [IV (16 bytes)][Encrypted Data][Auth Tag (16 bytes)] return Buffer.concat([wrappedKey, iv, encryptedData]);
return Buffer.concat([iv, encryptedData]);
} }
function decrypt(encryptedBlob: Buffer): Buffer { function decrypt(encryptedBlob: Buffer): Buffer {
if (!module) { const WRAPPED_KEY_LENGTH = 32 + 8; // AES-256 key + padding
throw new Error("PKCS#11 module is not initialized");
}
if (!sessionManager) { if (!module || !sessionManager) {
throw new Error("HSM Session manager is not initialized"); throw new Error("HSM service not initialized");
} }
const session = sessionManager.getSession(); const session = sessionManager.getSession();
const key = $findKey(session);
// Extract IV, ciphertext, and tag from the blob // Extract wrapped key, IV, and ciphertext
const iv = encryptedBlob.subarray(0, IV_LENGTH); const wrappedKey = encryptedBlob.subarray(0, WRAPPED_KEY_LENGTH);
const ciphertext = encryptedBlob.subarray(IV_LENGTH, encryptedBlob.length); const iv = encryptedBlob.subarray(WRAPPED_KEY_LENGTH, WRAPPED_KEY_LENGTH + IV_LENGTH);
const ciphertext = encryptedBlob.subarray(WRAPPED_KEY_LENGTH + IV_LENGTH);
// Unwrap the session key
const sessionKey = $unwrapKey(session, wrappedKey);
const algo = { const algo = {
name: appCfg.HSM_MECHANISM, name: appCfg.HSM_MECHANISM,
params: new graphene.AesGcm240Params(iv) // Pass both IV and tag params: new graphene.AesGcm240Params(iv)
}; };
const decipher = session.createDecipher(algo, new graphene.Key(key).toType()); const decipher = session.createDecipher(algo, new graphene.Key(sessionKey).toType());
// Allocate buffer for decrypted data
const outputBuffer = Buffer.alloc(ciphertext.length); const outputBuffer = Buffer.alloc(ciphertext.length);
const decrypted = decipher.once(ciphertext, outputBuffer); return decipher.once(ciphertext, outputBuffer);
return decrypted;
} }
return { return {
encrypt, encrypt,