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.
		
		
		
		
		
			
		
			
				
	
	
		
			344 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			344 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			JavaScript
		
	
| /* eslint-env browser */
 | |
| /* global dcodeIO, libsignal */
 | |
| 
 | |
| /* eslint-disable camelcase, no-bitwise */
 | |
| 
 | |
| module.exports = {
 | |
|   arrayBufferToBase64,
 | |
|   base64ToArrayBuffer,
 | |
|   bytesFromString,
 | |
|   concatenateBytes,
 | |
|   constantTimeEqual,
 | |
|   decryptAesCtr,
 | |
|   decryptAttachment,
 | |
|   decryptFile,
 | |
|   decryptSymmetric,
 | |
|   deriveAccessKey,
 | |
|   encryptAesCtr,
 | |
|   encryptAttachment,
 | |
|   encryptFile,
 | |
|   encryptSymmetric,
 | |
|   fromEncodedBinaryToArrayBuffer,
 | |
|   getAccessKeyVerifier,
 | |
|   getRandomBytes,
 | |
|   getViewOfArrayBuffer,
 | |
|   getZeroes,
 | |
|   highBitsToInt,
 | |
|   hmacSha256,
 | |
|   intsToByteHighAndLow,
 | |
|   splitBytes,
 | |
|   stringFromBytes,
 | |
|   trimBytes,
 | |
|   verifyAccessKey,
 | |
| };
 | |
| 
 | |
| function arrayBufferToBase64(arrayBuffer) {
 | |
|   return dcodeIO.ByteBuffer.wrap(arrayBuffer).toString('base64');
 | |
| }
 | |
| function base64ToArrayBuffer(base64string) {
 | |
|   return dcodeIO.ByteBuffer.wrap(base64string, 'base64').toArrayBuffer();
 | |
| }
 | |
| 
 | |
| function fromEncodedBinaryToArrayBuffer(key) {
 | |
|   return dcodeIO.ByteBuffer.wrap(key, 'binary').toArrayBuffer();
 | |
| }
 | |
| 
 | |
| function bytesFromString(string) {
 | |
|   return dcodeIO.ByteBuffer.wrap(string, 'utf8').toArrayBuffer();
 | |
| }
 | |
| function stringFromBytes(buffer) {
 | |
|   return dcodeIO.ByteBuffer.wrap(buffer).toString('utf8');
 | |
| }
 | |
| 
 | |
| // High-level Operations
 | |
| 
 | |
| // Path structure: 'fa/facdf99c22945b1c9393345599a276f4b36ad7ccdc8c2467f5441b742c2d11fa'
 | |
| function getAttachmentLabel(path) {
 | |
|   const filename = path.slice(3);
 | |
|   return base64ToArrayBuffer(filename);
 | |
| }
 | |
| 
 | |
| const PUB_KEY_LENGTH = 32;
 | |
| async function encryptAttachment(staticPublicKey, path, plaintext) {
 | |
|   const uniqueId = getAttachmentLabel(path);
 | |
|   return encryptFile(staticPublicKey, uniqueId, plaintext);
 | |
| }
 | |
| 
 | |
| async function decryptAttachment(staticPrivateKey, path, data) {
 | |
|   const uniqueId = getAttachmentLabel(path);
 | |
|   return decryptFile(staticPrivateKey, uniqueId, data);
 | |
| }
 | |
| 
 | |
| async function encryptFile(staticPublicKey, uniqueId, plaintext) {
 | |
|   const ephemeralKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
 | |
|   const agreement = await libsignal.Curve.async.calculateAgreement(
 | |
|     staticPublicKey,
 | |
|     ephemeralKeyPair.privKey
 | |
|   );
 | |
|   const key = await hmacSha256(agreement, uniqueId);
 | |
| 
 | |
|   const prefix = ephemeralKeyPair.pubKey.slice(1);
 | |
|   return concatenateBytes(prefix, await encryptSymmetric(key, plaintext));
 | |
| }
 | |
| 
 | |
