SubtleCrypto: deriveKey() 方法

Baseline 广泛可用 *

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2020 年 1 月⁩ 起,所有主流浏览器均已支持。

* 此特性的某些部分可能存在不同级别的支持。

安全上下文: 此功能仅在安全上下文(HTTPS)中可用,且支持此功能的浏览器数量有限。

注意:此功能在 Web Workers 中可用。

deriveKey() 方法是 SubtleCrypto 接口的一个方法,用于从主密钥派生出密钥。

它接收一些初始密钥材料、要使用的派生算法以及派生密钥所需的属性作为参数。它返回一个 Promise,该 Promise 将以一个代表新密钥的 CryptoKey 对象的形式得到满足。

值得注意的是,支持的密钥派生算法具有非常不同的特性,适用于各种不同的场景。有关详细信息,请参阅 支持的算法

语法

js
deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages)

参数

algorithm

一个定义要使用的派生算法的对象。

baseKey

一个表示派生算法输入的 CryptoKey。如果 algorithm 是 ECDH 或 X25519,那么它将是 ECDH 或 X25519 私钥。否则,它将是派生函数的初始密钥材料:例如,对于 PBKDF2,它可能是密码,使用 SubtleCrypto.importKey() 导入为 CryptoKey

derivedKeyType

一个定义派生密钥将用于的算法的对象

extractable

一个布尔值,指示是否可以使用 SubtleCrypto.exportKey()SubtleCrypto.wrapKey() 导出密钥。

keyUsages

一个 Array,指示可以使用派生密钥执行的操作。请注意,密钥用法必须由 derivedKeyAlgorithm 中设置的算法允许。数组的可能值为:

  • encrypt:密钥可用于 加密消息。
  • decrypt:密钥可用于 解密消息。
  • sign:密钥可用于 签名消息。
  • verify:密钥可用于 验证签名。
  • deriveKey:密钥可用于派生新密钥。
  • deriveBits:密钥可用于 派生位
  • wrapKey:密钥可用于 封装密钥
  • unwrapKey:密钥可用于 解封装密钥

返回值

一个 Promise,成功时会解析为 CryptoKey

异常

当遇到以下任一异常时,Promise 会被拒绝:

InvalidAccessError DOMException

当主密钥不是请求的派生算法的密钥,或者该密钥的 keyUsages 值不包含 deriveKey 时抛出。

NotSupported DOMException

尝试使用未知或不适合派生的算法时抛出,或者当派生密钥请求的算法未定义密钥长度时抛出。

SyntaxError DOMException

keyUsages 为空,但解封装的密钥类型为 secretprivate 时抛出。

支持的算法

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 上的完整代码。

js
/*
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”将要发送的明文消息,以及一个按钮,您可以点击它来启动加密过程。

html
<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 使用他的密钥副本解密后的文本。

html
<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 创建共享密钥。

js
/*
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 密钥、一个 初始化向量以及要加密或解密的文本作为参数。

加密和解密必须使用相同的初始化向量,但它不需要是秘密的,因此通常会与加密消息一起发送。然而,在这种情况下,由于我们实际上并没有发送消息,所以我们直接提供它。

js
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 算法不受支持而导致密钥生成失败的情况。

js
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 上的完整代码。

js
/*
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 上的完整代码。

js
/*
  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

浏览器兼容性

另见