前端RSA金鑰生成和加解密——window.crypto使用相關

charleschwang發表於2024-08-03

轉自簡書,原文地址,本文介紹window.crypto關於RSA方面的API。


crypto API支援常用的rsa、aes加解密,這邊介紹rsa的應用。

瀏覽器相容性

window.crypto需要chrome 37版本,ie 11,safari 11才支援全部API而基本的加解密在safari 7就可以。

生成公私鑰

crypto.subtle.generateKey(algorithm, extractable, keyUsages),其中:
1.algorithm引數根據不同演算法填入對應的引數對,rsa需要填入RsaHashedKeyGenParams物件包含有:

  • name,可選RSASSA-PKCS1-v1_5, RSA-PSS, or RSA-OAEP,這邊如果用於加解密是不支援舊的RSAES-PKCS1-v1_5的(jsencrypt.js支援),RSASSA-PKCS1-v1_5用於簽名

  • modulusLength,為rsa位數,推薦至少2048位(相當於112位的aes)才能較為安全,此引數最為影響效能,比如1024比2048快非常多,NIST建議目前的RSA秘鑰安全強度是2048位,如果需要工作到2030年之後,就使用3072位的秘鑰

  • publicExponent,一般直接為[0x01, 0x00, 0x01]

  • hash,摘要方式,可選SHA-256SHA-384SHA-512,這邊也允許SHA-1,但是因為其安全性所以基本不建議

2.extractable一般是true,表示是否允許以文字的方式匯出key

3.keyUsages是一個陣列,裡面可選encryptdecryptsign

window.crypto.subtle.generateKey(
    {
        name: "RSA-OAEP",
        modulusLength: 2048,
        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
        hash: {
            name: "SHA-512" // 這邊如果後端使用公鑰加密,要注意與前端一致
        },
    },
    true,
    ["encrypt", "decrypt"] // must be ["encrypt", "decrypt"] or ["wrapKey", "unwrapKey"]
)

函式結果返回一個promise物件,如果是對稱加密會得到一個金鑰CryptoKey型別,這邊rsa會得到一個金鑰對CryptoKeyPair,它有2個CryptoKey成員,privateKeypublicKey,我們匯出金鑰為文字或者加解密都將透過這2個成員物件。

匯出公私鑰

window.crypto.subtle.exportKey(format, key),其中:
1.format可選rawpkcs8spkijwk,我們這邊在匯出公鑰時選spki,私鑰選pkcs8

2.key就是上面CryptoKeyPairprivateKey或者publicKey
函式返回一個promise物件,結果是一個ArrayBuffer,這邊轉成pem風格。

// 匯出私鑰
 window.crypto.subtle.exportKey(
    "pkcs8", // 公鑰的話這邊填spki
    key.privateKey // 公鑰這邊是publicKey
).then(function(keydata2) {
    let privateKey = RSA2text(keydata1, 1) // 私鑰pem
}).catch(function(err) {
    console.error(err)
})
// pem格式文字
function RSA2text(buffer, isPrivate = 0) {
    let binary = ''
    const bytes = new Uint8Array(buffer)
    const len = bytes.byteLength
    for (let i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i])
    }
    const base64 = window.btoa(binary)
    let text = "-----BEGIN " + (isPrivate ? "PRIVATE" : "PUBLIC") + " KEY-----\n" // 這裡-----BEGIN和-----是固定的
    text += base64.replace(/[^\x00-\xff]/g, "$&\x01").replace(/.{64}\x01?/g, "$&\n") // 中間base64編碼
    text += "\n-----END " + (isPrivate ? "PRIVATE" : "PUBLIC") + " KEY-----" // 這裡-----END和-----是固定的
    return text
}

匯入公私鑰

window.crypto.subtle.importKey(
format,
keyData,
algorithm,
extractable,
keyUsages
)
,其中:
1.format可選rawpkcs8spkijwk,對應之前生成時的選擇,我們這邊在匯入公鑰時選spki,私鑰選pkcs8

2.keyData,即window.crypto.subtle.exportKey獲得的ArrayBuffer,由於在這裡時我們一般只有pem文字的,所以還需要做轉換成ArrayBuffer。

3.algorithm這邊我們是rsa,需要填入一個RsaHashedImportParams物件,這邊對應crypto.subtle.generateKey所需的RsaHashedKeyGenParams物件,含有:

  • name,都保持與之前一致
  • hash

4.extractablecrypto.subtle.generateKey

5.keyUsagescrypto.subtle.generateKey
函式返回一個promise物件,結果是一個CryptoKey

// 匯入公鑰
const pub = "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo5NwYVVSg6rmAIKoxvCI
4Rn7FYh0mOFrnr0q2+r99/ZGuYCj5b6FQ8BwaaU8XpRn/y3W7W2bCggNRwllWQ2r
dHIM+6vN2Yi/QYntKqbcRNlK1s02G2lw9pERaWi15+5P8+AFR8IHANm/Dd/19OlM
5FZ9hh+qG7FXFhV2i4r62pUZxhk6ykItOT16IH5poK9eEDhqsXZ+3UW6cGlxANgO
jHJEnZpNCI5tS/4kFhLogHvEd88MoapljL6cZXk3ZafvxgUwxI6BZIhlw0adh2sj
bByIHitjRxqKMDH7uSdV9zf8t5Wa0bZFcUpcb5Jx2QBWIlO1qP+Q4LLMbNvEHeBC
4wIDAQAB
-----END PUBLIC KEY-----"

const pemHeader = "-----BEGIN PUBLIC KEY-----" // 之前RSA2text函式里面的頭尾標識,這個是公鑰的
const pemFooter = "-----END PUBLIC KEY-----"
const pemContents = pub.substring(pemHeader.length, pub.length - pemFooter.length) // 去除pem頭尾
// base64解碼
const binaryDer = window.atob(pemContents)
// 轉為ArrayBuffer二進位制字串
const binary = str2ab(binaryDer)
window.crypto.subtle.importKey(
    "spki", // 這邊如果私鑰則是pkcs8
    binary , 
    {
        name: "RSA-OAEP",
        hash: "SHA-512" // 保持一致
    },
    true, 
    ["encrypt"] // 用於加密所以是公鑰,私鑰就是decrypt
)

function str2ab(str) {
    const buf = new ArrayBuffer(str.length)
    const bufView = new Uint8Array(buf)
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i)
    }
    return buf
}

加密解密

加密crypto.subtle.encrypt(algorithm, key, data),其中:
1.algorithm,加解密只支援RSA-OAEP不支援RSAES-PKCS1-v1_5

2.key即公鑰的CryptoKey物件

3.data是一個BufferSource物件,不能直接是要加密的字串。
結果是一個ArrayBuffer,可以使用window.btoa(String.fromCharCode(...new Uint8Array(e)))輸出為base64字串

const enc = new TextEncoder()
const data = enc.encode("sucks") // 這邊將要加密的字串轉為utf-8的Uint8Array
window.crypto.subtle.encrypt(
    {
        name: "RSA-OAEP"
    },
    publicKey, // 生成或者匯入的CryptoKey物件
    data
)

解密crypto.subtle.decrypt(algorithm, key, data),基本同加密,這邊data對應為加密返回的ArrayBuffer,如果是base64字串比如從後端加密過來的,就需要轉為Uint8Array。

function base64ToUint8Array(base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4)
    const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/')

    const rawData = window.atob(base64)
    const outputArray = new Uint8Array(rawData.length)

    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i)
    }
    return outputArray
}

返回值同加密

相關文章