Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/app/src/components/connect/MintRequestPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import Card from "@app/components/Card";
import { feeRate as feeRateSignal } from "@app/signals";
import { embeddableContentBytes } from "@app/svgSanitize";
import type { MintRequest } from "@app/connect/protocol";
import { sanitizeForDisplay } from "@lib/displayText";

/**
* Data URL for the preview, built from the bytes that will actually be
Expand Down Expand Up @@ -137,8 +138,8 @@ export default function MintRequestPanel({
Requested by
</Text>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">
{request.app ? `${request.app} — ` : ""}
{request.origin ?? "(no origin provided)"}
{request.app ? `${sanitizeForDisplay(request.app)} — ` : ""}
{request.origin ? sanitizeForDisplay(request.origin) : "(no origin provided)"}
</Code>
</Box>
) : (
Expand Down Expand Up @@ -248,7 +249,7 @@ export default function MintRequestPanel({
{autoReturn && (
<Text textStyle="small" mt={2}>
After approving you will be sent back to{" "}
{request.app || "the app"} at <b>{request.origin}</b>, which
{request.app ? sanitizeForDisplay(request.app) : "the app"} at <b>{sanitizeForDisplay(request.origin ?? "")}</b>, which
receives the result automatically.
</Text>
)}
Expand Down
99 changes: 93 additions & 6 deletions packages/app/src/components/connect/PsbtRequestPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ import {
Text,
Button,
} from "@chakra-ui/react";
import { useState } from "react";
import { MdCloudUpload, MdUndo, MdWarning } from "react-icons/md";
import Card from "@app/components/Card";
import { photonsToRXD } from "@lib/format";
import { sanitizeForDisplay } from "@lib/displayText";
import type { PsbtSignRequest } from "@app/connect/protocol";
import type { EnrichedPsbt } from "@app/connect/psbtFlow";
import type { PsbtInputSummary, PsbtOutputSummary } from "@lib/psbt";
Expand Down Expand Up @@ -95,6 +97,87 @@ function InputRow({
);
}

/**
* What an `OP_RETURN` output actually carries.
*
* This output pays nobody and cannot be spent, so its 0 RXD says nothing about
* what approving it does. The payload is the whole of it, and it is permanent —
* which makes it the one thing on this screen most worth showing, and the thing
* that used to read only as "(non-standard output)".
*
* Described, never interpreted. The wallet does not claim to know what another
* application's bytes mean; it says how big they are, how they are structured,
* and which parts happen to be readable. Text is rendered through
* `sanitizeForDisplay` because it is supplied by the requesting app, and hex is
* always available beside it for anything the text form cannot be trusted with.
*/
function DataOutputDetail({ data }: { data: NonNullable<PsbtOutputSummary["data"]> }) {
const [open, setOpen] = useState(false);

return (
<Box>
<HStack spacing={2} mb={1}>
<Text fontSize="sm" fontWeight="medium">
Data output
</Text>
<Badge colorScheme="purple" fontSize="xs">
{data.size} bytes
</Badge>
</HStack>
<Text fontSize="xs" color="gray.400">
Publishes data permanently. Pays no one and can never be spent.
</Text>
<Button
size="xs"
variant="link"
mt={1}
onClick={() => setOpen((shown) => !shown)}
>
{open ? "Hide contents" : "Show contents"}
</Button>

{open && (
<Stack spacing={2} mt={2}>
{data.pushes ? (
data.pushes.map((push, index) => (
<Box key={index}>
{push.text !== undefined && (
<Text fontSize="xs" wordBreak="break-all">
{sanitizeForDisplay(push.text)}
</Text>
)}
<Code
display="block"
fontSize="xs"
p={1}
borderRadius="md"
wordBreak="break-all"
>
{push.hex}
</Code>
</Box>
))
) : (
<Code
display="block"
fontSize="xs"
p={1}
borderRadius="md"
wordBreak="break-all"
>
{data.payloadHex}
</Code>
)}
<Text fontSize="xs" color="gray.500">
Content supplied by the requesting app. Photonic shows it; it does
not vouch for what it means.
</Text>
</Stack>
)}
</Box>
);
}

