Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multisig: Connect request and SignMultisigTransaction request #458

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ebda8a0
Add test for signing a multisig transaction
sisou Sep 26, 2022
6e72643
Add `signPartially` method to Key and use in test
sisou Sep 26, 2022
5611eb9
MonkeyPatch MerkleTree to verify multisig sender address
sisou Sep 26, 2022
e556d8e
[WIP] Add SignMultisigTransaction request
sisou Sep 26, 2022
44667a4
Add demo for multisig signing
sisou Sep 27, 2022
a3f4e21
Add multisig config badge to AddressInfo component
sisou Sep 27, 2022
a466a31
Add user and account name section
sisou Sep 29, 2022
d18c38c
Update title wording
sisou May 22, 2023
2176d0b
Demo of generating random RSA keys in a sandboxed iframe
sisou Oct 7, 2022
17fef90
Use node-forge in sandboxed iframe to generate deterministic RSA key
sisou Oct 7, 2022
3ede4d8
Use ArrayBuffers to exchange key material
sisou Oct 10, 2022
3e341cd
Extend 32-byte entropy to 1024-byte seed with PBKDF2
sisou Oct 10, 2022
149d0ff
Refactor Key config into an object and extend with rsaKeyPair option
sisou Oct 11, 2022
db20209
Add RSA key computation to Key class
sisou Oct 11, 2022
08686bd
Implement encrypted secret parsing and decryption
sisou Oct 11, 2022
8c8ad9e
Add Rust code compiled to WASM for secret aggregation
sisou Oct 11, 2022
8df25f5
Update multisig badge UI, show identicon for legacy accounts
sisou Oct 12, 2022
c6d71f4
Connect request
sisou Oct 22, 2023
4c02473
Extract LoginFileAccountIcon into a component
sisou Oct 18, 2022
2f5a2fe
Refactor multisig request type structure
sisou Oct 19, 2022
e6a2c73
Add comment about permission additions
sisou Oct 21, 2022
a33e4e5
Narrow encryptionKey algorithm type
sisou Oct 21, 2022
d6f1212
Add sandboxed iframe files to dist during build
sisou Oct 24, 2022
1d5ba94
Extract inline-script from RSAKeysIframe
sisou Oct 24, 2022
8768bee
Add explainer tooltip to connect UI
sisou May 22, 2023
5e61073
Clean-up, improve error handling
sisou Oct 22, 2023
14537af
Fix types for KeyguardCommand
sisou Dec 1, 2022
ef6abfb
Allow RSA key generation to use dynamic parameters
sisou Dec 5, 2022
e2a2743
Run yarn-deduplicate
sisou Jun 19, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.min.*
src/lib/bitcoin/BitcoinJS.js
src/lib/polygon/OpenGSN.js
src/lib/multisig/wasm/pkg
10 changes: 10 additions & 0 deletions client/src/KeyguardClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ReleaseKeyRequest,
ResetPasswordRequest,
SignTransactionRequest,
SignMultisigTransactionRequest,
SignMessageRequest,
SimpleRequest,
IFrameRequest,
Expand All @@ -39,6 +40,7 @@ import {
SignSwapRequest,
SignSwapTransactionsRequest,
SignSwapTransactionsResult,
ConnectRequest,
} from './PublicRequest';

import Observable from './Observable';
Expand Down Expand Up @@ -120,6 +122,10 @@ export class KeyguardClient {
this._redirectRequest<SignTransactionRequest>(KeyguardCommand.SIGN_TRANSACTION, request);
}

public signMultisigTransaction(request: SignMultisigTransactionRequest) {
this._redirectRequest<SignMultisigTransactionRequest>(KeyguardCommand.SIGN_MULTISIG_TRANSACTION, request);
}

public deriveAddress(request: DeriveAddressRequest) {
this._redirectRequest<DeriveAddressRequest>(KeyguardCommand.DERIVE_ADDRESS, request);
}
Expand Down Expand Up @@ -148,6 +154,10 @@ export class KeyguardClient {
this._redirectRequest<SignSwapRequest>(KeyguardCommand.SIGN_SWAP, request);
}

public connectAccount(request: ConnectRequest) {
this._redirectRequest<ConnectRequest>(KeyguardCommand.CONNECT_ACCOUNT, request);
}

/* IFRAME REQUESTS */

