diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..765c9d2 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + }, + "ignore": [] +} diff --git a/.github/ISSUE_TEMPLATE/1_general_issue.yml b/.github/ISSUE_TEMPLATE/1_general_issue.yml new file mode 100644 index 0000000..1e2fac6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_general_issue.yml @@ -0,0 +1,26 @@ +name: 💡 General Inquiry & Suggestions +description: Share general questions or suggestions that don't fit other categories +title: "[GENERAL] " +labels: ["question", "enhancement"] +body: + - type: markdown + attributes: + value: "Got a question or suggestion? We're all ears!" + - type: textarea + attributes: + label: Inquiry or Suggestion + description: What would you like to share with us? + placeholder: "I'm wondering about..." + validations: + required: false + - type: input + attributes: + label: Relevant Links or References + description: Optionally, add any relevant links or references. + placeholder: "e.g., https://github.com/example" + - type: checkboxes + attributes: + label: Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct. + required: true diff --git a/.github/ISSUE_TEMPLATE/2_bug_report.yml b/.github/ISSUE_TEMPLATE/2_bug_report.yml new file mode 100644 index 0000000..038107e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_bug_report.yml @@ -0,0 +1,71 @@ +name: 🐛 Bug Report & Test Failures +description: Report unexpected behaviors or failing tests +title: "[BUG] " +labels: ["bug", "help wanted"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! The more info you provide, the more we can help you. + + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true + + - type: input + attributes: + label: Package Version + description: What version of the SDK are you using? + placeholder: 4.1.0 + validations: + required: true + + - type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: false + + - type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: false + + - type: textarea + attributes: + label: Steps To Reproduce + description: Steps or code snippets to reproduce the behavior. + validations: + required: false + + - type: textarea + attributes: + label: Package.json (or lockfile) content + description: Packages used in your project. This will help us understand the environment you are working in, and check if there are dependencies that might be causing the issue. + validations: + required: false + + - type: input + attributes: + label: Link to Minimal Reproducible Example (StackBlitz, CodeSandbox, GitHub repo etc.) + description: | + [Please provide by forking this project](https://stackblitz.com/~/github.com/bcnmy/sdk-examples) and making relevant changes to the boilerplate. This makes investigating issues and helping you out significantly easier! For most issues, you will likely get asked to provide one so why not add one now :) + validations: + required: false + + - type: textarea + attributes: + label: Anything else? + description: | + Browser info? Screenshots? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/3_feature_request.yml b/.github/ISSUE_TEMPLATE/3_feature_request.yml new file mode 100644 index 0000000..ae049ec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/3_feature_request.yml @@ -0,0 +1,53 @@ +name: ✨ Feature Requests & Performance Improvements +description: Suggest a new feature or performance enhancement +title: "[FEATURE] " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: "Your suggestions inspire us to improve. Share your ideas below!" + + - type: markdown + attributes: + value: "## What" + + - type: input + attributes: + label: Feature or Improvement Description + description: Briefly describe the feature or improvement you're suggesting. + placeholder: "e.g., Add support for platform Z." + validations: + required: true + + - type: markdown + attributes: + value: "## Why" + + - type: textarea + attributes: + label: Benefits & Outcomes + description: Explain the benefits of your suggestion and the expected outcomes. + placeholder: "This improvement will improve performance by 30%..." + + - type: markdown + attributes: + value: "## How" + + - type: textarea + attributes: + label: Implementation Ideas + description: Share any thoughts on how this feature could be implemented. + placeholder: "We could approach this by..." + + - type: input + attributes: + label: Any References? + description: Provide links or references to similar features or standards. + placeholder: "EIP-1234, https://github.com/example" + + - type: checkboxes + attributes: + label: Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct. + required: true diff --git a/.github/ISSUE_TEMPLATE/4_security_report.yml b/.github/ISSUE_TEMPLATE/4_security_report.yml new file mode 100644 index 0000000..6fc0ef4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/4_security_report.yml @@ -0,0 +1,31 @@ +name: 🔒 Security Pre-Screening +description: Pre-screening for security-related reports +title: "[SECURITY PRE-SCREEN] " +labels: ["security", "triage needed"] +body: + - type: markdown + attributes: + value: "Security is our top priority. If you've discovered a potential security issue please proceed." + - type: checkboxes + attributes: + label: Security Level Acknowledgement + options: + - label: "I understand this issue will be public. It is NOT critical or high risk and does not endanger deployed contracts. If it is please email: security@biconomy.io" + required: true + - type: input + attributes: + label: Overview + description: Provide a summary of the non-critical security concern or question. + placeholder: "e.g., Questions about the audit process." + validations: + required: true + - type: textarea + attributes: + label: Additional Details + description: Offer more detail on your concern or question. + placeholder: "Provide any additional context..." + - type: input + attributes: + label: Suggestions for Mitigation + description: (Optional) Suggest ways to address the concern. + placeholder: "Potential mitigation steps include..." diff --git a/.github/ISSUE_TEMPLATE/5_document_improvement.yml b/.github/ISSUE_TEMPLATE/5_document_improvement.yml new file mode 100644 index 0000000..a4b0b52 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/5_document_improvement.yml @@ -0,0 +1,33 @@ +name: 📚 Documentation Improvement +description: Propose improvements or report issues with documentation +title: "[DOCS] " +labels: ["documentation"] +body: + - type: markdown + attributes: + value: "Help us enhance our documentation for everyone." + - type: input + attributes: + label: Documentation Page/Section + description: Which page or section are you referring to? + placeholder: "e.g., README.md, TSDoc guidelines." + validations: + required: true + - type: textarea + attributes: + label: Suggested Improvements + description: Detail the improvements or corrections needed. + placeholder: "The section on XYZ could clarify..." + validations: + required: true + - type: input + attributes: + label: Additional Comments + description: Any other comments or suggestions? + placeholder: "Consider adding examples for..." + - type: checkboxes + attributes: + label: Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct. + required: true diff --git a/.github/ISSUE_TEMPLATE/6_build_deployment_issue.yml b/.github/ISSUE_TEMPLATE/6_build_deployment_issue.yml new file mode 100644 index 0000000..e0034b1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/6_build_deployment_issue.yml @@ -0,0 +1,33 @@ +name: 🛠 Build & Deployment Issues +description: Report issues +title: "[BUILD/DEPLOY] " +labels: ["bug", "help wanted"] +body: + - type: markdown + attributes: + value: "Help us identify build or deployment problems to improve our processes." + - type: input + attributes: + label: Issue Summary + description: Briefly describe the issue encountered. + placeholder: "e.g., Failed to deploy contract due to..." + validations: + required: true + - type: textarea + attributes: + label: Error Logs & Messages + description: Provide any error logs or messages seen. + placeholder: "Error: Failed to..." + validations: + required: true + - type: input + attributes: + label: Environment & Tools + description: Mention the tools and environment where the issue occurred. + placeholder: "e.g., Truffle v5.3, Rinkeby testnet" + - type: checkboxes + attributes: + label: Code of Conduct + options: + - label: I agree to follow this project's Code of Conduct. + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 0000000..35e9206 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,22 @@ +## Pull Request for SDK Improvement + +**Describe your changes:** + + + +**Link any related issues:** + + + +**Testing:** + + + +**Note:** Please ensure all tests and lint checks pass before requesting a review. If there are any errors, fix them prior to submission. + +**Checklist:** + +- [ ] I have performed a self-review of my own code. +- [ ] I have added tests that prove my fix is effective or that my feature works. +- [ ] I have made corresponding changes to the documentation, if applicable. +- [ ] My changes generate no new warnings or errors. diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml new file mode 100644 index 0000000..1cd962c --- /dev/null +++ b/.github/actions/build/action.yml @@ -0,0 +1,16 @@ +name: "Build" +description: "Prepare repository, all dependencies and build" + +runs: + using: "composite" + steps: + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + + - name: Install dependencies + shell: bash + run: bun install --frozen-lockfile + + - name: Build + shell: bash + run: bun run build diff --git a/.github/actions/install-dependencies/action.yml b/.github/actions/install-dependencies/action.yml new file mode 100644 index 0000000..3dbb2bd --- /dev/null +++ b/.github/actions/install-dependencies/action.yml @@ -0,0 +1,13 @@ +name: "Install dependencies" +description: "Prepare repository and all dependencies" + +runs: + using: "composite" + steps: + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + + - name: Install dependencies + shell: bash + run: | + bun install --frozen-lockfile diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..96f601a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,14 @@ +name: build +on: + workflow_dispatch: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] +jobs: + build: + name: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build + uses: ./.github/actions/build diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..239974f --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,30 @@ +name: deploy docs +on: + workflow_dispatch: + push: + branches: + - main + +permissions: + contents: write +jobs: + deploy-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: git config --global user.email "gh@runner.com" + - run: git config --global user.name "gh-runner" + + - name: Install dependencies + uses: ./.github/actions/install-dependencies + + - name: Set remote url + run: git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/bcnmy/sdk.git + + - name: Run the tests + run: bun run docs:deploy + + - name: Deploy 🚀 + uses: JamesIves/github-pages-deploy-action@v4 + with: + folder: ./docs diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml new file mode 100644 index 0000000..d575b7c --- /dev/null +++ b/.github/workflows/pr-lint.yml @@ -0,0 +1,19 @@ +name: pr lint +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review, edited] +jobs: + enforce_title: + name: pr lint + runs-on: ubuntu-latest + steps: + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - uses: actions/checkout@v3 + - name: Install dependencies + uses: ./.github/actions/install-dependencies + + - name: Use commitlint to check PR title + run: echo "${{ github.event.pull_request.title }}" | bun commitlint diff --git a/.github/workflows/size-report.yml b/.github/workflows/size-report.yml new file mode 100644 index 0000000..053e2d8 --- /dev/null +++ b/.github/workflows/size-report.yml @@ -0,0 +1,38 @@ +name: size report +on: + workflow_dispatch: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + size-report: + name: size report + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: write-all + + steps: + - name: Clone repository + uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Set up Bun + uses: oven-sh/setup-bun@v1 + + - name: Install dependencies + shell: bash + run: | + bun install --frozen-lockfile + + - name: Report bundle size + uses: andresz1/size-limit-action@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + package_manager: bun diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a8fedd --- /dev/null +++ b/.gitignore @@ -0,0 +1,180 @@ +_cjs +_esm +_types + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Caches + +.cache + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +coverage +lib-cov + +# Coverage directory used by tools like istanbul +*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +docs + +sessionStorageData \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6d58094 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at graeme.barnes@biconomy.io or joe.pegler@biconomy.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other project leadership members. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://contributor-covenant.org/version/1/4][version] + +[homepage]: https://contributor-covenant.org +[version]: https://contributor-covenant.org/version/1/4/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..84dbcc8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Biconomy + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..78c25ca --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +[![Biconomy](https://img.shields.io/badge/Made_with_%F0%9F%8D%8A_by-Biconomy-ff4e17?style=flat)](https://biconomy.io) [![License MIT](https://img.shields.io/badge/License-MIT-blue?&style=flat)](./LICENSE) [![codecov](https://codecov.io/github/bcnmy/passkey/graph/badge.svg?token=DTdIR5aBDA)](https://codecov.io/github/bcnmy/passkey) + +# @biconomy/passkey 🚀 + +A WebAuthn-based passkey validator module for Biconomy's SDK (@biconomy/sdk). Enable secure transaction signing using device biometrics in your Web3 applications. + +## Key Features + +- 🔐 WebAuthn-based transaction signing +- 📜 ERC-7579 compliant module implementation +- 🤝 Seamless integration with Biconomy's Nexus smart accounts +- 🔄 Support for both registration and login flows +- 👆 Native device biometrics support + +## Installation + +Choose your preferred package manager: + +```bash +# npm +npm i @biconomy/passkey + +# yarn +yarn add @biconomy/passkey + +# pnpm +pnpm i @biconomy/passkey + +# bun +bun i @biconomy/passkey +``` + +# Quick Start Guide +### Here's a complete example showing how to set up and use the passkey validator: + +```typescript +import { toWebAuthnKey, WebAuthnMode, toPasskeyValidator } from "@biconomy/passkey" +import { createNexusClient, moduleActivator } from "@biconomy/sdk" +import { http } from "viem" +import { baseSepolia } from "viem/chains" + +// 1. Initial Setup +const account = privateKeyToAccount('0x...') +const chain = baseSepolia +const bundlerUrl = 'https://bundler.biconomy.io/api/v3/84532/nJPK7B3ru.dd7f7861-190d-41bd-af80-6877f74b8f44' + +// 2. Create Nexus Client +const nexusClient = await createNexusClient({ + signer: account, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) +}) + +// 3. Setup WebAuthn Credentials +const webAuthnKey = await toWebAuthnKey({ + passkeyName: "my-passkey", // Your passkey identifier + mode: WebAuthnMode.Register // Use .Login for existing passkeys +}) + +// 4. Initialize Passkey Validator +const passkeyValidator = await toPasskeyValidator({ + webAuthnKey, + signer: account, + accountAddress: nexusClient.account.address, + chainId: chain.id +}) + +// 5. Install Validator Module +const opHash = await nexusClient.installModule({ module: passkeyValidator }) +await nexusClient.waitForUserOperationReceipt({ hash: opHash }) + +// 6. Activate the Validator +nexusClient.extend(moduleActivator(passkeyValidator)) + +// 7. Send a Transaction +const tx = await nexusClient.sendTransaction({ + to: "0x...", + value: 1 +}) +``` \ No newline at end of file diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5200997 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,57 @@ +# Security Policy + +## Reporting a Vulnerability + +The safety and security of our sdk is our top priority. If you have discovered a security vulnerability, we appreciate your help in disclosing it to us responsibly. + +### Contact Us Directly for Critical or High-Risk Findings + +For critical or high-impact vulnerabilities that could affect our users, **please contact us directly** at: + +- Email: security@biconomy.io + +We'll work with you to assess and understand the scope of the issue. + +### For Other Issues + +For vulnerabilities that are less critical and do not immediately affect our users: + +1. Open an issue in our GitHub repository (`https://github.com/bcnmy/passkey/issues`). + +2. Provide detailed information about the issue and steps to reproduce. + +If your findings are eligible for a bounty, we will follow up with you on the payment process. + +### Scope + +The bounty program covers code in the `main` branch of our repository. The vulnerability must not have already been addressed or fixed in the `develop` branch. + +### Eligibility + +To be eligible for a bounty, researchers must: + +- Report a security bug that has not been previously reported. + +- Not violate our testing policies (detailed below). + +- Follow responsible disclosure guidelines. + +### Testing Policies + +- Do not conduct testing on the mainnet or public testnets. Local forks should be used for testing. + +- Avoid testing that generates significant traffic or could lead to denial of service. + +- Do not disclose the vulnerability publicly until we have had the chance to address it. + +### Out of Scope + +- Known issues listed in the issue tracker or already fixed in the `develop` branch. + +- Issues in third-party components. + +## Legal Notice + +By submitting a vulnerability report, you agree to comply with our responsible disclosure process. Public disclosure of the vulnerability without consent from us will render the vulnerability ineligible for a bounty. + +Thank you for helping to keep Biconomy 🍊 and the blockchain community safe! diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..2626939 --- /dev/null +++ b/biome.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", + "files": { + "ignore": [ + "package.json", + "node_modules", + "**/node_modules", + "cache", + "coverage", + "tsconfig.json", + "tsconfig.*.json", + "_cjs", + "_esm", + "_types", + "bun.lockb", + "docs", + "dist" + ] + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "warn" + }, + "style": { + "noUnusedTemplateLiteral": "warn" + } + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": true, + "lineWidth": 80, + "indentWidth": 2, + "indentStyle": "space" + }, + "javascript": { + "formatter": { + "semicolons": "asNeeded", + "trailingComma": "none" + } + } +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..ca253c8 Binary files /dev/null and b/bun.lockb differ diff --git a/config.json b/config.json new file mode 100644 index 0000000..765c9d2 --- /dev/null +++ b/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { + "onlyUpdatePeerDependentsWhenOutOfRange": true + }, + "ignore": [] +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..394319c --- /dev/null +++ b/package.json @@ -0,0 +1,92 @@ +{ + "name": "@biconomy/passkey", + "version": "0.0.1", + "author": "Biconomy", + "repository": "github:bcnmy/passkey", + "main": "./dist/_cjs/index.js", + "module": "./dist/_esm/index.js", + "devDependencies": { + "@biomejs/biome": "1.6.0", + "@changesets/cli": "^2.27.1", + "@commitlint/cli": "^19.4.1", + "@commitlint/config-conventional": "^19.4.1", + "@size-limit/esbuild-why": "^11", + "@size-limit/preset-small-lib": "^11", + "@types/bun": "latest", + "@types/yargs": "^17.0.33", + "@vitest/coverage-v8": "^1.3.1", + "buffer": "^6.0.3", + "concurrently": "^8.2.2", + "gh-pages": "^6.1.1", + "rimraf": "^5.0.5", + "simple-git-hooks": "^2.9.0", + "size-limit": "^11.1.5", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.8", + "tslib": "^2.6.3", + "typedoc": "^0.25.9", + "vitest": "^1.3.1", + "yargs": "^17.7.2" + }, + "peerDependencies": { + "typescript": "^5", + "viem": "^2.20.0", + "@biconomy/sdk": "latest", + "@simplewebauthn/browser": "^8.3.4", + "@simplewebauthn/typescript-types": "^8.3.4" + }, + "exports": { + ".": { + "types": "./dist/_types/index.d.ts", + "import": "./dist/_esm/index.js", + "default": "./dist/_cjs/index.js" + } + }, + "commitlint": { + "extends": [ + "@commitlint/config-conventional" + ] + }, + "description": "Passkey validator plug in for Biconomy SDK", + "files": [ + "dist/*", + "README.md" + ], + "homepage": "https://biconomy.io", + "keywords": [ + "erc-7579", + "modular smart account", + "account abstraction", + "biconomy", + "sdk", + "passkey" + ], + "license": "MIT", + "scripts": { + "format": "biome format . --write", + "lint": "biome check .", + "lint:fix": "bun run lint --apply", + "dev": "bun link && concurrently \"bun run esm:watch\" \"bun run cjs:watch\" \"bun run esm:watch:aliases\" \"bun run cjs:watch:aliases\"", + "build": "bun run clean && bun run build:cjs && bun run build:esm && bun run build:types", + "clean": "rimraf ./dist/_esm ./dist/_cjs ./dist/_types ./dist/tsconfig", + "changeset": "changeset", + "changeset:release": "bun run build && changeset publish", + "changeset:version": "changeset version && bun install --lockfile-only", + "changeset:release:canary": "original_name=$(bun run scripts/publish:canary.ts | grep ORIGINAL_NAME | cut -d'=' -f2) && npm publish && git checkout package.json && git tag -l '*-canary.*' | xargs git tag -d && git fetch --tags && git reset --hard && git clean -fd && echo \"Published canary version of $original_name as latest\"", + "esm:watch": "tsc --project ./tsconfig/tsconfig.esm.json --watch", + "cjs:watch": "tsc --project ./tsconfig/tsconfig.cjs.json --watch", + "esm:watch:aliases": "tsc-alias -p ./tsconfig/tsconfig.esm.json --watch", + "cjs:watch:aliases": "tsc-alias -p ./tsconfig/tsconfig.cjs.json --watch", + "build:cjs": "tsc --project ./tsconfig/tsconfig.cjs.json && tsc-alias -p ./tsconfig/tsconfig.cjs.json && echo > ./dist/_cjs/package.json '{\"type\":\"commonjs\"}'", + "build:esm": "tsc --project ./tsconfig/tsconfig.esm.json && tsc-alias -p ./tsconfig/tsconfig.esm.json && echo > ./dist/_esm/package.json '{\"type\": \"module\",\"sideEffects\":false}'", + "build:types": "tsc --project ./tsconfig/tsconfig.types.json && tsc-alias -p ./tsconfig/tsconfig.types.json", + }, + "sideEffects": false, + "simple-git-hooks": { + "pre-commit": "bun run format && bun run lint:fix", + "commit-msg": "npx --no -- commitlint --edit ${1}" + }, + "type": "module", + "types": "./dist/_types/index.d.ts", + "typings": "./dist/_types/index.d.ts" +} \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..acef305 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,4 @@ +export const PASSKEY_VALIDATOR_ADDRESS = + "0xD990393C670dCcE8b4d8F858FB98c9912dBFAa06" +export const DEFAULT_PASSKEY_SERVER_URL = + "https://passkeys.zerodev.app/api/v3/6e2a2efc-f9ad-4b1a-931d-a5888eb0fdb5" diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..3843a93 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from "./toWebAuthnKey" +export * from "./toPasskeyValidator" diff --git a/src/toPasskeyValidator.dx.test.ts b/src/toPasskeyValidator.dx.test.ts new file mode 100644 index 0000000..6b17bd2 --- /dev/null +++ b/src/toPasskeyValidator.dx.test.ts @@ -0,0 +1,87 @@ +import { + type NexusClient, + createNexusClient, + moduleActivator +} from "@biconomy/sdk" +import { http, type Chain, type LocalAccount } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import { baseSepolia } from "viem/chains" +import { beforeAll, describe, test } from "vitest" +import { toPasskeyValidator } from "./toPasskeyValidator" +import { WebAuthnMode, toWebAuthnKey } from "./toWebAuthnKey" + +describe.skip("modules.passkeyValidator.dx", async () => { + let bundlerUrl: string + let chain: Chain + + let eoaAccount: LocalAccount + let nexusClient: NexusClient + + beforeAll(async () => { + // Initialize the network and account details + chain = baseSepolia + bundlerUrl = + "https://bundler.biconomy.io/api/v3/6e2a2efc-f9ad-4b1a-931d-a5888eb0fdb5" + eoaAccount = privateKeyToAccount("0x...") + }) + + test("should setup and use passkey validator to sign a transaction", async () => { + /** + * This test demonstrates the creation and use of an passkey module: + * + * 1. Setup and Installation: + * - Create a Nexus client for the main account + * - Create the credentials for the passkey validator + * - Install the passkey validator module on the smart contract account + * - Create a Nexus client with the passkey validator module + * + * 2. Use the passkey validator to sign a transaction + * - Send a transaction using the passkey validator + * - Wait for the transaction to be mined and retrieve the receipt + * + * This test showcases how to install and setup a passkey validator module on Nexus + */ + nexusClient = await createNexusClient({ + signer: eoaAccount, + chain, + transport: http(), + bundlerTransport: http(bundlerUrl) + }) + + // Create the credentials for the passkey validator, these values will be used as initData when installing the module + const webAuthnKey = await toWebAuthnKey({ + passkeyName: "nexus", // Name of your passkey + mode: WebAuthnMode.Register // Here we are creating a new passkey, if you want to use an existing passkey, use WebAuthnMode.Login + }) + + // Initialize the passkey validator with the WebAuthn key and account details + const passkeyValidator = await toPasskeyValidator({ + webAuthnKey, + chainId: chain.id, + account: nexusClient.account + }) + + // Install the passkey validator module on the smart contract account + const opHash = await nexusClient.installModule({ module: passkeyValidator }) + // Wait for the installation transaction to be mined + await nexusClient.waitForUserOperationReceipt({ hash: opHash }) + + // Set the passkey validator as the active module on the account + nexusClient.extend(moduleActivator(passkeyValidator)) + + // Sending a transaction will be signed by the passkey validator + const txHash = await nexusClient.sendTransaction({ + calls: [ + { + to: eoaAccount.address, + value: 0n + } + ] + }) + + // Wait for the transaction to be mined and retrieve the receipt + const receipt = await nexusClient.waitForTransactionReceipt({ + hash: txHash + }) + }) +}) diff --git a/src/toPasskeyValidator.ts b/src/toPasskeyValidator.ts new file mode 100644 index 0000000..1cb23b2 --- /dev/null +++ b/src/toPasskeyValidator.ts @@ -0,0 +1,261 @@ +import { + type Module, + type NexusAccount, + type ToModuleParameters, + toModule +} from "@biconomy/sdk" +import { startAuthentication } from "@simplewebauthn/browser" +import type { PublicKeyCredentialRequestOptionsJSON } from "@simplewebauthn/typescript-types" +import { type SignableMessage, encodeAbiParameters } from "viem" +import { PASSKEY_VALIDATOR_ADDRESS } from "./constants" +import type { WebAuthnKey } from "./toWebAuthnKey" +import { + b64ToBytes, + base64FromUint8Array, + findQuoteIndices, + hexStringToUint8Array, + isRIP7212SupportedNetwork, + parseAndNormalizeSig, + uint8ArrayToHexString +} from "./utils" + +/** + * Signs a message using WebAuthn. + * + * @param message - The message to be signed. + * @param chainId - The chain ID for the network. + * @param allowCredentials - Optional credentials for authentication. + * @returns A promise that resolves to the encoded signature. + */ +const signMessageUsingWebAuthn = async ( + message: SignableMessage, + chainId: number, + allowCredentials?: PublicKeyCredentialRequestOptionsJSON["allowCredentials"] +) => { + let messageContent: string + if (typeof message === "string") { + // message is a string + messageContent = message + } else if ("raw" in message && typeof message.raw === "string") { + // message.raw is a Hex string + messageContent = message.raw + } else if ("raw" in message && message.raw instanceof Uint8Array) { + // message.raw is a ByteArray + messageContent = message.raw.toString() + } else { + throw new Error("Unsupported message format") + } + + // remove 0x prefix if present + const formattedMessage = messageContent.startsWith("0x") + ? messageContent.slice(2) + : messageContent + + const challenge = base64FromUint8Array( + hexStringToUint8Array(formattedMessage), + true + ) + + // prepare assertion options + const assertionOptions: PublicKeyCredentialRequestOptionsJSON = { + challenge, + allowCredentials, + userVerification: "required" + } + + // start authentication (signing) + + const cred = await startAuthentication(assertionOptions) + + // get authenticator data + const { authenticatorData } = cred.response + const authenticatorDataHex = uint8ArrayToHexString( + b64ToBytes(authenticatorData) + ) + + // get client data JSON + const clientDataJSON = atob(cred.response.clientDataJSON) + + // get challenge and response type location + const { beforeType } = findQuoteIndices(clientDataJSON) + + // get signature r,s + const { signature } = cred.response + const signatureHex = uint8ArrayToHexString(b64ToBytes(signature)) + const { r, s } = parseAndNormalizeSig(signatureHex) + + // encode signature + const encodedSignature = encodeAbiParameters( + [ + { name: "authenticatorData", type: "bytes" }, + { name: "clientDataJSON", type: "string" }, + { name: "responseTypeLocation", type: "uint256" }, + { name: "r", type: "uint256" }, + { name: "s", type: "uint256" }, + { name: "usePrecompiled", type: "bool" } + ], + [ + authenticatorDataHex, + clientDataJSON, + beforeType, + BigInt(r), + BigInt(s), + isRIP7212SupportedNetwork(chainId) + ] + ) + return encodedSignature +} + +export type ToPasskeyValidatorParameters = Omit< + ToModuleParameters, + "accountAddress" | "signer" +> & { + account: NexusAccount + webAuthnKey: WebAuthnKey + chainId: number +} + +/** + * Creates a passkey validator module + * + * @param params - The parameters for creating the passkey validator + * @param params.webAuthnKey - The WebAuthn key data containing public key coordinates and authenticator info + * @param params.chainId - The chain ID of the network + * @param params.account - The Nexus account instance + * + * @returns A Promise that resolves to a Module instance configured for passkey validation through the @biconomy/sdk + * + * @example + * ```typescript + * import { createNexusClient, moduleActivator } from "@biconomy/sdk"; + * import { http } from "viem"; + * import { baseSepolia } from "viem/chains"; + * import { WebAuthnMode, toWebAuthnKey } from "@biconomy/passkey"; + * + * // Initialize Nexus client + * const nexusClient = await createNexusClient({ + * signer: account, + * chain: baseSepolia, + * transport: http(), + * bundlerTransport: http("https://bundler.biconomy.io/api/v3/...") + * }); + * + * // Create WebAuthn credentials + * const webAuthnKey = await toWebAuthnKey({ + * passkeyName: "nexus", + * mode: WebAuthnMode.Register // Use Register for new passkey, Login for existing + * }); + * + * // Create and initialize the passkey validator + * const passkeyValidator = await toPasskeyValidator({ + * webAuthnKey, + * chainId: baseSepolia.id, + * account: nexusClient.account + * }); + * + * // Install the validator module + * const opHash = await nexusClient.installModule({ module: passkeyValidator }); + * await nexusClient.waitForUserOperationReceipt({ hash: opHash }); + * + * // Activate the passkey validator + * nexusClient.extend(moduleActivator(passkeyValidator)); + * + * // Use the passkey validator to sign and send transactions + * const txHash = await nexusClient.sendTransaction({ + * calls: [{ + * to: "0x...", + * value: 0n + * }] + * }); + * + * // Wait for transaction confirmation + * const receipt = await nexusClient.waitForTransactionReceipt({ hash: txHash }); + * ``` + */ +export async function toPasskeyValidator({ + webAuthnKey, + chainId, + account +}: ToPasskeyValidatorParameters): Promise { + return toModule({ + signer: account.signer, + address: PASSKEY_VALIDATOR_ADDRESS, + accountAddress: account.address, + signUserOpHash: async (userOpHash: `0x${string}`) => { + return signMessageUsingWebAuthn(userOpHash, chainId, [ + { id: webAuthnKey.authenticatorId, type: "public-key" } + ]) + }, + initData: encodeAbiParameters( + [ + { + components: [ + { name: "x", type: "uint256" }, + { name: "y", type: "uint256" } + ], + name: "webAuthnData", + type: "tuple" + }, + { + name: "authenticatorIdHash", + type: "bytes32" + } + ], + [ + { + x: webAuthnKey.pubX, + y: webAuthnKey.pubY + }, + webAuthnKey.authenticatorIdHash + ] + ), + moduleInitData: { + initData: encodeAbiParameters( + [ + { + components: [ + { name: "x", type: "uint256" }, + { name: "y", type: "uint256" } + ], + name: "webAuthnData", + type: "tuple" + }, + { + name: "authenticatorIdHash", + type: "bytes32" + } + ], + [ + { + x: webAuthnKey.pubX, + y: webAuthnKey.pubY + }, + webAuthnKey.authenticatorIdHash + ] + ), + address: PASSKEY_VALIDATOR_ADDRESS, + type: "validator" + }, + deInitData: "0x", // Add this line, adjust if needed + async getStubSignature() { + return encodeAbiParameters( + [ + { name: "authenticatorData", type: "bytes" }, + { name: "clientDataJSON", type: "string" }, + { name: "responseTypeLocation", type: "uint256" }, + { name: "r", type: "uint256" }, + { name: "s", type: "uint256" }, + { name: "usePrecompiled", type: "bool" } + ], + [ + "0x49960de5880e8c687434170f6476605b8fe4aeb9a28632c7995cf3ba831d97631d00000000", + '{"type":"webauthn.get","challenge":"tbxXNFS9X_4Byr1cMwqKrIGB-_30a0QhZ6y7ucM0BOE","origin":"http://localhost:3000","crossOrigin":false, "other_keys_can_be_added_here":"do not compare clientDataJSON against a template. See https://goo.gl/yabPex"}', + 1n, + 44941127272049826721201904734628716258498742255959991581049806490182030242267n, + 9910254599581058084911561569808925251374718953855182016200087235935345969636n, + false + ] + ) + } + }) +} diff --git a/src/toWebAuthnKey.ts b/src/toWebAuthnKey.ts new file mode 100644 index 0000000..f40200f --- /dev/null +++ b/src/toWebAuthnKey.ts @@ -0,0 +1,193 @@ +import { startAuthentication, startRegistration } from "@simplewebauthn/browser" +import { type Hex, concatHex, keccak256, pad, toHex } from "viem" +import { DEFAULT_PASSKEY_SERVER_URL } from "./constants.js" +import { b64ToBytes, uint8ArrayToHexString } from "./utils.js" + +export enum WebAuthnMode { + Register = "register", + Login = "login" +} + +export type WebAuthnKey = { + pubX: bigint + pubY: bigint + authenticatorId: string + authenticatorIdHash: Hex +} +type RequestCredentials = "include" | "omit" | "same-origin" + +export type BaseWebAuthnAccountParams = { + passkeyServerUrl?: string + rpID?: string + webAuthnKey?: WebAuthnKey + credentials?: RequestCredentials + passkeyServerHeaders?: Record +} + +export type RegisterWebAuthnAccountParams = BaseWebAuthnAccountParams & { + mode?: WebAuthnMode.Register + passkeyName: string +} + +export type LoginWebAuthnAccountParams = BaseWebAuthnAccountParams & { + mode: WebAuthnMode.Login + passkeyName?: string +} + +export type WebAuthnAccountParams = + | RegisterWebAuthnAccountParams + | LoginWebAuthnAccountParams + +export const encodeWebAuthnPubKey = (pubKey: WebAuthnKey) => { + return concatHex([ + toHex(pubKey.pubX, { size: 32 }), + toHex(pubKey.pubY, { size: 32 }), + pad(pubKey.authenticatorIdHash, { size: 32 }) + ]) +} + +export const toWebAuthnKey = async ({ + passkeyName, + passkeyServerUrl, + rpID, + webAuthnKey, + mode = WebAuthnMode.Register, + credentials = "include", + passkeyServerHeaders = {} +}: WebAuthnAccountParams): Promise => { + if (webAuthnKey) { + return webAuthnKey + } + if (!passkeyServerUrl) { + passkeyServerUrl = DEFAULT_PASSKEY_SERVER_URL + } + let pubKey: string | undefined + let authenticatorId: string | undefined + if (mode === WebAuthnMode.Login) { + // Get login options + const loginOptionsResponse = await fetch( + `${passkeyServerUrl}/login/options`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...passkeyServerHeaders + }, + body: JSON.stringify({ rpID }), + credentials + } + ) + const loginOptions: any = await loginOptionsResponse.json() + + // Start authentication (login) + const loginCred = await startAuthentication(loginOptions) + + authenticatorId = loginCred.id + + // Verify authentication + const loginVerifyResponse = await fetch( + `${passkeyServerUrl}/login/verify`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...passkeyServerHeaders + }, + body: JSON.stringify({ cred: loginCred, rpID }), + credentials + } + ) + + const loginVerifyResult: any = await loginVerifyResponse.json() + + if (!loginVerifyResult.verification.verified) { + throw new Error("Login not verified") + } + // Import the key + pubKey = loginVerifyResult.pubkey // Uint8Array pubkey + } else { + // Get registration options + const registerOptionsResponse = await fetch( + `${passkeyServerUrl}/register/options`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...passkeyServerHeaders + }, + body: JSON.stringify({ username: passkeyName, rpID }), + credentials + } + ) + const registerOptions: any = await registerOptionsResponse.json() + + // Start registration + const registerCred = await startRegistration(registerOptions.options) + + authenticatorId = registerCred.id + + // Verify registration + const registerVerifyResponse = await fetch( + `${passkeyServerUrl}/register/verify`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + ...passkeyServerHeaders + }, + body: JSON.stringify({ + userId: registerOptions.userId, + username: passkeyName, + cred: registerCred, + rpID + }), + credentials + } + ) + + const registerVerifyResult: any = await registerVerifyResponse.json() + if (!registerVerifyResult.verified) { + throw new Error("Registration not verified") + } + + // Import the key + pubKey = registerCred.response.publicKey + } + + if (!pubKey) { + throw new Error("No public key returned from registration credential") + } + if (!authenticatorId) { + throw new Error("No authenticator id returned from registration credential") + } + + const authenticatorIdHash = keccak256( + uint8ArrayToHexString(b64ToBytes(authenticatorId)) + ) + const spkiDer = Buffer.from(pubKey, "base64") + const key = await crypto.subtle.importKey( + "spki", + spkiDer, + { + name: "ECDSA", + namedCurve: "P-256" + }, + true, + ["verify"] + ) + + // Export the key to the raw format + const rawKey = await crypto.subtle.exportKey("raw", key) + const rawKeyBuffer = Buffer.from(rawKey) + + // The first byte is 0x04 (uncompressed), followed by x and y coordinates (32 bytes each for P-256) + const pubKeyX = rawKeyBuffer.subarray(1, 33).toString("hex") + const pubKeyY = rawKeyBuffer.subarray(33).toString("hex") + + return { + pubX: BigInt(`0x${pubKeyX}`), + pubY: BigInt(`0x${pubKeyY}`), + authenticatorId, + authenticatorIdHash + } +} diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..850ecf8 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,152 @@ +import { p256 } from "@noble/curves/p256" +import { type Hex, bytesToBigInt, hexToBytes } from "viem" + +const RIP7212_SUPPORTED_NETWORKS = [80001, 137] + +export const isRIP7212SupportedNetwork = (chainId: number): boolean => + RIP7212_SUPPORTED_NETWORKS.includes(chainId) + +export const uint8ArrayToHexString = (array: Uint8Array): `0x${string}` => { + return `0x${Array.from(array, (byte) => + byte.toString(16).padStart(2, "0") + ).join("")}` as `0x${string}` +} + +export const hexStringToUint8Array = (hexString: string): Uint8Array => { + const formattedHexString = hexString.startsWith("0x") + ? hexString.slice(2) + : hexString + const byteArray = new Uint8Array(formattedHexString.length / 2) + for (let i = 0; i < formattedHexString.length; i += 2) { + byteArray[i / 2] = Number.parseInt( + formattedHexString.substring(i, i + 2), + 16 + ) + } + return byteArray +} + +export const b64ToBytes = (base64: string): Uint8Array => { + const paddedBase64 = base64 + .replace(/-/g, "+") + .replace(/_/g, "/") + .padEnd(base64.length + ((4 - (base64.length % 4)) % 4), "=") + const binString = atob(paddedBase64) + return Uint8Array.from(binString, (m) => m.codePointAt(0) ?? 0) +} + +export const findQuoteIndices = ( + input: string +): { beforeType: bigint; beforeChallenge: bigint } => { + const beforeTypeIndex = BigInt(input.lastIndexOf('"type":"webauthn.get"')) + const beforeChallengeIndex = BigInt(input.indexOf('"challenge')) + return { + beforeType: beforeTypeIndex, + beforeChallenge: beforeChallengeIndex + } +} + +// Parse DER-encoded P256-SHA256 signature to contract-friendly signature +// and normalize it so the signature is not malleable. +export function parseAndNormalizeSig(derSig: Hex): { r: bigint; s: bigint } { + const parsedSignature = p256.Signature.fromDER(derSig.slice(2)) + const bSig = hexToBytes(`0x${parsedSignature.toCompactHex()}`) + // assert(bSig.length === 64, "signature is not 64 bytes"); + const bR = bSig.slice(0, 32) + const bS = bSig.slice(32) + + // Avoid malleability. Ensure low S (<= N/2 where N is the curve order) + const r = bytesToBigInt(bR) + let s = bytesToBigInt(bS) + const n = BigInt( + "0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551" + ) + if (s > n / 2n) { + s = n - s + } + return { r, s } +} + +type PasskeyValidatorSerializedData = { + entryPoint: Hex + validatorAddress: Hex + pubKeyX: bigint + pubKeyY: bigint + authenticatorId: string + authenticatorIdHash: Hex +} + +export const serializePasskeyValidatorData = ( + params: PasskeyValidatorSerializedData +) => { + // biome-ignore lint/suspicious/noExplicitAny: + const replacer = (_: string, value: any) => { + if (typeof value === "bigint") { + return value.toString() + } + return value + } + + const jsonString = JSON.stringify(params, replacer) + const uint8Array = new TextEncoder().encode(jsonString) + const base64String = bytesToBase64(uint8Array) + return base64String +} + +export const deserializePasskeyValidatorData = (params: string) => { + const uint8Array = base64ToBytes(params) + const jsonString = new TextDecoder().decode(uint8Array) + const parsed = JSON.parse(jsonString) as PasskeyValidatorSerializedData + return parsed +} + +function base64ToBytes(base64: string) { + const binString = atob(base64) + return Uint8Array.from(binString, (m) => m.codePointAt(0) as number) +} + +function bytesToBase64(bytes: Uint8Array) { + const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join("") + return btoa(binString) +} + +/** + * Convenience function for creating a base64 encoded string from an ArrayBuffer instance + * Copied from @hexagon/base64 package (base64.fromArrayBuffer) + * @public + * + * @param {Uint8Array} uint8Arr - Uint8Array to be encoded + * @param {boolean} [urlMode] - If set to true, URL mode string will be returned + * @returns {string} - Base64 representation of data + */ +export const base64FromUint8Array = ( + uint8Arr: Uint8Array, + urlMode: boolean +): string => { + const // Regular base64 characters + chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + const // Base64url characters + charsUrl = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" + + let result = "" + + const len = uint8Arr.length + const target = urlMode ? charsUrl : chars + + for (let i = 0; i < len; i += 3) { + result += target[uint8Arr[i] >> 2] + result += target[((uint8Arr[i] & 3) << 4) | (uint8Arr[i + 1] >> 4)] + result += target[((uint8Arr[i + 1] & 15) << 2) | (uint8Arr[i + 2] >> 6)] + result += target[uint8Arr[i + 2] & 63] + } + + const remainder = len % 3 + if (remainder === 2) { + result = result.substring(0, result.length - 1) + (urlMode ? "" : "=") + } else if (remainder === 1) { + result = result.substring(0, result.length - 2) + (urlMode ? "" : "==") + } + + return result +} diff --git a/tsconfig/tsconfig.base.json b/tsconfig/tsconfig.base.json new file mode 100644 index 0000000..4b41d4c --- /dev/null +++ b/tsconfig/tsconfig.base.json @@ -0,0 +1,33 @@ +{ + "include": [], + "compilerOptions": { + "incremental": false, + "useDefineForClassFields": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "useUnknownInCatchVariables": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "allowJs": false, + "checkJs": false, + "esModuleInterop": false, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": false, + "forceConsistentCasingInFileNames": true, + "verbatimModuleSyntax": true, + "importHelpers": true, + "moduleResolution": "NodeNext", + "module": "NodeNext", + "target": "ES2021", + "lib": ["ES2022"], + "skipLibCheck": true, + "noErrorTruncation": true, + "noEmit": true, + "strict": true + }, + "tsc-alias": { + "resolveFullPaths": true, + "verbose": false + } +} \ No newline at end of file diff --git a/tsconfig/tsconfig.cjs.json b/tsconfig/tsconfig.cjs.json new file mode 100644 index 0000000..4dd6705 --- /dev/null +++ b/tsconfig/tsconfig.cjs.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../dist/_cjs", + "removeComments": true, + "verbatimModuleSyntax": false, + "noEmit": false, + "rootDir": "../src", + }, + "exclude": [ + "../src/test/**/*.*", + "../src/**/*.test.*", + "../src/**/*.spec.*", + ] +} \ No newline at end of file diff --git a/tsconfig/tsconfig.esm.json b/tsconfig/tsconfig.esm.json new file mode 100644 index 0000000..6851f62 --- /dev/null +++ b/tsconfig/tsconfig.esm.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "es2015", + "outDir": "../dist/_esm", + "noEmit": false, + "rootDir": "../src" + }, + "exclude": [ + "../src/test/**/*.*", + "../src/**/*.test.*", + "../src/**/*.spec.*", + ] +} \ No newline at end of file diff --git a/tsconfig/tsconfig.json b/tsconfig/tsconfig.json new file mode 100644 index 0000000..4d8f6db --- /dev/null +++ b/tsconfig/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.base.json", + "include": [ + "../src/" + ], + "exclude": [ + "../src/**/*.test.ts", + "../src/**/*.test-d.ts", + "../src/**/*.bench.ts", + ], + "compilerOptions": { + "moduleResolution": "node", + "sourceMap": true, + "rootDir": "../src" + } +} \ No newline at end of file diff --git a/tsconfig/tsconfig.types.json b/tsconfig/tsconfig.types.json new file mode 100644 index 0000000..ddcfa46 --- /dev/null +++ b/tsconfig/tsconfig.types.json @@ -0,0 +1,18 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext", + "outDir": "../dist/_esm", + "declarationDir": "../dist/_types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "noEmit": false, + "rootDir": "../src/" + }, + "exclude": [ + "../src/test/**/*.*", + "../src/**/*.test.*", + "../src/**/*.spec.*", + ] +} \ No newline at end of file diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..6a717b2 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/index.ts"], + "basePath": "src", + "includes": "src", + "out": "docs", + "gitRevision": "main" +}