SubtleCrypto: deriveKey() 方法
Baseline 广泛可用 *
注意:此功能在 Web Workers 中可用。
deriveKey() 方法是 SubtleCrypto 接口的一个方法,用于从主密钥派生出密钥。
它接收一些初始密钥材料、要使用的派生算法以及派生密钥所需的属性作为参数。它返回一个 Promise,该 Promise 将以一个代表新密钥的 CryptoKey 对象的形式得到满足。
值得注意的是,支持的密钥派生算法具有非常不同的特性,适用于各种不同的场景。有关详细信息,请参阅 支持的算法。
语法
deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages)
参数
algorithm-
一个定义要使用的派生算法的对象。
- 要使用 ECDH,请传递一个
EcdhKeyDeriveParams对象,并将name属性指定为字符串ECDH。 - 要使用 HKDF,请传递一个
HkdfParams对象。 - 要使用 PBKDF2,请传递一个
Pbkdf2Params对象。 - 要使用 X25519,请传递一个
EcdhKeyDeriveParams对象,并将name属性指定为字符串X25519。
- 要使用 ECDH,请传递一个
baseKey-
一个表示派生算法输入的
CryptoKey。如果algorithm是 ECDH 或 X25519,那么它将是 ECDH 或 X25519 私钥。否则,它将是派生函数的初始密钥材料:例如,对于 PBKDF2,它可能是密码,使用SubtleCrypto.importKey()导入为CryptoKey。 derivedKeyType-
一个定义派生密钥将用于的算法的对象
- 对于 HMAC,请传递一个
HmacImportParams对象。 - 对于 AES-CTR、AES-CBC、AES-GCM 或 AES-KW,请传递一个
AesDerivedKeyParams对象。
- 对于 HMAC,请传递一个
extractable-
一个布尔值,指示是否可以使用
SubtleCrypto.exportKey()或SubtleCrypto.wrapKey()导出密钥。 keyUsages-
一个
Array,指示可以使用派生密钥执行的操作。请注意,密钥用法必须由derivedKeyAlgorithm中设置的算法允许。数组的可能值为:
返回值
异常
当遇到以下任一异常时,Promise 会被拒绝:
InvalidAccessErrorDOMException-
当主密钥不是请求的派生算法的密钥,或者该密钥的
keyUsages值不包含deriveKey时抛出。 NotSupportedDOMException-
尝试使用未知或不适合派生的算法时抛出,或者当派生密钥请求的算法未定义密钥长度时抛出。
SyntaxErrorDOMException-
当
keyUsages为空,但解封装的密钥类型为secret或private时抛出。
支持的算法
deriveKey() 支持的算法具有非常不同的特性,适用于不同的场景。
密钥派生算法
HKDF
HKDF 是一种*密钥派生函数*。它设计用于从高熵输入(例如 ECDH 密钥协商操作的输出)派生密钥材料。
它*不*适用于从低熵输入(例如密码)派生密钥。为此,请使用 PBKDF2。
HKDF 在 RFC 5869 中有规定。
PBKDF2
PBKDF2 也是一种*密钥派生函数*。它设计用于从相对低熵输入(例如密码)派生密钥材料。它通过将 HMAC 等函数应用于输入密码以及一些盐值,并重复此过程多次来派生密钥材料。过程重复的次数越多,密钥派生的计算成本就越高:这使得攻击者更难通过字典攻击来破解密钥。
PBKDF2 在 RFC 2898 中有规定。
密钥协商算法
ECDH
ECDH(椭圆曲线迪菲-赫尔曼)是一种*密钥协商算法*。它使两个人(每个人都拥有一个 ECDH 公钥/私钥对)能够生成共享密钥:即他们——且只有他们——共享的密钥。然后,他们可以将此共享密钥用作对称密钥来保护通信,或者将此密钥用作派生此类密钥的输入(例如,使用 HKDF 算法)。
ECDH 在 RFC 6090 中有规定。
X25519
X25519 是一种密钥协商算法,类似于 ECDH,但基于 Curve25519 椭圆曲线,这是 RFC 8032 中定义的爱德华兹曲线数字签名算法 (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 aliceKeyPair = await window.crypto.subtle.generateKey(
{
name: "ECDH",
namedCurve: "P-384",
},
false,
["deriveKey"],
);
let bobKeyPair = 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 aliceSecretKey = await deriveSecretKey(
aliceKeyPair.privateKey,
bobKeyPair.publicKey,
);
// Bob generates the same secret key using his private key and Alice's public key.
let bobSecretKey = await deriveSecretKey(
bobKeyPair.privateKey,
aliceKeyPair.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(aliceSecretKey);
});
// Bob can use his copy to decrypt the message.
let decryptButton = document.querySelector(".ecdh .decrypt-button");
decryptButton.addEventListener("click", () => {
decrypt(bobSecretKey);
});
}
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 的密钥对和共享密钥。它还为“Encrypt”按钮添加了一个点击事件处理程序,该处理程序将触发对第一个 <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 aliceKeyPair = await window.crypto.subtle.generateKey(
{
name: "X25519",
},
false,
["deriveKey"],
);
log(
`Created Alice's key pair: (algorithm: ${JSON.stringify(
aliceKeyPair.privateKey.algorithm,
)}, usages: ${aliceKeyPair.privateKey.usages})`,
);
const bobKeyPair = await window.crypto.subtle.generateKey(
{
name: "X25519",
},
false,
["deriveKey"],
);
log(
`Created Bob's key pair: (algorithm: ${JSON.stringify(
bobKeyPair.privateKey.algorithm,
)}, usages: ${bobKeyPair.privateKey.usages})`,
);
// Alice then generates a secret key using her private key and Bob's public key.
const aliceSecretKey = await deriveSecretKey(
aliceKeyPair.privateKey,
bobKeyPair.publicKey,
);
log(
`aliceSecretKey: ${aliceSecretKey.type} (algorithm: ${JSON.stringify(
aliceSecretKey.algorithm,
)}, usages: ${aliceSecretKey.usages}), `,
);
// Bob generates the same secret key using his private key and Alice's public key.
const bobSecretKey = await deriveSecretKey(
bobKeyPair.privateKey,
aliceKeyPair.publicKey,
);
log(
`bobSecretKey: ${bobSecretKey.type} (algorithm: ${JSON.stringify(
bobSecretKey.algorithm,
)}, usages: ${bobSecretKey.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(
aliceSecretKey,
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(
bobSecretKey,
initializationVector,
encryptedMessage,
);
decryptedInput.value = decryptedCiphertext;
log(`decryptedCiphertext: ${decryptedCiphertext}\n`);
});
} catch (e) {
log(e);
}
}
// Finally we call the method to set the example running.
agreeSharedSecretKey();
结果
点击“Encrypt”按钮来加密顶部 <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,
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 加密级别 2 # SubtleCrypto-method-deriveKey |
浏览器兼容性
加载中…