Skip to content

[CHORE] Add encrypt-key command to CLI #858

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

Merged
merged 6 commits into from
Dec 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,38 @@ Possible networks: `etc`, `eth`, `mordor`, `testnet-internal`
./bin/mantis cli generate-key-pairs 5
```

- encrypt private key (default passphrase is empty string)
```
./bin/mantis cli encrypt-key --passphrase=pass 00b11c32957057651d56cd83085ef3b259319057e0e887bd0fdaee657e6f75d0
```

Command output uses the same format as keystore so it could be used ex. to setup private faucet

ex.
```
{
"id":"3038d914-c4cd-43b7-9e91-3391ea443f95",
"address":"c28e15ebdcbb0bdcb281d9d5084182c9c66d5d12",
"version":3,
"crypto":{
"cipher":"aes-128-ctr",
"ciphertext":"6ecdb74b2a33dc3c016b460dccc96843d9d050aea3df27a3ae5348e85b3adc3e",
"cipherparams":{
"iv":"096b6490fe29e42e68e2db902920cad6"
},
"kdf":"scrypt",
"kdfparams":{
"salt":"cdcc875e116e2824ab02f387210c2f4ad7fd6fa1a4fc791cc92b981e3062a23e",
"n":262144,
"r":8,
"p":1,
"dklen":32
},
"mac":"8388ae431198d31d57e4c17f44335c2f15959b0d08d1145234d82f0d253fa593"
}
}
```

### Building the client

#### SBT
Expand Down
37 changes: 31 additions & 6 deletions src/main/scala/io/iohk/ethereum/cli/CliCommands.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.monovore.decline.{Command, Opts}
import io.iohk.ethereum.crypto
import io.iohk.ethereum.crypto._
import io.iohk.ethereum.domain.Address
import io.iohk.ethereum.keystore.{EncryptedKey, EncryptedKeyJsonCodec}
import io.iohk.ethereum.utils.ByteStringUtils
import java.security.SecureRandom
import io.iohk.ethereum.security.SecureRandomBuilder
Expand All @@ -16,9 +17,16 @@ object CliCommands extends SecureRandomBuilder {
val generateKeyPairsCommand = "generate-key-pairs"
val deriveAddressCommand = "derive-address"
val generateAllocsCommand = "generate-allocs"
val encryptKeyCommand = "encrypt-key"

val balanceOption = "balance"
val keyOption = "key"
val addressOption = "address"
val passphraseOption = "passphrase"

val privateKeyArgument = "private-key"
private val privateKeyOpt = Opts
.argument[String](privateKeyArgument)

private val GeneratePrivateKeyCommand: Command[String] =
Command(name = generatePrivateKeyCommand, header = "Generate private key") {
Expand Down Expand Up @@ -47,11 +55,7 @@ object CliCommands extends SecureRandomBuilder {

private val DeriveAddressFromPrivateKey: Command[String] =
Command(name = deriveAddressCommand, header = "Derive address from private key") {

Opts
.argument[String]("private-key")
.map(Hex.decode)
.map(privKeyToAddress)
privateKeyOpt.map(Hex.decode).map(privKeyToAddress)
}

private val GenerateAllocs: Command[String] =
Expand All @@ -74,6 +78,21 @@ object CliCommands extends SecureRandomBuilder {
}
}

private val EncryptKey: Command[String] =
Command(name = encryptKeyCommand, header = "Encrypt private key") {

val privateKey = privateKeyOpt.map(ByteStringUtils.string2hash)

val passphrase = Opts
.option[String](long = passphraseOption, short = "p", help = "Passphrase")
.withDefault("")

(privateKey, passphrase).mapN { (privateKey, passphrase) =>
val encKey = EncryptedKey(privateKey, passphrase, secureRandom)
EncryptedKeyJsonCodec.toJson(encKey)
}
}

private def allocs(addresses: List[String], balance: BigInt): String =
s""""alloc": ${addresses
.map(address => s"""$address: { "balance": $balance }""")
Expand All @@ -87,6 +106,12 @@ object CliCommands extends SecureRandomBuilder {
}

val api: Command[String] = Command.apply(name = "cli", header = "Mantis CLI") {
Opts.subcommands(GeneratePrivateKeyCommand, DeriveAddressFromPrivateKey, GenerateAllocs, GenerateKeyPairs)
Opts.subcommands(
GeneratePrivateKeyCommand,
DeriveAddressFromPrivateKey,
GenerateAllocs,
GenerateKeyPairs,
EncryptKey
)
}
}
26 changes: 26 additions & 0 deletions src/test/scala/io/iohk/ethereum/cli/CliCommandsSpec.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.iohk.ethereum.cli

import io.iohk.ethereum.keystore.EncryptedKeyJsonCodec
import io.iohk.ethereum.utils.ByteStringUtils
import org.scalatest.EitherValues
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
Expand Down Expand Up @@ -92,6 +94,30 @@ class CliCommandsSpec extends AnyFlatSpec with Matchers with EitherValues {
val stringSplit = result.right.get.split("\\n\\n")
stringSplit.length shouldEqual numOfKeysAsInt
}

behavior of encryptKeyCommand
it should "encrypt private key (without passphrase)" in {
val json = api.parse(Seq(encryptKeyCommand, privateKey)).value

val decrypted = (for {
encrypted <- EncryptedKeyJsonCodec.fromJson(json)
decrypted <- encrypted.decrypt("")
} yield decrypted).value

ByteStringUtils.hash2string(decrypted) shouldBe privateKey
}

it should "encrypt private key (with passphrase)" in {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minor: this test looks like a perfect candidate for a property-based one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really. I don't want to test decryption/encryption here. It should be done in a different place. I only want to check if I get the correct JSON when I specify the correct arguments

val pass = "pass"
val json = api.parse(Seq(encryptKeyCommand, argument(passphraseOption, Some(pass)), privateKey)).value

val decrypted = (for {
encrypted <- EncryptedKeyJsonCodec.fromJson(json)
decrypted <- encrypted.decrypt(pass)
} yield decrypted).value

ByteStringUtils.hash2string(decrypted) shouldBe privateKey
}
}

object Fixture {
Expand Down