Skip to content

Instantly share code, notes, and snippets.

@filipeandre
Last active February 27, 2025 12:13
Show Gist options
  • Save filipeandre/17be1f872a8e481b4eb3cf577fd87a36 to your computer and use it in GitHub Desktop.
Save filipeandre/17be1f872a8e481b4eb3cf577fd87a36 to your computer and use it in GitHub Desktop.
Prototype of XKS proxy in Node.js that leverages the official Azure SDKs to interact with Azure Key Vault

Sources

AWS KMS Custom Key Store (XKS) Documentation:

The design and endpoint concepts are inspired by AWS’s documentation for creating an external key store. AWS XKS Documentation

Azure Key Vault Keys Documentation:

Details about managing keys and using cryptographic operations with Azure Key Vault Azure Key Vault Keys Overview.

Azure SDK for JavaScript:

For using the official Azure libraries (including @azure/identity and @azure/keyvault-keys). Azure SDK for JavaScript GitHub Repository

Node.js Crypto Module Documentation:

Used here for generating random data keys. Node.js Crypto Documentation

XKS Proxy API Specification:

Used as reference to validate the implementation. XKS Proxy API Specification Github Repository

Endpoints

Health Check:

The /health endpoint provides a simple JSON response indicating the service is running.

Key Management:

  • POST /keys: Creates a new key in Azure Key Vault.
  • GET /keys/:keyName: Retrieves key details.

Cryptographic Operations:

  • POST /encrypt and /decrypt: Encrypt and decrypt data using a specified key.
  • POST /sign and /verify: Sign a digest and verify a signature.

Data Key Generation:

The /generateDataKey endpoint generates a random key using Node’s built-in crypto module and encrypts it with the specified key.

Each endpoint retrieves the required key from Azure Key Vault and creates a CryptographyClient to perform the necessary cryptographic operations.

