mirror of
https://github.com/FiloSottile/age.git
synced 2026-11-08 12:26:58 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eaf2cef49d | ||
|
|
581cff8473 | ||
|
|
40ef1b6a62 | ||
|
|
11659e8c97 | ||
|
|
7309913372 | ||
|
|
6f86a7f520 | ||
|
|
b59a9ecb5d | ||
|
|
bceb0e0423 | ||
|
|
53f0ebda67 | ||
|
|
f3fdc33c9e | ||
|
|
a81357c237 | ||
|
|
69e2222921 | ||
|
|
732f3e8a94 | ||
|
|
801a7e8b33 | ||
|
|
629b0dbbc9 | ||
|
|
710644eef8 | ||
|
|
6c8d072dd8 | ||
|
|
225044b061 | ||
|
|
6da7d26b4d | ||
|
|
19e87b75b7 | ||
|
|
5d96bfa9a9 | ||
|
|
f04064a41b | ||
|
|
0fa220e4d7 | ||
|
|
6546df3bac | ||
|
|
15df6e2cf7 | ||
|
|
50b61862d6 | ||
|
|
c418992942 | ||
|
|
902a3d4e6b | ||
|
|
4a5a042583 | ||
|
|
6fc795057f | ||
|
|
f7011ee36a | ||
|
|
a8573a5c8d | ||
|
|
a02075a0cc | ||
|
|
4147b86ac8 | ||
|
|
dc8716d8fc | ||
|
|
3f2deb5a3b | ||
|
|
97b6569a66 | ||
|
|
02ee8b969a | ||
|
|
cb4d1de4b7 | ||
|
|
e665eeafb0 | ||
|
|
4dee0155ee | ||
|
|
0522803919 | ||
|
|
f8507c1cac | ||
|
|
7ab2008136 | ||
|
|
f5a47fcc9a | ||
|
|
6f51e96429 | ||
|
|
53ccaf8b71 | ||
|
|
6593c56e33 | ||
|
|
31500bfa2f | ||
|
|
22e598d458 | ||
|
|
65f171a239 | ||
|
|
2194f6962c | ||
|
|
07c72f3b69 | ||
|
|
21a7203f6a | ||
|
|
0c650f815d | ||
|
|
189041b668 | ||
|
|
e609359651 | ||
|
|
33355dcc1c | ||
|
|
9a08b7e66f | ||
|
|
9b83d948f5 | ||
|
|
c9a35c0727 | ||
|
|
7d608d1219 | ||
|
|
6782356e45 | ||
|
|
085466567a | ||
|
|
292c3aaeea | ||
|
|
b32ea4c1f6 | ||
|
|
c7c7f1870f | ||
|
|
a7c4274d23 | ||
|
|
7088a73234 | ||
|
|
f0f8092d60 | ||
|
|
f28f85d87b | ||
|
|
f54bb8daab | ||
|
|
35e582514d | ||
|
|
111d0fe1f4 | ||
|
|
9fdb125641 | ||
|
|
a798d4ef31 | ||
|
|
68da6c5f50 | ||
|
|
2419b5da58 | ||
|
|
4e84199130 | ||
|
|
f9f6d7ca50 | ||
|
|
e43cf8b4a2 | ||
|
|
7935150f35 | ||
|
|
1110c3b6e1 | ||
|
|
1223fbe6da | ||
|
|
b194267aaa | ||
|
|
3807646596 | ||
|
|
dd887fdc87 | ||
|
|
0da94651f3 | ||
|
|
80c6b4edd9 | ||
|
|
b3fc5d19ea | ||
|
|
c02443e4d0 | ||
|
|
a5773f28a3 | ||
|
|
63301d18b4 | ||
|
|
d6fe1cf5c8 | ||
|
|
18edf29a75 | ||
|
|
1bf22e2163 | ||
|
|
3d73da544d | ||
|
|
c185781433 | ||
|
|
bbab440e19 | ||
|
|
158b2ce899 | ||
|
|
c434eee6e8 | ||
|
|
b142e0fd01 | ||
|
|
9a84e437b1 | ||
|
|
8c600131ec | ||
|
|
0369d9b7fb | ||
|
|
93ea79628d | ||
|
|
aefae7ca0f | ||
|
|
470186da64 | ||
|
|
1a5bcfeab9 | ||
|
|
7ef2aa8a4e | ||
|
|
a070570595 | ||
|
|
025d8d5c0b | ||
|
|
03f7237541 | ||
|
|
9821fcefc9 | ||
|
|
f90681e0df | ||
|
|
5ea3ee2f63 | ||
|
|
779b033c8b | ||
|
|
884b6f365d | ||
|
|
4c4e446f72 | ||
|
|
c624abc0ad | ||
|
|
2cc62919a6 | ||
|
|
8f7e6dcd45 | ||
|
|
a5bf40786d | ||
|
|
2a0aef5803 | ||
|
|
11fc3e293a | ||
|
|
2d009c8eaf | ||
|
|
fe362d8f0a | ||
|
|
a99e57c53c | ||
|
|
c4269b6185 | ||
|
|
dd0939ffaa | ||
|
|
7f61cf23bf | ||
|
|
022002171c | ||
|
|
0940f184fb | ||
|
|
37d95cc84a | ||
|
|
e9c118cea0 | ||
|
|
52dbe9eecf | ||
|
|
06cbe4f91e |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
env:
|
||||
CIRRUS_CLONE_DEPTH: 1
|
||||
|
||||
freebsd_12_task:
|
||||
freebsd_instance:
|
||||
image: freebsd-12-1-release-amd64
|
||||
install_script: pkg install -y go
|
||||
build_script: go build -v ./...
|
||||
test_script: go test -race ./...
|
||||
@@ -0,0 +1 @@
|
||||
*.age binary
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report about a bug in this implementation.
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Environment
|
||||
|
||||
* OS:
|
||||
* age version:
|
||||
|
||||
## What were you trying to do
|
||||
|
||||
## What happened
|
||||
|
||||
```
|
||||
<insert terminal transcript here>
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: Spec feedback
|
||||
about: Have a comment about the age spec as it's implemented by this and other tools?
|
||||
title: 'spec: '
|
||||
labels: 'spec'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- This is the issue tracker of a specific implementation of
|
||||
the age format, which is specified at https://age-encryption.org/v1
|
||||
|
||||
Please consider using the mailing list to discuss the specification:
|
||||
|
||||
https://age-encryption.org/ml -->
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
name: UX report
|
||||
about: Was age hard to use? It's not you, it's us. We want to hear about it.
|
||||
title: 'UX: '
|
||||
labels: 'UX report'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Did age not do what you expected?
|
||||
Was it hard to figure out how to do something?
|
||||
Could an error message be more helpful?
|
||||
It's not you, it's us. We want to hear about it. -->
|
||||
|
||||
## What were you trying to do
|
||||
|
||||
## What happened
|
||||
|
||||
```
|
||||
<insert terminal transcript here>
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
pull_request:
|
||||
name: Build binaries
|
||||
jobs:
|
||||
binaries:
|
||||
name: Build and upload
|
||||
runs-on: ubuntu-latest
|
||||
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 binaries
|
||||
run: |
|
||||
VERSION=$(git describe --tags)
|
||||
function build_age() {
|
||||
DIR="$(mktemp -d)"
|
||||
mkdir "$DIR/age"
|
||||
cp LICENSE "$DIR/age"
|
||||
echo -e "\n---\n" >> "$DIR/age/LICENSE"
|
||||
curl "https://golang.org/LICENSE?m=text" >> "$DIR/age/LICENSE"
|
||||
go build -o "$DIR/age" -ldflags "-X main.Version=$VERSION" ./cmd/...
|
||||
if [ "$GOOS" == "windows" ]; then
|
||||
( 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
|
||||
}
|
||||
export CGO_ENABLED=0
|
||||
GOOS=linux GOARCH=amd64 build_age
|
||||
GOOS=linux GOARCH=arm GOARM=6 build_age
|
||||
GOOS=linux GOARCH=arm64 build_age
|
||||
GOOS=darwin GOARCH=amd64 build_age
|
||||
GOOS=windows GOARCH=amd64 build_age
|
||||
- name: Upload workflow artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: age-binaries
|
||||
path: age-*
|
||||
- name: Upload release artifacts
|
||||
uses: actions/github-script@v3
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const fs = require("fs").promises;
|
||||
const { repo: { owner, repo }, sha } = context;
|
||||
|
||||
const release = await github.repos.getReleaseByTag({
|
||||
owner, repo,
|
||||
tag: process.env.GITHUB_REF.replace("refs/tags/", ""),
|
||||
});
|
||||
console.log("Release:", { release });
|
||||
|
||||
for (let file of await fs.readdir(".")) {
|
||||
if (!file.startsWith("age-")) continue;
|
||||
console.log("Uploading", file);
|
||||
await github.repos.uploadReleaseAsset({
|
||||
owner, repo,
|
||||
release_id: release.data.id,
|
||||
name: file,
|
||||
data: await fs.readFile(file),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
on: [push, pull_request]
|
||||
name: Go tip tests
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
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
|
||||
echo "GOROOT=" >> $GITHUB_ENV # workaround actions/virtual-environments#2655
|
||||
- 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
|
||||
echo "GOROOT=" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: go version
|
||||
- name: Run tests
|
||||
run: go test -race ./...
|
||||
@@ -0,0 +1,13 @@
|
||||
name: Interoperability tests
|
||||
on: push
|
||||
jobs:
|
||||
trigger:
|
||||
name: Trigger
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Trigger interoperability tests in str4d/rage
|
||||
run: |
|
||||
curl -X POST https://api.github.com/repos/str4d/rage/dispatches \
|
||||
-H 'Accept: application/vnd.github.v3+json' \
|
||||
-H 'Authorization: token ${{ secrets.RAGE_INTEROP_ACCESS_TOKEN }}' \
|
||||
--data '{"event_type": "age-interop-request", "client_payload": { "sha": "'"$GITHUB_SHA"'" }}'
|
||||
@@ -0,0 +1,33 @@
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
paths:
|
||||
- '**.ronn'
|
||||
name: Generate man pages
|
||||
jobs:
|
||||
ronn:
|
||||
runs-on: ubuntu-latest
|
||||
name: Ronn
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Run ronn
|
||||
uses: ./.github/workflows/ronn
|
||||
id: ronn
|
||||
- name: Undo email mangling
|
||||
# rdiscount randomizes the output for no good reason, which causes
|
||||
# changes to always get committed. Sigh.
|
||||
# https://github.com/davidfstr/rdiscount/blob/6b1471ec3/ext/generate.c#L781-L795
|
||||
run: |-
|
||||
for f in doc/*.html; do
|
||||
awk '/Filippo Valsorda/ { $0 = "<p>Filippo Valsorda <a href=\"mailto:age@filippo.io\" data-bare-link=\"true\">age@filippo.io</a></p>" } { print }' "$f" > "$f.tmp"
|
||||
mv "$f.tmp" "$f"
|
||||
done
|
||||
- name: Commit and push if changed
|
||||
run: |-
|
||||
git config user.name "GitHub Actions"
|
||||
git config user.email "actions@users.noreply.github.com"
|
||||
git add -A
|
||||
git commit -m "doc: regenerate groff and html man pages" || exit 0
|
||||
git push
|
||||
@@ -0,0 +1,8 @@
|
||||
FROM 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"]
|
||||
@@ -0,0 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "ronn", "~> 0.7.3"
|
||||
@@ -0,0 +1,20 @@
|
||||
GEM
|
||||
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
|
||||
@@ -0,0 +1,4 @@
|
||||
name: Ronn
|
||||
runs:
|
||||
using: docker
|
||||
image: Dockerfile
|
||||
@@ -0,0 +1,22 @@
|
||||
on: [push, pull_request]
|
||||
name: Go tests
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go: [1.15.x, 1.16.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 ./...
|
||||
@@ -0,0 +1,22 @@
|
||||
# Copyright 2019 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
|
||||
|
||||
class Age < Formula
|
||||
desc "Simple, modern, secure file encryption"
|
||||
homepage "https://filippo.io/age"
|
||||
url "https://github.com/FiloSottile/age/archive/v1.0.0-beta6.zip"
|
||||
sha256 "6ffa23aee0f03c3e00707915e4300591847a2b0c5157ca7a696eb39bfeb7359c"
|
||||
|
||||
depends_on "go" => :build
|
||||
|
||||
def install
|
||||
mkdir bin
|
||||
system "go", "build", "-trimpath", "-o", bin, "-ldflags", "-X main.Version=v#{version}", "filippo.io/age/cmd/..."
|
||||
prefix.install_metafiles
|
||||
man1.install "doc/age.1"
|
||||
man1.install "doc/age-keygen.1"
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,27 @@
|
||||
Copyright 2019 Google LLC
|
||||
|
||||
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 Google LLC 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.
|
||||
@@ -1,2 +0,0 @@
|
||||
This orphan master branch exists because it used to be checked out as
|
||||
a Homebrew Tap, and "brew update" doesn't update the default branch.
|
||||
@@ -0,0 +1,147 @@
|
||||
# age
|
||||
|
||||
[](https://pkg.go.dev/filippo.io/age)
|
||||
|
||||
age is a simple, modern and secure file encryption tool, format, and 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). To discuss the spec or other age related topics, please email [the mailing list](https://groups.google.com/d/forum/age-dev) at age-dev@googlegroups.com. age was designed by [@Benjojo12](https://twitter.com/Benjojo12) and [@FiloSottile](https://twitter.com/FiloSottile).
|
||||
|
||||
An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Usage:
|
||||
age (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
|
||||
age --passphrase [--armor] [-o OUTPUT] [INPUT]
|
||||
age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]
|
||||
|
||||
Options:
|
||||
-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.
|
||||
-d, --decrypt Decrypt the input to the output.
|
||||
-i, --identity PATH Use the identity file at PATH. Can be repeated.
|
||||
|
||||
INPUT defaults to standard input, and OUTPUT defaults to standard output.
|
||||
|
||||
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. Multiple key files can be provided, and any unused ones
|
||||
will be ignored. "-" may be used to read identities from standard input.
|
||||
```
|
||||
|
||||
### 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:
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
## Installation
|
||||
|
||||
On macOS or Linux, you can use Homebrew:
|
||||
|
||||
```
|
||||
brew tap filippo.io/age https://filippo.io/age
|
||||
brew install age
|
||||
```
|
||||
|
||||
On Windows, Linux, and macOS, you can use [the pre-built binaries](https://github.com/FiloSottile/age/releases).
|
||||
|
||||
If your system has [Go 1.13+](https://golang.org/dl/), you can build from source:
|
||||
|
||||
```
|
||||
git clone https://filippo.io/age && cd age
|
||||
go build -o . filippo.io/age/cmd/...
|
||||
```
|
||||
|
||||
On Arch Linux, age is available from AUR as [`age`](https://aur.archlinux.org/packages/age/) or [`age-git`](https://aur.archlinux.org/packages/age-git/):
|
||||
|
||||
```bash
|
||||
git clone https://aur.archlinux.org/age.git
|
||||
cd age
|
||||
makepkg -si
|
||||
```
|
||||
|
||||
On OpenBSD -current and 6.7+, you can use the port:
|
||||
|
||||
```
|
||||
pkg_add age
|
||||
```
|
||||
|
||||
On all supported versions of FreeBSD, you can build the security/age port or use pkg:
|
||||
|
||||
```
|
||||
pkg install age
|
||||
```
|
||||
|
||||
Help from new packagers is very welcome.
|
||||
@@ -0,0 +1,241 @@
|
||||
// Copyright 2019 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 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 encoraged 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")
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
for _, s := range hdr.Recipients {
|
||||
if s.Type == "scrypt" && len(hdr.Recipients) != 1 {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
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: %v", err)
|
||||
}
|
||||
|
||||
for _, r := range hdr.Recipients {
|
||||
if r.Type == "scrypt" && len(hdr.Recipients) != 1 {
|
||||
return nil, errors.New("an scrypt recipient must be the only one")
|
||||
}
|
||||
}
|
||||
|
||||
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: %v", 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
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
// Copyright 2019 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 age_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"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/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 := ioutil.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 := ioutil.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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,418 @@
|
||||
// Copyright 2019 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 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"
|
||||
"math/big"
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/internal/format"
|
||||
"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() age.Recipient {
|
||||
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
|
||||
}
|
||||
|
||||
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
|
||||
|
||||
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) {
|
||||
// ed25519.PublicKey is a little endian representation of the y-coordinate,
|
||||
// with the most significant bit set based on the sign of the x-coordinate.
|
||||
bigEndianY := make([]byte, ed25519.PublicKeySize)
|
||||
for i, b := range pk {
|
||||
bigEndianY[ed25519.PublicKeySize-i-1] = b
|
||||
}
|
||||
bigEndianY[0] &= 0b0111_1111
|
||||
|
||||
// The Montgomery u-coordinate is derived through the bilinear map
|
||||
//
|
||||
// u = (1 + y) / (1 - y)
|
||||
//
|
||||
// See https://blog.filippo.io/using-ed25519-keys-for-encryption.
|
||||
y := new(big.Int).SetBytes(bigEndianY)
|
||||
denom := new(big.Int).Sub(big.NewInt(1), y)
|
||||
if denom = denom.ModInverse(denom, curve25519P); denom == nil {
|
||||
return nil, errors.New("invalid point")
|
||||
}
|
||||
u := y.Mul(y.Add(y, big.NewInt(1)), denom)
|
||||
u.Mod(u, curve25519P)
|
||||
|
||||
out := make([]byte, curve25519.PointSize)
|
||||
uBytes := u.Bytes()
|
||||
for i, b := range uBytes {
|
||||
out[len(uBytes)-i-1] = b
|
||||
}
|
||||
|
||||
return out, 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)
|
||||
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() age.Recipient {
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright 2019 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 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright 2019 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 agessh
|
||||
|
||||
import (
|
||||
"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
|
||||
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) {
|
||||
switch t := pubKey.Type(); t {
|
||||
case "ssh-ed25519", "ssh-rsa":
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
|
||||
}
|
||||
return &EncryptedSSHIdentity{
|
||||
pubKey: pubKey,
|
||||
pemBytes: pemBytes,
|
||||
passphrase: passphrase,
|
||||
}, nil
|
||||
}
|
||||
|
||||
var _ age.Identity = &EncryptedSSHIdentity{}
|
||||
|
||||
func (i *EncryptedSSHIdentity) Recipient() (age.Recipient, error) {
|
||||
return ParseRecipient(string(ssh.MarshalAuthorizedKey(i.pubKey)))
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
switch k := k.(type) {
|
||||
case *ed25519.PrivateKey:
|
||||
i.decrypted, err = NewEd25519Identity(*k)
|
||||
// TODO: here and below, better check that the two public keys match,
|
||||
// rather than just the type.
|
||||
if i.pubKey.Type() != ssh.KeyAlgoED25519 {
|
||||
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", ssh.KeyAlgoED25519, i.pubKey.Type())
|
||||
}
|
||||
case *rsa.PrivateKey:
|
||||
i.decrypted, err = NewRSAIdentity(k)
|
||||
if i.pubKey.Type() != ssh.KeyAlgoRSA {
|
||||
return nil, fmt.Errorf("mismatched private (%s) and public (%s) SSH key types", ssh.KeyAlgoRSA, i.pubKey.Type())
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid SSH key: %v", err)
|
||||
}
|
||||
|
||||
return i.decrypted.Unwrap(stanzas)
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Copyright 2019 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 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"
|
||||
"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 != nil && len(line) == 0 {
|
||||
if err == io.EOF {
|
||||
err = errors.New("invalid armor: unexpected EOF")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return bytes.TrimSpace(line), nil
|
||||
}
|
||||
|
||||
if !r.started {
|
||||
line, err := getLine()
|
||||
if err != nil {
|
||||
return 0, r.setErr(err)
|
||||
}
|
||||
if string(line) != Header {
|
||||
return 0, r.setErr(errors.New("invalid armor first line: " + string(line)))
|
||||
}
|
||||
r.started = true
|
||||
}
|
||||
line, err := getLine()
|
||||
if err != nil {
|
||||
return 0, r.setErr(err)
|
||||
}
|
||||
if string(line) == Footer {
|
||||
return 0, r.setErr(io.EOF)
|
||||
}
|
||||
if len(line) > format.ColumnsPerLine {
|
||||
return 0, r.setErr(errors.New("invalid armor: column limit exceeded"))
|
||||
}
|
||||
r.unread = r.buf[:]
|
||||
n, err := base64.StdEncoding.Strict().Decode(r.unread, line)
|
||||
if err != nil {
|
||||
return 0, r.setErr(errors.New("invalid armor: " + err.Error()))
|
||||
}
|
||||
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(errors.New("invalid armor closing line: " + string(line)))
|
||||
}
|
||||
r.err = io.EOF
|
||||
}
|
||||
|
||||
nn := copy(p, r.unread)
|
||||
r.unread = r.unread[nn:]
|
||||
return nn, nil
|
||||
}
|
||||
|
||||
func (r *armoredReader) setErr(err error) error {
|
||||
r.err = err
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2019 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 armor_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"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 := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(out, plain) {
|
||||
t.Error("decoded value doesn't match")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright 2019 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 main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
"filippo.io/age"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
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 {
|
||||
log.Fatalf("age-keygen takes no arguments")
|
||||
}
|
||||
if len(flag.Args()) > 1 && convertFlag {
|
||||
log.Fatalf("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 {
|
||||
log.Fatalf("Failed to open output file %q: %v", outFlag, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatalf("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 {
|
||||
log.Fatalf("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 {
|
||||
fmt.Fprintf(os.Stderr, "Warning: writing secret key to a world-readable file.\n")
|
||||
}
|
||||
generate(out)
|
||||
}
|
||||
}
|
||||
|
||||
func generate(out *os.File) {
|
||||
k, err := age.GenerateX25519Identity()
|
||||
if err != nil {
|
||||
log.Fatalf("Internal error: %v", err)
|
||||
}
|
||||
|
||||
if !terminal.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 {
|
||||
log.Fatalf("Failed to parse input: %v", err)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
log.Fatalf("No identities found in the input")
|
||||
}
|
||||
for _, id := range ids {
|
||||
id, ok := id.(*age.X25519Identity)
|
||||
if !ok {
|
||||
log.Fatalf("Internal error: unexpected identity type: %T", id)
|
||||
}
|
||||
fmt.Fprintf(out, "%s\n", id.Recipient())
|
||||
}
|
||||
}
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
// Copyright 2019 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 main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
_log "log"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"filippo.io/age/armor"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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. 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
|
||||
|
||||
func main() {
|
||||
_log.SetFlags(0)
|
||||
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
|
||||
|
||||
if len(os.Args) == 1 {
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var (
|
||||
outFlag string
|
||||
decryptFlag, encryptFlag bool
|
||||
passFlag, versionFlag, armorFlag bool
|
||||
recipientFlags, identityFlags multiFlag
|
||||
recipientsFileFlags multiFlag
|
||||
)
|
||||
|
||||
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.Var(&identityFlags, "i", "identity (can be repeated)")
|
||||
flag.Var(&identityFlags, "identity", "identity (can be repeated)")
|
||||
flag.Parse()
|
||||
|
||||
if versionFlag {
|
||||
if Version != "" {
|
||||
fmt.Println(Version)
|
||||
return
|
||||
}
|
||||
if buildInfo, ok := debug.ReadBuildInfo(); ok {
|
||||
fmt.Println(buildInfo.Main.Version)
|
||||
return
|
||||
}
|
||||
fmt.Println("(unknown)")
|
||||
return
|
||||
}
|
||||
|
||||
if flag.NArg() > 1 {
|
||||
logFatalf("Error: too many arguments: %q.\n"+
|
||||
"Note that the input file must be specified after all flags.", flag.Args())
|
||||
}
|
||||
switch {
|
||||
case decryptFlag:
|
||||
if encryptFlag {
|
||||
logFatalf("Error: -e/--encrypt can't be used with -d/--decrypt.")
|
||||
}
|
||||
if armorFlag {
|
||||
logFatalf("Error: -a/--armor can't be used with -d/--decrypt.\n" +
|
||||
"Note that armored files are detected automatically.")
|
||||
}
|
||||
if passFlag {
|
||||
logFatalf("Error: -p/--passphrase can't be used with -d/--decrypt.\n" +
|
||||
"Note that password protected files are detected automatically.")
|
||||
}
|
||||
if len(recipientFlags) > 0 {
|
||||
logFatalf("Error: -r/--recipient can't be used with -d/--decrypt.\n" +
|
||||
"Did you mean to use -i/--identity to specify a private key?")
|
||||
}
|
||||
if len(recipientsFileFlags) > 0 {
|
||||
logFatalf("Error: -R/--recipients-file can't be used with -d/--decrypt.\n" +
|
||||
"Did you mean to use -i/--identity to specify a private key?")
|
||||
}
|
||||
default: // encrypt
|
||||
if len(identityFlags) > 0 && !encryptFlag {
|
||||
logFatalf("Error: -i/--identity can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt.\n" +
|
||||
"Did you forget to specify -d/--decrypt?")
|
||||
}
|
||||
if len(recipientFlags)+len(recipientsFileFlags)+len(identityFlags) == 0 && !passFlag {
|
||||
logFatalf("Error: missing recipients.\n" +
|
||||
"Did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
|
||||
}
|
||||
if len(recipientFlags) > 0 && passFlag {
|
||||
logFatalf("Error: -p/--passphrase can't be combined with -r/--recipient.")
|
||||
}
|
||||
if len(recipientsFileFlags) > 0 && passFlag {
|
||||
logFatalf("Error: -p/--passphrase can't be combined with -R/--recipients-file.")
|
||||
}
|
||||
if len(identityFlags) > 0 && passFlag {
|
||||
logFatalf("Error: -p/--passphrase can't be combined with -i/--identity.")
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
logFatalf("Error: failed to open input file %q: %v", name, err)
|
||||
}
|
||||
defer f.Close()
|
||||
in = f
|
||||
} else {
|
||||
stdinInUse = true
|
||||
}
|
||||
if name := outFlag; name != "" && name != "-" {
|
||||
f := newLazyOpener(name)
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
logFatalf("Error: failed to close output file %q: %v", name, err)
|
||||
}
|
||||
}()
|
||||
out = f
|
||||
} else if terminal.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 -".
|
||||
logFatalf("Error: refusing to output binary to the terminal.\n" +
|
||||
`Did you mean to use -a/--armor? Force with "-o -".`)
|
||||
}
|
||||
}
|
||||
if in == os.Stdin && terminal.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:
|
||||
decrypt(identityFlags, in, out)
|
||||
case passFlag:
|
||||
pass, err := passphrasePromptForEncryption()
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
encryptPass(pass, in, out, armorFlag)
|
||||
default:
|
||||
encryptKeys(recipientFlags, recipientsFileFlags, identityFlags, in, out, armorFlag)
|
||||
}
|
||||
}
|
||||
|
||||
func passphrasePromptForEncryption() (string, error) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase (leave empty to autogenerate a secure one): ")
|
||||
pass, err := readPassphrase()
|
||||
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, "-")
|
||||
fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "Confirm passphrase: ")
|
||||
confirm, err := readPassphrase()
|
||||
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 encryptKeys(keys, files, identities []string, in io.Reader, out io.Writer, armor bool) {
|
||||
var recipients []age.Recipient
|
||||
for _, arg := range keys {
|
||||
r, err := parseRecipient(arg)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
recipients = append(recipients, r)
|
||||
}
|
||||
for _, name := range files {
|
||||
recs, err := parseRecipientsFile(name)
|
||||
if err != nil {
|
||||
logFatalf("Error: failed to parse recipient file %q: %v", name, err)
|
||||
}
|
||||
recipients = append(recipients, recs...)
|
||||
}
|
||||
for _, name := range identities {
|
||||
ids, err := parseIdentitiesFile(name)
|
||||
if err != nil {
|
||||
logFatalf("Error reading %q: %v", name, err)
|
||||
}
|
||||
for _, id := range ids {
|
||||
r, err := identityToRecipient(id)
|
||||
if err != nil {
|
||||
logFatalf("Internal error processing %q: %v", name, err)
|
||||
}
|
||||
recipients = append(recipients, r)
|
||||
}
|
||||
}
|
||||
encrypt(recipients, in, out, armor)
|
||||
}
|
||||
|
||||
func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) {
|
||||
r, err := age.NewScryptRecipient(pass)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
encrypt([]age.Recipient{r}, in, out, armor)
|
||||
}
|
||||
|
||||
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 {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
}()
|
||||
out = a
|
||||
}
|
||||
w, err := age.Encrypt(out, recipients...)
|
||||
if err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
if _, err := io.Copy(w, in); err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func decrypt(keys []string, 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{passphrasePrompt},
|
||||
}
|
||||
|
||||
for _, name := range keys {
|
||||
ids, err := parseIdentitiesFile(name)
|
||||
if err != nil {
|
||||
logFatalf("Error reading %q: %v", name, err)
|
||||
}
|
||||
identities = append(identities, ids...)
|
||||
}
|
||||
|
||||
rr := bufio.NewReader(in)
|
||||
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 {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
if _, err := io.Copy(out, r); err != nil {
|
||||
logFatalf("Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func passphrasePrompt() (string, error) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase: ")
|
||||
pass, err := readPassphrase()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not read passphrase: %v", err)
|
||||
}
|
||||
return string(pass), nil
|
||||
}
|
||||
|
||||
func identityToRecipient(id age.Identity) (age.Recipient, error) {
|
||||
switch id := id.(type) {
|
||||
case *age.X25519Identity:
|
||||
return id.Recipient(), nil
|
||||
case *agessh.RSAIdentity:
|
||||
return id.Recipient(), nil
|
||||
case *agessh.Ed25519Identity:
|
||||
return id.Recipient(), nil
|
||||
case *agessh.EncryptedSSHIdentity:
|
||||
return id.Recipient()
|
||||
}
|
||||
return nil, fmt.Errorf("unexpected identity type: %T", id)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func logFatalf(format string, v ...interface{}) {
|
||||
_log.Printf(format, v...)
|
||||
_log.Fatalf("[ Did age not do what you expected? Could an error be more useful?" +
|
||||
" Tell us: https://filippo.io/age/report ]")
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2019 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 main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
func TestVectors(t *testing.T) {
|
||||
defaultIDs, err := parseIdentitiesFile("testdata/default_key.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
password, err := ioutil.ReadFile("testdata/default_password.txt")
|
||||
if err == nil {
|
||||
p := strings.TrimSpace(string(password))
|
||||
i, err := age.NewScryptIdentity(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defaultIDs = append(defaultIDs, i)
|
||||
}
|
||||
|
||||
files, _ := filepath.Glob("testdata/*.age")
|
||||
for _, f := range files {
|
||||
_, name := filepath.Split(f)
|
||||
name = strings.TrimSuffix(name, ".age")
|
||||
expectFailure := strings.HasPrefix(name, "fail_")
|
||||
expectNoMatch := strings.HasPrefix(name, "nomatch_")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
identities := defaultIDs
|
||||
ids, err := parseIdentitiesFile("testdata/" + name + "_key.txt")
|
||||
if err == nil {
|
||||
identities = ids
|
||||
}
|
||||
password, err := ioutil.ReadFile("testdata/" + name + "_password.txt")
|
||||
if err == nil {
|
||||
p := strings.TrimSpace(string(password))
|
||||
i, err := age.NewScryptIdentity(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
identities = []age.Identity{i}
|
||||
}
|
||||
|
||||
in, err := os.Open("testdata/" + name + ".age")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r, err := age.Decrypt(in, identities...)
|
||||
if expectFailure {
|
||||
if err == nil {
|
||||
t.Fatal("expected Decrypt failure")
|
||||
}
|
||||
if e := (&age.NoIdentityMatchError{}); errors.As(err, &e) {
|
||||
t.Errorf("got ErrIncorrectIdentity, expected more specific error")
|
||||
}
|
||||
} else if expectNoMatch {
|
||||
if err == nil {
|
||||
t.Fatal("expected Decrypt failure")
|
||||
}
|
||||
if e := (&age.NoIdentityMatchError{}); !errors.As(err, &e) {
|
||||
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2019 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 main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"filippo.io/age"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type LazyScryptIdentity struct {
|
||||
Passphrase func() (string, error)
|
||||
}
|
||||
|
||||
var _ age.Identity = &LazyScryptIdentity{}
|
||||
|
||||
func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
|
||||
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
|
||||
}
|
||||
|
||||
// readPassphrase reads a passphrase from the terminal. If stdin is not
|
||||
// connected to a terminal, it tries /dev/tty and fails if that's not available.
|
||||
// It does not read from a non-terminal stdin, so it does not check stdinInUse.
|
||||
func readPassphrase() ([]byte, error) {
|
||||
fd := int(os.Stdin.Fd())
|
||||
if !terminal.IsTerminal(fd) {
|
||||
tty, err := os.Open("/dev/tty")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("standard input is not a terminal, and opening /dev/tty failed: %v", err)
|
||||
}
|
||||
defer tty.Close()
|
||||
fd = int(tty.Fd())
|
||||
}
|
||||
defer fmt.Fprintf(os.Stderr, "\n")
|
||||
p, err := terminal.ReadPassword(fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2019 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 main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age"
|
||||
"filippo.io/age/agessh"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// stdinInUse is set in main. It's a singleton like os.Stdin.
|
||||
var stdinInUse bool
|
||||
|
||||
func parseRecipient(arg string) (age.Recipient, error) {
|
||||
switch {
|
||||
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, fmt.Errorf(`"github:" recipients were removed from the design.`+"\n"+
|
||||
"Instead, use recipient files like\n\n curl -O https://github.com/%s.keys\n age -R %s.keys\n\n", name, 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.
|
||||
log.Printf("Warning: 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 of *age.X25519Identity, *agessh.RSAIdentity, *agessh.Ed25519Identity, or
|
||||
// *agessh.EncryptedSSHIdentity.
|
||||
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)
|
||||
const pemHeader = "-----BEGIN"
|
||||
if peeked, _ := b.Peek(len(pemHeader)); string(peeked) == pemHeader {
|
||||
const privateKeySizeLimit = 1 << 14 // 16 KiB
|
||||
contents, err := ioutil.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)
|
||||
}
|
||||
|
||||
ids, err := age.ParseIdentities(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %v", name, err)
|
||||
}
|
||||
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) {
|
||||
fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name)
|
||||
pass, err := readPassphrase()
|
||||
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 := ioutil.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
|
||||
}
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
# created: 2021-02-02T13:09:43+01:00
|
||||
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
|
||||
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
|
||||
|
||||
# TODO: regenerate empty_recipient_body.age
|
||||
AGE-SECRET-KEY-1TRYTV7PQS5XPUYSTAQZCD7DQCWC7Q77YJD7UVFJRMW4J82Q6930QS70MRX
|
||||
+1
@@ -0,0 +1 @@
|
||||
now-major-idea-author-clerk-bronze-all-soul-uncover-glad
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
age-encryption.org/v1
|
||||
-> ssh-ed25519 cp09gQ Kf5JDNFFDUaOvups2MDfP47PlrpJthnmz0WNMfGj+C8
|
||||
hQ76lMAsG2pjR8GHTU+XU0giePyzE3prVmAw5MbMxSk
|
||||
--- CjO8qf3vd83otqHSflgWP5gQoe2Roo9tf/zgWEy9t0U
|
||||
èÙ®Ò%JðÇnj<bÖ: %UgZ4#™±àNÂ韟
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2gAAAKDgV/GC4Ffx
|
||||
ggAAAAtzc2gtZWQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2g
|
||||
AAAECvFoQXQzXgJLQ+Gz4PfEcfyZwC2gUjOiWTD//mTPyD8H9pO5pz22JZEasoS2LEWWvJ
|
||||
iUYI9M6l1uZc31FGYMXaAAAAG2ZpbGlwcG9AQmlzdHJvbWF0aC1NMS5sb2NhbAEC
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FGYMXa
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 alRneDshIh43nwyD5+fhuTD5TReSn88f2us4hzZPyzU
|
||||
pGduNK5MUhnuzMxW0qbZnC2k7mRzz69bbJpKQrRc7uc
|
||||
-> A7)h-grease !,_
|
||||
|
||||
--- 5bA0uXjBxI6wuI5SseCRgD5/G8LkSVISRe/hnrQMb9s
|
||||
¦Ã 1‘·¬Žä6_RÙäÚ…›…U<ï1ús®ÿ?`Ö+–’$§HÖW‚v?w8ZW
|
||||
@@ -0,0 +1,7 @@
|
||||
age-encryption.org/v1
|
||||
-> scrypt z8U9dYMQuK1fFdvtpQYLEQ 10
|
||||
5SVjw1bbFCZLdI1FR7RqfTd3yWo4KS1ikOjvz60Bpqhrv0W6o6/2oszxZEm1gEUC
|
||||
|
||||
--- YXxSwONGPBbV7woMuEFTYuA03qTYUF1k0Y8j/NDEu1o
|
||||
’Ì
|
||||
Å!í‡ i�Þ.f¦æš{ŸdËIE]© ñn”$È!b®2
|
||||
@@ -0,0 +1,6 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 UkSgrxSETNpdkHY8EwiiRivqks2QJLUzsNsVjUTDcmw
|
||||
8yB9TqsBo4Ypchw07AtemV5TW4sGwyPDPMIfRg8Ve8rbDXt4tCwnnKcMq2K6aoqx
|
||||
|
||||
--- vUhLU0U9Dc8YhbKy4SxKuq0iSqqjBWGnHfZG+9+O4v4
|
||||
タケ��イhエW諾餞I暠fッニ�索Q;ミRh皷w
|
||||
BIN
Binary file not shown.
@@ -0,0 +1,5 @@
|
||||
age-encryption.org/v1
|
||||
-> scrypt 1Q6WlGmsRulbN7bmUw8A1Q 23
|
||||
GP2lnzFuk1dgEkcMPmK6KkmuOm5gIWJzLeuwGcRsvAY
|
||||
--- vXvOsVbDbMc0x5Js1FS6k1ViOJ3H2ZdSUZo9bfvbzmU
|
||||
=ñÅÙœ£¨v>¹vhKMû'ÕßúÙ½NeçS†Äñ\(_ó
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
age-encryption.org/v1
|
||||
-> scrypt X6oOTRAjCR1xid0PlnNMFA 10
|
||||
hszKAHhyFVpUgt9niYpdYXVhhN+r+oiCLPZukDdQZBQ
|
||||
--- 7BRJPVjbIC1JntvHrA13PQrnsa3lkwhnNF/Pbo4BPs4
|
||||
4|)ÆS|Û‹Ò¿D�÷§}’Í2è%�e�=óœ6˜»ÇZ
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 Rp86RQ3LgUJpQy4X2RMUhURlBP28tCaLQ2ssysJfRhg
|
||||
83YXad/lj3/wFM4n7vlGIiBSgfhG8lfiP5U7ajjK3HM
|
||||
--- O2+UpzetsP2+7BPyGQ4C6VMTY6zwp5TiNpVcFy4qdyM
|
||||
璇漡�(擔Q:|c蔑鞮蓤�=f�6b坿!
|
||||
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
age-encryption.org/v1
|
||||
-> ssh-rsa jw/33g
|
||||
xv12FD3f2d7snIcuXBznTOWAlgCovW1Dqttk9uljWKy3GtRZ3t8jEWkQEOYkOk0M
|
||||
EHA6sHWfdPLdlS3DjQYjcaLFvwh3+XVKYNzP9MhLg8P8xvxVkn4aiCKd8ivEisp0
|
||||
bKGi4g/TJz/JKUg1SGbqDg966to0P5AWrkwAD7OMykQToqo56flrKXgFPleSWVWu
|
||||
umiwbxFYs7ltbRYvjzdpIj9l30lXkzrADP3RrrvTu/qT0IN3PMi3bOqm0kKz0vkd
|
||||
p4NpxKmfqQXavU+YZiyQL637V3cbKIAEJ1qmpkd2Tr2oUhfD5IgAoT1nC5tCIzRb
|
||||
DkPwM4k2FJgVX0KKvW3i0+k5tve4XWg82vq2OCj8+sl3A8cLX3g5zhh53DovUBVm
|
||||
qDU2HWf++3q9kUy1al0sFb2es4ih+tK74nPjBJZtX0n+4lMngz557+XuYnzZ2OkW
|
||||
QEq3b7Trdidw7Ak9S14tdXhj8oy7J1jdHsQ8/wehAc1v8MuBb1O7LxVIFxzBEBCA
|
||||
|
||||
--- QdCY4BN4vwp5jb+AFsyoHkvKW+EneZsZjPURH2tCF18
|
||||
«)KVMARsS‘M’ÚëûØ‚Kí>›ÕÕ«`¤~T0n·
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
-----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-----
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbigbhsQaZo2oXLgMug8nTfnzDhcf5cEzdOJZyF6i5aRQbqbXIOYeTS3Shpp/iE6d60qi93JBBfveAZqr76tK7lVK8fstvAAGgbo9L9Ru5nhWX0HTlZUOodUT2E0rgAeoFzfvcZTaajzPmikrESmuaJLcdJ3crbCIyovkTxc85KbSn+Ky5grntdVR7GXve0HuIgwuSXGNyO0/hEQyhCEco4w53LZDXl3Dlxgb52u0QCsFFiQJmZ3RWnbCz2mNjM9Vo5KRt2pJ4u74lVsfXCvAqG4GsDd5WXCNRNaQ7gUdw1eSnvvcy3hxeEqLNN9tHaFCjEuyPfXfTVd3t0SlXreyoNbV75rYvZ2F6aLxyMMys+5bpnX0tu2LSq4Bvj5xIsBokjgEd3UXyj1eErsQdUJ0gghAMpXOjVbISwPAGMvUsH2unGbjvfaxnK57S7KAXOYznjhBf5ayIleAc+4LVD/KeMtGbpkyXXozC9jwH9cZBbGU4BtElAbzh8=
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
age-encryption.org/v1
|
||||
-> scrypt qEa/WztCd2KJ4mKwNf1Yrw 10
|
||||
TQZ4GpAaH4aR4oSDWZTgeRT4wRby4jwmtB02dElWmVQ
|
||||
--- kOiEP6uoMyK9GKIsV77o4oaPuEr2Q0vdcu+1RKC3lLU
|
||||
hŸo²P¤V
w§\5~4ôënE½oÕd>rOš°mÒÛ¨
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019 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 main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func randomWord() string {
|
||||
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 gLine truncated
|
||||
@@ -0,0 +1,88 @@
|
||||
.\" generated with Ronn/v0.7.3
|
||||
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
||||
.
|
||||
.TH "AGE\-KEYGEN" "1" "October 2021" "" ""
|
||||
.
|
||||
.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
|
||||
@@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv='content-type' value='text/html;charset=utf8'>
|
||||
<meta name='generator' value='Ronn/v0.7.3 (http://github.com/rtomayko/ronn/tree/0.7.3)'>
|
||||
<title>age-keygen(1) - generate age(1) key pairs</title>
|
||||
<style type='text/css' media='all'>
|
||||
/* style: man */
|
||||
body#manpage {margin:0}
|
||||
.mp {max-width:100ex;padding:0 9ex 1ex 4ex}
|
||||
.mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
|
||||
.mp h2 {margin:10px 0 0 0}
|
||||
.mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
|
||||
.mp h3 {margin:0 0 0 4ex}
|
||||
.mp dt {margin:0;clear:left}
|
||||
.mp dt.flush {float:left;width:8ex}
|
||||
.mp dd {margin:0 0 0 9ex}
|
||||
.mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
|
||||
.mp pre {margin-bottom:20px}
|
||||
.mp pre+h2,.mp pre+h3 {margin-top:22px}
|
||||
.mp h2+pre,.mp h3+pre {margin-top:5px}
|
||||
.mp img {display:block;margin:auto}
|
||||
.mp h1.man-title {display:none}
|
||||
.mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
|
||||
.mp h2 {font-size:16px;line-height:1.25}
|
||||
.mp h1 {font-size:20px;line-height:2}
|
||||
.mp {text-align:justify;background:#fff}
|
||||
.mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
|
||||
.mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
|
||||
.mp u {text-decoration:underline}
|
||||
.mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
|
||||
.mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
|
||||
.mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
|
||||
.mp b.man-ref {font-weight:normal;color:#434241}
|
||||
.mp pre {padding:0 4ex}
|
||||
.mp pre code {font-weight:normal;color:#434241}
|
||||
.mp h2+pre,h3+pre {padding-left:0}
|
||||
ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
|
||||
ol.man-decor {width:100%}
|
||||
ol.man-decor li.tl {text-align:left}
|
||||
ol.man-decor li.tc {text-align:center;letter-spacing:4px}
|
||||
ol.man-decor li.tr {text-align:right;float:right}
|
||||
</style>
|
||||
</head>
|
||||
<!--
|
||||
The following styles are deprecated and will be removed at some point:
|
||||
div#man, div#man ol.man, div#man ol.head, div#man ol.man.
|
||||
|
||||
The .man-page, .man-decor, .man-head, .man-foot, .man-title, and
|
||||
.man-navigation should be used instead.
|
||||
-->
|
||||
<body id='manpage'>
|
||||
<div class='mp' id='man'>
|
||||
|
||||
<div class='man-navigation' style='display:none'>
|
||||
<a href="#NAME">NAME</a>
|
||||
<a href="#SYNOPSIS">SYNOPSIS</a>
|
||||
<a href="#DESCRIPTION">DESCRIPTION</a>
|
||||
<a href="#OPTIONS">OPTIONS</a>
|
||||
<a href="#EXAMPLES">EXAMPLES</a>
|
||||
<a href="#SEE-ALSO">SEE ALSO</a>
|
||||
<a href="#AUTHORS">AUTHORS</a>
|
||||
</div>
|
||||
|
||||
<ol class='man-decor man-head man head'>
|
||||
<li class='tl'>age-keygen(1)</li>
|
||||
<li class='tc'></li>
|
||||
<li class='tr'>age-keygen(1)</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="NAME">NAME</h2>
|
||||
<p class="man-name">
|
||||
<code>age-keygen</code> - <span class="man-whatis">generate <a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a> key pairs</span>
|
||||
</p>
|
||||
|
||||
<h2 id="SYNOPSIS">SYNOPSIS</h2>
|
||||
|
||||
<p><code>age-keygen</code> [<code>-o</code> <var>OUTPUT</var>]<br />
|
||||
<code>age-keygen</code> <code>-y</code> [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br /></p>
|
||||
|
||||
<h2 id="DESCRIPTION">DESCRIPTION</h2>
|
||||
|
||||
<p><code>age-keygen</code> generates a new native <a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a> key pair, and outputs the identity to
|
||||
standard output or to the <var>OUTPUT</var> file. The output includes the public key and
|
||||
the current time as comments.</p>
|
||||
|
||||
<p>If the output is not going to a terminal, <code>age-keygen</code> prints the public key to
|
||||
standard error.</p>
|
||||
|
||||
<h2 id="OPTIONS">OPTIONS</h2>
|
||||
|
||||
<dl>
|
||||
<dt><code>-o</code>, <code>--output</code>=<var>OUTPUT</var></dt><dd><p> Write the identity to <var>OUTPUT</var> instead of standard output.</p>
|
||||
|
||||
<p> If <var>OUTPUT</var> already exists, it is not overwritten.</p></dd>
|
||||
<dt class="flush"><code>-y</code></dt><dd><p> Read an identity file from <var>INPUT</var> or from standard input and output the
|
||||
corresponding recipient(s), one per line, with no comments.</p></dd>
|
||||
<dt><code>--version</code></dt><dd><p> Print the version and exit.</p></dd>
|
||||
</dl>
|
||||
|
||||
|
||||
<h2 id="EXAMPLES">EXAMPLES</h2>
|
||||
|
||||
<p>Generate a new identity:</p>
|
||||
|
||||
<pre><code>$ age-keygen
|
||||
# created: 2021-01-02T15:30:45+01:00
|
||||
# public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z
|
||||
AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9
|
||||
</code></pre>
|
||||
|
||||
<p>Write a new identity to <code>key.txt</code>:</p>
|
||||
|
||||
<pre><code>$ age-keygen -o key.txt
|
||||
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
</code></pre>
|
||||
|
||||
<p>Convert an identity to a recipient:</p>
|
||||
|
||||
<pre><code>$ age-keygen -y key.txt
|
||||
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
</code></pre>
|
||||
|
||||
<h2 id="SEE-ALSO">SEE ALSO</h2>
|
||||
|
||||
<p><a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a></p>
|
||||
|
||||
<h2 id="AUTHORS">AUTHORS</h2>
|
||||
|
||||
<p>Filippo Valsorda <a href="mailto:age@filippo.io" data-bare-link="true">age@filippo.io</a></p>
|
||||
|
||||
|
||||
<ol class='man-decor man-foot man foot'>
|
||||
<li class='tl'></li>
|
||||
<li class='tc'>October 2021</li>
|
||||
<li class='tr'>age-keygen(1)</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,57 @@
|
||||
age-keygen(1) -- generate age(1) key pairs
|
||||
====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`age-keygen` [`-o` <OUTPUT>]<br>
|
||||
`age-keygen` `-y` [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
|
||||
## 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>
|
||||
@@ -0,0 +1,296 @@
|
||||
.\" generated with Ronn/v0.7.3
|
||||
.\" http://github.com/rtomayko/ronn/tree/0.7.3
|
||||
.
|
||||
.TH "AGE" "1" "October 2021" "" ""
|
||||
.
|
||||
.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\-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\-\-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\-\-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 \fB\-R\fR/\fB\-\-recipients\-file\fR, 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 \fB\-r\fR/\fB\-\-recipient\fR, 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 \fB\-r\fR/\fB\-\-recipient\fR or \fB\-R\fR/\fB\-\-recipients\-file\fR\.
|
||||
.
|
||||
.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\.
|
||||
.
|
||||
.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\. 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
|
||||
c\. "\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\.
|
||||
.
|
||||
.IP
|
||||
If \fB\-e\fR/\fB\-\-encrypt\fR is explicitly specified (to avoid confusion), \fB\-i\fR/\fB\-\-identity\fR may also be used to encrypt to the \fBRECIPIENTS\fR corresponding to the \fBIDENTITIES\fR listed at \fIPATH\fR\. This allows using an identity file as a symmetric key, if desired\.
|
||||
.
|
||||
.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\.
|
||||
.
|
||||
.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 unauthenticathed 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 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 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
|
||||
+328
@@ -0,0 +1,328 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv='content-type' value='text/html;charset=utf8'>
|
||||
<meta name='generator' value='Ronn/v0.7.3 (http://github.com/rtomayko/ronn/tree/0.7.3)'>
|
||||
<title>age(1) - simple, modern, and secure file encryption</title>
|
||||
<style type='text/css' media='all'>
|
||||
/* style: man */
|
||||
body#manpage {margin:0}
|
||||
.mp {max-width:100ex;padding:0 9ex 1ex 4ex}
|
||||
.mp p,.mp pre,.mp ul,.mp ol,.mp dl {margin:0 0 20px 0}
|
||||
.mp h2 {margin:10px 0 0 0}
|
||||
.mp > p,.mp > pre,.mp > ul,.mp > ol,.mp > dl {margin-left:8ex}
|
||||
.mp h3 {margin:0 0 0 4ex}
|
||||
.mp dt {margin:0;clear:left}
|
||||
.mp dt.flush {float:left;width:8ex}
|
||||
.mp dd {margin:0 0 0 9ex}
|
||||
.mp h1,.mp h2,.mp h3,.mp h4 {clear:left}
|
||||
.mp pre {margin-bottom:20px}
|
||||
.mp pre+h2,.mp pre+h3 {margin-top:22px}
|
||||
.mp h2+pre,.mp h3+pre {margin-top:5px}
|
||||
.mp img {display:block;margin:auto}
|
||||
.mp h1.man-title {display:none}
|
||||
.mp,.mp code,.mp pre,.mp tt,.mp kbd,.mp samp,.mp h3,.mp h4 {font-family:monospace;font-size:14px;line-height:1.42857142857143}
|
||||
.mp h2 {font-size:16px;line-height:1.25}
|
||||
.mp h1 {font-size:20px;line-height:2}
|
||||
.mp {text-align:justify;background:#fff}
|
||||
.mp,.mp code,.mp pre,.mp pre code,.mp tt,.mp kbd,.mp samp {color:#131211}
|
||||
.mp h1,.mp h2,.mp h3,.mp h4 {color:#030201}
|
||||
.mp u {text-decoration:underline}
|
||||
.mp code,.mp strong,.mp b {font-weight:bold;color:#131211}
|
||||
.mp em,.mp var {font-style:italic;color:#232221;text-decoration:none}
|
||||
.mp a,.mp a:link,.mp a:hover,.mp a code,.mp a pre,.mp a tt,.mp a kbd,.mp a samp {color:#0000ff}
|
||||
.mp b.man-ref {font-weight:normal;color:#434241}
|
||||
.mp pre {padding:0 4ex}
|
||||
.mp pre code {font-weight:normal;color:#434241}
|
||||
.mp h2+pre,h3+pre {padding-left:0}
|
||||
ol.man-decor,ol.man-decor li {margin:3px 0 10px 0;padding:0;float:left;width:33%;list-style-type:none;text-transform:uppercase;color:#999;letter-spacing:1px}
|
||||
ol.man-decor {width:100%}
|
||||
ol.man-decor li.tl {text-align:left}
|
||||
ol.man-decor li.tc {text-align:center;letter-spacing:4px}
|
||||
ol.man-decor li.tr {text-align:right;float:right}
|
||||
</style>
|
||||
</head>
|
||||
<!--
|
||||
The following styles are deprecated and will be removed at some point:
|
||||
div#man, div#man ol.man, div#man ol.head, div#man ol.man.
|
||||
|
||||
The .man-page, .man-decor, .man-head, .man-foot, .man-title, and
|
||||
.man-navigation should be used instead.
|
||||
-->
|
||||
<body id='manpage'>
|
||||
<div class='mp' id='man'>
|
||||
|
||||
<div class='man-navigation' style='display:none'>
|
||||
<a href="#NAME">NAME</a>
|
||||
<a href="#SYNOPSIS">SYNOPSIS</a>
|
||||
<a href="#DESCRIPTION">DESCRIPTION</a>
|
||||
<a href="#OPTIONS">OPTIONS</a>
|
||||
<a href="#RECIPIENTS-AND-IDENTITIES">RECIPIENTS AND IDENTITIES</a>
|
||||
<a href="#EXIT-STATUS">EXIT STATUS</a>
|
||||
<a href="#BACKWARDS-COMPATIBILITY">BACKWARDS COMPATIBILITY</a>
|
||||
<a href="#EXAMPLES">EXAMPLES</a>
|
||||
<a href="#SEE-ALSO">SEE ALSO</a>
|
||||
<a href="#AUTHORS">AUTHORS</a>
|
||||
</div>
|
||||
|
||||
<ol class='man-decor man-head man head'>
|
||||
<li class='tl'>age(1)</li>
|
||||
<li class='tc'></li>
|
||||
<li class='tr'>age(1)</li>
|
||||
</ol>
|
||||
|
||||
<h2 id="NAME">NAME</h2>
|
||||
<p class="man-name">
|
||||
<code>age</code> - <span class="man-whatis">simple, modern, and secure file encryption</span>
|
||||
</p>
|
||||
|
||||
<h2 id="SYNOPSIS">SYNOPSIS</h2>
|
||||
|
||||
<p><code>age</code> [<code>--encrypt</code>] (<code>-r</code> <var>RECIPIENT</var> | <code>-R</code> <var>PATH</var>)... [<code>--armor</code>] [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br />
|
||||
<code>age</code> [<code>--encrypt</code>] <code>--passphrase</code> [<code>--armor</code>] [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br />
|
||||
<code>age</code> <code>--decrypt</code> [<code>-i</code> <var>PATH</var>]... [<code>-o</code> <var>OUTPUT</var>] [<var>INPUT</var>]<br /></p>
|
||||
|
||||
<h2 id="DESCRIPTION">DESCRIPTION</h2>
|
||||
|
||||
<p><code>age</code> encrypts or decrypts <var>INPUT</var> to <var>OUTPUT</var>. The <var>INPUT</var> argument is
|
||||
optional and defaults to standard input. Only a single <var>INPUT</var> file may be
|
||||
specified. If <code>-o</code> is not specified, <var>OUTPUT</var> defaults to standard output.</p>
|
||||
|
||||
<p>If <code>--passphrase</code> is specified, the file is encrypted with a passphrase
|
||||
requested interactively. Otherwise, it's encrypted to one or more
|
||||
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS</a> specified with <code>-r</code>/<code>--recipient</code> or
|
||||
<code>-R</code>/<code>--recipients-file</code>. Every recipient can decrypt the file.</p>
|
||||
|
||||
<p>In <code>--decrypt</code> mode, passphrase-encrypted files are detected automatically and
|
||||
the passphrase is requested interactively. Otherwise, one or more
|
||||
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> specified with <code>-i</code>/<code>--identity</code> are
|
||||
used to decrypt the file.</p>
|
||||
|
||||
<p><code>age</code> encrypted files are binary and not malleable, with around 200 bytes of
|
||||
overhead per recipient, plus 16 bytes every 64KiB of plaintext.</p>
|
||||
|
||||
<h2 id="OPTIONS">OPTIONS</h2>
|
||||
|
||||
<dl>
|
||||
<dt><code>-o</code>, <code>--output</code>=<var>OUTPUT</var></dt><dd><p> Write encrypted or decrypted file to <var>OUTPUT</var> instead of standard output.
|
||||
If <var>OUTPUT</var> already exists it will be overwritten.</p>
|
||||
|
||||
<p> If encrypting without <code>--armor</code>, <code>age</code> will refuse to output binary to a
|
||||
TTY. This can be forced by specifying <code>-</code> as <var>OUTPUT</var>.</p></dd>
|
||||
<dt><code>--version</code></dt><dd><p> Print the version and exit.</p></dd>
|
||||
</dl>
|
||||
|
||||
|
||||
<h3 id="Encryption-options">Encryption options</h3>
|
||||
|
||||
<dl>
|
||||
<dt><code>-e</code>, <code>--encrypt</code></dt><dd><p> Encrypt <var>INPUT</var> to <var>OUTPUT</var>. This is the default.</p></dd>
|
||||
<dt><code>-r</code>, <code>--recipient</code>=<var>RECIPIENT</var></dt><dd><p> Encrypt to the explicitly specified <var>RECIPIENT</var>. See the
|
||||
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS AND IDENTITIES</a> section for possible recipient formats.</p>
|
||||
|
||||
<p> This option can be repeated and combined with <code>-R</code>/<code>--recipients-file</code>,
|
||||
and the file can be decrypted by all provided recipients independently.</p></dd>
|
||||
<dt><code>-R</code>, <code>--recipients-file</code>=<var>PATH</var></dt><dd><p> Encrypt to the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS</a> listed in the
|
||||
file at <var>PATH</var>, one per line. Empty lines and lines starting with <code>#</code>
|
||||
are ignored as comments.</p>
|
||||
|
||||
<p> If <var>PATH</var> is <code>-</code>, the recipients are read from standard input. In
|
||||
this case, the <var>INPUT</var> argument must be specified.</p>
|
||||
|
||||
<p> This option can be repeated and combined with <code>-r</code>/<code>--recipient</code>,
|
||||
and the file can be decrypted by all provided recipients independently.</p></dd>
|
||||
<dt><code>-p</code>, <code>--passphrase</code></dt><dd><p> Encrypt with a passphrase, requested interactively from the terminal.
|
||||
<code>age</code> will offer to auto-generate a secure passphrase.</p>
|
||||
|
||||
<p> This option can't be used with <code>-r</code>/<code>--recipient</code> or
|
||||
<code>-R</code>/<code>--recipients-file</code>.</p></dd>
|
||||
<dt><code>-a</code>, <code>--armor</code></dt><dd><p> Encrypt to an ASCII-only "armored" encoding.</p>
|
||||
|
||||
<p> <code>age</code> armor is a strict version of PEM with type <code>AGE ENCRYPTED FILE</code>,
|
||||
canonical "strict" Base64, no headers, and no support for leading and
|
||||
trailing extra data.</p>
|
||||
|
||||
<p> Decryption transparently detects and decodes ASCII armoring.</p></dd>
|
||||
</dl>
|
||||
|
||||
|
||||
<h3 id="Decryption-options">Decryption options</h3>
|
||||
|
||||
<dl>
|
||||
<dt><code>-d</code>, <code>--decrypt</code></dt><dd><p> Decrypt <var>INPUT</var> to <var>OUTPUT</var>.</p>
|
||||
|
||||
<p> If <var>INPUT</var> is passphrase encrypted, it will be automatically detected
|
||||
and the passphrase will be requested interactively. Otherwise, the
|
||||
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> specified with <code>-i</code>/<code>--identity</code>
|
||||
are used.</p>
|
||||
|
||||
<p> ASCII armoring is transparently detected and decoded.</p></dd>
|
||||
<dt><code>-i</code>, <code>--identity</code>=<var>PATH</var></dt><dd><p> Decrypt using the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> at <var>PATH</var>.</p>
|
||||
|
||||
<p> <var>PATH</var> may be one of the following:</p>
|
||||
|
||||
<p> a. A file listing <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> one per line.
|
||||
Empty lines and lines starting with "<code>#</code>" are ignored as comments.</p>
|
||||
|
||||
<p> b. 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
|
||||
<a href="#SSH-keys" title="SSH keys" data-bare-link="true">SSH keys</a> section for more information, including supported key types.</p>
|
||||
|
||||
<p> c. "<code>-</code>", causing one of the options above to be read from standard input.
|
||||
In this case, the <var>INPUT</var> argument must be specified.</p>
|
||||
|
||||
<p> 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.</p>
|
||||
|
||||
<p> If <code>-e</code>/<code>--encrypt</code> is explicitly specified (to avoid confusion),
|
||||
<code>-i</code>/<code>--identity</code> may also be used to encrypt to the <code>RECIPIENTS</code>
|
||||
corresponding to the <code>IDENTITIES</code> listed at <var>PATH</var>. This allows using an
|
||||
identity file as a symmetric key, if desired.</p></dd>
|
||||
</dl>
|
||||
|
||||
|
||||
<h2 id="RECIPIENTS-AND-IDENTITIES">RECIPIENTS AND IDENTITIES</h2>
|
||||
|
||||
<p><code>RECIPIENTS</code> are public values, like a public key, that a file can be encrypted
|
||||
to. <code>IDENTITIES</code> are private values, like a private key, that allow decrypting
|
||||
a file encrypted to the corresponding <code>RECIPIENT</code>.</p>
|
||||
|
||||
<h3 id="Native-X25519-keys">Native X25519 keys</h3>
|
||||
|
||||
<p>Native <code>age</code> key pairs are generated with <a class="man-ref" href="age-keygen.1.html">age-keygen<span class="s">(1)</span></a>, and provide small
|
||||
encodings and strong encryption based on X25519. They are the recommended
|
||||
recipient type for most applications.</p>
|
||||
|
||||
<p>A <code>RECIPIENT</code> encoding begins with <code>age1</code> and looks like the following:</p>
|
||||
|
||||
<pre><code>age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh
|
||||
</code></pre>
|
||||
|
||||
<p>An <code>IDENTITY</code> encoding begins with <code>AGE-SECRET-KEY-1</code> and looks like the
|
||||
following:</p>
|
||||
|
||||
<pre><code>AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ
|
||||
</code></pre>
|
||||
|
||||
<p>An encrypted file can't be linked to the native recipient it's encrypted to
|
||||
without access to the corresponding identity.</p>
|
||||
|
||||
<h3 id="SSH-keys">SSH keys</h3>
|
||||
|
||||
<p>As a convenience feature, <code>age</code> also supports encrypting to RSA or Ed25519
|
||||
<span class="man-ref">ssh<span class="s">(1)</span></span> 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>
|
||||
|
||||
<p>A <code>RECIPIENT</code> encoding is an SSH public key in <code>authorized_keys</code> format
|
||||
(see the <code>AUTHORIZED_KEYS FILE FORMAT</code> section of <span class="man-ref">sshd<span class="s">(8)</span></span>), starting with
|
||||
<code>ssh-rsa</code> or <code>ssh-ed25519</code>, like the following:</p>
|
||||
|
||||
<pre><code>ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8=
|
||||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa
|
||||
</code></pre>
|
||||
|
||||
<p>The comment at the end of the line, if present, is ignored.</p>
|
||||
|
||||
<p>In recipient files passed to <code>-R</code>/<code>--recipients-file</code>, unsupported but valid
|
||||
SSH public keys are ignored with a warning, to facilitate using
|
||||
<code>authorized_keys</code> or GitHub <code>.keys</code> files. (See <a href="#EXAMPLES" title="EXAMPLES" data-bare-link="true">EXAMPLES</a>.)</p>
|
||||
|
||||
<p>An <code>IDENTITY</code> is an SSH private key <em>file</em> passed individually to
|
||||
<code>-i</code>/<code>--identity</code>. Note that keys held on hardware tokens such as YubiKeys
|
||||
or accessed via <span class="man-ref">ssh-agent<span class="s">(1)</span></span> are not supported.</p>
|
||||
|
||||
<p>An encrypted file <em>can</em> be linked to the SSH public key it was encrypted to.
|
||||
This is so that <code>age</code> can identify the correct SSH private key before
|
||||
requesting its password, if any.</p>
|
||||
|
||||
<h2 id="EXIT-STATUS">EXIT STATUS</h2>
|
||||
|
||||
<p><code>age</code> will exit 0 if and only if encryption or decryption are successful for the
|
||||
full length of the input.</p>
|
||||
|
||||
<p>If an error occurs during decryption, partial output might still be generated,
|
||||
but only if it was possible to securely authenticate it. No unauthenticathed
|
||||
output is ever released.</p>
|
||||
|
||||
<h2 id="BACKWARDS-COMPATIBILITY">BACKWARDS COMPATIBILITY</h2>
|
||||
|
||||
<p>Files encrypted with a stable version (not alpha, beta, or release candidate) of
|
||||
<code>age</code>, or with any v1.0.0 beta or release candidate, will decrypt with any later
|
||||
version of the tool.</p>
|
||||
|
||||
<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.</p>
|
||||
|
||||
<h2 id="EXAMPLES">EXAMPLES</h2>
|
||||
|
||||
<p>Generate a new identity, encrypt data, and decrypt:</p>
|
||||
|
||||
<pre><code>$ 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
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt <code>example.jpg</code> to multiple recipients and output to <code>example.jpg.age</code>:</p>
|
||||
|
||||
<pre><code>$ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p \
|
||||
-r age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg example.jpg
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt to a list of recipients:</p>
|
||||
|
||||
<pre><code>$ cat > recipients.txt
|
||||
# Alice
|
||||
age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
|
||||
# Bob
|
||||
age1lggyhqrw2nlhcxprm67z43rta597azn8gknawjehu9d9dl0jq3yqqvfafg
|
||||
|
||||
$ age -R recipients.txt example.jpg > example.jpg.age
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt and decrypt a file using a passphrase:</p>
|
||||
|
||||
<pre><code>$ 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:
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt and decrypt with an SSH public key:</p>
|
||||
|
||||
<pre><code>$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age
|
||||
|
||||
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg
|
||||
</code></pre>
|
||||
|
||||
<p>Encrypt to the SSH keys of a GitHub user:</p>
|
||||
|
||||
<pre><code>$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age
|
||||
</code></pre>
|
||||
|
||||
<h2 id="SEE-ALSO">SEE ALSO</h2>
|
||||
|
||||
<p><a class="man-ref" href="age-keygen.1.html">age-keygen<span class="s">(1)</span></a></p>
|
||||
|
||||
<h2 id="AUTHORS">AUTHORS</h2>
|
||||
|
||||
<p>Filippo Valsorda <a href="mailto:age@filippo.io" data-bare-link="true">age@filippo.io</a></p>
|
||||
|
||||
|
||||
<ol class='man-decor man-foot man foot'>
|
||||
<li class='tl'></li>
|
||||
<li class='tc'>October 2021</li>
|
||||
<li class='tr'>age(1)</li>
|
||||
</ol>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+239
@@ -0,0 +1,239 @@
|
||||
age(1) -- simple, modern, and secure file encryption
|
||||
====================================================
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
`age` [`--encrypt`] (`-r` <RECIPIENT> | `-R` <PATH>)... [`--armor`] [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
`age` [`--encrypt`] `--passphrase` [`--armor`] [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
`age` `--decrypt` [`-i` <PATH>]... [`-o` <OUTPUT>] [<INPUT>]<br>
|
||||
|
||||
## 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 `--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 `--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`=<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 `-R`/`--recipients-file`,
|
||||
and the file can be decrypted by all provided recipients independently.
|
||||
|
||||
* `-R`, `--recipients-file`=<PATH>:
|
||||
Encrypt to the [RECIPIENTS][RECIPIENTS AND IDENTITIES] 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 `-r`/`--recipient`,
|
||||
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 `-r`/`--recipient` or
|
||||
`-R`/`--recipients-file`.
|
||||
|
||||
* `-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.
|
||||
|
||||
### 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][RECIPIENTS AND IDENTITIES] specified with `-i`/`--identity`
|
||||
are used.
|
||||
|
||||
ASCII armoring is transparently detected and decoded.
|
||||
|
||||
* `-i`, `--identity`=<PATH>:
|
||||
Decrypt using the [IDENTITIES][RECIPIENTS AND IDENTITIES] at <PATH>.
|
||||
|
||||
<PATH> 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\. 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.
|
||||
|
||||
c\. "`-`", 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.
|
||||
|
||||
If `-e`/`--encrypt` is explicitly specified (to avoid confusion),
|
||||
`-i`/`--identity` may also be used to encrypt to the `RECIPIENTS`
|
||||
corresponding to the `IDENTITIES` listed at <PATH>. This allows using an
|
||||
identity file as a symmetric key, if desired.
|
||||
|
||||
## 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.
|
||||
|
||||
## 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 unauthenticathed
|
||||
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 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 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>
|
||||
@@ -0,0 +1,5 @@
|
||||
module filippo.io/age
|
||||
|
||||
go 1.13
|
||||
|
||||
require golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
@@ -0,0 +1,10 @@
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2017 Takatoshi Nakagawa
|
||||
// Copyright (c) 2019 Google LLC
|
||||
//
|
||||
// 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<<tobits - 1)
|
||||
for idx, value := range data {
|
||||
if value>>frombits != 0 {
|
||||
return nil, fmt.Errorf("invalid data range: data[%d]=%d (frombits=%d)", idx, value, frombits)
|
||||
}
|
||||
acc = acc<<frombits | uint32(value)
|
||||
bits += frombits
|
||||
for bits >= 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)+len(values)+7 > 90 {
|
||||
return "", fmt.Errorf("too long: hrp length=%d, data length=%d", len(hrp), len(values))
|
||||
}
|
||||
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 len(s) > 90 {
|
||||
return "", nil, fmt.Errorf("too long: len=%d", len(s))
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2013-2017 The btcsuite developers
|
||||
// Copyright (c) 2016-2017 The Lightning Network Developers
|
||||
// Copyright (c) 2019 Google LLC
|
||||
//
|
||||
// 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},
|
||||
{"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},
|
||||
// too long
|
||||
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", false},
|
||||
|
||||
// BIP 173 invalid vectors.
|
||||
{"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", false},
|
||||
{"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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
// Copyright 2019 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 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 recipientPrefix = []byte("->")
|
||||
|
||||
var footerPrefix = []byte("---")
|
||||
|
||||
func (r *Stanza) Marshal(w io.Writer) error {
|
||||
if _, err := w.Write(recipientPrefix); 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 ParseError string
|
||||
|
||||
func (e ParseError) Error() string {
|
||||
return "parsing age header: " + string(e)
|
||||
}
|
||||
|
||||
func errorf(format string, a ...interface{}) error {
|
||||
return ParseError(fmt.Sprintf(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: %v", err)
|
||||
}
|
||||
if line != intro {
|
||||
return nil, nil, errorf("unexpected intro: %q", line)
|
||||
}
|
||||
|
||||
var r *Stanza
|
||||
for {
|
||||
line, err := rr.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, nil, errorf("failed to read header: %v", err)
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(line, footerPrefix) {
|
||||
if r != nil {
|
||||
return nil, nil, errorf("malformed body line %q: reached footer without previous stanza being closed\nNote: this might be a file encrypted with an old beta version of rage. Use rage to decrypt it.", line)
|
||||
}
|
||||
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 {
|
||||
return nil, nil, errorf("malformed closing line %q: %v", line, err)
|
||||
}
|
||||
break
|
||||
|
||||
} else if bytes.HasPrefix(line, recipientPrefix) {
|
||||
if r != nil {
|
||||
return nil, nil, errorf("malformed body line %q: new stanza started without previous stanza being closed\nNote: this might be a file encrypted with an old beta version of rage. Use rage to decrypt it.", line)
|
||||
}
|
||||
r = &Stanza{}
|
||||
prefix, args := splitArgs(line)
|
||||
if prefix != string(recipientPrefix) || len(args) < 1 {
|
||||
return nil, nil, errorf("malformed recipient: %q", line)
|
||||
}
|
||||
for _, a := range args {
|
||||
if !isValidString(a) {
|
||||
return nil, nil, errorf("malformed recipient: %q", line)
|
||||
}
|
||||
}
|
||||
r.Type = args[0]
|
||||
r.Args = args[1:]
|
||||
h.Recipients = append(h.Recipients, r)
|
||||
|
||||
} else if r != nil {
|
||||
b, err := DecodeString(strings.TrimSuffix(string(line), "\n"))
|
||||
if err != nil {
|
||||
return nil, nil, errorf("malformed body line %q: %v", line, err)
|
||||
}
|
||||
if len(b) > BytesPerLine {
|
||||
return nil, nil, errorf("malformed body line %q: too long", line)
|
||||
}
|
||||
r.Body = append(r.Body, b...)
|
||||
if len(b) < BytesPerLine {
|
||||
// Only the last line of a body can be short.
|
||||
r = nil
|
||||
}
|
||||
|
||||
} else {
|
||||
return nil, nil, errorf("unexpected line: %q", line)
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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 format_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// Copyright 2019 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 stream implements a variant of the STREAM chunked encryption scheme.
|
||||
package stream
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"errors"
|
||||
"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 {
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2019 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 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 {
|
||||
b := length - n
|
||||
if b > stepSize {
|
||||
b = stepSize
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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 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
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2019 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 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
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright 2019 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 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright 2019 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 age
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"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<<logN, 8, 1, chacha20poly1305.KeySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
|
||||
}
|
||||
|
||||
wrappedKey, err := aeadEncrypt(k, fileKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.Body = wrappedKey
|
||||
|
||||
return []*Stanza{l}, nil
|
||||
}
|
||||
|
||||
// ScryptIdentity is a password-based identity.
|
||||
type ScryptIdentity struct {
|
||||
password []byte
|
||||
maxWorkFactor int
|
||||
}
|
||||
|
||||
var _ Identity = &ScryptIdentity{}
|
||||
|
||||
// NewScryptIdentity returns a new ScryptIdentity with the provided password.
|
||||
func NewScryptIdentity(password string) (*ScryptIdentity, error) {
|
||||
if len(password) == 0 {
|
||||
return nil, errors.New("passphrase can't be empty")
|
||||
}
|
||||
i := &ScryptIdentity{
|
||||
password: []byte(password),
|
||||
maxWorkFactor: 22, // 15s on a modern machine
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
// SetMaxWorkFactor sets the maximum accepted scrypt work factor to 2^logN.
|
||||
// It must be called before Unwrap.
|
||||
//
|
||||
// This caps the amount of work that Decrypt might have to do to process
|
||||
// received files. If SetMaxWorkFactor is not called, a fairly high default is
|
||||
// used, which might not be suitable for systems processing untrusted files.
|
||||
func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
|
||||
if logN > 30 || logN < 1 {
|
||||
panic("age: SetMaxWorkFactor called with illegal value")
|
||||
}
|
||||
i.maxWorkFactor = logN
|
||||
}
|
||||
|
||||
func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
|
||||
return multiUnwrap(i.unwrap, stanzas)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
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 {
|
||||
return nil, fmt.Errorf("invalid scrypt work factor: %v", logN)
|
||||
}
|
||||
|
||||
salt = append([]byte(scryptLabel), salt...)
|
||||
k, err := scrypt.Key(i.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
|
||||
}
|
||||
|
||||
// This AEAD is not robust, so an attacker could craft a message that
|
||||
// decrypts under two different keys (meaning two different passphrases) and
|
||||
// then use an error side-channel in an online decryption oracle to learn if
|
||||
// either key is correct. This is deemed acceptable because the use case (an
|
||||
// online decryption oracle) is not recommended, and the security loss is
|
||||
// only one bit. This also does not bypass any scrypt work, although that work
|
||||
// can be precomputed in an online oracle scenario.
|
||||
fileKey, err := aeadDecrypt(k, fileKeySize, block.Body)
|
||||
if err == errIncorrectCiphertextSize {
|
||||
return nil, errors.New("invalid scrypt recipient block: incorrect file key size")
|
||||
} else if err != nil {
|
||||
return nil, ErrIncorrectIdentity
|
||||
}
|
||||
return fileKey, nil
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"age": "homebrew/core"
|
||||
}
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 8hrlM+ZBG3Dd4fF2+a583zdTIWDk8/R41kCYZsvwTW4
|
||||
yO4PYdlMWDJ+CxgUNRqY5Z0T/m+g3FCh5jIxGLbCVXc
|
||||
--- I/imevZzy8120JSzmJnmn/KMk3p5A11V83Nk41m9NPE
|
||||
pĹĺ6$ˇRSů,ZŃʲsşMa€wč8 AzňĘ"rČř\…w4ˇ1;u鍯
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
# Test key for ExampleParseIdentities.
|
||||
AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU
|
||||
@@ -0,0 +1,210 @@
|
||||
// Copyright 2019 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 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)
|
||||
}
|
||||
Reference in new issue
Block a user