SubtleCrypto: deriveKey() 方法

基线 广泛可用

此功能已得到很好的建立,并且可以在许多设备和浏览器版本中使用。它自以下时间起在浏览器中可用 2015 年 7 月.

安全上下文:此功能仅在安全上下文(HTTPS)中可用,在某些或所有支持的浏览器中。

SubtleCrypto 接口的deriveKey() 方法可用于从主密钥派生密钥。

它将一些初始密钥材料、要使用的派生算法以及要派生的密钥所需属性作为参数。它返回一个Promise,该 Promise 将以表示新密钥的CryptoKey 对象来完成。

值得注意的是,支持的密钥派生算法具有截然不同的特性,并且适用于截然不同的情况。有关更多详细信息,请参阅支持的算法

语法

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

参数

algorithm

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

baseKey

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

derivedKeyAlgorithm

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

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(椭圆曲线 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 上查看完整代码。

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

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 的密钥对和共享密钥。它还为“加密”按钮添加了一个点击处理程序,该处理程序将触发对第一个 <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 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 上查看完整代码。

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: 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 的浏览器中加载。

另请参阅