Skip to content

Commit

Permalink
Merge pull request #97 from spacemeshos/feat-85-message-signing
Browse files Browse the repository at this point in the history
Add message signing and verifying
  • Loading branch information
brusherru authored Oct 3, 2024
2 parents a34e5c1 + 4a61a05 commit 7cb0ad9
Show file tree
Hide file tree
Showing 9 changed files with 536 additions and 60 deletions.
145 changes: 93 additions & 52 deletions src/components/KeyManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
IconKey,
IconPlus,
IconTrash,
IconWritingSign,
} from '@tabler/icons-react';

import useConfirmation from '../hooks/useConfirmation';
Expand Down Expand Up @@ -64,6 +65,7 @@ import ImportKeyFromLedgerModal from './ImportKeyFromLedgerModal';
import ImportKeyPairModal from './ImportKeyPairModal';
import RenameKeyModal from './RenameKeyModal';
import RevealSecretKeyModal from './RevealSecretKeyModal';
import SignMessageModal from './SignMessageModal';

type KeyManagerProps = {
isOpen: boolean;
Expand Down Expand Up @@ -117,9 +119,11 @@ function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
const createAccountModal = useDisclosure();
const importAccountModal = useDisclosure();
const editAccountModal = useDisclosure();
const signMessageModal = useDisclosure();

const [renameKeyIdx, setRenameKeyIdx] = useState(0);
const [editAccountIdx, setEditAccountIdx] = useState(0);
const [signMessageByKeyIndex, setSignMessageByKeyIndex] = useState(0);

const closeHandler = () => {
onClose();
Expand Down Expand Up @@ -241,59 +245,91 @@ function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
backgroundColor="blackAlpha.300"
borderRadius="md"
>
{key.type === KeyPairType.Software ? (
<Button
size="xx-small"
p={1}
fontSize="xx-small"
fontWeight="normal"
float="right"
display="flex"
alignItems="center"
borderRadius="sm"
textTransform="uppercase"
gap={1}
onClick={() => revealSecretKey(key)}
variant="ghostWhite"
>
<IconKey size={12} />
Export secret key
</Button>
) : (
<Badge
p={1}
fontSize="xx-small"
fontWeight="normal"
float="right"
display="flex"
alignItems="center"
colorScheme="orange"
gap={1}
>
<IconDeviceUsb size={12} />
Hardware
</Badge>
)}
<Text fontWeight="bold" mb={1}>
<Flex
float="right"
flexDir={{ base: 'column', sm: 'row-reverse' }}
alignItems="flex-end"
gap={{ base: 1, sm: 2 }}
>
{key.type === KeyPairType.Software ? (
<>
<Button
size="xx-small"
p={1}
fontSize="xx-small"
fontWeight="normal"
display="flex"
alignItems="center"
borderRadius="sm"
textTransform="uppercase"
gap={1}
onClick={() => revealSecretKey(key)}
variant="ghostWhite"
>
<IconKey size={12} />
Export secret key
</Button>
<Button
size="xx-small"
p={1}
fontSize="xx-small"
fontWeight="normal"
display="flex"
alignItems="center"
borderRadius="sm"
textTransform="uppercase"
gap={1}
onClick={() => {
setSignMessageByKeyIndex(idx);
signMessageModal.onOpen();
}}
variant="ghostWhite"
>
<IconWritingSign size={12} />
Sign message
</Button>
</>
) : (
<Badge
p={1}
fontSize="xx-small"
fontWeight="normal"
display="flex"
alignItems="center"
colorScheme="orange"
gap={1}
>
<IconDeviceUsb size={12} />
Hardware
</Badge>
)}
</Flex>
<Text as="div" fontWeight="bold" mb={1}>
{key.displayName}
<IconButton
ml={2}
aria-label="Rename key"
onClick={() => onRenameKey(idx)}
icon={<IconEdit size={12} />}
variant="whiteOutline"
borderWidth={1}
size="xs"
/>
<IconButton
ml={1}
aria-label="Delete key"
onClick={() => onDeleteKey(idx)}
icon={<IconTrash size={12} />}
variant="dangerOutline"
borderWidth={1}
size="xs"
/>
<Box
display={{ base: 'block', sm: 'inline-block' }}
ml={{ base: 0, sm: 2 }}
my={{ base: 2, sm: 0 }}
>
<IconButton
aria-label="Rename key"
onClick={() => onRenameKey(idx)}
icon={<IconEdit size={12} />}
variant="whiteOutline"
borderWidth={1}
size="xs"
mr={1}
/>
<IconButton
aria-label="Delete key"
onClick={() => onDeleteKey(idx)}
icon={<IconTrash size={12} />}
variant="dangerOutline"
borderWidth={1}
size="xs"
mr={1}
/>
</Box>
</Text>

<Text fontSize="xx-small">Public Key</Text>
Expand Down Expand Up @@ -483,6 +519,11 @@ function KeyManager({ isOpen, onClose }: KeyManagerProps): JSX.Element {
isOpen={editAccountModal.isOpen}
onClose={editAccountModal.onClose}
/>
<SignMessageModal
keyIndex={signMessageByKeyIndex}
isOpen={signMessageModal.isOpen}
onClose={signMessageModal.onClose}
/>
</DrawerBody>
</DrawerContent>
</Drawer>
Expand Down
10 changes: 10 additions & 0 deletions src/components/MainMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { MAIN_MENU_BUTTONS_SIZE } from '../utils/constants';

import KeyManager from './KeyManager';
import MnemonicsModal from './MnemonicsModal';
import VerifyMessageModal from './VerifyMessageModal';
import WipeOutAlert from './WipeOutAlert';

function MainMenu(): JSX.Element {
Expand All @@ -24,6 +25,7 @@ function MainMenu(): JSX.Element {
const wipeAlert = useDisclosure();

const keyManagerDrawer = useDisclosure();
const verifyMessageModal = useDisclosure();
const { revealMnemonics } = useMnemonics();
const iconSize = useBreakpointValue(MAIN_MENU_BUTTONS_SIZE, { ssr: false });

Expand All @@ -46,6 +48,10 @@ function MainMenu(): JSX.Element {
<MenuItem onClick={revealMnemonics}>Backup mnemonic</MenuItem>
<MenuItem onClick={exportWalletFile}>Export wallet file</MenuItem>
<MenuDivider />
<MenuItem onClick={verifyMessageModal.onOpen}>
Verify Message
</MenuItem>
<MenuDivider />
<MenuItem color="red" onClick={wipeAlert.onOpen}>
Wipe out
</MenuItem>
Expand All @@ -57,6 +63,10 @@ function MainMenu(): JSX.Element {
isOpen={keyManagerDrawer.isOpen}
onClose={keyManagerDrawer.onClose}
/>
<VerifyMessageModal
isOpen={verifyMessageModal.isOpen}
onClose={verifyMessageModal.onClose}
/>
</>
);
}
Expand Down
Empty file removed src/components/SignMessage.tsx
Empty file.
175 changes: 175 additions & 0 deletions src/components/SignMessageModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import fileDownload from 'js-file-download';
import { useState } from 'react';
import { useForm } from 'react-hook-form';

import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Spacer,
Text,
Textarea,
} from '@chakra-ui/react';

import useCopy from '../hooks/useCopy';
import { useSignMessage } from '../hooks/useSigning';
import usePassword from '../store/usePassword';
import useWallet from '../store/useWallet';
import { SignedMessage } from '../types/message';
import { KeyPairType } from '../types/wallet';
import { SIGNED_MESSAGE_PREFIX } from '../utils/constants';
import { toHexString } from '../utils/hexString';

type SignMessageModalProps = {
keyIndex: number;
isOpen: boolean;
onClose: () => void;
};

function SignMessageModal({
keyIndex,
isOpen,
onClose,
}: SignMessageModalProps): JSX.Element | null {
const { wallet } = useWallet();
const signMessage = useSignMessage();
const { withPassword } = usePassword();
const { register, reset, handleSubmit } = useForm<{ message: string }>();
const [signResult, setSignResult] = useState('');
const { isCopied, onCopy } = useCopy();

const keys = wallet?.keychain ?? [];
const key = keys[keyIndex];
if (!key) return null;

const close = () => {
setSignResult('');
reset();
onClose();
};

const download = () =>
fileDownload(signResult, `signed-message.json`, 'plain/text');

const submit = handleSubmit(async ({ message }) => {
const result = await withPassword(
async (password) => {
if (key.type === KeyPairType.Hardware) {
// Sign using Ledger device
throw new Error('Hardware wallet is not supported yet');
}
const text = `${SIGNED_MESSAGE_PREFIX}${message}`;
// Sign using local key
return JSON.stringify(
{
publicKey: key.publicKey,
text,
signature: toHexString(
await signMessage(text, key.publicKey, password)
),
} satisfies SignedMessage,
null,
2
);
},
'Sign message',
<>
Please enter your password to sign the message using key &quot;
{key.displayName}&quot;{' '}
<Text as="span" wordBreak="break-all">
({key.publicKey})
</Text>
</>
);
if (result) {
setSignResult(result);
} else {
setSignResult('');
}
});

return (
<Modal isOpen={isOpen} onClose={close} isCentered>
<ModalOverlay />
<ModalContent>
<ModalCloseButton />
<ModalHeader textAlign="center">Sign message</ModalHeader>
{signResult ? (
<>
<ModalBody>
<Text mb={4}>
Here is your message and the signature. You can copy them to the
clipboard and share with another party.
</Text>
<Textarea
readOnly
rows={10}
resize="none"
borderColor="brand.darkGreen"
value={signResult}
translate="no"
fontSize="xx-small"
/>
</ModalBody>
<ModalFooter gap={2}>
<Button
isDisabled={isCopied}
onClick={() => onCopy(signResult)}
variant="whiteModal"
w={20}
>
{isCopied ? 'Copied' : 'Copy'}
</Button>
<Button onClick={download} variant="whiteModal">
Download
</Button>
<Spacer />
<Button onClick={close} variant="whiteModal">
Close
</Button>
</ModalFooter>
</>
) : (
<>
<ModalBody>
<Text mb={4} fontSize="sm">
You are going to sign some text message using key &quot;
{key.displayName}&quot;{' '}
<Text as="span" wordBreak="break-all">
({key.publicKey})
</Text>
:
</Text>
<Textarea
{...register('message', {
required: 'Message cannot be empty',
})}
rows={4}
resize="none"
borderColor="brand.darkGreen"
translate="no"
/>
<Text fontSize="xs" color="gray" mt={2}>
The signature will be generated using the private key.
<br />
Another party may verify the message using your public key.
</Text>
</ModalBody>
<ModalFooter>
<Button onClick={submit} ml={2} variant="whiteModal">
Sign message...
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
);
}

export default SignMessageModal;
Loading

0 comments on commit 7cb0ad9

Please sign in to comment.