| 1 | import Base64 from "../base64.js"; |
| 2 | import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js"; |
| 3 | |
| 4 | export class RSACipher { |
| 5 | constructor() { |
| 6 | this._keyLength = 0; |
| 7 | this._keyBytes = 0; |
| 8 | this._n = null; |
| 9 | this._e = null; |
| 10 | this._d = null; |
| 11 | this._nBigInt = null; |
| 12 | this._eBigInt = null; |
| 13 | this._dBigInt = null; |
| 14 | this._extractable = false; |
| 15 | } |
| 16 | |
| 17 | get algorithm() { |
| 18 | return { name: "RSA-PKCS1-v1_5" }; |
| 19 | } |
| 20 | |
| 21 | _base64urlDecode(data) { |
| 22 | data = data.replace(/-/g, "+").replace(/_/g, "/"); |
| 23 | data = data.padEnd(Math.ceil(data.length / 4) * 4, "="); |
| 24 | return Base64.decode(data); |
| 25 | } |
| 26 | |
| 27 | _padArray(arr, length) { |
| 28 | const res = new Uint8Array(length); |
| 29 | res.set(arr, length - arr.length); |
| 30 | return res; |
| 31 | } |
| 32 | |
| 33 | static async generateKey(algorithm, extractable, _keyUsages) { |
| 34 | const cipher = new RSACipher; |
| 35 | await cipher._generateKey(algorithm, extractable); |
| 36 | return { privateKey: cipher }; |
| 37 | } |
| 38 | |
| 39 | async _generateKey(algorithm, extractable) { |
| 40 | this._keyLength = algorithm.modulusLength; |
| 41 | this._keyBytes = Math.ceil(this._keyLength / 8); |
| 42 | const key = await window.crypto.subtle.generateKey( |
| 43 | { |
| 44 | name: "RSA-OAEP", |
| 45 | modulusLength: algorithm.modulusLength, |
| 46 | publicExponent: algorithm.publicExponent, |
| 47 | hash: {name: "SHA-256"}, |
| 48 | }, |
| 49 | true, ["encrypt", "decrypt"]); |
| 50 | const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey); |
| 51 | this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes); |
| 52 | this._nBigInt = u8ArrayToBigInt(this._n); |
| 53 | this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes); |
| 54 | this._eBigInt = u8ArrayToBigInt(this._e); |
| 55 | this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes); |
| 56 | this._dBigInt = u8ArrayToBigInt(this._d); |
| 57 | this._extractable = extractable; |
| 58 | } |
| 59 | |
| 60 | static async importKey(key, _algorithm, extractable, keyUsages) { |
| 61 | if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") { |
| 62 | throw new Error("only support importing RSA public key"); |
| 63 | } |
| 64 | const cipher = new RSACipher; |
| 65 | await cipher._importKey(key, extractable); |
| 66 | return cipher; |
| 67 | } |
| 68 | |
| 69 | async _importKey(key, extractable) { |
| 70 | const n = key.n; |
| 71 | const e = key.e; |
| 72 | if (n.length !== e.length) { |
| 73 | throw new Error("the sizes of modulus and public exponent do not match"); |
| 74 | } |
| 75 | this._keyBytes = n.length; |
| 76 | this._keyLength = this._keyBytes * 8; |
| 77 | this._n = new Uint8Array(this._keyBytes); |
| 78 | this._e = new Uint8Array(this._keyBytes); |
| 79 | this._n.set(n); |
| 80 | this._e.set(e); |
| 81 | this._nBigInt = u8ArrayToBigInt(this._n); |
| 82 | this._eBigInt = u8ArrayToBigInt(this._e); |
| 83 | this._extractable = extractable; |
| 84 | } |
| 85 | |
| 86 | async encrypt(_algorithm, message) { |
| 87 | if (message.length > this._keyBytes - 11) { |
| 88 | return null; |
| 89 | } |
| 90 | const ps = new Uint8Array(this._keyBytes - message.length - 3); |
| 91 | window.crypto.getRandomValues(ps); |
| 92 | for (let i = 0; i < ps.length; i++) { |
| 93 | ps[i] = Math.floor(ps[i] * 254 / 255 + 1); |
| 94 | } |
| 95 | const em = new Uint8Array(this._keyBytes); |
| 96 | em[1] = 0x02; |
| 97 | em.set(ps, 2); |
| 98 | em.set(message, ps.length + 3); |
| 99 | const emBigInt = u8ArrayToBigInt(em); |
| 100 | const c = modPow(emBigInt, this._eBigInt, this._nBigInt); |
| 101 | return bigIntToU8Array(c, this._keyBytes); |
| 102 | } |
| 103 | |
| 104 | async decrypt(_algorithm, message) { |
| 105 | if (message.length !== this._keyBytes) { |
| 106 | return null; |
| 107 | } |
| 108 | const msgBigInt = u8ArrayToBigInt(message); |
| 109 | const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt); |
| 110 | const em = bigIntToU8Array(emBigInt, this._keyBytes); |
| 111 | if (em[0] !== 0x00 || em[1] !== 0x02) { |
| 112 | return null; |
| 113 | } |
| 114 | let i = 2; |
| 115 | for (; i < em.length; i++) { |
| 116 | if (em[i] === 0x00) { |
| 117 | break; |
| 118 | } |
| 119 | } |
| 120 | if (i === em.length) { |
| 121 | return null; |
| 122 | } |
| 123 | return em.slice(i + 1, em.length); |
| 124 | } |
| 125 | |
| 126 | async exportKey() { |
| 127 | if (!this._extractable) { |
| 128 | throw new Error("key is not extractable"); |
| 129 | } |
| 130 | return { n: this._n, e: this._e, d: this._d }; |
| 131 | } |
| 132 | } |