-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Problem: no keyring interface for e2ee to store arbitrary payload (#1413
) changelo add age encrypt/decrypt in unit test Update x/e2ee/keyring/keyring.go Signed-off-by: yihuang <[email protected]> fix lint
- Loading branch information
Showing
5 changed files
with
256 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |