/**
* Create a new passkey and get a pseudo-random value produced from the passkey PRF extension.
* @see {@link https://www.w3.org/TR/webauthn-3/#prf-extension}
- * @param {ArrayBuffer|ArrayBufferView<ArrayBufferLike>} challenge A cryptographic challenge for the passkey.
* @returns {Promise<ArrayBuffer&{byteLength:32}>} A 32 byte long `ArrayBuffer` containing a pseudo-random
* value produced from the passkey PRF extension.
*/
-async function createPrf(challenge) {
+async function createPrf() {
return navigator.credentials.create({
publicKey: {
rp: { name: '' },
user: { id: new ArrayBuffer(0), name: '', displayName: '' },
pubKeyCredParams: [{ type: 'public-key', alg: -7 }],
extensions: { prf: { eval: { first: new ArrayBuffer(0) } } },
- challenge
+ challenge: new ArrayBuffer(0)
}
}).then(c => c.getClientExtensionResults().prf.results.first);
}
* Request a "sign in" with a passkey created previously, if it exists. This function will return the same
* pseudo-random value produced during the passkey creation function.
* @see {@link https://www.w3.org/TR/webauthn-3/#prf-extension}
- * @param {ArrayBuffer|ArrayBufferView<ArrayBufferLike>} challenge A cryptographic challenge for the passkey.
* @returns {Promise<ArrayBuffer&{byteLength:32}>} A 32 byte long `ArrayBuffer` containing a pseudo-random
* value produced from the passkey PRF extension.
*/
-async function getPrf(challenge) {
+async function getPrf() {
return navigator.credentials.get({
publicKey: {
extensions: { prf: { eval: { first: new ArrayBuffer(0) } } },
- challenge
+ challenge: new ArrayBuffer(0)
}
}).then(c => c.getClientExtensionResults().prf.results.first);
}
/**
* Encrypt a `string` by creating a new passkey then using the passkey's PRF extension to generate a
- * HKDF key and encrypt the `string`. The `challenge`, `salt`, and `iv` values are embedded (in plaintext)
+ * HKDF key and encrypt the `string`. The `salt` and `iv` values are embedded (in plaintext)
* alongside the ciphertext.
* @param {string} plaintext An unencrypted value to encrypt using a new passkey.
- * @returns {Promise<string>} The encrypted plaintext (i.e., ciphertext) with `challenge`, `salt`, and
- * `iv` values in base64.
+ * @returns {Promise<string>} The encrypted plaintext (i.e., ciphertext) with `salt` and `iv` values
+ * in base64.
*/
export async function encrypt(plaintext) {
// Generate all required random values
- const challenge = crypto.getRandomValues(new Uint8Array(64)); // For passkey
const salt = crypto.getRandomValues(new Uint8Array(64)); // For HKDF
const iv = crypto.getRandomValues(new Uint8Array(12)); // For AES-GCM
const encodedPlaintext = encoder.encode(plaintext);
const info = encoder.encode('https://github.com/ardislu/static-encrypt-passkey');
- const prf = await createPrf(challenge);
+ const prf = await createPrf();
const key = await getKey(prf, salt, info);
const ciphertext = new Uint8Array(await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
encodedPlaintext
));
- const buffer = Uint8Array.from([...challenge, ...salt, ...iv, ...ciphertext]);
+ const buffer = Uint8Array.from([...salt, ...iv, ...ciphertext]);
const content = btoa(String.fromCharCode(...buffer));
return content;
}
/**
- * Decrypt a `string` that has been encrypted with a passkey. Assuming the required `challenge`, `salt`, and
- * `iv` values are embedded alongside the ciphertext.
+ * Decrypt a `string` that has been encrypted with a passkey. Assuming the required `salt` and `iv`
+ * values are embedded alongside the ciphertext.
* @param {string} content A base64-encoded ciphertext and required cryptographic values.
* @returns {Promise<string>} The decrypted plaintext.
* @throws {OperationError} Decryption operation failed.
export async function decrypt(content) {
// Extract random values
const buffer = Uint8Array.from(atob(content), c => c.charCodeAt(0));
- const challenge = buffer.slice(0, 64); // For passkey
- const salt = buffer.slice(64, 128); // For HKDF
- const iv = buffer.slice(128, 140); // For AES-GCM
- const ciphertext = buffer.slice(140);
+ const salt = buffer.slice(0, 64); // For HKDF
+ const iv = buffer.slice(64, 76); // For AES-GCM
+ const ciphertext = buffer.slice(76);
// Prepare non-crypto inputs
const info = new TextEncoder().encode('https://github.com/ardislu/static-encrypt-passkey');
- const prf = await getPrf(challenge);
+ const prf = await getPrf();
const key = await getKey(prf, salt, info);
const encodedPlaintext = new Uint8Array(await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },