]> Cypherpunks.ru repositories - gocheese.git/commitdiff
Add auth with argon2i and sha256 hashed passwords
authorElena Balakhonova <balakhonova@rnd.stcnet.ru>
Sat, 30 Nov 2019 17:53:26 +0000 (20:53 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Sun, 1 Dec 2019 20:42:34 +0000 (23:42 +0300)
README
argon2i.go [new file with mode: 0644]
auth_test.go [new file with mode: 0644]
go.mod
go.sum [new file with mode: 0644]
gocheese.go
sha256.go [new file with mode: 0644]

diff --git a/README b/README
index 8d5ce9501d732f2ef85b0d1110075e1ebd027e63..643279ca63f643d7d8c2dea550b4bb1807ec1ac6 100644 (file)
--- 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 (file)
index 0000000..d83a8fc
--- /dev/null
@@ -0,0 +1,89 @@
+/*
+GoCheese -- Python private package repository and caching proxy
+Copyright (C) 2019 Elena Balakhonova <balakhonova_e@riseup.net>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+// 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 (file)
index 0000000..1e49e2b
--- /dev/null
@@ -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 07c07150ad815dd5a0fc26fde8198653f18b4465..942934615242ba86ffde3be070404814f3e41a0f 100644 (file)
--- 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 (file)
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=
index 9035e99ccca883e1422520f8c3ee580606c06ae3..cde862ab49d8ade28c5e1664e2962f8e4b5edae2 100644 (file)
@@ -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(`^.*<a href="([^"]+)"[^>]*>(.+)</a><br/>.*$`)
        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 (file)
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 <balakhonova_e@riseup.net>
+
+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 <http://www.gnu.org/licenses/>.
+*/
+
+// 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
+}