// xks-proxy.mjs
// npm install express @azure/identity @azure/keyvault-keys
import express from 'express';
import { DefaultAzureCredential } from '@azure/identity';
import { KeyClient, CryptographyClient } from '@azure/keyvault-keys';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const port = process.env.PORT || 3000;
// Ensure the KEY_VAULT_NAME environment variable is set.
const keyVaultName = process.env.KEY_VAULT_NAME;
if (!keyVaultName) {
console.error('Please set the KEY_VAULT_NAME environment variable.');
process.exit(1);
}
const keyVaultUri = `https://${keyVaultName}.vault.azure.net`;
const credential = new DefaultAzureCredential();
const keyClient = new KeyClient(keyVaultUri, credential);
/**
* GET /health
* Health check endpoint to verify the proxy is running.
*/
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
/**
* POST /keys
* Create a new key in Azure Key Vault.
* Expected JSON body:
* {
* "keyName": "your-key-name",
* "keyType": "RSA" | "EC",
* "keyOps": ["encrypt", "decrypt", ...], // optional
* "options": { ... } // optional, e.g., { "size": 2048 }
* }
*/
app.post('/keys', async (req, res) => {
const { keyName, keyType, keyOps, options } = req.body;
if (!keyName || !keyType) {
return res.status(400).json({ error: 'Missing keyName or keyType' });
}
try {
const result = await keyClient.createKey(keyName, keyType, { keyOps, ...options });
res.json(result);
} catch (error) {
console.error('Create key error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* GET /keys/:keyName
* Retrieve key details from Azure Key Vault.
*/
app.get('/keys/:keyName', async (req, res) => {
const { keyName } = req.params;
try {
const key = await keyClient.getKey(keyName);
res.json(key);
} catch (error) {
console.error('Get key error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* POST /encrypt
* Encrypt data using a specified key.
* Expected JSON body:
* {
* "keyName": "your-key-name",
* "algorithm": "RSA-OAEP", // or any algorithm supported by the key
* "plaintext": "base64-encoded-data"
* }
*/
app.post('/encrypt', async (req, res) => {
const { keyName, algorithm, plaintext } = req.body;
if (!keyName || !algorithm || !plaintext) {
return res.status(400).json({ error: 'Missing parameters: keyName, algorithm, and plaintext are required.' });
}
try {
const key = await keyClient.getKey(keyName);
const cryptoClient = new CryptographyClient(key.id, credential);
const plainBuffer = Buffer.from(plaintext, 'base64');
const encryptResult = await cryptoClient.encrypt(algorithm, plainBuffer);
res.json({ ciphertext: encryptResult.result.toString('base64') });
} catch (error) {
console.error('Encryption error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* POST /decrypt
* Decrypt data using a specified key.
* Expected JSON body:
* {
* "keyName": "your-key-name",
* "algorithm": "RSA-OAEP",
* "ciphertext": "base64-encoded-data"
* }
*/
app.post('/decrypt', async (req, res) => {
const { keyName, algorithm, ciphertext } = req.body;
if (!keyName || !algorithm || !ciphertext) {
return res.status(400).json({ error: 'Missing parameters: keyName, algorithm, and ciphertext are required.' });
}
try {
const key = await keyClient.getKey(keyName);
const cryptoClient = new CryptographyClient(key.id, credential);
const cipherBuffer = Buffer.from(ciphertext, 'base64');
const decryptResult = await cryptoClient.decrypt(algorithm, cipherBuffer);
res.json({ plaintext: decryptResult.result.toString('base64') });
} catch (error) {
console.error('Decryption error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* POST /sign
* Sign a digest using a specified key.
* Expected JSON body:
* {
* "keyName": "your-key-name",
* "algorithm": "RS256", // or any supported signing algorithm
* "digest": "base64-encoded-digest"
* }
*/
app.post('/sign', async (req, res) => {
const { keyName, algorithm, digest } = req.body;
if (!keyName || !algorithm || !digest) {
return res.status(400).json({ error: 'Missing parameters: keyName, algorithm, and digest are required.' });
}
try {
const key = await keyClient.getKey(keyName);
const cryptoClient = new CryptographyClient(key.id, credential);
const digestBuffer = Buffer.from(digest, 'base64');
const signResult = await cryptoClient.sign(algorithm, digestBuffer);
res.json({ signature: signResult.result.toString('base64') });
} catch (error) {
console.error('Sign error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* POST /verify
* Verify a signature using a specified key.
* Expected JSON body:
* {
* "keyName": "your-key-name",
* "algorithm": "RS256",
* "digest": "base64-encoded-digest",
* "signature": "base64-encoded-signature"
* }
*/
app.post('/verify', async (req, res) => {
const { keyName, algorithm, digest, signature } = req.body;
if (!keyName || !algorithm || !digest || !signature) {
return res.status(400).json({ error: 'Missing parameters: keyName, algorithm, digest, and signature are required.' });
}
try {
const key = await keyClient.getKey(keyName);
const cryptoClient = new CryptographyClient(key.id, credential);
const digestBuffer = Buffer.from(digest, 'base64');
const signatureBuffer = Buffer.from(signature, 'base64');
const verifyResult = await cryptoClient.verify(algorithm, digestBuffer, signatureBuffer);
res.json({ isValid: verifyResult.result });
} catch (error) {
console.error('Verify error:', error);
res.status(500).json({ error: error.message });
}
});
/**
* POST /generateDataKey
* Generate a random data key and encrypt it with a specified key.
* Expected JSON body:
* {
* "keyName": "your-key-name",
* "algorithm": "RSA-OAEP",
* "keyLength": 32 // number of random bytes to generate (e.g., 32 for a 256-bit key)
* }
*/
app.post('/generateDataKey', async (req, res) => {
const { keyName, algorithm, keyLength } = req.body;
if (!keyName || !algorithm || !keyLength) {
return res.status(400).json({ error: 'Missing parameters: keyName, algorithm, and keyLength are required.' });
}
try {
// Generate random plaintext data key.
const dataKeyPlaintext = crypto.randomBytes(keyLength);
// Retrieve the key and encrypt the data key.
const key = await keyClient.getKey(keyName);
const cryptoClient = new CryptographyClient(key.id, credential);
const encryptResult = await cryptoClient.encrypt(algorithm, dataKeyPlaintext);
res.json({
plaintext: dataKeyPlaintext.toString('base64'),
ciphertext: encryptResult.result.toString('base64')
});
} catch (error) {
console.error('GenerateDataKey error:', error);
res.status(500).json({ error: error.message });
}
});
app.listen(port, () => {
console.log(`XKS proxy listening on port ${port}`);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment