From bb73b0ffb472af0f90676aa24fd092475485565e Mon Sep 17 00:00:00 2001 From: Elena Balakhonova Date: Sat, 30 Nov 2019 20:53:26 +0300 Subject: [PATCH] Add auth with argon2i and sha256 hashed passwords --- README | 27 ++++++++++++++-- argon2i.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ auth_test.go | 45 ++++++++++++++++++++++++++ go.mod | 2 ++ go.sum | 8 +++++ gocheese.go | 43 ++++++++++++++++++++++--- sha256.go | 41 ++++++++++++++++++++++++ 7 files changed, 249 insertions(+), 6 deletions(-) create mode 100644 argon2i.go create mode 100644 auth_test.go create mode 100644 go.sum create mode 100644 sha256.go diff --git a/README b/README index 8d5ce95..643279c 100644 --- a/README +++ b/README @@ -16,7 +16,31 @@ You can upload packages to it with twine: twine upload --repository-url http://gocheese.host:8080/simple/ \ --username spam \ - --password foo dist/tarball.tar.gz + --passwd foo dist/tarball.tar.gz + +You have to store your authentication data in a file (specified +with -passwd option) with following format: + + username:hashed-password + +Supported hashing algorithms are sha256 and Argon2i. +It's recommended to use Argon2i. + +To get Argon2i hashed-password you can use any of following tools: + + https://github.com/balakhonova/argon2i (Go) + https://github.com/p-h-c/phc-winner-argon2 (C) + +To get SHA256 hashed-password you can use your operating system tools: + + echo -n 'password' | sha256 - for BSD-based systems + echo -n 'password' | sha256sum - for Linux-based systems + +For example user "foo" with password "bar" can have the following +hashed passwords: + + foo:$sha256$fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 + foo:$argon2i$v=19$m=32768,t=3,p=4$OGU5MTM3YjVlYzQwZjhkZA$rVn53v6Ckpf7WH0676ZQLr9Hbm6VH3YnL6I9ONJcIIU Root directory has the following hierarchy: @@ -51,7 +75,6 @@ but nearly all the code was rewritten. It has huge differences: * no TLS support * no YAML configuration, just command-line arguments * no package overwriting ability -* no MD5-hashed passwords * atomic packages store on filesystem * proxying and caching of missing packages * SHA256-checksummed packages (both uploaded and proxied one) diff --git a/argon2i.go b/argon2i.go new file mode 100644 index 0000000..d83a8fc --- /dev/null +++ b/argon2i.go @@ -0,0 +1,89 @@ +/* +GoCheese -- Python private package repository and caching proxy +Copyright (C) 2019 Elena Balakhonova + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// Python private package repository and caching proxy +package main + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "strings" + + "golang.org/x/crypto/argon2" +) + +type Argon2iAuthData struct { + version int + memory uint32 + time uint32 + threads uint8 + salt []byte + password []byte +} + +func (ad Argon2iAuthData) Auth(password string) bool { + hashedPassword := argon2.Key( + []byte(password), + ad.salt, + ad.time, + ad.memory, + ad.threads, + uint32(len(ad.password)), + ) + return bytes.Equal(hashedPassword, ad.password) +} + +func parseArgon2i(params string) (Auther, error) { + var time, memory uint32 + var threads uint8 + var version int + var saltAndPasswordUnitedB64 string + n, err := fmt.Sscanf( + params, + "v=%d$m=%d,t=%d,p=%d$%s", + &version, + &memory, + &time, + &threads, + &saltAndPasswordUnitedB64, + ) + if n != 5 || err != nil { + return nil, fmt.Errorf("argon2i parameters %q have wrong format", params) + } + if version != argon2.Version { + return nil, errors.New("unsupported argon2i version") + } + saltAndPasswordSplittedB64 := strings.Split(saltAndPasswordUnitedB64, "$") + salt, err := base64.RawStdEncoding.DecodeString(saltAndPasswordSplittedB64[0]) + if err != nil { + return nil, errors.New("invalid salt format") + } + password, err := base64.RawStdEncoding.DecodeString(saltAndPasswordSplittedB64[1]) + if err != nil { + return nil, errors.New("invalid password format") + } + return Argon2iAuthData{ + version: version, + time: time, + memory: memory, + threads: threads, + salt: salt, + password: password, + }, nil +} diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 0000000..1e49e2b --- /dev/null +++ b/auth_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "testing" +) + +var argon2iParams = "v=19$m=32768,t=3,p=4$ZjY5MDA5MGVlYjM0Yjg2Nw$hS8nOADanSJkVd9x5qZ0JG6Vsj/qG3gUWCqhJdr2A3c" + +func TestSHA256(t *testing.T) { + algorithm, auther, err := strToAuther("$sha256$a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3") + if err != nil { + t.FailNow() + } + if algorithm != "sha256" { + t.FailNow() + } + if !auther.Auth("123") { + t.FailNow() + } + if auther.Auth("1234") { + t.FailNow() + } +} + +func TestArgon2i(t *testing.T) { + algorithm, auther, err := strToAuther("$argon2i$" + argon2iParams) + if err != nil { + t.FailNow() + } + if algorithm != "argon2i" { + t.FailNow() + } + if !auther.Auth("123") { + t.FailNow() + } + if auther.Auth("1234") { + t.FailNow() + } +} + +func BenchmarkParseArgon2i(b *testing.B) { + for i := 0; i < b.N; i++ { + parseArgon2i(argon2iParams) + } +} diff --git a/go.mod b/go.mod index 07c0715..9429346 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module go.cypherpunks.ru/gocheese go 1.12 + +require golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..791a997 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/gocheese.go b/gocheese.go index 9035e99..cde862a 100644 --- a/gocheese.go +++ b/gocheese.go @@ -22,6 +22,7 @@ import ( "bytes" "crypto/sha256" "encoding/hex" + "errors" "flag" "fmt" "io" @@ -63,7 +64,7 @@ var ( norefreshURLPath = flag.String("norefresh", "/norefresh/", "Non-refreshing URL path") refreshURLPath = flag.String("refresh", "/simple/", "Auto-refreshing URL path") pypiURL = flag.String("pypi", "https://pypi.org/simple/", "Upstream PyPI URL") - passwdPath = flag.String("passwd", "passwd", "Path to file with login:password lines") + passwdPath = flag.String("passwd", "passwd", "Path to file with auth") fsck = flag.Bool("fsck", false, "Check integrity of all packages") version = flag.Bool("version", false, "Print version information") warranty = flag.Bool("warranty", false, "Print warranty information") @@ -71,9 +72,13 @@ var ( pkgPyPI = regexp.MustCompile(`^.*]*>(.+)
.*$`) Version string = "UNKNOWN" - passwords map[string]string = make(map[string]string) + passwords map[string]Auther = make(map[string]Auther) ) +type Auther interface { + Auth(password string) bool +} + func mkdirForPkg(w http.ResponseWriter, r *http.Request, dir string) bool { path := filepath.Join(*root, dir) if _, err := os.Stat(path); os.IsNotExist(err) { @@ -259,9 +264,34 @@ func servePkg(w http.ResponseWriter, r *http.Request, dir, filename string) { http.ServeFile(w, r, path) } +func strToAuther(verifier string) (string, Auther, error) { + st := strings.SplitN(verifier, "$", 3) + if len(st) != 3 || st[0] != "" { + return "", nil, errors.New("invalid verifier structure") + } + algorithm := st[1] + var auther Auther + var err error + switch algorithm { + case "argon2i": + auther, err = parseArgon2i(st[2]) + case "sha256": + auther, err = parseSHA256(st[2]) + default: + err = errors.New("unknown hashing algorithm") + } + return algorithm, auther, err +} + func serveUpload(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() - if !ok || passwords[username] != password { + if !ok { + log.Println(r.RemoteAddr, "unauthenticated", username) + http.Error(w, "unauthenticated", http.StatusUnauthorized) + return + } + auther, ok := passwords[username] + if !ok || !auther.Auth(password) { log.Println(r.RemoteAddr, "unauthenticated", username) http.Error(w, "unauthenticated", http.StatusUnauthorized) return @@ -434,7 +464,12 @@ func main() { if len(splitted) != 2 { log.Fatal("Wrong login:password format") } - passwords[splitted[0]] = splitted[1] + _, auther, err := strToAuther(splitted[1]) + if err != nil { + log.Fatal(err) + } + passwords[splitted[0]] = auther + log.Println("Added password for " + splitted[0]) } log.Println("root:", *root, "bind:", *bind) http.HandleFunc(*norefreshURLPath, handler) diff --git a/sha256.go b/sha256.go new file mode 100644 index 0000000..a2d7beb --- /dev/null +++ b/sha256.go @@ -0,0 +1,41 @@ +/* +GoCheese -- Python private package repository and caching proxy +Copyright (C) 2019 Elena Balakhonova + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +// Python private package repository and caching proxy +package main + +import ( + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "fmt" +) + +type SHA256AuthData []byte + +func (expectedPasword SHA256AuthData) Auth(password string) bool { + hash := sha256.Sum256([]byte(password)) + return subtle.ConstantTimeCompare(hash[:], []byte(expectedPasword)) == 1 +} + +func parseSHA256(params string) (Auther, error) { + if len(params) != 64 { + return nil, fmt.Errorf("sha256 parameters %q have wrong format", params) + } + hash, err := hex.DecodeString(params) + return SHA256AuthData(hash), err +} -- 2.44.0