Skip to content

Commit

Permalink
Merge pull request #3365 from LiskHQ/3364-sign-message
Browse files Browse the repository at this point in the history
Enable users to sign messages using hardware wallets - Closes #3364
  • Loading branch information
reyraa authored Feb 10, 2021
2 parents 0405072 + 08a0503 commit 19fc18c
Show file tree
Hide file tree
Showing 10 changed files with 359 additions and 131 deletions.
3 changes: 3 additions & 0 deletions i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@
"Maximum floating point is 8.": "Maximum floating point is 8.",
"Message": "Message",
"Message (optional)": "Message (optional)",
"Message signature aborted on device": "Message signature aborted on device",
"Min": "Min",
"Minimize": "Minimize",
"Missed block": "Missed block",
Expand Down Expand Up @@ -319,6 +320,7 @@
"Please carefully write down these 12 words and store them in a safe place.": "Please carefully write down these 12 words and store them in a safe place.",
"Please check the address": "Please check the address",
"Please check the highlighted word and make sure it’s correct.": "Please check the highlighted word and make sure it’s correct.",
"Please confirm the message on your {{model}}": "Please confirm the message on your {{model}}",
"Please select the account you’d like to sign in to or": "Please select the account you’d like to sign in to or",
"Please sign in": "Please sign in",
"Please use the last not-initialized account before creating a new one!": "Please use the last not-initialized account before creating a new one!",
Expand Down Expand Up @@ -425,6 +427,7 @@
"The aggregated LSK volume transferred on the given time period.": "The aggregated LSK volume transferred on the given time period.",
"The current list only reflects the peers connected to the Lisk Service node.": "The current list only reflects the peers connected to the Lisk Service node.",
"The message can't contain whitespace at the beginning or end.": "The message can't contain whitespace at the beginning or end.",
"The message signature has been canceled on your {{model}}": "The message signature has been canceled on your {{model}}",
"The number of transactions submitted on the given time period.": "The number of transactions submitted on the given time period.",
"The sign message tool allows you to prove ownership of a transaction": "The sign message tool allows you to prove ownership of a transaction",
"The signature is correct": "The signature is correct",
Expand Down
20 changes: 20 additions & 0 deletions libs/hwManager/communication.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,25 @@ const signTransaction = async (data) => {
return response;
};

/**
* signMessage - Function.
* Use for sign a random message
* @param {object} data -> Object that contain the information about the device and data
* @param {string} data.deviceId -> Id of the hw device
* @param {number} data.index -> index of the account of which will extract information
* @param {object} data.message -> Object with all transaction information
*/
const signMessage = async (data) => {
const response = await executeCommand(
IPC_MESSAGES.HW_COMMAND,
{
action: IPC_MESSAGES.SIGN_MSG,
data,
},
);
return response;
};

