Skip to content

Commit

Permalink
Problem: no keyring interface for e2ee to store arbitrary payload (cr…
Browse files Browse the repository at this point in the history
…ypto-org-chain#1413)

changelo

add age encrypt/decrypt in unit test

Update x/e2ee/keyring/keyring.go

Signed-off-by: yihuang <huang@crypto.com>

fix lint
  • Loading branch information
yihuang committed Apr 30, 2024
1 parent d3592da commit ecf5784
Show file tree
Hide file tree
Showing 3 changed files with 253 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

* [#1407](https://github.com/crypto-org-chain/cronos/pull/1407) Add end-to-end encryption module.

### Improvements

* [#1413](https://github.com/crypto-org-chain/cronos/pull/1413) Add custom keyring implementation for e2ee module.

*April 22, 2024*

## v1.2.1
Expand Down
202 changes: 202 additions & 0 deletions x/e2ee/keyring/keyring.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package keyring

import (
"bufio"
"fmt"
"io"
"os"
"path/filepath"

"github.com/99designs/keyring"
"golang.org/x/crypto/bcrypt"

errorsmod "cosmossdk.io/errors"
"github.com/cosmos/cosmos-sdk/client/input"
sdkkeyring "github.com/cosmos/cosmos-sdk/crypto/keyring"
)

const (
keyringFileDirName = "e2ee-keyring-file"
keyringTestDirName = "e2ee-keyring-test"
passKeyringPrefix = "e2ee-keyring-%s" //nolint: gosec
maxPassphraseEntryAttempts = 3
)

type Keyring interface {
Get(string) ([]byte, error)
Set(string, []byte) error
}

func New(
appName, backend, rootDir string, userInput io.Reader,
) (Keyring, error) {
var (
db keyring.Keyring
err error
)
serviceName := appName + "-e2ee"
switch backend {
case sdkkeyring.BackendMemory:
return newKeystore(keyring.NewArrayKeyring(nil), sdkkeyring.BackendMemory), nil
case sdkkeyring.BackendTest:
db, err = keyring.Open(keyring.Config{
AllowedBackends: []keyring.BackendType{keyring.FileBackend},
ServiceName: serviceName,
FileDir: filepath.Join(rootDir, keyringTestDirName),
FilePasswordFunc: func(_ string) (string, error) {
return "test", nil
},
})
case sdkkeyring.BackendFile:
fileDir := filepath.Join(rootDir, keyringFileDirName)
db, err = keyring.Open(keyring.Config{
AllowedBackends: []keyring.BackendType{keyring.FileBackend},
ServiceName: serviceName,
FileDir: fileDir,
FilePasswordFunc: newRealPrompt(fileDir, userInput),
})
case sdkkeyring.BackendOS:
db, err = keyring.Open(keyring.Config{
ServiceName: serviceName,
FileDir: rootDir,
KeychainTrustApplication: true,
FilePasswordFunc: newRealPrompt(rootDir, userInput),
})
case sdkkeyring.BackendKWallet:
db, err = keyring.Open(keyring.Config{
AllowedBackends: []keyring.BackendType{keyring.KWalletBackend},
ServiceName: "kdewallet",
KWalletAppID: serviceName,
KWalletFolder: "",
})
case sdkkeyring.BackendPass:
prefix := fmt.Sprintf(passKeyringPrefix, serviceName)
db, err = keyring.Open(keyring.Config{
AllowedBackends: []keyring.BackendType{keyring.PassBackend},
ServiceName: serviceName,
PassPrefix: prefix,
})
default:
return nil, errorsmod.Wrap(sdkkeyring.ErrUnknownBacked, backend)
}

if err != nil {
return nil, err
}

return newKeystore(db, backend), nil
}

var _ Keyring = keystore{}

type keystore struct {
db keyring.Keyring
backend string
}

func newKeystore(kr keyring.Keyring, backend string) keystore {
return keystore{
db: kr,
backend: backend,
}
}

func (ks keystore) Get(name string) ([]byte, error) {
item, err := ks.db.Get(name)
if err != nil {
return nil, err
}

return item.Data, nil
}

func (ks keystore) Set(name string, secret []byte) error {
return ks.db.Set(keyring.Item{
Key: name,
Data: secret,
Label: name,
})
}

func newRealPrompt(dir string, buf io.Reader) func(string) (string, error) {
return func(prompt string) (string, error) {
keyhashStored := false
keyhashFilePath := filepath.Join(dir, "keyhash")

var keyhash []byte

_, err := os.Stat(keyhashFilePath)

switch {
case err == nil:
keyhash, err = os.ReadFile(keyhashFilePath)
if err != nil {
return "", errorsmod.Wrap(err, fmt.Sprintf("failed to read %s", keyhashFilePath))
}

keyhashStored = true

case os.IsNotExist(err):
keyhashStored = false

default:
return "", errorsmod.Wrap(err, fmt.Sprintf("failed to open %s", keyhashFilePath))
}

failureCounter := 0

for {
failureCounter++
if failureCounter > maxPassphraseEntryAttempts {
return "", sdkkeyring.ErrMaxPassPhraseAttempts
}

buf := bufio.NewReader(buf)
pass, err := input.GetPassword(fmt.Sprintf("Enter keyring passphrase (attempt %d/%d):", failureCounter, maxPassphraseEntryAttempts), buf)
if err != nil {
// NOTE: LGTM.io reports a false positive alert that states we are printing the password,
// but we only log the error.
//
// lgtm [go/clear-text-logging]
fmt.Fprintln(os.Stderr, err)
continue
}

if keyhashStored {
if err := bcrypt.CompareHashAndPassword(keyhash, []byte(pass)); err != nil {
fmt.Fprintln(os.Stderr, "incorrect passphrase")
continue
}

return pass, nil
}

reEnteredPass, err := input.GetPassword("Re-enter keyring passphrase:", buf)
if err != nil {
// NOTE: LGTM.io reports a false positive alert that states we are printing the password,
// but we only log the error.
//
// lgtm [go/clear-text-logging]
fmt.Fprintln(os.Stderr, err)
continue
}

if pass != reEnteredPass {
fmt.Fprintln(os.Stderr, "passphrase do not match")
continue
}

passwordHash, err := bcrypt.GenerateFromPassword([]byte(pass), 2)
if err != nil {
fmt.Fprintln(os.Stderr, err)
continue
}

if err := os.WriteFile(keyhashFilePath, passwordHash, 0o600); err != nil {
return "", err
}

return pass, nil
}
}
}
47 changes: 47 additions & 0 deletions x/e2ee/keyring/keyring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package keyring

import (
"bytes"
"io"
"testing"

"filippo.io/age"
"github.com/stretchr/testify/require"

"github.com/cosmos/cosmos-sdk/crypto/keyring"
)

func TestKeyring(t *testing.T) {
kr, err := New("cronosd", keyring.BackendTest, t.TempDir(), nil)
require.NoError(t, err)

identity, err := age.GenerateX25519Identity()
require.NoError(t, err)

var ciphertext []byte
{
dst := bytes.NewBuffer(nil)
writer, err := age.Encrypt(dst, identity.Recipient())
require.NoError(t, err)
writer.Write([]byte("test"))
writer.Close()
ciphertext = dst.Bytes()
}

require.NoError(t, kr.Set("test", []byte(identity.String())))

secret, err := kr.Get("test")
require.NoError(t, err)

identity, err = age.ParseX25519Identity(string(secret))
require.NoError(t, err)

{
reader, err := age.Decrypt(bytes.NewReader(ciphertext), identity)
require.NoError(t, err)
bz, err := io.ReadAll(reader)
require.NoError(t, err)

require.Equal(t, []byte("test"), bz)
}
}

0 comments on commit ecf5784

Please sign in to comment.