diff options
| author | Gab Virebent <gabriel1@virebent.art> | 2026-07-07 16:24:30 +0200 |
|---|---|---|
| committer | Gab Virebent <gabriel1@virebent.art> | 2026-07-07 16:24:30 +0200 |
| commit | cc21fb4978b54d643d8fb3f5a454e62da4b396c2 (patch) | |
| tree | 3050c8d4b68e28989cbe611001f8c6e961a08e51 /cmd | |
| download | qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.tar.gz qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.tar.xz qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.zip | |
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/qpass/main.go | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/cmd/qpass/main.go b/cmd/qpass/main.go new file mode 100644 index 0000000..8063ab8 --- /dev/null +++ b/cmd/qpass/main.go @@ -0,0 +1,264 @@ +package main + +import ( + crand "crypto/rand" + "flag" + "fmt" + "io" + "os" + "strings" + + "quantum-passgen/internal/entropy" + "quantum-passgen/internal/passgen" +) + +func main() { + if len(os.Args) > 1 && os.Args[1] == "entropy" { + runEntropy(os.Args[2:]) + return + } + runGenerate(os.Args[1:]) +} + +func runGenerate(args []string) { + opts := passgen.DefaultOptions() + fs := flag.NewFlagSet("qpass", flag.ExitOnError) + count := fs.Int("count", 1, "number of passwords to generate") + stats := fs.Bool("stats", false, "print alphabet size and estimated entropy to stderr") + rngMode := fs.String("rng", "os", "random engine: os or hybrid") + entropyFile := fs.String("entropy-file", "", "external entropy file/device; use - for stdin") + entropyDevice := fs.String("entropy-device", "", "external entropy device path") + entropyStdin := fs.Bool("entropy-stdin", false, "read external entropy from stdin") + quantumFile := fs.String("quantum-file", "", "deprecated alias for -entropy-file") + requireExternal := fs.Bool("require-external", false, "fail if external entropy cannot provide enough bytes") + replaceGlobal := fs.Bool("replace-global-rand", false, "advanced: install selected engine as crypto/rand.Reader for this process") + + fs.IntVar(&opts.Length, "length", opts.Length, "password length") + fs.BoolVar(&opts.UseLower, "lower", opts.UseLower, "include lowercase letters") + fs.BoolVar(&opts.UseUpper, "upper", opts.UseUpper, "include uppercase letters") + fs.BoolVar(&opts.UseDigits, "digits", opts.UseDigits, "include digits") + fs.BoolVar(&opts.UseSymbols, "symbols", opts.UseSymbols, "include symbols") + fs.Parse(args) + + if *count <= 0 { + exitf("count must be positive") + } + + random, hybrid, cleanup := configureRandom(randomConfig{ + mode: *rngMode, + entropyFile: *entropyFile, + entropyDevice: *entropyDevice, + entropyStdin: *entropyStdin, + quantumFile: *quantumFile, + requireExternal: *requireExternal, + rngFlagSet: flagWasSet(fs, "rng"), + }) + defer cleanup() + opts.Random = random + + if *replaceGlobal { + restore := entropy.InstallGlobal(random) + defer restore() + } + + for i := 0; i < *count; i++ { + password, err := passgen.Generate(opts) + if err != nil { + exitf("%v", err) + } + fmt.Println(password) + } + + if *stats { + alphabetSize := passgen.AlphabetSize(opts) + entropyBits, err := passgen.EntropyBits(opts.Length, alphabetSize) + if err != nil { + exitf("%v", err) + } + fmt.Fprintf(os.Stderr, "alphabet=%d estimated_entropy=%.1f bits\n", alphabetSize, entropyBits) + } + + if hybrid != nil && hybrid.Degraded() { + fmt.Fprintf(os.Stderr, "qpass: warning: external entropy degraded; continued with OS CSPRNG: %v\n", hybrid.DegradeError()) + } +} + +type randomConfig struct { + mode string + entropyFile string + entropyDevice string + entropyStdin bool + quantumFile string + requireExternal bool + rngFlagSet bool +} + +func configureRandom(cfg randomConfig) (io.Reader, *entropy.HybridReader, func()) { + cleanup := func() {} + source, closeSource := openExternalSource(cfg) + if closeSource != nil { + cleanup = closeSource + } + + mode := strings.ToLower(strings.TrimSpace(cfg.mode)) + if mode == "" { + mode = "os" + } + if source != nil && mode == "os" && !cfg.rngFlagSet { + mode = "hybrid" + } + + switch mode { + case "os": + if source != nil { + cleanup() + exitf("-rng os cannot be combined with an external entropy source") + } + return crand.Reader, nil, cleanup + case "hybrid": + if source == nil { + cleanup() + exitf("-rng hybrid requires -entropy-file, -entropy-device, -entropy-stdin, or -quantum-file") + } + reader, err := entropy.NewHybridReader(crand.Reader, source, entropy.HybridOptions{ + RequireExternal: cfg.requireExternal, + }) + if err != nil { + cleanup() + exitf("configure hybrid entropy: %v", err) + } + return reader, reader, cleanup + default: + cleanup() + exitf("unknown rng mode %q", cfg.mode) + return nil, nil, cleanup + } +} + +func openExternalSource(cfg randomConfig) (io.Reader, func()) { + sources := 0 + sourceLabel := "" + sourcePath := "" + + if cfg.quantumFile != "" { + sources++ + sourceLabel = "-quantum-file" + sourcePath = cfg.quantumFile + } + if cfg.entropyFile != "" { + sources++ + sourceLabel = "-entropy-file" + sourcePath = cfg.entropyFile + } + if cfg.entropyDevice != "" { + sources++ + sourceLabel = "-entropy-device" + sourcePath = cfg.entropyDevice + } + if cfg.entropyStdin { + sources++ + sourceLabel = "-entropy-stdin" + sourcePath = "-" + } + + if sources == 0 { + return nil, nil + } + if sources > 1 { + exitf("choose only one external entropy source") + } + if sourcePath == "-" { + return os.Stdin, nil + } + + file, err := os.Open(sourcePath) + if err != nil { + exitf("open %s: %v", sourceLabel, err) + } + return file, func() { + if err := file.Close(); err != nil { + fmt.Fprintf(os.Stderr, "qpass: warning: close %s: %v\n", sourceLabel, err) + } + } +} + +func flagWasSet(fs *flag.FlagSet, name string) bool { + found := false + fs.Visit(func(f *flag.Flag) { + if f.Name == name { + found = true + } + }) + return found +} + +func runEntropy(args []string) { + if len(args) == 0 { + exitf("usage: qpass entropy status|test") + } + + switch args[0] { + case "status": + runEntropyStatus(args[1:]) + case "test": + runEntropyTest(args[1:]) + default: + exitf("unknown entropy command %q", args[0]) + } +} + +func runEntropyStatus(args []string) { + fs := flag.NewFlagSet("qpass entropy status", flag.ExitOnError) + fs.Parse(args) + + var b [1]byte + if _, err := io.ReadFull(crand.Reader, b[:]); err != nil { + exitf("OS CSPRNG unavailable: %v", err) + } + + fmt.Println("os_csprng=available") + fmt.Println("external_source=not_configured") + fmt.Println("global_override=disabled_by_default") +} + +func runEntropyTest(args []string) { + fs := flag.NewFlagSet("qpass entropy test", flag.ExitOnError) + entropyFile := fs.String("entropy-file", "", "external entropy file/device; use - for stdin") + entropyDevice := fs.String("entropy-device", "", "external entropy device path") + entropyStdin := fs.Bool("entropy-stdin", false, "read external entropy from stdin") + quantumFile := fs.String("quantum-file", "", "deprecated alias for -entropy-file") + requireExternal := fs.Bool("require-external", false, "fail if external entropy cannot provide enough bytes") + bytesToRead := fs.Int("bytes", 64, "number of bytes to read without printing them") + fs.Parse(args) + + if *bytesToRead <= 0 { + exitf("bytes must be positive") + } + + random, hybrid, cleanup := configureRandom(randomConfig{ + mode: "hybrid", + entropyFile: *entropyFile, + entropyDevice: *entropyDevice, + entropyStdin: *entropyStdin, + quantumFile: *quantumFile, + requireExternal: *requireExternal, + }) + defer cleanup() + + buf := make([]byte, *bytesToRead) + if _, err := io.ReadFull(random, buf); err != nil { + exitf("entropy test failed: %v", err) + } + + fmt.Printf("mode=hybrid\n") + fmt.Printf("read_bytes=%d\n", len(buf)) + fmt.Printf("external_degraded=%t\n", hybrid.Degraded()) + if hybrid.Degraded() { + fmt.Printf("degrade_error=%v\n", hybrid.DegradeError()) + } +} + +func exitf(format string, args ...any) { + fmt.Fprintf(os.Stderr, "qpass: "+format+"\n", args...) + os.Exit(1) +} |
