diff --git a/.github/workflows/build-crypto-provider.yml b/.github/workflows/build-crypto-provider.yml new file mode 100644 index 0000000..f931022 --- /dev/null +++ b/.github/workflows/build-crypto-provider.yml @@ -0,0 +1,24 @@ +name: Build Crypto-Provider +on: + pull_request: + push: + branches: + - main + - release/** +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }}-build-crypto-provider + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.23.0" + - name: Build + run: make \ No newline at end of file diff --git a/.gitignore b/.gitignore index 723ef36..80a85b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -.idea \ No newline at end of file +.idea +.vscode +build/* +crypto-provider/build/* \ No newline at end of file diff --git a/crypto-provider/LICENSE b/crypto-provider/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/crypto-provider/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/crypto-provider/Makefile b/crypto-provider/Makefile new file mode 100644 index 0000000..498779b --- /dev/null +++ b/crypto-provider/Makefile @@ -0,0 +1,42 @@ +WALLET_BIN := build/wallet +golangci_version=v1.61.0 +golangci_installed_version=$(shell golangci-lint version --format short 2>/dev/null) + +.PHONY: demo run clean + +.DEFAULT_GOAL := build-wallet + +# Build the wallet command +build-wallet: + go build -o $(WALLET_BIN) ./cmd + +# Run the demo +demo: + cd demo; \ + go run main.go + +# Clean built binaries +clean: + rm -f $(WALLET_BIN) + +# Install golangci-lint +lint-install: + @echo "--> Checking golangci-lint installation" + @if [ "$(golangci_installed_version)" != "$(golangci_version)" ]; then \ + echo "--> Installing golangci-lint $(golangci_version)"; \ + go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(golangci_version); \ + fi + +# Run golangci-lint +lint: + @echo "--> Running linter" + $(MAKE) lint-install + @golangci-lint run --timeout=15m + +# Run golangci-lint and fix +lint-fix: + @echo "--> Running linter with fix" + $(MAKE) lint-install + @golangci-lint run --fix + +.PHONY: lint lint-fix lint-install \ No newline at end of file diff --git a/crypto-provider/README.md b/crypto-provider/README.md new file mode 100644 index 0000000..d49d60b --- /dev/null +++ b/crypto-provider/README.md @@ -0,0 +1,55 @@ +# crypto-provider + +

+ Crypto Provider Logo +

+ +## Overview + +This is a Proof of Concept of the crypto-providers ADR as described in the [ADR-001 Crypto Provider](https://github.com/cosmos/crypto/blob/main/docs/architecture/adr-001-crypto-provider.md). + +## Main Components + +The main components of this project are located in the `components` package. They include: + +- **CryptoProvider**: Aggregates functionalities of signing, verifying, and hashing, and provides metadata. +- **CryptoProviderFactory**: A factory interface for creating CryptoProviders. +- **BuildSource**: Various implementations for building CryptoProviders from different sources. +- **ProviderMetadata**: Metadata structure for the crypto provider. +- **Signer, Verifier, Hasher**: Interfaces for signing, verifying, and hashing data. +- **AddressFormatter**: Interface for formatting addresses from public key bytes. + +## Running the Demo App + +To run the demo app, just type the following command: + +```bash +make demo +``` + +### What the Demo Does + +The demo application performs the following steps: + +1. Creates a new wallet using an in-memory keyring. +2. Loads a JSON file and creates a new crypto provider from it. +3. Retrieves a signer from the selected provider. +4. Generates random data and signs it using the signer. +5. Retrieves a verifier from the provider and verifies the generated signature. + +### Demo Architecture + +```mermaid +graph TD + A[Demo Application] --> B[Wallet] + B --> C[Keyring] + B --> D[CryptoProviderFactory] + B --> Q[SimpleAddressFormatter] + D --> E[FileProviderFactory] + E --> F[FileProvider] + F --> G[Signer] + F --> H[Verifier] + F --> I[Hasher] + F --> O[ProviderMetadata] + F --> P[FileProviderConfig] +``` diff --git a/crypto-provider/cmd/list.go b/crypto-provider/cmd/list.go new file mode 100644 index 0000000..49952a3 --- /dev/null +++ b/crypto-provider/cmd/list.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "github.com/spf13/cobra" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List all providers", + Long: `This command lists all the crypto providers currently available in the wallet.`, + RunE: func(cmd *cobra.Command, args []string) error { + w, err := setup() + if err != nil { + return err + } + + providers, err := w.ListProviders() + if err != nil { + return fmt.Errorf("failed to list providers: %v", err) + } + + if len(providers) == 0 { + fmt.Println("No providers found.") + } else { + fmt.Println("Available providers:") + for _, provider := range providers { + fmt.Printf("- %s\n", provider) + } + } + + return nil + }, +} diff --git a/crypto-provider/cmd/register/register.go b/crypto-provider/cmd/register/register.go new file mode 100644 index 0000000..55c23fe --- /dev/null +++ b/crypto-provider/cmd/register/register.go @@ -0,0 +1,15 @@ +package register + +import ( + // Import all provider packages here + "github.com/cosmos/crypto-provider/pkg/factory" + _ "github.com/cosmos/crypto-provider/pkg/impl/file" + _ "github.com/cosmos/crypto-provider/pkg/impl/file/cmd" + // Add other providers as needed + // _ "github.com/cosmos/crypto-provider/pkg/impl/someprovider" +) + +// Init is a dummy function to ensure this package is imported +func Init() { + _ = factory.GetGlobalFactory() +} diff --git a/crypto-provider/cmd/root.go b/crypto-provider/cmd/root.go new file mode 100644 index 0000000..f8ba518 --- /dev/null +++ b/crypto-provider/cmd/root.go @@ -0,0 +1,49 @@ +package main + +import ( + "fmt" + "github.com/cosmos/crypto-provider/pkg/cli" + "github.com/cosmos/crypto-provider/pkg/keyring" + "github.com/cosmos/crypto-provider/pkg/wallet" + "os" + + "github.com/spf13/cobra" +) + +var ( + flags struct { + providersDir string + } +) + +// SimpleAddressFormatter implementation +type SimpleAddressFormatter struct{} + +func (f SimpleAddressFormatter) FormatAddress(pubKey []byte) (string, error) { + return fmt.Sprintf("addr_%x", pubKey[:8]), nil +} + +func setup() (wallet.Wallet, error) { + addressFormatter := SimpleAddressFormatter{} + w, err := wallet.NewKeyringWallet("wallet-app", keyring.BackendMemory, flags.providersDir, addressFormatter) + if err != nil { + return nil, fmt.Errorf("failed to create wallet: %v", err) + } + return w, nil +} + +func initFlags(rootCmd *cobra.Command) { + rootCmd.PersistentFlags().StringVar(&flags.providersDir, "providers-dir", "", "Directory containing provider configurations") + _ = rootCmd.MarkPersistentFlagRequired("providers-dir") +} + +func main() { + rootCmd := cli.GetRootCmd() + initFlags(rootCmd) + rootCmd.AddCommand(listCmd) + + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/crypto-provider/demo/main.go b/crypto-provider/demo/main.go new file mode 100644 index 0000000..bf790f6 --- /dev/null +++ b/crypto-provider/demo/main.go @@ -0,0 +1,86 @@ +package main + +import ( + "crypto/rand" + "fmt" + "log" + "os" + "path/filepath" + + "github.com/cosmos/crypto-provider/pkg/components" + "github.com/cosmos/crypto-provider/pkg/impl/file" + "github.com/cosmos/crypto-provider/pkg/keyring" + "github.com/cosmos/crypto-provider/pkg/wallet" +) + +const TestFile = "testdata/file_1.json" + +// SimpleAddressFormatter implementation +type SimpleAddressFormatter struct{} + +func (f SimpleAddressFormatter) FormatAddress(pubKey []byte) (string, error) { + return fmt.Sprintf("addr_%x", pubKey[:8]), nil +} + +func main() { + currentDir, _ := os.Getwd() + providersDir := filepath.Join(currentDir, "testdata") + + // Step 1: Create a new wallet + addressFormatter := SimpleAddressFormatter{} + w, err := wallet.NewKeyringWallet("demo-app", keyring.BackendMemory, providersDir, addressFormatter) + if err != nil { + log.Fatalf("Failed to create wallet: %v", err) + } + + // Step 2: Load JSON file and create a new crypto provider + jsonFilePath := filepath.Join(currentDir, TestFile) + jsonData, err := os.ReadFile(jsonFilePath) + if err != nil { + log.Fatalf("Failed to read JSON file: %v", err) + } + + buildSource := components.BuildSourceJson{JsonString: string(jsonData)} + err = w.NewCryptoProvider(file.ProviderTypeFile, buildSource) + if err != nil { + log.Fatalf("Failed to create new crypto provider: %v", err) + } + + // Step 3: List providers and get the first one + providerIDs, err := w.ListProviders() + if err != nil { + log.Fatalf("Failed to list providers: %v", err) + } + if len(providerIDs) == 0 { + log.Fatalf("No providers found") + } + + provider, err := w.GetCryptoProvider(providerIDs[0]) + if err != nil { + log.Fatalf("Failed to get provider: %v", err) + } + + // Step 4: Get signer from the provider + signer := provider.GetSigner() + + // Step 5: Generate random data and sign it + randomBytes := make([]byte, 32) + _, err = rand.Read(randomBytes) + if err != nil { + log.Fatalf("Failed to generate random bytes: %v", err) + } + + signature, err := signer.Sign(randomBytes, nil) + if err != nil { + log.Fatalf("Failed to sign data: %v", err) + } + + // Step 6: Get verifier and verify the signature + verifier := provider.GetVerifier() + valid, err := verifier.Verify(signature, randomBytes, nil) + if err != nil { + log.Fatalf("Failed to verify signature: %v", err) + } + + fmt.Printf("Signature verification result: %v\n", valid) +} diff --git a/crypto-provider/demo/testdata/file_1.json b/crypto-provider/demo/testdata/file_1.json new file mode 100644 index 0000000..014ad68 --- /dev/null +++ b/crypto-provider/demo/testdata/file_1.json @@ -0,0 +1,9 @@ +{ + "Name": "Demo_File_Provider_1", + "Type": "file", + "Version": "1.0.0", + "PublicKey": "ikbdrUbkZJXeTqLykGi+YNxq8JT9M0x+Ke95WKZNOfs=", + "Config": { + "FilePath": "testdata/key.json" + } +} diff --git a/crypto-provider/demo/testdata/key.json b/crypto-provider/demo/testdata/key.json new file mode 100644 index 0000000..b4ae29b --- /dev/null +++ b/crypto-provider/demo/testdata/key.json @@ -0,0 +1,6 @@ +{ + "priv_key": { + "type": "Ed25519", + "value": "8g7n4j2wR9oBpCDcOAlJ4MaX9jWXDxGRM/c7yFTCjmmKRt2tRuRkld5OovKQaL5g3GrwlP0zTH4p73lYpk05+w==" + } + } \ No newline at end of file diff --git a/crypto-provider/docs/architecture/adr-001-crypto-provider.md b/crypto-provider/docs/architecture/adr-001-crypto-provider.md new file mode 100644 index 0000000..0d2866f --- /dev/null +++ b/crypto-provider/docs/architecture/adr-001-crypto-provider.md @@ -0,0 +1,506 @@ +# ADR 001: Crypto Providers + +## Change log + +* July 1st 2024: Implementation proposal (Zondax AG: @raynaudoe @juliantoledano @jleni @educlerici-zondax @lucaslopezf) + +## Status + +PROPOSED + +## Abstract + +This ADR proposes the refactoring of the existing cryptographic code to support multiple cryptographic curves for signing and verification processes. With this update, we aim to facilitate the integration of new cryptographic curves through clean and simple interfaces. Additionally, support for Hardware Security Modules (HSM) is introduced as a complementary enhancement in this redesign. + +## Introduction + +The introduction of multi-curve support for the Interchain Stack offers significant advantages. Developers can choose the most appropriate curve based on security, performance, and compatibility requirements. This flexibility enhances the application's ability to adapt to evolving security standards and optimizes performance for specific use cases, helping to future-proofing the Interchain apps cryptographic capabilities. + +Special focus has been placed on the following key aspects: + +* modularity +* extensibility +* security +* maintainability +* developer experience + +On this document we'll introduce the concept of the `CryptoProvider` interface, which acts as a centralized controller for cryptographic operations, encapsulating the APIs for signing, verifying, and hashing functionalities. It abstracts the underlying cryptographic implementations, enabling a modular and extensible architecture. This allows users to easily switch between different cryptographic implementations without impacting the rest of the system. + +Key capabilities include: + +* **Signing**: Generate digital signatures for messages using various cryptographic curves. +* **Verifying**: Validate digital signatures against messages and public keys. +* **Hashing**: Perform hashing operations with different algorithms. +* **Hardware Security Module (HSM) Support**: Integrate with hardware devices and cloud-based HSMs for enhanced security. +* **Remote Signers**: Support for remote cryptographic operations, enabling interaction with secure, remote environments or cloud-based services. + +### Glossary + +1. **Interface**: In the context of this document, "interface" refers to Go's interface. + +2. **Module**: In this document, "module" refers to a Go module. + +3. **Package**: In the context of Go, a "package" refers to a unit of code organization. + +## Objectives + +The key objectives for this proposal are: + +Modular Design Philosophy + +* Establish a flexible and extensible foundation using interfaces to enable the seamless integration of various cryptographic curves. + +* Restructure, Refactor, and Decouple: Update the codebase to ensure modularity and future adaptability. + +Documentation & Community Engagement + +* Enhance documentation to ensure clarity, establish a good practices protocol and promote community engagement, providing a platform for feedback and collaborative growth. + +Backward Compatibility & Migration + +* Prioritize compatibility with previous version to avoid disruptions for existing users. + +* Design and propose a suitable migration path, ensuring transitions are as seamless as possible. + +Developer-Centric Approach + +* Prioritize clear, intuitive interfaces and best-practice design principles. + +Quality Assurance + +* Enhanced Test Coverage: Improve testing methodologies to ensure the robustness and reliability of the module. + +* Conduct an Audit: After implementation, perform a comprehensive audit to identify potential vulnerabilities and ensure the module's security and stability. + +## Technical Goals + +Multi-curve support: + +* Support for a wide range of cryptographic curves to be integrated seamlessly into any Interchain app in a modular way. + +Wide Hardware Device & Cloud-based HSM Interface Support: + +* Design a foundational interface for various hardware devices (Ledger, YubiKey, Thales, etc.) and cloud-based HSMs (Amazon, Azure) to support both current and future implementations. + +Testing: + +* Design an environment for testing, ensuring developers can validate integrations without compromising system integrity. + +## Proposed architecture + +In this section, we will first introduce the concept of a `CryptoProvider`, which serves as the main API. Following this, we will present the detailed components that make up the `CryptoProvider`. Lastly, we will introduce the storage and persistence layer, providing code snippets for each component to illustrate their implementation. + +### Crypto Provider + +This **interface** acts as a centralized controller, encapsulating the APIs for the **signing**, **verifying** and **hashing** functionalities. It acts as the main API with which the apps will interact with + +By abstracting the underlying cryptographic functionalities, `CryptoProvider` enables a modular and extensible architecture, aka 'pluggable cryptography'. It allows users to easily switch between different cryptographic implementations without impacting the rest of the system. + +The `CryptoProvider` interface includes getters for essential cryptographic functionalities and its metadata: + +```go +// CryptoProvider aggregates the functionalities of signing, verifying, and hashing, and provides metadata. +type CryptoProvider interface { + // GetSigner returns an instance of Signer. + GetSigner() Signer + + // GetVerifier returns an instance of Verifier. + GetVerifier() Verifier + + // GetHasher returns an instance of Hasher. + GetHasher() Hasher + + // Metadata returns metadata for the crypto provider. + Metadata() ProviderMetadata +} +``` + +#### Components + +The components defined here are designed to act as *wrappers* around the underlying proper functions. This architecture ensures that the actual cryptographic operations such as signing, hashing, and verifying are delegated to the specialized functions, that are implementation dependant. These wrapper components facilitate a clean and modular approach by abstracting the complexity of direct cryptographic function calls. + +In all of the interface's methods, we add an *options* input parameter of type `map[string]any`, designed to provide a flexible and dynamic way to pass various options and configurations to the `Sign`, `Verify`, and `Hash` functions. This approach allows developers to customize these processes by including any necessary parameters that might be required by specific algorithms or operational contexts. However, this requires that a type assertion for each option be performed inside the function's implementation. + +##### Signer + +Interface responsible for signing a message and returning the generated signature. +The `SignerOpts` map allows for flexible and dynamic configuration of the signing process. +This can include algorithm-specific parameters or any other contextual information +that might be necessary for the signing operation. + +```go +// Signer represents a general interface for signing messages. +type Signer interface { + // Sign takes a signDoc as input and returns the digital signature. + Sign(signDoc []byte, options SignerOpts) (Signature, error) +} + +type SignerOpts = map[string]any +``` + +###### Signature + +```go +// Signature represents a general interface for a digital signature. +type Signature interface { + // Bytes returns the byte representation of the signature. + Bytes() []byte + + // Equals checks if two signatures are identical. + Equals(other Signature) bool +} +``` + +##### Verifier + +Verifies if given a message belongs to a public key by validating against its respective signature. + +```go +// Verifier represents a general interface for verifying signatures. +type Verifier interface { + // Verify checks the digital signature against the message and a public key to determine its validity. + Verify(signature Signature, signDoc []byte, pubKey PubKey, options VerifierOpts) (bool, error) +} + +type VerifierOpts = map[string]any +``` + +##### Hasher + +This interface allows to have a specific hashing algorithm. + +```go +// Hasher represents a general interface for hashing data. +type Hasher interface { + // Hash takes an input byte array and returns the hashed output as a byte array. + Hash(input []byte, options HasherOpts) (output []byte, err error) +} + +type HasherOpts = map[string]any +``` + +##### Metadata + +The metadata allows uniquely identifying a `CryptoProvider` and also stores its configurations. + +```go +// ProviderMetadata holds metadata about the crypto provider. +type ProviderMetadata struct { + Name string + Type string + Version *semver.Version // Using semver type for versioning + Config map[string]any +} +``` + +##### Public Key + +*Note:* Here we decoupled the `Address` type from its corresponding `PubKey`. The corresponding codec step is proposed to be abstracted out from the `CryptoProvider` layer. + +```go +type PubKey interface { + Bytes() []byte + Equals(other PubKey) bool + Type() string +} +``` + +##### Private Key + +*Note*: For example, in hardware wallets, the `PrivKey` interface acts only as a *reference* to the real data. This is a design consideration and may be subject to change during implementation. + +Future enhancements could include additional security functions such as **zeroing** memory after private key usage to further enhance security measures. + +```go +type PrivKey interface { + Bytes() []byte + PubKey() PubKey + Equals(other PrivKey) bool + Type() string +} +``` + +##### Storage and persistence + +The storage and persistence layer is tasked with storing a `CryptoProvider`s. Specifically, this layer must: + +* Securely store the crypto provider's associated private key (only if stored locally, otherwise a reference to the private key will be stored instead). +* Store the `ProviderMetadata` struct which contains the data that distinguishes that provider. + +The purpose of this layer is to ensure that upon retrieval of the persisted data, we can access the provider's type, version, and specific configuration (which varies based on the provider type). This information will subsequently be utilized to initialize the appropriate factory. + +Proposed alternatives: + +* Using JSON file with a specification to store the crypto providers in a JSON format. +* Using Protobuf to store this information into a convenient Protobuf message. Apps that already use Protobuf might find this approach easier to integrate. + + +Below is the proposed protobuf message definition: + +###### Protobuf message structure + +```protobuf + +// cryptoprovider.proto + +syntax = "proto3"; + +package crypto; + +import "google/protobuf/any.proto"; + +// CryptoProvider holds all necessary information to instantiate and configure a CryptoProvider. +message CryptoProvider { + string name = 1; // (unique) name of the crypto provider. + google.protobuf.Any pub_key = 2; + string type = 3; // Type of the crypto provider + string version = 4; // Version (semver format) + map config = 5; // Configuration data with byte array values + google.protobuf.Any privKey = 6; // Optional if key is stored locally +} +``` + +name: +Specifies the unique name of the crypto provider. This name is used to identify and reference the specific crypto provider instance. + +pub_key (google.protobuf.Any): +Holds the public key associated with the crypto provider. + +type: +Specifies the type of the crypto provider. This field is used to identify and differentiate between various crypto provider implementations. Examples: `ledger`, `AWSCloudHSM`, `local-secp256k1` + +version: +Indicates the version of the crypto provider using semantic versioning. + +configuration (map): +Contains serialized configuration data as key-value pairs, where the key is a string and the value is a byte array. + +privKey (google.protobuf.Any): +An optional field that can store a private key if it is managed locally. + +##### Creating and loading a `CryptoProvider` + +For creating providers, we propose a *factory pattern* and a *registry* for these builders. + +Below, we present the proposed interfaces and code snippets to illustrate the proposed architecture. + +```go +// CryptoProviderFactory is a factory interface for creating CryptoProviders. +// Must be implemented by each CryptoProvider. +type CryptoProviderFactory interface { + CreateFromJSON(data []byte) (CryptoProvider, error) + CreateFromProto(data []byte) (CryptoProvider, error) + CreateFromConfig(metadata ProviderMetadata) (CryptoProvider, error) + Type() string +} +``` + +##### Illustrative Code Snippets + + The following code snippet demonstrates a provider **factory** and builder **registry**. Please note that this is for illustration purposes only. During actual implementation, these interfaces and methods may be subject to change and optimization. + +```go +// crypto/v2/providerFactory.go + +type Factory struct { + providerFactories map[string]CryptoProviderFactory +} + +// NewFactory creates a new Factory instance and initializes the providerFactories map. +func NewFactory() *Factory { + return &Factory{ + providerFactories: make(map[string]CryptoProviderFactory), + } +} + + +// RegisterCryptoProviderFactory is a function that registers a CryptoProviderFactory for its corresponding type. +func (f *Factory) RegisterCryptoProviderFactory(factory CryptoProviderFactory) string { + providerType := factory.Type() + f.providerFactories[providerType] = factory + return providerType +} + + +// CreateCryptoProviderFromJSON creates a CryptoProvider based on the provided JSON data. +func (f *Factory) CreateCryptoProviderFromJSON(data []byte) (CryptoProvider, error) { + if data == nil { + return nil, fmt.Errorf("data cannot be nil") + } + + // Assuming the JSON data contains a type field to determine the provider type + var metadata ProviderMetadata + if err := json.Unmarshal(data, &metadata); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON data: %v", err) + } + + factory, exists := f.providerFactories[metadata.Type] + if !exists { + return nil, fmt.Errorf("no factory registered for provider type %s", metadata.Type) + } + + return factory.CreateFromJSON(data) +} + +// CreateCryptoProviderFromProto creates a CryptoProvider based on the provided Proto data. +func (f *Factory) CreateCryptoProviderFromProto(data []byte) (CryptoProvider, error) { + if data == nil { + return nil, fmt.Errorf("data cannot be nil") + } + + // Assuming the Proto data contains a type field to determine the provider type + var metadata ProviderMetadata + if err := proto.Unmarshal(data, &metadata); err != nil { + return nil, fmt.Errorf("failed to unmarshal Proto data: %v", err) + } + + factory, exists := f.providerFactories[metadata.Type] + if !exists { + return nil, fmt.Errorf("no factory registered for provider type %s", metadata.Type) + } + + return factory.CreateFromProto(data) +} + +// CreateCryptoProviderFromConfig creates a CryptoProvider based on the provided ProviderMetadata. +func (f *Factory) CreateCryptoProviderFromConfig(config ProviderMetadata) (CryptoProvider, error) { + if config.Type == "" { + return nil, fmt.Errorf("config type cannot be empty") + } + + factory, exists := f.providerFactories[config.Type] + if !exists { + return nil, fmt.Errorf("no factory registered for provider type %s", config.Type) + } + + return factory.CreateFromConfig(config) +} +``` + +**Example**: Ledger HW implementation + +Below is an example implementation of how a Ledger hardware wallet `CryptoProvider` might implement the registration of its factory and how instantiation would work. + +```go +// crypto/v2/providers/ledger/factory.go + +const FACTORY_TYPE = "Ledger" + +type LedgerCryptoProviderFactory struct { + DevicePath string + // Any other necessary fields goes here +} + +func (f *LedgerCryptoProviderFactory) CreateFromJson(data []byte) (CryptoProvider, error) { + // Extract necessary data from the JSON to initialize a LedgerCryptoProvider + if data == nil { + return nil, fmt.Errorf("data is nil") + } + + var config map[string]interface{} + if err := json.Unmarshal(data, &config); err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON data: %v", err) + } + + // Assuming the JSON contains necessary fields like devicePath + devicePath, ok := config["devicePath"].(string) + if !ok { + return nil, fmt.Errorf("device path not found in JSON data") + } + + // Initialize the LedgerCryptoProvider with the device path + return &LedgerCryptoProvider{DevicePath: devicePath}, nil +} + +func (f *LedgerCryptoProviderFactory) Type() string { + return FACTORY_TYPE +} + +// crypto/v2/examples/registerProvider.go + +import ( + "crypto/v2/providers" + "log" +) + +func main() { + // Initialize a new factory instance + factory := NewFactory() + + // Create an instance of the ledger factory + ledgerFactory := &ledger.LedgerCryptoProviderFactory{} + + // Register the factory + factory.RegisterCryptoProviderFactory(ledgerFactory) + + // Example of loading a JSON file from a filesystem location + // Assuming jsonFilePath already exists + jsonData, err := ioutil.ReadFile(jsonFilePath) + if err != nil { + log.Fatalf("Error reading JSON file: %s", err) + } + + ledgerProvider, err := factory.CreateCryptoProviderFromJson(jsonData) + if err != nil { + log.Fatalf("Error creating crypto provider from json: %s", err) + } + + log.Printf("Provider from record created successfully: %+v", ledgerProvider.Metadata()) + + // ledgerProvider CryptoProvider ready to use +} +``` + +##### Especial use case: remote signers + +It's important to note that the `CryptoProvider` interface is versatile enough to be implemented as a remote signer. This capability allows for the integration of remote cryptographic operations, which can be particularly useful in distributed or cloud-based environments where local cryptographic resources are limited or need to be managed centrally. + +Here are a few of the services that can be leveraged: + +* AWS CloudHSM +* Azure Key Vault +* HashiCorp Vault +* Google Cloud KMS + +## Alternatives + +It is important to note that all the code presented in this document is not in its final form and could be subject to changes at the time of implementation. The examples and implementations discussed should be interpreted as alternatives, providing a conceptual framework rather than definitive solutions. This flexibility allows for adjustments based on further insights, technical evaluations, or changing requirements as development progresses. + +## Decision + +We will: + +* Refactor the module structure as described above. +* Define types and interfaces as the code attached. +* Refactor existing code into new structure and interfaces. +* Implement Unit Tests to ensure no backward compatibility issues. + +> While an ADR is in the DRAFT or PROPOSED stage, this section should contain a +> summary of issues to be solved in future iterations (usually referencing comments +> from a pull-request discussion). +> +> Later, this section can optionally list ideas or improvements the author or +> reviewers found during the analysis of this ADR. + +### Tentative Primitive Building Blocks + +This is a **tentative** list of primitives that we might want to support. +**This is not a final list or comprehensive, and it is subject to change.** +Moreover, it is important to emphasize the purpose of this work allows extensibility so any other primitive can be added in the future. + +* digital signatures + * RSA (PSS) + * ECDSA (secp256r1, secp256k1, etc.) + * EdDSA (ed25519, ed448) + * SR25519 + * Schnorr + * Lattice-based (Dilithium) + * BLS (BLS12-381, 377?) + +* Hashing + * sha2 / sha3 + * RIPEMD-160 + * blake2b,2s,3 + * Keccak-256 / shake256 + * bcrypt / scrypt / argon2, Argon2d/i/id + * Pedersen \ No newline at end of file diff --git a/crypto-provider/go.mod b/crypto-provider/go.mod new file mode 100644 index 0000000..4a07740 --- /dev/null +++ b/crypto-provider/go.mod @@ -0,0 +1,33 @@ +module github.com/cosmos/crypto-provider + +go 1.23 + +require ( + github.com/99designs/keyring v1.2.2 + github.com/Masterminds/semver/v3 v3.2.1 + github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 + github.com/mattn/go-isatty v0.0.20 + github.com/spf13/cobra v1.8.1 + golang.org/x/crypto v0.26.0 +) + +replace github.com/99designs/keyring => github.com/cosmos/keyring v1.2.0 + +require ( + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dvsekhvalnov/jose2go v1.6.0 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/mtibben/percent v0.2.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/testify v1.9.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.23.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) diff --git a/crypto-provider/go.sum b/crypto-provider/go.sum new file mode 100644 index 0000000..aaef1d6 --- /dev/null +++ b/crypto-provider/go.sum @@ -0,0 +1,68 @@ +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= +github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816 h1:41iFGWnSlI2gVpmOtVTJZNodLdLQLn/KsJqFvXwnd/s= +github.com/bgentry/speakeasy v0.1.1-0.20220910012023-760eaf8b6816/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/cosmos/keyring v1.2.0 h1:8C1lBP9xhImmIabyXW4c3vFjjLiBdGCmfLUfeZlV1Yo= +github.com/cosmos/keyring v1.2.0/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/crypto-provider/logo.png b/crypto-provider/logo.png new file mode 100644 index 0000000..d14766a Binary files /dev/null and b/crypto-provider/logo.png differ diff --git a/crypto-provider/pkg/cli/cmd.go b/crypto-provider/pkg/cli/cmd.go new file mode 100644 index 0000000..423b82b --- /dev/null +++ b/crypto-provider/pkg/cli/cmd.go @@ -0,0 +1,24 @@ +package cli + +import ( + "github.com/spf13/cobra" + "sync" +) + +var ( + rootCmd *cobra.Command + initRootCmd sync.Once +) + +// GetRootCmd returns the root command for the application +func GetRootCmd() *cobra.Command { + initRootCmd.Do(func() { + rootCmd = &cobra.Command{ + Use: "wallet", + Short: "Wallet is a CLI for managing crypto providers", + Long: `Wallet is a command-line application for managing and interacting with various crypto providers.`, + } + }) + + return rootCmd +} diff --git a/crypto-provider/pkg/cli/doc.go b/crypto-provider/pkg/cli/doc.go new file mode 100644 index 0000000..ef6a24d --- /dev/null +++ b/crypto-provider/pkg/cli/doc.go @@ -0,0 +1,26 @@ +/* +Package cli implements a "pluggable" command-line interface (CLI) system for managing specific CLI commands of each provider type in a way that doesn't +require modifying the core code + +Key components: + +1. Root Command (root.go): + - Implements a GetRootCmd() function using the singleton pattern and sync.Once for thread-safety. + - Initializes the root command and its flags. + +2. Provider Commands (e.g., pkg/impl/file/cmd/command.go): + - Each provider package implements its own set of subcommands. + - Uses an init() function to register its commands with the root command. + +How to add a new provider cli: + +1. Create a new package for your provider (e.g., pkg/impl/newprovider/cmd/). +2. In this package, create a file (e.g., command.go) with the following structure: + - Implement an init() function that calls GetRootCmd() and adds your provider's command. + - Create a NewCommand() function that returns a cobra.Command for your provider. + - Implement any subcommands specific to your provider. + +3. Import your new provider package in the register.go file for side effects. +*/ + +package cli diff --git a/crypto-provider/pkg/components/address_formatter.go b/crypto-provider/pkg/components/address_formatter.go new file mode 100644 index 0000000..3e7a030 --- /dev/null +++ b/crypto-provider/pkg/components/address_formatter.go @@ -0,0 +1,6 @@ +package components + +// AddressFormatter returns a formatted address from the given public key bytes +type AddressFormatter interface { + FormatAddress([]byte) (string, error) +} diff --git a/crypto-provider/pkg/components/factory.go b/crypto-provider/pkg/components/factory.go new file mode 100644 index 0000000..481adca --- /dev/null +++ b/crypto-provider/pkg/components/factory.go @@ -0,0 +1,130 @@ +package components + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +// CryptoProviderFactory is a factory interface for creating CryptoProviders. +// Must be implemented by each CryptoProvider. +type CryptoProviderFactory interface { + // Create creates a new CryptoProvider instance using the given source + Create(source BuildSource) (CryptoProvider, error) + + // Save saves the CryptoProvider to the underlying storage + Save(provider CryptoProvider) error + + // Type returns the type of the CryptoProvider that this factory creates + Type() string + + // SupportedSources returns the sources that this factory supports building CryptoProviders from + SupportedSources() []string +} + +// CryptoProviderConfig defines the configuration structure for CryptoProvider. +type CryptoProviderConfig struct { + ProviderType string `json:"provider_type"` + Options map[string]interface{} `json:"options"` +} + +type BuildSource interface { + Type() string + Validate() error +} + +// Common BuildSource implementations + +// BuildSourceNew ///////////////////////////////////////////////////////////////// +// is a BuildSource implementation that creates a new provider with default values +// ///////////////////////////////////////////////////////////////////////////////// +type BuildSourceNew struct { + Name string +} + +func (m BuildSourceNew) Type() string { return "new" } +func (m BuildSourceNew) Validate() error { return nil } + +// BuildSourceMetadata ///////////////////////////////////////////////////////////// +// is a BuildSource implementation that uses ProviderMetadata as source +// ///////////////////////////////////////////////////////////////////////////////// +type BuildSourceMetadata struct { + Metadata ProviderMetadata +} + +func (m BuildSourceMetadata) Type() string { return "metadata" } +func (m BuildSourceMetadata) Validate() error { return m.Metadata.Validate() } + +// BuildSourceMnemonic ///////////////////////////////////////////////////////////// +// is a BuildSource implementation that uses a mnemonic as source +// ///////////////////////////////////////////////////////////////////////////////// +type BuildSourceMnemonic struct { + Mnemonic string +} + +func (m BuildSourceMnemonic) Type() string { return "mnemonic" } +func (m BuildSourceMnemonic) Validate() error { + //TODO + return nil +} + +// BuildSourceJson ///////////////////////////////////////////////////////////////// +// is a BuildSource implementation that uses a JSON string as source +// ///////////////////////////////////////////////////////////////////////////////// +type BuildSourceJson struct { + JsonString string +} + +func (m BuildSourceJson) Type() string { return "json" } +func (m BuildSourceJson) Validate() error { + var jsonData map[string]interface{} + err := json.Unmarshal([]byte(m.JsonString), &jsonData) + if err != nil { + return err + } + // Additional validation can be added here if needed + return nil +} + +// BuildSourceConfig ////////////////////////////////////////////////////////////// +// is a BuildSource implementation that uses CryptoProviderConfig as source +// ///////////////////////////////////////////////////////////////////////////////// +type BuildSourceConfig struct { + Config CryptoProviderConfig +} + +func (m BuildSourceConfig) Type() string { return "config" } +func (m BuildSourceConfig) Validate() error { + // Validate the Config field + if m.Config.ProviderType == "" { + return fmt.Errorf("provider_type is required in the configuration") + } + // Additional validation can be added here if needed + return nil +} + +// BaseCryptoProviderFactory ////////////////////////////////////////////////////// + +type BaseFactory struct { + BaseDir string +} + +func (f *BaseFactory) Save(provider CryptoProvider) error { + metadata := provider.Metadata() + filename := fmt.Sprintf("%s.json", metadata.Name) + + path := filepath.Join(f.BaseDir, filename) + + // Create the directory if it doesn't exist + if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + data, err := metadata.Serialize() + if err != nil { + return fmt.Errorf("failed to marshal metadata: %w", err) + } + + return os.WriteFile(path, data, 0600) +} diff --git a/crypto-provider/pkg/components/hasher.go b/crypto-provider/pkg/components/hasher.go new file mode 100644 index 0000000..1f47a6d --- /dev/null +++ b/crypto-provider/pkg/components/hasher.go @@ -0,0 +1,9 @@ +package components + +// Hasher represents a general interface for hashing data. +type Hasher interface { + // Hash takes an input byte array and returns the hashed output as a byte array. + Hash(input []byte, options HasherOpts) (output []byte, err error) +} + +type HasherOpts = map[string]any diff --git a/crypto-provider/pkg/components/keys.go b/crypto-provider/pkg/components/keys.go new file mode 100644 index 0000000..94139df --- /dev/null +++ b/crypto-provider/pkg/components/keys.go @@ -0,0 +1,20 @@ +package components + +type PubKey interface { + Bytes() []byte + Equals(other PubKey) bool + Type() string +} + +// PrivKey interface with generics +type PrivKey[T PubKey] interface { + Bytes() []byte + PubKey() T + Equals(other PrivKey[T]) bool + Type() string +} + +// KeyFactory defines how a CryptoProvider creates and manages its keys +type KeyFactory interface { + GenPubKeyFromString(string) (PubKey, error) +} diff --git a/crypto-provider/pkg/components/metadata.go b/crypto-provider/pkg/components/metadata.go new file mode 100644 index 0000000..27828e5 --- /dev/null +++ b/crypto-provider/pkg/components/metadata.go @@ -0,0 +1,64 @@ +package components + +import ( + "encoding/json" + "fmt" + + "github.com/Masterminds/semver/v3" + + "github.com/cosmos/crypto-provider/pkg/keyring" +) + +type ProviderConfig = map[string]any + +type ProviderMetadata struct { + Version string `json:"version"` + Name string `json:"name"` + Type string `json:"type"` + PublicKey string `json:"publickey"` + Config ProviderConfig `json:"config"` +} + +func FromRecord(record *keyring.Record) (*ProviderMetadata, error) { + var meta ProviderMetadata + if record.CodecType != "json" { + return nil, fmt.Errorf("unsupported codec type: %s", record.CodecType) + } + + err := json.Unmarshal(record.Data, &meta) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal metadata: %w", err) + } + + return &meta, nil +} + +// Validate checks if the ProviderMetadata is valid +func (pm ProviderMetadata) Validate() error { + _, err := semver.NewVersion(pm.Version) + if err != nil { + return fmt.Errorf("invalid version: %w", err) + } + + if pm.Name == "" { + return fmt.Errorf("name is required") + } + if pm.Type == "" { + return fmt.Errorf("type is required") + } + + if pm.PublicKey == "" { + return fmt.Errorf("publickey is required") + } + + return nil +} + +// Serialize returns a JSON serialized string of the ProviderMetadata +func (pm ProviderMetadata) Serialize() ([]byte, error) { + data, err := json.MarshalIndent(pm, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal metadata: %w", err) + } + return data, nil +} diff --git a/crypto-provider/pkg/components/provider.go b/crypto-provider/pkg/components/provider.go new file mode 100644 index 0000000..be1ece0 --- /dev/null +++ b/crypto-provider/pkg/components/provider.go @@ -0,0 +1,27 @@ +package components + +// CryptoProvider aggregates the functionalities of signing, verifying, and hashing, and provides metadata. +type CryptoProvider interface { + // GetSigner returns an instance of Signer. + GetSigner() Signer + + // GetVerifier returns an instance of Verifier. + GetVerifier() Verifier + + // GetHasher returns an instance of Hasher. + GetHasher() Hasher + + // Metadata returns metadata for the crypto provider. + Metadata() ProviderMetadata + + // GetPubKey returns the public key of the provider + GetPubKey() PubKey + + // ProviderInitializer is an internal interface for keys initialization + ProviderInitializer +} + +type ProviderInitializer interface { + // InitializeKeys initializes the keys for the provider. + InitializeKeys() error +} diff --git a/crypto-provider/pkg/components/signer.go b/crypto-provider/pkg/components/signer.go new file mode 100644 index 0000000..97a6546 --- /dev/null +++ b/crypto-provider/pkg/components/signer.go @@ -0,0 +1,18 @@ +package components + +// Signer represents a general interface for signing messages. +type Signer interface { + // Sign takes a signDoc as input and returns the digital signature. + Sign(signDoc []byte, options SignerOpts) (Signature, error) +} + +type SignerOpts = map[string]any + +// Signature represents a general interface for a digital signature. +type Signature interface { + // Bytes returns the byte representation of the signature. + Bytes() []byte + + // Equals checks if two signatures are identical. + Equals(other Signature) bool +} diff --git a/crypto-provider/pkg/components/verifier.go b/crypto-provider/pkg/components/verifier.go new file mode 100644 index 0000000..e835da3 --- /dev/null +++ b/crypto-provider/pkg/components/verifier.go @@ -0,0 +1,9 @@ +package components + +// Verifier represents a general interface for verifying signatures. +type Verifier interface { + // Verify checks the digital signature against the message and a public key to determine its validity. + Verify(signature Signature, signDoc []byte, options VerifierOpts) (bool, error) +} + +type VerifierOpts = map[string]any diff --git a/crypto-provider/pkg/factory/doc.go b/crypto-provider/pkg/factory/doc.go new file mode 100644 index 0000000..975e36a --- /dev/null +++ b/crypto-provider/pkg/factory/doc.go @@ -0,0 +1,23 @@ +// Package factory provides a flexible and extensible factory pattern implementation +// for creating and managing crypto providers. +// +// The main components of this package are: +// +// - Factory: A singleton struct that manages the registration and creation of +// crypto providers. +// +// - RegisterFactory: A method to register new crypto provider factories. +// +// - CreateCryptoProvider: A method to create crypto providers based on two key parameters: +// 1. providerType: Specifies the type of crypto provider (e.g., "ledger", "file", "memory"). +// 2. source: Represents the data source from which the provider should be built. +// This could be a file path, a hardware device identifier, or any other +// source-specific information needed to initialize the provider. +// This approach allows for flexible creation of providers with different +// backends and data sources. +// +// - GetRegisteredFactories: A method to retrieve all registered factory types. +// +// This package is designed to be thread-safe and allows for easy extension of +// supported crypto provider types and data sources. +package factory diff --git a/crypto-provider/pkg/factory/factory.go b/crypto-provider/pkg/factory/factory.go new file mode 100644 index 0000000..eb0847c --- /dev/null +++ b/crypto-provider/pkg/factory/factory.go @@ -0,0 +1,104 @@ +package factory + +import ( + "encoding/json" + "fmt" + "sync" + + "github.com/cosmos/crypto-provider/pkg/components" +) + +var ( + factoryInstance *Factory + once sync.Once +) + +type Factory struct { + registry map[string]components.CryptoProviderFactory +} + +func GetGlobalFactory() *Factory { + once.Do(func() { + factoryInstance = newFactory() + }) + return factoryInstance +} + +// GetRegisteredFactories returns a slice of all registered factory types. +func (f *Factory) GetRegisteredFactories() []string { + factories := make([]string, 0, len(f.registry)) + for key := range f.registry { + factories = append(factories, key) + } + return factories +} + +// RegisterFactory is a function that registers a CryptoProviderFactory for its corresponding type. +func (f *Factory) RegisterFactory(factory components.CryptoProviderFactory) error { + providerType := factory.Type() + if providerType == "" { + return fmt.Errorf("provider type cannot be empty") + } + + if _, exists := f.registry[providerType]; exists { + fmt.Printf("warning: factory for provider type '%s' already registered\n", providerType) + return nil + } + + f.registry[providerType] = factory + return nil +} + +// CreateCryptoProvider creates a CryptoProvider based on the provided metadata. +func (f *Factory) CreateCryptoProvider(providerType string, source components.BuildSource) (components.CryptoProvider, error) { + factory, exists := f.registry[providerType] + if !exists { + return nil, fmt.Errorf("no factory registered for provider type: '%s'", providerType) + } + + provider, err := factory.Create(source) + if err != nil { + return nil, err + } + + // Type assert to internal interface + initializer, ok := provider.(components.ProviderInitializer) + if !ok { + return nil, fmt.Errorf("provider does not implement initializer interface") + } + + // Initialize keys using internal interface + if err := initializer.InitializeKeys(); err != nil { + return nil, fmt.Errorf("failed to initialize keys: %w. Check the implementation if InitializeKeys()", err) + } + + // Try get pubkey to check if it was initialized + pubKey := provider.GetPubKey() + if pubKey == nil { + return nil, fmt.Errorf("public key not available from provider") + } + + return provider, nil +} + +// LoadCryptoProvider loads a CryptoProvider from a raw JSON string. +func (f *Factory) LoadCryptoProvider(rawJSON string) (components.CryptoProvider, error) { + var config components.CryptoProviderConfig + if err := json.Unmarshal([]byte(rawJSON), &config); err != nil { + return nil, fmt.Errorf("failed to decode JSON: %w", err) + } + + if config.ProviderType == "" { + return nil, fmt.Errorf("provider_type is required in the configuration") + } + + source := components.BuildSourceConfig{Config: config} + return f.CreateCryptoProvider(config.ProviderType, source) +} + +// newFactory creates a new Factory instance and initializes the registry map. +func newFactory() *Factory { + return &Factory{ + registry: make(map[string]components.CryptoProviderFactory), + } +} diff --git a/crypto-provider/pkg/impl/file/cmd/cli.go b/crypto-provider/pkg/impl/file/cmd/cli.go new file mode 100644 index 0000000..d2d4c25 --- /dev/null +++ b/crypto-provider/pkg/impl/file/cmd/cli.go @@ -0,0 +1,51 @@ +package cmd + +import ( + "fmt" + "github.com/cosmos/crypto-provider/pkg/cli" + "github.com/spf13/cobra" +) + +func init() { + cmd := NewCommand() + cli.GetRootCmd().AddCommand(cmd) +} + +// NewCommand creates and returns the command for the file provider +func NewCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "file", + Short: "Manage file-based crypto providers", + Long: `This command allows you to interact with file-based crypto providers.`, + } + + // Add subcommands specific to file provider + cmd.AddCommand(newCreateCommand()) + cmd.AddCommand(newListCommand()) + + return cmd +} + +func newCreateCommand() *cobra.Command { + return &cobra.Command{ + Use: "create", + Short: "Create a new file-based provider", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Creating a new file-based provider...") + // Implement creation logic here + return nil + }, + } +} + +func newListCommand() *cobra.Command { + return &cobra.Command{ + Use: "list", + Short: "List all file-based providers", + RunE: func(cmd *cobra.Command, args []string) error { + fmt.Println("Listing all file-based providers...") + // Implement listing logic here + return nil + }, + } +} diff --git a/crypto-provider/pkg/impl/file/config.go b/crypto-provider/pkg/impl/file/config.go new file mode 100644 index 0000000..6ee9cbf --- /dev/null +++ b/crypto-provider/pkg/impl/file/config.go @@ -0,0 +1,37 @@ +package file + +import ( + "encoding/json" + "fmt" + + "github.com/cosmos/crypto-provider/pkg/components" +) + +// FileProviderConfig holds the configuration for the File Provider +type FileProviderConfig struct { + FilePath string `json:"filepath"` +} + +// BuildConfig creates a FileProviderConfig from the provided metadata +func BuildConfig(metadata components.ProviderMetadata) (FileProviderConfig, error) { + var config FileProviderConfig + jsonData, err := json.Marshal(metadata.Config) + if err != nil { + return FileProviderConfig{}, fmt.Errorf("failed to marshal config: %w", err) + } + + err = json.Unmarshal(jsonData, &config) + if err != nil { + return FileProviderConfig{}, fmt.Errorf("failed to unmarshal config: %w", err) + } + + return config, nil +} + +// Validate checks if the FileProviderConfig is valid +func (c FileProviderConfig) Validate() error { + if c.FilePath == "" { + return fmt.Errorf("FilePath cannot be empty") + } + return nil +} diff --git a/crypto-provider/pkg/impl/file/factory.go b/crypto-provider/pkg/impl/file/factory.go new file mode 100644 index 0000000..e991b6c --- /dev/null +++ b/crypto-provider/pkg/impl/file/factory.go @@ -0,0 +1,96 @@ +package file + +import ( + "encoding/json" + "fmt" + + "github.com/cosmos/crypto-provider/pkg/components" + "github.com/cosmos/crypto-provider/pkg/factory" +) + +// TODO: Should each provider have its own go.mod? + +const ( + SourceMetadata = "metadata" +) + +type FileProviderFactory struct { + components.BaseFactory +} + +// Register into the global factory +func init() { + f := factory.GetGlobalFactory() + err := f.RegisterFactory(&FileProviderFactory{}) + if err != nil { + // TODO err instead of panic + panic(fmt.Sprintf("failed to register factory: %v", err)) + } +} + +var _ components.CryptoProviderFactory = (*FileProviderFactory)(nil) + +func (f FileProviderFactory) Create(source components.BuildSource) (components.CryptoProvider, error) { + switch s := source.(type) { + case components.BuildSourceNew: + return createDefault(s.Name) + case components.BuildSourceMetadata: + return createFromMetadata(s.Metadata) + case components.BuildSourceJson: + return createFromJson(s.JsonString) + default: + return nil, fmt.Errorf("unsupported source type: %T", source) + } +} + +func createDefault(name string) (*FileProvider, error) { + meta := components.ProviderMetadata{ + Version: Version, + Type: ProviderTypeFile, + Name: name, + } + + return createFromMetadata(meta) +} + +func createFromMetadata(metadata components.ProviderMetadata) (*FileProvider, error) { + if err := metadata.Validate(); err != nil { + return nil, err + } + + providerConfig, err := BuildConfig(metadata) + if err != nil { + return nil, fmt.Errorf("failed to build config: %w", err) + } + + if err := providerConfig.Validate(); err != nil { + return nil, err + } + + return &FileProvider{ + filePath: providerConfig.FilePath, + metadata: metadata, + }, nil +} + +func createFromJson(jsonString string) (*FileProvider, error) { + var metadata components.ProviderMetadata + err := json.Unmarshal([]byte(jsonString), &metadata) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal JSON: %w", err) + } + return createFromMetadata(metadata) +} + +func (FileProviderFactory) Type() string { + return ProviderTypeFile +} + +func (f FileProviderFactory) SupportedSources() []string { + return []string{SourceMetadata, "new", "mnemonic", "json"} +} + +// Add this method to implement the full interface +func (f FileProviderFactory) Save(cp components.CryptoProvider) error { + return f.BaseFactory.Save(cp) +} diff --git a/crypto-provider/pkg/impl/file/hasher/hash.go b/crypto-provider/pkg/impl/file/hasher/hash.go new file mode 100644 index 0000000..2e2047a --- /dev/null +++ b/crypto-provider/pkg/impl/file/hasher/hash.go @@ -0,0 +1,10 @@ +package hasher + +import "github.com/cosmos/crypto-provider/pkg/components" + +type FileHash struct{} + +func (FileHash) Hash(input []byte, options components.HasherOpts) (output []byte, err error) { + //TODO implement me + panic("implement me") +} diff --git a/crypto-provider/pkg/impl/file/keys.go b/crypto-provider/pkg/impl/file/keys.go new file mode 100644 index 0000000..5eaa711 --- /dev/null +++ b/crypto-provider/pkg/impl/file/keys.go @@ -0,0 +1,63 @@ +package file + +import "github.com/cosmos/crypto-provider/pkg/components" + +type ( + PubKey = components.PubKey + PrivKey = components.PrivKey[components.PubKey] +) + +// dummyPubKey implements the PubKey interface +type dummyPubKey struct { + key string +} + +func (d *dummyPubKey) Bytes() []byte { + return []byte(d.key) +} + +func (d *dummyPubKey) Equals(other components.PubKey) bool { + if op, ok := other.(*dummyPubKey); ok { + return d.key == op.key + } + return false +} + +func (d *dummyPubKey) Type() string { + return "dummyPubKey" +} + +// NewPubKeyFromString creates a new PubKey instance from a string +func NewPubKeyFromString(key string) components.PubKey { + return &dummyPubKey{key: key} +} + +// Ensure dummyPrivKey implements the PrivKey interface +var _ PrivKey = (*dummyPrivKey)(nil) + +// Ensure dummyPubKey implements the PubKey interface +var _ components.PubKey = (*dummyPubKey)(nil) + +type dummyPrivKey struct { + key string +} + +func (d *dummyPrivKey) Bytes() []byte { + return []byte(d.key) +} + +func (d *dummyPrivKey) PubKey() components.PubKey { + // Create public key from private key + return NewPubKeyFromString(d.key) +} + +func (d *dummyPrivKey) Equals(other components.PrivKey[components.PubKey]) bool { + if op, ok := other.(*dummyPrivKey); ok { + return d.key == op.key + } + return false +} + +func (d *dummyPrivKey) Type() string { + return "dummyPrivKey" +} diff --git a/crypto-provider/pkg/impl/file/provider.go b/crypto-provider/pkg/impl/file/provider.go new file mode 100644 index 0000000..ce7b159 --- /dev/null +++ b/crypto-provider/pkg/impl/file/provider.go @@ -0,0 +1,49 @@ +package file + +import ( + "github.com/cosmos/crypto-provider/pkg/components" + "github.com/cosmos/crypto-provider/pkg/impl/file/hasher" + "github.com/cosmos/crypto-provider/pkg/impl/file/signer" + "github.com/cosmos/crypto-provider/pkg/impl/file/verifier" +) + +const ( + ProviderTypeFile = "file" + Version = "v1.0.0" +) + +type FileProvider struct { + filePath string + metadata components.ProviderMetadata +} + +var _ components.CryptoProvider = &FileProvider{} + +// GetSigner returns an instance of Signer. +func (fp *FileProvider) GetSigner() components.Signer { + return signer.NewFileSigner(fp.filePath) +} + +// GetVerifier returns an instance of Verifier. +func (fp *FileProvider) GetVerifier() components.Verifier { + v, _ := verifier.NewFileSigVerifier(fp.metadata.PublicKey) + return v +} + +// GetHasher returns an instance of Hasher. +func (fp *FileProvider) GetHasher() components.Hasher { + return hasher.FileHash{} +} + +// Metadata returns metadata for the crypto provider. +func (fp *FileProvider) Metadata() components.ProviderMetadata { + return fp.metadata +} + +func (fp *FileProvider) GetPubKey() components.PubKey { + return NewPubKeyFromString(fp.metadata.PublicKey) +} + +func (fp *FileProvider) InitializeKeys() error { + return nil +} diff --git a/crypto-provider/pkg/impl/file/signer/signer.go b/crypto-provider/pkg/impl/file/signer/signer.go new file mode 100644 index 0000000..357cdca --- /dev/null +++ b/crypto-provider/pkg/impl/file/signer/signer.go @@ -0,0 +1,71 @@ +package signer + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "fmt" + "github.com/cosmos/crypto-provider/pkg/components" + "os" + "path/filepath" +) + +type FileSigner struct { + privKeyPath string +} + +func NewFileSigner(privKeyPath string) *FileSigner { + return &FileSigner{privKeyPath: privKeyPath} +} + +func (fs FileSigner) Sign(signDoc []byte, options components.SignerOpts) (components.Signature, error) { + currentDir, _ := os.Getwd() + pemData, err := os.ReadFile(filepath.Join(currentDir, fs.privKeyPath)) + if err != nil { + return nil, fmt.Errorf("failed to read private key file: %v", err) + } + + // Parse the JSON file + var keyData struct { + PrivKey struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"priv_key"` + } + + err = json.Unmarshal(pemData, &keyData) + if err != nil { + return nil, fmt.Errorf("failed to parse JSON key file: %v", err) + } + + // Decode the base64 private key + privKeyBytes, err := base64.StdEncoding.DecodeString(keyData.PrivKey.Value) + if err != nil { + return nil, fmt.Errorf("failed to decode base64 private key: %v", err) + } + + // Ensure the key is of the correct type and length + if keyData.PrivKey.Type != "Ed25519" || len(privKeyBytes) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("invalid private key: expected Ed25519 key of length %d, got %s key of length %d", ed25519.PrivateKeySize, keyData.PrivKey.Type, len(privKeyBytes)) + } + + privKey := ed25519.PrivateKey(privKeyBytes) + + // Sign the document + signature := ed25519.Sign(privKey, signDoc) + + return &FileSignature{data: signature}, nil +} + +// FileSignature implements the types.Signature interface +type FileSignature struct { + data []byte +} + +func (fs *FileSignature) Bytes() []byte { + return fs.data +} + +func (fs *FileSignature) Equals(other components.Signature) bool { + return string(fs.data) == string(other.Bytes()) +} diff --git a/crypto-provider/pkg/impl/file/verifier/verifier.go b/crypto-provider/pkg/impl/file/verifier/verifier.go new file mode 100644 index 0000000..23eec91 --- /dev/null +++ b/crypto-provider/pkg/impl/file/verifier/verifier.go @@ -0,0 +1,30 @@ +package verifier + +import ( + "crypto/ed25519" + "encoding/base64" + "fmt" + "github.com/cosmos/crypto-provider/pkg/components" +) + +type FileSigVerifier struct { + pubKey ed25519.PublicKey +} + +func NewFileSigVerifier(pubKey string) (*FileSigVerifier, error) { + pubKeyBytes, err := base64.StdEncoding.DecodeString(pubKey) + if err != nil { + return nil, err + } + if len(pubKeyBytes) != ed25519.PublicKeySize { + return nil, fmt.Errorf("invalid public key size: expected %d, got %d", ed25519.PublicKeySize, len(pubKeyBytes)) + } + return &FileSigVerifier{ + pubKey: ed25519.PublicKey(pubKeyBytes), + }, nil +} + +func (v *FileSigVerifier) Verify(signature components.Signature, signDoc []byte, options components.VerifierOpts) (bool, error) { + sigBytes := signature.Bytes() + return ed25519.Verify(v.pubKey, signDoc, sigBytes), nil +} diff --git a/crypto-provider/pkg/keyring/internal/input.go b/crypto-provider/pkg/keyring/internal/input.go new file mode 100644 index 0000000..503e72f --- /dev/null +++ b/crypto-provider/pkg/keyring/internal/input.go @@ -0,0 +1,159 @@ +package internal + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/bgentry/speakeasy" + "github.com/mattn/go-isatty" + "golang.org/x/crypto/bcrypt" +) + +const ( + maxPassphraseEntryAttempts = 3 + // MinPassLength is the minimum acceptable password length + MinPassLength = 8 +) + +// NewRealPrompt creates a function that prompts for and manages keyring passphrases. +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 "", fmt.Errorf("failed to read %s: %w", keyhashFilePath, err) + } + + keyhashStored = true + + case os.IsNotExist(err): + keyhashStored = false + + default: + return "", fmt.Errorf("failed to open %s: %w", keyhashFilePath, err) + } + + failureCounter := 0 + + for { + failureCounter++ + if failureCounter > maxPassphraseEntryAttempts { + return "", fmt.Errorf("too many failed passphrase attempts") + } + + buf := bufio.NewReader(buf) + pass, err := 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 := 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 + } + } +} + +// getPassword will prompt for a password one-time +// It enforces the password length +func getPassword(prompt string, buf *bufio.Reader) (pass string, err error) { + if inputIsTty() { + pass, err = speakeasy.FAsk(os.Stderr, prompt) + } else { + pass, err = readLineFromBuf(buf) + } + + if err != nil { + return "", err + } + + if len(pass) < MinPassLength { + // Return the given password to the upstream client so it can handle a + // non-STDIN failure gracefully. + return pass, fmt.Errorf("password must be at least %d characters", MinPassLength) + } + + return pass, nil +} + +// inputIsTty returns true iff we have an interactive prompt, +// where we can disable echo and request to repeat the password. +// If false, we can optimize for piped input from another command +func inputIsTty() bool { + return isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) +} + +// readLineFromBuf reads one line from reader. +// Subsequent calls reuse the same buffer, so we don't lose +// any input when reading a password twice (to verify) +func readLineFromBuf(buf *bufio.Reader) (string, error) { + pass, err := buf.ReadString('\n') + + switch { + case errors.Is(err, io.EOF): + // If by any chance the error is EOF, but we were actually able to read + // something from the reader then don't return the EOF error. + // If we didn't read anything from the reader and got the EOF error, then + // it's safe to return EOF back to the caller. + if len(pass) > 0 { + // exit the switch statement + break + } + return "", err + + case err != nil: + return "", err + } + + return strings.TrimSpace(pass), nil +} diff --git a/crypto-provider/pkg/keyring/keyring.go b/crypto-provider/pkg/keyring/keyring.go new file mode 100644 index 0000000..3e0de50 --- /dev/null +++ b/crypto-provider/pkg/keyring/keyring.go @@ -0,0 +1,205 @@ +package keyring + +import ( + "errors" + "fmt" + "io" + "os" + + "github.com/99designs/keyring" + + "github.com/cosmos/crypto-provider/pkg/keyring/internal" +) + +// Backend options for Keyring +const ( + BackendFile = "file" + BackendOS = "os" + BackendTest = "test" + BackendMemory = "memory" +) + +//nolint:unused +const ( + keyringFileDirName = "keyring-file" + keyringTestDirName = "keyring-test" + passKeyringPrefix = "keyring-%s" +) + +var ( + _ Keyring = &keystore{} +) + +// Keyring exposes operations over a backend supported by github.com/99designs/keyring. +type Keyring interface { + // List returns all records in the keyring. + List() ([]*Record, error) + + // Get retrieves a record from the keyring by its uid. + Get(uid string) (*Record, error) + + // Delete removes a record from the keyring by its uid. + Delete(uid string) error + + // NewItem creates a new item and stores it in the keyring. + NewItem(uid string, data []byte, codecType string) (*Record, error) +} + +// NewKeyring creates a new instance of a keyring with the specified backend. +func NewKeyring(appName, backend, rootDir string, userInput io.Reader) (Keyring, error) { + var ( + db keyring.Keyring + err error + ) + + switch backend { + case BackendMemory: + return newInMemoryWithKeyring(keyring.NewArrayKeyring(nil)), err + case BackendTest: + db, err = keyring.Open(newTestBackendKeyringConfig(appName, rootDir)) + case BackendFile: + var cfg keyring.Config + cfg, err = newFileBackendKeyringConfig(appName, rootDir, userInput) + if err != nil { + return nil, err + } + db, err = keyring.Open(cfg) + case BackendOS: + db, err = keyring.Open(newOSBackendKeyringConfig(appName, rootDir, userInput)) + default: + return nil, fmt.Errorf("no available implementation for backend: %s", backend) + } + + if err != nil { + return nil, err + } + + return newKeystore(db, backend), nil +} + +// newInMemoryWithKeyring returns an in-memory keyring using the specified keyring.Keyring as the backing store. +func newInMemoryWithKeyring(kr keyring.Keyring) Keyring { + return newKeystore(kr, BackendMemory) +} + +type keystore struct { + db keyring.Keyring + backend string +} + +// List returns all records in the keystore. +func (ks keystore) List() ([]*Record, error) { + items, err := ks.db.Keys() + if err != nil { + return nil, err + } + + var records []*Record + for _, key := range items { + item, err := ks.db.Get(key) + if err != nil { + return nil, err + } + records = append(records, FromItem(item)) + } + + return records, nil +} + +// NewItem creates a new item and stores it in the keystore. +func (ks keystore) NewItem(uid string, data []byte, codecType string) (*Record, error) { + record := NewRecord(uid, data, codecType) + item := record.ToItem() + + err := ks.db.Set(item) + if err != nil { + return nil, err + } + + return record, nil +} + +// Get retrieves a record from the keystore by its uid. +func (ks keystore) Get(uid string) (*Record, error) { + item, err := ks.db.Get(uid) + if err != nil { + if errors.Is(err, keyring.ErrKeyNotFound) { + return nil, keyring.ErrKeyNotFound + } + return nil, err + } + + return FromItem(item), nil +} + +// Delete removes a record from the keystore by its uid. +func (ks keystore) Delete(uid string) error { + err := ks.db.Remove(uid) + if err != nil { + if errors.Is(err, keyring.ErrKeyNotFound) { + return keyring.ErrKeyNotFound + } + return err + } + return nil +} + +// newKeystore creates a new keystore instance with the given keyring and backend. +func newKeystore(kr keyring.Keyring, backend string) keystore { + return keystore{ + db: kr, + backend: backend, + } +} + +// newOSBackendKeyringConfig creates a new OS backend keyring configuration. +func newOSBackendKeyringConfig(appName, dir string, buf io.Reader) keyring.Config { + return keyring.Config{ + ServiceName: appName, + FileDir: dir, + KeychainTrustApplication: true, + FilePasswordFunc: internal.NewRealPrompt(dir, buf), + } +} + +// newTestBackendKeyringConfig creates a new test backend keyring configuration. +func newTestBackendKeyringConfig(appName, dir string) keyring.Config { + return keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.FileBackend}, + ServiceName: appName, + FileDir: dir, + FilePasswordFunc: func(_ string) (string, error) { + return "test", nil + }, + } +} + +// newPassBackendKeyringConfig creates a new pass backend keyring configuration. +// +//nolint:unused +func newPassBackendKeyringConfig(appName, _ string, _ io.Reader) (keyring.Config, error) { + prefix := fmt.Sprintf(passKeyringPrefix, appName) + + return keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.PassBackend}, + ServiceName: appName, + PassPrefix: prefix, + }, nil +} + +// newFileBackendKeyringConfig creates a new file backend keyring configuration. +func newFileBackendKeyringConfig(name, dir string, buf io.Reader) (keyring.Config, error) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + err := os.MkdirAll(dir, 0700) + if err != nil { + return keyring.Config{}, fmt.Errorf("failed to create keyring directory: %w", err) + } + } + + return keyring.Config{ + AllowedBackends: []keyring.BackendType{keyring.FileBackend}, + ServiceName: name, + FileDir: dir, + FilePasswordFunc: internal.NewRealPrompt(dir, buf), + }, nil +} diff --git a/crypto-provider/pkg/keyring/keyring_test.go b/crypto-provider/pkg/keyring/keyring_test.go new file mode 100644 index 0000000..625e59f --- /dev/null +++ b/crypto-provider/pkg/keyring/keyring_test.go @@ -0,0 +1,99 @@ +package keyring + +import ( + "testing" +) + +func TestKeyring(t *testing.T) { + tempDir := t.TempDir() + kr, err := NewKeyring("testapp", BackendTest, tempDir, nil) + if err != nil { + t.Errorf("failed to create keyring: %v", err) + return + } + + t.Run("NewItem", func(t *testing.T) { + record, err := kr.NewItem("testkey1", []byte("testvalue1"), "json") + if err != nil { + t.Errorf("failed to create new item: %v", err) + return + } + if record.Key != "testkey1" { + t.Errorf("expected key to be 'testkey1', got %s", record.Key) + } + if string(record.Data) != "testvalue1" { + t.Errorf("expected data to be 'testvalue1', got %s", record.Data) + } + if record.CodecType != "json" { + t.Errorf("expected codecType to be 'json', got %s", record.CodecType) + } + }) + + t.Run("Get", func(t *testing.T) { + record, err := kr.Get("testkey1") + if err != nil { + t.Errorf("failed to get item: %v", err) + return + } + if record.Key != "testkey1" { + t.Errorf("expected key to be 'testkey1', got %s", record.Key) + } + if string(record.Data) != "testvalue1" { + t.Errorf("expected data to be 'testvalue1', got %s", record.Data) + } + if record.CodecType != "json" { + t.Errorf("expected codecType to be 'json', got %s", record.CodecType) + } + + _, err = kr.Get("nonexistent") + if err == nil { + t.Errorf("expected error for nonexistent key, but got nil") + } + }) + + t.Run("List", func(t *testing.T) { + _, err := kr.NewItem("testkey2", []byte("testvalue2"), "protobuf") + if err != nil { + t.Errorf("failed to create new item: %v", err) + return + } + + records, err := kr.List() + if err != nil { + t.Errorf("failed to list items: %v", err) + return + } + if len(records) != 2 { + t.Errorf("expected 2 items, got %d", len(records)) + } + + keys := make(map[string]struct{}) + for _, record := range records { + keys[record.Key] = struct{}{} + } + if _, ok := keys["testkey1"]; !ok { + t.Errorf("expected key 'testkey1' to be present") + } + if _, ok := keys["testkey2"]; !ok { + t.Errorf("expected key 'testkey2' to be present") + } + }) + + t.Run("Delete", func(t *testing.T) { + err := kr.Delete("testkey1") + if err != nil { + t.Errorf("failed to delete item: %v", err) + return + } + + _, err = kr.Get("testkey1") + if err == nil { + t.Errorf("expected error for deleted key, but got nil") + } + + err = kr.Delete("nonexistent") + if err == nil { + t.Errorf("expected error for nonexistent key, but got nil") + } + }) +} diff --git a/crypto-provider/pkg/keyring/record.go b/crypto-provider/pkg/keyring/record.go new file mode 100644 index 0000000..b1fb28a --- /dev/null +++ b/crypto-provider/pkg/keyring/record.go @@ -0,0 +1,37 @@ +package keyring + +import "github.com/99designs/keyring" + +// Record a generic wrapper for keyring.Item +type Record struct { + Key string + Data []byte + CodecType string +} + +// NewRecord creates a new Record +func NewRecord(name string, data []byte, codecType string) *Record { + return &Record{ + Key: name, + Data: data, + CodecType: codecType, + } +} + +// ToItem converts the Record to a keyring.Item +func (r *Record) ToItem() keyring.Item { + return keyring.Item{ + Key: r.Key, + Data: r.Data, + Description: r.CodecType, + } +} + +// FromItem creates a Record from a keyring.Item +func FromItem(item keyring.Item) *Record { + return &Record{ + Key: item.Key, + Data: item.Data, + CodecType: item.Description, + } +} diff --git a/crypto-provider/pkg/wallet/wallet.go b/crypto-provider/pkg/wallet/wallet.go new file mode 100644 index 0000000..6589168 --- /dev/null +++ b/crypto-provider/pkg/wallet/wallet.go @@ -0,0 +1,168 @@ +package wallet + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/cosmos/crypto-provider/cmd/register" + "github.com/cosmos/crypto-provider/pkg/components" + "github.com/cosmos/crypto-provider/pkg/factory" + "github.com/cosmos/crypto-provider/pkg/keyring" +) + +// Wallet interface defines the operations that can be performed on a wallet. +type Wallet interface { + // NewCryptoProvider creates a new CryptoProvider and stores it in the Wallet + NewCryptoProvider(providerType string, source components.BuildSource) error + + // GetCryptoProvider retrieves a CryptoProvider from the Wallet. + GetCryptoProvider(id string) (components.CryptoProvider, error) + + // RetrieveCryptoProviderByAddress retrieves a CryptoProvider from the Wallet using its formatted address. + RetrieveCryptoProviderByAddress(address string) (components.CryptoProvider, error) + + // ListProviders returns a list of all stored CryptoProvider UIDs. + ListProviders() ([]string, error) + + // DeleteProvider removes a CryptoProvider from the Wallet. + DeleteProvider(id string) error + + // GetProviderMetadata retrieves the metadata of a stored CryptoProvider. + GetProviderMetadata(id string) (*components.ProviderMetadata, error) +} + +// KeyringWallet implements the Wallet interface using a Keyring backend. +type KeyringWallet struct { + kr keyring.Keyring + factory *factory.Factory + addressFormatter components.AddressFormatter +} + +// NewKeyringWallet creates a new KeyringWallet with the specified parameters. +func NewKeyringWallet(appName, backend, rootDir string, addressFormatter components.AddressFormatter) (Wallet, error) { + kr, err := keyring.NewKeyring(appName, backend, rootDir, os.Stdin) + if err != nil { + return nil, fmt.Errorf("failed to create keyring: %w", err) + } + + // Init register + register.Init() + + return &KeyringWallet{ + kr: kr, + factory: factory.GetGlobalFactory(), + addressFormatter: addressFormatter, + }, nil +} + +func (w *KeyringWallet) NewCryptoProvider(providerType string, source components.BuildSource) error { + provider, err := w.factory.CreateCryptoProvider(providerType, source) + if err != nil { + return err + } + + err = w.StoreCryptoProvider(provider.Metadata().Name, provider) + if err != nil { + return err + } + + fmt.Printf("CryptoProvider of type '%s' created with PublicKey: %s\n", providerType, provider.Metadata().PublicKey) + return nil +} + +// StoreCryptoProvider stores a CryptoProvider in the Keyring. +func (w *KeyringWallet) StoreCryptoProvider(uid string, provider components.CryptoProvider) error { + metadata := provider.Metadata() + data, err := json.Marshal(metadata) + if err != nil { + return fmt.Errorf("failed to marshal CryptoProvider metadata: %w", err) + } + + _, err = w.kr.NewItem(uid, data, "json") + if err != nil { + return fmt.Errorf("failed to store CryptoProvider: %w", err) + } + + return nil +} + +// GetCryptoProvider retrieves a CryptoProvider from the Keyring. +func (w *KeyringWallet) GetCryptoProvider(uid string) (components.CryptoProvider, error) { + record, err := w.kr.Get(uid) + if err != nil { + return nil, fmt.Errorf("failed to get record: %w", err) + } + + providerMeta, err := components.FromRecord(record) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal provider metadata: %w", err) + } + + provider, err := w.factory.CreateCryptoProvider(providerMeta.Type, components.BuildSourceMetadata{Metadata: *providerMeta}) + if err != nil { + return nil, fmt.Errorf("failed to create CryptoProvider from JSON: %w", err) + } + + return provider, nil +} + +// RetrieveCryptoProviderByAddress retrieves a CryptoProvider from the Wallet using its formatted address. +func (w *KeyringWallet) RetrieveCryptoProviderByAddress(address string) (components.CryptoProvider, error) { + providers, err := w.ListProviders() + if err != nil { + return nil, fmt.Errorf("failed to list providers: %w", err) + } + + for _, uid := range providers { + metadata, err := w.GetProviderMetadata(uid) + if err != nil { + continue + } + + formattedAddress, err := w.addressFormatter.FormatAddress([]byte(metadata.PublicKey)) + if err != nil { + fmt.Printf("failed to format address: %v\n", err) + continue + } + if formattedAddress == address { + return w.GetCryptoProvider(uid) + } + } + + return nil, fmt.Errorf("no provider found with address: %s", address) +} + +// ListProviders returns a list of all stored CryptoProvider UIDs. +func (w *KeyringWallet) ListProviders() ([]string, error) { + records, err := w.kr.List() + if err != nil { + return nil, fmt.Errorf("failed to list providers: %w", err) + } + + uids := make([]string, len(records)) + for i, record := range records { + uids[i] = record.Key + } + + return uids, nil +} + +// DeleteProvider removes a CryptoProvider from the Keyring. +func (w *KeyringWallet) DeleteProvider(uid string) error { + err := w.kr.Delete(uid) + if err != nil { + return fmt.Errorf("failed to delete provider: %w", err) + } + return nil +} + +// GetProviderMetadata retrieves the metadata of a stored CryptoProvider. +func (w *KeyringWallet) GetProviderMetadata(uid string) (*components.ProviderMetadata, error) { + record, err := w.kr.Get(uid) + if err != nil { + return nil, fmt.Errorf("failed to get provider metadata: %w", err) + } + + return components.FromRecord(record) +} diff --git a/crypto-provider/testdata/file_1.json b/crypto-provider/testdata/file_1.json new file mode 100644 index 0000000..014ad68 --- /dev/null +++ b/crypto-provider/testdata/file_1.json @@ -0,0 +1,9 @@ +{ + "Name": "Demo_File_Provider_1", + "Type": "file", + "Version": "1.0.0", + "PublicKey": "ikbdrUbkZJXeTqLykGi+YNxq8JT9M0x+Ke95WKZNOfs=", + "Config": { + "FilePath": "testdata/key.json" + } +} diff --git a/crypto-provider/testdata/key.json b/crypto-provider/testdata/key.json new file mode 100644 index 0000000..b4ae29b --- /dev/null +++ b/crypto-provider/testdata/key.json @@ -0,0 +1,6 @@ +{ + "priv_key": { + "type": "Ed25519", + "value": "8g7n4j2wR9oBpCDcOAlJ4MaX9jWXDxGRM/c7yFTCjmmKRt2tRuRkld5OovKQaL5g3GrwlP0zTH4p73lYpk05+w==" + } + } \ No newline at end of file