UUID-keyed lookups in SignalProtocolStore

This commit is contained in:
Fedor Indutny 2021-09-09 19:38:11 -07:00 committed by GitHub
parent 6323aedd9b
commit c7e7d55af4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 2094 additions and 1447 deletions

View file

@ -309,7 +309,6 @@
type="text/javascript"
src="js/rotate_signed_prekey_listener.js"
></script>
<script type="text/javascript" src="js/keychange_listener.js"></script>
</head>
<body class="overflow-hidden">
<div id="app-container">

View file

@ -1,34 +0,0 @@
// Copyright 2017-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global Whisper, SignalProtocolStore, ConversationController, _ */
/* eslint-disable more/no-then */
// eslint-disable-next-line func-names
(function () {
window.Whisper = window.Whisper || {};
Whisper.KeyChangeListener = {
init(signalProtocolStore) {
if (!(signalProtocolStore instanceof SignalProtocolStore)) {
throw new Error('KeyChangeListener requires a SignalProtocolStore');
}
signalProtocolStore.on('keychange', async identifier => {
const conversation = await ConversationController.getOrCreateAndWait(
identifier,
'private'
);
conversation.addKeyChange(identifier);
const groups = await ConversationController.getAllGroupsInvolvingId(
conversation.id
);
_.forEach(groups, group => {
group.addKeyChange(identifier);
});
});
},
};
})();

View file

@ -142,6 +142,9 @@ const Errors = require('../../ts/types/errors');
const MessageType = require('./types/message');
const MIME = require('../../ts/types/MIME');
const SettingsType = require('../../ts/types/Settings');
const { UUID } = require('../../ts/types/UUID');
const { Address } = require('../../ts/types/Address');
const { QualifiedAddress } = require('../../ts/types/QualifiedAddress');
// Views
const Initialization = require('./views/initialization');
@ -430,6 +433,9 @@ exports.setup = (options = {}) => {
MIME,
Settings: SettingsType,
VisualAttachment,
UUID,
Address,
QualifiedAddress,
};
const Views = {

View file

@ -1,158 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global textsecure */
describe('Key generation', function thisNeeded() {
const count = 10;
this.timeout(count * 2000);
function validateStoredKeyPair(keyPair) {
/* Ensure the keypair matches the format used internally by libsignal-protocol */
assert.isObject(keyPair, 'Stored keyPair is not an object');
assert.instanceOf(keyPair.pubKey, ArrayBuffer);
assert.instanceOf(keyPair.privKey, ArrayBuffer);
assert.strictEqual(keyPair.pubKey.byteLength, 33);
assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5);
assert.strictEqual(keyPair.privKey.byteLength, 32);
}
function itStoresPreKey(keyId) {
it(`prekey ${keyId} is valid`, () =>
textsecure.storage.protocol.loadPreKey(keyId).then(keyPair => {
validateStoredKeyPair(keyPair);
}));
}
function itStoresSignedPreKey(keyId) {
it(`signed prekey ${keyId} is valid`, () =>
textsecure.storage.protocol.loadSignedPreKey(keyId).then(keyPair => {
validateStoredKeyPair(keyPair);
}));
}
function validateResultKey(resultKey) {
return textsecure.storage.protocol
.loadPreKey(resultKey.keyId)
.then(keyPair => {
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
});
}
function validateResultSignedKey(resultSignedKey) {
return textsecure.storage.protocol
.loadSignedPreKey(resultSignedKey.keyId)
.then(keyPair => {
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
});
}
before(async () => {
localStorage.clear();
const keyPair = window.Signal.Curve.generateKeyPair();
await textsecure.storage.protocol.put('identityKey', keyPair);
});
describe('the first time', () => {
let result;
/* result should have this format
* {
* preKeys: [ { keyId, publicKey }, ... ],
* signedPreKey: { keyId, publicKey, signature },
* identityKey: <ArrayBuffer>
* }
*/
before(() => {
const accountManager = new textsecure.AccountManager('');
return accountManager.generateKeys(count).then(res => {
result = res;
});
});
for (let i = 1; i <= count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 0; i < count; i += 1) {
assert.strictEqual(result.preKeys[i].keyId, i + 1);
}
});
it('result contains the correct public keys', () =>
Promise.all(result.preKeys.map(validateResultKey)));
it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
describe('the second time', () => {
let result;
before(() => {
const accountManager = new textsecure.AccountManager('');
return accountManager.generateKeys(count).then(res => {
result = res;
});
});
for (let i = 1; i <= 2 * count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 1; i <= count; i += 1) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
}
});
it('result contains the correct public keys', () =>
Promise.all(result.preKeys.map(validateResultKey)));
it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
describe('the third time', () => {
let result;
before(() => {
const accountManager = new textsecure.AccountManager('');
return accountManager.generateKeys(count).then(res => {
result = res;
});
});
for (let i = 1; i <= 3 * count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 1; i <= count; i += 1) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
}
});
it('result contains the correct public keys', () =>
Promise.all(result.preKeys.map(validateResultKey)));
it('result contains a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
});

View file

@ -1,179 +0,0 @@
// Copyright 2016-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
function SignalProtocolStore() {
this.store = {};
}
SignalProtocolStore.prototype = {
VerifiedStatus: {
DEFAULT: 0,
VERIFIED: 1,
UNVERIFIED: 2,
},
getIdentityKeyPair() {
return Promise.resolve(this.get('identityKey'));
},
getLocalRegistrationId() {
return Promise.resolve(this.get('registrationId'));
},
put(key, value) {
if (
key === undefined ||
value === undefined ||
key === null ||
value === null
) {
throw new Error('Tried to store undefined/null');
}
this.store[key] = value;
},
get(key, defaultValue) {
if (key === null || key === undefined) {
throw new Error('Tried to get value for undefined/null key');
}
if (key in this.store) {
return this.store[key];
}
return defaultValue;
},
remove(key) {
if (key === null || key === undefined) {
throw new Error('Tried to remove value for undefined/null key');
}
delete this.store[key];
},
isTrustedIdentity(identifier, identityKey) {
if (identifier === null || identifier === undefined) {
throw new Error('tried to check identity key for undefined/null key');
}
if (!(identityKey instanceof ArrayBuffer)) {
throw new Error('Expected identityKey to be an ArrayBuffer');
}
const trusted = this.get(`identityKey${identifier}`);
if (trusted === undefined) {
return Promise.resolve(true);
}
return Promise.resolve(identityKey === trusted);
},
loadIdentityKey(identifier) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key');
}
return new Promise(resolve => {
resolve(this.get(`identityKey${identifier}`));
});
},
saveIdentity(identifier, identityKey) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to put identity key for undefined/null key');
}
return new Promise(resolve => {
const existing = this.get(`identityKey${identifier}`);
this.put(`identityKey${identifier}`, identityKey);
if (existing && existing !== identityKey) {
resolve(true);
} else {
resolve(false);
}
});
},
/* Returns a prekeypair object or undefined */
loadPreKey(keyId) {
return new Promise(resolve => {
const res = this.get(`25519KeypreKey${keyId}`);
resolve(res);
});
},
storePreKey(keyId, keyPair) {
return new Promise(resolve => {
resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
});
},
removePreKey(keyId) {
return new Promise(resolve => {
resolve(this.remove(`25519KeypreKey${keyId}`));
});
},
/* Returns a signed keypair object or undefined */
loadSignedPreKey(keyId) {
return new Promise(resolve => {
const res = this.get(`25519KeysignedKey${keyId}`);
resolve(res);
});
},
loadSignedPreKeys() {
return new Promise(resolve => {
const res = [];
const keys = Object.keys(this.store);
for (let i = 0, max = keys.length; i < max; i += 1) {
const key = keys[i];
if (key.startsWith('25519KeysignedKey')) {
res.push(this.store[key]);
}
}
resolve(res);
});
},
storeSignedPreKey(keyId, keyPair) {
return new Promise(resolve => {
resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
});
},
removeSignedPreKey(keyId) {
return new Promise(resolve => {
resolve(this.remove(`25519KeysignedKey${keyId}`));
});
},
loadSession(identifier) {
return new Promise(resolve => {
resolve(this.get(`session${identifier}`));
});
},
storeSession(identifier, record) {
return new Promise(resolve => {
resolve(this.put(`session${identifier}`, record));
});
},
removeAllSessions(identifier) {
return new Promise(resolve => {
const keys = Object.keys(this.store);
for (let i = 0, max = keys.length; i < max; i += 1) {
const key = keys[i];
if (key.match(RegExp(`^session${identifier.replace('+', '\\+')}.+`))) {
delete this.store[key];
}
}
resolve();
});
},
getDeviceIds(identifier) {
return new Promise(resolve => {
const deviceIds = [];
const keys = Object.keys(this.store);
for (let i = 0, max = keys.length; i < max; i += 1) {
const key = keys[i];
if (key.match(RegExp(`^session${identifier.replace('+', '\\+')}.+`))) {
deviceIds.push(parseInt(key.split('.')[1], 10));
}
}
resolve(deviceIds);
});
},
getUnprocessedCount: () => Promise.resolve(0),
getAllUnprocessed: () => Promise.resolve([]),
getUnprocessedById: () => Promise.resolve(null),
addUnprocessed: () => Promise.resolve(),
addMultipleUnprocessed: () => Promise.resolve(),
updateUnprocessedAttempts: () => Promise.resolve(),
updateUnprocessedWithData: () => Promise.resolve(),
updateUnprocessedsWithData: () => Promise.resolve(),
removeUnprocessed: () => Promise.resolve(),
removeAllUnprocessed: () => Promise.resolve(),
};

View file

@ -14,10 +14,6 @@
<script type="text/javascript" src="fake_web_api.js"></script>
<script type="text/javascript" src="test.js"></script>
<script
type="text/javascript"
src="in_memory_signal_protocol_store.js"
></script>
<script
type="text/javascript"
@ -36,7 +32,6 @@
></script>
<script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="generate_keys_test.js"></script>
<script type="text/javascript" src="task_with_timeout_test.js"></script>
<script type="text/javascript" src="account_manager_test.js"></script>
<script type="text/javascript" src="sendmessage_test.js"></script>

View file

@ -1,142 +0,0 @@
// Copyright 2015-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global textsecure, storage, ConversationController */
describe('SignalProtocolStore', () => {
const store = textsecure.storage.protocol;
const identifier = '+5558675309';
const identityKey = {
pubKey: window.Signal.Crypto.getRandomBytes(33),
privKey: window.Signal.Crypto.getRandomBytes(32),
};
const testKey = {
pubKey: window.Signal.Crypto.getRandomBytes(33),
privKey: window.Signal.Crypto.getRandomBytes(32),
};
before(async () => {
localStorage.clear();
ConversationController.reset();
// store.hydrateCaches();
await storage.fetch();
await ConversationController.load();
await ConversationController.getOrCreateAndWait(identifier, 'private');
});
it('retrieves my registration id', async () => {
store.put('registrationId', 1337);
const reg = await store.getLocalRegistrationId();
assert.strictEqual(reg, 1337);
});
it('retrieves my identity key', async () => {
store.put('identityKey', identityKey);
const key = await store.getIdentityKeyPair();
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
assertEqualArrayBuffers(key.privKey, identityKey.privKey);
});
it('stores identity keys', async () => {
await store.saveIdentity(identifier, testKey.pubKey);
const key = await store.loadIdentityKey(identifier);
assertEqualArrayBuffers(key, testKey.pubKey);
});
it('returns whether a key is trusted', async () => {
const newIdentity = window.Signal.Crypto.getRandomBytes(33);
await store.saveIdentity(identifier, testKey.pubKey);
const trusted = await store.isTrustedIdentity(identifier, newIdentity);
if (trusted) {
throw new Error('Allowed to overwrite identity key');
}
});
it('returns whether a key is untrusted', async () => {
await store.saveIdentity(identifier, testKey.pubKey);
const trusted = await store.isTrustedIdentity(identifier, testKey.pubKey);
if (!trusted) {
throw new Error('Allowed to overwrite identity key');
}
});
it('stores prekeys', async () => {
await store.storePreKey(1, testKey);
const key = await store.loadPreKey(1);
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey);
});
it('deletes prekeys', async () => {
await store.storePreKey(2, testKey);
await store.removePreKey(2, testKey);
const key = await store.loadPreKey(2);
assert.isUndefined(key);
});
it('stores signed prekeys', async () => {
await store.storeSignedPreKey(3, testKey);
const key = await store.loadSignedPreKey(3);
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
assertEqualArrayBuffers(key.privKey, testKey.privKey);
});
it('deletes signed prekeys', async () => {
await store.storeSignedPreKey(4, testKey);
await store.removeSignedPreKey(4, testKey);
const key = await store.loadSignedPreKey(4);
assert.isUndefined(key);
});
it('stores sessions', async () => {
const testRecord = 'an opaque string';
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
await Promise.all(
devices.map(async encodedNumber => {
await store.storeSession(encodedNumber, testRecord + encodedNumber);
})
);
const records = await Promise.all(
devices.map(store.loadSession.bind(store))
);
for (let i = 0, max = records.length; i < max; i += 1) {
assert.strictEqual(records[i], testRecord + devices[i]);
}
});
it('removes all sessions for a number', async () => {
const testRecord = 'an opaque string';
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
await Promise.all(
devices.map(async encodedNumber => {
await store.storeSession(encodedNumber, testRecord + encodedNumber);
})
);
await store.removeAllSessions(identifier);
const records = await Promise.all(
devices.map(store.loadSession.bind(store))
);
for (let i = 0, max = records.length; i < max; i += 1) {
assert.isUndefined(records[i]);
}
});
it('returns deviceIds for a number', async () => {
const testRecord = 'an opaque string';
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
await Promise.all(
devices.map(async encodedNumber => {
await store.storeSession(encodedNumber, testRecord + encodedNumber);
})
);
const deviceIds = await store.getDeviceIds(identifier);
assert.sameMembers(deviceIds, [1, 2, 3]);
});
it('returns empty array for a number with no device ids', async () => {
const deviceIds = await store.getDeviceIds('foo');
assert.sameMembers(deviceIds, []);
});
});

View file

