Skip to content

Commit

Permalink
Merge pull request #55 from tkhq/olivia/export
Browse files Browse the repository at this point in the history
Export private keys and wallets
  • Loading branch information
Olivia Thet authored Apr 29, 2024
2 parents 968cf29 + 2d5f686 commit b35acc5
Show file tree
Hide file tree
Showing 12 changed files with 506 additions and 51 deletions.
5 changes: 3 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,9 @@ build-local:
popd;

.PHONY: reproduce
reproduce: clean default
diff digests.txt digests-dist.txt \
reproduce: clean default digests.txt
@diff digests.txt digests-dist.txt \
&& echo "Digests are identical" \
|| echo "Warning: digests.txt and digests-dist.txt differ"

.PHONY: $(DIST_DIR)
Expand Down
4 changes: 4 additions & 0 deletions digests.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
59e258836b3ac1d15efb49c2ff9637bcf8d72aa6a74165999525b0520af3c16c turnkey.darwin-aarch64
da1534435bf06f6c988ec01c48c3a0645984327a349e26156c1d142df3d27ef4 turnkey.darwin-x86_64
ea239121f7c7816532f9bc36563439b86f53da6b8d2ce7b7342a2eda29dbfd07 turnkey.linux-aarch64
10e01fce0e0287bead3a3a600d43eb64510a68937d7c9345d9484bb1e3853e38 turnkey.linux-x86_64
56 changes: 53 additions & 3 deletions src/cmd/turnkey/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func TestHelpText(t *testing.T) {
assert.Contains(t, out, "Available Commands:")
}

