296 Commits

Author SHA1 Message Date
Filippo Valsorda
0293aca1d7 SIGSUM.md: add release playbook
Updates #617
2025-12-31 16:19:37 +01:00
GitHub Actions
e7601d8a67 doc: regenerate groff and html man pages 2025-12-31 11:05:09 +00:00
brettwhiteinc
acfa73142b doc: fix post-quantum example in age-keygen manpage (#675) 2025-12-31 12:03:49 +01:00
Filippo Valsorda
b8564adb6d .github/workflows: inject version into source release artifact
Updates #671
Updates golang/go#77020
2025-12-28 13:00:23 +01:00
Filippo Valsorda
e4c611f778 cmd,extra: restore the Version link-time variable
We don't need it in our builds, but it's useful for downstream packagers.

Fixes #671
Updates NixOS/nixpkgs#474666
Updates golang/go#77020
2025-12-28 12:49:37 +01:00
Filippo Valsorda
6a8065f2da SIGSUM.md: update policy for v1.3.0
Updates #617
2025-12-27 15:59:06 +01:00
Filippo Valsorda
50a600eef5 .github/workflows: improve release reproducibility
Remove the go-version-file test target, because it will just use the
toolchain line. It's a bit unfortunate we lose coverage of a go.mod
version that's older than oldstable, but that should not happen and it'd
not be a supported Go version anyway.
2025-12-27 15:59:06 +01:00
Filippo Valsorda
13aab81842 .github/workflows: build and release source tarball
Fixes #401
2025-12-27 15:59:06 +01:00
Filippo Valsorda
52338c20df .github/workflows: enable GitHub artifact attestation
Closes #621

Co-authored-by: Helio Machado <0x2b3bfa0+git@googlemail.com>
2025-12-27 10:28:25 +01:00
Filippo Valsorda
b70af41215 cmd/age: don't output binary plaintext to terminal
Closes #626
2025-12-26 23:16:25 +01:00
Filippo Valsorda
420273952a internal/stream: fix DecryptReaderAt concurrency 2025-12-26 22:18:54 +01:00
Filippo Valsorda
da2191789a age: add ExampleDecryptReaderAt with zip.NewReader 2025-12-26 21:42:24 +01:00
Filippo Valsorda
2ff5d341f6 age: add DecryptReaderAt 2025-12-26 20:30:27 +01:00
Filippo Valsorda
abe371e157 all: run "go fix" 2025-12-25 20:48:28 +01:00
Filippo Valsorda
ec92694aad age: add EncryptReader pull-based encryption API
Fixes #644
Fixes #654
Updates #638
2025-12-25 20:48:28 +01:00
Filippo Valsorda
92ac13f51c plugin: add NewTerminalUI
Closes #611
Closes #591

Co-authored-by: Nicolas Dumazet <nicdumz.commits@gmail.com>
2025-12-24 17:17:50 +01:00
GitHub Actions
a62324430d doc: regenerate groff and html man pages 2025-12-24 11:36:31 +00:00
Filippo Valsorda
41167479ce doc: include warning in age-plugin-batchpass(1) 2025-12-24 12:35:15 +01:00
GitHub Actions
4444afb267 doc: regenerate groff and html man pages 2025-12-24 11:28:31 +00:00
Filippo Valsorda
db8ed63595 cmd/age-plugin-batchpass: add detailed warning 2025-12-24 12:10:43 +01:00
Filippo Valsorda
f1cc23a3f6 cmd: move compatibility plugins to extra/ directory 2025-12-24 02:40:29 +01:00
Filippo Valsorda
50a81fd5a9 cmd/age-plugin-batchpass: plugin for non-interactive passphrase encryption
Fixes #603
Closes #641
Closes #520
Updates #256
Updates #182
Updates #257
Updates #275
Updates #346
Updates #386
Updates #445
Updates #590
Updates #572
2025-12-24 02:27:54 +01:00
Filippo Valsorda
44a4fcc27b age,cmd/age: improve error messages for mixed up identities/recipients
Not sure why ParseRecipients was hiding errors, when ParseIdentities
doesn't.

For #643
2025-12-23 23:26:16 +01:00
Filippo Valsorda
2e0f1efe4d age,cmd/age: detect invalid UTF-8 in identity and recipient files
For #663
2025-12-23 23:06:00 +01:00
Filippo Valsorda
a7586b7557 README: remove pkgx installation instructions
See https://github.com/FiloSottile/age/pull/659#issuecomment-3688047267.
2025-12-23 22:46:29 +01:00
Filippo Valsorda
de7813b5f6 README: add age-inspect documentation 2025-12-23 22:46:13 +01:00
RishikesavanRamesh
2f5cf5438c README: add Guix System to installation table (#615) 2025-12-23 22:44:19 +01:00
GitHub Actions
830d84e777 doc: regenerate groff and html man pages 2025-12-23 21:31:56 +00:00
Filippo Valsorda
b4cdeef465 cmd/age: accept leading whitespace before armored data
It was already accepted by the API, but the CLI did not handle it while
peeking to detect armored input.
2025-12-23 22:23:41 +01:00
Simone Ragusa
6aae5b48ea cmd/age: fix terminal escape sequences on Windows
If possible, we enable virtual terminal processing, which is necessary
for using terminal escape sequences on instances of the Windows Console.
When enabling virtual terminal processing fails, we completely avoid
using escape sequences to prevent weird characters to be printed to the
console.

Fixes #474
Closes #475

Co-authored-by: Filippo Valsorda <hi@filippo.io>
2025-12-23 22:12:19 +01:00
Filippo Valsorda
d36e4ce2c7 cmd/age-inspect: new command
Fixes #56
Closes #501
2025-12-23 21:39:39 +01:00
Filippo Valsorda
ca8a69b1b6 age: improve error on empty files
Fixes #416
2025-12-23 15:22:55 +01:00
Filippo Valsorda
38dd222823 age: return better errors from Decrypt for scrypt identities 2025-12-23 14:28:05 +01:00
Filippo Valsorda
c17d0b362c age: add NoIdentityMatchError.StanzaTypes field
For https://github.com/FiloSottile/age/issues/56#issuecomment-1962622903.
2025-12-23 13:13:03 +01:00
Filippo Valsorda
a36341de15 cmd/age: improve error message when -i is missing 2025-12-23 13:11:49 +01:00
Filippo Valsorda
f3b008d1b8 plugin: fix returning in-protocol errors from plugins
Fixes Foxboron/age-plugin-tpm#31
2025-12-23 13:11:49 +01:00
Filippo Valsorda
9795b63263 cmd/age,plugin: add plugin.NotFoundError and CLI hint
Fixes #486
2025-12-23 13:11:49 +01:00
Filippo Valsorda
bfae75d93d age,plugin: wrap more errors 2025-12-23 12:34:03 +01:00
Filippo Valsorda
0d5b598cd0 cmd/age: warn about duplicate command-line arguments
Fixes #284
2025-12-22 22:08:21 +01:00
GitHub Actions
50acf91174 doc: regenerate groff and html man pages 2025-12-22 18:42:32 +00:00
Filippo Valsorda
ba67de8a4e tag/internal/age-plugin-tagtest: add plugin for testing tag recipients 2025-12-22 19:41:12 +01:00
Filippo Valsorda
7fa810b20a tag: add Recipient.Tag and Bytes methods, and update tag scheme 2025-12-22 19:41:12 +01:00
Filippo Valsorda
1b18d6b279 age: update c2sp.org/CCTV/age testkit to include hybrid identities 2025-12-22 19:41:12 +01:00
Filippo Valsorda
ad7bb569eb cmd/age: fix testscript setup races 2025-12-22 19:41:12 +01:00
Filippo Valsorda
ed44098807 all: upgrade dependencies
Closes #610
2025-12-22 19:41:12 +01:00
Filippo Valsorda
83bab2ae6a plugin: avoid using deprecated math/rand.Read 2025-12-22 19:41:12 +01:00
Filippo Valsorda
96b6476140 armor: reject empty lines in armored data
Caught by the new CCTV test vectors!
2025-12-22 19:41:12 +01:00
Filippo Valsorda
d7409cdc74 .github/workflows: update and harden GitHub Actions workflows 2025-12-22 19:41:12 +01:00
Filippo Valsorda
de158f906b cmd/age-plugin-tag,cmd/age-plugin-tagpq: new backward compatibility plugins 2025-12-22 19:41:12 +01:00
Filippo Valsorda
78947d862d age: use native identities first in Decrypt 2025-12-22 19:41:12 +01:00
Filippo Valsorda
c6fcb5300c age,cmd/age,cmd/age-keygen: add post-quantum hybrid keys 2025-12-22 19:41:12 +01:00
Filippo Valsorda
6ece9e45ee tag: use filippo.io/hpke 2025-12-22 19:41:12 +01:00
Filippo Valsorda
e2d30695f2 cmd/age,tag: implement age1tagpq1.../p256mlkem768tag recipients
Test vectors generated from hpkewg/hpke-pq@19adaeb (hpkewg/hpke-pq#28 +
hpkewg/hpke-pq#32) and cfrg/draft-irtf-cfrg-concrete-hybrid-kems@1bbca40
(cfrg/draft-irtf-cfrg-concrete-hybrid-kems#16), plus the following diff:

diff --git a/reference-implementation/src/bin/generate.rs b/reference-implementation/src/bin/generate.rs
index 25e32e5..bc8f209 100644
--- a/reference-implementation/src/bin/generate.rs
+++ b/reference-implementation/src/bin/generate.rs
@@ -26,6 +26,15 @@ fn generate_test_vectors() -> TestVectors {
     // 5. QSF-P384-MLKEM1024 + SHAKE256 + AES-256-GCM
     vectors.push(TestVector:🆕:<QsfP384MlKem1024, Shake256, Aes256Gcm>());
 
+    vectors = TestVectors::new();
+
+    // age1pq - xwing
+    vectors.push(TestVector:🆕:<QsfX25519MlKem768, HkdfSha256, ChaChaPoly>());
+    // age1tag - p256tag
+    vectors.push(TestVector:🆕:<DhkemP256HkdfSha256, HkdfSha256, ChaChaPoly>());
+    // age1tagpq - p256mlkem768tag
+    vectors.push(TestVector:🆕:<QsfP256MlKem768, HkdfSha256, ChaChaPoly>());
+
     vectors
 }
 
diff --git a/reference-implementation/src/test_vectors.rs b/reference-implementation/src/test_vectors.rs
index 24335aa..4134fb5 100644
--- a/reference-implementation/src/test_vectors.rs
+++ b/reference-implementation/src/test_vectors.rs
@@ -369,6 +369,10 @@ impl TestVector {
             (0x0051, 0x0011, 0x0002) => self.v::<QsfP384MlKem1024, Shake256, Aes256Gcm>(),
             (0x0051, 0x0011, 0xffff) => self.v::<QsfP384MlKem1024, Shake256, ExportOnly>(),
 
+            // age pq combinations
+            (0x647a, 0x0001, 0x0003) => self.v::<QsfX25519MlKem768, HkdfSha256, ChaChaPoly>(),
+            (0x0050, 0x0001, 0x0003) => self.v::<QsfP256MlKem768, HkdfSha256, ChaChaPoly>(),
+
             _ => Err(format!(
                 "Unsupported algorithm combination: KEM={:#x}, KDF={:#x}, AEAD={:#x}",
                 self.kem_id, self.kdf_id, self.aead_id
2025-12-22 19:41:12 +01:00
Filippo Valsorda
e9295dd867 cmd/age,tag: implement age1tag1.../p256tag recipients
See C2SP/C2SP#156
2025-12-22 19:41:12 +01:00
Filippo Valsorda
acab3e5c9f plugin: add framework to implement plugins (#580)
Fixes #485
2025-12-07 20:10:01 +01:00
Filippo Valsorda
a8de3de174 age: add ExtractHeader, DecryptHeader, and NewInjectedFileKeyIdentity 2025-12-07 20:01:09 +01:00
Filippo Valsorda
ae74b61b59 cmd/age,internal/stream: improve error messages 2025-12-07 20:01:09 +01:00
Thibault
f882f40aa3 cmd/age: echo terminal input for public plugin prompts 2025-12-07 18:59:13 +01:00
Filippo Valsorda
6d2c4e236c README: move Sigsum instructions to separate file 2025-12-07 16:24:46 +01:00
Filippo Valsorda
75063d25b1 LICENSE: move copyright holders from AUTHORS file 2025-12-07 16:24:46 +01:00
Filippo Valsorda
20eba7e285 cmd/age,cmd/age-keygen: remove unnecessary injected Version
golang/go#50603 started stamping the VCS tag version.
2025-12-07 16:24:46 +01:00
Filippo Valsorda
15153e699f README: update Twitter links to GitHub 2025-07-14 18:29:48 +02:00
Helio Machado
fce45118ee .github/workflows: fix actions/setup-go cache (#622) 2025-06-15 20:42:25 +02:00
Richard Burte
c3657aca5c README: add winget installation instructions (#627) 2025-06-15 20:40:38 +02:00
Filippo Valsorda
0447d8d089 age: add links to docs 2025-05-10 14:59:20 +02:00
Filippo Valsorda
3d91014ea0 README: link to typage 2025-02-01 18:15:20 +01:00
Filippo Valsorda
482cf6fc9b plugin: restrict characters in plugin names
Thanks to ⬡-49016 for reporting this issue.

Fixes GHSA-32gq-x56h-299c
2024-12-18 16:01:18 +01:00
Alexander Yastrebov
cda3988cc7 all: fix staticcheck warnings (#589)
Co-authored-by: Filippo Valsorda <github@bip.filippo.io>
2024-12-18 15:55:57 +01:00
Filippo Valsorda
176e245b3c README: rotate Sigsum keys
Switched to a pair of keys, one kept offline and one on a Tillitis key.

The following script provides key continuity from the previous key.

---

cat << EOF > msg.txt
These are the new age Sigsum keys as of 2024-06-28.
The previous one won't be used anymore
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1WpnEswJLPzvXJDiswowy48U+G+G1kmgwUE2eaRHZG
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAz2WM5CyPLqiNjk7CLl4roDXwKhQ0QExXLebukZEZFS
EOF

cat << EOF > msg.txt.proof
version=1
log=c9e525b98f412ede185ff2ac5abf70920a2e63a6ae31c88b1138b85de328706b
leaf=e2f0 61d17df1ab563aedf70a662d5344b2d163c7a35aaeaa1ecc6c1631c02d46883e c4564b0b0b70ebb4236e26f971cb467c59200575c01c4d07a5d8e298a6d2319c59bbc162363ffb4f690b581851621bd3de311e9559a1a0432522e2b567647e03

size=24226
root_hash=03b0fc19c812e51e764474d161b944db4ea20dfc13815b738fa4f433f56954c0
signature=b95530661d716886926a49ad4e08435c7980348836eefe4706cc611a6af9dc6ddb160189d4aa177c97f807fa0240cb27b9d5f075084cf3a4a2514eade7d40f00
cosignature=1c997261f16e6e81d13f420900a2542a4b6a049c2d996324ee5d82a90ca3360c 1719587282 02d3f9b34d62f1b735e6c13f2ad7766ed2066a167dba25d526a2acbbf628588c58a888f9abab50ff151f8440076e8bf39564e4f893a32be432e97cd18b936c0e
cosignature=70b861a010f25030de6ff6a5267e0b951e70c04b20ba4a3ce41e7fba7b9b7dfc 1719587283 14b8654586763b6798dc7e7b46565236618f2824135076ba71e18908bf893d73be95c143b8c8cbe48de9ea6267ff5f23f8d870ded7d22ed6e54811393d174402

leaf_index=24157
node_hash=c5782abde765bc2c460d25f0bc1f8f89d787e5b8a141e862eeb550631cb61ca2
node_hash=283f014735692dfda045ca9c32352d5d0f99207862c35214dfaa69810eec6e2f
node_hash=24987bf3ec7c3c8932783faf0fe01ff49c7793c3d25925da180165c3292b786d
node_hash=ba14f9dd00a506474251599083e83a4bf7327491f6ebd5cca1ac8a6863456eb5
node_hash=55aceb2864d26c9c04c85a294e92dfdfa13bb45eeda0286d21f19411763237f5
node_hash=69cc9e7f7ad56da996abeb315dc2ffb3e1e1f02e8097facfae75f06f9e8f9bc9
node_hash=1408251ffc0d485551b058813d7b1227bb91a179b9f0851a6e69e9bb99623eec
node_hash=ca99c77b91c6aa9f55fba8d6d9c80058dcc0444a2e9df3f7a616d7e2d274fb58
node_hash=b95d29a868d7e4413dab001a414cfe4cab65e113bc831cf41ab9003250ede3c9
node_hash=5ced132a7cc05272b797b7ad2e71208366d8023d09bad755514cd86e37a849fd
node_hash=95803b981443b4ff080b5a14927e24e5efc8186b327320b633005213ca3aeff4
node_hash=3aad6b63102dede3851d575b01bb60a5832d9f31eb3405b73aa7d629a1acaaa8
node_hash=c929af9f6731f63a493668627f58810d892dc51f8aa1c9a4de1573cd3e51e62f
EOF

sigsum-verify -k age-sigsum-key.pub -p sigsum-trust-policy.txt msg.txt.proof < msg.txt
2024-08-21 12:36:58 +02:00
Filippo Valsorda
faefdc3c81 README: document Sigsum proofs 2024-06-19 10:44:28 +02:00
Filippo Valsorda
bbe6ce5eeb .github/workflows: update artifacts Actions
Co-authored-by: Rene Leonhardt <65483435+reneleonhardt@users.noreply.github.com>
2024-06-16 16:01:06 +02:00
Filippo Valsorda
1e1badabf7 .github/workflows: go-version stable, not latest 2024-06-16 14:59:53 +02:00
Filippo Valsorda
2293a9afef .github/workflows: use latest Go for bootstrap 2024-06-16 14:51:17 +02:00
Filippo Valsorda
01fe9cd84a README: add pkgx installation instructions
Closes #529
2024-06-16 14:49:24 +02:00
Filippo Valsorda
bd0511b415 cmd/age: detect output/input file reuse when possible
Fixes #491
2024-06-16 14:40:13 +02:00
Filippo Valsorda
febaaded87 cmd/age: create file for empty decryptions
Fixes #555
Updates #159
Updates #57
2024-06-16 13:55:32 +02:00
GitHub Actions
0a40718a93 doc: regenerate groff and html man pages 2024-06-16 10:03:57 +00:00
Filippo Valsorda
7ed486868a .github/workflows: apparently setup-go has no defaults 2024-06-16 06:03:13 -04:00
Filippo Valsorda
2a761fcb8c .github/workflows: update GitHub Actions 2024-06-16 06:03:13 -04:00
Filippo Valsorda
98e7afcbac all: upgrade dependencies 2024-06-16 06:03:13 -04:00
Filippo Valsorda
5ef63b6153 .github/workflows: install bootstrap Go 2024-06-16 06:03:13 -04:00
Filippo Valsorda
bc21ece498 .github/workflows: disable environment
It's very spammy, just move the secret to a repository secret.
2024-06-16 06:03:13 -04:00
Filippo Valsorda
69c21b83fb README: fix scoop command
Closes #564
2024-06-16 05:06:58 -04:00
Filippo Valsorda
35cf02b1d0 .github: link maintenance policy from CONTRIBUTING.md 2024-06-16 05:03:03 -04:00
Jakub Wilk
29b68c20fc README: fix typo (#534) 2024-01-10 06:40:17 -05:00
Filippo Valsorda
101cc86763 README: Debian 12 installation instructions 2023-09-20 08:41:00 -04:00
Filippo Valsorda
6ad4560f4a .github/workflows: drop FreeBSD tests
This is unfortunate, but without a live platform to test on,
I can't investigate issues, and CI is now failing with just

   ?   	filippo.io/age/cmd/age-keygen	[no test files]
  Killed

which really could be anything.
2023-08-07 18:44:57 -04:00
Filippo Valsorda
93055632ad cmd/age: fix FreeBSD tests 2023-08-06 19:39:31 +02:00
Filippo Valsorda
294b0aa1e3 plugin: skip execution tests on Windows for now 2023-08-06 19:03:27 +02:00
Filippo Valsorda
f1f96c25e0 plugin: build tag EncodeX25519Recipient which uses crypto/ecdh 2023-08-06 18:36:38 +02:00
Filippo Valsorda
9fd564d543 .github/workflows: update and fix CI 2023-08-06 18:29:16 +02:00
Filippo Valsorda
c89f0b932e age,plugin: add RecipientWithLabels 2023-08-05 21:34:47 +02:00
Filippo Valsorda
dd733c5c0f cmd/age: grease the client-controlled plugin phases 2023-08-05 21:34:14 +02:00
Filippo Valsorda
004b544d83 plugin: add EncodeX25519Recipient 2023-08-05 21:34:14 +02:00
Filippo Valsorda
02181d83e9 plugin: add identity and recipient encoding 2023-08-05 21:34:14 +02:00
Filippo Valsorda
6976c5fca5 plugin: expose package 2023-08-05 21:34:14 +02:00
Filippo Valsorda
980763a16e age: make TestVectorsRoundTrip a little stricter 2023-07-23 00:54:40 +02:00
Filippo Valsorda
4740a92ef9 age: use testkit vectors to test armor, header, and STREAM round-trips
Before

	filippo.io/age/armor	coverage: 72.3% of statements in filippo.io/age/...
	filippo.io/age/internal/format	coverage: 86.8% of statements in filippo.io/age/...
	filippo.io/age/internal/stream	coverage: 83.9% of statements in filippo.io/age/...

After

	filippo.io/age/armor	coverage: 88.0% of statements in filippo.io/age/...
	filippo.io/age/internal/format	coverage: 87.6% of statements in filippo.io/age/...
	filippo.io/age/internal/stream	coverage: 86.0% of statements in filippo.io/age/...
2023-07-23 00:18:41 +02:00
Stepan
6c36e167c8 README: update release download link (#512) 2023-06-30 09:05:44 -04:00
Filippo Valsorda
9f0a2d25ac README: add link to awesome age list 2023-04-22 16:40:45 -04:00
Helio Machado
b6537b1865 .github/workflows: trigger interop. tests using gh (#481) 2023-04-22 06:01:29 -04:00
GitHub Actions
486b6dac96 doc: regenerate groff and html man pages 2023-04-22 09:53:44 +00:00
zhsj
877ca247e3 .github/workflows: update ronn to 0.9 (#483)
This fixes apostrophes in generated manpage.
Apostrophes should be entered as `'` ; using `\'` produces an acute accent.

ronn in Ubuntu is from https://github.com/apjanke/ronn-ng
2023-04-22 05:53:03 -04:00
andros21
502b180b17 README: dark/light mode logo (#500)
https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#specifying-the-theme-an-image-is-shown-to
2023-04-22 05:46:50 -04:00
Filippo Valsorda
8e3f74c283 cmd/age: deflake TestScript and update testscript 2023-01-02 13:34:35 +01:00
Berk D. Demir
edf7388f77 age: depend on c2sp.org/CCTV/age for TestVectors
Simplifies importing test data from CCTV without needing to invoke
"go mod download" from TestVectors. Makes life easier for package
builders with no networking, like Nixpkgs.
2022-12-30 18:24:08 -05:00
Filippo Valsorda
5471e05672 Revert "all: temporarily disable testscript tests"
This reverts commit 90a446549a.
2022-12-29 21:53:31 +01:00
Filippo Valsorda
c6dcfa1efc all: temporarily disable testscript tests
They require a replace directive that breaks "go install". Will revert
this after tagging a new latest release.
2022-12-26 15:36:58 -05:00
Filippo Valsorda
a1fabee4c8 all: upgrade dependencies 2022-12-26 15:36:58 -05:00
Filippo Valsorda
7354aa0d08 internal/bech32: remove data length limit
Fixes #453
2022-12-23 12:28:06 +01:00
Filippo Valsorda
bf8d2a3911 age: move testkit to CCTV 2022-10-28 20:11:09 +02:00
Helio Machado
5d5c9c48d8 .github/workflows: parallelize build process (#456) 2022-10-10 07:41:06 -04:00
Helio Machado
e05ce267a6 .github/workflows: use gh to upload release artifacts (#455)
* .github/workflows: use gh to upload release artifacts

https://cli.github.com/manual/gh_release_upload

* .github/workflows: remove unnecessary braces

When you use expressions in an if conditional, you may omit the expression syntax ${{ }} because GitHub automatically evaluates the if conditional as an expression.

https://docs.github.com/en/actions/learn-github-actions/expressions
2022-10-09 22:38:36 -04:00
GitHub Actions
000e931101 doc: regenerate groff and html man pages 2022-09-28 09:47:53 +00:00
a1346054
ebf48f1bbc doc: trim trailing whitespace 2022-09-28 05:46:34 -04:00
a1346054
3f2209cab9 doc: fix typos 2022-09-28 05:46:34 -04:00
Filippo Valsorda
bbac0a501c README: add a couple links to the heading 2022-09-26 08:16:06 -04:00
Mehrnoosh Beniss
02a01c6a6f README: switch order of installation and usage (#448) 2022-09-26 08:10:01 -04:00
Filippo Valsorda
8328d19d3e .github/workflows: update Go and FreeBSD in test 2022-09-13 13:18:49 +02:00
Simone Ragusa
1f84a45175 .github/workflows: change Go LICENSE URL from golang.org to go.dev (#449)
Co-authored-by: Filippo Valsorda <github@bip.filippo.io>
2022-09-11 15:54:32 -04:00
Filippo Valsorda
084c974f53 all: use better implementation of testscript timeouts 2022-07-28 14:21:43 +02:00
Filippo Valsorda
36ae5671cf cmd/age: buffer armored ciphertext before prompting
A partial solution, still missing bracketed paste support.

Updates #364
2022-07-12 22:30:19 +02:00
Filippo Valsorda
8a02f4801f cmd/age: improve terminal-related testscripts 2022-07-12 22:26:51 +02:00
Filippo Valsorda
e41463e117 cmd/age: fix autogenerated password log line
The newline was dropped in 0ab5c73.
2022-07-12 22:26:51 +02:00
Filippo Valsorda
f66877cfa5 README: update installation instructions to provide v1.0.0+
Updates #371
2022-07-07 12:11:02 -04:00
Filippo Valsorda
891be91d42 armor: add FuzzMalleability test 2022-07-03 12:48:48 +02:00
Filippo Valsorda
a5d38ae6ce armor: correct whitespace and trailing garbage handling
Ignore whitespace before and after file, reject trailing garbage and
whitespace within the file.
2022-07-03 12:48:48 +02:00
Filippo Valsorda
799c2bf8e8 tests: add armor tests 2022-07-03 12:48:48 +02:00
Filippo Valsorda
e84d74239e age: wrap decryption errors through and add armor.Error 2022-07-03 12:48:48 +02:00
Filippo Valsorda
95ba0188d1 age: keep age lowercase in all docs 2022-07-03 12:48:48 +02:00
Filippo Valsorda
0ab5c738fb cmd/age: ensure TUI output goes all to the terminal 2022-07-03 12:48:48 +02:00
Filippo Valsorda
de7c1fb565 agessh: support PKCS#8-encoded Ed25519 private keys
OpenSSH never generated them (unencrypted, and golang.org/x/crypto/ssh
doesn't support encrypted PKCS#8 for now, so the encrypted_keys.go
change is technically superfluous) but there are other systems that
produce them (for example, 1Password). Unfortunately, ParseRawPrivateKey
returns a value type for PKCS#8 and a pointer type for the OpenSSH
format (golang/go#51974), so we need to handle both.

Fixes #429
2022-07-03 12:48:48 +02:00
Filippo Valsorda
92fb4d508c agessh: properly detect public key mismatch for encrypted keys 2022-07-03 12:48:48 +02:00
Filippo Valsorda
331b242a9c cmd/age: add scrypt testscript 2022-07-03 12:48:48 +02:00
Filippo Valsorda
c50f1ae2e1 cmd/age: replace tests with testscript 2022-06-21 12:41:38 +01:00
Filippo Valsorda
8023f06ce2 all: build tag tests that require Go 1.18 2022-06-20 22:33:31 +01:00
Filippo Valsorda
4f7bb44386 internal/format: add malleability fuzz test 2022-06-20 01:18:48 +01:00
Filippo Valsorda
3f56ac13fb internal/stream: reject trailing data (no EOF) after end of stream 2022-06-19 23:11:14 +02:00
Filippo Valsorda
2e090545df age: reject leading zeroes and sign in scrypt work factor 2022-06-19 17:52:30 +02:00
Filippo Valsorda
2088adf268 tests: add expected no match and minor additions 2022-06-19 00:11:23 +02:00
Filippo Valsorda
eaa4e03cfe tests: finish parsing and X25519 tests, distinguish HMAC errors
If the implementation re-encodes the header before checking the HMAC,
that would mask malleability issues: the HMAC check would fail because
the tests HMAC'd the original header, but an attacker could also produce
the right HMAC. Instead of duplicating every parsing tests (with the
original and re-encoded HMAC), we make the test framework distinguish
HMAC errors, which ensures bad encodings are recognized as such and not
bypassable HMAC errors.
2022-06-18 13:47:00 +02:00
Filippo Valsorda
f8a121dd87 tests: add more tests for hmac and X25519 encodings 2022-06-16 15:55:29 +02:00
Filippo Valsorda
bb4493a7cd tests: add X25519 low order point tests 2022-06-16 11:47:27 +02:00
Filippo Valsorda
92713afd1e tests: add scrypt tests and move Go files from testdata 2022-06-15 20:38:59 +02:00
Filippo Valsorda
787044bdb6 testdata: convert last chunk tests to testkit 2022-06-15 19:38:18 +02:00
Filippo Valsorda
a787511e01 testdata: add a few more testkit vectors 2022-06-15 18:54:09 +02:00
Filippo Valsorda
e49b1f9afe internal/testkit: new test framework 2022-06-15 18:53:07 +02:00
GitHub Actions
78bedc2873 doc: regenerate groff and html man pages 2022-06-11 15:09:45 +00:00
GitHub Actions
f4112110f1 doc: regenerate groff and html man pages 2022-05-24 13:59:08 +00:00
Filippo Valsorda
acb1170bbc doc: clarify -e -i docs
Fixes #339
2022-05-24 15:57:42 +02:00
Filippo Valsorda
56f6acca37 cmd/age: reject passphrase-encrypted files if -i is used
Passphrase-encrypted files make age(1) block, which would be unexpected
when decrypting files in a script using -i.
2022-05-24 15:57:42 +02:00
Filippo Valsorda
5824a13b57 cmd/age,internal/plugin: print a message if waiting on a plugin for too long 2022-05-24 15:57:42 +02:00
Filippo Valsorda
c0e80ef2c9 cmd/age: improve confirm dialog
Don't require enter after the selection number, print errors as
warnings, and retry if an unexpected selection is made.
2022-05-24 15:56:22 +02:00
Filippo Valsorda
eeb9a079da cmd/age: add -j option for data-less plugins
See str4d/rage#237 and str4d/rage#236
2022-05-24 15:56:22 +02:00
Filippo Valsorda
fbe51d501c doc: document plugins in age(1) 2022-05-24 15:56:22 +02:00
Filippo Valsorda
87a982b72e internal/plugin: refactor plugin handling code 2022-05-24 15:56:22 +02:00
Filippo Valsorda
5a0da177e9 internal/plugin,cmd/age: implement confirm protocol verb 2022-05-24 15:56:22 +02:00
Filippo Valsorda
349ed5ed3f cmd/age: clean up the terminal UI 2022-05-24 15:56:22 +02:00
Filippo Valsorda
384d0393e0 internal/plugin,cmd/age: add support for encrypting to plugin identities 2022-05-24 15:56:22 +02:00
Filippo Valsorda
7bad9c2ad8 internal/plugin: update to latest plugin spec
The main change is that phase 2 of the wrapping state machine is
interactive to accommodate symmetric plugins.
2022-05-24 15:56:22 +02:00
Filippo Valsorda
01b56b117c internal/plugin: complete experimental plugin support 2022-05-24 15:56:22 +02:00
Filippo Valsorda
f6a5b94705 internal/plugin,cmd/age: implement preliminary plugin client support 2022-05-24 15:56:22 +02:00
GitHub Actions
cff70cffe2 doc: regenerate groff and html man pages 2022-05-24 13:51:06 +00:00
Filippo Valsorda
73416d1ac5 .github/workflows: fix generation of groff man pages 2022-05-24 15:49:39 +02:00
Filippo Valsorda
30d8e65e03 internal/stream: disallow empty final chunks
A non-empty payload of length a multiple of the chunk size can be
encrypted in two ways: with the last chunk full, or with an extra empty
last chunk. This is mostly an oversight in the original spec.

Both age and rage generate full last chunks, so we should be still in
time to pick one of the two, and avoid the underspecification. It's not
the one I would have picked originally, maybe, because disallowing full
last chunks would have avoided the trial decryption, but oh well.
2022-05-24 15:47:13 +02:00
Filippo Valsorda
765400f0c1 internal/format: factor out a StanzaReader for use in the plugin protocol 2022-05-22 11:54:38 +02:00
hakerdefo
cb539f931c README: add openSUSE Tumbleweed and sort installation table entries (#421)
Adding the installation instructions for openSUSE Tumbleweed and sorting the table entries in alphabetical order.
2022-05-20 07:14:08 -04:00
Filippo Valsorda
d6c77e0838 README: update badges 2022-05-09 06:03:01 -04:00
Simon Brand
ac31f5c935 cmd/age: fallback to stdin if /dev/tty cannot be opened (#414) 2022-05-04 06:44:48 -04:00
Filippo Valsorda
e8771b6d8a README: specify minimum Alpine repo version 2022-04-28 18:40:52 -04:00
Tionis
d8fa2fb0de README: add Alpine package to installation section (#412) 2022-04-28 18:39:25 -04:00
Filippo Valsorda
3e1aa11e9d .github: add CONTRIBUTING.md 2022-04-27 18:19:34 -04:00
Richard Ulmer
f7fcbef542 internal/stream: remove unused variable from testRoundTrip (#396) 2022-04-27 07:31:48 -04:00
Filippo Valsorda
299ec50c32 cmd/age: improve error message for out-of-order flags
Fixes #160
Closes #345

Co-authored-by: puenka <puenka@users.noreply.github.com>
2022-04-26 21:15:26 +02:00
Filippo Valsorda
2e20ca5fa7 .github/workflows: fix FreeBSD tests (#410) 2022-04-26 14:29:40 -04:00
Helio Machado
d717942b93 internal/format: improve error message for old header format (#398) 2022-04-26 13:30:35 -04:00
Brendan Fattig
3b4fb67296 README: change Go download link from golang.org to go.dev (#403) 2022-04-08 16:53:12 -04:00
Dimitris Apostolou
f01e37b4d7 doc: fix typo in age(1) (#395) 2022-02-24 04:37:53 -05:00
Filippo Valsorda
3411802309 cmd/age: offer a hint when the file was corrupted by PowerShell
I would still like to find a way to offer a warning while doing the
encryption, rather than at decryption time, but better than nothing.

Updates #290
2022-01-07 12:26:02 +01:00
Arnau Díaz
4169274d04 cmd/age-keygen: actually exit on errorf (#383)
Co-authored-by: Arnau Diaz <arnau.diaz@adevinta.com>
2022-01-04 13:39:38 -05:00
Filippo Valsorda
e4ae4cf884 age: fix typo in package docs 2022-01-01 08:02:58 -05:00
Kot
ab3707c085 README: add Scoop package (#378) 2021-12-18 12:25:55 -05:00
Eng Zer Jun
7665b87dc2 all: move from io/ioutil to io and os packages (#353)
The io/ioutil package has been deprecated as of Go 1.16, see
https://golang.org/doc/go1.16#ioutil. This commit replaces the existing
io/ioutil functions with their new definitions in io and os packages.

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2021-12-18 12:06:22 -05:00
Michael Cook
08f52cc125 .github/workflows: skip signing if key pass isn't available (#377)
With the exception of GITHUB_TOKEN, secrets are not passed to the
runner when a workflow is triggered from a forked repository.

See https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#pull-request-events-for-forked-repositories
2021-12-18 10:54:10 -05:00
Filippo Valsorda
a21c212358 logo: mention the logo designers 2021-11-24 12:28:59 +01:00
Filippo Valsorda
dfa2052cb7 all: move copyright owners to AUTHORS file 2021-11-24 11:29:15 +01:00
Filippo Valsorda
0c41827056 logo: add logo resources 2021-11-24 11:24:39 +01:00
Filippo Valsorda
3432b88db9 README: recommend "go install" for building from source 2021-11-12 14:38:51 -05:00
Filippo Valsorda
e2fc716c8b .github/workflows: use vmactions/freebsd-vm for FreeBSD (#344)
Replaces CirrusCI dependency.
2021-10-16 14:16:01 -04:00
Filippo Valsorda
3bd9ab8e9b .github/workflows: sign Windows binaries
Fixes #326
Closes #328

Co-authored-by: Joshua Small <technion@lolware.net>
2021-10-04 11:59:48 +02:00
Ross Light
4e1d7631e5 README: add instructions for Chocolatey install (#338)
Uses the repository at https://github.com/vaporwave9/age-chocolatey
2021-10-03 14:48:54 -04:00
GitHub Actions
1c95ceae09 doc: regenerate groff and html man pages 2021-09-30 13:01:16 +00:00
puenka
be70dba53e doc: fix typo in age(1) (#336) 2021-09-30 08:59:41 -04:00
GitHub Actions
50c2f22ba7 doc: regenerate groff and html man pages 2021-09-25 13:57:46 +00:00
y-yagi
878682b574 doc: fix typo in age(1) (#333) 2021-09-25 09:56:28 -04:00
Filippo Valsorda
3d7a7ff0b8 .github/workflows: add -trimpath to build and refactor a bit 2021-09-10 12:09:00 +02:00
Filippo Valsorda
776e1780a9 .github/workflows: merge test and gotip workflows 2021-09-10 11:38:48 +02:00
Filippo Valsorda
427edf35cc .github/workflows: downscope permissions where possible 2021-09-10 11:38:48 +02:00
GitHub Actions
8d88096476 doc: regenerate groff and html man pages 2021-09-08 10:35:24 +00:00
Filippo Valsorda
5cad72c62e doc: clarify backwards compatibility section 2021-09-08 12:33:45 +02:00
Filippo Valsorda
fd1b393f9c README: fix man page links 2021-09-07 19:24:40 +02:00
Caleb Maclennan
79211ba255 HomebrewFormula: drop formula, now in Homebrew’s repository (#318)
* HomebrewFormula: drop formula, now in Homebrew’s repository
* doc: update brew install, tap not required
2021-09-07 09:22:09 -04:00
Filippo Valsorda
552aa0a07d README: resize and center the logo 2021-09-06 12:45:08 -04:00
Filippo Valsorda
47d8133c52 README: add new logo 🏛
With a background of the color of the default desktop dark theme,
because there is no good way to use a transparent image.

https://github.community/t/support-theme-context-for-images-in-light-vs-dark-mode/147981/69
2021-09-06 12:29:21 -04:00
Filippo Valsorda
36b0a4f611 all: update dependencies and module version
This should bring in a faster golang.org/x/crypto/curve25519.
2021-09-05 01:55:46 +02:00
Filippo Valsorda
fda89073cd README: document new dl.filippo.io links 2021-09-05 01:24:14 +02:00
GitHub Actions
7756fbfe45 doc: regenerate groff and html man pages 2021-09-04 16:08:15 +00:00
Filippo Valsorda
57f6b8acae age,doc: document backwards compatibility policy
Fixes #216
2021-09-04 18:06:38 +02:00
Aaron Bieber
e08055f4e5 all: update x/sys to v0.0.0-20210630005230-0f9fa26af87c (#299)
This allows age to be built on OpenBSD/mips64!
2021-07-14 18:33:55 +02:00
NORlogik
7cb6b84758 README: mention official Void Linux package (#294) 2021-07-09 21:25:18 +02:00
Filippo Valsorda
4ea591b25f HomebrewFormula: update age to v1.0.0-rc.3 2021-06-15 14:27:09 +02:00
Filippo Valsorda
9d4b2ae7ac age: move the scrypt lone recipient check out of Decrypt
The important one is the decryption side one, because when a user types
a password they expect it to both decrypt and authenticate the file.
Moved that one out of Decrypt and into ScryptIdentity, now that
Identities get all the stanzas. special_cases--

This also opens the door to other Identity implementations that do allow
multiple scrypt recipients, if someone really wants that. The CLI will
never allow it, but an explicit choice by an API consumer feels like
something we shouldn't interfere with.

Moreover, this also allows alternative Identity implementations that use
different recipient types to replicate the behavior if they have the
same authentication semantics.

The encryption side one is only a courtesy, to stop API users from
making files that won't decrypt. Unfortunately, that one needs to stay
as a special case in Encrypt, as the Recipient can't see around itself.
However, changed it to a type assertion, so custom recipients can
generate multiple scrypt recipient stanzas, if they really want.
2021-06-15 14:00:10 +02:00
GitHub Actions
1ddf01df2c doc: regenerate groff and html man pages 2021-06-14 13:24:26 +02:00
Filippo Valsorda
f4e28fe809 .github/workflows: fix non-idempotent ronn email mangling 2021-06-14 13:24:26 +02:00
Filippo Valsorda
0703f86521 cmd/age,cmd/age-keygen: normalize errors, warnings, and hints 2021-06-14 13:24:26 +02:00
Filippo Valsorda
fb97277f8d cmd/age: add support for encrypted identity files
Updates #252
Closes #132
2021-06-14 13:24:26 +02:00
Filippo Valsorda
fa5b575ceb cmd/age: use CONIN$/CONOUT$ on Windows for password prompts
Fixes #128
Closes #274

Co-authored-by: codesoap <codesoap@mailbox.org>
2021-06-02 11:04:02 +02:00
GitHub Actions
cde103daae doc: regenerate groff and html man pages 2021-06-01 10:26:04 +00:00
Andreas Wachowski
b403e96be8 doc: fix typo in age-keygen(1) (#273) 2021-06-01 06:25:16 -04:00
GitHub Actions
329a7ece8f doc: regenerate groff and html man pages 2021-05-26 14:47:36 +00:00
Filippo Valsorda
3cd503dce9 doc: SEC 1 encoding is for ECDSA, which we don't support 2021-05-26 16:44:38 +02:00
Filippo Valsorda
cd28d56599 .github/workflows: don't run ronn on new tags 2021-05-26 15:28:21 +02:00
Filippo Valsorda
a94f3c3dc9 HomebrewFormula: update age to v1.0.0-rc.2 2021-05-26 13:52:31 +02:00
Filippo Valsorda
6596145a2c armor: don't leave an empty line before the footer
Closes #264
Fixes #263
2021-05-26 13:35:30 +02:00
Filippo Valsorda
7a262e1ffd agessh: use allowed RSA key size in tests 2021-05-25 21:15:04 +02:00
Filippo Valsorda
0b895a9340 HomebrewFormula: drop man pages that are not yet in the release 2021-05-25 20:54:31 +02:00
Filippo Valsorda
c9aca162ef README: add pkg.go.dev and man page badges 2021-05-25 20:45:46 +02:00
GitHub Actions
c7c3012437 doc: regenerate groff and html man pages 2021-05-25 20:36:23 +02:00
Filippo Valsorda
e58a8859b9 doc: add age(1) and age-keygen(1) man pages
Closes #131
2021-05-25 20:36:23 +02:00
Filippo Valsorda
fb293ef526 agessh: reject small ssh-rsa keys
Fixes #266
2021-05-24 10:58:50 +02:00
Caleb Maclennan
3d5b49a348 README.md: drop system upgrade args from Arch Linux install (#270) 2021-05-24 04:41:27 -04:00
mjkalyan
cd4b2476bc README.md: add Gentoo installation method (#269) 2021-05-24 04:40:36 -04:00
Ryan Castellucci
759a88d3e8 cmd/age-keygen: don't warn about world-readable output for public keys (#268)
Fixes #267
2021-05-18 20:35:29 -04:00
Filippo Valsorda
85763d390a age: remove recipient limit
Fixes #139
2021-05-02 18:44:21 -04:00
Filippo Valsorda
fff82986fa README: clarify pronunciation reference
Updates #103
2021-04-23 11:38:05 -04:00
Filippo Valsorda
67ce088a41 README: add pronunciation
Fixes #103
2021-04-23 03:27:04 -04:00
Filippo Valsorda
3ad0bbed99 README: dry up installation instructions into a table 2021-04-23 02:34:51 -04:00
Simone Ragusa
7a55783693 README: add NixOS/Nix installation instructions (#197) 2021-04-23 02:13:24 -04:00
Herby Gillot
d271e916cf README: add instructions for installing via MacPorts (#179) 2021-04-23 02:12:51 -04:00
Robert-André Mauchin
50254ff522 README: add Fedora installation instructions (#183) 2021-04-23 02:10:02 -04:00
Filippo Valsorda
7a335c9d5d cmd/age: allow reading both passphrase and input from a terminal
Fixes #196
Closes #258
2021-04-23 02:06:50 -04:00
Filippo Valsorda
ff1b4ffb08 cmd/age,cmd/age-keygen: check Close() error on output files
Fixes #81
2021-04-23 00:11:12 -04:00
Filippo Valsorda
e63c22e327 Reapply "agessh: use filippo.io/edwards25519 for Ed25519 to Curve25519 conversion"
This reverts commit 629b0dbbc9.
2021-04-22 22:27:35 -04:00
Filippo Valsorda
a6a173e24f .github/workflows: add freebsd/amd64 and darwin/arm64 builds
Fixes #189
2021-04-22 22:22:57 -04:00
Filippo Valsorda
b4e0d7718f README: remove mailing list mention
It wasn't very active and was replaced by GitHub Discussions.
2021-04-19 00:21:27 -04:00
Filippo Valsorda
9e65644c3f .github: update "New issue" page (#211) 2021-04-18 18:45:50 -04:00
Christian Rebischke
290a2fd5ec README: mention official Arch Linux package (#204)
Signed-off-by: Christian Rebischke <chris@shibumi.dev>
2021-04-05 11:19:17 -04:00
Richard Ulmer
bad2c0d2e0 cmd/age: use golang.org/x/term instead of deprecated package (#205) 2021-04-05 09:22:51 -04:00
Ben Banfield-Zanin
dabc470bfe HomebrewFormula: update age.rb to 1.0.0-rc.1 (#199) 2021-03-21 07:12:27 -04:00
Filippo Valsorda
53f0ebda67 cmd/age: improve a couple error messages 2021-03-10 05:38:47 -05:00
Filippo Valsorda
f3fdc33c9e cmd/age-keygen: add -y mode to convert identity file to recipients
Copied -y from ssh-keygen. Copied the INPUT as only optional argument
from cmd/age.

Fixes #122
Closes #146
2021-03-10 05:38:47 -05:00
Filippo Valsorda
a81357c237 all: update golang.org/x/crypto
Picked the latest version that is packaged in Debian Bullseye.

Fixes #184
2021-03-10 05:38:47 -05:00
Filippo Valsorda
69e2222921 internal/format: drop go-fuzz test case
We were not running it in CI, so it had rotted or was going to rot.
We'll replace it with a native fuzz test once that's ready.
2021-03-10 05:38:47 -05:00
Filippo Valsorda
732f3e8a94 cmd/age: add -e and support encrypting with -i
This will come in handy for symmetric plugins, but make it require an
explicit -e so that missing a -d can't cause a mistaken encryption.
2021-03-10 05:38:47 -05:00
Filippo Valsorda
801a7e8b33 cmd/age: overwrite output file if existing
Not really the safest UX, but age is a UNIX tool, and this is what all
UNIX tools do, so adopt the principle of least surprise.
2021-03-10 05:38:47 -05:00
Filippo Valsorda
629b0dbbc9 Revert "agessh: use filippo.io/edwards25519 for Ed25519 to Curve25519 conversion"
The extra dependency makes it harder to package age. Temporarily drop it
to facilitate getting v1.0.0 into distributions.

This reverts commit 53ccaf8b71.
2021-03-10 05:38:47 -05:00
Filippo Valsorda
710644eef8 Revert "cmd/age: automatically load default SSH key paths"
It's not clear the convenience for SSH keys is worth having any
implicitly configured identity at all. Will revisit after v1.0.0.

This reverts commit 225044b061.
2021-03-10 05:38:47 -05:00
Filippo Valsorda
6c8d072dd8 .github/workflows: add Go tip tests and bump tests to Go 1.16 2021-03-09 18:51:05 -05:00
Filippo Valsorda
225044b061 cmd/age: automatically load default SSH key paths 2021-02-08 19:55:28 +01:00
Filippo Valsorda
6da7d26b4d all: add .gitattributes to protect .age files from autocrlf
It looks like a short message encrypted to a long RSA key is enough
ASCII that it gets autocrlf'd on Windows.
2021-02-08 19:55:28 +01:00
Filippo Valsorda
19e87b75b7 cmd/age: expand test vectors suite 2021-02-08 19:55:28 +01:00
Filippo Valsorda
5d96bfa9a9 age: make Identity and Recipient work on multiple stanzas
This is a breaking change, but like the other changes to these
interfaces it should not matter to consumers of the API that don't
implement custom Recipients or Identities, which is all of them so far,
as far as I can tell.

It became clear working on plugins that we might want Recipient to
return multiple recipient stanzas, for example if the plugin recipient
is an alias or a group. The Identity side is less important, but it
might help avoid round-trips and it makes sense to keep things
symmetric.
2021-02-08 19:55:28 +01:00
Filippo Valsorda
f04064a41b age: add NoIdentityMatchError
Closes #147
2021-02-08 19:55:28 +01:00
Filippo Valsorda
0fa220e4d7 age: remove IdentityMatcher
It was completely useless: the same checks in Match could be implemented
in Unwrap, returning an early ErrIncorrectIdentity.

Not sure why I added it. It felt clever at the time.
2021-02-08 19:55:28 +01:00
Filippo Valsorda
6546df3bac age: remove Type method from Recipient and Identity interfaces
The Type() method was a mistake, as proven by the fact that I can remove
it without losing any functionality. It gives special meaning to the
"0th argument" of recipient stanzas, when actually it should be left up
to Recipient implementations to make their own stanzas recognizable to
their Identity counterparts.

More importantly, there are totally reasonable Identity (and probably
Recipient) implementations that don't know their own stanza type in
advance. For example, a proxy plugin.

Concretely, it was only used to special-case "scrypt" recipients, and to
skip invoking Unwrap. The former can be done based on the returned
recipient stanza, and the latter is best avoided entirely: the Identity
should start by looking at the stanza and returning ErrIncorrectIdentity
if it's of the wrong type.

This is a breaking API change. However, we are still in beta, and none
of the public downstreams look like they would be affected, as they only
use Recipient and Identity implementations from this package, they only
use them with the interfaces defined in this package, and they don't
directly use the Type() method.
2021-02-08 19:55:28 +01:00
Filippo Valsorda
15df6e2cf7 internal/format: require the last line of stanzas to be short
We are going to reuse the stanza format for IPC in the plugin protocol,
but in that context we need stanzas to be self-closing. Currently they
almost are, but if the body is 0 modulo 48, there is no way to know if
the stanza is over after the last line.

Now, all stanzas have to end with a short line, even if empty.

No ciphertexts generated by age in the past are affected, but 3% of the
ciphertexts generated by rage will now stop working. They are still
supported by rage going forward. If it turns out to be a common issue,
we can add an exception.
2021-02-08 19:55:28 +01:00
James Brown
50b61862d6 HomebrewFormula: update to v1.0.0-beta6 (#180) 2021-02-08 19:55:28 +01:00
Filippo Valsorda
c418992942 cmd/age: touch up help text 2021-01-15 14:26:33 +01:00
codesoap
902a3d4e6b cmd/age: decide to buffer output based on stdin source
Buffering only when the armorFlag is set disregards use cases where data
from a tty stdin is decrypted or where binary data goes to a tty stdout.

Buffering is only necessary if stdin is a tty and stdout is a tty.

Co-authored-by: Filippo Valsorda <hi@filippo.io>
2021-01-07 16:51:43 -05:00
codesoap
4a5a042583 cmd/age: decouple output buffer and output denial decision
Co-authored-by: Filippo Valsorda <hi@filippo.io>
2021-01-07 16:51:43 -05:00
Tom Payne
6fc795057f cmd/age-keygen: add --output option as an alias for -o (#177) 2021-01-07 19:20:55 +01:00
Filippo Valsorda
f7011ee36a .github/workflows: include LICENSE in binary builds 2021-01-06 13:49:56 +01:00
Richard Ulmer
a8573a5c8d cmd/age: improve help text (#175) 2021-01-04 20:05:33 +01:00
Matthias Schneider
a02075a0cc internal/stream: fix readChunk documentation (#163)
r.c was renamed to r.src.
2021-01-03 15:26:48 +01:00
Filippo Valsorda
4147b86ac8 cmd/age: support "-R -" if stdin is not in use 2021-01-03 09:10:21 -05:00
Daan Sprenkels
dc8716d8fc cmd/age: support "-i -" if stdin is not in use
Fixes #143
Closes #145

Co-authored-by: Filippo Valsorda <hi@filippo.io>
2021-01-03 09:10:21 -05:00
Filippo Valsorda
3f2deb5a3b cmd/age: be less clever in the lazyOpener implementation
I am a fan of closures and DoerFunc interface adapters, but the boring
thing is more readable. Sigh.
2021-01-03 09:10:21 -05:00
Filippo Valsorda
97b6569a66 cmd/age: lazily open output file at first write
This avoids leaving behind an empty file when an error occurs before we
write the header (for example, because the passphrase is invalid). Do a
best-effort check before taking user input for whether the file exists
so we don't waste user effort. An error might still happen after user
input if other kind of open errors happen (for example, a permission
issue, or disk full).

Fixes #159
Fixes #57
Closes #169
2021-01-03 09:10:21 -05:00
Filippo Valsorda
02ee8b969a internal/format: buffer newlineWriter writes
Most writes in the cmd/age Writer stack are chunk-sized, so
approximately 64KiB. However, the newlineWriter, which splits lines at
64 columns, was doing a Write on the underlying Writer for each line,
making chunks effectively 48 bytes (before base64). There is no
buffering underneath it, so it was resulting in a lot of write syscalls.

Add a reusable bytes.Buffer to buffer the output of each
(*newlineWriter).Write call, and Write it all at once on the
destination.

This makes --armor just 50% slower than plain, instead of 10x.

Fixes #167
2021-01-03 09:10:21 -05:00
Filippo Valsorda
cb4d1de4b7 cmd/age: show usage if no arguments and flags are specified
Fixes #74
2021-01-03 09:10:21 -05:00
Filippo Valsorda
e665eeafb0 cmd/age-keygen: make world-readable warning less obscure
Fixes #149
Fixes #75
2021-01-03 09:10:21 -05:00
Filippo Valsorda
4dee0155ee cmd/age,cmd/age-keygen: improve help text
Closes #168
2021-01-03 09:10:21 -05:00
Filippo Valsorda
0522803919 cmd/age,cmd/age-keygen: add -version flag
Fixes #157
Fixes #101
Closes #97
2021-01-03 09:10:21 -05:00
Filippo Valsorda
f8507c1cac age,cmd/age: add ParseRecipients and -R for recipient files
Fixes #84
Fixes #66
Closes #165
Closes #158
Closes #115
Closes #64
Closes #43
Closes #20
2021-01-03 09:10:21 -05:00
Filippo Valsorda
7ab2008136 .github/workflows: extend testing matrix 2021-01-03 00:28:03 +01:00
Luca Corbo
f5a47fcc9a age: fix typo in ParseX25519Identity docs (#134) 2021-01-03 00:13:41 +01:00
Filippo Valsorda
6f51e96429 .github/workflows: add build.yml
Fixes #164
Fixes #148
Fixes #133
Closes #25
2021-01-02 23:41:45 +01:00
Filippo Valsorda
53ccaf8b71 agessh: use filippo.io/edwards25519 for Ed25519 to Curve25519 conversion 2020-12-04 18:45:52 +01:00
Filippo Valsorda
6593c56e33 .github/workflows: switch from Travis CI to GitHub Actions
So long, and thanks for all the builds!
2020-11-21 01:50:09 +01:00
Filippo Valsorda
31500bfa2f age: clean up examples of hardcoded data 2020-09-20 12:57:16 +02:00
Filippo Valsorda
22e598d458 age: replace ParseX25519Identities with ParseIdentities
The latter returns a []Identity that can be used with Decrypt directly.
2020-09-20 12:54:15 +02:00
Filippo Valsorda
65f171a239 age: add ParseX25519Identities and key management docs 2020-09-20 12:17:15 +02:00
Filippo Valsorda
2194f6962c age: mitigate multi-key attacks on ChaCha20Poly1305
It's possible to craft ChaCha20Poly1305 ciphertexts that decrypt under
multiple keys. (I know, it's wild.)

The impact is different for different recipients, but in general only
applies to Chosen Ciphertext Attacks against online decryption oracles:

* With the scrypt recipient, it lets the attacker make a recipient
  stanza that decrypts with multiple passwords, speeding up a bruteforce
  in terms of oracle queries (but not scrypt work, which can be
  precomputed) to logN by binary search.

  Limiting the ciphertext size limits the keys to two, which makes this
  acceptable: it's a loss of only one bit of security in a scenario
  (online decryption oracles) that is not recommended.

* With the X25519 recipient, it lets the attacker search for accepted
  public keys without using multiple recipient stanzas in the message.
  That lets the attacker bypass the 20 recipients limit (which was not
  actually intended to defend against deanonymization attacks).

  This is not really in the threat model for age: we make no attempt to
  provide anonymity in an online CCA scenario.

  Anyway, limiting the keys to two by enforcing short ciphertexts
  mitigates the attack: it only lets the attacker test 40 keys per
  message instead of 20.

* With the ssh-ed25519 recipient, the attack should be irrelevant, since
  the recipient stanza includes a 32-bit hash of the public key, making
  it decidedly not anonymous.

  Also to avoid breaking the abstraction in the agessh package, we don't
  mitigate the attack for this recipient, but we document the lack of
  anonymity.

This was reported by Paul Grubbs in the context of the upcoming paper
"Partitioning Oracle Attacks", USENIX Security 2021 (to appear), by
Julia Len, Paul Grubbs, and Thomas Ristenpart.
2020-09-19 18:52:59 +02:00
Shimmy Xu
07c72f3b69 internal/bech32: fix vet error by making rune conversion explicit (#141)
Fixes #138
2020-09-07 16:07:10 +02:00
Filippo Valsorda
21a7203f6a README: add link to Go documentation 2020-07-20 19:34:08 -04:00
Filippo Valsorda
0c650f815d age: fix filippo.io/age/agessh name in package docs
Thanks to @Mohammed90 for pointing this out.
2020-06-27 23:47:17 -04:00
107 changed files with 12350 additions and 1049 deletions

View File

@@ -1,9 +0,0 @@
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 -v ./...

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
*.age binary
testdata/testkit/* binary

34
.github/CONTRIBUTING.md vendored Normal file
View File

@@ -0,0 +1,34 @@
## Issues
I want to hear about any issues you encounter while using age.
Particularly appreciated are well researched, complete [issues](https://github.com/FiloSottile/age/issues/new/choose) with lots of context, **focusing on the intended outcome and/or use case**. Issues don't have to be just about bugs: if something was hard to figure out or unexpected, please file a **[UX report](https://github.com/FiloSottile/age/discussions/new?category=UX-reports)**! ✨
Not all issue reports might lead to a change, so please don't be offended if yours doesn't, but they are precious datapoints to understand how age could work better in aggregate.
## Pull requests
age is a little unusual in how it is maintained. I like to keep the code style consistent and complexity to a minimum, and going through many iterations of code review is a significant toil on both contributors and maintainers. age is also small enough that such a time investment is unlikely to pay off over ongoing contributions.
Therefore, **be prepared for your change to get reimplemented rather than merged**, and please don't be offended if that happens. PRs are still appreciated as a way to clarify the intended behavior, but are not at all required: prefer focusing on providing detailed context in an issue report instead.
To learn more, please see my [maintenance policy](https://github.com/FiloSottile/FiloSottile/blob/main/maintenance.md).
<!-- ## Feature requests
age is small, simple, and config-free by design. A lot of effort is put into resisting scope creep and enabling use cases by integrating and interoperating well with other projects rather than by adding features.
In particular, I'm unlikely to merge into the main repo anything I don't use myself, as I would not be the best person to maintain it. However, I'm always happy to discuss, learn about, and link to any age-related project! -->
## Other ways to contribute
age itself is not community maintained, but its ecosystem very much is, and that's where a lot of the strength of age is! Here are some ideas for ways to contribute to age and its ecosystem, besides contributing to this repository.
* **Write an article about how to use age for a certain community or use case.** The number one reason people don't use age is because they haven't heard about it and existing tutorials present more complex alternatives.
* Integrate age into existing projects that might use it, for example replacing legacy alternatives.
* Build and maintain an [age plugin](https://c2sp.org/age-plugin) for a KMS or platform.
* Watch the [discussions](https://github.com/FiloSottile/age/discussions) and help other users.
* Provide bindings in a language or framework that doesn't support age well.
* Package age for an ecosystem that doesn't have packages yet.
If you build or write something related to age, [let me know](https://github.com/FiloSottile/age/discussions/new?category=general)! 💖

View File

@@ -1,6 +1,6 @@
---
name: Bug report
about: Create a report about a bug in this implementation.
name: Bug report 🐞
about: Did you encounter a bug in this implementation?
title: ''
labels: ''
assignees: ''

10
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,10 @@
contact_links:
- name: UX report ✨
url: https://github.com/FiloSottile/age/discussions/new?category=UX-reports
about: Was age hard to use? It's not you, it's us. We want to hear about it.
- name: Spec feedback 📃
url: https://github.com/FiloSottile/age/discussions/new?category=Spec-feedback
about: Have a comment about the age spec as it's implemented by this and other tools?
- name: Questions, feature requests, and more 💬
url: https://github.com/FiloSottile/age/discussions
about: Do you need support? Did you make something with age? Do you have an idea? Tell us about it!

View File

@@ -1,15 +0,0 @@
---
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 -->

View File

@@ -1,21 +0,0 @@
---
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>
```

30
.github/workflows/LICENSE.suffix.txt vendored Normal file
View File

@@ -0,0 +1,30 @@
---
Copyright 2009 The Go Authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of 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.

123
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,123 @@
name: Build and upload binaries
on:
release:
types: [published]
push:
pull_request:
permissions:
contents: read
jobs:
build:
name: Build binaries
runs-on: ubuntu-latest
strategy:
matrix:
include:
- {GOOS: linux, GOARCH: amd64}
- {GOOS: linux, GOARCH: arm, GOARM: 6}
- {GOOS: linux, GOARCH: arm64}
- {GOOS: darwin, GOARCH: arm64}
- {GOOS: windows, GOARCH: amd64}
- {GOOS: freebsd, GOARCH: amd64}
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: Install Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: false
- name: Build binary
run: |
VERSION="$(git describe --tags)"
DIR="$(mktemp -d)"
mkdir "$DIR/age"
go build -o "$DIR/age" -trimpath ./cmd/...
cp LICENSE "$DIR/age/LICENSE"
cat .github/workflows/LICENSE.suffix.txt >> "$DIR/age/LICENSE"
if [ "$GOOS" == "windows" ]; then
sudo apt-get update && sudo apt-get install -y osslsigncode
if [ -n "${{ secrets.SIGN_PASS }}" ]; then
for exe in "$DIR"/age/*.exe; do
/usr/bin/osslsigncode sign -t "http://timestamp.comodoca.com" \
-certs .github/workflows/certs/uitacllc.crt \
-key .github/workflows/certs/uitacllc.key \
-pass "${{ secrets.SIGN_PASS }}" \
-n age -in "$exe" -out "$exe.signed"
mv "$exe.signed" "$exe"
done
fi
( cd "$DIR"; zip age.zip -r age )
mv "$DIR/age.zip" "age-$VERSION-$GOOS-$GOARCH.zip"
else
tar -cvzf "age-$VERSION-$GOOS-$GOARCH.tar.gz" -C "$DIR" age
fi
env:
CGO_ENABLED: 0
GOOS: ${{ matrix.GOOS }}
GOARCH: ${{ matrix.GOARCH }}
GOARM: ${{ matrix.GOARM }}
- name: Upload workflow artifacts
uses: actions/upload-artifact@v4
with:
name: age-artifacts-${{ matrix.GOOS }}-${{ matrix.GOARCH }}
path: age-*
source:
name: Package source code
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
persist-credentials: false
- name: Install Go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
cache: false
- name: Create source tarball
run: |
VERSION="$(git describe --tags)"
DIR="$(mktemp -d)"
mkdir "$DIR/age"
git archive --format=tar.gz HEAD | tar -xz -C "$DIR/age"
( cd "$DIR/age"; go mod vendor )
for cmd in "$DIR"/age/{cmd,extra}/*; do
echo "package main" >> "$cmd/version.go"
echo "" >> "$cmd/version.go"
echo "func init() { Version = \"$VERSION\" }" >> "$cmd/version.go"
done
tar -cvzf "age-$VERSION-source.tar.gz" -C "$DIR" age
- name: Upload workflow artifacts
uses: actions/upload-artifact@v4
with:
name: age-artifacts-source
path: age-*-source.tar.gz
upload:
name: Upload and attest release artifacts
if: github.event_name == 'release'
needs: [build, source]
permissions:
contents: write
attestations: write
id-token: write
runs-on: ubuntu-latest
steps:
- name: Download workflow artifacts
uses: actions/download-artifact@v4
with:
pattern: age-artifacts-*
merge-multiple: true
- name: Generate artifacts attestation
uses: actions/attest-build-provenance@v3
with:
subject-path: age-*
- name: Upload release artifacts
run: gh release upload "$GITHUB_REF_NAME" age-*
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}

14
.github/workflows/certs/README vendored Normal file
View File

@@ -0,0 +1,14 @@
In this folder there are
uitacllc.crt
PKCS#7 encoded certificate chain for a code signing certificate issued
to Up in the Air Consulting LLC valid until Sep 26 23:59:59 2024 GMT.
https://crt.sh/?id=5339775059
uitacllc.key
PEM encrypted private key for the leaf certificate above.
Its passphrase is long and randomly generated, so the awful legacy key
derivation doesn't really matter, and it makes osslsigncode happy.

BIN
.github/workflows/certs/uitacllc.crt vendored Normal file

Binary file not shown.

42
.github/workflows/certs/uitacllc.key vendored Normal file
View File

@@ -0,0 +1,42 @@
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-256-CBC,B93C1A166F3677D68FB9CB3E8A184729
UriYsaq3tLyvycDDB2YeQ+9L1P5VCPcfVkYR1ocleF8WxNDUPdz3RqbryAZZdXVO
0bcvAHTXkdI4Oiw5mN0S8fGsNq9zn+pyResx3lXtgN3oCDCe2SQn28uEEKxPzud5
0NRXYoBP+pLDjiuQ/6Lp7DovnAO/uxaPFvYRMiknNVOhwyHGWZyuUe01S9J9im7y
vgc1wkyQzmABIhARynEXHp3KnM9aF8X1/ck839lQRBFrvRFNm5rqiON26spr1Hu5
znrbVGROYk0XNdH5VHDk7V9k+v2WLL/b4nxlMymZpDzr9pXzX8olpLnQrarsMbHe
ysfXNTtQi5Dq6KXURW8VA4DmxAzTRNUxe2aA4JnAEyFU5LDLetTN9F9M7BUkHbXH
RpSbZqDjPwg7U98vuSwxjIkncHSiYYi3FmSoupLvV+eIP6qRSgONdzGlP5NTn4Lh
N1lYMPHPldH6UjLHrldkYN16TQlrqNHZExN91XvsZVjpyAgErY18xwi3CTEco45D
fRqsiWXtoas4LkafhSY0vfl5aFhY9YPUpS6uFdgWBvgcQeYb8meX5Nr4dNXVk5Wa
yRlYlW/X0TWC0T9qaBOPN/z7OWO5aL4jYRcKQQ+aR8gFcHGGCpRAKD369OneXfOQ
MD9UHoPG4WTBg/NU9OSskcywfuSOkwAGfBVNXrnEj6tYFjsjYK2nC2gm+opUCfm0
a1FeDb5nQSOgOJKUCO6Aj+0NvDvVLUOsTk1lfzSugIkmUOdV+rXHnrZC+90q8KfN
S2JlzwSZNg0e+VxZpnD7k7axHkbHrbebtrLvzKVnrh3s0OFAXN0isMw7yhhWtzUe
mPoQTZusLDOAJe/QPuNlDUgr4uoVZtoXrPzoZZkw2VFLwYy2g/EYvlK9BdVVTnRm
9Hq9IBDrZw+SV/7roaeVOXbzrQoxEoXcL7eo6iWvV5Q7Ll5C4ovelHKy3IAzcpYP
6LKfxAO2sIKTALrHbtBNG+O4RTtxOva1hyg27V4v2k53CF/GhoBRPSpbbupwppXc
lJJ9RtMTRfhCv/ObhdsJED+YUqFifTJfcnQ1iGN8dnBuGrjXxVCN0wgmv46Pdhn0
tUfGlkFquOOWamaVaIvp6JCVUDa1ezMzleILoYvrxvOuP+dGVrwTwVCXpx4JuUgp
d72/w+EnqlZnwsAzdrErJFXnHux981ZoojmG94km1B6gPPwMB8JRcD67lfhG/vne
IpTuuzGaSInf24cGNig01hbBuKSg79yNY0llkECPBXbEhfkemEMhg1WHoNP2eG8j
MHS5OCT5KiOfi77pSO3M2mGB1HWYE5R0lcMibukK9ZdyIYcTeMZ0RcGm6YSNv570
ok/Ex4LUCW66AIWFefmbIOtJSIMHlNKWRPJwnJxVoE5qgH0f/2xL3k15vpI55lAS
sabzegnYlElPbUlZGhgwjKknxgqMhFIW/ZS0h2FukFLwipr4qI47nHWz5dguNkYn
48sSKg3YMhVx/sT+X2A/6zqsC+p4PT7Ti5ruWb7S9L9vRuBdIDNE9qAwuz0g8Bs3
WhOx6OW2ZqDQEuRhN0lyGA0mwRC4HPFE9b8dnN8lNm+RsnMfNoFxzPnqtsxhEAwa
2a4ijT97ka94lDy7WQ2bwLRz7trKV/T6MeETKE4s7+z2dMTr1f8IwA2uCovFmO9T
aMQAePFEtDT3qwIPu0zH1ocSCkZ50f7RgVmp4FNn03uT/TnsASrr5CS9m8A9gjEn
QiztQyqt27fTT61YkNdA6lwbpFiByugVbS+mWsNa9kvBkgQkcMQwgrELmU9sYdBT
nRMa60i0nEINT/x3zFvT6R7Dl/O8/QhXLeYv20X2roghPw48IovLb8x7dT3YEQSn
ARIXXVPxwOVvS8xcCa69/+1HjC6vNG9dNNnAsVHxB8mDTBqmmLzAMOVzDoNWEgDd
zoRhQ3ORb1brPlKWg8um/svLiSV63ZYi2J8LPamoGmZ/7J8i5rjOpOeG493UICBR
JymmYGUo6/C1Ze8swdMHApVU/spo0s8BCGkMjYUAaxXD7RufN2DuY30Vny/DMn4y
XasuHS9RstD2Okv25PD06Y2H52HJ6MNdArmPZRe0k2ZbhATs5dXOfmaF5Z0f4IkE
G+hsxE1wlCo900ewntx16sBCbI0v9aE+Napf2+ueqPQ06CdfiTG5yOmeXzgR/8zS
KVmTHpmmFpYtj/N350BLAVb/Hwzmh+ieWnO7TUjvNAHUn2i5LZU65rN3GOlPyIlz
DzB2T6KjOUPFKqSRrIin14HLyf5w0vDuJhe5Zpe0hhYKvoKhwCEVefbmkasWeso3
xsXxOOoL39GA0QpYjR6ztqR8fS9jTeu5IY+zY5LO8yS7+StP3H8CcqRMuxb3ntym
-----END RSA PRIVATE KEY-----

View File

@@ -1,14 +1,16 @@
name: Interoperability tests
on: push
permissions:
contents: read
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"'" }}'
run: >
gh api repos/str4d/rage/dispatches
--field event_type="age-interop-request"
--field client_payload[sha]="$GITHUB_SHA"
env:
GITHUB_TOKEN: ${{ secrets.RAGE_INTEROP_ACCESS_TOKEN }}

58
.github/workflows/ronn.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Generate man pages
on:
push:
branches:
- '**'
paths:
- '**.ronn'
- '**/ronn.yml'
permissions:
contents: read
jobs:
ronn:
name: Ronn
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: geomys/sandboxed-step@v1.2.1
with:
persist-workspace-changes: true
run: |
sudo apt-get update && sudo apt-get install -y ronn
bash -O globstar -c 'ronn **/*.ronn'
# 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
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
- uses: actions/upload-artifact@v4
with:
name: man-pages
path: |
doc/*.1
doc/*.html
commit:
name: Commit changes
needs: ronn
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: true
- uses: actions/download-artifact@v4
with:
name: man-pages
path: doc/
- name: Commit and push if changed
run: |-
git config user.name "GitHub Actions"
git config user.email "actions@users.noreply.github.com"
git add doc/
git commit -m "doc: regenerate groff and html man pages" || exit 0
git push

75
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,75 @@
name: Go tests
on:
push:
pull_request:
schedule: # daily at 09:42 UTC
- cron: '42 9 * * *'
workflow_dispatch:
permissions:
contents: read
jobs:
test:
strategy:
fail-fast: false
matrix:
go:
- { go-version: stable }
- { go-version: oldstable }
os:
- ubuntu-latest
- macos-latest
- windows-latest
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go.go-version }}
- run: |
go test -race ./...
test-latest:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
go:
- { go-version: stable }
- { go-version: oldstable }
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go.go-version }}
- uses: geomys/sandboxed-step@v1.2.1
with:
run: |
go get -u -t ./...
go test -race ./...
staticcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: stable
- uses: geomys/sandboxed-step@v1.2.1
with:
run: go run honnef.co/go/tools/cmd/staticcheck@latest ./...
govulncheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-go@v6
with:
go-version: stable
- uses: geomys/sandboxed-step@v1.2.1
with:
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...

View File

@@ -1,5 +0,0 @@
os: linux
arch: arm64
dist: bionic
language: go
go: 1.x

View File

@@ -1,20 +0,0 @@
# 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-beta2.zip"
sha256 "b7417e94c32c7e9356e441815f814073009c4a6455da96bde1536fae8cb0edbf"
depends_on "go" => :build
def install
mkdir bin
system "go", "build", "-trimpath", "-o", bin, "filippo.io/age/cmd/..."
prefix.install_metafiles
end
end

View File

@@ -1,4 +1,6 @@
Copyright 2019 The age Authors
Copyright 2019 Google LLC
Copyright 2022 Filippo Valsorda
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
@@ -10,7 +12,7 @@ notice, this list of conditions and the following disclaimer.
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
* Neither the name of the age project nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

295
README.md
View File

@@ -1,43 +1,208 @@
# age
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/FiloSottile/age/blob/main/logo/logo_white.svg">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/FiloSottile/age/blob/main/logo/logo.svg">
<img alt="The age logo, a wireframe of St. Peters dome in Rome, with the text: age, file encryption" width="600" src="https://github.com/FiloSottile/age/blob/main/logo/logo.svg">
</picture>
</p>
age is a simple, modern and secure file encryption tool.
[![Go Reference](https://pkg.go.dev/badge/filippo.io/age.svg)](https://pkg.go.dev/filippo.io/age)
[![man page](<https://img.shields.io/badge/age(1)-man%20page-lightgrey>)](https://filippo.io/age/age.1)
[![C2SP specification](https://img.shields.io/badge/%C2%A7%23-specification-blueviolet)](https://age-encryption.org/v1)
It features small explicit keys, no config options, and UNIX-style composability.
age is a simple, modern and secure file encryption tool, format, and Go library.
It features small explicit keys, post-quantum support, 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 -d -i key.txt data.tar.gz.age > data.tar.gz
$ 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).
📜 The format specification is at [age-encryption.org/v1](https://age-encryption.org/v1). age was designed by [@benjojo](https://github.com/benjojo) and [@FiloSottile](https://github.com/FiloSottile).
An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
🦀 An alternative interoperable Rust implementation is available at [github.com/str4d/rage](https://github.com/str4d/rage).
🌍 [Typage](https://github.com/FiloSottile/typage) is a TypeScript implementation. It works in the browser, Node.js, Deno, and Bun.
🔑 Hardware PIV tokens such as YubiKeys are supported through the [age-plugin-yubikey](https://github.com/str4d/age-plugin-yubikey) plugin.
✨ For more plugins, implementations, tools, and integrations, check out the [awesome age](https://github.com/FiloSottile/awesome-age) list.
💬 The author pronounces it `[aɡe̞]` [with a hard *g*](https://translate.google.com/?sl=it&text=aghe), like GIF, and it's always spelled lowercase.
## Installation
<table>
<tr>
<td>Homebrew (macOS or Linux)</td>
<td>
<code>brew install age</code>
</td>
</tr>
<tr>
<td>MacPorts</td>
<td>
<code>port install age</code>
</td>
</tr>
<tr>
<td>Windows</td>
<td>
<code>winget install --id FiloSottile.age</code>
</td>
</tr>
<tr>
<td>Alpine Linux v3.15+</td>
<td>
<code>apk add age</code>
</td>
</tr>
<tr>
<td>Arch Linux</td>
<td>
<code>pacman -S age</code>
</td>
</tr>
<tr>
<td>Debian 12+ (Bookworm)</td>
<td>
<code>apt install age</code>
</td>
</tr>
<tr>
<td>Debian 11 (Bullseye)</td>
<td>
<code>apt install age/bullseye-backports</code>
(<a href="https://backports.debian.org/Instructions/#index2h2">enable backports</a> for age v1.0.0+)
</td>
</tr>
<tr>
<td>Fedora 33+</td>
<td>
<code>dnf install age</code>
</td>
</tr>
<tr>
<td>Gentoo Linux</td>
<td>
<code>emerge app-crypt/age</code>
</td>
</tr>
<tr>
<td>Guix System</td>
<td>
<code>guix package -i age</code>
</td>
</tr>
<tr>
<td>NixOS / Nix</td>
<td>
<code>nix-env -i age</code>
</td>
</tr>
<tr>
<td>openSUSE Tumbleweed</td>
<td>
<code>zypper install age</code>
</td>
</tr>
<tr>
<td>Ubuntu 22.04+</td>
<td>
<code>apt install age</code>
</td>
</tr>
<tr>
<td>Void Linux</td>
<td>
<code>xbps-install age</code>
</td>
</tr>
<tr>
<td>FreeBSD</td>
<td>
<code>pkg install age</code> (security/age)
</td>
</tr>
<tr>
<td>OpenBSD 6.7+</td>
<td>
<code>pkg_add age</code> (security/age)
</td>
</tr>
<tr>
<td>Chocolatey (Windows)</td>
<td>
<code>choco install age.portable</code>
</td>
</tr>
<tr>
<td>Scoop (Windows)</td>
<td>
<code>scoop bucket add extras && scoop install age</code>
</td>
</tr>
</table>
On Windows, Linux, macOS, and FreeBSD you can use the pre-built binaries.
```
https://dl.filippo.io/age/latest?for=linux/amd64
https://dl.filippo.io/age/v1.3.1?for=darwin/arm64
...
```
If you download the pre-built binaries, you can check their [Sigsum proofs](./SIGSUM.md).
If your system has [a supported version of Go](https://go.dev/dl/), you can build from source.
```
go install filippo.io/age/cmd/...@latest
```
Help from new packagers is very welcome.
## Usage
For the full documentation, read [the age(1) man page](https://filippo.io/age/age.1).
```
Usage:
age -r RECIPIENT [-a] [-o OUTPUT] [INPUT]
age --decrypt [-i KEY] [-o OUTPUT] [INPUT]
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.
-d, --decrypt Decrypt the input to the output.
-i, --identity KEY Use the private key file at path KEY. 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, as generated by age-keygen, ("age1...")
RECIPIENT can be an age public key generated by age-keygen ("age1...")
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
KEY is a path to a file with age secret keys, one per line
(ignoring "#" prefixed comments and empty lines), or to an SSH key file.
Multiple keys can be provided, and any unused ones will be ignored.
Recipient files contain one or more recipients, one per line. Empty lines
and lines starting with "#" are ignored as comments. "-" may be used to
read recipients from standard input.
Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
one per line, or an SSH key. Empty lines and lines starting with "#" are
ignored as comments. Passphrase encrypted age files can be used as
identity files. Multiple key files can be provided, and any unused ones
will be ignored. "-" may be used to read identities from standard input.
When --encrypt is specified explicitly, -i can also be used to encrypt to an
identity file symmetrically, instead or in addition to normal recipients.
```
### Multiple recipients
@@ -49,6 +214,43 @@ $ age -o example.jpg.age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sf
-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.
### Post-quantum keys
To generate hybrid post-quantum keys, which are secure against future quantum
computer attacks, use the `-pq` flag with `age-keygen`. This may become the
default in the future.
Post-quantum identities start with `AGE-SECRET-KEY-PQ-1...` and recipients with
`age1pq1...`. The recipients are unfortunately ~2000 characters long.
```
$ age-keygen -pq -o key.txt
$ age-keygen -y key.txt > recipient.txt
$ age -R recipient.txt example.jpg > example.jpg.age
$ age -d -i key.txt example.jpg.age > example.jpg
```
Support for post-quantum keys is built into age v1.3.0 and later. Alternatively,
the `age-plugin-pq` binary can be installed and placed in `$PATH` to add support
to any version and implementation of age that supports plugins. Recipients will
work out of the box, while identities will have to be converted to plugin
identities with `age-plugin-pq -identity`.
### 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.
@@ -61,55 +263,64 @@ $ age -d secrets.txt.age > secrets.txt
Enter passphrase:
```
### Passphrase-protected key files
If an identity file passed to `-i` is a passphrase encrypted age file, it will be automatically decrypted.
```
$ age-keygen | age -p > key.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
$ age -d -i key.age secrets.txt.age > secrets.txt
Enter passphrase for identity file "key.age":
```
Passphrase-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system. However, they can be useful if the identity file is stored remotely.
### SSH keys
As a convenience feature, age also supports encrypting to `ssh-rsa` and `ssh-ed25519` SSH public keys, and decrypting with the respective private key file. (`ssh-agent` is not supported.)
```
$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIZDRcvS8PnhXr30WKSKmf7WKKi92ACUa5nW589WukJz filippo@Bistromath.local
$ age -r "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIZDRcvS8PnhXr30WKSKmf7WKKi92ACUa5nW589WukJz" example.jpg > example.jpg.age
$ 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.
## Installation
#### Encrypting to a GitHub user
On macOS or Linux, you can use Homebrew:
Combining SSH key support and `-R`, you can easily encrypt a file to the SSH keys listed on a GitHub profile.
```
brew tap filippo.io/age https://filippo.io/age
brew install age
$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age
```
On Windows, Linux, and macOS, you can use [the pre-built binaries](https://github.com/FiloSottile/age/releases).
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.
If your system has [Go 1.13+](https://golang.org/dl/), you can build from source:
### Inspecting encrypted files
The `age-inspect` command can display metadata about an encrypted file without decrypting it, including the recipient types, whether it uses post-quantum encryption, and the payload size.
```
git clone https://filippo.io/age && cd age
go build -o . filippo.io/age/cmd/...
```
$ age-inspect secrets.age
secrets.age is an age file, version "age-encryption.org/v1".
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/):
This file is encrypted to the following recipient types:
- "mlkem768x25519"
```bash
git clone https://aur.archlinux.org/age.git
cd age
makepkg -si
```
This file uses post-quantum encryption.
On OpenBSD -current and 6.7+, you can use the port:
Size breakdown (assuming it decrypts successfully):
Header 1627 bytes
Encryption overhead 32 bytes
Payload 42 bytes
-------------------
Total 1701 bytes
```
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.
For scripting, use `--json` to get machine-readable output.

42
SIGSUM.md Normal file
View File

@@ -0,0 +1,42 @@
If you download the pre-built binaries of version v1.2.0+, you can check their
[Sigsum](https://www.sigsum.org) proofs, which are like signatures with extra
transparency: you can cryptographically verify that every proof is logged in a
public append-only log, so the age project can be held accountable for every
binary release we ever produced. This is similar to what the [Go Checksum
Database](https://go.dev/blog/module-mirror-launch) provides.
```
cat << EOF > age-sigsum-key.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1WpnEswJLPzvXJDiswowy48U+G+G1kmgwUE2eaRHZG
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAz2WM5CyPLqiNjk7CLl4roDXwKhQ0QExXLebukZEZFS
EOF
curl -JLO "https://dl.filippo.io/age/v1.3.1?for=darwin/arm64"
curl -JLO "https://dl.filippo.io/age/v1.3.1?for=darwin/arm64&proof"
go install sigsum.org/sigsum-go/cmd/sigsum-verify@v0.13.1
sigsum-verify -k age-sigsum-key.pub -P sigsum-generic-2025-1 \
age-v1.3.1-darwin-arm64.tar.gz.proof < age-v1.3.1-darwin-arm64.tar.gz
```
You can learn more about what's happening above in the [Sigsum
docs](https://www.sigsum.org/getting-started/).
### Release playbook
Dear future me, to sign a new release and produce Sigsum proofs, run the following
```
VERSION=v1.3.1
go install sigsum.org/sigsum-go/cmd/sigsum-verify@latest
go install github.com/tillitis/tkey-ssh-agent/cmd/tkey-ssh-agent@latest
tkey-ssh-agent --agent-socket tkey-ssh-agent.sock --uss
SSH_AUTH_SOCK=tkey-ssh-agent.sock ssh-add -L > tkey-ssh-agent.pub
passage other/sigsum-ratelimit > sigsum-ratelimit
gh release download $VERSION --dir artifacts/
SSH_AUTH_SOCK=tkey-ssh-agent.sock sigsum-submit -k tkey-ssh-agent.pub -P sigsum-generic-2025-1 -a sigsum-ratelimit -d filippo.io artifacts/*
gh release upload $VERSION artifacts/*.proof
```
In the future, we will move to reproducing the artifacts locally, and signing
those instead of the ones built by GitHub Actions.

494
age.go
View File

@@ -1,170 +1,379 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
// Package age implements file encryption according to the age-encryption.org/v1
// specification.
//
// For most use cases, use the Encrypt and Decrypt functions with
// X25519Recipient and X25519Identity. If passphrase encryption is required, use
// ScryptRecipient and ScryptIdentity. For compatibility with existing SSH keys
// use the filippo.io/agessh package.
// For most use cases, use the [Encrypt] and [Decrypt] functions with
// [HybridRecipient] and [HybridIdentity]. 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,
// age encrypted files are binary and not malleable. For encoding them as text,
// use the filippo.io/age/armor package.
//
// # Key management
//
// age does not have a global keyring. Instead, since age keys are small,
// textual, and cheap, you are encouraged to generate dedicated keys for each
// task and application.
//
// Recipient public keys can be passed around as command line flags and in
// config files, while secret keys should be stored in dedicated files, through
// secret management systems, or as environment variables.
//
// There is no default path for age keys. Instead, they should be stored at
// application-specific paths. The CLI supports files where private keys are
// listed one per line, ignoring empty lines and lines starting with "#". These
// files can be parsed with [ParseIdentities].
//
// When integrating age into a new system, it's recommended that you only
// support native (X25519 and hybrid) 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 (
"bytes"
"crypto/hmac"
"crypto/rand"
"errors"
"fmt"
"io"
"slices"
"sort"
"filippo.io/age/internal/format"
"filippo.io/age/internal/stream"
)
// An Identity is a private key or other value that can decrypt an opaque file
// key from a recipient stanza.
//
// Unwrap must return ErrIncorrectIdentity for recipient blocks that don't match
// the identity, any other error might be considered fatal.
// 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 [HybridIdentity], a
// plugin, or a custom implementation.
type Identity interface {
Type() string
Unwrap(block *Stanza) (fileKey []byte, err error)
}
// IdentityMatcher can be optionally implemented by an Identity that can
// communicate whether it can decrypt a recipient stanza without decrypting it.
//
// If an Identity implements IdentityMatcher, its Unwrap method will only be
// invoked on blocks for which Match returned nil. Match must return
// ErrIncorrectIdentity for recipient blocks that don't match the identity, any
// other error might be considered fatal.
type IdentityMatcher interface {
Identity
Match(block *Stanza) error
// 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 method directly, and
// should instead pass [Identity] implementations to [Decrypt].
Unwrap(stanzas []*Stanza) (fileKey []byte, err error)
}
// ErrIncorrectIdentity is returned by [Identity.Unwrap] if none of the
// recipient stanzas match the identity.
var ErrIncorrectIdentity = errors.New("incorrect identity for recipient block")
// A Recipient is a public key or other value that can encrypt an opaque file
// key to a recipient stanza.
// 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 [HybridRecipient],
// a plugin, or a custom implementation.
type Recipient interface {
Type() string
Wrap(fileKey []byte) (*Stanza, error)
// Most age API users won't need to interact with this method directly, and
// should instead pass [Recipient] implementations to [Encrypt].
Wrap(fileKey []byte) ([]*Stanza, error)
}
// RecipientWithLabels can be optionally implemented by a [Recipient], in which
// case [Encrypt] will use WrapWithLabels instead of [Recipient.Wrap].
//
// Encrypt will succeed only if the labels returned by all the recipients
// (assuming the empty set for those that don't implement RecipientWithLabels)
// are the same.
//
// This can be used to ensure a recipient is only used with other recipients
// with equivalent properties (for example by setting a "postquantum" label) or
// to ensure a recipient is always used alone (by returning a random label, for
// example to preserve its authentication properties).
type RecipientWithLabels interface {
WrapWithLabels(fileKey []byte) (s []*Stanza, labels []string, err 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 type directly, and should
// instead pass [Recipient] implementations to [Encrypt] and [Identity]
// implementations to [Decrypt].
type Stanza struct {
Type string
Args []string
Body []byte
}
// Encrypt returns a WriteCloser. Writes to the returned value 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 returned value when done for the last chunk
// to be encrypted and flushed to dst.
func Encrypt(dst io.Writer, recipients ...Recipient) (io.WriteCloser, error) {
const fileKeySize = 16
const streamNonceSize = 16
func encryptHdr(fileKey []byte, recipients ...Recipient) (*format.Header, error) {
if len(recipients) == 0 {
return nil, errors.New("no recipients specified")
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
return nil, err
}
hdr := &format.Header{}
var labels []string
for i, r := range recipients {
if r.Type() == "scrypt" && len(recipients) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
block, err := r.Wrap(fileKey)
stanzas, l, err := wrapWithLabels(r, fileKey)
if err != nil {
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %v", i, err)
return nil, fmt.Errorf("failed to wrap key for recipient #%d: %w", i, err)
}
sort.Strings(l)
if i == 0 {
labels = l
} else if !slicesEqual(labels, l) {
return nil, incompatibleLabelsError(labels, l)
}
for _, s := range stanzas {
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(s))
}
hdr.Recipients = append(hdr.Recipients, (*format.Stanza)(block))
}
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, 16)
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)
return hdr, nil
}
// Encrypt encrypts a file to one or more recipients. Every recipient will be
// able to decrypt the file.
//
// Writes to the returned WriteCloser are encrypted and written to dst as an age
// 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) {
fileKey := make([]byte, fileKeySize)
rand.Read(fileKey)
hdr, err := encryptHdr(fileKey, recipients...)
if err != nil {
return nil, err
}
if err := hdr.Marshal(dst); err != nil {
return nil, fmt.Errorf("failed to write header: %w", err)
}
nonce := make([]byte, streamNonceSize)
rand.Read(nonce)
if _, err := dst.Write(nonce); err != nil {
return nil, fmt.Errorf("failed to write nonce: %w", err)
}
return stream.NewEncryptWriter(streamKey(fileKey, nonce), dst)
}
// EncryptReader encrypts a file to one or more recipients. Every recipient will be
// able to decrypt the file.
//
// Reads from the returned Reader produce the encrypted file, where the plaintext
// is read from src.
func EncryptReader(src io.Reader, recipients ...Recipient) (io.Reader, error) {
fileKey := make([]byte, fileKeySize)
rand.Read(fileKey)
hdr, err := encryptHdr(fileKey, recipients...)
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
if err := hdr.Marshal(buf); err != nil {
return nil, fmt.Errorf("failed to prepare header: %w", err)
}
nonce := make([]byte, streamNonceSize)
rand.Read(nonce)
r, err := stream.NewEncryptReader(streamKey(fileKey, nonce), src)
if err != nil {
return nil, err
}
return io.MultiReader(buf, bytes.NewReader(nonce), r), nil
}
func wrapWithLabels(r Recipient, fileKey []byte) (s []*Stanza, labels []string, err error) {
if r, ok := r.(RecipientWithLabels); ok {
return r.WrapWithLabels(fileKey)
}
s, err = r.Wrap(fileKey)
return
}
func slicesEqual(s1, s2 []string) bool {
if len(s1) != len(s2) {
return false
}
for i := range s1 {
if s1[i] != s2[i] {
return false
}
}
return true
}
func incompatibleLabelsError(l1, l2 []string) error {
hasPQ1 := slices.Contains(l1, "postquantum")
hasPQ2 := slices.Contains(l2, "postquantum")
if hasPQ1 != hasPQ2 {
return fmt.Errorf("incompatible recipients: can't mix post-quantum and classic recipients, or the file would be vulnerable to quantum computers")
}
return fmt.Errorf("incompatible recipients: %q and %q can't be mixed", l1, l2)
}
// 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
// StanzaTypes are the first argument of each recipient stanza in the
// encrypted file's header.
StanzaTypes []string
}
func (e *NoIdentityMatchError) Error() string {
if len(e.Errors) == 1 {
return "identity did not match any of the recipients: " + e.Errors[0].Error()
}
return "no identity matched any of the recipients"
}
func (e *NoIdentityMatchError) Unwrap() []error {
return e.Errors
}
// Decrypt decrypts a file encrypted to one or more identities.
// All identities will be tried until one successfully decrypts the file.
// Native, non-interactive identities are tried before any other identities.
//
// Decrypt 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.
// from src. If no identity matches the encrypted file, the returned error will
// be of type [NoIdentityMatchError].
func Decrypt(src io.Reader, identities ...Identity) (io.Reader, error) {
hdr, payload, err := format.Parse(src)
if err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
fileKey, err := decryptHdr(hdr, identities...)
if err != nil {
return nil, err
}
nonce := make([]byte, streamNonceSize)
if _, err := io.ReadFull(payload, nonce); err != nil {
return nil, fmt.Errorf("failed to read nonce: %w", err)
}
return stream.NewDecryptReader(streamKey(fileKey, nonce), payload)
}
// DecryptReaderAt decrypts a file encrypted to one or more identities.
// All identities will be tried until one successfully decrypts the file.
// Native, non-interactive identities are tried before any other identities.
//
// DecryptReaderAt takes an underlying [io.ReaderAt] and its total encrypted
// size, and returns a ReaderAt of the decrypted plaintext and the plaintext
// size. These can be used for example to instantiate an [io.SectionReader],
// which implements [io.Reader] and [io.Seeker], or for [zip.NewReader].
// Note that ReaderAt by definition disregards the seek position of src.
//
// The ReadAt method of the returned ReaderAt can be called concurrently.
// The ReaderAt will internally cache the most recently decrypted chunk.
// DecryptReaderAt reads and decrypts the final chunk before returning,
// to authenticate the plaintext size.
//
// If no identity matches the encrypted file, the returned error will be of
// type [NoIdentityMatchError].
func DecryptReaderAt(src io.ReaderAt, encryptedSize int64, identities ...Identity) (io.ReaderAt, int64, error) {
srcReader := io.NewSectionReader(src, 0, encryptedSize)
hdr, payload, err := format.Parse(srcReader)
if err != nil {
return nil, 0, fmt.Errorf("failed to read header: %w", err)
}
buf := &bytes.Buffer{}
if err := hdr.Marshal(buf); err != nil {
return nil, 0, fmt.Errorf("failed to serialize header: %w", err)
}
fileKey, err := decryptHdr(hdr, identities...)
if err != nil {
return nil, 0, err
}
nonce := make([]byte, streamNonceSize)
if _, err := io.ReadFull(payload, nonce); err != nil {
return nil, 0, fmt.Errorf("failed to read nonce: %w", err)
}
payloadOffset := int64(buf.Len()) + int64(len(nonce))
payloadSize := encryptedSize - payloadOffset
plaintextSize, err := stream.PlaintextSize(payloadSize)
if err != nil {
return nil, 0, err
}
payloadReaderAt := io.NewSectionReader(src, payloadOffset, payloadSize)
r, err := stream.NewDecryptReaderAt(streamKey(fileKey, nonce), payloadReaderAt, payloadSize)
if err != nil {
return nil, 0, err
}
return r, plaintextSize, nil
}
func decryptHdr(hdr *format.Header, identities ...Identity) ([]byte, error) {
if len(identities) == 0 {
return nil, errors.New("no identities specified")
}
slices.SortStableFunc(identities, func(a, b Identity) int {
var aIsNative, bIsNative bool
switch a.(type) {
case *X25519Identity, *HybridIdentity, *ScryptIdentity:
aIsNative = true
}
switch b.(type) {
case *X25519Identity, *HybridIdentity, *ScryptIdentity:
bIsNative = true
}
if aIsNative && !bIsNative {
return -1
}
if !aIsNative && bIsNative {
return 1
}
return 0
})
hdr, payload, err := format.Parse(src)
if err != nil {
return nil, fmt.Errorf("failed to read header: %v", err)
stanzas := make([]*Stanza, 0, len(hdr.Recipients))
errNoMatch := &NoIdentityMatchError{}
for _, s := range hdr.Recipients {
errNoMatch.StanzaTypes = append(errNoMatch.StanzaTypes, s.Type)
stanzas = append(stanzas, (*Stanza)(s))
}
if len(hdr.Recipients) > 20 {
return nil, errors.New("too many recipients")
}
var fileKey []byte
RecipientsLoop:
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")
for _, id := range identities {
var err error
fileKey, err = id.Unwrap(stanzas)
if errors.Is(err, ErrIncorrectIdentity) {
errNoMatch.Errors = append(errNoMatch.Errors, err)
continue
}
for _, i := range identities {
if i.Type() != r.Type {
continue
}
if i, ok := i.(IdentityMatcher); ok {
err := i.Match((*Stanza)(r))
if err != nil {
if err == ErrIncorrectIdentity {
continue
}
return nil, err
}
}
fileKey, err = i.Unwrap((*Stanza)(r))
if err != nil {
if err == ErrIncorrectIdentity {
// TODO: we should collect these errors and return them as an
// []error type with an Error method. That will require turning
// ErrIncorrectIdentity into an interface or wrapper error.
continue
}
return nil, err
}
break RecipientsLoop
if err != nil {
return nil, err
}
break
}
if fileKey == nil {
return nil, errors.New("no identity matched a recipient")
return nil, errNoMatch
}
if mac, err := headerMAC(fileKey, hdr); err != nil {
@@ -173,10 +382,77 @@ RecipientsLoop:
return nil, errors.New("bad header MAC")
}
nonce := make([]byte, 16)
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)
return fileKey, nil
}
// 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
}
// ExtractHeader returns a detached header from the src file.
//
// The detached header can be decrypted with [DecryptHeader] (for example on a
// different system, without sharing the ciphertext) and then the file key can
// be used with [NewInjectedFileKeyIdentity].
//
// This is a low-level function that most users won't need.
func ExtractHeader(src io.Reader) ([]byte, error) {
hdr, _, err := format.Parse(src)
if err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
buf := &bytes.Buffer{}
if err := hdr.Marshal(buf); err != nil {
return nil, fmt.Errorf("failed to serialize header: %w", err)
}
return buf.Bytes(), nil
}
// DecryptHeader decrypts a detached header and returns a file key.
//
// The detached header can be produced by [ExtractHeader], and the
// returned file key can be used with [NewInjectedFileKeyIdentity].
//
// This is a low-level function that most users won't need.
// It is the caller's responsibility to keep track of what file the
// returned file key decrypts, and to ensure the file key is not used
// for any other purpose.
func DecryptHeader(header []byte, identities ...Identity) ([]byte, error) {
hdr, _, err := format.Parse(bytes.NewReader(header))
if err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
return decryptHdr(hdr, identities...)
}
type injectedFileKeyIdentity struct {
fileKey []byte
}
// NewInjectedFileKeyIdentity returns an [Identity] that always produces
// a fixed file key, allowing the use of a file key obtained out-of-band,
// for example via [DecryptHeader].
//
// This is a low-level function that most users won't need.
func NewInjectedFileKeyIdentity(fileKey []byte) Identity {
return injectedFileKeyIdentity{fileKey}
}
func (i injectedFileKeyIdentity) Unwrap(stanzas []*Stanza) (fileKey []byte, err error) {
return i.fileKey, nil
}

View File

@@ -1,18 +1,20 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package age_test
import (
"archive/zip"
"bytes"
"encoding/hex"
"errors"
"fmt"
"io"
"io/ioutil"
"io/fs"
"log"
"os"
"slices"
"strings"
"testing"
"filippo.io/age"
@@ -43,31 +45,59 @@ func ExampleEncrypt() {
// Encrypted file size: 219
}
var fileContents, _ = hex.DecodeString("6167652d656e6372797074696f6e2e6f72" +
"672f76310a2d3e20583235353139203868726c4d2b5a4247334464346646322b61353" +
"8337a64544957446b382f5234316b43595a7376775457340a794f345059646c4d5744" +
"4a2b437867554e527159355a30542f6d2b6733464368356a4978474c62435658630a2" +
"d2d2d20492f696d65765a7a79383132304a537a6d4a6e6d6e2f4b4d6b337035413131" +
"5638334e6b34316d394e50450a70c5e53624a1520753f92c5ad10ecab273ba4d61178" +
"07713e83820417a1df2ca08182272c8f85c857734a1311a3b75e98d0eaf")
// 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
var privateKey = "AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU"
func init() {
privateKey = "AGE-SECRET-KEY-184JMZMVQH3E6U0PSL869004Y3U2NYV7R30EU99CSEDNPH02YUVFSZW44VU"
}
func ExampleDecrypt() {
// 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)
log.Fatalf("Failed to parse private key: %v", err)
}
out := &bytes.Buffer{}
f := bytes.NewReader(fileContents)
f, err := os.Open("testdata/example.age")
if err != nil {
log.Fatalf("Failed to open file: %v", err)
}
r, err := age.Decrypt(f, identity)
if err != nil {
log.Fatalf("Failed to open encrypted file: %v", err)
}
out := &bytes.Buffer{}
if _, err := io.Copy(out, r); err != nil {
log.Fatalf("Failed to read encrypted file: %v", err)
}
fmt.Printf("File contents: %q\n", out.Bytes())
// Output:
// File contents: "Black lives matter."
}
func ExampleParseIdentities() {
keyFile, err := os.Open("testdata/example_keys.txt")
if err != nil {
log.Fatalf("Failed to open private keys file: %v", err)
}
identities, err := age.ParseIdentities(keyFile)
if err != nil {
log.Fatalf("Failed to parse private key: %v", err)
}
f, err := os.Open("testdata/example.age")
if err != nil {
log.Fatalf("Failed to open file: %v", err)
}
r, err := age.Decrypt(f, identities...)
if err != nil {
log.Fatalf("Failed to open encrypted file: %v", err)
}
out := &bytes.Buffer{}
if _, err := io.Copy(out, r); err != nil {
log.Fatalf("Failed to read encrypted file: %v", err)
}
@@ -117,7 +147,7 @@ func TestEncryptDecryptX25519(t *testing.T) {
if err != nil {
t.Fatal(err)
}
outBytes, err := ioutil.ReadAll(out)
outBytes, err := io.ReadAll(out)
if err != nil {
t.Fatal(err)
}
@@ -154,7 +184,364 @@ func TestEncryptDecryptScrypt(t *testing.T) {
if err != nil {
t.Fatal(err)
}
outBytes, err := ioutil.ReadAll(out)
outBytes, err := io.ReadAll(out)
if err != nil {
t.Fatal(err)
}
if string(outBytes) != helloWorld {
t.Errorf("wrong data: %q, excepted %q", outBytes, helloWorld)
}
}
func ExampleDecryptReaderAt() {
identity, err := age.ParseX25519Identity(privateKey)
if err != nil {
log.Fatalf("Failed to parse private key: %v", err)
}
f, err := os.Open("testdata/example.zip.age")
if err != nil {
log.Fatalf("Failed to open file: %v", err)
}
stat, err := f.Stat()
if err != nil {
log.Fatalf("Failed to stat file: %v", err)
}
r, size, err := age.DecryptReaderAt(f, stat.Size(), identity)
if err != nil {
log.Fatalf("Failed to open encrypted file: %v", err)
}
z, err := zip.NewReader(r, size)
if err != nil {
log.Fatalf("Failed to open zip: %v", err)
}
contents, err := fs.ReadFile(z, "example.txt")
if err != nil {
log.Fatalf("Failed to read file from zip: %v", err)
}
fmt.Printf("File contents: %q\n", contents)
// Output:
// File contents: "Black lives matter."
}
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)
}
})
}
}
type testRecipient struct {
labels []string
}
func (testRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
panic("expected WrapWithLabels instead")
}
func (t testRecipient) WrapWithLabels(fileKey []byte) (s []*age.Stanza, labels []string, err error) {
return []*age.Stanza{{Type: "test"}}, t.labels, nil
}
func TestLabels(t *testing.T) {
scrypt, err := age.NewScryptRecipient("xxx")
if err != nil {
t.Fatal(err)
}
i, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
x25519 := i.Recipient()
pqc := testRecipient{[]string{"postquantum"}}
pqcAndFoo := testRecipient{[]string{"postquantum", "foo"}}
fooAndPQC := testRecipient{[]string{"foo", "postquantum"}}
if _, err := age.Encrypt(io.Discard, scrypt, scrypt); err == nil {
t.Error("expected two scrypt recipients to fail")
}
if _, err := age.Encrypt(io.Discard, scrypt, x25519); err == nil {
t.Error("expected x25519 mixed with scrypt to fail")
}
if _, err := age.Encrypt(io.Discard, x25519, scrypt); err == nil {
t.Error("expected x25519 mixed with scrypt to fail")
}
if _, err := age.Encrypt(io.Discard, pqc, x25519); err == nil {
t.Error("expected x25519 mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, x25519, pqc); err == nil {
t.Error("expected x25519 mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, pqc, pqc); err != nil {
t.Errorf("expected two pqc to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, pqc); err != nil {
t.Errorf("expected one pqc to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, pqcAndFoo, pqc); err == nil {
t.Error("expected pqc+foo mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, pqc, pqcAndFoo); err == nil {
t.Error("expected pqc+foo mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, pqc, pqc, pqcAndFoo); err == nil {
t.Error("expected pqc+foo mixed with pqc to fail")
}
if _, err := age.Encrypt(io.Discard, pqcAndFoo, pqcAndFoo); err != nil {
t.Errorf("expected two pqc+foo to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, pqcAndFoo, fooAndPQC); err != nil {
t.Errorf("expected pqc+foo mixed with foo+pqc to work, got %v", err)
}
}
// testIdentity is a non-native identity that records if Unwrap is called.
type testIdentity struct {
called bool
}
func (ti *testIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
ti.called = true
return nil, age.ErrIncorrectIdentity
}
func TestDecryptNativeIdentitiesFirst(t *testing.T) {
correct, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
unrelated, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := age.Encrypt(buf, correct.Recipient())
if err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
nonNative := &testIdentity{}
// Pass identities: unrelated native, non-native, correct native.
// Native identities should be tried first, so correct should match
// before nonNative is ever called.
_, err = age.Decrypt(bytes.NewReader(buf.Bytes()), unrelated, nonNative, correct)
if err != nil {
t.Fatal(err)
}
if nonNative.called {
t.Error("non-native identity was called, but native identities should be tried first")
}
}
type stanzaTypeRecipient string
func (s stanzaTypeRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
return []*age.Stanza{{Type: string(s)}}, nil
}
func TestNoIdentityMatchErrorStanzaTypes(t *testing.T) {
a, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
b, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
wrong, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := age.Encrypt(buf, a.Recipient(), stanzaTypeRecipient("other"), 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)
}
_, err = age.Decrypt(bytes.NewReader(buf.Bytes()), wrong)
if err == nil {
t.Fatal("expected decryption to fail")
}
var noMatch *age.NoIdentityMatchError
if !errors.As(err, &noMatch) {
t.Fatalf("expected NoIdentityMatchError, got %T: %v", err, err)
}
want := []string{"X25519", "other", "X25519"}
if !slices.Equal(noMatch.StanzaTypes, want) {
t.Errorf("StanzaTypes = %v, want %v", noMatch.StanzaTypes, want)
}
}
func TestScryptIdentityErrors(t *testing.T) {
t.Run("not passphrase-encrypted", func(t *testing.T) {
i, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := age.Encrypt(buf, i.Recipient())
if err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
scryptID, err := age.NewScryptIdentity("password")
if err != nil {
t.Fatal(err)
}
_, err = age.Decrypt(bytes.NewReader(buf.Bytes()), scryptID)
if err == nil {
t.Fatal("expected decryption to fail")
}
if !errors.Is(err, age.ErrIncorrectIdentity) {
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
}
if !strings.Contains(err.Error(), "not passphrase-encrypted") {
t.Errorf("expected error to mention 'not passphrase-encrypted', got %v", err)
}
})
t.Run("incorrect passphrase", func(t *testing.T) {
r, err := age.NewScryptRecipient("correct-password")
if err != nil {
t.Fatal(err)
}
r.SetWorkFactor(10) // Low for fast test
buf := &bytes.Buffer{}
w, err := age.Encrypt(buf, r)
if err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
scryptID, err := age.NewScryptIdentity("wrong-password")
if err != nil {
t.Fatal(err)
}
_, err = age.Decrypt(bytes.NewReader(buf.Bytes()), scryptID)
if err == nil {
t.Fatal("expected decryption to fail")
}
if !errors.Is(err, age.ErrIncorrectIdentity) {
t.Errorf("expected ErrIncorrectIdentity, got %v", err)
}
if !strings.Contains(err.Error(), "incorrect passphrase") {
t.Errorf("expected error to mention 'incorrect passphrase', got %v", err)
}
})
}
func TestDetachedHeader(t *testing.T) {
i, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := age.Encrypt(buf, i.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)
}
encrypted := buf.Bytes()
header, err := age.ExtractHeader(bytes.NewReader(encrypted))
if err != nil {
t.Fatal(err)
}
fileKey, err := age.DecryptHeader(header, i)
if err != nil {
t.Fatal(err)
}
identity := age.NewInjectedFileKeyIdentity(fileKey)
out, err := age.Decrypt(bytes.NewReader(encrypted), identity)
if err != nil {
t.Fatal(err)
}
outBytes, err := io.ReadAll(out)
if err != nil {
t.Fatal(err)
}
if string(outBytes) != helloWorld {
t.Errorf("wrong data: %q, expected %q", outBytes, helloWorld)
}
}
func TestEncryptReader(t *testing.T) {
a, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
r, err := age.EncryptReader(strings.NewReader(helloWorld), a.Recipient())
if err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
if _, err := io.Copy(buf, r); err != nil {
t.Fatal(err)
}
out, err := age.Decrypt(buf, a)
if err != nil {
t.Fatal(err)
}
outBytes, err := io.ReadAll(out)
if err != nil {
t.Fatal(err)
}

View File

@@ -1,15 +1,16 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
// Package agessh provides age.Identity and age.Recipient implementations of
// types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH key files
// for encryption with age-encryption.org/v1.
// types "ssh-rsa" and "ssh-ed25519", which allow reusing existing SSH keys for
// encryption with age-encryption.org/v1.
//
// These should only be used for compatibility with existing keys, and native
// X25519 keys should be preferred otherwise.
// These recipient types should only be used for compatibility with existing
// keys, and native 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 (
@@ -21,10 +22,10 @@ import (
"errors"
"fmt"
"io"
"math/big"
"filippo.io/age"
"filippo.io/age/internal/format"
"filippo.io/edwards25519"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/curve25519"
"golang.org/x/crypto/hkdf"
@@ -45,8 +46,6 @@ type RSARecipient struct {
var _ age.Recipient = &RSARecipient{}
func (*RSARecipient) Type() string { return "ssh-rsa" }
func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
if pk.Type() != "ssh-rsa" {
return nil, errors.New("SSH public key is not an RSA key")
@@ -64,10 +63,13 @@ func NewRSARecipient(pk ssh.PublicKey) (*RSARecipient, error) {
} 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) {
func (r *RSARecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
l := &age.Stanza{
Type: "ssh-rsa",
Args: []string{sshFingerprint(r.sshKey)},
@@ -80,7 +82,7 @@ func (r *RSARecipient) Wrap(fileKey []byte) (*age.Stanza, error) {
}
l.Body = wrappedKey
return l, nil
return []*age.Stanza{l}, nil
}
type RSAIdentity struct {
@@ -90,8 +92,6 @@ type RSAIdentity struct {
var _ age.Identity = &RSAIdentity{}
func (*RSAIdentity) Type() string { return "ssh-rsa" }
func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {
s, err := ssh.NewSignerFromKey(key)
if err != nil {
@@ -103,7 +103,18 @@ func NewRSAIdentity(key *rsa.PrivateKey) (*RSAIdentity, error) {
return i, nil
}
func (i *RSAIdentity) Unwrap(block *age.Stanza) ([]byte, error) {
func (i *RSAIdentity) Recipient() *RSARecipient {
return &RSARecipient{
sshKey: i.sshKey,
pubKey: &i.k.PublicKey,
}
}
func (i *RSAIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
func (i *RSAIdentity) unwrap(block *age.Stanza) ([]byte, error) {
if block.Type != "ssh-rsa" {
return nil, age.ErrIncorrectIdentity
}
@@ -130,26 +141,28 @@ type Ed25519Recipient struct {
var _ age.Recipient = &Ed25519Recipient{}
func (*Ed25519Recipient) Type() string { return "ssh-ed25519" }
func NewEd25519Recipient(pk ssh.PublicKey) (*Ed25519Recipient, error) {
if pk.Type() != "ssh-ed25519" {
return nil, errors.New("SSH public key is not an Ed25519 key")
}
r := &Ed25519Recipient{
sshKey: pk,
}
if pk, ok := pk.(ssh.CryptoPublicKey); ok {
if pk, ok := pk.CryptoPublicKey().(ed25519.PublicKey); ok {
r.theirPublicKey = ed25519PublicKeyToCurve25519(pk)
} else {
return nil, errors.New("unexpected public key type")
}
} else {
cpk, ok := pk.(ssh.CryptoPublicKey)
if !ok {
return nil, errors.New("pk does not implement ssh.CryptoPublicKey")
}
return r, nil
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) {
@@ -174,40 +187,19 @@ func ParseRecipient(s string) (age.Recipient, error) {
return r, nil
}
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte {
// 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
func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) ([]byte, error) {
// See https://blog.filippo.io/using-ed25519-keys-for-encryption and
// https://pkg.go.dev/filippo.io/edwards25519#Point.BytesMontgomery.
p, err := new(edwards25519.Point).SetBytes(pk)
if err != nil {
return nil, err
}
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 := big.NewInt(1)
denom.ModInverse(denom.Sub(denom, y), curve25519P) // 1 / (1 - y)
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
return p.BytesMontgomery(), nil
}
const ed25519Label = "age-encryption.org/v1/ssh-ed25519"
func (r *Ed25519Recipient) Wrap(fileKey []byte) (*age.Stanza, error) {
func (r *Ed25519Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
ephemeral := make([]byte, curve25519.ScalarSize)
if _, err := rand.Read(ephemeral); err != nil {
return nil, err
@@ -250,7 +242,7 @@ func (r *Ed25519Recipient) Wrap(fileKey []byte) (*age.Stanza, error) {
}
l.Body = wrappedKey
return l, nil
return []*age.Stanza{l}, nil
}
type Ed25519Identity struct {
@@ -260,8 +252,6 @@ type Ed25519Identity struct {
var _ age.Identity = &Ed25519Identity{}
func (*Ed25519Identity) Type() string { return "ssh-ed25519" }
func NewEd25519Identity(key ed25519.PrivateKey) (*Ed25519Identity, error) {
s, err := ssh.NewSignerFromKey(key)
if err != nil {
@@ -284,6 +274,9 @@ func ParseIdentity(pemBytes []byte) (age.Identity, error) {
switch k := k.(type) {
case *ed25519.PrivateKey:
return NewEd25519Identity(*k)
// ParseRawPrivateKey returns inconsistent types. See Issue 429.
case ed25519.PrivateKey:
return NewEd25519Identity(k)
case *rsa.PrivateKey:
return NewRSAIdentity(k)
}
@@ -298,7 +291,18 @@ func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
return out[:curve25519.ScalarSize]
}
func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) {
func (i *Ed25519Identity) Recipient() *Ed25519Recipient {
return &Ed25519Recipient{
sshKey: i.sshKey,
theirPublicKey: i.ourPublicKey,
}
}
func (i *Ed25519Identity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
func (i *Ed25519Identity) unwrap(block *age.Stanza) ([]byte, error) {
if block.Type != "ssh-ed25519" {
return nil, age.ErrIncorrectIdentity
}
@@ -345,7 +349,33 @@ func (i *Ed25519Identity) Unwrap(block *age.Stanza) ([]byte, error) {
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)

View File

@@ -1,8 +1,6 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package agessh_test
@@ -11,15 +9,15 @@ import (
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"reflect"
"testing"
"filippo.io/age/agessh"
"filippo.io/age/internal/format"
"golang.org/x/crypto/ssh"
)
func TestSSHRSARoundTrip(t *testing.T) {
pk, err := rsa.GenerateKey(rand.Reader, 768)
pk, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
@@ -37,23 +35,21 @@ func TestSSHRSARoundTrip(t *testing.T) {
t.Fatal(err)
}
if r.Type() != i.Type() || r.Type() != "ssh-rsa" {
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
// 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)
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
b := &bytes.Buffer{}
(*format.Stanza)(block).Marshal(b)
t.Logf("%s", b.Bytes())
out, err := i.Unwrap(block)
out, err := i.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}
@@ -82,23 +78,21 @@ func TestSSHEd25519RoundTrip(t *testing.T) {
t.Fatal(err)
}
if r.Type() != i.Type() || r.Type() != "ssh-ed25519" {
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
// 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)
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
b := &bytes.Buffer{}
(*format.Stanza)(block).Marshal(b)
t.Logf("%s", b.Bytes())
out, err := i.Unwrap(block)
out, err := i.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}

View File

@@ -1,12 +1,11 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package agessh
import (
"crypto"
"crypto/ed25519"
"crypto/rsa"
"fmt"
@@ -15,16 +14,16 @@ import (
"golang.org/x/crypto/ssh"
)
// EncryptedSSHIdentity is an age.IdentityMatcher implementation based on a
// passphrase encrypted SSH private key.
// EncryptedSSHIdentity is an age.Identity implementation based on a passphrase
// encrypted SSH private key.
//
// It provides public key based matching and deferred decryption so the
// passphrase is only requested if necessary. If the application knows it will
// unconditionally have to decrypt the private key, it would be simpler to use
// ssh.ParseRawPrivateKeyWithPassphrase directly and pass the result to
// NewEd25519Identity or NewRSAIdentity.
// It requests the passphrase only if the public key matches a recipient stanza.
// If the application knows it will always have to decrypt the private key, it
// would be simpler to use ssh.ParseRawPrivateKeyWithPassphrase directly and
// pass the result to NewEd25519Identity or NewRSAIdentity.
type EncryptedSSHIdentity struct {
pubKey ssh.PublicKey
recipient age.Recipient
pemBytes []byte
passphrase func() ([]byte, error)
@@ -35,38 +34,67 @@ type EncryptedSSHIdentity struct {
//
// 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 in can often
// 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{
i := &EncryptedSSHIdentity{
pubKey: pubKey,
pemBytes: pemBytes,
passphrase: passphrase,
}, nil
}
switch t := pubKey.Type(); t {
case "ssh-ed25519":
r, err := NewEd25519Recipient(pubKey)
if err != nil {
return nil, err
}
i.recipient = r
case "ssh-rsa":
r, err := NewRSARecipient(pubKey)
if err != nil {
return nil, err
}
i.recipient = r
default:
return nil, fmt.Errorf("unsupported SSH key type: %v", t)
}
return i, nil
}
var _ age.IdentityMatcher = &EncryptedSSHIdentity{}
var _ age.Identity = &EncryptedSSHIdentity{}
// Type returns the type of the underlying private key, "ssh-ed25519" or "ssh-rsa".
func (i *EncryptedSSHIdentity) Type() string {
return i.pubKey.Type()
func (i *EncryptedSSHIdentity) Recipient() age.Recipient {
return i.recipient
}
// Unwrap implements age.Identity. If the private key is still encrypted, it
// will request the passphrase. The decrypted private key will be cached after
// the first successful invocation.
func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) {
// 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(block)
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()
@@ -78,36 +106,30 @@ func (i *EncryptedSSHIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err er
return nil, fmt.Errorf("failed to decrypt SSH key file: %v", err)
}
var pubKey interface {
Equal(x crypto.PublicKey) bool
}
switch k := k.(type) {
case *ed25519.PrivateKey:
i.decrypted, err = NewEd25519Identity(*k)
pubKey = k.Public().(ed25519.PublicKey)
// ParseRawPrivateKey returns inconsistent types. See Issue 429.
case ed25519.PrivateKey:
i.decrypted, err = NewEd25519Identity(k)
pubKey = k.Public().(ed25519.PublicKey)
case *rsa.PrivateKey:
i.decrypted, err = NewRSAIdentity(k)
pubKey = &k.PublicKey
default:
return nil, fmt.Errorf("unexpected SSH key type: %T", k)
}
if err != nil {
return nil, fmt.Errorf("invalid SSH key: %v", err)
}
if i.decrypted.Type() != i.pubKey.Type() {
return nil, fmt.Errorf("mismatched SSH key type: got %q, expected %q", i.decrypted.Type(), i.pubKey.Type())
if exp := i.pubKey.(ssh.CryptoPublicKey).CryptoPublicKey(); !pubKey.Equal(exp) {
return nil, fmt.Errorf("mismatched private and public SSH key")
}
return i.decrypted.Unwrap(block)
}
// Match implements age.IdentityMatcher without decrypting the private key, to
// ensure the passphrase is only obtained if necessary.
func (i *EncryptedSSHIdentity) Match(block *age.Stanza) error {
if block.Type != i.Type() {
return age.ErrIncorrectIdentity
}
if len(block.Args) < 1 {
return fmt.Errorf("invalid %v recipient block", i.Type())
}
if block.Args[0] != sshFingerprint(i.pubKey) {
return age.ErrIncorrectIdentity
}
return nil
return i.decrypted.Unwrap(stanzas)
}

View File

@@ -1,8 +1,6 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
// Package armor provides a strict, streaming implementation of the ASCII
// armoring format for age files.
@@ -16,6 +14,7 @@ import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io"
"filippo.io/age/internal/format"
@@ -28,7 +27,7 @@ const (
type armoredWriter struct {
started, closed bool
encoder io.WriteCloser
encoder *format.WrappedBase64Encoder
dst io.Writer
}
@@ -50,15 +49,20 @@ func (a *armoredWriter) Close() error {
if err := a.encoder.Close(); err != nil {
return err
}
_, err := io.WriteString(a.dst, "\n"+Footer+"\n")
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: base64.NewEncoder(base64.StdEncoding.Strict(),
format.NewlineWriter(dst))}
return &armoredWriter{
dst: dst,
encoder: format.NewWrappedBase64Encoder(base64.StdEncoding, dst),
}
}
type armoredReader struct {
@@ -85,22 +89,47 @@ func (r *armoredReader) Read(p []byte) (int, error) {
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")
}
if err == io.EOF && len(line) == 0 {
return nil, io.ErrUnexpectedEOF
} else if err != nil && err != io.EOF {
return nil, err
}
return bytes.TrimSpace(line), nil
line = bytes.TrimSuffix(line, []byte("\n"))
line = bytes.TrimSuffix(line, []byte("\r"))
return line, nil
}
if !r.started {
const maxWhitespace = 1024
drainTrailing := func() error {
buf, err := io.ReadAll(io.LimitReader(r.r, maxWhitespace))
if err != nil {
return err
}
if len(bytes.TrimSpace(buf)) != 0 {
return errors.New("trailing data after armored file")
}
if len(buf) == maxWhitespace {
return errors.New("too much trailing whitespace")
}
return io.EOF
}
var removedWhitespace int
for !r.started {
line, err := getLine()
if err != nil {
return 0, r.setErr(err)
}
// Ignore leading whitespace.
if len(bytes.TrimSpace(line)) == 0 {
removedWhitespace += len(line) + 1
if removedWhitespace > maxWhitespace {
return 0, r.setErr(errors.New("too much leading whitespace"))
}
continue
}
if string(line) != Header {
return 0, r.setErr(errors.New("invalid armor first line: " + string(line)))
return 0, r.setErr(fmt.Errorf("invalid first line: %q", line))
}
r.started = true
}
@@ -109,15 +138,18 @@ func (r *armoredReader) Read(p []byte) (int, error) {
return 0, r.setErr(err)
}
if string(line) == Footer {
return 0, r.setErr(io.EOF)
return 0, r.setErr(drainTrailing())
}
if len(line) == 0 {
return 0, r.setErr(errors.New("empty line in armored data"))
}
if len(line) > format.ColumnsPerLine {
return 0, r.setErr(errors.New("invalid armor: column limit exceeded"))
return 0, r.setErr(errors.New("column limit exceeded"))
}
r.unread = r.buf[:]
n, err := base64.StdEncoding.Strict().Decode(r.unread, line)
if err != nil {
return 0, r.setErr(errors.New("invalid armor: " + err.Error()))
return 0, r.setErr(err)
}
r.unread = r.unread[:n]
@@ -127,9 +159,9 @@ func (r *armoredReader) Read(p []byte) (int, error) {
return 0, r.setErr(err)
}
if string(line) != Footer {
return 0, r.setErr(errors.New("invalid armor closing line: " + string(line)))
return 0, r.setErr(fmt.Errorf("invalid closing line: %q", line))
}
r.err = io.EOF
r.setErr(drainTrailing())
}
nn := copy(p, r.unread)
@@ -137,7 +169,22 @@ func (r *armoredReader) Read(p []byte) (int, error) {
return nn, nil
}
type Error struct {
err error
}
func (e *Error) Error() string {
return "invalid armor: " + e.err.Error()
}
func (e *Error) Unwrap() error {
return e.err
}
func (r *armoredReader) setErr(err error) error {
if err != io.EOF {
err = &Error{err}
}
r.err = err
return err
}

View File

@@ -1,23 +1,26 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
//go:build go1.18
package armor_test
import (
"bytes"
"crypto/rand"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"testing"
"filippo.io/age"
"filippo.io/age/armor"
"filippo.io/age/internal/format"
)
func ExampleNewWriter() {
@@ -87,9 +90,15 @@ kB/RRusYjn+KVJ+KTioxj0THtzZPXcjFKuQ1
}
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, 611)
plain := make([]byte, size)
rand.Read(plain)
if _, err := w.Write(plain); err != nil {
t.Fatal(err)
}
@@ -101,12 +110,21 @@ func TestArmor(t *testing.T) {
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)
out, err := io.ReadAll(r)
if err != nil {
t.Fatal(err)
}
@@ -114,3 +132,50 @@ func TestArmor(t *testing.T) {
t.Error("decoded value doesn't match")
}
}
func FuzzMalleability(f *testing.F) {
tests, err := filepath.Glob("../testdata/testkit/*")
if err != nil {
f.Fatal(err)
}
for _, test := range tests {
contents, err := os.ReadFile(test)
if err != nil {
f.Fatal(err)
}
header, contents, ok := bytes.Cut(contents, []byte("\n\n"))
if !ok {
f.Fatal("testkit file without header")
}
if bytes.Contains(header, []byte("armored: yes")) {
f.Add(contents)
}
}
f.Fuzz(func(t *testing.T, data []byte) {
r := armor.NewReader(bytes.NewReader(data))
content, err := io.ReadAll(r)
if err != nil {
if _, ok := err.(*armor.Error); !ok {
t.Errorf("error type is %T: %v", err, err)
}
t.Skip()
}
buf := &bytes.Buffer{}
w := armor.NewWriter(buf)
if _, err := w.Write(content); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
if !bytes.Equal(normalize(buf.Bytes()), normalize(data)) {
t.Error("re-encoded output different from input")
}
})
}
func normalize(f []byte) []byte {
f = bytes.TrimSpace(f)
f = bytes.Replace(f, []byte("\r\n"), []byte("\n"), -1)
return f
}

128
cmd/age-inspect/inspect.go Normal file
View File

@@ -0,0 +1,128 @@
// Copyright 2025 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"runtime/debug"
"filippo.io/age/internal/inspect"
)
const usage = `Usage:
age-inspect [--json] [INPUT]
Options:
--json Output machine-readable JSON.
INPUT defaults to standard input. "-" may be used as INPUT to explicitly
read from standard input.`
// Version can be set at link time to override debug.BuildInfo.Main.Version when
// building manually without git history. It should look like "v1.2.3".
var Version string
func main() {
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
var (
versionFlag bool
jsonFlag bool
)
flag.BoolVar(&versionFlag, "version", false, "print the version")
flag.BoolVar(&jsonFlag, "json", false, "output machine-readable JSON")
flag.Parse()
if versionFlag {
if buildInfo, ok := debug.ReadBuildInfo(); ok && Version == "" {
Version = buildInfo.Main.Version
}
fmt.Println(Version)
return
}
if flag.NArg() > 1 {
flag.Usage()
os.Exit(1)
}
in := os.Stdin
var fileSize int64 = -1
if name := flag.Arg(0); name != "" && name != "-" {
f, err := os.Open(name)
if err != nil {
errorf("failed to open input file %q: %v", name, err)
}
defer f.Close()
in = f
if stat, err := f.Stat(); err == nil && stat.Mode().IsRegular() {
fileSize = stat.Size()
}
}
data, err := inspect.Inspect(in, fileSize)
if err != nil {
errorf("inspection failed: %v", err)
}
if jsonFlag {
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(data); err != nil {
errorf("failed to encode JSON output: %v", err)
}
} else {
name := flag.Arg(0)
if name == "" {
name = "<stdin>"
}
fmt.Printf("%s is an age file, version %q.\n", name, data.Version)
fmt.Printf("\n")
if data.Armor {
fmt.Printf("This file is ASCII-armored.\n")
fmt.Printf("\n")
}
fmt.Printf("This file is encrypted to the following recipient types:\n")
for _, t := range data.StanzaTypes {
fmt.Printf(" - %q\n", t)
}
fmt.Printf("\n")
switch data.Postquantum {
case "yes":
fmt.Printf("This file uses post-quantum encryption.\n")
fmt.Printf("\n")
case "no":
fmt.Printf("This file does NOT use post-quantum encryption.\n")
fmt.Printf("\n")
}
fmt.Printf("Size breakdown (assuming it decrypts successfully):\n")
fmt.Printf("\n")
fmt.Printf(" Header % 12d bytes\n", data.Sizes.Header)
if data.Armor {
fmt.Printf(" Armor overhead % 12d bytes\n", data.Sizes.Armor)
}
fmt.Printf(" Encryption overhead % 12d bytes\n", data.Sizes.Overhead)
fmt.Printf(" Payload % 12d bytes\n", data.Sizes.MinPayload)
fmt.Printf(" -------------------\n")
total := data.Sizes.Header + data.Sizes.Overhead + data.Sizes.MinPayload + data.Sizes.Armor
fmt.Printf(" Total % 12d bytes\n", total)
fmt.Printf("\n")
fmt.Printf("Tip: for machine-readable output, use --json.\n")
}
}
// l is a logger with no prefixes.
var l = log.New(os.Stderr, "", 0)
func errorf(format string, v ...any) {
l.Printf("age-inspect: error: "+format, v...)
l.Printf("age-inspect: report unexpected or unhelpful errors at https://filippo.io/age/report")
os.Exit(1)
}

View File

@@ -1,62 +1,185 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"runtime/debug"
"time"
"filippo.io/age"
"golang.org/x/crypto/ssh/terminal"
"golang.org/x/term"
)
const usage = `Usage:
age-keygen [-pq] [-o OUTPUT]
age-keygen -y [-o OUTPUT] [INPUT]
Options:
-pq Generate a post-quantum hybrid ML-KEM-768 + X25519 key pair.
(This might become the default in the future.)
-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 or, with the -pq flag, post-quantum
hybrid ML-KEM-768 + 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 -pq
# created: 2025-11-17T12:15:17+01:00
# public key: age1pq1pd[... 1950 more characters ...]
AGE-SECRET-KEY-PQ-1XXC4XS9DXHZ6TREKQTT3XECY8VNNU7GJ83C3Y49D0GZ3ZUME4JWS6QC3EF
$ 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 when
// building manually without git history. It should look like "v1.2.3".
var Version string
func main() {
log.SetFlags(0)
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
outFlag := flag.String("o", "", "output to `FILE` (default stdout)")
var outFlag string
var pqFlag, versionFlag, convertFlag bool
flag.BoolVar(&versionFlag, "version", false, "print the version")
flag.BoolVar(&pqFlag, "pq", false, "generate a post-quantum key pair")
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 {
log.Fatalf("age-keygen takes no arguments")
if versionFlag {
if buildInfo, ok := debug.ReadBuildInfo(); ok && Version == "" {
Version = buildInfo.Main.Version
}
fmt.Println(Version)
return
}
if len(flag.Args()) != 0 && !convertFlag {
errorf("too many arguments")
}
if len(flag.Args()) > 1 && convertFlag {
errorf("too many arguments")
}
if pqFlag && convertFlag {
errorf("-pq cannot be used with -y")
}
out := os.Stdout
if name := *outFlag; name != "" {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
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", name, err)
errorf("failed to open output file %q: %v", outFlag, err)
}
defer f.Close()
defer func() {
if err := f.Close(); err != nil {
errorf("failed to close output file %q: %v", outFlag, err)
}
}()
out = f
}
if fi, err := out.Stat(); err == nil {
if fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {
fmt.Fprintf(os.Stderr, "Warning: writing to a world-readable file.\n"+
"Consider setting the umask to 066 and trying again.\n")
in := os.Stdin
if inFile := flag.Arg(0); inFile != "" && inFile != "-" {
f, err := os.Open(inFile)
if err != nil {
errorf("failed to open input file %q: %v", inFile, err)
}
defer f.Close()
in = f
}
generate(out)
if convertFlag {
convert(in, out)
} else {
if fi, err := out.Stat(); err == nil && fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {
warning("writing secret key to a world-readable file")
}
generate(out, pqFlag)
}
}
func generate(out *os.File) {
k, err := age.GenerateX25519Identity()
if err != nil {
log.Fatalf("Internal error: %v", err)
func generate(out *os.File, pq bool) {
var i age.Identity
var r age.Recipient
if pq {
k, err := age.GenerateHybridIdentity()
if err != nil {
errorf("internal error: %v", err)
}
i = k
r = k.Recipient()
} else {
k, err := age.GenerateX25519Identity()
if err != nil {
errorf("internal error: %v", err)
}
i = k
r = k.Recipient()
}
if !terminal.IsTerminal(int(out.Fd())) {
fmt.Fprintf(os.Stderr, "Public key: %s\n", k.Recipient())
if !term.IsTerminal(int(out.Fd())) {
fmt.Fprintf(os.Stderr, "Public key: %s\n", r)
}
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)
fmt.Fprintf(out, "# public key: %s\n", r)
fmt.Fprintf(out, "%s\n", i)
}
func convert(in io.Reader, out io.Writer) {
ids, err := age.ParseIdentities(in)
if err != nil {
errorf("failed to parse input: %v", err)
}
if len(ids) == 0 {
errorf("no identities found in the input")
}
for _, id := range ids {
switch id := id.(type) {
case *age.X25519Identity:
fmt.Fprintf(out, "%s\n", id.Recipient())
case *age.HybridIdentity:
fmt.Fprintf(out, "%s\n", id.Recipient())
default:
errorf("internal error: unexpected identity type: %T", id)
}
}
}
func errorf(format string, v ...any) {
log.Printf("age-keygen: error: "+format, v...)
log.Fatalf("age-keygen: report unexpected or unhelpful errors at https://filippo.io/age/report")
}
func warning(msg string) {
log.Printf("age-keygen: warning: %s", msg)
}

View File

@@ -0,0 +1,214 @@
package main
import (
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"runtime/debug"
"strconv"
"strings"
"filippo.io/age"
"filippo.io/age/plugin"
)
const usage = `age-plugin-batchpass is an age plugin that enables non-interactive
passphrase-based encryption and decryption using environment variables.
WARNING: IN 90% OF CASES, YOU DON'T NEED THIS PLUGIN.
This functionality is not built into the age CLI because most applications
should use native keys instead of scripting passphrase-based encryption.
Humans are notoriously bad at remembering and generating strong passphrases.
age uses scrypt to partially mitigate this, which is necessarily very slow.
If a computer will be doing the remembering anyway, you can and should use
native keys instead. There is no need to manage separate public and private
keys, you encrypt directly to the private key:
$ age-keygen -o key.txt
$ age -e -i key.txt file.txt > file.txt.age
$ age -d -i key.txt file.txt.age > file.txt
Likewise, you can store a native identity string in an environment variable
or through your CI secrets manager and use it to encrypt and decrypt files
non-interactively:
$ export AGE_SECRET=$(age-keygen)
$ age -e -i <(echo "$AGE_SECRET") file.txt > file.txt.age
$ age -d -i <(echo "$AGE_SECRET") file.txt.age > file.txt
The age CLI also natively supports passphrase-encrypted identity files, so you
can use that functionality to non-interactively encrypt multiple files such that
you will be able to decrypt them later by entering the same passphrase:
$ age-keygen -pq | age -p -o encrypted-identity.txt
Public key: age1pq1cd[... 1950 more characters ...]
Enter passphrase (leave empty to autogenerate a secure one):
age: using autogenerated passphrase "eternal-erase-keen-suffer-fog-exclude-huge-scorpion-escape-scrub"
$ age -r age1pq1cd[... 1950 more characters ...] file.txt > file.txt.age
$ age -d -i encrypted-identity.txt file.txt.age > file.txt
Enter passphrase for identity file "encrypted-identity.txt":
Finally, when using this plugin care should be taken not to let the password be
persisted in the shell history or leaked to other users on multi-user systems.
Usage:
$ AGE_PASSPHRASE=password age -e -j batchpass file.txt > file.txt.age
$ AGE_PASSPHRASE=password age -d -j batchpass file.txt.age > file.txt
Alternatively, you can use AGE_PASSPHRASE_FD to read the passphrase from
a file descriptor. Trailing newlines are stripped from the file contents.
When encrypting, you can set AGE_PASSPHRASE_WORK_FACTOR to adjust the scrypt
work factor (between 1 and 30, default 18). Higher values are more secure
but slower.
When decrypting, you can set AGE_PASSPHRASE_MAX_WORK_FACTOR to limit the
maximum scrypt work factor accepted (between 1 and 30, default 30). This can
be used to avoid very slow decryptions.`
// Version can be set at link time to override debug.BuildInfo.Main.Version when
// building manually without git history. It should look like "v1.2.3".
var Version string
func main() {
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
p, err := plugin.New("batchpass")
if err != nil {
log.Fatal(err)
}
p.RegisterFlags(nil)
versionFlag := flag.Bool("version", false, "print the version")
flag.Parse()
if *versionFlag {
if buildInfo, ok := debug.ReadBuildInfo(); ok && Version == "" {
Version = buildInfo.Main.Version
}
fmt.Println(Version)
return
}
p.HandleIdentityAsRecipient(func(data []byte) (age.Recipient, error) {
if len(data) != 0 {
return nil, fmt.Errorf("batchpass identity does not take any payload")
}
pass, err := passphrase()
if err != nil {
return nil, err
}
r, err := age.NewScryptRecipient(pass)
if err != nil {
return nil, fmt.Errorf("failed to create scrypt recipient: %v", err)
}
if envWorkFactor := os.Getenv("AGE_PASSPHRASE_WORK_FACTOR"); envWorkFactor != "" {
workFactor, err := strconv.Atoi(envWorkFactor)
if err != nil {
return nil, fmt.Errorf("invalid AGE_PASSPHRASE_WORK_FACTOR: %v", err)
}
if workFactor > 30 || workFactor < 1 {
return nil, fmt.Errorf("AGE_PASSPHRASE_WORK_FACTOR must be between 1 and 30")
}
r.SetWorkFactor(workFactor)
}
return r, nil
})
p.HandleIdentity(func(data []byte) (age.Identity, error) {
if len(data) != 0 {
return nil, fmt.Errorf("batchpass identity does not take any payload")
}
pass, err := passphrase()
if err != nil {
return nil, err
}
maxWorkFactor := 0
if envMaxWorkFactor := os.Getenv("AGE_PASSPHRASE_MAX_WORK_FACTOR"); envMaxWorkFactor != "" {
maxWorkFactor, err = strconv.Atoi(envMaxWorkFactor)
if err != nil {
return nil, fmt.Errorf("invalid AGE_PASSPHRASE_MAX_WORK_FACTOR: %v", err)
}
if maxWorkFactor > 30 || maxWorkFactor < 1 {
return nil, fmt.Errorf("AGE_PASSPHRASE_MAX_WORK_FACTOR must be between 1 and 30")
}
}
return &batchpassIdentity{password: pass, maxWorkFactor: maxWorkFactor}, nil
})
os.Exit(p.Main())
}
type batchpassIdentity struct {
password string
maxWorkFactor int
}
func (i *batchpassIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
for _, s := range stanzas {
if s.Type == "scrypt" && len(stanzas) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
// Don't fallback to other identities, this plugin should mostly be used
// in isolation, from the CLI.
return nil, fmt.Errorf("file is not passphrase-encrypted")
}
ii, err := age.NewScryptIdentity(i.password)
if err != nil {
return nil, err
}
if i.maxWorkFactor != 0 {
ii.SetMaxWorkFactor(i.maxWorkFactor)
}
fileKey, err := ii.Unwrap(stanzas)
if errors.Is(err, age.ErrIncorrectIdentity) {
// ScryptIdentity returns ErrIncorrectIdentity to make it possible to
// try multiple passphrases from the API. If a user is invoking this
// plugin, it's safe to say they expect it to be the only mechanism to
// decrypt a passphrase-protected file.
return nil, fmt.Errorf("incorrect passphrase")
}
return fileKey, err
}
func passphrase() (string, error) {
envPASSPHRASE := os.Getenv("AGE_PASSPHRASE")
envFD := os.Getenv("AGE_PASSPHRASE_FD")
if envPASSPHRASE != "" && envFD != "" {
return "", fmt.Errorf("AGE_PASSPHRASE and AGE_PASSPHRASE_FD are mutually exclusive")
}
if envPASSPHRASE == "" && envFD == "" {
return "", fmt.Errorf("either AGE_PASSPHRASE or AGE_PASSPHRASE_FD must be set")
}
if envPASSPHRASE != "" {
return envPASSPHRASE, nil
}
fd, err := strconv.Atoi(envFD)
if err != nil {
return "", fmt.Errorf("invalid AGE_PASSPHRASE_FD: %v", err)
}
f := os.NewFile(uintptr(fd), "AGE_PASSPHRASE_FD")
if f == nil {
return "", fmt.Errorf("failed to open file descriptor %d", fd)
}
defer f.Close()
const maxPassphraseSize = 1024 * 1024 // 1 MiB
b, err := io.ReadAll(io.LimitReader(f, maxPassphraseSize+1))
if err != nil {
return "", fmt.Errorf("failed to read passphrase from fd %d: %v", fd, err)
}
if len(b) > maxPassphraseSize {
return "", fmt.Errorf("passphrase from fd %d is too long", fd)
}
return strings.TrimRight(string(b), "\r\n"), nil
}

View File

@@ -1,26 +1,76 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package main
import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"io"
_log "log"
"iter"
"os"
"path/filepath"
"regexp"
"runtime/debug"
"slices"
"strings"
"unicode"
"filippo.io/age"
"filippo.io/age/agessh"
"filippo.io/age/armor"
"golang.org/x/crypto/ssh/terminal"
"filippo.io/age/internal/term"
"filippo.io/age/plugin"
)
const usage = `Usage:
age [--encrypt] (-r RECIPIENT | -R PATH)... [--armor] [-o OUTPUT] [INPUT]
age [--encrypt] --passphrase [--armor] [-o OUTPUT] [INPUT]
age --decrypt [-i PATH]... [-o OUTPUT] [INPUT]
Options:
-e, --encrypt Encrypt the input to the output. Default if omitted.
-d, --decrypt Decrypt the input to the output.
-o, --output OUTPUT Write the result to the file at path OUTPUT.
-a, --armor Encrypt to a PEM encoded format.
-p, --passphrase Encrypt with a passphrase.
-r, --recipient RECIPIENT Encrypt to the specified RECIPIENT. Can be repeated.
-R, --recipients-file PATH Encrypt to recipients listed at PATH. Can be repeated.
-i, --identity PATH Use the identity file at PATH. Can be repeated.
INPUT defaults to standard input, and OUTPUT defaults to standard output.
If OUTPUT exists, it will be overwritten.
RECIPIENT can be an age public key generated by age-keygen ("age1...")
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
Recipient files contain one or more recipients, one per line. Empty lines
and lines starting with "#" are ignored as comments. "-" may be used to
read recipients from standard input.
Identity files contain one or more secret keys ("AGE-SECRET-KEY-1..."),
one per line, or an SSH key. Empty lines and lines starting with "#" are
ignored as comments. Passphrase encrypted age files can be used as
identity files. Multiple key files can be provided, and any unused ones
will be ignored. "-" may be used to read identities from standard input.
When --encrypt is specified explicitly, -i can also be used to encrypt to an
identity file symmetrically, instead or in addition to normal recipients.
Example:
$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
$ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age
$ age --decrypt -i key.txt -o data.tar.gz data.tar.gz.age`
// stdinInUse is used to ensure only one of input, recipients, or identities
// file is read from stdin. It's a singleton like os.Stdin.
var stdinInUse bool
type multiFlag []string
func (f *multiFlag) String() string { return fmt.Sprint(*f) }
@@ -30,45 +80,50 @@ func (f *multiFlag) Set(value string) error {
return nil
}
const usage = `Usage:
age -r RECIPIENT [-a] [-o OUTPUT] [INPUT]
age --decrypt [-i KEY] [-o OUTPUT] [INPUT]
type identityFlag struct {
Type, Value string
}
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.
-d, --decrypt Decrypt the input to the output.
-i, --identity KEY Use the private key file at path KEY. Can be repeated.
// identityFlags tracks -i and -j flags, preserving their relative order, so
// that "age -d -j agent -i encrypted-fallback-keys.age" behaves as expected.
type identityFlags []identityFlag
INPUT defaults to standard input, and OUTPUT defaults to standard output.
func (f *identityFlags) addIdentityFlag(value string) error {
*f = append(*f, identityFlag{Type: "i", Value: value})
return nil
}
RECIPIENT can be an age public key, as generated by age-keygen, ("age1...")
or an SSH public key ("ssh-ed25519 AAAA...", "ssh-rsa AAAA...").
func (f *identityFlags) addPluginFlag(value string) error {
*f = append(*f, identityFlag{Type: "j", Value: value})
return nil
}
KEY is a path to a file with age secret keys, one per line
(ignoring "#" prefixed comments and empty lines), or to an SSH key file.
Multiple keys can be provided, and any unused ones will be ignored.
Example:
$ age-keygen -o key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
$ tar cvz ~/data | age -r age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p > data.tar.gz.age
$ age -d -i key.txt -o data.tar.gz data.tar.gz.age`
// Version can be set at link time to override debug.BuildInfo.Main.Version when
// building manually without git history. It should look like "v1.2.3".
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, armorFlag, passFlag bool
recipientFlags, identityFlags multiFlag
decryptFlag, encryptFlag bool
passFlag, versionFlag, armorFlag bool
recipientFlags multiFlag
recipientsFileFlags multiFlag
identityFlags identityFlags
)
flag.BoolVar(&versionFlag, "version", false, "print the version")
flag.BoolVar(&decryptFlag, "d", false, "decrypt the input")
flag.BoolVar(&decryptFlag, "decrypt", false, "decrypt the input")
flag.BoolVar(&encryptFlag, "e", false, "encrypt the input")
flag.BoolVar(&encryptFlag, "encrypt", false, "encrypt the input")
flag.BoolVar(&passFlag, "p", false, "use a passphrase")
flag.BoolVar(&passFlag, "passphrase", false, "use a passphrase")
flag.StringVar(&outFlag, "o", "", "output to `FILE` (default stdout)")
@@ -77,108 +132,212 @@ func main() {
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(&identityFlags, "i", "identity (can be repeated)")
flag.Var(&identityFlags, "identity", "identity (can be repeated)")
flag.Var(&recipientsFileFlags, "R", "recipients file (can be repeated)")
flag.Var(&recipientsFileFlags, "recipients-file", "recipients file (can be repeated)")
flag.Func("i", "identity (can be repeated)", identityFlags.addIdentityFlag)
flag.Func("identity", "identity (can be repeated)", identityFlags.addIdentityFlag)
flag.Func("j", "data-less plugin (can be repeated)", identityFlags.addPluginFlag)
flag.Parse()
if flag.NArg() > 1 {
logFatalf("Error: too many arguments.\n" +
"age accepts a single optional argument for the input file.")
if versionFlag {
if buildInfo, ok := debug.ReadBuildInfo(); ok && Version == "" {
Version = buildInfo.Main.Version
}
fmt.Println(Version)
return
}
if flag.NArg() > 1 {
var hints []string
quotedArgs := strings.Trim(fmt.Sprintf("%q", flag.Args()), "[]")
// If the second argument looks like a flag, suggest moving the first
// argument to the back (as long as the arguments don't need quoting).
if strings.HasPrefix(flag.Arg(1), "-") {
hints = append(hints, "the input file must be specified after all flags")
safe := true
unsafeShell := regexp.MustCompile(`[^\w@%+=:,./-]`)
if slices.ContainsFunc(os.Args, unsafeShell.MatchString) {
safe = false
}
if safe {
i := len(os.Args) - flag.NArg()
newArgs := append([]string{}, os.Args[:i]...)
newArgs = append(newArgs, os.Args[i+1:]...)
newArgs = append(newArgs, os.Args[i])
hints = append(hints, "did you mean:")
hints = append(hints, " "+strings.Join(newArgs, " "))
}
} else {
hints = append(hints, "only a single input file may be specified at a time")
}
errorWithHint("too many INPUT arguments: "+quotedArgs, hints...)
}
switch {
case decryptFlag:
if encryptFlag {
errorf("-e/--encrypt can't be used with -d/--decrypt")
}
if armorFlag {
logFatalf("Error: -a/--armor can't be used with -d/--decrypt.\n" +
"Note that armored files are detected automatically.")
errorWithHint("-a/--armor can't be used with -d/--decrypt",
"note that armored files are detected automatically, try again without -a/--armor")
}
if passFlag {
logFatalf("Error: -p/--passphrase can't be used with -d/--decrypt.\n" +
"Note that password protected files are detected automatically.")
errorWithHint("-p/--passphrase can't be used with -d/--decrypt",
"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?")
errorWithHint("-r/--recipient can't be used with -d/--decrypt",
"did you mean to use -i/--identity to specify a private key?")
}
if len(recipientsFileFlags) > 0 {
errorWithHint("-R/--recipients-file can't be used with -d/--decrypt",
"did you mean to use -i/--identity to specify a private key?")
}
default: // encrypt
if len(identityFlags) > 0 {
logFatalf("Error: -i/--identity can't be used in encryption mode.\n" +
"Did you forget to specify -d/--decrypt?")
if len(identityFlags) > 0 && !encryptFlag {
errorWithHint("-i/--identity and -j can't be used in encryption mode unless symmetric encryption is explicitly selected with -e/--encrypt",
"did you forget to specify -d/--decrypt?")
}
if len(recipientFlags) == 0 && !passFlag {
logFatalf("Error: missing recipients.\n" +
"Did you forget to specify -r/--recipient or -p/--passphrase?")
if len(recipientFlags)+len(recipientsFileFlags)+len(identityFlags) == 0 && !passFlag {
errorWithHint("missing recipients",
"did you forget to specify -r/--recipient, -R/--recipients-file or -p/--passphrase?")
}
if len(recipientFlags) > 0 && passFlag {
logFatalf("Error: -p/--passphrase can't be combined with -r/--recipient.")
errorf("-p/--passphrase can't be combined with -r/--recipient")
}
if len(recipientsFileFlags) > 0 && passFlag {
errorf("-p/--passphrase can't be combined with -R/--recipients-file")
}
if len(identityFlags) > 0 && passFlag {
errorf("-p/--passphrase can't be combined with -i/--identity and -j")
}
}
var in, out io.ReadWriter = os.Stdin, os.Stdout
warnDuplicates(slices.Values(recipientFlags), "recipient")
warnDuplicates(slices.Values(recipientsFileFlags), "recipients file")
warnDuplicates(func(yield func(string) bool) {
for _, f := range identityFlags {
if f.Type == "i" && !yield(f.Value) {
return
}
}
}, "identity file")
var inUseFiles []string
for _, i := range identityFlags {
if i.Type != "i" {
continue
}
inUseFiles = append(inUseFiles, absPath(i.Value))
}
for _, f := range recipientsFileFlags {
inUseFiles = append(inUseFiles, absPath(f))
}
var in io.Reader = os.Stdin
var out io.Writer = os.Stdout
if name := flag.Arg(0); name != "" && name != "-" {
inUseFiles = append(inUseFiles, absPath(name))
f, err := os.Open(name)
if err != nil {
logFatalf("Error: failed to open input file %q: %v", name, err)
errorf("failed to open input file %q: %v", name, err)
}
defer f.Close()
in = f
} else {
stdinInUse = true
if decryptFlag && term.IsTerminal(os.Stdin) {
// If the input comes from a TTY, assume it's armored, and buffer up
// to the END line (or EOF/EOT) so that a password prompt or the
// output don't get in the way of typing the input. See Issue 364.
buf, err := bufferTerminalInput(in)
if err != nil {
errorf("failed to buffer terminal input: %v", err)
}
in = buf
}
}
if name := outFlag; name != "" && name != "-" {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
if err != nil {
logFatalf("Error: failed to open output file %q: %v", name, err)
for _, f := range inUseFiles {
if f == absPath(name) {
errorf("input and output file are the same: %q", name)
}
}
defer f.Close()
f := newLazyOpener(name)
defer func() {
if err := f.Close(); err != nil {
errorf("failed to close output file %q: %v", name, err)
}
}()
out = f
} else if terminal.IsTerminal(int(os.Stdout.Fd())) {
if armorFlag {
// If the output will go to a TTY, and it will be armored, 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) }()
} else if term.IsTerminal(os.Stdout) {
buf := &bytes.Buffer{}
defer func() {
if out == buf {
io.Copy(os.Stdout, buf)
}
}()
if name != "-" {
if decryptFlag {
// Buffer the output to check it's printable.
out = buf
defer func() {
if bytes.ContainsFunc(buf.Bytes(), func(r rune) bool {
return r != '\n' && r != '\r' && r != '\t' && unicode.IsControl(r)
}) {
errorWithHint("refusing to output binary to the terminal",
`force anyway with "-o -"`)
}
}()
} else if !armorFlag {
// If the output wouldn't be armored, refuse to send binary to
// the terminal unless explicitly requested with "-o -".
errorWithHint("refusing to output binary to the terminal",
"did you mean to use -a/--armor?",
`force anyway with "-o -"`)
}
}
if in == os.Stdin && term.IsTerminal(os.Stdin) {
// 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.
out = buf
} else if decryptFlag && name != "-" {
// TODO: buffer the output and check it's printable.
} else if name != "-" {
// 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 -".`)
}
}
switch {
case decryptFlag && len(identityFlags) == 0:
decryptPass(in, out)
case decryptFlag:
decrypt(identityFlags, in, out)
decryptNotPass(identityFlags, in, out)
case passFlag:
pass, err := passphrasePromptForEncryption()
if err != nil {
logFatalf("Error: %v", err)
}
encryptPass(pass, in, out, armorFlag)
encryptPass(in, out, armorFlag)
default:
encryptKeys(recipientFlags, in, out, armorFlag)
encryptNotPass(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()
pass, err := term.ReadSecret("Enter passphrase (leave empty to autogenerate a secure one):")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
p := string(pass)
if p == "" {
var words []string
for i := 0; i < 10; i++ {
for range 10 {
words = append(words, randomWord())
}
p = strings.Join(words, "-")
fmt.Fprintf(os.Stderr, "Using the autogenerated passphrase %q.\n", p)
err := printfToTerminal("using autogenerated passphrase %q", p)
if err != nil {
return "", fmt.Errorf("could not print passphrase: %v", err)
}
} else {
fmt.Fprintf(os.Stderr, "Confirm passphrase: ")
confirm, err := readPassphrase()
confirm, err := term.ReadSecret("Confirm passphrase:")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
@@ -189,93 +348,257 @@ func passphrasePromptForEncryption() (string, error) {
return p, nil
}
func encryptKeys(keys []string, in io.Reader, out io.Writer, armor bool) {
func encryptNotPass(recs, files []string, identities identityFlags, in io.Reader, out io.Writer, armor bool) {
var recipients []age.Recipient
for _, arg := range keys {
for _, arg := range recs {
r, err := parseRecipient(arg)
if err, ok := err.(gitHubRecipientError); ok {
errorWithHint(err.Error(), "instead, use recipient files like",
" curl -O https://github.com/"+err.username+".keys",
" age -R "+err.username+".keys")
}
if err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
recipients = append(recipients, r)
}
for _, name := range files {
recs, err := parseRecipientsFile(name)
if err != nil {
errorf("failed to parse recipient file %q: %v", name, err)
}
recipients = append(recipients, recs...)
}
for _, f := range identities {
switch f.Type {
case "i":
ids, err := parseIdentitiesFile(f.Value)
if err != nil {
errorf("reading %q: %v", f.Value, err)
}
r, err := identitiesToRecipients(ids)
if err != nil {
errorf("internal error processing %q: %v", f.Value, err)
}
recipients = append(recipients, r...)
case "j":
id, err := plugin.NewIdentityWithoutData(f.Value, plugin.NewTerminalUI(printf, warningf))
if err != nil {
errorf("initializing %q: %v", f.Value, err)
}
recipients = append(recipients, id.Recipient())
}
}
encrypt(recipients, in, out, armor)
}
func encryptPass(pass string, in io.Reader, out io.Writer, armor bool) {
func encryptPass(in io.Reader, out io.Writer, armor bool) {
pass, err := passphrasePromptForEncryption()
if err != nil {
errorf("%v", err)
}
r, err := age.NewScryptRecipient(pass)
if err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
testOnlyConfigureScryptIdentity(r)
encrypt([]age.Recipient{r}, in, out, armor)
}
var testOnlyConfigureScryptIdentity = func(*age.ScryptRecipient) {}
func encrypt(recipients []age.Recipient, in io.Reader, out io.Writer, withArmor bool) {
ageEncrypt := age.Encrypt
if withArmor {
a := armor.NewWriter(out)
defer func() {
if err := a.Close(); err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
}()
out = a
}
w, err := ageEncrypt(out, recipients...)
if err != nil {
logFatalf("Error: %v", err)
w, err := age.Encrypt(out, recipients...)
if e := new(plugin.NotFoundError); errors.As(err, &e) {
errorWithHint(err.Error(),
fmt.Sprintf("you might want to install the %q plugin", e.Name),
"visit https://age-encryption.org/awesome#plugins for a list of available plugins")
} else if err != nil {
errorf("%v", err)
}
if _, err := io.Copy(w, in); err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
if err := w.Close(); err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
}
func decrypt(keys []string, in io.Reader, out io.Writer) {
// crlfMangledIntro and utf16MangledIntro are the intro lines of the age format
// after mangling by various versions of PowerShell redirection, truncated to
// the length of the correct intro line. See issue 290.
const crlfMangledIntro = "age-encryption.org/v1" + "\r"
const utf16MangledIntro = "\xff\xfe" + "a\x00g\x00e\x00-\x00e\x00n\x00c\x00r\x00y\x00p\x00"
type rejectScryptIdentity struct{}
func (rejectScryptIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) {
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
return nil, age.ErrIncorrectIdentity
}
errorWithHint("file is passphrase-encrypted but identities were specified with -i/--identity or -j",
"remove all -i/--identity/-j flags to decrypt passphrase-encrypted files")
panic("unreachable")
}
func decryptNotPass(flags identityFlags, in io.Reader, out io.Writer) {
var identities []age.Identity
for _, f := range flags {
switch f.Type {
case "i":
ids, err := parseIdentitiesFile(f.Value)
if err != nil {
errorf("reading %q: %v", f.Value, err)
}
identities = append(identities, ids...)
case "j":
id, err := plugin.NewIdentityWithoutData(f.Value, plugin.NewTerminalUI(printf, warningf))
if err != nil {
errorf("initializing %q: %v", f.Value, err)
}
identities = append(identities, id)
}
}
identities = append(identities, rejectScryptIdentity{})
decrypt(identities, in, out)
}
func decryptPass(in io.Reader, out io.Writer) {
identities := []age.Identity{
// If there is an scrypt recipient (it will have to be the only one and)
// this identity will be invoked.
&LazyScryptIdentity{passphrasePrompt},
lazyScryptIdentity,
}
// TODO: use the default location if no arguments are provided:
// os.UserConfigDir()/age/keys.txt, ~/.ssh/id_rsa, ~/.ssh/id_ed25519
for _, name := range keys {
ids, err := parseIdentitiesFile(name)
if err != nil {
logFatalf("Error: %v", err)
}
identities = append(identities, ids...)
}
decrypt(identities, in, out)
}
func decrypt(identities []age.Identity, in io.Reader, out io.Writer) {
rr := bufio.NewReader(in)
if start, _ := rr.Peek(len(armor.Header)); string(start) == armor.Header {
if intro, _ := rr.Peek(len(crlfMangledIntro)); string(intro) == crlfMangledIntro ||
string(intro) == utf16MangledIntro {
errorWithHint("invalid header intro",
"it looks like this file was corrupted by PowerShell redirection",
"consider using -o or -a to encrypt files in PowerShell")
}
const maxWhitespace = 1024
start, _ := rr.Peek(maxWhitespace + len(armor.Header))
if strings.HasPrefix(string(bytes.TrimSpace(start)), armor.Header) {
in = armor.NewReader(rr)
} else {
in = rr
}
r, err := age.Decrypt(in, identities...)
if err != nil {
logFatalf("Error: %v", err)
if e := new(plugin.NotFoundError); errors.As(err, &e) {
errorWithHint(err.Error(),
fmt.Sprintf("you might want to install the %q plugin", e.Name),
"visit https://age-encryption.org/awesome#plugins for a list of available plugins")
} else if errors.As(err, new(*age.NoIdentityMatchError)) &&
len(identities) == 1 && identities[0] == lazyScryptIdentity {
errorWithHint("the file is not passphrase-encrypted, identities are required",
"specify identities with -i/--identity or -j to decrypt this file")
} else if err != nil {
errorf("%v", err)
}
out.Write(nil) // trigger the lazyOpener even if r is empty
if _, err := io.Copy(out, r); err != nil {
logFatalf("Error: %v", err)
errorf("%v", err)
}
}
func passphrasePrompt() (string, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase: ")
pass, err := readPassphrase()
var lazyScryptIdentity = &LazyScryptIdentity{passphrasePromptForDecryption}
func passphrasePromptForDecryption() (string, error) {
pass, err := term.ReadSecret("Enter passphrase:")
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
return string(pass), 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 ]")
func identitiesToRecipients(ids []age.Identity) ([]age.Recipient, error) {
var recipients []age.Recipient
for _, id := range ids {
switch id := id.(type) {
case *age.X25519Identity:
recipients = append(recipients, id.Recipient())
case *age.HybridIdentity:
recipients = append(recipients, id.Recipient())
case *plugin.Identity:
recipients = append(recipients, id.Recipient())
case *agessh.RSAIdentity:
recipients = append(recipients, id.Recipient())
case *agessh.Ed25519Identity:
recipients = append(recipients, id.Recipient())
case *agessh.EncryptedSSHIdentity:
recipients = append(recipients, id.Recipient())
case *EncryptedIdentity:
r, err := id.Recipients()
if err != nil {
return nil, err
}
recipients = append(recipients, r...)
default:
return nil, fmt.Errorf("unexpected identity type: %T", id)
}
}
return recipients, nil
}
type lazyOpener struct {
name string
f *os.File
err error
}
func newLazyOpener(name string) io.WriteCloser {
return &lazyOpener{name: name}
}
func (l *lazyOpener) Write(p []byte) (n int, err error) {
if l.f == nil && l.err == nil {
l.f, l.err = os.Create(l.name)
}
if l.err != nil {
return 0, l.err
}
return l.f.Write(p)
}
func (l *lazyOpener) Close() error {
if l.f != nil {
return l.f.Close()
}
return nil
}
func absPath(name string) string {
if abs, err := filepath.Abs(name); err == nil {
return abs
}
return name
}
func warnDuplicates(s iter.Seq[string], name string) {
seen := make(map[string]bool)
warned := make(map[string]bool)
for e := range s {
if seen[e] && !warned[e] {
warningf("duplicate %s %q", name, e)
warned[e] = true
}
seen[e] = true
}
}

View File

@@ -1,47 +1,77 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package main
import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"testing"
"filippo.io/age"
"filippo.io/age/plugin"
"github.com/rogpeppe/go-internal/testscript"
)
func TestVectors(t *testing.T) {
files, _ := filepath.Glob("testdata/*.age")
for _, f := range files {
name := strings.TrimSuffix(strings.TrimPrefix(f, "testdata/"), ".age")
t.Run(name, func(t *testing.T) {
identities, err := parseIdentitiesFile("testdata/" + name + "_key.txt")
if err != nil {
t.Fatal(err)
func TestMain(m *testing.M) {
testscript.Main(m, map[string]func(){
"age": func() {
testOnlyConfigureScryptIdentity = func(r *age.ScryptRecipient) {
r.SetWorkFactor(10)
}
for _, i := range identities {
t.Logf("%s", i.Type())
}
in, err := os.Open("testdata/" + name + ".age")
if err != nil {
t.Fatal(err)
}
r, err := age.Decrypt(in, identities...)
if err != nil {
t.Fatal(err)
}
out, err := ioutil.ReadAll(r)
if err != nil {
t.Fatal(err)
}
t.Logf("%s", out)
})
}
testOnlyFixedRandomWord = "four"
main()
},
"age-plugin-test": func() {
p, _ := plugin.New("test")
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
return testPlugin{}, nil
})
p.HandleIdentity(func(data []byte) (age.Identity, error) {
return testPlugin{}, nil
})
os.Exit(p.Main())
},
})
}
type testPlugin struct{}
func (testPlugin) Wrap(fileKey []byte) ([]*age.Stanza, error) {
return []*age.Stanza{{Type: "test", Body: fileKey}}, nil
}
func (testPlugin) Unwrap(ss []*age.Stanza) ([]byte, error) {
if len(ss) == 1 && ss[0].Type == "test" {
return ss[0].Body, nil
}
return nil, age.ErrIncorrectIdentity
}
var buildExtraCommands = sync.OnceValue(func() error {
bindir := filepath.SplitList(os.Getenv("PATH"))[0]
// Build age-keygen and age-plugin-pq into the test binary directory.
cmd := exec.Command("go", "build", "-o", bindir)
if testing.CoverMode() != "" {
cmd.Args = append(cmd.Args, "-cover")
}
cmd.Args = append(cmd.Args, "filippo.io/age/cmd/age-keygen")
cmd.Args = append(cmd.Args, "filippo.io/age/extra/age-plugin-pq")
cmd.Args = append(cmd.Args, "filippo.io/age/cmd/age-plugin-batchpass")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
})
func TestScript(t *testing.T) {
testscript.Run(t, testscript.Params{
Dir: "testdata",
Setup: func(e *testscript.Env) error {
return buildExtraCommands()
},
// TODO: enable AGEDEBUG=plugin without breaking stderr checks.
})
}

View File

@@ -1,30 +1,35 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package main
import (
"bytes"
"errors"
"fmt"
"os"
"filippo.io/age"
"golang.org/x/crypto/ssh/terminal"
)
// LazyScryptIdentity is an age.Identity that requests a passphrase only if it
// encounters an scrypt stanza. After obtaining a passphrase, it delegates to
// ScryptIdentity.
type LazyScryptIdentity struct {
Passphrase func() (string, error)
}
var _ age.Identity = &LazyScryptIdentity{}
func (i *LazyScryptIdentity) Type() string {
return "scrypt"
}
func (i *LazyScryptIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err error) {
func (i *LazyScryptIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
for _, s := range stanzas {
if s.Type == "scrypt" && len(stanzas) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
if len(stanzas) != 1 || stanzas[0].Type != "scrypt" {
return nil, age.ErrIncorrectIdentity
}
pass, err := i.Passphrase()
if err != nil {
return nil, fmt.Errorf("could not read passphrase: %v", err)
@@ -33,34 +38,67 @@ func (i *LazyScryptIdentity) Unwrap(block *age.Stanza) (fileKey []byte, err erro
if err != nil {
return nil, err
}
fileKey, err = ii.Unwrap(block)
if err == age.ErrIncorrectIdentity {
// The API will just ignore the identity if the passphrase is wrong, and
// move on, eventually returning "no identity matched a recipient".
// Since we only supply one identity from the CLI, make it a fatal
// error with a better message.
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
}
// stdinInUse is set in main. It's a singleton like os.Stdin.
var stdinInUse bool
type EncryptedIdentity struct {
Contents []byte
Passphrase func() (string, error)
NoMatchWarning func()
func readPassphrase() ([]byte, error) {
fd := int(os.Stdin.Fd())
if !terminal.IsTerminal(fd) || stdinInUse {
tty, err := os.Open("/dev/tty")
if err != nil {
return nil, fmt.Errorf("standard input is not available or 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
identities []age.Identity
}
var _ age.Identity = &EncryptedIdentity{}
func (i *EncryptedIdentity) Recipients() ([]age.Recipient, error) {
if i.identities == nil {
if err := i.decrypt(); err != nil {
return nil, err
}
}
return identitiesToRecipients(i.identities)
}
func (i *EncryptedIdentity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
if i.identities == nil {
if err := i.decrypt(); err != nil {
return nil, err
}
}
for _, id := range i.identities {
fileKey, err = id.Unwrap(stanzas)
if errors.Is(err, age.ErrIncorrectIdentity) {
continue
}
if err != nil {
return nil, err
}
return fileKey, nil
}
i.NoMatchWarning()
return nil, age.ErrIncorrectIdentity
}
func (i *EncryptedIdentity) decrypt() error {
d, err := age.Decrypt(bytes.NewReader(i.Contents), &LazyScryptIdentity{i.Passphrase})
if e := new(age.NoIdentityMatchError); errors.As(err, &e) {
return fmt.Errorf("identity file is encrypted with age but not with a passphrase")
}
if err != nil {
return fmt.Errorf("failed to decrypt identity file: %v", err)
}
i.identities, err = parseIdentities(d)
return err
}

View File

@@ -1,83 +1,256 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package main
import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"unicode/utf8"
"filippo.io/age"
"filippo.io/age/agessh"
"filippo.io/age/armor"
"filippo.io/age/internal/term"
"filippo.io/age/plugin"
"filippo.io/age/tag"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/ssh"
)
type gitHubRecipientError struct {
username string
}
func (gitHubRecipientError) Error() string {
return `"github:" recipients were removed from the design`
}
func parseRecipient(arg string) (age.Recipient, error) {
switch {
case strings.HasPrefix(arg, "age1tag1") || strings.HasPrefix(arg, "age1tagpq1"):
return tag.ParseRecipient(arg)
case strings.HasPrefix(arg, "age1pq1"):
return age.ParseHybridRecipient(arg)
case strings.HasPrefix(arg, "age1") && strings.Count(arg, "1") > 1:
return plugin.NewRecipient(arg, plugin.NewTerminalUI(printf, warningf))
case strings.HasPrefix(arg, "age1"):
return age.ParseX25519Recipient(arg)
case strings.HasPrefix(arg, "ssh-"):
return agessh.ParseRecipient(arg)
case strings.HasPrefix(arg, "github:"):
name := strings.TrimPrefix(arg, "github:")
return nil, gitHubRecipientError{name}
}
return nil, fmt.Errorf("unknown recipient type: %q", arg)
}
const privateKeySizeLimit = 1 << 24 // 16 MiB
func parseIdentitiesFile(name string) ([]age.Identity, error) {
f, err := os.Open(name)
if err != nil {
return nil, fmt.Errorf("failed to open file: %v", err)
}
defer f.Close()
contents, err := ioutil.ReadAll(io.LimitReader(f, 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)
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()
}
var ids []age.Identity
var ageParsingError error
scanner := bufio.NewScanner(bytes.NewReader(contents))
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 strings.HasPrefix(line, "-----BEGIN") {
return parseSSHIdentity(name, contents)
if !utf8.ValidString(line) {
return nil, fmt.Errorf("%q: recipients file is not valid UTF-8", name)
}
if ageParsingError != nil {
continue
if len(line) > lineLengthLimit {
return nil, fmt.Errorf("%q: line %d is too long", name, n)
}
i, err := age.ParseX25519Identity(line)
r, err := parseRecipient(line)
if err != nil {
ageParsingError = fmt.Errorf("malformed secret keys file %q: %v", name, err)
if t, ok := sshKeyType(line); ok {
// Skip unsupported but valid SSH public keys with a warning.
warningf("recipients file %q: ignoring unsupported SSH key of type %q at line %d", name, t, n)
continue
}
if strings.HasPrefix(line, "AGE-") {
return nil, fmt.Errorf("%q: error at line %d: apparent identity found in recipients file", name, n)
}
// Hide the error since it might unintentionally leak the contents
// of confidential files.
return nil, fmt.Errorf("%q: malformed recipient at line %d", name, n)
}
recs = append(recs, r)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("%q: failed to read recipients file: %v", name, err)
}
if len(recs) == 0 {
return nil, fmt.Errorf("%q: no recipients found", name)
}
return recs, nil
}
func sshKeyType(s string) (string, bool) {
// TODO: also ignore options? And maybe support multiple spaces and tabs as
// field separators like OpenSSH?
fields := strings.Split(s, " ")
if len(fields) < 2 {
return "", false
}
key, err := base64.StdEncoding.DecodeString(fields[1])
if err != nil {
return "", false
}
k := cryptobyte.String(key)
var typeLen uint32
var typeBytes []byte
if !k.ReadUint32(&typeLen) || !k.ReadBytes(&typeBytes, int(typeLen)) {
return "", false
}
if t := fields[0]; t == string(typeBytes) {
return t, true
}
return "", false
}
// parseIdentitiesFile parses a file that contains age or SSH keys. It returns
// one or more of *[age.X25519Identity], *[age.HybridIdentity],
// *[agessh.RSAIdentity], *[agessh.Ed25519Identity],
// *[agessh.EncryptedSSHIdentity], or *[EncryptedIdentity].
func parseIdentitiesFile(name string) ([]age.Identity, error) {
var f *os.File
if name == "-" {
if stdinInUse {
return nil, fmt.Errorf("standard input is used for multiple purposes")
}
stdinInUse = true
f = os.Stdin
} else {
var err error
f, err = os.Open(name)
if err != nil {
return nil, fmt.Errorf("failed to open file: %v", err)
}
defer f.Close()
}
b := bufio.NewReader(f)
p, _ := b.Peek(14) // length of "age-encryption" and "-----BEGIN AGE"
peeked := string(p)
switch {
// An age encrypted file, plain or armored.
case peeked == "age-encryption" || peeked == "-----BEGIN AGE":
var r io.Reader = b
if peeked == "-----BEGIN AGE" {
r = armor.NewReader(r)
}
const privateKeySizeLimit = 1 << 24 // 16 MiB
contents, err := io.ReadAll(io.LimitReader(r, privateKeySizeLimit))
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
if len(contents) == privateKeySizeLimit {
return nil, fmt.Errorf("failed to read %q: file too long", name)
}
return []age.Identity{&EncryptedIdentity{
Contents: contents,
Passphrase: func() (string, error) {
pass, err := term.ReadSecret(fmt.Sprintf("Enter passphrase for identity file %q:", name))
if err != nil {
return "", fmt.Errorf("could not read passphrase: %v", err)
}
return string(pass), nil
},
NoMatchWarning: func() {
warningf("encrypted identity file %q didn't match file's recipients", name)
},
}}, nil
// Another PEM file, possibly an SSH private key.
case strings.HasPrefix(peeked, "-----BEGIN"):
const privateKeySizeLimit = 1 << 14 // 16 KiB
contents, err := io.ReadAll(io.LimitReader(b, privateKeySizeLimit))
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
if len(contents) == privateKeySizeLimit {
return nil, fmt.Errorf("failed to read %q: file too long", name)
}
return parseSSHIdentity(name, contents)
// An unencrypted age identity file.
default:
ids, err := parseIdentities(b)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name, err)
}
return ids, nil
}
}
func parseIdentity(s string) (age.Identity, error) {
switch {
case strings.HasPrefix(s, "AGE-PLUGIN-"):
return plugin.NewIdentity(s, plugin.NewTerminalUI(printf, warningf))
case strings.HasPrefix(s, "AGE-SECRET-KEY-1"):
return age.ParseX25519Identity(s)
case strings.HasPrefix(s, "AGE-SECRET-KEY-PQ-1"):
return age.ParseHybridIdentity(s)
default:
return nil, fmt.Errorf("unknown identity type")
}
}
// parseIdentities is like [age.ParseIdentities], but supports plugin identities.
func parseIdentities(f io.Reader) ([]age.Identity, error) {
const privateKeySizeLimit = 1 << 24 // 16 MiB
var ids []age.Identity
scanner := bufio.NewScanner(io.LimitReader(f, privateKeySizeLimit))
var n int
for scanner.Scan() {
n++
line := scanner.Text()
if strings.HasPrefix(line, "#") || line == "" {
continue
}
if !utf8.ValidString(line) {
return nil, fmt.Errorf("identities file is not valid UTF-8")
}
i, err := parseIdentity(line)
if err != nil {
if strings.HasPrefix(line, "age1") {
return nil, fmt.Errorf("error at line %d: apparent recipient found in identities file", n)
}
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 %q: %v", name, err)
return nil, fmt.Errorf("failed to read identities file: %v", err)
}
if ageParsingError != nil {
return nil, ageParsingError
}
if len(ids) == 0 {
return nil, fmt.Errorf("no secret keys found in %q", name)
return nil, fmt.Errorf("no identities found")
}
return ids, nil
}
@@ -93,8 +266,7 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
}
}
passphrasePrompt := func() ([]byte, error) {
fmt.Fprintf(os.Stderr, "Enter passphrase for %q: ", name)
pass, err := readPassphrase()
pass, err := term.ReadSecret(fmt.Sprintf("Enter passphrase for %q:", name))
if err != nil {
return nil, fmt.Errorf("could not read passphrase for %q: %v", name, err)
}
@@ -114,14 +286,19 @@ func parseSSHIdentity(name string, pemBytes []byte) ([]age.Identity, error) {
}
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)
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)
contents, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("failed to read %q: %v", name+".pub", err)
}

21
cmd/age/testdata/armor.txt vendored Normal file
View File

@@ -0,0 +1,21 @@
age -d -i key.txt armored_with_leading_and_trailing_whitespace.txt
stdout test
-- key.txt --
# created: 2025-12-23T22:21:12+01:00
# public key: age15w9kgvgggmfra4sz6vk39kz4mveuq2sfv5vmcu090y0k2sluepaqv7z2fv
AGE-SECRET-KEY-18J6FVYJE2AFSJ0RPH6M29GMUU62UVRSCNWUJZSGETH6R38Q5AZ3S2DHAZ9
-- armored_with_leading_and_trailing_whitespace.txt --
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5ODhFNHR6RVg0SGVHZFBM
clBEclEzZ3NvOGhqVE9tcFZnbTc2c3R5a0Q4ClZjVzBLNjdxRElZV3E0Z3ZpZ255
T3JWTFBHRFA2cytpWWtkeU45dDRadmcKLS0tIHV3L3hOVmJjL0hMRXBQa05lMlRs
ZW45TndPeE9GcmRNeWFkR3YxeHg0YzQKJBp6KRlFFUE8jbAQUBlcAwaaQcPAflJD
pWGoOjYP33gTxJHNPg==
-----END AGE ENCRYPTED FILE-----

54
cmd/age/testdata/batchpass.txt vendored Normal file
View File

@@ -0,0 +1,54 @@
# encrypt and decrypt with AGE_PASSPHRASE
env AGE_PASSPHRASE_WORK_FACTOR=5
env AGE_PASSPHRASE=password
age -e -j batchpass -o test.age input
age -d -j batchpass test.age
cmp stdout input
# decrypt with AGE_PASSPHRASE_MAX_WORK_FACTOR
env AGE_PASSPHRASE_MAX_WORK_FACTOR=10
age -d -j batchpass test.age
cmp stdout input
# AGE_PASSPHRASE_MAX_WORK_FACTOR lower than work factor
env AGE_PASSPHRASE_MAX_WORK_FACTOR=3
! age -d -j batchpass test.age
stderr 'work factor'
env AGE_PASSPHRASE_MAX_WORK_FACTOR=
# error: both AGE_PASSPHRASE and AGE_PASSPHRASE_FD set
env AGE_PASSPHRASE=password
env AGE_PASSPHRASE_FD=3
! age -e -j batchpass -a input
stderr 'mutually exclusive'
# error: neither AGE_PASSPHRASE nor AGE_PASSPHRASE_FD set
env AGE_PASSPHRASE=
env AGE_PASSPHRASE_FD=
! age -e -j batchpass -a test.age
stderr 'must be set'
# error: incorrect passphrase
env AGE_PASSPHRASE=wrongpassword
! age -d -j batchpass test.age
stderr 'incorrect passphrase'
# error: encrypting to other recipients along with passphrase
env AGE_PASSPHRASE=password
! age -e -j batchpass -r age1gc2zwslg9j25pg9e9ld7623aewc2gjzquzgq90m75r75myqdt5cq3yudvh -a input
stderr 'incompatible recipients'
! age -e -r age1gc2zwslg9j25pg9e9ld7623aewc2gjzquzgq90m75r75myqdt5cq3yudvh -j batchpass -a input
stderr 'incompatible recipients'
# decrypt with native scrypt
[!linux] [!darwin] skip # no pty support
[darwin] [go1.20] skip # https://go.dev/issue/61779
ttyin terminal
age -d test.age
cmp stdout input
-- terminal --
password
password
-- input --
test

49
cmd/age/testdata/duplicates.txt vendored Normal file
View File

@@ -0,0 +1,49 @@
# Test duplicate recipient detection
age -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -r age1yv2sxrjnyymx4qkn3ehdd5zdq4xnrea5ljm4hsmgxn5tpv985anqkrwqa7 -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -o test.age input
stderr 'warning: duplicate recipient "age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85"'
# Test duplicates separated by different argument
age -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -a -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -o test2.age input
stderr 'warning: duplicate recipient "age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85"'
# Test duplicate recipients file detection
age -R recipients1.txt -R recipients2.txt -R recipients1.txt -o test3.age input
stderr 'warning: duplicate recipients file "recipients1.txt"'
# Test duplicates separated by output flag
age -R recipients1.txt -o test4.age -R recipients1.txt input
stderr 'warning: duplicate recipients file "recipients1.txt"'
# First create an encrypted file for decrypt tests
age -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -o encrypted.age input
# Test duplicate identity file detection (decrypt mode)
age -d -i key1.txt -i key2.txt -i key1.txt encrypted.age
stderr 'warning: duplicate identity file "key1.txt"'
# Test duplicates separated by different argument in decrypt mode
age -d -i key1.txt -o test.out -i key1.txt encrypted.age
stderr 'warning: duplicate identity file "key1.txt"'
# Test no warning when no duplicates
age -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -r age1yv2sxrjnyymx4qkn3ehdd5zdq4xnrea5ljm4hsmgxn5tpv985anqkrwqa7 -o test5.age input
! stderr 'warning: duplicate'
# Test multiple duplicates (same value repeated 3+ times)
age -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -r age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85 -o test6.age input
stderr 'warning: duplicate recipient "age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85"'
-- input --
test data
-- recipients1.txt --
age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85
-- recipients2.txt --
age1yv2sxrjnyymx4qkn3ehdd5zdq4xnrea5ljm4hsmgxn5tpv985anqkrwqa7
-- key1.txt --
# created: 2025-12-22T22:06:22+01:00
# public key: age1jh0u9yam5jhql7vy4acz90p0rhqcr0rcyu4zm85s9sdcwrs7rfgshtdg85
AGE-SECRET-KEY-1WRM2S8SP3XSKLLXAXS489EXZNKCKRZWYQLQ8D2NRNQWCVAPSMA9SC5JWZQ
-- key2.txt --
# created: 2025-12-22T22:06:27+01:00
# public key: age1yv2sxrjnyymx4qkn3ehdd5zdq4xnrea5ljm4hsmgxn5tpv985anqkrwqa7
AGE-SECRET-KEY-1WZ3MRPAWEWR4DG474H460MXX7J2T0TEYNJ0SKQDMKP02JU7UJ9UQFGLZCE

32
cmd/age/testdata/ed25519.txt vendored Normal file
View File

@@ -0,0 +1,32 @@
# encrypt and decrypt a file with -R
age -R key.pem.pub -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with -i
age -e -i key.pem -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with the wrong key
age -R otherkey.pem.pub -o test.age input
! age -d -i key.pem test.age
stderr 'no identity matched any of the recipients'
! stdout .
-- input --
test
-- key.pem --
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2gAAAKDgV/GC4Ffx
ggAAAAtzc2gtZWQyNTUxOQAAACB/aTuac9tiWRGrKEtixFlryYlGCPTOpdbmXN9RRmDF2g
AAAECvFoQXQzXgJLQ+Gz4PfEcfyZwC2gUjOiWTD//mTPyD8H9pO5pz22JZEasoS2LEWWvJ
iUYI9M6l1uZc31FGYMXaAAAAG2ZpbGlwcG9AQmlzdHJvbWF0aC1NMS5sb2NhbAEC
-----END OPENSSH PRIVATE KEY-----
-- key.pem.pub --
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEasoS2LEWWvJiUYI9M6l1uZc31FGYMXa
-- otherkey.pem.pub --
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJFlMdZUMrWjJ3hh60MLALXSqUdAjBo/qEMJzvpekpoM

View File

@@ -1,6 +0,0 @@
age-encryption.org/v1
-> ssh-ed25519 o1Hudg SZISkI5Qn8YgUBmTKG/Zp/QpFjXWvAivzvB+hOcN5W8
dYfwGWYvCwpSU5EXIC1XqfXdsBvCi3kMypdqCVShrpk
-> joint-oil-hw
--- gC/27VAgqOEzAQMKHvBjih7sJ1oDKht+HNdguTIbjt8
fëtAeµÖ¨&8{Ëéðνcat—íΘœ¯šË·}«=šC†Ÿu

View File

@@ -1,7 +0,0 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACAwKgrb/LkvtI887QylSoUh5xUlKr1fb37euR6et5jHowAAAJgxqUx+MalM
fgAAAAtzc2gtZWQyNTUxOQAAACAwKgrb/LkvtI887QylSoUh5xUlKr1fb37euR6et5jHow
AAAEC7gKj74YIwaM1BT2tnODjfeZJvo8lcazvL6Uljv3+nIDAqCtv8uS+0jzztDKVKhSHn
FSUqvV9vft65Hp63mMejAAAADnJ1bm5lckBmdi1hejMyAQIDBAUGBw==
-----END OPENSSH PRIVATE KEY-----

126
cmd/age/testdata/encrypted_keys.txt vendored Normal file
View File

@@ -0,0 +1,126 @@
# TODO: age-encrypted private keys, multiple identities, -i ordering, -e -i,
# age file password prompt during encryption
[!linux] [!darwin] skip # no pty support
[darwin] [go1.20] skip # https://go.dev/issue/61779
# use an encrypted OpenSSH private key without .pub file
age -R key_ed25519.pub -o ed25519.age input
rm key_ed25519.pub
ttyin terminal
age -d -i key_ed25519 ed25519.age
cmp stdout input
! stderr .
# -e -i with an encrypted OpenSSH private key
age -e -i key_ed25519 -o ed25519.age input
ttyin terminal
age -d -i key_ed25519 ed25519.age
cmp stdout input
# a file encrypted to the wrong key does not ask for the password
age -R key_ed25519_other.pub -o ed25519_other.age input
! age -d -i key_ed25519 ed25519_other.age
stderr 'no identity matched any of the recipients'
# use an encrypted legacy PEM private key with a .pub file
age -R key_rsa_legacy.pub -o rsa_legacy.age input
ttyin terminal
age -d -i key_rsa_legacy rsa_legacy.age
cmp stdout input
! stderr .
age -R key_rsa_other.pub -o rsa_other.age input
! age -d -i key_rsa_legacy rsa_other.age
stderr 'no identity matched any of the recipients'
# -e -i with an encrypted legacy PEM private key
age -e -i key_rsa_legacy -o rsa_legacy.age input
ttyin terminal
age -d -i key_rsa_legacy rsa_legacy.age
cmp stdout input
# legacy PEM private key without a .pub file causes an error
rm key_rsa_legacy.pub
! age -d -i key_rsa_legacy rsa_legacy.age
stderr 'key_rsa_legacy.pub'
# mismatched .pub file causes an error
cp key_rsa_legacy key_rsa_other
ttyin terminal
! age -d -i key_rsa_other rsa_other.age
stderr 'mismatched private and public SSH key'
# buffer armored ciphertext before prompting if stdin is the terminal
ttyin terminal
age -e -i key_ed25519 -a -o test.age input
exec cat test.age terminal # concatenated ciphertext + password
ttyin -stdin stdout
age -d -i key_ed25519
ttyout 'Enter passphrase'
! stderr .
cmp stdout input
-- input --
test
-- terminal --
password
-- key_ed25519 --
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCuvb97i7
U6Dz4+4SaF3kK1AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIKaVctg4/hmFbfof
Tv+yrC2IweO/Dd2AVDijFpaMO9fmAAAAoMO7yEnisRmzFdiExNt3XTYuLdP9m3jgOCroiF
TtBhh1lAB2qggzWExMRP3Ak8+AloXEcWiACwBYnqwxhQMh0RDCDKC/H/4SXO+ds4HFWil+
4bGF9wYZFU7IEjIK91CPGJ6YoWPn9dSdEjjbuCJtOMwHsysGyw5n/qSFPmSAPmA4YL2OzM
WFOJ5gB5o1LKZkDTcdt7kPziIoVd5QkqpnYsE=
-----END OPENSSH PRIVATE KEY-----
-- key_ed25519.pub --
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKaVctg4/hmFbfofTv+yrC2IweO/Dd2AVDijFpaMO9fm
-- key_ed25519_other.pub --
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINbTd+xfSBYKR/1Hp7FsoxwQAdIOk1Khye6ALBj7e1CV
-- key_rsa_legacy --
-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: AES-128-CBC,8045E7CF19D7794F4ADF5AC63179D985
OESHhWCho337W1Ajg+iMbsZx/FPtHM3YPHu/d1U51ERIUh0wVof2SK0ooENokr6g
O3fcv9Xga+Na4Ez+gsFRsIZOdqrJq+QBH0CAKi+Mz4KsU7teAobUBJgRB31Wt7eI
39KGZeaBJLMQ0FzQkDx5MCOg98iu9rt+Pg1bH8X88wV4vOv+tG4nmqgdpDmouo1Q
uW1TJxrdPhkINjaPZZ7gvjS8wuG9+qwQY76I0hGun9secf4VZDysqUnUp8UHYovR
dbvKCbglQy18mGL4kREJ/hH/9/maefS+pTMb2UX0onp9j7l3yNSvL4A4xW85ii6x
liVMnZvLvbfPtI7jjZtC8CjshRkZke4fSZF2nZP7zK2qVcqDFCtemaks+0i2ksel
D8clUKhBmq23VNAt+iy1stwHBporuaE6kEVJail5WPpgdfQjifpaMbTsZgOK+vGL
GKi8vSJWfMU3lTf/N++ks2FWxdq0TgQirsKsQ5mWobfxc1XehvvdJj8hUtArrP32
d4ge5DXPpmtkCzrc1+wt8Py/ANl9jV6c+4fCbpQ2snyzdFEhFtXHCEpMguN9MhKI
gaZIfAxvYcQr8Gwew/IB02Phda9tvDiedHvyHGJmSy/87fR6ECh47VDFL/UYu4jG
0hRtAZMMddGNfoosnO6OKBd09cgvXKCsUrbpAI7dF5TP5ItDkVb08hW446jBdgS9
7QqB0rPmlAjsJi7fsrDw7Nq9pOdqqCEwUMc9Lztnv66aX1d/Y2vQm9mrsDbyZKqn
g621rg7E4UHf7EGiDblfS234+TsNvwZ6sEbivU+3zqglPiOF71m6D0cKgaUZPOua
GNdyQz5e73hYa+NJ76IZ+IqkoJAFXBkd1nWcN6DUBYiKvqd4qO9xD+JvNtiFlQ9d
pyO9t4FTGvySh8CKyEUEdtj+2ftCIuZaUD2L5YJU1tlQV0EH42InOmkmphbHkW5v
lNAoZAny1Z0P6O0gn7xtVrgd7paVQfDCJtkvsm5zR6Yei5FUgY/9NPaRotzuZVAY
EfQC7JPdSdb5yusnXh7B9jGkgxhMIb6EPFFjIZ4iaV1RVgINSisGMSFzlqOz702b
Cawsr9nD438cjzMNYEmrihZZBjHon6hHrLmM9Aj2xgprsoNLP1jJQ6WpZDlrYsj0
XS0tSJmh0pM4Ey6j1VWNoaOxVseYLW7J9wGVfH/HJAc2k6Wg46P2e8lMT6Sj4YsT
EguDhUjXrgePC53ohcSF+I6x35Q1D6ttMnc3ODzmIcCisxAvWdAqi1yRlnBotRwg
S2vq3HU0yJFG8pJqw4vU9A9DlaMMT+ejEH+9xVwAWM+7n2lJcgthtWuShZCE6BB+
jVobSlTMArzQj4klTSbew1m9Waa6kKDezsAY66mryVNofCCeYDOBRecCm5JyMnWf
WBVnNx+kZ/YyvYeBcSh34u8rkjqGpzfM/oPE7GwIoZvbAirjLohL7u8oq2bfAYG0
/xIPwPJw1O3o5PHeu84bVIRqcKzGeaVL+5aUiZP9uNGUpqJWA5q2Sa5BOXV46yqO
DIS8q7uPCSbt5mPXPDGJ1CupCdA1stUf2kb0cDJ+LpUbPND9SebBlxSuR1D/YGqv
wlzfN5Usv/h/XNl98bYtpY8/skKPecyx3wG3VtwWH/5XVhvHz4TENjlKv/L2pbUC
Dv83WcL1N/i+jerYxDRmGe3NQOvyW4JaNzzjgb74T7rE1/3lf6qkmUHjxfo4VZAF
L/q2782OUs5Qt4/pYAIISzLdBw6XtTjZHirqa6YNrFvGucB3NG49AC0b1Z0acfrS
iimC2TvZpwunlLbyz2SQQL2c1zQ3U/Yfh2F1Zt8o6kK3RgKSSx57rK6nV7hXMGGp
C4HV3nLetZg8HexicqeRANLXuUDbCSpN8K4nW5G2g/yKPfsQHBV/RWEDfhndykja
+SmoY5IB+2zEbCC3MWiP9ZdIcCYOsq8wDZESMMW40DlVICjrf6UOqQ+ogci20qLS
CmpgmOPAaBZJG/sBU79eHUSjPCK6yDpSyc30oVn8FnoBTmOpt7R++Ub8RJxReXBt
+6o0NXYCJNaeVnk1bE4iavkJrXJCZvu44VBLS0WUs9W8TD4Iq8kNHsfQsfOuBXnQ
ncgoIe9HppnMGNoSzjYBNL/rprlbaOE55TkPqiQsiskRcaoeY53aTxoIykHmoj8G
wJo/00IR+NYir7tr03Vriw+uywPPGucVJGWTUGsNbHlS5j941IltflIf6FitElNr
JxVuJLgYiP3JhmWpdqA/uidYJMbIjunpn/8rVLrAil04SCSfUmaCdl7dkQ9x+3Mf
Erm699vIBQwvv0i+mcwKEvqSrhhNQ2F7vrb7NL8I2wUEPgQbv1PxSV6X0aYcxYVI
-----END RSA PRIVATE KEY-----
-- key_rsa_legacy.pub --
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCky7Clp8I3LVoqZWtat+QR6KmM0evFilmFhwenINIBbb8eS3ftDSkQy2YRrlAvO3h4EZffOIxANGL/yKVlRCIzvjsphi+tTHscZsQhwMnLEmxEayTq20hZKcwNA8TQdh2TW/w0KZmNZcxlTn4IK8W16komHcoH/qrRiXq8z3ROcfnv3Q4Hll9MUCwBkfy2DdBpWUMidQ1dAK4i3vXdseF74hJ0jFbPtS5mlpOsJZa0sdH1dnEl5M8wZS3PxyzM6JMkgzG7INp4sO/xGIisjl/QuSh2Fu93/EogdGXxIZChniUfzBx1DaHlerPPNSMP+uLbaOIAQrIPozhfdUdsCFDMoB7/PA6g1WVYZWAqjBZZW/GMOzPhih57NIFBSyMTzMi1KS6OBvYJvPf4IcvOa3May9ylLG/wZVhrHlQPbSsbRrraVtJ1P4gGQJ5U4d2AD2q+XtMb5f2i/holMXTVQl7Fa7RYi1TblDuW5OZCvmIawePBXAYbPg0OVFs3vAVEuAM=
-- key_rsa_other.pub --
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQiCWw2W++gX4wcwpDo6QIouwQ9PPwCVe7QPICzxztG27mzeKRM4xT2LURGSaQqg7OYIUTGrLqNsaLZW+FHHQlRAVv1LEbdEFa5JermBMJ5j/HxamE/7oV60gMRlgKW+4IZhVMPgRZaaXU0YPb9oACdMNM8kPkc5JaOJ8iO6B1RViybjLD+tsEEPXLp3Mrj+sJqs+IvNlJKXdeefOjNrGmLHKIFdHiWlZ+aAW+QLfMQiNXoTbGybFUSpNEbmK/1ITiRAly94NoUK9LoriueXR+WJIm9wP4SfHw+hMBz1cywdF2wwKmWWegizV/USEmhyNXUzHZzjbkgE84DrIq+NA7SUmw6C8ClMjdnRnnoIyga99yMIrYMny1KW/bk1NK4u6Tv17E+FFOS3vf2Gcj01/jOmAUIQwL8MjAHhnsZ4XAA5NHa2NRGWm+hw7fx5uX42Gyz8HidFda5Lij1pASBcx4U3qwb62X+IVN50jGIP6kRNmGtMLY1JgaoGDDkw9r6mU=

47
cmd/age/testdata/hybrid.txt vendored Normal file
View File

@@ -0,0 +1,47 @@
# encrypt and decrypt a file with -r
age -r age1pq1044e3hfmq2tltgck54m0gnpv8rfrwkdxjgl38psnwg8zfyv4uqdu63kk0u94ztt0q35k9dzuwgv405kxq6due2sre2kfsjftf7k2vz2mdpetku9pufy9d6ny6k5ss0cghpxdxrux0yd5xy40f8fu8ch9xn2rczyek2pqetqk0l9fqj0e3hfhkyfmcf9j6cpe5mf5stt9cymfq3xm276x4sramhn8srr42g78q9rkmqesevy33p4kdhkuggl99x9zx7j05uflx3s8q558xeaq9q9ctxqx52rzg6yffwykxngvxf7g3n83ddawagq2lnk59kcnexm54jrsmkjk9nemlge9v6p7fxu5z6yvq3ghpet8ww2c3h9fylvjx6j9njwz744376ap5r3xcpzh7qlk0kq3gknzfqhmxmjqypf6xz6geffdmy25apzsfr4ce0x825njkf62mgzx0x9n3zt0490u4dnt4468zse4zvd9sv0ycx2u2wsseuy4yymrg0nmfuxcwq9q0jykq7g5f59vunf95vnmcw336ksrh43fgapsd40u4227ymx56xtudtrte39twnq6fdfdj4p9pgx7pcqdk953za76fqcnvatx2cmywurag7ky4y8ecq5lvf25rydhxg6gmcn6vf78vtsqw79hp3mzv2u4v4jzn0nk50kl4gpv7k60cgz79c6k30jzy8dyetqtwg04tguxkv2p4hqj02yvsz2szaqdn9flu24rhth3v0t33023nwy27gt0x32eqpa5tpz6ygz3yzcsa3w889drpynzd5hlywuk9dwgafuax7upfnnxch4znrx4vactasg32u7vl886pxfswskk7wjgqkggrewtnaecfusl3nmg0xxh8e5nl5eft0xqwj5rk9ajn2j6yxehhgg5k6kpw5pk382hxmpu8wcd3rqx6m8905c7f8v0x2z9gyduxtqp2fdy9w2z4g2rrgu4d5cqes5tnwag007h2f326s3j9p6970xyd73rv5awx27zzezrax467c6vuye62ha88pzhw9xytjpezw3vg3l6x4k9rxwm9f6lwj6j7r9makg59rhgt0355s3jx9vw3raz5qdk5cnpny56q5h3dyd75suskuz63wmmfp9333tx4ry38vnh7jxy0j0cka99y7v2dmyjldjy0mqjetcefsmmg3pw0h9ll0rtskpmxz2cs8vmx6lu4v5rjtzpzgaqt46cwvnmhy5e3ye0tv6sre342lwg0tfd0yjkfz2haqc3hprpqf5ce8c9lx076dnk5cukn3tu38jgprrxt9sk9nqzt8grvkc4qzlg8jjf638v07ej9cf4eu48kphrgyjy83qm822xq8v9k22zwa7txr439x74j2f9ay7frkwtfdwn02enyx8stjpr4wgg25n5tj7uxyx0dqt3hnrqf7dcgt9fqnkjgemjfzyck6pawewptcayxm49sq3477f4cvm2p8fmg27rcmwm5l2vmzvdmymvkrr3aywu04y3zlhs7awu3r9u9lseheqpffvm2xf8y6w2qaj9g22pcu0sdsnqseusrxq9awru5lyqxnzftlslgpe3njegxkze98rveswp3u0xgx69wkg38vgp4k55lwwgv5l7ccv503mh9q23nud4xtm499xq276u40rxyrzaqfyuz2f3v5qr2vq00xs4g3ruynmq0c6nprvjcwqc5zrdn4cnzeeuwumjvqfjdpxr739n4cqcwytc8pjmfcj2qs3l2a8p5fzq7nvtng2jvvklx0x5ypnlkeuegsm587f6d4dkwel2es0lwwt88y2jdg8enjyph02tnjyndlx34h52fngjk96ggtu6vcxqkreqcxs4gwa8ww40t0kzlxfzyfhtjmfdm5fcrr5pt2xm4yslfynr0h9wkranhyadxf33kjn9xpg2fq5hnlq0 -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with -i
age -e -i key.txt -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with the wrong key
age -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -o test.age input
! age -d -i key.txt test.age
stderr 'no identity matched any of the recipients'
age -r age1pq13tgqupu990y5qlwwjy4zrzwvxg54cq89x9pxzdjenv4cl7affsqeerksrc6ufw3ndp4h5p3l5hm2jekkd7uay2mlh9eqg3sjcwmv5dcrwguc8djf83qr8gswtdrpav3jpw47kgkh6z0rnfk92xyqeyce48t02n4qz68augry3r3nd4rpdsw998vnnxzd2vepypktx2wxhfvd56jw7p32te6ttcyhc7tkqy2ks77jtjl8cgyal43pcd3g9wkcsgytccqtcnzcucjjdadpr0v7ye8u9fqusq6cq2ah0e4ejrltc8sk6763d7mjkuchmc5tkx6xztygqjx4ldxqh7jzkkyuv6ywt07ys4x09eq7gat4wuznrvgs3pkxjju8t9n0gpfqrsv658m5v09ltqc3c5z30n43ld43dwe5dn8nsk89f43tzsw2zta5kvl95zv0a94tc5y9r4ncj8g63fgljzhdxl96eg39rgd56yuvr2xdvavkk93a85n99jpjeqkfveprqc7eqft0wp7qgxcgsrwcdymgp0cqaj93jp4ckln36ylcwdzew234esa2awty25zc6pmc0jcpkrfuuptr9z5a4kx9ff4ppfff2lw95um853tcg47fa444er9k9j4e8vy5hfjx626j3tz3kc94jl8r66ahfprwtcyxae3swe9xxjhx5dfnt53n5fs2ut83t597wscjg9jc4f6h4uen2ll2ne30w7uvts8vuc5mpcfpp9espa78y0f8588m62kzez5kxxtcxykdkrnjx08w5az3pr6g6dsykwazcxtetpkq85mg7kay84z58slmvxmq56342pm6v8y3evr5wvc9g4j87jfhmryymkz9wk2en2v6tfy83rz657t0w7p3va46jt22f29u9ggzt9nu6qtguw4cp7cpj572ccyenw5rdh43h6s54g73gvxapsq3hw5yfkgttcyhkrjf9ykzd3wm5zsqgp5fca0nr56dnqe3kqrszasagqne0a83cguzwj9aw456jaxg9rurq4n4f6qec4srd0rxqfqc6rw22c2p4xyg25pls4dmxvce2fah8m6shue84preky2h0c4sa9y9tjuzn20gr9aav96e689nqx23s7zz0lvhq925jdkn04cdpzfscvhh93vh9f2tmwkzsq7pq0ncjk8g8cu75u77lyej46a4m2fefy8v2x4nzay05kulhyzk69520r7fdg3fzh0z7ysequcltr94hyf88h46hfujk30x9jp8u0pcjywh06g7tv483ypmc3pm3x3z4h9ph66lx228t3r96hyt764yxe2rn5clnfxkj3k5wf5hffehj58y56qyykwayk6qt9skvctxw20v9xy5ppnld3lushqpsutxht7cqygypdn5d2ppdgaerqzgehyrpdwzkhu0qe8w3u5h6htz8aa5zfpzy4s7sl8zdv3q0gq8ez08p86e4v3m82882yvawrvyrcxewrznwkvvez8m5aj6ktgvynyy0kc2trmtjzvjlxdf06e3rjmf55lwxrucfwu2sxdtnte83fgq0tvr2juv3pfqp8ddsrnckzqcfcvjp02mfg5y4aqlsunxpfcrdm46fphwsslrudwrfh542xg62kphca6h3xxqn538pprkknt35y00ygvrse5mxpvnstvcrnak5qduhf5dqslkn0yeadgpq6tv4wzy98kdjdzp22cpq6dy3kve856y2qlk7elyqyzj7ezpnh3vjwwmcm7ctp23k2sct86jd46ztplsq5vdjg9lyspxt0k8qx5udu9lwgulzapn3kdg7yd2zdz5dqf9933mpzwc32x8uxn8h2v5hlhdd4qk0uvwxhxyul8keaw39pz2avywk3wfly6veet9pjnj7nqecrgz824whs9sf7wl2shxk9kvkteht4z9x3w2gc6hqz5y -o test.age input
! age -d -i key.txt test.age
stderr 'no identity matched any of the recipients'
# cannot mix hybrid and X25519 recipients
! age -r age1pq1044e3hfmq2tltgck54m0gnpv8rfrwkdxjgl38psnwg8zfyv4uqdu63kk0u94ztt0q35k9dzuwgv405kxq6due2sre2kfsjftf7k2vz2mdpetku9pufy9d6ny6k5ss0cghpxdxrux0yd5xy40f8fu8ch9xn2rczyek2pqetqk0l9fqj0e3hfhkyfmcf9j6cpe5mf5stt9cymfq3xm276x4sramhn8srr42g78q9rkmqesevy33p4kdhkuggl99x9zx7j05uflx3s8q558xeaq9q9ctxqx52rzg6yffwykxngvxf7g3n83ddawagq2lnk59kcnexm54jrsmkjk9nemlge9v6p7fxu5z6yvq3ghpet8ww2c3h9fylvjx6j9njwz744376ap5r3xcpzh7qlk0kq3gknzfqhmxmjqypf6xz6geffdmy25apzsfr4ce0x825njkf62mgzx0x9n3zt0490u4dnt4468zse4zvd9sv0ycx2u2wsseuy4yymrg0nmfuxcwq9q0jykq7g5f59vunf95vnmcw336ksrh43fgapsd40u4227ymx56xtudtrte39twnq6fdfdj4p9pgx7pcqdk953za76fqcnvatx2cmywurag7ky4y8ecq5lvf25rydhxg6gmcn6vf78vtsqw79hp3mzv2u4v4jzn0nk50kl4gpv7k60cgz79c6k30jzy8dyetqtwg04tguxkv2p4hqj02yvsz2szaqdn9flu24rhth3v0t33023nwy27gt0x32eqpa5tpz6ygz3yzcsa3w889drpynzd5hlywuk9dwgafuax7upfnnxch4znrx4vactasg32u7vl886pxfswskk7wjgqkggrewtnaecfusl3nmg0xxh8e5nl5eft0xqwj5rk9ajn2j6yxehhgg5k6kpw5pk382hxmpu8wcd3rqx6m8905c7f8v0x2z9gyduxtqp2fdy9w2z4g2rrgu4d5cqes5tnwag007h2f326s3j9p6970xyd73rv5awx27zzezrax467c6vuye62ha88pzhw9xytjpezw3vg3l6x4k9rxwm9f6lwj6j7r9makg59rhgt0355s3jx9vw3raz5qdk5cnpny56q5h3dyd75suskuz63wmmfp9333tx4ry38vnh7jxy0j0cka99y7v2dmyjldjy0mqjetcefsmmg3pw0h9ll0rtskpmxz2cs8vmx6lu4v5rjtzpzgaqt46cwvnmhy5e3ye0tv6sre342lwg0tfd0yjkfz2haqc3hprpqf5ce8c9lx076dnk5cukn3tu38jgprrxt9sk9nqzt8grvkc4qzlg8jjf638v07ej9cf4eu48kphrgyjy83qm822xq8v9k22zwa7txr439x74j2f9ay7frkwtfdwn02enyx8stjpr4wgg25n5tj7uxyx0dqt3hnrqf7dcgt9fqnkjgemjfzyck6pawewptcayxm49sq3477f4cvm2p8fmg27rcmwm5l2vmzvdmymvkrr3aywu04y3zlhs7awu3r9u9lseheqpffvm2xf8y6w2qaj9g22pcu0sdsnqseusrxq9awru5lyqxnzftlslgpe3njegxkze98rveswp3u0xgx69wkg38vgp4k55lwwgv5l7ccv503mh9q23nud4xtm499xq276u40rxyrzaqfyuz2f3v5qr2vq00xs4g3ruynmq0c6nprvjcwqc5zrdn4cnzeeuwumjvqfjdpxr739n4cqcwytc8pjmfcj2qs3l2a8p5fzq7nvtng2jvvklx0x5ypnlkeuegsm587f6d4dkwel2es0lwwt88y2jdg8enjyph02tnjyndlx34h52fngjk96ggtu6vcxqkreqcxs4gwa8ww40t0kzlxfzyfhtjmfdm5fcrr5pt2xm4yslfynr0h9wkranhyadxf33kjn9xpg2fq5hnlq0 -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -o test.age input
stderr 'incompatible'
! age -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -r age1pq1044e3hfmq2tltgck54m0gnpv8rfrwkdxjgl38psnwg8zfyv4uqdu63kk0u94ztt0q35k9dzuwgv405kxq6due2sre2kfsjftf7k2vz2mdpetku9pufy9d6ny6k5ss0cghpxdxrux0yd5xy40f8fu8ch9xn2rczyek2pqetqk0l9fqj0e3hfhkyfmcf9j6cpe5mf5stt9cymfq3xm276x4sramhn8srr42g78q9rkmqesevy33p4kdhkuggl99x9zx7j05uflx3s8q558xeaq9q9ctxqx52rzg6yffwykxngvxf7g3n83ddawagq2lnk59kcnexm54jrsmkjk9nemlge9v6p7fxu5z6yvq3ghpet8ww2c3h9fylvjx6j9njwz744376ap5r3xcpzh7qlk0kq3gknzfqhmxmjqypf6xz6geffdmy25apzsfr4ce0x825njkf62mgzx0x9n3zt0490u4dnt4468zse4zvd9sv0ycx2u2wsseuy4yymrg0nmfuxcwq9q0jykq7g5f59vunf95vnmcw336ksrh43fgapsd40u4227ymx56xtudtrte39twnq6fdfdj4p9pgx7pcqdk953za76fqcnvatx2cmywurag7ky4y8ecq5lvf25rydhxg6gmcn6vf78vtsqw79hp3mzv2u4v4jzn0nk50kl4gpv7k60cgz79c6k30jzy8dyetqtwg04tguxkv2p4hqj02yvsz2szaqdn9flu24rhth3v0t33023nwy27gt0x32eqpa5tpz6ygz3yzcsa3w889drpynzd5hlywuk9dwgafuax7upfnnxch4znrx4vactasg32u7vl886pxfswskk7wjgqkggrewtnaecfusl3nmg0xxh8e5nl5eft0xqwj5rk9ajn2j6yxehhgg5k6kpw5pk382hxmpu8wcd3rqx6m8905c7f8v0x2z9gyduxtqp2fdy9w2z4g2rrgu4d5cqes5tnwag007h2f326s3j9p6970xyd73rv5awx27zzezrax467c6vuye62ha88pzhw9xytjpezw3vg3l6x4k9rxwm9f6lwj6j7r9makg59rhgt0355s3jx9vw3raz5qdk5cnpny56q5h3dyd75suskuz63wmmfp9333tx4ry38vnh7jxy0j0cka99y7v2dmyjldjy0mqjetcefsmmg3pw0h9ll0rtskpmxz2cs8vmx6lu4v5rjtzpzgaqt46cwvnmhy5e3ye0tv6sre342lwg0tfd0yjkfz2haqc3hprpqf5ce8c9lx076dnk5cukn3tu38jgprrxt9sk9nqzt8grvkc4qzlg8jjf638v07ej9cf4eu48kphrgyjy83qm822xq8v9k22zwa7txr439x74j2f9ay7frkwtfdwn02enyx8stjpr4wgg25n5tj7uxyx0dqt3hnrqf7dcgt9fqnkjgemjfzyck6pawewptcayxm49sq3477f4cvm2p8fmg27rcmwm5l2vmzvdmymvkrr3aywu04y3zlhs7awu3r9u9lseheqpffvm2xf8y6w2qaj9g22pcu0sdsnqseusrxq9awru5lyqxnzftlslgpe3njegxkze98rveswp3u0xgx69wkg38vgp4k55lwwgv5l7ccv503mh9q23nud4xtm499xq276u40rxyrzaqfyuz2f3v5qr2vq00xs4g3ruynmq0c6nprvjcwqc5zrdn4cnzeeuwumjvqfjdpxr739n4cqcwytc8pjmfcj2qs3l2a8p5fzq7nvtng2jvvklx0x5ypnlkeuegsm587f6d4dkwel2es0lwwt88y2jdg8enjyph02tnjyndlx34h52fngjk96ggtu6vcxqkreqcxs4gwa8ww40t0kzlxfzyfhtjmfdm5fcrr5pt2xm4yslfynr0h9wkranhyadxf33kjn9xpg2fq5hnlq0 -o test.age input
stderr 'incompatible'
# convert to plugin identity and use plugin
exec age-plugin-pq -identity -o key-plugin.txt key.txt
age -e -i key.txt -o test.age input
age -d -i key-plugin.txt test.age
cmp stdout input
! stderr .
age -e -i key-plugin.txt -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
-- input --
test
-- key.txt --
# created: 2025-11-17T13:27:37+01:00
# public key: age1pq1044e3hfmq2tltgck54m0gnpv8rfrwkdxjgl38psnwg8zfyv4uqdu63kk0u94ztt0q35k9dzuwgv405kxq6due2sre2kfsjftf7k2vz2mdpetku9pufy9d6ny6k5ss0cghpxdxrux0yd5xy40f8fu8ch9xn2rczyek2pqetqk0l9fqj0e3hfhkyfmcf9j6cpe5mf5stt9cymfq3xm276x4sramhn8srr42g78q9rkmqesevy33p4kdhkuggl99x9zx7j05uflx3s8q558xeaq9q9ctxqx52rzg6yffwykxngvxf7g3n83ddawagq2lnk59kcnexm54jrsmkjk9nemlge9v6p7fxu5z6yvq3ghpet8ww2c3h9fylvjx6j9njwz744376ap5r3xcpzh7qlk0kq3gknzfqhmxmjqypf6xz6geffdmy25apzsfr4ce0x825njkf62mgzx0x9n3zt0490u4dnt4468zse4zvd9sv0ycx2u2wsseuy4yymrg0nmfuxcwq9q0jykq7g5f59vunf95vnmcw336ksrh43fgapsd40u4227ymx56xtudtrte39twnq6fdfdj4p9pgx7pcqdk953za76fqcnvatx2cmywurag7ky4y8ecq5lvf25rydhxg6gmcn6vf78vtsqw79hp3mzv2u4v4jzn0nk50kl4gpv7k60cgz79c6k30jzy8dyetqtwg04tguxkv2p4hqj02yvsz2szaqdn9flu24rhth3v0t33023nwy27gt0x32eqpa5tpz6ygz3yzcsa3w889drpynzd5hlywuk9dwgafuax7upfnnxch4znrx4vactasg32u7vl886pxfswskk7wjgqkggrewtnaecfusl3nmg0xxh8e5nl5eft0xqwj5rk9ajn2j6yxehhgg5k6kpw5pk382hxmpu8wcd3rqx6m8905c7f8v0x2z9gyduxtqp2fdy9w2z4g2rrgu4d5cqes5tnwag007h2f326s3j9p6970xyd73rv5awx27zzezrax467c6vuye62ha88pzhw9xytjpezw3vg3l6x4k9rxwm9f6lwj6j7r9makg59rhgt0355s3jx9vw3raz5qdk5cnpny56q5h3dyd75suskuz63wmmfp9333tx4ry38vnh7jxy0j0cka99y7v2dmyjldjy0mqjetcefsmmg3pw0h9ll0rtskpmxz2cs8vmx6lu4v5rjtzpzgaqt46cwvnmhy5e3ye0tv6sre342lwg0tfd0yjkfz2haqc3hprpqf5ce8c9lx076dnk5cukn3tu38jgprrxt9sk9nqzt8grvkc4qzlg8jjf638v07ej9cf4eu48kphrgyjy83qm822xq8v9k22zwa7txr439x74j2f9ay7frkwtfdwn02enyx8stjpr4wgg25n5tj7uxyx0dqt3hnrqf7dcgt9fqnkjgemjfzyck6pawewptcayxm49sq3477f4cvm2p8fmg27rcmwm5l2vmzvdmymvkrr3aywu04y3zlhs7awu3r9u9lseheqpffvm2xf8y6w2qaj9g22pcu0sdsnqseusrxq9awru5lyqxnzftlslgpe3njegxkze98rveswp3u0xgx69wkg38vgp4k55lwwgv5l7ccv503mh9q23nud4xtm499xq276u40rxyrzaqfyuz2f3v5qr2vq00xs4g3ruynmq0c6nprvjcwqc5zrdn4cnzeeuwumjvqfjdpxr739n4cqcwytc8pjmfcj2qs3l2a8p5fzq7nvtng2jvvklx0x5ypnlkeuegsm587f6d4dkwel2es0lwwt88y2jdg8enjyph02tnjyndlx34h52fngjk96ggtu6vcxqkreqcxs4gwa8ww40t0kzlxfzyfhtjmfdm5fcrr5pt2xm4yslfynr0h9wkranhyadxf33kjn9xpg2fq5hnlq0
AGE-SECRET-KEY-PQ-16XDSLZ3XCSZ3236YJ5J0T9NAPLZTP96LJKQCVHJYUDTQVJR5J5PQTDPQCX

25
cmd/age/testdata/keygen.txt vendored Normal file
View File

@@ -0,0 +1,25 @@
exec age-keygen
stdout '# created: 20'
stdout '# public key: age1'
stdout 'AGE-SECRET-KEY-1'
stderr 'Public key: age1'
exec age-keygen -pq
stdout '# created: 20'
stdout '# public key: age1pq1'
stdout 'AGE-SECRET-KEY-PQ-1'
stderr 'Public key: age1pq1'
exec age-keygen -pq -o key.txt
! stdout .
stderr 'Public key: age1pq1'
grep '# created: 20' key.txt
grep '# public key: age1pq1' key.txt
grep 'AGE-SECRET-KEY-PQ-1' key.txt
stdin key.txt
exec age-keygen -y
stdout age1pq1
exec age-keygen -y key.txt
stdout age1pq1

78
cmd/age/testdata/output_file.txt vendored Normal file
View File

@@ -0,0 +1,78 @@
# https://github.com/FiloSottile/age/issues/57
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input
! age -o test.out -d -i wrong.txt test.age
! exists test.out
! age -o test.out -d test.age
! exists test.out
! age -o test.out -d -i notexist test.age
! exists test.out
! age -o test.out -d -i wrong.txt notexist
! exists test.out
! age -o test.out -r BAD
! exists test.out
! age -o test.out -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef notexist
! exists test.out
! age -o test.out -p notexist
! exists test.out
# https://github.com/FiloSottile/age/issues/555
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o empty.age empty
exists empty.age
age -d -i key.txt empty.age
! stdout .
! stderr .
age -d -i key.txt -o new empty.age
! stderr .
cmp new empty
# https://github.com/FiloSottile/age/issues/491
cp input inputcopy
! age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o inputcopy inputcopy
stderr 'input and output file are the same'
cmp inputcopy input
! age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o ./inputcopy inputcopy
stderr 'input and output file are the same'
cmp inputcopy input
mkdir foo
! age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o inputcopy foo/../inputcopy
stderr 'input and output file are the same'
cmp inputcopy input
cp key.txt keycopy
age -e -i keycopy -o test.age input
! age -d -i keycopy -o keycopy test.age
stderr 'input and output file are the same'
cmp key.txt keycopy
[!linux] [!darwin] skip # no pty support
[darwin] [go1.20] skip # https://go.dev/issue/61779
ttyin terminal
! age -p -o inputcopy inputcopy
stderr 'input and output file are the same'
cmp inputcopy input
# https://github.com/FiloSottile/age/issues/159
ttyin terminal
age -p -a -o test.age input
ttyin terminalwrong
! age -o test.out -d test.age
ttyout 'Enter passphrase'
stderr 'incorrect passphrase'
! exists test.out
-- terminal --
password
password
-- terminalwrong --
wrong
-- input --
age
-- empty --
-- key.txt --
# created: 2021-02-02T13:09:43+01:00
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
-- wrong.txt --
# created: 2024-06-16T12:14:00+02:00
# public key: age10k7vsqmeg3sp8elfyq5ts55feg4huarpcaf9dmljn9umydg3gymsvx4dp9
AGE-SECRET-KEY-1NPX08S4LELW9K68FKU0U05XXEKG6X7GT004TPNYLF86H3M00D3FQ3VQQNN

13
cmd/age/testdata/pkcs8.txt vendored Normal file
View File

@@ -0,0 +1,13 @@
# https://github.com/FiloSottile/age/discussions/428
# encrypt and decrypt a file with an Ed25519 key encoded with PKCS#8
age -e -i key.pem -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
-- input --
test
-- key.pem --
-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIJT4Wpo+YG11yybKL/bYXQW7ekz4PAsmV/4tfmY1vU7x
-----END PRIVATE KEY-----

42
cmd/age/testdata/plugin.txt vendored Normal file
View File

@@ -0,0 +1,42 @@
# encrypt and decrypt a file with a test plugin
age -r age1test10qdmzv9q -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
# very long identity and recipient
age -R long-recipient.txt -o test.age input
age -d -i long-key.txt test.age
cmp stdout input
! stderr .
# check that path separators are rejected
chmod 755 age-plugin-pwn/pwn
mkdir $TMPDIR/age-plugin-pwn
cp age-plugin-pwn/pwn $TMPDIR/age-plugin-pwn/pwn
! age -r age1pwn/pwn19gt89dfz input
! age -d -i pwn-identity.txt test.age
! age -d -j pwn/pwn test.age
! exists pwn
# check plugin not found hint
! age -r age1nonexistentplugin1pt5d8z -o test1.age
stderr /awesome#plugins
! age -d -i nonexistent-identity.txt test.age
stderr /awesome#plugins
-- input --
test
-- key.txt --
AGE-PLUGIN-TEST-10Q32NLXM
-- long-recipient.txt --
age1test10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qj6rl8p
-- long-key.txt --
AGE-PLUGIN-TEST-10PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7RC0PU8S7Q5U8SUD
-- pwn-identity.txt --
AGE-PLUGIN-PWN/PWN-19GYK4WLY
-- age-plugin-pwn/pwn --
#!/bin/sh
touch "$WORK/pwn"
-- nonexistent-identity.txt --
AGE-PLUGIN-NONEXISTENTPLUGIN-1R4XFW4

62
cmd/age/testdata/rsa.txt vendored Normal file
View File

@@ -0,0 +1,62 @@
# encrypt and decrypt a file with -R
age -R key.pem.pub -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with -i
age -e -i key.pem -o test.age input
age -d -i key.pem test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with the wrong key
age -R otherkey.pem.pub -o test.age input
! age -d -i key.pem test.age
stderr 'no identity matched any of the recipients'
-- input --
test
-- key.pem --
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
NhAAAAAwEAAQAAAYEA1C04rdClHoW4oG4bEGmaNqFy4DLoPJ0358w4XH+XBM3TiWcheouW
kUG6m1yDmHk0t0oaaf4hOnetKovdyQQX73gGaq++rSu5VSvH7LbwABoG6PS/UbuZ4Vl9B0
5WVDqHVE9hNK4AHqBc373GU2mo8z5opKxEprmiS3HSd3K2wiMqL5E8XPOSm0p/isuYK57X
VUexl73tB7iIMLklxjcjtP4REMoQhHKOMOdy2Q15dw5cYG+drtEArBRYkCZmd0Vp2ws9pj
YzPVaOSkbdqSeLu+JVbH1wrwKhuBrA3eVlwjUTWkO4FHcNXkp773Mt4cXhKizTfbR2hQox
Lsj31301Xd7dEpV63sqDW1e+a2L2dhemi8cjDMrPuW6Z19Lbti0quAb4+cSLAaJI4BHd1F
8o9XhK7EHVCdIIIQDKVzo1WyEsDwBjL1LB9rpxm4732sZyue0uygFzmM544QX+WsiJXgHP
uC1Q/ynjLRm6ZMl16MwvY8B/XGQWxlOAbRJQG84fAAAFmEwAjV1MAI1dAAAAB3NzaC1yc2
EAAAGBANQtOK3QpR6FuKBuGxBpmjahcuAy6DydN+fMOFx/lwTN04lnIXqLlpFBuptcg5h5
NLdKGmn+ITp3rSqL3ckEF+94Bmqvvq0ruVUrx+y28AAaBuj0v1G7meFZfQdOVlQ6h1RPYT
SuAB6gXN+9xlNpqPM+aKSsRKa5oktx0ndytsIjKi+RPFzzkptKf4rLmCue11VHsZe97Qe4
iDC5JcY3I7T+ERDKEIRyjjDnctkNeXcOXGBvna7RAKwUWJAmZndFadsLPaY2Mz1WjkpG3a
kni7viVWx9cK8CobgawN3lZcI1E1pDuBR3DV5Ke+9zLeHF4Sos0320doUKMS7I99d9NV3e
3RKVet7Kg1tXvmti9nYXpovHIwzKz7lumdfS27YtKrgG+PnEiwGiSOAR3dRfKPV4SuxB1Q
nSCCEAylc6NVshLA8AYy9Swfa6cZuO99rGcrntLsoBc5jOeOEF/lrIiV4Bz7gtUP8p4y0Z
umTJdejML2PAf1xkFsZTgG0SUBvOHwAAAAMBAAEAAAGBAKytAOu0Wi009sTZ1vzMdMzxJ+
R+ibKK4Oysr1HYJLesKvQwEncBE1C0BYJbEF4OhnCExmpsf+5tZ2iw25a01iX1sIMy9CNK
6lH+h36Gg1wR0n3Ucb+6xck4YyCHCIsT9v8OezW8Riympe8RK07HNtB/gfpCmLx3ZzWvNH
Ix0bq9k5+Su2WKdU4cmyACAZ2+b9DfwBCWaUlXTL8abzuZtF2gR5M6X6bq8/2o3zb2WFwk
O9nf/JxBTCK/jDQEjG+U9MyGxZIW5DeG1nNFtOzJoT8krIkeSOjQ5XQrkjCw+yihSCWMG+
s+SKO77u30SO7OCENsFIXpUzpt6+JmazlXjLW/OdYNooQMHtqCZzVMRgxiy3gDGF35YvgV
VnP5gVEW9HEZ0kD+x4Rl2kB6bV7jMi8BXrazQ1EmTasJFg1pv6iRJWzY1JoP2kRfgiHGL6
OqgrXakqo3hMJuz+JRU2/hlF13743MiIxpcbaaRqURoWuNRLHitVWE35/XVCez0C6OwQAA
AMEAoh106+3JbiZI19iRICR247IoOLpSSed98eQj+l3OYfJ86cQipSjxdSPWcP58yyyElY
d9q6K16sDTLAlRJzF7MFxSc80JY6RgFq/Sy4Jm0/Z10wwJhTgOkxq6IynzLnO7goRirE31
jxGif4nI2IYEQvv6MOD8TWA4axxGMw2StYB6P4R5peozf81oR6m79ERIDSkrm0RYYn931r
gVuxvo3ABVxMtg1lV80LJMayy87Oi8BehGBxMBgsKtQaH8+5h7AAAAwQD+8lJpBcrrHQKk
3o2XAZxB5Fool4f2iuZWTxA1vq0/TCUcEodrdWfLuDeVbDsFemW0vBSkKzf4NlZSs2DAKl
YWT6y18eyDyJXn0TNVTeO3F5mkkX5spqbjDcESSs3whIuDqXU++3sII7iMzGw50tDP4Dw6
TViEVM3anpeqlAbkciR5o9IJx3nRcGh81Bs4gticcRF0vqiJoAhNlSZXR1XMjevwt68i+4
RKPPQsTM7uJLm236VUhDivO1OJcBTLW7MAAADBANUNqH+//G4gIruBO3BsIvbzDw0DgRam
R1tqqn4g53boiv1RPtUJ2GbkCsisy5pU+JdTN7ekFEF8KWuunjImkfVyAiTFsHHmOoXV3Z
EX0mNDXOlKOP2YAIMrDt5CkPdEh6qQG21LCZXTWmwheZ9iN2vOl/fKqUW9lqd/kTe6WsON
hIpZhs2+oz54Riq1ZwzO9NkcYrvZoDKbDopL1r2ibw0mkgCJrxpWi0Yt2Iooh4GXXqP5C9
T8hrZCbrVJkjKd5QAAABtmaWxpcHBvQEJpc3Ryb21hdGgtTTEubG9jYWwBAgMEBQY=
-----END OPENSSH PRIVATE KEY-----
-- key.pem.pub --
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbigbhsQaZo2oXLgMug8nTfnzDhcf5cEzdOJZyF6i5aRQbqbXIOYeTS3Shpp/iE6d60qi93JBBfveAZqr76tK7lVK8fstvAAGgbo9L9Ru5nhWX0HTlZUOodUT2E0rgAeoFzfvcZTaajzPmikrESmuaJLcdJ3crbCIyovkTxc85KbSn+Ky5grntdVR7GXve0HuIgwuSXGNyO0/hEQyhCEco4w53LZDXl3Dlxgb52u0QCsFFiQJmZ3RWnbCz2mNjM9Vo5KRt2pJ4u74lVsfXCvAqG4GsDd5WXCNRNaQ7gUdw1eSnvvcy3hxeEqLNN9tHaFCjEuyPfXfTVd3t0SlXreyoNbV75rYvZ2F6aLxyMMys+5bpnX0tu2LSq4Bvj5xIsBokjgEd3UXyj1eErsQdUJ0gghAMpXOjVbISwPAGMvUsH2unGbjvfaxnK57S7KAXOYznjhBf5ayIleAc+4LVD/KeMtGbpkyXXozC9jwH9cZBbGU4BtElAbzh8=
-- otherkey.pem.pub --
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDF0OPu95EY25O5KmYFLIkiZZFKUlfvaRgmfIT6OcZvPRXBzo0MS/lcrYvAc0RsUVbZ1B3Y9oWmKt/IMXTztCXiza70rO1NI7ciayv5svY/wGMoveutddhA64IjrQKs4m+6Qmjs/dYTnfsk1BzmXrdRKUSqH6c4Id7pRLC1ySLu+4og3nTTpBRBpg+uSkc4Ua6ce6A6RX14PPJ+TAXMfZyKNyaubQhgzLB/CfdXxZqWdAnyooiE7fb6CEB5uppnA5BpPdcWAkSixbwxRHbRC+OSCqMOV6+z+NlO/qSOKJcXfCQnJP/qjJTJde0dYhXG4RILOzIkGVieGJJONDXvj61mMj568IhJz0AEf/UMhvEL79iJ6yZW82Go/zcYkDDfd3KRE3pW+6p9Onu3XqOiQABS+9rEVRBnqYsPajiHBIanBeXpWKGbjznakvxhdRifhOWwAsQDfLmGzh+JnV1vOUjyxKtLNv9zi/oeuYCaIyF7F6en8LMbYSz8YONMZygGxMU=

65
cmd/age/testdata/scrypt.txt vendored Normal file
View File

@@ -0,0 +1,65 @@
[!linux] [!darwin] skip # no pty support
[darwin] [go1.20] skip # https://go.dev/issue/61779
# encrypt with a provided passphrase
stdin input
ttyin terminal
age -p -o test.age
ttyout 'Enter passphrase'
! stderr .
! stdout .
# decrypt with a provided passphrase
ttyin terminal
age -d test.age
ttyout 'Enter passphrase'
! stderr .
cmp stdout input
# decrypt with the wrong passphrase
ttyin wrong
! age -d test.age
stderr 'incorrect passphrase'
# encrypt with a generated passphrase
stdin input
ttyin empty
age -p -o test.age
! stderr .
! stdout .
ttyin autogenerated
age -d test.age
cmp stdout input
# fail when -i is present
ttyin terminal
! age -d -i key.txt test.age
stderr 'file is passphrase-encrypted but identities were specified'
# fail when passphrases don't match
ttyin wrong
! age -p -o fail.age
stderr 'passphrases didn''t match'
! exists fail.age
# fail when -i is missing
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input
! age -d test.age
stderr 'file is not passphrase-encrypted, identities are required'
-- terminal --
password
password
-- wrong --
PASSWORD
password
-- input --
test
-- key.txt --
# created: 2021-02-02T13:09:43+01:00
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0
-- autogenerated --
four-four-four-four-four-four-four-four-four-four
-- empty --

57
cmd/age/testdata/terminal.txt vendored Normal file
View File

@@ -0,0 +1,57 @@
[!linux] [!darwin] skip # no pty support
[darwin] [go1.20] skip # https://go.dev/issue/61779
# controlling terminal is used instead of stdin/stderr
ttyin terminal
age -p -o test.age input
! stderr .
# autogenerated passphrase is printed to terminal
ttyin empty
age -p -o test.age input
ttyout 'autogenerated passphrase'
! stderr .
# with no controlling terminal, stdin terminal is used
## TODO: enable once https://golang.org/issue/53601 is fixed
## and Noctty is added to testscript.
# noctty
# ttyin -stdin terminal
# age -p -o test.age input
# ! stderr .
# no terminal causes an error
## TODO: enable once https://golang.org/issue/53601 is fixed
## and Noctty is added to testscript.
# noctty
# ! age -p -o test.age input
# stderr 'standard input is not a terminal'
# prompt for password before plaintext if stdin is the terminal
exec cat terminal input # concatenated password + input
ttyin -stdin stdout
age -p -a -o test.age
ttyout 'Enter passphrase'
! stderr .
# check the file was encrypted correctly
ttyin terminal
age -d test.age
cmp stdout input
# buffer armored ciphertext before prompting if stdin is the terminal
ttyin terminal
age -p -a -o test.age input
exec cat test.age terminal # concatenated ciphertext + password
ttyin -stdin stdout
age -d
ttyout 'Enter passphrase'
! stderr .
cmp stdout input
-- input --
test
-- terminal --
password
password
-- empty --

20
cmd/age/testdata/usage.txt vendored Normal file
View File

@@ -0,0 +1,20 @@
# -help
age -p -help
! stdout .
stderr 'Usage:'
# -h
age -p -h
! stdout .
stderr 'Usage:'
# unknown flag
! age -p -this-flag-does-not-exist
! stdout .
stderr 'flag provided but not defined'
stderr 'Usage:'
# no arguments
! age
! stdout .
stderr 'Usage:'

28
cmd/age/testdata/x25519.txt vendored Normal file
View File

@@ -0,0 +1,28 @@
# encrypt and decrypt a file with -r
age -r age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with -i
age -e -i key.txt -o test.age input
age -d -i key.txt test.age
cmp stdout input
! stderr .
# encrypt and decrypt a file with the wrong key
age -r age12phkzssndd5axajas2h74vtge62c86xjhd6u9anyanqhzvdg6sps0xthgl -o test.age input
! age -d -i key.txt test.age
stderr 'no identity matched any of the recipients'
# decrypt an empty file
! age -d -i key.txt empty
stderr empty
-- empty --
-- input --
test
-- key.txt --
# created: 2021-02-02T13:09:43+01:00
# public key: age1xmwwc06ly3ee5rytxm9mflaz2u56jjj36s0mypdrwsvlul66mv4q47ryef
AGE-SECRET-KEY-1EGTZVFFV20835NWYV6270LXYVK2VKNX2MMDKWYKLMGR48UAWX40Q2P2LM0

78
cmd/age/tui.go Normal file
View File

@@ -0,0 +1,78 @@
// Copyright 2021 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
// This file implements the terminal UI of cmd/age. The rules are:
//
// - Anything that requires user interaction goes to the terminal,
// and is erased afterwards if possible. This UI would be possible
// to replace with a pinentry with no output or UX changes.
//
// - Everything else goes to standard error with an "age:" prefix.
// No capitalized initials and no periods at the end.
//
// The one exception is the autogenerated passphrase, which goes to
// the terminal, since we really want it to reach the user only.
import (
"bytes"
"fmt"
"io"
"log"
"os"
"filippo.io/age/armor"
"filippo.io/age/internal/term"
)
// l is a logger with no prefixes.
var l = log.New(os.Stderr, "", 0)
func printf(format string, v ...any) {
l.Printf("age: "+format, v...)
}
func errorf(format string, v ...any) {
l.Printf("age: error: "+format, v...)
l.Printf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
os.Exit(1)
}
func warningf(format string, v ...any) {
l.Printf("age: warning: "+format, v...)
}
func errorWithHint(error string, hints ...string) {
l.Printf("age: error: %s", error)
for _, hint := range hints {
l.Printf("age: hint: %s", hint)
}
l.Printf("age: report unexpected or unhelpful errors at https://filippo.io/age/report")
os.Exit(1)
}
func printfToTerminal(format string, v ...any) error {
return term.WithTerminal(func(_, out *os.File) error {
_, err := fmt.Fprintf(out, "age: "+format+"\n", v...)
return err
})
}
func bufferTerminalInput(in io.Reader) (io.Reader, error) {
buf := &bytes.Buffer{}
if _, err := buf.ReadFrom(ReaderFunc(func(p []byte) (n int, err error) {
if bytes.Contains(buf.Bytes(), []byte(armor.Footer+"\n")) {
return 0, io.EOF
}
return in.Read(p)
})); err != nil {
return nil, err
}
return buf, nil
}
type ReaderFunc func(p []byte) (n int, err error)
func (f ReaderFunc) Read(p []byte) (n int, err error) { return f(p) }

View File

@@ -1,8 +1,6 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package main
@@ -12,7 +10,12 @@ import (
"strings"
)
var testOnlyFixedRandomWord string
func randomWord() string {
if testOnlyFixedRandomWord != "" {
return testOnlyFixedRandomWord
}
buf := make([]byte, 2)
if _, err := rand.Read(buf); err != nil {
panic(err)

95
doc/age-inspect.1 Normal file
View File

@@ -0,0 +1,95 @@
.\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "AGE\-INSPECT" "1" "December 2025" ""
.SH "NAME"
\fBage\-inspect\fR \- inspect age(1) encrypted files
.SH "SYNOPSIS"
\fBage\-inspect\fR [\fB\-\-json\fR] [\fIINPUT\fR]
.SH "DESCRIPTION"
\fBage\-inspect\fR reads an age(1) encrypted file from \fIINPUT\fR (or standard input) and displays metadata about it without decrypting\.
.P
This includes the recipient types, whether it uses post\-quantum encryption, and a size breakdown of the file components\.
.SH "OPTIONS"
.TP
\fB\-\-json\fR
Output machine\-readable JSON instead of human\-readable text\.
.TP
\fB\-\-version\fR
Print the version and exit\.
.SH "JSON FORMAT"
When \fB\-\-json\fR is specified, the output is a JSON object with these fields:
.IP "\[ci]" 4
\fBversion\fR: The age format version (e\.g\., \fB"age\-encryption\.org/v1"\fR)\.
.IP "\[ci]" 4
\fBpostquantum\fR: Whether the file uses post\-quantum encryption: \fB"yes"\fR, \fB"no"\fR, or \fB"unknown"\fR\.
.IP "\[ci]" 4
\fBarmor\fR: Boolean indicating whether the file is ASCII\-armored\.
.IP "\[ci]" 4
\fBstanza_types\fR: Array of recipient stanza type strings (e\.g\., \fB["X25519"]\fR or \fB["mlkem768x25519"]\fR)\.
.IP "\[ci]" 4
\fBsizes\fR: Object containing size information in bytes:
.IP "\[ci]" 4
\fBheader\fR: Size of the age header\.
.IP "\[ci]" 4
\fBarmor\fR: Armor encoding overhead (0 if not armored)\.
.IP "\[ci]" 4
\fBoverhead\fR: Stream encryption overhead\.
.IP "\[ci]" 4
\fBmin_payload\fR, \fBmax_payload\fR: Payload size bounds (currently always matching)\.
.IP "\[ci]" 4
\fBmin_padding\fR, \fBmax_padding\fR: Padding size bounds (currently always 0)\.
.IP "" 0
.IP
The fields add up to the total size of the file\.
.IP "" 0
.SH "EXAMPLES"
Inspect an encrypted file:
.IP "" 4
.nf
$ age\-inspect secrets\.age
secrets\.age is an age file, version "age\-encryption\.org/v1"\.
This file is encrypted to the following recipient types:
\- "mlkem768x25519"
This file uses post\-quantum encryption\.
Size breakdown (assuming it decrypts successfully):
Header 1627 bytes
Encryption overhead 32 bytes
Payload 42 bytes
\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-\-
Total 1701 bytes
Tip: for machine\-readable output, use \-\-json\.
.fi
.IP "" 0
.P
Get JSON output for scripting:
.IP "" 4
.nf
$ age\-inspect \-\-json secrets\.age
{
"version": "age\-encryption\.org/v1",
"postquantum": "yes",
"armor": false,
"stanza_types": [
"mlkem768x25519"
],
"sizes": {
"header": 1627,
"armor": 0,
"overhead": 32,
"min_payload": 42,
"max_payload": 42,
"min_padding": 0,
"max_padding": 0
}
}
.fi
.IP "" 0
.SH "SEE ALSO"
age(1), age\-keygen(1)
.SH "AUTHORS"
Filippo Valsorda \fIage@filippo\.io\fR

204
doc/age-inspect.1.html Normal file
View File

@@ -0,0 +1,204 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' content='text/html;charset=utf8'>
<meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>
<title>age-inspect(1) - inspect age(1) encrypted files</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="#JSON-FORMAT">JSON FORMAT</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-inspect(1)</li>
<li class='tc'></li>
<li class='tr'>age-inspect(1)</li>
</ol>
<h2 id="NAME">NAME</h2>
<p class="man-name">
<code>age-inspect</code> - <span class="man-whatis">inspect <a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a> encrypted files</span>
</p>
<h2 id="SYNOPSIS">SYNOPSIS</h2>
<p><code>age-inspect</code> [<code>--json</code>] [<var>INPUT</var>]</p>
<h2 id="DESCRIPTION">DESCRIPTION</h2>
<p><code>age-inspect</code> reads an <a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a> encrypted file from <var>INPUT</var> (or standard input)
and displays metadata about it without decrypting.</p>
<p>This includes the recipient types, whether it uses post-quantum encryption,
and a size breakdown of the file components.</p>
<h2 id="OPTIONS">OPTIONS</h2>
<dl>
<dt><code>--json</code></dt>
<dd> Output machine-readable JSON instead of human-readable text.</dd>
<dt><code>--version</code></dt>
<dd> Print the version and exit.</dd>
</dl>
<h2 id="JSON-FORMAT">JSON FORMAT</h2>
<p>When <code>--json</code> is specified, the output is a JSON object with these fields:</p>
<ul>
<li>
<p><code>version</code>:
The age format version (e.g., <code>"age-encryption.org/v1"</code>).</p>
</li>
<li>
<p><code>postquantum</code>:
Whether the file uses post-quantum encryption: <code>"yes"</code>, <code>"no"</code>, or
<code>"unknown"</code>.</p>
</li>
<li>
<p><code>armor</code>:
Boolean indicating whether the file is ASCII-armored.</p>
</li>
<li>
<p><code>stanza_types</code>:
Array of recipient stanza type strings (e.g., <code>["X25519"]</code> or
<code>["mlkem768x25519"]</code>).</p>
</li>
<li>
<p><code>sizes</code>:
Object containing size information in bytes:</p>
<ul>
<li>
<code>header</code>: Size of the age header.</li>
<li>
<code>armor</code>: Armor encoding overhead (0 if not armored).</li>
<li>
<code>overhead</code>: Stream encryption overhead.</li>
<li>
<code>min_payload</code>, <code>max_payload</code>: Payload size bounds (currently always matching).</li>
<li>
<code>min_padding</code>, <code>max_padding</code>: Padding size bounds (currently always 0).</li>
</ul>
<p>The fields add up to the total size of the file.</p>
</li>
</ul>
<h2 id="EXAMPLES">EXAMPLES</h2>
<p>Inspect an encrypted file:</p>
<pre><code>$ age-inspect secrets.age
secrets.age is an age file, version "age-encryption.org/v1".
This file is encrypted to the following recipient types:
- "mlkem768x25519"
This file uses post-quantum encryption.
Size breakdown (assuming it decrypts successfully):
Header 1627 bytes
Encryption overhead 32 bytes
Payload 42 bytes
-------------------
Total 1701 bytes
Tip: for machine-readable output, use --json.
</code></pre>
<p>Get JSON output for scripting:</p>
<pre><code>$ age-inspect --json secrets.age
{
"version": "age-encryption.org/v1",
"postquantum": "yes",
"armor": false,
"stanza_types": [
"mlkem768x25519"
],
"sizes": {
"header": 1627,
"armor": 0,
"overhead": 32,
"min_payload": 42,
"max_payload": 42,
"min_padding": 0,
"max_padding": 0
}
}
</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>, <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'>December 2025</li>
<li class='tr'>age-inspect(1)</li>
</ol>
</div>
</body>
</html>

102
doc/age-inspect.1.ronn Normal file
View File

@@ -0,0 +1,102 @@
age-inspect(1) -- inspect age(1) encrypted files
====================================================
## SYNOPSIS
`age-inspect` [`--json`] [<INPUT>]
## DESCRIPTION
`age-inspect` reads an age(1) encrypted file from <INPUT> (or standard input)
and displays metadata about it without decrypting.
This includes the recipient types, whether it uses post-quantum encryption,
and a size breakdown of the file components.
## OPTIONS
* `--json`:
Output machine-readable JSON instead of human-readable text.
* `--version`:
Print the version and exit.
## JSON FORMAT
When `--json` is specified, the output is a JSON object with these fields:
* `version`:
The age format version (e.g., `"age-encryption.org/v1"`).
* `postquantum`:
Whether the file uses post-quantum encryption: `"yes"`, `"no"`, or
`"unknown"`.
* `armor`:
Boolean indicating whether the file is ASCII-armored.
* `stanza_types`:
Array of recipient stanza type strings (e.g., `["X25519"]` or
`["mlkem768x25519"]`).
* `sizes`:
Object containing size information in bytes:
* `header`: Size of the age header.
* `armor`: Armor encoding overhead (0 if not armored).
* `overhead`: Stream encryption overhead.
* `min_payload`, `max_payload`: Payload size bounds (currently always matching).
* `min_padding`, `max_padding`: Padding size bounds (currently always 0).
The fields add up to the total size of the file.
## EXAMPLES
Inspect an encrypted file:
$ age-inspect secrets.age
secrets.age is an age file, version "age-encryption.org/v1".
This file is encrypted to the following recipient types:
- "mlkem768x25519"
This file uses post-quantum encryption.
Size breakdown (assuming it decrypts successfully):
Header 1627 bytes
Encryption overhead 32 bytes
Payload 42 bytes
-------------------
Total 1701 bytes
Tip: for machine-readable output, use --json.
Get JSON output for scripting:
$ age-inspect --json secrets.age
{
"version": "age-encryption.org/v1",
"postquantum": "yes",
"armor": false,
"stanza_types": [
"mlkem768x25519"
],
"sizes": {
"header": 1627,
"armor": 0,
"overhead": 32,
"min_payload": 42,
"max_payload": 42,
"min_padding": 0,
"max_padding": 0
}
}
## SEE ALSO
age(1), age-keygen(1)
## AUTHORS
Filippo Valsorda <age@filippo.io>

71
doc/age-keygen.1 Normal file
View File

@@ -0,0 +1,71 @@
.\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "AGE\-KEYGEN" "1" "December 2025" ""
.SH "NAME"
\fBage\-keygen\fR \- generate age(1) key pairs
.SH "SYNOPSIS"
\fBage\-keygen\fR [\fB\-pq\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\-pq\fR
Generate a post\-quantum hybrid ML\-KEM\-768 + X25519 key pair\.
.IP
In the future, this might become the default\.
.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 post\-quantum identity:
.IP "" 4
.nf
$ age\-keygen \-pq
# created: 2025\-11\-17T13:39:06+01:00
# public key: age1pq167[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.]
AGE\-SECRET\-KEY\-PQ\-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T
.fi
.IP "" 0
.P
Generate a new traditional 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 post\-quantum identity to \fBkey\.txt\fR:
.IP "" 4
.nf
$ age\-keygen \-pq \-o key\.txt
Public key: age1pq1cd[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.]
.fi
.IP "" 0
.P
Convert an identity to a recipient:
.IP "" 4
.nf
$ age\-keygen \-y key\.txt
age1pq1cd[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.]
.fi
.IP "" 0
.SH "SEE ALSO"
age(1), age\-inspect(1)
.SH "AUTHORS"
Filippo Valsorda \fIage@filippo\.io\fR

159
doc/age-keygen.1.html Normal file
View File

@@ -0,0 +1,159 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' content='text/html;charset=utf8'>
<meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>
<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>-pq</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>-pq</code></dt>
<dd> Generate a post-quantum hybrid ML-KEM-768 + X25519 key pair.
<p>In the future, this might become the default.</p>
</dd>
<dt>
<code>-o</code>, <code>--output</code>=<var>OUTPUT</var>
</dt>
<dd> Write the identity to <var>OUTPUT</var> instead of standard output.
<p>If <var>OUTPUT</var> already exists, it is not overwritten.</p>
</dd>
<dt><code>-y</code></dt>
<dd> Read an identity file from <var>INPUT</var> or from standard input and output the
corresponding recipient(s), one per line, with no comments.</dd>
<dt><code>--version</code></dt>
<dd> Print the version and exit.</dd>
</dl>
<h2 id="EXAMPLES">EXAMPLES</h2>
<p>Generate a new post-quantum identity:</p>
<pre><code>$ age-keygen -pq
# created: 2025-11-17T13:39:06+01:00
# public key: age1pq167[... 1950 more characters ...]
AGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T
</code></pre>
<p>Generate a new traditional 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 post-quantum identity to <code>key.txt</code>:</p>
<pre><code>$ age-keygen -pq -o key.txt
Public key: age1pq1cd[... 1950 more characters ...]
</code></pre>
<p>Convert an identity to a recipient:</p>
<pre><code>$ age-keygen -y key.txt
age1pq1cd[... 1950 more characters ...]
</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>, <a class="man-ref" href="age-inspect.1.html">age-inspect<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'>December 2025</li>
<li class='tr'>age-keygen(1)</li>
</ol>
</div>
</body>
</html>

69
doc/age-keygen.1.ronn Normal file
View File

@@ -0,0 +1,69 @@
age-keygen(1) -- generate age(1) key pairs
====================================================
## SYNOPSIS
`age-keygen` [`-pq`] [`-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
* `-pq`:
Generate a post-quantum hybrid ML-KEM-768 + X25519 key pair.
In the future, this might become the default.
* `-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 post-quantum identity:
$ age-keygen -pq
# created: 2025-11-17T13:39:06+01:00
# public key: age1pq167[... 1950 more characters ...]
AGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T
Generate a new traditional identity:
$ age-keygen
# created: 2021-01-02T15:30:45+01:00
# public key: age1lvyvwawkr0mcnnnncaghunadrqkmuf9e6507x9y920xxpp866cnql7dp2z
AGE-SECRET-KEY-1N9JEPW6DWJ0ZQUDX63F5A03GX8QUW7PXDE39N8UYF82VZ9PC8UFS3M7XA9
Write a new post-quantum identity to `key.txt`:
$ age-keygen -pq -o key.txt
Public key: age1pq1cd[... 1950 more characters ...]
Convert an identity to a recipient:
$ age-keygen -y key.txt
age1pq1cd[... 1950 more characters ...]
## SEE ALSO
age(1), age-inspect(1)
## AUTHORS
Filippo Valsorda <age@filippo.io>

View File

@@ -0,0 +1,87 @@
.\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "AGE\-PLUGIN\-BATCHPASS" "1" "December 2025" ""
.SH "NAME"
\fBage\-plugin\-batchpass\fR \- non\-interactive passphrase encryption plugin for age(1)
.SH "SYNOPSIS"
\fBage\fR \fB\-e\fR \fB\-j\fR \fBbatchpass\fR
.br
\fBage\fR \fB\-d\fR \fB\-j\fR \fBbatchpass\fR
.br
.SH "DESCRIPTION"
\fBage\-plugin\-batchpass\fR is an age(1) plugin that enables non\-interactive passphrase\-based encryption and decryption using environment variables\.
.SH "WARNING"
This functionality is not built into the age CLI because most applications should use native keys instead of scripting passphrase\-based encryption\.
.P
Humans are notoriously bad at remembering and generating strong passphrases\. age uses scrypt to partially mitigate this, which is necessarily very slow\.
.P
If a computer will be doing the remembering anyway, you can and should use native keys instead\. There is no need to manage separate public and private keys, you encrypt directly to the private key:
.IP "" 4
.nf
$ age\-keygen \-o key\.txt
$ age \-e \-i key\.txt file\.txt > file\.txt\.age
$ age \-d \-i key\.txt file\.txt\.age > file\.txt
.fi
.IP "" 0
.P
Likewise, you can store a native identity string in an environment variable or through your CI secrets manager and use it to encrypt and decrypt files non\-interactively:
.IP "" 4
.nf
$ export AGE_SECRET=$(age\-keygen)
$ age \-e \-i <(echo "$AGE_SECRET") file\.txt > file\.txt\.age
$ age \-d \-i <(echo "$AGE_SECRET") file\.txt\.age > file\.txt
.fi
.IP "" 0
.P
The age CLI also natively supports passphrase\-encrypted identity files, so you can use that functionality to non\-interactively encrypt multiple files such that you will be able to decrypt them later by entering the same passphrase:
.IP "" 4
.nf
$ age\-keygen \-pq | age \-p \-o encrypted\-identity\.txt
Public key: age1pq1cd[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.]
Enter passphrase (leave empty to autogenerate a secure one):
age: using autogenerated passphrase "eternal\-erase\-keen\-suffer\-fog\-exclude\-huge\-scorpion\-escape\-scrub"
$ age \-r age1pq1cd[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.] file\.txt > file\.txt\.age
$ age \-d \-i encrypted\-identity\.txt file\.txt\.age > file\.txt
Enter passphrase for identity file "encrypted\-identity\.txt":
.fi
.IP "" 0
.P
Finally, when using this plugin care should be taken not to let the password be persisted in the shell history or leaked to other users on multi\-user systems\.
.SH "ENVIRONMENT"
.TP
\fBAGE_PASSPHRASE\fR
The passphrase to use for encryption or decryption\. Mutually exclusive with \fBAGE_PASSPHRASE_FD\fR\.
.TP
\fBAGE_PASSPHRASE_FD\fR
A file descriptor number to read the passphrase from\. Trailing newlines are stripped from the file contents\. Mutually exclusive with \fBAGE_PASSPHRASE\fR\.
.TP
\fBAGE_PASSPHRASE_WORK_FACTOR\fR
The scrypt work factor to use when encrypting\. Must be between 1 and 30\. Default is 18\. Higher values are more secure but slower\.
.TP
\fBAGE_PASSPHRASE_MAX_WORK_FACTOR\fR
The maximum scrypt work factor to accept when decrypting\. Must be between 1 and 30\. Default is 30\. Can be used to avoid very slow decryptions\.
.SH "EXAMPLES"
Encrypt a file with a passphrase:
.IP "" 4
.nf
$ AGE_PASSPHRASE=secret age \-e \-j batchpass file\.txt > file\.txt\.age
.fi
.IP "" 0
.P
Decrypt a file with a passphrase:
.IP "" 4
.nf
$ AGE_PASSPHRASE=secret age \-d \-j batchpass file\.txt\.age > file\.txt
.fi
.IP "" 0
.P
Read the passphrase from a file descriptor:
.IP "" 4
.nf
$ AGE_PASSPHRASE_FD=3 age \-e \-j batchpass file\.txt 3< passphrase\.txt > file\.txt\.age
.fi
.IP "" 0
.SH "SEE ALSO"
age(1)
.SH "AUTHORS"
Filippo Valsorda \fIage@filippo\.io\fR

View File

@@ -0,0 +1,183 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' content='text/html;charset=utf8'>
<meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>
<title>age-plugin-batchpass(1) - non-interactive passphrase encryption plugin for age(1)</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="#WARNING">WARNING</a>
<a href="#ENVIRONMENT">ENVIRONMENT</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-plugin-batchpass(1)</li>
<li class='tc'></li>
<li class='tr'>age-plugin-batchpass(1)</li>
</ol>
<h2 id="NAME">NAME</h2>
<p class="man-name">
<code>age-plugin-batchpass</code> - <span class="man-whatis">non-interactive passphrase encryption plugin for <a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a></span>
</p>
<h2 id="SYNOPSIS">SYNOPSIS</h2>
<p><code>age</code> <code>-e</code> <code>-j</code> <code>batchpass</code><br>
<code>age</code> <code>-d</code> <code>-j</code> <code>batchpass</code><br></p>
<h2 id="DESCRIPTION">DESCRIPTION</h2>
<p><code>age-plugin-batchpass</code> is an <a class="man-ref" href="age.1.html">age<span class="s">(1)</span></a> plugin that enables non-interactive
passphrase-based encryption and decryption using environment variables.</p>
<h2 id="WARNING">WARNING</h2>
<p>This functionality is not built into the age CLI because most applications
should use native keys instead of scripting passphrase-based encryption.</p>
<p>Humans are notoriously bad at remembering and generating strong passphrases.
age uses scrypt to partially mitigate this, which is necessarily very slow.</p>
<p>If a computer will be doing the remembering anyway, you can and should use
native keys instead. There is no need to manage separate public and private
keys, you encrypt directly to the private key:</p>
<pre><code>$ age-keygen -o key.txt
$ age -e -i key.txt file.txt &gt; file.txt.age
$ age -d -i key.txt file.txt.age &gt; file.txt
</code></pre>
<p>Likewise, you can store a native identity string in an environment variable
or through your CI secrets manager and use it to encrypt and decrypt files
non-interactively:</p>
<pre><code>$ export AGE_SECRET=$(age-keygen)
$ age -e -i &lt;(echo "$AGE_SECRET") file.txt &gt; file.txt.age
$ age -d -i &lt;(echo "$AGE_SECRET") file.txt.age &gt; file.txt
</code></pre>
<p>The age CLI also natively supports passphrase-encrypted identity files, so you
can use that functionality to non-interactively encrypt multiple files such that
you will be able to decrypt them later by entering the same passphrase:</p>
<pre><code>$ age-keygen -pq | age -p -o encrypted-identity.txt
Public key: age1pq1cd[... 1950 more characters ...]
Enter passphrase (leave empty to autogenerate a secure one):
age: using autogenerated passphrase "eternal-erase-keen-suffer-fog-exclude-huge-scorpion-escape-scrub"
$ age -r age1pq1cd[... 1950 more characters ...] file.txt &gt; file.txt.age
$ age -d -i encrypted-identity.txt file.txt.age &gt; file.txt
Enter passphrase for identity file "encrypted-identity.txt":
</code></pre>
<p>Finally, when using this plugin care should be taken not to let the password be
persisted in the shell history or leaked to other users on multi-user systems.</p>
<h2 id="ENVIRONMENT">ENVIRONMENT</h2>
<dl>
<dt><code>AGE_PASSPHRASE</code></dt>
<dd> The passphrase to use for encryption or decryption.
Mutually exclusive with <code>AGE_PASSPHRASE_FD</code>.</dd>
<dt><code>AGE_PASSPHRASE_FD</code></dt>
<dd> A file descriptor number to read the passphrase from.
Trailing newlines are stripped from the file contents.
Mutually exclusive with <code>AGE_PASSPHRASE</code>.</dd>
<dt><code>AGE_PASSPHRASE_WORK_FACTOR</code></dt>
<dd> The scrypt work factor to use when encrypting.
Must be between 1 and 30. Default is 18.
Higher values are more secure but slower.</dd>
<dt><code>AGE_PASSPHRASE_MAX_WORK_FACTOR</code></dt>
<dd> The maximum scrypt work factor to accept when decrypting.
Must be between 1 and 30. Default is 30.
Can be used to avoid very slow decryptions.</dd>
</dl>
<h2 id="EXAMPLES">EXAMPLES</h2>
<p>Encrypt a file with a passphrase:</p>
<pre><code>$ AGE_PASSPHRASE=secret age -e -j batchpass file.txt &gt; file.txt.age
</code></pre>
<p>Decrypt a file with a passphrase:</p>
<pre><code>$ AGE_PASSPHRASE=secret age -d -j batchpass file.txt.age &gt; file.txt
</code></pre>
<p>Read the passphrase from a file descriptor:</p>
<pre><code>$ AGE_PASSPHRASE_FD=3 age -e -j batchpass file.txt 3&lt; passphrase.txt &gt; file.txt.age
</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'>December 2025</li>
<li class='tr'>age-plugin-batchpass(1)</li>
</ol>
</div>
</body>
</html>

View File

@@ -0,0 +1,94 @@
age-plugin-batchpass(1) -- non-interactive passphrase encryption plugin for age(1)
==================================================================================
## SYNOPSIS
`age` `-e` `-j` `batchpass`<br>
`age` `-d` `-j` `batchpass`<br>
## DESCRIPTION
`age-plugin-batchpass` is an age(1) plugin that enables non-interactive
passphrase-based encryption and decryption using environment variables.
## WARNING
This functionality is not built into the age CLI because most applications
should use native keys instead of scripting passphrase-based encryption.
Humans are notoriously bad at remembering and generating strong passphrases.
age uses scrypt to partially mitigate this, which is necessarily very slow.
If a computer will be doing the remembering anyway, you can and should use
native keys instead. There is no need to manage separate public and private
keys, you encrypt directly to the private key:
$ age-keygen -o key.txt
$ age -e -i key.txt file.txt > file.txt.age
$ age -d -i key.txt file.txt.age > file.txt
Likewise, you can store a native identity string in an environment variable
or through your CI secrets manager and use it to encrypt and decrypt files
non-interactively:
$ export AGE_SECRET=$(age-keygen)
$ age -e -i <(echo "$AGE_SECRET") file.txt > file.txt.age
$ age -d -i <(echo "$AGE_SECRET") file.txt.age > file.txt
The age CLI also natively supports passphrase-encrypted identity files, so you
can use that functionality to non-interactively encrypt multiple files such that
you will be able to decrypt them later by entering the same passphrase:
$ age-keygen -pq | age -p -o encrypted-identity.txt
Public key: age1pq1cd[... 1950 more characters ...]
Enter passphrase (leave empty to autogenerate a secure one):
age: using autogenerated passphrase "eternal-erase-keen-suffer-fog-exclude-huge-scorpion-escape-scrub"
$ age -r age1pq1cd[... 1950 more characters ...] file.txt > file.txt.age
$ age -d -i encrypted-identity.txt file.txt.age > file.txt
Enter passphrase for identity file "encrypted-identity.txt":
Finally, when using this plugin care should be taken not to let the password be
persisted in the shell history or leaked to other users on multi-user systems.
## ENVIRONMENT
* `AGE_PASSPHRASE`:
The passphrase to use for encryption or decryption.
Mutually exclusive with `AGE_PASSPHRASE_FD`.
* `AGE_PASSPHRASE_FD`:
A file descriptor number to read the passphrase from.
Trailing newlines are stripped from the file contents.
Mutually exclusive with `AGE_PASSPHRASE`.
* `AGE_PASSPHRASE_WORK_FACTOR`:
The scrypt work factor to use when encrypting.
Must be between 1 and 30. Default is 18.
Higher values are more secure but slower.
* `AGE_PASSPHRASE_MAX_WORK_FACTOR`:
The maximum scrypt work factor to accept when decrypting.
Must be between 1 and 30. Default is 30.
Can be used to avoid very slow decryptions.
## EXAMPLES
Encrypt a file with a passphrase:
$ AGE_PASSPHRASE=secret age -e -j batchpass file.txt > file.txt.age
Decrypt a file with a passphrase:
$ AGE_PASSPHRASE=secret age -d -j batchpass file.txt.age > file.txt
Read the passphrase from a file descriptor:
$ AGE_PASSPHRASE_FD=3 age -e -j batchpass file.txt 3< passphrase.txt > file.txt.age
## SEE ALSO
age(1)
## AUTHORS
Filippo Valsorda <age@filippo.io>

269
doc/age.1 Normal file
View File

@@ -0,0 +1,269 @@
.\" generated with Ronn-NG/v0.9.1
.\" http://github.com/apjanke/ronn-ng/tree/0.9.1
.TH "AGE" "1" "December 2025" ""
.SH "NAME"
\fBage\fR \- simple, modern, and secure file encryption
.SH "SYNOPSIS"
\fBage\fR [\fB\-\-encrypt\fR] (\fB\-r\fR \fIRECIPIENT\fR | \fB\-R\fR \fIPATH\fR)\|\.\|\.\|\. [\fB\-\-armor\fR] [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.br
\fBage\fR [\fB\-\-encrypt\fR] \fB\-\-passphrase\fR [\fB\-\-armor\fR] [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.br
\fBage\fR \fB\-\-decrypt\fR [\fB\-i\fR \fIPATH\fR | \fB\-j\fR \fIPLUGIN\fR]\|\.\|\.\|\. [\fB\-o\fR \fIOUTPUT\fR] [\fIINPUT\fR]
.br
.SH "DESCRIPTION"
\fBage\fR encrypts or decrypts \fIINPUT\fR to \fIOUTPUT\fR\. The \fIINPUT\fR argument is optional and defaults to standard input\. Only a single \fIINPUT\fR file may be specified\. If \fB\-o\fR is not specified, \fIOUTPUT\fR defaults to standard output\.
.P
If \fB\-p\fR/\fB\-\-passphrase\fR is specified, the file is encrypted with a passphrase requested interactively\. Otherwise, it's encrypted to one or more \fIRECIPIENTS\fR specified with \fB\-r\fR/\fB\-\-recipient\fR or \fB\-R\fR/\fB\-\-recipients\-file\fR\. Every recipient can decrypt the file\.
.P
In \fB\-d\fR/\fB\-\-decrypt\fR mode, passphrase\-encrypted files are detected automatically and the passphrase is requested interactively\. Otherwise, one or more \fIIDENTITIES\fR specified with \fB\-i\fR/\fB\-\-identity\fR are used to decrypt the file\.
.P
\fBage\fR encrypted files are binary and not malleable, with around 200 bytes of overhead per recipient, plus 16 bytes every 64KiB of plaintext\.
.SH "OPTIONS"
.TP
\fB\-o\fR, \fB\-\-output\fR=\fIOUTPUT\fR
Write encrypted or decrypted file to \fIOUTPUT\fR instead of standard output\. If \fIOUTPUT\fR already exists it will be overwritten\.
.IP
If encrypting without \fB\-\-armor\fR, \fBage\fR will refuse to output binary to a TTY\. This can be forced by specifying \fB\-\fR as \fIOUTPUT\fR\.
.TP
\fB\-\-version\fR
Print the version and exit\.
.SS "Encryption options"
.TP
\fB\-e\fR, \fB\-\-encrypt\fR
Encrypt \fIINPUT\fR to \fIOUTPUT\fR\. This is the default\.
.TP
\fB\-r\fR, \fB\-\-recipient\fR=\fIRECIPIENT\fR
Encrypt to the explicitly specified \fIRECIPIENT\fR\. See the \fIRECIPIENTS AND IDENTITIES\fR section for possible recipient formats\.
.IP
This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently\.
.TP
\fB\-R\fR, \fB\-\-recipients\-file\fR=\fIPATH\fR
Encrypt to the \fIRECIPIENTS\fR listed in the file at \fIPATH\fR, one per line\. Empty lines and lines starting with \fB#\fR are ignored as comments\.
.IP
If \fIPATH\fR is \fB\-\fR, the recipients are read from standard input\. In this case, the \fIINPUT\fR argument must be specified\.
.IP
This option can be repeated and combined with other recipient flags, and the file can be decrypted by all provided recipients independently\.
.TP
\fB\-p\fR, \fB\-\-passphrase\fR
Encrypt with a passphrase, requested interactively from the terminal\. \fBage\fR will offer to auto\-generate a secure passphrase\.
.IP
This option can't be used with other recipient flags\.
.TP
\fB\-a\fR, \fB\-\-armor\fR
Encrypt to an ASCII\-only "armored" encoding\.
.IP
\fBage\fR armor is a strict version of PEM with type \fBAGE ENCRYPTED FILE\fR, canonical "strict" Base64, no headers, and no support for leading and trailing extra data\.
.IP
Decryption transparently detects and decodes ASCII armoring\.
.TP
\fB\-i\fR, \fB\-\-identity\fR=\fIPATH\fR
Encrypt to the \fIRECIPIENTS\fR corresponding to the \fIIDENTITIES\fR listed in the file at \fIPATH\fR\. This is equivalent to converting the file at \fIPATH\fR to a recipients file with \fBage\-keygen \-y\fR and then passing that to \fB\-R\fR/\fB\-\-recipients\-file\fR\.
.IP
For the format of \fIPATH\fR, see the definition of \fB\-i\fR/\fB\-\-identity\fR in the \fIDecryption options\fR section\.
.IP
\fB\-e\fR/\fB\-\-encrypt\fR must be explicitly specified when using \fB\-i\fR/\fB\-\-identity\fR in encryption mode to avoid confusion\.
.TP
\fB\-j\fR \fIPLUGIN\fR
Encrypt using the data\-less \fIplugin\fR \fIPLUGIN\fR\.
.IP
This is equivalent to using \fB\-i\fR/\fB\-\-identity\fR with a file that contains a single plugin \fBIDENTITY\fR that encodes no plugin\-specific data\.
.IP
\fB\-e\fR/\fB\-\-encrypt\fR must be explicitly specified when using \fB\-j\fR in encryption mode to avoid confusion\.
.SS "Decryption options"
.TP
\fB\-d\fR, \fB\-\-decrypt\fR
Decrypt \fIINPUT\fR to \fIOUTPUT\fR\.
.IP
If \fIINPUT\fR is passphrase encrypted, it will be automatically detected and the passphrase will be requested interactively\. Otherwise, the \fIIDENTITIES\fR specified with \fB\-i\fR/\fB\-\-identity\fR are used\.
.IP
ASCII armoring is transparently detected and decoded\.
.TP
\fB\-i\fR, \fB\-\-identity\fR=\fIPATH\fR
Decrypt using the \fIIDENTITIES\fR at \fIPATH\fR\.
.IP
\fIPATH\fR may be one of the following:
.IP
a\. A file listing \fIIDENTITIES\fR one per line\. Empty lines and lines starting with "\fB#\fR" are ignored as comments\.
.IP
b\. A passphrase encrypted age file, containing \fIIDENTITIES\fR one per line like above\. The passphrase is requested interactively\. Note that passphrase\-protected identity files are not necessary for most use cases, where access to the encrypted identity file implies access to the whole system\.
.IP
c\. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format\. If the private key is password\-protected, the password is requested interactively only if the SSH identity matches the file\. See the \fISSH keys\fR section for more information, including supported key types\.
.IP
d\. "\fB\-\fR", causing one of the options above to be read from standard input\. In this case, the \fIINPUT\fR argument must be specified\.
.IP
This option can be repeated\. Identities are tried in the order in which are provided, and the first one matching one of the file's recipients is used\. Unused identities are ignored, but it is an error if the \fIINPUT\fR file is passphrase\-encrypted and \fB\-i\fR/\fB\-\-identity\fR is specified\.
.TP
\fB\-j\fR \fIPLUGIN\fR
Decrypt using the data\-less \fIplugin\fR \fIPLUGIN\fR\.
.IP
This is equivalent to using \fB\-i\fR/\fB\-\-identity\fR with a file that contains a single plugin \fBIDENTITY\fR that encodes no plugin\-specific data\.
.SH "RECIPIENTS AND IDENTITIES"
\fBRECIPIENTS\fR are public values, like a public key, that a file can be encrypted to\. \fBIDENTITIES\fR are private values, like a private key, that allow decrypting a file encrypted to the corresponding \fBRECIPIENT\fR\.
.SS "Native keys"
Native \fBage\fR key pairs are generated with age\-keygen(1), and provide small encodings and strong encryption based on X25519 for classic keys, and X25519 + ML\-KEM\-768 for post\-quantum hybrid keys\. The post\-quantum hybrid keys are secure against future quantum computers and are the recommended recipient type for most applications\.
.P
A hybrid \fBRECIPIENT\fR encoding begins with \fBage1pq1\fR and looks like the following:
.IP "" 4
.nf
age1pq167[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.]
.fi
.IP "" 0
.P
A hybrid \fBIDENTITY\fR encoding begins with \fBAGE\-SECRET\-KEY\-PQ\-1\fR and looks like the following:
.IP "" 4
.nf
AGE\-SECRET\-KEY\-PQ\-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T
.fi
.IP "" 0
.P
A classic \fBRECIPIENT\fR encoding begins with \fBage1\fR and looks like the following:
.IP "" 4
.nf
age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh
.fi
.IP "" 0
.P
A classic \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
A file can't be encrypted to both post\-quantum and classic keys, as that would defeat the post\-quantum security of the encryption\.
.P
An encrypted file can't be linked to the native recipient it's encrypted to without access to the corresponding identity\.
.SS "SSH keys"
As a convenience feature, \fBage\fR also supports encrypting to RSA or Ed25519 ssh(1) keys\. RSA keys must be at least 2048 bits\. This feature employs more complex cryptography, and should only be used when a native key is not available for the recipient\. Note that SSH keys might not be protected long\-term by the recipient, since they are revokable when used only for authentication\.
.P
A \fBRECIPIENT\fR encoding is an SSH public key in \fBauthorized_keys\fR format (see the \fBAUTHORIZED_KEYS FILE FORMAT\fR section of sshd(8)), starting with \fBssh\-rsa\fR or \fBssh\-ed25519\fR, like the following:
.IP "" 4
.nf
ssh\-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[\|\.\|\.\|\.]GU4BtElAbzh8=
ssh\-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[\|\.\|\.\|\.]l1uZc31FGYMXa
.fi
.IP "" 0
.P
The comment at the end of the line, if present, is ignored\.
.P
In recipient files passed to \fB\-R\fR/\fB\-\-recipients\-file\fR, unsupported but valid SSH public keys are ignored with a warning, to facilitate using \fBauthorized_keys\fR or GitHub \fB\.keys\fR files\. (See \fIEXAMPLES\fR\.)
.P
An \fBIDENTITY\fR is an SSH private key \fIfile\fR passed individually to \fB\-i\fR/\fB\-\-identity\fR\. Note that keys held on hardware tokens such as YubiKeys or accessed via ssh\-agent(1) are not supported\.
.P
An encrypted file \fIcan\fR be linked to the SSH public key it was encrypted to\. This is so that \fBage\fR can identify the correct SSH private key before requesting its password, if any\.
.SS "Plugins"
\fBage\fR can be extended through plugins\. A plugin is only loaded if a corresponding \fBRECIPIENT\fR or \fBIDENTITY\fR is specified\. (Simply decrypting a file encrypted with a plugin will not cause it to load, for security reasons among others\.)
.P
A \fBRECIPIENT\fR for a plugin named \fBexample\fR starts with \fBage1example1\fR, while an \fBIDENTITY\fR starts with \fBAGE\-PLUGIN\-EXAMPLE\-1\fR\. They both encode arbitrary plugin\-specific data, and are generated by the plugin\.
.P
When either is specified, \fBage\fR searches for \fBage\-plugin\-example\fR in the PATH and executes it to perform the file header encryption or decryption\. The plugin may request input from the user through \fBage\fR to complete the operation\.
.P
Plugins can be freely mixed with other plugins or natively supported keys\.
.P
A plugin is not bound to only encrypt or decrypt files meant for or generated by the plugin\. For example, a plugin can be used to decrypt files encrypted to a native X25519 \fBRECIPIENT\fR or even with a passphrase\. Similarly, a plugin can encrypt a file such that it can be decrypted without the use of any plugin\.
.P
Plugins for which the \fBIDENTITY\fR/\fBRECIPIENT\fR distinction doesn't make sense (such as a symmetric encryption plugin) may generate only an \fBIDENTITY\fR and instruct the user to perform encryption with the \fB\-e\fR/\fB\-\-encrypt\fR and \fB\-i\fR/\fB\-\-identity\fR flags\. Plugins for which the concept of separate identities doesn't make sense (such as a password\-encryption plugin) may instruct the user to use the \fB\-j\fR flag\.
.P
\fBage\fR can natively encrypt to recipients starting with \fBage1tag1\fR (using P\-256 ECDH) or \fBage1tagpq1\fR (using the ML\-KEM\-768 + P\-256 post\-quantum hybrid)\. These are intended to be the public side of private keys held in hardware\.
.P
They are directly supported to avoid the need to install the plugin, which may be platform\-specific, on the encrypting side\.
.P
The tag reduces privacy, by allowing an observer to correlate files with a recipient (but not files amongst them without knowledge of the recipient), but this is also a desirable property for hardware keys that require user interaction for each decryption operation\.
.SH "EXIT STATUS"
\fBage\fR will exit 0 if and only if encryption or decryption are successful for the full length of the input\.
.P
If an error occurs during decryption, partial output might still be generated, but only if it was possible to securely authenticate it\. No unauthenticated output is ever released\.
.SH "BACKWARDS COMPATIBILITY"
Files encrypted with a stable version (not alpha, beta, or release candidate) of \fBage\fR, or with any v1\.0\.0 beta or release candidate, will decrypt with any later version of the tool\.
.P
If decrypting older files poses a security risk, doing so might cause an error by default\. In this case, a flag will be provided to force the operation\.
.SH "EXAMPLES"
Generate a new post\-quantum identity, encrypt data, and decrypt:
.IP "" 4
.nf
$ age\-keygen \-pq \-o key\.txt
Public key: age1pq167[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.]
$ tar cvz ~/data | age \-r age1pq167[\|\.\|\.\|\.] > 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 age1pq167[\|\.\|\.\|\.] \-r age1pq1e3[\|\.\|\.\|\.] example\.jpg
.fi
.IP "" 0
.P
Encrypt to a list of recipients:
.IP "" 4
.nf
$ cat > recipients\.txt
# Alice
age1pq167[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.]
# Bob
age1pq1e3[\|\.\|\.\|\. 1950 more characters \|\.\|\.\|\.]
$ age \-R recipients\.txt example\.jpg > example\.jpg\.age
.fi
.IP "" 0
.P
Encrypt and decrypt a file using a passphrase:
.IP "" 4
.nf
$ age \-p secrets\.txt > secrets\.txt\.age
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "release\-response\-step\-brand\-wrap\-ankle\-pair\-unusual\-sword\-train"\.
$ age \-d secrets\.txt\.age > secrets\.txt
Enter passphrase:
.fi
.IP "" 0
.P
Encrypt and decrypt with a passphrase\-protected identity file:
.IP "" 4
.nf
$ age\-keygen | age \-p > key\.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip\-roast\-boring\-snake\-mention\-east\-wasp\-honey\-input\-actress"\.
$ age \-r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets\.txt > secrets\.txt\.age
$ age \-d \-i key\.age secrets\.txt\.age > secrets\.txt
Enter passphrase for identity file "key\.age":
.fi
.IP "" 0
.P
Encrypt and decrypt with an SSH public key:
.IP "" 4
.nf
$ age \-R ~/\.ssh/id_ed25519\.pub example\.jpg > example\.jpg\.age
$ age \-d \-i ~/\.ssh/id_ed25519 example\.jpg\.age > example\.jpg
.fi
.IP "" 0
.P
Encrypt and decrypt with age\-plugin\-yubikey:
.IP "" 4
.nf
$ age\-plugin\-yubikey # run interactive setup, generate identity file and obtain recipient
$ age \-r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets\.txt > secrets\.txt\.age
$ age \-d \-i age\-yubikey\-identity\-388178f3\.txt secrets\.txt\.age
.fi
.IP "" 0
.P
Encrypt to the SSH keys of a GitHub user:
.IP "" 4
.nf
$ curl https://github\.com/benjojo\.keys | age \-R \- example\.jpg > example\.jpg\.age
.fi
.IP "" 0
.SH "SEE ALSO"
age\-keygen(1), age\-inspect(1)
.SH "AUTHORS"
Filippo Valsorda \fIage@filippo\.io\fR

470
doc/age.1.html Normal file
View File

@@ -0,0 +1,470 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' content='text/html;charset=utf8'>
<meta name='generator' content='Ronn-NG/v0.9.1 (http://github.com/apjanke/ronn-ng/tree/0.9.1)'>
<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>-j</code> <var>PLUGIN</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>-p</code>/<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>-d</code>/<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> Write encrypted or decrypted file to <var>OUTPUT</var> instead of standard output.
If <var>OUTPUT</var> already exists it will be overwritten.
<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> Print the version and exit.</dd>
</dl>
<h3 id="Encryption-options">Encryption options</h3>
<dl>
<dt>
<code>-e</code>, <code>--encrypt</code>
</dt>
<dd> Encrypt <var>INPUT</var> to <var>OUTPUT</var>. This is the default.</dd>
<dt>
<code>-r</code>, <code>--recipient</code>=<var>RECIPIENT</var>
</dt>
<dd> 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>This option can be repeated and combined with other recipient flags,
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> 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>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 other recipient flags,
and the file can be decrypted by all provided recipients independently.</p>
</dd>
<dt>
<code>-p</code>, <code>--passphrase</code>
</dt>
<dd> Encrypt with a passphrase, requested interactively from the terminal.
<code>age</code> will offer to auto-generate a secure passphrase.
<p>This option can't be used with other recipient flags.</p>
</dd>
<dt>
<code>-a</code>, <code>--armor</code>
</dt>
<dd> Encrypt to an ASCII-only "armored" encoding.
<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>
<dt>
<code>-i</code>, <code>--identity</code>=<var>PATH</var>
</dt>
<dd> Encrypt to the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">RECIPIENTS</a> corresponding to the
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> listed in the file at <var>PATH</var>. This
is equivalent to converting the file at <var>PATH</var> to a recipients file with
<code>age-keygen -y</code> and then passing that to <code>-R</code>/<code>--recipients-file</code>.
<p>For the format of <var>PATH</var>, see the definition of <code>-i</code>/<code>--identity</code> in the
<a href="#Decryption-options" title="Decryption options" data-bare-link="true">Decryption options</a> section.</p>
<p><code>-e</code>/<code>--encrypt</code> must be explicitly specified when using <code>-i</code>/<code>--identity</code>
in encryption mode to avoid confusion.</p>
</dd>
<dt>
<code>-j</code> <var>PLUGIN</var>
</dt>
<dd> Encrypt using the data-less <a href="#Plugins" title="Plugins" data-bare-link="true">plugin</a> <var>PLUGIN</var>.
<p>This is equivalent to using <code>-i</code>/<code>--identity</code> with a file that contains a
single plugin <code>IDENTITY</code> that encodes no plugin-specific data.</p>
<p><code>-e</code>/<code>--encrypt</code> must be explicitly specified when using <code>-j</code> in encryption
mode to avoid confusion.</p>
</dd>
</dl>
<h3 id="Decryption-options">Decryption options</h3>
<dl>
<dt>
<code>-d</code>, <code>--decrypt</code>
</dt>
<dd> Decrypt <var>INPUT</var> to <var>OUTPUT</var>.
<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> Decrypt using the <a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> at <var>PATH</var>.
<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. A passphrase encrypted age file, containing
<a href="#RECIPIENTS-AND-IDENTITIES" title="RECIPIENTS AND IDENTITIES" data-bare-link="true">IDENTITIES</a> one per line like above.
The passphrase is requested interactively. Note that passphrase-protected
identity files are not necessary for most use cases, where access to the
encrypted identity file implies access to the whole system.</p>
<p>c. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.
If the private key is password-protected, the password is requested
interactively only if the SSH identity matches the file. See the
<a href="#SSH-keys" title="SSH keys" data-bare-link="true">SSH keys</a> section for more information, including supported key types.</p>
<p>d. "<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, but it is an error if the <var>INPUT</var> file is
passphrase-encrypted and <code>-i</code>/<code>--identity</code> is specified.</p>
</dd>
<dt>
<code>-j</code> <var>PLUGIN</var>
</dt>
<dd> Decrypt using the data-less <a href="#Plugins" title="Plugins" data-bare-link="true">plugin</a> <var>PLUGIN</var>.
<p>This is equivalent to using <code>-i</code>/<code>--identity</code> with a file that contains a
single plugin <code>IDENTITY</code> that encodes no plugin-specific data.</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-keys">Native 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 for classic keys, and X25519 +
ML-KEM-768 for post-quantum hybrid keys. The post-quantum hybrid keys are secure
against future quantum computers and are the recommended recipient type for most
applications.</p>
<p>A hybrid <code>RECIPIENT</code> encoding begins with <code>age1pq1</code> and looks like the following:</p>
<pre><code>age1pq167[... 1950 more characters ...]
</code></pre>
<p>A hybrid <code>IDENTITY</code> encoding begins with <code>AGE-SECRET-KEY-PQ-1</code> and looks like
the following:</p>
<pre><code>AGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T
</code></pre>
<p>A classic <code>RECIPIENT</code> encoding begins with <code>age1</code> and looks like the following:</p>
<pre><code>age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh
</code></pre>
<p>A classic <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>A file can't be encrypted to both post-quantum and classic keys, as that would
defeat the post-quantum security of the encryption.</p>
<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>
<h3 id="Plugins">Plugins</h3>
<p><code>age</code> can be extended through plugins. A plugin is only loaded if a corresponding
<code>RECIPIENT</code> or <code>IDENTITY</code> is specified. (Simply decrypting a file encrypted with
a plugin will not cause it to load, for security reasons among others.)</p>
<p>A <code>RECIPIENT</code> for a plugin named <code>example</code> starts with <code>age1example1</code>, while an
<code>IDENTITY</code> starts with <code>AGE-PLUGIN-EXAMPLE-1</code>. They both encode arbitrary
plugin-specific data, and are generated by the plugin.</p>
<p>When either is specified, <code>age</code> searches for <code>age-plugin-example</code> in the PATH
and executes it to perform the file header encryption or decryption. The plugin
may request input from the user through <code>age</code> to complete the operation.</p>
<p>Plugins can be freely mixed with other plugins or natively supported keys.</p>
<p>A plugin is not bound to only encrypt or decrypt files meant for or generated by
the plugin. For example, a plugin can be used to decrypt files encrypted to a
native X25519 <code>RECIPIENT</code> or even with a passphrase. Similarly, a plugin can
encrypt a file such that it can be decrypted without the use of any plugin.</p>
<p>Plugins for which the <code>IDENTITY</code>/<code>RECIPIENT</code> distinction doesn't make sense
(such as a symmetric encryption plugin) may generate only an <code>IDENTITY</code> and
instruct the user to perform encryption with the <code>-e</code>/<code>--encrypt</code> and
<code>-i</code>/<code>--identity</code> flags. Plugins for which the concept of separate identities
doesn't make sense (such as a password-encryption plugin) may instruct the user
to use the <code>-j</code> flag.</p>
<h4 id="Tagged-recipients">Tagged recipients</h4>
<p><code>age</code> can natively encrypt to recipients starting with <code>age1tag1</code> (using P-256
ECDH) or <code>age1tagpq1</code> (using the ML-KEM-768 + P-256 post-quantum hybrid). These
are intended to be the public side of private keys held in hardware.</p>
<p>They are directly supported to avoid the need to install the plugin, which may
be platform-specific, on the encrypting side.</p>
<p>The tag reduces privacy, by allowing an observer to correlate files with a
recipient (but not files amongst them without knowledge of the recipient),
but this is also a desirable property for hardware keys that require user
interaction for each decryption operation.</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 unauthenticated
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 post-quantum identity, encrypt data, and decrypt:</p>
<pre><code>$ age-keygen -pq -o key.txt
Public key: age1pq167[... 1950 more characters ...]
$ tar cvz ~/data | age -r age1pq167[...] &gt; 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 age1pq167[...] -r age1pq1e3[...] example.jpg
</code></pre>
<p>Encrypt to a list of recipients:</p>
<pre><code>$ cat &gt; recipients.txt
# Alice
age1pq167[... 1950 more characters ...]
# Bob
age1pq1e3[... 1950 more characters ...]
$ age -R recipients.txt example.jpg &gt; example.jpg.age
</code></pre>
<p>Encrypt and decrypt a file using a passphrase:</p>
<pre><code>$ age -p secrets.txt &gt; 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 &gt; secrets.txt
Enter passphrase:
</code></pre>
<p>Encrypt and decrypt with a passphrase-protected identity file:</p>
<pre><code>$ age-keygen | age -p &gt; key.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt &gt; secrets.txt.age
$ age -d -i key.age secrets.txt.age &gt; secrets.txt
Enter passphrase for identity file "key.age":
</code></pre>
<p>Encrypt and decrypt with an SSH public key:</p>
<pre><code>$ age -R ~/.ssh/id_ed25519.pub example.jpg &gt; example.jpg.age
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age &gt; example.jpg
</code></pre>
<p>Encrypt and decrypt with age-plugin-yubikey:</p>
<pre><code>$ age-plugin-yubikey # run interactive setup, generate identity file and obtain recipient
$ age -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets.txt &gt; secrets.txt.age
$ age -d -i age-yubikey-identity-388178f3.txt secrets.txt.age
</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 &gt; 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>, <a class="man-ref" href="age-inspect.1.html">age-inspect<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'>December 2025</li>
<li class='tr'>age(1)</li>
</ol>
</div>
</body>
</html>

342
doc/age.1.ronn Normal file
View File

@@ -0,0 +1,342 @@
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> | `-j` <PLUGIN>]... [`-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 `-p`/`--passphrase` is specified, the file is encrypted with a passphrase
requested interactively. Otherwise, it's encrypted to one or more
[RECIPIENTS][RECIPIENTS AND IDENTITIES] specified with `-r`/`--recipient` or
`-R`/`--recipients-file`. Every recipient can decrypt the file.
In `-d`/`--decrypt` mode, passphrase-encrypted files are detected automatically
and the passphrase is requested interactively. Otherwise, one or more
[IDENTITIES][RECIPIENTS AND IDENTITIES] specified with `-i`/`--identity` are
used to decrypt the file.
`age` encrypted files are binary and not malleable, with around 200 bytes of
overhead per recipient, plus 16 bytes every 64KiB of plaintext.
## OPTIONS
* `-o`, `--output`=<OUTPUT>:
Write encrypted or decrypted file to <OUTPUT> instead of standard output.
If <OUTPUT> already exists it will be overwritten.
If encrypting without `--armor`, `age` will refuse to output binary to a
TTY. This can be forced by specifying `-` as <OUTPUT>.
* `--version`:
Print the version and exit.
### Encryption options
* `-e`, `--encrypt`:
Encrypt <INPUT> to <OUTPUT>. This is the default.
* `-r`, `--recipient`=<RECIPIENT>:
Encrypt to the explicitly specified <RECIPIENT>. See the
[RECIPIENTS AND IDENTITIES][] section for possible recipient formats.
This option can be repeated and combined with other recipient flags,
and the file can be decrypted by all provided recipients independently.
* `-R`, `--recipients-file`=<PATH>:
Encrypt to the [RECIPIENTS][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 other recipient flags,
and the file can be decrypted by all provided recipients independently.
* `-p`, `--passphrase`:
Encrypt with a passphrase, requested interactively from the terminal.
`age` will offer to auto-generate a secure passphrase.
This option can't be used with other recipient flags.
* `-a`, `--armor`:
Encrypt to an ASCII-only "armored" encoding.
`age` armor is a strict version of PEM with type `AGE ENCRYPTED FILE`,
canonical "strict" Base64, no headers, and no support for leading and
trailing extra data.
Decryption transparently detects and decodes ASCII armoring.
* `-i`, `--identity`=<PATH>:
Encrypt to the [RECIPIENTS][RECIPIENTS AND IDENTITIES] corresponding to the
[IDENTITIES][RECIPIENTS AND IDENTITIES] listed in the file at <PATH>. This
is equivalent to converting the file at <PATH> to a recipients file with
`age-keygen -y` and then passing that to `-R`/`--recipients-file`.
For the format of <PATH>, see the definition of `-i`/`--identity` in the
[Decryption options][] section.
`-e`/`--encrypt` must be explicitly specified when using `-i`/`--identity`
in encryption mode to avoid confusion.
* `-j` <PLUGIN>:
Encrypt using the data-less [plugin][Plugins] <PLUGIN>.
This is equivalent to using `-i`/`--identity` with a file that contains a
single plugin `IDENTITY` that encodes no plugin-specific data.
`-e`/`--encrypt` must be explicitly specified when using `-j` in encryption
mode to avoid confusion.
### Decryption options
* `-d`, `--decrypt`:
Decrypt <INPUT> to <OUTPUT>.
If <INPUT> is passphrase encrypted, it will be automatically detected
and the passphrase will be requested interactively. Otherwise, the
[IDENTITIES][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\. A passphrase encrypted age file, containing
[IDENTITIES][RECIPIENTS AND IDENTITIES] one per line like above.
The passphrase is requested interactively. Note that passphrase-protected
identity files are not necessary for most use cases, where access to the
encrypted identity file implies access to the whole system.
c\. An SSH private key file, in PKCS#1, PKCS#8, or OpenSSH format.
If the private key is password-protected, the password is requested
interactively only if the SSH identity matches the file. See the
[SSH keys][] section for more information, including supported key types.
d\. "`-`", causing one of the options above to be read from standard input.
In this case, the <INPUT> argument must be specified.
This option can be repeated. Identities are tried in the order in which are
provided, and the first one matching one of the file's recipients is used.
Unused identities are ignored, but it is an error if the <INPUT> file is
passphrase-encrypted and `-i`/`--identity` is specified.
* `-j` <PLUGIN>:
Decrypt using the data-less [plugin][Plugins] <PLUGIN>.
This is equivalent to using `-i`/`--identity` with a file that contains a
single plugin `IDENTITY` that encodes no plugin-specific data.
## RECIPIENTS AND IDENTITIES
`RECIPIENTS` are public values, like a public key, that a file can be encrypted
to. `IDENTITIES` are private values, like a private key, that allow decrypting
a file encrypted to the corresponding `RECIPIENT`.
### Native keys
Native `age` key pairs are generated with age-keygen(1), and provide small
encodings and strong encryption based on X25519 for classic keys, and X25519 +
ML-KEM-768 for post-quantum hybrid keys. The post-quantum hybrid keys are secure
against future quantum computers and are the recommended recipient type for most
applications.
A hybrid `RECIPIENT` encoding begins with `age1pq1` and looks like the following:
age1pq167[... 1950 more characters ...]
A hybrid `IDENTITY` encoding begins with `AGE-SECRET-KEY-PQ-1` and looks like
the following:
AGE-SECRET-KEY-PQ-1K30MYPZAHAXHR22YHH27EGDVLU0QNSUH3DSV7J7NR3X6D9LHXNWSDLTV4T
A classic `RECIPIENT` encoding begins with `age1` and looks like the following:
age1gde3ncmahlqd9gg50tanl99r960llztrhfapnmx853s4tjum03uqfssgdh
A classic `IDENTITY` encoding begins with `AGE-SECRET-KEY-1` and looks like the
following:
AGE-SECRET-KEY-1KTYK6RVLN5TAPE7VF6FQQSKZ9HWWCDSKUGXXNUQDWZ7XXT5YK5LSF3UTKQ
A file can't be encrypted to both post-quantum and classic keys, as that would
defeat the post-quantum security of the encryption.
An encrypted file can't be linked to the native recipient it's encrypted to
without access to the corresponding identity.
### SSH keys
As a convenience feature, `age` also supports encrypting to RSA or Ed25519
ssh(1) keys. RSA keys must be at least 2048 bits. This feature employs more
complex cryptography, and should only be used when a native key is not available
for the recipient. Note that SSH keys might not be protected long-term by the
recipient, since they are revokable when used only for authentication.
A `RECIPIENT` encoding is an SSH public key in `authorized_keys` format
(see the `AUTHORIZED_KEYS FILE FORMAT` section of sshd(8)), starting with
`ssh-rsa` or `ssh-ed25519`, like the following:
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDULTit0KUehbi[...]GU4BtElAbzh8=
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH9pO5pz22JZEas[...]l1uZc31FGYMXa
The comment at the end of the line, if present, is ignored.
In recipient files passed to `-R`/`--recipients-file`, unsupported but valid
SSH public keys are ignored with a warning, to facilitate using
`authorized_keys` or GitHub `.keys` files. (See [EXAMPLES][].)
An `IDENTITY` is an SSH private key _file_ passed individually to
`-i`/`--identity`. Note that keys held on hardware tokens such as YubiKeys
or accessed via ssh-agent(1) are not supported.
An encrypted file _can_ be linked to the SSH public key it was encrypted to.
This is so that `age` can identify the correct SSH private key before
requesting its password, if any.
### Plugins
`age` can be extended through plugins. A plugin is only loaded if a corresponding
`RECIPIENT` or `IDENTITY` is specified. (Simply decrypting a file encrypted with
a plugin will not cause it to load, for security reasons among others.)
A `RECIPIENT` for a plugin named `example` starts with `age1example1`, while an
`IDENTITY` starts with `AGE-PLUGIN-EXAMPLE-1`. They both encode arbitrary
plugin-specific data, and are generated by the plugin.
When either is specified, `age` searches for `age-plugin-example` in the PATH
and executes it to perform the file header encryption or decryption. The plugin
may request input from the user through `age` to complete the operation.
Plugins can be freely mixed with other plugins or natively supported keys.
A plugin is not bound to only encrypt or decrypt files meant for or generated by
the plugin. For example, a plugin can be used to decrypt files encrypted to a
native X25519 `RECIPIENT` or even with a passphrase. Similarly, a plugin can
encrypt a file such that it can be decrypted without the use of any plugin.
Plugins for which the `IDENTITY`/`RECIPIENT` distinction doesn't make sense
(such as a symmetric encryption plugin) may generate only an `IDENTITY` and
instruct the user to perform encryption with the `-e`/`--encrypt` and
`-i`/`--identity` flags. Plugins for which the concept of separate identities
doesn't make sense (such as a password-encryption plugin) may instruct the user
to use the `-j` flag.
#### Tagged recipients
`age` can natively encrypt to recipients starting with `age1tag1` (using P-256
ECDH) or `age1tagpq1` (using the ML-KEM-768 + P-256 post-quantum hybrid). These
are intended to be the public side of private keys held in hardware.
They are directly supported to avoid the need to install the plugin, which may
be platform-specific, on the encrypting side.
The tag reduces privacy, by allowing an observer to correlate files with a
recipient (but not files amongst them without knowledge of the recipient),
but this is also a desirable property for hardware keys that require user
interaction for each decryption operation.
## EXIT STATUS
`age` will exit 0 if and only if encryption or decryption are successful for the
full length of the input.
If an error occurs during decryption, partial output might still be generated,
but only if it was possible to securely authenticate it. No unauthenticated
output is ever released.
## BACKWARDS COMPATIBILITY
Files encrypted with a stable version (not alpha, beta, or release candidate) of
`age`, or with any v1.0.0 beta or release candidate, will decrypt with any later
version of the tool.
If decrypting older files poses a security risk, doing so might cause an error
by default. In this case, a flag will be provided to force the operation.
## EXAMPLES
Generate a new post-quantum identity, encrypt data, and decrypt:
$ age-keygen -pq -o key.txt
Public key: age1pq167[... 1950 more characters ...]
$ tar cvz ~/data | age -r age1pq167[...] > 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 age1pq167[...] -r age1pq1e3[...] example.jpg
Encrypt to a list of recipients:
$ cat > recipients.txt
# Alice
age1pq167[... 1950 more characters ...]
# Bob
age1pq1e3[... 1950 more characters ...]
$ age -R recipients.txt example.jpg > example.jpg.age
Encrypt and decrypt a file using a passphrase:
$ age -p secrets.txt > secrets.txt.age
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "release-response-step-brand-wrap-ankle-pair-unusual-sword-train".
$ age -d secrets.txt.age > secrets.txt
Enter passphrase:
Encrypt and decrypt with a passphrase-protected identity file:
$ age-keygen | age -p > key.age
Public key: age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5
Enter passphrase (leave empty to autogenerate a secure one):
Using the autogenerated passphrase "hip-roast-boring-snake-mention-east-wasp-honey-input-actress".
$ age -r age1yhm4gctwfmrpz87tdslm550wrx6m79y9f2hdzt0lndjnehwj0ukqrjpyx5 secrets.txt > secrets.txt.age
$ age -d -i key.age secrets.txt.age > secrets.txt
Enter passphrase for identity file "key.age":
Encrypt and decrypt with an SSH public key:
$ age -R ~/.ssh/id_ed25519.pub example.jpg > example.jpg.age
$ age -d -i ~/.ssh/id_ed25519 example.jpg.age > example.jpg
Encrypt and decrypt with age-plugin-yubikey:
$ age-plugin-yubikey # run interactive setup, generate identity file and obtain recipient
$ age -r age1yubikey1qwt50d05nh5vutpdzmlg5wn80xq5negm4uj9ghv0snvdd3yysf5yw3rhl3t secrets.txt > secrets.txt.age
$ age -d -i age-yubikey-identity-388178f3.txt secrets.txt.age
Encrypt to the SSH keys of a GitHub user:
$ curl https://github.com/benjojo.keys | age -R - example.jpg > example.jpg.age
## SEE ALSO
age-keygen(1), age-inspect(1)
## AUTHORS
Filippo Valsorda <age@filippo.io>

View File

@@ -0,0 +1,151 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"runtime/debug"
"filippo.io/age"
"filippo.io/age/internal/bech32"
"filippo.io/age/plugin"
)
const usage = `Usage:
age-plugin-pq -identity [-o OUTPUT] [INPUT]
Options:
-identity Convert one or more native post-quantum identities from
INPUT or from standard input to plugin identities.
-o, --output OUTPUT Write the result to the file at path OUTPUT instead of
standard output.
age-plugin-pq is an age plugin for post-quantum hybrid ML-KEM-768 + X25519
recipients and identities. These are supported natively by age v1.3.0 and later,
but this plugin can be placed in $PATH to add support to any version and
implementation of age that supports plugins.
Recipients work out of the box, while identities need to be converted to plugin
identities with -identity. If OUTPUT already exists, it is not overwritten.`
// Version can be set at link time to override debug.BuildInfo.Main.Version when
// building manually without git history. It should look like "v1.2.3".
var Version string
func main() {
log.SetFlags(0)
p, err := plugin.New("pq")
if err != nil {
errorf("failed to create plugin: %v", err)
}
p.RegisterFlags(nil)
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
var outFlag string
var versionFlag, identityFlag bool
flag.BoolVar(&versionFlag, "version", false, "print the version")
flag.BoolVar(&identityFlag, "identity", false, "convert identities to plugin identities")
flag.StringVar(&outFlag, "o", "", "output to `FILE` (default stdout)")
flag.StringVar(&outFlag, "output", "", "output to `FILE` (default stdout)")
flag.Parse()
if versionFlag {
if buildInfo, ok := debug.ReadBuildInfo(); ok && Version == "" {
Version = buildInfo.Main.Version
}
fmt.Println(Version)
return
}
if identityFlag {
if len(flag.Args()) > 1 {
errorf("too many arguments")
}
out := os.Stdout
if outFlag != "" {
f, err := os.OpenFile(outFlag, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
errorf("failed to open output file %q: %v", outFlag, err)
}
defer func() {
if err := f.Close(); err != nil {
errorf("failed to close output file %q: %v", outFlag, err)
}
}()
out = f
}
if fi, err := out.Stat(); err == nil && fi.Mode().IsRegular() && fi.Mode().Perm()&0004 != 0 {
warning("writing secret key to a world-readable file")
}
in := os.Stdin
if inFile := flag.Arg(0); inFile != "" && inFile != "-" {
f, err := os.Open(inFile)
if err != nil {
errorf("failed to open input file %q: %v", inFile, err)
}
defer f.Close()
in = f
}
convert(in, out)
return
}
p.HandleRecipientEncoding(func(s string) (age.Recipient, error) {
return age.ParseHybridRecipient(s)
})
p.HandleIdentity(func(data []byte) (age.Identity, error) {
// Convert from a AGE-PLUGIN-PQ-1... payload to a
// AGE-SECRET-KEY-PQ-1... identity encoding.
s, err := bech32.Encode("AGE-SECRET-KEY-PQ-", data)
if err != nil {
return nil, err
}
return age.ParseHybridIdentity(s)
})
p.HandleIdentityAsRecipient(func(data []byte) (age.Recipient, error) {
s, err := bech32.Encode("AGE-SECRET-KEY-PQ-", data)
if err != nil {
return nil, err
}
i, err := age.ParseHybridIdentity(s)
if err != nil {
return nil, err
}
return i.Recipient(), nil
})
os.Exit(p.Main())
}
func convert(in io.Reader, out io.Writer) {
ids, err := age.ParseIdentities(in)
if err != nil {
errorf("failed to parse identities: %v", err)
}
for i, id := range ids {
hybridID, ok := id.(*age.HybridIdentity)
if !ok {
errorf("identity #%d is not a post-quantum hybrid identity", i+1)
}
_, data, err := bech32.Decode(hybridID.String())
if err != nil {
errorf("failed to decode identity #%d: %v", i+1, err)
}
fmt.Fprintln(out, plugin.EncodeIdentity("pq", data))
}
}
func errorf(format string, v ...any) {
log.Printf("age-plugin-pq: error: "+format, v...)
log.Fatalf("age-plugin-pq: report unexpected or unhelpful errors at https://filippo.io/age/report")
}
func warning(msg string) {
log.Printf("age-plugin-pq: warning: %s", msg)
}

View File

@@ -0,0 +1,51 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime/debug"
"filippo.io/age"
"filippo.io/age/plugin"
"filippo.io/age/tag"
)
const usage = `age-plugin-tag is an age plugin for P-256 tagged recipients. These are supported
natively by age v1.3.0 and later, but this plugin can be placed in $PATH to add
support to any version and implementation of age that supports plugins.
Usually, tagged recipients are the public side of private keys held in hardware,
where the identity side is handled by a different plugin.`
// Version can be set at link time to override debug.BuildInfo.Main.Version when
// building manually without git history. It should look like "v1.2.3".
var Version string
func main() {
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
p, err := plugin.New("tag")
if err != nil {
log.Fatal(err)
}
p.RegisterFlags(nil)
versionFlag := flag.Bool("version", false, "print the version")
flag.Parse()
if *versionFlag {
if buildInfo, ok := debug.ReadBuildInfo(); ok && Version == "" {
Version = buildInfo.Main.Version
}
fmt.Println(Version)
return
}
p.HandleRecipient(func(b []byte) (age.Recipient, error) {
return tag.NewClassicRecipient(b)
})
os.Exit(p.Main())
}

View File

@@ -0,0 +1,52 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"runtime/debug"
"filippo.io/age"
"filippo.io/age/plugin"
"filippo.io/age/tag"
)
const usage = `age-plugin-tagpq is an age plugin for ML-KEM-768 + P-256 post-quantum hybrid
tagged recipients. These are supported natively by age v1.3.0 and later, but
this plugin can be placed in $PATH to add support to any version and
implementation of age that supports plugins.
Usually, tagged recipients are the public side of private keys held in hardware,
where the identity side is handled by a different plugin.`
// Version can be set at link time to override debug.BuildInfo.Main.Version when
// building manually without git history. It should look like "v1.2.3".
var Version string
func main() {
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s\n", usage) }
p, err := plugin.New("tagpq")
if err != nil {
log.Fatal(err)
}
p.RegisterFlags(nil)
versionFlag := flag.Bool("version", false, "print the version")
flag.Parse()
if *versionFlag {
if buildInfo, ok := debug.ReadBuildInfo(); ok && Version == "" {
Version = buildInfo.Main.Version
}
fmt.Println(Version)
return
}
p.HandleRecipient(func(b []byte) (age.Recipient, error) {
return tag.NewHybridRecipient(b)
})
os.Exit(p.Main())
}

20
go.mod
View File

@@ -1,8 +1,22 @@
module filippo.io/age
go 1.13
go 1.24.0
// Release build version.
toolchain go1.25.5
require (
github.com/sergi/go-diff v1.1.0
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59
filippo.io/edwards25519 v1.1.0
filippo.io/hpke v0.4.0
filippo.io/nistec v0.0.4
golang.org/x/crypto v0.45.0
golang.org/x/sys v0.38.0
golang.org/x/term v0.37.0
)
// Test dependencies.
require (
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd
github.com/rogpeppe/go-internal v1.14.1
golang.org/x/tools v0.39.0 // indirect
)

47
go.sum
View File

@@ -1,29 +1,18 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM=
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd h1:ZLsPO6WdZ5zatV4UfVpr7oAwLGRZ+sebTUruuM4Ra3M=
c2sp.org/CCTV/age v0.0.0-20251208015420-e9274a7bdbfd/go.mod h1:SrHC2C7r5GkDk8R+NFVzYy/sdj0Ypg9htaPXQq5Cqeo=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
filippo.io/hpke v0.4.0 h1:p575VVQ6ted4pL+it6M00V/f2qTZITO0zgmdKCkd5+A=
filippo.io/hpke v0.4.0/go.mod h1:EmAN849/P3qdeK+PCMkDpDm83vRHM5cDipBJ8xbQLVY=
filippo.io/nistec v0.0.4 h1:F14ZHT5htWlMnQVPndX9ro9arf56cBhQxq4LnDI491s=
filippo.io/nistec v0.0.4/go.mod h1:PK/lw8I1gQT4hUML4QGaqljwdDaFcMyFKSXN7kjrtKI=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=

View File

@@ -1,5 +1,5 @@
// Copyright (c) 2017 Takatoshi Nakagawa
// Copyright (c) 2019 Google LLC
// Copyright (c) 2019 The age Authors
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
@@ -37,7 +37,7 @@ func polymod(values []byte) uint32 {
top := chk >> 25
chk = (chk & 0x1ffffff) << 5
chk = chk ^ uint32(v)
for i := 0; i < 5; i++ {
for i := range 5 {
bit := top >> i & 1
if bit == 1 {
chk ^= generator[i]
@@ -111,9 +111,6 @@ func Encode(hrp string, data []byte) (string, error) {
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)
}
@@ -144,9 +141,6 @@ func Encode(hrp string, data []byte) (string, error) {
// 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")
}

View File

@@ -1,6 +1,6 @@
// Copyright (c) 2013-2017 The btcsuite developers
// Copyright (c) 2016-2017 The Lightning Network Developers
// Copyright (c) 2019 Google LLC
// Copyright (c) 2019 The age Authors
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
@@ -28,7 +28,7 @@ func TestBech32(t *testing.T) {
str string
valid bool
}{
{"A12UEL5L", true},
{"A12UEL5L", true}, // empty
{"a12uel5l", true},
{"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", true},
{"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", true},
@@ -43,12 +43,13 @@ func TestBech32(t *testing.T) {
{"split1a2y9w", false}, // too short data part
{"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // empty hrp
// invalid character (DEL) in hrp
{"spl" + string(127) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false},
// too long
{"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", false},
{"spl" + string(rune(127)) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false},
// long vectors that we do accept despite the spec, see Issue 453
{"long10pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7rc0pu8s7qfcsvr0", true},
{"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", true},
// BIP 173 invalid vectors.
{"an84characterslonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1569pvx", false},
{"pzry9x0s0muk", false},
{"1pzry9x0s0muk", false},
{"x1b4n0q5v", false},

View File

@@ -1,21 +0,0 @@
# 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
# docker run --rm -v $PWD/workdir:/workdir $(docker build -q -f internal/format/Dockerfile.go-fuzz .)
FROM golang:1.14-alpine3.11
RUN apk add --no-cache git
RUN go get github.com/dvyukov/go-fuzz/...
ADD . $GOPATH/src/filippo.io/age/
WORKDIR $GOPATH/src/filippo.io/age
RUN go-fuzz-build ./internal/format
VOLUME /workdir
ENTRYPOINT ["go-fuzz", "-workdir", "/workdir", "-bin", "format-fuzz.zip"]

View File

@@ -1,8 +1,6 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
// Package format implements the age file format.
package format
@@ -43,50 +41,74 @@ func DecodeString(s string) ([]byte, error) {
var EncodeToString = b64.EncodeToString
const ColumnsPerLine = 64
const BytesPerLine = ColumnsPerLine / 4 * 3
// NewlineWriter returns a Writer that writes to dst, inserting an LF character
// every ColumnsPerLine bytes. It does not insert a newline neither at the
// beginning nor at the end of the stream.
func NewlineWriter(dst io.Writer) io.Writer {
return &newlineWriter{dst: dst}
// 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 newlineWriter struct {
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 *newlineWriter) Write(p []byte) (n int, err error) {
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 {
remainingInLine := ColumnsPerLine - (w.written % ColumnsPerLine)
if remainingInLine == ColumnsPerLine && w.written != 0 {
if _, err := w.dst.Write([]byte("\n")); err != nil {
return n, err
}
}
toWrite := remainingInLine
if toWrite > len(p) {
toWrite = len(p)
}
nn, err := w.dst.Write(p[:toWrite])
n += nn
w.written += nn
p = p[nn:]
if err != nil {
return n, err
toWrite := min(ColumnsPerLine-(w.written%ColumnsPerLine), len(p))
n, _ := w.buf.Write(p[:toWrite])
w.written += n
p = p[n:]
if w.written%ColumnsPerLine == 0 {
w.buf.Write([]byte("\n"))
}
}
return n, nil
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 stanzaPrefix = []byte("->")
var footerPrefix = []byte("---")
func (r *Stanza) Marshal(w io.Writer) error {
if _, err := w.Write(recipientPrefix); err != nil {
if _, err := w.Write(stanzaPrefix); err != nil {
return err
}
for _, a := range append([]string{r.Type}, r.Args...) {
@@ -97,10 +119,7 @@ func (r *Stanza) Marshal(w io.Writer) error {
if _, err := io.WriteString(w, "\n"); err != nil {
return err
}
if len(r.Body) == 0 {
return nil
}
ww := base64.NewEncoder(b64, NewlineWriter(w))
ww := NewWrappedBase64Encoder(b64, w)
if _, err := ww.Write(r.Body); err != nil {
return err
}
@@ -133,14 +152,81 @@ func (h *Header) Marshal(w io.Writer) error {
return err
}
type ParseError string
func (e ParseError) Error() string {
return "parsing age header: " + string(e)
type StanzaReader struct {
r *bufio.Reader
err error
}
func errorf(format string, a ...interface{}) error {
return ParseError(fmt.Sprintf(format, a...))
func NewStanzaReader(r *bufio.Reader) *StanzaReader {
return &StanzaReader{r: r}
}
func (r *StanzaReader) ReadStanza() (s *Stanza, err error) {
// Read errors are unrecoverable.
if r.err != nil {
return nil, r.err
}
defer func() { r.err = err }()
s = &Stanza{}
line, err := r.r.ReadBytes('\n')
if err != nil {
return nil, fmt.Errorf("failed to read line: %w", err)
}
if !bytes.HasPrefix(line, stanzaPrefix) {
return nil, fmt.Errorf("malformed stanza opening line: %q", line)
}
prefix, args := splitArgs(line)
if prefix != string(stanzaPrefix) || len(args) < 1 {
return nil, fmt.Errorf("malformed stanza: %q", line)
}
for _, a := range args {
if !isValidString(a) {
return nil, fmt.Errorf("malformed stanza: %q", line)
}
}
s.Type = args[0]
s.Args = args[1:]
for {
line, err := r.r.ReadBytes('\n')
if err != nil {
return nil, fmt.Errorf("failed to read line: %w", err)
}
b, err := DecodeString(strings.TrimSuffix(string(line), "\n"))
if err != nil {
if bytes.HasPrefix(line, footerPrefix) || bytes.HasPrefix(line, stanzaPrefix) {
return nil, fmt.Errorf("malformed body line %q: stanza ended without a short line\nnote: this might be a file encrypted with an old beta version of age or rage; use age v1.0.0-beta6 or rage to decrypt it", line)
}
return nil, errorf("malformed body line %q: %v", line, err)
}
if len(b) > BytesPerLine {
return nil, errorf("malformed body line %q: too long", line)
}
s.Body = append(s.Body, b...)
if len(b) < BytesPerLine {
// A stanza body always ends with a short line.
return s, nil
}
}
}
type ParseError struct {
err error
}
func (e *ParseError) Error() string {
return "parsing age header: " + e.err.Error()
}
func (e *ParseError) Unwrap() error {
return e.err
}
func errorf(format string, a ...any) error {
return &ParseError{fmt.Errorf(format, a...)}
}
// Parse returns the header and a Reader that begins at the start of the
@@ -150,66 +236,44 @@ func Parse(input io.Reader) (*Header, io.Reader, error) {
rr := bufio.NewReader(input)
line, err := rr.ReadString('\n')
if err != nil {
return nil, nil, errorf("failed to read intro: %v", err)
if err == io.EOF {
return nil, nil, errorf("file is empty")
} else if err != nil {
return nil, nil, errorf("failed to read intro: %w", err)
}
if line != intro {
return nil, nil, errorf("unexpected intro: %q", line)
}
var r *Stanza
sr := NewStanzaReader(rr)
for {
line, err := rr.ReadBytes('\n')
peek, err := rr.Peek(len(footerPrefix))
if err != nil {
return nil, nil, errorf("failed to read header: %v", err)
return nil, nil, errorf("failed to read header: %w", err)
}
if bytes.HasPrefix(line, footerPrefix) {
if bytes.Equal(peek, footerPrefix) {
line, err := rr.ReadBytes('\n')
if err != nil {
return nil, nil, fmt.Errorf("failed to read header: %w", err)
}
prefix, args := splitArgs(line)
if prefix != string(footerPrefix) || len(args) != 1 {
return nil, nil, errorf("malformed closing line: %q", line)
}
h.MAC, err = DecodeString(args[0])
if err != nil {
if err != nil || len(h.MAC) != 32 {
return nil, nil, errorf("malformed closing line %q: %v", line, err)
}
break
} else if bytes.HasPrefix(line, recipientPrefix) {
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)
}
if len(b) == 0 {
return nil, nil, errorf("malformed body line %q: line is empty", 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)
}
s, err := sr.ReadStanza()
if err != nil {
return nil, nil, fmt.Errorf("failed to parse header: %w", err)
}
h.Recipients = append(h.Recipients, s)
}
// If input is a bufio.Reader, rr might be equal to input because

View File

@@ -1,60 +0,0 @@
// 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
// +build gofuzz
package format
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/sergi/go-diff/diffmatchpatch"
)
func Fuzz(data []byte) int {
isArmored := bytes.HasPrefix(data, []byte("-----BEGIN AGE ENCRYPTED FILE-----"))
h, payload, err := Parse(bytes.NewReader(data))
if err != nil {
if h != nil {
panic("h != nil on error")
}
if payload != nil {
panic("payload != nil on error")
}
return 0
}
w := &bytes.Buffer{}
if isArmored {
w := ArmoredWriter(w)
if err := h.Marshal(w); err != nil {
panic(err)
}
if _, err := io.Copy(w, payload); err != nil {
if strings.Contains(err.Error(), "invalid armor") {
return 0
}
panic(err)
}
w.Close()
} else {
if err := h.Marshal(w); err != nil {
panic(err)
}
if _, err := io.Copy(w, payload); err != nil {
panic(err)
}
}
if !bytes.Equal(w.Bytes(), data) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(data), string(w.Bytes()), false)
fmt.Println(dmp.DiffToDelta(diffs))
panic("Marshal output different from input")
}
return 1
}

View File

@@ -0,0 +1,84 @@
// Copyright 2021 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build go1.18
package format_test
import (
"bytes"
"io"
"os"
"path/filepath"
"testing"
"filippo.io/age/internal/format"
)
func TestStanzaMarshal(t *testing.T) {
s := &format.Stanza{
Type: "test",
Args: []string{"1", "2", "3"},
Body: nil, // empty
}
buf := &bytes.Buffer{}
s.Marshal(buf)
if exp := "-> test 1 2 3\n\n"; buf.String() != exp {
t.Errorf("wrong empty stanza encoding: expected %q, got %q", exp, buf.String())
}
buf.Reset()
s.Body = []byte("AAA")
s.Marshal(buf)
if exp := "-> test 1 2 3\nQUFB\n"; buf.String() != exp {
t.Errorf("wrong normal stanza encoding: expected %q, got %q", exp, buf.String())
}
buf.Reset()
s.Body = bytes.Repeat([]byte("A"), format.BytesPerLine)
s.Marshal(buf)
if exp := "-> test 1 2 3\nQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\n\n"; buf.String() != exp {
t.Errorf("wrong 64 columns stanza encoding: expected %q, got %q", exp, buf.String())
}
}
func FuzzMalleability(f *testing.F) {
tests, err := filepath.Glob("../../testdata/testkit/*")
if err != nil {
f.Fatal(err)
}
for _, test := range tests {
contents, err := os.ReadFile(test)
if err != nil {
f.Fatal(err)
}
_, contents, ok := bytes.Cut(contents, []byte("\n\n"))
if !ok {
f.Fatal("testkit file without header")
}
f.Add(contents)
}
f.Fuzz(func(t *testing.T, data []byte) {
h, payload, err := format.Parse(bytes.NewReader(data))
if err != nil {
if h != nil {
t.Error("h != nil on error")
}
if payload != nil {
t.Error("payload != nil on error")
}
t.Skip()
}
w := &bytes.Buffer{}
if err := h.Marshal(w); err != nil {
t.Fatal(err)
}
if _, err := io.Copy(w, payload); err != nil {
t.Fatal(err)
}
if !bytes.Equal(w.Bytes(), data) {
t.Error("Marshal output different from input")
}
})
}

127
internal/inspect/inspect.go Normal file
View File

@@ -0,0 +1,127 @@
package inspect
import (
"bufio"
"bytes"
"fmt"
"io"
"strings"
"filippo.io/age/armor"
"filippo.io/age/internal/format"
"filippo.io/age/internal/stream"
)
type Metadata struct {
Version string `json:"version"`
Postquantum string `json:"postquantum"` // "yes" or "no" or "unknown"
Armor bool `json:"armor"`
StanzaTypes []string `json:"stanza_types"`
Sizes struct {
Header int64 `json:"header"`
Armor int64 `json:"armor"`
Overhead int64 `json:"overhead"`
// Currently, we don't do any padding, not MinPayload == MaxPayload and
// MinPadding == MaxPadding == 0, but that might change in the future.
MinPayload int64 `json:"min_payload"`
MaxPayload int64 `json:"max_payload"`
MinPadding int64 `json:"min_padding"`
MaxPadding int64 `json:"max_padding"`
} `json:"sizes"`
}
func Inspect(r io.Reader, fileSize int64) (*Metadata, error) {
data := &Metadata{
Version: "age-encryption.org/v1",
Postquantum: "unknown",
}
tr := &trackReader{r: r}
br := bufio.NewReader(tr)
const maxWhitespace = 1024
start, _ := br.Peek(maxWhitespace + len(armor.Header))
if strings.HasPrefix(string(bytes.TrimSpace(start)), armor.Header) {
r = armor.NewReader(br)
data.Armor = true
} else {
r = br
}
hdr, rest, err := format.Parse(r)
if err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
buf := &bytes.Buffer{}
if err := hdr.Marshal(buf); err != nil {
return nil, fmt.Errorf("failed to re-serialize header: %w", err)
}
data.Sizes.Header = int64(buf.Len())
for _, s := range hdr.Recipients {
data.StanzaTypes = append(data.StanzaTypes, s.Type)
switch s.Type {
case "X25519", "ssh-rsa", "ssh-ed25519", "age-encryption.org/p256tag", "piv-p256":
data.Postquantum = "no"
case "mlkem768x25519", "scrypt", "age-encryption.org/mlkem768p256tag":
if data.Postquantum != "no" {
data.Postquantum = "yes"
}
}
}
// If fileSize is not provided, or if it's the size of the armored file
// (which can have LF or CRLF line endings, varying its size), read to
// the end to determine it.
if fileSize == -1 || data.Armor {
n, err := io.Copy(io.Discard, rest)
if err != nil {
return nil, fmt.Errorf("failed to read rest of file: %w", err)
}
fileSize = data.Sizes.Header + n
if !tr.done {
panic("trackReader not done after io.Copy")
}
if tr.count != fileSize && !data.Armor {
panic("trackReader count mismatch")
}
data.Sizes.Armor = tr.count - fileSize
}
data.Sizes.Overhead, err = streamOverhead(fileSize - data.Sizes.Header)
if err != nil {
return nil, fmt.Errorf("failed to compute stream overhead: %w", err)
}
data.Sizes.MinPayload = fileSize - data.Sizes.Header - data.Sizes.Overhead
data.Sizes.MaxPayload = data.Sizes.MinPayload
return data, nil
}
type trackReader struct {
r io.Reader
count int64
done bool
}
func (tr *trackReader) Read(p []byte) (int, error) {
n, err := tr.r.Read(p)
tr.count += int64(n)
if err == io.EOF {
tr.done = true
} else if tr.done {
panic("non-EOF read after EOF")
}
return n, err
}
func streamOverhead(payloadSize int64) (int64, error) {
const streamNonceSize = 16
if payloadSize < streamNonceSize {
return 0, fmt.Errorf("encrypted size too small: %d", payloadSize)
}
encryptedSize := payloadSize - streamNonceSize
plaintextSize, err := stream.PlaintextSize(encryptedSize)
if err != nil {
return 0, err
}
return payloadSize - plaintextSize, nil
}

View File

@@ -0,0 +1,46 @@
package inspect
import (
"fmt"
"testing"
"filippo.io/age/internal/stream"
)
func TestStreamOverhead(t *testing.T) {
tests := []struct {
payloadSize int64
want int64
wantErr bool
}{
{payloadSize: 0, wantErr: true},
{payloadSize: 15, wantErr: true},
{payloadSize: 16, wantErr: true},
{payloadSize: 16 + 15, wantErr: true},
{payloadSize: 16 + 16, want: 16 + 16}, // empty plaintext
{payloadSize: 16 + 1 + 16, want: 16 + 16},
{payloadSize: 16 + stream.ChunkSize + 16, want: 16 + 16},
{payloadSize: 16 + stream.ChunkSize + 16 + 1, wantErr: true},
{payloadSize: 16 + stream.ChunkSize + 16 + 15, wantErr: true},
{payloadSize: 16 + stream.ChunkSize + 16 + 16, wantErr: true}, // empty final chunk
{payloadSize: 16 + stream.ChunkSize + 16 + 1 + 16, want: 16 + 16 + 16},
}
for _, tt := range tests {
name := "payloadSize=" + fmt.Sprint(tt.payloadSize)
t.Run(name, func(t *testing.T) {
got, gotErr := streamOverhead(tt.payloadSize)
if gotErr != nil {
if !tt.wantErr {
t.Errorf("streamOverhead() failed: %v", gotErr)
}
return
}
if tt.wantErr {
t.Fatal("streamOverhead() succeeded unexpectedly")
}
if got != tt.want {
t.Errorf("streamOverhead() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -1,24 +1,50 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
// Package stream implements a variant of the STREAM chunked encryption scheme.
package stream
import (
"bytes"
"crypto/cipher"
"encoding/binary"
"errors"
"fmt"
"io"
"sync/atomic"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/poly1305"
)
const ChunkSize = 64 * 1024
type Reader struct {
func EncryptedChunkCount(encryptedSize int64) (int64, error) {
chunks := (encryptedSize + encChunkSize - 1) / encChunkSize
plaintextSize := encryptedSize - chunks*chacha20poly1305.Overhead
expChunks := (plaintextSize + ChunkSize - 1) / ChunkSize
// Empty plaintext, the only case that allows (and requires) an empty chunk.
if plaintextSize == 0 {
expChunks = 1
}
if expChunks != chunks {
return 0, fmt.Errorf("invalid encrypted payload size: %d", encryptedSize)
}
return chunks, nil
}
func PlaintextSize(encryptedSize int64) (int64, error) {
chunks, err := EncryptedChunkCount(encryptedSize)
if err != nil {
return 0, err
}
plaintextSize := encryptedSize - chunks*chacha20poly1305.Overhead
return plaintextSize, nil
}
type DecryptReader struct {
a cipher.AEAD
src io.Reader
@@ -30,22 +56,19 @@ type Reader struct {
}
const (
encChunkSize = ChunkSize + poly1305.TagSize
encChunkSize = ChunkSize + chacha20poly1305.Overhead
lastChunkFlag = 0x01
)
func NewReader(key []byte, src io.Reader) (*Reader, error) {
func NewDecryptReader(key []byte, src io.Reader) (*DecryptReader, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
return &Reader{
a: aead,
src: src,
}, nil
return &DecryptReader{a: aead, src: src}, nil
}
func (r *Reader) Read(p []byte) (int, error) {
func (r *DecryptReader) Read(p []byte) (int, error) {
if len(r.unread) > 0 {
n := copy(p, r.unread)
r.unread = r.unread[n:]
@@ -68,16 +91,26 @@ func (r *Reader) Read(p []byte) (int, error) {
r.unread = r.unread[n:]
if last {
r.err = io.EOF
// Ensure there is an EOF after the last chunk as expected. In other
// words, check for trailing data after a full-length final chunk.
// Hopefully, the underlying reader supports returning EOF even if it
// had previously returned an EOF to ReadFull.
if _, err := r.src.Read(make([]byte, 1)); err == nil {
r.err = errors.New("trailing data after end of encrypted file")
} else if err != io.EOF {
r.err = fmt.Errorf("non-EOF error reading after end of encrypted file: %w", err)
} else {
r.err = io.EOF
}
}
return n, nil
}
// readChunk reads the next chunk of ciphertext from r.c and makes it available
// 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) {
func (r *DecryptReader) readChunk() (last bool, err error) {
if len(r.unread) != 0 {
panic("stream: internal error: readChunk called with dirty buffer")
}
@@ -89,7 +122,11 @@ func (r *Reader) readChunk() (last bool, err error) {
// 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.
// The last chunk can be short, but not empty unless it's the first and
// only chunk.
if !nonceIsZero(&r.nonce) && n == r.a.Overhead() {
return false, errors.New("last chunk is empty, try age v1.0.0, and please consider reporting this")
}
in = in[:n]
last = true
setLastChunkFlag(&r.nonce)
@@ -106,7 +143,7 @@ func (r *Reader) readChunk() (last bool, err error) {
out, err = r.a.Open(outBuf, r.nonce[:], in, nil)
}
if err != nil {
return false, errors.New("failed to decrypt and authenticate payload chunk")
return false, errors.New("failed to decrypt and authenticate payload chunk, file may be corrupted or tampered with")
}
incNonce(&r.nonce)
@@ -118,42 +155,44 @@ 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")
return
}
}
// The counter is 88 bits, this is unreachable.
panic("stream: chunk counter wrapped around")
}
func nonceForChunk(chunkIndex int64) *[chacha20poly1305.NonceSize]byte {
var nonce [chacha20poly1305.NonceSize]byte
binary.BigEndian.PutUint64(nonce[3:11], uint64(chunkIndex))
return &nonce
}
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 nonceIsZero(nonce *[chacha20poly1305.NonceSize]byte) bool {
return *nonce == [chacha20poly1305.NonceSize]byte{}
}
func NewWriter(key []byte, dst io.Writer) (*Writer, error) {
type EncryptWriter struct {
a cipher.AEAD
dst io.Writer
buf bytes.Buffer
nonce [chacha20poly1305.NonceSize]byte
err error
}
func NewEncryptWriter(key []byte, dst io.Writer) (*EncryptWriter, 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
return &EncryptWriter{a: aead, dst: dst}, nil
}
func (w *Writer) Write(p []byte) (n int, err error) {
// TODO: consider refactoring with a bytes.Buffer.
func (w *EncryptWriter) Write(p []byte) (n int, err error) {
if w.err != nil {
return 0, w.err
}
@@ -163,12 +202,13 @@ func (w *Writer) Write(p []byte) (n int, err error) {
total := len(p)
for len(p) > 0 {
freeBuf := w.buf[len(w.unwritten):ChunkSize]
n := copy(freeBuf, p)
n := min(len(p), ChunkSize-w.buf.Len())
w.buf.Write(p[:n])
p = p[n:]
w.unwritten = w.unwritten[:len(w.unwritten)+n]
if len(w.unwritten) == ChunkSize && len(p) > 0 {
// Only flush if there's a full chunk with bytes still to write, or we
// can't know if this is the last chunk yet.
if w.buf.Len() == ChunkSize && len(p) > 0 {
if err := w.flushChunk(notLastChunk); err != nil {
w.err = err
return 0, err
@@ -179,7 +219,7 @@ func (w *Writer) Write(p []byte) (n int, err error) {
}
// Close flushes the last chunk. It does not close the underlying Writer.
func (w *Writer) Close() error {
func (w *EncryptWriter) Close() error {
if w.err != nil {
return w.err
}
@@ -198,17 +238,214 @@ const (
notLastChunk = false
)
func (w *Writer) flushChunk(last bool) error {
if !last && len(w.unwritten) != ChunkSize {
func (w *EncryptWriter) flushChunk(last bool) error {
if !last && w.buf.Len() != 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]
w.buf.Grow(chacha20poly1305.Overhead)
ciphertext := w.a.Seal(w.buf.Bytes()[:0], w.nonce[:], w.buf.Bytes(), nil)
_, err := w.dst.Write(ciphertext)
incNonce(&w.nonce)
w.buf.Reset()
return err
}
type EncryptReader struct {
a cipher.AEAD
src io.Reader
// The first ready bytes of buf are already encrypted. This may be less than
// buf.Len(), because we need to over-read to know if a chunk is the last.
ready int
buf bytes.Buffer
nonce [chacha20poly1305.NonceSize]byte
err error
}
func NewEncryptReader(key []byte, src io.Reader) (*EncryptReader, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
return &EncryptReader{a: aead, src: src}, nil
}
func (r *EncryptReader) Read(p []byte) (int, error) {
if r.ready > 0 {
n, err := r.buf.Read(p[:min(len(p), r.ready)])
r.ready -= n
return n, err
}
if r.err != nil {
return 0, r.err
}
if len(p) == 0 {
return 0, nil
}
if err := r.feedBuffer(); err != nil {
r.err = err
return 0, err
}
n, err := r.buf.Read(p[:min(len(p), r.ready)])
r.ready -= n
return n, err
}
// feedBuffer reads and encrypts the next chunk from r.src and appends it to
// r.buf. It sets r.ready to the number of newly available bytes in r.buf.
func (r *EncryptReader) feedBuffer() error {
if r.ready > 0 {
panic("stream: internal error: feedBuffer called with dirty buffer")
}
// CopyN will use r.buf.ReadFrom/WriteTo to fill the buffer directly.
// We need ChunkSize + 1 bytes to determine if this is the last chunk.
_, err := io.CopyN(&r.buf, r.src, int64(ChunkSize-r.buf.Len()+1))
if err != nil && err != io.EOF {
return err
}
if last := r.buf.Len() <= ChunkSize; last {
setLastChunkFlag(&r.nonce)
// After Grow, we know r.buf.Bytes() has enough capacity for the
// overhead. We encrypt in place and then do a Write to include the
// overhead in the buffer.
r.buf.Grow(chacha20poly1305.Overhead)
plaintext := r.buf.Bytes()
r.a.Seal(plaintext[:0], r.nonce[:], plaintext, nil)
incNonce(&r.nonce)
r.buf.Write(plaintext[len(plaintext) : len(plaintext)+chacha20poly1305.Overhead])
r.ready = r.buf.Len()
r.err = io.EOF
return nil
}
// Same, but accounting for the tail byte which will remain unencrypted and
// needs to be shifted past the overhead.
if r.buf.Len() != ChunkSize+1 {
panic("stream: internal error: unexpected buffer length")
}
tailByte := r.buf.Bytes()[ChunkSize]
r.buf.Grow(chacha20poly1305.Overhead)
plaintext := r.buf.Bytes()[:ChunkSize]
r.a.Seal(plaintext[:0], r.nonce[:], plaintext, nil)
incNonce(&r.nonce)
r.buf.Write(plaintext[len(plaintext)+1 : len(plaintext)+chacha20poly1305.Overhead])
r.buf.WriteByte(tailByte)
r.ready = ChunkSize + chacha20poly1305.Overhead
return nil
}
type DecryptReaderAt struct {
a cipher.AEAD
src io.ReaderAt
size int64
chunks int64
cache atomic.Pointer[cachedChunk]
}
type cachedChunk struct {
off int64
data []byte
}
func NewDecryptReaderAt(key []byte, src io.ReaderAt, size int64) (*DecryptReaderAt, error) {
aead, err := chacha20poly1305.New(key)
if err != nil {
return nil, err
}
// Check that size is valid by decrypting the final chunk.
chunks, err := EncryptedChunkCount(size)
if err != nil {
return nil, err
}
finalChunkIndex := chunks - 1
finalChunkOff := finalChunkIndex * encChunkSize
finalChunkSize := size - finalChunkOff
finalChunk := make([]byte, finalChunkSize)
if _, err := src.ReadAt(finalChunk, finalChunkOff); err != nil {
return nil, fmt.Errorf("failed to read final chunk: %w", err)
}
nonce := nonceForChunk(finalChunkIndex)
setLastChunkFlag(nonce)
plaintext, err := aead.Open(finalChunk[:0], nonce[:], finalChunk, nil)
if err != nil {
return nil, fmt.Errorf("failed to decrypt and authenticate final chunk: %w", err)
}
cache := &cachedChunk{off: finalChunkOff, data: plaintext}
plaintextSize := size - chunks*chacha20poly1305.Overhead
r := &DecryptReaderAt{a: aead, src: src, size: plaintextSize, chunks: chunks}
r.cache.Store(cache)
return r, nil
}
func (r *DecryptReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
if off < 0 || off > r.size {
return 0, fmt.Errorf("offset out of range [0:%d]: %d", r.size, off)
}
if len(p) == 0 {
return 0, nil
}
var cacheUpdate *cachedChunk
chunk := make([]byte, encChunkSize)
for len(p) > 0 && off < r.size {
chunkIndex := off / ChunkSize
chunkOff := chunkIndex * encChunkSize
encSize := r.size + r.chunks*chacha20poly1305.Overhead
chunkSize := min(encSize-chunkOff, encChunkSize)
cached := r.cache.Load()
var plaintext []byte
if cached != nil && cached.off == chunkOff {
plaintext = cached.data
cacheUpdate = nil
} else {
nn, err := r.src.ReadAt(chunk[:chunkSize], chunkOff)
if err == io.EOF {
if int64(nn) != chunkSize {
err = io.ErrUnexpectedEOF
} else {
err = nil
}
}
if err != nil {
return n, fmt.Errorf("failed to read chunk at offset %d: %w", chunkOff, err)
}
nonce := nonceForChunk(chunkIndex)
if chunkIndex == r.chunks-1 {
setLastChunkFlag(nonce)
}
plaintext, err = r.a.Open(chunk[:0], nonce[:], chunk[:chunkSize], nil)
if err != nil {
return n, fmt.Errorf("failed to decrypt and authenticate chunk at offset %d: %w", chunkOff, err)
}
cacheUpdate = &cachedChunk{off: chunkOff, data: plaintext}
}
plainChunkOff := int(off - chunkIndex*ChunkSize)
copySize := min(len(plaintext)-plainChunkOff, len(p))
copy(p, plaintext[plainChunkOff:plainChunkOff+copySize])
p = p[copySize:]
off += int64(copySize)
n += copySize
}
if cacheUpdate != nil {
r.cache.Store(cacheUpdate)
}
if off == r.size {
return n, io.EOF
}
return n, nil
}

View File

@@ -1,8 +1,6 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package stream_test
@@ -10,7 +8,9 @@ import (
"bytes"
"crypto/rand"
"fmt"
"io"
"testing"
"testing/iotest"
"filippo.io/age/internal/stream"
"golang.org/x/crypto/chacha20poly1305"
@@ -19,12 +19,18 @@ import (
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) })
for _, length := range []int{0, 1000, cs - 1, cs, cs + 1, cs + 100, 2 * cs, 2*cs + 500} {
for _, stepSize := range []int{512, 600, 1000, cs - 1, cs, cs + 1} {
t.Run(fmt.Sprintf("len=%d,step=%d", length, stepSize), func(t *testing.T) {
testRoundTrip(t, stepSize, length)
})
}
}
length, stepSize := 2*cs+500, 1
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) {
@@ -32,68 +38,915 @@ func testRoundTrip(t *testing.T, stepSize, length int) {
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)
}
var ciphertext []byte
t.Run("EncryptWriter", func(t *testing.T) {
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
var n int
for n < length {
b := min(length-n, 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)
}
ciphertext = buf.Bytes()
})
t.Run("DecryptReader", func(t *testing.T) {
r, err := stream.NewDecryptReader(key, bytes.NewReader(ciphertext))
if err != nil {
t.Fatal(err)
}
var n int
readBuf := make([]byte, stepSize)
for n < length {
nn, err := r.Read(readBuf)
if err != nil {
t.Fatalf("Read error at index %d: %v", n, err)
}
if !bytes.Equal(readBuf[:nn], src[n:n+nn]) {
t.Errorf("wrong data at indexes %d - %d", n, n+nn)
}
n += nn
}
t.Run("TestReader", func(t *testing.T) {
if length > 1000 && testing.Short() {
t.Skip("skipping slow iotest.TestReader on long input")
}
r, _ := stream.NewDecryptReader(key, bytes.NewReader(ciphertext))
if err := iotest.TestReader(r, src); err != nil {
t.Error("iotest.TestReader error on DecryptReader:", err)
}
})
})
t.Run("DecryptReaderAt", func(t *testing.T) {
rAt, err := stream.NewDecryptReaderAt(key, bytes.NewReader(ciphertext), int64(len(ciphertext)))
if err != nil {
t.Fatal(err)
}
rr := io.NewSectionReader(rAt, 0, int64(len(ciphertext)))
var n int
readBuf := make([]byte, stepSize)
for n < length {
nn, err := rr.Read(readBuf)
if n+nn == length && err == io.EOF {
err = nil
}
if err != nil {
t.Fatalf("ReadAt 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
}
t.Run("TestReader", func(t *testing.T) {
if length > 1000 && testing.Short() {
t.Skip("skipping slow iotest.TestReader on long input")
}
rr := io.NewSectionReader(rAt, 0, int64(len(src)))
if err := iotest.TestReader(rr, src); err != nil {
t.Error("iotest.TestReader error on DecryptReaderAt:", err)
}
})
})
t.Run("EncryptReader", func(t *testing.T) {
er, err := stream.NewEncryptReader(key, bytes.NewReader(src))
if err != nil {
t.Fatal(err)
}
var n int
readBuf := make([]byte, stepSize)
for {
nn, err := er.Read(readBuf)
if nn == 0 && err == io.EOF {
break
} else if err != nil {
t.Fatalf("EncryptReader Read error at index %d: %v", n, err)
}
if !bytes.Equal(readBuf[:nn], ciphertext[n:n+nn]) {
t.Errorf("EncryptReader wrong data at indexes %d - %d", n, n+nn)
}
n += nn
}
if n != len(ciphertext) {
t.Errorf("EncryptReader read %d bytes, expected %d", n, len(ciphertext))
}
t.Run("TestReader", func(t *testing.T) {
if length > 1000 && testing.Short() {
t.Skip("skipping slow iotest.TestReader on long input")
}
er, _ := stream.NewEncryptReader(key, bytes.NewReader(src))
if err := iotest.TestReader(er, ciphertext); err != nil {
t.Error("iotest.TestReader error on EncryptReader:", err)
}
})
})
}
// trackingReaderAt wraps an io.ReaderAt and tracks whether ReadAt was called.
type trackingReaderAt struct {
r io.ReaderAt
called bool
}
func (t *trackingReaderAt) ReadAt(p []byte, off int64) (int, error) {
t.called = true
return t.r.ReadAt(p, off)
}
func (t *trackingReaderAt) reset() {
t.called = false
}
func TestDecryptReaderAt(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
w, err := stream.NewWriter(key, buf)
if err != nil {
// Create plaintext spanning exactly 3 chunks: 2 full chunks + partial third
// Chunk 0: [0, cs)
// Chunk 1: [cs, 2*cs)
// Chunk 2: [2*cs, 2*cs+500)
plaintextSize := 2*cs + 500
plaintext := make([]byte, plaintextSize)
if _, err := rand.Read(plaintext); 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)
}
// Encrypt
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Error("Close returned an error:", err)
t.Fatal(err)
}
ciphertext := buf.Bytes()
t.Logf("buffer size: %d", buf.Len())
// Create tracking ReaderAt
tracker := &trackingReaderAt{r: bytes.NewReader(ciphertext)}
r, err := stream.NewReader(key, buf)
// Create DecryptReaderAt (this reads and caches the final chunk)
ra, err := stream.NewDecryptReaderAt(key, tracker, int64(len(ciphertext)))
if err != nil {
t.Fatal(err)
}
tracker.reset()
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)
// Helper to check reads
checkRead := func(name string, off int64, size int, wantN int, wantEOF bool, wantSrcRead bool) {
t.Helper()
tracker.reset()
p := make([]byte, size)
n, err := ra.ReadAt(p, off)
if wantEOF {
if err != io.EOF {
t.Errorf("%s: got err=%v, want EOF", name, err)
}
} else {
if err != nil {
t.Errorf("%s: got err=%v, want nil", name, err)
}
}
if !bytes.Equal(readBuf[:nn], src[n:n+nn]) {
t.Errorf("wrong data at indexes %d - %d", n, n+nn)
if n != wantN {
t.Errorf("%s: got n=%d, want %d", name, n, wantN)
}
n += nn
if tracker.called != wantSrcRead {
t.Errorf("%s: src.ReadAt called=%v, want %v", name, tracker.called, wantSrcRead)
}
// Verify data correctness
if n > 0 && off >= 0 && off < int64(plaintextSize) {
end := int(off) + n
if end > plaintextSize {
end = plaintextSize
}
if !bytes.Equal(p[:n], plaintext[off:end]) {
t.Errorf("%s: data mismatch", name)
}
}
}
// Test 1: Read from final chunk (cached by constructor)
checkRead("final chunk (cached)", int64(2*cs+100), 100, 100, false, false)
// Test 2: Read spanning second and third chunk
checkRead("span chunks 1-2", int64(cs+cs-50), 100, 100, false, true)
// Test 3: Read from final chunk again (cached from test 2)
// When reading across chunks 1-2 in test 2, the loop processes chunk 1 then chunk 2,
// so chunk 2 ends up in the cache.
checkRead("final chunk after span", int64(2*cs+200), 100, 100, false, false)
// Test 4: Read from final chunk again (now cached)
checkRead("final chunk (cached again)", int64(2*cs+50), 50, 50, false, false)
// Test 5: Read from first chunk (not cached)
checkRead("first chunk", 0, 100, 100, false, true)
// Test 6: Read from first chunk again (now cached)
checkRead("first chunk (cached)", 50, 100, 100, false, false)
// Test 7: Read spanning all chunks
tracker.reset()
p := make([]byte, plaintextSize)
n, err := ra.ReadAt(p, 0)
if err != io.EOF {
t.Errorf("span all: got err=%v, want EOF", err)
}
if n != plaintextSize {
t.Errorf("span all: got n=%d, want %d", n, plaintextSize)
}
if !bytes.Equal(p, plaintext) {
t.Errorf("span all: data mismatch")
}
// Test 8: Read beyond the end (offset > size)
tracker.reset()
p = make([]byte, 100)
n, err = ra.ReadAt(p, int64(plaintextSize+100))
if err == nil {
t.Error("beyond end: expected error, got nil")
}
if n != 0 {
t.Errorf("beyond end: got n=%d, want 0", n)
}
// Test 9: Read with off = size (should return 0, EOF)
tracker.reset()
p = make([]byte, 100)
n, err = ra.ReadAt(p, int64(plaintextSize))
if err != io.EOF {
t.Errorf("off=size: got err=%v, want EOF", err)
}
if n != 0 {
t.Errorf("off=size: got n=%d, want 0", n)
}
// Test 10: Read spanning last chunk and beyond
tracker.reset()
p = make([]byte, 1000) // request more than available
n, err = ra.ReadAt(p, int64(2*cs+400))
if err != io.EOF {
t.Errorf("span last+beyond: got err=%v, want EOF", err)
}
wantN := 500 - 400 // only 100 bytes available from offset 2*cs+400
if n != wantN {
t.Errorf("span last+beyond: got n=%d, want %d", n, wantN)
}
if !bytes.Equal(p[:n], plaintext[2*cs+400:]) {
t.Error("span last+beyond: data mismatch")
}
// Test 11: Read spanning second+last chunk and beyond
tracker.reset()
p = make([]byte, cs+1000) // request more than available
n, err = ra.ReadAt(p, int64(cs+100))
if err != io.EOF {
t.Errorf("span 1-2+beyond: got err=%v, want EOF", err)
}
wantN = plaintextSize - (cs + 100)
if n != wantN {
t.Errorf("span 1-2+beyond: got n=%d, want %d", n, wantN)
}
if !bytes.Equal(p[:n], plaintext[cs+100:]) {
t.Error("span 1-2+beyond: data mismatch")
}
// Test 12: Negative offset
tracker.reset()
p = make([]byte, 100)
n, err = ra.ReadAt(p, -1)
if err == nil {
t.Error("negative offset: expected error, got nil")
}
if n != 0 {
t.Errorf("negative offset: got n=%d, want 0", n)
}
// Test 13: Zero-length read in the middle
tracker.reset()
p = make([]byte, 0)
n, err = ra.ReadAt(p, 100)
if err != nil {
t.Errorf("zero-length middle: got err=%v, want nil", err)
}
if n != 0 {
t.Errorf("zero-length middle: got n=%d, want 0", n)
}
// Test 14: Zero-length read at end
tracker.reset()
p = make([]byte, 0)
n, err = ra.ReadAt(p, int64(plaintextSize))
if err != nil {
t.Errorf("zero-length end: got err=%v, want nil", err)
}
if n != 0 {
t.Errorf("zero-length end: got n=%d, want 0", n)
}
// Test 15: Read exactly one chunk at chunk boundary
checkRead("exact chunk at boundary", int64(cs), cs, cs, false, true)
// Test 16: Read one byte at each chunk boundary
checkRead("one byte at start", 0, 1, 1, false, true)
checkRead("one byte at cs-1", int64(cs-1), 1, 1, false, false) // cached from test 15
checkRead("one byte at cs", int64(cs), 1, 1, false, true)
checkRead("one byte at 2*cs-1", int64(2*cs-1), 1, 1, false, false) // same chunk
checkRead("one byte at 2*cs", int64(2*cs), 1, 1, false, true)
checkRead("last byte", int64(plaintextSize-1), 1, 1, true, false) // same chunk, EOF because we reach end
// Test 17: Read crossing exactly one chunk boundary
checkRead("cross boundary 0-1", int64(cs-50), 100, 100, false, true)
checkRead("cross boundary 1-2", int64(2*cs-50), 100, 100, false, true)
}
func TestDecryptReaderAtEmpty(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
// Create empty encrypted file
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := buf.Bytes()
tracker := &trackingReaderAt{r: bytes.NewReader(ciphertext)}
ra, err := stream.NewDecryptReaderAt(key, tracker, int64(len(ciphertext)))
if err != nil {
t.Fatal(err)
}
tracker.reset()
// Test 1: Read from empty file at offset 0
p := make([]byte, 100)
n, err := ra.ReadAt(p, 0)
if err != io.EOF {
t.Errorf("empty read: got err=%v, want EOF", err)
}
if n != 0 {
t.Errorf("empty read: got n=%d, want 0", n)
}
// Test 2: Zero-length read from empty file
p = make([]byte, 0)
n, err = ra.ReadAt(p, 0)
if err != nil {
t.Errorf("empty zero-length: got err=%v, want nil", err)
}
if n != 0 {
t.Errorf("empty zero-length: got n=%d, want 0", n)
}
// Test 3: Read beyond empty file
p = make([]byte, 100)
n, err = ra.ReadAt(p, 1)
if err == nil {
t.Error("empty beyond: expected error, got nil")
}
if n != 0 {
t.Errorf("empty beyond: got n=%d, want 0", n)
}
}
func TestDecryptReaderAtSingleChunk(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
// Single chunk, not full
plaintext := make([]byte, 1000)
if _, err := rand.Read(plaintext); err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := buf.Bytes()
tracker := &trackingReaderAt{r: bytes.NewReader(ciphertext)}
ra, err := stream.NewDecryptReaderAt(key, tracker, int64(len(ciphertext)))
if err != nil {
t.Fatal(err)
}
tracker.reset()
// All reads should use cache (final chunk = only chunk)
p := make([]byte, 100)
n, err := ra.ReadAt(p, 0)
if err != nil {
t.Errorf("single chunk start: got err=%v, want nil", err)
}
if n != 100 {
t.Errorf("single chunk start: got n=%d, want 100", n)
}
if tracker.called {
t.Error("single chunk start: unexpected src.ReadAt call")
}
if !bytes.Equal(p[:n], plaintext[:100]) {
t.Error("single chunk start: data mismatch")
}
// Read at end
n, err = ra.ReadAt(p, 900)
if err != io.EOF {
t.Errorf("single chunk end: got err=%v, want EOF", err)
}
if n != 100 {
t.Errorf("single chunk end: got n=%d, want 100", n)
}
if tracker.called {
t.Error("single chunk end: unexpected src.ReadAt call")
}
if !bytes.Equal(p[:n], plaintext[900:]) {
t.Error("single chunk end: data mismatch")
}
}
func TestDecryptReaderAtFullChunks(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
// Exactly 2 full chunks
plaintext := make([]byte, 2*cs)
if _, err := rand.Read(plaintext); err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := buf.Bytes()
tracker := &trackingReaderAt{r: bytes.NewReader(ciphertext)}
ra, err := stream.NewDecryptReaderAt(key, tracker, int64(len(ciphertext)))
if err != nil {
t.Fatal(err)
}
tracker.reset()
// Read last byte of second chunk (cached)
p := make([]byte, 1)
n, err := ra.ReadAt(p, int64(2*cs-1))
if err != io.EOF {
t.Errorf("last byte: got err=%v, want EOF", err)
}
if n != 1 {
t.Errorf("last byte: got n=%d, want 1", n)
}
if tracker.called {
t.Error("last byte: unexpected src.ReadAt call (should be cached)")
}
if p[0] != plaintext[2*cs-1] {
t.Error("last byte: data mismatch")
}
// Read at exactly the boundary between chunks
p = make([]byte, 100)
n, err = ra.ReadAt(p, int64(cs-50))
if err != nil {
t.Errorf("boundary: got err=%v, want nil", err)
}
if n != 100 {
t.Errorf("boundary: got n=%d, want 100", n)
}
if !bytes.Equal(p, plaintext[cs-50:cs+50]) {
t.Error("boundary: data mismatch")
}
}
func TestDecryptReaderAtWrongKey(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
plaintext := make([]byte, 1000)
if _, err := rand.Read(plaintext); err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := buf.Bytes()
// Try to decrypt with wrong key
wrongKey := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(wrongKey); err != nil {
t.Fatal(err)
}
_, err = stream.NewDecryptReaderAt(wrongKey, bytes.NewReader(ciphertext), int64(len(ciphertext)))
if err == nil {
t.Error("wrong key: expected error, got nil")
}
}
func TestDecryptReaderAtInvalidSize(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
plaintext := make([]byte, 1000)
if _, err := rand.Read(plaintext); err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := buf.Bytes()
// Wrong size (too small)
_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(ciphertext), int64(len(ciphertext)-1))
if err == nil {
t.Error("wrong size (small): expected error, got nil")
}
// Wrong size (too large)
_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(ciphertext), int64(len(ciphertext)+1))
if err == nil {
t.Error("wrong size (large): expected error, got nil")
}
// Size that would imply empty final chunk (invalid)
// This would be: one full encrypted chunk + just overhead
invalidSize := int64(cs + chacha20poly1305.Overhead + chacha20poly1305.Overhead)
_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(make([]byte, invalidSize)), invalidSize)
if err == nil {
t.Error("invalid size (empty final chunk): expected error, got nil")
}
}
func TestDecryptReaderAtTruncated(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
plaintext := make([]byte, 2*cs+500)
if _, err := rand.Read(plaintext); err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := buf.Bytes()
// Truncate ciphertext but lie about size
truncated := ciphertext[:len(ciphertext)-100]
_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(truncated), int64(len(ciphertext)))
if err == nil {
t.Error("truncated: expected error, got nil")
}
}
func TestDecryptReaderAtTruncatedChunk(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
// Create 4 chunks: 3 full + 1 partial
plaintext := make([]byte, 3*cs+500)
if _, err := rand.Read(plaintext); err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := buf.Bytes()
// Truncate to 3 chunks (remove the actual final chunk)
// The third chunk was NOT encrypted with the last chunk flag,
// so decryption should fail when we try to use it as the final chunk.
encChunkSize := cs + 16 // ChunkSize + Overhead
truncatedSize := int64(3 * encChunkSize)
truncated := ciphertext[:truncatedSize]
_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(truncated), truncatedSize)
if err == nil {
t.Error("truncated at chunk boundary: expected error, got nil")
}
}
func TestDecryptReaderAtConcurrent(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
// Create plaintext spanning 3 chunks: 2 full + partial
plaintextSize := 2*cs + 500
plaintext := make([]byte, plaintextSize)
if _, err := rand.Read(plaintext); err != nil {
t.Fatal(err)
}
// Encrypt
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := buf.Bytes()
ra, err := stream.NewDecryptReaderAt(key, bytes.NewReader(ciphertext), int64(len(ciphertext)))
if err != nil {
t.Fatal(err)
}
t.Run("same chunk", func(t *testing.T) {
t.Parallel()
const goroutines = 10
const iterations = 100
errc := make(chan error, goroutines)
for g := range goroutines {
go func(id int) {
for i := range iterations {
off := int64((id*iterations + i) % 500)
p := make([]byte, 100)
n, err := ra.ReadAt(p, off)
if err != nil {
errc <- fmt.Errorf("goroutine %d iter %d: %v", id, i, err)
return
}
if n != 100 {
errc <- fmt.Errorf("goroutine %d iter %d: n=%d, want 100", id, i, n)
return
}
if !bytes.Equal(p, plaintext[off:off+100]) {
errc <- fmt.Errorf("goroutine %d iter %d: data mismatch", id, i)
return
}
}
errc <- nil
}(g)
}
for range goroutines {
if err := <-errc; err != nil {
t.Error(err)
}
}
})
t.Run("different chunks", func(t *testing.T) {
t.Parallel()
const goroutines = 10
const iterations = 100
errc := make(chan error, goroutines)
for g := range goroutines {
go func(id int) {
for i := range iterations {
// Each goroutine reads from a different chunk based on id
chunkIdx := id % 3
off := int64(chunkIdx*cs + (i % 400))
size := 100
if off+int64(size) > int64(plaintextSize) {
size = plaintextSize - int(off)
}
p := make([]byte, size)
n, err := ra.ReadAt(p, off)
if n == size && err == io.EOF {
err = nil // EOF at end is acceptable
}
if err != nil {
errc <- fmt.Errorf("goroutine %d iter %d: off=%d: %v", id, i, off, err)
return
}
if n != size {
errc <- fmt.Errorf("goroutine %d iter %d: n=%d, want %d", id, i, n, size)
return
}
if !bytes.Equal(p[:n], plaintext[off:off+int64(n)]) {
errc <- fmt.Errorf("goroutine %d iter %d: data mismatch", id, i)
return
}
}
errc <- nil
}(g)
}
for range goroutines {
if err := <-errc; err != nil {
t.Error(err)
}
}
})
t.Run("across chunks", func(t *testing.T) {
t.Parallel()
const goroutines = 10
const iterations = 100
errc := make(chan error, goroutines)
for g := range goroutines {
go func(id int) {
for i := range iterations {
// Read across chunk boundaries
boundary := (id%2 + 1) * cs // either cs or 2*cs
off := int64(boundary - 50 + (i % 30))
size := 100
if off+int64(size) > int64(plaintextSize) {
size = plaintextSize - int(off)
}
if size <= 0 {
continue
}
p := make([]byte, size)
n, err := ra.ReadAt(p, off)
if n == size && err == io.EOF {
err = nil
}
if err != nil {
errc <- fmt.Errorf("goroutine %d iter %d: off=%d size=%d: %v", id, i, off, size, err)
return
}
if n != size {
errc <- fmt.Errorf("goroutine %d iter %d: n=%d, want %d", id, i, n, size)
return
}
if !bytes.Equal(p[:n], plaintext[off:off+int64(n)]) {
errc <- fmt.Errorf("goroutine %d iter %d: data mismatch", id, i)
return
}
}
errc <- nil
}(g)
}
for range goroutines {
if err := <-errc; err != nil {
t.Error(err)
}
}
})
}
func TestDecryptReaderAtCorrupted(t *testing.T) {
key := make([]byte, chacha20poly1305.KeySize)
if _, err := rand.Read(key); err != nil {
t.Fatal(err)
}
plaintext := make([]byte, 2*cs+500)
if _, err := rand.Read(plaintext); err != nil {
t.Fatal(err)
}
buf := &bytes.Buffer{}
w, err := stream.NewEncryptWriter(key, buf)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write(plaintext); err != nil {
t.Fatal(err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
ciphertext := bytes.Clone(buf.Bytes())
// Corrupt final chunk - should fail in constructor
corruptedFinal := bytes.Clone(ciphertext)
corruptedFinal[len(corruptedFinal)-10] ^= 0xFF
_, err = stream.NewDecryptReaderAt(key, bytes.NewReader(corruptedFinal), int64(len(corruptedFinal)))
if err == nil {
t.Error("corrupted final: expected error, got nil")
}
// Corrupt first chunk - should fail on read
corruptedFirst := bytes.Clone(ciphertext)
corruptedFirst[10] ^= 0xFF
ra, err := stream.NewDecryptReaderAt(key, bytes.NewReader(corruptedFirst), int64(len(corruptedFirst)))
if err != nil {
t.Fatalf("corrupted first constructor: unexpected error: %v", err)
}
p := make([]byte, 100)
_, err = ra.ReadAt(p, 0)
if err == nil {
t.Error("corrupted first read: expected error, got nil")
}
}

122
internal/term/term.go Normal file
View File

@@ -0,0 +1,122 @@
package term
import (
"fmt"
"os"
"runtime"
"golang.org/x/term"
)
// enableVirtualTerminalProcessing tries to enable virtual terminal processing
// on Windows. If it fails, avoid using escape sequences to prevent weird
// characters being printed to the console.
var enableVirtualTerminalProcessing func(out *os.File) error
// clearLine clears the current line on the terminal, or opens a new line if
// terminal escape codes don't work.
func clearLine(out *os.File) {
const (
CUI = "\033[" // Control Sequence Introducer
CPL = CUI + "F" // Cursor Previous Line
EL = CUI + "K" // Erase in Line
)
// First, open a new line, which is guaranteed to work everywhere. Then, try
// to erase the line above with escape codes, if possible.
//
// (We use CRLF instead of LF to work around an apparent bug in WSL2's
// handling of CONOUT$. Only when running a Windows binary from WSL2, the
// cursor would not go back to the start of the line with a simple LF.
// Honestly, it's impressive CONIN$ and CONOUT$ work at all inside WSL2.)
fmt.Fprintf(out, "\r\n")
if enableVirtualTerminalProcessing == nil || enableVirtualTerminalProcessing(out) == nil {
fmt.Fprintf(out, CPL+EL)
}
}
// WithTerminal runs f with the terminal input and output files, if available.
// WithTerminal does not open a non-terminal stdin, so the caller does not need
// to check if stdin is in use.
func WithTerminal(f func(in, out *os.File) error) error {
if runtime.GOOS == "windows" {
in, err := os.OpenFile("CONIN$", os.O_RDWR, 0)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile("CONOUT$", os.O_WRONLY, 0)
if err != nil {
return err
}
defer out.Close()
return f(in, out)
} else if tty, err := os.OpenFile("/dev/tty", os.O_RDWR, 0); err == nil {
defer tty.Close()
return f(tty, tty)
} else if term.IsTerminal(int(os.Stdin.Fd())) {
return f(os.Stdin, os.Stdin)
} else {
return fmt.Errorf("standard input is not a terminal, and /dev/tty is not available: %v", err)
}
}
// ReadSecret reads a value from the terminal with no echo. The prompt is ephemeral.
func ReadSecret(prompt string) (s []byte, err error) {
err = WithTerminal(func(in, out *os.File) error {
fmt.Fprintf(out, "%s ", prompt)
defer clearLine(out)
s, err = term.ReadPassword(int(in.Fd()))
return err
})
return
}
// ReadPublic reads a value from the terminal. The prompt is ephemeral.
func ReadPublic(prompt string) (s []byte, err error) {
err = WithTerminal(func(in, out *os.File) error {
fmt.Fprintf(out, "%s ", prompt)
defer clearLine(out)
oldState, err := term.MakeRaw(int(in.Fd()))
if err != nil {
return err
}
defer term.Restore(int(in.Fd()), oldState)
t := term.NewTerminal(in, "")
line, err := t.ReadLine()
s = []byte(line)
return err
})
return
}
// ReadCharacter reads a single character from the terminal with no echo. The
// prompt is ephemeral.
func ReadCharacter(prompt string) (c byte, err error) {
err = WithTerminal(func(in, out *os.File) error {
fmt.Fprintf(out, "%s ", prompt)
defer clearLine(out)
oldState, err := term.MakeRaw(int(in.Fd()))
if err != nil {
return err
}
defer term.Restore(int(in.Fd()), oldState)
b := make([]byte, 1)
if _, err := in.Read(b); err != nil {
return err
}
c = b[0]
return nil
})
return
}
// IsTerminal returns whether the given file is a terminal.
func IsTerminal(f *os.File) bool {
return term.IsTerminal(int(f.Fd()))
}

View File

@@ -0,0 +1,48 @@
// Copyright 2022 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package term
import (
"errors"
"os"
"syscall"
"golang.org/x/sys/windows"
)
func init() {
enableVirtualTerminalProcessing = func(out *os.File) error {
// Some instances of the Windows Console (e.g., cmd.exe and Windows PowerShell)
// do not have the virtual terminal processing enabled, which is necessary to
// make terminal escape sequences work. For this reason the clearLine function
// may not properly work. Here we enable the virtual terminal processing, if
// possible.
//
// See https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences.
const (
ENABLE_PROCESSED_OUTPUT uint32 = 0x1
ENABLE_VIRTUAL_TERMINAL_PROCESSING uint32 = 0x4
)
kernel32DLL := windows.NewLazySystemDLL("Kernel32.dll")
setConsoleMode := kernel32DLL.NewProc("SetConsoleMode")
var mode uint32
if err := syscall.GetConsoleMode(syscall.Handle(out.Fd()), &mode); err != nil {
return err
}
mode |= ENABLE_PROCESSED_OUTPUT
mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING
// If the SetConsoleMode function fails, the return value is zero.
// See https://learn.microsoft.com/en-us/windows/console/setconsolemode#return-value.
if ret, _, _ := setConsoleMode.Call(out.Fd(), uintptr(mode)); ret == 0 {
return errors.New("SetConsoleMode failed")
}
return nil
}
}

12
logo/README.md Normal file
View File

@@ -0,0 +1,12 @@
The logos available in this folder are Copyright 2021 Filippo Valsorda.
Permission is granted to use the logos as long as they are unaltered, are not
combined with other text or graphic, and are not used to imply your project is
endorsed by or affiliated with the age project.
This permission can be revoked or rescinded for any reason and at any time,
selectively or otherwise.
If you require different terms, please email age-logo@filippo.io.
The logos were designed by [Studiovagante](https://www.studiovagante.it).

BIN
logo/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

579
logo/logo.svg Normal file
View File

@@ -0,0 +1,579 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Livello_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 298.9 141.89" style="enable-background:new 0 0 298.9 141.89;" xml:space="preserve">
<g>
<g>
<path d="M160.3,88.5c-0.86,0-1.29-0.43-1.29-1.29v-5.94c-3.01,4.22-7.83,8.09-14.81,8.09c-11.71,0-19.8-10.07-19.8-22.38
c0-12.31,8.09-22.38,19.8-22.38c6.89,0,11.62,3.61,14.81,8.18v-6.03c0-0.86,0.43-1.29,1.29-1.29h7.14c0.86,0,1.29,0.43,1.29,1.29
v40.46c0,0.86-0.43,1.29-1.29,1.29H160.3z M146.7,80.07c7.14,0,12.48-5.85,12.48-13.08c0-7.23-5.34-13.08-12.48-13.08
c-7.23,0-12.57,5.85-12.57,13.08C134.13,74.21,139.47,80.07,146.7,80.07z"/>
<path d="M223.74,86.95c0,13.51-9.64,23.5-23.33,23.5c-7.14,0-13.6-2.93-17.39-6.8c-0.52-0.52-0.52-1.12-0.09-1.72l3.79-4.99
c0.26-0.34,0.6-0.52,0.86-0.52c0.26,0,0.52,0.17,0.77,0.34c2.84,2.67,6.97,4.39,11.79,4.39c9.12,0,13.86-6.2,13.86-13.34v-6.97
c-3.01,4.22-7.75,8.09-14.89,8.09c-11.62,0-19.71-9.73-19.71-22.03s8.09-22.29,19.71-22.29c6.89,0,11.71,3.61,14.89,8.18v-6.03
c0-0.86,0.43-1.29,1.29-1.29h7.14c0.86,0,1.29,0.43,1.29,1.29V86.95z M201.7,79.64c7.23,0,12.57-5.42,12.57-12.74
c0-7.32-5.34-13-12.57-13c-7.23,0-12.57,5.85-12.57,13C189.13,74.21,194.47,79.64,201.7,79.64z"/>
<path d="M267.64,78.35c0.26-0.17,0.6-0.26,0.86-0.26c0.43,0,0.77,0.26,1.03,0.6l3.18,4.56c0.43,0.77,0.34,1.29-0.34,1.72
c-4.22,2.67-9.38,4.39-15.92,4.39c-12.22,0-22.04-10.07-22.04-22.38c0-11.71,8.18-22.38,20.92-22.38
c13.43,0,21.17,11.02,21.17,24.19c0,0.95-0.43,1.55-1.46,1.55h-31.42c1.12,5.85,6.02,10.76,13.08,10.76
C261.18,81.1,264.28,80.15,267.64,78.35z M267.21,63.2c-1.38-6.54-5.25-10.93-11.88-10.93c-6.2,0-10.5,4.3-11.62,10.93H267.21z"/>
</g>
<g>
<g>
<path d="M142.92,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h5.26c0.12,0,0.18,0.06,0.18,0.16v0.9
c0,0.11-0.06,0.16-0.18,0.16h-4.05v2.18h3.51c0.12,0,0.18,0.05,0.18,0.16v0.87c0,0.09-0.05,0.16-0.18,0.16h-3.51v2.92
c0,0.11-0.06,0.16-0.18,0.16H142.92z"/>
<path d="M152.11,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.02c0.12,0,0.18,0.06,0.18,0.16v7.36
c0,0.11-0.06,0.16-0.18,0.16H152.11z"/>
<path d="M157.89,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.02c0.12,0,0.18,0.06,0.18,0.16v6.31
h3.26c0.12,0,0.18,0.06,0.18,0.16v0.89c0,0.11-0.06,0.16-0.18,0.16H157.89z"/>
<path d="M171.38,118.32c0.13,0,0.18,0.08,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-3.64v2.02h3.21c0.12,0,0.18,0.06,0.18,0.16
v0.87c0,0.11-0.06,0.16-0.18,0.16h-3.21v2.01h3.65c0.12,0,0.18,0.06,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-4.86
c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16H171.38z"/>
<path d="M187.4,118.32c0.13,0,0.18,0.08,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-3.64v2.02h3.21c0.12,0,0.18,0.06,0.18,0.16
v0.87c0,0.11-0.06,0.16-0.18,0.16h-3.21v2.01h3.65c0.12,0,0.18,0.06,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-4.86
c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16H187.4z"/>
<path d="M197.47,126.01c-0.16,0-0.22-0.04-0.31-0.16l-4.17-5.33v5.33c0,0.11-0.06,0.16-0.18,0.16h-1.03
c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.04c0.16,0,0.23,0.07,0.32,0.16l4.16,5.33v-5.33
c0-0.11,0.06-0.16,0.18-0.16h1.04c0.12,0,0.18,0.06,0.18,0.16v7.36c0,0.11-0.06,0.16-0.18,0.16H197.47z"/>
<path d="M208.78,119.78c-0.04,0.05-0.1,0.09-0.18,0.09c-0.04,0-0.07-0.01-0.12-0.03c-0.48-0.26-0.94-0.39-1.56-0.39
c-1.71,0-2.98,1.22-2.98,2.74c0,1.52,1.27,2.73,2.98,2.73c0.53,0,1.12-0.12,1.6-0.42c0.05-0.03,0.09-0.04,0.12-0.04
c0.06,0,0.11,0.03,0.16,0.09l0.54,0.75c0.07,0.1,0.05,0.19-0.06,0.25c-0.7,0.39-1.48,0.59-2.36,0.59c-2.53,0-4.37-1.71-4.37-3.95
c0-2.22,1.84-3.97,4.37-3.97c0.92,0,1.71,0.24,2.33,0.59c0.11,0.06,0.13,0.13,0.07,0.23L208.78,119.78z"/>
<path d="M219.34,125.83c0.09,0.12,0.06,0.18-0.1,0.18h-1.15c-0.13,0-0.24-0.06-0.33-0.16l-1.88-2.47h-1.07v2.47
c0,0.11-0.06,0.16-0.18,0.16h-1.03c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h2.84c1.67,0,2.8,1.12,2.8,2.53
c0,1.44-1.22,2.15-1.9,2.33L219.34,125.83z M214.81,119.47v2.75h1.32c1.11,0,1.71-0.61,1.71-1.37c0-0.76-0.6-1.37-1.71-1.37
H214.81z"/>
<path d="M225.65,121.16l1.92-2.75c0.05-0.07,0.12-0.1,0.21-0.1h1.2c0.1,0,0.12,0.09,0.07,0.16l-2.68,3.78v3.58
c0,0.11-0.06,0.16-0.18,0.16h-1.04c-0.12,0-0.18-0.06-0.18-0.16v-3.58l-2.7-3.78c-0.05-0.08-0.02-0.16,0.07-0.16h1.21
c0.1,0,0.17,0.03,0.21,0.1L225.65,121.16z"/>
<path d="M232.88,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h2.81c1.7,0,2.78,1.12,2.78,2.53
c0,1.36-1.16,2.56-2.78,2.56h-1.6v2.44c0,0.11-0.06,0.16-0.18,0.16H232.88z M234.09,122.26h1.29c1.02,0,1.68-0.55,1.68-1.42
c0-0.88-0.66-1.37-1.68-1.37h-1.29V122.26z"/>
<path d="M244.19,126.01c-0.12,0-0.18-0.06-0.18-0.16v-6.3h-2.36c-0.12,0-0.18-0.06-0.18-0.16v-0.9c0-0.11,0.06-0.16,0.18-0.16
h6.1c0.12,0,0.18,0.06,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-2.36v6.3c0,0.11-0.06,0.16-0.18,0.16H244.19z"/>
<path d="M251.84,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.02c0.12,0,0.18,0.06,0.18,0.16v7.36
c0,0.11-0.06,0.16-0.18,0.16H251.84z"/>
<path d="M265.64,122.17c0,2.2-1.9,3.95-4.38,3.95c-2.48,0-4.37-1.71-4.37-3.95c0-2.22,1.89-3.97,4.37-3.97
C263.74,118.21,265.64,119.95,265.64,122.17z M264.24,122.17c0-1.52-1.27-2.74-2.98-2.74c-1.71,0-2.98,1.22-2.98,2.74
c0,1.52,1.27,2.73,2.98,2.73C262.95,124.9,264.24,123.69,264.24,122.17z"/>
<path d="M275.35,126.01c-0.16,0-0.22-0.04-0.31-0.16l-4.17-5.33v5.33c0,0.11-0.06,0.16-0.18,0.16h-1.03
c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.04c0.16,0,0.23,0.07,0.32,0.16l4.16,5.33v-5.33
c0-0.11,0.06-0.16,0.18-0.16h1.04c0.12,0,0.18,0.06,0.18,0.16v7.36c0,0.11-0.06,0.16-0.18,0.16H275.35z"/>
</g>
</g>
<g>
<path d="M109.23,72.27c-0.38-0.68-0.75-1.38-1.15-2.06c-0.34-0.57-0.67-1.34-1.04-1.9c-0.45-0.69-0.71-1.22-1.19-1.9
c-0.52-0.74-0.9-1.28-1.47-1.98c-0.63-0.79-1.47-1.74-2.14-2.5c-0.52-0.59-1.3-1.42-1.83-2.01c-0.16-0.18-2.04-2.14-3.51-3.45
c-2.03-1.76-4.38-3.5-5.64-4.39c-0.94-0.72-1.83-1.25-2.92-2.02c-0.08-0.04-0.19-0.09-0.21-0.16c-0.08-0.33-0.33-0.33-0.59-0.32
c-0.63,0.03-1.26,0.07-1.89,0.06c-0.71-0.01-1.42,0.04-2.13,0.08c-0.29,0.01-0.54-0.03-0.74-0.23c-0.13,0.07-0.24,0.18-0.35,0.19
c-0.52,0.03-1.05,0.03-1.58,0.04c-0.2,0.01-0.35-0.07-0.4-0.25c-0.06-0.18-0.17-0.29-0.34-0.38c1.27-0.08,2.52-0.14,3.77-0.18
c0.18-0.01,3.41,0.01,3.57-0.2c0.12-0.16,0.07-0.28-0.13-0.36c-0.08-0.03-0.18-0.06-0.23-0.12c-0.14-0.15-0.27-0.12-0.43-0.03
c-0.14,0.07-0.29,0.14-0.44,0.15c-0.62,0.03-1.24,0.04-1.85,0.06c-0.55,0.02-3.59,0.11-4.58,0.08c-0.01-0.13-0.07-0.27-0.03-0.35
c0.13-0.22,0.03-0.41-0.03-0.6c-0.1-0.34-0.19-0.68-0.16-1.04c0.02-0.3-0.1-0.45-0.45-0.57c-0.04,0.21-0.05,0.41-0.12,0.61
c-0.05,0.14-0.02,0.24,0.03,0.37c0.21,0.52,0.4,1.04,0.61,1.63c-0.91,0-1.76,0-2.66,0c0.06-0.71-0.05-1.42,0.01-2.13
c-0.21-0.32-0.35-0.33-0.6-0.49c0.36-0.21,0.72-0.12,1.09,0.04c0.11-0.24,0.32-0.21,0.52-0.21c0.72-0.01,1.44,0,2.16-0.03
c0.77-0.03,1.54-0.08,2.32,0.02c0.44,0.06,0.87,0.12,1.28,0.18c0.14,0.79,0.61,1.09,1.09,1.59c0.26,0.27,0.69,0.61,1.05,0.73
c0.02-0.02,0.03-0.04,0.05-0.07c-0.53-0.72-1.06-1.45-1.59-2.17c0-0.04,0-0.08,0.01-0.12c0.43-0.04,0.4-0.32,0.15-0.83
c-0.07-0.16-0.1-0.33-0.2-0.43c-0.2-0.21-0.13-2.15-0.14-2.45c0-0.26,0.03-0.53,0.04-0.79c0.02-0.9-0.01-1.8,0.06-2.69
c0.03-0.36-0.04-0.7-0.06-1.05c0-0.07-0.09-0.16-0.16-0.19c-0.09-0.04-0.21-0.03-0.31-0.03c-0.59,0.03-1.17,0.06-1.76,0.09
c-0.03,0-0.43,0-0.46,0c-0.56-0.07-0.84-0.08-1.4-0.1c-0.92-0.04-1.51,0.05-2.43,0.04c-0.79,0-1.71-0.01-2.5-0.03
c-0.45-0.01-1.04-0.03-1.48-0.05c-0.28-0.01-0.6-0.01-0.97-0.03c0.49-0.39,1.06-0.67,1.56-0.6c0.42,0.06,0.82,0.1,1.25,0.04
c0.99-0.14,1.99-0.15,2.99-0.09c0.83,0.05,1.65,0.15,2.47,0.31c0.36,0.07,1.06,0.07,1.34,0c0-0.03-0.62-0.03-0.88-0.09
c-0.25-0.05-0.64-0.09-0.89-0.14c-0.25-0.05-0.51-0.08-0.83-0.14c-0.11-0.17,0.29-0.14,0.31-0.14c0.8,0.01,1.43,0.07,2.23,0.1
c0.85,0.03,1.7,0.07,2.56,0.1c0.22,0.01,0.36-0.11,0.5-0.34c0.03-0.3-0.65-0.21-0.95-0.31c-0.01-0.1-0.03-0.19-0.04-0.29
c-0.18-0.12-0.35-0.1-0.51-0.08c-0.27,0.04-0.42-0.1-0.57-0.27c-0.16-0.17-0.33-0.34-0.48-0.51c-0.11-0.12-0.24-0.15-0.41-0.07
c-0.01,0.25-0.01,0.64,0.13,0.94c-0.46-0.01-1.31,0.01-1.54,0.01c-0.76,0.01-1.06-0.01-1.86,0c-0.64,0.06-1.4,0.21-1.86,0.01
c-0.09-0.17-0.16-0.64-0.32-0.66c-0.33,0.12-0.33,0.12-0.38,0.5C76.47,35.92,76.5,36,76.25,36c-0.24,0.04-0.42,0.03-0.61-0.07
c0.06-0.21,0.11-0.35,0.13-0.49c0.04-0.21-0.07-0.33-0.28-0.33c-0.21-0.01-0.28,0.14-0.37,0.3c-0.29,0.52-0.43,0.58-1.01,0.44
c0.01-0.04,0-0.08,0.02-0.11c0.12-0.15,0.13-0.32-0.04-0.41c-0.12-0.06-0.35-0.09-0.44-0.02c-0.21,0.15-0.37,0.38-0.57,0.6
c-0.23,0-0.5,0-0.78,0c0-0.06-0.01-0.1,0-0.14c0.04-0.14,0.04-0.29-0.11-0.34c-0.15-0.05-0.29-0.01-0.38,0.18
c-0.09,0.22-0.3,0.33-0.54,0.33c-0.24,0-0.49,0-0.8,0c0.24-0.27,0.43-0.51,0.64-0.74c0.17-0.19,0.18-0.2-0.01-0.45
c-0.17,0.02-0.33,0.05-0.51,0.08c-0.11,0.65-0.61,0.91-1.13,1.1c-0.36,0.13-0.76,0.17-1.15,0.25c-0.13,0.02-0.23,0.06-0.25,0.2
c-0.01,0.13,0.06,0.19,0.17,0.22c0.2,0.06,0.28,0.19,0.3,0.41c0.04,0.42,0.49,0.58,0.63,0.94c0.16-0.01,0.15,0.1,0.16,0.21
c0.03,0.33,0.08,0.65,0.11,0.98c0.06,0.56,0.18,1.13,0.14,1.69c-0.08,1.06-0.01,2.11,0.01,3.16c0,0.26,0.03,0.52,0.04,0.76
c0.47,0.34,0.73,0.19,0.79-0.5c0-0.01,0-0.03,0-0.04c-0.07-1.09-0.07-2.18-0.21-3.26c-0.12-0.91-0.07-1.84,0.12-2.76
c0.02-0.09,0.05-0.17,0.07-0.27c0.2-0.01,0.38-0.02,0.61-0.03c0,0.18,0,0.33,0,0.49c-0.01,0.87-0.02,1.74-0.02,2.61
c0.01,0.94,0.03,1.87,0.05,2.81c0,0.17,0,0.34,0.03,0.51c0.01,0.08,0.07,0.15,0.16,0.32c0.06-0.16,0.1-0.24,0.13-0.33
c0.09,0.1,0.12,0.17,0.16,0.18c0.12,0.04,0.25,0.1,0.36,0.07c0.06-0.02,0.11-0.19,0.12-0.29c0-0.25-0.01-0.5-0.04-0.75
c-0.06-0.5-0.16-0.99-0.19-1.49c-0.04-0.93-0.04-1.87-0.05-2.8c0-0.18,0.04-0.36,0.06-0.54c0.05-0.03,0.09-0.06,0.12-0.07
c0.14-0.02,0.27-0.04,0.27-0.22c0-0.15-0.1-0.18-0.23-0.21c-0.11-0.03-0.2-0.14-0.3-0.21c0.01-0.03,0.02-0.05,0.03-0.08
c0.29,0,0.59,0,0.9,0c0.04,0.17,0.12,0.36,0.13,0.55c0.06,0.89,0.12,1.79,0.16,2.69c0.05,1.11,0.07,2.21,0.11,3.32
c0.01,0.23,0.03,0.46,0.05,0.72c-1.57,0-3.1,0-4.62,0c-0.07,0.33-0.04,0.38,0.22,0.52c0.4,0.2,0.46,0.47,0.17,0.82
c-0.17,0.2-0.34,0.4-0.52,0.59c-0.12,0.13-0.24,0.25-0.37,0.35c-0.2,0.15-0.42,0.27-0.63,0.41c-0.39,0.27-0.79,0.51-1.29,0.55
c-0.27,0.02-0.54,0.09-0.79,0.14c-0.12,0.37-0.12,0.37,0.09,0.61c-0.09,0.08,9.04-0.12,13.65-0.05c-0.04,0.11,0.02,0.53-0.02,0.67
c-0.4,0-0.8,0.01-1.19,0c-0.16-0.01-0.32-0.06-0.54-0.11c-0.17,0.22-0.48,0.17-0.79,0.17c-0.46,0.01-0.92,0.04-1.38,0.05
c-0.14,0-0.28-0.02-0.43-0.04c0-0.11,0-0.2,0-0.29c-0.36-0.13-0.43,0.25-0.59,0.33c-0.33-0.04-7.18,0.04-7.44,0.03
c-0.34-0.01-0.68-0.06-1.02-0.08c-0.35-0.02-0.69,0.14-1.04-0.04c-0.13-0.07-0.25,0.02-0.34,0.14c-0.08,0.12-0.05,0.23,0.05,0.32
c0.04,0.04,0.12,0.05,0.23,0.1c-0.41,0.24-0.7,0.4-0.98,0.59c-1.21,0.82-2.53,1.47-3.66,2.4c-0.34,0.28-0.73,0.5-1.07,0.78
c-0.32,0.27-2.14,1.65-2.7,2.09c-0.6,0.47-5.88,5.57-6.81,6.58c-0.39,0.43-0.75,0.88-1.16,1.28c-0.38,0.37-1.25,1.52-1.37,1.67
c-0.88,1.06-4.49,5.8-5.9,8.45c-1.36,2.37-3.84,8-4.06,8.63c-0.26,0.76-0.52,1.51-0.67,2.3c-0.1,0.49-0.92,3.62-1.05,4.36
c-0.12,0.69-0.44,3.2-0.47,3.45c-0.1,0.9-0.19,1.81-0.28,2.75c-0.2,0.03-0.41,0.07-0.63,0.1c-0.16,0.02-0.25,0.12-0.26,0.26
c-0.01,0.08,0.07,0.18,0.12,0.26c0.04,0.06,0.12,0.1,0.16,0.16c0.16,0.21,0.38,0.26,0.63,0.25c0.78-0.03,1.55-0.09,2.33-0.1
c0.75-0.01,1.5,0,2.25,0.04c0.71,0.03,11.13,0.14,15.64,0.14c0.72,0,3.91-0.06,4.78-0.06c0.3,0,2.91-0.1,3.91-0.16
c1-0.06,11.01-0.24,11.59-0.23c0.05,0,4.25,0.13,5.42,0.14c0.21,0,2.03,0.06,2.72,0.02c1.26-0.08,15.17,0.11,16.68,0.09
c0.63-0.01,6.52-0.07,7.29-0.1c0.55-0.02,1.11-0.02,1.66-0.04c0.09,0,0.19-0.04,0.3-0.07c-0.07-0.25-0.22-0.33-0.41-0.38
c-0.14-0.04-0.27-0.09-0.41-0.14c-0.15-0.04-0.29-0.11-0.44-0.12c-0.25-0.02-3.64,0.06-4.56,0.11c-0.43,0.02-3.52-0.02-3.93-0.01
c-0.08,0-0.16-0.01-0.24-0.02c-0.25-0.02-0.49-0.05-0.74-0.07c-0.24-0.02-0.48-0.04-0.71-0.03c-0.83,0.02-5.75,0.01-6.65,0.02
c-0.63,0.01-5.53-0.01-5.83-0.01c-0.81-0.02-18.52,0.09-19.63,0.08c-0.33,0-4.97,0.16-5.02,0.16c-0.28,0.01-5.25-0.03-5.59-0.03
c-0.87,0.01-3.11,0-3.36,0.01c-0.51,0.01-2.73,0.01-3.32,0.02c-0.33,0-0.36,0-0.42-0.32c-0.17-0.9-0.18-1.81-0.15-2.72
c0.03-0.71,0.18-1.42,0.2-2.12c0.03-1.3,0.29-2.56,0.56-3.83c0.02-0.08,0.04-0.22,0-0.25c-0.11-0.08-0.06-0.16-0.04-0.24
c0.24-0.97,0.49-1.94,0.73-2.92c0.24-0.96,0.46-1.92,0.71-2.88c0.2-0.74,0.41-1.47,0.65-2.2c0.22-0.68,0.44-1.36,0.73-2
c0.4-0.9,0.87-1.78,1.32-2.66c0.51-1,1.03-1.99,1.53-2.99c0.33-0.66,0.63-1.34,1.07-1.93c0.04-0.05,0.09-0.15,0.07-0.17
c-0.2-0.18-0.02-0.3,0.06-0.43c0.25-0.4,0.51-0.8,0.77-1.2c0.49-0.75,0.97-1.5,1.47-2.25c0.73-1.08,3.38-4.74,4.06-5.43
c0.59-0.59,2.32-2.61,2.62-2.92c0.42-0.43,1.09-1.41,0.97-1.48c0.33-0.34,0.62-0.67,0.95-0.98c0.47-0.44,0.96-0.86,1.43-1.29
c0.27-0.25,1.37-1.21,1.53-1.57c0.01-0.03,0.08-0.05,0.13-0.06c0.35-0.08,0.66-0.23,0.83-0.56c0.17,0.01,0.33,0.01,0.54,0.02
c-0.08,0.09-0.12,0.14-0.16,0.19c-0.25,0.3-0.51,0.6-0.76,0.9c-0.34,0.42-0.67,0.84-1.01,1.27c-0.43,0.54-0.91,1.04-1.21,1.68
c-0.19,0.4-0.49,0.75-0.73,1.13c-0.7,1.12-1.41,2.22-2.04,3.39c-0.59,1.1-4.34,7.71-4.36,7.76c-0.18,0.37-0.35,0.74-0.52,1.1
c-0.13,0.29-0.26,0.58-0.4,0.86c-0.32,0.63-0.66,1.26-0.97,1.9c-0.21,0.44-0.41,0.88-0.58,1.34c-0.43,1.11-0.84,2.22-1.25,3.33
c-0.27,0.73-0.51,1.47-0.75,2.2c-0.21,0.65-0.42,1.31-0.6,1.96c-0.17,0.63-0.32,1.28-0.47,1.92c-0.02,0.1-0.05,0.21-0.07,0.31
c-0.02,0.1-0.04,0.2-0.05,0.3c0.02,0,0.03,0.01,0.05,0.01c0.06-0.14,0.13-0.28,0.19-0.42c-0.04,0.35-0.12,0.67-0.21,1
c-0.05,0.19-0.12,0.37-0.17,0.52c-0.22-0.14-0.42-0.3-0.65-0.41c-0.26-0.13-0.48-0.27-0.61-0.54c-0.05-0.1-0.14-0.19-0.24-0.26
c-0.15-0.11-0.3-0.19-0.38-0.37c-0.03-0.07-0.18-0.12-0.26-0.11c-0.14,0.02-0.28,0.07-0.39,0.15c-0.53,0.34-1.05,0.7-1.59,1.02
c-0.19,0.11-0.3,0.24-0.33,0.45c-0.02,0.22,0.15,0.29,0.31,0.34c-0.09,0.22-0.37,0.33-0.26,0.6c0.47,0.02,0.93,0.05,1.21-0.45
c-0.13-0.01-0.24-0.02-0.35-0.02c0.16-0.67,0.56-1.06,1.23-1.18c0.25,0.18,0.51,0.37,0.8,0.58c-0.06,0.57,0.94,0.86,1.43,0.48
c0,0.08,0.01,0.14,0,0.21c-0.1,0.52-0.2,1.03-0.29,1.55c-0.15,0.83-0.3,1.66-0.44,2.49c-0.06,0.36-0.1,0.73-0.13,1.1
c-0.04,0.58-0.03,1.16-0.08,1.73c-0.05,0.56-0.31,4-0.28,4.67c0,0,0.03,0.4,0.03,0.19c0.08-0.17,0.09-0.34,0.14-0.5
c0.22-0.76,0.31-1.55,0.39-2.33c0.09-0.84,0.19-1.68,0.31-2.51c0.17-1.09,0.37-2.18,0.55-3.27c0.15-0.9,0.3-1.79,0.46-2.69
c0.15-0.83,0.29-1.66,0.47-2.48c0.12-0.54,0.32-1.06,0.47-1.59c0.16-0.6,0.3-1.2,0.46-1.8c0.24-0.88,0.49-1.75,0.9-2.57
c0.15-0.3,0.27-0.6,0.4-0.91c0.02-0.06,0.06-0.17,0.04-0.18c-0.29-0.16-0.06-0.37,0-0.5c0.29-0.72,0.62-1.43,0.93-2.14
c0.06-0.13,0.12-0.26,0.18-0.39c0.06-0.12,1.18-2.1,1.63-3.04c0.57-1.19,1.11-2.39,1.72-3.56c0.36-0.7,0.7-1.42,1.23-2.02
c0.03-0.04,0.04-0.09,0.06-0.14c0.07-0.17,0.12-0.35,0.21-0.5c0.38-0.68,0.79-1.34,1.17-2.02c0.56-1.02,1.24-1.96,1.92-2.9
c0.36-0.5,0.7-1.02,1.04-1.54c0.25-0.38,0.46-0.8,0.76-1.15c0.5-0.6,1.05-1.17,1.59-1.76c-0.09-0.22-0.09-0.24,0.11-0.43
c0.4-0.38,1.05-1.28,0.96-1.32c0.23-0.21,0.42-0.31,0.61-0.42c0.38-0.21,0.38-0.21,0.62-0.15c-0.12,0.14-0.23,0.27-0.34,0.39
c-0.44,0.51-0.88,1.01-1.3,1.54c-0.28,0.35-0.48,0.76-0.76,1.11c-0.8,1.03-1.48,2.14-2.16,3.25c-0.37,0.59-0.78,1.16-1.12,1.77
c-0.53,0.95-1.05,1.92-1.53,2.9c-0.32,0.66-0.63,1.33-0.84,2.03c-0.19,0.63-0.39,1.26-0.66,1.86c-0.2,0.44-0.42,0.88-0.59,1.33
c-0.32,0.82-0.6,1.66-0.93,2.48c-0.44,1.09-1.87,5.3-2.1,6.34c-0.19,0.86-0.35,1.73-0.58,2.58c-0.25,0.94-0.55,1.87-0.74,2.83
c-0.09,0.45-0.26,0.89-0.32,1.34c-0.13,0.99-0.17,2-0.33,2.98c-0.18,1.09-0.14,2.18-0.21,3.27c-0.06,0.83,0.03,2.61,0.06,2.74
c0.3-1.16,0.13-2.28,0.36-3.36c0.02,0.14,0.05,0.28,0.07,0.42c0.11-0.3,0.15-0.59,0.18-0.88c0.21-2.3,0.63-4.57,1.12-6.82
c0.34-1.56,0.71-3.11,1.09-4.66c0.34-1.39,0.69-2.78,1.25-4.1c0.02-0.06,0.01-0.13,0.02-0.19c0.01-0.12,0.01-0.24,0.04-0.35
c0.31-1.2,0.77-2.35,1.24-3.5c0.45-1.08,0.93-2.15,1.41-3.23c0.48-1.07,0.97-2.14,1.46-3.2c0.28-0.6,0.57-1.19,0.86-1.78
c0.24-0.48,0.47-0.98,0.73-1.44c0.35-0.61,0.75-1.2,1.08-1.81c0.55-1.01,1.23-1.93,1.91-2.86c0.11-0.15,1.38-1.64,2-2.29
c0.22-0.23,0.48-0.42,0.54-0.79c0.09,0.01,0.19,0.02,0.3,0.03c-0.08,0.14-0.16,0.25-0.21,0.36c-0.37,0.82-0.73,1.64-1.09,2.45
c-0.34,0.76-0.56,1.56-0.84,2.34c-0.27,0.79-0.45,1.59-0.68,2.39c-0.13,0.48-0.27,0.96-0.41,1.44c-0.3,1.05-0.61,2.1-0.91,3.15
c-0.19,0.68-0.36,1.37-0.56,2.06c-0.19,0.67-0.4,1.33-0.61,2c-0.15,0.48-0.32,0.96-0.47,1.4c-0.35-0.21-0.71-0.38-1.03-0.6
c-0.46-0.32-0.97-0.32-1.48-0.26c-0.28,0.03-0.57,0.13-0.72,0.42c-0.27,0.51-0.53,1.03-0.78,1.55c-0.13,0.27-0.07,0.41,0.21,0.63
c0.34-0.19,0.65-0.39,0.69-0.84c0.01-0.11,0.12-0.24,0.21-0.32c0.2-0.17,0.42-0.32,0.63-0.48c0.41-0.31,0.71-0.25,0.96,0.2
c0.09,0.16,0.18,0.32,0.42,0.32c0-0.19,0-0.37,0-0.57c0.27,0.11,0.48,0.36,0.8,0.19c-0.12,0.43-0.22,0.8-0.32,1.18
c-0.15,0.58-1.63,9.63-1.69,10.38c-0.05,0.6-0.12,1.21-0.2,1.81c-0.07,0.57-0.12,1.15-0.23,1.72c-0.15,0.76-0.16,1.52-0.16,2.28
c0,0.34,0.12,0.69,0.07,1.01c-0.1,0.57-0.09,1.14-0.07,1.72c0.01,0.25,0.01,0.5-0.02,0.75c-0.04,0.31-0.11,0.62-0.16,0.93
c-0.01,0.06,0.03,0.13,0.05,0.25c0.08-0.52,0.17-0.97,0.23-1.43c0.08-0.6,0.14-1.21,0.21-1.81c0.08-0.63,0.17-1.25,0.24-1.88
c0.07-0.72,0.12-1.44,0.18-2.17c0.01-0.09,0.05-0.17,0.07-0.26c0.02,0.01,0.04,0.69,0.03,1.02c0.07-0.12,0.12-0.24,0.13-0.37
c0.08-0.77,0.15-1.55,0.21-2.33c0.11-1.31,0.27-2.62,0.5-3.91c0-0.03,0.01-0.05,0.01-0.08c0.27-1.02,0.55-2.03,0.82-3.05
c0.06-0.22,0.14-0.45,0.1-0.76c-0.19,0.3-0.17,0.64-0.42,0.83c0.09-0.41,0.27-0.8,0.3-1.21c0.06-0.71,0.47-1.33,0.41-2.06
c0-0.01,0.01-0.03,0.01-0.04c0.08-0.26,0.09-0.52,0.13-0.78c0.1-0.6,0.24-1.19,0.39-1.78c0.42-1.63,0.84-3.26,1.27-4.89
c0.21-0.8,0.36-1.62,0.77-2.36c0.03-0.05,0.04-0.14,0.01-0.17c-0.12-0.11-0.06-0.21-0.03-0.32c0.06-0.23,0.1-0.46,0.17-0.69
c0.26-0.83,0.52-1.66,0.79-2.48c0.06-0.19,0.17-0.35,0.24-0.54c0.1-0.27,0.16-0.55,0.27-0.82c0.3-0.73,0.58-1.47,0.92-2.19
c0.32-0.68,0.47-1.43,0.97-2.02c0.02-0.03,0.02-0.08,0.04-0.11c0.19-0.31,0.37-0.63,0.57-0.94c0.04-0.06,0.12-0.11,0.19-0.12
c0.21-0.01,0.43-0.01,0.66-0.01c-0.09,0.3-0.17,0.57-0.25,0.83c-0.15,0.49-0.31,0.97-0.44,1.47c-0.25,0.94-0.46,1.89-0.71,2.83
c-0.23,0.84-1.89,7.97-1.98,8.69c-0.07,0.56-1.18,5.38-1.35,9.4c-0.03,0.97-0.13,1.94-0.21,2.92c-0.02,0.3-0.04,0.6-0.07,0.9
c-0.05,0.48-0.14,0.96-0.16,1.45c-0.04,0.99-0.05,1.98-0.08,2.97c-0.02,0.79-0.06,1.58-0.08,2.37c-0.01,0.49,0,0.97,0,1.46
c0.1,0.07,0.22-0.88,0.29-1.4c0.01-0.08,0.02-0.17,0.06-0.22c0.11-0.12,0.08-0.22,0.06-0.36c-0.05-0.31-0.02-0.62,0.28-0.82
c-0.16-0.33-0.11-0.65-0.06-0.98c0.06-0.46,0.11-0.91,0.14-1.37c0.06-0.98,0.1-1.97,0.16-2.95c0.01-0.23,0.04-0.47,0.19-0.77
c0,0.72,0,1.35,0,1.98c0.05-0.17,0.04-0.33,0.05-0.5c0.03-0.46,0.06-0.92,0.09-1.38c0.07-1.13,0.12-2.26,0.23-3.39
c0.09-0.9,0.24-1.8,0.38-2.7c0.03-0.2,1.07-5.9,1.25-6.86c0.2-1.04,0.35-2.1,0.56-3.14c0.3-1.46,0.63-2.91,0.95-4.36
c0.23-1.06,0.49-2.1,0.94-3.09c0.05-0.1,0.03-0.23,0.05-0.35c-0.02,0-0.05-0.01-0.07-0.01c-0.09,0.16-0.18,0.32-0.26,0.48
c0.1-0.85,0.67-2.45,1.06-3.12c0.2,0.01,0.41,0.02,0.6,0.03c0.06,0.21,0.11,0.37,0.16,0.53c0.27,0.81,0.55,1.62,0.82,2.43
c0.33,0.97,0.66,1.94,0.92,2.93c0.14,0.56,0.28,1.12,0.4,1.69c0.11,0.53,0.18,1.06,0.27,1.59c0.07,0.4,0.16,0.79,0.23,1.19
c0.12,0.65,0.25,1.29,0.33,1.94c0.03,0.27,0.18,0.5,0.21,0.76c0.08,0.61,0.19,1.22,0.24,1.84c0.09,1.07,0.14,2.15,0.23,3.22
c0.06,0.76,0.14,1.52,0.23,2.28c0.09,0.76,0.27,1.49,0.45,2.23c0.17,0.69,0.34,1.39,0.28,2.1c-0.08,0.87,0.01,2.53,0.11,3.48
c0.06-0.3,0.11-1.32,0.16-1.53c0.09-0.03-0.05-2.85-0.11-4.23c0.03,0,0.05-0.01,0.08-0.01c0.04,0.23,0.08,0.46,0.12,0.69
c0.01-0.42,0.01-0.84-0.04-1.25c-0.1-0.88-0.23-1.75-0.35-2.62c-0.01-0.09-0.04-0.18-0.06-0.27c-0.05-0.19-0.11-0.38-0.15-0.57
c-0.16-0.92-0.3-1.85-0.45-2.77c-0.08-0.48-0.17-0.96-0.24-1.44c-0.11-0.72-0.2-1.43-0.3-2.15c-0.07-0.51-0.15-1.01-0.24-1.52
c-0.09-0.51-0.05-1.02-0.12-1.53c-0.07-0.48-0.11-0.96-0.23-1.43c-0.25-1-0.54-1.99-0.76-2.99c-0.28-1.25-0.52-2.5-0.93-3.72
c-0.09-0.26-0.09-0.56-0.13-0.84c0.2-0.03,0.34-0.04,0.48-0.08c0.22-0.06,0.35,0,0.44,0.21c0.11,0.26,0.26,0.51,0.39,0.76
c0.08,0.15,0.2,0.28,0.26,0.43c0.14,0.38,0.43,0.7,0.44,1.13c0,0.05,0.04,0.1,0.07,0.14c0.41,0.65,1.79,3.87,1.82,3.96
c0.36,0.85,0.54,1.76,0.8,2.64c0.04,0.13,0.06,0.25,0.09,0.38c0.05,0.21,0.08,0.42,0.15,0.61c0.27,0.78,0.54,1.57,0.55,2.41
c0,0.07,0.01,0.13,0.03,0.2c0.29,1.12,0.65,2.22,0.8,3.38c0.07,0.52,0.19,1.03,0.27,1.55c0.14,0.86,0.26,1.72,0.39,2.58
c0.02,0.13,0.1,1.12,0.23,1.1c0.06-0.49-0.21-1.9-0.11-2.39c0.25,0.93,0.25,1.91,0.53,2.83c-0.04-0.69-0.1-1.37-0.2-2.05
c-0.26-1.71-0.53-3.41-0.81-5.11c-0.16-0.96-0.34-1.92-0.51-2.88c-0.08-0.42-1.13-4.27-1.44-5.08c-0.45-1.17-0.88-2.35-1.35-3.51
c-0.3-0.74-0.68-1.46-1-2.19c-0.15-0.35-0.39-0.69-0.38-1.12c0.23-0.05,0.42-0.06,0.59,0.12c0.37,0.39,2.71,3.45,3.14,3.98
c0.81,1,1.58,2.04,2.14,3.21c0.29,0.61,0.64,1.18,0.93,1.79c0.38,0.77,0.75,1.54,1.09,2.33c0.52,1.18,1.01,2.37,1.52,3.56
c0.2,0.46,0.39,0.92,0.59,1.38c0.07,0.15,0.12,0.31,0.19,0.46c0.12,0.28,0.11,0.62,0.4,0.82c0.02-0.04,0.04-0.09,0.03-0.13
c-0.28-0.71-0.55-1.42-0.84-2.12c-0.49-1.22-0.99-2.43-1.49-3.65c-0.28-0.68-0.59-1.35-0.88-2.03c-0.28-0.66-0.53-1.33-0.82-1.99
c-0.16-0.36-0.37-0.7-0.55-1.04c-0.12-0.23-0.25-0.47-0.37-0.7c0.02-0.01,0.04-0.03,0.07-0.04c0.13,0.19,0.27,0.37,0.39,0.57
c0.2,0.31,0.39,0.62,0.58,0.92c0,0-1.16-2.2-1.76-3.17c-0.43-0.69-0.93-1.35-1.4-2.01c-0.39-0.55-0.78-1.1-1.18-1.64
c-0.13-0.18-0.28-0.34-0.46-0.55c0.23,0,0.41-0.01,0.59,0c0.18,0.01,0.31,0.12,0.3,0.3c-0.01,0.21,0.1,0.31,0.24,0.41
c0.12,0.09,0.22,0.19,0.32,0.29c0.39,0.39,0.76,0.81,1.16,1.19c0.64,0.61,1.23,1.26,1.75,1.98c0.36,0.5,0.72,1.01,1.08,1.51
c0.11,0.16,0.28,0.32,0.31,0.5c0.05,0.34,0.27,0.54,0.45,0.79c0.31,0.43,0.64,0.84,0.93,1.28c0.36,0.54,0.72,1.1,1.05,1.66
c0.35,0.62,0.69,1.24,0.99,1.89c0.3,0.64,0.55,1.31,0.84,1.96c0.08,0.18,0.18,0.34,0.27,0.51c0.03-0.01-0.09-0.36-0.13-0.52
c-0.04-0.16-0.86-2.36-0.97-2.59c-0.41-0.84-0.8-1.69-1.22-2.52c-0.33-0.66-0.69-1.31-1.03-1.96c-0.03-0.06-0.09-0.11-0.09-0.17
c-0.02-0.38-0.29-0.62-0.5-0.89c-0.42-0.55-0.85-1.1-1.29-1.65c-0.29-0.36-0.58-0.71-0.88-1.07c-0.26-0.31-0.51-0.63-0.76-0.95
c-0.01-0.01,0.51,0.26,0.72,0.44c0.68,0.59,1.38,1.17,2.02,1.81c0.47,0.46,0.9,0.96,1.28,1.49c0.31,0.43,0.68,0.79,1.1,1.12
c-0.47-0.81-1.12-1.5-1.64-2.28c0.39,0.29,0.77,0.59,1.1,0.94c0.62,0.66,1.21,1.34,1.81,2.03c0.43,0.5,1.99,2.35,2.1,2.43
c0.05-0.33-0.2-0.42-0.31-0.58c-0.39-0.55-0.79-1.09-1.2-1.62c-0.43-0.56-0.85-1.13-1.34-1.63c-1.2-1.21-2.43-2.38-3.65-3.56
c-0.39-0.38-0.77-0.76-1.16-1.13c-0.34-0.32-0.73-0.61-1.03-0.96c-0.26-0.31-0.57-0.48-0.94-0.59c-0.06-0.02-0.1-0.09-0.21-0.19
c0.44-0.04,0.8-0.07,1.16-0.11c-0.13,0.24-0.11,0.24-0.08,0.48c0.03,0.25,0.21,0.27,0.38,0.33c0.05,0.02,0.1,0.05,0.14,0.08
c0.53,0.36,1.06,0.72,1.59,1.1c0.75,0.53,1.37,1.2,2,1.87c0.39,0.42,0.77,0.86,1.15,1.29c0.01,0.01,0.04,0.01,0.06,0.02
c0.01-0.04,0.02-0.07,0.04-0.18c0.35,0.31,0.67,0.6,0.99,0.88c-0.45-0.63-1.01-1.16-1.45-1.8c0.3,0.19,3.84,3.56,4.54,3.85
c-0.92-1.18-1.9-2.25-2.95-3.25c0.44,0.24,0.81,0.58,1.17,0.91c0.38,0.35,0.75,0.71,1.11,1.07c0.34,0.35,1.06,0.96,1.38,1.33
c-0.06-0.11-0.43-0.55-0.5-0.67c0.05-0.02,0.09-0.04,0.11-0.06c0.03-0.02,0.04-0.06,0.07-0.09c-0.03-0.05-2.63-2.88-4.14-4
c-0.69-0.51-1.37-1.03-2.06-1.54c-0.54-0.4-1.09-0.78-1.64-1.17c-0.16-0.12-0.31-0.25-0.47-0.38c0.03-0.05,0.05-0.14,0.09-0.14
c0.1-0.01,0.22-0.02,0.31,0.02c0.22,0.11,0.43,0.25,0.63,0.38c0.66,0.41,1.31,0.83,1.96,1.25c0.58,0.37,1.16,0.73,1.73,1.1
c0.12,0.07,0.21,0.18,0.32,0.26c-0.02,0.02-0.03,0.04-0.05,0.06c-0.35-0.19-0.71-0.37-1.06-0.57c-0.32-0.18-0.62-0.39-0.93-0.57
c-0.09-0.05-0.19-0.08-0.29-0.12c0.11,0.11,0.21,0.23,0.33,0.32c0.57,0.44,1.16,0.87,1.74,1.3c0.15,0.11,0.48,0.29,0.5,0.27
c-0.32-0.26-0.64-0.53-0.96-0.79c0.1-0.03,0.15-0.01,0.21,0.03c0.93,0.56,1.81,1.19,2.66,1.87c0.07,0.06,0.15,0.11,0.22,0.16
c-0.12-0.16-0.07-0.43-0.19-0.59c1.18,0.92,5.41,4.74,5.46,4.77c0.53,0.58,1.5,1.54,1.6,1.65c0.09,0.1,1.46,1.8,2,2.45
c0.81,0.98,1.59,1.99,2.36,3c0.34,0.45,0.63,0.94,0.94,1.41c0.1,0.15,0.19,0.38,0.33,0.42c0.24,0.08,0.32,0.26,0.43,0.42
c0.29,0.44,0.57,0.88,0.84,1.33c0.67,1.13,1.34,2.26,2.01,3.38c0.03,0.06,0.1,0.09,0.15,0.13c-0.01-0.06-0.02-0.12-0.04-0.17
C109.64,73,109.43,72.64,109.23,72.27z M79.01,37.91c1.07,0.05,2.8-0.03,4.27,0.05c-0.04,0.44-0.08,0.85-0.11,1.26
c-0.01,0.13-0.03,0.26-0.02,0.39c0.07,1.09,0.14,2.18,0.21,3.27c0.01,0.24,0.01,0.47,0.01,0.71c0.01,0.16,0.01,0.3,0.14,0.44
c0.06,0.06,0.05,0.21,0.04,0.32c-0.01,0.27-0.1,0.54,0.05,0.81c-0.09-0.07-0.19-0.15-0.28-0.22c-0.54,0.17-3.35,0.29-4.2,0.21
c0.02-0.57-0.11-4.24-0.12-5.2c0-0.51,0.02-1.03,0.01-1.54C79.01,38.24,78.97,38.14,79.01,37.91z M78.3,37.95
c0.2,0.39,0.08,0.75,0.08,1.1c0,0.32,0,0.63,0,0.95c0,0.67-0.01,1.34,0.01,2.01c0.02,0.67,0.06,1.34,0.12,2.01
c0.03,0.39,0.13,0.77,0.19,1.16c-0.33,0-0.68,0-1.06,0c-0.04-0.47-0.05-0.92-0.11-1.37c-0.14-1.05-0.07-2.1-0.13-3.15
c-0.02-0.29-0.05-0.58-0.05-0.87c0-0.43,0.01-0.86,0.04-1.3c0.01-0.19-0.01-0.36-0.2-0.53C77.59,37.95,77.95,37.95,78.3,37.95z
M72.16,37.32c-0.41-0.06-2.45-0.08-2.8,0.03c-0.29-0.21-0.23-0.4-0.27-0.58c0.5-0.13,2.1-0.05,2.17-0.12
c0.29,0.03,0.74,0.01,1.01,0.05c0.08,0.01,0.38,0.11,0.39,0.15C72.67,37.01,72.26,37.34,72.16,37.32z M75.34,37.93
c0.35,0,0.83,0.04,1.2,0.04c0,0.29,0.11,0.59-0.08,0.87c-0.04,0.05-0.04,0.16-0.01,0.22c0.21,0.39,0.12,0.83,0.16,1.24
c0.06,0.65,0.16,1.29,0.16,1.95c0.01,0.96,0.1,1.93,0.1,2.96c-0.32,0-0.91-0.05-1.25-0.05c-0.14-0.86-0.18-1.42-0.22-2.32
c-0.04-0.91,0-1.93-0.05-2.84C75.31,39.4,75.25,38.54,75.34,37.93z M73.38,37.88c0.42,0,0.8,0,1.23,0c0,0.24,0,0.46,0,0.68
c0.01,0.33,0.03,0.65,0.03,0.98c0,0.66,0.04,1.32-0.01,1.97c-0.05,0.66-0.01,1.31,0.08,1.96c0.06,0.4,0.11,0.81,0.19,1.21
c0.03,0.13,0.12,0.25,0.19,0.38c-0.23,0.13-0.9,0.14-1.72,0.04c0.06-0.14,0.16-0.28,0.17-0.43c0.02-0.16-0.06-0.34-0.06-0.51
c-0.02-0.64-0.03-1.29-0.04-1.93c-0.01-0.34-0.01-0.68-0.01-1.02c0.01-0.75,0.02-1.5,0.03-2.25c0-0.21,0.03-0.41-0.07-0.62
C73.33,38.23,73.38,38.06,73.38,37.88z M76.21,48.65c-0.8,0.02-1.59,0.03-2.39,0.02c-0.52-0.01-3.97-0.05-5.17-0.06
c-0.51,0-1.01-0.06-1.61-0.1c0.56-0.39,1.09-0.67,1.49-1.07c0.39-0.4,0.73-0.89,0.91-1.49c0.41-0.06,0.84-0.17,1.31-0.08
c-0.11,0.45-0.24,0.82-0.68,0.99c-0.15,0.06-0.3,0.21-0.36,0.35c-0.12,0.31-0.38,0.46-0.61,0.66c-0.1,0.09-0.19,0.2-0.29,0.31
c0.17,0.13,0.28,0.08,0.4,0c0.73-0.51,1.34-1.13,1.79-1.9c0.13-0.22,0.28-0.33,0.52-0.34c0.27-0.02,0.54-0.06,0.82-0.1
c-0.13,0.68-0.45,1.24-0.93,1.71c-0.14,0.14-0.19,0.23-0.06,0.4c0.44-0.1,0.73-0.39,0.98-0.72c0.1-0.14,0.24-0.25,0.32-0.4
c0.06-0.1,0.12-0.24,0.1-0.35c-0.04-0.27-0.06-0.27,0.1-0.58c0.53,0,1.05,0.01,1.58,0c0.57-0.01,1.13-0.04,1.7-0.05
c0.08,0,0.17,0.03,0.25,0.05c-0.06,0.13-0.11,0.25-0.04,0.41c0.18,0.43,0.13,0.88,0.12,1.33c-0.01,0.25-0.04,0.5,0.15,0.71
c0.03,0.04,0.02,0.13,0.03,0.22C76.49,48.6,76.35,48.65,76.21,48.65z M37.01,93.47c-0.14,0.67-0.15,1.36-0.2,2.04
c0,0.04-0.01,0.08-0.02,0.12c-0.67,0-1.32,0-2,0c0-0.19-0.02-0.4,0-0.62c0.13-1.15,0.23-2.31,0.4-3.46
c0.12-0.79,0.32-1.57,0.49-2.36c0.04-0.18,0.06-0.36,0.1-0.54c0.19-0.87,0.35-1.76,0.6-2.62c0.25-0.88,0.59-1.75,0.89-2.62
c0.18-0.53,0.35-1.07,0.56-1.59c0.32-0.78,0.68-1.54,1.03-2.3c0.28-0.61,0.57-1.21,0.88-1.81c0.22-0.43,1.18-2.44,1.29-2.65
c1.48-2.85,4.81-7.4,5.22-7.84c1.2-1.86,4.38-4.84,4.8-5.28c0.2-0.21,4.78-4.13,5.35-4.66c0.66-0.6,1.33-1.2,2.06-1.72
c0.58-0.41,1.11-0.89,1.67-1.33c0.29-0.23,0.59-0.44,0.89-0.65c0.09-0.06,0.2-0.08,0.31-0.11c0.01,0.03,0.03,0.05,0.04,0.08
c-0.26,0.21-0.53,0.42-0.79,0.64c-1.14,0.93-2.32,1.83-3.41,2.82c-0.8,0.73-1.63,1.41-2.41,2.15c-0.58,0.54-1.89,1.93-2,2.06
c-0.94,1.06-3.29,3.75-3.51,4.05c-0.37,0.51-0.77,1.01-1.17,1.49c-0.28,0.34-1.16,1.44-1.31,1.67c-0.43,0.65-0.85,1.3-1.28,1.94
c-0.5,0.74-1.46,2.37-1.48,2.41c-0.17,0.31-0.33,0.62-0.5,0.93c-0.36,0.66-0.73,1.31-1.07,1.97c-0.26,0.5-0.51,1.01-0.76,1.52
c-0.31,0.62-0.64,1.23-0.93,1.86c-0.23,0.5-0.41,1.02-0.62,1.53c-0.14,0.34-0.29,0.67-0.43,1.01c-0.04,0.08-0.08,0.16-0.11,0.25
c-0.39,1.16-1.25,3.96-1.31,4.2c-0.07,0.29-0.14,0.58-0.21,0.87c-0.03,0.13-0.54,2.56-0.59,2.94
C37.36,90.77,37.07,93.17,37.01,93.47z M42.41,92.75c-0.16,0.61-0.25,2.7-0.25,3.16c-1.44-0.09-3-0.26-4.39-0.34
c-0.12-0.88,0.21-4.01,0.27-4.43c0.09-0.58,0.39-2.07,0.52-2.65c0.17-0.76,0.51-2.2,1-4.26c0.69-2.61,3.05-7.51,3.45-8.34
c0.26-0.54,0.57-1.06,0.86-1.59c0.06-0.12,0.11-0.24,0.18-0.35c0.12-0.2,0.25-0.39,0.38-0.59c0.14-0.21,0.3-0.4,0.42-0.62
c0.47-0.9,1-1.76,1.57-2.59c0.44-0.64,0.81-1.32,1.22-1.98c0.1-0.17,0.23-0.32,0.35-0.48c0.31-0.4,0.62-0.81,0.95-1.2
c0.35-0.41,0.7-0.82,1.07-1.22c0.42-0.46,0.88-0.87,1.25-1.36c0.27-0.35,0.53-0.71,0.84-1.03c0.57-0.59,1.11-1.19,1.72-1.73
c0.35-0.31,0.63-0.69,0.99-0.99c0.31-0.26,0.58-0.57,0.88-0.84c0.44-0.4,0.88-0.79,1.33-1.18c0.67-0.59,1.34-1.19,2.01-1.78
c0.23-0.2,0.46-0.4,0.68-0.6c0.42-0.38,0.85-0.74,1.37-0.98c0.13-0.06,0.22-0.18,0.34-0.26c0.59-0.4,1.18-0.79,1.77-1.19
c0.23-0.15,0.47-0.29,0.67-0.47c0.1-0.09,0.12-0.26,0.2-0.44c0.54-0.23,1.01-0.73,1.77-0.9c-0.32,0.29-0.56,0.54-0.82,0.75
c-0.72,0.59-1.41,1.23-2.09,1.87c-0.55,0.53-1.13,1.02-1.67,1.57c-0.72,0.73-1.41,1.5-2.11,2.24c-0.5,0.54-1.02,1.06-1.52,1.6
c-0.35,0.38-0.68,0.77-1.01,1.16c-0.76,0.89-1.53,1.78-2.27,2.69c-0.5,0.61-0.95,1.26-1.42,1.89c-0.17,0.23-0.37,0.44-0.54,0.67
c-0.23,0.3-0.44,0.61-0.67,0.92c-0.22,0.3-0.45,0.58-0.65,0.88c-0.36,0.58-0.71,1.17-1.05,1.76c-0.14,0.24-0.27,0.48-0.39,0.73
c-0.19,0.38-0.38,0.77-0.57,1.15c-0.12,0.23-0.24,0.46-0.37,0.69c-0.3,0.57-0.61,1.13-0.92,1.7c-0.13,0.23-0.28,0.45-0.38,0.69
c-0.23,0.51-0.45,1.04-0.67,1.55c-0.16,0.37-0.35,0.74-0.49,1.12c-0.16,0.44-0.28,0.91-0.44,1.35c-0.19,0.53-0.42,1.04-0.63,1.57
c-0.06,0.15-0.29,0.31-0.02,0.48c0.01,0.01-0.02,0.1-0.04,0.15c-0.33,0.95-0.66,1.89-0.98,2.84c-0.29,0.86-0.46,1.75-0.62,2.65
c-0.1,0.58-0.3,1.15-0.47,1.71c-0.12,0.42-0.49,3.92-0.5,3.98C42.5,92.18,42.48,92.48,42.41,92.75z M48.47,83.35
c-0.06-0.23-0.06-0.22,0.18-0.66C48.59,82.89,48.54,83.1,48.47,83.35z M51.59,73.28c-0.11,0.21-0.21,0.43-0.32,0.64
c-0.1,0.2-0.21,0.39-0.32,0.59c0.13-0.67,0.5-1.23,0.81-1.89C51.77,72.94,51.77,72.93,51.59,73.28z M61.6,57.9
c0.27-0.46,0.64-0.83,1.03-1.19C62.45,57.24,61.99,57.54,61.6,57.9z M66.67,52.56c-0.94,1.06-2.04,1.95-3.1,2.88
c-0.81,0.71-1.55,1.47-2.2,2.31c-0.36,0.47-0.68,0.97-1.02,1.45c-0.15,0.2-0.33,0.37-0.49,0.57c-0.05,0.06-0.08,0.15-0.11,0.23
c0.05,0.01,0.09,0.01,0.17,0.02c-0.03,0.07-0.04,0.13-0.08,0.17c-0.61,0.68-1.04,1.48-1.55,2.23c-0.46,0.67-0.97,1.31-1.44,1.96
c-0.13,0.18-0.25,0.37-0.36,0.57c-0.26,0.49-0.57,0.94-0.93,1.37c-0.29,0.35-0.53,0.75-0.79,1.13c-0.54,0.79-1.11,1.56-1.62,2.38
c-0.77,1.24-1.55,2.48-2.11,3.84c-0.17,0.41-0.33,0.8-0.37,1.24c-0.04,0.52-0.39,0.92-0.55,1.39c-0.23,0.66-0.47,1.31-0.7,1.96
c-0.08,0.24-0.17,0.47-0.24,0.71c-0.33,1.15-0.71,2.29-0.98,3.46c-0.24,1.02-0.4,2.07-0.56,3.11c-0.14,0.91-0.22,1.83-0.38,2.74
c-0.1,0.62-0.43,2.58-0.44,2.67c-0.03,0.46-0.2,2.1-0.22,2.47c-0.03,0.54-0.06,1.08-0.08,1.61c-0.01,0.27,0,0.55,0,0.83
c-0.41,0.14-2.74,0.19-3.81,0.08c0-0.25,0.01-0.49,0-0.74c-0.02-0.65,0.1-1.28,0.21-1.91c0.08-0.48,0.03-0.98,0.1-1.46
c0.09-0.62,0.23-1.24,0.34-1.86c0.09-0.52,0.16-1.04,0.26-1.56c0.06-0.34,0.16-0.66,0.24-1c0.11-0.5,0.21-1,0.31-1.51
c0.08-0.39,0.17-0.77,0.24-1.16c0.09-0.46,0.16-0.93,0.25-1.39c0.01-0.06,0.03-0.13,0.05-0.19c0.16-0.43,0.31-0.86,0.47-1.29
c0.14-0.38,0.27-0.77,0.41-1.15c0.3-0.8,0.58-1.61,0.9-2.4c0.13-0.34,0.33-0.65,0.52-1.04c-0.11,0.05-0.17,0.08-0.29,0.14
c0.05-0.14,0.06-0.25,0.11-0.34c0.43-0.89,0.86-1.78,1.31-2.66c0.21-0.41,0.46-0.8,0.67-1.2c0.24-0.45,0.45-0.92,0.71-1.36
c0.67-1.13,1.33-2.27,2.05-3.37c0.63-0.96,1.34-1.86,2.03-2.78c0.67-0.89,1.34-1.79,2.05-2.65c0.88-1.07,1.79-2.11,2.69-3.16
c0.2-0.23,0.44-0.41,0.66-0.62c0.05-0.04,0.13-0.08,0.14-0.13c0.08-0.41,0.44-0.57,0.69-0.83c0.47-0.48,0.97-0.93,1.41-1.43
c0.9-1.03,1.83-2.04,2.95-2.84c0.39-0.28,0.81-0.53,1.2-0.82c0.27-0.2,0.52-0.44,0.77-0.67c0.18-0.16,0.36-0.29,0.67-0.21
C66.76,52.45,66.72,52.51,66.67,52.56z M66.72,52.04c0.29-0.18,0.58-0.37,0.87-0.55c0.02,0.03,0.03,0.06,0.05,0.08
C67.38,51.82,67.19,52.15,66.72,52.04z M65.2,58.25c0.02-0.23,0.12-0.41,0.31-0.54C65.49,57.94,65.34,58.09,65.2,58.25z
M65.55,57.66c0.14-0.27,0.43-0.72,0.45-0.7C65.94,57.24,65.77,57.47,65.55,57.66z M66.19,56.55c0.08-0.11,0.16-0.23,0.24-0.34
C66.46,56.39,66.35,56.49,66.19,56.55z M70.23,51.47c-0.29,0.32-0.62,0.61-0.89,0.95c-0.56,0.73-1.1,1.48-1.64,2.22
c-0.32,0.44-0.64,0.87-0.96,1.31c0.47-1.13,3.09-4.52,3.67-4.74C70.35,51.29,70.3,51.39,70.23,51.47z M59.1,81.62
c-0.03-0.01-0.06-0.01-0.09-0.02c0.04-0.18,0.07-0.36,0.11-0.54c0.03,0.01,0.07,0.02,0.1,0.03C59.18,81.26,59.14,81.44,59.1,81.62
z M59.27,80.66c-0.02,0-0.04-0.01-0.06-0.01c0.01-0.07,0.02-0.14,0.03-0.21c0.03,0.01,0.05,0.01,0.08,0.02
C59.3,80.52,59.28,80.59,59.27,80.66z M59.33,80.19c-0.02,0-0.03,0-0.05-0.01c0.01-0.09,0.01-0.18,0.02-0.27
c0.03,0,0.05,0.01,0.08,0.01C59.37,80.02,59.35,80.11,59.33,80.19z M65.87,81.69c0.02-0.1,0.04-0.2,0.06-0.3
c0.02,0,0.05,0.01,0.07,0.01C66,81.5,65.91,81.69,65.87,81.69z M71.12,76.9c-0.03,0-0.07,0-0.1,0c0.01-0.12,0.01-0.25,0.02-0.37
c0.03,0,0.05,0,0.08,0C71.12,76.65,71.12,76.77,71.12,76.9z M73.15,62.99c-0.13-0.31,0.17-1.66,0.42-1.86
C73.43,61.75,73.29,62.37,73.15,62.99z M74.11,58.4c-0.14,0.74-0.29,1.47-0.44,2.21c-0.02,0.1-0.06,0.19-0.12,0.34
c-0.06-0.09,0.32-2.14,0.53-3.13c0.01-0.04,0.05-0.07,0.08-0.11C74.15,57.94,74.15,58.17,74.11,58.4z M74.17,57.7
c-0.02-0.39,0.14-0.73,0.28-1.08C74.37,56.98,74.47,57.39,74.17,57.7z M81.01,69.6c0.01,0.07,0.02,0.14,0.03,0.21
c0,0.01-0.01,0.03-0.02,0.03c-0.01,0-0.02-0.01-0.05-0.02c-0.01-0.06-0.03-0.13-0.05-0.2C80.95,69.61,80.98,69.61,81.01,69.6z
M80.97,69.06c-0.2-0.4-0.16-0.82-0.21-1.24c0.04-0.01,0.07-0.01,0.11-0.02C80.9,68.22,80.93,68.64,80.97,69.06z M80.77,67.26
c0.02,0.1,0.04,0.21,0.06,0.31c-0.03,0.01-0.06,0.01-0.08,0.02c-0.03-0.11-0.05-0.22-0.08-0.34
C80.7,67.25,80.74,67.26,80.77,67.26z M85.06,69.33c0.01,0,0.02,0,0.03,0c0.01,0.08,0.02,0.15,0.03,0.23c-0.02,0-0.03,0-0.05,0
C85.06,69.49,85.06,69.41,85.06,69.33z M79.18,49.68c0.03-0.14-0.09-0.44-0.06-0.57c0.32,0,0.62-0.05,1.09,0.17
c0.02,0.17,0.01,0.14,0.03,0.31C79.88,49.53,79.54,49.83,79.18,49.68z M80.84,50.72c0.25,0.02,0.25,0.02,0.31,0.35
C81.06,50.96,80.96,50.85,80.84,50.72z M88.63,57.84c-0.29-0.15-0.35-0.25-0.49-0.69C88.29,57.37,88.44,57.58,88.63,57.84z
M87.78,56.63c0.06,0.1,0.15,0.32,0.13,0.34C87.84,56.88,87.75,56.66,87.78,56.63z M84.87,52.11c-0.16-0.15-0.32-0.29-0.48-0.44
C84.7,51.65,84.9,52.09,84.87,52.11z"/>
<path d="M25.56,105.87c0.2,0.01,0.25,0.16,0.35,0.28c0.14,0.19,0.31,0.29,0.57,0.26c0.92-0.09,6.82-0.11,7.38-0.13
c0.21-0.01,2.94-0.11,2.94-0.08c-0.44,0.02,5.63,0.05,6.54,0.06c0.21,0,2.65,0.28,2.86,0.26c0,0,0,0,0,0
c-0.05-0.01-0.25-0.05-0.9-0.24c0.13-0.03,0.19-0.06,0.25-0.05c0.79,0.1,20.85-0.11,22.37-0.1c0.75,0,2.98,0.01,3.15,0.03
c0.16,0.02,9.08-0.31,9.95-0.21c0.55,0.06,1.1,0.11,1.65,0.16c0.65,0.06,1.31,0.1,1.96,0.15c0.02,0,0.05-0.01,0.09-0.02
c0.02-0.05,0.04-0.11,0.08-0.21c0.65,0,1.33-0.01,2,0c0.65,0.01,1.29,0.05,1.94,0.07c0.25,0.01,0.5,0.01,0.74-0.01
c0.25-0.02,0.5-0.05,0.76-0.04c0.82,0.04,1.65,0.04,2.48,0.05c0.13,0,0.26-0.01,0.4-0.02c0.43-0.01,0.87-0.07,1.3-0.01
c0.78,0.1,1.55,0.04,2.33,0c0.76-0.04,1.53-0.09,2.29-0.11c0.89-0.02,1.78,0,2.66-0.01c0.72,0,1.44-0.01,2.16-0.02
c0.08,0,0.16,0,0.23-0.03c0.4-0.17,0.4-0.18,0.67-0.16c0.06,0,0.12-0.01,0.18-0.02c-0.01-0.03-0.02-0.05-0.02-0.05
c-1.16-0.06-2.31-0.15-3.47-0.17c-1.44-0.03-2.87,0-4.31-0.01c-0.35,0-0.71-0.05-1.06-0.07c-0.26-0.01-0.53,0-0.79-0.01
c-0.72-0.04-1.45-0.1-2.17-0.12c-1.45-0.04-2.9-0.06-4.35-0.1c-0.41-0.01-0.82-0.04-1.22-0.05c-1.5-0.04-3-0.09-4.51-0.1
c-1.12,0-2.24,0.1-3.36,0.1c-1.49,0.01-2.98-0.03-4.47-0.03c-1.41,0-2.82,0.03-4.23,0.04c-0.47,0-0.94,0-1.42,0
c0.02-0.25,0.07-0.48,0.07-0.72c0.02-1.19,0.04-2.38,0.03-3.56c0-0.39-0.04-0.8-0.13-1.18c-0.06-0.25-0.24-0.47-0.18-0.8
c0.54-0.02,1.06-0.06,1.59-0.05c1.07,0.02,10.22-0.03,10.28-0.03c1.38,0.05,2.77-0.17,4.15,0.01c0.07,0.01,0.19,0,0.2-0.04
c0.08-0.2,0.25-0.11,0.38-0.12c0.14-0.01,0.29,0.03,0.43,0.02c0.51-0.04,1.02-0.09,1.52-0.14c0.03,0,0.06-0.04,0.11-0.07
c-0.09-0.18-0.19-0.26-0.38-0.22c-0.2,0.04-0.41,0.08-0.62,0.08c-2.67-0.02-5.34-0.04-8-0.06c-2.52-0.02-10.01,0.1-11,0.14
c-0.83,0.03-1.66,0-2.49-0.02c-0.88-0.03-9.45,0.04-9.77,0.03c-0.47-0.02-6.98,0.09-8.01,0.08c-0.87,0-6.47-0.14-7.47-0.1
c-0.72,0.03-2.75-0.1-3.47-0.28c-0.1-0.03-0.26-0.02-0.33,0.04c-0.17,0.16-0.36,0.15-0.55,0.14c-0.28-0.01-0.55,0-0.83-0.01
c-0.43-0.01-0.87,0-1.3-0.04c-0.58-0.06-0.59-0.07-0.55,0.54c0.5,0.05,0.57,0.12,0.69,0.63c0.02,0.1,0.06,0.21,0.06,0.31
c0.04,0.86,0.1,1.71,0.12,2.57c0.01,0.84-0.03,1.69-0.05,2.53c-0.01,0.22-0.03,0.44-0.05,0.65c-0.21,0-0.38,0-0.55,0
c-0.71,0-1.43,0-2.14-0.02c-0.63-0.02-2.97-0.1-3.51-0.1c-0.39,0-0.78-0.03-1.18-0.05c-0.08,0-0.17-0.06-0.22-0.03
c-0.24,0.13-0.49,0.16-0.76,0.15c-0.08,0-0.2,0.12-0.23,0.21C25.38,105.71,25.5,105.87,25.56,105.87z M68.77,98.95
c0.06,0.17,0.14,0.33,0.18,0.5c0.03,0.15,0.03,0.31,0.03,0.46c0,0.37,0,0.73,0,1.1c-0.01,0.97,0.11,1.94-0.06,2.91
c-0.01,0.07-0.03,0.17,0,0.22c0.18,0.32,0.08,0.67,0.11,1.05c-1.35,0.13-2.69,0.07-4.07,0.16c0-0.23-0.01-0.41,0-0.59
c0.05-0.85,0.1-1.71,0.15-2.56c0.03-0.5,0.07-1,0.09-1.49c0.01-0.13,0.02-0.28-0.04-0.39c-0.21-0.4-0.13-0.85-0.28-1.28
C66.21,98.9,67.5,99.01,68.77,98.95z M57.87,104.08c0.08-1.08,0.15-2.16,0.13-3.24c-0.01-0.53-0.06-1.05-0.09-1.62
c1.1-0.04,2.18-0.15,3.26-0.1c1.08,0.05,2.14-0.14,3.27-0.1c-0.03,0.51-0.05,0.95-0.07,1.4c-0.06,0.95-0.19,1.9,0.04,2.85
c0.01,0.04,0.01,0.08,0.01,0.12c0.02,0.63,0.04,1.26,0.06,1.92c-2.16,0.15-4.33-0.05-6.51,0.02
C57.8,104.91,57.84,104.48,57.87,104.08z M57.5,104.89c0.18,0.18,0.2,0.3,0.11,0.46C57.4,105.22,57.49,105.09,57.5,104.89z
M53.51,102.12c0-0.42,0.05-0.84,0.02-1.26c-0.04-0.46,0.06-0.91,0.02-1.37c-0.01-0.09,0.03-0.18,0.05-0.31
c0.25,0.03,0.92-0.12,1.06-0.12c0.75,0.02,1.5,0.04,2.24,0.06c0.34,1.04,0.22,2.1,0.13,3.16c-0.04,0.55-0.09,1.11-0.05,1.66
c0.03,0.41,0.04,0.85,0.33,1.2c0.02,0.03,0.04,0.07,0.05,0.11c0.01,0.02,0,0.05-0.01,0.07c-0.58-0.02-3.16,0.05-3.54,0.05
c-0.25,0-0.32-0.08-0.32-0.34C53.5,104.06,53.5,103.09,53.51,102.12z M47.58,99.17c1.74-0.17,3.4-0.03,5.01-0.01
c0.27,1.94,0.23,3.85,0.35,5.77c0.2,0.12,0.2,0.12,0.18,0.47c-0.14,0.01-0.28,0.02-0.42,0.03c-1.62,0.03-3.23,0.25-4.86,0.14
c-0.1-0.01-0.2-0.05-0.34-0.09C47.41,103.38,47.58,101.29,47.58,99.17z M47.13,105.08c0.13,0.19,0.14,0.31,0.03,0.45
C46.99,105.4,47.07,105.28,47.13,105.08z M42.65,104.74c-0.01-0.06-0.02-0.15,0.01-0.19c0.25-0.27,0.19-0.62,0.25-0.94
c0.03-0.14,0.03-0.29,0.03-0.43c0.01-0.19,0-0.39-0.06-0.59c-0.03,0.32-0.07,0.63-0.1,0.95c-0.03,0-0.05,0-0.08,0.01
c-0.01-0.05-0.04-0.1-0.04-0.15c0.03-0.62,0.06-1.24,0.1-1.86c0.04-0.52,0.1-1.05,0.14-1.57c0.02-0.3,0-0.6,0-0.94
c1.32,0.04,2.6,0.08,3.86,0.11c-0.03,0.58-0.08,1.13-0.09,1.68c-0.01,0.54,0.02,1.08,0.03,1.62c0,0.16-0.02,0.31-0.02,0.47
c0.01,0.84,0.03,1.69,0.05,2.56c-1.13,0.13-3.98,0.11-4.34-0.05c0.13-0.07,0.23-0.12,0.34-0.17
C42.71,105.08,42.68,104.91,42.65,104.74z M37.42,104.01c0.03-0.8,0.09-1.6,0.11-2.41c0.01-0.4,0.03-0.79,0.18-1.16
c0.04-0.1,0.07-0.19-0.04-0.28c-0.04-0.03-0.05-0.12-0.05-0.18c0-0.35,0-0.71,0-1.1c1.5,0.02,2.95,0.06,4.46,0.24
c0.06,0.81,0.14,1.6,0.16,2.39c0.02,0.58-0.04,1.16-0.05,1.73c-0.01,0.43,0,0.87,0.01,1.3c0,0.12,0.04,0.24,0.08,0.34
c0.08,0.19,0.09,0.38-0.01,0.57c-0.25,0.08-3.47,0.02-4.86-0.09C37.42,104.92,37.41,104.46,37.42,104.01z M34.42,105.29
c0.31-0.18,0.29-0.17,0.28-0.51c-0.02-0.68-0.01-1.37-0.02-2.05c-0.01-0.74-0.01-1.48-0.01-2.22c0-0.3,0.01-0.61,0.01-0.91
c0-0.2-0.02-0.4-0.03-0.64c0.67,0,1.37,0,2.07,0c-0.02,1.16,0.13,2.29,0.14,3.43c0.01,0.54-0.05,1.08-0.06,1.62
c-0.01,0.46,0,0.91,0,1.43c-0.84,0.02-1.66,0.05-2.57,0.07C34.33,105.39,34.36,105.32,34.42,105.29z"/>
<path d="M69.48,109.95c0.3,0,0.61-0.02,0.91-0.01c0.3,0.01,8.9-0.12,9.77-0.12c0.83,0,3.33-0.12,3.31-0.17c0.63,0,1.25,0,1.91,0
c-0.15-0.16-0.44-0.1-0.45-0.37c0-0.02-0.09-0.05-0.14-0.05c-0.32,0.02-0.61-0.08-0.88-0.24c-0.1-0.06-1.32-0.09-1.44-0.09
c-0.35-0.02-6.27,0.18-6.37,0.13c-0.39-0.17-15.17,0.27-15.24,0.27c-0.26,0.01-0.53,0.02-0.79,0.02c-1.89,0-5.86-0.02-5.96-0.03
c-0.03-0.2-0.11-0.52-0.13-0.52c-0.02,0.16-0.04,0.33-0.07,0.52c-0.27,0.26-0.57,0.17-1.07,0.19c-0.82,0.03-2.09-0.1-2.91-0.08
c-1,0.02-1.99-0.03-2.99,0.01c-1.59,0.06-3.19-0.01-4.79-0.02c-0.62-0.01-12.52-0.01-12.7-0.03c-0.58-0.08-1.16-0.07-1.73,0.04
c-0.11,0.02-0.26,0.09-0.31,0.19c-0.08,0.14,0,0.28,0.17,0.34c0.02,0.01,0.03,0.04,0.05,0.06c-0.12,0.27-0.12,0.27,0,0.57
c0.2,0,0.4,0,0.63,0c0.03,0.23,0.06,0.44,0.07,0.65c0.03,0.67,0.13,4.09,0.15,5.13c0.02,0.95,0.21,4.76,0.17,5.43
c-0.02,0.42-0.04,0.84-0.34,1.19c-0.07,0.08-0.07,0.21-0.11,0.35c0.61-0.05,0.52-0.6,0.78-0.88c-0.04,0.45-0.09,0.91-0.14,1.42
c0.05,0.03,0.13,0.05,0.18,0.1c0.11,0.11,0.48-0.15,0.5-0.27c0.02-0.17-0.13-2.33-0.13-2.62c0-0.38-0.06-0.77-0.02-1.15
c0.13-1.13,0.09-2.27,0.14-3.4c0.08-1.82,0.18-3.64,0.27-5.46c0.01-0.19,0.05-0.39,0.07-0.61c0.13-0.01,0.25-0.03,0.36-0.02
c0.51,0.02,6.93-0.05,7.76,0c0.34,0.02,1-0.02,1.37,0c0.03,0.44,0.26,1.08,0.05,1.5c-0.05,0.11-0.01,1.12,0.01,1.49
c0.02,0.46,0.09,3.16,0.06,3.83c-0.04,1.37-0.1,2.74-0.16,4.11c-0.03,0.7,0.35,2.18,0.53,2.24c0.06-0.14,0.15-0.26,0.17-0.4
c0.06-0.44,0.12-0.89,0.13-1.34c0.02-1.41,0.03-2.82,0.03-4.23c0-1.77-0.01-3.54-0.03-5.3c0-0.21-0.04-0.41-0.07-0.6
c-0.13-0.04-0.23-0.07-0.33-0.1c0-0.32,0-0.62,0-0.95c0.42,0,0.82,0,1.22,0c-0.37-0.17-0.79-0.03-1.14-0.26c0.68,0,1.35,0,2.05,0
c0.02,0.15,0.06,0.28,0.06,0.41c0.01,0.61,0.01,1.21,0.01,1.82c0,0.32,0.04,0.64-0.01,0.95c-0.1,0.64-0.09,1.28-0.08,1.93
c0.02,0.82-0.02,1.63-0.05,2.45c-0.03,0.75-0.07,1.5-0.11,2.25c-0.02,0.4-0.05,0.79-0.06,1.19c-0.01,0.43,0.01,0.87,0.02,1.3
c0.01,0.28,0.13,1.13,0.32,1.1c0.28-0.33,0.04-0.74,0.22-1.16c0.04,0.22,0.07,0.36,0.1,0.52c0.13-0.12,0.32-10.71,0.11-10.97
c-0.02-0.03-0.01-0.08-0.01-0.12c-0.11-0.59,0.13-1.14,0.2-1.73c0.28,0,6.86-0.03,7.09-0.03c0.76,0.01,1.53,0.03,2.29,0.04
c0.63,0.01,1.26,0,1.93,0c-0.02,0.53-0.04,1.04-0.06,1.58c-0.49-0.23-0.58-0.18-0.63,0.31c-0.04,0.43-0.14,0.86-0.09,1.29
c0.01,0.13,0.02,0.26,0.02,0.39c0,0.87-0.01,1.74-0.01,2.6c0,0.3,0,0.6,0,0.91c-0.01,1-0.03,2-0.02,3c0,0.45,0.05,0.89,0.06,1.34
c0.01,0.28,0.58,1.94,0.7,1.98c0.12-0.11,0.24-1.2,0.25-1.66c0.02-1.31,0.09-2.61,0.1-3.92c0.01-1.49-0.02-2.98-0.05-4.47
c-0.01-0.7-0.08-1.39-0.1-2.09c-0.01-0.42,0.01-0.83,0.02-1.31c0.59,0,7.36-0.22,7.92-0.24c0.85-0.03,1.71-0.11,2.56-0.11
c0.92,0,3.89,0.11,4.47,0.11c0,0.2,0,0.36,0,0.55c-0.08,0-0.15-0.01-0.21,0c-0.3,0.05-0.35,0.11-0.34,0.42
c0,0.03,0.01,0.05,0,0.08c-0.02,0.4,0.16,0.84-0.18,1.2c-0.03,0.03-0.02,0.1-0.01,0.15c0.01,0.1,0.07,6.5,0.08,6.79
c0.03,1.01,0.06,2.03,0.1,3.04c0.02,0.57,0.06,1.13,0.1,1.69c0.01,0.11,0.07,0.22,0.15,0.31c0.12,0.14,0.23,0.12,0.29-0.04
c0.05-0.12,0.07-0.25,0.12-0.41c-0.1,0.03-0.16,0.05-0.23,0.07c-0.09-0.37-0.08-0.36,0.09-0.67c0.1-0.18,0.23-2.42,0.19-2.44
c-0.2-0.15-0.14-1.02-0.14-1.25c0.02-1.65,0.24-7.9,0.16-9.37C69.25,110.02,69.32,109.95,69.48,109.95z M29.47,112.76
c-0.02,0-0.03,0-0.05,0.01c-0.01-0.06-0.03-0.12-0.03-0.19c0.01-0.48,0.04-0.97,0.04-1.45c0-0.1-0.04-0.2-0.06-0.3
c-0.03-0.11-0.07-0.21-0.14-0.38c0.19,0.02,0.31,0.04,0.45,0.06C29.61,111.28,29.54,112.02,29.47,112.76z M42.11,120.74
c-0.01,0-0.02,0-0.03,0c0-0.1,0-0.19,0-0.29c0.01,0,0.02,0,0.03,0C42.11,120.55,42.11,120.65,42.11,120.74z M42.27,111.08
c-0.16-0.22-0.15-0.45-0.16-0.71c0.12,0,0.2,0,0.33,0C42.38,110.62,42.32,110.85,42.27,111.08z"/>
<path d="M107.71,125.36c-0.01-0.23-0.02-0.43-0.04-0.64c-0.03-0.3-0.1-0.36-0.41-0.34c-1.22,0.08-2.45,0.16-3.67,0.24
c-0.83,0.05-4.02,0.15-4.78,0.17c-0.26,0.01-9.43,0.02-9.81,0.03c-0.79,0.03-1.58,0.11-2.37,0.11c-1.3,0-2.61-0.05-3.91-0.08
c-0.45-0.01-0.9,0-1.35-0.02c-1.62-0.05-11.08,0.02-11.54,0.03c-1.27,0.02-4.37,0.19-4.64,0.19c-0.76,0-1.52-0.02-2.29,0.01
c-1.11,0.05-2.21,0.05-3.32,0.09c-0.19,0.01-0.36,0.01-0.52-0.12c-0.07-0.06-0.66-0.06-0.84-0.05c-0.76,0.05-1.53,0.16-2.29,0.15
c-1.48-0.02-17.35-0.61-18.93-0.65c-1.27-0.03-8.68,0.01-9.73-0.02c-0.78-0.02-3.1-0.01-3.48,0.02c-0.31,0.03-0.62,0.1-0.94,0.13
c-0.25,0.02-0.42,0.11-0.53,0.4c0.28,0.12,2.77,0.3,3.75,0.29c0.96-0.02,1.92-0.04,2.88-0.03c0.8,0.01,1.61,0.05,2.41,0.08
c0.87,0.03,1.74,0.05,2.61,0.08c0.13,0,11.7,0.41,12.82,0.46c0.82,0.04,3.9,0.1,4.62,0.1c0.76,0,1.53,0.01,2.29,0.02
c0.24,0,0.72-0.06,0.72-0.09c-0.28-0.02-0.55-0.03-0.83-0.05c0-0.03,0-0.06,0-0.08c0.46-0.06,0.92-0.02,1.38,0.04
c0.02,0,3.43-0.02,7.29-0.04c0-0.02,0-0.03,0-0.05c0.07,0.01,0.15,0.02,0.22,0.03c-0.05,0.01-0.1,0.01-0.15,0.02
c4.88-0.03,10.43-0.07,10.73-0.07c1.29,0,2.58,0.01,3.88,0.02c0.63,0.01,5.69-0.03,6.23,0c0.79,0.03,2.65,0.01,2.79,0.03
c0.39,0.03,2.88-0.02,3.73-0.04c0.66-0.02,8.65-0.05,10.52-0.08c0.46-0.01,0.92,0,1.39,0.01c0.49,0.01,0.97,0.06,1.46,0.06
c0.8-0.01,1.6-0.05,2.4-0.06c0.74-0.02,1.48-0.01,2.21-0.03c0.28-0.01,0.55-0.08,0.83-0.12c0-0.03-0.01-0.05-0.01-0.08
C108.24,125.4,107.97,125.38,107.71,125.36z M49.63,125.76c-0.01,0-0.03-0.03-0.1-0.09c0.11,0.02,0.16,0.03,0.21,0.04
C49.7,125.73,49.67,125.75,49.63,125.76z"/>
<path d="M69.9,31.26c0.2-0.01,0.43-0.09,0.58,0c0.22,0.12,0.4,0.05,0.6,0.02c0.16-0.02,0.31-0.04,0.47-0.04
c0.69-0.02,1.37-0.03,2.06-0.04c1.24-0.02,2.48,0.09,3.72-0.03c0.59-0.06,1.18,0.03,1.78,0.04c0.84,0.02,1.69,0.05,2.53,0.03
c0.47-0.01,0.97,0.02,1.38-0.4c-0.11-0.08-0.21-0.16-0.25-0.19c-0.05-0.16-0.06-0.29-0.13-0.36c-0.16-0.17-0.34-0.34-0.53-0.47
c-0.15-0.1-0.22-0.23-0.24-0.39c-0.03-0.22-0.05-0.44-0.06-0.67c-0.02-0.28-0.1-0.42-0.25-0.44c-0.21-0.02-0.35,0.12-0.39,0.41
c-0.02,0.19,0,0.39,0,0.63c-0.19-0.02-0.39-0.06-0.59-0.04c-0.22,0.02-0.39,0.01-0.45-0.25c-0.14,0.02-0.26,0.03-0.37,0.05
c-0.16-0.37,0.18-0.68,0.07-1.05c-0.26-0.12-0.46-0.02-0.65,0.2c0.04,0.11,0.09,0.21,0.11,0.29c-0.21,0.21-0.4,0.4-0.59,0.58
c-0.2-0.03-0.41-0.05-0.59-0.07c-0.03-0.1-0.04-0.2-0.09-0.27c-0.19-0.28-0.32-0.57-0.3-0.92c0.01-0.21-0.13-0.3-0.34-0.27
c-0.11,0.01-0.22,0.04-0.36,0.06c-0.02-0.14-0.03-0.24-0.04-0.35c-0.06-0.53,0.06-1.09-0.24-1.58c-0.02-0.03-0.01-0.08-0.01-0.12
c0.01-0.41,0.03-0.82,0.03-1.22c0-0.49,0.19-0.96,0.08-1.45c-0.02-0.08,0.04-0.18,0.06-0.25c0.59,0.22,0.65,0.85,1.07,1.2
c-0.05-0.25-0.14-0.47-0.21-0.71c-0.14-0.45-0.21-0.77-0.26-1.23c-0.14-0.82-0.35-1.61-0.37-2.36c-0.01-0.22,0.27-0.35,0.46-0.43
c0.22-0.09,0.86-0.68,0.83-1.58c-0.03-0.84-0.41-1.39-0.91-1.59c-0.87-0.54-1.96,0-2.22,0.3c-0.26,0.23-0.58,0.7-0.54,1.38
c0.13,0.57,0.3,1.03,0.72,1.34c0.09,0.03,0.28,0.2,0.37,0.23c0.19,0.06,0.24,0.21,0.22,0.37c-0.04,0.3-0.1,0.6-0.16,0.89
c-0.06,0.31-0.23,1.36-0.31,1.67c-0.56,2.6-1.76,4.36-2.31,5.2c-0.06,0.1-0.15,0.19-0.25,0.24c-0.15,0.08-0.18,0.19-0.23,0.34
c-0.11,0.39-0.32,0.7-0.75,0.81c-0.02,0.01-0.04,0.03-0.06,0.05c-0.03,0.11-0.06,0.22-0.11,0.39c-0.08-0.1-0.15-0.15-0.15-0.19
c0.01-0.12,0.08-0.23,0.09-0.34c0.03-0.32,0.07-0.65,0.06-0.98c-0.01-0.23-0.28-0.34-0.47-0.24c-0.27,0.14-0.48,0.7-0.4,0.99
c0.06,0.2,0.09,0.4,0.15,0.64c-0.25,0.02-0.44,0.03-0.63,0.05c-0.23,0.03-0.27,0.11-0.22,0.41c0.25-0.11,0.37,0.04,0.48,0.22
c-0.02,0.04-0.03,0.07-0.05,0.08c-0.1,0.08-0.18,0.17-0.1,0.3c0.07,0.12,0.2,0.13,0.32,0.08c0.19-0.08,0.41-0.14,0.57-0.26
c0.23-0.18,0.44-0.2,0.69-0.1c0.09,0.03,0.23,0.06,0.29,0.01c0.3-0.25,0.66-0.24,1.01-0.22c0.46,0.02,0.92,0.07,1.37,0.08
c1.25,0.01,2.5,0.01,3.76-0.01c0.24,0,0.43,0.14,0.64,0.15c0.23,0,0.46-0.12,0.69-0.12c0.44,0.01,0.88,0.07,1.32,0.11
c0.02-0.02,0.21,0.05,0.22,0.05c0.38,0.29,0.84,0.1,0.98,0.75c-0.38,0.03-0.69,0.07-1,0.08c-0.11,0-0.26,0-0.34-0.07
c-0.24-0.2-0.55-0.02-0.81,0c-0.09,0.01-0.18,0.03-0.27,0.03c-1.35,0-2.71,0.01-4.07,0.01c-0.66,0-1.32-0.03-1.98-0.05
c-0.8-0.02-1.61-0.05-2.41-0.08c-0.41-0.01-0.82,0.02-1.22-0.06c-0.32-0.07-0.54,0.05-0.76,0.19c-0.16,0.11-0.16,0.19-0.11,0.39
C69.57,31.31,69.74,31.27,69.9,31.26z M75.86,18.67c-0.32-0.24-0.58-0.76-0.57-1.13c0.03-0.67,0.41-1.1,0.97-1.22
c0.6-0.12,1.24,0.22,1.47,0.79c0.3,0.99-0.23,1.68-0.69,1.71C76.75,18.84,76.18,18.91,75.86,18.67z M76.2,22.28
c0.09-0.37,0.15-0.75,0.22-1.12c0.12,0.2,0.23,1.19,0.19,1.69c-0.29,0.04-0.29,0.04-0.72,0.58C76.01,23,76.12,22.64,76.2,22.28z
M74.61,27.82c-0.06,0.42-0.32,0.83-0.04,1.28c-0.31,0.02-0.54,0.04-0.78,0.05c-0.24,0.01-0.29-0.07-0.23-0.32
c0.02-0.1-0.11-0.3-0.12-0.4c-0.03-0.39,0.18-0.4,0.47-0.61c0.17-0.12,0.46-0.68,0.58-0.86c0.3-0.44,0.58-1.06,0.74-1.56
c0.21-0.66,0.32-0.97,0.55-1.67c0.11,0.27,0.26,0.56,0.25,0.79c-0.01,0.24,0,0.72,0,0.96c0,0.85-0.27,1.46-0.68,2.22
c-0.06-0.01-0.32-0.12-0.38-0.13C74.77,27.52,74.64,27.63,74.61,27.82z M76.22,29.15c-0.33-0.02-0.67,0.07-1.02-0.06
c0.03-0.07,0.05-0.13,0.08-0.15c0.48-0.24,0.61-0.71,0.73-1.17c0.15-0.55,0.26-1.11,0.38-1.67
C76.32,27.11,76.55,28.14,76.22,29.15z M76.55,24.07c-0.17-0.32,0.05-0.61,0.05-0.91C76.65,23.47,76.71,23.78,76.55,24.07z
M77.85,29.11c-0.81,0.04-0.53-0.38-0.51-0.65c0.14,0.05,0.51,0.51,0.65,0.56C77.92,29.07,77.89,29.11,77.85,29.11z"/>
<path d="M53.59,108c0.02,0,0.05,0.02,0.07,0.03c0.22,0.03,0.44,0.09,0.66,0.09c0.38-0.01,11.62-0.25,12.26-0.3
c0.26-0.02,0.53,0,0.79-0.02c0.7-0.05,1.38-0.12,2.09-0.06c0.98,0.09,1.97,0.11,2.96,0.15c0.44,0.02,0.88,0,1.32-0.09
c-0.04-0.36-0.19-0.55-0.54-0.52c-0.14,0.01-0.29-0.02-0.43-0.05c-0.1-0.02-0.22-0.05-0.27-0.12c-0.14-0.18-0.31-0.17-0.5-0.15
c-0.64,0.07-3.8,0.31-4.73,0.3c-1.56-0.02-3.11-0.11-4.67-0.12c-1.29-0.01-2.59,0.07-3.88,0.11c-1.05,0.04-12.43,0-13.64,0.01
c-0.44,0-2.73,0.09-3.44,0.07c-1.07-0.03-12.25-0.1-14.17-0.15c-0.07,0-0.15-0.02-0.19,0.02c-0.22,0.2-0.49,0.18-0.75,0.18
c-0.28,0-0.47,0.09-0.62,0.33c-0.06,0.1-0.19,0.19-0.1,0.3c0.06,0.07,0.2,0.11,0.3,0.12c0.79,0.03,1.58,0.06,2.37,0.06
c1.15,0,2.3-0.02,3.44-0.04c0.92-0.02,9-0.21,10.03-0.02c0.04,0.01,2.15-0.01,3.16,0.01c1.95,0.04,3.9,0.09,5.86,0.14
c0.51,0.01,1.03,0.04,1.54,0.05c0.6,0.01,1.19,0,1.79-0.01c-0.15-0.04-0.3-0.09-0.46-0.11c-0.41-0.03-0.81-0.04-1.22-0.07
c-0.06,0-0.13-0.04-0.19-0.06c0-0.03,0.01-0.06,0.01-0.08C52.81,108,53.2,108,53.59,108z M72.09,107.5c0.22,0,0.46-0.06,0.66,0.14
c-0.23-0.01-0.45-0.03-0.68-0.04C72.08,107.57,72.09,107.53,72.09,107.5z"/>
<path d="M32.43,97.78c0.07,0.02,0.15,0.04,0.23,0.04c0.63,0.02,8.8-0.05,9.92-0.07c0.51-0.01,2.91-0.02,3.59-0.02
c0.29,0,0.58-0.05,0.87-0.05c1.07,0.01,2.13,0.03,3.2,0.04c2.73,0.01,5.47,0.03,8.2,0.01c2.74-0.02,15.51-0.2,16.56-0.24
c0.37-0.01,0.74,0,1.11,0c1.25-0.01,14,0.08,16.9-0.07c0.92-0.05,3.78-0.04,4.02-0.04c-0.03-0.31-0.19-0.42-0.4-0.44
c-0.31-0.03-0.63-0.03-0.95-0.03c-0.54-0.01-1.08-0.01-1.61-0.01c-0.16,0-0.32-0.01-0.47,0c-0.54,0.03-3.02,0.09-3.57,0.11
c-1.13,0.04-2.27,0.08-3.4,0.08c-9.18,0.01-31.34,0.12-31.84,0.13c-0.71,0.01-1.42,0.02-2.13,0.02c-0.54,0-12.79-0.05-14.62-0.06
c-0.2,0-3.64-0.13-3.83-0.17c-0.61-0.13-1.18,0.08-1.75,0.2c-0.15,0.03-0.25,0.15-0.25,0.32C32.17,97.69,32.3,97.74,32.43,97.78z"
/>
<path d="M80.49,83.44c0.03-0.15,0.05-0.3,0.08-0.45c-1.44-1.09-2.89-2.18-4.33-3.27c-0.25,0.17-0.27,0.24-0.12,0.45
c0.06,0.08,0.15,0.14,0.22,0.22c0.1,0.11,0.2,0.22,0.29,0.33c-0.01,0.03-0.03,0.05-0.04,0.08c-0.2-0.18-0.4-0.28-0.64-0.1
c-0.57,0.43-1.15,0.83-1.7,1.29c-0.37,0.31-0.68,0.68-1.02,1.03c-0.21,0.21-0.44,0.42-0.65,0.63c-0.09,0.09-0.2,0.18-0.26,0.3
c-0.04,0.08-0.03,0.2,0.01,0.28c0.03,0.06,0.15,0.1,0.22,0.1c0.13,0,0.25-0.04,0.4-0.08c0.02,0.21,0.03,0.38,0.05,0.57
c0.15-0.02,0.26-0.03,0.33-0.04c0.19,0.21-0.03,0.46,0.17,0.71c0.03-0.15,0.06-0.24,0.06-0.32c0-0.39,0.12-0.75,0.24-1.12
c0.14-0.46,0.45-0.8,0.8-1.11c0.06-0.05,0.14-0.1,0.21-0.1c0.29-0.03,0.58-0.05,0.87-0.07c0.06,0,0.12,0.04,0.27,0.1
c-0.44,0.12-0.7,0.4-1.08,0.45c-0.19,0.03-0.36,0.15-0.53,0.25c-0.06,0.04-0.1,0.15-0.1,0.23c0,0.05,0.1,0.1,0.16,0.14
c0.04,0.02,0.1,0.01,0.16,0c0.6-0.07,1.2-0.16,1.81-0.2c0.35-0.03,0.71,0.03,1.1,0.05c-0.05,0.07-0.06,0.11-0.07,0.12
c-0.4,0.11-2.07,0.36-2.35,0.29c-0.11,0.27-0.05,0.51-0.05,0.76c0,0.42-0.03,1.42,0.05,1.54c0.09,0.13,0.11,0.31,0.17,0.5
c0.27-0.16,1.26-0.3,1.51-0.24c0.13,0.03,0.29,0.02,0.42-0.03c0.25-0.09,1.36-0.09,1.41,0.06c0.37-0.14,0.51-0.42,0.37-0.73
c-0.18-0.4-0.16-1.82-0.2-1.98c-0.13,0.01-0.22,0.02-0.31,0.03c-0.12,0-0.28-0.05-0.37,0.01c-0.09,0.06-0.12,0.22-0.17,0.34
c-0.04,0.08-0.05,0.18-0.1,0.26c-0.03,0.05-0.09,0.07-0.2,0.15c0.08-0.34-0.08-0.43-0.31-0.48c-0.06-0.01-0.1-0.08-0.18-0.15
c0.21-0.06,0.37-0.11,0.54-0.15c0.23-0.05,0.24-0.23,0.21-0.39c0.37-0.03,0.71-0.05,1.04-0.07c0.32,0.23,0.27,0.63,0.45,0.9
c0.4-0.01,0.48-0.1,0.35-0.43c-0.11-0.27-0.25-0.52-0.02-0.85C79.88,83.49,80.19,83.5,80.49,83.44z M75.58,85.07
c-0.02-0.1-0.04-0.21-0.07-0.32C75.8,84.76,75.8,84.77,75.58,85.07z M76.65,84.47c0.15,0.04,0.27,0.06,0.38,0.11
c0.03,0.02,0.07,0.13,0.04,0.16c-0.11,0.18-0.27,0.06-0.42,0.08C76.65,84.7,76.65,84.61,76.65,84.47z M76.72,85.22
c0.08-0.01,0.17-0.01,0.29-0.02c0,0.16,0,0.31,0,0.45C76.73,85.59,76.73,85.59,76.72,85.22z M77.66,85.04
c0.04,0.26,0.06,0.46,0.09,0.67C77.48,85.62,77.45,85.42,77.66,85.04z M77.31,86.23c-0.15,0.05-0.24,0.08-0.33,0.1
c-0.02-0.05-0.05-0.08-0.04-0.09c0.04-0.07,0.09-0.14,0.13-0.2C77.14,86.09,77.2,86.14,77.31,86.23z M78.06,82.3
c-0.22-0.02-0.39-0.03-0.56-0.04C77.72,81.99,77.77,82,78.06,82.3z M76.05,82.08c0.23-0.29,0.4-0.34,0.74-0.24
c0.12,0.04,0.26,0.05,0.38,0.07c0,0.03,0,0.07-0.01,0.1C76.8,82.03,76.43,82.06,76.05,82.08z M78.21,83.01
c-0.18,0.01-0.36,0.03-0.55,0.03c-0.18,0-0.36-0.03-0.54-0.05c0.52-0.1,1.02-0.17,1.6-0.15C78.57,83.1,78.38,83,78.21,83.01z"/>
<path d="M56.07,124.61c0.04-0.27,0.08-0.52,0.13-0.77c0.08,0,0.39-0.44,0.46-0.66c0.07-0.21,0.05-1.55,0.07-1.77
c0.11-1.13,0.32-1.13,0.39-2.26c0.06-0.83,0.09-1.66,0.13-2.49c0.01-0.12,0-0.25,0-0.37c-0.03,0-0.06,0-0.1-0.01
c-0.1,0.67-0.2,1.35-0.3,2.02c-0.01,0-0.03,0-0.04,0c-0.01-0.66-0.02-1.31-0.04-1.97c-0.03-0.76-0.07-1.53-0.11-2.29
c-0.02-0.42-0.12-0.85-0.06-1.26c0.05-0.32-0.1-0.59-0.05-0.89c0.01-0.03-0.03-0.09-0.05-0.1c-0.21-0.09-0.17-0.28-0.17-0.44
c0-0.11,0.01-0.21-0.01-0.32c-0.03-0.14-0.23-0.22-0.31-0.11c-0.1,0.14-0.23,0.32-0.22,0.48c0.01,0.87,0.07,1.73,0.1,2.6
c0.02,0.58,0.06,1.16,0.01,1.73c-0.06,0.71-0.08,1.41-0.06,2.12c0.04,1.2,0.03,2.39,0.04,3.59c0.01,0.92,0.03,1.84,0.05,2.76
C55.94,124.35,55.91,124.5,56.07,124.61z M56.51,118.93c0.02,0,0.03,0,0.05,0c0,0.35,0,0.69,0,1.04c-0.02,0-0.03,0-0.05,0
C56.51,119.63,56.51,119.28,56.51,118.93z"/>
<path d="M45.67,116.82c-0.27,0.53-0.03,1.06-0.27,1.52c-0.06-0.87-0.09-1.74-0.1-2.6c-0.01-0.87,0.09-1.74-0.02-2.6
c-0.02-0.18,0.05-0.39-0.02-0.54c-0.14-0.32-0.1-0.64-0.1-0.97c0-0.14,0-0.29,0-0.43c0-0.13,0-0.26,0-0.39c-0.03,0-0.06,0-0.08,0
c-0.03,0.15-0.06,0.3-0.09,0.44c-0.22,0.12-0.49,0.21-0.49,0.5c0.01,0.58-0.09,1.15-0.07,1.73c0.05,1.37,0.03,2.74,0.04,4.1
c0.01,0.88,0.04,1.75,0.01,2.63c-0.04,1.08,0.01,2.16,0.12,3.24c0.02,0.17,0.05,0.34,0.22,0.38c0.17,0.03,0.2,0.13,0.23,0.27
c0.02,0.06,0.08,0.1,0.16,0.19c0.05-0.13,0.06-0.24,0.12-0.29c0.29-0.22,0.25-0.54,0.26-0.83c0.01-0.55-0.04-1.1-0.03-1.65
c0.02-1.13,0.08-2.27,0.11-3.4C45.68,117.7,45.67,117.3,45.67,116.82z"/>
<path d="M51.8,115.49c0.05-1.46-0.06-2.91-0.09-4.37c0-0.09-0.08-0.22-0.15-0.24c-0.06-0.02-0.16,0.08-0.25,0.12
c-0.09,0.04-0.17,0.08-0.25,0.11c0.26,1.72,0.23,3.43-0.09,5.14c-0.05-1.69-0.09-3.37-0.14-5.06c-0.09,0.18-0.15,0.37-0.15,0.56
c0,0.71,0.01,1.42,0.02,2.13c0.02,0.79,0.04,1.58,0.07,2.37c0.04,0.86,0.12,1.71,0.16,2.57c0.03,0.84,0.03,1.69,0.03,2.53
c0,0.44,0.06,2.54,0.06,2.79c0,0.15,0.09,0.21,0.21,0.25c0.15,0.05,0.58-1.14,0.57-1.37c-0.02-0.91-0.04-1.81-0.03-2.72
c0-0.59,0.06-1.19,0.06-1.78C51.83,117.52,51.76,116.51,51.8,115.49z"/>
<path d="M64.66,115.81c-0.03-0.37-0.03-0.73-0.03-1.1c0-0.74,0.02-1.48,0.02-2.22c0-0.44-0.21-0.59-0.66-0.47
c0.03-0.48,0.05-0.95,0.08-1.49c-0.19,0.21-0.25,0.39-0.26,0.59c-0.01,0.54-0.02,1.08-0.04,1.62c-0.01,0.3-0.02,0.6-0.02,0.91
c-0.01,0.59-0.01,1.18-0.01,1.77c0,0.14-0.02,0.29-0.02,0.43c0.01,0.8,0.03,1.61,0.04,2.41c0.01,0.75,0.03,1.5,0.04,2.25
c0.01,0.76,0,1.53-0.01,2.29c0,0.43,0,0.87,0,1.33c0.15,0.05,0.27,0.09,0.39,0.13c0.06,0.01,0.13,0.02,0.18,0
c0.17-0.08,0.3-0.64,0.16-0.74c-0.15-0.1-0.14-0.21-0.13-0.33c0.03-0.46,0.06-0.92,0.11-1.38
C64.77,119.82,64.81,117.82,64.66,115.81z"/>
<path d="M33.57,122.74c-0.01-0.22-0.01-0.45-0.05-0.67c-0.13-0.67,0.04-1.33,0.01-2c-0.06-1.14-0.03-2.29-0.02-3.44
c0.01-1.38,0.03-2.77,0.04-4.15c0-0.27-0.02-0.53-0.04-0.82c-0.08,0.05-0.11,0.06-0.17,0.1c-0.03-0.22-0.06-0.41-0.08-0.61
c-0.01-0.13-0.09-0.21-0.2-0.19c-0.09,0.01-0.18,0.09-0.23,0.16c-0.05,0.08-0.03,0.24-0.1,0.28c-0.23,0.15-0.23,0.37-0.23,0.59
c0.02,0.63,0.06,1.26,0.06,1.89c0,1.19-0.02,2.37-0.03,3.56c0,0.6-0.03,1.21,0.11,1.8c0.06,0.25,0.11,0.51,0.13,0.77
c0.04,0.62,0.06,1.24,0.09,1.85c0.02,0.29,0.02,0.58,0.07,0.87c0.05,0.25,0.15,0.5,0.24,0.74c0.02,0.06,0.12,0.09,0.24,0.17
c0.01-0.2-0.03-0.4,0.03-0.44C33.68,123.09,33.58,122.91,33.57,122.74z"/>
<path d="M81.35,34.96c0.21-0.23,0.72-0.07,0.72-0.58c0-0.02,0.04-0.05,0.06-0.05c0.25,0,0.19-0.22,0.22-0.35
c0.05-0.18-0.04-0.28-0.23-0.27c-0.24,0.01-0.47,0.03-0.71,0.07c-1.14,0.2-8.62,0.01-9.41,0.03c-0.31,0.01-0.77-0.1-1.28,0.04
c-0.12,0.19-0.06,0.34,0.07,0.41c0.15,0.09,0.33,0.18,0.5,0.19c0.93,0.06,1.86-0.04,2.79,0.08c0.74,0.1,1.5,0.08,2.25,0.09
c1.56,0.01,3.11,0,4.67,0.01c0.12,0,0.25,0,0.41,0c-0.06,0.13-0.09,0.19-0.12,0.26C81.27,34.96,81.3,35.05,81.35,34.96z"/>
<path d="M66.68,117.21c0.03-0.89,0.08-1.78,0.07-2.67c-0.01-0.5,0-1,0-1.5c0.01-0.42,0.03-0.84,0.04-1.26c0.01-0.45,0-0.9,0-1.35
c-0.03,0-0.06,0-0.09-0.01c-0.03,0.16-0.05,0.32-0.09,0.52c-0.11,0-0.24,0.02-0.36,0c-0.24-0.06-0.35-0.02-0.38,0.22
c-0.02,0.16,0.01,0.34,0.02,0.51c0.02,0.22,0.11,0.43-0.05,0.64c-0.04,0.06-0.04,0.18-0.01,0.26c0.19,0.56,0.16,1.14,0.16,1.72
c0,0.77-0.04,1.55-0.02,2.32c0.02,1.05,0.11,2.1,0.12,3.16c0.01,1-0.05,2-0.04,2.99c0,0.23,0.13,0.47,0.25,0.68
c0.1,0.17,0.31,0.13,0.36-0.05c0.06-0.2,0.12-0.42,0.09-0.62c-0.09-0.46-0.12-0.92-0.15-1.39c-0.02-0.41,0.01-0.82,0.01-1.23
C66.64,119.19,66.64,118.2,66.68,117.21z"/>
<path d="M31.42,115.57c0.01-0.87-0.04-1.73-0.08-2.6c-0.02-0.42-0.08-0.84-0.13-1.25c-0.02-0.14-0.03-0.3-0.23-0.32
c-0.18-0.01-0.36,0.12-0.41,0.31c-0.03,0.11-0.03,0.24-0.02,0.35c0.03,1.13,0.06,2.27,0.08,3.4c0.02,1.15,0.04,2.29,0.06,3.44
c0.03,1.38,0.05,2.77,0.09,4.15c0.01,0.31,0.08,0.61,0.13,0.93c0.35-0.07,0.3-0.31,0.32-0.47c0.14-0.13,0.19-0.6,0.16-0.71
c-0.05-0.19-0.1-0.39-0.1-0.58c-0.01-1.1-0.01-2.19,0.01-3.29C31.33,117.81,31.41,116.69,31.42,115.57z"/>
<path d="M74.57,68.98c-0.17,0.34-0.36,0.67-0.51,1.01c-0.14,0.32-0.07,0.65,0.13,0.94c0.05,0.08,0.18,0.17,0.23,0.16
c0.09-0.03,0.19-0.14,0.22-0.23c0.05-0.17,0.06-0.36,0.07-0.55c0.01-0.3,0.07-0.6,0.19-0.87c0.29,0,0.51-0.08,0.65-0.33
c0.06-0.11,0.18-0.19,0.27-0.29c0.12-0.15,0.25-0.23,0.47-0.18c0.26,0.05,0.53,0.03,0.85,0.05c0.03,0.06,0.09,0.17,0.16,0.31
c-0.59,0.05-1.12,0.15-1.61,0.41c0.06,0.28,0.05,0.3,0.18,0.32c0.59,0.11,1.19,0.4,1.81,0.01c0.05,0.18,0.09,0.34,0.14,0.5
c0.28-0.16-0.06-0.58,0.36-0.66c0.12,0.24,0.25,0.48,0.36,0.73c0.08,0.18-0.01,0.4-0.19,0.52c-0.16,0.1-0.3,0.06-0.38-0.11
c-0.04-0.08-0.05-0.18-0.09-0.26c-0.07-0.14-0.2-0.18-0.33-0.09c-0.13,0.09-0.14,0.22-0.07,0.35c0.16,0.27,0.65,0.49,0.88,0.37
c0.31-0.16,0.62-0.48,0.48-0.9c-0.14-0.43-0.35-0.85-0.56-1.25c-0.3-0.55-0.81-0.85-1.37-1.09c0.12-0.32-0.24-0.28-0.33-0.49
c0.06-0.17,0.04-0.32-0.17-0.43c-0.39-0.21-0.76-0.08-0.85,0.36c-0.05,0.23-0.01,0.49-0.01,0.81
C75.2,68.32,74.79,68.54,74.57,68.98z M77.63,69.59c-0.16-0.07-0.28-0.13-0.44-0.2c0.08-0.1,0.14-0.18,0.21-0.27
C77.61,69.21,77.62,69.38,77.63,69.59z"/>
<path d="M74.01,32.7c0.82-0.01,1.63-0.04,2.45-0.03c0.69,0,1.37,0.04,2.06,0.07c0.38,0.02,0.76,0.08,1.14,0.11
c0.33,0.03,0.66,0.05,0.99,0.05c0.17,0,0.34-0.09,0.5-0.13c0-0.1,0.01-0.12,0-0.13c-0.03-0.04-0.07-0.08-0.1-0.12
c-0.18-0.3-0.43-0.4-0.79-0.37c-0.56,0.05-1.13,0.04-1.7,0.04c-0.46,0-0.92-0.04-1.38-0.04c-0.72,0-1.45,0.04-2.17,0.04
c-1.19-0.01-2.37-0.05-3.56-0.08c-0.22-0.01-0.42,0.01-0.63,0.16c0.03,0.14-0.13,0.25,0.11,0.48c0.46,0.01,0.9,0.02,1.35,0.01
C72.86,32.73,73.43,32.71,74.01,32.7z"/>
<path d="M63.95,83.37c0.1-0.14,0.08-0.25,0.04-0.4c-0.13-0.44-0.51-0.66-0.8-0.95c-0.18-0.18-0.41-0.21-0.66-0.16
c-0.51,0.11-0.98,0.28-1.4,0.61c-0.28,0.22-0.58,0.41-0.86,0.63c-0.15,0.12-0.3,0.27-0.38,0.44c-0.06,0.14-0.04,0.33-0.02,0.49
c0.01,0.11,0.07,0.21,0.13,0.31c0.04,0.06,0.12,0.15,0.16,0.14c0.09-0.03,0.18-0.09,0.23-0.17c0.05-0.07,0.02-0.2,0.07-0.25
c0.3-0.29,0.62-0.57,0.94-0.83c0.11-0.09,0.26-0.13,0.42,0.01c-0.02,0.11-0.03,0.24-0.05,0.36c0.25,0.16,0.47,0.27,0.78,0.21
c0.28-0.05,0.57-0.01,0.86,0c0.14,0,0.28,0.02,0.4,0.04C63.87,83.64,63.87,83.48,63.95,83.37z M61.99,82.88
c0.26-0.05,0.45-0.08,0.64-0.11C62.54,83.15,62.42,83.18,61.99,82.88z M62.77,82.81c0.39-0.05,0.48,0.05,0.59,0.57
C63.02,83.29,63.13,82.88,62.77,82.81z"/>
<path d="M63.48,84.96c-0.24-0.11-0.45-0.06-0.65-0.03c-0.49,0.06-0.98,0.2-1.48,0.05c-0.06-0.02-0.15,0.03-0.23,0.05
c-0.02,0.12-0.04,0.23-0.06,0.32c-0.17,0.09-0.39-0.01-0.46,0.26c-0.04,0.17-0.27,0.3-0.12,0.5c0.75,0,1.49,0,2.23,0
c-0.39,0.21-0.8,0.35-1.24,0.23c-0.07-0.02-0.16-0.01-0.24-0.01c-0.1,0-0.23-0.03-0.3,0.02c-0.08,0.06-0.1,0.19-0.16,0.29
c-0.17,0.04-0.35,0.07-0.52,0.1c-0.06,0.21-0.04,0.34,0.2,0.35c0.26,0.01,0.53,0.01,0.79,0.04c0.55,0.07,1.06,0.04,1.51-0.37
c0.15-0.14,0.3-0.23,0.25-0.45c0.07-0.03,0.12-0.05,0.17-0.06c0.12-0.04,0.25-0.1,0.19-0.24c-0.03-0.07-0.14-0.14-0.23-0.16
c-0.19-0.05-0.38-0.07-0.59-0.11c0.03-0.14,0.05-0.26,0.08-0.36C62.9,85.22,63.26,85.27,63.48,84.96z"/>
<path d="M52.25,85.24c-0.01-0.25,0.18-0.32,0.33-0.44c0.05-0.04,0.11-0.14,0.09-0.19c-0.02-0.07-0.1-0.14-0.17-0.18
c-0.06-0.04-0.18,0-0.22-0.05c-0.28-0.28-0.55-0.12-0.83-0.02c-0.21,0.08-0.42,0.15-0.63,0.19c-0.33,0.06-0.38,0.09-0.43,0.38
c-0.16,0.12-0.29,0.21-0.42,0.3c0.07,0.09,0.15,0.18,0.23,0.28c-0.02,0.09-0.05,0.22-0.06,0.34c-0.05,0.34,0.02,0.45,0.31,0.63
c0.09,0.06,0.17,0.15,0.27,0.23c0.3-0.14,0.61-0.28,0.91-0.43c0.04-0.02,0.09-0.08,0.09-0.12c0.01-0.2,0.17-0.41-0.15-0.58
c0.23-0.09,0.38-0.15,0.52-0.21C52.14,85.34,52.25,85.29,52.25,85.24z"/>
<path d="M58.16,66.65c-0.16-0.14-0.35-0.12-0.52-0.03c-0.22,0.12-0.42,0.27-0.62,0.41c-0.18,0.13-0.35,0.27-0.51,0.42
c-0.06,0.06-0.1,0.15-0.13,0.23c-0.1,0.28-0.19,0.57-0.29,0.87c-0.22-0.1-0.43-0.11-0.58,0.1c-0.01,0.21,0.13,0.33,0.29,0.31
c0.3-0.03,0.6-0.09,0.88-0.19c0.22-0.08,0.25-0.24,0.15-0.48c-0.09-0.21-0.04-0.39,0.08-0.55c0.1-0.14,0.21-0.27,0.34-0.38
c0.28-0.25,0.51-0.22,0.82,0.1c-0.13,0.01-0.24,0.03-0.35,0.04c0,0.03,0,0.06-0.01,0.09c0.1,0.03,0.19,0.07,0.29,0.07
c0.3,0.01,0.38-0.1,0.3-0.4c-0.01-0.05-0.04-0.11-0.02-0.14C58.43,66.92,58.3,66.78,58.16,66.65z"/>
<path d="M54.73,70.12c0.03,0.18,0.13,0.29,0.32,0.28c0.25-0.02,0.49-0.04,0.7-0.22c0.08-0.07,0.2-0.12,0.3-0.16
c0.09-0.04,0.19-0.07,0.29-0.1c-0.14-0.27-0.14-0.28-0.54-0.42c-0.16-0.05-0.33-0.05-0.5-0.04c-0.05,0-0.1,0.13-0.12,0.21
c-0.01,0.06,0.04,0.14,0.08,0.25c-0.12-0.03-0.19-0.05-0.26-0.06C54.79,69.82,54.7,69.91,54.73,70.12z"/>
<path d="M57.42,69.01c0.02,0.11,0.05,0.22,0.09,0.4c0.14-0.13,0.24-0.19,0.3-0.28c0.05-0.06,0.09-0.21,0.07-0.23
c-0.08-0.06-0.19-0.12-0.29-0.12C57.48,68.79,57.4,68.88,57.42,69.01z"/>
<path d="M88.02,51.18c0.4,0.27,0.79,0.54,1.19,0.8c0.01,0,0.02,0,0.02,0c-0.06-0.08-0.11-0.17-0.19-0.22
c-0.25-0.19-0.51-0.37-0.77-0.55C88.21,51.17,88.04,51.16,88.02,51.18z"/>
<path d="M94.66,55.8c0.23,0.22,0.45,0.44,0.69,0.65c0.1,0.09,0.23,0.16,0.35,0.23c-0.04-0.05-0.08-0.1-0.12-0.15
C95.3,56.24,95.05,55.93,94.66,55.8z"/>
<path d="M94.63,55.76c-0.12-0.27-0.33-0.43-0.6-0.52C94.23,55.41,94.43,55.59,94.63,55.76z"/>
<path d="M109.9,73.57c-0.01,0.24,0.13,0.39,0.25,0.57L109.9,73.57z"/>
<path d="M91,69.63c0.01-0.23-0.24-1.14-0.41-1.28C90.64,68.53,90.93,69.42,91,69.63z"/>
<path d="M42.67,83.42c0.19-0.23,0.17-0.31-0.13-0.39c-0.4-0.11-0.78-0.05-1.16,0.11c-0.29,0.13-0.3,0.16-0.15,0.45
c-0.17,0.08-0.34,0.15-0.58,0.26c0.21,0.19,0.37,0.32,0.51,0.45c-0.19,0.05-0.41,0.04-0.51,0.15c-0.12,0.12-0.14,0.35-0.2,0.53
c-0.02,0.02-0.05,0.05-0.07,0.07c0.1,0.06,0.19,0.17,0.3,0.18c0.36,0.04,0.71,0.24,1.06,0.12c0.18,0.2,0.35,0.38,0.5,0.55
c0.4-0.19,0.46-0.25,0.43-0.47c-0.01-0.14-0.02-0.28-0.04-0.42c-0.05-0.37,0.05-0.76-0.2-1.09c-0.02-0.03-0.01-0.11,0.01-0.15
C42.51,83.66,42.58,83.53,42.67,83.42z M42.02,83.49c-0.12,0.06-0.25,0.08-0.38,0.12c-0.01-0.04-0.02-0.08-0.03-0.11
c0.22-0.16,0.46-0.24,0.76-0.2C42.36,83.6,42.13,83.43,42.02,83.49z"/>
<path d="M43.56,82.45c0.11-0.34,0.04-0.52-0.23-0.64c-0.17-0.07-0.34-0.15-0.51-0.2c-0.52-0.15-1.04-0.28-1.57-0.07
c-0.19,0.08-0.35,0.19-0.42,0.39c-0.03,0.08-0.03,0.19,0,0.26c0.09,0.18,0.21,0.35,0.32,0.52c0.05,0.08,0.12,0.15,0.22,0.01
c-0.05-0.17-0.14-0.36-0.02-0.46c0.35-0.06,0.62-0.13,0.89-0.15c0.26-0.02,0.56-0.1,0.72,0.27
C43.18,82.15,43.37,82.23,43.56,82.45z"/>
<path d="M74.64,46c-0.1,0.16-0.2,0.28-0.25,0.43c-0.08,0.22-0.13,0.45-0.21,0.67c-0.13,0.33-0.27,0.65-0.41,0.98
c-0.07,0.16-0.07,0.28,0.15,0.32c0.03-0.06,0.08-0.11,0.1-0.16c0.1-0.36,0.28-0.67,0.49-0.98c0.14-0.19,0.24-0.41,0.35-0.62
C74.97,46.39,74.9,46.15,74.64,46z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 64 KiB

BIN
logo/logo_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

581
logo/logo_white.svg Normal file
View File

@@ -0,0 +1,581 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Livello_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 298.9 141.89" style="enable-background:new 0 0 298.9 141.89;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M160.3,88.5c-0.86,0-1.29-0.43-1.29-1.29v-5.94c-3.01,4.22-7.83,8.09-14.81,8.09
c-11.71,0-19.8-10.07-19.8-22.38c0-12.31,8.09-22.38,19.8-22.38c6.89,0,11.62,3.61,14.81,8.18v-6.03c0-0.86,0.43-1.29,1.29-1.29
h7.14c0.86,0,1.29,0.43,1.29,1.29v40.46c0,0.86-0.43,1.29-1.29,1.29H160.3z M146.7,80.07c7.14,0,12.48-5.85,12.48-13.08
c0-7.23-5.34-13.08-12.48-13.08c-7.23,0-12.57,5.85-12.57,13.08C134.13,74.21,139.47,80.07,146.7,80.07z"/>
<path class="st0" d="M223.74,86.95c0,13.51-9.64,23.5-23.33,23.5c-7.14,0-13.6-2.93-17.39-6.8c-0.52-0.52-0.52-1.12-0.09-1.72
l3.79-4.99c0.26-0.34,0.6-0.52,0.86-0.52c0.26,0,0.52,0.17,0.77,0.34c2.84,2.67,6.97,4.39,11.79,4.39c9.12,0,13.86-6.2,13.86-13.34
v-6.97c-3.01,4.22-7.75,8.09-14.89,8.09c-11.62,0-19.71-9.73-19.71-22.03s8.09-22.29,19.71-22.29c6.89,0,11.71,3.61,14.89,8.18
v-6.03c0-0.86,0.43-1.29,1.29-1.29h7.14c0.86,0,1.29,0.43,1.29,1.29V86.95z M201.7,79.64c7.23,0,12.57-5.42,12.57-12.74
c0-7.32-5.34-13-12.57-13c-7.23,0-12.57,5.85-12.57,13C189.13,74.21,194.47,79.64,201.7,79.64z"/>
<path class="st0" d="M267.64,78.35c0.26-0.17,0.6-0.26,0.86-0.26c0.43,0,0.77,0.26,1.03,0.6l3.18,4.56
c0.43,0.77,0.34,1.29-0.34,1.72c-4.22,2.67-9.38,4.39-15.92,4.39c-12.22,0-22.04-10.07-22.04-22.38c0-11.71,8.18-22.38,20.92-22.38
c13.43,0,21.17,11.02,21.17,24.19c0,0.95-0.43,1.55-1.46,1.55h-31.42c1.12,5.85,6.02,10.76,13.08,10.76
C261.18,81.1,264.28,80.15,267.64,78.35z M267.21,63.2c-1.38-6.54-5.25-10.93-11.88-10.93c-6.2,0-10.5,4.3-11.62,10.93H267.21z"/>
</g>
<g>
<g>
<path class="st0" d="M142.92,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h5.26
c0.12,0,0.18,0.06,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-4.05v2.18h3.51c0.12,0,0.18,0.05,0.18,0.16v0.87
c0,0.09-0.05,0.16-0.18,0.16h-3.51v2.92c0,0.11-0.06,0.16-0.18,0.16H142.92z"/>
<path class="st0" d="M152.11,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.02
c0.12,0,0.18,0.06,0.18,0.16v7.36c0,0.11-0.06,0.16-0.18,0.16H152.11z"/>
<path class="st0" d="M157.89,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.02
c0.12,0,0.18,0.06,0.18,0.16v6.31h3.26c0.12,0,0.18,0.06,0.18,0.16v0.89c0,0.11-0.06,0.16-0.18,0.16H157.89z"/>
<path class="st0" d="M171.38,118.32c0.13,0,0.18,0.08,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-3.64v2.02h3.21
c0.12,0,0.18,0.06,0.18,0.16v0.87c0,0.11-0.06,0.16-0.18,0.16h-3.21v2.01h3.65c0.12,0,0.18,0.06,0.18,0.16v0.9
c0,0.11-0.06,0.16-0.18,0.16h-4.86c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16H171.38z"/>
<path class="st0" d="M187.4,118.32c0.13,0,0.18,0.08,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-3.64v2.02h3.21
c0.12,0,0.18,0.06,0.18,0.16v0.87c0,0.11-0.06,0.16-0.18,0.16h-3.21v2.01h3.65c0.12,0,0.18,0.06,0.18,0.16v0.9
c0,0.11-0.06,0.16-0.18,0.16h-4.86c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16H187.4z"/>
<path class="st0" d="M197.47,126.01c-0.16,0-0.22-0.04-0.31-0.16l-4.17-5.33v5.33c0,0.11-0.06,0.16-0.18,0.16h-1.03
c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.04c0.16,0,0.23,0.07,0.32,0.16l4.16,5.33v-5.33
c0-0.11,0.06-0.16,0.18-0.16h1.04c0.12,0,0.18,0.06,0.18,0.16v7.36c0,0.11-0.06,0.16-0.18,0.16H197.47z"/>
<path class="st0" d="M208.78,119.78c-0.04,0.05-0.1,0.09-0.18,0.09c-0.04,0-0.07-0.01-0.12-0.03c-0.48-0.26-0.94-0.39-1.56-0.39
c-1.71,0-2.98,1.22-2.98,2.74c0,1.52,1.27,2.73,2.98,2.73c0.53,0,1.12-0.12,1.6-0.42c0.05-0.03,0.09-0.04,0.12-0.04
c0.06,0,0.11,0.03,0.16,0.09l0.54,0.75c0.07,0.1,0.05,0.19-0.06,0.25c-0.7,0.39-1.48,0.59-2.36,0.59c-2.53,0-4.37-1.71-4.37-3.95
c0-2.22,1.84-3.97,4.37-3.97c0.92,0,1.71,0.24,2.33,0.59c0.11,0.06,0.13,0.13,0.07,0.23L208.78,119.78z"/>
<path class="st0" d="M219.34,125.83c0.09,0.12,0.06,0.18-0.1,0.18h-1.15c-0.13,0-0.24-0.06-0.33-0.16l-1.88-2.47h-1.07v2.47
c0,0.11-0.06,0.16-0.18,0.16h-1.03c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h2.84c1.67,0,2.8,1.12,2.8,2.53
c0,1.44-1.22,2.15-1.9,2.33L219.34,125.83z M214.81,119.47v2.75h1.32c1.11,0,1.71-0.61,1.71-1.37c0-0.76-0.6-1.37-1.71-1.37
H214.81z"/>
<path class="st0" d="M225.65,121.16l1.92-2.75c0.05-0.07,0.12-0.1,0.21-0.1h1.2c0.1,0,0.12,0.09,0.07,0.16l-2.68,3.78v3.58
c0,0.11-0.06,0.16-0.18,0.16h-1.04c-0.12,0-0.18-0.06-0.18-0.16v-3.58l-2.7-3.78c-0.05-0.08-0.02-0.16,0.07-0.16h1.21
c0.1,0,0.17,0.03,0.21,0.1L225.65,121.16z"/>
<path class="st0" d="M232.88,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h2.81
c1.7,0,2.78,1.12,2.78,2.53c0,1.36-1.16,2.56-2.78,2.56h-1.6v2.44c0,0.11-0.06,0.16-0.18,0.16H232.88z M234.09,122.26h1.29
c1.02,0,1.68-0.55,1.68-1.42c0-0.88-0.66-1.37-1.68-1.37h-1.29V122.26z"/>
<path class="st0" d="M244.19,126.01c-0.12,0-0.18-0.06-0.18-0.16v-6.3h-2.36c-0.12,0-0.18-0.06-0.18-0.16v-0.9
c0-0.11,0.06-0.16,0.18-0.16h6.1c0.12,0,0.18,0.06,0.18,0.16v0.9c0,0.11-0.06,0.16-0.18,0.16h-2.36v6.3
c0,0.11-0.06,0.16-0.18,0.16H244.19z"/>
<path class="st0" d="M251.84,126.01c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.02
c0.12,0,0.18,0.06,0.18,0.16v7.36c0,0.11-0.06,0.16-0.18,0.16H251.84z"/>
<path class="st0" d="M265.64,122.17c0,2.2-1.9,3.95-4.38,3.95c-2.48,0-4.37-1.71-4.37-3.95c0-2.22,1.89-3.97,4.37-3.97
C263.74,118.21,265.64,119.95,265.64,122.17z M264.24,122.17c0-1.52-1.27-2.74-2.98-2.74c-1.71,0-2.98,1.22-2.98,2.74
c0,1.52,1.27,2.73,2.98,2.73C262.95,124.9,264.24,123.69,264.24,122.17z"/>
<path class="st0" d="M275.35,126.01c-0.16,0-0.22-0.04-0.31-0.16l-4.17-5.33v5.33c0,0.11-0.06,0.16-0.18,0.16h-1.03
c-0.12,0-0.18-0.06-0.18-0.16v-7.36c0-0.11,0.06-0.16,0.18-0.16h1.04c0.16,0,0.23,0.07,0.32,0.16l4.16,5.33v-5.33
c0-0.11,0.06-0.16,0.18-0.16h1.04c0.12,0,0.18,0.06,0.18,0.16v7.36c0,0.11-0.06,0.16-0.18,0.16H275.35z"/>
</g>
</g>
<g>
<path class="st0" d="M109.23,72.27c-0.38-0.68-0.75-1.38-1.15-2.06c-0.34-0.57-0.67-1.34-1.04-1.9c-0.45-0.69-0.71-1.22-1.19-1.9
c-0.52-0.74-0.9-1.28-1.47-1.98c-0.63-0.79-1.47-1.74-2.14-2.5c-0.52-0.59-1.3-1.42-1.83-2.01c-0.16-0.18-2.04-2.14-3.51-3.45
c-2.03-1.76-4.38-3.5-5.64-4.39c-0.94-0.72-1.83-1.25-2.92-2.02c-0.08-0.04-0.19-0.09-0.21-0.16c-0.08-0.33-0.33-0.33-0.59-0.32
c-0.63,0.03-1.26,0.07-1.89,0.06c-0.71-0.01-1.42,0.04-2.13,0.08c-0.29,0.01-0.54-0.03-0.74-0.23c-0.13,0.07-0.24,0.18-0.35,0.19
c-0.52,0.03-1.05,0.03-1.58,0.04c-0.2,0.01-0.35-0.07-0.4-0.25c-0.06-0.18-0.17-0.29-0.34-0.38c1.27-0.08,2.52-0.14,3.77-0.18
c0.18-0.01,3.41,0.01,3.57-0.2c0.12-0.16,0.07-0.28-0.13-0.36c-0.08-0.03-0.18-0.06-0.23-0.12c-0.14-0.15-0.27-0.12-0.43-0.03
c-0.14,0.07-0.29,0.14-0.44,0.15c-0.62,0.03-1.24,0.04-1.85,0.06c-0.55,0.02-3.59,0.11-4.58,0.08c-0.01-0.13-0.07-0.27-0.03-0.35
c0.13-0.22,0.03-0.41-0.03-0.6c-0.1-0.34-0.19-0.68-0.16-1.04c0.02-0.3-0.1-0.45-0.45-0.57c-0.04,0.21-0.05,0.41-0.12,0.61
c-0.05,0.14-0.02,0.24,0.03,0.37c0.21,0.52,0.4,1.04,0.61,1.63c-0.91,0-1.76,0-2.66,0c0.06-0.71-0.05-1.42,0.01-2.13
c-0.21-0.32-0.35-0.33-0.6-0.49c0.36-0.21,0.72-0.12,1.09,0.04c0.11-0.24,0.32-0.21,0.52-0.21c0.72-0.01,1.44,0,2.16-0.03
c0.77-0.03,1.54-0.08,2.32,0.02c0.44,0.06,0.87,0.12,1.28,0.18c0.14,0.79,0.61,1.09,1.09,1.59c0.26,0.27,0.69,0.61,1.05,0.73
c0.02-0.02,0.03-0.04,0.05-0.07c-0.53-0.72-1.06-1.45-1.59-2.17c0-0.04,0-0.08,0.01-0.12c0.43-0.04,0.4-0.32,0.15-0.83
c-0.07-0.16-0.1-0.33-0.2-0.43c-0.2-0.21-0.13-2.15-0.14-2.45c0-0.26,0.03-0.53,0.04-0.79c0.02-0.9-0.01-1.8,0.06-2.69
c0.03-0.36-0.04-0.7-0.06-1.05c0-0.07-0.09-0.16-0.16-0.19c-0.09-0.04-0.21-0.03-0.31-0.03c-0.59,0.03-1.17,0.06-1.76,0.09
c-0.03,0-0.43,0-0.46,0c-0.56-0.07-0.84-0.08-1.4-0.1c-0.92-0.04-1.51,0.05-2.43,0.04c-0.79,0-1.71-0.01-2.5-0.03
c-0.45-0.01-1.04-0.03-1.48-0.05c-0.28-0.01-0.6-0.01-0.97-0.03c0.49-0.39,1.06-0.67,1.56-0.6c0.42,0.06,0.82,0.1,1.25,0.04
c0.99-0.14,1.99-0.15,2.99-0.09c0.83,0.05,1.65,0.15,2.47,0.31c0.36,0.07,1.06,0.07,1.34,0c0-0.03-0.62-0.03-0.88-0.09
c-0.25-0.05-0.64-0.09-0.89-0.14c-0.25-0.05-0.51-0.08-0.83-0.14c-0.11-0.17,0.29-0.14,0.31-0.14c0.8,0.01,1.43,0.07,2.23,0.1
c0.85,0.03,1.7,0.07,2.56,0.1c0.22,0.01,0.36-0.11,0.5-0.34c0.03-0.3-0.65-0.21-0.95-0.31c-0.01-0.1-0.03-0.19-0.04-0.29
c-0.18-0.12-0.35-0.1-0.51-0.08c-0.27,0.04-0.42-0.1-0.57-0.27c-0.16-0.17-0.33-0.34-0.48-0.51c-0.11-0.12-0.24-0.15-0.41-0.07
c-0.01,0.25-0.01,0.64,0.13,0.94c-0.46-0.01-1.31,0.01-1.54,0.01c-0.76,0.01-1.06-0.01-1.86,0c-0.64,0.06-1.4,0.21-1.86,0.01
c-0.09-0.17-0.16-0.64-0.32-0.66c-0.33,0.12-0.33,0.12-0.38,0.5C76.47,35.92,76.5,36,76.25,36c-0.24,0.04-0.42,0.03-0.61-0.07
c0.06-0.21,0.11-0.35,0.13-0.49c0.04-0.21-0.07-0.33-0.28-0.33c-0.21-0.01-0.28,0.14-0.37,0.3c-0.29,0.52-0.43,0.58-1.01,0.44
c0.01-0.04,0-0.08,0.02-0.11c0.12-0.15,0.13-0.32-0.04-0.41c-0.12-0.06-0.35-0.09-0.44-0.02c-0.21,0.15-0.37,0.38-0.57,0.6
c-0.23,0-0.5,0-0.78,0c0-0.06-0.01-0.1,0-0.14c0.04-0.14,0.04-0.29-0.11-0.34c-0.15-0.05-0.29-0.01-0.38,0.18
c-0.09,0.22-0.3,0.33-0.54,0.33c-0.24,0-0.49,0-0.8,0c0.24-0.27,0.43-0.51,0.64-0.74c0.17-0.19,0.18-0.2-0.01-0.45
c-0.17,0.02-0.33,0.05-0.51,0.08c-0.11,0.65-0.61,0.91-1.13,1.1c-0.36,0.13-0.76,0.17-1.15,0.25c-0.13,0.02-0.23,0.06-0.25,0.2
c-0.01,0.13,0.06,0.19,0.17,0.22c0.2,0.06,0.28,0.19,0.3,0.41c0.04,0.42,0.49,0.58,0.63,0.94c0.16-0.01,0.15,0.1,0.16,0.21
c0.03,0.33,0.08,0.65,0.11,0.98c0.06,0.56,0.18,1.13,0.14,1.69c-0.08,1.06-0.01,2.11,0.01,3.16c0,0.26,0.03,0.52,0.04,0.76
c0.47,0.34,0.73,0.19,0.79-0.5c0-0.01,0-0.03,0-0.04c-0.07-1.09-0.07-2.18-0.21-3.26c-0.12-0.91-0.07-1.84,0.12-2.76
c0.02-0.09,0.05-0.17,0.07-0.27c0.2-0.01,0.38-0.02,0.61-0.03c0,0.18,0,0.33,0,0.49c-0.01,0.87-0.02,1.74-0.02,2.61
c0.01,0.94,0.03,1.87,0.05,2.81c0,0.17,0,0.34,0.03,0.51c0.01,0.08,0.07,0.15,0.16,0.32c0.06-0.16,0.1-0.24,0.13-0.33
c0.09,0.1,0.12,0.17,0.16,0.18c0.12,0.04,0.25,0.1,0.36,0.07c0.06-0.02,0.11-0.19,0.12-0.29c0-0.25-0.01-0.5-0.04-0.75
c-0.06-0.5-0.16-0.99-0.19-1.49c-0.04-0.93-0.04-1.87-0.05-2.8c0-0.18,0.04-0.36,0.06-0.54c0.05-0.03,0.09-0.06,0.12-0.07
c0.14-0.02,0.27-0.04,0.27-0.22c0-0.15-0.1-0.18-0.23-0.21c-0.11-0.03-0.2-0.14-0.3-0.21c0.01-0.03,0.02-0.05,0.03-0.08
c0.29,0,0.59,0,0.9,0c0.04,0.17,0.12,0.36,0.13,0.55c0.06,0.89,0.12,1.79,0.16,2.69c0.05,1.11,0.07,2.21,0.11,3.32
c0.01,0.23,0.03,0.46,0.05,0.72c-1.57,0-3.1,0-4.62,0c-0.07,0.33-0.04,0.38,0.22,0.52c0.4,0.2,0.46,0.47,0.17,0.82
c-0.17,0.2-0.34,0.4-0.52,0.59c-0.12,0.13-0.24,0.25-0.37,0.35c-0.2,0.15-0.42,0.27-0.63,0.41c-0.39,0.27-0.79,0.51-1.29,0.55
c-0.27,0.02-0.54,0.09-0.79,0.14c-0.12,0.37-0.12,0.37,0.09,0.61c-0.09,0.08,9.04-0.12,13.65-0.05c-0.04,0.11,0.02,0.53-0.02,0.67
c-0.4,0-0.8,0.01-1.19,0c-0.16-0.01-0.32-0.06-0.54-0.11c-0.17,0.22-0.48,0.17-0.79,0.17c-0.46,0.01-0.92,0.04-1.38,0.05
c-0.14,0-0.28-0.02-0.43-0.04c0-0.11,0-0.2,0-0.29c-0.36-0.13-0.43,0.25-0.59,0.33c-0.33-0.04-7.18,0.04-7.44,0.03
c-0.34-0.01-0.68-0.06-1.02-0.08c-0.35-0.02-0.69,0.14-1.04-0.04c-0.13-0.07-0.25,0.02-0.34,0.14c-0.08,0.12-0.05,0.23,0.05,0.32
c0.04,0.04,0.12,0.05,0.23,0.1c-0.41,0.24-0.7,0.4-0.98,0.59c-1.21,0.82-2.53,1.47-3.66,2.4c-0.34,0.28-0.73,0.5-1.07,0.78
c-0.32,0.27-2.14,1.65-2.7,2.09c-0.6,0.47-5.88,5.57-6.81,6.58c-0.39,0.43-0.75,0.88-1.16,1.28c-0.38,0.37-1.25,1.52-1.37,1.67
c-0.88,1.06-4.49,5.8-5.9,8.45c-1.36,2.37-3.84,8-4.06,8.63c-0.26,0.76-0.52,1.51-0.67,2.3c-0.1,0.49-0.92,3.62-1.05,4.36
c-0.12,0.69-0.44,3.2-0.47,3.45c-0.1,0.9-0.19,1.81-0.28,2.75c-0.2,0.03-0.41,0.07-0.63,0.1c-0.16,0.02-0.25,0.12-0.26,0.26
c-0.01,0.08,0.07,0.18,0.12,0.26c0.04,0.06,0.12,0.1,0.16,0.16c0.16,0.21,0.38,0.26,0.63,0.25c0.78-0.03,1.55-0.09,2.33-0.1
c0.75-0.01,1.5,0,2.25,0.04c0.71,0.03,11.13,0.14,15.64,0.14c0.72,0,3.91-0.06,4.78-0.06c0.3,0,2.91-0.1,3.91-0.16
c1-0.06,11.01-0.24,11.59-0.23c0.05,0,4.25,0.13,5.42,0.14c0.21,0,2.03,0.06,2.72,0.02c1.26-0.08,15.17,0.11,16.68,0.09
c0.63-0.01,6.52-0.07,7.29-0.1c0.55-0.02,1.11-0.02,1.66-0.04c0.09,0,0.19-0.04,0.3-0.07c-0.07-0.25-0.22-0.33-0.41-0.38
c-0.14-0.04-0.27-0.09-0.41-0.14c-0.15-0.04-0.29-0.11-0.44-0.12c-0.25-0.02-3.64,0.06-4.56,0.11c-0.43,0.02-3.52-0.02-3.93-0.01
c-0.08,0-0.16-0.01-0.24-0.02c-0.25-0.02-0.49-0.05-0.74-0.07c-0.24-0.02-0.48-0.04-0.71-0.03c-0.83,0.02-5.75,0.01-6.65,0.02
c-0.63,0.01-5.53-0.01-5.83-0.01c-0.81-0.02-18.52,0.09-19.63,0.08c-0.33,0-4.97,0.16-5.02,0.16c-0.28,0.01-5.25-0.03-5.59-0.03
c-0.87,0.01-3.11,0-3.36,0.01c-0.51,0.01-2.73,0.01-3.32,0.02c-0.33,0-0.36,0-0.42-0.32c-0.17-0.9-0.18-1.81-0.15-2.72
c0.03-0.71,0.18-1.42,0.2-2.12c0.03-1.3,0.29-2.56,0.56-3.83c0.02-0.08,0.04-0.22,0-0.25c-0.11-0.08-0.06-0.16-0.04-0.24
c0.24-0.97,0.49-1.94,0.73-2.92c0.24-0.96,0.46-1.92,0.71-2.88c0.2-0.74,0.41-1.47,0.65-2.2c0.22-0.68,0.44-1.36,0.73-2
c0.4-0.9,0.87-1.78,1.32-2.66c0.51-1,1.03-1.99,1.53-2.99c0.33-0.66,0.63-1.34,1.07-1.93c0.04-0.05,0.09-0.15,0.07-0.17
c-0.2-0.18-0.02-0.3,0.06-0.43c0.25-0.4,0.51-0.8,0.77-1.2c0.49-0.75,0.97-1.5,1.47-2.25c0.73-1.08,3.38-4.74,4.06-5.43
c0.59-0.59,2.32-2.61,2.62-2.92c0.42-0.43,1.09-1.41,0.97-1.48c0.33-0.34,0.62-0.67,0.95-0.98c0.47-0.44,0.96-0.86,1.43-1.29
c0.27-0.25,1.37-1.21,1.53-1.57c0.01-0.03,0.08-0.05,0.13-0.06c0.35-0.08,0.66-0.23,0.83-0.56c0.17,0.01,0.33,0.01,0.54,0.02
c-0.08,0.09-0.12,0.14-0.16,0.19c-0.25,0.3-0.51,0.6-0.76,0.9c-0.34,0.42-0.67,0.84-1.01,1.27c-0.43,0.54-0.91,1.04-1.21,1.68
c-0.19,0.4-0.49,0.75-0.73,1.13c-0.7,1.12-1.41,2.22-2.04,3.39c-0.59,1.1-4.34,7.71-4.36,7.76c-0.18,0.37-0.35,0.74-0.52,1.1
c-0.13,0.29-0.26,0.58-0.4,0.86c-0.32,0.63-0.66,1.26-0.97,1.9c-0.21,0.44-0.41,0.88-0.58,1.34c-0.43,1.11-0.84,2.22-1.25,3.33
c-0.27,0.73-0.51,1.47-0.75,2.2c-0.21,0.65-0.42,1.31-0.6,1.96c-0.17,0.63-0.32,1.28-0.47,1.92c-0.02,0.1-0.05,0.21-0.07,0.31
c-0.02,0.1-0.04,0.2-0.05,0.3c0.02,0,0.03,0.01,0.05,0.01c0.06-0.14,0.13-0.28,0.19-0.42c-0.04,0.35-0.12,0.67-0.21,1
c-0.05,0.19-0.12,0.37-0.17,0.52c-0.22-0.14-0.42-0.3-0.65-0.41c-0.26-0.13-0.48-0.27-0.61-0.54c-0.05-0.1-0.14-0.19-0.24-0.26
c-0.15-0.11-0.3-0.19-0.38-0.37c-0.03-0.07-0.18-0.12-0.26-0.11c-0.14,0.02-0.28,0.07-0.39,0.15c-0.53,0.34-1.05,0.7-1.59,1.02
c-0.19,0.11-0.3,0.24-0.33,0.45c-0.02,0.22,0.15,0.29,0.31,0.34c-0.09,0.22-0.37,0.33-0.26,0.6c0.47,0.02,0.93,0.05,1.21-0.45
c-0.13-0.01-0.24-0.02-0.35-0.02c0.16-0.67,0.56-1.06,1.23-1.18c0.25,0.18,0.51,0.37,0.8,0.58c-0.06,0.57,0.94,0.86,1.43,0.48
c0,0.08,0.01,0.14,0,0.21c-0.1,0.52-0.2,1.03-0.29,1.55c-0.15,0.83-0.3,1.66-0.44,2.49c-0.06,0.36-0.1,0.73-0.13,1.1
c-0.04,0.58-0.03,1.16-0.08,1.73c-0.05,0.56-0.31,4-0.28,4.67c0,0,0.03,0.4,0.03,0.19c0.08-0.17,0.09-0.34,0.14-0.5
c0.22-0.76,0.31-1.55,0.39-2.33c0.09-0.84,0.19-1.68,0.31-2.51c0.17-1.09,0.37-2.18,0.55-3.27c0.15-0.9,0.3-1.79,0.46-2.69
c0.15-0.83,0.29-1.66,0.47-2.48c0.12-0.54,0.32-1.06,0.47-1.59c0.16-0.6,0.3-1.2,0.46-1.8c0.24-0.88,0.49-1.75,0.9-2.57
c0.15-0.3,0.27-0.6,0.4-0.91c0.02-0.06,0.06-0.17,0.04-0.18c-0.29-0.16-0.06-0.37,0-0.5c0.29-0.72,0.62-1.43,0.93-2.14
c0.06-0.13,0.12-0.26,0.18-0.39c0.06-0.12,1.18-2.1,1.63-3.04c0.57-1.19,1.11-2.39,1.72-3.56c0.36-0.7,0.7-1.42,1.23-2.02
c0.03-0.04,0.04-0.09,0.06-0.14c0.07-0.17,0.12-0.35,0.21-0.5c0.38-0.68,0.79-1.34,1.17-2.02c0.56-1.02,1.24-1.96,1.92-2.9
c0.36-0.5,0.7-1.02,1.04-1.54c0.25-0.38,0.46-0.8,0.76-1.15c0.5-0.6,1.05-1.17,1.59-1.76c-0.09-0.22-0.09-0.24,0.11-0.43
c0.4-0.38,1.05-1.28,0.96-1.32c0.23-0.21,0.42-0.31,0.61-0.42c0.38-0.21,0.38-0.21,0.62-0.15c-0.12,0.14-0.23,0.27-0.34,0.39
c-0.44,0.51-0.88,1.01-1.3,1.54c-0.28,0.35-0.48,0.76-0.76,1.11c-0.8,1.03-1.48,2.14-2.16,3.25c-0.37,0.59-0.78,1.16-1.12,1.77
c-0.53,0.95-1.05,1.92-1.53,2.9c-0.32,0.66-0.63,1.33-0.84,2.03c-0.19,0.63-0.39,1.26-0.66,1.86c-0.2,0.44-0.42,0.88-0.59,1.33
c-0.32,0.82-0.6,1.66-0.93,2.48c-0.44,1.09-1.87,5.3-2.1,6.34c-0.19,0.86-0.35,1.73-0.58,2.58c-0.25,0.94-0.55,1.87-0.74,2.83
c-0.09,0.45-0.26,0.89-0.32,1.34c-0.13,0.99-0.17,2-0.33,2.98c-0.18,1.09-0.14,2.18-0.21,3.27c-0.06,0.83,0.03,2.61,0.06,2.74
c0.3-1.16,0.13-2.28,0.36-3.36c0.02,0.14,0.05,0.28,0.07,0.42c0.11-0.3,0.15-0.59,0.18-0.88c0.21-2.3,0.63-4.57,1.12-6.82
c0.34-1.56,0.71-3.11,1.09-4.66c0.34-1.39,0.69-2.78,1.25-4.1c0.02-0.06,0.01-0.13,0.02-0.19c0.01-0.12,0.01-0.24,0.04-0.35
c0.31-1.2,0.77-2.35,1.24-3.5c0.45-1.08,0.93-2.15,1.41-3.23c0.48-1.07,0.97-2.14,1.46-3.2c0.28-0.6,0.57-1.19,0.86-1.78
c0.24-0.48,0.47-0.98,0.73-1.44c0.35-0.61,0.75-1.2,1.08-1.81c0.55-1.01,1.23-1.93,1.91-2.86c0.11-0.15,1.38-1.64,2-2.29
c0.22-0.23,0.48-0.42,0.54-0.79c0.09,0.01,0.19,0.02,0.3,0.03c-0.08,0.14-0.16,0.25-0.21,0.36c-0.37,0.82-0.73,1.64-1.09,2.45
c-0.34,0.76-0.56,1.56-0.84,2.34c-0.27,0.79-0.45,1.59-0.68,2.39c-0.13,0.48-0.27,0.96-0.41,1.44c-0.3,1.05-0.61,2.1-0.91,3.15
c-0.19,0.68-0.36,1.37-0.56,2.06c-0.19,0.67-0.4,1.33-0.61,2c-0.15,0.48-0.32,0.96-0.47,1.4c-0.35-0.21-0.71-0.38-1.03-0.6
c-0.46-0.32-0.97-0.32-1.48-0.26c-0.28,0.03-0.57,0.13-0.72,0.42c-0.27,0.51-0.53,1.03-0.78,1.55c-0.13,0.27-0.07,0.41,0.21,0.63
c0.34-0.19,0.65-0.39,0.69-0.84c0.01-0.11,0.12-0.24,0.21-0.32c0.2-0.17,0.42-0.32,0.63-0.48c0.41-0.31,0.71-0.25,0.96,0.2
c0.09,0.16,0.18,0.32,0.42,0.32c0-0.19,0-0.37,0-0.57c0.27,0.11,0.48,0.36,0.8,0.19c-0.12,0.43-0.22,0.8-0.32,1.18
c-0.15,0.58-1.63,9.63-1.69,10.38c-0.05,0.6-0.12,1.21-0.2,1.81c-0.07,0.57-0.12,1.15-0.23,1.72c-0.15,0.76-0.16,1.52-0.16,2.28
c0,0.34,0.12,0.69,0.07,1.01c-0.1,0.57-0.09,1.14-0.07,1.72c0.01,0.25,0.01,0.5-0.02,0.75c-0.04,0.31-0.11,0.62-0.16,0.93
c-0.01,0.06,0.03,0.13,0.05,0.25c0.08-0.52,0.17-0.97,0.23-1.43c0.08-0.6,0.14-1.21,0.21-1.81c0.08-0.63,0.17-1.25,0.24-1.88
c0.07-0.72,0.12-1.44,0.18-2.17c0.01-0.09,0.05-0.17,0.07-0.26c0.02,0.01,0.04,0.69,0.03,1.02c0.07-0.12,0.12-0.24,0.13-0.37
c0.08-0.77,0.15-1.55,0.21-2.33c0.11-1.31,0.27-2.62,0.5-3.91c0-0.03,0.01-0.05,0.01-0.08c0.27-1.02,0.55-2.03,0.82-3.05
c0.06-0.22,0.14-0.45,0.1-0.76c-0.19,0.3-0.17,0.64-0.42,0.83c0.09-0.41,0.27-0.8,0.3-1.21c0.06-0.71,0.47-1.33,0.41-2.06
c0-0.01,0.01-0.03,0.01-0.04c0.08-0.26,0.09-0.52,0.13-0.78c0.1-0.6,0.24-1.19,0.39-1.78c0.42-1.63,0.84-3.26,1.27-4.89
c0.21-0.8,0.36-1.62,0.77-2.36c0.03-0.05,0.04-0.14,0.01-0.17c-0.12-0.11-0.06-0.21-0.03-0.32c0.06-0.23,0.1-0.46,0.17-0.69
c0.26-0.83,0.52-1.66,0.79-2.48c0.06-0.19,0.17-0.35,0.24-0.54c0.1-0.27,0.16-0.55,0.27-0.82c0.3-0.73,0.58-1.47,0.92-2.19
c0.32-0.68,0.47-1.43,0.97-2.02c0.02-0.03,0.02-0.08,0.04-0.11c0.19-0.31,0.37-0.63,0.57-0.94c0.04-0.06,0.12-0.11,0.19-0.12
c0.21-0.01,0.43-0.01,0.66-0.01c-0.09,0.3-0.17,0.57-0.25,0.83c-0.15,0.49-0.31,0.97-0.44,1.47c-0.25,0.94-0.46,1.89-0.71,2.83
c-0.23,0.84-1.89,7.97-1.98,8.69c-0.07,0.56-1.18,5.38-1.35,9.4c-0.03,0.97-0.13,1.94-0.21,2.92c-0.02,0.3-0.04,0.6-0.07,0.9
c-0.05,0.48-0.14,0.96-0.16,1.45c-0.04,0.99-0.05,1.98-0.08,2.97c-0.02,0.79-0.06,1.58-0.08,2.37c-0.01,0.49,0,0.97,0,1.46
c0.1,0.07,0.22-0.88,0.29-1.4c0.01-0.08,0.02-0.17,0.06-0.22c0.11-0.12,0.08-0.22,0.06-0.36c-0.05-0.31-0.02-0.62,0.28-0.82
c-0.16-0.33-0.11-0.65-0.06-0.98c0.06-0.46,0.11-0.91,0.14-1.37c0.06-0.98,0.1-1.97,0.16-2.95c0.01-0.23,0.04-0.47,0.19-0.77
c0,0.72,0,1.35,0,1.98c0.05-0.17,0.04-0.33,0.05-0.5c0.03-0.46,0.06-0.92,0.09-1.38c0.07-1.13,0.12-2.26,0.23-3.39
c0.09-0.9,0.24-1.8,0.38-2.7c0.03-0.2,1.07-5.9,1.25-6.86c0.2-1.04,0.35-2.1,0.56-3.14c0.3-1.46,0.63-2.91,0.95-4.36
c0.23-1.06,0.49-2.1,0.94-3.09c0.05-0.1,0.03-0.23,0.05-0.35c-0.02,0-0.05-0.01-0.07-0.01c-0.09,0.16-0.18,0.32-0.26,0.48
c0.1-0.85,0.67-2.45,1.06-3.12c0.2,0.01,0.41,0.02,0.6,0.03c0.06,0.21,0.11,0.37,0.16,0.53c0.27,0.81,0.55,1.62,0.82,2.43
c0.33,0.97,0.66,1.94,0.92,2.93c0.14,0.56,0.28,1.12,0.4,1.69c0.11,0.53,0.18,1.06,0.27,1.59c0.07,0.4,0.16,0.79,0.23,1.19
c0.12,0.65,0.25,1.29,0.33,1.94c0.03,0.27,0.18,0.5,0.21,0.76c0.08,0.61,0.19,1.22,0.24,1.84c0.09,1.07,0.14,2.15,0.23,3.22
c0.06,0.76,0.14,1.52,0.23,2.28c0.09,0.76,0.27,1.49,0.45,2.23c0.17,0.69,0.34,1.39,0.28,2.1c-0.08,0.87,0.01,2.53,0.11,3.48
c0.06-0.3,0.11-1.32,0.16-1.53c0.09-0.03-0.05-2.85-0.11-4.23c0.03,0,0.05-0.01,0.08-0.01c0.04,0.23,0.08,0.46,0.12,0.69
c0.01-0.42,0.01-0.84-0.04-1.25c-0.1-0.88-0.23-1.75-0.35-2.62c-0.01-0.09-0.04-0.18-0.06-0.27c-0.05-0.19-0.11-0.38-0.15-0.57
c-0.16-0.92-0.3-1.85-0.45-2.77c-0.08-0.48-0.17-0.96-0.24-1.44c-0.11-0.72-0.2-1.43-0.3-2.15c-0.07-0.51-0.15-1.01-0.24-1.52
c-0.09-0.51-0.05-1.02-0.12-1.53c-0.07-0.48-0.11-0.96-0.23-1.43c-0.25-1-0.54-1.99-0.76-2.99c-0.28-1.25-0.52-2.5-0.93-3.72
c-0.09-0.26-0.09-0.56-0.13-0.84c0.2-0.03,0.34-0.04,0.48-0.08c0.22-0.06,0.35,0,0.44,0.21c0.11,0.26,0.26,0.51,0.39,0.76
c0.08,0.15,0.2,0.28,0.26,0.43c0.14,0.38,0.43,0.7,0.44,1.13c0,0.05,0.04,0.1,0.07,0.14c0.41,0.65,1.79,3.87,1.82,3.96
c0.36,0.85,0.54,1.76,0.8,2.64c0.04,0.13,0.06,0.25,0.09,0.38c0.05,0.21,0.08,0.42,0.15,0.61c0.27,0.78,0.54,1.57,0.55,2.41
c0,0.07,0.01,0.13,0.03,0.2c0.29,1.12,0.65,2.22,0.8,3.38c0.07,0.52,0.19,1.03,0.27,1.55c0.14,0.86,0.26,1.72,0.39,2.58
c0.02,0.13,0.1,1.12,0.23,1.1c0.06-0.49-0.21-1.9-0.11-2.39c0.25,0.93,0.25,1.91,0.53,2.83c-0.04-0.69-0.1-1.37-0.2-2.05
c-0.26-1.71-0.53-3.41-0.81-5.11c-0.16-0.96-0.34-1.92-0.51-2.88c-0.08-0.42-1.13-4.27-1.44-5.08c-0.45-1.17-0.88-2.35-1.35-3.51
c-0.3-0.74-0.68-1.46-1-2.19c-0.15-0.35-0.39-0.69-0.38-1.12c0.23-0.05,0.42-0.06,0.59,0.12c0.37,0.39,2.71,3.45,3.14,3.98
c0.81,1,1.58,2.04,2.14,3.21c0.29,0.61,0.64,1.18,0.93,1.79c0.38,0.77,0.75,1.54,1.09,2.33c0.52,1.18,1.01,2.37,1.52,3.56
c0.2,0.46,0.39,0.92,0.59,1.38c0.07,0.15,0.12,0.31,0.19,0.46c0.12,0.28,0.11,0.62,0.4,0.82c0.02-0.04,0.04-0.09,0.03-0.13
c-0.28-0.71-0.55-1.42-0.84-2.12c-0.49-1.22-0.99-2.43-1.49-3.65c-0.28-0.68-0.59-1.35-0.88-2.03c-0.28-0.66-0.53-1.33-0.82-1.99
c-0.16-0.36-0.37-0.7-0.55-1.04c-0.12-0.23-0.25-0.47-0.37-0.7c0.02-0.01,0.04-0.03,0.07-0.04c0.13,0.19,0.27,0.37,0.39,0.57
c0.2,0.31,0.39,0.62,0.58,0.92c0,0-1.16-2.2-1.76-3.17c-0.43-0.69-0.93-1.35-1.4-2.01c-0.39-0.55-0.78-1.1-1.18-1.64
c-0.13-0.18-0.28-0.34-0.46-0.55c0.23,0,0.41-0.01,0.59,0c0.18,0.01,0.31,0.12,0.3,0.3c-0.01,0.21,0.1,0.31,0.24,0.41
c0.12,0.09,0.22,0.19,0.32,0.29c0.39,0.39,0.76,0.81,1.16,1.19c0.64,0.61,1.23,1.26,1.75,1.98c0.36,0.5,0.72,1.01,1.08,1.51
c0.11,0.16,0.28,0.32,0.31,0.5c0.05,0.34,0.27,0.54,0.45,0.79c0.31,0.43,0.64,0.84,0.93,1.28c0.36,0.54,0.72,1.1,1.05,1.66
c0.35,0.62,0.69,1.24,0.99,1.89c0.3,0.64,0.55,1.31,0.84,1.96c0.08,0.18,0.18,0.34,0.27,0.51c0.03-0.01-0.09-0.36-0.13-0.52
c-0.04-0.16-0.86-2.36-0.97-2.59c-0.41-0.84-0.8-1.69-1.22-2.52c-0.33-0.66-0.69-1.31-1.03-1.96c-0.03-0.06-0.09-0.11-0.09-0.17
c-0.02-0.38-0.29-0.62-0.5-0.89c-0.42-0.55-0.85-1.1-1.29-1.65c-0.29-0.36-0.58-0.71-0.88-1.07c-0.26-0.31-0.51-0.63-0.76-0.95
c-0.01-0.01,0.51,0.26,0.72,0.44c0.68,0.59,1.38,1.17,2.02,1.81c0.47,0.46,0.9,0.96,1.28,1.49c0.31,0.43,0.68,0.79,1.1,1.12
c-0.47-0.81-1.12-1.5-1.64-2.28c0.39,0.29,0.77,0.59,1.1,0.94c0.62,0.66,1.21,1.34,1.81,2.03c0.43,0.5,1.99,2.35,2.1,2.43
c0.05-0.33-0.2-0.42-0.31-0.58c-0.39-0.55-0.79-1.09-1.2-1.62c-0.43-0.56-0.85-1.13-1.34-1.63c-1.2-1.21-2.43-2.38-3.65-3.56
c-0.39-0.38-0.77-0.76-1.16-1.13c-0.34-0.32-0.73-0.61-1.03-0.96c-0.26-0.31-0.57-0.48-0.94-0.59c-0.06-0.02-0.1-0.09-0.21-0.19
c0.44-0.04,0.8-0.07,1.16-0.11c-0.13,0.24-0.11,0.24-0.08,0.48c0.03,0.25,0.21,0.27,0.38,0.33c0.05,0.02,0.1,0.05,0.14,0.08
c0.53,0.36,1.06,0.72,1.59,1.1c0.75,0.53,1.37,1.2,2,1.87c0.39,0.42,0.77,0.86,1.15,1.29c0.01,0.01,0.04,0.01,0.06,0.02
c0.01-0.04,0.02-0.07,0.04-0.18c0.35,0.31,0.67,0.6,0.99,0.88c-0.45-0.63-1.01-1.16-1.45-1.8c0.3,0.19,3.84,3.56,4.54,3.85
c-0.92-1.18-1.9-2.25-2.95-3.25c0.44,0.24,0.81,0.58,1.17,0.91c0.38,0.35,0.75,0.71,1.11,1.07c0.34,0.35,1.06,0.96,1.38,1.33
c-0.06-0.11-0.43-0.55-0.5-0.67c0.05-0.02,0.09-0.04,0.11-0.06c0.03-0.02,0.04-0.06,0.07-0.09c-0.03-0.05-2.63-2.88-4.14-4
c-0.69-0.51-1.37-1.03-2.06-1.54c-0.54-0.4-1.09-0.78-1.64-1.17c-0.16-0.12-0.31-0.25-0.47-0.38c0.03-0.05,0.05-0.14,0.09-0.14
c0.1-0.01,0.22-0.02,0.31,0.02c0.22,0.11,0.43,0.25,0.63,0.38c0.66,0.41,1.31,0.83,1.96,1.25c0.58,0.37,1.16,0.73,1.73,1.1
c0.12,0.07,0.21,0.18,0.32,0.26c-0.02,0.02-0.03,0.04-0.05,0.06c-0.35-0.19-0.71-0.37-1.06-0.57c-0.32-0.18-0.62-0.39-0.93-0.57
c-0.09-0.05-0.19-0.08-0.29-0.12c0.11,0.11,0.21,0.23,0.33,0.32c0.57,0.44,1.16,0.87,1.74,1.3c0.15,0.11,0.48,0.29,0.5,0.27
c-0.32-0.26-0.64-0.53-0.96-0.79c0.1-0.03,0.15-0.01,0.21,0.03c0.93,0.56,1.81,1.19,2.66,1.87c0.07,0.06,0.15,0.11,0.22,0.16
c-0.12-0.16-0.07-0.43-0.19-0.59c1.18,0.92,5.41,4.74,5.46,4.77c0.53,0.58,1.5,1.54,1.6,1.65c0.09,0.1,1.46,1.8,2,2.45
c0.81,0.98,1.59,1.99,2.36,3c0.34,0.45,0.63,0.94,0.94,1.41c0.1,0.15,0.19,0.38,0.33,0.42c0.24,0.08,0.32,0.26,0.43,0.42
c0.29,0.44,0.57,0.88,0.84,1.33c0.67,1.13,1.34,2.26,2.01,3.38c0.03,0.06,0.1,0.09,0.15,0.13c-0.01-0.06-0.02-0.12-0.04-0.17
C109.64,73,109.43,72.64,109.23,72.27z M79.01,37.91c1.07,0.05,2.8-0.03,4.27,0.05c-0.04,0.44-0.08,0.85-0.11,1.26
c-0.01,0.13-0.03,0.26-0.02,0.39c0.07,1.09,0.14,2.18,0.21,3.27c0.01,0.24,0.01,0.47,0.01,0.71c0.01,0.16,0.01,0.3,0.14,0.44
c0.06,0.06,0.05,0.21,0.04,0.32c-0.01,0.27-0.1,0.54,0.05,0.81c-0.09-0.07-0.19-0.15-0.28-0.22c-0.54,0.17-3.35,0.29-4.2,0.21
c0.02-0.57-0.11-4.24-0.12-5.2c0-0.51,0.02-1.03,0.01-1.54C79.01,38.24,78.97,38.14,79.01,37.91z M78.3,37.95
c0.2,0.39,0.08,0.75,0.08,1.1c0,0.32,0,0.63,0,0.95c0,0.67-0.01,1.34,0.01,2.01c0.02,0.67,0.06,1.34,0.12,2.01
c0.03,0.39,0.13,0.77,0.19,1.16c-0.33,0-0.68,0-1.06,0c-0.04-0.47-0.05-0.92-0.11-1.37c-0.14-1.05-0.07-2.1-0.13-3.15
c-0.02-0.29-0.05-0.58-0.05-0.87c0-0.43,0.01-0.86,0.04-1.3c0.01-0.19-0.01-0.36-0.2-0.53C77.59,37.95,77.95,37.95,78.3,37.95z
M72.16,37.32c-0.41-0.06-2.45-0.08-2.8,0.03c-0.29-0.21-0.23-0.4-0.27-0.58c0.5-0.13,2.1-0.05,2.17-0.12
c0.29,0.03,0.74,0.01,1.01,0.05c0.08,0.01,0.38,0.11,0.39,0.15C72.67,37.01,72.26,37.34,72.16,37.32z M75.34,37.93
c0.35,0,0.83,0.04,1.2,0.04c0,0.29,0.11,0.59-0.08,0.87c-0.04,0.05-0.04,0.16-0.01,0.22c0.21,0.39,0.12,0.83,0.16,1.24
c0.06,0.65,0.16,1.29,0.16,1.95c0.01,0.96,0.1,1.93,0.1,2.96c-0.32,0-0.91-0.05-1.25-0.05c-0.14-0.86-0.18-1.42-0.22-2.32
c-0.04-0.91,0-1.93-0.05-2.84C75.31,39.4,75.25,38.54,75.34,37.93z M73.38,37.88c0.42,0,0.8,0,1.23,0c0,0.24,0,0.46,0,0.68
c0.01,0.33,0.03,0.65,0.03,0.98c0,0.66,0.04,1.32-0.01,1.97c-0.05,0.66-0.01,1.31,0.08,1.96c0.06,0.4,0.11,0.81,0.19,1.21
c0.03,0.13,0.12,0.25,0.19,0.38c-0.23,0.13-0.9,0.14-1.72,0.04c0.06-0.14,0.16-0.28,0.17-0.43c0.02-0.16-0.06-0.34-0.06-0.51
c-0.02-0.64-0.03-1.29-0.04-1.93c-0.01-0.34-0.01-0.68-0.01-1.02c0.01-0.75,0.02-1.5,0.03-2.25c0-0.21,0.03-0.41-0.07-0.62
C73.33,38.23,73.38,38.06,73.38,37.88z M76.21,48.65c-0.8,0.02-1.59,0.03-2.39,0.02c-0.52-0.01-3.97-0.05-5.17-0.06
c-0.51,0-1.01-0.06-1.61-0.1c0.56-0.39,1.09-0.67,1.49-1.07c0.39-0.4,0.73-0.89,0.91-1.49c0.41-0.06,0.84-0.17,1.31-0.08
c-0.11,0.45-0.24,0.82-0.68,0.99c-0.15,0.06-0.3,0.21-0.36,0.35c-0.12,0.31-0.38,0.46-0.61,0.66c-0.1,0.09-0.19,0.2-0.29,0.31
c0.17,0.13,0.28,0.08,0.4,0c0.73-0.51,1.34-1.13,1.79-1.9c0.13-0.22,0.28-0.33,0.52-0.34c0.27-0.02,0.54-0.06,0.82-0.1
c-0.13,0.68-0.45,1.24-0.93,1.71c-0.14,0.14-0.19,0.23-0.06,0.4c0.44-0.1,0.73-0.39,0.98-0.72c0.1-0.14,0.24-0.25,0.32-0.4
c0.06-0.1,0.12-0.24,0.1-0.35c-0.04-0.27-0.06-0.27,0.1-0.58c0.53,0,1.05,0.01,1.58,0c0.57-0.01,1.13-0.04,1.7-0.05
c0.08,0,0.17,0.03,0.25,0.05c-0.06,0.13-0.11,0.25-0.04,0.41c0.18,0.43,0.13,0.88,0.12,1.33c-0.01,0.25-0.04,0.5,0.15,0.71
c0.03,0.04,0.02,0.13,0.03,0.22C76.49,48.6,76.35,48.65,76.21,48.65z M37.01,93.47c-0.14,0.67-0.15,1.36-0.2,2.04
c0,0.04-0.01,0.08-0.02,0.12c-0.67,0-1.32,0-2,0c0-0.19-0.02-0.4,0-0.62c0.13-1.15,0.23-2.31,0.4-3.46
c0.12-0.79,0.32-1.57,0.49-2.36c0.04-0.18,0.06-0.36,0.1-0.54c0.19-0.87,0.35-1.76,0.6-2.62c0.25-0.88,0.59-1.75,0.89-2.62
c0.18-0.53,0.35-1.07,0.56-1.59c0.32-0.78,0.68-1.54,1.03-2.3c0.28-0.61,0.57-1.21,0.88-1.81c0.22-0.43,1.18-2.44,1.29-2.65
c1.48-2.85,4.81-7.4,5.22-7.84c1.2-1.86,4.38-4.84,4.8-5.28c0.2-0.21,4.78-4.13,5.35-4.66c0.66-0.6,1.33-1.2,2.06-1.72
c0.58-0.41,1.11-0.89,1.67-1.33c0.29-0.23,0.59-0.44,0.89-0.65c0.09-0.06,0.2-0.08,0.31-0.11c0.01,0.03,0.03,0.05,0.04,0.08
c-0.26,0.21-0.53,0.42-0.79,0.64c-1.14,0.93-2.32,1.83-3.41,2.82c-0.8,0.73-1.63,1.41-2.41,2.15c-0.58,0.54-1.89,1.93-2,2.06
c-0.94,1.06-3.29,3.75-3.51,4.05c-0.37,0.51-0.77,1.01-1.17,1.49c-0.28,0.34-1.16,1.44-1.31,1.67c-0.43,0.65-0.85,1.3-1.28,1.94
c-0.5,0.74-1.46,2.37-1.48,2.41c-0.17,0.31-0.33,0.62-0.5,0.93c-0.36,0.66-0.73,1.31-1.07,1.97c-0.26,0.5-0.51,1.01-0.76,1.52
c-0.31,0.62-0.64,1.23-0.93,1.86c-0.23,0.5-0.41,1.02-0.62,1.53c-0.14,0.34-0.29,0.67-0.43,1.01c-0.04,0.08-0.08,0.16-0.11,0.25
c-0.39,1.16-1.25,3.96-1.31,4.2c-0.07,0.29-0.14,0.58-0.21,0.87c-0.03,0.13-0.54,2.56-0.59,2.94
C37.36,90.77,37.07,93.17,37.01,93.47z M42.41,92.75c-0.16,0.61-0.25,2.7-0.25,3.16c-1.44-0.09-3-0.26-4.39-0.34
c-0.12-0.88,0.21-4.01,0.27-4.43c0.09-0.58,0.39-2.07,0.52-2.65c0.17-0.76,0.51-2.2,1-4.26c0.69-2.61,3.05-7.51,3.45-8.34
c0.26-0.54,0.57-1.06,0.86-1.59c0.06-0.12,0.11-0.24,0.18-0.35c0.12-0.2,0.25-0.39,0.38-0.59c0.14-0.21,0.3-0.4,0.42-0.62
c0.47-0.9,1-1.76,1.57-2.59c0.44-0.64,0.81-1.32,1.22-1.98c0.1-0.17,0.23-0.32,0.35-0.48c0.31-0.4,0.62-0.81,0.95-1.2
c0.35-0.41,0.7-0.82,1.07-1.22c0.42-0.46,0.88-0.87,1.25-1.36c0.27-0.35,0.53-0.71,0.84-1.03c0.57-0.59,1.11-1.19,1.72-1.73
c0.35-0.31,0.63-0.69,0.99-0.99c0.31-0.26,0.58-0.57,0.88-0.84c0.44-0.4,0.88-0.79,1.33-1.18c0.67-0.59,1.34-1.19,2.01-1.78
c0.23-0.2,0.46-0.4,0.68-0.6c0.42-0.38,0.85-0.74,1.37-0.98c0.13-0.06,0.22-0.18,0.34-0.26c0.59-0.4,1.18-0.79,1.77-1.19
c0.23-0.15,0.47-0.29,0.67-0.47c0.1-0.09,0.12-0.26,0.2-0.44c0.54-0.23,1.01-0.73,1.77-0.9c-0.32,0.29-0.56,0.54-0.82,0.75
c-0.72,0.59-1.41,1.23-2.09,1.87c-0.55,0.53-1.13,1.02-1.67,1.57c-0.72,0.73-1.41,1.5-2.11,2.24c-0.5,0.54-1.02,1.06-1.52,1.6
c-0.35,0.38-0.68,0.77-1.01,1.16c-0.76,0.89-1.53,1.78-2.27,2.69c-0.5,0.61-0.95,1.26-1.42,1.89c-0.17,0.23-0.37,0.44-0.54,0.67
c-0.23,0.3-0.44,0.61-0.67,0.92c-0.22,0.3-0.45,0.58-0.65,0.88c-0.36,0.58-0.71,1.17-1.05,1.76c-0.14,0.24-0.27,0.48-0.39,0.73
c-0.19,0.38-0.38,0.77-0.57,1.15c-0.12,0.23-0.24,0.46-0.37,0.69c-0.3,0.57-0.61,1.13-0.92,1.7c-0.13,0.23-0.28,0.45-0.38,0.69
c-0.23,0.51-0.45,1.04-0.67,1.55c-0.16,0.37-0.35,0.74-0.49,1.12c-0.16,0.44-0.28,0.91-0.44,1.35c-0.19,0.53-0.42,1.04-0.63,1.57
c-0.06,0.15-0.29,0.31-0.02,0.48c0.01,0.01-0.02,0.1-0.04,0.15c-0.33,0.95-0.66,1.89-0.98,2.84c-0.29,0.86-0.46,1.75-0.62,2.65
c-0.1,0.58-0.3,1.15-0.47,1.71c-0.12,0.42-0.49,3.92-0.5,3.98C42.5,92.18,42.48,92.48,42.41,92.75z M48.47,83.35
c-0.06-0.23-0.06-0.22,0.18-0.66C48.59,82.89,48.54,83.1,48.47,83.35z M51.59,73.28c-0.11,0.21-0.21,0.43-0.32,0.64
c-0.1,0.2-0.21,0.39-0.32,0.59c0.13-0.67,0.5-1.23,0.81-1.89C51.77,72.94,51.77,72.93,51.59,73.28z M61.6,57.9
c0.27-0.46,0.64-0.83,1.03-1.19C62.45,57.24,61.99,57.54,61.6,57.9z M66.67,52.56c-0.94,1.06-2.04,1.95-3.1,2.88
c-0.81,0.71-1.55,1.47-2.2,2.31c-0.36,0.47-0.68,0.97-1.02,1.45c-0.15,0.2-0.33,0.37-0.49,0.57c-0.05,0.06-0.08,0.15-0.11,0.23
c0.05,0.01,0.09,0.01,0.17,0.02c-0.03,0.07-0.04,0.13-0.08,0.17c-0.61,0.68-1.04,1.48-1.55,2.23c-0.46,0.67-0.97,1.31-1.44,1.96
c-0.13,0.18-0.25,0.37-0.36,0.57c-0.26,0.49-0.57,0.94-0.93,1.37c-0.29,0.35-0.53,0.75-0.79,1.13c-0.54,0.79-1.11,1.56-1.62,2.38
c-0.77,1.24-1.55,2.48-2.11,3.84c-0.17,0.41-0.33,0.8-0.37,1.24c-0.04,0.52-0.39,0.92-0.55,1.39c-0.23,0.66-0.47,1.31-0.7,1.96
c-0.08,0.24-0.17,0.47-0.24,0.71c-0.33,1.15-0.71,2.29-0.98,3.46c-0.24,1.02-0.4,2.07-0.56,3.11c-0.14,0.91-0.22,1.83-0.38,2.74
c-0.1,0.62-0.43,2.58-0.44,2.67c-0.03,0.46-0.2,2.1-0.22,2.47c-0.03,0.54-0.06,1.08-0.08,1.61c-0.01,0.27,0,0.55,0,0.83
c-0.41,0.14-2.74,0.19-3.81,0.08c0-0.25,0.01-0.49,0-0.74c-0.02-0.65,0.1-1.28,0.21-1.91c0.08-0.48,0.03-0.98,0.1-1.46
c0.09-0.62,0.23-1.24,0.34-1.86c0.09-0.52,0.16-1.04,0.26-1.56c0.06-0.34,0.16-0.66,0.24-1c0.11-0.5,0.21-1,0.31-1.51
c0.08-0.39,0.17-0.77,0.24-1.16c0.09-0.46,0.16-0.93,0.25-1.39c0.01-0.06,0.03-0.13,0.05-0.19c0.16-0.43,0.31-0.86,0.47-1.29
c0.14-0.38,0.27-0.77,0.41-1.15c0.3-0.8,0.58-1.61,0.9-2.4c0.13-0.34,0.33-0.65,0.52-1.04c-0.11,0.05-0.17,0.08-0.29,0.14
c0.05-0.14,0.06-0.25,0.11-0.34c0.43-0.89,0.86-1.78,1.31-2.66c0.21-0.41,0.46-0.8,0.67-1.2c0.24-0.45,0.45-0.92,0.71-1.36
c0.67-1.13,1.33-2.27,2.05-3.37c0.63-0.96,1.34-1.86,2.03-2.78c0.67-0.89,1.34-1.79,2.05-2.65c0.88-1.07,1.79-2.11,2.69-3.16
c0.2-0.23,0.44-0.41,0.66-0.62c0.05-0.04,0.13-0.08,0.14-0.13c0.08-0.41,0.44-0.57,0.69-0.83c0.47-0.48,0.97-0.93,1.41-1.43
c0.9-1.03,1.83-2.04,2.95-2.84c0.39-0.28,0.81-0.53,1.2-0.82c0.27-0.2,0.52-0.44,0.77-0.67c0.18-0.16,0.36-0.29,0.67-0.21
C66.76,52.45,66.72,52.51,66.67,52.56z M66.72,52.04c0.29-0.18,0.58-0.37,0.87-0.55c0.02,0.03,0.03,0.06,0.05,0.08
C67.38,51.82,67.19,52.15,66.72,52.04z M65.2,58.25c0.02-0.23,0.12-0.41,0.31-0.54C65.49,57.94,65.34,58.09,65.2,58.25z
M65.55,57.66c0.14-0.27,0.43-0.72,0.45-0.7C65.94,57.24,65.77,57.47,65.55,57.66z M66.19,56.55c0.08-0.11,0.16-0.23,0.24-0.34
C66.46,56.39,66.35,56.49,66.19,56.55z M70.23,51.47c-0.29,0.32-0.62,0.61-0.89,0.95c-0.56,0.73-1.1,1.48-1.64,2.22
c-0.32,0.44-0.64,0.87-0.96,1.31c0.47-1.13,3.09-4.52,3.67-4.74C70.35,51.29,70.3,51.39,70.23,51.47z M59.1,81.62
c-0.03-0.01-0.06-0.01-0.09-0.02c0.04-0.18,0.07-0.36,0.11-0.54c0.03,0.01,0.07,0.02,0.1,0.03C59.18,81.26,59.14,81.44,59.1,81.62z
M59.27,80.66c-0.02,0-0.04-0.01-0.06-0.01c0.01-0.07,0.02-0.14,0.03-0.21c0.03,0.01,0.05,0.01,0.08,0.02
C59.3,80.52,59.28,80.59,59.27,80.66z M59.33,80.19c-0.02,0-0.03,0-0.05-0.01c0.01-0.09,0.01-0.18,0.02-0.27
c0.03,0,0.05,0.01,0.08,0.01C59.37,80.02,59.35,80.11,59.33,80.19z M65.87,81.69c0.02-0.1,0.04-0.2,0.06-0.3
c0.02,0,0.05,0.01,0.07,0.01C66,81.5,65.91,81.69,65.87,81.69z M71.12,76.9c-0.03,0-0.07,0-0.1,0c0.01-0.12,0.01-0.25,0.02-0.37
c0.03,0,0.05,0,0.08,0C71.12,76.65,71.12,76.77,71.12,76.9z M73.15,62.99c-0.13-0.31,0.17-1.66,0.42-1.86
C73.43,61.75,73.29,62.37,73.15,62.99z M74.11,58.4c-0.14,0.74-0.29,1.47-0.44,2.21c-0.02,0.1-0.06,0.19-0.12,0.34
c-0.06-0.09,0.32-2.14,0.53-3.13c0.01-0.04,0.05-0.07,0.08-0.11C74.15,57.94,74.15,58.17,74.11,58.4z M74.17,57.7
c-0.02-0.39,0.14-0.73,0.28-1.08C74.37,56.98,74.47,57.39,74.17,57.7z M81.01,69.6c0.01,0.07,0.02,0.14,0.03,0.21
c0,0.01-0.01,0.03-0.02,0.03c-0.01,0-0.02-0.01-0.05-0.02c-0.01-0.06-0.03-0.13-0.05-0.2C80.95,69.61,80.98,69.61,81.01,69.6z
M80.97,69.06c-0.2-0.4-0.16-0.82-0.21-1.24c0.04-0.01,0.07-0.01,0.11-0.02C80.9,68.22,80.93,68.64,80.97,69.06z M80.77,67.26
c0.02,0.1,0.04,0.21,0.06,0.31c-0.03,0.01-0.06,0.01-0.08,0.02c-0.03-0.11-0.05-0.22-0.08-0.34C80.7,67.25,80.74,67.26,80.77,67.26
z M85.06,69.33c0.01,0,0.02,0,0.03,0c0.01,0.08,0.02,0.15,0.03,0.23c-0.02,0-0.03,0-0.05,0C85.06,69.49,85.06,69.41,85.06,69.33z
M79.18,49.68c0.03-0.14-0.09-0.44-0.06-0.57c0.32,0,0.62-0.05,1.09,0.17c0.02,0.17,0.01,0.14,0.03,0.31
C79.88,49.53,79.54,49.83,79.18,49.68z M80.84,50.72c0.25,0.02,0.25,0.02,0.31,0.35C81.06,50.96,80.96,50.85,80.84,50.72z
M88.63,57.84c-0.29-0.15-0.35-0.25-0.49-0.69C88.29,57.37,88.44,57.58,88.63,57.84z M87.78,56.63c0.06,0.1,0.15,0.32,0.13,0.34
C87.84,56.88,87.75,56.66,87.78,56.63z M84.87,52.11c-0.16-0.15-0.32-0.29-0.48-0.44C84.7,51.65,84.9,52.09,84.87,52.11z"/>
<path class="st0" d="M25.56,105.87c0.2,0.01,0.25,0.16,0.35,0.28c0.14,0.19,0.31,0.29,0.57,0.26c0.92-0.09,6.82-0.11,7.38-0.13
c0.21-0.01,2.94-0.11,2.94-0.08c-0.44,0.02,5.63,0.05,6.54,0.06c0.21,0,2.65,0.28,2.86,0.26c0,0,0,0,0,0
c-0.05-0.01-0.25-0.05-0.9-0.24c0.13-0.03,0.19-0.06,0.25-0.05c0.79,0.1,20.85-0.11,22.37-0.1c0.75,0,2.98,0.01,3.15,0.03
c0.16,0.02,9.08-0.31,9.95-0.21c0.55,0.06,1.1,0.11,1.65,0.16c0.65,0.06,1.31,0.1,1.96,0.15c0.02,0,0.05-0.01,0.09-0.02
c0.02-0.05,0.04-0.11,0.08-0.21c0.65,0,1.33-0.01,2,0c0.65,0.01,1.29,0.05,1.94,0.07c0.25,0.01,0.5,0.01,0.74-0.01
c0.25-0.02,0.5-0.05,0.76-0.04c0.82,0.04,1.65,0.04,2.48,0.05c0.13,0,0.26-0.01,0.4-0.02c0.43-0.01,0.87-0.07,1.3-0.01
c0.78,0.1,1.55,0.04,2.33,0c0.76-0.04,1.53-0.09,2.29-0.11c0.89-0.02,1.78,0,2.66-0.01c0.72,0,1.44-0.01,2.16-0.02
c0.08,0,0.16,0,0.23-0.03c0.4-0.17,0.4-0.18,0.67-0.16c0.06,0,0.12-0.01,0.18-0.02c-0.01-0.03-0.02-0.05-0.02-0.05
c-1.16-0.06-2.31-0.15-3.47-0.17c-1.44-0.03-2.87,0-4.31-0.01c-0.35,0-0.71-0.05-1.06-0.07c-0.26-0.01-0.53,0-0.79-0.01
c-0.72-0.04-1.45-0.1-2.17-0.12c-1.45-0.04-2.9-0.06-4.35-0.1c-0.41-0.01-0.82-0.04-1.22-0.05c-1.5-0.04-3-0.09-4.51-0.1
c-1.12,0-2.24,0.1-3.36,0.1c-1.49,0.01-2.98-0.03-4.47-0.03c-1.41,0-2.82,0.03-4.23,0.04c-0.47,0-0.94,0-1.42,0
c0.02-0.25,0.07-0.48,0.07-0.72c0.02-1.19,0.04-2.38,0.03-3.56c0-0.39-0.04-0.8-0.13-1.18c-0.06-0.25-0.24-0.47-0.18-0.8
c0.54-0.02,1.06-0.06,1.59-0.05c1.07,0.02,10.22-0.03,10.28-0.03c1.38,0.05,2.77-0.17,4.15,0.01c0.07,0.01,0.19,0,0.2-0.04
c0.08-0.2,0.25-0.11,0.38-0.12c0.14-0.01,0.29,0.03,0.43,0.02c0.51-0.04,1.02-0.09,1.52-0.14c0.03,0,0.06-0.04,0.11-0.07
c-0.09-0.18-0.19-0.26-0.38-0.22c-0.2,0.04-0.41,0.08-0.62,0.08c-2.67-0.02-5.34-0.04-8-0.06c-2.52-0.02-10.01,0.1-11,0.14
c-0.83,0.03-1.66,0-2.49-0.02c-0.88-0.03-9.45,0.04-9.77,0.03c-0.47-0.02-6.98,0.09-8.01,0.08c-0.87,0-6.47-0.14-7.47-0.1
c-0.72,0.03-2.75-0.1-3.47-0.28c-0.1-0.03-0.26-0.02-0.33,0.04c-0.17,0.16-0.36,0.15-0.55,0.14c-0.28-0.01-0.55,0-0.83-0.01
c-0.43-0.01-0.87,0-1.3-0.04c-0.58-0.06-0.59-0.07-0.55,0.54c0.5,0.05,0.57,0.12,0.69,0.63c0.02,0.1,0.06,0.21,0.06,0.31
c0.04,0.86,0.1,1.71,0.12,2.57c0.01,0.84-0.03,1.69-0.05,2.53c-0.01,0.22-0.03,0.44-0.05,0.65c-0.21,0-0.38,0-0.55,0
c-0.71,0-1.43,0-2.14-0.02c-0.63-0.02-2.97-0.1-3.51-0.1c-0.39,0-0.78-0.03-1.18-0.05c-0.08,0-0.17-0.06-0.22-0.03
c-0.24,0.13-0.49,0.16-0.76,0.15c-0.08,0-0.2,0.12-0.23,0.21C25.38,105.71,25.5,105.87,25.56,105.87z M68.77,98.95
c0.06,0.17,0.14,0.33,0.18,0.5c0.03,0.15,0.03,0.31,0.03,0.46c0,0.37,0,0.73,0,1.1c-0.01,0.97,0.11,1.94-0.06,2.91
c-0.01,0.07-0.03,0.17,0,0.22c0.18,0.32,0.08,0.67,0.11,1.05c-1.35,0.13-2.69,0.07-4.07,0.16c0-0.23-0.01-0.41,0-0.59
c0.05-0.85,0.1-1.71,0.15-2.56c0.03-0.5,0.07-1,0.09-1.49c0.01-0.13,0.02-0.28-0.04-0.39c-0.21-0.4-0.13-0.85-0.28-1.28
C66.21,98.9,67.5,99.01,68.77,98.95z M57.87,104.08c0.08-1.08,0.15-2.16,0.13-3.24c-0.01-0.53-0.06-1.05-0.09-1.62
c1.1-0.04,2.18-0.15,3.26-0.1c1.08,0.05,2.14-0.14,3.27-0.1c-0.03,0.51-0.05,0.95-0.07,1.4c-0.06,0.95-0.19,1.9,0.04,2.85
c0.01,0.04,0.01,0.08,0.01,0.12c0.02,0.63,0.04,1.26,0.06,1.92c-2.16,0.15-4.33-0.05-6.51,0.02
C57.8,104.91,57.84,104.48,57.87,104.08z M57.5,104.89c0.18,0.18,0.2,0.3,0.11,0.46C57.4,105.22,57.49,105.09,57.5,104.89z
M53.51,102.12c0-0.42,0.05-0.84,0.02-1.26c-0.04-0.46,0.06-0.91,0.02-1.37c-0.01-0.09,0.03-0.18,0.05-0.31
c0.25,0.03,0.92-0.12,1.06-0.12c0.75,0.02,1.5,0.04,2.24,0.06c0.34,1.04,0.22,2.1,0.13,3.16c-0.04,0.55-0.09,1.11-0.05,1.66
c0.03,0.41,0.04,0.85,0.33,1.2c0.02,0.03,0.04,0.07,0.05,0.11c0.01,0.02,0,0.05-0.01,0.07c-0.58-0.02-3.16,0.05-3.54,0.05
c-0.25,0-0.32-0.08-0.32-0.34C53.5,104.06,53.5,103.09,53.51,102.12z M47.58,99.17c1.74-0.17,3.4-0.03,5.01-0.01
c0.27,1.94,0.23,3.85,0.35,5.77c0.2,0.12,0.2,0.12,0.18,0.47c-0.14,0.01-0.28,0.02-0.42,0.03c-1.62,0.03-3.23,0.25-4.86,0.14
c-0.1-0.01-0.2-0.05-0.34-0.09C47.41,103.38,47.58,101.29,47.58,99.17z M47.13,105.08c0.13,0.19,0.14,0.31,0.03,0.45
C46.99,105.4,47.07,105.28,47.13,105.08z M42.65,104.74c-0.01-0.06-0.02-0.15,0.01-0.19c0.25-0.27,0.19-0.62,0.25-0.94
c0.03-0.14,0.03-0.29,0.03-0.43c0.01-0.19,0-0.39-0.06-0.59c-0.03,0.32-0.07,0.63-0.1,0.95c-0.03,0-0.05,0-0.08,0.01
c-0.01-0.05-0.04-0.1-0.04-0.15c0.03-0.62,0.06-1.24,0.1-1.86c0.04-0.52,0.1-1.05,0.14-1.57c0.02-0.3,0-0.6,0-0.94
c1.32,0.04,2.6,0.08,3.86,0.11c-0.03,0.58-0.08,1.13-0.09,1.68c-0.01,0.54,0.02,1.08,0.03,1.62c0,0.16-0.02,0.31-0.02,0.47
c0.01,0.84,0.03,1.69,0.05,2.56c-1.13,0.13-3.98,0.11-4.34-0.05c0.13-0.07,0.23-0.12,0.34-0.17
C42.71,105.08,42.68,104.91,42.65,104.74z M37.42,104.01c0.03-0.8,0.09-1.6,0.11-2.41c0.01-0.4,0.03-0.79,0.18-1.16
c0.04-0.1,0.07-0.19-0.04-0.28c-0.04-0.03-0.05-0.12-0.05-0.18c0-0.35,0-0.71,0-1.1c1.5,0.02,2.95,0.06,4.46,0.24
c0.06,0.81,0.14,1.6,0.16,2.39c0.02,0.58-0.04,1.16-0.05,1.73c-0.01,0.43,0,0.87,0.01,1.3c0,0.12,0.04,0.24,0.08,0.34
c0.08,0.19,0.09,0.38-0.01,0.57c-0.25,0.08-3.47,0.02-4.86-0.09C37.42,104.92,37.41,104.46,37.42,104.01z M34.42,105.29
c0.31-0.18,0.29-0.17,0.28-0.51c-0.02-0.68-0.01-1.37-0.02-2.05c-0.01-0.74-0.01-1.48-0.01-2.22c0-0.3,0.01-0.61,0.01-0.91
c0-0.2-0.02-0.4-0.03-0.64c0.67,0,1.37,0,2.07,0c-0.02,1.16,0.13,2.29,0.14,3.43c0.01,0.54-0.05,1.08-0.06,1.62
c-0.01,0.46,0,0.91,0,1.43c-0.84,0.02-1.66,0.05-2.57,0.07C34.33,105.39,34.36,105.32,34.42,105.29z"/>
<path class="st0" d="M69.48,109.95c0.3,0,0.61-0.02,0.91-0.01c0.3,0.01,8.9-0.12,9.77-0.12c0.83,0,3.33-0.12,3.31-0.17
c0.63,0,1.25,0,1.91,0c-0.15-0.16-0.44-0.1-0.45-0.37c0-0.02-0.09-0.05-0.14-0.05c-0.32,0.02-0.61-0.08-0.88-0.24
c-0.1-0.06-1.32-0.09-1.44-0.09c-0.35-0.02-6.27,0.18-6.37,0.13c-0.39-0.17-15.17,0.27-15.24,0.27c-0.26,0.01-0.53,0.02-0.79,0.02
c-1.89,0-5.86-0.02-5.96-0.03c-0.03-0.2-0.11-0.52-0.13-0.52c-0.02,0.16-0.04,0.33-0.07,0.52c-0.27,0.26-0.57,0.17-1.07,0.19
c-0.82,0.03-2.09-0.1-2.91-0.08c-1,0.02-1.99-0.03-2.99,0.01c-1.59,0.06-3.19-0.01-4.79-0.02c-0.62-0.01-12.52-0.01-12.7-0.03
c-0.58-0.08-1.16-0.07-1.73,0.04c-0.11,0.02-0.26,0.09-0.31,0.19c-0.08,0.14,0,0.28,0.17,0.34c0.02,0.01,0.03,0.04,0.05,0.06
c-0.12,0.27-0.12,0.27,0,0.57c0.2,0,0.4,0,0.63,0c0.03,0.23,0.06,0.44,0.07,0.65c0.03,0.67,0.13,4.09,0.15,5.13
c0.02,0.95,0.21,4.76,0.17,5.43c-0.02,0.42-0.04,0.84-0.34,1.19c-0.07,0.08-0.07,0.21-0.11,0.35c0.61-0.05,0.52-0.6,0.78-0.88
c-0.04,0.45-0.09,0.91-0.14,1.42c0.05,0.03,0.13,0.05,0.18,0.1c0.11,0.11,0.48-0.15,0.5-0.27c0.02-0.17-0.13-2.33-0.13-2.62
c0-0.38-0.06-0.77-0.02-1.15c0.13-1.13,0.09-2.27,0.14-3.4c0.08-1.82,0.18-3.64,0.27-5.46c0.01-0.19,0.05-0.39,0.07-0.61
c0.13-0.01,0.25-0.03,0.36-0.02c0.51,0.02,6.93-0.05,7.76,0c0.34,0.02,1-0.02,1.37,0c0.03,0.44,0.26,1.08,0.05,1.5
c-0.05,0.11-0.01,1.12,0.01,1.49c0.02,0.46,0.09,3.16,0.06,3.83c-0.04,1.37-0.1,2.74-0.16,4.11c-0.03,0.7,0.35,2.18,0.53,2.24
c0.06-0.14,0.15-0.26,0.17-0.4c0.06-0.44,0.12-0.89,0.13-1.34c0.02-1.41,0.03-2.82,0.03-4.23c0-1.77-0.01-3.54-0.03-5.3
c0-0.21-0.04-0.41-0.07-0.6c-0.13-0.04-0.23-0.07-0.33-0.1c0-0.32,0-0.62,0-0.95c0.42,0,0.82,0,1.22,0
c-0.37-0.17-0.79-0.03-1.14-0.26c0.68,0,1.35,0,2.05,0c0.02,0.15,0.06,0.28,0.06,0.41c0.01,0.61,0.01,1.21,0.01,1.82
c0,0.32,0.04,0.64-0.01,0.95c-0.1,0.64-0.09,1.28-0.08,1.93c0.02,0.82-0.02,1.63-0.05,2.45c-0.03,0.75-0.07,1.5-0.11,2.25
c-0.02,0.4-0.05,0.79-0.06,1.19c-0.01,0.43,0.01,0.87,0.02,1.3c0.01,0.28,0.13,1.13,0.32,1.1c0.28-0.33,0.04-0.74,0.22-1.16
c0.04,0.22,0.07,0.36,0.1,0.52c0.13-0.12,0.32-10.71,0.11-10.97c-0.02-0.03-0.01-0.08-0.01-0.12c-0.11-0.59,0.13-1.14,0.2-1.73
c0.28,0,6.86-0.03,7.09-0.03c0.76,0.01,1.53,0.03,2.29,0.04c0.63,0.01,1.26,0,1.93,0c-0.02,0.53-0.04,1.04-0.06,1.58
c-0.49-0.23-0.58-0.18-0.63,0.31c-0.04,0.43-0.14,0.86-0.09,1.29c0.01,0.13,0.02,0.26,0.02,0.39c0,0.87-0.01,1.74-0.01,2.6
c0,0.3,0,0.6,0,0.91c-0.01,1-0.03,2-0.02,3c0,0.45,0.05,0.89,0.06,1.34c0.01,0.28,0.58,1.94,0.7,1.98c0.12-0.11,0.24-1.2,0.25-1.66
c0.02-1.31,0.09-2.61,0.1-3.92c0.01-1.49-0.02-2.98-0.05-4.47c-0.01-0.7-0.08-1.39-0.1-2.09c-0.01-0.42,0.01-0.83,0.02-1.31
c0.59,0,7.36-0.22,7.92-0.24c0.85-0.03,1.71-0.11,2.56-0.11c0.92,0,3.89,0.11,4.47,0.11c0,0.2,0,0.36,0,0.55
c-0.08,0-0.15-0.01-0.21,0c-0.3,0.05-0.35,0.11-0.34,0.42c0,0.03,0.01,0.05,0,0.08c-0.02,0.4,0.16,0.84-0.18,1.2
c-0.03,0.03-0.02,0.1-0.01,0.15c0.01,0.1,0.07,6.5,0.08,6.79c0.03,1.01,0.06,2.03,0.1,3.04c0.02,0.57,0.06,1.13,0.1,1.69
c0.01,0.11,0.07,0.22,0.15,0.31c0.12,0.14,0.23,0.12,0.29-0.04c0.05-0.12,0.07-0.25,0.12-0.41c-0.1,0.03-0.16,0.05-0.23,0.07
c-0.09-0.37-0.08-0.36,0.09-0.67c0.1-0.18,0.23-2.42,0.19-2.44c-0.2-0.15-0.14-1.02-0.14-1.25c0.02-1.65,0.24-7.9,0.16-9.37
C69.25,110.02,69.32,109.95,69.48,109.95z M29.47,112.76c-0.02,0-0.03,0-0.05,0.01c-0.01-0.06-0.03-0.12-0.03-0.19
c0.01-0.48,0.04-0.97,0.04-1.45c0-0.1-0.04-0.2-0.06-0.3c-0.03-0.11-0.07-0.21-0.14-0.38c0.19,0.02,0.31,0.04,0.45,0.06
C29.61,111.28,29.54,112.02,29.47,112.76z M42.11,120.74c-0.01,0-0.02,0-0.03,0c0-0.1,0-0.19,0-0.29c0.01,0,0.02,0,0.03,0
C42.11,120.55,42.11,120.65,42.11,120.74z M42.27,111.08c-0.16-0.22-0.15-0.45-0.16-0.71c0.12,0,0.2,0,0.33,0
C42.38,110.62,42.32,110.85,42.27,111.08z"/>
<path class="st0" d="M107.71,125.36c-0.01-0.23-0.02-0.43-0.04-0.64c-0.03-0.3-0.1-0.36-0.41-0.34c-1.22,0.08-2.45,0.16-3.67,0.24
c-0.83,0.05-4.02,0.15-4.78,0.17c-0.26,0.01-9.43,0.02-9.81,0.03c-0.79,0.03-1.58,0.11-2.37,0.11c-1.3,0-2.61-0.05-3.91-0.08
c-0.45-0.01-0.9,0-1.35-0.02c-1.62-0.05-11.08,0.02-11.54,0.03c-1.27,0.02-4.37,0.19-4.64,0.19c-0.76,0-1.52-0.02-2.29,0.01
c-1.11,0.05-2.21,0.05-3.32,0.09c-0.19,0.01-0.36,0.01-0.52-0.12c-0.07-0.06-0.66-0.06-0.84-0.05c-0.76,0.05-1.53,0.16-2.29,0.15
c-1.48-0.02-17.35-0.61-18.93-0.65c-1.27-0.03-8.68,0.01-9.73-0.02c-0.78-0.02-3.1-0.01-3.48,0.02c-0.31,0.03-0.62,0.1-0.94,0.13
c-0.25,0.02-0.42,0.11-0.53,0.4c0.28,0.12,2.77,0.3,3.75,0.29c0.96-0.02,1.92-0.04,2.88-0.03c0.8,0.01,1.61,0.05,2.41,0.08
c0.87,0.03,1.74,0.05,2.61,0.08c0.13,0,11.7,0.41,12.82,0.46c0.82,0.04,3.9,0.1,4.62,0.1c0.76,0,1.53,0.01,2.29,0.02
c0.24,0,0.72-0.06,0.72-0.09c-0.28-0.02-0.55-0.03-0.83-0.05c0-0.03,0-0.06,0-0.08c0.46-0.06,0.92-0.02,1.38,0.04
c0.02,0,3.43-0.02,7.29-0.04c0-0.02,0-0.03,0-0.05c0.07,0.01,0.15,0.02,0.22,0.03c-0.05,0.01-0.1,0.01-0.15,0.02
c4.88-0.03,10.43-0.07,10.73-0.07c1.29,0,2.58,0.01,3.88,0.02c0.63,0.01,5.69-0.03,6.23,0c0.79,0.03,2.65,0.01,2.79,0.03
c0.39,0.03,2.88-0.02,3.73-0.04c0.66-0.02,8.65-0.05,10.52-0.08c0.46-0.01,0.92,0,1.39,0.01c0.49,0.01,0.97,0.06,1.46,0.06
c0.8-0.01,1.6-0.05,2.4-0.06c0.74-0.02,1.48-0.01,2.21-0.03c0.28-0.01,0.55-0.08,0.83-0.12c0-0.03-0.01-0.05-0.01-0.08
C108.24,125.4,107.97,125.38,107.71,125.36z M49.63,125.76c-0.01,0-0.03-0.03-0.1-0.09c0.11,0.02,0.16,0.03,0.21,0.04
C49.7,125.73,49.67,125.75,49.63,125.76z"/>
<path class="st0" d="M69.9,31.26c0.2-0.01,0.43-0.09,0.58,0c0.22,0.12,0.4,0.05,0.6,0.02c0.16-0.02,0.31-0.04,0.47-0.04
c0.69-0.02,1.37-0.03,2.06-0.04c1.24-0.02,2.48,0.09,3.72-0.03c0.59-0.06,1.18,0.03,1.78,0.04c0.84,0.02,1.69,0.05,2.53,0.03
c0.47-0.01,0.97,0.02,1.38-0.4c-0.11-0.08-0.21-0.16-0.25-0.19c-0.05-0.16-0.06-0.29-0.13-0.36c-0.16-0.17-0.34-0.34-0.53-0.47
c-0.15-0.1-0.22-0.23-0.24-0.39c-0.03-0.22-0.05-0.44-0.06-0.67c-0.02-0.28-0.1-0.42-0.25-0.44c-0.21-0.02-0.35,0.12-0.39,0.41
c-0.02,0.19,0,0.39,0,0.63c-0.19-0.02-0.39-0.06-0.59-0.04c-0.22,0.02-0.39,0.01-0.45-0.25c-0.14,0.02-0.26,0.03-0.37,0.05
c-0.16-0.37,0.18-0.68,0.07-1.05c-0.26-0.12-0.46-0.02-0.65,0.2c0.04,0.11,0.09,0.21,0.11,0.29c-0.21,0.21-0.4,0.4-0.59,0.58
c-0.2-0.03-0.41-0.05-0.59-0.07c-0.03-0.1-0.04-0.2-0.09-0.27c-0.19-0.28-0.32-0.57-0.3-0.92c0.01-0.21-0.13-0.3-0.34-0.27
c-0.11,0.01-0.22,0.04-0.36,0.06c-0.02-0.14-0.03-0.24-0.04-0.35c-0.06-0.53,0.06-1.09-0.24-1.58c-0.02-0.03-0.01-0.08-0.01-0.12
c0.01-0.41,0.03-0.82,0.03-1.22c0-0.49,0.19-0.96,0.08-1.45c-0.02-0.08,0.04-0.18,0.06-0.25c0.59,0.22,0.65,0.85,1.07,1.2
c-0.05-0.25-0.14-0.47-0.21-0.71c-0.14-0.45-0.21-0.77-0.26-1.23c-0.14-0.82-0.35-1.61-0.37-2.36c-0.01-0.22,0.27-0.35,0.46-0.43
c0.22-0.09,0.86-0.68,0.83-1.58c-0.03-0.84-0.41-1.39-0.91-1.59c-0.87-0.54-1.96,0-2.22,0.3c-0.26,0.23-0.58,0.7-0.54,1.38
c0.13,0.57,0.3,1.03,0.72,1.34c0.09,0.03,0.28,0.2,0.37,0.23c0.19,0.06,0.24,0.21,0.22,0.37c-0.04,0.3-0.1,0.6-0.16,0.89
c-0.06,0.31-0.23,1.36-0.31,1.67c-0.56,2.6-1.76,4.36-2.31,5.2c-0.06,0.1-0.15,0.19-0.25,0.24c-0.15,0.08-0.18,0.19-0.23,0.34
c-0.11,0.39-0.32,0.7-0.75,0.81c-0.02,0.01-0.04,0.03-0.06,0.05c-0.03,0.11-0.06,0.22-0.11,0.39c-0.08-0.1-0.15-0.15-0.15-0.19
c0.01-0.12,0.08-0.23,0.09-0.34c0.03-0.32,0.07-0.65,0.06-0.98c-0.01-0.23-0.28-0.34-0.47-0.24c-0.27,0.14-0.48,0.7-0.4,0.99
c0.06,0.2,0.09,0.4,0.15,0.64c-0.25,0.02-0.44,0.03-0.63,0.05c-0.23,0.03-0.27,0.11-0.22,0.41c0.25-0.11,0.37,0.04,0.48,0.22
c-0.02,0.04-0.03,0.07-0.05,0.08c-0.1,0.08-0.18,0.17-0.1,0.3c0.07,0.12,0.2,0.13,0.32,0.08c0.19-0.08,0.41-0.14,0.57-0.26
c0.23-0.18,0.44-0.2,0.69-0.1c0.09,0.03,0.23,0.06,0.29,0.01c0.3-0.25,0.66-0.24,1.01-0.22c0.46,0.02,0.92,0.07,1.37,0.08
c1.25,0.01,2.5,0.01,3.76-0.01c0.24,0,0.43,0.14,0.64,0.15c0.23,0,0.46-0.12,0.69-0.12c0.44,0.01,0.88,0.07,1.32,0.11
c0.02-0.02,0.21,0.05,0.22,0.05c0.38,0.29,0.84,0.1,0.98,0.75c-0.38,0.03-0.69,0.07-1,0.08c-0.11,0-0.26,0-0.34-0.07
c-0.24-0.2-0.55-0.02-0.81,0c-0.09,0.01-0.18,0.03-0.27,0.03c-1.35,0-2.71,0.01-4.07,0.01c-0.66,0-1.32-0.03-1.98-0.05
c-0.8-0.02-1.61-0.05-2.41-0.08c-0.41-0.01-0.82,0.02-1.22-0.06c-0.32-0.07-0.54,0.05-0.76,0.19c-0.16,0.11-0.16,0.19-0.11,0.39
C69.57,31.31,69.74,31.27,69.9,31.26z M75.86,18.67c-0.32-0.24-0.58-0.76-0.57-1.13c0.03-0.67,0.41-1.1,0.97-1.22
c0.6-0.12,1.24,0.22,1.47,0.79c0.3,0.99-0.23,1.68-0.69,1.71C76.75,18.84,76.18,18.91,75.86,18.67z M76.2,22.28
c0.09-0.37,0.15-0.75,0.22-1.12c0.12,0.2,0.23,1.19,0.19,1.69c-0.29,0.04-0.29,0.04-0.72,0.58C76.01,23,76.12,22.64,76.2,22.28z
M74.61,27.82c-0.06,0.42-0.32,0.83-0.04,1.28c-0.31,0.02-0.54,0.04-0.78,0.05c-0.24,0.01-0.29-0.07-0.23-0.32
c0.02-0.1-0.11-0.3-0.12-0.4c-0.03-0.39,0.18-0.4,0.47-0.61c0.17-0.12,0.46-0.68,0.58-0.86c0.3-0.44,0.58-1.06,0.74-1.56
c0.21-0.66,0.32-0.97,0.55-1.67c0.11,0.27,0.26,0.56,0.25,0.79c-0.01,0.24,0,0.72,0,0.96c0,0.85-0.27,1.46-0.68,2.22
c-0.06-0.01-0.32-0.12-0.38-0.13C74.77,27.52,74.64,27.63,74.61,27.82z M76.22,29.15c-0.33-0.02-0.67,0.07-1.02-0.06
c0.03-0.07,0.05-0.13,0.08-0.15c0.48-0.24,0.61-0.71,0.73-1.17c0.15-0.55,0.26-1.11,0.38-1.67C76.32,27.11,76.55,28.14,76.22,29.15
z M76.55,24.07c-0.17-0.32,0.05-0.61,0.05-0.91C76.65,23.47,76.71,23.78,76.55,24.07z M77.85,29.11c-0.81,0.04-0.53-0.38-0.51-0.65
c0.14,0.05,0.51,0.51,0.65,0.56C77.92,29.07,77.89,29.11,77.85,29.11z"/>
<path class="st0" d="M53.59,108c0.02,0,0.05,0.02,0.07,0.03c0.22,0.03,0.44,0.09,0.66,0.09c0.38-0.01,11.62-0.25,12.26-0.3
c0.26-0.02,0.53,0,0.79-0.02c0.7-0.05,1.38-0.12,2.09-0.06c0.98,0.09,1.97,0.11,2.96,0.15c0.44,0.02,0.88,0,1.32-0.09
c-0.04-0.36-0.19-0.55-0.54-0.52c-0.14,0.01-0.29-0.02-0.43-0.05c-0.1-0.02-0.22-0.05-0.27-0.12c-0.14-0.18-0.31-0.17-0.5-0.15
c-0.64,0.07-3.8,0.31-4.73,0.3c-1.56-0.02-3.11-0.11-4.67-0.12c-1.29-0.01-2.59,0.07-3.88,0.11c-1.05,0.04-12.43,0-13.64,0.01
c-0.44,0-2.73,0.09-3.44,0.07c-1.07-0.03-12.25-0.1-14.17-0.15c-0.07,0-0.15-0.02-0.19,0.02c-0.22,0.2-0.49,0.18-0.75,0.18
c-0.28,0-0.47,0.09-0.62,0.33c-0.06,0.1-0.19,0.19-0.1,0.3c0.06,0.07,0.2,0.11,0.3,0.12c0.79,0.03,1.58,0.06,2.37,0.06
c1.15,0,2.3-0.02,3.44-0.04c0.92-0.02,9-0.21,10.03-0.02c0.04,0.01,2.15-0.01,3.16,0.01c1.95,0.04,3.9,0.09,5.86,0.14
c0.51,0.01,1.03,0.04,1.54,0.05c0.6,0.01,1.19,0,1.79-0.01c-0.15-0.04-0.3-0.09-0.46-0.11c-0.41-0.03-0.81-0.04-1.22-0.07
c-0.06,0-0.13-0.04-0.19-0.06c0-0.03,0.01-0.06,0.01-0.08C52.81,108,53.2,108,53.59,108z M72.09,107.5c0.22,0,0.46-0.06,0.66,0.14
c-0.23-0.01-0.45-0.03-0.68-0.04C72.08,107.57,72.09,107.53,72.09,107.5z"/>
<path class="st0" d="M32.43,97.78c0.07,0.02,0.15,0.04,0.23,0.04c0.63,0.02,8.8-0.05,9.92-0.07c0.51-0.01,2.91-0.02,3.59-0.02
c0.29,0,0.58-0.05,0.87-0.05c1.07,0.01,2.13,0.03,3.2,0.04c2.73,0.01,5.47,0.03,8.2,0.01c2.74-0.02,15.51-0.2,16.56-0.24
c0.37-0.01,0.74,0,1.11,0c1.25-0.01,14,0.08,16.9-0.07c0.92-0.05,3.78-0.04,4.02-0.04c-0.03-0.31-0.19-0.42-0.4-0.44
c-0.31-0.03-0.63-0.03-0.95-0.03c-0.54-0.01-1.08-0.01-1.61-0.01c-0.16,0-0.32-0.01-0.47,0c-0.54,0.03-3.02,0.09-3.57,0.11
c-1.13,0.04-2.27,0.08-3.4,0.08c-9.18,0.01-31.34,0.12-31.84,0.13c-0.71,0.01-1.42,0.02-2.13,0.02c-0.54,0-12.79-0.05-14.62-0.06
c-0.2,0-3.64-0.13-3.83-0.17c-0.61-0.13-1.18,0.08-1.75,0.2c-0.15,0.03-0.25,0.15-0.25,0.32C32.17,97.69,32.3,97.74,32.43,97.78z"
/>
<path class="st0" d="M80.49,83.44c0.03-0.15,0.05-0.3,0.08-0.45c-1.44-1.09-2.89-2.18-4.33-3.27c-0.25,0.17-0.27,0.24-0.12,0.45
c0.06,0.08,0.15,0.14,0.22,0.22c0.1,0.11,0.2,0.22,0.29,0.33c-0.01,0.03-0.03,0.05-0.04,0.08c-0.2-0.18-0.4-0.28-0.64-0.1
c-0.57,0.43-1.15,0.83-1.7,1.29c-0.37,0.31-0.68,0.68-1.02,1.03c-0.21,0.21-0.44,0.42-0.65,0.63c-0.09,0.09-0.2,0.18-0.26,0.3
c-0.04,0.08-0.03,0.2,0.01,0.28c0.03,0.06,0.15,0.1,0.22,0.1c0.13,0,0.25-0.04,0.4-0.08c0.02,0.21,0.03,0.38,0.05,0.57
c0.15-0.02,0.26-0.03,0.33-0.04c0.19,0.21-0.03,0.46,0.17,0.71c0.03-0.15,0.06-0.24,0.06-0.32c0-0.39,0.12-0.75,0.24-1.12
c0.14-0.46,0.45-0.8,0.8-1.11c0.06-0.05,0.14-0.1,0.21-0.1c0.29-0.03,0.58-0.05,0.87-0.07c0.06,0,0.12,0.04,0.27,0.1
c-0.44,0.12-0.7,0.4-1.08,0.45c-0.19,0.03-0.36,0.15-0.53,0.25c-0.06,0.04-0.1,0.15-0.1,0.23c0,0.05,0.1,0.1,0.16,0.14
c0.04,0.02,0.1,0.01,0.16,0c0.6-0.07,1.2-0.16,1.81-0.2c0.35-0.03,0.71,0.03,1.1,0.05c-0.05,0.07-0.06,0.11-0.07,0.12
c-0.4,0.11-2.07,0.36-2.35,0.29c-0.11,0.27-0.05,0.51-0.05,0.76c0,0.42-0.03,1.42,0.05,1.54c0.09,0.13,0.11,0.31,0.17,0.5
c0.27-0.16,1.26-0.3,1.51-0.24c0.13,0.03,0.29,0.02,0.42-0.03c0.25-0.09,1.36-0.09,1.41,0.06c0.37-0.14,0.51-0.42,0.37-0.73
c-0.18-0.4-0.16-1.82-0.2-1.98c-0.13,0.01-0.22,0.02-0.31,0.03c-0.12,0-0.28-0.05-0.37,0.01c-0.09,0.06-0.12,0.22-0.17,0.34
c-0.04,0.08-0.05,0.18-0.1,0.26c-0.03,0.05-0.09,0.07-0.2,0.15c0.08-0.34-0.08-0.43-0.31-0.48c-0.06-0.01-0.1-0.08-0.18-0.15
c0.21-0.06,0.37-0.11,0.54-0.15c0.23-0.05,0.24-0.23,0.21-0.39c0.37-0.03,0.71-0.05,1.04-0.07c0.32,0.23,0.27,0.63,0.45,0.9
c0.4-0.01,0.48-0.1,0.35-0.43c-0.11-0.27-0.25-0.52-0.02-0.85C79.88,83.49,80.19,83.5,80.49,83.44z M75.58,85.07
c-0.02-0.1-0.04-0.21-0.07-0.32C75.8,84.76,75.8,84.77,75.58,85.07z M76.65,84.47c0.15,0.04,0.27,0.06,0.38,0.11
c0.03,0.02,0.07,0.13,0.04,0.16c-0.11,0.18-0.27,0.06-0.42,0.08C76.65,84.7,76.65,84.61,76.65,84.47z M76.72,85.22
c0.08-0.01,0.17-0.01,0.29-0.02c0,0.16,0,0.31,0,0.45C76.73,85.59,76.73,85.59,76.72,85.22z M77.66,85.04
c0.04,0.26,0.06,0.46,0.09,0.67C77.48,85.62,77.45,85.42,77.66,85.04z M77.31,86.23c-0.15,0.05-0.24,0.08-0.33,0.1
c-0.02-0.05-0.05-0.08-0.04-0.09c0.04-0.07,0.09-0.14,0.13-0.2C77.14,86.09,77.2,86.14,77.31,86.23z M78.06,82.3
c-0.22-0.02-0.39-0.03-0.56-0.04C77.72,81.99,77.77,82,78.06,82.3z M76.05,82.08c0.23-0.29,0.4-0.34,0.74-0.24
c0.12,0.04,0.26,0.05,0.38,0.07c0,0.03,0,0.07-0.01,0.1C76.8,82.03,76.43,82.06,76.05,82.08z M78.21,83.01
c-0.18,0.01-0.36,0.03-0.55,0.03c-0.18,0-0.36-0.03-0.54-0.05c0.52-0.1,1.02-0.17,1.6-0.15C78.57,83.1,78.38,83,78.21,83.01z"/>
<path class="st0" d="M56.07,124.61c0.04-0.27,0.08-0.52,0.13-0.77c0.08,0,0.39-0.44,0.46-0.66c0.07-0.21,0.05-1.55,0.07-1.77
c0.11-1.13,0.32-1.13,0.39-2.26c0.06-0.83,0.09-1.66,0.13-2.49c0.01-0.12,0-0.25,0-0.37c-0.03,0-0.06,0-0.1-0.01
c-0.1,0.67-0.2,1.35-0.3,2.02c-0.01,0-0.03,0-0.04,0c-0.01-0.66-0.02-1.31-0.04-1.97c-0.03-0.76-0.07-1.53-0.11-2.29
c-0.02-0.42-0.12-0.85-0.06-1.26c0.05-0.32-0.1-0.59-0.05-0.89c0.01-0.03-0.03-0.09-0.05-0.1c-0.21-0.09-0.17-0.28-0.17-0.44
c0-0.11,0.01-0.21-0.01-0.32c-0.03-0.14-0.23-0.22-0.31-0.11c-0.1,0.14-0.23,0.32-0.22,0.48c0.01,0.87,0.07,1.73,0.1,2.6
c0.02,0.58,0.06,1.16,0.01,1.73c-0.06,0.71-0.08,1.41-0.06,2.12c0.04,1.2,0.03,2.39,0.04,3.59c0.01,0.92,0.03,1.84,0.05,2.76
C55.94,124.35,55.91,124.5,56.07,124.61z M56.51,118.93c0.02,0,0.03,0,0.05,0c0,0.35,0,0.69,0,1.04c-0.02,0-0.03,0-0.05,0
C56.51,119.63,56.51,119.28,56.51,118.93z"/>
<path class="st0" d="M45.67,116.82c-0.27,0.53-0.03,1.06-0.27,1.52c-0.06-0.87-0.09-1.74-0.1-2.6c-0.01-0.87,0.09-1.74-0.02-2.6
c-0.02-0.18,0.05-0.39-0.02-0.54c-0.14-0.32-0.1-0.64-0.1-0.97c0-0.14,0-0.29,0-0.43c0-0.13,0-0.26,0-0.39c-0.03,0-0.06,0-0.08,0
c-0.03,0.15-0.06,0.3-0.09,0.44c-0.22,0.12-0.49,0.21-0.49,0.5c0.01,0.58-0.09,1.15-0.07,1.73c0.05,1.37,0.03,2.74,0.04,4.1
c0.01,0.88,0.04,1.75,0.01,2.63c-0.04,1.08,0.01,2.16,0.12,3.24c0.02,0.17,0.05,0.34,0.22,0.38c0.17,0.03,0.2,0.13,0.23,0.27
c0.02,0.06,0.08,0.1,0.16,0.19c0.05-0.13,0.06-0.24,0.12-0.29c0.29-0.22,0.25-0.54,0.26-0.83c0.01-0.55-0.04-1.1-0.03-1.65
c0.02-1.13,0.08-2.27,0.11-3.4C45.68,117.7,45.67,117.3,45.67,116.82z"/>
<path class="st0" d="M51.8,115.49c0.05-1.46-0.06-2.91-0.09-4.37c0-0.09-0.08-0.22-0.15-0.24c-0.06-0.02-0.16,0.08-0.25,0.12
c-0.09,0.04-0.17,0.08-0.25,0.11c0.26,1.72,0.23,3.43-0.09,5.14c-0.05-1.69-0.09-3.37-0.14-5.06c-0.09,0.18-0.15,0.37-0.15,0.56
c0,0.71,0.01,1.42,0.02,2.13c0.02,0.79,0.04,1.58,0.07,2.37c0.04,0.86,0.12,1.71,0.16,2.57c0.03,0.84,0.03,1.69,0.03,2.53
c0,0.44,0.06,2.54,0.06,2.79c0,0.15,0.09,0.21,0.21,0.25c0.15,0.05,0.58-1.14,0.57-1.37c-0.02-0.91-0.04-1.81-0.03-2.72
c0-0.59,0.06-1.19,0.06-1.78C51.83,117.52,51.76,116.51,51.8,115.49z"/>
<path class="st0" d="M64.66,115.81c-0.03-0.37-0.03-0.73-0.03-1.1c0-0.74,0.02-1.48,0.02-2.22c0-0.44-0.21-0.59-0.66-0.47
c0.03-0.48,0.05-0.95,0.08-1.49c-0.19,0.21-0.25,0.39-0.26,0.59c-0.01,0.54-0.02,1.08-0.04,1.62c-0.01,0.3-0.02,0.6-0.02,0.91
c-0.01,0.59-0.01,1.18-0.01,1.77c0,0.14-0.02,0.29-0.02,0.43c0.01,0.8,0.03,1.61,0.04,2.41c0.01,0.75,0.03,1.5,0.04,2.25
c0.01,0.76,0,1.53-0.01,2.29c0,0.43,0,0.87,0,1.33c0.15,0.05,0.27,0.09,0.39,0.13c0.06,0.01,0.13,0.02,0.18,0
c0.17-0.08,0.3-0.64,0.16-0.74c-0.15-0.1-0.14-0.21-0.13-0.33c0.03-0.46,0.06-0.92,0.11-1.38
C64.77,119.82,64.81,117.82,64.66,115.81z"/>
<path class="st0" d="M33.57,122.74c-0.01-0.22-0.01-0.45-0.05-0.67c-0.13-0.67,0.04-1.33,0.01-2c-0.06-1.14-0.03-2.29-0.02-3.44
c0.01-1.38,0.03-2.77,0.04-4.15c0-0.27-0.02-0.53-0.04-0.82c-0.08,0.05-0.11,0.06-0.17,0.1c-0.03-0.22-0.06-0.41-0.08-0.61
c-0.01-0.13-0.09-0.21-0.2-0.19c-0.09,0.01-0.18,0.09-0.23,0.16c-0.05,0.08-0.03,0.24-0.1,0.28c-0.23,0.15-0.23,0.37-0.23,0.59
c0.02,0.63,0.06,1.26,0.06,1.89c0,1.19-0.02,2.37-0.03,3.56c0,0.6-0.03,1.21,0.11,1.8c0.06,0.25,0.11,0.51,0.13,0.77
c0.04,0.62,0.06,1.24,0.09,1.85c0.02,0.29,0.02,0.58,0.07,0.87c0.05,0.25,0.15,0.5,0.24,0.74c0.02,0.06,0.12,0.09,0.24,0.17
c0.01-0.2-0.03-0.4,0.03-0.44C33.68,123.09,33.58,122.91,33.57,122.74z"/>
<path class="st0" d="M81.35,34.96c0.21-0.23,0.72-0.07,0.72-0.58c0-0.02,0.04-0.05,0.06-0.05c0.25,0,0.19-0.22,0.22-0.35
c0.05-0.18-0.04-0.28-0.23-0.27c-0.24,0.01-0.47,0.03-0.71,0.07c-1.14,0.2-8.62,0.01-9.41,0.03c-0.31,0.01-0.77-0.1-1.28,0.04
c-0.12,0.19-0.06,0.34,0.07,0.41c0.15,0.09,0.33,0.18,0.5,0.19c0.93,0.06,1.86-0.04,2.79,0.08c0.74,0.1,1.5,0.08,2.25,0.09
c1.56,0.01,3.11,0,4.67,0.01c0.12,0,0.25,0,0.41,0c-0.06,0.13-0.09,0.19-0.12,0.26C81.27,34.96,81.3,35.05,81.35,34.96z"/>
<path class="st0" d="M66.68,117.21c0.03-0.89,0.08-1.78,0.07-2.67c-0.01-0.5,0-1,0-1.5c0.01-0.42,0.03-0.84,0.04-1.26
c0.01-0.45,0-0.9,0-1.35c-0.03,0-0.06,0-0.09-0.01c-0.03,0.16-0.05,0.32-0.09,0.52c-0.11,0-0.24,0.02-0.36,0
c-0.24-0.06-0.35-0.02-0.38,0.22c-0.02,0.16,0.01,0.34,0.02,0.51c0.02,0.22,0.11,0.43-0.05,0.64c-0.04,0.06-0.04,0.18-0.01,0.26
c0.19,0.56,0.16,1.14,0.16,1.72c0,0.77-0.04,1.55-0.02,2.32c0.02,1.05,0.11,2.1,0.12,3.16c0.01,1-0.05,2-0.04,2.99
c0,0.23,0.13,0.47,0.25,0.68c0.1,0.17,0.31,0.13,0.36-0.05c0.06-0.2,0.12-0.42,0.09-0.62c-0.09-0.46-0.12-0.92-0.15-1.39
c-0.02-0.41,0.01-0.82,0.01-1.23C66.64,119.19,66.64,118.2,66.68,117.21z"/>
<path class="st0" d="M31.42,115.57c0.01-0.87-0.04-1.73-0.08-2.6c-0.02-0.42-0.08-0.84-0.13-1.25c-0.02-0.14-0.03-0.3-0.23-0.32
c-0.18-0.01-0.36,0.12-0.41,0.31c-0.03,0.11-0.03,0.24-0.02,0.35c0.03,1.13,0.06,2.27,0.08,3.4c0.02,1.15,0.04,2.29,0.06,3.44
c0.03,1.38,0.05,2.77,0.09,4.15c0.01,0.31,0.08,0.61,0.13,0.93c0.35-0.07,0.3-0.31,0.32-0.47c0.14-0.13,0.19-0.6,0.16-0.71
c-0.05-0.19-0.1-0.39-0.1-0.58c-0.01-1.1-0.01-2.19,0.01-3.29C31.33,117.81,31.41,116.69,31.42,115.57z"/>
<path class="st0" d="M74.57,68.98c-0.17,0.34-0.36,0.67-0.51,1.01c-0.14,0.32-0.07,0.65,0.13,0.94c0.05,0.08,0.18,0.17,0.23,0.16
c0.09-0.03,0.19-0.14,0.22-0.23c0.05-0.17,0.06-0.36,0.07-0.55c0.01-0.3,0.07-0.6,0.19-0.87c0.29,0,0.51-0.08,0.65-0.33
c0.06-0.11,0.18-0.19,0.27-0.29c0.12-0.15,0.25-0.23,0.47-0.18c0.26,0.05,0.53,0.03,0.85,0.05c0.03,0.06,0.09,0.17,0.16,0.31
c-0.59,0.05-1.12,0.15-1.61,0.41c0.06,0.28,0.05,0.3,0.18,0.32c0.59,0.11,1.19,0.4,1.81,0.01c0.05,0.18,0.09,0.34,0.14,0.5
c0.28-0.16-0.06-0.58,0.36-0.66c0.12,0.24,0.25,0.48,0.36,0.73c0.08,0.18-0.01,0.4-0.19,0.52c-0.16,0.1-0.3,0.06-0.38-0.11
c-0.04-0.08-0.05-0.18-0.09-0.26c-0.07-0.14-0.2-0.18-0.33-0.09c-0.13,0.09-0.14,0.22-0.07,0.35c0.16,0.27,0.65,0.49,0.88,0.37
c0.31-0.16,0.62-0.48,0.48-0.9c-0.14-0.43-0.35-0.85-0.56-1.25c-0.3-0.55-0.81-0.85-1.37-1.09c0.12-0.32-0.24-0.28-0.33-0.49
c0.06-0.17,0.04-0.32-0.17-0.43c-0.39-0.21-0.76-0.08-0.85,0.36c-0.05,0.23-0.01,0.49-0.01,0.81
C75.2,68.32,74.79,68.54,74.57,68.98z M77.63,69.59c-0.16-0.07-0.28-0.13-0.44-0.2c0.08-0.1,0.14-0.18,0.21-0.27
C77.61,69.21,77.62,69.38,77.63,69.59z"/>
<path class="st0" d="M74.01,32.7c0.82-0.01,1.63-0.04,2.45-0.03c0.69,0,1.37,0.04,2.06,0.07c0.38,0.02,0.76,0.08,1.14,0.11
c0.33,0.03,0.66,0.05,0.99,0.05c0.17,0,0.34-0.09,0.5-0.13c0-0.1,0.01-0.12,0-0.13c-0.03-0.04-0.07-0.08-0.1-0.12
c-0.18-0.3-0.43-0.4-0.79-0.37c-0.56,0.05-1.13,0.04-1.7,0.04c-0.46,0-0.92-0.04-1.38-0.04c-0.72,0-1.45,0.04-2.17,0.04
c-1.19-0.01-2.37-0.05-3.56-0.08c-0.22-0.01-0.42,0.01-0.63,0.16c0.03,0.14-0.13,0.25,0.11,0.48c0.46,0.01,0.9,0.02,1.35,0.01
C72.86,32.73,73.43,32.71,74.01,32.7z"/>
<path class="st0" d="M63.95,83.37c0.1-0.14,0.08-0.25,0.04-0.4c-0.13-0.44-0.51-0.66-0.8-0.95c-0.18-0.18-0.41-0.21-0.66-0.16
c-0.51,0.11-0.98,0.28-1.4,0.61c-0.28,0.22-0.58,0.41-0.86,0.63c-0.15,0.12-0.3,0.27-0.38,0.44c-0.06,0.14-0.04,0.33-0.02,0.49
c0.01,0.11,0.07,0.21,0.13,0.31c0.04,0.06,0.12,0.15,0.16,0.14c0.09-0.03,0.18-0.09,0.23-0.17c0.05-0.07,0.02-0.2,0.07-0.25
c0.3-0.29,0.62-0.57,0.94-0.83c0.11-0.09,0.26-0.13,0.42,0.01c-0.02,0.11-0.03,0.24-0.05,0.36c0.25,0.16,0.47,0.27,0.78,0.21
c0.28-0.05,0.57-0.01,0.86,0c0.14,0,0.28,0.02,0.4,0.04C63.87,83.64,63.87,83.48,63.95,83.37z M61.99,82.88
c0.26-0.05,0.45-0.08,0.64-0.11C62.54,83.15,62.42,83.18,61.99,82.88z M62.77,82.81c0.39-0.05,0.48,0.05,0.59,0.57
C63.02,83.29,63.13,82.88,62.77,82.81z"/>
<path class="st0" d="M63.48,84.96c-0.24-0.11-0.45-0.06-0.65-0.03c-0.49,0.06-0.98,0.2-1.48,0.05c-0.06-0.02-0.15,0.03-0.23,0.05
c-0.02,0.12-0.04,0.23-0.06,0.32c-0.17,0.09-0.39-0.01-0.46,0.26c-0.04,0.17-0.27,0.3-0.12,0.5c0.75,0,1.49,0,2.23,0
c-0.39,0.21-0.8,0.35-1.24,0.23c-0.07-0.02-0.16-0.01-0.24-0.01c-0.1,0-0.23-0.03-0.3,0.02c-0.08,0.06-0.1,0.19-0.16,0.29
c-0.17,0.04-0.35,0.07-0.52,0.1c-0.06,0.21-0.04,0.34,0.2,0.35c0.26,0.01,0.53,0.01,0.79,0.04c0.55,0.07,1.06,0.04,1.51-0.37
c0.15-0.14,0.3-0.23,0.25-0.45c0.07-0.03,0.12-0.05,0.17-0.06c0.12-0.04,0.25-0.1,0.19-0.24c-0.03-0.07-0.14-0.14-0.23-0.16
c-0.19-0.05-0.38-0.07-0.59-0.11c0.03-0.14,0.05-0.26,0.08-0.36C62.9,85.22,63.26,85.27,63.48,84.96z"/>
<path class="st0" d="M52.25,85.24c-0.01-0.25,0.18-0.32,0.33-0.44c0.05-0.04,0.11-0.14,0.09-0.19c-0.02-0.07-0.1-0.14-0.17-0.18
c-0.06-0.04-0.18,0-0.22-0.05c-0.28-0.28-0.55-0.12-0.83-0.02c-0.21,0.08-0.42,0.15-0.63,0.19c-0.33,0.06-0.38,0.09-0.43,0.38
c-0.16,0.12-0.29,0.21-0.42,0.3c0.07,0.09,0.15,0.18,0.23,0.28c-0.02,0.09-0.05,0.22-0.06,0.34c-0.05,0.34,0.02,0.45,0.31,0.63
c0.09,0.06,0.17,0.15,0.27,0.23c0.3-0.14,0.61-0.28,0.91-0.43c0.04-0.02,0.09-0.08,0.09-0.12c0.01-0.2,0.17-0.41-0.15-0.58
c0.23-0.09,0.38-0.15,0.52-0.21C52.14,85.34,52.25,85.29,52.25,85.24z"/>
<path class="st0" d="M58.16,66.65c-0.16-0.14-0.35-0.12-0.52-0.03c-0.22,0.12-0.42,0.27-0.62,0.41c-0.18,0.13-0.35,0.27-0.51,0.42
c-0.06,0.06-0.1,0.15-0.13,0.23c-0.1,0.28-0.19,0.57-0.29,0.87c-0.22-0.1-0.43-0.11-0.58,0.1c-0.01,0.21,0.13,0.33,0.29,0.31
c0.3-0.03,0.6-0.09,0.88-0.19c0.22-0.08,0.25-0.24,0.15-0.48c-0.09-0.21-0.04-0.39,0.08-0.55c0.1-0.14,0.21-0.27,0.34-0.38
c0.28-0.25,0.51-0.22,0.82,0.1c-0.13,0.01-0.24,0.03-0.35,0.04c0,0.03,0,0.06-0.01,0.09c0.1,0.03,0.19,0.07,0.29,0.07
c0.3,0.01,0.38-0.1,0.3-0.4c-0.01-0.05-0.04-0.11-0.02-0.14C58.43,66.92,58.3,66.78,58.16,66.65z"/>
<path class="st0" d="M54.73,70.12c0.03,0.18,0.13,0.29,0.32,0.28c0.25-0.02,0.49-0.04,0.7-0.22c0.08-0.07,0.2-0.12,0.3-0.16
c0.09-0.04,0.19-0.07,0.29-0.1c-0.14-0.27-0.14-0.28-0.54-0.42c-0.16-0.05-0.33-0.05-0.5-0.04c-0.05,0-0.1,0.13-0.12,0.21
c-0.01,0.06,0.04,0.14,0.08,0.25c-0.12-0.03-0.19-0.05-0.26-0.06C54.79,69.82,54.7,69.91,54.73,70.12z"/>
<path class="st0" d="M57.42,69.01c0.02,0.11,0.05,0.22,0.09,0.4c0.14-0.13,0.24-0.19,0.3-0.28c0.05-0.06,0.09-0.21,0.07-0.23
c-0.08-0.06-0.19-0.12-0.29-0.12C57.48,68.79,57.4,68.88,57.42,69.01z"/>
<path class="st0" d="M88.02,51.18c0.4,0.27,0.79,0.54,1.19,0.8c0.01,0,0.02,0,0.02,0c-0.06-0.08-0.11-0.17-0.19-0.22
c-0.25-0.19-0.51-0.37-0.77-0.55C88.21,51.17,88.04,51.16,88.02,51.18z"/>
<path class="st0" d="M94.66,55.8c0.23,0.22,0.45,0.44,0.69,0.65c0.1,0.09,0.23,0.16,0.35,0.23c-0.04-0.05-0.08-0.1-0.12-0.15
C95.3,56.24,95.05,55.93,94.66,55.8z"/>
<path class="st0" d="M94.63,55.76c-0.12-0.27-0.33-0.43-0.6-0.52C94.23,55.41,94.43,55.59,94.63,55.76z"/>
<path class="st0" d="M109.9,73.57c-0.01,0.24,0.13,0.39,0.25,0.57L109.9,73.57z"/>
<path class="st0" d="M91,69.63c0.01-0.23-0.24-1.14-0.41-1.28C90.64,68.53,90.93,69.42,91,69.63z"/>
<path class="st0" d="M42.67,83.42c0.19-0.23,0.17-0.31-0.13-0.39c-0.4-0.11-0.78-0.05-1.16,0.11c-0.29,0.13-0.3,0.16-0.15,0.45
c-0.17,0.08-0.34,0.15-0.58,0.26c0.21,0.19,0.37,0.32,0.51,0.45c-0.19,0.05-0.41,0.04-0.51,0.15c-0.12,0.12-0.14,0.35-0.2,0.53
c-0.02,0.02-0.05,0.05-0.07,0.07c0.1,0.06,0.19,0.17,0.3,0.18c0.36,0.04,0.71,0.24,1.06,0.12c0.18,0.2,0.35,0.38,0.5,0.55
c0.4-0.19,0.46-0.25,0.43-0.47c-0.01-0.14-0.02-0.28-0.04-0.42c-0.05-0.37,0.05-0.76-0.2-1.09c-0.02-0.03-0.01-0.11,0.01-0.15
C42.51,83.66,42.58,83.53,42.67,83.42z M42.02,83.49c-0.12,0.06-0.25,0.08-0.38,0.12c-0.01-0.04-0.02-0.08-0.03-0.11
c0.22-0.16,0.46-0.24,0.76-0.2C42.36,83.6,42.13,83.43,42.02,83.49z"/>
<path class="st0" d="M43.56,82.45c0.11-0.34,0.04-0.52-0.23-0.64c-0.17-0.07-0.34-0.15-0.51-0.2c-0.52-0.15-1.04-0.28-1.57-0.07
c-0.19,0.08-0.35,0.19-0.42,0.39c-0.03,0.08-0.03,0.19,0,0.26c0.09,0.18,0.21,0.35,0.32,0.52c0.05,0.08,0.12,0.15,0.22,0.01
c-0.05-0.17-0.14-0.36-0.02-0.46c0.35-0.06,0.62-0.13,0.89-0.15c0.26-0.02,0.56-0.1,0.72,0.27C43.18,82.15,43.37,82.23,43.56,82.45
z"/>
<path class="st0" d="M74.64,46c-0.1,0.16-0.2,0.28-0.25,0.43c-0.08,0.22-0.13,0.45-0.21,0.67c-0.13,0.33-0.27,0.65-0.41,0.98
c-0.07,0.16-0.07,0.28,0.15,0.32c0.03-0.06,0.08-0.11,0.1-0.16c0.1-0.36,0.28-0.67,0.49-0.98c0.14-0.19,0.24-0.41,0.35-0.62
C74.97,46.39,74.9,46.15,74.64,46z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 64 KiB

112
parse.go Normal file
View File

@@ -0,0 +1,112 @@
// Copyright 2021 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package age
import (
"bufio"
"fmt"
"io"
"strings"
"unicode/utf8"
)
// 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, and plugins, which involve invoking external programs.
//
// Currently, all returned values are of type *[X25519Identity] or
// *[HybridIdentity], 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
}
if !utf8.ValidString(line) {
return nil, fmt.Errorf("identities file is not valid UTF-8")
}
i, err := parseIdentity(line)
if err != nil {
return nil, fmt.Errorf("error at line %d: %v", n, err)
}
ids = append(ids, i)
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("failed to read identities file: %v", err)
}
if len(ids) == 0 {
return nil, fmt.Errorf("no identities found")
}
return ids, nil
}
func parseIdentity(arg string) (Identity, error) {
switch {
case strings.HasPrefix(arg, "AGE-SECRET-KEY-1"):
return ParseX25519Identity(arg)
case strings.HasPrefix(arg, "AGE-SECRET-KEY-PQ-1"):
return ParseHybridIdentity(arg)
default:
return nil, fmt.Errorf("unknown identity type: %q", arg)
}
}
// 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, tagged recipients, which have different privacy
// properties, and plugins, which involve invoking external programs.
//
// Currently, all returned values are of type *[X25519Recipient] or
// *[HybridRecipient] 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
}
if !utf8.ValidString(line) {
return nil, fmt.Errorf("recipients file is not valid UTF-8")
}
r, err := parseRecipient(line)
if err != nil {
return nil, fmt.Errorf("error at line %d: %v", n, err)
}
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
}
func parseRecipient(arg string) (Recipient, error) {
switch {
case strings.HasPrefix(arg, "age1pq1"):
return ParseHybridRecipient(arg)
case strings.HasPrefix(arg, "age1"):
return ParseX25519Recipient(arg)
default:
return nil, fmt.Errorf("unknown recipient type: %q", arg)
}
}

506
plugin/client.go Normal file
View File

@@ -0,0 +1,506 @@
// Copyright 2021 Google LLC
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package plugin
import (
"bufio"
"crypto/rand"
"errors"
"fmt"
"io"
mathrand "math/rand/v2"
"os"
"path/filepath"
"strconv"
"strings"
"time"
exec "golang.org/x/sys/execabs"
"filippo.io/age"
"filippo.io/age/internal/format"
)
type Recipient struct {
name string
encoding string
ui *ClientUI
// identity is true when encoding is an identity string.
identity bool
}
var _ age.Recipient = &Recipient{}
var _ age.RecipientWithLabels = &Recipient{}
func NewRecipient(s string, ui *ClientUI) (*Recipient, error) {
name, _, err := ParseRecipient(s)
if err != nil {
return nil, err
}
return &Recipient{
name: name, encoding: s, ui: ui,
}, nil
}
// Name returns the plugin name, which is used in the recipient ("age1name1...")
// and identity ("AGE-PLUGIN-NAME-1...") encodings, as well as in the plugin
// binary name ("age-plugin-name").
func (r *Recipient) Name() string {
return r.name
}
// String returns the recipient encoding string ("age1name1...") or
// "<identity-based recipient>" if r was created by [Identity.Recipient].
func (r *Recipient) String() string {
if r.identity {
return "<identity-based recipient>"
}
return r.encoding
}
func (r *Recipient) Wrap(fileKey []byte) (stanzas []*age.Stanza, err error) {
stanzas, _, err = r.WrapWithLabels(fileKey)
return
}
func (r *Recipient) WrapWithLabels(fileKey []byte) (stanzas []*age.Stanza, labels []string, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("%s plugin: %w", r.name, err)
}
}()
conn, err := openClientConnection(r.name, "recipient-v1")
if err != nil {
return nil, nil, fmt.Errorf("couldn't start plugin: %w", err)
}
defer conn.Close()
// Phase 1: client sends recipient or identity and file key
addType := "add-recipient"
if r.identity {
addType = "add-identity"
}
if err := writeStanza(conn, addType, r.encoding); err != nil {
return nil, nil, err
}
if _, err := writeGrease(conn); err != nil {
return nil, nil, err
}
if err := writeStanzaWithBody(conn, "wrap-file-key", fileKey); err != nil {
return nil, nil, err
}
if err := writeStanza(conn, "extension-labels"); err != nil {
return nil, nil, err
}
if err := writeStanza(conn, "done"); err != nil {
return nil, nil, err
}
// Phase 2: plugin responds with stanzas
sr := format.NewStanzaReader(bufio.NewReader(conn))
ReadLoop:
for {
s, err := r.ui.readStanza(r.name, sr)
if err != nil {
return nil, nil, err
}
switch s.Type {
case "recipient-stanza":
if len(s.Args) < 2 {
return nil, nil, fmt.Errorf("malformed recipient stanza: unexpected argument count")
}
n, err := strconv.Atoi(s.Args[0])
if err != nil {
return nil, nil, fmt.Errorf("malformed recipient stanza: invalid index")
}
// We only send a single file key, so the index must be 0.
if n != 0 {
return nil, nil, fmt.Errorf("malformed recipient stanza: unexpected index")
}
stanzas = append(stanzas, &age.Stanza{
Type: s.Args[1],
Args: s.Args[2:],
Body: s.Body,
})
if err := writeStanza(conn, "ok"); err != nil {
return nil, nil, err
}
case "labels":
if labels != nil {
return nil, nil, fmt.Errorf("repeated labels stanza")
}
labels = s.Args
if err := writeStanza(conn, "ok"); err != nil {
return nil, nil, err
}
case "error":
if err := writeStanza(conn, "ok"); err != nil {
return nil, nil, err
}
return nil, nil, fmt.Errorf("%s", s.Body)
case "done":
break ReadLoop
default:
if ok, err := r.ui.handle(r.name, conn, s); err != nil {
return nil, nil, err
} else if !ok {
if err := writeStanza(conn, "unsupported"); err != nil {
return nil, nil, err
}
}
}
}
if len(stanzas) == 0 {
return nil, nil, fmt.Errorf("received zero recipient stanzas")
}
return stanzas, labels, nil
}
type Identity struct {
name string
encoding string
ui *ClientUI
}
var _ age.Identity = &Identity{}
func NewIdentity(s string, ui *ClientUI) (*Identity, error) {
name, _, err := ParseIdentity(s)
if err != nil {
return nil, err
}
return &Identity{
name: name, encoding: s, ui: ui,
}, nil
}
func NewIdentityWithoutData(name string, ui *ClientUI) (*Identity, error) {
s := EncodeIdentity(name, nil)
if s == "" {
return nil, fmt.Errorf("invalid plugin name: %q", name)
}
return &Identity{
name: name, encoding: s, ui: ui,
}, nil
}
// Name returns the plugin name, which is used in the recipient ("age1name1...")
// and identity ("AGE-PLUGIN-NAME-1...") encodings, as well as in the plugin
// binary name ("age-plugin-name").
func (i *Identity) Name() string {
return i.name
}
// String returns the identity encoding string ("AGE-PLUGIN-NAME-1...").
func (i *Identity) String() string {
return i.encoding
}
// Recipient returns a Recipient wrapping this identity. When that Recipient is
// used to encrypt a file key, the identity encoding is provided as-is to the
// plugin, which is expected to support encrypting to identities.
func (i *Identity) Recipient() *Recipient {
return &Recipient{
name: i.name,
encoding: i.encoding,
identity: true,
ui: i.ui,
}
}
func (i *Identity) Unwrap(stanzas []*age.Stanza) (fileKey []byte, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("%s plugin: %w", i.name, err)
}
}()
conn, err := openClientConnection(i.name, "identity-v1")
if err != nil {
return nil, fmt.Errorf("couldn't start plugin: %w", err)
}
defer conn.Close()
// Phase 1: client sends the plugin the identity string and the stanzas
if err := writeStanza(conn, "add-identity", i.encoding); err != nil {
return nil, err
}
if _, err := writeGrease(conn); err != nil {
return nil, err
}
for _, rs := range stanzas {
s := &format.Stanza{
Type: "recipient-stanza",
Args: append([]string{"0", rs.Type}, rs.Args...),
Body: rs.Body,
}
if err := s.Marshal(conn); err != nil {
return nil, err
}
}
if err := writeStanza(conn, "done"); err != nil {
return nil, err
}
// Phase 2: plugin responds with various commands and a file key
sr := format.NewStanzaReader(bufio.NewReader(conn))
ReadLoop:
for {
s, err := i.ui.readStanza(i.name, sr)
if err != nil {
return nil, err
}
switch s.Type {
case "file-key":
if len(s.Args) != 1 {
return nil, fmt.Errorf("malformed file-key stanza: unexpected arguments count")
}
n, err := strconv.Atoi(s.Args[0])
if err != nil {
return nil, fmt.Errorf("malformed file-key stanza: invalid index")
}
// We only send a single file key, so the index must be 0.
if n != 0 {
return nil, fmt.Errorf("malformed file-key stanza: unexpected index")
}
if fileKey != nil {
return nil, fmt.Errorf("received duplicated file-key stanza")
}
fileKey = s.Body
if err := writeStanza(conn, "ok"); err != nil {
return nil, err
}
case "error":
if err := writeStanza(conn, "ok"); err != nil {
return nil, err
}
return nil, fmt.Errorf("%s", s.Body)
case "done":
break ReadLoop
default:
if ok, err := i.ui.handle(i.name, conn, s); err != nil {
return nil, err
} else if !ok {
if err := writeStanza(conn, "unsupported"); err != nil {
return nil, err
}
}
}
}
if fileKey == nil {
return nil, age.ErrIncorrectIdentity
}
return fileKey, nil
}
// ClientUI holds callbacks that will be invoked by (Un)Wrap if the plugin
// wishes to interact with the user. If any of them is nil or returns an error,
// failure will be reported to the plugin, but note that the error is otherwise
// discarded. Implementations are encouraged to display errors to the user
// before returning them.
type ClientUI struct {
// DisplayMessage displays the message, which is expected to have lowercase
// initials and no final period.
DisplayMessage func(name, message string) error
// RequestValue requests a secret or public input, with the provided prompt.
RequestValue func(name, prompt string, secret bool) (string, error)
// Confirm requests a confirmation with the provided prompt. The yes and no
// value are the choices provided to the user. no may be empty. The return
// value indicates whether the user selected the yes or no option.
Confirm func(name, prompt, yes, no string) (choseYes bool, err error)
// WaitTimer is invoked once (Un)Wrap has been waiting for 5 seconds on the
// plugin, for example because the plugin is waiting for an external event
// (e.g. a hardware token touch). Unlike the other callbacks, WaitTimer runs
// in a separate goroutine, and if missing it's simply ignored.
WaitTimer func(name string)
}
func (c *ClientUI) handle(name string, conn *clientConnection, s *format.Stanza) (ok bool, err error) {
switch s.Type {
case "msg":
if c.DisplayMessage == nil {
return true, writeStanza(conn, "fail")
}
if err := c.DisplayMessage(name, string(s.Body)); err != nil {
return true, writeStanza(conn, "fail")
}
return true, writeStanza(conn, "ok")
case "request-secret", "request-public":
if c.RequestValue == nil {
return true, writeStanza(conn, "fail")
}
secret, err := c.RequestValue(name, string(s.Body), s.Type == "request-secret")
if err != nil {
return true, writeStanza(conn, "fail")
}
return true, writeStanzaWithBody(conn, "ok", []byte(secret))
case "confirm":
if len(s.Args) != 1 && len(s.Args) != 2 {
return true, fmt.Errorf("malformed confirm stanza: unexpected number of arguments")
}
if c.Confirm == nil {
return true, writeStanza(conn, "fail")
}
yes, err := format.DecodeString(s.Args[0])
if err != nil {
return true, fmt.Errorf("malformed confirm stanza: invalid YES option encoding")
}
var no []byte
if len(s.Args) == 2 {
no, err = format.DecodeString(s.Args[1])
if err != nil {
return true, fmt.Errorf("malformed confirm stanza: invalid NO option encoding")
}
}
choseYes, err := c.Confirm(name, string(s.Body), string(yes), string(no))
if err != nil {
return true, writeStanza(conn, "fail")
}
result := "yes"
if !choseYes {
result = "no"
}
return true, writeStanza(conn, "ok", result)
default:
return false, nil
}
}
// readStanza calls r.ReadStanza and, if set, invokes WaitTimer in a separate
// goroutine if the call takes longer than 5 seconds.
func (c *ClientUI) readStanza(name string, r *format.StanzaReader) (*format.Stanza, error) {
if c.WaitTimer != nil {
defer time.AfterFunc(5*time.Second, func() { c.WaitTimer(name) }).Stop()
}
return r.ReadStanza()
}
type clientConnection struct {
cmd *exec.Cmd
io.Reader // stdout
io.Writer // stdin
close func()
}
// NotFoundError is returned by [Recipient.Wrap] and [Identity.Unwrap] when the
// plugin binary cannot be found.
type NotFoundError struct {
// Name is the plugin (not binary) name.
Name string
// Err is the underlying error, usually an [exec.Error] wrapping
// [exec.ErrNotFound].
Err error
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%q plugin not found: %v", e.Name, e.Err)
}
func (e *NotFoundError) Unwrap() error {
return e.Err
}
var testOnlyPluginPath string
func openClientConnection(name, protocol string) (*clientConnection, error) {
path := "age-plugin-" + name
if testOnlyPluginPath != "" {
path = filepath.Join(testOnlyPluginPath, path)
} else if strings.ContainsRune(name, os.PathSeparator) {
return nil, fmt.Errorf("invalid plugin name: %q", name)
}
cmd := exec.Command(path, "--age-plugin="+protocol)
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
cc := &clientConnection{
cmd: cmd,
Reader: stdout,
Writer: stdin,
close: func() {
stdin.Close()
stdout.Close()
},
}
if os.Getenv("AGEDEBUG") == "plugin" {
cc.Reader = io.TeeReader(cc.Reader, os.Stderr)
cc.Writer = io.MultiWriter(cc.Writer, os.Stderr)
cmd.Stderr = os.Stderr
}
// We don't want the plugins to rely on the working directory for anything
// as different clients might treat it differently, so we set it to an empty
// temporary directory.
cmd.Dir = os.TempDir()
if err := cmd.Start(); err != nil {
if errors.Is(err, exec.ErrNotFound) {
return nil, &NotFoundError{Name: name, Err: err}
}
return nil, err
}
return cc, nil
}
func (cc *clientConnection) Close() error {
// Close stdin and stdout and send SIGINT (if supported) to the plugin,
// then wait for it to cleanup and exit.
cc.close()
cc.cmd.Process.Signal(os.Interrupt)
return cc.cmd.Wait()
}
func writeStanza(conn io.Writer, t string, args ...string) error {
s := &format.Stanza{Type: t, Args: args}
return s.Marshal(conn)
}
func writeStanzaWithBody(conn io.Writer, t string, body []byte) error {
s := &format.Stanza{Type: t, Body: body}
return s.Marshal(conn)
}
func writeGrease(conn io.Writer) (sent bool, err error) {
if mathrand.IntN(3) == 0 {
return false, nil
}
s := &format.Stanza{Type: fmt.Sprintf("grease-%x", mathrand.Int())}
for i := 0; i < mathrand.IntN(3); i++ {
s.Args = append(s.Args, fmt.Sprintf("%d", mathrand.IntN(100)))
}
if mathrand.IntN(2) == 0 {
s.Body = make([]byte, mathrand.IntN(100))
rand.Read(s.Body)
}
return true, s.Marshal(conn)
}

227
plugin/client_test.go Normal file
View File

@@ -0,0 +1,227 @@
// Copyright 2023 The age Authors
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
package plugin
import (
"bytes"
"errors"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"filippo.io/age"
"filippo.io/age/internal/bech32"
)
func TestMain(m *testing.M) {
switch filepath.Base(os.Args[0]) {
case "age-plugin-test":
p, _ := New("test")
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
return testRecipient{}, nil
})
os.Exit(p.Main())
case "age-plugin-testpqc":
p, _ := New("testpqc")
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
return testPQCRecipient{}, nil
})
os.Exit(p.Main())
case "age-plugin-error":
p, _ := New("error")
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
return nil, errors.New("oh my, an error occurred")
})
p.HandleIdentity(func(data []byte) (age.Identity, error) {
return nil, errors.New("oh my, an error occurred")
})
os.Exit(p.Main())
default:
os.Exit(m.Run())
}
}
type testRecipient struct{}
func (testRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
return []*age.Stanza{{Type: "test", Body: fileKey}}, nil
}
type testPQCRecipient struct{}
var _ age.RecipientWithLabels = testPQCRecipient{}
func (testPQCRecipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
return []*age.Stanza{{Type: "test", Body: fileKey}}, nil
}
func (testPQCRecipient) WrapWithLabels(fileKey []byte) ([]*age.Stanza, []string, error) {
return []*age.Stanza{{Type: "test", Body: fileKey}}, []string{"postquantum"}, nil
}
func TestLabels(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows support is TODO")
}
temp := t.TempDir()
testOnlyPluginPath = temp
t.Cleanup(func() { testOnlyPluginPath = "" })
ex, err := os.Executable()
if err != nil {
t.Fatal(err)
}
if err := os.Link(ex, filepath.Join(temp, "age-plugin-test")); err != nil {
t.Fatal(err)
}
if err := os.Chmod(filepath.Join(temp, "age-plugin-test"), 0755); err != nil {
t.Fatal(err)
}
if err := os.Link(ex, filepath.Join(temp, "age-plugin-testpqc")); err != nil {
t.Fatal(err)
}
if err := os.Chmod(filepath.Join(temp, "age-plugin-testpqc"), 0755); err != nil {
t.Fatal(err)
}
name, err := bech32.Encode("age1test", nil)
if err != nil {
t.Fatal(err)
}
testPlugin, err := NewRecipient(name, &ClientUI{})
if err != nil {
t.Fatal(err)
}
namePQC, err := bech32.Encode("age1testpqc", nil)
if err != nil {
t.Fatal(err)
}
testPluginPQC, err := NewRecipient(namePQC, &ClientUI{})
if err != nil {
t.Fatal(err)
}
if _, err := age.Encrypt(io.Discard, testPluginPQC); err != nil {
t.Errorf("expected one pqc to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, testPluginPQC, testPluginPQC); err != nil {
t.Errorf("expected two pqc to work, got %v", err)
}
if _, err := age.Encrypt(io.Discard, testPluginPQC, testPlugin); err == nil {
t.Errorf("expected one pqc and one normal to fail")
}
if _, err := age.Encrypt(io.Discard, testPlugin, testPluginPQC); err == nil {
t.Errorf("expected one pqc and one normal to fail")
}
}
func TestNotFound(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows support is TODO")
}
r := EncodeRecipient("nonexistentplugin", nil)
t.Log(r)
testPluginRecipient, err := NewRecipient(r, &ClientUI{})
if err != nil {
t.Fatal(err)
}
var e *NotFoundError
if _, err := age.Encrypt(io.Discard, testPluginRecipient); err == nil {
t.Errorf("expected error for nonexistent plugin")
} else if !errors.As(err, &e) {
t.Errorf("expected NotFoundError, got %T: %v", err, err)
} else if e.Name != "nonexistentplugin" {
t.Errorf("expected NotFoundError.Name to be nonexistentplugin, got %q", e.Name)
} else if !errors.Is(err, exec.ErrNotFound) {
t.Errorf("expected error to wrap exec.ErrNotFound, got: %v", err)
}
buf := &bytes.Buffer{}
id, err := age.GenerateHybridIdentity()
if err != nil {
t.Fatal(err)
}
w, err := age.Encrypt(buf, id.Recipient())
if err != nil {
t.Fatal(err)
}
w.Close()
i := EncodeIdentity("nonexistentplugin", nil)
t.Log(i)
testPluginIdentity, err := NewIdentity(i, &ClientUI{})
if err != nil {
t.Fatal(err)
}
if _, err := age.Decrypt(buf, testPluginIdentity); err == nil {
t.Errorf("expected error for nonexistent plugin")
} else if errors.As(err, new(*age.NoIdentityMatchError)) {
t.Errorf("expected NotFoundError, got NoIdentityMatchError: %v", err)
} else if !errors.As(err, &e) {
t.Errorf("expected NotFoundError, got %T: %v", err, err)
} else if e.Name != "nonexistentplugin" {
t.Errorf("expected NotFoundError.Name to be nonexistentplugin, got %q", e.Name)
} else if !errors.Is(err, exec.ErrNotFound) {
t.Errorf("expected error to wrap exec.ErrNotFound, got: %v", err)
}
}
func TestPluginError(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Windows support is TODO")
}
temp := t.TempDir()
testOnlyPluginPath = temp
t.Cleanup(func() { testOnlyPluginPath = "" })
ex, err := os.Executable()
if err != nil {
t.Fatal(err)
}
if err := os.Link(ex, filepath.Join(temp, "age-plugin-error")); err != nil {
t.Fatal(err)
}
if err := os.Chmod(filepath.Join(temp, "age-plugin-error"), 0755); err != nil {
t.Fatal(err)
}
r := EncodeRecipient("error", nil)
testPluginRecipient, err := NewRecipient(r, &ClientUI{})
if err != nil {
t.Fatal(err)
}
if _, err := age.Encrypt(io.Discard, testPluginRecipient); err == nil {
t.Errorf("expected error from plugin")
} else if !strings.Contains(err.Error(), "oh my, an error occurred") {
t.Errorf("expected plugin error, got: %v", err)
}
buf := &bytes.Buffer{}
id, err := age.GenerateHybridIdentity()
if err != nil {
t.Fatal(err)
}
w, err := age.Encrypt(buf, id.Recipient())
if err != nil {
t.Fatal(err)
}
w.Close()
i := EncodeIdentity("error", nil)
testPluginIdentity, err := NewIdentity(i, &ClientUI{})
if err != nil {
t.Fatal(err)
}
if _, err := age.Decrypt(buf, testPluginIdentity); err == nil {
t.Errorf("expected error from plugin")
} else if !strings.Contains(err.Error(), "oh my, an error occurred") {
t.Errorf("expected plugin error, got: %v", err)
}
}

108
plugin/encode.go Normal file
View File

@@ -0,0 +1,108 @@
// Copyright 2023 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plugin
import (
"crypto/ecdh"
"crypto/mlkem"
"fmt"
"strings"
"filippo.io/age/internal/bech32"
"filippo.io/hpke"
)
// EncodeIdentity encodes a plugin identity string for a plugin with the given
// name. If the name is invalid, it returns an empty string.
func EncodeIdentity(name string, data []byte) string {
if !validPluginName(name) {
return ""
}
s, _ := bech32.Encode("AGE-PLUGIN-"+strings.ToUpper(name)+"-", data)
return s
}
// ParseIdentity decodes a plugin identity string. It returns the plugin name
// in lowercase and the encoded data.
func ParseIdentity(s string) (name string, data []byte, err error) {
hrp, data, err := bech32.Decode(s)
if err != nil {
return "", nil, fmt.Errorf("invalid identity encoding: %v", err)
}
if !strings.HasPrefix(hrp, "AGE-PLUGIN-") || !strings.HasSuffix(hrp, "-") {
return "", nil, fmt.Errorf("not a plugin identity: %v", err)
}
name = strings.TrimSuffix(strings.TrimPrefix(hrp, "AGE-PLUGIN-"), "-")
name = strings.ToLower(name)
if !validPluginName(name) {
return "", nil, fmt.Errorf("invalid plugin name: %q", name)
}
return name, data, nil
}
// EncodeRecipient encodes a plugin recipient string for a plugin with the given
// name. If the name is invalid, it returns an empty string.
func EncodeRecipient(name string, data []byte) string {
if !validPluginName(name) {
return ""
}
s, _ := bech32.Encode("age1"+strings.ToLower(name), data)
return s
}
// ParseRecipient decodes a plugin recipient string. It returns the plugin name
// in lowercase and the encoded data.
func ParseRecipient(s string) (name string, data []byte, err error) {
hrp, data, err := bech32.Decode(s)
if err != nil {
return "", nil, fmt.Errorf("invalid recipient encoding: %v", err)
}
if !strings.HasPrefix(hrp, "age1") {
return "", nil, fmt.Errorf("not a plugin recipient: %v", err)
}
name = strings.TrimPrefix(hrp, "age1")
if !validPluginName(name) {
return "", nil, fmt.Errorf("invalid plugin name: %q", name)
}
return name, data, nil
}
func validPluginName(name string) bool {
if name == "" {
return false
}
allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+-._"
for _, r := range name {
if !strings.ContainsRune(allowed, r) {
return false
}
}
return true
}
// EncodeX25519Recipient encodes a native X25519 recipient from a
// [crypto/ecdh.X25519] public key. It's meant for plugins that implement
// identities that are compatible with native recipients.
func EncodeX25519Recipient(pk *ecdh.PublicKey) (string, error) {
if pk.Curve() != ecdh.X25519() {
return "", fmt.Errorf("wrong ecdh Curve")
}
return bech32.Encode("age", pk.Bytes())
}
// EncodeHybridRecipient encodes a native MLKEM768-X25519 recipient from a
// [crypto/mlkem.EncapsulationKey768] and a [crypto/ecdh.X25519] public key.
// It's meant for plugins that implement identities that are compatible with
// native recipients.
func EncodeHybridRecipient(pq *mlkem.EncapsulationKey768, t *ecdh.PublicKey) (string, error) {
if t.Curve() != ecdh.X25519() {
return "", fmt.Errorf("wrong ecdh Curve")
}
pk, err := hpke.NewHybridPublicKey(pq, t)
if err != nil {
return "", fmt.Errorf("failed to create hybrid public key: %v", err)
}
return bech32.Encode("age1pq", pk.Bytes())
}

43
plugin/example_test.go Normal file
View File

@@ -0,0 +1,43 @@
package plugin_test
import (
"log"
"os"
"filippo.io/age"
"filippo.io/age/plugin"
)
type Recipient struct{}
func (r *Recipient) Wrap(fileKey []byte) ([]*age.Stanza, error) {
panic("unimplemented")
}
func NewRecipient(data []byte) (*Recipient, error) {
return &Recipient{}, nil
}
type Identity struct{}
func (i *Identity) Unwrap(s []*age.Stanza) ([]byte, error) {
panic("unimplemented")
}
func NewIdentity(data []byte) (*Identity, error) {
return &Identity{}, nil
}
func ExamplePlugin_main() {
p, err := plugin.New("example")
if err != nil {
log.Fatal(err)
}
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
return NewRecipient(data)
})
p.HandleIdentity(func(data []byte) (age.Identity, error) {
return NewIdentity(data)
})
os.Exit(p.Main())
}

676
plugin/plugin.go Normal file
View File

@@ -0,0 +1,676 @@
// Package plugin implements the age plugin protocol.
//
// [Recipient] and [Indentity] are plugin clients, that execute plugin binaries to
// perform encryption and decryption operations.
//
// [Plugin] is a framework for writing age plugins, that exposes an [age.Recipient]
// and/or [age.Identity] implementation as a plugin binary.
package plugin
import (
"bufio"
"errors"
"flag"
"fmt"
"io"
"os"
"strconv"
"filippo.io/age"
"filippo.io/age/internal/format"
)
// TODO: add plugin test framework.
// Plugin is a framework for writing age plugins. It allows exposing regular
// [age.Recipient] and [age.Identity] implementations as plugins, and handles
// all the protocol details.
type Plugin struct {
name string
fs *flag.FlagSet
sm *string
recipient func([]byte) (age.Recipient, error)
idAsRecipient func([]byte) (age.Recipient, error)
identity func([]byte) (age.Identity, error)
stdin io.Reader
stdout, stderr io.Writer
sr *format.StanzaReader
// broken is set if the protocol broke down during an interaction function
// called by a Recipient or Identity.
broken bool
}
// New creates a new Plugin with the given name.
//
// For example, a plugin named "frood" would be invoked as "age-plugin-frood".
func New(name string) (*Plugin, error) {
return &Plugin{name: name, stdin: os.Stdin,
stdout: os.Stdout, stderr: os.Stderr}, nil
}
// Name returns the name of the plugin.
func (p *Plugin) Name() string {
return p.name
}
// RegisterFlags registers the plugin's flags with the given [flag.FlagSet], or
// with the default [flag.CommandLine] if fs is nil. It must be called before
// [flag.Parse] and [Plugin.Main].
//
// This allows the plugin to expose additional flags when invoked manually, for
// example to implement a keygen mode.
func (p *Plugin) RegisterFlags(fs *flag.FlagSet) {
if fs == nil {
fs = flag.CommandLine
}
p.fs = fs
p.sm = fs.String("age-plugin", "", "age-plugin state machine")
}
// HandleRecipient registers a function to parse recipients of the form
// age1name1... into [age.Recipient] values. data is the decoded Bech32 payload.
//
// If the returned Recipient implements [age.RecipientWithLabels], Plugin will
// use it and enforce consistency across every returned stanza in an execution.
// If the client supports labels, they will be passed through the protocol.
//
// It must be called before [Plugin.Main], and can be called at most once.
// Otherwise, it panics.
func (p *Plugin) HandleRecipient(f func(data []byte) (age.Recipient, error)) {
if p.recipient != nil {
panic("HandleRecipient called twice")
}
p.recipient = f
}
// HandleIdentityAsRecipient registers a function to parse identities of the
// form AGE-PLUGIN-NAME-1... into [age.Recipient] values, for when identities
// are used as recipients. data is the decoded Bech32 payload.
//
// If the returned Recipient implements [age.RecipientWithLabels], Plugin will
// use it and enforce consistency across every returned stanza in an execution.
// If the client supports labels, they will be passed through the protocol.
//
// It must be called before [Plugin.Main], and can be called at most once.
// Otherwise, it panics.
func (p *Plugin) HandleIdentityAsRecipient(f func(data []byte) (age.Recipient, error)) {
if p.idAsRecipient != nil {
panic("HandleIdentityAsRecipient called twice")
}
p.idAsRecipient = f
}
// HandleIdentity registers a function to parse identities of the form
// AGE-PLUGIN-NAME-1... into [age.Identity] values. data is the decoded Bech32
// payload.
//
// It must be called before [Plugin.Main], and can be called at most once.
// Otherwise, it panics.
func (p *Plugin) HandleIdentity(f func(data []byte) (age.Identity, error)) {
if p.identity != nil {
panic("HandleIdentity called twice")
}
p.identity = f
}
// HandleRecipientEncoding is like [Plugin.HandleRecipient] but provides the
// full recipient encoding string to the callback.
//
// It allows using functions like ParseRecipient directly.
func (p *Plugin) HandleRecipientEncoding(f func(recipient string) (age.Recipient, error)) {
p.HandleRecipient(func(data []byte) (age.Recipient, error) {
return f(EncodeRecipient(p.name, data))
})
}
// HandleIdentityEncodingAsRecipient is like [Plugin.HandleIdentityAsRecipient] but
// provides the full identity encoding string to the callback.
func (p *Plugin) HandleIdentityEncodingAsRecipient(f func(identity string) (age.Recipient, error)) {
p.HandleIdentityAsRecipient(func(data []byte) (age.Recipient, error) {
return f(EncodeIdentity(p.name, data))
})
}
// HandleIdentityEncoding is like [Plugin.HandleIdentity] but provides the
// full identity encoding string to the callback.
//
// It allows using functions like ParseIdentity directly.
func (p *Plugin) HandleIdentityEncoding(f func(identity string) (age.Identity, error)) {
p.HandleIdentity(func(data []byte) (age.Identity, error) {
return f(EncodeIdentity(p.name, data))
})
}
// Main runs the plugin protocol. It returns an exit code to pass to os.Exit.
//
// It automatically calls [Plugin.RegisterFlags] and [flag.Parse] if they were
// not called before.
func (p *Plugin) Main() int {
if p.fs == nil {
p.RegisterFlags(nil)
}
if !p.fs.Parsed() {
p.fs.Parse(os.Args[1:])
}
if *p.sm == "recipient-v1" {
return p.RecipientV1()
}
if *p.sm == "identity-v1" {
return p.IdentityV1()
}
fmt.Fprintf(p.stderr, "unknown state machine %q", *p.sm)
return 4
}
// SetIO sets the plugin's input and output streams, which default to
// stdin/stdout/stderr.
//
// It must be called before [Plugin.Main].
func (p *Plugin) SetIO(stdin io.Reader, stdout, stderr io.Writer) {
p.stdin = stdin
p.stdout = stdout
p.stderr = stderr
}
// RecipientV1 implements the recipient-v1 state machine. It returns an exit
// code to pass to os.Exit.
//
// Most plugins should call [Plugin.Main] instead of this method.
func (p *Plugin) RecipientV1() int {
if p.recipient == nil && p.idAsRecipient == nil {
return p.fatalf("recipient-v1 not supported")
}
var recipientStrings, identityStrings []string
var fileKeys [][]byte
var supportsLabels bool
p.sr = format.NewStanzaReader(bufio.NewReader(p.stdin))
ReadLoop:
for {
s, err := p.sr.ReadStanza()
if err != nil {
return p.fatalf("failed to read stanza: %v", err)
}
switch s.Type {
case "add-recipient":
if err := expectStanzaWithNoBody(s, 1); err != nil {
return p.fatalf("%v", err)
}
recipientStrings = append(recipientStrings, s.Args[0])
case "add-identity":
if err := expectStanzaWithNoBody(s, 1); err != nil {
return p.fatalf("%v", err)
}
identityStrings = append(identityStrings, s.Args[0])
case "extension-labels":
if err := expectStanzaWithNoBody(s, 0); err != nil {
return p.fatalf("%v", err)
}
supportsLabels = true
case "wrap-file-key":
if err := expectStanzaWithBody(s, 0); err != nil {
return p.fatalf("%v", err)
}
fileKeys = append(fileKeys, s.Body)
case "done":
if err := expectStanzaWithNoBody(s, 0); err != nil {
return p.fatalf("%v", err)
}
break ReadLoop
default:
// Unsupported stanzas in uni-directional phases are ignored.
}
}
if len(recipientStrings)+len(identityStrings) == 0 {
return p.fatalf("no recipients or identities provided")
}
if len(fileKeys) == 0 {
return p.fatalf("no file keys provided")
}
var recipients, identities []age.Recipient
for i, s := range recipientStrings {
name, data, err := ParseRecipient(s)
if err != nil {
return p.recipientError(i, err)
}
if name != p.name {
return p.recipientError(i, fmt.Errorf("unsupported plugin name: %q", name))
}
if p.recipient == nil {
return p.recipientError(i, fmt.Errorf("recipient encodings not supported"))
}
r, err := p.recipient(data)
if err != nil {
return p.recipientError(i, err)
}
recipients = append(recipients, r)
}
for i, s := range identityStrings {
name, data, err := ParseIdentity(s)
if err != nil {
return p.identityError(i, err)
}
if name != p.name {
return p.identityError(i, fmt.Errorf("unsupported plugin name: %q", name))
}
if p.idAsRecipient == nil {
return p.identityError(i, fmt.Errorf("identity encodings not supported"))
}
r, err := p.idAsRecipient(data)
if err != nil {
return p.identityError(i, err)
}
identities = append(identities, r)
}
// Technically labels should be per-file key, but the client-side protocol
// extension shipped like this, and it doesn't feel worth making a v2.
var labels []string
stanzas := make([][]*age.Stanza, len(fileKeys))
for i, fk := range fileKeys {
for j, r := range recipients {
ss, ll, err := wrapWithLabels(r, fk)
if p.broken {
return 2
} else if err != nil {
return p.recipientError(j, err)
}
if i == 0 && j == 0 {
labels = ll
} else if err := checkLabels(ll, labels); err != nil {
return p.recipientError(j, err)
}
stanzas[i] = append(stanzas[i], ss...)
}
for j, r := range identities {
ss, ll, err := wrapWithLabels(r, fk)
if p.broken {
return 2
} else if err != nil {
return p.identityError(j, err)
}
if i == 0 && j == 0 && len(recipients) == 0 {
labels = ll
} else if err := checkLabels(ll, labels); err != nil {
return p.identityError(j, err)
}
stanzas[i] = append(stanzas[i], ss...)
}
}
if sent, err := writeGrease(p.stdout); err != nil {
return p.fatalf("failed to write grease: %v", err)
} else if sent {
if err := expectUnsupported(p.sr); err != nil {
return p.fatalf("%v", err)
}
}
if supportsLabels {
if err := writeStanza(p.stdout, "labels", labels...); err != nil {
return p.fatalf("failed to write labels stanza: %v", err)
}
if err := expectOk(p.sr); err != nil {
return p.fatalf("%v", err)
}
}
for i, ss := range stanzas {
for _, s := range ss {
if err := (&format.Stanza{Type: "recipient-stanza",
Args: append([]string{fmt.Sprint(i), s.Type}, s.Args...),
Body: s.Body}).Marshal(p.stdout); err != nil {
return p.fatalf("failed to write recipient-stanza: %v", err)
}
if err := expectOk(p.sr); err != nil {
return p.fatalf("%v", err)
}
}
if sent, err := writeGrease(p.stdout); err != nil {
return p.fatalf("failed to write grease: %v", err)
} else if sent {
if err := expectUnsupported(p.sr); err != nil {
return p.fatalf("%v", err)
}
}
}
if err := writeStanza(p.stdout, "done"); err != nil {
return p.fatalf("failed to write done stanza: %v", err)
}
return 0
}
func wrapWithLabels(r age.Recipient, fileKey []byte) ([]*age.Stanza, []string, error) {
if r, ok := r.(age.RecipientWithLabels); ok {
return r.WrapWithLabels(fileKey)
}
s, err := r.Wrap(fileKey)
return s, nil, err
}
func checkLabels(ll, labels []string) error {
if !slicesEqual(ll, labels) {
return fmt.Errorf("labels %q do not match previous recipients %q", ll, labels)
}
return nil
}
// IdentityV1 implements the identity-v1 state machine. It returns an exit code
// to pass to os.Exit.
//
// Most plugins should call [Plugin.Main] instead of this method.
func (p *Plugin) IdentityV1() int {
if p.identity == nil {
return p.fatalf("identity-v1 not supported")
}
var files [][]*age.Stanza
var identityStrings []string
p.sr = format.NewStanzaReader(bufio.NewReader(p.stdin))
ReadLoop:
for {
s, err := p.sr.ReadStanza()
if err != nil {
return p.fatalf("failed to read stanza: %v", err)
}
switch s.Type {
case "add-identity":
if err := expectStanzaWithNoBody(s, 1); err != nil {
return p.fatalf("%v", err)
}
identityStrings = append(identityStrings, s.Args[0])
case "recipient-stanza":
if len(s.Args) < 2 {
return p.fatalf("recipient-stanza stanza has %d arguments, want >=2", len(s.Args))
}
i, err := strconv.Atoi(s.Args[0])
if err != nil {
return p.fatalf("failed to parse recipient-stanza stanza argument: %v", err)
}
ss := &age.Stanza{Type: s.Args[1], Args: s.Args[2:], Body: s.Body}
switch i {
case len(files):
files = append(files, []*age.Stanza{ss})
case len(files) - 1:
files[len(files)-1] = append(files[len(files)-1], ss)
default:
return p.fatalf("unexpected file index %d, previous was %d", i, len(files)-1)
}
case "done":
if err := expectStanzaWithNoBody(s, 0); err != nil {
return p.fatalf("%v", err)
}
break ReadLoop
default:
// Unsupported stanzas in uni-directional phases are ignored.
}
}
if len(identityStrings) == 0 {
return p.fatalf("no identities provided")
}
if len(files) == 0 {
return p.fatalf("no stanzas provided")
}
var identities []age.Identity
for i, s := range identityStrings {
name, data, err := ParseIdentity(s)
if err != nil {
return p.identityError(i, err)
}
if name != p.name {
return p.identityError(i, fmt.Errorf("unsupported plugin name: %q", name))
}
if p.identity == nil {
return p.identityError(i, fmt.Errorf("identity encodings not supported"))
}
r, err := p.identity(data)
if err != nil {
return p.identityError(i, err)
}
identities = append(identities, r)
}
for i, ss := range files {
if sent, err := writeGrease(p.stdout); err != nil {
return p.fatalf("failed to write grease: %v", err)
} else if sent {
if err := expectUnsupported(p.sr); err != nil {
return p.fatalf("%v", err)
}
}
// TODO: there should be a mechanism to let the plugin decide the order
// in which identities are tried.
for _, id := range identities {
fk, err := id.Unwrap(ss)
if p.broken {
return 2
} else if errors.Is(err, age.ErrIncorrectIdentity) {
continue
} else if err != nil {
if err := p.writeError([]string{"stanza", fmt.Sprint(i), "0"}, err); err != nil {
return p.fatalf("%v", err)
}
// Note that we don't exit here, as the protocol allows
// continuing with other files.
break
}
s := &format.Stanza{Type: "file-key", Args: []string{fmt.Sprint(i)}, Body: fk}
if err := s.Marshal(p.stdout); err != nil {
return p.fatalf("failed to write file-key: %v", err)
}
if err := expectOk(p.sr); err != nil {
return p.fatalf("%v", err)
}
break
}
}
if err := writeStanza(p.stdout, "done"); err != nil {
return p.fatalf("failed to write done stanza: %v", err)
}
return 0
}
// DisplayMessage requests that the client display a message to the user. The
// message should start with a lowercase letter and have no final period.
// DisplayMessage returns an error if the client can't display the message, and
// may return before the message has been displayed to the user.
//
// It must only be called by a Wrap or Unwrap method invoked by [Plugin.Main].
func (p *Plugin) DisplayMessage(message string) error {
if err := writeStanzaWithBody(p.stdout, "msg", []byte(message)); err != nil {
return p.fatalInteractf("failed to write msg stanza: %v", err)
}
s, err := readOkOrFail(p.sr)
if err != nil {
return p.fatalInteractf("%v", err)
}
if s.Type == "fail" {
return fmt.Errorf("client failed to display message")
}
if err := expectStanzaWithNoBody(s, 0); err != nil {
return p.fatalInteractf("%v", err)
}
return nil
}
// RequestValue requests a secret or public input from the user through the
// client, with the provided prompt. It returns an error if the client can't
// request the input or if the user dismisses the prompt.
//
// It must only be called by a Wrap or Unwrap method invoked by [Plugin.Main].
func (p *Plugin) RequestValue(prompt string, secret bool) (string, error) {
t := "request-public"
if secret {
t = "request-secret"
}
if err := writeStanzaWithBody(p.stdout, t, []byte(prompt)); err != nil {
return "", p.fatalInteractf("failed to write stanza: %v", err)
}
s, err := readOkOrFail(p.sr)
if err != nil {
return "", p.fatalInteractf("%v", err)
}
if s.Type == "fail" {
return "", fmt.Errorf("client failed to request value")
}
if err := expectStanzaWithBody(s, 0); err != nil {
return "", p.fatalInteractf("%v", err)
}
return string(s.Body), nil
}
// Confirm requests a confirmation from the user through the client, with the
// provided prompt. The yes and no value are the choices provided to the user.
// no may be empty. The return value choseYes indicates whether the user
// selected the yes or no option. Confirm returns an error if the client can't
// request the confirmation.
//
// It must only be called by a Wrap or Unwrap method invoked by [Plugin.Main].
func (p *Plugin) Confirm(prompt, yes, no string) (choseYes bool, err error) {
args := []string{format.EncodeToString([]byte(yes))}
if no != "" {
args = append(args, format.EncodeToString([]byte(no)))
}
s := &format.Stanza{Type: "confirm", Args: args, Body: []byte(prompt)}
if err := s.Marshal(p.stdout); err != nil {
return false, p.fatalInteractf("failed to write confirm stanza: %v", err)
}
s, err = readOkOrFail(p.sr)
if err != nil {
return false, p.fatalInteractf("%v", err)
}
if s.Type == "fail" {
return false, fmt.Errorf("client failed to request confirmation")
}
if err := expectStanzaWithNoBody(s, 1); err != nil {
return false, p.fatalInteractf("%v", err)
}
return s.Args[0] == "yes", nil
}
// fatalInteractf prints the error to stderr and sets the broken flag, so the
// Wrap/Unwrap caller can exit with an error.
func (p *Plugin) fatalInteractf(format string, args ...any) error {
p.broken = true
fmt.Fprintf(p.stderr, format, args...)
return fmt.Errorf(format, args...)
}
func (p *Plugin) fatalf(format string, args ...any) int {
fmt.Fprintf(p.stderr, format, args...)
return 1
}
func expectStanzaWithNoBody(s *format.Stanza, wantArgs int) error {
if len(s.Args) != wantArgs {
return fmt.Errorf("%s stanza has %d arguments, want %d", s.Type, len(s.Args), wantArgs)
}
if len(s.Body) != 0 {
return fmt.Errorf("%s stanza has %d bytes of body, want 0", s.Type, len(s.Body))
}
return nil
}
func expectStanzaWithBody(s *format.Stanza, wantArgs int) error {
if len(s.Args) != wantArgs {
return fmt.Errorf("%s stanza has %d arguments, want %d", s.Type, len(s.Args), wantArgs)
}
if len(s.Body) == 0 {
return fmt.Errorf("%s stanza has 0 bytes of body, want >0", s.Type)
}
return nil
}
func (p *Plugin) recipientError(idx int, err error) int {
if err := p.writeError([]string{"recipient", fmt.Sprint(idx)}, err); err != nil {
return p.fatalf("%v", err)
}
return 3
}
func (p *Plugin) identityError(idx int, err error) int {
if err := p.writeError([]string{"identity", fmt.Sprint(idx)}, err); err != nil {
return p.fatalf("%v", err)
}
return 3
}
func expectOk(sr *format.StanzaReader) error {
ok, err := sr.ReadStanza()
if err != nil {
return fmt.Errorf("failed to read OK stanza: %v", err)
}
if ok.Type != "ok" {
return fmt.Errorf("expected OK stanza, got %q", ok.Type)
}
return expectStanzaWithNoBody(ok, 0)
}
func readOkOrFail(sr *format.StanzaReader) (*format.Stanza, error) {
s, err := sr.ReadStanza()
if err != nil {
return nil, fmt.Errorf("failed to read response stanza: %v", err)
}
switch s.Type {
case "fail":
if err := expectStanzaWithNoBody(s, 0); err != nil {
return nil, fmt.Errorf("%v", err)
}
return s, nil
case "ok":
return s, nil
default:
return nil, fmt.Errorf("expected ok or fail stanza, got %q", s.Type)
}
}
func expectUnsupported(sr *format.StanzaReader) error {
unsupported, err := sr.ReadStanza()
if err != nil {
return fmt.Errorf("failed to read unsupported stanza: %v", err)
}
if unsupported.Type != "unsupported" {
return fmt.Errorf("expected unsupported stanza, got %q", unsupported.Type)
}
return expectStanzaWithNoBody(unsupported, 0)
}
func (p *Plugin) writeError(args []string, err error) error {
s := &format.Stanza{Type: "error", Args: args}
s.Body = []byte(err.Error())
if err := s.Marshal(p.stdout); err != nil {
return fmt.Errorf("failed to write error stanza: %v", err)
}
if err := expectOk(p.sr); err != nil {
return fmt.Errorf("%v", err)
}
return nil
}
func slicesEqual(s1, s2 []string) bool {
if len(s1) != len(s2) {
return false
}
for i := range s1 {
if s1[i] != s2[i] {
return false
}
}
return true
}

78
plugin/tui.go Normal file
View File

@@ -0,0 +1,78 @@
package plugin
import (
"errors"
"fmt"
"filippo.io/age/internal/term"
)
// NewTerminalUI returns a [ClientUI] that uses the terminal to request inputs,
// and the provided functions to display messages and errors.
//
// The terminal is reached directly through /dev/tty or CONIN$/CONOUT$,
// bypassing standard input and output, so this UI can be used even when
// standard input or output are redirected.
func NewTerminalUI(printf, warningf func(format string, v ...any)) *ClientUI {
return &ClientUI{
DisplayMessage: func(name, message string) error {
printf("%s plugin: %s", name, message)
return nil
},
RequestValue: func(name, message string, isSecret bool) (s string, err error) {
defer func() {
if err != nil {
warningf("could not read value for age-plugin-%s: %v", name, err)
}
}()
if isSecret {
secret, err := term.ReadSecret(message)
if err != nil {
return "", err
}
return string(secret), nil
} else {
public, err := term.ReadPublic(message)
if err != nil {
return "", err
}
return string(public), nil
}
},
Confirm: func(name, message, yes, no string) (choseYes bool, err error) {
defer func() {
if err != nil {
warningf("could not read value for age-plugin-%s: %v", name, err)
}
}()
if no == "" {
message += fmt.Sprintf(" (press enter for %q)", yes)
_, err := term.ReadSecret(message)
if err != nil {
return false, err
}
return true, nil
}
message += fmt.Sprintf(" (press [1] for %q or [2] for %q)", yes, no)
for {
selection, err := term.ReadCharacter(message)
if err != nil {
return false, err
}
switch selection {
case '1':
return true, nil
case '2':
return false, nil
case '\x03': // CTRL-C
return false, errors.New("user cancelled prompt")
default:
warningf("reading value for age-plugin-%s: invalid selection %q", name, selection)
}
}
},
WaitTimer: func(name string) {
printf("waiting on %s plugin...", name)
},
}
}

181
pq.go Normal file
View File

@@ -0,0 +1,181 @@
// Copyright 2025 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package age
import (
"errors"
"fmt"
"strings"
"filippo.io/age/internal/bech32"
"filippo.io/age/internal/format"
"filippo.io/hpke"
"golang.org/x/crypto/chacha20poly1305"
)
const pqLabel = "age-encryption.org/mlkem768x25519"
// HybridRecipient is the standard age public key. Messages encrypted to
// this recipient can be decrypted with the corresponding [HybridIdentity].
//
// This recipient is safe against future cryptographically-relevant quantum
// computers, and can only be used along with other post-quantum recipients.
//
// 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 HybridRecipient struct {
pk hpke.PublicKey
}
var _ Recipient = &HybridRecipient{}
// newHybridRecipient returns a new [HybridRecipient] from a raw HPKE public key.
func newHybridRecipient(publicKey []byte) (*HybridRecipient, error) {
pk, err := hpke.MLKEM768X25519().NewPublicKey(publicKey)
if err != nil {
return nil, errors.New("invalid MLKEM768-X25519 public key")
}
return &HybridRecipient{pk: pk}, nil
}
// ParseHybridRecipient returns a new [HybridRecipient] from a Bech32 public key
// encoding with the "age1pq1" prefix.
func ParseHybridRecipient(s string) (*HybridRecipient, error) {
t, k, err := bech32.Decode(s)
if err != nil {
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
}
if t != "age1pq" {
return nil, fmt.Errorf("malformed recipient %q: invalid type %q", s, t)
}
r, err := newHybridRecipient(k)
if err != nil {
return nil, fmt.Errorf("malformed recipient %q: %v", s, err)
}
return r, nil
}
func (r *HybridRecipient) Wrap(fileKey []byte) ([]*Stanza, error) {
s, _, err := r.WrapWithLabels(fileKey)
return s, err
}
// WrapWithLabels implements [RecipientWithLabels], returning a single
// "postquantum" label. This ensures a HybridRecipient can't be mixed with other
// recipients that would defeat its post-quantum security.
//
// To unsafely bypass this restriction, wrap HybridRecipient in a [Recipient]
// type that doesn't expose WrapWithLabels.
func (r *HybridRecipient) WrapWithLabels(fileKey []byte) ([]*Stanza, []string, error) {
enc, s, err := hpke.NewSender(r.pk, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(pqLabel))
if err != nil {
return nil, nil, fmt.Errorf("failed to set up HPKE sender: %v", err)
}
ct, err := s.Seal(nil, fileKey)
if err != nil {
return nil, nil, fmt.Errorf("failed to encrypt file key: %v", err)
}
l := &Stanza{
Type: "mlkem768x25519",
Args: []string{format.EncodeToString(enc)},
Body: ct,
}
return []*Stanza{l}, []string{"postquantum"}, nil
}
// String returns the Bech32 public key encoding of r.
func (r *HybridRecipient) String() string {
s, _ := bech32.Encode("age1pq", r.pk.Bytes())
return s
}
// HybridIdentity is the standard age private key, which can decrypt messages
// encrypted to the corresponding [HybridRecipient].
type HybridIdentity struct {
k hpke.PrivateKey
}
var _ Identity = &HybridIdentity{}
// newHybridIdentity returns a new [HybridIdentity] from a raw HPKE private key.
func newHybridIdentity(secretKey []byte) (*HybridIdentity, error) {
k, err := hpke.MLKEM768X25519().NewPrivateKey(secretKey)
if err != nil {
return nil, errors.New("invalid MLKEM768-X25519 secret key")
}
return &HybridIdentity{k: k}, nil
}
// GenerateHybridIdentity randomly generates a new [HybridIdentity].
func GenerateHybridIdentity() (*HybridIdentity, error) {
k, err := hpke.MLKEM768X25519().GenerateKey()
if err != nil {
return nil, fmt.Errorf("failed to generate post-quantum identity: %v", err)
}
return &HybridIdentity{k: k}, nil
}
// ParseHybridIdentity returns a new [HybridIdentity] from a Bech32 private key
// encoding with the "AGE-SECRET-KEY-PQ-1" prefix.
func ParseHybridIdentity(s string) (*HybridIdentity, error) {
t, k, err := bech32.Decode(s)
if err != nil {
return nil, fmt.Errorf("malformed secret key: %v", err)
}
if t != "AGE-SECRET-KEY-PQ-" {
return nil, fmt.Errorf("malformed secret key: unknown type %q", t)
}
r, err := newHybridIdentity(k)
if err != nil {
return nil, fmt.Errorf("malformed secret key: %v", err)
}
return r, nil
}
func (i *HybridIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
return multiUnwrap(i.unwrap, stanzas)
}
func (i *HybridIdentity) unwrap(block *Stanza) ([]byte, error) {
if block.Type != "mlkem768x25519" {
return nil, ErrIncorrectIdentity
}
if len(block.Args) != 1 {
return nil, errors.New("invalid mlkem768x25519 recipient block")
}
enc, err := format.DecodeString(block.Args[0])
if err != nil {
return nil, fmt.Errorf("failed to parse mlkem768x25519 recipient: %v", err)
}
if len(block.Body) != fileKeySize+chacha20poly1305.Overhead {
return nil, errIncorrectCiphertextSize
}
r, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte(pqLabel))
if err != nil {
// MLKEM768-X25519 does implicit rejection, so a mismatched key does not
// hit this error path, but is only detected later when trying to open.
return nil, fmt.Errorf("invalid mlkem768x25519 recipient: %v", err)
}
fileKey, err := r.Open(nil, block.Body)
if err != nil {
return nil, ErrIncorrectIdentity
}
return fileKey, nil
}
// Recipient returns the public [HybridRecipient] value corresponding to i.
func (i *HybridIdentity) Recipient() *HybridRecipient {
return &HybridRecipient{pk: i.k.PublicKey()}
}
// String returns the Bech32 private key encoding of i.
func (i *HybridIdentity) String() string {
b, _ := i.k.Bytes()
s, _ := bech32.Encode("AGE-SECRET-KEY-PQ-", b)
return strings.ToUpper(s)
}

View File

@@ -1,14 +1,13 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package age
import (
"crypto/hmac"
"crypto/sha256"
"errors"
"io"
"filippo.io/age/internal/format"
@@ -16,6 +15,7 @@ import (
"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 {
@@ -30,11 +30,21 @@ func aeadEncrypt(key, plaintext []byte) ([]byte, error) {
return aead.Seal(nil, nonce, plaintext, nil), nil
}
func aeadDecrypt(key, ciphertext []byte) ([]byte, error) {
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)
}

View File

@@ -1,18 +1,16 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package age_test
import (
"bytes"
"crypto/rand"
"io"
"testing"
"filippo.io/age"
"filippo.io/age/internal/format"
)
func TestX25519RoundTrip(t *testing.T) {
@@ -22,10 +20,6 @@ func TestX25519RoundTrip(t *testing.T) {
}
r := i.Recipient()
if r.Type() != i.Type() || r.Type() != "X25519" {
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
}
if r1, err := age.ParseX25519Recipient(r.String()); err != nil {
t.Fatal(err)
} else if r1.String() != r.String() {
@@ -41,15 +35,12 @@ func TestX25519RoundTrip(t *testing.T) {
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
b := &bytes.Buffer{}
(*format.Stanza)(block).Marshal(b)
t.Logf("%s", b.Bytes())
out, err := i.Unwrap(block)
out, err := i.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}
@@ -59,6 +50,67 @@ func TestX25519RoundTrip(t *testing.T) {
}
}
func TestHybridRoundTrip(t *testing.T) {
i, err := age.GenerateHybridIdentity()
if err != nil {
t.Fatal(err)
}
r := i.Recipient()
if r1, err := age.ParseHybridRecipient(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.ParseHybridIdentity(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 TestHybridMixingRestrictions(t *testing.T) {
x25519, err := age.GenerateX25519Identity()
if err != nil {
t.Fatal(err)
}
hybrid, err := age.GenerateHybridIdentity()
if err != nil {
t.Fatal(err)
}
// Hybrid recipients can be used together.
if _, err := age.Encrypt(io.Discard, hybrid.Recipient(), hybrid.Recipient()); err != nil {
t.Errorf("expected two hybrid recipients to work, got %v", err)
}
// Hybrid and X25519 recipients cannot be mixed.
if _, err := age.Encrypt(io.Discard, hybrid.Recipient(), x25519.Recipient()); err == nil {
t.Error("expected hybrid mixed with X25519 to fail")
}
if _, err := age.Encrypt(io.Discard, x25519.Recipient(), hybrid.Recipient()); err == nil {
t.Error("expected X25519 mixed with hybrid to fail")
}
}
func TestScryptRoundTrip(t *testing.T) {
password := "twitch.tv/filosottile"
@@ -72,23 +124,16 @@ func TestScryptRoundTrip(t *testing.T) {
t.Fatal(err)
}
if r.Type() != i.Type() || r.Type() != "scrypt" {
t.Errorf("invalid Type values: %v, %v", r.Type(), i.Type())
}
fileKey := make([]byte, 16)
if _, err := rand.Read(fileKey); err != nil {
t.Fatal(err)
}
block, err := r.Wrap(fileKey)
stanzas, err := r.Wrap(fileKey)
if err != nil {
t.Fatal(err)
}
b := &bytes.Buffer{}
(*format.Stanza)(block).Marshal(b)
t.Logf("%s", b.Bytes())
out, err := i.Unwrap(block)
out, err := i.Unwrap(stanzas)
if err != nil {
t.Fatal(err)
}

View File

@@ -1,15 +1,15 @@
// Copyright 2019 Google LLC
//
// Copyright 2019 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
// license that can be found in the LICENSE file.
package age
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"regexp"
"strconv"
"filippo.io/age/internal/format"
@@ -19,14 +19,15 @@ import (
const scryptLabel = "age-encryption.org/v1/scrypt"
// ScryptRecipient is a password-based recipient.
// 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.
// [HybridRecipient] or [X25519Recipient].
type ScryptRecipient struct {
password []byte
workFactor int
@@ -34,8 +35,6 @@ type ScryptRecipient struct {
var _ Recipient = &ScryptRecipient{}
func (*ScryptRecipient) Type() string { return "scrypt" }
// NewScryptRecipient returns a new ScryptRecipient with the provided password.
func NewScryptRecipient(password string) (*ScryptRecipient, error) {
if len(password) == 0 {
@@ -60,8 +59,10 @@ func (r *ScryptRecipient) SetWorkFactor(logN int) {
r.workFactor = logN
}
func (r *ScryptRecipient) Wrap(fileKey []byte) (*Stanza, error) {
salt := make([]byte, 16)
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
}
@@ -84,7 +85,30 @@ func (r *ScryptRecipient) Wrap(fileKey []byte) (*Stanza, error) {
}
l.Body = wrappedKey
return l, nil
return []*Stanza{l}, nil
}
// WrapWithLabels implements [age.RecipientWithLabels], returning a random
// label. This ensures a ScryptRecipient can't be mixed with other recipients
// (including other ScryptRecipients).
//
// Users reasonably expect files encrypted to a passphrase to be [authenticated]
// by that passphrase, i.e. for it to be impossible to produce a file that
// decrypts successfully with a passphrase without knowing it. If a file is
// encrypted to other recipients, those parties can produce different files that
// would break that expectation.
//
// [authenticated]: https://words.filippo.io/dispatches/age-authentication/
func (r *ScryptRecipient) WrapWithLabels(fileKey []byte) (stanzas []*Stanza, labels []string, err error) {
stanzas, err = r.Wrap(fileKey)
random := make([]byte, 16)
if _, err := rand.Read(random); err != nil {
return nil, nil, err
}
labels = []string{hex.EncodeToString(random)}
return
}
// ScryptIdentity is a password-based identity.
@@ -95,8 +119,6 @@ type ScryptIdentity struct {
var _ Identity = &ScryptIdentity{}
func (*ScryptIdentity) Type() string { return "scrypt" }
// NewScryptIdentity returns a new ScryptIdentity with the provided password.
func NewScryptIdentity(password string) (*ScryptIdentity, error) {
if len(password) == 0 {
@@ -122,9 +144,26 @@ func (i *ScryptIdentity) SetMaxWorkFactor(logN int) {
i.maxWorkFactor = logN
}
func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
func (i *ScryptIdentity) Unwrap(stanzas []*Stanza) ([]byte, error) {
for _, s := range stanzas {
if s.Type == "scrypt" && len(stanzas) != 1 {
return nil, errors.New("an scrypt recipient must be the only one")
}
}
for _, s := range stanzas {
if s.Type != "scrypt" {
continue
}
return i.unwrap(s)
}
return nil, fmt.Errorf("%w: file is not passphrase-encrypted", ErrIncorrectIdentity)
}
var digitsRe = regexp.MustCompile(`^[1-9][0-9]*$`)
func (i *ScryptIdentity) unwrap(block *Stanza) ([]byte, error) {
if block.Type != "scrypt" {
return nil, ErrIncorrectIdentity
return nil, errors.New("internal error: unwrap called on non-scrypt stanza")
}
if len(block.Args) != 2 {
return nil, errors.New("invalid scrypt recipient block")
@@ -133,9 +172,12 @@ func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("failed to parse scrypt salt: %v", err)
}
if len(salt) != 16 {
if len(salt) != scryptSaltSize {
return nil, errors.New("invalid scrypt recipient block")
}
if w := block.Args[1]; !digitsRe.MatchString(w) {
return nil, fmt.Errorf("scrypt work factor encoding invalid: %q", w)
}
logN, err := strconv.Atoi(block.Args[1])
if err != nil {
return nil, fmt.Errorf("failed to parse scrypt work factor: %v", err)
@@ -143,19 +185,30 @@ func (i *ScryptIdentity) Unwrap(block *Stanza) ([]byte, error) {
if logN > i.maxWorkFactor {
return nil, fmt.Errorf("scrypt work factor too large: %v", logN)
}
if logN <= 0 {
if logN <= 0 { // unreachable
return nil, fmt.Errorf("invalid scrypt work factor: %v", logN)
}
salt = append([]byte(scryptLabel), salt...)
k, err := scrypt.Key(i.password, salt, 1<<logN, 8, 1, chacha20poly1305.KeySize)
if err != nil {
if err != nil { // unreachable
return nil, fmt.Errorf("failed to generate scrypt hash: %v", err)
}
fileKey, err := aeadDecrypt(k, block.Body)
if err != nil {
return nil, ErrIncorrectIdentity
// 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 {
// Wrap [ErrIncorrectIdentity] so that multiple passphrases can be tried
// in sequence by passing multiple [ScryptIdentity] values to [Decrypt].
return nil, fmt.Errorf("%w: incorrect passphrase", ErrIncorrectIdentity)
}
return fileKey, nil
}

View File

@@ -0,0 +1,61 @@
// Command age-plugin-tagtest is a that decrypts files encrypted to fixed
// age1tag1... or age1tagpq1... recipients for testing purposes.
//
// It can be used with the "-j" flag:
//
// go install ./tag/internal/age-plugin-tagtest
// age -d -j tagtest file.age
package main
import (
"errors"
"fmt"
"log"
"os"
"filippo.io/age"
"filippo.io/age/plugin"
"filippo.io/age/tag/internal/tagtest"
)
const classicRecipient = "age1tag1qwe0kafsjrar4txm6heqnhpfuggzr0gvznz7fvygxrlq90u5mq2pysxtw6h"
const hybridRecipient = "age1tagpq14h4z7cks9sxftfc8tq4xektt4854ur9rv76tvujdvtzk2fmyywkvh9z2emz3x4epvhz7qdt2v7uksyyq2cdzf3k04ny0g5sc3u4heqh3r9v4cnwhfjw0a2azpgmnk9xk02wvywt5szcq6q3jvwsjxvkn3tsk52vqjczdcvc398ym4j6cvqas4w99gkgt7ur3fmt4g873phr23tgxw3f7wgsz9zxz7m8cp27vpq3h5vc8nssjemtr2etmtmqkg4fzn2u9x9zvtysuya5yrytgx482ftx9864h8a6pprarxd3d0qe8nw2at5ekg3tsahtef7kawasxjamyckw2ans6v933vuypcfrra32f89r2v72mka9hhc55s49xe2khfsq7w9r2zynuzfx4fg6v7jjncsc87rw2yy8qp8hr27edus6zw5xd6m3hax2nxhl2dys9792z3wp5c034sfkrxe86guj7pfdh7sytzrufl9euhuhyf9w6c7z2nwf7v5v8f2s4gplgvfx4jj4le22k2qn242qkqkcwx7llfyrct7jm2wcv0ytypeh6h93ezgtd7q6zr428qze3dec5jxlc5xxjetyephp42fljft3s02p0570kjwyfeyjcnks2vglkvyus5g9l4z6m0gf8wu22ygfm40028txwjlxvvgnn7c36z783c6tmc9k5nef8nucj6u3ustff5vhtnzhnscsvsz79wzrkv3sujtntx4wezucy6lp49flmnyydn3khk2xsesw0ekn4u44nzqw2g2rjyrrl7crshlzttgpe0jvqycjzp9kmtz23t3yu0w9j4n344nnrf88k2jqqfpjxte38pcn0epr879pqsuvajxrkmsas89pvfrzwcewneujn08guj5pvvrtn5hzzg2y6u4wwjqqxx4x8w65yc4dchf750dft8kgcttt2f6j0j5v8s7tkaua78tte7artdfar544vl0rau79h95mc4ghp887z82s6rq93txpkvan86n963kagqkldngnkjcn28zdrh38vdxj002zqs9mx7zjvg3ynzdfhfakkynt9fyqpaxpsdrsqrycuhw5ykwgjz6wldef7xtu6p689234hstxe7v8e5422f2dy8ystn57z3fvy9yfrm4t3lye6ejk5n6x8zqexmql7lx965xcxuuy38xzyt8j9qprnwgfqgx54l4tnjdpzdde6xgmwtnpkfwvyr7rkgnavvjn6a3e56wtvjx3evmhjjxvukpq5zqrj0s4sntkz3yeszs5dty8q0q6m7dgp6mjpvaer0c4343g72eycfqzkjupeaemh0n8e935hqs8fh3jgk7fzyxctzuqlx6d2q9jaf8r9wu4sjxj5w6ppw7m9c3hxrzpcv2uek3kxnndgf2hd99q9v2ux8pjkv29ntslvnvhy09dvcy9578rt89gf4cj4cu79zjxtlj3dpct6rjme02zj3qspsade96njkkufu9zuq2lk3qwvddpxjkqm2hnpqwck54zug7ctvkgvk325lwkg4q5rf73zkgys5e9y8jqc96ntdyl4r78lgtw4k5uljk5ttf46s3gc0rq0jwmddnxt875twwq92505zh3zkse5ag2dhjjxyfzkn7xv3j0kv9r3jzpvgep8fq6z8mar509u4fvnhvthp2ah0r45lsyq0mm6fwkcs30v8k9wzvgt6uvcty6qsjvarjs3htym69zu43m4jd3k4tllrr8c05v6p6spuhup4hkk2p9fp9lxafe3pntcn4nk83gzhjjpcjwyg7jcyz5uancu0fakgz27up7ymzp2xv3sqyqewkkqynskw9qkvysrncxj0cy7dt6q8dsseuwmc2urfmcvkykf82wfa54t85hqx8gywhmhzunm2x0d66a4pwl0xl78fhkces5dpq8pfnp35m5a3u8vdam64zx5s5x9cmnrx3zr066f4f8hlecqnq2fd5quw79ljg3q5nvs6ggmm4gkc"
func init() {
c := tagtest.NewClassicIdentity("age-plugin-tagtest").Recipient().String()
if c != classicRecipient {
log.Fatalf("unexpected classic recipient: %s", c)
}
h := tagtest.NewHybridIdentity("age-plugin-tagtest").Recipient().String()
if h != hybridRecipient {
log.Fatalf("unexpected hybrid recipient: %s", h)
}
}
func main() {
p, err := plugin.New("tagtest")
if err != nil {
log.Fatal(err)
}
p.HandleIdentity(func(b []byte) (age.Identity, error) {
if len(b) != 0 {
return nil, fmt.Errorf("unexpected identity data")
}
return &tagtestIdentity{}, nil
})
os.Exit(p.Main())
}
type tagtestIdentity struct{}
func (i *tagtestIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) {
classic := tagtest.NewClassicIdentity("age-plugin-tagtest")
if key, err := classic.Unwrap(ss); err == nil {
return key, nil
} else if !errors.Is(err, age.ErrIncorrectIdentity) {
return nil, err
}
hybrid := tagtest.NewHybridIdentity("age-plugin-tagtest")
return hybrid.Unwrap(ss)
}

View File

@@ -0,0 +1,152 @@
// Copyright 2025 The age Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tagtest
import (
"crypto/ecdh"
"crypto/subtle"
"fmt"
"filippo.io/age"
"filippo.io/age/internal/format"
"filippo.io/age/tag"
"filippo.io/hpke"
"filippo.io/nistec"
)
type ClassicIdentity struct {
k hpke.PrivateKey
}
var _ age.Identity = &ClassicIdentity{}
func NewClassicIdentity(seed string) *ClassicIdentity {
k, err := hpke.DHKEM(ecdh.P256()).DeriveKeyPair([]byte(seed))
if err != nil {
panic(fmt.Sprintf("failed to generate key: %v", err))
}
return &ClassicIdentity{k: k}
}
func (i *ClassicIdentity) Recipient() *tag.Recipient {
uncompressed := i.k.PublicKey().Bytes()
p, err := nistec.NewP256Point().SetBytes(uncompressed)
if err != nil {
panic(fmt.Sprintf("failed to parse public key: %v", err))
}
r, err := tag.NewClassicRecipient(p.BytesCompressed())
if err != nil {
panic(fmt.Sprintf("failed to create recipient: %v", err))
}
return r
}
func (i *ClassicIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) {
for _, s := range ss {
if s.Type != "p256tag" {
continue
}
if len(s.Args) != 2 {
return nil, fmt.Errorf("malformed stanza")
}
tagArg, err := format.DecodeString(s.Args[0])
if err != nil {
return nil, fmt.Errorf("malformed tag: %v", err)
}
if len(tagArg) != 4 {
return nil, fmt.Errorf("invalid tag length: %d", len(tagArg))
}
enc, err := format.DecodeString(s.Args[1])
if err != nil {
return nil, fmt.Errorf("malformed encapsulated key: %v", err)
}
if len(enc) != 65 {
return nil, fmt.Errorf("invalid encapsulated key length: %d", len(enc))
}
if len(s.Body) != 32 {
return nil, fmt.Errorf("invalid encrypted file key length: %d", len(s.Body))
}
expTag, err := i.Recipient().Tag(enc)
if err != nil {
return nil, fmt.Errorf("failed to compute tag: %v", err)
}
if subtle.ConstantTimeCompare(tagArg, expTag[:4]) != 1 {
return nil, age.ErrIncorrectIdentity
}
r, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("age-encryption.org/p256tag"))
if err != nil {
return nil, fmt.Errorf("failed to unwrap file key: %v", err)
}
return r.Open(nil, s.Body)
}
return nil, age.ErrIncorrectIdentity
}
type HybridIdentity struct {
k hpke.PrivateKey
}
var _ age.Identity = &HybridIdentity{}
func NewHybridIdentity(seed string) *HybridIdentity {
k, err := hpke.MLKEM768P256().DeriveKeyPair([]byte(seed))
if err != nil {
panic(fmt.Sprintf("failed to generate key: %v", err))
}
return &HybridIdentity{k: k}
}
func (i *HybridIdentity) Recipient() *tag.Recipient {
r, err := tag.NewHybridRecipient(i.k.PublicKey().Bytes())
if err != nil {
panic(fmt.Sprintf("failed to create recipient: %v", err))
}
return r
}
func (i *HybridIdentity) Unwrap(ss []*age.Stanza) ([]byte, error) {
for _, s := range ss {
if s.Type != "mlkem768p256tag" {
continue
}
if len(s.Args) != 2 {
return nil, fmt.Errorf("malformed stanza")
}
tagArg, err := format.DecodeString(s.Args[0])
if err != nil {
return nil, fmt.Errorf("malformed tag: %v", err)
}
if len(tagArg) != 4 {
return nil, fmt.Errorf("invalid tag length: %d", len(tagArg))
}
enc, err := format.DecodeString(s.Args[1])
if err != nil {
return nil, fmt.Errorf("malformed encapsulated key: %v", err)
}
if len(enc) != 1153 {
return nil, fmt.Errorf("invalid encapsulated key length: %d", len(enc))
}
if len(s.Body) != 32 {
return nil, fmt.Errorf("invalid encrypted file key length: %d", len(s.Body))
}
expTag, err := i.Recipient().Tag(enc)
if err != nil {
return nil, fmt.Errorf("failed to compute tag: %v", err)
}
if subtle.ConstantTimeCompare(tagArg, expTag[:4]) != 1 {
return nil, age.ErrIncorrectIdentity
}
r, err := hpke.NewRecipient(enc, i.k, hpke.HKDFSHA256(), hpke.ChaCha20Poly1305(), []byte("age-encryption.org/mlkem768p256tag"))
if err != nil {
return nil, fmt.Errorf("failed to unwrap file key: %v", err)
}
return r.Open(nil, s.Body)
}
return nil, age.ErrIncorrectIdentity
}

Some files were not shown because too many files have changed in this diff Show More