SubtleCrypto: deriveKey() 方法
SubtleCrypto
接口的deriveKey()
方法可用于从主密钥派生密钥。
它将一些初始密钥材料、要使用的派生算法以及要派生的密钥所需属性作为参数。它返回一个Promise
,该 Promise 将以表示新密钥的CryptoKey
对象来完成。
值得注意的是,支持的密钥派生算法具有截然不同的特性,并且适用于截然不同的情况。有关更多详细信息,请参阅支持的算法。
语法
deriveKey(algorithm, baseKey, derivedKeyAlgorithm, extractable, keyUsages)
参数
algorithm
-
定义要使用的派生算法的对象。
- 要使用ECDH,请传递一个
EcdhKeyDeriveParams
对象,并将字符串ECDH
指定为name
属性。 - 要使用HKDF,请传递一个
HkdfParams
对象。 - 要使用X25519,请传递一个
EcdhKeyDeriveParams
对象,并将字符串X25519
指定为name
属性。
- 要使用ECDH,请传递一个
baseKey
-
表示派生算法输入的
CryptoKey
。如果algorithm
是 ECDH 或 X25519,则这将是 ECDH 或 X25519 私钥。否则,它将是派生函数的初始密钥材料:例如,对于 PBKDF2,它可能是密码,使用SubtleCrypto.importKey()
作为CryptoKey
导入。 derivedKeyAlgorithm
-
定义派生密钥将用于的算法的对象
- 对于HMAC,请传递一个
HmacKeyGenParams
对象。 - 对于AES-CTR、AES-CBC、AES-GCM 或AES-KW,请传递一个
AesKeyGenParams
对象。 - 对于HKDF,请传递一个
HkdfParams
对象。 - 对于PBKDF2,请传递一个
Pbkdf2Params
对象。
- 对于HMAC,请传递一个
extractable
-
一个布尔值,指示是否可以使用
SubtleCrypto.exportKey()
或SubtleCrypto.wrapKey()
导出密钥。 keyUsages
-
一个
Array
,指示可以使用派生密钥执行的操作。请注意,密钥用途必须由derivedKeyAlgorithm
中设置的算法允许。数组的可能值为
返回值
异常
当遇到以下异常之一时,promise 将被拒绝
InvalidAccessError
DOMException
-
当主密钥不是请求的派生算法的密钥,或者该密钥的
keyUsages
值不包含deriveKey
时引发。 NotSupported
DOMException
-
当尝试使用未知或不适合派生的算法,或者派生密钥请求的算法未定义密钥长度时引发。
SyntaxError
DOMException
-
当
keyUsages
为空但解包的密钥类型为secret
或private
时引发。
支持的算法
deriveKey()
支持的算法具有截然不同的特性,并且适用于不同的情况。
密钥派生算法
密钥协商算法
ECDH
ECDH(椭圆曲线 Diffie-Hellman)是一种密钥协商算法。它使两个分别拥有 ECDH 公钥/私钥对的人能够生成共享密钥:即他们(以及其他人)共享的密钥。然后,他们可以使用此共享密钥作为对称密钥来保护其通信,或者可以使用该密钥作为输入来派生此类密钥(例如,使用 HKDF 算法)。
ECDH 在RFC 6090 中指定。
X25519
X25519 是一种类似于 ECDH 的密钥协商算法,但构建在Curve25519 椭圆曲线之上,该曲线是RFC 8032 中定义的 Edwards-Curve 数字签名算法 (EdDSA) 系列算法的一部分。
Curve25519 算法广泛用于密码学,被认为是现有的一些最高效/最快的算法。与与 ECDH 一起使用的 NIST(国家标准与技术研究院)曲线密钥交换算法相比,Curve25519 更易于实现,并且其非政府起源意味着其设计选择背后的决策是透明和开放的。
X25519 在RFC 7748 中指定。
示例
注意:您可以在 GitHub 上尝试使用工作示例。
ECDH:派生共享密钥
在此示例中,Alice 和 Bob 分别生成 ECDH 密钥对,然后交换公钥。然后,他们使用 deriveKey()
派生共享的 AES 密钥,他们可以使用该密钥加密消息。在 GitHub 上查看完整代码。
/*
Derive an AES key, given:
- our ECDH private key
- their ECDH public key
*/
function deriveSecretKey(privateKey, publicKey) {
return window.crypto.subtle.deriveKey(
{
name: "ECDH",
public: publicKey,
},
privateKey,
{
name: "AES-GCM",
length: 256,
},
false,
["encrypt", "decrypt"],
);
}
async function agreeSharedSecretKey() {
// Generate 2 ECDH key pairs: one for Alice and one for Bob
// In more normal usage, they would generate their key pairs
// separately and exchange public keys securely
let alicesKeyPair = await window.crypto.subtle.generateKey(
{
name: "ECDH",
namedCurve: "P-384",
},
false,
["deriveKey"],
);
let bobsKeyPair = await window.crypto.subtle.generateKey(
{
name: "ECDH",
namedCurve: "P-384",
},
false,
["deriveKey"],
);
// Alice then generates a secret key using her private key and Bob's public key.
let alicesSecretKey = await deriveSecretKey(
alicesKeyPair.privateKey,
bobsKeyPair.publicKey,
);
// Bob generates the same secret key using his private key and Alice's public key.
let bobsSecretKey = await deriveSecretKey(
bobsKeyPair.privateKey,
alicesKeyPair.publicKey,
);
// Alice can then use her copy of the secret key to encrypt a message to Bob.
let encryptButton = document.querySelector(".ecdh .encrypt-button");
encryptButton.addEventListener("click", () => {
encrypt(alicesSecretKey);
});
// Bob can use his copy to decrypt the message.
let decryptButton = document.querySelector(".ecdh .decrypt-button");
decryptButton.addEventListener("click", () => {
decrypt(bobsSecretKey);
});
}
X25519:派生共享密钥
在此示例中,Alice 和 Bob 分别生成 X25519 密钥对,然后交换公钥。然后,他们各自使用 deriveKey()
从自己的私钥和对方的公钥派生共享的 AES 密钥。他们可以使用此共享密钥加密和解密他们交换的消息。
HTML
首先,我们定义一个 HTML <input>
,您将使用它输入“Alice”将发送的明文消息,以及一个可点击以启动加密过程的按钮。
<label for="message">Plaintext message from Alice (Enter):</label>
<input
type="text"
id="message"
name="message"
size="50"
value="The lion roars near dawn" />
<input id="encrypt-button" type="button" value="Encrypt" />
接下来是另外两个元素,用于在 Alice 使用其密钥副本加密明文后显示密文,以及在 Bob 使用其密钥副本解密密文后显示文本。
<div id="results">
<label for="encrypted">Encrypted (Alice)</label>
<input
type="text"
id="encrypted"
name="encrypted"
size="30"
value=""
readonly />
<label for="results">Decrypted (Bob)</label>
<input
type="text"
id="decrypted"
name="decrypted"
size="50"
value=""
readonly />
</div>
JavaScript
以下代码显示了我们如何使用 deriveKey()
。我们传入远程方的 X25519 公钥、本地方的 X25519 私钥,并指定派生密钥应为 AES-GCM 密钥。我们还将派生密钥设置为不可提取,并且适合加密和解密。
我们在代码的下方进一步使用此函数为 Bob 和 Alice 创建共享密钥。
/*
Derive an AES-GCM key, given:
- our X25519 private key
- their X25519 public key
*/
function deriveSecretKey(privateKey, publicKey) {
return window.crypto.subtle.deriveKey(
{
name: "X25519",
public: publicKey,
},
privateKey,
{
name: "AES-GCM",
length: 256,
},
false,
["encrypt", "decrypt"],
);
}
接下来,我们定义了 Alice 用于将明文消息进行 UTF-8 编码,然后加密,以及 Bob 用于解密和解码消息的函数。它们都将共享的 AES 密钥、初始化向量 和要加密或解密的文本作为参数。
加密和解密必须使用相同的初始化向量,但它不需要保密,因此通常会与加密消息一起发送。不过,在本例中,由于我们实际上并没有发送消息,因此我们直接将其提供。
async function encryptMessage(key, initializationVector, message) {
try {
const encoder = new TextEncoder();
encodedMessage = encoder.encode(message);
// iv will be needed for decryption
return await window.crypto.subtle.encrypt(
{ name: "AES-GCM", iv: initializationVector },
key,
encodedMessage,
);
} catch (e) {
console.log(e);
return `Encoding error`;
}
}
async function decryptMessage(key, initializationVector, ciphertext) {
try {
const decryptedText = await window.crypto.subtle.decrypt(
// The iv value must be the same as that used for encryption
{ name: "AES-GCM", iv: initializationVector },
key,
ciphertext,
);
const utf8Decoder = new TextDecoder();
return utf8Decoder.decode(decryptedText);
} catch (e) {
console.log(e);
return "Decryption error";
}
}
下面的 agreeSharedSecretKey()
函数在加载时被调用,以生成 Alice 和 Bob 的密钥对和共享密钥。它还为“加密”按钮添加了一个点击处理程序,该处理程序将触发对第一个 <input>
中定义的文本的加密和解密。请注意,所有代码都位于 try...catch
处理程序内,以确保我们可以在 X25519 算法不受支持而导致密钥生成失败的情况下记录该情况。
async function agreeSharedSecretKey() {
try {
// Generate 2 X25519 key pairs: one for Alice and one for Bob
// In more normal usage, they would generate their key pairs
// separately and exchange public keys securely
const alicesKeyPair = await window.crypto.subtle.generateKey(
{
name: "X25519",
},
false,
["deriveKey"],
);
log(
`Created Alices's key pair: (algorithm: ${JSON.stringify(
alicesKeyPair.privateKey.algorithm,
)}, usages: ${alicesKeyPair.privateKey.usages})`,
);
const bobsKeyPair = await window.crypto.subtle.generateKey(
{
name: "X25519",
},
false,
["deriveKey"],
);
log(
`Created Bob's key pair: (algorithm: ${JSON.stringify(
bobsKeyPair.privateKey.algorithm,
)}, usages: ${bobsKeyPair.privateKey.usages})`,
);
// Alice then generates a secret key using her private key and Bob's public key.
const alicesSecretKey = await deriveSecretKey(
alicesKeyPair.privateKey,
bobsKeyPair.publicKey,
);
log(
`alicesSecretKey: ${alicesSecretKey.type} (algorithm: ${JSON.stringify(
alicesSecretKey.algorithm,
)}, usages: ${alicesSecretKey.usages}), `,
);
// Bob generates the same secret key using his private key and Alice's public key.
const bobsSecretKey = await deriveSecretKey(
bobsKeyPair.privateKey,
alicesKeyPair.publicKey,
);
log(
`bobsSecretKey: ${bobsSecretKey.type} (algorithm: ${JSON.stringify(
bobsSecretKey.algorithm,
)}, usages: ${bobsSecretKey.usages}), \n`,
);
// Get access for the encrypt button and the three inputs
const encryptButton = document.querySelector("#encrypt-button");
const messageInput = document.querySelector("#message");
const encryptedInput = document.querySelector("#encrypted");
const decryptedInput = document.querySelector("#decrypted");
encryptButton.addEventListener("click", async () => {
log(`Plaintext: ${messageInput.value}`);
// Define the initialization vector used when encrypting and decrypting.
// This must be regenerated for every message!
const initializationVector = window.crypto.getRandomValues(
new Uint8Array(8),
);
// Alice can use her copy of the shared key to encrypt the message.
const encryptedMessage = await encryptMessage(
alicesSecretKey,
initializationVector,
messageInput.value,
);
// We then display part of the encrypted buffer and log the encrypted message
let buffer = new Uint8Array(encryptedMessage, 0, 5);
encryptedInput.value = `${buffer}...[${encryptedMessage.byteLength} bytes total]`;
log(
`encryptedMessage: ${buffer}...[${encryptedMessage.byteLength} bytes total]`,
);
// Bob uses his shared secret key to decrypt the message.
const decryptedCiphertext = await decryptMessage(
bobsSecretKey,
initializationVector,
encryptedMessage,
);
decryptedInput.value = decryptedCiphertext;
log(`decryptedCiphertext: ${decryptedCiphertext}\n`);
});
} catch (e) {
log(e);
}
}
// Finally we call the method to set the example running.
agreeSharedSecretKey();
结果
按下“加密”按钮,加密顶部 <input>
元素中的文本,并在接下来的两个元素中显示加密的密文和解密的密文。底部的日志区域提供了有关代码生成的密钥的信息。
PBKDF2:从密码派生 AES 密钥
在本例中,我们要求用户输入密码,然后使用它通过 PBKDF2 派生 AES 密钥,然后使用 AES 密钥加密消息。 在 GitHub 上查看完整代码。
/*
Get some key material to use as input to the deriveKey method.
The key material is a password supplied by the user.
*/
function getKeyMaterial() {
const password = window.prompt("Enter your password");
const enc = new TextEncoder();
return window.crypto.subtle.importKey(
"raw",
enc.encode(password),
"PBKDF2",
false,
["deriveBits", "deriveKey"],
);
}
async function encrypt(plaintext, salt, iv) {
const keyMaterial = await getKeyMaterial();
const key = await window.crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
return window.crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
}
HKDF:从共享密钥派生 AES 密钥
在本例中,我们加密给定共享密钥 secret
的消息 plainText
,该密钥本身可能已使用 ECDH 等算法派生。我们不直接使用共享密钥,而是将其作为 HKDF 函数的密钥材料,以派生 AES-GCM 加密密钥,然后使用该密钥加密消息。 在 GitHub 上查看完整代码。
/*
Given some key material and some random salt,
derive an AES-GCM key using HKDF.
*/
function getKey(keyMaterial, salt) {
return window.crypto.subtle.deriveKey(
{
name: "HKDF",
salt: salt,
info: new TextEncoder().encode("Encryption example"),
hash: "SHA-256",
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"],
);
}
async function encrypt(secret, plainText) {
const message = {
salt: window.crypto.getRandomValues(new Uint8Array(16)),
iv: window.crypto.getRandomValues(new Uint8Array(12)),
};
const key = await getKey(secret, message.salt);
message.ciphertext = await window.crypto.subtle.encrypt(
{
name: "AES-GCM",
iv: message.iv,
},
key,
plainText,
);
return message;
}
规范
规范 |
---|
Web Cryptography API # SubtleCrypto-method-deriveKey |
浏览器兼容性
BCD 表格仅在启用 JavaScript 的浏览器中加载。