Support for GV1 -> GV2 migration
This commit is contained in:
parent
a0baa3e03f
commit
2c69f2c367
32 changed files with 2626 additions and 341 deletions
|
@ -23,6 +23,7 @@ import WebSocketResource, {
|
|||
IncomingWebSocketRequest,
|
||||
} from './WebsocketResources';
|
||||
import Crypto from './Crypto';
|
||||
import { deriveMasterKeyFromGroupV1 } from '../Crypto';
|
||||
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||
import { IncomingIdentityKeyError } from './Errors';
|
||||
|
||||
|
@ -43,6 +44,8 @@ import { WebSocket } from './WebSocket';
|
|||
|
||||
import { deriveGroupFields, MASTER_KEY_LENGTH } from '../groups';
|
||||
|
||||
const GROUPV1_ID_LENGTH = 16;
|
||||
const GROUPV2_ID_LENGTH = 32;
|
||||
const RETRY_TIMEOUT = 2 * 60 * 1000;
|
||||
|
||||
declare global {
|
||||
|
@ -58,6 +61,7 @@ declare global {
|
|||
eventType?: string | number;
|
||||
groupDetails?: any;
|
||||
groupId?: string;
|
||||
groupV2Id?: string;
|
||||
messageRequestResponseType?: number | null;
|
||||
proto?: any;
|
||||
read?: any;
|
||||
|
@ -273,6 +277,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
delete this.socket.onclose;
|
||||
delete this.socket.onerror;
|
||||
delete this.socket.onopen;
|
||||
|
||||
this.socket = undefined;
|
||||
}
|
||||
|
||||
|
@ -1201,7 +1206,13 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
this.deriveGroupsV2Data(msg);
|
||||
if (this.isInvalidGroupData(msg, envelope)) {
|
||||
this.removeFromCache(envelope);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.deriveGroupV1Data(msg);
|
||||
this.deriveGroupV2Data(msg);
|
||||
|
||||
if (
|
||||
msg.flags &&
|
||||
|
@ -1377,7 +1388,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
return Promise.all(results);
|
||||
}
|
||||
|
||||
handleTypingMessage(
|
||||
async handleTypingMessage(
|
||||
envelope: EnvelopeClass,
|
||||
typingMessage: TypingMessageClass
|
||||
) {
|
||||
|
@ -1403,25 +1414,29 @@ class MessageReceiverInner extends EventTarget {
|
|||
ev.senderUuid = envelope.sourceUuid;
|
||||
ev.senderDevice = envelope.sourceDevice;
|
||||
|
||||
const groupIdBuffer = groupId ? groupId.toArrayBuffer() : null;
|
||||
|
||||
ev.typing = {
|
||||
typingMessage,
|
||||
timestamp: timestamp ? timestamp.toNumber() : Date.now(),
|
||||
groupId:
|
||||
groupIdBuffer && groupIdBuffer.byteLength <= 16
|
||||
? groupId.toString('binary')
|
||||
: null,
|
||||
groupV2Id:
|
||||
groupIdBuffer && groupIdBuffer.byteLength > 16
|
||||
? groupId.toString('base64')
|
||||
: null,
|
||||
started:
|
||||
action === window.textsecure.protobuf.TypingMessage.Action.STARTED,
|
||||
stopped:
|
||||
action === window.textsecure.protobuf.TypingMessage.Action.STOPPED,
|
||||
};
|
||||
|
||||
const groupIdBuffer = groupId ? groupId.toArrayBuffer() : null;
|
||||
|
||||
if (groupIdBuffer && groupIdBuffer.byteLength > 0) {
|
||||
if (groupIdBuffer.byteLength === GROUPV1_ID_LENGTH) {
|
||||
ev.typing.groupId = groupId.toString('binary');
|
||||
ev.typing.groupV2Id = await this.deriveGroupV2FromV1(groupIdBuffer);
|
||||
} else if (groupIdBuffer.byteLength === GROUPV2_ID_LENGTH) {
|
||||
ev.typing.groupV2Id = groupId.toString('base64');
|
||||
} else {
|
||||
window.log.error('handleTypingMessage: Received invalid groupId value');
|
||||
this.removeFromCache(envelope);
|
||||
}
|
||||
}
|
||||
|
||||
return this.dispatchEvent(ev);
|
||||
}
|
||||
|
||||
|
@ -1430,7 +1445,76 @@ class MessageReceiverInner extends EventTarget {
|
|||
this.removeFromCache(envelope);
|
||||
}
|
||||
|
||||
deriveGroupsV2Data(message: DataMessageClass) {
|
||||
isInvalidGroupData(
|
||||
message: DataMessageClass,
|
||||
envelope: EnvelopeClass
|
||||
): boolean {
|
||||
const { group, groupV2 } = message;
|
||||
|
||||
if (group) {
|
||||
const id = group.id.toArrayBuffer();
|
||||
const isInvalid = id.byteLength !== GROUPV1_ID_LENGTH;
|
||||
|
||||
if (isInvalid) {
|
||||
window.log.info(
|
||||
'isInvalidGroupData: invalid GroupV1 message from',
|
||||
this.getEnvelopeId(envelope)
|
||||
);
|
||||
}
|
||||
|
||||
return isInvalid;
|
||||
}
|
||||
|
||||
if (groupV2) {
|
||||
const masterKey = groupV2.masterKey.toArrayBuffer();
|
||||
const isInvalid = masterKey.byteLength !== MASTER_KEY_LENGTH;
|
||||
|
||||
if (isInvalid) {
|
||||
window.log.info(
|
||||
'isInvalidGroupData: invalid GroupV2 message from',
|
||||
this.getEnvelopeId(envelope)
|
||||
);
|
||||
}
|
||||
return isInvalid;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async deriveGroupV2FromV1(groupId: ArrayBuffer): Promise<string> {
|
||||
if (groupId.byteLength !== GROUPV1_ID_LENGTH) {
|
||||
throw new Error(
|
||||
`deriveGroupV2FromV1: had id with wrong byteLength: ${groupId.byteLength}`
|
||||
);
|
||||
}
|
||||
const masterKey = await deriveMasterKeyFromGroupV1(groupId);
|
||||
const data = deriveGroupFields(masterKey);
|
||||
|
||||
const toBase64 = MessageReceiverInner.arrayBufferToStringBase64;
|
||||
return toBase64(data.id);
|
||||
}
|
||||
|
||||
async deriveGroupV1Data(message: DataMessageClass) {
|
||||
const { group } = message;
|
||||
|
||||
if (!group) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!group.id) {
|
||||
throw new Error('deriveGroupV1Data: had falsey id');
|
||||
}
|
||||
|
||||
const id = group.id.toArrayBuffer();
|
||||
if (id.byteLength !== GROUPV1_ID_LENGTH) {
|
||||
throw new Error(
|
||||
`deriveGroupV1Data: had id with wrong byteLength: ${id.byteLength}`
|
||||
);
|
||||
}
|
||||
group.derivedGroupV2Id = await this.deriveGroupV2FromV1(id);
|
||||
}
|
||||
|
||||
deriveGroupV2Data(message: DataMessageClass) {
|
||||
const { groupV2 } = message;
|
||||
|
||||
if (!groupV2) {
|
||||
|
@ -1438,10 +1522,10 @@ class MessageReceiverInner extends EventTarget {
|
|||
}
|
||||
|
||||
if (!isNumber(groupV2.revision)) {
|
||||
throw new Error('deriveGroupsV2Data: revision was not a number');
|
||||
throw new Error('deriveGroupV2Data: revision was not a number');
|
||||
}
|
||||
if (!groupV2.masterKey) {
|
||||
throw new Error('deriveGroupsV2Data: had falsey masterKey');
|
||||
throw new Error('deriveGroupV2Data: had falsey masterKey');
|
||||
}
|
||||
|
||||
const toBase64 = MessageReceiverInner.arrayBufferToStringBase64;
|
||||
|
@ -1449,7 +1533,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
const length = masterKey.byteLength;
|
||||
if (length !== MASTER_KEY_LENGTH) {
|
||||
throw new Error(
|
||||
`deriveGroupsV2Data: masterKey had length ${length}, expected ${MASTER_KEY_LENGTH}`
|
||||
`deriveGroupV2Data: masterKey had length ${length}, expected ${MASTER_KEY_LENGTH}`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1522,7 +1606,13 @@ class MessageReceiverInner extends EventTarget {
|
|||
);
|
||||
}
|
||||
|
||||
this.deriveGroupsV2Data(sentMessage.message);
|
||||
if (this.isInvalidGroupData(sentMessage.message, envelope)) {
|
||||
this.removeFromCache(envelope);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
this.deriveGroupV1Data(sentMessage.message);
|
||||
this.deriveGroupV2Data(sentMessage.message);
|
||||
|
||||
window.log.info(
|
||||
'sent message to',
|
||||
|
@ -1630,14 +1720,32 @@ class MessageReceiverInner extends EventTarget {
|
|||
ev.confirm = this.removeFromCache.bind(this, envelope);
|
||||
ev.threadE164 = sync.threadE164;
|
||||
ev.threadUuid = sync.threadUuid;
|
||||
ev.groupId = sync.groupId ? sync.groupId.toString('binary') : null;
|
||||
ev.messageRequestResponseType = sync.type;
|
||||
|
||||
const idBuffer: ArrayBuffer = sync.groupId
|
||||
? sync.groupId.toArrayBuffer()
|
||||
: null;
|
||||
|
||||
if (idBuffer && idBuffer.byteLength > 0) {
|
||||
if (idBuffer.byteLength === GROUPV1_ID_LENGTH) {
|
||||
ev.groupId = sync.groupId.toString('binary');
|
||||
ev.groupV2Id = await this.deriveGroupV2FromV1(idBuffer);
|
||||
} else if (idBuffer.byteLength === GROUPV2_ID_LENGTH) {
|
||||
ev.groupV2Id = sync.groupId.toString('base64');
|
||||
} else {
|
||||
this.removeFromCache(envelope);
|
||||
window.log.error('Received message request with invalid groupId');
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
window.normalizeUuids(
|
||||
ev,
|
||||
['threadUuid'],
|
||||
'MessageReceiver::handleMessageRequestResponse'
|
||||
);
|
||||
|
||||
return this.dispatchAndWait(ev);
|
||||
}
|
||||
|
||||
async handleFetchLatest(
|
||||
|
|
|
@ -1705,6 +1705,20 @@ export default class MessageSender {
|
|||
return this.sendMessage(attrs, options);
|
||||
}
|
||||
|
||||
async createGroup(
|
||||
group: GroupClass,
|
||||
options: GroupCredentialsType
|
||||
): Promise<void> {
|
||||
return this.server.createGroup(group, options);
|
||||
}
|
||||
|
||||
async uploadGroupAvatar(
|
||||
avatar: ArrayBuffer,
|
||||
options: GroupCredentialsType
|
||||
): Promise<string> {
|
||||
return this.server.uploadGroupAvatar(avatar, options);
|
||||
}
|
||||
|
||||
async getGroup(options: GroupCredentialsType): Promise<GroupClass> {
|
||||
return this.server.getGroup(options);
|
||||
}
|
||||
|
|
|
@ -620,7 +620,7 @@ const URL_CALLS = {
|
|||
devices: 'v1/devices',
|
||||
directoryAuth: 'v1/directory/auth',
|
||||
discovery: 'v1/discovery',
|
||||
getGroupAvatarUpload: '/v1/groups/avatar/form',
|
||||
getGroupAvatarUpload: 'v1/groups/avatar/form',
|
||||
getGroupCredentials: 'v1/certificate/group',
|
||||
getIceServers: 'v1/accounts/turn',
|
||||
getStickerPackUpload: 'v1/sticker/pack/form',
|
||||
|
@ -689,6 +689,15 @@ export type WebAPIConnectType = {
|
|||
connect: (options: ConnectParametersType) => WebAPIType;
|
||||
};
|
||||
|
||||
export type CapabilitiesType = {
|
||||
gv2: boolean;
|
||||
'gv1-migration': boolean;
|
||||
};
|
||||
export type CapabilitiesUploadType = {
|
||||
'gv2-3': boolean;
|
||||
'gv1-migration': boolean;
|
||||
};
|
||||
|
||||
type StickerPackManifestType = any;
|
||||
|
||||
export type GroupCredentialType = {
|
||||
|
@ -796,7 +805,7 @@ export type WebAPIType = {
|
|||
) => Promise<GroupChangeClass>;
|
||||
modifyStorageRecords: MessageSender['modifyStorageRecords'];
|
||||
putAttachment: (encryptedBin: ArrayBuffer) => Promise<any>;
|
||||
registerCapabilities: (capabilities: Dictionary<boolean>) => Promise<void>;
|
||||
registerCapabilities: (capabilities: CapabilitiesUploadType) => Promise<void>;
|
||||
putStickers: (
|
||||
encryptedManifest: ArrayBuffer,
|
||||
encryptedStickers: Array<ArrayBuffer>,
|
||||
|
@ -1154,7 +1163,7 @@ export function initialize({
|
|||
});
|
||||
}
|
||||
|
||||
async function registerCapabilities(capabilities: Dictionary<boolean>) {
|
||||
async function registerCapabilities(capabilities: CapabilitiesUploadType) {
|
||||
return _ajax({
|
||||
call: 'registerCapabilities',
|
||||
httpType: 'PUT',
|
||||
|
@ -1280,11 +1289,14 @@ export function initialize({
|
|||
deviceName?: string | null,
|
||||
options: { accessKey?: ArrayBuffer } = {}
|
||||
) {
|
||||
const capabilities: CapabilitiesUploadType = {
|
||||
'gv2-3': true,
|
||||
'gv1-migration': true,
|
||||
};
|
||||
|
||||
const { accessKey } = options;
|
||||
const jsonData: any = {
|
||||
capabilities: {
|
||||
'gv2-3': true,
|
||||
},
|
||||
capabilities,
|
||||
fetchesMessages: true,
|
||||
name: deviceName || undefined,
|
||||
registrationId,
|
||||
|
@ -2010,9 +2022,10 @@ export function initialize({
|
|||
await _ajax({
|
||||
basicAuth,
|
||||
call: 'groups',
|
||||
httpType: 'PUT',
|
||||
contentType: 'application/x-protobuf',
|
||||
data,
|
||||
host: storageUrl,
|
||||
httpType: 'PUT',
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2027,10 +2040,10 @@ export function initialize({
|
|||
const response: ArrayBuffer = await _ajax({
|
||||
basicAuth,
|
||||
call: 'groups',
|
||||
httpType: 'GET',
|
||||
contentType: 'application/x-protobuf',
|
||||
responseType: 'arraybuffer',
|
||||
host: storageUrl,
|
||||
httpType: 'GET',
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
return window.textsecure.protobuf.Group.decode(response);
|
||||
|
@ -2049,11 +2062,11 @@ export function initialize({
|
|||
const response: ArrayBuffer = await _ajax({
|
||||
basicAuth,
|
||||
call: 'groups',
|
||||
httpType: 'PATCH',
|
||||
data,
|
||||
contentType: 'application/x-protobuf',
|
||||
responseType: 'arraybuffer',
|
||||
data,
|
||||
host: storageUrl,
|
||||
httpType: 'PATCH',
|
||||
responseType: 'arraybuffer',
|
||||
});
|
||||
|
||||
return window.textsecure.protobuf.GroupChange.decode(response);
|
||||
|
@ -2071,11 +2084,11 @@ export function initialize({
|
|||
const withDetails: ArrayBufferWithDetailsType = await _ajax({
|
||||
basicAuth,
|
||||
call: 'groupLog',
|
||||
urlParameters: `/${startVersion}`,
|
||||
httpType: 'GET',
|
||||
contentType: 'application/x-protobuf',
|
||||
responseType: 'arraybufferwithdetails',
|
||||
host: storageUrl,
|
||||
httpType: 'GET',
|
||||
responseType: 'arraybufferwithdetails',
|
||||
urlParameters: `/${startVersion}`,
|
||||
});
|
||||
const { data, response } = withDetails;
|
||||
const changes = window.textsecure.protobuf.GroupChanges.decode(data);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue