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

Problem: no keyring interface for e2ee to store arbitrary payload #1413

Merged
merged 1 commit into from
Apr 29, 2024
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* (versiondb) [#1379](https://github.com/crypto-org-chain/cronos/pull/1379) Flush versiondb when graceful shutdown, make rocksdb upgrade smooth.
* (store) [#1378](https://github.com/crypto-org-chain/cronos/pull/1378) Upgrade rocksdb to `v8.11.3`.
* (versiondb) [#1387](https://github.com/crypto-org-chain/cronos/pull/1387) Add dedicated config section for versiondb, prepare for sdk 0.50 integration.
* [#1413](https://github.com/crypto-org-chain/cronos/pull/1413) Add custom keyring implementation for e2ee module.

### Bug Fixes

Expand Down
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ require (
cosmossdk.io/x/evidence v0.1.0
cosmossdk.io/x/feegrant v0.1.0
cosmossdk.io/x/upgrade v0.1.1
filippo.io/age v1.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 +42,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 +61,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 +228,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
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ cosmossdk.io/x/feegrant v0.1.0/go.mod h1:4r+FsViJRpcZif/yhTn+E0E6OFfg4n0Lx+6cCtn
cosmossdk.io/x/upgrade v0.1.1 h1:aoPe2gNvH+Gwt/Pgq3dOxxQVU3j5P6Xf+DaUJTDZATc=
cosmossdk.io/x/upgrade v0.1.1/go.mod h1:MNLptLPcIFK9CWt7Ra//8WUZAxweyRDNcbs5nkOcQy0=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/age v1.1.1 h1:pIpO7l151hCnQ4BdyBujnGP2YlUo0uj6sAVNHGBvXHg=
filippo.io/age v1.1.1/go.mod h1:l03SrzDUrBkdBx8+IILdnn2KZysqQdbEBUQ4p3sqEQE=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
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

Check warning on line 40 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L39-L40

Added lines #L39 - L40 were not covered by tests
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)

Check warning on line 80 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L50-L80

Added lines #L50 - L80 were not covered by tests
}

if err != nil {
return nil, err

Check warning on line 84 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L84

Added line #L84 was not covered by tests
}

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

Check warning on line 107 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L107

Added line #L107 was not covered by tests
}

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")

Check warning on line 124 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L121-L124

Added lines #L121 - L124 were not covered by tests

var keyhash []byte

Check warning on line 126 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L126

Added line #L126 was not covered by tests

_, err := os.Stat(keyhashFilePath)

Check warning on line 128 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L128

Added line #L128 was not covered by tests

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

Check warning on line 134 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L130-L134

Added lines #L130 - L134 were not covered by tests
}

keyhashStored = true

Check warning on line 137 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L137

Added line #L137 was not covered by tests

case os.IsNotExist(err):
keyhashStored = false

Check warning on line 140 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L139-L140

Added lines #L139 - L140 were not covered by tests

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

Check warning on line 143 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L142-L143

Added lines #L142 - L143 were not covered by tests
}

failureCounter := 0

Check warning on line 146 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L146

Added line #L146 was not covered by tests

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

Check warning on line 151 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L148-L151

Added lines #L148 - L151 were not covered by tests
}

buf := bufio.NewReader(buf)
pass, err := input.GetPassword(fmt.Sprintf("Enter keyring passphrase (attempt %d/%d):", failureCounter, maxPassphraseEntryAttempts), buf)
if err != nil {

Check warning on line 156 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L154-L156

Added lines #L154 - L156 were not covered by tests
// 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

Check warning on line 162 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L161-L162

Added lines #L161 - L162 were not covered by tests
}

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

Check warning on line 168 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L165-L168

Added lines #L165 - L168 were not covered by tests
}

return pass, nil

Check warning on line 171 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L171

Added line #L171 was not covered by tests
}

reEnteredPass, err := input.GetPassword("Re-enter keyring passphrase:", buf)
if err != nil {

Check warning on line 175 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L174-L175

Added lines #L174 - L175 were not covered by tests
// 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

Check warning on line 181 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L180-L181

Added lines #L180 - L181 were not covered by tests
}

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

Check warning on line 186 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L184-L186

Added lines #L184 - L186 were not covered by tests
}

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

Check warning on line 192 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L189-L192

Added lines #L189 - L192 were not covered by tests
}

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

Check warning on line 196 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L195-L196

Added lines #L195 - L196 were not covered by tests
}

return pass, nil

Check warning on line 199 in x/e2ee/keyring/keyring.go

View check run for this annotation

Codecov / codecov/patch

x/e2ee/keyring/keyring.go#L199

Added line #L199 was not covered by tests
}
}
}
yihuang marked this conversation as resolved.
Show resolved Hide resolved
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/test-go/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)
}
}
Loading