mirror of
https://github.com/luxfi/age.git
synced 2026-07-27 03:39:38 +00:00
Both hybrid PQ KEMs are first-class; callers choose at runtime. Auto-detect from Bech32 prefix (zero-config at decode): age1pq1… → HPKE MLKEM768X25519 (existing, kept for compat) age1xw1… → X-Wing (IETF draft-connolly-cfrg-xwing-kem-10) Configurable at keygen: • GeneratePQIdentity(kem PQKemType) picks the KEM at generation time • Empty kem → AGE_PQ_KEM env var → default PQKemXWing • SupportedPQKems() enumerates options for CLIs Real X-Wing construction (unchanged from prior commit — verified): SHA3-256(ssM || ssX || ctX || pkX || XWingLabel) XWingLabel = 0x5c2e2f2f5e5c (6 bytes) Seed 32 · Pub 1216 · CT 1120 · SS 32 HPKE KEM codepoint 25722 Downgrade protection: "postquantum" label on WrapWithLabels prevents mixing X-Wing with non-PQ recipients (same rule as HybridRecipient). Migration path: age supports multi-recipient encryption natively, so operators can encrypt to both age1pq1… and age1xw1… during transition: age -r age1pq1aaa… -r age1xw1bbb… -o file.age plaintext Tests: 12 X-Wing tests pass including combiner spec vector, round-trip, multi-recipient, cross-KEM (X-Wing file decrypted by X-Wing only).
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
// 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],
|
|
// *[HybridIdentity], or *[XWingIdentity], 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-XW-1"):
|
|
return ParseXWingIdentity(arg)
|
|
case strings.HasPrefix(arg, "AGE-SECRET-KEY-PQ-1"):
|
|
return ParseHybridIdentity(arg)
|
|
case strings.HasPrefix(arg, "AGE-SECRET-KEY-1"):
|
|
return ParseX25519Identity(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],
|
|
// *[HybridRecipient], or *[XWingRecipient], 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, "age1xw1"):
|
|
return ParseXWingRecipient(arg)
|
|
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)
|
|
}
|
|
}
|