[
  {
    "path": ".gitattributes",
    "content": "*.age binary\ntestdata/testkit/* binary\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "## Issues\n\nI want to hear about any issues you encounter while using age.\n\nParticularly appreciated are well researched, complete [issues](https://github.com/FiloSottile/age/issues/new/choose) with lots of context, **focusing on the intended outcome and/or use case**. Issues don't have to be just about bugs: if something was hard to figure out or unexpected, please file a **[UX report](https://github.com/FiloSottile/age/discussions/new?category=UX-reports)**! ✨\n\nNot all issue reports might lead to a change, so please don't be offended if yours doesn't, but they are precious datapoints to understand how age could work better in aggregate.\n\n## Pull requests\n\nage is a little unusual in how it is maintained. I like to keep the code style consistent and complexity to a minimum, and going through many iterations of code review is a significant toil on both contributors and maintainers. age is also small enough that such a time investment is unlikely to pay off over ongoing contributions.\n\nTherefore, **be prepared for your change to get reimplemented rather than merged**, and please don't be offended if that happens. PRs are still appreciated as a way to clarify the intended behavior, but are not at all required: prefer focusing on providing detailed context in an issue report instead.\n\nTo learn more, please see my [maintenance policy](https://github.com/FiloSottile/FiloSottile/blob/main/maintenance.md).\n\n<!-- ## Feature requests\n\nage is small, simple, and config-free by design. A lot of effort is put into resisting scope creep and enabling use cases by integrating and interoperating well with other projects rather than by adding features.\n\nIn particular, I'm unlikely to merge into the main repo anything I don't use myself, as I would not be the best person to maintain it. However, I'm always happy to discuss, learn about, and link to any age-related project! -->\n\n## Other ways to contribute\n\nage itself is not community maintained, but its ecosystem very much is, and that's where a lot of the strength of age is! Here are some ideas for ways to contribute to age and its ecosystem, besides contributing to this repository.\n\n* **Write an article about how to use age for a certain community or use case.** The number one reason people don't use age is because they haven't heard about it and existing tutorials present more complex alternatives.\n* Integrate age into existing projects that might use it, for example replacing legacy alternatives.\n* Build and maintain an [age plugin](https://c2sp.org/age-plugin) for a KMS or platform.\n* Watch the [discussions](https://github.com/FiloSottile/age/discussions) and help other users.\n* Provide bindings in a language or framework that doesn't support age well.\n* Package age for an ecosystem that doesn't have packages yet.\n\nIf you build or write something related to age, [let me know](https://github.com/FiloSottile/age/discussions/new?category=general)! 💖\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Bug report 🐞\nabout: Did you encounter a bug in this implementation?\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n## Environment\n\n* OS:\n* age version:\n\n## What were you trying to do\n\n## What happened\n\n```\n<insert terminal transcript here>\n```\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "contact_links:\n  - name: UX report ✨\n    url: https://github.com/FiloSottile/age/discussions/new?category=UX-reports\n    about: Was age hard to use? It's not you, it's us. We want to hear about it.\n  - name: Spec feedback 📃\n    url: https://github.com/FiloSottile/age/discussions/new?category=Spec-feedback\n    about: Have a comment about the age spec as it's implemented by this and other tools?\n  - name: Questions, feature requests, and more 💬\n    url: https://github.com/FiloSottile/age/discussions\n    about: Do you need support? Did you make something with age? Do you have an idea? Tell us about it!\n"
  },
  {
    "path": ".github/workflows/LICENSE.suffix.txt",
    "content": "\n---\n\nCopyright 2009 The Go Authors.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of Google LLC nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build and upload binaries\non:\n  release:\n    types: [published]\n  push:\n  pull_request:\npermissions:\n  contents: read\njobs:\n  build:\n    name: Build binaries\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        include:\n          - {GOOS: linux, GOARCH: amd64}\n          - {GOOS: linux, GOARCH: arm, GOARM: 6}\n          - {GOOS: linux, GOARCH: arm64}\n          - {GOOS: darwin, GOARCH: arm64}\n          - {GOOS: darwin, GOARCH: amd64}\n          - {GOOS: windows, GOARCH: amd64}\n          - {GOOS: freebsd, GOARCH: amd64}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n          persist-credentials: false\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          cache: false\n      - name: Build binary\n        run: |\n          VERSION=\"$(git describe --tags)\"\n          DIR=\"$(mktemp -d)\"\n          mkdir \"$DIR/age\"\n          go build -o \"$DIR/age\" -trimpath ./cmd/...\n          cp LICENSE \"$DIR/age/LICENSE\"\n          cat .github/workflows/LICENSE.suffix.txt >> \"$DIR/age/LICENSE\"\n          if [ \"$GOOS\" == \"windows\" ]; then\n            sudo apt-get update && sudo apt-get install -y osslsigncode\n            if [ -n \"${{ secrets.SIGN_PASS }}\" ]; then\n              for exe in \"$DIR\"/age/*.exe; do\n                /usr/bin/osslsigncode sign -t \"http://timestamp.comodoca.com\" \\\n                  -certs .github/workflows/certs/uitacllc.crt \\\n                  -key .github/workflows/certs/uitacllc.key \\\n                  -pass \"${{ secrets.SIGN_PASS }}\" \\\n                  -n age -in \"$exe\" -out \"$exe.signed\"\n                mv \"$exe.signed\" \"$exe\"\n              done\n            fi\n            ( cd \"$DIR\"; zip age.zip -r age )\n            mv \"$DIR/age.zip\" \"age-$VERSION-$GOOS-$GOARCH.zip\"\n          else\n            tar -cvzf \"age-$VERSION-$GOOS-$GOARCH.tar.gz\" -C \"$DIR\" age\n          fi\n        env:\n          CGO_ENABLED: 0\n          GOOS: ${{ matrix.GOOS }}\n          GOARCH: ${{ matrix.GOARCH }}\n          GOARM: ${{ matrix.GOARM }}\n      - name: Upload workflow artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: age-artifacts-${{ matrix.GOOS }}-${{ matrix.GOARCH }}\n          path: age-*\n  source:\n    name: Package source code\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v5\n        with:\n          fetch-depth: 0\n          persist-credentials: false\n      - name: Install Go\n        uses: actions/setup-go@v6\n        with:\n          go-version-file: go.mod\n          cache: false\n      - name: Create source tarball\n        run: |\n          VERSION=\"$(git describe --tags)\"\n          DIR=\"$(mktemp -d)\"\n          mkdir \"$DIR/age\"\n          git archive --format=tar.gz HEAD | tar -xz -C \"$DIR/age\"\n          ( cd \"$DIR/age\"; go mod vendor )\n          for cmd in \"$DIR\"/age/{cmd,extra}/*; do\n            echo \"package main\" >> \"$cmd/version.go\"\n            echo \"\" >> \"$cmd/version.go\"\n            echo \"func init() { Version = \\\"$VERSION\\\" }\" >> \"$cmd/version.go\"\n          done\n          tar -cvzf \"age-$VERSION-source.tar.gz\" -C \"$DIR\" age\n      - name: Upload workflow artifacts\n        uses: actions/upload-artifact@v4\n        with:\n          name: age-artifacts-source\n          path: age-*-source.tar.gz\n  upload:\n    name: Upload and attest release artifacts\n    if: github.event_name == 'release'\n    needs: [build, source]\n    permissions:\n      contents: write\n      attestations: write\n      id-token: write\n    runs-on: ubuntu-latest\n    steps:\n      - name: Download workflow artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: age-artifacts-*\n          merge-multiple: true\n      - name: Generate artifacts attestation\n        uses: actions/attest-build-provenance@v3\n        with:\n          subject-path: age-*\n      - name: Upload release artifacts\n        run: gh release upload \"$GITHUB_REF_NAME\" age-*\n        env:\n          GH_REPO: ${{ github.repository }}\n          GH_TOKEN: ${{ github.token }}\n"
  },
  {
    "path": ".github/workflows/certs/README",
    "content": "In this folder there are\n\n    uitacllc.crt\n\n        PKCS#7 encoded certificate chain for a code signing certificate issued\n        to Up in the Air Consulting LLC valid until Sep 26 23:59:59 2024 GMT.\n\n        https://crt.sh/?id=5339775059\n\n    uitacllc.key\n\n        PEM encrypted private key for the leaf certificate above.\n        Its passphrase is long and randomly generated, so the awful legacy key\n        derivation doesn't really matter, and it makes osslsigncode happy.\n"
  },
  {
    "path": ".github/workflows/certs/uitacllc.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-256-CBC,B93C1A166F3677D68FB9CB3E8A184729\n\nUriYsaq3tLyvycDDB2YeQ+9L1P5VCPcfVkYR1ocleF8WxNDUPdz3RqbryAZZdXVO\n0bcvAHTXkdI4Oiw5mN0S8fGsNq9zn+pyResx3lXtgN3oCDCe2SQn28uEEKxPzud5\n0NRXYoBP+pLDjiuQ/6Lp7DovnAO/uxaPFvYRMiknNVOhwyHGWZyuUe01S9J9im7y\nvgc1wkyQzmABIhARynEXHp3KnM9aF8X1/ck839lQRBFrvRFNm5rqiON26spr1Hu5\nznrbVGROYk0XNdH5VHDk7V9k+v2WLL/b4nxlMymZpDzr9pXzX8olpLnQrarsMbHe\nysfXNTtQi5Dq6KXURW8VA4DmxAzTRNUxe2aA4JnAEyFU5LDLetTN9F9M7BUkHbXH\nRpSbZqDjPwg7U98vuSwxjIkncHSiYYi3FmSoupLvV+eIP6qRSgONdzGlP5NTn4Lh\nN1lYMPHPldH6UjLHrldkYN16TQlrqNHZExN91XvsZVjpyAgErY18xwi3CTEco45D\nfRqsiWXtoas4LkafhSY0vfl5aFhY9YPUpS6uFdgWBvgcQeYb8meX5Nr4dNXVk5Wa\nyRlYlW/X0TWC0T9qaBOPN/z7OWO5aL4jYRcKQQ+aR8gFcHGGCpRAKD369OneXfOQ\nMD9UHoPG4WTBg/NU9OSskcywfuSOkwAGfBVNXrnEj6tYFjsjYK2nC2gm+opUCfm0\na1FeDb5nQSOgOJKUCO6Aj+0NvDvVLUOsTk1lfzSugIkmUOdV+rXHnrZC+90q8KfN\nS2JlzwSZNg0e+VxZpnD7k7axHkbHrbebtrLvzKVnrh3s0OFAXN0isMw7yhhWtzUe\nmPoQTZusLDOAJe/QPuNlDUgr4uoVZtoXrPzoZZkw2VFLwYy2g/EYvlK9BdVVTnRm\n9Hq9IBDrZw+SV/7roaeVOXbzrQoxEoXcL7eo6iWvV5Q7Ll5C4ovelHKy3IAzcpYP\n6LKfxAO2sIKTALrHbtBNG+O4RTtxOva1hyg27V4v2k53CF/GhoBRPSpbbupwppXc\nlJJ9RtMTRfhCv/ObhdsJED+YUqFifTJfcnQ1iGN8dnBuGrjXxVCN0wgmv46Pdhn0\ntUfGlkFquOOWamaVaIvp6JCVUDa1ezMzleILoYvrxvOuP+dGVrwTwVCXpx4JuUgp\nd72/w+EnqlZnwsAzdrErJFXnHux981ZoojmG94km1B6gPPwMB8JRcD67lfhG/vne\nIpTuuzGaSInf24cGNig01hbBuKSg79yNY0llkECPBXbEhfkemEMhg1WHoNP2eG8j\nMHS5OCT5KiOfi77pSO3M2mGB1HWYE5R0lcMibukK9ZdyIYcTeMZ0RcGm6YSNv570\nok/Ex4LUCW66AIWFefmbIOtJSIMHlNKWRPJwnJxVoE5qgH0f/2xL3k15vpI55lAS\nsabzegnYlElPbUlZGhgwjKknxgqMhFIW/ZS0h2FukFLwipr4qI47nHWz5dguNkYn\n48sSKg3YMhVx/sT+X2A/6zqsC+p4PT7Ti5ruWb7S9L9vRuBdIDNE9qAwuz0g8Bs3\nWhOx6OW2ZqDQEuRhN0lyGA0mwRC4HPFE9b8dnN8lNm+RsnMfNoFxzPnqtsxhEAwa\n2a4ijT97ka94lDy7WQ2bwLRz7trKV/T6MeETKE4s7+z2dMTr1f8IwA2uCovFmO9T\naMQAePFEtDT3qwIPu0zH1ocSCkZ50f7RgVmp4FNn03uT/TnsASrr5CS9m8A9gjEn\nQiztQyqt27fTT61YkNdA6lwbpFiByugVbS+mWsNa9kvBkgQkcMQwgrELmU9sYdBT\nnRMa60i0nEINT/x3zFvT6R7Dl/O8/QhXLeYv20X2roghPw48IovLb8x7dT3YEQSn\nARIXXVPxwOVvS8xcCa69/+1HjC6vNG9dNNnAsVHxB8mDTBqmmLzAMOVzDoNWEgDd\nzoRhQ3ORb1brPlKWg8um/svLiSV63ZYi2J8LPamoGmZ/7J8i5rjOpOeG493UICBR\nJymmYGUo6/C1Ze8swdMHApVU/spo0s8BCGkMjYUAaxXD7RufN2DuY30Vny/DMn4y\nXasuHS9RstD2Okv25PD06Y2H52HJ6MNdArmPZRe0k2ZbhATs5dXOfmaF5Z0f4IkE\nG+hsxE1wlCo900ewntx16sBCbI0v9aE+Napf2+ueqPQ06CdfiTG5yOmeXzgR/8zS\nKVmTHpmmFpYtj/N350BLAVb/Hwzmh+ieWnO7TUjvNAHUn2i5LZU65rN3GOlPyIlz\nDzB2T6KjOUPFKqSRrIin14HLyf5w0vDuJhe5Zpe0hhYKvoKhwCEVefbmkasWeso3\nxsXxOOoL39GA0QpYjR6ztqR8fS9jTeu5IY+zY5LO8yS7+StP3H8CcqRMuxb3ntym\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": ".github/workflows/interop.yml",
    "content": "name: Interoperability tests\non: push\npermissions:\n  contents: read\njobs:\n  trigger:\n    name: Trigger\n    runs-on: ubuntu-latest\n    steps:\n      - name: Trigger interoperability tests in str4d/rage\n        run: >\n          gh api repos/str4d/rage/dispatches\n            --field event_type=\"age-interop-request\"\n            --field client_payload[sha]=\"$GITHUB_SHA\"\n        env:\n          GITHUB_TOKEN: ${{ secrets.RAGE_INTEROP_ACCESS_TOKEN }}\n"
  },
  {
    "path": ".github/workflows/ronn.yml",
    "content": "name: Generate man pages\non:\n  push:\n    branches:\n      - '**'\n    paths:\n      - '**.ronn'\n      - '**/ronn.yml'\npermissions:\n  contents: read\njobs:\n  ronn:\n    name: Ronn\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          persist-credentials: false\n      - uses: geomys/sandboxed-step@v1.2.1\n        with:\n          persist-workspace-changes: true\n          run: |\n            sudo apt-get update && sudo apt-get install -y ronn\n            bash -O globstar -c 'ronn **/*.ronn'\n            # rdiscount randomizes the output for no good reason, which causes\n            # changes to always get committed. Sigh.\n            # https://github.com/davidfstr/rdiscount/blob/6b1471ec3/ext/generate.c#L781-L795\n            for f in doc/*.html; do\n              awk '/Filippo Valsorda/ { $0 = \"<p>Filippo Valsorda <a href=\\\"mailto:age@filippo.io\\\" data-bare-link=\\\"true\\\">age@filippo.io</a></p>\" } { print }' \"$f\" > \"$f.tmp\"\n              mv \"$f.tmp\" \"$f\"\n            done\n      - uses: actions/upload-artifact@v4\n        with:\n          name: man-pages\n          path: |\n            doc/*.1\n            doc/*.html\n  commit:\n    name: Commit changes\n    needs: ronn\n    permissions:\n      contents: write\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          persist-credentials: true\n      - uses: actions/download-artifact@v4\n        with:\n          name: man-pages\n          path: doc/\n      - name: Commit and push if changed\n        run: |-\n          git config user.name \"GitHub Actions\"\n          git config user.email \"actions@users.noreply.github.com\"\n          git add doc/\n          git commit -m \"doc: regenerate groff and html man pages\" || exit 0\n          git push\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Go tests\non:\n  push:\n  pull_request:\n  schedule: # daily at 09:42 UTC\n    - cron: '42 9 * * *'\n  workflow_dispatch:\npermissions:\n  contents: read\njobs:\n  test:\n    strategy:\n      fail-fast: false\n      matrix:\n        go:\n          - { go-version: stable }\n          - { go-version: oldstable }\n        os:\n          - ubuntu-latest\n          - macos-latest\n          - windows-latest\n    runs-on: ${{ matrix.os }}\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go.go-version }}\n      - run: |\n          go test -race ./...\n  test-latest:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: false\n      matrix:\n        go:\n          - { go-version: stable }\n          - { go-version: oldstable }\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: ${{ matrix.go.go-version }}\n      - uses: geomys/sandboxed-step@v1.2.1\n        with:\n          run: |\n            go get -u -t ./...\n            go test -race ./...\n  staticcheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: stable\n      - uses: geomys/sandboxed-step@v1.2.1\n        with:\n          run: go run honnef.co/go/tools/cmd/staticcheck@latest ./...\n  govulncheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v5\n        with:\n          persist-credentials: false\n      - uses: actions/setup-go@v6\n        with:\n          go-version: stable\n      - uses: geomys/sandboxed-step@v1.2.1\n        with:\n          run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright 2019 The age Authors\nCopyright 2019 Google LLC\nCopyright 2022 Filippo Valsorda\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n   * Redistributions of source code must retain the above copyright\nnotice, this list of conditions and the following disclaimer.\n   * Redistributions in binary form must reproduce the above\ncopyright notice, this list of conditions and the following disclaimer\nin the documentation and/or other materials provided with the\ndistribution.\n   * Neither the name of the age project nor the names of its\ncontributors may be used to endorse or promote products derived from\nthis software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT\nOWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\nSPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT\nLIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,\nDATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY\nTHEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n    <picture>\n        <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://github.com/FiloSottile/age/blob/main/logo/logo_white.svg\">\n        <source media=\"(prefers-color-scheme: light)\" srcset=\"https://github.com/FiloSottile/age/blob/main/logo/logo.svg\">\n        <img alt=\"The age logo, a wireframe of St. Peters dome in Rome, with the text: age, file encryption\" width=\"600\" src=\"https://github.com/FiloSottile/age/blob/main/logo/logo.svg\">\n    </picture>\n</p>\n\n[![Go Reference](https://pkg.go.dev/badge/filippo.io/age.svg)](https://pkg.go.dev/filippo.io/age)\n[![man page](<https://img.shields.io/badge/age(1)-man%20page-lightgrey>)](https://filippo.io/age/age.1)\n[![C2SP specification](https://img.shields.io/badge/%C2%A7%23-specification-blueviolet)](https://age-encryption.org/v1)\n\nage is a simple, modern and secure file encryption tool, format, and Go library.\n\nIt features small explicit keys, post-quantum support, no config options, and UNIX-style composability.\n\n```\n$ age-keygen -o key.txt\nPublic key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p\n$ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age\n$ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz\n```\n\n📜 The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@benjojo](https://github.com/benjojo) and [@FiloSottile](https://github.com/FiloSottile).\n\n🦀 An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).\n\n🌍 [Typage](https://github.com/FiloSottile/typage) is a TypeScript implementation. It works in the browser, Node.js, Deno, and Bun.\n\n🔑 Hardware PIV tokens such as YubiKeys are supported through the [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) plugin.\n\n✨ For more plugins, implementations, tools, and integrations, check out the [awesome age](https://github.com/FiloSottile/awesome-age) list.\n\n💬 The author pronounces it `[aɡe̞]` [with a hard *g*](https://translate.google.com/?sl=it&text=aghe), like GIF, and it's always spelled lowercase.\n\n## Installation\n\n<table>\n    <tr>\n        <td>Homebrew (macOS or Linux)</td>\n        <td>\n            <code>brew install age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>MacPorts</td>\n        <td>\n            <code>port install age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Windows</td>\n        <td>\n            <code>winget install --id FiloSottile.age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Alpine Linux v3.15+</td>\n        <td>\n            <code>apk add age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Arch Linux</td>\n        <td>\n            <code>pacman -S age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Debian 12+ (Bookworm)</td>\n        <td>\n            <code>apt install age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Debian 11 (Bullseye)</td>\n        <td>\n            <code>apt install age/bullseye-backports</code>\n            (<a href=\"https://backports.debian.org/Instructions/#index2h2\">enable backports</a> for age v1.0.0+)\n        </td>\n    </tr>\n    <tr>\n        <td>Fedora 33+</td>\n        <td>\n            <code>dnf install age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Gentoo Linux</td>\n        <td>\n            <code>emerge app-crypt/age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Guix System</td>\n        <td>\n            <code>guix package -i age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>NixOS / Nix</td>\n        <td>\n            <code>nix-env -i age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>openSUSE Tumbleweed</td>\n        <td>\n            <code>zypper install age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Ubuntu 22.04+</td>\n        <td>\n            <code>apt install age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Void Linux</td>\n        <td>\n            <code>xbps-install age</code>\n        </td>\n    </tr>\n    <tr>\n        <td>FreeBSD</td>\n        <td>\n            <code>pkg install age</code> (security/age)\n        </td>\n    </tr>\n    <tr>\n        <td>OpenBSD 6.7+</td>\n        <td>\n            <code>pkg_add age</code> (security/age)\n        </td>\n    </tr>\n    <tr>\n        <td>Chocolatey (Windows)</td>\n        <td>\n            <code>choco install age.portable</code>\n        </td>\n    </tr>\n    <tr>\n        <td>Scoop (Windows)</td>\n        <td>\n            <code>scoop bucket add extras && scoop install age</code>\n        </td>\n    </tr>\n</table>\n\nOn Windows, Linux, macOS, and FreeBSD you can use the pre-built binaries.\n\n```\nhttps://dl.filippo.io/age/latest?for=linux/amd64\nhttps://dl.filippo.io/age/v1.3.1?for=darwin/arm64\n...\n```\n\nIf you download the pre-built binaries, you can check their [Sigsum proofs](./SIGSUM.md).\n\nIf your system has [a supported version of Go](https://go.dev/dl/), you can build from source.\n\n```\ngo install filippo.io/age/cmd/...@latest\n```\n\nHelp from new packagers is very welcome.\n\n## Usage\n\nFor the full documentation, read [the age(1) man page](https://filippo.io/age/age.1).\n\n```\nUsage:\n    age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]\n    age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]\n    age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]\n\nOptions:\n    -e, --encrypt               Encrypt the input to the output. Default if omitted.\n    -d, --decrypt               Decrypt the input to the output.\n    -o, --output OUTPUT         Write the result to the file at path OUTPUT.\n    -a, --armor                 Encrypt to a PEM encoded format.\n    -p, --passphrase            Encrypt with a passphrase.\n    -r, --recipient RECIPIENT   Encrypt to the specified RECIPIENT. Can be repeated.\n    -R, --recipients-file PATH  Encrypt to recipients listed at PATH. Can be repeated.\n    -i, --identity PATH         Use the identity file at PATH. Can be repeated.\n\nINPUT defaults to standard input, and OUTPUT defaults to standard output.\nIf OUTPUT exists, it will be overwritten.\n\nRECIPIENT can be an age public key generated by age-keygen (\"age1...\")\nor an SSH public key (\"ssh-ed25519 AAAA...\", \"ssh-rsa AAAA...\").\n\nRecipient files contain one or more recipients, one per line. Empty lines\nand lines starting with \"#\" are ignored as comments. \"-\" may be used to\nread recipients from standard input.\n\nIdentity files contain one or more secret keys (\"AGE-SECRET-KEY-1...\"),\none per line, or an SSH key. Empty lines and lines starting with \"#\" are\nignored as comments. Passphrase encrypted age files can be used as\nidentity files. Multiple key files can be provided, and any unused ones\nwill be ignored. \"-\" may be used to read identities from standard input.\n\nWhen --encrypt is specified explicitly, -i can also be used to encrypt to an\nidentity file symmetrically, instead or in addition to normal recipients.\n```\n\n### Multiple recipients\n\nFiles can be encrypted to multiple recipients by repeating `-r/--recipient`. Every recipient will be able to decrypt the file.\n\n```\n$ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \\\n    -r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg\n```\n\n#### Recipient files\n\nMultiple recipients can also be listed one per line in one or more files passed with the `-R/--recipients-file` flag.\n\n```\n$ cat recipients.txt\n# Alice\nage1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p\n# Bob\nage1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg\n$ age -R recipients.txt example.jpg > example.jpg.age\n```\n\nIf the argument to `-R` (or `-i`) is `-`, the file is read from standard input.\n\n### Post-quantum keys\n\nTo generate hybrid post-quantum keys, which are secure against future quantum\ncomputer attacks, use the `-pq` flag with `age-keygen`. This may become the\ndefault in the future.\n\nPost-quantum identities start with `AGE-SECRET-KEY-PQ-1...` and recipients with\n`age1pq1...`. The recipients are unfortunately ~2000 characters long.\n\n```\n$ age-keygen -pq -o key.txt\n$ age-keygen -y key.txt > recipient.txt\n$ age -R recipient.txt example.jpg > example.jpg.age\n$ age -d -i key.txt example.jpg.age > example.jpg\n```\n\nSupport for post-quantum keys is built into age v1.3.0 and later. Alternatively,\nthe `age-plugin-pq` binary can be installed and placed in `$PATH` to add support\nto any version and implementation of age that supports plugins. Recipients will\nwork out of the box, while identities will have to be converted to plugin\nidentities with `age-plugin-pq -identity`.\n\n### Passphrases\n\nFiles can be encrypted with a passphrase by using `-p/--passphrase`. By default age will automatically generate a secure passphrase. Passphrase protected files are automatically detected at decrypt time.\n\n```\n$ age -p secrets.txt > secrets.txt.age\nEnter passphrase (leave empty to autogenerate a secure one):\nUsing the autogenerated passphrase \"release-response-step-brand-wrap-ankle-pair-unusual-sword-train\".\n$ age -d secrets.txt.age > secrets.txt\nEnter passphrase:\n```\n\n### Passphrase-protected key files\n\nIf an identity file passed to `-i` is a passphrase encrypted age file, it will be automatically decrypted.\n\n```\n$ age-keygen | age -p > key.age\nPublic key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5\nEnter passphrase (leave empty to autogenerate a secure one):\nUsing the autogenerated passphrase \"hip-roast-boring-snake-mention-east-wasp-honey-input-actress\".\n$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age\n$ age -d -i key.age secrets.txt.age > secrets.txt\nEnter passphrase for identity file \"key.age\":\n```\n\nPassphrase-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system. However, they can be useful if the identity file is stored remotely.\n\n### SSH keys\n\nAs a convenience feature, age also supports encrypting to `ssh-rsa` and `ssh-ed25519` SSH public keys, and decrypting with the respective private key file. (`ssh-agent` is not supported.)\n\n```\n$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age\n$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg\n```\n\nNote that SSH key support employs more complex cryptography, and embeds a public key tag in the encrypted file, making it possible to track files that are encrypted to a specific public key.\n\n#### Encrypting to a GitHub user\n\nCombining SSH key support and `-R`, you can easily encrypt a file to the SSH keys listed on a GitHub profile.\n\n```\n$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age\n```\n\nKeep in mind that people might not protect SSH keys long-term, since they are revokable when used only for authentication, and that SSH keys held on YubiKeys can't be used to decrypt files.\n\n### Inspecting encrypted files\n\nThe `age-inspect` command can display metadata about an encrypted file without decrypting it, including the recipient types, whether it uses post-quantum encryption, and the payload size.\n\n```\n$ age-inspect secrets.age\nsecrets.age is an age file, version \"age-encryption.org/v1\".\n\nThis file is encrypted to the following recipient types:\n  - \"mlkem768x25519\"\n\nThis file uses post-quantum encryption.\n\nSize breakdown (assuming it decrypts successfully):\n\n    Header                      1627 bytes\n    Encryption overhead           32 bytes\n    Payload                       42 bytes\n                        -------------------\n    Total                       1701 bytes\n\n```\n\nFor scripting, use `--json` to get machine-readable output.\n"
  },
  {
    "path": "SIGSUM.md",
    "content": "If you download the pre-built binaries of version v1.2.0+, you can check their\n[Sigsum](https://www.sigsum.org) proofs, which are like signatures with extra\ntransparency: you can cryptographically verify that every proof is logged in a\npublic append-only log, so the age project can be held accountable for every\nbinary release we ever produced. This is similar to what the [Go Checksum\nDatabase](https://go.dev/blog/module-mirror-launch) provides.\n\n```\ncat << EOF > age-sigsum-key.pub\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1WpnEswJLPzvXJDiswowy48U+G+G1kmgwUE2eaRHZG\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAz2WM5CyPLqiNjk7CLl4roDXwKhQ0QExXLebukZEZFS\nEOF\n\ncurl -JLO \"https://dl.filippo.io/age/v1.3.1?for=darwin/arm64\"\ncurl -JLO \"https://dl.filippo.io/age/v1.3.1?for=darwin/arm64&proof\"\n\ngo install sigsum.org/sigsum-go/cmd/sigsum-verify@v0.13.1\nsigsum-verify -k age-sigsum-key.pub -P sigsum-generic-2025-1 \\\n    age-v1.3.1-darwin-arm64.tar.gz.proof < age-v1.3.1-darwin-arm64.tar.gz\n```\n\nYou can learn more about what's happening above in the [Sigsum\ndocs](https://www.sigsum.org/getting-started/).\n\n### Release playbook\n\nDear future me, to sign a new release and produce Sigsum proofs, run the following\n\n```\nVERSION=v1.3.1\ngo install sigsum.org/sigsum-go/cmd/sigsum-verify@latest\ngo install github.com/tillitis/tkey-ssh-agent/cmd/tkey-ssh-agent@main\ntkey-ssh-agent --agent-socket tkey-ssh-agent.sock --uss\npassage -c other/tkey-ssh-sigsum-age\nSSH_AUTH_SOCK=tkey-ssh-agent.sock ssh-add -L > tkey-ssh-agent.pub\npassage other/sigsum-ratelimit > sigsum-ratelimit\ngh release download $VERSION --repo FiloSottile/age --dir artifacts/\nSSH_AUTH_SOCK=tkey-ssh-agent.sock sigsum-submit -k tkey-ssh-agent.pub -P sigsum-generic-2025-1 -a sigsum-ratelimit -d filippo.io artifacts/*\ngh release upload $VERSION --repo FiloSottile/age artifacts/*.proof\n```\n\nIn the future, we will move to reproducing the artifacts locally, and signing\nthose instead of the ones built by GitHub Actions.\n"
  },
  {
    "path": "age.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package age implements file encryption according to the age-encryption.org/v1\n// specification.\n//\n// For most use cases, use the [Encrypt] and [Decrypt] functions with\n// [HybridRecipient] and [HybridIdentity]. If passphrase encryption is\n// required, use [ScryptRecipient] and [ScryptIdentity]. For compatibility with\n// existing SSH keys use the filippo.io/age/agessh package.\n//\n// age encrypted files are binary and not malleable. For encoding them as text,\n// use the filippo.io/age/armor package.\n//\n// # Key management\n//\n// age does not have a global keyring. Instead, since age keys are small,\n// textual, and cheap, you are encouraged to generate dedicated keys for each\n// task and application.\n//\n// Recipient public keys can be passed around as command line flags and in\n// config files, while secret keys should be stored in dedicated files, through\n// secret management systems, or as environment variables.\n//\n// There is no default path for age keys. Instead, they should be stored at\n// application-specific paths. The CLI supports files where private keys are\n// listed one per line, ignoring empty lines and lines starting with \"#\". These\n// files can be parsed with [ParseIdentities].\n//\n// When integrating age into a new system, it's recommended that you only\n// support native (X25519 and hybrid) keys, and not SSH keys. The latter are\n// supported for manual encryption operations. If you need to tie into existing\n// key management infrastructure, you might want to consider implementing your\n// own [Recipient] and [Identity].\n//\n// # Backwards compatibility\n//\n// Files encrypted with a stable version (not alpha, beta, or release candidate)\n// of age, or with any v1.0.0 beta or release candidate, will decrypt with any\n// later versions of the v1 API. This might change in v2, in which case v1 will\n// be maintained with security fixes for compatibility with older files.\n//\n// If decrypting an older file poses a security risk, doing so might require an\n// explicit opt-in in the API.\npackage age\n\nimport (\n\t\"bytes\"\n\t\"crypto/hmac\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\t\"sort\"\n\n\t\"filippo.io/age/internal/format\"\n\t\"filippo.io/age/internal/stream\"\n)\n\n// An Identity is passed to [Decrypt] to unwrap an opaque file key from a\n// recipient stanza. It can be for example a secret key like [HybridIdentity], a\n// plugin, or a custom implementation.\ntype Identity interface {\n\t// Unwrap must return an error wrapping [ErrIncorrectIdentity] if none of\n\t// the recipient stanzas match the identity, any other error will be\n\t// considered fatal.\n\t//\n\t// Most age API users won't need to interact with this method directly, and\n\t// should instead pass [Identity] implementations to [Decrypt].\n\tUnwrap(stanzas []*Stanza) (fileKey []byte, err error)\n}\n\n// ErrIncorrectIdentity is returned by [Identity.Unwrap] if none of the\n// recipient stanzas match the identity.\nvar ErrIncorrectIdentity = errors.New(\"incorrect identity for recipient block\")\n\n// A Recipient is passed to [Encrypt] to wrap an opaque file key to one or more\n// recipient stanza(s). It can be for example a public key like [HybridRecipient],\n// a plugin, or a custom implementation.\ntype Recipient interface {\n\t// Most age API users won't need to interact with this method directly, and\n\t// should instead pass [Recipient] implementations to [Encrypt].\n\tWrap(fileKey []byte) ([]*Stanza, error)\n}\n\n// RecipientWithLabels can be optionally implemented by a [Recipient], in which\n// case [Encrypt] will use WrapWithLabels instead of [Recipient.Wrap].\n//\n// Encrypt will succeed only if the labels returned by all the recipients\n// (assuming the empty set for those that don't implement RecipientWithLabels)\n// are the same.\n//\n// This can be used to ensure a recipient is only used with other recipients\n// with equivalent properties (for example by setting a \"postquantum\" label) or\n// to ensure a recipient is always used alone (by returning a random label, for\n// example to preserve its authentication properties).\ntype RecipientWithLabels interface {\n\tWrapWithLabels(fileKey []byte) (s []*Stanza, labels []string, err error)\n}\n\n// A Stanza is a section of the age header that encapsulates the file key as\n// encrypted to a specific recipient.\n//\n// Most age API users won't need to interact with this type directly, and should\n// instead pass [Recipient] implementations to [Encrypt] and [Identity]\n// implementations to [Decrypt].\ntype Stanza struct {\n\tType string\n\tArgs []string\n\tBody []byte\n}\n\nconst fileKeySize = 16\nconst streamNonceSize = 16\n\nfunc encryptHdr(fileKey []byte, recipients ...Recipient) (*format.Header, error) {\n\tif len(recipients) == 0 {\n\t\treturn nil, errors.New(\"no recipients specified\")\n\t}\n\n\thdr := &format.Header{}\n\tvar labels []string\n\tfor i, r := range recipients {\n\t\tstanzas, l, err := wrapWithLabels(r, fileKey)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to wrap key for recipient #%d: %w\", i, err)\n\t\t}\n\t\tsort.Strings(l)\n\t\tif i == 0 {\n\t\t\tlabels = l\n\t\t} else if !slicesEqual(labels, l) {\n\t\t\treturn nil, incompatibleLabelsError(labels, l)\n\t\t}\n\t\tfor _, s := range stanzas {\n\t\t\thdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))\n\t\t}\n\t}\n\tif mac, err := headerMAC(fileKey, hdr); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to compute header MAC: %v\", err)\n\t} else {\n\t\thdr.MAC = mac\n\t}\n\treturn hdr, nil\n}\n\n// Encrypt encrypts a file to one or more recipients. Every recipient will be\n// able to decrypt the file.\n//\n// Writes to the returned WriteCloser are encrypted and written to dst as an age\n// file. The caller must call Close on the WriteCloser when done for the last\n// chunk to be encrypted and flushed to dst.\nfunc Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {\n\tfileKey := make([]byte, fileKeySize)\n\trand.Read(fileKey)\n\n\thdr, err := encryptHdr(fileKey, recipients...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := hdr.Marshal(dst); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to write header: %w\", err)\n\t}\n\n\tnonce := make([]byte, streamNonceSize)\n\trand.Read(nonce)\n\tif _, err := dst.Write(nonce); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to write nonce: %w\", err)\n\t}\n\n\treturn stream.NewEncryptWriter(streamKey(fileKey, nonce), dst)\n}\n\n// EncryptReader encrypts a file to one or more recipients. Every recipient will be\n// able to decrypt the file.\n//\n// Reads from the returned Reader produce the encrypted file, where the plaintext\n// is read from src.\nfunc EncryptReader(src io.Reader, recipients ...Recipient) (io.Reader, error) {\n\tfileKey := make([]byte, fileKeySize)\n\trand.Read(fileKey)\n\n\thdr, err := encryptHdr(fileKey, recipients...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tbuf := &bytes.Buffer{}\n\tif err := hdr.Marshal(buf); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to prepare header: %w\", err)\n\t}\n\n\tnonce := make([]byte, streamNonceSize)\n\trand.Read(nonce)\n\n\tr, err := stream.NewEncryptReader(streamKey(fileKey, nonce), src)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn io.MultiReader(buf, bytes.NewReader(nonce), r), nil\n}\n\nfunc wrapWithLabels(r Recipient, fileKey []byte) (s []*Stanza, labels []string, err error) {\n\tif r, ok := r.(RecipientWithLabels); ok {\n\t\treturn r.WrapWithLabels(fileKey)\n\t}\n\ts, err = r.Wrap(fileKey)\n\treturn\n}\n\nfunc slicesEqual(s1, s2 []string) bool {\n\tif len(s1) != len(s2) {\n\t\treturn false\n\t}\n\tfor i := range s1 {\n\t\tif s1[i] != s2[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc incompatibleLabelsError(l1, l2 []string) error {\n\thasPQ1 := slices.Contains(l1, \"postquantum\")\n\thasPQ2 := slices.Contains(l2, \"postquantum\")\n\tif hasPQ1 != hasPQ2 {\n\t\treturn fmt.Errorf(\"incompatible recipients: can't mix post-quantum and classic recipients, or the file would be vulnerable to quantum computers\")\n\t}\n\treturn fmt.Errorf(\"incompatible recipients: %q and %q can't be mixed\", l1, l2)\n}\n\n// NoIdentityMatchError is returned by [Decrypt] when none of the supplied\n// identities match the encrypted file.\ntype NoIdentityMatchError struct {\n\t// Errors is a slice of all the errors returned to Decrypt by the Unwrap\n\t// calls it made. They all wrap [ErrIncorrectIdentity].\n\tErrors []error\n\t// StanzaTypes are the first argument of each recipient stanza in the\n\t// encrypted file's header.\n\tStanzaTypes []string\n}\n\nfunc (e *NoIdentityMatchError) Error() string {\n\tif len(e.Errors) == 1 {\n\t\treturn \"identity did not match any of the recipients: \" + e.Errors[0].Error()\n\t}\n\treturn \"no identity matched any of the recipients\"\n}\n\nfunc (e *NoIdentityMatchError) Unwrap() []error {\n\treturn e.Errors\n}\n\n// Decrypt decrypts a file encrypted to one or more identities.\n// All identities will be tried until one successfully decrypts the file.\n// Native, non-interactive identities are tried before any other identities.\n//\n// Decrypt returns a Reader reading the decrypted plaintext of the age file read\n// from src. If no identity matches the encrypted file, the returned error will\n// be of type [NoIdentityMatchError].\nfunc Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {\n\thdr, payload, err := format.Parse(src)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read header: %w\", err)\n\t}\n\n\tfileKey, err := decryptHdr(hdr, identities...)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnonce := make([]byte, streamNonceSize)\n\tif _, err := io.ReadFull(payload, nonce); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read nonce: %w\", err)\n\t}\n\n\treturn stream.NewDecryptReader(streamKey(fileKey, nonce), payload)\n}\n\n// DecryptReaderAt decrypts a file encrypted to one or more identities.\n// All identities will be tried until one successfully decrypts the file.\n// Native, non-interactive identities are tried before any other identities.\n//\n// DecryptReaderAt takes an underlying [io.ReaderAt] and its total encrypted\n// size, and returns a ReaderAt of the decrypted plaintext and the plaintext\n// size. These can be used for example to instantiate an [io.SectionReader],\n// which implements [io.Reader] and [io.Seeker], or for [zip.NewReader].\n// Note that ReaderAt by definition disregards the seek position of src.\n//\n// The ReadAt method of the returned ReaderAt can be called concurrently.\n// The ReaderAt will internally cache the most recently decrypted chunk.\n// DecryptReaderAt reads and decrypts the final chunk before returning,\n// to authenticate the plaintext size.\n//\n// If no identity matches the encrypted file, the returned error will be of\n// type [NoIdentityMatchError].\nfunc DecryptReaderAt(src io.ReaderAt, encryptedSize int64, identities ...Identity) (io.ReaderAt, int64, error) {\n\tsrcReader := io.NewSectionReader(src, 0, encryptedSize)\n\thdr, payload, err := format.Parse(srcReader)\n\tif err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"failed to read header: %w\", err)\n\t}\n\tbuf := &bytes.Buffer{}\n\tif err := hdr.Marshal(buf); err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"failed to serialize header: %w\", err)\n\t}\n\n\tfileKey, err := decryptHdr(hdr, identities...)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tnonce := make([]byte, streamNonceSize)\n\tif _, err := io.ReadFull(payload, nonce); err != nil {\n\t\treturn nil, 0, fmt.Errorf(\"failed to read nonce: %w\", err)\n\t}\n\n\tpayloadOffset := int64(buf.Len()) + int64(len(nonce))\n\tpayloadSize := encryptedSize - payloadOffset\n\tplaintextSize, err := stream.PlaintextSize(payloadSize)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\tpayloadReaderAt := io.NewSectionReader(src, payloadOffset, payloadSize)\n\tr, err := stream.NewDecryptReaderAt(streamKey(fileKey, nonce), payloadReaderAt, payloadSize)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\treturn r, plaintextSize, nil\n}\n\nfunc decryptHdr(hdr *format.Header, identities ...Identity) ([]byte, error) {\n\tif len(identities) == 0 {\n\t\treturn nil, errors.New(\"no identities specified\")\n\t}\n\tslices.SortStableFunc(identities, func(a, b Identity) int {\n\t\tvar aIsNative, bIsNative bool\n\t\tswitch a.(type) {\n\t\tcase *X25519Identity, *HybridIdentity, *ScryptIdentity:\n\t\t\taIsNative = true\n\t\t}\n\t\tswitch b.(type) {\n\t\tcase *X25519Identity, *HybridIdentity, *ScryptIdentity:\n\t\t\tbIsNative = true\n\t\t}\n\t\tif aIsNative && !bIsNative {\n\t\t\treturn -1\n\t\t}\n\t\tif !aIsNative && bIsNative {\n\t\t\treturn 1\n\t\t}\n\t\treturn 0\n\t})\n\n\tstanzas := make([]*Stanza, 0, len(hdr.Recipients))\n\terrNoMatch := &NoIdentityMatchError{}\n\tfor _, s := range hdr.Recipients {\n\t\terrNoMatch.StanzaTypes = append(errNoMatch.StanzaTypes, s.Type)\n\t\tstanzas = append(stanzas, (*Stanza)(s))\n\t}\n\tvar fileKey []byte\n\tfor _, id := range identities {\n\t\tvar err error\n\t\tfileKey, err = id.Unwrap(stanzas)\n\t\tif errors.Is(err, ErrIncorrectIdentity) {\n\t\t\terrNoMatch.Errors = append(errNoMatch.Errors, err)\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tbreak\n\t}\n\tif fileKey == nil {\n\t\treturn nil, errNoMatch\n\t}\n\n\tif mac, err := headerMAC(fileKey, hdr); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to compute header MAC: %v\", err)\n\t} else if !hmac.Equal(mac, hdr.MAC) {\n\t\treturn nil, errors.New(\"bad header MAC\")\n\t}\n\n\treturn fileKey, nil\n}\n\n// multiUnwrap is a helper that implements Identity.Unwrap in terms of a\n// function that unwraps a single recipient stanza.\nfunc multiUnwrap(unwrap func(*Stanza) ([]byte, error), stanzas []*Stanza) ([]byte, error) {\n\tfor _, s := range stanzas {\n\t\tfileKey, err := unwrap(s)\n\t\tif errors.Is(err, ErrIncorrectIdentity) {\n\t\t\t// If we ever start returning something interesting wrapping\n\t\t\t// ErrIncorrectIdentity, we should let it make its way up through\n\t\t\t// Decrypt into NoIdentityMatchError.Errors.\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn fileKey, nil\n\t}\n\treturn nil, ErrIncorrectIdentity\n}\n\n// ExtractHeader returns a detached header from the src file.\n//\n// The detached header can be decrypted with [DecryptHeader] (for example on a\n// different system, without sharing the ciphertext) and then the file key can\n// be used with [NewInjectedFileKeyIdentity].\n//\n// This is a low-level function that most users won't need.\nfunc ExtractHeader(src io.Reader) ([]byte, error) {\n\thdr, _, err := format.Parse(src)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read header: %w\", err)\n\t}\n\tbuf := &bytes.Buffer{}\n\tif err := hdr.Marshal(buf); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to serialize header: %w\", err)\n\t}\n\treturn buf.Bytes(), nil\n}\n\n// DecryptHeader decrypts a detached header and returns a file key.\n//\n// The detached header can be produced by [ExtractHeader], and the\n// returned file key can be used with [NewInjectedFileKeyIdentity].\n//\n// This is a low-level function that most users won't need.\n// It is the caller's responsibility to keep track of what file the\n// returned file key decrypts, and to ensure the file key is not used\n// for any other purpose.\nfunc DecryptHeader(header []byte, identities ...Identity) ([]byte, error) {\n\thdr, _, err := format.Parse(bytes.NewReader(header))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read header: %w\", err)\n\t}\n\treturn decryptHdr(hdr, identities...)\n}\n\ntype injectedFileKeyIdentity struct {\n\tfileKey []byte\n}\n\n// NewInjectedFileKeyIdentity returns an [Identity] that always produces\n// a fixed file key, allowing the use of a file key obtained out-of-band,\n// for example via [DecryptHeader].\n//\n// This is a low-level function that most users won't need.\nfunc NewInjectedFileKeyIdentity(fileKey []byte) Identity {\n\treturn injectedFileKeyIdentity{fileKey}\n}\n\nfunc (i injectedFileKeyIdentity) Unwrap(stanzas []*Stanza) (fileKey []byte, err error) {\n\treturn i.fileKey, nil\n}\n"
  },
  {
    "path": "age_test.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage age_test\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n)\n\nfunc ExampleEncrypt() {\n\tpublicKey := \"age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm\"\n\trecipient, err := age.ParseX25519Recipient(publicKey)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to parse public key %q: %v\", publicKey, err)\n\t}\n\n\tout := &bytes.Buffer{}\n\n\tw, err := age.Encrypt(out, recipient)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create encrypted file: %v\", err)\n\t}\n\tif _, err := io.WriteString(w, \"Black lives matter.\"); err != nil {\n\t\tlog.Fatalf(\"Failed to write to encrypted file: %v\", err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tlog.Fatalf(\"Failed to close encrypted file: %v\", err)\n\t}\n\n\tfmt.Printf(\"Encrypted file size: %d\\n\", out.Len())\n\t// Output:\n\t// Encrypted file size: 219\n}\n\n// DO NOT hardcode the private key. Store it in a secret storage solution,\n// on disk if the local machine is trusted, or have the user provide it.\nvar privateKey string\n\nfunc init() {\n\tprivateKey = \"AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU\"\n}\n\nfunc ExampleDecrypt() {\n\tidentity, err := age.ParseX25519Identity(privateKey)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to parse private key: %v\", err)\n\t}\n\n\tf, err := os.Open(\"testdata/example.age\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open file: %v\", err)\n\t}\n\n\tr, err := age.Decrypt(f, identity)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open encrypted file: %v\", err)\n\t}\n\tout := &bytes.Buffer{}\n\tif _, err := io.Copy(out, r); err != nil {\n\t\tlog.Fatalf(\"Failed to read encrypted file: %v\", err)\n\t}\n\n\tfmt.Printf(\"File contents: %q\\n\", out.Bytes())\n\t// Output:\n\t// File contents: \"Black lives matter.\"\n}\n\nfunc ExampleParseIdentities() {\n\tkeyFile, err := os.Open(\"testdata/example_keys.txt\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open private keys file: %v\", err)\n\t}\n\tidentities, err := age.ParseIdentities(keyFile)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to parse private key: %v\", err)\n\t}\n\n\tf, err := os.Open(\"testdata/example.age\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open file: %v\", err)\n\t}\n\n\tr, err := age.Decrypt(f, identities...)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open encrypted file: %v\", err)\n\t}\n\tout := &bytes.Buffer{}\n\tif _, err := io.Copy(out, r); err != nil {\n\t\tlog.Fatalf(\"Failed to read encrypted file: %v\", err)\n\t}\n\n\tfmt.Printf(\"File contents: %q\\n\", out.Bytes())\n\t// Output:\n\t// File contents: \"Black lives matter.\"\n}\n\nfunc ExampleGenerateX25519Identity() {\n\tidentity, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to generate key pair: %v\", err)\n\t}\n\n\tfmt.Printf(\"Public key: %s...\\n\", identity.Recipient().String()[:4])\n\tfmt.Printf(\"Private key: %s...\\n\", identity.String()[:16])\n\t// Output:\n\t// Public key: age1...\n\t// Private key: AGE-SECRET-KEY-1...\n}\n\nconst helloWorld = \"Hello, Twitch!\"\n\nfunc TestEncryptDecryptX25519(t *testing.T) {\n\ta, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tb, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tbuf := &bytes.Buffer{}\n\tw, err := age.Encrypt(buf, a.Recipient(), b.Recipient())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := io.WriteString(w, helloWorld); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tout, err := age.Decrypt(buf, b)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\toutBytes, err := io.ReadAll(out)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(outBytes) != helloWorld {\n\t\tt.Errorf(\"wrong data: %q, excepted %q\", outBytes, helloWorld)\n\t}\n}\n\nfunc TestEncryptDecryptScrypt(t *testing.T) {\n\tpassword := \"twitch.tv/filosottile\"\n\n\tr, err := age.NewScryptRecipient(password)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tr.SetWorkFactor(15)\n\tbuf := &bytes.Buffer{}\n\tw, err := age.Encrypt(buf, r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := io.WriteString(w, helloWorld); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\ti, err := age.NewScryptIdentity(password)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tout, err := age.Decrypt(buf, i)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\toutBytes, err := io.ReadAll(out)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(outBytes) != helloWorld {\n\t\tt.Errorf(\"wrong data: %q, excepted %q\", outBytes, helloWorld)\n\t}\n}\n\nfunc ExampleDecryptReaderAt() {\n\tidentity, err := age.ParseX25519Identity(privateKey)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to parse private key: %v\", err)\n\t}\n\n\tf, err := os.Open(\"testdata/example.zip.age\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open file: %v\", err)\n\t}\n\tstat, err := f.Stat()\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to stat file: %v\", err)\n\t}\n\n\tr, size, err := age.DecryptReaderAt(f, stat.Size(), identity)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open encrypted file: %v\", err)\n\t}\n\n\tz, err := zip.NewReader(r, size)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open zip: %v\", err)\n\t}\n\tcontents, err := fs.ReadFile(z, \"example.txt\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to read file from zip: %v\", err)\n\t}\n\n\tfmt.Printf(\"File contents: %q\\n\", contents)\n\t// Output:\n\t// File contents: \"Black lives matter.\"\n}\n\nfunc TestParseIdentities(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\twantCount int\n\t\twantErr   bool\n\t\tfile      string\n\t}{\n\t\t{\"valid\", 2, false, `\n# this is a comment\n# AGE-SECRET-KEY-1705XN76M8EYQ8M9PY4E2G3KA8DN7NSCGT3V4HMN20H3GCX4AS6HSSTG8D3\n#\n\nAGE-SECRET-KEY-1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59QJ\nAGE-SECRET-KEY-19WUMFE89H3928FRJ5U3JYRNHM6CERQGKSQ584AQ8QY7T7R09D32SWE4DYH`},\n\t\t{\"invalid\", 0, true, `\nAGE-SECRET-KEY-1705XN76M8EYQ8M9PY4E2G3KA8DN7NSCGT3V4HMN20H3GCX4AS6HSSTG8D3\nAGE-SECRET-KEY--1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59Q`},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot, err := age.ParseIdentities(strings.NewReader(tt.file))\n\t\t\tif (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"ParseIdentities() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif len(got) != tt.wantCount {\n\t\t\t\tt.Errorf(\"ParseIdentities() returned %d identities, want %d\", len(got), tt.wantCount)\n\t\t\t}\n\t\t})\n\t}\n}\n\ntype testRecipient struct {\n\tlabels []string\n}\n\nfunc (testRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\tpanic(\"expected WrapWithLabels instead\")\n}\n\nfunc (t testRecipient) WrapWithLabels(fileKey []byte) (s []*age.Stanza, labels []string, err error) {\n\treturn []*age.Stanza{{Type: \"test\"}}, t.labels, nil\n}\n\nfunc TestLabels(t *testing.T) {\n\tscrypt, err := age.NewScryptRecipient(\"xxx\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ti, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tx25519 := i.Recipient()\n\tpqc := testRecipient{[]string{\"postquantum\"}}\n\tpqcAndFoo := testRecipient{[]string{\"postquantum\", \"foo\"}}\n\tfooAndPQC := testRecipient{[]string{\"foo\", \"postquantum\"}}\n\n\tif _, err := age.Encrypt(io.Discard, scrypt, scrypt); err == nil {\n\t\tt.Error(\"expected two scrypt recipients to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, scrypt, x25519); err == nil {\n\t\tt.Error(\"expected x25519 mixed with scrypt to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, x25519, scrypt); err == nil {\n\t\tt.Error(\"expected x25519 mixed with scrypt to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, pqc, x25519); err == nil {\n\t\tt.Error(\"expected x25519 mixed with pqc to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, x25519, pqc); err == nil {\n\t\tt.Error(\"expected x25519 mixed with pqc to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, pqc, pqc); err != nil {\n\t\tt.Errorf(\"expected two pqc to work, got %v\", err)\n\t}\n\tif _, err := age.Encrypt(io.Discard, pqc); err != nil {\n\t\tt.Errorf(\"expected one pqc to work, got %v\", err)\n\t}\n\tif _, err := age.Encrypt(io.Discard, pqcAndFoo, pqc); err == nil {\n\t\tt.Error(\"expected pqc+foo mixed with pqc to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, pqc, pqcAndFoo); err == nil {\n\t\tt.Error(\"expected pqc+foo mixed with pqc to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, pqc, pqc, pqcAndFoo); err == nil {\n\t\tt.Error(\"expected pqc+foo mixed with pqc to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, pqcAndFoo, pqcAndFoo); err != nil {\n\t\tt.Errorf(\"expected two pqc+foo to work, got %v\", err)\n\t}\n\tif _, err := age.Encrypt(io.Discard, pqcAndFoo, fooAndPQC); err != nil {\n\t\tt.Errorf(\"expected pqc+foo mixed with foo+pqc to work, got %v\", err)\n\t}\n}\n\n// testIdentity is a non-native identity that records if Unwrap is called.\ntype testIdentity struct {\n\tcalled bool\n}\n\nfunc (ti *testIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {\n\tti.called = true\n\treturn nil, age.ErrIncorrectIdentity\n}\n\nfunc TestDecryptNativeIdentitiesFirst(t *testing.T) {\n\tcorrect, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tunrelated, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := age.Encrypt(buf, correct.Recipient())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tnonNative := &testIdentity{}\n\n\t// Pass identities: unrelated native, non-native, correct native.\n\t// Native identities should be tried first, so correct should match\n\t// before nonNative is ever called.\n\t_, err = age.Decrypt(bytes.NewReader(buf.Bytes()), unrelated, nonNative, correct)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif nonNative.called {\n\t\tt.Error(\"non-native identity was called, but native identities should be tried first\")\n\t}\n}\n\ntype stanzaTypeRecipient string\n\nfunc (s stanzaTypeRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\treturn []*age.Stanza{{Type: string(s)}}, nil\n}\n\nfunc TestNoIdentityMatchErrorStanzaTypes(t *testing.T) {\n\ta, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tb, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\twrong, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := age.Encrypt(buf, a.Recipient(), stanzaTypeRecipient(\"other\"), b.Recipient())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := io.WriteString(w, helloWorld); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = age.Decrypt(bytes.NewReader(buf.Bytes()), wrong)\n\tif err == nil {\n\t\tt.Fatal(\"expected decryption to fail\")\n\t}\n\n\tvar noMatch *age.NoIdentityMatchError\n\tif !errors.As(err, &noMatch) {\n\t\tt.Fatalf(\"expected NoIdentityMatchError, got %T: %v\", err, err)\n\t}\n\n\twant := []string{\"X25519\", \"other\", \"X25519\"}\n\tif !slices.Equal(noMatch.StanzaTypes, want) {\n\t\tt.Errorf(\"StanzaTypes = %v, want %v\", noMatch.StanzaTypes, want)\n\t}\n}\n\nfunc TestScryptIdentityErrors(t *testing.T) {\n\tt.Run(\"not passphrase-encrypted\", func(t *testing.T) {\n\t\ti, err := age.GenerateX25519Identity()\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tbuf := &bytes.Buffer{}\n\t\tw, err := age.Encrypt(buf, i.Recipient())\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif err := w.Close(); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tscryptID, err := age.NewScryptIdentity(\"password\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\t_, err = age.Decrypt(bytes.NewReader(buf.Bytes()), scryptID)\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected decryption to fail\")\n\t\t}\n\t\tif !errors.Is(err, age.ErrIncorrectIdentity) {\n\t\t\tt.Errorf(\"expected ErrIncorrectIdentity, got %v\", err)\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"not passphrase-encrypted\") {\n\t\t\tt.Errorf(\"expected error to mention 'not passphrase-encrypted', got %v\", err)\n\t\t}\n\t})\n\n\tt.Run(\"incorrect passphrase\", func(t *testing.T) {\n\t\tr, err := age.NewScryptRecipient(\"correct-password\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tr.SetWorkFactor(10) // Low for fast test\n\n\t\tbuf := &bytes.Buffer{}\n\t\tw, err := age.Encrypt(buf, r)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif err := w.Close(); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tscryptID, err := age.NewScryptIdentity(\"wrong-password\")\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\t_, err = age.Decrypt(bytes.NewReader(buf.Bytes()), scryptID)\n\t\tif err == nil {\n\t\t\tt.Fatal(\"expected decryption to fail\")\n\t\t}\n\t\tif !errors.Is(err, age.ErrIncorrectIdentity) {\n\t\t\tt.Errorf(\"expected ErrIncorrectIdentity, got %v\", err)\n\t\t}\n\t\tif !strings.Contains(err.Error(), \"incorrect passphrase\") {\n\t\t\tt.Errorf(\"expected error to mention 'incorrect passphrase', got %v\", err)\n\t\t}\n\t})\n}\n\nfunc TestDetachedHeader(t *testing.T) {\n\ti, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := age.Encrypt(buf, i.Recipient())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := io.WriteString(w, helloWorld); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tencrypted := buf.Bytes()\n\n\theader, err := age.ExtractHeader(bytes.NewReader(encrypted))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tfileKey, err := age.DecryptHeader(header, i)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tidentity := age.NewInjectedFileKeyIdentity(fileKey)\n\tout, err := age.Decrypt(bytes.NewReader(encrypted), identity)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\toutBytes, err := io.ReadAll(out)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(outBytes) != helloWorld {\n\t\tt.Errorf(\"wrong data: %q, expected %q\", outBytes, helloWorld)\n\t}\n}\n\nfunc TestEncryptReader(t *testing.T) {\n\ta, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tr, err := age.EncryptReader(strings.NewReader(helloWorld), a.Recipient())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tbuf := &bytes.Buffer{}\n\tif _, err := io.Copy(buf, r); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tout, err := age.Decrypt(buf, a)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\toutBytes, err := io.ReadAll(out)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif string(outBytes) != helloWorld {\n\t\tt.Errorf(\"wrong data: %q, excepted %q\", outBytes, helloWorld)\n\t}\n}\n"
  },
  {
    "path": "agessh/agessh.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package agessh provides age.Identity and age.Recipient implementations of\n// types \"ssh-rsa\" and \"ssh-ed25519\", which allow reusing existing SSH keys for\n// encryption with age-encryption.org/v1.\n//\n// These recipient types should only be used for compatibility with existing\n// keys, and native keys should be preferred otherwise.\n//\n// Note that these recipient types are not anonymous: the encrypted message will\n// include a short 32-bit ID of the public key.\npackage agessh\n\nimport (\n\t\"crypto/ed25519\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/internal/format\"\n\t\"filippo.io/edwards25519\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n\t\"golang.org/x/crypto/curve25519\"\n\t\"golang.org/x/crypto/hkdf\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc sshFingerprint(pk ssh.PublicKey) string {\n\th := sha256.Sum256(pk.Marshal())\n\treturn format.EncodeToString(h[:4])\n}\n\nconst oaepLabel = \"age-encryption.org/v1/ssh-rsa\"\n\ntype RSARecipient struct {\n\tsshKey ssh.PublicKey\n\tpubKey *rsa.PublicKey\n}\n\nvar _ age.Recipient = &RSARecipient{}\n\nfunc NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {\n\tif pk.Type() != \"ssh-rsa\" {\n\t\treturn nil, errors.New(\"SSH public key is not an RSA key\")\n\t}\n\tr := &RSARecipient{\n\t\tsshKey: pk,\n\t}\n\n\tif pk, ok := pk.(ssh.CryptoPublicKey); ok {\n\t\tif pk, ok := pk.CryptoPublicKey().(*rsa.PublicKey); ok {\n\t\t\tr.pubKey = pk\n\t\t} else {\n\t\t\treturn nil, errors.New(\"unexpected public key type\")\n\t\t}\n\t} else {\n\t\treturn nil, errors.New(\"pk does not implement ssh.CryptoPublicKey\")\n\t}\n\tif r.pubKey.Size() < 2048/8 {\n\t\treturn nil, errors.New(\"RSA key size is too small\")\n\t}\n\treturn r, nil\n}\n\nfunc (r *RSARecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\tl := &age.Stanza{\n\t\tType: \"ssh-rsa\",\n\t\tArgs: []string{sshFingerprint(r.sshKey)},\n\t}\n\n\twrappedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader,\n\t\tr.pubKey, fileKey, []byte(oaepLabel))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tl.Body = wrappedKey\n\n\treturn []*age.Stanza{l}, nil\n}\n\ntype RSAIdentity struct {\n\tk      *rsa.PrivateKey\n\tsshKey ssh.PublicKey\n}\n\nvar _ age.Identity = &RSAIdentity{}\n\nfunc NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {\n\ts, err := ssh.NewSignerFromKey(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ti := &RSAIdentity{\n\t\tk: key, sshKey: s.PublicKey(),\n\t}\n\treturn i, nil\n}\n\nfunc (i *RSAIdentity) Recipient() *RSARecipient {\n\treturn &RSARecipient{\n\t\tsshKey: i.sshKey,\n\t\tpubKey: &i.k.PublicKey,\n\t}\n}\n\nfunc (i *RSAIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {\n\treturn multiUnwrap(i.unwrap, stanzas)\n}\n\nfunc (i *RSAIdentity) unwrap(block *age.Stanza) ([]byte, error) {\n\tif block.Type != \"ssh-rsa\" {\n\t\treturn nil, age.ErrIncorrectIdentity\n\t}\n\tif len(block.Args) != 1 {\n\t\treturn nil, errors.New(\"invalid ssh-rsa recipient block\")\n\t}\n\n\tif block.Args[0] != sshFingerprint(i.sshKey) {\n\t\treturn nil, age.ErrIncorrectIdentity\n\t}\n\n\tfileKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, i.k,\n\t\tblock.Body, []byte(oaepLabel))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decrypt file key: %v\", err)\n\t}\n\treturn fileKey, nil\n}\n\ntype Ed25519Recipient struct {\n\tsshKey         ssh.PublicKey\n\ttheirPublicKey []byte\n}\n\nvar _ age.Recipient = &Ed25519Recipient{}\n\nfunc NewEd25519Recipient(pk ssh.PublicKey) (*Ed25519Recipient, error) {\n\tif pk.Type() != \"ssh-ed25519\" {\n\t\treturn nil, errors.New(\"SSH public key is not an Ed25519 key\")\n\t}\n\n\tcpk, ok := pk.(ssh.CryptoPublicKey)\n\tif !ok {\n\t\treturn nil, errors.New(\"pk does not implement ssh.CryptoPublicKey\")\n\t}\n\tepk, ok := cpk.CryptoPublicKey().(ed25519.PublicKey)\n\tif !ok {\n\t\treturn nil, errors.New(\"unexpected public key type\")\n\t}\n\tmpk, err := ed25519PublicKeyToCurve25519(epk)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid Ed25519 public key: %v\", err)\n\t}\n\n\treturn &Ed25519Recipient{\n\t\tsshKey:         pk,\n\t\ttheirPublicKey: mpk,\n\t}, nil\n}\n\nfunc ParseRecipient(s string) (age.Recipient, error) {\n\tpubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed SSH recipient: %q: %v\", s, err)\n\t}\n\n\tvar r age.Recipient\n\tswitch t := pubKey.Type(); t {\n\tcase \"ssh-rsa\":\n\t\tr, err = NewRSARecipient(pubKey)\n\tcase \"ssh-ed25519\":\n\t\tr, err = NewEd25519Recipient(pubKey)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown SSH recipient type: %q\", t)\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed SSH recipient: %q: %v\", s, err)\n\t}\n\n\treturn r, nil\n}\n\nfunc ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) {\n\t// See https://blog.filippo.io/using-ed25519-keys-for-encryption and\n\t// https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery.\n\tp, err := new(edwards25519.Point).SetBytes(pk)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn p.BytesMontgomery(), nil\n}\n\nconst ed25519Label = \"age-encryption.org/v1/ssh-ed25519\"\n\nfunc (r *Ed25519Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\tephemeral := make([]byte, curve25519.ScalarSize)\n\tif _, err := rand.Read(ephemeral); err != nil {\n\t\treturn nil, err\n\t}\n\tourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttweak := make([]byte, curve25519.ScalarSize)\n\ttH := hkdf.New(sha256.New, nil, r.sshKey.Marshal(), []byte(ed25519Label))\n\tif _, err := io.ReadFull(tH, tweak); err != nil {\n\t\treturn nil, err\n\t}\n\tsharedSecret, _ = curve25519.X25519(tweak, sharedSecret)\n\n\tl := &age.Stanza{\n\t\tType: \"ssh-ed25519\",\n\t\tArgs: []string{sshFingerprint(r.sshKey),\n\t\t\tformat.EncodeToString(ourPublicKey[:])},\n\t}\n\n\tsalt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey))\n\tsalt = append(salt, ourPublicKey...)\n\tsalt = append(salt, r.theirPublicKey...)\n\th := hkdf.New(sha256.New, sharedSecret, salt, []byte(ed25519Label))\n\twrappingKey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := io.ReadFull(h, wrappingKey); err != nil {\n\t\treturn nil, err\n\t}\n\n\twrappedKey, err := aeadEncrypt(wrappingKey, fileKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tl.Body = wrappedKey\n\n\treturn []*age.Stanza{l}, nil\n}\n\ntype Ed25519Identity struct {\n\tsecretKey, ourPublicKey []byte\n\tsshKey                  ssh.PublicKey\n}\n\nvar _ age.Identity = &Ed25519Identity{}\n\nfunc NewEd25519Identity(key ed25519.PrivateKey) (*Ed25519Identity, error) {\n\ts, err := ssh.NewSignerFromKey(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ti := &Ed25519Identity{\n\t\tsshKey:    s.PublicKey(),\n\t\tsecretKey: ed25519PrivateKeyToCurve25519(key),\n\t}\n\ti.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint)\n\treturn i, nil\n}\n\nfunc ParseIdentity(pemBytes []byte) (age.Identity, error) {\n\tk, err := ssh.ParseRawPrivateKey(pemBytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch k := k.(type) {\n\tcase *ed25519.PrivateKey:\n\t\treturn NewEd25519Identity(*k)\n\t// ParseRawPrivateKey returns inconsistent types. See Issue 429.\n\tcase ed25519.PrivateKey:\n\t\treturn NewEd25519Identity(k)\n\tcase *rsa.PrivateKey:\n\t\treturn NewRSAIdentity(k)\n\t}\n\n\treturn nil, fmt.Errorf(\"unsupported SSH identity type: %T\", k)\n}\n\nfunc ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {\n\th := sha512.New()\n\th.Write(pk.Seed())\n\tout := h.Sum(nil)\n\treturn out[:curve25519.ScalarSize]\n}\n\nfunc (i *Ed25519Identity) Recipient() *Ed25519Recipient {\n\treturn &Ed25519Recipient{\n\t\tsshKey:         i.sshKey,\n\t\ttheirPublicKey: i.ourPublicKey,\n\t}\n}\n\nfunc (i *Ed25519Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {\n\treturn multiUnwrap(i.unwrap, stanzas)\n}\n\nfunc (i *Ed25519Identity) unwrap(block *age.Stanza) ([]byte, error) {\n\tif block.Type != \"ssh-ed25519\" {\n\t\treturn nil, age.ErrIncorrectIdentity\n\t}\n\tif len(block.Args) != 2 {\n\t\treturn nil, errors.New(\"invalid ssh-ed25519 recipient block\")\n\t}\n\tpublicKey, err := format.DecodeString(block.Args[1])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse ssh-ed25519 recipient: %v\", err)\n\t}\n\tif len(publicKey) != curve25519.PointSize {\n\t\treturn nil, errors.New(\"invalid ssh-ed25519 recipient block\")\n\t}\n\n\tif block.Args[0] != sshFingerprint(i.sshKey) {\n\t\treturn nil, age.ErrIncorrectIdentity\n\t}\n\n\tsharedSecret, err := curve25519.X25519(i.secretKey, publicKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid X25519 recipient: %v\", err)\n\t}\n\n\ttweak := make([]byte, curve25519.ScalarSize)\n\ttH := hkdf.New(sha256.New, nil, i.sshKey.Marshal(), []byte(ed25519Label))\n\tif _, err := io.ReadFull(tH, tweak); err != nil {\n\t\treturn nil, err\n\t}\n\tsharedSecret, _ = curve25519.X25519(tweak, sharedSecret)\n\n\tsalt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey))\n\tsalt = append(salt, publicKey...)\n\tsalt = append(salt, i.ourPublicKey...)\n\th := hkdf.New(sha256.New, sharedSecret, salt, []byte(ed25519Label))\n\twrappingKey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := io.ReadFull(h, wrappingKey); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfileKey, err := aeadDecrypt(wrappingKey, block.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decrypt file key: %v\", err)\n\t}\n\treturn fileKey, nil\n}\n\n// multiUnwrap is copied from package age. It's a helper that implements\n// Identity.Unwrap in terms of a function that unwraps a single recipient\n// stanza.\nfunc multiUnwrap(unwrap func(*age.Stanza) ([]byte, error), stanzas []*age.Stanza) ([]byte, error) {\n\tfor _, s := range stanzas {\n\t\tfileKey, err := unwrap(s)\n\t\tif errors.Is(err, age.ErrIncorrectIdentity) {\n\t\t\t// If we ever start returning something interesting wrapping\n\t\t\t// ErrIncorrectIdentity, we should let it make its way up through\n\t\t\t// Decrypt into NoIdentityMatchError.Errors.\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn fileKey, nil\n\t}\n\treturn nil, age.ErrIncorrectIdentity\n}\n\n// aeadEncrypt and aeadDecrypt are copied from package age.\n//\n// They don't limit the file key size because multi-key attacks are irrelevant\n// against the ssh-ed25519 recipient. Being an asymmetric recipient, it would\n// only allow a more efficient search for accepted public keys against a\n// decryption oracle, but the ssh-X recipients are not anonymous (they have a\n// short recipient hash).\n\nfunc aeadEncrypt(key, plaintext []byte) ([]byte, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnonce := make([]byte, chacha20poly1305.NonceSize)\n\treturn aead.Seal(nil, nonce, plaintext, nil), nil\n}\n\nfunc aeadDecrypt(key, ciphertext []byte) ([]byte, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnonce := make([]byte, chacha20poly1305.NonceSize)\n\treturn aead.Open(nil, nonce, ciphertext, nil)\n}\n"
  },
  {
    "path": "agessh/agessh_test.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage agessh_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/ed25519\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"filippo.io/age/agessh\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\nfunc TestSSHRSARoundTrip(t *testing.T) {\n\tpk, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpub, err := ssh.NewPublicKey(&pk.PublicKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tr, err := agessh.NewRSARecipient(pub)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ti, err := agessh.NewRSAIdentity(pk)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// TODO: replace this with (and go-diff) with go-cmp.\n\tif !reflect.DeepEqual(r, i.Recipient()) {\n\t\tt.Fatalf(\"i.Recipient is different from r\")\n\t}\n\n\tfileKey := make([]byte, 16)\n\tif _, err := rand.Read(fileKey); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tstanzas, err := r.Wrap(fileKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tout, err := i.Unwrap(stanzas)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !bytes.Equal(fileKey, out) {\n\t\tt.Errorf(\"invalid output: %x, expected %x\", out, fileKey)\n\t}\n}\n\nfunc TestSSHEd25519RoundTrip(t *testing.T) {\n\tpub, priv, err := ed25519.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsshPubKey, err := ssh.NewPublicKey(pub)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tr, err := agessh.NewEd25519Recipient(sshPubKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ti, err := agessh.NewEd25519Identity(priv)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// TODO: replace this with (and go-diff) with go-cmp.\n\tif !reflect.DeepEqual(r, i.Recipient()) {\n\t\tt.Fatalf(\"i.Recipient is different from r\")\n\t}\n\n\tfileKey := make([]byte, 16)\n\tif _, err := rand.Read(fileKey); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tstanzas, err := r.Wrap(fileKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tout, err := i.Unwrap(stanzas)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !bytes.Equal(fileKey, out) {\n\t\tt.Errorf(\"invalid output: %x, expected %x\", out, fileKey)\n\t}\n}\n"
  },
  {
    "path": "agessh/encrypted_keys.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage agessh\n\nimport (\n\t\"crypto\"\n\t\"crypto/ed25519\"\n\t\"crypto/rsa\"\n\t\"fmt\"\n\n\t\"filippo.io/age\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\n// EncryptedSSHIdentity is an age.Identity implementation based on a passphrase\n// encrypted SSH private key.\n//\n// It requests the passphrase only if the public key matches a recipient stanza.\n// If the application knows it will always have to decrypt the private key, it\n// would be simpler to use ssh.ParseRawPrivateKeyWithPassphrase directly and\n// pass the result to NewEd25519Identity or NewRSAIdentity.\ntype EncryptedSSHIdentity struct {\n\tpubKey     ssh.PublicKey\n\trecipient  age.Recipient\n\tpemBytes   []byte\n\tpassphrase func() ([]byte, error)\n\n\tdecrypted age.Identity\n}\n\n// NewEncryptedSSHIdentity returns a new EncryptedSSHIdentity.\n//\n// pubKey must be the public key associated with the encrypted private key, and\n// it must have type \"ssh-ed25519\" or \"ssh-rsa\". For OpenSSH encrypted files it\n// can be extracted from an ssh.PassphraseMissingError, otherwise it can often\n// be found in \".pub\" files.\n//\n// pemBytes must be a valid input to ssh.ParseRawPrivateKeyWithPassphrase.\n// passphrase is a callback that will be invoked by Unwrap when the passphrase\n// is necessary.\nfunc NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) {\n\ti := &EncryptedSSHIdentity{\n\t\tpubKey:     pubKey,\n\t\tpemBytes:   pemBytes,\n\t\tpassphrase: passphrase,\n\t}\n\tswitch t := pubKey.Type(); t {\n\tcase \"ssh-ed25519\":\n\t\tr, err := NewEd25519Recipient(pubKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ti.recipient = r\n\tcase \"ssh-rsa\":\n\t\tr, err := NewRSARecipient(pubKey)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ti.recipient = r\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unsupported SSH key type: %v\", t)\n\t}\n\treturn i, nil\n}\n\nvar _ age.Identity = &EncryptedSSHIdentity{}\n\nfunc (i *EncryptedSSHIdentity) Recipient() age.Recipient {\n\treturn i.recipient\n}\n\n// Unwrap implements age.Identity. If the private key is still encrypted, and\n// any of the stanzas match the public key, it will request the passphrase. The\n// decrypted private key will be cached after the first successful invocation.\nfunc (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {\n\tif i.decrypted != nil {\n\t\treturn i.decrypted.Unwrap(stanzas)\n\t}\n\n\tvar match bool\n\tfor _, s := range stanzas {\n\t\tif s.Type != i.pubKey.Type() {\n\t\t\tcontinue\n\t\t}\n\t\tif len(s.Args) < 1 {\n\t\t\treturn nil, fmt.Errorf(\"invalid %v recipient block\", i.pubKey.Type())\n\t\t}\n\t\tif s.Args[0] != sshFingerprint(i.pubKey) {\n\t\t\tcontinue\n\t\t}\n\t\tmatch = true\n\t\tbreak\n\t}\n\tif !match {\n\t\treturn nil, age.ErrIncorrectIdentity\n\t}\n\n\tpassphrase, err := i.passphrase()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to obtain passphrase: %v\", err)\n\t}\n\tk, err := ssh.ParseRawPrivateKeyWithPassphrase(i.pemBytes, passphrase)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decrypt SSH key file: %v\", err)\n\t}\n\n\tvar pubKey interface {\n\t\tEqual(x crypto.PublicKey) bool\n\t}\n\tswitch k := k.(type) {\n\tcase *ed25519.PrivateKey:\n\t\ti.decrypted, err = NewEd25519Identity(*k)\n\t\tpubKey = k.Public().(ed25519.PublicKey)\n\t// ParseRawPrivateKey returns inconsistent types. See Issue 429.\n\tcase ed25519.PrivateKey:\n\t\ti.decrypted, err = NewEd25519Identity(k)\n\t\tpubKey = k.Public().(ed25519.PublicKey)\n\tcase *rsa.PrivateKey:\n\t\ti.decrypted, err = NewRSAIdentity(k)\n\t\tpubKey = &k.PublicKey\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unexpected SSH key type: %T\", k)\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid SSH key: %v\", err)\n\t}\n\n\tif exp := i.pubKey.(ssh.CryptoPublicKey).CryptoPublicKey(); !pubKey.Equal(exp) {\n\t\treturn nil, fmt.Errorf(\"mismatched private and public SSH key\")\n\t}\n\n\treturn i.decrypted.Unwrap(stanzas)\n}\n"
  },
  {
    "path": "armor/armor.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package armor provides a strict, streaming implementation of the ASCII\n// armoring format for age files.\n//\n// It's PEM with type \"AGE ENCRYPTED FILE\", 64 character columns, no headers,\n// and strict base64 decoding.\npackage armor\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"filippo.io/age/internal/format\"\n)\n\nconst (\n\tHeader = \"-----BEGIN AGE ENCRYPTED FILE-----\"\n\tFooter = \"-----END AGE ENCRYPTED FILE-----\"\n)\n\ntype armoredWriter struct {\n\tstarted, closed bool\n\tencoder         *format.WrappedBase64Encoder\n\tdst             io.Writer\n}\n\nfunc (a *armoredWriter) Write(p []byte) (int, error) {\n\tif !a.started {\n\t\tif _, err := io.WriteString(a.dst, Header+\"\\n\"); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\ta.started = true\n\treturn a.encoder.Write(p)\n}\n\nfunc (a *armoredWriter) Close() error {\n\tif a.closed {\n\t\treturn errors.New(\"ArmoredWriter already closed\")\n\t}\n\ta.closed = true\n\tif err := a.encoder.Close(); err != nil {\n\t\treturn err\n\t}\n\tfooter := Footer + \"\\n\"\n\tif !a.encoder.LastLineIsEmpty() {\n\t\tfooter = \"\\n\" + footer\n\t}\n\t_, err := io.WriteString(a.dst, footer)\n\treturn err\n}\n\nfunc NewWriter(dst io.Writer) io.WriteCloser {\n\t// TODO: write a test with aligned and misaligned sizes, and 8 and 10 steps.\n\treturn &armoredWriter{\n\t\tdst:     dst,\n\t\tencoder: format.NewWrappedBase64Encoder(base64.StdEncoding, dst),\n\t}\n}\n\ntype armoredReader struct {\n\tr       *bufio.Reader\n\tstarted bool\n\tunread  []byte // backed by buf\n\tbuf     [format.BytesPerLine]byte\n\terr     error\n}\n\nfunc NewReader(r io.Reader) io.Reader {\n\treturn &armoredReader{r: bufio.NewReader(r)}\n}\n\nfunc (r *armoredReader) Read(p []byte) (int, error) {\n\tif len(r.unread) > 0 {\n\t\tn := copy(p, r.unread)\n\t\tr.unread = r.unread[n:]\n\t\treturn n, nil\n\t}\n\tif r.err != nil {\n\t\treturn 0, r.err\n\t}\n\n\tgetLine := func() ([]byte, error) {\n\t\tline, err := r.r.ReadBytes('\\n')\n\t\tif err == io.EOF && len(line) == 0 {\n\t\t\treturn nil, io.ErrUnexpectedEOF\n\t\t} else if err != nil && err != io.EOF {\n\t\t\treturn nil, err\n\t\t}\n\t\tline = bytes.TrimSuffix(line, []byte(\"\\n\"))\n\t\tline = bytes.TrimSuffix(line, []byte(\"\\r\"))\n\t\treturn line, nil\n\t}\n\n\tconst maxWhitespace = 1024\n\tdrainTrailing := func() error {\n\t\tbuf, err := io.ReadAll(io.LimitReader(r.r, maxWhitespace))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif len(bytes.TrimSpace(buf)) != 0 {\n\t\t\treturn errors.New(\"trailing data after armored file\")\n\t\t}\n\t\tif len(buf) == maxWhitespace {\n\t\t\treturn errors.New(\"too much trailing whitespace\")\n\t\t}\n\t\treturn io.EOF\n\t}\n\n\tvar removedWhitespace int\n\tfor !r.started {\n\t\tline, err := getLine()\n\t\tif err != nil {\n\t\t\treturn 0, r.setErr(err)\n\t\t}\n\t\t// Ignore leading whitespace.\n\t\tif len(bytes.TrimSpace(line)) == 0 {\n\t\t\tremovedWhitespace += len(line) + 1\n\t\t\tif removedWhitespace > maxWhitespace {\n\t\t\t\treturn 0, r.setErr(errors.New(\"too much leading whitespace\"))\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif string(line) != Header {\n\t\t\treturn 0, r.setErr(fmt.Errorf(\"invalid first line: %q\", line))\n\t\t}\n\t\tr.started = true\n\t}\n\tline, err := getLine()\n\tif err != nil {\n\t\treturn 0, r.setErr(err)\n\t}\n\tif string(line) == Footer {\n\t\treturn 0, r.setErr(drainTrailing())\n\t}\n\tif len(line) == 0 {\n\t\treturn 0, r.setErr(errors.New(\"empty line in armored data\"))\n\t}\n\tif len(line) > format.ColumnsPerLine {\n\t\treturn 0, r.setErr(errors.New(\"column limit exceeded\"))\n\t}\n\tr.unread = r.buf[:]\n\tn, err := base64.StdEncoding.Strict().Decode(r.unread, line)\n\tif err != nil {\n\t\treturn 0, r.setErr(err)\n\t}\n\tr.unread = r.unread[:n]\n\n\tif n < format.BytesPerLine {\n\t\tline, err := getLine()\n\t\tif err != nil {\n\t\t\treturn 0, r.setErr(err)\n\t\t}\n\t\tif string(line) != Footer {\n\t\t\treturn 0, r.setErr(fmt.Errorf(\"invalid closing line: %q\", line))\n\t\t}\n\t\tr.setErr(drainTrailing())\n\t}\n\n\tnn := copy(p, r.unread)\n\tr.unread = r.unread[nn:]\n\treturn nn, nil\n}\n\ntype Error struct {\n\terr error\n}\n\nfunc (e *Error) Error() string {\n\treturn \"invalid armor: \" + e.err.Error()\n}\n\nfunc (e *Error) Unwrap() error {\n\treturn e.err\n}\n\nfunc (r *armoredReader) setErr(err error) error {\n\tif err != io.EOF {\n\t\terr = &Error{err}\n\t}\n\tr.err = err\n\treturn err\n}\n"
  },
  {
    "path": "armor/armor_test.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build go1.18\n\npackage armor_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"encoding/pem\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/armor\"\n\t\"filippo.io/age/internal/format\"\n)\n\nfunc ExampleNewWriter() {\n\tpublicKey := \"age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm\"\n\trecipient, err := age.ParseX25519Recipient(publicKey)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to parse public key %q: %v\", publicKey, err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tarmorWriter := armor.NewWriter(buf)\n\n\tw, err := age.Encrypt(armorWriter, recipient)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create encrypted file: %v\", err)\n\t}\n\tif _, err := io.WriteString(w, \"Black lives matter.\"); err != nil {\n\t\tlog.Fatalf(\"Failed to write to encrypted file: %v\", err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tlog.Fatalf(\"Failed to close encrypted file: %v\", err)\n\t}\n\n\tif err := armorWriter.Close(); err != nil {\n\t\tlog.Fatalf(\"Failed to close armor: %v\", err)\n\t}\n\n\tfmt.Printf(\"%s[...]\", buf.Bytes()[:35])\n\t// Output:\n\t// -----BEGIN AGE ENCRYPTED FILE-----\n\t// [...]\n}\n\nvar privateKey = \"AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU\"\n\nfunc ExampleNewReader() {\n\tfileContents := `-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4YWdhZHZ0WG1PZldDT1hD\nK3RPRzFkUlJnWlFBQlUwemtjeXFRMFp6V1VFCnRzZFV3a3Vkd1dSUWw2eEtrRkVv\nSHcvZnp6Q3lqLy9HMkM4ZjUyUGdDZjQKLS0tIDlpVUpuVUQ5YUJyUENFZ0lNSTB2\nekUvS3E5WjVUN0F5ZWR1ejhpeU5rZUUKsvPGYt7vf0o1kyJ1eVFMz1e4JnYYk1y1\nkB/RRusYjn+KVJ+KTioxj0THtzZPXcjFKuQ1\n-----END AGE ENCRYPTED FILE-----`\n\n\t// DO NOT hardcode the private key. Store it in a secret storage solution,\n\t// on disk if the local machine is trusted, or have the user provide it.\n\tidentity, err := age.ParseX25519Identity(privateKey)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to parse private key %q: %v\", privateKey, err)\n\t}\n\n\tout := &bytes.Buffer{}\n\tf := strings.NewReader(fileContents)\n\tarmorReader := armor.NewReader(f)\n\n\tr, err := age.Decrypt(armorReader, identity)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to open encrypted file: %v\", err)\n\t}\n\tif _, err := io.Copy(out, r); err != nil {\n\t\tlog.Fatalf(\"Failed to read encrypted file: %v\", err)\n\t}\n\n\tfmt.Printf(\"File contents: %q\\n\", out.Bytes())\n\t// Output:\n\t// File contents: \"Black lives matter.\"\n}\n\nfunc TestArmor(t *testing.T) {\n\tt.Run(\"PartialLine\", func(t *testing.T) { testArmor(t, 611) })\n\tt.Run(\"FullLine\", func(t *testing.T) { testArmor(t, 10*format.BytesPerLine) })\n}\n\nfunc testArmor(t *testing.T, size int) {\n\tbuf := &bytes.Buffer{}\n\tw := armor.NewWriter(buf)\n\tplain := make([]byte, size)\n\trand.Read(plain)\n\tif _, err := w.Write(plain); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tblock, _ := pem.Decode(buf.Bytes())\n\tif block == nil {\n\t\tt.Fatal(\"PEM decoding failed\")\n\t}\n\tif len(block.Headers) != 0 {\n\t\tt.Error(\"unexpected headers\")\n\t}\n\tif block.Type != \"AGE ENCRYPTED FILE\" {\n\t\tt.Errorf(\"unexpected type %q\", block.Type)\n\t}\n\tif !bytes.Equal(block.Bytes, plain) {\n\t\tt.Error(\"PEM decoded value doesn't match\")\n\t}\n\tif !bytes.Equal(buf.Bytes(), pem.EncodeToMemory(block)) {\n\t\tt.Error(\"PEM re-encoded value doesn't match\")\n\t}\n\n\tr := armor.NewReader(buf)\n\tout, err := io.ReadAll(r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif !bytes.Equal(out, plain) {\n\t\tt.Error(\"decoded value doesn't match\")\n\t}\n}\n\nfunc FuzzMalleability(f *testing.F) {\n\ttests, err := filepath.Glob(\"../testdata/testkit/*\")\n\tif err != nil {\n\t\tf.Fatal(err)\n\t}\n\tfor _, test := range tests {\n\t\tcontents, err := os.ReadFile(test)\n\t\tif err != nil {\n\t\t\tf.Fatal(err)\n\t\t}\n\t\theader, contents, ok := bytes.Cut(contents, []byte(\"\\n\\n\"))\n\t\tif !ok {\n\t\t\tf.Fatal(\"testkit file without header\")\n\t\t}\n\t\tif bytes.Contains(header, []byte(\"armored: yes\")) {\n\t\t\tf.Add(contents)\n\t\t}\n\t}\n\tf.Fuzz(func(t *testing.T, data []byte) {\n\t\tr := armor.NewReader(bytes.NewReader(data))\n\t\tcontent, err := io.ReadAll(r)\n\t\tif err != nil {\n\t\t\tif _, ok := err.(*armor.Error); !ok {\n\t\t\t\tt.Errorf(\"error type is %T: %v\", err, err)\n\t\t\t}\n\t\t\tt.Skip()\n\t\t}\n\t\tbuf := &bytes.Buffer{}\n\t\tw := armor.NewWriter(buf)\n\t\tif _, err := w.Write(content); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif err := w.Close(); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !bytes.Equal(normalize(buf.Bytes()), normalize(data)) {\n\t\t\tt.Error(\"re-encoded output different from input\")\n\t\t}\n\t})\n}\n\nfunc normalize(f []byte) []byte {\n\tf = bytes.TrimSpace(f)\n\tf = bytes.Replace(f, []byte(\"\\r\\n\"), []byte(\"\\n\"), -1)\n\treturn f\n}\n"
  },
  {
    "path": "cmd/age/age.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"iter\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/agessh\"\n\t\"filippo.io/age/armor\"\n\t\"filippo.io/age/internal/term\"\n\t\"filippo.io/age/plugin\"\n)\n\nconst usage = `Usage:\n    age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]\n    age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]\n    age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]\n\nOptions:\n    -e, --encrypt               Encrypt the input to the output. Default if omitted.\n    -d, --decrypt               Decrypt the input to the output.\n    -o, --output OUTPUT         Write the result to the file at path OUTPUT.\n    -a, --armor                 Encrypt to a PEM encoded format.\n    -p, --passphrase            Encrypt with a passphrase.\n    -r, --recipient RECIPIENT   Encrypt to the specified RECIPIENT. Can be repeated.\n    -R, --recipients-file PATH  Encrypt to recipients listed at PATH. Can be repeated.\n    -i, --identity PATH         Use the identity file at PATH. Can be repeated.\n\nINPUT defaults to standard input, and OUTPUT defaults to standard output.\nIf OUTPUT exists, it will be overwritten.\n\nRECIPIENT can be an age public key generated by age-keygen (\"age1...\")\nor an SSH public key (\"ssh-ed25519 AAAA...\", \"ssh-rsa AAAA...\").\n\nRecipient files contain one or more recipients, one per line. Empty lines\nand lines starting with \"#\" are ignored as comments. \"-\" may be used to\nread recipients from standard input.\n\nIdentity files contain one or more secret keys (\"AGE-SECRET-KEY-1...\"),\none per line, or an SSH key. Empty lines and lines starting with \"#\" are\nignored as comments. Passphrase encrypted age files can be used as\nidentity files. Multiple key files can be provided, and any unused ones\nwill be ignored. \"-\" may be used to read identities from standard input.\n\nWhen --encrypt is specified explicitly, -i can also be used to encrypt to an\nidentity file symmetrically, instead or in addition to normal recipients.\n\nExample:\n    $ age-keygen -o key.txt\n    Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p\n    $ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age\n    $ age --decrypt -i key.txt -o data.tar.gz data.tar.gz.age`\n\n// stdinInUse is used to ensure only one of input, recipients, or identities\n// file is read from stdin. It's a singleton like os.Stdin.\nvar stdinInUse bool\n\ntype multiFlag []string\n\nfunc (f *multiFlag) String() string { return fmt.Sprint(*f) }\n\nfunc (f *multiFlag) Set(value string) error {\n\t*f = append(*f, value)\n\treturn nil\n}\n\ntype identityFlag struct {\n\tType, Value string\n}\n\n// identityFlags tracks -i and -j flags, preserving their relative order, so\n// that \"age -d -j agent -i encrypted-fallback-keys.age\" behaves as expected.\ntype identityFlags []identityFlag\n\nfunc (f *identityFlags) addIdentityFlag(value string) error {\n\t*f = append(*f, identityFlag{Type: \"i\", Value: value})\n\treturn nil\n}\n\nfunc (f *identityFlags) addPluginFlag(value string) error {\n\t*f = append(*f, identityFlag{Type: \"j\", Value: value})\n\treturn nil\n}\n\n// Version can be set at link time to override debug.BuildInfo.Main.Version when\n// building manually without git history. It should look like \"v1.2.3\".\nvar Version string\n\nfunc main() {\n\tflag.Usage = func() { fmt.Fprintf(os.Stderr, \"%s\\n\", usage) }\n\n\tif len(os.Args) == 1 {\n\t\tflag.Usage()\n\t\tos.Exit(1)\n\t}\n\n\tvar (\n\t\toutFlag                          string\n\t\tdecryptFlag, encryptFlag         bool\n\t\tpassFlag, versionFlag, armorFlag bool\n\t\trecipientFlags                   multiFlag\n\t\trecipientsFileFlags              multiFlag\n\t\tidentityFlags                    identityFlags\n\t)\n\n\tflag.BoolVar(&versionFlag, \"version\", false, \"print the version\")\n\tflag.BoolVar(&decryptFlag, \"d\", false, \"decrypt the input\")\n\tflag.BoolVar(&decryptFlag, \"decrypt\", false, \"decrypt the input\")\n\tflag.BoolVar(&encryptFlag, \"e\", false, \"encrypt the input\")\n\tflag.BoolVar(&encryptFlag, \"encrypt\", false, \"encrypt the input\")\n\tflag.BoolVar(&passFlag, \"p\", false, \"use a passphrase\")\n\tflag.BoolVar(&passFlag, \"passphrase\", false, \"use a passphrase\")\n\tflag.StringVar(&outFlag, \"o\", \"\", \"output to `FILE` (default stdout)\")\n\tflag.StringVar(&outFlag, \"output\", \"\", \"output to `FILE` (default stdout)\")\n\tflag.BoolVar(&armorFlag, \"a\", false, \"generate an armored file\")\n\tflag.BoolVar(&armorFlag, \"armor\", false, \"generate an armored file\")\n\tflag.Var(&recipientFlags, \"r\", \"recipient (can be repeated)\")\n\tflag.Var(&recipientFlags, \"recipient\", \"recipient (can be repeated)\")\n\tflag.Var(&recipientsFileFlags, \"R\", \"recipients file (can be repeated)\")\n\tflag.Var(&recipientsFileFlags, \"recipients-file\", \"recipients file (can be repeated)\")\n\tflag.Func(\"i\", \"identity (can be repeated)\", identityFlags.addIdentityFlag)\n\tflag.Func(\"identity\", \"identity (can be repeated)\", identityFlags.addIdentityFlag)\n\tflag.Func(\"j\", \"data-less plugin (can be repeated)\", identityFlags.addPluginFlag)\n\tflag.Parse()\n\n\tif versionFlag {\n\t\tif buildInfo, ok := debug.ReadBuildInfo(); ok && Version == \"\" {\n\t\t\tVersion = buildInfo.Main.Version\n\t\t}\n\t\tfmt.Println(Version)\n\t\treturn\n\t}\n\n\tif flag.NArg() > 1 {\n\t\tvar hints []string\n\t\tquotedArgs := strings.Trim(fmt.Sprintf(\"%q\", flag.Args()), \"[]\")\n\n\t\t// If the second argument looks like a flag, suggest moving the first\n\t\t// argument to the back (as long as the arguments don't need quoting).\n\t\tif strings.HasPrefix(flag.Arg(1), \"-\") {\n\t\t\thints = append(hints, \"the input file must be specified after all flags\")\n\n\t\t\tsafe := true\n\t\t\tunsafeShell := regexp.MustCompile(`[^\\w@%+=:,./-]`)\n\t\t\tif slices.ContainsFunc(os.Args, unsafeShell.MatchString) {\n\t\t\t\tsafe = false\n\t\t\t}\n\t\t\tif safe {\n\t\t\t\ti := len(os.Args) - flag.NArg()\n\t\t\t\tnewArgs := append([]string{}, os.Args[:i]...)\n\t\t\t\tnewArgs = append(newArgs, os.Args[i+1:]...)\n\t\t\t\tnewArgs = append(newArgs, os.Args[i])\n\t\t\t\thints = append(hints, \"did you mean:\")\n\t\t\t\thints = append(hints, \"    \"+strings.Join(newArgs, \" \"))\n\t\t\t}\n\t\t} else {\n\t\t\thints = append(hints, \"only a single input file may be specified at a time\")\n\t\t}\n\n\t\terrorWithHint(\"too many INPUT arguments: \"+quotedArgs, hints...)\n\t}\n\n\tswitch {\n\tcase decryptFlag:\n\t\tif encryptFlag {\n\t\t\terrorf(\"-e/--encrypt can't be used with -d/--decrypt\")\n\t\t}\n\t\tif armorFlag {\n\t\t\terrorWithHint(\"-a/--armor can't be used with -d/--decrypt\",\n\t\t\t\t\"note that armored files are detected automatically, try again without -a/--armor\")\n\t\t}\n\t\tif passFlag {\n\t\t\terrorWithHint(\"-p/--passphrase can't be used with -d/--decrypt\",\n\t\t\t\t\"note that password protected files are detected automatically\")\n\t\t}\n\t\tif len(recipientFlags) > 0 {\n\t\t\terrorWithHint(\"-r/--recipient can't be used with -d/--decrypt\",\n\t\t\t\t\"did you mean to use -i/--identity to specify a private key?\")\n\t\t}\n\t\tif len(recipientsFileFlags) > 0 {\n\t\t\terrorWithHint(\"-R/--recipients-file can't be used with -d/--decrypt\",\n\t\t\t\t\"did you mean to use -i/--identity to specify a private key?\")\n\t\t}\n\tdefault: // encrypt\n\t\tif len(identityFlags) > 0 && !encryptFlag {\n\t\t\terrorWithHint(\"-i/--identity and -j can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt\",\n\t\t\t\t\"did you forget to specify -d/--decrypt?\")\n\t\t}\n\t\tif len(recipientFlags)+len(recipientsFileFlags)+len(identityFlags) == 0 && !passFlag {\n\t\t\terrorWithHint(\"missing recipients\",\n\t\t\t\t\"did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?\")\n\t\t}\n\t\tif len(recipientFlags) > 0 && passFlag {\n\t\t\terrorf(\"-p/--passphrase can't be combined with -r/--recipient\")\n\t\t}\n\t\tif len(recipientsFileFlags) > 0 && passFlag {\n\t\t\terrorf(\"-p/--passphrase can't be combined with -R/--recipients-file\")\n\t\t}\n\t\tif len(identityFlags) > 0 && passFlag {\n\t\t\terrorf(\"-p/--passphrase can't be combined with -i/--identity and -j\")\n\t\t}\n\t}\n\n\twarnDuplicates(slices.Values(recipientFlags), \"recipient\")\n\twarnDuplicates(slices.Values(recipientsFileFlags), \"recipients file\")\n\twarnDuplicates(func(yield func(string) bool) {\n\t\tfor _, f := range identityFlags {\n\t\t\tif f.Type == \"i\" && !yield(f.Value) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}, \"identity file\")\n\n\tvar inUseFiles []string\n\tfor _, i := range identityFlags {\n\t\tif i.Type != \"i\" {\n\t\t\tcontinue\n\t\t}\n\t\tinUseFiles = append(inUseFiles, absPath(i.Value))\n\t}\n\tfor _, f := range recipientsFileFlags {\n\t\tinUseFiles = append(inUseFiles, absPath(f))\n\t}\n\n\tvar in io.Reader = os.Stdin\n\tvar out io.Writer = os.Stdout\n\tif name := flag.Arg(0); name != \"\" && name != \"-\" {\n\t\tinUseFiles = append(inUseFiles, absPath(name))\n\t\tf, err := os.Open(name)\n\t\tif err != nil {\n\t\t\terrorf(\"failed to open input file %q: %v\", name, err)\n\t\t}\n\t\tdefer f.Close()\n\t\tin = f\n\t} else {\n\t\tstdinInUse = true\n\t\tif decryptFlag && term.IsTerminal(os.Stdin) {\n\t\t\t// If the input comes from a TTY, assume it's armored, and buffer up\n\t\t\t// to the END line (or EOF/EOT) so that a password prompt or the\n\t\t\t// output don't get in the way of typing the input. See Issue 364.\n\t\t\tbuf, err := bufferTerminalInput(in)\n\t\t\tif err != nil {\n\t\t\t\terrorf(\"failed to buffer terminal input: %v\", err)\n\t\t\t}\n\t\t\tin = buf\n\t\t}\n\t}\n\tif name := outFlag; name != \"\" && name != \"-\" {\n\t\tfor _, f := range inUseFiles {\n\t\t\tif f == absPath(name) {\n\t\t\t\terrorf(\"input and output file are the same: %q\", name)\n\t\t\t}\n\t\t}\n\t\tf := newLazyOpener(name)\n\t\tdefer func() {\n\t\t\tif err := f.Close(); err != nil {\n\t\t\t\terrorf(\"failed to close output file %q: %v\", name, err)\n\t\t\t}\n\t\t}()\n\t\tout = f\n\t} else if term.IsTerminal(os.Stdout) {\n\t\tbuf := &bytes.Buffer{}\n\t\tdefer func() {\n\t\t\tif out == buf {\n\t\t\t\tio.Copy(os.Stdout, buf)\n\t\t\t}\n\t\t}()\n\t\tif name != \"-\" {\n\t\t\tif decryptFlag {\n\t\t\t\t// Buffer the output to check it's printable.\n\t\t\t\tout = buf\n\t\t\t\tdefer func() {\n\t\t\t\t\tif bytes.ContainsFunc(buf.Bytes(), func(r rune) bool {\n\t\t\t\t\t\treturn r != '\\n' && r != '\\r' && r != '\\t' && unicode.IsControl(r)\n\t\t\t\t\t}) {\n\t\t\t\t\t\terrorWithHint(\"refusing to output binary to the terminal\",\n\t\t\t\t\t\t\t`force anyway with \"-o -\"`)\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t} else if !armorFlag {\n\t\t\t\t// If the output wouldn't be armored, refuse to send binary to\n\t\t\t\t// the terminal unless explicitly requested with \"-o -\".\n\t\t\t\terrorWithHint(\"refusing to output binary to the terminal\",\n\t\t\t\t\t\"did you mean to use -a/--armor?\",\n\t\t\t\t\t`force anyway with \"-o -\"`)\n\t\t\t}\n\t\t}\n\t\tif in == os.Stdin && term.IsTerminal(os.Stdin) {\n\t\t\t// If the input comes from a TTY and output will go to a TTY,\n\t\t\t// buffer it up so it doesn't get in the way of typing the input.\n\t\t\tout = buf\n\t\t}\n\t}\n\n\tswitch {\n\tcase decryptFlag && len(identityFlags) == 0:\n\t\tdecryptPass(in, out)\n\tcase decryptFlag:\n\t\tdecryptNotPass(identityFlags, in, out)\n\tcase passFlag:\n\t\tencryptPass(in, out, armorFlag)\n\tdefault:\n\t\tencryptNotPass(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag)\n\t}\n}\n\nfunc passphrasePromptForEncryption() (string, error) {\n\tpass, err := term.ReadSecret(\"Enter passphrase (leave empty to autogenerate a secure one):\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not read passphrase: %v\", err)\n\t}\n\tp := string(pass)\n\tif p == \"\" {\n\t\tvar words []string\n\t\tfor range 10 {\n\t\t\twords = append(words, randomWord())\n\t\t}\n\t\tp = strings.Join(words, \"-\")\n\t\terr := printfToTerminal(\"using autogenerated passphrase %q\", p)\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not print passphrase: %v\", err)\n\t\t}\n\t} else {\n\t\tconfirm, err := term.ReadSecret(\"Confirm passphrase:\")\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"could not read passphrase: %v\", err)\n\t\t}\n\t\tif string(confirm) != p {\n\t\t\treturn \"\", fmt.Errorf(\"passphrases didn't match\")\n\t\t}\n\t}\n\treturn p, nil\n}\n\nfunc encryptNotPass(recs, files []string, identities identityFlags, in io.Reader, out io.Writer, armor bool) {\n\tvar recipients []age.Recipient\n\tfor _, arg := range recs {\n\t\tr, err := parseRecipient(arg)\n\t\tif err, ok := err.(gitHubRecipientError); ok {\n\t\t\terrorWithHint(err.Error(), \"instead, use recipient files like\",\n\t\t\t\t\"    curl -O https://github.com/\"+err.username+\".keys\",\n\t\t\t\t\"    age -R \"+err.username+\".keys\")\n\t\t}\n\t\tif err != nil {\n\t\t\terrorf(\"%v\", err)\n\t\t}\n\t\trecipients = append(recipients, r)\n\t}\n\tfor _, name := range files {\n\t\trecs, err := parseRecipientsFile(name)\n\t\tif err != nil {\n\t\t\terrorf(\"failed to parse recipient file %q: %v\", name, err)\n\t\t}\n\t\trecipients = append(recipients, recs...)\n\t}\n\tfor _, f := range identities {\n\t\tswitch f.Type {\n\t\tcase \"i\":\n\t\t\tids, err := parseIdentitiesFile(f.Value)\n\t\t\tif err != nil {\n\t\t\t\terrorf(\"reading %q: %v\", f.Value, err)\n\t\t\t}\n\t\t\tr, err := identitiesToRecipients(ids)\n\t\t\tif err != nil {\n\t\t\t\terrorf(\"internal error processing %q: %v\", f.Value, err)\n\t\t\t}\n\t\t\trecipients = append(recipients, r...)\n\t\tcase \"j\":\n\t\t\tid, err := plugin.NewIdentityWithoutData(f.Value, plugin.NewTerminalUI(printf, warningf))\n\t\t\tif err != nil {\n\t\t\t\terrorf(\"initializing %q: %v\", f.Value, err)\n\t\t\t}\n\t\t\trecipients = append(recipients, id.Recipient())\n\t\t}\n\t}\n\tencrypt(recipients, in, out, armor)\n}\n\nfunc encryptPass(in io.Reader, out io.Writer, armor bool) {\n\tpass, err := passphrasePromptForEncryption()\n\tif err != nil {\n\t\terrorf(\"%v\", err)\n\t}\n\n\tr, err := age.NewScryptRecipient(pass)\n\tif err != nil {\n\t\terrorf(\"%v\", err)\n\t}\n\ttestOnlyConfigureScryptIdentity(r)\n\tencrypt([]age.Recipient{r}, in, out, armor)\n}\n\nvar testOnlyConfigureScryptIdentity = func(*age.ScryptRecipient) {}\n\nfunc encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor bool) {\n\tif withArmor {\n\t\ta := armor.NewWriter(out)\n\t\tdefer func() {\n\t\t\tif err := a.Close(); err != nil {\n\t\t\t\terrorf(\"%v\", err)\n\t\t\t}\n\t\t}()\n\t\tout = a\n\t}\n\tw, err := age.Encrypt(out, recipients...)\n\tif e := new(plugin.NotFoundError); errors.As(err, &e) {\n\t\terrorWithHint(err.Error(),\n\t\t\tfmt.Sprintf(\"you might want to install the %q plugin\", e.Name),\n\t\t\t\"visit https://age-encryption.org/awesome#plugins for a list of available plugins\")\n\t} else if err != nil {\n\t\terrorf(\"%v\", err)\n\t}\n\tif _, err := io.Copy(w, in); err != nil {\n\t\terrorf(\"%v\", err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\terrorf(\"%v\", err)\n\t}\n}\n\n// crlfMangledIntro and utf16MangledIntro are the intro lines of the age format\n// after mangling by various versions of PowerShell redirection, truncated to\n// the length of the correct intro line. See issue 290.\nconst crlfMangledIntro = \"age-encryption.org/v1\" + \"\\r\"\nconst utf16MangledIntro = \"\\xff\\xfe\" + \"a\\x00g\\x00e\\x00-\\x00e\\x00n\\x00c\\x00r\\x00y\\x00p\\x00\"\n\ntype rejectScryptIdentity struct{}\n\nfunc (rejectScryptIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {\n\tif len(stanzas) != 1 || stanzas[0].Type != \"scrypt\" {\n\t\treturn nil, age.ErrIncorrectIdentity\n\t}\n\terrorWithHint(\"file is passphrase-encrypted but identities were specified with -i/--identity or -j\",\n\t\t\"remove all -i/--identity/-j flags to decrypt passphrase-encrypted files\")\n\tpanic(\"unreachable\")\n}\n\nfunc decryptNotPass(flags identityFlags, in io.Reader, out io.Writer) {\n\tvar identities []age.Identity\n\tfor _, f := range flags {\n\t\tswitch f.Type {\n\t\tcase \"i\":\n\t\t\tids, err := parseIdentitiesFile(f.Value)\n\t\t\tif err != nil {\n\t\t\t\terrorf(\"reading %q: %v\", f.Value, err)\n\t\t\t}\n\t\t\tidentities = append(identities, ids...)\n\t\tcase \"j\":\n\t\t\tid, err := plugin.NewIdentityWithoutData(f.Value, plugin.NewTerminalUI(printf, warningf))\n\t\t\tif err != nil {\n\t\t\t\terrorf(\"initializing %q: %v\", f.Value, err)\n\t\t\t}\n\t\t\tidentities = append(identities, id)\n\t\t}\n\t}\n\tidentities = append(identities, rejectScryptIdentity{})\n\tdecrypt(identities, in, out)\n}\n\nfunc decryptPass(in io.Reader, out io.Writer) {\n\tidentities := []age.Identity{\n\t\t// If there is an scrypt recipient (it will have to be the only one and)\n\t\t// this identity will be invoked.\n\t\tlazyScryptIdentity,\n\t}\n\n\tdecrypt(identities, in, out)\n}\n\nfunc decrypt(identities []age.Identity, in io.Reader, out io.Writer) {\n\trr := bufio.NewReader(in)\n\tif intro, _ := rr.Peek(len(crlfMangledIntro)); string(intro) == crlfMangledIntro ||\n\t\tstring(intro) == utf16MangledIntro {\n\t\terrorWithHint(\"invalid header intro\",\n\t\t\t\"it looks like this file was corrupted by PowerShell redirection\",\n\t\t\t\"consider using -o or -a to encrypt files in PowerShell\")\n\t}\n\n\tconst maxWhitespace = 1024\n\tstart, _ := rr.Peek(maxWhitespace + len(armor.Header))\n\tif strings.HasPrefix(string(bytes.TrimSpace(start)), armor.Header) {\n\t\tin = armor.NewReader(rr)\n\t} else {\n\t\tin = rr\n\t}\n\n\tr, err := age.Decrypt(in, identities...)\n\tif e := new(plugin.NotFoundError); errors.As(err, &e) {\n\t\terrorWithHint(err.Error(),\n\t\t\tfmt.Sprintf(\"you might want to install the %q plugin\", e.Name),\n\t\t\t\"visit https://age-encryption.org/awesome#plugins for a list of available plugins\")\n\t} else if errors.As(err, new(*age.NoIdentityMatchError)) &&\n\t\tlen(identities) == 1 && identities[0] == lazyScryptIdentity {\n\t\terrorWithHint(\"the file is not passphrase-encrypted, identities are required\",\n\t\t\t\"specify identities with -i/--identity or -j to decrypt this file\")\n\t} else if err != nil {\n\t\terrorf(\"%v\", err)\n\t}\n\tout.Write(nil) // trigger the lazyOpener even if r is empty\n\tif _, err := io.Copy(out, r); err != nil {\n\t\terrorf(\"%v\", err)\n\t}\n}\n\nvar lazyScryptIdentity = &LazyScryptIdentity{passphrasePromptForDecryption}\n\nfunc passphrasePromptForDecryption() (string, error) {\n\tpass, err := term.ReadSecret(\"Enter passphrase:\")\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"could not read passphrase: %v\", err)\n\t}\n\treturn string(pass), nil\n}\n\nfunc identitiesToRecipients(ids []age.Identity) ([]age.Recipient, error) {\n\tvar recipients []age.Recipient\n\tfor _, id := range ids {\n\t\tswitch id := id.(type) {\n\t\tcase *age.X25519Identity:\n\t\t\trecipients = append(recipients, id.Recipient())\n\t\tcase *age.HybridIdentity:\n\t\t\trecipients = append(recipients, id.Recipient())\n\t\tcase *plugin.Identity:\n\t\t\trecipients = append(recipients, id.Recipient())\n\t\tcase *agessh.RSAIdentity:\n\t\t\trecipients = append(recipients, id.Recipient())\n\t\tcase *agessh.Ed25519Identity:\n\t\t\trecipients = append(recipients, id.Recipient())\n\t\tcase *agessh.EncryptedSSHIdentity:\n\t\t\trecipients = append(recipients, id.Recipient())\n\t\tcase *EncryptedIdentity:\n\t\t\tr, err := id.Recipients()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trecipients = append(recipients, r...)\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unexpected identity type: %T\", id)\n\t\t}\n\t}\n\treturn recipients, nil\n}\n\ntype lazyOpener struct {\n\tname string\n\tf    *os.File\n\terr  error\n}\n\nfunc newLazyOpener(name string) io.WriteCloser {\n\treturn &lazyOpener{name: name}\n}\n\nfunc (l *lazyOpener) Write(p []byte) (n int, err error) {\n\tif l.f == nil && l.err == nil {\n\t\tl.f, l.err = os.Create(l.name)\n\t}\n\tif l.err != nil {\n\t\treturn 0, l.err\n\t}\n\treturn l.f.Write(p)\n}\n\nfunc (l *lazyOpener) Close() error {\n\tif l.f != nil {\n\t\treturn l.f.Close()\n\t}\n\treturn nil\n}\n\nfunc absPath(name string) string {\n\tif abs, err := filepath.Abs(name); err == nil {\n\t\treturn abs\n\t}\n\treturn name\n}\n\nfunc warnDuplicates(s iter.Seq[string], name string) {\n\tseen := make(map[string]bool)\n\twarned := make(map[string]bool)\n\tfor e := range s {\n\t\tif seen[e] && !warned[e] {\n\t\t\twarningf(\"duplicate %s %q\", name, e)\n\t\t\twarned[e] = true\n\t\t}\n\t\tseen[e] = true\n\t}\n}\n"
  },
  {
    "path": "cmd/age/age_test.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/plugin\"\n\t\"github.com/rogpeppe/go-internal/testscript\"\n)\n\nfunc TestMain(m *testing.M) {\n\ttestscript.Main(m, map[string]func(){\n\t\t\"age\": func() {\n\t\t\ttestOnlyConfigureScryptIdentity = func(r *age.ScryptRecipient) {\n\t\t\t\tr.SetWorkFactor(10)\n\t\t\t}\n\t\t\ttestOnlyFixedRandomWord = \"four\"\n\t\t\tmain()\n\t\t},\n\t\t\"age-plugin-test\": func() {\n\t\t\tp, _ := plugin.New(\"test\")\n\t\t\tp.HandleRecipient(func(data []byte) (age.Recipient, error) {\n\t\t\t\treturn testPlugin{}, nil\n\t\t\t})\n\t\t\tp.HandleIdentity(func(data []byte) (age.Identity, error) {\n\t\t\t\treturn testPlugin{}, nil\n\t\t\t})\n\t\t\tos.Exit(p.Main())\n\t\t},\n\t})\n}\n\ntype testPlugin struct{}\n\nfunc (testPlugin) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\treturn []*age.Stanza{{Type: \"test\", Body: fileKey}}, nil\n}\n\nfunc (testPlugin) Unwrap(ss []*age.Stanza) ([]byte, error) {\n\tif len(ss) == 1 && ss[0].Type == \"test\" {\n\t\treturn ss[0].Body, nil\n\t}\n\treturn nil, age.ErrIncorrectIdentity\n}\n\nvar buildExtraCommands = sync.OnceValue(func() error {\n\tbindir := filepath.SplitList(os.Getenv(\"PATH\"))[0]\n\t// Build age-keygen and age-plugin-pq into the test binary directory.\n\tcmd := exec.Command(\"go\", \"build\", \"-o\", bindir)\n\tif testing.CoverMode() != \"\" {\n\t\tcmd.Args = append(cmd.Args, \"-cover\")\n\t}\n\tcmd.Args = append(cmd.Args, \"filippo.io/age/cmd/age-keygen\")\n\tcmd.Args = append(cmd.Args, \"filippo.io/age/extra/age-plugin-pq\")\n\tcmd.Args = append(cmd.Args, \"filippo.io/age/cmd/age-plugin-batchpass\")\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\treturn cmd.Run()\n})\n\nfunc TestScript(t *testing.T) {\n\ttestscript.Run(t, testscript.Params{\n\t\tDir: \"testdata\",\n\t\tSetup: func(e *testscript.Env) error {\n\t\t\treturn buildExtraCommands()\n\t\t},\n\t\t// TODO: enable AGEDEBUG=plugin without breaking stderr checks.\n\t})\n}\n"
  },
  {
    "path": "cmd/age/encrypted_keys.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"filippo.io/age\"\n)\n\n// LazyScryptIdentity is an age.Identity that requests a passphrase only if it\n// encounters an scrypt stanza. After obtaining a passphrase, it delegates to\n// ScryptIdentity.\ntype LazyScryptIdentity struct {\n\tPassphrase func() (string, error)\n}\n\nvar _ age.Identity = &LazyScryptIdentity{}\n\nfunc (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {\n\tfor _, s := range stanzas {\n\t\tif s.Type == \"scrypt\" && len(stanzas) != 1 {\n\t\t\treturn nil, errors.New(\"an scrypt recipient must be the only one\")\n\t\t}\n\t}\n\tif len(stanzas) != 1 || stanzas[0].Type != \"scrypt\" {\n\t\treturn nil, age.ErrIncorrectIdentity\n\t}\n\tpass, err := i.Passphrase()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not read passphrase: %v\", err)\n\t}\n\tii, err := age.NewScryptIdentity(pass)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfileKey, err = ii.Unwrap(stanzas)\n\tif errors.Is(err, age.ErrIncorrectIdentity) {\n\t\t// ScryptIdentity returns ErrIncorrectIdentity for an incorrect\n\t\t// passphrase, which would lead Decrypt to returning \"no identity\n\t\t// matched any recipient\". That makes sense in the API, where there\n\t\t// might be multiple configured ScryptIdentity. Since in cmd/age there\n\t\t// can be only one, return a better error message.\n\t\treturn nil, fmt.Errorf(\"incorrect passphrase\")\n\t}\n\treturn fileKey, err\n}\n\ntype EncryptedIdentity struct {\n\tContents       []byte\n\tPassphrase     func() (string, error)\n\tNoMatchWarning func()\n\n\tidentities []age.Identity\n}\n\nvar _ age.Identity = &EncryptedIdentity{}\n\nfunc (i *EncryptedIdentity) Recipients() ([]age.Recipient, error) {\n\tif i.identities == nil {\n\t\tif err := i.decrypt(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn identitiesToRecipients(i.identities)\n}\n\nfunc (i *EncryptedIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {\n\tif i.identities == nil {\n\t\tif err := i.decrypt(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tfor _, id := range i.identities {\n\t\tfileKey, err = id.Unwrap(stanzas)\n\t\tif errors.Is(err, age.ErrIncorrectIdentity) {\n\t\t\tcontinue\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn fileKey, nil\n\t}\n\ti.NoMatchWarning()\n\treturn nil, age.ErrIncorrectIdentity\n}\n\nfunc (i *EncryptedIdentity) decrypt() error {\n\td, err := age.Decrypt(bytes.NewReader(i.Contents), &LazyScryptIdentity{i.Passphrase})\n\tif e := new(age.NoIdentityMatchError); errors.As(err, &e) {\n\t\treturn fmt.Errorf(\"identity file is encrypted with age but not with a passphrase\")\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to decrypt identity file: %v\", err)\n\t}\n\ti.identities, err = parseIdentities(d)\n\treturn err\n}\n"
  },
  {
    "path": "cmd/age/parse.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"bufio\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/agessh\"\n\t\"filippo.io/age/armor\"\n\t\"filippo.io/age/internal/term\"\n\t\"filippo.io/age/plugin\"\n\t\"filippo.io/age/tag\"\n\t\"golang.org/x/crypto/cryptobyte\"\n\t\"golang.org/x/crypto/ssh\"\n)\n\ntype gitHubRecipientError struct {\n\tusername string\n}\n\nfunc (gitHubRecipientError) Error() string {\n\treturn `\"github:\" recipients were removed from the design`\n}\n\nfunc parseRecipient(arg string) (age.Recipient, error) {\n\tswitch {\n\tcase strings.HasPrefix(arg, \"age1tag1\") || strings.HasPrefix(arg, \"age1tagpq1\"):\n\t\treturn tag.ParseRecipient(arg)\n\tcase strings.HasPrefix(arg, \"age1pq1\"):\n\t\treturn age.ParseHybridRecipient(arg)\n\tcase strings.HasPrefix(arg, \"age1\") && strings.Count(arg, \"1\") > 1:\n\t\treturn plugin.NewRecipient(arg, plugin.NewTerminalUI(printf, warningf))\n\tcase strings.HasPrefix(arg, \"age1\"):\n\t\treturn age.ParseX25519Recipient(arg)\n\tcase strings.HasPrefix(arg, \"ssh-\"):\n\t\treturn agessh.ParseRecipient(arg)\n\tcase strings.HasPrefix(arg, \"github:\"):\n\t\tname := strings.TrimPrefix(arg, \"github:\")\n\t\treturn nil, gitHubRecipientError{name}\n\t}\n\n\treturn nil, fmt.Errorf(\"unknown recipient type: %q\", arg)\n}\n\nfunc parseRecipientsFile(name string) ([]age.Recipient, error) {\n\tvar f *os.File\n\tif name == \"-\" {\n\t\tif stdinInUse {\n\t\t\treturn nil, fmt.Errorf(\"standard input is used for multiple purposes\")\n\t\t}\n\t\tstdinInUse = true\n\t\tf = os.Stdin\n\t} else {\n\t\tvar err error\n\t\tf, err = os.Open(name)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to open recipient file: %v\", err)\n\t\t}\n\t\tdefer f.Close()\n\t}\n\n\tconst recipientFileSizeLimit = 16 << 20 // 16 MiB\n\tconst lineLengthLimit = 8 << 10         // 8 KiB, same as sshd(8)\n\tvar recs []age.Recipient\n\tscanner := bufio.NewScanner(io.LimitReader(f, recipientFileSizeLimit))\n\tvar n int\n\tfor scanner.Scan() {\n\t\tn++\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, \"#\") || line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !utf8.ValidString(line) {\n\t\t\treturn nil, fmt.Errorf(\"%q: recipients file is not valid UTF-8\", name)\n\t\t}\n\t\tif len(line) > lineLengthLimit {\n\t\t\treturn nil, fmt.Errorf(\"%q: line %d is too long\", name, n)\n\t\t}\n\t\tr, err := parseRecipient(line)\n\t\tif err != nil {\n\t\t\tif t, ok := sshKeyType(line); ok {\n\t\t\t\t// Skip unsupported but valid SSH public keys with a warning.\n\t\t\t\twarningf(\"recipients file %q: ignoring unsupported SSH key of type %q at line %d\", name, t, n)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.HasPrefix(line, \"AGE-\") {\n\t\t\t\treturn nil, fmt.Errorf(\"%q: error at line %d: apparent identity found in recipients file\", name, n)\n\t\t\t}\n\t\t\t// Hide the error since it might unintentionally leak the contents\n\t\t\t// of confidential files.\n\t\t\treturn nil, fmt.Errorf(\"%q: malformed recipient at line %d\", name, n)\n\t\t}\n\t\trecs = append(recs, r)\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"%q: failed to read recipients file: %v\", name, err)\n\t}\n\tif len(recs) == 0 {\n\t\treturn nil, fmt.Errorf(\"%q: no recipients found\", name)\n\t}\n\treturn recs, nil\n}\n\nfunc sshKeyType(s string) (string, bool) {\n\t// TODO: also ignore options? And maybe support multiple spaces and tabs as\n\t// field separators like OpenSSH?\n\tfields := strings.Split(s, \" \")\n\tif len(fields) < 2 {\n\t\treturn \"\", false\n\t}\n\tkey, err := base64.StdEncoding.DecodeString(fields[1])\n\tif err != nil {\n\t\treturn \"\", false\n\t}\n\tk := cryptobyte.String(key)\n\tvar typeLen uint32\n\tvar typeBytes []byte\n\tif !k.ReadUint32(&typeLen) || !k.ReadBytes(&typeBytes, int(typeLen)) {\n\t\treturn \"\", false\n\t}\n\tif t := fields[0]; t == string(typeBytes) {\n\t\treturn t, true\n\t}\n\treturn \"\", false\n}\n\n// parseIdentitiesFile parses a file that contains age or SSH keys. It returns\n// one or more of *[age.X25519Identity], *[age.HybridIdentity],\n// *[agessh.RSAIdentity], *[agessh.Ed25519Identity],\n// *[agessh.EncryptedSSHIdentity], or *[EncryptedIdentity].\nfunc parseIdentitiesFile(name string) ([]age.Identity, error) {\n\tvar f *os.File\n\tif name == \"-\" {\n\t\tif stdinInUse {\n\t\t\treturn nil, fmt.Errorf(\"standard input is used for multiple purposes\")\n\t\t}\n\t\tstdinInUse = true\n\t\tf = os.Stdin\n\t} else {\n\t\tvar err error\n\t\tf, err = os.Open(name)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to open file: %v\", err)\n\t\t}\n\t\tdefer f.Close()\n\t}\n\n\tb := bufio.NewReader(f)\n\tp, _ := b.Peek(14) // length of \"age-encryption\" and \"-----BEGIN AGE\"\n\tpeeked := string(p)\n\n\tswitch {\n\t// An age encrypted file, plain or armored.\n\tcase peeked == \"age-encryption\" || peeked == \"-----BEGIN AGE\":\n\t\tvar r io.Reader = b\n\t\tif peeked == \"-----BEGIN AGE\" {\n\t\t\tr = armor.NewReader(r)\n\t\t}\n\t\tconst privateKeySizeLimit = 1 << 24 // 16 MiB\n\t\tcontents, err := io.ReadAll(io.LimitReader(r, privateKeySizeLimit))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read %q: %v\", name, err)\n\t\t}\n\t\tif len(contents) == privateKeySizeLimit {\n\t\t\treturn nil, fmt.Errorf(\"failed to read %q: file too long\", name)\n\t\t}\n\t\treturn []age.Identity{&EncryptedIdentity{\n\t\t\tContents: contents,\n\t\t\tPassphrase: func() (string, error) {\n\t\t\t\tpass, err := term.ReadSecret(fmt.Sprintf(\"Enter passphrase for identity file %q:\", name))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", fmt.Errorf(\"could not read passphrase: %v\", err)\n\t\t\t\t}\n\t\t\t\treturn string(pass), nil\n\t\t\t},\n\t\t\tNoMatchWarning: func() {\n\t\t\t\twarningf(\"encrypted identity file %q didn't match file's recipients\", name)\n\t\t\t},\n\t\t}}, nil\n\n\t// Another PEM file, possibly an SSH private key.\n\tcase strings.HasPrefix(peeked, \"-----BEGIN\"):\n\t\tconst privateKeySizeLimit = 1 << 14 // 16 KiB\n\t\tcontents, err := io.ReadAll(io.LimitReader(b, privateKeySizeLimit))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read %q: %v\", name, err)\n\t\t}\n\t\tif len(contents) == privateKeySizeLimit {\n\t\t\treturn nil, fmt.Errorf(\"failed to read %q: file too long\", name)\n\t\t}\n\t\treturn parseSSHIdentity(name, contents)\n\n\t// An unencrypted age identity file.\n\tdefault:\n\t\tids, err := parseIdentities(b)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read %q: %v\", name, err)\n\t\t}\n\t\treturn ids, nil\n\t}\n}\n\nfunc parseIdentity(s string) (age.Identity, error) {\n\tswitch {\n\tcase strings.HasPrefix(s, \"AGE-PLUGIN-\"):\n\t\treturn plugin.NewIdentity(s, plugin.NewTerminalUI(printf, warningf))\n\tcase strings.HasPrefix(s, \"AGE-SECRET-KEY-1\"):\n\t\treturn age.ParseX25519Identity(s)\n\tcase strings.HasPrefix(s, \"AGE-SECRET-KEY-PQ-1\"):\n\t\treturn age.ParseHybridIdentity(s)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown identity type\")\n\t}\n}\n\n// parseIdentities is like [age.ParseIdentities], but supports plugin identities.\nfunc parseIdentities(f io.Reader) ([]age.Identity, error) {\n\tconst privateKeySizeLimit = 1 << 24 // 16 MiB\n\tvar ids []age.Identity\n\tscanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))\n\tvar n int\n\tfor scanner.Scan() {\n\t\tn++\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, \"#\") || line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !utf8.ValidString(line) {\n\t\t\treturn nil, fmt.Errorf(\"identities file is not valid UTF-8\")\n\t\t}\n\t\ti, err := parseIdentity(line)\n\t\tif err != nil {\n\t\t\tif strings.HasPrefix(line, \"age1\") {\n\t\t\t\treturn nil, fmt.Errorf(\"error at line %d: apparent recipient found in identities file\", n)\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"error at line %d: %v\", n, err)\n\t\t}\n\t\tids = append(ids, i)\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read identities file: %v\", err)\n\t}\n\tif len(ids) == 0 {\n\t\treturn nil, fmt.Errorf(\"no identities found\")\n\t}\n\treturn ids, nil\n}\n\nfunc parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {\n\tid, err := agessh.ParseIdentity(pemBytes)\n\tif sshErr, ok := err.(*ssh.PassphraseMissingError); ok {\n\t\tpubKey := sshErr.PublicKey\n\t\tif pubKey == nil {\n\t\t\tpubKey, err = readPubFile(name)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\tpassphrasePrompt := func() ([]byte, error) {\n\t\t\tpass, err := term.ReadSecret(fmt.Sprintf(\"Enter passphrase for %q:\", name))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"could not read passphrase for %q: %v\", name, err)\n\t\t\t}\n\t\t\treturn pass, nil\n\t\t}\n\t\ti, err := agessh.NewEncryptedSSHIdentity(pubKey, pemBytes, passphrasePrompt)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn []age.Identity{i}, nil\n\t}\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed SSH identity in %q: %v\", name, err)\n\t}\n\n\treturn []age.Identity{id}, nil\n}\n\nfunc readPubFile(name string) (ssh.PublicKey, error) {\n\tif name == \"-\" {\n\t\treturn nil, fmt.Errorf(`failed to obtain public key for \"-\" SSH key\n\nUse a file for which the corresponding \".pub\" file exists, or convert the private key to a modern format with \"ssh-keygen -p -m RFC4716\"`)\n\t}\n\tf, err := os.Open(name + \".pub\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(`failed to obtain public key for %q SSH key: %v\n\nEnsure %q exists, or convert the private key %q to a modern format with \"ssh-keygen -p -m RFC4716\"`, name, err, name+\".pub\", name)\n\t}\n\tdefer f.Close()\n\tcontents, err := io.ReadAll(f)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read %q: %v\", name+\".pub\", err)\n\t}\n\tpubKey, _, _, _, err := ssh.ParseAuthorizedKey(contents)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse %q: %v\", name+\".pub\", err)\n\t}\n\treturn pubKey, nil\n}\n"
  },
  {
    "path": "cmd/age/testdata/armor.txt",
    "content": "age -d -i key.txt armored_with_leading_and_trailing_whitespace.txt\nstdout test\n\n-- key.txt --\n# created: 2025-12-23T22:21:12+01:00\n# public key: age15w9kgvgggmfra4sz6vk39kz4mveuq2sfv5vmcu090y0k2sluepaqv7z2fv\nAGE-SECRET-KEY-18J6FVYJE2AFSJ0RPH6M29GMUU62UVRSCNWUJZSGETH6R38Q5AZ3S2DHAZ9\n\n-- armored_with_leading_and_trailing_whitespace.txt --\n\n   \n\n-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5ODhFNHR6RVg0SGVHZFBM\nclBEclEzZ3NvOGhqVE9tcFZnbTc2c3R5a0Q4ClZjVzBLNjdxRElZV3E0Z3ZpZ255\nT3JWTFBHRFA2cytpWWtkeU45dDRadmcKLS0tIHV3L3hOVmJjL0hMRXBQa05lMlRs\nZW45TndPeE9GcmRNeWFkR3YxeHg0YzQKJBp6KRlFFUE8jbAQUBlcAwaaQcPAflJD\npWGoOjYP33gTxJHNPg==\n-----END AGE ENCRYPTED FILE-----\n\n   \n"
  },
  {
    "path": "cmd/age/testdata/batchpass.txt",
    "content": "# encrypt and decrypt with AGE_PASSPHRASE\nenv AGE_PASSPHRASE_WORK_FACTOR=5\nenv AGE_PASSPHRASE=password\nage -e -j batchpass -o test.age input\nage -d -j batchpass test.age\ncmp stdout input\n\n# decrypt with AGE_PASSPHRASE_MAX_WORK_FACTOR\nenv AGE_PASSPHRASE_MAX_WORK_FACTOR=10\nage -d -j batchpass test.age\ncmp stdout input\n\n# AGE_PASSPHRASE_MAX_WORK_FACTOR lower than work factor\nenv AGE_PASSPHRASE_MAX_WORK_FACTOR=3\n! age -d -j batchpass test.age\nstderr 'work factor'\nenv AGE_PASSPHRASE_MAX_WORK_FACTOR=\n\n# error: both AGE_PASSPHRASE and AGE_PASSPHRASE_FD set\nenv AGE_PASSPHRASE=password\nenv AGE_PASSPHRASE_FD=3\n! age -e -j batchpass -a input\nstderr 'mutually exclusive'\n\n# error: neither AGE_PASSPHRASE nor AGE_PASSPHRASE_FD set\nenv AGE_PASSPHRASE=\nenv AGE_PASSPHRASE_FD=\n! age -e -j batchpass -a test.age\nstderr 'must be set'\n\n# error: incorrect passphrase\nenv AGE_PASSPHRASE=wrongpassword\n! age -d -j batchpass test.age\nstderr 'incorrect passphrase'\n\n# error: encrypting to other recipients along with passphrase\nenv AGE_PASSPHRASE=password\n! age -e -j batchpass -r age1gc2zwslg9j25pg9e9ld7623aewc2gjzquzgq90m75r75myqdt5cq3yudvh -a input\nstderr 'incompatible recipients'\n! age -e -r age1gc2zwslg9j25pg9e9ld7623aewc2gjzquzgq90m75r75myqdt5cq3yudvh -j batchpass -a input\nstderr 'incompatible recipients'\n\n# decrypt with native scrypt\n[!linux] [!darwin] skip # no pty support\n[darwin] [go1.20] skip # https://go.dev/issue/61779\nttyin terminal\nage -d test.age\ncmp stdout input\n\n-- terminal --\npassword\npassword\n-- input --\ntest\n"
  },
  {
    "path": "cmd/age/testdata/duplicates.txt",
    "content": "# Test duplicate recipient detection\nage -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -r age1yv2sxrjnyymx4qkn3ehdd5zdq4xnrea5ljm4hsmgxn5tpv985anqkrwqa7 -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -o test.age input\nstderr 'warning: duplicate recipient \"age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85\"'\n\n# Test duplicates separated by different argument\nage -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -a -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -o test2.age input\nstderr 'warning: duplicate recipient \"age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85\"'\n\n# Test duplicate recipients file detection\nage -R recipients1.txt -R recipients2.txt -R recipients1.txt -o test3.age input\nstderr 'warning: duplicate recipients file \"recipients1.txt\"'\n\n# Test duplicates separated by output flag\nage -R recipients1.txt -o test4.age -R recipients1.txt input\nstderr 'warning: duplicate recipients file \"recipients1.txt\"'\n\n# First create an encrypted file for decrypt tests\nage -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -o encrypted.age input\n\n# Test duplicate identity file detection (decrypt mode)\nage -d -i key1.txt -i key2.txt -i key1.txt encrypted.age\nstderr 'warning: duplicate identity file \"key1.txt\"'\n\n# Test duplicates separated by different argument in decrypt mode\nage -d -i key1.txt -o test.out -i key1.txt encrypted.age\nstderr 'warning: duplicate identity file \"key1.txt\"'\n\n# Test no warning when no duplicates\nage -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -r age1yv2sxrjnyymx4qkn3ehdd5zdq4xnrea5ljm4hsmgxn5tpv985anqkrwqa7 -o test5.age input\n! stderr 'warning: duplicate'\n\n# Test multiple duplicates (same value repeated 3+ times)\nage -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -o test6.age input\nstderr 'warning: duplicate recipient \"age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85\"'\n\n-- input --\ntest data\n-- recipients1.txt --\nage1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85\n-- recipients2.txt --\nage1yv2sxrjnyymx4qkn3ehdd5zdq4xnrea5ljm4hsmgxn5tpv985anqkrwqa7\n-- key1.txt --\n# created: 2025-12-22T22:06:22+01:00\n# public key: age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85\nAGE-SECRET-KEY-1WRM2S8SP3XSKLLXAXS489EXZNKCKRZWYQLQ8D2NRNQWCVAPSMA9SC5JWZQ\n-- key2.txt --\n# created: 2025-12-22T22:06:27+01:00\n# public key: age1yv2sxrjnyymx4qkn3ehdd5zdq4xnrea5ljm4hsmgxn5tpv985anqkrwqa7\nAGE-SECRET-KEY-1WZ3MRPAWEWR4DG474H460MXX7J2T0TEYNJ0SKQDMKP02JU7UJ9UQFGLZCE\n"
  },
  {
    "path": "cmd/age/testdata/ed25519.txt",
    "content": "# encrypt and decrypt a file with -R\nage -R key.pem.pub -o test.age input\nage -d -i key.pem test.age\ncmp stdout input\n! stderr .\n\n# encrypt and decrypt a file with -i\nage -e -i key.pem -o test.age input\nage -d -i key.pem test.age\ncmp stdout input\n! stderr .\n\n# encrypt and decrypt a file with the wrong key\nage -R otherkey.pem.pub -o test.age input\n! age -d -i key.pem test.age\nstderr 'no identity matched any of the recipients'\n! stdout .\n\n-- input --\ntest\n-- key.pem --\n-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2gAAAKDgV/GC4Ffx\nggAAAAtzc2gtZWQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2g\nAAAECvFoQXQzXgJLQ+Gz4PfEcfyZwC2gUjOiWTD//mTPyD8H9pO5pz22JZEasoS2LEWWvJ\niUYI9M6l1uZc31FGYMXaAAAAG2ZpbGlwcG9AQmlzdHJvbWF0aC1NMS5sb2NhbAEC\n-----END OPENSSH PRIVATE KEY-----\n-- key.pem.pub --\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FGYMXa\n-- otherkey.pem.pub --\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJFlMdZUMrWjJ3hh60MLALXSqUdAjBo/qEMJzvpekpoM\n"
  },
  {
    "path": "cmd/age/testdata/encrypted_keys.txt",
    "content": "# TODO: age-encrypted private keys, multiple identities, -i ordering, -e -i,\n# age file password prompt during encryption\n\n[!linux] [!darwin] skip # no pty support\n[darwin] [go1.20] skip # https://go.dev/issue/61779\n\n# use an encrypted OpenSSH private key without .pub file\nage -R key_ed25519.pub -o ed25519.age input\nrm key_ed25519.pub\nttyin terminal\nage -d -i key_ed25519 ed25519.age\ncmp stdout input\n! stderr .\n\n# -e -i with an encrypted OpenSSH private key\nage -e -i key_ed25519 -o ed25519.age input\nttyin terminal\nage -d -i key_ed25519 ed25519.age\ncmp stdout input\n\n# a file encrypted to the wrong key does not ask for the password\nage -R key_ed25519_other.pub -o ed25519_other.age input\n! age -d -i key_ed25519 ed25519_other.age\nstderr 'no identity matched any of the recipients'\n\n# use an encrypted legacy PEM private key with a .pub file\nage -R key_rsa_legacy.pub -o rsa_legacy.age input\nttyin terminal\nage -d -i key_rsa_legacy rsa_legacy.age\ncmp stdout input\n! stderr .\nage -R key_rsa_other.pub -o rsa_other.age input\n! age -d -i key_rsa_legacy rsa_other.age\nstderr 'no identity matched any of the recipients'\n\n# -e -i with an encrypted legacy PEM private key\nage -e -i key_rsa_legacy -o rsa_legacy.age input\nttyin terminal\nage -d -i key_rsa_legacy rsa_legacy.age\ncmp stdout input\n\n# legacy PEM private key without a .pub file causes an error\nrm key_rsa_legacy.pub\n! age -d -i key_rsa_legacy rsa_legacy.age\nstderr 'key_rsa_legacy.pub'\n\n# mismatched .pub file causes an error\ncp key_rsa_legacy key_rsa_other\nttyin terminal\n! age -d -i key_rsa_other rsa_other.age\nstderr 'mismatched private and public SSH key'\n\n# buffer armored ciphertext before prompting if stdin is the terminal\nttyin terminal\nage -e -i key_ed25519 -a -o test.age input\nexec cat test.age terminal # concatenated ciphertext + password\nttyin -stdin stdout\nage -d -i key_ed25519\nttyout 'Enter passphrase'\n! stderr .\ncmp stdout input\n\n-- input --\ntest\n-- terminal --\npassword\n-- key_ed25519 --\n-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCuvb97i7\nU6Dz4+4SaF3kK1AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKaVctg4/hmFbfof\nTv+yrC2IweO/Dd2AVDijFpaMO9fmAAAAoMO7yEnisRmzFdiExNt3XTYuLdP9m3jgOCroiF\nTtBhh1lAB2qggzWExMRP3Ak8+AloXEcWiACwBYnqwxhQMh0RDCDKC/H/4SXO+ds4HFWil+\n4bGF9wYZFU7IEjIK91CPGJ6YoWPn9dSdEjjbuCJtOMwHsysGyw5n/qSFPmSAPmA4YL2OzM\nWFOJ5gB5o1LKZkDTcdt7kPziIoVd5QkqpnYsE=\n-----END OPENSSH PRIVATE KEY-----\n-- key_ed25519.pub --\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKaVctg4/hmFbfofTv+yrC2IweO/Dd2AVDijFpaMO9fm\n-- key_ed25519_other.pub --\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINbTd+xfSBYKR/1Hp7FsoxwQAdIOk1Khye6ALBj7e1CV\n-- key_rsa_legacy --\n-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,8045E7CF19D7794F4ADF5AC63179D985\n\nOESHhWCho337W1Ajg+iMbsZx/FPtHM3YPHu/d1U51ERIUh0wVof2SK0ooENokr6g\nO3fcv9Xga+Na4Ez+gsFRsIZOdqrJq+QBH0CAKi+Mz4KsU7teAobUBJgRB31Wt7eI\n39KGZeaBJLMQ0FzQkDx5MCOg98iu9rt+Pg1bH8X88wV4vOv+tG4nmqgdpDmouo1Q\nuW1TJxrdPhkINjaPZZ7gvjS8wuG9+qwQY76I0hGun9secf4VZDysqUnUp8UHYovR\ndbvKCbglQy18mGL4kREJ/hH/9/maefS+pTMb2UX0onp9j7l3yNSvL4A4xW85ii6x\nliVMnZvLvbfPtI7jjZtC8CjshRkZke4fSZF2nZP7zK2qVcqDFCtemaks+0i2ksel\nD8clUKhBmq23VNAt+iy1stwHBporuaE6kEVJail5WPpgdfQjifpaMbTsZgOK+vGL\nGKi8vSJWfMU3lTf/N++ks2FWxdq0TgQirsKsQ5mWobfxc1XehvvdJj8hUtArrP32\nd4ge5DXPpmtkCzrc1+wt8Py/ANl9jV6c+4fCbpQ2snyzdFEhFtXHCEpMguN9MhKI\ngaZIfAxvYcQr8Gwew/IB02Phda9tvDiedHvyHGJmSy/87fR6ECh47VDFL/UYu4jG\n0hRtAZMMddGNfoosnO6OKBd09cgvXKCsUrbpAI7dF5TP5ItDkVb08hW446jBdgS9\n7QqB0rPmlAjsJi7fsrDw7Nq9pOdqqCEwUMc9Lztnv66aX1d/Y2vQm9mrsDbyZKqn\ng621rg7E4UHf7EGiDblfS234+TsNvwZ6sEbivU+3zqglPiOF71m6D0cKgaUZPOua\nGNdyQz5e73hYa+NJ76IZ+IqkoJAFXBkd1nWcN6DUBYiKvqd4qO9xD+JvNtiFlQ9d\npyO9t4FTGvySh8CKyEUEdtj+2ftCIuZaUD2L5YJU1tlQV0EH42InOmkmphbHkW5v\nlNAoZAny1Z0P6O0gn7xtVrgd7paVQfDCJtkvsm5zR6Yei5FUgY/9NPaRotzuZVAY\nEfQC7JPdSdb5yusnXh7B9jGkgxhMIb6EPFFjIZ4iaV1RVgINSisGMSFzlqOz702b\nCawsr9nD438cjzMNYEmrihZZBjHon6hHrLmM9Aj2xgprsoNLP1jJQ6WpZDlrYsj0\nXS0tSJmh0pM4Ey6j1VWNoaOxVseYLW7J9wGVfH/HJAc2k6Wg46P2e8lMT6Sj4YsT\nEguDhUjXrgePC53ohcSF+I6x35Q1D6ttMnc3ODzmIcCisxAvWdAqi1yRlnBotRwg\nS2vq3HU0yJFG8pJqw4vU9A9DlaMMT+ejEH+9xVwAWM+7n2lJcgthtWuShZCE6BB+\njVobSlTMArzQj4klTSbew1m9Waa6kKDezsAY66mryVNofCCeYDOBRecCm5JyMnWf\nWBVnNx+kZ/YyvYeBcSh34u8rkjqGpzfM/oPE7GwIoZvbAirjLohL7u8oq2bfAYG0\n/xIPwPJw1O3o5PHeu84bVIRqcKzGeaVL+5aUiZP9uNGUpqJWA5q2Sa5BOXV46yqO\nDIS8q7uPCSbt5mPXPDGJ1CupCdA1stUf2kb0cDJ+LpUbPND9SebBlxSuR1D/YGqv\nwlzfN5Usv/h/XNl98bYtpY8/skKPecyx3wG3VtwWH/5XVhvHz4TENjlKv/L2pbUC\nDv83WcL1N/i+jerYxDRmGe3NQOvyW4JaNzzjgb74T7rE1/3lf6qkmUHjxfo4VZAF\nL/q2782OUs5Qt4/pYAIISzLdBw6XtTjZHirqa6YNrFvGucB3NG49AC0b1Z0acfrS\niimC2TvZpwunlLbyz2SQQL2c1zQ3U/Yfh2F1Zt8o6kK3RgKSSx57rK6nV7hXMGGp\nC4HV3nLetZg8HexicqeRANLXuUDbCSpN8K4nW5G2g/yKPfsQHBV/RWEDfhndykja\n+SmoY5IB+2zEbCC3MWiP9ZdIcCYOsq8wDZESMMW40DlVICjrf6UOqQ+ogci20qLS\nCmpgmOPAaBZJG/sBU79eHUSjPCK6yDpSyc30oVn8FnoBTmOpt7R++Ub8RJxReXBt\n+6o0NXYCJNaeVnk1bE4iavkJrXJCZvu44VBLS0WUs9W8TD4Iq8kNHsfQsfOuBXnQ\nncgoIe9HppnMGNoSzjYBNL/rprlbaOE55TkPqiQsiskRcaoeY53aTxoIykHmoj8G\nwJo/00IR+NYir7tr03Vriw+uywPPGucVJGWTUGsNbHlS5j941IltflIf6FitElNr\nJxVuJLgYiP3JhmWpdqA/uidYJMbIjunpn/8rVLrAil04SCSfUmaCdl7dkQ9x+3Mf\nErm699vIBQwvv0i+mcwKEvqSrhhNQ2F7vrb7NL8I2wUEPgQbv1PxSV6X0aYcxYVI\n-----END RSA PRIVATE KEY-----\n-- key_rsa_legacy.pub --\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCky7Clp8I3LVoqZWtat+QR6KmM0evFilmFhwenINIBbb8eS3ftDSkQy2YRrlAvO3h4EZffOIxANGL/yKVlRCIzvjsphi+tTHscZsQhwMnLEmxEayTq20hZKcwNA8TQdh2TW/w0KZmNZcxlTn4IK8W16komHcoH/qrRiXq8z3ROcfnv3Q4Hll9MUCwBkfy2DdBpWUMidQ1dAK4i3vXdseF74hJ0jFbPtS5mlpOsJZa0sdH1dnEl5M8wZS3PxyzM6JMkgzG7INp4sO/xGIisjl/QuSh2Fu93/EogdGXxIZChniUfzBx1DaHlerPPNSMP+uLbaOIAQrIPozhfdUdsCFDMoB7/PA6g1WVYZWAqjBZZW/GMOzPhih57NIFBSyMTzMi1KS6OBvYJvPf4IcvOa3May9ylLG/wZVhrHlQPbSsbRrraVtJ1P4gGQJ5U4d2AD2q+XtMb5f2i/holMXTVQl7Fa7RYi1TblDuW5OZCvmIawePBXAYbPg0OVFs3vAVEuAM=\n-- key_rsa_other.pub --\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQiCWw2W++gX4wcwpDo6QIouwQ9PPwCVe7QPICzxztG27mzeKRM4xT2LURGSaQqg7OYIUTGrLqNsaLZW+FHHQlRAVv1LEbdEFa5JermBMJ5j/HxamE/7oV60gMRlgKW+4IZhVMPgRZaaXU0YPb9oACdMNM8kPkc5JaOJ8iO6B1RViybjLD+tsEEPXLp3Mrj+sJqs+IvNlJKXdeefOjNrGmLHKIFdHiWlZ+aAW+QLfMQiNXoTbGybFUSpNEbmK/1ITiRAly94NoUK9LoriueXR+WJIm9wP4SfHw+hMBz1cywdF2wwKmWWegizV/USEmhyNXUzHZzjbkgE84DrIq+NA7SUmw6C8ClMjdnRnnoIyga99yMIrYMny1KW/bk1NK4u6Tv17E+FFOS3vf2Gcj01/jOmAUIQwL8MjAHhnsZ4XAA5NHa2NRGWm+hw7fx5uX42Gyz8HidFda5Lij1pASBcx4U3qwb62X+IVN50jGIP6kRNmGtMLY1JgaoGDDkw9r6mU=\n"
  },
  {
    "path": "cmd/age/testdata/hybrid.txt",
    "content": "# encrypt and decrypt a file with -r\nage -r age1pq1044e3hfmq2tltgck54m0gnpv8rfrwkdxjgl38psnwg8zfyv4uqdu63kk0u94ztt0q35k9dzuwgv405kxq6due2sre2kfsjftf7k2vz2mdpetku9pufy9d6ny6k5ss0cghpxdxrux0yd5xy40f8fu8ch9xn2rczyek2pqetqk0l9fqj0e3hfhkyfmcf9j6cpe5mf5stt9cymfq3xm276x4sramhn8srr42g78q9rkmqesevy33p4kdhkuggl99x9zx7j05uflx3s8q558xeaq9q9ctxqx52rzg6yffwykxngvxf7g3n83ddawagq2lnk59kcnexm54jrsmkjk9nemlge9v6p7fxu5z6yvq3ghpet8ww2c3h9fylvjx6j9njwz744376ap5r3xcpzh7qlk0kq3gknzfqhmxmjqypf6xz6geffdmy25apzsfr4ce0x825njkf62mgzx0x9n3zt0490u4dnt4468zse4zvd9sv0ycx2u2wsseuy4yymrg0nmfuxcwq9q0jykq7g5f59vunf95vnmcw336ksrh43fgapsd40u4227ymx56xtudtrte39twnq6fdfdj4p9pgx7pcqdk953za76fqcnvatx2cmywurag7ky4y8ecq5lvf25rydhxg6gmcn6vf78vtsqw79hp3mzv2u4v4jzn0nk50kl4gpv7k60cgz79c6k30jzy8dyetqtwg04tguxkv2p4hqj02yvsz2szaqdn9flu24rhth3v0t33023nwy27gt0x32eqpa5tpz6ygz3yzcsa3w889drpynzd5hlywuk9dwgafuax7upfnnxch4znrx4vactasg32u7vl886pxfswskk7wjgqkggrewtnaecfusl3nmg0xxh8e5nl5eft0xqwj5rk9ajn2j6yxehhgg5k6kpw5pk382hxmpu8wcd3rqx6m8905c7f8v0x2z9gyduxtqp2fdy9w2z4g2rrgu4d5cqes5tnwag007h2f326s3j9p6970xyd73rv5awx27zzezrax467c6vuye62ha88pzhw9xytjpezw3vg3l6x4k9rxwm9f6lwj6j7r9makg59rhgt0355s3jx9vw3raz5qdk5cnpny56q5h3dyd75suskuz63wmmfp9333tx4ry38vnh7jxy0j0cka99y7v2dmyjldjy0mqjetcefsmmg3pw0h9ll0rtskpmxz2cs8vmx6lu4v5rjtzpzgaqt46cwvnmhy5e3ye0tv6sre342lwg0tfd0yjkfz2haqc3hprpqf5ce8c9lx076dnk5cukn3tu38jgprrxt9sk9nqzt8grvkc4qzlg8jjf638v07ej9cf4eu48kphrgyjy83qm822xq8v9k22zwa7txr439x74j2f9ay7frkwtfdwn02enyx8stjpr4wgg25n5tj7uxyx0dqt3hnrqf7dcgt9fqnkjgemjfzyck6pawewptcayxm49sq3477f4cvm2p8fmg27rcmwm5l2vmzvdmymvkrr3aywu04y3zlhs7awu3r9u9lseheqpffvm2xf8y6w2qaj9g22pcu0sdsnqseusrxq9awru5lyqxnzftlslgpe3njegxkze98rveswp3u0xgx69wkg38vgp4k55lwwgv5l7ccv503mh9q23nud4xtm499xq276u40rxyrzaqfyuz2f3v5qr2vq00xs4g3ruynmq0c6nprvjcwqc5zrdn4cnzeeuwumjvqfjdpxr739n4cqcwytc8pjmfcj2qs3l2a8p5fzq7nvtng2jvvklx0x5ypnlkeuegsm587f6d4dkwel2es0lwwt88y2jdg8enjyph02tnjyndlx34h52fngjk96ggtu6vcxqkreqcxs4gwa8ww40t0kzlxfzyfhtjmfdm5fcrr5pt2xm4yslfynr0h9wkranhyadxf33kjn9xpg2fq5hnlq0 -o test.age input\nage -d -i key.txt test.age\ncmp stdout input\n! stderr .\n\n# encrypt and decrypt a file with -i\nage -e -i key.txt -o test.age input\nage -d -i key.txt test.age\ncmp stdout input\n! stderr .\n\n# encrypt and decrypt a file with the wrong key\nage -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -o test.age input\n! age -d -i key.txt test.age\nstderr 'no identity matched any of the recipients'\n\nage -r age1pq13tgqupu990y5qlwwjy4zrzwvxg54cq89x9pxzdjenv4cl7affsqeerksrc6ufw3ndp4h5p3l5hm2jekkd7uay2mlh9eqg3sjcwmv5dcrwguc8djf83qr8gswtdrpav3jpw47kgkh6z0rnfk92xyqeyce48t02n4qz68augry3r3nd4rpdsw998vnnxzd2vepypktx2wxhfvd56jw7p32te6ttcyhc7tkqy2ks77jtjl8cgyal43pcd3g9wkcsgytccqtcnzcucjjdadpr0v7ye8u9fqusq6cq2ah0e4ejrltc8sk6763d7mjkuchmc5tkx6xztygqjx4ldxqh7jzkkyuv6ywt07ys4x09eq7gat4wuznrvgs3pkxjju8t9n0gpfqrsv658m5v09ltqc3c5z30n43ld43dwe5dn8nsk89f43tzsw2zta5kvl95zv0a94tc5y9r4ncj8g63fgljzhdxl96eg39rgd56yuvr2xdvavkk93a85n99jpjeqkfveprqc7eqft0wp7qgxcgsrwcdymgp0cqaj93jp4ckln36ylcwdzew234esa2awty25zc6pmc0jcpkrfuuptr9z5a4kx9ff4ppfff2lw95um853tcg47fa444er9k9j4e8vy5hfjx626j3tz3kc94jl8r66ahfprwtcyxae3swe9xxjhx5dfnt53n5fs2ut83t597wscjg9jc4f6h4uen2ll2ne30w7uvts8vuc5mpcfpp9espa78y0f8588m62kzez5kxxtcxykdkrnjx08w5az3pr6g6dsykwazcxtetpkq85mg7kay84z58slmvxmq56342pm6v8y3evr5wvc9g4j87jfhmryymkz9wk2en2v6tfy83rz657t0w7p3va46jt22f29u9ggzt9nu6qtguw4cp7cpj572ccyenw5rdh43h6s54g73gvxapsq3hw5yfkgttcyhkrjf9ykzd3wm5zsqgp5fca0nr56dnqe3kqrszasagqne0a83cguzwj9aw456jaxg9rurq4n4f6qec4srd0rxqfqc6rw22c2p4xyg25pls4dmxvce2fah8m6shue84preky2h0c4sa9y9tjuzn20gr9aav96e689nqx23s7zz0lvhq925jdkn04cdpzfscvhh93vh9f2tmwkzsq7pq0ncjk8g8cu75u77lyej46a4m2fefy8v2x4nzay05kulhyzk69520r7fdg3fzh0z7ysequcltr94hyf88h46hfujk30x9jp8u0pcjywh06g7tv483ypmc3pm3x3z4h9ph66lx228t3r96hyt764yxe2rn5clnfxkj3k5wf5hffehj58y56qyykwayk6qt9skvctxw20v9xy5ppnld3lushqpsutxht7cqygypdn5d2ppdgaerqzgehyrpdwzkhu0qe8w3u5h6htz8aa5zfpzy4s7sl8zdv3q0gq8ez08p86e4v3m82882yvawrvyrcxewrznwkvvez8m5aj6ktgvynyy0kc2trmtjzvjlxdf06e3rjmf55lwxrucfwu2sxdtnte83fgq0tvr2juv3pfqp8ddsrnckzqcfcvjp02mfg5y4aqlsunxpfcrdm46fphwsslrudwrfh542xg62kphca6h3xxqn538pprkknt35y00ygvrse5mxpvnstvcrnak5qduhf5dqslkn0yeadgpq6tv4wzy98kdjdzp22cpq6dy3kve856y2qlk7elyqyzj7ezpnh3vjwwmcm7ctp23k2sct86jd46ztplsq5vdjg9lyspxt0k8qx5udu9lwgulzapn3kdg7yd2zdz5dqf9933mpzwc32x8uxn8h2v5hlhdd4qk0uvwxhxyul8keaw39pz2avywk3wfly6veet9pjnj7nqecrgz824whs9sf7wl2shxk9kvkteht4z9x3w2gc6hqz5y -o test.age input\n! age -d -i key.txt test.age\nstderr 'no identity matched any of the recipients'\n\n# cannot mix hybrid and X25519 recipients\n! age -r age1pq1044e3hfmq2tltgck54m0gnpv8rfrwkdxjgl38psnwg8zfyv4uqdu63kk0u94ztt0q35k9dzuwgv405kxq6due2sre2kfsjftf7k2vz2mdpetku9pufy9d6ny6k5ss0cghpxdxrux0yd5xy40f8fu8ch9xn2rczyek2pqetqk0l9fqj0e3hfhkyfmcf9j6cpe5mf5stt9cymfq3xm276x4sramhn8srr42g78q9rkmqesevy33p4kdhkuggl99x9zx7j05uflx3s8q558xeaq9q9ctxqx52rzg6yffwykxngvxf7g3n83ddawagq2lnk59kcnexm54jrsmkjk9nemlge9v6p7fxu5z6yvq3ghpet8ww2c3h9fylvjx6j9njwz744376ap5r3xcpzh7qlk0kq3gknzfqhmxmjqypf6xz6geffdmy25apzsfr4ce0x825njkf62mgzx0x9n3zt0490u4dnt4468zse4zvd9sv0ycx2u2wsseuy4yymrg0nmfuxcwq9q0jykq7g5f59vunf95vnmcw336ksrh43fgapsd40u4227ymx56xtudtrte39twnq6fdfdj4p9pgx7pcqdk953za76fqcnvatx2cmywurag7ky4y8ecq5lvf25rydhxg6gmcn6vf78vtsqw79hp3mzv2u4v4jzn0nk50kl4gpv7k60cgz79c6k30jzy8dyetqtwg04tguxkv2p4hqj02yvsz2szaqdn9flu24rhth3v0t33023nwy27gt0x32eqpa5tpz6ygz3yzcsa3w889drpynzd5hlywuk9dwgafuax7upfnnxch4znrx4vactasg32u7vl886pxfswskk7wjgqkggrewtnaecfusl3nmg0xxh8e5nl5eft0xqwj5rk9ajn2j6yxehhgg5k6kpw5pk382hxmpu8wcd3rqx6m8905c7f8v0x2z9gyduxtqp2fdy9w2z4g2rrgu4d5cqes5tnwag007h2f326s3j9p6970xyd73rv5awx27zzezrax467c6vuye62ha88pzhw9xytjpezw3vg3l6x4k9rxwm9f6lwj6j7r9makg59rhgt0355s3jx9vw3raz5qdk5cnpny56q5h3dyd75suskuz63wmmfp9333tx4ry38vnh7jxy0j0cka99y7v2dmyjldjy0mqjetcefsmmg3pw0h9ll0rtskpmxz2cs8vmx6lu4v5rjtzpzgaqt46cwvnmhy5e3ye0tv6sre342lwg0tfd0yjkfz2haqc3hprpqf5ce8c9lx076dnk5cukn3tu38jgprrxt9sk9nqzt8grvkc4qzlg8jjf638v07ej9cf4eu48kphrgyjy83qm822xq8v9k22zwa7txr439x74j2f9ay7frkwtfdwn02enyx8stjpr4wgg25n5tj7uxyx0dqt3hnrqf7dcgt9fqnkjgemjfzyck6pawewptcayxm49sq3477f4cvm2p8fmg27rcmwm5l2vmzvdmymvkrr3aywu04y3zlhs7awu3r9u9lseheqpffvm2xf8y6w2qaj9g22pcu0sdsnqseusrxq9awru5lyqxnzftlslgpe3njegxkze98rveswp3u0xgx69wkg38vgp4k55lwwgv5l7ccv503mh9q23nud4xtm499xq276u40rxyrzaqfyuz2f3v5qr2vq00xs4g3ruynmq0c6nprvjcwqc5zrdn4cnzeeuwumjvqfjdpxr739n4cqcwytc8pjmfcj2qs3l2a8p5fzq7nvtng2jvvklx0x5ypnlkeuegsm587f6d4dkwel2es0lwwt88y2jdg8enjyph02tnjyndlx34h52fngjk96ggtu6vcxqkreqcxs4gwa8ww40t0kzlxfzyfhtjmfdm5fcrr5pt2xm4yslfynr0h9wkranhyadxf33kjn9xpg2fq5hnlq0 -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -o test.age input\nstderr 'incompatible'\n\n! age -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -r age1pq1044e3hfmq2tltgck54m0gnpv8rfrwkdxjgl38psnwg8zfyv4uqdu63kk0u94ztt0q35k9dzuwgv405kxq6due2sre2kfsjftf7k2vz2mdpetku9pufy9d6ny6k5ss0cghpxdxrux0yd5xy40f8fu8ch9xn2rczyek2pqetqk0l9fqj0e3hfhkyfmcf9j6cpe5mf5stt9cymfq3xm276x4sramhn8srr42g78q9rkmqesevy33p4kdhkuggl99x9zx7j05uflx3s8q558xeaq9q9ctxqx52rzg6yffwykxngvxf7g3n83ddawagq2lnk59kcnexm54jrsmkjk9nemlge9v6p7fxu5z6yvq3ghpet8ww2c3h9fylvjx6j9njwz744376ap5r3xcpzh7qlk0kq3gknzfqhmxmjqypf6xz6geffdmy25apzsfr4ce0x825njkf62mgzx0x9n3zt0490u4dnt4468zse4zvd9sv0ycx2u2wsseuy4yymrg0nmfuxcwq9q0jykq7g5f59vunf95vnmcw336ksrh43fgapsd40u4227ymx56xtudtrte39twnq6fdfdj4p9pgx7pcqdk953za76fqcnvatx2cmywurag7ky4y8ecq5lvf25rydhxg6gmcn6vf78vtsqw79hp3mzv2u4v4jzn0nk50kl4gpv7k60cgz79c6k30jzy8dyetqtwg04tguxkv2p4hqj02yvsz2szaqdn9flu24rhth3v0t33023nwy27gt0x32eqpa5tpz6ygz3yzcsa3w889drpynzd5hlywuk9dwgafuax7upfnnxch4znrx4vactasg32u7vl886pxfswskk7wjgqkggrewtnaecfusl3nmg0xxh8e5nl5eft0xqwj5rk9ajn2j6yxehhgg5k6kpw5pk382hxmpu8wcd3rqx6m8905c7f8v0x2z9gyduxtqp2fdy9w2z4g2rrgu4d5cqes5tnwag007h2f326s3j9p6970xyd73rv5awx27zzezrax467c6vuye62ha88pzhw9xytjpezw3vg3l6x4k9rxwm9f6lwj6j7r9makg59rhgt0355s3jx9vw3raz5qdk5cnpny56q5h3dyd75suskuz63wmmfp9333tx4ry38vnh7jxy0j0cka99y7v2dmyjldjy0mqjetcefsmmg3pw0h9ll0rtskpmxz2cs8vmx6lu4v5rjtzpzgaqt46cwvnmhy5e3ye0tv6sre342lwg0tfd0yjkfz2haqc3hprpqf5ce8c9lx076dnk5cukn3tu38jgprrxt9sk9nqzt8grvkc4qzlg8jjf638v07ej9cf4eu48kphrgyjy83qm822xq8v9k22zwa7txr439x74j2f9ay7frkwtfdwn02enyx8stjpr4wgg25n5tj7uxyx0dqt3hnrqf7dcgt9fqnkjgemjfzyck6pawewptcayxm49sq3477f4cvm2p8fmg27rcmwm5l2vmzvdmymvkrr3aywu04y3zlhs7awu3r9u9lseheqpffvm2xf8y6w2qaj9g22pcu0sdsnqseusrxq9awru5lyqxnzftlslgpe3njegxkze98rveswp3u0xgx69wkg38vgp4k55lwwgv5l7ccv503mh9q23nud4xtm499xq276u40rxyrzaqfyuz2f3v5qr2vq00xs4g3ruynmq0c6nprvjcwqc5zrdn4cnzeeuwumjvqfjdpxr739n4cqcwytc8pjmfcj2qs3l2a8p5fzq7nvtng2jvvklx0x5ypnlkeuegsm587f6d4dkwel2es0lwwt88y2jdg8enjyph02tnjyndlx34h52fngjk96ggtu6vcxqkreqcxs4gwa8ww40t0kzlxfzyfhtjmfdm5fcrr5pt2xm4yslfynr0h9wkranhyadxf33kjn9xpg2fq5hnlq0 -o test.age input\nstderr 'incompatible'\n\n# convert to plugin identity and use plugin\nexec age-plugin-pq -identity -o key-plugin.txt key.txt\n\nage -e -i key.txt -o test.age input\nage -d -i key-plugin.txt test.age\ncmp stdout input\n! stderr .\n\nage -e -i key-plugin.txt -o test.age input\nage -d -i key.txt test.age\ncmp stdout input\n! stderr .\n\n-- input --\ntest\n-- key.txt --\n# created: 2025-11-17T13:27:37+01:00\n# public key: age1pq1044e3hfmq2tltgck54m0gnpv8rfrwkdxjgl38psnwg8zfyv4uqdu63kk0u94ztt0q35k9dzuwgv405kxq6due2sre2kfsjftf7k2vz2mdpetku9pufy9d6ny6k5ss0cghpxdxrux0yd5xy40f8fu8ch9xn2rczyek2pqetqk0l9fqj0e3hfhkyfmcf9j6cpe5mf5stt9cymfq3xm276x4sramhn8srr42g78q9rkmqesevy33p4kdhkuggl99x9zx7j05uflx3s8q558xeaq9q9ctxqx52rzg6yffwykxngvxf7g3n83ddawagq2lnk59kcnexm54jrsmkjk9nemlge9v6p7fxu5z6yvq3ghpet8ww2c3h9fylvjx6j9njwz744376ap5r3xcpzh7qlk0kq3gknzfqhmxmjqypf6xz6geffdmy25apzsfr4ce0x825njkf62mgzx0x9n3zt0490u4dnt4468zse4zvd9sv0ycx2u2wsseuy4yymrg0nmfuxcwq9q0jykq7g5f59vunf95vnmcw336ksrh43fgapsd40u4227ymx56xtudtrte39twnq6fdfdj4p9pgx7pcqdk953za76fqcnvatx2cmywurag7ky4y8ecq5lvf25rydhxg6gmcn6vf78vtsqw79hp3mzv2u4v4jzn0nk50kl4gpv7k60cgz79c6k30jzy8dyetqtwg04tguxkv2p4hqj02yvsz2szaqdn9flu24rhth3v0t33023nwy27gt0x32eqpa5tpz6ygz3yzcsa3w889drpynzd5hlywuk9dwgafuax7upfnnxch4znrx4vactasg32u7vl886pxfswskk7wjgqkggrewtnaecfusl3nmg0xxh8e5nl5eft0xqwj5rk9ajn2j6yxehhgg5k6kpw5pk382hxmpu8wcd3rqx6m8905c7f8v0x2z9gyduxtqp2fdy9w2z4g2rrgu4d5cqes5tnwag007h2f326s3j9p6970xyd73rv5awx27zzezrax467c6vuye62ha88pzhw9xytjpezw3vg3l6x4k9rxwm9f6lwj6j7r9makg59rhgt0355s3jx9vw3raz5qdk5cnpny56q5h3dyd75suskuz63wmmfp9333tx4ry38vnh7jxy0j0cka99y7v2dmyjldjy0mqjetcefsmmg3pw0h9ll0rtskpmxz2cs8vmx6lu4v5rjtzpzgaqt46cwvnmhy5e3ye0tv6sre342lwg0tfd0yjkfz2haqc3hprpqf5ce8c9lx076dnk5cukn3tu38jgprrxt9sk9nqzt8grvkc4qzlg8jjf638v07ej9cf4eu48kphrgyjy83qm822xq8v9k22zwa7txr439x74j2f9ay7frkwtfdwn02enyx8stjpr4wgg25n5tj7uxyx0dqt3hnrqf7dcgt9fqnkjgemjfzyck6pawewptcayxm49sq3477f4cvm2p8fmg27rcmwm5l2vmzvdmymvkrr3aywu04y3zlhs7awu3r9u9lseheqpffvm2xf8y6w2qaj9g22pcu0sdsnqseusrxq9awru5lyqxnzftlslgpe3njegxkze98rveswp3u0xgx69wkg38vgp4k55lwwgv5l7ccv503mh9q23nud4xtm499xq276u40rxyrzaqfyuz2f3v5qr2vq00xs4g3ruynmq0c6nprvjcwqc5zrdn4cnzeeuwumjvqfjdpxr739n4cqcwytc8pjmfcj2qs3l2a8p5fzq7nvtng2jvvklx0x5ypnlkeuegsm587f6d4dkwel2es0lwwt88y2jdg8enjyph02tnjyndlx34h52fngjk96ggtu6vcxqkreqcxs4gwa8ww40t0kzlxfzyfhtjmfdm5fcrr5pt2xm4yslfynr0h9wkranhyadxf33kjn9xpg2fq5hnlq0\nAGE-SECRET-KEY-PQ-16XDSLZ3XCSZ3236YJ5J0T9NAPLZTP96LJKQCVHJYUDTQVJR5J5PQTDPQCX\n"
  },
  {
    "path": "cmd/age/testdata/keygen.txt",
    "content": "exec age-keygen\nstdout '# created: 20'\nstdout '# public key: age1'\nstdout 'AGE-SECRET-KEY-1'\nstderr 'Public key: age1'\n\nexec age-keygen -pq\nstdout '# created: 20'\nstdout '# public key: age1pq1'\nstdout 'AGE-SECRET-KEY-PQ-1'\nstderr 'Public key: age1pq1'\n\nexec age-keygen -pq -o key.txt\n! stdout .\nstderr 'Public key: age1pq1'\ngrep '# created: 20' key.txt\ngrep '# public key: age1pq1' key.txt\ngrep 'AGE-SECRET-KEY-PQ-1' key.txt\n\nstdin key.txt\nexec age-keygen -y\nstdout age1pq1\n\nexec age-keygen -y key.txt\nstdout age1pq1\n"
  },
  {
    "path": "cmd/age/testdata/output_file.txt",
    "content": "# https://github.com/FiloSottile/age/issues/57\nage -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input\n! age -o test.out -d -i wrong.txt test.age\n! exists test.out\n! age -o test.out -d test.age\n! exists test.out\n! age -o test.out -d -i notexist test.age\n! exists test.out\n! age -o test.out -d -i wrong.txt notexist\n! exists test.out\n! age -o test.out -r BAD\n! exists test.out\n! age -o test.out -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef notexist\n! exists test.out\n! age -o test.out -p notexist\n! exists test.out\n\n# https://github.com/FiloSottile/age/issues/555\nage -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o empty.age empty\nexists empty.age\nage -d -i key.txt empty.age\n! stdout .\n! stderr .\nage -d -i key.txt -o new empty.age\n! stderr .\ncmp new empty\n\n# https://github.com/FiloSottile/age/issues/491\ncp input inputcopy\n! age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o inputcopy inputcopy\nstderr 'input and output file are the same'\ncmp inputcopy input\n! age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o ./inputcopy inputcopy\nstderr 'input and output file are the same'\ncmp inputcopy input\nmkdir foo\n! age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o inputcopy foo/../inputcopy\nstderr 'input and output file are the same'\ncmp inputcopy input\ncp key.txt keycopy\nage -e -i keycopy -o test.age input\n! age -d -i keycopy -o keycopy test.age\nstderr 'input and output file are the same'\ncmp key.txt keycopy\n\n[!linux] [!darwin] skip # no pty support\n[darwin] [go1.20] skip # https://go.dev/issue/61779\n\nttyin terminal\n! age -p -o inputcopy inputcopy\nstderr 'input and output file are the same'\ncmp inputcopy input\n\n# https://github.com/FiloSottile/age/issues/159\nttyin terminal\nage -p -a -o test.age input\nttyin terminalwrong\n! age -o test.out -d test.age\nttyout 'Enter passphrase'\nstderr 'incorrect passphrase'\n! exists test.out\n\n-- terminal --\npassword\npassword\n-- terminalwrong --\nwrong\n-- input --\nage\n-- empty --\n-- key.txt --\n# created: 2021-02-02T13:09:43+01:00\n# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef\nAGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0\n-- wrong.txt --\n# created: 2024-06-16T12:14:00+02:00\n# public key: age10k7vsqmeg3sp8elfyq5ts55feg4huarpcaf9dmljn9umydg3gymsvx4dp9\nAGE-SECRET-KEY-1NPX08S4LELW9K68FKU0U05XXEKG6X7GT004TPNYLF86H3M00D3FQ3VQQNN\n"
  },
  {
    "path": "cmd/age/testdata/pkcs8.txt",
    "content": "# https://github.com/FiloSottile/age/discussions/428\n# encrypt and decrypt a file with an Ed25519 key encoded with PKCS#8\nage -e -i key.pem -o test.age input\nage -d -i key.pem test.age\ncmp stdout input\n! stderr .\n\n-- input --\ntest\n-- key.pem --\n-----BEGIN PRIVATE KEY-----\nMC4CAQAwBQYDK2VwBCIEIJT4Wpo+YG11yybKL/bYXQW7ekz4PAsmV/4tfmY1vU7x\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "cmd/age/testdata/plugin.txt",
    "content": "# encrypt and decrypt a file with a test plugin\nage -r age1test10qdmzv9q -o test.age input\nage -d -i key.txt test.age\ncmp stdout input\n! stderr .\n\n# very long identity and recipient\nage -R long-recipient.txt -o test.age input\nage -d -i long-key.txt test.age\ncmp stdout input\n! stderr .\n\n# check that path separators are rejected\nchmod 755 age-plugin-pwn/pwn\nmkdir $TMPDIR/age-plugin-pwn\ncp age-plugin-pwn/pwn $TMPDIR/age-plugin-pwn/pwn\n! age -r age1pwn/pwn19gt89dfz input\n! age -d -i pwn-identity.txt test.age\n! age -d -j pwn/pwn test.age\n! exists pwn\n\n# check plugin not found hint\n! age -r age1nonexistentplugin1pt5d8z -o test1.age\nstderr /awesome#plugins\n! age -d -i nonexistent-identity.txt test.age\nstderr /awesome#plugins\n\n-- input --\ntest\n-- key.txt --\nAGE-PLUGIN-TEST-10Q32NLXM\n-- long-recipient.txt --\nage1test10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qj6rl8p\n-- long-key.txt --\nAGE-PLUGIN-TEST-10PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7Q5U8SUD\n-- pwn-identity.txt --\nAGE-PLUGIN-PWN/PWN-19GYK4WLY\n-- age-plugin-pwn/pwn --\n#!/bin/sh\ntouch \"$WORK/pwn\"\n-- nonexistent-identity.txt --\nAGE-PLUGIN-NONEXISTENTPLUGIN-1R4XFW4\n"
  },
  {
    "path": "cmd/age/testdata/rsa.txt",
    "content": "# encrypt and decrypt a file with -R\nage -R key.pem.pub -o test.age input\nage -d -i key.pem test.age\ncmp stdout input\n! stderr .\n\n# encrypt and decrypt a file with -i\nage -e -i key.pem -o test.age input\nage -d -i key.pem test.age\ncmp stdout input\n! stderr .\n\n# encrypt and decrypt a file with the wrong key\nage -R otherkey.pem.pub -o test.age input\n! age -d -i key.pem test.age\nstderr 'no identity matched any of the recipients'\n\n-- input --\ntest\n-- key.pem --\n-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAYEA1C04rdClHoW4oG4bEGmaNqFy4DLoPJ0358w4XH+XBM3TiWcheouW\nkUG6m1yDmHk0t0oaaf4hOnetKovdyQQX73gGaq++rSu5VSvH7LbwABoG6PS/UbuZ4Vl9B0\n5WVDqHVE9hNK4AHqBc373GU2mo8z5opKxEprmiS3HSd3K2wiMqL5E8XPOSm0p/isuYK57X\nVUexl73tB7iIMLklxjcjtP4REMoQhHKOMOdy2Q15dw5cYG+drtEArBRYkCZmd0Vp2ws9pj\nYzPVaOSkbdqSeLu+JVbH1wrwKhuBrA3eVlwjUTWkO4FHcNXkp773Mt4cXhKizTfbR2hQox\nLsj31301Xd7dEpV63sqDW1e+a2L2dhemi8cjDMrPuW6Z19Lbti0quAb4+cSLAaJI4BHd1F\n8o9XhK7EHVCdIIIQDKVzo1WyEsDwBjL1LB9rpxm4732sZyue0uygFzmM544QX+WsiJXgHP\nuC1Q/ynjLRm6ZMl16MwvY8B/XGQWxlOAbRJQG84fAAAFmEwAjV1MAI1dAAAAB3NzaC1yc2\nEAAAGBANQtOK3QpR6FuKBuGxBpmjahcuAy6DydN+fMOFx/lwTN04lnIXqLlpFBuptcg5h5\nNLdKGmn+ITp3rSqL3ckEF+94Bmqvvq0ruVUrx+y28AAaBuj0v1G7meFZfQdOVlQ6h1RPYT\nSuAB6gXN+9xlNpqPM+aKSsRKa5oktx0ndytsIjKi+RPFzzkptKf4rLmCue11VHsZe97Qe4\niDC5JcY3I7T+ERDKEIRyjjDnctkNeXcOXGBvna7RAKwUWJAmZndFadsLPaY2Mz1WjkpG3a\nkni7viVWx9cK8CobgawN3lZcI1E1pDuBR3DV5Ke+9zLeHF4Sos0320doUKMS7I99d9NV3e\n3RKVet7Kg1tXvmti9nYXpovHIwzKz7lumdfS27YtKrgG+PnEiwGiSOAR3dRfKPV4SuxB1Q\nnSCCEAylc6NVshLA8AYy9Swfa6cZuO99rGcrntLsoBc5jOeOEF/lrIiV4Bz7gtUP8p4y0Z\numTJdejML2PAf1xkFsZTgG0SUBvOHwAAAAMBAAEAAAGBAKytAOu0Wi009sTZ1vzMdMzxJ+\nR+ibKK4Oysr1HYJLesKvQwEncBE1C0BYJbEF4OhnCExmpsf+5tZ2iw25a01iX1sIMy9CNK\n6lH+h36Gg1wR0n3Ucb+6xck4YyCHCIsT9v8OezW8Riympe8RK07HNtB/gfpCmLx3ZzWvNH\nIx0bq9k5+Su2WKdU4cmyACAZ2+b9DfwBCWaUlXTL8abzuZtF2gR5M6X6bq8/2o3zb2WFwk\nO9nf/JxBTCK/jDQEjG+U9MyGxZIW5DeG1nNFtOzJoT8krIkeSOjQ5XQrkjCw+yihSCWMG+\ns+SKO77u30SO7OCENsFIXpUzpt6+JmazlXjLW/OdYNooQMHtqCZzVMRgxiy3gDGF35YvgV\nVnP5gVEW9HEZ0kD+x4Rl2kB6bV7jMi8BXrazQ1EmTasJFg1pv6iRJWzY1JoP2kRfgiHGL6\nOqgrXakqo3hMJuz+JRU2/hlF13743MiIxpcbaaRqURoWuNRLHitVWE35/XVCez0C6OwQAA\nAMEAoh106+3JbiZI19iRICR247IoOLpSSed98eQj+l3OYfJ86cQipSjxdSPWcP58yyyElY\nd9q6K16sDTLAlRJzF7MFxSc80JY6RgFq/Sy4Jm0/Z10wwJhTgOkxq6IynzLnO7goRirE31\njxGif4nI2IYEQvv6MOD8TWA4axxGMw2StYB6P4R5peozf81oR6m79ERIDSkrm0RYYn931r\ngVuxvo3ABVxMtg1lV80LJMayy87Oi8BehGBxMBgsKtQaH8+5h7AAAAwQD+8lJpBcrrHQKk\n3o2XAZxB5Fool4f2iuZWTxA1vq0/TCUcEodrdWfLuDeVbDsFemW0vBSkKzf4NlZSs2DAKl\nYWT6y18eyDyJXn0TNVTeO3F5mkkX5spqbjDcESSs3whIuDqXU++3sII7iMzGw50tDP4Dw6\nTViEVM3anpeqlAbkciR5o9IJx3nRcGh81Bs4gticcRF0vqiJoAhNlSZXR1XMjevwt68i+4\nRKPPQsTM7uJLm236VUhDivO1OJcBTLW7MAAADBANUNqH+//G4gIruBO3BsIvbzDw0DgRam\nR1tqqn4g53boiv1RPtUJ2GbkCsisy5pU+JdTN7ekFEF8KWuunjImkfVyAiTFsHHmOoXV3Z\nEX0mNDXOlKOP2YAIMrDt5CkPdEh6qQG21LCZXTWmwheZ9iN2vOl/fKqUW9lqd/kTe6WsON\nhIpZhs2+oz54Riq1ZwzO9NkcYrvZoDKbDopL1r2ibw0mkgCJrxpWi0Yt2Iooh4GXXqP5C9\nT8hrZCbrVJkjKd5QAAABtmaWxpcHBvQEJpc3Ryb21hdGgtTTEubG9jYWwBAgMEBQY=\n-----END OPENSSH PRIVATE KEY-----\n-- key.pem.pub --\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbigbhsQaZo2oXLgMug8nTfnzDhcf5cEzdOJZyF6i5aRQbqbXIOYeTS3Shpp/iE6d60qi93JBBfveAZqr76tK7lVK8fstvAAGgbo9L9Ru5nhWX0HTlZUOodUT2E0rgAeoFzfvcZTaajzPmikrESmuaJLcdJ3crbCIyovkTxc85KbSn+Ky5grntdVR7GXve0HuIgwuSXGNyO0/hEQyhCEco4w53LZDXl3Dlxgb52u0QCsFFiQJmZ3RWnbCz2mNjM9Vo5KRt2pJ4u74lVsfXCvAqG4GsDd5WXCNRNaQ7gUdw1eSnvvcy3hxeEqLNN9tHaFCjEuyPfXfTVd3t0SlXreyoNbV75rYvZ2F6aLxyMMys+5bpnX0tu2LSq4Bvj5xIsBokjgEd3UXyj1eErsQdUJ0gghAMpXOjVbISwPAGMvUsH2unGbjvfaxnK57S7KAXOYznjhBf5ayIleAc+4LVD/KeMtGbpkyXXozC9jwH9cZBbGU4BtElAbzh8=\n-- otherkey.pem.pub --\nssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF0OPu95EY25O5KmYFLIkiZZFKUlfvaRgmfIT6OcZvPRXBzo0MS/lcrYvAc0RsUVbZ1B3Y9oWmKt/IMXTztCXiza70rO1NI7ciayv5svY/wGMoveutddhA64IjrQKs4m+6Qmjs/dYTnfsk1BzmXrdRKUSqH6c4Id7pRLC1ySLu+4og3nTTpBRBpg+uSkc4Ua6ce6A6RX14PPJ+TAXMfZyKNyaubQhgzLB/CfdXxZqWdAnyooiE7fb6CEB5uppnA5BpPdcWAkSixbwxRHbRC+OSCqMOV6+z+NlO/qSOKJcXfCQnJP/qjJTJde0dYhXG4RILOzIkGVieGJJONDXvj61mMj568IhJz0AEf/UMhvEL79iJ6yZW82Go/zcYkDDfd3KRE3pW+6p9Onu3XqOiQABS+9rEVRBnqYsPajiHBIanBeXpWKGbjznakvxhdRifhOWwAsQDfLmGzh+JnV1vOUjyxKtLNv9zi/oeuYCaIyF7F6en8LMbYSz8YONMZygGxMU=\n"
  },
  {
    "path": "cmd/age/testdata/scrypt.txt",
    "content": "[!linux] [!darwin] skip # no pty support\n[darwin] [go1.20] skip # https://go.dev/issue/61779\n\n# encrypt with a provided passphrase\nstdin input\nttyin terminal\nage -p -o test.age\nttyout 'Enter passphrase'\n! stderr .\n! stdout .\n\n# decrypt with a provided passphrase\nttyin terminal\nage -d test.age\nttyout 'Enter passphrase'\n! stderr .\ncmp stdout input\n\n# decrypt with the wrong passphrase\nttyin wrong\n! age -d test.age\nstderr 'incorrect passphrase'\n\n# encrypt with a generated passphrase\nstdin input\nttyin empty\nage -p -o test.age\n! stderr .\n! stdout .\nttyin autogenerated\nage -d test.age\ncmp stdout input\n\n# fail when -i is present\nttyin terminal\n! age -d -i key.txt test.age\nstderr 'file is passphrase-encrypted but identities were specified'\n\n# fail when passphrases don't match\nttyin wrong\n! age -p -o fail.age\nstderr 'passphrases didn''t match'\n! exists fail.age\n\n# fail when -i is missing\nage -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input\n! age -d test.age\nstderr 'file is not passphrase-encrypted, identities are required'\n\n-- terminal --\npassword\npassword\n-- wrong --\nPASSWORD\npassword\n-- input --\ntest\n-- key.txt --\n# created: 2021-02-02T13:09:43+01:00\n# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef\nAGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0\n-- autogenerated --\nfour-four-four-four-four-four-four-four-four-four\n-- empty --\n\n"
  },
  {
    "path": "cmd/age/testdata/terminal.txt",
    "content": "[!linux] [!darwin] skip # no pty support\n[darwin] [go1.20] skip # https://go.dev/issue/61779\n\n# controlling terminal is used instead of stdin/stderr\nttyin terminal\nage -p -o test.age input\n! stderr .\n\n# autogenerated passphrase is printed to terminal\nttyin empty\nage -p -o test.age input\nttyout 'autogenerated passphrase'\n! stderr .\n\n# with no controlling terminal, stdin terminal is used\n## TODO: enable once https://golang.org/issue/53601 is fixed\n## and Noctty is added to testscript.\n# noctty\n# ttyin -stdin terminal\n# age -p -o test.age input\n# ! stderr .\n\n# no terminal causes an error\n## TODO: enable once https://golang.org/issue/53601 is fixed\n## and Noctty is added to testscript.\n# noctty\n# ! age -p -o test.age input\n# stderr 'standard input is not a terminal'\n\n# prompt for password before plaintext if stdin is the terminal\nexec cat terminal input # concatenated password + input\nttyin -stdin stdout\nage -p -a -o test.age\nttyout 'Enter passphrase'\n! stderr .\n# check the file was encrypted correctly\nttyin terminal\nage -d test.age\ncmp stdout input\n\n# buffer armored ciphertext before prompting if stdin is the terminal\nttyin terminal\nage -p -a -o test.age input\nexec cat test.age terminal # concatenated ciphertext + password\nttyin -stdin stdout\nage -d\nttyout 'Enter passphrase'\n! stderr .\ncmp stdout input\n\n-- input --\ntest\n-- terminal --\npassword\npassword\n-- empty --\n\n"
  },
  {
    "path": "cmd/age/testdata/usage.txt",
    "content": "# -help\nage -p -help\n! stdout .\nstderr 'Usage:'\n\n# -h\nage -p -h\n! stdout .\nstderr 'Usage:'\n\n# unknown flag\n! age -p -this-flag-does-not-exist\n! stdout .\nstderr 'flag provided but not defined'\nstderr 'Usage:'\n\n# no arguments\n! age\n! stdout .\nstderr 'Usage:'\n"
  },
  {
    "path": "cmd/age/testdata/x25519.txt",
    "content": "# encrypt and decrypt a file with -r\nage -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input\nage -d -i key.txt test.age\ncmp stdout input\n! stderr .\n\n# encrypt and decrypt a file with -i\nage -e -i key.txt -o test.age input\nage -d -i key.txt test.age\ncmp stdout input\n! stderr .\n\n# encrypt and decrypt a file with the wrong key\nage -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -o test.age input\n! age -d -i key.txt test.age\nstderr 'no identity matched any of the recipients'\n\n# decrypt an empty file\n! age -d -i key.txt empty\nstderr empty\n\n-- empty --\n-- input --\ntest\n-- key.txt --\n# created: 2021-02-02T13:09:43+01:00\n# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef\nAGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0\n"
  },
  {
    "path": "cmd/age/tui.go",
    "content": "// Copyright 2021 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\n// This file implements the terminal UI of cmd/age. The rules are:\n//\n//   - Anything that requires user interaction goes to the terminal,\n//     and is erased afterwards if possible. This UI would be possible\n//     to replace with a pinentry with no output or UX changes.\n//\n//   - Everything else goes to standard error with an \"age:\" prefix.\n//     No capitalized initials and no periods at the end.\n//\n// The one exception is the autogenerated passphrase, which goes to\n// the terminal, since we really want it to reach the user only.\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\n\t\"filippo.io/age/armor\"\n\t\"filippo.io/age/internal/term\"\n)\n\n// l is a logger with no prefixes.\nvar l = log.New(os.Stderr, \"\", 0)\n\nfunc printf(format string, v ...any) {\n\tl.Printf(\"age: \"+format, v...)\n}\n\nfunc errorf(format string, v ...any) {\n\tl.Printf(\"age: error: \"+format, v...)\n\tl.Printf(\"age: report unexpected or unhelpful errors at https://filippo.io/age/report\")\n\tos.Exit(1)\n}\n\nfunc warningf(format string, v ...any) {\n\tl.Printf(\"age: warning: \"+format, v...)\n}\n\nfunc errorWithHint(error string, hints ...string) {\n\tl.Printf(\"age: error: %s\", error)\n\tfor _, hint := range hints {\n\t\tl.Printf(\"age: hint: %s\", hint)\n\t}\n\tl.Printf(\"age: report unexpected or unhelpful errors at https://filippo.io/age/report\")\n\tos.Exit(1)\n}\n\nfunc printfToTerminal(format string, v ...any) error {\n\treturn term.WithTerminal(func(_, out *os.File) error {\n\t\t_, err := fmt.Fprintf(out, \"age: \"+format+\"\\n\", v...)\n\t\treturn err\n\t})\n}\n\nfunc bufferTerminalInput(in io.Reader) (io.Reader, error) {\n\tbuf := &bytes.Buffer{}\n\tif _, err := buf.ReadFrom(ReaderFunc(func(p []byte) (n int, err error) {\n\t\tif bytes.Contains(buf.Bytes(), []byte(armor.Footer+\"\\n\")) {\n\t\t\treturn 0, io.EOF\n\t\t}\n\t\treturn in.Read(p)\n\t})); err != nil {\n\t\treturn nil, err\n\t}\n\treturn buf, nil\n}\n\ntype ReaderFunc func(p []byte) (n int, err error)\n\nfunc (f ReaderFunc) Read(p []byte) (n int, err error) { return f(p) }\n"
  },
  {
    "path": "cmd/age/wordlist.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/binary\"\n\t\"strings\"\n)\n\nvar testOnlyFixedRandomWord string\n\nfunc randomWord() string {\n\tif testOnlyFixedRandomWord != \"\" {\n\t\treturn testOnlyFixedRandomWord\n\t}\n\tbuf := make([]byte, 2)\n\tif _, err := rand.Read(buf); err != nil {\n\t\tpanic(err)\n\t}\n\tn := binary.BigEndian.Uint16(buf)\n\treturn wordlist[int(n)%2048]\n}\n\n// wordlist is the BIP39 list of 2048 english words, and it's used to generate\n// the suggested passphrases.\nvar wordlist = strings.Split(`abandon ability able about above absent absorb abstract absurd abuse access accident account accuse achieve acid acoustic acquire across act action actor actress actual adapt add addict address adjust admit adult advance advice aerobic affair afford afraid again age agent agree ahead aim air airport aisle alarm album alcohol alert alien all alley allow almost alone alpha already also alter always amateur amazing among amount amused analyst anchor ancient anger angle angry animal ankle announce annual another answer antenna antique anxiety any apart apology appear apple approve april arch arctic area arena argue arm armed armor army around arrange arrest arrive arrow art artefact artist artwork ask aspect assault asset assist assume asthma athlete atom attack attend attitude attract auction audit august aunt author auto autumn average avocado avoid awake aware away awesome awful awkward axis baby bachelor bacon badge bag balance balcony ball bamboo banana banner bar barely bargain barrel base basic basket battle beach bean beauty because become beef before begin behave behind believe below belt bench benefit best betray better between beyond bicycle bid bike bind biology bird birth bitter black blade blame blanket blast bleak bless blind blood blossom blouse blue blur blush board boat body boil bomb bone bonus book boost border boring borrow boss bottom bounce box boy bracket brain brand brass brave bread breeze brick bridge brief bright bring brisk broccoli broken bronze broom brother brown brush bubble buddy budget buffalo build bulb bulk bullet bundle bunker burden burger burst bus business busy butter buyer buzz cabbage cabin cable cactus cage cake call calm camera camp can canal cancel candy cannon canoe canvas canyon capable capital captain car carbon card cargo carpet carry cart case cash casino castle casual cat catalog catch category cattle caught cause caution cave ceiling celery cement census century cereal certain chair chalk champion change chaos chapter charge chase chat cheap check cheese chef cherry chest chicken chief child chimney choice choose chronic chuckle chunk churn cigar cinnamon circle citizen city civil claim clap clarify claw clay clean clerk clever click client cliff climb clinic clip clock clog close cloth cloud clown club clump cluster clutch coach coast coconut code coffee coil coin collect color column combine come comfort comic common company concert conduct confirm congress connect consider control convince cook cool copper copy coral core corn correct cost cotton couch country couple course cousin cover coyote crack cradle craft cram crane crash crater crawl crazy cream credit creek crew cricket crime crisp critic crop cross crouch crowd crucial cruel cruise crumble crunch crush cry crystal cube culture cup cupboard curious current curtain curve cushion custom cute cycle dad damage damp dance danger daring dash daughter dawn day deal debate debris decade december decide decline decorate decrease deer defense define defy degree delay deliver demand demise denial dentist deny depart depend deposit depth deputy derive describe desert design desk despair destroy detail detect develop device devote diagram dial diamond diary dice diesel diet differ digital dignity dilemma dinner dinosaur direct dirt disagree discover disease dish dismiss disorder display distance divert divide divorce dizzy doctor document dog doll dolphin domain donate donkey donor door dose double dove draft dragon drama drastic draw dream dress drift drill drink drip drive drop drum dry duck dumb dune during dust dutch duty dwarf dynamic eager eagle early earn earth easily east easy echo ecology economy edge edit educate effort egg eight either elbow elder electric elegant element elephant elevator elite else embark embody embrace emerge emotion employ empower empty enable enact end endless endorse enemy energy enforce engage engine enhance enjoy enlist enough enrich enroll ensure enter entire entry envelope episode equal equip era erase erode erosion error erupt escape essay essence estate eternal ethics evidence evil evoke evolve exact example excess exchange excite exclude excuse execute exercise exhaust exhibit exile exist exit exotic expand expect expire explain expose express extend extra eye eyebrow fabric face faculty fade faint faith fall false fame family famous fan fancy fantasy farm fashion fat fatal father fatigue fault favorite feature february federal fee feed feel female fence festival fetch fever few fiber fiction field figure file film filter final find fine finger finish fire firm first fiscal fish fit fitness fix flag flame flash flat flavor flee flight flip float flock floor flower fluid flush fly foam focus fog foil fold follow food foot force forest forget fork fortune forum forward fossil foster found fox fragile frame frequent fresh friend fringe frog front frost frown frozen fruit fuel fun funny furnace fury future gadget gain galaxy gallery game gap garage garbage garden garlic garment gas gasp gate gather gauge gaze general genius genre gentle genuine gesture ghost giant gift giggle ginger giraffe girl give glad glance glare glass glide glimpse globe gloom glory glove glow glue goat goddess gold good goose gorilla gospel gossip govern gown grab grace grain grant grape grass gravity great green grid grief grit grocery group grow grunt guard guess guide guilt guitar gun gym habit hair half hammer hamster hand happy harbor hard harsh harvest hat have hawk hazard head health heart heavy hedgehog height hello helmet help hen hero hidden high hill hint hip hire history hobby hockey hold hole holiday hollow home honey hood hope horn horror horse hospital host hotel hour hover hub huge human humble humor hundred hungry hunt hurdle hurry hurt husband hybrid ice icon idea identify idle ignore ill illegal illness image imitate immense immune impact impose improve impulse inch include income increase index indicate indoor industry infant inflict inform inhale inherit initial inject injury inmate inner innocent input inquiry insane insect inside inspire install intact interest into invest invite involve iron island isolate issue item ivory jacket jaguar jar jazz jealous jeans jelly jewel job join joke journey joy judge juice jump jungle junior junk just kangaroo keen keep ketchup key kick kid kidney kind kingdom kiss kit kitchen kite kitten kiwi knee knife knock know lab label labor ladder lady lake lamp language laptop large later latin laugh laundry lava law lawn lawsuit layer lazy leader leaf learn leave lecture left leg legal legend leisure lemon lend length lens leopard lesson letter level liar liberty library license life lift light like limb limit link lion liquid list little live lizard load loan lobster local lock logic lonely long loop lottery loud lounge love loyal lucky luggage lumber lunar lunch luxury lyrics machine mad magic magnet maid mail main major make mammal man manage mandate mango mansion manual maple marble march margin marine market marriage mask mass master match material math matrix matter maximum maze meadow mean measure meat mechanic medal media melody melt member memory mention menu mercy merge merit merry mesh message metal method middle midnight milk million mimic mind minimum minor minute miracle mirror misery miss mistake mix mixed mixture mobile model modify mom moment monitor monkey monster month moon moral more morning mosquito mother motion motor mountain mouse move movie much muffin mule multiply muscle museum mushroom music must mutual myself mystery myth naive name napkin narrow nasty nation nature near neck need negative neglect neither nephew nerve nest net network neutral never news next nice night noble noise nominee noodle normal north nose notable note nothing notice novel now nuclear number nurse nut oak obey object oblige obscure observe obtain obvious occur ocean october odor off offer office often oil okay old olive olympic omit once one onion online only open opera opinion oppose option orange orbit orchard order ordinary organ orient original orphan ostrich other outdoor outer output outside oval oven over own owner oxygen oyster ozone pact paddle page pair palace palm panda panel panic panther paper parade parent park parrot party pass patch path patient patrol pattern pause pave payment peace peanut pear peasant pelican pen penalty pencil people pepper perfect permit person pet phone photo phrase physical piano picnic picture piece pig pigeon pill pilot pink pioneer pipe pistol pitch pizza place planet plastic plate play please pledge pluck plug plunge poem poet point polar pole police pond pony pool popular portion position possible post potato pottery poverty powder power practice praise predict prefer prepare present pretty prevent price pride primary print priority prison private prize problem process produce profit program project promote proof property prosper protect proud provide public pudding pull pulp pulse pumpkin punch pupil puppy purchase purity purpose purse push put puzzle pyramid quality quantum quarter question quick quit quiz quote rabbit raccoon race rack radar radio rail rain raise rally ramp ranch random range rapid rare rate rather raven raw razor ready real reason rebel rebuild recall receive recipe record recycle reduce reflect reform refuse region regret regular reject relax release relief rely remain remember remind remove render renew rent reopen repair repeat replace report require rescue resemble resist resource response result retire retreat return reunion reveal review reward rhythm rib ribbon rice rich ride ridge rifle right rigid ring riot ripple risk ritual rival river road roast robot robust rocket romance roof rookie room rose rotate rough round route royal rubber rude rug rule run runway rural sad saddle sadness safe sail salad salmon salon salt salute same sample sand satisfy satoshi sauce sausage save say scale scan scare scatter scene scheme school science scissors scorpion scout scrap screen script scrub sea search season seat second secret section security seed seek segment select sell seminar senior sense sentence series service session settle setup seven shadow shaft shallow share shed shell sheriff shield shift shine ship shiver shock shoe shoot shop short shoulder shove shrimp shrug shuffle shy sibling sick side siege sight sign silent silk silly silver similar simple since sing siren sister situate six size skate sketch ski skill skin skirt skull slab slam sleep slender slice slide slight slim slogan slot slow slush small smart smile smoke smooth snack snake snap sniff snow soap soccer social sock soda soft solar soldier solid solution solve someone song soon sorry sort soul sound soup source south space spare spatial spawn speak special speed spell spend sphere spice spider spike spin spirit split spoil sponsor spoon sport spot spray spread spring spy square squeeze squirrel stable stadium staff stage stairs stamp stand start state stay steak steel stem step stereo stick still sting stock stomach stone stool story stove strategy street strike strong struggle student stuff stumble style subject submit subway success such sudden suffer sugar suggest suit summer sun sunny sunset super supply supreme sure surface surge surprise surround survey suspect sustain swallow swamp swap swarm swear sweet swift swim swing switch sword symbol symptom syrup system table tackle tag tail talent talk tank tape target task taste tattoo taxi teach team tell ten tenant tennis tent term test text thank that theme then theory there they thing this thought three thrive throw thumb thunder ticket tide tiger tilt timber time tiny tip tired tissue title toast tobacco today toddler toe together toilet token tomato tomorrow tone tongue tonight tool tooth top topic topple torch tornado tortoise toss total tourist toward tower town toy track trade traffic tragic train transfer trap trash travel tray treat tree trend trial tribe trick trigger trim trip trophy trouble truck true truly trumpet trust truth try tube tuition tumble tuna tunnel turkey turn turtle twelve twenty twice twin twist two type typical ugly umbrella unable unaware uncle uncover under undo unfair unfold unhappy uniform unique unit universe unknown unlock until unusual unveil update upgrade uphold upon upper upset urban urge usage use used useful useless usual utility vacant vacuum vague valid valley valve van vanish vapor various vast vault vehicle velvet vendor venture venue verb verify version very vessel veteran viable vibrant vicious victory video view village vintage violin virtual virus visa visit visual vital vivid vocal voice void volcano volume vote voyage wage wagon wait walk wall walnut want warfare warm warrior wash wasp waste water wave way wealth weapon wear weasel weather web wedding weekend weird welcome west wet whale what wheat wheel when where whip whisper wide width wife wild will win window wine wing wink winner winter wire wisdom wise wish witness wolf woman wonder wood wool word work world worry worth wrap wreck wrestle wrist write wrong yard year yellow you young youth zebra zero zone zoo`, \" \")\n"
  },
  {
    "path": "cmd/age-inspect/inspect.go",
    "content": "// Copyright 2025 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"filippo.io/age/internal/inspect\"\n)\n\nconst usage = `Usage:\n    age-inspect [--json] [INPUT]\n\nOptions:\n    --json                      Output machine-readable JSON.\n\nINPUT defaults to standard input. \"-\" may be used as INPUT to explicitly\nread from standard input.`\n\n// Version can be set at link time to override debug.BuildInfo.Main.Version when\n// building manually without git history. It should look like \"v1.2.3\".\nvar Version string\n\nfunc main() {\n\tflag.Usage = func() { fmt.Fprintf(os.Stderr, \"%s\\n\", usage) }\n\n\tvar (\n\t\tversionFlag bool\n\t\tjsonFlag    bool\n\t)\n\n\tflag.BoolVar(&versionFlag, \"version\", false, \"print the version\")\n\tflag.BoolVar(&jsonFlag, \"json\", false, \"output machine-readable JSON\")\n\tflag.Parse()\n\n\tif versionFlag {\n\t\tif buildInfo, ok := debug.ReadBuildInfo(); ok && Version == \"\" {\n\t\t\tVersion = buildInfo.Main.Version\n\t\t}\n\t\tfmt.Println(Version)\n\t\treturn\n\t}\n\n\tif flag.NArg() > 1 {\n\t\tflag.Usage()\n\t\tos.Exit(1)\n\t}\n\n\tin := os.Stdin\n\tvar fileSize int64 = -1\n\tif name := flag.Arg(0); name != \"\" && name != \"-\" {\n\t\tf, err := os.Open(name)\n\t\tif err != nil {\n\t\t\terrorf(\"failed to open input file %q: %v\", name, err)\n\t\t}\n\t\tdefer f.Close()\n\t\tin = f\n\t\tif stat, err := f.Stat(); err == nil && stat.Mode().IsRegular() {\n\t\t\tfileSize = stat.Size()\n\t\t}\n\t}\n\n\tdata, err := inspect.Inspect(in, fileSize)\n\tif err != nil {\n\t\terrorf(\"inspection failed: %v\", err)\n\t}\n\n\tif jsonFlag {\n\t\tenc := json.NewEncoder(os.Stdout)\n\t\tenc.SetIndent(\"\", \"    \")\n\t\tif err := enc.Encode(data); err != nil {\n\t\t\terrorf(\"failed to encode JSON output: %v\", err)\n\t\t}\n\t} else {\n\t\tname := flag.Arg(0)\n\t\tif name == \"\" {\n\t\t\tname = \"<stdin>\"\n\t\t}\n\t\tfmt.Printf(\"%s is an age file, version %q.\\n\", name, data.Version)\n\t\tfmt.Printf(\"\\n\")\n\t\tif data.Armor {\n\t\t\tfmt.Printf(\"This file is ASCII-armored.\\n\")\n\t\t\tfmt.Printf(\"\\n\")\n\t\t}\n\t\tfmt.Printf(\"This file is encrypted to the following recipient types:\\n\")\n\t\tfor _, t := range data.StanzaTypes {\n\t\t\tfmt.Printf(\"  - %q\\n\", t)\n\t\t}\n\t\tfmt.Printf(\"\\n\")\n\t\tswitch data.Postquantum {\n\t\tcase \"yes\":\n\t\t\tfmt.Printf(\"This file uses post-quantum encryption.\\n\")\n\t\t\tfmt.Printf(\"\\n\")\n\t\tcase \"no\":\n\t\t\tfmt.Printf(\"This file does NOT use post-quantum encryption.\\n\")\n\t\t\tfmt.Printf(\"\\n\")\n\t\t}\n\t\tfmt.Printf(\"Size breakdown (assuming it decrypts successfully):\\n\")\n\t\tfmt.Printf(\"\\n\")\n\t\tfmt.Printf(\"    Header              % 12d bytes\\n\", data.Sizes.Header)\n\t\tif data.Armor {\n\t\t\tfmt.Printf(\"    Armor overhead      % 12d bytes\\n\", data.Sizes.Armor)\n\t\t}\n\t\tfmt.Printf(\"    Encryption overhead % 12d bytes\\n\", data.Sizes.Overhead)\n\t\tfmt.Printf(\"    Payload             % 12d bytes\\n\", data.Sizes.MinPayload)\n\t\tfmt.Printf(\"                        -------------------\\n\")\n\t\ttotal := data.Sizes.Header + data.Sizes.Overhead + data.Sizes.MinPayload + data.Sizes.Armor\n\t\tfmt.Printf(\"    Total               % 12d bytes\\n\", total)\n\t\tfmt.Printf(\"\\n\")\n\t\tfmt.Printf(\"Tip: for machine-readable output, use --json.\\n\")\n\t}\n}\n\n// l is a logger with no prefixes.\nvar l = log.New(os.Stderr, \"\", 0)\n\nfunc errorf(format string, v ...any) {\n\tl.Printf(\"age-inspect: error: \"+format, v...)\n\tl.Printf(\"age-inspect: report unexpected or unhelpful errors at https://filippo.io/age/report\")\n\tos.Exit(1)\n}\n"
  },
  {
    "path": "cmd/age-keygen/keygen.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"time\"\n\n\t\"filippo.io/age\"\n\t\"golang.org/x/term\"\n)\n\nconst usage = `Usage:\n    age-keygen [-pq] [-o OUTPUT]\n    age-keygen -y [-o OUTPUT] [INPUT]\n\nOptions:\n    -pq                       Generate a post-quantum hybrid ML-KEM-768 + X25519 key pair.\n                              (This might become the default in the future.)\n    -o, --output OUTPUT       Write the result to the file at path OUTPUT.\n    -y                        Convert an identity file to a recipients file.\n\nage-keygen generates a new native X25519 or, with the -pq flag, post-quantum\nhybrid ML-KEM-768 + X25519 key pair, and outputs it to standard output or to\nthe OUTPUT file.\n\nIf an OUTPUT file is specified, the public key is printed to standard error.\nIf OUTPUT already exists, it is not overwritten.\n\nIn -y mode, age-keygen reads an identity file from INPUT or from standard\ninput and writes the corresponding recipient(s) to OUTPUT or to standard\noutput, one per line, with no comments.\n\nExamples:\n\n    $ age-keygen\n    # created: 2021-01-02T15:30:45+01:00\n    # public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z\n    AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9\n\n    $ age-keygen -pq\n    # created: 2025-11-17T12:15:17+01:00\n    # public key: age1pq1pd[... 1950 more characters ...]\n    AGE-SECRET-KEY-PQ-1XXC4XS9DXHZ6TREKQTT3XECY8VNNU7GJ83C3Y49D0GZ3ZUME4JWS6QC3EF\n\n    $ age-keygen -o key.txt\n    Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p\n\n    $ age-keygen -y key.txt\n    age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p`\n\n// Version can be set at link time to override debug.BuildInfo.Main.Version when\n// building manually without git history. It should look like \"v1.2.3\".\nvar Version string\n\nfunc main() {\n\tlog.SetFlags(0)\n\tflag.Usage = func() { fmt.Fprintf(os.Stderr, \"%s\\n\", usage) }\n\n\tvar outFlag string\n\tvar pqFlag, versionFlag, convertFlag bool\n\n\tflag.BoolVar(&versionFlag, \"version\", false, \"print the version\")\n\tflag.BoolVar(&pqFlag, \"pq\", false, \"generate a post-quantum key pair\")\n\tflag.BoolVar(&convertFlag, \"y\", false, \"convert identities to recipients\")\n\tflag.StringVar(&outFlag, \"o\", \"\", \"output to `FILE` (default stdout)\")\n\tflag.StringVar(&outFlag, \"output\", \"\", \"output to `FILE` (default stdout)\")\n\tflag.Parse()\n\n\tif versionFlag {\n\t\tif buildInfo, ok := debug.ReadBuildInfo(); ok && Version == \"\" {\n\t\t\tVersion = buildInfo.Main.Version\n\t\t}\n\t\tfmt.Println(Version)\n\t\treturn\n\t}\n\n\tif len(flag.Args()) != 0 && !convertFlag {\n\t\terrorf(\"too many arguments\")\n\t}\n\tif len(flag.Args()) > 1 && convertFlag {\n\t\terrorf(\"too many arguments\")\n\t}\n\tif pqFlag && convertFlag {\n\t\terrorf(\"-pq cannot be used with -y\")\n\t}\n\n\tout := os.Stdout\n\tif outFlag != \"\" {\n\t\tf, err := os.OpenFile(outFlag, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\t\tif err != nil {\n\t\t\terrorf(\"failed to open output file %q: %v\", outFlag, err)\n\t\t}\n\t\tdefer func() {\n\t\t\tif err := f.Close(); err != nil {\n\t\t\t\terrorf(\"failed to close output file %q: %v\", outFlag, err)\n\t\t\t}\n\t\t}()\n\t\tout = f\n\t}\n\n\tin := os.Stdin\n\tif inFile := flag.Arg(0); inFile != \"\" && inFile != \"-\" {\n\t\tf, err := os.Open(inFile)\n\t\tif err != nil {\n\t\t\terrorf(\"failed to open input file %q: %v\", inFile, err)\n\t\t}\n\t\tdefer f.Close()\n\t\tin = f\n\t}\n\n\tif convertFlag {\n\t\tconvert(in, out)\n\t} else {\n\t\tif fi, err := out.Stat(); err == nil && fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {\n\t\t\twarning(\"writing secret key to a world-readable file\")\n\t\t}\n\t\tgenerate(out, pqFlag)\n\t}\n}\n\nfunc generate(out *os.File, pq bool) {\n\tvar i age.Identity\n\tvar r age.Recipient\n\tif pq {\n\t\tk, err := age.GenerateHybridIdentity()\n\t\tif err != nil {\n\t\t\terrorf(\"internal error: %v\", err)\n\t\t}\n\t\ti = k\n\t\tr = k.Recipient()\n\t} else {\n\t\tk, err := age.GenerateX25519Identity()\n\t\tif err != nil {\n\t\t\terrorf(\"internal error: %v\", err)\n\t\t}\n\t\ti = k\n\t\tr = k.Recipient()\n\t}\n\n\tif !term.IsTerminal(int(out.Fd())) {\n\t\tfmt.Fprintf(os.Stderr, \"Public key: %s\\n\", r)\n\t}\n\n\tfmt.Fprintf(out, \"# created: %s\\n\", time.Now().Format(time.RFC3339))\n\tfmt.Fprintf(out, \"# public key: %s\\n\", r)\n\tfmt.Fprintf(out, \"%s\\n\", i)\n}\n\nfunc convert(in io.Reader, out io.Writer) {\n\tids, err := age.ParseIdentities(in)\n\tif err != nil {\n\t\terrorf(\"failed to parse input: %v\", err)\n\t}\n\tif len(ids) == 0 {\n\t\terrorf(\"no identities found in the input\")\n\t}\n\tfor _, id := range ids {\n\t\tswitch id := id.(type) {\n\t\tcase *age.X25519Identity:\n\t\t\tfmt.Fprintf(out, \"%s\\n\", id.Recipient())\n\t\tcase *age.HybridIdentity:\n\t\t\tfmt.Fprintf(out, \"%s\\n\", id.Recipient())\n\t\tdefault:\n\t\t\terrorf(\"internal error: unexpected identity type: %T\", id)\n\t\t}\n\n\t}\n}\n\nfunc errorf(format string, v ...any) {\n\tlog.Printf(\"age-keygen: error: \"+format, v...)\n\tlog.Fatalf(\"age-keygen: report unexpected or unhelpful errors at https://filippo.io/age/report\")\n}\n\nfunc warning(msg string) {\n\tlog.Printf(\"age-keygen: warning: %s\", msg)\n}\n"
  },
  {
    "path": "cmd/age-plugin-batchpass/plugin-batchpass.go",
    "content": "package main\n\nimport (\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/plugin\"\n)\n\nconst usage = `age-plugin-batchpass is an age plugin that enables non-interactive\npassphrase-based encryption and decryption using environment variables.\n\nWARNING: IN 90% OF CASES, YOU DON'T NEED THIS PLUGIN.\n\nThis functionality is not built into the age CLI because most applications\nshould use native keys instead of scripting passphrase-based encryption.\n\nHumans are notoriously bad at remembering and generating strong passphrases.\nage uses scrypt to partially mitigate this, which is necessarily very slow.\n\nIf a computer will be doing the remembering anyway, you can and should use\nnative keys instead. There is no need to manage separate public and private\nkeys, you encrypt directly to the private key:\n\n    $ age-keygen -o key.txt\n    $ age -e -i key.txt file.txt > file.txt.age\n    $ age -d -i key.txt file.txt.age > file.txt\n\nLikewise, you can store a native identity string in an environment variable\nor through your CI secrets manager and use it to encrypt and decrypt files\nnon-interactively:\n\n    $ export AGE_SECRET=$(age-keygen)\n    $ age -e -i <(echo \"$AGE_SECRET\") file.txt > file.txt.age\n    $ age -d -i <(echo \"$AGE_SECRET\") file.txt.age > file.txt\n\nThe age CLI also natively supports passphrase-encrypted identity files, so you\ncan use that functionality to non-interactively encrypt multiple files such that\nyou will be able to decrypt them later by entering the same passphrase:\n\n    $ age-keygen -pq | age -p -o encrypted-identity.txt\n    Public key: age1pq1cd[... 1950 more characters ...]\n    Enter passphrase (leave empty to autogenerate a secure one):\n    age: using autogenerated passphrase \"eternal-erase-keen-suffer-fog-exclude-huge-scorpion-escape-scrub\"\n    $ age -r age1pq1cd[... 1950 more characters ...] file.txt > file.txt.age\n    $ age -d -i encrypted-identity.txt file.txt.age > file.txt\n    Enter passphrase for identity file \"encrypted-identity.txt\":\n\nFinally, when using this plugin care should be taken not to let the password be\npersisted in the shell history or leaked to other users on multi-user systems.\n\nUsage:\n\n    $ AGE_PASSPHRASE=password age -e -j batchpass file.txt > file.txt.age\n\n    $ AGE_PASSPHRASE=password age -d -j batchpass file.txt.age > file.txt\n\nAlternatively, you can use AGE_PASSPHRASE_FD to read the passphrase from\na file descriptor. Trailing newlines are stripped from the file contents.\n\nWhen encrypting, you can set AGE_PASSPHRASE_WORK_FACTOR to adjust the scrypt\nwork factor (between 1 and 30, default 18). Higher values are more secure\nbut slower.\n\nWhen decrypting, you can set AGE_PASSPHRASE_MAX_WORK_FACTOR to limit the\nmaximum scrypt work factor accepted (between 1 and 30, default 30). This can\nbe used to avoid very slow decryptions.`\n\n// Version can be set at link time to override debug.BuildInfo.Main.Version when\n// building manually without git history. It should look like \"v1.2.3\".\nvar Version string\n\nfunc main() {\n\tflag.Usage = func() { fmt.Fprintf(os.Stderr, \"%s\\n\", usage) }\n\n\tp, err := plugin.New(\"batchpass\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tp.RegisterFlags(nil)\n\n\tversionFlag := flag.Bool(\"version\", false, \"print the version\")\n\tflag.Parse()\n\n\tif *versionFlag {\n\t\tif buildInfo, ok := debug.ReadBuildInfo(); ok && Version == \"\" {\n\t\t\tVersion = buildInfo.Main.Version\n\t\t}\n\t\tfmt.Println(Version)\n\t\treturn\n\t}\n\n\tp.HandleIdentityAsRecipient(func(data []byte) (age.Recipient, error) {\n\t\tif len(data) != 0 {\n\t\t\treturn nil, fmt.Errorf(\"batchpass identity does not take any payload\")\n\t\t}\n\t\tpass, err := passphrase()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tr, err := age.NewScryptRecipient(pass)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to create scrypt recipient: %v\", err)\n\t\t}\n\t\tif envWorkFactor := os.Getenv(\"AGE_PASSPHRASE_WORK_FACTOR\"); envWorkFactor != \"\" {\n\t\t\tworkFactor, err := strconv.Atoi(envWorkFactor)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid AGE_PASSPHRASE_WORK_FACTOR: %v\", err)\n\t\t\t}\n\t\t\tif workFactor > 30 || workFactor < 1 {\n\t\t\t\treturn nil, fmt.Errorf(\"AGE_PASSPHRASE_WORK_FACTOR must be between 1 and 30\")\n\t\t\t}\n\t\t\tr.SetWorkFactor(workFactor)\n\t\t}\n\t\treturn r, nil\n\t})\n\tp.HandleIdentity(func(data []byte) (age.Identity, error) {\n\t\tif len(data) != 0 {\n\t\t\treturn nil, fmt.Errorf(\"batchpass identity does not take any payload\")\n\t\t}\n\t\tpass, err := passphrase()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tmaxWorkFactor := 0\n\t\tif envMaxWorkFactor := os.Getenv(\"AGE_PASSPHRASE_MAX_WORK_FACTOR\"); envMaxWorkFactor != \"\" {\n\t\t\tmaxWorkFactor, err = strconv.Atoi(envMaxWorkFactor)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid AGE_PASSPHRASE_MAX_WORK_FACTOR: %v\", err)\n\t\t\t}\n\t\t\tif maxWorkFactor > 30 || maxWorkFactor < 1 {\n\t\t\t\treturn nil, fmt.Errorf(\"AGE_PASSPHRASE_MAX_WORK_FACTOR must be between 1 and 30\")\n\t\t\t}\n\t\t}\n\t\treturn &batchpassIdentity{password: pass, maxWorkFactor: maxWorkFactor}, nil\n\t})\n\tos.Exit(p.Main())\n}\n\ntype batchpassIdentity struct {\n\tpassword      string\n\tmaxWorkFactor int\n}\n\nfunc (i *batchpassIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {\n\tfor _, s := range stanzas {\n\t\tif s.Type == \"scrypt\" && len(stanzas) != 1 {\n\t\t\treturn nil, errors.New(\"an scrypt recipient must be the only one\")\n\t\t}\n\t}\n\tif len(stanzas) != 1 || stanzas[0].Type != \"scrypt\" {\n\t\t// Don't fallback to other identities, this plugin should mostly be used\n\t\t// in isolation, from the CLI.\n\t\treturn nil, fmt.Errorf(\"file is not passphrase-encrypted\")\n\t}\n\tii, err := age.NewScryptIdentity(i.password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif i.maxWorkFactor != 0 {\n\t\tii.SetMaxWorkFactor(i.maxWorkFactor)\n\t}\n\tfileKey, err := ii.Unwrap(stanzas)\n\tif errors.Is(err, age.ErrIncorrectIdentity) {\n\t\t// ScryptIdentity returns ErrIncorrectIdentity to make it possible to\n\t\t// try multiple passphrases from the API. If a user is invoking this\n\t\t// plugin, it's safe to say they expect it to be the only mechanism to\n\t\t// decrypt a passphrase-protected file.\n\t\treturn nil, fmt.Errorf(\"incorrect passphrase\")\n\t}\n\treturn fileKey, err\n}\n\nfunc passphrase() (string, error) {\n\tenvPASSPHRASE := os.Getenv(\"AGE_PASSPHRASE\")\n\tenvFD := os.Getenv(\"AGE_PASSPHRASE_FD\")\n\tif envPASSPHRASE != \"\" && envFD != \"\" {\n\t\treturn \"\", fmt.Errorf(\"AGE_PASSPHRASE and AGE_PASSPHRASE_FD are mutually exclusive\")\n\t}\n\tif envPASSPHRASE == \"\" && envFD == \"\" {\n\t\treturn \"\", fmt.Errorf(\"either AGE_PASSPHRASE or AGE_PASSPHRASE_FD must be set\")\n\t}\n\n\tif envPASSPHRASE != \"\" {\n\t\treturn envPASSPHRASE, nil\n\t}\n\n\tfd, err := strconv.Atoi(envFD)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"invalid AGE_PASSPHRASE_FD: %v\", err)\n\t}\n\tf := os.NewFile(uintptr(fd), \"AGE_PASSPHRASE_FD\")\n\tif f == nil {\n\t\treturn \"\", fmt.Errorf(\"failed to open file descriptor %d\", fd)\n\t}\n\tdefer f.Close()\n\tconst maxPassphraseSize = 1024 * 1024 // 1 MiB\n\tb, err := io.ReadAll(io.LimitReader(f, maxPassphraseSize+1))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to read passphrase from fd %d: %v\", fd, err)\n\t}\n\tif len(b) > maxPassphraseSize {\n\t\treturn \"\", fmt.Errorf(\"passphrase from fd %d is too long\", fd)\n\t}\n\treturn strings.TrimRight(string(b), \"\\r\\n\"), nil\n}\n"
  },
  {
    "path": "doc/age-inspect.1",
    "content": ".\\\" generated with Ronn-NG/v0.9.1\n.\\\" http://github.com/apjanke/ronn-ng/tree/0.9.1\n.TH \"AGE\\-INSPECT\" \"1\" \"December 2025\" \"\"\n.SH \"NAME\"\n\\fBage\\-inspect\\fR \\- inspect age(1) encrypted files\n.SH \"SYNOPSIS\"\n\\fBage\\-inspect\\fR [\\fB\\-\\-json\\fR] [\\fIINPUT\\fR]\n.SH \"DESCRIPTION\"\n\\fBage\\-inspect\\fR reads an age(1) encrypted file from \\fIINPUT\\fR (or standard input) and displays metadata about it without decrypting\\.\n.P\nThis includes the recipient types, whether it uses post\\-quantum encryption, and a size breakdown of the file components\\.\n.SH \"OPTIONS\"\n.TP\n\\fB\\-\\-json\\fR\nOutput machine\\-readable JSON instead of human\\-readable text\\.\n.TP\n\\fB\\-\\-version\\fR\nPrint the version and exit\\.\n.SH \"JSON FORMAT\"\nWhen \\fB\\-\\-json\\fR is specified, the output is a JSON object with these fields:\n.IP \"\\[ci]\" 4\n\\fBversion\\fR: The age format version (e\\.g\\., \\fB\"age\\-encryption\\.org/v1\"\\fR)\\.\n.IP \"\\[ci]\" 4\n\\fBpostquantum\\fR: Whether the file uses post\\-quantum encryption: \\fB\"yes\"\\fR, \\fB\"no\"\\fR, or \\fB\"unknown\"\\fR\\.\n.IP \"\\[ci]\" 4\n\\fBarmor\\fR: Boolean indicating whether the file is ASCII\\-armored\\.\n.IP \"\\[ci]\" 4\n\\fBstanza_types\\fR: Array of recipient stanza type strings (e\\.g\\., \\fB[\"X25519\"]\\fR or \\fB[\"mlkem768x25519\"]\\fR)\\.\n.IP \"\\[ci]\" 4\n\\fBsizes\\fR: Object containing size information in bytes:\n.IP \"\\[ci]\" 4\n\\fBheader\\fR: Size of the age header\\.\n.IP \"\\[ci]\" 4\n\\fBarmor\\fR: Armor encoding overhead (0 if not armored)\\.\n.IP \"\\[ci]\" 4\n\\fBoverhead\\fR: Stream encryption overhead\\.\n.IP \"\\[ci]\" 4\n\\fBmin_payload\\fR, \\fBmax_payload\\fR: Payload size bounds (currently always matching)\\.\n.IP \"\\[ci]\" 4\n\\fBmin_padding\\fR, \\fBmax_padding\\fR: Padding size bounds (currently always 0)\\.\n.IP \"\" 0\n.IP\nThe fields add up to the total size of the file\\.\n.IP \"\" 0\n.SH \"EXAMPLES\"\nInspect an encrypted file:\n.IP \"\" 4\n.nf\n$ age\\-inspect secrets\\.age\nsecrets\\.age is an age file, version \"age\\-encryption\\.org/v1\"\\.\n\nThis file is encrypted to the following recipient types:\n\\- \"mlkem768x25519\"\n\nThis file uses post\\-quantum encryption\\.\n\nSize breakdown (assuming it decrypts successfully):\n\n    Header                      1627 bytes\n    Encryption overhead           32 bytes\n    Payload                       42 bytes\n                        \\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\\-\n    Total                       1701 bytes\n\nTip: for machine\\-readable output, use \\-\\-json\\.\n.fi\n.IP \"\" 0\n.P\nGet JSON output for scripting:\n.IP \"\" 4\n.nf\n$ age\\-inspect \\-\\-json secrets\\.age\n{\n    \"version\": \"age\\-encryption\\.org/v1\",\n    \"postquantum\": \"yes\",\n    \"armor\": false,\n    \"stanza_types\": [\n        \"mlkem768x25519\"\n    ],\n    \"sizes\": {\n        \"header\": 1627,\n        \"armor\": 0,\n        \"overhead\": 32,\n        \"min_payload\": 42,\n        \"max_payload\": 42,\n        \"min_padding\": 0,\n        \"max_padding\": 0\n    }\n}\n.fi\n.IP \"\" 0\n.SH \"SEE ALSO\"\nage(1), age\\-keygen(1)\n.SH \"AUTHORS\"\nFilippo Valsorda \\fIage@filippo\\.io\\fR\n"
  },
  {
    "path": "doc/age-inspect.1.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta http-equiv='content-type' content='text/html;charset=utf8'>\n  <meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>\n  <title>age-inspect(1) - inspect age(1) encrypted files</title>\n  <style type='text/css' media='all'>\n  /* style: man */\n  body#manpage {margin:0}\n  .mp {max-width:100ex;padding:0 9ex 1ex 4ex}\n  .mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}\n  .mp h2 {margin:10px 0 0 0}\n  .mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}\n  .mp h3 {margin:0 0 0 4ex}\n  .mp dt {margin:0;clear:left}\n  .mp dt.flush {float:left;width:8ex}\n  .mp dd {margin:0 0 0 9ex}\n  .mp h1,.mp h2,.mp h3,.mp h4 {clear:left}\n  .mp pre {margin-bottom:20px}\n  .mp pre+h2,.mp pre+h3 {margin-top:22px}\n  .mp h2+pre,.mp h3+pre {margin-top:5px}\n  .mp img {display:block;margin:auto}\n  .mp h1.man-title {display:none}\n  .mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}\n  .mp h2 {font-size:16px;line-height:1.25}\n  .mp h1 {font-size:20px;line-height:2}\n  .mp {text-align:justify;background:#fff}\n  .mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}\n  .mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}\n  .mp u {text-decoration:underline}\n  .mp code,.mp strong,.mp b {font-weight:bold;color:#131211}\n  .mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}\n  .mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}\n  .mp b.man-ref {font-weight:normal;color:#434241}\n  .mp pre {padding:0 4ex}\n  .mp pre code {font-weight:normal;color:#434241}\n  .mp h2+pre,h3+pre {padding-left:0}\n  ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}\n  ol.man-decor {width:100%}\n  ol.man-decor li.tl {text-align:left}\n  ol.man-decor li.tc {text-align:center;letter-spacing:4px}\n  ol.man-decor li.tr {text-align:right;float:right}\n  </style>\n</head>\n<!--\n  The following styles are deprecated and will be removed at some point:\n  div#man, div#man ol.man, div#man ol.head, div#man ol.man.\n\n  The .man-page, .man-decor, .man-head, .man-foot, .man-title, and\n  .man-navigation should be used instead.\n-->\n<body id='manpage'>\n  <div class='mp' id='man'>\n\n  <div class='man-navigation' style='display:none'>\n    <a href=\"#NAME\">NAME</a>\n    <a href=\"#SYNOPSIS\">SYNOPSIS</a>\n    <a href=\"#DESCRIPTION\">DESCRIPTION</a>\n    <a href=\"#OPTIONS\">OPTIONS</a>\n    <a href=\"#JSON-FORMAT\">JSON FORMAT</a>\n    <a href=\"#EXAMPLES\">EXAMPLES</a>\n    <a href=\"#SEE-ALSO\">SEE ALSO</a>\n    <a href=\"#AUTHORS\">AUTHORS</a>\n  </div>\n\n  <ol class='man-decor man-head man head'>\n    <li class='tl'>age-inspect(1)</li>\n    <li class='tc'></li>\n    <li class='tr'>age-inspect(1)</li>\n  </ol>\n\n  \n\n<h2 id=\"NAME\">NAME</h2>\n<p class=\"man-name\">\n  <code>age-inspect</code> - <span class=\"man-whatis\">inspect <a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a> encrypted files</span>\n</p>\n<h2 id=\"SYNOPSIS\">SYNOPSIS</h2>\n\n<p><code>age-inspect</code> [<code>--json</code>] [<var>INPUT</var>]</p>\n\n<h2 id=\"DESCRIPTION\">DESCRIPTION</h2>\n\n<p><code>age-inspect</code> reads an <a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a> encrypted file from <var>INPUT</var> (or standard input)\nand displays metadata about it without decrypting.</p>\n\n<p>This includes the recipient types, whether it uses post-quantum encryption,\nand a size breakdown of the file components.</p>\n\n<h2 id=\"OPTIONS\">OPTIONS</h2>\n\n<dl>\n<dt><code>--json</code></dt>\n<dd>  Output machine-readable JSON instead of human-readable text.</dd>\n<dt><code>--version</code></dt>\n<dd>  Print the version and exit.</dd>\n</dl>\n\n<h2 id=\"JSON-FORMAT\">JSON FORMAT</h2>\n\n<p>When <code>--json</code> is specified, the output is a JSON object with these fields:</p>\n\n<ul>\n  <li>\n    <p><code>version</code>:\n  The age format version (e.g., <code>\"age-encryption.org/v1\"</code>).</p>\n  </li>\n  <li>\n    <p><code>postquantum</code>:\n  Whether the file uses post-quantum encryption: <code>\"yes\"</code>, <code>\"no\"</code>, or\n  <code>\"unknown\"</code>.</p>\n  </li>\n  <li>\n    <p><code>armor</code>:\n  Boolean indicating whether the file is ASCII-armored.</p>\n  </li>\n  <li>\n    <p><code>stanza_types</code>:\n  Array of recipient stanza type strings (e.g., <code>[\"X25519\"]</code> or\n  <code>[\"mlkem768x25519\"]</code>).</p>\n  </li>\n  <li>\n    <p><code>sizes</code>:\n  Object containing size information in bytes:</p>\n\n    <ul>\n      <li>\n<code>header</code>: Size of the age header.</li>\n      <li>\n<code>armor</code>: Armor encoding overhead (0 if not armored).</li>\n      <li>\n<code>overhead</code>: Stream encryption overhead.</li>\n      <li>\n<code>min_payload</code>, <code>max_payload</code>: Payload size bounds (currently always matching).</li>\n      <li>\n<code>min_padding</code>, <code>max_padding</code>: Padding size bounds (currently always 0).</li>\n    </ul>\n\n    <p>The fields add up to the total size of the file.</p>\n  </li>\n</ul>\n\n<h2 id=\"EXAMPLES\">EXAMPLES</h2>\n\n<p>Inspect an encrypted file:</p>\n\n<pre><code>$ age-inspect secrets.age\nsecrets.age is an age file, version \"age-encryption.org/v1\".\n\nThis file is encrypted to the following recipient types:\n- \"mlkem768x25519\"\n\nThis file uses post-quantum encryption.\n\nSize breakdown (assuming it decrypts successfully):\n\n    Header                      1627 bytes\n    Encryption overhead           32 bytes\n    Payload                       42 bytes\n                        -------------------\n    Total                       1701 bytes\n\nTip: for machine-readable output, use --json.\n</code></pre>\n\n<p>Get JSON output for scripting:</p>\n\n<pre><code>$ age-inspect --json secrets.age\n{\n    \"version\": \"age-encryption.org/v1\",\n    \"postquantum\": \"yes\",\n    \"armor\": false,\n    \"stanza_types\": [\n        \"mlkem768x25519\"\n    ],\n    \"sizes\": {\n        \"header\": 1627,\n        \"armor\": 0,\n        \"overhead\": 32,\n        \"min_payload\": 42,\n        \"max_payload\": 42,\n        \"min_padding\": 0,\n        \"max_padding\": 0\n    }\n}\n</code></pre>\n\n<h2 id=\"SEE-ALSO\">SEE ALSO</h2>\n\n<p><a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a>, <a class=\"man-ref\" href=\"age-keygen.1.html\">age-keygen<span class=\"s\">(1)</span></a></p>\n\n<h2 id=\"AUTHORS\">AUTHORS</h2>\n\n<p>Filippo Valsorda <a href=\"mailto:age@filippo.io\" data-bare-link=\"true\">age@filippo.io</a></p>\n\n  <ol class='man-decor man-foot man foot'>\n    <li class='tl'></li>\n    <li class='tc'>December 2025</li>\n    <li class='tr'>age-inspect(1)</li>\n  </ol>\n\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "doc/age-inspect.1.ronn",
    "content": "age-inspect(1) -- inspect age(1) encrypted files\n====================================================\n\n## SYNOPSIS\n\n`age-inspect` [`--json`] [<INPUT>]\n\n## DESCRIPTION\n\n`age-inspect` reads an age(1) encrypted file from <INPUT> (or standard input)\nand displays metadata about it without decrypting.\n\nThis includes the recipient types, whether it uses post-quantum encryption,\nand a size breakdown of the file components.\n\n## OPTIONS\n\n* `--json`:\n    Output machine-readable JSON instead of human-readable text.\n\n* `--version`:\n    Print the version and exit.\n\n## JSON FORMAT\n\nWhen `--json` is specified, the output is a JSON object with these fields:\n\n* `version`:\n    The age format version (e.g., `\"age-encryption.org/v1\"`).\n\n* `postquantum`:\n    Whether the file uses post-quantum encryption: `\"yes\"`, `\"no\"`, or\n    `\"unknown\"`.\n\n* `armor`:\n    Boolean indicating whether the file is ASCII-armored.\n\n* `stanza_types`:\n    Array of recipient stanza type strings (e.g., `[\"X25519\"]` or\n    `[\"mlkem768x25519\"]`).\n\n* `sizes`:\n    Object containing size information in bytes:\n\n    * `header`: Size of the age header.\n    * `armor`: Armor encoding overhead (0 if not armored).\n    * `overhead`: Stream encryption overhead.\n    * `min_payload`, `max_payload`: Payload size bounds (currently always matching).\n    * `min_padding`, `max_padding`: Padding size bounds (currently always 0).\n\n    The fields add up to the total size of the file.\n\n## EXAMPLES\n\nInspect an encrypted file:\n\n    $ age-inspect secrets.age\n    secrets.age is an age file, version \"age-encryption.org/v1\".\n\n    This file is encrypted to the following recipient types:\n    - \"mlkem768x25519\"\n\n    This file uses post-quantum encryption.\n\n    Size breakdown (assuming it decrypts successfully):\n\n        Header                      1627 bytes\n        Encryption overhead           32 bytes\n        Payload                       42 bytes\n                            -------------------\n        Total                       1701 bytes\n\n    Tip: for machine-readable output, use --json.\n\nGet JSON output for scripting:\n\n    $ age-inspect --json secrets.age\n    {\n        \"version\": \"age-encryption.org/v1\",\n        \"postquantum\": \"yes\",\n        \"armor\": false,\n        \"stanza_types\": [\n            \"mlkem768x25519\"\n        ],\n        \"sizes\": {\n            \"header\": 1627,\n            \"armor\": 0,\n            \"overhead\": 32,\n            \"min_payload\": 42,\n            \"max_payload\": 42,\n            \"min_padding\": 0,\n            \"max_padding\": 0\n        }\n    }\n\n## SEE ALSO\n\nage(1), age-keygen(1)\n\n## AUTHORS\n\nFilippo Valsorda <age@filippo.io>\n"
  },
  {
    "path": "doc/age-keygen.1",
    "content": ".\\\" generated with Ronn-NG/v0.9.1\n.\\\" http://github.com/apjanke/ronn-ng/tree/0.9.1\n.TH \"AGE\\-KEYGEN\" \"1\" \"December 2025\" \"\"\n.SH \"NAME\"\n\\fBage\\-keygen\\fR \\- generate age(1) key pairs\n.SH \"SYNOPSIS\"\n\\fBage\\-keygen\\fR [\\fB\\-pq\\fR] [\\fB\\-o\\fR \\fIOUTPUT\\fR]\n.br\n\\fBage\\-keygen\\fR \\fB\\-y\\fR [\\fB\\-o\\fR \\fIOUTPUT\\fR] [\\fIINPUT\\fR]\n.br\n.SH \"DESCRIPTION\"\n\\fBage\\-keygen\\fR generates a new native age(1) key pair, and outputs the identity to standard output or to the \\fIOUTPUT\\fR file\\. The output includes the public key and the current time as comments\\.\n.P\nIf the output is not going to a terminal, \\fBage\\-keygen\\fR prints the public key to standard error\\.\n.SH \"OPTIONS\"\n.TP\n\\fB\\-pq\\fR\nGenerate a post\\-quantum hybrid ML\\-KEM\\-768 + X25519 key pair\\.\n.IP\nIn the future, this might become the default\\.\n.TP\n\\fB\\-o\\fR, \\fB\\-\\-output\\fR=\\fIOUTPUT\\fR\nWrite the identity to \\fIOUTPUT\\fR instead of standard output\\.\n.IP\nIf \\fIOUTPUT\\fR already exists, it is not overwritten\\.\n.TP\n\\fB\\-y\\fR\nRead an identity file from \\fIINPUT\\fR or from standard input and output the corresponding recipient(s), one per line, with no comments\\.\n.TP\n\\fB\\-\\-version\\fR\nPrint the version and exit\\.\n.SH \"EXAMPLES\"\nGenerate a new post\\-quantum identity:\n.IP \"\" 4\n.nf\n$ age\\-keygen \\-pq\n# created: 2025\\-11\\-17T13:39:06+01:00\n# public key: age1pq167[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.]\nAGE\\-SECRET\\-KEY\\-PQ\\-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T\n.fi\n.IP \"\" 0\n.P\nGenerate a new traditional identity:\n.IP \"\" 4\n.nf\n$ age\\-keygen\n# created: 2021\\-01\\-02T15:30:45+01:00\n# public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z\nAGE\\-SECRET\\-KEY\\-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9\n.fi\n.IP \"\" 0\n.P\nWrite a new post\\-quantum identity to \\fBkey\\.txt\\fR:\n.IP \"\" 4\n.nf\n$ age\\-keygen \\-pq \\-o key\\.txt\nPublic key: age1pq1cd[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.]\n.fi\n.IP \"\" 0\n.P\nConvert an identity to a recipient:\n.IP \"\" 4\n.nf\n$ age\\-keygen \\-y key\\.txt\nage1pq1cd[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.]\n.fi\n.IP \"\" 0\n.SH \"SEE ALSO\"\nage(1), age\\-inspect(1)\n.SH \"AUTHORS\"\nFilippo Valsorda \\fIage@filippo\\.io\\fR\n"
  },
  {
    "path": "doc/age-keygen.1.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta http-equiv='content-type' content='text/html;charset=utf8'>\n  <meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>\n  <title>age-keygen(1) - generate age(1) key pairs</title>\n  <style type='text/css' media='all'>\n  /* style: man */\n  body#manpage {margin:0}\n  .mp {max-width:100ex;padding:0 9ex 1ex 4ex}\n  .mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}\n  .mp h2 {margin:10px 0 0 0}\n  .mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}\n  .mp h3 {margin:0 0 0 4ex}\n  .mp dt {margin:0;clear:left}\n  .mp dt.flush {float:left;width:8ex}\n  .mp dd {margin:0 0 0 9ex}\n  .mp h1,.mp h2,.mp h3,.mp h4 {clear:left}\n  .mp pre {margin-bottom:20px}\n  .mp pre+h2,.mp pre+h3 {margin-top:22px}\n  .mp h2+pre,.mp h3+pre {margin-top:5px}\n  .mp img {display:block;margin:auto}\n  .mp h1.man-title {display:none}\n  .mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}\n  .mp h2 {font-size:16px;line-height:1.25}\n  .mp h1 {font-size:20px;line-height:2}\n  .mp {text-align:justify;background:#fff}\n  .mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}\n  .mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}\n  .mp u {text-decoration:underline}\n  .mp code,.mp strong,.mp b {font-weight:bold;color:#131211}\n  .mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}\n  .mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}\n  .mp b.man-ref {font-weight:normal;color:#434241}\n  .mp pre {padding:0 4ex}\n  .mp pre code {font-weight:normal;color:#434241}\n  .mp h2+pre,h3+pre {padding-left:0}\n  ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}\n  ol.man-decor {width:100%}\n  ol.man-decor li.tl {text-align:left}\n  ol.man-decor li.tc {text-align:center;letter-spacing:4px}\n  ol.man-decor li.tr {text-align:right;float:right}\n  </style>\n</head>\n<!--\n  The following styles are deprecated and will be removed at some point:\n  div#man, div#man ol.man, div#man ol.head, div#man ol.man.\n\n  The .man-page, .man-decor, .man-head, .man-foot, .man-title, and\n  .man-navigation should be used instead.\n-->\n<body id='manpage'>\n  <div class='mp' id='man'>\n\n  <div class='man-navigation' style='display:none'>\n    <a href=\"#NAME\">NAME</a>\n    <a href=\"#SYNOPSIS\">SYNOPSIS</a>\n    <a href=\"#DESCRIPTION\">DESCRIPTION</a>\n    <a href=\"#OPTIONS\">OPTIONS</a>\n    <a href=\"#EXAMPLES\">EXAMPLES</a>\n    <a href=\"#SEE-ALSO\">SEE ALSO</a>\n    <a href=\"#AUTHORS\">AUTHORS</a>\n  </div>\n\n  <ol class='man-decor man-head man head'>\n    <li class='tl'>age-keygen(1)</li>\n    <li class='tc'></li>\n    <li class='tr'>age-keygen(1)</li>\n  </ol>\n\n  \n\n<h2 id=\"NAME\">NAME</h2>\n<p class=\"man-name\">\n  <code>age-keygen</code> - <span class=\"man-whatis\">generate <a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a> key pairs</span>\n</p>\n<h2 id=\"SYNOPSIS\">SYNOPSIS</h2>\n\n<p><code>age-keygen</code> [<code>-pq</code>] [<code>-o</code> <var>OUTPUT</var>]<br>\n<code>age-keygen</code> <code>-y</code> [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br></p>\n\n<h2 id=\"DESCRIPTION\">DESCRIPTION</h2>\n\n<p><code>age-keygen</code> generates a new native <a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a> key pair, and outputs the identity to\nstandard output or to the <var>OUTPUT</var> file. The output includes the public key and\nthe current time as comments.</p>\n\n<p>If the output is not going to a terminal, <code>age-keygen</code> prints the public key to\nstandard error.</p>\n\n<h2 id=\"OPTIONS\">OPTIONS</h2>\n\n<dl>\n<dt><code>-pq</code></dt>\n<dd>  Generate a post-quantum hybrid ML-KEM-768 + X25519 key pair.\n\n    <p>In the future, this might become the default.</p>\n</dd>\n<dt>\n<code>-o</code>, <code>--output</code>=<var>OUTPUT</var>\n</dt>\n<dd>  Write the identity to <var>OUTPUT</var> instead of standard output.\n\n    <p>If <var>OUTPUT</var> already exists, it is not overwritten.</p>\n</dd>\n<dt><code>-y</code></dt>\n<dd>  Read an identity file from <var>INPUT</var> or from standard input and output the\n  corresponding recipient(s), one per line, with no comments.</dd>\n<dt><code>--version</code></dt>\n<dd>  Print the version and exit.</dd>\n</dl>\n\n<h2 id=\"EXAMPLES\">EXAMPLES</h2>\n\n<p>Generate a new post-quantum identity:</p>\n\n<pre><code>$ age-keygen -pq\n# created: 2025-11-17T13:39:06+01:00\n# public key: age1pq167[... 1950 more characters ...]\nAGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T\n</code></pre>\n\n<p>Generate a new traditional identity:</p>\n\n<pre><code>$ age-keygen\n# created: 2021-01-02T15:30:45+01:00\n# public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z\nAGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9\n</code></pre>\n\n<p>Write a new post-quantum identity to <code>key.txt</code>:</p>\n\n<pre><code>$ age-keygen -pq -o key.txt\nPublic key: age1pq1cd[... 1950 more characters ...]\n</code></pre>\n\n<p>Convert an identity to a recipient:</p>\n\n<pre><code>$ age-keygen -y key.txt\nage1pq1cd[... 1950 more characters ...]\n</code></pre>\n\n<h2 id=\"SEE-ALSO\">SEE ALSO</h2>\n\n<p><a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a>, <a class=\"man-ref\" href=\"age-inspect.1.html\">age-inspect<span class=\"s\">(1)</span></a></p>\n\n<h2 id=\"AUTHORS\">AUTHORS</h2>\n\n<p>Filippo Valsorda <a href=\"mailto:age@filippo.io\" data-bare-link=\"true\">age@filippo.io</a></p>\n\n  <ol class='man-decor man-foot man foot'>\n    <li class='tl'></li>\n    <li class='tc'>December 2025</li>\n    <li class='tr'>age-keygen(1)</li>\n  </ol>\n\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "doc/age-keygen.1.ronn",
    "content": "age-keygen(1) -- generate age(1) key pairs\n====================================================\n\n## SYNOPSIS\n\n`age-keygen` [`-pq`] [`-o` <OUTPUT>]<br>\n`age-keygen` `-y` [`-o` <OUTPUT>] [<INPUT>]<br>\n\n## DESCRIPTION\n\n`age-keygen` generates a new native age(1) key pair, and outputs the identity to\nstandard output or to the <OUTPUT> file. The output includes the public key and\nthe current time as comments.\n\nIf the output is not going to a terminal, `age-keygen` prints the public key to\nstandard error.\n\n## OPTIONS\n\n* `-pq`:\n    Generate a post-quantum hybrid ML-KEM-768 + X25519 key pair.\n\n    In the future, this might become the default.\n\n* `-o`, `--output`=<OUTPUT>:\n    Write the identity to <OUTPUT> instead of standard output.\n\n    If <OUTPUT> already exists, it is not overwritten.\n\n* `-y`:\n    Read an identity file from <INPUT> or from standard input and output the\n    corresponding recipient(s), one per line, with no comments.\n\n* `--version`:\n    Print the version and exit.\n\n## EXAMPLES\n\nGenerate a new post-quantum identity:\n\n    $ age-keygen -pq\n    # created: 2025-11-17T13:39:06+01:00\n    # public key: age1pq167[... 1950 more characters ...]\n    AGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T\n\nGenerate a new traditional identity:\n\n    $ age-keygen\n    # created: 2021-01-02T15:30:45+01:00\n    # public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z\n    AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9\n\nWrite a new post-quantum identity to `key.txt`:\n\n    $ age-keygen -pq -o key.txt\n    Public key: age1pq1cd[... 1950 more characters ...]\n\nConvert an identity to a recipient:\n\n    $ age-keygen -y key.txt\n    age1pq1cd[... 1950 more characters ...]\n\n## SEE ALSO\n\nage(1), age-inspect(1)\n\n## AUTHORS\n\nFilippo Valsorda <age@filippo.io>\n"
  },
  {
    "path": "doc/age-plugin-batchpass.1",
    "content": ".\\\" generated with Ronn-NG/v0.9.1\n.\\\" http://github.com/apjanke/ronn-ng/tree/0.9.1\n.TH \"AGE\\-PLUGIN\\-BATCHPASS\" \"1\" \"December 2025\" \"\"\n.SH \"NAME\"\n\\fBage\\-plugin\\-batchpass\\fR \\- non\\-interactive passphrase encryption plugin for age(1)\n.SH \"SYNOPSIS\"\n\\fBage\\fR \\fB\\-e\\fR \\fB\\-j\\fR \\fBbatchpass\\fR\n.br\n\\fBage\\fR \\fB\\-d\\fR \\fB\\-j\\fR \\fBbatchpass\\fR\n.br\n.SH \"DESCRIPTION\"\n\\fBage\\-plugin\\-batchpass\\fR is an age(1) plugin that enables non\\-interactive passphrase\\-based encryption and decryption using environment variables\\.\n.SH \"WARNING\"\nThis functionality is not built into the age CLI because most applications should use native keys instead of scripting passphrase\\-based encryption\\.\n.P\nHumans are notoriously bad at remembering and generating strong passphrases\\. age uses scrypt to partially mitigate this, which is necessarily very slow\\.\n.P\nIf a computer will be doing the remembering anyway, you can and should use native keys instead\\. There is no need to manage separate public and private keys, you encrypt directly to the private key:\n.IP \"\" 4\n.nf\n$ age\\-keygen \\-o key\\.txt\n$ age \\-e \\-i key\\.txt file\\.txt > file\\.txt\\.age\n$ age \\-d \\-i key\\.txt file\\.txt\\.age > file\\.txt\n.fi\n.IP \"\" 0\n.P\nLikewise, you can store a native identity string in an environment variable or through your CI secrets manager and use it to encrypt and decrypt files non\\-interactively:\n.IP \"\" 4\n.nf\n$ export AGE_SECRET=$(age\\-keygen)\n$ age \\-e \\-i <(echo \"$AGE_SECRET\") file\\.txt > file\\.txt\\.age\n$ age \\-d \\-i <(echo \"$AGE_SECRET\") file\\.txt\\.age > file\\.txt\n.fi\n.IP \"\" 0\n.P\nThe age CLI also natively supports passphrase\\-encrypted identity files, so you can use that functionality to non\\-interactively encrypt multiple files such that you will be able to decrypt them later by entering the same passphrase:\n.IP \"\" 4\n.nf\n$ age\\-keygen \\-pq | age \\-p \\-o encrypted\\-identity\\.txt\nPublic key: age1pq1cd[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.]\nEnter passphrase (leave empty to autogenerate a secure one):\nage: using autogenerated passphrase \"eternal\\-erase\\-keen\\-suffer\\-fog\\-exclude\\-huge\\-scorpion\\-escape\\-scrub\"\n$ age \\-r age1pq1cd[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.] file\\.txt > file\\.txt\\.age\n$ age \\-d \\-i encrypted\\-identity\\.txt file\\.txt\\.age > file\\.txt\nEnter passphrase for identity file \"encrypted\\-identity\\.txt\":\n.fi\n.IP \"\" 0\n.P\nFinally, when using this plugin care should be taken not to let the password be persisted in the shell history or leaked to other users on multi\\-user systems\\.\n.SH \"ENVIRONMENT\"\n.TP\n\\fBAGE_PASSPHRASE\\fR\nThe passphrase to use for encryption or decryption\\. Mutually exclusive with \\fBAGE_PASSPHRASE_FD\\fR\\.\n.TP\n\\fBAGE_PASSPHRASE_FD\\fR\nA file descriptor number to read the passphrase from\\. Trailing newlines are stripped from the file contents\\. Mutually exclusive with \\fBAGE_PASSPHRASE\\fR\\.\n.TP\n\\fBAGE_PASSPHRASE_WORK_FACTOR\\fR\nThe scrypt work factor to use when encrypting\\. Must be between 1 and 30\\. Default is 18\\. Higher values are more secure but slower\\.\n.TP\n\\fBAGE_PASSPHRASE_MAX_WORK_FACTOR\\fR\nThe maximum scrypt work factor to accept when decrypting\\. Must be between 1 and 30\\. Default is 30\\. Can be used to avoid very slow decryptions\\.\n.SH \"EXAMPLES\"\nEncrypt a file with a passphrase:\n.IP \"\" 4\n.nf\n$ AGE_PASSPHRASE=secret age \\-e \\-j batchpass file\\.txt > file\\.txt\\.age\n.fi\n.IP \"\" 0\n.P\nDecrypt a file with a passphrase:\n.IP \"\" 4\n.nf\n$ AGE_PASSPHRASE=secret age \\-d \\-j batchpass file\\.txt\\.age > file\\.txt\n.fi\n.IP \"\" 0\n.P\nRead the passphrase from a file descriptor:\n.IP \"\" 4\n.nf\n$ AGE_PASSPHRASE_FD=3 age \\-e \\-j batchpass file\\.txt 3< passphrase\\.txt > file\\.txt\\.age\n.fi\n.IP \"\" 0\n.SH \"SEE ALSO\"\nage(1)\n.SH \"AUTHORS\"\nFilippo Valsorda \\fIage@filippo\\.io\\fR\n"
  },
  {
    "path": "doc/age-plugin-batchpass.1.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta http-equiv='content-type' content='text/html;charset=utf8'>\n  <meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>\n  <title>age-plugin-batchpass(1) - non-interactive passphrase encryption plugin for age(1)</title>\n  <style type='text/css' media='all'>\n  /* style: man */\n  body#manpage {margin:0}\n  .mp {max-width:100ex;padding:0 9ex 1ex 4ex}\n  .mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}\n  .mp h2 {margin:10px 0 0 0}\n  .mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}\n  .mp h3 {margin:0 0 0 4ex}\n  .mp dt {margin:0;clear:left}\n  .mp dt.flush {float:left;width:8ex}\n  .mp dd {margin:0 0 0 9ex}\n  .mp h1,.mp h2,.mp h3,.mp h4 {clear:left}\n  .mp pre {margin-bottom:20px}\n  .mp pre+h2,.mp pre+h3 {margin-top:22px}\n  .mp h2+pre,.mp h3+pre {margin-top:5px}\n  .mp img {display:block;margin:auto}\n  .mp h1.man-title {display:none}\n  .mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}\n  .mp h2 {font-size:16px;line-height:1.25}\n  .mp h1 {font-size:20px;line-height:2}\n  .mp {text-align:justify;background:#fff}\n  .mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}\n  .mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}\n  .mp u {text-decoration:underline}\n  .mp code,.mp strong,.mp b {font-weight:bold;color:#131211}\n  .mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}\n  .mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}\n  .mp b.man-ref {font-weight:normal;color:#434241}\n  .mp pre {padding:0 4ex}\n  .mp pre code {font-weight:normal;color:#434241}\n  .mp h2+pre,h3+pre {padding-left:0}\n  ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}\n  ol.man-decor {width:100%}\n  ol.man-decor li.tl {text-align:left}\n  ol.man-decor li.tc {text-align:center;letter-spacing:4px}\n  ol.man-decor li.tr {text-align:right;float:right}\n  </style>\n</head>\n<!--\n  The following styles are deprecated and will be removed at some point:\n  div#man, div#man ol.man, div#man ol.head, div#man ol.man.\n\n  The .man-page, .man-decor, .man-head, .man-foot, .man-title, and\n  .man-navigation should be used instead.\n-->\n<body id='manpage'>\n  <div class='mp' id='man'>\n\n  <div class='man-navigation' style='display:none'>\n    <a href=\"#NAME\">NAME</a>\n    <a href=\"#SYNOPSIS\">SYNOPSIS</a>\n    <a href=\"#DESCRIPTION\">DESCRIPTION</a>\n    <a href=\"#WARNING\">WARNING</a>\n    <a href=\"#ENVIRONMENT\">ENVIRONMENT</a>\n    <a href=\"#EXAMPLES\">EXAMPLES</a>\n    <a href=\"#SEE-ALSO\">SEE ALSO</a>\n    <a href=\"#AUTHORS\">AUTHORS</a>\n  </div>\n\n  <ol class='man-decor man-head man head'>\n    <li class='tl'>age-plugin-batchpass(1)</li>\n    <li class='tc'></li>\n    <li class='tr'>age-plugin-batchpass(1)</li>\n  </ol>\n\n  \n\n<h2 id=\"NAME\">NAME</h2>\n<p class=\"man-name\">\n  <code>age-plugin-batchpass</code> - <span class=\"man-whatis\">non-interactive passphrase encryption plugin for <a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a></span>\n</p>\n<h2 id=\"SYNOPSIS\">SYNOPSIS</h2>\n\n<p><code>age</code> <code>-e</code> <code>-j</code> <code>batchpass</code><br>\n<code>age</code> <code>-d</code> <code>-j</code> <code>batchpass</code><br></p>\n\n<h2 id=\"DESCRIPTION\">DESCRIPTION</h2>\n\n<p><code>age-plugin-batchpass</code> is an <a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a> plugin that enables non-interactive\npassphrase-based encryption and decryption using environment variables.</p>\n\n<h2 id=\"WARNING\">WARNING</h2>\n\n<p>This functionality is not built into the age CLI because most applications\nshould use native keys instead of scripting passphrase-based encryption.</p>\n\n<p>Humans are notoriously bad at remembering and generating strong passphrases.\nage uses scrypt to partially mitigate this, which is necessarily very slow.</p>\n\n<p>If a computer will be doing the remembering anyway, you can and should use\nnative keys instead. There is no need to manage separate public and private\nkeys, you encrypt directly to the private key:</p>\n\n<pre><code>$ age-keygen -o key.txt\n$ age -e -i key.txt file.txt &gt; file.txt.age\n$ age -d -i key.txt file.txt.age &gt; file.txt\n</code></pre>\n\n<p>Likewise, you can store a native identity string in an environment variable\nor through your CI secrets manager and use it to encrypt and decrypt files\nnon-interactively:</p>\n\n<pre><code>$ export AGE_SECRET=$(age-keygen)\n$ age -e -i &lt;(echo \"$AGE_SECRET\") file.txt &gt; file.txt.age\n$ age -d -i &lt;(echo \"$AGE_SECRET\") file.txt.age &gt; file.txt\n</code></pre>\n\n<p>The age CLI also natively supports passphrase-encrypted identity files, so you\ncan use that functionality to non-interactively encrypt multiple files such that\nyou will be able to decrypt them later by entering the same passphrase:</p>\n\n<pre><code>$ age-keygen -pq | age -p -o encrypted-identity.txt\nPublic key: age1pq1cd[... 1950 more characters ...]\nEnter passphrase (leave empty to autogenerate a secure one):\nage: using autogenerated passphrase \"eternal-erase-keen-suffer-fog-exclude-huge-scorpion-escape-scrub\"\n$ age -r age1pq1cd[... 1950 more characters ...] file.txt &gt; file.txt.age\n$ age -d -i encrypted-identity.txt file.txt.age &gt; file.txt\nEnter passphrase for identity file \"encrypted-identity.txt\":\n</code></pre>\n\n<p>Finally, when using this plugin care should be taken not to let the password be\npersisted in the shell history or leaked to other users on multi-user systems.</p>\n\n<h2 id=\"ENVIRONMENT\">ENVIRONMENT</h2>\n\n<dl>\n<dt><code>AGE_PASSPHRASE</code></dt>\n<dd>  The passphrase to use for encryption or decryption.\n  Mutually exclusive with <code>AGE_PASSPHRASE_FD</code>.</dd>\n<dt><code>AGE_PASSPHRASE_FD</code></dt>\n<dd>  A file descriptor number to read the passphrase from.\n  Trailing newlines are stripped from the file contents.\n  Mutually exclusive with <code>AGE_PASSPHRASE</code>.</dd>\n<dt><code>AGE_PASSPHRASE_WORK_FACTOR</code></dt>\n<dd>  The scrypt work factor to use when encrypting.\n  Must be between 1 and 30. Default is 18.\n  Higher values are more secure but slower.</dd>\n<dt><code>AGE_PASSPHRASE_MAX_WORK_FACTOR</code></dt>\n<dd>  The maximum scrypt work factor to accept when decrypting.\n  Must be between 1 and 30. Default is 30.\n  Can be used to avoid very slow decryptions.</dd>\n</dl>\n\n<h2 id=\"EXAMPLES\">EXAMPLES</h2>\n\n<p>Encrypt a file with a passphrase:</p>\n\n<pre><code>$ AGE_PASSPHRASE=secret age -e -j batchpass file.txt &gt; file.txt.age\n</code></pre>\n\n<p>Decrypt a file with a passphrase:</p>\n\n<pre><code>$ AGE_PASSPHRASE=secret age -d -j batchpass file.txt.age &gt; file.txt\n</code></pre>\n\n<p>Read the passphrase from a file descriptor:</p>\n\n<pre><code>$ AGE_PASSPHRASE_FD=3 age -e -j batchpass file.txt 3&lt; passphrase.txt &gt; file.txt.age\n</code></pre>\n\n<h2 id=\"SEE-ALSO\">SEE ALSO</h2>\n\n<p><a class=\"man-ref\" href=\"age.1.html\">age<span class=\"s\">(1)</span></a></p>\n\n<h2 id=\"AUTHORS\">AUTHORS</h2>\n\n<p>Filippo Valsorda <a href=\"mailto:age@filippo.io\" data-bare-link=\"true\">age@filippo.io</a></p>\n\n  <ol class='man-decor man-foot man foot'>\n    <li class='tl'></li>\n    <li class='tc'>December 2025</li>\n    <li class='tr'>age-plugin-batchpass(1)</li>\n  </ol>\n\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "doc/age-plugin-batchpass.1.ronn",
    "content": "age-plugin-batchpass(1) -- non-interactive passphrase encryption plugin for age(1)\n==================================================================================\n\n## SYNOPSIS\n\n`age` `-e` `-j` `batchpass`<br>\n`age` `-d` `-j` `batchpass`<br>\n\n## DESCRIPTION\n\n`age-plugin-batchpass` is an age(1) plugin that enables non-interactive\npassphrase-based encryption and decryption using environment variables.\n\n## WARNING\n\nThis functionality is not built into the age CLI because most applications\nshould use native keys instead of scripting passphrase-based encryption.\n\nHumans are notoriously bad at remembering and generating strong passphrases.\nage uses scrypt to partially mitigate this, which is necessarily very slow.\n\nIf a computer will be doing the remembering anyway, you can and should use\nnative keys instead. There is no need to manage separate public and private\nkeys, you encrypt directly to the private key:\n\n    $ age-keygen -o key.txt\n    $ age -e -i key.txt file.txt > file.txt.age\n    $ age -d -i key.txt file.txt.age > file.txt\n\nLikewise, you can store a native identity string in an environment variable\nor through your CI secrets manager and use it to encrypt and decrypt files\nnon-interactively:\n\n    $ export AGE_SECRET=$(age-keygen)\n    $ age -e -i <(echo \"$AGE_SECRET\") file.txt > file.txt.age\n    $ age -d -i <(echo \"$AGE_SECRET\") file.txt.age > file.txt\n\nThe age CLI also natively supports passphrase-encrypted identity files, so you\ncan use that functionality to non-interactively encrypt multiple files such that\nyou will be able to decrypt them later by entering the same passphrase:\n\n    $ age-keygen -pq | age -p -o encrypted-identity.txt\n    Public key: age1pq1cd[... 1950 more characters ...]\n    Enter passphrase (leave empty to autogenerate a secure one):\n    age: using autogenerated passphrase \"eternal-erase-keen-suffer-fog-exclude-huge-scorpion-escape-scrub\"\n    $ age -r age1pq1cd[... 1950 more characters ...] file.txt > file.txt.age\n    $ age -d -i encrypted-identity.txt file.txt.age > file.txt\n    Enter passphrase for identity file \"encrypted-identity.txt\":\n\nFinally, when using this plugin care should be taken not to let the password be\npersisted in the shell history or leaked to other users on multi-user systems.\n\n## ENVIRONMENT\n\n* `AGE_PASSPHRASE`:\n    The passphrase to use for encryption or decryption.\n    Mutually exclusive with `AGE_PASSPHRASE_FD`.\n\n* `AGE_PASSPHRASE_FD`:\n    A file descriptor number to read the passphrase from.\n    Trailing newlines are stripped from the file contents.\n    Mutually exclusive with `AGE_PASSPHRASE`.\n\n* `AGE_PASSPHRASE_WORK_FACTOR`:\n    The scrypt work factor to use when encrypting.\n    Must be between 1 and 30. Default is 18.\n    Higher values are more secure but slower.\n\n* `AGE_PASSPHRASE_MAX_WORK_FACTOR`:\n    The maximum scrypt work factor to accept when decrypting.\n    Must be between 1 and 30. Default is 30.\n    Can be used to avoid very slow decryptions.\n\n## EXAMPLES\n\nEncrypt a file with a passphrase:\n\n    $ AGE_PASSPHRASE=secret age -e -j batchpass file.txt > file.txt.age\n\nDecrypt a file with a passphrase:\n\n    $ AGE_PASSPHRASE=secret age -d -j batchpass file.txt.age > file.txt\n\nRead the passphrase from a file descriptor:\n\n    $ AGE_PASSPHRASE_FD=3 age -e -j batchpass file.txt 3< passphrase.txt > file.txt.age\n\n## SEE ALSO\n\nage(1)\n\n## AUTHORS\n\nFilippo Valsorda <age@filippo.io>\n"
  },
  {
    "path": "doc/age.1",
    "content": ".\\\" generated with Ronn-NG/v0.9.1\n.\\\" http://github.com/apjanke/ronn-ng/tree/0.9.1\n.TH \"AGE\" \"1\" \"December 2025\" \"\"\n.SH \"NAME\"\n\\fBage\\fR \\- simple, modern, and secure file encryption\n.SH \"SYNOPSIS\"\n\\fBage\\fR [\\fB\\-\\-encrypt\\fR] (\\fB\\-r\\fR \\fIRECIPIENT\\fR | \\fB\\-R\\fR \\fIPATH\\fR)\\|\\.\\|\\.\\|\\. [\\fB\\-\\-armor\\fR] [\\fB\\-o\\fR \\fIOUTPUT\\fR] [\\fIINPUT\\fR]\n.br\n\\fBage\\fR [\\fB\\-\\-encrypt\\fR] \\fB\\-\\-passphrase\\fR [\\fB\\-\\-armor\\fR] [\\fB\\-o\\fR \\fIOUTPUT\\fR] [\\fIINPUT\\fR]\n.br\n\\fBage\\fR \\fB\\-\\-decrypt\\fR [\\fB\\-i\\fR \\fIPATH\\fR | \\fB\\-j\\fR \\fIPLUGIN\\fR]\\|\\.\\|\\.\\|\\. [\\fB\\-o\\fR \\fIOUTPUT\\fR] [\\fIINPUT\\fR]\n.br\n.SH \"DESCRIPTION\"\n\\fBage\\fR encrypts or decrypts \\fIINPUT\\fR to \\fIOUTPUT\\fR\\. The \\fIINPUT\\fR argument is optional and defaults to standard input\\. Only a single \\fIINPUT\\fR file may be specified\\. If \\fB\\-o\\fR is not specified, \\fIOUTPUT\\fR defaults to standard output\\.\n.P\nIf \\fB\\-p\\fR/\\fB\\-\\-passphrase\\fR is specified, the file is encrypted with a passphrase requested interactively\\. Otherwise, it's encrypted to one or more \\fIRECIPIENTS\\fR specified with \\fB\\-r\\fR/\\fB\\-\\-recipient\\fR or \\fB\\-R\\fR/\\fB\\-\\-recipients\\-file\\fR\\. Every recipient can decrypt the file\\.\n.P\nIn \\fB\\-d\\fR/\\fB\\-\\-decrypt\\fR mode, passphrase\\-encrypted files are detected automatically and the passphrase is requested interactively\\. Otherwise, one or more \\fIIDENTITIES\\fR specified with \\fB\\-i\\fR/\\fB\\-\\-identity\\fR are used to decrypt the file\\.\n.P\n\\fBage\\fR encrypted files are binary and not malleable, with around 200 bytes of overhead per recipient, plus 16 bytes every 64KiB of plaintext\\.\n.SH \"OPTIONS\"\n.TP\n\\fB\\-o\\fR, \\fB\\-\\-output\\fR=\\fIOUTPUT\\fR\nWrite encrypted or decrypted file to \\fIOUTPUT\\fR instead of standard output\\. If \\fIOUTPUT\\fR already exists it will be overwritten\\.\n.IP\nIf encrypting without \\fB\\-\\-armor\\fR, \\fBage\\fR will refuse to output binary to a TTY\\. This can be forced by specifying \\fB\\-\\fR as \\fIOUTPUT\\fR\\.\n.TP\n\\fB\\-\\-version\\fR\nPrint the version and exit\\.\n.SS \"Encryption options\"\n.TP\n\\fB\\-e\\fR, \\fB\\-\\-encrypt\\fR\nEncrypt \\fIINPUT\\fR to \\fIOUTPUT\\fR\\. This is the default\\.\n.TP\n\\fB\\-r\\fR, \\fB\\-\\-recipient\\fR=\\fIRECIPIENT\\fR\nEncrypt to the explicitly specified \\fIRECIPIENT\\fR\\. See the \\fIRECIPIENTS AND IDENTITIES\\fR section for possible recipient formats\\.\n.IP\nThis option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently\\.\n.TP\n\\fB\\-R\\fR, \\fB\\-\\-recipients\\-file\\fR=\\fIPATH\\fR\nEncrypt to the \\fIRECIPIENTS\\fR listed in the file at \\fIPATH\\fR, one per line\\. Empty lines and lines starting with \\fB#\\fR are ignored as comments\\.\n.IP\nIf \\fIPATH\\fR is \\fB\\-\\fR, the recipients are read from standard input\\. In this case, the \\fIINPUT\\fR argument must be specified\\.\n.IP\nThis option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently\\.\n.TP\n\\fB\\-p\\fR, \\fB\\-\\-passphrase\\fR\nEncrypt with a passphrase, requested interactively from the terminal\\. \\fBage\\fR will offer to auto\\-generate a secure passphrase\\.\n.IP\nThis option can't be used with other recipient flags\\.\n.TP\n\\fB\\-a\\fR, \\fB\\-\\-armor\\fR\nEncrypt to an ASCII\\-only \"armored\" encoding\\.\n.IP\n\\fBage\\fR armor is a strict version of PEM with type \\fBAGE ENCRYPTED FILE\\fR, canonical \"strict\" Base64, no headers, and no support for leading and trailing extra data\\.\n.IP\nDecryption transparently detects and decodes ASCII armoring\\.\n.TP\n\\fB\\-i\\fR, \\fB\\-\\-identity\\fR=\\fIPATH\\fR\nEncrypt to the \\fIRECIPIENTS\\fR corresponding to the \\fIIDENTITIES\\fR listed in the file at \\fIPATH\\fR\\. This is equivalent to converting the file at \\fIPATH\\fR to a recipients file with \\fBage\\-keygen \\-y\\fR and then passing that to \\fB\\-R\\fR/\\fB\\-\\-recipients\\-file\\fR\\.\n.IP\nFor the format of \\fIPATH\\fR, see the definition of \\fB\\-i\\fR/\\fB\\-\\-identity\\fR in the \\fIDecryption options\\fR section\\.\n.IP\n\\fB\\-e\\fR/\\fB\\-\\-encrypt\\fR must be explicitly specified when using \\fB\\-i\\fR/\\fB\\-\\-identity\\fR in encryption mode to avoid confusion\\.\n.TP\n\\fB\\-j\\fR \\fIPLUGIN\\fR\nEncrypt using the data\\-less \\fIplugin\\fR \\fIPLUGIN\\fR\\.\n.IP\nThis is equivalent to using \\fB\\-i\\fR/\\fB\\-\\-identity\\fR with a file that contains a single plugin \\fBIDENTITY\\fR that encodes no plugin\\-specific data\\.\n.IP\n\\fB\\-e\\fR/\\fB\\-\\-encrypt\\fR must be explicitly specified when using \\fB\\-j\\fR in encryption mode to avoid confusion\\.\n.SS \"Decryption options\"\n.TP\n\\fB\\-d\\fR, \\fB\\-\\-decrypt\\fR\nDecrypt \\fIINPUT\\fR to \\fIOUTPUT\\fR\\.\n.IP\nIf \\fIINPUT\\fR is passphrase encrypted, it will be automatically detected and the passphrase will be requested interactively\\. Otherwise, the \\fIIDENTITIES\\fR specified with \\fB\\-i\\fR/\\fB\\-\\-identity\\fR are used\\.\n.IP\nASCII armoring is transparently detected and decoded\\.\n.TP\n\\fB\\-i\\fR, \\fB\\-\\-identity\\fR=\\fIPATH\\fR\nDecrypt using the \\fIIDENTITIES\\fR at \\fIPATH\\fR\\.\n.IP\n\\fIPATH\\fR may be one of the following:\n.IP\na\\. A file listing \\fIIDENTITIES\\fR one per line\\. Empty lines and lines starting with \"\\fB#\\fR\" are ignored as comments\\.\n.IP\nb\\. A passphrase encrypted age file, containing \\fIIDENTITIES\\fR one per line like above\\. The passphrase is requested interactively\\. Note that passphrase\\-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system\\.\n.IP\nc\\. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format\\. If the private key is password\\-protected, the password is requested interactively only if the SSH identity matches the file\\. See the \\fISSH keys\\fR section for more information, including supported key types\\.\n.IP\nd\\. \"\\fB\\-\\fR\", causing one of the options above to be read from standard input\\. In this case, the \\fIINPUT\\fR argument must be specified\\.\n.IP\nThis option can be repeated\\. Identities are tried in the order in which are provided, and the first one matching one of the file's recipients is used\\. Unused identities are ignored, but it is an error if the \\fIINPUT\\fR file is passphrase\\-encrypted and \\fB\\-i\\fR/\\fB\\-\\-identity\\fR is specified\\.\n.TP\n\\fB\\-j\\fR \\fIPLUGIN\\fR\nDecrypt using the data\\-less \\fIplugin\\fR \\fIPLUGIN\\fR\\.\n.IP\nThis is equivalent to using \\fB\\-i\\fR/\\fB\\-\\-identity\\fR with a file that contains a single plugin \\fBIDENTITY\\fR that encodes no plugin\\-specific data\\.\n.SH \"RECIPIENTS AND IDENTITIES\"\n\\fBRECIPIENTS\\fR are public values, like a public key, that a file can be encrypted to\\. \\fBIDENTITIES\\fR are private values, like a private key, that allow decrypting a file encrypted to the corresponding \\fBRECIPIENT\\fR\\.\n.SS \"Native keys\"\nNative \\fBage\\fR key pairs are generated with age\\-keygen(1), and provide small encodings and strong encryption based on X25519 for classic keys, and X25519 + ML\\-KEM\\-768 for post\\-quantum hybrid keys\\. The post\\-quantum hybrid keys are secure against future quantum computers and are the recommended recipient type for most applications\\.\n.P\nA hybrid \\fBRECIPIENT\\fR encoding begins with \\fBage1pq1\\fR and looks like the following:\n.IP \"\" 4\n.nf\nage1pq167[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.]\n.fi\n.IP \"\" 0\n.P\nA hybrid \\fBIDENTITY\\fR encoding begins with \\fBAGE\\-SECRET\\-KEY\\-PQ\\-1\\fR and looks like the following:\n.IP \"\" 4\n.nf\nAGE\\-SECRET\\-KEY\\-PQ\\-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T\n.fi\n.IP \"\" 0\n.P\nA classic \\fBRECIPIENT\\fR encoding begins with \\fBage1\\fR and looks like the following:\n.IP \"\" 4\n.nf\nage1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh\n.fi\n.IP \"\" 0\n.P\nA classic \\fBIDENTITY\\fR encoding begins with \\fBAGE\\-SECRET\\-KEY\\-1\\fR and looks like the following:\n.IP \"\" 4\n.nf\nAGE\\-SECRET\\-KEY\\-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ\n.fi\n.IP \"\" 0\n.P\nA file can't be encrypted to both post\\-quantum and classic keys, as that would defeat the post\\-quantum security of the encryption\\.\n.P\nAn encrypted file can't be linked to the native recipient it's encrypted to without access to the corresponding identity\\.\n.SS \"SSH keys\"\nAs a convenience feature, \\fBage\\fR also supports encrypting to RSA or Ed25519 ssh(1) keys\\. RSA keys must be at least 2048 bits\\. This feature employs more complex cryptography, and should only be used when a native key is not available for the recipient\\. Note that SSH keys might not be protected long\\-term by the recipient, since they are revokable when used only for authentication\\.\n.P\nA \\fBRECIPIENT\\fR encoding is an SSH public key in \\fBauthorized_keys\\fR format (see the \\fBAUTHORIZED_KEYS FILE FORMAT\\fR section of sshd(8)), starting with \\fBssh\\-rsa\\fR or \\fBssh\\-ed25519\\fR, like the following:\n.IP \"\" 4\n.nf\nssh\\-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[\\|\\.\\|\\.\\|\\.]GU4BtElAbzh8=\nssh\\-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[\\|\\.\\|\\.\\|\\.]l1uZc31FGYMXa\n.fi\n.IP \"\" 0\n.P\nThe comment at the end of the line, if present, is ignored\\.\n.P\nIn recipient files passed to \\fB\\-R\\fR/\\fB\\-\\-recipients\\-file\\fR, unsupported but valid SSH public keys are ignored with a warning, to facilitate using \\fBauthorized_keys\\fR or GitHub \\fB\\.keys\\fR files\\. (See \\fIEXAMPLES\\fR\\.)\n.P\nAn \\fBIDENTITY\\fR is an SSH private key \\fIfile\\fR passed individually to \\fB\\-i\\fR/\\fB\\-\\-identity\\fR\\. Note that keys held on hardware tokens such as YubiKeys or accessed via ssh\\-agent(1) are not supported\\.\n.P\nAn encrypted file \\fIcan\\fR be linked to the SSH public key it was encrypted to\\. This is so that \\fBage\\fR can identify the correct SSH private key before requesting its password, if any\\.\n.SS \"Plugins\"\n\\fBage\\fR can be extended through plugins\\. A plugin is only loaded if a corresponding \\fBRECIPIENT\\fR or \\fBIDENTITY\\fR is specified\\. (Simply decrypting a file encrypted with a plugin will not cause it to load, for security reasons among others\\.)\n.P\nA \\fBRECIPIENT\\fR for a plugin named \\fBexample\\fR starts with \\fBage1example1\\fR, while an \\fBIDENTITY\\fR starts with \\fBAGE\\-PLUGIN\\-EXAMPLE\\-1\\fR\\. They both encode arbitrary plugin\\-specific data, and are generated by the plugin\\.\n.P\nWhen either is specified, \\fBage\\fR searches for \\fBage\\-plugin\\-example\\fR in the PATH and executes it to perform the file header encryption or decryption\\. The plugin may request input from the user through \\fBage\\fR to complete the operation\\.\n.P\nPlugins can be freely mixed with other plugins or natively supported keys\\.\n.P\nA plugin is not bound to only encrypt or decrypt files meant for or generated by the plugin\\. For example, a plugin can be used to decrypt files encrypted to a native X25519 \\fBRECIPIENT\\fR or even with a passphrase\\. Similarly, a plugin can encrypt a file such that it can be decrypted without the use of any plugin\\.\n.P\nPlugins for which the \\fBIDENTITY\\fR/\\fBRECIPIENT\\fR distinction doesn't make sense (such as a symmetric encryption plugin) may generate only an \\fBIDENTITY\\fR and instruct the user to perform encryption with the \\fB\\-e\\fR/\\fB\\-\\-encrypt\\fR and \\fB\\-i\\fR/\\fB\\-\\-identity\\fR flags\\. Plugins for which the concept of separate identities doesn't make sense (such as a password\\-encryption plugin) may instruct the user to use the \\fB\\-j\\fR flag\\.\n.P\n\\fBage\\fR can natively encrypt to recipients starting with \\fBage1tag1\\fR (using P\\-256 ECDH) or \\fBage1tagpq1\\fR (using the ML\\-KEM\\-768 + P\\-256 post\\-quantum hybrid)\\. These are intended to be the public side of private keys held in hardware\\.\n.P\nThey are directly supported to avoid the need to install the plugin, which may be platform\\-specific, on the encrypting side\\.\n.P\nThe tag reduces privacy, by allowing an observer to correlate files with a recipient (but not files amongst them without knowledge of the recipient), but this is also a desirable property for hardware keys that require user interaction for each decryption operation\\.\n.SH \"EXIT STATUS\"\n\\fBage\\fR will exit 0 if and only if encryption or decryption are successful for the full length of the input\\.\n.P\nIf an error occurs during decryption, partial output might still be generated, but only if it was possible to securely authenticate it\\. No unauthenticated output is ever released\\.\n.SH \"BACKWARDS COMPATIBILITY\"\nFiles encrypted with a stable version (not alpha, beta, or release candidate) of \\fBage\\fR, or with any v1\\.0\\.0 beta or release candidate, will decrypt with any later version of the tool\\.\n.P\nIf decrypting older files poses a security risk, doing so might cause an error by default\\. In this case, a flag will be provided to force the operation\\.\n.SH \"EXAMPLES\"\nGenerate a new post\\-quantum identity, encrypt data, and decrypt:\n.IP \"\" 4\n.nf\n$ age\\-keygen \\-pq \\-o key\\.txt\nPublic key: age1pq167[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.]\n\n$ tar cvz ~/data | age \\-r age1pq167[\\|\\.\\|\\.\\|\\.] > data\\.tar\\.gz\\.age\n\n$ age \\-d \\-o data\\.tar\\.gz \\-i key\\.txt data\\.tar\\.gz\\.age\n.fi\n.IP \"\" 0\n.P\nEncrypt \\fBexample\\.jpg\\fR to multiple recipients and output to \\fBexample\\.jpg\\.age\\fR:\n.IP \"\" 4\n.nf\n$ age \\-o example\\.jpg\\.age \\-r age1pq167[\\|\\.\\|\\.\\|\\.] \\-r age1pq1e3[\\|\\.\\|\\.\\|\\.] example\\.jpg\n.fi\n.IP \"\" 0\n.P\nEncrypt to a list of recipients:\n.IP \"\" 4\n.nf\n$ cat > recipients\\.txt\n# Alice\nage1pq167[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.]\n# Bob\nage1pq1e3[\\|\\.\\|\\.\\|\\. 1950 more characters \\|\\.\\|\\.\\|\\.]\n\n$ age \\-R recipients\\.txt example\\.jpg > example\\.jpg\\.age\n.fi\n.IP \"\" 0\n.P\nEncrypt and decrypt a file using a passphrase:\n.IP \"\" 4\n.nf\n$ age \\-p secrets\\.txt > secrets\\.txt\\.age\nEnter passphrase (leave empty to autogenerate a secure one):\nUsing the autogenerated passphrase \"release\\-response\\-step\\-brand\\-wrap\\-ankle\\-pair\\-unusual\\-sword\\-train\"\\.\n\n$ age \\-d secrets\\.txt\\.age > secrets\\.txt\nEnter passphrase:\n.fi\n.IP \"\" 0\n.P\nEncrypt and decrypt with a passphrase\\-protected identity file:\n.IP \"\" 4\n.nf\n$ age\\-keygen | age \\-p > key\\.age\nPublic key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5\nEnter passphrase (leave empty to autogenerate a secure one):\nUsing the autogenerated passphrase \"hip\\-roast\\-boring\\-snake\\-mention\\-east\\-wasp\\-honey\\-input\\-actress\"\\.\n\n$ age \\-r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets\\.txt > secrets\\.txt\\.age\n\n$ age \\-d \\-i key\\.age secrets\\.txt\\.age > secrets\\.txt\nEnter passphrase for identity file \"key\\.age\":\n.fi\n.IP \"\" 0\n.P\nEncrypt and decrypt with an SSH public key:\n.IP \"\" 4\n.nf\n$ age \\-R ~/\\.ssh/id_ed25519\\.pub example\\.jpg > example\\.jpg\\.age\n\n$ age \\-d \\-i ~/\\.ssh/id_ed25519 example\\.jpg\\.age > example\\.jpg\n.fi\n.IP \"\" 0\n.P\nEncrypt and decrypt with age\\-plugin\\-yubikey:\n.IP \"\" 4\n.nf\n$ age\\-plugin\\-yubikey # run interactive setup, generate identity file and obtain recipient\n\n$ age \\-r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets\\.txt > secrets\\.txt\\.age\n\n$ age \\-d \\-i age\\-yubikey\\-identity\\-388178f3\\.txt secrets\\.txt\\.age\n.fi\n.IP \"\" 0\n.P\nEncrypt to the SSH keys of a GitHub user:\n.IP \"\" 4\n.nf\n$ curl https://github\\.com/benjojo\\.keys | age \\-R \\- example\\.jpg > example\\.jpg\\.age\n.fi\n.IP \"\" 0\n.SH \"SEE ALSO\"\nage\\-keygen(1), age\\-inspect(1)\n.SH \"AUTHORS\"\nFilippo Valsorda \\fIage@filippo\\.io\\fR\n"
  },
  {
    "path": "doc/age.1.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta http-equiv='content-type' content='text/html;charset=utf8'>\n  <meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>\n  <title>age(1) - simple, modern, and secure file encryption</title>\n  <style type='text/css' media='all'>\n  /* style: man */\n  body#manpage {margin:0}\n  .mp {max-width:100ex;padding:0 9ex 1ex 4ex}\n  .mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}\n  .mp h2 {margin:10px 0 0 0}\n  .mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}\n  .mp h3 {margin:0 0 0 4ex}\n  .mp dt {margin:0;clear:left}\n  .mp dt.flush {float:left;width:8ex}\n  .mp dd {margin:0 0 0 9ex}\n  .mp h1,.mp h2,.mp h3,.mp h4 {clear:left}\n  .mp pre {margin-bottom:20px}\n  .mp pre+h2,.mp pre+h3 {margin-top:22px}\n  .mp h2+pre,.mp h3+pre {margin-top:5px}\n  .mp img {display:block;margin:auto}\n  .mp h1.man-title {display:none}\n  .mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}\n  .mp h2 {font-size:16px;line-height:1.25}\n  .mp h1 {font-size:20px;line-height:2}\n  .mp {text-align:justify;background:#fff}\n  .mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}\n  .mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}\n  .mp u {text-decoration:underline}\n  .mp code,.mp strong,.mp b {font-weight:bold;color:#131211}\n  .mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}\n  .mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}\n  .mp b.man-ref {font-weight:normal;color:#434241}\n  .mp pre {padding:0 4ex}\n  .mp pre code {font-weight:normal;color:#434241}\n  .mp h2+pre,h3+pre {padding-left:0}\n  ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}\n  ol.man-decor {width:100%}\n  ol.man-decor li.tl {text-align:left}\n  ol.man-decor li.tc {text-align:center;letter-spacing:4px}\n  ol.man-decor li.tr {text-align:right;float:right}\n  </style>\n</head>\n<!--\n  The following styles are deprecated and will be removed at some point:\n  div#man, div#man ol.man, div#man ol.head, div#man ol.man.\n\n  The .man-page, .man-decor, .man-head, .man-foot, .man-title, and\n  .man-navigation should be used instead.\n-->\n<body id='manpage'>\n  <div class='mp' id='man'>\n\n  <div class='man-navigation' style='display:none'>\n    <a href=\"#NAME\">NAME</a>\n    <a href=\"#SYNOPSIS\">SYNOPSIS</a>\n    <a href=\"#DESCRIPTION\">DESCRIPTION</a>\n    <a href=\"#OPTIONS\">OPTIONS</a>\n    <a href=\"#RECIPIENTS-AND-IDENTITIES\">RECIPIENTS AND IDENTITIES</a>\n    <a href=\"#EXIT-STATUS\">EXIT STATUS</a>\n    <a href=\"#BACKWARDS-COMPATIBILITY\">BACKWARDS COMPATIBILITY</a>\n    <a href=\"#EXAMPLES\">EXAMPLES</a>\n    <a href=\"#SEE-ALSO\">SEE ALSO</a>\n    <a href=\"#AUTHORS\">AUTHORS</a>\n  </div>\n\n  <ol class='man-decor man-head man head'>\n    <li class='tl'>age(1)</li>\n    <li class='tc'></li>\n    <li class='tr'>age(1)</li>\n  </ol>\n\n  \n\n<h2 id=\"NAME\">NAME</h2>\n<p class=\"man-name\">\n  <code>age</code> - <span class=\"man-whatis\">simple, modern, and secure file encryption</span>\n</p>\n<h2 id=\"SYNOPSIS\">SYNOPSIS</h2>\n\n<p><code>age</code> [<code>--encrypt</code>] (<code>-r</code> <var>RECIPIENT</var> | <code>-R</code> <var>PATH</var>)... [<code>--armor</code>] [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br>\n<code>age</code> [<code>--encrypt</code>] <code>--passphrase</code> [<code>--armor</code>] [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br>\n<code>age</code> <code>--decrypt</code> [<code>-i</code> <var>PATH</var> | <code>-j</code> <var>PLUGIN</var>]... [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br></p>\n\n<h2 id=\"DESCRIPTION\">DESCRIPTION</h2>\n\n<p><code>age</code> encrypts or decrypts <var>INPUT</var> to <var>OUTPUT</var>. The <var>INPUT</var> argument is\noptional and defaults to standard input. Only a single <var>INPUT</var> file may be\nspecified. If <code>-o</code> is not specified, <var>OUTPUT</var> defaults to standard output.</p>\n\n<p>If <code>-p</code>/<code>--passphrase</code> is specified, the file is encrypted with a passphrase\nrequested interactively. Otherwise, it's encrypted to one or more\n<a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">RECIPIENTS</a> specified with <code>-r</code>/<code>--recipient</code> or\n<code>-R</code>/<code>--recipients-file</code>. Every recipient can decrypt the file.</p>\n\n<p>In <code>-d</code>/<code>--decrypt</code> mode, passphrase-encrypted files are detected automatically\nand the passphrase is requested interactively. Otherwise, one or more\n<a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">IDENTITIES</a> specified with <code>-i</code>/<code>--identity</code> are\nused to decrypt the file.</p>\n\n<p><code>age</code> encrypted files are binary and not malleable, with around 200 bytes of\noverhead per recipient, plus 16 bytes every 64KiB of plaintext.</p>\n\n<h2 id=\"OPTIONS\">OPTIONS</h2>\n\n<dl>\n<dt>\n<code>-o</code>, <code>--output</code>=<var>OUTPUT</var>\n</dt>\n<dd>  Write encrypted or decrypted file to <var>OUTPUT</var> instead of standard output.\n  If <var>OUTPUT</var> already exists it will be overwritten.\n\n    <p>If encrypting without <code>--armor</code>, <code>age</code> will refuse to output binary to a\n  TTY. This can be forced by specifying <code>-</code> as <var>OUTPUT</var>.</p>\n</dd>\n<dt><code>--version</code></dt>\n<dd>  Print the version and exit.</dd>\n</dl>\n\n<h3 id=\"Encryption-options\">Encryption options</h3>\n\n<dl>\n<dt>\n<code>-e</code>, <code>--encrypt</code>\n</dt>\n<dd>  Encrypt <var>INPUT</var> to <var>OUTPUT</var>. This is the default.</dd>\n<dt>\n<code>-r</code>, <code>--recipient</code>=<var>RECIPIENT</var>\n</dt>\n<dd>  Encrypt to the explicitly specified <var>RECIPIENT</var>. See the\n  <a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">RECIPIENTS AND IDENTITIES</a> section for possible recipient formats.\n\n    <p>This option can be repeated and combined with other recipient flags,\n  and the file can be decrypted by all provided recipients independently.</p>\n</dd>\n<dt>\n<code>-R</code>, <code>--recipients-file</code>=<var>PATH</var>\n</dt>\n<dd>  Encrypt to the <a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">RECIPIENTS</a> listed in the\n  file at <var>PATH</var>, one per line. Empty lines and lines starting with <code>#</code>\n  are ignored as comments.\n\n    <p>If <var>PATH</var> is <code>-</code>, the recipients are read from standard input. In\n  this case, the <var>INPUT</var> argument must be specified.</p>\n\n    <p>This option can be repeated and combined with other recipient flags,\n  and the file can be decrypted by all provided recipients independently.</p>\n</dd>\n<dt>\n<code>-p</code>, <code>--passphrase</code>\n</dt>\n<dd>  Encrypt with a passphrase, requested interactively from the terminal.\n  <code>age</code> will offer to auto-generate a secure passphrase.\n\n    <p>This option can't be used with other recipient flags.</p>\n</dd>\n<dt>\n<code>-a</code>, <code>--armor</code>\n</dt>\n<dd>  Encrypt to an ASCII-only \"armored\" encoding.\n\n    <p><code>age</code> armor is a strict version of PEM with type <code>AGE ENCRYPTED FILE</code>,\n  canonical \"strict\" Base64, no headers, and no support for leading and\n  trailing extra data.</p>\n\n    <p>Decryption transparently detects and decodes ASCII armoring.</p>\n</dd>\n<dt>\n<code>-i</code>, <code>--identity</code>=<var>PATH</var>\n</dt>\n<dd>  Encrypt to the <a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">RECIPIENTS</a> corresponding to the\n  <a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">IDENTITIES</a> listed in the file at <var>PATH</var>. This\n  is equivalent to converting the file at <var>PATH</var> to a recipients file with\n  <code>age-keygen -y</code> and then passing that to <code>-R</code>/<code>--recipients-file</code>.\n\n    <p>For the format of <var>PATH</var>, see the definition of <code>-i</code>/<code>--identity</code> in the\n  <a href=\"#Decryption-options\" title=\"Decryption options\" data-bare-link=\"true\">Decryption options</a> section.</p>\n\n    <p><code>-e</code>/<code>--encrypt</code> must be explicitly specified when using <code>-i</code>/<code>--identity</code>\n  in encryption mode to avoid confusion.</p>\n</dd>\n<dt>\n<code>-j</code> <var>PLUGIN</var>\n</dt>\n<dd>  Encrypt using the data-less <a href=\"#Plugins\" title=\"Plugins\" data-bare-link=\"true\">plugin</a> <var>PLUGIN</var>.\n\n    <p>This is equivalent to using <code>-i</code>/<code>--identity</code> with a file that contains a\n  single plugin <code>IDENTITY</code> that encodes no plugin-specific data.</p>\n\n    <p><code>-e</code>/<code>--encrypt</code> must be explicitly specified when using <code>-j</code> in encryption\n  mode to avoid confusion.</p>\n</dd>\n</dl>\n\n<h3 id=\"Decryption-options\">Decryption options</h3>\n\n<dl>\n<dt>\n<code>-d</code>, <code>--decrypt</code>\n</dt>\n<dd>  Decrypt <var>INPUT</var> to <var>OUTPUT</var>.\n\n    <p>If <var>INPUT</var> is passphrase encrypted, it will be automatically detected\n  and the passphrase will be requested interactively. Otherwise, the\n  <a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">IDENTITIES</a> specified with <code>-i</code>/<code>--identity</code>\n  are used.</p>\n\n    <p>ASCII armoring is transparently detected and decoded.</p>\n</dd>\n<dt>\n<code>-i</code>, <code>--identity</code>=<var>PATH</var>\n</dt>\n<dd>  Decrypt using the <a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">IDENTITIES</a> at <var>PATH</var>.\n\n    <p><var>PATH</var> may be one of the following:</p>\n\n    <p>a. A file listing <a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">IDENTITIES</a> one per line.\n  Empty lines and lines starting with \"<code>#</code>\" are ignored as comments.</p>\n\n    <p>b. A passphrase encrypted age file, containing\n  <a href=\"#RECIPIENTS-AND-IDENTITIES\" title=\"RECIPIENTS AND IDENTITIES\" data-bare-link=\"true\">IDENTITIES</a> one per line like above.\n  The passphrase is requested interactively. Note that passphrase-protected\n  identity files are not necessary for most use cases, where access to the\n  encrypted identity file implies access to the whole system.</p>\n\n    <p>c. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.\n  If the private key is password-protected, the password is requested\n  interactively only if the SSH identity matches the file. See the\n  <a href=\"#SSH-keys\" title=\"SSH keys\" data-bare-link=\"true\">SSH keys</a> section for more information, including supported key types.</p>\n\n    <p>d. \"<code>-</code>\", causing one of the options above to be read from standard input.\n  In this case, the <var>INPUT</var> argument must be specified.</p>\n\n    <p>This option can be repeated. Identities are tried in the order in which are\n  provided, and the first one matching one of the file's recipients is used.\n  Unused identities are ignored, but it is an error if the <var>INPUT</var> file is\n  passphrase-encrypted and <code>-i</code>/<code>--identity</code> is specified.</p>\n</dd>\n<dt>\n<code>-j</code> <var>PLUGIN</var>\n</dt>\n<dd>  Decrypt using the data-less <a href=\"#Plugins\" title=\"Plugins\" data-bare-link=\"true\">plugin</a> <var>PLUGIN</var>.\n\n    <p>This is equivalent to using <code>-i</code>/<code>--identity</code> with a file that contains a\n  single plugin <code>IDENTITY</code> that encodes no plugin-specific data.</p>\n</dd>\n</dl>\n\n<h2 id=\"RECIPIENTS-AND-IDENTITIES\">RECIPIENTS AND IDENTITIES</h2>\n\n<p><code>RECIPIENTS</code> are public values, like a public key, that a file can be encrypted\nto. <code>IDENTITIES</code> are private values, like a private key, that allow decrypting\na file encrypted to the corresponding <code>RECIPIENT</code>.</p>\n\n<h3 id=\"Native-keys\">Native keys</h3>\n\n<p>Native <code>age</code> key pairs are generated with <a class=\"man-ref\" href=\"age-keygen.1.html\">age-keygen<span class=\"s\">(1)</span></a>, and provide small\nencodings and strong encryption based on X25519 for classic keys, and X25519 +\nML-KEM-768 for post-quantum hybrid keys. The post-quantum hybrid keys are secure\nagainst future quantum computers and are the recommended recipient type for most\napplications.</p>\n\n<p>A hybrid <code>RECIPIENT</code> encoding begins with <code>age1pq1</code> and looks like the following:</p>\n\n<pre><code>age1pq167[... 1950 more characters ...]\n</code></pre>\n\n<p>A hybrid <code>IDENTITY</code> encoding begins with <code>AGE-SECRET-KEY-PQ-1</code> and looks like\nthe following:</p>\n\n<pre><code>AGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T\n</code></pre>\n\n<p>A classic <code>RECIPIENT</code> encoding begins with <code>age1</code> and looks like the following:</p>\n\n<pre><code>age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh\n</code></pre>\n\n<p>A classic <code>IDENTITY</code> encoding begins with <code>AGE-SECRET-KEY-1</code> and looks like the\nfollowing:</p>\n\n<pre><code>AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ\n</code></pre>\n\n<p>A file can't be encrypted to both post-quantum and classic keys, as that would\ndefeat the post-quantum security of the encryption.</p>\n\n<p>An encrypted file can't be linked to the native recipient it's encrypted to\nwithout access to the corresponding identity.</p>\n\n<h3 id=\"SSH-keys\">SSH keys</h3>\n\n<p>As a convenience feature, <code>age</code> also supports encrypting to RSA or Ed25519\n<span class=\"man-ref\">ssh<span class=\"s\">(1)</span></span> keys. RSA keys must be at least 2048 bits. This feature employs more\ncomplex cryptography, and should only be used when a native key is not available\nfor the recipient. Note that SSH keys might not be protected long-term by the\nrecipient, since they are revokable when used only for authentication.</p>\n\n<p>A <code>RECIPIENT</code> encoding is an SSH public key in <code>authorized_keys</code> format\n(see the <code>AUTHORIZED_KEYS FILE FORMAT</code> section of <span class=\"man-ref\">sshd<span class=\"s\">(8)</span></span>), starting with\n<code>ssh-rsa</code> or <code>ssh-ed25519</code>, like the following:</p>\n\n<pre><code>ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8=\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa\n</code></pre>\n\n<p>The comment at the end of the line, if present, is ignored.</p>\n\n<p>In recipient files passed to <code>-R</code>/<code>--recipients-file</code>, unsupported but valid\nSSH public keys are ignored with a warning, to facilitate using\n<code>authorized_keys</code> or GitHub <code>.keys</code> files. (See <a href=\"#EXAMPLES\" title=\"EXAMPLES\" data-bare-link=\"true\">EXAMPLES</a>.)</p>\n\n<p>An <code>IDENTITY</code> is an SSH private key <em>file</em> passed individually to\n<code>-i</code>/<code>--identity</code>. Note that keys held on hardware tokens such as YubiKeys\nor accessed via <span class=\"man-ref\">ssh-agent<span class=\"s\">(1)</span></span> are not supported.</p>\n\n<p>An encrypted file <em>can</em> be linked to the SSH public key it was encrypted to.\nThis is so that <code>age</code> can identify the correct SSH private key before\nrequesting its password, if any.</p>\n\n<h3 id=\"Plugins\">Plugins</h3>\n\n<p><code>age</code> can be extended through plugins. A plugin is only loaded if a corresponding\n<code>RECIPIENT</code> or <code>IDENTITY</code> is specified. (Simply decrypting a file encrypted with\na plugin will not cause it to load, for security reasons among others.)</p>\n\n<p>A <code>RECIPIENT</code> for a plugin named <code>example</code> starts with <code>age1example1</code>, while an\n<code>IDENTITY</code> starts with <code>AGE-PLUGIN-EXAMPLE-1</code>. They both encode arbitrary\nplugin-specific data, and are generated by the plugin.</p>\n\n<p>When either is specified, <code>age</code> searches for <code>age-plugin-example</code> in the PATH\nand executes it to perform the file header encryption or decryption. The plugin\nmay request input from the user through <code>age</code> to complete the operation.</p>\n\n<p>Plugins can be freely mixed with other plugins or natively supported keys.</p>\n\n<p>A plugin is not bound to only encrypt or decrypt files meant for or generated by\nthe plugin. For example, a plugin can be used to decrypt files encrypted to a\nnative X25519 <code>RECIPIENT</code> or even with a passphrase. Similarly, a plugin can\nencrypt a file such that it can be decrypted without the use of any plugin.</p>\n\n<p>Plugins for which the <code>IDENTITY</code>/<code>RECIPIENT</code> distinction doesn't make sense\n(such as a symmetric encryption plugin) may generate only an <code>IDENTITY</code> and\ninstruct the user to perform encryption with the <code>-e</code>/<code>--encrypt</code> and\n<code>-i</code>/<code>--identity</code> flags. Plugins for which the concept of separate identities\ndoesn't make sense (such as a password-encryption plugin) may instruct the user\nto use the <code>-j</code> flag.</p>\n\n<h4 id=\"Tagged-recipients\">Tagged recipients</h4>\n\n<p><code>age</code> can natively encrypt to recipients starting with <code>age1tag1</code> (using P-256\nECDH) or <code>age1tagpq1</code> (using the ML-KEM-768 + P-256 post-quantum hybrid). These\nare intended to be the public side of private keys held in hardware.</p>\n\n<p>They are directly supported to avoid the need to install the plugin, which may\nbe platform-specific, on the encrypting side.</p>\n\n<p>The tag reduces privacy, by allowing an observer to correlate files with a\nrecipient (but not files amongst them without knowledge of the recipient),\nbut this is also a desirable property for hardware keys that require user\ninteraction for each decryption operation.</p>\n\n<h2 id=\"EXIT-STATUS\">EXIT STATUS</h2>\n\n<p><code>age</code> will exit 0 if and only if encryption or decryption are successful for the\nfull length of the input.</p>\n\n<p>If an error occurs during decryption, partial output might still be generated,\nbut only if it was possible to securely authenticate it. No unauthenticated\noutput is ever released.</p>\n\n<h2 id=\"BACKWARDS-COMPATIBILITY\">BACKWARDS COMPATIBILITY</h2>\n\n<p>Files encrypted with a stable version (not alpha, beta, or release candidate) of\n<code>age</code>, or with any v1.0.0 beta or release candidate, will decrypt with any later\nversion of the tool.</p>\n\n<p>If decrypting older files poses a security risk, doing so might cause an error\nby default. In this case, a flag will be provided to force the operation.</p>\n\n<h2 id=\"EXAMPLES\">EXAMPLES</h2>\n\n<p>Generate a new post-quantum identity, encrypt data, and decrypt:</p>\n\n<pre><code>$ age-keygen -pq -o key.txt\nPublic key: age1pq167[... 1950 more characters ...]\n\n$ tar cvz ~/data | age -r age1pq167[...] &gt; data.tar.gz.age\n\n$ age -d -o data.tar.gz -i key.txt data.tar.gz.age\n</code></pre>\n\n<p>Encrypt <code>example.jpg</code> to multiple recipients and output to <code>example.jpg.age</code>:</p>\n\n<pre><code>$ age -o example.jpg.age -r age1pq167[...] -r age1pq1e3[...] example.jpg\n</code></pre>\n\n<p>Encrypt to a list of recipients:</p>\n\n<pre><code>$ cat &gt; recipients.txt\n# Alice\nage1pq167[... 1950 more characters ...]\n# Bob\nage1pq1e3[... 1950 more characters ...]\n\n$ age -R recipients.txt example.jpg &gt; example.jpg.age\n</code></pre>\n\n<p>Encrypt and decrypt a file using a passphrase:</p>\n\n<pre><code>$ age -p secrets.txt &gt; secrets.txt.age\nEnter passphrase (leave empty to autogenerate a secure one):\nUsing the autogenerated passphrase \"release-response-step-brand-wrap-ankle-pair-unusual-sword-train\".\n\n$ age -d secrets.txt.age &gt; secrets.txt\nEnter passphrase:\n</code></pre>\n\n<p>Encrypt and decrypt with a passphrase-protected identity file:</p>\n\n<pre><code>$ age-keygen | age -p &gt; key.age\nPublic key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5\nEnter passphrase (leave empty to autogenerate a secure one):\nUsing the autogenerated passphrase \"hip-roast-boring-snake-mention-east-wasp-honey-input-actress\".\n\n$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt &gt; secrets.txt.age\n\n$ age -d -i key.age secrets.txt.age &gt; secrets.txt\nEnter passphrase for identity file \"key.age\":\n</code></pre>\n\n<p>Encrypt and decrypt with an SSH public key:</p>\n\n<pre><code>$ age -R ~/.ssh/id_ed25519.pub example.jpg &gt; example.jpg.age\n\n$ age -d -i ~/.ssh/id_ed25519 example.jpg.age &gt; example.jpg\n</code></pre>\n\n<p>Encrypt and decrypt with age-plugin-yubikey:</p>\n\n<pre><code>$ age-plugin-yubikey # run interactive setup, generate identity file and obtain recipient\n\n$ age -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets.txt &gt; secrets.txt.age\n\n$ age -d -i age-yubikey-identity-388178f3.txt secrets.txt.age\n</code></pre>\n\n<p>Encrypt to the SSH keys of a GitHub user:</p>\n\n<pre><code>$ curl https://github.com/benjojo.keys | age -R - example.jpg &gt; example.jpg.age\n</code></pre>\n\n<h2 id=\"SEE-ALSO\">SEE ALSO</h2>\n\n<p><a class=\"man-ref\" href=\"age-keygen.1.html\">age-keygen<span class=\"s\">(1)</span></a>, <a class=\"man-ref\" href=\"age-inspect.1.html\">age-inspect<span class=\"s\">(1)</span></a></p>\n\n<h2 id=\"AUTHORS\">AUTHORS</h2>\n\n<p>Filippo Valsorda <a href=\"mailto:age@filippo.io\" data-bare-link=\"true\">age@filippo.io</a></p>\n\n  <ol class='man-decor man-foot man foot'>\n    <li class='tl'></li>\n    <li class='tc'>December 2025</li>\n    <li class='tr'>age(1)</li>\n  </ol>\n\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "doc/age.1.ronn",
    "content": "age(1) -- simple, modern, and secure file encryption\n====================================================\n\n## SYNOPSIS\n\n`age` [`--encrypt`] (`-r` <RECIPIENT> | `-R` <PATH>)... [`--armor`] [`-o` <OUTPUT>] [<INPUT>]<br>\n`age` [`--encrypt`] `--passphrase` [`--armor`] [`-o` <OUTPUT>] [<INPUT>]<br>\n`age` `--decrypt` [`-i` <PATH> | `-j` <PLUGIN>]... [`-o` <OUTPUT>] [<INPUT>]<br>\n\n## DESCRIPTION\n\n`age` encrypts or decrypts <INPUT> to <OUTPUT>. The <INPUT> argument is\noptional and defaults to standard input. Only a single <INPUT> file may be\nspecified. If `-o` is not specified, <OUTPUT> defaults to standard output.\n\nIf `-p`/`--passphrase` is specified, the file is encrypted with a passphrase\nrequested interactively. Otherwise, it's encrypted to one or more\n[RECIPIENTS][RECIPIENTS AND IDENTITIES] specified with `-r`/`--recipient` or\n`-R`/`--recipients-file`. Every recipient can decrypt the file.\n\nIn `-d`/`--decrypt` mode, passphrase-encrypted files are detected automatically\nand the passphrase is requested interactively. Otherwise, one or more\n[IDENTITIES][RECIPIENTS AND IDENTITIES] specified with `-i`/`--identity` are\nused to decrypt the file.\n\n`age` encrypted files are binary and not malleable, with around 200 bytes of\noverhead per recipient, plus 16 bytes every 64KiB of plaintext.\n\n## OPTIONS\n\n* `-o`, `--output`=<OUTPUT>:\n    Write encrypted or decrypted file to <OUTPUT> instead of standard output.\n    If <OUTPUT> already exists it will be overwritten.\n\n    If encrypting without `--armor`, `age` will refuse to output binary to a\n    TTY. This can be forced by specifying `-` as <OUTPUT>.\n\n* `--version`:\n    Print the version and exit.\n\n### Encryption options\n\n* `-e`, `--encrypt`:\n    Encrypt <INPUT> to <OUTPUT>. This is the default.\n\n* `-r`, `--recipient`=<RECIPIENT>:\n    Encrypt to the explicitly specified <RECIPIENT>. See the\n    [RECIPIENTS AND IDENTITIES][] section for possible recipient formats.\n\n    This option can be repeated and combined with other recipient flags,\n    and the file can be decrypted by all provided recipients independently.\n\n* `-R`, `--recipients-file`=<PATH>:\n    Encrypt to the [RECIPIENTS][RECIPIENTS AND IDENTITIES] listed in the\n    file at <PATH>, one per line. Empty lines and lines starting with `#`\n    are ignored as comments.\n\n    If <PATH> is `-`, the recipients are read from standard input. In\n    this case, the <INPUT> argument must be specified.\n\n    This option can be repeated and combined with other recipient flags,\n    and the file can be decrypted by all provided recipients independently.\n\n* `-p`, `--passphrase`:\n    Encrypt with a passphrase, requested interactively from the terminal.\n    `age` will offer to auto-generate a secure passphrase.\n\n    This option can't be used with other recipient flags.\n\n* `-a`, `--armor`:\n    Encrypt to an ASCII-only \"armored\" encoding.\n\n    `age` armor is a strict version of PEM with type `AGE ENCRYPTED FILE`,\n    canonical \"strict\" Base64, no headers, and no support for leading and\n    trailing extra data.\n\n    Decryption transparently detects and decodes ASCII armoring.\n\n* `-i`, `--identity`=<PATH>:\n    Encrypt to the [RECIPIENTS][RECIPIENTS AND IDENTITIES] corresponding to the\n    [IDENTITIES][RECIPIENTS AND IDENTITIES] listed in the file at <PATH>. This\n    is equivalent to converting the file at <PATH> to a recipients file with\n    `age-keygen -y` and then passing that to `-R`/`--recipients-file`.\n\n    For the format of <PATH>, see the definition of `-i`/`--identity` in the\n    [Decryption options][] section.\n\n    `-e`/`--encrypt` must be explicitly specified when using `-i`/`--identity`\n    in encryption mode to avoid confusion.\n\n* `-j` <PLUGIN>:\n    Encrypt using the data-less [plugin][Plugins] <PLUGIN>.\n\n    This is equivalent to using `-i`/`--identity` with a file that contains a\n    single plugin `IDENTITY` that encodes no plugin-specific data.\n\n    `-e`/`--encrypt` must be explicitly specified when using `-j` in encryption\n    mode to avoid confusion.\n\n### Decryption options\n\n* `-d`, `--decrypt`:\n    Decrypt <INPUT> to <OUTPUT>.\n\n    If <INPUT> is passphrase encrypted, it will be automatically detected\n    and the passphrase will be requested interactively. Otherwise, the\n    [IDENTITIES][RECIPIENTS AND IDENTITIES] specified with `-i`/`--identity`\n    are used.\n\n    ASCII armoring is transparently detected and decoded.\n\n* `-i`, `--identity`=<PATH>:\n    Decrypt using the [IDENTITIES][RECIPIENTS AND IDENTITIES] at <PATH>.\n\n    <PATH> may be one of the following:\n\n    a\\. A file listing [IDENTITIES][RECIPIENTS AND IDENTITIES] one per line.\n    Empty lines and lines starting with \"`#`\" are ignored as comments.\n\n    b\\. A passphrase encrypted age file, containing\n    [IDENTITIES][RECIPIENTS AND IDENTITIES] one per line like above.\n    The passphrase is requested interactively. Note that passphrase-protected\n    identity files are not necessary for most use cases, where access to the\n    encrypted identity file implies access to the whole system.\n\n    c\\. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.\n    If the private key is password-protected, the password is requested\n    interactively only if the SSH identity matches the file. See the\n    [SSH keys][] section for more information, including supported key types.\n\n    d\\. \"`-`\", causing one of the options above to be read from standard input.\n    In this case, the <INPUT> argument must be specified.\n\n    This option can be repeated. Identities are tried in the order in which are\n    provided, and the first one matching one of the file's recipients is used.\n    Unused identities are ignored, but it is an error if the <INPUT> file is\n    passphrase-encrypted and `-i`/`--identity` is specified.\n\n* `-j` <PLUGIN>:\n    Decrypt using the data-less [plugin][Plugins] <PLUGIN>.\n\n    This is equivalent to using `-i`/`--identity` with a file that contains a\n    single plugin `IDENTITY` that encodes no plugin-specific data.\n\n## RECIPIENTS AND IDENTITIES\n\n`RECIPIENTS` are public values, like a public key, that a file can be encrypted\nto. `IDENTITIES` are private values, like a private key, that allow decrypting\na file encrypted to the corresponding `RECIPIENT`.\n\n### Native keys\n\nNative `age` key pairs are generated with age-keygen(1), and provide small\nencodings and strong encryption based on X25519 for classic keys, and X25519 +\nML-KEM-768 for post-quantum hybrid keys. The post-quantum hybrid keys are secure\nagainst future quantum computers and are the recommended recipient type for most\napplications.\n\nA hybrid `RECIPIENT` encoding begins with `age1pq1` and looks like the following:\n\n    age1pq167[... 1950 more characters ...]\n\nA hybrid `IDENTITY` encoding begins with `AGE-SECRET-KEY-PQ-1` and looks like\nthe following:\n\n    AGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T\n\nA classic `RECIPIENT` encoding begins with `age1` and looks like the following:\n\n    age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh\n\nA classic `IDENTITY` encoding begins with `AGE-SECRET-KEY-1` and looks like the\nfollowing:\n\n    AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ\n\nA file can't be encrypted to both post-quantum and classic keys, as that would\ndefeat the post-quantum security of the encryption.\n\nAn encrypted file can't be linked to the native recipient it's encrypted to\nwithout access to the corresponding identity.\n\n### SSH keys\n\nAs a convenience feature, `age` also supports encrypting to RSA or Ed25519\nssh(1) keys. RSA keys must be at least 2048 bits. This feature employs more\ncomplex cryptography, and should only be used when a native key is not available\nfor the recipient. Note that SSH keys might not be protected long-term by the\nrecipient, since they are revokable when used only for authentication.\n\nA `RECIPIENT` encoding is an SSH public key in `authorized_keys` format\n(see the `AUTHORIZED_KEYS FILE FORMAT` section of sshd(8)), starting with\n`ssh-rsa` or `ssh-ed25519`, like the following:\n\n    ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8=\n    ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa\n\nThe comment at the end of the line, if present, is ignored.\n\nIn recipient files passed to `-R`/`--recipients-file`, unsupported but valid\nSSH public keys are ignored with a warning, to facilitate using\n`authorized_keys` or GitHub `.keys` files. (See [EXAMPLES][].)\n\nAn `IDENTITY` is an SSH private key _file_ passed individually to\n`-i`/`--identity`. Note that keys held on hardware tokens such as YubiKeys\nor accessed via ssh-agent(1) are not supported.\n\nAn encrypted file _can_ be linked to the SSH public key it was encrypted to.\nThis is so that `age` can identify the correct SSH private key before\nrequesting its password, if any.\n\n### Plugins\n\n`age` can be extended through plugins. A plugin is only loaded if a corresponding\n`RECIPIENT` or `IDENTITY` is specified. (Simply decrypting a file encrypted with\na plugin will not cause it to load, for security reasons among others.)\n\nA `RECIPIENT` for a plugin named `example` starts with `age1example1`, while an\n`IDENTITY` starts with `AGE-PLUGIN-EXAMPLE-1`. They both encode arbitrary\nplugin-specific data, and are generated by the plugin.\n\nWhen either is specified, `age` searches for `age-plugin-example` in the PATH\nand executes it to perform the file header encryption or decryption. The plugin\nmay request input from the user through `age` to complete the operation.\n\nPlugins can be freely mixed with other plugins or natively supported keys.\n\nA plugin is not bound to only encrypt or decrypt files meant for or generated by\nthe plugin. For example, a plugin can be used to decrypt files encrypted to a\nnative X25519 `RECIPIENT` or even with a passphrase. Similarly, a plugin can\nencrypt a file such that it can be decrypted without the use of any plugin.\n\nPlugins for which the `IDENTITY`/`RECIPIENT` distinction doesn't make sense\n(such as a symmetric encryption plugin) may generate only an `IDENTITY` and\ninstruct the user to perform encryption with the `-e`/`--encrypt` and\n`-i`/`--identity` flags. Plugins for which the concept of separate identities\ndoesn't make sense (such as a password-encryption plugin) may instruct the user\nto use the `-j` flag.\n\n#### Tagged recipients\n\n`age` can natively encrypt to recipients starting with `age1tag1` (using P-256\nECDH) or `age1tagpq1` (using the ML-KEM-768 + P-256 post-quantum hybrid). These\nare intended to be the public side of private keys held in hardware.\n\nThey are directly supported to avoid the need to install the plugin, which may\nbe platform-specific, on the encrypting side.\n\nThe tag reduces privacy, by allowing an observer to correlate files with a\nrecipient (but not files amongst them without knowledge of the recipient),\nbut this is also a desirable property for hardware keys that require user\ninteraction for each decryption operation.\n\n## EXIT STATUS\n\n`age` will exit 0 if and only if encryption or decryption are successful for the\nfull length of the input.\n\nIf an error occurs during decryption, partial output might still be generated,\nbut only if it was possible to securely authenticate it. No unauthenticated\noutput is ever released.\n\n## BACKWARDS COMPATIBILITY\n\nFiles encrypted with a stable version (not alpha, beta, or release candidate) of\n`age`, or with any v1.0.0 beta or release candidate, will decrypt with any later\nversion of the tool.\n\nIf decrypting older files poses a security risk, doing so might cause an error\nby default. In this case, a flag will be provided to force the operation.\n\n## EXAMPLES\n\nGenerate a new post-quantum identity, encrypt data, and decrypt:\n\n    $ age-keygen -pq -o key.txt\n    Public key: age1pq167[... 1950 more characters ...]\n\n    $ tar cvz ~/data | age -r age1pq167[...] > data.tar.gz.age\n\n    $ age -d -o data.tar.gz -i key.txt data.tar.gz.age\n\nEncrypt `example.jpg` to multiple recipients and output to `example.jpg.age`:\n\n    $ age -o example.jpg.age -r age1pq167[...] -r age1pq1e3[...] example.jpg\n\nEncrypt to a list of recipients:\n\n    $ cat > recipients.txt\n    # Alice\n    age1pq167[... 1950 more characters ...]\n    # Bob\n    age1pq1e3[... 1950 more characters ...]\n\n    $ age -R recipients.txt example.jpg > example.jpg.age\n\nEncrypt and decrypt a file using a passphrase:\n\n    $ age -p secrets.txt > secrets.txt.age\n    Enter passphrase (leave empty to autogenerate a secure one):\n    Using the autogenerated passphrase \"release-response-step-brand-wrap-ankle-pair-unusual-sword-train\".\n\n    $ age -d secrets.txt.age > secrets.txt\n    Enter passphrase:\n\nEncrypt and decrypt with a passphrase-protected identity file:\n\n    $ age-keygen | age -p > key.age\n    Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5\n    Enter passphrase (leave empty to autogenerate a secure one):\n    Using the autogenerated passphrase \"hip-roast-boring-snake-mention-east-wasp-honey-input-actress\".\n\n    $ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age\n\n    $ age -d -i key.age secrets.txt.age > secrets.txt\n    Enter passphrase for identity file \"key.age\":\n\nEncrypt and decrypt with an SSH public key:\n\n    $ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age\n\n    $ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg\n\nEncrypt and decrypt with age-plugin-yubikey:\n\n    $ age-plugin-yubikey # run interactive setup, generate identity file and obtain recipient\n\n    $ age -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets.txt > secrets.txt.age\n\n    $ age -d -i age-yubikey-identity-388178f3.txt secrets.txt.age\n\nEncrypt to the SSH keys of a GitHub user:\n\n    $ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age\n\n## SEE ALSO\n\nage-keygen(1), age-inspect(1)\n\n## AUTHORS\n\nFilippo Valsorda <age@filippo.io>\n"
  },
  {
    "path": "extra/age-plugin-pq/plugin-pq.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/internal/bech32\"\n\t\"filippo.io/age/plugin\"\n)\n\nconst usage = `Usage:\n    age-plugin-pq -identity [-o OUTPUT] [INPUT]\n\nOptions:\n    -identity                 Convert one or more native post-quantum identities from\n                              INPUT or from standard input to plugin identities.\n    -o, --output OUTPUT       Write the result to the file at path OUTPUT instead of\n                              standard output.\n\nage-plugin-pq is an age plugin for post-quantum hybrid ML-KEM-768 + X25519\nrecipients and identities. These are supported natively by age v1.3.0 and later,\nbut this plugin can be placed in $PATH to add support to any version and\nimplementation of age that supports plugins.\n\nRecipients work out of the box, while identities need to be converted to plugin\nidentities with -identity. If OUTPUT already exists, it is not overwritten.`\n\n// Version can be set at link time to override debug.BuildInfo.Main.Version when\n// building manually without git history. It should look like \"v1.2.3\".\nvar Version string\n\nfunc main() {\n\tlog.SetFlags(0)\n\n\tp, err := plugin.New(\"pq\")\n\tif err != nil {\n\t\terrorf(\"failed to create plugin: %v\", err)\n\t}\n\tp.RegisterFlags(nil)\n\n\tflag.Usage = func() { fmt.Fprintf(os.Stderr, \"%s\\n\", usage) }\n\n\tvar outFlag string\n\tvar versionFlag, identityFlag bool\n\tflag.BoolVar(&versionFlag, \"version\", false, \"print the version\")\n\tflag.BoolVar(&identityFlag, \"identity\", false, \"convert identities to plugin identities\")\n\tflag.StringVar(&outFlag, \"o\", \"\", \"output to `FILE` (default stdout)\")\n\tflag.StringVar(&outFlag, \"output\", \"\", \"output to `FILE` (default stdout)\")\n\tflag.Parse()\n\n\tif versionFlag {\n\t\tif buildInfo, ok := debug.ReadBuildInfo(); ok && Version == \"\" {\n\t\t\tVersion = buildInfo.Main.Version\n\t\t}\n\t\tfmt.Println(Version)\n\t\treturn\n\t}\n\n\tif identityFlag {\n\t\tif len(flag.Args()) > 1 {\n\t\t\terrorf(\"too many arguments\")\n\t\t}\n\n\t\tout := os.Stdout\n\t\tif outFlag != \"\" {\n\t\t\tf, err := os.OpenFile(outFlag, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)\n\t\t\tif err != nil {\n\t\t\t\terrorf(\"failed to open output file %q: %v\", outFlag, err)\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\tif err := f.Close(); err != nil {\n\t\t\t\t\terrorf(\"failed to close output file %q: %v\", outFlag, err)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tout = f\n\t\t}\n\t\tif fi, err := out.Stat(); err == nil && fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {\n\t\t\twarning(\"writing secret key to a world-readable file\")\n\t\t}\n\n\t\tin := os.Stdin\n\t\tif inFile := flag.Arg(0); inFile != \"\" && inFile != \"-\" {\n\t\t\tf, err := os.Open(inFile)\n\t\t\tif err != nil {\n\t\t\t\terrorf(\"failed to open input file %q: %v\", inFile, err)\n\t\t\t}\n\t\t\tdefer f.Close()\n\t\t\tin = f\n\t\t}\n\n\t\tconvert(in, out)\n\t\treturn\n\t}\n\n\tp.HandleRecipientEncoding(func(s string) (age.Recipient, error) {\n\t\treturn age.ParseHybridRecipient(s)\n\t})\n\tp.HandleIdentity(func(data []byte) (age.Identity, error) {\n\t\t// Convert from a AGE-PLUGIN-PQ-1... payload to a\n\t\t// AGE-SECRET-KEY-PQ-1... identity encoding.\n\t\ts, err := bech32.Encode(\"AGE-SECRET-KEY-PQ-\", data)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn age.ParseHybridIdentity(s)\n\t})\n\tp.HandleIdentityAsRecipient(func(data []byte) (age.Recipient, error) {\n\t\ts, err := bech32.Encode(\"AGE-SECRET-KEY-PQ-\", data)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\ti, err := age.ParseHybridIdentity(s)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn i.Recipient(), nil\n\t})\n\tos.Exit(p.Main())\n}\n\nfunc convert(in io.Reader, out io.Writer) {\n\tids, err := age.ParseIdentities(in)\n\tif err != nil {\n\t\terrorf(\"failed to parse identities: %v\", err)\n\t}\n\tfor i, id := range ids {\n\t\thybridID, ok := id.(*age.HybridIdentity)\n\t\tif !ok {\n\t\t\terrorf(\"identity #%d is not a post-quantum hybrid identity\", i+1)\n\t\t}\n\t\t_, data, err := bech32.Decode(hybridID.String())\n\t\tif err != nil {\n\t\t\terrorf(\"failed to decode identity #%d: %v\", i+1, err)\n\t\t}\n\t\tfmt.Fprintln(out, plugin.EncodeIdentity(\"pq\", data))\n\t}\n}\n\nfunc errorf(format string, v ...any) {\n\tlog.Printf(\"age-plugin-pq: error: \"+format, v...)\n\tlog.Fatalf(\"age-plugin-pq: report unexpected or unhelpful errors at https://filippo.io/age/report\")\n}\n\nfunc warning(msg string) {\n\tlog.Printf(\"age-plugin-pq: warning: %s\", msg)\n}\n"
  },
  {
    "path": "extra/age-plugin-tag/plugin-tag.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/plugin\"\n\t\"filippo.io/age/tag\"\n)\n\nconst usage = `age-plugin-tag is an age plugin for P-256 tagged recipients. These are supported\nnatively by age v1.3.0 and later, but this plugin can be placed in $PATH to add\nsupport to any version and implementation of age that supports plugins.\n\nUsually, tagged recipients are the public side of private keys held in hardware,\nwhere the identity side is handled by a different plugin.`\n\n// Version can be set at link time to override debug.BuildInfo.Main.Version when\n// building manually without git history. It should look like \"v1.2.3\".\nvar Version string\n\nfunc main() {\n\tflag.Usage = func() { fmt.Fprintf(os.Stderr, \"%s\\n\", usage) }\n\n\tp, err := plugin.New(\"tag\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tp.RegisterFlags(nil)\n\n\tversionFlag := flag.Bool(\"version\", false, \"print the version\")\n\tflag.Parse()\n\n\tif *versionFlag {\n\t\tif buildInfo, ok := debug.ReadBuildInfo(); ok && Version == \"\" {\n\t\t\tVersion = buildInfo.Main.Version\n\t\t}\n\t\tfmt.Println(Version)\n\t\treturn\n\t}\n\n\tp.HandleRecipient(func(b []byte) (age.Recipient, error) {\n\t\treturn tag.NewClassicRecipient(b)\n\t})\n\n\tos.Exit(p.Main())\n}\n"
  },
  {
    "path": "extra/age-plugin-tagpq/plugin-tagpq.go",
    "content": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"runtime/debug\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/plugin\"\n\t\"filippo.io/age/tag\"\n)\n\nconst usage = `age-plugin-tagpq is an age plugin for ML-KEM-768 + P-256 post-quantum hybrid\ntagged recipients. These are supported natively by age v1.3.0 and later, but\nthis plugin can be placed in $PATH to add support to any version and\nimplementation of age that supports plugins.\n\nUsually, tagged recipients are the public side of private keys held in hardware,\nwhere the identity side is handled by a different plugin.`\n\n// Version can be set at link time to override debug.BuildInfo.Main.Version when\n// building manually without git history. It should look like \"v1.2.3\".\nvar Version string\n\nfunc main() {\n\tflag.Usage = func() { fmt.Fprintf(os.Stderr, \"%s\\n\", usage) }\n\n\tp, err := plugin.New(\"tagpq\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tp.RegisterFlags(nil)\n\n\tversionFlag := flag.Bool(\"version\", false, \"print the version\")\n\tflag.Parse()\n\n\tif *versionFlag {\n\t\tif buildInfo, ok := debug.ReadBuildInfo(); ok && Version == \"\" {\n\t\t\tVersion = buildInfo.Main.Version\n\t\t}\n\t\tfmt.Println(Version)\n\t\treturn\n\t}\n\n\tp.HandleRecipient(func(b []byte) (age.Recipient, error) {\n\t\treturn tag.NewHybridRecipient(b)\n\t})\n\n\tos.Exit(p.Main())\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module filippo.io/age\n\ngo 1.24.0\n\n// Release build version.\ntoolchain go1.25.5\n\nrequire (\n\tfilippo.io/edwards25519 v1.1.0\n\tfilippo.io/hpke v0.4.0\n\tfilippo.io/nistec v0.0.4\n\tgolang.org/x/crypto v0.45.0\n\tgolang.org/x/sys v0.38.0\n\tgolang.org/x/term v0.37.0\n)\n\n// Test dependencies.\nrequire (\n\tc2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd\n\tgithub.com/rogpeppe/go-internal v1.14.1\n\tgolang.org/x/tools v0.39.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=\nc2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\nfilippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=\nfilippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=\nfilippo.io/nistec v0.0.4 h1:F14ZHT5htWlMnQVPndX9ro9arf56cBhQxq4LnDI491s=\nfilippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngolang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=\ngolang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=\ngolang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=\ngolang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=\ngolang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=\n"
  },
  {
    "path": "internal/bech32/bech32.go",
    "content": "// Copyright (c) 2017 Takatoshi Nakagawa\n// Copyright (c) 2019 The age Authors\n//\n// Permission is hereby granted, free of charge, to any person obtaining a copy\n// of this software and associated documentation files (the \"Software\"), to deal\n// in the Software without restriction, including without limitation the rights\n// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n// copies of the Software, and to permit persons to whom the Software is\n// furnished to do so, subject to the following conditions:\n//\n// The above copyright notice and this permission notice shall be included in\n// all copies or substantial portions of the Software.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\n// THE SOFTWARE.\n\n// Package bech32 is a modified version of the reference implementation of BIP173.\npackage bech32\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nvar charset = \"qpzry9x8gf2tvdw0s3jn54khce6mua7l\"\n\nvar generator = []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}\n\nfunc polymod(values []byte) uint32 {\n\tchk := uint32(1)\n\tfor _, v := range values {\n\t\ttop := chk >> 25\n\t\tchk = (chk & 0x1ffffff) << 5\n\t\tchk = chk ^ uint32(v)\n\t\tfor i := range 5 {\n\t\t\tbit := top >> i & 1\n\t\t\tif bit == 1 {\n\t\t\t\tchk ^= generator[i]\n\t\t\t}\n\t\t}\n\t}\n\treturn chk\n}\n\nfunc hrpExpand(hrp string) []byte {\n\th := []byte(strings.ToLower(hrp))\n\tvar ret []byte\n\tfor _, c := range h {\n\t\tret = append(ret, c>>5)\n\t}\n\tret = append(ret, 0)\n\tfor _, c := range h {\n\t\tret = append(ret, c&31)\n\t}\n\treturn ret\n}\n\nfunc verifyChecksum(hrp string, data []byte) bool {\n\treturn polymod(append(hrpExpand(hrp), data...)) == 1\n}\n\nfunc createChecksum(hrp string, data []byte) []byte {\n\tvalues := append(hrpExpand(hrp), data...)\n\tvalues = append(values, []byte{0, 0, 0, 0, 0, 0}...)\n\tmod := polymod(values) ^ 1\n\tret := make([]byte, 6)\n\tfor p := range ret {\n\t\tshift := 5 * (5 - p)\n\t\tret[p] = byte(mod>>shift) & 31\n\t}\n\treturn ret\n}\n\nfunc convertBits(data []byte, frombits, tobits byte, pad bool) ([]byte, error) {\n\tvar ret []byte\n\tacc := uint32(0)\n\tbits := byte(0)\n\tmaxv := byte(1<<tobits - 1)\n\tfor idx, value := range data {\n\t\tif value>>frombits != 0 {\n\t\t\treturn nil, fmt.Errorf(\"invalid data range: data[%d]=%d (frombits=%d)\", idx, value, frombits)\n\t\t}\n\t\tacc = acc<<frombits | uint32(value)\n\t\tbits += frombits\n\t\tfor bits >= tobits {\n\t\t\tbits -= tobits\n\t\t\tret = append(ret, byte(acc>>bits)&maxv)\n\t\t}\n\t}\n\tif pad {\n\t\tif bits > 0 {\n\t\t\tret = append(ret, byte(acc<<(tobits-bits))&maxv)\n\t\t}\n\t} else if bits >= frombits {\n\t\treturn nil, fmt.Errorf(\"illegal zero padding\")\n\t} else if byte(acc<<(tobits-bits))&maxv != 0 {\n\t\treturn nil, fmt.Errorf(\"non-zero padding\")\n\t}\n\treturn ret, nil\n}\n\n// Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase,\n// the output will be uppercase.\nfunc Encode(hrp string, data []byte) (string, error) {\n\tvalues, err := convertBits(data, 8, 5, true)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(hrp) < 1 {\n\t\treturn \"\", fmt.Errorf(\"invalid HRP: %q\", hrp)\n\t}\n\tfor p, c := range hrp {\n\t\tif c < 33 || c > 126 {\n\t\t\treturn \"\", fmt.Errorf(\"invalid HRP character: hrp[%d]=%d\", p, c)\n\t\t}\n\t}\n\tif strings.ToUpper(hrp) != hrp && strings.ToLower(hrp) != hrp {\n\t\treturn \"\", fmt.Errorf(\"mixed case HRP: %q\", hrp)\n\t}\n\tlower := strings.ToLower(hrp) == hrp\n\thrp = strings.ToLower(hrp)\n\tvar ret strings.Builder\n\tret.WriteString(hrp)\n\tret.WriteString(\"1\")\n\tfor _, p := range values {\n\t\tret.WriteByte(charset[p])\n\t}\n\tfor _, p := range createChecksum(hrp, values) {\n\t\tret.WriteByte(charset[p])\n\t}\n\tif lower {\n\t\treturn ret.String(), nil\n\t}\n\treturn strings.ToUpper(ret.String()), nil\n}\n\n// Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase.\nfunc Decode(s string) (hrp string, data []byte, err error) {\n\tif strings.ToLower(s) != s && strings.ToUpper(s) != s {\n\t\treturn \"\", nil, fmt.Errorf(\"mixed case\")\n\t}\n\tpos := strings.LastIndex(s, \"1\")\n\tif pos < 1 || pos+7 > len(s) {\n\t\treturn \"\", nil, fmt.Errorf(\"separator '1' at invalid position: pos=%d, len=%d\", pos, len(s))\n\t}\n\thrp = s[:pos]\n\tfor p, c := range hrp {\n\t\tif c < 33 || c > 126 {\n\t\t\treturn \"\", nil, fmt.Errorf(\"invalid character human-readable part: s[%d]=%d\", p, c)\n\t\t}\n\t}\n\ts = strings.ToLower(s)\n\tfor p, c := range s[pos+1:] {\n\t\td := strings.IndexRune(charset, c)\n\t\tif d == -1 {\n\t\t\treturn \"\", nil, fmt.Errorf(\"invalid character data part: s[%d]=%v\", p, c)\n\t\t}\n\t\tdata = append(data, byte(d))\n\t}\n\tif !verifyChecksum(hrp, data) {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid checksum\")\n\t}\n\tdata, err = convertBits(data[:len(data)-6], 5, 8, false)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\treturn hrp, data, nil\n}\n"
  },
  {
    "path": "internal/bech32/bech32_test.go",
    "content": "// Copyright (c) 2013-2017 The btcsuite developers\n// Copyright (c) 2016-2017 The Lightning Network Developers\n// Copyright (c) 2019 The age Authors\n//\n// Permission to use, copy, modify, and distribute this software for any\n// purpose with or without fee is hereby granted, provided that the above\n// copyright notice and this permission notice appear in all copies.\n//\n// THE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES\n// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF\n// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR\n// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES\n// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN\n// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF\n// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.\n\npackage bech32_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"filippo.io/age/internal/bech32\"\n)\n\nfunc TestBech32(t *testing.T) {\n\ttests := []struct {\n\t\tstr   string\n\t\tvalid bool\n\t}{\n\t\t{\"A12UEL5L\", true}, // empty\n\t\t{\"a12uel5l\", true},\n\t\t{\"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs\", true},\n\t\t{\"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw\", true},\n\t\t{\"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j\", true},\n\t\t{\"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w\", true},\n\n\t\t// invalid checksum\n\t\t{\"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w\", false},\n\t\t// invalid character (space) in hrp\n\t\t{\"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p\", false},\n\t\t{\"split1cheo2y9e2w\", false}, // invalid character (o) in data part\n\t\t{\"split1a2y9w\", false},      // too short data part\n\t\t{\"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w\", false}, // empty hrp\n\t\t// invalid character (DEL) in hrp\n\t\t{\"spl\" + string(rune(127)) + \"t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w\", false},\n\n\t\t// long vectors that we do accept despite the spec, see Issue 453\n\t\t{\"long10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qfcsvr0\", true},\n\t\t{\"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx\", true},\n\n\t\t// BIP 173 invalid vectors.\n\t\t{\"pzry9x0s0muk\", false},\n\t\t{\"1pzry9x0s0muk\", false},\n\t\t{\"x1b4n0q5v\", false},\n\t\t{\"li1dgmt3\", false},\n\t\t{\"de1lg7wt\\xff\", false},\n\t\t{\"A1G7SGD8\", false},\n\t\t{\"10a06t8\", false},\n\t\t{\"1qzzfhee\", false},\n\t}\n\n\tfor _, test := range tests {\n\t\tstr := test.str\n\t\thrp, decoded, err := bech32.Decode(str)\n\t\tif !test.valid {\n\t\t\t// Invalid string decoding should result in error.\n\t\t\tif err == nil {\n\t\t\t\tt.Errorf(\"expected decoding to fail for invalid string %v\", test.str)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Valid string decoding should result in no error.\n\t\tif err != nil {\n\t\t\tt.Errorf(\"expected string to be valid bech32: %v\", err)\n\t\t}\n\n\t\t// Check that it encodes to the same string.\n\t\tencoded, err := bech32.Encode(hrp, decoded)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"encoding failed: %v\", err)\n\t\t}\n\t\tif encoded != str {\n\t\t\tt.Errorf(\"expected data to encode to %v, but got %v\", str, encoded)\n\t\t}\n\n\t\t// Flip a bit in the string an make sure it is caught.\n\t\tpos := strings.LastIndexAny(str, \"1\")\n\t\tflipped := str[:pos+1] + string((str[pos+1] ^ 1)) + str[pos+2:]\n\t\tif _, _, err = bech32.Decode(flipped); err == nil {\n\t\t\tt.Error(\"expected decoding to fail\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/format/format.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package format implements the age file format.\npackage format\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n)\n\ntype Header struct {\n\tRecipients []*Stanza\n\tMAC        []byte\n}\n\n// Stanza is assignable to age.Stanza, and if this package is made public,\n// age.Stanza can be made a type alias of this type.\ntype Stanza struct {\n\tType string\n\tArgs []string\n\tBody []byte\n}\n\nvar b64 = base64.RawStdEncoding.Strict()\n\nfunc DecodeString(s string) ([]byte, error) {\n\t// CR and LF are ignored by DecodeString, but we don't want any malleability.\n\tif strings.ContainsAny(s, \"\\n\\r\") {\n\t\treturn nil, errors.New(`unexpected newline character`)\n\t}\n\treturn b64.DecodeString(s)\n}\n\nvar EncodeToString = b64.EncodeToString\n\nconst ColumnsPerLine = 64\n\nconst BytesPerLine = ColumnsPerLine / 4 * 3\n\n// NewWrappedBase64Encoder returns a WrappedBase64Encoder that writes to dst.\nfunc NewWrappedBase64Encoder(enc *base64.Encoding, dst io.Writer) *WrappedBase64Encoder {\n\tw := &WrappedBase64Encoder{dst: dst}\n\tw.enc = base64.NewEncoder(enc, WriterFunc(w.writeWrapped))\n\treturn w\n}\n\ntype WriterFunc func(p []byte) (int, error)\n\nfunc (f WriterFunc) Write(p []byte) (int, error) { return f(p) }\n\n// WrappedBase64Encoder is a standard base64 encoder that inserts an LF\n// character every ColumnsPerLine bytes. It does not insert a newline neither at\n// the beginning nor at the end of the stream, but it ensures the last line is\n// shorter than ColumnsPerLine, which means it might be empty.\ntype WrappedBase64Encoder struct {\n\tenc     io.WriteCloser\n\tdst     io.Writer\n\twritten int\n\tbuf     bytes.Buffer\n}\n\nfunc (w *WrappedBase64Encoder) Write(p []byte) (int, error) { return w.enc.Write(p) }\n\nfunc (w *WrappedBase64Encoder) Close() error {\n\treturn w.enc.Close()\n}\n\nfunc (w *WrappedBase64Encoder) writeWrapped(p []byte) (int, error) {\n\tif w.buf.Len() != 0 {\n\t\tpanic(\"age: internal error: non-empty WrappedBase64Encoder.buf\")\n\t}\n\tfor len(p) > 0 {\n\t\ttoWrite := min(ColumnsPerLine-(w.written%ColumnsPerLine), len(p))\n\t\tn, _ := w.buf.Write(p[:toWrite])\n\t\tw.written += n\n\t\tp = p[n:]\n\t\tif w.written%ColumnsPerLine == 0 {\n\t\t\tw.buf.Write([]byte(\"\\n\"))\n\t\t}\n\t}\n\tif _, err := w.buf.WriteTo(w.dst); err != nil {\n\t\t// We always return n = 0 on error because it's hard to work back to the\n\t\t// input length that ended up written out. Not ideal, but Write errors\n\t\t// are not recoverable anyway.\n\t\treturn 0, err\n\t}\n\treturn len(p), nil\n}\n\n// LastLineIsEmpty returns whether the last output line was empty, either\n// because no input was written, or because a multiple of BytesPerLine was.\n//\n// Calling LastLineIsEmpty before Close is meaningless.\nfunc (w *WrappedBase64Encoder) LastLineIsEmpty() bool {\n\treturn w.written%ColumnsPerLine == 0\n}\n\nconst intro = \"age-encryption.org/v1\\n\"\n\nvar stanzaPrefix = []byte(\"->\")\nvar footerPrefix = []byte(\"---\")\n\nfunc (r *Stanza) Marshal(w io.Writer) error {\n\tif _, err := w.Write(stanzaPrefix); err != nil {\n\t\treturn err\n\t}\n\tfor _, a := range append([]string{r.Type}, r.Args...) {\n\t\tif _, err := io.WriteString(w, \" \"+a); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif _, err := io.WriteString(w, \"\\n\"); err != nil {\n\t\treturn err\n\t}\n\tww := NewWrappedBase64Encoder(b64, w)\n\tif _, err := ww.Write(r.Body); err != nil {\n\t\treturn err\n\t}\n\tif err := ww.Close(); err != nil {\n\t\treturn err\n\t}\n\t_, err := io.WriteString(w, \"\\n\")\n\treturn err\n}\n\nfunc (h *Header) MarshalWithoutMAC(w io.Writer) error {\n\tif _, err := io.WriteString(w, intro); err != nil {\n\t\treturn err\n\t}\n\tfor _, r := range h.Recipients {\n\t\tif err := r.Marshal(w); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\t_, err := fmt.Fprintf(w, \"%s\", footerPrefix)\n\treturn err\n}\n\nfunc (h *Header) Marshal(w io.Writer) error {\n\tif err := h.MarshalWithoutMAC(w); err != nil {\n\t\treturn err\n\t}\n\tmac := b64.EncodeToString(h.MAC)\n\t_, err := fmt.Fprintf(w, \" %s\\n\", mac)\n\treturn err\n}\n\ntype StanzaReader struct {\n\tr   *bufio.Reader\n\terr error\n}\n\nfunc NewStanzaReader(r *bufio.Reader) *StanzaReader {\n\treturn &StanzaReader{r: r}\n}\n\nfunc (r *StanzaReader) ReadStanza() (s *Stanza, err error) {\n\t// Read errors are unrecoverable.\n\tif r.err != nil {\n\t\treturn nil, r.err\n\t}\n\tdefer func() { r.err = err }()\n\n\ts = &Stanza{}\n\n\tline, err := r.r.ReadBytes('\\n')\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read line: %w\", err)\n\t}\n\tif !bytes.HasPrefix(line, stanzaPrefix) {\n\t\treturn nil, fmt.Errorf(\"malformed stanza opening line: %q\", line)\n\t}\n\tprefix, args := splitArgs(line)\n\tif prefix != string(stanzaPrefix) || len(args) < 1 {\n\t\treturn nil, fmt.Errorf(\"malformed stanza: %q\", line)\n\t}\n\tfor _, a := range args {\n\t\tif !isValidString(a) {\n\t\t\treturn nil, fmt.Errorf(\"malformed stanza: %q\", line)\n\t\t}\n\t}\n\ts.Type = args[0]\n\ts.Args = args[1:]\n\n\tfor {\n\t\tline, err := r.r.ReadBytes('\\n')\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read line: %w\", err)\n\t\t}\n\n\t\tb, err := DecodeString(strings.TrimSuffix(string(line), \"\\n\"))\n\t\tif err != nil {\n\t\t\tif bytes.HasPrefix(line, footerPrefix) || bytes.HasPrefix(line, stanzaPrefix) {\n\t\t\t\treturn nil, fmt.Errorf(\"malformed body line %q: stanza ended without a short line\\nnote: this might be a file encrypted with an old beta version of age or rage; use age v1.0.0-beta6 or rage to decrypt it\", line)\n\t\t\t}\n\t\t\treturn nil, errorf(\"malformed body line %q: %v\", line, err)\n\t\t}\n\t\tif len(b) > BytesPerLine {\n\t\t\treturn nil, errorf(\"malformed body line %q: too long\", line)\n\t\t}\n\t\ts.Body = append(s.Body, b...)\n\t\tif len(b) < BytesPerLine {\n\t\t\t// A stanza body always ends with a short line.\n\t\t\treturn s, nil\n\t\t}\n\t}\n}\n\ntype ParseError struct {\n\terr error\n}\n\nfunc (e *ParseError) Error() string {\n\treturn \"parsing age header: \" + e.err.Error()\n}\n\nfunc (e *ParseError) Unwrap() error {\n\treturn e.err\n}\n\nfunc errorf(format string, a ...any) error {\n\treturn &ParseError{fmt.Errorf(format, a...)}\n}\n\n// Parse returns the header and a Reader that begins at the start of the\n// payload.\nfunc Parse(input io.Reader) (*Header, io.Reader, error) {\n\th := &Header{}\n\trr := bufio.NewReader(input)\n\n\tline, err := rr.ReadString('\\n')\n\tif err == io.EOF {\n\t\treturn nil, nil, errorf(\"file is empty\")\n\t} else if err != nil {\n\t\treturn nil, nil, errorf(\"failed to read intro: %w\", err)\n\t}\n\tif line != intro {\n\t\treturn nil, nil, errorf(\"unexpected intro: %q\", line)\n\t}\n\n\tsr := NewStanzaReader(rr)\n\tfor {\n\t\tpeek, err := rr.Peek(len(footerPrefix))\n\t\tif err != nil {\n\t\t\treturn nil, nil, errorf(\"failed to read header: %w\", err)\n\t\t}\n\n\t\tif bytes.Equal(peek, footerPrefix) {\n\t\t\tline, err := rr.ReadBytes('\\n')\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"failed to read header: %w\", err)\n\t\t\t}\n\n\t\t\tprefix, args := splitArgs(line)\n\t\t\tif prefix != string(footerPrefix) || len(args) != 1 {\n\t\t\t\treturn nil, nil, errorf(\"malformed closing line: %q\", line)\n\t\t\t}\n\t\t\th.MAC, err = DecodeString(args[0])\n\t\t\tif err != nil || len(h.MAC) != 32 {\n\t\t\t\treturn nil, nil, errorf(\"malformed closing line %q: %v\", line, err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\ts, err := sr.ReadStanza()\n\t\tif err != nil {\n\t\t\treturn nil, nil, fmt.Errorf(\"failed to parse header: %w\", err)\n\t\t}\n\t\th.Recipients = append(h.Recipients, s)\n\t}\n\n\t// If input is a bufio.Reader, rr might be equal to input because\n\t// bufio.NewReader short-circuits. In this case we can just return it (and\n\t// we would end up reading the buffer twice if we prepended the peek below).\n\tif rr == input {\n\t\treturn h, rr, nil\n\t}\n\t// Otherwise, unwind the bufio overread and return the unbuffered input.\n\tbuf, err := rr.Peek(rr.Buffered())\n\tif err != nil {\n\t\treturn nil, nil, errorf(\"internal error: %v\", err)\n\t}\n\tpayload := io.MultiReader(bytes.NewReader(buf), input)\n\treturn h, payload, nil\n}\n\nfunc splitArgs(line []byte) (string, []string) {\n\tl := strings.TrimSuffix(string(line), \"\\n\")\n\tparts := strings.Split(l, \" \")\n\treturn parts[0], parts[1:]\n}\n\nfunc isValidString(s string) bool {\n\tif len(s) == 0 {\n\t\treturn false\n\t}\n\tfor _, c := range s {\n\t\tif c < 33 || c > 126 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "internal/format/format_test.go",
    "content": "// Copyright 2021 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build go1.18\n\npackage format_test\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"filippo.io/age/internal/format\"\n)\n\nfunc TestStanzaMarshal(t *testing.T) {\n\ts := &format.Stanza{\n\t\tType: \"test\",\n\t\tArgs: []string{\"1\", \"2\", \"3\"},\n\t\tBody: nil, // empty\n\t}\n\tbuf := &bytes.Buffer{}\n\ts.Marshal(buf)\n\tif exp := \"-> test 1 2 3\\n\\n\"; buf.String() != exp {\n\t\tt.Errorf(\"wrong empty stanza encoding: expected %q, got %q\", exp, buf.String())\n\t}\n\n\tbuf.Reset()\n\ts.Body = []byte(\"AAA\")\n\ts.Marshal(buf)\n\tif exp := \"-> test 1 2 3\\nQUFB\\n\"; buf.String() != exp {\n\t\tt.Errorf(\"wrong normal stanza encoding: expected %q, got %q\", exp, buf.String())\n\t}\n\n\tbuf.Reset()\n\ts.Body = bytes.Repeat([]byte(\"A\"), format.BytesPerLine)\n\ts.Marshal(buf)\n\tif exp := \"-> test 1 2 3\\nQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\\n\\n\"; buf.String() != exp {\n\t\tt.Errorf(\"wrong 64 columns stanza encoding: expected %q, got %q\", exp, buf.String())\n\t}\n}\n\nfunc FuzzMalleability(f *testing.F) {\n\ttests, err := filepath.Glob(\"../../testdata/testkit/*\")\n\tif err != nil {\n\t\tf.Fatal(err)\n\t}\n\tfor _, test := range tests {\n\t\tcontents, err := os.ReadFile(test)\n\t\tif err != nil {\n\t\t\tf.Fatal(err)\n\t\t}\n\t\t_, contents, ok := bytes.Cut(contents, []byte(\"\\n\\n\"))\n\t\tif !ok {\n\t\t\tf.Fatal(\"testkit file without header\")\n\t\t}\n\t\tf.Add(contents)\n\t}\n\tf.Fuzz(func(t *testing.T, data []byte) {\n\t\th, payload, err := format.Parse(bytes.NewReader(data))\n\t\tif err != nil {\n\t\t\tif h != nil {\n\t\t\t\tt.Error(\"h != nil on error\")\n\t\t\t}\n\t\t\tif payload != nil {\n\t\t\t\tt.Error(\"payload != nil on error\")\n\t\t\t}\n\t\t\tt.Skip()\n\t\t}\n\t\tw := &bytes.Buffer{}\n\t\tif err := h.Marshal(w); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif _, err := io.Copy(w, payload); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !bytes.Equal(w.Bytes(), data) {\n\t\t\tt.Error(\"Marshal output different from input\")\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/inspect/inspect.go",
    "content": "package inspect\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"filippo.io/age/armor\"\n\t\"filippo.io/age/internal/format\"\n\t\"filippo.io/age/internal/stream\"\n)\n\ntype Metadata struct {\n\tVersion     string   `json:\"version\"`\n\tPostquantum string   `json:\"postquantum\"` // \"yes\" or \"no\" or \"unknown\"\n\tArmor       bool     `json:\"armor\"`\n\tStanzaTypes []string `json:\"stanza_types\"`\n\tSizes       struct {\n\t\tHeader   int64 `json:\"header\"`\n\t\tArmor    int64 `json:\"armor\"`\n\t\tOverhead int64 `json:\"overhead\"`\n\t\t// Currently, we don't do any padding, not MinPayload == MaxPayload and\n\t\t// MinPadding == MaxPadding == 0, but that might change in the future.\n\t\tMinPayload int64 `json:\"min_payload\"`\n\t\tMaxPayload int64 `json:\"max_payload\"`\n\t\tMinPadding int64 `json:\"min_padding\"`\n\t\tMaxPadding int64 `json:\"max_padding\"`\n\t} `json:\"sizes\"`\n}\n\nfunc Inspect(r io.Reader, fileSize int64) (*Metadata, error) {\n\tdata := &Metadata{\n\t\tVersion:     \"age-encryption.org/v1\",\n\t\tPostquantum: \"unknown\",\n\t}\n\n\ttr := &trackReader{r: r}\n\tbr := bufio.NewReader(tr)\n\tconst maxWhitespace = 1024\n\tstart, _ := br.Peek(maxWhitespace + len(armor.Header))\n\tif strings.HasPrefix(string(bytes.TrimSpace(start)), armor.Header) {\n\t\tr = armor.NewReader(br)\n\t\tdata.Armor = true\n\t} else {\n\t\tr = br\n\t}\n\n\thdr, rest, err := format.Parse(r)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read header: %w\", err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tif err := hdr.Marshal(buf); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to re-serialize header: %w\", err)\n\t}\n\tdata.Sizes.Header = int64(buf.Len())\n\n\tfor _, s := range hdr.Recipients {\n\t\tdata.StanzaTypes = append(data.StanzaTypes, s.Type)\n\t\tswitch s.Type {\n\t\tcase \"X25519\", \"ssh-rsa\", \"ssh-ed25519\", \"age-encryption.org/p256tag\", \"piv-p256\":\n\t\t\tdata.Postquantum = \"no\"\n\t\tcase \"mlkem768x25519\", \"scrypt\", \"age-encryption.org/mlkem768p256tag\":\n\t\t\tif data.Postquantum != \"no\" {\n\t\t\t\tdata.Postquantum = \"yes\"\n\t\t\t}\n\t\t}\n\t}\n\n\t// If fileSize is not provided, or if it's the size of the armored file\n\t// (which can have LF or CRLF line endings, varying its size), read to\n\t// the end to determine it.\n\tif fileSize == -1 || data.Armor {\n\t\tn, err := io.Copy(io.Discard, rest)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read rest of file: %w\", err)\n\t\t}\n\t\tfileSize = data.Sizes.Header + n\n\t\tif !tr.done {\n\t\t\tpanic(\"trackReader not done after io.Copy\")\n\t\t}\n\t\tif tr.count != fileSize && !data.Armor {\n\t\t\tpanic(\"trackReader count mismatch\")\n\t\t}\n\t\tdata.Sizes.Armor = tr.count - fileSize\n\t}\n\tdata.Sizes.Overhead, err = streamOverhead(fileSize - data.Sizes.Header)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to compute stream overhead: %w\", err)\n\t}\n\tdata.Sizes.MinPayload = fileSize - data.Sizes.Header - data.Sizes.Overhead\n\tdata.Sizes.MaxPayload = data.Sizes.MinPayload\n\treturn data, nil\n}\n\ntype trackReader struct {\n\tr     io.Reader\n\tcount int64\n\tdone  bool\n}\n\nfunc (tr *trackReader) Read(p []byte) (int, error) {\n\tn, err := tr.r.Read(p)\n\ttr.count += int64(n)\n\tif err == io.EOF {\n\t\ttr.done = true\n\t} else if tr.done {\n\t\tpanic(\"non-EOF read after EOF\")\n\t}\n\treturn n, err\n}\n\nfunc streamOverhead(payloadSize int64) (int64, error) {\n\tconst streamNonceSize = 16\n\tif payloadSize < streamNonceSize {\n\t\treturn 0, fmt.Errorf(\"encrypted size too small: %d\", payloadSize)\n\t}\n\tencryptedSize := payloadSize - streamNonceSize\n\tplaintextSize, err := stream.PlaintextSize(encryptedSize)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn payloadSize - plaintextSize, nil\n}\n"
  },
  {
    "path": "internal/inspect/inspect_test.go",
    "content": "package inspect\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"filippo.io/age/internal/stream\"\n)\n\nfunc TestStreamOverhead(t *testing.T) {\n\ttests := []struct {\n\t\tpayloadSize int64\n\t\twant        int64\n\t\twantErr     bool\n\t}{\n\t\t{payloadSize: 0, wantErr: true},\n\t\t{payloadSize: 15, wantErr: true},\n\t\t{payloadSize: 16, wantErr: true},\n\t\t{payloadSize: 16 + 15, wantErr: true},\n\t\t{payloadSize: 16 + 16, want: 16 + 16}, // empty plaintext\n\t\t{payloadSize: 16 + 1 + 16, want: 16 + 16},\n\t\t{payloadSize: 16 + stream.ChunkSize + 16, want: 16 + 16},\n\t\t{payloadSize: 16 + stream.ChunkSize + 16 + 1, wantErr: true},\n\t\t{payloadSize: 16 + stream.ChunkSize + 16 + 15, wantErr: true},\n\t\t{payloadSize: 16 + stream.ChunkSize + 16 + 16, wantErr: true}, // empty final chunk\n\t\t{payloadSize: 16 + stream.ChunkSize + 16 + 1 + 16, want: 16 + 16 + 16},\n\t}\n\tfor _, tt := range tests {\n\t\tname := \"payloadSize=\" + fmt.Sprint(tt.payloadSize)\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tgot, gotErr := streamOverhead(tt.payloadSize)\n\t\t\tif gotErr != nil {\n\t\t\t\tif !tt.wantErr {\n\t\t\t\t\tt.Errorf(\"streamOverhead() failed: %v\", gotErr)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif tt.wantErr {\n\t\t\t\tt.Fatal(\"streamOverhead() succeeded unexpectedly\")\n\t\t\t}\n\t\t\tif got != tt.want {\n\t\t\t\tt.Errorf(\"streamOverhead() = %v, want %v\", got, tt.want)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stream/stream.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package stream implements a variant of the STREAM chunked encryption scheme.\npackage stream\n\nimport (\n\t\"bytes\"\n\t\"crypto/cipher\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"sync/atomic\"\n\n\t\"golang.org/x/crypto/chacha20poly1305\"\n)\n\nconst ChunkSize = 64 * 1024\n\nfunc EncryptedChunkCount(encryptedSize int64) (int64, error) {\n\tchunks := (encryptedSize + encChunkSize - 1) / encChunkSize\n\n\tplaintextSize := encryptedSize - chunks*chacha20poly1305.Overhead\n\texpChunks := (plaintextSize + ChunkSize - 1) / ChunkSize\n\t// Empty plaintext, the only case that allows (and requires) an empty chunk.\n\tif plaintextSize == 0 {\n\t\texpChunks = 1\n\t}\n\tif expChunks != chunks {\n\t\treturn 0, fmt.Errorf(\"invalid encrypted payload size: %d\", encryptedSize)\n\t}\n\n\treturn chunks, nil\n}\n\nfunc PlaintextSize(encryptedSize int64) (int64, error) {\n\tchunks, err := EncryptedChunkCount(encryptedSize)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tplaintextSize := encryptedSize - chunks*chacha20poly1305.Overhead\n\treturn plaintextSize, nil\n}\n\ntype DecryptReader struct {\n\ta   cipher.AEAD\n\tsrc io.Reader\n\n\tunread []byte // decrypted but unread data, backed by buf\n\tbuf    [encChunkSize]byte\n\n\terr   error\n\tnonce [chacha20poly1305.NonceSize]byte\n}\n\nconst (\n\tencChunkSize  = ChunkSize + chacha20poly1305.Overhead\n\tlastChunkFlag = 0x01\n)\n\nfunc NewDecryptReader(key []byte, src io.Reader) (*DecryptReader, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &DecryptReader{a: aead, src: src}, nil\n}\n\nfunc (r *DecryptReader) Read(p []byte) (int, error) {\n\tif len(r.unread) > 0 {\n\t\tn := copy(p, r.unread)\n\t\tr.unread = r.unread[n:]\n\t\treturn n, nil\n\t}\n\tif r.err != nil {\n\t\treturn 0, r.err\n\t}\n\tif len(p) == 0 {\n\t\treturn 0, nil\n\t}\n\n\tlast, err := r.readChunk()\n\tif err != nil {\n\t\tr.err = err\n\t\treturn 0, err\n\t}\n\n\tn := copy(p, r.unread)\n\tr.unread = r.unread[n:]\n\n\tif last {\n\t\t// Ensure there is an EOF after the last chunk as expected. In other\n\t\t// words, check for trailing data after a full-length final chunk.\n\t\t// Hopefully, the underlying reader supports returning EOF even if it\n\t\t// had previously returned an EOF to ReadFull.\n\t\tif _, err := r.src.Read(make([]byte, 1)); err == nil {\n\t\t\tr.err = errors.New(\"trailing data after end of encrypted file\")\n\t\t} else if err != io.EOF {\n\t\t\tr.err = fmt.Errorf(\"non-EOF error reading after end of encrypted file: %w\", err)\n\t\t} else {\n\t\t\tr.err = io.EOF\n\t\t}\n\t}\n\n\treturn n, nil\n}\n\n// readChunk reads the next chunk of ciphertext from r.src and makes it available\n// in r.unread. last is true if the chunk was marked as the end of the message.\n// readChunk must not be called again after returning a last chunk or an error.\nfunc (r *DecryptReader) readChunk() (last bool, err error) {\n\tif len(r.unread) != 0 {\n\t\tpanic(\"stream: internal error: readChunk called with dirty buffer\")\n\t}\n\n\tin := r.buf[:]\n\tn, err := io.ReadFull(r.src, in)\n\tswitch {\n\tcase err == io.EOF:\n\t\t// A message can't end without a marked chunk. This message is truncated.\n\t\treturn false, io.ErrUnexpectedEOF\n\tcase err == io.ErrUnexpectedEOF:\n\t\t// The last chunk can be short, but not empty unless it's the first and\n\t\t// only chunk.\n\t\tif !nonceIsZero(&r.nonce) && n == r.a.Overhead() {\n\t\t\treturn false, errors.New(\"last chunk is empty, try age v1.0.0, and please consider reporting this\")\n\t\t}\n\t\tin = in[:n]\n\t\tlast = true\n\t\tsetLastChunkFlag(&r.nonce)\n\tcase err != nil:\n\t\treturn false, err\n\t}\n\n\toutBuf := make([]byte, 0, ChunkSize)\n\tout, err := r.a.Open(outBuf, r.nonce[:], in, nil)\n\tif err != nil && !last {\n\t\t// Check if this was a full-length final chunk.\n\t\tlast = true\n\t\tsetLastChunkFlag(&r.nonce)\n\t\tout, err = r.a.Open(outBuf, r.nonce[:], in, nil)\n\t}\n\tif err != nil {\n\t\treturn false, errors.New(\"failed to decrypt and authenticate payload chunk, file may be corrupted or tampered with\")\n\t}\n\n\tincNonce(&r.nonce)\n\tr.unread = r.buf[:copy(r.buf[:], out)]\n\treturn last, nil\n}\n\nfunc incNonce(nonce *[chacha20poly1305.NonceSize]byte) {\n\tfor i := len(nonce) - 2; i >= 0; i-- {\n\t\tnonce[i]++\n\t\tif nonce[i] != 0 {\n\t\t\treturn\n\t\t}\n\t}\n\t// The counter is 88 bits, this is unreachable.\n\tpanic(\"stream: chunk counter wrapped around\")\n}\n\nfunc nonceForChunk(chunkIndex int64) *[chacha20poly1305.NonceSize]byte {\n\tvar nonce [chacha20poly1305.NonceSize]byte\n\tbinary.BigEndian.PutUint64(nonce[3:11], uint64(chunkIndex))\n\treturn &nonce\n}\n\nfunc setLastChunkFlag(nonce *[chacha20poly1305.NonceSize]byte) {\n\tnonce[len(nonce)-1] = lastChunkFlag\n}\n\nfunc nonceIsZero(nonce *[chacha20poly1305.NonceSize]byte) bool {\n\treturn *nonce == [chacha20poly1305.NonceSize]byte{}\n}\n\ntype EncryptWriter struct {\n\ta     cipher.AEAD\n\tdst   io.Writer\n\tbuf   bytes.Buffer\n\tnonce [chacha20poly1305.NonceSize]byte\n\terr   error\n}\n\nfunc NewEncryptWriter(key []byte, dst io.Writer) (*EncryptWriter, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &EncryptWriter{a: aead, dst: dst}, nil\n}\n\nfunc (w *EncryptWriter) Write(p []byte) (n int, err error) {\n\tif w.err != nil {\n\t\treturn 0, w.err\n\t}\n\tif len(p) == 0 {\n\t\treturn 0, nil\n\t}\n\n\ttotal := len(p)\n\tfor len(p) > 0 {\n\t\tn := min(len(p), ChunkSize-w.buf.Len())\n\t\tw.buf.Write(p[:n])\n\t\tp = p[n:]\n\n\t\t// Only flush if there's a full chunk with bytes still to write, or we\n\t\t// can't know if this is the last chunk yet.\n\t\tif w.buf.Len() == ChunkSize && len(p) > 0 {\n\t\t\tif err := w.flushChunk(notLastChunk); err != nil {\n\t\t\t\tw.err = err\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t}\n\t}\n\treturn total, nil\n}\n\n// Close flushes the last chunk. It does not close the underlying Writer.\nfunc (w *EncryptWriter) Close() error {\n\tif w.err != nil {\n\t\treturn w.err\n\t}\n\n\tw.err = w.flushChunk(lastChunk)\n\tif w.err != nil {\n\t\treturn w.err\n\t}\n\n\tw.err = errors.New(\"stream.Writer is already closed\")\n\treturn nil\n}\n\nconst (\n\tlastChunk    = true\n\tnotLastChunk = false\n)\n\nfunc (w *EncryptWriter) flushChunk(last bool) error {\n\tif !last && w.buf.Len() != ChunkSize {\n\t\tpanic(\"stream: internal error: flush called with partial chunk\")\n\t}\n\n\tif last {\n\t\tsetLastChunkFlag(&w.nonce)\n\t}\n\tw.buf.Grow(chacha20poly1305.Overhead)\n\tciphertext := w.a.Seal(w.buf.Bytes()[:0], w.nonce[:], w.buf.Bytes(), nil)\n\t_, err := w.dst.Write(ciphertext)\n\tincNonce(&w.nonce)\n\tw.buf.Reset()\n\treturn err\n}\n\ntype EncryptReader struct {\n\ta   cipher.AEAD\n\tsrc io.Reader\n\n\t// The first ready bytes of buf are already encrypted. This may be less than\n\t// buf.Len(), because we need to over-read to know if a chunk is the last.\n\tready int\n\tbuf   bytes.Buffer\n\n\tnonce [chacha20poly1305.NonceSize]byte\n\terr   error\n}\n\nfunc NewEncryptReader(key []byte, src io.Reader) (*EncryptReader, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &EncryptReader{a: aead, src: src}, nil\n}\n\nfunc (r *EncryptReader) Read(p []byte) (int, error) {\n\tif r.ready > 0 {\n\t\tn, err := r.buf.Read(p[:min(len(p), r.ready)])\n\t\tr.ready -= n\n\t\treturn n, err\n\t}\n\tif r.err != nil {\n\t\treturn 0, r.err\n\t}\n\tif len(p) == 0 {\n\t\treturn 0, nil\n\t}\n\n\tif err := r.feedBuffer(); err != nil {\n\t\tr.err = err\n\t\treturn 0, err\n\t}\n\n\tn, err := r.buf.Read(p[:min(len(p), r.ready)])\n\tr.ready -= n\n\treturn n, err\n}\n\n// feedBuffer reads and encrypts the next chunk from r.src and appends it to\n// r.buf. It sets r.ready to the number of newly available bytes in r.buf.\nfunc (r *EncryptReader) feedBuffer() error {\n\tif r.ready > 0 {\n\t\tpanic(\"stream: internal error: feedBuffer called with dirty buffer\")\n\t}\n\n\t// CopyN will use r.buf.ReadFrom/WriteTo to fill the buffer directly.\n\t// We need ChunkSize + 1 bytes to determine if this is the last chunk.\n\t_, err := io.CopyN(&r.buf, r.src, int64(ChunkSize-r.buf.Len()+1))\n\tif err != nil && err != io.EOF {\n\t\treturn err\n\t}\n\n\tif last := r.buf.Len() <= ChunkSize; last {\n\t\tsetLastChunkFlag(&r.nonce)\n\n\t\t// After Grow, we know r.buf.Bytes() has enough capacity for the\n\t\t// overhead. We encrypt in place and then do a Write to include the\n\t\t// overhead in the buffer.\n\t\tr.buf.Grow(chacha20poly1305.Overhead)\n\t\tplaintext := r.buf.Bytes()\n\t\tr.a.Seal(plaintext[:0], r.nonce[:], plaintext, nil)\n\t\tincNonce(&r.nonce)\n\t\tr.buf.Write(plaintext[len(plaintext) : len(plaintext)+chacha20poly1305.Overhead])\n\t\tr.ready = r.buf.Len()\n\n\t\tr.err = io.EOF\n\t\treturn nil\n\t}\n\n\t// Same, but accounting for the tail byte which will remain unencrypted and\n\t// needs to be shifted past the overhead.\n\tif r.buf.Len() != ChunkSize+1 {\n\t\tpanic(\"stream: internal error: unexpected buffer length\")\n\t}\n\ttailByte := r.buf.Bytes()[ChunkSize]\n\tr.buf.Grow(chacha20poly1305.Overhead)\n\tplaintext := r.buf.Bytes()[:ChunkSize]\n\tr.a.Seal(plaintext[:0], r.nonce[:], plaintext, nil)\n\tincNonce(&r.nonce)\n\tr.buf.Write(plaintext[len(plaintext)+1 : len(plaintext)+chacha20poly1305.Overhead])\n\tr.buf.WriteByte(tailByte)\n\tr.ready = ChunkSize + chacha20poly1305.Overhead\n\n\treturn nil\n}\n\ntype DecryptReaderAt struct {\n\ta      cipher.AEAD\n\tsrc    io.ReaderAt\n\tsize   int64\n\tchunks int64\n\tcache  atomic.Pointer[cachedChunk]\n}\n\ntype cachedChunk struct {\n\toff  int64\n\tdata []byte\n}\n\nfunc NewDecryptReaderAt(key []byte, src io.ReaderAt, size int64) (*DecryptReaderAt, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Check that size is valid by decrypting the final chunk.\n\tchunks, err := EncryptedChunkCount(size)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfinalChunkIndex := chunks - 1\n\tfinalChunkOff := finalChunkIndex * encChunkSize\n\tfinalChunkSize := size - finalChunkOff\n\tfinalChunk := make([]byte, finalChunkSize)\n\tif _, err := src.ReadAt(finalChunk, finalChunkOff); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read final chunk: %w\", err)\n\t}\n\tnonce := nonceForChunk(finalChunkIndex)\n\tsetLastChunkFlag(nonce)\n\tplaintext, err := aead.Open(finalChunk[:0], nonce[:], finalChunk, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decrypt and authenticate final chunk: %w\", err)\n\t}\n\tcache := &cachedChunk{off: finalChunkOff, data: plaintext}\n\n\tplaintextSize := size - chunks*chacha20poly1305.Overhead\n\tr := &DecryptReaderAt{a: aead, src: src, size: plaintextSize, chunks: chunks}\n\tr.cache.Store(cache)\n\treturn r, nil\n}\n\nfunc (r *DecryptReaderAt) ReadAt(p []byte, off int64) (n int, err error) {\n\tif off < 0 || off > r.size {\n\t\treturn 0, fmt.Errorf(\"offset out of range [0:%d]: %d\", r.size, off)\n\t}\n\tif len(p) == 0 {\n\t\treturn 0, nil\n\t}\n\tvar cacheUpdate *cachedChunk\n\tchunk := make([]byte, encChunkSize)\n\tfor len(p) > 0 && off < r.size {\n\t\tchunkIndex := off / ChunkSize\n\t\tchunkOff := chunkIndex * encChunkSize\n\t\tencSize := r.size + r.chunks*chacha20poly1305.Overhead\n\t\tchunkSize := min(encSize-chunkOff, encChunkSize)\n\n\t\tcached := r.cache.Load()\n\t\tvar plaintext []byte\n\t\tif cached != nil && cached.off == chunkOff {\n\t\t\tplaintext = cached.data\n\t\t\tcacheUpdate = nil\n\t\t} else {\n\t\t\tnn, err := r.src.ReadAt(chunk[:chunkSize], chunkOff)\n\t\t\tif err == io.EOF {\n\t\t\t\tif int64(nn) != chunkSize {\n\t\t\t\t\terr = io.ErrUnexpectedEOF\n\t\t\t\t} else {\n\t\t\t\t\terr = nil\n\t\t\t\t}\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\treturn n, fmt.Errorf(\"failed to read chunk at offset %d: %w\", chunkOff, err)\n\t\t\t}\n\t\t\tnonce := nonceForChunk(chunkIndex)\n\t\t\tif chunkIndex == r.chunks-1 {\n\t\t\t\tsetLastChunkFlag(nonce)\n\t\t\t}\n\t\t\tplaintext, err = r.a.Open(chunk[:0], nonce[:], chunk[:chunkSize], nil)\n\t\t\tif err != nil {\n\t\t\t\treturn n, fmt.Errorf(\"failed to decrypt and authenticate chunk at offset %d: %w\", chunkOff, err)\n\t\t\t}\n\t\t\tcacheUpdate = &cachedChunk{off: chunkOff, data: plaintext}\n\t\t}\n\n\t\tplainChunkOff := int(off - chunkIndex*ChunkSize)\n\t\tcopySize := min(len(plaintext)-plainChunkOff, len(p))\n\t\tcopy(p, plaintext[plainChunkOff:plainChunkOff+copySize])\n\t\tp = p[copySize:]\n\t\toff += int64(copySize)\n\t\tn += copySize\n\t}\n\tif cacheUpdate != nil {\n\t\tr.cache.Store(cacheUpdate)\n\t}\n\tif off == r.size {\n\t\treturn n, io.EOF\n\t}\n\treturn n, nil\n}\n"
  },
  {
    "path": "internal/stream/stream_test.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage stream_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"fmt\"\n\t\"io\"\n\t\"testing\"\n\t\"testing/iotest\"\n\n\t\"filippo.io/age/internal/stream\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n)\n\nconst cs = stream.ChunkSize\n\nfunc TestRoundTrip(t *testing.T) {\n\tfor _, length := range []int{0, 1000, cs - 1, cs, cs + 1, cs + 100, 2 * cs, 2*cs + 500} {\n\t\tfor _, stepSize := range []int{512, 600, 1000, cs - 1, cs, cs + 1} {\n\t\t\tt.Run(fmt.Sprintf(\"len=%d,step=%d\", length, stepSize), func(t *testing.T) {\n\t\t\t\ttestRoundTrip(t, stepSize, length)\n\t\t\t})\n\t\t}\n\t}\n\n\tlength, stepSize := 2*cs+500, 1\n\tt.Run(fmt.Sprintf(\"len=%d,step=%d\", length, stepSize), func(t *testing.T) {\n\t\ttestRoundTrip(t, stepSize, length)\n\t})\n}\n\nfunc testRoundTrip(t *testing.T, stepSize, length int) {\n\tsrc := make([]byte, length)\n\tif _, err := rand.Read(src); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar ciphertext []byte\n\n\tt.Run(\"EncryptWriter\", func(t *testing.T) {\n\t\tbuf := &bytes.Buffer{}\n\t\tw, err := stream.NewEncryptWriter(key, buf)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tvar n int\n\t\tfor n < length {\n\t\t\tb := min(length-n, stepSize)\n\t\t\tnn, err := w.Write(src[n : n+b])\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif nn != b {\n\t\t\t\tt.Errorf(\"Write returned %d, expected %d\", nn, b)\n\t\t\t}\n\t\t\tn += nn\n\n\t\t\tnn, err = w.Write(src[n:n])\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif nn != 0 {\n\t\t\t\tt.Errorf(\"Write returned %d, expected 0\", nn)\n\t\t\t}\n\t\t}\n\t\tif err := w.Close(); err != nil {\n\t\t\tt.Error(\"Close returned an error:\", err)\n\t\t}\n\n\t\tciphertext = buf.Bytes()\n\t})\n\n\tt.Run(\"DecryptReader\", func(t *testing.T) {\n\t\tr, err := stream.NewDecryptReader(key, bytes.NewReader(ciphertext))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tvar n int\n\t\treadBuf := make([]byte, stepSize)\n\t\tfor n < length {\n\t\t\tnn, err := r.Read(readBuf)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"Read error at index %d: %v\", n, err)\n\t\t\t}\n\n\t\t\tif !bytes.Equal(readBuf[:nn], src[n:n+nn]) {\n\t\t\t\tt.Errorf(\"wrong data at indexes %d - %d\", n, n+nn)\n\t\t\t}\n\n\t\t\tn += nn\n\t\t}\n\n\t\tt.Run(\"TestReader\", func(t *testing.T) {\n\t\t\tif length > 1000 && testing.Short() {\n\t\t\t\tt.Skip(\"skipping slow iotest.TestReader on long input\")\n\t\t\t}\n\t\t\tr, _ := stream.NewDecryptReader(key, bytes.NewReader(ciphertext))\n\t\t\tif err := iotest.TestReader(r, src); err != nil {\n\t\t\t\tt.Error(\"iotest.TestReader error on DecryptReader:\", err)\n\t\t\t}\n\t\t})\n\t})\n\n\tt.Run(\"DecryptReaderAt\", func(t *testing.T) {\n\t\trAt, err := stream.NewDecryptReaderAt(key, bytes.NewReader(ciphertext), int64(len(ciphertext)))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\trr := io.NewSectionReader(rAt, 0, int64(len(ciphertext)))\n\n\t\tvar n int\n\t\treadBuf := make([]byte, stepSize)\n\t\tfor n < length {\n\t\t\tnn, err := rr.Read(readBuf)\n\t\t\tif n+nn == length && err == io.EOF {\n\t\t\t\terr = nil\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"ReadAt error at index %d: %v\", n, err)\n\t\t\t}\n\n\t\t\tif !bytes.Equal(readBuf[:nn], src[n:n+nn]) {\n\t\t\t\tt.Errorf(\"wrong data at indexes %d - %d\", n, n+nn)\n\t\t\t}\n\n\t\t\tn += nn\n\t\t}\n\n\t\tt.Run(\"TestReader\", func(t *testing.T) {\n\t\t\tif length > 1000 && testing.Short() {\n\t\t\t\tt.Skip(\"skipping slow iotest.TestReader on long input\")\n\t\t\t}\n\t\t\trr := io.NewSectionReader(rAt, 0, int64(len(src)))\n\t\t\tif err := iotest.TestReader(rr, src); err != nil {\n\t\t\t\tt.Error(\"iotest.TestReader error on DecryptReaderAt:\", err)\n\t\t\t}\n\t\t})\n\t})\n\n\tt.Run(\"EncryptReader\", func(t *testing.T) {\n\t\ter, err := stream.NewEncryptReader(key, bytes.NewReader(src))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\tvar n int\n\t\treadBuf := make([]byte, stepSize)\n\t\tfor {\n\t\t\tnn, err := er.Read(readBuf)\n\t\t\tif nn == 0 && err == io.EOF {\n\t\t\t\tbreak\n\t\t\t} else if err != nil {\n\t\t\t\tt.Fatalf(\"EncryptReader Read error at index %d: %v\", n, err)\n\t\t\t}\n\n\t\t\tif !bytes.Equal(readBuf[:nn], ciphertext[n:n+nn]) {\n\t\t\t\tt.Errorf(\"EncryptReader wrong data at indexes %d - %d\", n, n+nn)\n\t\t\t}\n\n\t\t\tn += nn\n\t\t}\n\t\tif n != len(ciphertext) {\n\t\t\tt.Errorf(\"EncryptReader read %d bytes, expected %d\", n, len(ciphertext))\n\t\t}\n\n\t\tt.Run(\"TestReader\", func(t *testing.T) {\n\t\t\tif length > 1000 && testing.Short() {\n\t\t\t\tt.Skip(\"skipping slow iotest.TestReader on long input\")\n\t\t\t}\n\t\t\ter, _ := stream.NewEncryptReader(key, bytes.NewReader(src))\n\t\t\tif err := iotest.TestReader(er, ciphertext); err != nil {\n\t\t\t\tt.Error(\"iotest.TestReader error on EncryptReader:\", err)\n\t\t\t}\n\t\t})\n\t})\n}\n\n// trackingReaderAt wraps an io.ReaderAt and tracks whether ReadAt was called.\ntype trackingReaderAt struct {\n\tr      io.ReaderAt\n\tcalled bool\n}\n\nfunc (t *trackingReaderAt) ReadAt(p []byte, off int64) (int, error) {\n\tt.called = true\n\treturn t.r.ReadAt(p, off)\n}\n\nfunc (t *trackingReaderAt) reset() {\n\tt.called = false\n}\n\nfunc TestDecryptReaderAt(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create plaintext spanning exactly 3 chunks: 2 full chunks + partial third\n\t// Chunk 0: [0, cs)\n\t// Chunk 1: [cs, 2*cs)\n\t// Chunk 2: [2*cs, 2*cs+500)\n\tplaintextSize := 2*cs + 500\n\tplaintext := make([]byte, plaintextSize)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Encrypt\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\t// Create tracking ReaderAt\n\ttracker := &trackingReaderAt{r: bytes.NewReader(ciphertext)}\n\n\t// Create DecryptReaderAt (this reads and caches the final chunk)\n\tra, err := stream.NewDecryptReaderAt(key, tracker, int64(len(ciphertext)))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttracker.reset()\n\n\t// Helper to check reads\n\tcheckRead := func(name string, off int64, size int, wantN int, wantEOF bool, wantSrcRead bool) {\n\t\tt.Helper()\n\t\ttracker.reset()\n\t\tp := make([]byte, size)\n\t\tn, err := ra.ReadAt(p, off)\n\n\t\tif wantEOF {\n\t\t\tif err != io.EOF {\n\t\t\t\tt.Errorf(\"%s: got err=%v, want EOF\", name, err)\n\t\t\t}\n\t\t} else {\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"%s: got err=%v, want nil\", name, err)\n\t\t\t}\n\t\t}\n\n\t\tif n != wantN {\n\t\t\tt.Errorf(\"%s: got n=%d, want %d\", name, n, wantN)\n\t\t}\n\n\t\tif tracker.called != wantSrcRead {\n\t\t\tt.Errorf(\"%s: src.ReadAt called=%v, want %v\", name, tracker.called, wantSrcRead)\n\t\t}\n\n\t\t// Verify data correctness\n\t\tif n > 0 && off >= 0 && off < int64(plaintextSize) {\n\t\t\tend := int(off) + n\n\t\t\tif end > plaintextSize {\n\t\t\t\tend = plaintextSize\n\t\t\t}\n\t\t\tif !bytes.Equal(p[:n], plaintext[off:end]) {\n\t\t\t\tt.Errorf(\"%s: data mismatch\", name)\n\t\t\t}\n\t\t}\n\t}\n\n\t// Test 1: Read from final chunk (cached by constructor)\n\tcheckRead(\"final chunk (cached)\", int64(2*cs+100), 100, 100, false, false)\n\n\t// Test 2: Read spanning second and third chunk\n\tcheckRead(\"span chunks 1-2\", int64(cs+cs-50), 100, 100, false, true)\n\n\t// Test 3: Read from final chunk again (cached from test 2)\n\t// When reading across chunks 1-2 in test 2, the loop processes chunk 1 then chunk 2,\n\t// so chunk 2 ends up in the cache.\n\tcheckRead(\"final chunk after span\", int64(2*cs+200), 100, 100, false, false)\n\n\t// Test 4: Read from final chunk again (now cached)\n\tcheckRead(\"final chunk (cached again)\", int64(2*cs+50), 50, 50, false, false)\n\n\t// Test 5: Read from first chunk (not cached)\n\tcheckRead(\"first chunk\", 0, 100, 100, false, true)\n\n\t// Test 6: Read from first chunk again (now cached)\n\tcheckRead(\"first chunk (cached)\", 50, 100, 100, false, false)\n\n\t// Test 7: Read spanning all chunks\n\ttracker.reset()\n\tp := make([]byte, plaintextSize)\n\tn, err := ra.ReadAt(p, 0)\n\tif err != io.EOF {\n\t\tt.Errorf(\"span all: got err=%v, want EOF\", err)\n\t}\n\tif n != plaintextSize {\n\t\tt.Errorf(\"span all: got n=%d, want %d\", n, plaintextSize)\n\t}\n\tif !bytes.Equal(p, plaintext) {\n\t\tt.Errorf(\"span all: data mismatch\")\n\t}\n\n\t// Test 8: Read beyond the end (offset > size)\n\ttracker.reset()\n\tp = make([]byte, 100)\n\tn, err = ra.ReadAt(p, int64(plaintextSize+100))\n\tif err == nil {\n\t\tt.Error(\"beyond end: expected error, got nil\")\n\t}\n\tif n != 0 {\n\t\tt.Errorf(\"beyond end: got n=%d, want 0\", n)\n\t}\n\n\t// Test 9: Read with off = size (should return 0, EOF)\n\ttracker.reset()\n\tp = make([]byte, 100)\n\tn, err = ra.ReadAt(p, int64(plaintextSize))\n\tif err != io.EOF {\n\t\tt.Errorf(\"off=size: got err=%v, want EOF\", err)\n\t}\n\tif n != 0 {\n\t\tt.Errorf(\"off=size: got n=%d, want 0\", n)\n\t}\n\n\t// Test 10: Read spanning last chunk and beyond\n\ttracker.reset()\n\tp = make([]byte, 1000) // request more than available\n\tn, err = ra.ReadAt(p, int64(2*cs+400))\n\tif err != io.EOF {\n\t\tt.Errorf(\"span last+beyond: got err=%v, want EOF\", err)\n\t}\n\twantN := 500 - 400 // only 100 bytes available from offset 2*cs+400\n\tif n != wantN {\n\t\tt.Errorf(\"span last+beyond: got n=%d, want %d\", n, wantN)\n\t}\n\tif !bytes.Equal(p[:n], plaintext[2*cs+400:]) {\n\t\tt.Error(\"span last+beyond: data mismatch\")\n\t}\n\n\t// Test 11: Read spanning second+last chunk and beyond\n\ttracker.reset()\n\tp = make([]byte, cs+1000) // request more than available\n\tn, err = ra.ReadAt(p, int64(cs+100))\n\tif err != io.EOF {\n\t\tt.Errorf(\"span 1-2+beyond: got err=%v, want EOF\", err)\n\t}\n\twantN = plaintextSize - (cs + 100)\n\tif n != wantN {\n\t\tt.Errorf(\"span 1-2+beyond: got n=%d, want %d\", n, wantN)\n\t}\n\tif !bytes.Equal(p[:n], plaintext[cs+100:]) {\n\t\tt.Error(\"span 1-2+beyond: data mismatch\")\n\t}\n\n\t// Test 12: Negative offset\n\ttracker.reset()\n\tp = make([]byte, 100)\n\tn, err = ra.ReadAt(p, -1)\n\tif err == nil {\n\t\tt.Error(\"negative offset: expected error, got nil\")\n\t}\n\tif n != 0 {\n\t\tt.Errorf(\"negative offset: got n=%d, want 0\", n)\n\t}\n\n\t// Test 13: Zero-length read in the middle\n\ttracker.reset()\n\tp = make([]byte, 0)\n\tn, err = ra.ReadAt(p, 100)\n\tif err != nil {\n\t\tt.Errorf(\"zero-length middle: got err=%v, want nil\", err)\n\t}\n\tif n != 0 {\n\t\tt.Errorf(\"zero-length middle: got n=%d, want 0\", n)\n\t}\n\n\t// Test 14: Zero-length read at end\n\ttracker.reset()\n\tp = make([]byte, 0)\n\tn, err = ra.ReadAt(p, int64(plaintextSize))\n\tif err != nil {\n\t\tt.Errorf(\"zero-length end: got err=%v, want nil\", err)\n\t}\n\tif n != 0 {\n\t\tt.Errorf(\"zero-length end: got n=%d, want 0\", n)\n\t}\n\n\t// Test 15: Read exactly one chunk at chunk boundary\n\tcheckRead(\"exact chunk at boundary\", int64(cs), cs, cs, false, true)\n\n\t// Test 16: Read one byte at each chunk boundary\n\tcheckRead(\"one byte at start\", 0, 1, 1, false, true)\n\tcheckRead(\"one byte at cs-1\", int64(cs-1), 1, 1, false, false) // cached from test 15\n\tcheckRead(\"one byte at cs\", int64(cs), 1, 1, false, true)\n\tcheckRead(\"one byte at 2*cs-1\", int64(2*cs-1), 1, 1, false, false) // same chunk\n\tcheckRead(\"one byte at 2*cs\", int64(2*cs), 1, 1, false, true)\n\tcheckRead(\"last byte\", int64(plaintextSize-1), 1, 1, true, false) // same chunk, EOF because we reach end\n\n\t// Test 17: Read crossing exactly one chunk boundary\n\tcheckRead(\"cross boundary 0-1\", int64(cs-50), 100, 100, false, true)\n\tcheckRead(\"cross boundary 1-2\", int64(2*cs-50), 100, 100, false, true)\n}\n\nfunc TestDecryptReaderAtEmpty(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create empty encrypted file\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\ttracker := &trackingReaderAt{r: bytes.NewReader(ciphertext)}\n\tra, err := stream.NewDecryptReaderAt(key, tracker, int64(len(ciphertext)))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttracker.reset()\n\n\t// Test 1: Read from empty file at offset 0\n\tp := make([]byte, 100)\n\tn, err := ra.ReadAt(p, 0)\n\tif err != io.EOF {\n\t\tt.Errorf(\"empty read: got err=%v, want EOF\", err)\n\t}\n\tif n != 0 {\n\t\tt.Errorf(\"empty read: got n=%d, want 0\", n)\n\t}\n\n\t// Test 2: Zero-length read from empty file\n\tp = make([]byte, 0)\n\tn, err = ra.ReadAt(p, 0)\n\tif err != nil {\n\t\tt.Errorf(\"empty zero-length: got err=%v, want nil\", err)\n\t}\n\tif n != 0 {\n\t\tt.Errorf(\"empty zero-length: got n=%d, want 0\", n)\n\t}\n\n\t// Test 3: Read beyond empty file\n\tp = make([]byte, 100)\n\tn, err = ra.ReadAt(p, 1)\n\tif err == nil {\n\t\tt.Error(\"empty beyond: expected error, got nil\")\n\t}\n\tif n != 0 {\n\t\tt.Errorf(\"empty beyond: got n=%d, want 0\", n)\n\t}\n}\n\nfunc TestDecryptReaderAtSingleChunk(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Single chunk, not full\n\tplaintext := make([]byte, 1000)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\ttracker := &trackingReaderAt{r: bytes.NewReader(ciphertext)}\n\tra, err := stream.NewDecryptReaderAt(key, tracker, int64(len(ciphertext)))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttracker.reset()\n\n\t// All reads should use cache (final chunk = only chunk)\n\tp := make([]byte, 100)\n\tn, err := ra.ReadAt(p, 0)\n\tif err != nil {\n\t\tt.Errorf(\"single chunk start: got err=%v, want nil\", err)\n\t}\n\tif n != 100 {\n\t\tt.Errorf(\"single chunk start: got n=%d, want 100\", n)\n\t}\n\tif tracker.called {\n\t\tt.Error(\"single chunk start: unexpected src.ReadAt call\")\n\t}\n\tif !bytes.Equal(p[:n], plaintext[:100]) {\n\t\tt.Error(\"single chunk start: data mismatch\")\n\t}\n\n\t// Read at end\n\tn, err = ra.ReadAt(p, 900)\n\tif err != io.EOF {\n\t\tt.Errorf(\"single chunk end: got err=%v, want EOF\", err)\n\t}\n\tif n != 100 {\n\t\tt.Errorf(\"single chunk end: got n=%d, want 100\", n)\n\t}\n\tif tracker.called {\n\t\tt.Error(\"single chunk end: unexpected src.ReadAt call\")\n\t}\n\tif !bytes.Equal(p[:n], plaintext[900:]) {\n\t\tt.Error(\"single chunk end: data mismatch\")\n\t}\n}\n\nfunc TestDecryptReaderAtFullChunks(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Exactly 2 full chunks\n\tplaintext := make([]byte, 2*cs)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\ttracker := &trackingReaderAt{r: bytes.NewReader(ciphertext)}\n\tra, err := stream.NewDecryptReaderAt(key, tracker, int64(len(ciphertext)))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttracker.reset()\n\n\t// Read last byte of second chunk (cached)\n\tp := make([]byte, 1)\n\tn, err := ra.ReadAt(p, int64(2*cs-1))\n\tif err != io.EOF {\n\t\tt.Errorf(\"last byte: got err=%v, want EOF\", err)\n\t}\n\tif n != 1 {\n\t\tt.Errorf(\"last byte: got n=%d, want 1\", n)\n\t}\n\tif tracker.called {\n\t\tt.Error(\"last byte: unexpected src.ReadAt call (should be cached)\")\n\t}\n\tif p[0] != plaintext[2*cs-1] {\n\t\tt.Error(\"last byte: data mismatch\")\n\t}\n\n\t// Read at exactly the boundary between chunks\n\tp = make([]byte, 100)\n\tn, err = ra.ReadAt(p, int64(cs-50))\n\tif err != nil {\n\t\tt.Errorf(\"boundary: got err=%v, want nil\", err)\n\t}\n\tif n != 100 {\n\t\tt.Errorf(\"boundary: got n=%d, want 100\", n)\n\t}\n\tif !bytes.Equal(p, plaintext[cs-50:cs+50]) {\n\t\tt.Error(\"boundary: data mismatch\")\n\t}\n}\n\nfunc TestDecryptReaderAtWrongKey(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tplaintext := make([]byte, 1000)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\t// Try to decrypt with wrong key\n\twrongKey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(wrongKey); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t_, err = stream.NewDecryptReaderAt(wrongKey, bytes.NewReader(ciphertext), int64(len(ciphertext)))\n\tif err == nil {\n\t\tt.Error(\"wrong key: expected error, got nil\")\n\t}\n}\n\nfunc TestDecryptReaderAtInvalidSize(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tplaintext := make([]byte, 1000)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\t// Wrong size (too small)\n\t_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(ciphertext), int64(len(ciphertext)-1))\n\tif err == nil {\n\t\tt.Error(\"wrong size (small): expected error, got nil\")\n\t}\n\n\t// Wrong size (too large)\n\t_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(ciphertext), int64(len(ciphertext)+1))\n\tif err == nil {\n\t\tt.Error(\"wrong size (large): expected error, got nil\")\n\t}\n\n\t// Size that would imply empty final chunk (invalid)\n\t// This would be: one full encrypted chunk + just overhead\n\tinvalidSize := int64(cs + chacha20poly1305.Overhead + chacha20poly1305.Overhead)\n\t_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(make([]byte, invalidSize)), invalidSize)\n\tif err == nil {\n\t\tt.Error(\"invalid size (empty final chunk): expected error, got nil\")\n\t}\n}\n\nfunc TestDecryptReaderAtTruncated(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tplaintext := make([]byte, 2*cs+500)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\t// Truncate ciphertext but lie about size\n\ttruncated := ciphertext[:len(ciphertext)-100]\n\t_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(truncated), int64(len(ciphertext)))\n\tif err == nil {\n\t\tt.Error(\"truncated: expected error, got nil\")\n\t}\n}\n\nfunc TestDecryptReaderAtTruncatedChunk(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create 4 chunks: 3 full + 1 partial\n\tplaintext := make([]byte, 3*cs+500)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\t// Truncate to 3 chunks (remove the actual final chunk)\n\t// The third chunk was NOT encrypted with the last chunk flag,\n\t// so decryption should fail when we try to use it as the final chunk.\n\tencChunkSize := cs + 16 // ChunkSize + Overhead\n\ttruncatedSize := int64(3 * encChunkSize)\n\ttruncated := ciphertext[:truncatedSize]\n\n\t_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(truncated), truncatedSize)\n\tif err == nil {\n\t\tt.Error(\"truncated at chunk boundary: expected error, got nil\")\n\t}\n}\n\nfunc TestDecryptReaderAtConcurrent(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Create plaintext spanning 3 chunks: 2 full + partial\n\tplaintextSize := 2*cs + 500\n\tplaintext := make([]byte, plaintextSize)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Encrypt\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := buf.Bytes()\n\n\tra, err := stream.NewDecryptReaderAt(key, bytes.NewReader(ciphertext), int64(len(ciphertext)))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tt.Run(\"same chunk\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tconst goroutines = 10\n\t\tconst iterations = 100\n\t\terrc := make(chan error, goroutines)\n\n\t\tfor g := range goroutines {\n\t\t\tgo func(id int) {\n\t\t\t\tfor i := range iterations {\n\t\t\t\t\toff := int64((id*iterations + i) % 500)\n\t\t\t\t\tp := make([]byte, 100)\n\t\t\t\t\tn, err := ra.ReadAt(p, off)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: %v\", id, i, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif n != 100 {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: n=%d, want 100\", id, i, n)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif !bytes.Equal(p, plaintext[off:off+100]) {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: data mismatch\", id, i)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\terrc <- nil\n\t\t\t}(g)\n\t\t}\n\n\t\tfor range goroutines {\n\t\t\tif err := <-errc; err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"different chunks\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tconst goroutines = 10\n\t\tconst iterations = 100\n\t\terrc := make(chan error, goroutines)\n\n\t\tfor g := range goroutines {\n\t\t\tgo func(id int) {\n\t\t\t\tfor i := range iterations {\n\t\t\t\t\t// Each goroutine reads from a different chunk based on id\n\t\t\t\t\tchunkIdx := id % 3\n\t\t\t\t\toff := int64(chunkIdx*cs + (i % 400))\n\t\t\t\t\tsize := 100\n\t\t\t\t\tif off+int64(size) > int64(plaintextSize) {\n\t\t\t\t\t\tsize = plaintextSize - int(off)\n\t\t\t\t\t}\n\t\t\t\t\tp := make([]byte, size)\n\t\t\t\t\tn, err := ra.ReadAt(p, off)\n\t\t\t\t\tif n == size && err == io.EOF {\n\t\t\t\t\t\terr = nil // EOF at end is acceptable\n\t\t\t\t\t}\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: off=%d: %v\", id, i, off, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif n != size {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: n=%d, want %d\", id, i, n, size)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif !bytes.Equal(p[:n], plaintext[off:off+int64(n)]) {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: data mismatch\", id, i)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\terrc <- nil\n\t\t\t}(g)\n\t\t}\n\n\t\tfor range goroutines {\n\t\t\tif err := <-errc; err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}\n\t})\n\n\tt.Run(\"across chunks\", func(t *testing.T) {\n\t\tt.Parallel()\n\t\tconst goroutines = 10\n\t\tconst iterations = 100\n\t\terrc := make(chan error, goroutines)\n\n\t\tfor g := range goroutines {\n\t\t\tgo func(id int) {\n\t\t\t\tfor i := range iterations {\n\t\t\t\t\t// Read across chunk boundaries\n\t\t\t\t\tboundary := (id%2 + 1) * cs // either cs or 2*cs\n\t\t\t\t\toff := int64(boundary - 50 + (i % 30))\n\t\t\t\t\tsize := 100\n\t\t\t\t\tif off+int64(size) > int64(plaintextSize) {\n\t\t\t\t\t\tsize = plaintextSize - int(off)\n\t\t\t\t\t}\n\t\t\t\t\tif size <= 0 {\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\tp := make([]byte, size)\n\t\t\t\t\tn, err := ra.ReadAt(p, off)\n\t\t\t\t\tif n == size && err == io.EOF {\n\t\t\t\t\t\terr = nil\n\t\t\t\t\t}\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: off=%d size=%d: %v\", id, i, off, size, err)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif n != size {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: n=%d, want %d\", id, i, n, size)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif !bytes.Equal(p[:n], plaintext[off:off+int64(n)]) {\n\t\t\t\t\t\terrc <- fmt.Errorf(\"goroutine %d iter %d: data mismatch\", id, i)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\terrc <- nil\n\t\t\t}(g)\n\t\t}\n\n\t\tfor range goroutines {\n\t\t\tif err := <-errc; err != nil {\n\t\t\t\tt.Error(err)\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc TestDecryptReaderAtCorrupted(t *testing.T) {\n\tkey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := rand.Read(key); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tplaintext := make([]byte, 2*cs+500)\n\tif _, err := rand.Read(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tw, err := stream.NewEncryptWriter(key, buf)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tciphertext := bytes.Clone(buf.Bytes())\n\n\t// Corrupt final chunk - should fail in constructor\n\tcorruptedFinal := bytes.Clone(ciphertext)\n\tcorruptedFinal[len(corruptedFinal)-10] ^= 0xFF\n\t_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(corruptedFinal), int64(len(corruptedFinal)))\n\tif err == nil {\n\t\tt.Error(\"corrupted final: expected error, got nil\")\n\t}\n\n\t// Corrupt first chunk - should fail on read\n\tcorruptedFirst := bytes.Clone(ciphertext)\n\tcorruptedFirst[10] ^= 0xFF\n\tra, err := stream.NewDecryptReaderAt(key, bytes.NewReader(corruptedFirst), int64(len(corruptedFirst)))\n\tif err != nil {\n\t\tt.Fatalf(\"corrupted first constructor: unexpected error: %v\", err)\n\t}\n\tp := make([]byte, 100)\n\t_, err = ra.ReadAt(p, 0)\n\tif err == nil {\n\t\tt.Error(\"corrupted first read: expected error, got nil\")\n\t}\n}\n"
  },
  {
    "path": "internal/term/term.go",
    "content": "package term\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\n\t\"golang.org/x/term\"\n)\n\n// enableVirtualTerminalProcessing tries to enable virtual terminal processing\n// on Windows. If it fails, avoid using escape sequences to prevent weird\n// characters being printed to the console.\nvar enableVirtualTerminalProcessing func(out *os.File) error\n\n// clearLine clears the current line on the terminal, or opens a new line if\n// terminal escape codes don't work.\nfunc clearLine(out *os.File) {\n\tconst (\n\t\tCUI = \"\\033[\"   // Control Sequence Introducer\n\t\tCPL = CUI + \"F\" // Cursor Previous Line\n\t\tEL  = CUI + \"K\" // Erase in Line\n\t)\n\n\t// First, open a new line, which is guaranteed to work everywhere. Then, try\n\t// to erase the line above with escape codes, if possible.\n\t//\n\t// (We use CRLF instead of LF to work around an apparent bug in WSL2's\n\t// handling of CONOUT$. Only when running a Windows binary from WSL2, the\n\t// cursor would not go back to the start of the line with a simple LF.\n\t// Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)\n\tfmt.Fprintf(out, \"\\r\\n\")\n\tif enableVirtualTerminalProcessing == nil || enableVirtualTerminalProcessing(out) == nil {\n\t\tfmt.Fprintf(out, CPL+EL)\n\t}\n}\n\n// WithTerminal runs f with the terminal input and output files, if available.\n// WithTerminal does not open a non-terminal stdin, so the caller does not need\n// to check if stdin is in use.\nfunc WithTerminal(f func(in, out *os.File) error) error {\n\tif runtime.GOOS == \"windows\" {\n\t\tin, err := os.OpenFile(\"CONIN$\", os.O_RDWR, 0)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer in.Close()\n\t\tout, err := os.OpenFile(\"CONOUT$\", os.O_WRONLY, 0)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer out.Close()\n\t\treturn f(in, out)\n\t} else if tty, err := os.OpenFile(\"/dev/tty\", os.O_RDWR, 0); err == nil {\n\t\tdefer tty.Close()\n\t\treturn f(tty, tty)\n\t} else if term.IsTerminal(int(os.Stdin.Fd())) {\n\t\treturn f(os.Stdin, os.Stdin)\n\t} else {\n\t\treturn fmt.Errorf(\"standard input is not a terminal, and /dev/tty is not available: %v\", err)\n\t}\n}\n\n// ReadSecret reads a value from the terminal with no echo. The prompt is ephemeral.\nfunc ReadSecret(prompt string) (s []byte, err error) {\n\terr = WithTerminal(func(in, out *os.File) error {\n\t\tfmt.Fprintf(out, \"%s \", prompt)\n\t\tdefer clearLine(out)\n\t\ts, err = term.ReadPassword(int(in.Fd()))\n\t\treturn err\n\t})\n\treturn\n}\n\n// ReadPublic reads a value from the terminal. The prompt is ephemeral.\nfunc ReadPublic(prompt string) (s []byte, err error) {\n\terr = WithTerminal(func(in, out *os.File) error {\n\t\tfmt.Fprintf(out, \"%s \", prompt)\n\t\tdefer clearLine(out)\n\n\t\toldState, err := term.MakeRaw(int(in.Fd()))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer term.Restore(int(in.Fd()), oldState)\n\n\t\tt := term.NewTerminal(in, \"\")\n\t\tline, err := t.ReadLine()\n\t\ts = []byte(line)\n\t\treturn err\n\t})\n\treturn\n}\n\n// ReadCharacter reads a single character from the terminal with no echo. The\n// prompt is ephemeral.\nfunc ReadCharacter(prompt string) (c byte, err error) {\n\terr = WithTerminal(func(in, out *os.File) error {\n\t\tfmt.Fprintf(out, \"%s \", prompt)\n\t\tdefer clearLine(out)\n\n\t\toldState, err := term.MakeRaw(int(in.Fd()))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer term.Restore(int(in.Fd()), oldState)\n\n\t\tb := make([]byte, 1)\n\t\tif _, err := in.Read(b); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc = b[0]\n\t\treturn nil\n\t})\n\treturn\n}\n\n// IsTerminal returns whether the given file is a terminal.\nfunc IsTerminal(f *os.File) bool {\n\treturn term.IsTerminal(int(f.Fd()))\n}\n"
  },
  {
    "path": "internal/term/term_windows.go",
    "content": "// Copyright 2022 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage term\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"syscall\"\n\n\t\"golang.org/x/sys/windows\"\n)\n\nfunc init() {\n\tenableVirtualTerminalProcessing = func(out *os.File) error {\n\t\t// Some instances of the Windows Console (e.g., cmd.exe and Windows PowerShell)\n\t\t// do not have the virtual terminal processing enabled, which is necessary to\n\t\t// make terminal escape sequences work. For this reason the clearLine function\n\t\t// may not properly work. Here we enable the virtual terminal processing, if\n\t\t// possible.\n\t\t//\n\t\t// See https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences.\n\n\t\tconst (\n\t\t\tENABLE_PROCESSED_OUTPUT            uint32 = 0x1\n\t\t\tENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4\n\t\t)\n\n\t\tkernel32DLL := windows.NewLazySystemDLL(\"Kernel32.dll\")\n\t\tsetConsoleMode := kernel32DLL.NewProc(\"SetConsoleMode\")\n\n\t\tvar mode uint32\n\t\tif err := syscall.GetConsoleMode(syscall.Handle(out.Fd()), &mode); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tmode |= ENABLE_PROCESSED_OUTPUT\n\t\tmode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING\n\n\t\t// If the SetConsoleMode function fails, the return value is zero.\n\t\t// See https://learn.microsoft.com/en-us/windows/console/setconsolemode#return-value.\n\t\tif ret, _, _ := setConsoleMode.Call(out.Fd(), uintptr(mode)); ret == 0 {\n\t\t\treturn errors.New(\"SetConsoleMode failed\")\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "logo/README.md",
    "content": "The logos available in this folder are Copyright 2021 Filippo Valsorda.\n\nPermission is granted to use the logos as long as they are unaltered, are not\ncombined with other text or graphic, and are not used to imply your project is\nendorsed by or affiliated with the age project.\n\nThis permission can be revoked or rescinded for any reason and at any time,\nselectively or otherwise.\n\nIf you require different terms, please email age-logo@filippo.io.\n\nThe logos were designed by [Studiovagante](https://www.studiovagante.it).\n"
  },
  {
    "path": "parse.go",
    "content": "// Copyright 2021 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage age\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"unicode/utf8\"\n)\n\n// ParseIdentities parses a file with one or more private key encodings, one per\n// line. Empty lines and lines starting with \"#\" are ignored.\n//\n// This is the same syntax as the private key files accepted by the CLI, except\n// the CLI also accepts SSH private keys, which are not recommended for the\n// average application, and plugins, which involve invoking external programs.\n//\n// Currently, all returned values are of type *[X25519Identity] or\n// *[HybridIdentity], but different types might be returned in the future.\nfunc ParseIdentities(f io.Reader) ([]Identity, error) {\n\tconst privateKeySizeLimit = 1 << 24 // 16 MiB\n\tvar ids []Identity\n\tscanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))\n\tvar n int\n\tfor scanner.Scan() {\n\t\tn++\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, \"#\") || line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !utf8.ValidString(line) {\n\t\t\treturn nil, fmt.Errorf(\"identities file is not valid UTF-8\")\n\t\t}\n\t\ti, err := parseIdentity(line)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error at line %d: %v\", n, err)\n\t\t}\n\t\tids = append(ids, i)\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read identities file: %v\", err)\n\t}\n\tif len(ids) == 0 {\n\t\treturn nil, fmt.Errorf(\"no identities found\")\n\t}\n\treturn ids, nil\n}\n\nfunc parseIdentity(arg string) (Identity, error) {\n\tswitch {\n\tcase strings.HasPrefix(arg, \"AGE-SECRET-KEY-1\"):\n\t\treturn ParseX25519Identity(arg)\n\tcase strings.HasPrefix(arg, \"AGE-SECRET-KEY-PQ-1\"):\n\t\treturn ParseHybridIdentity(arg)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown identity type: %q\", arg)\n\t}\n}\n\n// ParseRecipients parses a file with one or more public key encodings, one per\n// line. Empty lines and lines starting with \"#\" are ignored.\n//\n// This is the same syntax as the recipients files accepted by the CLI, except\n// the CLI also accepts SSH recipients, which are not recommended for the\n// average application, tagged recipients, which have different privacy\n// properties, and plugins, which involve invoking external programs.\n//\n// Currently, all returned values are of type *[X25519Recipient] or\n// *[HybridRecipient] but different types might be returned in the future.\nfunc ParseRecipients(f io.Reader) ([]Recipient, error) {\n\tconst recipientFileSizeLimit = 1 << 24 // 16 MiB\n\tvar recs []Recipient\n\tscanner := bufio.NewScanner(io.LimitReader(f, recipientFileSizeLimit))\n\tvar n int\n\tfor scanner.Scan() {\n\t\tn++\n\t\tline := scanner.Text()\n\t\tif strings.HasPrefix(line, \"#\") || line == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif !utf8.ValidString(line) {\n\t\t\treturn nil, fmt.Errorf(\"recipients file is not valid UTF-8\")\n\t\t}\n\t\tr, err := parseRecipient(line)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"error at line %d: %v\", n, err)\n\t\t}\n\t\trecs = append(recs, r)\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read recipients file: %v\", err)\n\t}\n\tif len(recs) == 0 {\n\t\treturn nil, fmt.Errorf(\"no recipients found\")\n\t}\n\treturn recs, nil\n}\n\nfunc parseRecipient(arg string) (Recipient, error) {\n\tswitch {\n\tcase strings.HasPrefix(arg, \"age1pq1\"):\n\t\treturn ParseHybridRecipient(arg)\n\tcase strings.HasPrefix(arg, \"age1\"):\n\t\treturn ParseX25519Recipient(arg)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"unknown recipient type: %q\", arg)\n\t}\n}\n"
  },
  {
    "path": "plugin/client.go",
    "content": "// Copyright 2021 Google LLC\n//\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file or at\n// https://developers.google.com/open-source/licenses/bsd\n\npackage plugin\n\nimport (\n\t\"bufio\"\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\tmathrand \"math/rand/v2\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\texec \"golang.org/x/sys/execabs\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/internal/format\"\n)\n\ntype Recipient struct {\n\tname     string\n\tencoding string\n\tui       *ClientUI\n\n\t// identity is true when encoding is an identity string.\n\tidentity bool\n}\n\nvar _ age.Recipient = &Recipient{}\nvar _ age.RecipientWithLabels = &Recipient{}\n\nfunc NewRecipient(s string, ui *ClientUI) (*Recipient, error) {\n\tname, _, err := ParseRecipient(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Recipient{\n\t\tname: name, encoding: s, ui: ui,\n\t}, nil\n}\n\n// Name returns the plugin name, which is used in the recipient (\"age1name1...\")\n// and identity (\"AGE-PLUGIN-NAME-1...\") encodings, as well as in the plugin\n// binary name (\"age-plugin-name\").\nfunc (r *Recipient) Name() string {\n\treturn r.name\n}\n\n// String returns the recipient encoding string (\"age1name1...\") or\n// \"<identity-based recipient>\" if r was created by [Identity.Recipient].\nfunc (r *Recipient) String() string {\n\tif r.identity {\n\t\treturn \"<identity-based recipient>\"\n\t}\n\treturn r.encoding\n}\n\nfunc (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {\n\tstanzas, _, err = r.WrapWithLabels(fileKey)\n\treturn\n}\n\nfunc (r *Recipient) WrapWithLabels(fileKey []byte) (stanzas []*age.Stanza, labels []string, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"%s plugin: %w\", r.name, err)\n\t\t}\n\t}()\n\n\tconn, err := openClientConnection(r.name, \"recipient-v1\")\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"couldn't start plugin: %w\", err)\n\t}\n\tdefer conn.Close()\n\n\t// Phase 1: client sends recipient or identity and file key\n\taddType := \"add-recipient\"\n\tif r.identity {\n\t\taddType = \"add-identity\"\n\t}\n\tif err := writeStanza(conn, addType, r.encoding); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif _, err := writeGrease(conn); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err := writeStanzaWithBody(conn, \"wrap-file-key\", fileKey); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err := writeStanza(conn, \"extension-labels\"); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err := writeStanza(conn, \"done\"); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Phase 2: plugin responds with stanzas\n\tsr := format.NewStanzaReader(bufio.NewReader(conn))\nReadLoop:\n\tfor {\n\t\ts, err := r.ui.readStanza(r.name, sr)\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tswitch s.Type {\n\t\tcase \"recipient-stanza\":\n\t\t\tif len(s.Args) < 2 {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"malformed recipient stanza: unexpected argument count\")\n\t\t\t}\n\t\t\tn, err := strconv.Atoi(s.Args[0])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"malformed recipient stanza: invalid index\")\n\t\t\t}\n\t\t\t// We only send a single file key, so the index must be 0.\n\t\t\tif n != 0 {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"malformed recipient stanza: unexpected index\")\n\t\t\t}\n\n\t\t\tstanzas = append(stanzas, &age.Stanza{\n\t\t\t\tType: s.Args[1],\n\t\t\t\tArgs: s.Args[2:],\n\t\t\t\tBody: s.Body,\n\t\t\t})\n\n\t\t\tif err := writeStanza(conn, \"ok\"); err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\tcase \"labels\":\n\t\t\tif labels != nil {\n\t\t\t\treturn nil, nil, fmt.Errorf(\"repeated labels stanza\")\n\t\t\t}\n\t\t\tlabels = s.Args\n\n\t\t\tif err := writeStanza(conn, \"ok\"); err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\t\tcase \"error\":\n\t\t\tif err := writeStanza(conn, \"ok\"); err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t}\n\n\t\t\treturn nil, nil, fmt.Errorf(\"%s\", s.Body)\n\t\tcase \"done\":\n\t\t\tbreak ReadLoop\n\t\tdefault:\n\t\t\tif ok, err := r.ui.handle(r.name, conn, s); err != nil {\n\t\t\t\treturn nil, nil, err\n\t\t\t} else if !ok {\n\t\t\t\tif err := writeStanza(conn, \"unsupported\"); err != nil {\n\t\t\t\t\treturn nil, nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(stanzas) == 0 {\n\t\treturn nil, nil, fmt.Errorf(\"received zero recipient stanzas\")\n\t}\n\n\treturn stanzas, labels, nil\n}\n\ntype Identity struct {\n\tname     string\n\tencoding string\n\tui       *ClientUI\n}\n\nvar _ age.Identity = &Identity{}\n\nfunc NewIdentity(s string, ui *ClientUI) (*Identity, error) {\n\tname, _, err := ParseIdentity(s)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Identity{\n\t\tname: name, encoding: s, ui: ui,\n\t}, nil\n}\n\nfunc NewIdentityWithoutData(name string, ui *ClientUI) (*Identity, error) {\n\ts := EncodeIdentity(name, nil)\n\tif s == \"\" {\n\t\treturn nil, fmt.Errorf(\"invalid plugin name: %q\", name)\n\t}\n\treturn &Identity{\n\t\tname: name, encoding: s, ui: ui,\n\t}, nil\n}\n\n// Name returns the plugin name, which is used in the recipient (\"age1name1...\")\n// and identity (\"AGE-PLUGIN-NAME-1...\") encodings, as well as in the plugin\n// binary name (\"age-plugin-name\").\nfunc (i *Identity) Name() string {\n\treturn i.name\n}\n\n// String returns the identity encoding string (\"AGE-PLUGIN-NAME-1...\").\nfunc (i *Identity) String() string {\n\treturn i.encoding\n}\n\n// Recipient returns a Recipient wrapping this identity. When that Recipient is\n// used to encrypt a file key, the identity encoding is provided as-is to the\n// plugin, which is expected to support encrypting to identities.\nfunc (i *Identity) Recipient() *Recipient {\n\treturn &Recipient{\n\t\tname:     i.name,\n\t\tencoding: i.encoding,\n\t\tidentity: true,\n\t\tui:       i.ui,\n\t}\n}\n\nfunc (i *Identity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {\n\tdefer func() {\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"%s plugin: %w\", i.name, err)\n\t\t}\n\t}()\n\n\tconn, err := openClientConnection(i.name, \"identity-v1\")\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"couldn't start plugin: %w\", err)\n\t}\n\tdefer conn.Close()\n\n\t// Phase 1: client sends the plugin the identity string and the stanzas\n\tif err := writeStanza(conn, \"add-identity\", i.encoding); err != nil {\n\t\treturn nil, err\n\t}\n\tif _, err := writeGrease(conn); err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, rs := range stanzas {\n\t\ts := &format.Stanza{\n\t\t\tType: \"recipient-stanza\",\n\t\t\tArgs: append([]string{\"0\", rs.Type}, rs.Args...),\n\t\t\tBody: rs.Body,\n\t\t}\n\t\tif err := s.Marshal(conn); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\tif err := writeStanza(conn, \"done\"); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Phase 2: plugin responds with various commands and a file key\n\tsr := format.NewStanzaReader(bufio.NewReader(conn))\nReadLoop:\n\tfor {\n\t\ts, err := i.ui.readStanza(i.name, sr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch s.Type {\n\t\tcase \"file-key\":\n\t\t\tif len(s.Args) != 1 {\n\t\t\t\treturn nil, fmt.Errorf(\"malformed file-key stanza: unexpected arguments count\")\n\t\t\t}\n\t\t\tn, err := strconv.Atoi(s.Args[0])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"malformed file-key stanza: invalid index\")\n\t\t\t}\n\t\t\t// We only send a single file key, so the index must be 0.\n\t\t\tif n != 0 {\n\t\t\t\treturn nil, fmt.Errorf(\"malformed file-key stanza: unexpected index\")\n\t\t\t}\n\t\t\tif fileKey != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"received duplicated file-key stanza\")\n\t\t\t}\n\n\t\t\tfileKey = s.Body\n\n\t\t\tif err := writeStanza(conn, \"ok\"); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\tcase \"error\":\n\t\t\tif err := writeStanza(conn, \"ok\"); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn nil, fmt.Errorf(\"%s\", s.Body)\n\t\tcase \"done\":\n\t\t\tbreak ReadLoop\n\t\tdefault:\n\t\t\tif ok, err := i.ui.handle(i.name, conn, s); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t} else if !ok {\n\t\t\t\tif err := writeStanza(conn, \"unsupported\"); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif fileKey == nil {\n\t\treturn nil, age.ErrIncorrectIdentity\n\t}\n\treturn fileKey, nil\n}\n\n// ClientUI holds callbacks that will be invoked by (Un)Wrap if the plugin\n// wishes to interact with the user. If any of them is nil or returns an error,\n// failure will be reported to the plugin, but note that the error is otherwise\n// discarded. Implementations are encouraged to display errors to the user\n// before returning them.\ntype ClientUI struct {\n\t// DisplayMessage displays the message, which is expected to have lowercase\n\t// initials and no final period.\n\tDisplayMessage func(name, message string) error\n\n\t// RequestValue requests a secret or public input, with the provided prompt.\n\tRequestValue func(name, prompt string, secret bool) (string, error)\n\n\t// Confirm requests a confirmation with the provided prompt. The yes and no\n\t// value are the choices provided to the user. no may be empty. The return\n\t// value indicates whether the user selected the yes or no option.\n\tConfirm func(name, prompt, yes, no string) (choseYes bool, err error)\n\n\t// WaitTimer is invoked once (Un)Wrap has been waiting for 5 seconds on the\n\t// plugin, for example because the plugin is waiting for an external event\n\t// (e.g. a hardware token touch). Unlike the other callbacks, WaitTimer runs\n\t// in a separate goroutine, and if missing it's simply ignored.\n\tWaitTimer func(name string)\n}\n\nfunc (c *ClientUI) handle(name string, conn *clientConnection, s *format.Stanza) (ok bool, err error) {\n\tswitch s.Type {\n\tcase \"msg\":\n\t\tif c.DisplayMessage == nil {\n\t\t\treturn true, writeStanza(conn, \"fail\")\n\t\t}\n\t\tif err := c.DisplayMessage(name, string(s.Body)); err != nil {\n\t\t\treturn true, writeStanza(conn, \"fail\")\n\t\t}\n\t\treturn true, writeStanza(conn, \"ok\")\n\tcase \"request-secret\", \"request-public\":\n\t\tif c.RequestValue == nil {\n\t\t\treturn true, writeStanza(conn, \"fail\")\n\t\t}\n\t\tsecret, err := c.RequestValue(name, string(s.Body), s.Type == \"request-secret\")\n\t\tif err != nil {\n\t\t\treturn true, writeStanza(conn, \"fail\")\n\t\t}\n\t\treturn true, writeStanzaWithBody(conn, \"ok\", []byte(secret))\n\tcase \"confirm\":\n\t\tif len(s.Args) != 1 && len(s.Args) != 2 {\n\t\t\treturn true, fmt.Errorf(\"malformed confirm stanza: unexpected number of arguments\")\n\t\t}\n\t\tif c.Confirm == nil {\n\t\t\treturn true, writeStanza(conn, \"fail\")\n\t\t}\n\t\tyes, err := format.DecodeString(s.Args[0])\n\t\tif err != nil {\n\t\t\treturn true, fmt.Errorf(\"malformed confirm stanza: invalid YES option encoding\")\n\t\t}\n\t\tvar no []byte\n\t\tif len(s.Args) == 2 {\n\t\t\tno, err = format.DecodeString(s.Args[1])\n\t\t\tif err != nil {\n\t\t\t\treturn true, fmt.Errorf(\"malformed confirm stanza: invalid NO option encoding\")\n\t\t\t}\n\t\t}\n\t\tchoseYes, err := c.Confirm(name, string(s.Body), string(yes), string(no))\n\t\tif err != nil {\n\t\t\treturn true, writeStanza(conn, \"fail\")\n\t\t}\n\t\tresult := \"yes\"\n\t\tif !choseYes {\n\t\t\tresult = \"no\"\n\t\t}\n\t\treturn true, writeStanza(conn, \"ok\", result)\n\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n// readStanza calls r.ReadStanza and, if set, invokes WaitTimer in a separate\n// goroutine if the call takes longer than 5 seconds.\nfunc (c *ClientUI) readStanza(name string, r *format.StanzaReader) (*format.Stanza, error) {\n\tif c.WaitTimer != nil {\n\t\tdefer time.AfterFunc(5*time.Second, func() { c.WaitTimer(name) }).Stop()\n\t}\n\treturn r.ReadStanza()\n}\n\ntype clientConnection struct {\n\tcmd       *exec.Cmd\n\tio.Reader // stdout\n\tio.Writer // stdin\n\tclose     func()\n}\n\n// NotFoundError is returned by [Recipient.Wrap] and [Identity.Unwrap] when the\n// plugin binary cannot be found.\ntype NotFoundError struct {\n\t// Name is the plugin (not binary) name.\n\tName string\n\t// Err is the underlying error, usually an [exec.Error] wrapping\n\t// [exec.ErrNotFound].\n\tErr error\n}\n\nfunc (e *NotFoundError) Error() string {\n\treturn fmt.Sprintf(\"%q plugin not found: %v\", e.Name, e.Err)\n}\n\nfunc (e *NotFoundError) Unwrap() error {\n\treturn e.Err\n}\n\nvar testOnlyPluginPath string\n\nfunc openClientConnection(name, protocol string) (*clientConnection, error) {\n\tpath := \"age-plugin-\" + name\n\tif testOnlyPluginPath != \"\" {\n\t\tpath = filepath.Join(testOnlyPluginPath, path)\n\t} else if strings.ContainsRune(name, os.PathSeparator) {\n\t\treturn nil, fmt.Errorf(\"invalid plugin name: %q\", name)\n\t}\n\tcmd := exec.Command(path, \"--age-plugin=\"+protocol)\n\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tstdin, err := cmd.StdinPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcc := &clientConnection{\n\t\tcmd:    cmd,\n\t\tReader: stdout,\n\t\tWriter: stdin,\n\t\tclose: func() {\n\t\t\tstdin.Close()\n\t\t\tstdout.Close()\n\t\t},\n\t}\n\n\tif os.Getenv(\"AGEDEBUG\") == \"plugin\" {\n\t\tcc.Reader = io.TeeReader(cc.Reader, os.Stderr)\n\t\tcc.Writer = io.MultiWriter(cc.Writer, os.Stderr)\n\t\tcmd.Stderr = os.Stderr\n\t}\n\n\t// We don't want the plugins to rely on the working directory for anything\n\t// as different clients might treat it differently, so we set it to an empty\n\t// temporary directory.\n\tcmd.Dir = os.TempDir()\n\n\tif err := cmd.Start(); err != nil {\n\t\tif errors.Is(err, exec.ErrNotFound) {\n\t\t\treturn nil, &NotFoundError{Name: name, Err: err}\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn cc, nil\n}\n\nfunc (cc *clientConnection) Close() error {\n\t// Close stdin and stdout and send SIGINT (if supported) to the plugin,\n\t// then wait for it to cleanup and exit.\n\tcc.close()\n\tcc.cmd.Process.Signal(os.Interrupt)\n\treturn cc.cmd.Wait()\n}\n\nfunc writeStanza(conn io.Writer, t string, args ...string) error {\n\ts := &format.Stanza{Type: t, Args: args}\n\treturn s.Marshal(conn)\n}\n\nfunc writeStanzaWithBody(conn io.Writer, t string, body []byte) error {\n\ts := &format.Stanza{Type: t, Body: body}\n\treturn s.Marshal(conn)\n}\n\nfunc writeGrease(conn io.Writer) (sent bool, err error) {\n\tif mathrand.IntN(3) == 0 {\n\t\treturn false, nil\n\t}\n\ts := &format.Stanza{Type: fmt.Sprintf(\"grease-%x\", mathrand.Int())}\n\tfor i := 0; i < mathrand.IntN(3); i++ {\n\t\ts.Args = append(s.Args, fmt.Sprintf(\"%d\", mathrand.IntN(100)))\n\t}\n\tif mathrand.IntN(2) == 0 {\n\t\ts.Body = make([]byte, mathrand.IntN(100))\n\t\trand.Read(s.Body)\n\t}\n\treturn true, s.Marshal(conn)\n}\n"
  },
  {
    "path": "plugin/client_test.go",
    "content": "// Copyright 2023 The age Authors\n//\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file or at\n// https://developers.google.com/open-source/licenses/bsd\n\npackage plugin\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/internal/bech32\"\n)\n\nfunc TestMain(m *testing.M) {\n\tswitch filepath.Base(os.Args[0]) {\n\tcase \"age-plugin-test\":\n\t\tp, _ := New(\"test\")\n\t\tp.HandleRecipient(func(data []byte) (age.Recipient, error) {\n\t\t\treturn testRecipient{}, nil\n\t\t})\n\t\tos.Exit(p.Main())\n\tcase \"age-plugin-testpqc\":\n\t\tp, _ := New(\"testpqc\")\n\t\tp.HandleRecipient(func(data []byte) (age.Recipient, error) {\n\t\t\treturn testPQCRecipient{}, nil\n\t\t})\n\t\tos.Exit(p.Main())\n\tcase \"age-plugin-error\":\n\t\tp, _ := New(\"error\")\n\t\tp.HandleRecipient(func(data []byte) (age.Recipient, error) {\n\t\t\treturn nil, errors.New(\"oh my, an error occurred\")\n\t\t})\n\t\tp.HandleIdentity(func(data []byte) (age.Identity, error) {\n\t\t\treturn nil, errors.New(\"oh my, an error occurred\")\n\t\t})\n\t\tos.Exit(p.Main())\n\tdefault:\n\t\tos.Exit(m.Run())\n\t}\n}\n\ntype testRecipient struct{}\n\nfunc (testRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\treturn []*age.Stanza{{Type: \"test\", Body: fileKey}}, nil\n}\n\ntype testPQCRecipient struct{}\n\nvar _ age.RecipientWithLabels = testPQCRecipient{}\n\nfunc (testPQCRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\treturn []*age.Stanza{{Type: \"test\", Body: fileKey}}, nil\n}\n\nfunc (testPQCRecipient) WrapWithLabels(fileKey []byte) ([]*age.Stanza, []string, error) {\n\treturn []*age.Stanza{{Type: \"test\", Body: fileKey}}, []string{\"postquantum\"}, nil\n}\n\nfunc TestLabels(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"Windows support is TODO\")\n\t}\n\ttemp := t.TempDir()\n\ttestOnlyPluginPath = temp\n\tt.Cleanup(func() { testOnlyPluginPath = \"\" })\n\tex, err := os.Executable()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Link(ex, filepath.Join(temp, \"age-plugin-test\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Chmod(filepath.Join(temp, \"age-plugin-test\"), 0755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Link(ex, filepath.Join(temp, \"age-plugin-testpqc\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Chmod(filepath.Join(temp, \"age-plugin-testpqc\"), 0755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tname, err := bech32.Encode(\"age1test\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttestPlugin, err := NewRecipient(name, &ClientUI{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tnamePQC, err := bech32.Encode(\"age1testpqc\", nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttestPluginPQC, err := NewRecipient(namePQC, &ClientUI{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif _, err := age.Encrypt(io.Discard, testPluginPQC); err != nil {\n\t\tt.Errorf(\"expected one pqc to work, got %v\", err)\n\t}\n\tif _, err := age.Encrypt(io.Discard, testPluginPQC, testPluginPQC); err != nil {\n\t\tt.Errorf(\"expected two pqc to work, got %v\", err)\n\t}\n\tif _, err := age.Encrypt(io.Discard, testPluginPQC, testPlugin); err == nil {\n\t\tt.Errorf(\"expected one pqc and one normal to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, testPlugin, testPluginPQC); err == nil {\n\t\tt.Errorf(\"expected one pqc and one normal to fail\")\n\t}\n}\n\nfunc TestNotFound(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"Windows support is TODO\")\n\t}\n\n\tr := EncodeRecipient(\"nonexistentplugin\", nil)\n\tt.Log(r)\n\ttestPluginRecipient, err := NewRecipient(r, &ClientUI{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tvar e *NotFoundError\n\tif _, err := age.Encrypt(io.Discard, testPluginRecipient); err == nil {\n\t\tt.Errorf(\"expected error for nonexistent plugin\")\n\t} else if !errors.As(err, &e) {\n\t\tt.Errorf(\"expected NotFoundError, got %T: %v\", err, err)\n\t} else if e.Name != \"nonexistentplugin\" {\n\t\tt.Errorf(\"expected NotFoundError.Name to be nonexistentplugin, got %q\", e.Name)\n\t} else if !errors.Is(err, exec.ErrNotFound) {\n\t\tt.Errorf(\"expected error to wrap exec.ErrNotFound, got: %v\", err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tid, err := age.GenerateHybridIdentity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw, err := age.Encrypt(buf, id.Recipient())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw.Close()\n\n\ti := EncodeIdentity(\"nonexistentplugin\", nil)\n\tt.Log(i)\n\ttestPluginIdentity, err := NewIdentity(i, &ClientUI{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := age.Decrypt(buf, testPluginIdentity); err == nil {\n\t\tt.Errorf(\"expected error for nonexistent plugin\")\n\t} else if errors.As(err, new(*age.NoIdentityMatchError)) {\n\t\tt.Errorf(\"expected NotFoundError, got NoIdentityMatchError: %v\", err)\n\t} else if !errors.As(err, &e) {\n\t\tt.Errorf(\"expected NotFoundError, got %T: %v\", err, err)\n\t} else if e.Name != \"nonexistentplugin\" {\n\t\tt.Errorf(\"expected NotFoundError.Name to be nonexistentplugin, got %q\", e.Name)\n\t} else if !errors.Is(err, exec.ErrNotFound) {\n\t\tt.Errorf(\"expected error to wrap exec.ErrNotFound, got: %v\", err)\n\t}\n}\n\nfunc TestPluginError(t *testing.T) {\n\tif runtime.GOOS == \"windows\" {\n\t\tt.Skip(\"Windows support is TODO\")\n\t}\n\ttemp := t.TempDir()\n\ttestOnlyPluginPath = temp\n\tt.Cleanup(func() { testOnlyPluginPath = \"\" })\n\tex, err := os.Executable()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Link(ex, filepath.Join(temp, \"age-plugin-error\")); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := os.Chmod(filepath.Join(temp, \"age-plugin-error\"), 0755); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tr := EncodeRecipient(\"error\", nil)\n\ttestPluginRecipient, err := NewRecipient(r, &ClientUI{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := age.Encrypt(io.Discard, testPluginRecipient); err == nil {\n\t\tt.Errorf(\"expected error from plugin\")\n\t} else if !strings.Contains(err.Error(), \"oh my, an error occurred\") {\n\t\tt.Errorf(\"expected plugin error, got: %v\", err)\n\t}\n\n\tbuf := &bytes.Buffer{}\n\tid, err := age.GenerateHybridIdentity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw, err := age.Encrypt(buf, id.Recipient())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tw.Close()\n\n\ti := EncodeIdentity(\"error\", nil)\n\ttestPluginIdentity, err := NewIdentity(i, &ClientUI{})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := age.Decrypt(buf, testPluginIdentity); err == nil {\n\t\tt.Errorf(\"expected error from plugin\")\n\t} else if !strings.Contains(err.Error(), \"oh my, an error occurred\") {\n\t\tt.Errorf(\"expected plugin error, got: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "plugin/encode.go",
    "content": "// Copyright 2023 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage plugin\n\nimport (\n\t\"crypto/ecdh\"\n\t\"crypto/mlkem\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"filippo.io/age/internal/bech32\"\n\t\"filippo.io/hpke\"\n)\n\n// EncodeIdentity encodes a plugin identity string for a plugin with the given\n// name. If the name is invalid, it returns an empty string.\nfunc EncodeIdentity(name string, data []byte) string {\n\tif !validPluginName(name) {\n\t\treturn \"\"\n\t}\n\ts, _ := bech32.Encode(\"AGE-PLUGIN-\"+strings.ToUpper(name)+\"-\", data)\n\treturn s\n}\n\n// ParseIdentity decodes a plugin identity string. It returns the plugin name\n// in lowercase and the encoded data.\nfunc ParseIdentity(s string) (name string, data []byte, err error) {\n\thrp, data, err := bech32.Decode(s)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid identity encoding: %v\", err)\n\t}\n\tif !strings.HasPrefix(hrp, \"AGE-PLUGIN-\") || !strings.HasSuffix(hrp, \"-\") {\n\t\treturn \"\", nil, fmt.Errorf(\"not a plugin identity: %v\", err)\n\t}\n\tname = strings.TrimSuffix(strings.TrimPrefix(hrp, \"AGE-PLUGIN-\"), \"-\")\n\tname = strings.ToLower(name)\n\tif !validPluginName(name) {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid plugin name: %q\", name)\n\t}\n\treturn name, data, nil\n}\n\n// EncodeRecipient encodes a plugin recipient string for a plugin with the given\n// name. If the name is invalid, it returns an empty string.\nfunc EncodeRecipient(name string, data []byte) string {\n\tif !validPluginName(name) {\n\t\treturn \"\"\n\t}\n\ts, _ := bech32.Encode(\"age1\"+strings.ToLower(name), data)\n\treturn s\n}\n\n// ParseRecipient decodes a plugin recipient string. It returns the plugin name\n// in lowercase and the encoded data.\nfunc ParseRecipient(s string) (name string, data []byte, err error) {\n\thrp, data, err := bech32.Decode(s)\n\tif err != nil {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid recipient encoding: %v\", err)\n\t}\n\tif !strings.HasPrefix(hrp, \"age1\") {\n\t\treturn \"\", nil, fmt.Errorf(\"not a plugin recipient: %v\", err)\n\t}\n\tname = strings.TrimPrefix(hrp, \"age1\")\n\tif !validPluginName(name) {\n\t\treturn \"\", nil, fmt.Errorf(\"invalid plugin name: %q\", name)\n\t}\n\treturn name, data, nil\n}\n\nfunc validPluginName(name string) bool {\n\tif name == \"\" {\n\t\treturn false\n\t}\n\tallowed := \"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-._\"\n\tfor _, r := range name {\n\t\tif !strings.ContainsRune(allowed, r) {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// EncodeX25519Recipient encodes a native X25519 recipient from a\n// [crypto/ecdh.X25519] public key. It's meant for plugins that implement\n// identities that are compatible with native recipients.\nfunc EncodeX25519Recipient(pk *ecdh.PublicKey) (string, error) {\n\tif pk.Curve() != ecdh.X25519() {\n\t\treturn \"\", fmt.Errorf(\"wrong ecdh Curve\")\n\t}\n\treturn bech32.Encode(\"age\", pk.Bytes())\n}\n\n// EncodeHybridRecipient encodes a native MLKEM768-X25519 recipient from a\n// [crypto/mlkem.EncapsulationKey768] and a [crypto/ecdh.X25519] public key.\n// It's meant for plugins that implement identities that are compatible with\n// native recipients.\nfunc EncodeHybridRecipient(pq *mlkem.EncapsulationKey768, t *ecdh.PublicKey) (string, error) {\n\tif t.Curve() != ecdh.X25519() {\n\t\treturn \"\", fmt.Errorf(\"wrong ecdh Curve\")\n\t}\n\tpk, err := hpke.NewHybridPublicKey(pq, t)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to create hybrid public key: %v\", err)\n\t}\n\treturn bech32.Encode(\"age1pq\", pk.Bytes())\n}\n"
  },
  {
    "path": "plugin/example_test.go",
    "content": "package plugin_test\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/plugin\"\n)\n\ntype Recipient struct{}\n\nfunc (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\tpanic(\"unimplemented\")\n}\n\nfunc NewRecipient(data []byte) (*Recipient, error) {\n\treturn &Recipient{}, nil\n}\n\ntype Identity struct{}\n\nfunc (i *Identity) Unwrap(s []*age.Stanza) ([]byte, error) {\n\tpanic(\"unimplemented\")\n}\n\nfunc NewIdentity(data []byte) (*Identity, error) {\n\treturn &Identity{}, nil\n}\n\nfunc ExamplePlugin_main() {\n\tp, err := plugin.New(\"example\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tp.HandleRecipient(func(data []byte) (age.Recipient, error) {\n\t\treturn NewRecipient(data)\n\t})\n\tp.HandleIdentity(func(data []byte) (age.Identity, error) {\n\t\treturn NewIdentity(data)\n\t})\n\tos.Exit(p.Main())\n}\n"
  },
  {
    "path": "plugin/plugin.go",
    "content": "// Package plugin implements the age plugin protocol.\n//\n// [Recipient] and [Indentity] are plugin clients, that execute plugin binaries to\n// perform encryption and decryption operations.\n//\n// [Plugin] is a framework for writing age plugins, that exposes an [age.Recipient]\n// and/or [age.Identity] implementation as a plugin binary.\npackage plugin\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strconv\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/internal/format\"\n)\n\n// TODO: add plugin test framework.\n\n// Plugin is a framework for writing age plugins. It allows exposing regular\n// [age.Recipient] and [age.Identity] implementations as plugins, and handles\n// all the protocol details.\ntype Plugin struct {\n\tname string\n\tfs   *flag.FlagSet\n\tsm   *string\n\n\trecipient     func([]byte) (age.Recipient, error)\n\tidAsRecipient func([]byte) (age.Recipient, error)\n\tidentity      func([]byte) (age.Identity, error)\n\n\tstdin          io.Reader\n\tstdout, stderr io.Writer\n\n\tsr *format.StanzaReader\n\t// broken is set if the protocol broke down during an interaction function\n\t// called by a Recipient or Identity.\n\tbroken bool\n}\n\n// New creates a new Plugin with the given name.\n//\n// For example, a plugin named \"frood\" would be invoked as \"age-plugin-frood\".\nfunc New(name string) (*Plugin, error) {\n\treturn &Plugin{name: name, stdin: os.Stdin,\n\t\tstdout: os.Stdout, stderr: os.Stderr}, nil\n}\n\n// Name returns the name of the plugin.\nfunc (p *Plugin) Name() string {\n\treturn p.name\n}\n\n// RegisterFlags registers the plugin's flags with the given [flag.FlagSet], or\n// with the default [flag.CommandLine] if fs is nil. It must be called before\n// [flag.Parse] and [Plugin.Main].\n//\n// This allows the plugin to expose additional flags when invoked manually, for\n// example to implement a keygen mode.\nfunc (p *Plugin) RegisterFlags(fs *flag.FlagSet) {\n\tif fs == nil {\n\t\tfs = flag.CommandLine\n\t}\n\tp.fs = fs\n\tp.sm = fs.String(\"age-plugin\", \"\", \"age-plugin state machine\")\n}\n\n// HandleRecipient registers a function to parse recipients of the form\n// age1name1... into [age.Recipient] values. data is the decoded Bech32 payload.\n//\n// If the returned Recipient implements [age.RecipientWithLabels], Plugin will\n// use it and enforce consistency across every returned stanza in an execution.\n// If the client supports labels, they will be passed through the protocol.\n//\n// It must be called before [Plugin.Main], and can be called at most once.\n// Otherwise, it panics.\nfunc (p *Plugin) HandleRecipient(f func(data []byte) (age.Recipient, error)) {\n\tif p.recipient != nil {\n\t\tpanic(\"HandleRecipient called twice\")\n\t}\n\tp.recipient = f\n}\n\n// HandleIdentityAsRecipient registers a function to parse identities of the\n// form AGE-PLUGIN-NAME-1... into [age.Recipient] values, for when identities\n// are used as recipients. data is the decoded Bech32 payload.\n//\n// If the returned Recipient implements [age.RecipientWithLabels], Plugin will\n// use it and enforce consistency across every returned stanza in an execution.\n// If the client supports labels, they will be passed through the protocol.\n//\n// It must be called before [Plugin.Main], and can be called at most once.\n// Otherwise, it panics.\nfunc (p *Plugin) HandleIdentityAsRecipient(f func(data []byte) (age.Recipient, error)) {\n\tif p.idAsRecipient != nil {\n\t\tpanic(\"HandleIdentityAsRecipient called twice\")\n\t}\n\tp.idAsRecipient = f\n}\n\n// HandleIdentity registers a function to parse identities of the form\n// AGE-PLUGIN-NAME-1... into [age.Identity] values. data is the decoded Bech32\n// payload.\n//\n// It must be called before [Plugin.Main], and can be called at most once.\n// Otherwise, it panics.\nfunc (p *Plugin) HandleIdentity(f func(data []byte) (age.Identity, error)) {\n\tif p.identity != nil {\n\t\tpanic(\"HandleIdentity called twice\")\n\t}\n\tp.identity = f\n}\n\n// HandleRecipientEncoding is like [Plugin.HandleRecipient] but provides the\n// full recipient encoding string to the callback.\n//\n// It allows using functions like ParseRecipient directly.\nfunc (p *Plugin) HandleRecipientEncoding(f func(recipient string) (age.Recipient, error)) {\n\tp.HandleRecipient(func(data []byte) (age.Recipient, error) {\n\t\treturn f(EncodeRecipient(p.name, data))\n\t})\n}\n\n// HandleIdentityEncodingAsRecipient is like [Plugin.HandleIdentityAsRecipient] but\n// provides the full identity encoding string to the callback.\nfunc (p *Plugin) HandleIdentityEncodingAsRecipient(f func(identity string) (age.Recipient, error)) {\n\tp.HandleIdentityAsRecipient(func(data []byte) (age.Recipient, error) {\n\t\treturn f(EncodeIdentity(p.name, data))\n\t})\n}\n\n// HandleIdentityEncoding is like [Plugin.HandleIdentity] but provides the\n// full identity encoding string to the callback.\n//\n// It allows using functions like ParseIdentity directly.\nfunc (p *Plugin) HandleIdentityEncoding(f func(identity string) (age.Identity, error)) {\n\tp.HandleIdentity(func(data []byte) (age.Identity, error) {\n\t\treturn f(EncodeIdentity(p.name, data))\n\t})\n}\n\n// Main runs the plugin protocol. It returns an exit code to pass to os.Exit.\n//\n// It automatically calls [Plugin.RegisterFlags] and [flag.Parse] if they were\n// not called before.\nfunc (p *Plugin) Main() int {\n\tif p.fs == nil {\n\t\tp.RegisterFlags(nil)\n\t}\n\tif !p.fs.Parsed() {\n\t\tp.fs.Parse(os.Args[1:])\n\t}\n\tif *p.sm == \"recipient-v1\" {\n\t\treturn p.RecipientV1()\n\t}\n\tif *p.sm == \"identity-v1\" {\n\t\treturn p.IdentityV1()\n\t}\n\tfmt.Fprintf(p.stderr, \"unknown state machine %q\", *p.sm)\n\treturn 4\n}\n\n// SetIO sets the plugin's input and output streams, which default to\n// stdin/stdout/stderr.\n//\n// It must be called before [Plugin.Main].\nfunc (p *Plugin) SetIO(stdin io.Reader, stdout, stderr io.Writer) {\n\tp.stdin = stdin\n\tp.stdout = stdout\n\tp.stderr = stderr\n}\n\n// RecipientV1 implements the recipient-v1 state machine. It returns an exit\n// code to pass to os.Exit.\n//\n// Most plugins should call [Plugin.Main] instead of this method.\nfunc (p *Plugin) RecipientV1() int {\n\tif p.recipient == nil && p.idAsRecipient == nil {\n\t\treturn p.fatalf(\"recipient-v1 not supported\")\n\t}\n\n\tvar recipientStrings, identityStrings []string\n\tvar fileKeys [][]byte\n\tvar supportsLabels bool\n\n\tp.sr = format.NewStanzaReader(bufio.NewReader(p.stdin))\nReadLoop:\n\tfor {\n\t\ts, err := p.sr.ReadStanza()\n\t\tif err != nil {\n\t\t\treturn p.fatalf(\"failed to read stanza: %v\", err)\n\t\t}\n\n\t\tswitch s.Type {\n\t\tcase \"add-recipient\":\n\t\t\tif err := expectStanzaWithNoBody(s, 1); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t\trecipientStrings = append(recipientStrings, s.Args[0])\n\t\tcase \"add-identity\":\n\t\t\tif err := expectStanzaWithNoBody(s, 1); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tidentityStrings = append(identityStrings, s.Args[0])\n\t\tcase \"extension-labels\":\n\t\t\tif err := expectStanzaWithNoBody(s, 0); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tsupportsLabels = true\n\t\tcase \"wrap-file-key\":\n\t\t\tif err := expectStanzaWithBody(s, 0); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tfileKeys = append(fileKeys, s.Body)\n\t\tcase \"done\":\n\t\t\tif err := expectStanzaWithNoBody(s, 0); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tbreak ReadLoop\n\t\tdefault:\n\t\t\t// Unsupported stanzas in uni-directional phases are ignored.\n\t\t}\n\t}\n\n\tif len(recipientStrings)+len(identityStrings) == 0 {\n\t\treturn p.fatalf(\"no recipients or identities provided\")\n\t}\n\tif len(fileKeys) == 0 {\n\t\treturn p.fatalf(\"no file keys provided\")\n\t}\n\n\tvar recipients, identities []age.Recipient\n\tfor i, s := range recipientStrings {\n\t\tname, data, err := ParseRecipient(s)\n\t\tif err != nil {\n\t\t\treturn p.recipientError(i, err)\n\t\t}\n\t\tif name != p.name {\n\t\t\treturn p.recipientError(i, fmt.Errorf(\"unsupported plugin name: %q\", name))\n\t\t}\n\t\tif p.recipient == nil {\n\t\t\treturn p.recipientError(i, fmt.Errorf(\"recipient encodings not supported\"))\n\t\t}\n\t\tr, err := p.recipient(data)\n\t\tif err != nil {\n\t\t\treturn p.recipientError(i, err)\n\t\t}\n\t\trecipients = append(recipients, r)\n\t}\n\tfor i, s := range identityStrings {\n\t\tname, data, err := ParseIdentity(s)\n\t\tif err != nil {\n\t\t\treturn p.identityError(i, err)\n\t\t}\n\t\tif name != p.name {\n\t\t\treturn p.identityError(i, fmt.Errorf(\"unsupported plugin name: %q\", name))\n\t\t}\n\t\tif p.idAsRecipient == nil {\n\t\t\treturn p.identityError(i, fmt.Errorf(\"identity encodings not supported\"))\n\t\t}\n\t\tr, err := p.idAsRecipient(data)\n\t\tif err != nil {\n\t\t\treturn p.identityError(i, err)\n\t\t}\n\t\tidentities = append(identities, r)\n\t}\n\n\t// Technically labels should be per-file key, but the client-side protocol\n\t// extension shipped like this, and it doesn't feel worth making a v2.\n\tvar labels []string\n\n\tstanzas := make([][]*age.Stanza, len(fileKeys))\n\tfor i, fk := range fileKeys {\n\t\tfor j, r := range recipients {\n\t\t\tss, ll, err := wrapWithLabels(r, fk)\n\t\t\tif p.broken {\n\t\t\t\treturn 2\n\t\t\t} else if err != nil {\n\t\t\t\treturn p.recipientError(j, err)\n\t\t\t}\n\t\t\tif i == 0 && j == 0 {\n\t\t\t\tlabels = ll\n\t\t\t} else if err := checkLabels(ll, labels); err != nil {\n\t\t\t\treturn p.recipientError(j, err)\n\t\t\t}\n\t\t\tstanzas[i] = append(stanzas[i], ss...)\n\t\t}\n\t\tfor j, r := range identities {\n\t\t\tss, ll, err := wrapWithLabels(r, fk)\n\t\t\tif p.broken {\n\t\t\t\treturn 2\n\t\t\t} else if err != nil {\n\t\t\t\treturn p.identityError(j, err)\n\t\t\t}\n\t\t\tif i == 0 && j == 0 && len(recipients) == 0 {\n\t\t\t\tlabels = ll\n\t\t\t} else if err := checkLabels(ll, labels); err != nil {\n\t\t\t\treturn p.identityError(j, err)\n\t\t\t}\n\t\t\tstanzas[i] = append(stanzas[i], ss...)\n\t\t}\n\t}\n\n\tif sent, err := writeGrease(p.stdout); err != nil {\n\t\treturn p.fatalf(\"failed to write grease: %v\", err)\n\t} else if sent {\n\t\tif err := expectUnsupported(p.sr); err != nil {\n\t\t\treturn p.fatalf(\"%v\", err)\n\t\t}\n\t}\n\n\tif supportsLabels {\n\t\tif err := writeStanza(p.stdout, \"labels\", labels...); err != nil {\n\t\t\treturn p.fatalf(\"failed to write labels stanza: %v\", err)\n\t\t}\n\t\tif err := expectOk(p.sr); err != nil {\n\t\t\treturn p.fatalf(\"%v\", err)\n\t\t}\n\t}\n\n\tfor i, ss := range stanzas {\n\t\tfor _, s := range ss {\n\t\t\tif err := (&format.Stanza{Type: \"recipient-stanza\",\n\t\t\t\tArgs: append([]string{fmt.Sprint(i), s.Type}, s.Args...),\n\t\t\t\tBody: s.Body}).Marshal(p.stdout); err != nil {\n\t\t\t\treturn p.fatalf(\"failed to write recipient-stanza: %v\", err)\n\t\t\t}\n\t\t\tif err := expectOk(p.sr); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\t\tif sent, err := writeGrease(p.stdout); err != nil {\n\t\t\treturn p.fatalf(\"failed to write grease: %v\", err)\n\t\t} else if sent {\n\t\t\tif err := expectUnsupported(p.sr); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := writeStanza(p.stdout, \"done\"); err != nil {\n\t\treturn p.fatalf(\"failed to write done stanza: %v\", err)\n\t}\n\treturn 0\n}\n\nfunc wrapWithLabels(r age.Recipient, fileKey []byte) ([]*age.Stanza, []string, error) {\n\tif r, ok := r.(age.RecipientWithLabels); ok {\n\t\treturn r.WrapWithLabels(fileKey)\n\t}\n\ts, err := r.Wrap(fileKey)\n\treturn s, nil, err\n}\n\nfunc checkLabels(ll, labels []string) error {\n\tif !slicesEqual(ll, labels) {\n\t\treturn fmt.Errorf(\"labels %q do not match previous recipients %q\", ll, labels)\n\t}\n\treturn nil\n}\n\n// IdentityV1 implements the identity-v1 state machine. It returns an exit code\n// to pass to os.Exit.\n//\n// Most plugins should call [Plugin.Main] instead of this method.\nfunc (p *Plugin) IdentityV1() int {\n\tif p.identity == nil {\n\t\treturn p.fatalf(\"identity-v1 not supported\")\n\t}\n\n\tvar files [][]*age.Stanza\n\tvar identityStrings []string\n\n\tp.sr = format.NewStanzaReader(bufio.NewReader(p.stdin))\nReadLoop:\n\tfor {\n\t\ts, err := p.sr.ReadStanza()\n\t\tif err != nil {\n\t\t\treturn p.fatalf(\"failed to read stanza: %v\", err)\n\t\t}\n\n\t\tswitch s.Type {\n\t\tcase \"add-identity\":\n\t\t\tif err := expectStanzaWithNoBody(s, 1); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tidentityStrings = append(identityStrings, s.Args[0])\n\t\tcase \"recipient-stanza\":\n\t\t\tif len(s.Args) < 2 {\n\t\t\t\treturn p.fatalf(\"recipient-stanza stanza has %d arguments, want >=2\", len(s.Args))\n\t\t\t}\n\t\t\ti, err := strconv.Atoi(s.Args[0])\n\t\t\tif err != nil {\n\t\t\t\treturn p.fatalf(\"failed to parse recipient-stanza stanza argument: %v\", err)\n\t\t\t}\n\t\t\tss := &age.Stanza{Type: s.Args[1], Args: s.Args[2:], Body: s.Body}\n\t\t\tswitch i {\n\t\t\tcase len(files):\n\t\t\t\tfiles = append(files, []*age.Stanza{ss})\n\t\t\tcase len(files) - 1:\n\t\t\t\tfiles[len(files)-1] = append(files[len(files)-1], ss)\n\t\t\tdefault:\n\t\t\t\treturn p.fatalf(\"unexpected file index %d, previous was %d\", i, len(files)-1)\n\t\t\t}\n\t\tcase \"done\":\n\t\t\tif err := expectStanzaWithNoBody(s, 0); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tbreak ReadLoop\n\t\tdefault:\n\t\t\t// Unsupported stanzas in uni-directional phases are ignored.\n\t\t}\n\t}\n\n\tif len(identityStrings) == 0 {\n\t\treturn p.fatalf(\"no identities provided\")\n\t}\n\tif len(files) == 0 {\n\t\treturn p.fatalf(\"no stanzas provided\")\n\t}\n\n\tvar identities []age.Identity\n\tfor i, s := range identityStrings {\n\t\tname, data, err := ParseIdentity(s)\n\t\tif err != nil {\n\t\t\treturn p.identityError(i, err)\n\t\t}\n\t\tif name != p.name {\n\t\t\treturn p.identityError(i, fmt.Errorf(\"unsupported plugin name: %q\", name))\n\t\t}\n\t\tif p.identity == nil {\n\t\t\treturn p.identityError(i, fmt.Errorf(\"identity encodings not supported\"))\n\t\t}\n\t\tr, err := p.identity(data)\n\t\tif err != nil {\n\t\t\treturn p.identityError(i, err)\n\t\t}\n\t\tidentities = append(identities, r)\n\t}\n\n\tfor i, ss := range files {\n\t\tif sent, err := writeGrease(p.stdout); err != nil {\n\t\t\treturn p.fatalf(\"failed to write grease: %v\", err)\n\t\t} else if sent {\n\t\t\tif err := expectUnsupported(p.sr); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t}\n\n\t\t// TODO: there should be a mechanism to let the plugin decide the order\n\t\t// in which identities are tried.\n\t\tfor _, id := range identities {\n\t\t\tfk, err := id.Unwrap(ss)\n\t\t\tif p.broken {\n\t\t\t\treturn 2\n\t\t\t} else if errors.Is(err, age.ErrIncorrectIdentity) {\n\t\t\t\tcontinue\n\t\t\t} else if err != nil {\n\t\t\t\tif err := p.writeError([]string{\"stanza\", fmt.Sprint(i), \"0\"}, err); err != nil {\n\t\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t\t}\n\t\t\t\t// Note that we don't exit here, as the protocol allows\n\t\t\t\t// continuing with other files.\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\ts := &format.Stanza{Type: \"file-key\", Args: []string{fmt.Sprint(i)}, Body: fk}\n\t\t\tif err := s.Marshal(p.stdout); err != nil {\n\t\t\t\treturn p.fatalf(\"failed to write file-key: %v\", err)\n\t\t\t}\n\t\t\tif err := expectOk(p.sr); err != nil {\n\t\t\t\treturn p.fatalf(\"%v\", err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif err := writeStanza(p.stdout, \"done\"); err != nil {\n\t\treturn p.fatalf(\"failed to write done stanza: %v\", err)\n\t}\n\treturn 0\n}\n\n// DisplayMessage requests that the client display a message to the user. The\n// message should start with a lowercase letter and have no final period.\n// DisplayMessage returns an error if the client can't display the message, and\n// may return before the message has been displayed to the user.\n//\n// It must only be called by a Wrap or Unwrap method invoked by [Plugin.Main].\nfunc (p *Plugin) DisplayMessage(message string) error {\n\tif err := writeStanzaWithBody(p.stdout, \"msg\", []byte(message)); err != nil {\n\t\treturn p.fatalInteractf(\"failed to write msg stanza: %v\", err)\n\t}\n\ts, err := readOkOrFail(p.sr)\n\tif err != nil {\n\t\treturn p.fatalInteractf(\"%v\", err)\n\t}\n\tif s.Type == \"fail\" {\n\t\treturn fmt.Errorf(\"client failed to display message\")\n\t}\n\tif err := expectStanzaWithNoBody(s, 0); err != nil {\n\t\treturn p.fatalInteractf(\"%v\", err)\n\t}\n\treturn nil\n}\n\n// RequestValue requests a secret or public input from the user through the\n// client, with the provided prompt. It returns an error if the client can't\n// request the input or if the user dismisses the prompt.\n//\n// It must only be called by a Wrap or Unwrap method invoked by [Plugin.Main].\nfunc (p *Plugin) RequestValue(prompt string, secret bool) (string, error) {\n\tt := \"request-public\"\n\tif secret {\n\t\tt = \"request-secret\"\n\t}\n\tif err := writeStanzaWithBody(p.stdout, t, []byte(prompt)); err != nil {\n\t\treturn \"\", p.fatalInteractf(\"failed to write stanza: %v\", err)\n\t}\n\ts, err := readOkOrFail(p.sr)\n\tif err != nil {\n\t\treturn \"\", p.fatalInteractf(\"%v\", err)\n\t}\n\tif s.Type == \"fail\" {\n\t\treturn \"\", fmt.Errorf(\"client failed to request value\")\n\t}\n\tif err := expectStanzaWithBody(s, 0); err != nil {\n\t\treturn \"\", p.fatalInteractf(\"%v\", err)\n\t}\n\treturn string(s.Body), nil\n}\n\n// Confirm requests a confirmation from the user through the client, with the\n// provided prompt. The yes and no value are the choices provided to the user.\n// no may be empty. The return value choseYes indicates whether the user\n// selected the yes or no option. Confirm returns an error if the client can't\n// request the confirmation.\n//\n// It must only be called by a Wrap or Unwrap method invoked by [Plugin.Main].\nfunc (p *Plugin) Confirm(prompt, yes, no string) (choseYes bool, err error) {\n\targs := []string{format.EncodeToString([]byte(yes))}\n\tif no != \"\" {\n\t\targs = append(args, format.EncodeToString([]byte(no)))\n\t}\n\ts := &format.Stanza{Type: \"confirm\", Args: args, Body: []byte(prompt)}\n\tif err := s.Marshal(p.stdout); err != nil {\n\t\treturn false, p.fatalInteractf(\"failed to write confirm stanza: %v\", err)\n\t}\n\ts, err = readOkOrFail(p.sr)\n\tif err != nil {\n\t\treturn false, p.fatalInteractf(\"%v\", err)\n\t}\n\tif s.Type == \"fail\" {\n\t\treturn false, fmt.Errorf(\"client failed to request confirmation\")\n\t}\n\tif err := expectStanzaWithNoBody(s, 1); err != nil {\n\t\treturn false, p.fatalInteractf(\"%v\", err)\n\t}\n\treturn s.Args[0] == \"yes\", nil\n}\n\n// fatalInteractf prints the error to stderr and sets the broken flag, so the\n// Wrap/Unwrap caller can exit with an error.\nfunc (p *Plugin) fatalInteractf(format string, args ...any) error {\n\tp.broken = true\n\tfmt.Fprintf(p.stderr, format, args...)\n\treturn fmt.Errorf(format, args...)\n}\n\nfunc (p *Plugin) fatalf(format string, args ...any) int {\n\tfmt.Fprintf(p.stderr, format, args...)\n\treturn 1\n}\n\nfunc expectStanzaWithNoBody(s *format.Stanza, wantArgs int) error {\n\tif len(s.Args) != wantArgs {\n\t\treturn fmt.Errorf(\"%s stanza has %d arguments, want %d\", s.Type, len(s.Args), wantArgs)\n\t}\n\tif len(s.Body) != 0 {\n\t\treturn fmt.Errorf(\"%s stanza has %d bytes of body, want 0\", s.Type, len(s.Body))\n\t}\n\treturn nil\n}\n\nfunc expectStanzaWithBody(s *format.Stanza, wantArgs int) error {\n\tif len(s.Args) != wantArgs {\n\t\treturn fmt.Errorf(\"%s stanza has %d arguments, want %d\", s.Type, len(s.Args), wantArgs)\n\t}\n\tif len(s.Body) == 0 {\n\t\treturn fmt.Errorf(\"%s stanza has 0 bytes of body, want >0\", s.Type)\n\t}\n\treturn nil\n}\n\nfunc (p *Plugin) recipientError(idx int, err error) int {\n\tif err := p.writeError([]string{\"recipient\", fmt.Sprint(idx)}, err); err != nil {\n\t\treturn p.fatalf(\"%v\", err)\n\t}\n\treturn 3\n}\n\nfunc (p *Plugin) identityError(idx int, err error) int {\n\tif err := p.writeError([]string{\"identity\", fmt.Sprint(idx)}, err); err != nil {\n\t\treturn p.fatalf(\"%v\", err)\n\t}\n\treturn 3\n}\n\nfunc expectOk(sr *format.StanzaReader) error {\n\tok, err := sr.ReadStanza()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read OK stanza: %v\", err)\n\t}\n\tif ok.Type != \"ok\" {\n\t\treturn fmt.Errorf(\"expected OK stanza, got %q\", ok.Type)\n\t}\n\treturn expectStanzaWithNoBody(ok, 0)\n}\n\nfunc readOkOrFail(sr *format.StanzaReader) (*format.Stanza, error) {\n\ts, err := sr.ReadStanza()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response stanza: %v\", err)\n\t}\n\tswitch s.Type {\n\tcase \"fail\":\n\t\tif err := expectStanzaWithNoBody(s, 0); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"%v\", err)\n\t\t}\n\t\treturn s, nil\n\tcase \"ok\":\n\t\treturn s, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"expected ok or fail stanza, got %q\", s.Type)\n\t}\n}\n\nfunc expectUnsupported(sr *format.StanzaReader) error {\n\tunsupported, err := sr.ReadStanza()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to read unsupported stanza: %v\", err)\n\t}\n\tif unsupported.Type != \"unsupported\" {\n\t\treturn fmt.Errorf(\"expected unsupported stanza, got %q\", unsupported.Type)\n\t}\n\treturn expectStanzaWithNoBody(unsupported, 0)\n}\n\nfunc (p *Plugin) writeError(args []string, err error) error {\n\ts := &format.Stanza{Type: \"error\", Args: args}\n\ts.Body = []byte(err.Error())\n\tif err := s.Marshal(p.stdout); err != nil {\n\t\treturn fmt.Errorf(\"failed to write error stanza: %v\", err)\n\t}\n\tif err := expectOk(p.sr); err != nil {\n\t\treturn fmt.Errorf(\"%v\", err)\n\t}\n\treturn nil\n}\n\nfunc slicesEqual(s1, s2 []string) bool {\n\tif len(s1) != len(s2) {\n\t\treturn false\n\t}\n\tfor i := range s1 {\n\t\tif s1[i] != s2[i] {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "plugin/tui.go",
    "content": "package plugin\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\n\t\"filippo.io/age/internal/term\"\n)\n\n// NewTerminalUI returns a [ClientUI] that uses the terminal to request inputs,\n// and the provided functions to display messages and errors.\n//\n// The terminal is reached directly through /dev/tty or CONIN$/CONOUT$,\n// bypassing standard input and output, so this UI can be used even when\n// standard input or output are redirected.\nfunc NewTerminalUI(printf, warningf func(format string, v ...any)) *ClientUI {\n\treturn &ClientUI{\n\t\tDisplayMessage: func(name, message string) error {\n\t\t\tprintf(\"%s plugin: %s\", name, message)\n\t\t\treturn nil\n\t\t},\n\t\tRequestValue: func(name, message string, isSecret bool) (s string, err error) {\n\t\t\tdefer func() {\n\t\t\t\tif err != nil {\n\t\t\t\t\twarningf(\"could not read value for age-plugin-%s: %v\", name, err)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tif isSecret {\n\t\t\t\tsecret, err := term.ReadSecret(message)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\treturn string(secret), nil\n\t\t\t} else {\n\t\t\t\tpublic, err := term.ReadPublic(message)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\treturn string(public), nil\n\t\t\t}\n\t\t},\n\t\tConfirm: func(name, message, yes, no string) (choseYes bool, err error) {\n\t\t\tdefer func() {\n\t\t\t\tif err != nil {\n\t\t\t\t\twarningf(\"could not read value for age-plugin-%s: %v\", name, err)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tif no == \"\" {\n\t\t\t\tmessage += fmt.Sprintf(\" (press enter for %q)\", yes)\n\t\t\t\t_, err := term.ReadSecret(message)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn false, err\n\t\t\t\t}\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t\tmessage += fmt.Sprintf(\" (press [1] for %q or [2] for %q)\", yes, no)\n\t\t\tfor {\n\t\t\t\tselection, err := term.ReadCharacter(message)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn false, err\n\t\t\t\t}\n\t\t\t\tswitch selection {\n\t\t\t\tcase '1':\n\t\t\t\t\treturn true, nil\n\t\t\t\tcase '2':\n\t\t\t\t\treturn false, nil\n\t\t\t\tcase '\\x03': // CTRL-C\n\t\t\t\t\treturn false, errors.New(\"user cancelled prompt\")\n\t\t\t\tdefault:\n\t\t\t\t\twarningf(\"reading value for age-plugin-%s: invalid selection %q\", name, selection)\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\tWaitTimer: func(name string) {\n\t\t\tprintf(\"waiting on %s plugin...\", name)\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "pq.go",
    "content": "// Copyright 2025 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage age\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"filippo.io/age/internal/bech32\"\n\t\"filippo.io/age/internal/format\"\n\t\"filippo.io/hpke\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n)\n\nconst pqLabel = \"age-encryption.org/mlkem768x25519\"\n\n// HybridRecipient is the standard age public key. Messages encrypted to\n// this recipient can be decrypted with the corresponding [HybridIdentity].\n//\n// This recipient is safe against future cryptographically-relevant quantum\n// computers, and can only be used along with other post-quantum recipients.\n//\n// This recipient is anonymous, in the sense that an attacker can't tell from\n// the message alone if it is encrypted to a certain recipient.\ntype HybridRecipient struct {\n\tpk hpke.PublicKey\n}\n\nvar _ Recipient = &HybridRecipient{}\n\n// newHybridRecipient returns a new [HybridRecipient] from a raw HPKE public key.\nfunc newHybridRecipient(publicKey []byte) (*HybridRecipient, error) {\n\tpk, err := hpke.MLKEM768X25519().NewPublicKey(publicKey)\n\tif err != nil {\n\t\treturn nil, errors.New(\"invalid MLKEM768-X25519 public key\")\n\t}\n\treturn &HybridRecipient{pk: pk}, nil\n}\n\n// ParseHybridRecipient returns a new [HybridRecipient] from a Bech32 public key\n// encoding with the \"age1pq1\" prefix.\nfunc ParseHybridRecipient(s string) (*HybridRecipient, error) {\n\tt, k, err := bech32.Decode(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed recipient %q: %v\", s, err)\n\t}\n\tif t != \"age1pq\" {\n\t\treturn nil, fmt.Errorf(\"malformed recipient %q: invalid type %q\", s, t)\n\t}\n\tr, err := newHybridRecipient(k)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed recipient %q: %v\", s, err)\n\t}\n\treturn r, nil\n}\n\nfunc (r *HybridRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {\n\ts, _, err := r.WrapWithLabels(fileKey)\n\treturn s, err\n}\n\n// WrapWithLabels implements [RecipientWithLabels], returning a single\n// \"postquantum\" label. This ensures a HybridRecipient can't be mixed with other\n// recipients that would defeat its post-quantum security.\n//\n// To unsafely bypass this restriction, wrap HybridRecipient in a [Recipient]\n// type that doesn't expose WrapWithLabels.\nfunc (r *HybridRecipient) WrapWithLabels(fileKey []byte) ([]*Stanza, []string, error) {\n\tenc, s, err := hpke.NewSender(r.pk, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(pqLabel))\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to set up HPKE sender: %v\", err)\n\t}\n\tct, err := s.Seal(nil, fileKey)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to encrypt file key: %v\", err)\n\t}\n\n\tl := &Stanza{\n\t\tType: \"mlkem768x25519\",\n\t\tArgs: []string{format.EncodeToString(enc)},\n\t\tBody: ct,\n\t}\n\n\treturn []*Stanza{l}, []string{\"postquantum\"}, nil\n}\n\n// String returns the Bech32 public key encoding of r.\nfunc (r *HybridRecipient) String() string {\n\ts, _ := bech32.Encode(\"age1pq\", r.pk.Bytes())\n\treturn s\n}\n\n// HybridIdentity is the standard age private key, which can decrypt messages\n// encrypted to the corresponding [HybridRecipient].\ntype HybridIdentity struct {\n\tk hpke.PrivateKey\n}\n\nvar _ Identity = &HybridIdentity{}\n\n// newHybridIdentity returns a new [HybridIdentity] from a raw HPKE private key.\nfunc newHybridIdentity(secretKey []byte) (*HybridIdentity, error) {\n\tk, err := hpke.MLKEM768X25519().NewPrivateKey(secretKey)\n\tif err != nil {\n\t\treturn nil, errors.New(\"invalid MLKEM768-X25519 secret key\")\n\t}\n\treturn &HybridIdentity{k: k}, nil\n}\n\n// GenerateHybridIdentity randomly generates a new [HybridIdentity].\nfunc GenerateHybridIdentity() (*HybridIdentity, error) {\n\tk, err := hpke.MLKEM768X25519().GenerateKey()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to generate post-quantum identity: %v\", err)\n\t}\n\treturn &HybridIdentity{k: k}, nil\n}\n\n// ParseHybridIdentity returns a new [HybridIdentity] from a Bech32 private key\n// encoding with the \"AGE-SECRET-KEY-PQ-1\" prefix.\nfunc ParseHybridIdentity(s string) (*HybridIdentity, error) {\n\tt, k, err := bech32.Decode(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed secret key: %v\", err)\n\t}\n\tif t != \"AGE-SECRET-KEY-PQ-\" {\n\t\treturn nil, fmt.Errorf(\"malformed secret key: unknown type %q\", t)\n\t}\n\tr, err := newHybridIdentity(k)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed secret key: %v\", err)\n\t}\n\treturn r, nil\n}\n\nfunc (i *HybridIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {\n\treturn multiUnwrap(i.unwrap, stanzas)\n}\n\nfunc (i *HybridIdentity) unwrap(block *Stanza) ([]byte, error) {\n\tif block.Type != \"mlkem768x25519\" {\n\t\treturn nil, ErrIncorrectIdentity\n\t}\n\tif len(block.Args) != 1 {\n\t\treturn nil, errors.New(\"invalid mlkem768x25519 recipient block\")\n\t}\n\tenc, err := format.DecodeString(block.Args[0])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse mlkem768x25519 recipient: %v\", err)\n\t}\n\tif len(block.Body) != fileKeySize+chacha20poly1305.Overhead {\n\t\treturn nil, errIncorrectCiphertextSize\n\t}\n\n\tr, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(pqLabel))\n\tif err != nil {\n\t\t// MLKEM768-X25519 does implicit rejection, so a mismatched key does not\n\t\t// hit this error path, but is only detected later when trying to open.\n\t\treturn nil, fmt.Errorf(\"invalid mlkem768x25519 recipient: %v\", err)\n\t}\n\tfileKey, err := r.Open(nil, block.Body)\n\tif err != nil {\n\t\treturn nil, ErrIncorrectIdentity\n\t}\n\treturn fileKey, nil\n}\n\n// Recipient returns the public [HybridRecipient] value corresponding to i.\nfunc (i *HybridIdentity) Recipient() *HybridRecipient {\n\treturn &HybridRecipient{pk: i.k.PublicKey()}\n}\n\n// String returns the Bech32 private key encoding of i.\nfunc (i *HybridIdentity) String() string {\n\tb, _ := i.k.Bytes()\n\ts, _ := bech32.Encode(\"AGE-SECRET-KEY-PQ-\", b)\n\treturn strings.ToUpper(s)\n}\n"
  },
  {
    "path": "primitives.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage age\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"io\"\n\n\t\"filippo.io/age/internal/format\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n\t\"golang.org/x/crypto/hkdf\"\n)\n\n// aeadEncrypt encrypts a message with a one-time key.\nfunc aeadEncrypt(key, plaintext []byte) ([]byte, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\t// The nonce is fixed because this function is only used in places where the\n\t// spec guarantees each key is only used once (by deriving it from values\n\t// that include fresh randomness), allowing us to save the overhead.\n\t// For the code that encrypts the actual payload, look at the\n\t// filippo.io/age/internal/stream package.\n\tnonce := make([]byte, chacha20poly1305.NonceSize)\n\treturn aead.Seal(nil, nonce, plaintext, nil), nil\n}\n\nvar errIncorrectCiphertextSize = errors.New(\"encrypted value has unexpected length\")\n\n// aeadDecrypt decrypts a message of an expected fixed size.\n//\n// The message size is limited to mitigate multi-key attacks, where a ciphertext\n// can be crafted that decrypts successfully under multiple keys. Short\n// ciphertexts can only target two keys, which has limited impact.\nfunc aeadDecrypt(key []byte, size int, ciphertext []byte) ([]byte, error) {\n\taead, err := chacha20poly1305.New(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(ciphertext) != size+aead.Overhead() {\n\t\treturn nil, errIncorrectCiphertextSize\n\t}\n\tnonce := make([]byte, chacha20poly1305.NonceSize)\n\treturn aead.Open(nil, nonce, ciphertext, nil)\n}\n\nfunc headerMAC(fileKey []byte, hdr *format.Header) ([]byte, error) {\n\th := hkdf.New(sha256.New, fileKey, nil, []byte(\"header\"))\n\thmacKey := make([]byte, 32)\n\tif _, err := io.ReadFull(h, hmacKey); err != nil {\n\t\treturn nil, err\n\t}\n\thh := hmac.New(sha256.New, hmacKey)\n\tif err := hdr.MarshalWithoutMAC(hh); err != nil {\n\t\treturn nil, err\n\t}\n\treturn hh.Sum(nil), nil\n}\n\nfunc streamKey(fileKey, nonce []byte) []byte {\n\th := hkdf.New(sha256.New, fileKey, nonce, []byte(\"payload\"))\n\tstreamKey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := io.ReadFull(h, streamKey); err != nil {\n\t\tpanic(\"age: internal error: failed to read from HKDF: \" + err.Error())\n\t}\n\treturn streamKey\n}\n"
  },
  {
    "path": "recipients_test.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage age_test\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"io\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n)\n\nfunc TestX25519RoundTrip(t *testing.T) {\n\ti, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tr := i.Recipient()\n\n\tif r1, err := age.ParseX25519Recipient(r.String()); err != nil {\n\t\tt.Fatal(err)\n\t} else if r1.String() != r.String() {\n\t\tt.Errorf(\"recipient did not round-trip through parsing: got %q, want %q\", r1, r)\n\t}\n\tif i1, err := age.ParseX25519Identity(i.String()); err != nil {\n\t\tt.Fatal(err)\n\t} else if i1.String() != i.String() {\n\t\tt.Errorf(\"identity did not round-trip through parsing: got %q, want %q\", i1, i)\n\t}\n\n\tfileKey := make([]byte, 16)\n\tif _, err := rand.Read(fileKey); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tstanzas, err := r.Wrap(fileKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tout, err := i.Unwrap(stanzas)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !bytes.Equal(fileKey, out) {\n\t\tt.Errorf(\"invalid output: %x, expected %x\", out, fileKey)\n\t}\n}\n\nfunc TestHybridRoundTrip(t *testing.T) {\n\ti, err := age.GenerateHybridIdentity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tr := i.Recipient()\n\n\tif r1, err := age.ParseHybridRecipient(r.String()); err != nil {\n\t\tt.Fatal(err)\n\t} else if r1.String() != r.String() {\n\t\tt.Errorf(\"recipient did not round-trip through parsing: got %q, want %q\", r1, r)\n\t}\n\tif i1, err := age.ParseHybridIdentity(i.String()); err != nil {\n\t\tt.Fatal(err)\n\t} else if i1.String() != i.String() {\n\t\tt.Errorf(\"identity did not round-trip through parsing: got %q, want %q\", i1, i)\n\t}\n\n\tfileKey := make([]byte, 16)\n\tif _, err := rand.Read(fileKey); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tstanzas, err := r.Wrap(fileKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tout, err := i.Unwrap(stanzas)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !bytes.Equal(fileKey, out) {\n\t\tt.Errorf(\"invalid output: %x, expected %x\", out, fileKey)\n\t}\n}\n\nfunc TestHybridMixingRestrictions(t *testing.T) {\n\tx25519, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\thybrid, err := age.GenerateHybridIdentity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\t// Hybrid recipients can be used together.\n\tif _, err := age.Encrypt(io.Discard, hybrid.Recipient(), hybrid.Recipient()); err != nil {\n\t\tt.Errorf(\"expected two hybrid recipients to work, got %v\", err)\n\t}\n\n\t// Hybrid and X25519 recipients cannot be mixed.\n\tif _, err := age.Encrypt(io.Discard, hybrid.Recipient(), x25519.Recipient()); err == nil {\n\t\tt.Error(\"expected hybrid mixed with X25519 to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, x25519.Recipient(), hybrid.Recipient()); err == nil {\n\t\tt.Error(\"expected X25519 mixed with hybrid to fail\")\n\t}\n}\n\nfunc TestScryptRoundTrip(t *testing.T) {\n\tpassword := \"twitch.tv/filosottile\"\n\n\tr, err := age.NewScryptRecipient(password)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tr.SetWorkFactor(15)\n\ti, err := age.NewScryptIdentity(password)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tfileKey := make([]byte, 16)\n\tif _, err := rand.Read(fileKey); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tstanzas, err := r.Wrap(fileKey)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tout, err := i.Unwrap(stanzas)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !bytes.Equal(fileKey, out) {\n\t\tt.Errorf(\"invalid output: %x, expected %x\", out, fileKey)\n\t}\n}\n"
  },
  {
    "path": "scrypt.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage age\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\n\t\"filippo.io/age/internal/format\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n\t\"golang.org/x/crypto/scrypt\"\n)\n\nconst scryptLabel = \"age-encryption.org/v1/scrypt\"\n\n// ScryptRecipient is a password-based recipient. Anyone with the password can\n// decrypt the message.\n//\n// If a ScryptRecipient is used, it must be the only recipient for the file: it\n// can't be mixed with other recipient types and can't be used multiple times\n// for the same file.\n//\n// Its use is not recommended for automated systems, which should prefer\n// [HybridRecipient] or [X25519Recipient].\ntype ScryptRecipient struct {\n\tpassword   []byte\n\tworkFactor int\n}\n\nvar _ Recipient = &ScryptRecipient{}\n\n// NewScryptRecipient returns a new ScryptRecipient with the provided password.\nfunc NewScryptRecipient(password string) (*ScryptRecipient, error) {\n\tif len(password) == 0 {\n\t\treturn nil, errors.New(\"passphrase can't be empty\")\n\t}\n\tr := &ScryptRecipient{\n\t\tpassword: []byte(password),\n\t\t// TODO: automatically scale this to 1s (with a min) in the CLI.\n\t\tworkFactor: 18, // 1s on a modern machine\n\t}\n\treturn r, nil\n}\n\n// SetWorkFactor sets the scrypt work factor to 2^logN.\n// It must be called before Wrap.\n//\n// If SetWorkFactor is not called, a reasonable default is used.\nfunc (r *ScryptRecipient) SetWorkFactor(logN int) {\n\tif logN > 30 || logN < 1 {\n\t\tpanic(\"age: SetWorkFactor called with illegal value\")\n\t}\n\tr.workFactor = logN\n}\n\nconst scryptSaltSize = 16\n\nfunc (r *ScryptRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {\n\tsalt := make([]byte, scryptSaltSize)\n\tif _, err := rand.Read(salt[:]); err != nil {\n\t\treturn nil, err\n\t}\n\n\tlogN := r.workFactor\n\tl := &Stanza{\n\t\tType: \"scrypt\",\n\t\tArgs: []string{format.EncodeToString(salt), strconv.Itoa(logN)},\n\t}\n\n\tsalt = append([]byte(scryptLabel), salt...)\n\tk, err := scrypt.Key(r.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to generate scrypt hash: %v\", err)\n\t}\n\n\twrappedKey, err := aeadEncrypt(k, fileKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tl.Body = wrappedKey\n\n\treturn []*Stanza{l}, nil\n}\n\n// WrapWithLabels implements [age.RecipientWithLabels], returning a random\n// label. This ensures a ScryptRecipient can't be mixed with other recipients\n// (including other ScryptRecipients).\n//\n// Users reasonably expect files encrypted to a passphrase to be [authenticated]\n// by that passphrase, i.e. for it to be impossible to produce a file that\n// decrypts successfully with a passphrase without knowing it. If a file is\n// encrypted to other recipients, those parties can produce different files that\n// would break that expectation.\n//\n// [authenticated]: https://words.filippo.io/dispatches/age-authentication/\nfunc (r *ScryptRecipient) WrapWithLabels(fileKey []byte) (stanzas []*Stanza, labels []string, err error) {\n\tstanzas, err = r.Wrap(fileKey)\n\n\trandom := make([]byte, 16)\n\tif _, err := rand.Read(random); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tlabels = []string{hex.EncodeToString(random)}\n\n\treturn\n}\n\n// ScryptIdentity is a password-based identity.\ntype ScryptIdentity struct {\n\tpassword      []byte\n\tmaxWorkFactor int\n}\n\nvar _ Identity = &ScryptIdentity{}\n\n// NewScryptIdentity returns a new ScryptIdentity with the provided password.\nfunc NewScryptIdentity(password string) (*ScryptIdentity, error) {\n\tif len(password) == 0 {\n\t\treturn nil, errors.New(\"passphrase can't be empty\")\n\t}\n\ti := &ScryptIdentity{\n\t\tpassword:      []byte(password),\n\t\tmaxWorkFactor: 22, // 15s on a modern machine\n\t}\n\treturn i, nil\n}\n\n// SetMaxWorkFactor sets the maximum accepted scrypt work factor to 2^logN.\n// It must be called before Unwrap.\n//\n// This caps the amount of work that Decrypt might have to do to process\n// received files. If SetMaxWorkFactor is not called, a fairly high default is\n// used, which might not be suitable for systems processing untrusted files.\nfunc (i *ScryptIdentity) SetMaxWorkFactor(logN int) {\n\tif logN > 30 || logN < 1 {\n\t\tpanic(\"age: SetMaxWorkFactor called with illegal value\")\n\t}\n\ti.maxWorkFactor = logN\n}\n\nfunc (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {\n\tfor _, s := range stanzas {\n\t\tif s.Type == \"scrypt\" && len(stanzas) != 1 {\n\t\t\treturn nil, errors.New(\"an scrypt recipient must be the only one\")\n\t\t}\n\t}\n\tfor _, s := range stanzas {\n\t\tif s.Type != \"scrypt\" {\n\t\t\tcontinue\n\t\t}\n\t\treturn i.unwrap(s)\n\t}\n\treturn nil, fmt.Errorf(\"%w: file is not passphrase-encrypted\", ErrIncorrectIdentity)\n}\n\nvar digitsRe = regexp.MustCompile(`^[1-9][0-9]*$`)\n\nfunc (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {\n\tif block.Type != \"scrypt\" {\n\t\treturn nil, errors.New(\"internal error: unwrap called on non-scrypt stanza\")\n\t}\n\tif len(block.Args) != 2 {\n\t\treturn nil, errors.New(\"invalid scrypt recipient block\")\n\t}\n\tsalt, err := format.DecodeString(block.Args[0])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse scrypt salt: %v\", err)\n\t}\n\tif len(salt) != scryptSaltSize {\n\t\treturn nil, errors.New(\"invalid scrypt recipient block\")\n\t}\n\tif w := block.Args[1]; !digitsRe.MatchString(w) {\n\t\treturn nil, fmt.Errorf(\"scrypt work factor encoding invalid: %q\", w)\n\t}\n\tlogN, err := strconv.Atoi(block.Args[1])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse scrypt work factor: %v\", err)\n\t}\n\tif logN > i.maxWorkFactor {\n\t\treturn nil, fmt.Errorf(\"scrypt work factor too large: %v\", logN)\n\t}\n\tif logN <= 0 { // unreachable\n\t\treturn nil, fmt.Errorf(\"invalid scrypt work factor: %v\", logN)\n\t}\n\n\tsalt = append([]byte(scryptLabel), salt...)\n\tk, err := scrypt.Key(i.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)\n\tif err != nil { // unreachable\n\t\treturn nil, fmt.Errorf(\"failed to generate scrypt hash: %v\", err)\n\t}\n\n\t// This AEAD is not robust, so an attacker could craft a message that\n\t// decrypts under two different keys (meaning two different passphrases) and\n\t// then use an error side-channel in an online decryption oracle to learn if\n\t// either key is correct. This is deemed acceptable because the use case (an\n\t// online decryption oracle) is not recommended, and the security loss is\n\t// only one bit. This also does not bypass any scrypt work, although that work\n\t// can be precomputed in an online oracle scenario.\n\tfileKey, err := aeadDecrypt(k, fileKeySize, block.Body)\n\tif err == errIncorrectCiphertextSize {\n\t\treturn nil, errors.New(\"invalid scrypt recipient block: incorrect file key size\")\n\t} else if err != nil {\n\t\t// Wrap [ErrIncorrectIdentity] so that multiple passphrases can be tried\n\t\t// in sequence by passing multiple [ScryptIdentity] values to [Decrypt].\n\t\treturn nil, fmt.Errorf(\"%w: incorrect passphrase\", ErrIncorrectIdentity)\n\t}\n\treturn fileKey, nil\n}\n"
  },
  {
    "path": "tag/internal/age-plugin-tagtest/plugin-tagtest.go",
    "content": "// Command age-plugin-tagtest is a that decrypts files encrypted to fixed\n// age1tag1... or age1tagpq1... recipients for testing purposes.\n//\n// It can be used with the \"-j\" flag:\n//\n//\tgo install ./tag/internal/age-plugin-tagtest\n//\tage -d -j tagtest file.age\npackage main\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/plugin\"\n\t\"filippo.io/age/tag/internal/tagtest\"\n)\n\nconst classicRecipient = \"age1tag1qwe0kafsjrar4txm6heqnhpfuggzr0gvznz7fvygxrlq90u5mq2pysxtw6h\"\n\nconst hybridRecipient = \"age1tagpq14h4z7cks9sxftfc8tq4xektt4854ur9rv76tvujdvtzk2fmyywkvh9z2emz3x4epvhz7qdt2v7uksyyq2cdzf3k04ny0g5sc3u4heqh3r9v4cnwhfjw0a2azpgmnk9xk02wvywt5szcq6q3jvwsjxvkn3tsk52vqjczdcvc398ym4j6cvqas4w99gkgt7ur3fmt4g873phr23tgxw3f7wgsz9zxz7m8cp27vpq3h5vc8nssjemtr2etmtmqkg4fzn2u9x9zvtysuya5yrytgx482ftx9864h8a6pprarxd3d0qe8nw2at5ekg3tsahtef7kawasxjamyckw2ans6v933vuypcfrra32f89r2v72mka9hhc55s49xe2khfsq7w9r2zynuzfx4fg6v7jjncsc87rw2yy8qp8hr27edus6zw5xd6m3hax2nxhl2dys9792z3wp5c034sfkrxe86guj7pfdh7sytzrufl9euhuhyf9w6c7z2nwf7v5v8f2s4gplgvfx4jj4le22k2qn242qkqkcwx7llfyrct7jm2wcv0ytypeh6h93ezgtd7q6zr428qze3dec5jxlc5xxjetyephp42fljft3s02p0570kjwyfeyjcnks2vglkvyus5g9l4z6m0gf8wu22ygfm40028txwjlxvvgnn7c36z783c6tmc9k5nef8nucj6u3ustff5vhtnzhnscsvsz79wzrkv3sujtntx4wezucy6lp49flmnyydn3khk2xsesw0ekn4u44nzqw2g2rjyrrl7crshlzttgpe0jvqycjzp9kmtz23t3yu0w9j4n344nnrf88k2jqqfpjxte38pcn0epr879pqsuvajxrkmsas89pvfrzwcewneujn08guj5pvvrtn5hzzg2y6u4wwjqqxx4x8w65yc4dchf750dft8kgcttt2f6j0j5v8s7tkaua78tte7artdfar544vl0rau79h95mc4ghp887z82s6rq93txpkvan86n963kagqkldngnkjcn28zdrh38vdxj002zqs9mx7zjvg3ynzdfhfakkynt9fyqpaxpsdrsqrycuhw5ykwgjz6wldef7xtu6p689234hstxe7v8e5422f2dy8ystn57z3fvy9yfrm4t3lye6ejk5n6x8zqexmql7lx965xcxuuy38xzyt8j9qprnwgfqgx54l4tnjdpzdde6xgmwtnpkfwvyr7rkgnavvjn6a3e56wtvjx3evmhjjxvukpq5zqrj0s4sntkz3yeszs5dty8q0q6m7dgp6mjpvaer0c4343g72eycfqzkjupeaemh0n8e935hqs8fh3jgk7fzyxctzuqlx6d2q9jaf8r9wu4sjxj5w6ppw7m9c3hxrzpcv2uek3kxnndgf2hd99q9v2ux8pjkv29ntslvnvhy09dvcy9578rt89gf4cj4cu79zjxtlj3dpct6rjme02zj3qspsade96njkkufu9zuq2lk3qwvddpxjkqm2hnpqwck54zug7ctvkgvk325lwkg4q5rf73zkgys5e9y8jqc96ntdyl4r78lgtw4k5uljk5ttf46s3gc0rq0jwmddnxt875twwq92505zh3zkse5ag2dhjjxyfzkn7xv3j0kv9r3jzpvgep8fq6z8mar509u4fvnhvthp2ah0r45lsyq0mm6fwkcs30v8k9wzvgt6uvcty6qsjvarjs3htym69zu43m4jd3k4tllrr8c05v6p6spuhup4hkk2p9fp9lxafe3pntcn4nk83gzhjjpcjwyg7jcyz5uancu0fakgz27up7ymzp2xv3sqyqewkkqynskw9qkvysrncxj0cy7dt6q8dsseuwmc2urfmcvkykf82wfa54t85hqx8gywhmhzunm2x0d66a4pwl0xl78fhkces5dpq8pfnp35m5a3u8vdam64zx5s5x9cmnrx3zr066f4f8hlecqnq2fd5quw79ljg3q5nvs6ggmm4gkc\"\n\nfunc init() {\n\tc := tagtest.NewClassicIdentity(\"age-plugin-tagtest\").Recipient().String()\n\tif c != classicRecipient {\n\t\tlog.Fatalf(\"unexpected classic recipient: %s\", c)\n\t}\n\th := tagtest.NewHybridIdentity(\"age-plugin-tagtest\").Recipient().String()\n\tif h != hybridRecipient {\n\t\tlog.Fatalf(\"unexpected hybrid recipient: %s\", h)\n\t}\n}\n\nfunc main() {\n\tp, err := plugin.New(\"tagtest\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tp.HandleIdentity(func(b []byte) (age.Identity, error) {\n\t\tif len(b) != 0 {\n\t\t\treturn nil, fmt.Errorf(\"unexpected identity data\")\n\t\t}\n\t\treturn &tagtestIdentity{}, nil\n\t})\n\tos.Exit(p.Main())\n}\n\ntype tagtestIdentity struct{}\n\nfunc (i *tagtestIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) {\n\tclassic := tagtest.NewClassicIdentity(\"age-plugin-tagtest\")\n\tif key, err := classic.Unwrap(ss); err == nil {\n\t\treturn key, nil\n\t} else if !errors.Is(err, age.ErrIncorrectIdentity) {\n\t\treturn nil, err\n\t}\n\thybrid := tagtest.NewHybridIdentity(\"age-plugin-tagtest\")\n\treturn hybrid.Unwrap(ss)\n}\n"
  },
  {
    "path": "tag/internal/tagtest/tagtest.go",
    "content": "// Copyright 2025 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage tagtest\n\nimport (\n\t\"crypto/ecdh\"\n\t\"crypto/subtle\"\n\t\"fmt\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/internal/format\"\n\t\"filippo.io/age/tag\"\n\t\"filippo.io/hpke\"\n\t\"filippo.io/nistec\"\n)\n\ntype ClassicIdentity struct {\n\tk hpke.PrivateKey\n}\n\nvar _ age.Identity = &ClassicIdentity{}\n\nfunc NewClassicIdentity(seed string) *ClassicIdentity {\n\tk, err := hpke.DHKEM(ecdh.P256()).DeriveKeyPair([]byte(seed))\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to generate key: %v\", err))\n\t}\n\treturn &ClassicIdentity{k: k}\n}\n\nfunc (i *ClassicIdentity) Recipient() *tag.Recipient {\n\tuncompressed := i.k.PublicKey().Bytes()\n\tp, err := nistec.NewP256Point().SetBytes(uncompressed)\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to parse public key: %v\", err))\n\t}\n\tr, err := tag.NewClassicRecipient(p.BytesCompressed())\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to create recipient: %v\", err))\n\t}\n\treturn r\n}\n\nfunc (i *ClassicIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) {\n\tfor _, s := range ss {\n\t\tif s.Type != \"p256tag\" {\n\t\t\tcontinue\n\t\t}\n\t\tif len(s.Args) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"malformed stanza\")\n\t\t}\n\t\ttagArg, err := format.DecodeString(s.Args[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"malformed tag: %v\", err)\n\t\t}\n\t\tif len(tagArg) != 4 {\n\t\t\treturn nil, fmt.Errorf(\"invalid tag length: %d\", len(tagArg))\n\t\t}\n\t\tenc, err := format.DecodeString(s.Args[1])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"malformed encapsulated key: %v\", err)\n\t\t}\n\t\tif len(enc) != 65 {\n\t\t\treturn nil, fmt.Errorf(\"invalid encapsulated key length: %d\", len(enc))\n\t\t}\n\t\tif len(s.Body) != 32 {\n\t\t\treturn nil, fmt.Errorf(\"invalid encrypted file key length: %d\", len(s.Body))\n\t\t}\n\n\t\texpTag, err := i.Recipient().Tag(enc)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to compute tag: %v\", err)\n\t\t}\n\t\tif subtle.ConstantTimeCompare(tagArg, expTag[:4]) != 1 {\n\t\t\treturn nil, age.ErrIncorrectIdentity\n\t\t}\n\n\t\tr, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(\"age-encryption.org/p256tag\"))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unwrap file key: %v\", err)\n\t\t}\n\t\treturn r.Open(nil, s.Body)\n\t}\n\treturn nil, age.ErrIncorrectIdentity\n}\n\ntype HybridIdentity struct {\n\tk hpke.PrivateKey\n}\n\nvar _ age.Identity = &HybridIdentity{}\n\nfunc NewHybridIdentity(seed string) *HybridIdentity {\n\tk, err := hpke.MLKEM768P256().DeriveKeyPair([]byte(seed))\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to generate key: %v\", err))\n\t}\n\treturn &HybridIdentity{k: k}\n}\n\nfunc (i *HybridIdentity) Recipient() *tag.Recipient {\n\tr, err := tag.NewHybridRecipient(i.k.PublicKey().Bytes())\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"failed to create recipient: %v\", err))\n\t}\n\treturn r\n}\n\nfunc (i *HybridIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) {\n\tfor _, s := range ss {\n\t\tif s.Type != \"mlkem768p256tag\" {\n\t\t\tcontinue\n\t\t}\n\t\tif len(s.Args) != 2 {\n\t\t\treturn nil, fmt.Errorf(\"malformed stanza\")\n\t\t}\n\t\ttagArg, err := format.DecodeString(s.Args[0])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"malformed tag: %v\", err)\n\t\t}\n\t\tif len(tagArg) != 4 {\n\t\t\treturn nil, fmt.Errorf(\"invalid tag length: %d\", len(tagArg))\n\t\t}\n\t\tenc, err := format.DecodeString(s.Args[1])\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"malformed encapsulated key: %v\", err)\n\t\t}\n\t\tif len(enc) != 1153 {\n\t\t\treturn nil, fmt.Errorf(\"invalid encapsulated key length: %d\", len(enc))\n\t\t}\n\t\tif len(s.Body) != 32 {\n\t\t\treturn nil, fmt.Errorf(\"invalid encrypted file key length: %d\", len(s.Body))\n\t\t}\n\n\t\texpTag, err := i.Recipient().Tag(enc)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to compute tag: %v\", err)\n\t\t}\n\t\tif subtle.ConstantTimeCompare(tagArg, expTag[:4]) != 1 {\n\t\t\treturn nil, age.ErrIncorrectIdentity\n\t\t}\n\n\t\tr, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(\"age-encryption.org/mlkem768p256tag\"))\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to unwrap file key: %v\", err)\n\t\t}\n\t\treturn r.Open(nil, s.Body)\n\t}\n\treturn nil, age.ErrIncorrectIdentity\n}\n"
  },
  {
    "path": "tag/tag.go",
    "content": "// Copyright 2025 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Package tag implements tagged P-256 or hybrid P-256 + ML-KEM-768 recipients,\n// which can be used with identities stored on hardware keys, usually supported\n// by dedicated plugins.\n//\n// The tag reduces privacy, by allowing an observer to correlate files with a\n// recipient (but not files amongst them without knowledge of the recipient),\n// but this is also a desirable property for hardware keys that require user\n// interaction for each decryption operation.\npackage tag\n\nimport (\n\t\"crypto/ecdh\"\n\t\"crypto/hkdf\"\n\t\"crypto/mlkem\"\n\t\"crypto/sha256\"\n\t\"fmt\"\n\t\"slices\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/internal/format\"\n\t\"filippo.io/age/plugin\"\n\t\"filippo.io/hpke\"\n\t\"filippo.io/nistec\"\n)\n\n// Recipient is a tagged P-256 or hybrid P-256 + ML-KEM-768 recipient.\n//\n// The latter recipient is safe against future cryptographically-relevant\n// quantum computers, and can only be used along with other post-quantum\n// recipients.\ntype Recipient struct {\n\tpk hpke.PublicKey\n}\n\nvar _ age.Recipient = &Recipient{}\n\n// ParseRecipient returns a new [Recipient] from a Bech32 public key\n// encoding with the \"age1tag1\" or \"age1tagpq1\" prefix.\nfunc ParseRecipient(s string) (*Recipient, error) {\n\tt, k, err := plugin.ParseRecipient(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed recipient %q: %v\", s, err)\n\t}\n\tswitch t {\n\tcase \"tag\":\n\t\tr, err := NewClassicRecipient(k)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"malformed recipient %q: %v\", s, err)\n\t\t}\n\t\treturn r, nil\n\tcase \"tagpq\":\n\t\tr, err := NewHybridRecipient(k)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"malformed recipient %q: %v\", s, err)\n\t\t}\n\t\treturn r, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"malformed recipient %q: invalid type %q\", s, t)\n\t}\n}\n\nconst compressedPointSize = 1 + 32\nconst uncompressedPointSize = 1 + 32 + 32\n\n// NewClassicRecipient returns a new P-256 [Recipient] from a raw public key.\nfunc NewClassicRecipient(publicKey []byte) (*Recipient, error) {\n\tif len(publicKey) != compressedPointSize {\n\t\treturn nil, fmt.Errorf(\"invalid tag recipient public key size %d\", len(publicKey))\n\t}\n\tp, err := nistec.NewP256Point().SetBytes(publicKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid tag recipient public key: %v\", err)\n\t}\n\tk, err := hpke.DHKEM(ecdh.P256()).NewPublicKey(p.Bytes())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid tag recipient public key: %v\", err)\n\t}\n\treturn &Recipient{k}, nil\n}\n\n// NewHybridRecipient returns a new hybrid P-256 + ML-KEM-768 [Recipient] from\n// raw concatenated public keys.\nfunc NewHybridRecipient(publicKey []byte) (*Recipient, error) {\n\tk, err := hpke.MLKEM768P256().NewPublicKey(publicKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid tagpq recipient public key: %v\", err)\n\t}\n\treturn &Recipient{k}, nil\n}\n\n// Hybrid reports whether r is a hybrid P-256 + ML-KEM-768 recipient.\nfunc (r *Recipient) Hybrid() bool {\n\treturn r.pk.KEM().ID() == hpke.MLKEM768P256().ID()\n}\n\nfunc (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {\n\ts, _, err := r.WrapWithLabels(fileKey)\n\treturn s, err\n}\n\n// Tag computes the 4-byte tag for the given ciphertext enc.\n//\n// This is a low-level method exposed for use by plugins that implement\n// identities compatible with tagged recipients.\nfunc (r *Recipient) Tag(enc []byte) ([]byte, error) {\n\tlabel, tagRecipient := \"age-encryption.org/p256tag\", r.Bytes()\n\tif r.Hybrid() {\n\t\tlabel = \"age-encryption.org/mlkem768p256tag\"\n\t\t// In hybrid mode, the tag is computed over just the P-256 part.\n\t\ttagRecipient = tagRecipient[mlkem.EncapsulationKeySize768:]\n\t\tif len(enc) != mlkem.CiphertextSize768+uncompressedPointSize {\n\t\t\treturn nil, fmt.Errorf(\"invalid ciphertext size\")\n\t\t}\n\t} else if len(enc) != uncompressedPointSize {\n\t\treturn nil, fmt.Errorf(\"invalid ciphertext size\")\n\t}\n\trh := sha256.Sum256(tagRecipient)\n\ttag, err := hkdf.Extract(sha256.New, append(slices.Clip(enc), rh[:4]...), []byte(label))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to compute tag: %v\", err)\n\t}\n\treturn tag[:4], nil\n}\n\n// WrapWithLabels implements [age.RecipientWithLabels], returning a single\n// \"postquantum\" label if r is a hybrid P-256 + ML-KEM-768 recipient. This\n// ensures a hybrid Recipient can't be mixed with other recipients that would\n// defeat its post-quantum security.\n//\n// To unsafely bypass this restriction, wrap Recipient in an [age.Recipient]\n// type that doesn't expose WrapWithLabels.\nfunc (r *Recipient) WrapWithLabels(fileKey []byte) ([]*age.Stanza, []string, error) {\n\tlabel, arg := \"age-encryption.org/p256tag\", \"p256tag\"\n\tif r.Hybrid() {\n\t\tlabel, arg = \"age-encryption.org/mlkem768p256tag\", \"mlkem768p256tag\"\n\t}\n\n\tenc, s, err := hpke.NewSender(r.pk, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(label))\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to set up HPKE sender: %v\", err)\n\t}\n\tct, err := s.Seal(nil, fileKey)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to encrypt file key: %v\", err)\n\t}\n\n\ttag, err := r.Tag(enc)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to compute tag: %v\", err)\n\t}\n\n\tl := &age.Stanza{\n\t\tType: arg,\n\t\tArgs: []string{\n\t\t\tformat.EncodeToString(tag[:4]),\n\t\t\tformat.EncodeToString(enc),\n\t\t},\n\t\tBody: ct,\n\t}\n\n\tif r.Hybrid() {\n\t\treturn []*age.Stanza{l}, []string{\"postquantum\"}, nil\n\t}\n\treturn []*age.Stanza{l}, nil, nil\n}\n\n// Bytes returns the raw recipient encoding.\nfunc (r *Recipient) Bytes() []byte {\n\tif r.Hybrid() {\n\t\treturn r.pk.Bytes()\n\t}\n\tp, err := nistec.NewP256Point().SetBytes(r.pk.Bytes())\n\tif err != nil {\n\t\tpanic(\"internal error: invalid P-256 public key\")\n\t}\n\treturn p.BytesCompressed()\n}\n\n// String returns the Bech32 public key encoding of r.\nfunc (r *Recipient) String() string {\n\tif r.Hybrid() {\n\t\treturn plugin.EncodeRecipient(\"tagpq\", r.Bytes())\n\t}\n\treturn plugin.EncodeRecipient(\"tag\", r.Bytes())\n}\n"
  },
  {
    "path": "tag/tag_test.go",
    "content": "// Copyright 2025 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage tag_test\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/tag\"\n\t\"filippo.io/age/tag/internal/tagtest\"\n)\n\nfunc TestClassicRoundTrip(t *testing.T) {\n\ti := tagtest.NewClassicIdentity(\"test\")\n\tr := i.Recipient()\n\n\tif r.Hybrid() {\n\t\tt.Error(\"classic recipient incorrectly reports as hybrid\")\n\t}\n\n\tr1, err := tag.ParseRecipient(r.String())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif r1.String() != r.String() {\n\t\tt.Errorf(\"recipient did not round-trip through parsing: got %q, want %q\", r1.String(), r.String())\n\t}\n\tif r1.Hybrid() {\n\t\tt.Error(\"parsed classic recipient incorrectly reports as hybrid\")\n\t}\n\n\tplaintext := []byte(\"hello world\")\n\n\tencrypted := &bytes.Buffer{}\n\tw, err := age.Encrypt(encrypted, r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdecrypted, err := age.Decrypt(encrypted, i)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tout, err := io.ReadAll(decrypted)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !bytes.Equal(plaintext, out) {\n\t\tt.Errorf(\"invalid output: %q, expected %q\", out, plaintext)\n\t}\n}\n\nfunc TestHybridRoundTrip(t *testing.T) {\n\ti := tagtest.NewHybridIdentity(\"test\")\n\tr := i.Recipient()\n\n\tif !r.Hybrid() {\n\t\tt.Error(\"hybrid recipient incorrectly reports as classic\")\n\t}\n\n\tr1, err := tag.ParseRecipient(r.String())\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif r1.String() != r.String() {\n\t\tt.Errorf(\"recipient did not round-trip through parsing: got %q, want %q\", r1.String(), r.String())\n\t}\n\tif !r1.Hybrid() {\n\t\tt.Error(\"parsed hybrid recipient incorrectly reports as classic\")\n\t}\n\n\tplaintext := []byte(\"hello world\")\n\n\tencrypted := &bytes.Buffer{}\n\tw, err := age.Encrypt(encrypted, r)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := w.Write(plaintext); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif err := w.Close(); err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdecrypted, err := age.Decrypt(encrypted, i)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tout, err := io.ReadAll(decrypted)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif !bytes.Equal(plaintext, out) {\n\t\tt.Errorf(\"invalid output: %q, expected %q\", out, plaintext)\n\t}\n}\n\nfunc TestTagHybridMixingRestrictions(t *testing.T) {\n\tx25519, err := age.GenerateX25519Identity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\ttagHybrid := tagtest.NewHybridIdentity(\"test\").Recipient()\n\n\t// Hybrid tag recipients can be used together with hybrid recipients.\n\thybrid, err := age.GenerateHybridIdentity()\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif _, err := age.Encrypt(io.Discard, tagHybrid, hybrid.Recipient()); err != nil {\n\t\tt.Errorf(\"expected hybrid tag + hybrid to work, got %v\", err)\n\t}\n\n\t// Hybrid tag and X25519 recipients cannot be mixed.\n\tif _, err := age.Encrypt(io.Discard, tagHybrid, x25519.Recipient()); err == nil {\n\t\tt.Error(\"expected hybrid tag mixed with X25519 to fail\")\n\t}\n\tif _, err := age.Encrypt(io.Discard, x25519.Recipient(), tagHybrid); err == nil {\n\t\tt.Error(\"expected X25519 mixed with hybrid tag to fail\")\n\t}\n\n\t// Classic tag and X25519 recipients can be mixed (both are non-PQ).\n\ttagClassic := tagtest.NewClassicIdentity(\"test\").Recipient()\n\tif _, err := age.Encrypt(io.Discard, tagClassic, x25519.Recipient()); err != nil {\n\t\tt.Errorf(\"expected classic tag + X25519 to work, got %v\", err)\n\t}\n}\n"
  },
  {
    "path": "testdata/example.age",
    "content": "age-encryption.org/v1\n-> X25519 8hrlM+ZBG3Dd4fF2+a583zdTIWDk8/R41kCYZsvwTW4\nyO4PYdlMWDJ+CxgUNRqY5Z0T/m+g3FCh5jIxGLbCVXc\n--- I/imevZzy8120JSzmJnmn/KMk3p5A11V83Nk41m9NPE\np6$R\u0007S,Z\u000eʲsMa\u0017w\u00138 Az\u001d\b\u0018\"r\\w41\u001a;u\u000e"
  },
  {
    "path": "testdata/example.zip.age",
    "content": "age-encryption.org/v1\n-> X25519 5CD81lZA72aQi0v6EnniOGkwaswpZ0AxCZNdiUVzP04\nol9DvdkiZWeRI4vMKRBVNxowDKwir4UPqYinSM5zqUI\n--- 2tyNGCaPoT6UnuOy7sQJf1eXn4pb7z2ukSgTDIxrJxU\nW\n\u001dd\u0006tUbT0(yK\u0004APdr1M\u001c\u0014~ kX>c܍[$9,\r\u0006G{턚Ftk*}9TޱhLЏW-RdˣHSdU\u0010Ě\u0007FsÈ2y\u0005+)]\b\u001c\u0012/,k=8(XRA01RY k4N6tvbc\u000b\u000b_F0dvx\u0001\r$X\u0002\u0012\u0015\u001f~ \r/"
  },
  {
    "path": "testdata/example_keys.txt",
    "content": "# Test key for ExampleParseIdentities.\nAGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU\n"
  },
  {
    "path": "testkit_test.go",
    "content": "// Copyright 2022 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build go1.18\n\npackage age_test\n\nimport (\n\t\"bytes\"\n\t\"compress/zlib\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\t\"io/fs\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"filippo.io/age\"\n\t\"filippo.io/age/armor\"\n\t\"filippo.io/age/internal/format\"\n\t\"filippo.io/age/internal/inspect\"\n\t\"filippo.io/age/internal/stream\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n\t\"golang.org/x/crypto/hkdf\"\n\n\tagetest \"c2sp.org/CCTV/age\"\n)\n\nfunc forEachVector(t *testing.T, f func(t *testing.T, v *vector)) {\n\ttests, err := fs.ReadDir(agetest.Vectors, \".\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tfor _, test := range tests {\n\t\tname := test.Name()\n\t\tcontents, err := fs.ReadFile(agetest.Vectors, name)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tf(t, parseVector(t, contents))\n\t\t})\n\t}\n}\n\ntype vector struct {\n\texpect      string\n\tpayloadHash *[32]byte\n\tfileKey     *[16]byte\n\tidentities  []age.Identity\n\tarmored     bool\n\tfile        []byte\n}\n\nfunc parseVector(t *testing.T, test []byte) *vector {\n\tvar z bool\n\tv := &vector{file: test}\n\tfor {\n\t\tline, rest, ok := bytes.Cut(v.file, []byte(\"\\n\"))\n\t\tif !ok {\n\t\t\tt.Fatal(\"invalid test file: no payload\")\n\t\t}\n\t\tv.file = rest\n\t\tif len(line) == 0 {\n\t\t\tbreak\n\t\t}\n\t\tkey, value, _ := strings.Cut(string(line), \": \")\n\t\tswitch key {\n\t\tcase \"expect\":\n\t\t\tswitch value {\n\t\t\tcase \"success\":\n\t\t\tcase \"HMAC failure\":\n\t\t\tcase \"header failure\":\n\t\t\tcase \"armor failure\":\n\t\t\tcase \"payload failure\":\n\t\t\tcase \"no match\":\n\t\t\tdefault:\n\t\t\t\tt.Fatal(\"invalid test file: unknown expect value:\", value)\n\t\t\t}\n\t\t\tv.expect = value\n\t\tcase \"payload\":\n\t\t\th, err := hex.DecodeString(value)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tv.payloadHash = (*[32]byte)(h)\n\t\tcase \"file key\":\n\t\t\th, err := hex.DecodeString(value)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tv.fileKey = (*[16]byte)(h)\n\t\tcase \"identity\":\n\t\t\tvar i age.Identity\n\t\t\ti, err := age.ParseX25519Identity(value)\n\t\t\tif err != nil {\n\t\t\t\ti, err = age.ParseHybridIdentity(value)\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tv.identities = append(v.identities, i)\n\t\tcase \"passphrase\":\n\t\t\ti, err := age.NewScryptIdentity(value)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tv.identities = append(v.identities, i)\n\t\tcase \"armored\":\n\t\t\tv.armored = true\n\t\tcase \"compressed\":\n\t\t\tif value != \"zlib\" {\n\t\t\t\tt.Fatal(\"invalid test file: unknown compression:\", value)\n\t\t\t}\n\t\t\tz = true\n\t\tcase \"comment\":\n\t\t\tt.Log(value)\n\t\tdefault:\n\t\t\tt.Fatal(\"invalid test file: unknown header key:\", key)\n\t\t}\n\t}\n\tif z {\n\t\tr, err := zlib.NewReader(bytes.NewReader(v.file))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tb, err := io.ReadAll(r)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif err := r.Close(); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tv.file = b\n\t}\n\treturn v\n}\n\nfunc TestVectors(t *testing.T) {\n\tforEachVector(t, func(t *testing.T, v *vector) {\n\t\tvar plaintext []byte\n\t\tt.Run(\"Decrypt\", func(t *testing.T) { plaintext = testDecrypt(t, v) })\n\t\tt.Run(\"DecryptReaderAt\", func(t *testing.T) { testDecryptReaderAt(t, v, plaintext) })\n\t\tt.Run(\"Inspect\", func(t *testing.T) { testInspect(t, v, plaintext) })\n\t\tt.Run(\"RoundTrip\", func(t *testing.T) { testVectorRoundTrip(t, v) })\n\t})\n}\n\nfunc testDecrypt(t *testing.T, v *vector) []byte {\n\tvar in io.Reader = bytes.NewReader(v.file)\n\tif v.armored {\n\t\tin = armor.NewReader(in)\n\t}\n\tr, err := age.Decrypt(in, v.identities...)\n\tif err != nil && strings.HasSuffix(err.Error(), \"bad header MAC\") {\n\t\tif v.expect == \"HMAC failure\" {\n\t\t\tt.Log(err)\n\t\t\treturn nil\n\t\t}\n\t\tt.Fatalf(\"expected %s, got HMAC error\", v.expect)\n\t} else if e := new(armor.Error); errors.As(err, &e) {\n\t\tif v.expect == \"armor failure\" {\n\t\t\tt.Log(err)\n\t\t\treturn nil\n\t\t}\n\t\tt.Fatalf(\"expected %s, got: %v\", v.expect, err)\n\t} else if _, ok := err.(*age.NoIdentityMatchError); ok {\n\t\tif v.expect == \"no match\" {\n\t\t\tt.Log(err)\n\t\t\treturn nil\n\t\t}\n\t\tt.Fatalf(\"expected %s, got: %v\", v.expect, err)\n\t} else if err != nil {\n\t\tif v.expect == \"header failure\" {\n\t\t\tt.Log(err)\n\t\t\treturn nil\n\t\t}\n\t\tt.Fatalf(\"expected %s, got: %v\", v.expect, err)\n\t} else if v.expect != \"success\" && v.expect != \"payload failure\" &&\n\t\tv.expect != \"armor failure\" {\n\t\tt.Fatalf(\"expected %s, got success\", v.expect)\n\t}\n\tout, err := io.ReadAll(r)\n\tif err != nil && v.expect == \"success\" {\n\t\tt.Fatalf(\"expected %s, got: %v\", v.expect, err)\n\t} else if err != nil {\n\t\tt.Log(err)\n\t\tif v.expect == \"armor failure\" {\n\t\t\tif e := new(armor.Error); !errors.As(err, &e) {\n\t\t\t\tt.Errorf(\"expected armor.Error, got %T\", err)\n\t\t\t}\n\t\t}\n\t\tif v.payloadHash != nil && sha256.Sum256(out) != *v.payloadHash {\n\t\t\tt.Errorf(\"partial payload hash mismatch, read %d bytes\", len(out))\n\t\t}\n\t\treturn out\n\t} else if v.expect != \"success\" {\n\t\tt.Fatalf(\"expected %s, got success\", v.expect)\n\t}\n\tif sha256.Sum256(out) != *v.payloadHash {\n\t\tt.Error(\"payload hash mismatch\")\n\t}\n\treturn out\n}\n\nfunc testDecryptReaderAt(t *testing.T, v *vector, plaintext []byte) {\n\tif v.armored {\n\t\tt.Skip(\"armor.NewReader does not implement ReaderAt\")\n\t}\n\trAt, s, err := age.DecryptReaderAt(bytes.NewReader(v.file), int64(len(v.file)), v.identities...)\n\tswitch v.expect {\n\tcase \"success\":\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected success, got: %v\", err)\n\t\t}\n\t\tif int64(len(plaintext)) != s {\n\t\t\tt.Errorf(\"unexpected size: got %d, want %d\", s, len(plaintext))\n\t\t}\n\tcase \"payload failure\":\n\t\t// DecryptReaderAt detects some (but not all) payload failures upfront,\n\t\t// either from the size of the payload, or by decrypting the last chunk\n\t\t// to authenticate its size.\n\t\tif err != nil {\n\t\t\tt.Log(err)\n\t\t\treturn\n\t\t}\n\tdefault:\n\t\tif err != nil {\n\t\t\tt.Log(err)\n\t\t\treturn\n\t\t}\n\t\tt.Fatalf(\"expected %s, got success\", v.expect)\n\t}\n\tout, err := io.ReadAll(io.NewSectionReader(rAt, 0, s))\n\tif v.expect == \"success\" {\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"expected success, got: %v\", err)\n\t\t}\n\t} else {\n\t\tif err == nil {\n\t\t\tt.Fatalf(\"expected %s, got success\", v.expect)\n\t\t}\n\t\tt.Log(err)\n\t\t// We can't check the partial payload hash, because the ReaderAt will\n\t\t// notice errors that a linearly scanning Reader could not. For example,\n\t\t// if there are two final chunks, the linear Reader will decrypt the\n\t\t// first one and then error out on the second, while the ReaderAt will\n\t\t// decrypt the second one to check the size, and then know that the\n\t\t// first chunk could not be the last one. Instead, check that the\n\t\t// prefix, if any, matches.\n\t\tif !bytes.HasPrefix(plaintext, out) {\n\t\t\tt.Errorf(\"partial payload prefix mismatch, read %d bytes\", len(out))\n\t\t}\n\t\treturn\n\t}\n\tif sha256.Sum256(out) != *v.payloadHash {\n\t\tt.Error(\"payload hash mismatch\")\n\t}\n}\n\nfunc testInspect(t *testing.T, v *vector, plaintext []byte) {\n\tif v.expect != \"success\" {\n\t\tt.Skip(\"invalid file, can't inspect\")\n\t}\n\tfor _, fileSize := range []int64{int64(len(v.file)), -1} {\n\t\tmetadata, err := inspect.Inspect(bytes.NewReader(v.file), fileSize)\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"inspect failed: %v\", err)\n\t\t}\n\t\tif metadata.Armor != v.armored {\n\t\t\tt.Errorf(\"unexpected armor: %v\", metadata.Armor)\n\t\t}\n\t\tif metadata.Armor && metadata.Sizes.Armor == 0 {\n\t\t\tt.Errorf(\"expected non-zero armor size\")\n\t\t}\n\t\tif metadata.Sizes.Armor+metadata.Sizes.Header+metadata.Sizes.Overhead+metadata.Sizes.MinPayload != int64(len(v.file)) {\n\t\t\tt.Errorf(\"size breakdown does not add up to file size\")\n\t\t}\n\t\tif metadata.Sizes.MinPayload != int64(len(plaintext)) {\n\t\t\tt.Errorf(\"unexpected payload size: got %d, want %d\", metadata.Sizes.MinPayload, len(plaintext))\n\t\t}\n\t\tif metadata.Sizes.MaxPayload != metadata.Sizes.MinPayload {\n\t\t\tt.Errorf(\"unexpected max payload size: got %d, want %d\", metadata.Sizes.MaxPayload, metadata.Sizes.MinPayload)\n\t\t}\n\t\tif metadata.Sizes.MinPadding != 0 || metadata.Sizes.MaxPadding != 0 {\n\t\t\tt.Errorf(\"unexpected padding sizes: got min %d max %d, want 0\", metadata.Sizes.MinPadding, metadata.Sizes.MaxPadding)\n\t\t}\n\t}\n}\n\n// testVectorsRoundTrip checks that any (valid) armor, header, and/or STREAM\n// payload in the test vectors re-encodes identically.\nfunc testVectorRoundTrip(t *testing.T, v *vector) {\n\tif v.armored {\n\t\tif v.expect == \"armor failure\" {\n\t\t\tt.Skip(\"invalid armor, nothing to round-trip\")\n\t\t}\n\t\tt.Run(\"armor\", func(t *testing.T) {\n\t\t\tpayload, err := io.ReadAll(armor.NewReader(bytes.NewReader(v.file)))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tbuf := &bytes.Buffer{}\n\t\t\tw := armor.NewWriter(buf)\n\t\t\tif _, err := w.Write(payload); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\tif err := w.Close(); err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\t\t\t// Armor format is not perfectly strict: CRLF ↔ LF and trailing and\n\t\t\t// leading spaces are allowed and won't round-trip.\n\t\t\texpect := bytes.Replace(v.file, []byte(\"\\r\\n\"), []byte(\"\\n\"), -1)\n\t\t\texpect = bytes.TrimSpace(expect)\n\t\t\texpect = append(expect, '\\n')\n\t\t\tif !bytes.Equal(buf.Bytes(), expect) {\n\t\t\t\tt.Error(\"got a different armor encoding\")\n\t\t\t}\n\t\t})\n\t\t// Armor tests are not interesting beyond their armor encoding.\n\t\treturn\n\t}\n\n\tif v.expect == \"header failure\" {\n\t\tt.Skip(\"invalid header, nothing to round-trip\")\n\t}\n\thdr, p, err := format.Parse(bytes.NewReader(v.file))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tpayload, err := io.ReadAll(p)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tt.Run(\"header\", func(t *testing.T) {\n\t\tbuf := &bytes.Buffer{}\n\t\tif err := hdr.Marshal(buf); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tbuf.Write(payload)\n\t\tif !bytes.Equal(buf.Bytes(), v.file) {\n\t\t\tt.Error(\"got a different header+payload encoding\")\n\t\t}\n\t})\n\n\tif v.expect != \"success\" {\n\t\treturn\n\t}\n\n\tt.Run(\"STREAM\", func(t *testing.T) {\n\t\tnonce, payload := payload[:16], payload[16:]\n\t\tkey := streamKey(v.fileKey[:], nonce)\n\n\t\tr, err := stream.NewDecryptReader(key, bytes.NewReader(payload))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tplaintext, err := io.ReadAll(r)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\trAt, err := stream.NewDecryptReaderAt(key, bytes.NewReader(payload), int64(len(payload)))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tplaintextAt, err := io.ReadAll(io.NewSectionReader(rAt, 0, int64(len(plaintext))))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !bytes.Equal(plaintextAt, plaintext) {\n\t\t\tt.Errorf(\"got a different plaintext from DecryptReaderAt\")\n\t\t}\n\n\t\tbuf := &bytes.Buffer{}\n\t\tw, err := stream.NewEncryptWriter(key, buf)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif _, err := w.Write(plaintext); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif err := w.Close(); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !bytes.Equal(buf.Bytes(), payload) {\n\t\t\tt.Error(\"got a different STREAM ciphertext\")\n\t\t}\n\n\t\ter, err := stream.NewEncryptReader(key, bytes.NewReader(plaintext))\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tciphertext, err := io.ReadAll(er)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tif !bytes.Equal(ciphertext, payload) {\n\t\t\tt.Error(\"got a different STREAM ciphertext from EncryptReader\")\n\t\t}\n\t})\n}\n\nfunc streamKey(fileKey, nonce []byte) []byte {\n\th := hkdf.New(sha256.New, fileKey, nonce, []byte(\"payload\"))\n\tstreamKey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := io.ReadFull(h, streamKey); err != nil {\n\t\tpanic(\"age: internal error: failed to read from HKDF: \" + err.Error())\n\t}\n\treturn streamKey\n}\n"
  },
  {
    "path": "x25519.go",
    "content": "// Copyright 2019 The age Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\npackage age\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"filippo.io/age/internal/bech32\"\n\t\"filippo.io/age/internal/format\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n\t\"golang.org/x/crypto/curve25519\"\n\t\"golang.org/x/crypto/hkdf\"\n)\n\nconst x25519Label = \"age-encryption.org/v1/X25519\"\n\n// X25519Recipient is the standard age pre-quantum public key. Messages\n// encrypted to this recipient can be decrypted with the corresponding\n// [X25519Identity]. For post-quantum resistance, use [HybridRecipient].\n//\n// This recipient is anonymous, in the sense that an attacker can't tell from\n// the message alone if it is encrypted to a certain recipient.\ntype X25519Recipient struct {\n\ttheirPublicKey []byte\n}\n\nvar _ Recipient = &X25519Recipient{}\n\n// newX25519RecipientFromPoint returns a new X25519Recipient from a raw Curve25519 point.\nfunc newX25519RecipientFromPoint(publicKey []byte) (*X25519Recipient, error) {\n\tif len(publicKey) != curve25519.PointSize {\n\t\treturn nil, errors.New(\"invalid X25519 public key\")\n\t}\n\tr := &X25519Recipient{\n\t\ttheirPublicKey: make([]byte, curve25519.PointSize),\n\t}\n\tcopy(r.theirPublicKey, publicKey)\n\treturn r, nil\n}\n\n// ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key\n// encoding with the \"age1\" prefix.\nfunc ParseX25519Recipient(s string) (*X25519Recipient, error) {\n\tt, k, err := bech32.Decode(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed recipient %q: %v\", s, err)\n\t}\n\tif t != \"age\" {\n\t\treturn nil, fmt.Errorf(\"malformed recipient %q: invalid type %q\", s, t)\n\t}\n\tr, err := newX25519RecipientFromPoint(k)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed recipient %q: %v\", s, err)\n\t}\n\treturn r, nil\n}\n\nfunc (r *X25519Recipient) Wrap(fileKey []byte) ([]*Stanza, error) {\n\tephemeral := make([]byte, curve25519.ScalarSize)\n\tif _, err := rand.Read(ephemeral); err != nil {\n\t\treturn nil, err\n\t}\n\tourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tsharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tl := &Stanza{\n\t\tType: \"X25519\",\n\t\tArgs: []string{format.EncodeToString(ourPublicKey)},\n\t}\n\n\tsalt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey))\n\tsalt = append(salt, ourPublicKey...)\n\tsalt = append(salt, r.theirPublicKey...)\n\th := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))\n\twrappingKey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := io.ReadFull(h, wrappingKey); err != nil {\n\t\treturn nil, err\n\t}\n\n\twrappedKey, err := aeadEncrypt(wrappingKey, fileKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tl.Body = wrappedKey\n\n\treturn []*Stanza{l}, nil\n}\n\n// String returns the Bech32 public key encoding of r.\nfunc (r *X25519Recipient) String() string {\n\ts, _ := bech32.Encode(\"age\", r.theirPublicKey)\n\treturn s\n}\n\n// X25519Identity is the standard pre-quantum age private key, which can decrypt\n// messages encrypted to the corresponding [X25519Recipient]. For post-quantum\n// resistance, use [HybridIdentity].\ntype X25519Identity struct {\n\tsecretKey, ourPublicKey []byte\n}\n\nvar _ Identity = &X25519Identity{}\n\n// newX25519IdentityFromScalar returns a new X25519Identity from a raw Curve25519 scalar.\nfunc newX25519IdentityFromScalar(secretKey []byte) (*X25519Identity, error) {\n\tif len(secretKey) != curve25519.ScalarSize {\n\t\treturn nil, errors.New(\"invalid X25519 secret key\")\n\t}\n\ti := &X25519Identity{\n\t\tsecretKey: make([]byte, curve25519.ScalarSize),\n\t}\n\tcopy(i.secretKey, secretKey)\n\ti.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint)\n\treturn i, nil\n}\n\n// GenerateX25519Identity randomly generates a new X25519Identity.\nfunc GenerateX25519Identity() (*X25519Identity, error) {\n\tsecretKey := make([]byte, curve25519.ScalarSize)\n\tif _, err := rand.Read(secretKey); err != nil {\n\t\treturn nil, fmt.Errorf(\"internal error: %v\", err)\n\t}\n\treturn newX25519IdentityFromScalar(secretKey)\n}\n\n// ParseX25519Identity returns a new X25519Identity from a Bech32 private key\n// encoding with the \"AGE-SECRET-KEY-1\" prefix.\nfunc ParseX25519Identity(s string) (*X25519Identity, error) {\n\tt, k, err := bech32.Decode(s)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed secret key: %v\", err)\n\t}\n\tif t != \"AGE-SECRET-KEY-\" {\n\t\treturn nil, fmt.Errorf(\"malformed secret key: unknown type %q\", t)\n\t}\n\tr, err := newX25519IdentityFromScalar(k)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"malformed secret key: %v\", err)\n\t}\n\treturn r, nil\n}\n\nfunc (i *X25519Identity) Unwrap(stanzas []*Stanza) ([]byte, error) {\n\treturn multiUnwrap(i.unwrap, stanzas)\n}\n\nfunc (i *X25519Identity) unwrap(block *Stanza) ([]byte, error) {\n\tif block.Type != \"X25519\" {\n\t\treturn nil, ErrIncorrectIdentity\n\t}\n\tif len(block.Args) != 1 {\n\t\treturn nil, errors.New(\"invalid X25519 recipient block\")\n\t}\n\tpublicKey, err := format.DecodeString(block.Args[0])\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to parse X25519 recipient: %v\", err)\n\t}\n\tif len(publicKey) != curve25519.PointSize {\n\t\treturn nil, errors.New(\"invalid X25519 recipient block\")\n\t}\n\n\tsharedSecret, err := curve25519.X25519(i.secretKey, publicKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid X25519 recipient: %v\", err)\n\t}\n\n\tsalt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey))\n\tsalt = append(salt, publicKey...)\n\tsalt = append(salt, i.ourPublicKey...)\n\th := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label))\n\twrappingKey := make([]byte, chacha20poly1305.KeySize)\n\tif _, err := io.ReadFull(h, wrappingKey); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfileKey, err := aeadDecrypt(wrappingKey, fileKeySize, block.Body)\n\tif err == errIncorrectCiphertextSize {\n\t\treturn nil, errors.New(\"invalid X25519 recipient block: incorrect file key size\")\n\t} else if err != nil {\n\t\treturn nil, ErrIncorrectIdentity\n\t}\n\treturn fileKey, nil\n}\n\n// Recipient returns the public X25519Recipient value corresponding to i.\nfunc (i *X25519Identity) Recipient() *X25519Recipient {\n\tr := &X25519Recipient{}\n\tr.theirPublicKey = i.ourPublicKey\n\treturn r\n}\n\n// String returns the Bech32 private key encoding of i.\nfunc (i *X25519Identity) String() string {\n\ts, _ := bech32.Encode(\"AGE-SECRET-KEY-\", i.secretKey)\n\treturn strings.ToUpper(s)\n}\n"
  }
]