fix(transactions): mask still-encrypted values instead of showing ciphertext
Before a legacy encrypted user unlocks and the decrypt-migration runs, the transaction list rendered raw base64 ciphertext as the description/notes (the old EncryptedText component used to mask these while locked). Show a neutral placeholder whenever a row is still encrypted (description_iv / notes_iv set), so the list never leaks unreadable ciphertext. The placeholder clears automatically once the row is decrypted and re-synced as plaintext.
This commit is contained in:
parent
b581206a35
commit
bac28f332c
|
|
@ -8,7 +8,10 @@ import { AccountName } from '@/components/accounts/account-name';
|
|||
import { BankLogo } from '@/components/bank-logo';
|
||||
import { LabelBadges } from '@/components/shared/label-combobox';
|
||||
import { CategoryCell } from '@/components/transactions/category-cell';
|
||||
import { TransactionDescription } from '@/components/transactions/transaction-description';
|
||||
import {
|
||||
ENCRYPTED_PLACEHOLDER,
|
||||
TransactionDescription,
|
||||
} from '@/components/transactions/transaction-description';
|
||||
import { AmountDisplay } from '@/components/ui/amount-display';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
|
|
@ -214,6 +217,7 @@ export function createTransactionColumns({
|
|||
<div className="flex-grow truncate">
|
||||
<TransactionDescription
|
||||
text={transaction.description}
|
||||
encrypted={!!transaction.description_iv}
|
||||
/>
|
||||
</div>
|
||||
{showLabels && hasLabels && (
|
||||
|
|
@ -226,7 +230,11 @@ export function createTransactionColumns({
|
|||
{showNotes && hasNotes && (
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground">
|
||||
<div className="truncate text-muted-foreground/80">
|
||||
<span>{transaction.notes}</span>
|
||||
<span>
|
||||
{transaction.notes_iv
|
||||
? ENCRYPTED_PLACEHOLDER
|
||||
: transaction.notes}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import { PrivacyModeProvider } from '@/contexts/privacy-mode-context';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type React from 'react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
ENCRYPTED_PLACEHOLDER,
|
||||
TransactionDescription,
|
||||
} from './transaction-description';
|
||||
|
||||
function renderWithPrivacy(ui: React.ReactElement) {
|
||||
return render(<PrivacyModeProvider>{ui}</PrivacyModeProvider>);
|
||||
}
|
||||
|
||||
describe('TransactionDescription', () => {
|
||||
it('renders the plaintext description when not encrypted', () => {
|
||||
renderWithPrivacy(
|
||||
<TransactionDescription text="Coffee at Starbucks" />,
|
||||
);
|
||||
|
||||
expect(screen.getByText('Coffee at Starbucks')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('masks a still-encrypted value instead of leaking raw ciphertext', () => {
|
||||
const ciphertext = 'U2FsdGVkX1+abc123def456==';
|
||||
|
||||
renderWithPrivacy(
|
||||
<TransactionDescription text={ciphertext} encrypted />,
|
||||
);
|
||||
|
||||
expect(screen.queryByText(ciphertext)).not.toBeInTheDocument();
|
||||
expect(screen.getByText(ENCRYPTED_PLACEHOLDER)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -35,19 +35,31 @@ function getFakeDescription(seed: string): string {
|
|||
return FAKE_DESCRIPTIONS[index];
|
||||
}
|
||||
|
||||
/**
|
||||
* Shown in place of a still-encrypted legacy value before the user unlocks and
|
||||
* the decrypt-migration runs, so the list never renders raw ciphertext.
|
||||
*/
|
||||
export const ENCRYPTED_PLACEHOLDER = '••••••••••';
|
||||
|
||||
interface TransactionDescriptionProps {
|
||||
text: string;
|
||||
className?: string;
|
||||
encrypted?: boolean;
|
||||
}
|
||||
|
||||
export function TransactionDescription({
|
||||
text,
|
||||
className = '',
|
||||
encrypted = false,
|
||||
}: TransactionDescriptionProps) {
|
||||
const { isPrivacyModeEnabled } = usePrivacyMode();
|
||||
|
||||
const fakeDescription = useMemo(() => getFakeDescription(text), [text]);
|
||||
|
||||
if (encrypted) {
|
||||
return <span className={className}>{ENCRYPTED_PLACEHOLDER}</span>;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{isPrivacyModeEnabled ? fakeDescription : text}
|
||||
|
|
|
|||
Loading…
Reference in New Issue