Repository: onflow/flow-cli (go)

Issue:
Add ability to decode PEM files to use for keys
### Issue To Be Solved

A common format used by users is to store their keys in PEM files. For example someone can store their keys using Gcloud and to retrieve the public key (https://cloud.google.com/kms/docs/retrieve-public-key) they get back a PEM file. When this is given to us, we can easily use this to decode their public key and create their account on mainnet.

### (Optional): Suggest A Solution
Something like below:
```
    "keys": [
      {
        "type": "pem",
        "index": 0,
        "signatureAlgorithm": "ECDSA_P256",
        "hashAlgorithm": "SHA3_256",
        "context": {
          "publicKeyPath": "/config/keys.pem"
        }
      },
]
```
And once the account is printed, it should display the decoded public key.

### (Optional): Context
A highly requested feature from partners looking to launch and our team responsible for deploying them.


Pull request:
Decode PEM public key
Closes #262 

## Description
Support decoding of PEM keys. 

Patch:
diff --git a/docs/decode-keys.md b/docs/decode-keys.md
index a80a76f4..3c2a53ca 100644
--- a/docs/decode-keys.md
+++ b/docs/decode-keys.md
@@ -1,29 +1,62 @@
 ---
-title: Decode Account Key with the Flow CLI
-sidebar_title: Decode Account Keys
-description: How to decode rlp encoded key pair from the command line
+title: Decode Public Keys with the Flow CLI
+sidebar_title: Decode Public Keys
+description: How to decode Flow public keys from the command line
 ---
 
-The Flow CLI provides a command to decode RLP encoded account key.
+The Flow CLI provides a command to decode encoded public account keys.
 
 ```shell
-flow keys decode <rlp encoded account key>
+flow keys decode <rlp|pem> <encoded public key>
 ```
 
 ## Example Usage
 
+### Decode RLP Encoded Public Key
 ```shell
-> flow keys decode f847b84084d716c14b051ad6b001624f738f5d302636e6b07cc75e4530af7776a4368a2b586dbefc0564ee28384c2696f178cbed52e62811bcc9ecb59568c996d342db2402038203e8
+> flow keys decode rlp f847b84084d716c14b051ad6b001624f738f5d302636e6b07cc75e4530af7776a4368a2b586dbefc0564ee28384c2696f178cbed52e62811bcc9ecb59568c996d342db2402038203e8
 
-Public Key 		 84d716c14b051ad6b001624f738f5d302636e6b07cc75e4530af7776a4368a2b586dbefc0564ee28384c2696f178cbed52e62811bcc9ecb59568c996d342db24 
+Public Key 		 84d716c1...bcc9ecb59568c996d342db24 
 Signature algorithm 	 ECDSA_P256
 Hash algorithm 		 SHA3_256
 Weight 			 1000
 Revoked 		 false
 ```
 
+### Decode PEM Encoded Public Key From File
+```shell
+> flow keys decode pem --from-file key.pem
+
+Public Key 		 d479b3c...c4615360039a6660a366a95f 
+Signature algorithm 	 ECDSA_P256
+Hash algorithm 		 UNKNOWN
+Revoked 		 false
+
+```
+
+## Arguments
+
+### Encoding
+- Valid inputs: `rlp`, `pem` 
+
+First argument specifies a valid encoding of the public key provided.
+
+### Optional: Public Key
+- Name: `encoded public key`
+- Valid inputs: valid encoded key content
+
+Optional second argument provides content of the encoded public key. 
+If this argument is omitted the `--from-file` must be used instead.  
+
 ## Flags
 
+### From File
+
+- Flag: `--from-file`
+- Valid inputs: valid filepath
+
+Provide file with the encoded public key. 
+
 ### Filter
 
 - Flag: `--filter`
diff --git a/internal/keys/decode.go b/internal/keys/decode.go
index cf6c34e8..34844211 100644
--- a/internal/keys/decode.go
+++ b/internal/keys/decode.go
@@ -25,16 +25,20 @@ import (
 	"github.com/onflow/flow-cli/pkg/flowcli/services"
 )
 
-type flagsDecode struct{}
+type flagsDecode struct {
+	SigAlgo  string `default:"ECDSA_P256" flag:"sig-algo" info:"Signature algorithm"`
+	FromFile string `default:"" flag:"from-file" info:"Load key from file"`
+}
 
 var decodeFlags = flagsDecode{}
 
 var DecodeCommand = &command.Command{
 	Cmd: &cobra.Command{
-		Use:     "decode <rlp encoded account key>",
-		Short:   "Decode a rlp encoded account key",
-		Args:    cobra.ExactArgs(1),
-		Example: "flow keys decode f847b8408...2402038203e8",
+		Use:       "decode <rlp|pem> <encoded public key>",
+		Short:     "Decode an encoded public key",
+		Args:      cobra.RangeArgs(1, 2),
+		ValidArgs: []string{"rlp", "pem"},
+		Example:   "flow keys decode rlp f847b8408...2402038203e8",
 	},
 	Flags: &decodeFlags,
 	Run: func(
@@ -43,14 +47,23 @@ var DecodeCommand = &command.Command{
 		globalFlags command.GlobalFlags,
 		services *services.Services,
 	) (command.Result, error) {
-		rlpEncoded := args[0]
+		encoding := args[0]
+
+		var encoded string
+		if len(args) > 1 {
+			encoded = args[1]
+		}
 
-		accountKey, err := services.Keys.Decode(rlpEncoded)
+		/* TODO: --from-file flag should be removed and replaced with $(echo file)
+		but cobra has an issue with parsing pem content as it recognize it as flag due to ---- characters */
+		accountKey, err := services.Keys.Decode(encoded, encoding, decodeFlags.FromFile, decodeFlags.SigAlgo)
 		if err != nil {
 			return nil, err
 		}
 
-		pubKey := accountKey.PublicKey
-		return &KeyResult{publicKey: pubKey, accountKey: accountKey}, err
+		return &KeyResult{
+			publicKey:  accountKey.PublicKey,
+			accountKey: accountKey,
+		}, err
 	},
 }
diff --git a/internal/keys/keys.go b/internal/keys/keys.go
index 7a779f23..d28b0e25 100644
--- a/internal/keys/keys.go
+++ b/internal/keys/keys.go
@@ -77,8 +77,11 @@ func (k *KeyResult) String() string {
 	if k.accountKey != nil {
 		_, _ = fmt.Fprintf(writer, "Signature algorithm \t %s\n", k.accountKey.SigAlgo)
 		_, _ = fmt.Fprintf(writer, "Hash algorithm \t %s\n", k.accountKey.HashAlgo)
-		_, _ = fmt.Fprintf(writer, "Weight \t %d\n", k.accountKey.Weight)
 		_, _ = fmt.Fprintf(writer, "Revoked \t %t\n", k.accountKey.Revoked)
+
+		if k.accountKey.Weight >= 0 { // dont show empty value
+			_, _ = fmt.Fprintf(writer, "Weight \t %d\n", k.accountKey.Weight)
+		}
 	}
 
 	writer.Flush()
diff --git a/pkg/flowcli/services/keys.go b/pkg/flowcli/services/keys.go
index 1cf7f8d8..2e0ddfe1 100644
--- a/pkg/flowcli/services/keys.go
+++ b/pkg/flowcli/services/keys.go
@@ -21,6 +21,7 @@ package services
 import (
 	"encoding/hex"
 	"fmt"
+	"strings"
 
 	"github.com/onflow/flow-go-sdk"
 	"github.com/onflow/flow-go-sdk/crypto"
@@ -51,6 +52,9 @@ func NewKeys(
 	}
 }
 
+const PEM string = "pem"
+const RLP string = "rlp"
+
 // Generate generates a new private key from the given seed and signature algorithm.
 func (k *Keys) Generate(inputSeed string, signatureAlgo string) (crypto.PrivateKey, error) {
 	var seed []byte
@@ -72,22 +76,66 @@ func (k *Keys) Generate(inputSeed string, signatureAlgo string) (crypto.PrivateK
 
 	privateKey, err := crypto.GeneratePrivateKey(sigAlgo, seed)
 	if err != nil {
-		return nil, fmt.Errorf("failed to generate private key: %v", err)
+		return nil, fmt.Errorf("failed to generate private key: %w", err)
 	}
 
 	return privateKey, nil
 }
 
-func (k *Keys) Decode(publicKey string) (*flow.AccountKey, error) {
+// Decode encoded public key using supported encoding type
+func (k *Keys) Decode(encoded string, encoding string, fromFile string, sigAlgo string) (*flow.AccountKey, error) {
+	// todo refactor this to commands (as part of api refactor) and pass typed values
+	if encoded != "" && fromFile != "" {
+		return nil, fmt.Errorf("can not pass both command argument and from file flag")
+	}
+	if encoded == "" && fromFile == "" {
+		return nil, fmt.Errorf("provide argument for encoded key or use from file flag")
+	}
+
+	if fromFile != "" {
+		e, err := util.LoadFile(fromFile)
+		if err != nil {
+			return nil, err
+		}
+		encoded = strings.TrimSpace(string(e))
+	}
+
+	switch strings.ToLower(encoding) {
+	case PEM:
+		return decodePEM(encoded, sigAlgo)
+	case RLP:
+		return decodeRLP(encoded)
+	default:
+		return nil, fmt.Errorf("encoding type not supported. Valid encoding: RLP and PEM")
+	}
+}
+
+func decodeRLP(publicKey string) (*flow.AccountKey, error) {
 	publicKeyBytes, err := hex.DecodeString(publicKey)
 	if err != nil {
-		return nil, fmt.Errorf("failed to decode public key: %v", err)
+		return nil, fmt.Errorf("failed to decode public key: %w", err)
 	}
 
 	accountKey, err := flow.DecodeAccountKey(publicKeyBytes)
 	if err != nil {
-		return nil, fmt.Errorf("failed to decode private key bytes: %v", err)
+		return nil, fmt.Errorf("failed to decode: %w", err)
 	}
 
 	return accountKey, nil
 }
+
+func decodePEM(key string, sigAlgo string) (*flow.AccountKey, error) {
+	pk, err := crypto.DecodePublicKeyPEM(
+		crypto.StringToSignatureAlgorithm(sigAlgo),
+		key,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return &flow.AccountKey{
+		PublicKey: pk,
+		SigAlgo:   crypto.ECDSA_P256,
+		Weight:    -1,
+	}, nil
+}


Test patch:
diff --git a/pkg/flowcli/services/keys_test.go b/pkg/flowcli/services/keys_test.go
index 09edaead..c579be4b 100644
--- a/pkg/flowcli/services/keys_test.go
+++ b/pkg/flowcli/services/keys_test.go
@@ -64,7 +64,12 @@ func TestKeys(t *testing.T) {
 	})
 
 	t.Run("RLP decode keys", func(t *testing.T) {
-		dkey, err := keys.Decode("f847b84084d716c14b051ad6b001624f738f5d302636e6b07cc75e4530af7776a4368a2b586dbefc0564ee28384c2696f178cbed52e62811bcc9ecb59568c996d342db2402038203e8")
+		dkey, err := keys.Decode(
+			"f847b84084d716c14b051ad6b001624f738f5d302636e6b07cc75e4530af7776a4368a2b586dbefc0564ee28384c2696f178cbed52e62811bcc9ecb59568c996d342db2402038203e8",
+			"rlp",
+			"",
+			crypto.ECDSA_P256.String(),
+		)
 
 		assert.NoError(t, err)
 		assert.Equal(t, dkey.PublicKey.String(), "0x84d716c14b051ad6b001624f738f5d302636e6b07cc75e4530af7776a4368a2b586dbefc0564ee28384c2696f178cbed52e62811bcc9ecb59568c996d342db24")