├── go.mod ├── README.md ├── go.sum └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/htpasswd 2 | 3 | go 1.25.0 4 | 5 | require ( 6 | github.com/mattn/go-tty v0.0.7 7 | github.com/nathanaelle/password/v2 v2.0.1 8 | golang.org/x/crypto v0.43.0 9 | ) 10 | 11 | require ( 12 | github.com/mattn/go-isatty v0.0.20 // indirect 13 | golang.org/x/sys v0.38.0 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # htpasswd 2 | 3 | Re-write of htpasswd in Go 4 | 5 | ## Usage 6 | 7 | ``` 8 | Usage: 9 | htpasswd [-cimBdpsDv] [-C cost] passwordfile username 10 | htpasswd -b[cmBdpsDv] [-C cost] passwordfile username password 11 | 12 | htpasswd -n[imBdps] [-C cost] username 13 | htpasswd -nb[mBdps] [-C cost] username password 14 | -c Create a new file. 15 | -n Don't update file; display results on stdout. 16 | -b Use the password from the command line rather than prompting for it. 17 | -i Read password from stdin without verification (for script usage). 18 | -m Force MD5 encryption of the password (default). 19 | -B Force bcrypt encryption of the password (very secure). 20 | -C Set the computing time used for the bcrypt algorithm 21 | (higher is more secure but slower, default: 5, valid: 4 to 31). 22 | -d Force CRYPT encryption of the password (8 chars max, insecure). 23 | -s Force SHA encryption of the password (insecure). 24 | -p Do not encrypt the password (plaintext, insecure). 25 | -D Delete the specified user. 26 | -v Verify password for the specified user. 27 | On other systems than Windows and NetWare the '-p' flag will probably not work. 28 | The SHA algorithm does not use a salt and is less secure than the MD5 algorithm. 29 | ``` 30 | 31 | ## Installation 32 | 33 | ``` 34 | $ go get github.com/mattn/htpasswd 35 | ``` 36 | 37 | ## License 38 | 39 | MIT 40 | 41 | ## Author 42 | 43 | Yasuhiro Matsumoto (a.k.a. mattn) 44 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 2 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 3 | github.com/mattn/go-tty v0.0.7 h1:KJ486B6qI8+wBO7kQxYgmmEFDaFEE96JMBQ7h400N8Q= 4 | github.com/mattn/go-tty v0.0.7/go.mod h1:f2i5ZOvXBU/tCABmLmOfzLz9azMo5wdAaElRNnJKr+k= 5 | github.com/nathanaelle/password/v2 v2.0.1 h1:ItoCTdsuIWzilYmllQPa3DR3YoCXcpfxScWLqr8Ii2s= 6 | github.com/nathanaelle/password/v2 v2.0.1/go.mod h1:eaoT+ICQEPNtikBRIAatN8ThWwMhVG+r1jTw60BvPJk= 7 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 8 | golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 9 | golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= 10 | golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= 11 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 12 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 13 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 14 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 15 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 16 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 17 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 18 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/rand" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "strconv" 11 | "strings" 12 | 13 | "golang.org/x/crypto/bcrypt" 14 | 15 | "github.com/mattn/go-tty" 16 | "github.com/nathanaelle/password/v2" 17 | ) 18 | 19 | func randomBytes(n int) ([]byte, error) { 20 | b := make([]byte, n) 21 | r, err := rand.Read(b) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return b[:r], nil 26 | } 27 | 28 | func randomString(n int) (string, error) { 29 | const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" 30 | b, err := randomBytes(n) 31 | if err != nil { 32 | return "", err 33 | } 34 | for i, c := range b { 35 | b[i] = letters[c%byte(len(letters))] 36 | } 37 | return string(b), nil 38 | } 39 | 40 | func init() { 41 | password.Register(password.APR1, password.BCRYPT) 42 | } 43 | 44 | func main() { 45 | var create bool 46 | var dontupdate bool 47 | var useargument bool 48 | var noverify bool 49 | var forcemd5 bool = true 50 | var forcebcrypt bool 51 | var forcesha256 bool 52 | var bcryptcost int = bcrypt.DefaultCost 53 | var noencrypt bool 54 | var deleteuser bool 55 | var verifyuser bool 56 | 57 | usage := func() { 58 | fmt.Fprint(os.Stderr, `Usage: 59 | htpasswd [-cimBdpsDv] [-C cost] passwordfile username 60 | htpasswd -b[cmBdpsDv] [-C cost] passwordfile username password 61 | 62 | htpasswd -n[imBdps] [-C cost] username 63 | htpasswd -nb[mBdps] [-C cost] username password 64 | -c Create a new file. 65 | -n Don't update file; display results on stdout. 66 | -b Use the password from the command line rather than prompting for it. 67 | -i Read password from stdin without verification (for script usage). 68 | -m Force MD5 encryption of the password (default). 69 | -B Force bcrypt encryption of the password (very secure). 70 | -C Set the computing time used for the bcrypt algorithm 71 | (higher is more secure but slower, default: 5, valid: 4 to 31). 72 | -d Force CRYPT encryption of the password (8 chars max, insecure). 73 | -s Force SHA encryption of the password (insecure). 74 | -p Do not encrypt the password (plaintext, insecure). 75 | -D Delete the specified user. 76 | -v Verify password for the specified user. 77 | On other systems than Windows and NetWare the '-p' flag will probably not work. 78 | The SHA algorithm does not use a salt and is less secure than the MD5 algorithm. 79 | `) 80 | } 81 | 82 | args := []string{} 83 | for i := 1; i < len(os.Args); i++ { 84 | arg := os.Args[i] 85 | if len(arg) > 0 && arg[0] == '-' { 86 | for _, fl := range arg[1:] { 87 | switch fl { 88 | case 'c': 89 | create = true 90 | case 'n': 91 | dontupdate = true 92 | case 'b': 93 | useargument = true 94 | case 'i': 95 | noverify = true 96 | case 'm': 97 | forcemd5 = true 98 | case 'B': 99 | forcebcrypt = true 100 | case 'C': 101 | if i == len(os.Args)-1 { 102 | usage() 103 | os.Exit(2) 104 | } 105 | v, err := strconv.Atoi(os.Args[i+1]) 106 | if err != nil { 107 | fmt.Fprintln(os.Stderr, "htpasswd: argument to -C must be a positive integer") 108 | os.Exit(2) 109 | } 110 | bcryptcost = v 111 | i++ 112 | case 's': 113 | forcesha256 = true 114 | case 'p': 115 | noencrypt = true 116 | case 'D': 117 | deleteuser = true 118 | case 'v': 119 | verifyuser = true 120 | } 121 | } 122 | } else { 123 | args = append(args, arg) 124 | } 125 | } 126 | 127 | check := 0 128 | if create { 129 | check++ 130 | } 131 | if dontupdate { 132 | check++ 133 | } 134 | if verifyuser { 135 | check++ 136 | } 137 | if deleteuser { 138 | check++ 139 | } 140 | if check != 0 && check > 1 { 141 | fmt.Fprintln(os.Stderr, "htpasswd: only one of -c -n -v -D may be specified") 142 | os.Exit(2) 143 | } 144 | 145 | if len(args) != 2 && (!useargument || len(args) != 3) { 146 | usage() 147 | os.Exit(130) 148 | } 149 | file := args[0] 150 | user := args[1] 151 | 152 | var content []byte 153 | var err error 154 | 155 | if dontupdate { 156 | content, err = ioutil.ReadAll(os.Stdout) 157 | if err != nil { 158 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 159 | os.Exit(1) 160 | } 161 | } else if !create { 162 | f, err := os.Open(file) 163 | if err != nil { 164 | if os.IsNotExist(err) { 165 | fmt.Fprintf(os.Stderr, "htpasswd: cannot modify file %s; use '-c' to create it\n", file) 166 | } else { 167 | fmt.Fprintf(os.Stderr, "htpasswd: cannot open file %s for read/write access\n", file) 168 | } 169 | os.Exit(1) 170 | } 171 | 172 | content, err = ioutil.ReadAll(f) 173 | f.Close() 174 | if err != nil { 175 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 176 | os.Exit(1) 177 | } 178 | } 179 | 180 | var input string 181 | if noverify { 182 | b, err := ioutil.ReadAll(os.Stdin) 183 | if err != nil { 184 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 185 | os.Exit(1) 186 | } 187 | input = string(b) 188 | } else if len(args) == 2 && !deleteuser { 189 | t, err := tty.Open() 190 | if err != nil { 191 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 192 | os.Exit(1) 193 | } 194 | defer t.Close() 195 | 196 | fmt.Print("New password: ") 197 | input, err = t.ReadPasswordNoEcho() 198 | if err != nil { 199 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 200 | os.Exit(1) 201 | } 202 | fmt.Print("Re-type new password: ") 203 | retry, err := t.ReadPasswordNoEcho() 204 | if err != nil { 205 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 206 | os.Exit(1) 207 | } 208 | if input != retry { 209 | fmt.Fprintln(os.Stderr, "htpasswd: password verification error") 210 | os.Exit(3) 211 | } 212 | } else if len(args) == 3 { 213 | input = args[2] 214 | } 215 | 216 | var result string 217 | if !noencrypt && !deleteuser { 218 | if forcebcrypt { 219 | b, err := bcrypt.GenerateFromPassword([]byte(input), bcryptcost) 220 | if err != nil { 221 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 222 | os.Exit(1) 223 | } 224 | result = string(b) 225 | } else if forcemd5 { 226 | s, err := randomString(8) 227 | if err != nil { 228 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 229 | os.Exit(1) 230 | } 231 | result = password.APR1.Crypt([]byte(input), []byte(s), nil) 232 | } else if forcesha256 { 233 | s, err := randomString(8) 234 | if err != nil { 235 | fmt.Fprintf(os.Stderr, "htpasswd: %v", err) 236 | os.Exit(1) 237 | } 238 | result = password.SHA256.Crypt([]byte(input), []byte(s), nil) 239 | } 240 | if dontupdate { 241 | fmt.Print(result) 242 | return 243 | } 244 | } else { 245 | result = input 246 | } 247 | 248 | if create { 249 | err = ioutil.WriteFile(file, []byte(user+":"+result+"\n"), 0644) 250 | if err != nil { 251 | fmt.Fprintf(os.Stderr, "htpasswd: cannot open file %s for read/write access\n", file) 252 | os.Exit(1) 253 | 254 | } 255 | fmt.Fprintf(os.Stderr, "Adding password for user %s\n", user) 256 | } else { 257 | found := false 258 | scanner := bufio.NewScanner(bytes.NewReader(content)) 259 | var buf bytes.Buffer 260 | for scanner.Scan() { 261 | line := scanner.Text() 262 | token := strings.SplitN(line, ":", 2) 263 | if len(token) != 2 { 264 | continue 265 | } 266 | if token[0] == user { 267 | found = true 268 | if !deleteuser { 269 | buf.WriteString(user + ":" + result + "\n") 270 | } 271 | } else { 272 | buf.WriteString(line + "\n") 273 | } 274 | } 275 | if !found && !deleteuser { 276 | buf.WriteString(user + ":" + result + "\n") 277 | } 278 | err = ioutil.WriteFile(file, buf.Bytes(), 0644) 279 | if err != nil { 280 | fmt.Fprintf(os.Stderr, "htpasswd: cannot open file %s for read/write access\n", file) 281 | os.Exit(1) 282 | } 283 | fmt.Fprintf(os.Stderr, "Updating password for user %s\n", user) 284 | } 285 | } 286 | --------------------------------------------------------------------------------