I want to protect a RSA private key stored in localStorage by wrapping it with a key derived from the user's password.
However when unwrapping the key the error DOMException: An invalid or illegal string was specified
is thrown.
This is a minimal snippet for reproducing the issue:
const base64ToArrayBuffer = (data) => {
const binaryKey = atob(data);
const keyBytes = new Uint8Array(binaryKey.length);
for (let i = 0; i < binaryKey.length; i++) {
keyBytes[i] = binaryKey.charCodeAt(i);
}
return keyBytes.buffer;
}
const bufferToBase64 = (data) => btoa(String.fromCharCode(... new Uint8Array(data)));
const fn = async () => {
// Generate RSA key pair
const keyPair = await crypto.subtle.generateKey({
name: "RSA-OAEP",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-512"
}, true, ["encrypt", "decrypt"]);
// Save private key
// Encrypt the private key
const textEncoder = new TextEncoder();
const salt = new Uint8Array(16);
crypto.getRandomValues(salt);
const passwordKey = await crypto.subtle.importKey("raw", textEncoder.encode(window.prompt("password")), "PBKDF2", true, ["deriveKey"]);
const derivedKey = await crypto.subtle.deriveKey({
name: "PBKDF2",
hash: "SHA-256",
salt,
iterations: 210000
}, passwordKey, {
name: "AES-CBC",
length: 256
}, true, ["wrapKey", "unwrapKey"]);
const iv = new Uint8Array(16);
crypto.getRandomValues(iv);
const wrappedPrivateKey = await crypto.subtle.wrapKey("pkcs8", keyPair.privateKey, derivedKey, {
name: "AES-CBC",
iv
});
const b64WrappedPrivateKey = bufferToBase64(wrappedPrivateKey);
const b64Salt = bufferToBase64(salt);
const b64IV = bufferToBase64(iv);
const encryptedPrivateKey = base64ToArrayBuffer(b64WrappedPrivateKey);
const unwrapSalt = base64ToArrayBuffer(b64Salt);
const unwrapIV = base64ToArrayBuffer(b64IV);
const unwrapPasswordKey = await crypto.subtle.importKey("raw", textEncoder.encode(window.prompt("password unwrap")), "PBKDF2", true, ["deriveKey"]);
const unwrappingKey = await crypto.subtle.deriveKey({
name: "PBKDF2",
hash: "SHA-256",
salt: unwrapSalt,
iterations: 210000
}, unwrapPasswordKey, {
name: "AES-CBC",
length: 256
}, true, ["wrapKey", "unwrapKey"]);
try {
const privateKey = await crypto.subtle.unwrapKey("pkcs8", encryptedPrivateKey, unwrappingKey, {
name: "AES-CBC",
iv: unwrapIV
}, {
name: "RSA-OAEP",
hash: "SHA-512"
}, true, ["encrypt", "decrypt"]);
console.log("Success");
} catch (err) {
console.log(err);
}
};
(async () => {
await fn();
})()
Security is considered up to a certain point because this is just a demo project.
After debugging with the Node.js crypto library, I found out that the actual error was Unsupported key usage for an RSA-OAEP key
because I was passing the encrypt
usage for a RSA private key which is not possible.