package main import ( "errors" "flag" "fmt" "io" "log" "os" "time" "unsafe" "golang.org/x/term" "crypto/sha256" "golang.org/x/crypto/curve25519" "filippo.io/age" ) type X25519Identity struct { secretKey, ourPublicKey []byte } const usage = `Usage: age-gen-passphrase [-o OUTPUT] [--raw-input] Options: -o, --output OUTPUT Write the result to the file at path OUTPUT. --raw-output Print stripped keys (without additional text or comments) Mostly similar to age-keygen Required password strenght can be changes via --entropy-level flag. Possible values high, medium (default), low, verylow, stupid and numbers from 1 to 4 (inclusive). Each word or number is mapped following this list: - high (or 4) - 44 characters - medium (or 3) - 22 characters - low (or 2) - 12 characters - verylow (or 1) - 8 characters - stupid - no limit ` func main() { log.SetFlags(0) flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s", usage) } var ( rawOutput bool outputFile string entropyLevel string ) flag.BoolVar(&rawOutput, "raw-output", false, "Print stripped keys (without additional text or comments)") flag.StringVar(&outputFile, "o", "", "Write the result to the file at path OUTPUT") flag.StringVar(&outputFile, "output", "", "Write the result to the file at path OUTPUT") flag.StringVar(&entropyLevel, "entropy-level", "medium", "Manages required strenght of password. Read more in --help") flag.Parse() passbytes, err := getPasswordBytes() if err != nil { errorf("Failed to get password, error: %s\n", err) } sum := sha256.Sum256(passbytes) k, err := newX25519IdentityFromScalar(sum[:]) if err != nil { errorf("internal error: %v", err) } // if user is not seeing private keyfile, which also contains public key, // also duplicate public key it to stderr, // but if user sees public key via stdout, no need for duplication if outputFile != "" { if !rawOutput { fmt.Printf("Public key: %s\n", k.Recipient()) } else { fmt.Printf("%s", k.Recipient()) } } output := os.Stdout if outputFile != "" { output, err = os.Create(outputFile) if err != nil { errorf("failed to create output file, error: %s", err) } } err = writeSecretKey(output, k, !rawOutput) if err != nil { fmt.Printf("Failed to write secret key to file, error: %s\n", err) } } func getPasswordBytes() ([]byte, error) { if term.IsTerminal(int(os.Stdin.Fd())) { fmt.Fprintf(os.Stderr, "Enter password: ") passbytes, err := term.ReadPassword(int(os.Stdin.Fd())) fmt.Println() return passbytes, err } else { return io.ReadAll(os.Stdin) } } func writeSecretKey(f *os.File, key *age.X25519Identity, verbose bool) error { var err error if verbose { _, err = fmt.Fprintf(f, "# created: %s\n", time.Now().Format(time.RFC3339)) if err != nil { return err } _, err = fmt.Fprintf(f, "# public key: %s\n", key.Recipient()) if err != nil { return err } _, err = fmt.Fprintf(f, "%s\n", key) } else { _, err = fmt.Fprintf(f, "%s", key) } return err } // almost a copy of private function in age/x25519.go func newX25519IdentityFromScalar(secretKey []byte) (*age.X25519Identity, error) { if len(secretKey) != curve25519.ScalarSize { return nil, errors.New("invalid X25519 secret key") } i := &X25519Identity{ secretKey: make([]byte, curve25519.ScalarSize), } copy(i.secretKey, secretKey) i.ourPublicKey, _ = curve25519.X25519(i.secretKey, curve25519.Basepoint) return (*age.X25519Identity)(unsafe.Pointer(i)), nil } func errorf(format string, v ...interface{}) { log.Fatalf("age-gen-passphrase ERROR: "+format, v...) }