Continuing my earlier answer, I wrote a small program that demonstrates generating Sign, Verify and Encrypt, Decrypt keys on Node (not the browser) from a master key. Run it with ts-node. Sample package.json:
{
"name": "crypto",
"private": true,
"scripts": {
"genKeys": "ts-node src/gen_keys.ts"
},
"devDependencies": {
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}
!! FOR EDUCATIONAL PURPOSES ONLY. YOU ASSUME ALL RISKS. DO NOT COPY PASTE FOR USE IN PRODUCTION !!
async function main(): Promise<void> {
const crypto: Crypto = globalThis.crypto
const toHex = (buffer: ArrayBuffer): string => {
const hashArray = Array.from(new Uint8Array(buffer))
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
return hashHex
}
const fromHex = (hashHex: string): ArrayBuffer => {
if (typeof hashHex !== "string") {
throw new TypeError("Expected input to be a string")
}
if ((hashHex.length % 2) !== 0) {
throw new RangeError("Expected string to be an even number of characters")
}
const result = new Uint8Array(hashHex.length / 2)
for (let i = 0, j = 0; i < result.length; i += 1, j += 2) {
result[i] = parseInt(hashHex.substring(j, j + 2), 16)
}
return result.buffer
}
/*
* Keep below string in a secret location, such as AWS secrets, 512 random bits
*/
const getSecretRawKeyStr = async (): Promise<string> => {
return "1eb85f2e8833d299e0a4fec2ba501f72c6944a660abc1ecabc20f4c0f06ff27d2d5159bec98821ef4d893755d0b711ccdc0e881a7adb6f54acbdbeb5c8dfebd9";
}
const makeMasterKey = async (): Promise<CryptoKey> => {
const rawKeyBuf = fromHex(await getSecretRawKeyStr())
return crypto.subtle.importKey(
"raw",
rawKeyBuf,
{ name: "HKDF" },
false,
["deriveKey"],
);
}
const makeEncDecKey = async (masterKey: CryptoKey, salt: ArrayBuffer, purpose: string): Promise<CryptoKey> => {
/*
* Any change to salt or purpose will generate a different key
*/
return crypto.subtle.deriveKey(
{
name: "HKDF",
salt: salt,
info: new TextEncoder().encode(purpose),
hash: "SHA-256",
},
masterKey,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
}
const makeSignVerifyKey = async (masterKey: CryptoKey, salt: ArrayBuffer, purpose: string): Promise<CryptoKey> => {
/*
* Any change to salt or purpose will generate a different key
*/
return crypto.subtle.deriveKey(
{
name: "HKDF",
salt: salt,
info: new TextEncoder().encode(purpose),
hash: "SHA-256",
},
masterKey,
{
name: "HMAC",
hash: { name: "SHA-512" },
},
true,
["sign", "verify"],
);
}
const masterKey = await makeMasterKey()
/*
* Depending on your use case, keep this in secrets or generate
* for each session
*/
const getSaltForEncDec = () => "eaa6867c1963b8f25168f36d5c92019a"
const encDecKeyA = await makeEncDecKey(
masterKey,
fromHex(getSaltForEncDec()),
"This key is for enc and dec")
const encDecKeyB = await makeEncDecKey(
masterKey,
fromHex(getSaltForEncDec()),
"This key is for enc and dec")
console.log(await crypto.subtle.exportKey("jwk", encDecKeyA))
console.log(await crypto.subtle.exportKey("jwk", encDecKeyB))
const secretText = "All work and no play makes Jack a dull boy"
/*
* Depending on your use case, keep this in secrets or generate
* for each session
*/
const getIvForEncDec = () => "c739e132c311fc8df216a61a1d9840b4"
const encrypted: ArrayBuffer = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: fromHex(getIvForEncDec())
},
encDecKeyA,
new TextEncoder().encode(secretText)
)
console.log(`Encrypted ${toHex(encrypted)}`)
const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt(
{
name: "AES-GCM",
/*
* Iv must be same as that used for encryption
*/
iv: fromHex(getIvForEncDec())
},
encDecKeyB,
encrypted
))
console.log(`Decrypted ${decrypted}`)
/*
* Depending on your use case, keep this in secrets or
* generate for each session
*/
const getSaltForSignVerify = () => "e2add8a6cd3543522aad6ddbe59132b7"
const signVerifyKeyA = await makeSignVerifyKey(
masterKey,
fromHex(getSaltForSignVerify()),
"This key is for sign and verify")
const signVerifyKeyB = await makeSignVerifyKey(
masterKey,
fromHex(getSaltForSignVerify()),
"This key is for sign and verify")
console.log(await crypto.subtle.exportKey("jwk", signVerifyKeyA))
console.log(await crypto.subtle.exportKey("jwk", signVerifyKeyB))
const signature = await crypto.subtle.sign(
"HMAC",
signVerifyKeyA,
new TextEncoder().encode(secretText)
)
const signatureValid = await crypto.subtle.verify(
"HMAC",
signVerifyKeyB,
signature,
new TextEncoder().encode(secretText),
)
console.log(`Signature ${toHex(signature)}, verified ${signatureValid}`)
//console.log(toHex(crypto.getRandomValues(new Uint8Array(16))))
}
main().catch(console.error);
deriveKey
with ECDSA. Sounds like your issue is more specific than your question suggests. You might get a better answer if you post a more specific question that includes the minimal code you are using to implement the feature of the library you are using and explain exactly where you are stuck in that code. – Neb