diff --git a/hasher/hasher_test.go b/hasher/hasher_test.go index a334dff..97ccab5 100644 --- a/hasher/hasher_test.go +++ b/hasher/hasher_test.go @@ -61,9 +61,9 @@ func TestSHA256Hasher(t *testing.T) { t.Parallel() tests := []struct { - name string - in []byte - out string + name string + in []byte + expected string }{ {"empty", []byte(""), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}, {"abc", []byte("abc"), "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}, @@ -77,7 +77,7 @@ func TestSHA256Hasher(t *testing.T) { result, _ := h.Hash(test.in) - assert.Equal(t, test.out, hex.EncodeToString(result)) + assert.Equal(t, test.expected, hex.EncodeToString(result)) }) } } diff --git a/kv/kv.go b/kv/kv.go index 178d9a2..6a5e5cb 100644 --- a/kv/kv.go +++ b/kv/kv.go @@ -9,7 +9,6 @@ type KeyValue struct { Key []byte // Value is the serialized representation of the value. Value []byte - // ModRevision is the revision number of the last modification to this key. ModRevision int64 } diff --git a/marshaller/marshaller.go b/marshaller/marshaller.go index 83702b4..9ec6f14 100644 --- a/marshaller/marshaller.go +++ b/marshaller/marshaller.go @@ -17,7 +17,7 @@ var ErrUnmarshall = errors.New("failed to unmarshal") // implements one time for all objects. // Required for `integrity.Storage` to set marshalling format for any type object // and as recommendation for developers of `Storage` wrappers. -type DefaultMarshaller interface { +type DefaultMarshaller interface { //nolint:iface Marshal(data any) ([]byte, error) Unmarshal(data []byte, out any) error } @@ -25,8 +25,8 @@ type DefaultMarshaller interface { // Marshallable - custom object serialization, implements for each object. // Required for `integrity.Storage` type to set marshalling format to specific object // and as recommendation for developers of `Storage` wrappers. -type Marshallable interface { - Marshal() ([]byte, error) +type Marshallable interface { //nolint:iface + Marshal(data any) ([]byte, error) Unmarshal(data []byte, out any) error } @@ -34,7 +34,7 @@ type Marshallable interface { type YAMLMarshaller struct{} // NewYamlMarshaller creates new NewYamlMarshaller object. -func NewYamlMarshaller() YAMLMarshaller { +func NewYamlMarshaller() Marshallable { return YAMLMarshaller{} } diff --git a/namer/key.go b/namer/key.go new file mode 100644 index 0000000..ab9750e --- /dev/null +++ b/namer/key.go @@ -0,0 +1,70 @@ +package namer + +// KeyType represents key types. +type KeyType int + +const ( + // KeyTypeValue represents data type. + KeyTypeValue KeyType = iota + 1 + // KeyTypeHash represents hash of the data type. + KeyTypeHash + // KeyTypeSignature represents signature of the data type. + KeyTypeSignature +) + +// Key defines the minimal interface required by keys. +type Key interface { + Name() string // Get object name. + Type() KeyType // Get key type. + Property() string // Get metadata (e.g., algorithm version). + Raw() []byte // Get raw data. + Build() string // Reconstruct raw key string. +} + +// DefaultKey implements default realization. +type DefaultKey struct { + name string // Object identifier. + keytype KeyType // Type of object (hash/signature/value). + property string // Additional metadata (version/algorithm). + raw []byte // Raw key string. +} + +// NewDefaultKey returns new Key object. +func NewDefaultKey(n string, k KeyType, p string, r []byte) DefaultKey { + return DefaultKey{ + name: n, + keytype: k, + property: p, + raw: r, + } +} + +// Name returns name of the key. +func (k DefaultKey) Name() string { + return k.name +} + +// Type returns type of the key. +func (k DefaultKey) Type() KeyType { + return k.keytype +} + +// Property returns property of the key. +func (k DefaultKey) Property() string { + return k.property +} + +// Raw returns raw of the key. +func (k DefaultKey) Raw() []byte { + return k.raw +} + +// Build should reconstruct key from signature and digest or not? +func (k DefaultKey) Build() string { + return string(k.raw) +} + +// String returns string representation of the `raw` field. +func (k DefaultKey) String() string { + return string(k.raw) +} diff --git a/namer/namer.go b/namer/namer.go index f272a00..ab2e551 100644 --- a/namer/namer.go +++ b/namer/namer.go @@ -2,41 +2,276 @@ package namer import ( + "bytes" + "errors" + "fmt" + "iter" + "strings" + "github.com/tarantool/go-storage/kv" -) + "github.com/tarantool/go-storage/verification" -// KeyType represents key types. -type KeyType int + "github.com/tarantool/go-storage/hasher" + "github.com/tarantool/go-storage/marshaller" +) const ( - // KeyTypeValue represents data type. - KeyTypeValue KeyType = iota + 1 - // KeyTypeHash represents hash of the data type. - KeyTypeHash - // KeyTypeSignature represents signature of the data type. - KeyTypeSignature + hashName = "hash" + signatureName = "sig" + keysPerName = 3 ) -// Key implements internal realization. -type Key struct { - Name string // Object identificator. - Type KeyType // Type of the object. - Property string // Additional information (version/algorithm). +var ( + // ErrInvalidKey is returned when missing key, hash or signature. + ErrInvalidKey = errors.New("missing key, hash or signature") + // ErrHashMismatch is returned when hash mismatch. + ErrHashMismatch = errors.New("hash mismatch") + // ErrInvalidInput is returned when input data is invalid. + ErrInvalidInput = errors.New("failed to generate: invalid input data") + // ErrInvalidPrefix is returned when prefix didn't match. + ErrInvalidPrefix = errors.New("invalid prefix") +) + +//----------------------------------------------------------------------------- + +// Results represents Namer working result. +type Results struct { + IsSingle bool // True if result contains only one object name. + IsSingleName string // Cached name when isSingle=true. + Result map[string][]Key // Grouped keys: object name -> key list. +} + +// SelectSingle gets keys for single-name case (if applicable). +func (r *Results) SelectSingle() ([]Key, bool) { + if r.IsSingle { + return r.Result[r.IsSingleName], true + } + + return nil, false +} + +// Items return iterator over all name->keys groups. +func (r *Results) Items() iter.Seq2[string, []Key] { + return func(yield func(str string, res []Key) bool) { + for i, v := range r.Result { + if !yield(i, v) { + return + } + } + } +} + +// Select gets keys for a specific object name. +func (r *Results) Select(name string) ([]Key, bool) { + if i, ok := r.Result[name]; ok { + return i, true + } + + return nil, false +} + +// Len returns the number of unique object names. +func (r *Results) Len() int { + return len(r.Result) +} + +//----------------------------------------------------------------------------- + +// DefaultNamer represents default namer. +type DefaultNamer struct { + prefix string + hasher hasher.Hasher + signerverifier verification.SignerVerifier + marshaller marshaller.Marshallable +} + +// NewDefaultNamer returns new DefaultNamer object. +func NewDefaultNamer(prefix string, hasher hasher.Hasher, signerverifier verification.SignerVerifier, + marshaller marshaller.Marshallable, +) *DefaultNamer { + prefix = strings.TrimPrefix(prefix, "/") + + return &DefaultNamer{ + prefix: prefix, + hasher: hasher, + signerverifier: signerverifier, + marshaller: marshaller, + } +} + +// GenerateMulti generates all keys for an object names. +func (n *DefaultNamer) GenerateMulti(kvs []kv.KeyValue) Results { + var out Results + + out.Result = make(map[string][]Key) + + for _, keyvalue := range kvs { + keys, err := n.Generate(keyvalue) + if err != nil { + continue + } + + out.Result[string(keyvalue.Key)] = keys + } + + if len(kvs) == 1 { + out.IsSingle = true + out.IsSingleName = string(kvs[0].Key) + } + + return out } -// Namer represents keys naming strategy. -type Namer interface { - GenerateNames(name string) []string // Object's keys generation. - ParseNames(names []string) []Key // Convert names into keys. +// Generate generates all required keys for an object name. +func (n *DefaultNamer) Generate(keyvalue kv.KeyValue) ([]Key, error) { + name := string(keyvalue.Key) + if name == "" { + return nil, ErrInvalidInput + } + + marshalled, err := n.marshaller.Marshal(keyvalue.Value) + if err != nil { + return nil, fmt.Errorf("failed to marshal: %w", err) + } + + hash, err := n.hasher.Hash(marshalled) + if err != nil { + return nil, fmt.Errorf("failed to hash: %w", err) + } + + signature, err := n.signerverifier.Sign(hash) + if err != nil { + return nil, fmt.Errorf("failed to sign: %w", err) + } + + valK := NewDefaultKey( + "/"+n.prefix+"/"+name, + KeyTypeValue, + "", + marshalled, + ) + hashK := NewDefaultKey( + "/"+n.prefix+"/"+hashName+"/"+n.hasher.Name()+"/"+name, + KeyTypeHash, + n.hasher.Name(), + hash, + ) + sigK := NewDefaultKey( + "/"+n.prefix+"/"+signatureName+"/"+name, + KeyTypeSignature, + "", + signature, + ) + + return []Key{valK, hashK, sigK}, nil } -// Generator generates signer K/V pairs. -// Implementation should use `generic` and will used for strong typing of the solution. -type Generator[T any] interface { - Generate(name string, value T) ([]kv.KeyValue, error) +// ParseKVMulti convert set of raw KVs to set of KVs with original key/values. +func (n *DefaultNamer) ParseKVMulti(kvs []kv.KeyValue) ([]kv.KeyValue, error) { + out := make([]kv.KeyValue, 0, len(kvs)/keysPerName) + + var temp_results Results + + // Combine keys in threes to get complete set of data to decode the original value. + for _, keyvalue := range kvs { + key, err := n.ParseKV(keyvalue) + if err != nil { + return nil, err + } + + temp_results.Result[key.Name()] = append(temp_results.Result[key.Name()], key) + } + + // Decoding. + for _, keyset := range temp_results.Items() { + keyvalue, err := n.DecodeKey(keyset) + if err != nil { + return nil, err + } + + out = append(out, keyvalue) + } + + return out, nil } -// Validator validates and build the object from K/V. -type Validator[T any] interface { - Validate(pairs []kv.KeyValue) (T, error) +// ParseKV converts KV to Key to combine it by `name`. +func (n *DefaultNamer) ParseKV(keyvalue kv.KeyValue) (Key, error) { + var out Key + + result, cut := strings.CutPrefix(string(keyvalue.Key), n.prefix) + if !cut { + return DefaultKey{}, ErrInvalidPrefix + } + + result, _ = strings.CutPrefix(result, "/") + + parts := strings.Split(result, "/") + partsLen := len(parts) + + switch parts[0] { + case hashName + "/": + out = NewDefaultKey( + parts[partsLen-1], + KeyTypeHash, + parts[partsLen-2], + keyvalue.Value, + ) + case signatureName + "/": + out = NewDefaultKey( + parts[partsLen-1], + KeyTypeSignature, + "", + keyvalue.Value, + ) + default: + out = NewDefaultKey( + parts[partsLen-1], + KeyTypeValue, + "", + keyvalue.Value, + ) + } + + return out, nil +} + +// DecodeKey converts sets of three Keys into original KV. +func (n *DefaultNamer) DecodeKey(keyset []Key) (kv.KeyValue, error) { + var out kv.KeyValue + + var marshalledValue, hash, signature []byte + + for _, key := range keyset { + switch { + case strings.Contains(key.Name(), hashName): + hash = key.Raw() + case strings.Contains(key.Name(), signatureName): + signature = key.Raw() + default: + out.Key = []byte(key.Name()) + marshalledValue = key.Raw() + } + } + + if out.Key == nil || hash == nil || signature == nil { + return out, ErrInvalidKey + } + + err := n.marshaller.Unmarshal(marshalledValue, &out.Value) + if err != nil { + return out, fmt.Errorf("failed to unmarshal: %w", err) + } + + computedHash, err := n.hasher.Hash(out.Key) + if !bytes.Equal(computedHash, hash) || err != nil { + return out, ErrHashMismatch + } + + err = n.signerverifier.Verify(out.Value, signature) + if err != nil { + return out, fmt.Errorf("signature verification failed: %w", err) + } + + return out, nil } diff --git a/namer/namer_test.go b/namer/namer_test.go new file mode 100644 index 0000000..e8ea606 --- /dev/null +++ b/namer/namer_test.go @@ -0,0 +1,142 @@ +package namer_test + +import ( + "crypto/rand" + "crypto/rsa" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/go-storage/hasher" + "github.com/tarantool/go-storage/kv" + "github.com/tarantool/go-storage/marshaller" + "github.com/tarantool/go-storage/namer" + + "github.com/tarantool/go-storage/verification" +) + +func TestGenerate(t *testing.T) { + t.Parallel() + + tests := []struct { + prefix string + name string + value string + expected namer.Results + }{ + { + "/tt/config", + "all", + "value", + namer.Results{ + IsSingle: true, + IsSingleName: "all", + Result: map[string][]namer.Key{ + "all": []namer.Key{ + namer.DefaultKey{ + name: "/tt/config/all", + keytype: 1, + property: "", + raw: []uint8{ + 0x2d, 0x20, 0x31, 0x31, 0x38, 0xa, 0x2d, 0x20, 0x39, 0x37, 0xa, 0x2d, 0x20, 0x31, 0x30, 0x38, + 0xa, 0x2d, 0x20, 0x31, 0x31, 0x37, 0xa, 0x2d, 0x20, 0x31, 0x30, 0x31, 0xa, + }, + }, + namer.DefaultKey{ + name: "/tt/config/hash/sha256/all", + keytype: 2, + property: "sha256", + raw: []uint8{ + 0xa3, 0xc7, 0xb9, 0x70, 0xc5, 0x1b, 0x9b, 0xee, 0x4e, 0xdc, 0x82, 0xfe, 0x2d, 0x79, 0xf8, 0x15, + 0x24, 0x8d, 0xbb, 0x2e, 0x3a, 0x9b, 0x26, 0x63, 0x6e, 0xf2, 0xa3, 0xbe, 0xed, 0xbf, 0xc7, 0x58, + }, + }, + namer.DefaultKey{ + name: "/tt/config/sig/all", + keytype: 3, + property: "", + raw: []uint8{ + 0x2d, 0x38, 0x3b, 0xa1, 0x4a, 0xd3, 0x57, 0x18, 0x21, 0x63, 0x80, 0x2b, 0x99, 0xcc, 0xf1, 0x0f, + 0x4f, 0x7d, 0x7e, 0xb2, 0x23, 0x89, 0xc9, 0x8c, 0xb6, 0x73, 0xd0, 0x8e, 0x37, 0x98, 0x7f, 0xef, + 0xfb, 0x70, 0x1c, 0x25, 0x7d, 0x68, 0x03, 0x44, 0x73, 0x14, 0xe8, 0x3e, 0x33, 0x3d, 0x9d, 0x79, + 0xec, 0x83, 0x95, 0x63, 0x68, 0x79, 0xa3, 0xc1, 0x23, 0x32, 0x31, 0x18, 0x94, 0xb3, 0xad, 0x9a, + 0x16, 0x67, 0xdb, 0x58, 0x28, 0x6e, 0x95, 0x4c, 0x7e, 0x7b, 0xe8, 0xbf, 0x2e, 0x44, 0x31, 0xd8, + 0x4a, 0x62, 0x26, 0x5d, 0x6c, 0xb6, 0x67, 0x05, 0xcf, 0xaf, 0x22, 0x44, 0xef, 0x0c, 0x32, 0x45, + 0xf4, 0xcd, 0xf0, 0x31, 0xb9, 0xa3, 0x90, 0xb1, 0x0a, 0x4e, 0xe3, 0x16, 0x58, 0x81, 0xf5, 0x69, + 0x58, 0x3c, 0x1d, 0x67, 0x0b, 0x65, 0x0d, 0x38, 0x06, 0x87, 0xb5, 0x5c, 0x0a, 0x74, 0x84, 0xf0, + 0x6d, 0xa1, 0x20, 0xb1, 0x12, 0x81, 0xb4, 0xcb, 0xd3, 0x65, 0x2a, 0x3d, 0x4c, 0xf3, 0x8d, 0x9b, + 0x51, 0x68, 0x18, 0x3e, 0xbf, 0x78, 0x22, 0x33, 0xca, 0x20, 0x55, 0x24, 0xba, 0x57, 0x2b, 0xcf, + 0x43, 0xd0, 0x38, 0xbd, 0x51, 0x42, 0xef, 0x46, 0x45, 0x94, 0xde, 0xca, 0xda, 0x04, 0x2e, 0xc2, + 0x61, 0x54, 0xf7, 0xb3, 0xfd, 0x33, 0xd6, 0x1b, 0x48, 0xd3, 0x9a, 0x08, 0xfe, 0xad, 0xfb, 0x0a, + 0x7b, 0x1b, 0x8d, 0xd5, 0x9b, 0xe9, 0xfe, 0xe1, 0x69, 0x1c, 0xad, 0xf4, 0xa0, 0x7b, 0xad, 0xe6, + 0x53, 0x32, 0xc2, 0x37, 0x62, 0x3d, 0xab, 0xcd, 0x18, 0x82, 0x3a, 0xc6, 0x8b, 0x12, 0xd1, 0x28, + 0xe1, 0x72, 0xb1, 0xf9, 0x5a, 0x31, 0xb8, 0xa1, 0x72, 0x54, 0xbf, 0x4d, 0xc8, 0xa7, 0x07, 0xf5, + 0x05, 0xf0, 0x67, 0xeb, 0x32, 0xda, 0x41, 0x82, 0xeb, 0xdb, 0xcb, 0x96, 0xfe, 0x95, 0x19, 0x8e, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + hasher := hasher.NewSHA256Hasher() + + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + require.NoError(t, err) + + sv := verification.NewRSAPSS(*privateKey, privateKey.PublicKey) + + mr := marshaller.NewYamlMarshaller() + + dn := namer.NewDefaultNamer(tt.prefix, hasher, sv, mr) + + result := dn.GenerateMulti([]kv.KeyValue{ + { + Key: []byte(tt.name), + Value: []byte(tt.value), + ModRevision: 1, + }}, + ) + require.Equal(t, tt.expected, result) + }) + } +} + +/* +func TestParseNames(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + prefix string + names []string + expected []namer.Key + }{ + { + "all", + "/tt/config", + []string{"/tt/config/all", "/tt/config/hash/all", "/tt/config/sig/all"}, + []namer.Key{ + {Name: "/tt/config/all", Type: namer.KeyTypeValue, Property: ""}, + {Name: "/tt/config/hash/all", Type: namer.KeyTypeHash, Property: ""}, + {Name: "/tt/config/sig/all", Type: namer.KeyTypeSignature, Property: ""}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + dn := namer.NewDefaultNamer(tt.prefix) + + result := dn.ParseNames(tt.names) + require.Equal(t, tt.expected, result) + }) + } +}. +*/ diff --git a/crypto/interfaces.go b/verification/interfaces.go similarity index 88% rename from crypto/interfaces.go rename to verification/interfaces.go index 0dcbab3..f9acfe8 100644 --- a/crypto/interfaces.go +++ b/verification/interfaces.go @@ -1,5 +1,5 @@ -// Package crypto implements crypto interfaces. -package crypto +// Package verification implements verification interfaces. +package verification // Signer implements high-level API for package signing. type Signer interface { diff --git a/crypto/rsa.go b/verification/rsa.go similarity index 89% rename from crypto/rsa.go rename to verification/rsa.go index 0f2234e..bdcc3dd 100644 --- a/crypto/rsa.go +++ b/verification/rsa.go @@ -1,4 +1,4 @@ -package crypto +package verification import ( "crypto" @@ -29,12 +29,12 @@ func NewRSAPSS(privKey rsa.PrivateKey, pubKey rsa.PublicKey) RSAPSS { } // Name implements SignerVerifier interface. -func (r *RSAPSS) Name() string { +func (r RSAPSS) Name() string { return "RSASSA-PSS" } // Sign generates SHA-256 digest and signs it using RSASSA-PSS. -func (r *RSAPSS) Sign(data []byte) ([]byte, error) { +func (r RSAPSS) Sign(data []byte) ([]byte, error) { digest, err := r.hasher.Hash(data) if err != nil { return []byte{}, fmt.Errorf("failed to get hash: %w", err) @@ -52,7 +52,7 @@ func (r *RSAPSS) Sign(data []byte) ([]byte, error) { } // Verify compares data with signature. -func (r *RSAPSS) Verify(data []byte, signature []byte) error { +func (r RSAPSS) Verify(data []byte, signature []byte) error { digest, err := r.hasher.Hash(data) if err != nil { return fmt.Errorf("failed to get hash: %w", err) diff --git a/crypto/rsa_test.go b/verification/rsa_test.go similarity index 81% rename from crypto/rsa_test.go rename to verification/rsa_test.go index 3645f35..091f305 100644 --- a/crypto/rsa_test.go +++ b/verification/rsa_test.go @@ -1,4 +1,4 @@ -package crypto_test +package verification_test import ( "crypto/rand" @@ -6,13 +6,14 @@ import ( "testing" "github.com/stretchr/testify/require" - "github.com/tarantool/go-storage/crypto" + + "github.com/tarantool/go-storage/verification" ) func TestRsaWithoutKeys(t *testing.T) { t.Parallel() - rsapss := crypto.NewRSAPSS(rsa.PrivateKey{}, rsa.PublicKey{}) //nolint:exhaustruct + rsapss := verification.NewRSAPSS(rsa.PrivateKey{}, rsa.PublicKey{}) //nolint:exhaustruct require.NotNil(t, rsapss, "rsapss must be returned") data := []byte("abc") @@ -31,7 +32,7 @@ func TestRsaOnlyPrivateKey(t *testing.T) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) - rsapss := crypto.NewRSAPSS(*privateKey, rsa.PublicKey{}) //nolint:exhaustruct + rsapss := verification.NewRSAPSS(*privateKey, rsa.PublicKey{}) //nolint:exhaustruct require.NotNil(t, rsapss, "rsapss must be returned") data := []byte("abc") @@ -50,7 +51,7 @@ func TestRsaOnlyPublicKey(t *testing.T) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) - rsapss := crypto.NewRSAPSS(rsa.PrivateKey{}, privateKey.PublicKey) //nolint:exhaustruct + rsapss := verification.NewRSAPSS(rsa.PrivateKey{}, privateKey.PublicKey) //nolint:exhaustruct require.NotNil(t, rsapss, "rsapss must be returned") data := []byte("abc") @@ -60,7 +61,7 @@ func TestRsaOnlyPublicKey(t *testing.T) { require.Nil(t, sig, "signature must be nil") // Re-create to have a valid sign. - rsapss = crypto.NewRSAPSS(*privateKey, privateKey.PublicKey) + rsapss = verification.NewRSAPSS(*privateKey, privateKey.PublicKey) require.NotNil(t, rsapss, "rsapss must be returned") sign, err := rsapss.Sign(data) @@ -77,7 +78,7 @@ func TestRsaSignVerify(t *testing.T) { privateKey, err := rsa.GenerateKey(rand.Reader, 2048) require.NoError(t, err) - rsapss := crypto.NewRSAPSS(*privateKey, privateKey.PublicKey) + rsapss := verification.NewRSAPSS(*privateKey, privateKey.PublicKey) require.NotNil(t, rsapss, "rsapss must be returned") data := []byte("abc")