Skip to content

Commit

Permalink
feat: initial support jwt signing on ledger
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Jun 20, 2022
1 parent c6c8c52 commit 5f4696e
Show file tree
Hide file tree
Showing 51 changed files with 689 additions and 869 deletions.
27 changes: 16 additions & 11 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@ledgerhq/hw-transport-webusb": "6.24.1",
"@noble/secp256k1": "1.5.5",
"@reach/alert": "0.15.3",
"@reach/auto-id": "0.15.3",
"@reach/rect": "0.15.3",
Expand All @@ -100,26 +101,28 @@
"@sentry/react": "6.16.1",
"@sentry/tracing": "6.16.1",
"@stacks/blockchain-api-client": "4.0.1",
"@stacks/common": "4.0.1",
"@stacks/connect": "6.7.0",
"@stacks/auth": "4.1.0",
"@stacks/common": "4.1.0",
"@stacks/connect": "6.8.1",
"@stacks/connect-ui": "5.5.0",
"@stacks/encryption": "4.0.1",
"@stacks/network": "4.0.1",
"@stacks/encryption": "4.1.0",
"@stacks/network": "4.1.0",
"@stacks/rpc-client": "1.0.3",
"@stacks/storage": "4.0.1",
"@stacks/transactions": "4.0.1",
"@stacks/storage": "4.1.2",
"@stacks/transactions": "4.1.0",
"@stacks/ui": "7.10.0",
"@stacks/ui-core": "7.3.0",
"@stacks/ui-theme": "7.5.0",
"@stacks/ui-utils": "7.5.0",
"@stacks/wallet-sdk": "4.0.1",
"@stacks/wallet-sdk": "4.1.2",
"@styled-system/theme-get": "5.1.2",
"@tippyjs/react": "4.2.6",
"@vkontakte/vk-qr": "2.0.13",
"@zondax/ledger-blockstack": "0.2.0",
"@zondax/ledger-blockstack": "0.24.0",
"are-passive-events-supported": "1.1.1",
"argon2-browser": "1.18.0",
"assert": "2.0.0",
"base64url": "3.0.1",
"bignumber.js": "9.0.2",
"bip32": "2.0.6",
"bitcoinjs-lib": "5.2.0",
Expand All @@ -130,6 +133,7 @@
"dayjs": "1.10.7",
"dns-packet": "5.3.0",
"downshift": "6.1.7",
"ecdsa-sig-formatter": "1.0.11",
"fast-deep-equal": "3.1.3",
"formik": "2.2.9",
"http-server": "14.0.0",
Expand Down Expand Up @@ -176,7 +180,7 @@
"@emotion/cache": "11.7.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.4",
"@schemastore/web-manifest": "0.0.5",
"@stacks/connect-react": "16.0.0",
"@stacks/connect-react": "17.0.1",
"@stacks/eslint-config": "1.0.10",
"@stacks/prettier-config": "0.0.10",
"@stacks/stacks-blockchain-api-types": "0.65.0",
Expand Down Expand Up @@ -214,7 +218,7 @@
"@types/webpack": "5.28.0",
"@types/webpack-dev-server": "4.5.0",
"@types/zxcvbn": "4.4.1",
"audit-ci": "5.1.2",
"audit-ci": "6.3.0",
"babel-loader": "8.2.3",
"base64-loader": "1.0.0",
"bip39": "3.0.4",
Expand Down Expand Up @@ -277,7 +281,8 @@
"resolutions": {
"**/**/prismjs": "1.27.0",
"**/**/xmldom": "github:xmldom/xmldom#0.7.0",
"**/**/@stacks/network": "4.0.0",
"**/**/@stacks/network": "4.1.0",
"**/**/@stacks/transactions": "4.1.0",
"@redux-devtools/cli/**/tar": "4.4.18",
"@types/react": "17.0.37",
"@types/react-dom": "17.0.11",
Expand Down
3 changes: 2 additions & 1 deletion src/app/common/actions/finalize-auth-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface FinalizeAuthParams {
authResponse: string;
authRequest: string;
}

/**
* Call this function at the end of onboarding.
*
Expand Down Expand Up @@ -42,7 +43,7 @@ export const finalizeAuthResponse = ({
};
chrome.tabs.sendMessage(tabId, responseMessage);
deleteTabForRequest(StorageKey.authenticationRequests, authRequest);
// window.close();
window.close();
} catch (error) {
logger.debug('Failed to get Tab ID for authentication request:', authRequest);
throw new Error(
Expand Down
24 changes: 9 additions & 15 deletions src/app/common/signature/requests.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,17 @@
import { Account, getAppPrivateKey } from '@stacks/wallet-sdk';
import { getAppPrivateKey } from '@stacks/wallet-sdk';
import { SignaturePayload } from '@stacks/connect';
import { decodeToken, TokenVerifier } from 'jsontokens';
import { getPublicKeyFromPrivate } from '@stacks/encryption';
import { getAddressFromPrivateKey, TransactionVersion } from '@stacks/transactions';
import { AccountWithAddress } from '@app/store/accounts/account.models';

export function getPayloadFromToken(requestToken: string) {
const token = decodeToken(requestToken);
return token.payload as unknown as SignaturePayload;
}

function getTransactionVersionFromRequest(signature: SignaturePayload) {
const { network } = signature;
if (!network) return TransactionVersion.Mainnet;
if (![TransactionVersion.Mainnet, TransactionVersion.Testnet].includes(network.version)) {
throw new Error('Invalid network version provided');
}
return network.version;
}

const UNAUTHORIZED_SIGNATURE_REQUEST =
'The signature request provided is not signed by this wallet.';

/**
* Verify a transaction request.
* A transaction request is a signed JWT that is created on an app,
Expand All @@ -43,7 +35,7 @@ const UNAUTHORIZED_SIGNATURE_REQUEST =
*/
interface VerifySignatureRequestArgs {
requestToken: string;
accounts: Account[];
accounts: AccountWithAddress[];
appDomain: string;
}
export async function verifySignatureRequest({
Expand All @@ -54,22 +46,24 @@ export async function verifySignatureRequest({
const token = decodeToken(requestToken);
const signature = token.payload as unknown as SignaturePayload;
const { publicKey, stxAddress } = signature;
const txVersion = getTransactionVersionFromRequest(signature);
const verifier = new TokenVerifier('ES256k', publicKey);
const isSigned = await verifier.verifyAsync(requestToken);
if (!isSigned) {
throw new Error('Signature request is not signed');
}
const foundAccount = accounts.find(account => {
if (account.type === 'ledger') {
throw new Error('sdlkjsdlkf');
}
const appPrivateKey = getAppPrivateKey({
account,
appDomain,
});
const appPublicKey = getPublicKeyFromPrivate(appPrivateKey);
if (appPublicKey !== publicKey) return false;
if (!stxAddress) return true;
const accountStxAddress = getAddressFromPrivateKey(account.stxPrivateKey, txVersion);
if (stxAddress !== accountStxAddress) return false;

if (stxAddress !== account.address) return false;
return true;
});
if (!foundAccount) {
Expand Down
20 changes: 10 additions & 10 deletions src/app/common/transactions/generate-unsigned-txs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface GenerateUnsignedTxArgs<TxPayload> {
nonce?: number;
}

export type ContractCallPayload = Omit<ConnectContractCallPayload, 'network'> &
type ContractCallPayload = Omit<ConnectContractCallPayload, 'network'> &
TempCorrectNetworkPackageType;
type GenerateUnsignedContractCallTxArgs = GenerateUnsignedTxArgs<ContractCallPayload>;

Expand All @@ -62,8 +62,8 @@ function generateUnsignedContractCallTx(args: GenerateUnsignedContractCallTxArgs
publicKey,
anchorMode: anchorMode ?? AnchorMode.Any,
functionArgs: fnArgs,
nonce: initNonce(nonce),
fee: new BN(fee, 10),
nonce: initNonce(nonce)?.toString(),
fee: new BN(fee, 10).toString(),
postConditionMode: postConditionMode,
postConditions: getPostConditions(postConditions),
network,
Expand All @@ -72,7 +72,7 @@ function generateUnsignedContractCallTx(args: GenerateUnsignedContractCallTxArgs
return makeUnsignedContractCall(options);
}

export type ContractDeployPayload = Omit<ConnectContractDeployPayload, 'network'> &
type ContractDeployPayload = Omit<ConnectContractDeployPayload, 'network'> &
TempCorrectNetworkPackageType;
type GenerateUnsignedContractDeployTxArgs = GenerateUnsignedTxArgs<ContractDeployPayload>;

Expand All @@ -82,8 +82,8 @@ function generateUnsignedContractDeployTx(args: GenerateUnsignedContractDeployTx
const options = {
contractName,
codeBody,
nonce: initNonce(nonce),
fee: new BN(fee, 10),
nonce: initNonce(nonce)?.toString(),
fee: new BN(fee, 10)?.toString(),
publicKey,
anchorMode: anchorMode ?? AnchorMode.Any,
postConditionMode: postConditionMode,
Expand All @@ -93,7 +93,7 @@ function generateUnsignedContractDeployTx(args: GenerateUnsignedContractDeployTx
return makeUnsignedContractDeploy(options);
}

export type STXTransferPayload = Omit<ConnectSTXTransferPayload, 'network'> &
type STXTransferPayload = Omit<ConnectSTXTransferPayload, 'network'> &
TempCorrectNetworkPackageType;
type GenerateUnsignedStxTransferTxArgs = GenerateUnsignedTxArgs<STXTransferPayload>;

Expand All @@ -105,9 +105,9 @@ function generateUnsignedStxTransferTx(args: GenerateUnsignedStxTransferTxArgs)
memo,
publicKey,
anchorMode: anchorMode ?? AnchorMode.Any,
amount: new BN(amount),
nonce: initNonce(nonce),
fee: new BN(fee, 10),
amount: new BN(amount).toString(),
nonce: initNonce(nonce)?.toString(),
fee: new BN(fee, 10).toString(),
network,
};
return makeUnsignedSTXTokenTransfer(options);
Expand Down
83 changes: 27 additions & 56 deletions src/app/common/unsafe-auth-response.ts
Original file line number Diff line number Diff line change
@@ -1,64 +1,35 @@
import { makeDIDFromAddress } from '@stacks/auth';
import { makeUUID4, nextMonth } from '@stacks/common';
import { publicKeyToAddress } from '@stacks/encryption';
import { createUnsecuredToken } from 'jsontokens';
import base64url from 'base64url';

export async function makeUnsafeAuthResponse(
publicKey: string,
// eslint-disable-next-line @typescript-eslint/ban-types
profile: {} = {},
username: string | null = null,
_metadata: any | null,
coreToken: string | null = null,
_appPrivateKey: string | null = null,
expiresAt: number = nextMonth().getTime(),
_transitPublicKey: string | null = null,
_hubUrl: string | null = null,
_blockstackAPIUrl: string | null = null,
_associationToken: string | null = null
): Promise<string> {
const address = publicKeyToAddress(publicKey);
export async function makeLedgerCompatibleUnsignedAuthResponsePayload({
dataPublicKey,
profile = {},
expiresAt = nextMonth().getTime(),
}: {
dataPublicKey: string;
profile: any;
expiresAt?: number;
}): Promise<string> {
const address = publicKeyToAddress(dataPublicKey);

// /* See if we should encrypt with the transit key */
// let privateKeyPayload = appPrivateKey;
const coreTokenPayload = coreToken;
const additionalProperties = {};
// if (appPrivateKey !== undefined && appPrivateKey !== null) {
// // Logger.info(`blockstack.js: generating v${VERSION} auth response`)
// if (transitPublicKey !== undefined && transitPublicKey !== null) {
// privateKeyPayload = await encryptPrivateKey(transitPublicKey, appPrivateKey);
// if (coreToken !== undefined && coreToken !== null) {
// coreTokenPayload = await encryptPrivateKey(transitPublicKey, coreToken);
// }
// }
// additionalProperties = {
// email: metadata?.email ? metadata.email : null,
// profile_url: metadata?.profileUrl ? metadata.profileUrl : null,
// hubUrl,
// blockstackAPIUrl,
// associationToken,
// version: VERSION,
// };
// } else {
// // Logger.info('blockstack.js: generating legacy auth response')
// }
const payload = {
jti: makeUUID4(),
iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds
exp: Math.floor(expiresAt / 1000), // JWT times are in seconds
iss: makeDIDFromAddress(address),
public_keys: [dataPublicKey],
profile,
};

/* Create the payload */
const payload = Object.assign(
{},
{
jti: makeUUID4(),
iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds
exp: Math.floor(expiresAt / 1000), // JWT times are in seconds
iss: makeDIDFromAddress(address),
// private_key: privateKeyPayload,
public_keys: [publicKey],
profile,
username,
core_token: coreTokenPayload,
},
additionalProperties
);
const header = { typ: 'JWT', alg: 'ES256K' };

return createUnsecuredToken(payload);
const formedHeader = base64url.encode(JSON.stringify(header));

const formedPayload = base64url.encode(JSON.stringify(payload));

const inputToSign = [formedHeader, formedPayload].join('.');

return inputToSign;
}
12 changes: 12 additions & 0 deletions src/app/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,15 @@ export function getFullyQualifiedAssetName(asset?: AssetWithMeta) {
export function doesBrowserSupportWebUsbApi() {
return Boolean((navigator as any).usb);
}

const isFullPage = document.location.pathname.startsWith('/index.html');

const pageMode = isFullPage ? 'full' : 'popup';

type PageMode = 'popup' | 'full';

type WhenPageModeMap<T> = Record<PageMode, T>;

export function whenPageMode<T>(pageModeMap: WhenPageModeMap<T>) {
return pageModeMap[pageMode];
}
9 changes: 7 additions & 2 deletions src/app/common/utils/open-in-new-tab.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { isValidUrl } from '@app/common/validation/validate-url';
import { RouteUrls } from '@shared/route-urls';

export const openInNewTab = (url: string) => {
export function openInNewTab(url: string) {
if (!isValidUrl(url)) return;
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (newWindow) newWindow.opener = null;
};
}

export function openIndexPageInNewTab(path: RouteUrls | string) {
return chrome.tabs.create({ url: chrome.runtime.getURL('index.html#' + path) });
}
Loading

0 comments on commit 5f4696e

Please sign in to comment.