diff --git a/Cargo.lock b/Cargo.lock index bec8dee1a49c43..d29e2276929aba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7432,6 +7432,15 @@ dependencies = [ "solana-decode-error", ] +[[package]] +name = "solana-presigner" +version = "2.2.0" +dependencies = [ + "solana-pubkey", + "solana-signature", + "solana-signer", +] + [[package]] name = "solana-program" version = "2.2.0" @@ -8056,11 +8065,9 @@ dependencies = [ "curve25519-dalek 4.1.3", "digest 0.10.7", "ed25519-dalek", - "ed25519-dalek-bip32", "generic-array 0.14.7", "getrandom 0.1.16", "hex", - "hmac 0.12.1", "itertools 0.12.1", "js-sys", "lazy_static", @@ -8070,7 +8077,6 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "pbkdf2 0.11.0", "qualifier_attr", "rand 0.7.3", "rand 0.8.5", @@ -8097,6 +8103,7 @@ dependencies = [ "solana-native-token", "solana-packet", "solana-precompile-error", + "solana-presigner", "solana-program", "solana-program-memory", "solana-pubkey", @@ -8104,9 +8111,12 @@ dependencies = [ "solana-sdk", "solana-sdk-macro", "solana-secp256k1-recover", + "solana-seed-derivable", + "solana-seed-phrase", "solana-serde-varint", "solana-short-vec", "solana-signature", + "solana-signer", "solana-time-utils", "solana-transaction-error", "static_assertions", @@ -8145,6 +8155,29 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" +[[package]] +name = "solana-seed-derivable" +version = "2.2.0" +dependencies = [ + "ed25519-dalek", + "ed25519-dalek-bip32", + "solana-derivation-path", + "solana-seed-phrase", + "solana-signer", +] + +[[package]] +name = "solana-seed-phrase" +version = "2.2.0" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.11.0", + "sha2 0.10.8", + "solana-seed-phrase", + "solana-signer", + "tiny-bip39", +] + [[package]] name = "solana-send-transaction-service" version = "2.2.0" @@ -8223,6 +8256,22 @@ dependencies = [ "solana-sanitize", ] +[[package]] +name = "solana-signer" +version = "2.2.0" +dependencies = [ + "bs58", + "ed25519-dalek", + "rand 0.7.3", + "serde_json", + "solana-pubkey", + "solana-signature", + "solana-signer", + "solana-transaction-error", + "static_assertions", + "wasm-bindgen", +] + [[package]] name = "solana-slot-hashes" version = "2.2.0" @@ -9075,7 +9124,10 @@ dependencies = [ "solana-derivation-path", "solana-instruction", "solana-pubkey", - "solana-sdk", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature", + "solana-signer", "subtle", "thiserror", "tiny-bip39", diff --git a/Cargo.toml b/Cargo.toml index 52d36f3b238ffb..7a062effef54db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,6 +130,7 @@ members = [ "sdk/package-metadata-macro", "sdk/packet", "sdk/precompile-error", + "sdk/presigner", "sdk/program", "sdk/program-entrypoint", "sdk/program-error", @@ -139,10 +140,13 @@ members = [ "sdk/pubkey", "sdk/rent", "sdk/sanitize", + "sdk/seed-derivable", + "sdk/seed-phrase", "sdk/serde-varint", "sdk/serialize-utils", "sdk/sha256-hasher", "sdk/signature", + "sdk/signer", "sdk/slot-hashes", "sdk/slot-history", "sdk/stable-layout", @@ -359,6 +363,7 @@ quinn = "0.11.4" quinn-proto = "0.11.7" quote = "1.0" rand = "0.8.5" +rand0-7 = { package = "rand", version = "0.7" } rand_chacha = "0.3.1" rayon = "1.10.0" reed-solomon-erasure = "6.0.0" @@ -470,6 +475,7 @@ solana-perf = { path = "perf", version = "=2.2.0" } solana-poh = { path = "poh", version = "=2.2.0" } solana-poseidon = { path = "poseidon", version = "=2.2.0" } solana-precompile-error = { path = "sdk/precompile-error", version = "=2.2.0" } +solana-presigner = { path = "sdk/presigner", version = "=2.2.0" } solana-program = { path = "sdk/program", version = "=2.2.0", default-features = false } solana-program-error = { path = "sdk/program-error", version = "=2.2.0" } solana-program-memory = { path = "sdk/program-memory", version = "=2.2.0" } @@ -484,10 +490,13 @@ solana-rayon-threadlimit = { path = "rayon-threadlimit", version = "=2.2.0" } solana-remote-wallet = { path = "remote-wallet", version = "=2.2.0", default-features = false } solana-rent = { path = "sdk/rent", version = "=2.2.0", default-features = false } solana-sanitize = { path = "sdk/sanitize", version = "=2.2.0" } +solana-seed-derivable = { path = "sdk/seed-derivable", version = "=2.2.0" } +solana-seed-phrase = { path = "sdk/seed-phrase", version = "=2.2.0" } solana-serde-varint = { path = "sdk/serde-varint", version = "=2.2.0" } solana-serialize-utils = { path = "sdk/serialize-utils", version = "=2.2.0" } solana-sha256-hasher = { path = "sdk/sha256-hasher", version = "=2.2.0" } solana-signature = { path = "sdk/signature", version = "=2.2.0", default-features = false } +solana-signer = { path = "sdk/signer", version = "=2.2.0" } solana-slot-hashes = { path = "sdk/slot-hashes", version = "=2.2.0" } solana-slot-history = { path = "sdk/slot-history", version = "=2.2.0" } solana-time-utils = { path = "sdk/time-utils", version = "=2.2.0" } diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 98dd73792c191a..34f1cdf15edbe7 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -5803,6 +5803,15 @@ dependencies = [ "solana-decode-error", ] +[[package]] +name = "solana-presigner" +version = "2.2.0" +dependencies = [ + "solana-pubkey", + "solana-signature", + "solana-signer", +] + [[package]] name = "solana-program" version = "2.2.0" @@ -6807,9 +6816,7 @@ dependencies = [ "chrono", "digest 0.10.7", "ed25519-dalek", - "ed25519-dalek-bip32", "getrandom 0.1.14", - "hmac 0.12.1", "itertools 0.12.1", "js-sys", "lazy_static", @@ -6819,7 +6826,6 @@ dependencies = [ "num-derive", "num-traits", "num_enum", - "pbkdf2 0.11.0", "qualifier_attr", "rand 0.7.3", "rand 0.8.5", @@ -6843,15 +6849,19 @@ dependencies = [ "solana-native-token", "solana-packet", "solana-precompile-error", + "solana-presigner", "solana-program", "solana-program-memory", "solana-pubkey", "solana-sanitize", "solana-sdk-macro", "solana-secp256k1-recover", + "solana-seed-derivable", + "solana-seed-phrase", "solana-serde-varint", "solana-short-vec", "solana-signature", + "solana-signer", "solana-time-utils", "solana-transaction-error", "thiserror", @@ -6884,6 +6894,27 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "468aa43b7edb1f9b7b7b686d5c3aeb6630dc1708e86e31343499dd5c4d775183" +[[package]] +name = "solana-seed-derivable" +version = "2.2.0" +dependencies = [ + "ed25519-dalek", + "ed25519-dalek-bip32", + "solana-derivation-path", + "solana-seed-phrase", + "solana-signer", +] + +[[package]] +name = "solana-seed-phrase" +version = "2.2.0" +dependencies = [ + "hmac 0.12.1", + "pbkdf2 0.11.0", + "sha2 0.10.8", + "solana-signer", +] + [[package]] name = "solana-send-transaction-service" version = "2.2.0" @@ -6944,6 +6975,19 @@ dependencies = [ "solana-sanitize", ] +[[package]] +name = "solana-signer" +version = "2.2.0" +dependencies = [ + "bs58", + "ed25519-dalek", + "rand 0.7.3", + "solana-pubkey", + "solana-signature", + "solana-transaction-error", + "wasm-bindgen", +] + [[package]] name = "solana-slot-hashes" version = "2.2.0" @@ -7512,7 +7556,10 @@ dependencies = [ "solana-derivation-path", "solana-instruction", "solana-pubkey", - "solana-sdk", + "solana-seed-derivable", + "solana-seed-phrase", + "solana-signature", + "solana-signer", "subtle", "thiserror", "zeroize", diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index 3d8b1ac1725267..43817ef958c792 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -28,14 +28,17 @@ full = [ "rand0-7", "serde_json", "solana-signature", + "solana-signer", "ed25519-dalek", - "ed25519-dalek-bip32", "libsecp256k1", "sha3", "solana-commitment-config", "digest", "solana-pubkey/rand", "dep:solana-precompile-error", + "dep:solana-presigner", + "dep:solana-seed-derivable", + "dep:solana-seed-phrase", "dep:solana-transaction-error" ] borsh = ["dep:borsh", "solana-program/borsh", "solana-secp256k1-recover/borsh"] @@ -65,12 +68,10 @@ chrono = { workspace = true, features = ["alloc"], optional = true } curve25519-dalek = { workspace = true, optional = true } digest = { workspace = true, optional = true } ed25519-dalek = { workspace = true, optional = true } -ed25519-dalek-bip32 = { workspace = true, optional = true } generic-array = { workspace = true, features = [ "serde", "more_lengths", ], optional = true } -hmac = { workspace = true } itertools = { workspace = true } lazy_static = { workspace = true } libsecp256k1 = { workspace = true, optional = true, features = ["hmac"] } @@ -79,10 +80,9 @@ memmap2 = { workspace = true, optional = true } num-derive = { workspace = true } num-traits = { workspace = true } num_enum = { workspace = true } -pbkdf2 = { workspace = true } qualifier_attr = { workspace = true, optional = true } rand = { workspace = true, optional = true } -rand0-7 = { package = "rand", version = "0.7", optional = true } +rand0-7 = { workspace = true, optional = true } serde = { workspace = true } serde_bytes = { workspace = true } serde_derive = { workspace = true } @@ -109,12 +109,15 @@ solana-instruction = { workspace = true } solana-native-token = { workspace = true } solana-packet = { workspace = true, features = ["bincode", "serde"] } solana-precompile-error = { workspace = true, optional = true } +solana-presigner = { workspace = true, optional = true } solana-program = { workspace = true } solana-program-memory = { workspace = true } solana-pubkey = { workspace = true, default-features = false, features = ["std"] } solana-sanitize = { workspace = true } solana-sdk-macro = { workspace = true } solana-secp256k1-recover = { workspace = true } +solana-seed-derivable = { workspace = true, optional = true } +solana-seed-phrase = { workspace = true, optional = true } solana-serde-varint = { workspace = true } solana-short-vec = { workspace = true } solana-signature = { workspace = true, features = [ @@ -123,6 +126,7 @@ solana-signature = { workspace = true, features = [ "std", "verify", ], optional = true } +solana-signer = { workspace = true, optional = true, features = ["keypair"] } solana-time-utils = { workspace = true } solana-transaction-error = { workspace = true, features = ["serde"], optional = true } thiserror = { workspace = true } diff --git a/sdk/presigner/Cargo.toml b/sdk/presigner/Cargo.toml new file mode 100644 index 00000000000000..14a7cb09a26e37 --- /dev/null +++ b/sdk/presigner/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "solana-presigner" +description = "A Solana `Signer` implementation representing an externally-constructed `Signature`." +documentation = "https://docs.rs/solana-presigner" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +solana-pubkey = { workspace = true } +solana-signature = { workspace = true, features = ["verify"] } +solana-signer = { workspace = true } + +[dev-dependencies] +solana-signer = { workspace = true, features = ["keypair"] } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/sdk/src/signer/presigner.rs b/sdk/presigner/src/lib.rs similarity index 84% rename from sdk/src/signer/presigner.rs rename to sdk/presigner/src/lib.rs index 649bd3c3101d95..1b2ea5e34ee362 100644 --- a/sdk/src/signer/presigner.rs +++ b/sdk/presigner/src/lib.rs @@ -1,12 +1,8 @@ -#![cfg(feature = "full")] - +pub use solana_signer::PresignerError; use { - crate::{ - pubkey::Pubkey, - signature::Signature, - signer::{Signer, SignerError}, - }, - thiserror::Error, + solana_pubkey::Pubkey, + solana_signature::Signature, + solana_signer::{Signer, SignerError}, }; /// A `Signer` implementation that represents a `Signature` that has been @@ -28,12 +24,6 @@ impl Presigner { } } -#[derive(Debug, Error, PartialEq, Eq)] -pub enum PresignerError { - #[error("pre-generated signature cannot verify data")] - VerificationFailure, -} - impl Signer for Presigner { fn try_pubkey(&self) -> Result { Ok(self.pubkey) @@ -63,7 +53,7 @@ where #[cfg(test)] mod tests { - use {super::*, crate::signer::keypair::keypair_from_seed}; + use {super::*, solana_signer::keypair::keypair_from_seed}; #[test] fn test_presigner() { diff --git a/sdk/seed-derivable/Cargo.toml b/sdk/seed-derivable/Cargo.toml new file mode 100644 index 00000000000000..5133b01afbbf41 --- /dev/null +++ b/sdk/seed-derivable/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "solana-seed-derivable" +description = "Solana trait defining the interface by which keys are derived." +documentation = "https://docs.rs/solana-seed-derivable" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +ed25519-dalek = { workspace = true } +ed25519-dalek-bip32 = { workspace = true } +solana-derivation-path = { workspace = true } +solana-seed-phrase = { workspace = true, features = ["keypair"] } +solana-signer = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/sdk/seed-derivable/src/lib.rs b/sdk/seed-derivable/src/lib.rs new file mode 100644 index 00000000000000..cf2e1ac84be98d --- /dev/null +++ b/sdk/seed-derivable/src/lib.rs @@ -0,0 +1,66 @@ +//! The interface by which keys are derived. +use { + ed25519_dalek_bip32::Error as Bip32Error, + solana_derivation_path::DerivationPath, + solana_seed_phrase::keypair_from_seed_phrase_and_passphrase, + solana_signer::keypair::{keypair_from_seed, Keypair}, + std::error, +}; + +/// The `SeedDerivable` trait defines the interface by which cryptographic keys/keypairs are +/// derived from byte seeds, derivation paths, and passphrases. +pub trait SeedDerivable: Sized { + fn from_seed(seed: &[u8]) -> Result>; + fn from_seed_and_derivation_path( + seed: &[u8], + derivation_path: Option, + ) -> Result>; + fn from_seed_phrase_and_passphrase( + seed_phrase: &str, + passphrase: &str, + ) -> Result>; +} + +impl SeedDerivable for Keypair { + fn from_seed(seed: &[u8]) -> Result> { + keypair_from_seed(seed) + } + + fn from_seed_and_derivation_path( + seed: &[u8], + derivation_path: Option, + ) -> Result> { + keypair_from_seed_and_derivation_path(seed, derivation_path) + } + + fn from_seed_phrase_and_passphrase( + seed_phrase: &str, + passphrase: &str, + ) -> Result> { + keypair_from_seed_phrase_and_passphrase(seed_phrase, passphrase) + } +} + +/// Generates a Keypair using Bip32 Hierarchical Derivation if derivation-path is provided; +/// otherwise generates the base Bip44 Solana keypair from the seed +pub fn keypair_from_seed_and_derivation_path( + seed: &[u8], + derivation_path: Option, +) -> Result> { + let derivation_path = derivation_path.unwrap_or_default(); + bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into()) +} + +/// Generates a Keypair using Bip32 Hierarchical Derivation +fn bip32_derived_keypair( + seed: &[u8], + derivation_path: DerivationPath, +) -> Result { + let extended = ed25519_dalek_bip32::ExtendedSecretKey::from_seed(seed) + .and_then(|extended| extended.derive(&derivation_path))?; + let extended_public_key = extended.public_key(); + Ok(Keypair::from(ed25519_dalek::Keypair { + secret: extended.secret_key, + public: extended_public_key, + })) +} diff --git a/sdk/seed-phrase/Cargo.toml b/sdk/seed-phrase/Cargo.toml new file mode 100644 index 00000000000000..31ae3cb8a10a93 --- /dev/null +++ b/sdk/seed-phrase/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "solana-seed-phrase" +description = "Solana functions for generating keypairs from seed phrases." +documentation = "https://docs.rs/solana-seed-phrase" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +hmac = { workspace = true } +pbkdf2 = { workspace = true } +sha2 = { workspace = true } +solana-signer = { workspace = true } + +[dev-dependencies] +solana-seed-phrase = { path = ".", features = ["dev-context-only-utils"] } +tiny-bip39 = { workspace = true } + +[features] +dev-context-only-utils = ["keypair"] +keypair = ["solana-signer/keypair"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/sdk/seed-phrase/src/lib.rs b/sdk/seed-phrase/src/lib.rs new file mode 100644 index 00000000000000..0766035e38e73b --- /dev/null +++ b/sdk/seed-phrase/src/lib.rs @@ -0,0 +1,54 @@ +//! Functions for generating keypairs from seed phrases. +use hmac::Hmac; +#[cfg(feature = "keypair")] +use solana_signer::keypair::{keypair_from_seed, Keypair}; + +pub fn generate_seed_from_seed_phrase_and_passphrase( + seed_phrase: &str, + passphrase: &str, +) -> Vec { + const PBKDF2_ROUNDS: u32 = 2048; + const PBKDF2_BYTES: usize = 64; + + let salt = format!("mnemonic{passphrase}"); + + let mut seed = vec![0u8; PBKDF2_BYTES]; + pbkdf2::pbkdf2::>( + seed_phrase.as_bytes(), + salt.as_bytes(), + PBKDF2_ROUNDS, + &mut seed, + ); + seed +} + +#[cfg(feature = "keypair")] +pub fn keypair_from_seed_phrase_and_passphrase( + seed_phrase: &str, + passphrase: &str, +) -> Result> { + keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase( + seed_phrase, + passphrase, + )) +} + +#[cfg(test)] +mod tests { + use { + super::*, + bip39::{Language, Mnemonic, MnemonicType, Seed}, + solana_signer::Signer, + }; + + #[test] + fn test_keypair_from_seed_phrase_and_passphrase() { + let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); + let passphrase = "42"; + let seed = Seed::new(&mnemonic, passphrase); + let expected_keypair = keypair_from_seed(seed.as_bytes()).unwrap(); + let keypair = + keypair_from_seed_phrase_and_passphrase(mnemonic.phrase(), passphrase).unwrap(); + assert_eq!(keypair.pubkey(), expected_keypair.pubkey()); + } +} diff --git a/sdk/signer/Cargo.toml b/sdk/signer/Cargo.toml new file mode 100644 index 00000000000000..3cb35b92755bcb --- /dev/null +++ b/sdk/signer/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "solana-signer" +description = "Solana abstractions and implementations for transaction signers." +documentation = "https://docs.rs/solana-signer" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[dependencies] +bs58 = { workspace = true, features = ["std"], optional = true } +ed25519-dalek = { workspace = true, optional = true } +rand0-7 = { workspace = true, optional = true } +solana-pubkey = { workspace = true } +solana-signature = { workspace = true } +solana-transaction-error = { workspace = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { workspace = true } + +[dev-dependencies] +serde_json = { workspace = true } +solana-signer = { path = ".", features = ["dev-context-only-utils"] } +static_assertions = { workspace = true } + +[features] +dev-context-only-utils = ["keypair"] +keypair = [ + "dep:bs58", + "dep:ed25519-dalek", + "dep:rand0-7", +] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] diff --git a/sdk/signer/src/keypair.rs b/sdk/signer/src/keypair.rs new file mode 100644 index 00000000000000..f0062a9f2c135b --- /dev/null +++ b/sdk/signer/src/keypair.rs @@ -0,0 +1,377 @@ +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; +use { + crate::{EncodableKey, EncodableKeypair, Signer, SignerError}, + ed25519_dalek::Signer as DalekSigner, + rand0_7::{rngs::OsRng, CryptoRng, RngCore}, + solana_pubkey::Pubkey, + solana_signature::Signature, + std::{ + error, + io::{Read, Write}, + path::Path, + }, +}; + +/// A vanilla Ed25519 key pair +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +#[derive(Debug)] +pub struct Keypair(ed25519_dalek::Keypair); + +impl Keypair { + /// Can be used for generating a Keypair without a dependency on `rand` types + pub const SECRET_KEY_LENGTH: usize = 32; + + /// Constructs a new, random `Keypair` using a caller-provided RNG + pub fn generate(csprng: &mut R) -> Self + where + R: CryptoRng + RngCore, + { + Self(ed25519_dalek::Keypair::generate(csprng)) + } + + /// Constructs a new, random `Keypair` using `OsRng` + pub fn new() -> Self { + let mut rng = OsRng; + Self::generate(&mut rng) + } + + /// Recovers a `Keypair` from a byte array + pub fn from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < ed25519_dalek::KEYPAIR_LENGTH { + return Err(ed25519_dalek::SignatureError::from_source(String::from( + "candidate keypair byte array is too short", + ))); + } + let secret = + ed25519_dalek::SecretKey::from_bytes(&bytes[..ed25519_dalek::SECRET_KEY_LENGTH])?; + let public = + ed25519_dalek::PublicKey::from_bytes(&bytes[ed25519_dalek::SECRET_KEY_LENGTH..])?; + let expected_public = ed25519_dalek::PublicKey::from(&secret); + (public == expected_public) + .then_some(Self(ed25519_dalek::Keypair { secret, public })) + .ok_or(ed25519_dalek::SignatureError::from_source(String::from( + "keypair bytes do not specify same pubkey as derived from their secret key", + ))) + } + + /// Returns this `Keypair` as a byte array + pub fn to_bytes(&self) -> [u8; 64] { + self.0.to_bytes() + } + + /// Recovers a `Keypair` from a base58-encoded string + pub fn from_base58_string(s: &str) -> Self { + let mut buf = [0u8; ed25519_dalek::KEYPAIR_LENGTH]; + bs58::decode(s).onto(&mut buf).unwrap(); + Self::from_bytes(&buf).unwrap() + } + + /// Returns this `Keypair` as a base58-encoded string + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.0.to_bytes()).into_string() + } + + /// Gets this `Keypair`'s SecretKey + pub fn secret(&self) -> &ed25519_dalek::SecretKey { + &self.0.secret + } + + /// Allows Keypair cloning + /// + /// Note that the `Clone` trait is intentionally unimplemented because making a + /// second copy of sensitive secret keys in memory is usually a bad idea. + /// + /// Only use this in tests or when strictly required. Consider using [`std::sync::Arc`] + /// instead. + pub fn insecure_clone(&self) -> Self { + Self(ed25519_dalek::Keypair { + // This will never error since self is a valid keypair + secret: ed25519_dalek::SecretKey::from_bytes(self.0.secret.as_bytes()).unwrap(), + public: self.0.public, + }) + } +} + +#[cfg(target_arch = "wasm32")] +#[allow(non_snake_case)] +#[wasm_bindgen] +impl Keypair { + /// Create a new `Keypair ` + #[wasm_bindgen(constructor)] + pub fn constructor() -> Keypair { + Keypair::new() + } + + /// Convert a `Keypair` to a `Uint8Array` + pub fn toBytes(&self) -> Box<[u8]> { + self.to_bytes().into() + } + + /// Recover a `Keypair` from a `Uint8Array` + pub fn fromBytes(bytes: &[u8]) -> Result { + Keypair::from_bytes(bytes).map_err(|e| e.to_string().into()) + } + + /// Return the `Pubkey` for this `Keypair` + #[wasm_bindgen(js_name = pubkey)] + pub fn js_pubkey(&self) -> Pubkey { + // `wasm_bindgen` does not support traits (`Signer) yet + self.pubkey() + } +} + +impl From for Keypair { + fn from(value: ed25519_dalek::Keypair) -> Self { + Self(value) + } +} + +#[cfg(test)] +static_assertions::const_assert_eq!(Keypair::SECRET_KEY_LENGTH, ed25519_dalek::SECRET_KEY_LENGTH); + +impl Signer for Keypair { + #[inline] + fn pubkey(&self) -> Pubkey { + Pubkey::from(self.0.public.to_bytes()) + } + + fn try_pubkey(&self) -> Result { + Ok(self.pubkey()) + } + + fn sign_message(&self, message: &[u8]) -> Signature { + Signature::from(self.0.sign(message).to_bytes()) + } + + fn try_sign_message(&self, message: &[u8]) -> Result { + Ok(self.sign_message(message)) + } + + fn is_interactive(&self) -> bool { + false + } +} + +impl PartialEq for Keypair +where + T: Signer, +{ + fn eq(&self, other: &T) -> bool { + self.pubkey() == other.pubkey() + } +} + +impl EncodableKey for Keypair { + fn read(reader: &mut R) -> Result> { + read_keypair(reader) + } + + fn write(&self, writer: &mut W) -> Result> { + write_keypair(self, writer) + } +} + +impl EncodableKeypair for Keypair { + type Pubkey = Pubkey; + + /// Returns the associated pubkey. Use this function specifically for settings that involve + /// reading or writing pubkeys. For other settings, use `Signer::pubkey()` instead. + fn encodable_pubkey(&self) -> Self::Pubkey { + self.pubkey() + } +} + +/// Reads a JSON-encoded `Keypair` from a `Reader` implementor +pub fn read_keypair(reader: &mut R) -> Result> { + let mut buffer = String::new(); + reader.read_to_string(&mut buffer)?; + let trimmed = buffer.trim(); + if !trimmed.starts_with('[') || !trimmed.ends_with(']') { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "Input must be a JSON array", + ) + .into()); + } + // we already checked that the string has at least two chars, + // so 1..trimmed.len() - 1 won't be out of bounds + #[allow(clippy::arithmetic_side_effects)] + let contents = &trimmed[1..trimmed.len() - 1]; + let elements_vec: Vec<&str> = contents.split(',').map(|s| s.trim()).collect(); + let len = elements_vec.len(); + let elements: [&str; ed25519_dalek::KEYPAIR_LENGTH] = + elements_vec.try_into().map_err(|_| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!( + "Expected {} elements, found {}", + ed25519_dalek::KEYPAIR_LENGTH, + len + ), + ) + })?; + let mut out = [0u8; ed25519_dalek::KEYPAIR_LENGTH]; + for (idx, element) in elements.into_iter().enumerate() { + let parsed: u8 = element.parse()?; + out[idx] = parsed; + } + Keypair::from_bytes(&out) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()).into()) +} + +/// Reads a `Keypair` from a file +pub fn read_keypair_file>(path: F) -> Result> { + Keypair::read_from_file(path) +} + +/// Writes a `Keypair` to a `Write` implementor with JSON-encoding +pub fn write_keypair( + keypair: &Keypair, + writer: &mut W, +) -> Result> { + let keypair_bytes = keypair.0.to_bytes(); + let mut result = Vec::with_capacity(64 * 4 + 2); // Estimate capacity: 64 numbers * (up to 3 digits + 1 comma) + 2 brackets + + result.push(b'['); // Opening bracket + + for (i, &num) in keypair_bytes.iter().enumerate() { + if i > 0 { + result.push(b','); // Comma separator for all elements except the first + } + + // Convert number to string and then to bytes + let num_str = num.to_string(); + result.extend_from_slice(num_str.as_bytes()); + } + + result.push(b']'); // Closing bracket + writer.write_all(&result)?; + let as_string = String::from_utf8(result)?; + Ok(as_string) +} + +/// Writes a `Keypair` to a file with JSON-encoding +pub fn write_keypair_file>( + keypair: &Keypair, + outfile: F, +) -> Result> { + keypair.write_to_file(outfile) +} + +/// Constructs a `Keypair` from caller-provided seed entropy +pub fn keypair_from_seed(seed: &[u8]) -> Result> { + if seed.len() < ed25519_dalek::SECRET_KEY_LENGTH { + return Err("Seed is too short".into()); + } + let secret = ed25519_dalek::SecretKey::from_bytes(&seed[..ed25519_dalek::SECRET_KEY_LENGTH]) + .map_err(|e| e.to_string())?; + let public = ed25519_dalek::PublicKey::from(&secret); + let dalek_keypair = ed25519_dalek::Keypair { secret, public }; + Ok(Keypair(dalek_keypair)) +} + +#[cfg(test)] +mod tests { + use { + super::*, + std::{ + fs::{self, File}, + mem, + }, + }; + + fn tmp_file_path(name: &str) -> String { + use std::env; + let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); + let keypair = Keypair::new(); + + format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey()) + } + + #[test] + fn test_write_keypair_file() { + let outfile = tmp_file_path("test_write_keypair_file.json"); + let serialized_keypair = write_keypair_file(&Keypair::new(), &outfile).unwrap(); + let keypair_vec: Vec = serde_json::from_str(&serialized_keypair).unwrap(); + assert!(Path::new(&outfile).exists()); + assert_eq!( + keypair_vec, + read_keypair_file(&outfile).unwrap().0.to_bytes().to_vec() + ); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + assert_eq!( + File::open(&outfile) + .expect("open") + .metadata() + .expect("metadata") + .permissions() + .mode() + & 0o777, + 0o600 + ); + } + + assert_eq!( + read_keypair_file(&outfile).unwrap().pubkey().as_ref().len(), + mem::size_of::() + ); + fs::remove_file(&outfile).unwrap(); + assert!(!Path::new(&outfile).exists()); + } + + #[test] + fn test_write_keypair_file_overwrite_ok() { + let outfile = tmp_file_path("test_write_keypair_file_overwrite_ok.json"); + + write_keypair_file(&Keypair::new(), &outfile).unwrap(); + write_keypair_file(&Keypair::new(), &outfile).unwrap(); + } + + #[test] + fn test_write_keypair_file_truncate() { + let outfile = tmp_file_path("test_write_keypair_file_truncate.json"); + + write_keypair_file(&Keypair::new(), &outfile).unwrap(); + read_keypair_file(&outfile).unwrap(); + + // Ensure outfile is truncated + { + let mut f = File::create(&outfile).unwrap(); + f.write_all(String::from_utf8([b'a'; 2048].to_vec()).unwrap().as_bytes()) + .unwrap(); + } + write_keypair_file(&Keypair::new(), &outfile).unwrap(); + read_keypair_file(&outfile).unwrap(); + } + + #[test] + fn test_keypair_from_seed() { + let good_seed = vec![0; 32]; + assert!(keypair_from_seed(&good_seed).is_ok()); + + let too_short_seed = vec![0; 31]; + assert!(keypair_from_seed(&too_short_seed).is_err()); + } + + #[test] + fn test_keypair() { + let keypair = keypair_from_seed(&[0u8; 32]).unwrap(); + let pubkey = keypair.pubkey(); + let data = [1u8]; + let sig = keypair.sign_message(&data); + + // Signer + assert_eq!(keypair.try_pubkey().unwrap(), pubkey); + assert_eq!(keypair.pubkey(), pubkey); + assert_eq!(keypair.try_sign_message(&data).unwrap(), sig); + assert_eq!(keypair.sign_message(&data), sig); + + // PartialEq + let keypair2 = keypair_from_seed(&[0u8; 32]).unwrap(); + assert_eq!(keypair, keypair2); + } +} diff --git a/sdk/signer/src/lib.rs b/sdk/signer/src/lib.rs new file mode 100644 index 00000000000000..06aa4ddf4c1276 --- /dev/null +++ b/sdk/signer/src/lib.rs @@ -0,0 +1,279 @@ +//! Abstractions and implementations for transaction signers. +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +use { + core::fmt, + solana_pubkey::Pubkey, + solana_signature::Signature, + solana_transaction_error::TransactionError, + std::{ + error, + fs::{self, File, OpenOptions}, + io::{Read, Write}, + ops::Deref, + path::Path, + }, +}; + +#[cfg(feature = "keypair")] +pub mod keypair; +pub mod null_signer; +pub mod signers; + +#[derive(Debug, PartialEq, Eq)] +pub enum PresignerError { + VerificationFailure, +} + +impl std::error::Error for PresignerError {} + +impl fmt::Display for PresignerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::VerificationFailure => f.write_str("pre-generated signature cannot verify data"), + } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub enum SignerError { + KeypairPubkeyMismatch, + NotEnoughSigners, + TransactionError(TransactionError), + Custom(String), + // Presigner-specific Errors + PresignerError(PresignerError), + // Remote Keypair-specific Errors + Connection(String), + InvalidInput(String), + NoDeviceFound, + Protocol(String), + UserCancel(String), + TooManySigners, +} + +impl std::error::Error for SignerError { + fn source(&self) -> ::core::option::Option<&(dyn std::error::Error + 'static)> { + match self { + Self::KeypairPubkeyMismatch => None, + Self::NotEnoughSigners => None, + Self::TransactionError(e) => Some(e), + Self::Custom(_) => None, + Self::PresignerError(e) => Some(e), + Self::Connection(_) => None, + Self::InvalidInput(_) => None, + Self::NoDeviceFound => None, + Self::Protocol(_) => None, + Self::UserCancel(_) => None, + Self::TooManySigners => None, + } + } +} +impl fmt::Display for SignerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SignerError::KeypairPubkeyMismatch => f.write_str("keypair-pubkey mismatch"), + SignerError::NotEnoughSigners => f.write_str("not enough signers"), + SignerError::TransactionError(_) => f.write_str("transaction error"), + SignerError::Custom(e) => write!(f, "custom error: {e}",), + SignerError::PresignerError(_) => f.write_str("presigner error"), + SignerError::Connection(e) => write!(f, "connection error: {e}",), + SignerError::InvalidInput(s) => write!(f, "invalid input: {s}",), + SignerError::NoDeviceFound => f.write_str("no device found"), + SignerError::Protocol(s) => { + write!(f, "{s}") + } + SignerError::UserCancel(s) => { + write!(f, "{s}") + } + SignerError::TooManySigners => f.write_str("too many signers"), + } + } +} + +impl From for SignerError { + fn from(source: TransactionError) -> Self { + SignerError::TransactionError(source) + } +} + +impl From for SignerError { + fn from(source: PresignerError) -> Self { + SignerError::PresignerError(source) + } +} + +/// The `Signer` trait declares operations that all digital signature providers +/// must support. It is the primary interface by which signers are specified in +/// `Transaction` signing interfaces +pub trait Signer { + /// Infallibly gets the implementor's public key. Returns the all-zeros + /// `Pubkey` if the implementor has none. + fn pubkey(&self) -> Pubkey { + self.try_pubkey().unwrap_or_default() + } + /// Fallibly gets the implementor's public key + fn try_pubkey(&self) -> Result; + /// Infallibly produces an Ed25519 signature over the provided `message` + /// bytes. Returns the all-zeros `Signature` if signing is not possible. + fn sign_message(&self, message: &[u8]) -> Signature { + self.try_sign_message(message).unwrap_or_default() + } + /// Fallibly produces an Ed25519 signature over the provided `message` bytes. + fn try_sign_message(&self, message: &[u8]) -> Result; + /// Whether the implementation requires user interaction to sign + fn is_interactive(&self) -> bool; +} + +/// This implements `Signer` for all ptr types - `Box/Rc/Arc/&/&mut` etc +impl> Signer for Container { + #[inline] + fn pubkey(&self) -> Pubkey { + self.deref().pubkey() + } + + fn try_pubkey(&self) -> Result { + self.deref().try_pubkey() + } + + fn sign_message(&self, message: &[u8]) -> Signature { + self.deref().sign_message(message) + } + + fn try_sign_message(&self, message: &[u8]) -> Result { + self.deref().try_sign_message(message) + } + + fn is_interactive(&self) -> bool { + self.deref().is_interactive() + } +} + +impl PartialEq for dyn Signer { + fn eq(&self, other: &dyn Signer) -> bool { + self.pubkey() == other.pubkey() + } +} + +impl Eq for dyn Signer {} + +impl std::fmt::Debug for dyn Signer { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(fmt, "Signer: {:?}", self.pubkey()) + } +} + +/// Removes duplicate signers while preserving order. O(n²) +pub fn unique_signers(signers: Vec<&dyn Signer>) -> Vec<&dyn Signer> { + let capacity = signers.len(); + let mut out = Vec::with_capacity(capacity); + let mut seen = std::collections::HashSet::with_capacity(capacity); + for signer in signers { + let pubkey = signer.pubkey(); + if !seen.contains(&pubkey) { + seen.insert(pubkey); + out.push(signer); + } + } + out +} + +/// The `EncodableKey` trait defines the interface by which cryptographic keys/keypairs are read, +/// written, and derived from sources. +pub trait EncodableKey: Sized { + fn read(reader: &mut R) -> Result>; + fn read_from_file>(path: F) -> Result> { + let mut file = File::open(path.as_ref())?; + Self::read(&mut file) + } + fn write(&self, writer: &mut W) -> Result>; + fn write_to_file>(&self, outfile: F) -> Result> { + let outfile = outfile.as_ref(); + + if let Some(outdir) = outfile.parent() { + fs::create_dir_all(outdir)?; + } + + let mut f = { + #[cfg(not(unix))] + { + OpenOptions::new() + } + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + OpenOptions::new().mode(0o600) + } + } + .write(true) + .truncate(true) + .create(true) + .open(outfile)?; + + self.write(&mut f) + } +} + +/// The `EncodableKeypair` trait extends `EncodableKey` for asymmetric keypairs, i.e. have +/// associated public keys. +pub trait EncodableKeypair: EncodableKey { + type Pubkey: ToString; + + /// Returns an encodable representation of the associated public key. + fn encodable_pubkey(&self) -> Self::Pubkey; +} + +#[cfg(test)] +mod tests { + use {super::*, crate::keypair::Keypair}; + + fn pubkeys(signers: &[&dyn Signer]) -> Vec { + signers.iter().map(|x| x.pubkey()).collect() + } + + #[test] + fn test_unique_signers() { + let alice = Keypair::new(); + let bob = Keypair::new(); + assert_eq!( + pubkeys(&unique_signers(vec![&alice, &bob, &alice])), + pubkeys(&[&alice, &bob]) + ); + } + + #[test] + fn test_containers() { + use std::{rc::Rc, sync::Arc}; + + struct Foo { + #[allow(unused)] + signer: S, + } + + fn foo(_s: impl Signer) {} + + let _arc_signer = Foo { + signer: Arc::new(Keypair::new()), + }; + foo(Arc::new(Keypair::new())); + + let _rc_signer = Foo { + signer: Rc::new(Keypair::new()), + }; + foo(Rc::new(Keypair::new())); + + let _ref_signer = Foo { + signer: &Keypair::new(), + }; + foo(Keypair::new()); + + let _box_signer = Foo { + signer: Box::new(Keypair::new()), + }; + foo(Box::new(Keypair::new())); + + let _signer = Foo { + signer: Keypair::new(), + }; + foo(Keypair::new()); + } +} diff --git a/sdk/src/signer/null_signer.rs b/sdk/signer/src/null_signer.rs similarity index 87% rename from sdk/src/signer/null_signer.rs rename to sdk/signer/src/null_signer.rs index 2e9508511832fd..b96eea7b89e752 100644 --- a/sdk/src/signer/null_signer.rs +++ b/sdk/signer/src/null_signer.rs @@ -1,9 +1,7 @@ -#![cfg(feature = "full")] - -use crate::{ - pubkey::Pubkey, - signature::Signature, - signer::{Signer, SignerError}, +use { + crate::{Signer, SignerError}, + solana_pubkey::Pubkey, + solana_signature::Signature, }; /// NullSigner - A `Signer` implementation that always produces `Signature::default()`. diff --git a/sdk/src/signer/signers.rs b/sdk/signer/src/signers.rs similarity index 96% rename from sdk/src/signer/signers.rs rename to sdk/signer/src/signers.rs index 5b41b5f93717f2..8859bbf59f9046 100644 --- a/sdk/src/signer/signers.rs +++ b/sdk/signer/src/signers.rs @@ -1,8 +1,7 @@ -#![cfg(feature = "full")] - -use crate::{ - pubkey::Pubkey, - signature::{Signature, Signer, SignerError}, +use { + crate::{Signer, SignerError}, + solana_pubkey::Pubkey, + solana_signature::Signature, }; /// Convenience trait for working with mixed collections of `Signer`s diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 4c4ead3010697e..323e0ba1d06352 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -35,8 +35,6 @@ // Allows macro expansion of `use ::solana_sdk::*` to work within this crate extern crate self as solana_sdk; -#[cfg(feature = "full")] -pub use signer::signers; #[cfg(feature = "full")] pub use solana_commitment_config as commitment_config; #[cfg(not(target_os = "solana"))] @@ -60,6 +58,8 @@ pub use solana_program::{ }; #[cfg(feature = "borsh")] pub use solana_program::{borsh, borsh0_10, borsh1}; +#[cfg(feature = "full")] +pub use solana_signer::signers; pub mod client; pub mod compute_budget; pub mod deserialize_utils; diff --git a/sdk/src/signer/keypair.rs b/sdk/src/signer/keypair.rs index ecd98ec3c7aa3f..f7f8c33fce93b6 100644 --- a/sdk/src/signer/keypair.rs +++ b/sdk/src/signer/keypair.rs @@ -1,387 +1,7 @@ -#![cfg(feature = "full")] - -#[cfg(target_arch = "wasm32")] -use wasm_bindgen::prelude::*; -use { - crate::{ - pubkey::Pubkey, - signature::Signature, - signer::{EncodableKey, EncodableKeypair, SeedDerivable, Signer, SignerError}, - }, - ed25519_dalek::Signer as DalekSigner, - ed25519_dalek_bip32::Error as Bip32Error, - hmac::Hmac, - rand0_7::{rngs::OsRng, CryptoRng, RngCore}, - solana_derivation_path::DerivationPath, - std::{ - error, - io::{Read, Write}, - path::Path, +pub use { + solana_seed_derivable::keypair_from_seed_and_derivation_path, + solana_seed_phrase::{ + generate_seed_from_seed_phrase_and_passphrase, keypair_from_seed_phrase_and_passphrase, }, + solana_signer::keypair::*, }; - -/// A vanilla Ed25519 key pair -#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] -#[derive(Debug)] -pub struct Keypair(ed25519_dalek::Keypair); - -impl Keypair { - /// Can be used for generating a Keypair without a dependency on `rand` types - pub const SECRET_KEY_LENGTH: usize = 32; - - /// Constructs a new, random `Keypair` using a caller-provided RNG - pub fn generate(csprng: &mut R) -> Self - where - R: CryptoRng + RngCore, - { - Self(ed25519_dalek::Keypair::generate(csprng)) - } - - /// Constructs a new, random `Keypair` using `OsRng` - pub fn new() -> Self { - let mut rng = OsRng; - Self::generate(&mut rng) - } - - /// Recovers a `Keypair` from a byte array - pub fn from_bytes(bytes: &[u8]) -> Result { - if bytes.len() < ed25519_dalek::KEYPAIR_LENGTH { - return Err(ed25519_dalek::SignatureError::from_source(String::from( - "candidate keypair byte array is too short", - ))); - } - let secret = - ed25519_dalek::SecretKey::from_bytes(&bytes[..ed25519_dalek::SECRET_KEY_LENGTH])?; - let public = - ed25519_dalek::PublicKey::from_bytes(&bytes[ed25519_dalek::SECRET_KEY_LENGTH..])?; - let expected_public = ed25519_dalek::PublicKey::from(&secret); - (public == expected_public) - .then_some(Self(ed25519_dalek::Keypair { secret, public })) - .ok_or(ed25519_dalek::SignatureError::from_source(String::from( - "keypair bytes do not specify same pubkey as derived from their secret key", - ))) - } - - /// Returns this `Keypair` as a byte array - pub fn to_bytes(&self) -> [u8; 64] { - self.0.to_bytes() - } - - /// Recovers a `Keypair` from a base58-encoded string - pub fn from_base58_string(s: &str) -> Self { - Self::from_bytes(&bs58::decode(s).into_vec().unwrap()).unwrap() - } - - /// Returns this `Keypair` as a base58-encoded string - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.0.to_bytes()).into_string() - } - - /// Gets this `Keypair`'s SecretKey - pub fn secret(&self) -> &ed25519_dalek::SecretKey { - &self.0.secret - } - - /// Allows Keypair cloning - /// - /// Note that the `Clone` trait is intentionally unimplemented because making a - /// second copy of sensitive secret keys in memory is usually a bad idea. - /// - /// Only use this in tests or when strictly required. Consider using [`std::sync::Arc`] - /// instead. - pub fn insecure_clone(&self) -> Self { - Self(ed25519_dalek::Keypair { - // This will never error since self is a valid keypair - secret: ed25519_dalek::SecretKey::from_bytes(self.0.secret.as_bytes()).unwrap(), - public: self.0.public, - }) - } -} - -#[cfg(test)] -static_assertions::const_assert_eq!(Keypair::SECRET_KEY_LENGTH, ed25519_dalek::SECRET_KEY_LENGTH); - -impl Signer for Keypair { - #[inline] - fn pubkey(&self) -> Pubkey { - Pubkey::from(self.0.public.to_bytes()) - } - - fn try_pubkey(&self) -> Result { - Ok(self.pubkey()) - } - - fn sign_message(&self, message: &[u8]) -> Signature { - Signature::from(self.0.sign(message).to_bytes()) - } - - fn try_sign_message(&self, message: &[u8]) -> Result { - Ok(self.sign_message(message)) - } - - fn is_interactive(&self) -> bool { - false - } -} - -impl PartialEq for Keypair -where - T: Signer, -{ - fn eq(&self, other: &T) -> bool { - self.pubkey() == other.pubkey() - } -} - -impl EncodableKey for Keypair { - fn read(reader: &mut R) -> Result> { - read_keypair(reader) - } - - fn write(&self, writer: &mut W) -> Result> { - write_keypair(self, writer) - } -} - -impl SeedDerivable for Keypair { - fn from_seed(seed: &[u8]) -> Result> { - keypair_from_seed(seed) - } - - fn from_seed_and_derivation_path( - seed: &[u8], - derivation_path: Option, - ) -> Result> { - keypair_from_seed_and_derivation_path(seed, derivation_path) - } - - fn from_seed_phrase_and_passphrase( - seed_phrase: &str, - passphrase: &str, - ) -> Result> { - keypair_from_seed_phrase_and_passphrase(seed_phrase, passphrase) - } -} - -impl EncodableKeypair for Keypair { - type Pubkey = Pubkey; - - /// Returns the associated pubkey. Use this function specifically for settings that involve - /// reading or writing pubkeys. For other settings, use `Signer::pubkey()` instead. - fn encodable_pubkey(&self) -> Self::Pubkey { - self.pubkey() - } -} - -/// Reads a JSON-encoded `Keypair` from a `Reader` implementor -pub fn read_keypair(reader: &mut R) -> Result> { - let bytes: Vec = serde_json::from_reader(reader)?; - Keypair::from_bytes(&bytes) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()).into()) -} - -/// Reads a `Keypair` from a file -pub fn read_keypair_file>(path: F) -> Result> { - Keypair::read_from_file(path) -} - -/// Writes a `Keypair` to a `Write` implementor with JSON-encoding -pub fn write_keypair( - keypair: &Keypair, - writer: &mut W, -) -> Result> { - let keypair_bytes = keypair.0.to_bytes(); - let serialized = serde_json::to_string(&keypair_bytes.to_vec())?; - writer.write_all(serialized.as_bytes())?; - Ok(serialized) -} - -/// Writes a `Keypair` to a file with JSON-encoding -pub fn write_keypair_file>( - keypair: &Keypair, - outfile: F, -) -> Result> { - keypair.write_to_file(outfile) -} - -/// Constructs a `Keypair` from caller-provided seed entropy -pub fn keypair_from_seed(seed: &[u8]) -> Result> { - if seed.len() < ed25519_dalek::SECRET_KEY_LENGTH { - return Err("Seed is too short".into()); - } - let secret = ed25519_dalek::SecretKey::from_bytes(&seed[..ed25519_dalek::SECRET_KEY_LENGTH]) - .map_err(|e| e.to_string())?; - let public = ed25519_dalek::PublicKey::from(&secret); - let dalek_keypair = ed25519_dalek::Keypair { secret, public }; - Ok(Keypair(dalek_keypair)) -} - -/// Generates a Keypair using Bip32 Hierarchical Derivation if derivation-path is provided; -/// otherwise generates the base Bip44 Solana keypair from the seed -pub fn keypair_from_seed_and_derivation_path( - seed: &[u8], - derivation_path: Option, -) -> Result> { - let derivation_path = derivation_path.unwrap_or_default(); - bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into()) -} - -/// Generates a Keypair using Bip32 Hierarchical Derivation -fn bip32_derived_keypair( - seed: &[u8], - derivation_path: DerivationPath, -) -> Result { - let extended = ed25519_dalek_bip32::ExtendedSecretKey::from_seed(seed) - .and_then(|extended| extended.derive(&derivation_path))?; - let extended_public_key = extended.public_key(); - Ok(Keypair(ed25519_dalek::Keypair { - secret: extended.secret_key, - public: extended_public_key, - })) -} - -pub fn generate_seed_from_seed_phrase_and_passphrase( - seed_phrase: &str, - passphrase: &str, -) -> Vec { - const PBKDF2_ROUNDS: u32 = 2048; - const PBKDF2_BYTES: usize = 64; - - let salt = format!("mnemonic{passphrase}"); - - let mut seed = vec![0u8; PBKDF2_BYTES]; - pbkdf2::pbkdf2::>( - seed_phrase.as_bytes(), - salt.as_bytes(), - PBKDF2_ROUNDS, - &mut seed, - ); - seed -} - -pub fn keypair_from_seed_phrase_and_passphrase( - seed_phrase: &str, - passphrase: &str, -) -> Result> { - keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase( - seed_phrase, - passphrase, - )) -} - -#[cfg(test)] -mod tests { - use { - super::*, - bip39::{Language, Mnemonic, MnemonicType, Seed}, - std::{ - fs::{self, File}, - mem, - }, - }; - - fn tmp_file_path(name: &str) -> String { - use std::env; - let out_dir = env::var("FARF_DIR").unwrap_or_else(|_| "farf".to_string()); - let keypair = Keypair::new(); - - format!("{}/tmp/{}-{}", out_dir, name, keypair.pubkey()) - } - - #[test] - fn test_write_keypair_file() { - let outfile = tmp_file_path("test_write_keypair_file.json"); - let serialized_keypair = write_keypair_file(&Keypair::new(), &outfile).unwrap(); - let keypair_vec: Vec = serde_json::from_str(&serialized_keypair).unwrap(); - assert!(Path::new(&outfile).exists()); - assert_eq!( - keypair_vec, - read_keypair_file(&outfile).unwrap().0.to_bytes().to_vec() - ); - - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - assert_eq!( - File::open(&outfile) - .expect("open") - .metadata() - .expect("metadata") - .permissions() - .mode() - & 0o777, - 0o600 - ); - } - - assert_eq!( - read_keypair_file(&outfile).unwrap().pubkey().as_ref().len(), - mem::size_of::() - ); - fs::remove_file(&outfile).unwrap(); - assert!(!Path::new(&outfile).exists()); - } - - #[test] - fn test_write_keypair_file_overwrite_ok() { - let outfile = tmp_file_path("test_write_keypair_file_overwrite_ok.json"); - - write_keypair_file(&Keypair::new(), &outfile).unwrap(); - write_keypair_file(&Keypair::new(), &outfile).unwrap(); - } - - #[test] - fn test_write_keypair_file_truncate() { - let outfile = tmp_file_path("test_write_keypair_file_truncate.json"); - - write_keypair_file(&Keypair::new(), &outfile).unwrap(); - read_keypair_file(&outfile).unwrap(); - - // Ensure outfile is truncated - { - let mut f = File::create(&outfile).unwrap(); - f.write_all(String::from_utf8([b'a'; 2048].to_vec()).unwrap().as_bytes()) - .unwrap(); - } - write_keypair_file(&Keypair::new(), &outfile).unwrap(); - read_keypair_file(&outfile).unwrap(); - } - - #[test] - fn test_keypair_from_seed() { - let good_seed = vec![0; 32]; - assert!(keypair_from_seed(&good_seed).is_ok()); - - let too_short_seed = vec![0; 31]; - assert!(keypair_from_seed(&too_short_seed).is_err()); - } - - #[test] - fn test_keypair_from_seed_phrase_and_passphrase() { - let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English); - let passphrase = "42"; - let seed = Seed::new(&mnemonic, passphrase); - let expected_keypair = keypair_from_seed(seed.as_bytes()).unwrap(); - let keypair = - keypair_from_seed_phrase_and_passphrase(mnemonic.phrase(), passphrase).unwrap(); - assert_eq!(keypair.pubkey(), expected_keypair.pubkey()); - } - - #[test] - fn test_keypair() { - let keypair = keypair_from_seed(&[0u8; 32]).unwrap(); - let pubkey = keypair.pubkey(); - let data = [1u8]; - let sig = keypair.sign_message(&data); - - // Signer - assert_eq!(keypair.try_pubkey().unwrap(), pubkey); - assert_eq!(keypair.pubkey(), pubkey); - assert_eq!(keypair.try_sign_message(&data).unwrap(), sig); - assert_eq!(keypair.sign_message(&data), sig); - - // PartialEq - let keypair2 = keypair_from_seed(&[0u8; 32]).unwrap(); - assert_eq!(keypair, keypair2); - } -} diff --git a/sdk/src/signer/mod.rs b/sdk/src/signer/mod.rs index ad3ca085cbc52b..36b789741d80d8 100644 --- a/sdk/src/signer/mod.rs +++ b/sdk/src/signer/mod.rs @@ -1,244 +1,9 @@ -//! Abstractions and implementations for transaction signers. - #![cfg(feature = "full")] - -use { - crate::{ - pubkey::Pubkey, - signature::{PresignerError, Signature}, +pub use { + solana_presigner as presigner, + solana_seed_derivable::SeedDerivable, + solana_signer::{ + null_signer, signers, unique_signers, EncodableKey, EncodableKeypair, Signer, SignerError, }, - itertools::Itertools, - solana_derivation_path::DerivationPath, - solana_transaction_error::TransactionError, - std::{ - error, - fs::{self, File, OpenOptions}, - io::{Read, Write}, - ops::Deref, - path::Path, - }, - thiserror::Error, }; - pub mod keypair; -pub mod null_signer; -pub mod presigner; -pub mod signers; - -#[derive(Debug, Error, PartialEq, Eq)] -pub enum SignerError { - #[error("keypair-pubkey mismatch")] - KeypairPubkeyMismatch, - - #[error("not enough signers")] - NotEnoughSigners, - - #[error("transaction error")] - TransactionError(#[from] TransactionError), - - #[error("custom error: {0}")] - Custom(String), - - // Presigner-specific Errors - #[error("presigner error")] - PresignerError(#[from] PresignerError), - - // Remote Keypair-specific Errors - #[error("connection error: {0}")] - Connection(String), - - #[error("invalid input: {0}")] - InvalidInput(String), - - #[error("no device found")] - NoDeviceFound, - - #[error("{0}")] - Protocol(String), - - #[error("{0}")] - UserCancel(String), - - #[error("too many signers")] - TooManySigners, -} - -/// The `Signer` trait declares operations that all digital signature providers -/// must support. It is the primary interface by which signers are specified in -/// `Transaction` signing interfaces -pub trait Signer { - /// Infallibly gets the implementor's public key. Returns the all-zeros - /// `Pubkey` if the implementor has none. - fn pubkey(&self) -> Pubkey { - self.try_pubkey().unwrap_or_default() - } - /// Fallibly gets the implementor's public key - fn try_pubkey(&self) -> Result; - /// Infallibly produces an Ed25519 signature over the provided `message` - /// bytes. Returns the all-zeros `Signature` if signing is not possible. - fn sign_message(&self, message: &[u8]) -> Signature { - self.try_sign_message(message).unwrap_or_default() - } - /// Fallibly produces an Ed25519 signature over the provided `message` bytes. - fn try_sign_message(&self, message: &[u8]) -> Result; - /// Whether the implementation requires user interaction to sign - fn is_interactive(&self) -> bool; -} - -/// This implements `Signer` for all ptr types - `Box/Rc/Arc/&/&mut` etc -impl> Signer for Container { - #[inline] - fn pubkey(&self) -> Pubkey { - self.deref().pubkey() - } - - fn try_pubkey(&self) -> Result { - self.deref().try_pubkey() - } - - fn sign_message(&self, message: &[u8]) -> Signature { - self.deref().sign_message(message) - } - - fn try_sign_message(&self, message: &[u8]) -> Result { - self.deref().try_sign_message(message) - } - - fn is_interactive(&self) -> bool { - self.deref().is_interactive() - } -} - -impl PartialEq for dyn Signer { - fn eq(&self, other: &dyn Signer) -> bool { - self.pubkey() == other.pubkey() - } -} - -impl Eq for dyn Signer {} - -impl std::fmt::Debug for dyn Signer { - fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(fmt, "Signer: {:?}", self.pubkey()) - } -} - -/// Removes duplicate signers while preserving order. O(n²) -pub fn unique_signers(signers: Vec<&dyn Signer>) -> Vec<&dyn Signer> { - signers.into_iter().unique_by(|s| s.pubkey()).collect() -} - -/// The `EncodableKey` trait defines the interface by which cryptographic keys/keypairs are read, -/// written, and derived from sources. -pub trait EncodableKey: Sized { - fn read(reader: &mut R) -> Result>; - fn read_from_file>(path: F) -> Result> { - let mut file = File::open(path.as_ref())?; - Self::read(&mut file) - } - fn write(&self, writer: &mut W) -> Result>; - fn write_to_file>(&self, outfile: F) -> Result> { - let outfile = outfile.as_ref(); - - if let Some(outdir) = outfile.parent() { - fs::create_dir_all(outdir)?; - } - - let mut f = { - #[cfg(not(unix))] - { - OpenOptions::new() - } - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - OpenOptions::new().mode(0o600) - } - } - .write(true) - .truncate(true) - .create(true) - .open(outfile)?; - - self.write(&mut f) - } -} - -/// The `SeedDerivable` trait defines the interface by which cryptographic keys/keypairs are -/// derived from byte seeds, derivation paths, and passphrases. -pub trait SeedDerivable: Sized { - fn from_seed(seed: &[u8]) -> Result>; - fn from_seed_and_derivation_path( - seed: &[u8], - derivation_path: Option, - ) -> Result>; - fn from_seed_phrase_and_passphrase( - seed_phrase: &str, - passphrase: &str, - ) -> Result>; -} - -/// The `EncodableKeypair` trait extends `EncodableKey` for asymmetric keypairs, i.e. have -/// associated public keys. -pub trait EncodableKeypair: EncodableKey { - type Pubkey: ToString; - - /// Returns an encodable representation of the associated public key. - fn encodable_pubkey(&self) -> Self::Pubkey; -} - -#[cfg(test)] -mod tests { - use {super::*, crate::signer::keypair::Keypair}; - - fn pubkeys(signers: &[&dyn Signer]) -> Vec { - signers.iter().map(|x| x.pubkey()).collect() - } - - #[test] - fn test_unique_signers() { - let alice = Keypair::new(); - let bob = Keypair::new(); - assert_eq!( - pubkeys(&unique_signers(vec![&alice, &bob, &alice])), - pubkeys(&[&alice, &bob]) - ); - } - - #[test] - fn test_containers() { - use std::{rc::Rc, sync::Arc}; - - struct Foo { - #[allow(unused)] - signer: S, - } - - fn foo(_s: impl Signer) {} - - let _arc_signer = Foo { - signer: Arc::new(Keypair::new()), - }; - foo(Arc::new(Keypair::new())); - - let _rc_signer = Foo { - signer: Rc::new(Keypair::new()), - }; - foo(Rc::new(Keypair::new())); - - let _ref_signer = Foo { - signer: &Keypair::new(), - }; - foo(Keypair::new()); - - let _box_signer = Foo { - signer: Box::new(Keypair::new()), - }; - foo(Box::new(Keypair::new())); - - let _signer = Foo { - signer: Keypair::new(), - }; - foo(Keypair::new()); - } -} diff --git a/sdk/src/wasm/keypair.rs b/sdk/src/wasm/keypair.rs index 6f2ffebbb7ccf5..5c5da471a7878a 100644 --- a/sdk/src/wasm/keypair.rs +++ b/sdk/src/wasm/keypair.rs @@ -1,34 +1,3 @@ -//! `Keypair` Javascript interface -#![cfg(target_arch = "wasm32")] -#![allow(non_snake_case)] -use { - crate::signer::{keypair::Keypair, Signer}, - solana_program::{pubkey::Pubkey, wasm::display_to_jsvalue}, - wasm_bindgen::prelude::*, -}; - -#[wasm_bindgen] -impl Keypair { - /// Create a new `Keypair ` - #[wasm_bindgen(constructor)] - pub fn constructor() -> Keypair { - Keypair::new() - } - - /// Convert a `Keypair` to a `Uint8Array` - pub fn toBytes(&self) -> Box<[u8]> { - self.to_bytes().into() - } - - /// Recover a `Keypair` from a `Uint8Array` - pub fn fromBytes(bytes: &[u8]) -> Result { - Keypair::from_bytes(bytes).map_err(display_to_jsvalue) - } - - /// Return the `Pubkey` for this `Keypair` - #[wasm_bindgen(js_name = pubkey)] - pub fn js_pubkey(&self) -> Pubkey { - // `wasm_bindgen` does not support traits (`Signer) yet - self.pubkey() - } -} +//! This module is empty but has not yet been removed because that would +//! technically be a breaking change. There was never anything to import +//! from here. diff --git a/zk-token-sdk/Cargo.toml b/zk-token-sdk/Cargo.toml index f5e431f54c94ae..39f7e584767b33 100644 --- a/zk-token-sdk/Cargo.toml +++ b/zk-token-sdk/Cargo.toml @@ -17,7 +17,7 @@ num-derive = { workspace = true } num-traits = { workspace = true } solana-curve25519 = { workspace = true } solana-instruction = { workspace = true, features = ["std"] } -solana-pubkey = { workspace = true } +solana-pubkey = { workspace = true, features = ["bytemuck"] } thiserror = { workspace = true } [dev-dependencies] @@ -37,7 +37,10 @@ serde_derive = { workspace = true } serde_json = { workspace = true } sha3 = { workspace = true } solana-derivation-path = { workspace = true } -solana-sdk = { workspace = true } +solana-seed-derivable = { workspace = true } +solana-seed-phrase = { workspace = true } +solana-signature = { workspace = true } +solana-signer = { workspace = true } subtle = { workspace = true } zeroize = { workspace = true, features = ["zeroize_derive"] } diff --git a/zk-token-sdk/src/encryption/auth_encryption.rs b/zk-token-sdk/src/encryption/auth_encryption.rs index c61d27e486f7f1..a1395e472abe5b 100644 --- a/zk-token-sdk/src/encryption/auth_encryption.rs +++ b/zk-token-sdk/src/encryption/auth_encryption.rs @@ -7,13 +7,10 @@ use { base64::{prelude::BASE64_STANDARD, Engine}, sha3::{Digest, Sha3_512}, solana_derivation_path::DerivationPath, - solana_sdk::{ - signature::Signature, - signer::{ - keypair::generate_seed_from_seed_phrase_and_passphrase, EncodableKey, SeedDerivable, - Signer, SignerError, - }, - }, + solana_seed_derivable::SeedDerivable, + solana_seed_phrase::generate_seed_from_seed_phrase_and_passphrase, + solana_signature::Signature, + solana_signer::{EncodableKey, Signer, SignerError}, std::{ convert::TryInto, error, fmt, @@ -268,7 +265,8 @@ impl fmt::Display for AeCiphertext { mod tests { use { super::*, - solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::null_signer::NullSigner}, + solana_pubkey::Pubkey, + solana_signer::{keypair::Keypair, null_signer::NullSigner}, }; #[test] diff --git a/zk-token-sdk/src/encryption/elgamal.rs b/zk-token-sdk/src/encryption/elgamal.rs index d4ffc2028bf27a..b642a337ca74d0 100644 --- a/zk-token-sdk/src/encryption/elgamal.rs +++ b/zk-token-sdk/src/encryption/elgamal.rs @@ -34,13 +34,10 @@ use { }, serde::{Deserialize, Serialize}, solana_derivation_path::DerivationPath, - solana_sdk::{ - signature::Signature, - signer::{ - keypair::generate_seed_from_seed_phrase_and_passphrase, EncodableKey, EncodableKeypair, - SeedDerivable, Signer, SignerError, - }, - }, + solana_seed_derivable::SeedDerivable, + solana_seed_phrase::generate_seed_from_seed_phrase_and_passphrase, + solana_signature::Signature, + solana_signer::{EncodableKey, EncodableKeypair, Signer, SignerError}, std::convert::TryInto, subtle::{Choice, ConstantTimeEq}, zeroize::Zeroize, @@ -870,7 +867,8 @@ mod tests { super::*, crate::encryption::pedersen::Pedersen, bip39::{Language, Mnemonic, MnemonicType, Seed}, - solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::null_signer::NullSigner}, + solana_pubkey::Pubkey, + solana_signer::{keypair::Keypair, null_signer::NullSigner}, std::fs::{self, File}, }; diff --git a/zk-token-sdk/src/sigma_proofs/pubkey_proof.rs b/zk-token-sdk/src/sigma_proofs/pubkey_proof.rs index 2498a1e68686b5..57b0f87bdcd69e 100644 --- a/zk-token-sdk/src/sigma_proofs/pubkey_proof.rs +++ b/zk-token-sdk/src/sigma_proofs/pubkey_proof.rs @@ -138,10 +138,7 @@ impl PubkeyValidityProof { #[cfg(test)] mod test { - use { - super::*, - solana_sdk::{pubkey::Pubkey, signature::Keypair}, - }; + use {super::*, solana_pubkey::Pubkey, solana_signer::keypair::Keypair}; #[test] fn test_pubkey_proof_correctness() {