Skip to content

Commit

Permalink
feat(cmd/wallet): Import key from PEM file
Browse files Browse the repository at this point in the history
  • Loading branch information
matevz committed Oct 19, 2023
1 parent 6e84110 commit 9883374
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 14 deletions.
14 changes: 11 additions & 3 deletions cmd/wallet/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ var createCmd = &cobra.Command{
cfg := config.Global()
name := args[0]

checkAccountExists(cfg, name)

af, err := wallet.Load(accKind)
cobra.CheckErr(err)

Expand All @@ -37,9 +39,6 @@ var createCmd = &cobra.Command{
err = accCfg.SetConfigFromFlags()
cobra.CheckErr(err)

if _, exists := cfg.AddressBook.All[name]; exists {
cobra.CheckErr(fmt.Errorf("address named '%s' already exists in address book", name))
}
err = cfg.Wallet.Create(name, passphrase, accCfg)
cobra.CheckErr(err)

Expand All @@ -48,6 +47,15 @@ var createCmd = &cobra.Command{
},
}

func checkAccountExists(cfg *config.Config, name string) {
if _, exists := cfg.Wallet.All[name]; exists {
cobra.CheckErr(fmt.Errorf("account '%s' already exists in the wallet", name))
}
if _, exists := cfg.AddressBook.All[name]; exists {
cobra.CheckErr(fmt.Errorf("address named '%s' already exists in the address book", name))
}
}

func init() {
flags := flag.NewFlagSet("", flag.ContinueOnError)
kinds := make([]string, 0, len(wallet.AvailableKinds()))
Expand Down
9 changes: 1 addition & 8 deletions cmd/wallet/import.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package wallet

import (
"fmt"

"github.com/AlecAivazis/survey/v2"
"github.com/spf13/cobra"

Expand All @@ -20,12 +18,7 @@ var importCmd = &cobra.Command{
cfg := config.Global()
name := args[0]

if _, exists := cfg.Wallet.All[name]; exists {
cobra.CheckErr(fmt.Errorf("account '%s' already exists in the wallet", name))
}
if _, exists := cfg.AddressBook.All[name]; exists {
cobra.CheckErr(fmt.Errorf("address named '%s' already exists in the address book", name))
}
checkAccountExists(cfg, name)

// NOTE: We only support importing into the file-based wallet for now.
af, err := wallet.Load(walletFile.Kind)
Expand Down
88 changes: 88 additions & 0 deletions cmd/wallet/import_file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package wallet

import (
"encoding/base64"
"encoding/hex"
"encoding/pem"
"fmt"
"os"

"github.com/spf13/cobra"

"github.com/oasisprotocol/cli/cmd/common"
"github.com/oasisprotocol/cli/config"
"github.com/oasisprotocol/cli/wallet"
walletFile "github.com/oasisprotocol/cli/wallet/file"
)

var importFileCmd = &cobra.Command{
Use: "import-file <name> <entity.pem>",
Short: "Import an existing account from file",
Long: "Import the private key from an existing PEM file",
Args: cobra.ExactArgs(2),
Run: func(cmd *cobra.Command, args []string) {
cfg := config.Global()
name := args[0]
filename := args[1]

checkAccountExists(cfg, name)

rawFile, err := os.ReadFile(filename)
cobra.CheckErr(err)

block, _ := pem.Decode(rawFile)
if block == nil { //nolint: staticcheck
cobra.CheckErr(fmt.Errorf("failed to decode PEM file"))
}

algorithm, err := detectAlgorithm(block.Type) //nolint: staticcheck
cobra.CheckErr(err)

// Ask for passphrase.
passphrase := common.AskNewPassphrase()

accCfg := &config.Account{
Kind: walletFile.Kind,
Config: map[string]interface{}{
"algorithm": algorithm,
},
}

src := &wallet.ImportSource{
Kind: wallet.ImportKindPrivateKey,
Data: encodeKeyData(algorithm, block.Bytes), //nolint: staticcheck
}

err = cfg.Wallet.Import(name, passphrase, accCfg, src)
cobra.CheckErr(err)

err = cfg.Save()
cobra.CheckErr(err)
},
}

// detectAlgorithm detects the key type based on the PEM type.
func detectAlgorithm(pemType string) (string, error) {
switch pemType {
case "ED25519 PRIVATE KEY":
return wallet.AlgorithmEd25519Raw, nil
case "EC PRIVATE KEY":
return wallet.AlgorithmSecp256k1Raw, nil
case "SR25519 PRIVATE KEY":
return wallet.AlgorithmSr25519Raw, nil
}

return "", fmt.Errorf("unsupported PEM type: %s", pemType)
}

// encodeKeyData re-encodes the key in raw bytes back to the user-readable string for import.
func encodeKeyData(algorithm string, rawKey []byte) string {
switch algorithm {
case wallet.AlgorithmEd25519Raw, wallet.AlgorithmSr25519Raw:
return base64.StdEncoding.EncodeToString(rawKey)
case wallet.AlgorithmSecp256k1Raw:
return hex.EncodeToString(rawKey)
}

return ""
}
File renamed without changes.
1 change: 1 addition & 0 deletions cmd/wallet/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func init() {
Cmd.AddCommand(renameCmd)
Cmd.AddCommand(setDefaultCmd)
Cmd.AddCommand(importCmd)
Cmd.AddCommand(importFileCmd)
Cmd.AddCommand(exportCmd)
Cmd.AddCommand(remoteSignerCmd)
}
38 changes: 35 additions & 3 deletions docs/wallet.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@ Oasis CLI for your accounts:
used for accounts living on EVM-compatible ParaTimes such as Sapphire or
Emerald. The same account can be imported into Metamask and other Ethereum
wallets.
- `ed25519-raw`: [Ed25519] keypair imported directly from the Base64-encoded
private key. No key derivation is involved. This setting is primarily used by
the network validators to sign the governance and other consensus-layer
transactions.
- `ed25519-legacy`: [Ed25519] keypair using a legacy 5-component derivation
path. This is the preferred setting for Oasis accounts stored on a hardware
wallet like Ledger. It is called legacy, because it was first implemented
before the [ADR-8] was standardized.
- `sr25519-adr8`: [Sr25519] keypair using the [ADR-8] derivation path. This is
an alternative signature scheme for signing ParaTime transactions.
- `ed25519-raw`, `secp256k1-raw` and `sr25519-raw`: Respective Ed25519,
Secp256k1 and Sr25519 keypairs imported directly from Base32 or Hex-encoded
private keys. No key derivation is involved.
- `secp256k1-raw` and `sr25519-raw`: Respective Secp256k1 and Sr25519 keypairs
imported directly from the Hex- or Base64-encoded private key. No key
derivation is involved.

:::tip

Expand Down Expand Up @@ -358,6 +362,34 @@ name of the desired default account.

## Advanced

### Import an Existing Keypair from PEM file {#import-file}

Existing node operators may already use their Ed25519 private key for running
their nodes stored in a PEM-encoded file typically named `entity.pem`. In order
to submit their governance transaction, for example to vote on the network
upgrade using the Oasis CLI, they need to import the key into the Oasis CLI
wallet:

```shell
oasis wallet import-file my_entity entity.pem
```

```
? Choose a new passphrase:
? Repeat passphrase:
```

The key is now safely stored and encrypted inside the Oasis CLI.

```shell
oasis wallet list
```

```
ACCOUNT KIND ADDRESS
my_entity file (ed25519-raw) oasis1qpe0vnm0ahczgc353vytvtz9r829le4pjux8lc5z
```

### Remote Signer for `oasis-node` {#remote-signer}

You can bind the account in your Oasis CLI wallet with a local instance of
Expand Down

0 comments on commit 9883374

Please sign in to comment.