mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
chore!: ensure consistent secret token generation and hashing (#20388)
This PR uses the same sha256 hashing technique as we use for APIKeys. So now all randomly generated secrets will be hashed with sha256 for consistency. This is a breaking change for the oauth tokens. Since oauth is only allowed for dev builds and experimental, this is ok.
This commit is contained in:
+28
-15
@@ -2,6 +2,7 @@ package apikey
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
@@ -44,12 +45,17 @@ type CreateParams struct {
|
||||
// database representation. It is the responsibility of the caller to insert it
|
||||
// into the database.
|
||||
func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error) {
|
||||
keyID, keySecret, err := generateKey()
|
||||
// Length of an API Key ID.
|
||||
keyID, err := cryptorand.String(10)
|
||||
if err != nil {
|
||||
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("generate API key: %w", err)
|
||||
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("generate API key ID: %w", err)
|
||||
}
|
||||
|
||||
hashed := sha256.Sum256([]byte(keySecret))
|
||||
// Length of an API Key secret.
|
||||
keySecret, hashedSecret, err := GenerateSecret(22)
|
||||
if err != nil {
|
||||
return database.InsertAPIKeyParams{}, "", xerrors.Errorf("generate API key secret: %w", err)
|
||||
}
|
||||
|
||||
// Default expires at to now+lifetime, or use the configured value if not
|
||||
// set.
|
||||
@@ -120,7 +126,7 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
|
||||
ExpiresAt: params.ExpiresAt.UTC(),
|
||||
CreatedAt: dbtime.Now(),
|
||||
UpdatedAt: dbtime.Now(),
|
||||
HashedSecret: hashed[:],
|
||||
HashedSecret: hashedSecret,
|
||||
LoginType: params.LoginType,
|
||||
Scopes: scopes,
|
||||
AllowList: params.AllowList,
|
||||
@@ -128,17 +134,24 @@ func Generate(params CreateParams) (database.InsertAPIKeyParams, string, error)
|
||||
}, token, nil
|
||||
}
|
||||
|
||||
// generateKey a new ID and secret for an API key.
|
||||
func generateKey() (id string, secret string, err error) {
|
||||
// Length of an API Key ID.
|
||||
id, err = cryptorand.String(10)
|
||||
func GenerateSecret(length int) (secret string, hashed []byte, err error) {
|
||||
secret, err = cryptorand.String(length)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", nil, err
|
||||
}
|
||||
// Length of an API Key secret.
|
||||
secret, err = cryptorand.String(22)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return id, secret, nil
|
||||
hash := HashSecret(secret)
|
||||
return secret, hash, nil
|
||||
}
|
||||
|
||||
// ValidateHash compares a secret against an expected hashed secret.
|
||||
func ValidateHash(hashedSecret []byte, secret string) bool {
|
||||
hash := HashSecret(secret)
|
||||
return subtle.ConstantTimeCompare(hashedSecret, hash) == 1
|
||||
}
|
||||
|
||||
// HashSecret is the single function used to hash API key secrets.
|
||||
// Use this to ensure a consistent hashing algorithm.
|
||||
func HashSecret(secret string) []byte {
|
||||
hash := sha256.Sum256([]byte(secret))
|
||||
return hash[:]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package apikey_test
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -126,8 +125,8 @@ func TestGenerate(t *testing.T) {
|
||||
require.Equal(t, key.ID, keytokens[0])
|
||||
|
||||
// Assert that the hashed secret is correct.
|
||||
hashed := sha256.Sum256([]byte(keytokens[1]))
|
||||
assert.ElementsMatch(t, hashed, key.HashedSecret)
|
||||
equal := apikey.ValidateHash(key.HashedSecret, keytokens[1])
|
||||
require.True(t, equal, "valid secret")
|
||||
|
||||
assert.Equal(t, tc.params.UserID, key.UserID)
|
||||
assert.WithinDuration(t, dbtime.Now(), key.CreatedAt, time.Second*5)
|
||||
@@ -173,3 +172,17 @@ func TestGenerate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestInvalid just ensures the false case is asserted by some tests.
|
||||
// Otherwise, a function that just `returns true` might pass all tests incorrectly.
|
||||
func TestInvalid(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.Falsef(t, apikey.ValidateHash([]byte{}, "secret"), "empty hash")
|
||||
|
||||
secret, hash, err := apikey.GenerateSecret(10)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Falsef(t, apikey.ValidateHash(hash, secret+"_"), "different secret")
|
||||
require.Falsef(t, apikey.ValidateHash(hash[:len(hash)-1], secret), "different hash length")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user