Skip to content

feat: add qr keyring package #60

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

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open

feat: add qr keyring package #60

wants to merge 44 commits into from

Conversation

mikesposito
Copy link
Member

@mikesposito mikesposito commented Sep 25, 2024

This is a new Keyring implementation compatible with QR-based wallets, meant to replace @keystonehq/metamask-airgapped-keyring.

The goals are:

  • Internalize the package
  • Simplify internal logic (e.g. no event-based logic)
  • Remove the need of KeyringController special methods (See methods nuked in chore(keyring-controller): remove QRKeyring-related code core#6031)
  • Provide a consumer-injectable transport bridge to allow clients to control QR scanning implementation
  • Improve API ergonomics
  • Allow clients to easily support multiple QR devices per user

This package can be tested on Extension through this PR: MetaMask/metamask-extension#33893

How does it work?

The QrKeyring in this package is split into two main components:

  • QrKeyring: The main keyring class that exposes the Keyring API. Some of its logic is implemented in internal utility classes:
    • Device: A stateful internal wrapper for the keyring's public keys derivation logic and account management. In the code it's implemented by a separate class, but it can be seen as an internal component of the keyring, and It is not exported from the package.
  • QrKeyringBridge: The interface that a transport bridge must implement: it allows the client to control how QR codes are scanned. It's supposed to be implemented by a class that is injected by the client: when using KeyringController, it can be injected through a custom keyring builder (See this keyring builder that will be used by Extension).
    • QrKeyringScannerBridge: A generic bridge that allows the client to pass function hooks at construction time, which will be called when the keyring needs to scan a QR code.

Device pairing flow

Pairing is the process of initializing the keyring with a device, which is done by submitting the device details to the keyring in the form of a serialized UR, which usually comes from the "sync" QR scanned on the device. The client can trigger this process in three ways:

  • At construction time, by passing a SerializedUR object to the QrKeyring constructor.
    • In this case, the scan is done before the keyring is constructed, and thus the keyring will directly receive the QR contents.
  • By calling the QrKeyring.pairDevice() method, which allows the client to pass a SerializedUR object at any time.
    • Same as above, the scan is done outside the keyring, and the keyring will directly receive the QR contents.
  • By calling the QrKeyring.getFirstPage() method, which will trigger a scan request if no device is paired yet.
    • In this case, the keyring will request a scan through the bridge, and the client will implement the scan logic.

Extension uses getFirstPage() to trigger the pairing flow, which will then request a scan through the bridge

Like for other Hardare wallets, the pairing flow is initiated in connectHardware() method. See the sequence diagram below for the pairing flow on Extension, and see the Extension requestScan callback for the implementation of the scan logic.

sequenceDiagram
        autonumber
        participant A as MM Extension
        create participant B as QrKeyring
        A->>B: getFirstPage()
        alt if not initialized
        create participant C as Bridge
        B->>C: requestScan(PAIRING)
        C-->>+A: 
        note over A,C: The client controls the bridge and implements the scan logic
        A-->>-C: 
        destroy C
        C->>B: return CBOR 
        create participant D as Int. derivation logic
        B->>D: init(CBOR)
        end
        B->>D: accountAtIndex(0..4)
        destroy D
        D->>B: return accounts
        destroy B
        B->>A: return accounts
Loading

Mobile

TBD - Likely using .pairDevice() given the current implementation of QR interactions on mobile.

Signing flow

sequenceDiagram
        autonumber
        participant A as Client
        create participant B as QrKeyring
        A->>B: sign{Transaction, PersonalMessage, TypedData}()
        alt not initialized
        B->>A: Fail
        end
        create participant C as Bridge
        B->>C: requestScan(SIGN)
        C-->>+A: 
        note over A,C: The client controls the bridge and implements the scan logic
        A-->>-C: 
        destroy C
        C->>B: return signature CBOR 
        destroy B
        B->>A: return hex signature
Loading

Account addition flow

No scan required after the initial pairing.

sequenceDiagram
        autonumber
        participant A as Client
        alt non sequential account addition 
        create participant B as QrKeyring
        A->>B: setAccountToUnlock( i )
        end
        A->>B: addAccounts( n )
        loop n-times
        create participant D as Int. derivation logic
        B->>D: accountAtIndex( i )
        destroy D
        D->>B: return account
        end
        B->>A: return created accounts
Loading

Examples

@danroc
Copy link
Contributor

danroc commented Sep 25, 2024

Thank you @mikesposito! Maybe we should import this using git merge --allow-unrelated-histories (+ manual fixes) to keep the history from the original repo.

@mikesposito
Copy link
Member Author

mikesposito commented Sep 25, 2024

@danroc this PR is not moving the package from another source - this is a new implementation, and only shares a similar account derivation strategy from URs with the original keyring package currently used on clients

Do you think we should bring in its commit history anyway? Also, the package is in a Keystone monorepo, which would include commits to other packages potentially unrelated

@danroc
Copy link
Contributor

danroc commented Sep 25, 2024

Sorry @mikesposito, I didn't realise it was a new implementation! In this case it makes sense to start fresh, thank you for the clarification.

Copy link

This PR is marked as stale because it has been open for 60 days with no activity. Please remove the stale label or leave a comment, or it will be closed in 14 days.

@github-actions github-actions bot added the Stale label Nov 25, 2024
@danroc danroc removed the Stale label Nov 28, 2024
Copy link

socket-security bot commented Feb 12, 2025

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​keystonehq/​metamask-airgapped-keyring@​0.15.2971007183100
Added@​keystonehq/​bc-ur-registry-eth@​0.19.11001007283100
Updatedcross-spawn@​7.0.6 ⏵ 7.0.39985 -1599 +380100
Updated@​metamask/​utils@​9.3.0 ⏵ 9.2.1991009389 -5100

View full report

Copy link

socket-security bot commented Feb 12, 2025

Caution

Review the following alerts detected in dependencies.

According to your organization's Security Policy, you must resolve all "Block" alerts before proceeding. Learn more about Socket for GitHub.

Action Severity Alert (click for details)
Block High
[email protected] has a High CVE.

CVE: GHSA-3xgq-45jj-v275 Regular Expression Denial of Service (ReDoS) in cross-spawn (HIGH)

Affected versions: >= 7.0.0 < 7.0.5; < 6.0.6

Patched version: 7.0.5

From: yarn.locknpm/[email protected]

ℹ Read more on: This package | This alert | What is a CVE?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Remove or replace dependencies that include known high severity CVEs. Consumers can use dependency overrides or npm audit fix --force to remove vulnerable dependencies.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Block Low
[email protected] has a New author.

New Author: ljharb

Previous Author: lukechilds

From: ?npm/@keystonehq/[email protected]npm/[email protected]

ℹ Read more on: This package | This alert | What is new author?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at [email protected].

Suggestion: Scrutinize new collaborator additions to packages because they now have the ability to publish code into your dependency tree. Packages should avoid frequent or unnecessary additions or changes to publishing rights.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/[email protected]. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

This comment has been minimized.

@mikesposito

This comment has been minimized.

@mikesposito

This comment has been minimized.

@mikesposito

This comment has been minimized.

* @returns An array of IndexedAddress objects, each containing the address and its index
* @throws Will throw an error if the source is not initialized
*/
getAddressesPage(page: number, pageSize = 5): IndexedAddress[] {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, I just noticed that the original package uses a different function to build pages when in "account" mode: https://github.com/KeystoneHQ/keystone-sdk-base/blob/22815365ecafac31c5845b4cc9d48c44985b11b8/packages/base-eth-keyring/src/BaseKeyring.ts#L382

I probably need to adapt this code to have the same behavior in "account" mode.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though, looking at this closely, it does seem that __getLedgerLivePage doesn't do anything different compared to __getNormalPage, besides skipping index caching. We are probably good to go in terms of how close this code is to the original.

It does seem that in Account mode the keyring should only list and unlock specific indexes passed from the device at pair time, throwing error when calling addressForIndex for missing indexes: this means that in cases where there is an incomplete accounts page in Account mode the user will be unable to fetch that page

@mikesposito mikesposito marked this pull request as ready for review July 8, 2025 08:41
@mikesposito mikesposito requested review from a team as code owners July 8, 2025 08:41
cursor[bot]

This comment was marked as resolved.

@mikesposito mikesposito marked this pull request as draft July 8, 2025 08:47
@mikesposito mikesposito marked this pull request as ready for review July 8, 2025 09:01
@dawnseeker8 dawnseeker8 requested a review from mcmire July 8, 2025 09:35
mcmire
mcmire previously approved these changes Jul 8, 2025
Copy link
Contributor

@mcmire mcmire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@mikesposito
Copy link
Member Author

@metamaskbot publish-preview

Copy link

github-actions bot commented Jul 9, 2025

Preview builds have been published. See these instructions (from the core monorepo) for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/keyring-api": "18.0.0-a9b0f29",
  "@metamask-previews/eth-hd-keyring": "12.1.0-a9b0f29",
  "@metamask-previews/eth-ledger-bridge-keyring": "11.1.1-a9b0f29",
  "@metamask-previews/eth-qr-keyring": "0.0.0-a9b0f29",
  "@metamask-previews/eth-simple-keyring": "10.0.0-a9b0f29",
  "@metamask-previews/eth-trezor-keyring": "9.0.0-a9b0f29",
  "@metamask-previews/keyring-internal-api": "6.2.0-a9b0f29",
  "@metamask-previews/keyring-internal-snap-client": "4.1.0-a9b0f29",
  "@metamask-previews/eth-snap-keyring": "13.0.0-a9b0f29",
  "@metamask-previews/keyring-snap-client": "5.0.0-a9b0f29",
  "@metamask-previews/keyring-snap-sdk": "4.0.0-a9b0f29",
  "@metamask-previews/keyring-utils": "3.0.0-a9b0f29"
}

@mikesposito
Copy link
Member Author

@metamaskbot publish-preview

Copy link

Preview builds have been published. See these instructions (from the core monorepo) for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/keyring-api": "18.0.0-36c61e7",
  "@metamask-previews/eth-hd-keyring": "12.1.0-36c61e7",
  "@metamask-previews/eth-ledger-bridge-keyring": "11.1.1-36c61e7",
  "@metamask-previews/eth-qr-keyring": "0.0.0-36c61e7",
  "@metamask-previews/eth-simple-keyring": "10.0.0-36c61e7",
  "@metamask-previews/eth-trezor-keyring": "9.0.0-36c61e7",
  "@metamask-previews/keyring-internal-api": "6.2.0-36c61e7",
  "@metamask-previews/keyring-internal-snap-client": "4.1.0-36c61e7",
  "@metamask-previews/eth-snap-keyring": "13.0.0-36c61e7",
  "@metamask-previews/keyring-snap-client": "5.0.0-36c61e7",
  "@metamask-previews/keyring-snap-sdk": "4.0.0-36c61e7",
  "@metamask-previews/keyring-utils": "3.0.0-36c61e7"
}

