Skip to content

Commit 981f03b

Browse files
committed
Provide extracted key with encryption
1 parent e4fcf4c commit 981f03b

File tree

1 file changed

+116
-13
lines changed

1 file changed

+116
-13
lines changed

src/index.ts

Lines changed: 116 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,69 @@
1+
interface DetailedEncryptionResult {
2+
vault: string;
3+
exportedKeyString: string;
4+
}
5+
16
interface EncryptionResult {
27
data: string;
38
iv: string;
49
salt?: string;
510
}
611

12+
interface DetailedDecryptResult {
13+
exportedKeyString: string;
14+
vault: unknown;
15+
salt: string;
16+
}
17+
18+
const EXPORT_FORMAT = 'jwk';
19+
const DERIVED_KEY_FORMAT = 'AES-GCM';
20+
const STRING_ENCODING = 'utf-8';
21+
722
/**
823
* Encrypts a data object that can be any serializable value using
924
* a provided password.
1025
*
1126
* @param {string} password - password to use for encryption
1227
* @param {R} dataObj - data to encrypt
28+
* @param {CryptoKey} key - a CryptoKey instance
29+
* @param {string} salt - salt used to encrypt
1330
* @returns {Promise<string>} cypher text
1431
*/
1532
export async function encrypt<R>(
1633
password: string,
1734
dataObj: R,
35+
key?: CryptoKey,
36+
salt: string = generateSalt(),
1837
): Promise<string> {
19-
const salt = generateSalt();
20-
21-
const passwordDerivedKey = await keyFromPassword(password, salt);
22-
const payload = await encryptWithKey(passwordDerivedKey, dataObj);
38+
const cryptoKey = key || (await keyFromPassword(password, salt));
39+
const payload = await encryptWithKey(cryptoKey, dataObj);
2340
payload.salt = salt;
2441
return JSON.stringify(payload);
2542
}
2643

