-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathweb-crypto.ts
305 lines (281 loc) · 8.67 KB
/
web-crypto.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
type Signature = ArrayBuffer;
type Data = ArrayBuffer;
// Configuration for IndexedDb for local key store
export const IDBConfig = {
DATABASE_NAME: "KeyDb",
OBJECT_STORE_NAME: "KeyObjectStore",
VERSION: 1,
KEY_ID: 1,
};
interface CryptoConfig {
name: string;
modulusLength: 1024 | 2048 | 4096;
publicExponent: Uint8Array;
hash: "SHA-1" | "SHA-256" | "SHA-384" | "SHA-512";
extractable: false;
keyUsages: KeyUsage[];
}
// Configuration for WebCrypto
export const CryptoConfig: CryptoConfig = {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 4096,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: "SHA-384",
extractable: false,
keyUsages: ["sign", "verify"],
};
/**
* Returns a new non-exportable key pair for signing and verifying.
* @returns {Promise<CryptoKeyPair>}
*/
function makeKeys(): Promise<CryptoKeyPair> {
return window.crypto.subtle.generateKey(
{
name: CryptoConfig.name,
modulusLength: CryptoConfig.modulusLength,
publicExponent: CryptoConfig.publicExponent,
hash: CryptoConfig.hash,
},
CryptoConfig.extractable, //whether the key is extractable (i.e. can be used in exportKey)
CryptoConfig.keyUsages
);
}
/**
* Takes a payload and signs it with a key pair. Returns the signature only, not the payload.
* @param data Data to sign
* @param keys Key pair to sign with
* @returns signature
*/
function sign(data: Data, key: CryptoKey): Promise<Signature> {
return window.crypto.subtle.sign(
{
name: CryptoConfig.name,
hash: CryptoConfig.hash,
},
key, //from generateKey or importKey above
data //ArrayBuffer of data you want to sign
);
}
/**
* Given data, a key pair, and a signature, verifies that the signature is valid for the data.
* @param data Payload to verify
* @param keys Public key to verify with
* @param signature Signature to verify
* @returns true if data was signed with signature is valid, false otherwise
*/
function verify(
data: Data,
key: CryptoKey,
signature: Signature
): Promise<boolean> {
return window.crypto.subtle.verify(
{
name: CryptoConfig.name,
hash: CryptoConfig.hash, //the length of the salt
},
key, //from generateKey or importKey above
signature, //ArrayBuffer of the signature
data //ArrayBuffer of the data
);
}
function openDb() {
const indexedDB = window.indexedDB;
// Open (or create) the database
const open = indexedDB.open(IDBConfig.DATABASE_NAME, IDBConfig.VERSION);
// Create the schema
open.onupgradeneeded = function () {
const db = open.result;
db.createObjectStore(IDBConfig.OBJECT_STORE_NAME, {
keyPath: "id",
});
};
return open;
}
/**
* Gets the public key from the local key store. If the key does not exist, creates a new key pair
* @returns Public key as JsonWebKey
*/
export async function getPublicKey(): Promise<JsonWebKey> {
return new Promise((resolve, reject) => {
// Open (or create) the database
const open = openDb();
open.onsuccess = function () {
// Start a new transaction
const db = open.result;
const tx = db.transaction(IDBConfig.OBJECT_STORE_NAME, "readwrite");
const store = tx.objectStore(IDBConfig.OBJECT_STORE_NAME);
const getKeys = store.get(IDBConfig.KEY_ID) as IDBRequest<{
id: number;
keys: CryptoKeyPair;
}>;
getKeys.onsuccess = async function () {
try {
const keys = await getKeysFromDbOrCreate(getKeys, db);
const publicKey = await window.crypto.subtle.exportKey(
"jwk",
keys.publicKey
);
resolve(publicKey);
} catch (e) {
reject(e);
}
};
};
});
}
/**
* Signs a payload with a key pair that is stored in browser indexedDb.
* If key pair does not exist, creates a new key pair using WebCrypto
* and stores the keys in indexedDb.
* @param data payload to sign
* @returns Signature as ArrayBuffer
*/
export async function signPayload(data: Data): Promise<Signature> {
return new Promise((resolve, reject) => {
// Open (or create) the database
const open = openDb();
open.onsuccess = function () {
// Start a new transaction
const db = open.result;
const tx = db.transaction(IDBConfig.OBJECT_STORE_NAME, "readwrite");
const store = tx.objectStore(IDBConfig.OBJECT_STORE_NAME);
const getKeys = store.get(IDBConfig.KEY_ID) as IDBRequest<{
id: number;
keys: CryptoKeyPair;
}>;
getKeys.onsuccess = async function () {
try {
const keys = await getKeysFromDbOrCreate(getKeys, db);
const signature = await sign(data, keys.privateKey);
resolve(signature);
} catch (e) {
reject(e);
}
};
getKeys.onerror = async function () {
reject(getKeys.error);
};
};
});
}
// function to get keys or create them if they don't exist
async function getKeysFromDbOrCreate(
keyRequest: IDBRequest<{
id: number;
keys: CryptoKeyPair;
}>,
db: IDBDatabase
): Promise<CryptoKeyPair> {
let keys = keyRequest.result?.keys;
if (!keys) {
// Keys do not exist, create new keys
keys = await makeKeys();
if (makeKeys() === undefined) {
throw new Error(
"Could not create keys - Your browser does not support WebCrypto or you are running without SSL enabled."
);
}
// Create new transaction to store new keys, as the previous one is closed
const tx = db.transaction(IDBConfig.OBJECT_STORE_NAME, "readwrite");
const newTxStore = tx.objectStore(IDBConfig.OBJECT_STORE_NAME);
newTxStore.put({ id: 1, keys });
}
return keys;
}
/**
* Verifies that a payload was signed with signature using a key pair that is stored in browser indexedDb.
* @param data payload to verify
* @param signature signature used to sign payload
* @returns true if data was signed with signature is valid, false otherwise
*/
export function verifyPayload(
data: Data,
signature: Signature
): Promise<boolean> {
return new Promise((resolve, reject) => {
// Open (or create) the database
const open = openDb();
open.onsuccess = function () {
// Start a new transaction
const db = open.result;
const tx = db.transaction(IDBConfig.OBJECT_STORE_NAME, "readwrite");
const store = tx.objectStore(IDBConfig.OBJECT_STORE_NAME);
const getKeys = store.get(IDBConfig.KEY_ID) as IDBRequest<{
id: number;
keys: CryptoKeyPair;
}>;
getKeys.onsuccess = async function () {
const key = getKeys.result.keys.publicKey;
const isValid = await verify(data, key, signature);
resolve(isValid);
};
getKeys.onerror = async function () {
reject(getKeys.error);
};
};
});
}
// Helpers
/**
* Convert a normal string to a Base64Url string - removes chars that are not allowed in a url
* @param data
* @returns
*/
export function convertStringToBase64UrlString(data: string): string {
return arrayBufferToBase64UrlString(stringToArrayBuffer(data));
}
/**
* Converts an ArrayBuffer to a base64url string
* @param arrayBuffer ArrayBuffer to convert
* @returns base64 string
*/
export function arrayBufferToBase64UrlString(arrayBuffer: ArrayBuffer): string {
const byteArray = new Uint8Array(arrayBuffer);
let byteString = "";
byteArray.forEach((byte) => {
byteString += String.fromCharCode(byte);
});
return btoa(byteString)
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
/**
* Converts a string already in base64url format to an ArrayBuffer
* @param base64UrlString base64 string
* @returns ArrayBuffer
*/
export function base64UrlStringToArrayBuffer(
base64UrlString: string
): ArrayBuffer {
let base64Formatted = base64UrlString
.replace(/-/g, "+")
.replace(/_/g, "/")
.replace(/\s/g, "");
// Pad out with standard base64 required padding characters
const pad = base64Formatted.length % 4;
if (pad) {
if (pad === 1) {
throw new Error(
"InvalidLengthError: Input base64url string is the wrong length to determine padding"
);
}
base64Formatted += new Array(5 - pad).join("=");
}
const binary_string = atob(base64Formatted);
const len = binary_string.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary_string.charCodeAt(i);
}
return bytes.buffer;
}
/**
* Converts a string to base64 and returns an ArrayBuffer representation. Important because we need a binary representation of the string to sign it.
* @param str string
* @returns ArrayBuffer of base64 string
*/
export function stringToArrayBuffer(str: string): ArrayBuffer {
const base64Encoded = btoa(str);
return base64UrlStringToArrayBuffer(base64Encoded);
}