feat(crypto): use passwap for machine and app secrets (#7657)

* feat(crypto): use passwap for machine and app secrets

* fix command package tests

* add hash generator command test

* naming convention, fix query tests

* rename PasswordHasher and cleanup start commands

* add reducer tests

* fix intergration tests, cleanup old config

* add app secret unit tests

* solve setup panics

* fix push of updated events

* add missing event translations

* update documentation

* solve linter errors

* remove nolint:SA1019 as it doesn't seem to help anyway

* add nolint to deprecated filter usage

* update users migration version

* remove unused ClientSecret from APIConfigChangedEvent

---------

Co-authored-by: Livio Spring <livio.a@gmail.com>
This commit is contained in:
Tim Möhlmann
2024-04-05 12:35:49 +03:00
committed by GitHub
parent 5931fb8f28
commit 2089992d75
135 changed files with 2407 additions and 1779 deletions

View File

@@ -1,27 +0,0 @@
package crypto
import (
"golang.org/x/crypto/bcrypt"
)
var _ HashAlgorithm = (*BCrypt)(nil)
type BCrypt struct {
cost int
}
func NewBCrypt(cost int) *BCrypt {
return &BCrypt{cost: cost}
}
func (b *BCrypt) Algorithm() string {
return "bcrypt"
}
func (b *BCrypt) Hash(value []byte) ([]byte, error) {
return bcrypt.GenerateFromPassword(value, b.cost)
}
func (b *BCrypt) CompareHash(hashed, value []byte) error {
return bcrypt.CompareHashAndPassword(hashed, value)
}

View File

@@ -26,7 +26,7 @@ type GeneratorConfig struct {
type Generator interface {
Length() uint
Expiry() time.Duration
Alg() Crypto
Alg() EncryptionAlgorithm
Runes() []rune
}
@@ -53,7 +53,7 @@ type encryptionGenerator struct {
alg EncryptionAlgorithm
}
func (g *encryptionGenerator) Alg() Crypto {
func (g *encryptionGenerator) Alg() EncryptionAlgorithm {
return g.alg
}
@@ -64,22 +64,30 @@ func NewEncryptionGenerator(config GeneratorConfig, algorithm EncryptionAlgorith
}
}
type hashGenerator struct {
type HashGenerator struct {
generator
alg HashAlgorithm
hasher *Hasher
}
func (g *hashGenerator) Alg() Crypto {
return g.alg
}
func NewHashGenerator(config GeneratorConfig, algorithm HashAlgorithm) Generator {
return &hashGenerator{
func NewHashGenerator(config GeneratorConfig, hasher *Hasher) *HashGenerator {
return &HashGenerator{
newGenerator(config),
algorithm,
hasher,
}
}
func (g *HashGenerator) NewCode() (encoded, plain string, err error) {
plain, err = GenerateRandomString(g.Length(), g.Runes())
if err != nil {
return "", "", err
}
encoded, err = g.hasher.Hash(plain)
if err != nil {
return "", "", err
}
return encoded, plain, nil
}
func newGenerator(config GeneratorConfig) generator {
var runes []rune
if config.IncludeLowerLetters {
@@ -120,21 +128,11 @@ func IsCodeExpired(creationDate time.Time, expiry time.Duration) bool {
return creationDate.Add(expiry).Before(time.Now().UTC())
}
func VerifyCode(creationDate time.Time, expiry time.Duration, cryptoCode *CryptoValue, verificationCode string, g Generator) error {
return VerifyCodeWithAlgorithm(creationDate, expiry, cryptoCode, verificationCode, g.Alg())
}
func VerifyCodeWithAlgorithm(creationDate time.Time, expiry time.Duration, cryptoCode *CryptoValue, verificationCode string, algorithm Crypto) error {
func VerifyCode(creationDate time.Time, expiry time.Duration, cryptoCode *CryptoValue, verificationCode string, algorithm EncryptionAlgorithm) error {
if IsCodeExpired(creationDate, expiry) {
return zerrors.ThrowPreconditionFailed(nil, "CODE-QvUQ4P", "Errors.User.Code.Expired")
}
switch alg := algorithm.(type) {
case EncryptionAlgorithm:
return verifyEncryptedCode(cryptoCode, verificationCode, alg)
case HashAlgorithm:
return verifyHashedCode(cryptoCode, verificationCode, alg)
}
return zerrors.ThrowInvalidArgument(nil, "CODE-fW2gNa", "Errors.User.Code.GeneratorAlgNotSupported")
return verifyEncryptedCode(cryptoCode, verificationCode, algorithm)
}
func GenerateRandomString(length uint, chars []rune) (string, error) {
@@ -173,10 +171,3 @@ func verifyEncryptedCode(cryptoCode *CryptoValue, verificationCode string, alg E
}
return nil
}
func verifyHashedCode(cryptoCode *CryptoValue, verificationCode string, alg HashAlgorithm) error {
if cryptoCode == nil {
return zerrors.ThrowInvalidArgument(nil, "CRYPT-2q3r", "cryptoCode must not be nil")
}
return CompareHash(cryptoCode, []byte(verificationCode), alg)
}

View File

@@ -5,6 +5,7 @@
//
// mockgen -source code.go -destination ./code_mock.go -package crypto
//
// Package crypto is a generated GoMock package.
package crypto
@@ -39,10 +40,10 @@ func (m *MockGenerator) EXPECT() *MockGeneratorMockRecorder {
}
// Alg mocks base method.
func (m *MockGenerator) Alg() Crypto {
func (m *MockGenerator) Alg() EncryptionAlgorithm {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Alg")
ret0, _ := ret[0].(Crypto)
ret0, _ := ret[0].(EncryptionAlgorithm)
return ret0
}

View File

@@ -60,32 +60,13 @@ func createMockEncryptionAlgorithm(ctrl *gomock.Controller, encryptFunction func
return mCrypto
}
func CreateMockHashAlg(ctrl *gomock.Controller) HashAlgorithm {
mCrypto := NewMockHashAlgorithm(ctrl)
mCrypto.EXPECT().Algorithm().AnyTimes().Return("hash")
mCrypto.EXPECT().Hash(gomock.Any()).AnyTimes().DoAndReturn(
func(code []byte) ([]byte, error) {
return code, nil
},
)
mCrypto.EXPECT().CompareHash(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
func(hashed, comparer []byte) error {
if string(hashed) != string(comparer) {
return zerrors.ThrowInternal(nil, "id", "invalid")
}
return nil
},
)
return mCrypto
}
func createMockCrypto(t *testing.T) Crypto {
mCrypto := NewMockCrypto(gomock.NewController(t))
func createMockCrypto(t *testing.T) EncryptionAlgorithm {
mCrypto := NewMockEncryptionAlgorithm(gomock.NewController(t))
mCrypto.EXPECT().Algorithm().AnyTimes().Return("crypto")
return mCrypto
}
func createMockGenerator(t *testing.T, crypto Crypto) Generator {
func createMockGenerator(t *testing.T, crypto EncryptionAlgorithm) Generator {
mGenerator := NewMockGenerator(gomock.NewController(t))
mGenerator.EXPECT().Alg().AnyTimes().Return(crypto)
return mGenerator

View File

@@ -102,25 +102,10 @@ func TestVerifyCode(t *testing.T) {
},
false,
},
{
"hash alg ok",
args{
creationDate: time.Now(),
expiry: 5 * time.Minute,
cryptoCode: &CryptoValue{
CryptoType: TypeHash,
Algorithm: "hash",
Crypted: []byte("code"),
},
verificationCode: "code",
g: createMockGenerator(t, CreateMockHashAlg(gomock.NewController(t))),
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := VerifyCode(tt.args.creationDate, tt.args.expiry, tt.args.cryptoCode, tt.args.verificationCode, tt.args.g); (err != nil) != tt.wantErr {
if err := VerifyCode(tt.args.creationDate, tt.args.expiry, tt.args.cryptoCode, tt.args.verificationCode, tt.args.g.Alg()); (err != nil) != tt.wantErr {
t.Errorf("VerifyCode() error = %v, wantErr %v", err, tt.wantErr)
}
})
@@ -222,85 +207,3 @@ func Test_verifyEncryptedCode(t *testing.T) {
})
}
}
func Test_verifyHashedCode(t *testing.T) {
type args struct {
cryptoCode *CryptoValue
verificationCode string
alg HashAlgorithm
}
tests := []struct {
name string
args args
wantErr bool
}{
{
"nil error",
args{
cryptoCode: nil,
verificationCode: "",
alg: CreateMockHashAlg(gomock.NewController(t)),
},
true,
},
{
"wrong cryptotype error",
args{
cryptoCode: &CryptoValue{
CryptoType: TypeEncryption,
Crypted: nil,
},
verificationCode: "",
alg: CreateMockHashAlg(gomock.NewController(t)),
},
true,
},
{
"wrong algorithm error",
args{
cryptoCode: &CryptoValue{
CryptoType: TypeHash,
Algorithm: "hash2",
Crypted: nil,
},
verificationCode: "",
alg: CreateMockHashAlg(gomock.NewController(t)),
},
true,
},
{
"wrong verification code error",
args{
cryptoCode: &CryptoValue{
CryptoType: TypeHash,
Algorithm: "hash",
Crypted: []byte("code"),
},
verificationCode: "wrong",
alg: CreateMockHashAlg(gomock.NewController(t)),
},
true,
},
{
"verification code ok",
args{
cryptoCode: &CryptoValue{
CryptoType: TypeHash,
Algorithm: "hash",
Crypted: []byte("code"),
},
verificationCode: "code",
alg: CreateMockHashAlg(gomock.NewController(t)),
},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := verifyHashedCode(tt.args.cryptoCode, tt.args.verificationCode, tt.args.alg); (err != nil) != tt.wantErr {
t.Errorf("verifyHashedCode() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -13,12 +13,8 @@ const (
TypeHash // Depcrecated: use [passwap.Swapper] instead
)
type Crypto interface {
Algorithm() string
}
type EncryptionAlgorithm interface {
Crypto
Algorithm() string
EncryptionKeyID() string
DecryptionKeyIDs() []string
Encrypt(value []byte) ([]byte, error)
@@ -26,13 +22,6 @@ type EncryptionAlgorithm interface {
DecryptString(hashed []byte, keyID string) (string, error)
}
// Depcrecated: use [passwap.Swapper] instead
type HashAlgorithm interface {
Crypto
Hash(value []byte) ([]byte, error)
CompareHash(hashed, comparer []byte) error
}
type CryptoValue struct {
CryptoType CryptoType
Algorithm string
@@ -59,14 +48,8 @@ func (c *CryptoValue) Scan(src interface{}) error {
type CryptoType int
func Crypt(value []byte, c Crypto) (*CryptoValue, error) {
switch alg := c.(type) {
case EncryptionAlgorithm:
return Encrypt(value, alg)
case HashAlgorithm:
return Hash(value, alg)
}
return nil, zerrors.ThrowInternal(nil, "CRYPT-r4IaHZ", "algorithm not supported")
func Crypt(value []byte, alg EncryptionAlgorithm) (*CryptoValue, error) {
return Encrypt(value, alg)
}
func Encrypt(value []byte, alg EncryptionAlgorithm) (*CryptoValue, error) {
@@ -108,33 +91,6 @@ func checkEncryptionAlgorithm(value *CryptoValue, alg EncryptionAlgorithm) error
return zerrors.ThrowInvalidArgument(nil, "CRYPT-Kq12vn", "value was encrypted with a different key")
}
func Hash(value []byte, alg HashAlgorithm) (*CryptoValue, error) {
hashed, err := alg.Hash(value)
if err != nil {
return nil, zerrors.ThrowInternal(err, "CRYPT-rBVaJU", "error hashing value")
}
return &CryptoValue{
CryptoType: TypeHash,
Algorithm: alg.Algorithm(),
Crypted: hashed,
}, nil
}
func CompareHash(value *CryptoValue, comparer []byte, alg HashAlgorithm) error {
if value.Algorithm != alg.Algorithm() {
return zerrors.ThrowInvalidArgument(nil, "CRYPT-HF32f", "value was hashed with a different algorithm")
}
return alg.CompareHash(value.Crypted, comparer)
}
func FillHash(value []byte, alg HashAlgorithm) *CryptoValue {
return &CryptoValue{
CryptoType: TypeHash,
Algorithm: alg.Algorithm(),
Crypted: value,
}
}
func CheckToken(alg EncryptionAlgorithm, token string, content string) error {
if token == "" {
return zerrors.ThrowPermissionDenied(nil, "CRYPTO-Sfefs", "Errors.Intent.InvalidToken")
@@ -152,3 +108,12 @@ func CheckToken(alg EncryptionAlgorithm, token string, content string) error {
}
return nil
}
// SecretOrEncodedHash returns the Crypted value from legacy [CryptoValue] if it is not nil.
// otherwise it will returns the encoded hash string.
func SecretOrEncodedHash(secret *CryptoValue, encoded string) string {
if secret != nil {
return string(secret.Crypted)
}
return encoded
}

View File

@@ -5,6 +5,7 @@
//
// mockgen -source crypto.go -destination ./crypto_mock.go -package crypto
//
// Package crypto is a generated GoMock package.
package crypto
@@ -14,43 +15,6 @@ import (
gomock "go.uber.org/mock/gomock"
)
// MockCrypto is a mock of Crypto interface.
type MockCrypto struct {
ctrl *gomock.Controller
recorder *MockCryptoMockRecorder
}
// MockCryptoMockRecorder is the mock recorder for MockCrypto.
type MockCryptoMockRecorder struct {
mock *MockCrypto
}
// NewMockCrypto creates a new mock instance.
func NewMockCrypto(ctrl *gomock.Controller) *MockCrypto {
mock := &MockCrypto{ctrl: ctrl}
mock.recorder = &MockCryptoMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockCrypto) EXPECT() *MockCryptoMockRecorder {
return m.recorder
}
// Algorithm mocks base method.
func (m *MockCrypto) Algorithm() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Algorithm")
ret0, _ := ret[0].(string)
return ret0
}
// Algorithm indicates an expected call of Algorithm.
func (mr *MockCryptoMockRecorder) Algorithm() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockCrypto)(nil).Algorithm))
}
// MockEncryptionAlgorithm is a mock of EncryptionAlgorithm interface.
type MockEncryptionAlgorithm struct {
ctrl *gomock.Controller
@@ -160,69 +124,3 @@ func (mr *MockEncryptionAlgorithmMockRecorder) EncryptionKeyID() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EncryptionKeyID", reflect.TypeOf((*MockEncryptionAlgorithm)(nil).EncryptionKeyID))
}
// MockHashAlgorithm is a mock of HashAlgorithm interface.
type MockHashAlgorithm struct {
ctrl *gomock.Controller
recorder *MockHashAlgorithmMockRecorder
}
// MockHashAlgorithmMockRecorder is the mock recorder for MockHashAlgorithm.
type MockHashAlgorithmMockRecorder struct {
mock *MockHashAlgorithm
}
// NewMockHashAlgorithm creates a new mock instance.
func NewMockHashAlgorithm(ctrl *gomock.Controller) *MockHashAlgorithm {
mock := &MockHashAlgorithm{ctrl: ctrl}
mock.recorder = &MockHashAlgorithmMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockHashAlgorithm) EXPECT() *MockHashAlgorithmMockRecorder {
return m.recorder
}
// Algorithm mocks base method.
func (m *MockHashAlgorithm) Algorithm() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Algorithm")
ret0, _ := ret[0].(string)
return ret0
}
// Algorithm indicates an expected call of Algorithm.
func (mr *MockHashAlgorithmMockRecorder) Algorithm() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Algorithm", reflect.TypeOf((*MockHashAlgorithm)(nil).Algorithm))
}
// CompareHash mocks base method.
func (m *MockHashAlgorithm) CompareHash(hashed, comparer []byte) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CompareHash", hashed, comparer)
ret0, _ := ret[0].(error)
return ret0
}
// CompareHash indicates an expected call of CompareHash.
func (mr *MockHashAlgorithmMockRecorder) CompareHash(hashed, comparer any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompareHash", reflect.TypeOf((*MockHashAlgorithm)(nil).CompareHash), hashed, comparer)
}
// Hash mocks base method.
func (m *MockHashAlgorithm) Hash(value []byte) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Hash", value)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Hash indicates an expected call of Hash.
func (mr *MockHashAlgorithmMockRecorder) Hash(value any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Hash", reflect.TypeOf((*MockHashAlgorithm)(nil).Hash), value)
}

View File

@@ -60,7 +60,7 @@ func (a *alg) Algorithm() string {
func TestCrypt(t *testing.T) {
type args struct {
value []byte
c Crypto
c EncryptionAlgorithm
}
tests := []struct {
name string
@@ -74,18 +74,6 @@ func TestCrypt(t *testing.T) {
&CryptoValue{CryptoType: TypeEncryption, Algorithm: "enc", KeyID: "keyID", Crypted: []byte("test")},
false,
},
{
"hash",
args{[]byte("test"), &mockHashCrypto{}},
&CryptoValue{CryptoType: TypeHash, Algorithm: "hash", Crypted: []byte("test")},
false,
},
{
"wrong type",
args{[]byte("test"), &alg{}},
nil,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
@@ -208,66 +196,3 @@ func TestDecryptString(t *testing.T) {
})
}
}
func TestHash(t *testing.T) {
type args struct {
value []byte
c HashAlgorithm
}
tests := []struct {
name string
args args
want *CryptoValue
wantErr bool
}{
{
"ok",
args{[]byte("test"), &mockHashCrypto{}},
&CryptoValue{CryptoType: TypeHash, Algorithm: "hash", Crypted: []byte("test")},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Hash(tt.args.value, tt.args.c)
if (err != nil) != tt.wantErr {
t.Errorf("Hash() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Hash() = %v, want %v", got, tt.want)
}
})
}
}
func TestCompareHash(t *testing.T) {
type args struct {
value *CryptoValue
comparer []byte
c HashAlgorithm
}
tests := []struct {
name string
args args
wantErr bool
}{
{
"ok",
args{&CryptoValue{CryptoType: TypeHash, Algorithm: "hash", Crypted: []byte("test")}, []byte("test"), &mockHashCrypto{}},
false,
},
{
"wrong",
args{&CryptoValue{CryptoType: TypeHash, Algorithm: "hash", Crypted: []byte("test")}, []byte("test2"), &mockHashCrypto{}},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CompareHash(tt.args.value, tt.args.comparer, tt.args.c); (err != nil) != tt.wantErr {
t.Errorf("CompareHash() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

View File

@@ -16,12 +16,12 @@ import (
"github.com/zitadel/zitadel/internal/zerrors"
)
type PasswordHasher struct {
type Hasher struct {
*passwap.Swapper
Prefixes []string
}
func (h *PasswordHasher) EncodingSupported(encodedHash string) bool {
func (h *Hasher) EncodingSupported(encodedHash string) bool {
for _, prefix := range h.Prefixes {
if strings.HasPrefix(encodedHash, prefix) {
return true
@@ -54,12 +54,12 @@ const (
HashModeSHA512 HashMode = "sha512"
)
type PasswordHashConfig struct {
type HashConfig struct {
Verifiers []HashName
Hasher HasherConfig
}
func (c *PasswordHashConfig) PasswordHasher() (*PasswordHasher, error) {
func (c *HashConfig) NewHasher() (*Hasher, error) {
verifiers, vPrefixes, err := c.buildVerifiers()
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "CRYPT-sahW9", "password hash config invalid")
@@ -68,7 +68,7 @@ func (c *PasswordHashConfig) PasswordHasher() (*PasswordHasher, error) {
if err != nil {
return nil, zerrors.ThrowInvalidArgument(err, "CRYPT-Que4r", "password hash config invalid")
}
return &PasswordHasher{
return &Hasher{
Swapper: passwap.NewSwapper(hasher, verifiers...),
Prefixes: append(hPrefixes, vPrefixes...),
}, nil
@@ -105,7 +105,7 @@ var knowVerifiers = map[HashName]prefixVerifier{
},
}
func (c *PasswordHashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) {
func (c *HashConfig) buildVerifiers() (verifiers []verifier.Verifier, prefixes []string, err error) {
verifiers = make([]verifier.Verifier, len(c.Verifiers))
prefixes = make([]string, 0, len(c.Verifiers)+1)
for i, name := range c.Verifiers {

View File

@@ -49,7 +49,7 @@ func TestPasswordHasher_EncodingSupported(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := &PasswordHasher{
h := &Hasher{
Prefixes: []string{bcrypt.Prefix, argon2.Prefix},
}
got := h.EncodingSupported(tt.encodedHash)
@@ -340,11 +340,11 @@ func TestPasswordHashConfig_PasswordHasher(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &PasswordHashConfig{
c := &HashConfig{
Verifiers: tt.fields.Verifiers,
Hasher: tt.fields.Hasher,
}
got, err := c.PasswordHasher()
got, err := c.NewHasher()
if tt.wantErr {
assert.Error(t, err)
return