44+
/**
45+
* Encrypts a data object that can be any serializable value using
46+
* a provided password.
47+
*
48+
* @param {string} password - password to use for encryption
49+
* @param {R} dataObj - data to encrypt
50+
* @returns {Promise<DetailedEncryptionResult>} object with vault and exportedKeyString
51+
*/
52+
export async function encryptWithDetail<R>(
53+
password: string,
54+
dataObj: R,
55+
): Promise<DetailedEncryptionResult> {
56+
const salt = generateSalt();
57+
const key = await keyFromPassword(password, salt);
58+
const exportedKeyString = await exportKey(key);
59+
const vault = await encrypt(password, dataObj, key, salt);
60+
61+
return {
62+
vault,
63+
exportedKeyString,
64+
};
65+
}
66+
2767
/**
2868
* Encrypts the provided serializable javascript object using the
2969
* provided CryptoKey and returns an object containing the cypher text and
@@ -37,12 +77,12 @@ export async function encryptWithKey<R>(
3777
dataObj: R,
3878
): Promise<EncryptionResult> {
3979
const data = JSON.stringify(dataObj);
40-
const dataBuffer = Buffer.from(data, 'utf-8');
80+
const dataBuffer = Buffer.from(data, STRING_ENCODING);
4181
const vector = global.crypto.getRandomValues(new Uint8Array(16));
4282

4383
const buf = await global.crypto.subtle.encrypt(
4484
{
45-
name: 'AES-GCM',
85+
name: DERIVED_KEY_FORMAT,
4686
iv: vector,
4787
},
4888
key,
@@ -63,12 +103,45 @@ export async function encryptWithKey<R>(
63103
* the resulting value
64104
* @param {string} password - password to decrypt with
65105
* @param {string} text - cypher text to decrypt
106+
* @param {CryptoKey} key - a key to use for decrypting
107+
* @returns {object}
66108
*/
67-
export async function decrypt<R>(password: string, text: string): Promise<R> {
109+
export async function decrypt(
110+
password: string,
111+
text: string,
112+
key?: CryptoKey,
113+
): Promise<unknown> {
114+
const payload = JSON.parse(text);
115+
const { salt } = payload;
116+
117+
const cryptoKey = key || (await keyFromPassword(password, salt));
118+
119+
const result = await decryptWithKey(cryptoKey, payload);
120+
return result;
121+
}
122+
123+
/**
124+
* Given a password and a cypher text, decrypts the text and returns
125+
* the resulting value, keyString, and salt
126+
* @param {string} password - password to decrypt with
127+
* @param {string} text - cypher text to decrypt
128+
* @returns {object}
129+
*/
130+
export async function decryptWithDetail(
131+
password: string,
132+
text: string,
133+
): Promise<DetailedDecryptResult> {
68134
const payload = JSON.parse(text);
69135
const { salt } = payload;
70136
const key = await keyFromPassword(password, salt);
71-
return await decryptWithKey(key, payload);
137+
const exportedKeyString = await exportKey(key);
138+
const vault = await decrypt(password, text, key);
139+
140+
return {
141+
exportedKeyString,
142+
vault,
143+
salt,
144+
};
72145
}
73146

74147
/**
@@ -87,13 +160,13 @@ export async function decryptWithKey<R>(
87160
let decryptedObj;
88161
try {
89162
const result = await crypto.subtle.decrypt(
90-
{ name: 'AES-GCM', iv: vector },
163+
{ name: DERIVED_KEY_FORMAT, iv: vector },
91164
key,
92165
encryptedData,
93166
);
94167

95168
const decryptedData = new Uint8Array(result);
96-
const decryptedStr = Buffer.from(decryptedData).toString('utf-8');
169+
const decryptedStr = Buffer.from(decryptedData).toString(STRING_ENCODING);
97170
decryptedObj = JSON.parse(decryptedStr);
98171
} catch (e) {
99172
throw new Error('Incorrect password');
@@ -102,6 +175,36 @@ export async function decryptWithKey<R>(
102175
return decryptedObj;
103176
}
104177

178+
/**
179+
* Receives an exported CryptoKey string and creates a key
180+
* @param {string} keyString - keyString to import
181+
* @returns {CryptoKey}
182+
*/
183+
export async function createKeyFromString(
184+
keyString: string,
185+
): Promise<CryptoKey> {
186+
const key = await window.crypto.subtle.importKey(
187+
EXPORT_FORMAT,
188+
JSON.parse(keyString),
189+
DERIVED_KEY_FORMAT,
190+
true,
191+
['encrypt', 'decrypt'],
192+
);
193+
194+
return key;
195+
}
196+
197+
/**
198+
* Receives an exported CryptoKey string, creates a key,
199+
* and decrypts cipher text with the reconstructed key
200+
* @param {CryptoKey} key - key to export
201+
* @returns {string}
202+
*/
203+
async function exportKey(key: CryptoKey): Promise<string> {
204+
const exportedKey = await window.crypto.subtle.exportKey(EXPORT_FORMAT, key);
205+
return JSON.stringify(exportedKey);
206+
}
207+
105208
/**
106209
* Generate a CryptoKey from a password and random salt
107210
* @param {string} password - The password to use to generate key
@@ -111,7 +214,7 @@ export async function keyFromPassword(
111214
password: string,
112215
salt: string,
113216
): Promise<CryptoKey> {
114-
const passBuffer = Buffer.from(password, 'utf-8');
217+
const passBuffer = Buffer.from(password, STRING_ENCODING);
115218
const saltBuffer = Buffer.from(salt, 'base64');
116219

117220
const key = await global.crypto.subtle.importKey(
@@ -130,8 +233,8 @@ export async function keyFromPassword(
130233
hash: 'SHA-256',
131234
},
132235
key,
133-
{ name: 'AES-GCM', length: 256 },
134-
false,
236+
{ name: DERIVED_KEY_FORMAT, length: 256 },
237+
true,
135238
['encrypt', 'decrypt'],
136239
);
137240

0 commit comments

Comments
 (0)