Skip to content

Commit

Permalink
Problem: no keyring interface for e2ee to store arbitrary payload
Browse files Browse the repository at this point in the history
  • Loading branch information
yihuang committed Apr 29, 2024
1 parent 3275b57 commit 38ca8d9
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 2 deletions.
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
cosmossdk.io/x/evidence v0.1.0
cosmossdk.io/x/feegrant v0.1.0
cosmossdk.io/x/upgrade v0.1.1
github.com/99designs/keyring v1.2.2
github.com/cometbft/cometbft v0.38.7-0.20240412124004-1f67e396cf45
github.com/cosmos/cosmos-db v1.0.3-0.20240408151834-e75f6e4b28d8
github.com/cosmos/cosmos-proto v1.0.0-beta.4
Expand All @@ -40,6 +41,8 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
github.com/test-go/testify v1.1.4
golang.org/x/crypto v0.21.0
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de
google.golang.org/grpc v1.63.2
google.golang.org/protobuf v1.33.0
Expand All @@ -57,7 +60,6 @@ require (
cosmossdk.io/x/tx v0.13.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/99designs/keyring v1.2.2 // indirect
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
github.com/DataDog/zstd v1.5.5 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
Expand Down Expand Up @@ -225,7 +227,6 @@ require (
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.23.0 // indirect
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"
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), err
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
}
}
}
19 changes: 19 additions & 0 deletions x/e2ee/keyring/keyring_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package keyring

import (
"testing"

"github.com/cosmos/cosmos-sdk/crypto/keyring"
"github.com/test-go/testify/require"
)

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

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

secret, err := kr.Get("test")
require.NoError(t, err)
require.Equal(t, []byte("test"), secret)
}

0 comments on commit 38ca8d9

Please sign in to comment.