signal-desktop/libtextsecure/crypto.js

231 lines
7.5 KiB
JavaScript
Raw Normal View History

2018-07-21 21:51:20 +00:00
/* global libsignal, crypto, textsecure, dcodeIO, window */
/* eslint-disable more/no-then, no-bitwise */
2018-07-21 21:51:20 +00:00
// eslint-disable-next-line func-names
(function() {
const { encrypt, decrypt, calculateMAC, verifyMAC } = libsignal.crypto;
2018-07-21 21:51:20 +00:00
const PROFILE_IV_LENGTH = 12; // bytes
const PROFILE_KEY_LENGTH = 32; // bytes
const PROFILE_TAG_LENGTH = 128; // bits
const PROFILE_NAME_PADDED_LENGTH = 26; // bytes
function verifyDigest(data, theirDigest) {
2018-07-21 21:51:20 +00:00
return crypto.subtle.digest({ name: 'SHA-256' }, data).then(ourDigest => {
const a = new Uint8Array(ourDigest);
const b = new Uint8Array(theirDigest);
let result = 0;
for (let i = 0; i < theirDigest.byteLength; i += 1) {
result |= a[i] ^ b[i];
}
if (result !== 0) {
throw new Error('Bad digest');
}
});
}
function calculateDigest(data) {
return crypto.subtle.digest({ name: 'SHA-256' }, data);
}
window.textsecure = window.textsecure || {};
window.textsecure.crypto = {
// Decrypts message into a raw string
2018-07-21 21:51:20 +00:00
decryptWebsocketMessage(message, signalingKey) {
const decodedMessage = message.toArrayBuffer();
2018-07-21 21:51:20 +00:00
if (signalingKey.byteLength !== 52) {
throw new Error('Got invalid length signalingKey');
}
if (decodedMessage.byteLength < 1 + 16 + 10) {
throw new Error('Got invalid length message');
}
2018-07-21 21:51:20 +00:00
if (new Uint8Array(decodedMessage)[0] !== 1) {
throw new Error(`Got bad version number: ${decodedMessage[0]}`);
}
2018-07-21 21:51:20 +00:00
const aesKey = signalingKey.slice(0, 32);
const macKey = signalingKey.slice(32, 32 + 20);
2018-07-21 21:51:20 +00:00
const iv = decodedMessage.slice(1, 1 + 16);
const ciphertext = decodedMessage.slice(
1 + 16,
decodedMessage.byteLength - 10
);
2018-07-21 21:51:20 +00:00
const ivAndCiphertext = decodedMessage.slice(
0,
decodedMessage.byteLength - 10
);
2018-07-21 21:51:20 +00:00
const mac = decodedMessage.slice(
decodedMessage.byteLength - 10,
decodedMessage.byteLength
);
2018-07-21 21:51:20 +00:00
return verifyMAC(ivAndCiphertext, macKey, mac, 10).then(() =>
decrypt(aesKey, ciphertext, iv)
);
},
2018-07-21 21:51:20 +00:00
decryptAttachment(encryptedBin, keys, theirDigest) {
if (keys.byteLength !== 64) {
throw new Error('Got invalid length attachment keys');
}
if (encryptedBin.byteLength < 16 + 32) {
throw new Error('Got invalid length attachment');
}
2018-07-21 21:51:20 +00:00
const aesKey = keys.slice(0, 32);
const macKey = keys.slice(32, 64);
2018-07-21 21:51:20 +00:00
const iv = encryptedBin.slice(0, 16);
const ciphertext = encryptedBin.slice(16, encryptedBin.byteLength - 32);
const ivAndCiphertext = encryptedBin.slice(
0,
encryptedBin.byteLength - 32
);
const mac = encryptedBin.slice(
encryptedBin.byteLength - 32,
encryptedBin.byteLength
);
2018-07-21 21:51:20 +00:00
return verifyMAC(ivAndCiphertext, macKey, mac, 32)
.then(() => {
if (theirDigest) {
return verifyDigest(encryptedBin, theirDigest);
}
return null;
})
2018-07-21 21:51:20 +00:00
.then(() => decrypt(aesKey, ciphertext, iv));
},
2018-07-21 21:51:20 +00:00
encryptAttachment(plaintext, keys, iv) {
if (
!(plaintext instanceof ArrayBuffer) &&
!ArrayBuffer.isView(plaintext)
) {
throw new TypeError(
2018-07-21 21:51:20 +00:00
`\`plaintext\` must be an \`ArrayBuffer\` or \`ArrayBufferView\`; got: ${typeof plaintext}`
);
}
2018-07-21 21:51:20 +00:00
if (keys.byteLength !== 64) {
throw new Error('Got invalid length attachment keys');
}
2018-07-21 21:51:20 +00:00
if (iv.byteLength !== 16) {
throw new Error('Got invalid length attachment iv');
}
2018-07-21 21:51:20 +00:00
const aesKey = keys.slice(0, 32);
const macKey = keys.slice(32, 64);
2018-07-21 21:51:20 +00:00
return encrypt(aesKey, plaintext, iv).then(ciphertext => {
const ivAndCiphertext = new Uint8Array(16 + ciphertext.byteLength);
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(new Uint8Array(ciphertext), 16);
2018-07-21 21:51:20 +00:00
return calculateMAC(macKey, ivAndCiphertext.buffer).then(mac => {
const encryptedBin = new Uint8Array(16 + ciphertext.byteLength + 32);
encryptedBin.set(ivAndCiphertext);
encryptedBin.set(new Uint8Array(mac), 16 + ciphertext.byteLength);
2018-07-21 21:51:20 +00:00
return calculateDigest(encryptedBin.buffer).then(digest => ({
ciphertext: encryptedBin.buffer,
digest,
}));
});
});
},
2018-07-21 21:51:20 +00:00
encryptProfile(data, key) {
const iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH);
if (key.byteLength !== PROFILE_KEY_LENGTH) {
throw new Error('Got invalid length profile key');
}
2018-07-21 21:51:20 +00:00
if (iv.byteLength !== PROFILE_IV_LENGTH) {
throw new Error('Got invalid length profile iv');
}
return crypto.subtle
.importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt'])
2018-07-21 21:51:20 +00:00
.then(keyForEncryption =>
crypto.subtle
.encrypt(
2018-07-21 21:51:20 +00:00
{ name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH },
keyForEncryption,
data
)
2018-07-21 21:51:20 +00:00
.then(ciphertext => {
const ivAndCiphertext = new Uint8Array(
PROFILE_IV_LENGTH + ciphertext.byteLength
);
Profiles (#1453) * Add AES-GCM encryption for profiles With tests. * Add profileKey to DataMessage protobuf // FREEBIE * Decrypt and save profile names // FREEBIE * Save incoming profile keys * Move pad/unpad to crypto module // FREEBIE * Support fetching avatars from the cdn // FREEBIE * Translate failed authentication errors When AES-GCM authentication fails, webcrypto returns a very generic error. The same error is thrown for invalid length inputs, but our earlier checks in decryptProfile should rule out those failure modes and leave us safe to assume that we either had bad ciphertext or the wrong key. // FREEBIE * Handle profile avatars (wip) and log decrypt errors // FREEBIE * Display profile avatars Synced contact avatars will still override profile avatars. * Display profile names in convo list Only if we don't have a synced contact name. // FREEBIE * Make cdn url an environment config Use different ones for staging and production // FREEBIE * Display profile name in conversation header * Display profile name in group messages * Update conversation header if profile avatar changes // FREEBIE * Style profile names small with ~ * Save profileKeys from contact sync messages // FREEBIE * Save profile keys from provisioning messages For standalone accounts, generate a random profile key. // FREEBIE * Special case for one-time sync of our profile key Android will use a contact sync message to sync a profile key from Android clients who have just upgraded and generated their profile key. Normally we should receive this data in a provisioning message. // FREEBIE * Infer profile sharing from synced data messages * Populate profile keys on outgoing messages Requires that `profileSharing` be set on the conversation. // FREEBIE * Support for the profile key update flag When receiving a message with this flag, don't init a message record, just process the profile key and move on. // FREEBIE * Display profile names in group member list * Refresh contact's profile on profile key changes // FREEBIE * Catch errors on profile save // FREEBIE * Save our own synced contact info Don't return early if we get a contact sync for our own number // FREEBIE
2017-09-11 16:50:35 +00:00
ivAndCiphertext.set(new Uint8Array(iv));
ivAndCiphertext.set(
new Uint8Array(ciphertext),
PROFILE_IV_LENGTH
);
Profiles (#1453) * Add AES-GCM encryption for profiles With tests. * Add profileKey to DataMessage protobuf // FREEBIE * Decrypt and save profile names // FREEBIE * Save incoming profile keys * Move pad/unpad to crypto module // FREEBIE * Support fetching avatars from the cdn // FREEBIE * Translate failed authentication errors When AES-GCM authentication fails, webcrypto returns a very generic error. The same error is thrown for invalid length inputs, but our earlier checks in decryptProfile should rule out those failure modes and leave us safe to assume that we either had bad ciphertext or the wrong key. // FREEBIE * Handle profile avatars (wip) and log decrypt errors // FREEBIE * Display profile avatars Synced contact avatars will still override profile avatars. * Display profile names in convo list Only if we don't have a synced contact name. // FREEBIE * Make cdn url an environment config Use different ones for staging and production // FREEBIE * Display profile name in conversation header * Display profile name in group messages * Update conversation header if profile avatar changes // FREEBIE * Style profile names small with ~ * Save profileKeys from contact sync messages // FREEBIE * Save profile keys from provisioning messages For standalone accounts, generate a random profile key. // FREEBIE * Special case for one-time sync of our profile key Android will use a contact sync message to sync a profile key from Android clients who have just upgraded and generated their profile key. Normally we should receive this data in a provisioning message. // FREEBIE * Infer profile sharing from synced data messages * Populate profile keys on outgoing messages Requires that `profileSharing` be set on the conversation. // FREEBIE * Support for the profile key update flag When receiving a message with this flag, don't init a message record, just process the profile key and move on. // FREEBIE * Display profile names in group member list * Refresh contact's profile on profile key changes // FREEBIE * Catch errors on profile save // FREEBIE * Save our own synced contact info Don't return early if we get a contact sync for our own number // FREEBIE
2017-09-11 16:50:35 +00:00
return ivAndCiphertext.buffer;
2018-07-21 21:51:20 +00:00
})
);
},
2018-07-21 21:51:20 +00:00
decryptProfile(data, key) {
if (data.byteLength < 12 + 16 + 1) {
2018-07-21 21:51:20 +00:00
throw new Error(`Got too short input: ${data.byteLength}`);
}
2018-07-21 21:51:20 +00:00
const iv = data.slice(0, PROFILE_IV_LENGTH);
const ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength);
if (key.byteLength !== PROFILE_KEY_LENGTH) {
throw new Error('Got invalid length profile key');
}
2018-07-21 21:51:20 +00:00
if (iv.byteLength !== PROFILE_IV_LENGTH) {
throw new Error('Got invalid length profile iv');
}
2018-07-21 21:51:20 +00:00
const error = new Error(); // save stack
return crypto.subtle
.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt'])
2018-07-21 21:51:20 +00:00
.then(keyForEncryption =>
crypto.subtle
.decrypt(
2018-07-21 21:51:20 +00:00
{ name: 'AES-GCM', iv, tagLength: PROFILE_TAG_LENGTH },
keyForEncryption,
ciphertext
)
2018-07-21 21:51:20 +00:00
.catch(e => {
Profiles (#1453) * Add AES-GCM encryption for profiles With tests. * Add profileKey to DataMessage protobuf // FREEBIE * Decrypt and save profile names // FREEBIE * Save incoming profile keys * Move pad/unpad to crypto module // FREEBIE * Support fetching avatars from the cdn // FREEBIE * Translate failed authentication errors When AES-GCM authentication fails, webcrypto returns a very generic error. The same error is thrown for invalid length inputs, but our earlier checks in decryptProfile should rule out those failure modes and leave us safe to assume that we either had bad ciphertext or the wrong key. // FREEBIE * Handle profile avatars (wip) and log decrypt errors // FREEBIE * Display profile avatars Synced contact avatars will still override profile avatars. * Display profile names in convo list Only if we don't have a synced contact name. // FREEBIE * Make cdn url an environment config Use different ones for staging and production // FREEBIE * Display profile name in conversation header * Display profile name in group messages * Update conversation header if profile avatar changes // FREEBIE * Style profile names small with ~ * Save profileKeys from contact sync messages // FREEBIE * Save profile keys from provisioning messages For standalone accounts, generate a random profile key. // FREEBIE * Special case for one-time sync of our profile key Android will use a contact sync message to sync a profile key from Android clients who have just upgraded and generated their profile key. Normally we should receive this data in a provisioning message. // FREEBIE * Infer profile sharing from synced data messages * Populate profile keys on outgoing messages Requires that `profileSharing` be set on the conversation. // FREEBIE * Support for the profile key update flag When receiving a message with this flag, don't init a message record, just process the profile key and move on. // FREEBIE * Display profile names in group member list * Refresh contact's profile on profile key changes // FREEBIE * Catch errors on profile save // FREEBIE * Save our own synced contact info Don't return early if we get a contact sync for our own number // FREEBIE
2017-09-11 16:50:35 +00:00
if (e.name === 'OperationError') {
// bad mac, basically.
error.message =
'Failed to decrypt profile data. Most likely the profile key has changed.';
Profiles (#1453) * Add AES-GCM encryption for profiles With tests. * Add profileKey to DataMessage protobuf // FREEBIE * Decrypt and save profile names // FREEBIE * Save incoming profile keys * Move pad/unpad to crypto module // FREEBIE * Support fetching avatars from the cdn // FREEBIE * Translate failed authentication errors When AES-GCM authentication fails, webcrypto returns a very generic error. The same error is thrown for invalid length inputs, but our earlier checks in decryptProfile should rule out those failure modes and leave us safe to assume that we either had bad ciphertext or the wrong key. // FREEBIE * Handle profile avatars (wip) and log decrypt errors // FREEBIE * Display profile avatars Synced contact avatars will still override profile avatars. * Display profile names in convo list Only if we don't have a synced contact name. // FREEBIE * Make cdn url an environment config Use different ones for staging and production // FREEBIE * Display profile name in conversation header * Display profile name in group messages * Update conversation header if profile avatar changes // FREEBIE * Style profile names small with ~ * Save profileKeys from contact sync messages // FREEBIE * Save profile keys from provisioning messages For standalone accounts, generate a random profile key. // FREEBIE * Special case for one-time sync of our profile key Android will use a contact sync message to sync a profile key from Android clients who have just upgraded and generated their profile key. Normally we should receive this data in a provisioning message. // FREEBIE * Infer profile sharing from synced data messages * Populate profile keys on outgoing messages Requires that `profileSharing` be set on the conversation. // FREEBIE * Support for the profile key update flag When receiving a message with this flag, don't init a message record, just process the profile key and move on. // FREEBIE * Display profile names in group member list * Refresh contact's profile on profile key changes // FREEBIE * Catch errors on profile save // FREEBIE * Save our own synced contact info Don't return early if we get a contact sync for our own number // FREEBIE
2017-09-11 16:50:35 +00:00
error.name = 'ProfileDecryptError';
throw error;
}
2018-07-21 21:51:20 +00:00
})
);
},
2018-07-21 21:51:20 +00:00
encryptProfileName(name, key) {
const padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH);
padded.set(new Uint8Array(name));
return textsecure.crypto.encryptProfile(padded.buffer, key);
},
2018-07-21 21:51:20 +00:00
decryptProfileName(encryptedProfileName, key) {
const data = dcodeIO.ByteBuffer.wrap(
encryptedProfileName,
'base64'
).toArrayBuffer();
2018-07-21 21:51:20 +00:00
return textsecure.crypto.decryptProfile(data, key).then(decrypted => {
// unpad
const padded = new Uint8Array(decrypted);
let i;
for (i = padded.length; i > 0; i -= 1) {
if (padded[i - 1] !== 0x00) {
break;
}
2018-07-21 21:51:20 +00:00
}
Profiles (#1453) * Add AES-GCM encryption for profiles With tests. * Add profileKey to DataMessage protobuf // FREEBIE * Decrypt and save profile names // FREEBIE * Save incoming profile keys * Move pad/unpad to crypto module // FREEBIE * Support fetching avatars from the cdn // FREEBIE * Translate failed authentication errors When AES-GCM authentication fails, webcrypto returns a very generic error. The same error is thrown for invalid length inputs, but our earlier checks in decryptProfile should rule out those failure modes and leave us safe to assume that we either had bad ciphertext or the wrong key. // FREEBIE * Handle profile avatars (wip) and log decrypt errors // FREEBIE * Display profile avatars Synced contact avatars will still override profile avatars. * Display profile names in convo list Only if we don't have a synced contact name. // FREEBIE * Make cdn url an environment config Use different ones for staging and production // FREEBIE * Display profile name in conversation header * Display profile name in group messages * Update conversation header if profile avatar changes // FREEBIE * Style profile names small with ~ * Save profileKeys from contact sync messages // FREEBIE * Save profile keys from provisioning messages For standalone accounts, generate a random profile key. // FREEBIE * Special case for one-time sync of our profile key Android will use a contact sync message to sync a profile key from Android clients who have just upgraded and generated their profile key. Normally we should receive this data in a provisioning message. // FREEBIE * Infer profile sharing from synced data messages * Populate profile keys on outgoing messages Requires that `profileSharing` be set on the conversation. // FREEBIE * Support for the profile key update flag When receiving a message with this flag, don't init a message record, just process the profile key and move on. // FREEBIE * Display profile names in group member list * Refresh contact's profile on profile key changes // FREEBIE * Catch errors on profile save // FREEBIE * Save our own synced contact info Don't return early if we get a contact sync for our own number // FREEBIE
2017-09-11 16:50:35 +00:00
2018-07-21 21:51:20 +00:00
return dcodeIO.ByteBuffer.wrap(padded)
.slice(0, i)
.toArrayBuffer();
});
},
2018-07-21 21:51:20 +00:00
getRandomBytes(size) {
return libsignal.crypto.getRandomBytes(size);
},
};
})();