pax_global_header00006660000000000000000000000064143575355220014525gustar00rootroot0000000000000052 comment=ccc4b44dbca526356345f80ebc8bca39c4731c3d age-1.1.1/000077500000000000000000000000001435753552200122615ustar00rootroot00000000000000age-1.1.1/.gitattributes000066400000000000000000000000471435753552200151550ustar00rootroot00000000000000*.age binary testdata/testkit/* binary age-1.1.1/.github/000077500000000000000000000000001435753552200136215ustar00rootroot00000000000000age-1.1.1/.github/CONTRIBUTING.md000066400000000000000000000054021435753552200160530ustar00rootroot00000000000000## Issues I want to hear about any issues you encounter while using age. Particularly 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)**! ✨ Not 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. ## Pull requests age 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. Therefore, **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. ## Other ways to contribute age 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. * **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. * Integrate age into existing projects that might use it, for example replacing legacy alternatives. * Build and maintain an [age plugin](https://c2sp.org/age-plugin) for a KMS or platform. * Watch the [discussions](https://github.com/FiloSottile/age/discussions) and help other users. * Provide bindings in a language or framework that doesn't support age well. * Package age for an ecosystem that doesn't have packages yet. If you build or write something related to age, [let me know](https://github.com/FiloSottile/age/discussions/new?category=general)! 💖 age-1.1.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001435753552200160045ustar00rootroot00000000000000age-1.1.1/.github/ISSUE_TEMPLATE/bug-report.md000066400000000000000000000003731435753552200204170ustar00rootroot00000000000000--- name: Bug report 🐞 about: Did you encounter a bug in this implementation? title: '' labels: '' assignees: '' --- ## Environment * OS: * age version: ## What were you trying to do ## What happened ``` ``` age-1.1.1/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000011471435753552200177770ustar00rootroot00000000000000contact_links: - name: UX report ✨ url: https://github.com/FiloSottile/age/discussions/new?category=UX-reports about: Was age hard to use? It's not you, it's us. We want to hear about it. - name: Spec feedback 📃 url: https://github.com/FiloSottile/age/discussions/new?category=Spec-feedback about: Have a comment about the age spec as it's implemented by this and other tools? - name: Questions, feature requests, and more 💬 url: https://github.com/FiloSottile/age/discussions about: Do you need support? Did you make something with age? Do you have an idea? Tell us about it! age-1.1.1/.github/workflows/000077500000000000000000000000001435753552200156565ustar00rootroot00000000000000age-1.1.1/.github/workflows/build.yml000066400000000000000000000054541435753552200175100ustar00rootroot00000000000000name: Build and upload binaries on: release: types: [published] push: pull_request: permissions: contents: read jobs: build: name: Build binaries runs-on: ubuntu-latest environment: "Build, sign, release binaries" strategy: matrix: include: - {GOOS: linux, GOARCH: amd64} - {GOOS: linux, GOARCH: arm, GOARM: 6} - {GOOS: linux, GOARCH: arm64} - {GOOS: darwin, GOARCH: amd64} - {GOOS: darwin, GOARCH: arm64} - {GOOS: windows, GOARCH: amd64} - {GOOS: freebsd, GOARCH: amd64} steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: 1.x - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: Build binary run: | cp LICENSE "$RUNNER_TEMP/LICENSE" echo -e "\n---\n" >> "$RUNNER_TEMP/LICENSE" curl -L "https://go.dev/LICENSE?m=text" >> "$RUNNER_TEMP/LICENSE" VERSION="$(git describe --tags)" DIR="$(mktemp -d)" mkdir "$DIR/age" cp "$RUNNER_TEMP/LICENSE" "$DIR/age" go build -o "$DIR/age" -ldflags "-X main.Version=$VERSION" -trimpath ./cmd/... if [ "$GOOS" == "windows" ]; then sudo apt-get update && sudo apt-get install -y osslsigncode if [ -n "${{ secrets.SIGN_PASS }}" ]; then for exe in "$DIR"/age/*.exe; do /usr/bin/osslsigncode sign -t "http://timestamp.comodoca.com" \ -certs .github/workflows/certs/uitacllc.crt \ -key .github/workflows/certs/uitacllc.key \ -pass "${{ secrets.SIGN_PASS }}" \ -n age -in "$exe" -out "$exe.signed" mv "$exe.signed" "$exe" done fi ( cd "$DIR"; zip age.zip -r age ) mv "$DIR/age.zip" "age-$VERSION-$GOOS-$GOARCH.zip" else tar -cvzf "age-$VERSION-$GOOS-$GOARCH.tar.gz" -C "$DIR" age fi env: CGO_ENABLED: 0 GOOS: ${{ matrix.GOOS }} GOARCH: ${{ matrix.GOARCH }} GOARM: ${{ matrix.GOARM }} - name: Upload workflow artifacts uses: actions/upload-artifact@v2 with: name: age-binaries path: age-* upload: name: Upload release binaries if: github.event_name == 'release' needs: build permissions: contents: write runs-on: ubuntu-latest steps: - name: Download workflow artifacts uses: actions/download-artifact@v2 with: name: age-binaries - name: Upload release artifacts run: gh release upload "$GITHUB_REF_NAME" age-* env: GH_REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} age-1.1.1/.github/workflows/certs/000077500000000000000000000000001435753552200167765ustar00rootroot00000000000000age-1.1.1/.github/workflows/certs/README000066400000000000000000000007371435753552200176650ustar00rootroot00000000000000In this folder there are uitacllc.crt PKCS#7 encoded certificate chain for a code signing certificate issued to Up in the Air Consulting LLC valid until Sep 26 23:59:59 2024 GMT. https://crt.sh/?id=5339775059 uitacllc.key PEM encrypted private key for the leaf certificate above. Its passphrase is long and randomly generated, so the awful legacy key derivation doesn't really matter, and it makes osslsigncode happy. age-1.1.1/.github/workflows/certs/uitacllc.crt000066400000000000000000000127451435753552200213210ustar00rootroot000000000000000 *H 010  *H 00S[Ffg!0  *H  0T1 0 UGB10U Sectigo Limited1+0)U"Sectigo Public Code Signing CA R360 210927000000Z 240926235959Z0n1 0 UUS10U New York1%0#U Up in the Air Consulting LLC1%0#U Up in the Air Consulting LLC00  *H 0<ЫT`074mYd37!0! pZs*wk?8BC= /j!\0kkm<Ύ{z2U9PnYCtʘZ)ypa2Ӭt90XsJ+6-u obզ6"ө00U#0* (oH+T) 0Uk;U~}C"i D_#l3 s0U0 U00U% 0 +0 `HB0JU C0A05 +10%0#+https://sectigo.com/CPS0g 0IUB0@0><:8http://crl.sectigo.com/SectigoPublicCodeSigningCAR36.crl0y+m0k0D+08http://crt.sectigo.com/SectigoPublicCodeSigningCAR36.crt0#+0http://ocsp.sectigo.com0  *H  xt S >+u+yAn00bm R;y ! 0  *H  0V1 0 UGB10U Sectigo Limited1-0+U$Sectigo Public Code Signing Root R460 210322000000Z 360321235959Z0T1 0 UGB10U Sectigo Limited1+0)U"Sectigo Public Code Signing CA R3600  *H 0+S5?`CjSSl~Xye3 FFlCx@PVN$y6cUb*j}aj$c/ѿv&1xR:ڛ]=-m#{Ub$r`q6ל3!%O>$EӖpcx?NѪeW~+(D̪` HiZsw^5hu9WB%$4RQ͑:qw,ܟ@h9o>iMS>ZMŤLnc~`DBܿ5![~W=S;kgIlGLv\~G8`F~@FYo[d0`0U#02뒚5H/(@Bp 6\0U* (oH+T) 0U0U00U% 0 +0U 00U 0g 0KUD0B0@><:http://crl.sectigo.com/SectigoPublicCodeSigningRootR46.crl0{+o0m0F+0:http://crt.sectigo.com/SectigoPublicCodeSigningRootR46.p7c0#+0http://ocsp.sectigo.com0  *H  wc6n{ uDaVqё?@@rd9}1QoRЇc$;}'keӥ iI9mn䠃'ښ +PK[d6~J=qas< 3W47]=v?fҔ1im4zxܒPdǪ-j/n!@ ٞ v' ֺ)p6.O2tFkG5w3Oc.-ekqm>,ugmcc6iE>nZ_[˝00@ 8gS0eoti>h;g3H@dFsleU[WxF|p@)\+EgO(^Lz>2F 6귪F : fX\) 08)00U2뒚5H/(@Bp 6\0U0U00U% 0 +0U 00U 0g 0CU<0:08642http://crl.comodoca.com/AAACertificateServices.crl04+(0&0$+0http://ocsp.comodoca.com0  *H  tDiF$ g:E}RN!9k^- y;SʹD\cg@Dr*2]@gB'r#8L FO&|fQ6TXl. S)oO ]ƋHR12L=l9s 9 hH86%wňLf}/cΘi߬<][/tϸ~׃bEGRU靌:bQ$)XE{Z0200  *H 0{1 0 UGB10U Greater Manchester10U Salford10U Comodo CA Limited1!0U AAA Certificate Services0 040101000000Z 281231235959Z0{1 0 UGB10U Greater Manchester10U Salford10U Comodo CA Limited1!0U AAA Certificate Services0"0  *H 0 @nvMEDFȃ*]P1 p"I-Tc̶nhF SL$rNT z3`ډU"XOhF'v5,^deHav PfxbV18'2Xok+c_s8x6Qx:B /I-߬tMG)b&{>%ݝ5h Ä ^/00U #>)00U0U00{Ut0r08642http://crl.comodoca.com/AAACertificateServices.crl06420http://crl.comodo.net/AAACertificateServices.crl0  *H V{DOX̦Ihv]`֍PO&N氥tTAOZ``J¿Ĺt-}kF/j4,}Z  /\:l7U S@lXenage@filippo.io

" } { print }' "$f" > "$f.tmp" mv "$f.tmp" "$f" done - name: Upload generated files uses: actions/upload-artifact@v3 with: name: man-pages path: | doc/*.1 doc/*.html commit: name: Commit changes needs: ronn permissions: contents: write runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Download generated files uses: actions/download-artifact@v2 with: name: man-pages path: doc/ - name: Commit and push if changed run: |- git config user.name "GitHub Actions" git config user.email "actions@users.noreply.github.com" git add doc/ git commit -m "doc: regenerate groff and html man pages" || exit 0 git push age-1.1.1/.github/workflows/ronn/000077500000000000000000000000001435753552200166325ustar00rootroot00000000000000age-1.1.1/.github/workflows/ronn/Dockerfile000066400000000000000000000003651435753552200206300ustar00rootroot00000000000000FROM ruby:3.0.1-buster RUN apt-get update && apt-get install -y groff RUN bundle config --global frozen 1 COPY Gemfile Gemfile.lock ./ RUN bundle install ENTRYPOINT ["bash", "-O", "globstar", "-c", \ "/usr/local/bundle/bin/ronn **/*.ronn"] age-1.1.1/.github/workflows/ronn/Gemfile000066400000000000000000000001251435753552200201230ustar00rootroot00000000000000# frozen_string_literal: true source "https://rubygems.org" gem "ronn", "~> 0.7.3" age-1.1.1/.github/workflows/ronn/Gemfile.lock000066400000000000000000000004571435753552200210620ustar00rootroot00000000000000GEM remote: https://rubygems.org/ specs: hpricot (0.8.6) mustache (1.1.1) rdiscount (2.2.0.2) ronn (0.7.3) hpricot (>= 0.8.2) mustache (>= 0.7.0) rdiscount (>= 1.5.8) PLATFORMS aarch64-linux x86_64-linux DEPENDENCIES ronn (~> 0.7.3) BUNDLED WITH 2.2.15 age-1.1.1/.github/workflows/ronn/action.yml000066400000000000000000000000651435753552200206330ustar00rootroot00000000000000name: Ronn runs: using: docker image: Dockerfile age-1.1.1/.github/workflows/test.yml000066400000000000000000000036671435753552200173740ustar00rootroot00000000000000name: Go tests on: [push, pull_request] permissions: contents: read jobs: test: name: Test strategy: fail-fast: false matrix: go: [1.18.x, 1.19.x] os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go ${{ matrix.go }} uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: Run tests run: go test -race ./... freebsd: name: Test (FreeBSD) runs-on: macos-10.15 steps: - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 0 - name: Run tests # Unpinned Action allowed with read-only permissions. uses: vmactions/freebsd-vm@v0 with: prepare: | freebsd-version pkg install -y go go version run: go test -buildvcs=false -race ./... gotip: name: Test (Go tip) strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: Install Go tip (UNIX) if: runner.os != 'Windows' run: | git clone --filter=tree:0 https://go.googlesource.com/go $HOME/gotip cd $HOME/gotip/src && ./make.bash echo "$HOME/gotip/bin" >> $GITHUB_PATH - name: Install Go tip (Windows) if: runner.os == 'Windows' run: | git clone --filter=tree:0 https://go.googlesource.com/go $HOME/gotip cd $HOME/gotip/src && ./make.bat echo "$HOME/gotip/bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Checkout repository uses: actions/checkout@v2 with: fetch-depth: 0 - run: go version - name: Run tests run: go test -race ./... age-1.1.1/AUTHORS000066400000000000000000000003101435753552200133230ustar00rootroot00000000000000# This is the official list of age authors for copyright purposes. # To be included, send a change adding the individual or company # who owns a contribution's copyright. Google LLC Filippo Valsorda age-1.1.1/LICENSE000066400000000000000000000026621435753552200132740ustar00rootroot00000000000000Copyright 2019 The age Authors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the age project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. age-1.1.1/README.md000066400000000000000000000215411435753552200135430ustar00rootroot00000000000000

The age logo, an wireframe of St. Peters dome in Rome, with the text: age, file encryption

