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

feat: Add encrypt-data CLI #529

Merged
merged 11 commits into from
Dec 12, 2022
108 changes: 108 additions & 0 deletions cmd/panacead/cmd/encrypt_data.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package cmd

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

"github.com/btcsuite/btcd/btcec"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/medibloc/panacea-core/v2/crypto"
oracletypes "github.com/medibloc/panacea-core/v2/x/oracle/types"
"github.com/spf13/cobra"
"github.com/tendermint/tendermint/libs/cli"
)

func EncryptDataCmd(defaultNodeHome string) *cobra.Command {
cmd := &cobra.Command{
Use: "encrypt-data [input-file-path] [output-file-path] [key-name]",
Short: "Encrypt data with shared key which consists of oracle public key and provider's private key",
Long: `
This command can encrypt data with shared key which consists of oracle public key and provider's private key.
The key to be used for encryption should be stored in the localStore.
If not stored, please add the key first via the following command.
panacead keys add ...
`,
Args: cobra.ExactArgs(3),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientQueryContext(cmd)
if err != nil {
return err
}

queryClient := oracletypes.NewQueryClient(clientCtx)

origData, err := os.ReadFile(args[0])
if err != nil {
return err
}

params, err := queryClient.Params(cmd.Context(), &oracletypes.QueryOracleParamsRequest{})
if err != nil {
return err
}

oraclePubKey := params.GetParams().GetOraclePublicKey()

encryptedData, err := encrypt(clientCtx, args[2], origData, oraclePubKey)
if err != nil {
return err
}

if err := os.WriteFile(args[1], encryptedData, 0644); err != nil {
return err
}

return nil
},
}

cmd.PersistentFlags().String(flags.FlagHome, defaultNodeHome, "The application home directory")
cmd.PersistentFlags().String(flags.FlagKeyringDir, "", "The client Keyring directory; if omitted, the default 'home' directory will be used")
cmd.PersistentFlags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)")
cmd.PersistentFlags().String(cli.OutputFlag, "text", "Output format (text|json)")
cmd.PersistentFlags().String(flags.FlagChainID, "", "The network chain ID")

flags.AddQueryFlagsToCmd(cmd)

return cmd
}

func encrypt(clientCtx client.Context, keyName string, origData []byte, oraclePubKeyStr string) ([]byte, error) {
// get unsafe export private key from keystore
privKeyHex, err := keyring.NewUnsafe(clientCtx.Keyring).UnsafeExportPrivKeyHex(keyName)
if err != nil {
return nil, err
}

privKeyBz, err := hex.DecodeString(privKeyHex)
if err != nil {
return nil, err
}

privKey, _ := btcec.PrivKeyFromBytes(btcec.S256(), privKeyBz)

// oracle public key
oraclePubKeyBz, err := base64.StdEncoding.DecodeString(oraclePubKeyStr)
if err != nil {
return nil, err
}

oraclePubKey, err := btcec.ParsePubKey(oraclePubKeyBz, btcec.S256())
if err != nil {
return nil, err
}

// shared key
sharedKey := crypto.DeriveSharedKey(privKey, oraclePubKey, crypto.KDFSHA256)

// encrypt data
encryptedData, err := crypto.Encrypt(sharedKey, nil, origData)
if err != nil {
return nil, err
}

return encryptedData, nil
}
1 change: 1 addition & 0 deletions cmd/panacead/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func initRootCmd(rootCmd *cobra.Command, encodingConfig params.EncodingConfig) {
debug.Cmd(),
// this line is used by starport scaffolding # stargate/root/commands
AddGenesisWasmMsgCmd(app.DefaultNodeHome),
EncryptDataCmd(app.DefaultNodeHome),
)

a := appCreator{encodingConfig}
Expand Down
83 changes: 83 additions & 0 deletions crypto/aes256.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package crypto

import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"

"github.com/btcsuite/btcd/btcec"
)

// DeriveSharedKey derives a shared key (which can be used for asymmetric encryption)
// using a specified KDF (Key Derivation Function)
// from a shared secret generated by Diffie-Hellman key exchange (ECDH).
func DeriveSharedKey(priv *btcec.PrivateKey, pub *btcec.PublicKey, kdf func([]byte) []byte) []byte {
sharedSecret := btcec.GenerateSharedSecret(priv, pub)
return kdf(sharedSecret)
}

// KDFSHA256 is a key derivation function which uses SHA256.
func KDFSHA256(in []byte) []byte {
out := sha256.Sum256(in)
return out[:]
}

// Encrypt combines secretKey and secondKey to encrypt with AES256-GCM method.
func Encrypt(secretKey, additional, data []byte) ([]byte, error) {
if len(secretKey) != 32 {
return nil, fmt.Errorf("secret key is not for AES-256: total %d bits", 8*len(secretKey))
}

// prepare AES-256-GSM cipher
block, err := aes.NewCipher(secretKey)
if err != nil {
return nil, err
}

aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

// make random nonce
nonce := make([]byte, aesGCM.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}

// encrypt data with second key
ciphertext := aesGCM.Seal(nonce, nonce, data, additional)
return ciphertext, nil
}

// Decrypt combines secretKey and secondKey to decrypt AES256-GCM.
func Decrypt(secretKey []byte, additional []byte, ciphertext []byte) ([]byte, error) {
if len(secretKey) != 32 {
return nil, fmt.Errorf("secret key is not for AES-256: total %d bits", 8*len(secretKey))
}

// prepare AES-256-GCM cipher
block, err := aes.NewCipher(secretKey)
if err != nil {
return nil, err
}

aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}

nonceSize := aesgcm.NonceSize()
nonce, pureCiphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

// decrypt ciphertext with second key
plaintext, err := aesgcm.Open(nil, nonce, pureCiphertext, additional)
if err != nil {
return nil, err
}

return plaintext, nil
}
34 changes: 34 additions & 0 deletions crypto/aes256_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package crypto

import (
"crypto/rand"
"io"
"testing"

"github.com/btcsuite/btcd/btcec"
"github.com/stretchr/testify/require"
)

func TestDecryptWithAES256(t *testing.T) {
privKey1, err := btcec.NewPrivateKey(btcec.S256())
require.NoError(t, err)
privKey2, err := btcec.NewPrivateKey(btcec.S256())
require.NoError(t, err)

data := []byte("hello, Panacea")

shareKey1 := DeriveSharedKey(privKey1, privKey2.PubKey(), KDFSHA256)
shareKey2 := DeriveSharedKey(privKey2, privKey1.PubKey(), KDFSHA256)

nonce := make([]byte, 12)
_, err = io.ReadFull(rand.Reader, nonce)
require.NoError(t, err)

encryptedData, err := Encrypt(shareKey1, nonce, data)
require.NoError(t, err)

decryptedData, err := Decrypt(shareKey2, nonce, encryptedData)
require.NoError(t, err)

require.Equal(t, decryptedData, data)
}