Receive support for Sender Key
Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
parent
e5f9c0db28
commit
e6bab06510
28 changed files with 743 additions and 164 deletions
10
package.json
10
package.json
|
@ -68,6 +68,7 @@
|
|||
"fs-xattr": "0.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@signalapp/signal-client": "0.5.1",
|
||||
"@sindresorhus/is": "0.8.0",
|
||||
"@types/pino": "6.3.6",
|
||||
"@types/pino-multi-stream": "5.1.0",
|
||||
|
@ -102,7 +103,6 @@
|
|||
"intl-tel-input": "12.1.15",
|
||||
"jquery": "3.5.0",
|
||||
"js-yaml": "3.13.1",
|
||||
"libsignal-client": "https://github.com/signalapp/libsignal-client-node.git#afa8f40eaa218b7fff94278d9b2ab2e13a2ee04b",
|
||||
"linkify-it": "2.2.0",
|
||||
"lodash": "4.17.21",
|
||||
"lru-cache": "6.0.0",
|
||||
|
@ -294,7 +294,7 @@
|
|||
"asarUnpack": [
|
||||
"**/*.node",
|
||||
"node_modules/zkgroup/libzkgroup.*",
|
||||
"node_modules/libsignal-client/build/*.node"
|
||||
"node_modules/@signalapp/signal-client/build/*.node"
|
||||
],
|
||||
"artifactName": "${name}-mac-${version}.${ext}",
|
||||
"category": "public.app-category.social-networking",
|
||||
|
@ -320,7 +320,7 @@
|
|||
"node_modules/spellchecker/vendor/hunspell_dictionaries",
|
||||
"node_modules/sharp",
|
||||
"node_modules/zkgroup/libzkgroup.*",
|
||||
"node_modules/libsignal-client/build/*.node"
|
||||
"node_modules/@signalapp/signal-client/build/*.node"
|
||||
],
|
||||
"artifactName": "${name}-win-${version}.${ext}",
|
||||
"certificateSubjectName": "Signal (Quiet Riddle Ventures, LLC)",
|
||||
|
@ -350,7 +350,7 @@
|
|||
"node_modules/spellchecker/vendor/hunspell_dictionaries",
|
||||
"node_modules/sharp",
|
||||
"node_modules/zkgroup/libzkgroup.*",
|
||||
"node_modules/libsignal-client/build/*.node"
|
||||
"node_modules/@signalapp/signal-client/build/*.node"
|
||||
],
|
||||
"target": [
|
||||
"deb"
|
||||
|
@ -438,7 +438,7 @@
|
|||
"!node_modules/better-sqlite3/deps/*",
|
||||
"!node_modules/better-sqlite3/src/*",
|
||||
"node_modules/better-sqlite3/build/Release/better_sqlite3.node",
|
||||
"node_modules/libsignal-client/build/*${platform}*.node",
|
||||
"node_modules/@signalapp/signal-client/build/*${platform}*.node",
|
||||
"node_modules/ringrtc/build/${platform}/**",
|
||||
"!**/node_modules/ffi-napi/deps",
|
||||
"!**/node_modules/react-dom/*/*.development.js",
|
||||
|
|
|
@ -12,6 +12,7 @@ message Envelope {
|
|||
PREKEY_BUNDLE = 3;
|
||||
RECEIPT = 5;
|
||||
UNIDENTIFIED_SENDER = 6;
|
||||
SENDERKEY = 7;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
|
@ -27,12 +28,13 @@ message Envelope {
|
|||
}
|
||||
|
||||
message Content {
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional SyncMessage syncMessage = 2;
|
||||
optional CallingMessage callingMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional ReceiptMessage receiptMessage = 5;
|
||||
optional TypingMessage typingMessage = 6;
|
||||
optional DataMessage dataMessage = 1;
|
||||
optional SyncMessage syncMessage = 2;
|
||||
optional CallingMessage callingMessage = 3;
|
||||
optional NullMessage nullMessage = 4;
|
||||
optional ReceiptMessage receiptMessage = 5;
|
||||
optional TypingMessage typingMessage = 6;
|
||||
optional bytes senderKeyDistributionMessage = 7;
|
||||
}
|
||||
|
||||
// Everything in CallingMessage must be kept in sync with RingRTC (ringrtc-node).
|
||||
|
@ -362,7 +364,7 @@ message SyncMessage {
|
|||
optional bool readReceipts = 1;
|
||||
optional bool unidentifiedDeliveryIndicators = 2;
|
||||
optional bool typingIndicators = 3;
|
||||
// 4 is reserved
|
||||
reserved 4;
|
||||
optional uint32 provisioningVersion = 5;
|
||||
optional bool linkPreviews = 6;
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@ message ServerCertificate {
|
|||
|
||||
message SenderCertificate {
|
||||
message Certificate {
|
||||
optional string sender = 1;
|
||||
optional string senderUuid = 6;
|
||||
optional uint32 senderDevice = 2;
|
||||
optional fixed64 expires = 3;
|
||||
optional bytes identityKey = 4;
|
||||
optional ServerCertificate signer = 5;
|
||||
optional string senderE164 = 1;
|
||||
optional string senderUuid = 6;
|
||||
optional uint32 senderDevice = 2;
|
||||
optional fixed64 expires = 3;
|
||||
optional bytes identityKey = 4;
|
||||
optional ServerCertificate signer = 5;
|
||||
}
|
||||
|
||||
optional bytes certificate = 1;
|
||||
|
@ -31,16 +31,34 @@ message UnidentifiedSenderMessage {
|
|||
|
||||
message Message {
|
||||
enum Type {
|
||||
PREKEY_MESSAGE = 1;
|
||||
MESSAGE = 2;
|
||||
PREKEY_MESSAGE = 1;
|
||||
MESSAGE = 2;
|
||||
// Further cases should line up with Envelope.Type, even though old cases don't.
|
||||
|
||||
// Our parser does not handle reserved in enums: DESKTOP-1569
|
||||
// reserved 3 to 6;
|
||||
|
||||
SENDERKEY_MESSAGE = 7;
|
||||
}
|
||||
|
||||
enum ContentHint {
|
||||
// Commented out here, even though it is correct syntax. Our parser cannot handle it.
|
||||
|
||||
// Our parser does not handle reserved in enums: DESKTOP-1569
|
||||
// reserved 0; // A content hint of "default" should never be encoded.
|
||||
|
||||
SUPPLEMENTARY = 1;
|
||||
RETRY = 2;
|
||||
}
|
||||
|
||||
optional Type type = 1;
|
||||
optional SenderCertificate senderCertificate = 2;
|
||||
optional bytes content = 3;
|
||||
optional ContentHint contentHint = 4;
|
||||
optional bytes groupId = 5;
|
||||
}
|
||||
|
||||
optional bytes ephemeralPublic = 1;
|
||||
optional bytes encryptedStatic = 2;
|
||||
optional bytes encryptedMessage = 3;
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ const {
|
|||
const SKIPPED_DEPENDENCIES = new Set([
|
||||
'ringrtc',
|
||||
'zkgroup',
|
||||
'libsignal-client',
|
||||
'@signalapp/signal-client',
|
||||
]);
|
||||
|
||||
const rootDir = join(__dirname, '..');
|
||||
|
|
|
@ -9,7 +9,7 @@ const { readFile } = require('fs');
|
|||
const config = require('url').parse(window.location.toString(), true).query;
|
||||
const { noop, uniqBy } = require('lodash');
|
||||
const pMap = require('p-map');
|
||||
const client = require('libsignal-client');
|
||||
const client = require('@signalapp/signal-client');
|
||||
const { deriveStickerPackKey } = require('../ts/Crypto');
|
||||
const {
|
||||
getEnvironment,
|
||||
|
|
|
@ -23,7 +23,7 @@ describe('Crypto', () => {
|
|||
const result = window.Signal.Crypto.deriveSecrets(input, salt, info);
|
||||
assert.lengthOf(result, 3);
|
||||
result.forEach(part => {
|
||||
// This is a smoke test; HKDF is tested as part of libsignal-client.
|
||||
// This is a smoke test; HKDF is tested as part of @signalapp/signal-client.
|
||||
assert.instanceOf(part, ArrayBuffer);
|
||||
assert.strictEqual(part.byteLength, 32);
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import pProps from 'p-props';
|
||||
import { chunk } from 'lodash';
|
||||
import { HKDF } from 'libsignal-client';
|
||||
import { HKDF } from '@signalapp/signal-client';
|
||||
import { calculateAgreement, generateKeyPair } from './Curve';
|
||||
|
||||
import {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as client from 'libsignal-client';
|
||||
import * as client from '@signalapp/signal-client';
|
||||
|
||||
import { constantTimeEqual, typedArrayToArrayBuffer } from './Crypto';
|
||||
import {
|
||||
|
|
|
@ -8,17 +8,20 @@ import { isNumber } from 'lodash';
|
|||
|
||||
import {
|
||||
Direction,
|
||||
ProtocolAddress,
|
||||
SessionStore,
|
||||
SessionRecord,
|
||||
IdentityKeyStore,
|
||||
PreKeyRecord,
|
||||
PreKeyStore,
|
||||
PrivateKey,
|
||||
ProtocolAddress,
|
||||
PublicKey,
|
||||
SignedPreKeyStore,
|
||||
SenderKeyRecord,
|
||||
SenderKeyStore,
|
||||
SessionRecord,
|
||||
SessionStore,
|
||||
SignedPreKeyRecord,
|
||||
} from 'libsignal-client';
|
||||
SignedPreKeyStore,
|
||||
Uuid,
|
||||
} from '@signalapp/signal-client';
|
||||
import { freezePreKey, freezeSignedPreKey } from './SignalProtocolStore';
|
||||
|
||||
import { typedArrayToArrayBuffer } from './Crypto';
|
||||
|
@ -131,6 +134,36 @@ export class PreKeys extends PreKeyStore {
|
|||
}
|
||||
}
|
||||
|
||||
export class SenderKeys extends SenderKeyStore {
|
||||
async saveSenderKey(
|
||||
sender: ProtocolAddress,
|
||||
distributionId: Uuid,
|
||||
record: SenderKeyRecord
|
||||
): Promise<void> {
|
||||
const encodedAddress = encodedNameFromAddress(sender);
|
||||
|
||||
await window.textsecure.storage.protocol.saveSenderKey(
|
||||
encodedAddress,
|
||||
distributionId,
|
||||
record
|
||||
);
|
||||
}
|
||||
|
||||
async getSenderKey(
|
||||
sender: ProtocolAddress,
|
||||
distributionId: Uuid
|
||||
): Promise<SenderKeyRecord | null> {
|
||||
const encodedAddress = encodedNameFromAddress(sender);
|
||||
|
||||
const senderKey = await window.textsecure.storage.protocol.getSenderKey(
|
||||
encodedAddress,
|
||||
distributionId
|
||||
);
|
||||
|
||||
return senderKey || null;
|
||||
}
|
||||
}
|
||||
|
||||
export class SignedPreKeys extends SignedPreKeyStore {
|
||||
async saveSignedPreKey(
|
||||
id: number,
|
||||
|
|
|
@ -8,13 +8,14 @@ import { isNumber } from 'lodash';
|
|||
import * as z from 'zod';
|
||||
|
||||
import {
|
||||
SessionRecord,
|
||||
Direction,
|
||||
PreKeyRecord,
|
||||
PrivateKey,
|
||||
PublicKey,
|
||||
SenderKeyRecord,
|
||||
SessionRecord,
|
||||
SignedPreKeyRecord,
|
||||
Direction,
|
||||
} from 'libsignal-client';
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import {
|
||||
constantTimeEqual,
|
||||
|
@ -30,6 +31,7 @@ import {
|
|||
import {
|
||||
KeyPairType,
|
||||
IdentityKeyType,
|
||||
SenderKeyType,
|
||||
SessionType,
|
||||
SignedPreKeyType,
|
||||
OuterSignedPrekeyType,
|
||||
|
@ -90,8 +92,8 @@ async function normalizeEncodedAddress(
|
|||
}
|
||||
}
|
||||
|
||||
type HasIdType = {
|
||||
id: string | number;
|
||||
type HasIdType<T> = {
|
||||
id: T;
|
||||
};
|
||||
type CacheEntryType<DBType, HydratedType> =
|
||||
| {
|
||||
|
@ -100,24 +102,22 @@ type CacheEntryType<DBType, HydratedType> =
|
|||
}
|
||||
| { hydrated: true; fromDB: DBType; item: HydratedType };
|
||||
|
||||
async function _fillCaches<T extends HasIdType, HydratedType>(
|
||||
async function _fillCaches<ID, T extends HasIdType<ID>, HydratedType>(
|
||||
object: SignalProtocolStore,
|
||||
field: keyof SignalProtocolStore,
|
||||
itemsPromise: Promise<Array<T>>
|
||||
): Promise<void> {
|
||||
const items = await itemsPromise;
|
||||
|
||||
const cache: Record<string, CacheEntryType<T, HydratedType>> = Object.create(
|
||||
null
|
||||
);
|
||||
const cache = new Map<ID, CacheEntryType<T, HydratedType>>();
|
||||
for (let i = 0, max = items.length; i < max; i += 1) {
|
||||
const fromDB = items[i];
|
||||
const { id } = fromDB;
|
||||
|
||||
cache[id] = {
|
||||
cache.set(id, {
|
||||
fromDB,
|
||||
hydrated: false,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
window.log.info(`SignalProtocolStore: Finished caching ${field} data`);
|
||||
|
@ -193,17 +193,21 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
|
||||
ourRegistrationId?: number;
|
||||
|
||||
identityKeys?: Record<string, CacheEntryType<IdentityKeyType, PublicKey>>;
|
||||
identityKeys?: Map<string, CacheEntryType<IdentityKeyType, PublicKey>>;
|
||||
|
||||
sessions?: Record<string, CacheEntryType<SessionType, SessionRecord>>;
|
||||
senderKeys?: Map<string, CacheEntryType<SenderKeyType, SenderKeyRecord>>;
|
||||
|
||||
preKeys?: Record<string, CacheEntryType<PreKeyType, PreKeyRecord>>;
|
||||
sessions?: Map<string, CacheEntryType<SessionType, SessionRecord>>;
|
||||
|
||||
signedPreKeys?: Record<
|
||||
string,
|
||||
preKeys?: Map<number, CacheEntryType<PreKeyType, PreKeyRecord>>;
|
||||
|
||||
signedPreKeys?: Map<
|
||||
number,
|
||||
CacheEntryType<SignedPreKeyType, SignedPreKeyRecord>
|
||||
>;
|
||||
|
||||
senderKeyQueues: Map<string, PQueue> = new Map<string, PQueue>();
|
||||
|
||||
sessionQueues: Map<string, PQueue> = new Map<string, PQueue>();
|
||||
|
||||
async hydrateCaches(): Promise<void> {
|
||||
|
@ -216,22 +220,27 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
const item = await window.Signal.Data.getItemById('registrationId');
|
||||
this.ourRegistrationId = item ? item.value : undefined;
|
||||
})(),
|
||||
_fillCaches<IdentityKeyType, PublicKey>(
|
||||
_fillCaches<string, IdentityKeyType, PublicKey>(
|
||||
this,
|
||||
'identityKeys',
|
||||
window.Signal.Data.getAllIdentityKeys()
|
||||
),
|
||||
_fillCaches<SessionType, SessionRecord>(
|
||||
_fillCaches<string, SessionType, SessionRecord>(
|
||||
this,
|
||||
'sessions',
|
||||
window.Signal.Data.getAllSessions()
|
||||
),
|
||||
_fillCaches<PreKeyType, PreKeyRecord>(
|
||||
_fillCaches<number, PreKeyType, PreKeyRecord>(
|
||||
this,
|
||||
'preKeys',
|
||||
window.Signal.Data.getAllPreKeys()
|
||||
),
|
||||
_fillCaches<SignedPreKeyType, SignedPreKeyRecord>(
|
||||
_fillCaches<string, SenderKeyType, SenderKeyRecord>(
|
||||
this,
|
||||
'senderKeys',
|
||||
window.Signal.Data.getAllSenderKeys()
|
||||
),
|
||||
_fillCaches<number, SignedPreKeyType, SignedPreKeyRecord>(
|
||||
this,
|
||||
'signedPreKeys',
|
||||
window.Signal.Data.getAllSignedPreKeys()
|
||||
|
@ -249,12 +258,12 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
|
||||
// PreKeys
|
||||
|
||||
async loadPreKey(keyId: string | number): Promise<PreKeyRecord | undefined> {
|
||||
async loadPreKey(keyId: number): Promise<PreKeyRecord | undefined> {
|
||||
if (!this.preKeys) {
|
||||
throw new Error('loadPreKey: this.preKeys not yet cached!');
|
||||
}
|
||||
|
||||
const entry = this.preKeys[keyId];
|
||||
const entry = this.preKeys.get(keyId);
|
||||
if (!entry) {
|
||||
window.log.error('Failed to fetch prekey:', keyId);
|
||||
return undefined;
|
||||
|
@ -266,11 +275,11 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
}
|
||||
|
||||
const item = hydratePreKey(entry.fromDB);
|
||||
this.preKeys[keyId] = {
|
||||
this.preKeys.set(keyId, {
|
||||
hydrated: true,
|
||||
fromDB: entry.fromDB,
|
||||
item,
|
||||
};
|
||||
});
|
||||
window.log.info('Successfully fetched prekey (cache miss):', keyId);
|
||||
return item;
|
||||
}
|
||||
|
@ -279,7 +288,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
if (!this.preKeys) {
|
||||
throw new Error('storePreKey: this.preKeys not yet cached!');
|
||||
}
|
||||
if (this.preKeys[keyId]) {
|
||||
if (this.preKeys.has(keyId)) {
|
||||
throw new Error(`storePreKey: prekey ${keyId} already exists!`);
|
||||
}
|
||||
|
||||
|
@ -290,10 +299,10 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
};
|
||||
|
||||
await window.Signal.Data.createOrUpdatePreKey(fromDB);
|
||||
this.preKeys[keyId] = {
|
||||
this.preKeys.set(keyId, {
|
||||
hydrated: false,
|
||||
fromDB,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async removePreKey(keyId: number): Promise<void> {
|
||||
|
@ -310,12 +319,14 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
);
|
||||
}
|
||||
|
||||
delete this.preKeys[keyId];
|
||||
this.preKeys.delete(keyId);
|
||||
await window.Signal.Data.removePreKeyById(keyId);
|
||||
}
|
||||
|
||||
async clearPreKeyStore(): Promise<void> {
|
||||
this.preKeys = Object.create(null);
|
||||
if (this.preKeys) {
|
||||
this.preKeys.clear();
|
||||
}
|
||||
await window.Signal.Data.removeAllPreKeys();
|
||||
}
|
||||
|
||||
|
@ -328,7 +339,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
throw new Error('loadSignedPreKey: this.signedPreKeys not yet cached!');
|
||||
}
|
||||
|
||||
const entry = this.signedPreKeys[keyId];
|
||||
const entry = this.signedPreKeys.get(keyId);
|
||||
if (!entry) {
|
||||
window.log.error('Failed to fetch signed prekey:', keyId);
|
||||
return undefined;
|
||||
|
@ -340,11 +351,11 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
}
|
||||
|
||||
const item = hydrateSignedPreKey(entry.fromDB);
|
||||
this.signedPreKeys[keyId] = {
|
||||
this.signedPreKeys.set(keyId, {
|
||||
hydrated: true,
|
||||
item,
|
||||
fromDB: entry.fromDB,
|
||||
};
|
||||
});
|
||||
window.log.info('Successfully fetched signed prekey (cache miss):', keyId);
|
||||
return item;
|
||||
}
|
||||
|
@ -358,7 +369,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
throw new Error('loadSignedPreKeys takes no arguments');
|
||||
}
|
||||
|
||||
const entries = Object.values(this.signedPreKeys);
|
||||
const entries = Array.from(this.signedPreKeys.values());
|
||||
return entries.map(entry => {
|
||||
const preKey = entry.fromDB;
|
||||
return {
|
||||
|
@ -391,10 +402,10 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
};
|
||||
|
||||
await window.Signal.Data.createOrUpdateSignedPreKey(fromDB);
|
||||
this.signedPreKeys[keyId] = {
|
||||
this.signedPreKeys.set(keyId, {
|
||||
hydrated: false,
|
||||
fromDB,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async removeSignedPreKey(keyId: number): Promise<void> {
|
||||
|
@ -402,15 +413,126 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
throw new Error('removeSignedPreKey: this.signedPreKeys not yet cached!');
|
||||
}
|
||||
|
||||
delete this.signedPreKeys[keyId];
|
||||
this.signedPreKeys.delete(keyId);
|
||||
await window.Signal.Data.removeSignedPreKeyById(keyId);
|
||||
}
|
||||
|
||||
async clearSignedPreKeysStore(): Promise<void> {
|
||||
this.signedPreKeys = Object.create(null);
|
||||
if (this.signedPreKeys) {
|
||||
this.signedPreKeys.clear();
|
||||
}
|
||||
await window.Signal.Data.removeAllSignedPreKeys();
|
||||
}
|
||||
|
||||
// Sender Key Queue
|
||||
|
||||
async enqueueSenderKeyJob<T>(
|
||||
encodedAddress: string,
|
||||
task: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const senderId = await normalizeEncodedAddress(encodedAddress);
|
||||
const queue = this._getSenderKeyQueue(senderId);
|
||||
|
||||
return queue.add<T>(task);
|
||||
}
|
||||
|
||||
private _createSenderKeyQueue(): PQueue {
|
||||
return new PQueue({ concurrency: 1, timeout: 1000 * 60 * 2 });
|
||||
}
|
||||
|
||||
private _getSenderKeyQueue(senderId: string): PQueue {
|
||||
const cachedQueue = this.senderKeyQueues.get(senderId);
|
||||
if (cachedQueue) {
|
||||
return cachedQueue;
|
||||
}
|
||||
|
||||
const freshQueue = this._createSenderKeyQueue();
|
||||
this.senderKeyQueues.set(senderId, freshQueue);
|
||||
return freshQueue;
|
||||
}
|
||||
|
||||
// Sender Keys
|
||||
|
||||
private getSenderKeyId(senderKeyId: string, distributionId: string): string {
|
||||
return `${senderKeyId}--${distributionId}`;
|
||||
}
|
||||
|
||||
async saveSenderKey(
|
||||
encodedAddress: string,
|
||||
distributionId: string,
|
||||
record: SenderKeyRecord
|
||||
): Promise<void> {
|
||||
if (!this.senderKeys) {
|
||||
throw new Error('saveSenderKey: this.senderKeys not yet cached!');
|
||||
}
|
||||
|
||||
try {
|
||||
const senderId = await normalizeEncodedAddress(encodedAddress);
|
||||
const id = this.getSenderKeyId(senderId, distributionId);
|
||||
|
||||
const fromDB: SenderKeyType = {
|
||||
id,
|
||||
senderId,
|
||||
distributionId,
|
||||
data: record.serialize(),
|
||||
lastUpdatedDate: Date.now(),
|
||||
};
|
||||
|
||||
await window.Signal.Data.createOrUpdateSenderKey(fromDB);
|
||||
|
||||
this.senderKeys.set(id, {
|
||||
hydrated: true,
|
||||
fromDB,
|
||||
item: record,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorString = error && error.stack ? error.stack : error;
|
||||
window.log.error(
|
||||
`saveSenderKey: failed to save senderKey ${encodedAddress}/${distributionId}: ${errorString}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getSenderKey(
|
||||
encodedAddress: string,
|
||||
distributionId: string
|
||||
): Promise<SenderKeyRecord | undefined> {
|
||||
if (!this.senderKeys) {
|
||||
throw new Error('getSenderKey: this.senderKeys not yet cached!');
|
||||
}
|
||||
|
||||
try {
|
||||
const senderId = await normalizeEncodedAddress(encodedAddress);
|
||||
const id = this.getSenderKeyId(senderId, distributionId);
|
||||
|
||||
const entry = this.senderKeys.get(id);
|
||||
if (!entry) {
|
||||
window.log.error('Failed to fetch sender key:', id);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (entry.hydrated) {
|
||||
window.log.info('Successfully fetched signed prekey (cache hit):', id);
|
||||
return entry.item;
|
||||
}
|
||||
|
||||
const item = SenderKeyRecord.deserialize(entry.fromDB.data);
|
||||
this.senderKeys.set(id, {
|
||||
hydrated: true,
|
||||
item,
|
||||
fromDB: entry.fromDB,
|
||||
});
|
||||
window.log.info('Successfully fetched signed prekey (cache miss):', id);
|
||||
return item;
|
||||
} catch (error) {
|
||||
const errorString = error && error.stack ? error.stack : error;
|
||||
window.log.error(
|
||||
`getSenderKey: failed to load senderKey ${encodedAddress}/${distributionId}: ${errorString}`
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// Session Queue
|
||||
|
||||
async enqueueSessionJob<T>(
|
||||
|
@ -453,7 +575,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
|
||||
try {
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
const entry = this.sessions[id];
|
||||
const entry = this.sessions.get(id);
|
||||
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
|
@ -464,11 +586,11 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
}
|
||||
|
||||
const item = await this._maybeMigrateSession(entry.fromDB);
|
||||
this.sessions[id] = {
|
||||
this.sessions.set(id, {
|
||||
hydrated: true,
|
||||
item,
|
||||
fromDB: entry.fromDB,
|
||||
};
|
||||
});
|
||||
return item;
|
||||
} catch (error) {
|
||||
const errorString = error && error.stack ? error.stack : error;
|
||||
|
@ -544,11 +666,11 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
};
|
||||
|
||||
await window.Signal.Data.createOrUpdateSession(fromDB);
|
||||
this.sessions[id] = {
|
||||
this.sessions.set(id, {
|
||||
hydrated: true,
|
||||
fromDB,
|
||||
item: record,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
const errorString = error && error.stack ? error.stack : error;
|
||||
window.log.error(
|
||||
|
@ -574,7 +696,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
);
|
||||
}
|
||||
|
||||
const allSessions = Object.values(this.sessions);
|
||||
const allSessions = Array.from(this.sessions.values());
|
||||
const entries = allSessions.filter(
|
||||
session => session.fromDB.conversationId === id
|
||||
);
|
||||
|
@ -618,7 +740,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
try {
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
await window.Signal.Data.removeSessionById(id);
|
||||
delete this.sessions[id];
|
||||
this.sessions.delete(id);
|
||||
} catch (e) {
|
||||
window.log.error(
|
||||
`removeSession: Failed to delete session for ${encodedAddress}`
|
||||
|
@ -639,12 +761,12 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
|
||||
const id = window.ConversationController.getConversationId(identifier);
|
||||
|
||||
const entries = Object.values(this.sessions);
|
||||
const entries = Array.from(this.sessions.values());
|
||||
|
||||
for (let i = 0, max = entries.length; i < max; i += 1) {
|
||||
const entry = entries[i];
|
||||
if (entry.fromDB.conversationId === id) {
|
||||
delete this.sessions[entry.fromDB.id];
|
||||
this.sessions.delete(entry.fromDB.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -681,7 +803,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
window.log.info(`archiveSession: session for ${encodedAddress}`);
|
||||
|
||||
const id = await normalizeEncodedAddress(encodedAddress);
|
||||
const entry = this.sessions[id];
|
||||
const entry = this.sessions.get(id);
|
||||
|
||||
await this._archiveSession(entry);
|
||||
}
|
||||
|
@ -700,7 +822,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
const [identifier, deviceId] = window.textsecure.utils.unencodeNumber(id);
|
||||
const deviceIdNumber = parseInt(deviceId, 10);
|
||||
|
||||
const allEntries = Object.values(this.sessions);
|
||||
const allEntries = Array.from(this.sessions.values());
|
||||
const entries = allEntries.filter(
|
||||
entry =>
|
||||
entry.fromDB.conversationId === identifier &&
|
||||
|
@ -725,7 +847,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
);
|
||||
|
||||
const id = window.ConversationController.getConversationId(identifier);
|
||||
const allEntries = Object.values(this.sessions);
|
||||
const allEntries = Array.from(this.sessions.values());
|
||||
const entries = allEntries.filter(
|
||||
entry => entry.fromDB.conversationId === id
|
||||
);
|
||||
|
@ -738,7 +860,9 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
}
|
||||
|
||||
async clearSessionStore(): Promise<void> {
|
||||
this.sessions = Object.create(null);
|
||||
if (this.sessions) {
|
||||
this.sessions.clear();
|
||||
}
|
||||
window.Signal.Data.removeAllSessions();
|
||||
}
|
||||
|
||||
|
@ -757,7 +881,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
);
|
||||
}
|
||||
|
||||
const entry = this.identityKeys[id];
|
||||
const entry = this.identityKeys.get(id);
|
||||
if (!entry) {
|
||||
return undefined;
|
||||
}
|
||||
|
@ -869,10 +993,10 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
const { id } = data;
|
||||
|
||||
await window.Signal.Data.createOrUpdateIdentityKey(data);
|
||||
this.identityKeys[id] = {
|
||||
this.identityKeys.set(id, {
|
||||
hydrated: false,
|
||||
fromDB: data,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async saveIdentity(
|
||||
|
@ -1271,7 +1395,7 @@ export class SignalProtocolStore extends EventsMixin {
|
|||
|
||||
const id = window.ConversationController.getConversationId(identifier);
|
||||
if (id) {
|
||||
delete this.identityKeys[id];
|
||||
this.identityKeys.delete(id);
|
||||
await window.Signal.Data.removeIdentityKeyById(id);
|
||||
await this.removeAllSessions(id);
|
||||
}
|
||||
|
|
53
ts/groups.ts
53
ts/groups.ts
|
@ -23,6 +23,7 @@ import dataInterface from './sql/Client';
|
|||
import { toWebSafeBase64, fromWebSafeBase64 } from './util/webSafeBase64';
|
||||
import { assert } from './util/assert';
|
||||
import { isMoreRecentThan } from './util/timestamp';
|
||||
import { isByteBufferEmpty } from './util/isByteBufferEmpty';
|
||||
import {
|
||||
ConversationAttributesType,
|
||||
GroupV2MemberType,
|
||||
|
@ -321,10 +322,10 @@ export function parseGroupLink(
|
|||
throw error;
|
||||
}
|
||||
|
||||
if (!hasData(inviteLinkProto.v1Contents.groupMasterKey)) {
|
||||
if (isByteBufferEmpty(inviteLinkProto.v1Contents.groupMasterKey)) {
|
||||
throw new Error('v1Contents.groupMasterKey had no data!');
|
||||
}
|
||||
if (!hasData(inviteLinkProto.v1Contents.inviteLinkPassword)) {
|
||||
if (isByteBufferEmpty(inviteLinkProto.v1Contents.inviteLinkPassword)) {
|
||||
throw new Error('v1Contents.inviteLinkPassword had no data!');
|
||||
}
|
||||
|
||||
|
@ -4673,10 +4674,6 @@ function isValidProfileKey(buffer?: ArrayBuffer): boolean {
|
|||
return Boolean(buffer && buffer.byteLength === 32);
|
||||
}
|
||||
|
||||
function hasData(data: ProtoBinaryType): boolean {
|
||||
return data && data.limit > 0;
|
||||
}
|
||||
|
||||
function normalizeTimestamp(
|
||||
timestamp: ProtoBigNumberType
|
||||
): number | ProtoBigNumberType {
|
||||
|
@ -4703,7 +4700,7 @@ function decryptGroupChange(
|
|||
): GroupChangeClass.Actions {
|
||||
const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams);
|
||||
|
||||
if (hasData(actions.sourceUuid)) {
|
||||
if (!isByteBufferEmpty(actions.sourceUuid)) {
|
||||
try {
|
||||
actions.sourceUuid = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -4752,7 +4749,7 @@ function decryptGroupChange(
|
|||
// deleteMembers?: Array<GroupChangeClass.Actions.DeleteMemberAction>;
|
||||
actions.deleteMembers = compact(
|
||||
(actions.deleteMembers || []).map(deleteMember => {
|
||||
if (hasData(deleteMember.deletedUserId)) {
|
||||
if (!isByteBufferEmpty(deleteMember.deletedUserId)) {
|
||||
try {
|
||||
deleteMember.deletedUserId = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -4792,7 +4789,7 @@ function decryptGroupChange(
|
|||
// modifyMemberRoles?: Array<GroupChangeClass.Actions.ModifyMemberRoleAction>;
|
||||
actions.modifyMemberRoles = compact(
|
||||
(actions.modifyMemberRoles || []).map(modifyMember => {
|
||||
if (hasData(modifyMember.userId)) {
|
||||
if (!isByteBufferEmpty(modifyMember.userId)) {
|
||||
try {
|
||||
modifyMember.userId = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -4840,7 +4837,7 @@ function decryptGroupChange(
|
|||
// >;
|
||||
actions.modifyMemberProfileKeys = compact(
|
||||
(actions.modifyMemberProfileKeys || []).map(modifyMemberProfileKey => {
|
||||
if (hasData(modifyMemberProfileKey.presentation)) {
|
||||
if (!isByteBufferEmpty(modifyMemberProfileKey.presentation)) {
|
||||
const { profileKey, uuid } = decryptProfileKeyCredentialPresentation(
|
||||
clientZkGroupCipher,
|
||||
modifyMemberProfileKey.presentation.toArrayBuffer()
|
||||
|
@ -4910,7 +4907,7 @@ function decryptGroupChange(
|
|||
// >;
|
||||
actions.deletePendingMembers = compact(
|
||||
(actions.deletePendingMembers || []).map(deletePendingMember => {
|
||||
if (hasData(deletePendingMember.deletedUserId)) {
|
||||
if (!isByteBufferEmpty(deletePendingMember.deletedUserId)) {
|
||||
try {
|
||||
deletePendingMember.deletedUserId = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -4952,7 +4949,7 @@ function decryptGroupChange(
|
|||
// >;
|
||||
actions.promotePendingMembers = compact(
|
||||
(actions.promotePendingMembers || []).map(promotePendingMember => {
|
||||
if (hasData(promotePendingMember.presentation)) {
|
||||
if (!isByteBufferEmpty(promotePendingMember.presentation)) {
|
||||
const { profileKey, uuid } = decryptProfileKeyCredentialPresentation(
|
||||
clientZkGroupCipher,
|
||||
promotePendingMember.presentation.toArrayBuffer()
|
||||
|
@ -4991,7 +4988,7 @@ function decryptGroupChange(
|
|||
);
|
||||
|
||||
// modifyTitle?: GroupChangeClass.Actions.ModifyTitleAction;
|
||||
if (actions.modifyTitle && hasData(actions.modifyTitle.title)) {
|
||||
if (actions.modifyTitle && !isByteBufferEmpty(actions.modifyTitle.title)) {
|
||||
try {
|
||||
actions.modifyTitle.title = window.textsecure.protobuf.GroupAttributeBlob.decode(
|
||||
decryptGroupBlob(
|
||||
|
@ -5017,7 +5014,7 @@ function decryptGroupChange(
|
|||
// GroupChangeClass.Actions.ModifyDisappearingMessagesTimerAction;
|
||||
if (
|
||||
actions.modifyDisappearingMessagesTimer &&
|
||||
hasData(actions.modifyDisappearingMessagesTimer.timer)
|
||||
!isByteBufferEmpty(actions.modifyDisappearingMessagesTimer.timer)
|
||||
) {
|
||||
try {
|
||||
actions.modifyDisappearingMessagesTimer.timer = window.textsecure.protobuf.GroupAttributeBlob.decode(
|
||||
|
@ -5106,7 +5103,7 @@ function decryptGroupChange(
|
|||
actions.deleteMemberPendingAdminApprovals = compact(
|
||||
(actions.deleteMemberPendingAdminApprovals || []).map(
|
||||
deletePendingApproval => {
|
||||
if (hasData(deletePendingApproval.deletedUserId)) {
|
||||
if (!isByteBufferEmpty(deletePendingApproval.deletedUserId)) {
|
||||
try {
|
||||
deletePendingApproval.deletedUserId = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -5150,7 +5147,7 @@ function decryptGroupChange(
|
|||
actions.promoteMemberPendingAdminApprovals = compact(
|
||||
(actions.promoteMemberPendingAdminApprovals || []).map(
|
||||
promoteAdminApproval => {
|
||||
if (hasData(promoteAdminApproval.userId)) {
|
||||
if (!isByteBufferEmpty(promoteAdminApproval.userId)) {
|
||||
try {
|
||||
promoteAdminApproval.userId = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -5183,7 +5180,7 @@ function decryptGroupChange(
|
|||
// modifyInviteLinkPassword?: GroupChangeClass.Actions.ModifyInviteLinkPasswordAction;
|
||||
if (
|
||||
actions.modifyInviteLinkPassword &&
|
||||
hasData(actions.modifyInviteLinkPassword.inviteLinkPassword)
|
||||
!isByteBufferEmpty(actions.modifyInviteLinkPassword.inviteLinkPassword)
|
||||
) {
|
||||
actions.modifyInviteLinkPassword.inviteLinkPassword = actions.modifyInviteLinkPassword.inviteLinkPassword.toString(
|
||||
'base64'
|
||||
|
@ -5200,7 +5197,7 @@ export function decryptGroupTitle(
|
|||
secretParams: string
|
||||
): string | undefined {
|
||||
const clientZkGroupCipher = getClientZkGroupCipher(secretParams);
|
||||
if (hasData(title)) {
|
||||
if (!isByteBufferEmpty(title)) {
|
||||
const blob = window.textsecure.protobuf.GroupAttributeBlob.decode(
|
||||
decryptGroupBlob(clientZkGroupCipher, title.toArrayBuffer())
|
||||
);
|
||||
|
@ -5221,7 +5218,7 @@ function decryptGroupState(
|
|||
const clientZkGroupCipher = getClientZkGroupCipher(groupSecretParams);
|
||||
|
||||
// title
|
||||
if (hasData(groupState.title)) {
|
||||
if (!isByteBufferEmpty(groupState.title)) {
|
||||
try {
|
||||
groupState.title = window.textsecure.protobuf.GroupAttributeBlob.decode(
|
||||
decryptGroupBlob(clientZkGroupCipher, groupState.title.toArrayBuffer())
|
||||
|
@ -5241,7 +5238,7 @@ function decryptGroupState(
|
|||
// Note: decryption happens during application of the change, on download of the avatar
|
||||
|
||||
// disappearing message timer
|
||||
if (hasData(groupState.disappearingMessagesTimer)) {
|
||||
if (!isByteBufferEmpty(groupState.disappearingMessagesTimer)) {
|
||||
try {
|
||||
groupState.disappearingMessagesTimer = window.textsecure.protobuf.GroupAttributeBlob.decode(
|
||||
decryptGroupBlob(
|
||||
|
@ -5314,7 +5311,7 @@ function decryptGroupState(
|
|||
}
|
||||
|
||||
// inviteLinkPassword
|
||||
if (hasData(groupState.inviteLinkPassword)) {
|
||||
if (!isByteBufferEmpty(groupState.inviteLinkPassword)) {
|
||||
groupState.inviteLinkPassword = groupState.inviteLinkPassword.toString(
|
||||
'base64'
|
||||
);
|
||||
|
@ -5331,7 +5328,7 @@ function decryptMember(
|
|||
logId: string
|
||||
) {
|
||||
// userId
|
||||
if (hasData(member.userId)) {
|
||||
if (!isByteBufferEmpty(member.userId)) {
|
||||
try {
|
||||
member.userId = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -5359,7 +5356,7 @@ function decryptMember(
|
|||
}
|
||||
|
||||
// profileKey
|
||||
if (hasData(member.profileKey)) {
|
||||
if (!isByteBufferEmpty(member.profileKey)) {
|
||||
member.profileKey = decryptProfileKey(
|
||||
clientZkGroupCipher,
|
||||
member.profileKey.toArrayBuffer(),
|
||||
|
@ -5387,7 +5384,7 @@ function decryptMemberPendingProfileKey(
|
|||
logId: string
|
||||
) {
|
||||
// addedByUserId
|
||||
if (hasData(member.addedByUserId)) {
|
||||
if (!isByteBufferEmpty(member.addedByUserId)) {
|
||||
try {
|
||||
member.addedByUserId = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -5433,7 +5430,7 @@ function decryptMemberPendingProfileKey(
|
|||
const { userId, profileKey, role } = member.member;
|
||||
|
||||
// userId
|
||||
if (hasData(userId)) {
|
||||
if (!isByteBufferEmpty(userId)) {
|
||||
try {
|
||||
member.member.userId = decryptUuid(
|
||||
clientZkGroupCipher,
|
||||
|
@ -5467,7 +5464,7 @@ function decryptMemberPendingProfileKey(
|
|||
}
|
||||
|
||||
// profileKey
|
||||
if (hasData(profileKey)) {
|
||||
if (!isByteBufferEmpty(profileKey)) {
|
||||
try {
|
||||
member.member.profileKey = decryptProfileKey(
|
||||
clientZkGroupCipher,
|
||||
|
@ -5512,7 +5509,7 @@ function decryptMemberPendingAdminApproval(
|
|||
const { userId, profileKey } = member;
|
||||
|
||||
// userId
|
||||
if (hasData(userId)) {
|
||||
if (!isByteBufferEmpty(userId)) {
|
||||
try {
|
||||
member.userId = decryptUuid(clientZkGroupCipher, userId.toArrayBuffer());
|
||||
} catch (error) {
|
||||
|
@ -5541,7 +5538,7 @@ function decryptMemberPendingAdminApproval(
|
|||
}
|
||||
|
||||
// profileKey
|
||||
if (hasData(profileKey)) {
|
||||
if (!isByteBufferEmpty(profileKey)) {
|
||||
try {
|
||||
member.profileKey = decryptProfileKey(
|
||||
clientZkGroupCipher,
|
||||
|
|
|
@ -11,7 +11,10 @@ import * as path from 'path';
|
|||
import pino from 'pino';
|
||||
import { createStream } from 'rotating-file-stream';
|
||||
|
||||
import { initLogger, LogLevel as SignalClientLogLevel } from 'libsignal-client';
|
||||
import {
|
||||
initLogger,
|
||||
LogLevel as SignalClientLogLevel,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import { uploadDebugLogs } from './debuglogs';
|
||||
import { redactAll } from '../../js/modules/privacy';
|
||||
|
@ -204,7 +207,7 @@ initLogger(
|
|||
} else if (file) {
|
||||
fileString = ` ${file}`;
|
||||
}
|
||||
const logString = `libsignal-client ${message} ${target}${fileString}`;
|
||||
const logString = `@signalapp/signal-client ${message} ${target}${fileString}`;
|
||||
|
||||
if (level === SignalClientLogLevel.Trace) {
|
||||
log.trace(logString);
|
||||
|
|
|
@ -48,6 +48,7 @@ import {
|
|||
MessageTypeUnhydrated,
|
||||
PreKeyType,
|
||||
SearchResultMessageType,
|
||||
SenderKeyType,
|
||||
ServerInterface,
|
||||
SessionType,
|
||||
SignedPreKeyType,
|
||||
|
@ -134,6 +135,11 @@ const dataInterface: ClientInterface = {
|
|||
removeItemById,
|
||||
removeAllItems,
|
||||
|
||||
createOrUpdateSenderKey,
|
||||
getSenderKeyById,
|
||||
removeAllSenderKeys,
|
||||
getAllSenderKeys,
|
||||
|
||||
createOrUpdateSession,
|
||||
createOrUpdateSessions,
|
||||
getSessionById,
|
||||
|
@ -736,6 +742,23 @@ async function removeAllItems() {
|
|||
await channels.removeAllItems();
|
||||
}
|
||||
|
||||
// Sender Keys
|
||||
|
||||
async function createOrUpdateSenderKey(key: SenderKeyType): Promise<void> {
|
||||
await channels.createOrUpdateSenderKey(key);
|
||||
}
|
||||
async function getSenderKeyById(
|
||||
id: string
|
||||
): Promise<SenderKeyType | undefined> {
|
||||
return channels.getSenderKeyById(id);
|
||||
}
|
||||
async function removeAllSenderKeys(): Promise<void> {
|
||||
await channels.removeAllSenderKeys();
|
||||
}
|
||||
async function getAllSenderKeys(): Promise<Array<SenderKeyType>> {
|
||||
return channels.getAllSenderKeys();
|
||||
}
|
||||
|
||||
// Sessions
|
||||
|
||||
async function createOrUpdateSession(data: SessionType) {
|
||||
|
|
|
@ -66,6 +66,16 @@ export type ClientSearchResultMessageType = MessageType & {
|
|||
bodyRanges: [];
|
||||
snippet: string;
|
||||
};
|
||||
export type SenderKeyType = {
|
||||
// Primary key
|
||||
id: string;
|
||||
// These two are combined into one string to give us the final id
|
||||
senderId: string;
|
||||
distributionId: string;
|
||||
// Raw data to serialize/deserialize into signal-client SenderKeyRecord
|
||||
data: Buffer;
|
||||
lastUpdatedDate: number;
|
||||
};
|
||||
export type SessionType = {
|
||||
id: string;
|
||||
conversationId: string;
|
||||
|
@ -171,6 +181,11 @@ export type DataInterface = {
|
|||
removeAllItems: () => Promise<void>;
|
||||
getAllItems: () => Promise<Array<ItemType>>;
|
||||
|
||||
createOrUpdateSenderKey: (key: SenderKeyType) => Promise<void>;
|
||||
getSenderKeyById: (id: string) => Promise<SenderKeyType | undefined>;
|
||||
removeAllSenderKeys: () => Promise<void>;
|
||||
getAllSenderKeys: () => Promise<Array<SenderKeyType>>;
|
||||
|
||||
createOrUpdateSession: (data: SessionType) => Promise<void>;
|
||||
createOrUpdateSessions: (array: Array<SessionType>) => Promise<void>;
|
||||
getSessionById: (id: string) => Promise<SessionType | undefined>;
|
||||
|
|
|
@ -49,6 +49,7 @@ import {
|
|||
MessageMetricsType,
|
||||
PreKeyType,
|
||||
SearchResultMessageType,
|
||||
SenderKeyType,
|
||||
ServerInterface,
|
||||
SessionType,
|
||||
SignedPreKeyType,
|
||||
|
@ -86,7 +87,7 @@ type StickerRow = Readonly<{
|
|||
|
||||
type EmptyQuery = [];
|
||||
type ArrayQuery = Array<Array<null | number | string>>;
|
||||
type Query = { [key: string]: null | number | string };
|
||||
type Query = { [key: string]: null | number | string | Buffer };
|
||||
|
||||
// Because we can't force this module to conform to an interface, we narrow our exports
|
||||
// to this one default export, which does conform to the interface.
|
||||
|
@ -125,6 +126,11 @@ const dataInterface: ServerInterface = {
|
|||
removeItemById,
|
||||
removeAllItems,
|
||||
|
||||
createOrUpdateSenderKey,
|
||||
getSenderKeyById,
|
||||
removeAllSenderKeys,
|
||||
getAllSenderKeys,
|
||||
|
||||
createOrUpdateSession,
|
||||
createOrUpdateSessions,
|
||||
getSessionById,
|
||||
|
@ -1625,6 +1631,7 @@ async function updateToSchemaVersion25(currentVersion: number, db: Database) {
|
|||
|
||||
db.pragma('user_version = 25');
|
||||
})();
|
||||
console.log('updateToSchemaVersion25: success!');
|
||||
}
|
||||
|
||||
async function updateToSchemaVersion26(currentVersion: number, db: Database) {
|
||||
|
@ -1660,6 +1667,7 @@ async function updateToSchemaVersion26(currentVersion: number, db: Database) {
|
|||
|
||||
db.pragma('user_version = 26');
|
||||
})();
|
||||
console.log('updateToSchemaVersion26: success!');
|
||||
}
|
||||
|
||||
async function updateToSchemaVersion27(currentVersion: number, db: Database) {
|
||||
|
@ -1697,6 +1705,7 @@ async function updateToSchemaVersion27(currentVersion: number, db: Database) {
|
|||
|
||||
db.pragma('user_version = 27');
|
||||
})();
|
||||
console.log('updateToSchemaVersion27: success!');
|
||||
}
|
||||
|
||||
function updateToSchemaVersion28(currentVersion: number, db: Database) {
|
||||
|
@ -1718,6 +1727,7 @@ function updateToSchemaVersion28(currentVersion: number, db: Database) {
|
|||
|
||||
db.pragma('user_version = 28');
|
||||
})();
|
||||
console.log('updateToSchemaVersion28: success!');
|
||||
}
|
||||
|
||||
function updateToSchemaVersion29(currentVersion: number, db: Database) {
|
||||
|
@ -1751,6 +1761,28 @@ function updateToSchemaVersion29(currentVersion: number, db: Database) {
|
|||
|
||||
db.pragma('user_version = 29');
|
||||
})();
|
||||
console.log('updateToSchemaVersion29: success!');
|
||||
}
|
||||
|
||||
function updateToSchemaVersion30(currentVersion: number, db: Database) {
|
||||
if (currentVersion >= 30) {
|
||||
return;
|
||||
}
|
||||
|
||||
db.transaction(() => {
|
||||
db.exec(`
|
||||
CREATE TABLE senderKeys(
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
senderId TEXT NOT NULL,
|
||||
distributionId TEXT NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
lastUpdatedDate NUMBER NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
db.pragma('user_version = 30');
|
||||
})();
|
||||
console.log('updateToSchemaVersion30: success!');
|
||||
}
|
||||
|
||||
const SCHEMA_VERSIONS = [
|
||||
|
@ -1783,6 +1815,7 @@ const SCHEMA_VERSIONS = [
|
|||
updateToSchemaVersion27,
|
||||
updateToSchemaVersion28,
|
||||
updateToSchemaVersion29,
|
||||
updateToSchemaVersion30,
|
||||
];
|
||||
|
||||
function updateSchema(db: Database): void {
|
||||
|
@ -2087,6 +2120,49 @@ function removeAllItems(): Promise<void> {
|
|||
return removeAllFromTable(ITEMS_TABLE);
|
||||
}
|
||||
|
||||
async function createOrUpdateSenderKey(key: SenderKeyType): Promise<void> {
|
||||
const db = getInstance();
|
||||
|
||||
prepare(
|
||||
db,
|
||||
`
|
||||
INSERT OR REPLACE INTO senderKeys (
|
||||
id,
|
||||
senderId,
|
||||
distributionId,
|
||||
data,
|
||||
lastUpdatedDate
|
||||
) values (
|
||||
$id,
|
||||
$senderId,
|
||||
$distributionId,
|
||||
$data,
|
||||
$lastUpdatedDate
|
||||
)
|
||||
`
|
||||
).run(key);
|
||||
}
|
||||
async function getSenderKeyById(
|
||||
id: string
|
||||
): Promise<SenderKeyType | undefined> {
|
||||
const db = getInstance();
|
||||
const row = prepare(db, 'SELECT * FROM senderKeys WHERE id = $id').get({
|
||||
id,
|
||||
});
|
||||
|
||||
return row;
|
||||
}
|
||||
async function removeAllSenderKeys(): Promise<void> {
|
||||
const db = getInstance();
|
||||
prepare(db, 'DELETE FROM senderKeys').run({});
|
||||
}
|
||||
async function getAllSenderKeys(): Promise<Array<SenderKeyType>> {
|
||||
const db = getInstance();
|
||||
const rows = prepare(db, 'SELECT * FROM senderKeys').all({});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
const SESSIONS_TABLE = 'sessions';
|
||||
async function createOrUpdateSession(data: SessionType): Promise<void> {
|
||||
const db = getInstance();
|
||||
|
@ -4635,6 +4711,7 @@ async function removeAll(): Promise<void> {
|
|||
DELETE FROM items;
|
||||
DELETE FROM messages;
|
||||
DELETE FROM preKeys;
|
||||
DELETE FROM senderKeys;
|
||||
DELETE FROM sessions;
|
||||
DELETE FROM signedPreKeys;
|
||||
DELETE FROM unprocessed;
|
||||
|
@ -4657,6 +4734,7 @@ async function removeAllConfiguration(): Promise<void> {
|
|||
DELETE FROM identityKeys;
|
||||
DELETE FROM items;
|
||||
DELETE FROM preKeys;
|
||||
DELETE FROM senderKeys;
|
||||
DELETE FROM sessions;
|
||||
DELETE FROM signedPreKeys;
|
||||
DELETE FROM unprocessed;
|
||||
|
|
|
@ -37,6 +37,7 @@ type CleanedDataValue =
|
|||
| boolean
|
||||
| null
|
||||
| undefined
|
||||
| Buffer
|
||||
| CleanedObject
|
||||
| CleanedArray;
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
|
@ -110,6 +111,10 @@ function cleanDataInner(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
if (data instanceof Buffer) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const dataAsRecord = data as Record<string, unknown>;
|
||||
|
||||
if (
|
||||
|
|
|
@ -61,6 +61,15 @@ describe('cleanDataForIpc', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('keeps Buffers in a field', () => {
|
||||
const buffer = Buffer.from('AABBCC', 'hex');
|
||||
|
||||
assert.deepEqual(cleanDataForIpc(buffer), {
|
||||
cleaned: buffer,
|
||||
pathsChanged: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('converts valid dates to ISO strings', () => {
|
||||
assert.deepEqual(cleanDataForIpc(new Date(924588548000)), {
|
||||
cleaned: '1999-04-20T06:09:08.000Z',
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Direction, SessionRecord } from 'libsignal-client';
|
||||
import {
|
||||
Direction,
|
||||
SenderKeyRecord,
|
||||
SessionRecord,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import { signal } from '../protobuf/compiled';
|
||||
import { sessionStructureToArrayBuffer } from '../util/sessionTranslation';
|
||||
|
@ -14,7 +18,12 @@ import { clampPrivateKey, setPublicKeyTypeByte } from '../Curve';
|
|||
import { SignalProtocolStore } from '../SignalProtocolStore';
|
||||
import { IdentityKeyType, KeyPairType } from '../textsecure/Types.d';
|
||||
|
||||
const { RecordStructure, SessionStructure } = signal.proto.storage;
|
||||
const {
|
||||
RecordStructure,
|
||||
SessionStructure,
|
||||
SenderKeyRecordStructure,
|
||||
SenderKeyStateStructure,
|
||||
} = signal.proto.storage;
|
||||
|
||||
describe('SignalProtocolStore', () => {
|
||||
const number = '+5558675309';
|
||||
|
@ -47,6 +56,41 @@ describe('SignalProtocolStore', () => {
|
|||
);
|
||||
}
|
||||
|
||||
function getSenderKeyRecord(): SenderKeyRecord {
|
||||
const proto = new SenderKeyRecordStructure();
|
||||
|
||||
const state = new SenderKeyStateStructure();
|
||||
|
||||
state.senderKeyId = 4;
|
||||
|
||||
const senderChainKey = new SenderKeyStateStructure.SenderChainKey();
|
||||
|
||||
senderChainKey.iteration = 10;
|
||||
senderChainKey.seed = toUint8Array(getPublicKey());
|
||||
state.senderChainKey = senderChainKey;
|
||||
|
||||
const senderSigningKey = new SenderKeyStateStructure.SenderSigningKey();
|
||||
senderSigningKey.public = toUint8Array(getPublicKey());
|
||||
senderSigningKey.private = toUint8Array(getPrivateKey());
|
||||
|
||||
state.senderSigningKey = senderSigningKey;
|
||||
|
||||
state.senderMessageKeys = [];
|
||||
const messageKey = new SenderKeyStateStructure.SenderMessageKey();
|
||||
messageKey.iteration = 234;
|
||||
messageKey.seed = toUint8Array(getPublicKey());
|
||||
state.senderMessageKeys.push(messageKey);
|
||||
|
||||
proto.senderKeyStates = [];
|
||||
proto.senderKeyStates.push(state);
|
||||
|
||||
return SenderKeyRecord.deserialize(
|
||||
Buffer.from(
|
||||
signal.proto.storage.SenderKeyRecordStructure.encode(proto).finish()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function toUint8Array(buffer: ArrayBuffer): Uint8Array {
|
||||
return new Uint8Array(buffer);
|
||||
}
|
||||
|
@ -109,6 +153,49 @@ describe('SignalProtocolStore', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('senderKeys', () => {
|
||||
it('roundtrips in memory', async () => {
|
||||
const distributionId = window.getGuid();
|
||||
const expected = getSenderKeyRecord();
|
||||
|
||||
const deviceId = 1;
|
||||
const encodedAddress = `${number}.${deviceId}`;
|
||||
|
||||
await store.saveSenderKey(encodedAddress, distributionId, expected);
|
||||
|
||||
const actual = await store.getSenderKey(encodedAddress, distributionId);
|
||||
if (!actual) {
|
||||
throw new Error('getSenderKey returned nothing!');
|
||||
}
|
||||
|
||||
assert.isTrue(
|
||||
constantTimeEqual(expected.serialize(), actual.serialize())
|
||||
);
|
||||
});
|
||||
|
||||
it('roundtrips through database', async () => {
|
||||
const distributionId = window.getGuid();
|
||||
const expected = getSenderKeyRecord();
|
||||
|
||||
const deviceId = 1;
|
||||
const encodedAddress = `${number}.${deviceId}`;
|
||||
|
||||
await store.saveSenderKey(encodedAddress, distributionId, expected);
|
||||
|
||||
// Re-fetch from the database to ensure we get the latest database value
|
||||
await store.hydrateCaches();
|
||||
|
||||
const actual = await store.getSenderKey(encodedAddress, distributionId);
|
||||
if (!actual) {
|
||||
throw new Error('getSenderKey returned nothing!');
|
||||
}
|
||||
|
||||
assert.isTrue(
|
||||
constantTimeEqual(expected.serialize(), actual.serialize())
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveIdentity', () => {
|
||||
const identifier = `${number}.1`;
|
||||
|
||||
|
|
31
ts/test-electron/util/isByteBufferEmpty_test.ts
Normal file
31
ts/test-electron/util/isByteBufferEmpty_test.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { isByteBufferEmpty } from '../../util/isByteBufferEmpty';
|
||||
|
||||
describe('isByteBufferEmpty', () => {
|
||||
it('returns true for undefined', () => {
|
||||
assert.isTrue(isByteBufferEmpty(undefined));
|
||||
});
|
||||
|
||||
it('returns true for object missing limit', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const brokenByteBuffer: any = {};
|
||||
|
||||
assert.isTrue(isByteBufferEmpty(brokenByteBuffer));
|
||||
});
|
||||
|
||||
it('returns true for object limit', () => {
|
||||
const emptyByteBuffer = new window.dcodeIO.ByteBuffer(0);
|
||||
|
||||
assert.isTrue(isByteBufferEmpty(emptyByteBuffer));
|
||||
});
|
||||
|
||||
it('returns false for object limit', () => {
|
||||
const byteBuffer = window.dcodeIO.ByteBuffer.wrap('AABBCC', 'hex');
|
||||
|
||||
assert.isFalse(isByteBufferEmpty(byteBuffer));
|
||||
});
|
||||
});
|
12
ts/textsecure.d.ts
vendored
12
ts/textsecure.d.ts
vendored
|
@ -573,6 +573,7 @@ export declare class ContentClass {
|
|||
nullMessage?: NullMessageClass;
|
||||
receiptMessage?: ReceiptMessageClass;
|
||||
typingMessage?: TypingMessageClass;
|
||||
senderKeyDistributionMessage?: ByteBufferClass;
|
||||
}
|
||||
|
||||
export declare class DataMessageClass {
|
||||
|
@ -733,6 +734,7 @@ export declare namespace EnvelopeClass {
|
|||
static PREKEY_BUNDLE: number;
|
||||
static RECEIPT: number;
|
||||
static UNIDENTIFIED_SENDER: number;
|
||||
static SENDERKEY: number;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1345,7 +1347,7 @@ export declare namespace SenderCertificateClass {
|
|||
) => Certificate;
|
||||
toArrayBuffer: () => ArrayBuffer;
|
||||
|
||||
sender?: string;
|
||||
senderE164?: string;
|
||||
senderUuid?: string;
|
||||
senderDevice?: number;
|
||||
expires?: ProtoBigNumberType;
|
||||
|
@ -1377,6 +1379,8 @@ export declare namespace UnidentifiedSenderMessageClass {
|
|||
type?: number;
|
||||
senderCertificate?: SenderCertificateClass;
|
||||
content?: ProtoBinaryType;
|
||||
contentHint?: number;
|
||||
groupId?: ProtoBinaryType;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1384,5 +1388,11 @@ export declare namespace UnidentifiedSenderMessageClass.Message {
|
|||
class Type {
|
||||
static PREKEY_MESSAGE: number;
|
||||
static MESSAGE: number;
|
||||
static SENDERKEY_MESSAGE: number;
|
||||
}
|
||||
|
||||
class ContentHint {
|
||||
static SUPPLEMENTARY: number;
|
||||
static RETRY: number;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,20 +14,25 @@ import PQueue from 'p-queue';
|
|||
import { v4 as getGuid } from 'uuid';
|
||||
|
||||
import {
|
||||
groupDecrypt,
|
||||
PreKeySignalMessage,
|
||||
processSenderKeyDistributionMessage,
|
||||
ProtocolAddress,
|
||||
PublicKey,
|
||||
SealedSenderDecryptionResult,
|
||||
sealedSenderDecryptMessage,
|
||||
sealedSenderDecryptToUsmc,
|
||||
SenderKeyDistributionMessage,
|
||||
signalDecrypt,
|
||||
signalDecryptPreKey,
|
||||
SignalMessage,
|
||||
} from 'libsignal-client';
|
||||
UnidentifiedSenderMessageContent,
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import {
|
||||
IdentityKeys,
|
||||
PreKeys,
|
||||
SenderKeys,
|
||||
Sessions,
|
||||
SignedPreKeys,
|
||||
} from '../LibSignalStores';
|
||||
|
@ -43,6 +48,7 @@ import WebSocketResource, {
|
|||
import Crypto from './Crypto';
|
||||
import { deriveMasterKeyFromGroupV1, typedArrayToArrayBuffer } from '../Crypto';
|
||||
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||
import { isByteBufferEmpty } from '../util/isByteBufferEmpty';
|
||||
|
||||
import {
|
||||
AttachmentPointerClass,
|
||||
|
@ -56,6 +62,7 @@ import {
|
|||
UnprocessedType,
|
||||
VerifiedClass,
|
||||
} from '../textsecure.d';
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
|
||||
import { WebSocket } from './WebSocket';
|
||||
|
||||
|
@ -962,9 +969,12 @@ class MessageReceiverInner extends EventTarget {
|
|||
|
||||
async decrypt(
|
||||
envelope: EnvelopeClass,
|
||||
ciphertext: any
|
||||
ciphertext: ByteBufferClass
|
||||
): Promise<ArrayBuffer | null> {
|
||||
const { serverTrustRoot } = this;
|
||||
const envelopeTypeEnum = window.textsecure.protobuf.Envelope.Type;
|
||||
const unidentifiedSenderTypeEnum =
|
||||
window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||
|
||||
const identifier = envelope.sourceUuid || envelope.source;
|
||||
const { sourceDevice } = envelope;
|
||||
|
@ -989,7 +999,32 @@ class MessageReceiverInner extends EventTarget {
|
|||
ArrayBuffer | { isMe: boolean } | { isBlocked: boolean } | undefined
|
||||
>;
|
||||
|
||||
if (envelope.type === window.textsecure.protobuf.Envelope.Type.CIPHERTEXT) {
|
||||
if (envelope.type === envelopeTypeEnum.SENDERKEY) {
|
||||
window.log.info('sender key message from', this.getEnvelopeId(envelope));
|
||||
if (!identifier) {
|
||||
throw new Error(
|
||||
'MessageReceiver.decrypt: No identifier for SENDERKEY message'
|
||||
);
|
||||
}
|
||||
if (!sourceDevice) {
|
||||
throw new Error(
|
||||
'MessageReceiver.decrypt: No sourceDevice for SENDERKEY message'
|
||||
);
|
||||
}
|
||||
|
||||
const senderKeyStore = new SenderKeys();
|
||||
const address = `${identifier}.${sourceDevice}`;
|
||||
const messageBuffer = Buffer.from(ciphertext.toArrayBuffer());
|
||||
promise = window.textsecure.storage.protocol.enqueueSenderKeyJob(
|
||||
address,
|
||||
() =>
|
||||
groupDecrypt(
|
||||
ProtocolAddress.new(identifier, sourceDevice),
|
||||
senderKeyStore,
|
||||
messageBuffer
|
||||
).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext)))
|
||||
);
|
||||
} else if (envelope.type === envelopeTypeEnum.CIPHERTEXT) {
|
||||
window.log.info('message from', this.getEnvelopeId(envelope));
|
||||
if (!identifier) {
|
||||
throw new Error(
|
||||
|
@ -1016,9 +1051,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
identityKeyStore
|
||||
).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext)))
|
||||
);
|
||||
} else if (
|
||||
envelope.type === window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE
|
||||
) {
|
||||
} else if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) {
|
||||
window.log.info('prekey message from', this.getEnvelopeId(envelope));
|
||||
if (!identifier) {
|
||||
throw new Error(
|
||||
|
@ -1047,17 +1080,14 @@ class MessageReceiverInner extends EventTarget {
|
|||
signedPreKeyStore
|
||||
).then(plaintext => this.unpad(typedArrayToArrayBuffer(plaintext)))
|
||||
);
|
||||
} else if (
|
||||
envelope.type ===
|
||||
window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER
|
||||
) {
|
||||
} else if (envelope.type === envelopeTypeEnum.UNIDENTIFIED_SENDER) {
|
||||
window.log.info('received unidentified sender message');
|
||||
const buffer = Buffer.from(ciphertext.toArrayBuffer());
|
||||
|
||||
const decryptSealedSender = async (): Promise<
|
||||
SealedSenderDecryptionResult | null | { isBlocked: true }
|
||||
SealedSenderDecryptionResult | Buffer | null | { isBlocked: true }
|
||||
> => {
|
||||
const messageContent = await sealedSenderDecryptToUsmc(
|
||||
const messageContent: UnidentifiedSenderMessageContent = await sealedSenderDecryptToUsmc(
|
||||
buffer,
|
||||
identityKeyStore
|
||||
);
|
||||
|
@ -1101,6 +1131,30 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
if (
|
||||
messageContent.msgType() ===
|
||||
unidentifiedSenderTypeEnum.SENDERKEY_MESSAGE
|
||||
) {
|
||||
const sealedSenderIdentifier = certificate.senderUuid();
|
||||
const sealedSenderSourceDevice = certificate.senderDeviceId();
|
||||
const senderKeyStore = new SenderKeys();
|
||||
|
||||
const address = `${sealedSenderIdentifier}.${sealedSenderSourceDevice}`;
|
||||
|
||||
return window.textsecure.storage.protocol.enqueueSenderKeyJob(
|
||||
address,
|
||||
() =>
|
||||
groupDecrypt(
|
||||
ProtocolAddress.new(
|
||||
sealedSenderIdentifier,
|
||||
sealedSenderSourceDevice
|
||||
),
|
||||
senderKeyStore,
|
||||
buffer
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const sealedSenderIdentifier = envelope.sourceUuid || envelope.source;
|
||||
const address = `${sealedSenderIdentifier}.${envelope.sourceDevice}`;
|
||||
return window.textsecure.storage.protocol.enqueueSessionJob(
|
||||
|
@ -1128,6 +1182,9 @@ class MessageReceiverInner extends EventTarget {
|
|||
if ('isBlocked' in result) {
|
||||
return result;
|
||||
}
|
||||
if (result instanceof Buffer) {
|
||||
return this.unpad(typedArrayToArrayBuffer(result));
|
||||
}
|
||||
|
||||
const content = typedArrayToArrayBuffer(result.message());
|
||||
|
||||
|
@ -1390,7 +1447,10 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
async handleDataMessage(envelope: EnvelopeClass, msg: DataMessageClass) {
|
||||
async handleDataMessage(
|
||||
envelope: EnvelopeClass,
|
||||
msg: DataMessageClass
|
||||
): Promise<void> {
|
||||
window.log.info(
|
||||
'MessageReceiver.handleDataMessage',
|
||||
this.getEnvelopeId(envelope)
|
||||
|
@ -1519,35 +1579,101 @@ class MessageReceiverInner extends EventTarget {
|
|||
async innerHandleContentMessage(
|
||||
envelope: EnvelopeClass,
|
||||
plaintext: ArrayBuffer
|
||||
) {
|
||||
): Promise<void> {
|
||||
const content = window.textsecure.protobuf.Content.decode(plaintext);
|
||||
|
||||
// Note: a distribution message can be tacked on to any other message, so we
|
||||
// make sure to process it first. If that fails, we still try to process
|
||||
// the rest of the message.
|
||||
try {
|
||||
if (content.senderKeyDistributionMessage) {
|
||||
await this.handleSenderKeyDistributionMessage(
|
||||
envelope,
|
||||
content.senderKeyDistributionMessage
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorString = error && error.stack ? error.stack : error;
|
||||
window.log.error(
|
||||
`innerHandleContentMessage: Failed to process sender key distribution message: ${errorString}`
|
||||
);
|
||||
}
|
||||
|
||||
if (content.syncMessage) {
|
||||
return this.handleSyncMessage(envelope, content.syncMessage);
|
||||
await this.handleSyncMessage(envelope, content.syncMessage);
|
||||
return;
|
||||
}
|
||||
if (content.dataMessage) {
|
||||
return this.handleDataMessage(envelope, content.dataMessage);
|
||||
await this.handleDataMessage(envelope, content.dataMessage);
|
||||
return;
|
||||
}
|
||||
if (content.nullMessage) {
|
||||
this.handleNullMessage(envelope);
|
||||
return undefined;
|
||||
await this.handleNullMessage(envelope);
|
||||
return;
|
||||
}
|
||||
if (content.callingMessage) {
|
||||
return this.handleCallingMessage(envelope, content.callingMessage);
|
||||
await this.handleCallingMessage(envelope, content.callingMessage);
|
||||
return;
|
||||
}
|
||||
if (content.receiptMessage) {
|
||||
return this.handleReceiptMessage(envelope, content.receiptMessage);
|
||||
await this.handleReceiptMessage(envelope, content.receiptMessage);
|
||||
return;
|
||||
}
|
||||
if (content.typingMessage) {
|
||||
return this.handleTypingMessage(envelope, content.typingMessage);
|
||||
await this.handleTypingMessage(envelope, content.typingMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
throw new Error('Unsupported content message');
|
||||
|
||||
if (isByteBufferEmpty(content.senderKeyDistributionMessage)) {
|
||||
throw new Error('Unsupported content message');
|
||||
}
|
||||
}
|
||||
|
||||
async handleSenderKeyDistributionMessage(
|
||||
envelope: EnvelopeClass,
|
||||
distributionMessage: ByteBufferClass
|
||||
): Promise<void> {
|
||||
const envelopeId = this.getEnvelopeId(envelope);
|
||||
window.log.info(`handleSenderKeyDistributionMessage: ${envelopeId}`);
|
||||
|
||||
// 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 { sourceDevice } = envelope;
|
||||
if (!identifier) {
|
||||
throw new Error(
|
||||
`handleSenderKeyDistributionMessage: No identifier for envelope ${envelopeId}`
|
||||
);
|
||||
}
|
||||
if (!isNumber(sourceDevice)) {
|
||||
throw new Error(
|
||||
`handleSenderKeyDistributionMessage: Missing sourceDevice for envelope ${envelopeId}`
|
||||
);
|
||||
}
|
||||
|
||||
const sender = ProtocolAddress.new(identifier, sourceDevice);
|
||||
const senderKeyDistributionMessage = SenderKeyDistributionMessage.deserialize(
|
||||
Buffer.from(distributionMessage.toArrayBuffer())
|
||||
);
|
||||
const senderKeyStore = new SenderKeys();
|
||||
const address = `${identifier}.${sourceDevice}`;
|
||||
|
||||
await window.textsecure.storage.protocol.enqueueSenderKeyJob(address, () =>
|
||||
processSenderKeyDistributionMessage(
|
||||
sender,
|
||||
senderKeyDistributionMessage,
|
||||
senderKeyStore
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async handleCallingMessage(
|
||||
envelope: EnvelopeClass,
|
||||
callingMessage: CallingMessageClass
|
||||
) {
|
||||
): Promise<void> {
|
||||
this.removeFromCache(envelope);
|
||||
await window.Signal.Services.calling.handleCallingMessage(
|
||||
envelope,
|
||||
|
@ -1558,7 +1684,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
async handleReceiptMessage(
|
||||
envelope: EnvelopeClass,
|
||||
receiptMessage: ReceiptMessageClass
|
||||
) {
|
||||
): Promise<void> {
|
||||
const results = [];
|
||||
if (
|
||||
receiptMessage.type ===
|
||||
|
@ -1593,13 +1719,13 @@ class MessageReceiverInner extends EventTarget {
|
|||
results.push(this.dispatchAndWait(ev));
|
||||
}
|
||||
}
|
||||
return Promise.all(results);
|
||||
await Promise.all(results);
|
||||
}
|
||||
|
||||
async handleTypingMessage(
|
||||
envelope: EnvelopeClass,
|
||||
typingMessage: TypingMessageClass
|
||||
) {
|
||||
): Promise<void> {
|
||||
const ev = new Event('typing');
|
||||
|
||||
this.removeFromCache(envelope);
|
||||
|
@ -1612,7 +1738,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
window.log.warn(
|
||||
`Typing message envelope timestamp (${envelopeTimestamp}) did not match typing timestamp (${typingTimestamp})`
|
||||
);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1645,10 +1771,10 @@ class MessageReceiverInner extends EventTarget {
|
|||
}
|
||||
}
|
||||
|
||||
return this.dispatchEvent(ev);
|
||||
await this.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
handleNullMessage(envelope: EnvelopeClass) {
|
||||
handleNullMessage(envelope: EnvelopeClass): void {
|
||||
window.log.info(
|
||||
'MessageReceiver.handleNullMessage',
|
||||
this.getEnvelopeId(envelope)
|
||||
|
@ -1783,7 +1909,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
async handleSyncMessage(
|
||||
envelope: EnvelopeClass,
|
||||
syncMessage: SyncMessageClass
|
||||
) {
|
||||
): Promise<void> {
|
||||
const unidentified = syncMessage.sent
|
||||
? syncMessage.sent.unidentifiedStatus || []
|
||||
: [];
|
||||
|
@ -2026,7 +2152,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
async handleRead(
|
||||
envelope: EnvelopeClass,
|
||||
read: Array<SyncMessageClass.Read>
|
||||
) {
|
||||
): Promise<void> {
|
||||
window.log.info('MessageReceiver.handleRead', this.getEnvelopeId(envelope));
|
||||
const results = [];
|
||||
for (let i = 0; i < read.length; i += 1) {
|
||||
|
@ -2046,7 +2172,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
results.push(this.dispatchAndWait(ev));
|
||||
}
|
||||
return Promise.all(results);
|
||||
await Promise.all(results);
|
||||
}
|
||||
|
||||
handleContacts(envelope: EnvelopeClass, contacts: SyncMessageClass.Contacts) {
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
sealedSenderEncryptMessage,
|
||||
SenderCertificate,
|
||||
signalEncrypt,
|
||||
} from 'libsignal-client';
|
||||
} from '@signalapp/signal-client';
|
||||
|
||||
import { ServerKeysType, WebAPIType } from './WebAPI';
|
||||
import { ContentClass, DataMessageClass } from '../textsecure.d';
|
||||
|
|
5
ts/textsecure/Types.d.ts
vendored
5
ts/textsecure/Types.d.ts
vendored
|
@ -3,11 +3,12 @@
|
|||
|
||||
export {
|
||||
IdentityKeyType,
|
||||
SignedPreKeyType,
|
||||
PreKeyType,
|
||||
SenderKeyType,
|
||||
SessionType,
|
||||
SignedPreKeyType,
|
||||
UnprocessedType,
|
||||
UnprocessedUpdateType,
|
||||
SessionType,
|
||||
} from '../sql/Interface';
|
||||
|
||||
// How the legacy APIs generate these types
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2019-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { PrivateKey, PublicKey } from 'libsignal-client';
|
||||
import { PrivateKey, PublicKey } from '@signalapp/signal-client';
|
||||
|
||||
export function keyPair(): Record<string, Buffer> {
|
||||
const privKey = PrivateKey.generate();
|
||||
|
|
10
ts/util/isByteBufferEmpty.ts
Normal file
10
ts/util/isByteBufferEmpty.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
// Copyright 2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
import { ByteBufferClass } from '../window.d';
|
||||
|
||||
export function isByteBufferEmpty(data?: ByteBufferClass): boolean {
|
||||
return !data || !isNumber(data.limit) || data.limit === 0;
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import { PublicKey, Fingerprint } from 'libsignal-client';
|
||||
import { PublicKey, Fingerprint } from '@signalapp/signal-client';
|
||||
import { ConversationType } from '../state/ducks/conversations';
|
||||
|
||||
export async function generateSecurityNumber(
|
||||
|
|
|
@ -17,7 +17,7 @@ const EXTERNAL_MODULE = new Set([
|
|||
'fsevents',
|
||||
'got',
|
||||
'jquery',
|
||||
'libsignal-client',
|
||||
'@signalapp/signal-client',
|
||||
'node-fetch',
|
||||
'node-sass',
|
||||
'pino',
|
||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -1467,6 +1467,14 @@
|
|||
react-lifecycles-compat "^3.0.4"
|
||||
warning "^3.0.0"
|
||||
|
||||
"@signalapp/signal-client@0.5.1":
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@signalapp/signal-client/-/signal-client-0.5.1.tgz#b893a658db92f7fe3d3657ac9a4f83909ac1d09d"
|
||||
integrity sha512-d3wM2vS4IcPGmBzcjigD1Y14J3j4rP+dTpE1J5xrPfknLgGPXLR+dX4I6RU9nFVe5toCyrRnTSBjQbBn/SixKA==
|
||||
dependencies:
|
||||
node-gyp-build "^4.2.3"
|
||||
uuid "^8.3.0"
|
||||
|
||||
"@sindresorhus/is@0.8.0":
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.8.0.tgz#073aee40b0aab2d4ace33c0a2a2672a37da6fa12"
|
||||
|
@ -11134,12 +11142,6 @@ levn@~0.3.0:
|
|||
prelude-ls "~1.1.2"
|
||||
type-check "~0.3.2"
|
||||
|
||||
"libsignal-client@https://github.com/signalapp/libsignal-client-node.git#afa8f40eaa218b7fff94278d9b2ab2e13a2ee04b":
|
||||
version "0.3.3"
|
||||
resolved "https://github.com/signalapp/libsignal-client-node.git#afa8f40eaa218b7fff94278d9b2ab2e13a2ee04b"
|
||||
dependencies:
|
||||
bindings "^1.5.0"
|
||||
|
||||
lie@*:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lie/-/lie-3.2.0.tgz#4f13f2f8bbb027d383db338c43043545791aa8dc"
|
||||
|
@ -12339,7 +12341,7 @@ node-forge@0.10.0, node-forge@^0.10.0:
|
|||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.10.0.tgz#32dea2afb3e9926f02ee5ce8794902691a676bf3"
|
||||
integrity sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==
|
||||
|
||||
node-gyp-build@^4.2.1:
|
||||
node-gyp-build@^4.2.1, node-gyp-build@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739"
|
||||
integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==
|
||||
|
@ -18025,6 +18027,11 @@ uuid@^3.4.0:
|
|||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
uuid@^8.3.0:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
v8-compile-cache@^2.0.3:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"
|
||||
|
|
Loading…
Reference in a new issue