-
Notifications
You must be signed in to change notification settings - Fork 12
/
index.js
128 lines (110 loc) · 4.48 KB
/
index.js
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
var crypto = require('crypto');
var scmp = require('scmp');
// Arbitrary min length, nothing should shorter than this:
var MIN_KEY_LENGTH = 16;
function createEncryptor(opts) {
if( typeof(opts) == 'string' ) {
opts = {
key: opts,
hmac: true,
debug: false
};
}
var key = opts.key;
var verifyHmac = opts.hmac;
var debug = opts.debug;
var reviver = opts.reviver;
if( !key || typeof(key) != 'string' ) {
throw new Error('a string key must be specified');
}
if( key.length < MIN_KEY_LENGTH ) {
throw new Error('key must be at least ' + MIN_KEY_LENGTH + ' characters long');
}
if( reviver !== undefined && reviver !== null && typeof(reviver) != 'function' ) {
throw new Error('reviver must be a function');
}
// Use SHA-256 to derive a 32-byte key from the specified string.
// NOTE: We could alternatively do some kind of key stretching here.
var cryptoKey = crypto.createHash('sha256').update(key).digest();
// Returns the HMAC(text) using the derived cryptoKey
// Defaults to returning the result as hex.
function hmac(text, format) {
format = format || 'hex';
return crypto.createHmac('sha256', cryptoKey).update(text).digest(format);
}
// Encrypts an arbitrary object using the derived cryptoKey and retursn the result as text.
// The object is first serialized to JSON (via JSON.stringify) and the result is encrypted.
//
// The format of the output is:
// [<hmac>]<iv><encryptedJson>
//
// <hmac> : Optional HMAC
// <iv> : Randomly generated initailization vector
// <encryptedJson> : The encrypted object
function encrypt(obj) {
var json = JSON.stringify(obj);
// First generate a random IV.
// AES-256 IV size is sixteen bytes:
var iv = crypto.randomBytes(16);
// Make sure to use the 'iv' variant when creating the cipher object:
var cipher = crypto.createCipheriv('aes-256-cbc', cryptoKey, iv);
// Generate the encrypted json:
var encryptedJson = cipher.update(json, 'utf8', 'base64') + cipher.final('base64');
// Include the hex-encoded IV + the encrypted base64 data
// NOTE: We're using hex for encoding the IV to ensure that it's of constant length.
var result = iv.toString('hex') + encryptedJson;
if( verifyHmac ) {
// Prepend an HMAC to the result to verify it's integrity prior to decrypting.
// NOTE: We're using hex for encoding the hmac to ensure that it's of constant length
result = hmac(result, 'hex') + result;
}
return result;
}
// Decrypts the encrypted cipherText and returns back the original object.
// If the cipherText cannot be decrypted (bad key, bad text, bad serialization) then it returns null.
//
// NOTE: This function never throws an error. It will instead return null if it cannot decrypt the cipherText.
// NOTE: It's possible that the data decrypted is null (since it's valid input for encrypt(...)).
// It's up to the caller to decide if the result is valid.
function decrypt(cipherText) {
if( !cipherText ) {
return null;
}
try {
if( verifyHmac ) {
// Extract the HMAC from the start of the message:
var expectedHmac = cipherText.substring(0, 64);
// The remaining message is the IV + encrypted message:
cipherText = cipherText.substring(64);
// Calculate the actual HMAC of the message:
var actualHmac = hmac(cipherText);
if( !scmp(Buffer.from(actualHmac, 'hex'), Buffer.from(expectedHmac, 'hex')) ) {
throw new Error('HMAC does not match');
}
}
// Extract the IV from the beginning of the message:
var iv = Buffer.from(cipherText.substring(0,32), 'hex');
// The remaining text is the encrypted JSON:
var encryptedJson = cipherText.substring(32);
// Make sure to use the 'iv' variant when creating the decipher object:
var decipher = crypto.createDecipheriv('aes-256-cbc', cryptoKey, iv);
// Decrypt the JSON:
var json = decipher.update(encryptedJson, 'base64', 'utf8') + decipher.final('utf8');
// Return the parsed object:
return JSON.parse(json, reviver);
} catch( e ) {
// If we get an error log it and ignore it. Decrypting should never fail.
if( debug ) {
console.error('Exception in decrypt (ignored): %s', e);
}
return null;
}
}
return {
encrypt: encrypt,
decrypt: decrypt,
hmac: hmac
};
}
module.exports = createEncryptor;
module.exports.createEncryptor = createEncryptor;