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.
		
		
		
		
		
			
		
			
				
	
	
		
			451 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			451 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
/* eslint-env browser */
 | 
						|
/* global dcodeIO, libsignal */
 | 
						|
 | 
						|
/* eslint-disable camelcase, no-bitwise */
 | 
						|
 | 
						|
module.exports = {
 | 
						|
  arrayBufferToBase64,
 | 
						|
  base64ToArrayBuffer,
 | 
						|
  bytesFromString,
 | 
						|
  concatenateBytes,
 | 
						|
  constantTimeEqual,
 | 
						|
  decryptAesCtr,
 | 
						|
  decryptDeviceName,
 | 
						|
  decryptAttachment,
 | 
						|
  decryptFile,
 | 
						|
  decryptSymmetric,
 | 
						|
  deriveAccessKey,
 | 
						|
  encryptAesCtr,
 | 
						|
  encryptDeviceName,
 | 
						|
  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
 | 
						|
 | 
						|
async function encryptDeviceName(deviceName, identityPublic) {
 | 
						|
  const plaintext = bytesFromString(deviceName);
 | 
						|
  const ephemeralKeyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
 | 
						|
  const masterSecret = await libsignal.Curve.async.calculateAgreement(
 | 
						|
    identityPublic,
 | 
						|
    ephemeralKeyPair.privKey
 | 
						|
  );
 | 
						|
 | 
						|
  const key1 = await hmacSha256(masterSecret, bytesFromString('auth'));
 | 
						|
  const syntheticIv = _getFirstBytes(await hmacSha256(key1, plaintext), 16);
 | 
						|
 | 
						|
  const key2 = await hmacSha256(masterSecret, bytesFromString('cipher'));
 | 
						|
  const cipherKey = await hmacSha256(key2, syntheticIv);
 | 
						|
 | 
						|
  const counter = getZeroes(16);
 | 
						|
  const ciphertext = await encryptAesCtr(cipherKey, plaintext, counter);
 | 
						|
 | 
						|
  return {
 | 
						|
    ephemeralPublic: ephemeralKeyPair.pubKey,
 | 
						|
    syntheticIv,
 | 
						|
    ciphertext,
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
async function decryptDeviceName(
 | 
						|
  { ephemeralPublic, syntheticIv, ciphertext } = {},
 | 
						|
  identityPrivate
 | 
						|
) {
 | 
						|
  const masterSecret = await libsignal.Curve.async.calculateAgreement(
 | 
						|
    ephemeralPublic,
 | 
						|
    identityPrivate
 | 
						|
  );
 | 
						|
 | 
						|
  const key2 = await hmacSha256(masterSecret, bytesFromString('cipher'));
 | 
						|
  const cipherKey = await hmacSha256(key2, syntheticIv);
 | 
						|
 | 
						|
  const counter = getZeroes(16);
 | 
						|
  const plaintext = await decryptAesCtr(cipherKey, ciphertext, counter);
 | 
						|
 | 
						|
  const key1 = await hmacSha256(masterSecret, bytesFromString('auth'));
 | 
						|
  const ourSyntheticIv = _getFirstBytes(await hmacSha256(key1, plaintext), 16);
 | 
						|
 | 
						|
  if (!constantTimeEqual(ourSyntheticIv, syntheticIv)) {
 | 
						|
    throw new Error('decryptDeviceName: synthetic IV did not match');
 | 
						|
  }
 | 
						|
 | 
						|
  return stringFromBytes(plaintext);
 | 
						|
}
 | 
						|
 | 
						|
// 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);
 | 
						|
}
 |