| async function decryptFile(staticPrivateKey, uniqueId, data) {
 | |
|   const ephemeralPublicKey = _getFirstBytes(data, PUB_KEY_LENGTH);
 | |
|   const ciphertext = _getBytes(data, PUB_KEY_LENGTH, data.byteLength);
 | |
|   const agreement = await libsignal.Curve.async.calculateAgreement(
 | |
|     ephemeralPublicKey,
 | |
|     staticPrivateKey
 | |
|   );
 | |
| 
 | |
|   const key = await hmacSha256(agreement, uniqueId);
 | |
| 
 | |
|   return decryptSymmetric(key, ciphertext);
 | |
| }
 | |
| 
 | |
| async function deriveAccessKey(profileKey) {
 | |
|   const iv = getZeroes(12);
 | |
|   const plaintext = getZeroes(16);
 | |
|   const accessKey = await _encrypt_aes_gcm(profileKey, iv, plaintext);
 | |
|   return _getFirstBytes(accessKey, 16);
 | |
| }
 | |
| 
 | |
| async function getAccessKeyVerifier(accessKey) {
 | |
|   const plaintext = getZeroes(32);
 | |
|   const hmac = await hmacSha256(accessKey, plaintext);
 | |
| 
 | |
|   return hmac;
 | |
| }
 | |
| 
 | |
