Extra toast for Message Receiver errors

This commit is contained in:
Fedor Indutny 2023-05-03 13:53:28 -07:00 committed by Josh Perez
parent 36b3e2de08
commit ca4aad6bad
4 changed files with 96 additions and 11 deletions

View file

@ -88,6 +88,7 @@ import type {
ErrorEvent, ErrorEvent,
FetchLatestEvent, FetchLatestEvent,
GroupEvent, GroupEvent,
InvalidPlaintextEvent,
KeysEvent, KeysEvent,
MessageEvent, MessageEvent,
MessageEventData, MessageEventData,
@ -140,7 +141,11 @@ import * as Conversation from './types/Conversation';
import * as Stickers from './types/Stickers'; import * as Stickers from './types/Stickers';
import * as Errors from './types/errors'; import * as Errors from './types/errors';
import { SignalService as Proto } from './protobuf'; import { SignalService as Proto } from './protobuf';
import { onRetryRequest, onDecryptionError } from './util/handleRetry'; import {
onRetryRequest,
onDecryptionError,
onInvalidPlaintextMessage,
} from './util/handleRetry';
import { themeChanged } from './shims/themeChanged'; import { themeChanged } from './shims/themeChanged';
import { createIPCEvents } from './util/createIPCEvents'; import { createIPCEvents } from './util/createIPCEvents';
import { RemoveAllConfiguration } from './types/RemoveAllConfiguration'; import { RemoveAllConfiguration } from './types/RemoveAllConfiguration';
@ -390,6 +395,14 @@ export async function startApp(): Promise<void> {
drop(onDecryptionErrorQueue.add(() => onDecryptionError(event))); drop(onDecryptionErrorQueue.add(() => onDecryptionError(event)));
}) })
); );
messageReceiver.addEventListener(
'invalid-plaintext',
queuedEventListener((event: InvalidPlaintextEvent): void => {
drop(
onDecryptionErrorQueue.add(() => onInvalidPlaintextMessage(event))
);
})
);
messageReceiver.addEventListener( messageReceiver.addEventListener(
'retry-request', 'retry-request',
queuedEventListener((event: RetryRequestEvent): void => { queuedEventListener((event: RetryRequestEvent): void => {

View file

@ -96,6 +96,7 @@ import {
DecryptionErrorEvent, DecryptionErrorEvent,
SentEvent, SentEvent,
ProfileKeyUpdateEvent, ProfileKeyUpdateEvent,
InvalidPlaintextEvent,
MessageEvent, MessageEvent,
RetryRequestEvent, RetryRequestEvent,
ReadEvent, ReadEvent,
@ -160,6 +161,11 @@ type DecryptSealedSenderResult = Readonly<{
unsealedPlaintext?: SealedSenderDecryptionResult; unsealedPlaintext?: SealedSenderDecryptionResult;
}>; }>;
type InnerDecryptResultType = Readonly<{
plaintext: Uint8Array;
wasEncrypted: boolean;
}>;
type CacheAddItemType = { type CacheAddItemType = {
envelope: ProcessedEnvelope; envelope: ProcessedEnvelope;
data: UnprocessedType; data: UnprocessedType;
@ -533,6 +539,11 @@ export default class MessageReceiver
handler: (ev: DecryptionErrorEvent) => void handler: (ev: DecryptionErrorEvent) => void
): void; ): void;
public override addEventListener(
name: 'invalid-plaintext',
handler: (ev: InvalidPlaintextEvent) => void
): void;
public override addEventListener( public override addEventListener(
name: 'sent', name: 'sent',
handler: (ev: SentEvent) => void handler: (ev: SentEvent) => void
@ -1395,18 +1406,20 @@ export default class MessageReceiver
} }
log.info(logId); log.info(logId);
const plaintext = await this.decrypt( const decryptResult = await this.decrypt(
stores, stores,
envelope, envelope,
ciphertext, ciphertext,
uuidKind uuidKind
); );
if (!plaintext) { if (!decryptResult) {
log.warn(`${logId}: plaintext was falsey`); log.warn(`${logId}: plaintext was falsey`);
return { plaintext, envelope }; return { plaintext: undefined, envelope };
} }
const { plaintext, wasEncrypted } = decryptResult;
// Note: we need to process this as part of decryption, because we might need this // Note: we need to process this as part of decryption, because we might need this
// sender key to decrypt the next message in the queue! // sender key to decrypt the next message in the queue!
let isGroupV2 = false; let isGroupV2 = false;
@ -1414,6 +1427,32 @@ export default class MessageReceiver
let inProgressMessageType = ''; let inProgressMessageType = '';
try { try {
const content = Proto.Content.decode(plaintext); const content = Proto.Content.decode(plaintext);
if (!wasEncrypted && Bytes.isEmpty(content.decryptionErrorMessage)) {
log.warn(
`${logId}: dropping plaintext envelope without decryption error message`
);
const event = new InvalidPlaintextEvent({
senderDevice: envelope.sourceDevice ?? 1,
senderUuid: envelope.sourceUuid,
timestamp: envelope.timestamp,
});
this.removeFromCache(envelope);
const envelopeId = getEnvelopeId(envelope);
// Avoid deadlocks by scheduling processing on decrypted queue
drop(
this.addToQueue(
async () => this.dispatchEvent(event),
`decrypted/dispatchEvent/InvalidPlaintextEvent(${envelopeId})`,
TaskType.Decrypted
)
);
return { plaintext: undefined, envelope };
}
isGroupV2 = isGroupV2 =
Boolean(content.dataMessage?.groupV2) || Boolean(content.dataMessage?.groupV2) ||
@ -1742,7 +1781,7 @@ export default class MessageReceiver
envelope: UnsealedEnvelope, envelope: UnsealedEnvelope,
ciphertext: Uint8Array, ciphertext: Uint8Array,
uuidKind: UUIDKind uuidKind: UUIDKind
): Promise<Uint8Array | undefined> { ): Promise<InnerDecryptResultType | undefined> {
const { sessionStore, identityKeyStore, zone } = stores; const { sessionStore, identityKeyStore, zone } = stores;
const logId = getEnvelopeId(envelope); const logId = getEnvelopeId(envelope);
@ -1784,7 +1823,10 @@ export default class MessageReceiver
const buffer = Buffer.from(ciphertext); const buffer = Buffer.from(ciphertext);
const plaintextContent = PlaintextContent.deserialize(buffer); const plaintextContent = PlaintextContent.deserialize(buffer);
return this.unpad(plaintextContent.body()); return {
plaintext: this.unpad(plaintextContent.body()),
wasEncrypted: false,
};
} }
if (envelope.type === envelopeTypeEnum.CIPHERTEXT) { if (envelope.type === envelopeTypeEnum.CIPHERTEXT) {
log.info(`decrypt/${logId}: ciphertext message`); log.info(`decrypt/${logId}: ciphertext message`);
@ -1813,7 +1855,7 @@ export default class MessageReceiver
), ),
zone zone
); );
return plaintext; return { plaintext, wasEncrypted: true };
} }
if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) { if (envelope.type === envelopeTypeEnum.PREKEY_BUNDLE) {
log.info(`decrypt/${logId}: prekey message`); log.info(`decrypt/${logId}: prekey message`);
@ -1846,7 +1888,7 @@ export default class MessageReceiver
), ),
zone zone
); );
return plaintext; return { plaintext, wasEncrypted: true };
} }
if (envelope.type === envelopeTypeEnum.UNIDENTIFIED_SENDER) { if (envelope.type === envelopeTypeEnum.UNIDENTIFIED_SENDER) {
log.info(`decrypt/${logId}: unidentified message`); log.info(`decrypt/${logId}: unidentified message`);
@ -1857,7 +1899,7 @@ export default class MessageReceiver
); );
if (plaintext) { if (plaintext) {
return this.unpad(plaintext); return { plaintext: this.unpad(plaintext), wasEncrypted: false };
} }
if (unsealedPlaintext) { if (unsealedPlaintext) {
@ -1871,7 +1913,7 @@ export default class MessageReceiver
// Return just the content because that matches the signature of the other // Return just the content because that matches the signature of the other
// decrypt methods used above. // decrypt methods used above.
return this.unpad(content); return { plaintext: this.unpad(content), wasEncrypted: true };
} }
throw new Error('Unexpected lack of plaintext from unidentified sender'); throw new Error('Unexpected lack of plaintext from unidentified sender');
@ -1884,7 +1926,7 @@ export default class MessageReceiver
envelope: UnsealedEnvelope, envelope: UnsealedEnvelope,
ciphertext: Uint8Array, ciphertext: Uint8Array,
uuidKind: UUIDKind uuidKind: UUIDKind
): Promise<Uint8Array | undefined> { ): Promise<InnerDecryptResultType | undefined> {
try { try {
return await this.innerDecrypt(stores, envelope, ciphertext, uuidKind); return await this.innerDecrypt(stores, envelope, ciphertext, uuidKind);
} catch (error) { } catch (error) {

View file

@ -161,6 +161,18 @@ export class DecryptionErrorEvent extends ConfirmableEvent {
} }
} }
export type InvalidPlaintextEventData = Readonly<{
senderDevice: number;
senderUuid: string;
timestamp: number;
}>;
export class InvalidPlaintextEvent extends Event {
constructor(public readonly data: InvalidPlaintextEventData) {
super('invalid-plaintext');
}
}
export type RetryRequestEventData = Readonly<{ export type RetryRequestEventData = Readonly<{
groupId?: string; groupId?: string;
ratchetKey?: PublicKey; ratchetKey?: PublicKey;

View file

@ -29,6 +29,7 @@ import type { ConversationModel } from '../models/conversations';
import type { import type {
DecryptionErrorEvent, DecryptionErrorEvent,
DecryptionErrorEventData, DecryptionErrorEventData,
InvalidPlaintextEvent,
RetryRequestEvent, RetryRequestEvent,
RetryRequestEventData, RetryRequestEventData,
} from '../textsecure/messageReceiverEvents'; } from '../textsecure/messageReceiverEvents';
@ -205,6 +206,23 @@ function maybeShowDecryptionToast(
}); });
} }
export function onInvalidPlaintextMessage({
data,
}: InvalidPlaintextEvent): void {
const { senderUuid, senderDevice, timestamp } = data;
const logId = `${senderUuid}.${senderDevice} ${timestamp}`;
log.info(`onInvalidPlaintextMessage/${logId}: Starting...`);
const conversation = window.ConversationController.getOrCreate(
senderUuid,
'private'
);
const name = conversation.getTitle();
maybeShowDecryptionToast(logId, name, senderDevice);
}
export async function onDecryptionError( export async function onDecryptionError(
event: DecryptionErrorEvent event: DecryptionErrorEvent
): Promise<void> { ): Promise<void> {