Social Graph: read-only state sync with primary device

This commit is contained in:
Josh Perez 2020-07-06 20:56:56 -04:00 committed by Scott Nonnenberg
parent 12745a2c79
commit 8502d23576
19 changed files with 1035 additions and 57 deletions

View file

@ -1,5 +1,6 @@
{ {
"serverUrl": "https://textsecure-service-staging.whispersystems.org", "serverUrl": "https://textsecure-service-staging.whispersystems.org",
"storageUrl": "https://storage-staging.signal.org",
"cdn": { "cdn": {
"0": "https://cdn-staging.signal.org", "0": "https://cdn-staging.signal.org",
"2": "https://cdn2-staging.signal.org" "2": "https://cdn2-staging.signal.org"

View file

@ -1,5 +1,6 @@
{ {
"serverUrl": "https://textsecure-service.whispersystems.org", "serverUrl": "https://textsecure-service.whispersystems.org",
"storageUrl": "https://storage.signal.org",
"cdn": { "cdn": {
"0": "https://cdn.signal.org", "0": "https://cdn.signal.org",
"2": "https://cdn2.signal.org" "2": "https://cdn2.signal.org"

View file

@ -1675,6 +1675,8 @@
addQueuedEventListener('viewSync', onViewSync); addQueuedEventListener('viewSync', onViewSync);
addQueuedEventListener('messageRequestResponse', onMessageRequestResponse); addQueuedEventListener('messageRequestResponse', onMessageRequestResponse);
addQueuedEventListener('profileKeyUpdate', onProfileKeyUpdate); addQueuedEventListener('profileKeyUpdate', onProfileKeyUpdate);
addQueuedEventListener('fetchLatest', onFetchLatestSync);
addQueuedEventListener('keys', onKeysSync);
window.Signal.AttachmentDownloads.start({ window.Signal.AttachmentDownloads.start({
getMessageReceiver: () => messageReceiver, getMessageReceiver: () => messageReceiver,
@ -1688,6 +1690,7 @@
if (connectCount === 1) { if (connectCount === 1) {
window.Signal.Stickers.downloadQueuedPacks(); window.Signal.Stickers.downloadQueuedPacks();
await window.textsecure.messaging.sendRequestKeySyncMessage();
} }
// On startup after upgrading to a new version, request a contact sync // On startup after upgrading to a new version, request a contact sync
@ -2728,6 +2731,45 @@
Whisper.ViewSyncs.onSync(sync); Whisper.ViewSyncs.onSync(sync);
} }
async function onFetchLatestSync(ev) {
ev.confirm();
const { eventType } = ev;
const FETCH_LATEST_ENUM = textsecure.protobuf.SyncMessage.FetchLatest.Type;
switch (eventType) {
case FETCH_LATEST_ENUM.LOCAL_PROFILE:
// Intentionally do nothing since we'll be receiving the storage manifest request
// and will update local profile along with that.
break;
case FETCH_LATEST_ENUM.STORAGE_MANIFEST:
window.log.info('onFetchLatestSync: fetching latest manifest');
await window.Signal.Util.runStorageServiceSyncJob();
break;
default:
window.log.info(
`onFetchLatestSync: Unknown type encountered ${eventType}`
);
}
}
async function onKeysSync(ev) {
ev.confirm();
const { storageServiceKey } = ev;
if (storageServiceKey) {
window.log.info('onKeysSync: received keys');
const storageServiceKeyBase64 = window.Signal.Crypto.arrayBufferToBase64(
storageServiceKey
);
storage.put('storageKey', storageServiceKeyBase64);
await window.Signal.Util.runStorageServiceSyncJob();
}
}
async function onMessageRequestResponse(ev) { async function onMessageRequestResponse(ev) {
ev.confirm(); ev.confirm();

View file

@ -39,6 +39,14 @@
writeNewAttachmentData, writeNewAttachmentData,
} = window.Signal.Migrations; } = window.Signal.Migrations;
const { addStickerPackReference } = window.Signal.Data; const { addStickerPackReference } = window.Signal.Data;
const {
arrayBufferToBase64,
base64ToArrayBuffer,
deriveAccessKey,
getRandomBytes,
stringFromBytes,
verifyAccessKey,
} = window.Signal.Crypto;
const COLORS = [ const COLORS = [
'red', 'red',
@ -760,6 +768,8 @@
verified verified
); );
} }
return keyChange;
}, },
sendVerifySyncMessage(e164, uuid, state) { sendVerifySyncMessage(e164, uuid, state) {
// Because syncVerification sends a (null) message to the target of the verify and // Because syncVerification sends a (null) message to the target of the verify and
@ -1754,11 +1764,7 @@
// If we've never fetched user's profile, we default to what we have // If we've never fetched user's profile, we default to what we have
if (sealedSender === SEALED_SENDER.UNKNOWN) { if (sealedSender === SEALED_SENDER.UNKNOWN) {
const info = { const info = {
accessKey: accessKey: accessKey || arrayBufferToBase64(getRandomBytes(16)),
accessKey ||
window.Signal.Crypto.arrayBufferToBase64(
window.Signal.Crypto.getRandomBytes(16)
),
// Indicates that a client is capable of receiving uuid-only messages. // Indicates that a client is capable of receiving uuid-only messages.
// Not used yet. // Not used yet.
uuidCapable, uuidCapable,
@ -1777,9 +1783,7 @@
accessKey: accessKey:
accessKey && sealedSender === SEALED_SENDER.ENABLED accessKey && sealedSender === SEALED_SENDER.ENABLED
? accessKey ? accessKey
: window.Signal.Crypto.arrayBufferToBase64( : arrayBufferToBase64(getRandomBytes(16)),
window.Signal.Crypto.getRandomBytes(16)
),
// Indicates that a client is capable of receiving uuid-only messages. // Indicates that a client is capable of receiving uuid-only messages.
// Not used yet. // Not used yet.
uuidCapable, uuidCapable,
@ -2343,9 +2347,7 @@
}); });
} }
const identityKey = window.Signal.Crypto.base64ToArrayBuffer( const identityKey = base64ToArrayBuffer(profile.identityKey);
profile.identityKey
);
const changed = await textsecure.storage.protocol.saveIdentity( const changed = await textsecure.storage.protocol.saveIdentity(
`${id}.1`, `${id}.1`,
identityKey, identityKey,
@ -2375,9 +2377,9 @@
sealedSender: SEALED_SENDER.UNRESTRICTED, sealedSender: SEALED_SENDER.UNRESTRICTED,
}); });
} else if (accessKey && profile.unidentifiedAccess) { } else if (accessKey && profile.unidentifiedAccess) {
const haveCorrectKey = await window.Signal.Crypto.verifyAccessKey( const haveCorrectKey = await verifyAccessKey(
window.Signal.Crypto.base64ToArrayBuffer(accessKey), base64ToArrayBuffer(accessKey),
window.Signal.Crypto.base64ToArrayBuffer(profile.unidentifiedAccess) base64ToArrayBuffer(profile.unidentifiedAccess)
); );
if (haveCorrectKey) { if (haveCorrectKey) {
@ -2466,8 +2468,8 @@
} }
// decode // decode
const keyBuffer = window.Signal.Crypto.base64ToArrayBuffer(key); const keyBuffer = base64ToArrayBuffer(key);
const data = window.Signal.Crypto.base64ToArrayBuffer(encryptedName); const data = base64ToArrayBuffer(encryptedName);
// decrypt // decrypt
const { given, family } = await textsecure.crypto.decryptProfileName( const { given, family } = await textsecure.crypto.decryptProfileName(
@ -2476,10 +2478,8 @@
); );
// encode // encode
const profileName = window.Signal.Crypto.stringFromBytes(given); const profileFamilyName = family ? stringFromBytes(family) : null;
const profileFamilyName = family const profileName = given ? stringFromBytes(given) : null;
? window.Signal.Crypto.stringFromBytes(family)
: null;
// set // set
this.set({ profileName, profileFamilyName }); this.set({ profileName, profileFamilyName });
@ -2494,7 +2494,7 @@
if (!key) { if (!key) {
return; return;
} }
const keyBuffer = window.Signal.Crypto.base64ToArrayBuffer(key); const keyBuffer = base64ToArrayBuffer(key);
// decrypt // decrypt
const decrypted = await textsecure.crypto.decryptProfile( const decrypted = await textsecure.crypto.decryptProfile(
@ -2577,15 +2577,9 @@
return; return;
} }
const profileKeyBuffer = window.Signal.Crypto.base64ToArrayBuffer( const profileKeyBuffer = base64ToArrayBuffer(profileKey);
profileKey const accessKeyBuffer = await deriveAccessKey(profileKeyBuffer);
); const accessKey = arrayBufferToBase64(accessKeyBuffer);
const accessKeyBuffer = await window.Signal.Crypto.deriveAccessKey(
profileKeyBuffer
);
const accessKey = window.Signal.Crypto.arrayBufferToBase64(
accessKeyBuffer
);
this.set({ accessKey }); this.set({ accessKey });
}, },
async deriveProfileKeyVersionIfNeeded() { async deriveProfileKeyVersionIfNeeded() {

View file

@ -31,6 +31,7 @@
} }
loadProtoBufs('SignalService.proto'); loadProtoBufs('SignalService.proto');
loadProtoBufs('SignalStorage.proto');
loadProtoBufs('SubProtocol.proto'); loadProtoBufs('SubProtocol.proto');
loadProtoBufs('DeviceMessages.proto'); loadProtoBufs('DeviceMessages.proto');
loadProtoBufs('Stickers.proto'); loadProtoBufs('Stickers.proto');

View file

@ -191,6 +191,7 @@ function prepareURL(pathSegments, moreKeys) {
version: app.getVersion(), version: app.getVersion(),
buildExpiration: config.get('buildExpiration'), buildExpiration: config.get('buildExpiration'),
serverUrl: config.get('serverUrl'), serverUrl: config.get('serverUrl'),
storageUrl: config.get('storageUrl'),
cdnUrl0: config.get('cdn').get('0'), cdnUrl0: config.get('cdn').get('0'),
cdnUrl2: config.get('cdn').get('2'), cdnUrl2: config.get('cdn').get('2'),
certificateAuthority: config.get('certificateAuthority'), certificateAuthority: config.get('certificateAuthority'),

View file

@ -316,6 +316,7 @@ try {
window.WebAPI = window.textsecure.WebAPI.initialize({ window.WebAPI = window.textsecure.WebAPI.initialize({
url: config.serverUrl, url: config.serverUrl,
storageUrl: config.storageUrl,
cdnUrlObject: { cdnUrlObject: {
'0': config.cdnUrl0, '0': config.cdnUrl0,
'2': config.cdnUrl2, '2': config.cdnUrl2,

View file

@ -24,7 +24,6 @@ message Envelope {
optional bytes content = 8; // Contains an encrypted Content optional bytes content = 8; // Contains an encrypted Content
optional string serverGuid = 9; optional string serverGuid = 9;
optional uint64 serverTimestamp = 10; optional uint64 serverTimestamp = 10;
} }
message Content { message Content {
@ -185,10 +184,10 @@ message DataMessage {
} }
message Sticker { message Sticker {
optional bytes packId = 1; optional bytes packId = 1;
optional bytes packKey = 2; optional bytes packKey = 2;
optional uint32 stickerId = 3; optional uint32 stickerId = 3;
optional AttachmentPointer data = 4; optional AttachmentPointer data = 4;
} }
message Reaction { message Reaction {
@ -299,9 +298,9 @@ message SyncMessage {
} }
message Blocked { message Blocked {
repeated string numbers = 1; repeated string numbers = 1;
repeated string uuids = 3; repeated string uuids = 3;
repeated bytes groupIds = 2; repeated bytes groupIds = 2;
} }
message Request { message Request {
@ -311,11 +310,16 @@ message SyncMessage {
GROUPS = 2; GROUPS = 2;
BLOCKED = 3; BLOCKED = 3;
CONFIGURATION = 4; CONFIGURATION = 4;
KEYS = 5;
} }
optional Type type = 1; optional Type type = 1;
} }
message Keys {
optional bytes storageService = 1;
}
message Read { message Read {
optional string sender = 1; optional string sender = 1;
optional string senderUuid = 3; optional string senderUuid = 3;
@ -323,10 +327,11 @@ message SyncMessage {
} }
message Configuration { message Configuration {
optional bool readReceipts = 1; optional bool readReceipts = 1;
optional bool unidentifiedDeliveryIndicators = 2; optional bool unidentifiedDeliveryIndicators = 2;
optional bool typingIndicators = 3; optional bool typingIndicators = 3;
optional bool linkPreviews = 4; optional bool linkPreviews = 4;
optional uint32 provisioningVersion = 5;
} }
message StickerPackOperation { message StickerPackOperation {
@ -334,6 +339,7 @@ message SyncMessage {
INSTALL = 0; INSTALL = 0;
REMOVE = 1; REMOVE = 1;
} }
optional bytes packId = 1; optional bytes packId = 1;
optional bytes packKey = 2; optional bytes packKey = 2;
optional Type type = 3; optional Type type = 3;
@ -360,6 +366,16 @@ message SyncMessage {
optional Type type = 4; optional Type type = 4;
} }
message FetchLatest {
enum Type {
UNKNOWN = 0;
LOCAL_PROFILE = 1;
STORAGE_MANIFEST = 2;
}
optional Type type = 1;
}
optional Sent sent = 1; optional Sent sent = 1;
optional Contacts contacts = 2; optional Contacts contacts = 2;
optional Groups groups = 3; optional Groups groups = 3;
@ -371,6 +387,8 @@ message SyncMessage {
optional bytes padding = 8; optional bytes padding = 8;
repeated StickerPackOperation stickerPackOperation = 10; repeated StickerPackOperation stickerPackOperation = 10;
optional ViewOnceOpen viewOnceOpen = 11; optional ViewOnceOpen viewOnceOpen = 11;
optional FetchLatest fetchLatest = 12;
optional Keys keys = 13;
optional MessageRequestResponse messageRequestResponse = 14; optional MessageRequestResponse messageRequestResponse = 14;
} }

102
protos/SignalStorage.proto Normal file
View file

@ -0,0 +1,102 @@
package signalservice;
option java_package = "org.whispersystems.signalservice.internal.storage";
option java_outer_classname = "SignalStorageProtos";
message StorageManifest {
optional uint64 version = 1;
optional bytes value = 2;
}
message StorageItem {
optional bytes key = 1;
optional bytes value = 2;
}
message StorageItems {
repeated StorageItem items = 1;
}
message ReadOperation {
repeated bytes readKey = 1;
}
message WriteOperation {
optional StorageManifest manifest = 1;
repeated StorageItem insertItem = 2;
repeated bytes deleteKey = 3;
optional bool clearAll = 4;
}
message ManifestRecord {
message Identifier {
enum Type {
UNKNOWN = 0;
CONTACT = 1;
GROUPV1 = 2;
GROUPV2 = 3;
ACCOUNT = 4;
}
optional bytes raw = 1;
optional Type type = 2;
}
optional uint64 version = 1;
repeated Identifier keys = 2;
}
message StorageRecord {
oneof record {
ContactRecord contact = 1;
GroupV1Record groupV1 = 2;
GroupV2Record groupV2 = 3;
AccountRecord account = 4;
}
}
message ContactRecord {
enum IdentityState {
DEFAULT = 0;
VERIFIED = 1;
UNVERIFIED = 2;
}
optional string serviceUuid = 1;
optional string serviceE164 = 2;
optional bytes profileKey = 3;
optional bytes identityKey = 4;
optional IdentityState identityState = 5;
optional string givenName = 6;
optional string familyName = 7;
optional string username = 8;
optional bool blocked = 9;
optional bool whitelisted = 10;
optional bool archived = 11;
}
message GroupV1Record {
optional bytes id = 1;
optional bool blocked = 2;
optional bool whitelisted = 3;
optional bool archived = 4;
}
message GroupV2Record {
optional bytes masterKey = 1;
optional bool blocked = 2;
optional bool whitelisted = 3;
optional bool archived = 4;
}
message AccountRecord {
optional bytes profileKey = 1;
optional string givenName = 2;
optional string familyName = 3;
optional string avatarUrl = 4;
optional bool noteToSelfArchived = 5;
optional bool readReceipts = 6;
optional bool sealedSenderIndicators = 7;
optional bool typingIndicators = 8;
optional bool linkPreviews = 9;
}

View file

@ -34,6 +34,7 @@ const { initialize: initializeWebAPI } = require('../ts/textsecure/WebAPI');
const WebAPI = initializeWebAPI({ const WebAPI = initializeWebAPI({
url: config.serverUrl, url: config.serverUrl,
storageUrl: config.storageUrl,
cdnUrlObject: { cdnUrlObject: {
'0': config.cdnUrl0, '0': config.cdnUrl0,
'2': config.cdnUrl2, '2': config.cdnUrl2,

View file

@ -184,6 +184,20 @@ export async function decryptFile(
return decryptSymmetric(key, ciphertext); return decryptSymmetric(key, ciphertext);
} }
export async function deriveStorageManifestKey(
storageServiceKey: ArrayBuffer,
version: number
) {
return hmacSha256(storageServiceKey, bytesFromString(`Manifest_${version}`));
}
export async function deriveStorageItemKey(
storageServiceKey: ArrayBuffer,
itemID: string
) {
return hmacSha256(storageServiceKey, bytesFromString(`Item_${itemID}`));
}
export async function deriveAccessKey(profileKey: ArrayBuffer) { export async function deriveAccessKey(profileKey: ArrayBuffer) {
const iv = getZeroes(12); const iv = getZeroes(12);
const plaintext = getZeroes(16); const plaintext = getZeroes(16);

201
ts/textsecure.d.ts vendored
View file

@ -25,6 +25,16 @@ export type UnprocessedType = {
version: number; version: number;
}; };
export type StorageServiceCallOptionsType = {
credentials?: StorageServiceCredentials;
greaterThanVersion?: string;
};
export type StorageServiceCredentials = {
username: string;
password: string;
};
export type TextSecureType = { export type TextSecureType = {
createTaskWithTimeout: ( createTaskWithTimeout: (
task: () => Promise<any>, task: () => Promise<any>,
@ -70,6 +80,14 @@ export type TextSecureType = {
) => Promise<DownloadAttachmentType>; ) => Promise<DownloadAttachmentType>;
}; };
messaging: { messaging: {
getStorageCredentials: () => Promise<StorageServiceCredentials>;
getStorageManifest: (
options: StorageServiceCallOptionsType
) => Promise<ArrayBuffer>;
getStorageRecords: (
data: ArrayBuffer,
options: StorageServiceCallOptionsType
) => Promise<ArrayBuffer>;
sendStickerPackSync: ( sendStickerPackSync: (
operations: Array<{ operations: Array<{
packId: string; packId: string;
@ -125,18 +143,42 @@ export type StorageProtocolType = StorageType & {
keyPair: KeyPairType, keyPair: KeyPairType,
confirmed?: boolean confirmed?: boolean
) => Promise<void>; ) => Promise<void>;
loadIdentityKey: (identifier: string) => Promise<ArrayBuffer | undefined>;
loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>; loadSignedPreKeys: () => Promise<Array<StoredSignedPreKeyType>>;
processVerifiedMessage: (
identifier: string,
verifiedStatus: number,
publicKey: ArrayBuffer
) => Promise<boolean>;
saveIdentityWithAttributes: ( saveIdentityWithAttributes: (
number: string, number: string,
options: IdentityKeyRecord options: IdentityKeyRecord
) => Promise<void>; ) => Promise<void>;
setVerified: (
encodedAddress: string,
verifiedStatus: number,
publicKey?: ArrayBuffer
) => Promise<void>;
removeSignedPreKey: (keyId: number) => Promise<void>; removeSignedPreKey: (keyId: number) => Promise<void>;
removeAllData: () => Promise<void>; removeAllData: () => Promise<void>;
}; };
// Protobufs // Protobufs
type ProtobufCollectionType = { type StorageServiceProtobufTypes = {
AccountRecord: typeof AccountRecordClass;
ContactRecord: typeof ContactRecordClass;
GroupV1Record: typeof GroupV1RecordClass;
GroupV2Record: typeof GroupV2RecordClass;
ManifestRecord: typeof ManifestRecordClass;
ReadOperation: typeof ReadOperation;
StorageItem: typeof StorageItemClass;
StorageItems: typeof StorageItemsClass;
StorageManifest: typeof StorageManifest;
StorageRecord: typeof StorageRecordClass;
};
type ProtobufCollectionType = StorageServiceProtobufTypes & {
AttachmentPointer: typeof AttachmentPointerClass; AttachmentPointer: typeof AttachmentPointerClass;
ContactDetails: typeof ContactDetailsClass; ContactDetails: typeof ContactDetailsClass;
Content: typeof ContentClass; Content: typeof ContentClass;
@ -458,6 +500,33 @@ export declare namespace GroupDetailsClass {
} }
} }
declare enum ManifestType {
UNKNOWN,
CONTACT,
GROUPV1,
GROUPV2,
ACCOUNT,
}
type ManifestRecordIdentifier = {
raw: ProtoBinaryType;
type: ManifestType;
};
export declare class ManifestRecordClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => ManifestRecordClass;
static Identifier: {
Type: typeof ManifestType;
};
version: ProtoBigNumberType;
keys: ManifestRecordIdentifier[];
}
export declare class NullMessageClass { export declare class NullMessageClass {
static decode: ( static decode: (
data: ArrayBuffer | ByteBufferClass, data: ArrayBuffer | ByteBufferClass,
@ -526,6 +595,127 @@ export declare namespace ReceiptMessageClass {
} }
} }
// Storage Service related types
declare class StorageManifest {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => StorageManifest;
version?: ProtoBigNumberType | null;
value?: ByteBufferClass | null;
}
export declare class StorageRecordClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => StorageRecordClass;
contact?: ContactRecordClass | null;
groupV1?: GroupV1RecordClass | null;
groupV2?: GroupV2RecordClass | null;
account?: AccountRecordClass | null;
}
export declare class StorageItemClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => StorageItemClass;
key?: ByteBufferClass | null;
value?: ByteBufferClass | null;
}
export declare class StorageItemsClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => StorageItemsClass;
items?: StorageItemClass[] | null;
}
export declare enum ContactRecordIdentityState {
DEFAULT = 0,
VERIFIED = 1,
UNVERIFIED = 2,
}
export declare class ContactRecordClass {
static IdentityState: typeof ContactRecordIdentityState;
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => ContactRecordClass;
serviceUuid?: string | null;
serviceE164?: string | null;
profileKey?: ByteBufferClass | null;
identityKey?: ByteBufferClass | null;
identityState?: ContactRecordIdentityState | null;
givenName?: string | null;
familyName?: string | null;
username?: string | null;
blocked?: boolean | null;
whitelisted?: boolean | null;
archived?: boolean | null;
}
export declare class GroupV1RecordClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => GroupV1RecordClass;
id?: ByteBufferClass | null;
blocked?: boolean | null;
whitelisted?: boolean | null;
archived?: boolean | null;
}
export declare class GroupV2RecordClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => GroupV2RecordClass;
masterKey?: ByteBufferClass | null;
blocked?: boolean | null;
whitelisted?: boolean | null;
archived?: boolean | null;
}
export declare class AccountRecordClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => AccountRecordClass;
profileKey?: ByteBufferClass | null;
givenName?: string | null;
familyName?: string | null;
avatarUrl?: string | null;
noteToSelfArchived?: boolean | null;
readReceipts?: boolean | null;
sealedSenderIndicators?: boolean | null;
typingIndicators?: boolean | null;
linkPreviews?: boolean | null;
}
declare class ReadOperation {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => ReadOperation;
readKey: ArrayBuffer[] | ByteBufferClass[];
toArrayBuffer: () => ArrayBuffer;
}
export declare class SyncMessageClass { export declare class SyncMessageClass {
static decode: ( static decode: (
data: ArrayBuffer | ByteBufferClass, data: ArrayBuffer | ByteBufferClass,
@ -544,6 +734,8 @@ export declare class SyncMessageClass {
stickerPackOperation?: Array<SyncMessageClass.StickerPackOperation>; stickerPackOperation?: Array<SyncMessageClass.StickerPackOperation>;
viewOnceOpen?: SyncMessageClass.ViewOnceOpen; viewOnceOpen?: SyncMessageClass.ViewOnceOpen;
messageRequestResponse?: SyncMessageClass.MessageRequestResponse; messageRequestResponse?: SyncMessageClass.MessageRequestResponse;
fetchLatest?: SyncMessageClass.FetchLatest;
keys?: SyncMessageClass.Keys;
} }
// Note: we need to use namespaces to express nested classes in Typescript // Note: we need to use namespaces to express nested classes in Typescript
@ -595,6 +787,12 @@ export declare namespace SyncMessageClass {
senderUuid?: string; senderUuid?: string;
timestamp?: ProtoBinaryType; timestamp?: ProtoBinaryType;
} }
class FetchLatest {
type?: number;
}
class Keys {
storageService?: ByteBufferClass;
}
class MessageRequestResponse { class MessageRequestResponse {
threadE164?: string; threadE164?: string;
@ -612,6 +810,7 @@ export declare namespace SyncMessageClass.Request {
static CONFIGURATION: number; static CONFIGURATION: number;
static CONTACTS: number; static CONTACTS: number;
static GROUPS: number; static GROUPS: number;
static KEYS: number;
} }
} }

View file

@ -42,6 +42,7 @@ declare global {
data?: any; data?: any;
deliveryReceipt?: any; deliveryReceipt?: any;
error?: any; error?: any;
eventType?: string | number;
groupDetails?: any; groupDetails?: any;
groupId?: string; groupId?: string;
messageRequestResponseType?: number; messageRequestResponseType?: number;
@ -56,6 +57,7 @@ declare global {
stickerPacks?: any; stickerPacks?: any;
threadE164?: string; threadE164?: string;
threadUuid?: string; threadUuid?: string;
storageServiceKey?: ArrayBuffer;
timestamp?: any; timestamp?: any;
typing?: any; typing?: any;
verified?: any; verified?: any;
@ -1437,6 +1439,10 @@ class MessageReceiverInner extends EventTarget {
envelope, envelope,
syncMessage.messageRequestResponse syncMessage.messageRequestResponse
); );
} else if (syncMessage.fetchLatest) {
return this.handleFetchLatest(envelope, syncMessage.fetchLatest);
} else if (syncMessage.keys) {
return this.handleKeys(envelope, syncMessage.keys);
} }
this.removeFromCache(envelope); this.removeFromCache(envelope);
@ -1490,6 +1496,29 @@ class MessageReceiverInner extends EventTarget {
['threadUuid'], ['threadUuid'],
'MessageReceiver::handleMessageRequestResponse' 'MessageReceiver::handleMessageRequestResponse'
); );
}
async handleFetchLatest(
envelope: EnvelopeClass,
sync: SyncMessageClass.FetchLatest
) {
window.log.info('got fetch latest sync message');
const ev = new Event('fetchLatest');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.eventType = sync.type;
return this.dispatchAndWait(ev);
}
async handleKeys(envelope: EnvelopeClass, sync: SyncMessageClass.Keys) {
window.log.info('got keys sync message');
if (!sync.storageService) {
return;
}
const ev = new Event('keys');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.storageServiceKey = sync.storageService.toArrayBuffer();
return this.dispatchAndWait(ev); return this.dispatchAndWait(ev);
} }

View file

@ -12,6 +12,8 @@ import {
CallingMessageClass, CallingMessageClass,
ContentClass, ContentClass,
DataMessageClass, DataMessageClass,
StorageServiceCallOptionsType,
StorageServiceCredentials,
} from '../textsecure.d'; } from '../textsecure.d';
import { MessageError, SignedPreKeyRotationError } from './Errors'; import { MessageError, SignedPreKeyRotationError } from './Errors';
@ -810,6 +812,33 @@ export default class MessageSender {
return Promise.resolve(); return Promise.resolve();
} }
async sendRequestKeySyncMessage(options: SendOptionsType) {
const myUuid = window.textsecure.storage.user.getUuid();
const myNumber = window.textsecure.storage.user.getNumber();
const myDevice = window.textsecure.storage.user.getDeviceId();
if (myDevice === 1 || myDevice === '1') {
return;
}
const request = new window.textsecure.protobuf.SyncMessage.Request();
request.type = window.textsecure.protobuf.SyncMessage.Request.Type.KEYS;
const syncMessage = this.createSyncMessage();
syncMessage.request = request;
const contentMessage = new window.textsecure.protobuf.Content();
contentMessage.syncMessage = syncMessage;
const silent = true;
await this.sendIndividualProto(
myUuid || myNumber,
contentMessage,
Date.now(),
silent,
options
);
}
async sendTypingMessage( async sendTypingMessage(
options: { options: {
recipientId: string; recipientId: string;
@ -1630,4 +1659,21 @@ export default class MessageSender {
async makeProxiedRequest(url: string, options?: ProxiedRequestOptionsType) { async makeProxiedRequest(url: string, options?: ProxiedRequestOptionsType) {
return this.server.makeProxiedRequest(url, options); return this.server.makeProxiedRequest(url, options);
} }
async getStorageCredentials(): Promise<StorageServiceCredentials> {
return this.server.getStorageCredentials();
}
async getStorageManifest(
options: StorageServiceCallOptionsType
): Promise<ArrayBuffer> {
return this.server.getStorageManifest(options);
}
async getStorageRecords(
data: ArrayBuffer,
options: StorageServiceCallOptionsType
): Promise<ArrayBuffer> {
return this.server.getStorageRecords(data, options);
}
} }

View file

@ -10,6 +10,12 @@ import { getRandomValue } from '../Crypto';
import PQueue from 'p-queue'; import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid'; import { v4 as getGuid } from 'uuid';
import {
StorageServiceCallOptionsType,
StorageServiceCredentials,
TextSecureType,
} from '../textsecure.d';
// tslint:disable no-bitwise // tslint:disable no-bitwise
function _btoa(str: any) { function _btoa(str: any) {
@ -196,6 +202,7 @@ function getContentType(response: Response) {
} }
type HeaderListType = { [name: string]: string }; type HeaderListType = { [name: string]: string };
type HTTPCodeType = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type PromiseAjaxOptionsType = { type PromiseAjaxOptionsType = {
accessKey?: string; accessKey?: string;
@ -212,7 +219,7 @@ type PromiseAjaxOptionsType = {
responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails'; responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails';
stack?: string; stack?: string;
timeout?: number; timeout?: number;
type: 'GET' | 'POST' | 'PUT' | 'DELETE'; type: HTTPCodeType;
unauthenticated?: boolean; unauthenticated?: boolean;
user?: string; user?: string;
validateResponse?: any; validateResponse?: any;
@ -479,13 +486,17 @@ const URL_CALLS = {
getIceServers: 'v1/accounts/turn', getIceServers: 'v1/accounts/turn',
attachmentId: 'v2/attachments/form/upload', attachmentId: 'v2/attachments/form/upload',
deliveryCert: 'v1/certificate/delivery', deliveryCert: 'v1/certificate/delivery',
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
registerCapabilities: 'v1/devices/capabilities',
devices: 'v1/devices', devices: 'v1/devices',
keys: 'v2/keys', keys: 'v2/keys',
messages: 'v1/messages', messages: 'v1/messages',
profile: 'v1/profile', profile: 'v1/profile',
registerCapabilities: 'v1/devices/capabilities',
signed: 'v2/keys/signed', signed: 'v2/keys/signed',
storageManifest: 'v1/storage/manifest',
storageModify: 'v1/storage/',
storageRead: 'v1/storage/read',
storageToken: 'v1/storage/auth',
supportUnauthenticatedDelivery: 'v1/devices/unauthenticated_delivery',
getStickerPackUpload: 'v1/sticker/pack/form', getStickerPackUpload: 'v1/sticker/pack/form',
whoami: 'v1/accounts/whoami', whoami: 'v1/accounts/whoami',
config: 'v1/config', config: 'v1/config',
@ -493,6 +504,7 @@ const URL_CALLS = {
type InitializeOptionsType = { type InitializeOptionsType = {
url: string; url: string;
storageUrl: string;
cdnUrlObject: { cdnUrlObject: {
readonly '0': string; readonly '0': string;
readonly [propName: string]: string; readonly [propName: string]: string;
@ -513,12 +525,18 @@ type MessageType = any;
type AjaxOptionsType = { type AjaxOptionsType = {
accessKey?: string; accessKey?: string;
call: keyof typeof URL_CALLS; call: keyof typeof URL_CALLS;
httpType: 'GET' | 'POST' | 'PUT' | 'DELETE'; contentType?: string;
data?: ArrayBuffer | Buffer | string;
host?: string;
httpType: HTTPCodeType;
jsonData?: any; jsonData?: any;
password?: string;
responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails'; responseType?: 'json' | 'arraybuffer' | 'arraybufferwithdetails';
schema?: any;
timeout?: number; timeout?: number;
unauthenticated?: boolean; unauthenticated?: boolean;
urlParameters?: string; urlParameters?: string;
username?: string;
validateResponse?: any; validateResponse?: any;
}; };
@ -571,6 +589,9 @@ export type WebAPIType = {
getSenderCertificate: (withUuid?: boolean) => Promise<any>; getSenderCertificate: (withUuid?: boolean) => Promise<any>;
getSticker: (packId: string, stickerId: string) => Promise<any>; getSticker: (packId: string, stickerId: string) => Promise<any>;
getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>; getStickerPackManifest: (packId: string) => Promise<StickerPackManifestType>;
getStorageCredentials: TextSecureType['messaging']['getStorageCredentials'];
getStorageManifest: TextSecureType['messaging']['getStorageManifest'];
getStorageRecords: TextSecureType['messaging']['getStorageRecords'];
makeProxiedRequest: ( makeProxiedRequest: (
targetUrl: string, targetUrl: string,
options?: ProxiedRequestOptionsType options?: ProxiedRequestOptionsType
@ -650,6 +671,7 @@ export type ProxiedRequestOptionsType = {
// tslint:disable-next-line max-func-body-length // tslint:disable-next-line max-func-body-length
export function initialize({ export function initialize({
url, url,
storageUrl,
cdnUrlObject, cdnUrlObject,
certificateAuthority, certificateAuthority,
contentProxyUrl, contentProxyUrl,
@ -659,6 +681,9 @@ export function initialize({
if (!is.string(url)) { if (!is.string(url)) {
throw new Error('WebAPI.initialize: Invalid server url'); throw new Error('WebAPI.initialize: Invalid server url');
} }
if (!is.string(storageUrl)) {
throw new Error('WebAPI.initialize: Invalid storageUrl');
}
if (!is.object(cdnUrlObject)) { if (!is.object(cdnUrlObject)) {
throw new Error('WebAPI.initialize: Invalid cdnUrlObject'); throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
} }
@ -713,6 +738,9 @@ export function initialize({
getSenderCertificate, getSenderCertificate,
getSticker, getSticker,
getStickerPackManifest, getStickerPackManifest,
getStorageCredentials,
getStorageManifest,
getStorageRecords,
makeProxiedRequest, makeProxiedRequest,
putAttachment, putAttachment,
registerCapabilities, registerCapabilities,
@ -737,16 +765,16 @@ export function initialize({
return _outerAjax(null, { return _outerAjax(null, {
certificateAuthority, certificateAuthority,
contentType: 'application/json; charset=utf-8', contentType: param.contentType || 'application/json; charset=utf-8',
data: param.jsonData && _jsonThing(param.jsonData), data: param.data || (param.jsonData && _jsonThing(param.jsonData)),
host: url, host: param.host || url,
password, password: param.password || password,
path: URL_CALLS[param.call] + param.urlParameters, path: URL_CALLS[param.call] + param.urlParameters,
proxyUrl, proxyUrl,
responseType: param.responseType, responseType: param.responseType,
timeout: param.timeout, timeout: param.timeout,
type: param.httpType, type: param.httpType,
user: username, user: param.username || username,
validateResponse: param.validateResponse, validateResponse: param.validateResponse,
version, version,
unauthenticated: param.unauthenticated, unauthenticated: param.unauthenticated,
@ -821,6 +849,50 @@ export function initialize({
}); });
} }
async function getStorageCredentials(): Promise<StorageServiceCredentials> {
return _ajax({
call: 'storageToken',
httpType: 'GET',
responseType: 'json',
schema: { username: 'string', password: 'string' },
});
}
async function getStorageManifest(
options: StorageServiceCallOptionsType = {}
): Promise<ArrayBuffer> {
const { credentials, greaterThanVersion } = options;
return _ajax({
call: 'storageManifest',
contentType: 'application/x-protobuf',
host: storageUrl,
httpType: 'GET',
responseType: 'arraybuffer',
urlParameters: greaterThanVersion
? `/version/${greaterThanVersion}`
: '',
...credentials,
});
}
async function getStorageRecords(
data: ArrayBuffer,
options: StorageServiceCallOptionsType = {}
): Promise<ArrayBuffer> {
const { credentials } = options;
return _ajax({
call: 'storageRead',
contentType: 'application/x-protobuf',
data,
host: storageUrl,
httpType: 'PUT',
responseType: 'arraybuffer',
...credentials,
});
}
async function registerSupportForUnauthenticatedDelivery() { async function registerSupportForUnauthenticatedDelivery() {
return _ajax({ return _ajax({
call: 'supportUnauthenticatedDelivery', call: 'supportUnauthenticatedDelivery',

View file

@ -15,6 +15,7 @@ import { isFileDangerous } from './isFileDangerous';
import { makeLookup } from './makeLookup'; import { makeLookup } from './makeLookup';
import { migrateColor } from './migrateColor'; import { migrateColor } from './migrateColor';
import { missingCaseError } from './missingCaseError'; import { missingCaseError } from './missingCaseError';
import { runStorageServiceSyncJob } from './storageService';
import * as zkgroup from './zkgroup'; import * as zkgroup from './zkgroup';
export { export {
@ -33,5 +34,6 @@ export {
migrateColor, migrateColor,
missingCaseError, missingCaseError,
Registration, Registration,
runStorageServiceSyncJob,
zkgroup, zkgroup,
}; };

View file

@ -223,7 +223,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "js/models/conversations.js", "path": "js/models/conversations.js",
"line": " await wrap(", "line": " await wrap(",
"lineNumber": 644, "lineNumber": 652,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-06-09T20:26:46.515Z" "updated": "2020-06-09T20:26:46.515Z"
}, },
@ -11799,7 +11799,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SendMessage.ts", "path": "ts/textsecure/SendMessage.ts",
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();", "line": " return window.dcodeIO.ByteBuffer.wrap(string, 'hex').toArrayBuffer();",
"lineNumber": 30, "lineNumber": 32,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-05-28T18:08:02.658Z" "updated": "2020-05-28T18:08:02.658Z"
}, },
@ -11807,7 +11807,7 @@
"rule": "jQuery-wrap(", "rule": "jQuery-wrap(",
"path": "ts/textsecure/SendMessage.ts", "path": "ts/textsecure/SendMessage.ts",
"line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();", "line": " return window.dcodeIO.ByteBuffer.wrap(string, 'base64').toArrayBuffer();",
"lineNumber": 33, "lineNumber": 35,
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-05-28T18:08:02.658Z" "updated": "2020-05-28T18:08:02.658Z"
}, },
@ -11875,4 +11875,4 @@
"reasonCategory": "falseMatch", "reasonCategory": "falseMatch",
"updated": "2020-04-05T23:45:16.746Z" "updated": "2020-04-05T23:45:16.746Z"
} }
] ]

428
ts/util/storageService.ts Normal file
View file

@ -0,0 +1,428 @@
/* tslint:disable no-backbone-get-set-outside-model */
import _ from 'lodash';
import PQueue from 'p-queue';
import Crypto from '../textsecure/Crypto';
import {
arrayBufferToBase64,
base64ToArrayBuffer,
constantTimeEqual,
deriveStorageItemKey,
deriveStorageManifestKey,
} from '../Crypto';
import {
AccountRecordClass,
ContactRecordClass,
GroupV1RecordClass,
ManifestRecordClass,
StorageItemClass,
} from '../textsecure.d';
import { ConversationType } from '../window.d';
function fromRecordVerified(verified: number): number {
const VERIFIED_ENUM = window.textsecure.storage.protocol.VerifiedStatus;
const STATE_ENUM = window.textsecure.protobuf.ContactRecord.IdentityState;
switch (verified) {
case STATE_ENUM.VERIFIED:
return VERIFIED_ENUM.VERIFIED;
case STATE_ENUM.UNVERIFIED:
return VERIFIED_ENUM.UNVERIFIED;
default:
return VERIFIED_ENUM.DEFAULT;
}
}
async function fetchManifest(manifestVersion: string) {
window.log.info('storageService.fetchManifest');
try {
const credentials = await window.textsecure.messaging.getStorageCredentials();
window.storage.put('storageCredentials', credentials);
const manifestBinary = await window.textsecure.messaging.getStorageManifest(
{
credentials,
greaterThanVersion: manifestVersion,
}
);
const encryptedManifest = window.textsecure.protobuf.StorageManifest.decode(
manifestBinary
);
// If we don't get a value we're assuming we're receiving a 204
// it would be nice to get an actual e.code 204 and check against that.
if (!encryptedManifest.value || !encryptedManifest.version) {
window.log.info('storageService.fetchManifest: nothing changed');
return;
}
const storageKeyBase64 = window.storage.get('storageKey');
const storageKey = base64ToArrayBuffer(storageKeyBase64);
const storageManifestKey = await deriveStorageManifestKey(
storageKey,
encryptedManifest.version.toNumber()
);
const decryptedManifest = await Crypto.decryptProfile(
encryptedManifest.value.toArrayBuffer(),
storageManifestKey
);
return window.textsecure.protobuf.ManifestRecord.decode(decryptedManifest);
} catch (err) {
window.log.error(`storageService.fetchManifest: ${err}`);
if (err.code === 404) {
// No manifest exists, we create one
return { version: 0, keys: [] };
} else if (err.code === 204) {
// noNewerManifest we're ok
return;
}
throw err;
}
}
async function mergeGroupV1Record(
storageID: string,
groupV1Record: GroupV1RecordClass
): Promise<void> {
window.log.info(`storageService.mergeGroupV1Record: merging ${storageID}`);
if (!groupV1Record.id) {
window.log.info(
`storageService.mergeGroupV1Record: no ID for ${storageID}`
);
return;
}
const conversation = await window.ConversationController.getOrCreateAndWait(
groupV1Record.id.toBinary(),
'group'
);
conversation.set({
isArchived: Boolean(groupV1Record.archived),
storageID,
});
window.Signal.Data.updateConversation(conversation.attributes);
window.log.info(`storageService.mergeGroupV1Record: merged ${storageID}`);
}
async function mergeContactRecord(
storageID: string,
contactRecord: ContactRecordClass
): Promise<void> {
window.normalizeUuids(
contactRecord,
['serviceUuid'],
'storageService.mergeContactRecord'
);
if (!contactRecord.serviceE164) {
window.log.info(
`storageService.mergeContactRecord: no E164 for ${storageID}, uuid: ${contactRecord.serviceUuid}. Dropping record`
);
return;
}
const id = contactRecord.serviceE164 || contactRecord.serviceUuid;
if (!id) {
window.log.info(
`storageService.mergeContactRecord: no ID for ${storageID}`
);
return;
}
window.log.info(`storageService.mergeContactRecord: merging ${storageID}`);
const conversation = await window.ConversationController.getOrCreateAndWait(
id,
'private'
);
if (contactRecord.blocked === true) {
window.storage.addBlockedNumber(conversation.id);
} else if (contactRecord.blocked === false) {
window.storage.removeBlockedNumber(conversation.id);
}
const verified = contactRecord.identityState
? fromRecordVerified(contactRecord.identityState)
: window.textsecure.storage.protocol.VerifiedStatus.DEFAULT;
conversation.set({
isArchived: Boolean(contactRecord.archived),
profileFamilyName: contactRecord.familyName,
profileKey: contactRecord.profileKey
? arrayBufferToBase64(contactRecord.profileKey.toArrayBuffer())
: null,
profileName: contactRecord.givenName,
profileSharing: Boolean(contactRecord.whitelisted),
storageID,
verified,
});
if (
contactRecord.serviceUuid &&
(!conversation.get('uuid') ||
conversation.get('uuid') !== contactRecord.serviceUuid)
) {
window.log.info(
`storageService.mergeContactRecord: updating UUID ${storageID}`
);
conversation.set({ uuid: contactRecord.serviceUuid });
}
if (contactRecord.serviceE164 && !conversation.get('e164')) {
window.log.info(
`storageService.mergeContactRecord: updating E164 ${storageID}`
);
conversation.set({ e164: contactRecord.serviceE164 });
}
const identityKey = await window.textsecure.storage.protocol.loadIdentityKey(
conversation.id
);
const identityKeyChanged =
identityKey && contactRecord.identityKey
? !constantTimeEqual(
identityKey,
contactRecord.identityKey.toArrayBuffer()
)
: false;
if (identityKeyChanged && contactRecord.identityKey) {
await window.textsecure.storage.protocol.processVerifiedMessage(
conversation.id,
verified,
contactRecord.identityKey.toArrayBuffer()
);
} else if (conversation.get('verified')) {
await window.textsecure.storage.protocol.setVerified(
conversation.id,
verified
);
}
window.Signal.Data.updateConversation(conversation.attributes);
window.log.info(`storageService.mergeContactRecord: merged ${storageID}`);
}
async function mergeAccountRecord(
storageID: string,
accountRecord: AccountRecordClass
): Promise<void> {
window.log.info(`storageService.mergeAccountRecord: merging ${storageID}`);
const {
profileKey,
linkPreviews,
readReceipts,
sealedSenderIndicators,
typingIndicators,
} = accountRecord;
window.storage.put('read-receipt-setting', readReceipts);
if (typeof sealedSenderIndicators === 'boolean') {
window.storage.put('sealedSenderIndicators', sealedSenderIndicators);
}
if (typeof typingIndicators === 'boolean') {
window.storage.put('typingIndicators', typingIndicators);
}
if (typeof linkPreviews === 'boolean') {
window.storage.put('linkPreviews', linkPreviews);
}
if (profileKey) {
window.storage.put('profileKey', profileKey.toArrayBuffer());
}
window.log.info(
`storageService.mergeAccountRecord: merged settings ${storageID}`
);
const ourID = window.ConversationController.getOurConversationId();
if (!ourID) {
return;
}
const conversation = await window.ConversationController.getOrCreateAndWait(
ourID,
'private'
);
conversation.set({
profileFamilyName: accountRecord.familyName,
profileKey: accountRecord.profileKey
? arrayBufferToBase64(accountRecord.profileKey.toArrayBuffer())
: null,
profileName: accountRecord.givenName,
storageID,
});
window.Signal.Data.updateConversation(conversation.attributes);
window.log.info(
`storageService.mergeAccountRecord: merged profile ${storageID}`
);
}
// tslint:disable-next-line max-func-body-length
async function processManifest(
manifest: ManifestRecordClass
): Promise<boolean> {
const credentials = window.storage.get('storageCredentials');
const storageKeyBase64 = window.storage.get('storageKey');
const storageKey = base64ToArrayBuffer(storageKeyBase64);
const remoteKeysTypeMap = new Map();
manifest.keys.forEach(key => {
remoteKeysTypeMap.set(
arrayBufferToBase64(key.raw.toArrayBuffer()),
key.type
);
});
const localKeys = window
.getConversations()
.map((conversation: ConversationType) => conversation.get('storageID'))
.filter(Boolean);
window.log.info(
`storageService.processManifest localKeys.length ${localKeys.length}`
);
const remoteKeys = Array.from(remoteKeysTypeMap.keys());
const remoteOnly = remoteKeys.filter(
(key: string) => !localKeys.includes(key)
);
window.log.info(
`storageService.processManifest remoteOnly.length ${remoteOnly.length}`
);
const readOperation = new window.textsecure.protobuf.ReadOperation();
readOperation.readKey = remoteOnly.map(base64ToArrayBuffer);
const storageItemsBuffer = await window.textsecure.messaging.getStorageRecords(
readOperation.toArrayBuffer(),
{
credentials,
}
);
const storageItems = window.textsecure.protobuf.StorageItems.decode(
storageItemsBuffer
);
if (!storageItems.items) {
return false;
}
const queue = new PQueue({ concurrency: 4 });
const mergedItems = storageItems.items.map(
(storageRecordWrapper: StorageItemClass) => async () => {
const { key, value: storageItemCiphertext } = storageRecordWrapper;
if (!key || !storageItemCiphertext) {
return;
}
const base64ItemID = arrayBufferToBase64(key.toArrayBuffer());
const storageItemKey = await deriveStorageItemKey(
storageKey,
base64ItemID
);
const storageItemPlaintext = await Crypto.decryptProfile(
storageItemCiphertext.toArrayBuffer(),
storageItemKey
);
const storageRecord = window.textsecure.protobuf.StorageRecord.decode(
storageItemPlaintext
);
const itemType = remoteKeysTypeMap.get(base64ItemID);
const ITEM_TYPE =
window.textsecure.protobuf.ManifestRecord.Identifier.Type;
try {
if (itemType === ITEM_TYPE.UNKNOWN) {
window.log.info('storageService.processManifest: Unknown item type');
} else if (itemType === ITEM_TYPE.CONTACT && storageRecord.contact) {
await mergeContactRecord(base64ItemID, storageRecord.contact);
} else if (itemType === ITEM_TYPE.GROUPV1 && storageRecord.groupV1) {
await mergeGroupV1Record(base64ItemID, storageRecord.groupV1);
} else if (itemType === ITEM_TYPE.GROUPV2 && storageRecord.groupV2) {
window.log.info(
'storageService.processManifest: Skipping GroupV2 item'
);
} else if (itemType === ITEM_TYPE.ACCOUNT && storageRecord.account) {
await mergeAccountRecord(base64ItemID, storageRecord.account);
}
} catch (err) {
window.log.error(
`storageService.processManifest: merging record failed ${base64ItemID}`
);
}
}
);
try {
await queue.addAll(mergedItems);
await queue.onEmpty();
return true;
} catch (err) {
window.log.error('storageService.processManifest: merging failed');
return false;
}
}
export async function runStorageServiceSyncJob() {
const localManifestVersion = '0'; // window.storage.get('manifestVersion') || 0;
let manifest;
try {
manifest = await fetchManifest(localManifestVersion);
// Guarding against no manifests being returned, everything should be ok
if (!manifest) {
return;
}
} catch (err) {
// We are supposed to retry here if it's a retryable error
window.log.error(
`storageService.runStorageServiceSyncJob: failed! ${
err && err.stack ? err.stack : String(err)
}`
);
return;
}
const version = manifest.version.toNumber();
window.log.info(
`runStorageServiceSyncJob: manifest versions - previous: ${localManifestVersion}, current: ${version}`
);
const shouldUpdateVersion = await processManifest(manifest);
if (shouldUpdateVersion) {
return;
window.storage.put('manifestVersion', version);
}
}

28
ts/window.d.ts vendored
View file

@ -6,12 +6,13 @@ import {
SignalProtocolAddressClass, SignalProtocolAddressClass,
StorageType, StorageType,
} from './libsignal.d'; } from './libsignal.d';
import { TextSecureType } from './textsecure.d'; import { ContactRecordIdentityState, TextSecureType } from './textsecure.d';
import { WebAPIConnectType } from './textsecure/WebAPI'; import { WebAPIConnectType } from './textsecure/WebAPI';
import { CallingClass, CallHistoryDetailsType } from './services/calling'; import { CallingClass, CallHistoryDetailsType } from './services/calling';
import * as Crypto from './Crypto'; import * as Crypto from './Crypto';
import { ColorType, LocalizerType } from './types/Util'; import { ColorType, LocalizerType } from './types/Util';
import { SendOptionsType } from './textsecure/SendMessage'; import { SendOptionsType } from './textsecure/SendMessage';
import Data from './sql/Client';
type TaskResultType = any; type TaskResultType = any;
@ -49,11 +50,15 @@ declare global {
put: (key: string, value: any) => void; put: (key: string, value: any) => void;
remove: (key: string) => void; remove: (key: string) => void;
get: <T = any>(key: string) => T | undefined; get: <T = any>(key: string) => T | undefined;
addBlockedNumber: (number: string) => void;
isBlocked: (number: string) => boolean;
removeBlockedNumber: (number: string) => void;
}; };
textsecure: TextSecureType; textsecure: TextSecureType;
Signal: { Signal: {
Crypto: typeof Crypto; Crypto: typeof Crypto;
Data: typeof Data;
Metadata: { Metadata: {
SecretSessionCipher: typeof SecretSessionCipherClass; SecretSessionCipher: typeof SecretSessionCipherClass;
createCertificateValidator: ( createCertificateValidator: (
@ -77,7 +82,25 @@ declare global {
} }
} }
export type ConversationAttributes = {
e164?: string | null;
isArchived?: boolean;
profileFamilyName?: string | null;
profileKey?: string | null;
profileName?: string | null;
profileSharing?: boolean;
name?: string;
storageID?: string;
uuid?: string | null;
verified?: number;
};
export type ConversationType = { export type ConversationType = {
attributes: ConversationAttributes;
fromRecordVerified: (
verified: ContactRecordIdentityState
) => ContactRecordIdentityState;
set: (props: Partial<ConversationAttributes>) => void;
updateE164: (e164?: string) => void; updateE164: (e164?: string) => void;
updateUuid: (uuid?: string) => void; updateUuid: (uuid?: string) => void;
id: string; id: string;
@ -109,6 +132,7 @@ export type ConversationControllerType = {
) => ConversationType; ) => ConversationType;
getConversationId: (identifier: string) => string | null; getConversationId: (identifier: string) => string | null;
ensureContactIds: (o: { e164?: string; uuid?: string }) => string; ensureContactIds: (o: { e164?: string; uuid?: string }) => string;
getOurConversationId: () => string | null;
prepareForSend: ( prepareForSend: (
id: string, id: string,
options: Object options: Object
@ -117,6 +141,7 @@ export type ConversationControllerType = {
sendOptions: Object; sendOptions: Object;
}; };
get: (identifier: string) => null | ConversationType; get: (identifier: string) => null | ConversationType;
map: (mapFn: (conversation: ConversationType) => any) => any;
}; };
export type DCodeIOType = { export type DCodeIOType = {
@ -161,6 +186,7 @@ export class ByteBufferClass {
static wrap: (value: any, type?: string) => ByteBufferClass; static wrap: (value: any, type?: string) => ByteBufferClass;
toString: (type: string) => string; toString: (type: string) => string;
toArrayBuffer: () => ArrayBuffer; toArrayBuffer: () => ArrayBuffer;
toBinary: () => string;
slice: (start: number, end?: number) => ByteBufferClass; slice: (start: number, end?: number) => ByteBufferClass;
append: (data: ArrayBuffer) => void; append: (data: ArrayBuffer) => void;
limit: number; limit: number;