| async function verifyAccessKey(accessKey, theirVerifier) {
 | |
|   const ourVerifier = await getAccessKeyVerifier(accessKey);
 | |
| 
 | |
|   if (constantTimeEqual(ourVerifier, theirVerifier)) {
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   return false;
 | |
| }
 | |
| 
 | |
| const IV_LENGTH = 16;
 | |
| const MAC_LENGTH = 16;
 | |
| const NONCE_LENGTH = 16;
 | |
| 
 | |
| async function encryptSymmetric(key, plaintext) {
 | |
|   const iv = getZeroes(IV_LENGTH);
 | |
|   const nonce = getRandomBytes(NONCE_LENGTH);
 | |
| 
 | |
|   const cipherKey = await hmacSha256(key, nonce);
 | |
|   const macKey = await hmacSha256(key, cipherKey);
 | |
| 
 | |
|   const cipherText = await _encrypt_aes256_CBC_PKCSPadding(cipherKey, iv, plaintext);
 | |
|   const mac = _getFirstBytes(await hmacSha256(macKey, cipherText), MAC_LENGTH);
 | |
| 
 | |
|   return concatenateBytes(nonce, cipherText, mac);
 | |
| }
 | |
| 
 | |
| async function decryptSymmetric(key, data) {
 | |
|   const iv = getZeroes(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 hmacSha256(key, nonce);
 | |
|   const macKey = await hmacSha256(key, cipherKey);
 | |
| 
 | |
|   const ourMac = _getFirstBytes(await hmacSha256(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;
 | |
| }
 | |
| 
 | |
| // Encryption
 | |
| 
 | |
| async function hmacSha256(key, plaintext) {
 | |
|   const algorithm = {
 | |
|     name: 'HMAC',
 | |
|     hash: 'SHA-256',
 | |
|   };
 | |
|   const extractable = false;
 | |
| 
 | |
|   const cryptoKey = await window.crypto.subtle.importKey('raw', key, algorithm, extractable, [
 | |
|     'sign',
 | |
|   ]);
 | |
| 
 | |
|   return window.crypto.subtle.sign(algorithm, cryptoKey, plaintext);
 | |
| }
 | |
| 
 | |
| async function _encrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
 | |
|   const algorithm = {
 | |
|     name: 'AES-CBC',
 | |
|     iv,
 | |
|   };
 | |
|   const extractable = false;
 | |
| 
 | |
|   const cryptoKey = await window.crypto.subtle.importKey('raw', key, algorithm, extractable, [
 | |
|     'encrypt',
 | |
|   ]);
 | |
| 
 | |
|   return window.crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
 | |
| }
 | |
| 
 | |
| async function _decrypt_aes256_CBC_PKCSPadding(key, iv, plaintext) {
 | |
|   const algorithm = {
 | |
|     name: 'AES-CBC',
 | |
|     iv,
 | |
|   };
 | |
|   const extractable = false;
 | |
| 
 | |
|   const cryptoKey = await window.crypto.subtle.importKey('raw', key, algorithm, extractable, [
 | |
|     'decrypt',
 | |
|   ]);
 | |
|   return window.crypto.subtle.decrypt(algorithm, cryptoKey, plaintext);
 | |
| }
 | |
| 
 | |
| async function encryptAesCtr(key, plaintext, counter) {
 | |
|   const extractable = false;
 | |
|   const algorithm = {
 | |
|     name: 'AES-CTR',
 | |
|     counter: new Uint8Array(counter),
 | |
|     length: 128,
 | |
|   };
 | |
| 
 | |
|   const cryptoKey = await crypto.subtle.importKey('raw', key, algorithm, extractable, ['encrypt']);
 | |
| 
 | |
|   const ciphertext = await crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
 | |
| 
 | |
|   return ciphertext;
 | |
| }
 | |
| 
 | |
| async function decryptAesCtr(key, ciphertext, counter) {
 | |
|   const extractable = false;
 | |
|   const algorithm = {
 | |
|     name: 'AES-CTR',
 | |
|     counter: new Uint8Array(counter),
 | |
|     length: 128,
 | |
|   };
 | |
| 
 | |
|   const cryptoKey = await crypto.subtle.importKey('raw', key, algorithm, extractable, ['decrypt']);
 | |
|   const plaintext = await crypto.subtle.decrypt(algorithm, cryptoKey, ciphertext);
 | |
|   return plaintext;
 | |
| }
 | |
| 
 | |
| async function _encrypt_aes_gcm(key, iv, plaintext) {
 | |
|   const algorithm = {
 | |
|     name: 'AES-GCM',
 | |
|     iv,
 | |
|   };
 | |
|   const extractable = false;
 | |
| 
 | |
|   const cryptoKey = await crypto.subtle.importKey('raw', key, algorithm, extractable, ['encrypt']);
 | |
|   return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
 | |
| }
 | |
| 
 | |
| // Utility
 | |
| 
 | |
| function getRandomBytes(n) {
 | |
|   const bytes = new Uint8Array(n);
 | |
|   window.crypto.getRandomValues(bytes);
 | |
|   return bytes;
 | |
| }
 | |
| 
 | |
| function getZeroes(n) {
 | |
|   const result = new Uint8Array(n);
 | |
| 
 | |
|   const value = 0;
 | |
|   const startIndex = 0;
 | |
|   const endExclusive = n;
 | |
|   result.fill(value, startIndex, endExclusive);
 | |
| 
 | |
|   return result;
 | |
| }
 | |
| 
 | |
| function highBitsToInt(byte) {
 | |
|   return (byte & 0xff) >> 4;
 | |
| }
 | |
| 
 | |
| function intsToByteHighAndLow(highValue, lowValue) {
 | |
|   return ((highValue << 4) | lowValue) & 0xff;
 | |
| }
 | |
| 
 | |
| function trimBytes(buffer, length) {
 | |
|   return _getFirstBytes(buffer, length);
 | |
| }
 | |
| 
 | |
| function getViewOfArrayBuffer(buffer, start, finish) {
 | |
|   const source = new Uint8Array(buffer);
 | |
|   const result = source.slice(start, finish);
 | |
|   return result.buffer;
 | |
| }
 | |
| 
 | |
| function concatenateBytes(...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.buffer;
 | |
| }
 | |
| 
 | |
| function splitBytes(buffer, ...lengths) {
 | |
|   const total = lengths.reduce((acc, length) => acc + length, 0);
 | |
| 
 | |
|   if (total !== buffer.byteLength) {
 | |
|     throw new Error(
 | |
|       `Requested lengths total ${total} does not match source total ${buffer.byteLength}`
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   const source = new Uint8Array(buffer);
 | |
|   const results = [];
 | |
|   let position = 0;
 | |
| 
 | |
|   for (let i = 0, max = lengths.length; i < max; i += 1) {
 | |
|     const length = lengths[i];
 | |
|     const result = new Uint8Array(length);
 | |
|     const section = source.slice(position, position + length);
 | |
|     result.set(section);
 | |
|     position += result.byteLength;
 | |
| 
 | |
|     results.push(result);
 | |
|   }
 | |
| 
 | |
|   return results;
 | |
| }
 | |
| 
 | |
| // Internal-only
 | |
| 
 | |
| 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);
 | |
| }
 |