├── .cargo └── config.toml ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── code-bug.md │ └── doc-bug.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── move.yml │ ├── rust.yml │ └── typescript.yml ├── .gitignore ├── .licensesnip ├── .pre-commit-config-example.yaml ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── Design.md ├── Dockerfile ├── LICENSE ├── README.md ├── SecurityBestPractices.md ├── TermsOfService.md ├── UsingSeal.md ├── crates ├── crypto │ ├── Cargo.toml │ └── src │ │ ├── dem.rs │ │ ├── elgamal.rs │ │ ├── gf256.rs │ │ ├── ibe.rs │ │ ├── lib.rs │ │ ├── polynomial.rs │ │ ├── tss.rs │ │ └── utils.rs ├── key-server │ ├── Cargo.toml │ ├── key-server-config.yaml │ └── src │ │ ├── cache.rs │ │ ├── errors.rs │ │ ├── externals.rs │ │ ├── key_server_options.rs │ │ ├── metrics.rs │ │ ├── mvr.rs │ │ ├── server.rs │ │ ├── signed_message.rs │ │ ├── tests │ │ ├── e2e.rs │ │ ├── externals.rs │ │ ├── mod.rs │ │ ├── pd.rs │ │ ├── server.rs │ │ ├── tle.rs │ │ ├── whitelist.rs │ │ ├── whitelist_v1 │ │ │ ├── Move.toml │ │ │ └── sources │ │ │ │ └── whitelist_v1.move │ │ └── whitelist_v2 │ │ │ ├── Move.toml │ │ │ └── sources │ │ │ └── whitelist_v2.move │ │ ├── types.rs │ │ └── valid_ptb.rs └── seal-cli │ ├── Cargo.toml │ └── src │ └── main.rs ├── deny.toml ├── examples ├── .gitignore ├── README.md ├── frontend │ ├── .eslintrc.cjs │ ├── .prettierrc │ ├── index.html │ ├── package.json │ ├── pnpm-lock.yaml │ ├── src │ │ ├── Allowlist.tsx │ │ ├── AllowlistView.tsx │ │ ├── App.tsx │ │ ├── CreateAllowlist.tsx │ │ ├── CreateSubscriptionService.tsx │ │ ├── EncryptAndUpload.tsx │ │ ├── OwnedAllowlists.tsx │ │ ├── OwnedSubscriptionServices.tsx │ │ ├── SubscriptionService.tsx │ │ ├── SubscriptionView.tsx │ │ ├── constants.ts │ │ ├── index.html │ │ ├── main.tsx │ │ ├── networkConfig.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── vercel.json │ └── vite.config.ts └── move │ ├── Move.toml │ └── sources │ ├── allowlist.move │ ├── subscription.move │ └── utils.move ├── move ├── patterns │ ├── Move.toml │ └── sources │ │ ├── account_based.move │ │ ├── key_request.move │ │ ├── private_data.move │ │ ├── subscription.move │ │ ├── tle.move │ │ ├── voting.move │ │ └── whitelist.move └── seal │ ├── Move.toml │ └── sources │ ├── bf_hmac_encryption.move │ ├── gf256.move │ ├── hmac256ctr.move │ ├── kdf.move │ ├── key_server.move │ └── polynomial.move ├── rust-toolchain.toml ├── rustfmt.toml └── scripts ├── changed-files.sh └── get_current_version.sh /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [alias] 2 | # Collection of project wide clippy lints. This is done via an alias because 3 | # clippy doesn't currently allow for specifiying project-wide lints in a 4 | # configuration file. This is a similar workaround to the ones presented here: 5 | # 6 | xclippy = [ 7 | "clippy", "--all-targets", "--all-features", "--", 8 | "-Wclippy::all", 9 | "-Wclippy::disallowed_methods", 10 | "-Aclippy::unnecessary_get_then_check", 11 | ] 12 | xlint = "run --package x --bin x -- lint" 13 | xtest = "run --package x --bin x -- external-crates-tests" 14 | 15 | # Configuration specifically for running clippy on `external-crates/move/`. 16 | # Some of these allows are to avoid code churn; others are filed as issues on the `sui` repo now. 17 | move-clippy = [ 18 | "clippy", 19 | "--all-targets", 20 | "--", 21 | "-Wclippy::all", 22 | "-Wclippy::disallowed_methods", 23 | "-Aclippy::upper_case_acronyms", 24 | "-Aclippy::type_complexity", 25 | "-Aclippy::new_without_default", 26 | "-Aclippy::question_mark", 27 | "-Aclippy::unnecessary_get_then_check", 28 | "-Aclippy::needless_borrows_for_generic_args", 29 | ] 30 | 31 | [build] 32 | rustflags = ["-C", "force-frame-pointers=yes", "-C", "force-unwind-tables=yes"] 33 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | /examples @joyqvq @benr-ml @jonas-lj 2 | /crates @jonas-lj @benr-ml @joyqvq 3 | /move @jonas-lj @benr-ml @joyqvq 4 | *.md @benr-ml @abhinavg6 @joyqvq @jonas-lj 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/code-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Seal Code Bug 3 | about: Create a new software bug for issues encountered running Seal 4 | title: 'Seal Code Bug or Feature Request' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | ## Steps to Reproduce Issue 10 | 11 | Fill this in with the concrete steps needed to reproduce the bug. When providing code in the reproduction steps, use the smallest snippet of code that demonstrates the issue, removing any extraneous details. 12 | 13 | e.g. 14 | 1. Call function . 15 | 2. Use return value and call . 16 | 17 | ## Expected Result 18 | 19 | Specify what outcome you expected should have resulted, but didn't. 20 | 21 | e.g. 22 | Expected to return 42. 23 | 24 | ## Actual Result 25 | 26 | Specify what the actual unexpected outcome was. 27 | 28 | e.g. 29 | returned 41. 30 | 31 | ## System Information 32 | 33 | * OS: 34 | * Compiler: 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/doc-bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Seal doc content issue or request 3 | about: Creates an issue for Seal documentation 4 | title: 'Seal doc content issue or request' 5 | labels: doc-issue 6 | assignees: 'abhinavg6' 7 | --- 8 | 9 | If this is an issue with existing content, provide the URL or GitHub path to the topic. Otherwise, use **New**. 10 | 11 | Describe the issue or request. Provide as much detail as possible. For issues, it is helpful to copy the specific section of the topic into the issue. 12 | 13 | Thank you for taking the time to let us know about the issue or request. We triage all new issues and requests within 5 business days. We will follow up if we need additional information. 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Describe the changes or additions included in this PR. 4 | 5 | ## Test plan 6 | 7 | How did you test the new or updated feature? -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Documentation for all configuration options: 2 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | schedule: 9 | interval: "weekly" 10 | day: "sunday" 11 | groups: 12 | cargo-minor-and-patch-dependencies: 13 | update-types: 14 | - "minor" 15 | - "patch" 16 | 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: "monthly" 21 | groups: 22 | github-actions-all: 23 | patterns: ["*"] 24 | -------------------------------------------------------------------------------- /.github/workflows/move.yml: -------------------------------------------------------------------------------- 1 | name: Move 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened, ready_for_review] 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 20 | with: 21 | fetch-depth: 1 22 | 23 | - name: Install Sui 24 | uses: cloudposse-github-actions/install-gh-releases@5a4dcf2f959d64832a8e2fe3e8063e4adac6af88 # pin@v1.4.1 25 | with: 26 | config: |- 27 | mystenlabs/sui: 28 | platform: ubuntu 29 | arch: x86_64 30 | extension-matching: true 31 | tag: testnet-v1.49.1 32 | 33 | - name: Run move tests 34 | run: | 35 | for dir in move/*; do 36 | echo "Running sui move test in $dir" 37 | sui move test --path $dir 38 | done 39 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, synchronize, reopened, ready_for_review] 9 | 10 | env: 11 | CARGO_TERM_COLOR: always 12 | # Disable incremental compilation. 13 | # 14 | # Incremental compilation is useful as part of an edit-build-test-edit cycle, 15 | # as it lets the compiler avoid recompiling code that hasn't changed. However, 16 | # on CI, we're not making small edits; we're almost always building the entire 17 | # project from scratch. Thus, incremental compilation on CI actually 18 | # introduces *additional* overhead to support making future builds 19 | # faster...but no future builds will ever occur in any given CI environment. 20 | # 21 | # See https://matklad.github.io/2021/09/04/fast-rust-builds.html#ci-workflow 22 | # for details. 23 | CARGO_INCREMENTAL: 0 24 | # Allow more retries for network requests in cargo (downloading crates) and 25 | # rustup (installing toolchains). This should help to reduce flaky CI failures 26 | # from transient network timeouts or other issues. 27 | CARGO_NET_RETRY: 10 28 | RUSTUP_MAX_RETRIES: 10 29 | # Don't emit giant backtraces in the CI logs. 30 | RUST_BACKTRACE: short 31 | 32 | jobs: 33 | license-check: 34 | name: license-check 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 38 | - name: Install licensesnip 39 | uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # pin@v2.52.4 40 | with: 41 | tool: licensesnip@1.7.0 42 | - run: licensesnip check 43 | 44 | test: 45 | runs-on: ${{ matrix.os }} 46 | strategy: 47 | matrix: 48 | os: 49 | - ubuntu-ghcloud 50 | fail-fast: false 51 | env: 52 | RUSTFLAGS: -D warnings 53 | steps: 54 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 55 | - name: Install correct Rust toolchain 56 | run: rustup update && rustup toolchain install 57 | - uses: taiki-e/install-action@735e5933943122c5ac182670a935f54a949265c1 # pin@v2.52.4 58 | with: 59 | tool: cargo-nextest 60 | - name: Run tests 61 | run: cargo nextest run --all-features 62 | - name: Run doctests 63 | run: | 64 | cargo test --doc --all-features 65 | - name: Check for uncommitted changes 66 | run: scripts/changed-files.sh 67 | 68 | clippy: 69 | runs-on: ubuntu-ghcloud 70 | steps: 71 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 72 | - name: Install correct Rust toolchain 73 | run: rustup update && rustup toolchain install 74 | # See '.cargo/config' for list of enabled/disabled clippy lints 75 | - name: cargo clippy 76 | run: cargo xclippy -D warnings 77 | 78 | rustfmt: 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 82 | - name: Install correct Rust toolchain 83 | run: rustup update && rustup toolchain install 84 | - name: Check formatting 85 | run: cargo fmt --all -- --check 86 | 87 | cargo-deny: 88 | name: cargo-deny (advisories, licenses, bans, ...) 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 92 | - uses: EmbarkStudios/cargo-deny-action@34899fc7ba81ca6268d5947a7a16b4649013fea1 # pin@v2 93 | with: 94 | command: check 95 | arguments: --all-features 96 | log-level: warn 97 | env: 98 | CARGO_TERM_COLOR: always 99 | CARGO_INCREMENTAL: 0 100 | CARGO_NET_RETRY: 10 101 | RUSTUP_MAX_RETRIES: 10 102 | RUST_BACKTRACE: full 103 | -------------------------------------------------------------------------------- /.github/workflows/typescript.yml: -------------------------------------------------------------------------------- 1 | name: TypeScript CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize, reopened, ready_for_review] 8 | 9 | jobs: 10 | build: 11 | name: Lint, Build, and Test 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: examples/frontend 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # pin@v4 19 | with: 20 | fetch-depth: 2 21 | - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # pin@v4.1.0 22 | with: 23 | version: 9.1.1 24 | - name: Install Nodejs 25 | uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # pin@v4.4.0 26 | with: 27 | node-version: '20' 28 | cache: 'pnpm' 29 | cache-dependency-path: 'examples/frontend/pnpm-lock.yaml' 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | - name: Build 33 | run: pnpm build 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | **/.history/ 3 | **/.vscode/ 4 | **/.idea/ 5 | 6 | # OSX 7 | .DS_Store 8 | 9 | # Rust build directory 10 | **/target 11 | .pre-commit* 12 | !.pre-commit-config-example.yaml 13 | 14 | # Move build directory 15 | build 16 | !crates/sui-single-node-benchmark/tests/data/package_publish_from_bytecode/package_a/build 17 | !crates/sui-single-node-benchmark/tests/data/package_publish_from_bytecode/package_b/build 18 | storage 19 | !consensus/core/src/storage 20 | !crates/sui-types/src/storage 21 | !narwhal/storage 22 | 23 | # Move-related files 24 | Move.lock 25 | !crates/sui-framework/packages/move-stdlib/Move.lock 26 | !crates/sui-framework/packages/sui-framework/Move.lock 27 | !crates/sui-framework/packages/sui-system/Move.lock 28 | !crates/sui-framework/packages/deepbook/Move.lock 29 | .trace 30 | .coverage_map.mvcov 31 | 32 | # Thumbnails 33 | ._* 34 | 35 | # Emacs 36 | \#*\# 37 | .\#* 38 | 39 | # Python 40 | .pyc 41 | 42 | # Node.js 43 | node_modules 44 | .tsbuildinfo 45 | *.tsbuildinfo 46 | .turbo 47 | 48 | # App build directories 49 | dist/ 50 | storybook-static/ 51 | web-ext-artifacts/ 52 | .next/ 53 | next-env.d.ts 54 | 55 | # App test artifacts 56 | coverage/ 57 | test-results/ 58 | playwright-report/ 59 | playwright/.cache/ 60 | 61 | # logs 62 | wallet.log.* 63 | sui.log.* 64 | .vercel 65 | npm-debug.log* 66 | yarn-debug.log* 67 | yarn-error.log* 68 | 69 | # misc 70 | *.key 71 | .env 72 | checkpoints_dir/* 73 | light_client.yaml 74 | docs/content/references/sui-api/sui-graphql/* 75 | docs/content/references/framework/** 76 | 77 | lcov.info 78 | 79 | **/build/** 80 | -------------------------------------------------------------------------------- /.licensesnip: -------------------------------------------------------------------------------- 1 | Copyright (c), Mysten Labs, Inc. 2 | SPDX-License-Identifier: Apache-2.0 3 | -------------------------------------------------------------------------------- /.pre-commit-config-example.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-merge-conflict 6 | - id: check-yaml 7 | - id: check-symlinks 8 | - id: end-of-file-fixer 9 | - id: mixed-line-ending 10 | - id: trailing-whitespace 11 | - repo: https://github.com/notken12/licensesnip 12 | rev: f01f898 13 | hooks: 14 | - id: licensesnip 15 | args: [] 16 | pass_filenames: false 17 | - repo: https://github.com/EmbarkStudios/cargo-deny 18 | rev: 0.18.2 19 | hooks: 20 | - id: cargo-deny 21 | args: ["--all-features", "check", "--hide-inclusion-graph"] 22 | - repo: local 23 | hooks: 24 | - id: cargo-fmt 25 | name: cargo-fmt 26 | entry: cargo fmt 27 | language: rust 28 | types: [rust] 29 | pass_filenames: false 30 | - id: cargo-test 31 | name: cargo-test 32 | entry: cargo nextest run 33 | language: rust 34 | files: (^crates/|Cargo\.(toml|lock)$|nextest\.toml$) 35 | pass_filenames: false 36 | verbose: true 37 | - id: cargo-doctests 38 | name: cargo-doctests 39 | entry: cargo test --doc 40 | language: rust 41 | files: (^crates/|Cargo\.(toml|lock)$) 42 | pass_filenames: false 43 | - id: clippy-with-tests 44 | name: clippy-with-tests 45 | entry: cargo clippy 46 | args: ["--all-features", "--tests", "--", "-D", "warnings"] 47 | language: rust 48 | files: ^(crates/|Cargo\.(toml|lock)$) 49 | pass_filenames: false 50 | - id: clippy 51 | name: clippy 52 | entry: cargo clippy 53 | args: ["--all-features", "--", "-D", "warnings"] 54 | language: rust 55 | files: ^(crates/|Cargo\.(toml|lock)$) 56 | pass_filenames: false 57 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to This Project 2 | 3 | Thanks for considering making a contribution to Seal or its documentation. Before you get started, please take a moment to read these guidelines. 4 | 5 | ## Important Note 6 | 7 | We appreciate contributions, but **simple typo fixes (e.g., minor spelling errors, punctuation changes, or trivial rewording) will be ignored** unless they significantly improve clarity or fix a critical issue. If you are unsure whether your change is substantial enough, consider opening an issue first to discuss it. 8 | 9 | ## Example Frontend Application 10 | 11 | The example frontend in this repository is provided as a reference implementation only. We will not accept pull requests for UI/UX improvements or cosmetic changes to the example frontend unless they fix actual bugs or critical issues. The example is meant to demonstrate functionality, not serve as a production-ready application. 12 | 13 | We encourage you to use this example as a starting point for your own frontend applications. If you build upon this example to create your own implementation, we'd love to hear about it! Feel free to share your projects with the community, and we may highlight notable implementations in our documentation or community channels. 14 | 15 | ## Reporting Issues 16 | 17 | Found a bug or security vulnerability? Please check the existing issues before opening a new one. 18 | 19 | Provide as much detail as possible, including steps to reproduce the issue, expected behavior, and actual behavior. 20 | 21 | ## Documentation 22 | 23 | Is something missing or incorrect in our documentation? You can make a PR if you prefer to fix it yourself. 24 | 25 | For larger documentation issues, please create an issue in GitHub. 26 | 27 | ## Proposing Code Changes 28 | 29 | Fork the repository and create a new branch for your changes. Ensure your branch is based on the latest `main`. 30 | 31 | Follow the coding style and conventions used in the project, see [*Code Standards*](#code-standards) and [*Pre-commit Hooks*](#pre-commit-hooks) for further details. 32 | 33 | If your change is significant, please open an issue first to discuss it. 34 | 35 | ## Submitting a Pull Request 36 | 37 | Ensure your changes are well-tested. Provide a clear description of your changes in the pull request. 38 | 39 | Reference any relevant issue numbers in your pull request. Be responsive to feedback from maintainers. 40 | 41 | ## Code Standards 42 | 43 | Follow existing code structure and formatting. 44 | 45 | Write meaningful commit messages. 46 | 47 | Ensure all tests pass before submitting a pull request. 48 | 49 | ## Pre-commit Hooks 50 | 51 | We have CI jobs running for every PR to test and lint the repository. You can install Git pre-commit 52 | hooks to ensure that these check pass even *before pushing your changes* to GitHub. To use this, the 53 | following steps are required: 54 | 55 | 1. Install [Rust](https://www.rust-lang.org/tools/install). 56 | 1. Install [nextest](https://nexte.st/). 57 | 1. [Install pre-commit](https://pre-commit.com/#install) using `pip` or your OS's package manager. 58 | 1. Run `pre-commit install -c .pre-commit-config-example.yaml` in the repository. 59 | 60 | After this setup, the code will be checked, reformatted, and tested whenever you create a Git commit. 61 | 62 | You can also use adjust the pre-commit configuration or use a different pre-commit configuration if you wish: 63 | 64 | 1. Create a file `.pre-commit-config.yaml`, optionally copying and adapting `.pre-commit-config-example.yaml` 65 | (this is set to be ignored by Git). 66 | 1. Run `pre-commit install -c .pre-commit-config.yaml`. 67 | 68 | ## License 69 | 70 | By contributing, you agree that your contributions will be licensed under the same license as this project. 71 | 72 | Thank you for contributing! 73 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["crates/*"] 4 | 5 | [workspace.package] 6 | version = "0.4.1" 7 | authors = ["Mysten Labs "] 8 | edition = "2024" 9 | license = "Apache-2.0" 10 | 11 | [workspace.dependencies] 12 | fastcrypto = { git = "https://github.com/MystenLabs/fastcrypto", rev = "69d496c71fb37e3d22fe85e5bbfd4256d61422b9", features = ["aes"] } 13 | bcs = "0.1.6" 14 | serde = "1.0.210" 15 | serde_json = "1.0.138" 16 | itertools = { version = "0.13.0" } 17 | anyhow = "1.0" 18 | rand = "0.8.5" 19 | hex = "0.4" 20 | clap = { version = "4.5.17", features = ["derive"] } 21 | tracing = "0.1.37" 22 | serde_with = "3.11.0" 23 | 24 | # Sui dependencies 25 | sui_types = { git = "https://github.com/mystenlabs/sui", rev = "42ba6c0", package = "sui-types"} 26 | mysten-service = { git = "https://github.com/mystenlabs/sui", rev = "42ba6c0", package = "mysten-service" } 27 | sui_sdk = { git = "https://github.com/mystenlabs/sui", rev = "42ba6c0", package = "sui-sdk"} 28 | shared_crypto = { git = "https://github.com/MystenLabs/sui", rev = "42ba6c0", package = "shared-crypto" } 29 | move-core-types = { git = "https://github.com/MystenLabs/sui.git", rev = "42ba6c0" } 30 | 31 | [profile.release] 32 | panic = 'abort' 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Start with a Rust base image 2 | FROM rust:1.87-bullseye AS builder 3 | 4 | ARG PROFILE=release 5 | 6 | WORKDIR work 7 | 8 | COPY ./crates ./crates 9 | COPY ./Cargo.toml ./ 10 | 11 | RUN cargo build --bin key-server --profile $PROFILE --config net.git-fetch-with-cli=true 12 | FROM debian:bullseye-slim AS runtime 13 | ARG master_key 14 | ARG key_server_object_id 15 | # TODO: remove this when the legacy key server is no longer needed 16 | ARG legacy_key_server_object_id 17 | ARG network 18 | 19 | EXPOSE 2024 20 | 21 | RUN apt-get update && apt-get install -y cmake clang libpq5 ca-certificates libpq-dev postgresql 22 | 23 | COPY --from=builder /work/target/release/key-server /opt/key-server/bin/ 24 | 25 | ENV MASTER_KEY=$master_key 26 | ENV KEY_SERVER_OBJECT_ID=$key_server_object_id 27 | ENV NETWORK=$network 28 | 29 | # TODO: remove this when the legacy key server is no longer needed 30 | ENV LEGACY_KEY_SERVER_OBJECT_ID=$legacy_key_server_object_id 31 | 32 | ENTRYPOINT ["/opt/key-server/bin/key-server"] 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Seal 2 | 3 | Seal is a decentralized secrets management (DSM) service that relies on access control policies defined and validated on [Sui](https://docs.sui.io/concepts/components). Application developers and users can use Seal to secure sensitive data at rest on decentralized storage like [Walrus](https://docs.wal.app/), or on any other onchain / offchain storage. 4 | 5 | > [!IMPORTANT] 6 | > Seal Beta is in Testnet. Refer to the [Seal Beta Terms of Service](TermsOfService.md). 7 | 8 | ### Features 9 | 10 | - **Encryption and Decryption:** Seal supports encryption of sensitive data using a [secret sharing mechanism](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing), where one can encrypt sensitive data using different public keys, and store those on Walrus or another storage. When needed, one can decrypt the encrypted parts using private keys generated by off-chain key servers (see below). Seal currently supports client-side encryption (CSE) where the application / user is responsible to encrypt and decrypt the data. 11 | - **Access control on Sui:** Seal leverages [Sui](https://docs.sui.io/concepts/components) for controlling access to the decryption keys, and thus allows access control for the sensitive data itself. A user can achieve this by implementing an application specific Move package which must follow some Seal conventions. The application specific logic in the Move package controls when to allow or disallow access to a key, and this flexibility provides a way to realize a number of authorization scenarios. Refer to [Seal design](Design.md) and [Using Seal](UsingSeal.md) for details. 12 | - **Decentralized gatekeeping using off-chain servers:** Seal key server is a simple and lightweight service which is queried by an application or user to validate the access control on Sui and to generate a identity-based private key which can be further used to decrypt the encrypted data. Different parties can operate their own Seal key servers, thus allowing users to realize `t-out-of-n` threshold encryption. In such a scenario, `n` is the number of total Seal key servers, and `t` is the threshold number of key servers that a user chooses for their use case. Seal does not require any specific key server to be used, and applications or users can choose their preferred key servers based on their trust assumptions or regulatory requirements. Refer to [Seal design](Design.md) and [Using Seal](UsingSeal.md) for details. 13 | - **Seamless access:** Applications can interact with Seal key servers using software development kits (SDKs). The [typescript SDK](https://www.npmjs.com/package/@mysten/seal) is available through npm. 14 | 15 | > [!NOTE] 16 | > Currently there are two separate Mysten Labs managed Testnet Seal key servers. Users can choose `1-out-of-1`, `1-out-of-2` or `2-out-of-2` key servers for threshold encryption. More key servers managed by other parties would come online later. If you’re interested in operating one, reach out to the Sui Foundation or Mysten Labs team. 17 | 18 | ### Use cases 19 | 20 | There are a number of Web3 use cases that could utilize Seal to secure sensitive data in a safe and scalable manner. Some of those are: 21 | 22 | - Secure personal data on [Walrus](https://docs.wal.app/) or some other storage, such that it’s only accessible by the user who uploaded it. 23 | - Share secure content stored on [Walrus](https://docs.wal.app/) or some other storage with a specific allowlist of users. 24 | - Share gated content on a content subscription application with a verified list of subscribers. 25 | - Realize end-to-end private messages using Sui and Walrus. 26 | - Implement secure voting and MEV resilient trading in Move. 27 | - and more… 28 | 29 | ### Potential future capabilities 30 | 31 | We’re looking for community feedback on what other capabilities would make sense to add to Seal. Some options are: 32 | 33 | - Realize a Seal key server with a Multi-party computation (MPC) committee. 34 | - Support Server-side encryption (SSE) to allow decryption of the secret data by Seal key servers, as an alternative to, or an extension of, client-side encryption (CSE). 35 | - Digital Right Management (DRM) to allow encryption and decryption on the client-side in a secure & trusted environment, similar to how popular streaming services like Netflix, Youtube, HBO etc. use the DRM technology. 36 | 37 | ### Non-goals 38 | 39 | Even though Seal is supposed to be a generic and flexible secret management service for a variety of use cases, following are not its supposed goals. We do not recommend using Seal for such use cases and instead ask that you look for a more relevant product / service. 40 | 41 | - Seal is not a key management service like [AWS KMS](https://aws.amazon.com/kms/) or any other such Web2 service, in the sense that Seal backends do not store any application or user specific keys. A Seal key server only stores its specific master key pair, where the master public key is used by users to encrypt sensitive data, and the master private key is used to derive identity-based keys to decrypt the data. 42 | - Seal is not a privacy preserving technology like [zkLogin](https://docs.sui.io/concepts/cryptography/zklogin). Instead Seal is supposed to be a collection of components allowing application developers and users to secure sensitive data on- or off-chain by using the security properties of threshold encryption, Sui, and Move. 43 | - Seal should not be used to store highly sensitive data like a user’s wallet keys, or regulated personal data like PHI (personal health information), or any local / state / federal government-level secret data, or other data of similar sensitivity. Some advanced capabilities may be added to Seal in future to allow using it for some of such use cases, but those do not exist yet. 44 | 45 | ### Upgrade policy 46 | 47 | Seal strives to maintain backward compatibility. Any breaking changes to the Key Server API or the TypeScript SDK will be communicated in advance on both GitHub and in the Seal Discord channel. 48 | We will provide a clear migration path for existing users to upgrade to new versions. 49 | New versions of the Key Server will be published under [GitHub Releases](https://github.com/MystenLabs/seal/releases) and tagged with the appropriate version number. 50 | Both the [SDK](https://github.com/MystenLabs/ts-sdks/blob/main/packages/seal/src/key-server.ts#L31) and the [Key Server](https://github.com/MystenLabs/seal/blob/main/crates/key-server/src/server.rs#L85) are configured to expect specific versions from each other. Either component may reject messages from outdated or deprecated counterparties. 51 | 52 | ### Contact Us 53 | 54 | For questions about Seal, use case discussions, or integration support, contact the Seal team on [Sui Discord](https://discord.com/channels/916379725201563759/1356767654265880586). 55 | 56 | ## More information 57 | - [Seal Design](Design.md) 58 | - [Using Seal](UsingSeal.md) 59 | - [Security Best Practices and Risk Mitigations](SecurityBestPractices.md) 60 | - [Seal Beta Terms of Service](TermsOfService.md) 61 | -------------------------------------------------------------------------------- /SecurityBestPractices.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents: 2 | 3 | - [Introduction](README.md) 4 | - [Seal Design](Design.md) 5 | - [Using Seal](UsingSeal.md) 6 | - [Seal Beta Terms of Service](TermsOfService.md) 7 | 8 | # Security best practices and risk mitigations 9 | 10 | When using Seal to manage encrypted data and access policies, it’s important to understand and mitigate certain risks associated with key management, data availability, and operational trust. This section outlines recommendations for developers to follow when integrating Seal into production systems, especially for use cases involving sensitive or long-lived data. 11 | 12 | ## Choose an appropriate threshold configuration 13 | 14 | Seal supports **threshold encryption** using multiple independent key servers. When encrypting data, developers must select a threshold configuration (for example, `2-of-3` or `3-of-5`) based on the sensitivity of the data and how long it needs to remain accessible. 15 | 16 | A poorly chosen threshold can result in unintended data loss. If too many key servers in a configuration go offline or become unavailable in the future, users may not be able to obtain enough decryption shares to recover their keys. Always ensure that the configuration balances fault tolerance with desired security guarantees. 17 | 18 | ## Vet and establish relationships with key server providers 19 | 20 | Each key server in a Seal threshold configuration plays a critical role in data availability. As Seal is permissionless, anyone can run a key server. However, developers should treat key server selection as a trust decision. 21 | 22 | To reduce operational risk, you should: 23 | 24 | * Choose key servers operated by organizations or parties that you can trust. 25 | * Establish a clear business or legal agreement with each provider, if possible. 26 | * Ensure that terms of service specify obligations around availability, incident response, and service continuity. 27 | 28 | Legal agreements can serve as a deterrent to unilateral service disruptions and provide a recourse mechanism if a provider fails to meet expectations. 29 | 30 | ## Use layered encryption for critical or large data 31 | 32 | If you're handling data that is highly sensitive, large in size, or difficult to re-encrypt frequently, consider using **envelope encryption**. 33 | 34 | In this approach: 35 | 36 | * You generate your own symmetric encryption key for the data. 37 | * Encrypt the data with that key. 38 | * Use Seal to encrypt and manage access to that key. 39 | 40 | This setup gives you the ability to **rotate or update** the Seal key servers in your threshold configuration, without needing to re-encrypt the data itself. You only need to re-encrypt the small, symmetric key. This is particularly useful for data that must remain accessible for years, or that is stored immutably on systems like Walrus. 41 | 42 | ## Understand the risks of leaked decryption keys 43 | 44 | Seal uses **client-side encryption** by default. That means applications or users retrieve the decryption key from Seal’s key servers and use it locally to decrypt the data. 45 | 46 | If a user or application leaks the decryption key - intentionally or not - the encrypted data could be decrypted by unauthorized parties. Because Seal key servers do not emit on-chain logs of key delivery events, there is no on-chain audit trail showing which user or wallet obtained the key. 47 | 48 | To help detect or respond to such incidents: 49 | 50 | * Implement audit logging or telemetry in your application. 51 | * Log key access attempts, decryption events, and user behavior. 52 | * Store logs in a tamper-evident system such as Walrus, or anchor them to the chain if required. 53 | 54 | This can support transparency, internal review, or regulatory compliance in high-trust scenarios. 55 | 56 | [Back to table of contents](#table-of-contents) 57 | -------------------------------------------------------------------------------- /TermsOfService.md: -------------------------------------------------------------------------------- 1 | ## Table of Contents: 2 | 3 | - [Introduction](README.md) 4 | - [Seal Design](Design.md) 5 | - [Using Seal](UsingSeal.md) 6 | 7 | # Seal Beta Terms of Service 8 | 9 | **Effective Date: March 3, 2025** 10 | 11 | These Terms of Service (the “Terms”) constitute a legally binding agreement between you (“User,” “you,” or “your”) and Mysten Labs, Inc. (“Mysten Labs,” “we,” “us,” or “our”) governing your access to and use of the beta version of Seal, including the software development kit (SDK) and the key store services (collectively, the “Services”). By accessing, downloading, installing, or otherwise using the Services, you acknowledge that you have read, understood, and agreed to be bound by these Terms. If you do not agree to these Terms, you must not access or use the Services. 12 | 13 | ## 1. Acceptance of Terms 14 | 15 | By using the Services, you represent and warrant that you have the legal authority to enter into these Terms. If you are using the Services on behalf of an organization, you represent and warrant that you have the authority to bind that organization to these Terms, and “you” shall refer to such organization. 16 | 17 | We reserve the right to modify or amend these Terms at any time at its sole discretion. Any such modifications shall be effective upon posting. Your continued use of the Services following the posting of modifications constitutes acceptance of the revised Terms. 18 | 19 | ## 2. Beta Disclaimer 20 | 21 | The Services comprises beta, prerelease code and are not at the level of performance or compatibility of a final, generally available product offering. The Services may not operate correctly and may be substantially modified prior to release of its production version (if released at all). The Services are provided “AS IS” without warranty of any kind, including without limitation,any warranty as to performance, non-infringement, of third party rights, merchantability, or fitness for a particular purpose. The entire risk arising out of the use, testing, or performance of the Services remains with you. 22 | 23 | ## 3. License Grant and Restrictions 24 | 25 | ### 3.1 License Grant 26 | 27 | Subject to your compliance with these Terms, Mysten Labs hereby grants you a limited, non-exclusive, non-transferable, revocable, and non-sublicensable license to access and use the Services solely for the purposes of developing, testing, and integrating applications with the Services. 28 | 29 | ### 3.2 License Restrictions 30 | 31 | You agree that you shall not: 32 | 33 | * Modify, reverse-engineer, decompile, disassemble, or create derivative works from different parts of the Services, except to the extent expressly permitted by applicable law. 34 | * Use the Services in any manner that violates applicable laws or regulations. 35 | * Use the Services to store, transmit, or manage illegal, infringing, or harmful content. 36 | * Distribute, sublicense, lease, rent, or sell the Services or any portion thereof. 37 | * Use the Services for commercial exploitation without prior written consent from Mysten Labs. 38 | 39 | We reserve the right to suspend or terminate your access to the Services if you breach these restrictions. 40 | 41 | ## 4. Access Control and Security 42 | 43 | The SDK operates on the Seal key store services which further validate the application developer-defined access control policies on Sui blockchain for managing access to sensitive data. You acknowledge and agree that you are solely responsible for configuring and enforcing the access control policies to protect your data. 44 | 45 | We do not warrant or guarantee the security of any data stored or processed using the Services. We shall not be liable for any unauthorized access, data breaches, or security vulnerabilities arising from misconfigurations, third-party attacks, or other factors beyond its control. 46 | 47 | ## 5. Data Storage and Integration 48 | 49 | The Services enable secure data storage on decentralized storage solutions, such as Walrus or other on-chain/off-chain storage providers. We do not store, retain, or control any user data and shall have no responsibility for data loss, corruption, or inaccessibility resulting from third-party storage solutions. 50 | 51 | You acknowledge and assume all risks associated with decentralized storage, including but not limited to network failures, smart contract vulnerabilities, and external service disruptions. 52 | 53 | Furthermore, you acknowledge and understand that the Services may be used to generate decryption keys in connection with the key store services. You agree that the storage and safeguarding of such decryption keys are your responsibility. We are not responsible for any theft, misappropriation, or unauthorized use of such decryption keys in connection with your use of the Service. 54 | 55 | ## 6. Modifications, Updates, and Support 56 | 57 | We reserve the right to modify, update, or discontinue the Services at any time without prior notice. We are under no obligation to provide support, maintenance, or updates for the Services. 58 | 59 | Users are responsible for ensuring compatibility with the latest version of the Services. We shall not be liable for any issues arising from the use of outdated versions of the Services. 60 | 61 | ## 7. Disclaimer of Warranties 62 | 63 | TO THE MAXIMUM EXTENT PERMITTED BY LAW, THE SERVICES ARE PROVIDED “AS IS” AND “AS AVAILABLE,” WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, OR NON-INFRINGEMENT. 64 | 65 | MYSTEN LABS MAKES NO WARRANTY THAT THE SERVICES WILL BE ERROR-FREE, UNINTERRUPTED, OR SECURE OR THAT ANY DEFECTS WILL BE CORRECTED. 66 | 67 | ## 8. Limitation of Liability 68 | 69 | TO THE FULLEST EXTENT PERMITTED BY LAW, MYSTEN LABS AND ITS AFFILIATES, OFFICERS, DIRECTORS, EMPLOYEES, AND AGENTS SHALL NOT BE LIABLE FOR ANY (A) DIRECT, INDIRECT, INCIDENTAL, CONSEQUENTIAL, SPECIAL, OR PUNITIVE DAMAGES ARISING FROM OR RELATED TO YOUR USE OF THE SERVICES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES; (B) LOSS OF DATA, LOSS OF PROFITS, BUSINESS INTERRUPTION, OR SYSTEM FAILURES RESULTING FROM USE OF THE SERVICES; OR (C)DAMAGES ARISING FROM THIRD-PARTY STORAGE SOLUTIONS, SMART CONTRACT VULNERABILITIES, OR NETWORK FAILURES. IN NO EVENT SHALL OUR TOTAL LIABILITY EXCEED THE GREATER OF $100 OR THE AMOUNT YOU PAID (IF ANY) TO US. 70 | 71 | ## 9. Feedback and Contributions 72 | 73 | You may voluntarily submit feedback, suggestions, or recommendations regarding the Services. By submitting feedback, you grant Mysten Labs a perpetual, irrevocable, royalty-free, worldwide license to use, modify, and implement such feedback without any obligation to you. 74 | 75 | If you contribute code or modifications to the Services, such contributions may be subject to open-source licensing terms, if applicable. 76 | 77 | ## 10. Termination 78 | 79 | Mysten Labs reserves the right to suspend or terminate your access to the Services at any time, with or without cause. Upon termination, you must cease all use of the Services and delete all copies in your possession. 80 | 81 | Sections 3 (License Restrictions), 4 (Security), 5 (Data Storage), 7 (Disclaimer of Warranties), 8 (Limitation of Liability), and 11 (Governing Law) shall survive termination. 82 | 83 | ## 11. Governing Law and Dispute Resolution 84 | 85 | These Terms shall be governed by and construed in accordance with the laws of California, without regard to its conflict of law principles. 86 | 87 | Any dispute arising out of or relating to these Terms shall be exclusively submitted to the courts of Santa Clara County, California. 88 | 89 | ## 12. Contact Information 90 | 91 | For inquiries related to these Terms, please contact Mysten Labs at legal@mystenlabs.com. 92 | 93 | [Back to table of contents](#table-of-contents) -------------------------------------------------------------------------------- /crates/crypto/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crypto" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [dependencies] 9 | fastcrypto.workspace = true 10 | rand.workspace = true 11 | serde.workspace = true 12 | hex.workspace = true 13 | bcs.workspace = true 14 | itertools.workspace = true 15 | serde_with.workspace = true 16 | typenum = "1.16.0" 17 | sui_types.workspace = true 18 | -------------------------------------------------------------------------------- /crates/crypto/src/elgamal.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use fastcrypto::groups::{GroupElement, Scalar}; 5 | use fastcrypto::traits::AllowedRng; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Serialize, Deserialize)] 9 | pub struct SecretKey(G::ScalarType); 10 | 11 | #[derive(Serialize, Deserialize)] 12 | pub struct PublicKey(G); 13 | 14 | #[derive(Serialize, Deserialize)] 15 | pub struct VerificationKey(G); 16 | 17 | #[derive(Serialize, Deserialize)] 18 | pub struct Encryption(pub G, pub G); 19 | 20 | pub fn genkey, R: AllowedRng>( 21 | rng: &mut R, 22 | ) -> (SecretKey, PublicKey, VerificationKey) { 23 | let sk = G::ScalarType::rand(rng); 24 | ( 25 | SecretKey(sk), 26 | PublicKey(G::generator() * sk), 27 | VerificationKey(VG::generator() * sk), 28 | ) 29 | } 30 | 31 | pub fn encrypt( 32 | rng: &mut R, 33 | msg: &G, 34 | pk: &PublicKey, 35 | ) -> Encryption { 36 | let r = G::ScalarType::rand(rng); 37 | Encryption(G::generator() * r, pk.0 * r + msg) 38 | } 39 | 40 | pub fn decrypt(sk: &SecretKey, e: &Encryption) -> G { 41 | e.1 - e.0 * sk.0 42 | } 43 | -------------------------------------------------------------------------------- /crates/crypto/src/gf256.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use fastcrypto::error::FastCryptoResult; 5 | use fastcrypto::{error::FastCryptoError::InvalidInput, traits::AllowedRng}; 6 | use rand::Rng; 7 | use std::ops::{AddAssign, Neg}; 8 | use std::{ 9 | iter::{Product, Sum}, 10 | ops::{Add, Div, Mul, Sub}, 11 | }; 12 | 13 | /// This represents an element in the Galois field of order 2⁸ represented as F₂(x) / , also known as Rinjdael's finite field. 14 | #[derive(Clone, Debug, Copy, Eq, PartialEq, Hash)] 15 | pub struct GF256(pub(crate) u8); 16 | 17 | /// Table of Eᵢ = gⁱ where g = 0x03 generates the multiplicative group of the field. 18 | const EXP: [u8; 255] = [ 19 | 0x01, 0x03, 0x05, 0x0f, 0x11, 0x33, 0x55, 0xff, 0x1a, 0x2e, 0x72, 0x96, 0xa1, 0xf8, 0x13, 0x35, 20 | 0x5f, 0xe1, 0x38, 0x48, 0xd8, 0x73, 0x95, 0xa4, 0xf7, 0x02, 0x06, 0x0a, 0x1e, 0x22, 0x66, 0xaa, 21 | 0xe5, 0x34, 0x5c, 0xe4, 0x37, 0x59, 0xeb, 0x26, 0x6a, 0xbe, 0xd9, 0x70, 0x90, 0xab, 0xe6, 0x31, 22 | 0x53, 0xf5, 0x04, 0x0c, 0x14, 0x3c, 0x44, 0xcc, 0x4f, 0xd1, 0x68, 0xb8, 0xd3, 0x6e, 0xb2, 0xcd, 23 | 0x4c, 0xd4, 0x67, 0xa9, 0xe0, 0x3b, 0x4d, 0xd7, 0x62, 0xa6, 0xf1, 0x08, 0x18, 0x28, 0x78, 0x88, 24 | 0x83, 0x9e, 0xb9, 0xd0, 0x6b, 0xbd, 0xdc, 0x7f, 0x81, 0x98, 0xb3, 0xce, 0x49, 0xdb, 0x76, 0x9a, 25 | 0xb5, 0xc4, 0x57, 0xf9, 0x10, 0x30, 0x50, 0xf0, 0x0b, 0x1d, 0x27, 0x69, 0xbb, 0xd6, 0x61, 0xa3, 26 | 0xfe, 0x19, 0x2b, 0x7d, 0x87, 0x92, 0xad, 0xec, 0x2f, 0x71, 0x93, 0xae, 0xe9, 0x20, 0x60, 0xa0, 27 | 0xfb, 0x16, 0x3a, 0x4e, 0xd2, 0x6d, 0xb7, 0xc2, 0x5d, 0xe7, 0x32, 0x56, 0xfa, 0x15, 0x3f, 0x41, 28 | 0xc3, 0x5e, 0xe2, 0x3d, 0x47, 0xc9, 0x40, 0xc0, 0x5b, 0xed, 0x2c, 0x74, 0x9c, 0xbf, 0xda, 0x75, 29 | 0x9f, 0xba, 0xd5, 0x64, 0xac, 0xef, 0x2a, 0x7e, 0x82, 0x9d, 0xbc, 0xdf, 0x7a, 0x8e, 0x89, 0x80, 30 | 0x9b, 0xb6, 0xc1, 0x58, 0xe8, 0x23, 0x65, 0xaf, 0xea, 0x25, 0x6f, 0xb1, 0xc8, 0x43, 0xc5, 0x54, 31 | 0xfc, 0x1f, 0x21, 0x63, 0xa5, 0xf4, 0x07, 0x09, 0x1b, 0x2d, 0x77, 0x99, 0xb0, 0xcb, 0x46, 0xca, 32 | 0x45, 0xcf, 0x4a, 0xde, 0x79, 0x8b, 0x86, 0x91, 0xa8, 0xe3, 0x3e, 0x42, 0xc6, 0x51, 0xf3, 0x0e, 33 | 0x12, 0x36, 0x5a, 0xee, 0x29, 0x7b, 0x8d, 0x8c, 0x8f, 0x8a, 0x85, 0x94, 0xa7, 0xf2, 0x0d, 0x17, 34 | 0x39, 0x4b, 0xdd, 0x7c, 0x84, 0x97, 0xa2, 0xfd, 0x1c, 0x24, 0x6c, 0xb4, 0xc7, 0x52, 0xf6, 35 | ]; 36 | 37 | /// Table of Lᵢ = LOG[i + 1] such that g^Lᵢ = i where g = 0x03. 38 | const LOG: [u8; 255] = [ 39 | 0x00, 0x19, 0x01, 0x32, 0x02, 0x1a, 0xc6, 0x4b, 0xc7, 0x1b, 0x68, 0x33, 0xee, 0xdf, 0x03, 0x64, 40 | 0x04, 0xe0, 0x0e, 0x34, 0x8d, 0x81, 0xef, 0x4c, 0x71, 0x08, 0xc8, 0xf8, 0x69, 0x1c, 0xc1, 0x7d, 41 | 0xc2, 0x1d, 0xb5, 0xf9, 0xb9, 0x27, 0x6a, 0x4d, 0xe4, 0xa6, 0x72, 0x9a, 0xc9, 0x09, 0x78, 0x65, 42 | 0x2f, 0x8a, 0x05, 0x21, 0x0f, 0xe1, 0x24, 0x12, 0xf0, 0x82, 0x45, 0x35, 0x93, 0xda, 0x8e, 0x96, 43 | 0x8f, 0xdb, 0xbd, 0x36, 0xd0, 0xce, 0x94, 0x13, 0x5c, 0xd2, 0xf1, 0x40, 0x46, 0x83, 0x38, 0x66, 44 | 0xdd, 0xfd, 0x30, 0xbf, 0x06, 0x8b, 0x62, 0xb3, 0x25, 0xe2, 0x98, 0x22, 0x88, 0x91, 0x10, 0x7e, 45 | 0x6e, 0x48, 0xc3, 0xa3, 0xb6, 0x1e, 0x42, 0x3a, 0x6b, 0x28, 0x54, 0xfa, 0x85, 0x3d, 0xba, 0x2b, 46 | 0x79, 0x0a, 0x15, 0x9b, 0x9f, 0x5e, 0xca, 0x4e, 0xd4, 0xac, 0xe5, 0xf3, 0x73, 0xa7, 0x57, 0xaf, 47 | 0x58, 0xa8, 0x50, 0xf4, 0xea, 0xd6, 0x74, 0x4f, 0xae, 0xe9, 0xd5, 0xe7, 0xe6, 0xad, 0xe8, 0x2c, 48 | 0xd7, 0x75, 0x7a, 0xeb, 0x16, 0x0b, 0xf5, 0x59, 0xcb, 0x5f, 0xb0, 0x9c, 0xa9, 0x51, 0xa0, 0x7f, 49 | 0x0c, 0xf6, 0x6f, 0x17, 0xc4, 0x49, 0xec, 0xd8, 0x43, 0x1f, 0x2d, 0xa4, 0x76, 0x7b, 0xb7, 0xcc, 50 | 0xbb, 0x3e, 0x5a, 0xfb, 0x60, 0xb1, 0x86, 0x3b, 0x52, 0xa1, 0x6c, 0xaa, 0x55, 0x29, 0x9d, 0x97, 51 | 0xb2, 0x87, 0x90, 0x61, 0xbe, 0xdc, 0xfc, 0xbc, 0x95, 0xcf, 0xcd, 0x37, 0x3f, 0x5b, 0xd1, 0x53, 52 | 0x39, 0x84, 0x3c, 0x41, 0xa2, 0x6d, 0x47, 0x14, 0x2a, 0x9e, 0x5d, 0x56, 0xf2, 0xd3, 0xab, 0x44, 53 | 0x11, 0x92, 0xd9, 0x23, 0x20, 0x2e, 0x89, 0xb4, 0x7c, 0xb8, 0x26, 0x77, 0x99, 0xe3, 0xa5, 0x67, 54 | 0x4a, 0xed, 0xde, 0xc5, 0x31, 0xfe, 0x18, 0x0d, 0x63, 0x8c, 0x80, 0xc0, 0xf7, 0x70, 0x07, 55 | ]; 56 | 57 | fn log(x: &GF256) -> u16 { 58 | assert_ne!(x.0, 0); 59 | LOG[x.0 as usize - 1] as u16 60 | } 61 | 62 | fn exp(x: u16) -> GF256 { 63 | GF256(EXP[x as usize % 255]) 64 | } 65 | 66 | #[allow(clippy::suspicious_arithmetic_impl)] 67 | impl Add<&GF256> for &GF256 { 68 | type Output = GF256; 69 | 70 | fn add(self, rhs: &GF256) -> Self::Output { 71 | GF256(self.0 ^ rhs.0) 72 | } 73 | } 74 | 75 | #[allow(clippy::suspicious_op_assign_impl)] 76 | impl AddAssign<&GF256> for GF256 { 77 | fn add_assign(&mut self, rhs: &GF256) { 78 | self.0 ^= rhs.0; 79 | } 80 | } 81 | 82 | #[allow(clippy::suspicious_arithmetic_impl)] 83 | impl Sub<&GF256> for &GF256 { 84 | type Output = GF256; 85 | 86 | fn sub(self, rhs: &GF256) -> Self::Output { 87 | self + rhs // Same as addition in binary fields 88 | } 89 | } 90 | 91 | impl Neg for &GF256 { 92 | type Output = GF256; 93 | 94 | fn neg(self) -> Self::Output { 95 | &GF256::zero() - self 96 | } 97 | } 98 | 99 | impl Mul<&GF256> for &GF256 { 100 | type Output = GF256; 101 | 102 | fn mul(self, rhs: &GF256) -> Self::Output { 103 | if self.0 == 0 || rhs.0 == 0 { 104 | GF256::zero() 105 | } else { 106 | exp(log(self) + log(rhs)) 107 | } 108 | } 109 | } 110 | 111 | #[allow(clippy::suspicious_arithmetic_impl)] 112 | impl Div<&GF256> for &GF256 { 113 | type Output = FastCryptoResult; 114 | 115 | fn div(self, rhs: &GF256) -> Self::Output { 116 | if rhs.0 == 0 { 117 | return Err(InvalidInput); 118 | } else if self.0 == 0 { 119 | return Ok(GF256::zero()); 120 | } 121 | Ok(exp(255 + log(self) - log(rhs))) 122 | } 123 | } 124 | 125 | impl Product for GF256 { 126 | fn product>(iter: I) -> Self { 127 | iter.fold(Self(1), |acc, x| &acc * &x) 128 | } 129 | } 130 | 131 | impl Sum for GF256 { 132 | fn sum>(iter: I) -> Self { 133 | iter.fold(Self(0), |acc, x| &acc + &x) 134 | } 135 | } 136 | 137 | impl From for GF256 { 138 | fn from(value: u8) -> Self { 139 | Self(value) 140 | } 141 | } 142 | 143 | impl From<&u8> for GF256 { 144 | fn from(value: &u8) -> Self { 145 | Self(*value) 146 | } 147 | } 148 | 149 | impl From for u8 { 150 | fn from(value: GF256) -> Self { 151 | value.0 152 | } 153 | } 154 | 155 | impl GF256 { 156 | pub fn rand(rng: &mut R) -> Self { 157 | Self(rng.r#gen()) 158 | } 159 | 160 | pub fn zero() -> Self { 161 | Self(0) 162 | } 163 | 164 | pub fn one() -> Self { 165 | Self(1) 166 | } 167 | } 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use crate::gf256::GF256; 172 | 173 | #[test] 174 | fn test_field_ops() { 175 | // Test vector, partly from https://en.wikipedia.org/wiki/Finite_field_arithmetic#Rijndael's_(AES)_finite_field 176 | let a = GF256(0x53); 177 | let b = GF256(0xca); 178 | assert_eq!(&a + &b, GF256(0x99)); 179 | assert_eq!(&a - &b, GF256(0x99)); 180 | assert_eq!(&a * &b, GF256(0x01)); 181 | assert_eq!((&a / &b).unwrap(), GF256(0xb5)); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /crates/crypto/src/polynomial.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::gf256::GF256; 5 | use fastcrypto::error::FastCryptoResult; 6 | use itertools::Itertools; 7 | use std::iter::{Product, Sum}; 8 | use std::ops::{Add, Div, Mul}; 9 | use std::{unreachable, vec}; 10 | 11 | /// This represents a polynomial over the Galois Field GF256. 12 | /// See [gf256](crate::gf256) for more details. 13 | #[derive(Debug, Clone, PartialEq, Eq)] 14 | pub struct Polynomial(pub(crate) Vec); 15 | 16 | impl Polynomial { 17 | /// Returns the degree of this polynomial. 18 | pub fn degree(&self) -> usize { 19 | self.0.len() - 1 20 | } 21 | 22 | /// Evaluate this polynomial at a given point x. 23 | pub fn evaluate(&self, x: &GF256) -> GF256 { 24 | // Horner's method to evaluate the polynomial at x 25 | self.0 26 | .iter() 27 | .rev() 28 | .fold(GF256::zero(), |sum, coefficient| &(&sum * x) + coefficient) 29 | } 30 | 31 | /// Return the zero polynomial. 32 | pub fn zero() -> Self { 33 | Self(vec![]) 34 | } 35 | 36 | /// Return the one polynomial. 37 | pub fn one() -> Self { 38 | Self(vec![GF256::one()]) 39 | } 40 | 41 | /// Strip trailing zeros to create a unique representation of the polynomial. 42 | fn strip_trailing_zeros(mut self) -> Self { 43 | while self.0.last() == Some(&GF256::zero()) { 44 | self.0.pop(); 45 | } 46 | self 47 | } 48 | 49 | /// Return a polynomial of the form x + constant 50 | fn monic_linear(constant: GF256) -> Self { 51 | Self(vec![constant, GF256::one()]) 52 | } 53 | 54 | /// Create a polynomial `p` given a set of `points` such that `p(x) = y` for all `(x,y)` in `points`. 55 | /// The degree will be at most points.len() - 1. 56 | /// It is assumed that the x-values are distinct, otherwise the function will panic. 57 | pub fn interpolate(points: &[(GF256, GF256)]) -> Self { 58 | // Lagrangian interpolation, see e.g. https://en.wikipedia.org/wiki/Lagrange_polynomial 59 | points 60 | .iter() 61 | .enumerate() 62 | .map(|(j, (x_j, y_j))| { 63 | points 64 | .iter() 65 | .enumerate() 66 | .filter(|(i, _)| *i != j) 67 | .map(|(_, (x_i, _))| { 68 | (Self::monic_linear(-x_i) / &(x_j - x_i)).expect("Divisor is never zero") 69 | }) 70 | .product::() 71 | * y_j 72 | }) 73 | .sum() 74 | } 75 | } 76 | 77 | impl Add for &Polynomial { 78 | type Output = Polynomial; 79 | 80 | fn add(self, other: &Polynomial) -> Self::Output { 81 | Polynomial( 82 | self.0 83 | .iter() 84 | .zip_longest(other.0.iter()) 85 | .map(|p| match p.left_and_right() { 86 | (Some(a), Some(b)) => a + b, 87 | (Some(a), None) => *a, 88 | (None, Some(b)) => *b, 89 | _ => unreachable!(), 90 | }) 91 | .collect(), 92 | ) 93 | .strip_trailing_zeros() 94 | } 95 | } 96 | 97 | impl Sum for Polynomial { 98 | fn sum>(iter: I) -> Self { 99 | iter.fold(Polynomial::zero(), |sum, term| &sum + &term) 100 | } 101 | } 102 | 103 | impl Mul<&GF256> for Polynomial { 104 | type Output = Polynomial; 105 | 106 | fn mul(self, s: &GF256) -> Self::Output { 107 | Polynomial(self.0.into_iter().map(|a| &a * s).collect()).strip_trailing_zeros() 108 | } 109 | } 110 | 111 | #[allow(clippy::suspicious_arithmetic_impl)] 112 | impl Mul for &Polynomial { 113 | type Output = Polynomial; 114 | 115 | fn mul(self, other: &Polynomial) -> Self::Output { 116 | let degree = self.degree() + other.degree(); 117 | Polynomial( 118 | (0..=degree) 119 | .map(|i| { 120 | (0..=i) 121 | .filter(|j| j <= &self.degree() && i - j <= other.degree()) 122 | .map(|j| &self.0[j] * &other.0[i - j]) 123 | .sum() 124 | }) 125 | .collect(), 126 | ) 127 | } 128 | } 129 | 130 | #[allow(clippy::suspicious_arithmetic_impl)] 131 | impl Div<&GF256> for Polynomial { 132 | type Output = FastCryptoResult; 133 | 134 | fn div(self, divisor: &GF256) -> Self::Output { 135 | let inverse = (&GF256::one() / divisor)?; 136 | Ok(Polynomial(self.0.iter().map(|a| a * &inverse).collect()).strip_trailing_zeros()) 137 | } 138 | } 139 | 140 | impl Product for Polynomial { 141 | fn product>(iter: I) -> Self { 142 | iter.fold(Self::one(), |product, factor| &product * &factor) 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | mod tests { 148 | use crate::gf256::GF256; 149 | use crate::polynomial::Polynomial; 150 | 151 | #[test] 152 | fn test_polynomial_evaluation() { 153 | let x = GF256::from(2); 154 | let c = [GF256::from(1), GF256::from(2), GF256::from(3)]; 155 | let result = Polynomial(c.to_vec()).evaluate(&x); 156 | assert_eq!( 157 | [ 158 | c[0], 159 | [c[1], x].into_iter().product(), 160 | [c[2], x, x].into_iter().product() 161 | ] 162 | .into_iter() 163 | .sum::(), 164 | result 165 | ); 166 | } 167 | 168 | #[test] 169 | fn test_arithmetic() { 170 | let p1 = Polynomial(vec![GF256::from(1), GF256::from(2), GF256::from(3)]); 171 | let p2 = Polynomial(vec![GF256::from(4), GF256::from(5)]); 172 | let p3 = Polynomial(vec![GF256::from(2)]); 173 | assert_eq!( 174 | &p1 + &p2, 175 | Polynomial(vec![GF256::from(5), GF256::from(7), GF256::from(3)]) 176 | ); 177 | assert_eq!( 178 | &p1 * &p3, 179 | Polynomial(vec![GF256::from(2), GF256::from(4), GF256::from(6)]) 180 | ); 181 | } 182 | 183 | #[test] 184 | fn test_interpolation() { 185 | let x = [GF256::from(1), GF256::from(2), GF256::from(3)]; 186 | let y = [GF256::from(7), GF256::from(11), GF256::from(17)]; 187 | let points = x 188 | .iter() 189 | .zip(y.iter()) 190 | .map(|(x, y)| (*x, *y)) 191 | .collect::>(); 192 | 193 | let p = Polynomial::interpolate(&points); 194 | 195 | assert!(p.degree() <= points.len()); 196 | for (x, y) in points { 197 | assert_eq!(y, p.evaluate(&x)); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /crates/crypto/src/utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use fastcrypto::error::FastCryptoError::InvalidInput; 5 | use fastcrypto::error::FastCryptoResult; 6 | use fastcrypto::traits::AllowedRng; 7 | use itertools::Itertools; 8 | 9 | pub(crate) fn xor(a: &[u8; N], b: &[u8; N]) -> [u8; N] { 10 | xor_unchecked(a, b) 11 | .try_into() 12 | .expect("Inputs are guaranteed to have the same lengths") 13 | } 14 | 15 | /// XOR two byte slices together. 16 | /// If one of the slices is shorter than the other, the result will be the length of the shorter slice. 17 | pub(crate) fn xor_unchecked(a: &[u8], b: &[u8]) -> Vec { 18 | a.iter().zip(b.iter()).map(|(x, y)| x ^ y).collect() 19 | } 20 | 21 | pub(crate) fn generate_random_bytes(rng: &mut R) -> [u8; N] { 22 | let mut bytes = [0u8; N]; 23 | rng.fill_bytes(&mut bytes); 24 | bytes 25 | } 26 | 27 | /// Convert N vectors of the same length, M, into M arrays of length N such that matrix[i][j] = transpose(&matrix)[j][i]. 28 | /// Returns with an InvalidInput error if the input does not have length equal to N 29 | /// or if the elements of this vector do not all have the same length. 30 | pub(crate) fn transpose(matrix: &[Vec]) -> FastCryptoResult> { 31 | if matrix.len() != N || matrix.is_empty() { 32 | return Err(InvalidInput); 33 | } 34 | let m = matrix 35 | .iter() 36 | .map(Vec::len) 37 | .all_equal_value() 38 | .map_err(|_| InvalidInput)?; 39 | 40 | Ok((0..m) 41 | .map(|i| { 42 | matrix 43 | .iter() 44 | .map(|row| row[i]) 45 | .collect_vec() 46 | .try_into() 47 | .expect("This will never fail since the length is guaranteed to be N") 48 | }) 49 | .collect()) 50 | } 51 | -------------------------------------------------------------------------------- /crates/key-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "key-server" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [[bin]] 9 | name = "key-server" 10 | path = "src/server.rs" 11 | 12 | [dependencies] 13 | fastcrypto.workspace = true 14 | rand.workspace = true 15 | serde.workspace = true 16 | serde_json.workspace = true 17 | hex.workspace = true 18 | bcs.workspace = true 19 | tracing.workspace = true 20 | sui_types.workspace = true 21 | mysten-service.workspace = true 22 | sui_sdk.workspace = true 23 | shared_crypto.workspace = true 24 | move-core-types.workspace = true 25 | mvr_types = { git = "https://github.com/MystenLabs/mvr", rev = "1993d7188f62564b05f0ccab46bbfb24b0eea326", package = "mvr-types" } 26 | 27 | tokio = { version = "1.44.2", features = ["full"] } 28 | axum = { version = "0.7", features = ["macros"] } 29 | reqwest = { version = "0.12.15", features = ["json"] } 30 | tower-http = { version = "0.6.0", features = ["cors"] } 31 | crypto = { path = "../crypto" } 32 | tap = "1.0.1" 33 | prometheus = "0.13.3" 34 | anyhow = "1.0.79" 35 | lru = "0.13.0" 36 | parking_lot = "0.12.3" 37 | once_cell = "1.20.2" 38 | chrono = "0.4.39" 39 | semver = { version = "1.0.26", features = ["serde"] } 40 | jsonrpsee = "0.24.0" 41 | serde_yaml = "0.9" 42 | serde_with = { version = "3.12.0", features = ["macros"] } 43 | duration-str = "0.17.0" 44 | 45 | move-binding-derive = { git = "https://github.com/MystenLabs/move-binding.git", rev = "99f68a28c2f19be40a09e5f1281af748df9a8d3e" } 46 | move-types = { git = "https://github.com/MystenLabs/move-binding.git", rev = "99f68a28c2f19be40a09e5f1281af748df9a8d3e" } 47 | sui-sdk-types = { git = "https://github.com/mystenlabs/sui-rust-sdk", features = ["serde"], rev = "86a9e06" } 48 | sui-transaction-builder = { git = "https://github.com/mystenlabs/sui-rust-sdk", rev = "86a9e06" } 49 | prometheus_closure_metric = { git = "https://github.com/MystenLabs/sui", rev = "42ba6c0", package = "prometheus-closure-metric" } 50 | 51 | [dev-dependencies] 52 | tracing-test = "0.2.5" 53 | test_cluster = { git = "https://github.com/mystenlabs/sui", rev = "42ba6c0", package = "test-cluster" } 54 | sui_move_build = { git = "https://github.com/mystenlabs/sui", rev = "42ba6c0", package = "sui-move-build" } 55 | futures = "0.3" 56 | -------------------------------------------------------------------------------- /crates/key-server/key-server-config.yaml: -------------------------------------------------------------------------------- 1 | # The on-chain object ID of the legacy key server object. 2 | legacy_key_server_object_id: '0x0000000000000000000000000000000000000000000000000000000000000000' 3 | 4 | # The on-chain object ID of the key server object. 5 | key_server_object_id: '0x0000000000000000000000000000000000000000000000000000000000000000' 6 | 7 | # The network to use. Should be one of: 8 | network: Testnet 9 | # network: Mainnet 10 | # network: Devnet 11 | # network: Custom 12 | # graphql_url: 'url_to_graphql_endpoint' 13 | # node_url: 'url_to_node_endpoint' 14 | 15 | # Optional configurations: 16 | # 17 | # The port that the prometheus agent can use to poll for the metrics. 18 | # metrics_host_port: 9184 19 | # 20 | # The minimum version of the SDK that is required to use this service. 21 | # sdk_version_requirement: '>=0.4.5' 22 | # 23 | # Update intervals and timeouts for various operations. Example values are shown below. 24 | # checkpoint_update_interval: '10s' 25 | # rgp_update_interval: '60s' 26 | # allowed_staleness: '2m' 27 | # session_key_ttl_max: '30m' 28 | -------------------------------------------------------------------------------- /crates/key-server/src/cache.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::externals::current_epoch_time; 5 | use lru::LruCache; 6 | use parking_lot::Mutex; 7 | use std::hash::Hash; 8 | use std::num::NonZero; 9 | 10 | pub(crate) const CACHE_SIZE: usize = 1000; 11 | pub(crate) const CACHE_TTL: u64 = 3 * 60 * 1000; // 3 minutes 12 | 13 | // Generic LRU cache with TTL 14 | 15 | struct CacheEntry { 16 | pub value: V, 17 | pub expiry: u64, 18 | } 19 | 20 | pub(crate) struct Cache { 21 | ttl: u64, 22 | cache: Mutex>>, 23 | } 24 | 25 | impl Cache { 26 | /// Create a new cache with a given TTL and size. 27 | /// Panics if ttl or size is 0. 28 | pub fn new(ttl: u64, size: usize) -> Self { 29 | assert!(size > 0 && ttl > 0, "TTL and size must be greater than 0"); 30 | Self { 31 | ttl, 32 | cache: Mutex::new(LruCache::new(NonZero::new(size).expect("fixed value"))), 33 | } 34 | } 35 | 36 | pub fn get(&self, key: &K) -> Option { 37 | let mut cache = self.cache.lock(); 38 | match cache.get(key) { 39 | Some(entry) => { 40 | if entry.expiry < current_epoch_time() { 41 | cache.pop(key); 42 | None 43 | } else { 44 | Some(entry.value) 45 | } 46 | } 47 | None => None, 48 | } 49 | } 50 | 51 | pub fn insert(&self, key: K, value: V) { 52 | let mut cache = self.cache.lock(); 53 | cache.put( 54 | key, 55 | CacheEntry { 56 | value, 57 | expiry: current_epoch_time() + self.ttl, 58 | }, 59 | ); 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use super::*; 66 | use std::thread::sleep; 67 | use std::time::Duration; 68 | 69 | #[test] 70 | fn test_cache_insert_and_get() { 71 | let cache = Cache::new(1000, 10); 72 | cache.insert(1, "value1"); 73 | assert_eq!(cache.get(&1), Some("value1")); 74 | } 75 | 76 | #[test] 77 | fn test_cache_expiry() { 78 | let cache = Cache::new(1000, 10); 79 | cache.insert(1, "value1"); 80 | sleep(Duration::from_millis(1100)); 81 | assert_eq!(cache.get(&1), None); 82 | } 83 | 84 | #[test] 85 | fn test_cache_overwrite() { 86 | let cache = Cache::new(1000, 10); 87 | cache.insert(1, "value1"); 88 | cache.insert(1, "value2"); 89 | assert_eq!(cache.get(&1), Some("value2")); 90 | } 91 | 92 | #[test] 93 | fn test_cache_lru_eviction() { 94 | let cache = Cache::new(1000, 2); 95 | cache.insert(1, "value1"); 96 | cache.insert(2, "value2"); 97 | cache.insert(3, "value3"); 98 | assert_eq!(cache.get(&1), None); // Should be evicted 99 | assert_eq!(cache.get(&2), Some("value2")); 100 | assert_eq!(cache.get(&3), Some("value3")); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /crates/key-server/src/errors.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use axum::http::StatusCode; 5 | use axum::response::{IntoResponse, Response}; 6 | use axum::Json; 7 | use serde::Serialize; 8 | 9 | #[derive(Debug, Serialize, PartialEq)] 10 | pub enum InternalError { 11 | InvalidPTB(String), 12 | InvalidPackage, 13 | NoAccess, 14 | InvalidSignature, 15 | InvalidSessionSignature, 16 | InvalidCertificate, 17 | InvalidSDKVersion, 18 | DeprecatedSDKVersion, 19 | MissingRequiredHeader(String), 20 | InvalidParameter, 21 | InvalidMVRName, 22 | InvalidServiceId, 23 | Failure, // Internal error, try again later 24 | } 25 | 26 | #[derive(Debug, Serialize)] 27 | pub struct ErrorResponse { 28 | error: InternalError, 29 | message: String, 30 | } 31 | 32 | impl IntoResponse for InternalError { 33 | fn into_response(self) -> Response { 34 | let (status, message) = match self { 35 | InternalError::InvalidPTB(ref inner) => { 36 | (StatusCode::FORBIDDEN, format!("Invalid PTB: {}", inner)) 37 | } 38 | InternalError::InvalidPackage => { 39 | (StatusCode::FORBIDDEN, "Invalid package ID".to_string()) 40 | } 41 | InternalError::NoAccess => (StatusCode::FORBIDDEN, "Access denied".to_string()), 42 | InternalError::InvalidCertificate => ( 43 | StatusCode::FORBIDDEN, 44 | "Invalid certificate time or ttl".to_string(), 45 | ), 46 | InternalError::InvalidSignature => { 47 | (StatusCode::FORBIDDEN, "Invalid user signature".to_string()) 48 | } 49 | InternalError::InvalidSDKVersion => { 50 | (StatusCode::BAD_REQUEST, "Invalid SDK version".to_string()) 51 | } 52 | InternalError::DeprecatedSDKVersion => ( 53 | StatusCode::UPGRADE_REQUIRED, 54 | "Deprecated SDK version".to_string(), 55 | ), 56 | InternalError::MissingRequiredHeader(ref inner) => ( 57 | StatusCode::BAD_REQUEST, 58 | format!("Missing required header: {}", inner).to_string(), 59 | ), 60 | InternalError::InvalidSessionSignature => ( 61 | StatusCode::FORBIDDEN, 62 | "Invalid session key signature".to_string(), 63 | ), 64 | InternalError::InvalidParameter => ( 65 | StatusCode::FORBIDDEN, 66 | "Invalid parameter. If the object was just created, try again later.".to_string(), 67 | ), 68 | InternalError::InvalidMVRName => { 69 | (StatusCode::FORBIDDEN, "Invalid MVR name".to_string()) 70 | } 71 | InternalError::InvalidServiceId => { 72 | (StatusCode::BAD_REQUEST, "Invalid service ID".to_string()) 73 | } 74 | InternalError::Failure => ( 75 | StatusCode::SERVICE_UNAVAILABLE, 76 | "Internal server error, please try again later".to_string(), 77 | ), 78 | }; 79 | 80 | let error_response = ErrorResponse { 81 | error: self, 82 | message, 83 | }; 84 | 85 | (status, Json(error_response)).into_response() 86 | } 87 | } 88 | 89 | impl InternalError { 90 | pub fn as_str(&self) -> &'static str { 91 | match self { 92 | InternalError::InvalidPTB(_) => "InvalidPTB", 93 | InternalError::InvalidPackage => "InvalidPackage", 94 | InternalError::NoAccess => "NoAccess", 95 | InternalError::InvalidCertificate => "InvalidCertificate", 96 | InternalError::InvalidSignature => "InvalidSignature", 97 | InternalError::InvalidSessionSignature => "InvalidSessionSignature", 98 | InternalError::InvalidSDKVersion => "InvalidSDKVersion", 99 | InternalError::DeprecatedSDKVersion => "DeprecatedSDKVersion", 100 | InternalError::MissingRequiredHeader(_) => "MissingRequiredHeader", 101 | InternalError::InvalidParameter => "InvalidParameter", 102 | InternalError::InvalidMVRName => "InvalidMVRName", 103 | InternalError::InvalidServiceId => "InvalidServiceId", 104 | InternalError::Failure => "Failure", 105 | } 106 | } 107 | } 108 | 109 | #[macro_export] 110 | macro_rules! return_err { 111 | ($err:expr, $msg:expr $(, $arg:expr)*) => {{ 112 | debug!($msg $(, $arg)*); 113 | return Err($err); 114 | }}; 115 | } 116 | -------------------------------------------------------------------------------- /crates/key-server/src/key_server_options.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::from_mins; 5 | use crate::types::Network; 6 | use duration_str::deserialize_duration; 7 | use semver::VersionReq; 8 | use serde::{Deserialize, Serialize}; 9 | use std::time::Duration; 10 | use sui_types::base_types::ObjectID; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct KeyServerOptions { 14 | // TODO: remove this when the legacy key server is no longer needed 15 | /// The object ID of the legacy key server object. 16 | pub legacy_key_server_object_id: ObjectID, 17 | 18 | /// The object ID of the key server object. 19 | pub key_server_object_id: ObjectID, 20 | 21 | /// The network this key server is running on. 22 | pub network: Network, 23 | 24 | /// The minimum version of the SDK that is required to use this service. 25 | #[serde(default = "default_sdk_version_requirement")] 26 | pub sdk_version_requirement: VersionReq, 27 | 28 | #[serde(default = "default_metrics_host_port")] 29 | pub metrics_host_port: u16, 30 | 31 | /// The interval at which the latest checkpoint timestamp is updated. 32 | #[serde( 33 | default = "default_checkpoint_update_interval", 34 | deserialize_with = "deserialize_duration" 35 | )] 36 | pub checkpoint_update_interval: Duration, 37 | 38 | /// The interval at which the reference gas price is updated. 39 | #[serde( 40 | default = "default_rgp_update_interval", 41 | deserialize_with = "deserialize_duration" 42 | )] 43 | pub rgp_update_interval: Duration, 44 | 45 | /// The allowed staleness of the full node. 46 | /// When setting this duration, note a timestamp on Sui may be a bit late compared to 47 | /// the current time, but it shouldn't be more than a second. 48 | #[serde( 49 | default = "default_allowed_staleness", 50 | deserialize_with = "deserialize_duration" 51 | )] 52 | pub allowed_staleness: Duration, 53 | 54 | /// The maximum time to live for a session key. 55 | #[serde( 56 | default = "default_session_key_ttl_max", 57 | deserialize_with = "deserialize_duration" 58 | )] 59 | pub session_key_ttl_max: Duration, 60 | } 61 | 62 | impl KeyServerOptions { 63 | pub fn new_with_default_values( 64 | network: Network, 65 | legacy_key_server_object_id: ObjectID, 66 | key_server_object_id: ObjectID, 67 | ) -> Self { 68 | Self { 69 | network, 70 | sdk_version_requirement: default_sdk_version_requirement(), 71 | legacy_key_server_object_id, 72 | key_server_object_id, 73 | metrics_host_port: default_metrics_host_port(), 74 | checkpoint_update_interval: default_checkpoint_update_interval(), 75 | rgp_update_interval: default_rgp_update_interval(), 76 | allowed_staleness: default_allowed_staleness(), 77 | session_key_ttl_max: default_session_key_ttl_max(), 78 | } 79 | } 80 | } 81 | 82 | fn default_checkpoint_update_interval() -> Duration { 83 | Duration::from_secs(10) 84 | } 85 | 86 | fn default_rgp_update_interval() -> Duration { 87 | Duration::from_secs(60) 88 | } 89 | 90 | fn default_session_key_ttl_max() -> Duration { 91 | from_mins(30) 92 | } 93 | 94 | fn default_allowed_staleness() -> Duration { 95 | from_mins(2) 96 | } 97 | 98 | fn default_metrics_host_port() -> u16 { 99 | 9184 100 | } 101 | 102 | fn default_sdk_version_requirement() -> VersionReq { 103 | VersionReq::parse(">=0.4.5").expect("Failed to parse default SDK version requirement") 104 | } 105 | 106 | #[test] 107 | fn test_parse_config() { 108 | use std::str::FromStr; 109 | 110 | let valid_configuration = 111 | "network: Mainnet\nsdk_version_requirement: '>=0.2.7'\nmetrics_host_port: 1234\nlegacy_key_server_object_id: '0x0000000000000000000000000000000000000000000000000000000000000001'\nkey_server_object_id: '0x0000000000000000000000000000000000000000000000000000000000000002'\ncheckpoint_update_interval: '13s'"; 112 | 113 | let options: KeyServerOptions = 114 | serde_yaml::from_str(valid_configuration).expect("Failed to parse valid configuration"); 115 | assert_eq!(options.network, Network::Mainnet); 116 | assert_eq!(options.sdk_version_requirement.to_string(), ">=0.2.7"); 117 | assert_eq!(options.metrics_host_port, 1234); 118 | assert_eq!( 119 | options.legacy_key_server_object_id, 120 | ObjectID::from_str("0x0000000000000000000000000000000000000000000000000000000000000001") 121 | .unwrap() 122 | ); 123 | assert_eq!( 124 | options.key_server_object_id, 125 | ObjectID::from_str("0x0x0000000000000000000000000000000000000000000000000000000000000002") 126 | .unwrap() 127 | ); 128 | assert_eq!(options.checkpoint_update_interval, Duration::from_secs(13)); 129 | 130 | let valid_configuration_custom_network = 131 | "network: !Custom\n graphql_url: https://graphql.dk\n node_url: https://node.dk\nlegacy_key_server_object_id: '0x0'\nkey_server_object_id: '0x0'\n"; 132 | let options: KeyServerOptions = serde_yaml::from_str(valid_configuration_custom_network) 133 | .expect("Failed to parse valid configuration"); 134 | assert_eq!( 135 | options.network, 136 | Network::Custom { 137 | graphql_url: "https://graphql.dk".to_string(), 138 | node_url: "https://node.dk".to_string(), 139 | } 140 | ); 141 | 142 | let missing_object_id = "legacy_key_server_object_id: '0x0'\n"; 143 | assert!(serde_yaml::from_str::(missing_object_id).is_err()); 144 | 145 | let unknown_option = "a_complete_unknown: 'a rolling stone'\n"; 146 | assert!(serde_yaml::from_str::(unknown_option).is_err()); 147 | } 148 | -------------------------------------------------------------------------------- /crates/key-server/src/metrics.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use prometheus::{ 5 | register_histogram_with_registry, register_int_counter_vec_with_registry, 6 | register_int_counter_with_registry, Histogram, IntCounter, IntCounterVec, Registry, 7 | }; 8 | use std::time::Instant; 9 | 10 | #[derive(Debug)] 11 | pub(crate) struct Metrics { 12 | /// Total number of requests received 13 | pub requests: IntCounter, 14 | 15 | /// Total number of service requests received 16 | pub service_requests: IntCounter, 17 | 18 | /// Total number of internal errors by type 19 | errors: IntCounterVec, 20 | 21 | /// Delay of timestamp of the latest checkpoint 22 | pub checkpoint_timestamp_delay: Histogram, 23 | 24 | /// Duration of getting the latest checkpoint timestamp 25 | pub get_checkpoint_timestamp_duration: Histogram, 26 | 27 | /// Status of requests of getting the latest checkpoint timestamp 28 | pub get_checkpoint_timestamp_status: IntCounterVec, 29 | 30 | /// Status of requests of getting the reference gas price 31 | pub get_reference_gas_price_status: IntCounterVec, 32 | 33 | /// Duration of check_policy 34 | pub check_policy_duration: Histogram, 35 | 36 | /// Duration of fetch_pkg_ids 37 | pub fetch_pkg_ids_duration: Histogram, 38 | 39 | /// Total number of requests per number of ids 40 | pub requests_per_number_of_ids: Histogram, 41 | } 42 | 43 | impl Metrics { 44 | pub(crate) fn new(registry: &Registry) -> Self { 45 | Self { 46 | requests: register_int_counter_with_registry!( 47 | "total_requests", 48 | "Total number of fetch_key requests received", 49 | registry 50 | ) 51 | .unwrap(), 52 | errors: register_int_counter_vec_with_registry!( 53 | "internal_errors", 54 | "Total number of internal errors by type", 55 | &["internal_error_type"], 56 | registry 57 | ) 58 | .unwrap(), 59 | service_requests: register_int_counter_with_registry!( 60 | "service_requests", 61 | "Total number of service requests received", 62 | registry 63 | ) 64 | .unwrap(), 65 | checkpoint_timestamp_delay: register_histogram_with_registry!( 66 | "checkpoint_timestamp_delay", 67 | "Delay of timestamp of the latest checkpoint", 68 | buckets(0.0, 120000.0, 1000.0), 69 | registry 70 | ) 71 | .unwrap(), 72 | get_checkpoint_timestamp_duration: register_histogram_with_registry!( 73 | "checkpoint_timestamp_duration", 74 | "Duration of getting the latest checkpoint timestamp", 75 | default_external_call_duration_buckets(), 76 | registry 77 | ) 78 | .unwrap(), 79 | get_checkpoint_timestamp_status: register_int_counter_vec_with_registry!( 80 | "checkpoint_timestamp_status", 81 | "Status of request to get the latest timestamp", 82 | &["status"], 83 | registry, 84 | ) 85 | .unwrap(), 86 | fetch_pkg_ids_duration: register_histogram_with_registry!( 87 | "fetch_pkg_ids_duration", 88 | "Duration of fetch_pkg_ids", 89 | default_fast_call_duration_buckets(), 90 | registry 91 | ) 92 | .unwrap(), 93 | check_policy_duration: register_histogram_with_registry!( 94 | "check_policy_duration", 95 | "Duration of check_policy", 96 | default_fast_call_duration_buckets(), 97 | registry 98 | ) 99 | .unwrap(), 100 | get_reference_gas_price_status: register_int_counter_vec_with_registry!( 101 | "get_reference_gas_price_status", 102 | "Status of requests of getting the reference gas price", 103 | &["status"], 104 | registry 105 | ) 106 | .unwrap(), 107 | requests_per_number_of_ids: register_histogram_with_registry!( 108 | "requests_per_number_of_ids", 109 | "Total number of requests per number of ids", 110 | buckets(0.0, 5.0, 1.0), 111 | registry 112 | ) 113 | .unwrap(), 114 | } 115 | } 116 | 117 | pub(crate) fn observe_error(&self, error_type: &str) { 118 | self.errors.with_label_values(&[error_type]).inc(); 119 | } 120 | } 121 | 122 | /// If metrics is Some, apply the closure and measure the duration of the closure and call set_duration with the duration. 123 | /// Otherwise, just call the closure. 124 | pub(crate) fn call_with_duration(metrics: Option<&Histogram>, closure: impl FnOnce() -> T) -> T { 125 | if let Some(metrics) = metrics { 126 | let start = Instant::now(); 127 | let result = closure(); 128 | metrics.observe(start.elapsed().as_millis() as f64); 129 | result 130 | } else { 131 | closure() 132 | } 133 | } 134 | 135 | /// Create a callback function which when called will add the input transformed by f to the histogram. 136 | pub(crate) fn observation_callback f64>( 137 | histogram: &Histogram, 138 | f: U, 139 | ) -> impl Fn(T) + use { 140 | let histogram = histogram.clone(); 141 | move |t| { 142 | histogram.observe(f(t)); 143 | } 144 | } 145 | 146 | pub(crate) fn status_callback(metrics: &IntCounterVec) -> impl Fn(bool) + use<> { 147 | let metrics = metrics.clone(); 148 | move |status: bool| { 149 | let value = match status { 150 | true => "success", 151 | false => "failure", 152 | }; 153 | metrics.with_label_values(&[value]).inc(); 154 | } 155 | } 156 | 157 | fn buckets(start: f64, end: f64, step: f64) -> Vec { 158 | let mut buckets = vec![]; 159 | let mut current = start; 160 | while current < end { 161 | buckets.push(current); 162 | current += step; 163 | } 164 | buckets.push(end); 165 | buckets 166 | } 167 | 168 | fn default_external_call_duration_buckets() -> Vec { 169 | buckets(50.0, 2000.0, 50.0) 170 | } 171 | 172 | fn default_fast_call_duration_buckets() -> Vec { 173 | buckets(10.0, 100.0, 10.0) 174 | } 175 | -------------------------------------------------------------------------------- /crates/key-server/src/signed_message.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | use crate::types::{ElGamalPublicKey, ElgamalVerificationKey}; 4 | use chrono::{DateTime, Utc}; 5 | use fastcrypto::ed25519::Ed25519PublicKey; 6 | use serde::{Deserialize, Serialize}; 7 | use sui_types::transaction::ProgrammableTransaction; 8 | use tracing::debug; 9 | 10 | /// The format of the personal message shown to the user. 11 | pub fn signed_message( 12 | package_name: String, // should use the original package id 13 | vk: &Ed25519PublicKey, 14 | creation_time: u64, 15 | ttl_min: u16, 16 | ) -> String { 17 | let res = format!( 18 | "Accessing keys of package {} for {} mins from {}, session key {}", 19 | package_name, 20 | ttl_min, 21 | DateTime::::from_timestamp((creation_time / 1000) as i64, 0) // convert to seconds 22 | .expect("tested that in the future"), 23 | vk, 24 | ); 25 | debug!("Signed message: {}", res.clone()); 26 | res 27 | } 28 | 29 | #[derive(Serialize, Deserialize)] 30 | struct RequestFormat { 31 | ptb: Vec, 32 | enc_key: Vec, 33 | enc_verification_key: Vec, 34 | } 35 | 36 | pub fn signed_request( 37 | ptb: &ProgrammableTransaction, 38 | enc_key: &ElGamalPublicKey, 39 | enc_verification_key: &ElgamalVerificationKey, 40 | ) -> Vec { 41 | let req = RequestFormat { 42 | ptb: bcs::to_bytes(&ptb).expect("should serialize"), 43 | enc_key: bcs::to_bytes(&enc_key).expect("should serialize"), 44 | enc_verification_key: bcs::to_bytes(&enc_verification_key).expect("should serialize"), 45 | }; 46 | bcs::to_bytes(&req).expect("should serialize") 47 | } 48 | 49 | #[cfg(test)] 50 | mod tests { 51 | use crate::signed_message::{signed_message, signed_request}; 52 | use crypto::elgamal::genkey; 53 | use fastcrypto::ed25519::Ed25519KeyPair; 54 | use fastcrypto::traits::KeyPair; 55 | use rand::rngs::StdRng; 56 | use rand::SeedableRng; 57 | use std::str::FromStr; 58 | use sui_types::base_types::ObjectID; 59 | use sui_types::crypto::deterministic_random_account_key; 60 | use sui_types::programmable_transaction_builder::ProgrammableTransactionBuilder; 61 | use sui_types::Identifier; 62 | 63 | #[test] 64 | fn test_signed_message_regression() { 65 | let pkg_id = 66 | ObjectID::from_str("0xc457b42d48924087ea3f22d35fd2fe9afdf5bdfe38cc51c0f14f3282f6d5") 67 | .unwrap(); 68 | let (_, kp): (_, Ed25519KeyPair) = deterministic_random_account_key(); 69 | let creation_time = 1622548800; // Fixed timestamp 70 | let ttl_min = 30; 71 | 72 | let expected_output = "Accessing keys of package 0x0000c457b42d48924087ea3f22d35fd2fe9afdf5bdfe38cc51c0f14f3282f6d5 for 30 mins from 1970-01-19 18:42:28 UTC, session key DX2rNYyNrapO+gBJp1sHQ2VVsQo2ghm7aA9wVxNJ13U="; 73 | 74 | let result = signed_message( 75 | pkg_id.to_hex_uncompressed(), 76 | kp.public(), 77 | creation_time, 78 | ttl_min, 79 | ); 80 | assert_eq!(result, expected_output); 81 | } 82 | 83 | #[test] 84 | fn test_signed_message_mvr_regression() { 85 | let (_, kp): (_, Ed25519KeyPair) = deterministic_random_account_key(); 86 | let creation_time = 1622548800; // Fixed timestamp 87 | let ttl_min = 30; 88 | 89 | let expected_output = "Accessing keys of package @my/package for 30 mins from 1970-01-19 18:42:28 UTC, session key DX2rNYyNrapO+gBJp1sHQ2VVsQo2ghm7aA9wVxNJ13U="; 90 | 91 | let result = signed_message( 92 | "@my/package".to_string(), 93 | kp.public(), 94 | creation_time, 95 | ttl_min, 96 | ); 97 | assert_eq!(result, expected_output); 98 | } 99 | 100 | #[test] 101 | fn test_signed_request_regression() { 102 | let mut builder = ProgrammableTransactionBuilder::new(); 103 | let pkg_id = ObjectID::from_str( 104 | "0xd92bc457b42d48924087ea3f22d35fd2fe9afdf5bdfe38cc51c0f14f3282f6d5", 105 | ) 106 | .unwrap(); 107 | builder.programmable_move_call( 108 | pkg_id, 109 | Identifier::new("bla").unwrap(), 110 | Identifier::new("seal_approve_x").unwrap(), 111 | vec![], 112 | vec![], 113 | ); 114 | let ptb = builder.finish(); 115 | let eg_keys = genkey(&mut StdRng::from_seed([0; 32])); 116 | 117 | let expected_output = "38000100d92bc457b42d48924087ea3f22d35fd2fe9afdf5bdfe38cc51c0f14f3282f6d503626c610e7365616c5f617070726f76655f7800003085946cd4134ecb8f7739bbd3522d1c8fab793c6c431a8b0b77b4f1885d4c096aafab755e7b8bce8688410cee9908fb29608faaf686c0dcbe3f65f1130e8be538d7ea009347d397f517188dfa14417618887a0412e404fff56efbafb63d1fc4970a1187b4ccb6e767a91822312e533fa53dee69f77ef5130be095e147ff3d40e96e8ddc4bf554dae3bcc34048fe9330cccf"; 118 | 119 | let result = signed_request(&ptb, &eg_keys.1, &eg_keys.2); 120 | assert_eq!(hex::encode(result), expected_output); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /crates/key-server/src/tests/e2e.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::tests::externals::get_key; 5 | use crate::tests::whitelist::{add_user_to_whitelist, create_whitelist, whitelist_create_ptb}; 6 | use crate::tests::SealTestCluster; 7 | use crypto::{seal_decrypt, seal_encrypt, EncryptionInput, IBEPublicKeys, IBEUserSecretKeys}; 8 | use tracing_test::traced_test; 9 | 10 | #[traced_test] 11 | #[tokio::test] 12 | async fn test_e2e() { 13 | let mut tc = SealTestCluster::new(3, 1).await; 14 | let (examples_package_id, _) = tc.publish("patterns").await; 15 | 16 | let (whitelist, cap) = create_whitelist(tc.get_mut(), examples_package_id).await; 17 | 18 | // Create test users 19 | let user_address = tc.users[0].address; 20 | add_user_to_whitelist( 21 | tc.get_mut(), 22 | examples_package_id, 23 | whitelist, 24 | cap, 25 | user_address, 26 | ) 27 | .await; 28 | 29 | // We know the version at this point 30 | let initial_shared_version = 3; 31 | 32 | // Get keys from two key servers 33 | let ptb = whitelist_create_ptb(examples_package_id, whitelist, initial_shared_version); 34 | 35 | // Send requests to the key servers and decrypt the responses 36 | let usk0 = get_key( 37 | &tc.servers[0].server, 38 | &examples_package_id, 39 | ptb.clone(), 40 | &tc.users[0].keypair, 41 | ) 42 | .await 43 | .unwrap(); 44 | let usk1 = get_key( 45 | &tc.servers[1].server, 46 | &examples_package_id, 47 | ptb, 48 | &tc.users[0].keypair, 49 | ) 50 | .await 51 | .unwrap(); 52 | 53 | // Register the three services on-chain 54 | let (package_id, _) = tc.publish("seal").await; 55 | 56 | let mut services = vec![]; 57 | for i in 0..3 { 58 | services.push( 59 | tc.register_key_server( 60 | package_id, 61 | &format!("Test server {}", i), 62 | &format!("https:://testserver{}.com", i), 63 | tc.servers[i].public_key, 64 | ) 65 | .await, 66 | ); 67 | } 68 | 69 | // Read the public keys from the service objects 70 | let pks = tc.get_public_keys(&services).await; 71 | assert_eq!( 72 | pks, 73 | tc.servers.iter().map(|s| s.public_key).collect::>() 74 | ); 75 | let pks = IBEPublicKeys::BonehFranklinBLS12381(pks); 76 | 77 | // Encrypt a message 78 | let message = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; 79 | let services = services.to_vec(); 80 | let encryption = seal_encrypt( 81 | examples_package_id, 82 | whitelist.to_vec(), 83 | services.clone(), 84 | &pks, 85 | 2, 86 | EncryptionInput::Aes256Gcm { 87 | data: message.to_vec(), 88 | aad: None, 89 | }, 90 | ) 91 | .unwrap() 92 | .0; 93 | 94 | // Decrypt the message 95 | let decryption = seal_decrypt( 96 | &encryption, 97 | &IBEUserSecretKeys::BonehFranklinBLS12381(services.into_iter().zip([usk0, usk1]).collect()), 98 | Some(&pks), 99 | ) 100 | .unwrap(); 101 | 102 | assert_eq!(decryption, message); 103 | } 104 | -------------------------------------------------------------------------------- /crates/key-server/src/tests/externals.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::externals::current_epoch_time; 5 | use crate::signed_message::signed_request; 6 | use crate::valid_ptb::ValidPtb; 7 | use crate::{ 8 | signed_message, 9 | types::{ElGamalPublicKey, ElgamalVerificationKey}, 10 | Certificate, Server, 11 | }; 12 | use crypto::elgamal; 13 | use fastcrypto::ed25519::Ed25519Signature; 14 | use fastcrypto::traits::{KeyPair, Signer}; 15 | use fastcrypto::{ed25519::Ed25519KeyPair, error::FastCryptoResult, groups::bls12381::G1Element}; 16 | use rand::thread_rng; 17 | use shared_crypto::intent::{Intent, IntentMessage, PersonalMessage}; 18 | use sui_types::{ 19 | base_types::ObjectID, crypto::Signature, signature::GenericSignature, 20 | transaction::ProgrammableTransaction, 21 | }; 22 | 23 | pub(super) fn sign( 24 | pkg_id: &ObjectID, 25 | ptb: &ProgrammableTransaction, 26 | eg_pk: &ElGamalPublicKey, 27 | eg_vk: &ElgamalVerificationKey, 28 | kp: &Ed25519KeyPair, 29 | creation_time: u64, 30 | ttl_min: u16, 31 | ) -> (Certificate, Ed25519Signature) { 32 | // We use the same eddsa keypair for both the certificate and the request signature 33 | 34 | // create the cert 35 | let msg_to_sign = signed_message::signed_message( 36 | pkg_id.to_hex_uncompressed(), 37 | kp.public(), 38 | creation_time, 39 | ttl_min, 40 | ); 41 | let personal_msg = PersonalMessage { 42 | message: msg_to_sign.as_bytes().to_vec(), 43 | }; 44 | let msg_with_intent = IntentMessage::new(Intent::personal_message(), personal_msg.clone()); 45 | let cert_sig = GenericSignature::Signature(Signature::new_secure(&msg_with_intent, kp)); 46 | let cert = Certificate { 47 | user: kp.public().into(), 48 | session_vk: kp.public().clone(), 49 | creation_time, 50 | ttl_min, 51 | signature: cert_sig, 52 | mvr_name: None, 53 | }; 54 | // session sig 55 | let signed_msg = signed_request(ptb, eg_pk, eg_vk); 56 | let request_sig = kp.sign(&signed_msg); 57 | (cert, request_sig) 58 | } 59 | 60 | pub(crate) async fn get_key( 61 | server: &Server, 62 | pkg_id: &ObjectID, 63 | ptb: ProgrammableTransaction, 64 | kp: &Ed25519KeyPair, 65 | ) -> FastCryptoResult { 66 | let (sk, pk, vk) = elgamal::genkey(&mut thread_rng()); 67 | let (cert, req_sig) = sign(pkg_id, &ptb, &pk, &vk, kp, current_epoch_time(), 1); 68 | server 69 | .check_request( 70 | &ValidPtb::try_from(ptb).unwrap(), 71 | &pk, 72 | &vk, 73 | &req_sig, 74 | &cert, 75 | 1000, 76 | None, 77 | None, 78 | None, 79 | ) 80 | .await 81 | .map(|ids| { 82 | elgamal::decrypt( 83 | &sk, 84 | &server.create_response(&ids, &pk).decryption_keys[0].encrypted_key, 85 | ) 86 | }) 87 | .map_err(|_| fastcrypto::error::FastCryptoError::GeneralOpaqueError) 88 | } 89 | -------------------------------------------------------------------------------- /crates/key-server/src/tests/pd.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crate::tests::externals::get_key; 5 | use crate::tests::SealTestCluster; 6 | use sui_sdk::{json::SuiJsonValue, rpc_types::ObjectChange}; 7 | use sui_types::base_types::{ObjectDigest, SequenceNumber}; 8 | use sui_types::{ 9 | base_types::{ObjectID, SuiAddress}, 10 | programmable_transaction_builder::ProgrammableTransactionBuilder, 11 | transaction::{ObjectArg, ProgrammableTransaction}, 12 | Identifier, 13 | }; 14 | use test_cluster::TestCluster; 15 | use tracing_test::traced_test; 16 | 17 | #[traced_test] 18 | #[tokio::test] 19 | async fn test_pd() { 20 | let mut tc = SealTestCluster::new(1, 2).await; 21 | 22 | let (package_id, _) = tc.publish("patterns").await; 23 | 24 | // create PrivateData with nonce=package_id, owned by addr1 25 | let (pd, version, digest) = 26 | create_private_data(tc.users[0].address, tc.get_mut(), package_id).await; 27 | 28 | // addr1 should have access 29 | let ptb = pd_create_ptb(tc.get_mut(), package_id, package_id, pd, version, digest).await; 30 | assert!( 31 | get_key(tc.server(), &package_id, ptb.clone(), &tc.users[0].keypair) 32 | .await 33 | .is_ok() 34 | ); 35 | // addr2 should not have access 36 | assert!(get_key(tc.server(), &package_id, ptb, &tc.users[1].keypair) 37 | .await 38 | .is_err()); 39 | 40 | // addr1 should not have access to a different nonce 41 | let ptb = pd_create_ptb( 42 | &mut tc.cluster, 43 | package_id, 44 | ObjectID::random(), 45 | pd, 46 | version, 47 | digest, 48 | ) 49 | .await; 50 | assert!( 51 | get_key(tc.server(), &package_id, ptb.clone(), &tc.users[0].keypair) 52 | .await 53 | .is_err() 54 | ); 55 | } 56 | 57 | pub(crate) async fn create_private_data( 58 | user: SuiAddress, 59 | cluster: &mut TestCluster, 60 | package_id: ObjectID, 61 | ) -> (ObjectID, SequenceNumber, ObjectDigest) { 62 | let builder = cluster.sui_client().transaction_builder(); 63 | let tx = builder 64 | .move_call( 65 | cluster.get_address_0(), 66 | package_id, 67 | "private_data", 68 | "store_entry", 69 | vec![], 70 | vec![ 71 | SuiJsonValue::from_object_id(package_id), 72 | SuiJsonValue::from_object_id(package_id), 73 | ], 74 | None, 75 | 50_000_000, 76 | None, 77 | ) 78 | .await 79 | .unwrap(); 80 | let response = cluster.sign_and_execute_transaction(&tx).await; 81 | 82 | let mut pd: Option = None; 83 | for created in response.object_changes.unwrap() { 84 | if let ObjectChange::Created { 85 | object_type, 86 | object_id, 87 | .. 88 | } = created 89 | { 90 | if object_type.name.as_str() == "PrivateData" { 91 | pd.replace(object_id); 92 | }; 93 | } 94 | } 95 | 96 | let builder = cluster.sui_client().transaction_builder(); 97 | let tx = builder 98 | .transfer_object(cluster.get_address_0(), pd.unwrap(), None, 50_000_000, user) 99 | .await 100 | .unwrap(); 101 | let response = cluster.sign_and_execute_transaction(&tx).await; 102 | assert!(response.status_ok().unwrap()); 103 | for modified in response.object_changes.unwrap() { 104 | if let ObjectChange::Mutated { 105 | object_type, 106 | object_id, 107 | version, 108 | digest, 109 | .. 110 | } = modified 111 | { 112 | if object_type.name.as_str() == "PrivateData" { 113 | return (object_id, version, digest); 114 | } 115 | } 116 | } 117 | 118 | panic!("should have found the pd object"); 119 | } 120 | 121 | async fn pd_create_ptb( 122 | cluster: &mut TestCluster, 123 | package_id: ObjectID, 124 | nonce: ObjectID, 125 | pd: ObjectID, 126 | version: SequenceNumber, 127 | digest: ObjectDigest, 128 | ) -> ProgrammableTransaction { 129 | let mut builder = ProgrammableTransactionBuilder::new(); 130 | // the id = creator || nonce which in this case is the package_id 131 | let id = [ 132 | bcs::to_bytes(&cluster.get_address_0()).unwrap(), 133 | bcs::to_bytes(&nonce).unwrap(), 134 | ] 135 | .concat(); 136 | let id = builder.pure(id).unwrap(); 137 | let pd = builder 138 | .obj(ObjectArg::ImmOrOwnedObject((pd, version, digest))) 139 | .unwrap(); 140 | 141 | builder.programmable_move_call( 142 | package_id, 143 | Identifier::new("private_data").unwrap(), 144 | Identifier::new("seal_approve").unwrap(), 145 | vec![], 146 | vec![id, pd], 147 | ); 148 | builder.finish() 149 | } 150 | -------------------------------------------------------------------------------- /crates/key-server/src/tests/server.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use std::time::{SystemTime, UNIX_EPOCH}; 5 | use tracing_test::traced_test; 6 | 7 | use crate::externals::get_latest_checkpoint_timestamp; 8 | use crate::tests::SealTestCluster; 9 | 10 | #[tokio::test] 11 | async fn test_get_latest_checkpoint_timestamp() { 12 | let tc = SealTestCluster::new(0, 0).await; 13 | 14 | let tolerance = 20000; 15 | let timestamp = get_latest_checkpoint_timestamp(tc.cluster.sui_client().clone()) 16 | .await 17 | .unwrap(); 18 | 19 | let actual_timestamp = SystemTime::now() 20 | .duration_since(UNIX_EPOCH) 21 | .expect("Time went backwards") 22 | .as_millis() as u64; 23 | 24 | let diff = actual_timestamp - timestamp; 25 | assert!(diff < tolerance); 26 | } 27 | 28 | #[tokio::test] 29 | async fn test_timestamp_updater() { 30 | let tc = SealTestCluster::new(1, 0).await; 31 | 32 | let mut receiver = tc 33 | .server() 34 | .spawn_latest_checkpoint_timestamp_updater(None) 35 | .await; 36 | 37 | let tolerance = 20000; 38 | 39 | let timestamp = *receiver.borrow_and_update(); 40 | let actual_timestamp = SystemTime::now() 41 | .duration_since(UNIX_EPOCH) 42 | .expect("Time went backwards") 43 | .as_millis() as u64; 44 | 45 | let diff = actual_timestamp - timestamp; 46 | assert!(diff < tolerance); 47 | 48 | // Get a new timestamp 49 | receiver 50 | .changed() 51 | .await 52 | .expect("Failed to get latest timestamp"); 53 | let new_timestamp = *receiver.borrow_and_update(); 54 | assert!(new_timestamp >= timestamp); 55 | } 56 | 57 | #[traced_test] 58 | #[tokio::test] 59 | async fn test_rgp_updater() { 60 | let tc = SealTestCluster::new(1, 0).await; 61 | 62 | let mut receiver = tc.server().spawn_reference_gas_price_updater(None).await; 63 | 64 | let price = *receiver.borrow_and_update(); 65 | assert_eq!(price, tc.cluster.get_reference_gas_price().await); 66 | 67 | receiver.changed().await.expect("Failed to get latest rgp"); 68 | } 69 | -------------------------------------------------------------------------------- /crates/key-server/src/tests/whitelist_v1/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } 9 | 10 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 11 | # Revision can be a branch, a tag, and a commit hash. 12 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 13 | 14 | # For local dependencies use `local = path`. Path is relative to the package root 15 | # Local = { local = "../path/to" } 16 | 17 | # To resolve a version conflict and force a specific version for dependency 18 | # override use `override = true` 19 | # Override = { local = "../conflicting/version", override = true } 20 | 21 | [addresses] 22 | test = "0x0" 23 | 24 | # Named addresses will be accessible in Move as `@name`. They're also exported: 25 | # for example, `std = "0x1"` is exported by the Standard Library. 26 | # alice = "0xA11CE" 27 | 28 | [dev-dependencies] 29 | # The dev-dependencies section allows overriding dependencies for `--test` and 30 | # `--dev` modes. You can introduce test-only dependencies here. 31 | # Local = { local = "../path/to/dev-build" } 32 | 33 | [dev-addresses] 34 | # The dev-addresses section allows overwriting named addresses for the `--test` 35 | # and `--dev` modes. 36 | # alice = "0xB0B" 37 | 38 | -------------------------------------------------------------------------------- /crates/key-server/src/tests/whitelist_v1/sources/whitelist_v1.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // copy of whitelist.move, just updated the version 5 | 6 | module test::whitelist; 7 | 8 | use sui::table; 9 | 10 | const ENoAccess: u64 = 1; 11 | const EInvalidCap: u64 = 2; 12 | const EDuplicate: u64 = 3; 13 | const ENotInWhitelist: u64 = 4; 14 | const EWrongVersion: u64 = 5; 15 | 16 | const VERSION: u64 = 1; 17 | 18 | public struct Whitelist has key { 19 | id: UID, 20 | version: u64, 21 | addresses: table::Table, 22 | } 23 | 24 | public struct Cap has key { 25 | id: UID, 26 | wl_id: ID, 27 | } 28 | 29 | ////////////////////////////////////////// 30 | /////// Simple whitelist with an admin cap 31 | 32 | /// Create a whitelist with an admin cap. 33 | /// The associated key-ids are [pkg id][whitelist id][nonce] for any nonce (thus 34 | /// many key-ids can be created for the same whitelist). 35 | public fun create_whitelist(ctx: &mut TxContext): (Cap, Whitelist) { 36 | let wl = Whitelist { 37 | id: object::new(ctx), 38 | version: VERSION, 39 | addresses: table::new(ctx), 40 | }; 41 | let cap = Cap { 42 | id: object::new(ctx), 43 | wl_id: object::id(&wl), 44 | }; 45 | (cap, wl) 46 | } 47 | 48 | // Helper function for creating a whitelist and send it back to sender. 49 | entry fun create_whitelist_entry(ctx: &mut TxContext) { 50 | let (cap, wl) = create_whitelist(ctx); 51 | transfer::share_object(wl); 52 | transfer::transfer(cap, ctx.sender()); 53 | } 54 | 55 | public fun add(wl: &mut Whitelist, cap: &Cap, account: address) { 56 | assert!(cap.wl_id == object::id(wl), EInvalidCap); 57 | assert!(!wl.addresses.contains(account), EDuplicate); 58 | wl.addresses.add(account, true); 59 | } 60 | 61 | public fun remove(wl: &mut Whitelist, cap: &Cap, account: address) { 62 | assert!(cap.wl_id == object::id(wl), EInvalidCap); 63 | assert!(wl.addresses.contains(account), ENotInWhitelist); 64 | wl.addresses.remove(account); 65 | } 66 | 67 | ////////////////////////////////////////////////////////// 68 | /// Access control 69 | /// key format: [pkg id][whitelist id][random nonce] 70 | /// (Alternative key format: [pkg id][creator address][random nonce] - see private_data.move) 71 | 72 | /// All whitelisted addresses can access all IDs with the prefix of the whitelist 73 | fun check_policy(caller: address, id: vector, wl: &Whitelist): bool { 74 | // Check we are using the right version of the package. 75 | assert!(wl.version == VERSION, EWrongVersion); 76 | 77 | // Check if the id has the right prefix 78 | let prefix = wl.id.to_bytes(); 79 | let mut i = 0; 80 | if (prefix.length() > id.length()) { 81 | return false 82 | }; 83 | while (i < prefix.length()) { 84 | if (prefix[i] != id[i]) { 85 | return false 86 | }; 87 | i = i + 1; 88 | }; 89 | 90 | // Check if user is in the whitelist 91 | wl.addresses.contains(caller) 92 | } 93 | 94 | entry fun seal_approve(id: vector, wl: &Whitelist, ctx: &TxContext) { 95 | assert!(check_policy(ctx.sender(), id, wl), ENoAccess); 96 | } 97 | 98 | #[test_only] 99 | public fun destroy_for_testing(wl: Whitelist, cap: Cap) { 100 | let Whitelist { id, version: _, addresses } = wl; 101 | addresses.drop(); 102 | object::delete(id); 103 | let Cap { id, .. } = cap; 104 | object::delete(id); 105 | } 106 | 107 | #[test] 108 | fun test_approve() { 109 | let ctx = &mut tx_context::dummy(); 110 | let (cap, mut wl) = create_whitelist(ctx); 111 | wl.add(&cap, @0x1); 112 | wl.remove(&cap, @0x1); 113 | wl.add(&cap, @0x2); 114 | 115 | // Fail for invalid id 116 | assert!(!check_policy(@0x2, b"123", &wl), 1); 117 | // Work for valid id, user 2 is in the whitelist 118 | let mut obj_id = object::id(&wl).to_bytes(); 119 | obj_id.push_back(11); 120 | assert!(check_policy(@0x2, obj_id, &wl), 1); 121 | // Fail for user 1 122 | assert!(!check_policy(@0x1, obj_id, &wl), 1); 123 | 124 | destroy_for_testing(wl, cap); 125 | } 126 | -------------------------------------------------------------------------------- /crates/key-server/src/tests/whitelist_v2/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } 9 | 10 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 11 | # Revision can be a branch, a tag, and a commit hash. 12 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 13 | 14 | # For local dependencies use `local = path`. Path is relative to the package root 15 | # Local = { local = "../path/to" } 16 | 17 | # To resolve a version conflict and force a specific version for dependency 18 | # override use `override = true` 19 | # Override = { local = "../conflicting/version", override = true } 20 | 21 | [addresses] 22 | test = "0x0" 23 | 24 | # Named addresses will be accessible in Move as `@name`. They're also exported: 25 | # for example, `std = "0x1"` is exported by the Standard Library. 26 | # alice = "0xA11CE" 27 | 28 | [dev-dependencies] 29 | # The dev-dependencies section allows overriding dependencies for `--test` and 30 | # `--dev` modes. You can introduce test-only dependencies here. 31 | # Local = { local = "../path/to/dev-build" } 32 | 33 | [dev-addresses] 34 | # The dev-addresses section allows overwriting named addresses for the `--test` 35 | # and `--dev` modes. 36 | # alice = "0xB0B" 37 | 38 | -------------------------------------------------------------------------------- /crates/key-server/src/tests/whitelist_v2/sources/whitelist_v2.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // copy of whitelist.move, just updated the version 5 | 6 | module test::whitelist; 7 | 8 | use sui::table; 9 | 10 | const ENoAccess: u64 = 1; 11 | const EInvalidCap: u64 = 2; 12 | const EDuplicate: u64 = 3; 13 | const ENotInWhitelist: u64 = 4; 14 | const EWrongVersion: u64 = 5; 15 | 16 | const VERSION: u64 = 2; 17 | 18 | public struct Whitelist has key { 19 | id: UID, 20 | version: u64, 21 | addresses: table::Table, 22 | } 23 | 24 | public struct Cap has key { 25 | id: UID, 26 | wl_id: ID, 27 | } 28 | 29 | ////////////////////////////////////////// 30 | /////// Simple whitelist with an admin cap 31 | 32 | /// Create a whitelist with an admin cap. 33 | /// The associated key-ids are [pkg id][whitelist id][nonce] for any nonce (thus 34 | /// many key-ids can be created for the same whitelist). 35 | public fun create_whitelist(ctx: &mut TxContext): (Cap, Whitelist) { 36 | let wl = Whitelist { 37 | id: object::new(ctx), 38 | version: VERSION, 39 | addresses: table::new(ctx), 40 | }; 41 | let cap = Cap { 42 | id: object::new(ctx), 43 | wl_id: object::id(&wl), 44 | }; 45 | (cap, wl) 46 | } 47 | 48 | // Helper function for creating a whitelist and send it back to sender. 49 | entry fun create_whitelist_entry(ctx: &mut TxContext) { 50 | let (cap, wl) = create_whitelist(ctx); 51 | transfer::share_object(wl); 52 | transfer::transfer(cap, ctx.sender()); 53 | } 54 | 55 | public fun add(wl: &mut Whitelist, cap: &Cap, account: address) { 56 | assert!(cap.wl_id == object::id(wl), EInvalidCap); 57 | assert!(!wl.addresses.contains(account), EDuplicate); 58 | wl.addresses.add(account, true); 59 | } 60 | 61 | public fun remove(wl: &mut Whitelist, cap: &Cap, account: address) { 62 | assert!(cap.wl_id == object::id(wl), EInvalidCap); 63 | assert!(wl.addresses.contains(account), ENotInWhitelist); 64 | wl.addresses.remove(account); 65 | } 66 | 67 | entry fun upgrade_version(wl: &mut Whitelist, cap: &Cap) { 68 | assert!(cap.wl_id == object::id(wl), EInvalidCap); 69 | assert!(wl.version < VERSION, EWrongVersion); 70 | wl.version = VERSION; 71 | } 72 | 73 | ////////////////////////////////////////////////////////// 74 | /// Access control 75 | /// key format: [pkg id][whitelist id][random nonce] 76 | /// (Alternative key format: [pkg id][creator address][random nonce] - see private_data.move) 77 | 78 | /// All whitelisted addresses can access all IDs with the prefix of the whitelist 79 | fun check_policy(caller: address, id: vector, wl: &Whitelist): bool { 80 | // Check we are using the right version of the package. 81 | assert!(wl.version == VERSION, EWrongVersion); 82 | 83 | // Check if the id has the right prefix 84 | let prefix = wl.id.to_bytes(); 85 | let mut i = 0; 86 | if (prefix.length() > id.length()) { 87 | return false 88 | }; 89 | while (i < prefix.length()) { 90 | if (prefix[i] != id[i]) { 91 | return false 92 | }; 93 | i = i + 1; 94 | }; 95 | 96 | // Check if user is in the whitelist 97 | wl.addresses.contains(caller) 98 | } 99 | 100 | entry fun seal_approve(id: vector, wl: &Whitelist, ctx: &TxContext) { 101 | assert!(check_policy(ctx.sender(), id, wl), ENoAccess); 102 | } 103 | 104 | #[test_only] 105 | public fun destroy_for_testing(wl: Whitelist, cap: Cap) { 106 | let Whitelist { id, version: _, addresses } = wl; 107 | addresses.drop(); 108 | object::delete(id); 109 | let Cap { id, .. } = cap; 110 | object::delete(id); 111 | } 112 | 113 | #[test] 114 | fun test_approve() { 115 | let ctx = &mut tx_context::dummy(); 116 | let (cap, mut wl) = create_whitelist(ctx); 117 | wl.add(&cap, @0x1); 118 | wl.remove(&cap, @0x1); 119 | wl.add(&cap, @0x2); 120 | 121 | // Fail for invalid id 122 | assert!(!check_policy(@0x2, b"123", &wl), 1); 123 | // Work for valid id, user 2 is in the whitelist 124 | let mut obj_id = object::id(&wl).to_bytes(); 125 | obj_id.push_back(11); 126 | assert!(check_policy(@0x2, obj_id, &wl), 1); 127 | // Fail for user 1 128 | assert!(!check_policy(@0x1, obj_id, &wl), 1); 129 | 130 | destroy_for_testing(wl, cap); 131 | } 132 | -------------------------------------------------------------------------------- /crates/key-server/src/types.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | use crypto::elgamal; 5 | use crypto::ibe; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// The Identity-based encryption types. 9 | pub type IbeMasterKey = ibe::MasterKey; 10 | type IbeDerivedKey = ibe::UserSecretKey; 11 | type IbePublicKey = ibe::PublicKey; 12 | 13 | /// ElGamal related types. 14 | pub type ElGamalPublicKey = elgamal::PublicKey; 15 | pub type ElgamalEncryption = elgamal::Encryption; 16 | pub type ElgamalVerificationKey = elgamal::VerificationKey; 17 | 18 | /// Proof-of-possession of a key-servers master key. 19 | pub type MasterKeyPOP = ibe::ProofOfPossession; 20 | 21 | #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] 22 | pub enum Network { 23 | Devnet, 24 | Testnet, 25 | Mainnet, 26 | Custom { 27 | node_url: String, 28 | graphql_url: String, 29 | }, 30 | #[cfg(test)] 31 | TestCluster, 32 | } 33 | 34 | impl Network { 35 | pub fn node_url(&self) -> String { 36 | match self { 37 | Network::Devnet => "https://fullnode.devnet.sui.io:443".into(), 38 | Network::Testnet => "https://fullnode.testnet.sui.io:443".into(), 39 | Network::Mainnet => "https://fullnode.mainnet.sui.io:443".into(), 40 | Network::Custom { node_url, .. } => node_url.clone(), 41 | #[cfg(test)] 42 | Network::TestCluster => panic!(), // Currently not used, but can be found from cluster.rpc_url() if needed 43 | } 44 | } 45 | 46 | pub fn graphql_url(&self) -> String { 47 | match self { 48 | Network::Devnet => "https://sui-devnet.mystenlabs.com/graphql".into(), 49 | Network::Testnet => "https://sui-testnet.mystenlabs.com/graphql".into(), 50 | Network::Mainnet => "https://sui-mainnet.mystenlabs.com/graphql".into(), 51 | Network::Custom { graphql_url, .. } => graphql_url.clone(), 52 | #[cfg(test)] 53 | Network::TestCluster => panic!("GraphQL is not available on test cluster"), 54 | } 55 | } 56 | 57 | pub fn from_str(str: &str) -> Self { 58 | match str.to_ascii_lowercase().as_str() { 59 | "devnet" => Network::Devnet, 60 | "testnet" => Network::Testnet, 61 | "mainnet" => Network::Mainnet, 62 | "custom" => Network::Custom { 63 | node_url: std::env::var("NODE_URL").expect("NODE_URL must be set"), 64 | graphql_url: std::env::var("GRAPHQL_URL").expect("GRAPHQL_URL must be set"), 65 | }, 66 | _ => panic!("Unknown network: {}", str), 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /crates/seal-cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "seal-cli" 3 | version.workspace = true 4 | authors.workspace = true 5 | edition.workspace = true 6 | license.workspace = true 7 | 8 | [dependencies] 9 | clap.workspace = true 10 | fastcrypto.workspace = true 11 | rand.workspace = true 12 | serde.workspace = true 13 | bcs.workspace = true 14 | 15 | crypto = { path = "../crypto" } 16 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | /frontend/node_modules 2 | /frontend/.env 3 | /move/build 4 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | This reference application includes two different functionalities to showcase Seal's capabilities: 4 | 5 | **Allowlist-Based Access** 6 | 7 | An allowlist enables a creator to manage a set of authorized addresses by adding or removing members as needed. The creator can associate encrypted files with the allowlist, ensuring that only designated members can access the content. 8 | 9 | To gain access, a user must sign a personal message, which is then verified against the allowlist. Upon successful verification, the user retrieves two key shares from two independent servers. If the membership check is validated, the user combines these key shares to derive the decryption key, which allows them to access and view the decrypted content. 10 | 11 | **Subscription-Based Access** 12 | 13 | A subscription service allows a creator to define a service with a specified price (denominated in MIST) and a time period (X minutes). When a user purchases a subscription, it is represented as a non-fungible token (NFT) stored on-chain. 14 | 15 | To access the service, the user must sign a personal message, which is then validated by the servers. The servers verify whether the subscription is active for the next X minutes by referencing the on-chain Sui clock and ensuring the user holds a valid subscription NFT. If the conditions are met, the user retrieves the decryption key, enabling access to the decrypted content. 16 | 17 | > **IMPORTANT** 18 | > This reference application serves as a demonstration of Seal's capabilities and is intended solely as a playground environment. It does not provide guarantees of uptime, reliability, or correctness. Users are strongly advised not to connect their primary wallets or upload any sensitive content while utilizing this application. 19 | > 20 | > By accessing and using this reference application, you acknowledge and accept the inherent risks associated with cryptographic and blockchain-based systems. You confirm that you possess a working knowledge of Digital Assets and understand the implications of their usage. 21 | > 22 | > You further acknowledge that you are solely responsible for all actions taken within this application, including but not limited to connecting your wallet, adding content, or providing approvals or permissions by cryptographically signing blockchain messages or transactions. 23 | > 24 | > Mysten Labs, Inc., along with its affiliates and employees, assumes no responsibility for the security, integrity, or compliance of any content added or actions performed within this reference application. Users must exercise caution and use this application at their own risk. 25 | 26 | ## Run locally 27 | 28 | ```bash 29 | cd frontend 30 | pnpm install 31 | pnpm dev 32 | ``` 33 | -------------------------------------------------------------------------------- /examples/frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | es2020: true, 6 | node: true, 7 | }, 8 | extends: [ 9 | 'eslint:recommended', 10 | 'plugin:@typescript-eslint/recommended', 11 | 'plugin:react/recommended', 12 | 'plugin:react-hooks/recommended', 13 | ], 14 | ignorePatterns: ['dist', '.eslintrc.cjs'], 15 | parser: '@typescript-eslint/parser', 16 | plugins: ['react-refresh', 'react', '@typescript-eslint'], 17 | rules: { 18 | 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }], 19 | 'react/react-in-jsx-scope': 'off', 20 | 'react/prop-types': 'off', 21 | }, 22 | settings: { 23 | react: { 24 | version: 'detect', 25 | }, 26 | }, 27 | parserOptions: { 28 | ecmaVersion: 'latest', 29 | sourceType: 'module', 30 | ecmaFeatures: { 31 | jsx: true, 32 | }, 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /examples/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxSingleQuote": false, 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /examples/frontend/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Seal Example Apps 13 | 14 | 59 | 60 | 61 |
62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /examples/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seal-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "lint:fix": "eslint . --ext ts,tsx --fix && prettier --write 'src/**/*.{js,jsx,ts,tsx,json,css,scss,md}'", 11 | "format": "prettier --write '**/*.{js,jsx,ts,tsx,json,css,scss,md}'", 12 | "prettier:check": "prettier -c --ignore-unknown .", 13 | "prettier:fix": "prettier -w --ignore-unknown .", 14 | "prettier:fix:watch": "onchange '**' -i -f add -f change -j 5 -- prettier -w --ignore-unknown {{file}}", 15 | "eslint:check": "eslint --max-warnings=0 .", 16 | "eslint:fix": "pnpm run eslint:check --fix", 17 | "preview": "vite preview", 18 | "test": "jest" 19 | }, 20 | "directories": { 21 | "test": "tests" 22 | }, 23 | "dependencies": { 24 | "@mysten/bcs": "^1.6.0", 25 | "@mysten/dapp-kit": "^0.16.9", 26 | "@mysten/seal": "0.4.10", 27 | "@mysten/sui": "^1.30.5", 28 | "@radix-ui/colors": "^3.0.0", 29 | "@radix-ui/react-icons": "^1.3.2", 30 | "@radix-ui/react-label": "^2.1.2", 31 | "@radix-ui/react-popover": "^1.1.6", 32 | "@radix-ui/react-slot": "^1.1.2", 33 | "@radix-ui/themes": "^3.2.1", 34 | "@tanstack/react-query": "^5.71.10", 35 | "clsx": "^2.1.1", 36 | "idb-keyval": "3.0.0", 37 | "lucide-react": "0.487.0", 38 | "react": "^19.1.0", 39 | "react-dom": "^19.1.0", 40 | "react-router-dom": "^7.4.1", 41 | "tailwind-merge": "^3.1.0" 42 | }, 43 | "devDependencies": { 44 | "@types/react": "^19.1.0", 45 | "@types/react-dom": "^19.1.1", 46 | "@typescript-eslint/eslint-plugin": "^8.29.0", 47 | "@typescript-eslint/parser": "^8.29.0", 48 | "@vitejs/plugin-react": "4.3.4", 49 | "@vitejs/plugin-react-swc": "^3.8.1", 50 | "eslint": "^9.23.0", 51 | "eslint-plugin-react": "^7.37.5", 52 | "eslint-plugin-react-hooks": "^5.2.0", 53 | "eslint-plugin-react-refresh": "^0.4.19", 54 | "prettier": "^3.5.3", 55 | "typescript": "^5.8.2", 56 | "vite": "^6.2.5" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /examples/frontend/src/Allowlist.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { useCurrentAccount, useSignAndExecuteTransaction, useSuiClient } from '@mysten/dapp-kit'; 4 | import { Transaction } from '@mysten/sui/transactions'; 5 | import { Button, Card, Flex } from '@radix-ui/themes'; 6 | import { useNetworkVariable } from './networkConfig'; 7 | import { useEffect, useState } from 'react'; 8 | import { X } from 'lucide-react'; 9 | import { useParams } from 'react-router-dom'; 10 | import { isValidSuiAddress } from '@mysten/sui/utils'; 11 | import { getObjectExplorerLink } from './utils'; 12 | 13 | export interface Allowlist { 14 | id: string; 15 | name: string; 16 | list: string[]; 17 | } 18 | 19 | interface AllowlistProps { 20 | setRecipientAllowlist: React.Dispatch>; 21 | setCapId: React.Dispatch>; 22 | } 23 | 24 | export function Allowlist({ setRecipientAllowlist, setCapId }: AllowlistProps) { 25 | const packageId = useNetworkVariable('packageId'); 26 | const suiClient = useSuiClient(); 27 | const currentAccount = useCurrentAccount(); 28 | const [allowlist, setAllowlist] = useState(); 29 | const { id } = useParams(); 30 | const [capId, setInnerCapId] = useState(); 31 | 32 | useEffect(() => { 33 | async function getAllowlist() { 34 | // load all caps 35 | const res = await suiClient.getOwnedObjects({ 36 | owner: currentAccount?.address!, 37 | options: { 38 | showContent: true, 39 | showType: true, 40 | }, 41 | filter: { 42 | StructType: `${packageId}::allowlist::Cap`, 43 | }, 44 | }); 45 | 46 | // find the cap for the given allowlist id 47 | const capId = res.data 48 | .map((obj) => { 49 | const fields = (obj!.data!.content as { fields: any }).fields; 50 | return { 51 | id: fields?.id.id, 52 | allowlist_id: fields?.allowlist_id, 53 | }; 54 | }) 55 | .filter((item) => item.allowlist_id === id) 56 | .map((item) => item.id) as string[]; 57 | setCapId(capId[0]); 58 | setInnerCapId(capId[0]); 59 | 60 | // load the allowlist for the given id 61 | const allowlist = await suiClient.getObject({ 62 | id: id!, 63 | options: { showContent: true }, 64 | }); 65 | const fields = (allowlist.data?.content as { fields: any })?.fields || {}; 66 | setAllowlist({ 67 | id: id!, 68 | name: fields.name, 69 | list: fields.list, 70 | }); 71 | setRecipientAllowlist(id!); 72 | } 73 | 74 | // Call getAllowlist immediately 75 | getAllowlist(); 76 | 77 | // Set up interval to call getAllowlist every 3 seconds 78 | const intervalId = setInterval(() => { 79 | getAllowlist(); 80 | }, 3000); 81 | 82 | // Cleanup interval on component unmount 83 | return () => clearInterval(intervalId); 84 | }, [id, currentAccount?.address]); // Only depend on id 85 | 86 | const { mutate: signAndExecute } = useSignAndExecuteTransaction({ 87 | execute: async ({ bytes, signature }) => 88 | await suiClient.executeTransactionBlock({ 89 | transactionBlock: bytes, 90 | signature, 91 | options: { 92 | showRawEffects: true, 93 | showEffects: true, 94 | }, 95 | }), 96 | }); 97 | 98 | const addItem = (newAddressToAdd: string, wl_id: string, cap_id: string) => { 99 | if (newAddressToAdd.trim() !== '') { 100 | if (!isValidSuiAddress(newAddressToAdd.trim())) { 101 | alert('Invalid address'); 102 | return; 103 | } 104 | const tx = new Transaction(); 105 | tx.moveCall({ 106 | arguments: [tx.object(wl_id), tx.object(cap_id), tx.pure.address(newAddressToAdd.trim())], 107 | target: `${packageId}::allowlist::add`, 108 | }); 109 | tx.setGasBudget(10000000); 110 | 111 | signAndExecute( 112 | { 113 | transaction: tx, 114 | }, 115 | { 116 | onSuccess: async (result) => { 117 | console.log('res', result); 118 | }, 119 | }, 120 | ); 121 | } 122 | }; 123 | 124 | const removeItem = (addressToRemove: string, wl_id: string, cap_id: string) => { 125 | if (addressToRemove.trim() !== '') { 126 | const tx = new Transaction(); 127 | tx.moveCall({ 128 | arguments: [tx.object(wl_id), tx.object(cap_id), tx.pure.address(addressToRemove.trim())], 129 | target: `${packageId}::allowlist::remove`, 130 | }); 131 | tx.setGasBudget(10000000); 132 | 133 | signAndExecute( 134 | { 135 | transaction: tx, 136 | }, 137 | { 138 | onSuccess: async (result) => { 139 | console.log('res', result); 140 | }, 141 | }, 142 | ); 143 | } 144 | }; 145 | 146 | return ( 147 | 148 | 149 |

150 | Admin View: Allowlist {allowlist?.name} (ID{' '} 151 | {allowlist?.id && getObjectExplorerLink(allowlist.id)}) 152 |

153 |

154 | Share  155 | 161 | this link 162 | {' '} 163 | with users to access the files associated with this allowlist. 164 |

165 | 166 | 167 | 168 | 177 | 178 | 179 |

Allowed Users:

180 | {Array.isArray(allowlist?.list) && allowlist?.list.length > 0 ? ( 181 |
    182 | {allowlist?.list.map((listItem, itemIndex) => ( 183 |
  • 184 | 185 |

    {listItem}

    186 | 194 |
    195 |
  • 196 | ))} 197 |
198 | ) : ( 199 |

No user in this allowlist.

200 | )} 201 |
202 |
203 | ); 204 | } 205 | -------------------------------------------------------------------------------- /examples/frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import React from 'react'; 5 | import { ConnectButton, useCurrentAccount } from '@mysten/dapp-kit'; 6 | import { Box, Button, Card, Container, Flex, Grid } from '@radix-ui/themes'; 7 | import { CreateAllowlist } from './CreateAllowlist'; 8 | import { Allowlist } from './Allowlist'; 9 | import WalrusUpload from './EncryptAndUpload'; 10 | import { useState } from 'react'; 11 | import { CreateService } from './CreateSubscriptionService'; 12 | import FeedsToSubscribe from './SubscriptionView'; 13 | import { Service } from './SubscriptionService'; 14 | import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'; 15 | import { AllAllowlist } from './OwnedAllowlists'; 16 | import { AllServices } from './OwnedSubscriptionServices'; 17 | import Feeds from './AllowlistView'; 18 | 19 | function LandingPage() { 20 | return ( 21 | 22 | 23 | 24 |
25 |

Allowlist Example

26 |

27 | Shows how a creator can define an allowlist based access. The creator first creates an 28 | allowlist and can add or remove users in the list. The creator can then associate 29 | encrypted files to the allowlist. Only users in the allowlist have access to decrypt 30 | the files. 31 |

32 |
33 | 34 | 35 | 36 |
37 |
38 | 39 | 40 |
41 |

Subscription Example

42 |

43 | Shows how a creator can define a subscription based access to its published files. The 44 | creator defines subcription fee and how long a subscription is valid for. The creator 45 | can then associate encrypted files to the service. Only users who have purchased a 46 | subscription (NFT) have access to decrypt the files, along with the condition that the 47 | subscription must not have expired (i.e. the subscription creation timestamp plus the 48 | TTL is smaller than the current clock time). 49 |

50 |
51 | 52 | 53 | 54 |
55 |
56 |
57 | ); 58 | } 59 | 60 | function App() { 61 | const currentAccount = useCurrentAccount(); 62 | const [recipientAllowlist, setRecipientAllowlist] = useState(''); 63 | const [capId, setCapId] = useState(''); 64 | return ( 65 | 66 | 67 |

Seal Example Apps

68 | {/*

TODO: add seal logo

*/} 69 | 70 | 71 | 72 |
73 | 74 |

75 | 1. Code is available{' '} 76 | here. 77 |

78 |

79 | 2. These examples are for Testnet only. Make sure you wallet is set to Testnet and has 80 | some balance (can request from faucet.sui.io). 81 |

82 |

83 | 3. Blobs are only stored on Walrus Testnet for 1 epoch by default, older files cannot be 84 | retrieved even if you have access. 85 |

86 |

87 | 4. Currently only image files are supported, and the UI is minimal, designed for demo 88 | purposes only! 89 |

90 |

91 | 5. If you encounter issues when uploading to or reading from Walrus using the example 92 | frontend, it usually means the public publisher and/or aggregator configured in 93 | `vite.config.ts` is not available. This example does not guarantee performance and 94 | downstream service quality and is only for demo purpose. In your own application, consider 95 | running your own publisher and/or aggregator according to{' '} 96 | 97 | the documentation 98 | 99 | . Or consider choosing and monitoring other reliable public publisher and aggregator from{' '} 100 | the list. 101 |

102 |
103 | {currentAccount ? ( 104 | 105 | 106 | } /> 107 | 111 | } /> 112 | 116 | 120 | 125 | 126 | } 127 | /> 128 | } /> 129 | } 132 | /> 133 | 134 | } 135 | /> 136 | 140 | } /> 141 | 145 | 149 | 154 | 155 | } 156 | /> 157 | } /> 158 | } 161 | /> 162 | 163 | } 164 | /> 165 | 166 | 167 | ) : ( 168 |

Please connect your wallet to continue

169 | )} 170 |
171 | ); 172 | } 173 | 174 | export default App; 175 | -------------------------------------------------------------------------------- /examples/frontend/src/CreateAllowlist.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Transaction } from '@mysten/sui/transactions'; 5 | import { Button, Card, Flex } from '@radix-ui/themes'; 6 | import { useSignAndExecuteTransaction, useSuiClient } from '@mysten/dapp-kit'; 7 | import { useState } from 'react'; 8 | import { useNetworkVariable } from './networkConfig'; 9 | import { useNavigate } from 'react-router-dom'; 10 | 11 | export function CreateAllowlist() { 12 | const navigate = useNavigate(); 13 | const [name, setName] = useState(''); 14 | const packageId = useNetworkVariable('packageId'); 15 | const suiClient = useSuiClient(); 16 | const { mutate: signAndExecute } = useSignAndExecuteTransaction({ 17 | execute: async ({ bytes, signature }) => 18 | await suiClient.executeTransactionBlock({ 19 | transactionBlock: bytes, 20 | signature, 21 | options: { 22 | showRawEffects: true, 23 | showEffects: true, 24 | }, 25 | }), 26 | }); 27 | 28 | function createAllowlist(name: string) { 29 | if (name === '') { 30 | alert('Please enter a name for the allowlist'); 31 | return; 32 | } 33 | const tx = new Transaction(); 34 | tx.moveCall({ 35 | target: `${packageId}::allowlist::create_allowlist_entry`, 36 | arguments: [tx.pure.string(name)], 37 | }); 38 | tx.setGasBudget(10000000); 39 | signAndExecute( 40 | { 41 | transaction: tx, 42 | }, 43 | { 44 | onSuccess: async (result) => { 45 | console.log('res', result); 46 | // Extract the created allowlist object ID from the transaction result 47 | const allowlistObject = result.effects?.created?.find( 48 | (item) => item.owner && typeof item.owner === 'object' && 'Shared' in item.owner, 49 | ); 50 | const createdObjectId = allowlistObject?.reference?.objectId; 51 | if (createdObjectId) { 52 | window.open( 53 | `${window.location.origin}/allowlist-example/admin/allowlist/${createdObjectId}`, 54 | '_blank', 55 | ); 56 | } 57 | }, 58 | }, 59 | ); 60 | } 61 | 62 | const handleViewAll = () => { 63 | navigate(`/allowlist-example/admin/allowlists`); 64 | }; 65 | 66 | return ( 67 | 68 |

Admin View: Allowlist

69 | 70 | setName(e.target.value)} /> 71 | 79 | 82 | 83 |
84 | ); 85 | } 86 | -------------------------------------------------------------------------------- /examples/frontend/src/CreateSubscriptionService.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { Transaction } from '@mysten/sui/transactions'; 5 | import { Button, Card, Flex } from '@radix-ui/themes'; 6 | import { useSignAndExecuteTransaction, useSuiClient } from '@mysten/dapp-kit'; 7 | import { useState } from 'react'; 8 | import { useNetworkVariable } from './networkConfig'; 9 | import { useNavigate } from 'react-router-dom'; 10 | 11 | export function CreateService() { 12 | const [price, setPrice] = useState(''); 13 | const [ttl, setTtl] = useState(''); 14 | const [name, setName] = useState(''); 15 | const packageId = useNetworkVariable('packageId'); 16 | const suiClient = useSuiClient(); 17 | const navigate = useNavigate(); 18 | const { mutate: signAndExecute } = useSignAndExecuteTransaction({ 19 | execute: async ({ bytes, signature }) => 20 | await suiClient.executeTransactionBlock({ 21 | transactionBlock: bytes, 22 | signature, 23 | options: { 24 | showRawEffects: true, 25 | showEffects: true, 26 | }, 27 | }), 28 | }); 29 | 30 | function createService(price: number, ttl: number, name: string) { 31 | if (price === 0 || ttl === 0 || name === '') { 32 | alert('Please fill in all fields'); 33 | return; 34 | } 35 | const ttlMs = ttl * 60 * 1000; 36 | const tx = new Transaction(); 37 | tx.moveCall({ 38 | target: `${packageId}::subscription::create_service_entry`, 39 | arguments: [tx.pure.u64(price), tx.pure.u64(ttlMs), tx.pure.string(name)], 40 | }); 41 | tx.setGasBudget(10000000); 42 | signAndExecute( 43 | { 44 | transaction: tx, 45 | }, 46 | { 47 | onSuccess: async (result) => { 48 | console.log('res', result); 49 | const subscriptionObject = result.effects?.created?.find( 50 | (item) => item.owner && typeof item.owner === 'object' && 'Shared' in item.owner, 51 | ); 52 | const createdObjectId = subscriptionObject?.reference?.objectId; 53 | if (createdObjectId) { 54 | window.open( 55 | `${window.location.origin}/subscription-example/admin/service/${createdObjectId}`, 56 | '_blank', 57 | ); 58 | } 59 | }, 60 | }, 61 | ); 62 | } 63 | const handleViewAll = () => { 64 | navigate(`/subscription-example/admin/services`); 65 | }; 66 | return ( 67 | 68 |

Admin View: Subscription

69 | 70 | Price in Mist: setPrice(e.target.value)} /> 71 | Subscription duration in minutes: setTtl(e.target.value)} /> 72 | Name of the service: setName(e.target.value)} /> 73 | 74 | 82 | 85 | 86 | 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /examples/frontend/src/OwnedAllowlists.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useCurrentAccount, useSuiClient } from '@mysten/dapp-kit'; 5 | import { useCallback, useEffect, useState } from 'react'; 6 | import { useNetworkVariable } from './networkConfig'; 7 | import { Button, Card } from '@radix-ui/themes'; 8 | import { getObjectExplorerLink } from './utils'; 9 | 10 | export interface Cap { 11 | id: string; 12 | allowlist_id: string; 13 | } 14 | 15 | export interface CardItem { 16 | cap_id: string; 17 | allowlist_id: string; 18 | list: string[]; 19 | name: string; 20 | } 21 | 22 | export function AllAllowlist() { 23 | const packageId = useNetworkVariable('packageId'); 24 | const currentAccount = useCurrentAccount(); 25 | const suiClient = useSuiClient(); 26 | 27 | const [cardItems, setCardItems] = useState([]); 28 | 29 | const getCapObj = useCallback(async () => { 30 | if (!currentAccount?.address) return; 31 | 32 | const res = await suiClient.getOwnedObjects({ 33 | owner: currentAccount?.address, 34 | options: { 35 | showContent: true, 36 | showType: true, 37 | }, 38 | filter: { 39 | StructType: `${packageId}::allowlist::Cap`, 40 | }, 41 | }); 42 | const caps = res.data 43 | .map((obj) => { 44 | const fields = (obj!.data!.content as { fields: any }).fields; 45 | return { 46 | id: fields?.id.id, 47 | allowlist_id: fields?.allowlist_id, 48 | }; 49 | }) 50 | .filter((item) => item !== null) as Cap[]; 51 | const cardItems: CardItem[] = await Promise.all( 52 | caps.map(async (cap) => { 53 | const allowlist = await suiClient.getObject({ 54 | id: cap.allowlist_id, 55 | options: { showContent: true }, 56 | }); 57 | const fields = (allowlist.data?.content as { fields: any })?.fields || {}; 58 | return { 59 | cap_id: cap.id, 60 | allowlist_id: cap.allowlist_id, 61 | list: fields.list, 62 | name: fields.name, 63 | }; 64 | }), 65 | ); 66 | setCardItems(cardItems); 67 | }, [currentAccount?.address]); 68 | 69 | useEffect(() => { 70 | getCapObj(); 71 | }, [getCapObj]); 72 | 73 | return ( 74 | 75 |

Admin View: Owned Allowlists

76 |

77 | These are all the allowlists that you have created. Click manage to edit the allowlist and 78 | upload new files to the allowlist. 79 |

80 | {cardItems.map((item) => ( 81 | 82 |

83 | {item.name} (ID {getObjectExplorerLink(item.allowlist_id)}) 84 |

85 | 95 |
96 | ))} 97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /examples/frontend/src/OwnedSubscriptionServices.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { useCurrentAccount, useSuiClient } from '@mysten/dapp-kit'; 5 | import { useEffect, useState } from 'react'; 6 | import { useNetworkVariable } from './networkConfig'; 7 | import { Button, Card } from '@radix-ui/themes'; 8 | import { getObjectExplorerLink } from './utils'; 9 | 10 | export interface Cap { 11 | id: string; 12 | service_id: string; 13 | } 14 | 15 | export interface CardItem { 16 | id: string; 17 | fee: string; 18 | ttl: string; 19 | name: string; 20 | owner: string; 21 | } 22 | 23 | export function AllServices() { 24 | const packageId = useNetworkVariable('packageId'); 25 | const currentAccount = useCurrentAccount(); 26 | const suiClient = useSuiClient(); 27 | 28 | const [cardItems, setCardItems] = useState([]); 29 | 30 | useEffect(() => { 31 | async function getCapObj() { 32 | // get all owned cap objects 33 | const res = await suiClient.getOwnedObjects({ 34 | owner: currentAccount?.address!, 35 | options: { 36 | showContent: true, 37 | showType: true, 38 | }, 39 | filter: { 40 | StructType: `${packageId}::subscription::Cap`, 41 | }, 42 | }); 43 | const caps = res.data 44 | .map((obj) => { 45 | const fields = (obj!.data!.content as { fields: any }).fields; 46 | return { 47 | id: fields?.id.id, 48 | service_id: fields?.service_id, 49 | }; 50 | }) 51 | .filter((item) => item !== null) as Cap[]; 52 | 53 | // get all services of all the owned cap objects 54 | const cardItems: CardItem[] = await Promise.all( 55 | caps.map(async (cap) => { 56 | const service = await suiClient.getObject({ 57 | id: cap.service_id, 58 | options: { showContent: true }, 59 | }); 60 | const fields = (service.data?.content as { fields: any })?.fields || {}; 61 | return { 62 | id: cap.service_id, 63 | fee: fields.fee, 64 | ttl: fields.ttl, 65 | owner: fields.owner, 66 | name: fields.name, 67 | }; 68 | }), 69 | ); 70 | setCardItems(cardItems); 71 | } 72 | 73 | // Call getCapObj immediately 74 | getCapObj(); 75 | 76 | // Set up interval to call getCapObj every 3 seconds 77 | const intervalId = setInterval(() => { 78 | getCapObj(); 79 | }, 3000); 80 | 81 | // Cleanup interval on component unmount 82 | return () => clearInterval(intervalId); 83 | }, [currentAccount?.address]); // Empty dependency array since we don't need any external values 84 | 85 | return ( 86 |
87 |

Admin View: Owned Subscription Services

88 |

89 | This is all the services that you have created. Click manage to upload new files to the 90 | service. 91 |

92 | {cardItems.map((item) => ( 93 | 94 |

95 | 96 | {item.name} (ID {getObjectExplorerLink(item.id)}) 97 | 98 |

99 |

Subscription Fee: {item.fee} MIST

100 |

Subscription Duration: {item.ttl ? parseInt(item.ttl) / 60 / 1000 : 'null'} minutes

101 | 111 |
112 | ))} 113 |
114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /examples/frontend/src/SubscriptionService.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { useCurrentAccount, useSuiClient } from '@mysten/dapp-kit'; 4 | import { Card, Flex } from '@radix-ui/themes'; 5 | import { useEffect, useState } from 'react'; 6 | import { useParams } from 'react-router-dom'; 7 | import { useNetworkVariable } from './networkConfig'; 8 | import { getObjectExplorerLink } from './utils'; 9 | 10 | export interface Service { 11 | id: string; 12 | fee: number; 13 | ttl: number; 14 | owner: string; 15 | name: string; 16 | } 17 | 18 | interface AllowlistProps { 19 | setRecipientAllowlist: React.Dispatch>; 20 | setCapId: React.Dispatch>; 21 | } 22 | 23 | export function Service({ setRecipientAllowlist, setCapId }: AllowlistProps) { 24 | const suiClient = useSuiClient(); 25 | const packageId = useNetworkVariable('packageId'); 26 | const currentAccount = useCurrentAccount(); 27 | const [service, setService] = useState(); 28 | const { id } = useParams(); 29 | 30 | useEffect(() => { 31 | async function getService() { 32 | // load the service for the given id 33 | const service = await suiClient.getObject({ 34 | id: id!, 35 | options: { showContent: true }, 36 | }); 37 | const fields = (service.data?.content as { fields: any })?.fields || {}; 38 | setService({ 39 | id: id!, 40 | fee: fields.fee, 41 | ttl: fields.ttl, 42 | owner: fields.owner, 43 | name: fields.name, 44 | }); 45 | setRecipientAllowlist(id!); 46 | 47 | // load all caps 48 | const res = await suiClient.getOwnedObjects({ 49 | owner: currentAccount?.address!, 50 | options: { 51 | showContent: true, 52 | showType: true, 53 | }, 54 | filter: { 55 | StructType: `${packageId}::subscription::Cap`, 56 | }, 57 | }); 58 | 59 | // find the cap for the given service id 60 | const capId = res.data 61 | .map((obj) => { 62 | const fields = (obj!.data!.content as { fields: any }).fields; 63 | return { 64 | id: fields?.id.id, 65 | service_id: fields?.service_id, 66 | }; 67 | }) 68 | .filter((item) => item.service_id === id) 69 | .map((item) => item.id) as string[]; 70 | setCapId(capId[0]); 71 | } 72 | 73 | // Call getService immediately 74 | getService(); 75 | 76 | // Set up interval to call getService every 3 seconds 77 | const intervalId = setInterval(() => { 78 | getService(); 79 | }, 3000); 80 | 81 | // Cleanup interval on component unmount 82 | return () => clearInterval(intervalId); 83 | }, [id]); // Only depend on id since it's needed for the API calls 84 | 85 | return ( 86 | 87 | 88 |

89 | Admin View: Service {service?.name} (ID {service?.id && getObjectExplorerLink(service.id)} 90 | ) 91 |

92 |

93 | Share  94 | 101 | this link 102 | {' '} 103 | with other users to subscribe to this service and access its files. 104 |

105 | 106 | 107 |

108 | Subscription duration:{' '} 109 | {service?.ttl ? service?.ttl / 60 / 1000 : 'null'} minutes 110 |

111 |

112 | Subscription fee: {service?.fee} MIST 113 |

114 |
115 |
116 |
117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /examples/frontend/src/constants.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const DEVNET_PACKAGE_ID = '0xTODO'; 5 | export const TESTNET_PACKAGE_ID = 6 | '0x4cb081457b1e098d566a277f605ba48410e26e66eaab5b3be4f6c560e9501800'; 7 | export const MAINNET_PACKAGE_ID = '0xTODO'; 8 | -------------------------------------------------------------------------------- /examples/frontend/src/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom/client'; 5 | import '@mysten/dapp-kit/dist/index.css'; 6 | import '@radix-ui/themes/styles.css'; 7 | 8 | import { SuiClientProvider, WalletProvider } from '@mysten/dapp-kit'; 9 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 10 | import { Theme } from '@radix-ui/themes'; 11 | import App from './App'; 12 | import { networkConfig } from './networkConfig'; 13 | 14 | const queryClient = new QueryClient(); 15 | 16 | ReactDOM.createRoot(document.getElementById('root')!).render( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | , 28 | ); 29 | -------------------------------------------------------------------------------- /examples/frontend/src/networkConfig.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | import { getFullnodeUrl } from '@mysten/sui/client'; 4 | import { TESTNET_PACKAGE_ID } from './constants'; 5 | import { createNetworkConfig } from '@mysten/dapp-kit'; 6 | 7 | const { networkConfig, useNetworkVariable, useNetworkVariables } = createNetworkConfig({ 8 | testnet: { 9 | url: getFullnodeUrl('testnet'), 10 | variables: { 11 | packageId: TESTNET_PACKAGE_ID, 12 | }, 13 | }, 14 | }); 15 | 16 | export { useNetworkVariable, useNetworkVariables, networkConfig }; 17 | -------------------------------------------------------------------------------- /examples/frontend/src/utils.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { SealClient, SessionKey, NoAccessError, EncryptedObject } from '@mysten/seal'; 5 | import { SuiClient } from '@mysten/sui/client'; 6 | import { Transaction } from '@mysten/sui/transactions'; 7 | import React from 'react'; 8 | 9 | export type MoveCallConstructor = (tx: Transaction, id: string) => void; 10 | 11 | export const downloadAndDecrypt = async ( 12 | blobIds: string[], 13 | sessionKey: SessionKey, 14 | suiClient: SuiClient, 15 | sealClient: SealClient, 16 | moveCallConstructor: (tx: Transaction, id: string) => void, 17 | setError: (error: string | null) => void, 18 | setDecryptedFileUrls: (urls: string[]) => void, 19 | setIsDialogOpen: (open: boolean) => void, 20 | setReloadKey: (updater: (prev: number) => number) => void, 21 | ) => { 22 | const aggregators = [ 23 | 'aggregator1', 24 | 'aggregator2', 25 | 'aggregator3', 26 | 'aggregator4', 27 | 'aggregator5', 28 | 'aggregator6', 29 | ]; 30 | // First, download all files in parallel (ignore errors) 31 | const downloadResults = await Promise.all( 32 | blobIds.map(async (blobId) => { 33 | try { 34 | const controller = new AbortController(); 35 | const timeout = setTimeout(() => controller.abort(), 10000); 36 | const randomAggregator = aggregators[Math.floor(Math.random() * aggregators.length)]; 37 | const aggregatorUrl = `/${randomAggregator}/v1/blobs/${blobId}`; 38 | const response = await fetch(aggregatorUrl, { signal: controller.signal }); 39 | clearTimeout(timeout); 40 | if (!response.ok) { 41 | return null; 42 | } 43 | return await response.arrayBuffer(); 44 | } catch (err) { 45 | console.error(`Blob ${blobId} cannot be retrieved from Walrus`, err); 46 | return null; 47 | } 48 | }), 49 | ); 50 | 51 | // Filter out failed downloads 52 | const validDownloads = downloadResults.filter((result): result is ArrayBuffer => result !== null); 53 | console.log('validDownloads count', validDownloads.length); 54 | 55 | if (validDownloads.length === 0) { 56 | const errorMsg = 57 | 'Cannot retrieve files from this Walrus aggregator, try again (a randomly selected aggregator will be used). Files uploaded more than 1 epoch ago have been deleted from Walrus.'; 58 | console.error(errorMsg); 59 | setError(errorMsg); 60 | return; 61 | } 62 | 63 | // Fetch keys in batches of <=10 64 | for (let i = 0; i < validDownloads.length; i += 10) { 65 | const batch = validDownloads.slice(i, i + 10); 66 | const ids = batch.map((enc) => EncryptedObject.parse(new Uint8Array(enc)).id); 67 | const tx = new Transaction(); 68 | ids.forEach((id) => moveCallConstructor(tx, id)); 69 | const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true }); 70 | try { 71 | await sealClient.fetchKeys({ ids, txBytes, sessionKey, threshold: 2 }); 72 | } catch (err) { 73 | console.log(err); 74 | const errorMsg = 75 | err instanceof NoAccessError 76 | ? 'No access to decryption keys' 77 | : 'Unable to decrypt files, try again'; 78 | console.error(errorMsg, err); 79 | setError(errorMsg); 80 | return; 81 | } 82 | } 83 | 84 | // Then, decrypt files sequentially 85 | const decryptedFileUrls: string[] = []; 86 | for (const encryptedData of validDownloads) { 87 | const fullId = EncryptedObject.parse(new Uint8Array(encryptedData)).id; 88 | const tx = new Transaction(); 89 | moveCallConstructor(tx, fullId); 90 | const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true }); 91 | try { 92 | // Note that all keys are fetched above, so this only local decryption is done 93 | const decryptedFile = await sealClient.decrypt({ 94 | data: new Uint8Array(encryptedData), 95 | sessionKey, 96 | txBytes, 97 | }); 98 | const blob = new Blob([decryptedFile], { type: 'image/jpg' }); 99 | decryptedFileUrls.push(URL.createObjectURL(blob)); 100 | } catch (err) { 101 | console.log(err); 102 | const errorMsg = 103 | err instanceof NoAccessError 104 | ? 'No access to decryption keys' 105 | : 'Unable to decrypt files, try again'; 106 | console.error(errorMsg, err); 107 | setError(errorMsg); 108 | return; 109 | } 110 | } 111 | 112 | if (decryptedFileUrls.length > 0) { 113 | setDecryptedFileUrls(decryptedFileUrls); 114 | setIsDialogOpen(true); 115 | setReloadKey((prev) => prev + 1); 116 | } 117 | }; 118 | 119 | export const getObjectExplorerLink = (id: string): React.ReactElement => { 120 | return React.createElement( 121 | 'a', 122 | { 123 | href: `https://testnet.suivision.xyz/object/${id}`, 124 | target: '_blank', 125 | rel: 'noopener noreferrer', 126 | style: { textDecoration: 'underline' }, 127 | }, 128 | id.slice(0, 10) + '...', 129 | ); 130 | }; 131 | -------------------------------------------------------------------------------- /examples/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | }, 24 | "types": ["react"] 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "jest.config.js"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /examples/frontend/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [ 3 | { 4 | "source": "/aggregator1/:path*", 5 | "destination": "https://aggregator.walrus-testnet.walrus.space/:path*" 6 | }, 7 | { 8 | "source": "/aggregator2/:path*", 9 | "destination": "https://wal-aggregator-testnet.staketab.org/:path*" 10 | }, 11 | { 12 | "source": "/aggregator3/:path*", 13 | "destination": "https://walrus-testnet-aggregator.redundex.com/:path*" 14 | }, 15 | { 16 | "source": "/aggregator4/:path*", 17 | "destination": "https://walrus-testnet-aggregator.nodes.guru/:path*" 18 | }, 19 | { 20 | "source": "/aggregator5/:path*", 21 | "destination": "https://aggregator.walrus.banansen.dev/:path*" 22 | }, 23 | { 24 | "source": "/aggregator6/:path*", 25 | "destination": "https://walrus-testnet-aggregator.everstake.one/:path*" 26 | }, 27 | { 28 | "source": "/publisher1/:path*", 29 | "destination": "https://publisher.walrus-testnet.walrus.space/:path*" 30 | }, 31 | { 32 | "source": "/publisher2/:path*", 33 | "destination": "https://wal-publisher-testnet.staketab.org/:path*" 34 | }, 35 | { 36 | "source": "/publisher3/:path*", 37 | "destination": "https://walrus-testnet-publisher.redundex.com/:path*" 38 | }, 39 | { 40 | "source": "/publisher4/:path*", 41 | "destination": "https://walrus-testnet-publisher.nodes.guru/:path*" 42 | }, 43 | { 44 | "source": "/publisher5/:path*", 45 | "destination": "https://publisher.walrus.banansen.dev/:path*" 46 | }, 47 | { 48 | "source": "/publisher6/:path*", 49 | "destination": "https://walrus-testnet-publisher.everstake.one/:path*" 50 | }, 51 | { 52 | "source": "/(.*)", 53 | "destination": "/index.html" 54 | } 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /examples/frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { defineConfig } from 'vite'; 5 | import react from '@vitejs/plugin-react'; 6 | 7 | export default defineConfig({ 8 | plugins: [react()], 9 | server: { 10 | proxy: { 11 | '/aggregator1/v1': { 12 | target: 'https://aggregator.walrus-testnet.walrus.space', 13 | changeOrigin: true, 14 | secure: false, 15 | rewrite: (path) => path.replace(/^\/aggregator1/, ''), 16 | }, 17 | '/aggregator2/v1': { 18 | target: 'https://wal-aggregator-testnet.staketab.org', 19 | changeOrigin: true, 20 | secure: false, 21 | rewrite: (path) => path.replace(/^\/aggregator2/, ''), 22 | }, 23 | '/aggregator3/v1': { 24 | target: 'https://walrus-testnet-aggregator.redundex.com', 25 | changeOrigin: true, 26 | secure: false, 27 | rewrite: (path) => path.replace(/^\/aggregator3/, ''), 28 | }, 29 | '/aggregator4/v1': { 30 | target: 'https://walrus-testnet-aggregator.nodes.guru', 31 | changeOrigin: true, 32 | secure: false, 33 | rewrite: (path) => path.replace(/^\/aggregator4/, ''), 34 | }, 35 | '/aggregator5/v1': { 36 | target: 'https://aggregator.walrus.banansen.dev', 37 | changeOrigin: true, 38 | secure: false, 39 | rewrite: (path) => path.replace(/^\/aggregator5/, ''), 40 | }, 41 | '/aggregator6/v1': { 42 | target: 'https://walrus-testnet-aggregator.everstake.one', 43 | changeOrigin: true, 44 | secure: false, 45 | rewrite: (path) => path.replace(/^\/aggregator6/, ''), 46 | }, 47 | '/publisher1/v1': { 48 | target: 'https://publisher.walrus-testnet.walrus.space', 49 | changeOrigin: true, 50 | secure: false, 51 | rewrite: (path) => path.replace(/^\/publisher1/, ''), 52 | }, 53 | '/publisher2/v1': { 54 | target: 'https://wal-publisher-testnet.staketab.org', 55 | changeOrigin: true, 56 | secure: false, 57 | rewrite: (path) => path.replace(/^\/publisher2/, ''), 58 | }, 59 | '/publisher3/v1': { 60 | target: 'https://walrus-testnet-publisher.redundex.com', 61 | changeOrigin: true, 62 | secure: false, 63 | rewrite: (path) => path.replace(/^\/publisher3/, ''), 64 | }, 65 | '/publisher4/v1': { 66 | target: 'https://walrus-testnet-publisher.nodes.guru', 67 | changeOrigin: true, 68 | secure: false, 69 | rewrite: (path) => path.replace(/^\/publisher4/, ''), 70 | }, 71 | '/publisher5/v1': { 72 | target: 'https://publisher.walrus.banansen.dev', 73 | changeOrigin: true, 74 | secure: false, 75 | rewrite: (path) => path.replace(/^\/publisher5/, ''), 76 | }, 77 | '/publisher6/v1': { 78 | target: 'https://walrus-testnet-publisher.everstake.one', 79 | changeOrigin: true, 80 | secure: false, 81 | rewrite: (path) => path.replace(/^\/publisher6/, ''), 82 | }, 83 | }, 84 | }, 85 | }); 86 | -------------------------------------------------------------------------------- /examples/move/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "walrus" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } 9 | 10 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 11 | # Revision can be a branch, a tag, and a commit hash. 12 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 13 | 14 | # For local dependencies use `local = path`. Path is relative to the package root 15 | # Local = { local = "../path/to" } 16 | 17 | # To resolve a version conflict and force a specific version for dependency 18 | # override use `override = true` 19 | # Override = { local = "../conflicting/version", override = true } 20 | 21 | [addresses] 22 | walrus = "0x0" 23 | 24 | # Named addresses will be accessible in Move as `@name`. They're also exported: 25 | # for example, `std = "0x1"` is exported by the Standard Library. 26 | # alice = "0xA11CE" 27 | 28 | [dev-dependencies] 29 | # The dev-dependencies section allows overriding dependencies for `--test` and 30 | # `--dev` modes. You can introduce test-only dependencies here. 31 | # Local = { local = "../path/to/dev-build" } 32 | 33 | [dev-addresses] 34 | # The dev-addresses section allows overwriting named addresses for the `--test` 35 | # and `--dev` modes. 36 | # alice = "0xB0B" 37 | 38 | -------------------------------------------------------------------------------- /examples/move/sources/allowlist.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Based on the allowlist pattern 5 | 6 | module walrus::allowlist; 7 | 8 | use std::string::String; 9 | use sui::dynamic_field as df; 10 | use walrus::utils::is_prefix; 11 | 12 | const EInvalidCap: u64 = 0; 13 | const ENoAccess: u64 = 1; 14 | const EDuplicate: u64 = 2; 15 | const MARKER: u64 = 3; 16 | 17 | public struct Allowlist has key { 18 | id: UID, 19 | name: String, 20 | list: vector
, 21 | } 22 | 23 | public struct Cap has key { 24 | id: UID, 25 | allowlist_id: ID, 26 | } 27 | 28 | ////////////////////////////////////////// 29 | /////// Simple allowlist with an admin cap 30 | 31 | /// Create an allowlist with an admin cap. 32 | /// The associated key-ids are [pkg id]::[allowlist id][nonce] for any nonce (thus 33 | /// many key-ids can be created for the same allowlist). 34 | public fun create_allowlist(name: String, ctx: &mut TxContext): Cap { 35 | let allowlist = Allowlist { 36 | id: object::new(ctx), 37 | list: vector::empty(), 38 | name: name, 39 | }; 40 | let cap = Cap { 41 | id: object::new(ctx), 42 | allowlist_id: object::id(&allowlist), 43 | }; 44 | transfer::share_object(allowlist); 45 | cap 46 | } 47 | 48 | // convenience function to create a allowlist and send it back to sender (simpler ptb for cli) 49 | entry fun create_allowlist_entry(name: String, ctx: &mut TxContext) { 50 | transfer::transfer(create_allowlist(name, ctx), ctx.sender()); 51 | } 52 | 53 | public fun add(allowlist: &mut Allowlist, cap: &Cap, account: address) { 54 | assert!(cap.allowlist_id == object::id(allowlist), EInvalidCap); 55 | assert!(!allowlist.list.contains(&account), EDuplicate); 56 | allowlist.list.push_back(account); 57 | } 58 | 59 | public fun remove(allowlist: &mut Allowlist, cap: &Cap, account: address) { 60 | assert!(cap.allowlist_id == object::id(allowlist), EInvalidCap); 61 | allowlist.list = allowlist.list.filter!(|x| x != account); // TODO: more efficient impl? 62 | } 63 | 64 | ////////////////////////////////////////////////////////// 65 | /// Access control 66 | /// key format: [pkg id]::[allowlist id][random nonce] 67 | /// (Alternative key format: [pkg id]::[creator address][random nonce] - see private_data.move) 68 | 69 | public fun namespace(allowlist: &Allowlist): vector { 70 | allowlist.id.to_bytes() 71 | } 72 | 73 | /// All allowlisted addresses can access all IDs with the prefix of the allowlist 74 | fun approve_internal(caller: address, id: vector, allowlist: &Allowlist): bool { 75 | // Check if the id has the right prefix 76 | let namespace = namespace(allowlist); 77 | if (!is_prefix(namespace, id)) { 78 | return false 79 | }; 80 | 81 | // Check if user is in the allowlist 82 | allowlist.list.contains(&caller) 83 | } 84 | 85 | entry fun seal_approve(id: vector, allowlist: &Allowlist, ctx: &TxContext) { 86 | assert!(approve_internal(ctx.sender(), id, allowlist), ENoAccess); 87 | } 88 | 89 | /// Encapsulate a blob into a Sui object and attach it to the allowlist 90 | public fun publish(allowlist: &mut Allowlist, cap: &Cap, blob_id: String) { 91 | assert!(cap.allowlist_id == object::id(allowlist), EInvalidCap); 92 | df::add(&mut allowlist.id, blob_id, MARKER); 93 | } 94 | 95 | #[test_only] 96 | public fun new_allowlist_for_testing(ctx: &mut TxContext): Allowlist { 97 | use std::string::utf8; 98 | 99 | Allowlist { 100 | id: object::new(ctx), 101 | name: utf8(b"test"), 102 | list: vector::empty(), 103 | } 104 | } 105 | 106 | #[test_only] 107 | public fun new_cap_for_testing(ctx: &mut TxContext, allowlist: &Allowlist): Cap { 108 | Cap { 109 | id: object::new(ctx), 110 | allowlist_id: object::id(allowlist), 111 | } 112 | } 113 | 114 | #[test_only] 115 | public fun destroy_for_testing(allowlist: Allowlist, cap: Cap) { 116 | let Allowlist { id, .. } = allowlist; 117 | object::delete(id); 118 | let Cap { id, .. } = cap; 119 | object::delete(id); 120 | } 121 | -------------------------------------------------------------------------------- /examples/move/sources/subscription.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Based on the subscription pattern. 5 | // TODO: document and add tests 6 | 7 | module walrus::subscription; 8 | 9 | use std::string::String; 10 | use sui::{clock::Clock, coin::Coin, dynamic_field as df, sui::SUI}; 11 | use walrus::utils::is_prefix; 12 | 13 | const EInvalidCap: u64 = 0; 14 | const EInvalidFee: u64 = 1; 15 | const ENoAccess: u64 = 2; 16 | const MARKER: u64 = 3; 17 | 18 | public struct Service has key { 19 | id: UID, 20 | fee: u64, 21 | ttl: u64, 22 | owner: address, 23 | name: String, 24 | } 25 | 26 | public struct Subscription has key { 27 | id: UID, 28 | service_id: ID, 29 | created_at: u64, 30 | } 31 | 32 | public struct Cap has key { 33 | id: UID, 34 | service_id: ID, 35 | } 36 | 37 | ////////////////////////////////////////// 38 | /////// Simple a service 39 | 40 | /// Create a service. 41 | /// The associated key-ids are [pkg id]::[service id][nonce] for any nonce (thus 42 | /// many key-ids can be created for the same service). 43 | public fun create_service(fee: u64, ttl: u64, name: String, ctx: &mut TxContext): Cap { 44 | let service = Service { 45 | id: object::new(ctx), 46 | fee: fee, 47 | ttl: ttl, 48 | owner: ctx.sender(), 49 | name: name, 50 | }; 51 | let cap = Cap { 52 | id: object::new(ctx), 53 | service_id: object::id(&service), 54 | }; 55 | transfer::share_object(service); 56 | cap 57 | } 58 | 59 | // convenience function to create a service and share it (simpler ptb for cli) 60 | entry fun create_service_entry(fee: u64, ttl: u64, name: String, ctx: &mut TxContext) { 61 | transfer::transfer(create_service(fee, ttl, name, ctx), ctx.sender()); 62 | } 63 | 64 | public fun subscribe( 65 | fee: Coin, 66 | service: &Service, 67 | c: &Clock, 68 | ctx: &mut TxContext, 69 | ): Subscription { 70 | assert!(fee.value() == service.fee, EInvalidFee); 71 | transfer::public_transfer(fee, service.owner); 72 | Subscription { 73 | id: object::new(ctx), 74 | service_id: object::id(service), 75 | created_at: c.timestamp_ms(), 76 | } 77 | } 78 | 79 | public fun transfer(sub: Subscription, to: address) { 80 | transfer::transfer(sub, to); 81 | } 82 | 83 | #[test_only] 84 | public fun destroy_for_testing(ser: Service, sub: Subscription) { 85 | let Service { id, .. } = ser; 86 | object::delete(id); 87 | let Subscription { id, .. } = sub; 88 | object::delete(id); 89 | } 90 | 91 | ////////////////////////////////////////////////////////// 92 | /// Access control 93 | /// key format: [pkg id]::[service id][random nonce] 94 | 95 | /// All allowlisted addresses can access all IDs with the prefix of the allowlist 96 | fun approve_internal(id: vector, sub: &Subscription, service: &Service, c: &Clock): bool { 97 | if (object::id(service) != sub.service_id) { 98 | return false 99 | }; 100 | if (c.timestamp_ms() > sub.created_at + service.ttl) { 101 | return false 102 | }; 103 | 104 | // Check if the id has the right prefix 105 | is_prefix(service.id.to_bytes(), id) 106 | } 107 | 108 | entry fun seal_approve(id: vector, sub: &Subscription, service: &Service, c: &Clock) { 109 | assert!(approve_internal(id, sub, service, c), ENoAccess); 110 | } 111 | 112 | /// Encapsulate a blob into a Sui object and attach it to the Subscription 113 | public fun publish(service: &mut Service, cap: &Cap, blob_id: String) { 114 | assert!(cap.service_id == object::id(service), EInvalidCap); 115 | df::add(&mut service.id, blob_id, MARKER); 116 | } 117 | -------------------------------------------------------------------------------- /examples/move/sources/utils.move: -------------------------------------------------------------------------------- 1 | module walrus::utils; 2 | 3 | /// Returns true if `prefix` is a prefix of `word`. 4 | public(package) fun is_prefix(prefix: vector, word: vector): bool { 5 | if (prefix.length() > word.length()) { 6 | return false 7 | }; 8 | let mut i = 0; 9 | while (i < prefix.length()) { 10 | if (prefix[i] != word[i]) { 11 | return false 12 | }; 13 | i = i + 1; 14 | }; 15 | true 16 | } 17 | -------------------------------------------------------------------------------- /move/patterns/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "patterns" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | 7 | [dependencies] 8 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } 9 | seal = { local = "../seal" } 10 | 11 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 12 | # Revision can be a branch, a tag, and a commit hash. 13 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 14 | 15 | # For local dependencies use `local = path`. Path is relative to the package root 16 | # Local = { local = "../path/to" } 17 | 18 | # To resolve a version conflict and force a specific version for dependency 19 | # override use `override = true` 20 | # Override = { local = "../conflicting/version", override = true } 21 | 22 | [addresses] 23 | patterns = "0x0" 24 | 25 | # Named addresses will be accessible in Move as `@name`. They're also exported: 26 | # for example, `std = "0x1"` is exported by the Standard Library. 27 | # alice = "0xA11CE" 28 | 29 | [dev-dependencies] 30 | # The dev-dependencies section allows overriding dependencies for `--test` and 31 | # `--dev` modes. You can introduce test-only dependencies here. 32 | # Local = { local = "../path/to/dev-build" } 33 | 34 | [dev-addresses] 35 | # The dev-addresses section allows overwriting named addresses for the `--test` 36 | # and `--dev` modes. 37 | # alice = "0xB0B" 38 | 39 | -------------------------------------------------------------------------------- /move/patterns/sources/account_based.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// Account based encryption: 5 | /// - Anyone can encrypt to address B using key-id [pkg id]::[bcs::to_bytes(B)]. 6 | /// - Only the owner of account B can access the associated key. 7 | /// 8 | /// Use cases that can be built on top of this: offchain secure messaging. 9 | /// 10 | /// This pattern does NOT implement versioning, please see other patterns for 11 | /// examples of versioning. 12 | /// 13 | module patterns::account_based; 14 | 15 | use sui::bcs; 16 | 17 | const ENoAccess: u64 = 1; 18 | 19 | ///////////////////////////////////// 20 | /// Access control 21 | /// key format: [pkg id][bcs::to_bytes(B)] for address B 22 | 23 | fun check_policy(id: vector, ctx: &TxContext): bool { 24 | let caller_bytes = bcs::to_bytes(&ctx.sender()); 25 | id == caller_bytes 26 | } 27 | 28 | entry fun seal_approve(id: vector, ctx: &TxContext) { 29 | assert!(check_policy(id, ctx), ENoAccess); 30 | } 31 | 32 | #[test] 33 | fun test_check_policy() { 34 | let ctx = tx_context::dummy(); 35 | let sender = ctx.sender(); 36 | let id = bcs::to_bytes(&sender); 37 | assert!(check_policy(id, &ctx), 0); 38 | 39 | let id = bcs::to_bytes(&0x0232); 40 | assert!(!check_policy(id, &ctx), 0); 41 | } 42 | -------------------------------------------------------------------------------- /move/patterns/sources/key_request.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// KeyRequest pattern: 5 | /// - Policy is checked onchain, and if granted, a KeyRequest object is returned to the user. 6 | /// - The user can then use the KeyRequest object to access the associated key using Seal. 7 | /// 8 | /// Dapp developers need to define how to contrust KeyRequest, and implement seal_approve that 9 | /// only calls verify. Seal is agnostic to the actual policy. 10 | /// 11 | /// Use cases that can be built on top of this: pay per key request, complex policies in which 12 | /// safety during dryRun must be guaranteed. 13 | /// 14 | /// See a test below for an example of how to use this pattern with a whitelist. 15 | /// 16 | /// This pattern does NOT implement versioning, please see other patterns for 17 | /// examples of versioning. 18 | /// 19 | module patterns::key_request { 20 | use std::{ascii::String, type_name}; 21 | use sui::clock::Clock; 22 | 23 | /// KeyRequest object has all the info needed to access a key. 24 | public struct KeyRequest has key, store { 25 | id: UID, 26 | package: String, // Hex 27 | inner_id: vector, 28 | user: address, 29 | valid_till: u64, 30 | } 31 | 32 | /// Any contract can create a KeyRequest object associated with a given witness T (inaccessible to other contracts). 33 | /// ttl is the number of milliseconds after which the KeyRequest object expires. 34 | public fun request_key( 35 | _w: T, 36 | id: vector, 37 | user: address, 38 | c: &Clock, 39 | ttl: u64, 40 | ctx: &mut TxContext, 41 | ): KeyRequest { 42 | // The package of the caller (via the witness T). 43 | let package = type_name::get_with_original_ids().get_address(); 44 | KeyRequest { 45 | id: object::new(ctx), 46 | package, 47 | inner_id: id, 48 | user, 49 | valid_till: c.timestamp_ms() + ttl, 50 | } 51 | } 52 | 53 | public fun destroy(req: KeyRequest) { 54 | let KeyRequest { id, .. } = req; 55 | object::delete(id); 56 | } 57 | 58 | /// Verify that the KeyRequest is consistent with the given parameters, and that it has not expired. 59 | /// The dapp needs to call only this function in seal_approve. 60 | public fun verify( 61 | req: &KeyRequest, 62 | _w: T, 63 | id: vector, 64 | user: address, 65 | c: &Clock, 66 | ): bool { 67 | let package = type_name::get_with_original_ids().get_address(); 68 | (req.package == package) && (req.inner_id == id) && (req.user == user) && (c.timestamp_ms() <= req.valid_till) 69 | } 70 | } 71 | 72 | /// Example of how to use the KeyRequest pattern with a whitelist. 73 | #[test_only] 74 | module patterns::key_request_whitelist_test { 75 | use patterns::key_request as kro; 76 | use sui::clock::Clock; 77 | 78 | const ENoAccess: u64 = 1; 79 | 80 | const TTL: u64 = 60_000; // 1 minute 81 | 82 | public struct Whitelist has key { 83 | id: UID, 84 | users: vector
, 85 | } 86 | 87 | // Just a static whitelist for the example, see the Whitelist pattern for a dynamic one. 88 | public fun create_whitelist(users: vector
, ctx: &mut TxContext): Whitelist { 89 | Whitelist { 90 | id: object::new(ctx), 91 | users: users, 92 | } 93 | } 94 | 95 | #[test_only] 96 | public fun destroy_for_testing(wl: Whitelist) { 97 | let Whitelist { id, .. } = wl; 98 | object::delete(id); 99 | } 100 | 101 | public struct WITNESS has drop {} 102 | 103 | /// Users request access using request_access. 104 | public fun request_access(wl: &Whitelist, c: &Clock, ctx: &mut TxContext): kro::KeyRequest { 105 | assert!(wl.users.contains(&ctx.sender()), ENoAccess); 106 | kro::request_key(WITNESS {}, wl.id.to_bytes(), ctx.sender(), c, TTL, ctx) 107 | } 108 | 109 | /// Seal only checks consistency of the request using req.verify. 110 | /// The actual policy is checked in request_access above. 111 | entry fun seal_approve(id: vector, req: &kro::KeyRequest, c: &Clock, ctx: &TxContext) { 112 | assert!(req.verify(WITNESS {}, id, ctx.sender(), c), ENoAccess); 113 | } 114 | 115 | #[test] 116 | fun test_e2e() { 117 | use sui::clock; 118 | 119 | let ctx = &mut tx_context::dummy(); // sender = 0x0 120 | let c = clock::create_for_testing(ctx); // time = 0 121 | 122 | let wl = create_whitelist(vector[@0x0, @0x1], ctx); 123 | let kr = request_access(&wl, &c, ctx); 124 | seal_approve(object::id(&wl).to_bytes(), &kr, &c, ctx); 125 | 126 | kr.destroy(); 127 | destroy_for_testing(wl); 128 | c.destroy_for_testing(); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /move/patterns/sources/private_data.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// Owner private data pattern: 5 | /// - Anyone can encrypt any data and store it encrypted as a Sui object. 6 | /// - The owner of the Sui object can always decrypt the data. 7 | /// 8 | /// Use cases that can be built on top of this: personal key storage, private NFTs. 9 | /// 10 | /// This pattern does NOT implement versioning, please see other patterns for 11 | /// examples of versioning. 12 | /// 13 | module patterns::private_data; 14 | 15 | const ENoAccess: u64 = 77; 16 | 17 | public struct PrivateData has key, store { 18 | id: UID, 19 | creator: address, 20 | nonce: vector, 21 | data: vector, 22 | } 23 | 24 | /// The encryption key id is [pkg id][creator address][random nonce] 25 | /// - The creator address is used to ensure that only the creator can create an object for that key id 26 | /// (otherwise, others can try to frontrun and create an object for the same key id). 27 | /// - The random nonce is used to ensure that the key id is unique even if the object is transferred to 28 | /// another user. 29 | /// - A single user can create unlimited number of key ids, simply by using different nonces. 30 | fun compute_key_id(sender: address, nonce: vector): vector { 31 | let mut blob = sender.to_bytes(); 32 | blob.append(nonce); 33 | blob 34 | } 35 | 36 | /// Store an encrypted data that was encrypted using the above key id. 37 | public fun store(nonce: vector, data: vector, ctx: &mut TxContext): PrivateData { 38 | PrivateData { 39 | id: object::new(ctx), 40 | creator: ctx.sender(), 41 | nonce, 42 | data: data, 43 | } 44 | } 45 | 46 | // Helper function for storing and sending back to sender. 47 | entry fun store_entry(nonce: vector, data: vector, ctx: &mut TxContext) { 48 | transfer::transfer(store(nonce, data, ctx), ctx.sender()); 49 | } 50 | 51 | ////////////////////////////////////////////// 52 | /// Access control 53 | /// key format: [pkg id][creator][nonce] 54 | fun check_policy(id: vector, e: &PrivateData): bool { 55 | // Only owner can call this function (enforced by MoveVM) 56 | 57 | // Check the key id is correct. 58 | let key_id = compute_key_id(e.creator, e.nonce); 59 | key_id == id 60 | } 61 | 62 | entry fun seal_approve(id: vector, e: &PrivateData) { 63 | assert!(check_policy(id, e), ENoAccess); 64 | } 65 | 66 | #[test_only] 67 | public fun destroy(e: PrivateData) { 68 | let PrivateData { id, .. } = e; 69 | object::delete(id); 70 | } 71 | 72 | #[test] 73 | fun test_internal_policy() { 74 | let ctx = &mut tx_context::dummy(); 75 | let ed = store(b"nonce", b"data", ctx); 76 | 77 | assert!(!check_policy(b"bla", &ed), 0); 78 | assert!(check_policy(compute_key_id(@0x0, b"nonce"), &ed), 0); 79 | ed.destroy(); 80 | } 81 | -------------------------------------------------------------------------------- /move/patterns/sources/subscription.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// Subscription pattern: 5 | /// - Anyone can create a service that requires a subscription. 6 | /// - Anyone can buy a subscription to the service for a certain period. 7 | /// - Anyone with an active subscription can access its service related keys. 8 | /// 9 | /// Use cases that can be built on top of this: subscription based access to content. 10 | /// 11 | /// This pattern implements global versioning per package. 12 | /// 13 | module patterns::subscription; 14 | 15 | use sui::clock::Clock; 16 | use sui::coin::Coin; 17 | use sui::sui::SUI; 18 | 19 | const EInvalidFee: u64 = 12; 20 | const ENoAccess: u64 = 77; 21 | const EWrongVersion: u64 = 5; 22 | 23 | const VERSION: u64 = 1; 24 | 25 | // Manage the version of the package for which seal_approve functions should be evaluated with. 26 | public struct PackageVersion has key { 27 | id: UID, 28 | version: u64, 29 | } 30 | 31 | public struct PackageVersionCap has key { 32 | id: UID, 33 | } 34 | 35 | // PackageVersionCap can also be used to upgrade the version of PackageVersion in future versions, 36 | // see https://docs.sui.io/concepts/sui-move-concepts/packages/upgrade#versioned-shared-objects 37 | 38 | fun init(ctx: &mut TxContext) { 39 | transfer::share_object(PackageVersion { 40 | id: object::new(ctx), 41 | version: VERSION, 42 | }); 43 | transfer::transfer(PackageVersionCap { id: object::new(ctx) }, ctx.sender()); 44 | } 45 | 46 | public struct Service has key { 47 | id: UID, 48 | fee: u64, 49 | ttl: u64, 50 | owner: address, 51 | } 52 | 53 | /// Subscription can only be transferred to another address (but not stored / shared / received, etc). 54 | public struct Subscription has key { 55 | id: UID, 56 | service_id: ID, 57 | created_at: u64, 58 | } 59 | 60 | ////////////////////////////////////////// 61 | /////// Simple a service 62 | 63 | /// Create a service. 64 | /// The associated key-ids are [pkg id][service id][nonce] for any nonce (thus 65 | /// many key-ids can be created for the same service). 66 | public fun create_service(fee: u64, ttl: u64, ctx: &mut TxContext): Service { 67 | Service { 68 | id: object::new(ctx), 69 | fee: fee, 70 | ttl: ttl, 71 | owner: ctx.sender(), 72 | } 73 | } 74 | 75 | // convenience function to create a service and share it (simpler ptb for cli) 76 | entry fun create_service_entry(fee: u64, ttl: u64, ctx: &mut TxContext) { 77 | transfer::share_object(create_service(fee, ttl, ctx)); 78 | } 79 | 80 | public fun subscribe( 81 | fee: Coin, 82 | service: &Service, 83 | c: &Clock, 84 | ctx: &mut TxContext, 85 | ): Subscription { 86 | assert!(fee.value() == service.fee, EInvalidFee); 87 | transfer::public_transfer(fee, service.owner); 88 | Subscription { 89 | id: object::new(ctx), 90 | service_id: object::id(service), 91 | created_at: c.timestamp_ms(), 92 | } 93 | } 94 | 95 | public fun transfer(sub: Subscription, to: address) { 96 | transfer::transfer(sub, to); 97 | } 98 | 99 | #[test_only] 100 | public fun destroy_for_testing(ser: Service, sub: Subscription) { 101 | let Service { id, .. } = ser; 102 | object::delete(id); 103 | let Subscription { id, .. } = sub; 104 | object::delete(id); 105 | } 106 | 107 | #[test_only] 108 | public fun create_for_testing(ctx: &mut TxContext): (PackageVersion, PackageVersionCap) { 109 | let pkg_version = PackageVersion { 110 | id: object::new(ctx), 111 | version: VERSION, 112 | }; 113 | (pkg_version, PackageVersionCap { id: object::new(ctx) }) 114 | } 115 | 116 | #[test_only] 117 | public fun destroy_versions_for_testing( 118 | pkg_version: PackageVersion, 119 | pkg_version_cap: PackageVersionCap, 120 | ) { 121 | let PackageVersion { id, .. } = pkg_version; 122 | object::delete(id); 123 | let PackageVersionCap { id, .. } = pkg_version_cap; 124 | object::delete(id); 125 | } 126 | 127 | ////////////////////////////////////////////////////////// 128 | /// Access control 129 | /// key format: [pkg id][service id][random nonce] 130 | 131 | /// All addresses can access all IDs with the prefix of the service 132 | fun check_policy( 133 | id: vector, 134 | pkg_version: &PackageVersion, 135 | sub: &Subscription, 136 | service: &Service, 137 | c: &Clock, 138 | ): bool { 139 | // Check we are using the right version of the package. 140 | assert!(pkg_version.version == VERSION, EWrongVersion); 141 | 142 | if (object::id(service) != sub.service_id) { 143 | return false 144 | }; 145 | if (c.timestamp_ms() > sub.created_at + service.ttl) { 146 | return false 147 | }; 148 | 149 | // Check if the id has the right prefix 150 | let namespace = service.id.to_bytes(); 151 | let mut i = 0; 152 | if (namespace.length() > id.length()) { 153 | return false 154 | }; 155 | while (i < namespace.length()) { 156 | if (namespace[i] != id[i]) { 157 | return false 158 | }; 159 | i = i + 1; 160 | }; 161 | true 162 | } 163 | 164 | entry fun seal_approve( 165 | id: vector, 166 | pkg_version: &PackageVersion, 167 | sub: &Subscription, 168 | service: &Service, 169 | c: &Clock, 170 | ) { 171 | assert!(check_policy(id, pkg_version, sub, service, c), ENoAccess); 172 | } 173 | 174 | #[test] 175 | fun test_approve() { 176 | use sui::clock; 177 | use sui::coin; 178 | 179 | let ctx = &mut tx_context::dummy(); 180 | let mut c = clock::create_for_testing(ctx); // time = 0 181 | let coin = coin::mint_for_testing(10, ctx); 182 | let (pkg_version, _pkg_version_cap) = create_for_testing(ctx); 183 | 184 | let ser = create_service(10, 2, ctx); 185 | let sub = subscribe(coin, &ser, &c, ctx); 186 | 187 | let mut obj_id = object::id(&ser).to_bytes(); 188 | obj_id.push_back(11); 189 | 190 | // Work for time 0 191 | assert!(check_policy(obj_id, &pkg_version, &sub, &ser, &c)); 192 | c.increment_for_testing(1); 193 | assert!(check_policy(obj_id, &pkg_version, &sub, &ser, &c)); 194 | // time 3 should fail 195 | c.increment_for_testing(2); 196 | assert!(!check_policy(obj_id, &pkg_version, &sub, &ser, &c)); 197 | 198 | destroy_for_testing(ser, sub); 199 | destroy_versions_for_testing(pkg_version, _pkg_version_cap); 200 | c.destroy_for_testing(); 201 | } 202 | -------------------------------------------------------------------------------- /move/patterns/sources/tle.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// Time lock encryption pattern: 5 | /// - Anyone can encrypt to time T using key-id [pkg id][bcs::to_bytes(T)]. 6 | /// - Anyone can request the key for key-id = T after time T has passed. 7 | /// 8 | /// Use cases that can be built on top of this: MEV resilient trading, secure voting. 9 | /// 10 | /// Similar patterns: 11 | /// - Time lock encryption with an Update Cap - Anyone can create a shared object UpdatableTle{ id: UID, end_time: u64 } 12 | /// and receive UpdateCap { id: UID, updatable_tle_id: ID }. The associated key-id is [pkg id][id of UpdatableTle]. 13 | /// The cap owner can increase the end_time before the end_time has passed. Once the end_time has passed, anyone 14 | /// can request the key. 15 | /// 16 | /// This pattern does NOT implement versioning, please see other patterns for 17 | /// examples of versioning. 18 | /// 19 | module patterns::tle; 20 | 21 | use sui::{bcs::{Self, BCS}, clock}; 22 | 23 | const ENoAccess: u64 = 77; 24 | 25 | ///////////////////////////////////// 26 | /// Access control 27 | /// key format: [pkg id][bcs::to_bytes(T)] 28 | 29 | fun check_policy(id: vector, c: &clock::Clock): bool { 30 | let mut prepared: BCS = bcs::new(id); 31 | let t = prepared.peel_u64(); 32 | let leftovers = prepared.into_remainder_bytes(); 33 | 34 | // Check that the time has passed. 35 | (leftovers.length() == 0) && (c.timestamp_ms() >= t) 36 | } 37 | 38 | entry fun seal_approve(id: vector, c: &clock::Clock) { 39 | assert!(check_policy(id, c), ENoAccess); 40 | } 41 | 42 | #[test] 43 | fun test_approve() { 44 | let ctx = &mut tx_context::dummy(); 45 | let mut c = clock::create_for_testing(ctx); // time = 0 46 | let t = 1u64; 47 | let id = bcs::to_bytes(&t); 48 | 49 | // 0 < 1 50 | assert!(!check_policy(id, &c), 0); 51 | 52 | // 1 == 1 53 | c.increment_for_testing(1); 54 | assert!(check_policy(id, &c), 0); 55 | // 2 > 1 56 | c.increment_for_testing(1); 57 | assert!(check_policy(id, &c), 0); 58 | 59 | c.destroy_for_testing(); 60 | } 61 | -------------------------------------------------------------------------------- /move/patterns/sources/whitelist.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// Whitelist pattern: 5 | /// - Anyone can create a whitelist which defines a unique key-id. 6 | /// - Anyone can encrypt to that key-id. 7 | /// - Anyone on the whitelist can request the key associated with the whitelist's key-id, 8 | /// allowing it to decrypt all data encrypted to that key-id. 9 | /// 10 | /// Use cases that can be built on top of this: subscription based access to encrypted files. 11 | /// 12 | /// Similar patterns: 13 | /// - Whitelist with temporary privacy: same whitelist as below, but also store created_at: u64. 14 | /// After a fixed TTL anyone can access the key, regardless of being on the whitelist. 15 | /// Temporary privacy can be useful for compliance reasons, e.g., GDPR. 16 | /// 17 | /// This pattern implements versioning per whitelist. 18 | /// 19 | module patterns::whitelist; 20 | 21 | use sui::table; 22 | 23 | const ENoAccess: u64 = 1; 24 | const EInvalidCap: u64 = 2; 25 | const EDuplicate: u64 = 3; 26 | const ENotInWhitelist: u64 = 4; 27 | const EWrongVersion: u64 = 5; 28 | 29 | const VERSION: u64 = 1; 30 | 31 | public struct Whitelist has key { 32 | id: UID, 33 | version: u64, 34 | addresses: table::Table, 35 | } 36 | 37 | public struct Cap has key { 38 | id: UID, 39 | wl_id: ID, 40 | } 41 | 42 | ////////////////////////////////////////// 43 | /////// Simple whitelist with an admin cap 44 | 45 | /// Create a whitelist with an admin cap. 46 | /// The associated key-ids are [pkg id][whitelist id][nonce] for any nonce (thus 47 | /// many key-ids can be created for the same whitelist). 48 | public fun create_whitelist(ctx: &mut TxContext): (Cap, Whitelist) { 49 | let wl = Whitelist { 50 | id: object::new(ctx), 51 | version: VERSION, 52 | addresses: table::new(ctx), 53 | }; 54 | let cap = Cap { 55 | id: object::new(ctx), 56 | wl_id: object::id(&wl), 57 | }; 58 | (cap, wl) 59 | } 60 | 61 | // Helper function for creating a whitelist and send it back to sender. 62 | entry fun create_whitelist_entry(ctx: &mut TxContext) { 63 | let (cap, wl) = create_whitelist(ctx); 64 | transfer::share_object(wl); 65 | transfer::transfer(cap, ctx.sender()); 66 | } 67 | 68 | public fun add(wl: &mut Whitelist, cap: &Cap, account: address) { 69 | assert!(cap.wl_id == object::id(wl), EInvalidCap); 70 | assert!(!wl.addresses.contains(account), EDuplicate); 71 | wl.addresses.add(account, true); 72 | } 73 | 74 | public fun remove(wl: &mut Whitelist, cap: &Cap, account: address) { 75 | assert!(cap.wl_id == object::id(wl), EInvalidCap); 76 | assert!(wl.addresses.contains(account), ENotInWhitelist); 77 | wl.addresses.remove(account); 78 | } 79 | 80 | // Cap can also be used to upgrade the version of Whitelist in future versions, 81 | // see https://docs.sui.io/concepts/sui-move-concepts/packages/upgrade#versioned-shared-objects 82 | 83 | ////////////////////////////////////////////////////////// 84 | /// Access control 85 | /// key format: [pkg id][whitelist id][random nonce] 86 | /// (Alternative key format: [pkg id][creator address][random nonce] - see private_data.move) 87 | 88 | /// All whitelisted addresses can access all IDs with the prefix of the whitelist 89 | fun check_policy(caller: address, id: vector, wl: &Whitelist): bool { 90 | // Check we are using the right version of the package. 91 | assert!(wl.version == VERSION, EWrongVersion); 92 | 93 | // Check if the id has the right prefix 94 | let prefix = wl.id.to_bytes(); 95 | let mut i = 0; 96 | if (prefix.length() > id.length()) { 97 | return false 98 | }; 99 | while (i < prefix.length()) { 100 | if (prefix[i] != id[i]) { 101 | return false 102 | }; 103 | i = i + 1; 104 | }; 105 | 106 | // Check if user is in the whitelist 107 | wl.addresses.contains(caller) 108 | } 109 | 110 | entry fun seal_approve(id: vector, wl: &Whitelist, ctx: &TxContext) { 111 | assert!(check_policy(ctx.sender(), id, wl), ENoAccess); 112 | } 113 | 114 | #[test_only] 115 | public fun destroy_for_testing(wl: Whitelist, cap: Cap) { 116 | let Whitelist { id, version: _, addresses } = wl; 117 | addresses.drop(); 118 | object::delete(id); 119 | let Cap { id, .. } = cap; 120 | object::delete(id); 121 | } 122 | 123 | #[test] 124 | fun test_approve() { 125 | let ctx = &mut tx_context::dummy(); 126 | let (cap, mut wl) = create_whitelist(ctx); 127 | wl.add(&cap, @0x1); 128 | wl.remove(&cap, @0x1); 129 | wl.add(&cap, @0x2); 130 | 131 | // Fail for invalid id 132 | assert!(!check_policy(@0x2, b"123", &wl), 1); 133 | // Work for valid id, user 2 is in the whitelist 134 | let mut obj_id = object::id(&wl).to_bytes(); 135 | obj_id.push_back(11); 136 | assert!(check_policy(@0x2, obj_id, &wl), 1); 137 | // Fail for user 1 138 | assert!(!check_policy(@0x1, obj_id, &wl), 1); 139 | 140 | destroy_for_testing(wl, cap); 141 | } 142 | -------------------------------------------------------------------------------- /move/seal/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "seal" 3 | edition = "2024.beta" # edition = "legacy" to use legacy (pre-2024) Move 4 | # license = "" # e.g., "MIT", "GPL", "Apache 2.0" 5 | # authors = ["..."] # e.g., ["Joe Smith (joesmith@noemail.com)", "John Snow (johnsnow@noemail.com)"] 6 | # published-at = "0xfe2c71f4dc19cb596a5042bca40b9cd4b1e3517b37bc8c21491c3be33b3056af" 7 | 8 | [dependencies] 9 | Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "framework/testnet" } 10 | 11 | # For remote import, use the `{ git = "...", subdir = "...", rev = "..." }`. 12 | # Revision can be a branch, a tag, and a commit hash. 13 | # MyRemotePackage = { git = "https://some.remote/host.git", subdir = "remote/path", rev = "main" } 14 | 15 | # For local dependencies use `local = path`. Path is relative to the package root 16 | # Local = { local = "../path/to" } 17 | 18 | # To resolve a version conflict and force a specific version for dependency 19 | # override use `override = true` 20 | # Override = { local = "../conflicting/version", override = true } 21 | 22 | [addresses] 23 | seal = "0x0" 24 | 25 | # Named addresses will be accessible in Move as `@name`. They're also exported: 26 | # for example, `std = "0x1"` is exported by the Standard Library. 27 | # alice = "0xA11CE" 28 | 29 | [dev-dependencies] 30 | # The dev-dependencies section allows overriding dependencies for `--test` and 31 | # `--dev` modes. You can introduce test-only dependencies here. 32 | # Local = { local = "../path/to/dev-build" } 33 | 34 | [dev-addresses] 35 | # The dev-addresses section allows overwriting named addresses for the `--test` 36 | # and `--dev` modes. 37 | # alice = "0xB0B" 38 | 39 | -------------------------------------------------------------------------------- /move/seal/sources/gf256.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | /// Implementation of the Galois field GF(2^8) with irreducible polynomial x^8 + x^4 + x^3 + x + 1. 5 | /// This is the field used in AES. 6 | module seal::gf256; 7 | 8 | /// Table of Eᵢ = gⁱ where g = 0x03 generates the multiplicative group of the field. 9 | const EXP: vector = vector[ 10 | 0x01, 0x03, 0x05, 0x0f, 0x11, 0x33, 0x55, 0xff, 0x1a, 0x2e, 0x72, 0x96, 0xa1, 0xf8, 0x13, 0x35, 11 | 0x5f, 0xe1, 0x38, 0x48, 0xd8, 0x73, 0x95, 0xa4, 0xf7, 0x02, 0x06, 0x0a, 0x1e, 0x22, 0x66, 0xaa, 12 | 0xe5, 0x34, 0x5c, 0xe4, 0x37, 0x59, 0xeb, 0x26, 0x6a, 0xbe, 0xd9, 0x70, 0x90, 0xab, 0xe6, 0x31, 13 | 0x53, 0xf5, 0x04, 0x0c, 0x14, 0x3c, 0x44, 0xcc, 0x4f, 0xd1, 0x68, 0xb8, 0xd3, 0x6e, 0xb2, 0xcd, 14 | 0x4c, 0xd4, 0x67, 0xa9, 0xe0, 0x3b, 0x4d, 0xd7, 0x62, 0xa6, 0xf1, 0x08, 0x18, 0x28, 0x78, 0x88, 15 | 0x83, 0x9e, 0xb9, 0xd0, 0x6b, 0xbd, 0xdc, 0x7f, 0x81, 0x98, 0xb3, 0xce, 0x49, 0xdb, 0x76, 0x9a, 16 | 0xb5, 0xc4, 0x57, 0xf9, 0x10, 0x30, 0x50, 0xf0, 0x0b, 0x1d, 0x27, 0x69, 0xbb, 0xd6, 0x61, 0xa3, 17 | 0xfe, 0x19, 0x2b, 0x7d, 0x87, 0x92, 0xad, 0xec, 0x2f, 0x71, 0x93, 0xae, 0xe9, 0x20, 0x60, 0xa0, 18 | 0xfb, 0x16, 0x3a, 0x4e, 0xd2, 0x6d, 0xb7, 0xc2, 0x5d, 0xe7, 0x32, 0x56, 0xfa, 0x15, 0x3f, 0x41, 19 | 0xc3, 0x5e, 0xe2, 0x3d, 0x47, 0xc9, 0x40, 0xc0, 0x5b, 0xed, 0x2c, 0x74, 0x9c, 0xbf, 0xda, 0x75, 20 | 0x9f, 0xba, 0xd5, 0x64, 0xac, 0xef, 0x2a, 0x7e, 0x82, 0x9d, 0xbc, 0xdf, 0x7a, 0x8e, 0x89, 0x80, 21 | 0x9b, 0xb6, 0xc1, 0x58, 0xe8, 0x23, 0x65, 0xaf, 0xea, 0x25, 0x6f, 0xb1, 0xc8, 0x43, 0xc5, 0x54, 22 | 0xfc, 0x1f, 0x21, 0x63, 0xa5, 0xf4, 0x07, 0x09, 0x1b, 0x2d, 0x77, 0x99, 0xb0, 0xcb, 0x46, 0xca, 23 | 0x45, 0xcf, 0x4a, 0xde, 0x79, 0x8b, 0x86, 0x91, 0xa8, 0xe3, 0x3e, 0x42, 0xc6, 0x51, 0xf3, 0x0e, 24 | 0x12, 0x36, 0x5a, 0xee, 0x29, 0x7b, 0x8d, 0x8c, 0x8f, 0x8a, 0x85, 0x94, 0xa7, 0xf2, 0x0d, 0x17, 25 | 0x39, 0x4b, 0xdd, 0x7c, 0x84, 0x97, 0xa2, 0xfd, 0x1c, 0x24, 0x6c, 0xb4, 0xc7, 0x52, 0xf6, 26 | ]; 27 | 28 | /// Table of Lᵢ = LOG[i + 1] such that g^Lᵢ = i where g = 0x03. 29 | const LOG: vector = vector[ 30 | 0x00, 0x19, 0x01, 0x32, 0x02, 0x1a, 0xc6, 0x4b, 0xc7, 0x1b, 0x68, 0x33, 0xee, 0xdf, 0x03, 0x64, 31 | 0x04, 0xe0, 0x0e, 0x34, 0x8d, 0x81, 0xef, 0x4c, 0x71, 0x08, 0xc8, 0xf8, 0x69, 0x1c, 0xc1, 0x7d, 32 | 0xc2, 0x1d, 0xb5, 0xf9, 0xb9, 0x27, 0x6a, 0x4d, 0xe4, 0xa6, 0x72, 0x9a, 0xc9, 0x09, 0x78, 0x65, 33 | 0x2f, 0x8a, 0x05, 0x21, 0x0f, 0xe1, 0x24, 0x12, 0xf0, 0x82, 0x45, 0x35, 0x93, 0xda, 0x8e, 0x96, 34 | 0x8f, 0xdb, 0xbd, 0x36, 0xd0, 0xce, 0x94, 0x13, 0x5c, 0xd2, 0xf1, 0x40, 0x46, 0x83, 0x38, 0x66, 35 | 0xdd, 0xfd, 0x30, 0xbf, 0x06, 0x8b, 0x62, 0xb3, 0x25, 0xe2, 0x98, 0x22, 0x88, 0x91, 0x10, 0x7e, 36 | 0x6e, 0x48, 0xc3, 0xa3, 0xb6, 0x1e, 0x42, 0x3a, 0x6b, 0x28, 0x54, 0xfa, 0x85, 0x3d, 0xba, 0x2b, 37 | 0x79, 0x0a, 0x15, 0x9b, 0x9f, 0x5e, 0xca, 0x4e, 0xd4, 0xac, 0xe5, 0xf3, 0x73, 0xa7, 0x57, 0xaf, 38 | 0x58, 0xa8, 0x50, 0xf4, 0xea, 0xd6, 0x74, 0x4f, 0xae, 0xe9, 0xd5, 0xe7, 0xe6, 0xad, 0xe8, 0x2c, 39 | 0xd7, 0x75, 0x7a, 0xeb, 0x16, 0x0b, 0xf5, 0x59, 0xcb, 0x5f, 0xb0, 0x9c, 0xa9, 0x51, 0xa0, 0x7f, 40 | 0x0c, 0xf6, 0x6f, 0x17, 0xc4, 0x49, 0xec, 0xd8, 0x43, 0x1f, 0x2d, 0xa4, 0x76, 0x7b, 0xb7, 0xcc, 41 | 0xbb, 0x3e, 0x5a, 0xfb, 0x60, 0xb1, 0x86, 0x3b, 0x52, 0xa1, 0x6c, 0xaa, 0x55, 0x29, 0x9d, 0x97, 42 | 0xb2, 0x87, 0x90, 0x61, 0xbe, 0xdc, 0xfc, 0xbc, 0x95, 0xcf, 0xcd, 0x37, 0x3f, 0x5b, 0xd1, 0x53, 43 | 0x39, 0x84, 0x3c, 0x41, 0xa2, 0x6d, 0x47, 0x14, 0x2a, 0x9e, 0x5d, 0x56, 0xf2, 0xd3, 0xab, 0x44, 44 | 0x11, 0x92, 0xd9, 0x23, 0x20, 0x2e, 0x89, 0xb4, 0x7c, 0xb8, 0x26, 0x77, 0x99, 0xe3, 0xa5, 0x67, 45 | 0x4a, 0xed, 0xde, 0xc5, 0x31, 0xfe, 0x18, 0x0d, 0x63, 0x8c, 0x80, 0xc0, 0xf7, 0x70, 0x07, 46 | ]; 47 | 48 | #[allow(implicit_const_copy)] 49 | fun log(x: u8): u16 { 50 | assert!(x != 0); 51 | *LOG.borrow((x - 1) as u64) as u16 52 | } 53 | 54 | #[allow(implicit_const_copy)] 55 | fun exp(x: u16): u8 { 56 | *EXP.borrow((x % 255) as u64) 57 | } 58 | 59 | public(package) fun add(x: u8, y: u8): u8 { 60 | x ^ y 61 | } 62 | 63 | public(package) fun sub(x: u8, y: u8): u8 { 64 | add(x, y) 65 | } 66 | 67 | public(package) fun mul(x: u8, y: u8): u8 { 68 | if (x == 0 || y == 0) { 69 | return 0 70 | }; 71 | exp(log(x) + log(y)) 72 | } 73 | 74 | public(package) fun div(x: u8, y: u8): u8 { 75 | mul(x, exp(255 - log(y))) 76 | } 77 | 78 | #[test] 79 | fun test_field_ops() { 80 | // Test vector, partly from https://en.wikipedia.org/wiki/Finite_field_arithmetic#Rijndael's_(AES)_finite_field 81 | let a = 0x53; 82 | let b = 0xca; 83 | assert!(add(a, b) == 0x99); 84 | assert!(sub(a, b) == 0x99); 85 | assert!(mul(a, b) == 0x01); 86 | assert!(div(a, b) == 0xb5); 87 | } 88 | -------------------------------------------------------------------------------- /move/seal/sources/hmac256ctr.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | module seal::hmac256ctr; 5 | 6 | use std::{bcs, option::{none, some}}; 7 | use sui::hmac::hmac_sha3_256; 8 | 9 | const ENC_TAG: vector = b"HMAC-CTR-ENC"; 10 | const MAC_TAG: vector = b"HMAC-CTR-MAC"; 11 | 12 | /// Decrypt a message that was encrypted in Hmac256Ctr mode. 13 | public(package) fun decrypt( 14 | ciphertext: &vector, 15 | mac: &vector, 16 | aad: &vector, 17 | key: &vector, 18 | ): Option> { 19 | if (mac(key, aad, ciphertext) != mac) { 20 | return none() 21 | }; 22 | 23 | let mut next_block = 0; 24 | let mut i = 0; 25 | let mut current_mask = vector[]; 26 | some(ciphertext.map_ref!(|b| { 27 | if (i == 0) { 28 | current_mask = 29 | hmac_sha3_256(key, &vector[ENC_TAG, bcs::to_bytes(&(next_block as u64))].flatten()); 30 | next_block = next_block + 1; 31 | }; 32 | let result = *b ^ current_mask[i]; 33 | i = (i + 1) % 32; 34 | result 35 | })) 36 | } 37 | 38 | fun mac(key: &vector, aux: &vector, ciphertext: &vector): vector { 39 | let mut mac_input: vector = MAC_TAG; 40 | mac_input.append(bcs::to_bytes(&aux.length())); 41 | mac_input.append(*aux); 42 | mac_input.append(*ciphertext); 43 | hmac_sha3_256(key, &mac_input) 44 | } 45 | 46 | #[test] 47 | fun test_decrypt() { 48 | let key = x"76532ed510f487739f775afe6b64bc506e0097b9709f33fc5a18cb1c57fac66d"; 49 | let ciphertext = x"711ec5be4348c6194475dd2a45"; 50 | let mac = x"a94c5de42a5a0219fcd6871d379df4870c35e6406ebdfb7a51594fc18a1192bd"; 51 | let aux = b"something"; 52 | let decrypted = decrypt(&ciphertext, &mac, &aux, &key).borrow(); 53 | assert!(decrypted == b"Hello, world!"); 54 | } 55 | 56 | #[test] 57 | fun test_decrypt_fail() { 58 | let key = x"4804597e77d5025ab89d8559fe826dbd5591aaa5a0a3ca19ea572350e2a08c6b"; 59 | let ciphertext = x"98bf8da0ccbb35b6cf41effc83"; 60 | let mac = x"6c3d7fdb9b3a16a552b43a3300d6493f328e97aebf0697645cd35348ac926ec2"; 61 | let aux = b"something else"; 62 | assert!(decrypt(&ciphertext, &mac, &aux, &key) == none()); 63 | } 64 | 65 | #[test] 66 | fun test_decrypt_long() { 67 | let key = x"5bfdfd7c814903f1311bebacfffa3c001cbeb1cbb3275baa9aafe21fadd9f396"; 68 | let ciphertext = 69 | x"feadb8c8f781036f86b6a9f436cac6f9f68ba8fc8b8444f0331a5820f78580f32034f698f7ce15f25defae1749f0131c0a8b8c5e751b96aacf507d0dbd4d7790440d196a339fcb8498ca7dd236014e353729b7aa2cf524284a8d2305d2378494eadd6f"; 70 | let mac = x"85d498365972c3dc7a53f94232f9cb10dcc94eff064d6835d41d7a7536b47b51"; 71 | let aux = b"Mark Twain"; 72 | let decrypted = decrypt(&ciphertext, &mac, &aux, &key).borrow(); 73 | assert!( 74 | decrypted == b"The difference between a Miracle and a Fact is exactly the difference between a mermaid and a seal.", 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /move/seal/sources/kdf.move: -------------------------------------------------------------------------------- 1 | module seal::kdf; 2 | 3 | use std::hash::sha3_256; 4 | use sui::{bls12381::{Self, G1, G2, GT}, group_ops::Element}; 5 | 6 | const DST_KDF: vector = b"SUI-SEAL-IBE-BLS12381-H2-00"; 7 | const DST_ID: vector = b"SUI-SEAL-IBE-BLS12381-00"; 8 | 9 | public(package) fun kdf( 10 | input: &Element, 11 | nonce: &Element, 12 | gid: &Element, 13 | object_id: address, 14 | index: u8, 15 | ): vector { 16 | let mut bytes = DST_KDF; 17 | bytes.append(*input.bytes()); 18 | bytes.append(*nonce.bytes()); 19 | bytes.append(*gid.bytes()); 20 | bytes.append(object_id.to_bytes()); 21 | bytes.push_back(index); 22 | sha3_256(bytes) 23 | } 24 | 25 | public(package) fun hash_to_g1_with_dst(id: &vector): Element { 26 | let mut bytes = DST_ID; 27 | bytes.append(*id); 28 | bls12381::hash_to_g1(&bytes) 29 | } 30 | 31 | #[test] 32 | fun test_kdf() { 33 | use sui::bls12381::{scalar_from_u64, g2_generator, gt_generator, g2_mul, gt_mul}; 34 | let r = scalar_from_u64(12345u64); 35 | let x = gt_mul(&r, >_generator()); 36 | let nonce = g2_mul(&r, &g2_generator()); 37 | let gid = hash_to_g1_with_dst(&vector[0]); 38 | let derived_key = kdf(&x, &nonce, &gid, @0x0, 42); 39 | let expected = x"89befdfd6aecdce1305ddbca891d1c29f0507cfd5225cd6b11e52e60f088ea87"; 40 | assert!(derived_key == expected); 41 | } 42 | -------------------------------------------------------------------------------- /move/seal/sources/key_server.move: -------------------------------------------------------------------------------- 1 | // Copyright (c), Mysten Labs, Inc. 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Permissionless registration of a key server: 5 | // - Key server should expose an endpoint /service that returns the official object id of its key server (to prevent 6 | // impersonation) and a PoP(key=IBE key, m=[key_server_id | IBE public key]). 7 | // - Key server should expose an endpoint /fetch_key that allows users to request a key from the key server. 8 | 9 | module seal::key_server; 10 | 11 | use std::string::String; 12 | use sui::{bls12381::{G2, g2_from_bytes}, dynamic_field as df, group_ops::Element}; 13 | 14 | const EInvalidCap: u64 = 0; 15 | const EInvalidKeyType: u64 = 1; 16 | const EInvalidVersion: u64 = 2; 17 | const KeyTypeBonehFranklinBLS12381: u8 = 0; 18 | 19 | public struct KeyServer has key { 20 | id: UID, 21 | first_version: u64, 22 | last_version: u64, 23 | } 24 | 25 | public struct KeyServerV1 has store { 26 | name: String, 27 | url: String, 28 | key_type: u8, 29 | pk: vector, 30 | } 31 | 32 | public struct Cap has key { 33 | id: UID, 34 | key_server_id: ID, 35 | } 36 | 37 | public fun create_v1( 38 | name: String, 39 | url: String, 40 | key_type: u8, 41 | pk: vector, 42 | ctx: &mut TxContext, 43 | ): Cap { 44 | // Currently only BLS12-381 is supported. 45 | assert!(key_type == KeyTypeBonehFranklinBLS12381, EInvalidKeyType); 46 | let _ = g2_from_bytes(&pk); 47 | 48 | let mut key_server = KeyServer { 49 | id: object::new(ctx), 50 | first_version: 1, 51 | last_version: 1, 52 | }; 53 | 54 | let key_server_v1 = KeyServerV1 { 55 | name, 56 | url, 57 | key_type, 58 | pk, 59 | }; 60 | df::add(&mut key_server.id, 1, key_server_v1); 61 | 62 | let cap = Cap { 63 | id: object::new(ctx), 64 | key_server_id: object::id(&key_server), 65 | }; 66 | 67 | transfer::share_object(key_server); 68 | cap 69 | } 70 | 71 | // Helper function to register a key server and transfer the cap to the caller. 72 | entry fun create_and_transfer_v1( 73 | name: String, 74 | url: String, 75 | key_type: u8, 76 | pk: vector, 77 | ctx: &mut TxContext, 78 | ) { 79 | let cap = create_v1(name, url, key_type, pk, ctx); 80 | transfer::transfer(cap, ctx.sender()); 81 | } 82 | 83 | public fun v1(s: &KeyServer): &KeyServerV1 { 84 | assert!(df::exists_(&s.id, 1), EInvalidVersion); 85 | df::borrow(&s.id, 1) 86 | } 87 | 88 | public fun name(s: &KeyServer): String { 89 | let v1 = v1(s); 90 | v1.name 91 | } 92 | 93 | public fun url(s: &KeyServer): String { 94 | let v1 = v1(s); 95 | v1.url 96 | } 97 | 98 | public fun key_type(s: &mut KeyServer): u8 { 99 | let v1 = v1(s); 100 | v1.key_type 101 | } 102 | 103 | public fun pk(s: &KeyServer): &vector { 104 | let v1 = v1(s); 105 | &v1.pk 106 | } 107 | 108 | public fun id(s: &KeyServer): &UID { 109 | &s.id 110 | } 111 | 112 | public fun pk_as_bf_bls12381(s: &KeyServer): Element { 113 | let v1: &KeyServerV1 = v1(s); 114 | assert!(v1.key_type == KeyTypeBonehFranklinBLS12381, EInvalidKeyType); 115 | g2_from_bytes(&v1.pk) 116 | } 117 | 118 | public fun update(s: &mut KeyServer, cap: &Cap, url: String) { 119 | assert!(object::id(s) == cap.key_server_id, EInvalidCap); 120 | assert!(df::exists_(&s.id, 1), EInvalidVersion); 121 | let v1: &mut KeyServerV1 = df::borrow_mut(&mut s.id, 1); 122 | v1.url = url; 123 | } 124 | 125 | #[test_only] 126 | public fun destroy_cap(c: Cap) { 127 | let Cap { id, .. } = c; 128 | object::delete(id); 129 | } 130 | 131 | #[test] 132 | fun test_flow() { 133 | use sui::test_scenario::{Self, next_tx, ctx}; 134 | use sui::bls12381::{g2_generator}; 135 | use std::string; 136 | 137 | let addr1 = @0xA; 138 | let mut scenario = test_scenario::begin(addr1); 139 | 140 | let pk = g2_generator(); 141 | let pk_bytes = *pk.bytes(); 142 | let cap = create_v1( 143 | string::utf8(b"mysten"), 144 | string::utf8(b"https::/mysten-labs.com"), 145 | 0, 146 | pk_bytes, 147 | ctx(&mut scenario), 148 | ); 149 | next_tx(&mut scenario, addr1); 150 | 151 | let mut s: KeyServer = test_scenario::take_shared(&scenario); 152 | assert!(name(&s) == string::utf8(b"mysten"), 0); 153 | assert!(url(&s) == string::utf8(b"https::/mysten-labs.com"), 0); 154 | assert!(pk(&s) == pk.bytes(), 0); 155 | s.update(&cap, string::utf8(b"https::/mysten-labs2.com")); 156 | assert!(url(&s) == string::utf8(b"https::/mysten-labs2.com"), 0); 157 | 158 | test_scenario::return_shared(s); 159 | destroy_cap(cap); 160 | test_scenario::end(scenario); 161 | } 162 | -------------------------------------------------------------------------------- /move/seal/sources/polynomial.move: -------------------------------------------------------------------------------- 1 | module seal::polynomial; 2 | 3 | use seal::gf256; 4 | 5 | /// This represents a polynomial over GF(2^8). 6 | /// The first coefficient is the constant term. 7 | public struct Polynomial has copy, drop, store { 8 | coefficients: vector, 9 | } 10 | 11 | public(package) fun get_constant_term(p: &Polynomial): u8 { 12 | if (p.coefficients.is_empty()) { 13 | return 0 14 | }; 15 | p.coefficients[0] 16 | } 17 | 18 | fun add(x: &Polynomial, y: &Polynomial): Polynomial { 19 | let x_length: u64 = x.coefficients.length(); 20 | let y_length: u64 = y.coefficients.length(); 21 | if (x_length < y_length) { 22 | // We assume that x is the longer vector 23 | return add(y, x) 24 | }; 25 | let mut coefficients: vector = vector::empty(); 26 | y_length.do!(|i| coefficients.push_back(gf256::add(x.coefficients[i], y.coefficients[i]))); 27 | (x_length - y_length).do!(|i| coefficients.push_back(x.coefficients[i + y_length])); 28 | let result = Polynomial { coefficients }; 29 | reduce(result); 30 | result 31 | } 32 | 33 | public(package) fun degree(x: &Polynomial): u64 { 34 | x.coefficients.length() - 1 35 | } 36 | 37 | fun reduce(mut x: Polynomial) { 38 | while (x.coefficients.length() > 0 && x.coefficients[x.coefficients.length() - 1] == 0) { 39 | x.coefficients.pop_back(); 40 | }; 41 | } 42 | 43 | fun mul(x: &Polynomial, y: &Polynomial): Polynomial { 44 | let degree = x.degree() + y.degree(); 45 | 46 | let coefficients = vector::tabulate!(degree + 1, |i| { 47 | let mut sum = 0; 48 | i.do_eq!(|j| { 49 | if (j <= x.degree() && i - j <= y.degree()) { 50 | sum = gf256::add(sum, gf256::mul(x.coefficients[j], y.coefficients[i - j])); 51 | } 52 | }); 53 | sum 54 | }); 55 | let result = Polynomial { coefficients }; 56 | reduce(result); 57 | result 58 | } 59 | 60 | fun div(x: &Polynomial, s: u8): Polynomial { 61 | scale(x, gf256::div(1, s)) 62 | } 63 | 64 | fun scale(x: &Polynomial, s: u8): Polynomial { 65 | Polynomial { coefficients: x.coefficients.map_ref!(|c| gf256::mul(*c, s)) } 66 | } 67 | 68 | /// Return x - c 69 | fun monic_linear(c: &u8): Polynomial { 70 | Polynomial { coefficients: vector[gf256::sub(0, *c), 1] } 71 | } 72 | 73 | public(package) fun interpolate(x: &vector, y: &vector): Polynomial { 74 | assert!(x.length() == y.length()); 75 | let n = x.length(); 76 | let mut sum = Polynomial { coefficients: vector::empty() }; 77 | n.do!(|j| { 78 | let mut product = Polynomial { coefficients: vector[1] }; 79 | n.do!(|i| { 80 | if (i != j) { 81 | product = 82 | mul( 83 | &product, 84 | &div(&monic_linear(&x[i]), gf256::sub(x[j], x[i])), 85 | ); 86 | }; 87 | }); 88 | sum = add(&sum, &scale(&product, y[j])); 89 | }); 90 | sum 91 | } 92 | 93 | public fun evaluate(p: &Polynomial, x: u8): u8 { 94 | let mut result = 0; 95 | let n = p.coefficients.length(); 96 | n.do!(|i| { 97 | result = gf256::add(gf256::mul(result, x), p.coefficients[n - i - 1]); 98 | }); 99 | result 100 | } 101 | 102 | #[test] 103 | fun test_arithmetic() { 104 | let x = Polynomial { coefficients: vector[1, 2, 3] }; 105 | let y = Polynomial { coefficients: vector[4, 5] }; 106 | let z = Polynomial { coefficients: vector[2] }; 107 | assert!(x.add(&y).coefficients == vector[5, 7, 3]); 108 | assert!(x.mul(&z).coefficients == vector[2, 4, 6]); 109 | assert!(x.mul(&y).coefficients == x"040d060f"); 110 | } 111 | 112 | #[test] 113 | fun test_interpolate() { 114 | let x = vector[1, 2, 3]; 115 | let y = vector[7, 11, 17]; 116 | let p = interpolate(&x, &y); 117 | assert!(p.coefficients == x"1d150f"); 118 | x.zip_do!(y, |x, y| assert!(p.evaluate(x) == y)); 119 | } 120 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.87" 3 | components = ["clippy", "rustfmt"] 4 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | style_edition = "2021" 2 | -------------------------------------------------------------------------------- /scripts/changed-files.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c), Mysten Labs, Inc. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | set -e 6 | 7 | # Check for modified or untracked files after CI has run 8 | diff="$(git diff)" 9 | echo "${diff}" 10 | [[ -z "${diff}" ]] 11 | 12 | changed_files="$(git status --porcelain)" 13 | echo "${changed_files}" 14 | [[ -z "${changed_files}" ]] 15 | -------------------------------------------------------------------------------- /scripts/get_current_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c), Mysten Labs, Inc. 3 | # SPDX-License-Identifier: Apache-2.0 4 | 5 | # Script requirements: 6 | # - curl 7 | # - jq 8 | 9 | # Fail on first error, on undefined variables, and on failures in pipelines. 10 | set -euo pipefail 11 | 12 | # Go to the repo root directory. 13 | cd "$(git rev-parse --show-toplevel)" 14 | 15 | # Check 1 argument is given 16 | if [ $# -lt 1 ] 17 | then 18 | echo "Usage : $0 " 19 | exit 1 20 | fi 21 | 22 | # The first argument should be the name of a crate. 23 | CRATE_NAME="$1" 24 | 25 | cargo metadata --format-version 1 | \ 26 | jq --arg crate_name "$CRATE_NAME" --exit-status -r \ 27 | '.packages[] | select(.name == $crate_name) | .version' 28 | --------------------------------------------------------------------------------