Skip to content

Commit b74507b

Browse files
authored
feat: add optional token hashing (#25982)
Add optional token hashing with `--use-hashed-tokens` command line option.
1 parent b67326f commit b74507b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+2726
-716
lines changed

auth.go

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ var ErrUnableToCreateToken = &errors.Error{
2020
// Authorization is an authorization. 🎉
2121
type Authorization struct {
2222
ID platform.ID `json:"id"`
23-
Token string `json:"token"`
23+
Token string `json:"token,omitempty"`
24+
HashedToken string `json:"hashedToken,omitempty"`
2425
Status Status `json:"status"`
2526
Description string `json:"description"`
2627
OrgID platform.ID `json:"orgID"`
@@ -35,7 +36,60 @@ type AuthorizationUpdate struct {
3536
Description *string `json:"description,omitempty"`
3637
}
3738

39+
const (
40+
// authTokenClearValue is used to indicate Token or HashedToken are cleared (not set).
41+
authTokenClearValue = ""
42+
)
43+
44+
// IsAuthTokenSet returns true if token is considered set. Applies
45+
// to be both unhashed tokens (Authorization.Token) and
46+
// hashed tokens (Authorization.HashedToken).
47+
func IsAuthTokenSet(token string) bool {
48+
return token != authTokenClearValue
49+
}
50+
51+
// IsTokenSet returns true if Token is set.
52+
func (a *Authorization) IsTokenSet() bool {
53+
return IsAuthTokenSet(a.Token)
54+
}
55+
56+
// IsTokenClear returns true if Token is unset.
57+
func (a *Authorization) IsTokenClear() bool {
58+
return !a.IsTokenSet()
59+
}
60+
61+
// ClearToken clears Token.
62+
func (a *Authorization) ClearToken() {
63+
a.Token = authTokenClearValue
64+
}
65+
66+
// IsHashedTokenSet returns true if HashedToken is set.
67+
func (a *Authorization) IsHashedTokenSet() bool {
68+
return IsAuthTokenSet(a.HashedToken)
69+
}
70+
71+
// IsHashedTokenClear returns true if Token is unset.
72+
func (a *Authorization) IsHashedTokenClear() bool {
73+
return !a.IsHashedTokenSet()
74+
}
75+
76+
// ClearToken clears HashedToken.
77+
func (a *Authorization) ClearHashedToken() {
78+
a.HashedToken = authTokenClearValue
79+
}
80+
81+
// NoTokensSet returns true if neither Token nor HashedToken is set.
82+
func (a *Authorization) NoTokensSet() bool {
83+
return a.IsTokenClear() && a.IsHashedTokenClear()
84+
}
85+
86+
// BothTokensSet returns true if both Token and Hashed token is set.
87+
func (a *Authorization) BothTokensSet() bool {
88+
return a.IsTokenSet() && a.IsHashedTokenSet()
89+
}
90+
3891
// Valid ensures that the authorization is valid.
92+
// Valid does not check if tokens are set properly.
3993
func (a *Authorization) Valid() error {
4094
for _, p := range a.Permissions {
4195
if p.Resource.OrgID != nil && *p.Resource.OrgID != a.OrgID {

authorization/error.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ func ErrInvalidAuthIDError(err error) *errors.Error {
5050
}
5151
}
5252

53-
// UnexpectedAuthIndexError is used when the error comes from an internal system.
54-
func UnexpectedAuthIndexError(err error) *errors.Error {
53+
// UnexpectedAuthBucketError is used when the error comes from an internal system.
54+
func UnexpectedAuthBucketError(index []byte, err error) *errors.Error {
5555
var e *errors.Error
5656
if !errors2.As(err, &e) {
5757
e = &errors.Error{
58-
Msg: fmt.Sprintf("unexpected error retrieving auth index; Err: %v", err),
58+
Msg: fmt.Sprintf("unexpected error retrieving auth bucket %q; Err: %v", index, err),
5959
Code: errors.EInternal,
6060
Err: err,
6161
}

authorization/hasher.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package authorization
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/go-crypt/crypt"
8+
"github.com/go-crypt/crypt/algorithm"
9+
influxdb2_algo "github.com/influxdata/influxdb/v2/pkg/crypt/algorithm/influxdb2"
10+
)
11+
12+
var (
13+
ErrNoDecoders = errors.New("no authorization decoders specified")
14+
)
15+
16+
type AuthorizationHasher struct {
17+
// hasher encodes tokens into hashed PHC-encoded tokens.
18+
hasher algorithm.Hash
19+
20+
// decoder decodes hashed PHC-encoded tokens into crypt.Digest objects.
21+
decoder *crypt.Decoder
22+
23+
// allHashers is the list of all hashers which could be used for hashed index lookup.
24+
allHashers []algorithm.Hash
25+
}
26+
27+
const (
28+
DefaultHashVariant = influxdb2_algo.VariantSHA256
29+
DefaultHashVariantName = influxdb2_algo.VariantIdentifierSHA256
30+
31+
// HashVariantNameUnknown is the placeholder name used for unknown or unsupported hash variants.
32+
HashVariantNameUnknown = "N/A"
33+
)
34+
35+
type authorizationHasherOptions struct {
36+
hasherVariant influxdb2_algo.Variant
37+
decoderVariants []influxdb2_algo.Variant
38+
}
39+
40+
type AuthorizationHasherOption func(o *authorizationHasherOptions)
41+
42+
func WithHasherVariant(variant influxdb2_algo.Variant) AuthorizationHasherOption {
43+
return func(o *authorizationHasherOptions) {
44+
o.hasherVariant = variant
45+
}
46+
}
47+
48+
func WithDecoderVariants(variants []influxdb2_algo.Variant) AuthorizationHasherOption {
49+
return func(o *authorizationHasherOptions) {
50+
o.decoderVariants = variants
51+
}
52+
}
53+
54+
// NewAuthorizationHasher creates an AuthorizationHasher for influxdb2 algorithm hashed tokens.
55+
// variantName specifies which token hashing variant to use, with blank indicating to use the default
56+
// hashing variant. By default, all variants of the influxdb2 hashing scheme are supported for
57+
// maximal compatibility.
58+
func NewAuthorizationHasher(opts ...AuthorizationHasherOption) (*AuthorizationHasher, error) {
59+
options := authorizationHasherOptions{
60+
hasherVariant: DefaultHashVariant,
61+
decoderVariants: influxdb2_algo.AllVariants,
62+
}
63+
64+
for _, o := range opts {
65+
o(&options)
66+
}
67+
68+
if len(options.decoderVariants) == 0 {
69+
return nil, fmt.Errorf("error in NewAuthorizationHasher: %w", ErrNoDecoders)
70+
}
71+
72+
// Create the hasher used for hashing new tokens before storage.
73+
hasher, err := influxdb2_algo.New(influxdb2_algo.WithVariant(options.hasherVariant))
74+
if err != nil {
75+
return nil, fmt.Errorf("creating hasher %s for AuthorizationHasher: %w", options.hasherVariant.Prefix(), err)
76+
}
77+
78+
// Create decoder and register all requested decoder variants.
79+
decoder := crypt.NewDecoder()
80+
for _, variant := range options.decoderVariants {
81+
if err := variant.RegisterDecoder(decoder); err != nil {
82+
return nil, fmt.Errorf("registering variant %s with decoder: %w", variant.Prefix(), err)
83+
}
84+
}
85+
86+
// Create all variant hashers needed for requested decoder variants. This is required for operations where
87+
// all potential variations of a raw token must be hashed, such as looking up a hash in the hashed token index.
88+
var allHashers []algorithm.Hash
89+
for _, variant := range options.decoderVariants {
90+
h, err := influxdb2_algo.New(influxdb2_algo.WithVariant(variant))
91+
if err != nil {
92+
return nil, fmt.Errorf("creating hasher %s for authorization service index lookups: %w", variant.Prefix(), err)
93+
}
94+
allHashers = append(allHashers, h)
95+
}
96+
97+
return &AuthorizationHasher{
98+
hasher: hasher,
99+
decoder: decoder,
100+
allHashers: allHashers,
101+
}, nil
102+
}
103+
104+
// Hash generates a PHC-encoded hash of token using the selected hash algorithm variant.
105+
func (h *AuthorizationHasher) Hash(token string) (string, error) {
106+
digest, err := h.hasher.Hash(token)
107+
if err != nil {
108+
return "", fmt.Errorf("hashing raw token failed: %w", err)
109+
}
110+
return digest.Encode(), nil
111+
}
112+
113+
// AllHashes generates a list of PHC-encoded hashes of token for all deterministic (i.e. non-salted) supported hashes.
114+
func (h *AuthorizationHasher) AllHashes(token string) ([]string, error) {
115+
hashes := make([]string, len(h.allHashers))
116+
for idx, hasher := range h.allHashers {
117+
digest, err := hasher.Hash(token)
118+
if err != nil {
119+
variantName := HashVariantNameUnknown
120+
if influxdb_hasher, ok := hasher.(*influxdb2_algo.Hasher); ok {
121+
variantName = influxdb_hasher.Variant().Prefix()
122+
}
123+
return nil, fmt.Errorf("hashing raw token failed (variant=%s): %w", variantName, err)
124+
}
125+
hashes[idx] = digest.Encode()
126+
}
127+
return hashes, nil
128+
}
129+
130+
// AllHashesCount returns the number of hash variants available through AllHashes.
131+
func (h *AuthorizationHasher) AllHashesCount() int {
132+
return len(h.allHashers)
133+
}
134+
135+
// Decode decodes a PHC-encoded hash into a Digest object that can be matched.
136+
func (h *AuthorizationHasher) Decode(phc string) (algorithm.Digest, error) {
137+
return h.decoder.Decode(phc)
138+
}
139+
140+
// Match determines if a raw token matches a PHC-encoded token.
141+
func (h *AuthorizationHasher) Match(phc string, token string) (bool, error) {
142+
digest, err := h.Decode(phc)
143+
if err != nil {
144+
return false, err
145+
}
146+
147+
return digest.MatchAdvanced(token)
148+
}

authorization/hasher_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package authorization_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/go-crypt/crypt/algorithm"
7+
"github.com/influxdata/influxdb/v2/authorization"
8+
influxdb2_algo "github.com/influxdata/influxdb/v2/pkg/crypt/algorithm/influxdb2"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func Test_NewAuthorizationHasher_EmptyDecoderVariants(t *testing.T) {
13+
hasher, err := authorization.NewAuthorizationHasher(
14+
authorization.WithDecoderVariants([]influxdb2_algo.Variant{}),
15+
)
16+
17+
require.ErrorIs(t, err, authorization.ErrNoDecoders)
18+
require.Nil(t, hasher)
19+
}
20+
21+
func TestNewAuthorizationHasher_WithInvalidDecoderVariant(t *testing.T) {
22+
// Test that using an invalid decoder variant returns an error
23+
hasher, err := authorization.NewAuthorizationHasher(
24+
authorization.WithDecoderVariants([]influxdb2_algo.Variant{
25+
influxdb2_algo.Variant(-1), // Invalid variant
26+
}),
27+
)
28+
29+
// Should return an error and nil hasher
30+
require.ErrorIs(t, err, algorithm.ErrParameterInvalid)
31+
require.Contains(t, err.Error(), "registering variant")
32+
require.Nil(t, hasher)
33+
}
34+
35+
func TestNewAuthorizationHasher_WithHasherVariantInvalid(t *testing.T) {
36+
// Test that using VariantNone returns an error
37+
hasher, err := authorization.NewAuthorizationHasher(
38+
authorization.WithHasherVariant(influxdb2_algo.Variant(-1)),
39+
)
40+
41+
// Should return an error and nil hasher
42+
require.ErrorIs(t, err, algorithm.ErrParameterInvalid)
43+
require.ErrorContains(t, err, "creating hasher")
44+
require.Nil(t, hasher)
45+
}

0 commit comments

Comments
 (0)