diff --git a/Makefile b/Makefile index dd00187cd..3ebc54ebe 100644 --- a/Makefile +++ b/Makefile @@ -312,6 +312,10 @@ test_all_with_json_coverage: generate_rpc_openapi ## Run all go unit tests, outp test_race: ## Identify all unit tests that may result in race conditions go test ${VERBOSE_TEST} -race ./... +.PHONY: test_app +test_app: ## Run all go app module unit tests + go test ${VERBOSE_TEST} -p=1 -count=1 ./app/... + .PHONY: test_utility test_utility: ## Run all go utility module unit tests go test ${VERBOSE_TEST} -p=1 -count=1 ./utility/... diff --git a/app/client/cli/doc/CHANGELOG.md b/app/client/doc/CHANGELOG.md similarity index 84% rename from app/client/cli/doc/CHANGELOG.md rename to app/client/doc/CHANGELOG.md index 2cb25caad..2004287cf 100644 --- a/app/client/cli/doc/CHANGELOG.md +++ b/app/client/doc/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.0.5] - 2023-02-02 + +### Added + +- Create `Keybase` interface to handle CRUD operations for `KeyPairs` with a `BadgerDB` backend +- Add logic to create, import, export, list, delete and update (passphrase) key pairs +- Add logic to sign and verify arbitrary messages +- Add unit tests for the keybase + ## [0.0.0.4] - 2023-01-10 - The `client` (i.e. CLI) no longer instantiates a `P2P` module along with a bus of optional modules. Instead, it instantiates a `client-only` `P2P` module that is disconnected from consensus and persistence. Interactions with the persistence & consensus layer happen via RPC. diff --git a/app/client/cli/doc/README.md b/app/client/doc/README.md similarity index 100% rename from app/client/cli/doc/README.md rename to app/client/doc/README.md diff --git a/app/client/cli/doc/commands/.gitkeep b/app/client/doc/commands/.gitkeep similarity index 100% rename from app/client/cli/doc/commands/.gitkeep rename to app/client/doc/commands/.gitkeep diff --git a/app/client/cli/doc/commands/client.md b/app/client/doc/commands/client.md similarity index 100% rename from app/client/cli/doc/commands/client.md rename to app/client/doc/commands/client.md diff --git a/app/client/cli/doc/commands/client_Account.md b/app/client/doc/commands/client_Account.md similarity index 100% rename from app/client/cli/doc/commands/client_Account.md rename to app/client/doc/commands/client_Account.md diff --git a/app/client/cli/doc/commands/client_Account_Send.md b/app/client/doc/commands/client_Account_Send.md similarity index 100% rename from app/client/cli/doc/commands/client_Account_Send.md rename to app/client/doc/commands/client_Account_Send.md diff --git a/app/client/cli/doc/commands/client_Application.md b/app/client/doc/commands/client_Application.md similarity index 100% rename from app/client/cli/doc/commands/client_Application.md rename to app/client/doc/commands/client_Application.md diff --git a/app/client/cli/doc/commands/client_Application_EditStake.md b/app/client/doc/commands/client_Application_EditStake.md similarity index 100% rename from app/client/cli/doc/commands/client_Application_EditStake.md rename to app/client/doc/commands/client_Application_EditStake.md diff --git a/app/client/cli/doc/commands/client_Application_Stake.md b/app/client/doc/commands/client_Application_Stake.md similarity index 100% rename from app/client/cli/doc/commands/client_Application_Stake.md rename to app/client/doc/commands/client_Application_Stake.md diff --git a/app/client/cli/doc/commands/client_Application_Unpause.md b/app/client/doc/commands/client_Application_Unpause.md similarity index 100% rename from app/client/cli/doc/commands/client_Application_Unpause.md rename to app/client/doc/commands/client_Application_Unpause.md diff --git a/app/client/cli/doc/commands/client_Application_Unstake.md b/app/client/doc/commands/client_Application_Unstake.md similarity index 100% rename from app/client/cli/doc/commands/client_Application_Unstake.md rename to app/client/doc/commands/client_Application_Unstake.md diff --git a/app/client/cli/doc/commands/client_Consensus.md b/app/client/doc/commands/client_Consensus.md similarity index 100% rename from app/client/cli/doc/commands/client_Consensus.md rename to app/client/doc/commands/client_Consensus.md diff --git a/app/client/cli/doc/commands/client_Consensus_Height.md b/app/client/doc/commands/client_Consensus_Height.md similarity index 100% rename from app/client/cli/doc/commands/client_Consensus_Height.md rename to app/client/doc/commands/client_Consensus_Height.md diff --git a/app/client/cli/doc/commands/client_Consensus_Round.md b/app/client/doc/commands/client_Consensus_Round.md similarity index 100% rename from app/client/cli/doc/commands/client_Consensus_Round.md rename to app/client/doc/commands/client_Consensus_Round.md diff --git a/app/client/cli/doc/commands/client_Consensus_State.md b/app/client/doc/commands/client_Consensus_State.md similarity index 100% rename from app/client/cli/doc/commands/client_Consensus_State.md rename to app/client/doc/commands/client_Consensus_State.md diff --git a/app/client/cli/doc/commands/client_Consensus_Step.md b/app/client/doc/commands/client_Consensus_Step.md similarity index 100% rename from app/client/cli/doc/commands/client_Consensus_Step.md rename to app/client/doc/commands/client_Consensus_Step.md diff --git a/app/client/cli/doc/commands/client_Fisherman.md b/app/client/doc/commands/client_Fisherman.md similarity index 100% rename from app/client/cli/doc/commands/client_Fisherman.md rename to app/client/doc/commands/client_Fisherman.md diff --git a/app/client/cli/doc/commands/client_Fisherman_EditStake.md b/app/client/doc/commands/client_Fisherman_EditStake.md similarity index 100% rename from app/client/cli/doc/commands/client_Fisherman_EditStake.md rename to app/client/doc/commands/client_Fisherman_EditStake.md diff --git a/app/client/cli/doc/commands/client_Fisherman_Stake.md b/app/client/doc/commands/client_Fisherman_Stake.md similarity index 100% rename from app/client/cli/doc/commands/client_Fisherman_Stake.md rename to app/client/doc/commands/client_Fisherman_Stake.md diff --git a/app/client/cli/doc/commands/client_Fisherman_Unpause.md b/app/client/doc/commands/client_Fisherman_Unpause.md similarity index 100% rename from app/client/cli/doc/commands/client_Fisherman_Unpause.md rename to app/client/doc/commands/client_Fisherman_Unpause.md diff --git a/app/client/cli/doc/commands/client_Fisherman_Unstake.md b/app/client/doc/commands/client_Fisherman_Unstake.md similarity index 100% rename from app/client/cli/doc/commands/client_Fisherman_Unstake.md rename to app/client/doc/commands/client_Fisherman_Unstake.md diff --git a/app/client/cli/doc/commands/client_Governance.md b/app/client/doc/commands/client_Governance.md similarity index 100% rename from app/client/cli/doc/commands/client_Governance.md rename to app/client/doc/commands/client_Governance.md diff --git a/app/client/cli/doc/commands/client_Governance_ChangeParameter.md b/app/client/doc/commands/client_Governance_ChangeParameter.md similarity index 100% rename from app/client/cli/doc/commands/client_Governance_ChangeParameter.md rename to app/client/doc/commands/client_Governance_ChangeParameter.md diff --git a/app/client/cli/doc/commands/client_Node.md b/app/client/doc/commands/client_Node.md similarity index 100% rename from app/client/cli/doc/commands/client_Node.md rename to app/client/doc/commands/client_Node.md diff --git a/app/client/cli/doc/commands/client_Node_EditStake.md b/app/client/doc/commands/client_Node_EditStake.md similarity index 100% rename from app/client/cli/doc/commands/client_Node_EditStake.md rename to app/client/doc/commands/client_Node_EditStake.md diff --git a/app/client/cli/doc/commands/client_Node_Stake.md b/app/client/doc/commands/client_Node_Stake.md similarity index 100% rename from app/client/cli/doc/commands/client_Node_Stake.md rename to app/client/doc/commands/client_Node_Stake.md diff --git a/app/client/cli/doc/commands/client_Node_Unpause.md b/app/client/doc/commands/client_Node_Unpause.md similarity index 100% rename from app/client/cli/doc/commands/client_Node_Unpause.md rename to app/client/doc/commands/client_Node_Unpause.md diff --git a/app/client/cli/doc/commands/client_Node_Unstake.md b/app/client/doc/commands/client_Node_Unstake.md similarity index 100% rename from app/client/cli/doc/commands/client_Node_Unstake.md rename to app/client/doc/commands/client_Node_Unstake.md diff --git a/app/client/cli/doc/commands/client_System.md b/app/client/doc/commands/client_System.md similarity index 100% rename from app/client/cli/doc/commands/client_System.md rename to app/client/doc/commands/client_System.md diff --git a/app/client/cli/doc/commands/client_System_Health.md b/app/client/doc/commands/client_System_Health.md similarity index 100% rename from app/client/cli/doc/commands/client_System_Health.md rename to app/client/doc/commands/client_System_Health.md diff --git a/app/client/cli/doc/commands/client_System_Version.md b/app/client/doc/commands/client_System_Version.md similarity index 100% rename from app/client/cli/doc/commands/client_System_Version.md rename to app/client/doc/commands/client_System_Version.md diff --git a/app/client/cli/doc/commands/client_Validator.md b/app/client/doc/commands/client_Validator.md similarity index 100% rename from app/client/cli/doc/commands/client_Validator.md rename to app/client/doc/commands/client_Validator.md diff --git a/app/client/cli/doc/commands/client_Validator_EditStake.md b/app/client/doc/commands/client_Validator_EditStake.md similarity index 100% rename from app/client/cli/doc/commands/client_Validator_EditStake.md rename to app/client/doc/commands/client_Validator_EditStake.md diff --git a/app/client/cli/doc/commands/client_Validator_Stake.md b/app/client/doc/commands/client_Validator_Stake.md similarity index 100% rename from app/client/cli/doc/commands/client_Validator_Stake.md rename to app/client/doc/commands/client_Validator_Stake.md diff --git a/app/client/cli/doc/commands/client_Validator_Unpause.md b/app/client/doc/commands/client_Validator_Unpause.md similarity index 100% rename from app/client/cli/doc/commands/client_Validator_Unpause.md rename to app/client/doc/commands/client_Validator_Unpause.md diff --git a/app/client/cli/doc/commands/client_Validator_Unstake.md b/app/client/doc/commands/client_Validator_Unstake.md similarity index 100% rename from app/client/cli/doc/commands/client_Validator_Unstake.md rename to app/client/doc/commands/client_Validator_Unstake.md diff --git a/app/client/keybase/README.md b/app/client/keybase/README.md new file mode 100644 index 000000000..68e370ad1 --- /dev/null +++ b/app/client/keybase/README.md @@ -0,0 +1,81 @@ +# Keybase + +This document is intended to outline the current Keybase implementation used by the V1 client, and is primarily focused on its design and implementation as well as testing. + +- [Backend Database](#backend-database) +- [Keybase Interface](#keybase-interface) + - [V0\<-\>V1 Interoperability](#v0-v1-interoperability) + - [Keybase Code Structure](#keybase-code-structure) +- [Makefile Testing Helper](#makefile-testing-helper) +- [KeyPair Encryption \& Armouring](#keypair-encryption--armouring) +- [TODO: Future Work](#todo-future-work) + +_TODO(#150): The current keybase has not been integrated with any CLI endpoints, and as such is only accessible through the [keybase interface](#keybase-interface)_ + +## Backend Database + +The Keybase package uses a filesystem key-value database, `BadgerDB`, as its backend to persistently store keys locally on the client machine. The DB stores the local keys encoded as `[]byte` using `encoding/gob`. + +The `KeyPair` defined in [crypto package](../../../shared/core/crypto) is the data structure that's stored in the DB. Specifically: + +- **Key**: The `[]byte` returned by the `GetAddressBytes()` function is used as the key in the key-value store. +- **Value**: The `gob` encoded struct of the entire `KeyPair`, containing both the `PublicKey` and `PrivKeyArmour` (JSON encoded, encrypted private key string), is the value. + +The Keybase DB layer exposes several functions, defined by the [Keybase interface](#keybase-interface), to fulfill CRUD operations on the DB itself and oeprate with the Keypairs. + +## Keybase Interface + +The [Keybase interface](./keybase.go) exposes the CRUD operations to operate on keys, and supports the following operations: + +- Create password protected private keys +- Export/Import string/json keypairs +- Retrieve public/private keys or keypairs +- List all keys stored +- Check keys exist in the keybase +- Update passphrase on a private key +- Message signing and verification + +### V0<->V1 Interoperability + +The `Keybase` interface supports full interoperability of key export & import between Pocket [V0](https://github.com/pokt-network/pocket-core)<->[V1](https://github.com/pokt-network/pocket). + +Any private key created in the V0 protocol can be imported into V1 via one of the following two ways: + +1. **JSON keyfile**: This method will take the JSON encoded, encrypted private key, and will import it into the V1 keybase. The `passphrase` supplied must be the same as the one use to encrypt the key in the first place or the key won't be importable. + +2. **Private Key Hex String**: This method will directly import the private key from the hex string provided and encrypt it with the passphrase provided. This enables the passphrase to be different from the original as the provided plaintext is already decrypted. + +Although key pairs are stored in the local DB using the serialized (`[]byte`) representation of the public key, the associated address can be used for accessing the record in the DB for simplicity. + +Keys can be created without a password by specifying an empty (`""`) passphrase. The private key will still be encrypted at rest but will use the empty string as the passphrase for decryption. + +### Keybase Code Structure + +```bash +app +└── client + └── keybase + ├── README.md + ├── keybase.go + ├── keybase_test.go + └── keystore.go +``` + +The interface is found in [keybase.go](./keybase.go) whereas its implementation can be found in [keystore.go](./keystore.go) + +## Makefile Testing Helper + +The unit tests for the keybase are defined in [keybase_test.go](./keybase_test.go) and can therefore be executed alongside other application specific tests by running `make test_app`. + +## KeyPair Encryption & Armouring + +The [documentation in the crypto library](../../../shared/crypto/README.md) covers all of the details related to the `KeyPair` interface, as well as `PrivateKey` encryption, armouring and unarmouring. + +The primitives and functions defined there are heavily used throughout this package. + +## TODO: Future Work + +- [ ] Improve error handling and error messages for importing keys with invalid strings/invalid JSON +- [ ] Research and implement threshold signatures and threshold keys +- [ ] Look into a fully feature signature implementation beyond trivial `[]byte` messages +- [ ] Integrate the keybase with the CLI (#150) diff --git a/app/client/keybase/keybase.go b/app/client/keybase/keybase.go new file mode 100644 index 000000000..567fd2e7f --- /dev/null +++ b/app/client/keybase/keybase.go @@ -0,0 +1,37 @@ +package keybase + +import "github.com/pokt-network/pocket/shared/crypto" + +// Keybase interface implements the CRUD operations for the keybase +type Keybase interface { + // Close the DB connection + Stop() error + + // Create new keypair entry in DB + Create(passphrase, hint string) error + // Insert a new keypair from the private key hex string provided into the DB + ImportFromString(privStr, passphrase, hint string) error + // Insert a new keypair from the JSON string of the encrypted private key into the DB + ImportFromJSON(jsonStr, passphrase string) error + + // Accessors + Get(address string) (crypto.KeyPair, error) + GetPubKey(address string) (crypto.PublicKey, error) + GetPrivKey(address, passphrase string) (crypto.PrivateKey, error) + GetAll() (addresses []string, keyPairs []crypto.KeyPair, err error) + Exists(address string) (bool, error) + + // Exporters + ExportPrivString(address, passphrase string) (string, error) + ExportPrivJSON(address, passphrase string) (string, error) + + // Updator + UpdatePassphrase(address, oldPassphrase, newPassphrase, hint string) error + + // Sign Messages + Sign(address, passphrase string, msg []byte) ([]byte, error) + Verify(address string, msg, sig []byte) (bool, error) + + // Removals + Delete(address, passphrase string) error +} diff --git a/app/client/keybase/keybase_test.go b/app/client/keybase/keybase_test.go new file mode 100644 index 000000000..aafbf0865 --- /dev/null +++ b/app/client/keybase/keybase_test.go @@ -0,0 +1,473 @@ +package keybase + +import ( + "encoding/hex" + "testing" + + "github.com/pokt-network/pocket/runtime/test_artifacts/keygenerator" + "github.com/pokt-network/pocket/shared/crypto" + "github.com/stretchr/testify/require" +) + +//nolint:gosec // G101 Credentials are for tests +const ( + // Example account + testPrivString = "045e8380086abc6f6e941d6fe47ca93b86723bc246ec8c4beee411b410028675ed78c49592f836f7a4d47d4fb6a0e6b19f07aebc201d005f6b2c6afe389086e9" + testPubString = "ed78c49592f836f7a4d47d4fb6a0e6b19f07aebc201d005f6b2c6afe389086e9" + testAddr = "26e16ccab7a898400022476332e2972b8199f2f9" + + // Other + testPassphrase = "Testing@Testing123" + testNewPassphrase = "321gnitsetgnitset" + testHint = "testing" + testTx = "79fca587bbcfd5da86d73e1d849769017b1c91cc8177dec0fc0e3e0d345f2b35" + + // JSON account + testJSONAddr = "572f306e2d29cb8d77c02ebed7d11a5750c815f2" + testJSONPubString = "408bec6320b540aa0cc86b3e633e214f2fd4dce4caa08f164fa3a9d3e577b46c" + testJSONPrivString = "3554119cec1c0c8c5b3845a5d3fc6346eb44ed21aab5c063ae9b6b1d38bec275408bec6320b540aa0cc86b3e633e214f2fd4dce4caa08f164fa3a9d3e577b46c" + testJSONString = `{"kdf":"scrypt","salt":"197d2754445a7e5ce3e6c8d7b1d0ff6f","secparam":"12","hint":"pocket wallet","ciphertext":"B/AORJrSeQrR5ewQGel4FeCCXscoCsMUzq9gXAAxDqjXMmMxa7TedBTuemtO82JyTCoQWFHbGxRx8A7IoETNh5T5yBAjNNrr7DDkVrcfSAM3ez9lQem17DsfowCvRtmbesDlvbSZMRy8mQgClLqWRN+c6W/fPQ/lxLUy1G1A965U/uImcMXzSwbfqYrBPEux"}` +) + +func TestKeybase_CreateNewKey(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + err := db.Create(testPassphrase, testHint) + require.NoError(t, err) + + addresses, keypairs, err := db.GetAll() + require.NoError(t, err) + require.Equal(t, len(addresses), 1) + require.Equal(t, len(keypairs), 1) + + addr := addresses[0] + kp := keypairs[0] + require.Equal(t, len(kp.GetAddressBytes()), crypto.AddressLen) + require.Equal(t, addr, kp.GetAddressString()) +} + +func TestKeybase_CreateNewKeyNoPassphrase(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + err := db.Create("", "") + require.NoError(t, err) + + addresses, keypairs, err := db.GetAll() + require.NoError(t, err) + require.Equal(t, len(addresses), 1) + require.Equal(t, len(keypairs), 1) + + addr := addresses[0] + kp := keypairs[0] + require.Equal(t, len(kp.GetAddressBytes()), crypto.AddressLen) + require.Equal(t, addr, kp.GetAddressString()) +} + +func TestKeybase_ImportKeyFromString(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + err := db.ImportFromString(testPrivString, testPassphrase, testHint) + require.NoError(t, err) + + addresses, keypairs, err := db.GetAll() + require.NoError(t, err) + require.Equal(t, len(addresses), 1) + require.Equal(t, len(keypairs), 1) + + addr := addresses[0] + kp := keypairs[0] + require.Equal(t, len(kp.GetAddressBytes()), crypto.AddressLen) + require.Equal(t, addr, kp.GetAddressString()) + require.Equal(t, kp.GetAddressString(), testAddr) + require.Equal(t, kp.GetPublicKey().String(), testPubString) + + privKey, err := kp.Unarmour(testPassphrase) + require.NoError(t, err) + require.Equal(t, privKey.String(), testPrivString) +} + +func TestKeybase_ImportKeyFromStringNoPassphrase(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + err := db.ImportFromString(testPrivString, "", "") + require.NoError(t, err) + + addresses, keypairs, err := db.GetAll() + require.NoError(t, err) + require.Equal(t, len(addresses), 1) + require.Equal(t, len(keypairs), 1) + + addr := addresses[0] + kp := keypairs[0] + require.Equal(t, len(kp.GetAddressBytes()), crypto.AddressLen) + require.Equal(t, addr, kp.GetAddressString()) + require.Equal(t, kp.GetAddressString(), testAddr) + require.Equal(t, kp.GetPublicKey().String(), testPubString) + + privKey, err := kp.Unarmour("") + require.NoError(t, err) + require.Equal(t, privKey.String(), testPrivString) +} + +// TODO: Improve this test/create functions to check string validity +func TestKeybase_ImportKeyFromStringInvalidString(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + falseAddr := testKey.String() + "aa" + falseBz, err := hex.DecodeString(falseAddr) + require.NoError(t, err) + + err = db.ImportFromString(falseAddr, testPassphrase, testHint) + require.EqualError(t, err, crypto.ErrInvalidPrivateKeyLen(len(falseBz)).Error()) +} + +func TestKeybase_ImportKeyFromJSON(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + err := db.ImportFromJSON(testJSONString, testPassphrase) + require.NoError(t, err) + + addresses, keypairs, err := db.GetAll() + require.NoError(t, err) + require.Equal(t, len(addresses), 1) + require.Equal(t, len(keypairs), 1) + + addr := addresses[0] + kp := keypairs[0] + require.Equal(t, len(kp.GetAddressBytes()), crypto.AddressLen) + require.Equal(t, addr, kp.GetAddressString()) + require.Equal(t, kp.GetAddressString(), testJSONAddr) + require.Equal(t, kp.GetPublicKey().String(), testJSONPubString) + + privKey, err := kp.Unarmour(testPassphrase) + require.NoError(t, err) + require.Equal(t, privKey.String(), testJSONPrivString) +} + +func TestKeybase_GetKey(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + kp, err := db.Get(testKey.Address().String()) + require.NoError(t, err) + require.Equal(t, testKey.Address().Bytes(), kp.GetAddressBytes()) + require.Equal(t, kp.GetAddressString(), testKey.Address().String()) + + privKey, err := kp.Unarmour(testPassphrase) + require.NoError(t, err) + + equal := privKey.Equals(testKey) + require.Equal(t, equal, true) + require.Equal(t, privKey.String(), testKey.String()) +} + +func TestKeybase_GetKeyDoesntExist(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + kp, err := db.Get(testKey.Address().String()) + require.EqualError(t, err, ErrorAddrNotFound(testKey.Address().String()).Error()) + require.Equal(t, kp, nil) +} + +func TestKeybase_CheckKeyExists(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + exists, err := db.Exists(testKey.Address().String()) + require.NoError(t, err) + require.Equal(t, exists, true) +} + +func TestKeybase_CheckKeyExistsDoesntExist(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + exists, err := db.Exists(testKey.Address().String()) + require.EqualError(t, err, ErrorAddrNotFound(testKey.Address().String()).Error()) + require.Equal(t, exists, false) +} + +func TestKeybase_GetAllKeys(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + pkm := make(map[string]crypto.PrivateKey, 0) + pks := createTestKeys(t, 5) + for i := 0; i < 5; i++ { + err := db.ImportFromString(pks[i].String(), testPassphrase, testHint) + require.NoError(t, err) + pkm[pks[i].Address().String()] = pks[i] + } + + addresses, keypairs, err := db.GetAll() + require.NoError(t, err) + require.Equal(t, len(keypairs), 5) + + for i := 0; i < 5; i++ { + privKey, err := keypairs[i].Unarmour(testPassphrase) + require.NoError(t, err) + + require.Equal(t, addresses[i], keypairs[i].GetAddressString()) + require.Equal(t, addresses[i], privKey.Address().String()) + + equal := privKey.Equals(pkm[privKey.Address().String()]) + require.Equal(t, equal, true) + } +} + +func TestKeybase_GetPubKey(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + pubKey, err := db.GetPubKey(testKey.Address().String()) + require.NoError(t, err) + require.Equal(t, testKey.Address().Bytes(), pubKey.Address().Bytes()) + require.Equal(t, pubKey.Address().String(), testKey.Address().String()) + + equal := pubKey.Equals(testKey.PublicKey()) + require.Equal(t, equal, true) +} + +func TestKeybase_GetPrivKey(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + privKey, err := db.GetPrivKey(testKey.Address().String(), testPassphrase) + require.NoError(t, err) + require.Equal(t, testKey.Address().Bytes(), privKey.Address().Bytes()) + require.Equal(t, privKey.Address().String(), testKey.Address().String()) + + equal := privKey.Equals(testKey) + require.Equal(t, equal, true) + require.Equal(t, privKey.String(), testKey.String()) +} + +func TestKeybase_GetPrivKeyWrongPassphrase(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + privKey, err := db.GetPrivKey(testKey.Address().String(), testNewPassphrase) + require.Equal(t, err, crypto.ErrorWrongPassphrase) + require.Nil(t, privKey) +} + +func TestKeybase_UpdatePassphrase(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + _, err = db.GetPrivKey(testKey.Address().String(), testPassphrase) + require.NoError(t, err) + + err = db.UpdatePassphrase(testKey.Address().String(), testPassphrase, testNewPassphrase, testHint) + require.NoError(t, err) + + privKey, err := db.GetPrivKey(testKey.Address().String(), testNewPassphrase) + require.NoError(t, err) + require.Equal(t, testKey.Address().Bytes(), privKey.Address().Bytes()) + require.Equal(t, privKey.Address().String(), testKey.Address().String()) + + equal := privKey.Equals(testKey) + require.Equal(t, equal, true) + require.Equal(t, privKey.String(), testKey.String()) +} + +func TestKeybase_UpdatePassphraseWrongPassphrase(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + _, err = db.GetPrivKey(testKey.Address().String(), testPassphrase) + require.NoError(t, err) + + err = db.UpdatePassphrase(testKey.Address().String(), testNewPassphrase, testNewPassphrase, testHint) + require.ErrorIs(t, err, crypto.ErrorWrongPassphrase) +} + +func TestKeybase_DeleteKey(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + _, err = db.GetPrivKey(testKey.Address().String(), testPassphrase) + require.NoError(t, err) + + err = db.Delete(testKey.Address().String(), testPassphrase) + require.NoError(t, err) + + kp, err := db.Get(testKey.Address().String()) + require.EqualError(t, err, ErrorAddrNotFound(testKey.Address().String()).Error()) + require.Equal(t, kp, nil) +} + +func TestKeybase_DeleteKeyWrongPassphrase(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + testKey := createTestKeys(t, 1)[0] + + err := db.ImportFromString(testKey.String(), testPassphrase, testHint) + require.NoError(t, err) + + _, err = db.GetPrivKey(testKey.Address().String(), testPassphrase) + require.NoError(t, err) + + err = db.Delete(testKey.Address().String(), testNewPassphrase) + require.ErrorIs(t, err, crypto.ErrorWrongPassphrase) +} + +func TestKeybase_SignMessage(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + pk := createTestKeyFromString(t, testPrivString) + + err := db.ImportFromString(testPrivString, testPassphrase, testHint) + require.NoError(t, err) + + privKey, err := db.GetPrivKey(pk.Address().String(), testPassphrase) + require.NoError(t, err) + + txBz, err := hex.DecodeString(testTx) + require.NoError(t, err) + + signedMsg, err := db.Sign(privKey.Address().String(), testPassphrase, txBz) + require.NoError(t, err) + + verified, err := db.Verify(privKey.Address().String(), txBz, signedMsg) + require.NoError(t, err) + require.Equal(t, verified, true) +} + +func TestKeybase_SignMessageWrongPassphrase(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + pk := createTestKeyFromString(t, testPrivString) + + err := db.ImportFromString(testPrivString, testPassphrase, testHint) + require.NoError(t, err) + + privKey, err := db.GetPrivKey(pk.Address().String(), testPassphrase) + require.NoError(t, err) + + txBz, err := hex.DecodeString(testTx) + require.NoError(t, err) + + signedMsg, err := db.Sign(privKey.Address().String(), testNewPassphrase, txBz) + require.ErrorIs(t, err, crypto.ErrorWrongPassphrase) + require.Nil(t, signedMsg) +} + +func TestKeybase_ExportString(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + err := db.ImportFromString(testPrivString, testPassphrase, testHint) + require.NoError(t, err) + + privStr, err := db.ExportPrivString(testAddr, testPassphrase) + require.NoError(t, err) + require.Equal(t, privStr, testPrivString) +} + +func TestKeybase_ExportJSON(t *testing.T) { + db := initDB(t) + defer stopDB(t, db) + + err := db.ImportFromString(testPrivString, testPassphrase, testHint) + require.NoError(t, err) + + jsonStr, err := db.ExportPrivJSON(testAddr, testPassphrase) + require.NoError(t, err) + + err = db.Delete(testAddr, testPassphrase) + require.NoError(t, err) + + err = db.ImportFromJSON(jsonStr, testPassphrase) + require.NoError(t, err) + + privKey, err := db.GetPrivKey(testAddr, testPassphrase) + require.NoError(t, err) + require.Equal(t, privKey.Address().String(), testAddr) + require.Equal(t, privKey.String(), testPrivString) +} + +func initDB(t *testing.T) Keybase { + db, err := NewKeybaseInMemory() + require.NoError(t, err) + return db +} + +func createTestKeys(t *testing.T, n int) []crypto.PrivateKey { + pks := make([]crypto.PrivateKey, 0) + for i := 0; i < n; i++ { + privKeyString, _, _ := keygenerator.GetInstance().Next() + privKey, err := crypto.NewPrivateKey(privKeyString) + require.NoError(t, err) + pks = append(pks, privKey) + + } + return pks +} + +func createTestKeyFromString(t *testing.T, str string) crypto.PrivateKey { + privKey, err := crypto.NewPrivateKey(str) + require.NoError(t, err) + return privKey +} + +func stopDB(t *testing.T, db Keybase) { + err := db.Stop() + require.NoError(t, err) +} diff --git a/app/client/keybase/keystore.go b/app/client/keybase/keystore.go new file mode 100644 index 000000000..418816979 --- /dev/null +++ b/app/client/keybase/keystore.go @@ -0,0 +1,351 @@ +package keybase + +import ( + "bytes" + "encoding/hex" + "fmt" + "os" + "strings" + + "github.com/dgraph-io/badger/v3" + "github.com/pokt-network/pocket/shared/crypto" +) + +// Errors +func ErrorAddrNotFound(addr string) error { + return fmt.Errorf("No key found with address: %s", addr) +} + +// badgerKeybase implements the KeyBase interface +var _ Keybase = &badgerKeybase{} + +// badgerKeybase implements the Keybase struct using the BadgerDB backend +type badgerKeybase struct { + db *badger.DB +} + +// Creates/Opens the DB at the specified path +func NewKeybase(path string) (Keybase, error) { + pathExists, err := dirExists(path) // Creates path if it doesn't exist + if err != nil || !pathExists { + return nil, err + } + db, err := badger.Open(badgerOptions(path)) + if err != nil { + return nil, err + } + return &badgerKeybase{db: db}, nil +} + +// Creates/Opens the DB in Memory +// FOR TESTING PURPOSES ONLY +func NewKeybaseInMemory() (Keybase, error) { + db, err := badger.Open(badgerOptions("").WithInMemory(true)) + if err != nil { + return nil, err + } + return &badgerKeybase{db: db}, nil +} + +// Close the DB +func (keybase *badgerKeybase) Stop() error { + return keybase.db.Close() +} + +// Create a new key and store the serialised KeyPair encoding in the DB +// Using the PublicKey.Address() return value as the key for storage +func (keybase *badgerKeybase) Create(passphrase, hint string) error { + err := keybase.db.Update(func(tx *badger.Txn) error { + keyPair, err := crypto.CreateNewKey(passphrase, hint) + if err != nil { + return err + } + + // Use key address as key in DB + addrKey := keyPair.GetAddressBytes() + + // Encode KeyPair into []byte for value + keypairBz, err := keyPair.Marshal() + if err != nil { + return err + } + + return tx.Set(addrKey, keypairBz) + }) + + return err +} + +// Create a new KeyPair from the private key hex string and store the serialised KeyPair encoding in the DB +// Using the PublicKey.Address() return value as the key for storage +func (keybase *badgerKeybase) ImportFromString(privKeyHex, passphrase, hint string) error { + err := keybase.db.Update(func(tx *badger.Txn) error { + keyPair, err := crypto.CreateNewKeyFromString(privKeyHex, passphrase, hint) + if err != nil { + return err + } + + // Use key address as key in DB + addrKey := keyPair.GetAddressBytes() + + // Encode KeyPair into []byte for value + keypairBz, err := keyPair.Marshal() + if err != nil { + return err + } + + return tx.Set(addrKey, keypairBz) + }) + + return err +} + +// Create a new KeyPair from the private key JSON string and store the serialised KeyPair encoding in the DB +// Using the PublicKey.Address() return value as the key for storage +func (keybase *badgerKeybase) ImportFromJSON(jsonStr, passphrase string) error { + err := keybase.db.Update(func(tx *badger.Txn) error { + keyPair, err := crypto.ImportKeyFromJSON(jsonStr, passphrase) + if err != nil { + return err + } + + // Use key address as key in DB + addrKey := keyPair.GetAddressBytes() + + // Encode KeyPair into []byte for value + keypairBz, err := keyPair.Marshal() + if err != nil { + return err + } + + return tx.Set(addrKey, keypairBz) + }) + + return err +} + +// Returns a KeyPair struct provided the address was found in the DB +func (keybase *badgerKeybase) Get(address string) (crypto.KeyPair, error) { + kp := crypto.GetKeypair() + addrBz, err := hex.DecodeString(address) + if err != nil { + return nil, err + } + + err = keybase.db.View(func(tx *badger.Txn) error { + item, err := tx.Get(addrBz) + if err != nil && strings.Contains(err.Error(), "not found") { + return ErrorAddrNotFound(address) + } else if err != nil { + return err + } + + value, err := item.ValueCopy(nil) + if err != nil { + return err + } + + // Decode []byte value back into KeyPair + if err := kp.Unmarshal(value); err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, err + } + + return kp, nil +} + +// Returns a PublicKey interface provided the address was found in the DB +func (keybase *badgerKeybase) GetPubKey(address string) (crypto.PublicKey, error) { + kp, err := keybase.Get(address) + if err != nil { + return nil, err + } + + return kp.GetPublicKey(), nil +} + +// Returns a PrivateKey interface provided the address was found in the DB and the passphrase was correct +func (keybase *badgerKeybase) GetPrivKey(address, passphrase string) (crypto.PrivateKey, error) { + kp, err := keybase.Get(address) + if err != nil { + return nil, err + } + + privKey, err := kp.Unarmour(passphrase) + if err != nil { + return nil, err + } + + return privKey, nil +} + +// Get all the addresses and key pairs stored in the keybase +// Returns addresses stored and all the KeyPair structs stored in the DB +func (keybase *badgerKeybase) GetAll() (addresses []string, keyPairs []crypto.KeyPair, err error) { + // View executes the function provided managing a read only transaction + err = keybase.db.View(func(tx *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchSize = 5 + it := tx.NewIterator(opts) + defer it.Close() + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + err := item.Value(func(val []byte) error { + b := make([]byte, len(val)) + copy(b, val) + + // Decode []byte value back into KeyPair + kp := crypto.GetKeypair() + if err := kp.Unmarshal(b); err != nil { + return err + } + + addresses = append(addresses, kp.GetAddressString()) + keyPairs = append(keyPairs, kp) + return nil + }) + if err != nil { + return err + } + } + return nil + }) + + if err != nil { + return nil, nil, err + } + + return addresses, keyPairs, nil +} + +// Check whether an address is currently stored in the DB +func (keybase *badgerKeybase) Exists(address string) (bool, error) { + val, err := keybase.Get(address) + if err != nil { + return false, err + } + return val != nil, nil +} + +// Export the Private Key string of the given address +func (keybase *badgerKeybase) ExportPrivString(address, passphrase string) (string, error) { + kp, err := keybase.Get(address) + if err != nil { + return "", err + } + return kp.ExportString(passphrase) +} + +// Export the Private Key of the given address as a JSON object +func (keybase *badgerKeybase) ExportPrivJSON(address, passphrase string) (string, error) { + kp, err := keybase.Get(address) + if err != nil { + return "", err + } + return kp.ExportJSON(passphrase) +} + +func (keybase *badgerKeybase) UpdatePassphrase(address, oldPassphrase, newPassphrase, hint string) error { + // Check the oldPassphrase is correct + privKey, err := keybase.GetPrivKey(address, oldPassphrase) + if err != nil { + return err + } + privStr := privKey.String() + + addrBz, err := hex.DecodeString(address) + if err != nil { + return err + } + + err = keybase.db.Update(func(tx *badger.Txn) error { + keyPair, err := crypto.CreateNewKeyFromString(privStr, newPassphrase, hint) + if err != nil { + return err + } + + // Use key address as key in DB + addrKey := keyPair.GetAddressBytes() + if !bytes.Equal(addrKey, addrBz) { + return fmt.Errorf("Key address does not match previous address.") + } + + // Encode KeyPair into []byte for value + keypairBz, err := keyPair.Marshal() + if err != nil { + return err + } + + return tx.Set(addrKey, keypairBz) + }) + + return err +} + +// Sign a message using the key address provided +func (keybase *badgerKeybase) Sign(address, passphrase string, msg []byte) ([]byte, error) { + privKey, err := keybase.GetPrivKey(address, passphrase) + if err != nil { + return nil, err + } + return privKey.Sign(msg) +} + +// Verify a message has been signed correctly +func (keybase *badgerKeybase) Verify(address string, msg, sig []byte) (bool, error) { + kp, err := keybase.Get(address) + if err != nil { + return false, err + } + pubKey := kp.GetPublicKey() + return pubKey.Verify(msg, sig), nil +} + +// Remove a KeyPair from the DB given the address +func (keybase *badgerKeybase) Delete(address, passphrase string) error { + if _, err := keybase.GetPrivKey(address, passphrase); err != nil { + return err + } + + addrBz, err := hex.DecodeString(address) + if err != nil { + return err + } + + err = keybase.db.Update(func(tx *badger.Txn) error { + return tx.Delete(addrBz) + }) + return err +} + +// Return badger.Options for the given DB path - disable logging +func badgerOptions(path string) badger.Options { + opts := badger.DefaultOptions(path) + opts.Logger = nil // Badger logger is very noisy + return opts +} + +// Check directory exists and creates path if it doesn't exist +func dirExists(path string) (bool, error) { + stat, err := os.Stat(path) + if err == nil { + // Exists but not directory + if !stat.IsDir() { + return false, fmt.Errorf("Keybase path is not a directory: %s", path) + } + return true, nil + } + if os.IsNotExist(err) { + // Create directories in path recursively + if err := os.MkdirAll(path, os.ModePerm); err != nil { + return false, fmt.Errorf("Error creating directory at path: %s, (%v)", path, err.Error()) + } + return true, nil + } + return false, err +} diff --git a/go.mod b/go.mod index c389c8cb7..7d6b4f985 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/mapstructure v1.5.0 github.com/quasilyte/go-ruleguard/dsl v0.3.21 - github.com/rs/zerolog v1.15.0 + github.com/rs/zerolog v1.27.0 github.com/spf13/cobra v1.6.0 github.com/spf13/viper v1.13.0 ) @@ -50,7 +50,6 @@ require ( github.com/golang/protobuf v1.5.2 github.com/golang/snappy v0.0.4 // indirect github.com/google/flatbuffers v22.9.29+incompatible // indirect - github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect github.com/klauspost/compress v1.15.11 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect @@ -60,24 +59,26 @@ require ( github.com/sirupsen/logrus v1.9.0 // indirect go.opencensus.io v0.23.0 // indirect golang.org/x/net v0.2.0 // indirect - gotest.tools v2.2.0+incompatible // indirect ) require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/gotestyourself/gotestyourself v2.2.0+incompatible // indirect github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/jackc/puddle/v2 v2.1.2 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/lib/pq v1.10.2 // indirect + github.com/lib/pq v1.10.6 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/rogpeppe/go-internal v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect go.uber.org/atomic v1.10.0 // indirect golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 // indirect golang.org/x/term v0.2.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect + gotest.tools v2.2.0+incompatible // indirect ) require ( @@ -117,7 +118,7 @@ require ( golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/text v0.4.0 // indirect - golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/tools v0.1.12 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index a0a4d4bae..9cd2f2275 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,7 @@ github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= @@ -322,8 +323,8 @@ github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3 github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= +github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= @@ -336,6 +337,7 @@ github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYt github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -377,6 +379,7 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -412,11 +415,14 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua github.com/quasilyte/go-ruleguard/dsl v0.3.21 h1:vNkC6fC6qMLzCOGbnIHOd5ixUGgTbp3Z4fGnUgULlDA= github.com/quasilyte/go-ruleguard/dsl v0.3.21/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= +github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= +github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= +github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -708,8 +714,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE= -golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/shared/CHANGELOG.md b/shared/CHANGELOG.md index b4157de8d..b6c0252b0 100644 --- a/shared/CHANGELOG.md +++ b/shared/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.0.0.19] - 2023-02-02 + +- Add `KeyPair` interface +- Add logic to create new keypairs, encrypt/armour them and decrypt/unarmour them + ## [0.0.0.18] - 2023-01-31 - Match naming conventions in `Param` protobuf file diff --git a/shared/crypto/README.md b/shared/crypto/README.md new file mode 100644 index 000000000..a7a1da699 --- /dev/null +++ b/shared/crypto/README.md @@ -0,0 +1,105 @@ +# Pocket Crypto + +- [KeyPair Interface](#keypair-interface) + - [KeyPair Code Structure](#keypair-code-structure) +- [Encryption and Armouring](#encryption-and-armouring) + +_DOCUMENT: Note that this README is a WIP and does not exhaustively document all the current types in this package_ + +## KeyPair Interface + +The [KeyPair interface](./keypair.go) exposes methods related to operating on `PublicKey` types and `PrivKeyArmour` strings, such as: + +- Retrieve the PublicKey or armoured PrivateKey JSON string +- Get PublicKey address `[]byte` or hex `string` +- Unarmour the PrivateKey JSON string +- Export the PrivateKey hex string or JSON as an armoured string +- Marshal or unmarshal the KeyPair to/from a `[]byte` + +The [KeyPair](./keypair.go) interface is implemented by the `encKeyPair` struct which stores: + +1. `PublicKey` of the KeyPair +2. `PrivateKey` armoured JSON string + +The PrivateKey armoured JSON string is created after the [encryption step](#encryption-and-armouring) has encrypted the PrivateKey and marshalled it into a JSON string. + +### KeyPair Code Structure + +The KeyPair code is separated into two files: [keypair.go](./keypair.go) and [armour.go](./armour.go) + +```bash +shared +└── crypto + ├── armour.go + └── keypair.go +``` + +## Encryption and Armouring + +The passphrase provided or `""` (default) is used for encrypting and armouring new or imported keys. + +The following flowchart shows this process: + +```mermaid +flowchart LR + subgraph C[core lib] + A["rand([16]byte)"] + end + subgraph S[scrypt lib] + B["key(salt, pass, ...)"] + end + subgraph AES-GCM + direction TB + D["Cipher(key)"] + E["GCM(block)"] + F["Seal(plaintext, nonce)"] + D--Block-->E + E--Nonce-->F + end + subgraph Armour + direction LR + G["base64Encode(encryptedPrivateKey)"] + H["hexEncode(Salt)"] + G --> armoured + H --> armoured + end + C--Salt-->S + S--Key-->AES-GCM + AES-GCM--encryptedPrivateKey-->Armour + C--Salt-->Armour + kdf --> Armour + hint --> Armour + Armour--Marshal-->Return(encryptedArmouredPrivateKey) +``` + +The process above is reversed when unarmouring and decrypting a key in the keybase: + +```mermaid +flowchart LR + subgraph U[Unarmour] + armoured + B["hexDecode(salt)"] + C["base64Decode(cipherText)"] + D["verify"] + armoured--salt-->B + armoured--cipherText-->C + armoured--kdf-->D + + end + subgraph S[scrypt lib] + E["key(salt, pass, ...)"] + end + subgraph AES-GCM + direction TB + F["Cipher(key)"] + G["GCM(block)"] + H["Open(encryptedBytes, nonce)"] + F--Block-->G + G--Nonce-->H + end + encryptedArmouredPrivateKey --Unmarshal--> U + B--Salt-->S + C--encryptedBytes-->AES-GCM + S--Key-->AES-GCM + AES-GCM-->PrivateKey +``` diff --git a/shared/crypto/armour.go b/shared/crypto/armour.go new file mode 100644 index 000000000..5adb825ad --- /dev/null +++ b/shared/crypto/armour.go @@ -0,0 +1,208 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + crand "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "golang.org/x/crypto/scrypt" +) + +const ( + // Encryption params + kdf = "scrypt" + randBz = 16 + AESNonceSize = 12 + // Scrypt params + n = 32768 // CPU/memory cost param; power of 2 greater than 1 + r = 8 // r * p < 2³⁰ + p = 1 // r * p < 2³⁰ + klen = 32 // bytes + // Sec param + secParam = 12 +) + +// Errors +var ( + ErrorWrongPassphrase = errors.New("Can't decrypt private key: wrong passphrase") +) + +// Armoured Private Key struct with fields to unarmour it later +type armouredKey struct { + Kdf string `json:"kdf"` + Salt string `json:"salt"` + SecParam string `json:"secparam"` + Hint string `json:"hint"` + CipherText string `json:"ciphertext"` +} + +// Generate new armoured private key struct with parameters for unarmouring +func newArmouredKey(kdf, salt, hint, cipherText string) armouredKey { + return armouredKey{ + Kdf: kdf, + Salt: salt, + SecParam: strconv.Itoa(secParam), + Hint: hint, + CipherText: cipherText, + } +} + +// Encrypt the given privKey with the passphrase, armour it by encoding the encrypted +// []byte into base64, and convert into a json string with the parameters for unarmouring +func encryptArmourPrivKey(privKey PrivateKey, passphrase, hint string) (string, error) { + // Encrypt privKey usign AES-256 GCM Cipher + saltBz, encBz, err := encryptPrivKey(privKey, passphrase) + if err != nil { + return "", err + } + + // Armour encrypted bytes by encoding into Base64 + armourStr := base64.RawStdEncoding.EncodeToString(encBz) + + // Create ArmouredKey object so can unarmour later + armoured := newArmouredKey(kdf, hex.EncodeToString(saltBz), hint, armourStr) + + // Encode armoured struct into []byte + js, err := json.Marshal(armoured) + if err != nil { + return "", err + } + + return string(js), nil +} + +// Encrypt the given privKey with the passphrase using a randomly +// generated salt and the AES-256 GCM cipher +func encryptPrivKey(privKey PrivateKey, passphrase string) (saltBz, encBz []byte, err error) { + // Get random bytes for salt + saltBz = randBytes(randBz) + + // Derive key for encryption, see: https://pkg.go.dev/golang.org/x/crypto/scrypt#Key + encryptionKey, err := scrypt.Key([]byte(passphrase), saltBz, n, r, p, klen) + if err != nil { + return nil, nil, err + } + + // Encrypt using AES + privKeyHexString := privKey.String() + encBz, err = encryptAESGCM(encryptionKey, []byte(privKeyHexString)) + if err != nil { + return nil, nil, err + } + + return saltBz, encBz, nil +} + +// Unarmor and decrypt the private key using the passphrase provided +func unarmourDecryptPrivKey(armourStr, passphrase string) (privKey PrivateKey, err error) { + // Decode armourStr back into ArmouredKey struct + ak := armouredKey{} + err = json.Unmarshal([]byte(armourStr), &ak) + if err != nil { + return nil, err + } + + // Check the ArmouredKey for the correct parameters on kdf and salt + if ak.Kdf != kdf { + return nil, fmt.Errorf("Unrecognized KDF type: %v", ak.Kdf) + } + if ak.Salt == "" { + return nil, fmt.Errorf("Missing salt bytes") + } + + // Decoding the salt + saltBz, err := hex.DecodeString(ak.Salt) + if err != nil { + return nil, fmt.Errorf("Error decoding salt: %v", err.Error()) + } + + // Decoding the "armoured" ciphertext stored in base64 + encBz, err := base64.RawStdEncoding.DecodeString(ak.CipherText) + if err != nil { + return nil, fmt.Errorf("Error decoding ciphertext: %v", err.Error()) + } + + // Decrypt the actual privkey with the parameters + privKey, err = decryptPrivKey(saltBz, encBz, passphrase) + + return privKey, err +} + +// Decrypt the AES-256 GCM encrypted bytes using the passphrase given +func decryptPrivKey(saltBz, encBz []byte, passphrase string) (PrivateKey, error) { + // Derive key for decryption, see: https://pkg.go.dev/golang.org/x/crypto/scrypt#Key + encryptionKey, err := scrypt.Key([]byte(passphrase), saltBz, n, r, p, klen) + if err != nil { + return nil, err + } + + // Decrypt using AES + privKeyRawHexBz, err := decryptAESGCM(encryptionKey, encBz) + if err != nil { + return nil, err + } + bz, err := hex.DecodeString(string(privKeyRawHexBz)) + if err != nil { + return nil, err + } + + // Get private key from decrypted bytes + pk, err := NewPrivateKeyFromBytes(bz) + if err != nil { + return nil, err + } + + return pk, nil +} + +// Encrypt using AES-256 GCM Cipher +func encryptAESGCM(key, plaintext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonce := key[:AESNonceSize] + encBz := gcm.Seal(nil, nonce, plaintext, nil) + return encBz, nil +} + +// Decrypt using AES-256 GCM Cipher +func decryptAESGCM(key, encBz []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + nonce := key[:AESNonceSize] + result, err := gcm.Open(nil, nonce, encBz, nil) + if err != nil && strings.Contains(err.Error(), "authentication failed") { + return nil, ErrorWrongPassphrase + } else if err != nil { + return nil, fmt.Errorf("Can't Decrypt Using AES : %s \n", err) + } + return result, nil +} + +// Use OS randomness +func randBytes(numBytes int) []byte { + b := make([]byte, numBytes) + _, err := crand.Read(b) + if err != nil { + panic(err) + } + return b +} diff --git a/shared/crypto/keypair.go b/shared/crypto/keypair.go new file mode 100644 index 000000000..ee6008e7b --- /dev/null +++ b/shared/crypto/keypair.go @@ -0,0 +1,173 @@ +package crypto + +import ( + "bytes" + "crypto/ed25519" + "encoding/gob" +) + +// Encoding is used to serialise the data to store the KeyPairs in the database +func init() { + gob.Register(Ed25519PublicKey{}) + gob.Register(ed25519.PublicKey{}) + gob.Register(encKeyPair{}) +} + +// The KeyPair interface exposes functions relating to public and encrypted private key pairs +type KeyPair interface { + // Accessors + GetPublicKey() PublicKey + GetPrivArmour() string + + // Public Key Address + GetAddressBytes() []byte + GetAddressString() string // hex string + + // Unarmour + Unarmour(passphrase string) (PrivateKey, error) + + // Export + ExportString(passphrase string) (string, error) + ExportJSON(passphrase string) (string, error) + + // Marshalling + Marshal() ([]byte, error) + Unmarshal([]byte) error +} + +var _ KeyPair = &encKeyPair{} + +// encKeyPair struct stores the public key and the passphrase encrypted private key +type encKeyPair struct { + PublicKey PublicKey + PrivKeyArmour string +} + +// Generate a new KeyPair struct given the public key and armoured private key +func newKeyPair(pub PublicKey, priv string) KeyPair { + return &encKeyPair{ + PublicKey: pub, + PrivKeyArmour: priv, + } +} + +// Return an empty KeyPair interface +func GetKeypair() KeyPair { + return &encKeyPair{} +} + +// Return the public key +func (kp encKeyPair) GetPublicKey() PublicKey { + return kp.PublicKey +} + +// Return private key armoured string +func (kp encKeyPair) GetPrivArmour() string { + return kp.PrivKeyArmour +} + +// Return the byte slice address of the public key +func (kp encKeyPair) GetAddressBytes() []byte { + return kp.PublicKey.Address().Bytes() +} + +// Return the string address of the public key +func (kp encKeyPair) GetAddressString() string { + return kp.PublicKey.Address().String() +} + +// Unarmour the private key with the passphrase provided +func (kp encKeyPair) Unarmour(passphrase string) (PrivateKey, error) { + return unarmourDecryptPrivKey(kp.PrivKeyArmour, passphrase) +} + +// Export Private Key String +func (kp encKeyPair) ExportString(passphrase string) (string, error) { + privKey, err := unarmourDecryptPrivKey(kp.PrivKeyArmour, passphrase) + if err != nil { + return "", err + } + return privKey.String(), nil +} + +// Export Private Key as armoured JSON string with fields to decrypt +func (kp encKeyPair) ExportJSON(passphrase string) (string, error) { + _, err := unarmourDecryptPrivKey(kp.PrivKeyArmour, passphrase) + if err != nil { + return "", err + } + return kp.PrivKeyArmour, nil +} + +// Marshal KeyPair into a []byte +func (kp encKeyPair) Marshal() ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(kp); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// Unmarshal []byte into an encKeyPair struct +func (kp *encKeyPair) Unmarshal(bz []byte) error { + var keyPair encKeyPair + keypairBz := new(bytes.Buffer) + keypairBz.Write(bz) + dec := gob.NewDecoder(keypairBz) + if err := dec.Decode(&keyPair); err != nil { + return err + } + *kp = keyPair + return nil +} + +// Generate new private ED25519 key and encrypt and armour it as a string +// Returns a KeyPair struct of the Public Key and Armoured String +func CreateNewKey(passphrase, hint string) (KeyPair, error) { + privKey, err := GeneratePrivateKey() + if err != nil { + return nil, err + } + + privArmour, err := encryptArmourPrivKey(privKey, passphrase, hint) + if err != nil || privArmour == "" { + return nil, err + } + + pubKey := privKey.PublicKey() + kp := newKeyPair(pubKey, privArmour) + + return kp, nil +} + +// Generate new KeyPair from the hex string provided, encrypt and armour it as a string +func CreateNewKeyFromString(privStrHex, passphrase, hint string) (KeyPair, error) { + privKey, err := NewPrivateKey(privStrHex) + if err != nil { + return nil, err + } + + privArmour, err := encryptArmourPrivKey(privKey, passphrase, hint) + if err != nil || privArmour == "" { + return nil, err + } + + pubKey := privKey.PublicKey() + kp := newKeyPair(pubKey, privArmour) + + return kp, nil +} + +// Create new KeyPair from the JSON encoded privStr +func ImportKeyFromJSON(jsonStr, passphrase string) (KeyPair, error) { + // Get Private Key from armouredStr + privKey, err := unarmourDecryptPrivKey(jsonStr, passphrase) + if err != nil { + return nil, err + } + pubKey := privKey.PublicKey() + kp := newKeyPair(pubKey, jsonStr) + + return kp, nil +}