[![Go Reference](https://pkg.go.dev/badge/filippo.io/age.svg)](https://pkg.go.dev/filippo.io/age) [![man page]()](https://filippo.io/age/age.1) [![C2SP specification](https://img.shields.io/badge/%C2%A7%23-specification-blueviolet)](https://age-encryption.org/v1) age is a simple, modern and secure file encryption tool, format, and Go library. It features small explicit keys, no config options, and UNIX-style composability. ``` $ age-keygen -o key.txt Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p $ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age $ age --decrypt -i key.txt data.tar.gz.age > data.tar.gz ``` 📜 The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile). 📬 Follow the maintenance of this project by subscribing to [Maintainer Dispatches](https://filippo.io/newsletter)! 🦀 An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage). 🔑 Hardware PIV tokens such as YubiKeys are supported through the [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) plugin. 💬 The author pronounces it `[aɡe̞]`, like the Italian [“aghe”](https://translate.google.com/?sl=it&text=aghe). ## Installation
Homebrew (macOS or Linux) brew install age
MacPorts port install age
Alpine Linux v3.15+ apk add age
Arch Linux pacman -S age
Debian 11+ (Bullseye) apt install age/bullseye-backports (enable backports for age v1.0.0+)
Fedora 33+ dnf install age
Gentoo Linux emerge app-crypt/age
NixOS / Nix nix-env -i age
openSUSE Tumbleweed zypper install age
Ubuntu 22.04+ apt install age
Void Linux xbps-install age
FreeBSD pkg install age (security/age)
OpenBSD 6.7+ pkg_add age (security/age)
Chocolatey (Windows) choco install age.portable
Scoop (Windows) scoop bucket add extras; scoop install age
On Windows, Linux, macOS, and FreeBSD you can use the pre-built binaries. ``` https://dl.filippo.io/age/latest?for=linux/amd64 https://dl.filippo.io/age/v1.0.0-rc.1?for=darwin/arm64 ... ``` If your system has [a supported version of Go](https://go.dev/dl/), you can build from source. ``` go install filippo.io/age/cmd/...@latest ``` Help from new packagers is very welcome. ## Usage For the full documentation, read [the age(1) man page](https://filippo.io/age/age.1). ``` Usage: age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT] age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT] age --decrypt [-i PATH]... [-o OUTPUT] [INPUT] Options: -e, --encrypt Encrypt the input to the output. Default if omitted. -d, --decrypt Decrypt the input to the output. -o, --output OUTPUT Write the result to the file at path OUTPUT. -a, --armor Encrypt to a PEM encoded format. -p, --passphrase Encrypt with a passphrase. -r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated. -R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated. -i, --identity PATH Use the identity file at PATH. Can be repeated. INPUT defaults to standard input, and OUTPUT defaults to standard output. If OUTPUT exists, it will be overwritten. RECIPIENT can be an age public key generated by age-keygen ("age1...") or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA..."). Recipient files contain one or more recipients, one per line. Empty lines and lines starting with "#" are ignored as comments. "-" may be used to read recipients from standard input. Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."), one per line, or an SSH key. Empty lines and lines starting with "#" are ignored as comments. Passphrase encrypted age files can be used as identity files. Multiple key files can be provided, and any unused ones will be ignored. "-" may be used to read identities from standard input. When --encrypt is specified explicitly, -i can also be used to encrypt to an identity file symmetrically, instead or in addition to normal recipients. ``` ### Multiple recipients Files can be encrypted to multiple recipients by repeating `-r/--recipient`. Every recipient will be able to decrypt the file. ``` $ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \ -r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg ``` #### Recipient files Multiple recipients can also be listed one per line in one or more files passed with the `-R/--recipients-file` flag. ``` $ cat recipients.txt # Alice age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p # Bob age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg $ age -R recipients.txt example.jpg > example.jpg.age ``` If the argument to `-R` (or `-i`) is `-`, the file is read from standard input. ### Passphrases Files 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. ``` $ age -p secrets.txt > secrets.txt.age Enter passphrase (leave empty to autogenerate a secure one): Using the autogenerated passphrase "release-response-step-brand-wrap-ankle-pair-unusual-sword-train". $ age -d secrets.txt.age > secrets.txt Enter passphrase: ``` ### Passphrase-protected key files If an identity file passed to `-i` is a passphrase encrypted age file, it will be automatically decrypted. ``` $ age-keygen | age -p > key.age Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 Enter passphrase (leave empty to autogenerate a secure one): Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress". $ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age $ age -d -i key.age secrets.txt.age > secrets.txt Enter passphrase for identity file "key.age": ``` Passphrase-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. ### SSH keys As 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.) ``` $ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age $ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg ``` Note 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. #### Encrypting to a GitHub user Combining SSH key support and `-R`, you can easily encrypt a file to the SSH keys listed on a GitHub profile. ``` $ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age ``` Keep 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. age-1.1.1/age.go000066400000000000000000000176461435753552200133620ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package age implements file encryption according to the age-encryption.org/v1 // specification. // // For most use cases, use the Encrypt and Decrypt functions with // X25519Recipient and X25519Identity. If passphrase encryption is required, use // ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys // use the filippo.io/age/agessh package. // // age encrypted files are binary and not malleable. For encoding them as text, // use the filippo.io/age/armor package. // // Key management // // age does not have a global keyring. Instead, since age keys are small, // textual, and cheap, you are encouraged to generate dedicated keys for each // task and application. // // Recipient public keys can be passed around as command line flags and in // config files, while secret keys should be stored in dedicated files, through // secret management systems, or as environment variables. // // There is no default path for age keys. Instead, they should be stored at // application-specific paths. The CLI supports files where private keys are // listed one per line, ignoring empty lines and lines starting with "#". These // files can be parsed with ParseIdentities. // // When integrating age into a new system, it's recommended that you only // support X25519 keys, and not SSH keys. The latter are supported for manual // encryption operations. If you need to tie into existing key management // infrastructure, you might want to consider implementing your own Recipient // and Identity. // // Backwards compatibility // // Files encrypted with a stable version (not alpha, beta, or release candidate) // of age, or with any v1.0.0 beta or release candidate, will decrypt with any // later versions of the v1 API. This might change in v2, in which case v1 will // be maintained with security fixes for compatibility with older files. // // If decrypting an older file poses a security risk, doing so might require an // explicit opt-in in the API. package age import ( "crypto/hmac" "crypto/rand" "errors" "fmt" "io" "filippo.io/age/internal/format" "filippo.io/age/internal/stream" ) // An Identity is passed to Decrypt to unwrap an opaque file key from a // recipient stanza. It can be for example a secret key like X25519Identity, a // plugin, or a custom implementation. // // Unwrap must return an error wrapping ErrIncorrectIdentity if none of the // recipient stanzas match the identity, any other error will be considered // fatal. // // Most age API users won't need to interact with this directly, and should // instead pass Recipient implementations to Encrypt and Identity // implementations to Decrypt. type Identity interface { Unwrap(stanzas []*Stanza) (fileKey []byte, err error) } var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block") // A Recipient is passed to Encrypt to wrap an opaque file key to one or more // recipient stanza(s). It can be for example a public key like X25519Recipient, // a plugin, or a custom implementation. // // Most age API users won't need to interact with this directly, and should // instead pass Recipient implementations to Encrypt and Identity // implementations to Decrypt. type Recipient interface { Wrap(fileKey []byte) ([]*Stanza, error) } // A Stanza is a section of the age header that encapsulates the file key as // encrypted to a specific recipient. // // Most age API users won't need to interact with this directly, and should // instead pass Recipient implementations to Encrypt and Identity // implementations to Decrypt. type Stanza struct { Type string Args []string Body []byte } const fileKeySize = 16 const streamNonceSize = 16 // Encrypt encrypts a file to one or more recipients. // // Writes to the returned WriteCloser are encrypted and written to dst as an age // file. Every recipient will be able to decrypt the file. // // The caller must call Close on the WriteCloser when done for the last chunk to // be encrypted and flushed to dst. func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) { if len(recipients) == 0 { return nil, errors.New("no recipients specified") } // As a best effort, prevent an API user from generating a file that the // ScryptIdentity will refuse to decrypt. This check can't unfortunately be // implemented as part of the Recipient interface, so it lives as a special // case in Encrypt. for _, r := range recipients { if _, ok := r.(*ScryptRecipient); ok && len(recipients) != 1 { return nil, errors.New("an ScryptRecipient must be the only one for the file") } } fileKey := make([]byte, fileKeySize) if _, err := rand.Read(fileKey); err != nil { return nil, err } hdr := &format.Header{} for i, r := range recipients { stanzas, err := r.Wrap(fileKey) if err != nil { return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err) } for _, s := range stanzas { hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s)) } } if mac, err := headerMAC(fileKey, hdr); err != nil { return nil, fmt.Errorf("failed to compute header MAC: %v", err) } else { hdr.MAC = mac } if err := hdr.Marshal(dst); err != nil { return nil, fmt.Errorf("failed to write header: %v", err) } nonce := make([]byte, streamNonceSize) if _, err := rand.Read(nonce); err != nil { return nil, err } if _, err := dst.Write(nonce); err != nil { return nil, fmt.Errorf("failed to write nonce: %v", err) } return stream.NewWriter(streamKey(fileKey, nonce), dst) } // NoIdentityMatchError is returned by Decrypt when none of the supplied // identities match the encrypted file. type NoIdentityMatchError struct { // Errors is a slice of all the errors returned to Decrypt by the Unwrap // calls it made. They all wrap ErrIncorrectIdentity. Errors []error } func (*NoIdentityMatchError) Error() string { return "no identity matched any of the recipients" } // Decrypt decrypts a file encrypted to one or more identities. // // It returns a Reader reading the decrypted plaintext of the age file read // from src. All identities will be tried until one successfully decrypts the file. func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) { if len(identities) == 0 { return nil, errors.New("no identities specified") } hdr, payload, err := format.Parse(src) if err != nil { return nil, fmt.Errorf("failed to read header: %w", err) } stanzas := make([]*Stanza, 0, len(hdr.Recipients)) for _, s := range hdr.Recipients { stanzas = append(stanzas, (*Stanza)(s)) } errNoMatch := &NoIdentityMatchError{} var fileKey []byte for _, id := range identities { fileKey, err = id.Unwrap(stanzas) if errors.Is(err, ErrIncorrectIdentity) { errNoMatch.Errors = append(errNoMatch.Errors, err) continue } if err != nil { return nil, err } break } if fileKey == nil { return nil, errNoMatch } if mac, err := headerMAC(fileKey, hdr); err != nil { return nil, fmt.Errorf("failed to compute header MAC: %v", err) } else if !hmac.Equal(mac, hdr.MAC) { return nil, errors.New("bad header MAC") } nonce := make([]byte, streamNonceSize) if _, err := io.ReadFull(payload, nonce); err != nil { return nil, fmt.Errorf("failed to read nonce: %w", err) } return stream.NewReader(streamKey(fileKey, nonce), payload) } // multiUnwrap is a helper that implements Identity.Unwrap in terms of a // function that unwraps a single recipient stanza. func multiUnwrap(unwrap func(*Stanza) ([]byte, error), stanzas []*Stanza) ([]byte, error) { for _, s := range stanzas { fileKey, err := unwrap(s) if errors.Is(err, ErrIncorrectIdentity) { // If we ever start returning something interesting wrapping // ErrIncorrectIdentity, we should let it make its way up through // Decrypt into NoIdentityMatchError.Errors. continue } if err != nil { return nil, err } return fileKey, nil } return nil, ErrIncorrectIdentity } age-1.1.1/age_test.go000066400000000000000000000124331435753552200144060ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package age_test import ( "bytes" "fmt" "io" "log" "os" "strings" "testing" "filippo.io/age" ) func ExampleEncrypt() { publicKey := "age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm" recipient, err := age.ParseX25519Recipient(publicKey) if err != nil { log.Fatalf("Failed to parse public key %q: %v", publicKey, err) } out := &bytes.Buffer{} w, err := age.Encrypt(out, recipient) if err != nil { log.Fatalf("Failed to create encrypted file: %v", err) } if _, err := io.WriteString(w, "Black lives matter."); err != nil { log.Fatalf("Failed to write to encrypted file: %v", err) } if err := w.Close(); err != nil { log.Fatalf("Failed to close encrypted file: %v", err) } fmt.Printf("Encrypted file size: %d\n", out.Len()) // Output: // Encrypted file size: 219 } // DO NOT hardcode the private key. Store it in a secret storage solution, // on disk if the local machine is trusted, or have the user provide it. var privateKey string func init() { privateKey = "AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU" } func ExampleDecrypt() { identity, err := age.ParseX25519Identity(privateKey) if err != nil { log.Fatalf("Failed to parse private key: %v", err) } f, err := os.Open("testdata/example.age") if err != nil { log.Fatalf("Failed to open file: %v", err) } r, err := age.Decrypt(f, identity) if err != nil { log.Fatalf("Failed to open encrypted file: %v", err) } out := &bytes.Buffer{} if _, err := io.Copy(out, r); err != nil { log.Fatalf("Failed to read encrypted file: %v", err) } fmt.Printf("File contents: %q\n", out.Bytes()) // Output: // File contents: "Black lives matter." } func ExampleParseIdentities() { keyFile, err := os.Open("testdata/example_keys.txt") if err != nil { log.Fatalf("Failed to open private keys file: %v", err) } identities, err := age.ParseIdentities(keyFile) if err != nil { log.Fatalf("Failed to parse private key: %v", err) } f, err := os.Open("testdata/example.age") if err != nil { log.Fatalf("Failed to open file: %v", err) } r, err := age.Decrypt(f, identities...) if err != nil { log.Fatalf("Failed to open encrypted file: %v", err) } out := &bytes.Buffer{} if _, err := io.Copy(out, r); err != nil { log.Fatalf("Failed to read encrypted file: %v", err) } fmt.Printf("File contents: %q\n", out.Bytes()) // Output: // File contents: "Black lives matter." } func ExampleGenerateX25519Identity() { identity, err := age.GenerateX25519Identity() if err != nil { log.Fatalf("Failed to generate key pair: %v", err) } fmt.Printf("Public key: %s...\n", identity.Recipient().String()[:4]) fmt.Printf("Private key: %s...\n", identity.String()[:16]) // Output: // Public key: age1... // Private key: AGE-SECRET-KEY-1... } const helloWorld = "Hello, Twitch!" func TestEncryptDecryptX25519(t *testing.T) { a, err := age.GenerateX25519Identity() if err != nil { t.Fatal(err) } b, err := age.GenerateX25519Identity() if err != nil { t.Fatal(err) } buf := &bytes.Buffer{} w, err := age.Encrypt(buf, a.Recipient(), b.Recipient()) if err != nil { t.Fatal(err) } if _, err := io.WriteString(w, helloWorld); err != nil { t.Fatal(err) } if err := w.Close(); err != nil { t.Fatal(err) } out, err := age.Decrypt(buf, b) if err != nil { t.Fatal(err) } outBytes, err := io.ReadAll(out) if err != nil { t.Fatal(err) } if string(outBytes) != helloWorld { t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld) } } func TestEncryptDecryptScrypt(t *testing.T) { password := "twitch.tv/filosottile" r, err := age.NewScryptRecipient(password) if err != nil { t.Fatal(err) } r.SetWorkFactor(15) buf := &bytes.Buffer{} w, err := age.Encrypt(buf, r) if err != nil { t.Fatal(err) } if _, err := io.WriteString(w, helloWorld); err != nil { t.Fatal(err) } if err := w.Close(); err != nil { t.Fatal(err) } i, err := age.NewScryptIdentity(password) if err != nil { t.Fatal(err) } out, err := age.Decrypt(buf, i) if err != nil { t.Fatal(err) } outBytes, err := io.ReadAll(out) if err != nil { t.Fatal(err) } if string(outBytes) != helloWorld { t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld) } } func TestParseIdentities(t *testing.T) { tests := []struct { name string wantCount int wantErr bool file string }{ {"valid", 2, false, ` # this is a comment # AGE-SECRET-KEY-1705XN76M8EYQ8M9PY4E2G3KA8DN7NSCGT3V4HMN20H3GCX4AS6HSSTG8D3 # AGE-SECRET-KEY-1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59QJ AGE-SECRET-KEY-19WUMFE89H3928FRJ5U3JYRNHM6CERQGKSQ584AQ8QY7T7R09D32SWE4DYH`}, {"invalid", 0, true, ` AGE-SECRET-KEY-1705XN76M8EYQ8M9PY4E2G3KA8DN7NSCGT3V4HMN20H3GCX4AS6HSSTG8D3 AGE-SECRET-KEY--1D6K0SGAX3NU66R4GYFZY0UQWCLM3UUSF3CXLW4KXZM342WQSJ82QKU59Q`}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := age.ParseIdentities(strings.NewReader(tt.file)) if (err != nil) != tt.wantErr { t.Errorf("ParseIdentities() error = %v, wantErr %v", err, tt.wantErr) return } if len(got) != tt.wantCount { t.Errorf("ParseIdentities() returned %d identities, want %d", len(got), tt.wantCount) } }) } } age-1.1.1/agessh/000077500000000000000000000000001435753552200135335ustar00rootroot00000000000000age-1.1.1/agessh/agessh.go000066400000000000000000000247231435753552200153440ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package agessh provides age.Identity and age.Recipient implementations of // types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH keys for // encryption with age-encryption.org/v1. // // These recipient types should only be used for compatibility with existing // keys, and native X25519 keys should be preferred otherwise. // // Note that these recipient types are not anonymous: the encrypted message will // include a short 32-bit ID of the public key. package agessh import ( "crypto/ed25519" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/sha512" "errors" "fmt" "io" "filippo.io/age" "filippo.io/age/internal/format" "filippo.io/edwards25519" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/hkdf" "golang.org/x/crypto/ssh" ) func sshFingerprint(pk ssh.PublicKey) string { h := sha256.Sum256(pk.Marshal()) return format.EncodeToString(h[:4]) } const oaepLabel = "age-encryption.org/v1/ssh-rsa" type RSARecipient struct { sshKey ssh.PublicKey pubKey *rsa.PublicKey } var _ age.Recipient = &RSARecipient{} func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) { if pk.Type() != "ssh-rsa" { return nil, errors.New("SSH public key is not an RSA key") } r := &RSARecipient{ sshKey: pk, } if pk, ok := pk.(ssh.CryptoPublicKey); ok { if pk, ok := pk.CryptoPublicKey().(*rsa.PublicKey); ok { r.pubKey = pk } else { return nil, errors.New("unexpected public key type") } } else { return nil, errors.New("pk does not implement ssh.CryptoPublicKey") } if r.pubKey.Size() < 2048/8 { return nil, errors.New("RSA key size is too small") } return r, nil } func (r *RSARecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { l := &age.Stanza{ Type: "ssh-rsa", Args: []string{sshFingerprint(r.sshKey)}, } wrappedKey, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, r.pubKey, fileKey, []byte(oaepLabel)) if err != nil { return nil, err } l.Body = wrappedKey return []*age.Stanza{l}, nil } type RSAIdentity struct { k *rsa.PrivateKey sshKey ssh.PublicKey } var _ age.Identity = &RSAIdentity{} func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) { s, err := ssh.NewSignerFromKey(key) if err != nil { return nil, err } i := &RSAIdentity{ k: key, sshKey: s.PublicKey(), } return i, nil } func (i *RSAIdentity) Recipient() *RSARecipient { return &RSARecipient{ sshKey: i.sshKey, pubKey: &i.k.PublicKey, } } func (i *RSAIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) { return multiUnwrap(i.unwrap, stanzas) } func (i *RSAIdentity) unwrap(block *age.Stanza) ([]byte, error) { if block.Type != "ssh-rsa" { return nil, age.ErrIncorrectIdentity } if len(block.Args) != 1 { return nil, errors.New("invalid ssh-rsa recipient block") } if block.Args[0] != sshFingerprint(i.sshKey) { return nil, age.ErrIncorrectIdentity } fileKey, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, i.k, block.Body, []byte(oaepLabel)) if err != nil { return nil, fmt.Errorf("failed to decrypt file key: %v", err) } return fileKey, nil } type Ed25519Recipient struct { sshKey ssh.PublicKey theirPublicKey []byte } var _ age.Recipient = &Ed25519Recipient{} func NewEd25519Recipient(pk ssh.PublicKey) (*Ed25519Recipient, error) { if pk.Type() != "ssh-ed25519" { return nil, errors.New("SSH public key is not an Ed25519 key") } cpk, ok := pk.(ssh.CryptoPublicKey) if !ok { return nil, errors.New("pk does not implement ssh.CryptoPublicKey") } epk, ok := cpk.CryptoPublicKey().(ed25519.PublicKey) if !ok { return nil, errors.New("unexpected public key type") } mpk, err := ed25519PublicKeyToCurve25519(epk) if err != nil { return nil, fmt.Errorf("invalid Ed25519 public key: %v", err) } return &Ed25519Recipient{ sshKey: pk, theirPublicKey: mpk, }, nil } func ParseRecipient(s string) (age.Recipient, error) { pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(s)) if err != nil { return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err) } var r age.Recipient switch t := pubKey.Type(); t { case "ssh-rsa": r, err = NewRSARecipient(pubKey) case "ssh-ed25519": r, err = NewEd25519Recipient(pubKey) default: return nil, fmt.Errorf("unknown SSH recipient type: %q", t) } if err != nil { return nil, fmt.Errorf("malformed SSH recipient: %q: %v", s, err) } return r, nil } func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) { // See https://blog.filippo.io/using-ed25519-keys-for-encryption and // https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery. p, err := new(edwards25519.Point).SetBytes(pk) if err != nil { return nil, err } return p.BytesMontgomery(), nil } const ed25519Label = "age-encryption.org/v1/ssh-ed25519" func (r *Ed25519Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) { ephemeral := make([]byte, curve25519.ScalarSize) if _, err := rand.Read(ephemeral); err != nil { return nil, err } ourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint) if err != nil { return nil, err } sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey) if err != nil { return nil, err } tweak := make([]byte, curve25519.ScalarSize) tH := hkdf.New(sha256.New, nil, r.sshKey.Marshal(), []byte(ed25519Label)) if _, err := io.ReadFull(tH, tweak); err != nil { return nil, err } sharedSecret, _ = curve25519.X25519(tweak, sharedSecret) l := &age.Stanza{ Type: "ssh-ed25519", Args: []string{sshFingerprint(r.sshKey), format.EncodeToString(ourPublicKey[:])}, } salt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey)) salt = append(salt, ourPublicKey...) salt = append(salt, r.theirPublicKey...) h := hkdf.New(sha256.New, sharedSecret, salt, []byte(ed25519Label)) wrappingKey := make([]byte, chacha20poly1305.KeySize) if _, err := io.ReadFull(h, wrappingKey); err != nil { return nil, err } wrappedKey, err := aeadEncrypt(wrappingKey, fileKey) if err != nil { return nil, err } l.Body = wrappedKey return []*age.Stanza{l}, nil } type Ed25519Identity struct { secretKey, ourPublicKey []byte sshKey ssh.PublicKey } var _ age.Identity = &Ed25519Identity{} func NewEd25519Identity(key ed25519.PrivateKey) (*Ed25519Identity, error) { s, err := ssh.NewSignerFromKey(key) if err != nil { return nil, err } i := &Ed25519Identity{ sshKey: s.PublicKey(), secretKey: ed25519PrivateKeyToCurve25519(key), } i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint) return i, nil } func ParseIdentity(pemBytes []byte) (age.Identity, error) { k, err := ssh.ParseRawPrivateKey(pemBytes) if err != nil { return nil, err } switch k := k.(type) { case *ed25519.PrivateKey: return NewEd25519Identity(*k) // ParseRawPrivateKey returns inconsistent types. See Issue 429. case ed25519.PrivateKey: return NewEd25519Identity(k) case *rsa.PrivateKey: return NewRSAIdentity(k) } return nil, fmt.Errorf("unsupported SSH identity type: %T", k) } func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte { h := sha512.New() h.Write(pk.Seed()) out := h.Sum(nil) return out[:curve25519.ScalarSize] } func (i *Ed25519Identity) Recipient() *Ed25519Recipient { return &Ed25519Recipient{ sshKey: i.sshKey, theirPublicKey: i.ourPublicKey, } } func (i *Ed25519Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) { return multiUnwrap(i.unwrap, stanzas) } func (i *Ed25519Identity) unwrap(block *age.Stanza) ([]byte, error) { if block.Type != "ssh-ed25519" { return nil, age.ErrIncorrectIdentity } if len(block.Args) != 2 { return nil, errors.New("invalid ssh-ed25519 recipient block") } publicKey, err := format.DecodeString(block.Args[1]) if err != nil { return nil, fmt.Errorf("failed to parse ssh-ed25519 recipient: %v", err) } if len(publicKey) != curve25519.PointSize { return nil, errors.New("invalid ssh-ed25519 recipient block") } if block.Args[0] != sshFingerprint(i.sshKey) { return nil, age.ErrIncorrectIdentity } sharedSecret, err := curve25519.X25519(i.secretKey, publicKey) if err != nil { return nil, fmt.Errorf("invalid X25519 recipient: %v", err) } tweak := make([]byte, curve25519.ScalarSize) tH := hkdf.New(sha256.New, nil, i.sshKey.Marshal(), []byte(ed25519Label)) if _, err := io.ReadFull(tH, tweak); err != nil { return nil, err } sharedSecret, _ = curve25519.X25519(tweak, sharedSecret) salt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey)) salt = append(salt, publicKey...) salt = append(salt, i.ourPublicKey...) h := hkdf.New(sha256.New, sharedSecret, salt, []byte(ed25519Label)) wrappingKey := make([]byte, chacha20poly1305.KeySize) if _, err := io.ReadFull(h, wrappingKey); err != nil { return nil, err } fileKey, err := aeadDecrypt(wrappingKey, block.Body) if err != nil { return nil, fmt.Errorf("failed to decrypt file key: %v", err) } return fileKey, nil } // multiUnwrap is copied from package age. It's a helper that implements // Identity.Unwrap in terms of a function that unwraps a single recipient // stanza. func multiUnwrap(unwrap func(*age.Stanza) ([]byte, error), stanzas []*age.Stanza) ([]byte, error) { for _, s := range stanzas { fileKey, err := unwrap(s) if errors.Is(err, age.ErrIncorrectIdentity) { // If we ever start returning something interesting wrapping // ErrIncorrectIdentity, we should let it make its way up through // Decrypt into NoIdentityMatchError.Errors. continue } if err != nil { return nil, err } return fileKey, nil } return nil, age.ErrIncorrectIdentity } // aeadEncrypt and aeadDecrypt are copied from package age. // // They don't limit the file key size because multi-key attacks are irrelevant // against the ssh-ed25519 recipient. Being an asymmetric recipient, it would // only allow a more efficient search for accepted public keys against a // decryption oracle, but the ssh-X recipients are not anonymous (they have a // short recipient hash). func aeadEncrypt(key, plaintext []byte) ([]byte, error) { aead, err := chacha20poly1305.New(key) if err != nil { return nil, err } nonce := make([]byte, chacha20poly1305.NonceSize) return aead.Seal(nil, nonce, plaintext, nil), nil } func aeadDecrypt(key, ciphertext []byte) ([]byte, error) { aead, err := chacha20poly1305.New(key) if err != nil { return nil, err } nonce := make([]byte, chacha20poly1305.NonceSize) return aead.Open(nil, nonce, ciphertext, nil) } age-1.1.1/agessh/agessh_test.go000066400000000000000000000037211435753552200163760ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package agessh_test import ( "bytes" "crypto/ed25519" "crypto/rand" "crypto/rsa" "reflect" "testing" "filippo.io/age/agessh" "golang.org/x/crypto/ssh" ) func TestSSHRSARoundTrip(t *testing.T) { pk, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatal(err) } pub, err := ssh.NewPublicKey(&pk.PublicKey) if err != nil { t.Fatal(err) } r, err := agessh.NewRSARecipient(pub) if err != nil { t.Fatal(err) } i, err := agessh.NewRSAIdentity(pk) if err != nil { t.Fatal(err) } // TODO: replace this with (and go-diff) with go-cmp. if !reflect.DeepEqual(r, i.Recipient()) { t.Fatalf("i.Recipient is different from r") } fileKey := make([]byte, 16) if _, err := rand.Read(fileKey); err != nil { t.Fatal(err) } stanzas, err := r.Wrap(fileKey) if err != nil { t.Fatal(err) } out, err := i.Unwrap(stanzas) if err != nil { t.Fatal(err) } if !bytes.Equal(fileKey, out) { t.Errorf("invalid output: %x, expected %x", out, fileKey) } } func TestSSHEd25519RoundTrip(t *testing.T) { pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { t.Fatal(err) } sshPubKey, err := ssh.NewPublicKey(pub) if err != nil { t.Fatal(err) } r, err := agessh.NewEd25519Recipient(sshPubKey) if err != nil { t.Fatal(err) } i, err := agessh.NewEd25519Identity(priv) if err != nil { t.Fatal(err) } // TODO: replace this with (and go-diff) with go-cmp. if !reflect.DeepEqual(r, i.Recipient()) { t.Fatalf("i.Recipient is different from r") } fileKey := make([]byte, 16) if _, err := rand.Read(fileKey); err != nil { t.Fatal(err) } stanzas, err := r.Wrap(fileKey) if err != nil { t.Fatal(err) } out, err := i.Unwrap(stanzas) if err != nil { t.Fatal(err) } if !bytes.Equal(fileKey, out) { t.Errorf("invalid output: %x, expected %x", out, fileKey) } } age-1.1.1/agessh/encrypted_keys.go000066400000000000000000000074761435753552200171300ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package agessh import ( "crypto" "crypto/ed25519" "crypto/rsa" "fmt" "filippo.io/age" "golang.org/x/crypto/ssh" ) // EncryptedSSHIdentity is an age.Identity implementation based on a passphrase // encrypted SSH private key. // // It requests the passphrase only if the public key matches a recipient stanza. // If the application knows it will always have to decrypt the private key, it // would be simpler to use ssh.ParseRawPrivateKeyWithPassphrase directly and // pass the result to NewEd25519Identity or NewRSAIdentity. type EncryptedSSHIdentity struct { pubKey ssh.PublicKey recipient age.Recipient pemBytes []byte passphrase func() ([]byte, error) decrypted age.Identity } // NewEncryptedSSHIdentity returns a new EncryptedSSHIdentity. // // pubKey must be the public key associated with the encrypted private key, and // it must have type "ssh-ed25519" or "ssh-rsa". For OpenSSH encrypted files it // can be extracted from an ssh.PassphraseMissingError, otherwise it can often // be found in ".pub" files. // // pemBytes must be a valid input to ssh.ParseRawPrivateKeyWithPassphrase. // passphrase is a callback that will be invoked by Unwrap when the passphrase // is necessary. func NewEncryptedSSHIdentity(pubKey ssh.PublicKey, pemBytes []byte, passphrase func() ([]byte, error)) (*EncryptedSSHIdentity, error) { i := &EncryptedSSHIdentity{ pubKey: pubKey, pemBytes: pemBytes, passphrase: passphrase, } switch t := pubKey.Type(); t { case "ssh-ed25519": r, err := NewEd25519Recipient(pubKey) if err != nil { return nil, err } i.recipient = r case "ssh-rsa": r, err := NewRSARecipient(pubKey) if err != nil { return nil, err } i.recipient = r default: return nil, fmt.Errorf("unsupported SSH key type: %v", t) } return i, nil } var _ age.Identity = &EncryptedSSHIdentity{} func (i *EncryptedSSHIdentity) Recipient() age.Recipient { return i.recipient } // Unwrap implements age.Identity. If the private key is still encrypted, and // any of the stanzas match the public key, it will request the passphrase. The // decrypted private key will be cached after the first successful invocation. func (i *EncryptedSSHIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { if i.decrypted != nil { return i.decrypted.Unwrap(stanzas) } var match bool for _, s := range stanzas { if s.Type != i.pubKey.Type() { continue } if len(s.Args) < 1 { return nil, fmt.Errorf("invalid %v recipient block", i.pubKey.Type()) } if s.Args[0] != sshFingerprint(i.pubKey) { continue } match = true break } if !match { return nil, age.ErrIncorrectIdentity } passphrase, err := i.passphrase() if err != nil { return nil, fmt.Errorf("failed to obtain passphrase: %v", err) } k, err := ssh.ParseRawPrivateKeyWithPassphrase(i.pemBytes, passphrase) if err != nil { return nil, fmt.Errorf("failed to decrypt SSH key file: %v", err) } var pubKey interface { Equal(x crypto.PublicKey) bool } switch k := k.(type) { case *ed25519.PrivateKey: i.decrypted, err = NewEd25519Identity(*k) pubKey = k.Public().(ed25519.PublicKey) // ParseRawPrivateKey returns inconsistent types. See Issue 429. case ed25519.PrivateKey: i.decrypted, err = NewEd25519Identity(k) pubKey = k.Public().(ed25519.PublicKey) case *rsa.PrivateKey: i.decrypted, err = NewRSAIdentity(k) pubKey = &k.PublicKey default: return nil, fmt.Errorf("unexpected SSH key type: %T", k) } if err != nil { return nil, fmt.Errorf("invalid SSH key: %v", err) } if exp := i.pubKey.(ssh.CryptoPublicKey).CryptoPublicKey(); !pubKey.Equal(exp) { return nil, fmt.Errorf("mismatched private and public SSH key") } return i.decrypted.Unwrap(stanzas) } age-1.1.1/armor/000077500000000000000000000000001435753552200134015ustar00rootroot00000000000000age-1.1.1/armor/armor.go000066400000000000000000000100001435753552200150370ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package armor provides a strict, streaming implementation of the ASCII // armoring format for age files. // // It's PEM with type "AGE ENCRYPTED FILE", 64 character columns, no headers, // and strict base64 decoding. package armor import ( "bufio" "bytes" "encoding/base64" "errors" "fmt" "io" "filippo.io/age/internal/format" ) const ( Header = "-----BEGIN AGE ENCRYPTED FILE-----" Footer = "-----END AGE ENCRYPTED FILE-----" ) type armoredWriter struct { started, closed bool encoder *format.WrappedBase64Encoder dst io.Writer } func (a *armoredWriter) Write(p []byte) (int, error) { if !a.started { if _, err := io.WriteString(a.dst, Header+"\n"); err != nil { return 0, err } } a.started = true return a.encoder.Write(p) } func (a *armoredWriter) Close() error { if a.closed { return errors.New("ArmoredWriter already closed") } a.closed = true if err := a.encoder.Close(); err != nil { return err } footer := Footer + "\n" if !a.encoder.LastLineIsEmpty() { footer = "\n" + footer } _, err := io.WriteString(a.dst, footer) return err } func NewWriter(dst io.Writer) io.WriteCloser { // TODO: write a test with aligned and misaligned sizes, and 8 and 10 steps. return &armoredWriter{ dst: dst, encoder: format.NewWrappedBase64Encoder(base64.StdEncoding, dst), } } type armoredReader struct { r *bufio.Reader started bool unread []byte // backed by buf buf [format.BytesPerLine]byte err error } func NewReader(r io.Reader) io.Reader { return &armoredReader{r: bufio.NewReader(r)} } func (r *armoredReader) Read(p []byte) (int, error) { if len(r.unread) > 0 { n := copy(p, r.unread) r.unread = r.unread[n:] return n, nil } if r.err != nil { return 0, r.err } getLine := func() ([]byte, error) { line, err := r.r.ReadBytes('\n') if err == io.EOF && len(line) == 0 { return nil, io.ErrUnexpectedEOF } else if err != nil && err != io.EOF { return nil, err } line = bytes.TrimSuffix(line, []byte("\n")) line = bytes.TrimSuffix(line, []byte("\r")) return line, nil } const maxWhitespace = 1024 drainTrailing := func() error { buf, err := io.ReadAll(io.LimitReader(r.r, maxWhitespace)) if err != nil { return err } if len(bytes.TrimSpace(buf)) != 0 { return errors.New("trailing data after armored file") } if len(buf) == maxWhitespace { return errors.New("too much trailing whitespace") } return io.EOF } var removedWhitespace int for !r.started { line, err := getLine() if err != nil { return 0, r.setErr(err) } // Ignore leading whitespace. if len(bytes.TrimSpace(line)) == 0 { removedWhitespace += len(line) + 1 if removedWhitespace > maxWhitespace { return 0, r.setErr(errors.New("too much leading whitespace")) } continue } if string(line) != Header { return 0, r.setErr(fmt.Errorf("invalid first line: %q", line)) } r.started = true } line, err := getLine() if err != nil { return 0, r.setErr(err) } if string(line) == Footer { return 0, r.setErr(drainTrailing()) } if len(line) > format.ColumnsPerLine { return 0, r.setErr(errors.New("column limit exceeded")) } r.unread = r.buf[:] n, err := base64.StdEncoding.Strict().Decode(r.unread, line) if err != nil { return 0, r.setErr(err) } r.unread = r.unread[:n] if n < format.BytesPerLine { line, err := getLine() if err != nil { return 0, r.setErr(err) } if string(line) != Footer { return 0, r.setErr(fmt.Errorf("invalid closing line: %q", line)) } r.setErr(drainTrailing()) } nn := copy(p, r.unread) r.unread = r.unread[nn:] return nn, nil } type Error struct { err error } func (e *Error) Error() string { return "invalid armor: " + e.err.Error() } func (e *Error) Unwrap() error { return e.err } func (r *armoredReader) setErr(err error) error { if err != io.EOF { err = &Error{err} } r.err = err return err } age-1.1.1/armor/armor_test.go000066400000000000000000000110431435753552200161060ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build go1.18 // +build go1.18 package armor_test import ( "bytes" "crypto/rand" "encoding/pem" "fmt" "io" "log" "os" "path/filepath" "strings" "testing" "filippo.io/age" "filippo.io/age/armor" "filippo.io/age/internal/format" ) func ExampleNewWriter() { publicKey := "age1cy0su9fwf3gf9mw868g5yut09p6nytfmmnktexz2ya5uqg9vl9sss4euqm" recipient, err := age.ParseX25519Recipient(publicKey) if err != nil { log.Fatalf("Failed to parse public key %q: %v", publicKey, err) } buf := &bytes.Buffer{} armorWriter := armor.NewWriter(buf) w, err := age.Encrypt(armorWriter, recipient) if err != nil { log.Fatalf("Failed to create encrypted file: %v", err) } if _, err := io.WriteString(w, "Black lives matter."); err != nil { log.Fatalf("Failed to write to encrypted file: %v", err) } if err := w.Close(); err != nil { log.Fatalf("Failed to close encrypted file: %v", err) } if err := armorWriter.Close(); err != nil { log.Fatalf("Failed to close armor: %v", err) } fmt.Printf("%s[...]", buf.Bytes()[:35]) // Output: // -----BEGIN AGE ENCRYPTED FILE----- // [...] } var privateKey = "AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU" func ExampleNewReader() { fileContents := `-----BEGIN AGE ENCRYPTED FILE----- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4YWdhZHZ0WG1PZldDT1hD K3RPRzFkUlJnWlFBQlUwemtjeXFRMFp6V1VFCnRzZFV3a3Vkd1dSUWw2eEtrRkVv SHcvZnp6Q3lqLy9HMkM4ZjUyUGdDZjQKLS0tIDlpVUpuVUQ5YUJyUENFZ0lNSTB2 ekUvS3E5WjVUN0F5ZWR1ejhpeU5rZUUKsvPGYt7vf0o1kyJ1eVFMz1e4JnYYk1y1 kB/RRusYjn+KVJ+KTioxj0THtzZPXcjFKuQ1 -----END AGE ENCRYPTED FILE-----` // DO NOT hardcode the private key. Store it in a secret storage solution, // on disk if the local machine is trusted, or have the user provide it. identity, err := age.ParseX25519Identity(privateKey) if err != nil { log.Fatalf("Failed to parse private key %q: %v", privateKey, err) } out := &bytes.Buffer{} f := strings.NewReader(fileContents) armorReader := armor.NewReader(f) r, err := age.Decrypt(armorReader, identity) if err != nil { log.Fatalf("Failed to open encrypted file: %v", err) } if _, err := io.Copy(out, r); err != nil { log.Fatalf("Failed to read encrypted file: %v", err) } fmt.Printf("File contents: %q\n", out.Bytes()) // Output: // File contents: "Black lives matter." } func TestArmor(t *testing.T) { t.Run("PartialLine", func(t *testing.T) { testArmor(t, 611) }) t.Run("FullLine", func(t *testing.T) { testArmor(t, 10*format.BytesPerLine) }) } func testArmor(t *testing.T, size int) { buf := &bytes.Buffer{} w := armor.NewWriter(buf) plain := make([]byte, size) rand.Read(plain) if _, err := w.Write(plain); err != nil { t.Fatal(err) } if err := w.Close(); err != nil { t.Fatal(err) } block, _ := pem.Decode(buf.Bytes()) if block == nil { t.Fatal("PEM decoding failed") } if len(block.Headers) != 0 { t.Error("unexpected headers") } if block.Type != "AGE ENCRYPTED FILE" { t.Errorf("unexpected type %q", block.Type) } if !bytes.Equal(block.Bytes, plain) { t.Error("PEM decoded value doesn't match") } if !bytes.Equal(buf.Bytes(), pem.EncodeToMemory(block)) { t.Error("PEM re-encoded value doesn't match") } r := armor.NewReader(buf) out, err := io.ReadAll(r) if err != nil { t.Fatal(err) } if !bytes.Equal(out, plain) { t.Error("decoded value doesn't match") } } func FuzzMalleability(f *testing.F) { tests, err := filepath.Glob("../testdata/testkit/*") if err != nil { f.Fatal(err) } for _, test := range tests { contents, err := os.ReadFile(test) if err != nil { f.Fatal(err) } header, contents, ok := bytes.Cut(contents, []byte("\n\n")) if !ok { f.Fatal("testkit file without header") } if bytes.Contains(header, []byte("armored: yes")) { f.Add(contents) } } f.Fuzz(func(t *testing.T, data []byte) { r := armor.NewReader(bytes.NewReader(data)) content, err := io.ReadAll(r) if err != nil { if _, ok := err.(*armor.Error); !ok { t.Errorf("error type is %T: %v", err, err) } t.Skip() } buf := &bytes.Buffer{} w := armor.NewWriter(buf) if _, err := w.Write(content); err != nil { t.Fatal(err) } if err := w.Close(); err != nil { t.Fatal(err) } if !bytes.Equal(normalize(buf.Bytes()), normalize(data)) { t.Error("re-encoded output different from input") } }) } func normalize(f []byte) []byte { f = bytes.TrimSpace(f) f = bytes.Replace(f, []byte("\r\n"), []byte("\n"), -1) return f } age-1.1.1/cmd/000077500000000000000000000000001435753552200130245ustar00rootroot00000000000000age-1.1.1/cmd/age-keygen/000077500000000000000000000000001435753552200150405ustar00rootroot00000000000000age-1.1.1/cmd/age-keygen/keygen.go000066400000000000000000000103101435753552200166440ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "flag" "fmt" "io" "log" "os" "runtime/debug" "time" "filippo.io/age" "golang.org/x/term" ) const usage = `Usage: age-keygen [-o OUTPUT] age-keygen -y [-o OUTPUT] [INPUT] Options: -o, --output OUTPUT Write the result to the file at path OUTPUT. -y Convert an identity file to a recipients file. age-keygen generates a new native X25519 key pair, and outputs it to standard output or to the OUTPUT file. If an OUTPUT file is specified, the public key is printed to standard error. If OUTPUT already exists, it is not overwritten. In -y mode, age-keygen reads an identity file from INPUT or from standard input and writes the corresponding recipient(s) to OUTPUT or to standard output, one per line, with no comments. Examples: $ age-keygen # created: 2021-01-02T15:30:45+01:00 # public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9 $ age-keygen -o key.txt Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p $ age-keygen -y key.txt age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p` // Version can be set at link time to override debug.BuildInfo.Main.Version, // which is "(devel)" when building from within the module. See // golang.org/issue/29814 and golang.org/issue/29228. var Version string func main() { log.SetFlags(0) flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) } var ( versionFlag, convertFlag bool outFlag string ) flag.BoolVar(&versionFlag, "version", false, "print the version") flag.BoolVar(&convertFlag, "y", false, "convert identities to recipients") flag.StringVar(&outFlag, "o", "", "output to `FILE` (default stdout)") flag.StringVar(&outFlag, "output", "", "output to `FILE` (default stdout)") flag.Parse() if len(flag.Args()) != 0 && !convertFlag { errorf("too many arguments") } if len(flag.Args()) > 1 && convertFlag { errorf("too many arguments") } if versionFlag { if Version != "" { fmt.Println(Version) return } if buildInfo, ok := debug.ReadBuildInfo(); ok { fmt.Println(buildInfo.Main.Version) return } fmt.Println("(unknown)") return } out := os.Stdout if outFlag != "" { f, err := os.OpenFile(outFlag, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) if err != nil { errorf("failed to open output file %q: %v", outFlag, err) } defer func() { if err := f.Close(); err != nil { errorf("failed to close output file %q: %v", outFlag, err) } }() out = f } in := os.Stdin if inFile := flag.Arg(0); inFile != "" && inFile != "-" { f, err := os.Open(inFile) if err != nil { errorf("failed to open input file %q: %v", inFile, err) } defer f.Close() in = f } if convertFlag { convert(in, out) } else { if fi, err := out.Stat(); err == nil && fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 { warning("writing secret key to a world-readable file") } generate(out) } } func generate(out *os.File) { k, err := age.GenerateX25519Identity() if err != nil { errorf("internal error: %v", err) } if !term.IsTerminal(int(out.Fd())) { fmt.Fprintf(os.Stderr, "Public key: %s\n", k.Recipient()) } fmt.Fprintf(out, "# created: %s\n", time.Now().Format(time.RFC3339)) fmt.Fprintf(out, "# public key: %s\n", k.Recipient()) fmt.Fprintf(out, "%s\n", k) } func convert(in io.Reader, out io.Writer) { ids, err := age.ParseIdentities(in) if err != nil { errorf("failed to parse input: %v", err) } if len(ids) == 0 { errorf("no identities found in the input") } for _, id := range ids { id, ok := id.(*age.X25519Identity) if !ok { errorf("internal error: unexpected identity type: %T", id) } fmt.Fprintf(out, "%s\n", id.Recipient()) } } func errorf(format string, v ...interface{}) { log.Printf("age-keygen: error: "+format, v...) log.Fatalf("age-keygen: report unexpected or unhelpful errors at https://filippo.io/age/report") } func warning(msg string) { log.Printf("age-keygen: warning: " + msg) } age-1.1.1/cmd/age/000077500000000000000000000000001435753552200135605ustar00rootroot00000000000000age-1.1.1/cmd/age/age.go000066400000000000000000000406011435753552200146440ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bufio" "bytes" "flag" "fmt" "io" "os" "regexp" "runtime/debug" "strings" "filippo.io/age" "filippo.io/age/agessh" "filippo.io/age/armor" "filippo.io/age/internal/plugin" "golang.org/x/term" ) const usage = `Usage: age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT] age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT] age --decrypt [-i PATH]... [-o OUTPUT] [INPUT] Options: -e, --encrypt Encrypt the input to the output. Default if omitted. -d, --decrypt Decrypt the input to the output. -o, --output OUTPUT Write the result to the file at path OUTPUT. -a, --armor Encrypt to a PEM encoded format. -p, --passphrase Encrypt with a passphrase. -r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated. -R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated. -i, --identity PATH Use the identity file at PATH. Can be repeated. INPUT defaults to standard input, and OUTPUT defaults to standard output. If OUTPUT exists, it will be overwritten. RECIPIENT can be an age public key generated by age-keygen ("age1...") or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA..."). Recipient files contain one or more recipients, one per line. Empty lines and lines starting with "#" are ignored as comments. "-" may be used to read recipients from standard input. Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."), one per line, or an SSH key. Empty lines and lines starting with "#" are ignored as comments. Passphrase encrypted age files can be used as identity files. Multiple key files can be provided, and any unused ones will be ignored. "-" may be used to read identities from standard input. When --encrypt is specified explicitly, -i can also be used to encrypt to an identity file symmetrically, instead or in addition to normal recipients. Example: $ age-keygen -o key.txt Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p $ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age $ age --decrypt -i key.txt -o data.tar.gz data.tar.gz.age` // Version can be set at link time to override debug.BuildInfo.Main.Version, // which is "(devel)" when building from within the module. See // golang.org/issue/29814 and golang.org/issue/29228. var Version string // stdinInUse is used to ensure only one of input, recipients, or identities // file is read from stdin. It's a singleton like os.Stdin. var stdinInUse bool type multiFlag []string func (f *multiFlag) String() string { return fmt.Sprint(*f) } func (f *multiFlag) Set(value string) error { *f = append(*f, value) return nil } type identityFlag struct { Type, Value string } // identityFlags tracks -i and -j flags, preserving their relative order, so // that "age -d -j agent -i encrypted-fallback-keys.age" behaves as expected. type identityFlags []identityFlag func (f *identityFlags) addIdentityFlag(value string) error { *f = append(*f, identityFlag{Type: "i", Value: value}) return nil } func (f *identityFlags) addPluginFlag(value string) error { *f = append(*f, identityFlag{Type: "j", Value: value}) return nil } func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) } if len(os.Args) == 1 { flag.Usage() exit(1) } var ( outFlag string decryptFlag, encryptFlag bool passFlag, versionFlag, armorFlag bool recipientFlags multiFlag recipientsFileFlags multiFlag identityFlags identityFlags ) flag.BoolVar(&versionFlag, "version", false, "print the version") flag.BoolVar(&decryptFlag, "d", false, "decrypt the input") flag.BoolVar(&decryptFlag, "decrypt", false, "decrypt the input") flag.BoolVar(&encryptFlag, "e", false, "encrypt the input") flag.BoolVar(&encryptFlag, "encrypt", false, "encrypt the input") flag.BoolVar(&passFlag, "p", false, "use a passphrase") flag.BoolVar(&passFlag, "passphrase", false, "use a passphrase") flag.StringVar(&outFlag, "o", "", "output to `FILE` (default stdout)") flag.StringVar(&outFlag, "output", "", "output to `FILE` (default stdout)") flag.BoolVar(&armorFlag, "a", false, "generate an armored file") flag.BoolVar(&armorFlag, "armor", false, "generate an armored file") flag.Var(&recipientFlags, "r", "recipient (can be repeated)") flag.Var(&recipientFlags, "recipient", "recipient (can be repeated)") flag.Var(&recipientsFileFlags, "R", "recipients file (can be repeated)") flag.Var(&recipientsFileFlags, "recipients-file", "recipients file (can be repeated)") flag.Func("i", "identity (can be repeated)", identityFlags.addIdentityFlag) flag.Func("identity", "identity (can be repeated)", identityFlags.addIdentityFlag) flag.Func("j", "data-less plugin (can be repeated)", identityFlags.addPluginFlag) flag.Parse() if versionFlag { if Version != "" { fmt.Println(Version) return } if buildInfo, ok := debug.ReadBuildInfo(); ok { // TODO: use buildInfo.Settings to prepare a pseudoversion such as // v0.0.0-20210817164053-32db794688a5+dirty on Go 1.18+. fmt.Println(buildInfo.Main.Version) return } fmt.Println("(unknown)") return } if flag.NArg() > 1 { var hints []string quotedArgs := strings.Trim(fmt.Sprintf("%q", flag.Args()), "[]") // If the second argument looks like a flag, suggest moving the first // argument to the back (as long as the arguments don't need quoting). if strings.HasPrefix(flag.Arg(1), "-") { hints = append(hints, "the input file must be specified after all flags") safe := true unsafeShell := regexp.MustCompile(`[^\w@%+=:,./-]`) for _, arg := range os.Args { if unsafeShell.MatchString(arg) { safe = false break } } if safe { i := len(os.Args) - flag.NArg() newArgs := append([]string{}, os.Args[:i]...) newArgs = append(newArgs, os.Args[i+1:]...) newArgs = append(newArgs, os.Args[i]) hints = append(hints, "did you mean:") hints = append(hints, " "+strings.Join(newArgs, " ")) } } else { hints = append(hints, "only a single input file may be specified at a time") } errorWithHint("too many INPUT arguments: "+quotedArgs, hints...) } switch { case decryptFlag: if encryptFlag { errorf("-e/--encrypt can't be used with -d/--decrypt") } if armorFlag { errorWithHint("-a/--armor can't be used with -d/--decrypt", "note that armored files are detected automatically") } if passFlag { errorWithHint("-p/--passphrase can't be used with -d/--decrypt", "note that password protected files are detected automatically") } if len(recipientFlags) > 0 { errorWithHint("-r/--recipient can't be used with -d/--decrypt", "did you mean to use -i/--identity to specify a private key?") } if len(recipientsFileFlags) > 0 { errorWithHint("-R/--recipients-file can't be used with -d/--decrypt", "did you mean to use -i/--identity to specify a private key?") } default: // encrypt if len(identityFlags) > 0 && !encryptFlag { errorWithHint("-i/--identity and -j can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt", "did you forget to specify -d/--decrypt?") } if len(recipientFlags)+len(recipientsFileFlags)+len(identityFlags) == 0 && !passFlag { errorWithHint("missing recipients", "did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?") } if len(recipientFlags) > 0 && passFlag { errorf("-p/--passphrase can't be combined with -r/--recipient") } if len(recipientsFileFlags) > 0 && passFlag { errorf("-p/--passphrase can't be combined with -R/--recipients-file") } if len(identityFlags) > 0 && passFlag { errorf("-p/--passphrase can't be combined with -i/--identity and -j") } } var in io.Reader = os.Stdin var out io.Writer = os.Stdout if name := flag.Arg(0); name != "" && name != "-" { f, err := os.Open(name) if err != nil { errorf("failed to open input file %q: %v", name, err) } defer f.Close() in = f } else { stdinInUse = true if decryptFlag && term.IsTerminal(int(os.Stdin.Fd())) { // If the input comes from a TTY, assume it's armored, and buffer up // to the END line (or EOF/EOT) so that a password prompt or the // output don't get in the way of typing the input. See Issue 364. buf, err := bufferTerminalInput(in) if err != nil { errorf("failed to buffer terminal input: %v", err) } in = buf } } if name := outFlag; name != "" && name != "-" { f := newLazyOpener(name) defer func() { if err := f.Close(); err != nil { errorf("failed to close output file %q: %v", name, err) } }() out = f } else if term.IsTerminal(int(os.Stdout.Fd())) { if name != "-" { if decryptFlag { // TODO: buffer the output and check it's printable. } else if !armorFlag { // If the output wouldn't be armored, refuse to send binary to // the terminal unless explicitly requested with "-o -". errorWithHint("refusing to output binary to the terminal", "did you mean to use -a/--armor?", `force anyway with "-o -"`) } } if in == os.Stdin && term.IsTerminal(int(os.Stdin.Fd())) { // If the input comes from a TTY and output will go to a TTY, // buffer it up so it doesn't get in the way of typing the input. buf := &bytes.Buffer{} defer func() { io.Copy(os.Stdout, buf) }() out = buf } } switch { case decryptFlag && len(identityFlags) == 0: decryptPass(in, out) case decryptFlag: decryptNotPass(identityFlags, in, out) case passFlag: encryptPass(in, out, armorFlag) default: encryptNotPass(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag) } } func passphrasePromptForEncryption() (string, error) { pass, err := readSecret("Enter passphrase (leave empty to autogenerate a secure one):") if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } p := string(pass) if p == "" { var words []string for i := 0; i < 10; i++ { words = append(words, randomWord()) } p = strings.Join(words, "-") err := printfToTerminal("using autogenerated passphrase %q", p) if err != nil { return "", fmt.Errorf("could not print passphrase: %v", err) } } else { confirm, err := readSecret("Confirm passphrase:") if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } if string(confirm) != p { return "", fmt.Errorf("passphrases didn't match") } } return p, nil } func encryptNotPass(recs, files []string, identities identityFlags, in io.Reader, out io.Writer, armor bool) { var recipients []age.Recipient for _, arg := range recs { r, err := parseRecipient(arg) if err, ok := err.(gitHubRecipientError); ok { errorWithHint(err.Error(), "instead, use recipient files like", " curl -O https://github.com/"+err.username+".keys", " age -R "+err.username+".keys") } if err != nil { errorf("%v", err) } recipients = append(recipients, r) } for _, name := range files { recs, err := parseRecipientsFile(name) if err != nil { errorf("failed to parse recipient file %q: %v", name, err) } recipients = append(recipients, recs...) } for _, f := range identities { switch f.Type { case "i": ids, err := parseIdentitiesFile(f.Value) if err != nil { errorf("reading %q: %v", f.Value, err) } r, err := identitiesToRecipients(ids) if err != nil { errorf("internal error processing %q: %v", f.Value, err) } recipients = append(recipients, r...) case "j": id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI) if err != nil { errorf("initializing %q: %v", f.Value, err) } recipients = append(recipients, id.Recipient()) } } encrypt(recipients, in, out, armor) } func encryptPass(in io.Reader, out io.Writer, armor bool) { pass, err := passphrasePromptForEncryption() if err != nil { errorf("%v", err) } r, err := age.NewScryptRecipient(pass) if err != nil { errorf("%v", err) } testOnlyConfigureScryptIdentity(r) encrypt([]age.Recipient{r}, in, out, armor) } var testOnlyConfigureScryptIdentity = func(*age.ScryptRecipient) {} func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor bool) { if withArmor { a := armor.NewWriter(out) defer func() { if err := a.Close(); err != nil { errorf("%v", err) } }() out = a } w, err := age.Encrypt(out, recipients...) if err != nil { errorf("%v", err) } if _, err := io.Copy(w, in); err != nil { errorf("%v", err) } if err := w.Close(); err != nil { errorf("%v", err) } } // crlfMangledIntro and utf16MangledIntro are the intro lines of the age format // after mangling by various versions of PowerShell redirection, truncated to // the length of the correct intro line. See issue 290. const crlfMangledIntro = "age-encryption.org/v1" + "\r" const utf16MangledIntro = "\xff\xfe" + "a\x00g\x00e\x00-\x00e\x00n\x00c\x00r\x00y\x00p\x00" type rejectScryptIdentity struct{} func (rejectScryptIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) { if len(stanzas) != 1 || stanzas[0].Type != "scrypt" { return nil, age.ErrIncorrectIdentity } errorWithHint("file is passphrase-encrypted but identities were specified with -i/--identity or -j", "remove all -i/--identity/-j flags to decrypt passphrase-encrypted files") panic("unreachable") } func decryptNotPass(flags identityFlags, in io.Reader, out io.Writer) { identities := []age.Identity{rejectScryptIdentity{}} for _, f := range flags { switch f.Type { case "i": ids, err := parseIdentitiesFile(f.Value) if err != nil { errorf("reading %q: %v", f.Value, err) } identities = append(identities, ids...) case "j": id, err := plugin.NewIdentityWithoutData(f.Value, pluginTerminalUI) if err != nil { errorf("initializing %q: %v", f.Value, err) } identities = append(identities, id) } } decrypt(identities, in, out) } func decryptPass(in io.Reader, out io.Writer) { identities := []age.Identity{ // If there is an scrypt recipient (it will have to be the only one and) // this identity will be invoked. &LazyScryptIdentity{passphrasePromptForDecryption}, } decrypt(identities, in, out) } func decrypt(identities []age.Identity, in io.Reader, out io.Writer) { rr := bufio.NewReader(in) if intro, _ := rr.Peek(len(crlfMangledIntro)); string(intro) == crlfMangledIntro || string(intro) == utf16MangledIntro { errorWithHint("invalid header intro", "it looks like this file was corrupted by PowerShell redirection", "consider using -o or -a to encrypt files in PowerShell") } if start, _ := rr.Peek(len(armor.Header)); string(start) == armor.Header { in = armor.NewReader(rr) } else { in = rr } r, err := age.Decrypt(in, identities...) if err != nil { errorf("%v", err) } if _, err := io.Copy(out, r); err != nil { errorf("%v", err) } } func passphrasePromptForDecryption() (string, error) { pass, err := readSecret("Enter passphrase:") if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } return string(pass), nil } func identitiesToRecipients(ids []age.Identity) ([]age.Recipient, error) { var recipients []age.Recipient for _, id := range ids { switch id := id.(type) { case *age.X25519Identity: recipients = append(recipients, id.Recipient()) case *plugin.Identity: recipients = append(recipients, id.Recipient()) case *agessh.RSAIdentity: recipients = append(recipients, id.Recipient()) case *agessh.Ed25519Identity: recipients = append(recipients, id.Recipient()) case *agessh.EncryptedSSHIdentity: recipients = append(recipients, id.Recipient()) case *EncryptedIdentity: r, err := id.Recipients() if err != nil { return nil, err } recipients = append(recipients, r...) default: return nil, fmt.Errorf("unexpected identity type: %T", id) } } return recipients, nil } type lazyOpener struct { name string f *os.File err error } func newLazyOpener(name string) io.WriteCloser { return &lazyOpener{name: name} } func (l *lazyOpener) Write(p []byte) (n int, err error) { if l.f == nil && l.err == nil { l.f, l.err = os.Create(l.name) } if l.err != nil { return 0, l.err } return l.f.Write(p) } func (l *lazyOpener) Close() error { if l.f != nil { return l.f.Close() } return nil } age-1.1.1/cmd/age/age_test.go000066400000000000000000000035601435753552200157060ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build ignore package main import ( "bufio" "os" "testing" "filippo.io/age" "github.com/rogpeppe/go-internal/testscript" ) func TestMain(m *testing.M) { os.Exit(testscript.RunMain(m, map[string]func() int{ "age": func() (exitCode int) { testOnlyPanicInsteadOfExit = true defer func() { if testOnlyDidExit { exitCode = recover().(int) } }() testOnlyConfigureScryptIdentity = func(r *age.ScryptRecipient) { r.SetWorkFactor(10) } testOnlyFixedRandomWord = "four" main() return 0 }, "age-plugin-test": func() (exitCode int) { // TODO: use plugin server package once it's available. switch os.Args[1] { case "--age-plugin=recipient-v1": scanner := bufio.NewScanner(os.Stdin) scanner.Scan() // add-recipient scanner.Scan() // body scanner.Scan() // wrap-file-key scanner.Scan() // body fileKey := scanner.Text() scanner.Scan() // done scanner.Scan() // body os.Stdout.WriteString("-> recipient-stanza 0 test\n") os.Stdout.WriteString(fileKey + "\n") os.Stdout.WriteString("-> done\n\n") return 0 case "--age-plugin=identity-v1": scanner := bufio.NewScanner(os.Stdin) scanner.Scan() // add-identity scanner.Scan() // body scanner.Scan() // recipient-stanza scanner.Scan() // body fileKey := scanner.Text() scanner.Scan() // done scanner.Scan() // body os.Stdout.WriteString("-> file-key 0\n") os.Stdout.WriteString(fileKey + "\n") os.Stdout.WriteString("-> done\n\n") return 0 default: return 1 } }, })) } func TestScript(t *testing.T) { testscript.Run(t, testscript.Params{ Dir: "testdata", // TODO: enable AGEDEBUG=plugin without breaking stderr checks. }) } age-1.1.1/cmd/age/encrypted_keys.go000066400000000000000000000053571435753552200171510ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bytes" "errors" "fmt" "filippo.io/age" ) // LazyScryptIdentity is an age.Identity that requests a passphrase only if it // encounters an scrypt stanza. After obtaining a passphrase, it delegates to // ScryptIdentity. type LazyScryptIdentity struct { Passphrase func() (string, error) } var _ age.Identity = &LazyScryptIdentity{} func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { for _, s := range stanzas { if s.Type == "scrypt" && len(stanzas) != 1 { return nil, errors.New("an scrypt recipient must be the only one") } } if len(stanzas) != 1 || stanzas[0].Type != "scrypt" { return nil, age.ErrIncorrectIdentity } pass, err := i.Passphrase() if err != nil { return nil, fmt.Errorf("could not read passphrase: %v", err) } ii, err := age.NewScryptIdentity(pass) if err != nil { return nil, err } fileKey, err = ii.Unwrap(stanzas) if errors.Is(err, age.ErrIncorrectIdentity) { // ScryptIdentity returns ErrIncorrectIdentity for an incorrect // passphrase, which would lead Decrypt to returning "no identity // matched any recipient". That makes sense in the API, where there // might be multiple configured ScryptIdentity. Since in cmd/age there // can be only one, return a better error message. return nil, fmt.Errorf("incorrect passphrase") } return fileKey, err } type EncryptedIdentity struct { Contents []byte Passphrase func() (string, error) NoMatchWarning func() identities []age.Identity } var _ age.Identity = &EncryptedIdentity{} func (i *EncryptedIdentity) Recipients() ([]age.Recipient, error) { if i.identities == nil { if err := i.decrypt(); err != nil { return nil, err } } return identitiesToRecipients(i.identities) } func (i *EncryptedIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { if i.identities == nil { if err := i.decrypt(); err != nil { return nil, err } } for _, id := range i.identities { fileKey, err = id.Unwrap(stanzas) if errors.Is(err, age.ErrIncorrectIdentity) { continue } if err != nil { return nil, err } return fileKey, nil } i.NoMatchWarning() return nil, age.ErrIncorrectIdentity } func (i *EncryptedIdentity) decrypt() error { d, err := age.Decrypt(bytes.NewReader(i.Contents), &LazyScryptIdentity{i.Passphrase}) if e := new(age.NoIdentityMatchError); errors.As(err, &e) { return fmt.Errorf("identity file is encrypted with age but not with a passphrase") } if err != nil { return fmt.Errorf("failed to decrypt identity file: %v", err) } i.identities, err = parseIdentities(d) return err } age-1.1.1/cmd/age/parse.go000066400000000000000000000201651435753552200152250ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bufio" "encoding/base64" "fmt" "io" "os" "strings" "filippo.io/age" "filippo.io/age/agessh" "filippo.io/age/armor" "filippo.io/age/internal/plugin" "golang.org/x/crypto/cryptobyte" "golang.org/x/crypto/ssh" ) type gitHubRecipientError struct { username string } func (gitHubRecipientError) Error() string { return `"github:" recipients were removed from the design` } func parseRecipient(arg string) (age.Recipient, error) { switch { case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1: return plugin.NewRecipient(arg, pluginTerminalUI) case strings.HasPrefix(arg, "age1"): return age.ParseX25519Recipient(arg) case strings.HasPrefix(arg, "ssh-"): return agessh.ParseRecipient(arg) case strings.HasPrefix(arg, "github:"): name := strings.TrimPrefix(arg, "github:") return nil, gitHubRecipientError{name} } return nil, fmt.Errorf("unknown recipient type: %q", arg) } func parseRecipientsFile(name string) ([]age.Recipient, error) { var f *os.File if name == "-" { if stdinInUse { return nil, fmt.Errorf("standard input is used for multiple purposes") } stdinInUse = true f = os.Stdin } else { var err error f, err = os.Open(name) if err != nil { return nil, fmt.Errorf("failed to open recipient file: %v", err) } defer f.Close() } const recipientFileSizeLimit = 16 << 20 // 16 MiB const lineLengthLimit = 8 << 10 // 8 KiB, same as sshd(8) var recs []age.Recipient scanner := bufio.NewScanner(io.LimitReader(f, recipientFileSizeLimit)) var n int for scanner.Scan() { n++ line := scanner.Text() if strings.HasPrefix(line, "#") || line == "" { continue } if len(line) > lineLengthLimit { return nil, fmt.Errorf("%q: line %d is too long", name, n) } r, err := parseRecipient(line) if err != nil { if t, ok := sshKeyType(line); ok { // Skip unsupported but valid SSH public keys with a warning. warningf("recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n) continue } // Hide the error since it might unintentionally leak the contents // of confidential files. return nil, fmt.Errorf("%q: malformed recipient at line %d", name, n) } recs = append(recs, r) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("%q: failed to read recipients file: %v", name, err) } if len(recs) == 0 { return nil, fmt.Errorf("%q: no recipients found", name) } return recs, nil } func sshKeyType(s string) (string, bool) { // TODO: also ignore options? And maybe support multiple spaces and tabs as // field separators like OpenSSH? fields := strings.Split(s, " ") if len(fields) < 2 { return "", false } key, err := base64.StdEncoding.DecodeString(fields[1]) if err != nil { return "", false } k := cryptobyte.String(key) var typeLen uint32 var typeBytes []byte if !k.ReadUint32(&typeLen) || !k.ReadBytes(&typeBytes, int(typeLen)) { return "", false } if t := fields[0]; t == string(typeBytes) { return t, true } return "", false } // parseIdentitiesFile parses a file that contains age or SSH keys. It returns // one or more of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity, // *agessh.EncryptedSSHIdentity, or *EncryptedIdentity. func parseIdentitiesFile(name string) ([]age.Identity, error) { var f *os.File if name == "-" { if stdinInUse { return nil, fmt.Errorf("standard input is used for multiple purposes") } stdinInUse = true f = os.Stdin } else { var err error f, err = os.Open(name) if err != nil { return nil, fmt.Errorf("failed to open file: %v", err) } defer f.Close() } b := bufio.NewReader(f) p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE" peeked := string(p) switch { // An age encrypted file, plain or armored. case peeked == "age-encryption" || peeked == "-----BEGIN AGE": var r io.Reader = b if peeked == "-----BEGIN AGE" { r = armor.NewReader(r) } const privateKeySizeLimit = 1 << 24 // 16 MiB contents, err := io.ReadAll(io.LimitReader(r, privateKeySizeLimit)) if err != nil { return nil, fmt.Errorf("failed to read %q: %v", name, err) } if len(contents) == privateKeySizeLimit { return nil, fmt.Errorf("failed to read %q: file too long", name) } return []age.Identity{&EncryptedIdentity{ Contents: contents, Passphrase: func() (string, error) { pass, err := readSecret(fmt.Sprintf("Enter passphrase for identity file %q:", name)) if err != nil { return "", fmt.Errorf("could not read passphrase: %v", err) } return string(pass), nil }, NoMatchWarning: func() { warningf("encrypted identity file %q didn't match file's recipients", name) }, }}, nil // Another PEM file, possibly an SSH private key. case strings.HasPrefix(peeked, "-----BEGIN"): const privateKeySizeLimit = 1 << 14 // 16 KiB contents, err := io.ReadAll(io.LimitReader(b, privateKeySizeLimit)) if err != nil { return nil, fmt.Errorf("failed to read %q: %v", name, err) } if len(contents) == privateKeySizeLimit { return nil, fmt.Errorf("failed to read %q: file too long", name) } return parseSSHIdentity(name, contents) // An unencrypted age identity file. default: ids, err := parseIdentities(b) if err != nil { return nil, fmt.Errorf("failed to read %q: %v", name, err) } return ids, nil } } func parseIdentity(s string) (age.Identity, error) { switch { case strings.HasPrefix(s, "AGE-PLUGIN-"): return plugin.NewIdentity(s, pluginTerminalUI) case strings.HasPrefix(s, "AGE-SECRET-KEY-1"): return age.ParseX25519Identity(s) default: return nil, fmt.Errorf("unknown identity type") } } // parseIdentities is like age.ParseIdentities, but supports plugin identities. func parseIdentities(f io.Reader) ([]age.Identity, error) { const privateKeySizeLimit = 1 << 24 // 16 MiB var ids []age.Identity scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit)) var n int for scanner.Scan() { n++ line := scanner.Text() if strings.HasPrefix(line, "#") || line == "" { continue } i, err := parseIdentity(line) if err != nil { return nil, fmt.Errorf("error at line %d: %v", n, err) } ids = append(ids, i) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("failed to read secret keys file: %v", err) } if len(ids) == 0 { return nil, fmt.Errorf("no secret keys found") } return ids, nil } func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) { id, err := agessh.ParseIdentity(pemBytes) if sshErr, ok := err.(*ssh.PassphraseMissingError); ok { pubKey := sshErr.PublicKey if pubKey == nil { pubKey, err = readPubFile(name) if err != nil { return nil, err } } passphrasePrompt := func() ([]byte, error) { pass, err := readSecret(fmt.Sprintf("Enter passphrase for %q:", name)) if err != nil { return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err) } return pass, nil } i, err := agessh.NewEncryptedSSHIdentity(pubKey, pemBytes, passphrasePrompt) if err != nil { return nil, err } return []age.Identity{i}, nil } if err != nil { return nil, fmt.Errorf("malformed SSH identity in %q: %v", name, err) } return []age.Identity{id}, nil } func readPubFile(name string) (ssh.PublicKey, error) { if name == "-" { return nil, fmt.Errorf(`failed to obtain public key for "-" SSH key Use a file for which the corresponding ".pub" file exists, or convert the private key to a modern format with "ssh-keygen -p -m RFC4716"`) } f, err := os.Open(name + ".pub") if err != nil { return nil, fmt.Errorf(`failed to obtain public key for %q SSH key: %v Ensure %q exists, or convert the private key %q to a modern format with "ssh-keygen -p -m RFC4716"`, name, err, name+".pub", name) } defer f.Close() contents, err := io.ReadAll(f) if err != nil { return nil, fmt.Errorf("failed to read %q: %v", name+".pub", err) } pubKey, _, _, _, err := ssh.ParseAuthorizedKey(contents) if err != nil { return nil, fmt.Errorf("failed to parse %q: %v", name+".pub", err) } return pubKey, nil } age-1.1.1/cmd/age/testdata/000077500000000000000000000000001435753552200153715ustar00rootroot00000000000000age-1.1.1/cmd/age/testdata/ed25519.txt000066400000000000000000000021061435753552200171270ustar00rootroot00000000000000# encrypt and decrypt a file with -R age -R key.pem.pub -o test.age input age -d -i key.pem test.age cmp stdout input ! stderr . # encrypt and decrypt a file with -i age -e -i key.pem -o test.age input age -d -i key.pem test.age cmp stdout input ! stderr . # encrypt and decrypt a file with the wrong key age -R otherkey.pem.pub -o test.age input ! age -d -i key.pem test.age stderr 'no identity matched any of the recipients' ! stdout . -- input -- test -- key.pem -- -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2gAAAKDgV/GC4Ffx ggAAAAtzc2gtZWQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2g AAAECvFoQXQzXgJLQ+Gz4PfEcfyZwC2gUjOiWTD//mTPyD8H9pO5pz22JZEasoS2LEWWvJ iUYI9M6l1uZc31FGYMXaAAAAG2ZpbGlwcG9AQmlzdHJvbWF0aC1NMS5sb2NhbAEC -----END OPENSSH PRIVATE KEY----- -- key.pem.pub -- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FGYMXa -- otherkey.pem.pub -- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJFlMdZUMrWjJ3hh60MLALXSqUdAjBo/qEMJzvpekpoM age-1.1.1/cmd/age/testdata/encrypted_keys.txt000066400000000000000000000142221435753552200211630ustar00rootroot00000000000000# TODO: age-encrypted private keys, multiple identities, -i ordering, -e -i, # age file password prompt during encryption [windows] skip # no pty support # use an encrypted OpenSSH private key without .pub file age -R key_ed25519.pub -o ed25519.age input rm key_ed25519.pub pty terminal age -d -i key_ed25519 ed25519.age cmp stdout input ! stderr . # -e -i with an encrypted OpenSSH private key age -e -i key_ed25519 -o ed25519.age input pty terminal age -d -i key_ed25519 ed25519.age cmp stdout input # a file encrypted to the wrong key does not ask for the password age -R key_ed25519_other.pub -o ed25519_other.age input ! age -d -i key_ed25519 ed25519_other.age stderr 'no identity matched any of the recipients' # use an encrypted legacy PEM private key with a .pub file age -R key_rsa_legacy.pub -o rsa_legacy.age input pty terminal age -d -i key_rsa_legacy rsa_legacy.age cmp stdout input ! stderr . age -R key_rsa_other.pub -o rsa_other.age input ! age -d -i key_rsa_legacy rsa_other.age stderr 'no identity matched any of the recipients' # -e -i with an encrypted legacy PEM private key age -e -i key_rsa_legacy -o rsa_legacy.age input pty terminal age -d -i key_rsa_legacy rsa_legacy.age cmp stdout input # legacy PEM private key without a .pub file causes an error rm key_rsa_legacy.pub ! age -d -i key_rsa_legacy rsa_legacy.age stderr 'key_rsa_legacy.pub' # mismatched .pub file causes an error cp key_rsa_legacy key_rsa_other pty terminal ! age -d -i key_rsa_other rsa_other.age stderr 'mismatched private and public SSH key' # buffer armored ciphertext before prompting if stdin is the terminal pty terminal age -e -i key_ed25519 -a -o test.age input exec cat test.age terminal # concatenated ciphertext + password pty -stdin stdout age -d -i key_ed25519 ptyout 'Enter passphrase' ! stderr . cmp stdout input -- input -- test -- terminal -- password -- key_ed25519 -- -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCuvb97i7 U6Dz4+4SaF3kK1AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKaVctg4/hmFbfof Tv+yrC2IweO/Dd2AVDijFpaMO9fmAAAAoMO7yEnisRmzFdiExNt3XTYuLdP9m3jgOCroiF TtBhh1lAB2qggzWExMRP3Ak8+AloXEcWiACwBYnqwxhQMh0RDCDKC/H/4SXO+ds4HFWil+ 4bGF9wYZFU7IEjIK91CPGJ6YoWPn9dSdEjjbuCJtOMwHsysGyw5n/qSFPmSAPmA4YL2OzM WFOJ5gB5o1LKZkDTcdt7kPziIoVd5QkqpnYsE= -----END OPENSSH PRIVATE KEY----- -- key_ed25519.pub -- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKaVctg4/hmFbfofTv+yrC2IweO/Dd2AVDijFpaMO9fm -- key_ed25519_other.pub -- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINbTd+xfSBYKR/1Hp7FsoxwQAdIOk1Khye6ALBj7e1CV -- key_rsa_legacy -- -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: AES-128-CBC,8045E7CF19D7794F4ADF5AC63179D985 OESHhWCho337W1Ajg+iMbsZx/FPtHM3YPHu/d1U51ERIUh0wVof2SK0ooENokr6g O3fcv9Xga+Na4Ez+gsFRsIZOdqrJq+QBH0CAKi+Mz4KsU7teAobUBJgRB31Wt7eI 39KGZeaBJLMQ0FzQkDx5MCOg98iu9rt+Pg1bH8X88wV4vOv+tG4nmqgdpDmouo1Q uW1TJxrdPhkINjaPZZ7gvjS8wuG9+qwQY76I0hGun9secf4VZDysqUnUp8UHYovR dbvKCbglQy18mGL4kREJ/hH/9/maefS+pTMb2UX0onp9j7l3yNSvL4A4xW85ii6x liVMnZvLvbfPtI7jjZtC8CjshRkZke4fSZF2nZP7zK2qVcqDFCtemaks+0i2ksel D8clUKhBmq23VNAt+iy1stwHBporuaE6kEVJail5WPpgdfQjifpaMbTsZgOK+vGL GKi8vSJWfMU3lTf/N++ks2FWxdq0TgQirsKsQ5mWobfxc1XehvvdJj8hUtArrP32 d4ge5DXPpmtkCzrc1+wt8Py/ANl9jV6c+4fCbpQ2snyzdFEhFtXHCEpMguN9MhKI gaZIfAxvYcQr8Gwew/IB02Phda9tvDiedHvyHGJmSy/87fR6ECh47VDFL/UYu4jG 0hRtAZMMddGNfoosnO6OKBd09cgvXKCsUrbpAI7dF5TP5ItDkVb08hW446jBdgS9 7QqB0rPmlAjsJi7fsrDw7Nq9pOdqqCEwUMc9Lztnv66aX1d/Y2vQm9mrsDbyZKqn g621rg7E4UHf7EGiDblfS234+TsNvwZ6sEbivU+3zqglPiOF71m6D0cKgaUZPOua GNdyQz5e73hYa+NJ76IZ+IqkoJAFXBkd1nWcN6DUBYiKvqd4qO9xD+JvNtiFlQ9d pyO9t4FTGvySh8CKyEUEdtj+2ftCIuZaUD2L5YJU1tlQV0EH42InOmkmphbHkW5v lNAoZAny1Z0P6O0gn7xtVrgd7paVQfDCJtkvsm5zR6Yei5FUgY/9NPaRotzuZVAY EfQC7JPdSdb5yusnXh7B9jGkgxhMIb6EPFFjIZ4iaV1RVgINSisGMSFzlqOz702b Cawsr9nD438cjzMNYEmrihZZBjHon6hHrLmM9Aj2xgprsoNLP1jJQ6WpZDlrYsj0 XS0tSJmh0pM4Ey6j1VWNoaOxVseYLW7J9wGVfH/HJAc2k6Wg46P2e8lMT6Sj4YsT EguDhUjXrgePC53ohcSF+I6x35Q1D6ttMnc3ODzmIcCisxAvWdAqi1yRlnBotRwg S2vq3HU0yJFG8pJqw4vU9A9DlaMMT+ejEH+9xVwAWM+7n2lJcgthtWuShZCE6BB+ jVobSlTMArzQj4klTSbew1m9Waa6kKDezsAY66mryVNofCCeYDOBRecCm5JyMnWf WBVnNx+kZ/YyvYeBcSh34u8rkjqGpzfM/oPE7GwIoZvbAirjLohL7u8oq2bfAYG0 /xIPwPJw1O3o5PHeu84bVIRqcKzGeaVL+5aUiZP9uNGUpqJWA5q2Sa5BOXV46yqO DIS8q7uPCSbt5mPXPDGJ1CupCdA1stUf2kb0cDJ+LpUbPND9SebBlxSuR1D/YGqv wlzfN5Usv/h/XNl98bYtpY8/skKPecyx3wG3VtwWH/5XVhvHz4TENjlKv/L2pbUC Dv83WcL1N/i+jerYxDRmGe3NQOvyW4JaNzzjgb74T7rE1/3lf6qkmUHjxfo4VZAF L/q2782OUs5Qt4/pYAIISzLdBw6XtTjZHirqa6YNrFvGucB3NG49AC0b1Z0acfrS iimC2TvZpwunlLbyz2SQQL2c1zQ3U/Yfh2F1Zt8o6kK3RgKSSx57rK6nV7hXMGGp C4HV3nLetZg8HexicqeRANLXuUDbCSpN8K4nW5G2g/yKPfsQHBV/RWEDfhndykja +SmoY5IB+2zEbCC3MWiP9ZdIcCYOsq8wDZESMMW40DlVICjrf6UOqQ+ogci20qLS CmpgmOPAaBZJG/sBU79eHUSjPCK6yDpSyc30oVn8FnoBTmOpt7R++Ub8RJxReXBt +6o0NXYCJNaeVnk1bE4iavkJrXJCZvu44VBLS0WUs9W8TD4Iq8kNHsfQsfOuBXnQ ncgoIe9HppnMGNoSzjYBNL/rprlbaOE55TkPqiQsiskRcaoeY53aTxoIykHmoj8G wJo/00IR+NYir7tr03Vriw+uywPPGucVJGWTUGsNbHlS5j941IltflIf6FitElNr JxVuJLgYiP3JhmWpdqA/uidYJMbIjunpn/8rVLrAil04SCSfUmaCdl7dkQ9x+3Mf Erm699vIBQwvv0i+mcwKEvqSrhhNQ2F7vrb7NL8I2wUEPgQbv1PxSV6X0aYcxYVI -----END RSA PRIVATE KEY----- -- key_rsa_legacy.pub -- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCky7Clp8I3LVoqZWtat+QR6KmM0evFilmFhwenINIBbb8eS3ftDSkQy2YRrlAvO3h4EZffOIxANGL/yKVlRCIzvjsphi+tTHscZsQhwMnLEmxEayTq20hZKcwNA8TQdh2TW/w0KZmNZcxlTn4IK8W16komHcoH/qrRiXq8z3ROcfnv3Q4Hll9MUCwBkfy2DdBpWUMidQ1dAK4i3vXdseF74hJ0jFbPtS5mlpOsJZa0sdH1dnEl5M8wZS3PxyzM6JMkgzG7INp4sO/xGIisjl/QuSh2Fu93/EogdGXxIZChniUfzBx1DaHlerPPNSMP+uLbaOIAQrIPozhfdUdsCFDMoB7/PA6g1WVYZWAqjBZZW/GMOzPhih57NIFBSyMTzMi1KS6OBvYJvPf4IcvOa3May9ylLG/wZVhrHlQPbSsbRrraVtJ1P4gGQJ5U4d2AD2q+XtMb5f2i/holMXTVQl7Fa7RYi1TblDuW5OZCvmIawePBXAYbPg0OVFs3vAVEuAM= -- key_rsa_other.pub -- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQiCWw2W++gX4wcwpDo6QIouwQ9PPwCVe7QPICzxztG27mzeKRM4xT2LURGSaQqg7OYIUTGrLqNsaLZW+FHHQlRAVv1LEbdEFa5JermBMJ5j/HxamE/7oV60gMRlgKW+4IZhVMPgRZaaXU0YPb9oACdMNM8kPkc5JaOJ8iO6B1RViybjLD+tsEEPXLp3Mrj+sJqs+IvNlJKXdeefOjNrGmLHKIFdHiWlZ+aAW+QLfMQiNXoTbGybFUSpNEbmK/1ITiRAly94NoUK9LoriueXR+WJIm9wP4SfHw+hMBz1cywdF2wwKmWWegizV/USEmhyNXUzHZzjbkgE84DrIq+NA7SUmw6C8ClMjdnRnnoIyga99yMIrYMny1KW/bk1NK4u6Tv17E+FFOS3vf2Gcj01/jOmAUIQwL8MjAHhnsZ4XAA5NHa2NRGWm+hw7fx5uX42Gyz8HidFda5Lij1pASBcx4U3qwb62X+IVN50jGIP6kRNmGtMLY1JgaoGDDkw9r6mU= age-1.1.1/cmd/age/testdata/pkcs8.txt000066400000000000000000000005541435753552200171660ustar00rootroot00000000000000# https://github.com/FiloSottile/age/discussions/428 # encrypt and decrypt a file with an Ed25519 key encoded with PKCS#8 age -e -i key.pem -o test.age input age -d -i key.pem test.age cmp stdout input ! stderr . -- input -- test -- key.pem -- -----BEGIN PRIVATE KEY----- MC4CAQAwBQYDK2VwBCIEIJT4Wpo+YG11yybKL/bYXQW7ekz4PAsmV/4tfmY1vU7x -----END PRIVATE KEY----- age-1.1.1/cmd/age/testdata/plugin.txt000066400000000000000000000071721435753552200174370ustar00rootroot00000000000000# encrypt and decrypt a file with a test plugin age -r age1test10qdmzv9q -o test.age input age -d -i key.txt test.age cmp stdout input ! stderr . # very long identity and recipient age -R long-recipient.txt -o test.age input age -d -i long-key.txt test.age cmp stdout input ! stderr . -- input -- test -- key.txt -- AGE-PLUGIN-TEST-10Q32NLXM -- long-recipient.txt -- age1test10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qj6rl8p -- long-key.txt -- AGE-PLUGIN-TEST-10PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7Q5U8SUD age-1.1.1/cmd/age/testdata/rsa.txt000066400000000000000000000102061435753552200167160ustar00rootroot00000000000000# encrypt and decrypt a file with -R age -R key.pem.pub -o test.age input age -d -i key.pem test.age cmp stdout input ! stderr . # encrypt and decrypt a file with -i age -e -i key.pem -o test.age input age -d -i key.pem test.age cmp stdout input ! stderr . # encrypt and decrypt a file with the wrong key age -R otherkey.pem.pub -o test.age input ! age -d -i key.pem test.age stderr 'no identity matched any of the recipients' -- input -- test -- key.pem -- -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn NhAAAAAwEAAQAAAYEA1C04rdClHoW4oG4bEGmaNqFy4DLoPJ0358w4XH+XBM3TiWcheouW kUG6m1yDmHk0t0oaaf4hOnetKovdyQQX73gGaq++rSu5VSvH7LbwABoG6PS/UbuZ4Vl9B0 5WVDqHVE9hNK4AHqBc373GU2mo8z5opKxEprmiS3HSd3K2wiMqL5E8XPOSm0p/isuYK57X VUexl73tB7iIMLklxjcjtP4REMoQhHKOMOdy2Q15dw5cYG+drtEArBRYkCZmd0Vp2ws9pj YzPVaOSkbdqSeLu+JVbH1wrwKhuBrA3eVlwjUTWkO4FHcNXkp773Mt4cXhKizTfbR2hQox Lsj31301Xd7dEpV63sqDW1e+a2L2dhemi8cjDMrPuW6Z19Lbti0quAb4+cSLAaJI4BHd1F 8o9XhK7EHVCdIIIQDKVzo1WyEsDwBjL1LB9rpxm4732sZyue0uygFzmM544QX+WsiJXgHP uC1Q/ynjLRm6ZMl16MwvY8B/XGQWxlOAbRJQG84fAAAFmEwAjV1MAI1dAAAAB3NzaC1yc2 EAAAGBANQtOK3QpR6FuKBuGxBpmjahcuAy6DydN+fMOFx/lwTN04lnIXqLlpFBuptcg5h5 NLdKGmn+ITp3rSqL3ckEF+94Bmqvvq0ruVUrx+y28AAaBuj0v1G7meFZfQdOVlQ6h1RPYT SuAB6gXN+9xlNpqPM+aKSsRKa5oktx0ndytsIjKi+RPFzzkptKf4rLmCue11VHsZe97Qe4 iDC5JcY3I7T+ERDKEIRyjjDnctkNeXcOXGBvna7RAKwUWJAmZndFadsLPaY2Mz1WjkpG3a kni7viVWx9cK8CobgawN3lZcI1E1pDuBR3DV5Ke+9zLeHF4Sos0320doUKMS7I99d9NV3e 3RKVet7Kg1tXvmti9nYXpovHIwzKz7lumdfS27YtKrgG+PnEiwGiSOAR3dRfKPV4SuxB1Q nSCCEAylc6NVshLA8AYy9Swfa6cZuO99rGcrntLsoBc5jOeOEF/lrIiV4Bz7gtUP8p4y0Z umTJdejML2PAf1xkFsZTgG0SUBvOHwAAAAMBAAEAAAGBAKytAOu0Wi009sTZ1vzMdMzxJ+ R+ibKK4Oysr1HYJLesKvQwEncBE1C0BYJbEF4OhnCExmpsf+5tZ2iw25a01iX1sIMy9CNK 6lH+h36Gg1wR0n3Ucb+6xck4YyCHCIsT9v8OezW8Riympe8RK07HNtB/gfpCmLx3ZzWvNH Ix0bq9k5+Su2WKdU4cmyACAZ2+b9DfwBCWaUlXTL8abzuZtF2gR5M6X6bq8/2o3zb2WFwk O9nf/JxBTCK/jDQEjG+U9MyGxZIW5DeG1nNFtOzJoT8krIkeSOjQ5XQrkjCw+yihSCWMG+ s+SKO77u30SO7OCENsFIXpUzpt6+JmazlXjLW/OdYNooQMHtqCZzVMRgxiy3gDGF35YvgV VnP5gVEW9HEZ0kD+x4Rl2kB6bV7jMi8BXrazQ1EmTasJFg1pv6iRJWzY1JoP2kRfgiHGL6 OqgrXakqo3hMJuz+JRU2/hlF13743MiIxpcbaaRqURoWuNRLHitVWE35/XVCez0C6OwQAA AMEAoh106+3JbiZI19iRICR247IoOLpSSed98eQj+l3OYfJ86cQipSjxdSPWcP58yyyElY d9q6K16sDTLAlRJzF7MFxSc80JY6RgFq/Sy4Jm0/Z10wwJhTgOkxq6IynzLnO7goRirE31 jxGif4nI2IYEQvv6MOD8TWA4axxGMw2StYB6P4R5peozf81oR6m79ERIDSkrm0RYYn931r gVuxvo3ABVxMtg1lV80LJMayy87Oi8BehGBxMBgsKtQaH8+5h7AAAAwQD+8lJpBcrrHQKk 3o2XAZxB5Fool4f2iuZWTxA1vq0/TCUcEodrdWfLuDeVbDsFemW0vBSkKzf4NlZSs2DAKl YWT6y18eyDyJXn0TNVTeO3F5mkkX5spqbjDcESSs3whIuDqXU++3sII7iMzGw50tDP4Dw6 TViEVM3anpeqlAbkciR5o9IJx3nRcGh81Bs4gticcRF0vqiJoAhNlSZXR1XMjevwt68i+4 RKPPQsTM7uJLm236VUhDivO1OJcBTLW7MAAADBANUNqH+//G4gIruBO3BsIvbzDw0DgRam R1tqqn4g53boiv1RPtUJ2GbkCsisy5pU+JdTN7ekFEF8KWuunjImkfVyAiTFsHHmOoXV3Z EX0mNDXOlKOP2YAIMrDt5CkPdEh6qQG21LCZXTWmwheZ9iN2vOl/fKqUW9lqd/kTe6WsON hIpZhs2+oz54Riq1ZwzO9NkcYrvZoDKbDopL1r2ibw0mkgCJrxpWi0Yt2Iooh4GXXqP5C9 T8hrZCbrVJkjKd5QAAABtmaWxpcHBvQEJpc3Ryb21hdGgtTTEubG9jYWwBAgMEBQY= -----END OPENSSH PRIVATE KEY----- -- key.pem.pub -- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbigbhsQaZo2oXLgMug8nTfnzDhcf5cEzdOJZyF6i5aRQbqbXIOYeTS3Shpp/iE6d60qi93JBBfveAZqr76tK7lVK8fstvAAGgbo9L9Ru5nhWX0HTlZUOodUT2E0rgAeoFzfvcZTaajzPmikrESmuaJLcdJ3crbCIyovkTxc85KbSn+Ky5grntdVR7GXve0HuIgwuSXGNyO0/hEQyhCEco4w53LZDXl3Dlxgb52u0QCsFFiQJmZ3RWnbCz2mNjM9Vo5KRt2pJ4u74lVsfXCvAqG4GsDd5WXCNRNaQ7gUdw1eSnvvcy3hxeEqLNN9tHaFCjEuyPfXfTVd3t0SlXreyoNbV75rYvZ2F6aLxyMMys+5bpnX0tu2LSq4Bvj5xIsBokjgEd3UXyj1eErsQdUJ0gghAMpXOjVbISwPAGMvUsH2unGbjvfaxnK57S7KAXOYznjhBf5ayIleAc+4LVD/KeMtGbpkyXXozC9jwH9cZBbGU4BtElAbzh8= -- otherkey.pem.pub -- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF0OPu95EY25O5KmYFLIkiZZFKUlfvaRgmfIT6OcZvPRXBzo0MS/lcrYvAc0RsUVbZ1B3Y9oWmKt/IMXTztCXiza70rO1NI7ciayv5svY/wGMoveutddhA64IjrQKs4m+6Qmjs/dYTnfsk1BzmXrdRKUSqH6c4Id7pRLC1ySLu+4og3nTTpBRBpg+uSkc4Ua6ce6A6RX14PPJ+TAXMfZyKNyaubQhgzLB/CfdXxZqWdAnyooiE7fb6CEB5uppnA5BpPdcWAkSixbwxRHbRC+OSCqMOV6+z+NlO/qSOKJcXfCQnJP/qjJTJde0dYhXG4RILOzIkGVieGJJONDXvj61mMj568IhJz0AEf/UMhvEL79iJ6yZW82Go/zcYkDDfd3KRE3pW+6p9Onu3XqOiQABS+9rEVRBnqYsPajiHBIanBeXpWKGbjznakvxhdRifhOWwAsQDfLmGzh+JnV1vOUjyxKtLNv9zi/oeuYCaIyF7F6en8LMbYSz8YONMZygGxMU= age-1.1.1/cmd/age/testdata/scrypt.txt000066400000000000000000000022041435753552200174540ustar00rootroot00000000000000[windows] skip # no pty support # encrypt with a provided passphrase stdin input pty terminal age -p -o test.age ptyout 'Enter passphrase' ! stderr . ! stdout . # decrypt with a provided passphrase pty terminal age -d test.age ptyout 'Enter passphrase' ! stderr . cmp stdout input # decrypt with the wrong passphrase pty wrong ! age -d test.age stderr 'incorrect passphrase' # encrypt with a generated passphrase stdin input pty empty age -p -o test.age ! stderr . ! stdout . pty autogenerated age -d test.age cmp stdout input # fail when -i is present pty terminal ! age -d -i key.txt test.age stderr 'file is passphrase-encrypted but identities were specified' # fail when passphrases don't match pty wrong ! age -p -o fail.age stderr 'passphrases didn''t match' ! exists fail.age -- terminal -- password password -- wrong -- PASSWORD password -- input -- test -- key.txt -- # created: 2021-02-02T13:09:43+01:00 # public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 -- autogenerated -- four-four-four-four-four-four-four-four-four-four -- empty -- age-1.1.1/cmd/age/testdata/terminal.txt000066400000000000000000000024361435753552200177520ustar00rootroot00000000000000[windows] skip # no pty support # controlling terminal is used instead of stdin/stderr pty terminal age -p -o test.age input ! stderr . # autogenerated passphrase is printed to terminal pty empty age -p -o test.age input ptyout 'autogenerated passphrase' ! stderr . # with no controlling terminal, stdin terminal is used ## TODO: enable once https://golang.org/issue/53601 is fixed ## and Noctty is added to testscript. # noctty # pty -stdin terminal # age -p -o test.age input # ! stderr . # no terminal causes an error ## TODO: enable once https://golang.org/issue/53601 is fixed ## and Noctty is added to testscript. # noctty # ! age -p -o test.age input # stderr 'standard input is not a terminal' # prompt for password before plaintext if stdin is the terminal exec cat terminal input # concatenated password + input pty -stdin stdout age -p -a -o test.age ptyout 'Enter passphrase' ! stderr . # check the file was encrypted correctly pty terminal age -d test.age cmp stdout input # buffer armored ciphertext before prompting if stdin is the terminal pty terminal age -p -a -o test.age input exec cat test.age terminal # concatenated ciphertext + password pty -stdin stdout age -d ptyout 'Enter passphrase' ! stderr . cmp stdout input -- input -- test -- terminal -- password password -- empty -- age-1.1.1/cmd/age/testdata/usage.txt000066400000000000000000000004011435753552200172310ustar00rootroot00000000000000# -help age -p -help ! stdout . stderr 'Usage:' # -h age -p -h ! stdout . stderr 'Usage:' # unknown flag ! age -p -this-flag-does-not-exist ! stdout . stderr 'flag provided but not defined' stderr 'Usage:' # no arguments ! age ! stdout . stderr 'Usage:' age-1.1.1/cmd/age/testdata/x25519.txt000066400000000000000000000013531435753552200170110ustar00rootroot00000000000000# encrypt and decrypt a file with -r age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input age -d -i key.txt test.age cmp stdout input ! stderr . # encrypt and decrypt a file with -i age -e -i key.txt -o test.age input age -d -i key.txt test.age cmp stdout input ! stderr . # encrypt and decrypt a file with the wrong key age -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -o test.age input ! age -d -i key.txt test.age stderr 'no identity matched any of the recipients' -- input -- test -- key.txt -- # created: 2021-02-02T13:09:43+01:00 # public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0 age-1.1.1/cmd/age/tui.go000066400000000000000000000140741435753552200147160ustar00rootroot00000000000000// Copyright 2021 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main // This file implements the terminal UI of cmd/age. The rules are: // // - Anything that requires user interaction goes to the terminal, // and is erased afterwards if possible. This UI would be possible // to replace with a pinentry with no output or UX changes. // // - Everything else goes to standard error with an "age:" prefix. // No capitalized initials and no periods at the end. import ( "bytes" "errors" "fmt" "io" "log" "os" "runtime" "filippo.io/age/armor" "filippo.io/age/internal/plugin" "golang.org/x/term" ) // l is a logger with no prefixes. var l = log.New(os.Stderr, "", 0) func printf(format string, v ...interface{}) { l.Printf("age: "+format, v...) } func errorf(format string, v ...interface{}) { l.Printf("age: error: "+format, v...) l.Printf("age: report unexpected or unhelpful errors at https://filippo.io/age/report") exit(1) } func warningf(format string, v ...interface{}) { l.Printf("age: warning: "+format, v...) } func errorWithHint(error string, hints ...string) { l.Printf("age: error: %s", error) for _, hint := range hints { l.Printf("age: hint: %s", hint) } l.Printf("age: report unexpected or unhelpful errors at https://filippo.io/age/report") exit(1) } // If testOnlyPanicInsteadOfExit is true, exit will set testOnlyDidExit and // panic instead of calling os.Exit. This way, the wrapper in TestMain can // recover the panic and return the exit code only if it was originated in exit. var testOnlyPanicInsteadOfExit bool var testOnlyDidExit bool func exit(code int) { if testOnlyPanicInsteadOfExit { testOnlyDidExit = true panic(code) } os.Exit(code) } // clearLine clears the current line on the terminal, or opens a new line if // terminal escape codes don't work. func clearLine(out io.Writer) { const ( CUI = "\033[" // Control Sequence Introducer CPL = CUI + "F" // Cursor Previous Line EL = CUI + "K" // Erase in Line ) // First, open a new line, which is guaranteed to work everywhere. Then, try // to erase the line above with escape codes. // // (We use CRLF instead of LF to work around an apparent bug in WSL2's // handling of CONOUT$. Only when running a Windows binary from WSL2, the // cursor would not go back to the start of the line with a simple LF. // Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.) fmt.Fprintf(out, "\r\n"+CPL+EL) } // withTerminal runs f with the terminal input and output files, if available. // withTerminal does not open a non-terminal stdin, so the caller does not need // to check stdinInUse. func withTerminal(f func(in, out *os.File) error) error { if runtime.GOOS == "windows" { in, err := os.OpenFile("CONIN$", os.O_RDWR, 0) if err != nil { return err } defer in.Close() out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0) if err != nil { return err } defer out.Close() return f(in, out) } else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil { defer tty.Close() return f(tty, tty) } else if term.IsTerminal(int(os.Stdin.Fd())) { return f(os.Stdin, os.Stdin) } else { return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err) } } func printfToTerminal(format string, v ...interface{}) error { return withTerminal(func(_, out *os.File) error { _, err := fmt.Fprintf(out, "age: "+format+"\n", v...) return err }) } // readSecret reads a value from the terminal with no echo. The prompt is ephemeral. func readSecret(prompt string) (s []byte, err error) { err = withTerminal(func(in, out *os.File) error { fmt.Fprintf(out, "%s ", prompt) defer clearLine(out) s, err = term.ReadPassword(int(in.Fd())) return err }) return } // readCharacter reads a single character from the terminal with no echo. The // prompt is ephemeral. func readCharacter(prompt string) (c byte, err error) { err = withTerminal(func(in, out *os.File) error { fmt.Fprintf(out, "%s ", prompt) defer clearLine(out) oldState, err := term.MakeRaw(int(in.Fd())) if err != nil { return err } defer term.Restore(int(in.Fd()), oldState) b := make([]byte, 1) if _, err := in.Read(b); err != nil { return err } c = b[0] return nil }) return } var pluginTerminalUI = &plugin.ClientUI{ DisplayMessage: func(name, message string) error { printf("%s plugin: %s", name, message) return nil }, RequestValue: func(name, message string, _ bool) (s string, err error) { defer func() { if err != nil { warningf("could not read value for age-plugin-%s: %v", name, err) } }() secret, err := readSecret(message) if err != nil { return "", err } return string(secret), nil }, Confirm: func(name, message, yes, no string) (choseYes bool, err error) { defer func() { if err != nil { warningf("could not read value for age-plugin-%s: %v", name, err) } }() if no == "" { message += fmt.Sprintf(" (press enter for %q)", yes) _, err := readSecret(message) if err != nil { return false, err } return true, nil } message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no) for { selection, err := readCharacter(message) if err != nil { return false, err } switch selection { case '1': return true, nil case '2': return false, nil case '\x03': // CTRL-C return false, errors.New("user cancelled prompt") default: warningf("reading value for age-plugin-%s: invalid selection %q", name, selection) } } }, WaitTimer: func(name string) { printf("waiting on %s plugin...", name) }, } func bufferTerminalInput(in io.Reader) (io.Reader, error) { buf := &bytes.Buffer{} if _, err := buf.ReadFrom(ReaderFunc(func(p []byte) (n int, err error) { if bytes.Contains(buf.Bytes(), []byte(armor.Footer+"\n")) { return 0, io.EOF } return in.Read(p) })); err != nil { return nil, err } return buf, nil } type ReaderFunc func(p []byte) (n int, err error) func (f ReaderFunc) Read(p []byte) (n int, err error) { return f(p) } age-1.1.1/cmd/age/wordlist.go000066400000000000000000000327241435753552200157660ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "crypto/rand" "encoding/binary" "strings" ) var testOnlyFixedRandomWord string func randomWord() string { if testOnlyFixedRandomWord != "" { return testOnlyFixedRandomWord } buf := make([]byte, 2) if _, err := rand.Read(buf); err != nil { panic(err) } n := binary.BigEndian.Uint16(buf) return wordlist[int(n)%2048] } // wordlist is the BIP39 list of 2048 english words, and it's used to generate // the suggested passphrases. var 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`, " ") age-1.1.1/doc/000077500000000000000000000000001435753552200130265ustar00rootroot00000000000000age-1.1.1/doc/age-keygen.1000066400000000000000000000033411435753552200151250ustar00rootroot00000000000000.\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . .TH "AGE\-KEYGEN" "1" "September 2022" "" "" . .SH "NAME" \fBage\-keygen\fR \- generate age(1) key pairs . .SH "SYNOPSIS" \fBage\-keygen\fR [\fB\-o\fR \fIOUTPUT\fR] . .br \fBage\-keygen\fR \fB\-y\fR [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR] . .br . .SH "DESCRIPTION" \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\. . .P If the output is not going to a terminal, \fBage\-keygen\fR prints the public key to standard error\. . .SH "OPTIONS" . .TP \fB\-o\fR, \fB\-\-output\fR=\fIOUTPUT\fR Write the identity to \fIOUTPUT\fR instead of standard output\. . .IP If \fIOUTPUT\fR already exists, it is not overwritten\. . .TP \fB\-y\fR Read an identity file from \fIINPUT\fR or from standard input and output the corresponding recipient(s), one per line, with no comments\. . .TP \fB\-\-version\fR Print the version and exit\. . .SH "EXAMPLES" Generate a new identity: . .IP "" 4 . .nf $ age\-keygen # created: 2021\-01\-02T15:30:45+01:00 # public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z AGE\-SECRET\-KEY\-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9 . .fi . .IP "" 0 . .P Write a new identity to \fBkey\.txt\fR: . .IP "" 4 . .nf $ age\-keygen \-o key\.txt Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p . .fi . .IP "" 0 . .P Convert an identity to a recipient: . .IP "" 4 . .nf $ age\-keygen \-y key\.txt age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p . .fi . .IP "" 0 . .SH "SEE ALSO" age(1) . .SH "AUTHORS" Filippo Valsorda \fIage@filippo\.io\fR age-1.1.1/doc/age-keygen.1.html000066400000000000000000000122101435753552200160630ustar00rootroot00000000000000 age-keygen(1) - generate age(1) key pairs
  1. age-keygen(1)
  2. age-keygen(1)

NAME

age-keygen - generate age(1) key pairs

SYNOPSIS

age-keygen [-o OUTPUT]
age-keygen -y [-o OUTPUT] [INPUT]

DESCRIPTION

age-keygen generates a new native age(1) key pair, and outputs the identity to standard output or to the OUTPUT file. The output includes the public key and the current time as comments.

If the output is not going to a terminal, age-keygen prints the public key to standard error.

OPTIONS

-o, --output=OUTPUT

Write the identity to OUTPUT instead of standard output.

If OUTPUT already exists, it is not overwritten.

-y

Read an identity file from INPUT or from standard input and output the corresponding recipient(s), one per line, with no comments.

--version

Print the version and exit.

EXAMPLES

Generate a new identity:

$ age-keygen
# created: 2021-01-02T15:30:45+01:00
# public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z
AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9

Write a new identity to key.txt:

$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Convert an identity to a recipient:

$ age-keygen -y key.txt
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

SEE ALSO

age(1)

AUTHORS

Filippo Valsorda age@filippo.io

  1. September 2022
  2. age-keygen(1)
age-1.1.1/doc/age-keygen.1.ronn000066400000000000000000000026621435753552200161050ustar00rootroot00000000000000age-keygen(1) -- generate age(1) key pairs ==================================================== ## SYNOPSIS `age-keygen` [`-o` ]
`age-keygen` `-y` [`-o` ] []
## DESCRIPTION `age-keygen` generates a new native age(1) key pair, and outputs the identity to standard output or to the file. The output includes the public key and the current time as comments. If the output is not going to a terminal, `age-keygen` prints the public key to standard error. ## OPTIONS * `-o`, `--output`=: Write the identity to instead of standard output. If already exists, it is not overwritten. * `-y`: Read an identity file from or from standard input and output the corresponding recipient(s), one per line, with no comments. * `--version`: Print the version and exit. ## EXAMPLES Generate a new identity: $ age-keygen # created: 2021-01-02T15:30:45+01:00 # public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9 Write a new identity to `key.txt`: $ age-keygen -o key.txt Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p Convert an identity to a recipient: $ age-keygen -y key.txt age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p ## SEE ALSO age(1) ## AUTHORS Filippo Valsorda age-1.1.1/doc/age.1000066400000000000000000000333231435753552200136500ustar00rootroot00000000000000.\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . .TH "AGE" "1" "September 2022" "" "" . .SH "NAME" \fBage\fR \- simple, modern, and secure file encryption . .SH "SYNOPSIS" \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] . .br \fBage\fR [\fB\-\-encrypt\fR] \fB\-\-passphrase\fR [\fB\-\-armor\fR] [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR] . .br \fBage\fR \fB\-\-decrypt\fR [\fB\-i\fR \fIPATH\fR | \fB\-j\fR \fIPLUGIN\fR]\.\.\. [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR] . .br . .SH "DESCRIPTION" \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\. . .P If \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\. . .P In \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\. . .P \fBage\fR encrypted files are binary and not malleable, with around 200 bytes of overhead per recipient, plus 16 bytes every 64KiB of plaintext\. . .SH "OPTIONS" . .TP \fB\-o\fR, \fB\-\-output\fR=\fIOUTPUT\fR Write encrypted or decrypted file to \fIOUTPUT\fR instead of standard output\. If \fIOUTPUT\fR already exists it will be overwritten\. . .IP If 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\. . .TP \fB\-\-version\fR Print the version and exit\. . .SS "Encryption options" . .TP \fB\-e\fR, \fB\-\-encrypt\fR Encrypt \fIINPUT\fR to \fIOUTPUT\fR\. This is the default\. . .TP \fB\-r\fR, \fB\-\-recipient\fR=\fIRECIPIENT\fR Encrypt to the explicitly specified \fIRECIPIENT\fR\. See the \fIRECIPIENTS AND IDENTITIES\fR section for possible recipient formats\. . .IP This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently\. . .TP \fB\-R\fR, \fB\-\-recipients\-file\fR=\fIPATH\fR Encrypt 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\. . .IP If \fIPATH\fR is \fB\-\fR, the recipients are read from standard input\. In this case, the \fIINPUT\fR argument must be specified\. . .IP This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently\. . .TP \fB\-p\fR, \fB\-\-passphrase\fR Encrypt with a passphrase, requested interactively from the terminal\. \fBage\fR will offer to auto\-generate a secure passphrase\. . .IP This option can\'t be used with other recipient flags\. . .TP \fB\-a\fR, \fB\-\-armor\fR Encrypt to an ASCII\-only "armored" encoding\. . .IP \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\. . .IP Decryption transparently detects and decodes ASCII armoring\. . .TP \fB\-i\fR, \fB\-\-identity\fR=\fIPATH\fR Encrypt 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\. . .IP For the format of \fIPATH\fR, see the definition of \fB\-i\fR/\fB\-\-identity\fR in the \fIDecryption options\fR section\. . .IP \fB\-e\fR/\fB\-\-encrypt\fR must be explicitly specified when using \fB\-i\fR/\fB\-\-identity\fR in encryption mode to avoid confusion\. . .TP \fB\-j\fR \fIPLUGIN\fR Encrypt using the data\-less \fIplugin\fR \fIPLUGIN\fR\. . .IP This 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\. . .IP \fB\-e\fR/\fB\-\-encrypt\fR must be explicitly specified when using \fB\-j\fR in encryption mode to avoid confusion\. . .SS "Decryption options" . .TP \fB\-d\fR, \fB\-\-decrypt\fR Decrypt \fIINPUT\fR to \fIOUTPUT\fR\. . .IP If \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\. . .IP ASCII armoring is transparently detected and decoded\. . .TP \fB\-i\fR, \fB\-\-identity\fR=\fIPATH\fR Decrypt using the \fIIDENTITIES\fR at \fIPATH\fR\. . .IP \fIPATH\fR may be one of the following: . .IP a\. A file listing \fIIDENTITIES\fR one per line\. Empty lines and lines starting with "\fB#\fR" are ignored as comments\. . .IP b\. 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\. . .IP c\. 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\. . .IP d\. "\fB\-\fR", causing one of the options above to be read from standard input\. In this case, the \fIINPUT\fR argument must be specified\. . .IP This 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\. . .TP \fB\-j\fR \fIPLUGIN\fR Decrypt using the data\-less \fIplugin\fR \fIPLUGIN\fR\. . .IP This 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\. . .SH "RECIPIENTS AND IDENTITIES" \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\. . .SS "Native X25519 keys" Native \fBage\fR key pairs are generated with age\-keygen(1), and provide small encodings and strong encryption based on X25519\. They are the recommended recipient type for most applications\. . .P A \fBRECIPIENT\fR encoding begins with \fBage1\fR and looks like the following: . .IP "" 4 . .nf age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh . .fi . .IP "" 0 . .P An \fBIDENTITY\fR encoding begins with \fBAGE\-SECRET\-KEY\-1\fR and looks like the following: . .IP "" 4 . .nf AGE\-SECRET\-KEY\-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ . .fi . .IP "" 0 . .P An encrypted file can\'t be linked to the native recipient it\'s encrypted to without access to the corresponding identity\. . .SS "SSH keys" As 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\. . .P A \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: . .IP "" 4 . .nf ssh\-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[\.\.\.]GU4BtElAbzh8= ssh\-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[\.\.\.]l1uZc31FGYMXa . .fi . .IP "" 0 . .P The comment at the end of the line, if present, is ignored\. . .P In 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\.) . .P An \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\. . .P An 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\. . .SS "Plugins" \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\.) . .P A \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\. . .P When 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\. . .P Plugins can be freely mixed with other plugins or natively supported keys\. . .P A 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\. . .P Plugins 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\. . .SH "EXIT STATUS" \fBage\fR will exit 0 if and only if encryption or decryption are successful for the full length of the input\. . .P If 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\. . .SH "BACKWARDS COMPATIBILITY" Files 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\. . .P If 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\. . .SH "EXAMPLES" Generate a new identity, encrypt data, and decrypt: . .IP "" 4 . .nf $ age\-keygen \-o key\.txt Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p $ tar cvz ~/data | age \-r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data\.tar\.gz\.age $ age \-d \-o data\.tar\.gz \-i key\.txt data\.tar\.gz\.age . .fi . .IP "" 0 . .P Encrypt \fBexample\.jpg\fR to multiple recipients and output to \fBexample\.jpg\.age\fR: . .IP "" 4 . .nf $ age \-o example\.jpg\.age \-r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \e \-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example\.jpg . .fi . .IP "" 0 . .P Encrypt to a list of recipients: . .IP "" 4 . .nf $ cat > recipients\.txt # Alice age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p # Bob age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg $ age \-R recipients\.txt example\.jpg > example\.jpg\.age . .fi . .IP "" 0 . .P Encrypt and decrypt a file using a passphrase: . .IP "" 4 . .nf $ age \-p secrets\.txt > secrets\.txt\.age Enter passphrase (leave empty to autogenerate a secure one): Using the autogenerated passphrase "release\-response\-step\-brand\-wrap\-ankle\-pair\-unusual\-sword\-train"\. $ age \-d secrets\.txt\.age > secrets\.txt Enter passphrase: . .fi . .IP "" 0 . .P Encrypt and decrypt with a passphrase\-protected identity file: . .IP "" 4 . .nf $ age\-keygen | age \-p > key\.age Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 Enter passphrase (leave empty to autogenerate a secure one): Using the autogenerated passphrase "hip\-roast\-boring\-snake\-mention\-east\-wasp\-honey\-input\-actress"\. $ age \-r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets\.txt > secrets\.txt\.age $ age \-d \-i key\.age secrets\.txt\.age > secrets\.txt Enter passphrase for identity file "key\.age": . .fi . .IP "" 0 . .P Encrypt and decrypt with an SSH public key: . .IP "" 4 . .nf $ age \-R ~/\.ssh/id_ed25519\.pub example\.jpg > example\.jpg\.age $ age \-d \-i ~/\.ssh/id_ed25519 example\.jpg\.age > example\.jpg . .fi . .IP "" 0 . .P Encrypt and decrypt with age\-plugin\-yubikey: . .IP "" 4 . .nf $ age\-plugin\-yubikey # run interactive setup, generate identity file and obtain recipient $ age \-r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets\.txt > secrets\.txt\.age $ age \-d \-i age\-yubikey\-identity\-388178f3\.txt secrets\.txt\.age . .fi . .IP "" 0 . .P Encrypt to the SSH keys of a GitHub user: . .IP "" 4 . .nf $ curl https://github\.com/benjojo\.keys | age \-R \- example\.jpg > example\.jpg\.age . .fi . .IP "" 0 . .SH "SEE ALSO" age\-keygen(1) . .SH "AUTHORS" Filippo Valsorda \fIage@filippo\.io\fR age-1.1.1/doc/age.1.html000066400000000000000000000474661435753552200146300ustar00rootroot00000000000000 age(1) - simple, modern, and secure file encryption
  1. age(1)
  2. age(1)

NAME

age - simple, modern, and secure file encryption

SYNOPSIS

age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
age --decrypt [-i PATH | -j PLUGIN]... [-o OUTPUT] [INPUT]

DESCRIPTION

age encrypts or decrypts INPUT to OUTPUT. The INPUT argument is optional and defaults to standard input. Only a single INPUT file may be specified. If -o is not specified, OUTPUT defaults to standard output.

If -p/--passphrase is specified, the file is encrypted with a passphrase requested interactively. Otherwise, it's encrypted to one or more RECIPIENTS specified with -r/--recipient or -R/--recipients-file. Every recipient can decrypt the file.

In -d/--decrypt mode, passphrase-encrypted files are detected automatically and the passphrase is requested interactively. Otherwise, one or more IDENTITIES specified with -i/--identity are used to decrypt the file.

age encrypted files are binary and not malleable, with around 200 bytes of overhead per recipient, plus 16 bytes every 64KiB of plaintext.

OPTIONS

-o, --output=OUTPUT

Write encrypted or decrypted file to OUTPUT instead of standard output. If OUTPUT already exists it will be overwritten.

If encrypting without --armor, age will refuse to output binary to a TTY. This can be forced by specifying - as OUTPUT.

--version

Print the version and exit.

Encryption options

-e, --encrypt

Encrypt INPUT to OUTPUT. This is the default.

-r, --recipient=RECIPIENT

Encrypt to the explicitly specified RECIPIENT. See the RECIPIENTS AND IDENTITIES section for possible recipient formats.

This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently.

-R, --recipients-file=PATH

Encrypt to the RECIPIENTS listed in the file at PATH, one per line. Empty lines and lines starting with # are ignored as comments.

If PATH is -, the recipients are read from standard input. In this case, the INPUT argument must be specified.

This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently.

-p, --passphrase

Encrypt with a passphrase, requested interactively from the terminal. age will offer to auto-generate a secure passphrase.

This option can't be used with other recipient flags.

-a, --armor

Encrypt to an ASCII-only "armored" encoding.

age armor is a strict version of PEM with type AGE ENCRYPTED FILE, canonical "strict" Base64, no headers, and no support for leading and trailing extra data.

Decryption transparently detects and decodes ASCII armoring.

-i, --identity=PATH

Encrypt to the RECIPIENTS corresponding to the IDENTITIES listed in the file at PATH. This is equivalent to converting the file at PATH to a recipients file with age-keygen -y and then passing that to -R/--recipients-file.

For the format of PATH, see the definition of -i/--identity in the Decryption options section.

-e/--encrypt must be explicitly specified when using -i/--identity in encryption mode to avoid confusion.

-j PLUGIN

Encrypt using the data-less plugin PLUGIN.

This is equivalent to using -i/--identity with a file that contains a single plugin IDENTITY that encodes no plugin-specific data.

-e/--encrypt must be explicitly specified when using -j in encryption mode to avoid confusion.

Decryption options

-d, --decrypt

Decrypt INPUT to OUTPUT.

If INPUT is passphrase encrypted, it will be automatically detected and the passphrase will be requested interactively. Otherwise, the IDENTITIES specified with -i/--identity are used.

ASCII armoring is transparently detected and decoded.

-i, --identity=PATH

Decrypt using the IDENTITIES at PATH.

PATH may be one of the following:

a. A file listing IDENTITIES one per line. Empty lines and lines starting with "#" are ignored as comments.

b. A passphrase encrypted age file, containing IDENTITIES 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.

c. 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 SSH keys section for more information, including supported key types.

d. "-", causing one of the options above to be read from standard input. In this case, the INPUT argument must be specified.

This 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 INPUT file is passphrase-encrypted and -i/--identity is specified.

-j PLUGIN

Decrypt using the data-less plugin PLUGIN.

This is equivalent to using -i/--identity with a file that contains a single plugin IDENTITY that encodes no plugin-specific data.

RECIPIENTS AND IDENTITIES

RECIPIENTS are public values, like a public key, that a file can be encrypted to. IDENTITIES are private values, like a private key, that allow decrypting a file encrypted to the corresponding RECIPIENT.

Native X25519 keys

Native age key pairs are generated with age-keygen(1), and provide small encodings and strong encryption based on X25519. They are the recommended recipient type for most applications.

A RECIPIENT encoding begins with age1 and looks like the following:

age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh

An IDENTITY encoding begins with AGE-SECRET-KEY-1 and looks like the following:

AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ

An encrypted file can't be linked to the native recipient it's encrypted to without access to the corresponding identity.

SSH keys

As a convenience feature, age 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.

A RECIPIENT encoding is an SSH public key in authorized_keys format (see the AUTHORIZED_KEYS FILE FORMAT section of sshd(8)), starting with ssh-rsa or ssh-ed25519, like the following:

ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8=
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa

The comment at the end of the line, if present, is ignored.

In recipient files passed to -R/--recipients-file, unsupported but valid SSH public keys are ignored with a warning, to facilitate using authorized_keys or GitHub .keys files. (See EXAMPLES.)

An IDENTITY is an SSH private key file passed individually to -i/--identity. Note that keys held on hardware tokens such as YubiKeys or accessed via ssh-agent(1) are not supported.

An encrypted file can be linked to the SSH public key it was encrypted to. This is so that age can identify the correct SSH private key before requesting its password, if any.

Plugins

age can be extended through plugins. A plugin is only loaded if a corresponding RECIPIENT or IDENTITY is specified. (Simply decrypting a file encrypted with a plugin will not cause it to load, for security reasons among others.)

A RECIPIENT for a plugin named example starts with age1example1, while an IDENTITY starts with AGE-PLUGIN-EXAMPLE-1. They both encode arbitrary plugin-specific data, and are generated by the plugin.

When either is specified, age searches for age-plugin-example in the PATH and executes it to perform the file header encryption or decryption. The plugin may request input from the user through age to complete the operation.

Plugins can be freely mixed with other plugins or natively supported keys.

A 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 RECIPIENT or even with a passphrase. Similarly, a plugin can encrypt a file such that it can be decrypted without the use of any plugin.

Plugins for which the IDENTITY/RECIPIENT distinction doesn't make sense (such as a symmetric encryption plugin) may generate only an IDENTITY and instruct the user to perform encryption with the -e/--encrypt and -i/--identity 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 -j flag.

EXIT STATUS

age will exit 0 if and only if encryption or decryption are successful for the full length of the input.

If 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.

BACKWARDS COMPATIBILITY

Files encrypted with a stable version (not alpha, beta, or release candidate) of age, or with any v1.0.0 beta or release candidate, will decrypt with any later version of the tool.

If 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.

EXAMPLES

Generate a new identity, encrypt data, and decrypt:

$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

$ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age

$ age -d -o data.tar.gz -i key.txt data.tar.gz.age

Encrypt example.jpg to multiple recipients and output to example.jpg.age:

$ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
    -r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg

Encrypt to a list of recipients:

$ cat > recipients.txt
# Alice
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
# Bob
age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg

$ age -R recipients.txt example.jpg > example.jpg.age

Encrypt and decrypt a file using a passphrase:

$ age -p secrets.txt > secrets.txt.age
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "release-response-step-brand-wrap-ankle-pair-unusual-sword-train".

$ age -d secrets.txt.age > secrets.txt
Enter passphrase:

Encrypt and decrypt with a passphrase-protected identity file:

$ age-keygen | age -p > key.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".

$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age

$ age -d -i key.age secrets.txt.age > secrets.txt
Enter passphrase for identity file "key.age":

Encrypt and decrypt with an SSH public key:

$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age

$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg

Encrypt and decrypt with age-plugin-yubikey:

$ age-plugin-yubikey # run interactive setup, generate identity file and obtain recipient

$ age -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets.txt > secrets.txt.age

$ age -d -i age-yubikey-identity-388178f3.txt secrets.txt.age

Encrypt to the SSH keys of a GitHub user:

$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age

SEE ALSO

age-keygen(1)

AUTHORS

Filippo Valsorda age@filippo.io

  1. September 2022
  2. age(1)
age-1.1.1/doc/age.1.ronn000066400000000000000000000310611435753552200146200ustar00rootroot00000000000000age(1) -- simple, modern, and secure file encryption ==================================================== ## SYNOPSIS `age` [`--encrypt`] (`-r` | `-R` )... [`--armor`] [`-o` ] []
`age` [`--encrypt`] `--passphrase` [`--armor`] [`-o` ] []
`age` `--decrypt` [`-i` | `-j` ]... [`-o` ] []
## DESCRIPTION `age` encrypts or decrypts to . The argument is optional and defaults to standard input. Only a single file may be specified. If `-o` is not specified, defaults to standard output. If `-p`/`--passphrase` is specified, the file is encrypted with a passphrase requested interactively. Otherwise, it's encrypted to one or more [RECIPIENTS][RECIPIENTS AND IDENTITIES] specified with `-r`/`--recipient` or `-R`/`--recipients-file`. Every recipient can decrypt the file. In `-d`/`--decrypt` mode, passphrase-encrypted files are detected automatically and the passphrase is requested interactively. Otherwise, one or more [IDENTITIES][RECIPIENTS AND IDENTITIES] specified with `-i`/`--identity` are used to decrypt the file. `age` encrypted files are binary and not malleable, with around 200 bytes of overhead per recipient, plus 16 bytes every 64KiB of plaintext. ## OPTIONS * `-o`, `--output`=: Write encrypted or decrypted file to instead of standard output. If already exists it will be overwritten. If encrypting without `--armor`, `age` will refuse to output binary to a TTY. This can be forced by specifying `-` as . * `--version`: Print the version and exit. ### Encryption options * `-e`, `--encrypt`: Encrypt to . This is the default. * `-r`, `--recipient`=: Encrypt to the explicitly specified . See the [RECIPIENTS AND IDENTITIES][] section for possible recipient formats. This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently. * `-R`, `--recipients-file`=: Encrypt to the [RECIPIENTS][RECIPIENTS AND IDENTITIES] listed in the file at , one per line. Empty lines and lines starting with `#` are ignored as comments. If is `-`, the recipients are read from standard input. In this case, the argument must be specified. This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently. * `-p`, `--passphrase`: Encrypt with a passphrase, requested interactively from the terminal. `age` will offer to auto-generate a secure passphrase. This option can't be used with other recipient flags. * `-a`, `--armor`: Encrypt to an ASCII-only "armored" encoding. `age` armor is a strict version of PEM with type `AGE ENCRYPTED FILE`, canonical "strict" Base64, no headers, and no support for leading and trailing extra data. Decryption transparently detects and decodes ASCII armoring. * `-i`, `--identity`=: Encrypt to the [RECIPIENTS][RECIPIENTS AND IDENTITIES] corresponding to the [IDENTITIES][RECIPIENTS AND IDENTITIES] listed in the file at . This is equivalent to converting the file at to a recipients file with `age-keygen -y` and then passing that to `-R`/`--recipients-file`. For the format of , see the definition of `-i`/`--identity` in the [Decryption options][] section. `-e`/`--encrypt` must be explicitly specified when using `-i`/`--identity` in encryption mode to avoid confusion. * `-j` : Encrypt using the data-less [plugin][Plugins] . This is equivalent to using `-i`/`--identity` with a file that contains a single plugin `IDENTITY` that encodes no plugin-specific data. `-e`/`--encrypt` must be explicitly specified when using `-j` in encryption mode to avoid confusion. ### Decryption options * `-d`, `--decrypt`: Decrypt to . If is passphrase encrypted, it will be automatically detected and the passphrase will be requested interactively. Otherwise, the [IDENTITIES][RECIPIENTS AND IDENTITIES] specified with `-i`/`--identity` are used. ASCII armoring is transparently detected and decoded. * `-i`, `--identity`=: Decrypt using the [IDENTITIES][RECIPIENTS AND IDENTITIES] at . may be one of the following: a\. A file listing [IDENTITIES][RECIPIENTS AND IDENTITIES] one per line. Empty lines and lines starting with "`#`" are ignored as comments. b\. A passphrase encrypted age file, containing [IDENTITIES][RECIPIENTS AND IDENTITIES] 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. c\. 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 [SSH keys][] section for more information, including supported key types. d\. "`-`", causing one of the options above to be read from standard input. In this case, the argument must be specified. This 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 file is passphrase-encrypted and `-i`/`--identity` is specified. * `-j` : Decrypt using the data-less [plugin][Plugins] . This is equivalent to using `-i`/`--identity` with a file that contains a single plugin `IDENTITY` that encodes no plugin-specific data. ## RECIPIENTS AND IDENTITIES `RECIPIENTS` are public values, like a public key, that a file can be encrypted to. `IDENTITIES` are private values, like a private key, that allow decrypting a file encrypted to the corresponding `RECIPIENT`. ### Native X25519 keys Native `age` key pairs are generated with age-keygen(1), and provide small encodings and strong encryption based on X25519. They are the recommended recipient type for most applications. A `RECIPIENT` encoding begins with `age1` and looks like the following: age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh An `IDENTITY` encoding begins with `AGE-SECRET-KEY-1` and looks like the following: AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ An encrypted file can't be linked to the native recipient it's encrypted to without access to the corresponding identity. ### SSH keys As a convenience feature, `age` 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. A `RECIPIENT` encoding is an SSH public key in `authorized_keys` format (see the `AUTHORIZED_KEYS FILE FORMAT` section of sshd(8)), starting with `ssh-rsa` or `ssh-ed25519`, like the following: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa The comment at the end of the line, if present, is ignored. In recipient files passed to `-R`/`--recipients-file`, unsupported but valid SSH public keys are ignored with a warning, to facilitate using `authorized_keys` or GitHub `.keys` files. (See [EXAMPLES][].) An `IDENTITY` is an SSH private key _file_ passed individually to `-i`/`--identity`. Note that keys held on hardware tokens such as YubiKeys or accessed via ssh-agent(1) are not supported. An encrypted file _can_ be linked to the SSH public key it was encrypted to. This is so that `age` can identify the correct SSH private key before requesting its password, if any. ### Plugins `age` can be extended through plugins. A plugin is only loaded if a corresponding `RECIPIENT` or `IDENTITY` is specified. (Simply decrypting a file encrypted with a plugin will not cause it to load, for security reasons among others.) A `RECIPIENT` for a plugin named `example` starts with `age1example1`, while an `IDENTITY` starts with `AGE-PLUGIN-EXAMPLE-1`. They both encode arbitrary plugin-specific data, and are generated by the plugin. When either is specified, `age` searches for `age-plugin-example` in the PATH and executes it to perform the file header encryption or decryption. The plugin may request input from the user through `age` to complete the operation. Plugins can be freely mixed with other plugins or natively supported keys. A 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 `RECIPIENT` or even with a passphrase. Similarly, a plugin can encrypt a file such that it can be decrypted without the use of any plugin. Plugins for which the `IDENTITY`/`RECIPIENT` distinction doesn't make sense (such as a symmetric encryption plugin) may generate only an `IDENTITY` and instruct the user to perform encryption with the `-e`/`--encrypt` and `-i`/`--identity` 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 `-j` flag. ## EXIT STATUS `age` will exit 0 if and only if encryption or decryption are successful for the full length of the input. If 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. ## BACKWARDS COMPATIBILITY Files encrypted with a stable version (not alpha, beta, or release candidate) of `age`, or with any v1.0.0 beta or release candidate, will decrypt with any later version of the tool. If 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. ## EXAMPLES Generate a new identity, encrypt data, and decrypt: $ age-keygen -o key.txt Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p $ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age $ age -d -o data.tar.gz -i key.txt data.tar.gz.age Encrypt `example.jpg` to multiple recipients and output to `example.jpg.age`: $ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \ -r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg Encrypt to a list of recipients: $ cat > recipients.txt # Alice age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p # Bob age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg $ age -R recipients.txt example.jpg > example.jpg.age Encrypt and decrypt a file using a passphrase: $ age -p secrets.txt > secrets.txt.age Enter passphrase (leave empty to autogenerate a secure one): Using the autogenerated passphrase "release-response-step-brand-wrap-ankle-pair-unusual-sword-train". $ age -d secrets.txt.age > secrets.txt Enter passphrase: Encrypt and decrypt with a passphrase-protected identity file: $ age-keygen | age -p > key.age Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 Enter passphrase (leave empty to autogenerate a secure one): Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress". $ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age $ age -d -i key.age secrets.txt.age > secrets.txt Enter passphrase for identity file "key.age": Encrypt and decrypt with an SSH public key: $ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age $ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg Encrypt and decrypt with age-plugin-yubikey: $ age-plugin-yubikey # run interactive setup, generate identity file and obtain recipient $ age -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets.txt > secrets.txt.age $ age -d -i age-yubikey-identity-388178f3.txt secrets.txt.age Encrypt to the SSH keys of a GitHub user: $ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age ## SEE ALSO age-keygen(1) ## AUTHORS Filippo Valsorda age-1.1.1/go.mod000066400000000000000000000002331435753552200133650ustar00rootroot00000000000000module filippo.io/age go 1.19 require ( filippo.io/edwards25519 v1.0.0 golang.org/x/crypto v0.4.0 golang.org/x/sys v0.3.0 golang.org/x/term v0.3.0 ) age-1.1.1/go.sum000066400000000000000000000011621435753552200134140ustar00rootroot00000000000000filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8= golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= age-1.1.1/internal/000077500000000000000000000000001435753552200140755ustar00rootroot00000000000000age-1.1.1/internal/bech32/000077500000000000000000000000001435753552200151435ustar00rootroot00000000000000age-1.1.1/internal/bech32/bech32.go000066400000000000000000000116261435753552200165460ustar00rootroot00000000000000// Copyright (c) 2017 Takatoshi Nakagawa // Copyright (c) 2019 The age Authors // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. // Package bech32 is a modified version of the reference implementation of BIP173. package bech32 import ( "fmt" "strings" ) var charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" var generator = []uint32{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3} func polymod(values []byte) uint32 { chk := uint32(1) for _, v := range values { top := chk >> 25 chk = (chk & 0x1ffffff) << 5 chk = chk ^ uint32(v) for i := 0; i < 5; i++ { bit := top >> i & 1 if bit == 1 { chk ^= generator[i] } } } return chk } func hrpExpand(hrp string) []byte { h := []byte(strings.ToLower(hrp)) var ret []byte for _, c := range h { ret = append(ret, c>>5) } ret = append(ret, 0) for _, c := range h { ret = append(ret, c&31) } return ret } func verifyChecksum(hrp string, data []byte) bool { return polymod(append(hrpExpand(hrp), data...)) == 1 } func createChecksum(hrp string, data []byte) []byte { values := append(hrpExpand(hrp), data...) values = append(values, []byte{0, 0, 0, 0, 0, 0}...) mod := polymod(values) ^ 1 ret := make([]byte, 6) for p := range ret { shift := 5 * (5 - p) ret[p] = byte(mod>>shift) & 31 } return ret } func convertBits(data []byte, frombits, tobits byte, pad bool) ([]byte, error) { var ret []byte acc := uint32(0) bits := byte(0) maxv := byte(1<>frombits != 0 { return nil, fmt.Errorf("invalid data range: data[%d]=%d (frombits=%d)", idx, value, frombits) } acc = acc<= tobits { bits -= tobits ret = append(ret, byte(acc>>bits)&maxv) } } if pad { if bits > 0 { ret = append(ret, byte(acc<<(tobits-bits))&maxv) } } else if bits >= frombits { return nil, fmt.Errorf("illegal zero padding") } else if byte(acc<<(tobits-bits))&maxv != 0 { return nil, fmt.Errorf("non-zero padding") } return ret, nil } // Encode encodes the HRP and a bytes slice to Bech32. If the HRP is uppercase, // the output will be uppercase. func Encode(hrp string, data []byte) (string, error) { values, err := convertBits(data, 8, 5, true) if err != nil { return "", err } if len(hrp) < 1 { return "", fmt.Errorf("invalid HRP: %q", hrp) } for p, c := range hrp { if c < 33 || c > 126 { return "", fmt.Errorf("invalid HRP character: hrp[%d]=%d", p, c) } } if strings.ToUpper(hrp) != hrp && strings.ToLower(hrp) != hrp { return "", fmt.Errorf("mixed case HRP: %q", hrp) } lower := strings.ToLower(hrp) == hrp hrp = strings.ToLower(hrp) var ret strings.Builder ret.WriteString(hrp) ret.WriteString("1") for _, p := range values { ret.WriteByte(charset[p]) } for _, p := range createChecksum(hrp, values) { ret.WriteByte(charset[p]) } if lower { return ret.String(), nil } return strings.ToUpper(ret.String()), nil } // Decode decodes a Bech32 string. If the string is uppercase, the HRP will be uppercase. func Decode(s string) (hrp string, data []byte, err error) { if strings.ToLower(s) != s && strings.ToUpper(s) != s { return "", nil, fmt.Errorf("mixed case") } pos := strings.LastIndex(s, "1") if pos < 1 || pos+7 > len(s) { return "", nil, fmt.Errorf("separator '1' at invalid position: pos=%d, len=%d", pos, len(s)) } hrp = s[:pos] for p, c := range hrp { if c < 33 || c > 126 { return "", nil, fmt.Errorf("invalid character human-readable part: s[%d]=%d", p, c) } } s = strings.ToLower(s) for p, c := range s[pos+1:] { d := strings.IndexRune(charset, c) if d == -1 { return "", nil, fmt.Errorf("invalid character data part: s[%d]=%v", p, c) } data = append(data, byte(d)) } if !verifyChecksum(hrp, data) { return "", nil, fmt.Errorf("invalid checksum") } data, err = convertBits(data[:len(data)-6], 5, 8, false) if err != nil { return "", nil, err } return hrp, data, nil } age-1.1.1/internal/bech32/bech32_test.go000066400000000000000000000116131435753552200176010ustar00rootroot00000000000000// Copyright (c) 2013-2017 The btcsuite developers // Copyright (c) 2016-2017 The Lightning Network Developers // Copyright (c) 2019 The age Authors // // Permission to use, copy, modify, and distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. package bech32_test import ( "strings" "testing" "filippo.io/age/internal/bech32" ) func TestBech32(t *testing.T) { tests := []struct { str string valid bool }{ {"A12UEL5L", true}, // empty {"a12uel5l", true}, {"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", true}, {"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", true}, {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", true}, {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", true}, // invalid checksum {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", false}, // invalid character (space) in hrp {"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", false}, {"split1cheo2y9e2w", false}, // invalid character (o) in data part {"split1a2y9w", false}, // too short data part {"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // empty hrp // invalid character (DEL) in hrp {"spl" + string(rune(127)) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // long vectors that we do accept despite the spec, see Issue 453 {"long10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qfcsvr0", true}, {"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", true}, // BIP 173 invalid vectors. {"pzry9x0s0muk", false}, {"1pzry9x0s0muk", false}, {"x1b4n0q5v", false}, {"li1dgmt3", false}, {"de1lg7wt\xff", false}, {"A1G7SGD8", false}, {"10a06t8", false}, {"1qzzfhee", false}, } for _, test := range tests { str := test.str hrp, decoded, err := bech32.Decode(str) if !test.valid { // Invalid string decoding should result in error. if err == nil { t.Errorf("expected decoding to fail for invalid string %v", test.str) } continue } // Valid string decoding should result in no error. if err != nil { t.Errorf("expected string to be valid bech32: %v", err) } // Check that it encodes to the same string. encoded, err := bech32.Encode(hrp, decoded) if err != nil { t.Errorf("encoding failed: %v", err) } if encoded != str { t.Errorf("expected data to encode to %v, but got %v", str, encoded) } // Flip a bit in the string an make sure it is caught. pos := strings.LastIndexAny(str, "1") flipped := str[:pos+1] + string((str[pos+1] ^ 1)) + str[pos+2:] if _, _, err = bech32.Decode(flipped); err == nil { t.Error("expected decoding to fail") } } } age-1.1.1/internal/format/000077500000000000000000000000001435753552200153655ustar00rootroot00000000000000age-1.1.1/internal/format/format.go000066400000000000000000000175371435753552200172210ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package format implements the age file format. package format import ( "bufio" "bytes" "encoding/base64" "errors" "fmt" "io" "strings" ) type Header struct { Recipients []*Stanza MAC []byte } // Stanza is assignable to age.Stanza, and if this package is made public, // age.Stanza can be made a type alias of this type. type Stanza struct { Type string Args []string Body []byte } var b64 = base64.RawStdEncoding.Strict() func DecodeString(s string) ([]byte, error) { // CR and LF are ignored by DecodeString, but we don't want any malleability. if strings.ContainsAny(s, "\n\r") { return nil, errors.New(`unexpected newline character`) } return b64.DecodeString(s) } var EncodeToString = b64.EncodeToString const ColumnsPerLine = 64 const BytesPerLine = ColumnsPerLine / 4 * 3 // NewWrappedBase64Encoder returns a WrappedBase64Encoder that writes to dst. func NewWrappedBase64Encoder(enc *base64.Encoding, dst io.Writer) *WrappedBase64Encoder { w := &WrappedBase64Encoder{dst: dst} w.enc = base64.NewEncoder(enc, WriterFunc(w.writeWrapped)) return w } type WriterFunc func(p []byte) (int, error) func (f WriterFunc) Write(p []byte) (int, error) { return f(p) } // WrappedBase64Encoder is a standard base64 encoder that inserts an LF // character every ColumnsPerLine bytes. It does not insert a newline neither at // the beginning nor at the end of the stream, but it ensures the last line is // shorter than ColumnsPerLine, which means it might be empty. type WrappedBase64Encoder struct { enc io.WriteCloser dst io.Writer written int buf bytes.Buffer } func (w *WrappedBase64Encoder) Write(p []byte) (int, error) { return w.enc.Write(p) } func (w *WrappedBase64Encoder) Close() error { return w.enc.Close() } func (w *WrappedBase64Encoder) writeWrapped(p []byte) (int, error) { if w.buf.Len() != 0 { panic("age: internal error: non-empty WrappedBase64Encoder.buf") } for len(p) > 0 { toWrite := ColumnsPerLine - (w.written % ColumnsPerLine) if toWrite > len(p) { toWrite = len(p) } n, _ := w.buf.Write(p[:toWrite]) w.written += n p = p[n:] if w.written%ColumnsPerLine == 0 { w.buf.Write([]byte("\n")) } } if _, err := w.buf.WriteTo(w.dst); err != nil { // We always return n = 0 on error because it's hard to work back to the // input length that ended up written out. Not ideal, but Write errors // are not recoverable anyway. return 0, err } return len(p), nil } // LastLineIsEmpty returns whether the last output line was empty, either // because no input was written, or because a multiple of BytesPerLine was. // // Calling LastLineIsEmpty before Close is meaningless. func (w *WrappedBase64Encoder) LastLineIsEmpty() bool { return w.written%ColumnsPerLine == 0 } const intro = "age-encryption.org/v1\n" var stanzaPrefix = []byte("->") var footerPrefix = []byte("---") func (r *Stanza) Marshal(w io.Writer) error { if _, err := w.Write(stanzaPrefix); err != nil { return err } for _, a := range append([]string{r.Type}, r.Args...) { if _, err := io.WriteString(w, " "+a); err != nil { return err } } if _, err := io.WriteString(w, "\n"); err != nil { return err } ww := NewWrappedBase64Encoder(b64, w) if _, err := ww.Write(r.Body); err != nil { return err } if err := ww.Close(); err != nil { return err } _, err := io.WriteString(w, "\n") return err } func (h *Header) MarshalWithoutMAC(w io.Writer) error { if _, err := io.WriteString(w, intro); err != nil { return err } for _, r := range h.Recipients { if err := r.Marshal(w); err != nil { return err } } _, err := fmt.Fprintf(w, "%s", footerPrefix) return err } func (h *Header) Marshal(w io.Writer) error { if err := h.MarshalWithoutMAC(w); err != nil { return err } mac := b64.EncodeToString(h.MAC) _, err := fmt.Fprintf(w, " %s\n", mac) return err } type StanzaReader struct { r *bufio.Reader err error } func NewStanzaReader(r *bufio.Reader) *StanzaReader { return &StanzaReader{r: r} } func (r *StanzaReader) ReadStanza() (s *Stanza, err error) { // Read errors are unrecoverable. if r.err != nil { return nil, r.err } defer func() { r.err = err }() s = &Stanza{} line, err := r.r.ReadBytes('\n') if err != nil { return nil, fmt.Errorf("failed to read line: %w", err) } if !bytes.HasPrefix(line, stanzaPrefix) { return nil, fmt.Errorf("malformed stanza opening line: %q", line) } prefix, args := splitArgs(line) if prefix != string(stanzaPrefix) || len(args) < 1 { return nil, fmt.Errorf("malformed stanza: %q", line) } for _, a := range args { if !isValidString(a) { return nil, fmt.Errorf("malformed stanza: %q", line) } } s.Type = args[0] s.Args = args[1:] for { line, err := r.r.ReadBytes('\n') if err != nil { return nil, fmt.Errorf("failed to read line: %w", err) } b, err := DecodeString(strings.TrimSuffix(string(line), "\n")) if err != nil { if bytes.HasPrefix(line, footerPrefix) || bytes.HasPrefix(line, stanzaPrefix) { return 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) } return nil, errorf("malformed body line %q: %v", line, err) } if len(b) > BytesPerLine { return nil, errorf("malformed body line %q: too long", line) } s.Body = append(s.Body, b...) if len(b) < BytesPerLine { // A stanza body always ends with a short line. return s, nil } } } type ParseError struct { err error } func (e *ParseError) Error() string { return "parsing age header: " + e.err.Error() } func (e *ParseError) Unwrap() error { return e.err } func errorf(format string, a ...interface{}) error { return &ParseError{fmt.Errorf(format, a...)} } // Parse returns the header and a Reader that begins at the start of the // payload. func Parse(input io.Reader) (*Header, io.Reader, error) { h := &Header{} rr := bufio.NewReader(input) line, err := rr.ReadString('\n') if err != nil { return nil, nil, errorf("failed to read intro: %w", err) } if line != intro { return nil, nil, errorf("unexpected intro: %q", line) } sr := NewStanzaReader(rr) for { peek, err := rr.Peek(len(footerPrefix)) if err != nil { return nil, nil, errorf("failed to read header: %w", err) } if bytes.Equal(peek, footerPrefix) { line, err := rr.ReadBytes('\n') if err != nil { return nil, nil, fmt.Errorf("failed to read header: %w", err) } prefix, args := splitArgs(line) if prefix != string(footerPrefix) || len(args) != 1 { return nil, nil, errorf("malformed closing line: %q", line) } h.MAC, err = DecodeString(args[0]) if err != nil || len(h.MAC) != 32 { return nil, nil, errorf("malformed closing line %q: %v", line, err) } break } s, err := sr.ReadStanza() if err != nil { return nil, nil, fmt.Errorf("failed to parse header: %w", err) } h.Recipients = append(h.Recipients, s) } // If input is a bufio.Reader, rr might be equal to input because // bufio.NewReader short-circuits. In this case we can just return it (and // we would end up reading the buffer twice if we prepended the peek below). if rr == input { return h, rr, nil } // Otherwise, unwind the bufio overread and return the unbuffered input. buf, err := rr.Peek(rr.Buffered()) if err != nil { return nil, nil, errorf("internal error: %v", err) } payload := io.MultiReader(bytes.NewReader(buf), input) return h, payload, nil } func splitArgs(line []byte) (string, []string) { l := strings.TrimSuffix(string(line), "\n") parts := strings.Split(l, " ") return parts[0], parts[1:] } func isValidString(s string) bool { if len(s) == 0 { return false } for _, c := range s { if c < 33 || c > 126 { return false } } return true } age-1.1.1/internal/format/format_test.go000066400000000000000000000037171435753552200202530ustar00rootroot00000000000000// Copyright 2021 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build go1.18 // +build go1.18 package format_test import ( "bytes" "io" "os" "path/filepath" "testing" "filippo.io/age/internal/format" ) func TestStanzaMarshal(t *testing.T) { s := &format.Stanza{ Type: "test", Args: []string{"1", "2", "3"}, Body: nil, // empty } buf := &bytes.Buffer{} s.Marshal(buf) if exp := "-> test 1 2 3\n\n"; buf.String() != exp { t.Errorf("wrong empty stanza encoding: expected %q, got %q", exp, buf.String()) } buf.Reset() s.Body = []byte("AAA") s.Marshal(buf) if exp := "-> test 1 2 3\nQUFB\n"; buf.String() != exp { t.Errorf("wrong normal stanza encoding: expected %q, got %q", exp, buf.String()) } buf.Reset() s.Body = bytes.Repeat([]byte("A"), format.BytesPerLine) s.Marshal(buf) if exp := "-> test 1 2 3\nQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\n\n"; buf.String() != exp { t.Errorf("wrong 64 columns stanza encoding: expected %q, got %q", exp, buf.String()) } } func FuzzMalleability(f *testing.F) { tests, err := filepath.Glob("../../testdata/testkit/*") if err != nil { f.Fatal(err) } for _, test := range tests { contents, err := os.ReadFile(test) if err != nil { f.Fatal(err) } _, contents, ok := bytes.Cut(contents, []byte("\n\n")) if !ok { f.Fatal("testkit file without header") } f.Add(contents) } f.Fuzz(func(t *testing.T, data []byte) { h, payload, err := format.Parse(bytes.NewReader(data)) if err != nil { if h != nil { t.Error("h != nil on error") } if payload != nil { t.Error("payload != nil on error") } t.Skip() } w := &bytes.Buffer{} if err := h.Marshal(w); err != nil { t.Fatal(err) } if _, err := io.Copy(w, payload); err != nil { t.Fatal(err) } if !bytes.Equal(w.Bytes(), data) { t.Error("Marshal output different from input") } }) } age-1.1.1/internal/plugin/000077500000000000000000000000001435753552200153735ustar00rootroot00000000000000age-1.1.1/internal/plugin/client.go000066400000000000000000000265551435753552200172150ustar00rootroot00000000000000// Copyright 2021 Google LLC // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd // Package plugin implements the age plugin protocol. package plugin import ( "bufio" "bytes" "fmt" "io" "os" "strconv" "strings" "time" exec "golang.org/x/sys/execabs" "filippo.io/age" "filippo.io/age/internal/bech32" "filippo.io/age/internal/format" ) type Recipient struct { name string encoding string ui *ClientUI // identity is true when encoding is an identity string. identity bool } var _ age.Recipient = &Recipient{} func NewRecipient(s string, ui *ClientUI) (*Recipient, error) { hrp, _, err := bech32.Decode(s) if err != nil { return nil, fmt.Errorf("invalid recipient encoding %q: %v", s, err) } if !strings.HasPrefix(hrp, "age1") { return nil, fmt.Errorf("not a plugin recipient %q: %v", s, err) } name := strings.TrimPrefix(hrp, "age1") return &Recipient{ name: name, encoding: s, ui: ui, }, nil } // Name returns the plugin name, which is used in the recipient ("age1name1...") // and identity ("AGE-PLUGIN-NAME-1...") encodings, as well as in the plugin // binary name ("age-plugin-name"). func (r *Recipient) Name() string { return r.name } func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) { defer func() { if err != nil { err = fmt.Errorf("%s plugin: %w", r.name, err) } }() conn, err := openClientConnection(r.name, "recipient-v1") if err != nil { return nil, fmt.Errorf("couldn't start plugin: %v", err) } defer conn.Close() // Phase 1: client sends recipient or identity and file key addType := "add-recipient" if r.identity { addType = "add-identity" } if err := writeStanza(conn, addType, r.encoding); err != nil { return nil, err } if err := writeStanzaWithBody(conn, "wrap-file-key", fileKey); err != nil { return nil, err } if err := writeStanza(conn, "done"); err != nil { return nil, err } // Phase 2: plugin responds with stanzas sr := format.NewStanzaReader(bufio.NewReader(conn)) ReadLoop: for { s, err := r.ui.readStanza(r.name, sr) if err != nil { return nil, err } switch s.Type { case "recipient-stanza": if len(s.Args) < 2 { return nil, fmt.Errorf("malformed recipient stanza: unexpected argument count") } n, err := strconv.Atoi(s.Args[0]) if err != nil { return nil, fmt.Errorf("malformed recipient stanza: invalid index") } // We only send a single file key, so the index must be 0. if n != 0 { return nil, fmt.Errorf("malformed recipient stanza: unexpected index") } stanzas = append(stanzas, &age.Stanza{ Type: s.Args[1], Args: s.Args[2:], Body: s.Body, }) if err := writeStanza(conn, "ok"); err != nil { return nil, err } case "error": if err := writeStanza(conn, "ok"); err != nil { return nil, err } return nil, fmt.Errorf("%s", s.Body) case "done": break ReadLoop default: if ok, err := r.ui.handle(r.name, conn, s); err != nil { return nil, err } else if !ok { if err := writeStanza(conn, "unsupported"); err != nil { return nil, err } } } } if len(stanzas) == 0 { return nil, fmt.Errorf("received zero recipient stanzas") } return stanzas, nil } type Identity struct { name string encoding string ui *ClientUI } var _ age.Identity = &Identity{} func NewIdentity(s string, ui *ClientUI) (*Identity, error) { hrp, _, err := bech32.Decode(s) if err != nil { return nil, fmt.Errorf("invalid identity encoding: %v", err) } if !strings.HasPrefix(hrp, "AGE-PLUGIN-") || !strings.HasSuffix(hrp, "-") { return nil, fmt.Errorf("not a plugin identity: %v", err) } name := strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-") name = strings.ToLower(name) return &Identity{ name: name, encoding: s, ui: ui, }, nil } func NewIdentityWithoutData(name string, ui *ClientUI) (*Identity, error) { s, err := bech32.Encode("AGE-PLUGIN-"+strings.ToUpper(name)+"-", nil) if err != nil { return nil, err } return &Identity{ name: name, encoding: s, ui: ui, }, nil } // Name returns the plugin name, which is used in the recipient ("age1name1...") // and identity ("AGE-PLUGIN-NAME-1...") encodings, as well as in the plugin // binary name ("age-plugin-name"). func (i *Identity) Name() string { return i.name } // Recipient returns a Recipient wrapping this identity. When that Recipient is // used to encrypt a file key, the identity encoding is provided as-is to the // plugin, which is expected to support encrypting to identities. func (i *Identity) Recipient() *Recipient { return &Recipient{ name: i.name, encoding: i.encoding, identity: true, ui: i.ui, } } func (i *Identity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) { defer func() { if err != nil { err = fmt.Errorf("%s plugin: %w", i.name, err) } }() conn, err := openClientConnection(i.name, "identity-v1") if err != nil { return nil, fmt.Errorf("couldn't start plugin: %v", err) } defer conn.Close() // Phase 1: client sends the plugin the identity string and the stanzas if err := writeStanza(conn, "add-identity", i.encoding); err != nil { return nil, err } for _, rs := range stanzas { s := &format.Stanza{ Type: "recipient-stanza", Args: append([]string{"0", rs.Type}, rs.Args...), Body: rs.Body, } if err := s.Marshal(conn); err != nil { return nil, err } } if err := writeStanza(conn, "done"); err != nil { return nil, err } // Phase 2: plugin responds with various commands and a file key sr := format.NewStanzaReader(bufio.NewReader(conn)) ReadLoop: for { s, err := i.ui.readStanza(i.name, sr) if err != nil { return nil, err } switch s.Type { case "file-key": if len(s.Args) != 1 { return nil, fmt.Errorf("malformed file-key stanza: unexpected arguments count") } n, err := strconv.Atoi(s.Args[0]) if err != nil { return nil, fmt.Errorf("malformed file-key stanza: invalid index") } // We only send a single file key, so the index must be 0. if n != 0 { return nil, fmt.Errorf("malformed file-key stanza: unexpected index") } if fileKey != nil { return nil, fmt.Errorf("received duplicated file-key stanza") } fileKey = s.Body if err := writeStanza(conn, "ok"); err != nil { return nil, err } case "error": if err := writeStanza(conn, "ok"); err != nil { return nil, err } return nil, fmt.Errorf("%s", s.Body) case "done": break ReadLoop default: if ok, err := i.ui.handle(i.name, conn, s); err != nil { return nil, err } else if !ok { if err := writeStanza(conn, "unsupported"); err != nil { return nil, err } } } } if fileKey == nil { return nil, age.ErrIncorrectIdentity } return fileKey, nil } // ClientUI holds callbacks that will be invoked by (Un)Wrap if the plugin // wishes to interact with the user. If any of them is nil or returns an error, // failure will be reported to the plugin, but note that the error is otherwise // discarded. Implementations are encouraged to display errors to the user // before returning them. type ClientUI struct { // DisplayMessage displays the message, which is expected to have lowercase // initials and no final period. DisplayMessage func(name, message string) error // RequestValue requests a secret or public input, with the provided prompt. RequestValue func(name, prompt string, secret bool) (string, error) // Confirm requests a confirmation with the provided prompt. The yes and no // value are the choices provided to the user. no may be empty. The return // value indicates whether the user selected the yes or no option. Confirm func(name, prompt, yes, no string) (choseYes bool, err error) // WaitTimer is invoked once (Un)Wrap has been waiting for 5 seconds on the // plugin, for example because the plugin is waiting for an external event // (e.g. a hardware token touch). Unlike the other callbacks, WaitTimer runs // in a separate goroutine, and if missing it's simply ignored. WaitTimer func(name string) } func (c *ClientUI) handle(name string, conn *clientConnection, s *format.Stanza) (ok bool, err error) { switch s.Type { case "msg": if c.DisplayMessage == nil { return true, writeStanza(conn, "fail") } if err := c.DisplayMessage(name, string(s.Body)); err != nil { return true, writeStanza(conn, "fail") } return true, writeStanza(conn, "ok") case "request-secret", "request-public": if c.RequestValue == nil { return true, writeStanza(conn, "fail") } secret, err := c.RequestValue(name, string(s.Body), s.Type == "request-secret") if err != nil { return true, writeStanza(conn, "fail") } return true, writeStanzaWithBody(conn, "ok", []byte(secret)) case "confirm": if len(s.Args) != 1 && len(s.Args) != 2 { return true, fmt.Errorf("malformed confirm stanza: unexpected number of arguments") } if c.Confirm == nil { return true, writeStanza(conn, "fail") } yes, err := format.DecodeString(s.Args[0]) if err != nil { return true, fmt.Errorf("malformed confirm stanza: invalid YES option encoding") } var no []byte if len(s.Args) == 2 { no, err = format.DecodeString(s.Args[1]) if err != nil { return true, fmt.Errorf("malformed confirm stanza: invalid NO option encoding") } } choseYes, err := c.Confirm(name, string(s.Body), string(yes), string(no)) if err != nil { return true, writeStanza(conn, "fail") } result := "yes" if !choseYes { result = "no" } return true, writeStanza(conn, "ok", result) default: return false, nil } } // readStanza calls r.ReadStanza and, if set, invokes WaitTimer in a separate // goroutine if the call takes longer than 5 seconds. func (c *ClientUI) readStanza(name string, r *format.StanzaReader) (*format.Stanza, error) { if c.WaitTimer != nil { defer time.AfterFunc(5*time.Second, func() { c.WaitTimer(name) }).Stop() } return r.ReadStanza() } type clientConnection struct { cmd *exec.Cmd io.Reader // stdout io.Writer // stdin stderr bytes.Buffer close func() } func openClientConnection(name, protocol string) (*clientConnection, error) { cmd := exec.Command("age-plugin-"+name, "--age-plugin="+protocol) stdout, err := cmd.StdoutPipe() if err != nil { return nil, err } stdin, err := cmd.StdinPipe() if err != nil { return nil, err } cc := &clientConnection{ cmd: cmd, Reader: stdout, Writer: stdin, close: func() { stdin.Close() stdout.Close() }, } if os.Getenv("AGEDEBUG") == "plugin" { cc.Reader = io.TeeReader(cc.Reader, os.Stderr) cc.Writer = io.MultiWriter(cc.Writer, os.Stderr) cmd.Stderr = os.Stderr } // We don't want the plugins to rely on the working directory for anything // as different clients might treat it differently, so we set it to an empty // temporary directory. cmd.Dir = os.TempDir() if err := cmd.Start(); err != nil { return nil, err } return cc, nil } func (cc *clientConnection) Close() error { // Close stdin and stdout and send SIGINT (if supported) to the plugin, // then wait for it to cleanup and exit. cc.close() cc.cmd.Process.Signal(os.Interrupt) return cc.cmd.Wait() } func writeStanza(conn io.Writer, t string, args ...string) error { s := &format.Stanza{Type: t, Args: args} return s.Marshal(conn) } func writeStanzaWithBody(conn io.Writer, t string, body []byte) error { s := &format.Stanza{Type: t, Body: body} return s.Marshal(conn) } age-1.1.1/internal/stream/000077500000000000000000000000001435753552200153705ustar00rootroot00000000000000age-1.1.1/internal/stream/stream.go000066400000000000000000000123311435753552200172120ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package stream implements a variant of the STREAM chunked encryption scheme. package stream import ( "crypto/cipher" "errors" "fmt" "io" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/poly1305" ) const ChunkSize = 64 * 1024 type Reader struct { a cipher.AEAD src io.Reader unread []byte // decrypted but unread data, backed by buf buf [encChunkSize]byte err error nonce [chacha20poly1305.NonceSize]byte } const ( encChunkSize = ChunkSize + poly1305.TagSize lastChunkFlag = 0x01 ) func NewReader(key []byte, src io.Reader) (*Reader, error) { aead, err := chacha20poly1305.New(key) if err != nil { return nil, err } return &Reader{ a: aead, src: src, }, nil } func (r *Reader) Read(p []byte) (int, error) { if len(r.unread) > 0 { n := copy(p, r.unread) r.unread = r.unread[n:] return n, nil } if r.err != nil { return 0, r.err } if len(p) == 0 { return 0, nil } last, err := r.readChunk() if err != nil { r.err = err return 0, err } n := copy(p, r.unread) r.unread = r.unread[n:] if last { // Ensure there is an EOF after the last chunk as expected. In other // words, check for trailing data after a full-length final chunk. // Hopefully, the underlying reader supports returning EOF even if it // had previously returned an EOF to ReadFull. if _, err := r.src.Read(make([]byte, 1)); err == nil { r.err = errors.New("trailing data after end of encrypted file") } else if err != io.EOF { r.err = fmt.Errorf("non-EOF error reading after end of encrypted file: %w", err) } else { r.err = io.EOF } } return n, nil } // readChunk reads the next chunk of ciphertext from r.src and makes it available // in r.unread. last is true if the chunk was marked as the end of the message. // readChunk must not be called again after returning a last chunk or an error. func (r *Reader) readChunk() (last bool, err error) { if len(r.unread) != 0 { panic("stream: internal error: readChunk called with dirty buffer") } in := r.buf[:] n, err := io.ReadFull(r.src, in) switch { case err == io.EOF: // A message can't end without a marked chunk. This message is truncated. return false, io.ErrUnexpectedEOF case err == io.ErrUnexpectedEOF: // The last chunk can be short, but not empty unless it's the first and // only chunk. if !nonceIsZero(&r.nonce) && n == r.a.Overhead() { return false, errors.New("last chunk is empty, try age v1.0.0, and please consider reporting this") } in = in[:n] last = true setLastChunkFlag(&r.nonce) case err != nil: return false, err } outBuf := make([]byte, 0, ChunkSize) out, err := r.a.Open(outBuf, r.nonce[:], in, nil) if err != nil && !last { // Check if this was a full-length final chunk. last = true setLastChunkFlag(&r.nonce) out, err = r.a.Open(outBuf, r.nonce[:], in, nil) } if err != nil { return false, errors.New("failed to decrypt and authenticate payload chunk") } incNonce(&r.nonce) r.unread = r.buf[:copy(r.buf[:], out)] return last, nil } func incNonce(nonce *[chacha20poly1305.NonceSize]byte) { for i := len(nonce) - 2; i >= 0; i-- { nonce[i]++ if nonce[i] != 0 { break } else if i == 0 { // The counter is 88 bits, this is unreachable. panic("stream: chunk counter wrapped around") } } } func setLastChunkFlag(nonce *[chacha20poly1305.NonceSize]byte) { nonce[len(nonce)-1] = lastChunkFlag } func nonceIsZero(nonce *[chacha20poly1305.NonceSize]byte) bool { return *nonce == [chacha20poly1305.NonceSize]byte{} } type Writer struct { a cipher.AEAD dst io.Writer unwritten []byte // backed by buf buf [encChunkSize]byte nonce [chacha20poly1305.NonceSize]byte err error } func NewWriter(key []byte, dst io.Writer) (*Writer, error) { aead, err := chacha20poly1305.New(key) if err != nil { return nil, err } w := &Writer{ a: aead, dst: dst, } w.unwritten = w.buf[:0] return w, nil } func (w *Writer) Write(p []byte) (n int, err error) { // TODO: consider refactoring with a bytes.Buffer. if w.err != nil { return 0, w.err } if len(p) == 0 { return 0, nil } total := len(p) for len(p) > 0 { freeBuf := w.buf[len(w.unwritten):ChunkSize] n := copy(freeBuf, p) p = p[n:] w.unwritten = w.unwritten[:len(w.unwritten)+n] if len(w.unwritten) == ChunkSize && len(p) > 0 { if err := w.flushChunk(notLastChunk); err != nil { w.err = err return 0, err } } } return total, nil } // Close flushes the last chunk. It does not close the underlying Writer. func (w *Writer) Close() error { if w.err != nil { return w.err } w.err = w.flushChunk(lastChunk) if w.err != nil { return w.err } w.err = errors.New("stream.Writer is already closed") return nil } const ( lastChunk = true notLastChunk = false ) func (w *Writer) flushChunk(last bool) error { if !last && len(w.unwritten) != ChunkSize { panic("stream: internal error: flush called with partial chunk") } if last { setLastChunkFlag(&w.nonce) } buf := w.a.Seal(w.buf[:0], w.nonce[:], w.unwritten, nil) _, err := w.dst.Write(buf) w.unwritten = w.buf[:0] incNonce(&w.nonce) return err } age-1.1.1/internal/stream/stream_test.go000066400000000000000000000034471435753552200202610ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package stream_test import ( "bytes" "crypto/rand" "fmt" "testing" "filippo.io/age/internal/stream" "golang.org/x/crypto/chacha20poly1305" ) const cs = stream.ChunkSize func TestRoundTrip(t *testing.T) { for _, stepSize := range []int{512, 600, 1000, cs} { for _, length := range []int{0, 1000, cs, cs + 100} { t.Run(fmt.Sprintf("len=%d,step=%d", length, stepSize), func(t *testing.T) { testRoundTrip(t, stepSize, length) }) } } } func testRoundTrip(t *testing.T, stepSize, length int) { src := make([]byte, length) if _, err := rand.Read(src); err != nil { t.Fatal(err) } buf := &bytes.Buffer{} key := make([]byte, chacha20poly1305.KeySize) if _, err := rand.Read(key); err != nil { t.Fatal(err) } w, err := stream.NewWriter(key, buf) if err != nil { t.Fatal(err) } var n int for n < length { b := length - n if b > stepSize { b = stepSize } nn, err := w.Write(src[n : n+b]) if err != nil { t.Fatal(err) } if nn != b { t.Errorf("Write returned %d, expected %d", nn, b) } n += nn nn, err = w.Write(src[n:n]) if err != nil { t.Fatal(err) } if nn != 0 { t.Errorf("Write returned %d, expected 0", nn) } } if err := w.Close(); err != nil { t.Error("Close returned an error:", err) } t.Logf("buffer size: %d", buf.Len()) r, err := stream.NewReader(key, buf) if err != nil { t.Fatal(err) } n = 0 readBuf := make([]byte, stepSize) for n < length { nn, err := r.Read(readBuf) if err != nil { t.Fatalf("Read error at index %d: %v", n, err) } if !bytes.Equal(readBuf[:nn], src[n:n+nn]) { t.Errorf("wrong data at indexes %d - %d", n, n+nn) } n += nn } } age-1.1.1/logo/000077500000000000000000000000001435753552200132215ustar00rootroot00000000000000age-1.1.1/logo/README.md000066400000000000000000000010131435753552200144730ustar00rootroot00000000000000The logos available in this folder are Copyright 2021 Filippo Valsorda. Permission is granted to use the logos as long as they are unaltered, are not combined with other text or graphic, and are not used to imply your project is endorsed by or affiliated with the age project. This permission can be revoked or rescinded for any reason and at any time, selectively or otherwise. If you require different terms, please email age-logo@filippo.io. The logos were designed by [Studiovagante](https://www.studiovagante.it). age-1.1.1/logo/logo.png000066400000000000000000001147001435753552200146720ustar00rootroot00000000000000PNG  IHDRPQ pHYs.#.#x?v IDATxݿr#Iʍ܈ki4 m!FM!KFk7Ȑ3"d/7xRdIa2P+@4PU:yx?= xz@ 7o@= xz@ 7o@= xz@ 7o@= xz@ 7o@= xz@ 7o@= xz@ 7o@= xz@ 7o@= xz@ 7o@c̥1fccru^-tyΛ `le٧e2{ .4c6˲Y,{_gY{@`1,˾{̲&gz16|ܕ% @[o&sea~mU1- @4W0U E%׭ԟE*7Su^kyݎsLKVV?E`\pȷWmmvD`<ߩʵ7cRTB`6.,N O*9oI@V];H& + 笯yF"x0imK.0y繭j|u~&tt ,yn~ Vۀ\@g.Z7yL97srx7|} x0'|Po&sX`)gO| ] ,cB9 At \E`.tG5:G`.t^3soB5^@L1<˲+yG`j1oKo@%un>9l0Sy"x0 1+6˲;oKo&͟-2& <ۃN:]veه]J(G3o(L{j*]No.,:ٮ{<7t6t ;H!.y~÷9o&I2Uc^kO7dm,.}b)Zu3sD`r1z];>eL{Mocy0SE`jYTX&! x02'z'CR )xe 3_B`17YWVmvç  C6[!`AW73؆ uNݰG H1V}^қ-n=8`ofnao!<c"n'hcY=cVەUI`7lU[Wc6Ƙ1oK&sPQ2x1j!+nc&ܨiZmc܌13m% ^y^4 ?@4 |"^5& x 4[IUC秆f @tdodT!TP 1T7ߛ9Me6cyn>د<cV*jC64c֏ |`qlϠ!TQۣ~PA!nIMw=,:s}ǢV;U B~ !x06@\6E`m*-:oo6VWٰVZ#x0CM+*uR^!x06X:sy\:A`Ty-Ԅ`.Hf #x"x!2WobrUcY[.w9 1c5>2xf ^560 *`L΀]cySG c Ih @l%5"D`l\gU9y/J71٫ys!j&s>!2\Ken<1c/{ޫ!u"˲߳?MGS~`(\W ܽ\؃Z)RZ'x.| Lm7/Txn'AI֕vgY0wge#wzsUٝ_+,A] F]8 ]s:{IQvPA= AcyÓjԅ\fʣ$إp&_*ǞӒa ʍ >A`u2Aq͔QNdIGEA`Pu]SZf,~-"6X '9ṰY'^zT0:ߋXglc?əN;jCownBNip)]zc۲Va٫J+=gVAGm?cIE A0nR] s~Tϝz()o2J';&3g1UErྐ;(+Y' ܊Nb4mnM`;"x0P}{F-MCABp ¬:5Q7>Ih~WP6 u@Ov$E :t M2C!9wעtvb2:g_vF :|c7".J8xžW_䀁u:Aײַr荄f{(@WκQ*BUjryΩ 6[iI_h>U^¤dϰNJ֣6ǰnQNx8 @ ۽`47DTT9nCK[rj0qt%s)}"-ӆ6r}lb'FT []'9&(D_Nf金-IeUlej b}ʴ,[V롦T\Y{0cYʼ݁+sanw*ك%L fH: ܫL~v/x%Z*Z;# dU [&XTq)c\UcCk9Xug^7{L*'m/=!yb ;F`aQqˢp7M-+lku_j޾ϳG9S0&^qz~c;fH- ɿy1woyJPEs PVS$C8k?.{$3F%M4.T :BM' HFlc̓ ,kVu}fXs;σM;촬*FkY8q?oqb#)LUy皦M'7ɜQ[ yv]UX6d>y\vj>< 6Gmd[ckɺb:_o*XC9xˎSLu 6ץB6nx@Bbq|(m G6ژ ~SB63s)E6CkiTMf @%ҙ{Vďʖ}*qjL@;WD[,A6I!|*'5];o:p;uQ{6H!|8$dhy:qe/rljJ GΝ1Nu~Z\VT )X'ur&ﻌ*(ewƍ1vG nKnsȲ[>߃p.k`~C8 9&qq:9-CxKۗπ_m%[ l*i6XӁ_v#sֹ~+UB{wrk|Q:JB_N 9Hj˲ ܎7lAd\2:9oB6?TNvgM?zri?xC\ƆH[ o@0c뎤@୼p…Z2x4L=nqw꾲Ȳβ7We0lJ:#MdNN~/Ol:;rʳ֫HţW=fùyC7x 8C`P]/Tئurl J @<4 mހ ;ipQ8H.9#M<]fC7]:7WZ澫Fr;MCUet%Ab((72d5{%󽹹Ga=˲IP*caT`LVb"N~糚8q*W' 1$cZo>~- 3zfqeX'yyRw/¥~c<k%c]yY9_n㿶m| IDAT۸˲m˳^e+;!\zHP_K;˛m.L1^s'}ehߺ{v.Z̟4.T3!XUnίr& ӣl9^u.paMͯCP?yfn!:ME\~SUoIٵmA!%t~%󨆞KwXG W{ ev]b1ۊ7%*dɈ\e o 4+n5BmG Q9ЯVHs B;ߤݎRMnKٹ t uUm[-C͞Ou t7}WpS:lO^,h |;& sU}肽-0 /v ``@|d+Sldxtv_Y-.% 5@Kɚi#x&J L6>E4"Uۍ20eH??L;8?sW<)B%,{P: ,W&gBd_·+Uiu÷L>,㥄p.\7 $;S2$KI*%71 ^Fl"Al$g(8UA!۔t&np:rFZ{}$UyEAVKi x;8?(jJU$|-rοM=uZ.-v 0-T3\{")xǐt\ s DGc6b`]L5S=JWAfbU[)h7_LkyV)c͜v% >v),A?lPtm60P$GPʳ;) :wv&@,+A]N?FF!Ms'Zfh! ےebY2\L"eEw`^A6Bf_Z8x ܗ=\˂-Ysv7Xrs"p{}V~Ku߿len_2ߋ|ʿY~Vr2 _:n|Rݙ^*o1Z7l|YeZNX%y沾L67 IuAe/+qBZ]CAI0_3\r;5b_x/ 5e8XzR!e(\[,{exݡ9riNpǵ˓H ~P"K79ϲ/@5x,}/>?NTolo.Itp=Y(L{uJǿݳ{;B7˷AϪ@p- VƝx_=esqC蛳V{:? <ǩq?|Twχ˼/I@GSB8O_&}2 ¶…pT•_V_f(.R8*K1>.\Ҽ\֓ }@몴<q=)Y~F~p[ 5 BC'o7w~=_0خ䶅<*t;ڪfr p»υ{<6҉4F]oAt$UUe=x/M&݄ԏo}:aޅ(/o?m'Ԗa*')vW]S>+V1qoBWs%^YbD*ʅK2g_.K(7U+݊&f.͟]t%wdリ]FU >N`8Ɋ@W`_vSt|?UV|g(qe5I _s:]Nmnr<9&[Мt |v)TQȅ7 R]Ӝ GAwWJbϗsy^Vq TUBUe5c.r/{_EetTJv3(VнTmrT]J+JP?]?fg5vWiɵT azNڪ_<_Lm\/7^>T9ҳ*+|TQ'\~)<]WSkRP{nEgwMPϭ+-N do%@ iU|i"xb6GWݝ8Wv\eةy2]f_}o+Yޝm^/]+Ԝry90yS9JEzߘmTU/XVDte:ҖBmsE~n$DŽƉWB0tEύme&Z]&= >WܑC6Oiocj& zGPW=]t%Rm=H;e2-ܻ*P[#BozyI^ɹORw"x"R24sgouETJv Ν?TMs/CL%C5n΀]ާPV>*`No^P57LՅT _- VywdžhE]_dwEomrVvn"N';9>uľ5<މf8=0ѾnzF?3wr =!Fp.( tsQ㺌8*o9Kw`|tr}>z../ґhً·*UH|~uin^Z !~Cj O Ν%$mC'y}x(t{d>M.XYv0 nuV=Yy8Tnr&U٘|UTyy&ܰn;^T&VMR w8fgݜ4sAL$t+A)TN@Kַ \ sE  V~@w?,UI7; G hosW|u!Y`iՆ =q]8[WޛܯˊM2:s=7Y=<lꛪ0^}=WZĭ*jN[o ߙ,·>Gd5oi#^د 1mmS0/v <鏁#`We` l^lJ%ջ̯"e T=,i3TH;V&2=(rۅ 9 n<~;5Vv?c2}|@·iHЭ/Gd *g XI:Mc̦yQexnwކܿY:L>&MuhXÐw}, $&<~/;s.}C-pIy\u<%x"XI}X׎ 0 BU`hi ?%g>cVKWS-@?;rqJoH{pw+mcӪw'WRMkUAk+c~6 mLņApa<17Ut-o@ BG_PvsjxLo v+Zߐ֦-kX5@^:>ݴJaSb`n Zh [Ry8L-0mTTDngbzp r,¼F 7c۝J3Ƭ Vk~ ܯ(+N>Gsxys5~iz'ݲ?F?UJ0%À7 ޞ܁TY]#|KjD1vr:I~' 3Vwөn17Ct!7Eeߊ'u VBWŜwjHijN(%ϥk&QL}\ u6Mކ$A߆|~J  x)hGNQ6;·pb0iހn]#NJuiF 1u Zy֝>͛GJڀ sJTހN~M6~mu7 _:*?|b9u<&/zVJN#ֹaIE ̽ ~H_S!t v*Hӷϳ[$9BVcj&Ua7 CsOF:U P$~3t߳oKHD\Yr-pb]/&#=-I8^{IvJPt>u0)jHo#gq| ±:Sh}se}J!$u*uºzMÆoTL'xކ:8q5Xw#xGE h!НU&?r^ZM[nDz!u k u$H^⎺3.૵%8/ñZV-ldZ1T[2e;[%pR9CJe͈79RF ;"ż}{$Ÿ$7Culqk5'[yݲǑ!mMɃ@t>Tś~%0Cylh!$u¨u)mtjn0JR5G nw1źm\1 7ЀTnbU ~cEF᮵uꆪγ򆡶6сJ Uh'gȮMkMlefePUoTwn &{*8l'_C|TŁϡoH'xPSAwj g~ׁ!r6oPifR 9C@Wut/m'V'GFP1t~2Y`hj&aԩDw1sT2ƬPLh_-TĽM3R疽Fp =}u')}AԦN&sjC6%T*ߕ"a,gѯT'0 *PqE:'9x Nn06ۣ7 3tylZ$znέתT^z_6`GSjltrjms5[l3L6j2|{G|R 7 {"aCr7" G@S2<> ~?{79;t:&Sa ?|Sv0{vU.:lN>Me o'AZg`ER/ù+TTM9rNᬓqOaE`h k{vj P;i { H՚6rY6O[`^oM9*2ݘ.{(P+n۔kpaÉ;AH`uUFBMߦ6[nA#GE[1o#m/PbB0}Jh2 N ت!MWK=_ʪHaː4l0W^?>|f!}~Kmκ4 FBycK?1#"x n޼c*`L&%. Q-kxy[R9:6 t|nUQ+Hsɑ~F;LM sqs!R8 !xnb(kxlBO5S+-'#ip6*svk=Vy •OցP`޼CT:l|+Eӎ%dr0'#I}qⷲoH!x6B7;7t۪5|E:(Ug~pV5:4,MM#UwC;8LNW;mۚMw u 9:NsIEe!~+( ܺA(t[zu|jH:W"\y:Qv=W-U FoФbMޮAqJ% x 樋͍jR! Gұ.ЦzG%jQ67@!@fw?}iޝ\PaٲDnEeQBҘZn2wr0VzsCTxN-t"|ƋD7~3dT RC7Ulvxf*7Pq *t=-$S@ÜMyiQcDhC1 ]V }a0 IDATJ>)L:Psַ*6}[g*T!v~ ?(, uԺWT)@qMwPW⿦$zj39UR1;l:[|qCC’`ހ?J4g٨kn7 u󇦮ۄM^Uv!]Y:-Mv^{65,LRU2q9@ Z0{^%Z0P*O6uv嬘_ur^7UUQ:nımi֦-rM4/V%`7DsaּJc(ᡞȠҼn̅vtb;0Uv{8Ԩ{?j;{YVQiWg; D[L??!=?5A 4J55su[p7nzh꺃a~mI۹7$j79::0zQ& S"UnV9(@9WWETpS%UuӁ!6[͓˥IDemz 3BMTD.] M ӊ7 hwsl>u<0v+P fI%$8ߚLiP)Bl|sOjjVV5%=<(A߾j7fJ[AhR{1>t$ MΦvM|oz2tZ:j0+́<7̎(!8ẇnrz TS*~`ΆOrYɏ j7/ ;Dwr8nvڶ.}6 s" F@YEIyݮUױ= {2_%gé~ڊz#ZPvWmxƘgU=r࠸0PUwihyvSGo l,(k}i"*@07G05͛`9n_+GV8TWgC7wsAT:WqR=Ιj:TX᪃a}hٴa7& YxiWMBI\ xlxٶSQLtZ1ϭð嶕kv8~pCd9mʸ"/^B7m]o]u4}'mg΃7>ϼB&Aܐ2bt~*af14OJww?.pK/;<874+ BS6۹lU.|aG ;k;[ 95Ck JBSkiܕjNЍ|/jTn"^ kv/RME0@-ol}.ML_ktt^dCII0.#ccp -’9x6lW5n\e <vuu{(v =ϣmgӾZ? u'Zaݚ'׸qCuF^@Fu+֪"*4̳R:4Ԉ٠N(\PP>YW<ieo(M'ޜSᆮwzh*'}T信aW4V_MvjkE>'6[B1k<OX/`DTyK0 fgT}U a/`en'ׯ fN;>^@[xC5eO{UMnPS?xs #C ]w`UK jPa啘V[K|]Ԙʿj7sSUjFW=(߫v{a)VoUwr\wQ0^6c̐\oo d֏*':'>~_=CG "ږVI5:Vv[*Ҏyvu.@皨Rå7rrWfӤ~oB: WEeUST. Hxje>Ž&[1wYA*dd =8^5 3 F1qi;T9N&CS3iۜ˙M SU|L:eUT`NC͇hԪ` im>9:injLcGg:y (1'f?b 0ؗg_wst j*!6jGx&0gvse'T+CǮЪP&ۓ/!\*ˬZ5lB>atmEp*|Q z` Tks}ݑ<ocIxjݪ 1!pU2TNo]kM\Ju^ϳcg{V /ܴQ a/t~{%TX}J I4ѓsC @ސ<@Ӂ:1VCLnm*]fn'WB~{(A7usYZzv#jxvZcgi򽹥핔k*kz:&<c"xU)!>(ڭNCPeXU|\3ǙCMJhWo 5>=&|Фyu[ID 2oHV!7޼3'P:P!ڭRh 3ͺq 15I՚{o$~DNSD#X*8mvз}49.ֹ!e' =Ԩ{!ANE| \U۹aSv]]Y@X;9)[ёE2M95HqE"~pa=!xC!Y\kƘt\{XU\]Uu{?}f';]$Fb G3d0OBF >aźv"3큊 Ÿ`c qg-k1!:lsIo M9Jɐl* +@ @"?aJE2 7FWg `%Uqըv{ jnېV L-z4?? eOCSR&#^jU|G=.'8!ҡYq,ipݮ*iԙg ,~:CE:ybn0Dtb$h\o#k(R[' R20zt@ސ9պzLndiqU|TAS֥ǚ%C 3_y`ζbjy)3tdL9:}%X'lj,i gmf;ؙqثj<; sr]!C_:x7 xՍEx:$RtNi7kAFEV! = T:DScm|rF0>7DOl㱮xBU0 rgSWkzC 3ͼ> 05sOHI _kl/'q!za9ZtZS0Mz"Uugz1J Aj"U}I ܒ{ÑyZܱRgv"xC,>^Pk:Ĵ:;*Z@^o/;mS{`>; 6,3TX(biX'pL:rB  bwnKNtmjEj,u:>Ը?̴ 7H~Ey*`digN~T/t9db*  h<˩Un8aQ|~xf~!+d .Rnq 0.)1[;N8#N!0>mMW%\_1[dd|1l!J顛en*زno}WU~2Ay-q"0IrBp߂TG| FʁtfoKثϨ`GXUvMJ. ?VZVUc ?̪M<ϛtLy4wÔ~ŁF}Ƙ%sHEm'bΩz1[Ƈ?ẃ ѩQo7J[M_J|tZ݀z e]g)krЊ Z;Ud_ e5>Q}B-@D5mۢMU&sPs[֠jCo);I-x?&)kN  ݘ= S>%")T@nYn.<{ʟۭ!i!y ՐVvxM ݘ= k=sec=[RFs7;oP2* X:C6XA_VwH Lx{lЄ [z2W=QO*>MDht d;ޮ ?5F;wժFkMXAo]>OJa#`*3MB$dAQ0+2=K%l/i@ 1ڃTVT lsv T5 6~cbsO:2wMnx[Iv4RHTbUJc0nJ[F"_oB*6oxQ0T}/uY0̫xMKopq-g,).3PD:)jnx캻Q⠝)0Ӻic>ħH0 bѤڭvU;ruvvj>R *b19[C*MfAŒ}PlwS:r!d$=0tRzht Yvi :ت;dfG r~7=4}YL 7OdʓjSLq'>ɰɐ잨r.O{.#G]f=\!:ĪZvhYy[ l·ʤZ׏qy4}VyN0x#tÜL aéWUnOЄqV~1<|.}2"sM\;?&AErkmy]9UOoԬ2 R&SnFI8nj3k)X 5\ T$]ў]7S6 Szs.“&AJn+>J!j/'.c&01SvaXpރ}Ra%sǼL}gL'w$;y@L> 9eiI}_dHP|alUݮVl4i}ffML+Nd!>yFfP U62 >T ed] Md!m9C!X%n f]6f2ǮsDH@j7-wj.WU4y6-x'^`=-1;ns1ATmcAO<$U7mz0!y眃3Y/~GUwsV9A)ۥ\_2uF 2tߛ+ mzn'%t\vxBN$?,aq;es?@]4UpXL-#0S?4kRA60S6[f0 U؉rwɁ G*H( e)ۉԇPm)>}u=AM=tecY~[ +yz#gl) I&>CW|Hp\{siş(e_cՐ@\R|-SLCWE¶ŀUv%#~ UV|z|%G!vc>Xl[Er(gwbhϞ5<0 ` K ~ mȐS$u.{mj#Op&~?oh1T:nJo]1wac7UJem7C QA{e`F3DKz7TWRi8cHt!K7#;Mo躉( ;^\:hxg; ciۋ>*dC0^IƁ Вf͵"%9qyǧEqԼ u#A]E4oꚢa)tu1۰ofH@@olX dzSMq@Tr:_G>x4('4[Ć\tws>ɥR \玫RL} & Q`vn=o@[=mUuIi1m]OVL4@YtvR?P'Y2۵49P.Ï#jpVٓ, :@!-m0J1 6UҵtxyfʐDxrMM+7YCY?7Pt|.Ėj5t=`6TGuM["ҵp Z~5%v C҉}զ .2!|0nΒtCE4i+*b2}Sٞ&?z9/aqd} 7tBU#{yf9J>'L-'ӡ{A4E& EɽTj_ l> 狼$4k<Vy)A}v׍{/9Cfwh>Du]Ov]U"zL>O=qMӨ O(^ ETi=3@/ a̛Z5s_0燒ʷssX*Uy7djN]\7Tu)ה szF^  z 4YYИ& 9qp?]Og2}Nh #xC金zKohtSw[IY/?Mʊ 'DzX%GA?8R.wwfA,x[zgbڛ0AP1$$, w6kfgZr 熛>Sdž{Jz:ztFɯr;7߳ }@{~- #^ngCcLVR#ta9{ Fߤ }zUV2 IB?Lqn !57?pѯ Pi~ ~L8{U^ֲW粩>2"EkބdڝwR^;uZ6].]6 Fxlx#[:nGBm϶s ='OUAʫDe`~s5 J46y@fEcl7y8_{}uh>UM8rT*~R/3@d tU34zv6iPsfm'vKz!P;Yӣ`}M͎,fCYAE$]94$,| 8Z6\C.z_yIv#;z~n8?K}_%c)19œorK f#; mo,MN CM|*PM=}PXMUy}TӮv51KxU5t~utj@ 2ۍSq`NamOdX۸Ly ]enj|d/p'kB*#ZmM˪on6N:F :'t#0LE`*#Fp `0f*e"14޽PkMx{g= \97Q%V|C*^˖ZjL<_>Yq)t#Z7W&shW(~{.v=GUn+$xc,Ept`- SfՐf`'t_bXO94{+cmytyC\̬ySvJ6ϻyx_Z> $sl>em1)y+ԇlD<4Ƴܼq]^/=\T סD[>Ȳ,p5}SeTZk(UY,cS5sWt=}j]t,c6C^v)}x@ OIæ:Ѝ}D{s{QF+}ܛ].1@$-5{[Dr#j7DUMݚCN[Zx;48lQ~gj!Wܤw{kVܴ90k۴~̃{8E[yNދtmbq\gu#oޞhG*"T f]+ʁKh ڄ6qM}0W>yW+J*x87Q ޏ.$'&ޛGos(W,~s^|s`[ow(˩} QEVt\M3Ԟ 843z˷ [T3αЌ0rz:#Ҹ."UsTZZʠGK7c"7udz 8(B!JnF?şڑ\vhC|>o=U< Dqi9}-廊rSq(Gxo VCodJFQuM /J[|p>!ayjH);Ǝdo*AY.?WG6|%_` 8 `yVJC+?;>ss/%(Gsm ǂƐw{Yt n]~ߧOq {n a<ֱ6q!G_m|_~N&q-:kĢ?mwTl݇R'.ޣ7qM:{ ̄﷉/>1/Ձj>ec@T+:o9t.$<Gs] 6alכּRol.BoaD`y_KJ!aҡX+ 3ucoπ.=?  >-\/NJ <*U./=%/P^YOdBK7qdjPծd_ۈw4}?G%1|5/ +!.fz*<>]n,q}(¶5UWqt}YǢǼJchٞ;fʕp(k (G5 MPXՎ-L/Dk`،b9jx⦾9(3p@aSn@L|lTMx-jhY&!xc(Ň+,\6bFKY1V7*ʵ~0do*cZuua>S>E[)€uש;tSeo,NL'< *)6/t 397[z`T7jcP(X:ԎSYki7jc6O禥l P;>p,{U*l*`ʘ<C( UU tJr9D6ZHf @{kll!CWo.U,>.5|bw KTT'xc{ eC6R=fW&Sr=ѕ ZÎ*ހKwġh%WRU>*ތcWzP5T/"||v[ŸV,0m꭮nФ9(AG*T,M1.CPco,ZU^VhȐTUð) 6w+8IWm7T'}ؤQR\'AF_Ev̎y_.,ywԶ mEW[ұoCŔ}0>=NLyG=lYlSY\4Y/F 8e-L` oQimBrې^_+`h+97dڄHoZ|MEa2R &ƙpF,'CFªwݧ\{55RRpRo PTXaY7R'Ŀ[ټ_1LFFKZ6Bh PnVt&-3N! o(諸 +_v Xv%ϓq0pjoQx;zml fК,3N>ސ`,B1w9[Jj۽Yљ[1L_N75VQ@_)|NeII(e/RJ7)×oeK ,[f[^=bE2S$ h-f"z]wy+/wϥ@SwCxǴo1e,5tk8{wj/+:;p_g-t /*?, )|ʞ1L,JJa=$7uچ!k'8_٬(L9fUd u2-Mc,=x{ zF0=6xXNTq]ܜ}*ǡ+bЬ1^~lYtvVb9Xv3@7oUccNŶ}>fYeQ;P8  u!Psܥi;V^wY]"xQ4ݿjrC}ξA[Rj.LJ2QhP QgãymIoVCzL^2܍ Vvq瘥k; ),Xr1e1#[y>˲.E^)GA +1d; Gg2t,ˮ=[yިoZt5)|#:Xz@6=eY6O$xu;D߷x#2n h+z]6Xp6d(UVſ`cIH [u;nwx1vovVRJ@.;+;Fsےwlzmw-KA7z 67j7 xR>0E <{U5~nUM@VbA^t]kduWkn_%8rhu,{ò2XL)}n,~-ط#,j7GJW/,R׳=vTCoit˽4@~O?j(fוg1L{w1_ z٩o J;~u:¦6S~wTgYvl{Mc!LJC):: `K'@1+{gJ o_\>{3bޘ};IFv3έ=gj*>` fR+ٔ|!nsOxv(TnkCΨMd6t=f99bvt m4*۵O#6 s-~7,q 5E ovƜ?vNJ,[x3WQRJۮSJ8.}N‡J1+x&ǚ2,!JDoٜ}ݺ(BRJϱZ|.̋uyUmu,1h xc yҘsGrwJXvŏ!.~g1q厱,BEݏKAL0tF&cж 8uMbY>_ZaluC,1hAmKR,~- \y&,J f'`ds Ѐe[ƭp MK,pU7"|͕uc~ͲsЎᄟɹLP/!2A3R=o\7p]~T8 sŀ>nYmm~ Xإ`Wi rJ2|~x76dxՒe2,Ng\Ff<,<ޘXUS,x[jo)gˁYOX?<7 W@?70EA6eِaZ\?Y9я{,塛X KoK0 3!BY6,# &xc+})zp.`|}Sٝn#q%z,˲5+]lYQ;.oY0eY^%aw2{WvϧMUJEvV |IlgT*5TeYvRvoRJ=fNhureJC."pnv8Me\sow[/W?^ 0eSyH_HCЏ X~v|]^*C՟}lpP{eY)E<u3eՀ8M_U=^{<5ڄ끎x~LYRG00ueǃW=&hIQ-qvU];W3 6ݴm%RJws0n$wN>90%ۊXLTe%ջ˔T@ul*=KW5W^|q?6,An꧷o%XUb{L nb9@uUYמim 1{4Ųv,S, }!jc*R,F\KKf$xTw k U{mCCLV+-~pvVGB6Zei"U{^#}}#$TYV=Nk@>˲6}Eȭ姫 !x[,Qy8q52Cy>uOE5[:>D?Ç,nNQۅ7[x`yo+R;'&h.wH`o+y`q}>ҳ=Җ|;O`:ۍ3&x[?W”9HqYE_MVU帻#w7`pmjKrUVj&#{0x,&m0 .yw9`[+^n$xEK)`kbSTEZbyep3)  [h)BKm{r1ޟXzNljMg}cvݵ=BIDAT4t~w䱱MmNMTE:p"llm#RJy,@5KʲlGXL.~o$Pb;٫r8!y`pdm䴘Uu>FŲ򯥷(NEЦ mD J:p"-y=66^/ݹ)V#";+~bn㿗Y].A R8X2 u.b'<{APJiaۢ?tUZ6qHQͶkc@|S`5D1+$`v )WJUgGaljJUPo"hlh;vnb[g_{c.쪴P;NyYd%PG,3*§^{/[󳦿Yi#́0?~6>FpUxWULS %k yc)<aR i {~&C xۮjuRv|@J=o+_?u۠뉂Ua]ARϑ $NK5\,m/TՆW॔\;έy8;P,{05iyUy n_rh?& z(|˨{=!g_co5C^C6߿zm}58˾U_ 'x;EeUV2TM3ks.P5_nD0@=&Xf FCo0zc,ͺ@FK}*oRNz l3UV#:j@hRNm h;UndB"4[R#{}Pox]T0 N}kl:#xciMtZHXW:X h%g˗pV70._FlUiC}CCN"xh#(Xyvk6Cvڄ*gl-Y#T{>j:X vQIBS -h"`9ޛ!YJb>vmQNKcZe?,TJɒSX8i~;N `8ՊQDZqRJW+} ^qpu'ӓR^qk95˲˚-`TmS? & FZ:u6f82mTeoVv&.p _ٳ+ne1@FaK=.3-T˧lEV^1vz! R:L>@*Fe=LBvZTIm実[d ߍTM^uAO)i͐_qpCxu@l.,y\1~_)e.ƅ)?c!˲AǦ=qp?d@RU||=?K`_#}s깻2J)߇{}T<_잹c^f=">TRc؛E!CߍxFiϟ9}!J!ה~g f|8ϔ}9_\Y&~Rzs;PucLZRJ""89<3RJ5u_cJPHqu#];:BYm!^ů9k:֋s>u%K:=NXIx٧-V^䷨[91B>-7^DqanOTuC^t uK!>"MQsclHs0{,m|tK^:xF|k 'FϸIʋnS,,wJ,O Tnm﵈pqJ^֜۶ץ_D֦S{.Zj_[TwrNG~n0ek߰f{75Z{P[߸^5DNaE|w݁Э& cj"R)T|w]xnwVXTzFY1:džP:ƦM1\#'5瞿u)C>l>g \[Q Oض{18ւv bs(0m;h6U7gy6TC< ɲ,ڼ.]ͱchX^wS9*f?~_s,/rkt |k,.NZy6sz @!SWV N=R߸S.^DqMTFɛ뺡H!˲ۆ}u}/*]9Z\S輛smTB7|ǜ繗 9P6-A{7SQEXWe3T5eMGQw,DȵDueXT毣!x?ׅo/f5Uӎmۆhg;xrp~n8ocCq ԝ򲾻s )|a±?hTӞc,2c5⡡W9<"v[}|hZzWЛ_!{u $xc±=V|^!C-:~_M}f~;CIojV׼h',[ }^>pR:~\Z!4q *m"*|:~Zu k,K|PmXw&W>|yG|/{|Ϣ12j䧔cTpQ7;P:90XYnáねl7 _}7LfnvS(/5)]7[Յ Gc}RkdsYw\L> 53-NmmT$oZ~Z{xMYU1dz w}VjXuPJ ,a[:Kvu_PCTyNo0,zW6FƺϗoO,ܗv`h~Igz[bm]%p 2۶׎2ZWZTU (+ K_iau}r/J)93;UÒUb,8ve7V#];t߱]p4+ hxOjoՇS>$cTE)Y9gK%CYcj5ga|9iTy%V46gA}|-˲UU|׿ ³>)jg M?6<J+vu!CzdOXVvc#>O/>qݸ)ۻM_5]Ʋ|gƟG7isu({:->OۅV_ 8jfUWui"UeM[ پ0|hM!;tvHܵ]qܦL]Txv3f|zVw Qq=iawmT=1~,%k맔ؗqMIY .ƙ1.;^ `.ZI-38w!DmŅ {qFiִv9ÁîU}ärQy{ö!T*S 27_:B֛؊>׬7KD{&|Kc%T=瞿.9 ߊw=+dgFm#*]\b{6C a8T5(7S?q ˺h'n1z|ޞňπ/Vѷq-K>_q"JM0MP8a͗KCp~sOKvQ~oko6}ET=V3)[X}f7'$ՁF]n J6EI#M9l{cMԃmAz&}hWM>Rsˎ7bC%YW %F?:,Bw"y˩ogeݰ\,˜{ T <% `b[1wb)uujM\3ɻ.#n羯ޯL wu|q0t/jVu<;M~sW{>{j~ =^SSnRI]|\6QطCn|f r5;LO#''x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 F x`7 ?LK|"5IENDB`age-1.1.1/logo/logo.svg000066400000000000000000002001571435753552200147070ustar00rootroot00000000000000 age-1.1.1/logo/logo_white.png000066400000000000000000001062211435753552200160710ustar00rootroot00000000000000PNG  IHDRPQ pHYs.#.#x?v IDATxݿ#IW'( ȐӜ'h4 me:6d а,2dO#CΐdO.|s3#2dFĉ}hx= x:@t 7o@ x:@t 7o@ x:@t 7o@ x:@t 7o@ x:@t 7o@ x:@t 7o@ x:@t 7o@ x:@t 7o@ye,ˎY}S?ݷoxQ-pTi , e!˲7"˲@h1fYE)˲ee|=ȿ2-F,˲s;KfC7g&Un.|(aZ-2F{UpYz^"=27cj7(ǧ:Up B{Jc(E`rmPV)AN}e9p),AIڃ~p 6Q`*|ZG`*"x0zғyo s&x T試/9) ̲,{yLt%@LJ.y$D` t5f *7SRmSe+> 6;$ *I*a)5oNWeY|Qh  ʷ-Mo6u}jzT1&7c+؞B bz1}9Ч} `jaw[Eo#˲?7@gj `6y=K=!x0Fʶyx}#x0Fk#x06i l,Sϩڍo[oO\K7;%y.;3ta~t X,,{˭moےw7caC[ۛLs򧭀5oච½zgϴS7.#oRwZbv$y @2MgbUEܑw71MUzm<=UeRBݦ,Io߾՛T{5nӳuG~9%`ouTFX͂ {M#]\Pp{wUo[{ @lKؚIwyg]_^P70Qobe{j6;4f)O6w2tLfRYs,~p<'t *՜gU67rgnT]6lETP0Q,@k ̜ lwg2u/!FͪK Xoj &ޑYa~,Y0obzLYJ^& @nկqJ0 g[V3{VBU>jw;欞:7+~ #x[hI׫T-l<{Y%0obj7[Y}5& @ P-r|Ϲ ι33I*0Qobp]ymvg&[*Om0ز]ǷWpvADP`h3*yEMz1/[ uYLM==t#x:7^T}o@X\@vKͮ j,^`W 5zmX<C7za56JbLobuPVav}Z]4lY@ yLoPl*|A- <,7j t ڵP&^-<[/K c{r @Fobt ~KN`ژj FʴY}wY@+L[ʟv*h}\1071I6Kd[wv[c @lna1j6?gEXh&obSc``u c*~?[T;F 3 l²[+xR]E~2swl AVHy l&a-d*nO.saݝZV]PMa:{'e6]{;I+`o _m*pPnB!=@$ @_f.={Y4`UUٞmi0>T9{-byiB~VIieӷw,XTsLn3p'Ymnlh^ y<V>gQ2s,{|yT8Wܛz/mi#xз[PIV,LU*zq*6zA`~f?|\ʧ6m gt#x0=t~`**V/xg_/'?^sJԿJ5\]Ӏv{ 1T|βl'_PWy۶!:&/`SRd5]V+ ^g[9~ ~@-omT4F6@<D %U*Bi_:xMC 6EZ-djQ)C 6u0utRuv!xVwR8cWi \cнnؓ Px+ק*7 @* luCc9Q;v4əL},Ú6~8u9A? mI;"mT; JV @0h1{S5[[Nrș|Tܾ1m0 mAϫll&ӴP͔QNdP蛞UW R;s vVmu@ӌl=e*m\E,UO3!>/GZB a!gIvv.<3inF`w讀;ucs•ʚnl].2'>!ȏK^OP˅m)UVq6l, ߢ3myb>;0 n$gWfV2-eq =Ώ\]GUi @wf=0JP5wu@gKkϱTBam2ScXv#cCt`lmKB)n0*ߑC,b}pMN)t62H{01̟ Nf&- 17Cp=Fv3{Mc]A}S7(dv0P1F?/jq£l6T0:pIPd_IxLd~5SL]߷ҫ}߮=zYׂ:V^̙"?W;?0=bĐl6 Ժ"Zzef IcS_6_5B0Jwm"=㜟8 ԲA'^R廏&;'p+65#SQldGIdpYaǂjגpf=ބo{-eJH؏-9Eoģ++=P*tm.;~{UfS$sgΟJ WA^~:|{ ܴ_[BiM rJGT# mmrc_>;C*FvF%Qn3*X\sIOxK҇T}Uصa/U^ F~;iq6G+TE-Q @i`#rLt{ʙ^ʪ6^+dzAv ϫ&'}qDU2;|q$ @]| gĿT|g>wMI E&^B~MGO/ZӴ0#?J>_E%gL՞{ﶼEo0T^lpBM' HW @];90+Z`:n=־SSMWwrp>wOd\6BM;h "x!¾s9P<^$ltϻ[%ר~x@l8te6BF6iT}tb.OLvdwU՟+hB^_tLM:Z4S,'u_ƈ,d;M}"E6M3_jiT.LPk5Zbk}q|hjN۫bTp·`L۴P>nхlA ,rkI^{[ 2Hi!|?M8Nߦv)h9˲%9zΝ1;@,f'ɅoE Msd69jn3 M2n0kPO9gYk߫f~w.]rvk/ {gNH^<bC薶;q}&E >Nn@6olҭ)':_UµZS_׷-N~>ߝmm<}庶-v^vǀ>mr3y?zoHpA&mrt6Ց#x| H |+n9Ct6۪<=]v{*:A%SNoeoٞxemr<ƅmz\17L:rS\lHioR:I 6&W14NnH;]Iu_Q//coYOY/U#"d69>@ >M#Fq+}6Dz2i\̎ tv M圝T;8.A}sTu KWTv!˲[*,7Eվ@n'^QBm&CN+ az@6Ro*\!&m ȹLW=l'.{^3 ݖ*4,oz 69A>'| ~[`ʲHo9{KU c96ٷb|~[đQ:NoL5*Yvmg]ws9Vx3OٳHNت7;Xx+_{7_;ai\v+7] g0VK;(4㽚 [J 84*WGo߾M5UaVeRѵQ=|aU;.2(}Tq9'Oo9s>= Vc zV!:p`2yEgA-qA^\U=kzHMW^Ҳ`?,3l*_#֏̟xr*'NV VS_ 0-ne*hi:Ws\\8bw2{)٠ʆdelhfEe O<_rU^ShF;aZ^;^ S=a֩ov;&'y~G-I1qW9fb87`:XUrs~0%gv3]ԅf?$\6ܺ|~W>i8ˮ7,z<Ɏs. Ξ*Ku\IM=1A|NL1x{rMJgᅯn;Y<މ3o4ld d՝5 t{m%l{ʙ:3|G湆`y˻oe=|=fgsV)=4הU~ \ϭj_c=@Tg9,3W!CLƴ93M{ 7WTgjɫ^[΀'tJ5 2qUm{ |U[~Vܳ강E߷jlv';֥'~3sO?L{{0%E$Y652 XDbms@KɖFeLv>y;|Ul?HH6SaV[(ZԜ{}w:)03as+ a*+&pVk3E6쉼sLiq1Me jv6$ Yvʯr}+r>2EBmR߷h}?y^&WׂY?z^̔ۥl_MnA ߙדQf&CW_dm;ܙ˶B}^vvh4ԿR;Ǹ f/!iހq(i<峙 `MuPQP*<0Ɇdu%ڵg}E( AMe2WcrKN÷* m3%*BuNYd\yVF)oS&M<'+xo`8c Uny]҉Tn=ހ ]%{B\.ty+w}Tֲ{1܄~ ZW&yB=E AbQ߅YD!iz1gM{2/Ŋi(sbgcs5EOZxB-VisWȮ~47uUAg28 Q uuk` g9[Ug୯ST-7#D{V*}y~G;·O5٫Kۛ\Y&4=(}-3¹}]MhUy8lB'R 1p Ŝ7 ME^-TLM3ԃCI/}z'u1}v:z.oj Jz.='ڙ3oJV3!Vwru Vr՗۩ ІoWe^go+Sw&|ϭ܏ }cv-5o*+z6P"LmcyL8pw6m9 23=F͞p۟mzK#̛Z=h 9˂2__ {a5 u˪ Y_=b{zm{ V굹 lyv3]'m5'b={EWABƺM%4.-]nH Dz:nןew\(Pq!L@X24L3- yl8tZ;{S^N7<ֽ>{&#:~Qnڪ?rmZZY(>._m\ƺM>Ť-nD۸&G [^Ylv) >nZjGUi&:z.94LhCVa} {]~2nz/|s]An߭bji;^g*ml5nVhuo~MwUVٙt-j'f/1-o@f%!؏w[ e_aW+]Ȫ!i]aW(z{1Lzs_rYj|֪zA(|sWM<NNI^QH24mTՄ:O|UEwP<}o'=cD97 >2-/t[,Y{Bf9ӎ>n6TsYj]uv=ۊJ}ژ*VeifWVvEּGwGm1J· ·LSU4u"jB<|oNT"--CNm~SHNu%Twq#x74Sg72P{* d{RgF}L-}KO1ݘ;=x*4=t ]-BPUvw]@uT,@o/곢÷wG3-mrVi؊eQ Kg,d ĩ'+ۀ^k4)H=4ySwrexf'Ta:yf-)w aí*ۋ6+s?G .wU*Ӌ ~)9+HS]' {M+7پ_'Ea^` >t`sB't+۱r[W^+w;sn˖{5/y'm[Li{.F;" GaYy1=i!Ƹ$t+F1TM#|KK%[}1[ހ^&M =߻A‘; !_;OGz) *d.[|^\͊MLzo*;gMUe>PPw/?z.|݅O~f7XR~·tޣm2[{b./v W<鏞ZHkYm%[`!/Z{^[9*KI3T4S}7y} 2^^ǼVMu. }@빫pp6g !|Bj9^*bu{V!^̟Py?5͈0H. BB첮؝]iowՃ7=,D&/vQgP+7cފJUWT|U6h8:ͮHCJ^ S,PT#.1oUfޜ  ݮ/C't- sUt!kϴ؍Ҕ?W6Sx!Axk]'d7S^Z0Թ|ky_M_{A·tn~QE>H}DTż9` >x="x`-L} /rWb3O۳og]L3I- ݇=ըZD)Lw_rn7;yR~cҮLz-Xtap5ٔ pѷ][֠ͽ!Tz3;0|q߽S3Y _&nY= c3uv+6UìUgZe=n0m8mTEns4cA 7}YUmrUk^^He+Bw6NۮPZ=s{5$zShՃKȋj @9>ҕƎmuo+׻n h/t{Rfd~s_ 2X2)uݲKR SmkPMVmנ)AG qH  )mg{F[b&,2ԻX+ Y4o@{|g50t[xȟ ۣTsJƄ[栶j7G*$@\%9]} ]H܆o oX7BֻM#^Sc4S8oN7%xЎ]N? ႯO[Y/j܇O Mt2`;7x==iIsToKi%,Htt0GV8U%aG7fC߇E/`L~^{S3ښoj&_np [t l[k$x{_p2o%Վ>ۊa2:I)xs2MYtPOކcFIm6]Mv-YN1-~N)42W8PzX LjZP6\[J+n/ cR*kuToR d4~ĺMstKi1Jo@}]n[OE٪[Փn_3X3!MM} fTe5&BR%JXNwzrHأݫʩ2]v/$nÊgހz%[&]V9k>|/t:}c\'mc66сJȎ7s&W5T71*.pg ,8ށ^:͢-#xC*?ހ|moe ʦ́Aoߠ͆7&nGx;va{u 4=xmyTNT*>RJ4x>t+>> q}+<*s/yU$z{V;T*&qy'ЎTpɸ~t'0+Z8@L5ʅNjSP`9.`}mE:7 3r5[L%u|\!ಖ~m vB_}K1-|hr/ʥpbJ[tw'0Jo\cRgv@o@ͅ`2}lZ&!ZTryj@j۲*zͧ8^[ |qߪ;0~lT kFGYiy~wRcK71qzBncgв[!m8+l[pM2\ 5}oUlc[JVuE%$XLƉim݈E'+A1^ tL3S,U6aQ6[ꤡug{uv^m FP%VתTeLƉeX-/Nlf YennFU"\ϬBWKW]4daNŚ~mo.(ݙ^ڥZlOJ,KRYe>^Ti{Ho!xle nտ+n3e*f&TX7\oc*CAWM۩~زf_7;5up4_Y_n3ҏt1YJB s%Z^v >T=]!}2 \UTULns8};u7={ݚTeL7O},d?@0u,T5pzv0 uۘ^hM=+0VTaPŹB]+ףN_˶ϚN(Z,b:7L](a T;U |/оnzj궅V==u[{"Z5y²m`Hv0S7: qtmk\Ɋv @%o;9' ۪(42 550ԷU1.(;c,qVvUT1xշwl 7tҼ'To3 B|AˑN/B A0 ]΋K^fCMN_WHŜ/`/%nk(_/6_O;maJWf]cR[` ]X83۝XIdZFb(Y8JU뗎_0tEۗ6骥ŔEj& Qtj.p b!@[lѲn k`˲9VU=x=tUni?I/;m%|sPV6ZH5Ÿl+lfƋ2qbFն4@,Q87^+_s'~YP5)ɢɎ7{8WҐpFEL3p,S!U|܎%.?z?C>zuWңIPomſJ ʪ8L}x_8(hV̫jW6)0Xr_Q˅TUsA?Uy}Ųj7cI^*UqnEֵF[>o]Rg/$R9d֍OCcK7W!EUzal&h[L13lf_dOEULw2s5R%4iok + 9潞[8\L?ϧʦw,TXWB8p0*Hm2Nl`e4Ӂalǜ/SL3 lojj&J.3PgWano)Bl-!ԹN ƭ,סjd\3ȶ\g1{M=1mHQ'f7b 0Bf`Z5eoUbZuiNveWsc&XŧCǶmhU5fI:SAc֫Zթxs# c/A~|xN*Qź‘ za,*P 4N1:Z\{m3Ru {-Iѭ=aj1!7VO9TW f]VeA*omUǰ ]=Lzөz,1jsd)M^nV㾏ruvl}:42JeEJ|m Rw;H$ZPŦkVj& *U0gzת  N|SbU⍩úȶJ8GU;%8,ġT뾒@dސ)Kw&$0٘.$8zT:v &k{ TեթZs M⇵k#[ЮX֊ (ؽX{/!xCBf&{܄\!gOkmfvUl``g-fgNh&AG*UcaE.ʚ~PP/6yO` .oH҄Zy=fQw\{ <Õw_mU *a 캮xzFІWȫژz !U9֜L{6 ^ŸB8 R+Rs#[v>wKsjov>*Tk2z M>0m0sVM"xCt[B Z3LUvGjhW]{Ŵ:kLGS0t$V0Vo]Lb {3OXޯXm!5@zygmcƷ0Cw_WnYiMԡ`V̥Q|L|KhAYTw)8|”d oH*:^vs+T<mUefGT!LNͻ4ޤ/? aT씤TA~;ZU4~zEĺ1)7dzV\ ,Y!iܙƋ8xoU2U{"x;Y ++żyޔ{q<VMDj$x {1o"@TLp-cvu)x;kvASJ;>>k!}iއ+­'ޠ8Ń|JwoF9]a.z&ʼn춬Y, RϬ ֪ة dZv\(ftLϹ*UtvAuv3UˮƕRvLʥPqٕcb}2^ [v"+|{BmXtPo/|rG0<7`fvE;mMJveM]vpiflB`eALGJdtvrX\"Rlm^SlF! 'a!z[U``QWZ0-3[=w骺c=C8]&>8Ճ%7GbZc5 1E͙*[W_yNRq03jp{ T=EPj*EjdYn},i'o,돱JwM,hܞ1|?0[ZT S햵P³gloL̔A@) x -߂IIaZ[|r )'iԮ17FO|-tUTֵ e=TUR4T" X nIK ?J4zcoY!+].Z-Pw ŪVU:X\XAomOJa#`,ЙgᯄnIJ}^vFԬu`t]_17DhMh:ز=Aa;,[QW'꫿&kX~< ᦷU- )$-wy{ !(R-7"mi.d*jnJNS tif*޺vm۹6*ư榷Mqu_"x,hf&m.~ౠT!xC,T].UF(}[ ~,mT`uYUb,=;f92K{ qy>mm&=1tRzXbP-dP:ت:eN3"xCuMo n>{Q)A 4wws\m. ~n݂ ,tҞz{I"xC tZvn!H[œus!>+~\ 0%c=ʓ/6jc ĝ;6\3&a.|$7ENv'Va?y"pBڨgT]j72WLYK1ĺ02O#9 Un07^+WJ@Ƽ.pc[%wW/97 mT5m"w~UoN1x[/uԫ1-cߩ沏ݜx>_'As" y|'z)"[HRPs:0j9`hUD=L3Yi6: ҃&_}1Ѿt@]czst& >\M{>HEL:[xR m7 ܪ_&aHj7maN|j6UuC?k+V=b_:)K[6nm.t/S~ Ȯ/~r㠺 Z >HXo[c^`3sphV[LV}LU^Uc]($U|u' ?}We2+a[L9®ӏ7kȉ$hsf7wvʟ T4%K~m׽|.S90 l_@m,TjI,q 4S֙О݄]'{//m-DES~N@M!sU뎾C>x!7 %[f.zn*l[ +>i]ZJ)U>[ME_&z Wx}.>zGT?2;Ր@\ܔӟFn?cUU>3~F CXʱjhگdi4Qc56M,6"뺿-%,`4wL{u\oB[ .;ϱI\w}c![8Xy# Bhՙv;t/sCwzsVZxLetXtz]>Hue`*\MمA@ƺ|jɱZQbvR@𻜜*}ʱDU#ʢJg&jcu.թvgX)w;ffu7׫0>}q`46X#\<'Y˱[\bU<]5V$EqKQ0} ~Q]mr;k ݺژfjX@@b]`.g@kIs L9+{nGרּ[\F@}U}*sUbv'Ǫo#B>LUQQUZ B}7G݆du]6٪VZ :?Nnͪ@#aiXdab;jbvc_pAݢE7i Q UX\ N4[EjԙʪTz31m߹٩jSV=ۊmGuMn5Hצ ڱq56|oeIEN*]7EwqSE,`+BC|Q^V=u`b]ۙ{pᦞb/zy0~O@LJe"hP{gQkܙzWLrIPX4Q'4k2v.^%rM/;Cv|:V=YX]'$|,8F%MƱlpn:oKho7;h&gleڄAyn_m7Z }| >|qdr3$[}?c ;*SuRY4Ʌ;y.>T)Pgc4ɸpp?jO{GƜ0>qb+}o\*ҜKh {̲;T$Zmj:so. \O%ϗT lWm>-hhzPK=D%<,s*gPM gz$r\F& }]@O `% NnGsgڨvf~7ͫ07ly۝UL9_ }5ch'yl}:Id%4xlX}4oP٘67AWX%WsًkUp*jlM,۫@PpIA80 (6S1_*hƦa"<ϓ2n Ly|lCDTvl!|}nе y/AoRsͩn Tg̯9!Ƴ|z^'vLn׆{yJ][j- I~zK8Kü$PϦ FZLi5[</7h[fޤ ]JiPc+ ' K|S햩hy:s\N^OfûA ߙ˔Qyl+$|( ݞw+y* 2{nj`*PD,0:oz5_b\j\%r+p|iuB_Ŏd!Nhx xR++ z ءt;KfB6%A~(!|Kkɇ!|KtyWr̉7Zlt@g /ͅhw&Pp4i^=S7$)Z|sS:DxD?pcz׷ _Z\Ѯ/ Rp]ֽw1^kأ\~"swo8E6~nTV)MwNފ}qZ%[7%ef4нum,s!խa \;cpp/Tm|VyU{3-}vV{R^tr q4QtI5I |twN[Q"TSt)t RlX& -W7(_hq¦g.  /%}q@c{hϏ{ޯs0yjo7Y,B=\= jhfinKEzfEvă ;I+sUV6TO'OiVl#*xa%""O IW n(LmpU;˭b6|cU?e4S}o|^J6=1GB7|$ɉa)*nS Kֲ)ǂoy"!mo_mjW5x@!9=^uY5SНvt^Lk]"Q[wb+h, BVZMʦjۀkNxʠ6BYJmZ{sQ-53͛!IA\e[@y]' P 7tWA{Y꽕#^n`xِ&os0 C\ D]kVr*9=;jeu%=}~k`Nj=im;^dN(scQj#1xCf**Ku&RZ2n,/x+J/_E~>V|䵼/kܺg~|}ɢ'y;Onq`#f/ܶHPm#hʱنr" SV<UnmC6,}E݊C/g&xlp (AW ֽ]kV\8nj[>w!{˶Y\%d[D][ k,Y 7toג *},wF'$fu+tRtEUHh6V4-b/*뱤Zi+x{/|G M8{߭T=qSO?S֪Wh3Iz#B.p,Dieڨ"ӽWљȲ@>6f]9[ IDAT79\.S~egn o+YeQݳ i܎:vT*!&SKѶlӁ[ ۡ'oh[n7Y[ [*/:9J`YX43Uie"U~~ ~V׀WĐ@ӯ_p+!W#x-.xHm:L+5w B쥏«n4SfMBB=]62; m\+i}U޻'񽪳_r?л#O% }9l"pO@tADi!g`r鐰WQx{|JP0K`&Z&h~Ȁl*'a J?t2 b/ : l7-VW.&(Z56C"S||oqPQ^/='Ov\Tmy4,5'& mݪTS}YKaVWHfFXY[gW8Ş<'^n-^}7Mm:dzMI=s" \e>ohUYm߻:hǝ~C ؑ \< 9lsus1?de1-n@ .BFO+A"{R_M.L*@uB[=tծ75pk+}~6g  ܜ,^>cu8&[ժe s.loĒvXanoJވX pP\Yu[:M ׎6-~m oƔTU}&,}sk9,ވ˲Tߨ$4@c_P\Ā zUe,oQv-Uq&L]ؼojlX!إ `^2cHJF m7V*bgK]ch:Pث92 ;楸-±=6V )&2VOF Y4U4e%Z> |w7+'x#z+b-URN1`,`ކ9o ވ,A)~,v3}e8|`INc ;fLM{H?Wi,YΩ͊˜Bv*ۇqޒu/+\hM]=x;_цC[X]510' L]R/:T]p5UV-c 6KZ,{[ܲs춭bm5L0h Yoދ|BXXy[%xe.!A@K?9LU=&cCr|K6Q1kx.l&g!:5eP*'6). T7ںvH!\0olPgʎmλk=]is }_RIolLmf>9[\1·υ/!8k,RV:<1* GF[PtSkdqW+~Krt\?3 Ou,UKD{JĿ-J=cM%SP#xܿi$nYϖ+KBjݾam_ggHNb]u7%eW^`S{K?}gOM0}x c ̿܄*c"N!S.j b4B6$,owfe[C6|uU `|c|s/xݏq˖0\UsBAHO1ABm3?9tzN? CP3Mb&[i%xqsv ٹ0luZX3Tuu/}ov\('x)ƪЊ96݀n1ϙ q"!zǷAik[faMg1bU%xx[S5glC5ޟn)p_pVqBǙ`sT!"8z\#jp 3b.JoҔ}BOa)C+քg! kjb)@ cNo {4\[c:.슾4ʱ+le5.q3o-ϱo{η9Xuꢢ?[ ^[C\+n0%-cۄ/ammf7`-,((h`s kֺ? 3 `o!R7\q#K>27 <}vvo-U9.TaYNS_<>DdSnoX"+0W; 'xc- [ kt Kz(!xhNFB7o1f 1Bҩ6̟.n X@ޢFq ^؝n cW-p5VY> iNFniXSS8>}Wu Rd$@SW*eAa!`j]c{N[|xuY.ɫcNq[O-Bך_2vجp=I>zl[ w pa܅UOU.C>xnEsߟB@P]m§C8_r}*~>GWvz޿6!O>fz鸘c{JO00x cR>-Y+xOu_qbH`C,N^B覅oqg9x  գN#`$e)KŒ]em*6v?U~m^~ys%㮻uʖݖ@ `C(cv5LlY[n9]O9*`NaBH;`B7æ~P*pߖ-nc~@ P,,*7CDϑb,e,+_~ڷB3} Cf͊-eGY }lmĪ[Q =ORSŒ<U4_cc<03幷;V4`vFvH`orem'yY\V-a0ܴ ޖe.o{ #9Wde}ͪbZE]}K 4K۾p*UZuC`,UXh4n $xb:j (4NL7Y2,Wޣۉ+!xi]Yۍ<]FYR=k0RVawb;% Z/J/apkR`=oTַl7IHKm{K߭'rc+pljN - >F.:-fؚ}.z=^~8n~`m]|}V=vP-8@tdmt]rkuS"ơ5) ٴ{8z"l$lDӒצ^]SlM)!L:rhǮcv!C&7Oy[eo)CY5Q\6;Ϊc 0кIx)^wY8lÆeXZY@M-nWr:Z`a0qlZcRsua>e̱esK.s&dlU?ǽYa |{!KR]Ltu1B[`.}w|׹ S_U:kh`vof_SP{HXx̅2m*qxA`kM%޾E0mm'`^ w9gа4bk>XSȓ`zS[`M-شoR6A^ŀ- ;*RU@gv#4597#W Ua}ARϔb~۶õܶeZ6ʪ~0hXmX&e;#pSUor^ , iZB1i1CP~%1ߜwhodbSӱ翟:ksm{Tʂaܡ|VKYeUV2k+ؙ59Wەݯ&Xf f޶'`L}ǘUZFs} s)'x#3E?&uQ lMUVs ފK'`5o&+vw-ZJm:SM !x[D*m!* xȷw9Aw][ ۶4U=.`/KN`oۢzm b`*SW"Z -ت w-~Fl[?l͂׽[H˚-`TO@1Ck-e82WiagrPT-ŲˑU x#%]}L3g*`xc Vd*|±Q[Ya5ҫ=E-%epr524KTMv51l2u.1oj3mvwkC8-;sԋ*.ƅM㻻^ŷ{~=1?x+3ڳxnFú‗pe\ }U=~6o'xۜHӛAvszi!I *\D !~H!J*ӧDkhv[1N}|N^J)Lކccߝ#W!|7u}oLjr*|]qF||_u(8_c%cV"|09|;~髦۶5]ܦHo]fXzX_mJZկ[n n 0kLZ\`~},JHc0kLO?Ku±wQy,txێ{ZuqZkϲ.Ʃ~ϔoUalʖ7-EӀږ Y 1VExϵYOڬk^˰2<Mg^kuͲmH]PךUcy=qVqn^ʞstݲK|%.[j;Ez 2!,4z6깒}yjm@,lܨ2<'?%3Э& yB,I=9\d{gxjwҶ^sJE׳sYæ7=7heX]ҡE-cvSN˅o,t1al[m½oC{ePMؚvkvքeUM_.Ż* _wq?!tMqD xr^.s@ig'9[u_klbU#^U{X͸L> _BhuMnZrLu4fUUp?ל3hV^R55yZs*16jo򹟺:vMߓ|gyot1׀˥.| IDATP䗉'v1; _*wVֻ^^Uo_'ԅ©]ׄ~3x7*a9,Z_mC_ '>`?n`ܪ3sw5~Y:5R-IY2x{Y ;w27c}zA6!xVRS)>}&/#nUֻ!xʷ߹}M{4.?-ڰM>wW:|I|>BmQcxZϨ~[vTՔ607!9|h:бa+~5~q%*WmJ,|yf-TqY@_9!ŇcXr}[H_`’ IR˪@BLwacmޅ^6D9ۺr;R6?^cVU=Yq^fyT&&wtp o)鵎cBY}MA6癟l֪T'7s>n$"x[.KcફUX[TfZg/(k܆ khX0uUjc_s.kܱ]pSj9|^yMdW3&x[Cot9L}d8vR` V6.|[e%kmt_sr_~x!\U='RCsBULJSîu%{X7KP&VK i RÝ|/5Tm-#`\mq`UXW NU3,t3ׅl1WUoM:=EQ^@@]qeHe U̦q&rӼ9vT+ 5Z x>߾qUyTt!Wj뗉_[?Mܟ+_63Cƪ.kS1uw[mѷ3-CxǍmjZyG\5ax˚z7;4mxƬ|vCzWG;-:\,_;VscmCʶ}% \Jl~o@qO֜ՄkM.|fe팪6{FetfNYi}lN31(OTp^YaoXXf)6,7mW3&&՝a[9U"{b*wTU1dh->5lPU5U¥쫛1k.1cfNS %t:^ `> 9A&gn[iga9|hy%lYJޘޅݎDm^̰ۙ% i<ԔnI݇m@`WwKvAI.ĉ#TxT~3sއ!F7*~L`WV3dr?^f'jK8ܹϮeO3ƾ [N1poq毹⎳\e1s<ݷeo.]*3]YӾV}?ä|}%!T*-wls(p2ֺXY6u1 U/) va|L۱Ļf3|]`jm·ya`ըm܅o({+VR.j _ ]՗}?}St)<^n#4劇Ɯs}R^p>̌m>Ms0nY[? Omn n~s/3p קx, y+%MʾǮ>HKcVi3F.h=CF6 =9ܥ<&\ק}tW=?>`NaunYR _%OymY.D믿{ c)GD,Mɵc\zs]cn~}?Z5baVv/+Vu?6`\Z|u}USctVysϽKQk8u=+q RF&x@|7H@  $ xo @7H@  $ xo @7H@  $ xo @7H@  $ xo @7H@  $ xo @7H@  $ xo @7H@  $ xo @7H@  $ xo @lR^IENDB`age-1.1.1/logo/logo_white.svg000066400000000000000000002002601435753552200161020ustar00rootroot00000000000000 age-1.1.1/parse.go000066400000000000000000000050051435753552200137220ustar00rootroot00000000000000// Copyright 2021 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package age import ( "bufio" "fmt" "io" "strings" ) // ParseIdentities parses a file with one or more private key encodings, one per // line. Empty lines and lines starting with "#" are ignored. // // This is the same syntax as the private key files accepted by the CLI, except // the CLI also accepts SSH private keys, which are not recommended for the // average application. // // Currently, all returned values are of type *X25519Identity, but different // types might be returned in the future. func ParseIdentities(f io.Reader) ([]Identity, error) { const privateKeySizeLimit = 1 << 24 // 16 MiB var ids []Identity scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit)) var n int for scanner.Scan() { n++ line := scanner.Text() if strings.HasPrefix(line, "#") || line == "" { continue } i, err := ParseX25519Identity(line) if err != nil { return nil, fmt.Errorf("error at line %d: %v", n, err) } ids = append(ids, i) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("failed to read secret keys file: %v", err) } if len(ids) == 0 { return nil, fmt.Errorf("no secret keys found") } return ids, nil } // ParseRecipients parses a file with one or more public key encodings, one per // line. Empty lines and lines starting with "#" are ignored. // // This is the same syntax as the recipients files accepted by the CLI, except // the CLI also accepts SSH recipients, which are not recommended for the // average application. // // Currently, all returned values are of type *X25519Recipient, but different // types might be returned in the future. func ParseRecipients(f io.Reader) ([]Recipient, error) { const recipientFileSizeLimit = 1 << 24 // 16 MiB var recs []Recipient scanner := bufio.NewScanner(io.LimitReader(f, recipientFileSizeLimit)) var n int for scanner.Scan() { n++ line := scanner.Text() if strings.HasPrefix(line, "#") || line == "" { continue } r, err := ParseX25519Recipient(line) if err != nil { // Hide the error since it might unintentionally leak the contents // of confidential files. return nil, fmt.Errorf("malformed recipient at line %d", n) } recs = append(recs, r) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("failed to read recipients file: %v", err) } if len(recs) == 0 { return nil, fmt.Errorf("no recipients found") } return recs, nil } age-1.1.1/primitives.go000066400000000000000000000044651435753552200150140ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package age import ( "crypto/hmac" "crypto/sha256" "errors" "io" "filippo.io/age/internal/format" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/hkdf" ) // aeadEncrypt encrypts a message with a one-time key. func aeadEncrypt(key, plaintext []byte) ([]byte, error) { aead, err := chacha20poly1305.New(key) if err != nil { return nil, err } // The nonce is fixed because this function is only used in places where the // spec guarantees each key is only used once (by deriving it from values // that include fresh randomness), allowing us to save the overhead. // For the code that encrypts the actual payload, look at the // filippo.io/age/internal/stream package. nonce := make([]byte, chacha20poly1305.NonceSize) return aead.Seal(nil, nonce, plaintext, nil), nil } var errIncorrectCiphertextSize = errors.New("encrypted value has unexpected length") // aeadDecrypt decrypts a message of an expected fixed size. // // The message size is limited to mitigate multi-key attacks, where a ciphertext // can be crafted that decrypts successfully under multiple keys. Short // ciphertexts can only target two keys, which has limited impact. func aeadDecrypt(key []byte, size int, ciphertext []byte) ([]byte, error) { aead, err := chacha20poly1305.New(key) if err != nil { return nil, err } if len(ciphertext) != size+aead.Overhead() { return nil, errIncorrectCiphertextSize } nonce := make([]byte, chacha20poly1305.NonceSize) return aead.Open(nil, nonce, ciphertext, nil) } func headerMAC(fileKey []byte, hdr *format.Header) ([]byte, error) { h := hkdf.New(sha256.New, fileKey, nil, []byte("header")) hmacKey := make([]byte, 32) if _, err := io.ReadFull(h, hmacKey); err != nil { return nil, err } hh := hmac.New(sha256.New, hmacKey) if err := hdr.MarshalWithoutMAC(hh); err != nil { return nil, err } return hh.Sum(nil), nil } func streamKey(fileKey, nonce []byte) []byte { h := hkdf.New(sha256.New, fileKey, nonce, []byte("payload")) streamKey := make([]byte, chacha20poly1305.KeySize) if _, err := io.ReadFull(h, streamKey); err != nil { panic("age: internal error: failed to read from HKDF: " + err.Error()) } return streamKey } age-1.1.1/recipients_test.go000066400000000000000000000032541435753552200160200ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package age_test import ( "bytes" "crypto/rand" "testing" "filippo.io/age" ) func TestX25519RoundTrip(t *testing.T) { i, err := age.GenerateX25519Identity() if err != nil { t.Fatal(err) } r := i.Recipient() if r1, err := age.ParseX25519Recipient(r.String()); err != nil { t.Fatal(err) } else if r1.String() != r.String() { t.Errorf("recipient did not round-trip through parsing: got %q, want %q", r1, r) } if i1, err := age.ParseX25519Identity(i.String()); err != nil { t.Fatal(err) } else if i1.String() != i.String() { t.Errorf("identity did not round-trip through parsing: got %q, want %q", i1, i) } fileKey := make([]byte, 16) if _, err := rand.Read(fileKey); err != nil { t.Fatal(err) } stanzas, err := r.Wrap(fileKey) if err != nil { t.Fatal(err) } out, err := i.Unwrap(stanzas) if err != nil { t.Fatal(err) } if !bytes.Equal(fileKey, out) { t.Errorf("invalid output: %x, expected %x", out, fileKey) } } func TestScryptRoundTrip(t *testing.T) { password := "twitch.tv/filosottile" r, err := age.NewScryptRecipient(password) if err != nil { t.Fatal(err) } r.SetWorkFactor(15) i, err := age.NewScryptIdentity(password) if err != nil { t.Fatal(err) } fileKey := make([]byte, 16) if _, err := rand.Read(fileKey); err != nil { t.Fatal(err) } stanzas, err := r.Wrap(fileKey) if err != nil { t.Fatal(err) } out, err := i.Unwrap(stanzas) if err != nil { t.Fatal(err) } if !bytes.Equal(fileKey, out) { t.Errorf("invalid output: %x, expected %x", out, fileKey) } } age-1.1.1/scrypt.go000066400000000000000000000126661435753552200141470ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package age import ( "crypto/rand" "errors" "fmt" "regexp" "strconv" "filippo.io/age/internal/format" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/scrypt" ) const scryptLabel = "age-encryption.org/v1/scrypt" // ScryptRecipient is a password-based recipient. Anyone with the password can // decrypt the message. // // If a ScryptRecipient is used, it must be the only recipient for the file: it // can't be mixed with other recipient types and can't be used multiple times // for the same file. // // Its use is not recommended for automated systems, which should prefer // X25519Recipient. type ScryptRecipient struct { password []byte workFactor int } var _ Recipient = &ScryptRecipient{} // NewScryptRecipient returns a new ScryptRecipient with the provided password. func NewScryptRecipient(password string) (*ScryptRecipient, error) { if len(password) == 0 { return nil, errors.New("passphrase can't be empty") } r := &ScryptRecipient{ password: []byte(password), // TODO: automatically scale this to 1s (with a min) in the CLI. workFactor: 18, // 1s on a modern machine } return r, nil } // SetWorkFactor sets the scrypt work factor to 2^logN. // It must be called before Wrap. // // If SetWorkFactor is not called, a reasonable default is used. func (r *ScryptRecipient) SetWorkFactor(logN int) { if logN > 30 || logN < 1 { panic("age: SetWorkFactor called with illegal value") } r.workFactor = logN } const scryptSaltSize = 16 func (r *ScryptRecipient) Wrap(fileKey []byte) ([]*Stanza, error) { salt := make([]byte, scryptSaltSize) if _, err := rand.Read(salt[:]); err != nil { return nil, err } logN := r.workFactor l := &Stanza{ Type: "scrypt", Args: []string{format.EncodeToString(salt), strconv.Itoa(logN)}, } salt = append([]byte(scryptLabel), salt...) k, err := scrypt.Key(r.password, salt, 1< 30 || logN < 1 { panic("age: SetMaxWorkFactor called with illegal value") } i.maxWorkFactor = logN } func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) { for _, s := range stanzas { if s.Type == "scrypt" && len(stanzas) != 1 { return nil, errors.New("an scrypt recipient must be the only one") } } return multiUnwrap(i.unwrap, stanzas) } var digitsRe = regexp.MustCompile(`^[1-9][0-9]*$`) func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) { if block.Type != "scrypt" { return nil, ErrIncorrectIdentity } if len(block.Args) != 2 { return nil, errors.New("invalid scrypt recipient block") } salt, err := format.DecodeString(block.Args[0]) if err != nil { return nil, fmt.Errorf("failed to parse scrypt salt: %v", err) } if len(salt) != scryptSaltSize { return nil, errors.New("invalid scrypt recipient block") } if w := block.Args[1]; !digitsRe.MatchString(w) { return nil, fmt.Errorf("scrypt work factor encoding invalid: %q", w) } logN, err := strconv.Atoi(block.Args[1]) if err != nil { return nil, fmt.Errorf("failed to parse scrypt work factor: %v", err) } if logN > i.maxWorkFactor { return nil, fmt.Errorf("scrypt work factor too large: %v", logN) } if logN <= 0 { // unreachable return nil, fmt.Errorf("invalid scrypt work factor: %v", logN) } salt = append([]byte(scryptLabel), salt...) k, err := scrypt.Key(i.password, salt, 1< X25519 8hrlM+ZBG3Dd4fF2+a583zdTIWDk8/R41kCYZsvwTW4 yO4PYdlMWDJ+CxgUNRqY5Z0T/m+g3FCh5jIxGLbCVXc --- I/imevZzy8120JSzmJnmn/KMk3p5A11V83Nk41m9NPE p6$RS,ZʲsMaw8 Az"r\w41;uage-1.1.1/testdata/example_keys.txt000066400000000000000000000001621435753552200173200ustar00rootroot00000000000000# Test key for ExampleParseIdentities. AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU age-1.1.1/testkit_test.go000066400000000000000000000101151435753552200153340ustar00rootroot00000000000000// Copyright 2022 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build go1.18 // +build go1.18 package age_test import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "os" "os/exec" "path/filepath" "strings" "testing" "filippo.io/age" "filippo.io/age/armor" ) func TestVectors(t *testing.T) { if _, err := exec.LookPath("go"); err != nil { t.Skipf("skipping test because 'go' command is unavailable: %v", err) } // Download the testkit files from CCTV using `go mod download -json` so the // cached source of the testdata can be reused. path := "c2sp.org/CCTV/age@v0.0.0-20221027185432-cfaa74dc42af" cmd := exec.Command("go", "mod", "download", "-json", path) output, err := cmd.Output() if err != nil { t.Fatalf("failed to run `go mod download -json %s`, output: %s", path, output) } var dm struct { Dir string // absolute path to cached source root directory } if err := json.Unmarshal(output, &dm); err != nil { t.Fatal(err) } testkitDir := filepath.Join(dm.Dir, "testdata") tests, err := filepath.Glob(testkitDir + "/*") if err != nil { t.Fatal(err) } for _, test := range tests { contents, err := os.ReadFile(test) if err != nil { t.Fatal(err) } name := filepath.Base(test) t.Run(name, func(t *testing.T) { testVector(t, contents) }) } } func testVector(t *testing.T, test []byte) { var ( expect string payloadHash *[32]byte identities []age.Identity armored bool ) for { line, rest, ok := bytes.Cut(test, []byte("\n")) if !ok { t.Fatal("invalid test file: no payload") } test = rest if len(line) == 0 { break } key, value, _ := strings.Cut(string(line), ": ") switch key { case "expect": switch value { case "success": case "HMAC failure": case "header failure": case "armor failure": case "payload failure": case "no match": default: t.Fatal("invalid test file: unknown expect value:", value) } expect = value case "payload": h, err := hex.DecodeString(value) if err != nil { t.Fatal(err) } payloadHash = (*[32]byte)(h) case "identity": i, err := age.ParseX25519Identity(value) if err != nil { t.Fatal(err) } identities = append(identities, i) case "passphrase": i, err := age.NewScryptIdentity(value) if err != nil { t.Fatal(err) } identities = append(identities, i) case "armored": armored = true case "file key": // Ignored. case "comment": t.Log(value) default: t.Fatal("invalid test file: unknown header key:", key) } } var in io.Reader = bytes.NewReader(test) if armored { in = armor.NewReader(in) } r, err := age.Decrypt(in, identities...) if err != nil && strings.HasSuffix(err.Error(), "bad header MAC") { if expect == "HMAC failure" { t.Log(err) return } t.Fatalf("expected %s, got HMAC error", expect) } else if e := new(armor.Error); errors.As(err, &e) { if expect == "armor failure" { t.Log(err) return } t.Fatalf("expected %s, got: %v", expect, err) } else if _, ok := err.(*age.NoIdentityMatchError); ok { if expect == "no match" { t.Log(err) return } t.Fatalf("expected %s, got: %v", expect, err) } else if err != nil { if expect == "header failure" { t.Log(err) return } t.Fatalf("expected %s, got: %v", expect, err) } else if expect != "success" && expect != "payload failure" && expect != "armor failure" { t.Fatalf("expected %s, got success", expect) } out, err := io.ReadAll(r) if err != nil && expect == "success" { t.Fatalf("expected %s, got: %v", expect, err) } else if err != nil { t.Log(err) if expect == "armor failure" { if e := new(armor.Error); !errors.As(err, &e) { t.Errorf("expected armor.Error, got %T", err) } } if payloadHash != nil && sha256.Sum256(out) != *payloadHash { t.Error("partial payload hash mismatch") } return } else if expect != "success" { t.Fatalf("expected %s, got success", expect) } if sha256.Sum256(out) != *payloadHash { t.Error("payload hash mismatch") } } age-1.1.1/x25519.go000066400000000000000000000141371435753552200134730ustar00rootroot00000000000000// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package age import ( "crypto/rand" "crypto/sha256" "errors" "fmt" "io" "strings" "filippo.io/age/internal/bech32" "filippo.io/age/internal/format" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/curve25519" "golang.org/x/crypto/hkdf" ) const x25519Label = "age-encryption.org/v1/X25519" // X25519Recipient is the standard age public key. Messages encrypted to this // recipient can be decrypted with the corresponding X25519Identity. // // This recipient is anonymous, in the sense that an attacker can't tell from // the message alone if it is encrypted to a certain recipient. type X25519Recipient struct { theirPublicKey []byte } var _ Recipient = &X25519Recipient{} // newX25519RecipientFromPoint returns a new X25519Recipient from a raw Curve25519 point. func newX25519RecipientFromPoint(publicKey []byte) (*X25519Recipient, error) { if len(publicKey) != curve25519.PointSize { return nil, errors.New("invalid X25519 public key") } r := &X25519Recipient{ theirPublicKey: make([]byte, curve25519.PointSize), } copy(r.theirPublicKey, publicKey) return r, nil } // ParseX25519Recipient returns a new X25519Recipient from a Bech32 public key // encoding with the "age1" prefix. func ParseX25519Recipient(s string) (*X25519Recipient, error) { t, k, err := bech32.Decode(s) if err != nil { return nil, fmt.Errorf("malformed recipient %q: %v", s, err) } if t != "age" { return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t) } r, err := newX25519RecipientFromPoint(k) if err != nil { return nil, fmt.Errorf("malformed recipient %q: %v", s, err) } return r, nil } func (r *X25519Recipient) Wrap(fileKey []byte) ([]*Stanza, error) { ephemeral := make([]byte, curve25519.ScalarSize) if _, err := rand.Read(ephemeral); err != nil { return nil, err } ourPublicKey, err := curve25519.X25519(ephemeral, curve25519.Basepoint) if err != nil { return nil, err } sharedSecret, err := curve25519.X25519(ephemeral, r.theirPublicKey) if err != nil { return nil, err } l := &Stanza{ Type: "X25519", Args: []string{format.EncodeToString(ourPublicKey)}, } salt := make([]byte, 0, len(ourPublicKey)+len(r.theirPublicKey)) salt = append(salt, ourPublicKey...) salt = append(salt, r.theirPublicKey...) h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label)) wrappingKey := make([]byte, chacha20poly1305.KeySize) if _, err := io.ReadFull(h, wrappingKey); err != nil { return nil, err } wrappedKey, err := aeadEncrypt(wrappingKey, fileKey) if err != nil { return nil, err } l.Body = wrappedKey return []*Stanza{l}, nil } // String returns the Bech32 public key encoding of r. func (r *X25519Recipient) String() string { s, _ := bech32.Encode("age", r.theirPublicKey) return s } // X25519Identity is the standard age private key, which can decrypt messages // encrypted to the corresponding X25519Recipient. type X25519Identity struct { secretKey, ourPublicKey []byte } var _ Identity = &X25519Identity{} // newX25519IdentityFromScalar returns a new X25519Identity from a raw Curve25519 scalar. func newX25519IdentityFromScalar(secretKey []byte) (*X25519Identity, error) { if len(secretKey) != curve25519.ScalarSize { return nil, errors.New("invalid X25519 secret key") } i := &X25519Identity{ secretKey: make([]byte, curve25519.ScalarSize), } copy(i.secretKey, secretKey) i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint) return i, nil } // GenerateX25519Identity randomly generates a new X25519Identity. func GenerateX25519Identity() (*X25519Identity, error) { secretKey := make([]byte, curve25519.ScalarSize) if _, err := rand.Read(secretKey); err != nil { return nil, fmt.Errorf("internal error: %v", err) } return newX25519IdentityFromScalar(secretKey) } // ParseX25519Identity returns a new X25519Identity from a Bech32 private key // encoding with the "AGE-SECRET-KEY-1" prefix. func ParseX25519Identity(s string) (*X25519Identity, error) { t, k, err := bech32.Decode(s) if err != nil { return nil, fmt.Errorf("malformed secret key: %v", err) } if t != "AGE-SECRET-KEY-" { return nil, fmt.Errorf("malformed secret key: unknown type %q", t) } r, err := newX25519IdentityFromScalar(k) if err != nil { return nil, fmt.Errorf("malformed secret key: %v", err) } return r, nil } func (i *X25519Identity) Unwrap(stanzas []*Stanza) ([]byte, error) { return multiUnwrap(i.unwrap, stanzas) } func (i *X25519Identity) unwrap(block *Stanza) ([]byte, error) { if block.Type != "X25519" { return nil, ErrIncorrectIdentity } if len(block.Args) != 1 { return nil, errors.New("invalid X25519 recipient block") } publicKey, err := format.DecodeString(block.Args[0]) if err != nil { return nil, fmt.Errorf("failed to parse X25519 recipient: %v", err) } if len(publicKey) != curve25519.PointSize { return nil, errors.New("invalid X25519 recipient block") } sharedSecret, err := curve25519.X25519(i.secretKey, publicKey) if err != nil { return nil, fmt.Errorf("invalid X25519 recipient: %v", err) } salt := make([]byte, 0, len(publicKey)+len(i.ourPublicKey)) salt = append(salt, publicKey...) salt = append(salt, i.ourPublicKey...) h := hkdf.New(sha256.New, sharedSecret, salt, []byte(x25519Label)) wrappingKey := make([]byte, chacha20poly1305.KeySize) if _, err := io.ReadFull(h, wrappingKey); err != nil { return nil, err } fileKey, err := aeadDecrypt(wrappingKey, fileKeySize, block.Body) if err == errIncorrectCiphertextSize { return nil, errors.New("invalid X25519 recipient block: incorrect file key size") } else if err != nil { return nil, ErrIncorrectIdentity } return fileKey, nil } // Recipient returns the public X25519Recipient value corresponding to i. func (i *X25519Identity) Recipient() *X25519Recipient { r := &X25519Recipient{} r.theirPublicKey = i.ourPublicKey return r } // String returns the Bech32 private key encoding of i. func (i *X25519Identity) String() string { s, _ := bech32.Encode("AGE-SECRET-KEY-", i.secretKey) return strings.ToUpper(s) }