@ -189,6 +189,8 @@ try {
statistics = {};
}
const ourUuid = window.textsecure.storage.user.getUuid();
event.sender.send('additional-log-data-response', {
capabilities: ourCapabilities || {},
remoteConfig: _.mapValues(remoteConfig, ({ value, enabled }) => {
@ -200,7 +202,7 @@ try {
user: {
deviceId: window.textsecure.storage.user.getDeviceId(),
e164: window.textsecure.storage.user.getNumber(),
uuid: window.textsecure.storage.user.getUuid(),
uuid: ourUuid && ourUuid.toString(),
conversationId: ourConversation && ourConversation.id,
},
});

View file

@ -248,11 +248,6 @@
></script>
<script type="text/javascript" src="../js/libphonenumber-util.js"></script>
<script
type="text/javascript"
src="../js/keychange_listener.js"
data-cover
></script>
<script
type="text/javascript"
src="../js/expiring_messages.js"
@ -313,7 +308,6 @@
<script type="text/javascript" src="views/list_view_test.js"></script>
<script type="text/javascript" src="libphonenumber_util_test.js"></script>
<script type="text/javascript" src="keychange_listener_test.js"></script>
<script type="text/javascript" src="reliable_trigger_test.js"></script>
<script type="text/javascript" src="database_test.js"></script>
<script type="text/javascript" src="i18n_test.js"></script>

View file

@ -7,6 +7,7 @@ import PQueue from 'p-queue';
import dataInterface from './sql/Client';
import {
ConversationModelCollectionType,
ConversationAttributesType,
ConversationAttributesTypeType,
} from './model-types.d';
import { ConversationModel } from './models/conversations';
@ -15,6 +16,9 @@ import { assert } from './util/assert';
import { isValidGuid } from './util/isValidGuid';
import { map, reduce } from './util/iterables';
import { isGroupV1, isGroupV2 } from './util/whatTypeOfConversation';
import { UUID } from './types/UUID';
import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress';
const MAX_MESSAGE_BODY_LENGTH = 64 * 1024;
@ -156,7 +160,7 @@ export class ConversationController {
}
dangerouslyCreateAndAdd(
attributes: Partial<ConversationModel>
attributes: Partial<ConversationAttributesType>
): ConversationModel {
return this._conversations.add(attributes);
}
@ -295,7 +299,7 @@ export class ConversationController {
getOurConversationId(): string | undefined {
const e164 = window.textsecure.storage.user.getNumber();
const uuid = window.textsecure.storage.user.getUuid();
const uuid = window.textsecure.storage.user.getUuid()?.toString();
return this.ensureContactIds({ e164, uuid, highTrust: true });
}
@ -639,13 +643,14 @@ export class ConversationController {
}
const obsoleteId = obsolete.get('id');
const obsoleteUuid = obsolete.get('uuid');
const currentId = current.get('id');
window.log.warn('combineConversations: Combining two conversations', {
obsolete: obsoleteId,
current: currentId,
});
if (conversationType === 'private') {
if (conversationType === 'private' && obsoleteUuid) {
if (!current.get('profileKey') && obsolete.get('profileKey')) {
window.log.warn(
'combineConversations: Copying profile key from old to new contact'
@ -661,21 +666,30 @@ export class ConversationController {
window.log.warn(
'combineConversations: Delete all sessions tied to old conversationId'
);
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
obsoleteId
);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
ourUuid,
identifier: obsoleteUuid,
});
await Promise.all(
deviceIds.map(async deviceId => {
await window.textsecure.storage.protocol.removeSession(
`${obsoleteId}.${deviceId}`
const addr = new QualifiedAddress(
ourUuid,
Address.create(obsoleteUuid, deviceId)
);
await window.textsecure.storage.protocol.removeSession(addr);
})
);
window.log.warn(
'combineConversations: Delete all identity information tied to old conversationId'
);
await window.textsecure.storage.protocol.removeIdentityKey(obsoleteId);
if (obsoleteUuid) {
await window.textsecure.storage.protocol.removeIdentityKey(
new UUID(obsoleteUuid)
);
}
window.log.warn(
'combineConversations: Ensure that all V1 groups have new conversationId instead of old'

View file

@ -23,28 +23,42 @@ import {
Uuid,
} from '@signalapp/signal-client';
import { freezePreKey, freezeSignedPreKey } from './SignalProtocolStore';
import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress';
import type { UUID } from './types/UUID';
import { typedArrayToArrayBuffer } from './Crypto';
import { Zone } from './util/Zone';
function encodedNameFromAddress(address: ProtocolAddress): string {
function encodeAddress(address: ProtocolAddress): Address {
const name = address.name();
const deviceId = address.deviceId();
const encodedName = `${name}.${deviceId}`;
return encodedName;
return Address.create(name, deviceId);
}
export type SessionsOptions = {
readonly zone?: Zone;
};
function toQualifiedAddress(
ourUuid: UUID,
address: ProtocolAddress
): QualifiedAddress {
return new QualifiedAddress(ourUuid, encodeAddress(address));
}
export type SessionsOptions = Readonly<{
ourUuid: UUID;
zone?: Zone;
}>;
export class Sessions extends SessionStore {
private readonly ourUuid: UUID;
private readonly zone: Zone | undefined;
constructor(options: SessionsOptions = {}) {
constructor({ ourUuid, zone }: SessionsOptions) {
super();
this.zone = options.zone;
this.ourUuid = ourUuid;
this.zone = zone;
}
async saveSession(
@ -52,16 +66,16 @@ export class Sessions extends SessionStore {
record: SessionRecord
): Promise<void> {
await window.textsecure.storage.protocol.storeSession(
encodedNameFromAddress(address),
toQualifiedAddress(this.ourUuid, address),
record,
{ zone: this.zone }
);
}
async getSession(name: ProtocolAddress): Promise<SessionRecord | null> {
const encodedName = encodedNameFromAddress(name);
const encodedAddress = toQualifiedAddress(this.ourUuid, name);
const record = await window.textsecure.storage.protocol.loadSession(
encodedName,
encodedAddress,
{ zone: this.zone }
);
@ -71,27 +85,36 @@ export class Sessions extends SessionStore {
async getExistingSessions(
addresses: Array<ProtocolAddress>
): Promise<Array<SessionRecord>> {
const encodedAddresses = addresses.map(encodedNameFromAddress);
const encodedAddresses = addresses.map(addr =>
toQualifiedAddress(this.ourUuid, addr)
);
return window.textsecure.storage.protocol.loadSessions(encodedAddresses, {
zone: this.zone,
});
}
}
export type IdentityKeysOptions = {
readonly zone?: Zone;
};
export type IdentityKeysOptions = Readonly<{
ourUuid: UUID;
zone?: Zone;
}>;
export class IdentityKeys extends IdentityKeyStore {
private readonly ourUuid: UUID;
private readonly zone: Zone | undefined;
constructor({ zone }: IdentityKeysOptions = {}) {
constructor({ ourUuid, zone }: IdentityKeysOptions) {
super();
this.ourUuid = ourUuid;
this.zone = zone;
}
async getIdentityKey(): Promise<PrivateKey> {
const keyPair = await window.textsecure.storage.protocol.getIdentityKeyPair();
const keyPair = await window.textsecure.storage.protocol.getIdentityKeyPair(
this.ourUuid
);
if (!keyPair) {
throw new Error('IdentityKeyStore/getIdentityKey: No identity key!');
}
@ -99,7 +122,9 @@ export class IdentityKeys extends IdentityKeyStore {
}
async getLocalRegistrationId(): Promise<number> {
const id = await window.textsecure.storage.protocol.getLocalRegistrationId();
const id = await window.textsecure.storage.protocol.getLocalRegistrationId(
this.ourUuid
);
if (!isNumber(id)) {
throw new Error(
'IdentityKeyStore/getLocalRegistrationId: No registration id!'
@ -109,9 +134,9 @@ export class IdentityKeys extends IdentityKeyStore {
}
async getIdentity(address: ProtocolAddress): Promise<PublicKey | null> {
const encodedName = encodedNameFromAddress(address);
const encodedAddress = encodeAddress(address);
const key = await window.textsecure.storage.protocol.loadIdentityKey(
encodedName
encodedAddress.uuid
);
if (!key) {
@ -122,13 +147,13 @@ export class IdentityKeys extends IdentityKeyStore {
}
async saveIdentity(name: ProtocolAddress, key: PublicKey): Promise<boolean> {
const encodedName = encodedNameFromAddress(name);
const encodedAddress = encodeAddress(name);
const publicKey = typedArrayToArrayBuffer(key.serialize());
// Pass `zone` to let `saveIdentity` archive sibling sessions when identity
// key changes.
return window.textsecure.storage.protocol.saveIdentity(
encodedName,
encodedAddress,
publicKey,
false,
{ zone: this.zone }
@ -140,27 +165,42 @@ export class IdentityKeys extends IdentityKeyStore {
key: PublicKey,
direction: Direction
): Promise<boolean> {
const encodedName = encodedNameFromAddress(name);
const encodedAddress = encodeAddress(name);
const publicKey = typedArrayToArrayBuffer(key.serialize());
return window.textsecure.storage.protocol.isTrustedIdentity(
encodedName,
encodedAddress,
publicKey,
direction
);
}
}
export type PreKeysOptions = Readonly<{
ourUuid: UUID;
}>;
export class PreKeys extends PreKeyStore {
private readonly ourUuid: UUID;
constructor({ ourUuid }: PreKeysOptions) {
super();
this.ourUuid = ourUuid;
}
async savePreKey(id: number, record: PreKeyRecord): Promise<void> {
await window.textsecure.storage.protocol.storePreKey(
this.ourUuid,
id,
freezePreKey(record)
);
}
async getPreKey(id: number): Promise<PreKeyRecord> {
const preKey = await window.textsecure.storage.protocol.loadPreKey(id);
const preKey = await window.textsecure.storage.protocol.loadPreKey(
this.ourUuid,
id
);
if (preKey === undefined) {
throw new Error(`getPreKey: PreKey ${id} not found`);
@ -170,17 +210,28 @@ export class PreKeys extends PreKeyStore {
}
async removePreKey(id: number): Promise<void> {
await window.textsecure.storage.protocol.removePreKey(id);
await window.textsecure.storage.protocol.removePreKey(this.ourUuid, id);
}
}
export type SenderKeysOptions = Readonly<{
ourUuid: UUID;
}>;
export class SenderKeys extends SenderKeyStore {
private readonly ourUuid: UUID;
constructor({ ourUuid }: SenderKeysOptions) {
super();
this.ourUuid = ourUuid;
}
async saveSenderKey(
sender: ProtocolAddress,
distributionId: Uuid,
record: SenderKeyRecord
): Promise<void> {
const encodedAddress = encodedNameFromAddress(sender);
const encodedAddress = toQualifiedAddress(this.ourUuid, sender);
await window.textsecure.storage.protocol.saveSenderKey(
encodedAddress,
@ -193,7 +244,7 @@ export class SenderKeys extends SenderKeyStore {
sender: ProtocolAddress,
distributionId: Uuid
): Promise<SenderKeyRecord | null> {
const encodedAddress = encodedNameFromAddress(sender);
const encodedAddress = toQualifiedAddress(this.ourUuid, sender);
const senderKey = await window.textsecure.storage.protocol.getSenderKey(
encodedAddress,
@ -204,12 +255,24 @@ export class SenderKeys extends SenderKeyStore {
}
}
export type SignedPreKeysOptions = Readonly<{
ourUuid: UUID;
}>;
export class SignedPreKeys extends SignedPreKeyStore {
private readonly ourUuid: UUID;
constructor({ ourUuid }: SignedPreKeysOptions) {
super();
this.ourUuid = ourUuid;
}
async saveSignedPreKey(
id: number,
record: SignedPreKeyRecord
): Promise<void> {
await window.textsecure.storage.protocol.storeSignedPreKey(
this.ourUuid,
id,
freezeSignedPreKey(record),
true
@ -218,6 +281,7 @@ export class SignedPreKeys extends SignedPreKeyStore {
async getSignedPreKey(id: number): Promise<SignedPreKeyRecord> {
const signedPreKey = await window.textsecure.storage.protocol.loadSignedPreKey(
this.ourUuid,
id
);

File diff suppressed because it is too large Load diff

View file

@ -66,6 +66,7 @@ import {
EnvelopeEvent,
} from './textsecure/messageReceiverEvents';
import type { WebAPIType } from './textsecure/WebAPI';
import * as KeyChangeListener from './textsecure/KeyChangeListener';
import { isDirectConversation, isGroupV2 } from './util/whatTypeOfConversation';
import { getSendOptions } from './util/getSendOptions';
import { BackOff, FIBONACCI_TIMEOUTS } from './util/BackOff';
@ -497,7 +498,7 @@ export async function startApp(): Promise<void> {
window.document.title = window.getTitle();
window.Whisper.KeyChangeListener.init(window.textsecure.storage.protocol);
KeyChangeListener.init(window.textsecure.storage.protocol);
window.textsecure.storage.protocol.on('removePreKey', () => {
window.getAccountManager().refreshPreKeys();
});
@ -921,7 +922,7 @@ export async function startApp(): Promise<void> {
conversation.format()
);
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
const ourConversationId = window.ConversationController.getOurConversationId();
const themeSetting = window.Events.getThemeSetting();
@ -1065,7 +1066,7 @@ export async function startApp(): Promise<void> {
window.Whisper.events.on('userChanged', (reconnect = false) => {
const newDeviceId = window.textsecure.storage.user.getDeviceId();
const newNumber = window.textsecure.storage.user.getNumber();
const newUuid = window.textsecure.storage.user.getUuid();
const newUuid = window.textsecure.storage.user.getUuid()?.toString();
const ourConversation = window.ConversationController.getOurConversation();
if (ourConversation?.get('e164') !== newNumber) {
@ -2136,30 +2137,9 @@ export async function startApp(): Promise<void> {
const deviceId = window.textsecure.storage.user.getDeviceId();
// If we didn't capture a UUID on registration, go get it from the server
if (!window.textsecure.storage.user.getUuid()) {
try {
const { uuid } = await server.whoami();
assert(deviceId, 'We should have device id');
window.textsecure.storage.user.setUuidAndDeviceId(uuid, deviceId);
const ourNumber = window.textsecure.storage.user.getNumber();
assert(ourNumber, 'We should have number');
const me = await window.ConversationController.getOrCreateAndWait(
ourNumber,
'private'
);
me.updateUuid(uuid);
await server.authenticate(
window.textsecure.storage.user.getWebAPICredentials()
);
} catch (error) {
window.log.error(
'Error: Unable to retrieve UUID from service.',
error && error.stack ? error.stack : error
);
}
window.log.error('UUID not captured during registration, unlinking');
return unlinkAndDisconnect(RemoveAllConfiguration.Full);
}
if (connectCount === 1) {
@ -2587,7 +2567,7 @@ export async function startApp(): Promise<void> {
(details.number &&
details.number === window.textsecure.storage.user.getNumber()) ||
(details.uuid &&
details.uuid === window.textsecure.storage.user.getUuid())
details.uuid === window.textsecure.storage.user.getUuid()?.toString())
) {
// special case for syncing details about ourselves
if (details.profileKey) {
@ -2845,7 +2825,7 @@ export async function startApp(): Promise<void> {
});
function onEnvelopeReceived({ envelope }: EnvelopeEvent) {
const ourUuid = window.textsecure.storage.user.getUuid();
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
if (envelope.sourceUuid && envelope.sourceUuid !== ourUuid) {
window.ConversationController.ensureContactIds({
e164: envelope.source,
@ -3083,7 +3063,7 @@ export async function startApp(): Promise<void> {
return new window.Whisper.Message(({
source: window.textsecure.storage.user.getNumber(),
sourceUuid: window.textsecure.storage.user.getUuid(),
sourceUuid: window.textsecure.storage.user.getUuid()?.toString(),
sourceDevice: data.device,
sent_at: timestamp,
serverTimestamp: data.serverTimestamp,
@ -3216,7 +3196,7 @@ export async function startApp(): Promise<void> {
const { data, confirm } = event;
const source = window.textsecure.storage.user.getNumber();
const sourceUuid = window.textsecure.storage.user.getUuid();
const sourceUuid = window.textsecure.storage.user.getUuid()?.toString();
strictAssert(source && sourceUuid, 'Missing user number and uuid');
const messageDescriptor = getMessageDescriptor({
@ -3492,7 +3472,7 @@ export async function startApp(): Promise<void> {
switch (eventType) {
case FETCH_LATEST_ENUM.LOCAL_PROFILE: {
const ourUuid = window.textsecure.storage.user.getUuid();
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
const ourE164 = window.textsecure.storage.user.getNumber();
await getProfile(ourUuid, ourE164);
break;

View file

@ -550,19 +550,12 @@ function buildGroupProto(
);
}
const me = window.ConversationController.get(ourConversationId);
if (!me) {
throw new Error(
`buildGroupProto/${logId}: unable to find our own conversation!`
);
}
const ourUuid = window.storage.user.getCheckedUuid();
const ourUuid = me.get('uuid');
if (!ourUuid) {
throw new Error(`buildGroupProto/${logId}: unable to find our own uuid!`);
}
const ourUuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, ourUuid);
const ourUuidCipherTextBuffer = encryptUuid(
clientZkGroupCipher,
ourUuid.toString()
);
proto.membersPendingProfileKey = (attributes.pendingMembersV2 || []).map(
item => {
@ -627,15 +620,11 @@ export async function buildAddMembersChange(
);
const clientZkGroupCipher = getClientZkGroupCipher(secretParams);
const ourConversationId = window.ConversationController.getOurConversationIdOrThrow();
const ourConversation = window.ConversationController.get(ourConversationId);
const ourUuid = ourConversation?.get('uuid');
if (!ourUuid) {
throw new Error(
`buildAddMembersChange/${logId}: unable to find our own UUID!`
);
}
const ourUuidCipherTextBuffer = encryptUuid(clientZkGroupCipher, ourUuid);
const ourUuid = window.storage.user.getCheckedUuid();
const ourUuidCipherTextBuffer = encryptUuid(
clientZkGroupCipher,
ourUuid.toString()
);
const now = Date.now();
@ -1727,10 +1716,12 @@ export async function createGroupV2({
timestamp,
});
const ourUuid = window.storage.user.getCheckedUuid();
const createdTheGroupMessage: MessageAttributesType = {
...generateBasicMessage(),
type: 'group-v2-change',
sourceUuid: conversation.ourUuid,
sourceUuid: ourUuid.toString(),
conversationId: conversation.id,
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: timestamp,

View file

@ -37,6 +37,7 @@ import { missingCaseError } from '../util/missingCaseError';
import { sniffImageMimeType } from '../util/sniffImageMimeType';
import { isValidE164 } from '../util/isValidE164';
import { MIMEType, IMAGE_WEBP } from '../types/MIME';
import { UUID } from '../types/UUID';
import {
arrayBufferToBase64,
base64ToArrayBuffer,
@ -157,8 +158,6 @@ export class ConversationModel extends window.Backbone
jobQueue?: typeof window.PQueueType;
ourUuid?: string;
storeName?: string | null;
throttledBumpTyping?: () => void;
@ -239,7 +238,6 @@ export class ConversationModel extends window.Backbone
this.storeName = 'conversations';
this.ourUuid = window.textsecure.storage.user.getUuid();
this.verifiedEnum = window.textsecure.storage.protocol.VerifiedStatus;
// This may be overridden by window.ConversationController.getOrCreate, and signify
@ -2105,7 +2103,14 @@ export class ConversationModel extends window.Backbone
}
async safeGetVerified(): Promise<number> {
const promise = window.textsecure.storage.protocol.getVerified(this.id);
const uuid = this.get('uuid');
if (!uuid) {
return window.textsecure.storage.protocol.VerifiedStatus.DEFAULT;
}
const promise = window.textsecure.storage.protocol.getVerified(
new UUID(uuid)
);
return promise.catch(
() => window.textsecure.storage.protocol.VerifiedStatus.DEFAULT
);
@ -2182,21 +2187,31 @@ export class ConversationModel extends window.Backbone
);
}
const uuid = this.get('uuid');
const beginningVerified = this.get('verified');
let keyChange;
if (options.viaSyncMessage) {
strictAssert(
uuid,
`Sync message didn't update uuid for conversation: ${this.id}`
);
// handle the incoming key from the sync messages - need different
// behavior if that key doesn't match the current key
keyChange = await window.textsecure.storage.protocol.processVerifiedMessage(
this.id,
new UUID(uuid),
verified,
options.key || undefined
);
} else {
} else if (uuid) {
keyChange = await window.textsecure.storage.protocol.setVerified(
this.id,
new UUID(uuid),
verified
);
} else {
window.log.warn(
`_setVerified(${this.id}): no uuid to update protocol storage`
);
}
this.set({ verified });
@ -2227,12 +2242,8 @@ export class ConversationModel extends window.Backbone
local: !options.viaSyncMessage,
});
}
if (!options.viaSyncMessage) {
await this.sendVerifySyncMessage(
this.get('e164'),
this.get('uuid'),
verified
);
if (!options.viaSyncMessage && uuid) {
await this.sendVerifySyncMessage(this.get('e164'), uuid, verified);
}
return keyChange;
@ -2240,7 +2251,7 @@ export class ConversationModel extends window.Backbone
async sendVerifySyncMessage(
e164: string | undefined,
uuid: string | undefined,
uuid: string,
state: number
): Promise<CallbackResultType | void> {
const identifier = uuid || e164;
@ -2268,7 +2279,7 @@ export class ConversationModel extends window.Backbone
const options = { ...sendOptions, ...contactSendOptions };
const key = await window.textsecure.storage.protocol.loadIdentityKey(
identifier
UUID.checkedLookup(identifier)
);
if (!key) {
throw new Error(
@ -2353,12 +2364,20 @@ export class ConversationModel extends window.Backbone
);
}
return window.textsecure.storage.protocol.setApproval(this.id, true);
const uuid = this.get('uuid');
if (!uuid) {
window.log.warn(`setApproved(${this.id}): no uuid, ignoring`);
return;
}
return window.textsecure.storage.protocol.setApproval(new UUID(uuid), true);
}
safeIsUntrusted(): boolean {
const uuid = this.get('uuid');
try {
return window.textsecure.storage.protocol.isUntrusted(this.id);
strictAssert(uuid, `No uuid for conversation: ${this.id}`);
return window.textsecure.storage.protocol.isUntrusted(new UUID(uuid));
} catch (err) {
return false;
}
@ -2526,11 +2545,11 @@ export class ConversationModel extends window.Backbone
await this.notify(model);
}
async addKeyChange(keyChangedId: string): Promise<void> {
async addKeyChange(keyChangedId: UUID): Promise<void> {
window.log.info(
'adding key change advisory for',
this.idForLogging(),
keyChangedId,
keyChangedId.toString(),
this.get('timestamp')
);
@ -2541,7 +2560,7 @@ export class ConversationModel extends window.Backbone
sent_at: this.get('timestamp'),
received_at: window.Signal.Util.incrementMessageCounter(),
received_at_ms: timestamp,
key_changed: keyChangedId,
key_changed: keyChangedId.toString(),
readStatus: ReadStatus.Unread,
schemaVersion: Message.VERSION_NEEDED_FOR_DISPLAY,
// TODO: DESKTOP-722
@ -4839,7 +4858,7 @@ export class ConversationModel extends window.Backbone
return;
}
const ourUuid = window.textsecure.storage.user.getUuid();
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
const mentionsMe = (message.get('bodyRanges') || []).some(
range => range.mentionUuid && range.mentionUuid === ourUuid
);

View file

@ -221,7 +221,7 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
this.CURRENT_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.CURRENT;
this.INITIAL_PROTOCOL_VERSION = Proto.DataMessage.ProtocolVersion.INITIAL;
this.OUR_UUID = window.textsecure.storage.user.getUuid();
this.OUR_UUID = window.textsecure.storage.user.getUuid()?.toString();
this.on('change', this.notifyRedux);
}
@ -2881,7 +2881,8 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const profileKey = dataMessage.profileKey.toString('base64');
if (
source === window.textsecure.storage.user.getNumber() ||
sourceUuid === window.textsecure.storage.user.getUuid()
sourceUuid ===
window.textsecure.storage.user.getUuid()?.toString()
) {
conversation.set({ profileSharing: true });
} else if (isDirectConversation(conversation.attributes)) {

View file

@ -53,6 +53,7 @@ import {
ProcessGroupCallRingRequestResult,
} from '../types/Calling';
import { LocalizerType } from '../types/Util';
import { UUID } from '../types/UUID';
import { ConversationModel } from '../models/conversations';
import * as Bytes from '../Bytes';
import {
@ -60,7 +61,6 @@ import {
arrayBufferToUuid,
typedArrayToArrayBuffer,
} from '../Crypto';
import { assert } from '../util/assert';
import { dropNull, shallowDropNull } from '../util/dropNull';
import { getOwn } from '../util/getOwn';
import * as durations from '../util/durations';
@ -262,7 +262,7 @@ export class CallingClass {
}
private attemptToGiveOurUuidToRingRtc(): void {
const ourUuid = window.textsecure.storage.user.getUuid();
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
if (!ourUuid) {
// This can happen if we're not linked. It's okay if we hit this case.
return;
@ -1392,15 +1392,17 @@ export class CallingClass {
return;
}
const remoteUserId = envelope.sourceUuid || envelope.source;
const remoteUserId = envelope.sourceUuid;
const remoteDeviceId = this.parseDeviceId(envelope.sourceDevice);
if (!remoteUserId || !remoteDeviceId || !this.localDeviceId) {
window.log.error('Missing identifier, ignoring call message.');
return;
}
const senderIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord(
remoteUserId
const { storage } = window.textsecure;
const senderIdentityRecord = await storage.protocol.getOrMigrateIdentityRecord(
new UUID(remoteUserId)
);
if (!senderIdentityRecord) {
window.log.error(
@ -1410,14 +1412,9 @@ export class CallingClass {
}
const senderIdentityKey = senderIdentityRecord.publicKey.slice(1); // Ignore the type header, it is not used.
const ourIdentifier =
window.textsecure.storage.user.getUuid() ||
window.textsecure.storage.user.getNumber();
assert(ourIdentifier, 'We should have either uuid or number');
const ourUuid = storage.user.getCheckedUuid();
const receiverIdentityRecord = window.textsecure.storage.protocol.getIdentityRecord(
ourIdentifier
);
const receiverIdentityRecord = storage.protocol.getIdentityRecord(ourUuid);
if (!receiverIdentityRecord) {
window.log.error(
'Missing receiver identity record; ignoring call message.'

View file

@ -115,7 +115,7 @@ export function getCredentialsForToday(
}
export async function maybeFetchNewCredentials(): Promise<void> {
const uuid = window.textsecure.storage.user.getUuid();
const uuid = window.textsecure.storage.user.getUuid()?.toString();
if (!uuid) {
window.log.info('maybeFetchCredentials: no UUID, returning early');
return;

View file

@ -166,6 +166,11 @@ async function generateManifest(
storageRecord.account = await toAccountRecord(conversation);
identifier.type = ITEM_TYPE.ACCOUNT;
} else if (conversationType === ConversationTypes.Direct) {
// Contacts must have UUID
if (!conversation.get('uuid')) {
continue;
}
storageRecord = new Proto.StorageRecord();
// eslint-disable-next-line no-await-in-loop
storageRecord.contact = await toContactRecord(conversation);

View file

@ -35,6 +35,8 @@ import {
} from '../util/universalExpireTimer';
import { ourProfileKeyService } from './ourProfileKey';
import { isGroupV1, isGroupV2 } from '../util/whatTypeOfConversation';
import { UUID } from '../types/UUID';
import * as Errors from '../types/errors';
import { SignalService as Proto } from '../protobuf';
const { updateConversation } = dataInterface;
@ -118,9 +120,19 @@ export async function toContactRecord(
if (profileKey) {
contactRecord.profileKey = Bytes.fromBase64(String(profileKey));
}
const identityKey = await window.textsecure.storage.protocol.loadIdentityKey(
conversation.id
);
let maybeUuid: UUID | undefined;
try {
maybeUuid = uuid ? new UUID(uuid) : undefined;
} catch (error) {
window.log.warn(
`Invalid uuid in contact record: ${Errors.toLogFormat(error)}`
);
}
const identityKey = maybeUuid
? await window.textsecure.storage.protocol.loadIdentityKey(maybeUuid)
: undefined;
if (identityKey) {
contactRecord.identityKey = new FIXMEU8(identityKey);
}
@ -723,6 +735,11 @@ export async function mergeContactRecord(
const e164 = contactRecord.serviceE164 || undefined;
const uuid = contactRecord.serviceUuid || undefined;
// All contacts must have UUID
if (!uuid) {
return false;
}
const id = window.ConversationController.ensureContactIds({
e164,
uuid,

View file

@ -50,14 +50,17 @@ import {
ConversationType,
DeleteSentProtoRecipientOptionsType,
IdentityKeyType,
IdentityKeyIdType,
ItemKeyType,
ItemType,
LastConversationMessagesType,
MessageType,
MessageTypeUnhydrated,
PreKeyType,
PreKeyIdType,
SearchResultMessageType,
SenderKeyType,
SenderKeyIdType,
SentMessageDBType,
SentMessagesType,
SentProtoType,
@ -66,7 +69,9 @@ import {
SentRecipientsType,
ServerInterface,
SessionType,
SessionIdType,
SignedPreKeyType,
SignedPreKeyIdType,
StickerPackStatusType,
StickerPackType,
StickerType,
@ -634,17 +639,10 @@ async function removeIndexedDBFiles() {
const IDENTITY_KEY_KEYS = ['publicKey'];
async function createOrUpdateIdentityKey(data: IdentityKeyType) {
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, {
...data,
id: window.ConversationController.getConversationId(data.id),
});
const updated = keysFromArrayBuffer(IDENTITY_KEY_KEYS, data);
await channels.createOrUpdateIdentityKey(updated);
}
async function getIdentityKeyById(identifier: string) {
const id = window.ConversationController.getConversationId(identifier);
if (!id) {
throw new Error('getIdentityKeyById: unable to find conversationId');
}
async function getIdentityKeyById(id: IdentityKeyIdType) {
const data = await channels.getIdentityKeyById(id);
return keysToArrayBuffer(IDENTITY_KEY_KEYS, data);
@ -655,11 +653,7 @@ async function bulkAddIdentityKeys(array: Array<IdentityKeyType>) {
);
await channels.bulkAddIdentityKeys(updated);
}
async function removeIdentityKeyById(identifier: string) {
const id = window.ConversationController.getConversationId(identifier);
if (!id) {
throw new Error('removeIdentityKeyById: unable to find conversationId');
}
async function removeIdentityKeyById(id: IdentityKeyIdType) {
await channels.removeIdentityKeyById(id);
}
async function removeAllIdentityKeys() {
@ -677,7 +671,7 @@ async function createOrUpdatePreKey(data: PreKeyType) {
const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data);
await channels.createOrUpdatePreKey(updated);
}
async function getPreKeyById(id: number) {
async function getPreKeyById(id: PreKeyIdType) {
const data = await channels.getPreKeyById(id);
return keysToArrayBuffer(PRE_KEY_KEYS, data);
@ -686,7 +680,7 @@ async function bulkAddPreKeys(array: Array<PreKeyType>) {
const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data));
await channels.bulkAddPreKeys(updated);
}
async function removePreKeyById(id: number) {
async function removePreKeyById(id: PreKeyIdType) {
await channels.removePreKeyById(id);
}
async function removeAllPreKeys() {
@ -705,7 +699,7 @@ async function createOrUpdateSignedPreKey(data: SignedPreKeyType) {
const updated = keysFromArrayBuffer(PRE_KEY_KEYS, data);
await channels.createOrUpdateSignedPreKey(updated);
}
async function getSignedPreKeyById(id: number) {
async function getSignedPreKeyById(id: SignedPreKeyIdType) {
const data = await channels.getSignedPreKeyById(id);
return keysToArrayBuffer(PRE_KEY_KEYS, data);
@ -721,7 +715,7 @@ async function bulkAddSignedPreKeys(array: Array<SignedPreKeyType>) {
const updated = map(array, data => keysFromArrayBuffer(PRE_KEY_KEYS, data));
await channels.bulkAddSignedPreKeys(updated);
}
async function removeSignedPreKeyById(id: number) {
async function removeSignedPreKeyById(id: SignedPreKeyIdType) {
await channels.removeSignedPreKeyById(id);
}
async function removeAllSignedPreKeys() {
@ -731,7 +725,6 @@ async function removeAllSignedPreKeys() {
// Items
const ITEM_KEYS: Partial<Record<ItemKeyType, Array<string>>> = {
identityKey: ['value.pubKey', 'value.privKey'],
senderCertificate: ['value.serialized'],
senderCertificateNoE164: ['value.serialized'],
profileKey: ['value'],
@ -749,7 +742,9 @@ async function createOrUpdateItem<K extends ItemKeyType>(data: ItemType<K>) {
await channels.createOrUpdateItem(updated);
}
async function getItemById<K extends ItemKeyType>(id: K): Promise<ItemType<K>> {
async function getItemById<K extends ItemKeyType>(
id: K
): Promise<ItemType<K> | undefined> {
const keys = ITEM_KEYS[id];
const data = await channels.getItemById(id);
@ -788,7 +783,7 @@ async function createOrUpdateSenderKey(key: SenderKeyType): Promise<void> {
await channels.createOrUpdateSenderKey(key);
}
async function getSenderKeyById(
id: string
id: SenderKeyIdType
): Promise<SenderKeyType | undefined> {
return channels.getSenderKeyById(id);
}
@ -798,7 +793,7 @@ async function removeAllSenderKeys(): Promise<void> {
async function getAllSenderKeys(): Promise<Array<SenderKeyType>> {
return channels.getAllSenderKeys();
}
async function removeSenderKeyById(id: string): Promise<void> {
async function removeSenderKeyById(id: SenderKeyIdType): Promise<void> {
return channels.removeSenderKeyById(id);
}
@ -879,7 +874,7 @@ async function commitSessionsAndUnprocessed(options: {
async function bulkAddSessions(array: Array<SessionType>) {
await channels.bulkAddSessions(array);
}
async function removeSessionById(id: string) {
async function removeSessionById(id: SessionIdType) {
await channels.removeSessionById(id);
}

View file

@ -19,6 +19,8 @@ import type { ProcessGroupCallRingRequestResult } from '../types/Calling';
import { StorageAccessType } from '../types/Storage.d';
import type { AttachmentType } from '../types/Attachment';
import { BodyRangesType } from '../types/Util';
import type { QualifiedAddressStringType } from '../types/QualifiedAddress';
import type { UUIDStringType } from '../types/UUID';
import type { RemoveAllConfiguration } from '../types/RemoveAllConfiguration';
export type AttachmentDownloadJobTypeType =
@ -57,14 +59,17 @@ export type EmojiType = {
shortName: string;
lastUsage: number;
};
export type IdentityKeyType = {
firstUse: boolean;
id: string;
id: UUIDStringType | `conversation:${UUIDStringType}`;
nonblockingApproval: boolean;
publicKey: ArrayBuffer;
timestamp: number;
verified: number;
};
export type IdentityKeyIdType = IdentityKeyType['id'];
export type ItemKeyType = keyof StorageAccessType;
export type AllItemsType = Partial<StorageAccessType>;
export type ItemType<K extends ItemKeyType> = {
@ -76,10 +81,13 @@ export type MessageTypeUnhydrated = {
json: string;
};
export type PreKeyType = {
id: number;
id: `${UUIDStringType}:${number}`;
keyId: number;
ourUuid: UUIDStringType;
privateKey: ArrayBuffer;
publicKey: ArrayBuffer;
};
export type PreKeyIdType = PreKeyType['id'];
export type SearchResultMessageType = {
json: string;
snippet: string;
@ -114,7 +122,7 @@ export type SentMessageDBType = {
export type SenderKeyType = {
// Primary key
id: string;
id: `${QualifiedAddressStringType}--${string}`;
// These two are combined into one string to give us the final id
senderId: string;
distributionId: string;
@ -122,21 +130,28 @@ export type SenderKeyType = {
data: Buffer;
lastUpdatedDate: number;
};
export type SenderKeyIdType = SenderKeyType['id'];
export type SessionType = {
id: string;
id: QualifiedAddressStringType;
ourUuid: UUIDStringType;
uuid: UUIDStringType;
conversationId: string;
deviceId: number;
record: string;
version?: number;
};
export type SessionIdType = SessionType['id'];
export type SignedPreKeyType = {
confirmed: boolean;
// eslint-disable-next-line camelcase
created_at: number;
id: number;
ourUuid: UUIDStringType;
id: `${UUIDStringType}:${number}`;
keyId: number;
privateKey: ArrayBuffer;
publicKey: ArrayBuffer;
};
export type SignedPreKeyIdType = SignedPreKeyType['id'];
export type StickerType = Readonly<{
id: number;
@ -227,23 +242,27 @@ export type DataInterface = {
removeIndexedDBFiles: () => Promise<void>;
createOrUpdateIdentityKey: (data: IdentityKeyType) => Promise<void>;
getIdentityKeyById: (id: string) => Promise<IdentityKeyType | undefined>;
getIdentityKeyById: (
id: IdentityKeyIdType
) => Promise<IdentityKeyType | undefined>;
bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>;
removeIdentityKeyById: (id: string) => Promise<void>;
removeIdentityKeyById: (id: IdentityKeyIdType) => Promise<void>;
removeAllIdentityKeys: () => Promise<void>;
getAllIdentityKeys: () => Promise<Array<IdentityKeyType>>;
createOrUpdatePreKey: (data: PreKeyType) => Promise<void>;
getPreKeyById: (id: number) => Promise<PreKeyType | undefined>;
getPreKeyById: (id: PreKeyIdType) => Promise<PreKeyType | undefined>;
bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>;
removePreKeyById: (id: number) => Promise<void>;
removePreKeyById: (id: PreKeyIdType) => Promise<void>;
removeAllPreKeys: () => Promise<void>;
getAllPreKeys: () => Promise<Array<PreKeyType>>;
createOrUpdateSignedPreKey: (data: SignedPreKeyType) => Promise<void>;
getSignedPreKeyById: (id: number) => Promise<SignedPreKeyType | undefined>;
getSignedPreKeyById: (
id: SignedPreKeyIdType
) => Promise<SignedPreKeyType | undefined>;
bulkAddSignedPreKeys: (array: Array<SignedPreKeyType>) => Promise<void>;
removeSignedPreKeyById: (id: number) => Promise<void>;
removeSignedPreKeyById: (id: SignedPreKeyIdType) => Promise<void>;
removeAllSignedPreKeys: () => Promise<void>;
getAllSignedPreKeys: () => Promise<Array<SignedPreKeyType>>;
@ -254,10 +273,10 @@ export type DataInterface = {
getAllItems: () => Promise<AllItemsType>;
createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>;
getSenderKeyById: (id: string) => Promise<SenderKeyType | undefined>;
getSenderKeyById: (id: SenderKeyIdType) => Promise<SenderKeyType | undefined>;
removeAllSenderKeys: () => Promise<void>;
getAllSenderKeys: () => Promise<Array<SenderKeyType>>;
removeSenderKeyById: (id: string) => Promise<void>;
removeSenderKeyById: (id: SenderKeyIdType) => Promise<void>;
insertSentProto: (
proto: SentProtoType,
@ -296,7 +315,7 @@ export type DataInterface = {
unprocessed: Array<UnprocessedType>;
}): Promise<void>;
bulkAddSessions: (array: Array<SessionType>) => Promise<void>;
removeSessionById: (id: string) => Promise<void>;
removeSessionById: (id: SessionIdType) => Promise<void>;
removeSessionsByConversation: (conversationId: string) => Promise<void>;
removeAllSessions: () => Promise<void>;
getAllSessions: () => Promise<Array<SessionType>>;

View file

@ -30,6 +30,7 @@ import {
} from 'lodash';
import { ReadStatus } from '../messages/MessageReadStatus';
import Helpers from '../textsecure/Helpers';
import { GroupV2MemberType } from '../model-types.d';
import { ReactionType } from '../types/Reactions';
import { STORAGE_UI_KEYS } from '../types/StorageUIKeys';
@ -40,6 +41,7 @@ import { dropNull } from '../util/dropNull';
import { isNormalNumber } from '../util/isNormalNumber';
import { isNotNil } from '../util/isNotNil';
import { missingCaseError } from '../util/missingCaseError';
import { isValidGuid } from '../util/isValidGuid';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import * as durations from '../util/durations';
import { formatCountForLogging } from '../logging/formatCountForLogging';
@ -55,6 +57,7 @@ import {
DeleteSentProtoRecipientOptionsType,
EmojiType,
IdentityKeyType,
IdentityKeyIdType,
ItemKeyType,
ItemType,
LastConversationMessagesServerType,
@ -62,8 +65,10 @@ import {
MessageType,
MessageTypeUnhydrated,
PreKeyType,
PreKeyIdType,
SearchResultMessageType,
SenderKeyType,
SenderKeyIdType,
SentMessageDBType,
SentMessagesType,
SentProtoType,
@ -72,7 +77,9 @@ import {
SentRecipientsType,
ServerInterface,
SessionType,
SessionIdType,
SignedPreKeyType,
SignedPreKeyIdType,
StickerPackStatusType,
StickerPackType,
StickerType,
@ -2101,6 +2108,336 @@ function updateToSchemaVersion40(currentVersion: number, db: Database) {
console.log('updateToSchemaVersion40: success!');
}
function updateToSchemaVersion41(currentVersion: number, db: Database) {
if (currentVersion >= 41) {
return;
}
const getConversationUuid = db.prepare<Query>(
`
SELECT uuid
FROM
conversations
WHERE
id = $conversationId
`
);
const clearSessionsAndKeys = () => {
// ts/background.ts will ask user to relink so all that matters here is
// to maintain an invariant:
//
// After this migration all sessions and keys are prefixed by
// "uuid:".
db.exec(
`
DELETE FROM senderKeys;
DELETE FROM sessions;
DELETE FROM signedPreKeys;
DELETE FROM preKeys;
`
);
assertSync(removeById<string>('items', 'identityKey', db));
assertSync(removeById<string>('items', 'registrationId', db));
};
const moveIdentityKeyToMap = (ourUuid: string) => {
type IdentityKeyType = {
privKey: string;
publicKey: string;
};
const identityKey = assertSync(
getById<string, { value: IdentityKeyType }>('items', 'identityKey', db)
);
type RegistrationId = number;
const registrationId = assertSync(
getById<string, { value: RegistrationId }>('items', 'registrationId', db)
);
if (identityKey) {
assertSync(
createOrUpdateSync<ItemKeyType>(
'items',
{
id: 'identityKeyMap',
value: {
[ourUuid]: identityKey.value,
},
},
db
)
);
}
if (registrationId) {
assertSync(
createOrUpdateSync<ItemKeyType>(
'items',
{
id: 'registrationIdMap',
value: {
[ourUuid]: registrationId.value,
},
},
db
)
);
}
assertSync(removeById<string>('items', 'identityKey', db));
assertSync(removeById<string>('items', 'registrationId', db));
};
const prefixKeys = (ourUuid: string) => {
for (const table of ['signedPreKeys', 'preKeys']) {
// Add numeric `keyId` field to keys
db.prepare<EmptyQuery>(
`
UPDATE ${table}
SET
json = json_insert(
json,
'$.keyId',
json_extract(json, '$.id')
)
`
).run();
// Update id to include suffix and add `ourUuid` field
db.prepare<Query>(
`
UPDATE ${table}
SET
id = $ourUuid || ':' || id,
json = json_set(
json,
'$.id',
$ourUuid || ':' || json_extract(json, '$.id'),
'$.ourUuid',
$ourUuid
)
`
).run({ ourUuid });
}
const senderKeys: ReadonlyArray<{
id: string;
senderId: string;
}> = db.prepare<EmptyQuery>('SELECT id, senderId FROM senderKeys').all();
console.log(`Updating ${senderKeys.length} sender keys`);
const updateSenderKey = db.prepare<Query>(
`
UPDATE senderKeys
SET
id = $newId,
senderId = $newSenderId
WHERE
id = $id
`
);
const deleteSenderKey = db.prepare<Query>(
'DELETE FROM senderKeys WHERE id = $id'
);
let updated = 0;
let deleted = 0;
for (const { id, senderId } of senderKeys) {
const [conversationId] = Helpers.unencodeNumber(senderId);
const { uuid } = getConversationUuid.get({ conversationId });
if (!uuid) {
deleted += 1;
deleteSenderKey.run({ id });
continue;
}
updated += 1;
updateSenderKey.run({
id,
newId: `${ourUuid}:${id.replace(conversationId, uuid)}`,
newSenderId: `${senderId.replace(conversationId, uuid)}`,
});
}
console.log(
`Updated ${senderKeys.length} sender keys: ` +
`updated: ${updated}, deleted: ${deleted}`
);
};
const updateSessions = (ourUuid: string) => {
// Use uuid instead of conversation id in existing sesions and prefix id
// with ourUuid.
//
// Set ourUuid column and field in json
const allSessions = db
.prepare<EmptyQuery>('SELECT id, conversationId FROM SESSIONS')
.all();
console.log(`Updating ${allSessions.length} sessions`);
const updateSession = db.prepare<Query>(
`
UPDATE sessions
SET
id = $newId,
ourUuid = $ourUuid,
uuid = $uuid,
json = json_set(
sessions.json,
'$.id',
$newId,
'$.uuid',
$uuid,
'$.ourUuid',
$ourUuid
)
WHERE
id = $id
`
);
const deleteSession = db.prepare<Query>(
'DELETE FROM sessions WHERE id = $id'
);
let updated = 0;
let deleted = 0;
for (const { id, conversationId } of allSessions) {
const { uuid } = getConversationUuid.get({ conversationId });
if (!uuid) {
deleted += 1;
deleteSession.run({ id });
continue;
}
const newId = `${ourUuid}:${id.replace(conversationId, uuid)}`;
updated += 1;
updateSession.run({
id,
newId,
uuid,
ourUuid,
});
}
console.log(
`Updated ${allSessions.length} sessions: ` +
`updated: ${updated}, deleted: ${deleted}`
);
};
const updateIdentityKeys = () => {
const identityKeys: ReadonlyArray<{
id: string;
}> = db.prepare<EmptyQuery>('SELECT id FROM identityKeys').all();
console.log(`Updating ${identityKeys.length} identity keys`);
const updateIdentityKey = db.prepare<Query>(
`
UPDATE identityKeys
SET
id = $newId,
json = json_set(
identityKeys.json,
'$.id',
$newId
)
WHERE
id = $id
`
);
let migrated = 0;
for (const { id } of identityKeys) {
const { uuid } = getConversationUuid.get({ conversationId: id });
let newId: string;
if (uuid) {
migrated += 1;
newId = uuid;
} else {
newId = `conversation:${id}`;
}
updateIdentityKey.run({ id, newId });
}
console.log(`Migrated ${migrated} identity keys`);
};
db.transaction(() => {
db.exec(
`
-- Change type of 'id' column from INTEGER to STRING
ALTER TABLE preKeys
RENAME TO old_preKeys;
ALTER TABLE signedPreKeys
RENAME TO old_signedPreKeys;
CREATE TABLE preKeys(
id STRING PRIMARY KEY ASC,
json TEXT
);
CREATE TABLE signedPreKeys(
id STRING PRIMARY KEY ASC,
json TEXT
);
-- sqlite handles the type conversion
INSERT INTO preKeys SELECT * FROM old_preKeys;
INSERT INTO signedPreKeys SELECT * FROM old_signedPreKeys;
DROP TABLE old_preKeys;
DROP TABLE old_signedPreKeys;
-- Alter sessions
ALTER TABLE sessions
ADD COLUMN ourUuid STRING;
ALTER TABLE sessions
ADD COLUMN uuid STRING;
`
);
const ourUuid = getOurUuid(db);
if (!isValidGuid(ourUuid)) {
console.error(
'updateToSchemaVersion41: no uuid is available clearing sessions'
);
clearSessionsAndKeys();
db.pragma('user_version = 41');
return;
}
prefixKeys(ourUuid);
updateSessions(ourUuid);
moveIdentityKeyToMap(ourUuid);
updateIdentityKeys();
db.pragma('user_version = 41');
})();
console.log('updateToSchemaVersion41: success!');
}
const SCHEMA_VERSIONS = [
updateToSchemaVersion1,
updateToSchemaVersion2,
@ -2142,6 +2479,7 @@ const SCHEMA_VERSIONS = [
updateToSchemaVersion38,
updateToSchemaVersion39,
updateToSchemaVersion40,
updateToSchemaVersion41,
];
function updateSchema(db: Database): void {
@ -2173,6 +2511,23 @@ function updateSchema(db: Database): void {
}
}
function getOurUuid(db: Database): string | undefined {
const UUID_ID: ItemKeyType = 'uuid_id';
const row: { json: string } | undefined = db
.prepare<Query>('SELECT json FROM items WHERE id = $id;')
.get({ id: UUID_ID });
if (!row) {
return undefined;
}
const { value } = JSON.parse(row.json);
const [ourUuid] = Helpers.unencodeNumber(String(value).toLowerCase());
return ourUuid;
}
let globalInstance: Database | undefined;
let globalInstanceRenderer: Database | undefined;
let databaseFilePath: string | undefined;
@ -2370,13 +2725,15 @@ const IDENTITY_KEYS_TABLE = 'identityKeys';
function createOrUpdateIdentityKey(data: IdentityKeyType): Promise<void> {
return createOrUpdate(IDENTITY_KEYS_TABLE, data);
}
function getIdentityKeyById(id: string): Promise<IdentityKeyType | undefined> {
async function getIdentityKeyById(
id: IdentityKeyIdType
): Promise<IdentityKeyType | undefined> {
return getById(IDENTITY_KEYS_TABLE, id);
}
function bulkAddIdentityKeys(array: Array<IdentityKeyType>): Promise<void> {
return bulkAdd(IDENTITY_KEYS_TABLE, array);
}
function removeIdentityKeyById(id: string): Promise<void> {
async function removeIdentityKeyById(id: IdentityKeyIdType): Promise<void> {
return removeById(IDENTITY_KEYS_TABLE, id);
}
function removeAllIdentityKeys(): Promise<void> {
@ -2390,13 +2747,15 @@ const PRE_KEYS_TABLE = 'preKeys';
function createOrUpdatePreKey(data: PreKeyType): Promise<void> {
return createOrUpdate(PRE_KEYS_TABLE, data);
}
function getPreKeyById(id: number): Promise<PreKeyType | undefined> {
async function getPreKeyById(
id: PreKeyIdType
): Promise<PreKeyType | undefined> {
return getById(PRE_KEYS_TABLE, id);
}
function bulkAddPreKeys(array: Array<PreKeyType>): Promise<void> {
return bulkAdd(PRE_KEYS_TABLE, array);
}
function removePreKeyById(id: number): Promise<void> {
async function removePreKeyById(id: PreKeyIdType): Promise<void> {
return removeById(PRE_KEYS_TABLE, id);
}
function removeAllPreKeys(): Promise<void> {
@ -2410,15 +2769,15 @@ const SIGNED_PRE_KEYS_TABLE = 'signedPreKeys';
function createOrUpdateSignedPreKey(data: SignedPreKeyType): Promise<void> {
return createOrUpdate(SIGNED_PRE_KEYS_TABLE, data);
}
function getSignedPreKeyById(
id: number
async function getSignedPreKeyById(
id: SignedPreKeyIdType
): Promise<SignedPreKeyType | undefined> {
return getById(SIGNED_PRE_KEYS_TABLE, id);
}
function bulkAddSignedPreKeys(array: Array<SignedPreKeyType>): Promise<void> {
return bulkAdd(SIGNED_PRE_KEYS_TABLE, array);
}
function removeSignedPreKeyById(id: number): Promise<void> {
async function removeSignedPreKeyById(id: SignedPreKeyIdType): Promise<void> {
return removeById(SIGNED_PRE_KEYS_TABLE, id);
}
function removeAllSignedPreKeys(): Promise<void> {
@ -2445,7 +2804,7 @@ function createOrUpdateItem<K extends ItemKeyType>(
): Promise<void> {
return createOrUpdate(ITEMS_TABLE, data);
}
function getItemById<K extends ItemKeyType>(
async function getItemById<K extends ItemKeyType>(
id: K
): Promise<ItemType<K> | undefined> {
return getById(ITEMS_TABLE, id);
@ -2467,7 +2826,7 @@ async function getAllItems(): Promise<AllItemsType> {
return result;
}
function removeItemById(id: ItemKeyType): Promise<void> {
async function removeItemById(id: ItemKeyType): Promise<void> {
return removeById(ITEMS_TABLE, id);
}
function removeAllItems(): Promise<void> {
@ -2497,7 +2856,7 @@ async function createOrUpdateSenderKey(key: SenderKeyType): Promise<void> {
).run(key);
}
async function getSenderKeyById(
id: string
id: SenderKeyIdType
): Promise<SenderKeyType | undefined> {
const db = getInstance();
const row = prepare(db, 'SELECT * FROM senderKeys WHERE id = $id').get({
@ -2516,7 +2875,7 @@ async function getAllSenderKeys(): Promise<Array<SenderKeyType>> {
return rows;
}
async function removeSenderKeyById(id: string): Promise<void> {
async function removeSenderKeyById(id: SenderKeyIdType): Promise<void> {
const db = getInstance();
prepare(db, 'DELETE FROM senderKeys WHERE id = $id').run({ id });
}
@ -2840,7 +3199,7 @@ async function _getAllSentProtoMessageIds(): Promise<Array<SentMessageDBType>> {
const SESSIONS_TABLE = 'sessions';
function createOrUpdateSessionSync(data: SessionType): void {
const db = getInstance();
const { id, conversationId } = data;
const { id, conversationId, ourUuid, uuid } = data;
if (!id) {
throw new Error(
'createOrUpdateSession: Provided data did not have a truthy id'
@ -2858,16 +3217,22 @@ function createOrUpdateSessionSync(data: SessionType): void {
INSERT OR REPLACE INTO sessions (
id,
conversationId,
ourUuid,
uuid,
json
) values (
$id,
$conversationId,
$ourUuid,
$uuid,
$json
)
`
).run({
id,
conversationId,
ourUuid,
uuid,
json: objectToJSON(data),
});
}
@ -2910,7 +3275,7 @@ async function commitSessionsAndUnprocessed({
function bulkAddSessions(array: Array<SessionType>): Promise<void> {
return bulkAdd(SESSIONS_TABLE, array);
}
function removeSessionById(id: string): Promise<void> {
async function removeSessionById(id: SessionIdType): Promise<void> {
return removeById(SESSIONS_TABLE, id);
}
async function removeSessionsByConversation(
@ -2933,11 +3298,11 @@ function getAllSessions(): Promise<Array<SessionType>> {
return getAllFromTable(SESSIONS_TABLE);
}
function createOrUpdateSync(
function createOrUpdateSync<Key extends string | number>(
table: string,
data: Record<string, unknown> & { id: string | number }
data: Record<string, unknown> & { id: Key },
db = getInstance()
): void {
const db = getInstance();
const { id } = data;
if (!id) {
throw new Error('createOrUpdate: Provided data did not have a truthy id');
@ -2979,11 +3344,11 @@ async function bulkAdd(
})();
}
async function getById<T>(
function getById<Key extends string | number, Result = unknown>(
table: string,
id: string | number
): Promise<T | undefined> {
const db = getInstance();
id: Key,
db = getInstance()
): Result | undefined {
const row = db
.prepare<Query>(
`
@ -3003,12 +3368,11 @@ async function getById<T>(
return jsonToObject(row.json);
}
async function removeById(
function removeById<Key extends string | number>(
table: string,
id: string | number | Array<string | number>
): Promise<void> {
const db = getInstance();
id: Key | Array<Key>,
db = getInstance()
): void {
if (!Array.isArray(id)) {
db.prepare<Query>(
`
@ -4922,7 +5286,7 @@ async function resetAttachmentDownloadPending(): Promise<void> {
`
).run();
}
function removeAttachmentDownloadJob(id: string): Promise<void> {
async function removeAttachmentDownloadJob(id: string): Promise<void> {
return removeById(ATTACHMENT_DOWNLOADS_TABLE, id);
}
function removeAllAttachmentDownloadJobs(): Promise<void> {

View file

@ -6,6 +6,7 @@
*/
import { assert } from 'chai';
import { v4 as getGuid } from 'uuid';
import MessageReceiver from '../textsecure/MessageReceiver';
import { IncomingWebSocketRequest } from '../textsecure/WebsocketResources';
@ -22,11 +23,20 @@ describe('MessageReceiver', () => {
const uuid = 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee';
const deviceId = 1;
let oldUuid: string | undefined;
let oldDeviceId: number | undefined;
beforeEach(async () => {
oldUuid = window.storage.user.getUuid()?.toString();
oldDeviceId = window.storage.user.getDeviceId();
await window.storage.user.setUuidAndDeviceId(getGuid(), 2);
await window.storage.protocol.hydrateCaches();
});
afterEach(async () => {
if (oldUuid !== undefined && oldDeviceId !== undefined) {
await window.storage.user.setUuidAndDeviceId(oldUuid, oldDeviceId);
}
await window.storage.protocol.removeAllUnprocessed();
});

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,12 @@
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import { v4 as getGuid } from 'uuid';
import { getRandomBytes } from '../../Crypto';
import AccountManager from '../../textsecure/AccountManager';
import { OuterSignedPrekeyType } from '../../textsecure/Types.d';
import { UUID } from '../../types/UUID';
/* eslint-disable @typescript-eslint/no-explicit-any */
@ -21,15 +23,18 @@ describe('AccountManager', () => {
let originalGetIdentityKeyPair: any;
let originalLoadSignedPreKeys: any;
let originalRemoveSignedPreKey: any;
let originalGetUuid: any;
let signedPreKeys: Array<OuterSignedPrekeyType>;
const DAY = 1000 * 60 * 60 * 24;
const pubKey = getRandomBytes(33);
const privKey = getRandomBytes(32);
const identityKey = window.Signal.Curve.generateKeyPair();
beforeEach(async () => {
const identityKey = window.Signal.Curve.generateKeyPair();
const ourUuid = new UUID(getGuid());
originalGetUuid = window.textsecure.storage.user.getUuid;
originalGetIdentityKeyPair =
window.textsecure.storage.protocol.getIdentityKeyPair;
originalLoadSignedPreKeys =
@ -37,12 +42,15 @@ describe('AccountManager', () => {
originalRemoveSignedPreKey =
window.textsecure.storage.protocol.removeSignedPreKey;
window.textsecure.storage.user.getUuid = () => ourUuid;
window.textsecure.storage.protocol.getIdentityKeyPair = async () =>
identityKey;
window.textsecure.storage.protocol.loadSignedPreKeys = async () =>
signedPreKeys;
});
afterEach(() => {
window.textsecure.storage.user.getUuid = originalGetUuid;
window.textsecure.storage.protocol.getIdentityKeyPair = originalGetIdentityKeyPair;
window.textsecure.storage.protocol.loadSignedPreKeys = originalLoadSignedPreKeys;
window.textsecure.storage.protocol.removeSignedPreKey = originalRemoveSignedPreKey;
@ -51,7 +59,10 @@ describe('AccountManager', () => {
describe('encrypted device name', () => {
it('roundtrips', async () => {
const deviceName = 'v2.5.0 on Ubunto 20.04';
const encrypted = await accountManager.encryptDeviceName(deviceName);
const encrypted = await accountManager.encryptDeviceName(
deviceName,
identityKey
);
if (!encrypted) {
throw new Error('failed to encrypt!');
}
@ -62,7 +73,10 @@ describe('AccountManager', () => {
});
it('handles falsey deviceName', async () => {
const encrypted = await accountManager.encryptDeviceName('');
const encrypted = await accountManager.encryptDeviceName(
'',
identityKey
);
assert.strictEqual(encrypted, null);
});
});
@ -146,7 +160,10 @@ describe('AccountManager', () => {
];
let count = 0;
window.textsecure.storage.protocol.removeSignedPreKey = async keyId => {
window.textsecure.storage.protocol.removeSignedPreKey = async (
_,
keyId
) => {
if (keyId !== 4) {
throw new Error(`Wrong keys were eliminated! ${keyId}`);
}

View file

@ -1,43 +1,56 @@
// Copyright 2017-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* global ConversationController, SignalProtocolStore, Whisper */
import { assert } from 'chai';
import { v4 as getGuid } from 'uuid';
import { getRandomBytes } from '../../Crypto';
import { Address } from '../../types/Address';
import { UUID } from '../../types/UUID';
import { SignalProtocolStore } from '../../SignalProtocolStore';
import type { ConversationModel } from '../../models/conversations';
import * as KeyChangeListener from '../../textsecure/KeyChangeListener';
const { Whisper } = window;
describe('KeyChangeListener', () => {
const STORAGE_KEYS_TO_RESTORE = ['number_id', 'uuid_id'];
const oldStorageValues = new Map();
let oldNumberId: string | undefined;
let oldUuidId: string | undefined;
const phoneNumberWithKeyChange = '+13016886524'; // nsa
const addressString = `${phoneNumberWithKeyChange}.1`;
const oldKey = window.Signal.Crypto.getRandomBytes(33);
const newKey = window.Signal.Crypto.getRandomBytes(33);
let store;
const ourUuid = getGuid();
const uuidWithKeyChange = getGuid();
const address = Address.create(uuidWithKeyChange, 1);
const oldKey = getRandomBytes(33);
const newKey = getRandomBytes(33);
let store: SignalProtocolStore;
before(async () => {
window.ConversationController.reset();
await window.ConversationController.load();
STORAGE_KEYS_TO_RESTORE.forEach(key => {
oldStorageValues.set(key, window.textsecure.storage.get(key));
});
window.textsecure.storage.put('number_id', '+14155555556.2');
window.textsecure.storage.put('uuid_id', `${window.getGuid()}.2`);
const { storage } = window.textsecure;
oldNumberId = storage.get('number_id');
oldUuidId = storage.get('uuid_id');
await storage.put('number_id', '+14155555556.2');
await storage.put('uuid_id', `${ourUuid}.2`);
});
after(async () => {
await window.Signal.Data.removeAll();
await window.storage.fetch();
oldStorageValues.forEach((oldValue, key) => {
if (oldValue) {
window.textsecure.storage.put(key, oldValue);
} else {
window.textsecure.storage.remove(key);
}
});
const { storage } = window.textsecure;
await storage.fetch();
if (oldNumberId) {
await storage.put('number_id', oldNumberId);
}
if (oldUuidId) {
await storage.put('uuid_id', oldUuidId);
}
});
let convo;
let convo: ConversationModel;
beforeEach(async () => {
window.ConversationController.reset();
@ -45,46 +58,46 @@ describe('KeyChangeListener', () => {
await window.ConversationController.loadPromise();
convo = window.ConversationController.dangerouslyCreateAndAdd({
id: phoneNumberWithKeyChange,
id: uuidWithKeyChange,
type: 'private',
});
await window.Signal.Data.saveConversation(convo.attributes);
store = new SignalProtocolStore();
await store.hydrateCaches();
Whisper.KeyChangeListener.init(store);
return store.saveIdentity(addressString, oldKey);
KeyChangeListener.init(store);
return store.saveIdentity(address, oldKey);
});
afterEach(async () => {
await window.Signal.Data.removeAllMessagesInConversation(convo.id, {
logId: phoneNumberWithKeyChange,
logId: uuidWithKeyChange,
MessageCollection: Whisper.MessageCollection,
});
await window.Signal.Data.removeConversation(convo.id, {
Conversation: Whisper.Conversation,
});
await store.removeIdentityKey(phoneNumberWithKeyChange);
await store.removeIdentityKey(new UUID(uuidWithKeyChange));
});
describe('When we have a conversation with this contact', () => {
it('generates a key change notice in the private conversation with this contact', done => {
const original = convo.addKeyChange;
convo.addKeyChange = keyChangedId => {
assert.equal(phoneNumberWithKeyChange, keyChangedId);
convo.addKeyChange = async keyChangedId => {
assert.equal(uuidWithKeyChange, keyChangedId.toString());
convo.addKeyChange = original;
done();
};
store.saveIdentity(addressString, newKey);
store.saveIdentity(address, newKey);
});
});
describe('When we have a group with this contact', () => {
let groupConvo;
let groupConvo: ConversationModel;
beforeEach(async () => {
groupConvo = ConversationController.dangerouslyCreateAndAdd({
groupConvo = window.ConversationController.dangerouslyCreateAndAdd({
id: 'groupId',
type: 'group',
members: [convo.id],
@ -94,7 +107,7 @@ describe('KeyChangeListener', () => {
afterEach(async () => {
await window.Signal.Data.removeAllMessagesInConversation(groupConvo.id, {
logId: phoneNumberWithKeyChange,
logId: uuidWithKeyChange,
MessageCollection: Whisper.MessageCollection,
});
await window.Signal.Data.removeConversation(groupConvo.id, {
@ -104,13 +117,13 @@ describe('KeyChangeListener', () => {
it('generates a key change notice in the group conversation with this contact', done => {
const original = groupConvo.addKeyChange;
groupConvo.addKeyChange = keyChangedId => {
assert.equal(phoneNumberWithKeyChange, keyChangedId);
groupConvo.addKeyChange = async keyChangedId => {
assert.equal(uuidWithKeyChange, keyChangedId.toString());
groupConvo.addKeyChange = original;
done();
};
store.saveIdentity(addressString, newKey);
store.saveIdentity(address, newKey);
});
});
});

View file

@ -0,0 +1,198 @@
// Copyright 2015-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { assert } from 'chai';
import {
typedArrayToArrayBuffer as toArrayBuffer,
arrayBufferToBase64 as toBase64,
constantTimeEqual,
} from '../../Crypto';
import { generateKeyPair } from '../../Curve';
import AccountManager, {
GeneratedKeysType,
} from '../../textsecure/AccountManager';
import { PreKeyType, SignedPreKeyType } from '../../textsecure/Types.d';
import { UUID } from '../../types/UUID';
const { textsecure } = window;
const assertEqualArrayBuffers = (a: ArrayBuffer, b: ArrayBuffer) => {
assert.isTrue(constantTimeEqual(a, b));
};
describe('Key generation', function thisNeeded() {
const count = 10;
const ourUuid = new UUID('aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee');
this.timeout(count * 2000);
function itStoresPreKey(keyId: number): void {
it(`prekey ${keyId} is valid`, async () => {
const keyPair = await textsecure.storage.protocol.loadPreKey(
ourUuid,
keyId
);
assert(keyPair, `PreKey ${keyId} not found`);
});
}
function itStoresSignedPreKey(keyId: number): void {
it(`signed prekey ${keyId} is valid`, async () => {
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
ourUuid,
keyId
);
assert(keyPair, `SignedPreKey ${keyId} not found`);
});
}
async function validateResultKey(
resultKey: Pick<PreKeyType, 'keyId' | 'publicKey'>
): Promise<void> {
const keyPair = await textsecure.storage.protocol.loadPreKey(
ourUuid,
resultKey.keyId
);
if (!keyPair) {
throw new Error(`PreKey ${resultKey.keyId} not found`);
}
assertEqualArrayBuffers(
resultKey.publicKey,
toArrayBuffer(keyPair.publicKey().serialize())
);
}
async function validateResultSignedKey(
resultSignedKey: Pick<SignedPreKeyType, 'keyId' | 'publicKey'>
) {
const keyPair = await textsecure.storage.protocol.loadSignedPreKey(
ourUuid,
resultSignedKey.keyId
);
if (!keyPair) {
throw new Error(`SignedPreKey ${resultSignedKey.keyId} not found`);
}
assertEqualArrayBuffers(
resultSignedKey.publicKey,
toArrayBuffer(keyPair.publicKey().serialize())
);
}
before(async () => {
const keyPair = generateKeyPair();
await textsecure.storage.put('identityKeyMap', {
[ourUuid.toString()]: {
privKey: toBase64(keyPair.privKey),
pubKey: toBase64(keyPair.pubKey),
},
});
await textsecure.storage.user.setUuidAndDeviceId(ourUuid.toString(), 1);
await textsecure.storage.protocol.hydrateCaches();
});
after(async () => {
await textsecure.storage.protocol.clearPreKeyStore();
await textsecure.storage.protocol.clearSignedPreKeysStore();
});
describe('the first time', () => {
let result: GeneratedKeysType;
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
});
for (let i = 1; i <= count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 0; i < count; i += 1) {
assert.strictEqual(result.preKeys[i].keyId, i + 1);
}
});
it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey));
});
it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 1);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
describe('the second time', () => {
let result: GeneratedKeysType;
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
});
for (let i = 1; i <= 2 * count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(1);
itStoresSignedPreKey(2);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 1; i <= count; i += 1) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
}
});
it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey));
});
it('returns a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 2);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
describe('the third time', () => {
let result: GeneratedKeysType;
before(async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const accountManager = new AccountManager({} as any);
result = await accountManager.generateKeys(count);
});
for (let i = 1; i <= 3 * count; i += 1) {
itStoresPreKey(i);
}
itStoresSignedPreKey(2);
itStoresSignedPreKey(3);
it(`result contains ${count} preKeys`, () => {
assert.isArray(result.preKeys);
assert.lengthOf(result.preKeys, count);
for (let i = 0; i < count; i += 1) {
assert.isObject(result.preKeys[i]);
}
});
it('result contains the correct keyIds', () => {
for (let i = 1; i <= count; i += 1) {
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
}
});
it('result contains the correct public keys', async () => {
await Promise.all(result.preKeys.map(validateResultKey));
});
it('result contains a signed prekey', () => {
assert.strictEqual(result.signedPreKey.keyId, 3);
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
return validateResultSignedKey(result.signedPreKey);
});
});
});

View file

@ -7,6 +7,7 @@
/* eslint-disable class-methods-use-this */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import PQueue from 'p-queue';
import { omit } from 'lodash';
import EventTarget from './EventTarget';
import { WebAPIType } from './WebAPI';
@ -21,6 +22,7 @@ import {
generateRegistrationId,
getRandomBytes,
typedArrayToArrayBuffer,
arrayBufferToBase64,
} from '../Crypto';
import {
generateKeyPair,
@ -29,7 +31,7 @@ import {
} from '../Curve';
import { isMoreRecentThan, isOlderThan } from '../util/timestamp';
import { ourProfileKeyService } from '../services/ourProfileKey';
import { assert } from '../util/assert';
import { assert, strictAssert } from '../util/assert';
import { getProvisioningUrl } from '../util/getProvisioningUrl';
import { SignalService as Proto } from '../protobuf';
@ -56,7 +58,7 @@ function getIdentifier(id: string | undefined) {
return parts[0];
}
type GeneratedKeysType = {
export type GeneratedKeysType = {
preKeys: Array<{
keyId: number;
publicKey: ArrayBuffer;
@ -89,16 +91,10 @@ export default class AccountManager extends EventTarget {
return this.server.requestVerificationSMS(number);
}
async encryptDeviceName(name: string, providedIdentityKey?: KeyPairType) {
async encryptDeviceName(name: string, identityKey: KeyPairType) {
if (!name) {
return null;
}
const identityKey =
providedIdentityKey ||
(await window.textsecure.storage.protocol.getIdentityKeyPair());
if (!identityKey) {
throw new Error('Identity key was not provided and is not in database!');
}
const encrypted = await window.Signal.Crypto.encryptDeviceName(
name,
identityKey.pubKey
@ -114,7 +110,10 @@ export default class AccountManager extends EventTarget {
}
async decryptDeviceName(base64: string) {
const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair();
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const identityKey = await window.textsecure.storage.protocol.getIdentityKeyPair(
ourUuid
);
if (!identityKey) {
throw new Error('decryptDeviceName: No identity key pair!');
}
@ -144,8 +143,19 @@ export default class AccountManager extends EventTarget {
if (isNameEncrypted) {
return;
}
const deviceName = window.textsecure.storage.user.getDeviceName();
const base64 = await this.encryptDeviceName(deviceName || '');
const { storage } = window.textsecure;
const deviceName = storage.user.getDeviceName();
const identityKeyPair = await storage.protocol.getIdentityKeyPair(
storage.user.getCheckedUuid()
);
strictAssert(
identityKeyPair !== undefined,
"Can't encrypt device name without identity key pair"
);
const base64 = await this.encryptDeviceName(
deviceName || '',
identityKeyPair
);
if (base64) {
await this.server.updateDeviceName(base64);
@ -310,6 +320,7 @@ export default class AccountManager extends EventTarget {
async rotateSignedPreKey() {
return this.queueTask(async () => {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
if (typeof signedKeyId !== 'number') {
throw new Error('Invalid signedKeyId');
@ -318,7 +329,7 @@ export default class AccountManager extends EventTarget {
const store = window.textsecure.storage.protocol;
const { server, cleanSignedPreKeys } = this;
const existingKeys = await store.loadSignedPreKeys();
const existingKeys = await store.loadSignedPreKeys(ourUuid);
existingKeys.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
const confirmedKeys = existingKeys.filter(key => key.confirmed);
const mostRecent = confirmedKeys[0];
@ -332,7 +343,7 @@ export default class AccountManager extends EventTarget {
// eslint-disable-next-line consistent-return
return store
.getIdentityKeyPair()
.getIdentityKeyPair(ourUuid)
.then(
async (identityKey: KeyPairType | undefined) => {
if (!identityKey) {
@ -357,7 +368,7 @@ export default class AccountManager extends EventTarget {
window.log.info('Saving new signed prekey', res.keyId);
return Promise.all([
window.textsecure.storage.put('signedKeyId', signedKeyId + 1),
store.storeSignedPreKey(res.keyId, res.keyPair),
store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair),
server.setSignedPreKey({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
@ -369,7 +380,12 @@ export default class AccountManager extends EventTarget {
window.log.info('Confirming new signed prekey', res.keyId);
return Promise.all([
window.textsecure.storage.remove('signedKeyRotationRejected'),
store.storeSignedPreKey(res.keyId, res.keyPair, confirmed),
store.storeSignedPreKey(
ourUuid,
res.keyId,
res.keyPair,
confirmed
),
]);
})
.then(cleanSignedPreKeys);
@ -409,9 +425,10 @@ export default class AccountManager extends EventTarget {
}
async cleanSignedPreKeys() {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const store = window.textsecure.storage.protocol;
const allKeys = await store.loadSignedPreKeys();
const allKeys = await store.loadSignedPreKeys(ourUuid);
allKeys.sort((a, b) => (b.created_at || 0) - (a.created_at || 0));
const confirmed = allKeys.filter(key => key.confirmed);
const unconfirmed = allKeys.filter(key => !key.confirmed);
@ -448,7 +465,7 @@ export default class AccountManager extends EventTarget {
window.log.info(
`Removing signed prekey: ${key.keyId} with timestamp ${timestamp}${confirmedText}`
);
await store.removeSignedPreKey(key.keyId);
await store.removeSignedPreKey(ourUuid, key.keyId);
}
})
);
@ -464,17 +481,14 @@ export default class AccountManager extends EventTarget {
readReceipts?: boolean | null,
options: { accessKey?: ArrayBuffer; uuid?: string } = {}
): Promise<void> {
const { storage } = window.textsecure;
const { accessKey, uuid } = options;
let password = btoa(utils.getString(getRandomBytes(16)));
password = password.substring(0, password.length - 2);
const registrationId = generateRegistrationId();
const previousNumber = getIdentifier(
window.textsecure.storage.get('number_id')
);
const previousUuid = getIdentifier(
window.textsecure.storage.get('uuid_id')
);
const previousNumber = getIdentifier(storage.get('number_id'));
const previousUuid = getIdentifier(storage.get('uuid_id'));
let encryptedDeviceName;
if (deviceName) {
@ -500,7 +514,10 @@ export default class AccountManager extends EventTarget {
{ accessKey, uuid }
);
const uuidChanged = previousUuid && uuid && previousUuid !== uuid;
const ourUuid = uuid || response.uuid;
strictAssert(ourUuid !== undefined, 'Should have UUID after registration');
const uuidChanged = previousUuid && ourUuid && previousUuid !== ourUuid;
// We only consider the number changed if we didn't have a UUID before
const numberChanged =
@ -519,7 +536,7 @@ export default class AccountManager extends EventTarget {
}
try {
await window.textsecure.storage.protocol.removeAllData();
await storage.protocol.removeAllData();
window.log.info('Successfully deleted previous data');
} catch (error) {
window.log.error(
@ -530,23 +547,34 @@ export default class AccountManager extends EventTarget {
}
await Promise.all([
window.textsecure.storage.remove('identityKey'),
window.textsecure.storage.user.removeCredentials(),
window.textsecure.storage.remove('registrationId'),
window.textsecure.storage.remove('regionCode'),
window.textsecure.storage.remove('userAgent'),
window.textsecure.storage.remove('profileKey'),
window.textsecure.storage.remove('read-receipt-setting'),
storage.user.removeCredentials(),
storage.remove('regionCode'),
storage.remove('userAgent'),
storage.remove('profileKey'),
storage.remove('read-receipt-setting'),
]);
if (previousUuid) {
await Promise.all([
storage.put(
'identityKeyMap',
omit(storage.get('identityKeyMap') || {}, previousUuid)
),
storage.put(
'registrationIdMap',
omit(storage.get('registrationIdMap') || {}, previousUuid)
),
]);
}
// `setCredentials` needs to be called
// before `saveIdentifyWithAttributes` since `saveIdentityWithAttributes`
// indirectly calls `ConversationController.getConverationId()` which
// initializes the conversation for the given number (our number) which
// calls out to the user storage API to get the stored UUID and number
// information.
await window.textsecure.storage.user.setCredentials({
uuid,
await storage.user.setCredentials({
uuid: ourUuid,
number,
deviceId: response.deviceId ?? 1,
deviceName: deviceName ?? undefined,
@ -558,7 +586,7 @@ export default class AccountManager extends EventTarget {
// below.
const conversationId = window.ConversationController.ensureContactIds({
e164: number,
uuid,
uuid: ourUuid,
highTrust: true,
});
@ -568,36 +596,42 @@ export default class AccountManager extends EventTarget {
// update our own identity key, which may have changed
// if we're relinking after a reinstall on the master device
await window.textsecure.storage.protocol.saveIdentityWithAttributes(
uuid || number,
{
publicKey: identityKeyPair.pubKey,
firstUse: true,
timestamp: Date.now(),
verified: window.textsecure.storage.protocol.VerifiedStatus.VERIFIED,
nonblockingApproval: true,
}
);
await storage.protocol.saveIdentityWithAttributes(ourUuid, {
publicKey: identityKeyPair.pubKey,
firstUse: true,
timestamp: Date.now(),
verified: storage.protocol.VerifiedStatus.VERIFIED,
nonblockingApproval: true,
});
await window.textsecure.storage.put('identityKey', identityKeyPair);
await window.textsecure.storage.put('registrationId', registrationId);
const identityKeyMap = {
...(storage.get('identityKeyMap') || {}),
[ourUuid]: {
pubKey: arrayBufferToBase64(identityKeyPair.pubKey),
privKey: arrayBufferToBase64(identityKeyPair.privKey),
},
};
const registrationIdMap = {
...(storage.get('registrationIdMap') || {}),
[ourUuid]: registrationId,
};
await storage.put('identityKeyMap', identityKeyMap);
await storage.put('registrationIdMap', registrationIdMap);
if (profileKey) {
await ourProfileKeyService.set(profileKey);
}
if (userAgent) {
await window.textsecure.storage.put('userAgent', userAgent);
await storage.put('userAgent', userAgent);
}
await window.textsecure.storage.put(
'read-receipt-setting',
Boolean(readReceipts)
);
await storage.put('read-receipt-setting', Boolean(readReceipts));
const regionCode = window.libphonenumber.util.getRegionCodeForNumber(
number
);
await window.textsecure.storage.put('regionCode', regionCode);
await window.textsecure.storage.protocol.hydrateCaches();
await storage.put('regionCode', regionCode);
await storage.protocol.hydrateCaches();
}
async clearSessionsAndPreKeys() {
@ -626,7 +660,8 @@ export default class AccountManager extends EventTarget {
}
window.log.info('confirmKeys: confirming key', key.keyId);
await store.storeSignedPreKey(key.keyId, key.keyPair, confirmed);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await store.storeSignedPreKey(ourUuid, key.keyId, key.keyPair, confirmed);
}
async generateKeys(count: number, providedProgressCallback?: Function) {
@ -636,6 +671,7 @@ export default class AccountManager extends EventTarget {
: null;
const startId = window.textsecure.storage.get('maxPreKeyId', 1);
const signedKeyId = window.textsecure.storage.get('signedKeyId', 1);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
if (typeof startId !== 'number') {
throw new Error('Invalid maxPreKeyId');
@ -645,7 +681,7 @@ export default class AccountManager extends EventTarget {
}
const store = window.textsecure.storage.protocol;
return store.getIdentityKeyPair().then(async identityKey => {
return store.getIdentityKeyPair(ourUuid).then(async identityKey => {
if (!identityKey) {
throw new Error('generateKeys: No identity key pair!');
}
@ -659,7 +695,7 @@ export default class AccountManager extends EventTarget {
for (let keyId = startId; keyId < startId + count; keyId += 1) {
promises.push(
Promise.resolve(generatePreKey(keyId)).then(async res => {
await store.storePreKey(res.keyId, res.keyPair);
await store.storePreKey(ourUuid, res.keyId, res.keyPair);
result.preKeys.push({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
@ -674,7 +710,7 @@ export default class AccountManager extends EventTarget {
promises.push(
Promise.resolve(generateSignedPreKey(identityKey, signedKeyId)).then(
async res => {
await store.storeSignedPreKey(res.keyId, res.keyPair);
await store.storeSignedPreKey(ourUuid, res.keyId, res.keyPair);
result.signedPreKey = {
keyId: res.keyId,
publicKey: res.keyPair.pubKey,

View file

@ -0,0 +1,25 @@
// Copyright 2017-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { UUID } from '../types/UUID';
import type { SignalProtocolStore } from '../SignalProtocolStore';
export function init(signalProtocolStore: SignalProtocolStore): void {
signalProtocolStore.on(
'keychange',
async (uuid: UUID): Promise<void> => {
const conversation = await window.ConversationController.getOrCreateAndWait(
uuid.toString(),
'private'
);
conversation.addKeyChange(uuid);
const groups = await window.ConversationController.getAllGroupsInvolvingId(
conversation.id
);
for (const group of groups) {
group.addKeyChange(uuid);
}
}
);
}

View file

@ -45,6 +45,9 @@ import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { Zone } from '../util/Zone';
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
import { DownloadedAttachmentType } from '../types/Attachment';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import * as Errors from '../types/errors';
import { SignalService as Proto } from '../protobuf';
@ -754,8 +757,9 @@ export default class MessageReceiver
pendingSessions: true,
pendingUnprocessed: true,
});
const sessionStore = new Sessions({ zone });
const identityKeyStore = new IdentityKeys({ zone });
const ourUuid = this.storage.user.getCheckedUuid();
const sessionStore = new Sessions({ zone, ourUuid });
const identityKeyStore = new IdentityKeys({ zone, ourUuid });
const failed: Array<UnprocessedType> = [];
// Below we:
@ -1228,18 +1232,12 @@ export default class MessageReceiver
ciphertext: Uint8Array
): Promise<DecryptSealedSenderResult> {
const localE164 = this.storage.user.getNumber();
const localUuid = this.storage.user.getUuid();
const ourUuid = this.storage.user.getCheckedUuid();
const localDeviceId = parseIntOrThrow(
this.storage.user.getDeviceId(),
'MessageReceiver.decryptSealedSender: localDeviceId'
);
if (!localUuid) {
throw new Error(
'MessageReceiver.decryptSealedSender: Failed to fetch local UUID'
);
}
const logId = this.getEnvelopeId(envelope);
const { unsealedContent: messageContent, certificate } = envelope;
@ -1280,9 +1278,12 @@ export default class MessageReceiver
);
const sealedSenderIdentifier = certificate.senderUuid();
const sealedSenderSourceDevice = certificate.senderDeviceId();
const senderKeyStore = new SenderKeys();
const senderKeyStore = new SenderKeys({ ourUuid });
const address = `${sealedSenderIdentifier}.${sealedSenderSourceDevice}`;
const address = new QualifiedAddress(
ourUuid,
Address.create(sealedSenderIdentifier, sealedSenderSourceDevice)
);
const plaintext = await this.storage.protocol.enqueueSenderKeyJob(
address,
@ -1305,11 +1306,22 @@ export default class MessageReceiver
'unidentified message/passing to sealedSenderDecryptMessage'
);
const preKeyStore = new PreKeys();
const signedPreKeyStore = new SignedPreKeys();
const preKeyStore = new PreKeys({ ourUuid });
const signedPreKeyStore = new SignedPreKeys({ ourUuid });
const sealedSenderIdentifier = envelope.sourceUuid || envelope.source;
const address = `${sealedSenderIdentifier}.${envelope.sourceDevice}`;
const sealedSenderIdentifier = envelope.sourceUuid;
strictAssert(
sealedSenderIdentifier !== undefined,
'Empty sealed sender identifier'
);
strictAssert(
envelope.sourceDevice !== undefined,
'Empty sealed sender device'
);
const address = new QualifiedAddress(
ourUuid,
Address.create(sealedSenderIdentifier, envelope.sourceDevice)
);
const unsealedPlaintext = await this.storage.protocol.enqueueSessionJob(
address,
() =>
@ -1318,7 +1330,7 @@ export default class MessageReceiver
PublicKey.deserialize(Buffer.from(this.serverTrustRoot)),
envelope.serverTimestamp,
localE164 || null,
localUuid,
ourUuid.toString(),
localDeviceId,
sessionStore,
identityKeyStore,
@ -1341,11 +1353,20 @@ export default class MessageReceiver
const logId = this.getEnvelopeId(envelope);
const envelopeTypeEnum = Proto.Envelope.Type;
const identifier = envelope.sourceUuid || envelope.source;
const identifier = envelope.sourceUuid;
const { sourceDevice } = envelope;
const preKeyStore = new PreKeys();
const signedPreKeyStore = new SignedPreKeys();
const ourUuid = this.storage.user.getCheckedUuid();
const preKeyStore = new PreKeys({ ourUuid });
const signedPreKeyStore = new SignedPreKeys({ ourUuid });
strictAssert(identifier !== undefined, 'Empty identifier');
strictAssert(sourceDevice !== undefined, 'Empty source device');
const address = new QualifiedAddress(
ourUuid,
Address.create(identifier, sourceDevice)
);
if (envelope.type === envelopeTypeEnum.PLAINTEXT_CONTENT) {
window.log.info(`decrypt/${logId}: plaintext message`);
@ -1368,7 +1389,6 @@ export default class MessageReceiver
}
const signalMessage = SignalMessage.deserialize(Buffer.from(ciphertext));
const address = `${identifier}.${sourceDevice}`;
const plaintext = await this.storage.protocol.enqueueSessionJob(
address,
async () =>
@ -1400,7 +1420,6 @@ export default class MessageReceiver
Buffer.from(ciphertext)
);
const address = `${identifier}.${sourceDevice}`;
const plaintext = await this.storage.protocol.enqueueSessionJob(
address,
async () =>
@ -1562,7 +1581,7 @@ export default class MessageReceiver
const isBlocked = groupId ? this.isGroupBlocked(groupId) : false;
const { source, sourceUuid } = envelope;
const ourE164 = this.storage.user.getNumber();
const ourUuid = this.storage.user.getUuid();
const ourUuid = this.storage.user.getCheckedUuid().toString();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
@ -1613,7 +1632,7 @@ export default class MessageReceiver
);
let p: Promise<void> = Promise.resolve();
// eslint-disable-next-line no-bitwise
const destination = envelope.sourceUuid || envelope.source;
const destination = envelope.sourceUuid;
if (!destination) {
throw new Error(
'MessageReceiver.handleDataMessage: source and sourceUuid were falsey'
@ -1651,7 +1670,7 @@ export default class MessageReceiver
const isBlocked = groupId ? this.isGroupBlocked(groupId) : false;
const { source, sourceUuid } = envelope;
const ourE164 = this.storage.user.getNumber();
const ourUuid = this.storage.user.getUuid();
const ourUuid = this.storage.user.getCheckedUuid().toString();
const isMe =
(source && ourE164 && source === ourE164) ||
(sourceUuid && ourUuid && sourceUuid === ourUuid);
@ -1711,8 +1730,7 @@ export default class MessageReceiver
}
const { timestamp } = envelope;
const identifier =
envelope.groupId || envelope.sourceUuid || envelope.source;
const identifier = envelope.groupId || envelope.sourceUuid;
const conversation = window.ConversationController.get(identifier);
try {
@ -1848,7 +1866,7 @@ export default class MessageReceiver
// Note: we don't call removeFromCache here because this message can be combined
// with a dataMessage, for example. That processing will dictate cache removal.
const identifier = envelope.sourceUuid || envelope.source;
const identifier = envelope.sourceUuid;
const { sourceDevice } = envelope;
if (!identifier) {
throw new Error(
@ -1865,8 +1883,12 @@ export default class MessageReceiver
const senderKeyDistributionMessage = SenderKeyDistributionMessage.deserialize(
Buffer.from(distributionMessage)
);
const senderKeyStore = new SenderKeys();
const address = `${identifier}.${sourceDevice}`;
const ourUuid = this.storage.user.getCheckedUuid();
const senderKeyStore = new SenderKeys({ ourUuid });
const address = new QualifiedAddress(
ourUuid,
Address.create(identifier, sourceDevice)
);
await this.storage.protocol.enqueueSenderKeyJob(
address,
@ -2109,11 +2131,11 @@ export default class MessageReceiver
syncMessage: ProcessedSyncMessage
): Promise<void> {
const ourNumber = this.storage.user.getNumber();
const ourUuid = this.storage.user.getUuid();
const ourUuid = this.storage.user.getCheckedUuid();
const fromSelfSource = envelope.source && envelope.source === ourNumber;
const fromSelfSourceUuid =
envelope.sourceUuid && envelope.sourceUuid === ourUuid;
envelope.sourceUuid && envelope.sourceUuid === ourUuid.toString();
if (!fromSelfSource && !fromSelfSourceUuid) {
throw new Error('Received sync message from another number');
}
@ -2536,8 +2558,14 @@ export default class MessageReceiver
}
private async handleEndSession(identifier: string): Promise<void> {
const theirUuid = UUID.lookup(identifier);
if (!theirUuid) {
window.log.warn(`handleEndSession: uuid not found for ${identifier}`);
return;
}
window.log.info(`handleEndSession: closing sessions for ${identifier}`);
await this.storage.protocol.archiveAllSessions(identifier);
await this.storage.protocol.archiveAllSessions(theirUuid);
}
private async processDecrypted(

View file

@ -32,6 +32,9 @@ import {
} from './Errors';
import { CallbackResultType, CustomError } from './Types.d';
import { isValidNumber } from '../types/PhoneNumber';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { Sessions, IdentityKeys } from '../LibSignalStores';
import { typedArrayToArrayBuffer as toArrayBuffer } from '../Crypto';
import { updateConversationsWithUuidLookup } from '../updateConversationsWithUuidLookup';
@ -237,9 +240,11 @@ export default class OutgoingMessage {
recurse?: boolean
): () => Promise<void> {
return async () => {
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
identifier
);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
ourUuid,
identifier,
});
if (deviceIds.length === 0) {
this.registerError(
identifier,
@ -386,9 +391,12 @@ export default class OutgoingMessage {
// We don't send to ourselves unless sealedSender is enabled
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
if ((identifier === ourNumber || identifier === ourUuid) && !sealedSender) {
if (
(identifier === ourNumber || identifier === ourUuid.toString()) &&
!sealedSender
) {
deviceIds = reject(
deviceIds,
deviceId =>
@ -399,18 +407,22 @@ export default class OutgoingMessage {
);
}
const sessionStore = new Sessions();
const identityKeyStore = new IdentityKeys();
const sessionStore = new Sessions({ ourUuid });
const identityKeyStore = new IdentityKeys({ ourUuid });
return Promise.all(
deviceIds.map(async destinationDeviceId => {
const address = `${identifier}.${destinationDeviceId}`;
const theirUuid = UUID.checkedLookup(identifier);
const address = new QualifiedAddress(
ourUuid,
new Address(theirUuid, destinationDeviceId)
);
return window.textsecure.storage.protocol.enqueueSessionJob<SendMetadata>(
address,
async () => {
const protocolAddress = ProtocolAddress.new(
identifier,
theirUuid.toString(),
destinationDeviceId
);
@ -566,7 +578,10 @@ export default class OutgoingMessage {
p = Promise.all(
error.response.staleDevices.map(async (deviceId: number) => {
await window.textsecure.storage.protocol.archiveSession(
`${identifier}.${deviceId}`
new QualifiedAddress(
ourUuid,
new Address(UUID.checkedLookup(identifier), deviceId)
)
);
})
);
@ -595,7 +610,7 @@ export default class OutgoingMessage {
window.log.info('closing all sessions for', identifier);
window.textsecure.storage.protocol
.archiveAllSessions(identifier)
.archiveAllSessions(UUID.checkedLookup(identifier))
.then(
() => {
throw error;
@ -623,10 +638,13 @@ export default class OutgoingMessage {
identifier: string,
deviceIdsToRemove: Array<number>
): Promise<void> {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const theirUuid = UUID.checkedLookup(identifier);
await Promise.all(
deviceIdsToRemove.map(async deviceId => {
await window.textsecure.storage.protocol.archiveSession(
`${identifier}.${deviceId}`
new QualifiedAddress(ourUuid, new Address(theirUuid, deviceId))
);
})
);
@ -675,9 +693,11 @@ export default class OutgoingMessage {
);
}
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
identifier
);
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds({
ourUuid,
identifier,
});
if (deviceIds.length === 0) {
await this.getKeysForIdentifier(identifier);
}

View file

@ -18,6 +18,9 @@ import {
import { assert } from '../util/assert';
import { parseIntOrThrow } from '../util/parseIntOrThrow';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { SenderKeys } from '../LibSignalStores';
import {
ChallengeType,
@ -737,14 +740,14 @@ export default class MessageSender {
}
const myE164 = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getUuid()?.toString();
const groupMembers = groupV2?.members || groupV1?.members || [];
// We should always have a UUID but have this check just in case we don't.
let isNotMe: (recipient: string) => boolean;
if (myUuid) {
isNotMe = r => r !== myE164 && r !== myUuid;
isNotMe = r => r !== myE164 && r !== myUuid.toString();
} else {
isNotMe = r => r !== myE164;
}
@ -1030,8 +1033,7 @@ export default class MessageSender {
isUpdate?: boolean;
options?: SendOptionsType;
}>): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const dataMessage = Proto.DataMessage.decode(
new FIXMEU8(encodedDataMessage)
@ -1086,7 +1088,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp,
contentHint: ContentHint.RESENDABLE,
@ -1097,8 +1099,7 @@ export default class MessageSender {
async sendRequestBlockSyncMessage(
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.BLOCKED;
@ -1110,7 +1111,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.IMPLICIT,
@ -1121,8 +1122,7 @@ export default class MessageSender {
async sendRequestConfigurationSyncMessage(
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.CONFIGURATION;
@ -1134,7 +1134,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.IMPLICIT,
@ -1145,8 +1145,7 @@ export default class MessageSender {
async sendRequestGroupSyncMessage(
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.GROUPS;
@ -1158,7 +1157,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.IMPLICIT,
@ -1169,8 +1168,7 @@ export default class MessageSender {
async sendRequestContactSyncMessage(
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.CONTACTS;
@ -1182,7 +1180,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.IMPLICIT,
@ -1193,8 +1191,7 @@ export default class MessageSender {
async sendFetchManifestSyncMessage(
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myUuid = window.textsecure.storage.user.getUuid();
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const fetchLatest = new Proto.SyncMessage.FetchLatest();
fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.STORAGE_MANIFEST;
@ -1207,7 +1204,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.IMPLICIT,
@ -1218,8 +1215,7 @@ export default class MessageSender {
async sendFetchLocalProfileSyncMessage(
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myUuid = window.textsecure.storage.user.getUuid();
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const fetchLatest = new Proto.SyncMessage.FetchLatest();
fetchLatest.type = Proto.SyncMessage.FetchLatest.Type.LOCAL_PROFILE;
@ -1232,7 +1228,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.IMPLICIT,
@ -1243,8 +1239,7 @@ export default class MessageSender {
async sendRequestKeySyncMessage(
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myUuid = window.textsecure.storage.user.getUuid();
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const request = new Proto.SyncMessage.Request();
request.type = Proto.SyncMessage.Request.Type.KEYS;
@ -1257,7 +1252,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.IMPLICIT,
@ -1273,8 +1268,7 @@ export default class MessageSender {
}>,
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const syncMessage = this.createSyncMessage();
syncMessage.read = [];
@ -1289,7 +1283,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.RESENDABLE,
@ -1305,8 +1299,7 @@ export default class MessageSender {
}>,
options?: SendOptionsType
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const syncMessage = this.createSyncMessage();
syncMessage.viewed = views.map(view => new Proto.SyncMessage.Viewed(view));
@ -1316,7 +1309,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.RESENDABLE,
@ -1330,8 +1323,7 @@ export default class MessageSender {
timestamp: number,
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const syncMessage = this.createSyncMessage();
@ -1349,7 +1341,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.RESENDABLE,
@ -1366,8 +1358,7 @@ export default class MessageSender {
}>,
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const syncMessage = this.createSyncMessage();
@ -1390,7 +1381,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.RESENDABLE,
@ -1406,8 +1397,7 @@ export default class MessageSender {
}>,
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const ENUM = Proto.SyncMessage.StickerPackOperation.Type;
const packOperations = operations.map(item => {
@ -1430,7 +1420,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: contentMessage,
timestamp: Date.now(),
contentHint: ContentHint.IMPLICIT,
@ -1445,8 +1435,7 @@ export default class MessageSender {
identityKey: Readonly<ArrayBuffer>,
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getCheckedUuid();
const now = Date.now();
if (!destinationE164 && !destinationUuid) {
@ -1488,7 +1477,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
return this.sendIndividualProto({
identifier: myUuid || myNumber,
identifier: myUuid.toString(),
proto: secondMessage,
timestamp: now,
contentHint: ContentHint.RESENDABLE,
@ -1675,6 +1664,7 @@ export default class MessageSender {
proto.timestamp = timestamp;
const identifier = uuid || e164;
const theirUuid = UUID.checkedLookup(identifier);
const logError = (prefix: string) => (error: Error) => {
window.log.error(prefix, error && error.stack ? error.stack : error);
@ -1684,7 +1674,7 @@ export default class MessageSender {
const { ContentHint } = Proto.UnidentifiedSenderMessage.Message;
const sendToContactPromise = window.textsecure.storage.protocol
.archiveAllSessions(identifier)
.archiveAllSessions(theirUuid)
.catch(logError('resetSession/archiveAllSessions1 error:'))
.then(async () => {
window.log.info(
@ -1706,14 +1696,14 @@ export default class MessageSender {
})
.then(async result => {
await window.textsecure.storage.protocol
.archiveAllSessions(identifier)
.archiveAllSessions(theirUuid)
.catch(logError('resetSession/archiveAllSessions2 error:'));
return result;
});
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getUuid()?.toString();
// We already sent the reset session to our other devices in the code above!
if ((e164 && e164 === myNumber) || (uuid && uuid === myUuid)) {
return sendToContactPromise;
@ -1882,7 +1872,7 @@ export default class MessageSender {
: undefined;
const myE164 = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getUuid()?.toString();
const identifiers = recipients.filter(id => id !== myE164 && id !== myUuid);
if (identifiers.length === 0) {
@ -1921,20 +1911,21 @@ export default class MessageSender {
async getSenderKeyDistributionMessage(
distributionId: string
): Promise<SenderKeyDistributionMessage> {
const ourUuid = window.textsecure.storage.user.getUuid();
if (!ourUuid) {
throw new Error(
'getSenderKeyDistributionMessage: Failed to fetch our UUID!'
);
}
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourDeviceId = parseIntOrThrow(
window.textsecure.storage.user.getDeviceId(),
'getSenderKeyDistributionMessage'
);
const protocolAddress = ProtocolAddress.new(ourUuid, ourDeviceId);
const address = `${ourUuid}.${ourDeviceId}`;
const senderKeyStore = new SenderKeys();
const protocolAddress = ProtocolAddress.new(
ourUuid.toString(),
ourDeviceId
);
const address = new QualifiedAddress(
ourUuid,
new Address(ourUuid, ourDeviceId)
);
const senderKeyStore = new SenderKeys({ ourUuid });
return window.textsecure.storage.protocol.enqueueSenderKeyJob(
address,
@ -2044,7 +2035,7 @@ export default class MessageSender {
options?: Readonly<SendOptionsType>
): Promise<CallbackResultType> {
const myNumber = window.textsecure.storage.user.getNumber();
const myUuid = window.textsecure.storage.user.getUuid();
const myUuid = window.textsecure.storage.user.getUuid()?.toString();
const recipients = groupIdentifiers.filter(
identifier => identifier !== myNumber && identifier !== myUuid
);

View file

@ -6,9 +6,14 @@ import type { IncomingWebSocketRequest } from './WebsocketResources';
export {
IdentityKeyType,
IdentityKeyIdType,
PreKeyIdType,
PreKeyType,
SenderKeyIdType,
SenderKeyType,
SessionIdType,
SessionType,
SignedPreKeyIdType,
SignedPreKeyType,
UnprocessedType,
UnprocessedUpdateType,

View file

@ -10,6 +10,9 @@ import {
import { UnregisteredUserError } from './Errors';
import { Sessions, IdentityKeys } from '../LibSignalStores';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { ServerKeysType, WebAPIType } from './WebAPI';
export async function getKeysForIdentifier(
@ -32,7 +35,11 @@ export async function getKeysForIdentifier(
};
} catch (error) {
if (error.name === 'HTTPError' && error.code === 404) {
await window.textsecure.storage.protocol.archiveAllSessions(identifier);
const theirUuid = UUID.lookup(identifier);
if (theirUuid) {
await window.textsecure.storage.protocol.archiveAllSessions(theirUuid);
}
}
throw new UnregisteredUserError(identifier, error);
}
@ -72,8 +79,9 @@ async function handleServerKeys(
response: ServerKeysType,
devicesToUpdate?: Array<number>
): Promise<void> {
const sessionStore = new Sessions();
const identityKeyStore = new IdentityKeys();
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const sessionStore = new Sessions({ ourUuid });
const identityKeyStore = new IdentityKeys({ ourUuid });
await Promise.all(
response.devices.map(async device => {
@ -95,7 +103,11 @@ async function handleServerKeys(
`getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}`
);
}
const protocolAddress = ProtocolAddress.new(identifier, deviceId);
const theirUuid = UUID.checkedLookup(identifier);
const protocolAddress = ProtocolAddress.new(
theirUuid.toString(),
deviceId
);
const preKeyId = preKey?.keyId || null;
const preKeyObject = preKey
? PublicKey.deserialize(Buffer.from(preKey.publicKey))
@ -118,7 +130,10 @@ async function handleServerKeys(
identityKey
);
const address = `${identifier}.${deviceId}`;
const address = new QualifiedAddress(
ourUuid,
new Address(theirUuid, deviceId)
);
await window.textsecure.storage.protocol
.enqueueSessionJob(address, () =>
processPreKeyBundle(

View file

@ -5,6 +5,7 @@ import { WebAPICredentials } from '../Types.d';
import { strictAssert } from '../../util/assert';
import { StorageInterface } from '../../types/Storage.d';
import { UUID } from '../../types/UUID';
import Helpers from '../Helpers';
@ -56,10 +57,16 @@ export class User {
return Helpers.unencodeNumber(numberId)[0];
}
public getUuid(): string | undefined {
public getUuid(): UUID | undefined {
const uuid = this.storage.get('uuid_id');
if (uuid === undefined) return undefined;
return Helpers.unencodeNumber(uuid.toLowerCase())[0];
return new UUID(Helpers.unencodeNumber(uuid.toLowerCase())[0]);
}
public getCheckedUuid(): UUID {
const uuid = this.getUuid();
strictAssert(uuid !== undefined, 'Must have our own uuid');
return uuid;
}
public getDeviceId(): number | undefined {

30
ts/types/Address.ts Normal file
View file

@ -0,0 +1,30 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { strictAssert } from '../util/assert';
import { UUID, UUIDStringType } from './UUID';
export type AddressStringType = `${UUIDStringType}.${number}`;
const ADDRESS_REGEXP = /^([0-9a-f-]+).(\d+)$/i;
export class Address {
constructor(public readonly uuid: UUID, public readonly deviceId: number) {}
public toString(): AddressStringType {
return `${this.uuid.toString()}.${this.deviceId}`;
}
public static parse(value: string): Address {
const match = value.match(ADDRESS_REGEXP);
strictAssert(match !== null, `Invalid Address: ${value}`);
const [whole, uuid, deviceId] = match;
strictAssert(whole === value, 'Integrity check');
return Address.create(uuid, parseInt(deviceId, 10));
}
public static create(uuid: string, deviceId: number): Address {
return new Address(new UUID(uuid), deviceId);
}
}

View file

@ -0,0 +1,48 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { strictAssert } from '../util/assert';
import { UUID, UUIDStringType } from './UUID';
import { Address, AddressStringType } from './Address';
const QUALIFIED_ADDRESS_REGEXP = /^([0-9a-f-]+):([0-9a-f-]+).(\d+)$/i;
export type QualifiedAddressCreateOptionsType = Readonly<{
ourUuid: string;
uuid: string;
deviceId: number;
}>;
export type QualifiedAddressStringType = `${UUIDStringType}:${AddressStringType}`;
export class QualifiedAddress {
constructor(
public readonly ourUuid: UUID,
public readonly address: Address
) {}
public get uuid(): UUID {
return this.address.uuid;
}
public get deviceId(): number {
return this.address.deviceId;
}
public toString(): QualifiedAddressStringType {
return `${this.ourUuid.toString()}:${this.address.toString()}`;
}
public static parse(value: string): QualifiedAddress {
const match = value.match(QUALIFIED_ADDRESS_REGEXP);
strictAssert(match !== null, `Invalid QualifiedAddress: ${value}`);
const [whole, ourUuid, uuid, deviceId] = match;
strictAssert(whole === value, 'Integrity check');
return new QualifiedAddress(
new UUID(ourUuid),
Address.create(uuid, parseInt(deviceId, 10))
);
}
}

12
ts/types/Storage.d.ts vendored
View file

@ -30,6 +30,14 @@ export type ThemeSettingType = 'system' | 'light' | 'dark';
export type NotificationSettingType = 'message' | 'name' | 'count' | 'off';
export type IdentityKeyMap = Record<
string,
{
privKey: string;
pubKey: string;
}
>;
// This should be in sync with `STORAGE_UI_KEYS` in `ts/types/StorageUIKeys.ts`.
export type StorageAccessType = {
'always-relay-calls': boolean;
@ -55,7 +63,7 @@ export type StorageAccessType = {
customColors: CustomColorsItemType;
device_name: string;
hasRegisterSupportForUnauthenticatedDelivery: boolean;
identityKey: KeyPairType;
identityKeyMap: IdentityKeyMap;
lastHeartbeat: number;
lastStartup: number;
lastAttemptedToRefreshProfilesAt: number;
@ -64,7 +72,7 @@ export type StorageAccessType = {
password: string;
profileKey: ArrayBuffer;
regionCode: string;
registrationId: number;
registrationIdMap: Record<string, number>;
remoteBuildExpiration: number;
sessionResets: SessionResetsType;
showStickerPickerHint: boolean;

44
ts/types/UUID.ts Normal file
View file

@ -0,0 +1,44 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { strictAssert } from '../util/assert';
import { isValidGuid } from '../util/isValidGuid';
export type UUIDStringType = `${string}-${string}-${string}-${string}-${string}`;
export class UUID {
constructor(protected readonly value: string) {
strictAssert(isValidGuid(value), `Invalid UUID: ${value}`);
}
public toString(): UUIDStringType {
return (this.value as unknown) as UUIDStringType;
}
public isEqual(other: UUID): boolean {
return this.value === other.value;
}
public static parse(value: string): UUID {
return new UUID(value);
}
public static lookup(identifier: string): UUID | undefined {
const conversation = window.ConversationController.get(identifier);
const uuid = conversation?.get('uuid');
if (uuid === undefined) {
return undefined;
}
return new UUID(uuid);
}
public static checkedLookup(identifier: string): UUID {
const uuid = UUID.lookup(identifier);
strictAssert(
uuid !== undefined,
`Conversation ${identifier} not found or has no uuid`
);
return uuid;
}
}

View file

@ -3,6 +3,9 @@
import { ProfileKeyCredentialRequestContext } from 'zkgroup';
import { SEALED_SENDER } from '../types/SealedSender';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import {
base64ToArrayBuffer,
stringFromBytes,
@ -54,6 +57,7 @@ export async function getProfile(
const uuid = c.get('uuid')!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const identifier = c.getSendTarget()!;
const targetUuid = UUID.checkedLookup(identifier);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const profileKeyVersionHex = c.get('profileKeyVersion')!;
const existingProfileKeyCredential = c.get('profileKeyCredential');
@ -115,15 +119,16 @@ export async function getProfile(
const identityKey = base64ToArrayBuffer(profile.identityKey);
const changed = await window.textsecure.storage.protocol.saveIdentity(
`${identifier}.1`,
new Address(targetUuid, 1),
identityKey,
false
);
if (changed) {
// save identity will close all sessions except for .1, so we
// must close that one manually.
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await window.textsecure.storage.protocol.archiveSession(
`${identifier}.1`
new QualifiedAddress(ourUuid, new Address(targetUuid, 1))
);
}

View file

@ -15,6 +15,8 @@ import { isGroupV2 } from './whatTypeOfConversation';
import { isOlderThan } from './timestamp';
import { parseIntOrThrow } from './parseIntOrThrow';
import * as RemoteConfig from '../RemoteConfig';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { ConversationModel } from '../models/conversations';
import {
@ -184,7 +186,11 @@ async function archiveSessionOnMatch({
return;
}
const address = `${requesterUuid}.${requesterDevice}`;
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const address = new QualifiedAddress(
ourUuid,
Address.create(requesterUuid, requesterDevice)
);
const session = await window.textsecure.storage.protocol.loadSession(address);
if (session && session.currentRatchetKeyMatches(ratchetKey)) {
@ -500,9 +506,10 @@ function scheduleSessionReset(senderUuid: string, senderDevice: number) {
}
lightSessionResetQueue.add(() => {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
window.textsecure.storage.protocol.lightSessionReset(
senderUuid,
senderDevice
new QualifiedAddress(ourUuid, Address.create(senderUuid, senderDevice))
);
});
}

View file

@ -3,6 +3,7 @@
import { PublicKey, Fingerprint } from '@signalapp/signal-client';
import { ConversationType } from '../state/ducks/conversations';
import { UUID } from '../types/UUID';
import { assert } from './assert';
@ -33,16 +34,18 @@ export async function generateSecurityNumber(
export async function generateSecurityNumberBlock(
contact: ConversationType
): Promise<Array<string>> {
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const { storage } = window.textsecure;
const ourNumber = storage.user.getNumber();
const ourUuid = storage.user.getCheckedUuid();
const us = window.textsecure.storage.protocol.getIdentityRecord(
ourUuid || ourNumber || ''
);
const us = storage.protocol.getIdentityRecord(ourUuid);
const ourKey = us ? us.publicKey : null;
const them = window.textsecure.storage.protocol.getIdentityRecord(contact.id);
const theirKey = them ? them.publicKey : null;
const theirUuid = UUID.lookup(contact.id);
const them = theirUuid
? await storage.protocol.getOrMigrateIdentityRecord(theirUuid)
: undefined;
const theirKey = them?.publicKey;
if (!ourKey) {
throw new Error('Could not load our key');

View file

@ -20,6 +20,9 @@ import {
SenderCertificateMode,
SendLogCallbackType,
} from '../textsecure/OutgoingMessage';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import { UUID } from '../types/UUID';
import { isEnabled } from '../RemoteConfig';
import { isOlderThan } from './timestamp';
@ -286,10 +289,14 @@ export async function sendToGroupViaSenderKey(options: {
}
// 2. Fetch all devices we believe we'll be sending to
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const {
devices: currentDevices,
emptyIdentifiers,
} = await window.textsecure.storage.protocol.getOpenDevices(recipients);
} = await window.textsecure.storage.protocol.getOpenDevices(
ourUuid,
recipients
);
// 3. If we have no open sessions with people we believe we are sending to, and we
// believe that any have signal accounts, fetch their prekey bundle and start
@ -669,7 +676,13 @@ async function markIdentifierUnregistered(identifier: string) {
conversation.setUnregistered();
window.Signal.Data.updateConversation(conversation.attributes);
await window.textsecure.storage.protocol.archiveAllSessions(identifier);
const uuid = UUID.lookup(identifier);
if (!uuid) {
window.log.warn(`No uuid found for ${identifier}`);
return;
}
await window.textsecure.storage.protocol.archiveAllSessions(uuid);
}
function isIdentifierRegistered(identifier: string) {
@ -695,10 +708,13 @@ async function handle409Response(logId: string, error: Error) {
// Archive sessions with devices that have been removed
if (devices.extraDevices && devices.extraDevices.length > 0) {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
await _waitForAll({
tasks: devices.extraDevices.map(deviceId => async () => {
const address = `${uuid}.${deviceId}`;
await window.textsecure.storage.protocol.archiveSession(address);
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(ourUuid, Address.create(uuid, deviceId))
);
}),
});
}
@ -727,11 +743,14 @@ async function handle410Response(
tasks: parsed.data.map(item => async () => {
const { uuid, devices } = item;
if (devices.staleDevices && devices.staleDevices.length > 0) {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
// First, archive our existing sessions with these devices
await _waitForAll({
tasks: devices.staleDevices.map(deviceId => async () => {
const address = `${uuid}.${deviceId}`;
await window.textsecure.storage.protocol.archiveSession(address);
await window.textsecure.storage.protocol.archiveSession(
new QualifiedAddress(ourUuid, Address.create(uuid, deviceId))
);
}),
});
@ -822,24 +841,24 @@ async function encryptForSenderKey({
distributionId: string;
groupId: string;
}): Promise<Buffer> {
const ourUuid = window.textsecure.storage.user.getUuid();
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
if (!ourUuid || !ourDeviceId) {
if (!ourDeviceId) {
throw new Error(
'encryptForSenderKey: Unable to fetch our uuid or deviceId'
);
}
const sender = ProtocolAddress.new(
ourUuid,
ourUuid.toString(),
parseIntOrThrow(ourDeviceId, 'encryptForSenderKey, ourDeviceId')
);
const ourAddress = getOurAddress();
const senderKeyStore = new SenderKeys();
const senderKeyStore = new SenderKeys({ ourUuid });
const message = Buffer.from(padMessage(new FIXMEU8(contentMessage)));
const ciphertextMessage = await window.textsecure.storage.protocol.enqueueSenderKeyJob(
ourAddress,
new QualifiedAddress(ourUuid, ourAddress),
() => groupEncrypt(sender, distributionId, senderKeyStore, message)
);
@ -874,9 +893,14 @@ async function encryptForSenderKey({
return 1;
})
.map(device => ProtocolAddress.new(device.identifier, device.id));
const identityKeyStore = new IdentityKeys();
const sessionStore = new Sessions();
.map(device => {
return ProtocolAddress.new(
UUID.checkedLookup(device.identifier).toString(),
device.id
);
});
const identityKeyStore = new IdentityKeys({ ourUuid });
const sessionStore = new Sessions({ ourUuid });
return sealedSenderMultiRecipientEncrypt(
content,
recipients,
@ -998,13 +1022,13 @@ export function _analyzeSenderKeyDevices(
};
}
function getOurAddress(): string {
const ourUuid = window.textsecure.storage.user.getUuid();
function getOurAddress(): Address {
const ourUuid = window.textsecure.storage.user.getCheckedUuid();
const ourDeviceId = window.textsecure.storage.user.getDeviceId();
if (!ourUuid || !ourDeviceId) {
throw new Error('getOurAddress: Unable to fetch our uuid or deviceId');
if (!ourDeviceId) {
throw new Error('getOurAddress: Unable to fetch our deviceId');
}
return `${ourUuid}.${ourDeviceId}`;
return new Address(ourUuid, ourDeviceId);
}
async function resetSenderKey(conversation: ConversationModel): Promise<void> {
@ -1023,7 +1047,7 @@ async function resetSenderKey(conversation: ConversationModel): Promise<void> {
}
const { distributionId } = senderKeyInfo;
const address = getOurAddress();
const ourAddress = getOurAddress();
// Note: We preserve existing distributionId to minimize space for sender key storage
conversation.set({
@ -1035,8 +1059,9 @@ async function resetSenderKey(conversation: ConversationModel): Promise<void> {
});
window.Signal.Data.updateConversation(conversation.attributes);
const ourUuid = window.storage.user.getCheckedUuid();
await window.textsecure.storage.protocol.removeSenderKey(
address,
new QualifiedAddress(ourUuid, ourAddress),
distributionId
);
}

View file

@ -25,7 +25,7 @@ export function isDirectConversation(
export function isMe(conversationAttrs: ConversationAttributesType): boolean {
const { e164, uuid } = conversationAttrs;
const ourNumber = window.textsecure.storage.user.getNumber();
const ourUuid = window.textsecure.storage.user.getUuid();
const ourUuid = window.textsecure.storage.user.getUuid()?.toString();
return Boolean((e164 && e164 === ourNumber) || (uuid && uuid === ourUuid));
}

7
ts/window.d.ts vendored
View file

@ -117,6 +117,9 @@ import { MessageController } from './util/MessageController';
import { isValidGuid } from './util/isValidGuid';
import { StateType } from './state/reducer';
import { SystemTraySetting } from './types/SystemTraySetting';
import { UUID } from './types/UUID';
import { Address } from './types/Address';
import { QualifiedAddress } from './types/QualifiedAddress';
import { CI } from './CI';
import { IPCEventsType } from './util/createIPCEvents';
import { ConversationView } from './views/conversation_view';
@ -396,6 +399,9 @@ declare global {
path: string;
};
VisualAttachment: any;
UUID: typeof UUID;
Address: typeof Address;
QualifiedAddress: typeof QualifiedAddress;
};
Util: typeof Util;
GroupChange: {
@ -595,7 +601,6 @@ export type WhisperType = {
MessageCollection: typeof MessageModelCollectionType;
GroupMemberConversation: WhatIsThis;
KeyChangeListener: WhatIsThis;
RotateSignedPreKeyListener: WhatIsThis;
WallClockListener: WhatIsThis;