Skip to content

Commit fbb5a07

Browse files
author
Ioan Moldovan
authored
#5868 Display warning for incorrect pgp checksum (#5883)
* feat: display warning for incorrect pgp checksum * fix: ui test * fix: pr reviews
1 parent e7636ca commit fbb5a07

File tree

11 files changed

+83
-15
lines changed

11 files changed

+83
-15
lines changed

extension/chrome/elements/pgp_block.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ export class PgpBlockView extends View {
110110
if (data?.separateQuotedContentAndRenderText) {
111111
this.quoteModule.separateQuotedContentAndRenderText(
112112
data.separateQuotedContentAndRenderText.decryptedContent,
113-
data.separateQuotedContentAndRenderText.isHtml
113+
data.separateQuotedContentAndRenderText.isHtml,
114+
data.separateQuotedContentAndRenderText.isChecksumInvalid
114115
);
115116
}
116117
if (data?.setFrameColor) {

extension/chrome/elements/pgp_block_modules/pgp-block-quote-module.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Xss } from '../../../js/common/platform/xss.js';
99
export class PgpBlockViewQuoteModule {
1010
public constructor(private view: PgpBlockView) {}
1111

12-
public separateQuotedContentAndRenderText = (decryptedContent: string, isHtml: boolean) => {
12+
public separateQuotedContentAndRenderText = (decryptedContent: string, isHtml: boolean, isChecksumInvalid: boolean) => {
1313
if (isHtml) {
1414
const message = $('<div>').html(Xss.htmlSanitizeKeepBasicTags(decryptedContent)); // xss-sanitized
1515
let htmlBlockQuoteExists = false;
@@ -32,10 +32,10 @@ export class PgpBlockViewQuoteModule {
3232
message[0].removeChild(shouldBeQuoted[i]);
3333
quotedHtml += shouldBeQuoted[i].outerHTML;
3434
}
35-
this.view.renderModule.renderContent(message.html(), false);
35+
this.view.renderModule.renderContent(message.html(), false, isChecksumInvalid);
3636
this.appendCollapsedQuotedContentButton(quotedHtml, true);
3737
} else {
38-
this.view.renderModule.renderContent(decryptedContent, false);
38+
this.view.renderModule.renderContent(decryptedContent, false, isChecksumInvalid);
3939
}
4040
} else {
4141
const lines = decryptedContent.split(/\r?\n/);
@@ -62,7 +62,7 @@ export class PgpBlockViewQuoteModule {
6262
// only got quoted part, no real text -> show everything as real text, without quoting
6363
lines.push(...linesQuotedPart.splice(0, linesQuotedPart.length));
6464
}
65-
this.view.renderModule.renderContent(Str.escapeTextAsRenderableHtml(lines.join('\n')), false);
65+
this.view.renderModule.renderContent(Str.escapeTextAsRenderableHtml(lines.join('\n')), false, isChecksumInvalid);
6666
if (linesQuotedPart.join('').trim()) {
6767
this.appendCollapsedQuotedContentButton(linesQuotedPart.join('\n'));
6868
}

extension/chrome/elements/pgp_block_modules/pgp-block-render-module.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,11 @@ export class PgpBlockViewRenderModule {
4646
});
4747
};
4848

49-
public renderContent = (htmlContent: string, isErr: boolean) => {
49+
public renderContent = (htmlContent: string, isErr: boolean, isChecksumInvalid = false) => {
5050
let contentWithLink = linkifyHtml(htmlContent);
51-
51+
if (isChecksumInvalid) {
52+
contentWithLink = `<div class="pgp-invalid-checksum">${Lang.pgpBlock.invalidCheckSum}</div>${contentWithLink}}`;
53+
}
5254
// Temporary workaround for an issue where 'cryptup_reply' divs are not being hidden when replying to all
5355
// messages from the FES. The root cause is that FES currently returns the body of
5456
// password message replies as 'text/plain', which does not hide the divs as intended.

extension/css/cryptup.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,6 +1592,13 @@ td {
15921592
align-items: center;
15931593
}
15941594

1595+
.pgp-invalid-checksum {
1596+
border-left: 3px solid #d18826;
1597+
color: #d18826;
1598+
margin: 10px 0;
1599+
padding-left: 5px;
1600+
}
1601+
15951602
.pgp_badge {
15961603
opacity: 1;
15971604
margin-bottom: 1em;

extension/js/common/core/crypto/key.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,60 @@ export class KeyUtil {
489489
return keys.map(k => ({ id: k.id, emails: k.emails, armored: KeyUtil.armor(k), family: k.family }));
490490
}
491491

492+
public static validateChecksum = (armoredText: string): boolean => {
493+
const lines = armoredText.split('\n').map(l => l.trim());
494+
495+
// Filter out known non-data lines
496+
const dataCandidates = lines.filter(line => line.length > 0 && !line.startsWith('-----') && !line.startsWith('Version:') && !line.startsWith('Comment:'));
497+
498+
// Find checksum line
499+
const checksumIndex = dataCandidates.findIndex(line => line.startsWith('='));
500+
if (checksumIndex === -1) return false;
501+
502+
const checksumLine = dataCandidates[checksumIndex].slice(1);
503+
const dataLines = dataCandidates.slice(0, checksumIndex);
504+
505+
// Decode checksum
506+
let providedBytes: string;
507+
try {
508+
providedBytes = atob(checksumLine);
509+
} catch {
510+
return false;
511+
}
512+
if (providedBytes.length !== 3) return false;
513+
514+
const providedCRC = (providedBytes.charCodeAt(0) << 16) | (providedBytes.charCodeAt(1) << 8) | providedBytes.charCodeAt(2);
515+
516+
// Attempt to decode all data lines (some may not be base64)
517+
const decodedChunks: string[] = [];
518+
for (const line of dataLines) {
519+
try {
520+
decodedChunks.push(atob(line));
521+
} catch {
522+
// Not a valid base64 line, skip it
523+
}
524+
}
525+
526+
if (decodedChunks.length === 0) return false;
527+
528+
const rawData = decodedChunks.join('');
529+
const dataBytes = new Uint8Array([...rawData].map(c => c.charCodeAt(0)));
530+
531+
return KeyUtil.crc24(dataBytes) === providedCRC;
532+
};
533+
534+
private static crc24 = (dataBytes: Uint8Array): number => {
535+
let crc = 0xb704ce;
536+
for (const dataByte of dataBytes) {
537+
crc ^= dataByte << 16;
538+
for (let j = 0; j < 8; j++) {
539+
crc <<= 1;
540+
if (crc & 0x1000000) crc ^= 0x1864cfb;
541+
}
542+
}
543+
return crc & 0xffffff;
544+
};
545+
492546
private static getSortValue(pubinfo: PubkeyInfo): number {
493547
const expirationSortValue = typeof pubinfo.pubkey.expiration === 'undefined' ? Infinity : pubinfo.pubkey.expiration;
494548
// sort non-revoked first, then non-expired

extension/js/common/lang.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ export const Lang = {
9393
cannotLocate: 'Could not locate this message.',
9494
brokenLink: 'It seems it contains a broken link.',
9595
pwdMsgAskSenderUsePubkey: 'This appears to be a password-protected message. Please ask the sender to encrypt messages for your Public Key instead.',
96+
invalidCheckSum:
97+
'Warning: Checksum mismatch detected.\nThis indicates the message may have been altered or corrupted during transmission.\nDecryption may still succeed, but verify the message source and integrity if possible.',
9698
},
9799
compose: {
98100
abortSending: 'A message is currently being sent. Closing the compose window may abort sending the message.\nAbort sending?',

extension/js/common/message-renderer.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ export class MessageRenderer {
521521
sigResult: VerifyRes | undefined,
522522
renderModule: RenderInterface,
523523
retryVerification: (() => Promise<VerifyRes | undefined>) | undefined,
524-
plainSubject: string | undefined
524+
plainSubject: string | undefined,
525+
isChecksumInvalid = false
525526
): Promise<{ publicKeys?: string[] }> => {
526527
if (isEncrypted) {
527528
renderModule.renderEncryptionStatus('encrypted');
@@ -603,7 +604,7 @@ export class MessageRenderer {
603604
);
604605
}
605606
decryptedContent = this.clipMessageIfLimitExceeds(decryptedContent);
606-
renderModule.separateQuotedContentAndRenderText(decryptedContent, isHtml);
607+
renderModule.separateQuotedContentAndRenderText(decryptedContent, isHtml, isChecksumInvalid);
607608
await MessageRenderer.renderPgpSignatureCheckResult(renderModule, sigResult, Boolean(signerEmail), retryVerification);
608609
if (renderableAttachments.length) {
609610
renderModule.renderInnerAttachments(renderableAttachments, isEncrypted);
@@ -743,7 +744,8 @@ export class MessageRenderer {
743744
result.signature,
744745
renderModule,
745746
this.getRetryVerification(signerEmail, verificationPubs => MessageRenderer.decryptFunctionToVerifyRes(() => decrypt(verificationPubs))),
746-
plainSubject
747+
plainSubject,
748+
!KeyUtil.validateChecksum(encryptedData.toString())
747749
);
748750
} else if (result.error.type === DecryptErrTypes.format) {
749751
if (fallbackToPlainText) {

extension/js/common/render-interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export interface RenderInterface extends RenderInterfaceBase {
2424
renderPassphraseNeeded(longids: string[]): void;
2525
renderErr(errBoxContent: string, renderRawMsg: string | undefined, errMsg?: string): void;
2626
renderInnerAttachments(attachments: TransferableAttachment[], isEncrypted: boolean): void;
27-
separateQuotedContentAndRenderText(decryptedContent: string, isHtml: boolean): void;
27+
separateQuotedContentAndRenderText(decryptedContent: string, isHtml: boolean, isChecksumInvalid: boolean): void;
2828
renderVerificationInProgress(): void;
2929
renderSignatureOffline(retry: () => void): void;
3030
}

extension/js/common/render-message.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export type MessageInfo = {
2323
export type RenderMessage = {
2424
done?: true;
2525
resizePgpBlockFrame?: true;
26-
separateQuotedContentAndRenderText?: { decryptedContent: string; isHtml: boolean };
26+
separateQuotedContentAndRenderText?: { decryptedContent: string; isHtml: boolean; isChecksumInvalid: boolean };
2727
renderText?: string;
2828
progressOperation?: { operationId: string; text: string; perc?: number; init?: boolean };
2929
setFrameColor?: 'green' | 'gray' | 'red';

extension/js/common/render-relay.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ export class RenderRelay implements RenderInterface {
7878
this.relay({ resizePgpBlockFrame: true });
7979
};
8080

81-
public separateQuotedContentAndRenderText = (decryptedContent: string, isHtml: boolean) => {
82-
this.relay({ separateQuotedContentAndRenderText: { decryptedContent, isHtml } });
81+
public separateQuotedContentAndRenderText = (decryptedContent: string, isHtml: boolean, isChecksumInvalid: boolean) => {
82+
this.relay({ separateQuotedContentAndRenderText: { decryptedContent, isHtml, isChecksumInvalid } });
8383
};
8484

8585
public renderText = (text: string) => {

0 commit comments

Comments
 (0)