diff --git a/README.md b/README.md
index 62205b5..0f431c2 100644
--- a/README.md
+++ b/README.md
@@ -16,11 +16,24 @@ Run:
# edit .env
go run main.go
+_Note:
+The emulator creates new account addresses deterministically. This means that deleting the emulators docker volume will cause the emulator to start from the beginning and give the same addresses as before possibly ending in duplicate key errors in database._
+
## Configuration
### Database
-| Config variable | ENV | descrpition | default | examples |
-| --------------- | :-----: | ------------------------------------------------------------------------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| DatabaseType | DB_TYPE | Type of database driver | sqlite | |
-| DatabaseDSN | DB_DSN | Data source name ([DSN](https://en.wikipedia.org/wiki/Data_source_name)) for database connection | wallet.db | mysql://john:pass@localhost:3306/my_db
user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai
For more: https://gorm.io/docs/connecting_to_the_database.html |
+| Config variable | ENV | descrpition | default | examples |
+| --------------- | :-----: | ------------------------------------------------------------------------------------------------ | --------- | --------- |
+| DatabaseType | DB_TYPE | Type of database driver | sqlite | |
+| DatabaseDSN | DB_DSN | Data source name ([DSN](https://en.wikipedia.org/wiki/Data_source_name)) for database connection | wallet.db | See below |
+
+Examples of Database DSN
+
+ mysql://john:pass@localhost:3306/my_db
+
+ user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local
+
+ host=localhost user=gorm password=gorm dbname=gorm port=9920 sslmode=disable TimeZone=Asia/Shanghai
+
+For more: https://gorm.io/docs/connecting_to_the_database.html
diff --git a/pkg/account/account.go b/account/account.go
similarity index 81%
rename from pkg/account/account.go
rename to account/account.go
index 6dfded4..7c9824e 100644
--- a/pkg/account/account.go
+++ b/account/account.go
@@ -4,9 +4,9 @@ import (
"context"
"fmt"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/flow_helpers"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/keys"
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "github.com/eqlabs/flow-nft-wallet-service/flow_helpers"
+ "github.com/eqlabs/flow-nft-wallet-service/keys"
"github.com/onflow/flow-go-sdk"
"github.com/onflow/flow-go-sdk/client"
"github.com/onflow/flow-go-sdk/templates"
@@ -15,13 +15,13 @@ import (
func Create(
ctx context.Context,
fc *client.Client,
- ks keys.Store,
+ km keys.Manager,
) (
newAccount data.Account,
- newAccountKey data.AccountKey,
+ newKey keys.Key,
err error,
) {
- serviceAuth, err := ks.ServiceAuthorizer(ctx, fc)
+ serviceAuth, err := km.AdminAuthorizer()
if err != nil {
return
}
@@ -32,12 +32,12 @@ func Create(
}
// Generate a new key pair
- wrapped, err := ks.Generate(ctx, 0, flow.AccountKeyWeightThreshold)
+ wrapped, err := km.Generate(0, flow.AccountKeyWeightThreshold)
if err != nil {
return
}
publicKey := wrapped.FlowKey
- newAccountKey = wrapped.AccountKey
+ newKey = wrapped.AccountKey
// Setup a transaction to create an account
tx := templates.CreateAccount([]*flow.AccountKey{publicKey}, nil, serviceAuth.Address)
@@ -82,7 +82,6 @@ func Create(
return
}
- newAccountKey.AccountAddress = newAddress
newAccount.Address = newAddress
return
diff --git a/pkg/account/service.go b/account/service.go
similarity index 73%
rename from pkg/account/service.go
rename to account/service.go
index 83630e6..555c38f 100644
--- a/pkg/account/service.go
+++ b/account/service.go
@@ -5,8 +5,8 @@ import (
"fmt"
"log"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/keys"
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "github.com/eqlabs/flow-nft-wallet-service/keys"
"github.com/onflow/flow-go-sdk"
"github.com/onflow/flow-go-sdk/client"
)
@@ -14,7 +14,7 @@ import (
type Service struct {
l *log.Logger
db data.Store
- ks keys.Store
+ km keys.Manager
fc *client.Client
chainId flow.ChainID // TODO: how do we want to handle different chains?
}
@@ -22,9 +22,9 @@ type Service struct {
func NewService(
l *log.Logger,
db data.Store,
- ks keys.Store,
+ km keys.Manager,
fc *client.Client) *Service {
- return &Service{l, db, ks, fc, flow.Emulator}
+ return &Service{l, db, km, fc, flow.Emulator}
}
func (s *Service) List(ctx context.Context) (accounts []data.Account, err error) {
@@ -33,18 +33,18 @@ func (s *Service) List(ctx context.Context) (accounts []data.Account, err error)
}
func (s *Service) Create(ctx context.Context) (account data.Account, err error) {
- account, key, err := Create(ctx, s.fc, s.ks)
+ account, key, err := Create(ctx, s.fc, s.km)
if err != nil {
return
}
- // Store the generated key
- err = s.ks.Save(key)
+ accountKey, err := s.km.Save(key)
if err != nil {
return
}
+ account.Keys = []data.Key{accountKey}
- // Store the account
+ // Store
err = s.db.InsertAccount(account)
return
@@ -55,7 +55,15 @@ func (s *Service) Details(ctx context.Context, address string) (account data.Acc
if err != nil {
return
}
+
+ // keys, err := s.db.AccountKeys(address)
+ // if err != nil {
+ // return
+ // }
+
account, err = s.db.Account(address)
+ // account.Keys = keys
+
return
}
diff --git a/api/account.http b/api/account.http
index fb82ce1..32d5ddc 100644
--- a/api/account.http
+++ b/api/account.http
@@ -6,5 +6,5 @@ POST http://localhost:3000/v1/accounts HTTP/1.1
content-type: application/json
###
-GET http://localhost:3000/v1/accounts/test HTTP/1.1
+GET http://localhost:3000/v1/accounts/0f7025fa05b578e3 HTTP/1.1
content-type: application/json
diff --git a/data/gorm/accountstore.go b/data/gorm/accountstore.go
new file mode 100644
index 0000000..fcac1d2
--- /dev/null
+++ b/data/gorm/accountstore.go
@@ -0,0 +1,38 @@
+package gorm
+
+import (
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "gorm.io/gorm"
+)
+
+type AccountStore struct {
+ db *gorm.DB
+}
+
+func newAccountStore(db *gorm.DB) *AccountStore {
+ db.AutoMigrate(&data.Account{}, &data.Key{})
+ return &AccountStore{db}
+}
+
+// List all accounts
+func (s *AccountStore) Accounts() (accounts []data.Account, err error) {
+ err = s.db.Select("address").Find(&accounts).Error
+ return
+}
+
+// Insert new account
+func (s *AccountStore) InsertAccount(account data.Account) error {
+ return s.db.Create(&account).Error
+}
+
+// Get account details
+func (s *AccountStore) Account(address string) (account data.Account, err error) {
+ err = s.db.Preload("Keys").First(&account, "address = ?", address).Error
+ return
+}
+
+// Get account key with index
+func (s *AccountStore) AccountKey(address string, index int) (key data.Key, err error) {
+ err = s.db.Where("account_address = ? AND index = ?", address, index).First(&key).Error
+ return
+}
diff --git a/data/gorm/store.go b/data/gorm/store.go
new file mode 100644
index 0000000..aa0d0a2
--- /dev/null
+++ b/data/gorm/store.go
@@ -0,0 +1,20 @@
+package gorm
+
+import (
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "gorm.io/gorm"
+)
+
+type Store struct {
+ data.AccountStore
+}
+
+func NewStore(dialector gorm.Dialector) (*Store, error) {
+ db, err := gorm.Open(dialector, &gorm.Config{})
+ if err != nil {
+ return &Store{}, err
+ }
+ return &Store{
+ AccountStore: newAccountStore(db),
+ }, nil
+}
diff --git a/data/store.go b/data/store.go
new file mode 100644
index 0000000..2631f6d
--- /dev/null
+++ b/data/store.go
@@ -0,0 +1,45 @@
+package data
+
+import (
+ "time"
+
+ "gorm.io/gorm"
+)
+
+const (
+ DB_TYPE_POSTGRESQL = "psql"
+ DB_TYPE_MYSQL = "mysql"
+ DB_TYPE_SQLITE = "sqlite"
+)
+
+type Store interface {
+ AccountStore
+}
+
+type AccountStore interface {
+ Accounts() ([]Account, error)
+ InsertAccount(a Account) error
+ Account(address string) (Account, error)
+ AccountKey(address string, index int) (Key, error)
+}
+
+// Storable account
+type Account struct {
+ Address string `json:"address" gorm:"primaryKey"`
+ Keys []Key `json:"keys" gorm:"foreignKey:AccountAddress;references:Address;constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
+ CreatedAt time.Time `json:"-"`
+ UpdatedAt time.Time `json:"-"`
+ DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
+}
+
+// Storable account key
+type Key struct {
+ ID int `json:"-" gorm:"primaryKey"`
+ AccountAddress string `json:"-" gorm:"index"`
+ Index int `json:"index" gorm:"index"`
+ Type string `json:"type"`
+ Value []byte `json:"-"`
+ CreatedAt time.Time `json:"-"`
+ UpdatedAt time.Time `json:"-"`
+ DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
+}
diff --git a/pkg/flow_helpers/flow_helpers.go b/flow_helpers/flow_helpers.go
similarity index 100%
rename from pkg/flow_helpers/flow_helpers.go
rename to flow_helpers/flow_helpers.go
diff --git a/pkg/handlers/accounts.go b/handlers/accounts.go
similarity index 96%
rename from pkg/handlers/accounts.go
rename to handlers/accounts.go
index b271318..27413c4 100644
--- a/pkg/handlers/accounts.go
+++ b/handlers/accounts.go
@@ -6,7 +6,7 @@ import (
"log"
"net/http"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/account"
+ "github.com/eqlabs/flow-nft-wallet-service/account"
"github.com/gorilla/mux"
)
diff --git a/pkg/handlers/fungible_tokens.go b/handlers/fungible_tokens.go
similarity index 83%
rename from pkg/handlers/fungible_tokens.go
rename to handlers/fungible_tokens.go
index 46bf83f..daf0e0a 100644
--- a/pkg/handlers/fungible_tokens.go
+++ b/handlers/fungible_tokens.go
@@ -4,8 +4,8 @@ import (
"log"
"net/http"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/keys"
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "github.com/eqlabs/flow-nft-wallet-service/keys"
"github.com/gorilla/mux"
"github.com/onflow/flow-go-sdk/client"
)
@@ -14,15 +14,15 @@ type FungibleTokens struct {
l *log.Logger
c *client.Client
db data.Store
- ks keys.Store
+ km keys.Manager
}
func NewFungibleTokens(
l *log.Logger,
c *client.Client,
db data.Store,
- ks keys.Store) *FungibleTokens {
- return &FungibleTokens{l, c, db, ks}
+ km keys.Manager) *FungibleTokens {
+ return &FungibleTokens{l, c, db, km}
}
func (s *FungibleTokens) Details(rw http.ResponseWriter, r *http.Request) {
diff --git a/pkg/handlers/transactions.go b/handlers/transactions.go
similarity index 75%
rename from pkg/handlers/transactions.go
rename to handlers/transactions.go
index edb0306..6df2076 100644
--- a/pkg/handlers/transactions.go
+++ b/handlers/transactions.go
@@ -4,8 +4,8 @@ import (
"log"
"net/http"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/keys"
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "github.com/eqlabs/flow-nft-wallet-service/keys"
"github.com/onflow/flow-go-sdk/client"
)
@@ -13,15 +13,15 @@ type Transactions struct {
l *log.Logger
c *client.Client
db data.Store
- ks keys.Store
+ km keys.Manager
}
func NewTransactions(
l *log.Logger,
c *client.Client,
db data.Store,
- ks keys.Store) *Transactions {
- return &Transactions{l, c, db, ks}
+ km keys.Manager) *Transactions {
+ return &Transactions{l, c, db, km}
}
func (s *Transactions) List(rw http.ResponseWriter, r *http.Request) {
diff --git a/keys/google/google.go b/keys/google/google.go
new file mode 100644
index 0000000..44e0f5b
--- /dev/null
+++ b/keys/google/google.go
@@ -0,0 +1,58 @@
+package google
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "github.com/onflow/flow-go-sdk/crypto/cloudkms"
+
+ kms "cloud.google.com/go/kms/apiv1"
+ kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
+)
+
+// Creates a new asymmetric signing key in Google KMS and returns a cloudkms.Key (the "raw" result isn't needed)
+func AsymKey(ctx context.Context, parent, id string) (createdKey cloudkms.Key, err error) {
+ kmsClient, err := kms.NewKeyManagementClient(ctx)
+ if err != nil {
+ return
+ }
+
+ req := &kmspb.CreateCryptoKeyRequest{
+ Parent: parent,
+ CryptoKeyId: id,
+ CryptoKey: &kmspb.CryptoKey{
+ Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN,
+ VersionTemplate: &kmspb.CryptoKeyVersionTemplate{
+ Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256,
+ },
+ // TODO: Set relevant labels at creation, update post-creation if necessary
+ Labels: map[string]string{
+ "service": "flow-nft-wallet-service",
+ "account_address": "",
+ "chain_id": "",
+ "environment": "development",
+ },
+ },
+ }
+
+ googleKey, err := kmsClient.CreateCryptoKey(ctx, req)
+ if err != nil {
+ return
+ }
+
+ // Append cryptoKeyVersions so that we can utilize the KeyFromResourceID method
+ createdKey, err = cloudkms.KeyFromResourceID(fmt.Sprintf("%s/cryptoKeyVersions/1", googleKey.Name))
+ if err != nil {
+ fmt.Println("Could not create cloudkms.Key from ResourceId:", googleKey.Name)
+ return
+ }
+
+ // Validate key name
+ if !strings.HasPrefix(createdKey.ResourceID(), googleKey.Name) {
+ fmt.Println("WARNING: created Google KMS key name does not match the expected", createdKey.ResourceID(), " vs ", googleKey.Name)
+ // TODO: Handle scenario
+ }
+
+ return
+}
diff --git a/keys/manager.go b/keys/manager.go
new file mode 100644
index 0000000..f597d9f
--- /dev/null
+++ b/keys/manager.go
@@ -0,0 +1,38 @@
+package keys
+
+import (
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "github.com/onflow/flow-go-sdk"
+ "github.com/onflow/flow-go-sdk/crypto"
+)
+
+const (
+ ACCOUNT_KEY_TYPE_LOCAL = "local"
+ ACCOUNT_KEY_TYPE_GOOGLE_KMS = "google_kms"
+)
+
+type Manager interface {
+ Generate(keyIndex int, weight int) (Wrapped, error)
+ Save(Key) (data.Key, error)
+ Load(data.Key) (Key, error)
+ AdminAuthorizer() (Authorizer, error)
+ UserAuthorizer(address string) (Authorizer, error)
+}
+
+// "In flight" account key
+type Key struct {
+ Index int `json:"index"`
+ Type string `json:"type"`
+ Value string `json:"-"`
+}
+
+type Authorizer struct {
+ Address flow.Address
+ Key *flow.AccountKey
+ Signer crypto.Signer
+}
+
+type Wrapped struct {
+ FlowKey *flow.AccountKey
+ AccountKey Key
+}
diff --git a/keys/simple/encryption.go b/keys/simple/encryption.go
new file mode 100644
index 0000000..c955e70
--- /dev/null
+++ b/keys/simple/encryption.go
@@ -0,0 +1,66 @@
+package simple
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "fmt"
+ "io"
+)
+
+type SymmetricCrypter struct {
+ key []byte
+}
+
+func NewCrypter(key []byte) *SymmetricCrypter {
+ return &SymmetricCrypter{key}
+}
+
+func (s *SymmetricCrypter) Encrypt(original []byte) ([]byte, error) {
+ // TODO: research if these could be stored in the struct
+ c, err := aes.NewCipher(s.key)
+ if err != nil {
+ return []byte(""), err
+ }
+
+ // TODO: research if these could be stored in the struct
+ gcm, err := cipher.NewGCM(c)
+ if err != nil {
+ return []byte(""), err
+ }
+
+ nonce := make([]byte, gcm.NonceSize())
+ if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
+ return []byte(""), err
+ }
+
+ return gcm.Seal(nonce, nonce, original, nil), nil
+}
+
+func (s *SymmetricCrypter) Decrypt(original []byte) ([]byte, error) {
+ // TODO: research if these could be stored in the struct
+ c, err := aes.NewCipher(s.key)
+ if err != nil {
+ return []byte(""), err
+ }
+
+ // TODO: research if these could be stored in the struct
+ gcm, err := cipher.NewGCM(c)
+ if err != nil {
+ return []byte(""), err
+ }
+
+ nonceSize := gcm.NonceSize()
+ if len(original) < nonceSize {
+ return []byte(""), fmt.Errorf("message too short")
+ }
+
+ nonce, ciphertext := original[:nonceSize], original[nonceSize:]
+
+ plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
+ if err != nil {
+ return []byte(""), err
+ }
+
+ return plaintext, nil
+}
diff --git a/keys/simple/manager.go b/keys/simple/manager.go
new file mode 100644
index 0000000..c8e3608
--- /dev/null
+++ b/keys/simple/manager.go
@@ -0,0 +1,231 @@
+package simple
+
+import (
+ "context"
+ "crypto/rand"
+ "fmt"
+ "os"
+
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "github.com/eqlabs/flow-nft-wallet-service/keys"
+ "github.com/eqlabs/flow-nft-wallet-service/keys/google"
+ "github.com/onflow/flow-go-sdk"
+ "github.com/onflow/flow-go-sdk/client"
+ "github.com/onflow/flow-go-sdk/crypto"
+ "github.com/onflow/flow-go-sdk/crypto/cloudkms"
+
+ "github.com/google/uuid"
+)
+
+type KeyManager struct {
+ db data.Store
+ fc *client.Client
+ serviceAddress string
+ serviceKey keys.Key
+ defaultKeyManager string
+ crypter *SymmetricCrypter
+ signAlgo crypto.SignatureAlgorithm
+ hashAlgo crypto.HashAlgorithm
+}
+
+type GoogleKMSConfig struct {
+ ProjectID string
+ LocationID string
+ KeyRingID string
+}
+
+func NewKeyManager(
+ db data.Store,
+ fc *client.Client,
+ serviceAddress string,
+ serviceKey keys.Key,
+ defaultKeyManager string,
+ encryptionKey string,
+) (*KeyManager, error) {
+ return &KeyManager{
+ db,
+ fc,
+ serviceAddress,
+ serviceKey,
+ defaultKeyManager,
+ NewCrypter([]byte(encryptionKey)),
+ crypto.ECDSA_P256, // TODO: config
+ crypto.SHA3_256, // TODO: config
+ }, nil
+}
+
+func (s *KeyManager) Generate(keyIndex int, weight int) (result keys.Wrapped, err error) {
+ switch s.defaultKeyManager {
+ case keys.ACCOUNT_KEY_TYPE_LOCAL:
+ seed := make([]byte, crypto.MinSeedLength)
+ _, err := rand.Read(seed)
+ if err != nil {
+ break
+ }
+
+ privateKey, err := crypto.GeneratePrivateKey(s.signAlgo, seed)
+ if err != nil {
+ break
+ }
+
+ flowKey := flow.NewAccountKey().
+ FromPrivateKey(privateKey).
+ SetHashAlgo(s.hashAlgo).
+ SetWeight(weight)
+ flowKey.Index = keyIndex
+
+ accountKey := keys.Key{
+ Index: keyIndex,
+ Type: keys.ACCOUNT_KEY_TYPE_LOCAL,
+ Value: privateKey.String(),
+ }
+
+ result.AccountKey = accountKey
+ result.FlowKey = flowKey
+
+ case keys.ACCOUNT_KEY_TYPE_GOOGLE_KMS:
+ // TODO: Take this as a param / config instead
+ gkmsConfig := GoogleKMSConfig{
+ ProjectID: os.Getenv("GOOGLE_KMS_PROJECT_ID"),
+ LocationID: os.Getenv("GOOGLE_KMS_LOCATION_ID"),
+ KeyRingID: os.Getenv("GOOGLE_KMS_KEYRING_ID"),
+ }
+
+ ctx := context.Background()
+
+ keyUUID := uuid.New()
+
+ // Create the new key in Google KMS
+ createdKey, err := google.AsymKey(
+ ctx,
+ fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", gkmsConfig.ProjectID, gkmsConfig.LocationID, gkmsConfig.KeyRingID),
+ fmt.Sprintf("flow-wallet-account-key-%s", keyUUID.String()),
+ )
+ if err != nil {
+ break
+ }
+
+ client, err := cloudkms.NewClient(ctx)
+ if err != nil {
+ break
+ }
+
+ // Get the public key (using flow-go-sdk's cloudkms.Client)
+ publicKey, hashAlgorithm, err := client.GetPublicKey(ctx, createdKey)
+ if err != nil {
+ break
+ }
+
+ accountKey := keys.Key{
+ Index: keyIndex,
+ Type: keys.ACCOUNT_KEY_TYPE_GOOGLE_KMS,
+ Value: createdKey.ResourceID(),
+ }
+
+ flowKey := flow.NewAccountKey().
+ SetPublicKey(publicKey).
+ SetHashAlgo(hashAlgorithm).
+ SetWeight(weight)
+ flowKey.Index = keyIndex
+
+ result.AccountKey = accountKey
+ result.FlowKey = flowKey
+
+ default:
+ err = fmt.Errorf("keyStore.Generate() not implmented for %s", s.defaultKeyManager)
+ }
+ return
+}
+
+func (s *KeyManager) Save(key keys.Key) (result data.Key, err error) {
+ encValue, err := s.crypter.Encrypt([]byte(key.Value))
+ if err != nil {
+ return
+ }
+ result.Index = key.Index
+ result.Type = key.Type
+ result.Value = encValue
+ return
+}
+
+func (s *KeyManager) Load(key data.Key) (result keys.Key, err error) {
+ decValue, err := s.crypter.Decrypt([]byte(key.Value))
+ if err != nil {
+ return
+ }
+ result.Index = key.Index
+ result.Type = key.Type
+ result.Value = string(decValue)
+ return
+}
+
+func (s *KeyManager) AdminAuthorizer() (keys.Authorizer, error) {
+ return s.MakeAuthorizer(s.serviceAddress)
+}
+
+func (s *KeyManager) UserAuthorizer(address string) (keys.Authorizer, error) {
+ return s.MakeAuthorizer(address)
+}
+
+func (s *KeyManager) MakeAuthorizer(address string) (authorizer keys.Authorizer, err error) {
+ var accountKey keys.Key
+ ctx := context.Background()
+
+ authorizer.Address = flow.HexToAddress(address)
+
+ if address == s.serviceAddress {
+ accountKey = s.serviceKey
+ } else {
+ var rawKey data.Key
+ rawKey, err = s.db.AccountKey(address, 0)
+ if err != nil {
+ return
+ }
+ accountKey, err = s.Load(rawKey)
+ if err != nil {
+ return
+ }
+ }
+
+ flowAcc, err := s.fc.GetAccount(ctx, flow.HexToAddress(address))
+ if err != nil {
+ return
+ }
+
+ authorizer.Key = flowAcc.Keys[accountKey.Index]
+
+ // TODO: Decide whether we want to allow this kind of flexibility
+ // or should we just panic if `accountKey.Type` != `s.defaultKeyManager`
+ switch accountKey.Type {
+ case keys.ACCOUNT_KEY_TYPE_LOCAL:
+ pk, err := crypto.DecodePrivateKeyHex(s.signAlgo, accountKey.Value)
+ if err != nil {
+ break
+ }
+ authorizer.Signer = crypto.NewInMemorySigner(pk, s.hashAlgo)
+ case keys.ACCOUNT_KEY_TYPE_GOOGLE_KMS:
+ kmsClient, err := cloudkms.NewClient(ctx)
+ if err != nil {
+ break
+ }
+
+ kmsKey, err := cloudkms.KeyFromResourceID(accountKey.Value)
+ if err != nil {
+ break
+ }
+
+ sig, err := kmsClient.SignerForKey(
+ ctx,
+ flow.HexToAddress(address),
+ kmsKey,
+ )
+ if err != nil {
+ break
+ }
+ authorizer.Signer = sig
+ default:
+ err = fmt.Errorf("accountKey.Type not recognised: %s", accountKey.Type)
+ }
+
+ return
+}
diff --git a/main.go b/main.go
index f073dd1..8735aa0 100644
--- a/main.go
+++ b/main.go
@@ -11,11 +11,12 @@ import (
"time"
"github.com/caarlos0/env/v6"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/account"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data/gorm"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/handlers"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/keys/simple"
+ "github.com/eqlabs/flow-nft-wallet-service/account"
+ "github.com/eqlabs/flow-nft-wallet-service/data"
+ "github.com/eqlabs/flow-nft-wallet-service/data/gorm"
+ "github.com/eqlabs/flow-nft-wallet-service/handlers"
+ "github.com/eqlabs/flow-nft-wallet-service/keys"
+ "github.com/eqlabs/flow-nft-wallet-service/keys/simple"
"github.com/gorilla/mux"
"github.com/joho/godotenv"
"github.com/onflow/flow-go-sdk/client"
@@ -69,11 +70,11 @@ func main() {
var db data.Store
switch cfg.DatabaseType {
case data.DB_TYPE_POSTGRESQL:
- db, err = gorm.NewDataStore(postgres.Open(cfg.DatabaseDSN))
+ db, err = gorm.NewStore(postgres.Open(cfg.DatabaseDSN))
case data.DB_TYPE_MYSQL:
- db, err = gorm.NewDataStore(mysql.Open(cfg.DatabaseDSN))
+ db, err = gorm.NewStore(mysql.Open(cfg.DatabaseDSN))
case data.DB_TYPE_SQLITE:
- db, err = gorm.NewDataStore(sqlite.Open(cfg.DatabaseDSN))
+ db, err = gorm.NewStore(sqlite.Open(cfg.DatabaseDSN))
default:
err = fmt.Errorf("database type '%s' not supported", cfg.DatabaseType)
}
@@ -81,13 +82,14 @@ func main() {
log.Fatal(err)
}
- ks, err := simple.NewKeyStore(
+ km, err := simple.NewKeyManager(
db,
- data.AccountKey{
- AccountAddress: cfg.ServiceAccountAddress,
- Index: cfg.ServiceAccountKeyIndex,
- Type: cfg.ServiceAccountKeyType,
- Value: cfg.ServiceAccountKeyValue,
+ fc,
+ cfg.ServiceAccountAddress,
+ keys.Key{
+ Index: cfg.ServiceAccountKeyIndex,
+ Type: cfg.ServiceAccountKeyType,
+ Value: cfg.ServiceAccountKeyValue,
},
cfg.DefaultKeyManager,
cfg.EncryptionKey,
@@ -96,11 +98,11 @@ func main() {
log.Fatal(err)
}
- accountService := account.NewService(l, db, ks, fc)
+ accountService := account.NewService(l, db, km, fc)
accounts := handlers.NewAccounts(l, accountService)
- // transactions := handlers.NewTransactions(l, fc, db, ks)
- // fungibleTokens := handlers.NewFungibleTokens(l, fc, db, ks)
+ // transactions := handlers.NewTransactions(l, fc, db, km)
+ // fungibleTokens := handlers.NewFungibleTokens(l, fc, db, km)
r := mux.NewRouter()
diff --git a/pkg/data/data.go b/pkg/data/data.go
deleted file mode 100644
index deeccc0..0000000
--- a/pkg/data/data.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package data
-
-import (
- "time"
-
- "gorm.io/gorm"
-)
-
-type Account struct {
- Address string `json:"address" gorm:"primaryKey"`
- CreatedAt time.Time `json:"-"`
- UpdatedAt time.Time `json:"-"`
- DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
-}
-
-type AccountKey struct {
- AccountAddress string `json:"address" gorm:"primaryKey"`
- Index int `json:"index"`
- Type string `json:"type"` // local, google_kms
- Value string `json:"value"` // local: private key, google_kms: resource id
- CreatedAt time.Time `json:"-"`
- UpdatedAt time.Time `json:"-"`
- DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
-}
diff --git a/pkg/data/gorm/accountstore.go b/pkg/data/gorm/accountstore.go
deleted file mode 100644
index ea95223..0000000
--- a/pkg/data/gorm/accountstore.go
+++ /dev/null
@@ -1,55 +0,0 @@
-package gorm
-
-import (
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "gorm.io/gorm"
-)
-
-type AccountStore struct {
- db *gorm.DB
-}
-
-func newAccountStore(db *gorm.DB) *AccountStore {
- db.AutoMigrate(&data.Account{}, &data.AccountKey{})
- return &AccountStore{db}
-}
-
-// List all accounts
-func (s *AccountStore) Accounts() (accounts []data.Account, err error) {
- result := s.db.Find(&accounts)
- err = result.Error
- return
-}
-
-// Insert new account
-func (s *AccountStore) InsertAccount(account data.Account) error {
- result := s.db.Create(&account)
- return result.Error
-}
-
-// Get account details
-func (s *AccountStore) Account(address string) (account data.Account, err error) {
- result := s.db.First(&account, "address = ?", address)
- err = result.Error
- return
-}
-
-// List all account keys
-func (s *AccountStore) AccountKeys() (keys []data.AccountKey, err error) {
- result := s.db.Find(&keys)
- err = result.Error
- return
-}
-
-// Insert new account key
-func (s *AccountStore) InsertAccountKey(key data.AccountKey) error {
- result := s.db.Create(&key)
- return result.Error
-}
-
-// Get account key details
-func (s *AccountStore) AccountKey(address string) (key data.AccountKey, err error) {
- result := s.db.First(&key, "address = ?", address)
- err = result.Error
- return
-}
diff --git a/pkg/data/gorm/datastore.go b/pkg/data/gorm/datastore.go
deleted file mode 100644
index ded0c82..0000000
--- a/pkg/data/gorm/datastore.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package gorm
-
-import (
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "gorm.io/gorm"
-)
-
-type DataStore struct {
- data.AccountStore
-}
-
-func NewDataStore(dialector gorm.Dialector) (*DataStore, error) {
- db, err := gorm.Open(dialector, &gorm.Config{})
- if err != nil {
- return &DataStore{}, nil
- }
- return &DataStore{
- AccountStore: newAccountStore(db),
- }, nil
-}
diff --git a/pkg/data/store.go b/pkg/data/store.go
deleted file mode 100644
index b872626..0000000
--- a/pkg/data/store.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package data
-
-const (
- DB_TYPE_POSTGRESQL = "psql"
- DB_TYPE_MYSQL = "mysql"
- DB_TYPE_SQLITE = "sqlite"
-)
-
-type Store interface {
- AccountStore
-}
-
-type AccountStore interface {
- Accounts() ([]Account, error)
- InsertAccount(a Account) error
- Account(address string) (Account, error)
- AccountKeys() ([]AccountKey, error)
- InsertAccountKey(k AccountKey) error
- AccountKey(address string) (AccountKey, error)
-}
diff --git a/pkg/keys/simple/store.go b/pkg/keys/simple/store.go
deleted file mode 100644
index eec575e..0000000
--- a/pkg/keys/simple/store.go
+++ /dev/null
@@ -1,266 +0,0 @@
-package simple
-
-import (
- "context"
- "crypto/rand"
- "fmt"
- "os"
- "strings"
-
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/keys"
- "github.com/onflow/flow-go-sdk"
- "github.com/onflow/flow-go-sdk/client"
- "github.com/onflow/flow-go-sdk/crypto"
- "github.com/onflow/flow-go-sdk/crypto/cloudkms"
-
- kms "cloud.google.com/go/kms/apiv1"
- "github.com/google/uuid"
- kmspb "google.golang.org/genproto/googleapis/cloud/kms/v1"
-)
-
-type KeyStore struct {
- db data.Store
- serviceAcct data.AccountKey
- defaultKeyManager string
- encryptionKey string
- signAlgo crypto.SignatureAlgorithm
- hashAlgo crypto.HashAlgorithm
-}
-
-type GoogleKMSConfig struct {
- ProjectID string
- LocationID string
- KeyRingID string
-}
-
-func NewKeyStore(
- db data.Store,
- serviceAcct data.AccountKey,
- defaultKeyManager string,
- encryptionKey string,
-) (*KeyStore, error) {
- return &KeyStore{
- db,
- serviceAcct,
- defaultKeyManager,
- encryptionKey,
- crypto.ECDSA_P256, // TODO: config
- crypto.SHA3_256, // TODO: config
- }, nil
-}
-
-func (s *KeyStore) Generate(ctx context.Context, keyIndex int, weight int) (keys.NewKeyWrapper, error) {
- switch s.defaultKeyManager {
- case keys.ACCOUNT_KEY_TYPE_LOCAL:
- seed := make([]byte, crypto.MinSeedLength)
- _, err := rand.Read(seed)
- if err != nil {
- return keys.NewKeyWrapper{}, err
- }
- privateKey, err := crypto.GeneratePrivateKey(s.signAlgo, seed)
- if err != nil {
- return keys.NewKeyWrapper{}, err
- }
-
- flowKey := flow.NewAccountKey().
- FromPrivateKey(privateKey).
- SetHashAlgo(s.hashAlgo).
- SetWeight(weight)
- flowKey.Index = keyIndex
-
- accountKey := data.AccountKey{
- Index: keyIndex,
- Type: keys.ACCOUNT_KEY_TYPE_LOCAL,
- Value: privateKey.String(),
- }
- return keys.NewKeyWrapper{FlowKey: flowKey, AccountKey: accountKey}, nil
- case keys.ACCOUNT_KEY_TYPE_GOOGLE_KMS:
- // TODO: Take this as a param / config instead
- gkmsConfig := GoogleKMSConfig{
- ProjectID: os.Getenv("GOOGLE_KMS_PROJECT_ID"),
- LocationID: os.Getenv("GOOGLE_KMS_LOCATION_ID"),
- KeyRingID: os.Getenv("GOOGLE_KMS_KEYRING_ID"),
- }
- keyUUID := uuid.New()
-
- // Create the new key in Google KMS
- createdKey, err := createKeyAsymmetricSign(
- ctx,
- fmt.Sprintf("projects/%s/locations/%s/keyRings/%s", gkmsConfig.ProjectID, gkmsConfig.LocationID, gkmsConfig.KeyRingID),
- fmt.Sprintf("flow-wallet-account-key-%s", keyUUID.String()),
- )
- if err != nil {
- fmt.Println(err)
- return keys.NewKeyWrapper{}, err
- }
-
- client, err := cloudkms.NewClient(ctx)
- if err != nil {
- fmt.Println(err)
- return keys.NewKeyWrapper{}, err
- }
-
- // Get the public key (using flow-go-sdk's cloudkms.Client)
- publicKey, hashAlgorithm, err := client.GetPublicKey(ctx, createdKey)
- if err != nil {
- fmt.Println(err)
- return keys.NewKeyWrapper{}, err
- }
-
- accountKey := data.AccountKey{
- Index: keyIndex,
- Type: keys.ACCOUNT_KEY_TYPE_GOOGLE_KMS,
- Value: createdKey.ResourceID(),
- }
-
- flowKey := flow.NewAccountKey().
- SetPublicKey(publicKey).
- SetHashAlgo(hashAlgorithm).
- SetWeight(weight)
- flowKey.Index = keyIndex
-
- return keys.NewKeyWrapper{FlowKey: flowKey, AccountKey: accountKey}, nil
- default:
- return keys.NewKeyWrapper{}, fmt.Errorf("keyStore.Generate() not implmented for %s", s.defaultKeyManager)
- }
-}
-
-func (s *KeyStore) Save(key data.AccountKey) error {
- switch key.Type {
- case keys.ACCOUNT_KEY_TYPE_LOCAL:
- // TODO: encrypt key.Value
- if s.encryptionKey != "" {
- panic("key encryption not implemented")
- }
- err := s.db.InsertAccountKey(key)
- return err
- default:
- // TODO: google_kms
- return fmt.Errorf("keyStore.Save() not implmented for %s", s.defaultKeyManager)
- }
-}
-
-func (s *KeyStore) Delete(address string, keyIndex int) error {
- panic("not implemented") // TODO: implement
-}
-
-func (s *KeyStore) ServiceAuthorizer(ctx context.Context, fc *client.Client) (keys.Authorizer, error) {
- return s.MakeAuthorizer(ctx, fc, s.serviceAcct.AccountAddress)
-}
-
-func (s *KeyStore) AccountAuthorizer(ctx context.Context, fc *client.Client, address string) (keys.Authorizer, error) {
- return s.MakeAuthorizer(ctx, fc, address)
-}
-
-func (s *KeyStore) MakeAuthorizer(ctx context.Context, fc *client.Client, address string) (keys.Authorizer, error) {
- var (
- accountKey data.AccountKey
- authorizer keys.Authorizer = keys.Authorizer{}
- err error
- )
-
- authorizer.Address = flow.HexToAddress(address)
-
- if address == s.serviceAcct.AccountAddress {
- accountKey = s.serviceAcct
- } else {
- accountKey, err = s.db.AccountKey(address)
- if err != nil {
- return authorizer, err
- }
- if s.encryptionKey != "" {
- // TODO: decrypt accountKey.Value
- panic("key decryption not implemented")
- }
- }
-
- flowAcc, err := fc.GetAccount(ctx, flow.HexToAddress(address))
- if err != nil {
- return authorizer, err
- }
-
- authorizer.Key = flowAcc.Keys[accountKey.Index]
-
- // TODO: Decide whether we want to allow this kind of flexibility
- // or should we just panic if `accountKey.Type` != `s.defaultKeyManager`
- switch accountKey.Type {
- case keys.ACCOUNT_KEY_TYPE_LOCAL:
- pk, err := crypto.DecodePrivateKeyHex(s.signAlgo, accountKey.Value)
- if err != nil {
- return authorizer, err
- }
- authorizer.Signer = crypto.NewInMemorySigner(pk, s.hashAlgo)
- case keys.ACCOUNT_KEY_TYPE_GOOGLE_KMS:
- kmsClient, err := cloudkms.NewClient(ctx)
- if err != nil {
- return authorizer, err
- }
-
- kmsKey, err := cloudkms.KeyFromResourceID(accountKey.Value)
- if err != nil {
- return authorizer, err
- }
-
- sig, err := kmsClient.SignerForKey(
- ctx,
- flow.HexToAddress(address),
- kmsKey,
- )
- if err != nil {
- return authorizer, err
- }
- authorizer.Signer = sig
- default:
- return authorizer,
- fmt.Errorf("accountKey.Type not recognised: %s", accountKey.Type)
- }
-
- return authorizer, nil
-}
-
-// Creates a new asymmetric signing key in Google KMS and returns a cloudkms.Key (the "raw" result isn't needed)
-func createKeyAsymmetricSigningKey(ctx context.Context, parent, id string) (createdKey cloudkms.Key, err error) {
- kmsClient, err := kms.NewKeyManagementClient(ctx)
- if err != nil {
- return
- }
-
- req := &kmspb.CreateCryptoKeyRequest{
- Parent: parent,
- CryptoKeyId: id,
- CryptoKey: &kmspb.CryptoKey{
- Purpose: kmspb.CryptoKey_ASYMMETRIC_SIGN,
- VersionTemplate: &kmspb.CryptoKeyVersionTemplate{
- Algorithm: kmspb.CryptoKeyVersion_EC_SIGN_P256_SHA256,
- },
- // TODO: Set relevant labels at creation, update post-creation if necessary
- Labels: map[string]string{
- "service": "flow-nft-wallet-service",
- "account_address": "",
- "chain_id": "",
- "environment": "development",
- },
- },
- }
-
- googleKey, err := kmsClient.CreateCryptoKey(ctx, req)
- if err != nil {
- return
- }
-
- // Append cryptoKeyVersions so that we can utilize the KeyFromResourceID method
- createdKey, err = cloudkms.KeyFromResourceID(fmt.Sprintf("%s/cryptoKeyVersions/1", googleKey.Name))
- if err != nil {
- fmt.Println("Could not create cloudkms.Key from ResourceId:", googleKey.Name)
- return
- }
-
- // Validate key name
- if !strings.HasPrefix(createdKey.ResourceID(), googleKey.Name) {
- fmt.Println("WARNING: created Google KMS key name does not match the expected", createdKey.ResourceID(), " vs ", googleKey.Name)
- // TODO: Handle scenario
- }
-
- return
-}
diff --git a/pkg/keys/store.go b/pkg/keys/store.go
deleted file mode 100644
index 83277fe..0000000
--- a/pkg/keys/store.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package keys
-
-import (
- "context"
-
- "github.com/eqlabs/flow-nft-wallet-service/pkg/data"
- "github.com/onflow/flow-go-sdk"
- "github.com/onflow/flow-go-sdk/client"
- "github.com/onflow/flow-go-sdk/crypto"
-)
-
-const (
- ACCOUNT_KEY_TYPE_LOCAL = "local"
- ACCOUNT_KEY_TYPE_GOOGLE_KMS = "google_kms"
-)
-
-type Store interface {
- Generate(ctx context.Context, keyIndex int, weight int) (NewKeyWrapper, error)
- Save(data.AccountKey) error
- Delete(address string, keyIndex int) error
- ServiceAuthorizer(ctx context.Context, fc *client.Client) (Authorizer, error)
- AccountAuthorizer(ctx context.Context, fc *client.Client, address string) (Authorizer, error)
-}
-
-type Authorizer struct {
- Address flow.Address
- Key *flow.AccountKey
- Signer crypto.Signer
-}
-
-type NewKeyWrapper struct {
- FlowKey *flow.AccountKey
- AccountKey data.AccountKey
-}
diff --git a/pkg/tokens/fungible.go b/tokens/fungible.go
similarity index 100%
rename from pkg/tokens/fungible.go
rename to tokens/fungible.go
diff --git a/pkg/tokens/non_fungible.go b/tokens/non_fungible.go
similarity index 82%
rename from pkg/tokens/non_fungible.go
rename to tokens/non_fungible.go
index 9e0bb02..8944dea 100644
--- a/pkg/tokens/non_fungible.go
+++ b/tokens/non_fungible.go
@@ -5,8 +5,8 @@ import (
"io/ioutil"
"strings"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/flow_helpers"
- "github.com/eqlabs/flow-nft-wallet-service/pkg/keys"
+ "github.com/eqlabs/flow-nft-wallet-service/flow_helpers"
+ "github.com/eqlabs/flow-nft-wallet-service/keys"
"github.com/onflow/flow-go-sdk"
"github.com/onflow/flow-go-sdk/client"
)
@@ -17,13 +17,13 @@ type NftInfo struct {
Name string
}
-func SetupNFT(ctx context.Context, fc *client.Client, ks keys.Store, address string, n NftInfo) (*flow.TransactionResult, error) {
- serviceAuth, err := ks.ServiceAuthorizer(ctx, fc)
+func SetupNFT(ctx context.Context, fc *client.Client, km keys.Manager, address string, n NftInfo) (*flow.TransactionResult, error) {
+ serviceAuth, err := km.AdminAuthorizer()
if err != nil {
return &flow.TransactionResult{}, err
}
- accountAuth, err := ks.AccountAuthorizer(ctx, fc, address)
+ accountAuth, err := km.UserAuthorizer(address)
if err != nil {
return &flow.TransactionResult{}, err
}