├── pyseto ├── py.typed ├── versions │ ├── __init__.py │ ├── v4.py │ ├── v2.py │ └── v1.py ├── exceptions.py ├── __init__.py ├── utils.py ├── token.py ├── pyseto.py ├── key_interface.py └── key.py ├── tests ├── __init__.py ├── keys │ ├── public_key_ed25519.pem │ ├── public_key_x25519.pem │ ├── public_key_ed25519_2.pem │ ├── public_key_x25519_2.pem │ ├── private_key_ed25519.pem │ ├── private_key_ed25519_2.pem │ ├── private_key_x25519.pem │ ├── private_key_x25519_2.pem │ ├── public_key_ed25519.json │ ├── private_key_ed25519.json │ ├── public_key_ecdsa_p384.pem │ ├── public_key_ecdsa_p384_2.pem │ ├── public_key_ecdsa_p384.json │ ├── private_key_ecdsa_p384.pem │ ├── private_key_ecdsa_p384_2.pem │ ├── private_key_ecdsa_p384.json │ ├── public_key_rsa.json │ ├── public_key_rsa.pem │ ├── public_key_rsa_2.pem │ ├── private_key_rsa.json │ ├── private_key_rsa_2.pem │ └── private_key_rsa.pem ├── vectors │ └── PASERK │ │ ├── k3.pid.json │ │ ├── k3.public.json │ │ ├── k1.pid.json │ │ ├── k1.lid.json │ │ ├── k2.lid.json │ │ ├── k2.pid.json │ │ ├── k3.lid.json │ │ ├── k4.lid.json │ │ ├── k4.pid.json │ │ ├── k1.local.json │ │ ├── k2.local.json │ │ ├── k3.local.json │ │ ├── k4.local.json │ │ ├── k2.public.json │ │ ├── k4.public.json │ │ ├── k2.local-wrap.pie.json │ │ ├── k4.local-wrap.pie.json │ │ ├── k1.local-wrap.pie.json │ │ ├── k3.local-wrap.pie.json │ │ ├── k1.local-pw.json │ │ ├── k2.local-pw.json │ │ ├── k3.local-pw.json │ │ ├── k4.local-pw.json │ │ ├── k1.public.json │ │ ├── k2.secret-wrap.pie.json │ │ ├── k4.secret-wrap.pie.json │ │ ├── k2.sid.json │ │ ├── k4.sid.json │ │ ├── k2.seal.json │ │ ├── k4.seal.json │ │ ├── k2.secret-pw.json │ │ ├── k4.secret-pw.json │ │ ├── k2.secret.json │ │ ├── k4.secret.json │ │ ├── k1.sid.json │ │ ├── k3.seal.json │ │ ├── k3.secret-wrap.pie.json │ │ ├── k3.secret-pw.json │ │ ├── k3.sid.json │ │ ├── k3.secret.json │ │ ├── k1.secret.json │ │ ├── k1.secret-wrap.pie.json │ │ ├── k1.secret-pw.json │ │ └── k1.seal.json ├── utils.py ├── test_utils.py ├── test_key_interface.py ├── test_token.py ├── test_v4.py ├── test_v1.py ├── test_v3.py └── test_v2.py ├── docs ├── changes.rst ├── installation.rst ├── api.rst ├── Makefile ├── conf.py ├── index.rst └── paserk_usage.rst ├── .github ├── dependabot.yml ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── cd.yml │ ├── ci.yml │ └── codeql-analysis.yml ├── .readthedocs.yml ├── SECURITY.md ├── tox.ini ├── LICENSE ├── pyproject.toml ├── .pre-commit-config.yaml ├── .gitignore └── CODE_OF_CONDUCT.md /pyseto/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pyseto/versions/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "pip" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | -------------------------------------------------------------------------------- /tests/keys/public_key_ed25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/public_key_x25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VuAyEAFv8IXsICYj0paznDK/99GyCsFOIGnfY87ayyNSIvSB4= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/public_key_ed25519_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VwAyEAkv4y3wCgwetRuJUt/EKjNJzaTWMKCNcadaGg6obUFdI= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/public_key_x25519_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MCowBQYDK2VuAyEAoMfvlI5DN08JRFP2fhWvZ6vBEl28yFeS9O9YQUjNyCY= 3 | -----END PUBLIC KEY----- 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [dajiaji] 4 | open_collective: # Replace with a single Open Collective username 5 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | You can install PySETO with pip: 5 | 6 | .. code-block:: console 7 | 8 | $ pip install pyseto 9 | -------------------------------------------------------------------------------- /tests/keys/private_key_ed25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/private_key_ed25519_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VwBCIEIGmfHRcqkCfnAOB7234NNeuBpHUVHSLX4z3s4hsaTEQ8 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/private_key_x25519.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VuBCIEIFAF7jSCZHFgWvC8hUkXr55Az6Pot2g4zOAUxck0/6x8 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/private_key_x25519_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MC4CAQAwBQYDK2VuBCIEIMAXvyHjAeXy9x4MXF6rwGbDKw7crgDriFTFXO+XsS1F 3 | -----END PRIVATE KEY----- 4 | -------------------------------------------------------------------------------- /tests/keys/public_key_ed25519.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "use": "sig", 4 | "crv": "Ed25519", 5 | "kid": "Ed25519-01", 6 | "x": "2E6dX83gqD_D0eAmqnaHe1TC1xuld6iAKXfw2OVATr0", 7 | "alg": "EdDSA" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/private_key_ed25519.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "OKP", 3 | "d": "L8JS08VsFZoZxGa9JvzYmCWOwg7zaKcei3KZmYsj7dc", 4 | "use": "sig", 5 | "crv": "Ed25519", 6 | "kid": "Ed25519-01", 7 | "x": "2E6dX83gqD_D0eAmqnaHe1TC1xuld6iAKXfw2OVATr0" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/public_key_ecdsa_p384.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE7ObQwS3UPiW2D52/4olxQtePuqTgl9CR 3 | zbuQkiyq1BBEwzX+ib9zriwmje/6sMBRyUbUyJfZR7gwWpZFOolVL4l8Y7KdHg5w 4 | 8cTtbJlLOS6IMgYisqXdF0hXHy1yPkZn 5 | -----END PUBLIC KEY----- 6 | -------------------------------------------------------------------------------- /tests/keys/public_key_ecdsa_p384_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEWKQ1XJjRcyFdNP4xEIjdUI7TyEOcjVu4 3 | uEsLy3wCeP4xGngjn47Ubm62W+xvvHxioyd1ydj70mMCdrPLx3NS+SsRXdPDCG2T 4 | E48v8Q5gD/AcsrqFAahcj6CFXLTlgZFg 5 | -----END PUBLIC KEY----- 6 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3.12" 7 | jobs: 8 | post_install: 9 | - pip install poetry 10 | - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with docs 11 | 12 | sphinx: 13 | configuration: docs/conf.py 14 | -------------------------------------------------------------------------------- /tests/keys/public_key_ecdsa_p384.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "use": "sig", 4 | "crv": "P-384", 5 | "kid": "P-384-01", 6 | "x": "_XyN9woHaS0mPimSW-etwJMEDSzxIMjp4PjezavU8SHJoClz1bQrcmPb1ZJxHxhI", 7 | "y": "GCNfc32p9sRotx7u2oDGJ3Eqz6q5zPHLdizNn83oRsUTN31eCWfGLHWRury3xF50", 8 | "alg": "ES384" 9 | } 10 | -------------------------------------------------------------------------------- /tests/keys/private_key_ecdsa_p384.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIGkAgEBBDBRL3j2Ozm3+43DbMEdeDZaNPqZPThOagVn3rQ1ACdWWasskoJ7CNbl 3 | /+PE7qx1PpagBwYFK4EEACKhZANiAATs5tDBLdQ+JbYPnb/iiXFC14+6pOCX0JHN 4 | u5CSLKrUEETDNf6Jv3OuLCaN7/qwwFHJRtTIl9lHuDBalkU6iVUviXxjsp0eDnDx 5 | xO1smUs5LogyBiKypd0XSFcfLXI+Rmc= 6 | -----END EC PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /tests/keys/private_key_ecdsa_p384_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MIGkAgEBBDCZhgSMdDy05aAmpJ/BrsELZ8DDMHqGGldhCAW47V7IVAdGRgJPatFy 3 | IKYsyS2ph5SgBwYFK4EEACKhZANiAARYpDVcmNFzIV00/jEQiN1QjtPIQ5yNW7i4 4 | SwvLfAJ4/jEaeCOfjtRubrZb7G+8fGKjJ3XJ2PvSYwJ2s8vHc1L5KxFd08MIbZMT 5 | jy/xDmAP8ByyuoUBqFyPoIVctOWBkWA= 6 | -----END EC PRIVATE KEY----- 7 | -------------------------------------------------------------------------------- /tests/keys/private_key_ecdsa_p384.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "EC", 3 | "d": "1pImEKbrr771-RKi8Tb7tou_WjiR7kwui_nMu16449rk3lzAqf9buUhTkJ-pogkb", 4 | "use": "sig", 5 | "crv": "P-384", 6 | "kid": "P-384-01", 7 | "x": "_XyN9woHaS0mPimSW-etwJMEDSzxIMjp4PjezavU8SHJoClz1bQrcmPb1ZJxHxhI", 8 | "y": "GCNfc32p9sRotx7u2oDGJ3Eqz6q5zPHLdizNn83oRsUTN31eCWfGLHWRury3xF50" 9 | } 10 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.8.x | :white_check_mark: | 8 | | <1.8 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | Please use maintainer's email: dajiaji@gmail.com 13 | 14 | DO NOT REPORT SECURITY VULNERABILITIES VIA THE ISSUES OF THIS REPOSITORY. 15 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | .. automodule:: pyseto 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | :member-order: bysource 9 | 10 | .. automodule:: pyseto.key_interface 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | :member-order: bysource 15 | 16 | .. automodule:: pyseto.token 17 | :members: 18 | :undoc-members: 19 | :show-inheritance: 20 | :member-order: bysource 21 | -------------------------------------------------------------------------------- /tests/keys/public_key_rsa.json: -------------------------------------------------------------------------------- 1 | { 2 | "kty": "RSA", 3 | "e": "AQAB", 4 | "use": "sig", 5 | "kid": "RS256-01", 6 | "alg": "RS256", 7 | "n": "o4hWSF-rtXeKI8J32sX3agnJZMyprxQWdmfkaL3jsLK13Yelk7ChlaKIPWU0GHU7Wh_Ce3aslUIb7KsU51Fo3olAVBVszjTYZFu5IIgKtZoni-r8NVHqcmT6JLGhoVGVRaPjEGudmCtxocgiWXGHucl14DChV6PAcPlcbcosNmeJ5z6ia8lp5det-KkjFTe0-EdWB2kTzZvEmbotbtPL-B0BH_jYqmmZWgCU_TZ76QgORiOPmN-GW5RrLOEkay-f-Sx6XGTZxC_heN6eT-Ql1KgGQnagRDOAmcKy4CJp2vmODR8LbOyZl22C8x5wR5Vzz-9NE_WIF0nHT1ivIFOOZQ" 8 | } 9 | -------------------------------------------------------------------------------- /tests/keys/public_key_rsa.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAy9THKaw4pvjiQ1/KRZYK 3 | w/6Fqe8ra3v32Zk2l3wnYGoy9Vh1Rq/7BrFfcKrPGL/GtaDAuSuO7TOWgTzFSI3S 4 | JemdTQpB9hS0qr8yo2bbETDnjMz+9j5L8cpNsM9Sv2EHEZUYcbKBPDGr/WMc2uRp 5 | I8UK7IAiMdIAb+VHv15wF82taoSfBBSUkEmoV2Z1POXfQN6KBMaR0VN/UqP0N9tg 6 | 8BaiD0e2QXWL1/jwgdtJmQiOEFbA2MmjkVBTlCzPsWUKvIEYI1tu/EgjdQBVztPP 7 | lyCBP6GKiMQWh9z5YyNwpRgOSJL9e9m4wdxWoOHLH9WdbVLC7IxC5x7DiJnWveLv 8 | FwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /tests/keys/public_key_rsa_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyT53fjKCUzdfOqlUNYcy 3 | cGAWbiSTg8TOHr/9KjliYusJR/V5j+3WCDV1itik8fPKTs2dtIz+bQ+WvFFGIhP0 4 | ftr/y6dYGbJpKxJoqAdwA01FSsr4Xu8QOyKWp9jwJVNOQtyYr2gQAbJPLSRHxynZ 5 | t8qU46is+kTWU8Y7Nw44kMZMv9yI36mdYA00Ve2Z9/BxDDlvQQim72aCJHmpMn1N 6 | RvMpB0IWY5q6k/sCrWfNwggzT+bU5n0U9C7SUbnx5uoEq+f/sQnTCj3LJI8jfQP2 7 | GJ9KWsjeh89abSusLvYAOSo+JiFIvhr5VrQ4km+/fYzdfJyLy4wF/QNhSOnsegSh 8 | hwIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: dajiaji 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.pid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.pid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.pid-1", 6 | "key": "02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k3.pid.mL4lGxNG7cz128frmpn83_76V9C7LmV2sHAMtJ8vIdwG" 8 | }, 9 | { 10 | "name": "k3.pid-2", 11 | "key": "02707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 12 | "paserk": "k3.pid.gnwg7IkzZyQF9wJgLLT0OpbdMT7BYmdQoG2u-xXpeeHz" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.public Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.public-1", 6 | "key": "02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k3.public.AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 8 | }, 9 | { 10 | "name": "k3.public-2", 11 | "key": "02707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 12 | "paserk": "k3.public.AnBxcnN0dXZ3eHl6e3x9fn-AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2enw" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-python@v4 15 | with: 16 | python-version: "3.12" 17 | 18 | - name: Install dependencies 19 | run: python -m pip install poetry 20 | 21 | - name: Build and publish 22 | env: 23 | POETRY_USERNAME: ${{ secrets.PYPI_USERNAME }} 24 | POETRY_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 25 | run: | 26 | poetry build 27 | poetry publish -u $POETRY_USERNAME -p $POETRY_PASSWORD 28 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.pid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.pid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.pid-1", 6 | "key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", 7 | "paserk": "k1.pid.oxQIZk0yciX7cLRZ3C0Psdoj-RUqmVHrlnIYGNma6xy8" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.lid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.lid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.lid-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k1.lid.hX7AFMLetrMi9GRENnuqkxV8UPxOkFXrcNpyls9U9Pmd" 8 | }, 9 | { 10 | "name": "k1.lid-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k1.lid.Itkhw2RHx7sR6k4kqe31ekYRG1c8JfowN7h8jwdjb8sm" 13 | }, 14 | { 15 | "name": "k1.lid-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k1.lid.qAQFESETVi8r1m6CjDmj1kf3qKDpY3GELGR5a000Ku4M" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.lid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.lid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.lid-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k2.lid.WpgZ4rDluvqGSwOFQfbkxem-i3lRJ92XPPPwHEDm-gtE" 8 | }, 9 | { 10 | "name": "k2.lid-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k2.lid.keK316jg65NYOw6BbBHJHeQ7YWpyuHfNRxBVtY3kNoXG" 13 | }, 14 | { 15 | "name": "k2.lid-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k2.lid.n-Eq3LSDD2sdKE5DjJmZmXQ7-O7BT8mhYoSg_TvDC6x1" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.pid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.pid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.pid-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k2.pid.gUztDB_2F1oxWaUa0RYxGYtLw_0TN-g5mw_M7HUMLh7D" 8 | }, 9 | { 10 | "name": "k2.pid-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k2.pid.4zgEvkSaB64DlcV9ChYZPEqBATLwUsB5zCrlpEOk2wD9" 13 | }, 14 | { 15 | "name": "k2.pid-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k2.pid.2Jd0YsfxY7YWVCHWG0BOc9CCVZmSqRXz0B-p7ENnkxqG" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.lid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.lid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.lid-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k3.lid.c2Wpke9KunV6-Tow8dV1wsvVFRkjcTYt_7ZzOtIDRFpM" 8 | }, 9 | { 10 | "name": "k3.lid-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k3.lid.5GB-DfqfPOIMr0-y4IV8323vrjMt3mZMh_R3J3raH38l" 13 | }, 14 | { 15 | "name": "k3.lid-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k3.lid.Gd3T6cJNElhwD4gu9JXlaysLNClYmFTD6GRUnXRotCEr" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.lid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.lid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.lid-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k4.lid.bqltbNc4JLUAmc9Xtpok-fBuI0dQN5_m3CD9W_nbh559" 8 | }, 9 | { 10 | "name": "k4.lid-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k4.lid.iVtYQDjr5gEijCSjJC3fQaJm7nCeQSeaty0Jixy8dbsk" 13 | }, 14 | { 15 | "name": "k4.lid-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k4.lid.-v0wjDR1FVxNT2to41Ay1P4_8X6HIxnybX1nZ1a4FCTm" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.pid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.pid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.pid-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k4.pid.S_XQmeEwHbbvRmiyfXfHYpLGjXGzjTRSDoT1YtTakWFE" 8 | }, 9 | { 10 | "name": "k4.pid-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k4.pid.9ShR3xc8-qVJ_di0tc9nx0IDIqbatdeM2mqLFBJsKRHs" 13 | }, 14 | { 15 | "name": "k4.pid-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k4.pid.-nyvbaTz8U6TQz7OZWW-iB3va31iAxIpUgzUcVQVmW9A" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.local Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.local-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k1.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 8 | }, 9 | { 10 | "name": "k1.local-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k1.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8" 13 | }, 14 | { 15 | "name": "k1.local-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k1.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpA" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.local Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.local-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k2.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 8 | }, 9 | { 10 | "name": "k2.local-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k2.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8" 13 | }, 14 | { 15 | "name": "k2.local-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k2.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpA" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.local Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.local-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k3.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 8 | }, 9 | { 10 | "name": "k3.local-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k3.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8" 13 | }, 14 | { 15 | "name": "k3.local-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k3.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpA" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.local.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.local Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.local-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k4.local.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 8 | }, 9 | { 10 | "name": "k4.local-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k4.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8" 13 | }, 14 | { 15 | "name": "k4.local-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k4.local.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpA" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.public Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.public-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k2.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 8 | }, 9 | { 10 | "name": "k2.public-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k2.public.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8" 13 | }, 14 | { 15 | "name": "k2.public-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k2.public.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpA" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.public Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.public-1", 6 | "key": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "paserk": "k4.public.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" 8 | }, 9 | { 10 | "name": "k4.public-2", 11 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 12 | "paserk": "k4.public.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8" 13 | }, 14 | { 15 | "name": "k4.public-3", 16 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 17 | "paserk": "k4.public.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpA" 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Any, Dict 4 | 5 | from pyseto.utils import base64url_decode 6 | 7 | 8 | def get_path(name: str) -> str: 9 | return os.path.join(os.path.dirname(os.path.realpath(__file__)), name) 10 | 11 | 12 | def load_key(name: str) -> str: 13 | with open(get_path(name)) as key_file: 14 | k = key_file.read() 15 | return k 16 | 17 | 18 | def load_jwk(name: str) -> Dict[str, Any]: 19 | with open(get_path(name)) as key_file: 20 | jwk = json.loads(key_file.read()) 21 | res = {} 22 | res["d"] = base64url_decode(jwk["d"]) if "d" in jwk else b"" 23 | res["x"] = base64url_decode(jwk["x"]) if "x" in jwk else b"" 24 | res["y"] = base64url_decode(jwk["y"]) if "y" in jwk else b"" 25 | 26 | if "d" in jwk and "x" in jwk and "y" not in jwk: 27 | res["x"] = b"" 28 | return res 29 | -------------------------------------------------------------------------------- /pyseto/exceptions.py: -------------------------------------------------------------------------------- 1 | class PysetoError(Exception): 2 | """ 3 | Base class for all exceptions. 4 | """ 5 | 6 | pass 7 | 8 | 9 | class NotSupportedError(PysetoError): 10 | """ 11 | An Exception occurred when the function is not supported for the key object. 12 | """ 13 | 14 | pass 15 | 16 | 17 | class EncryptError(PysetoError): 18 | """ 19 | An Exception occurred when an encryption process failed. 20 | """ 21 | 22 | pass 23 | 24 | 25 | class DecryptError(PysetoError): 26 | """ 27 | An Exception occurred when an decryption process failed. 28 | """ 29 | 30 | pass 31 | 32 | 33 | class SignError(PysetoError): 34 | """ 35 | An Exception occurred when a signing process failed. 36 | """ 37 | 38 | pass 39 | 40 | 41 | class VerifyError(PysetoError): 42 | """ 43 | An Exception occurred when a verification process failed. 44 | """ 45 | 46 | pass 47 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyseto.utils import i2osp 4 | 5 | 6 | class TestUtils: 7 | """ 8 | Tests for utils. 9 | """ 10 | 11 | def test_utils_i2osp_invalid_arg(self): 12 | with pytest.raises(ValueError) as err: 13 | i2osp(270, 1) 14 | pytest.fail("i2osp should fail.") 15 | assert "integer too large" in str(err.value) 16 | 17 | def test_utils_i2osp_with_padding(self): 18 | res = i2osp( 19 | 1773640271034215956220647394962766686220368790999773790300657174885851868252873740857850633982047596450206042428564987609136709554719966122707327315574236228, 20 | 66, 21 | ) 22 | assert ( 23 | res 24 | == b"\x00\x84H\xbd\x0e;\xc5\xb7\xdf\\\x1f\xf9\x03\xd2Db\xd1\xdf7\x1b}\x80g4A}\xb0\x19\xcd-\xc6n\x1e&\xe8\xafm\xdd!\xb6\xc3 9}\xb5%\xb3\x02\x87V\xf8\x94C\xe1O\xeb\x14f\x93\xe5\xc7\x8e}#\xa4D" 25 | ) 26 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.local-wrap.pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.local-wrap.pie Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.local-wrap.pie-1", 6 | "unwrapped": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "wrapping-key":"707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 8 | "paserk": "k2.local-wrap.pie.vRFzOX-h6lsRQBpBaRbPv5WbT8Kcv_a_NDUfp4kcn-Un6mL2-H1nuZ5YxctgLT4I476TViftvpJu6XJ4iwraTprnVB8KrZaMo387BznW4wYOrC7CZaBpg683sOnmDjtb" 9 | }, 10 | { 11 | "name": "k2.local-wrap.pie-2", 12 | "unwrapped": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 13 | "wrapping-key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "paserk": "k2.local-wrap.pie.UNK6-S4s4uZ2oc7Ntujea9FdRDWgmWmkFJrZPQtVb_Z4GF2iWN7UVPXyKGYfF1WkqVk7a4iWuxAx8KwpoNZEPHK1Ym6PtROKDeMpBPo-G0I9cDyh_r764LGy3NqRb6_0" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.local-wrap.pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.local-wrap.pie Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.local-wrap.pie-1", 6 | "unwrapped": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "wrapping-key":"707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 8 | "paserk": "k4.local-wrap.pie.y-PC8Zh6P1DoOBUdhRr7W8GWSgHtRKvE8PWWYA-qXy3fxJDmaRsxcZVQzuvXHZuBg5MqCgh_y5K0WbukJCrDX73Wdf631VBnE1DNHafbjnGNzFNWP59ba9ifsOAgE7Bw" 9 | }, 10 | { 11 | "name": "k4.local-wrap.pie-2", 12 | "unwrapped": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 13 | "wrapping-key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "paserk": "k4.local-wrap.pie.cy-Mu6zSfhu6q0_XdAM9p1zre_joUWjreSjHgisVNh-oHaNarN4_c7xuSyaHwqEDxF7lTbfNplBGU7wTeUyt__hZyj1J38NdNxVwuXamJY2QhRE-kWYA9_16xTsGwCQX" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.local-wrap.pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.local-wrap.pie Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.local-wrap.pie-1", 6 | "unwrapped": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "wrapping-key":"707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 8 | "paserk": "k1.local-wrap.pie.px2xARZYyBu7Wfv3HCWP7nUOBPXrVicGWPd_SAKETuq2_HZOmofjUZYTQA338KXtGidUIDCxv218a3p-J16TaR8gAZ8P6LhWN_U6Spd0d5JZcecPMXlWwvoCDhOT88gUJdVXrnzTl6HBK7l4OE4yMg" 9 | }, 10 | { 11 | "name": "k1.local-wrap.pie-2", 12 | "unwrapped": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 13 | "wrapping-key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "paserk": "k1.local-wrap.pie.aMF7_m3KXH8Rgoo4ow1FYEThHAhaNR1deM9SbRRnHR9-ao5qckA-b0sltysyHG8jPor1oVTCiTxS0Bx8Rt6Dnxy9rMGEYwrqfQfrXJLNnvh6O19Id0TwJ-vMnagj3xJeGEZMSO8K9JaZrgh6sBAWng" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.local-wrap.pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.local-wrap.pie Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.local-wrap.pie-1", 6 | "unwrapped": "0000000000000000000000000000000000000000000000000000000000000000", 7 | "wrapping-key":"707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 8 | "paserk": "k3.local-wrap.pie.cLJLT84tUuU-ZLPqKWfhlDw4c2Fhk896z97sK2eM2-HYB3dk_NrHsSS340sJPsBsb7VeFpDBQMzzqRXr4Oylrpzmg-NZC9FVqgaWm1gtEikm-1yvlGRYwstUFLvUF30NrBE3GxYzI63DqJPqfmHSmQ" 9 | }, 10 | { 11 | "name": "k3.local-wrap.pie-2", 12 | "unwrapped": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 13 | "wrapping-key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "paserk": "k3.local-wrap.pie.bkbHfW4bBJQ8jcPfLOYxUrg4SkKRHbsywYZwRvxUGFt1je2idZxlFr8sbkB6jTZ6hnrVlI25G2hqZtfdQyFIUcrRAiBrCWNPP1b3afdD9_YxsAXoKEA3X4AZhReuvHCzuPqXNCtrvJtpupGZn-PLFQ" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.local-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.local-pw Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.local-pw-1", 6 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 7 | "password": "correct horse battery staple", 8 | "options": {"iterations": 1000}, 9 | "paserk": "k1.local-pw.VKx8qIQe3enmkPXYEC6qi4V30ycbeG6RlP75f9nn--gAAAPo5V11btISiVqj9T93t5LShRpfxA1mcb1Ck7sCI-UnU_znnV1d-imNLno6NM0PNC4HsdT5UrD-9f-ZvTUK3kKG2xF5pcvPxZjDrggHzZT1A9T-n5q2FbD_HGw0JhYeyErT" 10 | }, 11 | { 12 | "name": "k1.local-pw-2", 13 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "password": "correct horse battery staple", 15 | "options": {"iterations": 10000}, 16 | "paserk": "k1.local-pw.hWz7cS2OoNYcquIPzbyKmz9Kta0PhQwxtLdwSvCloFoAACcQJ0opIhYSi_ZfFQMDudybcjRW2QE6Z-6u7SqZgbnaGSdFt4DzAMwJAJ1BnmBOe54POR0qhKoq79dO5rEB-t-sWo_1C2UKX2OgTpqHBAopLKQEVDOjPPO7Mmh19g9z0kvW" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.local-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.local-pw Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.local-pw-1", 6 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 7 | "password": "correct horse battery staple", 8 | "options": {"memlimit": 67108864, "opslimit": 2}, 9 | "paserk": "k2.local-pw.vriwmfTyiNlP0_0YrLajjQAAAAAEAAAAAAAAAgAAAAEYrv81QxQEEJCJXk4uTCpFDCTasAay2QR7v1vo7xrmSGbwvbApgFMkDXaUjmYTY1vPv0LlOxxRh-b4zUoY_Ru_vs5EPaa4NtGzXRiXBu71yoTZf8Yxz-rH" 10 | }, 11 | { 12 | "name": "k2.local-pw-2", 13 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "password": "correct horse battery staple", 15 | "options": {"memlimit": 268435456, "opslimit": 3}, 16 | "paserk": "k2.local-pw.B87CSl-9AWEzCzRVznPLnwAAAAAQAAAAAAAAAwAAAAG9P4nX4yiLT4z4tsbmLGFLWDeEkkgt-P0o7__XYHpHqlOit9t8HCmxX29p9j6XO3vybXtpFXCVNLHgUGX5_q2snktiu4XHuZMf65gV8BP32fVWAwJhO3_B" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.local-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.local-pw Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.local-pw-1", 6 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 7 | "password": "correct horse battery staple", 8 | "options": {"iterations": 1000}, 9 | "paserk": "k3.local-pw.3_mLGmsVTYEz-wtP6Xiu7rFYvGw26ZsZGagGLxWdwToAAAPoHT9jnrFPCZlSHty7cnWCusJb2AIhL9reTADbOB0OqS0kfTp5VmdmQZKHU2KqU1EoOO82xRReS8EPCFtMrrcwQulsKq7KDB7bxgrk3ueWtHRrPYlbCoRY-lSQKHKBGURL" 10 | }, 11 | { 12 | "name": "k3.local-pw-2", 13 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "password": "correct horse battery staple", 15 | "options": {"iterations": 10000}, 16 | "paserk": "k3.local-pw.i8HI5x3xBVS8hIjkMCcLRzdwwiBbXflrwiO9dW1JjxEAACcQLvmda_TZbm45IR1qg6wWkkMh5c0-kB4_ic3nhV3tegdMN7f2axXFNoK-4s69K1hF6rdF5DybpOBu6oMIemS8UnrYEEW_zrNm73WUnrOnPcUtMdRhK4X6SzH3oaUqRkgH" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.local-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.local-pw Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.local-pw-1", 6 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 7 | "password": "correct horse battery staple", 8 | "options": {"memlimit": 67108864, "opslimit": 2}, 9 | "paserk": "k4.local-pw.-0q-gj9oN18gifSrvpClFwAAAAAEAAAAAAAAAgAAAAH1hyLMFQGs5F1aZoysb7bRtc91SYXu2-bi-mmISIF5cs-SQHp1MoppBFc9I1LTkZA4KsVR_ipH3XdGLj3Pe77qCE64HI1cPG1LNDF0vINnGOrLEaE1Clfi" 10 | }, 11 | { 12 | "name": "k4.local-pw-2", 13 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "password": "correct horse battery staple", 15 | "options": {"memlimit": 268435456, "opslimit": 3}, 16 | "paserk": "k4.local-pw.3oPc6UhC5SCQjL0sCCeTgQAAAAAQAAAAAAAAAwAAAAHimvu_i1YAd7f8VZSilxXd4gXM-sefO6VyEV7qmuDJXx3xuMcg45tjWQit-wOugj-Q-CzhMGYEFNImI2s0gMA8SZE0d_-HbmRM6MsC0XqzlxWpSI8rTyO-" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /pyseto/__init__.py: -------------------------------------------------------------------------------- 1 | from .exceptions import ( 2 | DecryptError, 3 | EncryptError, 4 | NotSupportedError, 5 | PysetoError, 6 | SignError, 7 | VerifyError, 8 | ) 9 | from .key import Key 10 | from .key_interface import KeyInterface 11 | from .paseto import Paseto 12 | from .pyseto import decode, encode 13 | from .token import Token 14 | 15 | __version__ = "1.8.5" 16 | __title__ = "PySETO" 17 | __description__ = "A Python implementation of PASETO/PASERK." 18 | __url__ = "https://pyseto.readthedocs.io" 19 | __uri__ = __url__ 20 | __doc__ = __description__ + " <" + __uri__ + ">" 21 | __author__ = "AJITOMI Daisuke" 22 | __email__ = "ajitomi@gmail.com" 23 | __license__ = "MIT" 24 | __copyright__ = "Copyright 2021-2022 Ajitomi Daisuke" 25 | __all__ = [ 26 | "encode", 27 | "decode", 28 | "Key", 29 | "KeyInterface", 30 | "Paseto", 31 | "PysetoError", 32 | "Token", 33 | "DecryptError", 34 | "EncryptError", 35 | "NotSupportedError", 36 | "SignError", 37 | "VerifyError", 38 | ] 39 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.public.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.public Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.public-1", 6 | "key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", 7 | "paserk": "k1.public.MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh_uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW-gmLbgYO_SZYfWF_M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB_0AIELh0mE5vwdihOCbdV6alUyhKC1-1w_FW6HWcp_JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQAB" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | check 4 | build 5 | build_docs 6 | py{39,310,311,312,313} 7 | isolated_build = True 8 | skip_missing_interpreters = True 9 | 10 | 11 | [gh-actions] 12 | python = 13 | 3.9: py39 14 | 3.10: py310 15 | 3.11: py311 16 | 3.12: check, build, build_docs, py312 17 | 3.13: py313 18 | 19 | 20 | [testenv:check] 21 | allowlist_externals = poetry 22 | skip_install = true 23 | commands = 24 | poetry install --no-root --only main,dev 25 | poetry run pre-commit run --all-files 26 | 27 | 28 | [testenv:build] 29 | allowlist_externals = poetry 30 | skip_install = true 31 | commands = 32 | poetry build 33 | 34 | 35 | [testenv:build_docs] 36 | allowlist_externals = poetry 37 | skip_install = true 38 | commands = 39 | poetry install --only main,docs 40 | poetry run sphinx-build -n -T -W -b html -d {envtmpdir}/doctrees docs docs/_build/html 41 | 42 | 43 | [testenv] 44 | allowlist_externals = poetry 45 | skip_install = true 46 | commands = 47 | poetry install --only main,dev 48 | poetry run pytest -ra --cov=pyseto --cov-report=term --cov-report=xml tests 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Ajitomi Daisuke 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.secret-wrap.pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.secret-wrap.pie Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.secret-wrap.pie-1", 6 | "unwrapped": "00000000000000000000000000000000000000000000000000000000000000003b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 7 | "wrapping-key":"707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 8 | "paserk": "k2.secret-wrap.pie.7POlSwAxJP-yZYTi1oIsPC9kI34Ui3oiP0c_mgvZYuZKSbXHRD3g64yyzkDjDysonw-X3_TGKXksOAFhB5VF-tIru8xS8jy9c6xdFaYq459hXBmXLONWJSmtavF-VyXhQeffX0igJRZzxBOelMM9wvHlTKNHiJbEYWGAQRCWsIg" 9 | }, 10 | { 11 | "name": "k2.secret-wrap.pie-2", 12 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 13 | "wrapping-key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 14 | "paserk": "k2.secret-wrap.pie.MMdHghvp1yDr2nqeFmrrlsiZC9O4MMfobSKML62CTzofArLMxqRNUA7ONGlUea5IwMFc6G7Nka2PqrBDaXW1yREpuyFcmfgdTmTIwWGHb-SgrgHe5RDg221beOvbo2hxTzjBnXay_hfPsJPA97PPWdYH_9vAa06piaEux94TUoc" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.secret-wrap.pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.secret-wrap.pie Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.secret-wrap.pie-1", 6 | "unwrapped": "00000000000000000000000000000000000000000000000000000000000000003b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 7 | "wrapping-key":"707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 8 | "paserk": "k4.secret-wrap.pie.NC6xj8t0VuK-0KE7Fy6PAKtbQwEFRyQMe39A0ctrkaIcS1zjVgvYTN6cu1AZM7bU2bz-jzKclAWu3Bln6xhSOsUqcQPi6Kw_LtKXLRCeggiuPnaqWfIT4qacjXtXhFvOvDPye21fbWOPuoNM9VppuTzN0LzYDYgNYCPsbWt2n4c" 9 | }, 10 | { 11 | "name": "k4.secret-wrap.pie-2", 12 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 13 | "wrapping-key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 14 | "paserk": "k4.secret-wrap.pie.dYA31PP6a-d1Cyk3xt2Dz8kpGSlbpwkG5UyrLcgRspSvq1RUO1UQicQNE3-eXYUYGhXrG9zAVnR93tize-IPtiFEyO70U3bWEXd0uU7asDJQ19I3V2mf5OPIcKQl-TnY0XXtw5DPqY1yEFEbA9WTiDG0I3z6KTWA2z09NWm0OHQ" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.sid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.sid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.sid-1", 6 | "key": "00000000000000000000000000000000000000000000000000000000000000003b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 7 | "seed": "0000000000000000000000000000000000000000000000000000000000000000", 8 | "paserk": "k2.sid.72HIqql6GuWauqz7l11ZvNufO04l7Lwk1X_uPpbsx9E8" 9 | }, 10 | { 11 | "name": "k2.sid-2", 12 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 13 | "seed": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "paserk": "k2.sid.9wfgiRJhydmagHQ9kKOOxQm3OXRTCPxkelCzxw1sJRkV" 15 | }, 16 | { 17 | "name": "k2.sid-3", 18 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e9060fe37571a5d6e7d30b15154ce4a9fb92c70c870848f4ccdf1626588097f73f7", 19 | "seed": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 20 | "paserk": "k2.sid.t22mHkArR7jNtHWm6VzMQ6nMPP3Ab0AyX3rymFdIVG0T" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.sid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.sid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.sid-1", 6 | "key": "00000000000000000000000000000000000000000000000000000000000000003b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 7 | "seed": "0000000000000000000000000000000000000000000000000000000000000000", 8 | "paserk": "k4.sid.YujQ-NvcGquQ0Q-arRf8iYEcXiSOKg2Vk5az-n1lxiUd" 9 | }, 10 | { 11 | "name": "k4.sid-2", 12 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 13 | "seed": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 14 | "paserk": "k4.sid.gHYyx8y5YzqKEZeYoMDqUOKejdSnY_AWhYZiSCMjR1V5" 15 | }, 16 | { 17 | "name": "k4.sid-3", 18 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e9060fe37571a5d6e7d30b15154ce4a9fb92c70c870848f4ccdf1626588097f73f7", 19 | "seed": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 20 | "paserk": "k4.sid.2_m4h6ZTO3qm_PIpl-eYyAqTbNTgmIPQ85POmUEyZHNd" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "pyseto" 7 | version = "1.8.5" 8 | description = "A Python implementation of PASETO/PASERK." 9 | authors = ["Ajitomi Daisuke "] 10 | license = "MIT" 11 | readme = "README.md" 12 | repository = "https://github.com/dajiaji/pyseto" 13 | 14 | include = [ 15 | "CHANGES.rst", 16 | "docs", 17 | "poetry.lock", 18 | "tests", 19 | "tox.ini", 20 | ] 21 | 22 | exclude = [ 23 | "docs/_build", 24 | ] 25 | 26 | [tool.poetry.dependencies] 27 | python = ">=3.9.2,<4.0" 28 | cryptography = ">=42.0.1,<47" 29 | pycryptodomex = ">=3.18.0" 30 | iso8601 = ">=1.0.2,<3.0.0" 31 | argon2-cffi = ">=23.1.0" 32 | 33 | [tool.poetry.group.dev] 34 | optional = true 35 | 36 | [tool.poetry.group.dev.dependencies] 37 | pytest = "^8.3.5" 38 | pytest-cov = ">=6,<8" 39 | tox = "^4.24.2" 40 | pre-commit = "^4.1.0" 41 | freezegun = "^1.5.1" 42 | 43 | [tool.poetry.group.docs] 44 | optional = true 45 | 46 | [tool.poetry.group.docs.dependencies] 47 | sphinx = ">=7.1,<8.0.0" 48 | sphinx-rtd-theme = "^3.0.2" 49 | sphinx-autodoc-typehints = ">=2.3.0,<3.0.0" 50 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black-pre-commit-mirror 3 | rev: 25.12.0 4 | hooks: 5 | - id: black 6 | args: [--line-length, "128"] 7 | 8 | - repo: https://github.com/asottile/blacken-docs 9 | rev: 1.20.0 10 | hooks: 11 | - id: blacken-docs 12 | 13 | - repo: https://github.com/PyCQA/flake8 14 | rev: 7.3.0 15 | hooks: 16 | - id: flake8 17 | args: [--ignore, "E203,E501,W503"] 18 | additional_dependencies: [flake8-bugbear] 19 | 20 | - repo: https://github.com/PyCQA/isort 21 | rev: 7.0.0 22 | hooks: 23 | - id: isort 24 | args: [--profile, "black"] 25 | 26 | - repo: https://github.com/pre-commit/mirrors-mypy 27 | rev: v1.19.0 28 | hooks: 29 | - id: mypy 30 | args: [--ignore-missing-imports] 31 | additional_dependencies: [types-freezegun] 32 | 33 | - repo: https://github.com/pre-commit/pre-commit-hooks 34 | rev: v6.0.0 35 | hooks: 36 | - id: check-json 37 | - id: check-toml 38 | - id: check-yaml 39 | - id: debug-statements 40 | - id: end-of-file-fixer 41 | - id: fix-byte-order-marker 42 | - id: trailing-whitespace 43 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.seal.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.seal Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.seal-1", 6 | "sealing-secret-key": "407796f4bc4b8184e9fe0c54b336822d34823092ad873d87ba14c3efb9db8c1db7715bd661458d928654d3e832f53ff5c9480542e0e3d4c9b032c768c7ce6023", 7 | "sealing-public-key": "b7715bd661458d928654d3e832f53ff5c9480542e0e3d4c9b032c768c7ce6023", 8 | "unsealed": "0000000000000000000000000000000000000000000000000000000000000000", 9 | "paserk": "k2.seal.10ES-djeXd_5XMrznfTJGDf1zJKWQ0pjvcGyb--vQf-ApdP6zthkiyIX5lR3zfpVzVuZJ4WwWIIXuCtqiTFpeEQmrTch_G91TvQfxOLU2nEBW9_4VeIiFHrQuwL3IdxB" 10 | }, 11 | { 12 | "name": "k2.seal-2", 13 | "sealing-secret-key": "a770cf90f55d8a6dec51190eb640cb25ce31f7e5eb87a00ca9859022e6da9518a0fbc3dc2f99a538b40fb7616a83cf4276b6cf223fff5a2c2d3236235eb87dc7", 14 | "sealing-public-key": "a0fbc3dc2f99a538b40fb7616a83cf4276b6cf223fff5a2c2d3236235eb87dc7", 15 | "unsealed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16 | "paserk": "k2.seal.rJZgCJrVrUFRPehByeSoO1wAUxG072_yZcyHKARrNU_ShbDBbW6xKtQl7se_PZZ9z1z484vcWmp0iTWGcVplCj2oZClme5JBCQeYSqc2lDev3xTFgOiRxX71gnnxSBkO" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.seal.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.seal Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.seal-1", 6 | "sealing-secret-key": "407796f4bc4b8184e9fe0c54b336822d34823092ad873d87ba14c3efb9db8c1db7715bd661458d928654d3e832f53ff5c9480542e0e3d4c9b032c768c7ce6023", 7 | "sealing-public-key": "b7715bd661458d928654d3e832f53ff5c9480542e0e3d4c9b032c768c7ce6023", 8 | "unsealed": "0000000000000000000000000000000000000000000000000000000000000000", 9 | "paserk": "k4.seal.OPFn-AEUsKUWtAUZrutVvd9YaZ4CmV4_lk6ii8N72l5gTnl8RlL_zRFqWTZZV9gSnPzARQ_QklrZ2Qs6cJGKOENNOnsDXL5haXcr-QbTXgoLVBvT4ruJ8MdjWXGRTVc9" 10 | }, 11 | { 12 | "name": "k4.seal-2", 13 | "sealing-secret-key": "a770cf90f55d8a6dec51190eb640cb25ce31f7e5eb87a00ca9859022e6da9518a0fbc3dc2f99a538b40fb7616a83cf4276b6cf223fff5a2c2d3236235eb87dc7", 14 | "sealing-public-key": "a0fbc3dc2f99a538b40fb7616a83cf4276b6cf223fff5a2c2d3236235eb87dc7", 15 | "unsealed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16 | "paserk": "k4.seal.3-VOL4pX5b7eV3uMhYHfOhJNN77YyYtd7wYXrH9rRucKNmq0aO-6AWIFU4xOXUCBk0mzBZeWAPAKrvejqixqeRXm-MQXt8yFGHmM1RzpdJw80nabbyDIsNCpBwltU-uj" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.secret-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.secret-pw Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.secret-pw-1", 6 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 7 | "password": "correct horse battery staple", 8 | "options": {"memlimit": 67108864, "opslimit": 2}, 9 | "paserk": "k2.secret-pw.7CmJPtrh6L_Uv5z4yOTRowAAAAAEAAAAAAAAAgAAAAGU9OiNm2lT7p43UF7NTwnwXJPJ7qN-Q3CuEXkj1mDS6wntUBBShjBvCywheKdrO1586sHzSjCVDveEs9Yv1hs98-rH5DEA1rrDacfmo9TFU48Me8hqN79TpMo38pxGrDhCXA1uRtGeUkgemaH67K_WHJ2NE8DpoRU" 10 | }, 11 | { 12 | "name": "k2.secret-pw-2", 13 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 14 | "password": "correct horse battery staple", 15 | "options": {"memlimit": 268435456, "opslimit": 3}, 16 | "paserk": "k2.secret-pw.mCUvOp7SWBr-qZm45vIR7QAAAAAQAAAAAAAAAwAAAAGpO-GFtnY-prgs4UDG40g9WQdnZlqrBNV2i1sCVfdY3tXq36diLbxmDOA72VvDfw-1_7s0PUZV-heFYLwMFvVpcmRuFXFbVVXB0yfwVAwH0VbFYty7GM4PZtiOB-zjwQbMltRqyI_uK1OziAbJGJkS8uuU7hgQdlI" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.secret-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.secret-pw Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.secret-pw-1", 6 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 7 | "password": "correct horse battery staple", 8 | "options": {"memlimit": 67108864, "opslimit": 2}, 9 | "paserk": "k4.secret-pw.Stkwnh1lHUA7p3t2GDRxdQAAAAAEAAAAAAAAAgAAAAEUtfYRjsLAnE5hGX0Ni8H_W2XdVz5laZ9MdByIYgnDQnXEEx7NyXzBHhKdNVa12XhSLNTNMLuSo5kDMsJUHlEMt8yIE-F7GMDvBXTFvNFniK1Ao0TreYqIYTSKfIvfcZhwiWuHqFGddVhOvTrNt8zi53IeF-g089U" 10 | }, 11 | { 12 | "name": "k4.secret-pw-2", 13 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 14 | "password": "correct horse battery staple", 15 | "options": {"memlimit": 268435456, "opslimit": 3}, 16 | "paserk": "k4.secret-pw.8SqqKhga2erPtJdHMtSD3QAAAAAQAAAAAAAAAwAAAAFgsqMCqzX86kHsjfVlP05h7FBHA-438QAYiiTY4IhpGLDnZLmxLrB4A6P_cC_o2zZR_kxzf5NgsmrsAe-FgrI4e0zd2FhVC3G9d6huc8aKqe-wcUSTLpQsCFTnkuVHM2_sIXQaPoKQl14g-ZjmGEMjtVXiDX6Tb2k" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k2.secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k2.secret Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k2.secret-1", 6 | "key": "00000000000000000000000000000000000000000000000000000000000000003b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 7 | "secret-key-seed": "0000000000000000000000000000000000000000000000000000000000000000", 8 | "public-key": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 9 | "paserk": "k2.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7aie8zrakLWKjqNAqbw1zZTIVdx3iQ6Y6wEihi1naKQ" 10 | }, 11 | { 12 | "name": "k2.secret-2", 13 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 14 | "secret-key-seed": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 15 | "public-key": "1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 16 | "paserk": "k2.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ" 17 | }, 18 | { 19 | "name": "k2.secret-3", 20 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e9060fe37571a5d6e7d30b15154ce4a9fb92c70c870848f4ccdf1626588097f73f7", 21 | "secret-key-seed": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 22 | "public-key": "60fe37571a5d6e7d30b15154ce4a9fb92c70c870848f4ccdf1626588097f73f7", 23 | "paserk": "k2.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpBg_jdXGl1ufTCxUVTOSp-5LHDIcISPTM3xYmWICX9z9w" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k4.secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k4.secret Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k4.secret-1", 6 | "key": "00000000000000000000000000000000000000000000000000000000000000003b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 7 | "secret-key-seed": "0000000000000000000000000000000000000000000000000000000000000000", 8 | "public-key": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", 9 | "paserk": "k4.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7aie8zrakLWKjqNAqbw1zZTIVdx3iQ6Y6wEihi1naKQ" 10 | }, 11 | { 12 | "name": "k4.secret-2", 13 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 14 | "secret-key-seed": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 15 | "public-key": "1ce56a48c82ff99162a14bc544612674e5d61fb9317e65d4055780fdbcb4dc35", 16 | "paserk": "k4.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo8c5WpIyC_5kWKhS8VEYSZ05dYfuTF-ZdQFV4D9vLTcNQ" 17 | }, 18 | { 19 | "name": "k4.secret-3", 20 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e9060fe37571a5d6e7d30b15154ce4a9fb92c70c870848f4ccdf1626588097f73f7", 21 | "secret-key-seed": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e90", 22 | "public-key": "60fe37571a5d6e7d30b15154ce4a9fb92c70c870848f4ccdf1626588097f73f7", 23 | "paserk": "k4.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjpBg_jdXGl1ufTCxUVTOSp-5LHDIcISPTM3xYmWICX9z9w" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tests/keys/private_key_rsa.json: -------------------------------------------------------------------------------- 1 | { 2 | "p": "522lC3SOUd0cPamIQu1mzFIgSJO0zXaS2VhFw9rzvFN3EWzSCawfhURSA7Luf_zy8hNle7fc1ak7rztPbcJYU4kaUs-3s2uSh0OkjFFY-O22MTlSFy08SnUUWjOr6SANdZwXBvwFp_bHo2L5exaoVDbBWWFydAknMdXxesj7uVM", 3 | "kty": "RSA", 4 | "q": "tOU_OTANjWxYkBlXsM0zZnHe2Y-cr6LWoEdXOPf-Qz3qBtODsBBCGEMIWeIMZiYVXWgupTFxUbSvi2XzOSLW_kmzX66Onhg_HxFr6f0GwUC2r0o7DTxkGmImpoGhs-nLY-0lSTJsYWeCRqWvF8Z4lBIScwwJQPNKUrLjM2FeSmc", 5 | "d": "nGQfN_meid33UvmAi811LoiLfIWp5RCN5tC2hGDNJkfSWziFZbeu4B5esnBdoOJET409_brhk6B0eiKmzA10ABY8We1fb9jj9R4u7miL7V6uYUXpysg7MQlWm87wpwkRe1cfKK3yOfuaIUD3cJh9V3TN8Qgw1FJ7EM-40Bpfq-pQiebd9kScYbDBgU6UpdEqGvvN_F1-t3e9cnKF8R3Az6L3MyCZzMP_1VU2ec06D-_js232SZr5wBaUoYGBw6v5EQLlm4uRfBVh830Nj13nYUMBdxFta_vxuVjyT7nQ4lKnr9UegsLNuIdNjzQG-u7YHO51Wz_4XpzhR14oWRQ7xQ", 6 | "e": "AQAB", 7 | "use": "sig", 8 | "kid": "RS256-01", 9 | "qi": "1Zo1K-2F34u9NGVS1FvWT1AYTCCpJgpmnRoQ-sFP20W5q_W2pXKniSFn1de7_swUrDf_gQmTOuWHlMwDfD_vJwaUElRlR6qVUjHaLTjWKQMYUnaVLJEUa6sgON1mYJ3agH-AYZqv1mWGpOt2yEvTisX6lkK5wWWyqKOJhHyTkK8", 10 | "dp": "CBnIayw7RxZMRMRIZr9Ul9ZQFvpEm-SIo175oi9p1K-_PTbn6zrBJ7MKg-Khgo1iG6MeLER1UG1KD4ot75Ob7-CesUNgFMGxMVbmzZqTWLNJa1OsUe9dauXKPpYMcG6Uygcarz3nHMgAmPF_9hUG81uvTOeiT_l6C76HY1rhpM0", 11 | "alg": "RS256", 12 | "dq": "X-RqiHE9retyYyjcAGA20Caq4J-tirmClsJarVthENogVfAIDewAbYYTRjp7IicsCjDxESbNkGd86yNnNLGQUIpXKPCKr6ngxCJjF03HJ-ibLv7loNWTpxzCql9rjcjwxY7vxgaRx2ysdbDcyXivcKbH2u7VdPXDP2WO5SzHZB8", 13 | "n": "o4hWSF-rtXeKI8J32sX3agnJZMyprxQWdmfkaL3jsLK13Yelk7ChlaKIPWU0GHU7Wh_Ce3aslUIb7KsU51Fo3olAVBVszjTYZFu5IIgKtZoni-r8NVHqcmT6JLGhoVGVRaPjEGudmCtxocgiWXGHucl14DChV6PAcPlcbcosNmeJ5z6ia8lp5det-KkjFTe0-EdWB2kTzZvEmbotbtPL-B0BH_jYqmmZWgCU_TZ76QgORiOPmN-GW5RrLOEkay-f-Sx6XGTZxC_heN6eT-Ql1KgGQnagRDOAmcKy4CJp2vmODR8LbOyZl22C8x5wR5Vzz-9NE_WIF0nHT1ivIFOOZQ" 14 | } 15 | -------------------------------------------------------------------------------- /tests/keys/private_key_rsa_2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEAyT53fjKCUzdfOqlUNYcycGAWbiSTg8TOHr/9KjliYusJR/V5 3 | j+3WCDV1itik8fPKTs2dtIz+bQ+WvFFGIhP0ftr/y6dYGbJpKxJoqAdwA01FSsr4 4 | Xu8QOyKWp9jwJVNOQtyYr2gQAbJPLSRHxynZt8qU46is+kTWU8Y7Nw44kMZMv9yI 5 | 36mdYA00Ve2Z9/BxDDlvQQim72aCJHmpMn1NRvMpB0IWY5q6k/sCrWfNwggzT+bU 6 | 5n0U9C7SUbnx5uoEq+f/sQnTCj3LJI8jfQP2GJ9KWsjeh89abSusLvYAOSo+JiFI 7 | vhr5VrQ4km+/fYzdfJyLy4wF/QNhSOnsegShhwIDAQABAoIBAQCrbj80japfYUgK 8 | HLzTbHwXJoN1CEtxcHF12G69TWz2funFFuObLCurin667Jp2UgsA3j990q3aEfGC 9 | MmngFcetWLmWrtNp8IWOX3wvx0YwSd7BO+4gUN4uweQ8z5yi7zg31NtscOVRzKeN 10 | +N38VzZUTopO9R2FhKZfbvp6h+6/w+ozLNK4xs5Rf9N057KqOl8lLIZMfcB1L23x 11 | iCbQFcrLtxNxkjy0DLotWALRx77AsXZengUAzs7JjJdAh76RT6RFtqroHD9tIOJt 12 | 09ztA/DsYhMRZ7pWU9TcC6e4HszfB/fpjWa6UiWAP11hKUIObaGrQl4L4efUgxFX 13 | RqTHSaBBAoGBAOnrcB1gMGkg5VOKvnuQKOZz9xR6+FFUg6vsEj5kNCFIbdEIQtm+ 14 | kD8Z4qI+QTJH9JmdKCcEyJBaWHwHlZdQmKGKoKFww0hr6KhWZtEE6DVsDbcUiTyl 15 | 0RA2CI6UYmImrAr33lWdtQe6c5FLpJ3dMO2XzH3+kmc32ZsnX/HipjUZAoGBANw9 16 | b60U75HKLb5Ak7oVeVMYA1oghs0lIXE0zKCHUVIjhsvu50DfCC3q0MAPuUSWq7vQ 17 | ZvNwqfkgV6UU+RbO92TUYBvXEcYFpuN1MCQ+QOxO7+SDhDuZHEO5DTKuC1mOaHN7 18 | IIU45jE1qVemuokPBKWWCI9Nt9jPyczClQ6mDL+fAoGBAN5ToJa7AqgY1P4HI65r 19 | QQRv+cmkavlZ+1/fGyPjt0xe+8kuMeOk5JKk9qu/wdWZYFjUbwJO4JWa2PjheIN9 20 | 3ePDz5JBi8n8jf/ZRxWotU95KLv2r81NyJvAz3c8fxnMqb4vAkmbgCEdTJQvMgG8 21 | 5rEbkwe5esrR4Wg/D0jmY5SZAoGBAMqq7l2D6oFqX3W3zzlqEV9ZtZv2WpXlGUUn 22 | pTYDz92aO6zmh3+2vbwjXT2gOojXbelC+boDbyaFkKGlDyLiCd6LYHW+D+j9wnmF 23 | mWuArhXG/hJ2w/RkDRJ086QzbNZYqd267E4r8ghOrSqvjvAZFdmamv3kMs4dSEJY 24 | vPnh69FZAoGBAJDJOMvazfl2F5+0weJpGwgthpwKQ3otSGrRbBVtbC8LEyl6DFz6 25 | uxCBazJBSYthUgoFkiP+207LhnQ5TYP6z/WGx/K8H6ixdYVMw2pASZliUobSHGMH 26 | FQsOdNpMzK1qPuhA/iVmga+0yit/SIVXXdsObUOcA4QOQ8ValYjIGHMQ 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/keys/private_key_rsa.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDL1McprDim+OJD 3 | X8pFlgrD/oWp7ytre/fZmTaXfCdgajL1WHVGr/sGsV9wqs8Yv8a1oMC5K47tM5aB 4 | PMVIjdIl6Z1NCkH2FLSqvzKjZtsRMOeMzP72Pkvxyk2wz1K/YQcRlRhxsoE8Mav9 5 | Yxza5GkjxQrsgCIx0gBv5Ue/XnAXza1qhJ8EFJSQSahXZnU85d9A3ooExpHRU39S 6 | o/Q322DwFqIPR7ZBdYvX+PCB20mZCI4QVsDYyaORUFOULM+xZQq8gRgjW278SCN1 7 | AFXO08+XIIE/oYqIxBaH3PljI3ClGA5Ikv172bjB3Fag4csf1Z1tUsLsjELnHsOI 8 | mda94u8XAgMBAAECggEAY/hdIjw8oPAqkR7XJx/68bvQtNL/+byYiBnUI6IZ8fjE 9 | O38CLgQ720F9bNXpstrES+qm+2q4s+/8XjB/FiGwszWMF+/zAA2RurkBzhCxWxIg 10 | TvdwCTfuqY0uY4ybqg+dlusITSNerTYzg5hr25PxWUDYJbyGiObVngvXD63yzkkK 11 | V7/rtkIGwehw17YlO17jzq46ljD3KAokZRvRfn+yN/FwFN7Ps691rAUQ7qFe83CD 12 | 991OSxW0apPHAWDSNUq0iFOJRcCLbaYLVjdcyQxgtQjJAwzp0NOpwRm9fpiD1noY 13 | wZ0ECch0a9dVKxzCxiImDjULTG/nWOUqKuNt9YMMkQKBgQD5YzXRAOQrXmYWPEeS 14 | Gp+H6TLVm5GwcN96VzUAiHsu/+XOvBXNFn/aA6LTw9RDqdOd8tyE5BJ4JruMh2hh 15 | JraFK9Q53sMrpK2osNl26yNz2Gwcaas/Q4NHIxZ7S3+uLVk5yf6Pt9QSgK7FIit5 16 | 0Unt0NyuUAeLzDM68XkSwy/MSQKBgQDRPFc6Nyq7b5Zv+iwHamZE4mcIkUl7ReO3 17 | gqbFButFlt1Zrp6IZ3zrZHJMd6rcxJPawnEtavJ5/7ic3HTi9u08Ig6aShd4Gtjf 18 | c0Bzy/QjL7c4jAl7Oe6Srs7+aYeYEsHay+6UqFNtoBNt170o8axcyVT59Wpg8JfV 19 | x+3btrUgXwKBgFuhqFRzD2Mf/EKPQ3zba2J0vMjfsFg0IOjCwia//wL142pikWAZ 20 | tEBWta05dvSloauXYI8zfdXH5YFs/10y7D1iLUhWIUAX8fbZhA29t/kIvDB0YweL 21 | tuCcc9Y5l2BYKUCih3YnBTUHMzAbrf3EeHV5GJyzqxIipXAy6VmLfjSBAoGAYgBJ 22 | OtMsCK6zX69lf/OuAwWoDLNAH6UUi5d+VTpwaB/JfX/0cR9Hu1lIz9gdB1prhkMc 23 | j5FQESRda9s2RDgz1b596HZbl727Zq2supAEwZZP0wSETFtOCxYljbQP9oKSmdB6 24 | 5tYQfYBEmWY7Czpm6O7EBm+Ua+NEurTyC78ABzECgYEA3LAbZmtIk/ZYqpebGuGM 25 | e6si4Kme8GNb1i0PseDAgYufT8foTphcandADuHRohwLjWgQUZCI7/MRsc+bKdgX 26 | O3onNO+NVw1bAkHUK4r2zLSvHN0/5EYQW1KFoOUf1Ns2OM2xX/6UQCSghq8b6jP0 27 | lx1zZBksUMQsRWgwTPlKqXE= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Python CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | name: Python ${{ matrix.python-version }} on ${{ matrix.platform }} 12 | runs-on: ${{ matrix.platform }} 13 | env: 14 | USING_COVERAGE: "3.12" 15 | 16 | strategy: 17 | matrix: 18 | platform: [ubuntu-latest, windows-latest] 19 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-python@v4 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | 27 | - name: Install dependencies 28 | run: python -m pip install tox-gh-actions poetry 29 | 30 | - name: Run tox 31 | run: poetry run tox 32 | 33 | - name: Upload coverage to Codecov 34 | if: contains(env.USING_COVERAGE, matrix.python-version) && matrix.platform == 'ubuntu-latest' 35 | uses: codecov/codecov-action@v4 36 | with: 37 | fail_ci_if_error: true 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | verbose: true 40 | 41 | package: 42 | name: Build package 43 | runs-on: ${{ matrix.os }} 44 | 45 | strategy: 46 | matrix: 47 | os: [ubuntu-latest, windows-latest, macos-latest] 48 | 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: actions/setup-python@v4 52 | with: 53 | python-version: "3.12" 54 | 55 | - name: Install poetry 56 | run: python -m pip install poetry 57 | 58 | - name: Build package 59 | run: poetry build 60 | 61 | - name: Show result 62 | run: ls -l dist 63 | 64 | - name: Install package 65 | run: python -m pip install . 66 | 67 | - name: Import package 68 | run: python -c "import pyseto; print(pyseto.__version__)" 69 | -------------------------------------------------------------------------------- /pyseto/utils.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from typing import List 3 | 4 | 5 | def i2osp(x: int, x_len: int) -> bytes: 6 | """ 7 | Integer-to-Octet-String primitive 8 | """ 9 | if x >= 256**x_len: 10 | raise ValueError("integer too large") 11 | digits = [] 12 | while x: 13 | digits.append(int(x % 256)) 14 | x //= 256 15 | for _ in range(x_len - len(digits)): 16 | digits.append(0) 17 | return bytes.fromhex("".join("%.2x" % x for x in digits[::-1])) 18 | 19 | 20 | def os2ip(octet_string: bytes) -> int: 21 | """ 22 | Octet-String-to-Integer primitive 23 | """ 24 | x_len = len(octet_string) 25 | octet_string = octet_string[::-1] 26 | x = 0 27 | for i in range(x_len): 28 | x += octet_string[i] * 256**i 29 | return x 30 | 31 | 32 | def base64url_decode(v: str) -> bytes: 33 | bv = v.encode("ascii") 34 | rem = len(bv) % 4 35 | if rem > 0: 36 | bv += b"=" * (4 - rem) 37 | return base64.urlsafe_b64decode(bv) 38 | 39 | 40 | def base64url_encode(input: bytes) -> bytes: 41 | return base64.urlsafe_b64encode(input).replace(b"=", b"") 42 | 43 | 44 | def _le64(n: int) -> bytes: 45 | s = bytearray(8) 46 | for i in range(8): 47 | if i == 7: 48 | n = n & 127 49 | s[i] = n & 255 50 | n = n >> 8 51 | return bytes(s) 52 | 53 | 54 | def pae(pieces: List[bytes]) -> bytes: 55 | output = _le64(len(pieces)) 56 | for i in range(len(pieces)): 57 | output += _le64(len(pieces[i])) 58 | output += pieces[i] 59 | return output 60 | 61 | 62 | def ec_public_key_compress(x: int, y: int) -> bytes: 63 | bx = x.to_bytes(48, byteorder="big") 64 | by = y.to_bytes((y.bit_length() + 7) // 8, byteorder="big") 65 | s = bytearray(1) 66 | s[0] = 0x02 + (by[len(by) - 1] & 1) 67 | return bytes(s) + bx 68 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.sid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.sid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.sid-1", 6 | "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", 7 | "paserk": "k1.sid.aBIISj44Kjs5PmIPh1q6DtTOTy2sz2ov14JJTch_5DHv" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.seal.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.seal Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.seal-1", 6 | "sealing-secret-key": "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDAhUb6WGhABE1MTj0x7E/5acgyap23kh7hUAVoAavKyfhYcmI3n1Q7L\nJpHxNb792H6gBwYFK4EEACKhZANiAAT5H7mTSOyjfILDtSuavZfalI3doM8pRUlb\nTzNyYLqM9iVmajpc0JRXvKuBtGtYi7Yft+eqFr6BuzGrdb4Z1vkvRcI504m0qKiE\nzjhi6u4sNgzW23rrVkRYkb2oE3SJPko=\n-----END EC PRIVATE KEY-----", 7 | "sealing-public-key": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+R+5k0jso3yCw7Urmr2X2pSN3aDPKUVJ\nW08zcmC6jPYlZmo6XNCUV7yrgbRrWIu2H7fnqha+gbsxq3W+Gdb5L0XCOdOJtKio\nhM44YuruLDYM1tt661ZEWJG9qBN0iT5K\n-----END PUBLIC KEY-----", 8 | "unsealed": "0000000000000000000000000000000000000000000000000000000000000000", 9 | "paserk": "k3.seal.iM5XY53oujutf0gFZItnSmnwiJDrTbn6qerw_uOEJqbCPyAchEEc0ENz0CL2FXhWAoN7PFKofO9UrEjetGydlAWRvzLvUy6VXIAqTSX0Gabd6ScdXngKD0buxx_sEI5X2E3Q57NyXz3FwwB-J5kpcVMQmZaJQ371f9xeKSPLVhlW" 10 | }, 11 | { 12 | "name": "k3.seal-2", 13 | "sealing-secret-key": "-----BEGIN EC PRIVATE KEY-----\nMIGkAgEBBDAhUb6WGhABE1MTj0x7E/5acgyap23kh7hUAVoAavKyfhYcmI3n1Q7L\nJpHxNb792H6gBwYFK4EEACKhZANiAAT5H7mTSOyjfILDtSuavZfalI3doM8pRUlb\nTzNyYLqM9iVmajpc0JRXvKuBtGtYi7Yft+eqFr6BuzGrdb4Z1vkvRcI504m0qKiE\nzjhi6u4sNgzW23rrVkRYkb2oE3SJPko=\n-----END EC PRIVATE KEY-----", 14 | "sealing-public-key": "-----BEGIN PUBLIC KEY-----\nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+R+5k0jso3yCw7Urmr2X2pSN3aDPKUVJ\nW08zcmC6jPYlZmo6XNCUV7yrgbRrWIu2H7fnqha+gbsxq3W+Gdb5L0XCOdOJtKio\nhM44YuruLDYM1tt661ZEWJG9qBN0iT5K\n-----END PUBLIC KEY-----", 15 | "unsealed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16 | "paserk": "k3.seal.utQTzb7zGDXC-apbXsIby7yFZxoFFzUb40J1-zszMIjNkf3WLwEmqj9fuQwA8IKfAyWOvtZAxT4--kzAkYEKk492x2bypDps_a1Gsp-L9we22lWEkNVpqA-DWr9uoTRhSWRh87hFNla5ZzrpqMgeP1UOSDGKuRs62gZxFhCzsLka" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.secret-wrap.pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.secret-wrap.pie Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.secret-wrap.pie-1", 6 | "unwrapped": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", 7 | "wrapping-key":"707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 8 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b3445454143494459674145716f664b4972364c4254654f7363636538794374644734644f324b4c703575590a5766644234494a554b6a685641764a647631557062447055586a6879646771334e6866655370596d4c4739646e70692f6b704c634b666a304862306f6d6852380a36646f7845375877754d414b594c484f485836426e58704448587951366735660a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 9 | "paserk": "k3.secret-wrap.pie.hLBw_r4wY1mTy9hu27ibzOnFfW37jc3SER1fu3x2sh0cyE31abqEFzocZRg-KgAGY7syAoMsBZZi7ZlJ4xFreLdCi7cQWpIH3ejUQ5WIRMRDK2lh4X9knh2Bj26XaBOSqtK62ACF-V2utRiI3QVeOOLhH-GWuD8RovVQvA1Vv5Q" 10 | }, 11 | { 12 | "name": "k3.secret-wrap.pie-2", 13 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 14 | "wrapping-key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 15 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b344545414349445967414547706b49495a4b624a667668356a33562b5562416c637174424f58376b424e6c0a69344e757946727974362b6d584c516e4c734567714e6333575a6f4d2f55516e476a4c5554574b6b2b7559597a454d537a32336a45726974455541626e624c420a4d5441432b2b7137344b564a47754e6977454b4448556239726658676a66646e0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 16 | "paserk": "k3.secret-wrap.pie._9Npd2vzpHCHdGYDQ5QFMc6Nirlv3ubsPPb-xJnh6cLImLhpirH_PDQWRIkmb95m-fc1XpqwY2QQbUkkvb2-_tznGQBt0Yg2-0Lux2MNxObkxDIfxfz5lD_tToF58BIuTh9_Pny0dy27HmJPnUBgy7JP2Je-h3doq6i0V2Q5hRA" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.secret-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.secret-pw Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.secret-pw-1", 6 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 7 | "password": "correct horse battery staple", 8 | "options": {"iterations": 1000}, 9 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b344545414349445967414547706b49495a4b624a667668356a33562b5562416c637174424f58376b424e6c0a69344e757946727974362b6d584c516e4c734567714e6333575a6f4d2f55516e476a4c5554574b6b2b7559597a454d537a32336a45726974455541626e624c420a4d5441432b2b7137344b564a47754e6977454b4448556239726658676a66646e0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 10 | "paserk": "k3.secret-pw.PwKCZfMhhDfxrbbAkRqc32Rc7A1xEGX3RNYr7hB0QlEAAAPoieZcUSyzO8e3ahOdC-Tb6qcnydeSlugJn8QRJTUmicfrnFFHR4Bs_xLrhRPybltSu5QMk7IuE2g7ioexgS4oZGyjTpFbCDprWhLKlGHrPhp6MFeHU3TEFyCvG44rqwdEnZKVBfLCSMw5UXd88KVxxQ" 11 | }, 12 | { 13 | "name": "k3.secret-pw-2", 14 | "unwrapped": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 15 | "password": "correct horse battery staple", 16 | "options": {"iterations": 10000}, 17 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b344545414349445967414547706b49495a4b624a667668356a33562b5562416c637174424f58376b424e6c0a69344e757946727974362b6d584c516e4c734567714e6333575a6f4d2f55516e476a4c5554574b6b2b7559597a454d537a32336a45726974455541626e624c420a4d5441432b2b7137344b564a47754e6977454b4448556239726658676a66646e0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 18 | "paserk": "k3.secret-pw.xUi0yR1HF4Iwlqi8TXU_jMBHih9E_QNP_05BuEHeF14AACcQ1weD6WANOvNS_pPIuykjolPg-4tFmQ1VVD2ZkcCt0GGNQSySr6TClSGVuetwsCcFq2D5KSNT2Ztknb66nmg3d6HsAPQ1k9C1GmhCHJxPw3OSU-T_VrklxP2NEmyR1_eybOUmUSLWfSeLpcmOg-Su-A" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.sid.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.sid Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.sid-1", 6 | "key": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", 7 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b3445454143494459674145716f664b4972364c4254654f7363636538794374644734644f324b4c703575590a5766644234494a554b6a685641764a647631557062447055586a6879646771334e6866655370596d4c4739646e70692f6b704c634b666a304862306f6d6852380a36646f7845375877754d414b594c484f485836426e58704448587951366735660a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 8 | "paserk": "k3.sid.DjlX1m4BBFtsnbwzw1zv_x0yRcrZpsvdr_gIxh_hg_Rv" 9 | }, 10 | { 11 | "name": "k3.sid-2", 12 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 13 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b344545414349445967414547706b49495a4b624a667668356a33562b5562416c637174424f58376b424e6c0a69344e757946727974362b6d584c516e4c734567714e6333575a6f4d2f55516e476a4c5554574b6b2b7559597a454d537a32336a45726974455541626e624c420a4d5441432b2b7137344b564a47754e6977454b4448556239726658676a66646e0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 14 | "paserk": "k3.sid.mNalRnF8T60OMPdi1TWSMcub-51v3Au2VB1MOqPrw8zG" 15 | }, 16 | { 17 | "name": "k3.sid-3", 18 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9ea0", 19 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b344545414349445967414575457544387a72334c5934574a4c3963536e6d52384b475445783449774567540a68636c326c34374e316f334345413551303370477557746a79712f6f774164684b39503968746734555439325468784a6c71485a6432555268665636714d6c420a53657a73733262506c2b366f484363475a7866624441587a34684c594e5752610a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 20 | "paserk": "k3.sid.2y01jpWJruAPv3epVJkTtDDvdHLsU3luYV9cvGgsR4C6" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k3.secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k3.secret Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k3.secret-1", 6 | "key": "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", 7 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b3445454143494459674145716f664b4972364c4254654f7363636538794374644734644f324b4c703575590a5766644234494a554b6a685641764a647631557062447055586a6879646771334e6866655370596d4c4739646e70692f6b704c634b666a304862306f6d6852380a36646f7845375877754d414b594c484f485836426e58704448587951366735660a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 8 | "paserk": "k3.secret.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB" 9 | }, 10 | { 11 | "name": "k3.secret-2", 12 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f", 13 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b344545414349445967414547706b49495a4b624a667668356a33562b5562416c637174424f58376b424e6c0a69344e757946727974362b6d584c516e4c734567714e6333575a6f4d2f55516e476a4c5554574b6b2b7559597a454d537a32336a45726974455541626e624c420a4d5441432b2b7137344b564a47754e6977454b4448556239726658676a66646e0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 14 | "paserk": "k3.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo-QkZKTlJWWl5iZmpucnZ6f" 15 | }, 16 | { 17 | "name": "k3.secret-3", 18 | "key": "707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9ea0", 19 | "public-key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d485977454159484b6f5a497a6a3043415159464b344545414349445967414575457544387a72334c5934574a4c3963536e6d52384b475445783449774567540a68636c326c34374e316f334345413551303370477557746a79712f6f774164684b39503968746734555439325468784a6c71485a6432555268665636714d6c420a53657a73733262506c2b366f484363475a7866624441587a34684c594e5752610a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d", 20 | "paserk": "k3.secret.cHFyc3R1dnd4eXp7fH1-f4CBgoOEhYaHiImKi4yNjo-QkZKTlJWWl5iZmpucnZ6g" 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /tests/test_key_interface.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | 3 | import pytest 4 | 5 | from pyseto import NotSupportedError 6 | from pyseto.key_interface import KeyInterface 7 | 8 | 9 | class TestKeyInterface: 10 | """ 11 | Tests for KeyInterface. 12 | """ 13 | 14 | @pytest.mark.parametrize( 15 | "version, purpose, key", 16 | [ 17 | (1, "local", token_bytes(32)), 18 | (2, "local", token_bytes(32)), 19 | (3, "local", token_bytes(32)), 20 | (4, "local", token_bytes(32)), 21 | ], 22 | ) 23 | def test_key_interface_constructor_local(self, version, purpose, key): 24 | k = KeyInterface(version, purpose, key) 25 | assert isinstance(k, KeyInterface) 26 | assert k.version == version 27 | assert k.purpose == purpose 28 | assert k.is_secret is True 29 | with pytest.raises(NotSupportedError) as err: 30 | k.encrypt(b"Hello world!") 31 | pytest.fail("KeyInterface.encrypt() should fail.") 32 | assert "A key for public does not have encrypt()." in str(err.value) 33 | with pytest.raises(NotSupportedError) as err: 34 | k.decrypt(b"xxxxxx") 35 | pytest.fail("KeyInterface.decrypt() should fail.") 36 | assert "A key for public does not have decrypt()." in str(err.value) 37 | with pytest.raises(NotSupportedError) as err: 38 | k.sign(b"Hello world!") 39 | pytest.fail("KeyInterface.sign() should fail.") 40 | assert "A key for local does not have sign()." in str(err.value) 41 | with pytest.raises(NotSupportedError) as err: 42 | k.verify(b"xxxxxx") 43 | pytest.fail("KeyInterface.verify() should fail.") 44 | assert "A key for local does not have verify()." in str(err.value) 45 | with pytest.raises(NotImplementedError) as err: 46 | k.to_paserk() 47 | pytest.fail("KeyInterface.to_paserk() should fail.") 48 | assert "The PASERK expression for the key is not supported yet." in str(err.value) 49 | with pytest.raises(NotImplementedError) as err: 50 | k.to_paserk_id() 51 | pytest.fail("KeyInterface.to_paserk_id() should fail.") 52 | assert "The PASERK ID for the key is not supported yet." in str(err.value) 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Pycharm 132 | .idea/ 133 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '43 13 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'python' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /pyseto/token.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from .utils import base64url_decode 4 | 5 | 6 | class Token(object): 7 | """ 8 | The parsed token object which is a return value of :func:`pyseto.decode `. 9 | """ 10 | 11 | def __init__( 12 | self, 13 | version: str, 14 | purpose: str, 15 | payload: Union[bytes, dict], 16 | footer: Union[bytes, dict] = b"", 17 | ): 18 | self._version = version 19 | self._purpose = purpose 20 | self._payload = payload 21 | self._footer = footer 22 | self._header = (version + "." + purpose + ".").encode("utf-8") 23 | 24 | @classmethod 25 | def new(cls, token: Union[bytes, str]): 26 | token = token if isinstance(token, str) else token.decode("utf-8") 27 | t = token.split(".") 28 | if len(t) != 3 and len(t) != 4: 29 | raise ValueError("token is invalid.") 30 | if not t[2]: 31 | raise ValueError("Empty payload.") 32 | p = base64url_decode(t[2]) 33 | f = base64url_decode(t[3]) if len(t) == 4 else b"" 34 | return cls(t[0], t[1], p, f) 35 | 36 | @property 37 | def version(self) -> str: 38 | """ 39 | The version of the token. It will be ``"v1"``, ``"v2"``, ``"v3"`` or ``"v4"``. 40 | """ 41 | return self._version 42 | 43 | @property 44 | def purpose(self) -> str: 45 | """ 46 | The purpose of the token. It will be ``"local"`` or ``"public"``. 47 | """ 48 | return self._purpose 49 | 50 | @property 51 | def header(self) -> bytes: 52 | """ 53 | The header of the token. It will be ``".."``. 54 | For example, ``"v1.local."``. 55 | """ 56 | return self._header 57 | 58 | @property 59 | def payload(self) -> Union[bytes, dict]: 60 | """ 61 | The payload of the token which is a decoded binary string. It's not Base64 encoded data. 62 | """ 63 | return self._payload 64 | 65 | @payload.setter 66 | def payload(self, payload: Union[bytes, dict]): 67 | """ 68 | A setter of the payload. 69 | """ 70 | self._payload = payload 71 | return 72 | 73 | @property 74 | def footer(self) -> Union[bytes, dict]: 75 | """ 76 | The footer of the token which is a decoded binary string. It's not Base64 encoded data. 77 | """ 78 | return self._footer 79 | 80 | @footer.setter 81 | def footer(self, footer: Union[bytes, dict]): 82 | """ 83 | A setter of the footer. 84 | """ 85 | self._footer = footer 86 | return 87 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | from pyseto import __version__ as python_pyseto_version 18 | 19 | # -- Project information ----------------------------------------------------- 20 | 21 | project = "PySETO" 22 | copyright = "2021, AJITOMI Daisuke" 23 | author = "AJITOMI Daisuke" 24 | 25 | # The full version, including alpha/beta/rc tags 26 | release = python_pyseto_version 27 | 28 | 29 | # -- General configuration --------------------------------------------------- 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [ 35 | "sphinx.ext.autodoc", 36 | "sphinx.ext.viewcode", 37 | "sphinx.ext.todo", 38 | "sphinx.ext.napoleon", 39 | ] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | # templates_path = ['_templates'] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 48 | 49 | 50 | # -- Options for HTML output ------------------------------------------------- 51 | 52 | # The theme to use for HTML and HTML Help pages. See the documentation for 53 | # a list of builtin themes. 54 | # 55 | html_theme = "sphinx_rtd_theme" 56 | 57 | # Add any paths that contain custom static files (such as style sheets) here, 58 | # relative to this directory. They are copied after the builtin static files, 59 | # so a file named "default.css" will overwrite the builtin "default.css". 60 | # html_static_path = ['_static'] 61 | 62 | # -- Extension configuration ------------------------------------------------- 63 | 64 | # Napoleon settings 65 | napoleon_google_docstring = True 66 | napoleon_numpy_docstring = False 67 | napoleon_include_init_with_doc = True 68 | napoleon_include_private_with_doc = False 69 | napoleon_include_special_with_doc = True 70 | napoleon_use_admonition_for_examples = False 71 | napoleon_use_admonition_for_notes = False 72 | napoleon_use_admonition_for_references = False 73 | napoleon_use_ivar = False 74 | napoleon_use_param = True 75 | napoleon_use_rtype = True 76 | 77 | # -- Options for todo extension ---------------------------------------------- 78 | 79 | # If true, `todo` and `todoList` produce output, else they produce nothing. 80 | todo_include_todos = True 81 | -------------------------------------------------------------------------------- /tests/test_token.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pyseto.token import Token 4 | from pyseto.utils import base64url_decode 5 | 6 | 7 | class TestToken: 8 | """ 9 | Tests for Token. 10 | """ 11 | 12 | def test_token_new(self): 13 | token = Token.new( 14 | b"v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8" 15 | ) 16 | assert token.version == "v1" 17 | assert token.purpose == "local" 18 | assert token.payload == base64url_decode( 19 | "WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8" 20 | ) 21 | assert token.footer == b"" 22 | 23 | def test_token_new_with_str(self): 24 | token = Token.new( 25 | "v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8" 26 | ) 27 | assert token.version == "v1" 28 | assert token.purpose == "local" 29 | assert token.payload == base64url_decode( 30 | "WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8" 31 | ) 32 | assert token.footer == b"" 33 | 34 | def test_token_setter_payload(self): 35 | token = Token.new( 36 | b"v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8" 37 | ) 38 | token.payload = b"updated-payload" 39 | assert token.payload == b"updated-payload" 40 | 41 | def test_token_setter_footer(self): 42 | token = Token.new( 43 | b"v1.local.WzhIh1MpbqVNXNt7-HbWvL-JwAym3Tomad9Pc2nl7wK87vGraUVvn2bs8BBNo7jbukCNrkVID0jCK2vr5bP18G78j1bOTbBcP9HZzqnraEdspcjd_PvrxDEhj9cS2MG5fmxtvuoHRp3M24HvxTtql9z26KTfPWxJN5bAJaAM6gos8fnfjJO8oKiqQMaiBP_Cqncmqw8" 44 | ) 45 | token.footer = b"updated-footer" 46 | assert token.footer == b"updated-footer" 47 | 48 | @pytest.mark.parametrize( 49 | "token, msg", 50 | [ 51 | ("v1", "token is invalid."), 52 | ("v1.", "token is invalid."), 53 | ("v1.local", "token is invalid."), 54 | ("v1.local.", "Empty payload."), 55 | ("v1.local.p.f.x", "token is invalid."), 56 | ("v1.local.p.f.x.y", "token is invalid."), 57 | ], 58 | ) 59 | def test_token_new_with_invalid_token(self, token, msg): 60 | with pytest.raises(ValueError) as err: 61 | Token.new(token) 62 | pytest.fail("Token.new() should fail.") 63 | assert msg in str(err.value) 64 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at ajitomi@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] 44 | 45 | [homepage]: https://www.contributor-covenant.org/ 46 | [version]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 47 | -------------------------------------------------------------------------------- /pyseto/pyseto.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Any, List, Optional, Union 3 | 4 | from .key_interface import KeyInterface 5 | from .paseto import Paseto 6 | from .token import Token 7 | 8 | # export 9 | _paseto = Paseto() 10 | 11 | 12 | def encode( 13 | key: KeyInterface, 14 | payload: Union[bytes, str, dict], 15 | footer: Union[bytes, str] = b"", 16 | implicit_assertion: Union[bytes, str] = b"", 17 | nonce: bytes = b"", 18 | serializer: Any = json, 19 | exp: int = 0, 20 | ) -> bytes: 21 | """ 22 | Encodes a message to a PASETO token with a key for encryption or signing. 23 | 24 | Args: 25 | key (KeyInterface): A key for encryption or signing. 26 | payload (Union[bytes, str, dict]): A message to be encrypted or signed. 27 | footer (Union[bytes, str]): A footer. 28 | implicit_assertion (Union[bytes, str]): An implicit assertion. It is 29 | only used in ``v3`` or ``v4``. 30 | nonce (bytes): A nonce. If omitted(it's recommended), a nonce will be 31 | generated with ``secrets.token_bytes()`` internally. If you don't 32 | want ot use ``secrets.token_bytes()``, you can specify it via this 33 | parameter explicitly. 34 | serializer (Any): A serializer which is used when the type of 35 | ``payload`` is ``object``. It must have a ``dumps()`` function to 36 | serialize the payload. Typically, you can use ``json`` or ``cbor2``. 37 | exp (int): An expiration time (seconds) of the PASETO token. It will be 38 | set in the payload as the registered ``exp`` claim when serializer 39 | is ``json`` and this value > ``0``. If the value <= ``0``, the 40 | ``exp`` claim will not be set. 41 | Returns: 42 | bytes: A PASETO token. 43 | Raise: 44 | ValueError: Invalid arguments. 45 | EncryptError: Failed to encrypt the message. 46 | SignError: Failed to sign the message. 47 | """ 48 | return _paseto.encode(key, payload, footer, implicit_assertion, nonce, serializer, exp) 49 | 50 | 51 | def decode( 52 | keys: Union[KeyInterface, List[KeyInterface]], 53 | token: Union[bytes, str], 54 | implicit_assertion: Union[bytes, str] = b"", 55 | deserializer: Optional[Any] = None, 56 | aud: str = "", 57 | ) -> Token: 58 | """ 59 | Decodes a PASETO token with a key for decryption and/or verifying. 60 | 61 | Args: 62 | keys (KeyInterface): A key for decryption or verifying the signature in the token. 63 | token (Union[bytes, str]): A PASETO token to be decrypted or verified. 64 | implicit_assertion (Union[bytes, str]): An implicit assertion. It is 65 | only used in ``v3`` or ``v4``. 66 | deserializer (Optional[Any]): A deserializer which is used when you want to 67 | deserialize a ``payload`` attribute in the response object. It must have a 68 | ``loads()`` function to deserialize the payload. Typically, you can use 69 | ``json`` or ``cbor2``. 70 | aud (str): An audience claim value for the token verification. 71 | If ``deserializer=json`` and the payload of the token does not 72 | include an ``aud`` value that matches this value, the 73 | verification will fail. 74 | Returns: 75 | Token: A parsed PASETO token object. 76 | Raise: 77 | ValueError: Invalid arguments. 78 | DecryptError: Failed to decrypt the message. 79 | VerifyError: Failed to verify the message. 80 | """ 81 | return _paseto.decode(keys, token, implicit_assertion, deserializer, aud) 82 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.secret.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.secret Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.secret-1", 6 | "key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", 7 | "public-key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyaTgTt53ph3p5GHgwoGW\nwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwx\nKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1\nOt0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAA\npVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6al\nUyhKC1+1w/FW6HWcp/JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8\nowIDAQAB\n-----END PUBLIC KEY-----", 8 | "paserk": "k1.secret.MIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9GCjNFCd723nCHxBtN1qd74MSh_uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N02HnanxFLW-gmLbgYO_SZYfWF_M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNxkRnAwIMjeTJB_0AIELh0mE5vwdihOCbdV6alUyhKC1-1w_FW6HWcp_JG1kKC8DPIidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3qfd7to-C3D5hRzAcMn6Azvf9qc-VybEI6RnjTHxDZWK5EajSP4_sQ15e8ivUk0JoWdJ53feL-hnQvwsab28gghSghrxM2kGwGA1XgO-SVawqJt8SjvE-Q-__01ZKK0OyA0cDJjX3L9RoPUN_moMeAPFw0hqkFEhm72GSVCEY1eY-cOXmL3icxnsnlUD__SS9q33RxF2y5oiW1edqcRqhW_7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB-0X_PPh-1nYoq6xwqL0ZKDwrQ8SDhW_rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU-lWFUkB42AjuoECgYEA5z_CXqDFfZ8MXCPAOeui8y5HNDtu30aR-HOXsBDnRI8huXsGND04FfmXR7nkghr08fFVDmE4PeKUk810YJb-IAJo8wrOZ0682n6yEMO58omqKin-iIUVrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H-IviPIylyECgYEA3znwAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL-EwLeVc1zD9yj1axcDelICDZxCZynU7kDnrQcFkT0bjH_gC8Jk3v7XT9l1UDDqC1b7rm_X5wFIZ_rmNa1rVZhL1o_tKx5tvM2syJ1q95v7NdygFIEIW-qbIKbc6Wz0MCgYBsUZdQD-qx_xAhELX364I2epTryHMUrs-tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59_Q9ss-gocV9hB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij-w02qKVBjcHkb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT_z5bJx_Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq_s4K1LJtUT3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwmpcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxIuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9_HM9ovdP0Iy" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to PySETO 2 | ================= 3 | 4 | PySETO is a `PASETO (Platform-Agnostic SEcurity TOkens)`_ implementation written 5 | in Python which supports all of the versions and purposes below: 6 | 7 | - `Version 4: Sodium Modern`_ 8 | - ✅ Local: Symmetric Authenticated Encryption 9 | - XChaCha20 + BLAKE2b-MAC (Encrypt-then-MAC). 10 | - ✅ Public: Asymmetric Authentication (Public-Key Signatures) 11 | - EdDSA over Curve25519. 12 | - `Version 3: NIST Modern`_ 13 | - ✅ Local: Symmetric Authenticated Encryption 14 | - AES-256-CTR + HMAC-SHA384 (Encrypt-then-MAC). 15 | - ✅ Public: Asymmetric Authentication (Public-Key Signatures) 16 | - ECDSA over NIST P-384, with SHA-384, using `RFC 6979 deterministic k-values`_ 17 | - `Version 2: Sodium Original`_ 18 | - ✅ Local: Symmetric Authenticated Encryption 19 | - XChaCha20-Poly1305 (192-bit nonce, 256-bit key, 128-bit authentication tag). 20 | - ✅ Public: Asymmetric Authentication (Public-Key Signatures) 21 | - EdDSA over Curve25519. 22 | - `Version 1: NIST Compatibility`_ 23 | - ✅ Local: Symmetric Authenticated Encryption 24 | - AES-256-CTR + HMAC-SHA384 (Encrypt-then-MAC). 25 | - ✅ Public: Asymmetric Authentication (Public-Key Signatures) 26 | - RSASSA-PSS with 2048-bit key, SHA384 hashing and MGF1+SHA384. 27 | 28 | In addition, PySETO also supports `PASERK (Platform-Agnostic Serialized Keys)`_. 29 | 30 | You can install PySETO with pip: 31 | 32 | .. code-block:: console 33 | 34 | $ pip install pyseto 35 | 36 | 37 | And then, you can use it as follows: 38 | 39 | 40 | v4.public 41 | --------- 42 | 43 | .. code-block:: pycon 44 | 45 | >>> import pyseto 46 | >>> from pyseto import Key 47 | >>> secret_key_pem = b"-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEILTL+0PfTOIQcn2VPkpxMwf6Gbt9n4UEFDjZ4RuUKjd0\n-----END PRIVATE KEY-----" 48 | >>> public_key_pem = b"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI=\n-----END PUBLIC KEY-----" 49 | >>> secret_key = Key.new(version=4, purpose="public", key=secret_key_pem) 50 | >>> token = pyseto.encode( 51 | ... secret_key, 52 | ... '{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}', 53 | ... ) 54 | >>> token 55 | B'v4.public.eyJkYXRhIjogInRoaXMgaXMgYSBzaWduZWQgbWVzc2FnZSIsICJleHAiOiAiMjAyMi0wMS0wMVQwMDowMDowMCswMDowMCJ9l1YiKei2FESvHBSGPkn70eFO1hv3tXH0jph1IfZyEfgm3t1DjkYqD5r4aHWZm1eZs_3_bZ9pBQlZGp0DPSdzDg' 56 | >>> public_key = Key.new(4, "public", public_key_pem) 57 | >>> decoded = pyseto.decode(public_key, token) 58 | >>> decoded.payload 59 | B'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 60 | 61 | v4.local 62 | -------- 63 | 64 | .. code-block:: pycon 65 | 66 | >>> import pyseto 67 | >>> from pyseto import Key 68 | >>> key = Key.new(version=4, purpose="local", key=b"our-secret") 69 | >>> token = pyseto.encode( 70 | ... key, '{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 71 | ... ) 72 | >>> token 73 | b'v4.local.VXJUUePf8zL1670zhOmbO7eRdccapuXlf76fRCkntiRauk2qQFOaBQOk4ISSRXQZvcGG2C5H74ShLzoU3YorK4xdfjHBj4ESoRB5mt1FWf8MEXoDQiIHQ4WDyMR57ferhaKJM6FwgcwM2xINWy1xCSFz5f7al0c8RUnd4xO_42beR83ye0jRYg' 74 | >>> decoded = pyseto.decode(key, token) 75 | >>> decoded.payload 76 | b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 77 | 78 | 79 | Index 80 | ----- 81 | 82 | .. toctree:: 83 | :maxdepth: 2 84 | 85 | installation 86 | paseto_usage 87 | paserk_usage 88 | api 89 | changes 90 | 91 | .. _`PASETO (Platform-Agnostic SEcurity TOkens)`: https://paseto.io/ 92 | .. _`Version 1: NIST Compatibility`: https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version1.md 93 | .. _`Version 2: Sodium Original`: https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version2.md 94 | .. _`Version 3: NIST Modern`: https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version3.md 95 | .. _`Version 4: Sodium Modern`: https://github.com/paseto-standard/paseto-spec/blob/master/docs/01-Protocol-Versions/Version4.md 96 | .. _`RFC 6979 deterministic k-values`: https://datatracker.ietf.org/doc/html/rfc6979 97 | .. _`PASERK (Platform-Agnostic Serialized Keys)`: https://github.com/paseto-standard/paserk. 98 | -------------------------------------------------------------------------------- /tests/test_v4.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | 3 | import pytest 4 | 5 | import pyseto 6 | from pyseto import DecryptError, EncryptError, Key, VerifyError 7 | from pyseto.versions.v4 import V4Local, V4Public 8 | 9 | from .utils import load_key 10 | 11 | 12 | class TestV4Local: 13 | """ 14 | Tests for v4.local. 15 | """ 16 | 17 | @pytest.mark.parametrize( 18 | "key, msg", 19 | [ 20 | (b"", "key must be specified."), 21 | (token_bytes(65), "key length must be up to 64 bytes."), 22 | ], 23 | ) 24 | def test_v4_local_new_with_invalid_arg(self, key, msg): 25 | with pytest.raises(ValueError) as err: 26 | Key.new(4, "local", key) 27 | pytest.fail("Key.new() should fail.") 28 | assert msg in str(err.value) 29 | 30 | def test_v4_local_decrypt_via_decode_with_wrong_key(self): 31 | k1 = Key.new(4, "local", b"our-secret") 32 | k2 = Key.new(4, "local", b"others-secret") 33 | token = pyseto.encode(k1, b"Hello world!") 34 | with pytest.raises(DecryptError) as err: 35 | pyseto.decode(k2, token) 36 | pytest.fail("pyseto.decode() should fail.") 37 | assert "Failed to decrypt." in str(err.value) 38 | 39 | def test_v4_local_encrypt_with_invalid_arg(self): 40 | k = Key.new(4, "local", b"our-secret") 41 | with pytest.raises(EncryptError) as err: 42 | k.encrypt(None) 43 | pytest.fail("pyseto.encrypt() should fail.") 44 | assert "Failed to encrypt." in str(err.value) 45 | 46 | @pytest.mark.parametrize( 47 | "nonce", 48 | [ 49 | token_bytes(1), 50 | token_bytes(8), 51 | token_bytes(31), 52 | token_bytes(33), 53 | token_bytes(64), 54 | ], 55 | ) 56 | def test_v4_local_encrypt_via_encode_with_wrong_nonce(self, nonce): 57 | k = Key.new(4, "local", b"our-secret") 58 | with pytest.raises(ValueError) as err: 59 | pyseto.encode(k, b"Hello world!", nonce=nonce) 60 | pytest.fail("pyseto.encode() should fail.") 61 | assert "nonce must be 32 bytes long." in str(err.value) 62 | 63 | @pytest.mark.parametrize( 64 | "paserk, msg", 65 | [ 66 | ("xx.local.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 67 | ("k1.local.AAAAAAAAAAAAAAAA", "Invalid PASERK version: k1."), 68 | ("k4.local.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 69 | ("k4.public.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 70 | ("k4.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 71 | ("k4.public.AAAAAAAAAAAAAAAA", "Invalid PASERK type: public."), 72 | ], 73 | ) 74 | def test_v4_local_from_paserk_with_invalid_args(self, paserk, msg): 75 | with pytest.raises(ValueError) as err: 76 | V4Local.from_paserk(paserk) 77 | pytest.fail("Key.from_paserk should fail.") 78 | assert msg in str(err.value) 79 | 80 | def test_v4_local_to_peer_paserk_id(self): 81 | k = Key.new(4, "local", b"our-secret") 82 | assert k.to_peer_paserk_id() == "" 83 | 84 | 85 | class TestV4Public: 86 | """ 87 | Tests for v4.public. 88 | """ 89 | 90 | def test_v4_public_to_paserk_id(self): 91 | sk = Key.new(4, "public", load_key("keys/private_key_ed25519.pem")) 92 | pk = Key.new(4, "public", load_key("keys/public_key_ed25519.pem")) 93 | assert sk.to_peer_paserk_id() == pk.to_paserk_id() 94 | assert pk.to_peer_paserk_id() == "" 95 | 96 | def test_v4_public_verify_via_encode_with_wrong_key(self): 97 | sk = Key.new(4, "public", load_key("keys/private_key_ed25519.pem")) 98 | pk = Key.new(4, "public", load_key("keys/public_key_ed25519_2.pem")) 99 | token = pyseto.encode(sk, b"Hello world!") 100 | with pytest.raises(VerifyError) as err: 101 | pyseto.decode(pk, token) 102 | pytest.fail("pyseto.decode() should fail.") 103 | assert "Failed to verify." in str(err.value) 104 | 105 | @pytest.mark.parametrize( 106 | "paserk, msg", 107 | [ 108 | ("xx.public.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 109 | ("k1.public.AAAAAAAAAAAAAAAA", "Invalid PASERK version: k1."), 110 | ("k4.public.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 111 | ("k4.local.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 112 | ("k4.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 113 | ("k4.local.AAAAAAAAAAAAAAAA", "Invalid PASERK type: local."), 114 | ], 115 | ) 116 | def test_v4_public_from_paserk_with_invalid_args(self, paserk, msg): 117 | with pytest.raises(ValueError) as err: 118 | V4Public.from_paserk(paserk) 119 | pytest.fail("Key.from_paserk should fail.") 120 | assert msg in str(err.value) 121 | -------------------------------------------------------------------------------- /pyseto/versions/v4.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | from secrets import token_bytes 4 | from typing import Any, Union 5 | 6 | from cryptography.hazmat.primitives import serialization 7 | from cryptography.hazmat.primitives.asymmetric.ed25519 import ( 8 | Ed25519PrivateKey, 9 | Ed25519PublicKey, 10 | ) 11 | 12 | from ..exceptions import DecryptError, SignError, VerifyError 13 | from ..key_sodium import SodiumKey 14 | from ..utils import base64url_encode, pae 15 | 16 | 17 | class V4Local(SodiumKey): 18 | """ 19 | The key object for v4.local. 20 | """ 21 | 22 | _VERSION = 4 23 | _TYPE = "local" 24 | 25 | def __init__(self, key: Union[str, bytes]): 26 | super().__init__(key) 27 | if len(self._key) > 64: 28 | raise ValueError("key length must be up to 64 bytes.") 29 | return 30 | 31 | def to_paserk_id(self) -> str: 32 | h = "k4.lid." 33 | p = self.to_paserk() 34 | b = hashlib.blake2b(digest_size=33) 35 | b.update((h + p).encode("utf-8")) 36 | d = b.digest() 37 | return h + base64url_encode(d).decode("utf-8") 38 | 39 | def encrypt( 40 | self, 41 | payload: bytes, 42 | footer: bytes = b"", 43 | implicit_assertion: bytes = b"", 44 | nonce: bytes = b"", 45 | ) -> bytes: 46 | if nonce: 47 | if len(nonce) != 32: 48 | raise ValueError("nonce must be 32 bytes long.") 49 | else: 50 | nonce = token_bytes(32) 51 | tmp = self._generate_hash(self._key, b"paseto-encryption-key" + nonce, 56) 52 | ek = tmp[0:32] 53 | n2 = tmp[32:] 54 | ak = self._generate_hash(self._key, b"paseto-auth-key-for-aead" + nonce, 32) 55 | 56 | c = self._encrypt(ek, n2, payload) 57 | pre_auth = pae([self.header, nonce, c, footer, implicit_assertion]) 58 | t = self._generate_hash(ak, pre_auth, 32) 59 | token = self._header + base64url_encode(nonce + c + t) 60 | if footer: 61 | token += b"." + base64url_encode(footer) 62 | return token 63 | 64 | def decrypt(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 65 | n = payload[0:32] 66 | c = payload[32 : len(payload) - 32] 67 | t = payload[-32:] 68 | tmp = self._generate_hash(self._key, b"paseto-encryption-key" + n, 56) 69 | ek = tmp[0:32] 70 | n2 = tmp[32:] 71 | ak = self._generate_hash(self._key, b"paseto-auth-key-for-aead" + n, 32) 72 | pre_auth = pae([self.header, n, c, footer, implicit_assertion]) 73 | t2 = self._generate_hash(ak, pre_auth, 32) 74 | if not hmac.compare_digest(t, t2): 75 | raise DecryptError("Failed to decrypt.") 76 | return self._decrypt(ek, n2, c) 77 | 78 | 79 | class V4Public(SodiumKey): 80 | """ 81 | The key object for v4.public. 82 | """ 83 | 84 | _VERSION = 4 85 | _TYPE = "public" 86 | 87 | def __init__(self, key: Any): 88 | super().__init__(key) 89 | self._sig_size = 64 90 | 91 | if not isinstance(self._key, (Ed25519PublicKey, Ed25519PrivateKey)): 92 | raise ValueError("The key is not Ed25519 key.") 93 | 94 | if isinstance(self._key, Ed25519PublicKey): 95 | self._is_secret = False 96 | return 97 | 98 | # @classmethod 99 | # def from_public_bytes(cls, key: bytes): 100 | # try: 101 | # k = Ed25519PublicKey.from_public_bytes(key) 102 | # except Exception as err: 103 | # raise ValueError("Invalid bytes for the key.") from err 104 | # return cls(k) 105 | 106 | def to_paserk_id(self) -> str: 107 | p = self.to_paserk() 108 | h = "k4.pid." if isinstance(self._key, Ed25519PublicKey) else "k4.sid." 109 | b = hashlib.blake2b(digest_size=33) 110 | b.update((h + p).encode("utf-8")) 111 | d = b.digest() 112 | return h + base64url_encode(d).decode("utf-8") 113 | 114 | def to_peer_paserk_id(self) -> str: 115 | if not self._is_secret: 116 | return "" 117 | 118 | h1 = "k4.public." 119 | pub = self._key.public_key().public_bytes( 120 | encoding=serialization.Encoding.Raw, 121 | format=serialization.PublicFormat.Raw, 122 | ) 123 | p = h1 + base64url_encode(pub).decode("utf-8") 124 | 125 | h2 = "k4.pid." 126 | b = hashlib.blake2b(digest_size=33) 127 | b.update((h2 + p).encode("utf-8")) 128 | d = b.digest() 129 | return h2 + base64url_encode(d).decode("utf-8") 130 | 131 | def sign(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 132 | if isinstance(self._key, Ed25519PublicKey): 133 | raise ValueError("A public key cannot be used for signing.") 134 | 135 | m2 = pae([self.header, payload, footer, implicit_assertion]) 136 | try: 137 | return self._key.sign(m2) 138 | except Exception as err: 139 | raise SignError("Failed to sign.") from err 140 | 141 | def verify(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b""): 142 | if len(payload) <= self._sig_size: 143 | raise ValueError("Invalid payload.") 144 | 145 | sig = payload[-self._sig_size :] 146 | m = payload[: len(payload) - self._sig_size] 147 | k = self._key if isinstance(self._key, Ed25519PublicKey) else self._key.public_key() 148 | m2 = pae([self.header, m, footer, implicit_assertion]) 149 | try: 150 | k.verify(sig, m2) 151 | except Exception as err: 152 | raise VerifyError("Failed to verify.") from err 153 | return m 154 | -------------------------------------------------------------------------------- /pyseto/versions/v2.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from secrets import token_bytes 3 | from typing import Any, Union 4 | 5 | # from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 6 | from Cryptodome.Cipher import ChaCha20_Poly1305 7 | from cryptography.hazmat.primitives import serialization 8 | from cryptography.hazmat.primitives.asymmetric.ed25519 import ( 9 | Ed25519PrivateKey, 10 | Ed25519PublicKey, 11 | ) 12 | 13 | from ..exceptions import DecryptError, EncryptError, SignError, VerifyError 14 | from ..key_sodium import SodiumKey 15 | from ..utils import base64url_encode, pae 16 | 17 | 18 | class V2Local(SodiumKey): 19 | """ 20 | The key object for v2.local. 21 | """ 22 | 23 | _VERSION = 2 24 | _TYPE = "local" 25 | 26 | def __init__(self, key: Union[str, bytes]): 27 | super().__init__(key) 28 | if len(self._key) != 32: 29 | raise ValueError("key must be 32 bytes long.") 30 | return 31 | 32 | def to_paserk_id(self) -> str: 33 | h = "k2.lid." 34 | p = self.to_paserk() 35 | b = hashlib.blake2b(digest_size=33) 36 | b.update((h + p).encode("utf-8")) 37 | d = b.digest() 38 | return h + base64url_encode(d).decode("utf-8") 39 | 40 | def encrypt( 41 | self, 42 | payload: bytes, 43 | footer: bytes = b"", 44 | implicit_assertion: bytes = b"", 45 | nonce: bytes = b"", 46 | ) -> bytes: 47 | n = self._generate_nonce(nonce, payload) 48 | pre_auth = pae([self.header, n, footer]) 49 | 50 | try: 51 | cipher = ChaCha20_Poly1305.new(key=self._key, nonce=n) 52 | cipher.update(pre_auth) 53 | c, tag = cipher.encrypt_and_digest(payload) 54 | token = self._header + base64url_encode(n + c + tag) 55 | if footer: 56 | token += b"." + base64url_encode(footer) 57 | return token 58 | except Exception as err: 59 | raise EncryptError("Failed to encrypt.") from err 60 | 61 | def decrypt(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 62 | n = payload[0:24] 63 | c = payload[24 : len(payload) - 16] 64 | tag = payload[-16:] 65 | pre_auth = pae([self.header, n, footer]) 66 | 67 | try: 68 | cipher = ChaCha20_Poly1305.new(key=self._key, nonce=n) 69 | cipher.update(pre_auth) 70 | return cipher.decrypt_and_verify(c, tag) 71 | except Exception as err: 72 | raise DecryptError("Failed to decrypt.") from err 73 | 74 | @staticmethod 75 | def _generate_nonce(key: bytes, msg: bytes) -> bytes: 76 | if key: 77 | if len(key) != 24: 78 | raise ValueError("nonce must be 24 bytes long.") 79 | else: 80 | key = token_bytes(24) 81 | 82 | try: 83 | h = hashlib.blake2b(key=key, digest_size=24) 84 | h.update(msg) 85 | return h.digest() 86 | except Exception as err: 87 | raise EncryptError("Failed to generate internal nonce.") from err 88 | 89 | 90 | class V2Public(SodiumKey): 91 | """ 92 | The key object for v2.public. 93 | """ 94 | 95 | _VERSION = 2 96 | _TYPE = "public" 97 | 98 | def __init__(self, key: Any): 99 | super().__init__(key) 100 | self._sig_size = 64 101 | 102 | if not isinstance(self._key, (Ed25519PublicKey, Ed25519PrivateKey)): 103 | raise ValueError("The key is not Ed25519 key.") 104 | 105 | if isinstance(self._key, Ed25519PublicKey): 106 | self._is_secret = False 107 | return 108 | 109 | def to_paserk_id(self) -> str: 110 | p = self.to_paserk() 111 | h = "k2.pid." if isinstance(self._key, Ed25519PublicKey) else "k2.sid." 112 | b = hashlib.blake2b(digest_size=33) 113 | b.update((h + p).encode("utf-8")) 114 | d = b.digest() 115 | return h + base64url_encode(d).decode("utf-8") 116 | 117 | def to_peer_paserk_id(self) -> str: 118 | if not self._is_secret: 119 | return "" 120 | 121 | h1 = "k2.public." 122 | pub = self._key.public_key().public_bytes( 123 | encoding=serialization.Encoding.Raw, 124 | format=serialization.PublicFormat.Raw, 125 | ) 126 | p = h1 + base64url_encode(pub).decode("utf-8") 127 | 128 | h2 = "k2.pid." 129 | b = hashlib.blake2b(digest_size=33) 130 | b.update((h2 + p).encode("utf-8")) 131 | d = b.digest() 132 | return h2 + base64url_encode(d).decode("utf-8") 133 | 134 | # @classmethod 135 | # def from_public_bytes(cls, key: bytes): 136 | # try: 137 | # k = Ed25519PublicKey.from_public_bytes(key) 138 | # except Exception as err: 139 | # raise ValueError("Invalid bytes for the key.") from err 140 | # return cls(k) 141 | 142 | def sign(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 143 | if isinstance(self._key, Ed25519PublicKey): 144 | raise ValueError("A public key cannot be used for signing.") 145 | m2 = pae([self.header, payload, footer]) 146 | try: 147 | return self._key.sign(m2) 148 | except Exception as err: 149 | raise SignError("Failed to sign.") from err 150 | 151 | def verify(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b""): 152 | if len(payload) <= self._sig_size: 153 | raise ValueError("Invalid payload.") 154 | 155 | sig = payload[-self._sig_size :] 156 | m = payload[: len(payload) - self._sig_size] 157 | k = self._key if isinstance(self._key, Ed25519PublicKey) else self._key.public_key() 158 | m2 = pae([self.header, m, footer]) 159 | try: 160 | k.verify(sig, m2) 161 | except Exception as err: 162 | raise VerifyError("Failed to verify.") from err 163 | return m 164 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.secret-wrap.pie.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.secret-wrap.pie Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.secret-wrap.pie-1", 6 | "unwrapped": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", 7 | "wrapping-key":"707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f", 8 | "paserk": "k1.secret-wrap.pie.npl0BPDoiKzl7qCmqEvBV1t9igbxNHs83S6ymepSS0O9iBRdbtmBd68WLz6RQ2eJi-YYryYNvCSwxEx7LegpuJ0RLfyUyy3cqN3j41MUKq9cODvwjWDpKF_NAy7JtMd5PjbBCL60Ro2uY6oqJNtM2Hiczu5Wmp8uflkDAhe0KBuufwoyqYCUD3Dwgwaqk7Rh6z7ondkwJriujSEeywVOzDpMsImQPpXWdcrMjpnTtSmArdLQ7YyVqjIBSEYkA-nF9qzHYPbGcwFB2grrFtxMOMcK0kFjbUaJhF5r8d4fGiSc4YSFPqA9Qf2H438P4F_uG6j7giBUxS16twsOjS4eaz5ONyEsZk4D5ESpxJzhiymrXg7917HjSMXZcJ1utImuCaOSKjnjM4J5cq05ragvEhpELciyf6vEVnayIhrg_OUdWFWjRiLQnYc9SB6WS_Gyy5ALXpZ-NMMHllvgHm3_eedolKJIFzNTkioRfZJL7WNXLbQaY_e1TTAkusy36fQ6vJ0plC6EdDKkj1UnzDMBu6OBEgCcIOJX9Xefh2S1D9gC5FEC5RvDDiRIdKIZO_9O3np27XEbUt6Fk0eJUb5egL1hm1iiZ-Ux4owkb8iXDcJueAn4-DRvFk0VODCLXOD1XXIfxsELSY0q0hwPcI2k_CkY1rzGxlPvP1WLLzJje_Q0pRSlyN5GsH93JtR1_z0MXWtley06ngDzdXQi7meKQovj9D0BQCn8cVUBpbWazolAdAv8RElwSD5bgDVF-hlYzZ50zpFWY_ADjrR2kKXhDp4ebg48Iqqnn5xQ4vfHLC3RIokzL4xAylzuoGP30Hup42g5ThRc1dPTMeXBW5ICQ_9QdEUBEUd6tp-6HHFqTrXOMfyAzcl6NEOfisLIY6J5ibTte_ir6PCNqiKBjYsiQ-HwxMMcZ7FalV6Gk26IeWVAnX7roh_FjVz04r1JTLusF3JOzk9onv3UeMJyroXga4qn5lU0_aGCOLKBVOp3PstF_vTlQ57gi6xT2MqKTOQ_akI_jpW1-KSQ1A4m_p_whw11Lb8qXHpnp68xtV8pPAV5W1i3Exqsx03l3L_e6nAkKR2B3zc3gmfruoKa6oIp2hYXfovurkpVN1vX91qyNW7rBnjH6ouB18Mbv3KW_8XGFYPMloyVFpg1Cxzy53hP2V4H_gR-M76w3nb_13l1xZGEut1Pl49WrwEh-7WDvaDVEV5FpzTczi-ab19o4U-mFewLlNxydGq2rqFgCX12BoJh3NlAwWTOhHtvbBuRBfL_PQoD3uDSkV6QtYKD6XP_2n3PyMkKs5dEcKKLiY0e9CimvrNwGcFHWrQOtT1oP5U_UAelrYJH3qXEOYb8klcbbYfQxJuIgxZH2nRp2De2_sHIVfqu6o1ziSzrEetkTnK8E8A5ZXYAmNZev1YdYtTT5P06WfqJvw716hGYK-oSzlCalP7znS1xYZFcuWLYpwWHQyyF3AVyMXcbRW8hRmX84KHcVc-iQZLLudazcAR24kKJiaysVIWDZ6NOultQr86hCD6MZaYhwCyivBDLQ88FEO7UtylnWth9sT-k7ljnQYcT1KBUxB1G3JNx9F9OHg2Lz5KXn_cKlw0stbYtVTjpY9tmOCXiDsE1npE_lR8BZjPNgKb6AM0U9YtRnPNW2Myey3mhMpsznPevN8aQYDn7CCRucK2_Hb4" 9 | }, 10 | { 11 | "name": "k1.secret-wrap.pie-2", 12 | "unwrapped": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", 13 | "wrapping-key": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 14 | "paserk": "k1.secret-wrap.pie.6xorqK8MPVRVFSQYBY1QLfKSbR7Ovm835aCiuUzgQJZvbNUHD9v1eqwoamR6iTbvxNm8amFY-JN3OZZPaSHdzOjJ2-Q00p5oEAGFxDTpUHKNoiuHBBnqSyxJrrbyEMbIuyKXlbkoe9b5UNtBkrtCGTyNYEN4E9neZ5bx3tT_JzhZea7weCvUqEpfaxEzNG4ahtcRxL4WmFiyY2WEKkjrxQkCuog6D_7Oo_3UcB6vguNvBgfHwhb0ABOBjAY0YyFUNmXai9CGT42bL_AHLvgcyUtJ6WT-yxQ6iWVDzdtEAngkMKOwM-6nwDH12Agke-YZVyON_GzAezycdk_Ck1DsEec3X6tK9AtK0-zdFE7yxAqn5qIlm-FzUycg36JgwoxD1u1TfXXj0q1uolLBM26P9WFW1wIIeVLjLKcGBH6jdBCqguAOqI9AcwjmvuS6krx4ZUfYH6wEHDzDQlyf-zy8ox0enZMFSWxIkV7ROhxf89eSaC1JSZLvpnS1-otru_9mRyBwxZs9ro2xez7zcfcPDmu_LoqPJ7pqH-9MaUV_KBOPkGGn3uXOC5StW5kuVr_VRBTNpSGCdUi7z2DPsD5Svvcr5t2P5xh8nDeTP7C0aD_L6zNO3tQXpxWnG2adynm3SCfMIkQu3GbvYy8B2uBInxDvwmljQOSHoH4kS5D5K1BCCv6Wf7NgePiiPLN6cr3JRdvbzE6OU3y6BAcFBP9NoUivbl5NSBkLWgrwV6vNtZJl3P1vngqa3A38wqc1hGfWBJvRYwJ9HDcL8IYFoCKRSq-e824jhQE5wogpgmHYucVKlBoOJNZA2YxfHuCfF3ak644Ok2Tyab5tvYVrWkZZA70k87Hom9F-5Olf5fp2Kw4oCdJh6re3SZOG-aUq8jMknMafoinIyHCe-wp6JWOlPZfUYaWNFFxXGTi7sx58UI5QN4nUNbe-Zq4ky21QGHXXcTQvNENr1nnECdR2Q8FYtitBKXoxNStWqT-vtgFgUNSiRnr1yL8Nqt_X-kWPuN9sY42JbfPfDuIJPpL35ZTHlZDZpZamDj-h6WRFJW7gd3MlJ6KFSOw3JAuuDvoPRe0_LaWQF2yDiI2YIPl05zWZREluEyTS2Xkh6JZ6Zb5ioZ_rXE5yP0jFS8ImPNKAHg9hlhtAc97hc23T5DxIOQeMojg2mSUPMESg5p4Ctdm0LAHxy1h-ithNe6_nLZPmbtj0Hq1cU9QQyNGWkLDFQNeN2k1-cPF4EeSSJGo__lD-75VqQTSTf35p4QQd1nndsdFVpyTI1VzS2RaxVCvNotVUw8_AICYz08CkUJqP-EAVkpYKbCTAAS_gpT-3glOtBBoTq9gTq1fg-_u3arB4NmxAD-0X21XVHsfUkoJz-K2RZmBfQCzCqmLTqnzPL14omLgdtlr5BVyo_u6Uyk2vxCfFTrjkuDVh3twbM2PLYj7yUah6D_t5nViy85RLLprjcUkXaddfuQwj2AAAqI_r4_IpieRZOiVB7YKDGhxraQg18Jo1XJdV3lO8ADGtHM59x-CeaNTsYpk_S10adTtaqcbQb95kYRfNIxnQrbacLtEePRHVpng4hVF0FZtZAqjT8DroaozBIQEoqivAXryMTgGBLWhpBQa3ssnDY90-Wh20yS-7Do7YmYewRPGb22eSKHL45Y2NHriqYr_nAo6o-km0ZN6FfGBbCuE" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.secret-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.secret-pw Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.secret-pw-1", 6 | "unwrapped": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", 7 | "password": "correct horse battery staple", 8 | "options": {"iterations": 1000}, 9 | "paserk": "k1.secret-pw.oDGpcaS5d-i2UIk8WNkOwk2U4qWtvxRA7lAU86xVQJcAAAPo-1akgLKZaTLl-uSDVFBIpNsX3XdHCy4mqdj40yj5g8cCWbVqKedrenkGDROnAfvkEfjIWs20a7pIF8XnF0ibcjylTNASgPbIZt3Wvo8lhFIVv6TyWNn58sY9BFGLDpJOj1F87iTpPKgt5cWE0J8696ghYkWUSvapHONHVTEEASGCvafC21_sRFX6U6Vvlb4XpLekr79vPoxFXxboK5J5pGwChfGSU3yVcqzl6ifFAcbgzzSktQUt9akHAQc0KPDu6DLiM6t_V7CqYbMTSjxJ9rfajHr5y0JKQQW4u6pOMCZ2taPfxVFoTvN9SkS07u1usorepE-P0coVCr9FhreNjJFjZK1bXAY1NlK7TatcXoeUjpXxkkfzN2ia_G3EJOgWKd8IyhnnANIbMrdTGmOwxZR0n7GFRV-K7CQldkGwPzkmEiIVw4RW30V2QiH9Ugw74EtdMX1BmBnsimi7xcE5ud1uSFYEnnmDaiH49rRUyXCH5uUsi241ffqoz-KykPB_jkwWntQdUzsNet648wjbAzoxGKSkIm5NLsBV_tYUPaOf3rVPrQ1FVduBDO37D9kA_n0ARqiYEJunzkgSEDnczglNgNKRqU8zCObcCMtt3y8U2klDaqLMi31Pw19i2hI6KnJty4dokH3AV1PBHcN-G2eU8AbHAWcDtenNolwQKys2lcmEOafO6OXAmzuu3qsHt47Ui0kpnG-9vP9whQ5mbiFKhL0F2aCu35uhQydnUzSbeP-1Lj08JjO0y-_anQ54_fxKJdjMAlyIQwVYYUB9aFNy2AcbWUUAIoN9QWoyi2mumwAt_kW0-9y-9Db2z89qWp2KyEFdj-9JfbmmvJtLFI-WcPrls9KRvye7KbLQ3tnvQeYpo5Hs76QvhzT9RwQPq_MX8NwRJAlXKszsjPY4_pABrOpmkyHt4jE7UBiGnqPCnWJSVLwupEsuSWqwD9l1URTQ6kMdHrHKGK1eO9GfRijE9yU0tEY_I_TK5GKTK3ONVYz5wkMq4f4TkYGuEohbo76yPl6oW_E1kVsG0R7gnzlHX8YSlqaccKeCkrYecl4zBYPkVH8idmkMFX4aDzkLtRN_XSeRNQ7Gci365jDxV5wwwn_9hIIs2kPxudyP_VLCZrlQZ001T2oWEpMM-bonYMc4s5QPxnObVDAJbxL0KeVKwOQrqCIfd-252ifUhJta7_q76wNhHZMDaKXQ_0LxhorR3vSTPYLbzoO6dpnZRYl3d0K3q_cp_xu7UVIplgVP_Jmct3rdyoSdOxf7u16OAkgyalc0Nbl-kYBPXTwbEto7nUVVhn3kKL3SViDbuTNlx2VVLd73PNx3hSTANBLDEAdMaBPI9rN0CeLc6fMFZS3d0igspHmYvCek4cVrxttUCYloojXI39N_9hy27f_a3Pe9XyQx9GDOujMTNY6ayycku1EPFT1jAy2V_8zsPcwMDSKKagAnlsY7CmoEJ5QjXs9xN8blumHM6F7rTFYaOIPwHt_QItrI0iL-q3m7T4kErW0QVy-opz9Qmwcbr-ZYdcH6zlj9XjeF51fA6Pb-B81T7qQjc3DXy5YOBvPN8vOolDVYo3ecQUgV4Yv_Yyf-pp4yTDqUfQFQ8ZBP4S1JD9DKbskX7v4M9oD3u3MrkK-53sMcOSaTR2R1vrnWmkh_IQUpGAoIPg" 10 | }, 11 | { 12 | "name": "k1.secret-pw-2", 13 | "unwrapped": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAyaTgTt53ph3p5GHgwoGWwz5hRfWXSQA08NCOwe0FEgALWos9\nGCjNFCd723nCHxBtN1qd74MSh/uN88JPIbwxKheDp4kxo4YMN5trPaF0e9G6Bj1N\n02HnanxFLW+gmLbgYO/SZYfWF/M8yLBcu5Y1Ot0ZxDDDXS9wIQTtBE0ne3YbxgZJ\nAZTU5XqyQ1DxdzYyC5lF6yBaR5UQtCYTnXAApVRuUI2Sd6L1E2vl9bSBumZ5IpNx\nkRnAwIMjeTJB/0AIELh0mE5vwdihOCbdV6alUyhKC1+1w/FW6HWcp/JG1kKC8DPI\nidZ78Bbqv9YFzkAbNni5eSBOsXVBKG78Zsc8owIDAQABAoIBAF22jLDa34yKdns3\nqfd7to+C3D5hRzAcMn6Azvf9qc+VybEI6RnjTHxDZWK5EajSP4/sQ15e8ivUk0Jo\nWdJ53feL+hnQvwsab28gghSghrxM2kGwGA1XgO+SVawqJt8SjvE+Q+//01ZKK0Oy\nA0cDJjX3L9RoPUN/moMeAPFw0hqkFEhm72GSVCEY1eY+cOXmL3icxnsnlUD//SS9\nq33RxF2y5oiW1edqcRqhW/7L1yYMbxHFUcxWh8WUwjn1AAhoCOUzF8ZB+0X/PPh+\n1nYoq6xwqL0ZKDwrQ8SDhW/rNDLeO9gic5rl7EetRQRbFvsZ40AdsX2wU+lWFUkB\n42AjuoECgYEA5z/CXqDFfZ8MXCPAOeui8y5HNDtu30aR+HOXsBDnRI8huXsGND04\nFfmXR7nkghr08fFVDmE4PeKUk810YJb+IAJo8wrOZ0682n6yEMO58omqKin+iIUV\nrPXLSLo5CChrqw2J4vgzolzPw3N5I8FJdLomb9FkrV84H+IviPIylyECgYEA3znw\nAG29QX6ATEfFpGVOcogorHCntd4niaWCq5ne5sFL+EwLeVc1zD9yj1axcDelICDZ\nxCZynU7kDnrQcFkT0bjH/gC8Jk3v7XT9l1UDDqC1b7rm/X5wFIZ/rmNa1rVZhL1o\n/tKx5tvM2syJ1q95v7NdygFIEIW+qbIKbc6Wz0MCgYBsUZdQD+qx/xAhELX364I2\nepTryHMUrs+tGygQVrqdiJX5dcDgM1TUJkdQV6jLsKjPs4Vt6OgZRMrnuLMsk02R\n3M8gGQ25ok4f4nyyEZxGGWnVujn55KzUiYWhGWmhgp18UCkoYa59/Q9ss+gocV9h\nB9j9Q43vD80QUjiF4z0DQQKBgC7XQX1VibkMim93QAnXGDcAS0ij+w02qKVBjcHk\nb9mMBhz8GAxGOIu7ZJafYmxhwMyVGB0I1FQeEczYCJUKnBYN6Clsjg6bnBT/z5bJ\nx/Jx1qCzX3Uh6vLjpjc5sf4L39Tyye1u2NXQmZPwB5x9BdcsFConSq/s4K1LJtUT\n3KFxAoGBANGcQ8nObi3m4wROyKrkCWcWxFFMnpwxv0pW727Hn9wuaOs4UbesCnwm\npcMTfzGUDuzYXCtAq2pJl64HG6wsdkWmjBTJEpm6b9ibOBN3qFV2zQ0HyyKlMWxI\nuVSj9gOo61hF7UH9XB6R4HRdlpBOuIbgAWZ46dkj9/HM9ovdP0Iy\n-----END RSA PRIVATE KEY-----", 14 | "password": "correct horse battery staple", 15 | "options": {"iterations": 10000}, 16 | "paserk": "k1.secret-pw.TDW6ZPnnZ_y2GhytrvRxGSoNEkBFTZ8nNwdUOsUQ_wgAACcQzafA69-6cDz-7Vlw7_8OMRFnPlFyifP1gtfpg4ussla5B95fucPNIz6RL9j9bj8LxeApTOeC_KfFNCjjqXn7ZF9c395c39RGIGjCKdu0DahGAGsRmKzwi2xQhAT728lcd85JDfrn-Sy6fo21EQ0c68recod-ZsvbBtiS63r16mxaOOWMC604JdwCHtSQ_Cb2Zdtdb7ypi1sV1OLiDn3sIv2ZAtMG4YosCmcNcwN7P7ZJ1rcIEZH-CmNu-NB5lNn9IPUa_oOmF0Y1CxkQBRKijQ3fUDpRmgB3ZstPTW3DGj63xjYlELz9EE495ZbV2sCtEs8VxUjcvCoiRCGB4t2gpv9cQ7dsL0GSfbwu0Q4Ql_rxkna1zUA0CqqwOk4OEbCa97HNN4rIUY36IH5dXn842dIeQR__2ahAAKaZwpd7OCFiZZK4o1xS1eQvVgO0jFobxo5mZr7QJjqebxjOiey1L6ZdgLs8CXv2ioLW2lGRvJ1WMJOdoWd3-Trsh9fVGMw0q0cpa-gmUauxRSSNqs1gOl3Uk_L_IzxBTPzO7qOBWiCo8dcH9mSsyrHyM9aK488qEgChuQezAceSq5OGpRSP6lnHUitkzfRZSHM6nzTNpTbva97Dx3Q0pRxWA286q2w433h6GKX2s46pzka1wHnUjMBjPX-8tozXdYUGtgqTgfH4JYJt63zmAA6Z9I7Ya3oJA7uszfIRNXgYZi5oE_V0i_6mx952k00IqzWmI89SuTmWpwxbwNv1HsG2jS64BnPI0Z2YtiOf6LRBpr6Uf-ltT457nSWIoKJwZ1AY8s0u86Ma_9XUNvi04LX-eHEsgVcd4QOkxQCbGKGmDKSUioaKKnCPzCotR6s6yWQxH4yrLOVMPbY6AiJSn8_LkAPsNXTkuBkeoJvVq1HimomFWO3Ym34i0iPaAzSy0l3dhz-WkreJscFTmu4iK5lqN9pmZGCKoGS2qEbgAVC5ziIMqe3ch9yTfsr6TCDWF4vwyOLRV2EXiRyt2-HZbjsphzjG8hJKQaQpp-BDQe4q9VTTzqQyXJC17Kpp_iFdE_HaA4-FjofggKpR7pYUBwfDNEkHtUyqhtmdLKmp-4B1rdnNOl-3erGIZgc5g28u2mlHQGCodrG0spe6S_aroLmx1mePD8J9xRFoM0Yu_dMnIAyUCRt4zJaUPF7d-PfL329kJsnfHZY13cGKPPxghmH6G-rsM1NGxTjOdtm7zoZqO18I8TXWIMttDqQtsNA7GLpGkpYlZ1bhFPLn6k-EGJrG96wvBP_G2nKVXdcS0c8gH54zKgV9nzVn3W1WL6a_UYfdNshI9n_yznP9q0VJWL-AQ5-vwH2GDFmr4gG9KlkbuNjo88p4_T1bpavTmhYDxBahgClTj_rGLy_i51fpVVELYB6eZfMy8kTUgOOZ-2qMndv8gH7T1YunWq5y9KLvrVeegFpd-91M9RPHTnLuDxuievPYFEl0AvVKidNmBi9oEIPntUaf_Da0c_NBBgrPozjdCtea4SxKUe6AkgqawYS3mrL-hn1MyS-4sq7P_IEfABBX4yIjF6vZdrJnnr8kupgqAjJ6FLQPu0c3GH5n6Nx7JoWPQfZmxZ2Oya_t8kZgcgwCoC4hNv9Q3D5m77bMZdMu30QfCQRebuAb526zyc-a20cPV0urHTMMlWg_gw" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /tests/test_v1.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | 3 | import pytest 4 | 5 | import pyseto 6 | from pyseto import DecryptError, EncryptError, Key, VerifyError 7 | from pyseto.versions.v1 import V1Local, V1Public 8 | 9 | from .utils import load_key 10 | 11 | 12 | class TestV1Local: 13 | """ 14 | Tests for v1.local. 15 | """ 16 | 17 | @pytest.mark.parametrize( 18 | "key, msg", 19 | [ 20 | (b"", "key must be specified."), 21 | ], 22 | ) 23 | def test_v1_local_new_with_invalid_arg(self, key, msg): 24 | with pytest.raises(ValueError) as err: 25 | Key.new(1, "local", key) 26 | pytest.fail("Key.new() should fail.") 27 | assert msg in str(err.value) 28 | 29 | @pytest.mark.parametrize( 30 | "key", 31 | [ 32 | None, 33 | 0, 34 | ], 35 | ) 36 | def test_v1_local__generate_hash_with_invalid_arg(self, key): 37 | with pytest.raises(EncryptError) as err: 38 | V1Local._generate_hash(key, b"Hello world!", 32) 39 | pytest.fail("V1Local._generate_hash() should fail.") 40 | assert "Failed to generate hash." in str(err.value) 41 | 42 | @pytest.mark.parametrize( 43 | "ptk", 44 | [ 45 | None, 46 | 0, 47 | ], 48 | ) 49 | def test_v1_local__encode_pie_with_invalid_ptk(self, ptk): 50 | with pytest.raises(EncryptError) as err: 51 | V1Local._encode_pie("v1.local-wrap.pie.", token_bytes(32), ptk) 52 | pytest.fail("V1Local._encode_pie() should fail.") 53 | assert "Failed to encrypt." in str(err.value) 54 | 55 | def test_v1_local_decrypt_via_decode_with_wrong_key(self): 56 | k1 = Key.new(1, "local", b"our-secret") 57 | k2 = Key.new(1, "local", b"others-secret") 58 | token = pyseto.encode(k1, b"Hello world!") 59 | with pytest.raises(DecryptError) as err: 60 | pyseto.decode(k2, token) 61 | pytest.fail("pyseto.decode() should fail.") 62 | assert "Failed to decrypt." in str(err.value) 63 | 64 | def test_v1_local_encrypt_with_invalid_arg(self): 65 | k = Key.new(1, "local", b"our-secret") 66 | with pytest.raises(EncryptError) as err: 67 | k.encrypt(None) 68 | pytest.fail("pyseto.encrypt() should fail.") 69 | assert "Failed to encrypt." in str(err.value) 70 | 71 | @pytest.mark.parametrize( 72 | "nonce", 73 | [ 74 | token_bytes(1), 75 | token_bytes(8), 76 | token_bytes(31), 77 | token_bytes(33), 78 | token_bytes(64), 79 | ], 80 | ) 81 | def test_v1_local_encrypt_via_encode_with_wrong_nonce(self, nonce): 82 | k = Key.new(1, "local", b"our-secret") 83 | with pytest.raises(ValueError) as err: 84 | pyseto.encode(k, b"Hello world!", nonce=nonce) 85 | pytest.fail("pyseto.encode() should fail.") 86 | assert "nonce must be 32 bytes long." in str(err.value) 87 | 88 | @pytest.mark.parametrize( 89 | "paserk, msg", 90 | [ 91 | ("xx.local.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 92 | ("k2.local.AAAAAAAAAAAAAAAA", "Invalid PASERK version: k2."), 93 | ("k1.local.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 94 | ("k1.public.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 95 | ("k1.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 96 | ("k1.public.AAAAAAAAAAAAAAAA", "Invalid PASERK type: public."), 97 | ( 98 | "k1.local-wrap.AAAAAAAAAAAAAAAA", 99 | "local-wrap needs wrapping_key.", 100 | ), 101 | ( 102 | "k1.secret-wrap.AAAAAAAAAAAAAAAA", 103 | "Invalid PASERK type: secret-wrap.", 104 | ), 105 | ( 106 | "k1.local-pw.AAAAAAAAAAAAAAAA", 107 | "local-pw needs password.", 108 | ), 109 | ], 110 | ) 111 | def test_vl_local_from_paserk_with_invalid_args(self, paserk, msg): 112 | with pytest.raises(ValueError) as err: 113 | V1Local.from_paserk(paserk) 114 | pytest.fail("Key.from_paserk should fail.") 115 | assert msg in str(err.value) 116 | 117 | @pytest.mark.parametrize( 118 | "paserk, msg", 119 | [ 120 | ("xx.local-wrap.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 121 | ("k1.local-wrap.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 122 | ("k1.local-wrap.xxx.AAAAAAAAAAAAAAAA", "Unknown wrapping algorithm: xxx."), 123 | ("k1.xxx.pie.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 124 | ], 125 | ) 126 | def test_v1_local_from_paserk_with_wrapping_key_and_invalid_args(self, paserk, msg): 127 | with pytest.raises(ValueError) as err: 128 | V1Local.from_paserk(paserk, wrapping_key=token_bytes(32)) 129 | pytest.fail("Key.from_paserk should fail.") 130 | assert msg in str(err.value) 131 | 132 | def test_v1_local_to_peer_paserk_id(self): 133 | k = Key.new(1, "local", b"our-secret") 134 | assert k.to_peer_paserk_id() == "" 135 | 136 | 137 | class TestV1Public: 138 | """ 139 | Tests for v1.public. 140 | """ 141 | 142 | def test_v1_public_to_paserk_id(self): 143 | sk = Key.new(1, "public", load_key("keys/private_key_rsa.pem")) 144 | pk = Key.new(1, "public", load_key("keys/public_key_rsa.pem")) 145 | assert sk.to_peer_paserk_id() == pk.to_paserk_id() 146 | assert pk.to_peer_paserk_id() == "" 147 | 148 | def test_v1_public_verify_via_encode_with_wrong_key(self): 149 | sk = Key.new(1, "public", load_key("keys/private_key_rsa.pem")) 150 | pk = Key.new(1, "public", load_key("keys/public_key_rsa_2.pem")) 151 | token = pyseto.encode(sk, b"Hello world!") 152 | with pytest.raises(VerifyError) as err: 153 | pyseto.decode(pk, token) 154 | pytest.fail("pyseto.decode() should fail.") 155 | assert "Failed to verify." in str(err.value) 156 | 157 | @pytest.mark.parametrize( 158 | "paserk, msg", 159 | [ 160 | ("xx.public.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 161 | ("k2.public.AAAAAAAAAAAAAAAA", "Invalid PASERK version: k2."), 162 | ("k1.public.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 163 | ("k1.local.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 164 | ("k1.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 165 | ("k1.local.AAAAAAAAAAAAAAAA", "Invalid PASERK type: local."), 166 | ( 167 | "k1.local-wrap.AAAAAAAAAAAAAAAA", 168 | "Invalid PASERK type: local-wrap.", 169 | ), 170 | ( 171 | "k1.secret-wrap.AAAAAAAAAAAAAAAA", 172 | "secret-wrap needs wrapping_key.", 173 | ), 174 | ], 175 | ) 176 | def test_vl_public_from_paserk_with_invalid_args(self, paserk, msg): 177 | with pytest.raises(ValueError) as err: 178 | V1Public.from_paserk(paserk) 179 | pytest.fail("Key.from_paserk should fail.") 180 | assert msg in str(err.value) 181 | 182 | @pytest.mark.parametrize( 183 | "paserk, msg", 184 | [ 185 | ("xx.secret-wrap.pie.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 186 | ("k1.secret-wrap.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 187 | ("k1.secret-wrap.xxx.AAAAAAAAAAAAAAAA", "Unknown wrapping algorithm: xxx."), 188 | ("k1.xxx.pie.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 189 | ], 190 | ) 191 | def test_v1_public_from_paserk_with_wrapping_key_and_invalid_args(self, paserk, msg): 192 | with pytest.raises(ValueError) as err: 193 | V1Public.from_paserk(paserk, wrapping_key=token_bytes(32)) 194 | pytest.fail("Key.from_paserk should fail.") 195 | assert msg in str(err.value) 196 | -------------------------------------------------------------------------------- /docs/paserk_usage.rst: -------------------------------------------------------------------------------- 1 | PASERK Usage Examples 2 | ===================== 3 | 4 | `PASERK (Platform-Agnostic Serialized Keys)`_ is an extension to PASETO that provides key-wrapping and serialization. 5 | 6 | This page shows various examples to use PySETO with PASERK. 7 | 8 | .. contents:: 9 | :local: 10 | 11 | Serializing/Deserializing PASERK 12 | -------------------------------- 13 | 14 | ``pyseto.Key`` used for encryption and signature can be generated from PASERK or converted to PASERK as follows: 15 | 16 | .. code-block:: python 17 | 18 | import pyseto 19 | from pyseto import Key 20 | 21 | # pyseto.Key can be generated from PASERK. 22 | symmetric_key = Key.new(version=4, purpose="local", key=b"our-secret") 23 | private_key = Key.from_paserk( 24 | "k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog" 25 | ) 26 | public_key = Key.from_paserk("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI") 27 | 28 | token = pyseto.encode( 29 | private_key, 30 | b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}', 31 | ) 32 | decoded = pyseto.decode(public_key, token) 33 | 34 | assert ( 35 | decoded.payload 36 | == b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 37 | ) 38 | 39 | # PASERK can be derived from pyseto.Key. 40 | assert symmetric_key.to_paserk() == "k4.local.b3VyLXNlY3JldA" 41 | assert ( 42 | private_key.to_paserk() 43 | == "k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog" 44 | ) 45 | assert public_key.to_paserk() == "k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI" 46 | 47 | Serializing PASERK ID 48 | --------------------- 49 | 50 | ``pyseto.Key`` can also be converted to PASERK ID as follows: 51 | 52 | .. code-block:: python 53 | 54 | import pyseto 55 | from pyseto import Key 56 | 57 | # pyseto.Key can be generated from PASERK. 58 | symmetric_key = Key.new(version=4, purpose="local", key=b"our-secret") 59 | private_key = Key.from_paserk( 60 | "k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog" 61 | ) 62 | public_key = Key.from_paserk("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI") 63 | 64 | # PASERK ID can be derived from pyseto.Key. 65 | assert ( 66 | symmetric_key.to_paserk_id() 67 | == "k4.lid._D6kgTzxgiPGk35gMj9bukgj4En2H94u22wVX9zaoh05" 68 | ) 69 | assert ( 70 | private_key.to_paserk() 71 | == "k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog" 72 | ) 73 | assert ( 74 | public_key.to_paserk_id() == "k4.pid.yh4-bJYjOYAG6CWy0zsfPmpKylxS7uAWrxqVmBN2KAiJ" 75 | ) 76 | 77 | Key Wrapping 78 | ------------ 79 | 80 | If you call ``to_paserk`` with ``wrapping_key``, you can get a wrapped (encrypted) PASERK with the wrapping key. 81 | The wrapped PASERK can be decrypted by calling ``from_paserk`` with ``wrapping key``. 82 | 83 | In case of ``local-wrap.pie``: 84 | 85 | .. code-block:: python 86 | 87 | import pyseto 88 | from pyseto import Key 89 | 90 | raw_key = Key.new(version=4, purpose="local", key=b"our-secret") 91 | wrapping_key = token_bytes(32) 92 | wpk = raw_key.to_paserk(wrapping_key=wrapping_key) 93 | token = pyseto.encode( 94 | raw_key, b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 95 | ) 96 | 97 | unwrapped_key = Key.from_paserk(wpk, wrapping_key=wrapping_key) 98 | decoded = pyseto.decode(unwrapped_key, token) 99 | 100 | # assert wpk == "k4.local-wrap.pie.TNKEwC4K1xBcgJ_GiwWAoRlQFE33HJO3oN9DHEZ05pieSCd-W7bgAL64VG9TZ_pBkuNBFHNrfOGHtnfnhYGdbz5-x3CxShhPJxg" 101 | assert ( 102 | decoded.payload 103 | == b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 104 | ) 105 | 106 | In case of ``secret-wrap.pie``: 107 | 108 | .. code-block:: python 109 | 110 | import pyseto 111 | from pyseto import Key 112 | 113 | raw_private_key = Key.from_paserk( 114 | "k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog" 115 | ) 116 | wrapping_key = token_bytes(32) 117 | wpk = raw_private_key.to_paserk(wrapping_key=wrapping_key) 118 | unwrapped_private_key = Key.from_paserk(wpk, wrapping_key=wrapping_key) 119 | token = pyseto.encode( 120 | unwrapped_private_key, 121 | b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}', 122 | ) 123 | 124 | public_key = Key.from_paserk("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI") 125 | decoded = pyseto.decode(public_key, token) 126 | 127 | # assert wpk == "k4.secret-wrap.pie.excv7V4-NaECy5hpji-tkSkMvyjsAgNxA-mGALgdjyvGNyDlTb89bJ35R1e3tILgbMpEW5WXMXzySe2T-sBz-ZAcs1j7rbD3ZWvsBTM6K5N9wWfAxbR4ppCXH_H5__9yY-kBaF2NimyAJyduhOhSmqLm6TTSucpAOakEJOXePW8" 128 | assert ( 129 | decoded.payload 130 | == b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 131 | ) 132 | 133 | Password-based Key Encryption 134 | ----------------------------- 135 | 136 | If you call ``to_paserk`` with ``password``, you can get a wrapped (encrypted) PASERK with the password. 137 | The wrapped PASERK can be decrypted by calling ``from_paserk`` with ``passwrod``. 138 | 139 | In case of ``local-pw``: 140 | 141 | .. code-block:: python 142 | 143 | import pyseto 144 | from pyseto import Key 145 | 146 | raw_key = Key.new(version=4, purpose="local", key=b"our-secret") 147 | token = pyseto.encode( 148 | raw_key, b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 149 | ) 150 | 151 | wpk = raw_key.to_paserk(password="our-secret") 152 | unwrapped_key = Key.from_paserk(wpk, password="our-secret") 153 | decoded = pyseto.decode(unwrapped_key, token) 154 | 155 | # assert wpk == "k4.local-pw.HrCs9Pu-2LB0l7jkHB-x2gAAAAAA8AAAAAAAAgAAAAGttW0IHZjQCHJdg-Vc3tqO_GSLR4vzLl-yrKk2I-l8YHj6jWpC0lQB2Z7uzTtVyV1rd_EZQPzHdw5VOtyucP0FkCU" 156 | assert ( 157 | decoded.payload 158 | == b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 159 | ) 160 | 161 | In case of ``secret-pw``: 162 | 163 | .. code-block:: python 164 | 165 | import pyseto 166 | from pyseto import Key 167 | 168 | raw_private_key = Key.from_paserk( 169 | "k4.secret.tMv7Q99M4hByfZU-SnEzB_oZu32fhQQUONnhG5QqN3Qeudu7vAR8A_1wYE4AcfCYfhayi3VyJcEfAEFdDiCxog" 170 | ) 171 | wpk = raw_private_key.to_paserk(password="our-secret") 172 | unwrapped_private_key = Key.from_paserk(wpk, password="our-secret") 173 | token = pyseto.encode( 174 | unwrapped_private_key, 175 | b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}', 176 | ) 177 | 178 | public_key = Key.from_paserk("k4.public.Hrnbu7wEfAP9cGBOAHHwmH4Wsot1ciXBHwBBXQ4gsaI") 179 | decoded = pyseto.decode(public_key, token) 180 | 181 | # assert wpk == "k4.secret-pw.MEMW4K1MaD5nWigCLyEyFAAAAAAA8AAAAAAAAgAAAAFU-tArtryNVjS2n2hCYiM11V6tOyuIog69Bjb0yNZanrLJ3afGclb3kPzQ6IhK8ob9E4QgRdEALGWCizZ0RCPFF_M95IQDfmdYKC0Er656UgKUK4UKG9JlxP4o81UwoJoZYz_D1zTlltipEa5RiNvUtNU8vLKoGSY" 182 | assert ( 183 | decoded.payload 184 | == b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 185 | ) 186 | 187 | Asymmetric Encryption 188 | --------------------- 189 | 190 | At this time, PySETO supports asymmetric encryption (key sealing) for `v2` and `v4`. 191 | 192 | .. code-block:: python 193 | 194 | import pyseto 195 | from pyseto import Key 196 | 197 | private_key_pem = b"-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VuBCIEIFAF7jSCZHFgWvC8hUkXr55Az6Pot2g4zOAUxck0/6x8\n-----END PRIVATE KEY-----" 198 | public_key_pem = b"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VuAyEAFv8IXsICYj0paznDK/99GyCsFOIGnfY87ayyNSIvSB4=\n-----END PUBLIC KEY-----" 199 | 200 | raw_key = Key.new(version=4, purpose="local", key=b"our-secret") 201 | token = pyseto.encode( 202 | raw_key, 203 | b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}', 204 | ) 205 | 206 | sealed_key = raw_key.to_paserk(sealing_key=public_key_pem) 207 | unsealed_key = Key.from_paserk(sealed_key, unsealing_key=private_key_pem) 208 | decoded = pyseto.decode(unsealed_key, token) 209 | 210 | assert ( 211 | decoded.payload 212 | == b'{"data": "this is a signed message", "exp": "2022-01-01T00:00:00+00:00"}' 213 | ) 214 | 215 | .. _`PASERK (Platform-Agnostic Serialized Keys)`: https://github.com/paseto-standard/paserk 216 | -------------------------------------------------------------------------------- /pyseto/key_interface.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Union 2 | 3 | from .exceptions import NotSupportedError 4 | 5 | 6 | class KeyInterface: 7 | """ 8 | The key interface class for PASETO. 9 | 10 | :func:`pyseto.Key.new ` returns an object which has this interface. 11 | """ 12 | 13 | _VERSION = 0 14 | _TYPE = "" 15 | 16 | def __init__(self, version: int, purpose: str, key: Any): 17 | self._version = version 18 | self._purpose = purpose 19 | self._header = (f"v{self._version}." + self._purpose + ".").encode("utf-8") 20 | self._sig_size = 0 21 | self._key: Any = key 22 | if not self._key: 23 | raise ValueError("key must be specified.") 24 | self._is_secret = True 25 | return 26 | 27 | @property 28 | def version(self) -> int: 29 | """ 30 | The version of the key. It will be ``1``, ``2``, ``3`` or ``4``. 31 | """ 32 | return self._version 33 | 34 | @property 35 | def purpose(self) -> str: 36 | """ 37 | The purpose of the key. It will be ``"local"`` or ``"public"``. 38 | """ 39 | return self._purpose 40 | 41 | @property 42 | def header(self) -> bytes: 43 | """ 44 | The header value for a PASETO token. It will be ``"v.."``. 45 | For example, ``"v1.local."``. 46 | """ 47 | return self._header 48 | 49 | @property 50 | def is_secret(self) -> bool: 51 | """ 52 | If it is True, the key is a symmetric key or an asymmetric secret key. 53 | """ 54 | return self._is_secret 55 | 56 | # @property 57 | # def key(self) -> Any: 58 | # """ 59 | # Byte string or pyca/cryptography key object of the key. 60 | # If the key is ``public``, the key object is one of the pyca/cryptography objects bellow: 61 | 62 | # - ``RSAPrivateKey`` 63 | # - ``RSAPublicKey`` 64 | # - ``Ed25519PrivateKey`` 65 | # - ``Ed25519PublicKey`` 66 | # - ``EllipticCurvePrivateKey`` 67 | # - ``EllipticCurvePublicKey`` 68 | # """ 69 | # return self._key 70 | 71 | def to_paserk( 72 | self, 73 | wrapping_key: Union[bytes, str] = b"", 74 | password: Union[bytes, str] = b"", 75 | sealing_key: Union[bytes, str] = b"", 76 | iteration: int = 100000, 77 | memory_cost: int = 15 * 1024, 78 | time_cost: int = 2, 79 | parallelism: int = 1, 80 | ) -> str: 81 | """ 82 | Returns the PASERK expression of the key. 83 | 84 | Args: 85 | wrapping_key (Union[bytes, str]): A wrapping key to wrap the key. 86 | If the `wrapping_key` is specified, `password` should not be 87 | specified. 88 | password (Union[bytes, str]): A password to wrap the key. If the 89 | `password` is specified, `wrapping_key` should not be specified. 90 | iteration (int): An iteration count used for password-based key 91 | wrapping. This argument will only be used when the `password` is 92 | specified. 93 | memory_cost (int): Amount of memory to use for password-based key 94 | wrapping using argon2. This argument will only be used when 95 | the `password` is specified for `v2/v4` key. 96 | time_cost (int): Number of iterations to perform for password-based 97 | key wrapping using argon2. This argument will only be used when 98 | the `password` is specified for `v2/v4` key. 99 | parallelism (int): Degree of parallelism for password-based key 100 | wrapping using argon2. This argument will only be used when 101 | the `password` is specified for `v2/v4` key. 102 | Returns: 103 | str: A PASERK string. 104 | Raise: 105 | ValueError: Invalid arguments. 106 | EncryptError: Failed to wrap the key. 107 | """ 108 | raise NotImplementedError("The PASERK expression for the key is not supported yet.") 109 | 110 | def to_paserk_id(self) -> str: 111 | """ 112 | Returns the PASERK ID of the key. 113 | 114 | Returns: 115 | str: A PASERK ID string. 116 | """ 117 | raise NotImplementedError("The PASERK ID for the key is not supported yet.") 118 | 119 | def to_peer_paserk_id(self) -> str: 120 | """ 121 | Returns the peer(public) PASERK ID of the key. 122 | It can be used only in case that the key is `k2.secret` or `k4.secret`. 123 | 124 | Returns: 125 | str: A peer PASERK ID string. If the key is neither `k2.secret` nor 126 | `k4.secret`, an empty string will be returned. 127 | """ 128 | return "" 129 | 130 | def encrypt( 131 | self, 132 | payload: bytes, 133 | footer: bytes = b"", 134 | implicit_assertion: bytes = b"", 135 | nonce: bytes = b"", 136 | ) -> bytes: 137 | """ 138 | Encrypts a message to a PASETO token with the key. 139 | 140 | This function is calld in :func:`pyseto.encode ` so you 141 | don't need to call it directly. 142 | 143 | Args: 144 | payload (bytes): A message to be encrypted which will be the payload 145 | part of the PASETO token. 146 | footer (bytes): A footer. 147 | implicit_assertion (Union[bytes, str]): An implicit assertion. It is 148 | only used in ``v3`` or ``v4``. 149 | nonce (bytes): A nonce. 150 | Returns: 151 | bytes: A PASETO token. 152 | Raise: 153 | ValueError: Invalid arguments. 154 | EncryptError: Failed to encrypt the message. 155 | NotSupportedError: The key does not support the operation. 156 | """ 157 | raise NotSupportedError("A key for public does not have encrypt().") 158 | 159 | def decrypt(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 160 | """ 161 | Decrypts an encrypted PASETO token with the key. 162 | 163 | This function is calld in :func:`pyseto.decode ` so you 164 | don't need to call it directly. 165 | 166 | Args: 167 | payload (bytes): A message to be decrypted which is the payload part 168 | of the PASETO token. 169 | footer (bytes): A footer. 170 | implicit_assertion (Union[bytes, str]): An implicit assertion. It is 171 | only used in ``v3`` or ``v4``. 172 | Returns: 173 | bytes: A dcrypted payload. 174 | Raise: 175 | ValueError: Invalid arguments. 176 | DecryptError: Failed to decrypt the message. 177 | NotSupportedError: The key does not support the operation. 178 | """ 179 | raise NotSupportedError("A key for public does not have decrypt().") 180 | 181 | def sign(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 182 | """ 183 | Signs a message with the key and makes a PASETO token. 184 | 185 | This function is calld in :func:`pyseto.encode ` so you 186 | don't need to call it directly. 187 | 188 | Args: 189 | payload (bytes): A message to be signed and encoded which will be 190 | the payload part of the PASETO token. 191 | footer (bytes): A footer. 192 | implicit_assertion (Union[bytes, str]): An implicit assertion. It is 193 | only used in ``v3`` or ``v4``. 194 | nonce (bytes): A nonce. 195 | Returns: 196 | bytes: A PASETO token. 197 | Raise: 198 | ValueError: Invalid arguments. 199 | EncryptError: Failed to sign the message. 200 | NotSupportedError: The key does not support the operation. 201 | """ 202 | raise NotSupportedError("A key for local does not have sign().") 203 | 204 | def verify(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 205 | """ 206 | Verifies and decodes a signed PASETO token with the key. 207 | 208 | This function is calld in :func:`pyseto.decode ` so you 209 | don't need to call it directly. 210 | 211 | Args: 212 | payload (bytes): A message to be verified and decoded which is the 213 | payload part of the PASETO token. 214 | footer (bytes): A footer. 215 | implicit_assertion (Union[bytes, str]): An implicit assertion. It is 216 | only used in ``v3`` or ``v4``. 217 | Returns: 218 | bytes: A verified and decoded payload. 219 | Raise: 220 | ValueError: Invalid arguments. 221 | DecryptError: Failed to verify the message. 222 | NotSupportedError: The key does not support the operation. 223 | """ 224 | raise NotSupportedError("A key for local does not have verify().") 225 | -------------------------------------------------------------------------------- /tests/test_v3.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | 3 | import pytest 4 | 5 | import pyseto 6 | from pyseto import DecryptError, EncryptError, Key, SignError, VerifyError 7 | from pyseto.versions.v3 import V3Local, V3Public 8 | 9 | from .utils import load_key 10 | 11 | 12 | class TestV3Local: 13 | """ 14 | Tests for v3.local. 15 | """ 16 | 17 | @pytest.mark.parametrize( 18 | "key, msg", 19 | [ 20 | (b"", "key must be specified."), 21 | ], 22 | ) 23 | def test_v3_local_new_with_invalid_arg(self, key, msg): 24 | with pytest.raises(ValueError) as err: 25 | Key.new(3, "local", key) 26 | pytest.fail("Key.new() should fail.") 27 | assert msg in str(err.value) 28 | 29 | def test_v3_local_decrypt_via_decode_with_wrong_key(self): 30 | k1 = Key.new(3, "local", b"our-secret") 31 | k2 = Key.new(3, "local", b"others-secret") 32 | token = pyseto.encode(k1, b"Hello world!") 33 | with pytest.raises(DecryptError) as err: 34 | pyseto.decode(k2, token) 35 | pytest.fail("pyseto.decode() should fail.") 36 | assert "Failed to decrypt." in str(err.value) 37 | 38 | def test_v3_local_encrypt_with_invalid_arg(self): 39 | k = Key.new(3, "local", b"our-secret") 40 | with pytest.raises(EncryptError) as err: 41 | k.encrypt(None) 42 | pytest.fail("pyseto.encrypt() should fail.") 43 | assert "Failed to encrypt." in str(err.value) 44 | 45 | @pytest.mark.parametrize( 46 | "nonce", 47 | [ 48 | token_bytes(1), 49 | token_bytes(8), 50 | token_bytes(31), 51 | token_bytes(33), 52 | token_bytes(64), 53 | ], 54 | ) 55 | def test_v3_local_encrypt_via_encode_with_wrong_nonce(self, nonce): 56 | k = Key.new(3, "local", b"our-secret") 57 | with pytest.raises(ValueError) as err: 58 | pyseto.encode(k, b"Hello world!", nonce=nonce) 59 | pytest.fail("pyseto.encode() should fail.") 60 | assert "nonce must be 32 bytes long." in str(err.value) 61 | 62 | @pytest.mark.parametrize( 63 | "paserk, msg", 64 | [ 65 | ("xx.local.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 66 | ("k4.local.AAAAAAAAAAAAAAAA", "Invalid PASERK version: k4."), 67 | ("k3.local.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 68 | ("k3.public.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 69 | ("k3.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 70 | ("k3.public.AAAAAAAAAAAAAAAA", "Invalid PASERK type: public."), 71 | ( 72 | "k3.local-wrap.AAAAAAAAAAAAAAAA", 73 | "local-wrap needs wrapping_key.", 74 | ), 75 | ( 76 | "k3.secret-wrap.AAAAAAAAAAAAAAAA", 77 | "Invalid PASERK type: secret-wrap.", 78 | ), 79 | ( 80 | "k3.local-pw.AAAAAAAAAAAAAAAA", 81 | "local-pw needs password.", 82 | ), 83 | ], 84 | ) 85 | def test_v3_local_from_paserk_with_invalid_args(self, paserk, msg): 86 | with pytest.raises(ValueError) as err: 87 | V3Local.from_paserk(paserk) 88 | pytest.fail("Key.from_paserk should fail.") 89 | assert msg in str(err.value) 90 | 91 | @pytest.mark.parametrize( 92 | "paserk, msg", 93 | [ 94 | ("xx.local-wrap.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 95 | ("k3.local-wrap.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 96 | ("k3.local-wrap.xxx.AAAAAAAAAAAAAAAA", "Unknown wrapping algorithm: xxx."), 97 | ("k3.xxx.pie.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 98 | ], 99 | ) 100 | def test_v3_local_from_paserk_with_wrapping_key_and_invalid_args(self, paserk, msg): 101 | with pytest.raises(ValueError) as err: 102 | V3Local.from_paserk(paserk, wrapping_key=token_bytes(32)) 103 | pytest.fail("Key.from_paserk should fail.") 104 | assert msg in str(err.value) 105 | 106 | def test_v3_local_to_peer_paserk_id(self): 107 | k = Key.new(3, "local", b"our-secret") 108 | assert k.to_peer_paserk_id() == "" 109 | 110 | # @pytest.mark.parametrize( 111 | # "paserk, msg", 112 | # [ 113 | # ("k3.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 114 | # ], 115 | # ) 116 | # def test_v3_local_from_paserk_with_unsealing_key_and_invalid_args( 117 | # self, paserk, msg 118 | # ): 119 | 120 | # with pytest.raises(ValueError) as err: 121 | # V3Local.from_paserk(paserk, unsealing_key=token_bytes(32)) 122 | # pytest.fail("Key.from_paserk should fail.") 123 | # assert msg in str(err.value) 124 | 125 | # def test_v3_local_from_paserk_with_wrong_unsealing_key(self): 126 | 127 | # k = Key.new(3, "local", token_bytes(32)) 128 | # with open(get_path("keys/public_key_x25519.pem")) as key_file: 129 | # sealed_key = k.to_paserk(sealing_key=key_file.read()) 130 | 131 | # with open(get_path("keys/private_key_x25519_2.pem")) as key_file: 132 | # unsealing_key = key_file.read() 133 | 134 | # with pytest.raises(DecryptError) as err: 135 | # Key.from_paserk(sealed_key, unsealing_key=unsealing_key) 136 | # pytest.fail("Key.from_paserk should fail.") 137 | # assert "Failed to unseal a key." in str(err.value) 138 | 139 | 140 | class TestV3Public: 141 | """ 142 | Tests for v3.public. 143 | """ 144 | 145 | def test_v3_public_to_paserk_id(self): 146 | sk = Key.new(3, "public", load_key("keys/private_key_ecdsa_p384.pem")) 147 | pk = Key.new(3, "public", load_key("keys/public_key_ecdsa_p384.pem")) 148 | assert sk.to_peer_paserk_id() == pk.to_paserk_id() 149 | assert pk.to_peer_paserk_id() == "" 150 | 151 | def test_v3_public_verify_via_encode_with_wrong_key(self): 152 | sk = Key.new(3, "public", load_key("keys/private_key_ecdsa_p384.pem")) 153 | pk = Key.new(3, "public", load_key("keys/public_key_ecdsa_p384_2.pem")) 154 | token = pyseto.encode(sk, b"Hello world!") 155 | with pytest.raises(VerifyError) as err: 156 | pyseto.decode(pk, token) 157 | pytest.fail("pyseto.decode() should fail.") 158 | assert "Failed to verify." in str(err.value) 159 | 160 | @pytest.mark.parametrize( 161 | "paserk, msg", 162 | [ 163 | ("xx.public.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 164 | ("k4.public.AAAAAAAAAAAAAAAA", "Invalid PASERK version: k4."), 165 | ("k3.public.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 166 | ("k3.local.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 167 | ("k3.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 168 | ("k3.local.AAAAAAAAAAAAAAAA", "Invalid PASERK type: local."), 169 | ( 170 | "k3.local-wrap.AAAAAAAAAAAAAAAA", 171 | "Invalid PASERK type: local-wrap.", 172 | ), 173 | ( 174 | "k3.secret-wrap.AAAAAAAAAAAAAAAA", 175 | "secret-wrap needs wrapping_key.", 176 | ), 177 | ], 178 | ) 179 | def test_v3_public_from_paserk_with_invalid_args(self, paserk, msg): 180 | with pytest.raises(ValueError) as err: 181 | V3Public.from_paserk(paserk) 182 | pytest.fail("Key.from_paserk should fail.") 183 | assert msg in str(err.value) 184 | 185 | @pytest.mark.parametrize( 186 | "paserk, msg", 187 | [ 188 | ("xx.secret-wrap.pie.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 189 | ("k3.secret-wrap.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 190 | ("k3.secret-wrap.xxx.AAAAAAAAAAAAAAAA", "Unknown wrapping algorithm: xxx."), 191 | ("k3.xxx.pie.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 192 | ], 193 | ) 194 | def test_v3_public_from_paserk_with_wrapping_key_and_invalid_args(self, paserk, msg): 195 | with pytest.raises(ValueError) as err: 196 | V3Public.from_paserk(paserk, wrapping_key=token_bytes(32)) 197 | pytest.fail("Key.from_paserk should fail.") 198 | assert msg in str(err.value) 199 | 200 | def test_v3_public_from_public_bytes_with_invalid_args(self): 201 | with pytest.raises(ValueError) as err: 202 | V3Public.from_public_bytes(b"xxx") 203 | pytest.fail("Key.from_paserk should fail.") 204 | assert "Invalid bytes for the key." in str(err.value) 205 | 206 | def test_v3_public_sign_via_encode_with_invalid_key(self): 207 | with pytest.raises((ValueError, SignError)) as err: 208 | k = Key.from_paserk( 209 | "k3.secret-pw.mXsR2qVqmcDxmSWeQCnCwNeIxe5RDQ3ehnQvdXFj-YgAAAPoFI8eRXCL8PFpVW_CWOvGHnvMPy0BkMlKF1AtmBYGKold9i-ALC2oflkemYdbncrHbiKGd8zfjTQu2tTo2ayOMHybk_-hhopwJ2IUallYfLfUzPuqvtOQfVxXLtUBPnmR75dhRiPDgzdIO1OMbqa3Z1LDevvzbrcPyhHqmJSZioeJ7j1Mu8DJOvrIK0pWHmjDq_eg4YFnaOgz7I3Tkxx89A", 210 | password="correct horse battery staple".encode("utf-8").hex(), 211 | ) 212 | pyseto.encode(k, b"Hello world!") 213 | pytest.fail("pyseto.sign() should fail.") 214 | assert "Failed to sign" or "Invalid EC Key" in str(err.value) 215 | -------------------------------------------------------------------------------- /tests/vectors/PASERK/k1.seal.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PASERK k1.seal Test Vectors", 3 | "tests": [ 4 | { 5 | "name": "k1.seal-1", 6 | "sealing-secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKAIBAAKCAgEAzmVvxmE2BDmhYZ43Wc379SdoFE7TJ6R9oPmkNieDQ3PWt7tH\nT4nzxSF+/nhS3ii64VAXxz1jPnCiA+L5TiFJ1xOk4vcbi6Dw3cwq/FzIqoJC0JO5\ninA91MZvDaCApe4t1e9piBYRVfHaQvnTsDCvm/5eHFsCQid1386od70ms6fDWF/W\nTirCsUs3AmenQ4jbQfHrc+ZcvEkaeMDC3sQZPvIggLLXsAO4bqbHN37GCNjpKhIC\npyqC3rNtjxjRjUrMEpxhw3stXSg++YchtSJ4y62cOeiJV/YSbpyuBv3FtO4HRq//\nZg1bhIZZvqVZ5FibvgN2QuK5B/1+XUXR+OzhN8EgGVzz33P5ioUlz+ACPDFmICvb\nmrGP1z9iAjT8hOwpEV4Pjz9zZRMKQgaWkk5v5MGDn2hVB+hM/8zIjxtc6f4MmJdG\n6auC61ZZeYPe1w1Axr1SfIVFUuo27Slk5EAFyjwRIyyWUXxTX0ZBPUK0rwNRjfML\nm/LXQCR6+WarKbvn/yDgmkpBRYS/6u9sL9EOBJgGXLk3VA6pO64kw7+WXNzK+O4b\n27pijw0yPo0ipfM0XFAEk+6Klz3eocEnu9Zfl0xXjJIbOmS7ELua1EloZ47TBaka\nFP52tac4o/JWrc7RciqVzu5DLjYoQfbrQQeAaLZNx7bFgQ/l1X6FhouHHksCAwEA\nAQKCAgA78aYG/dDMZViDm4oD4RleWDWYQFR/XPzHtfmFaPBstMYV0qShazWLeXEC\nwzt+HmI4ciSVzzR1vAyyCEfT80MY3jGzYYV0hieuo0+Qv/nf27lADseCd5rdo0w5\nrvXuO0DAs6n4xj/+sCFt9lUPBPTb34LBxYTgZcmMWN8A98S3xO+tSRha13x4NZoi\nbqxbpvHXM0XpAWzJmom/rGxsepPCflwGROrzygDU0kdlzMIDa8w8VcLDfktIFuCp\nANJLsP+YICccuTexqLCxlBesHfjrXt7SysCo9WThD0xT53x2UfN3CkKpBxQfZ03F\nWwzCjyGcjDDauDJ5KZx7xn13SIUYQAyh0TIgYq5lRTYycJ6LzQ8zqz/irotqXlv2\n84QnyzPtvUyQynB7FoR30TDHSDzOjVkvWVySqPiuxIgESCjAaQYqEc6sfwXcp4XZ\neOU59SSsBSt0HpnrgZhGA2vK+lPfn91n95MaII4vmE1PpUef8PPtwbF4cM/YCTfW\nBAnCs21VV+AJa79Ae6/TmIUr5+/saL5CWcMXljTtCSXarJJM9AUOaU+bNxYo0TVF\nbLyT4tvutSNpb5h2RDfmVADf46WP1ioH/HazdPNe93+8SoIjNIKbEBq5yMY+Vr7q\neHB+3aY5TohF8Pf9xSBpbVA2FzA3EVwaC94hhun1q38JFbRlcQKCAQEA/7/vHkUm\nm1wIAJMr9mK9AdtNm0VAwpk9D2bIV8QH/gfTFZ81LVJWEXDBVvC5uoFjS7RcHGF8\nbWAglpCKtZiIfwKMc5sPJAwksCWg6mDbBWE0V1Z/UfRLpwtc9NzO/s1eUp6UH5XK\nMnZmuD6QaCvbMNoHNUt9gWtnPZ/gnUloXmFnYBu0mHj7MdQoA4ZQfCiy+sQ04s7F\nzAMWfbe5PxwT9/woszJ+uN69ai4PKcnc0WTcgyvZqU656ZLFmin8RbU2V6hg6NMQ\nIjJor6psXb74iEPa5Vb1VAMNmKgz9sOePbymsBJ4Y63cfgmpmPWLOgX5pDKO3Dw4\n7tYzeXD32eEJ/wKCAQEAzpkjrwqWrFsD/TkY8K8sZSp1rPjUj69VfWSSnJgENw6y\ncbmfx7Vw7tR2F6i+qQT7m4agJT6RBw1epN8od499Ql+uC9QeIYm3pdG0TXBIBCA4\n03imULCgdcVFUmRvqVX8A723rq3RPTdgkUesJXVqqk7fPql3zGrk9xrN9GWQqWh0\njXQnvCgMaojIKYOF51ZmBZdwORv0mC6B9rAvFD+7Q9e89yARu4gR/P47W641kgld\nhCLocWB57W5x7bThNYLsrJG2wCMSdXFjw8JNsMYO7s9M8a6Z4DIyFyezzenY2gtQ\nPjXqaqegrioS3q1woDrJblP8yb+uHfKZy8aD0RLztQKCAQBi6NYPvOq7dxJZNpHw\nDivPBgOzo0ryd9VXmYat+tCkfF40pIgqaQeEYzlC6ILMELJYWv6ssz8uBdlUob4j\nkURo6pFrPHLUnCWsQkFDpAXQxNE7XeaLyZFgn1JqGOOtQ7vQ9CeRN5slfQkpBHlQ\n8HUrJYdYI0P8w48AFE8IRZWpur4CO3TS6ycrFEQNaOrDufHObgeOGC4DQsZ2BJIO\nSEuowsry0vqTgQF8iSewH7PY/8sQp+rcQehA31Sw1MAOLZFAwYwJP2ej8h7uoVib\nwPnZqXSE8eabgTrG6XZ/XxRaCBXnTp1k357A1/fRglVAMYNk73C02E2kgQ8TTo9s\nok39AoIBAQCo57KwzOtahh7AzAmD3PNi0k/a1qSRxDsUhUEIHZB0ouNo7uWelMx7\ntd/GgANAk/5QrMQJLxnKtjeGe5vOA9XYifj1Wro3mSw3uTa7iOyX0vAilCUFGyJU\npq+CKPLRcqbTOCwP97N1ZOziWcJ37YMDMfB6fnqe+VWwYI25HcAjgG6ppylFP0jH\nYISkzA6Rj6VhNOpfBmf2cy91y5zx5Rjo4lxvfhyBQUHToNZOoiLR/i4idZer+cA1\nSXKDcLoe0adFfuv2MbZJpiZ2SUjTGVnkDD9P5/uNu/wPyjnKQ0EzIsS718CK6fkH\n6wX6X7oQhX9hX/Dv0HI9sbXjT609JU1xAoIBAE5aeA8a53gP9TyfrR6YmZUpgKAP\ntsjkWG6TPzjVnj+ttPLTDEfJ+LvDIY/UCsAcwie/bzVcYaqMm/ohIrqblKksyVjk\nOnUABuZSo8aDNcvGsF7KfBkkZ5svvJ5qFvVHSmvqJSE7uug7wvtxgje8oTYN8YoT\nZUn4LSus3Gf58E7t4JPJpSVVYM0x/iGDjzDwciNnk7qZJ8/it3x+1kfOshXIcWYR\nfw3Ui5L1gMVIJ1SmBBdXmHRLLNbT3ZDTDLIywNEyXYTiMXWW1GawKPqhXqVhhwiL\n3ynxwLz6UDf7YXIZUD2TTyhHPZugcTN7q/UqBj6yknbtgfu1CBeB8PVoLcE=\n-----END RSA PRIVATE KEY-----", 7 | "sealing-public-key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzmVvxmE2BDmhYZ43Wc37\n9SdoFE7TJ6R9oPmkNieDQ3PWt7tHT4nzxSF+/nhS3ii64VAXxz1jPnCiA+L5TiFJ\n1xOk4vcbi6Dw3cwq/FzIqoJC0JO5inA91MZvDaCApe4t1e9piBYRVfHaQvnTsDCv\nm/5eHFsCQid1386od70ms6fDWF/WTirCsUs3AmenQ4jbQfHrc+ZcvEkaeMDC3sQZ\nPvIggLLXsAO4bqbHN37GCNjpKhICpyqC3rNtjxjRjUrMEpxhw3stXSg++YchtSJ4\ny62cOeiJV/YSbpyuBv3FtO4HRq//Zg1bhIZZvqVZ5FibvgN2QuK5B/1+XUXR+Ozh\nN8EgGVzz33P5ioUlz+ACPDFmICvbmrGP1z9iAjT8hOwpEV4Pjz9zZRMKQgaWkk5v\n5MGDn2hVB+hM/8zIjxtc6f4MmJdG6auC61ZZeYPe1w1Axr1SfIVFUuo27Slk5EAF\nyjwRIyyWUXxTX0ZBPUK0rwNRjfMLm/LXQCR6+WarKbvn/yDgmkpBRYS/6u9sL9EO\nBJgGXLk3VA6pO64kw7+WXNzK+O4b27pijw0yPo0ipfM0XFAEk+6Klz3eocEnu9Zf\nl0xXjJIbOmS7ELua1EloZ47TBakaFP52tac4o/JWrc7RciqVzu5DLjYoQfbrQQeA\naLZNx7bFgQ/l1X6FhouHHksCAwEAAQ==\n-----END PUBLIC KEY-----", 8 | "unsealed": "0000000000000000000000000000000000000000000000000000000000000000", 9 | "paserk": "k1.seal.Rh_7sGJlX63bix8ZR_WF4CuIx3y_qrLopKo_5JmKwAmGeWST1J2HIjwO5-JX1U5LV68mZzWSu3o5mb4P9finTLdNlC0_Legurx15NCq_LvO2UqPRqFt7DGPO722HElQKtJciR0VCuf_WUOyalbgjVfNruYoMvtONqqI1g7MQr1Fa1tFwe5MXhOaX4V7eJ0DodauHj_i6KjX7-T3xllIzfGXQEUEEoSgyrCo4IGCs3E5gRBK-rlrojhDtt8LebZxOwOmOvTIfcTC0m8aGvDm6HPYzBWefiTSnZInoVfI6yt4JOozxqFVzMx2QP3Kok2iCy6VW8386WTeeZ15Jr95Yt0bc3bayYonCDkvEkwpyV6Y-wPRvZpwlt84NsNrPb-WmsPbtDs7MIuun0b089zwfmYcxy0LRDlJhMreTlDkueuJftyV8sbE9FNfWFF34azIwM9xudcsH266OpUGaQH_phl7or66hbEpeSp4A91U5K4nvMXr-ohdLublIF8WZMH_trQCHo0DomXkVHlFSaLnSHyxIJ8KhQCVleJ2ZjKMXeJ0KSgPA7crE4HwB7G5yhsEprRg_pR0481zoF5YOKbelQiRPwEdcI1E4OomcWfdf1OWw469-SBJJQM_zK3sqBiRHW56ZwIGdJaZDdp0GyHOkGSNL_L-LBIJ7P05TxdwjwmpZD8Lcn3O1XNqygIPTbtSLGkylzpX468sMBp6XiyYZy5lcqDnjceNrWkN8zLMmpoDiEUBLje1wA14KWEs9g0HFXwze88O_J0dp-RDEDWmQJQ" 10 | }, 11 | { 12 | "name": "k1.seal-2", 13 | "sealing-secret-key": "-----BEGIN RSA PRIVATE KEY-----\nMIIJKAIBAAKCAgEAzmVvxmE2BDmhYZ43Wc379SdoFE7TJ6R9oPmkNieDQ3PWt7tH\nT4nzxSF+/nhS3ii64VAXxz1jPnCiA+L5TiFJ1xOk4vcbi6Dw3cwq/FzIqoJC0JO5\ninA91MZvDaCApe4t1e9piBYRVfHaQvnTsDCvm/5eHFsCQid1386od70ms6fDWF/W\nTirCsUs3AmenQ4jbQfHrc+ZcvEkaeMDC3sQZPvIggLLXsAO4bqbHN37GCNjpKhIC\npyqC3rNtjxjRjUrMEpxhw3stXSg++YchtSJ4y62cOeiJV/YSbpyuBv3FtO4HRq//\nZg1bhIZZvqVZ5FibvgN2QuK5B/1+XUXR+OzhN8EgGVzz33P5ioUlz+ACPDFmICvb\nmrGP1z9iAjT8hOwpEV4Pjz9zZRMKQgaWkk5v5MGDn2hVB+hM/8zIjxtc6f4MmJdG\n6auC61ZZeYPe1w1Axr1SfIVFUuo27Slk5EAFyjwRIyyWUXxTX0ZBPUK0rwNRjfML\nm/LXQCR6+WarKbvn/yDgmkpBRYS/6u9sL9EOBJgGXLk3VA6pO64kw7+WXNzK+O4b\n27pijw0yPo0ipfM0XFAEk+6Klz3eocEnu9Zfl0xXjJIbOmS7ELua1EloZ47TBaka\nFP52tac4o/JWrc7RciqVzu5DLjYoQfbrQQeAaLZNx7bFgQ/l1X6FhouHHksCAwEA\nAQKCAgA78aYG/dDMZViDm4oD4RleWDWYQFR/XPzHtfmFaPBstMYV0qShazWLeXEC\nwzt+HmI4ciSVzzR1vAyyCEfT80MY3jGzYYV0hieuo0+Qv/nf27lADseCd5rdo0w5\nrvXuO0DAs6n4xj/+sCFt9lUPBPTb34LBxYTgZcmMWN8A98S3xO+tSRha13x4NZoi\nbqxbpvHXM0XpAWzJmom/rGxsepPCflwGROrzygDU0kdlzMIDa8w8VcLDfktIFuCp\nANJLsP+YICccuTexqLCxlBesHfjrXt7SysCo9WThD0xT53x2UfN3CkKpBxQfZ03F\nWwzCjyGcjDDauDJ5KZx7xn13SIUYQAyh0TIgYq5lRTYycJ6LzQ8zqz/irotqXlv2\n84QnyzPtvUyQynB7FoR30TDHSDzOjVkvWVySqPiuxIgESCjAaQYqEc6sfwXcp4XZ\neOU59SSsBSt0HpnrgZhGA2vK+lPfn91n95MaII4vmE1PpUef8PPtwbF4cM/YCTfW\nBAnCs21VV+AJa79Ae6/TmIUr5+/saL5CWcMXljTtCSXarJJM9AUOaU+bNxYo0TVF\nbLyT4tvutSNpb5h2RDfmVADf46WP1ioH/HazdPNe93+8SoIjNIKbEBq5yMY+Vr7q\neHB+3aY5TohF8Pf9xSBpbVA2FzA3EVwaC94hhun1q38JFbRlcQKCAQEA/7/vHkUm\nm1wIAJMr9mK9AdtNm0VAwpk9D2bIV8QH/gfTFZ81LVJWEXDBVvC5uoFjS7RcHGF8\nbWAglpCKtZiIfwKMc5sPJAwksCWg6mDbBWE0V1Z/UfRLpwtc9NzO/s1eUp6UH5XK\nMnZmuD6QaCvbMNoHNUt9gWtnPZ/gnUloXmFnYBu0mHj7MdQoA4ZQfCiy+sQ04s7F\nzAMWfbe5PxwT9/woszJ+uN69ai4PKcnc0WTcgyvZqU656ZLFmin8RbU2V6hg6NMQ\nIjJor6psXb74iEPa5Vb1VAMNmKgz9sOePbymsBJ4Y63cfgmpmPWLOgX5pDKO3Dw4\n7tYzeXD32eEJ/wKCAQEAzpkjrwqWrFsD/TkY8K8sZSp1rPjUj69VfWSSnJgENw6y\ncbmfx7Vw7tR2F6i+qQT7m4agJT6RBw1epN8od499Ql+uC9QeIYm3pdG0TXBIBCA4\n03imULCgdcVFUmRvqVX8A723rq3RPTdgkUesJXVqqk7fPql3zGrk9xrN9GWQqWh0\njXQnvCgMaojIKYOF51ZmBZdwORv0mC6B9rAvFD+7Q9e89yARu4gR/P47W641kgld\nhCLocWB57W5x7bThNYLsrJG2wCMSdXFjw8JNsMYO7s9M8a6Z4DIyFyezzenY2gtQ\nPjXqaqegrioS3q1woDrJblP8yb+uHfKZy8aD0RLztQKCAQBi6NYPvOq7dxJZNpHw\nDivPBgOzo0ryd9VXmYat+tCkfF40pIgqaQeEYzlC6ILMELJYWv6ssz8uBdlUob4j\nkURo6pFrPHLUnCWsQkFDpAXQxNE7XeaLyZFgn1JqGOOtQ7vQ9CeRN5slfQkpBHlQ\n8HUrJYdYI0P8w48AFE8IRZWpur4CO3TS6ycrFEQNaOrDufHObgeOGC4DQsZ2BJIO\nSEuowsry0vqTgQF8iSewH7PY/8sQp+rcQehA31Sw1MAOLZFAwYwJP2ej8h7uoVib\nwPnZqXSE8eabgTrG6XZ/XxRaCBXnTp1k357A1/fRglVAMYNk73C02E2kgQ8TTo9s\nok39AoIBAQCo57KwzOtahh7AzAmD3PNi0k/a1qSRxDsUhUEIHZB0ouNo7uWelMx7\ntd/GgANAk/5QrMQJLxnKtjeGe5vOA9XYifj1Wro3mSw3uTa7iOyX0vAilCUFGyJU\npq+CKPLRcqbTOCwP97N1ZOziWcJ37YMDMfB6fnqe+VWwYI25HcAjgG6ppylFP0jH\nYISkzA6Rj6VhNOpfBmf2cy91y5zx5Rjo4lxvfhyBQUHToNZOoiLR/i4idZer+cA1\nSXKDcLoe0adFfuv2MbZJpiZ2SUjTGVnkDD9P5/uNu/wPyjnKQ0EzIsS718CK6fkH\n6wX6X7oQhX9hX/Dv0HI9sbXjT609JU1xAoIBAE5aeA8a53gP9TyfrR6YmZUpgKAP\ntsjkWG6TPzjVnj+ttPLTDEfJ+LvDIY/UCsAcwie/bzVcYaqMm/ohIrqblKksyVjk\nOnUABuZSo8aDNcvGsF7KfBkkZ5svvJ5qFvVHSmvqJSE7uug7wvtxgje8oTYN8YoT\nZUn4LSus3Gf58E7t4JPJpSVVYM0x/iGDjzDwciNnk7qZJ8/it3x+1kfOshXIcWYR\nfw3Ui5L1gMVIJ1SmBBdXmHRLLNbT3ZDTDLIywNEyXYTiMXWW1GawKPqhXqVhhwiL\n3ynxwLz6UDf7YXIZUD2TTyhHPZugcTN7q/UqBj6yknbtgfu1CBeB8PVoLcE=\n-----END RSA PRIVATE KEY-----", 14 | "sealing-public-key": "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAzmVvxmE2BDmhYZ43Wc37\n9SdoFE7TJ6R9oPmkNieDQ3PWt7tHT4nzxSF+/nhS3ii64VAXxz1jPnCiA+L5TiFJ\n1xOk4vcbi6Dw3cwq/FzIqoJC0JO5inA91MZvDaCApe4t1e9piBYRVfHaQvnTsDCv\nm/5eHFsCQid1386od70ms6fDWF/WTirCsUs3AmenQ4jbQfHrc+ZcvEkaeMDC3sQZ\nPvIggLLXsAO4bqbHN37GCNjpKhICpyqC3rNtjxjRjUrMEpxhw3stXSg++YchtSJ4\ny62cOeiJV/YSbpyuBv3FtO4HRq//Zg1bhIZZvqVZ5FibvgN2QuK5B/1+XUXR+Ozh\nN8EgGVzz33P5ioUlz+ACPDFmICvbmrGP1z9iAjT8hOwpEV4Pjz9zZRMKQgaWkk5v\n5MGDn2hVB+hM/8zIjxtc6f4MmJdG6auC61ZZeYPe1w1Axr1SfIVFUuo27Slk5EAF\nyjwRIyyWUXxTX0ZBPUK0rwNRjfMLm/LXQCR6+WarKbvn/yDgmkpBRYS/6u9sL9EO\nBJgGXLk3VA6pO64kw7+WXNzK+O4b27pijw0yPo0ipfM0XFAEk+6Klz3eocEnu9Zf\nl0xXjJIbOmS7ELua1EloZ47TBakaFP52tac4o/JWrc7RciqVzu5DLjYoQfbrQQeA\naLZNx7bFgQ/l1X6FhouHHksCAwEAAQ==\n-----END PUBLIC KEY-----", 15 | "unsealed": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16 | "paserk": "k1.seal.QmXSEH_ei6jX3xvcZ5_l4k53mBTgJSRHWS07MB56tX0hBUqKE-ur-SdjRn68DF_j9BbUSif5kqAn4h7ozfW1IRr4tp26_gVdSN9wUcCMVFpc7kF_unOd8dJlqKuxAxy8pfVRtN5no12JKogWU9EBE1y8iv6k1CDetlU8htQsBhvpcNigBhFooNt8J2UZrNppDSlzO-bqJcz7gPFpqdp31wwCi9PIoaUG52GHTRrm600p6FftQ9V2XdnA5gL0TZQhRG3EstYR9s6p-Irrs7BcqH5QpC0ZRLcnsJHMkSvX79g7fkiJeEHbLRTjE0xUT8sUk6fxgSNJws8XDsBU-4p74OKW3GyZmtbxwhHN6hO4OJHRtV3MoLRJMDUVYCVfDVkEMYXd9kOq9_rRgeP_cCnHhbLncIpEUUfvLB_8OAesU6h88ulKDdsyIs2a7HCrvjfKwni4iz8yAabUf7JTzieoX9zRsYe52i6vmujSgiuiwKS_EbdPTKNiPu308QHfu_bubXYkRXmU6ieziqU1f4FAklxGNK9sZpK_uEYIUrhiT6uqIJmkMU410XFnVwkebd6OGXFoGvvIyaq0iPaF---fHXyCnmI9fyjV0kHy57I18pbDY-ujMUCnjZ-yYYmk2DyhYPURq50yNVptIjx7B8mU660KPWkACO1NUQhIdACDkHseNb7mER71l64A0Hmu9PZB4VHBH4NBhtdvll2vIVcLPk-BdDa1PziJy6QZweIel-IufvLl1awLu67d2D8DJw25NAPuCsdrfZMMqhe02nAmGg" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /pyseto/key.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Union 2 | 3 | from cryptography.hazmat.primitives.asymmetric import ec 4 | from cryptography.hazmat.primitives.asymmetric.ed25519 import ( 5 | Ed25519PrivateKey, 6 | Ed25519PublicKey, 7 | ) 8 | from cryptography.hazmat.primitives.serialization import ( 9 | load_pem_private_key, 10 | load_pem_public_key, 11 | ) 12 | 13 | from .key_interface import KeyInterface 14 | from .versions.v1 import V1Local, V1Public 15 | from .versions.v2 import V2Local, V2Public 16 | from .versions.v3 import V3Local, V3Public 17 | from .versions.v4 import V4Local, V4Public 18 | 19 | 20 | class Key: 21 | """ 22 | Tha factory methods for PASETO keys. 23 | """ 24 | 25 | _PASERK_TYPE_SUPPORTED = [ 26 | "local", 27 | "public", 28 | "secret", 29 | "seal", 30 | "local-wrap", 31 | "secret-wrap", 32 | "local-pw", 33 | "secret-pw", 34 | ] 35 | 36 | @classmethod 37 | def new(cls, version: int, purpose: str, key: Union[bytes, str] = b"") -> KeyInterface: 38 | """ 39 | Constructor of a PASETO key object which has 40 | :class:`KeyInterface `. 41 | 42 | Args: 43 | version(int): The version of the key. It will be ``1``, 44 | ``2``, ``3`` or ``4``. 45 | purpose (str): The purpose of the key. It will be ``public`` or 46 | ``local``. 47 | key (Union[bytes, str]): A key itself or keying material. 48 | Returns: 49 | KeyInterface: A PASETO key object. 50 | Raise: 51 | ValueError: Invalid arguments. 52 | """ 53 | bkey = key if isinstance(key, bytes) else key.encode("utf-8") 54 | if purpose == "local": 55 | return cls._create_private_key(version, bkey) 56 | 57 | elif purpose == "public": 58 | k: Any = None 59 | if bkey.startswith(b"-----BEGIN EC PRIVATE"): 60 | k = load_pem_private_key(bkey, password=None) 61 | elif bkey.startswith(b"-----BEGIN PRIVATE"): 62 | k = load_pem_private_key(bkey, password=None) 63 | elif bkey.startswith(b"-----BEGIN PUBLIC"): 64 | k = load_pem_public_key(bkey) 65 | elif bkey.startswith(b"-----BEGIN RSA PRIVATE"): 66 | k = load_pem_private_key(bkey, password=None) 67 | else: 68 | raise ValueError("Invalid or unsupported PEM format.") 69 | return cls._create_public_key(version, k) 70 | 71 | raise ValueError(f"Invalid purpose: {purpose}.") 72 | 73 | @classmethod 74 | def from_paserk( 75 | cls, 76 | paserk: str, 77 | wrapping_key: Union[bytes, str] = b"", 78 | password: Union[bytes, str] = b"", 79 | unsealing_key: Union[bytes, str] = b"", 80 | ) -> KeyInterface: 81 | """ 82 | Generates a PASETO key object which has 83 | :class:`KeyInterface ` from PASERK. 84 | 85 | Args: 86 | paserk (str): A PASERK string. 87 | wrapping_key (Union[bytes, str]): A wrapping key. If the 88 | `wrapping_key` is specified, `password` should not be specified. 89 | password (Union[bytes, str]): A password for key wrapping. If the 90 | `password` is specified, `wrapping_key` should not be specified. 91 | unsealing_key (Union[bytes, str]): A password for key wrapping. If the 92 | `password` is specified, `wrapping_key` should not be specified. 93 | Returns: 94 | KeyInterface: A PASETO key object. 95 | Raise: 96 | ValueError: Invalid arguments. 97 | """ 98 | 99 | frags = paserk.split(".") 100 | if frags[1] not in cls._PASERK_TYPE_SUPPORTED: 101 | raise ValueError(f"Invalid PASERK key type: {frags[1]}.") 102 | bwk = wrapping_key if isinstance(wrapping_key, bytes) else wrapping_key.encode("utf-8") 103 | bpw = password if isinstance(password, bytes) else password.encode("utf-8") 104 | bsk = unsealing_key if isinstance(unsealing_key, bytes) else unsealing_key.encode("utf-8") 105 | 106 | if frags[0] == "k1": 107 | return ( 108 | V1Local.from_paserk(paserk, bwk, bpw, bsk) 109 | if frags[1].startswith("local") or frags[1] == "seal" 110 | else V1Public.from_paserk(paserk, bwk, bpw) 111 | ) 112 | if frags[0] == "k2": 113 | return ( 114 | V2Local.from_paserk(paserk, bwk, bpw, bsk) 115 | if frags[1].startswith("local") or frags[1] == "seal" 116 | else V2Public.from_paserk(paserk, bwk, bpw) 117 | ) 118 | if frags[0] == "k3": 119 | return ( 120 | V3Local.from_paserk(paserk, bwk, bpw, bsk) 121 | if frags[1].startswith("local") or frags[1] == "seal" 122 | else V3Public.from_paserk(paserk, bwk, bpw) 123 | ) 124 | if frags[0] == "k4": 125 | return ( 126 | V4Local.from_paserk(paserk, bwk, bpw, bsk) 127 | if frags[1].startswith("local") or frags[1] == "seal" 128 | else V4Public.from_paserk(paserk, bwk, bpw) 129 | ) 130 | raise ValueError(f"Invalid PASERK version: {frags[0]}.") 131 | 132 | # @classmethod 133 | # def from_public_bytes(cls, version: int, key: bytes) -> KeyInterface: 134 | # if version == 1: 135 | # raise ValueError(f"RSA key is not supported.") 136 | # if version == 2: 137 | # return V2Public.from_public_bytes(key) 138 | # if version == 3: 139 | # return V3Public.from_public_bytes(key) 140 | # if version == 4: 141 | # return V4Public.from_public_bytes(key) 142 | # raise ValueError(f"Invalid version: {version}.") 143 | 144 | @staticmethod 145 | def from_asymmetric_key_params(version: int, x: bytes = b"", y: bytes = b"", d: bytes = b"") -> KeyInterface: 146 | """ 147 | Constructor of a PASETO key object which has 148 | :class:`KeyInterface ` wth 149 | asymmetric key parameters (x-coordinate, y-coordinate, and/or private 150 | key). This is intended to be used to generate keys for PASETO from JWK 151 | and other sources. 152 | 153 | Args: 154 | version(int): The version of the key. It will be ``1``, 155 | ``2``, ``3`` or ``4``. 156 | x (bytes): The x coordinate of the key. 157 | y (bytes): The y coordinate of the key. 158 | d (bytes): The private key component of the key. 159 | Returns: 160 | KeyInterface: A PASETO key object. 161 | Raise: 162 | ValueError: Invalid arguments. 163 | """ 164 | k: Any = None 165 | 166 | if version == 1: 167 | raise ValueError("v1.public is not supported on from_key_parameters.") 168 | 169 | if version == 2: 170 | if x and d: 171 | raise ValueError("Only one of x or d should be set for v2.public.") 172 | if x: 173 | try: 174 | k = Ed25519PublicKey.from_public_bytes(x) 175 | except Exception as err: 176 | raise ValueError("Failed to load key.") from err 177 | return V2Public(k) 178 | if d: 179 | try: 180 | k = Ed25519PrivateKey.from_private_bytes(d) 181 | except Exception as err: 182 | raise ValueError("Failed to load key.") from err 183 | return V2Public(k) 184 | raise ValueError("x or d should be set for v2.public.") 185 | 186 | if version == 3: 187 | if not x or not y: 188 | raise ValueError("x and y (and d) should be set for v3.public.") 189 | try: 190 | pn = ec.EllipticCurvePublicNumbers( 191 | x=int.from_bytes(x, byteorder="big"), 192 | y=int.from_bytes(y, byteorder="big"), 193 | curve=ec.SECP384R1(), 194 | ) 195 | k = pn.public_key() 196 | except Exception as err: 197 | raise ValueError("Failed to load key.") from err 198 | 199 | if not d: 200 | return V3Public(k) 201 | try: 202 | k = ec.EllipticCurvePrivateNumbers(int.from_bytes(d, byteorder="big"), pn).private_key() 203 | except Exception as err: 204 | raise ValueError("Failed to load key.") from err 205 | return V3Public(k) 206 | 207 | if version == 4: 208 | if x and d: 209 | raise ValueError("Only one of x or d should be set for v4.public.") 210 | if x: 211 | try: 212 | k = Ed25519PublicKey.from_public_bytes(x) 213 | except Exception as err: 214 | raise ValueError("Failed to load key.") from err 215 | return V4Public(k) 216 | if d: 217 | try: 218 | k = Ed25519PrivateKey.from_private_bytes(d) 219 | except Exception as err: 220 | raise ValueError("Failed to load key.") from err 221 | return V4Public(k) 222 | raise ValueError("x or d should be set for v4.public.") 223 | 224 | raise ValueError(f"Invalid version: {version}.") 225 | 226 | @staticmethod 227 | def _create_public_key(version: int, key: Any) -> KeyInterface: 228 | if version == 1: 229 | return V1Public(key) 230 | if version == 2: 231 | return V2Public(key) 232 | if version == 3: 233 | return V3Public(key) 234 | if version == 4: 235 | return V4Public(key) 236 | raise ValueError(f"Invalid version: {version}.") 237 | 238 | @staticmethod 239 | def _create_private_key(version: int, key: bytes) -> KeyInterface: 240 | if version == 1: 241 | return V1Local(key) 242 | if version == 2: 243 | return V2Local(key) 244 | if version == 3: 245 | return V3Local(key) 246 | if version == 4: 247 | return V4Local(key) 248 | raise ValueError(f"Invalid version: {version}.") 249 | -------------------------------------------------------------------------------- /tests/test_v2.py: -------------------------------------------------------------------------------- 1 | from secrets import token_bytes 2 | 3 | import pytest 4 | 5 | import pyseto 6 | from pyseto import DecryptError, EncryptError, Key, VerifyError 7 | from pyseto.versions.v2 import V2Local, V2Public 8 | 9 | from .utils import get_path, load_key 10 | 11 | 12 | class TestV2Local: 13 | """ 14 | Tests for v2.local. 15 | """ 16 | 17 | @pytest.mark.parametrize( 18 | "key, msg", 19 | [ 20 | (b"", "key must be specified."), 21 | (token_bytes(1), "key must be 32 bytes long."), 22 | (token_bytes(8), "key must be 32 bytes long."), 23 | (token_bytes(16), "key must be 32 bytes long."), 24 | (token_bytes(31), "key must be 32 bytes long."), 25 | (token_bytes(33), "key must be 32 bytes long."), 26 | ], 27 | ) 28 | def test_v2_local_new_with_invalid_arg(self, key, msg): 29 | with pytest.raises(ValueError) as err: 30 | Key.new(2, "local", key) 31 | pytest.fail("Key.new() should fail.") 32 | assert msg in str(err.value) 33 | 34 | @pytest.mark.parametrize( 35 | "key", 36 | [ 37 | None, 38 | 0, 39 | token_bytes(65), 40 | ], 41 | ) 42 | def test_v2_local__generate_hash_with_invalid_arg(self, key): 43 | with pytest.raises(EncryptError) as err: 44 | V2Local._generate_hash(key, b"Hello world!", 32) 45 | pytest.fail("V2Local._generate_hash() should fail.") 46 | assert "Failed to generate hash." in str(err.value) 47 | 48 | @pytest.mark.parametrize( 49 | "ptk", 50 | [ 51 | None, 52 | 0, 53 | ], 54 | ) 55 | def test_v2_local__encode_pie_with_invalid_ptk(self, ptk): 56 | with pytest.raises(EncryptError) as err: 57 | V2Local._encode_pie("v2.local-wrap.pie.", token_bytes(32), ptk) 58 | pytest.fail("V2Local._encode_pie() should fail.") 59 | assert "Failed to encrypt." in str(err.value) 60 | 61 | def test_v2_local_decrypt_via_decode_with_wrong_key(self): 62 | k1 = Key.new(2, "local", token_bytes(32)) 63 | k2 = Key.new(2, "local", token_bytes(32)) 64 | token = pyseto.encode(k1, b"Hello world!") 65 | with pytest.raises(DecryptError) as err: 66 | pyseto.decode(k2, token) 67 | pytest.fail("pyseto.decode() should fail.") 68 | assert "Failed to decrypt." in str(err.value) 69 | 70 | def test_v2_local_encrypt_with_invalid_arg(self): 71 | k = Key.new(2, "local", token_bytes(32)) 72 | with pytest.raises(EncryptError) as err: 73 | k.encrypt(None) 74 | pytest.fail("pyseto.encrypt() should fail.") 75 | assert "Failed to generate internal nonce." in str(err.value) 76 | 77 | @pytest.mark.parametrize( 78 | "nonce", 79 | [ 80 | token_bytes(1), 81 | token_bytes(8), 82 | token_bytes(23), 83 | token_bytes(25), 84 | token_bytes(32), 85 | ], 86 | ) 87 | def test_v2_local_encrypt_via_encode_with_wrong_nonce(self, nonce): 88 | k = Key.new(2, "local", token_bytes(32)) 89 | with pytest.raises(ValueError) as err: 90 | pyseto.encode(k, b"Hello world!", nonce=nonce) 91 | pytest.fail("pyseto.encode() should fail.") 92 | assert "nonce must be 24 bytes long." in str(err.value) 93 | 94 | @pytest.mark.parametrize( 95 | "paserk, msg", 96 | [ 97 | ("xx.local.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 98 | ("k3.local.AAAAAAAAAAAAAAAA", "Invalid PASERK version: k3."), 99 | ("k2.local.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 100 | ("k2.public.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 101 | ("k2.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 102 | ("k2.public.AAAAAAAAAAAAAAAA", "Invalid PASERK type: public."), 103 | ( 104 | "k2.local-wrap.AAAAAAAAAAAAAAAA", 105 | "local-wrap needs wrapping_key.", 106 | ), 107 | ( 108 | "k2.secret-wrap.AAAAAAAAAAAAAAAA", 109 | "Invalid PASERK type: secret-wrap.", 110 | ), 111 | ( 112 | "k2.local-pw.AAAAAAAAAAAAAAAA", 113 | "local-pw needs password.", 114 | ), 115 | ( 116 | "k2.seal.AAAAAAAAAAAAAAAA", 117 | "seal needs unsealing_key.", 118 | ), 119 | ], 120 | ) 121 | def test_v2_local_from_paserk_with_invalid_args(self, paserk, msg): 122 | with pytest.raises(ValueError) as err: 123 | V2Local.from_paserk(paserk) 124 | pytest.fail("Key.from_paserk should fail.") 125 | assert msg in str(err.value) 126 | 127 | @pytest.mark.parametrize( 128 | "paserk, msg", 129 | [ 130 | ("xx.local-wrap.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 131 | ("k2.local-wrap.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 132 | ("k2.local-wrap.xxx.AAAAAAAAAAAAAAAA", "Unknown wrapping algorithm: xxx."), 133 | ("k2.xxx.pie.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 134 | ], 135 | ) 136 | def test_v2_local_from_paserk_with_wrapping_key_and_invalid_args(self, paserk, msg): 137 | with pytest.raises(ValueError) as err: 138 | V2Local.from_paserk(paserk, wrapping_key=token_bytes(32)) 139 | pytest.fail("Key.from_paserk should fail.") 140 | assert msg in str(err.value) 141 | 142 | @pytest.mark.parametrize( 143 | "paserk, msg", 144 | [ 145 | ("k2.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 146 | ("k2.seal.AAAAAAAAAAAAAAAA", "Invalid or unsupported PEM format."), 147 | ], 148 | ) 149 | def test_v2_local_from_paserk_with_unsealing_key_and_invalid_args(self, paserk, msg): 150 | with pytest.raises(ValueError) as err: 151 | V2Local.from_paserk(paserk, unsealing_key=token_bytes(32)) 152 | pytest.fail("Key.from_paserk should fail.") 153 | assert msg in str(err.value) 154 | 155 | def test_v2_local_to_paserk_with_invalid_sealing_key(self): 156 | k = Key.new(2, "local", token_bytes(32)) 157 | with pytest.raises(ValueError) as err: 158 | k.to_paserk(sealing_key=b"not-PEM-formatted-key") 159 | pytest.fail("Key.from_paserk should fail.") 160 | assert "Invalid or unsupported PEM format." in str(err.value) 161 | 162 | def test_v2_local_from_paserk_with_wrong_unsealing_key(self): 163 | k = Key.new(2, "local", token_bytes(32)) 164 | with open(get_path("keys/public_key_x25519.pem")) as key_file: 165 | sealed_key = k.to_paserk(sealing_key=key_file.read()) 166 | 167 | with open(get_path("keys/private_key_x25519_2.pem")) as key_file: 168 | unsealing_key = key_file.read() 169 | 170 | with pytest.raises(DecryptError) as err: 171 | Key.from_paserk(sealed_key, unsealing_key=unsealing_key) 172 | pytest.fail("Key.from_paserk should fail.") 173 | assert "Failed to unseal a key." in str(err.value) 174 | 175 | def test_v2_local_to_peer_paserk_id(self): 176 | k = Key.new(2, "local", token_bytes(32)) 177 | assert k.to_peer_paserk_id() == "" 178 | 179 | 180 | class TestV2Public: 181 | """ 182 | Tests for v2.public. 183 | """ 184 | 185 | def test_v2_public_to_paserk_id(self): 186 | sk = Key.new(2, "public", load_key("keys/private_key_ed25519.pem")) 187 | pk = Key.new(2, "public", load_key("keys/public_key_ed25519.pem")) 188 | assert sk.to_peer_paserk_id() == pk.to_paserk_id() 189 | assert pk.to_peer_paserk_id() == "" 190 | 191 | def test_v2_public_verify_via_encode_with_wrong_key(self): 192 | sk = Key.new(2, "public", load_key("keys/private_key_ed25519.pem")) 193 | pk = Key.new(2, "public", load_key("keys/public_key_ed25519_2.pem")) 194 | token = pyseto.encode(sk, b"Hello world!") 195 | with pytest.raises(VerifyError) as err: 196 | pyseto.decode(pk, token) 197 | pytest.fail("pyseto.decode() should fail.") 198 | assert "Failed to verify." in str(err.value) 199 | 200 | def test_v2_public_to_paserk_with_sealing_key(self): 201 | k = Key.new(2, "public", load_key("keys/private_key_ed25519.pem")) 202 | with pytest.raises(ValueError) as err: 203 | k.to_paserk(sealing_key=b"xxx") 204 | pytest.fail("pyseto.to_paserk() should fail.") 205 | assert "Key sealing can only be used for local key." in str(err.value) 206 | 207 | # def test_v2_public_from_paserk_with_wrong_unsealing_key(self): 208 | # key = Key.new(2, "local", token_bytes(32)) 209 | # pk = Key.new(2, "public", load_key("keys/public_key_ed25519.pem")) 210 | # sealing_key = pk.public_bytes(Encoding.Raw, PublicFormat.Raw) 211 | # sealed = key.to_paserk(sealing_key=sealing_key) 212 | # sk = Key.new(2, "public", load_key("keys/private_key_ed25519_2.pem")) 213 | # with pytest.raises(ValueError) as err: 214 | # Key.from_paserk(unsealing_key=unsealing_key) 215 | # pytest.fail("pyseto.from_paserk() should fail.") 216 | # assert "Failed to unseal a key." in str(err.value) 217 | 218 | @pytest.mark.parametrize( 219 | "paserk, msg", 220 | [ 221 | ("xx.public.AAAAAAAAAAAAAAAA", "Invalid PASERK version: xx."), 222 | ("k3.public.AAAAAAAAAAAAAAAA", "Invalid PASERK version: k3."), 223 | ("k2.public.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 224 | ("k2.local.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK format."), 225 | ("k2.xxx.AAAAAAAAAAAAAAAA", "Invalid PASERK type: xxx."), 226 | ("k2.local.AAAAAAAAAAAAAAAA", "Invalid PASERK type: local."), 227 | ( 228 | "k2.local-wrap.AAAAAAAAAAAAAAAA", 229 | "Invalid PASERK type: local-wrap.", 230 | ), 231 | ( 232 | "k2.secret-wrap.AAAAAAAAAAAAAAAA", 233 | "secret-wrap needs wrapping_key.", 234 | ), 235 | ( 236 | "k2.secret-pw.AAAAAAAAAAAAAAAA", 237 | "secret-pw needs password.", 238 | ), 239 | ], 240 | ) 241 | def test_v2_public_from_paserk_with_invalid_args(self, paserk, msg): 242 | with pytest.raises(ValueError) as err: 243 | V2Public.from_paserk(paserk) 244 | pytest.fail("Key.from_paserk should fail.") 245 | assert msg in str(err.value) 246 | -------------------------------------------------------------------------------- /pyseto/versions/v1.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | from secrets import token_bytes 4 | from typing import Any, Union 5 | 6 | from cryptography.hazmat.primitives import hashes, serialization 7 | from cryptography.hazmat.primitives.asymmetric import padding 8 | from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey 9 | from cryptography.hazmat.primitives.kdf.hkdf import HKDF 10 | from cryptography.hazmat.primitives.serialization import ( 11 | load_der_private_key, 12 | load_der_public_key, 13 | ) 14 | 15 | from ..exceptions import DecryptError, SignError, VerifyError 16 | from ..key_interface import KeyInterface 17 | from ..key_nist import NISTKey 18 | from ..utils import base64url_decode, base64url_encode, pae 19 | 20 | 21 | class V1Local(NISTKey): 22 | """ 23 | The key object for v1.local. 24 | """ 25 | 26 | _VERSION = 1 27 | _TYPE = "local" 28 | 29 | def __init__(self, key: Union[str, bytes]): 30 | super().__init__(key) 31 | return 32 | 33 | def to_paserk_id(self) -> str: 34 | h = "k1.lid." 35 | p = self.to_paserk() 36 | digest = hashes.Hash(hashes.SHA384()) 37 | digest.update((h + p).encode("utf-8")) 38 | d = digest.finalize() 39 | return h + base64url_encode(d[0:33]).decode("utf-8") 40 | 41 | def encrypt( 42 | self, 43 | payload: bytes, 44 | footer: bytes = b"", 45 | implicit_assertion: bytes = b"", 46 | nonce: bytes = b"", 47 | ) -> bytes: 48 | if nonce: 49 | if len(nonce) != 32: 50 | raise ValueError("nonce must be 32 bytes long.") 51 | else: 52 | nonce = token_bytes(32) 53 | 54 | n = self._generate_hash(nonce, payload, 32) 55 | e = HKDF( 56 | algorithm=hashes.SHA384(), 57 | length=32, 58 | salt=n[0:16], 59 | info=b"paseto-encryption-key", 60 | ) 61 | a = HKDF( 62 | algorithm=hashes.SHA384(), 63 | length=32, 64 | salt=n[0:16], 65 | info=b"paseto-auth-key-for-aead", 66 | ) 67 | ek = e.derive(self._key) 68 | ak = a.derive(self._key) 69 | 70 | c = self._encrypt(ek, n[16:], payload) 71 | pre_auth = pae([self.header, n, c, footer]) 72 | t = hmac.new(ak, pre_auth, hashlib.sha384).digest() 73 | token = self._header + base64url_encode(n + c + t) 74 | if footer: 75 | token += b"." + base64url_encode(footer) 76 | return token 77 | 78 | def decrypt(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 79 | n = payload[0:32] 80 | t = payload[-48:] 81 | c = payload[32 : len(payload) - 48] 82 | e = HKDF( 83 | algorithm=hashes.SHA384(), 84 | length=32, 85 | salt=n[0:16], 86 | info=b"paseto-encryption-key", 87 | ) 88 | a = HKDF( 89 | algorithm=hashes.SHA384(), 90 | length=32, 91 | salt=n[0:16], 92 | info=b"paseto-auth-key-for-aead", 93 | ) 94 | ek = e.derive(self._key) 95 | ak = a.derive(self._key) 96 | 97 | pre_auth = pae([self.header, n, c, footer]) 98 | t2 = hmac.new(ak, pre_auth, hashlib.sha384).digest() 99 | if not hmac.compare_digest(t, t2): 100 | raise DecryptError("Failed to decrypt.") 101 | return self._decrypt(ek, n[16:], c) 102 | 103 | 104 | class V1Public(NISTKey): 105 | """ 106 | The key object for v1.public. 107 | """ 108 | 109 | _VERSION = 1 110 | _TYPE = "public" 111 | 112 | def __init__(self, key: Any): 113 | super().__init__(key) 114 | 115 | self._sig_size = 256 116 | if not isinstance(self._key, (RSAPublicKey, RSAPrivateKey)): 117 | raise ValueError("The key is not RSA key.") 118 | 119 | if isinstance(self._key, RSAPublicKey): 120 | self._is_secret = False 121 | 122 | self._padding = padding.PSS(mgf=padding.MGF1(hashes.SHA384()), salt_length=48) 123 | return 124 | 125 | @classmethod 126 | def from_paserk( 127 | cls, 128 | paserk: str, 129 | wrapping_key: bytes = b"", 130 | password: bytes = b"", 131 | unsealing_key: bytes = b"", 132 | ) -> KeyInterface: 133 | if wrapping_key and password: 134 | raise ValueError("Only one of wrapping_key or password should be specified.") 135 | 136 | frags = paserk.split(".") 137 | if frags[0] != "k1": 138 | raise ValueError(f"Invalid PASERK version: {frags[0]}.") 139 | 140 | if wrapping_key: 141 | if len(frags) != 4: 142 | raise ValueError("Invalid PASERK format.") 143 | if frags[2] != "pie": 144 | raise ValueError(f"Unknown wrapping algorithm: {frags[2]}.") 145 | 146 | if frags[1] == "secret-wrap": 147 | h = "k1.secret-wrap.pie." 148 | k = cls._decode_pie(h, wrapping_key, frags[3]) 149 | return cls(load_der_private_key(k, password=None)) 150 | raise ValueError(f"Invalid PASERK type: {frags[1]}.") 151 | 152 | if len(frags) != 3: 153 | raise ValueError("Invalid PASERK format.") 154 | 155 | if password: 156 | if frags[1] == "secret-pw": 157 | h = "k1.secret-pw." 158 | k = cls._decode_pbkw(h, password, frags[2]) 159 | return cls(load_der_private_key(k, password=None)) 160 | raise ValueError(f"Invalid PASERK type: {frags[1]}.") 161 | 162 | wrapped = base64url_decode(frags[2]) 163 | if frags[1] == "public": 164 | return cls(load_der_public_key(wrapped)) 165 | if frags[1] == "secret": 166 | return cls(load_der_private_key(wrapped, password=None)) 167 | if frags[1] == "secret-wrap": 168 | raise ValueError(f"{frags[1]} needs wrapping_key.") 169 | raise ValueError(f"Invalid PASERK type: {frags[1]}.") 170 | 171 | def to_paserk( 172 | self, 173 | wrapping_key: Union[bytes, str] = b"", 174 | password: Union[bytes, str] = b"", 175 | sealing_key: Union[bytes, str] = b"", 176 | iteration: int = 100000, 177 | memory_cost: int = 15 * 1024, 178 | time_cost: int = 2, 179 | parallelism: int = 1, 180 | ) -> str: 181 | if wrapping_key and password: 182 | raise ValueError("Only one of wrapping_key or password should be specified.") 183 | 184 | if wrapping_key: 185 | # secret-wrap 186 | if not isinstance(self._key, RSAPrivateKey): 187 | raise ValueError("Public key cannot be wrapped.") 188 | 189 | bkey = wrapping_key if isinstance(wrapping_key, bytes) else wrapping_key.encode("utf-8") 190 | h = "k1.secret-wrap.pie." 191 | priv = self._key.private_bytes( 192 | encoding=serialization.Encoding.DER, 193 | format=serialization.PrivateFormat.TraditionalOpenSSL, 194 | encryption_algorithm=serialization.NoEncryption(), 195 | ) 196 | return h + self._encode_pie(h, bkey, priv) 197 | 198 | if password: 199 | # secret-pw 200 | if not isinstance(self._key, RSAPrivateKey): 201 | raise ValueError("Public key cannot be wrapped.") 202 | 203 | bpw = password if isinstance(password, bytes) else password.encode("utf-8") 204 | h = "k1.secret-pw." 205 | priv = self._key.private_bytes( 206 | encoding=serialization.Encoding.DER, 207 | format=serialization.PrivateFormat.TraditionalOpenSSL, 208 | encryption_algorithm=serialization.NoEncryption(), 209 | ) 210 | return h + self._encode_pbkw(h, bpw, priv, iteration) 211 | 212 | # public 213 | if isinstance(self._key, RSAPublicKey): 214 | pub = self._key.public_bytes( 215 | encoding=serialization.Encoding.DER, 216 | format=serialization.PublicFormat.SubjectPublicKeyInfo, 217 | ) 218 | return "k1.public." + base64url_encode(pub).decode("utf-8") 219 | 220 | # secret 221 | priv = self._key.private_bytes( 222 | encoding=serialization.Encoding.DER, 223 | format=serialization.PrivateFormat.TraditionalOpenSSL, 224 | encryption_algorithm=serialization.NoEncryption(), 225 | ) 226 | return "k1.secret." + base64url_encode(priv).decode("utf-8") 227 | 228 | def to_paserk_id(self) -> str: 229 | p = self.to_paserk() 230 | h = "k1.pid." if isinstance(self._key, RSAPublicKey) else "k1.sid." 231 | digest = hashes.Hash(hashes.SHA384()) 232 | digest.update((h + p).encode("utf-8")) 233 | d = digest.finalize() 234 | return h + base64url_encode(d[0:33]).decode("utf-8") 235 | 236 | def to_peer_paserk_id(self) -> str: 237 | if not self._is_secret: 238 | return "" 239 | 240 | k = self._key.public_key().public_bytes( 241 | encoding=serialization.Encoding.DER, 242 | format=serialization.PublicFormat.SubjectPublicKeyInfo, 243 | ) 244 | p = "k1.public." + base64url_encode(k).decode("utf-8") 245 | 246 | h = "k1.pid." 247 | digest = hashes.Hash(hashes.SHA384()) 248 | digest.update((h + p).encode("utf-8")) 249 | d = digest.finalize() 250 | return h + base64url_encode(d[0:33]).decode("utf-8") 251 | 252 | def sign(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b"") -> bytes: 253 | if isinstance(self._key, RSAPublicKey): 254 | raise ValueError("A public key cannot be used for signing.") 255 | m2 = pae([self.header, payload, footer]) 256 | try: 257 | return self._key.sign(m2, self._padding, hashes.SHA384()) 258 | except Exception as err: 259 | raise SignError("Failed to sign.") from err 260 | 261 | def verify(self, payload: bytes, footer: bytes = b"", implicit_assertion: bytes = b""): 262 | if len(payload) <= self._sig_size: 263 | raise ValueError("Invalid payload.") 264 | 265 | sig = payload[-self._sig_size :] 266 | m = payload[: len(payload) - self._sig_size] 267 | k = self._key if isinstance(self._key, RSAPublicKey) else self._key.public_key() 268 | m2 = pae([self.header, m, footer]) 269 | try: 270 | k.verify(sig, m2, self._padding, hashes.SHA384()) 271 | except Exception as err: 272 | raise VerifyError("Failed to verify.") from err 273 | return m 274 | --------------------------------------------------------------------------------