Compare commits

..

23 Commits
v0.1.1 ... main

Author SHA1 Message Date
akulij
b47de3e1b6 implement hex/raw input 2025-03-03 00:18:43 +00:00
akulij
9cde36c5c2 rename 'sum' variable that contains hashed password to more appropriate name 'secretKey' 2025-03-03 00:18:43 +00:00
akulij
181ac703ae add --input-type flag 2025-03-03 00:18:37 +00:00
akulij
b6be7799b9 README: fix wrong todo tasks formating 2025-02-28 04:21:59 +00:00
akulij
ecaa3c8a9a README: remove unnecessary decription header 2025-02-28 01:08:30 +00:00
akulij
f921077964 add option to build and run this repo using nix 2025-02-28 00:15:27 +00:00
akulij
c9cb1bb2d8 README: mark task to add sigpipe handle as done 2025-02-27 22:19:28 +00:00
akulij
5184859c54 handle sigpipe for user friendliness 2025-02-27 22:18:18 +00:00
akulij
bd018136d6 delete sigint handler since it doesn't do anything 2025-02-27 22:13:45 +00:00
akulij
d924708a1c fix: signal handler setter doesn't need to be ran asynchronously 2025-02-27 22:12:13 +00:00
akulij
5f13d1d409 change password input method
in previous method if press ctrl-c during password enter terminal echo
kept off. It was possible to fix it to set `stty echo` in sigint
handler, but this solution is more elegant
2025-02-27 22:07:41 +00:00
akulij
d5192456ce handle sigint 2025-02-27 22:07:30 +00:00
akulij
3161afeb3b README: add task to accept another input as hash 2025-02-27 21:36:40 +00:00
akulij
a873717f17 move flags parsing out of main function 2025-02-27 21:27:36 +00:00
akulij
c3c506e206 move cmd utility location to follow projects name 2025-02-27 20:02:49 +00:00
akulij
6ded6ec767 rename project from age-gen-passphrase to age-passgen 2025-02-27 20:01:44 +00:00
akulij
5522a1265e FIX: missing --entropy-level flag in --help 2025-02-27 19:52:00 +00:00
akulij
a9943d2358 use entropy level for password 2025-02-27 03:02:39 +00:00
akulij
7e28ea3c94 add --entropy-level flag 2025-02-27 01:51:29 +00:00
akulij
dbe48ec586 fix usage info 2025-02-27 00:00:48 +00:00
akulij
adc6b32c3b check raw/verbose output todo as done 2025-02-26 23:41:24 +00:00
akulij
14439a6479 use flags --output and --raw-output
Now logic for output looks like:
password from stdin (stays as was)
keys: private to stdout OR private to file (via -o flag) and public to stdout
stderr for errors and password prompt
2025-02-26 23:22:47 +00:00
akulij
3b1079985a add usage flags 2025-02-26 23:07:36 +00:00
5 changed files with 341 additions and 102 deletions

View File

@ -1,7 +1,6 @@
# Generate age keys from passphrase # Generate age keys from passphrase
## Description This utility (age-passgen) generates secret and public keys (into stdout) from your entered passphrase or piped stdin
This utility (age-gen-passphrase) generates secret and public keys (into stdout) from your entered passphrase or piped stdin
Strong password highly recomended Strong password highly recomended
## Password selection ## Password selection
@ -10,5 +9,6 @@ Exact amount of required characters can be calculated by formula: $\lceil 256 /
## TODO ## TODO
[ ] piped/terminal output as raw/verbose - [X] piped/terminal output as raw/verbose
[ ] handle broken pipe signal since program will so much depend on pipes - [X] handle broken pipe signal since program will so much depend on pipes
- [ ] option to read hex number that will be used instead of hash of password (usefull if user already has sha 256 hash of smth or want to use something else as input instead)

View File

