Skip to content

Commit 1562be8

Browse files
authored
Merge pull request #12 from ST2Projects/tk/9/api-keys-should-be-hashed-with-argon2
Tk/9/api keys should be hashed with argon2
2 parents 1b16a94 + a384203 commit 1562be8

File tree

8 files changed

+128
-45
lines changed

8 files changed

+128
-45
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,4 @@ fabric.properties
171171

172172
resources
173173
dist/
174+
./ssh-sentinel-server

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ doc:
88
godoc -http=:6060
99

1010
release:
11-
shell goreleaser release
11+
$(shell goreleaser release --rm-dist)
1212

1313
release-dry:
1414
$(shell goreleaser release --skip-publish)

crypto/argon_helper.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package crypto
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/subtle"
6+
"encoding/base64"
7+
"fmt"
8+
"golang.org/x/crypto/argon2"
9+
"strings"
10+
)
11+
12+
const hashFormat = "$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s"
13+
14+
type PasswordConfig struct {
15+
time uint32
16+
memory uint32
17+
threads uint8
18+
keyLen uint32
19+
}
20+
21+
func (c PasswordConfig) DefaultConfig() *PasswordConfig {
22+
return &PasswordConfig{
23+
time: 1,
24+
memory: 64 * 1024,
25+
threads: 4,
26+
keyLen: 32,
27+
}
28+
}
29+
30+
func GenerateHash(config *PasswordConfig, s string) (string, error) {
31+
salt := make([]byte, 16)
32+
if _, err := rand.Read(salt); err != nil {
33+
return "", err
34+
}
35+
36+
hash := argon2.IDKey([]byte(s), salt, config.time, config.memory, config.threads, config.keyLen)
37+
38+
saltB64 := base64.RawStdEncoding.EncodeToString(salt)
39+
hashB64 := base64.RawStdEncoding.EncodeToString(hash)
40+
41+
finalHash := fmt.Sprintf(hashFormat, argon2.Version, config.memory, config.time, config.threads, saltB64, hashB64)
42+
43+
return finalHash, nil
44+
}
45+
46+
func Validate(s, hash string) (bool, error) {
47+
48+
hashParts := strings.Split(hash, "$")
49+
50+
config := &PasswordConfig{}
51+
52+
_, err := fmt.Sscanf(hashParts[3], "m=%d,t=%d,p=%d", &config.memory, &config.time, &config.threads)
53+
if err != nil {
54+
return false, err
55+
}
56+
57+
salt, err := base64.RawStdEncoding.DecodeString(hashParts[4])
58+
if err != nil {
59+
return false, err
60+
}
61+
62+
decodedHash, err := base64.RawStdEncoding.DecodeString(hashParts[5])
63+
64+
config.keyLen = uint32(len(decodedHash))
65+
66+
comparisonHash := argon2.IDKey([]byte(s), salt, config.time, config.memory, config.threads, config.keyLen)
67+
68+
return subtle.ConstantTimeCompare(decodedHash, comparisonHash) == 1, nil
69+
}

crypto/argon_helper_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package crypto
2+
3+
import "testing"
4+
5+
func TestDefaultConfig(t *testing.T) {
6+
config := PasswordConfig{}.DefaultConfig()
7+
8+
if config.time != 1 {
9+
t.Errorf("Incorrect default time %d . Expected '1'", config.time)
10+
}
11+
12+
if config.memory != 65536 {
13+
t.Errorf("Incorrect default memory %d . Expected '65536'", config.memory)
14+
}
15+
16+
if config.threads != 4 {
17+
t.Errorf("Incorrect default memory %d . Expected '4'", config.threads)
18+
}
19+
20+
if config.keyLen != 32 {
21+
t.Errorf("Incorrect default key length %d . Expected '32'", config.keyLen)
22+
}
23+
}

http-tests.http

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,30 @@
33
POST http://localhost/ssh
44
Content-Type: application/json
55

6-
{"username": "test", "api_key": "47e69901-7feb-40fd-a48f-8d42211a16f0", "principals": ["test"], "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICazCPU7VgxhgZWNXr2bA2nErkFyXRz4IMddcZLDN7MU xxx@aaa"}
6+
{"username": "test", "api_key": "63cac958-6a06-4f18-9a58-d26779f89ab1", "principals": ["test"], "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICazCPU7VgxhgZWNXr2bA2nErkFyXRz4IMddcZLDN7MU xxx@aaa"}
77

