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
|
@ -147,6 +147,24 @@
|
|||
},
|
||||
});
|
||||
|
||||
async function normalizeEncodedAddress(encodedAddress) {
|
||||
const [identifier, deviceId] = textsecure.utils.unencodeNumber(
|
||||
encodedAddress
|
||||
);
|
||||
try {
|
||||
const conv = await ConversationController.getOrCreateAndWait(
|
||||
identifier,
|
||||
'private'
|
||||
);
|
||||
return `${conv.get('id')}.${deviceId}`;
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`could not get conversation for identifier ${identifier}`
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function SignalProtocolStore() {
|
||||
this.sessionUpdateBatcher = window.Signal.Util.createBatcher({
|
||||
wait: 500,
|
||||
|
@ -322,66 +340,98 @@
|
|||
|
||||
// Sessions
|
||||
|
||||
async loadSession(encodedNumber) {
|
||||
if (encodedNumber === null || encodedNumber === undefined) {
|
||||
async loadSession(encodedAddress) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to get session for undefined/null number');
|
||||
}
|
||||
|
||||
const session = this.sessions[encodedNumber];
|
||||
if (session) {
|
||||
return session.record;
|
||||
try {
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
const session = this.sessions[id];
|
||||
|
||||
if (session) {
|
||||
return session.record;
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.error(`could not load session ${encodedAddress}`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
async storeSession(encodedNumber, record) {
|
||||
if (encodedNumber === null || encodedNumber === undefined) {
|
||||
async storeSession(encodedAddress, record) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to put session for undefined/null number');
|
||||
}
|
||||
const unencoded = textsecure.utils.unencodeNumber(encodedNumber);
|
||||
const number = unencoded[0];
|
||||
const unencoded = textsecure.utils.unencodeNumber(encodedAddress);
|
||||
const deviceId = parseInt(unencoded[1], 10);
|
||||
|
||||
const data = {
|
||||
id: encodedNumber,
|
||||
number,
|
||||
deviceId,
|
||||
record,
|
||||
};
|
||||
try {
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
|
||||
this.sessions[encodedNumber] = data;
|
||||
const data = {
|
||||
id,
|
||||
conversationId: textsecure.utils.unencodeNumber(id)[0],
|
||||
deviceId,
|
||||
record,
|
||||
};
|
||||
|
||||
// Note: Because these are cached in memory, we batch and make these database
|
||||
// updates out of band.
|
||||
this.sessionUpdateBatcher.add(data);
|
||||
this.sessions[id] = data;
|
||||
|
||||
// Note: Because these are cached in memory, we batch and make these database
|
||||
// updates out of band.
|
||||
this.sessionUpdateBatcher.add(data);
|
||||
} catch (e) {
|
||||
window.log.error(`could not store session for ${encodedAddress}`);
|
||||
}
|
||||
},
|
||||
async getDeviceIds(number) {
|
||||
if (number === null || number === undefined) {
|
||||
async getDeviceIds(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get device ids for undefined/null number');
|
||||
}
|
||||
|
||||
const allSessions = Object.values(this.sessions);
|
||||
const sessions = allSessions.filter(session => session.number === number);
|
||||
return _.pluck(sessions, 'deviceId');
|
||||
try {
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
const allSessions = Object.values(this.sessions);
|
||||
const sessions = allSessions.filter(
|
||||
session => session.conversationId === id
|
||||
);
|
||||
|
||||
return _.pluck(sessions, 'deviceId');
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`could not get device ids for identifier ${identifier}`
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
async removeSession(encodedNumber) {
|
||||
window.log.info('deleting session for ', encodedNumber);
|
||||
delete this.sessions[encodedNumber];
|
||||
await window.Signal.Data.removeSessionById(encodedNumber);
|
||||
async removeSession(encodedAddress) {
|
||||
window.log.info('deleting session for ', encodedAddress);
|
||||
try {
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
delete this.sessions[id];
|
||||
await window.Signal.Data.removeSessionById(id);
|
||||
} catch (e) {
|
||||
window.log.error(`could not delete session for ${encodedAddress}`);
|
||||
}
|
||||
},
|
||||
async removeAllSessions(number) {
|
||||
if (number === null || number === undefined) {
|
||||
async removeAllSessions(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to remove sessions for undefined/null number');
|
||||
}
|
||||
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
|
||||
const allSessions = Object.values(this.sessions);
|
||||
|
||||
for (let i = 0, max = allSessions.length; i < max; i += 1) {
|
||||
const session = allSessions[i];
|
||||
if (session.number === number) {
|
||||
if (session.conversationId === id) {
|
||||
delete this.sessions[session.id];
|
||||
}
|
||||
}
|
||||
await window.Signal.Data.removeSessionsByNumber(number);
|
||||
|
||||
await window.Signal.Data.removeSessionsById(identifier);
|
||||
},
|
||||
async archiveSiblingSessions(identifier) {
|
||||
const address = libsignal.SignalProtocolAddress.fromString(identifier);
|
||||
|
@ -404,12 +454,15 @@
|
|||
})
|
||||
);
|
||||
},
|
||||
async archiveAllSessions(number) {
|
||||
const deviceIds = await this.getDeviceIds(number);
|
||||
async archiveAllSessions(identifier) {
|
||||
const deviceIds = await this.getDeviceIds(identifier);
|
||||
|
||||
await Promise.all(
|
||||
deviceIds.map(async deviceId => {
|
||||
const address = new libsignal.SignalProtocolAddress(number, deviceId);
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
identifier,
|
||||
deviceId
|
||||
);
|
||||
window.log.info('closing session for', address.toString());
|
||||
const sessionCipher = new libsignal.SessionCipher(
|
||||
textsecure.storage.protocol,
|
||||
|
@ -426,16 +479,35 @@
|
|||
|
||||
// Identity Keys
|
||||
|
||||
async isTrustedIdentity(identifier, publicKey, direction) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
getIdentityRecord(identifier) {
|
||||
try {
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
const record = this.identityKeys[id];
|
||||
|
||||
if (record) {
|
||||
return record;
|
||||
}
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`could not get identity record for identifier ${identifier}`
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
|
||||
async isTrustedIdentity(encodedAddress, publicKey, direction) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const isOurNumber = number === textsecure.storage.user.getNumber();
|
||||
const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
|
||||
const isOurIdentifier =
|
||||
identifier === textsecure.storage.user.getNumber() ||
|
||||
identifier === textsecure.storage.user.getUuid();
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
|
||||
if (isOurNumber) {
|
||||
if (isOurIdentifier) {
|
||||
const existing = identityRecord ? identityRecord.publicKey : null;
|
||||
return equalArrayBuffers(existing, publicKey);
|
||||
}
|
||||
|
@ -482,8 +554,8 @@
|
|||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const id = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.getIdentityRecord(id);
|
||||
|
||||
if (identityRecord) {
|
||||
return identityRecord.publicKey;
|
||||
|
@ -496,8 +568,8 @@
|
|||
this.identityKeys[id] = data;
|
||||
await window.Signal.Data.createOrUpdateIdentityKey(data);
|
||||
},
|
||||
async saveIdentity(identifier, publicKey, nonblockingApproval) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
async saveIdentity(encodedAddress, publicKey, nonblockingApproval) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
if (!(publicKey instanceof ArrayBuffer)) {
|
||||
|
@ -509,14 +581,15 @@
|
|||
nonblockingApproval = false;
|
||||
}
|
||||
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identifer = textsecure.utils.unencodeNumber(encodedAddress)[0];
|
||||
const identityRecord = this.getIdentityRecord(identifer);
|
||||
const id = ConversationController.getConversationId(identifer);
|
||||
|
||||
if (!identityRecord || !identityRecord.publicKey) {
|
||||
// Lookup failed, or the current key was removed, so save this one.
|
||||
window.log.info('Saving new identity...');
|
||||
await this._saveIdentityKey({
|
||||
id: number,
|
||||
id,
|
||||
publicKey,
|
||||
firstUse: true,
|
||||
timestamp: Date.now(),
|
||||
|
@ -542,7 +615,7 @@
|
|||
}
|
||||
|
||||
await this._saveIdentityKey({
|
||||
id: number,
|
||||
id,
|
||||
publicKey,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
|
@ -551,14 +624,14 @@
|
|||
});
|
||||
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
this.trigger('keychange', identifer);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'saveIdentity error triggering keychange:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
}
|
||||
await this.archiveSiblingSessions(identifier);
|
||||
await this.archiveSiblingSessions(encodedAddress);
|
||||
|
||||
return true;
|
||||
} else if (this.isNonBlockingApprovalRequired(identityRecord)) {
|
||||
|
@ -579,16 +652,21 @@
|
|||
!identityRecord.nonblockingApproval
|
||||
);
|
||||
},
|
||||
async saveIdentityWithAttributes(identifier, attributes) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
async saveIdentityWithAttributes(encodedAddress, attributes) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
const conv = await ConversationController.getOrCreateAndWait(
|
||||
identifier,
|
||||
'private'
|
||||
);
|
||||
const id = conv.get('id');
|
||||
|
||||
const updates = {
|
||||
id: number,
|
||||
id,
|
||||
...identityRecord,
|
||||
...attributes,
|
||||
};
|
||||
|
@ -600,26 +678,26 @@
|
|||
throw model.validationError;
|
||||
}
|
||||
},
|
||||
async setApproval(identifier, nonblockingApproval) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
async setApproval(encodedAddress, nonblockingApproval) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to set approval for undefined/null identifier');
|
||||
}
|
||||
if (typeof nonblockingApproval !== 'boolean') {
|
||||
throw new Error('Invalid approval status');
|
||||
}
|
||||
|
||||
const number = textsecure.utils.unencodeNumber(identifier)[0];
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identifier = textsecure.utils.unencodeNumber(encodedAddress)[0];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
throw new Error(`No identity record for ${identifier}`);
|
||||
}
|
||||
|
||||
identityRecord.nonblockingApproval = nonblockingApproval;
|
||||
await this._saveIdentityKey(identityRecord);
|
||||
},
|
||||
async setVerified(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
async setVerified(encodedAddress, verifiedStatus, publicKey) {
|
||||
if (encodedAddress === null || encodedAddress === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (!validateVerifiedStatus(verifiedStatus)) {
|
||||
|
@ -629,9 +707,10 @@
|
|||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(encodedAddress);
|
||||
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
throw new Error(`No identity record for ${encodedAddress}`);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -650,14 +729,14 @@
|
|||
window.log.info('No identity record for specified publicKey');
|
||||
}
|
||||
},
|
||||
async getVerified(number) {
|
||||
if (number === null || number === undefined) {
|
||||
async getVerified(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
throw new Error(`No identity record for ${identifier}`);
|
||||
}
|
||||
|
||||
const verifiedStatus = identityRecord.verified;
|
||||
|
@ -681,15 +760,16 @@
|
|||
// This function encapsulates the non-Java behavior, since the mobile apps don't
|
||||
// currently receive contact syncs and therefore will see a verify sync with
|
||||
// UNVERIFIED status
|
||||
async processUnverifiedMessage(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
async processUnverifiedMessage(identifier, verifiedStatus, publicKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (publicKey !== undefined && !(publicKey instanceof ArrayBuffer)) {
|
||||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
|
||||
const isPresent = Boolean(identityRecord);
|
||||
let isEqual = false;
|
||||
|
||||
|
@ -703,7 +783,7 @@
|
|||
identityRecord.verified !== VerifiedStatus.UNVERIFIED
|
||||
) {
|
||||
await textsecure.storage.protocol.setVerified(
|
||||
number,
|
||||
identifier,
|
||||
verifiedStatus,
|
||||
publicKey
|
||||
);
|
||||
|
@ -711,17 +791,20 @@
|
|||
}
|
||||
|
||||
if (!isPresent || !isEqual) {
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(
|
||||
identifier,
|
||||
{
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (isPresent && !isEqual) {
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
this.trigger('keychange', identifier);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'processUnverifiedMessage error triggering keychange:',
|
||||
|
@ -729,7 +812,7 @@
|
|||
);
|
||||
}
|
||||
|
||||
await this.archiveAllSessions(number);
|
||||
await this.archiveAllSessions(identifier);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -743,8 +826,8 @@
|
|||
},
|
||||
// This matches the Java method as of
|
||||
// https://github.com/signalapp/Signal-Android/blob/d0bb68e1378f689e4d10ac6a46014164992ca4e4/src/org/thoughtcrime/securesms/util/IdentityUtil.java#L188
|
||||
async processVerifiedMessage(number, verifiedStatus, publicKey) {
|
||||
if (number === null || number === undefined) {
|
||||
async processVerifiedMessage(identifier, verifiedStatus, publicKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
if (!validateVerifiedStatus(verifiedStatus)) {
|
||||
|
@ -754,7 +837,7 @@
|
|||
throw new Error('Invalid public key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
|
||||
const isPresent = Boolean(identityRecord);
|
||||
let isEqual = false;
|
||||
|
@ -775,7 +858,7 @@
|
|||
verifiedStatus === VerifiedStatus.DEFAULT
|
||||
) {
|
||||
await textsecure.storage.protocol.setVerified(
|
||||
number,
|
||||
identifier,
|
||||
verifiedStatus,
|
||||
publicKey
|
||||
);
|
||||
|
@ -788,17 +871,20 @@
|
|||
(isPresent && !isEqual) ||
|
||||
(isPresent && identityRecord.verified !== VerifiedStatus.VERIFIED))
|
||||
) {
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(number, {
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
});
|
||||
await textsecure.storage.protocol.saveIdentityWithAttributes(
|
||||
identifier,
|
||||
{
|
||||
publicKey,
|
||||
verified: verifiedStatus,
|
||||
firstUse: false,
|
||||
timestamp: Date.now(),
|
||||
nonblockingApproval: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (isPresent && !isEqual) {
|
||||
try {
|
||||
this.trigger('keychange', number);
|
||||
this.trigger('keychange', identifier);
|
||||
} catch (error) {
|
||||
window.log.error(
|
||||
'processVerifiedMessage error triggering keychange:',
|
||||
|
@ -806,7 +892,7 @@
|
|||
);
|
||||
}
|
||||
|
||||
await this.archiveAllSessions(number);
|
||||
await this.archiveAllSessions(identifier);
|
||||
|
||||
// true signifies that we overwrote a previous key with a new one
|
||||
return true;
|
||||
|
@ -818,14 +904,14 @@
|
|||
// state we had before.
|
||||
return false;
|
||||
},
|
||||
async isUntrusted(number) {
|
||||
if (number === null || number === undefined) {
|
||||
async isUntrusted(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to set verified for undefined/null key');
|
||||
}
|
||||
|
||||
const identityRecord = this.identityKeys[number];
|
||||
const identityRecord = this.getIdentityRecord(identifier);
|
||||
if (!identityRecord) {
|
||||
throw new Error(`No identity record for ${number}`);
|
||||
throw new Error(`No identity record for ${identifier}`);
|
||||
}
|
||||
|
||||
if (
|
||||
|
@ -838,10 +924,13 @@
|
|||
|
||||
return false;
|
||||
},
|
||||
async removeIdentityKey(number) {
|
||||
delete this.identityKeys[number];
|
||||
await window.Signal.Data.removeIdentityKeyById(number);
|
||||
await textsecure.storage.protocol.removeAllSessions(number);
|
||||
async removeIdentityKey(identifier) {
|
||||
const id = ConversationController.getConversationId(identifier);
|
||||
if (id) {
|
||||
delete this.identityKeys[id];
|
||||
await window.Signal.Data.removeIdentityKeyById(id);
|
||||
await textsecure.storage.protocol.removeAllSessions(id);
|
||||
}
|
||||
},
|
||||
|
||||
// Not yet processed messages - for resiliency
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue