It's possible to export a key in different format (However not all type of key support all format don't know why !). For this to be possible when you generate/import the key you need to specify that the key is extractable as you said. The Web Cryptography API says:
If the [[extractable]] internal slot of key is false, then throw an InvalidAccessError.
However you can export key securely (But some malicious js it your page can also extract it).
For instance if you want to be able to export a ECDSA key:
window.crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256", // the curve name
},
true, // <== Here if you want it to be exportable !!
["sign", "verify"] // usage
)
.then(function(key){
//returns a keypair object
console.log(key);
console.log(key.publicKey);
console.log(key.privateKey);
})
.catch(function(err){
console.error(err);
});
Then you can export the public and private key in JWT. Example for the private key:
window.crypto.subtle.exportKey(
"jwk", // here you can change the format but i think that only jwk is supported for both public and private key. JWK is easier to use later
privateKey
)
.then(function(keydata){
//returns the exported key data
console.log(keydata);
})
.catch(function(err){
console.error(err);
});
Then you can save it in json file and let the user download it and import it later. To add additional security you could ask for a password to encrypt the json file in AES. And forbid export once user imported the key. He/She already has it so it's useless for him to export it again.
To import the key just load the file and import the private or/and public key.
window.crypto.subtle.importKey(
"jwk",
{
kty: myKetPubOrPrivateFromJson.kty,
crv: myKetPubOrPrivateFromJson.crv,
x: myKetPubOrPrivateFromJson.x,
y: myKetPubOrPrivateFromJson.y,
ext: myKetPubOrPrivateFromJson.ext,
},
{
name: "ECDSA",
namedCurve: "P-256", // i think you can change it by myKetPubOrPrivateFromJson.crv not sure about that
},
false, // <== it's useless to be able to export the key again
myKetPubOrPrivateFromJson.key_ops
)
.then(function(publicKey){
//returns a publicKey (or privateKey if you are importing a private key)
console.log(publicKey);
})
.catch(function(err){
console.error(err);
});
It's also possible to use a wrap/unwrap function however it doesn't seem to be possible to use it with ECDSA and ECDH key but here is a quick and DIRTY example (live):
function str2Buffer(data) {
const utf8Str = decodeURI(encodeURIComponent(data));
const len = utf8Str.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = utf8Str.charCodeAt(i);
}
return arr.buffer;
}
function buffer2Hex(buffer) {
return Array.from(new Uint8Array(buffer)).map(b => ('00' + b.toString(16)).slice(-2)).join('');
}
function hex2Buffer(data) {
if (data.length % 2 === 0) {
const bytes = [];
for (let i = 0; i < data.length; i += 2) {
bytes.push(parseInt(data.substr(i, 2), 16));
}
return new Uint8Array(bytes).buffer;
} elseĀ {
throw new Error('Wrong string format');
}
}
function createAesKey(password, salt) {
const passwordBuf = typeof password === 'string' ? str2Buffer(password) : password;
return window.crypto.subtle.importKey(
'raw',
passwordBuf,
'PBKDF2',
false,
['deriveKey', 'deriveBits']
).then(derivedKey =>
window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: str2Buffer(salt),
iterations: 1000,
hash: { name: 'SHA-512' }
},
derivedKey,
{name: 'AES-CBC', length: 256},
false,
['wrapKey', 'unwrapKey']
)
);
}
function genKeyPair() {
return window.crypto.subtle.generateKey(
{
name: "RSA-PSS",
modulusLength: 2048, //can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
true, // <== Here exportable
["sign", "verify"] // usage
)
}
function exportKey(keyToWrap, wrappingKey) {
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const promise = new Promise(function(resolve, reject) {
window.crypto.subtle.wrapKey(
"jwk",
keyToWrap, //the key you want to wrap, must be able to export to above format
wrappingKey, //the AES-CBC key with "wrapKey" usage flag
{ //these are the wrapping key's algorithm options
name: "AES-CBC",
//Don't re-use initialization vectors!
//Always generate a new iv every time your encrypt!
iv: iv,
}
).then(result => {
const wrap = { key: buffer2Hex(result), iv: buffer2Hex(iv) };
resolve(wrap);
});
});
return promise;
}
function importKey(key, unwrappingKey, iv, usages) {
return window.crypto.subtle.unwrapKey(
"jwk",
key, //the key you want to unwrap
unwrappingKey, //the AES-CBC key with "unwrapKey" usage flag
{ //these are the wrapping key's algorithm options
name: "AES-CBC",
iv: iv, //The initialization vector you used to encrypt
},
{ //this what you want the wrapped key to become (same as when wrapping)
name: "RSA-PSS",
modulusLength: 2048, //can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
false, //whether the key is extractable (i.e. can be used in exportKey)
usages //the usages you want the unwrapped key to have
);
}
createAesKey("password", "usernameassalt").then(aesKey => {
genKeyPair().then(keyPair => {
exportKey(keyPair.publicKey, aesKey)
.then(publicKey => {
exportKey(keyPair.privateKey, aesKey)
.then(privateKey => {
const exportKeys = {publicKey: publicKey, privateKey: privateKey };
appDiv.innerHTML = `AesKey = ${aesKey}<br />
KeyPair: <ul>
<li>publicKey: ${keyPair.publicKey}</li><li>privateKey: ${keyPair.privateKey}</li>
</ul>
Exported: <ul>
<li>publicKey:
<ul>
<li>key: ${exportKeys.publicKey.key}</li>
<li>iv: ${exportKeys.publicKey.iv}</li>
</ul>
</li>
<li>privateKey:
<ul>
<li>key: ${exportKeys.privateKey.key}</li>
<li>iv: ${exportKeys.privateKey.iv}</li>
</ul>
</li>
<ul>`;
importKey(hex2Buffer(exportKeys.privateKey.key), aesKey, hex2Buffer(exportKeys.privateKey.iv), ["sign"]).then(key => console.log(key)).catch(error => console.log(error.message));
});
});
});
});