/**
* checkIfInsideLiskApp - Function.
* To check if Lisk App is open on the device
Expand Down Expand Up @@ -143,4 +162,5 @@ export {
subscribeToDevicesList,
validatePin,
getAddress,
signMessage,
};
2 changes: 2 additions & 0 deletions libs/hwManager/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ export const IPC_MESSAGES = {
HW_DISCONNECTED: 'hwDisconnected',
MISSING_PIN: 'pin_not_provided_from_ui',
SIGN_TRANSACTION: 'SIGN_TX',
SIGN_MSG: 'SIGN_MSG',
VALIDATE_PIN: 'validateTrezorPin',
};
export const FUNCTION_TYPES = {
[IPC_MESSAGES.GET_PUBLIC_KEY]: 'getPublicKey',
[IPC_MESSAGES.GET_ADDRESS]: 'getAddress',
[IPC_MESSAGES.SIGN_TRANSACTION]: 'signTransaction',
[IPC_MESSAGES.SIGN_MSG]: 'signMessage',
};
17 changes: 17 additions & 0 deletions libs/hwManager/manufacturers/ledger/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,10 +146,27 @@ const signTransaction = async (transporter, { device, data }) => {
}
};

// eslint-disable-next-line max-statements
const signMessage = async (transporter, { device, data }) => {
let transport = null;
try {
transport = await transporter.open(device.path);
const liskLedger = new DposLedger(transport);
const ledgerAccount = getLedgerAccount(data.index);
const signature = await liskLedger.signMSG(ledgerAccount, data.message);
transport.close();
return getBufferToHex(signature.slice(0, 64));
} catch (error) {
if (transport) transport.close();
throw new Error(error);
}
};

export default {
checkIfInsideLiskApp,
getAddress,
getPublicKey,
listener,
signTransaction,
signMessage,
};
25 changes: 25 additions & 0 deletions libs/hwManager/manufacturers/trezor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ const signTransaction = (transporter, { device, data }) => {
});
};

const signMessage = (transporter, { device, data }) => {
const trezorDevice = transporter.asArray().find(d => d.features.device_id === device.deviceId);
if (!trezorDevice) Promise.reject(new Error('DEVICE_IS_NOT_CONNECTED'));

return new Promise((resolve, reject) => {
trezorDevice.waitForSessionAndRun(async (session) => {
try {
const { message } = await session.typedCall(
'LiskSignMessage',
'LiskMessageSignature',
{
address_n: getHardenedPath(data.index),
// eslint-disable-next-line new-cap
message: new Buffer.from(data.message, 'utf8').toString('hex'),
},
);
return resolve(message.signature);
} catch (err) {
return reject();
}
});
});
};

const checkIfInsideLiskApp = async ({ device }) => device;

export default {
Expand All @@ -140,4 +164,5 @@ export default {
getPublicKey,
listener,
signTransaction,
signMessage,
};
174 changes: 121 additions & 53 deletions src/components/screens/signMessage/confirmMessage.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,84 @@
import React from 'react';
import React, { useEffect, useRef, useState } from 'react';
import liskClient from 'Utils/lisk-client'; // eslint-disable-line
import CopyToClipboard from 'react-copy-to-clipboard';
import styles from './signMessage.css';
import Box from '../../toolbox/box';
import BoxInfoText from '../../toolbox/box/infoText';
import BoxContent from '../../toolbox/box/content';
import BoxFooter from '../../toolbox/box/footer';
import BoxHeader from '../../toolbox/box/header';
import { AutoResizeTextarea } from '../../toolbox/inputs';
import { SecondaryButton, PrimaryButton } from '../../toolbox/buttons';
import { loginType } from '../../../constants/hwConstants';
import { signMessageByHW } from '../../../utils/hwManager';
import LoadingIcon from '../hwWalletLogin/loadingIcon';

class ConfirmMessage extends React.Component {
constructor(props) {
super(props);
const ConfirmationPending = ({ t, account }) => (
<BoxContent className={styles.noPadding}>
<BoxInfoText className={styles.pendingWrapper}>
<span>
{t('Please confirm the message on your {{model}}', { model: account.hwInfo.deviceModel })}
</span>
<LoadingIcon />
</BoxInfoText>
</BoxContent>
);

this.state = {
copied: false,
};
const Error = ({ t }) => (
<BoxContent className={styles.noPadding}>
<BoxInfoText>
<span>
{t('Message signature aborted on device')}
</span>
</BoxInfoText>
</BoxContent>
);

this.copy = this.copy.bind(this);
}
const Result = ({
t, signature, copied, copy, prevStep,
}) => (
<>
<BoxContent className={styles.noPadding}>
<AutoResizeTextarea
className={`${styles.result} result`}
value={signature}
readOnly
/>
</BoxContent>
<BoxFooter direction="horizontal">
<SecondaryButton onClick={prevStep} className={styles.button}>
{t('Go back')}
</SecondaryButton>
<CopyToClipboard
onCopy={copy}
text={signature}
>
<PrimaryButton disabled={copied} className={styles.button}>
{copied ? t('Copied!') : t('Copy to clipboard')}
</PrimaryButton>
</CopyToClipboard>
</BoxFooter>
</>
);

copy() {
this.setState({ copied: true });
this.timeout = setTimeout(() => this.setState({ copied: false }), 3000);
}
const ConfirmMessage = ({
apiVersion,
prevStep,
message,
account,
t,
}) => {
const [copied, setCopied] = useState(false);
const [signature, setSignature] = useState();
const [error, setError] = useState();
const ref = useRef();

componentWillUnmount() {
clearTimeout(this.timeout);
}
const copy = () => {
setCopied(true);
ref.current = setTimeout(() => setCopied(false), 3000);
};

sign() {
const Lisk = liskClient(this.props.apiVersion);
const { message, account } = this.props;
const signUsingPassphrase = (Lisk) => {
const signedMessage = Lisk.cryptography.signMessageWithPassphrase(
message,
account.passphrase,
Expand All @@ -43,40 +90,61 @@ class ConfirmMessage extends React.Component {
signature: signedMessage.signature,
});
return result;
}
};

render() {
const { t, prevStep } = this.props;
const { copied } = this.state;
const result = this.sign();
return (
<Box>
<BoxHeader>
<h1>{t('Sign a message')}</h1>
</BoxHeader>
<BoxContent className={styles.noPadding}>
<AutoResizeTextarea
className={`${styles.result} result`}
value={result}
readOnly
/>
</BoxContent>
<BoxFooter direction="horizontal">
<SecondaryButton onClick={prevStep} className={styles.button}>
{t('Go back')}
</SecondaryButton>
<CopyToClipboard
onCopy={this.copy}
text={result}
>
<PrimaryButton disabled={copied} className={styles.button}>
{copied ? t('Copied!') : t('Copy to clipboard')}
</PrimaryButton>
</CopyToClipboard>
</BoxFooter>
</Box>
);
}
}
const signUsingHW = async (Lisk) => {
const signedMessage = await signMessageByHW({
account,
message,
});
const result = Lisk.cryptography.printSignedMessage({
message,
publicKey: account.publicKey,
signature: signedMessage,
});
return result;
};

useEffect(() => {
const Lisk = liskClient(apiVersion);
if (account.loginType === loginType.normal) {
setSignature(signUsingPassphrase(Lisk));
} else {
signUsingHW(Lisk)
.then(setSignature)
.catch(setError);
}
return () => clearTimeout(ref.current);
}, []);

const confirmationPending = account.loginType !== loginType.normal && !error && !signature;

return (
<Box>
<BoxHeader>
<h1>{t('Sign a message')}</h1>
</BoxHeader>
{
confirmationPending ? <ConfirmationPending t={t} account={account} /> : null
}
{
error ? <Error t={t} /> : null
}
{
!error && !confirmationPending
? (
<Result
t={t}
signature={signature}
copied={copied}
copy={copy}
prevStep={prevStep}
/>
)
: null
}
</Box>
);
};

export default ConfirmMessage;
Loading

0 comments on commit 19fc18c

Please sign in to comment.