public async list(): Promise<ListResult> {
Expand Down
2 changes: 2 additions & 0 deletions client/src/KeyguardCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ export enum KeyguardCommand {
EXPORT = 'export',
CHANGE_PASSWORD = 'change-password',
SIGN_TRANSACTION = 'sign-transaction',
SIGN_MULTISIG_TRANSACTION = 'sign-multisig-transaction',
SIGN_MESSAGE = 'sign-message',
CONNECT_ACCOUNT = 'connect',
DERIVE_ADDRESS = 'derive-address',

// Bitcoin
Expand Down
71 changes: 69 additions & 2 deletions client/src/PublicRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export type BitcoinTransactionInfo = {
};

export type SignTransactionRequestLayout = 'standard' | 'checkout' | 'cashlink';
export type SignMultisigTransactionRequestLayout = 'standard';
export type SignBtcTransactionRequestLayout = 'standard' | 'checkout';

// Specific Requests
Expand Down Expand Up @@ -188,6 +189,42 @@ export type SignTransactionRequest
| SignTransactionRequestCheckout
| SignTransactionRequestCashlink;

export type EncryptionKeyParams = {
kdf: string,
iterations: number,
keySize: number,
};

export type MultisigConfig = {
publicKeys: Uint8Array[],
numberOfSigners: number,
signerPublicKeys: Uint8Array[],
secret: {
aggregatedSecret: Uint8Array,
} | {
encryptedSecrets: Uint8Array[],
bScalar: Uint8Array,
keyParams: EncryptionKeyParams,
},
aggregatedCommitment: Uint8Array,
userName?: string,
};

export type SignMultisigTransactionRequestCommon = Transform<SignTransactionRequestCommon, 'keyLabel' | 'senderLabel', {
keyLabel: string, // Not optional
senderLabel: string, // Not optional
}> & {
multisigConfig: MultisigConfig,
};

export type SignMultisigTransactionRequestStandard = SignMultisigTransactionRequestCommon & {
layout?: 'standard',
recipientLabel?: string,
};

export type SignMultisigTransactionRequest
= SignMultisigTransactionRequestStandard;

export type SignBtcTransactionRequestStandard = SimpleRequest & BitcoinTransactionInfo & {
layout?: 'standard',
};
Expand Down Expand Up @@ -452,6 +489,24 @@ export type DerivePolygonAddressResult = {
}>,
};

export type ConnectRequest = SimpleRequest & {
appLogoUrl: string,
permissions: KeyguardCommand[],
requestedKeyPaths: string[],
challenge: string,
};

export type ConnectResult = {
signatures: SignatureResult[],
encryptionKey: {
format: 'spki',
keyData: Uint8Array,
algorithm: { name: string, hash: string },
keyUsages: ['encrypt'],
keyParams: EncryptionKeyParams,
},
};

// Request unions

export type RedirectRequest
Expand All @@ -461,9 +516,11 @@ export type RedirectRequest
| ImportRequest
| RemoveKeyRequest
| SignMessageRequest
| ConnectRequest
| SignTransactionRequest
| SignBtcTransactionRequest
| SignPolygonTransactionRequest
| SignMultisigTransactionRequest
| SimpleRequest
| DeriveBtcXPubRequest
| DerivePolygonAddressRequest
Expand Down Expand Up @@ -502,6 +559,7 @@ export type KeyResult = SingleKeyResult[];
export type ListResult = KeyInfoObject[];
export type ListLegacyResult = LegacyKeyInfoObject[];
export type SignTransactionResult = SignatureResult;
export type SignMultisigTransactionResult = SignatureResult;
export type SimpleResult = { success: boolean };
export type SignedBitcoinTransaction = {
transactionHash: string,
Expand Down Expand Up @@ -532,7 +590,10 @@ export type RedirectResult
= DerivedAddress[]
| ExportResult
| KeyResult
| SignatureResult
| ConnectResult
| SignTransactionResult
| SignMultisigTransactionResult
| SignedBitcoinTransaction
| SignedPolygonTransaction
| SimpleResult
Expand All @@ -545,7 +606,10 @@ export type Result = RedirectResult | IFrameResult;
// Derived Result types

export type ResultType<T extends RedirectRequest> =
T extends Is<T, SignMessageRequest> | Is<T, SignTransactionRequest> ? SignatureResult :
T extends Is<T, SignMessageRequest> ? SignatureResult :
T extends Is<T, SignTransactionRequest> ? SignTransactionResult :
T extends Is<T, SignMultisigTransactionRequest> ? SignMultisigTransactionResult :
T extends Is<T, ConnectRequest> ? ConnectResult :
T extends Is<T, DeriveAddressRequest> ? DerivedAddress[] :
T extends Is<T, CreateRequest> | Is<T, ImportRequest> | Is<T, ResetPasswordRequest> ? KeyResult :
T extends Is<T, ExportRequest> ? ExportResult :
Expand All @@ -558,7 +622,10 @@ export type ResultType<T extends RedirectRequest> =
never;

export type ResultByCommand<T extends KeyguardCommand> =
T extends KeyguardCommand.SIGN_MESSAGE | KeyguardCommand.SIGN_TRANSACTION ? SignatureResult :
T extends KeyguardCommand.SIGN_MESSAGE ? SignatureResult :
T extends KeyguardCommand.SIGN_TRANSACTION ? SignTransactionResult :
T extends KeyguardCommand.SIGN_MULTISIG_TRANSACTION ? SignMultisigTransactionResult :
T extends KeyguardCommand.CONNECT_ACCOUNT ? ConnectResult :
T extends KeyguardCommand.DERIVE_ADDRESS ? DerivedAddress[] :
T extends KeyguardCommand.CREATE | KeyguardCommand.IMPORT ? KeyResult :
T extends KeyguardCommand.EXPORT ? ExportResult :
Expand Down
20 changes: 5 additions & 15 deletions client/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -327,21 +327,16 @@
dependencies:
"@types/node" "*"

"@types/node@*":
version "10.7.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.7.1.tgz#b704d7c259aa40ee052eec678758a68d07132a2e"
integrity sha512-EGoI4ylB/lPOaqXqtzAyL8HcgOuCtH2hkEaLmkueOYufsTFWBn4VCvlCDC2HW8Q+9iF+QVC3sxjDKQYjHQeZ9w==
"@types/node@*", "@types/node@^12.12.6", "@types/node@^12.6.1":
version "12.20.55"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==

"@types/node@^10.3.2":
version "10.17.60"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b"
integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==

"@types/node@^12.12.6", "@types/node@^12.6.1":
version "12.20.55"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240"
integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==

"@types/pbkdf2@^3.0.0":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@types/pbkdf2/-/pbkdf2-3.1.0.tgz#039a0e9b67da0cdc4ee5dab865caa6b267bb66b1"
Expand Down Expand Up @@ -1773,12 +1768,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"

inherits@2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=

inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4:
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
Expand Down
119 changes: 119 additions & 0 deletions demos/ConnectAccount.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SignTransaction | Keyguard Demo</title>
<link href="../src/nimiq-style.css" rel="stylesheet">
<script src="../node_modules/@nimiq/core-web/web-offline.js"></script>
<script src="../src/lib/KeyInfo.js"></script>
<script src="../src/lib/KeyStore.js"></script>
<script src="../node_modules/@nimiq/rpc/dist/rpc.umd.js"></script>

<style>
.row {
margin: 1rem 0;
}

label, input {
display: block;
margin: 5px;
}

#result {
max-width: 900px;
overflow-wrap: break-word;
}
</style>
</head>
<body>

<form class="center">
<div class="row">
<label>Account to connect</label>
<select id="account"></select>
</div>

<div class="row">
<label>Challenge</label>
<input id="challenge" value="some random characters">
</div>

<div class="row">
<label>Permissions</label>
<label>
<input type="checkbox" id="permission-sign-multisig-transaction" checked> sign-multisig-transaction
</label>
</div>

<!-- <br><button id="redirect" type="button" class="small" disabled>Connect Account (redirect)</button> -->
<br><button id="popup" type="button" class="small">Connect Account (popup)</button>

<p id="result"></p>
</form>

<script>
const accountSelector = document.querySelector('#account');

async function loadAccounts() {
/** @type {KeyInfo[]} */
const keyInfos = await KeyStore.instance.list();
const dom = document.createDocumentFragment();
for (const keyInfo of keyInfos) {
const option = document.createElement('option');
option.value = keyInfo.id;
option.textContent = keyInfo.defaultAddress.toUserFriendlyAddress();
dom.appendChild(option);
}
accountSelector.appendChild(dom);
}
loadAccounts();

document.querySelector('button#popup').addEventListener('click', async () => {
connectAccountPopup(await generateRequest());
});

async function generateRequest() {
const keyId = accountSelector.value;
const challenge = document.querySelector('#challenge').value;
const permissions = ['sign-multisig-transaction']; // TODO

const request = {
appName: 'Nimiq Multisig',

keyId,
keyLabel: 'Some Account',

appLogoUrl: `${window.location.origin}/demos/multisig-logo.svg`,
permissions,
requestedKeyPaths: [`m/44'/242'/0'/0'`],
challenge,
};

return request;
}

// function connectAccountRedirect(txRequest) {
// return client.connect(txRequest, RedirectRequestBehavior.withLocalState({ keyId: txRequest.keyId }));
// }

async function connectAccountPopup(txRequest) {
const keyguard = window.open('../src/request/connect/', 'ConnectAccount Demo',
`left=${window.innerWidth / 2 - 350},top=75,width=700,height=850,location=yes,dependent=yes`);
const rpc = new Rpc.PostMessageRpcClient(keyguard, '*');
await rpc.init();

try {
const result = await rpc.call('request', txRequest);
console.log('Keyguard result:', result);
document.querySelector('#result').textContent = 'TX signed: ' + JSON.stringify(result);
} catch (e) {
console.error('Keyguard error', e);
document.querySelector('#result').textContent = `Error: ${e.message || e}`;
}

keyguard.close();
}
</script>

</body>
</html>