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

瀏覽器相容性

另見