Passive UUID support

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
Ken Powers 2020-03-05 13:14:58 -08:00 committed by Scott Nonnenberg
parent f64ca0ed21
commit a90246cbe5
49 changed files with 2226 additions and 776 deletions

View file

@ -1118,8 +1118,14 @@ async function importConversations(dir, options) {
function getMessageKey(message) {
const ourNumber = textsecure.storage.user.getNumber();
const ourUuid = textsecure.storage.user.getUuid();
const source = message.source || ourNumber;
if (source === ourNumber) {
const sourceUuid = message.sourceUuid || ourUuid;
if (
(source && source === ourNumber) ||
(sourceUuid && sourceUuid === ourUuid)
) {
return `${source} ${message.timestamp}`;
}

View file

@ -1,4 +1,4 @@
/* global window, setTimeout, IDBKeyRange */
/* global window, setTimeout, IDBKeyRange, ConversationController */
const electron = require('electron');
@ -84,10 +84,10 @@ module.exports = {
createOrUpdateSession,
createOrUpdateSessions,
getSessionById,
getSessionsByNumber,
getSessionsById,
bulkAddSessions,
removeSessionById,
removeSessionsByNumber,
removeSessionsById,
removeAllSessions,
getAllSessions,
@ -431,10 +431,14 @@ async function removeIndexedDBFiles() {
const IDENTITY_KEY_KEYS = ['publicKey'];
async function createOrUpdateIdentityKey(data) {
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data);
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, {
...data,
id: ConversationController.getConversationId(data.id),
});
await channels.createOrUpdateIdentityKey(updated);
}
async function getIdentityKeyById(id) {
async function getIdentityKeyById(identifier) {
const id = ConversationController.getConversationId(identifier);
const data = await channels.getIdentityKeyById(id);
return keysToArrayBuffer(IDENTITY_KEY_KEYS, data);
}
@ -444,7 +448,8 @@ async function bulkAddIdentityKeys(array) {
);
await channels.bulkAddIdentityKeys(updated);
}
async function removeIdentityKeyById(id) {
async function removeIdentityKeyById(identifier) {
const id = ConversationController.getConversationId(identifier);
await channels.removeIdentityKeyById(id);
}
async function removeAllIdentityKeys() {
@ -515,6 +520,11 @@ const ITEM_KEYS = {
'value.signature',
'value.serialized',
],
senderCertificateWithUuid: [
'value.certificate',
'value.signature',
'value.serialized',
],
signaling_key: ['value'],
profileKey: ['value'],
};
@ -572,8 +582,8 @@ async function getSessionById(id) {
const session = await channels.getSessionById(id);
return session;
}
async function getSessionsByNumber(number) {
const sessions = await channels.getSessionsByNumber(number);
async function getSessionsById(id) {
const sessions = await channels.getSessionsById(id);
return sessions;
}
async function bulkAddSessions(array) {
@ -582,8 +592,8 @@ async function bulkAddSessions(array) {
async function removeSessionById(id) {
await channels.removeSessionById(id);
}
async function removeSessionsByNumber(number) {
await channels.removeSessionsByNumber(number);
async function removeSessionsById(id) {
await channels.removeSessionsById(id);
}
async function removeAllSessions(id) {
await channels.removeAllSessions(id);
@ -799,11 +809,12 @@ async function getAllMessageIds() {
async function getMessageBySender(
// eslint-disable-next-line camelcase
{ source, sourceDevice, sent_at },
{ source, sourceUuid, sourceDevice, sent_at },
{ Message }
) {
const messages = await channels.getMessageBySender({
source,
sourceUuid,
sourceDevice,
sent_at,
});

View file

@ -117,13 +117,14 @@ function _createSenderCertificateFromBuffer(serialized) {
!certificate.identityKey ||
!certificate.senderDevice ||
!certificate.expires ||
!certificate.sender
!(certificate.sender || certificate.senderUuid)
) {
throw new Error('Missing fields');
}
return {
sender: certificate.sender,
senderUuid: certificate.senderUuid,
senderDevice: certificate.senderDevice,
expires: certificate.expires.toNumber(),
identityKey: certificate.identityKey.toArrayBuffer(),
@ -344,7 +345,7 @@ SecretSessionCipher.prototype = {
// public Pair<SignalProtocolAddress, byte[]> decrypt(
// CertificateValidator validator, byte[] ciphertext, long timestamp)
async decrypt(validator, ciphertext, timestamp, me) {
async decrypt(validator, ciphertext, timestamp, me = {}) {
// Capture this.xxx variables to replicate Java's implicit this syntax
const signalProtocolStore = this.storage;
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
@ -401,18 +402,29 @@ SecretSessionCipher.prototype = {
);
}
const { sender, senderDevice } = content.senderCertificate;
const { number, deviceId } = me || {};
if (sender === number && senderDevice === deviceId) {
const { sender, senderUuid, senderDevice } = content.senderCertificate;
if (
((sender && me.number && sender === me.number) ||
(senderUuid && me.uuid && senderUuid === me.uuid)) &&
senderDevice === me.deviceId
) {
return {
isMe: true,
};
}
const address = new libsignal.SignalProtocolAddress(sender, senderDevice);
const addressE164 =
sender && new libsignal.SignalProtocolAddress(sender, senderDevice);
const addressUuid =
senderUuid &&
new libsignal.SignalProtocolAddress(
senderUuid.toLowerCase(),
senderDevice
);
try {
return {
sender: address,
sender: addressE164,
senderUuid: addressUuid,
content: await _decryptWithUnidentifiedSenderMessage(content),
};
} catch (error) {
@ -421,7 +433,8 @@ SecretSessionCipher.prototype = {
error = new Error('Decryption error was falsey!');
}
error.sender = address;
error.sender = addressE164;
error.senderUuid = addressUuid;
throw error;
}
@ -504,7 +517,7 @@ SecretSessionCipher.prototype = {
const signalProtocolStore = this.storage;
const sender = new libsignal.SignalProtocolAddress(
message.senderCertificate.sender,
message.senderCertificate.sender || message.senderCertificate.senderUuid,
message.senderCertificate.senderDevice
);

View file

@ -8,6 +8,7 @@ const { escapeRegExp } = require('lodash');
const APP_ROOT_PATH = path.join(__dirname, '..', '..', '..');
const PHONE_NUMBER_PATTERN = /\+\d{7,12}(\d{3})/g;
const UUID_PATTERN = /[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{10}([0-9A-F]{2})/gi;
const GROUP_ID_PATTERN = /(group\()([^)]+)(\))/g;
const REDACTION_PLACEHOLDER = '[REDACTED]';
@ -64,6 +65,15 @@ exports.redactPhoneNumbers = text => {
return text.replace(PHONE_NUMBER_PATTERN, `+${REDACTION_PLACEHOLDER}$1`);
};
// redactUuids :: String -> String
exports.redactUuids = text => {
if (!is.string(text)) {
throw new TypeError("'text' must be a string");
}
return text.replace(UUID_PATTERN, `${REDACTION_PLACEHOLDER}$1`);
};
// redactGroupIds :: String -> String
exports.redactGroupIds = text => {
if (!is.string(text)) {
@ -84,7 +94,8 @@ exports.redactSensitivePaths = exports._redactPath(APP_ROOT_PATH);
exports.redactAll = compose(
exports.redactSensitivePaths,
exports.redactGroupIds,
exports.redactPhoneNumbers
exports.redactPhoneNumbers,
exports.redactUuids
);
const removeNewlines = text => text.replace(/\r?\n|\r/g, '');

View file

@ -16,7 +16,15 @@ let scheduleNext = null;
function refreshOurProfile() {
window.log.info('refreshOurProfile');
const ourNumber = textsecure.storage.user.getNumber();
const conversation = ConversationController.getOrCreate(ourNumber, 'private');
const ourUuid = textsecure.storage.user.getUuid();
const conversation = ConversationController.getOrCreate(
// This is explicitly ourNumber first in order to avoid creating new
// conversations when an old one exists
ourNumber || ourUuid,
'private'
);
conversation.updateUuid(ourUuid);
conversation.updateE164(ourNumber);
conversation.getProfiles();
}
@ -66,21 +74,36 @@ function initialize({ events, storage, navigator, logger }) {
async function run() {
logger.info('refreshSenderCertificate: Getting new certificate...');
try {
const username = storage.get('number_id');
const password = storage.get('password');
const server = WebAPI.connect({ username, password });
const OLD_USERNAME = storage.get('number_id');
const USERNAME = storage.get('uuid_id');
const PASSWORD = storage.get('password');
const server = WebAPI.connect({
username: USERNAME || OLD_USERNAME,
password: PASSWORD,
});
const { certificate } = await server.getSenderCertificate();
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(certificate);
const decoded = textsecure.protobuf.SenderCertificate.decode(arrayBuffer);
await Promise.all(
[false, true].map(async withUuid => {
const { certificate } = await server.getSenderCertificate(withUuid);
const arrayBuffer = window.Signal.Crypto.base64ToArrayBuffer(
certificate
);
const decoded = textsecure.protobuf.SenderCertificate.decode(
arrayBuffer
);
decoded.certificate = decoded.certificate.toArrayBuffer();
decoded.signature = decoded.signature.toArrayBuffer();
decoded.serialized = arrayBuffer;
decoded.certificate = decoded.certificate.toArrayBuffer();
decoded.signature = decoded.signature.toArrayBuffer();
decoded.serialized = arrayBuffer;
storage.put(
`senderCertificate${withUuid ? 'WithUuid' : ''}`,
decoded
);
})
);
storage.put('senderCertificate', decoded);
scheduledTime = null;
scheduleNextRotation();
} catch (error) {
logger.error(

View file

@ -394,12 +394,14 @@ const URL_CALLS = {
attachmentId: 'v2/attachments/form/upload',
deliveryCert: 'v1/certificate/delivery',
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
registerCapabilities: 'v1/devices/capabilities',
devices: 'v1/devices',
keys: 'v2/keys',
messages: 'v1/messages',
profile: 'v1/profile',
signed: 'v2/keys/signed',
getStickerPackUpload: 'v1/sticker/pack/form',
whoami: 'v1/accounts/whoami',
};
module.exports = {
@ -451,8 +453,8 @@ function initialize({
getAttachment,
getAvatar,
getDevices,
getKeysForNumber,
getKeysForNumberUnauth,
getKeysForIdentifier,
getKeysForIdentifierUnauth,
getMessageSocket,
getMyKeys,
getProfile,
@ -463,6 +465,7 @@ function initialize({
getStickerPackManifest,
makeProxiedRequest,
putAttachment,
registerCapabilities,
putStickers,
registerKeys,
registerSupportForUnauthenticatedDelivery,
@ -473,6 +476,7 @@ function initialize({
sendMessagesUnauth,
setSignedPreKey,
updateDeviceName,
whoami,
};
function _ajax(param) {
@ -535,12 +539,21 @@ function initialize({
});
}
function getSenderCertificate() {
function whoami() {
return _ajax({
call: 'whoami',
httpType: 'GET',
responseType: 'json',
});
}
function getSenderCertificate(withUuid = false) {
return _ajax({
call: 'deliveryCert',
httpType: 'GET',
responseType: 'json',
schema: { certificate: 'string' },
urlParameters: withUuid ? '?includeUuid=true' : undefined,
});
}
@ -552,19 +565,27 @@ function initialize({
});
}
function getProfile(number) {
function registerCapabilities(capabilities) {
return _ajax({
call: 'registerCapabilities',
httpType: 'PUT',
jsonData: { capabilities },
});
}
function getProfile(identifier) {
return _ajax({
call: 'profile',
httpType: 'GET',
urlParameters: `/${number}`,
urlParameters: `/${identifier}`,
responseType: 'json',
});
}
function getProfileUnauth(number, { accessKey } = {}) {
function getProfileUnauth(identifier, { accessKey } = {}) {
return _ajax({
call: 'profile',
httpType: 'GET',
urlParameters: `/${number}`,
urlParameters: `/${identifier}`,
responseType: 'json',
unauthenticated: true,
accessKey,
@ -623,17 +644,17 @@ function initialize({
let call;
let urlPrefix;
let schema;
let responseType;
if (deviceName) {
jsonData.name = deviceName;
call = 'devices';
urlPrefix = '/';
schema = { deviceId: 'number' };
responseType = 'json';
} else {
call = 'accounts';
urlPrefix = '/code/';
jsonData.capabilities = {
uuid: true,
};
}
// We update our saved username and password, since we're creating a new account
@ -643,14 +664,14 @@ function initialize({
const response = await _ajax({
call,
httpType: 'PUT',
responseType: 'json',
urlParameters: urlPrefix + code,
jsonData,
responseType,
validateResponse: schema,
});
// From here on out, our username will be our phone number combined with device
username = `${number}.${response.deviceId || 1}`;
// From here on out, our username will be our UUID or E164 combined with device
username = `${response.uuid || number}.${response.deviceId || 1}`;
return response;
}
@ -768,25 +789,25 @@ function initialize({
return res;
}
function getKeysForNumber(number, deviceId = '*') {
function getKeysForIdentifier(identifier, deviceId = '*') {
return _ajax({
call: 'keys',
httpType: 'GET',
urlParameters: `/${number}/${deviceId}`,
urlParameters: `/${identifier}/${deviceId}`,
responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' },
}).then(handleKeys);
}
function getKeysForNumberUnauth(
number,
function getKeysForIdentifierUnauth(
identifier,
deviceId = '*',
{ accessKey } = {}
) {
return _ajax({
call: 'keys',
httpType: 'GET',
urlParameters: `/${number}/${deviceId}`,
urlParameters: `/${identifier}/${deviceId}`,
responseType: 'json',
validateResponse: { identityKey: 'string', devices: 'object' },
unauthenticated: true,