Receive support for Sender Key

Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
Scott Nonnenberg 2021-05-13 18:18:43 -07:00
parent e5f9c0db28
commit e6bab06510
28 changed files with 743 additions and 164 deletions

View file

@ -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",

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -16,7 +16,7 @@ const {
const SKIPPED_DEPENDENCIES = new Set([
'ringrtc',
'zkgroup',
'libsignal-client',
'@signalapp/signal-client',
]);
const rootDir = join(__dirname, '..');

View file

@ -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,

View file

@ -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);
});

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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);
}

View file

@ -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,

View file

@ -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);

View file

@ -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) {

View file

@ -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>;

View file

@ -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;

View file

@ -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 (

View file

@ -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',

View file

@ -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`;

View 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
View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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';

View file

@ -3,11 +3,12 @@
export {
IdentityKeyType,
SignedPreKeyType,
PreKeyType,
SenderKeyType,
SessionType,
SignedPreKeyType,
UnprocessedType,
UnprocessedUpdateType,
SessionType,
} from '../sql/Interface';
// How the legacy APIs generate these types

View file

@ -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();

View 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;
}

View file

@ -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(

View file

@ -17,7 +17,7 @@ const EXTERNAL_MODULE = new Set([
'fsevents',
'got',
'jquery',
'libsignal-client',
'@signalapp/signal-client',
'node-fetch',
'node-sass',
'pino',

View file

@ -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"