Allow requesting profile without profileKey
This commit is contained in:
parent
80e445389f
commit
4a00ea46bc
7 changed files with 354 additions and 208 deletions
7
ts/model-types.d.ts
vendored
7
ts/model-types.d.ts
vendored
|
@ -225,6 +225,11 @@ export type MessageAttributesType = {
|
||||||
|
|
||||||
export type ConversationAttributesTypeType = 'private' | 'group';
|
export type ConversationAttributesTypeType = 'private' | 'group';
|
||||||
|
|
||||||
|
export type ConversationLastProfileType = Readonly<{
|
||||||
|
profileKey: string;
|
||||||
|
profileKeyVersion: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type ConversationAttributesType = {
|
export type ConversationAttributesType = {
|
||||||
accessKey?: string | null;
|
accessKey?: string | null;
|
||||||
addedBy?: string;
|
addedBy?: string;
|
||||||
|
@ -262,7 +267,7 @@ export type ConversationAttributesType = {
|
||||||
path: string;
|
path: string;
|
||||||
};
|
};
|
||||||
profileKeyCredential?: string | null;
|
profileKeyCredential?: string | null;
|
||||||
profileKeyVersion?: string | null;
|
lastProfile?: ConversationLastProfileType;
|
||||||
quotedMessageId?: string | null;
|
quotedMessageId?: string | null;
|
||||||
sealedSender?: unknown;
|
sealedSender?: unknown;
|
||||||
sentMessageCount: number;
|
sentMessageCount: number;
|
||||||
|
|
|
@ -8,6 +8,7 @@ import PQueue from 'p-queue';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ConversationAttributesType,
|
ConversationAttributesType,
|
||||||
|
ConversationLastProfileType,
|
||||||
ConversationModelCollectionType,
|
ConversationModelCollectionType,
|
||||||
LastMessageStatus,
|
LastMessageStatus,
|
||||||
MessageAttributesType,
|
MessageAttributesType,
|
||||||
|
@ -145,6 +146,7 @@ const SEND_REPORTING_THRESHOLD_MS = 25;
|
||||||
const MESSAGE_LOAD_CHUNK_SIZE = 30;
|
const MESSAGE_LOAD_CHUNK_SIZE = 30;
|
||||||
|
|
||||||
const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([
|
const ATTRIBUTES_THAT_DONT_INVALIDATE_PROPS_CACHE = new Set([
|
||||||
|
'lastProfile',
|
||||||
'profileLastFetchedAt',
|
'profileLastFetchedAt',
|
||||||
'needsStorageServiceSync',
|
'needsStorageServiceSync',
|
||||||
'storageID',
|
'storageID',
|
||||||
|
@ -4575,20 +4577,16 @@ export class ConversationModel extends window.Backbone
|
||||||
return this._activeProfileFetch;
|
return this._activeProfileFetch;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setEncryptedProfileName(encryptedName: string): Promise<void> {
|
async setEncryptedProfileName(
|
||||||
|
encryptedName: string,
|
||||||
|
decryptionKey: Uint8Array
|
||||||
|
): Promise<void> {
|
||||||
if (!encryptedName) {
|
if (!encryptedName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const key = this.get('profileKey');
|
|
||||||
if (!key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// decode
|
|
||||||
const keyBuffer = Bytes.fromBase64(key);
|
|
||||||
|
|
||||||
// decrypt
|
// decrypt
|
||||||
const { given, family } = decryptProfileName(encryptedName, keyBuffer);
|
const { given, family } = decryptProfileName(encryptedName, decryptionKey);
|
||||||
|
|
||||||
// encode
|
// encode
|
||||||
const profileName = given ? Bytes.toString(given) : undefined;
|
const profileName = given ? Bytes.toString(given) : undefined;
|
||||||
|
@ -4617,7 +4615,10 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setProfileAvatar(avatarPath: undefined | null | string): Promise<void> {
|
async setProfileAvatar(
|
||||||
|
avatarPath: undefined | null | string,
|
||||||
|
decryptionKey: Uint8Array
|
||||||
|
): Promise<void> {
|
||||||
if (isMe(this.attributes)) {
|
if (isMe(this.attributes)) {
|
||||||
if (avatarPath) {
|
if (avatarPath) {
|
||||||
window.storage.put('avatarUrl', avatarPath);
|
window.storage.put('avatarUrl', avatarPath);
|
||||||
|
@ -4632,14 +4633,9 @@ export class ConversationModel extends window.Backbone
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatar = await window.textsecure.messaging.getAvatar(avatarPath);
|
const avatar = await window.textsecure.messaging.getAvatar(avatarPath);
|
||||||
const key = this.get('profileKey');
|
|
||||||
if (!key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const keyBuffer = Bytes.fromBase64(key);
|
|
||||||
|
|
||||||
// decrypt
|
// decrypt
|
||||||
const decrypted = decryptProfile(avatar, keyBuffer);
|
const decrypted = decryptProfile(avatar, decryptionKey);
|
||||||
|
|
||||||
// update the conversation avatar only if hash differs
|
// update the conversation avatar only if hash differs
|
||||||
if (decrypted) {
|
if (decrypted) {
|
||||||
|
@ -4666,10 +4662,6 @@ export class ConversationModel extends window.Backbone
|
||||||
`Setting sealedSender to UNKNOWN for conversation ${this.idForLogging()}`
|
`Setting sealedSender to UNKNOWN for conversation ${this.idForLogging()}`
|
||||||
);
|
);
|
||||||
this.set({
|
this.set({
|
||||||
about: undefined,
|
|
||||||
aboutEmoji: undefined,
|
|
||||||
profileAvatar: undefined,
|
|
||||||
profileKeyVersion: undefined,
|
|
||||||
profileKeyCredential: null,
|
profileKeyCredential: null,
|
||||||
accessKey: null,
|
accessKey: null,
|
||||||
sealedSender: SEALED_SENDER.UNKNOWN,
|
sealedSender: SEALED_SENDER.UNKNOWN,
|
||||||
|
@ -4685,10 +4677,7 @@ export class ConversationModel extends window.Backbone
|
||||||
this.captureChange('profileKey');
|
this.captureChange('profileKey');
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all([
|
this.deriveAccessKeyIfNeeded();
|
||||||
this.deriveAccessKeyIfNeeded(),
|
|
||||||
this.deriveProfileKeyVersionIfNeeded(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// We will update the conversation during storage service sync
|
// We will update the conversation during storage service sync
|
||||||
if (!viaStorageServiceSync) {
|
if (!viaStorageServiceSync) {
|
||||||
|
@ -4700,7 +4689,7 @@ export class ConversationModel extends window.Backbone
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async deriveAccessKeyIfNeeded(): Promise<void> {
|
deriveAccessKeyIfNeeded(): void {
|
||||||
const profileKey = this.get('profileKey');
|
const profileKey = this.get('profileKey');
|
||||||
if (!profileKey) {
|
if (!profileKey) {
|
||||||
return;
|
return;
|
||||||
|
@ -4715,29 +4704,90 @@ export class ConversationModel extends window.Backbone
|
||||||
this.set({ accessKey });
|
this.set({ accessKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
async deriveProfileKeyVersionIfNeeded(): Promise<void> {
|
deriveProfileKeyVersion(): string | undefined {
|
||||||
const profileKey = this.get('profileKey');
|
const profileKey = this.get('profileKey');
|
||||||
if (!profileKey) {
|
if (!profileKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uuid = this.get('uuid');
|
const uuid = this.get('uuid');
|
||||||
if (!uuid || this.get('profileKeyVersion')) {
|
if (!uuid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastProfile = this.get('lastProfile');
|
||||||
|
if (lastProfile?.profileKey === profileKey) {
|
||||||
|
return lastProfile.profileKeyVersion;
|
||||||
|
}
|
||||||
|
|
||||||
const profileKeyVersion = Util.zkgroup.deriveProfileKeyVersion(
|
const profileKeyVersion = Util.zkgroup.deriveProfileKeyVersion(
|
||||||
profileKey,
|
profileKey,
|
||||||
uuid
|
uuid
|
||||||
);
|
);
|
||||||
if (!profileKeyVersion) {
|
if (!profileKeyVersion) {
|
||||||
log.warn(
|
log.warn(
|
||||||
'deriveProfileKeyVersionIfNeeded: Failed to derive profile key version, clearing profile key.'
|
'deriveProfileKeyVersion: Failed to derive profile key version, ' +
|
||||||
|
'clearing profile key.'
|
||||||
);
|
);
|
||||||
this.setProfileKey(undefined);
|
this.setProfileKey(undefined);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.set({ profileKeyVersion });
|
return profileKeyVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateLastProfile(
|
||||||
|
oldValue: ConversationLastProfileType | undefined,
|
||||||
|
{ profileKey, profileKeyVersion }: ConversationLastProfileType
|
||||||
|
): Promise<void> {
|
||||||
|
const lastProfile = this.get('lastProfile');
|
||||||
|
|
||||||
|
// Atomic updates only
|
||||||
|
if (lastProfile !== oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
lastProfile?.profileKey === profileKey &&
|
||||||
|
lastProfile?.profileKeyVersion === profileKeyVersion
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(
|
||||||
|
'ConversationModel.updateLastProfile: updating for',
|
||||||
|
this.idForLogging()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.set({ lastProfile: { profileKey, profileKeyVersion } });
|
||||||
|
|
||||||
|
await window.Signal.Data.updateConversation(this.attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeLastProfile(
|
||||||
|
oldValue: ConversationLastProfileType | undefined
|
||||||
|
): Promise<void> {
|
||||||
|
// Atomic updates only
|
||||||
|
if (this.get('lastProfile') !== oldValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn(
|
||||||
|
'ConversationModel.removeLastProfile: called for',
|
||||||
|
this.idForLogging()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.set({
|
||||||
|
lastProfile: undefined,
|
||||||
|
|
||||||
|
// We don't have any knowledge of profile anymore. Drop all associated
|
||||||
|
// data.
|
||||||
|
about: undefined,
|
||||||
|
aboutEmoji: undefined,
|
||||||
|
profileAvatar: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
await window.Signal.Data.updateConversation(this.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasMember(identifier: string): boolean {
|
hasMember(identifier: string): boolean {
|
||||||
|
|
|
@ -1140,16 +1140,16 @@ export async function mergeAccountRecord(
|
||||||
});
|
});
|
||||||
|
|
||||||
let needsProfileFetch = false;
|
let needsProfileFetch = false;
|
||||||
if (accountRecord.profileKey && accountRecord.profileKey.length > 0) {
|
if (profileKey && profileKey.length > 0) {
|
||||||
needsProfileFetch = await conversation.setProfileKey(
|
needsProfileFetch = await conversation.setProfileKey(
|
||||||
Bytes.toBase64(accountRecord.profileKey),
|
Bytes.toBase64(profileKey),
|
||||||
{ viaStorageServiceSync: true }
|
{ viaStorageServiceSync: true }
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (avatarUrl) {
|
if (avatarUrl) {
|
||||||
await conversation.setProfileAvatar(avatarUrl);
|
await conversation.setProfileAvatar(avatarUrl, profileKey);
|
||||||
window.storage.put('avatarUrl', avatarUrl);
|
window.storage.put('avatarUrl', avatarUrl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
|
const { hasConflict, details: extraDetails } = doesRecordHavePendingChanges(
|
||||||
|
|
|
@ -44,7 +44,6 @@ describe('routineProfileRefresh', () => {
|
||||||
muteExpiresAt: 0,
|
muteExpiresAt: 0,
|
||||||
profileAvatar: undefined,
|
profileAvatar: undefined,
|
||||||
profileKeyCredential: UUID.generate().toString(),
|
profileKeyCredential: UUID.generate().toString(),
|
||||||
profileKeyVersion: '',
|
|
||||||
profileSharing: true,
|
profileSharing: true,
|
||||||
quotedMessageId: null,
|
quotedMessageId: null,
|
||||||
sealedSender: 1,
|
sealedSender: 1,
|
||||||
|
|
|
@ -29,6 +29,8 @@ import type { UUID, UUIDStringType } from '../types/UUID';
|
||||||
import type {
|
import type {
|
||||||
ChallengeType,
|
ChallengeType,
|
||||||
GetGroupLogOptionsType,
|
GetGroupLogOptionsType,
|
||||||
|
GetProfileOptionsType,
|
||||||
|
GetProfileUnauthOptionsType,
|
||||||
GroupCredentialsType,
|
GroupCredentialsType,
|
||||||
GroupLogResponseType,
|
GroupLogResponseType,
|
||||||
MultiRecipient200ResponseType,
|
MultiRecipient200ResponseType,
|
||||||
|
@ -1996,21 +1998,10 @@ export default class MessageSender {
|
||||||
|
|
||||||
async getProfile(
|
async getProfile(
|
||||||
uuid: UUID,
|
uuid: UUID,
|
||||||
options: Readonly<{
|
options: GetProfileOptionsType | GetProfileUnauthOptionsType
|
||||||
accessKey?: string;
|
|
||||||
profileKeyVersion: string;
|
|
||||||
profileKeyCredentialRequest?: string;
|
|
||||||
userLanguages: ReadonlyArray<string>;
|
|
||||||
}>
|
|
||||||
): ReturnType<WebAPIType['getProfile']> {
|
): ReturnType<WebAPIType['getProfile']> {
|
||||||
const { accessKey } = options;
|
if (options.accessKey !== undefined) {
|
||||||
|
return this.server.getProfileUnauth(uuid.toString(), options);
|
||||||
if (accessKey) {
|
|
||||||
const unauthOptions = {
|
|
||||||
...options,
|
|
||||||
accessKey,
|
|
||||||
};
|
|
||||||
return this.server.getProfileUnauth(uuid.toString(), unauthOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.server.getProfile(uuid.toString(), options);
|
return this.server.getProfile(uuid.toString(), options);
|
||||||
|
|
|
@ -778,6 +778,32 @@ export type GetUuidsForE164sV2OptionsType = Readonly<{
|
||||||
accessKeys: ReadonlyArray<string>;
|
accessKeys: ReadonlyArray<string>;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
type GetProfileCommonOptionsType = Readonly<
|
||||||
|
{
|
||||||
|
userLanguages: ReadonlyArray<string>;
|
||||||
|
credentialType?: 'pni' | 'profileKey';
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
profileKeyVersion?: undefined;
|
||||||
|
profileKeyCredentialRequest?: undefined;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
profileKeyVersion: string;
|
||||||
|
profileKeyCredentialRequest?: string;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type GetProfileOptionsType = GetProfileCommonOptionsType &
|
||||||
|
Readonly<{
|
||||||
|
accessKey?: undefined;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type GetProfileUnauthOptionsType = GetProfileCommonOptionsType &
|
||||||
|
Readonly<{
|
||||||
|
accessKey: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
export type WebAPIType = {
|
export type WebAPIType = {
|
||||||
startRegistration(): unknown;
|
startRegistration(): unknown;
|
||||||
finishRegistration(baton: unknown): void;
|
finishRegistration(baton: unknown): void;
|
||||||
|
@ -829,22 +855,12 @@ export type WebAPIType = {
|
||||||
getMyKeys: (uuidKind: UUIDKind) => Promise<number>;
|
getMyKeys: (uuidKind: UUIDKind) => Promise<number>;
|
||||||
getProfile: (
|
getProfile: (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
options: {
|
options: GetProfileOptionsType
|
||||||
profileKeyVersion: string;
|
|
||||||
profileKeyCredentialRequest?: string;
|
|
||||||
userLanguages: ReadonlyArray<string>;
|
|
||||||
credentialType?: 'pni' | 'profileKey';
|
|
||||||
}
|
|
||||||
) => Promise<ProfileType>;
|
) => Promise<ProfileType>;
|
||||||
getProfileForUsername: (username: string) => Promise<ProfileType>;
|
getProfileForUsername: (username: string) => Promise<ProfileType>;
|
||||||
getProfileUnauth: (
|
getProfileUnauth: (
|
||||||
identifier: string,
|
identifier: string,
|
||||||
options: {
|
options: GetProfileUnauthOptionsType
|
||||||
accessKey: string;
|
|
||||||
profileKeyVersion: string;
|
|
||||||
profileKeyCredentialRequest?: string;
|
|
||||||
userLanguages: ReadonlyArray<string>;
|
|
||||||
}
|
|
||||||
) => Promise<ProfileType>;
|
) => Promise<ProfileType>;
|
||||||
getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>;
|
getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>;
|
||||||
getProvisioningResource: (
|
getProvisioningResource: (
|
||||||
|
@ -1482,14 +1498,25 @@ export function initialize({
|
||||||
|
|
||||||
function getProfileUrl(
|
function getProfileUrl(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
profileKeyVersion: string,
|
{
|
||||||
profileKeyCredentialRequest?: string,
|
profileKeyVersion,
|
||||||
credentialType: 'pni' | 'profileKey' = 'profileKey'
|
profileKeyCredentialRequest,
|
||||||
|
credentialType = 'profileKey',
|
||||||
|
}: GetProfileCommonOptionsType
|
||||||
) {
|
) {
|
||||||
let profileUrl = `/${identifier}/${profileKeyVersion}`;
|
let profileUrl = `/${identifier}`;
|
||||||
|
if (profileKeyVersion !== undefined) {
|
||||||
if (profileKeyCredentialRequest) {
|
profileUrl += `/${profileKeyVersion}`;
|
||||||
profileUrl += `/${profileKeyCredentialRequest}?credentialType=${credentialType}`;
|
if (profileKeyCredentialRequest !== undefined) {
|
||||||
|
profileUrl +=
|
||||||
|
`/${profileKeyCredentialRequest}` +
|
||||||
|
`?credentialType=${credentialType}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
strictAssert(
|
||||||
|
profileKeyCredentialRequest === undefined,
|
||||||
|
'getProfileUrl called without version, but with request'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return profileUrl;
|
return profileUrl;
|
||||||
|
@ -1497,29 +1524,15 @@ export function initialize({
|
||||||
|
|
||||||
async function getProfile(
|
async function getProfile(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
options: {
|
options: GetProfileOptionsType
|
||||||
profileKeyVersion: string;
|
|
||||||
profileKeyCredentialRequest?: string;
|
|
||||||
userLanguages: ReadonlyArray<string>;
|
|
||||||
credentialType?: 'pni' | 'profileKey';
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
const {
|
const { profileKeyVersion, profileKeyCredentialRequest, userLanguages } =
|
||||||
profileKeyVersion,
|
options;
|
||||||
profileKeyCredentialRequest,
|
|
||||||
userLanguages,
|
|
||||||
credentialType = 'profileKey',
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
return (await _ajax({
|
return (await _ajax({
|
||||||
call: 'profile',
|
call: 'profile',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
urlParameters: getProfileUrl(
|
urlParameters: getProfileUrl(identifier, options),
|
||||||
identifier,
|
|
||||||
profileKeyVersion,
|
|
||||||
profileKeyCredentialRequest,
|
|
||||||
credentialType
|
|
||||||
),
|
|
||||||
headers: {
|
headers: {
|
||||||
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
|
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
|
||||||
},
|
},
|
||||||
|
@ -1561,12 +1574,7 @@ export function initialize({
|
||||||
|
|
||||||
async function getProfileUnauth(
|
async function getProfileUnauth(
|
||||||
identifier: string,
|
identifier: string,
|
||||||
options: {
|
options: GetProfileUnauthOptionsType
|
||||||
accessKey: string;
|
|
||||||
profileKeyVersion: string;
|
|
||||||
profileKeyCredentialRequest?: string;
|
|
||||||
userLanguages: ReadonlyArray<string>;
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
accessKey,
|
accessKey,
|
||||||
|
@ -1578,11 +1586,7 @@ export function initialize({
|
||||||
return (await _ajax({
|
return (await _ajax({
|
||||||
call: 'profile',
|
call: 'profile',
|
||||||
httpType: 'GET',
|
httpType: 'GET',
|
||||||
urlParameters: getProfileUrl(
|
urlParameters: getProfileUrl(identifier, options),
|
||||||
identifier,
|
|
||||||
profileKeyVersion,
|
|
||||||
profileKeyCredentialRequest
|
|
||||||
),
|
|
||||||
headers: {
|
headers: {
|
||||||
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
|
'Accept-Language': formatAcceptLanguageHeader(userLanguages),
|
||||||
},
|
},
|
||||||
|
|
|
@ -3,6 +3,12 @@
|
||||||
|
|
||||||
import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup';
|
import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup';
|
||||||
import { SEALED_SENDER } from '../types/SealedSender';
|
import { SEALED_SENDER } from '../types/SealedSender';
|
||||||
|
import * as Errors from '../types/errors';
|
||||||
|
import type {
|
||||||
|
GetProfileOptionsType,
|
||||||
|
GetProfileUnauthOptionsType,
|
||||||
|
} from '../textsecure/WebAPI';
|
||||||
|
import { HTTPError } from '../textsecure/Errors';
|
||||||
import { Address } from '../types/Address';
|
import { Address } from '../types/Address';
|
||||||
import { QualifiedAddress } from '../types/QualifiedAddress';
|
import { QualifiedAddress } from '../types/QualifiedAddress';
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
|
@ -12,36 +18,26 @@ import {
|
||||||
getClientZkProfileOperations,
|
getClientZkProfileOperations,
|
||||||
handleProfileKeyCredential,
|
handleProfileKeyCredential,
|
||||||
} from './zkgroup';
|
} from './zkgroup';
|
||||||
import { getSendOptions } from './getSendOptions';
|
|
||||||
import { isMe } from './whatTypeOfConversation';
|
import { isMe } from './whatTypeOfConversation';
|
||||||
|
import type { ConversationModel } from '../models/conversations';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import { getUserLanguages } from './userLanguages';
|
import { getUserLanguages } from './userLanguages';
|
||||||
import { parseBadgesFromServer } from '../badges/parseBadgesFromServer';
|
import { parseBadgesFromServer } from '../badges/parseBadgesFromServer';
|
||||||
|
import { strictAssert } from './assert';
|
||||||
|
|
||||||
export async function getProfile(
|
async function doGetProfile(c: ConversationModel): Promise<void> {
|
||||||
providedUuid?: string,
|
const idForLogging = c.idForLogging();
|
||||||
providedE164?: string
|
const { messaging } = window.textsecure;
|
||||||
): Promise<void> {
|
strictAssert(
|
||||||
if (!window.textsecure.messaging) {
|
messaging,
|
||||||
throw new Error(
|
'getProfile: window.textsecure.messaging not available'
|
||||||
'Conversation.getProfile: window.textsecure.messaging not available'
|
);
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { updatesUrl } = window.SignalContext.config;
|
const { updatesUrl } = window.SignalContext.config;
|
||||||
if (typeof updatesUrl !== 'string') {
|
strictAssert(
|
||||||
throw new Error('getProfile expected updatesUrl to be a defined string');
|
typeof updatesUrl === 'string',
|
||||||
}
|
'getProfile: expected updatesUrl to be a defined string'
|
||||||
|
);
|
||||||
const id = window.ConversationController.ensureContactIds({
|
|
||||||
uuid: providedUuid,
|
|
||||||
e164: providedE164,
|
|
||||||
});
|
|
||||||
const c = window.ConversationController.get(id);
|
|
||||||
if (!c) {
|
|
||||||
log.error('getProfile: failed to find conversation; doing nothing');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const clientZkProfileCipher = getClientZkProfileOperations(
|
const clientZkProfileCipher = getClientZkProfileOperations(
|
||||||
window.getServerPublicParams()
|
window.getServerPublicParams()
|
||||||
|
@ -54,27 +50,40 @@ export async function getProfile(
|
||||||
|
|
||||||
let profile;
|
let profile;
|
||||||
|
|
||||||
try {
|
c.deriveAccessKeyIfNeeded();
|
||||||
await Promise.all([
|
|
||||||
c.deriveAccessKeyIfNeeded(),
|
|
||||||
c.deriveProfileKeyVersionIfNeeded(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const profileKey = c.get('profileKey');
|
const profileKey = c.get('profileKey');
|
||||||
const uuid = c.getCheckedUuid('getProfile');
|
const profileKeyVersion = c.deriveProfileKeyVersion();
|
||||||
const profileKeyVersionHex = c.get('profileKeyVersion');
|
const uuid = c.getCheckedUuid('getProfile');
|
||||||
if (!profileKeyVersionHex) {
|
const existingProfileKeyCredential = c.get('profileKeyCredential');
|
||||||
throw new Error('No profile key version available');
|
const lastProfile = c.get('lastProfile');
|
||||||
}
|
|
||||||
const existingProfileKeyCredential = c.get('profileKeyCredential');
|
|
||||||
|
|
||||||
let profileKeyCredentialRequestHex: undefined | string;
|
let profileCredentialRequestContext:
|
||||||
let profileCredentialRequestContext:
|
| undefined
|
||||||
| undefined
|
| ProfileKeyCredentialRequestContext;
|
||||||
| ProfileKeyCredentialRequestContext;
|
|
||||||
|
|
||||||
if (profileKey && profileKeyVersionHex && !existingProfileKeyCredential) {
|
let getProfileOptions: GetProfileOptionsType | GetProfileUnauthOptionsType;
|
||||||
log.info('Generating request...');
|
|
||||||
|
let accessKey = c.get('accessKey');
|
||||||
|
if (profileKey) {
|
||||||
|
strictAssert(
|
||||||
|
profileKeyVersion && accessKey,
|
||||||
|
'profileKeyVersion and accessKey are derived from profileKey'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingProfileKeyCredential) {
|
||||||
|
getProfileOptions = {
|
||||||
|
accessKey,
|
||||||
|
profileKeyVersion,
|
||||||
|
userLanguages,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
log.info(
|
||||||
|
'getProfile: generating profile key credential request for ' +
|
||||||
|
`conversation ${idForLogging}`
|
||||||
|
);
|
||||||
|
|
||||||
|
let profileKeyCredentialRequestHex: undefined | string;
|
||||||
({
|
({
|
||||||
requestHex: profileKeyCredentialRequestHex,
|
requestHex: profileKeyCredentialRequestHex,
|
||||||
context: profileCredentialRequestContext,
|
context: profileCredentialRequestContext,
|
||||||
|
@ -83,40 +92,76 @@ export async function getProfile(
|
||||||
uuid.toString(),
|
uuid.toString(),
|
||||||
profileKey
|
profileKey
|
||||||
));
|
));
|
||||||
|
|
||||||
|
getProfileOptions = {
|
||||||
|
accessKey,
|
||||||
|
userLanguages,
|
||||||
|
profileKeyVersion,
|
||||||
|
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
strictAssert(
|
||||||
|
!accessKey,
|
||||||
|
'accessKey have to be absent because there is no profileKey'
|
||||||
|
);
|
||||||
|
|
||||||
const { sendMetadata = {} } = await getSendOptions(c.attributes);
|
if (lastProfile?.profileKeyVersion) {
|
||||||
const getInfo = sendMetadata[uuid.toString()] || {};
|
getProfileOptions = {
|
||||||
|
userLanguages,
|
||||||
|
profileKeyVersion: lastProfile.profileKeyVersion,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
getProfileOptions = { userLanguages };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (getInfo.accessKey) {
|
const isVersioned = Boolean(getProfileOptions.profileKeyVersion);
|
||||||
|
log.info(
|
||||||
|
`getProfile: getting ${isVersioned ? 'versioned' : 'unversioned'} ` +
|
||||||
|
`profile for conversation ${idForLogging}`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (getProfileOptions.accessKey) {
|
||||||
try {
|
try {
|
||||||
profile = await window.textsecure.messaging.getProfile(uuid, {
|
profile = await messaging.getProfile(uuid, getProfileOptions);
|
||||||
accessKey: getInfo.accessKey,
|
|
||||||
profileKeyVersion: profileKeyVersionHex,
|
|
||||||
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
|
||||||
userLanguages,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 401 || error.code === 403) {
|
if (!(error instanceof HTTPError)) {
|
||||||
log.info(
|
|
||||||
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
|
|
||||||
);
|
|
||||||
c.set({ sealedSender: SEALED_SENDER.DISABLED });
|
|
||||||
profile = await window.textsecure.messaging.getProfile(uuid, {
|
|
||||||
profileKeyVersion: profileKeyVersionHex,
|
|
||||||
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
|
||||||
userLanguages,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
if (error.code === 401 || error.code === 403) {
|
||||||
|
await c.setProfileKey(undefined);
|
||||||
|
|
||||||
|
// Retry fetch using last known profileKeyVersion or fetch
|
||||||
|
// unversioned profile.
|
||||||
|
return doGetProfile(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 404) {
|
||||||
|
await c.removeLastProfile(lastProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
profile = await window.textsecure.messaging.getProfile(uuid, {
|
try {
|
||||||
profileKeyVersion: profileKeyVersionHex,
|
// We won't get the credential, but lets either fetch:
|
||||||
profileKeyCredentialRequest: profileKeyCredentialRequestHex,
|
// - a versioned profile using last known profileKeyVersion
|
||||||
userLanguages,
|
// - some basic profile information (capabilities, badges, etc).
|
||||||
});
|
profile = await messaging.getProfile(uuid, getProfileOptions);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HTTPError && error.code === 404) {
|
||||||
|
log.info(`getProfile: failed to find a profile for ${idForLogging}`);
|
||||||
|
|
||||||
|
await c.removeLastProfile(lastProfile);
|
||||||
|
if (!isVersioned) {
|
||||||
|
log.info(`getProfile: marking ${idForLogging} as unregistered`);
|
||||||
|
c.setUnregistered();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.identityKey) {
|
if (profile.identityKey) {
|
||||||
|
@ -136,10 +181,14 @@ export async function getProfile(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessKey = c.get('accessKey');
|
// Update accessKey to prevent race conditions. Since we run asynchronous
|
||||||
|
// requests above - it is possible that someone updates or erases
|
||||||
|
// the profile key from under us.
|
||||||
|
accessKey = c.get('accessKey');
|
||||||
|
|
||||||
if (profile.unrestrictedUnidentifiedAccess && profile.unidentifiedAccess) {
|
if (profile.unrestrictedUnidentifiedAccess && profile.unidentifiedAccess) {
|
||||||
log.info(
|
log.info(
|
||||||
`Setting sealedSender to UNRESTRICTED for conversation ${c.idForLogging()}`
|
`getProfile: setting sealedSender to UNRESTRICTED for conversation ${idForLogging}`
|
||||||
);
|
);
|
||||||
c.set({
|
c.set({
|
||||||
sealedSender: SEALED_SENDER.UNRESTRICTED,
|
sealedSender: SEALED_SENDER.UNRESTRICTED,
|
||||||
|
@ -152,14 +201,14 @@ export async function getProfile(
|
||||||
|
|
||||||
if (haveCorrectKey) {
|
if (haveCorrectKey) {
|
||||||
log.info(
|
log.info(
|
||||||
`Setting sealedSender to ENABLED for conversation ${c.idForLogging()}`
|
`getProfile: setting sealedSender to ENABLED for conversation ${idForLogging}`
|
||||||
);
|
);
|
||||||
c.set({
|
c.set({
|
||||||
sealedSender: SEALED_SENDER.ENABLED,
|
sealedSender: SEALED_SENDER.ENABLED,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
log.info(
|
log.warn(
|
||||||
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
|
`getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}`
|
||||||
);
|
);
|
||||||
c.set({
|
c.set({
|
||||||
sealedSender: SEALED_SENDER.DISABLED,
|
sealedSender: SEALED_SENDER.DISABLED,
|
||||||
|
@ -167,20 +216,22 @@ export async function getProfile(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.info(
|
log.info(
|
||||||
`Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
|
`getProfile: setting sealedSender to DISABLED for conversation ${idForLogging}`
|
||||||
);
|
);
|
||||||
c.set({
|
c.set({
|
||||||
sealedSender: SEALED_SENDER.DISABLED,
|
sealedSender: SEALED_SENDER.DISABLED,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rawDecryptionKey = c.get('profileKey') || lastProfile?.profileKey;
|
||||||
|
const decryptionKey = rawDecryptionKey
|
||||||
|
? Bytes.fromBase64(rawDecryptionKey)
|
||||||
|
: undefined;
|
||||||
if (profile.about) {
|
if (profile.about) {
|
||||||
const key = c.get('profileKey');
|
if (decryptionKey) {
|
||||||
if (key) {
|
|
||||||
const keyBuffer = Bytes.fromBase64(key);
|
|
||||||
const decrypted = decryptProfile(
|
const decrypted = decryptProfile(
|
||||||
Bytes.fromBase64(profile.about),
|
Bytes.fromBase64(profile.about),
|
||||||
keyBuffer
|
decryptionKey
|
||||||
);
|
);
|
||||||
c.set('about', Bytes.toString(trimForDisplay(decrypted)));
|
c.set('about', Bytes.toString(trimForDisplay(decrypted)));
|
||||||
}
|
}
|
||||||
|
@ -189,12 +240,10 @@ export async function getProfile(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (profile.aboutEmoji) {
|
if (profile.aboutEmoji) {
|
||||||
const key = c.get('profileKey');
|
if (decryptionKey) {
|
||||||
if (key) {
|
|
||||||
const keyBuffer = Bytes.fromBase64(key);
|
|
||||||
const decrypted = decryptProfile(
|
const decrypted = decryptProfile(
|
||||||
Bytes.fromBase64(profile.aboutEmoji),
|
Bytes.fromBase64(profile.aboutEmoji),
|
||||||
keyBuffer
|
decryptionKey
|
||||||
);
|
);
|
||||||
c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted)));
|
c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted)));
|
||||||
}
|
}
|
||||||
|
@ -243,7 +292,11 @@ export async function getProfile(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
switch (error?.code) {
|
if (!(error instanceof HTTPError)) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (error.code) {
|
||||||
case 401:
|
case 401:
|
||||||
case 403:
|
case 403:
|
||||||
if (
|
if (
|
||||||
|
@ -251,46 +304,49 @@ export async function getProfile(
|
||||||
c.get('sealedSender') === SEALED_SENDER.UNRESTRICTED
|
c.get('sealedSender') === SEALED_SENDER.UNRESTRICTED
|
||||||
) {
|
) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`getProfile: Got 401/403 when using accessKey for ${c.idForLogging()}, removing profileKey`
|
`getProfile: Got 401/403 when using accessKey for ${idForLogging}, removing profileKey`
|
||||||
);
|
);
|
||||||
c.setProfileKey(undefined);
|
c.setProfileKey(undefined);
|
||||||
}
|
}
|
||||||
if (c.get('sealedSender') === SEALED_SENDER.UNKNOWN) {
|
if (c.get('sealedSender') === SEALED_SENDER.UNKNOWN) {
|
||||||
log.warn(
|
log.warn(
|
||||||
`getProfile: Got 401/403 when using accessKey for ${c.idForLogging()}, setting sealedSender = DISABLED`
|
`getProfile: Got 401/403 when using accessKey for ${idForLogging}, setting sealedSender = DISABLED`
|
||||||
);
|
);
|
||||||
c.set('sealedSender', SEALED_SENDER.DISABLED);
|
c.set('sealedSender', SEALED_SENDER.DISABLED);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case 404:
|
|
||||||
log.info(
|
|
||||||
`getProfile: failed to find a profile for ${c.idForLogging()}`
|
|
||||||
);
|
|
||||||
c.setUnregistered();
|
|
||||||
return;
|
|
||||||
default:
|
default:
|
||||||
log.warn(
|
log.warn(
|
||||||
'getProfile failure:',
|
'getProfile failure:',
|
||||||
c.idForLogging(),
|
idForLogging,
|
||||||
error && error.stack ? error.stack : error
|
Errors.toLogFormat(error)
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decryptionKeyString = profileKey || lastProfile?.profileKey;
|
||||||
|
const decryptionKey = decryptionKeyString
|
||||||
|
? Bytes.fromBase64(decryptionKeyString)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
let isSuccessfullyDecrypted = true;
|
||||||
if (profile.name) {
|
if (profile.name) {
|
||||||
try {
|
if (decryptionKey) {
|
||||||
await c.setEncryptedProfileName(profile.name);
|
try {
|
||||||
} catch (error) {
|
await c.setEncryptedProfileName(profile.name, decryptionKey);
|
||||||
log.warn(
|
} catch (error) {
|
||||||
'getProfile decryption failure:',
|
log.warn(
|
||||||
c.idForLogging(),
|
'getProfile decryption failure:',
|
||||||
error && error.stack ? error.stack : error
|
idForLogging,
|
||||||
);
|
Errors.toLogFormat(error)
|
||||||
await c.set({
|
);
|
||||||
profileName: undefined,
|
isSuccessfullyDecrypted = false;
|
||||||
profileFamilyName: undefined,
|
await c.set({
|
||||||
});
|
profileName: undefined,
|
||||||
|
profileFamilyName: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
c.set({
|
c.set({
|
||||||
|
@ -300,17 +356,58 @@ export async function getProfile(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await c.setProfileAvatar(profile.avatar);
|
if (decryptionKey) {
|
||||||
|
await c.setProfileAvatar(profile.avatar, decryptionKey);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === 403 || error.code === 404) {
|
if (error instanceof HTTPError) {
|
||||||
log.info(`Clearing profile avatar for conversation ${c.idForLogging()}`);
|
if (error.code === 403 || error.code === 404) {
|
||||||
c.set({
|
log.warn(
|
||||||
profileAvatar: null,
|
`getProfile: clearing profile avatar for conversation ${idForLogging}`
|
||||||
});
|
);
|
||||||
|
c.set({
|
||||||
|
profileAvatar: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.warn(
|
||||||
|
`getProfile: failed to decrypt avatar for conversation ${idForLogging}`,
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
isSuccessfullyDecrypted = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.set('profileLastFetchedAt', Date.now());
|
c.set('profileLastFetchedAt', Date.now());
|
||||||
|
|
||||||
|
// After we successfully decrypted - update lastProfile property
|
||||||
|
if (
|
||||||
|
isSuccessfullyDecrypted &&
|
||||||
|
profileKey &&
|
||||||
|
getProfileOptions.profileKeyVersion
|
||||||
|
) {
|
||||||
|
await c.updateLastProfile(lastProfile, {
|
||||||
|
profileKey,
|
||||||
|
profileKeyVersion: getProfileOptions.profileKeyVersion,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
window.Signal.Data.updateConversation(c.attributes);
|
window.Signal.Data.updateConversation(c.attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getProfile(
|
||||||
|
providedUuid?: string,
|
||||||
|
providedE164?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const id = window.ConversationController.ensureContactIds({
|
||||||
|
uuid: providedUuid,
|
||||||
|
e164: providedE164,
|
||||||
|
});
|
||||||
|
const c = window.ConversationController.get(id);
|
||||||
|
if (!c) {
|
||||||
|
log.error('getProfile: failed to find conversation; doing nothing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await doGetProfile(c);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue