Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wallet: Add import-file command #146

Merged
merged 3 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(cmd/wallet): Import key from PEM file
  • Loading branch information
matevz committed Oct 19, 2023
commit 9883374fb480f793653774372e76cc4fad20cb5d
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