summaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorGab Virebent <gabriel1@virebent.art>2026-07-07 16:24:30 +0200
committerGab Virebent <gabriel1@virebent.art>2026-07-07 16:24:30 +0200
commitcc21fb4978b54d643d8fb3f5a454e62da4b396c2 (patch)
tree3050c8d4b68e28989cbe611001f8c6e961a08e51 /cmd
downloadqpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.tar.gz
qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.tar.xz
qpass-cc21fb4978b54d643d8fb3f5a454e62da4b396c2.zip
Initial commitHEADmain
Diffstat (limited to 'cmd')
-rw-r--r--cmd/qpass/main.go264
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)
+}