You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
	
	
		
			152 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			JavaScript
		
	
		
		
			
		
	
	
			152 lines
		
	
	
		
			3.7 KiB
		
	
	
	
		
			JavaScript
		
	
| 
											8 years ago
										 | /* eslint-env browser */ | ||
|  | 
 | ||
|  | /* eslint-disable camelcase */ | ||
|  | 
 | ||
|  | module.exports = { | ||
|  |   encryptSymmetric, | ||
|  |   decryptSymmetric, | ||
|  |   constantTimeEqual, | ||
|  | }; | ||
|  | 
 | ||
|  | const IV_LENGTH = 16; | ||
|  | const MAC_LENGTH = 16; | ||
|  | const NONCE_LENGTH = 16; | ||
|  | 
 | ||
|  | async function encryptSymmetric(key, plaintext) { | ||
|  |   const iv = _getZeros(IV_LENGTH); | ||
|  |   const nonce = _getRandomBytes(NONCE_LENGTH); | ||
|  | 
 | ||
|  |   const cipherKey = await _hmac_SHA256(key, nonce); | ||
|  |   const macKey = await _hmac_SHA256(key, cipherKey); | ||
|  | 
 | ||
|  |   const cipherText = await _encrypt_aes256_CBC_PKCSPadding(cipherKey, iv, plaintext); | ||
|  |   const mac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH); | ||
|  | 
 | ||
|  |   return _concatData([nonce, cipherText, mac]); | ||
|  | } | ||
|  | 
 | ||
|  | async function decryptSymmetric(key, data) { | ||
|  |   const iv = _getZeros(IV_LENGTH); | ||
|  | 
 | ||
|  |   const nonce = _getFirstBytes(data, NONCE_LENGTH); | ||
|  |   const cipherText = _getBytes( | ||
|  |     data, | ||
|  |     NONCE_LENGTH, | ||
|  |     data.byteLength - NONCE_LENGTH - MAC_LENGTH | ||
|  |   ); | ||
|  |   const theirMac = _getBytes(data, data.byteLength - MAC_LENGTH, MAC_LENGTH); | ||
|  | 
 | ||
|  |   const cipherKey = await _hmac_SHA256(key, nonce); | ||
|  |   const macKey = await _hmac_SHA256(key, cipherKey); | ||
|  | 
 | ||
|  |   const ourMac = _getFirstBytes(await _hmac_SHA256(macKey, cipherText), MAC_LENGTH); | ||
|  |   if (!constantTimeEqual(theirMac, ourMac)) { | ||
|  |     throw new Error('decryptSymmetric: Failed to decrypt; MAC verification failed'); | ||
|  |   } | ||
|  | 
 | ||
|  |   return _decrypt_aes256_CBC_PKCSPadding(cipherKey, iv, cipherText); | ||
|  | } | ||
|  | 
 | ||
|  | function constantTimeEqual(left, right) { | ||
|  |   if (left.byteLength !== right.byteLength) { | ||
|  |     return false; | ||
|  |   } | ||
|  |   let result = 0; | ||
|  |   const ta1 = new Uint8Array(left); | ||
|  |   const ta2 = new Uint8Array(right); | ||
|  |   for (let i = 0, max = left.byteLength; i < max; i += 1) { | ||
|  |     // eslint-disable-next-line no-bitwise
 | ||
|  |     result |= ta1[i] ^ ta2[i]; | ||
|  |   } | ||
|  |   return result === 0; | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | async function _hmac_SHA256(key, data) { | ||
|  |   const extractable = false; | ||
|  |   const cryptoKey = await window.crypto.subtle.importKey( | ||
|  |     'raw', | ||
|  |     key, | ||
|  |     { name: 'HMAC', hash: { name: 'SHA-256' } }, | ||
|  |     extractable, | ||
|  |     ['sign'] | ||
|  |   ); | ||
|  | 
 | ||
|  |   return window.crypto.subtle.sign({ name: 'HMAC', hash: 'SHA-256' }, cryptoKey, data); | ||
|  | } | ||
|  | 
 | ||
|  | async function _encrypt_aes256_CBC_PKCSPadding(key, iv, data) { | ||
|  |   const extractable = false; | ||
|  |   const cryptoKey = await window.crypto.subtle.importKey( | ||
|  |     'raw', | ||
|  |     key, | ||
|  |     { name: 'AES-CBC' }, | ||
|  |     extractable, | ||
|  |     ['encrypt'] | ||
|  |   ); | ||
|  | 
 | ||
|  |   return window.crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, data); | ||
|  | } | ||
|  | 
 | ||
|  | async function _decrypt_aes256_CBC_PKCSPadding(key, iv, data) { | ||
|  |   const extractable = false; | ||
|  |   const cryptoKey = await window.crypto.subtle.importKey( | ||
|  |     'raw', | ||
|  |     key, | ||
|  |     { name: 'AES-CBC' }, | ||
|  |     extractable, | ||
|  |     ['decrypt'] | ||
|  |   ); | ||
|  | 
 | ||
|  |   return window.crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, data); | ||
|  | } | ||
|  | 
 | ||
|  | 
 | ||
|  | function _getRandomBytes(n) { | ||
|  |   const bytes = new Uint8Array(n); | ||
|  |   window.crypto.getRandomValues(bytes); | ||
|  |   return bytes; | ||
|  | } | ||
|  | 
 | ||
|  | function _getZeros(n) { | ||
|  |   const result = new Uint8Array(n); | ||
|  | 
 | ||
|  |   const value = 0; | ||
|  |   const startIndex = 0; | ||
|  |   const endExclusive = n; | ||
|  |   result.fill(value, startIndex, endExclusive); | ||
|  | 
 | ||
|  |   return result; | ||
|  | } | ||
|  | 
 | ||
|  | function _getFirstBytes(data, n) { | ||
|  |   const source = new Uint8Array(data); | ||
|  |   return source.subarray(0, n); | ||
|  | } | ||
|  | 
 | ||
|  | function _getBytes(data, start, n) { | ||
|  |   const source = new Uint8Array(data); | ||
|  |   return source.subarray(start, start + n); | ||
|  | } | ||
|  | 
 | ||
|  | function _concatData(elements) { | ||
|  |   const length = elements.reduce( | ||
|  |     (total, element) => total + element.byteLength, | ||
|  |     0 | ||
|  |   ); | ||
|  | 
 | ||
|  |   const result = new Uint8Array(length); | ||
|  |   let position = 0; | ||
|  | 
 | ||
|  |   for (let i = 0, max = elements.length; i < max; i += 1) { | ||
|  |     const element = new Uint8Array(elements[i]); | ||
|  |     result.set(element, position); | ||
|  |     position += element.byteLength; | ||
|  |   } | ||
|  |   if (position !== result.length) { | ||
|  |     throw new Error('problem concatenating!'); | ||
|  |   } | ||
|  | 
 | ||
|  |   return result; | ||
|  | } |