func TestKeygenInTmpFolder(t *testing.T) {
func TestAPIKeygenInTmpFolder(t *testing.T) {
orgID := uuid.New()

tmpDir, err := os.MkdirTemp(TempDir, "keys")
Expand All @@ -83,7 +83,34 @@ func TestKeygenInTmpFolder(t *testing.T) {
assert.Equal(t, parsedOut["privateKeyFile"], tmpDir+"/mykey.private")
}

func TestKeygenDetectExistingKey(t *testing.T) {
func TestEncryptionKeygenInTmpFolder(t *testing.T) {
orgID := uuid.New()
userID := uuid.New()

tmpDir, err := os.MkdirTemp(TempDir, "encryption-keys")
assert.Nil(t, err)

defer func() { assert.Nil(t, os.RemoveAll(tmpDir)) }()

out, err := RunCliWithArgs(t, []string{"generate", "encryption-key", "--encryption-keys-folder", tmpDir, "--encryption-key-name", "mykey", "--organization", orgID.String(), "--user", userID.String()})
assert.Nil(t, err)

assert.FileExists(t, tmpDir+"/mykey.public")
assert.FileExists(t, tmpDir+"/mykey.private")

publicKeyData, err := os.ReadFile(tmpDir + "/mykey.public")
assert.Nil(t, err)

var parsedOut map[string]string

assert.Nil(t, json.Unmarshal([]byte(out), &parsedOut))

assert.Equal(t, parsedOut["publicKey"], string(publicKeyData))
assert.Equal(t, parsedOut["publicKeyFile"], tmpDir+"/mykey.public")
assert.Equal(t, parsedOut["privateKeyFile"], tmpDir+"/mykey.private")
}

func TestAPIKeygenDetectExistingKey(t *testing.T) {
orgID := uuid.New()

tmpDir, err := os.MkdirTemp(TempDir, "keys")
Expand All @@ -100,7 +127,30 @@ func TestKeygenDetectExistingKey(t *testing.T) {
assert.FileExists(t, tmpDir+"/myexistingkey.public")
assert.FileExists(t, tmpDir+"/myexistingkey.private")

_, err = RunCliWithArgs(t, []string{"gen", "--organization", orgID.String(), "--keys-folder", tmpDir, "--key-name", "myexistingkey"})
_, err = RunCliWithArgs(t, []string{"generate", "api-key", "--organization", orgID.String(), "--keys-folder", tmpDir, "--key-name", "myexistingkey"})
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "exit status 1")
}

func TestEncryptionKeygenDetectExistingKey(t *testing.T) {
orgID := uuid.New()
userID := uuid.New()

tmpDir, err := os.MkdirTemp(TempDir, "encryption-keys")
defer func() { assert.Nil(t, os.RemoveAll(tmpDir)) }()

assert.Nil(t, err)

err = os.WriteFile(tmpDir+"/myexistingkey.public", []byte("mykey.public"), 0o755)
assert.Nil(t, err)

err = os.WriteFile(tmpDir+"/myexistingkey.private", []byte("mykey.private"), 0o755)
assert.Nil(t, err)

assert.FileExists(t, tmpDir+"/myexistingkey.public")
assert.FileExists(t, tmpDir+"/myexistingkey.private")

_, err = RunCliWithArgs(t, []string{"generate", "encryption-key", "--organization", orgID.String(), "--user", userID.String(), "--encryption-keys-folder", tmpDir, "--encryption-key-name", "myexistingkey"})
assert.NotNil(t, err)
assert.Equal(t, err.Error(), "exit status 1")
}
Expand Down
6 changes: 3 additions & 3 deletions src/cmd/turnkey/pkg/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ var APIClient *sdk.Client
// LoadKeypair require-loads the keypair referenced by the given name or as referenced form the global KeyName variable, if name is empty.
func LoadKeypair(name string) {
if name == "" {
name = KeyName
name = ApiKeyName
}

if keyStore == nil {
if apiKeyStore == nil {
OutputError(eris.New("keystore not loaded"))
}

apiKey, err := keyStore.Load(name)
apiKey, err := apiKeyStore.Load(name)
if err != nil {
OutputError(err)
}
Expand Down
124 changes: 124 additions & 0 deletions src/cmd/turnkey/pkg/decrypt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package pkg

import (
"github.com/rotisserie/eris"
"github.com/spf13/cobra"
"github.com/tkhq/go-sdk/pkg/enclave_encrypt"
"github.com/tkhq/go-sdk/pkg/encryptionkey"
)

var (
// Filepath to write the export bundle to.
exportBundlePath string

// EncryptionKeypair is the loaded Encryption Keypair.
EncryptionKeypair *encryptionkey.Key
)

func init() {
decryptCmd.Flags().StringVar(&exportBundlePath, "export-bundle-input", "", "filepath to read the export bundle from.")
decryptCmd.Flags().StringVar(&plaintextPath, "plaintext-output", "", "optional filepath to write the plaintext from that will be decrypted.")

rootCmd.AddCommand(decryptCmd)
}

var decryptCmd = &cobra.Command{
Use: "decrypt",
Short: "Decrypt a ciphertext",
Long: `Decrypt a ciphertext from a bundle exported from a Turnkey secure enclave.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
basicSetup(cmd)
LoadEncryptionKeypair("")
},
PreRun: func(cmd *cobra.Command, args []string) {
if exportBundlePath == "" {
OutputError(eris.New("--export-bundle-input must be specified"))
}
},
Run: func(cmd *cobra.Command, args []string) {
// read from export bundle path
exportBundle, err := readFile(exportBundlePath)
if err != nil {
OutputError(err)
}

// get encryption key
tkPrivateKey := EncryptionKeypair.GetPrivateKey()
kemPrivateKey, err := encryptionkey.DecodeTurnkeyPrivateKey(tkPrivateKey)
if err != nil {
OutputError(eris.Wrap(err, "failed to decode encryption private key"))
}

// set up enclave encrypt client
signerPublic, err := hexToPublicKey(signerPublicKey)
if err != nil {
OutputError(err)
}

encryptClient, err := enclave_encrypt.NewEnclaveEncryptClientFromTargetKey(signerPublic, *kemPrivateKey)
if err != nil {
OutputError(err)
}

// decrypt ciphertext
plaintextBytes, err := encryptClient.Decrypt([]byte(exportBundle), Organization)
if err != nil {
OutputError(err)
}

plaintext := string(plaintextBytes)

// output the plaintext if no filepath is passed
if plaintextPath == "" {
Output(plaintext)
return
}

err = writeFile(plaintext, plaintextPath)
if err != nil {
OutputError(err)
}
},
}

// LoadEncryptionKeypair require-loads the keypair referenced by the given name or as referenced form the global KeyName variable, if name is empty.
func LoadEncryptionKeypair(name string) {
if name == "" {
name = EncryptionKeyName
}

if encryptionKeyStore == nil {
OutputError(eris.New("encryption keystore not loaded"))
}

encryptionKey, err := encryptionKeyStore.Load(name)
if err != nil {
OutputError(err)
}

if encryptionKey == nil {
OutputError(eris.New("Encryption key not loaded"))
}

EncryptionKeypair = encryptionKey

// If we haven't had the organization explicitly set try to load it from key metadata.
if Organization == "" {
Organization = encryptionKey.Organization
}

// If org is _still_ empty, the encryption key is not usable.
if Organization == "" {
OutputError(eris.New("failed to associate the encryption key with an organization; please manually specify the organization ID"))
}

// If we haven't had the user explicitly set try to load it from key metadata.
if User == "" {
User = encryptionKey.User
}

// If user is _still_ empty, the encryption key is not usable.
if User == "" {
OutputError(eris.New("failed to associate the encryption key with a user; please manually specify the user ID"))
}
}
10 changes: 7 additions & 3 deletions src/cmd/turnkey/pkg/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (

var (
// user is the user ID to import wallets and private keys with.
user string
User string

// Filepath to write the import bundle to.
importBundlePath string
Expand All @@ -33,7 +33,7 @@ func init() {
encryptCmd.Flags().StringVar(&encryptedBundlePath, "encrypted-bundle-output", "", "filepath to read the encrypted bundle from.")
encryptCmd.Flags().StringVar(&plaintextPath, "plaintext-input", "", "filepath to read the plaintext from that will be encrypted.")
encryptCmd.Flags().StringVar(&keyFormat, "key-format", "mnemonic", "optional formatting to apply to the plaintext before it is encrypted.")
encryptCmd.Flags().StringVar(&user, "user", "", "ID of user to encrypting the plaintext.")
encryptCmd.Flags().StringVar(&User, "user", "", "ID of user to encrypting the plaintext.")

rootCmd.AddCommand(encryptCmd)
}
Expand All @@ -42,6 +42,10 @@ var encryptCmd = &cobra.Command{
Use: "encrypt",
Short: "Encrypt a plaintext",
Long: `Encrypt a plaintext into a bundle to be imported to a Turnkey secure enclave.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
basicSetup(cmd)
LoadEncryptionKeypair("")
},
PreRun: func(cmd *cobra.Command, args []string) {
if importBundlePath == "" {
OutputError(eris.New("--import-bundle-input must be specified"))
Expand Down Expand Up @@ -98,7 +102,7 @@ var encryptCmd = &cobra.Command{
}

// encrypt plaintext
clientSendMsg, err := encryptClient.Encrypt(plaintextBytes, []byte(importBundle), Organization, user)
clientSendMsg, err := encryptClient.Encrypt(plaintextBytes, []byte(importBundle), Organization, User)
if err != nil {
OutputError(err)
}
Expand Down
62 changes: 60 additions & 2 deletions src/cmd/turnkey/pkg/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import (
"github.com/spf13/cobra"

"github.com/tkhq/go-sdk/pkg/apikey"
"github.com/tkhq/go-sdk/pkg/encryptionkey"
"github.com/tkhq/go-sdk/pkg/store/local"
)

func init() {
generateCmd.AddCommand(apiKeyCmd)

encryptionKeyCmd.Flags().StringVar(&User, "user", "", "ID of user to generating the encryption key")
generateCmd.AddCommand(encryptionKeyCmd)

rootCmd.AddCommand(generateCmd)
}

Expand Down Expand Up @@ -52,11 +56,11 @@ var apiKeyCmd = &cobra.Command{
apiKey.Metadata.PublicKey = apiKey.TkPublicKey
apiKey.Metadata.Organizations = []string{Organization}

if err = keyStore.Store(name, apiKey); err != nil {
if err = apiKeyStore.Store(name, apiKey); err != nil {
OutputError(eris.Wrap(err, "failed to store new API keypair"))
}

localStore, ok := keyStore.(*local.Store)
localStore, ok := apiKeyStore.(*local.Store[apikey.Key, apikey.Metadata])
if !ok {
OutputError(eris.Wrap(err, "unhandled keystore type: expected *local.Store"))
}
Expand All @@ -68,3 +72,57 @@ var apiKeyCmd = &cobra.Command{
})
},
}

// Represents the command to generate an encryption key
var encryptionKeyCmd = &cobra.Command{
Use: "encryption-key",
Short: "Generate a Turnkey encryption key",
Long: `Generate a new encryption key that can be used for encrypting text sent from Turnkey secure enclaves.`,
PreRun: func(cmd *cobra.Command, args []string) {
if Organization == "" {
OutputError(eris.New("--organization must be specified"))
}

if User == "" {
OutputError(eris.New("--user must be specified"))
}
},
Run: func(cmd *cobra.Command, args []string) {
name, err := cmd.Flags().GetString("encryption-key-name")
if err != nil {
OutputError(eris.Wrap(err, "failed to read encryption key name"))
}

encryptionKey, err := encryptionkey.New(User, Organization)
if err != nil {
OutputError(eris.Wrap(err, "failed to create encryption keypair"))
}

if name == "-" {
Output(map[string]string{
"publicKey": encryptionKey.TkPublicKey,
"privateKey": encryptionKey.TkPrivateKey,
})
}

encryptionKey.Metadata.Name = name
encryptionKey.Metadata.PublicKey = encryptionKey.TkPublicKey
encryptionKey.Metadata.Organization = Organization
encryptionKey.Metadata.User = User

if err = encryptionKeyStore.Store(name, encryptionKey); err != nil {
OutputError(eris.Wrap(err, "failed to store new encryption keypair"))
}

localStore, ok := encryptionKeyStore.(*local.Store[encryptionkey.Key, encryptionkey.Metadata])
if !ok {
OutputError(eris.Wrap(err, "unhandled keystore type: expected *local.Store"))
}

Output(map[string]string{
"publicKey": encryptionKey.TkPublicKey,
"publicKeyFile": localStore.PublicKeyFile(name),
"privateKeyFile": localStore.PrivateKeyFile(name),
})
},
}
Loading

0 comments on commit b35acc5

Please sign in to comment.