main
1import Base64 from "../base64.js";
2import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
3
4export 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}