1
|
|
|
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ |
2
|
|
|
/* AES counter-mode (CTR) implementation in JavaScript (c) Chris Veness 2005-2016 */ |
3
|
|
|
/* MIT Licence */ |
4
|
|
|
/* www.movable-type.co.uk/scripts/aes.html */ |
5
|
|
|
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ |
6
|
|
|
|
7
|
|
|
/* global WorkerGlobalScope */ |
8
|
|
|
'use strict'; |
9
|
|
|
if (typeof module!='undefined' && module.exports) var Aes = require('./aes.js'); // ≡ import Aes from 'aes.js' |
10
|
|
|
|
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Aes.Ctr: Counter-mode (CTR) wrapper for AES. |
14
|
|
|
* |
15
|
|
|
* This encrypts a Unicode string to produces a base64 ciphertext using 128/192/256-bit AES, |
16
|
|
|
* and the converse to decrypt an encrypted ciphertext. |
17
|
|
|
* |
18
|
|
|
* See http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf |
19
|
|
|
* |
20
|
|
|
* @augments Aes |
21
|
|
|
*/ |
22
|
|
|
Aes.Ctr = {}; |
23
|
|
|
|
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Encrypt a text using AES encryption in Counter mode of operation. |
27
|
|
|
* |
28
|
|
|
* Unicode multi-byte character safe |
29
|
|
|
* |
30
|
|
|
* @param {string} plaintext - Source text to be encrypted. |
31
|
|
|
* @param {string} password - The password to use to generate a key for encryption. |
32
|
|
|
* @param {number} nBits - Number of bits to be used in the key; 128 / 192 / 256. |
33
|
|
|
* @returns {string} Encrypted text. |
34
|
|
|
* |
35
|
|
|
* @example |
36
|
|
|
* var encr = Aes.Ctr.encrypt('big secret', 'pāşšŵōřđ', 256); // 'lwGl66VVwVObKIr6of8HVqJr' |
37
|
|
|
*/ |
38
|
|
|
Aes.Ctr.encrypt = function(plaintext, password, nBits) { |
39
|
|
|
var blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES |
40
|
|
|
if (!(nBits==128 || nBits==192 || nBits==256)) throw new Error('Key size is not 128 / 192 / 256'); |
41
|
|
|
plaintext = String(plaintext).utf8Encode(); |
42
|
|
|
password = String(password).utf8Encode(); |
43
|
|
|
|
44
|
|
|
// use AES itself to encrypt password to get cipher key (using plain password as source for key |
45
|
|
|
// expansion) - gives us well encrypted key (though hashed key might be preferred for prod'n use) |
46
|
|
|
var nBytes = nBits/8; // no bytes in key (16/24/32) |
47
|
|
|
var pwBytes = new Array(nBytes); |
48
|
|
|
for (var i=0; i<nBytes; i++) { // use 1st 16/24/32 chars of password for key |
49
|
|
|
pwBytes[i] = i<password.length ? password.charCodeAt(i) : 0; |
50
|
|
|
} |
51
|
|
|
var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes)); // gives us 16-byte key |
52
|
|
|
key = key.concat(key.slice(0, nBytes-16)); // expand key to 16/24/32 bytes long |
53
|
|
|
|
54
|
|
|
// initialise 1st 8 bytes of counter block with nonce (NIST SP800-38A §B.2): [0-1] = millisec, |
55
|
|
|
// [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106 |
56
|
|
|
var counterBlock = new Array(blockSize); |
57
|
|
|
|
58
|
|
|
var nonce = (new Date()).getTime(); // timestamp: milliseconds since 1-Jan-1970 |
59
|
|
|
var nonceMs = nonce%1000; |
60
|
|
|
var nonceSec = Math.floor(nonce/1000); |
61
|
|
|
var nonceRnd = Math.floor(Math.random()*0xffff); |
62
|
|
|
// for debugging: nonce = nonceMs = nonceSec = nonceRnd = 0; |
63
|
|
|
|
64
|
|
|
for (var i=0; i<2; i++) counterBlock[i] = (nonceMs >>> i*8) & 0xff; |
65
|
|
|
for (var i=0; i<2; i++) counterBlock[i+2] = (nonceRnd >>> i*8) & 0xff; |
66
|
|
|
for (var i=0; i<4; i++) counterBlock[i+4] = (nonceSec >>> i*8) & 0xff; |
67
|
|
|
|
68
|
|
|
// and convert it to a string to go on the front of the ciphertext |
69
|
|
|
var ctrTxt = ''; |
70
|
|
|
for (var i=0; i<8; i++) ctrTxt += String.fromCharCode(counterBlock[i]); |
71
|
|
|
|
72
|
|
|
// generate key schedule - an expansion of the key into distinct Key Rounds for each round |
73
|
|
|
var keySchedule = Aes.keyExpansion(key); |
74
|
|
|
|
75
|
|
|
var blockCount = Math.ceil(plaintext.length/blockSize); |
76
|
|
|
var ciphertext = ''; |
77
|
|
|
|
78
|
|
|
for (var b=0; b<blockCount; b++) { |
79
|
|
|
// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) |
80
|
|
|
// done in two stages for 32-bit ops: using two words allows us to go past 2^32 blocks (68GB) |
81
|
|
|
for (var c=0; c<4; c++) counterBlock[15-c] = (b >>> c*8) & 0xff; |
82
|
|
|
for (var c=0; c<4; c++) counterBlock[15-c-4] = (b/0x100000000 >>> c*8); |
83
|
|
|
|
84
|
|
|
var cipherCntr = Aes.cipher(counterBlock, keySchedule); // -- encrypt counter block -- |
85
|
|
|
|
86
|
|
|
// block size is reduced on final block |
87
|
|
|
var blockLength = b<blockCount-1 ? blockSize : (plaintext.length-1)%blockSize+1; |
88
|
|
|
var cipherChar = new Array(blockLength); |
89
|
|
|
|
90
|
|
|
for (var i=0; i<blockLength; i++) { |
91
|
|
|
// -- xor plaintext with ciphered counter char-by-char -- |
92
|
|
|
cipherChar[i] = cipherCntr[i] ^ plaintext.charCodeAt(b*blockSize+i); |
93
|
|
|
cipherChar[i] = String.fromCharCode(cipherChar[i]); |
94
|
|
|
} |
95
|
|
|
ciphertext += cipherChar.join(''); |
96
|
|
|
|
97
|
|
|
// if within web worker, announce progress every 1000 blocks (roughly every 50ms) |
98
|
|
|
if (typeof WorkerGlobalScope != 'undefined' && self instanceof WorkerGlobalScope) { |
99
|
|
|
if (b%1000 == 0) self.postMessage({ progress: b/blockCount }); |
100
|
|
|
} |
101
|
|
|
} |
102
|
|
|
|
103
|
|
|
ciphertext = (ctrTxt+ciphertext).base64Encode(); |
104
|
|
|
|
105
|
|
|
return ciphertext; |
106
|
|
|
}; |
107
|
|
|
|
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Decrypt a text encrypted by AES in counter mode of operation |
111
|
|
|
* |
112
|
|
|
* @param {string} ciphertext - Cipher text to be decrypted. |
113
|
|
|
* @param {string} password - Password to use to generate a key for decryption. |
114
|
|
|
* @param {number} nBits - Number of bits to be used in the key; 128 / 192 / 256. |
115
|
|
|
* @returns {string} Decrypted text |
116
|
|
|
* |
117
|
|
|
* @example |
118
|
|
|
* var decr = Aes.Ctr.decrypt('lwGl66VVwVObKIr6of8HVqJr', 'pāşšŵōřđ', 256); // 'big secret' |
119
|
|
|
*/ |
120
|
|
|
Aes.Ctr.decrypt = function(ciphertext, password, nBits) { |
121
|
|
|
var blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES |
122
|
|
|
if (!(nBits==128 || nBits==192 || nBits==256)) throw new Error('Key size is not 128 / 192 / 256'); |
123
|
|
|
ciphertext = String(ciphertext).base64Decode(); |
124
|
|
|
password = String(password).utf8Encode(); |
125
|
|
|
|
126
|
|
|
// use AES to encrypt password (mirroring encrypt routine) |
127
|
|
|
var nBytes = nBits/8; // no bytes in key |
128
|
|
|
var pwBytes = new Array(nBytes); |
129
|
|
|
for (var i=0; i<nBytes; i++) { |
130
|
|
|
pwBytes[i] = i<password.length ? password.charCodeAt(i) : 0; |
131
|
|
|
} |
132
|
|
|
var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes)); |
133
|
|
|
key = key.concat(key.slice(0, nBytes-16)); // expand key to 16/24/32 bytes long |
134
|
|
|
|
135
|
|
|
// recover nonce from 1st 8 bytes of ciphertext |
136
|
|
|
var counterBlock = new Array(8); |
137
|
|
|
var ctrTxt = ciphertext.slice(0, 8); |
138
|
|
|
for (var i=0; i<8; i++) counterBlock[i] = ctrTxt.charCodeAt(i); |
139
|
|
|
|
140
|
|
|
// generate key schedule |
141
|
|
|
var keySchedule = Aes.keyExpansion(key); |
142
|
|
|
|
143
|
|
|
// separate ciphertext into blocks (skipping past initial 8 bytes) |
144
|
|
|
var nBlocks = Math.ceil((ciphertext.length-8) / blockSize); |
145
|
|
|
var ct = new Array(nBlocks); |
146
|
|
|
for (var b=0; b<nBlocks; b++) ct[b] = ciphertext.slice(8+b*blockSize, 8+b*blockSize+blockSize); |
147
|
|
|
ciphertext = ct; // ciphertext is now array of block-length strings |
148
|
|
|
|
149
|
|
|
// plaintext will get generated block-by-block into array of block-length strings |
150
|
|
|
var plaintext = ''; |
151
|
|
|
|
152
|
|
|
for (var b=0; b<nBlocks; b++) { |
153
|
|
|
// set counter (block #) in last 8 bytes of counter block (leaving nonce in 1st 8 bytes) |
154
|
|
|
for (var c=0; c<4; c++) counterBlock[15-c] = ((b) >>> c*8) & 0xff; |
155
|
|
|
for (var c=0; c<4; c++) counterBlock[15-c-4] = (((b+1)/0x100000000-1) >>> c*8) & 0xff; |
156
|
|
|
|
157
|
|
|
var cipherCntr = Aes.cipher(counterBlock, keySchedule); // encrypt counter block |
158
|
|
|
|
159
|
|
|
var plaintxtByte = new Array(ciphertext[b].length); |
160
|
|
|
for (var i=0; i<ciphertext[b].length; i++) { |
161
|
|
|
// -- xor plaintext with ciphered counter byte-by-byte -- |
162
|
|
|
plaintxtByte[i] = cipherCntr[i] ^ ciphertext[b].charCodeAt(i); |
163
|
|
|
plaintxtByte[i] = String.fromCharCode(plaintxtByte[i]); |
164
|
|
|
} |
165
|
|
|
plaintext += plaintxtByte.join(''); |
166
|
|
|
|
167
|
|
|
// if within web worker, announce progress every 1000 blocks (roughly every 50ms) |
168
|
|
|
if (typeof WorkerGlobalScope != 'undefined' && self instanceof WorkerGlobalScope) { |
169
|
|
|
if (b%1000 == 0) self.postMessage({ progress: b/nBlocks }); |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
plaintext = plaintext.utf8Decode(); // decode from UTF8 back to Unicode multi-byte chars |
174
|
|
|
|
175
|
|
|
return plaintext; |
176
|
|
|
}; |
177
|
|
|
|
178
|
|
|
|
179
|
|
|
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ |
180
|
|
|
|
181
|
|
|
/* Extend String object with method to encode multi-byte string to utf8 |
182
|
|
|
* - monsur.hossa.in/2012/07/20/utf-8-in-javascript.html |
183
|
|
|
* - note utf8Encode is an identity function with 7-bit ascii strings, but not with 8-bit strings; |
184
|
|
|
* - utf8Encode('x') = 'x', but utf8Encode('ça') = 'ça', and utf8Encode('ça') = 'ça'*/ |
185
|
|
|
if (typeof String.prototype.utf8Encode == 'undefined') { |
186
|
|
|
String.prototype.utf8Encode = function() { |
187
|
|
|
return unescape( encodeURIComponent( this ) ); |
188
|
|
|
}; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/* Extend String object with method to decode utf8 string to multi-byte */ |
192
|
|
|
if (typeof String.prototype.utf8Decode == 'undefined') { |
193
|
|
|
String.prototype.utf8Decode = function() { |
194
|
|
|
try { |
195
|
|
|
return decodeURIComponent( escape( this ) ); |
196
|
|
|
} catch (e) { |
197
|
|
|
return this; // invalid UTF-8? return as-is |
198
|
|
|
} |
199
|
|
|
}; |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
/* Extend String object with method to encode base64 |
203
|
|
|
* - developer.mozilla.org/en-US/docs/Web/API/window.btoa, nodejs.org/api/buffer.html |
204
|
|
|
* - note: btoa & Buffer/binary work on single-byte Unicode (C0/C1), so ok for utf8 strings, not for general Unicode... |
205
|
|
|
* - note: if btoa()/atob() are not available (eg IE9-), try github.com/davidchambers/Base64.js */ |
206
|
|
|
if (typeof String.prototype.base64Encode == 'undefined') { |
207
|
|
|
String.prototype.base64Encode = function() { |
208
|
|
|
if (typeof btoa != 'undefined') return btoa(this); // browser |
209
|
|
|
if (typeof Buffer != 'undefined') return new Buffer(this, 'binary').toString('base64'); // Node.js |
210
|
|
|
throw new Error('No Base64 Encode'); |
211
|
|
|
}; |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/* Extend String object with method to decode base64 */ |
215
|
|
|
if (typeof String.prototype.base64Decode == 'undefined') { |
216
|
|
|
String.prototype.base64Decode = function() { |
217
|
|
|
if (typeof atob != 'undefined') return atob(this); // browser |
218
|
|
|
if (typeof Buffer != 'undefined') return new Buffer(this, 'base64').toString('binary'); // Node.js |
219
|
|
|
throw new Error('No Base64 Decode'); |
220
|
|
|
}; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
|
224
|
|
|
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ |
225
|
|
|
if (typeof module != 'undefined' && module.exports) module.exports = Aes.Ctr; // ≡ export default Aes.Ctr |
226
|
|
|
|