Passive UUID support
Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
parent
f64ca0ed21
commit
a90246cbe5
49 changed files with 2226 additions and 776 deletions
|
@ -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}`;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -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, '');
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue