Support endorsements for group 1:1 sends

Co-authored-by: trevor-signal <131492920+trevor-signal@users.noreply.github.com>
This commit is contained in:
Jamie Kyle
2024-10-10 10:57:22 -07:00
committed by GitHub
parent 76a77a9b7f
commit e617981e59
26 changed files with 1296 additions and 796 deletions

View File

@@ -42,6 +42,7 @@ import { Sessions, IdentityKeys } from '../LibSignalStores';
import { getKeysForServiceId } from './getKeysForServiceId';
import { SignalService as Proto } from '../protobuf';
import * as log from '../logging/log';
import type { GroupSendToken } from '../types/GroupSendEndorsements';
export const enum SenderCertificateMode {
WithE164,
@@ -306,13 +307,20 @@ export default class OutgoingMessage {
serviceId: ServiceIdString,
jsonData: ReadonlyArray<MessageType>,
timestamp: number,
{ accessKey }: { accessKey?: string } = {}
{
accessKey,
groupSendToken,
}: {
accessKey: string | null;
groupSendToken: GroupSendToken | null;
} = { accessKey: null, groupSendToken: null }
): Promise<void> {
let promise;
if (accessKey) {
if (accessKey != null || groupSendToken != null) {
promise = this.server.sendMessagesUnauth(serviceId, jsonData, timestamp, {
accessKey,
groupSendToken,
online: this.online,
story: this.story,
urgent: this.urgent,
@@ -393,7 +401,11 @@ export default class OutgoingMessage {
recurse?: boolean
): Promise<void> {
const { sendMetadata } = this;
const { accessKey, senderCertificate } = sendMetadata?.[serviceId] || {};
const {
accessKey = null,
groupSendToken = null,
senderCertificate,
} = sendMetadata?.[serviceId] || {};
if (accessKey && !senderCertificate) {
log.warn(
@@ -401,7 +413,9 @@ export default class OutgoingMessage {
);
}
const sealedSender = Boolean(accessKey && senderCertificate);
const sealedSender =
(accessKey != null || groupSendToken != null) &&
senderCertificate != null;
// We don't send to ourselves unless sealedSender is enabled
const ourNumber = window.textsecure.storage.user.getNumber();
@@ -508,6 +522,7 @@ export default class OutgoingMessage {
if (sealedSender) {
return this.transmitMessage(serviceId, jsonData, this.timestamp, {
accessKey,
groupSendToken,
}).then(
() => {
this.recipients[serviceId] = deviceIds;

View File

@@ -36,8 +36,6 @@ import {
import type {
ChallengeType,
GetGroupLogOptionsType,
GetProfileOptionsType,
GetProfileUnauthOptionsType,
GroupCredentialsType,
GroupLogResponseType,
ProxiedRequestOptionsType,
@@ -103,12 +101,22 @@ import {
getProtoForCallHistory,
} from '../util/callDisposition';
import { MAX_MESSAGE_COUNT } from '../util/deleteForMe.types';
import type { GroupSendToken } from '../types/GroupSendEndorsements';
export type SendIdentifierData =
| {
accessKey: string;
senderCertificate: SerializedCertificateType | null;
groupSendToken: null;
}
| {
accessKey: null;
senderCertificate: SerializedCertificateType | null;
groupSendToken: GroupSendToken;
};
export type SendMetadataType = {
[serviceId: ServiceIdString]: {
accessKey: string;
senderCertificate?: SerializedCertificateType;
};
[serviceId: ServiceIdString]: SendIdentifierData;
};
export type SendOptionsType = {
@@ -2321,17 +2329,6 @@ export default class MessageSender {
// Note: instead of updating these functions, or adding new ones, remove these and go
// directly to window.textsecure.messaging.server.<function>
async getProfile(
serviceId: ServiceIdString,
options: GetProfileOptionsType | GetProfileUnauthOptionsType
): ReturnType<WebAPIType['getProfile']> {
if (options.accessKey !== undefined) {
return this.server.getProfileUnauth(serviceId, options);
}
return this.server.getProfile(serviceId, options);
}
async getAvatar(path: string): Promise<ReturnType<WebAPIType['getAvatar']>> {
return this.server.getAvatar(path);
}

View File

@@ -74,6 +74,10 @@ import { isStagingServer } from '../util/isStagingServer';
import type { IWebSocketResource } from './WebsocketResources';
import type { GroupSendToken } from '../types/GroupSendEndorsements';
import { parseUnknown, safeParseUnknown } from '../util/schemas';
import type {
ProfileFetchAuthRequestOptions,
ProfileFetchUnauthRequestOptions,
} from '../services/profiles';
// Note: this will break some code that expects to be able to use err.response when a
// web request fails, because it will force it to text. But it is very useful for
@@ -91,7 +95,7 @@ function resolveLibsignalNetEnvironment(url: string): Net.Environment {
}
function _createRedactor(
...toReplace: ReadonlyArray<string | undefined>
...toReplace: ReadonlyArray<string | undefined | null>
): RedactUrl {
// NOTE: It would be nice to remove this cast, but TypeScript doesn't support
// it. However, there is [an issue][0] that discusses this in more detail.
@@ -898,31 +902,6 @@ export type CdsLookupOptionsType = Readonly<{
useLibsignal?: boolean;
}>;
type GetProfileCommonOptionsType = Readonly<
{
userLanguages: ReadonlyArray<string>;
} & (
| {
profileKeyVersion?: undefined;
profileKeyCredentialRequest?: undefined;
}
| {
profileKeyVersion: string;
profileKeyCredentialRequest?: string;
}
)
>;
export type GetProfileOptionsType = GetProfileCommonOptionsType &
Readonly<{
accessKey?: undefined;
}>;
export type GetProfileUnauthOptionsType = GetProfileCommonOptionsType &
Readonly<{
accessKey: string;
}>;
export type GetGroupCredentialsOptionsType = Readonly<{
startDayInMs: number;
endDayInMs: number;
@@ -1311,14 +1290,14 @@ export type WebAPIType = {
}>;
getProfile: (
serviceId: ServiceIdString,
options: GetProfileOptionsType
options: ProfileFetchAuthRequestOptions
) => Promise<ProfileType>;
getAccountForUsername: (
options: GetAccountForUsernameOptionsType
) => Promise<GetAccountForUsernameResultType>;
getProfileUnauth: (
serviceId: ServiceIdString,
options: GetProfileUnauthOptionsType
options: ProfileFetchUnauthRequestOptions
) => Promise<ProfileType>;
getBadgeImageFile: (imageUrl: string) => Promise<Uint8Array>;
getSubscriptionConfiguration: (
@@ -1413,7 +1392,8 @@ export type WebAPIType = {
messageArray: ReadonlyArray<MessageType>,
timestamp: number,
options: {
accessKey?: string;
accessKey: string | null;
groupSendToken: GroupSendToken | null;
online?: boolean;
story?: boolean;
urgent?: boolean;
@@ -1421,8 +1401,8 @@ export type WebAPIType = {
) => Promise<void>;
sendWithSenderKey: (
payload: Uint8Array,
accessKeys: Uint8Array | undefined,
groupSendToken: GroupSendToken | undefined,
accessKeys: Uint8Array | null,
groupSendToken: GroupSendToken | null,
timestamp: number,
options: {
online?: boolean;
@@ -2177,19 +2157,19 @@ export function initialize({
{
profileKeyVersion,
profileKeyCredentialRequest,
}: GetProfileCommonOptionsType
}: ProfileFetchAuthRequestOptions | ProfileFetchUnauthRequestOptions
) {
let profileUrl = `/${serviceId}`;
if (profileKeyVersion !== undefined) {
if (profileKeyVersion != null) {
profileUrl += `/${profileKeyVersion}`;
if (profileKeyCredentialRequest !== undefined) {
if (profileKeyCredentialRequest != null) {
profileUrl +=
`/${profileKeyCredentialRequest}` +
'?credentialType=expiringProfileKey';
}
} else {
strictAssert(
profileKeyCredentialRequest === undefined,
profileKeyCredentialRequest == null,
'getProfileUrl called without version, but with request'
);
}
@@ -2199,7 +2179,7 @@ export function initialize({
async function getProfile(
serviceId: ServiceIdString,
options: GetProfileOptionsType
options: ProfileFetchAuthRequestOptions
) {
const { profileKeyVersion, profileKeyCredentialRequest, userLanguages } =
options;
@@ -2258,15 +2238,25 @@ export function initialize({
async function getProfileUnauth(
serviceId: ServiceIdString,
options: GetProfileUnauthOptionsType
options: ProfileFetchUnauthRequestOptions
) {
const {
accessKey,
groupSendToken,
profileKeyVersion,
profileKeyCredentialRequest,
userLanguages,
} = options;
if (profileKeyVersion != null || profileKeyCredentialRequest != null) {
// Without an up-to-date profile key, we won't be able to read the
// profile anyways so there's no point in falling back to endorsements.
strictAssert(
groupSendToken == null,
'Should not use endorsements for fetching a versioned profile'
);
}
return (await _ajax({
call: 'profile',
httpType: 'GET',
@@ -2276,8 +2266,8 @@ export function initialize({
},
responseType: 'json',
unauthenticated: true,
accessKey,
groupSendToken: undefined,
accessKey: accessKey ?? undefined,
groupSendToken: groupSendToken ?? undefined,
redactUrl: _createRedactor(
serviceId,
profileKeyVersion,
@@ -3273,11 +3263,13 @@ export function initialize({
timestamp: number,
{
accessKey,
groupSendToken,
online,
urgent = true,
story = false,
}: {
accessKey?: string;
accessKey: string | null;
groupSendToken: GroupSendToken | null;
online?: boolean;
story?: boolean;
urgent?: boolean;
@@ -3297,8 +3289,8 @@ export function initialize({
jsonData,
responseType: 'json',
unauthenticated: true,
accessKey,
groupSendToken: undefined,
accessKey: accessKey ?? undefined,
groupSendToken: groupSendToken ?? undefined,
});
}
@@ -3334,8 +3326,8 @@ export function initialize({
async function sendWithSenderKey(
data: Uint8Array,
accessKeys: Uint8Array | undefined,
groupSendToken: GroupSendToken | undefined,
accessKeys: Uint8Array | null,
groupSendToken: GroupSendToken | null,
timestamp: number,
{
online,
@@ -3360,7 +3352,7 @@ export function initialize({
responseType: 'json',
unauthenticated: true,
accessKey: accessKeys != null ? Bytes.toBase64(accessKeys) : undefined,
groupSendToken,
groupSendToken: groupSendToken ?? undefined,
});
const parseResult = safeParseUnknown(
multiRecipient200ResponseSchema,