cursor[bot]

This comment was marked as resolved.

@mikesposito
Copy link
Member Author

@metamaskbot publish-preview

Copy link

Preview builds have been published. See these instructions (from the core monorepo) for more information about preview builds.

Expand for full list of packages and versions.
{
  "@metamask-previews/keyring-api": "18.0.0-d2e6996",
  "@metamask-previews/eth-hd-keyring": "12.1.0-d2e6996",
  "@metamask-previews/eth-ledger-bridge-keyring": "11.1.1-d2e6996",
  "@metamask-previews/eth-qr-keyring": "0.0.0-d2e6996",
  "@metamask-previews/eth-simple-keyring": "10.0.0-d2e6996",
  "@metamask-previews/eth-trezor-keyring": "9.0.0-d2e6996",
  "@metamask-previews/keyring-internal-api": "6.2.0-d2e6996",
  "@metamask-previews/keyring-internal-snap-client": "4.1.0-d2e6996",
  "@metamask-previews/eth-snap-keyring": "13.0.0-d2e6996",
  "@metamask-previews/keyring-snap-client": "5.0.0-d2e6996",
  "@metamask-previews/keyring-snap-sdk": "4.0.0-d2e6996",
  "@metamask-previews/keyring-utils": "3.0.0-d2e6996"
}

cursor[bot]

This comment was marked as resolved.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Incorrect Transaction Casting Causes Errors

The code incorrectly casts non-legacy transactions to FeeMarketEIP1559Transaction. The @ethereumjs/tx library supports other types, such as AccessListEIP2930Transaction (EIP-2930). Since the getMessageToSign() method is available on all TypedTransaction types, this cast is unnecessary and can lead to runtime errors or incorrect behavior.

packages/keyring-eth-qr/src/device.ts#L339-L348

const dataType =
transaction.type === TransactionType.Legacy
? DataType.transaction
: DataType.typedTransaction;
const messageToSign = Buffer.from(
transaction.type === TransactionType.Legacy
? RLP.encode(transaction.getMessageToSign())
: (transaction as FeeMarketEIP1559Transaction).getMessageToSign(),
);

Fix in CursorFix in Web


Was this report helpful? Give feedback by reacting with 👍 or 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Internalize simplified QRKeyring implementation
3 participants