├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── release-stubs.yml │ ├── release.yml │ └── zizmor.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── dev-requirements.txt ├── pyproject.toml ├── pyrage-stubs ├── LICENSE ├── README.md ├── pyproject.toml ├── pyrage │ ├── __init__.pyi │ ├── passphrase.pyi │ ├── plugin.pyi │ ├── py.typed │ ├── ssh.pyi │ └── x25519.pyi └── setup.py ├── src ├── lib.rs ├── passphrase.rs ├── plugin.rs ├── ssh.rs └── x25519.rs └── test ├── __init__.py ├── assets ├── ed25519 ├── ed25519.pub ├── rsa2048 ├── rsa2048.pub ├── rsa4096 └── rsa4096.pub ├── test_passphrase.py ├── test_pyrage.py ├── test_ssh.py ├── test_x25519.py └── utils.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | groups: 9 | actions: 10 | patterns: 11 | - "*" 12 | 13 | - package-ecosystem: cargo 14 | directory: "/" 15 | schedule: 16 | interval: weekly 17 | groups: 18 | cargo: 19 | patterns: 20 | - "*" 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | persist-credentials: false 19 | 20 | - name: Format 21 | run: cargo fmt && git diff --exit-code 22 | 23 | - name: Lint 24 | run: | 25 | rustup update 26 | rustup component add clippy 27 | cargo clippy -- -D warnings 28 | 29 | test: 30 | strategy: 31 | matrix: 32 | platform: 33 | - macos-latest 34 | - ubuntu-latest 35 | - windows-latest 36 | # Use 22.04, as 24.04 is unstable atm: 37 | # https://github.com/orgs/community/discussions/148648#discussioncomment-11890717 38 | - ubuntu-22.04-arm 39 | python: 40 | - "3.9" 41 | - "3.10" 42 | - "3.11" 43 | - "3.12" 44 | - "3.13" 45 | runs-on: ${{ matrix.platform }} 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | with: 50 | persist-credentials: false 51 | 52 | - uses: actions/setup-python@v5 53 | with: 54 | python-version: ${{ matrix.python }} 55 | 56 | - name: Build 57 | shell: bash 58 | run: make develop 59 | 60 | - name: Test 61 | shell: bash 62 | run: make test 63 | 64 | all-tests-pass: 65 | if: always() 66 | 67 | needs: 68 | - test 69 | 70 | runs-on: ubuntu-latest 71 | 72 | steps: 73 | - name: check test jobs 74 | uses: re-actors/alls-green@v1.2.2 75 | with: 76 | jobs: ${{ toJSON(needs) }} 77 | -------------------------------------------------------------------------------- /.github/workflows/release-stubs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | inputs: 4 | dry-run: 5 | required: true 6 | default: false 7 | type: boolean 8 | name: release-stubs 9 | 10 | permissions: {} 11 | 12 | jobs: 13 | release-stubs: 14 | name: build pyrage-stubs 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | persist-credentials: false 21 | 22 | - uses: actions/setup-python@v5 23 | with: 24 | python-version-file: pyproject.toml 25 | 26 | - name: build pyrage-stubs 27 | run: | 28 | make dist-pyrage-stubs 29 | 30 | - name: upload dists 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: dist 34 | path: dist/ 35 | 36 | pypi-publish: 37 | name: publish all dists to PyPI 38 | runs-on: ubuntu-latest 39 | environment: release 40 | permissions: 41 | # Used for OIDC publishing. 42 | id-token: write 43 | needs: 44 | - release-stubs 45 | 46 | steps: 47 | - name: fetch dists 48 | uses: actions/download-artifact@v4 49 | with: 50 | name: dist 51 | path: dist/ 52 | 53 | - name: publish 54 | if: ${{ !inputs.dry-run }} 55 | uses: pypa/gh-action-pypi-publish@v1.12.4 56 | with: 57 | attestations: true 58 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: 4 | - published 5 | 6 | name: release 7 | 8 | permissions: {} 9 | 10 | jobs: 11 | release-linux: 12 | name: build Linux release dists 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | persist-credentials: false 18 | 19 | - uses: actions/setup-python@v5 20 | with: 21 | python-version-file: pyproject.toml 22 | 23 | - name: build wheels 24 | run: make dist-pyrage 25 | 26 | - name: upload linux dists 27 | uses: actions/upload-artifact@v4 28 | with: 29 | name: pyrage-dists-linux 30 | path: dist/ 31 | 32 | release-linux-arm: 33 | name: build Linux arm release dists 34 | # Use 22.04, as 24.04 is unstable atm: 35 | # https://github.com/orgs/community/discussions/148648#discussioncomment-11890717 36 | runs-on: ubuntu-22.04-arm 37 | steps: 38 | - uses: actions/checkout@v4 39 | with: 40 | persist-credentials: false 41 | 42 | - uses: actions/setup-python@v5 43 | with: 44 | python-version-file: pyproject.toml 45 | 46 | - name: build wheels 47 | run: make dist-pyrage 48 | 49 | - name: upload linux dists 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: pyrage-dists-linux-arm 53 | path: dist/ 54 | 55 | release-macos: 56 | name: build macOS release dists 57 | runs-on: macos-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | with: 61 | persist-credentials: false 62 | 63 | - uses: actions/setup-python@v5 64 | with: 65 | python-version-file: pyproject.toml 66 | 67 | - name: build wheels 68 | run: | 69 | rustup target add aarch64-apple-darwin x86_64-apple-darwin 70 | make env 71 | source env/bin/activate 72 | maturin build --release --strip --target universal2-apple-darwin 73 | mv target/wheels/ dist/ 74 | 75 | - name: upload macos dists 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: pyrage-dists-macos 79 | path: dist/ 80 | 81 | release-windows: 82 | name: build Windows release dists 83 | runs-on: windows-latest 84 | steps: 85 | - uses: actions/checkout@v4 86 | with: 87 | persist-credentials: false 88 | 89 | - uses: actions/setup-python@v5 90 | with: 91 | python-version-file: pyproject.toml 92 | 93 | - name: build wheels 94 | shell: bash 95 | run: | 96 | make env 97 | source env/Scripts/activate 98 | maturin build --release --strip 99 | mv target/wheels/ dist/ 100 | 101 | - name: upload windows dists 102 | uses: actions/upload-artifact@v4 103 | with: 104 | name: pyrage-dists-windows 105 | path: dist/ 106 | 107 | pypi-publish: 108 | name: publish all dists to PyPI 109 | runs-on: ubuntu-latest 110 | environment: release 111 | permissions: 112 | # Used for OIDC publishing. 113 | # Used to sign the release's artifacts with sigstore-python. 114 | id-token: write 115 | 116 | # Used to attach signing artifacts to the published release. 117 | contents: write 118 | needs: 119 | - release-linux 120 | - release-linux-arm 121 | - release-macos 122 | - release-windows 123 | 124 | steps: 125 | - name: fetch dists 126 | uses: actions/download-artifact@v4 127 | with: 128 | pattern: pyrage-dists-* 129 | path: dist/ 130 | merge-multiple: true 131 | 132 | - name: publish 133 | uses: pypa/gh-action-pypi-publish@v1.12.4 134 | -------------------------------------------------------------------------------- /.github/workflows/zizmor.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions Security Analysis with zizmor 🌈 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | pull_request: 7 | branches: ["*"] 8 | 9 | permissions: {} 10 | 11 | jobs: 12 | zizmor: 13 | name: zizmor latest via Cargo 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | security-events: write 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 21 | with: 22 | persist-credentials: false 23 | - name: Install the latest version of uv 24 | uses: astral-sh/setup-uv@f0ec1fc3b38f5e7cd731bb6ce540c5af426746bb # v6.1.0 25 | - name: Run zizmor 🌈 26 | run: uvx zizmor --format sarif . > results.sarif 27 | env: 28 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Upload SARIF file 30 | uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 31 | with: 32 | sarif_file: results.sarif 33 | category: zizmor 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /env 3 | __pycache__ 4 | *.pyc 5 | /dist 6 | *.egg-info/ 7 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aead" 7 | version = "0.5.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" 10 | dependencies = [ 11 | "crypto-common", 12 | "generic-array", 13 | ] 14 | 15 | [[package]] 16 | name = "aes" 17 | version = "0.8.3" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" 20 | dependencies = [ 21 | "cfg-if", 22 | "cipher", 23 | "cpufeatures", 24 | ] 25 | 26 | [[package]] 27 | name = "aes-gcm" 28 | version = "0.10.3" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" 31 | dependencies = [ 32 | "aead", 33 | "aes", 34 | "cipher", 35 | "ctr", 36 | "ghash", 37 | "subtle", 38 | ] 39 | 40 | [[package]] 41 | name = "age" 42 | version = "0.11.1" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "57fc171f4874fa10887e47088f81a55fcf030cd421aa31ec2b370cafebcc608a" 45 | dependencies = [ 46 | "aes", 47 | "aes-gcm", 48 | "age-core", 49 | "base64", 50 | "bcrypt-pbkdf", 51 | "bech32", 52 | "cbc", 53 | "chacha20poly1305", 54 | "cipher", 55 | "cookie-factory", 56 | "ctr", 57 | "curve25519-dalek", 58 | "hmac", 59 | "i18n-embed", 60 | "i18n-embed-fl", 61 | "lazy_static", 62 | "nom", 63 | "num-traits", 64 | "pin-project", 65 | "rand", 66 | "rsa", 67 | "rust-embed", 68 | "scrypt", 69 | "sha2", 70 | "subtle", 71 | "which", 72 | "wsl", 73 | "x25519-dalek", 74 | "zeroize", 75 | ] 76 | 77 | [[package]] 78 | name = "age-core" 79 | version = "0.11.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99" 82 | dependencies = [ 83 | "base64", 84 | "chacha20poly1305", 85 | "cookie-factory", 86 | "hkdf", 87 | "io_tee", 88 | "nom", 89 | "rand", 90 | "secrecy", 91 | "sha2", 92 | "tempfile", 93 | ] 94 | 95 | [[package]] 96 | name = "arc-swap" 97 | version = "1.6.0" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" 100 | 101 | [[package]] 102 | name = "autocfg" 103 | version = "1.1.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 106 | 107 | [[package]] 108 | name = "base64" 109 | version = "0.21.7" 110 | source = "registry+https://github.com/rust-lang/crates.io-index" 111 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 112 | 113 | [[package]] 114 | name = "base64ct" 115 | version = "1.6.0" 116 | source = "registry+https://github.com/rust-lang/crates.io-index" 117 | checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" 118 | 119 | [[package]] 120 | name = "basic-toml" 121 | version = "0.1.9" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "823388e228f614e9558c6804262db37960ec8821856535f5c3f59913140558f8" 124 | dependencies = [ 125 | "serde", 126 | ] 127 | 128 | [[package]] 129 | name = "bcrypt-pbkdf" 130 | version = "0.10.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" 133 | dependencies = [ 134 | "blowfish", 135 | "pbkdf2", 136 | "sha2", 137 | ] 138 | 139 | [[package]] 140 | name = "bech32" 141 | version = "0.9.1" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" 144 | 145 | [[package]] 146 | name = "bitflags" 147 | version = "1.3.2" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 150 | 151 | [[package]] 152 | name = "bitflags" 153 | version = "2.5.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" 156 | 157 | [[package]] 158 | name = "block-buffer" 159 | version = "0.10.4" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 162 | dependencies = [ 163 | "generic-array", 164 | ] 165 | 166 | [[package]] 167 | name = "block-padding" 168 | version = "0.3.3" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" 171 | dependencies = [ 172 | "generic-array", 173 | ] 174 | 175 | [[package]] 176 | name = "blowfish" 177 | version = "0.9.1" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" 180 | dependencies = [ 181 | "byteorder", 182 | "cipher", 183 | ] 184 | 185 | [[package]] 186 | name = "bytecount" 187 | version = "0.6.8" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" 190 | 191 | [[package]] 192 | name = "byteorder" 193 | version = "1.5.0" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 196 | 197 | [[package]] 198 | name = "camino" 199 | version = "1.1.9" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" 202 | dependencies = [ 203 | "serde", 204 | ] 205 | 206 | [[package]] 207 | name = "cargo-platform" 208 | version = "0.1.9" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" 211 | dependencies = [ 212 | "serde", 213 | ] 214 | 215 | [[package]] 216 | name = "cargo_metadata" 217 | version = "0.14.2" 218 | source = "registry+https://github.com/rust-lang/crates.io-index" 219 | checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" 220 | dependencies = [ 221 | "camino", 222 | "cargo-platform", 223 | "semver", 224 | "serde", 225 | "serde_json", 226 | ] 227 | 228 | [[package]] 229 | name = "cbc" 230 | version = "0.1.2" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" 233 | dependencies = [ 234 | "cipher", 235 | ] 236 | 237 | [[package]] 238 | name = "cfg-if" 239 | version = "1.0.0" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 242 | 243 | [[package]] 244 | name = "chacha20" 245 | version = "0.9.1" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" 248 | dependencies = [ 249 | "cfg-if", 250 | "cipher", 251 | "cpufeatures", 252 | ] 253 | 254 | [[package]] 255 | name = "chacha20poly1305" 256 | version = "0.10.1" 257 | source = "registry+https://github.com/rust-lang/crates.io-index" 258 | checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" 259 | dependencies = [ 260 | "aead", 261 | "chacha20", 262 | "cipher", 263 | "poly1305", 264 | "zeroize", 265 | ] 266 | 267 | [[package]] 268 | name = "cipher" 269 | version = "0.4.4" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" 272 | dependencies = [ 273 | "crypto-common", 274 | "inout", 275 | "zeroize", 276 | ] 277 | 278 | [[package]] 279 | name = "const-oid" 280 | version = "0.9.5" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" 283 | 284 | [[package]] 285 | name = "cookie-factory" 286 | version = "0.3.2" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "396de984970346b0d9e93d1415082923c679e5ae5c3ee3dcbd104f5610af126b" 289 | 290 | [[package]] 291 | name = "cpufeatures" 292 | version = "0.2.11" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" 295 | dependencies = [ 296 | "libc", 297 | ] 298 | 299 | [[package]] 300 | name = "crossbeam-utils" 301 | version = "0.8.20" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" 304 | 305 | [[package]] 306 | name = "crypto-common" 307 | version = "0.1.6" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 310 | dependencies = [ 311 | "generic-array", 312 | "rand_core", 313 | "typenum", 314 | ] 315 | 316 | [[package]] 317 | name = "ctr" 318 | version = "0.9.2" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" 321 | dependencies = [ 322 | "cipher", 323 | ] 324 | 325 | [[package]] 326 | name = "curve25519-dalek" 327 | version = "4.1.1" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" 330 | dependencies = [ 331 | "cfg-if", 332 | "cpufeatures", 333 | "curve25519-dalek-derive", 334 | "fiat-crypto", 335 | "platforms", 336 | "rustc_version", 337 | "subtle", 338 | "zeroize", 339 | ] 340 | 341 | [[package]] 342 | name = "curve25519-dalek-derive" 343 | version = "0.1.1" 344 | source = "registry+https://github.com/rust-lang/crates.io-index" 345 | checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" 346 | dependencies = [ 347 | "proc-macro2", 348 | "quote", 349 | "syn", 350 | ] 351 | 352 | [[package]] 353 | name = "dashmap" 354 | version = "6.1.0" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 357 | dependencies = [ 358 | "cfg-if", 359 | "crossbeam-utils", 360 | "hashbrown", 361 | "lock_api", 362 | "once_cell", 363 | "parking_lot_core", 364 | ] 365 | 366 | [[package]] 367 | name = "der" 368 | version = "0.7.8" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" 371 | dependencies = [ 372 | "const-oid", 373 | "zeroize", 374 | ] 375 | 376 | [[package]] 377 | name = "digest" 378 | version = "0.10.7" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 381 | dependencies = [ 382 | "block-buffer", 383 | "const-oid", 384 | "crypto-common", 385 | "subtle", 386 | ] 387 | 388 | [[package]] 389 | name = "displaydoc" 390 | version = "0.2.4" 391 | source = "registry+https://github.com/rust-lang/crates.io-index" 392 | checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" 393 | dependencies = [ 394 | "proc-macro2", 395 | "quote", 396 | "syn", 397 | ] 398 | 399 | [[package]] 400 | name = "either" 401 | version = "1.10.0" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 404 | 405 | [[package]] 406 | name = "errno" 407 | version = "0.3.8" 408 | source = "registry+https://github.com/rust-lang/crates.io-index" 409 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 410 | dependencies = [ 411 | "libc", 412 | "windows-sys", 413 | ] 414 | 415 | [[package]] 416 | name = "error-chain" 417 | version = "0.12.4" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" 420 | dependencies = [ 421 | "version_check", 422 | ] 423 | 424 | [[package]] 425 | name = "fastrand" 426 | version = "2.0.2" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" 429 | 430 | [[package]] 431 | name = "fiat-crypto" 432 | version = "0.2.6" 433 | source = "registry+https://github.com/rust-lang/crates.io-index" 434 | checksum = "1676f435fc1dadde4d03e43f5d62b259e1ce5f40bd4ffb21db2b42ebe59c1382" 435 | 436 | [[package]] 437 | name = "find-crate" 438 | version = "0.6.3" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2" 441 | dependencies = [ 442 | "toml", 443 | ] 444 | 445 | [[package]] 446 | name = "fluent" 447 | version = "0.16.0" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7" 450 | dependencies = [ 451 | "fluent-bundle", 452 | "unic-langid", 453 | ] 454 | 455 | [[package]] 456 | name = "fluent-bundle" 457 | version = "0.15.2" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd" 460 | dependencies = [ 461 | "fluent-langneg", 462 | "fluent-syntax", 463 | "intl-memoizer", 464 | "intl_pluralrules", 465 | "rustc-hash", 466 | "self_cell 0.10.3", 467 | "smallvec", 468 | "unic-langid", 469 | ] 470 | 471 | [[package]] 472 | name = "fluent-langneg" 473 | version = "0.13.0" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" 476 | dependencies = [ 477 | "unic-langid", 478 | ] 479 | 480 | [[package]] 481 | name = "fluent-syntax" 482 | version = "0.11.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78" 485 | dependencies = [ 486 | "thiserror", 487 | ] 488 | 489 | [[package]] 490 | name = "generic-array" 491 | version = "0.14.7" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 494 | dependencies = [ 495 | "typenum", 496 | "version_check", 497 | ] 498 | 499 | [[package]] 500 | name = "getrandom" 501 | version = "0.2.11" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" 504 | dependencies = [ 505 | "cfg-if", 506 | "libc", 507 | "wasi", 508 | ] 509 | 510 | [[package]] 511 | name = "ghash" 512 | version = "0.5.0" 513 | source = "registry+https://github.com/rust-lang/crates.io-index" 514 | checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" 515 | dependencies = [ 516 | "opaque-debug", 517 | "polyval", 518 | ] 519 | 520 | [[package]] 521 | name = "glob" 522 | version = "0.3.2" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 525 | 526 | [[package]] 527 | name = "hashbrown" 528 | version = "0.14.3" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" 531 | 532 | [[package]] 533 | name = "heck" 534 | version = "0.5.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 537 | 538 | [[package]] 539 | name = "hkdf" 540 | version = "0.12.3" 541 | source = "registry+https://github.com/rust-lang/crates.io-index" 542 | checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" 543 | dependencies = [ 544 | "hmac", 545 | ] 546 | 547 | [[package]] 548 | name = "hmac" 549 | version = "0.12.1" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" 552 | dependencies = [ 553 | "digest", 554 | ] 555 | 556 | [[package]] 557 | name = "home" 558 | version = "0.5.9" 559 | source = "registry+https://github.com/rust-lang/crates.io-index" 560 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" 561 | dependencies = [ 562 | "windows-sys", 563 | ] 564 | 565 | [[package]] 566 | name = "i18n-config" 567 | version = "0.4.7" 568 | source = "registry+https://github.com/rust-lang/crates.io-index" 569 | checksum = "8e88074831c0be5b89181b05e6748c4915f77769ecc9a4c372f88b169a8509c9" 570 | dependencies = [ 571 | "basic-toml", 572 | "log", 573 | "serde", 574 | "serde_derive", 575 | "thiserror", 576 | "unic-langid", 577 | ] 578 | 579 | [[package]] 580 | name = "i18n-embed" 581 | version = "0.15.2" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "a7839d8c7bb8da7bd58c1112d3a1aeb7f178ff3df4ae87783e758ca3bfb750b7" 584 | dependencies = [ 585 | "arc-swap", 586 | "fluent", 587 | "fluent-langneg", 588 | "fluent-syntax", 589 | "i18n-embed-impl", 590 | "intl-memoizer", 591 | "lazy_static", 592 | "log", 593 | "parking_lot", 594 | "rust-embed", 595 | "thiserror", 596 | "unic-langid", 597 | "walkdir", 598 | ] 599 | 600 | [[package]] 601 | name = "i18n-embed-fl" 602 | version = "0.9.2" 603 | source = "registry+https://github.com/rust-lang/crates.io-index" 604 | checksum = "f6e9571c3cba9eba538eaa5ee40031b26debe76f0c7e17bafc97ea57a76cd82e" 605 | dependencies = [ 606 | "dashmap", 607 | "find-crate", 608 | "fluent", 609 | "fluent-syntax", 610 | "i18n-config", 611 | "i18n-embed", 612 | "lazy_static", 613 | "proc-macro-error2", 614 | "proc-macro2", 615 | "quote", 616 | "strsim", 617 | "syn", 618 | "unic-langid", 619 | ] 620 | 621 | [[package]] 622 | name = "i18n-embed-impl" 623 | version = "0.8.4" 624 | source = "registry+https://github.com/rust-lang/crates.io-index" 625 | checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2" 626 | dependencies = [ 627 | "find-crate", 628 | "i18n-config", 629 | "proc-macro2", 630 | "quote", 631 | "syn", 632 | ] 633 | 634 | [[package]] 635 | name = "indoc" 636 | version = "2.0.4" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" 639 | 640 | [[package]] 641 | name = "inout" 642 | version = "0.1.3" 643 | source = "registry+https://github.com/rust-lang/crates.io-index" 644 | checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" 645 | dependencies = [ 646 | "block-padding", 647 | "generic-array", 648 | ] 649 | 650 | [[package]] 651 | name = "intl-memoizer" 652 | version = "0.5.1" 653 | source = "registry+https://github.com/rust-lang/crates.io-index" 654 | checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f" 655 | dependencies = [ 656 | "type-map", 657 | "unic-langid", 658 | ] 659 | 660 | [[package]] 661 | name = "intl_pluralrules" 662 | version = "7.0.2" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" 665 | dependencies = [ 666 | "unic-langid", 667 | ] 668 | 669 | [[package]] 670 | name = "io_tee" 671 | version = "0.1.1" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304" 674 | 675 | [[package]] 676 | name = "itoa" 677 | version = "1.0.14" 678 | source = "registry+https://github.com/rust-lang/crates.io-index" 679 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 680 | 681 | [[package]] 682 | name = "lazy_static" 683 | version = "1.4.0" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 686 | dependencies = [ 687 | "spin", 688 | ] 689 | 690 | [[package]] 691 | name = "libc" 692 | version = "0.2.150" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" 695 | 696 | [[package]] 697 | name = "libm" 698 | version = "0.2.8" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" 701 | 702 | [[package]] 703 | name = "linux-raw-sys" 704 | version = "0.4.13" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 707 | 708 | [[package]] 709 | name = "lock_api" 710 | version = "0.4.11" 711 | source = "registry+https://github.com/rust-lang/crates.io-index" 712 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 713 | dependencies = [ 714 | "autocfg", 715 | "scopeguard", 716 | ] 717 | 718 | [[package]] 719 | name = "log" 720 | version = "0.4.20" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 723 | 724 | [[package]] 725 | name = "memchr" 726 | version = "2.6.4" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" 729 | 730 | [[package]] 731 | name = "memoffset" 732 | version = "0.9.0" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" 735 | dependencies = [ 736 | "autocfg", 737 | ] 738 | 739 | [[package]] 740 | name = "minimal-lexical" 741 | version = "0.2.1" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 744 | 745 | [[package]] 746 | name = "nom" 747 | version = "7.1.3" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 750 | dependencies = [ 751 | "memchr", 752 | "minimal-lexical", 753 | ] 754 | 755 | [[package]] 756 | name = "num-bigint-dig" 757 | version = "0.8.4" 758 | source = "registry+https://github.com/rust-lang/crates.io-index" 759 | checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" 760 | dependencies = [ 761 | "byteorder", 762 | "lazy_static", 763 | "libm", 764 | "num-integer", 765 | "num-iter", 766 | "num-traits", 767 | "rand", 768 | "smallvec", 769 | "zeroize", 770 | ] 771 | 772 | [[package]] 773 | name = "num-integer" 774 | version = "0.1.45" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" 777 | dependencies = [ 778 | "autocfg", 779 | "num-traits", 780 | ] 781 | 782 | [[package]] 783 | name = "num-iter" 784 | version = "0.1.43" 785 | source = "registry+https://github.com/rust-lang/crates.io-index" 786 | checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" 787 | dependencies = [ 788 | "autocfg", 789 | "num-integer", 790 | "num-traits", 791 | ] 792 | 793 | [[package]] 794 | name = "num-traits" 795 | version = "0.2.17" 796 | source = "registry+https://github.com/rust-lang/crates.io-index" 797 | checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" 798 | dependencies = [ 799 | "autocfg", 800 | "libm", 801 | ] 802 | 803 | [[package]] 804 | name = "once_cell" 805 | version = "1.19.0" 806 | source = "registry+https://github.com/rust-lang/crates.io-index" 807 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 808 | 809 | [[package]] 810 | name = "opaque-debug" 811 | version = "0.3.0" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" 814 | 815 | [[package]] 816 | name = "parking_lot" 817 | version = "0.12.1" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 820 | dependencies = [ 821 | "lock_api", 822 | "parking_lot_core", 823 | ] 824 | 825 | [[package]] 826 | name = "parking_lot_core" 827 | version = "0.9.9" 828 | source = "registry+https://github.com/rust-lang/crates.io-index" 829 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 830 | dependencies = [ 831 | "cfg-if", 832 | "libc", 833 | "redox_syscall", 834 | "smallvec", 835 | "windows-targets 0.48.5", 836 | ] 837 | 838 | [[package]] 839 | name = "pbkdf2" 840 | version = "0.12.2" 841 | source = "registry+https://github.com/rust-lang/crates.io-index" 842 | checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" 843 | dependencies = [ 844 | "digest", 845 | "hmac", 846 | ] 847 | 848 | [[package]] 849 | name = "pin-project" 850 | version = "1.1.3" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" 853 | dependencies = [ 854 | "pin-project-internal", 855 | ] 856 | 857 | [[package]] 858 | name = "pin-project-internal" 859 | version = "1.1.3" 860 | source = "registry+https://github.com/rust-lang/crates.io-index" 861 | checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" 862 | dependencies = [ 863 | "proc-macro2", 864 | "quote", 865 | "syn", 866 | ] 867 | 868 | [[package]] 869 | name = "pkcs1" 870 | version = "0.7.5" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" 873 | dependencies = [ 874 | "der", 875 | "pkcs8", 876 | "spki", 877 | ] 878 | 879 | [[package]] 880 | name = "pkcs8" 881 | version = "0.10.2" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" 884 | dependencies = [ 885 | "der", 886 | "spki", 887 | ] 888 | 889 | [[package]] 890 | name = "platforms" 891 | version = "3.3.0" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "626dec3cac7cc0e1577a2ec3fc496277ec2baa084bebad95bb6fdbfae235f84c" 894 | 895 | [[package]] 896 | name = "poly1305" 897 | version = "0.8.0" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" 900 | dependencies = [ 901 | "cpufeatures", 902 | "opaque-debug", 903 | "universal-hash", 904 | ] 905 | 906 | [[package]] 907 | name = "polyval" 908 | version = "0.6.1" 909 | source = "registry+https://github.com/rust-lang/crates.io-index" 910 | checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" 911 | dependencies = [ 912 | "cfg-if", 913 | "cpufeatures", 914 | "opaque-debug", 915 | "universal-hash", 916 | ] 917 | 918 | [[package]] 919 | name = "portable-atomic" 920 | version = "1.6.0" 921 | source = "registry+https://github.com/rust-lang/crates.io-index" 922 | checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" 923 | 924 | [[package]] 925 | name = "ppv-lite86" 926 | version = "0.2.17" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 929 | 930 | [[package]] 931 | name = "proc-macro-error-attr2" 932 | version = "2.0.0" 933 | source = "registry+https://github.com/rust-lang/crates.io-index" 934 | checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" 935 | dependencies = [ 936 | "proc-macro2", 937 | "quote", 938 | ] 939 | 940 | [[package]] 941 | name = "proc-macro-error2" 942 | version = "2.0.1" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" 945 | dependencies = [ 946 | "proc-macro-error-attr2", 947 | "proc-macro2", 948 | "quote", 949 | "syn", 950 | ] 951 | 952 | [[package]] 953 | name = "proc-macro2" 954 | version = "1.0.86" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 957 | dependencies = [ 958 | "unicode-ident", 959 | ] 960 | 961 | [[package]] 962 | name = "pulldown-cmark" 963 | version = "0.9.6" 964 | source = "registry+https://github.com/rust-lang/crates.io-index" 965 | checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" 966 | dependencies = [ 967 | "bitflags 2.5.0", 968 | "memchr", 969 | "unicase", 970 | ] 971 | 972 | [[package]] 973 | name = "pyo3" 974 | version = "0.24.2" 975 | source = "registry+https://github.com/rust-lang/crates.io-index" 976 | checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" 977 | dependencies = [ 978 | "cfg-if", 979 | "indoc", 980 | "libc", 981 | "memoffset", 982 | "once_cell", 983 | "portable-atomic", 984 | "pyo3-build-config", 985 | "pyo3-ffi", 986 | "pyo3-macros", 987 | "unindent", 988 | ] 989 | 990 | [[package]] 991 | name = "pyo3-build-config" 992 | version = "0.24.2" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" 995 | dependencies = [ 996 | "once_cell", 997 | "target-lexicon", 998 | ] 999 | 1000 | [[package]] 1001 | name = "pyo3-ffi" 1002 | version = "0.24.2" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" 1005 | dependencies = [ 1006 | "libc", 1007 | "pyo3-build-config", 1008 | ] 1009 | 1010 | [[package]] 1011 | name = "pyo3-file" 1012 | version = "0.12.0" 1013 | source = "registry+https://github.com/rust-lang/crates.io-index" 1014 | checksum = "8d97fbb05588c29774af01833e598fae7e714a3f87d36b286e0466f6b678e4a7" 1015 | dependencies = [ 1016 | "pyo3", 1017 | "skeptic", 1018 | ] 1019 | 1020 | [[package]] 1021 | name = "pyo3-macros" 1022 | version = "0.24.2" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" 1025 | dependencies = [ 1026 | "proc-macro2", 1027 | "pyo3-macros-backend", 1028 | "quote", 1029 | "syn", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "pyo3-macros-backend" 1034 | version = "0.24.2" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" 1037 | dependencies = [ 1038 | "heck", 1039 | "proc-macro2", 1040 | "pyo3-build-config", 1041 | "quote", 1042 | "syn", 1043 | ] 1044 | 1045 | [[package]] 1046 | name = "pyrage" 1047 | version = "1.2.5" 1048 | dependencies = [ 1049 | "age", 1050 | "age-core", 1051 | "pyo3", 1052 | "pyo3-file", 1053 | ] 1054 | 1055 | [[package]] 1056 | name = "quote" 1057 | version = "1.0.36" 1058 | source = "registry+https://github.com/rust-lang/crates.io-index" 1059 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 1060 | dependencies = [ 1061 | "proc-macro2", 1062 | ] 1063 | 1064 | [[package]] 1065 | name = "rand" 1066 | version = "0.8.5" 1067 | source = "registry+https://github.com/rust-lang/crates.io-index" 1068 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1069 | dependencies = [ 1070 | "libc", 1071 | "rand_chacha", 1072 | "rand_core", 1073 | ] 1074 | 1075 | [[package]] 1076 | name = "rand_chacha" 1077 | version = "0.3.1" 1078 | source = "registry+https://github.com/rust-lang/crates.io-index" 1079 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1080 | dependencies = [ 1081 | "ppv-lite86", 1082 | "rand_core", 1083 | ] 1084 | 1085 | [[package]] 1086 | name = "rand_core" 1087 | version = "0.6.4" 1088 | source = "registry+https://github.com/rust-lang/crates.io-index" 1089 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1090 | dependencies = [ 1091 | "getrandom", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "redox_syscall" 1096 | version = "0.4.1" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 1099 | dependencies = [ 1100 | "bitflags 1.3.2", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "rsa" 1105 | version = "0.9.6" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" 1108 | dependencies = [ 1109 | "const-oid", 1110 | "digest", 1111 | "num-bigint-dig", 1112 | "num-integer", 1113 | "num-traits", 1114 | "pkcs1", 1115 | "pkcs8", 1116 | "rand_core", 1117 | "signature", 1118 | "spki", 1119 | "subtle", 1120 | "zeroize", 1121 | ] 1122 | 1123 | [[package]] 1124 | name = "rust-embed" 1125 | version = "8.2.0" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "a82c0bbc10308ed323529fd3c1dce8badda635aa319a5ff0e6466f33b8101e3f" 1128 | dependencies = [ 1129 | "rust-embed-impl", 1130 | "rust-embed-utils", 1131 | "walkdir", 1132 | ] 1133 | 1134 | [[package]] 1135 | name = "rust-embed-impl" 1136 | version = "8.2.0" 1137 | source = "registry+https://github.com/rust-lang/crates.io-index" 1138 | checksum = "6227c01b1783cdfee1bcf844eb44594cd16ec71c35305bf1c9fb5aade2735e16" 1139 | dependencies = [ 1140 | "proc-macro2", 1141 | "quote", 1142 | "rust-embed-utils", 1143 | "syn", 1144 | "walkdir", 1145 | ] 1146 | 1147 | [[package]] 1148 | name = "rust-embed-utils" 1149 | version = "8.2.0" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "8cb0a25bfbb2d4b4402179c2cf030387d9990857ce08a32592c6238db9fa8665" 1152 | dependencies = [ 1153 | "sha2", 1154 | "walkdir", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "rustc-hash" 1159 | version = "1.1.0" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1162 | 1163 | [[package]] 1164 | name = "rustc_version" 1165 | version = "0.4.0" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 1168 | dependencies = [ 1169 | "semver", 1170 | ] 1171 | 1172 | [[package]] 1173 | name = "rustix" 1174 | version = "0.38.28" 1175 | source = "registry+https://github.com/rust-lang/crates.io-index" 1176 | checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" 1177 | dependencies = [ 1178 | "bitflags 2.5.0", 1179 | "errno", 1180 | "libc", 1181 | "linux-raw-sys", 1182 | "windows-sys", 1183 | ] 1184 | 1185 | [[package]] 1186 | name = "ryu" 1187 | version = "1.0.18" 1188 | source = "registry+https://github.com/rust-lang/crates.io-index" 1189 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 1190 | 1191 | [[package]] 1192 | name = "salsa20" 1193 | version = "0.10.2" 1194 | source = "registry+https://github.com/rust-lang/crates.io-index" 1195 | checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" 1196 | dependencies = [ 1197 | "cipher", 1198 | ] 1199 | 1200 | [[package]] 1201 | name = "same-file" 1202 | version = "1.0.6" 1203 | source = "registry+https://github.com/rust-lang/crates.io-index" 1204 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1205 | dependencies = [ 1206 | "winapi-util", 1207 | ] 1208 | 1209 | [[package]] 1210 | name = "scopeguard" 1211 | version = "1.2.0" 1212 | source = "registry+https://github.com/rust-lang/crates.io-index" 1213 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1214 | 1215 | [[package]] 1216 | name = "scrypt" 1217 | version = "0.11.0" 1218 | source = "registry+https://github.com/rust-lang/crates.io-index" 1219 | checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" 1220 | dependencies = [ 1221 | "pbkdf2", 1222 | "salsa20", 1223 | "sha2", 1224 | ] 1225 | 1226 | [[package]] 1227 | name = "secrecy" 1228 | version = "0.10.3" 1229 | source = "registry+https://github.com/rust-lang/crates.io-index" 1230 | checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" 1231 | dependencies = [ 1232 | "zeroize", 1233 | ] 1234 | 1235 | [[package]] 1236 | name = "self_cell" 1237 | version = "0.10.3" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d" 1240 | dependencies = [ 1241 | "self_cell 1.0.2", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "self_cell" 1246 | version = "1.0.2" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "e388332cd64eb80cd595a00941baf513caffae8dce9cfd0467fc9c66397dade6" 1249 | 1250 | [[package]] 1251 | name = "semver" 1252 | version = "1.0.21" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" 1255 | dependencies = [ 1256 | "serde", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "serde" 1261 | version = "1.0.215" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" 1264 | dependencies = [ 1265 | "serde_derive", 1266 | ] 1267 | 1268 | [[package]] 1269 | name = "serde_derive" 1270 | version = "1.0.215" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" 1273 | dependencies = [ 1274 | "proc-macro2", 1275 | "quote", 1276 | "syn", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "serde_json" 1281 | version = "1.0.137" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" 1284 | dependencies = [ 1285 | "itoa", 1286 | "memchr", 1287 | "ryu", 1288 | "serde", 1289 | ] 1290 | 1291 | [[package]] 1292 | name = "sha2" 1293 | version = "0.10.8" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 1296 | dependencies = [ 1297 | "cfg-if", 1298 | "cpufeatures", 1299 | "digest", 1300 | ] 1301 | 1302 | [[package]] 1303 | name = "signature" 1304 | version = "2.2.0" 1305 | source = "registry+https://github.com/rust-lang/crates.io-index" 1306 | checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" 1307 | dependencies = [ 1308 | "digest", 1309 | "rand_core", 1310 | ] 1311 | 1312 | [[package]] 1313 | name = "skeptic" 1314 | version = "0.13.7" 1315 | source = "registry+https://github.com/rust-lang/crates.io-index" 1316 | checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" 1317 | dependencies = [ 1318 | "bytecount", 1319 | "cargo_metadata", 1320 | "error-chain", 1321 | "glob", 1322 | "pulldown-cmark", 1323 | "tempfile", 1324 | "walkdir", 1325 | ] 1326 | 1327 | [[package]] 1328 | name = "smallvec" 1329 | version = "1.11.2" 1330 | source = "registry+https://github.com/rust-lang/crates.io-index" 1331 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" 1332 | 1333 | [[package]] 1334 | name = "spin" 1335 | version = "0.5.2" 1336 | source = "registry+https://github.com/rust-lang/crates.io-index" 1337 | checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" 1338 | 1339 | [[package]] 1340 | name = "spki" 1341 | version = "0.7.3" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" 1344 | dependencies = [ 1345 | "base64ct", 1346 | "der", 1347 | ] 1348 | 1349 | [[package]] 1350 | name = "strsim" 1351 | version = "0.11.1" 1352 | source = "registry+https://github.com/rust-lang/crates.io-index" 1353 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1354 | 1355 | [[package]] 1356 | name = "subtle" 1357 | version = "2.5.0" 1358 | source = "registry+https://github.com/rust-lang/crates.io-index" 1359 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 1360 | 1361 | [[package]] 1362 | name = "syn" 1363 | version = "2.0.87" 1364 | source = "registry+https://github.com/rust-lang/crates.io-index" 1365 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" 1366 | dependencies = [ 1367 | "proc-macro2", 1368 | "quote", 1369 | "unicode-ident", 1370 | ] 1371 | 1372 | [[package]] 1373 | name = "target-lexicon" 1374 | version = "0.13.2" 1375 | source = "registry+https://github.com/rust-lang/crates.io-index" 1376 | checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" 1377 | 1378 | [[package]] 1379 | name = "tempfile" 1380 | version = "3.9.0" 1381 | source = "registry+https://github.com/rust-lang/crates.io-index" 1382 | checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" 1383 | dependencies = [ 1384 | "cfg-if", 1385 | "fastrand", 1386 | "redox_syscall", 1387 | "rustix", 1388 | "windows-sys", 1389 | ] 1390 | 1391 | [[package]] 1392 | name = "thiserror" 1393 | version = "1.0.50" 1394 | source = "registry+https://github.com/rust-lang/crates.io-index" 1395 | checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" 1396 | dependencies = [ 1397 | "thiserror-impl", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "thiserror-impl" 1402 | version = "1.0.50" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" 1405 | dependencies = [ 1406 | "proc-macro2", 1407 | "quote", 1408 | "syn", 1409 | ] 1410 | 1411 | [[package]] 1412 | name = "tinystr" 1413 | version = "0.7.5" 1414 | source = "registry+https://github.com/rust-lang/crates.io-index" 1415 | checksum = "83c02bf3c538ab32ba913408224323915f4ef9a6d61c0e85d493f355921c0ece" 1416 | dependencies = [ 1417 | "displaydoc", 1418 | ] 1419 | 1420 | [[package]] 1421 | name = "toml" 1422 | version = "0.5.11" 1423 | source = "registry+https://github.com/rust-lang/crates.io-index" 1424 | checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" 1425 | dependencies = [ 1426 | "serde", 1427 | ] 1428 | 1429 | [[package]] 1430 | name = "type-map" 1431 | version = "0.4.0" 1432 | source = "registry+https://github.com/rust-lang/crates.io-index" 1433 | checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46" 1434 | dependencies = [ 1435 | "rustc-hash", 1436 | ] 1437 | 1438 | [[package]] 1439 | name = "typenum" 1440 | version = "1.17.0" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1443 | 1444 | [[package]] 1445 | name = "unic-langid" 1446 | version = "0.9.3" 1447 | source = "registry+https://github.com/rust-lang/crates.io-index" 1448 | checksum = "887622f8e7b723780c5e64b04dcc0c9b8f426ada7cca6790cd3ea3bf0f08037a" 1449 | dependencies = [ 1450 | "unic-langid-impl", 1451 | ] 1452 | 1453 | [[package]] 1454 | name = "unic-langid-impl" 1455 | version = "0.9.3" 1456 | source = "registry+https://github.com/rust-lang/crates.io-index" 1457 | checksum = "5adeb847e35eed4efbffd9fb2e4d078b91ece56e4d6a3c0d2df55b3a1dac07d5" 1458 | dependencies = [ 1459 | "serde", 1460 | "tinystr", 1461 | ] 1462 | 1463 | [[package]] 1464 | name = "unicase" 1465 | version = "2.8.1" 1466 | source = "registry+https://github.com/rust-lang/crates.io-index" 1467 | checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 1468 | 1469 | [[package]] 1470 | name = "unicode-ident" 1471 | version = "1.0.12" 1472 | source = "registry+https://github.com/rust-lang/crates.io-index" 1473 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1474 | 1475 | [[package]] 1476 | name = "unindent" 1477 | version = "0.2.3" 1478 | source = "registry+https://github.com/rust-lang/crates.io-index" 1479 | checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" 1480 | 1481 | [[package]] 1482 | name = "universal-hash" 1483 | version = "0.5.1" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" 1486 | dependencies = [ 1487 | "crypto-common", 1488 | "subtle", 1489 | ] 1490 | 1491 | [[package]] 1492 | name = "version_check" 1493 | version = "0.9.4" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1496 | 1497 | [[package]] 1498 | name = "walkdir" 1499 | version = "2.4.0" 1500 | source = "registry+https://github.com/rust-lang/crates.io-index" 1501 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 1502 | dependencies = [ 1503 | "same-file", 1504 | "winapi-util", 1505 | ] 1506 | 1507 | [[package]] 1508 | name = "wasi" 1509 | version = "0.11.0+wasi-snapshot-preview1" 1510 | source = "registry+https://github.com/rust-lang/crates.io-index" 1511 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1512 | 1513 | [[package]] 1514 | name = "which" 1515 | version = "4.4.2" 1516 | source = "registry+https://github.com/rust-lang/crates.io-index" 1517 | checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" 1518 | dependencies = [ 1519 | "either", 1520 | "home", 1521 | "once_cell", 1522 | "rustix", 1523 | ] 1524 | 1525 | [[package]] 1526 | name = "winapi" 1527 | version = "0.3.9" 1528 | source = "registry+https://github.com/rust-lang/crates.io-index" 1529 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1530 | dependencies = [ 1531 | "winapi-i686-pc-windows-gnu", 1532 | "winapi-x86_64-pc-windows-gnu", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "winapi-i686-pc-windows-gnu" 1537 | version = "0.4.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1540 | 1541 | [[package]] 1542 | name = "winapi-util" 1543 | version = "0.1.6" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 1546 | dependencies = [ 1547 | "winapi", 1548 | ] 1549 | 1550 | [[package]] 1551 | name = "winapi-x86_64-pc-windows-gnu" 1552 | version = "0.4.0" 1553 | source = "registry+https://github.com/rust-lang/crates.io-index" 1554 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1555 | 1556 | [[package]] 1557 | name = "windows-sys" 1558 | version = "0.52.0" 1559 | source = "registry+https://github.com/rust-lang/crates.io-index" 1560 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 1561 | dependencies = [ 1562 | "windows-targets 0.52.4", 1563 | ] 1564 | 1565 | [[package]] 1566 | name = "windows-targets" 1567 | version = "0.48.5" 1568 | source = "registry+https://github.com/rust-lang/crates.io-index" 1569 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 1570 | dependencies = [ 1571 | "windows_aarch64_gnullvm 0.48.5", 1572 | "windows_aarch64_msvc 0.48.5", 1573 | "windows_i686_gnu 0.48.5", 1574 | "windows_i686_msvc 0.48.5", 1575 | "windows_x86_64_gnu 0.48.5", 1576 | "windows_x86_64_gnullvm 0.48.5", 1577 | "windows_x86_64_msvc 0.48.5", 1578 | ] 1579 | 1580 | [[package]] 1581 | name = "windows-targets" 1582 | version = "0.52.4" 1583 | source = "registry+https://github.com/rust-lang/crates.io-index" 1584 | checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" 1585 | dependencies = [ 1586 | "windows_aarch64_gnullvm 0.52.4", 1587 | "windows_aarch64_msvc 0.52.4", 1588 | "windows_i686_gnu 0.52.4", 1589 | "windows_i686_msvc 0.52.4", 1590 | "windows_x86_64_gnu 0.52.4", 1591 | "windows_x86_64_gnullvm 0.52.4", 1592 | "windows_x86_64_msvc 0.52.4", 1593 | ] 1594 | 1595 | [[package]] 1596 | name = "windows_aarch64_gnullvm" 1597 | version = "0.48.5" 1598 | source = "registry+https://github.com/rust-lang/crates.io-index" 1599 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 1600 | 1601 | [[package]] 1602 | name = "windows_aarch64_gnullvm" 1603 | version = "0.52.4" 1604 | source = "registry+https://github.com/rust-lang/crates.io-index" 1605 | checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" 1606 | 1607 | [[package]] 1608 | name = "windows_aarch64_msvc" 1609 | version = "0.48.5" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1612 | 1613 | [[package]] 1614 | name = "windows_aarch64_msvc" 1615 | version = "0.52.4" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" 1618 | 1619 | [[package]] 1620 | name = "windows_i686_gnu" 1621 | version = "0.48.5" 1622 | source = "registry+https://github.com/rust-lang/crates.io-index" 1623 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1624 | 1625 | [[package]] 1626 | name = "windows_i686_gnu" 1627 | version = "0.52.4" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" 1630 | 1631 | [[package]] 1632 | name = "windows_i686_msvc" 1633 | version = "0.48.5" 1634 | source = "registry+https://github.com/rust-lang/crates.io-index" 1635 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1636 | 1637 | [[package]] 1638 | name = "windows_i686_msvc" 1639 | version = "0.52.4" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" 1642 | 1643 | [[package]] 1644 | name = "windows_x86_64_gnu" 1645 | version = "0.48.5" 1646 | source = "registry+https://github.com/rust-lang/crates.io-index" 1647 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1648 | 1649 | [[package]] 1650 | name = "windows_x86_64_gnu" 1651 | version = "0.52.4" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" 1654 | 1655 | [[package]] 1656 | name = "windows_x86_64_gnullvm" 1657 | version = "0.48.5" 1658 | source = "registry+https://github.com/rust-lang/crates.io-index" 1659 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1660 | 1661 | [[package]] 1662 | name = "windows_x86_64_gnullvm" 1663 | version = "0.52.4" 1664 | source = "registry+https://github.com/rust-lang/crates.io-index" 1665 | checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" 1666 | 1667 | [[package]] 1668 | name = "windows_x86_64_msvc" 1669 | version = "0.48.5" 1670 | source = "registry+https://github.com/rust-lang/crates.io-index" 1671 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1672 | 1673 | [[package]] 1674 | name = "windows_x86_64_msvc" 1675 | version = "0.52.4" 1676 | source = "registry+https://github.com/rust-lang/crates.io-index" 1677 | checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" 1678 | 1679 | [[package]] 1680 | name = "wsl" 1681 | version = "0.1.0" 1682 | source = "registry+https://github.com/rust-lang/crates.io-index" 1683 | checksum = "f8dab7ac864710bdea6594becbea5b5050333cf34fefb0dc319567eb347950d4" 1684 | 1685 | [[package]] 1686 | name = "x25519-dalek" 1687 | version = "2.0.0" 1688 | source = "registry+https://github.com/rust-lang/crates.io-index" 1689 | checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" 1690 | dependencies = [ 1691 | "curve25519-dalek", 1692 | "rand_core", 1693 | "serde", 1694 | "zeroize", 1695 | ] 1696 | 1697 | [[package]] 1698 | name = "zeroize" 1699 | version = "1.7.0" 1700 | source = "registry+https://github.com/rust-lang/crates.io-index" 1701 | checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" 1702 | dependencies = [ 1703 | "zeroize_derive", 1704 | ] 1705 | 1706 | [[package]] 1707 | name = "zeroize_derive" 1708 | version = "1.4.2" 1709 | source = "registry+https://github.com/rust-lang/crates.io-index" 1710 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 1711 | dependencies = [ 1712 | "proc-macro2", 1713 | "quote", 1714 | "syn", 1715 | ] 1716 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyrage" 3 | version = "1.2.5" 4 | authors = ["William Woodruff "] 5 | edition = "2021" 6 | description = "Python bindings for rage (age in Rust)" 7 | homepage = "https://github.com/woodruffw/pyrage" 8 | repository = "https://github.com/woodruffw/pyrage" 9 | readme = "README.md" 10 | license = "MIT" 11 | 12 | [package.metadata.release] 13 | publish = false # handled by GitHub Actions 14 | push = true 15 | 16 | [lib] 17 | crate-type = ["cdylib"] 18 | 19 | [dependencies] 20 | age-core = "0.11" 21 | age = { version = "0.11.1", features = ["ssh", "plugin"] } 22 | pyo3 = { version = "0.24.2", features = [ 23 | "extension-module", 24 | "abi3", 25 | "abi3-py39", 26 | "py-clone", 27 | ] } 28 | pyo3-file = "0.12.0" 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 William Woodruff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VENV = env 2 | VENV_BIN = $(VENV)/bin 3 | 4 | ifeq ($(OS), Windows_NT) 5 | VENV_BIN=$(VENV)/Scripts 6 | endif 7 | 8 | 9 | .PHONY: all 10 | all: 11 | @echo "Run my targets individually!" 12 | 13 | .PHONY: env 14 | env: env/pyvenv.cfg 15 | 16 | $(VENV)/pyvenv.cfg: dev-requirements.txt 17 | python -m venv $(VENV) 18 | $(VENV_BIN)/python -m pip install --upgrade pip 19 | $(VENV_BIN)/python -m pip install --requirement dev-requirements.txt 20 | 21 | .PHONY: develop 22 | develop: env 23 | . $(VENV_BIN)/activate && maturin develop --extras=dev 24 | $(VENV_BIN)/python -m pip install --editable ./pyrage-stubs 25 | 26 | .PHONY: test 27 | test: develop 28 | $(VENV_BIN)/python -m unittest 29 | 30 | .PHONY: dist 31 | dist: dist-pyrage dist-pyrage-stubs 32 | 33 | .PHONY: dist-pyrage 34 | dist-pyrage: env 35 | docker run --rm -v $(shell pwd):/io ghcr.io/pyo3/maturin build --release --sdist --strip --out dist 36 | 37 | .PHONY: dist-pyrage-stubs 38 | dist-pyrage-stubs: env 39 | $(VENV_BIN)/python -m build ./pyrage-stubs --outdir dist 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pyrage 2 | ====== 3 | 4 | [![CI](https://github.com/woodruffw/pyrage/actions/workflows/ci.yml/badge.svg)](https://github.com/woodruffw/pyrage/actions/workflows/ci.yml) 5 | [![PyPI version](https://badge.fury.io/py/pyrage.svg)](https://badge.fury.io/py/pyrage) 6 | [![PyPI Downloads](https://static.pepy.tech/badge/pyrage)](https://pepy.tech/projects/pyrage) 7 | 8 | Python bindings for the [Rust implementation of `age`](https://github.com/str4d/rage). 9 | 10 | ## Index 11 | 12 | * [Installation](#installation) 13 | * [Usage](#usage) 14 | * [Development](#development) 15 | * [Licensing](#licensing) 16 | 17 | ## Installation 18 | 19 | You can install `pyrage` with `pip`: 20 | 21 | ```console 22 | $ python -m pip install pyrage 23 | ``` 24 | 25 | [PEP 561](https://peps.python.org/pep-0561/)-style type stubs are also available: 26 | 27 | ```console 28 | $ python -m pip install pyrage-stubs 29 | ``` 30 | 31 | See the [development instructions](#development) below for manual installations. 32 | 33 | ## Usage 34 | 35 | ### Identity generation (x25519 only) 36 | 37 | ```python 38 | from pyrage import x25519 39 | 40 | ident = x25519.Identity.generate() 41 | 42 | # returns the public key 43 | ident.to_public() 44 | 45 | # returns the private key 46 | str(ident) 47 | ``` 48 | 49 | ### Identity-based encryption and decryption 50 | 51 | ```python 52 | from pyrage import encrypt, decrypt, ssh, x25519 53 | 54 | # load some identities 55 | alice = x25519.Identity.from_str("AGE-SECRET-KEY-...") 56 | bob = ssh.Identity.from_buffer(b"---BEGIN OPENSSH PRIVATE KEY----...") 57 | 58 | # load some recipients 59 | carol = x25519.Recipient.from_str("age1z...") 60 | dave = ssh.Recipient.from_str("ssh-ed25519 ...") 61 | 62 | # encryption 63 | encrypted = encrypt(b"bob can't be trusted", [carol, dave, alice.to_public()]) 64 | 65 | # decryption 66 | decrypted = decrypt(encrypted, [alice, bob]) 67 | ``` 68 | 69 | ### Passphrase encryption and decryption 70 | 71 | ```python 72 | from pyrage import passphrase 73 | 74 | encrypted = passphrase.encrypt(b"something secret", "my extremely secure password") 75 | decrypted = passphrase.decrypt(encrypted, "my extremely secure password") 76 | ``` 77 | 78 | ## Development 79 | 80 | ```console 81 | $ source env/bin/activate 82 | $ make develop 83 | ``` 84 | 85 | ## Licensing 86 | 87 | `pyrage` is released and distributed under the terms of the [MIT License](./LICENSE). 88 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | build 2 | wheel 3 | twine 4 | maturin 5 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["maturin>=1.0,<2.0"] 3 | build-backend = "maturin" 4 | 5 | [project] 6 | name = "pyrage" 7 | classifiers = [ 8 | "Programming Language :: Rust", 9 | "Operating System :: POSIX :: Linux", 10 | ] 11 | requires-python = ">=3.9" 12 | dynamic = ["version"] 13 | -------------------------------------------------------------------------------- /pyrage-stubs/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 William Woodruff 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyrage-stubs/README.md: -------------------------------------------------------------------------------- 1 | pyrage-stubs 2 | ============ 3 | 4 | This is a [PEP 561](https://peps.python.org/pep-0561/)-compatible stubs 5 | package for [`pyrage`](https://github.com/woodruffw/pyrage). 6 | 7 | -------------------------------------------------------------------------------- /pyrage-stubs/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "pyrage-stubs" 7 | version = "1.2.4" 8 | description = "A PEP 561 stub package for pyrage's types" 9 | readme = "README.md" 10 | license = { file = "LICENSE" } 11 | authors = [{ name = "William Woodruff", email = "william@yossarian.net" }] 12 | classifiers = [ 13 | "Intended Audience :: Developers", 14 | "License :: OSI Approved :: MIT License", 15 | "Topic :: Security :: Cryptography", 16 | "Typing :: Stubs Only", 17 | ] 18 | dependencies = ["pyrage>=1.2.0,<2.0"] 19 | requires-python = ">= 3.8" 20 | 21 | [project.urls] 22 | Homepage = "https://pypi.org/project/pyrage/" 23 | Issues = "https://github.com/woodruffw/pyrage/issues" 24 | Source = "https://github.com/woodruffw/pyrage/tree/main/pyrage-stubs" 25 | 26 | [tool.setuptools.package-data] 27 | "*" = ["*.pyi"] 28 | -------------------------------------------------------------------------------- /pyrage-stubs/pyrage/__init__.pyi: -------------------------------------------------------------------------------- 1 | from io import BufferedIOBase 2 | from typing import Sequence, Union 3 | 4 | from pyrage import passphrase, plugin, ssh, x25519 5 | from pyrage.plugin import IdentityPluginV1, RecipientPluginV1 6 | from pyrage.ssh import Identity as SSHIdentity 7 | from pyrage.ssh import Recipient as SSHRecipient 8 | from pyrage.x25519 import Identity as X25519Identity 9 | from pyrage.x25519 import Recipient as X25519Recipient 10 | 11 | _Identity = Union[SSHIdentity, X25519Identity, IdentityPluginV1] 12 | _Recipient = Union[SSHRecipient, X25519Recipient, RecipientPluginV1] 13 | 14 | __all__ = ( 15 | "ssh", 16 | "x25519", 17 | "passphrase", 18 | "plugin", 19 | "encrypt", 20 | "encrypt_file", 21 | "encrypt_io", 22 | "decrypt", 23 | "decrypt_file", 24 | "decrypt_io", 25 | "RecipientError", 26 | "IdentityError", 27 | "EncryptError", 28 | "DecryptError", 29 | ) 30 | 31 | 32 | class RecipientError(Exception): 33 | ... 34 | 35 | class IdentityError(Exception): 36 | ... 37 | 38 | class EncryptError(Exception): 39 | ... 40 | 41 | class DecryptError(Exception): 42 | ... 43 | 44 | 45 | def encrypt(plaintext: bytes, recipients: Sequence[_Recipient]) -> bytes: ... 46 | def encrypt_file(infile: str, outfile: str, recipients: Sequence[_Recipient]) -> None: ... 47 | def encrypt_io(in_io: BufferedIOBase, out_io: BufferedIOBase, recipients: Sequence[_Recipient]) -> bytes: ... 48 | 49 | def decrypt(ciphertext: bytes, identities: Sequence[_Identity]) -> bytes: ... 50 | def decrypt_file(infile: str, outfile: str, identities: Sequence[_Identity]) -> None: ... 51 | def decrypt_io(in_io: BufferedIOBase, out_io: BufferedIOBase, identities: Sequence[_Identity]) -> None: ... 52 | -------------------------------------------------------------------------------- /pyrage-stubs/pyrage/passphrase.pyi: -------------------------------------------------------------------------------- 1 | def encrypt(plaintext: bytes, passphrase: str) -> bytes: 2 | ... 3 | 4 | 5 | def decrypt(ciphertext: bytes, passphrase: str) -> bytes: 6 | ... 7 | -------------------------------------------------------------------------------- /pyrage-stubs/pyrage/plugin.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import Sequence, Self, Optional, Protocol 3 | 4 | 5 | class Callbacks(Protocol): 6 | def display_message(self, message: str) -> None: 7 | ... 8 | 9 | def confirm(self, message: str, yes_string: str, no_string: Optional[str]) -> Optional[bool]: 10 | ... 11 | 12 | def request_public_string(self, description: str) -> Optional[str]: 13 | ... 14 | 15 | def request_passphrase(self, description: str) -> Optional[str]: 16 | ... 17 | 18 | 19 | class Recipient: 20 | @classmethod 21 | def from_str(cls, v: str) -> Recipient: 22 | ... 23 | 24 | def plugin(self) -> str: 25 | ... 26 | 27 | 28 | class RecipientPluginV1: 29 | def __new__(cls, plugin_name: str, recipients: Sequence[Recipient], identities: Sequence[Identity], callbacks: Callbacks) -> Self: 30 | ... 31 | 32 | 33 | class Identity: 34 | @classmethod 35 | def from_str(cls, v: str) -> Identity: 36 | ... 37 | 38 | @classmethod 39 | def default_for_plugin(cls, plugin: str) -> Identity: 40 | ... 41 | 42 | def plugin(self) -> str: 43 | ... 44 | 45 | 46 | class IdentityPluginV1: 47 | def __new__(cls, plugin_name: str, identities: Sequence[Identity], callbacks: Callbacks) -> Self: 48 | ... 49 | -------------------------------------------------------------------------------- /pyrage-stubs/pyrage/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woodruffw/pyrage/16e97e81626a57232017f2a26b2fd4f0a637edda/pyrage-stubs/pyrage/py.typed -------------------------------------------------------------------------------- /pyrage-stubs/pyrage/ssh.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class Identity: 5 | @classmethod 6 | def from_buffer(cls, buf: bytes) -> Identity: 7 | ... 8 | 9 | 10 | class Recipient: 11 | @classmethod 12 | def from_str(cls, v: str) -> Recipient: 13 | ... 14 | -------------------------------------------------------------------------------- /pyrage-stubs/pyrage/x25519.pyi: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | class Identity: 5 | @classmethod 6 | def generate(cls) -> Identity: 7 | ... 8 | 9 | @classmethod 10 | def from_str(cls, v: str) -> Identity: 11 | ... 12 | 13 | def to_public(self) -> Recipient: 14 | ... 15 | 16 | 17 | class Recipient: 18 | @classmethod 19 | def from_str(cls, v: str) -> Recipient: 20 | ... 21 | -------------------------------------------------------------------------------- /pyrage-stubs/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | # editable installs don't work with pyproject.toml + setuptools yet, 4 | # so we need this stub. 5 | 6 | setup() 7 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unsafe_code)] 2 | 3 | use std::collections::HashSet; 4 | use std::io::Write; 5 | use std::{fs::File, io::Read}; 6 | 7 | use age::{ 8 | DecryptError as RageDecryptError, EncryptError as RageEncryptError, Encryptor, Identity, 9 | Recipient, 10 | }; 11 | use age_core::format::{FileKey, Stanza}; 12 | use pyo3::{ 13 | create_exception, 14 | exceptions::{PyException, PyTypeError}, 15 | prelude::*, 16 | py_run, 17 | types::PyBytes, 18 | }; 19 | use pyo3_file::PyFileLikeObject; 20 | 21 | mod passphrase; 22 | mod plugin; 23 | mod ssh; 24 | mod x25519; 25 | 26 | // These exceptions are raised by the `pyrage.ssh` and `pyrage.x25519` APIs, 27 | // where appropriate. 28 | create_exception!(pyrage, RecipientError, PyException); 29 | create_exception!(pyrage, IdentityError, PyException); 30 | 31 | // This is a wrapper trait for age's `Recipient`, providing trait downcasting. 32 | // 33 | // We need this so that we can pass multiple different types of recipients 34 | // into the Python-level `encrypt` API. 35 | trait PyrageRecipient: Recipient { 36 | fn as_recipient(self: Box) -> Box; 37 | } 38 | 39 | // This is a wrapper trait for age's `Identity`, providing trait downcasting. 40 | // 41 | // We need this so that we can pass multiple different types of identities 42 | // into the Python-level `decrypt` API. 43 | trait PyrageIdentity: Identity { 44 | fn as_identity(&self) -> &dyn Identity; 45 | } 46 | 47 | // This macro generates two trait impls for each passed in type: 48 | // 49 | // * An age `Receipient` impl, using the underlying trait impl. 50 | // * A `PyrageRecipient` impl, by consuming the instance and downcasting. 51 | macro_rules! recipient_traits { 52 | ($($t:ty),+) => { 53 | $( 54 | impl Recipient for $t { 55 | fn wrap_file_key(&self, file_key: &FileKey) -> Result<(Vec, HashSet), RageEncryptError> { 56 | self.0.wrap_file_key(file_key) 57 | } 58 | } 59 | 60 | impl PyrageRecipient for $t { 61 | fn as_recipient(self: Box) -> Box { 62 | self as Box 63 | } 64 | } 65 | )* 66 | } 67 | } 68 | 69 | recipient_traits!(ssh::Recipient, x25519::Recipient, plugin::RecipientPluginV1); 70 | 71 | // This macro generates two trait impls for each passed in type: 72 | // 73 | // * An age `Identity` impl, using the underlying trait impl. 74 | // * A `PyrageIdentity` impl, by borrowing the instance and downcasting. 75 | macro_rules! identity_traits { 76 | ($($t:ty),+) => { 77 | $( 78 | impl Identity for $t { 79 | fn unwrap_stanza(&self, stanza: &Stanza) -> Option> { 80 | self.0.unwrap_stanza(stanza) 81 | } 82 | } 83 | 84 | impl PyrageIdentity for $t { 85 | fn as_identity(&self) -> &dyn Identity { 86 | self as &dyn Identity 87 | } 88 | } 89 | )* 90 | } 91 | } 92 | 93 | identity_traits!(ssh::Identity, x25519::Identity, plugin::IdentityPluginV1); 94 | 95 | // This is where the magic happens, and why we need to do the trait dance 96 | // above: `FromPyObject` is a third-party trait, so we need to implement it 97 | // for `Box` instead of `Box`. 98 | // 99 | // The implementation itself is straightforward: we try to turn the 100 | // `PyAny` into each concrete recipient type, which we then perform the trait 101 | // cast on. 102 | impl<'source> FromPyObject<'source> for Box { 103 | fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult { 104 | if let Ok(recipient) = ob.extract::() { 105 | Ok(Box::new(recipient) as Box) 106 | } else if let Ok(recipient) = ob.extract::() { 107 | Ok(Box::new(recipient) as Box) 108 | } else if let Ok(recipient) = ob.extract::() { 109 | Ok(Box::new(recipient) as Box) 110 | } else { 111 | Err(PyTypeError::new_err( 112 | "invalid type (expected a recipient type)", 113 | )) 114 | } 115 | } 116 | } 117 | 118 | // Similar to the above: we try to turn the `PyAny` into a concrete identity type, 119 | // which we then perform the trait cast on. 120 | impl<'source> FromPyObject<'source> for Box { 121 | fn extract_bound(ob: &Bound<'source, PyAny>) -> PyResult { 122 | if let Ok(identity) = ob.extract::() { 123 | Ok(Box::new(identity) as Box) 124 | } else if let Ok(identity) = ob.extract::() { 125 | Ok(Box::new(identity) as Box) 126 | } else if let Ok(identity) = ob.extract::() { 127 | Ok(Box::new(identity) as Box) 128 | } else { 129 | Err(PyTypeError::new_err( 130 | "invalid type (expected an identity type)", 131 | )) 132 | } 133 | } 134 | } 135 | 136 | create_exception!(pyrage, EncryptError, PyException); 137 | 138 | #[pyfunction] 139 | fn encrypt<'p>( 140 | py: Python<'p>, 141 | plaintext: &[u8], 142 | recipients: Vec>, 143 | ) -> PyResult> { 144 | // This turns each `dyn PyrageRecipient` into a `dyn Recipient`, which 145 | // is what the underlying `age` API expects. 146 | let recipients = recipients 147 | .into_iter() 148 | .map(|pr| pr.as_recipient()) 149 | .collect::>(); 150 | 151 | let encryptor = Encryptor::with_recipients(recipients.iter().map(|r| r.as_ref())) 152 | .map_err(|_| EncryptError::new_err("expected at least one recipient"))?; 153 | let mut encrypted = vec![]; 154 | let mut writer = encryptor 155 | .wrap_output(&mut encrypted) 156 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 157 | writer 158 | .write_all(plaintext) 159 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 160 | writer 161 | .finish() 162 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 163 | 164 | // TODO: Avoid this copy. Maybe PyBytes::new_with? 165 | Ok(PyBytes::new(py, &encrypted)) 166 | } 167 | 168 | #[pyfunction] 169 | fn encrypt_file( 170 | infile: String, 171 | outfile: String, 172 | recipients: Vec>, 173 | ) -> PyResult<()> { 174 | // This turns each `dyn PyrageRecipient` into a `dyn Recipient`, which 175 | // is what the underlying `age` API expects. 176 | let recipients = recipients 177 | .into_iter() 178 | .map(|pr| pr.as_recipient()) 179 | .collect::>(); 180 | 181 | let reader = File::open(infile)?; 182 | let writer = File::create(outfile)?; 183 | 184 | let mut reader = std::io::BufReader::new(reader); 185 | let mut writer = std::io::BufWriter::new(writer); 186 | 187 | let encryptor = Encryptor::with_recipients(recipients.iter().map(|r| r.as_ref())) 188 | .map_err(|_| EncryptError::new_err("expected at least one recipient"))?; 189 | let mut writer = encryptor 190 | .wrap_output(&mut writer) 191 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 192 | 193 | std::io::copy(&mut reader, &mut writer).map_err(|e| EncryptError::new_err(e.to_string()))?; 194 | 195 | writer 196 | .finish() 197 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 198 | 199 | Ok(()) 200 | } 201 | 202 | create_exception!(pyrage, DecryptError, PyException); 203 | 204 | #[pyfunction] 205 | fn decrypt<'p>( 206 | py: Python<'p>, 207 | ciphertext: &[u8], 208 | identities: Vec>, 209 | ) -> PyResult> { 210 | let identities = identities.iter().map(|pi| pi.as_ref().as_identity()); 211 | 212 | let decryptor = 213 | age::Decryptor::new(ciphertext).map_err(|e| DecryptError::new_err(e.to_string()))?; 214 | 215 | let mut decrypted = vec![]; 216 | let mut reader = decryptor 217 | .decrypt(identities) 218 | .map_err(|e| DecryptError::new_err(e.to_string()))?; 219 | reader 220 | .read_to_end(&mut decrypted) 221 | .map_err(|e| DecryptError::new_err(e.to_string()))?; 222 | 223 | // TODO: Avoid this copy. Maybe PyBytes::new_with? 224 | Ok(PyBytes::new(py, &decrypted)) 225 | } 226 | 227 | #[pyfunction] 228 | fn decrypt_file( 229 | infile: String, 230 | outfile: String, 231 | identities: Vec>, 232 | ) -> PyResult<()> { 233 | let identities = identities.iter().map(|pi| pi.as_ref().as_identity()); 234 | 235 | let reader = File::open(infile)?; 236 | let writer = File::create(outfile)?; 237 | 238 | let reader = std::io::BufReader::new(reader); 239 | let mut writer = std::io::BufWriter::new(writer); 240 | 241 | let decryptor = 242 | age::Decryptor::new_buffered(reader).map_err(|e| DecryptError::new_err(e.to_string()))?; 243 | 244 | let mut reader = decryptor 245 | .decrypt(identities) 246 | .map_err(|e| DecryptError::new_err(e.to_string()))?; 247 | 248 | std::io::copy(&mut reader, &mut writer)?; 249 | 250 | Ok(()) 251 | } 252 | 253 | fn from_pyobject(file: PyObject, read_only: bool) -> PyResult { 254 | // is a file-like 255 | PyFileLikeObject::with_requirements(file, read_only, !read_only, false, false) 256 | } 257 | 258 | #[pyfunction] 259 | fn encrypt_io( 260 | reader: PyObject, 261 | writer: PyObject, 262 | recipients: Vec>, 263 | ) -> PyResult<()> { 264 | // This turns each `dyn PyrageRecipient` into a `dyn Recipient`, which 265 | // is what the underlying `age` API expects. 266 | let recipients = recipients 267 | .into_iter() 268 | .map(|pr| pr.as_recipient()) 269 | .collect::>(); 270 | let reader = from_pyobject(reader, true)?; 271 | let writer = from_pyobject(writer, false)?; 272 | let mut reader = std::io::BufReader::new(reader); 273 | let mut writer = std::io::BufWriter::new(writer); 274 | let encryptor = Encryptor::with_recipients(recipients.iter().map(|r| r.as_ref())) 275 | .map_err(|_| EncryptError::new_err("expected at least one recipient"))?; 276 | let mut writer = encryptor 277 | .wrap_output(&mut writer) 278 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 279 | std::io::copy(&mut reader, &mut writer).map_err(|e| EncryptError::new_err(e.to_string()))?; 280 | writer 281 | .finish() 282 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 283 | Ok(()) 284 | } 285 | 286 | #[pyfunction] 287 | fn decrypt_io( 288 | reader: PyObject, 289 | writer: PyObject, 290 | identities: Vec>, 291 | ) -> PyResult<()> { 292 | let identities = identities.iter().map(|pi| pi.as_ref().as_identity()); 293 | let reader = from_pyobject(reader, true)?; 294 | let writer = from_pyobject(writer, false)?; 295 | let reader = std::io::BufReader::new(reader); 296 | let mut writer = std::io::BufWriter::new(writer); 297 | let decryptor = 298 | age::Decryptor::new_buffered(reader).map_err(|e| DecryptError::new_err(e.to_string()))?; 299 | let mut reader = decryptor 300 | .decrypt(identities) 301 | .map_err(|e| DecryptError::new_err(e.to_string()))?; 302 | std::io::copy(&mut reader, &mut writer)?; 303 | Ok(()) 304 | } 305 | 306 | #[pymodule] 307 | fn pyrage(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 308 | // HACK(ww): pyO3 modules are not packages, so we need this nasty 309 | // `py_run!` hack to support `from pyrage import ...` and similar 310 | // import patterns. 311 | let x25519 = x25519::module(py)?; 312 | py_run!( 313 | py, 314 | x25519, 315 | "import sys; sys.modules['pyrage.x25519'] = x25519" 316 | ); 317 | m.add_submodule(&x25519)?; 318 | 319 | let ssh = ssh::module(py)?; 320 | py_run!(py, ssh, "import sys; sys.modules['pyrage.ssh'] = ssh"); 321 | m.add_submodule(&ssh)?; 322 | 323 | let passphrase = passphrase::module(py)?; 324 | py_run!( 325 | py, 326 | passphrase, 327 | "import sys; sys.modules['pyrage.passphrase'] = passphrase" 328 | ); 329 | m.add_submodule(&passphrase)?; 330 | 331 | let plugin = plugin::module(py)?; 332 | py_run!( 333 | py, 334 | plugin, 335 | "import sys; sys.modules['pyrage.plugin'] = plugin" 336 | ); 337 | m.add_submodule(&plugin)?; 338 | 339 | m.add("IdentityError", py.get_type::())?; 340 | m.add("RecipientError", py.get_type::())?; 341 | 342 | m.add("EncryptError", py.get_type::())?; 343 | m.add_wrapped(wrap_pyfunction!(encrypt))?; 344 | m.add_wrapped(wrap_pyfunction!(encrypt_file))?; 345 | m.add_wrapped(wrap_pyfunction!(encrypt_io))?; 346 | m.add("DecryptError", py.get_type::())?; 347 | m.add_wrapped(wrap_pyfunction!(decrypt))?; 348 | m.add_wrapped(wrap_pyfunction!(decrypt_file))?; 349 | m.add_wrapped(wrap_pyfunction!(decrypt_io))?; 350 | 351 | Ok(()) 352 | } 353 | -------------------------------------------------------------------------------- /src/passphrase.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | iter, 4 | }; 5 | 6 | use age::{scrypt, Decryptor, Encryptor}; 7 | use pyo3::{prelude::*, types::PyBytes}; 8 | 9 | use crate::{DecryptError, EncryptError}; 10 | 11 | #[pyfunction] 12 | fn encrypt<'p>(py: Python<'p>, plaintext: &[u8], passphrase: &str) -> PyResult> { 13 | let encryptor = Encryptor::with_user_passphrase(passphrase.into()); 14 | let mut encrypted = vec![]; 15 | let mut writer = encryptor 16 | .wrap_output(&mut encrypted) 17 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 18 | writer 19 | .write_all(plaintext) 20 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 21 | writer 22 | .finish() 23 | .map_err(|e| EncryptError::new_err(e.to_string()))?; 24 | 25 | Ok(PyBytes::new(py, &encrypted)) 26 | } 27 | 28 | #[pyfunction] 29 | fn decrypt<'p>( 30 | py: Python<'p>, 31 | ciphertext: &[u8], 32 | passphrase: &str, 33 | ) -> PyResult> { 34 | let decryptor = Decryptor::new(ciphertext).map_err(|e| DecryptError::new_err(e.to_string()))?; 35 | let mut decrypted = vec![]; 36 | let mut reader = decryptor 37 | .decrypt(iter::once(&scrypt::Identity::new(passphrase.into()) as _)) 38 | .map_err(|e| DecryptError::new_err(e.to_string()))?; 39 | reader 40 | .read_to_end(&mut decrypted) 41 | .map_err(|e| DecryptError::new_err(e.to_string()))?; 42 | 43 | Ok(PyBytes::new(py, &decrypted)) 44 | } 45 | 46 | pub(crate) fn module(py: Python) -> PyResult> { 47 | let module = PyModule::new(py, "passphrase")?; 48 | 49 | module.add_wrapped(wrap_pyfunction!(encrypt))?; 50 | module.add_wrapped(wrap_pyfunction!(decrypt))?; 51 | 52 | Ok(module) 53 | } 54 | -------------------------------------------------------------------------------- /src/plugin.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use std::sync::Arc; 3 | 4 | use pyo3::{prelude::*, types::PyType}; 5 | 6 | use crate::{DecryptError, EncryptError, IdentityError, RecipientError}; 7 | 8 | /// Hack, because the orphan rule would prevent us from deriving a 9 | /// foreign trait on a foreign object. Instead, define a newtype. 10 | /// 11 | /// Inner type is PyAny, because we do duck-typing at runtime, and 12 | /// declaring a protocol in the type stubs. 13 | #[derive(Clone)] 14 | pub(crate) struct PyCallbacks(Py); 15 | 16 | impl PyCallbacks { 17 | fn new(inner: Bound<'_, PyAny>) -> PyResult { 18 | Ok(Self(inner.unbind())) 19 | } 20 | } 21 | 22 | // Since we have no way to pass errors from these callbacks, we might 23 | // as well panic. 24 | // 25 | // These callbacks don't look like they're supposed to fail anyway. 26 | impl age::Callbacks for PyCallbacks { 27 | fn display_message(&self, message: &str) { 28 | Python::with_gil(|py| { 29 | self.0 30 | .call_method1(py, pyo3::intern!(py, "display_message"), (message,)) 31 | .expect("`display_message` callback error") 32 | }); 33 | } 34 | fn confirm(&self, message: &str, yes_string: &str, no_string: Option<&str>) -> Option { 35 | Python::with_gil(|py| { 36 | self.0 37 | .call_method1( 38 | py, 39 | pyo3::intern!(py, "confirm"), 40 | (message, yes_string, no_string), 41 | ) 42 | .expect("`confirm` callback error") 43 | .extract::>(py) 44 | }) 45 | .expect("type error in `confirm` callback") 46 | } 47 | fn request_public_string(&self, description: &str) -> Option { 48 | Python::with_gil(|py| { 49 | self.0 50 | .call_method1( 51 | py, 52 | pyo3::intern!(py, "request_public_string"), 53 | (description,), 54 | ) 55 | .expect("`request_public_string` callback error") 56 | .extract::>(py) 57 | }) 58 | .expect("type error in `request_public_string` callback") 59 | } 60 | fn request_passphrase(&self, description: &str) -> Option { 61 | Python::with_gil(|py| { 62 | self.0 63 | .call_method1(py, pyo3::intern!(py, "request_passphrase"), (description,)) 64 | .expect("`request_passphrase` callback error") 65 | .extract::>(py) 66 | }) 67 | .expect("type error in `request_passphrase` callback") 68 | .map(age::secrecy::SecretString::from) 69 | } 70 | } 71 | 72 | #[pyclass(module = "pyrage.plugin")] 73 | #[derive(Clone)] 74 | pub(crate) struct Recipient(pub(crate) age::plugin::Recipient); 75 | 76 | #[pymethods] 77 | impl Recipient { 78 | #[classmethod] 79 | fn from_str(_cls: &Bound<'_, PyType>, v: &str) -> PyResult { 80 | age::plugin::Recipient::from_str(v) 81 | .map(Self) 82 | .map_err(RecipientError::new_err) 83 | } 84 | 85 | fn plugin(&self) -> String { 86 | self.0.plugin().to_owned() 87 | } 88 | 89 | fn __str__(&self) -> String { 90 | self.0.to_string() 91 | } 92 | } 93 | 94 | #[pyclass(module = "pyrage.plugin")] 95 | #[derive(Clone)] 96 | pub(crate) struct Identity(pub(crate) age::plugin::Identity); 97 | 98 | #[pymethods] 99 | impl Identity { 100 | #[classmethod] 101 | fn from_str(_cls: &Bound<'_, PyType>, v: &str) -> PyResult { 102 | age::plugin::Identity::from_str(v) 103 | .map(Self) 104 | .map_err(|e| IdentityError::new_err(e.to_string())) 105 | } 106 | 107 | #[classmethod] 108 | fn default_for_plugin(_cls: &Bound<'_, PyType>, plugin: &str) -> Self { 109 | Self(age::plugin::Identity::default_for_plugin(plugin)) 110 | } 111 | 112 | fn plugin(&self) -> String { 113 | self.0.plugin().to_owned() 114 | } 115 | 116 | fn __str__(&self) -> String { 117 | self.0.to_string() 118 | } 119 | } 120 | 121 | #[pyclass(module = "pyrage.plugin")] 122 | #[derive(Clone)] 123 | pub(crate) struct RecipientPluginV1(pub(crate) Arc>); 124 | 125 | #[pymethods] 126 | impl RecipientPluginV1 { 127 | #[new] 128 | #[pyo3( 129 | text_signature = "(plugin_name: str, recipients: typing.Sequence[Recipient], identities: typing.Sequence[Identity], callbacks: Callbacks)" 130 | )] 131 | fn new( 132 | _py: Python<'_>, 133 | plugin_name: &str, 134 | recipients: Vec, 135 | identities: Vec, 136 | callbacks: Bound<'_, PyAny>, 137 | ) -> PyResult { 138 | age::plugin::RecipientPluginV1::new( 139 | plugin_name, 140 | recipients 141 | .into_iter() 142 | .map(|i| i.0) 143 | .collect::>() 144 | .as_slice(), 145 | identities 146 | .into_iter() 147 | .map(|i| i.0) 148 | .collect::>() 149 | .as_slice(), 150 | PyCallbacks::new(callbacks)?, 151 | ) 152 | .map(Arc::new) 153 | .map(Self) 154 | .map_err(|err| EncryptError::new_err(err.to_string())) 155 | } 156 | } 157 | 158 | #[pyclass(module = "pyrage.plugin")] 159 | #[derive(Clone)] 160 | pub(crate) struct IdentityPluginV1(pub(crate) Arc>); 161 | 162 | #[pymethods] 163 | impl IdentityPluginV1 { 164 | #[new] 165 | #[pyo3( 166 | text_signature = "(plugin_name: str, identities: typing.Sequence[Identity], callbacks: Callbacks)" 167 | )] 168 | fn new( 169 | _py: Python<'_>, 170 | plugin_name: &str, 171 | identities: Vec, 172 | callbacks: Bound<'_, PyAny>, 173 | ) -> PyResult { 174 | age::plugin::IdentityPluginV1::new( 175 | plugin_name, 176 | identities 177 | .into_iter() 178 | .map(|i| i.0) 179 | .collect::>() 180 | .as_slice(), 181 | PyCallbacks::new(callbacks)?, 182 | ) 183 | .map(Arc::new) 184 | .map(Self) 185 | .map_err(|err| DecryptError::new_err(err.to_string())) 186 | } 187 | } 188 | 189 | pub(crate) fn module(py: Python<'_>) -> PyResult> { 190 | let module = PyModule::new(py, "plugin")?; 191 | 192 | module.add_class::()?; 193 | module.add_class::()?; 194 | module.add_class::()?; 195 | module.add_class::()?; 196 | 197 | Ok(module) 198 | } 199 | -------------------------------------------------------------------------------- /src/ssh.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use pyo3::{prelude::*, types::PyType}; 4 | 5 | use crate::{IdentityError, RecipientError}; 6 | 7 | #[pyclass(module = "pyrage.ssh")] 8 | #[derive(Clone)] 9 | pub(crate) struct Recipient(pub(crate) age::ssh::Recipient); 10 | 11 | #[pymethods] 12 | impl Recipient { 13 | #[classmethod] 14 | fn from_str(_cls: &Bound<'_, PyType>, v: &str) -> PyResult { 15 | let recipient = age::ssh::Recipient::from_str(v) 16 | .map_err(|e| RecipientError::new_err(format!("invalid public key: {:?}", e)))?; 17 | 18 | Ok(Self(recipient)) 19 | } 20 | } 21 | 22 | #[pyclass(module = "pyrage.ssh")] 23 | #[derive(Clone)] 24 | pub(crate) struct Identity(pub(crate) age::ssh::Identity); 25 | 26 | #[pymethods] 27 | impl Identity { 28 | #[classmethod] 29 | fn from_buffer(_cls: &Bound<'_, PyType>, buf: &[u8]) -> PyResult { 30 | let identity = age::ssh::Identity::from_buffer(buf, None) 31 | .map_err(|e| IdentityError::new_err(e.to_string()))?; 32 | 33 | match identity { 34 | age::ssh::Identity::Unencrypted(_) => Ok(Self(identity)), 35 | age::ssh::Identity::Encrypted(_) => { 36 | Err(IdentityError::new_err("ssh key must be decrypted first")) 37 | } 38 | age::ssh::Identity::Unsupported(uk) => { 39 | // Unsupported doesn't have a Display impl, only a hardcoded `display` function. 40 | Err(IdentityError::new_err(format!("unsupported key: {:?}", uk))) 41 | } 42 | } 43 | } 44 | } 45 | 46 | pub(crate) fn module(py: Python) -> PyResult> { 47 | let module = PyModule::new(py, "ssh")?; 48 | 49 | module.add_class::()?; 50 | module.add_class::()?; 51 | 52 | Ok(module) 53 | } 54 | -------------------------------------------------------------------------------- /src/x25519.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use age::secrecy::ExposeSecret; 4 | use pyo3::{prelude::*, types::PyType}; 5 | 6 | use crate::{IdentityError, RecipientError}; 7 | 8 | #[pyclass(module = "pyrage.x25519")] 9 | #[derive(Clone)] 10 | pub(crate) struct Recipient(pub(crate) age::x25519::Recipient); 11 | 12 | #[pymethods] 13 | impl Recipient { 14 | #[classmethod] 15 | fn from_str(_cls: &Bound<'_, PyType>, v: &str) -> PyResult { 16 | age::x25519::Recipient::from_str(v) 17 | .map(Self) 18 | .map_err(RecipientError::new_err) 19 | } 20 | 21 | fn __str__(&self) -> String { 22 | self.0.to_string() 23 | } 24 | } 25 | 26 | #[pyclass(module = "pyrage.x25519")] 27 | #[derive(Clone)] 28 | pub(crate) struct Identity(pub(crate) age::x25519::Identity); 29 | 30 | #[pymethods] 31 | impl Identity { 32 | #[classmethod] 33 | fn generate(_cls: &Bound<'_, PyType>) -> Self { 34 | Self(age::x25519::Identity::generate()) 35 | } 36 | 37 | #[classmethod] 38 | fn from_str(_cls: &Bound<'_, PyType>, v: &str) -> PyResult { 39 | let identity = age::x25519::Identity::from_str(v) 40 | .map_err(|e| IdentityError::new_err(e.to_string()))?; 41 | 42 | Ok(Self(identity)) 43 | } 44 | 45 | fn to_public(&self) -> Recipient { 46 | Recipient(self.0.to_public()) 47 | } 48 | 49 | fn __str__(&self) -> String { 50 | self.0.to_string().expose_secret().into() 51 | } 52 | } 53 | 54 | pub(crate) fn module(py: Python) -> PyResult> { 55 | let module = PyModule::new(py, "x25519")?; 56 | 57 | module.add_class::()?; 58 | module.add_class::()?; 59 | 60 | Ok(module) 61 | } 62 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woodruffw/pyrage/16e97e81626a57232017f2a26b2fd4f0a637edda/test/__init__.py -------------------------------------------------------------------------------- /test/assets/ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACC5DNBFWVREAghAW1UJkeaIH2bPwT2aYcTps1R5BzqRrQAAAJCXCgUPlwoF 4 | DwAAAAtzc2gtZWQyNTUxOQAAACC5DNBFWVREAghAW1UJkeaIH2bPwT2aYcTps1R5BzqRrQ 5 | AAAECYsb4GzX6OSJoDFv0sipfwhCwdBSRlwm70WAjwKb+KsrkM0EVZVEQCCEBbVQmR5ogf 6 | Zs/BPZphxOmzVHkHOpGtAAAACXRlc3RAdGVzdAECAwQ= 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /test/assets/ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILkM0EVZVEQCCEBbVQmR5ogfZs/BPZphxOmzVHkHOpGt test@test 2 | -------------------------------------------------------------------------------- /test/assets/rsa2048: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEAvsXq1H6BQYBQFVIT3O/PuRE0pBus/b3DzqYyRdXPMcfNL5DyEzKO 4 | 1e7O+qNi6V1FW9Xp0RwIhnL8GMBEHnkul0Ko0wqdLwSFOhFWdYxh2iOZIyhauRUNAF9Is/ 5 | alULgl1PMGEmp4/Fdk9PIlDqpGIxBcgiJwLADMoa2aB4xOX2JxTYYUNno/Rk3ymMJ/XgSx 6 | b0XI68EhKrvh8P5Q82zWgT6SQxS30gU045FRfNRB7Mnp9cW2I5+X9XP+jQOFuO7PI26DV+ 7 | oGrHP8x6KDkJGyK/1RVRq0/J5Jmfcx6uWWjkgVhWurHvd3ocuNjdMGDV/Hot/BaP9k/CzZ 8 | rhqA/Vf+wQAAA8DlxwzN5ccMzQAAAAdzc2gtcnNhAAABAQC+xerUfoFBgFAVUhPc78+5ET 9 | SkG6z9vcPOpjJF1c8xx80vkPITMo7V7s76o2LpXUVb1enRHAiGcvwYwEQeeS6XQqjTCp0v 10 | BIU6EVZ1jGHaI5kjKFq5FQ0AX0iz9qVQuCXU8wYSanj8V2T08iUOqkYjEFyCInAsAMyhrZ 11 | oHjE5fYnFNhhQ2ej9GTfKYwn9eBLFvRcjrwSEqu+Hw/lDzbNaBPpJDFLfSBTTjkVF81EHs 12 | yen1xbYjn5f1c/6NA4W47s8jboNX6gasc/zHooOQkbIr/VFVGrT8nkmZ9zHq5ZaOSBWFa6 13 | se93ehy42N0wYNX8ei38Fo/2T8LNmuGoD9V/7BAAAAAwEAAQAAAQEAmrH07WCzwYCh79gB 14 | oVuZDpIEJLsIeS2jFPQxue1ZuR0ypkqb8bNCnfmrPxS0RVXLw19f3aAkzZl6ETv/QNK1VB 15 | TKv29Q7Gp2+hJLEMDILFJHgHndr02BoHajhsMPZLgefWKf3tkVyFG53OJp6E1s2EwRJ7lz 16 | SktPpB8Y0twURUUwyqBDbuICa+J/cYPNlkRPzk78GnATO5q96u2sDGqacgmPhqaQ1zMXhC 17 | slsGb68mwR4UYBqSAPywVSQxvFFF9nohXr/hxx631eM/5HcOThLVO179bQL0Qr5iqrn42B 18 | SOXwGsNkL9XEMnAmaURnCn4ku781P7ylD4HrudrWe3DbUQAAAIEAjlInfdAMZHVTVK2AJs 19 | UWC2kRGZO+H2WVHm57cgszX0io77GipizE1i/guLhXb9Tfq34cvwMcCKDHTvpdW5DPVMH2 20 | IxxqwZEUypuMruRmZkxIaZIo+S+IsJqPD5GwYkrZrOdN8zxkczet3z7mw2thELx3Yj8HZb 21 | 54DIiu3ML38AwAAACBAOTFKop/O8ixwMWE4tiIY6nrG701+n8X5UMYPZSneNG/lplwSS1u 22 | 85iWeE49FxFOGfG0RqAxbZUgfLcLGIkkHaqFdyofslK2RmAbxDw3OopTRAa2RnwRv3tof2 23 | jNcmML+KRekJXPioemtv312pcQzWYEl1jtJEXppq19t4EjRkDlAAAAgQDVevI9Gjwymb/m 24 | R4DLv4/cTFpv8He74X29pQNt/il8OXvWxkUa11sqZb1ALdIXDFw8YjwPRND7zGQKXtWLXy 25 | 9RsGxKT2t+ETS/a7wUUdQzMKhH3MKP+S4011So31xQxYJSN5uLc2aKQBb4YUbmN6iY3MRU 26 | lZIUS6ps+MAQ4RdUrQAAAAl0ZXN0QHRlc3Q= 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/assets/rsa2048.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+xerUfoFBgFAVUhPc78+5ETSkG6z9vcPOpjJF1c8xx80vkPITMo7V7s76o2LpXUVb1enRHAiGcvwYwEQeeS6XQqjTCp0vBIU6EVZ1jGHaI5kjKFq5FQ0AX0iz9qVQuCXU8wYSanj8V2T08iUOqkYjEFyCInAsAMyhrZoHjE5fYnFNhhQ2ej9GTfKYwn9eBLFvRcjrwSEqu+Hw/lDzbNaBPpJDFLfSBTTjkVF81EHsyen1xbYjn5f1c/6NA4W47s8jboNX6gasc/zHooOQkbIr/VFVGrT8nkmZ9zHq5ZaOSBWFa6se93ehy42N0wYNX8ei38Fo/2T8LNmuGoD9V/7B test@test 2 | -------------------------------------------------------------------------------- /test/assets/rsa4096: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAACFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAgEAt0tOoxqlkeqgtWKtMHsu273tanl99ZnJtOoCUe1crcSGY1WvWmO7 4 | hSVixodLQ16wPlmxi+BBgPtRd7G3G+mfE8H/dAx95dEY+Il88C2/6egC1fs0+UXU9sZyX3 5 | UEjq3XTc8cGw9Inp2OCsvtnawEVNtiB71Bx28IeJSPhhXkc71ij6nCh6xcLGXg8uCM1NM7 6 | FM7IqLphKxrCey9dEo5isrK6aFOnkDjw0E+b/bihzEfPx1w8AGFYak1rFV+n7FfaNIvFg+ 7 | 72q5AU5srYRekSqDZeZpTepQ4A3VPkgg1sat0ry36kHJxQtDsxyuKSMlg5vgbaY47JnKV7 8 | JppyWIz0bsIjQPrvet43IGah0UCn8HY3yqaP7kpBtIh6Ms6yNzDdLv7+flhwywGqzRPjuF 9 | ZeN4SLQdSkgn7J8+SZtwE3dAfVfRqENxOhaedU7tanJB+Qf7sgQjnH5kNcRuR8ImeZA2uI 10 | yFL7XWy5MhtJI8aIRivXgHXKYp0DYoFs34ypYI0ZsGlgKmu3mER0EqYVKbnti1wIt1m06h 11 | WuK6d+TXJM+w4jkVOCWsQYdR0rc0oC6L/ee+Y1F9S7UPoGeN/2xp16JsbKVwM8FsE1muY0 12 | gEO4hdY6+q1M4l75Tqi0lR3927l3m0Ant2Q3iClZ5QgNwgK58xp3+OdLm6aaJfZjJOes9Q 13 | sAAAdA6A0VdOgNFXQAAAAHc3NoLXJzYQAAAgEAt0tOoxqlkeqgtWKtMHsu273tanl99ZnJ 14 | tOoCUe1crcSGY1WvWmO7hSVixodLQ16wPlmxi+BBgPtRd7G3G+mfE8H/dAx95dEY+Il88C 15 | 2/6egC1fs0+UXU9sZyX3UEjq3XTc8cGw9Inp2OCsvtnawEVNtiB71Bx28IeJSPhhXkc71i 16 | j6nCh6xcLGXg8uCM1NM7FM7IqLphKxrCey9dEo5isrK6aFOnkDjw0E+b/bihzEfPx1w8AG 17 | FYak1rFV+n7FfaNIvFg+72q5AU5srYRekSqDZeZpTepQ4A3VPkgg1sat0ry36kHJxQtDsx 18 | yuKSMlg5vgbaY47JnKV7JppyWIz0bsIjQPrvet43IGah0UCn8HY3yqaP7kpBtIh6Ms6yNz 19 | DdLv7+flhwywGqzRPjuFZeN4SLQdSkgn7J8+SZtwE3dAfVfRqENxOhaedU7tanJB+Qf7sg 20 | QjnH5kNcRuR8ImeZA2uIyFL7XWy5MhtJI8aIRivXgHXKYp0DYoFs34ypYI0ZsGlgKmu3mE 21 | R0EqYVKbnti1wIt1m06hWuK6d+TXJM+w4jkVOCWsQYdR0rc0oC6L/ee+Y1F9S7UPoGeN/2 22 | xp16JsbKVwM8FsE1muY0gEO4hdY6+q1M4l75Tqi0lR3927l3m0Ant2Q3iClZ5QgNwgK58x 23 | p3+OdLm6aaJfZjJOes9QsAAAADAQABAAACAQCa7/dq/A1VCMYMTXskbhwv954G0Ofegb71 24 | RJHtev1KjcMjmLx/ZaqPZAjY1GiUbCgCrILeQBRe2RIGYZzs5VBovSYYjK6nY1b+UAniZG 25 | nDaTFse8pNo+/fIeRwOFQ/30cOKLITVZoHKht1mOgkzdFjgYaeECbYVE0O0FV5/Gt+Qmpc 26 | Eh2z3ciUZxH9fD7PcDaS4jDQH6wfaaFXy7ObCez4LQhRQ9Vyng9BpumvUTDDozhJxveZQr 27 | AgO4z3JkKmD6UNodC2M3sjjsl8RZ8HiXsR4dNuVwzU7L9iV5XjMUGiG473lvdWj/PQRfFT 28 | FK6MC/PTY6ubWImn9mHhQdDCdXPv8850/zWPchnv9U3LKXQiAKCeRbaIQ7O0SlQn/zT8ke 29 | IxF83YMu2S8ESMGQ5Ka0EI2+uRdA1ffj6PRHRu+hv+ql7rVtv5358A6B5nvoV0GYoZnp3O 30 | lto8f06YPK02Ps/lNXLe7xM1kebSZNvgAK74iuW8m8ePfe4G3vFczEag4PF5fgaCMGx+M7 31 | iGpvwa1U7AcSPCPvfxDtrfDIa+2X9UgdvL3gDcO0icFZCCxyMgqMKcuPyxpmcwNCDSTkt+ 32 | mAwt14VL3ddU5+wY37XnfXPtgKn3ZsS1WCAodRDh7NcejIMvZZBhEWwJRYrHrJPt2CqZeC 33 | vU7R49f8ZMMcKHd8fJYQAAAQB275QXCLGYsRphNu1sXTcN5Mqizar4OpRK32ocTZRTvOSG 34 | bx2SVhNYCFls62p9fzpQs6l4ZZUD41jkTauJ1dkiSFZXOsHK9hmBmAnx/Cccetyqn9R0Cw 35 | 4vy+MsHPOuHfw+5F+euFlmX8C0GCXKPIyglYAGrKu/rT8YF4YRphhC70e9TqwS5LKz9jWa 36 | v4bO80HNtSoI++ikJ3Haxd6h0i1fZI8LjEX7prhltCcz8fvNOijNvt5DlhCZQgkC/f6UJb 37 | gAciLTZWNm9jtpp0jA2lojBzTWn5vz2ikFBOUgRxa5R9b+g+vE0fDDt+m6/0ARcW6t31zQ 38 | Y9CLSExDnMd56K3GAAABAQDwTVkgkib37tsfOC3aFQ9NhQcxTY9n9SHk/zhZUeHQZ4UiMW 39 | 3K0VElML+aegW37Sg4xxFhuyHcDu6ei62O80oj6kWzE3MpbSZmtGc/XUEkOgH8yS8lCZ5X 40 | QQRErRq957jJtf1kZ4vm3BUm5EufeOIA5xwwrAwpuSIoKp+Sy1vUiDb4QOly1Wj64Uj09N 41 | u5mmtsLkkDMCypWGGSWEJc3dErsG5a3TSd3O4vWs073VWS4dWhWKW6ToviqDmRp58qMsa/ 42 | m4m3LQEcV6Se6J9Yf8Y+VbgHl0Jgcddky8HAF9h7Y4a/nC95qsp3XGVhLtNmfUDhTSWUWD 43 | vkIBlDpgrhaMCJAAABAQDDRJiObZAygXOwBsDzld4FuRACfrjuBLSVyAd3GZz2gzxeu6JD 44 | KNWZo7Shd4INnmRr6wTH+3wt4YtdPFVxSpVZCbxMgKrxu4VgMKmd05oaNLb8iJtyYa1Pu4 45 | NXcdrdohQpXC0ojZrNy0Y2n1M4Ip5gM1qRYmtP9/IePKjZXXQ2NIq8gsOBP8vSXS5qcDH4 46 | 7HHoFr+erkr0aJRJVx9QVYHYsEZs4Yu4d9g7XRnX4t2V1Xu/V3KF9p+4DGos4+mhmz+pyP 47 | cAf5FrTUo+sDTcDHSP9X1GyD9c8ywCcNH49aAmMqKgGVcqGnU153DsN4tgUVqtcl+k5V6H 48 | x8k2GEn0AdvzAAAACXRlc3RAdGVzdAE= 49 | -----END OPENSSH PRIVATE KEY----- 50 | -------------------------------------------------------------------------------- /test/assets/rsa4096.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC3S06jGqWR6qC1Yq0wey7bve1qeX31mcm06gJR7VytxIZjVa9aY7uFJWLGh0tDXrA+WbGL4EGA+1F3sbcb6Z8Twf90DH3l0Rj4iXzwLb/p6ALV+zT5RdT2xnJfdQSOrddNzxwbD0ienY4Ky+2drARU22IHvUHHbwh4lI+GFeRzvWKPqcKHrFwsZeDy4IzU0zsUzsioumErGsJ7L10SjmKysrpoU6eQOPDQT5v9uKHMR8/HXDwAYVhqTWsVX6fsV9o0i8WD7varkBTmythF6RKoNl5mlN6lDgDdU+SCDWxq3SvLfqQcnFC0OzHK4pIyWDm+BtpjjsmcpXsmmnJYjPRuwiNA+u963jcgZqHRQKfwdjfKpo/uSkG0iHoyzrI3MN0u/v5+WHDLAarNE+O4Vl43hItB1KSCfsnz5Jm3ATd0B9V9GoQ3E6Fp51Tu1qckH5B/uyBCOcfmQ1xG5HwiZ5kDa4jIUvtdbLkyG0kjxohGK9eAdcpinQNigWzfjKlgjRmwaWAqa7eYRHQSphUpue2LXAi3WbTqFa4rp35Nckz7DiORU4JaxBh1HStzSgLov9575jUX1LtQ+gZ43/bGnXomxspXAzwWwTWa5jSAQ7iF1jr6rUziXvlOqLSVHf3buXebQCe3ZDeIKVnlCA3CArnzGnf450ubppol9mMk56z1Cw== test@test 2 | -------------------------------------------------------------------------------- /test/test_passphrase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyrage import passphrase 4 | 5 | 6 | class TestPassphrase(unittest.TestCase): 7 | def test_roundtrip(self): 8 | plaintext = b"junk" 9 | encrypted = passphrase.encrypt(plaintext, "some password") 10 | decrypted = passphrase.decrypt(encrypted, "some password") 11 | 12 | self.assertEqual(plaintext, decrypted) 13 | -------------------------------------------------------------------------------- /test/test_pyrage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import tempfile 3 | import unittest 4 | from io import BytesIO 5 | 6 | import pyrage 7 | 8 | from .utils import ssh_keypair 9 | 10 | 11 | class TestPyrage(unittest.TestCase): 12 | def test_encrypt_fails_with_no_receipients(self): 13 | with self.assertRaisesRegex( 14 | pyrage.EncryptError, "expected at least one recipient" 15 | ): 16 | pyrage.encrypt(b"test", []) 17 | 18 | def test_roundtrip(self): 19 | identity = pyrage.x25519.Identity.generate() 20 | recipient = identity.to_public() 21 | 22 | encrypted = pyrage.encrypt(b"test", [recipient]) 23 | decrypted = pyrage.decrypt(encrypted, [identity]) 24 | 25 | self.assertEqual(b"test", decrypted) 26 | 27 | def test_roundtrip_io_fh(self): 28 | identity = pyrage.x25519.Identity.generate() 29 | recipient = identity.to_public() 30 | with tempfile.TemporaryFile() as unencrypted: 31 | unencrypted.write(b"test") 32 | unencrypted.seek(0) 33 | with tempfile.TemporaryFile() as encrypted: 34 | pyrage.encrypt_io(unencrypted, encrypted, [recipient]) 35 | encrypted.seek(0) 36 | with tempfile.TemporaryFile() as decrypted: 37 | pyrage.decrypt_io(encrypted, decrypted, [identity]) 38 | decrypted.seek(0) 39 | unencrypted.seek(0) 40 | self.assertEqual(unencrypted.read(), decrypted.read()) 41 | 42 | def test_roundtrip_io_bytesio(self): 43 | identity = pyrage.x25519.Identity.generate() 44 | recipient = identity.to_public() 45 | unencrypted = BytesIO(b'test') 46 | encrypted = BytesIO() 47 | decrypted = BytesIO() 48 | pyrage.encrypt_io(unencrypted, encrypted, [recipient]) 49 | encrypted.seek(0) 50 | pyrage.decrypt_io(encrypted, decrypted, [identity]) 51 | decrypted.seek(0) 52 | unencrypted.seek(0) 53 | self.assertEqual(unencrypted.read(), decrypted.read()) 54 | 55 | def test_roundtrip_io_fail(self): 56 | identity = pyrage.x25519.Identity.generate() 57 | recipient = identity.to_public() 58 | 59 | with self.assertRaises(TypeError): 60 | input = 'test' 61 | output = BytesIO() 62 | pyrage.encrypt_io(input, output, [recipient]) 63 | 64 | with self.assertRaises(TypeError): 65 | input = BytesIO() 66 | output = 'test' 67 | pyrage.encrypt_io(input, output, [recipient]) 68 | 69 | with self.assertRaises(TypeError): 70 | input = 'test' 71 | output = BytesIO() 72 | pyrage.decrypt_io(input, output, [recipient]) 73 | 74 | with self.assertRaises(TypeError): 75 | input = BytesIO() 76 | output = 'test' 77 | pyrage.decrypt_io(input, output, [recipient]) 78 | 79 | 80 | def test_roundtrip_file(self): 81 | identity = pyrage.x25519.Identity.generate() 82 | recipient = identity.to_public() 83 | 84 | with tempfile.TemporaryDirectory() as tempdir: 85 | unencrypted = os.path.join(tempdir, "unencrypted") 86 | encrypted = os.path.join(tempdir, "encrypted") 87 | decrypted = os.path.join(tempdir, "decrypted") 88 | 89 | with open(unencrypted, "wb") as file: 90 | file.write(b"test") 91 | 92 | pyrage.encrypt_file(unencrypted, encrypted, [recipient]) 93 | pyrage.decrypt_file(encrypted, decrypted, [identity]) 94 | 95 | with open(unencrypted, "rb") as file1: 96 | with open(decrypted, "rb") as file2: 97 | self.assertEqual(file1.read(), file2.read()) 98 | 99 | def test_decrypt_fails_wrong_recipient(self): 100 | alice = pyrage.x25519.Identity.generate() 101 | bob = pyrage.x25519.Identity.generate() 102 | 103 | # alice encrypts to herself 104 | encrypted = pyrage.encrypt(b"test", [alice.to_public()]) 105 | 106 | # bob tries to decrypt and fails 107 | with self.assertRaisesRegex(pyrage.DecryptError, "No matching keys found"): 108 | pyrage.decrypt(encrypted, [bob]) 109 | 110 | # one key matches, so decryption succeeds 111 | decrypted = pyrage.decrypt(encrypted, [alice, bob]) 112 | self.assertEqual(b"test", decrypted) 113 | 114 | def test_roundtrip_matrix(self): 115 | identities = [] 116 | recipients = [] 117 | 118 | age_identity = pyrage.x25519.Identity.generate() 119 | identities.append(age_identity) 120 | age_recipient = age_identity.to_public() 121 | recipients.append(age_recipient) 122 | 123 | for filename in ["ed25519", "rsa4096", "rsa2048"]: 124 | pubkey, privkey = ssh_keypair(filename) 125 | identities.append(pyrage.ssh.Identity.from_buffer(privkey.encode())) 126 | recipients.append(pyrage.ssh.Recipient.from_str(pubkey)) 127 | 128 | # Encrypt to all recipients, decode using each identity. 129 | encrypted = pyrage.encrypt(b"test matrix", recipients) 130 | for identity in identities: 131 | self.assertEqual(b"test matrix", pyrage.decrypt(encrypted, [identity])) 132 | 133 | 134 | if __name__ == "__main__": 135 | unittest.main() 136 | -------------------------------------------------------------------------------- /test/test_ssh.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyrage import ssh, RecipientError 4 | 5 | from .utils import ssh_keypair 6 | 7 | 8 | class TestIdentity(unittest.TestCase): 9 | def test_from_buffer(self): 10 | for filename in ["ed25519", "rsa4096", "rsa2048"]: 11 | _pubkey, privkey = ssh_keypair(filename) 12 | identity = ssh.Identity.from_buffer(privkey.encode()) 13 | self.assertIsInstance(identity, ssh.Identity) 14 | 15 | 16 | class TestRecipient(unittest.TestCase): 17 | def test_from_str(self): 18 | for filename in ["ed25519", "rsa4096", "rsa2048"]: 19 | pubkey, _privkey = ssh_keypair(filename) 20 | recipient = ssh.Recipient.from_str(pubkey) 21 | self.assertIsInstance(recipient, ssh.Recipient) 22 | 23 | def test_from_str_invalid(self): 24 | with self.assertRaisesRegex(RecipientError, "invalid SSH recipient"): 25 | ssh.Recipient.from_str("invalid ssh pubkey") 26 | -------------------------------------------------------------------------------- /test/test_x25519.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pyrage import x25519, IdentityError, RecipientError 4 | 5 | 6 | class TestIdentity(unittest.TestCase): 7 | def test_generate(self): 8 | identity = x25519.Identity.generate() 9 | self.assertIsInstance(identity, x25519.Identity) 10 | self.assertTrue(str(identity).startswith("AGE-SECRET-KEY")) 11 | 12 | recipient = identity.to_public() 13 | self.assertTrue(str(recipient).startswith("age")) 14 | 15 | def test_from_str(self): 16 | generated = x25519.Identity.generate() 17 | parsed = x25519.Identity.from_str(str(generated)) 18 | self.assertIsInstance(parsed, x25519.Identity) 19 | 20 | def test_from_str_invalid(self): 21 | with self.assertRaisesRegex(IdentityError, "invalid Bech32 encoding"): 22 | x25519.Identity.from_str("BAD-PREFIX") 23 | 24 | 25 | class TestRecipient(unittest.TestCase): 26 | def test_from_str(self): 27 | recipient = x25519.Recipient.from_str( 28 | "age1zvkyg2lqzraa2lnjvqej32nkuu0ues2s82hzrye869xeexvn73equnujwj" 29 | ) 30 | self.assertIsInstance(recipient, x25519.Recipient) 31 | self.assertEqual( 32 | str(recipient), 33 | "age1zvkyg2lqzraa2lnjvqej32nkuu0ues2s82hzrye869xeexvn73equnujwj", 34 | ) 35 | 36 | def test_from_str_invalid(self): 37 | with self.assertRaisesRegex(RecipientError, "invalid Bech32 encoding"): 38 | x25519.Recipient.from_str("badprefix") 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /test/utils.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | _HERE = Path(__file__).parent 4 | _ASSETS = _HERE / "assets" 5 | 6 | assert _ASSETS.is_dir(), "missing test assets directory" 7 | 8 | 9 | def ssh_keypair(name): 10 | (pub, priv) = (_ASSETS / f"{name}.pub", _ASSETS / name) 11 | return (pub.read_text(), priv.read_text()) 12 | --------------------------------------------------------------------------------