function OutputRow({ output }: { output: PsbtOutputSummary }) {
return (
<Flex
Expand All @@ -106,9 +189,13 @@ function OutputRow({ output }: { output: PsbtOutputSummary }) {
gap={3}
>
<Box minW={0} flex={1}>
<Text fontSize="sm" noOfLines={1} title={output.script}>
{output.address ?? "(non-standard output)"}
</Text>
{output.data ? (
<DataOutputDetail data={output.data} />
) : (
<Text fontSize="sm" noOfLines={1} title={output.script}>
{output.address ?? "(unrecognised script)"}
</Text>
)}
{output.mine && (
<Badge colorScheme="green" fontSize="xs" mt={1}>
To your wallet
Expand Down Expand Up @@ -184,8 +271,8 @@ export default function PsbtRequestPanel({
Requested by
</Text>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">
{request.app ? `${request.app} — ` : ""}
{request.origin ?? "(no origin provided)"}
{request.app ? `${sanitizeForDisplay(request.app)} — ` : ""}
{request.origin ? sanitizeForDisplay(request.origin) : "(no origin provided)"}
</Code>
</Box>
) : (
Expand Down Expand Up @@ -288,7 +375,7 @@ export default function PsbtRequestPanel({
{autoReturn && (
<Text textStyle="small" mt={2}>
After approving you will be sent back to{" "}
{request.app || "the app"} at <b>{request.origin}</b>, which
{request.app ? sanitizeForDisplay(request.app) : "the app"} at <b>{sanitizeForDisplay(request.origin ?? "")}</b>, which
receives the result automatically.
</Text>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { previewSwapAccept, type SwapAcceptPreview } from "@app/connect/swapFlow
import { electrumStatus } from "@app/signals";
import { ElectrumStatus } from "@app/types";
import type { SwapAcceptRequest } from "@app/connect/protocol";
import { sanitizeForDisplay } from "@lib/displayText";

export default function SwapAcceptRequestPanel({
request,
Expand Down Expand Up @@ -98,8 +99,8 @@ export default function SwapAcceptRequestPanel({
Requested by
</Text>
<Text fontSize="sm" wordBreak="break-all">
{request.app ? `${request.app} — ` : ""}
{request.origin ?? "(no origin provided)"}
{request.app ? `${sanitizeForDisplay(request.app)} — ` : ""}
{request.origin ? sanitizeForDisplay(request.origin) : "(no origin provided)"}
</Text>
</Box>
) : (
Expand Down Expand Up @@ -216,7 +217,7 @@ export default function SwapAcceptRequestPanel({
{autoReturn && (
<Text textStyle="small" mt={3}>
After approving you will be sent back to{" "}
{request.app || "the app"} at <b>{request.origin}</b>, which
{request.app ? sanitizeForDisplay(request.app) : "the app"} at <b>{sanitizeForDisplay(request.origin ?? "")}</b>, which
receives the result automatically.
</Text>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import TokenContent from "@app/components/TokenContent";
import db from "@app/db";
import { SwapStatus } from "@app/types";
import { photonsToRXD } from "@lib/format";
import { sanitizeForDisplay } from "@lib/displayText";
import type { SwapCancelRequest } from "@app/connect/protocol";

export default function SwapCancelRequestPanel({
Expand Down Expand Up @@ -64,8 +65,8 @@ export default function SwapCancelRequestPanel({
Requested by
</Text>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">
{request.app ? `${request.app} — ` : ""}
{request.origin ?? "(no origin provided)"}
{request.app ? `${sanitizeForDisplay(request.app)} — ` : ""}
{request.origin ? sanitizeForDisplay(request.origin) : "(no origin provided)"}
</Code>
</Box>
) : (
Expand Down Expand Up @@ -126,7 +127,7 @@ export default function SwapCancelRequestPanel({
{autoReturn && (
<Text textStyle="small" mt={3}>
After approving you will be sent back to{" "}
{request.app || "the app"} at <b>{request.origin}</b>, which
{request.app ? sanitizeForDisplay(request.app) : "the app"} at <b>{sanitizeForDisplay(request.origin ?? "")}</b>, which
receives the result automatically.
</Text>
)}
Expand Down
7 changes: 4 additions & 3 deletions packages/app/src/components/connect/SwapOfferRequestPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Card from "@app/components/Card";
import TokenContent from "@app/components/TokenContent";
import db from "@app/db";
import type { SwapOfferRequest } from "@app/connect/protocol";
import { sanitizeForDisplay } from "@lib/displayText";

export default function SwapOfferRequestPanel({
request,
Expand Down Expand Up @@ -58,8 +59,8 @@ export default function SwapOfferRequestPanel({
Requested by
</Text>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">
{request.app ? `${request.app} — ` : ""}
{request.origin ?? "(no origin provided)"}
{request.app ? `${sanitizeForDisplay(request.app)} — ` : ""}
{request.origin ? sanitizeForDisplay(request.origin) : "(no origin provided)"}
</Code>
</Box>
) : (
Expand Down Expand Up @@ -118,7 +119,7 @@ export default function SwapOfferRequestPanel({
{autoReturn && (
<Text textStyle="small" mt={3}>
After approving you will be sent back to{" "}
{request.app || "the app"} at <b>{request.origin}</b>, which
{request.app ? sanitizeForDisplay(request.app) : "the app"} at <b>{sanitizeForDisplay(request.origin ?? "")}</b>, which
receives the offer automatically.
</Text>
)}
Expand Down
33 changes: 28 additions & 5 deletions packages/app/src/pages/Connect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
import { withSwapWif, withWif } from "@app/wallet";
import { signMessageWithWif } from "@lib/sign";
import { PsbtError, psbtFromBase64, type Psbt } from "@lib/psbt";
import { hasUnsafeDisplayChars, sanitizeForDisplay } from "@lib/displayText";
import {
buildCallbackUrl,
buildErrorCallbackUrl,
Expand Down Expand Up @@ -776,6 +777,13 @@ function RequestPanel({
onReject: () => void;
}) {
const recognized = isRecognizedConnectChallenge(request.challenge);
// Bidi overrides and invisible characters can make this screen read as one
// thing while the signature covers another. They are replaced below; this
// says so, because a silently cleaned string is still a lie by omission.
const hiddenFormatting =
hasUnsafeDisplayChars(request.challenge) ||
hasUnsafeDisplayChars(request.app ?? "") ||
hasUnsafeDisplayChars(request.origin ?? "");
const addressMismatch =
!!request.address && request.address !== signerAddress;

Expand Down Expand Up @@ -811,8 +819,8 @@ function RequestPanel({
Requested by
</Text>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">
{request.app ? `${request.app} — ` : ""}
{request.origin ?? "(no origin provided)"}
{request.app ? `${sanitizeForDisplay(request.app)} — ` : ""}
{request.origin ? sanitizeForDisplay(request.origin) : "(no origin provided)"}
</Code>
</Box>
) : (
Expand All @@ -839,6 +847,21 @@ function RequestPanel({
</Alert>
)}

{hiddenFormatting && (
<Alert status="warning" mb={4} borderRadius="lg">
<AlertIcon />
<Box>
<AlertTitle fontSize="sm">Contains hidden formatting</AlertTitle>
<AlertDescription fontSize="sm">
This request contains characters that can hide or reorder text
on screen. They are shown as {"\uFFFD"} below. Read what you are
signing carefully, and reject it if it does not look like what
you expected.
</AlertDescription>
</Box>
</Alert>
)}

<Text textStyle="label" mb={1}>
Message to sign
</Text>
Expand All @@ -851,7 +874,7 @@ function RequestPanel({
whiteSpace="pre-wrap"
wordBreak="break-all"
>
{request.challenge}
{sanitizeForDisplay(request.challenge)}
</Code>

<Text textStyle="label" mb={1}>
Expand All @@ -874,8 +897,8 @@ function RequestPanel({

{autoReturn && (
<Text textStyle="small" mt={4}>
After signing you will be sent back to {request.app || "the app"} at{" "}
<b>{request.origin}</b>, which receives your address and signature
After signing you will be sent back to {request.app ? sanitizeForDisplay(request.app) : "the app"} at{" "}
<b>{sanitizeForDisplay(request.origin ?? "")}</b>, which receives your address and signature
automatically.
</Text>
)}
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/pages/SignAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import { isNonceConsumed, consumeNonce } from "@app/connect/consumedNonces";
import SignTxAction from "@app/pages/SignTxAction";
import type { SelectableInput } from "@lib/coinSelect";
import type { UnfinalizedInput } from "@lib/types";
import { sanitizeForDisplay } from "@lib/displayText";

const DEV = import.meta.env.DEV === true;
/** Bound both API fetches; a hung request must not hold a spend page open. */
Expand Down Expand Up @@ -576,10 +577,10 @@ function SignCoreAction() {
verify a page there initiated it. `origin` is attacker-writable, so
"requested by" would assert provenance we can't prove. */}
<Text textStyle="label" mb={1}>Signing for</Text>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">{req.origin}</Code>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">{sanitizeForDisplay(req.origin ?? "")}</Code>
<Text color="text.muted" fontSize="xs" mt={1}>
Any website can open this screen. Only continue if you just started this
action on {req.origin}.
action on {sanitizeForDisplay(req.origin ?? "")}.
</Text>
</Box>
<Box mb={4}>
Expand Down
5 changes: 3 additions & 2 deletions packages/app/src/pages/SignTxAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { p2pkhScript } from "@lib/script";
import { photonsToRXD } from "@lib/format";
import { transferRadiant } from "@lib/transfer";
import type { SelectableInput } from "@lib/coinSelect";
import { sanitizeForDisplay } from "@lib/displayText";
import { useLiveQuery } from "dexie-react-hooks";
import { isNativePlatform } from "@app/platform";
import {
Expand Down Expand Up @@ -420,10 +421,10 @@ export default function SignTxAction() {
<Card2 p={6} mb={4}>
<Box mb={4}>
<Text textStyle="label" mb={1}>Signing for</Text>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">{req.origin}</Code>
<Code w="100%" p={2} borderRadius="md" wordBreak="break-all">{sanitizeForDisplay(req.origin ?? "")}</Code>
<Text color="text.muted" fontSize="xs" mt={1}>
Any website can open this screen. Only continue if you just started this send
on {req.origin}.
on {sanitizeForDisplay(req.origin ?? "")}.
</Text>
</Box>
<Box mb={4}>
Expand Down
Loading
Loading