diff --git a/client.go b/client.go index 85e4932..96a4058 100644 --- a/client.go +++ b/client.go @@ -2,7 +2,10 @@ package sdk import ( + "net/http" + "github.com/go-openapi/runtime" + httptransport "github.com/go-openapi/runtime/client" "github.com/go-openapi/strfmt" "github.com/pkg/errors" @@ -11,22 +14,123 @@ import ( "github.com/tkhq/go-sdk/pkg/store/local" ) +const DefaultClientVersion = "go-sdk" + +type config struct { + apiKey *apikey.Key + clientVersion string + registry strfmt.Registry + transportConfig *client.TransportConfig +} + +// OptionFunc defines a function which sets configuration options for a Client. +type OptionFunc func(c *config) error + +// WithClientVersion overrides the client version used for this API client. +func WithClientVersion(clientVersion string) OptionFunc { + return func(c *config) error { + c.clientVersion = clientVersion + + return nil + } +} + +// WithRegistry sets the registry formats used for this API client. +func WithRegistry(registry strfmt.Registry) OptionFunc { + return func(c *config) error { + c.registry = registry + + return nil + } +} + +// WithTransportConfig sets the TransportConfig used for this API client. +func WithTransportConfig(transportConfig client.TransportConfig) OptionFunc { + return func(c *config) error { + c.transportConfig = &transportConfig + + return nil + } +} + +// WithAPIKey sets the API key used for this API client. +// Users would normally use WithAPIKeyName. This offers a lower-level custom API +// key. +func WithAPIKey(apiKey *apikey.Key) OptionFunc { + return func(c *config) error { + c.apiKey = apiKey + + return nil + } +} + +// WithAPIKeyName sets the API key to the key loaded from the local keystore +// with the provided name. +func WithAPIKeyName(keyname string) OptionFunc { + return func(c *config) error { + apiKey, err := local.New[*apikey.Key]().Load(keyname) + if err != nil { + return errors.Wrap(err, "failed to load API key") + } + + c.apiKey = apiKey + + return nil + } +} + // New returns a new API Client with the given API key name from the default keystore. -func New(keyname string) (*Client, error) { - apiKey, err := local.New[*apikey.Key, apikey.Metadata]().Load(keyname) - if err != nil { - return nil, errors.Wrap(err, "failed to load API key") +func New(options ...OptionFunc) (*Client, error) { + c := &config{ + clientVersion: DefaultClientVersion, + transportConfig: client.DefaultTransportConfig(), } + for _, o := range options { + err := o(c) + if err != nil { + return nil, err + } + } + + // Create transport and client + transport := httptransport.New( + c.transportConfig.Host, + c.transportConfig.BasePath, + c.transportConfig.Schemes, + ) + + // Add client version header + transport.Transport = SetClientVersion(transport.Transport, c.clientVersion) + return &Client{ - Client: client.NewHTTPClient(nil), - Authenticator: &Authenticator{Key: apiKey}, - APIKey: apiKey, + Client: client.New(transport, c.registry), + Authenticator: &Authenticator{Key: c.apiKey}, + APIKey: c.apiKey, }, nil } +func SetClientVersion(inner http.RoundTripper, clientVersion string) http.RoundTripper { + return &addClientVersion{ + inner: inner, + Version: clientVersion, + } +} + +type addClientVersion struct { + inner http.RoundTripper + Version string +} + +func (acv *addClientVersion) RoundTrip(r *http.Request) (*http.Response, error) { + r.Header.Set("X-Client-Version", acv.Version) + + return acv.inner.RoundTrip(r) +} + // NewHTTPClient returns a new base HTTP API client. // Most users will call New() instead. +// Deprecated: Use New(WithRegistry(formats)) instead. func NewHTTPClient(formats strfmt.Registry) *client.TurnkeyAPI { return client.NewHTTPClient(formats) } diff --git a/examples/signing/sign_raw_payload/main.go b/examples/signing/sign_raw_payload/main.go index 446cd8b..785234e 100644 --- a/examples/signing/sign_raw_payload/main.go +++ b/examples/signing/sign_raw_payload/main.go @@ -14,7 +14,7 @@ import ( func main() { // NB: make sure to create and register an API key, first. - client, err := sdk.New("default") + client, err := sdk.New(sdk.WithAPIKeyName("default")) if err != nil { log.Fatal("failed to create new SDK client:", err) } diff --git a/examples/signing/sign_transaction/main.go b/examples/signing/sign_transaction/main.go index 1a4d78a..d7c109b 100644 --- a/examples/signing/sign_transaction/main.go +++ b/examples/signing/sign_transaction/main.go @@ -14,7 +14,7 @@ import ( func main() { // NB: make sure to create and register an API key, first. - client, err := sdk.New("default") + client, err := sdk.New(sdk.WithAPIKeyName("default")) if err != nil { log.Fatal("failed to create new SDK client:", err) } diff --git a/examples/wallets/create_wallet/main.go b/examples/wallets/create_wallet/main.go index d1a38e9..cfe9a71 100644 --- a/examples/wallets/create_wallet/main.go +++ b/examples/wallets/create_wallet/main.go @@ -14,7 +14,7 @@ import ( func main() { // NB: make sure to create and register an API key, first. - client, err := sdk.New("default") + client, err := sdk.New(sdk.WithAPIKeyName("default")) if err != nil { log.Fatal("failed to create new SDK client:", err) } diff --git a/examples/wallets/create_wallet_accounts/main.go b/examples/wallets/create_wallet_accounts/main.go index c9debb4..9dbf400 100644 --- a/examples/wallets/create_wallet_accounts/main.go +++ b/examples/wallets/create_wallet_accounts/main.go @@ -14,7 +14,7 @@ import ( func main() { // NB: make sure to create and register an API key, first. - client, err := sdk.New("default") + client, err := sdk.New(sdk.WithAPIKeyName("default")) if err != nil { log.Fatal("failed to create new SDK client:", err) } diff --git a/examples/whoami/whoami.go b/examples/whoami/whoami.go index e143e0a..210c0a0 100644 --- a/examples/whoami/whoami.go +++ b/examples/whoami/whoami.go @@ -12,7 +12,7 @@ import ( func main() { // NB: make sure to create and register an API key, first. - client, err := sdk.New("") + client, err := sdk.New() if err != nil { log.Fatal("failed to create new SDK client:", err) } diff --git a/pkg/apikey/apikey.go b/pkg/apikey/apikey.go index 83ca4ca..d411273 100644 --- a/pkg/apikey/apikey.go +++ b/pkg/apikey/apikey.go @@ -50,11 +50,11 @@ type APIStamp struct { // New generates a new Turnkey API key. func New(organizationID string, opts ...optionFunc) (*Key, error) { if organizationID == "" { - return nil, fmt.Errorf("please supply a valid Organization UUID") + return nil, errors.New("please supply a valid Organization UUID") } if _, err := uuid.Parse(organizationID); err != nil { - return nil, fmt.Errorf("failed to parse organization ID") + return nil, errors.New("failed to parse organization ID") } apiKey := &Key{ diff --git a/pkg/apikey/ecdsa.go b/pkg/apikey/ecdsa.go index 393e64f..219a998 100644 --- a/pkg/apikey/ecdsa.go +++ b/pkg/apikey/ecdsa.go @@ -29,6 +29,10 @@ func (k *ecdsaKey) sign(msg []byte) (string, error) { return hex.EncodeToString(sigBytes), nil } +// TurnkeyECDSAPublicKeyBytes is the expected number of bytes for a public ECDSA +// key. +const TurnkeyECDSAPublicKeyBytes = 33 + // EncodePrivateECDSAKey encodes an ECDSA private key into the Turnkey format. // For now, "Turnkey format" = raw DER form. func EncodePrivateECDSAKey(privateKey *ecdsa.PrivateKey) string { @@ -81,7 +85,7 @@ func DecodeTurnkeyPublicECDSAKey(encodedPublicKey string, scheme signatureScheme return nil, err } - if len(bytes) != 33 { + if len(bytes) != TurnkeyECDSAPublicKeyBytes { return nil, fmt.Errorf("expected a 33-bytes-long public key (compressed). Got %d bytes", len(bytes)) } @@ -99,7 +103,7 @@ func DecodeTurnkeyPublicECDSAKey(encodedPublicKey string, scheme signatureScheme pubkey, err := dcrec.ParsePubKey(bytes) if err != nil { - return nil, fmt.Errorf("cannot parse bytes into secp256k1 public key") + return nil, errors.New("cannot parse bytes into secp256k1 public key") } x = pubkey.X() diff --git a/pkg/encryptionkey/encryptionkey.go b/pkg/encryptionkey/encryptionkey.go index 47630c2..18cdabe 100644 --- a/pkg/encryptionkey/encryptionkey.go +++ b/pkg/encryptionkey/encryptionkey.go @@ -4,7 +4,6 @@ package encryptionkey import ( "encoding/hex" "encoding/json" - "fmt" "os" "github.com/cloudflare/circl/hpke" @@ -39,19 +38,19 @@ type Key struct { // New generates a new Turnkey encryption key. func New(userID string, organizationID string) (*Key, error) { if userID == "" { - return nil, fmt.Errorf("please supply a valid User UUID") + return nil, errors.New("please supply a valid User UUID") } if _, err := uuid.Parse(userID); err != nil { - return nil, fmt.Errorf("failed to parse user ID") + return nil, errors.New("failed to parse user ID") } if organizationID == "" { - return nil, fmt.Errorf("please supply a valid Organization UUID") + return nil, errors.New("please supply a valid Organization UUID") } if _, err := uuid.Parse(organizationID); err != nil { - return nil, fmt.Errorf("failed to parse organization ID") + return nil, errors.New("failed to parse organization ID") } _, privateKey, err := KemID.Scheme().GenerateKeyPair() diff --git a/pkg/store/local/local.go b/pkg/store/local/local.go index df80cb7..bc5e801 100644 --- a/pkg/store/local/local.go +++ b/pkg/store/local/local.go @@ -26,6 +26,8 @@ const ( publicKeyExtension = "public" privateKeyExtension = "private" metadataExtension = "meta" + fileOwnerRWGroupRAllR = 0o0644 + fileOwnerRW = 0o0600 ) // Store defines an api key Store using the local filesystem. @@ -123,7 +125,6 @@ func getConfigDir() string { var err error cfgDir, err = os.UserConfigDir() - if err != nil { shouldUseHomeDir = true } @@ -211,7 +212,7 @@ func (s *Store[T, M]) Store(name string, keypair common.IKey[M]) error { return errors.Errorf("a keypair named %q already exists; exiting", name) } - if err = createKeyFile(s.PublicKeyFile(name), keypair.GetPublicKey(), 0o0644); err != nil { + if err = createKeyFile(s.PublicKeyFile(name), keypair.GetPublicKey(), fileOwnerRWGroupRAllR); err != nil { return errors.Wrap(err, "failed to store public key to file") } @@ -220,11 +221,11 @@ func (s *Store[T, M]) Store(name string, keypair common.IKey[M]) error { privateKeyData = fmt.Sprintf("%s:%s", privateKeyData, curve) } - if err = createKeyFile(s.PrivateKeyFile(name), privateKeyData, 0o0600); err != nil { + if err = createKeyFile(s.PrivateKeyFile(name), privateKeyData, fileOwnerRW); err != nil { return errors.Wrap(err, "failed to store private key to file") } - if err = s.createMetadataFile(s.MetadataFile(name), keypair.GetMetadata(), 0o0600); err != nil { + if err = s.createMetadataFile(s.MetadataFile(name), keypair.GetMetadata(), fileOwnerRW); err != nil { return errors.Wrap(err, "failed to store key metadata") } diff --git a/pkg/store/local/local_test.go b/pkg/store/local/local_test.go index a75b160..e35c113 100644 --- a/pkg/store/local/local_test.go +++ b/pkg/store/local/local_test.go @@ -14,11 +14,7 @@ import ( // MacOSX has $HOME set by default. func TestGetKeyDirPathMacOSX(t *testing.T) { - require.NoError(t, os.Setenv("HOME", "/home/dir")) - - defer func() { - require.NoError(t, os.Unsetenv("HOME")) - }() + t.Setenv("HOME", "/home/dir") // Need to unset this explicitly: the test runner has this set by default! originalValue := os.Getenv("XDG_CONFIG_HOME") @@ -36,17 +32,8 @@ func TestGetKeyDirPathMacOSX(t *testing.T) { // On UNIX, we expect XDG_CONFIG_HOME to be set. // If it's not set, we're back to a MacOSX-like system. func TestGetKeyDirPathUnix(t *testing.T) { - require.NoError(t, os.Setenv("XDG_CONFIG_HOME", "/special/dir")) - - defer func() { - require.NoError(t, os.Unsetenv("XDG_CONFIG_HOME")) - }() - - require.NoError(t, os.Setenv("HOME", "/home/dir")) - - defer func() { - require.NoError(t, os.Unsetenv("HOME")) - }() + t.Setenv("XDG_CONFIG_HOME", "/special/dir") + t.Setenv("HOME", "/home/dir") assert.Equal(t, "/special/dir/turnkey/keys", local.DefaultAPIKeysDir()) assert.Equal(t, "/special/dir/turnkey/encryption-keys", local.DefaultEncryptionKeysDir())