|
// 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}`); |
|
}); |