// Copyright 2019 The age Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "flag" "fmt" "io" "log" "os" "runtime/debug" "time" "filippo.io/age" "golang.org/x/term" ) const usage = `Usage: age-keygen [-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. When generating a key, if the output is not going to a terminal, 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) } 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 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") } var converted []byte if convertFlag { 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 } converted = convert(in) } 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 convertFlag { writef(out, "%s", converted) } 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, 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 !term.IsTerminal(int(out.Fd())) { fmt.Fprintf(os.Stderr, "Public key: %s\n", r) } writef(out, "# created: %s\n", time.Now().Format(time.RFC3339)) writef(out, "# public key: %s\n", r) writef(out, "%s\n", i) } func writef(out io.Writer, format string, v ...any) { if _, err := fmt.Fprintf(out, format, v...); err != nil { errorf("failed to write output: %v", err) } } func convert(in io.Reader) []byte { 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") } var out []byte for _, id := range ids { switch id := id.(type) { case *age.X25519Identity: out = append(out, id.Recipient().String()...) case *age.HybridIdentity: out = append(out, id.Recipient().String()...) default: errorf("internal error: unexpected identity type: %T", id) } out = append(out, '\n') } return out } 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) }