@ -1,97 +0,0 @@
package main
import (
"errors"
"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
}
func main() {
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 !term.IsTerminal(int(os.Stdout.Fd())) {
fmt.Fprintf(os.Stderr, "Public key: %s\n", k.Recipient())
}
err = writeSecretKey(os.Stdout, k)
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.Print("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) error {
var err error
_, 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)
if err != nil {
return err
}
return nil
}
// 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...)
}

322
cmd/age-passgen/main.go Normal file
View File

@ -0,0 +1,322 @@
package main
import (
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"slices"
"strconv"
"strings"
"syscall"
"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-passgen [-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)
--entropy-level VALUE Manages required strenght of password (more info down below)
--input-type TYPE Type of input from stdin. Can be 'password' (default), 'hash', 'raw'
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
`
type InputType int
const (
InputPassword InputType = iota
InputHash
InputRaw
)
type Flags struct {
RawOutput bool
OutputFile string
EntropyLevel int
InputType InputType
}
func main() {
setSystemSignalHandlers()
flags, err := parseFlags()
if err != nil {
errorf("error while parsing arguments: %s\n", err)
}
var secretKey [curve25519.ScalarSize]byte
if flags.InputType == InputPassword {
secretKey, err = getInputPassword(flags.EntropyLevel)
if err != nil {
errorf("Failed to get password, error: %s\n", err)
}
} else if flags.InputType == InputHash {
secretKey, err = getInputHash()
if err != nil {
errorf("Failed to read hash, error: %s\n", err)
}
} else if flags.InputType == InputRaw {
secretKey, err = getInputRaw()
if err != nil {
errorf("Failed to read raw data, error: %s\n", err)
}
} else {
errorf("No such input type implemented!!!")
}
k, err := newX25519IdentityFromScalar(secretKey[:])
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 flags.OutputFile != "" {
if !flags.RawOutput {
fmt.Printf("Public key: %s\n", k.Recipient())
} else {
fmt.Printf("%s", k.Recipient())
}
}
output := os.Stdout
if flags.OutputFile != "" {
output, err = os.Create(flags.OutputFile)
if err != nil {
errorf("failed to create output file, error: %s", err)
}
}
err = writeSecretKey(output, k, !flags.RawOutput)
if err != nil {
fmt.Printf("Failed to write secret key to file, error: %s\n", err)
}
}
func getInputPassword(entropyLevel int) ([curve25519.ScalarSize]byte, error) {
passbytes, err := getPasswordBytes()
if err != nil {
return [curve25519.ScalarSize]byte{}, err
}
valid := isEntropyValid(passbytes, entropyLevel)
if !valid {
return [curve25519.ScalarSize]byte{}, errors.New("You should choose stroger password!!! (or change entropy level, read more with --help)\n")
}
return sha256.Sum256(passbytes), nil
}
func getInputHash() ([curve25519.ScalarSize]byte, error) {
hashStringBytes, err := getPasswordBytes()
if err != nil {
return [curve25519.ScalarSize]byte{}, err
}
hashString := strings.TrimSpace(string(hashStringBytes))
passbytes, err := hex.DecodeString(hashString)
if err != nil {
fmt.Printf("HEXSTR:%s|\n", hashString)
return [curve25519.ScalarSize]byte{}, errors.New(fmt.Sprintf("Unable to decode hash, error: %s\n", err))
}
if len(passbytes) != curve25519.ScalarSize {
return [curve25519.ScalarSize]byte{}, errors.New(fmt.Sprintf("Wrong input lenght of sha256 hash! (may be it is not a hash at all) Expected %d bytes, got: %d\n", curve25519.ScalarSize, len(passbytes)))
}
// making `possibly` stack allocated out of the one in heap
var key [curve25519.ScalarSize]byte
copy(key[:], passbytes)
return key, nil
}
func getInputRaw() ([curve25519.ScalarSize]byte, error) {
passbytes, err := getPasswordBytes()
if err != nil {
return [curve25519.ScalarSize]byte{}, err
}
if len(passbytes) != curve25519.ScalarSize {
return [curve25519.ScalarSize]byte{}, errors.New(fmt.Sprintf("Wrong amount of entered data! Expected %d bytes, got: %d\n", curve25519.ScalarSize, len(passbytes)))
}
// making `possibly` stack allocated out of the one in heap
var key [curve25519.ScalarSize]byte
copy(key[:], passbytes)
return key, nil
}
func setSystemSignalHandlers() {
go handleSigpipe()
}
func handleSigpipe() {
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGPIPE)
<-c
errorf("Recieved SIGPIPE. Check if your programs that give input or recieve input do not stops before this one")
os.Exit(1)
}
func parseFlags() (*Flags, error) {
log.SetFlags(0)
flag.Usage = func() { fmt.Fprintf(os.Stderr, "%s", usage) }
var (
rawOutput bool
outputFile string
entropyLevel string
inputType 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.StringVar(&inputType, "input-type", "password", "Type of input from stdin. Can be 'password' (default), 'hash', 'raw'")
flag.Parse()
eLevel, err := parseEntropyLevel(entropyLevel)
if err != nil {
return nil, err
}
iType, err := parseInputType(inputType)
if err != nil {
return nil, err
}
return &Flags{
RawOutput: rawOutput,
OutputFile: outputFile,
EntropyLevel: eLevel,
InputType: iType,
}, nil
}
func parseInputType(inputType string) (InputType, error) {
m := map[string]InputType{
"password": InputPassword,
"hash": InputHash,
"raw": InputRaw,
}
iType, ok := m[inputType]
if !ok {
return InputPassword, errors.New("wrong input type")
}
return iType, nil
}
func parseEntropyLevel(entropyLevel string) (int, error) {
if i, err := strconv.Atoi(entropyLevel); err == nil {
if i == 0 {
return 0, errors.New("No such entropy level `0`, try `stupid`")
} else if 1 <= i && i <= 4 {
return i, nil
} else {
return 0, errors.New("Wrong entropy level `" + strconv.Itoa(i) + "`, level should be within range 1 to 4")
}
}
//if it is not number, let's try words
levelWords := [...]string{"stupid", "verylow", "low", "medium", "high"}
idx := slices.Index(levelWords[:], entropyLevel)
if idx == -1 {
return 0, errors.New("Such entropy level does not exists: " + entropyLevel + "\nMay be misstyped?")
}
return idx, nil
}
func isEntropyValid(passbytes []byte, entropyLevel int) bool {
lengthsMap := [...]int{0, 8, 12, 22, 44}
return len(passbytes) >= lengthsMap[entropyLevel]
}
func getPasswordBytes() ([]byte, error) {
if term.IsTerminal(int(os.Stdin.Fd())) {
oldState, err := term.MakeRaw(0)
defer term.Restore(0, oldState)
screen := struct {
io.Reader
io.Writer
}{os.Stdin, os.Stdout}
t := term.NewTerminal(screen, "")
pass, err := t.ReadPassword("Enter pass: ")
return []byte(pass), 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-passgen ERROR: "+format, v...)
}

View File

@ -8,6 +8,20 @@
let pkgs = nixpkgs.legacyPackages.${system}; in let pkgs = nixpkgs.legacyPackages.${system}; in
{ {
devShells.default = import ./shell.nix { inherit pkgs; }; devShells.default = import ./shell.nix { inherit pkgs; };
packages.default = pkgs.buildGoModule {
pname = "age-passgen";
version = "unversioned";
src = ./.;
vendorHash = "sha256-Y6R8c9PzRq0tJ0b06f0LuFfrdFvxQ7h/86a6gg6UOro=";
};
apps.default = {
type = "app";
program = "${self.packages.${system}.default}/bin/age-passgen";
};
} }
); );
} }

2
go.mod
View File

@ -1,4 +1,4 @@
module github.com/akulij/age-gen-passphrase module github.com/akulij/age-passgen
go 1.22.2 go 1.22.2