8+
> {%
9+
// Response handler
10+
client.test("Request was successful", function () {
11+
client.assert(response.status === 200, "Status was not 200");
12+
client.assert(response.body.signedKey !== "", "signed key was empty!");
13+
// let responseBody = JSON.parse(response.body.toString());
14+
// client.assert(responseBody.signedKey != null, "Signed key was empty / null");
15+
});
16+
%}
817

9-
###
18+
### testFailedRequestUnAuthorisedAPIKey
19+
POST http://localhost/ssh
20+
Content-Type: application/json
21+
22+
{"username": "test", "api_key": "aabb", "principals": ["test"], "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICazCPU7VgxhgZWNXr2bA2nErkFyXRz4IMddcZLDN7MU xxx@aaa"}
23+
24+
> {%
25+
// Response handler
26+
client.test("Request failed", function () {
27+
client.assert(response.status === 401, "Status was not 401");
28+
client.assert(response.body.signedKey === "", "signed key was not empty!");
29+
// let responseBody = JSON.parse(response.body.toString());
30+
// client.assert(responseBody.signedKey != null, "Signed key was empty / null");
31+
});
32+
%}

model/db/user.go

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package db
22

33
import (
4-
"crypto/sha256"
5-
"crypto/subtle"
6-
"encoding/hex"
74
"github.com/google/uuid"
85
log "github.com/sirupsen/logrus"
96
"gorm.io/gorm"
7+
"ssh-sentinel-server/crypto"
108
)
119

1210
type User struct {
@@ -34,40 +32,17 @@ type APIKey struct {
3432
func NewAPIKey() (APIKey, string) {
3533
id := uuid.New().String()
3634

37-
sha := sha256.New()
38-
sha.Write([]byte(id))
39-
finalValue := sha.Sum(nil)
40-
41-
apiKey := APIKey{}
42-
43-
apiKey.Key = hex.EncodeToString(finalValue)
44-
45-
return apiKey, id
46-
}
47-
48-
func AsAPIKey(key uuid.UUID) APIKey {
49-
sha := sha256.New()
50-
sha.Write([]byte(key.String()))
51-
finalValue := sha.Sum(nil)
52-
5335
apiKey := APIKey{}
54-
apiKey.Key = hex.EncodeToString(finalValue)
55-
return apiKey
56-
}
5736

58-
func (k *APIKey) Validate(other string) bool {
59-
sha := sha256.New()
60-
sha.Write([]byte(other))
61-
otherSum := sha.Sum(nil)
62-
63-
thisDecoded, err := hex.DecodeString(k.Key)
37+
k, err := crypto.GenerateHash(crypto.PasswordConfig{}.DefaultConfig(), id)
6438

6539
if err != nil {
66-
log.Fatal("Failed to decode key", err)
40+
log.Fatal("Cannot create key", err)
6741
}
6842

69-
// ConstantTimeCompare returns 1 when equal...
70-
return subtle.ConstantTimeCompare(thisDecoded, otherSum) == 1
43+
apiKey.Key = k
44+
45+
return apiKey, id
7146
}
7247

7348
func (p *Principal) String() string {

model/db/user_test.go

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,6 @@ package db
22

33
import "testing"
44

5-
func TestAPIKeyType_Validate(t *testing.T) {
6-
myKey := APIKey{Key: "e072bebc2cf6191881f0a1af2af353e1ded499e77b9d05a0425a25c3fce90807"}
7-
8-
validated := myKey.Validate("39d61458-7c4c-4e58-a79d-37f02a448ca9")
9-
10-
if !validated {
11-
t.Error("Key did not validate")
12-
}
13-
}
14-
155
func TestUser_Table(t *testing.T) {
166
u := User{}
177

server/handlers.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
log "github.com/sirupsen/logrus"
88
"io/ioutil"
99
"net/http"
10+
"ssh-sentinel-server/crypto"
1011
"ssh-sentinel-server/helper"
1112
model "ssh-sentinel-server/model/http"
1213
"ssh-sentinel-server/sql"
@@ -33,9 +34,10 @@ func AuthenticationHandler(next http.Handler) http.Handler {
3334

3435
user := sql.GetUserByUsername(signRequest.Username)
3536

36-
hasValidAPIKey := user.APIKey.Validate(signRequest.APIKey)
37+
hasValidAPIKey, err := crypto.Validate(signRequest.APIKey, user.APIKey.Key)
3738

3839
if !hasValidAPIKey {
40+
w.WriteHeader(http.StatusUnauthorized)
3941
panic(helper.NewError("Unauthorised key"))
4042
}
4143

0 commit comments

Comments
 (0)