From d3a6a5d8cdd2c54de3821c7a7f8e5f9a9bcbe99f Mon Sep 17 00:00:00 2001 From: Joy Wang <108701016+joyqvq@users.noreply.github.com> Date: Wed, 10 Jan 2024 22:20:14 +0800 Subject: [PATCH 01/11] feat: use bech32 encoding for private key --- .changeset/two-olives-enjoy.md | 5 + apps/wallet/package.json | 1 + .../src/shared/utils/from-exported-keypair.ts | 17 +- .../validation/privateKeyValidation.ts | 19 +- .../app/pages/accounts/ExportAccountPage.tsx | 2 +- .../pages/accounts/ImportPrivateKeyPage.tsx | 9 +- .../src/unit_tests/transaction_tests.rs | 4 +- .../src/context_data/pg_backend.rs | 414 ++++++++++++++++++ crates/sui-keys/src/keystore.rs | 17 +- crates/sui-keys/tests/tests.rs | 3 +- crates/sui-rosetta/src/main.rs | 10 +- crates/sui-sdk/examples/sign_tx_guide.rs | 17 +- crates/sui-types/src/crypto.rs | 41 +- .../sui-types/src/unit_tests/crypto_tests.rs | 12 + crates/sui-types/src/unit_tests/utils.rs | 3 +- .../unit_tests/zk_login_authenticator_test.rs | 5 +- .../src/unit_tests/zklogin_test_vectors.json | 20 +- crates/sui/Cargo.toml | 1 + crates/sui/src/keytool.rs | 197 +++++---- crates/sui/src/unit_tests/keytool_tests.rs | 104 +++-- docs/content/references/cli/keytool.mdx | 3 +- sdk/typescript/package.json | 1 + sdk/typescript/src/cryptography/keypair.ts | 65 ++- .../src/keypairs/ed25519/keypair.ts | 36 +- .../src/keypairs/secp256k1/keypair.ts | 17 +- .../src/keypairs/secp256r1/keypair.ts | 18 +- .../unit/cryptography/ed25519-keypair.test.ts | 39 +- .../cryptography/secp256k1-keypair.test.ts | 27 +- .../cryptography/secp256r1-keypair.test.ts | 38 +- 29 files changed, 853 insertions(+), 292 deletions(-) create mode 100644 .changeset/two-olives-enjoy.md create mode 100644 crates/sui-graphql-rpc/src/context_data/pg_backend.rs diff --git a/.changeset/two-olives-enjoy.md b/.changeset/two-olives-enjoy.md new file mode 100644 index 0000000000000..fdd19210d536e --- /dev/null +++ b/.changeset/two-olives-enjoy.md @@ -0,0 +1,5 @@ +--- +'@mysten/sui.js': major +--- + +Use Bech32 instead of Hex for private key encoding for import and export diff --git a/apps/wallet/package.json b/apps/wallet/package.json index 72d9f1972796a..fe67e36ad171e 100644 --- a/apps/wallet/package.json +++ b/apps/wallet/package.json @@ -135,6 +135,7 @@ "@sentry/browser": "^7.61.0", "@tanstack/react-query": "^5.0.0", "@tanstack/react-query-persist-client": "^4.29.25", + "bech32": "^2.0.0", "bignumber.js": "^9.1.1", "bootstrap-icons": "^1.10.5", "buffer": "^6.0.3", diff --git a/apps/wallet/src/shared/utils/from-exported-keypair.ts b/apps/wallet/src/shared/utils/from-exported-keypair.ts index 720f51de93a31..1b7926ee88a3e 100644 --- a/apps/wallet/src/shared/utils/from-exported-keypair.ts +++ b/apps/wallet/src/shared/utils/from-exported-keypair.ts @@ -2,17 +2,24 @@ // SPDX-License-Identifier: Apache-2.0 import { type ExportedKeypair, type Keypair } from '@mysten/sui.js/cryptography'; +import { + decodeSuiPrivateKey, + LEGACY_PRIVATE_KEY_SIZE, + PRIVATE_KEY_SIZE, +} from '@mysten/sui.js/cryptography/keypair'; import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519'; import { Secp256k1Keypair } from '@mysten/sui.js/keypairs/secp256k1'; import { Secp256r1Keypair } from '@mysten/sui.js/keypairs/secp256r1'; -import { fromB64 } from '@mysten/sui.js/utils'; -const PRIVATE_KEY_SIZE = 32; -const LEGACY_PRIVATE_KEY_SIZE = 64; +export function validateExportedKeypair(keypair: ExportedKeypair): ExportedKeypair { + const _kp = decodeSuiPrivateKey(keypair.privateKey); + return keypair; +} + export function fromExportedKeypair(keypair: ExportedKeypair): Keypair { - const secretKey = fromB64(keypair.privateKey); + const { schema, secretKey } = decodeSuiPrivateKey(keypair.privateKey); - switch (keypair.schema) { + switch (schema) { case 'ED25519': let pureSecretKey = secretKey; if (secretKey.length === LEGACY_PRIVATE_KEY_SIZE) { diff --git a/apps/wallet/src/ui/app/helpers/validation/privateKeyValidation.ts b/apps/wallet/src/ui/app/helpers/validation/privateKeyValidation.ts index a79ee131e7cbd..5d5db3afc5ac2 100644 --- a/apps/wallet/src/ui/app/helpers/validation/privateKeyValidation.ts +++ b/apps/wallet/src/ui/app/helpers/validation/privateKeyValidation.ts @@ -1,6 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +import { decodeSuiPrivateKey } from '@mysten/sui.js/cryptography/keypair'; import { hexToBytes } from '@noble/hashes/utils'; import * as Yup from 'yup'; import { z } from 'zod'; @@ -10,28 +11,16 @@ export const privateKeyValidation = z .trim() .nonempty('Private Key is a required field.') .transform((privateKey, context) => { - const hexValue = privateKey.startsWith('0x') ? privateKey.slice(2) : privateKey; - let privateKeyBytes: Uint8Array | undefined; - try { - privateKeyBytes = hexToBytes(hexValue); + decodeSuiPrivateKey(privateKey); } catch (error) { context.addIssue({ code: 'custom', - message: 'Private Key must be a hexadecimal value. It may optionally begin with "0x".', - }); - return z.NEVER; - } - - if (![32, 64].includes(privateKeyBytes.length)) { - context.addIssue({ - code: 'custom', - message: 'Private Key must be either 32 or 64 bytes.', + message: 'Private Key must be a Bech32 encoded 33-byte string', }); return z.NEVER; } - - return hexValue; + return privateKey; }); /** @deprecated Prefer Zod over Yup for doing schema validation! */ diff --git a/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx b/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx index 449cb39a72b9c..925aeaf8ec5eb 100644 --- a/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx +++ b/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx @@ -31,7 +31,7 @@ export function ExportAccountPage() { password, accountID: account.id, }); - return `0x${bytesToHex(fromB64(privateKey))}`; + return privateKey; }, }); const navigate = useNavigate(); diff --git a/apps/wallet/src/ui/app/pages/accounts/ImportPrivateKeyPage.tsx b/apps/wallet/src/ui/app/pages/accounts/ImportPrivateKeyPage.tsx index c77171df24a47..ea896bcbf7c18 100644 --- a/apps/wallet/src/ui/app/pages/accounts/ImportPrivateKeyPage.tsx +++ b/apps/wallet/src/ui/app/pages/accounts/ImportPrivateKeyPage.tsx @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 import { Text } from '_app/shared/text'; -import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519'; -import { hexToBytes } from '@noble/hashes/utils'; +import { validateExportedKeypair } from '_src/shared/utils/from-exported-keypair'; +import type { ExportedKeypair } from '@mysten/sui.js/cryptography'; import { useNavigate } from 'react-router-dom'; import { useAccountsFormContext } from '../../components/accounts/AccountsFormContext'; @@ -29,7 +29,10 @@ export function ImportPrivateKeyPage() { onSubmit={({ privateKey }) => { setAccountsFormValues({ type: 'imported', - keyPair: Ed25519Keypair.fromSecretKey(hexToBytes(privateKey).slice(0, 32)).export(), + keyPair: validateExportedKeypair({ + schema: 'ED25519', + privateKey: privateKey, + } as ExportedKeypair), }); navigate('/accounts/protect-account?accountType=imported'); }} diff --git a/crates/sui-core/src/unit_tests/transaction_tests.rs b/crates/sui-core/src/unit_tests/transaction_tests.rs index 9312a8232d7a2..063bce06a84a2 100644 --- a/crates/sui-core/src/unit_tests/transaction_tests.rs +++ b/crates/sui-core/src/unit_tests/transaction_tests.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use fastcrypto::ed25519::Ed25519KeyPair; -use fastcrypto::traits::{EncodeDecodeBase64, KeyPair}; +use fastcrypto::traits::KeyPair; use fastcrypto_zkp::bn254::zk_login::{parse_jwks, OIDCProvider, ZkLoginInputs}; use mysten_network::Multiaddr; use rand::{rngs::StdRng, SeedableRng}; @@ -805,7 +805,7 @@ async fn zk_multisig_test() { let mut pks = vec![]; let mut kps_and_zklogin_inputs = vec![]; for test in test_datum { - let kp = SuiKeyPair::decode_base64(&test.kp).unwrap(); + let kp = SuiKeyPair::decode(&test.kp).unwrap(); let inputs = ZkLoginInputs::from_json(&test.zklogin_inputs, &test.address_seed).unwrap(); let pk_zklogin = PublicKey::from_zklogin_inputs(&inputs).unwrap(); pks.push(pk_zklogin); diff --git a/crates/sui-graphql-rpc/src/context_data/pg_backend.rs b/crates/sui-graphql-rpc/src/context_data/pg_backend.rs new file mode 100644 index 0000000000000..0fe479aebdba5 --- /dev/null +++ b/crates/sui-graphql-rpc/src/context_data/pg_backend.rs @@ -0,0 +1,414 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + db_backend::{BalanceQuery, Explain, Explained, GenericQueryBuilder}, + db_data_provider::{DbValidationError, PageLimit, TypeFilterError}, +}; +use crate::{ + context_data::db_data_provider::PgManager, + error::Error, + types::{object::DeprecatedObjectFilter, sui_address::SuiAddress}, +}; +use async_trait::async_trait; +use diesel::{ + pg::Pg, + query_builder::{AstPass, QueryFragment}, + BoolExpressionMethods, ExpressionMethods, PgConnection, QueryDsl, QueryResult, RunQueryDsl, + TextExpressionMethods, +}; +use std::str::FromStr; +use sui_indexer::{ + schema_v2::{display, objects}, + types_v2::OwnerType, +}; +use sui_types::parse_sui_struct_tag; +use tap::TapFallible; +use tracing::{info, warn}; + +pub(crate) const EXPLAIN_COSTING_LOG_TARGET: &str = "gql-explain-costing"; + +pub(crate) struct PgQueryBuilder; + +impl GenericQueryBuilder for PgQueryBuilder { + fn get_obj(address: Vec, version: Option) -> objects::BoxedQuery<'static, Pg> { + let mut query = objects::dsl::objects.into_boxed(); + query = query.filter(objects::dsl::object_id.eq(address)); + + if let Some(version) = version { + query = query.filter(objects::dsl::object_version.eq(version)); + } + query + } + fn get_obj_by_type(object_type: String) -> objects::BoxedQuery<'static, Pg> { + objects::dsl::objects + .filter(objects::dsl::object_type.eq(object_type)) + .limit(1) // Fetches for a single object and as such has a limit of 1 + .into_boxed() + } + + fn get_display_by_obj_type(object_type: String) -> display::BoxedQuery<'static, Pg> { + display::dsl::display + .filter(display::dsl::object_type.eq(object_type)) + .limit(1) + .into_boxed() + } + + fn multi_get_coins( + before: Option>, + after: Option>, + limit: PageLimit, + address: Option>, + coin_type: String, + ) -> objects::BoxedQuery<'static, Pg> { + let mut query = order_objs(before, after, &limit); + query = query.limit(limit.value() + 1); + + if let Some(address) = address { + query = query + .filter(objects::dsl::owner_id.eq(address)) + // Leverage index on objects table + .filter(objects::dsl::owner_type.eq(OwnerType::Address as i16)); + } + query = query.filter(objects::dsl::coin_type.eq(coin_type)); + + query + } + fn multi_get_objs( + before: Option>, + after: Option>, + limit: PageLimit, + filter: Option, + owner_type: Option, + ) -> Result, Error> { + let mut query = order_objs(before, after, &limit); + query = query.limit(limit.value() + 1); + + let Some(filter) = filter else { + return Ok(query); + }; + + if let Some(object_ids) = filter.object_ids { + query = query.filter( + objects::dsl::object_id.eq_any( + object_ids + .into_iter() + .map(|id| id.into_vec()) + .collect::>(), + ), + ); + } + + if let Some(owner) = filter.owner { + query = query.filter(objects::dsl::owner_id.eq(owner.into_vec())); + + match owner_type { + Some(OwnerType::Address) => { + query = query.filter(objects::dsl::owner_type.eq(OwnerType::Address as i16)); + } + Some(OwnerType::Object) => { + query = query.filter(objects::dsl::owner_type.eq(OwnerType::Object as i16)); + } + None => { + query = query.filter( + objects::dsl::owner_type + .eq(OwnerType::Address as i16) + .or(objects::dsl::owner_type.eq(OwnerType::Object as i16)), + ); + } + _ => Err(DbValidationError::InvalidOwnerType)?, + } + } + + if let Some(object_type) = filter.type_ { + let format = "package[::module[::type[]]]"; + let parts: Vec<_> = object_type.splitn(3, "::").collect(); + + if parts.iter().any(|&part| part.is_empty()) { + return Err(DbValidationError::InvalidType( + TypeFilterError::MissingComponents(object_type, format).to_string(), + ))?; + } + + if parts.len() == 1 { + // We check for a leading 0x to determine if it is an address + // And otherwise process it as a primitive type + if parts[0].starts_with("0x") { + let package = SuiAddress::from_str(parts[0]) + .map_err(|e| DbValidationError::InvalidType(e.to_string()))?; + query = query.filter(objects::dsl::object_type.like(format!("{}::%", package))); + } else { + query = query.filter(objects::dsl::object_type.eq(parts[0].to_string())); + } + } else if parts.len() == 2 { + // Only package addresses are allowed if there are two or more parts + let package = SuiAddress::from_str(parts[0]) + .map_err(|e| DbValidationError::InvalidType(e.to_string()))?; + query = query.filter( + objects::dsl::object_type.like(format!("{}::{}::%", package, parts[1])), + ); + } else if parts.len() == 3 { + let validated_type = parse_sui_struct_tag(&object_type) + .map_err(|e| DbValidationError::InvalidType(e.to_string()))?; + + if validated_type.type_params.is_empty() { + query = query.filter( + objects::dsl::object_type + .like(format!( + "{}<%", + validated_type.to_canonical_string(/* with_prefix */ true) + )) + .or(objects::dsl::object_type + .eq(validated_type.to_canonical_string(/* with_prefix */ true))), + ); + } else { + query = query.filter( + objects::dsl::object_type + .eq(validated_type.to_canonical_string(/* with_prefix */ true)), + ); + } + } else { + return Err(DbValidationError::InvalidType( + TypeFilterError::TooManyComponents(object_type, 3, format).to_string(), + ) + .into()); + } + } + + Ok(query) + } + fn multi_get_balances(address: Vec) -> BalanceQuery<'static, Pg> { + let query = objects::dsl::objects + .group_by(objects::dsl::coin_type) + .select(( + diesel::dsl::sql::>( + "CAST(SUM(coin_balance) AS BIGINT)", + ), + diesel::dsl::sql::>( + "COUNT(*)", + ), + objects::dsl::coin_type, + )) + .filter(objects::dsl::owner_id.eq(address)) + .filter(objects::dsl::owner_type.eq(OwnerType::Address as i16)) + .filter(objects::dsl::coin_type.is_not_null()) + .into_boxed(); + + query + } + fn get_balance(address: Vec, coin_type: String) -> BalanceQuery<'static, Pg> { + let query = PgQueryBuilder::multi_get_balances(address); + query.filter(objects::dsl::coin_type.eq(coin_type)) + } +} + +/// Allows methods like load(), get_result(), etc. on an Explained query +impl RunQueryDsl for Explained {} + +/// Implement logic for prefixing queries with "EXPLAIN" +impl QueryFragment for Explained +where + T: QueryFragment, +{ + fn walk_ast<'b>(&'b self, mut out: AstPass<'_, 'b, Pg>) -> QueryResult<()> { + out.push_sql("EXPLAIN (FORMAT JSON) "); + self.query.walk_ast(out.reborrow())?; + Ok(()) + } +} + +#[async_trait] +pub trait PgQueryExecutor { + async fn run_query_async(&self, query: F) -> Result + where + F: FnOnce(&mut PgConnection) -> Result + Send + 'static, + E: From + std::error::Error + Send + 'static, + T: Send + 'static; + + async fn run_query_async_with_cost( + &self, + mut query_builder_fn: Q, + execute_fn: EF, + ) -> Result + where + Q: FnMut() -> Result + Send + 'static, + QResult: diesel::query_builder::QueryFragment + + diesel::query_builder::Query + + diesel::query_builder::QueryId + + Send + + 'static, + EF: FnOnce(QResult) -> F + Send + 'static, + F: FnOnce(&mut PgConnection) -> Result + Send + 'static, + E: From + std::error::Error + Send + 'static, + T: Send + 'static; +} + +#[async_trait] +impl PgQueryExecutor for PgManager { + async fn run_query_async(&self, query: F) -> Result + where + F: FnOnce(&mut PgConnection) -> Result + Send + 'static, + E: From + std::error::Error + Send + 'static, + T: Send + 'static, + { + self.inner + .run_query_async(query) + .await + .map_err(|e| Error::Internal(e.to_string())) + } + + /// Takes a query_builder_fn that returns Result and a lambda to execute the query + /// Spawns a blocking task that determines the cost of the query fragment + /// And if within limits, then executes the query + async fn run_query_async_with_cost( + &self, + mut query_builder_fn: Q, + execute_fn: EF, + ) -> Result + where + Q: FnMut() -> Result + Send + 'static, + QResult: diesel::query_builder::QueryFragment + + diesel::query_builder::Query + + diesel::query_builder::QueryId + + Send + + 'static, + EF: FnOnce(QResult) -> F + Send + 'static, + F: FnOnce(&mut PgConnection) -> Result + Send + 'static, + E: From + std::error::Error + Send + 'static, + T: Send + 'static, + { + let max_db_query_cost = self.limits.max_db_query_cost; + self.inner + .spawn_blocking(move |this| { + let query = query_builder_fn()?; + let explain_result: Option = this + .run_query(|conn| query.explain().get_result(conn)) + .tap_err(|e| { + warn!( + target: EXPLAIN_COSTING_LOG_TARGET, + "Failed to get explain result: {}", e + ) + }) + .ok(); // Fine to not propagate this error as explain-based costing is not critical today + + if let Some(explain_result) = explain_result { + let cost = extract_cost(&explain_result) + .tap_err(|e| { + warn!( + target: EXPLAIN_COSTING_LOG_TARGET, + "Failed to get cost from explain result: {}", e + ) + }) + .ok(); // Fine to not propagate this error as explain-based costing is not critical today + + if let Some(cost) = cost { + if cost > max_db_query_cost as f64 { + warn!( + target: EXPLAIN_COSTING_LOG_TARGET, + cost, + max_db_query_cost, + exceeds = true + ); + } else { + info!(target: EXPLAIN_COSTING_LOG_TARGET, cost,); + } + } + } + + let query = query_builder_fn()?; + let execute_closure = execute_fn(query); + this.run_query(execute_closure) + .map_err(|e| Error::Internal(e.to_string())) + }) + .await + } +} + +pub fn extract_cost(explain_result: &str) -> Result { + let parsed: serde_json::Value = + serde_json::from_str(explain_result).map_err(|e| Error::Internal(e.to_string()))?; + if let Some(cost) = parsed + .get(0) + .and_then(|entry| entry.get("Plan")) + .and_then(|plan| plan.get("Total Cost")) + .and_then(|cost| cost.as_f64()) + { + Ok(cost) + } else { + Err(Error::Internal( + "Failed to get cost from query plan".to_string(), + )) + } +} + +fn order_objs( + before: Option>, + after: Option>, + limit: &PageLimit, +) -> objects::BoxedQuery<'static, Pg> { + let mut query = objects::dsl::objects.into_boxed(); + match limit { + PageLimit::First(_) => { + if let Some(after) = after { + query = query.filter(objects::dsl::object_id.gt(after)); + } + query = query.order(objects::dsl::object_id.asc()); + } + PageLimit::Last(_) => { + if let Some(before) = before { + query = query.filter(objects::dsl::object_id.lt(before)); + } + query = query.order(objects::dsl::object_id.desc()); + } + } + query +} + +pub(crate) type QueryBuilder = PgQueryBuilder; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invalid_json() { + let explain_result = "invalid json"; + let result = extract_cost(explain_result); + assert!(matches!(result, Err(Error::Internal(_)))); + } + + #[test] + fn test_missing_entry_at_0() { + let explain_result = "[]"; + let result = extract_cost(explain_result); + assert!(matches!(result, Err(Error::Internal(_)))); + } + + #[test] + fn test_missing_plan() { + let explain_result = r#"[{}]"#; + let result = extract_cost(explain_result); + assert!(matches!(result, Err(Error::Internal(_)))); + } + + #[test] + fn test_missing_total_cost() { + let explain_result = r#"[{"Plan": {}}]"#; + let result = extract_cost(explain_result); + assert!(matches!(result, Err(Error::Internal(_)))); + } + + #[test] + fn test_failure_on_conversion_to_f64() { + let explain_result = r#"[{"Plan": {"Total Cost": "string_instead_of_float"}}]"#; + let result = extract_cost(explain_result); + assert!(matches!(result, Err(Error::Internal(_)))); + } + + #[test] + fn test_happy_scenario() { + let explain_result = r#"[{"Plan": {"Total Cost": 1.0}}]"#; + let result = extract_cost(explain_result).unwrap(); + assert_eq!(result, 1.0); + } +} diff --git a/crates/sui-keys/src/keystore.rs b/crates/sui-keys/src/keystore.rs index b2baa1a96e214..11d02b2b45de7 100644 --- a/crates/sui-keys/src/keystore.rs +++ b/crates/sui-keys/src/keystore.rs @@ -225,7 +225,7 @@ impl AccountKeystore for FileBasedKeystore { address, Alias { alias, - public_key_base64: EncodeDecodeBase64::encode_base64(&keypair.public()), + public_key_base64: keypair.public().encode_base64(), }, ); self.keys.insert(address, keypair); @@ -370,8 +370,7 @@ impl FileBasedKeystore { .iter() .zip(names) .map(|((sui_address, skp), alias)| { - let public_key_base64 = EncodeDecodeBase64::encode_base64(&skp.public()); - + let public_key_base64 = skp.public().encode_base64(); ( *sui_address, Alias { @@ -422,12 +421,18 @@ impl FileBasedKeystore { } pub fn save_keystore(&self) -> Result<(), anyhow::Error> { + println!( + "Keys saved as Base64 with 33 bytes `flag || privkey` ($BASE64_STR). + To see Bech32 format encoding, use `sui keytool export $SUI_ADDRESS` where + $SUI_ADDRESS can be found with `sui keytool list`. Or use `sui keytool convert $BASE64_STR`." + ); + if let Some(path) = &self.path { let store = serde_json::to_string_pretty( &self .keys .values() - .map(EncodeDecodeBase64::encode_base64) + .map(|k| k.encode_base64()) .collect::>(), ) .with_context(|| format!("Cannot serialize keystore to file: {}", path.display()))?; @@ -491,7 +496,7 @@ impl AccountKeystore for InMemKeystore { ) }); - let public_key_base64 = EncodeDecodeBase64::encode_base64(&keypair.public()); + let public_key_base64 = keypair.public().encode_base64(); let alias = Alias { alias, public_key_base64, @@ -584,7 +589,7 @@ impl InMemKeystore { .iter() .zip(random_names(HashSet::new(), keys.len())) .map(|((sui_address, skp), alias)| { - let public_key_base64 = EncodeDecodeBase64::encode_base64(&skp.public()); + let public_key_base64 = skp.public().encode_base64(); ( *sui_address, Alias { diff --git a/crates/sui-keys/tests/tests.rs b/crates/sui-keys/tests/tests.rs index 11393161236b2..b20f655fabaa3 100644 --- a/crates/sui-keys/tests/tests.rs +++ b/crates/sui-keys/tests/tests.rs @@ -5,7 +5,6 @@ use std::fs; use std::str::FromStr; use fastcrypto::hash::HashFunction; -use fastcrypto::traits::EncodeDecodeBase64; use sui_keys::key_derive::generate_new_key; use tempfile::TempDir; @@ -110,7 +109,7 @@ fn keystore_no_aliases() { let temp_dir = TempDir::new().unwrap(); let mut keystore_path = temp_dir.path().join("sui.keystore"); let (_, keypair, _, _) = generate_new_key(SignatureScheme::ED25519, None, None).unwrap(); - let private_keys = vec![EncodeDecodeBase64::encode_base64(&keypair)]; + let private_keys = vec![keypair.encode().unwrap()]; let keystore_data = serde_json::to_string_pretty(&private_keys).unwrap(); fs::write(&keystore_path, keystore_data).unwrap(); diff --git a/crates/sui-rosetta/src/main.rs b/crates/sui-rosetta/src/main.rs index 953223468bb85..35cd307a05ed8 100644 --- a/crates/sui-rosetta/src/main.rs +++ b/crates/sui-rosetta/src/main.rs @@ -12,17 +12,17 @@ use std::time::Duration; use anyhow::anyhow; use clap::Parser; use fastcrypto::encoding::{Encoding, Hex}; +use fastcrypto::traits::EncodeDecodeBase64; use serde_json::{json, Value}; -use tracing::info; -use tracing::log::warn; - use sui_config::{sui_config_dir, Config, NodeConfig, SUI_FULLNODE_CONFIG, SUI_KEYSTORE_FILENAME}; use sui_node::SuiNode; use sui_rosetta::types::{CurveType, PrefundedAccount, SuiEnv}; use sui_rosetta::{RosettaOfflineServer, RosettaOnlineServer, SUI}; use sui_sdk::{SuiClient, SuiClientBuilder}; use sui_types::base_types::SuiAddress; -use sui_types::crypto::{EncodeDecodeBase64, KeypairTraits, SuiKeyPair, ToFromBytes}; +use sui_types::crypto::{KeypairTraits, SuiKeyPair, ToFromBytes}; +use tracing::info; +use tracing::log::warn; #[derive(Parser)] #[clap(name = "sui-rosetta", rename_all = "kebab-case", author, version)] @@ -210,7 +210,7 @@ fn read_prefunded_account(path: &Path) -> Result, anyhow:: .iter() .map(|kpstr| { let key = SuiKeyPair::decode_base64(kpstr); - key.map(|k| (Into::::into(&k.public()), k)) + key.map(|k| (SuiAddress::from(&k.public()), k)) }) .collect::, _>>() .unwrap(); diff --git a/crates/sui-sdk/examples/sign_tx_guide.rs b/crates/sui-sdk/examples/sign_tx_guide.rs index 4666ccb7e9bd0..9b72d57871ba1 100644 --- a/crates/sui-sdk/examples/sign_tx_guide.rs +++ b/crates/sui-sdk/examples/sign_tx_guide.rs @@ -53,7 +53,7 @@ async fn main() -> Result<(), anyhow::Error> { let _skp_rand_1 = SuiKeyPair::Secp256k1(get_key_pair_from_rng(&mut rand::rngs::OsRng).1); let _skp_rand_2 = SuiKeyPair::Secp256r1(get_key_pair_from_rng(&mut rand::rngs::OsRng).1); - // import a keypair from a base64 encoded 32-byte `private key`. + // import a keypair from a base64 encoded 32-byte `private key` assuming scheme is Ed25519. let _skp_import_no_flag_0 = SuiKeyPair::Ed25519(Ed25519KeyPair::from_bytes( &Base64::decode("1GPhHHkVlF6GrCty2IuBkM+tj/e0jn64ksJ1pc8KPoI=") .map_err(|_| anyhow!("Invalid base64"))?, @@ -79,6 +79,21 @@ async fn main() -> Result<(), anyhow::Error> { SuiKeyPair::decode_base64("AtRj4Rx5FZRehqwrctiLgZDPrY/3tI5+uJLCdaXPCj6C") .map_err(|_| anyhow!("Invalid base64"))?; + // import a keypair from a Bech32 encoded 33-byte `flag || private key`. + // this is the format of a private key exported from Sui Wallet or sui.keystore. + let _skp_import_with_flag_0 = SuiKeyPair::decode( + "suiprivkey1qzdlfxn2qa2lj5uprl8pyhexs02sg2wrhdy7qaq50cqgnffw4c2477kg9h3", + ) + .map_err(|_| anyhow!("Invalid Bech32"))?; + let _skp_import_with_flag_1 = SuiKeyPair::decode( + "suiprivkey1qqesr6xhua2dkt840v9yefely578q5ad90znnpmhhgpekfvwtxke6ef2xyg", + ) + .map_err(|_| anyhow!("Invalid Bech32"))?; + let _skp_import_with_flag_2 = SuiKeyPair::decode( + "suiprivkey1qprzkcs823gcrk7n4hy8pzhntdxakpqk32qwjg9f2wyc3myj78egvtw3ecr", + ) + .map_err(|_| anyhow!("Invalid Bech32"))?; + // replace `skp_determ_0` with the variable names above let pk = skp_determ_0.public(); let sender = SuiAddress::from(&pk); diff --git a/crates/sui-types/src/crypto.rs b/crates/sui-types/src/crypto.rs index 7d0f89bdc85a8..0ceff8ec72e7e 100644 --- a/crates/sui-types/src/crypto.rs +++ b/crates/sui-types/src/crypto.rs @@ -47,7 +47,7 @@ use crate::error::{SuiError, SuiResult}; use crate::signature::GenericSignature; use crate::sui_serde::{Readable, SuiBitmap}; pub use enum_dispatch::enum_dispatch; -use fastcrypto::encoding::{Base64, Encoding, Hex}; +use fastcrypto::encoding::{Base64, Bech32, Encoding, Hex}; use fastcrypto::error::FastCryptoError; use fastcrypto::hash::{Blake2b256, HashFunction}; pub use fastcrypto::traits::Signer; @@ -75,7 +75,6 @@ pub type AggregateAuthoritySignatureAsBytes = BLS12381AggregateSignatureAsBytes; pub type AccountKeyPair = Ed25519KeyPair; pub type AccountPublicKey = Ed25519PublicKey; pub type AccountPrivateKey = Ed25519PrivateKey; -pub type AccountSignature = Ed25519Signature; pub type NetworkKeyPair = Ed25519KeyPair; pub type NetworkPublicKey = Ed25519PublicKey; @@ -84,6 +83,7 @@ pub type NetworkPrivateKey = Ed25519PrivateKey; pub type DefaultHash = Blake2b256; pub const DEFAULT_EPOCH_ID: EpochId = 0; +pub const SUI_PRIV_KEY_PREFIX: &str = "suiprivkey"; /// Creates a proof of that the authority account address is owned by the /// holder of authority protocol key, and also ensures that the authority @@ -161,18 +161,18 @@ impl Signer for SuiKeyPair { } } -impl FromStr for SuiKeyPair { - type Err = eyre::Report; +impl EncodeDecodeBase64 for SuiKeyPair { + fn encode_base64(&self) -> String { + Base64::encode(self.to_bytes()) + } - fn from_str(s: &str) -> Result { - let kp = Self::decode_base64(s).map_err(|e| eyre!("{}", e.to_string()))?; - Ok(kp) + fn decode_base64(value: &str) -> Result { + let bytes = Base64::decode(value).map_err(|e| eyre!("{}", e.to_string()))?; + Self::from_bytes(&bytes) } } - -impl EncodeDecodeBase64 for SuiKeyPair { - /// Encode a SuiKeyPair as `flag || privkey` in Base64. Note that the pubkey is not encoded. - fn encode_base64(&self) -> String { +impl SuiKeyPair { + pub fn to_bytes(&self) -> Vec { let mut bytes: Vec = Vec::new(); bytes.push(self.public().flag()); @@ -187,12 +187,10 @@ impl EncodeDecodeBase64 for SuiKeyPair { bytes.extend_from_slice(kp.as_bytes()); } } - Base64::encode(&bytes[..]) + bytes } - /// Decode a SuiKeyPair from `flag || privkey` in Base64. The public key is computed directly from the private key bytes. - fn decode_base64(value: &str) -> Result { - let bytes = Base64::decode(value).map_err(|e| eyre!("{}", e.to_string()))?; + pub fn from_bytes(bytes: &[u8]) -> Result { match SignatureScheme::from_flag_byte(bytes.first().ok_or_else(|| eyre!("Invalid length"))?) { Ok(x) => match x { @@ -214,6 +212,16 @@ impl EncodeDecodeBase64 for SuiKeyPair { _ => Err(eyre!("Invalid bytes")), } } + /// Encode a SuiKeyPair as `flag || privkey` in Bech32 starting with "suiprivkey" to a string. Note that the pubkey is not encoded. + pub fn encode(&self) -> Result { + Bech32::encode(self.to_bytes(), SUI_PRIV_KEY_PREFIX) + } + + /// Decode a SuiKeyPair from `flag || privkey` in Bech32 starting with "suiprivkey" to SuiKeyPair. The public key is computed directly from the private key bytes. + pub fn decode(value: &str) -> Result { + let bytes = Bech32::decode(value, SUI_PRIV_KEY_PREFIX)?; + Self::from_bytes(&bytes) + } } impl Serialize for SuiKeyPair { @@ -233,8 +241,7 @@ impl<'de> Deserialize<'de> for SuiKeyPair { { use serde::de::Error; let s = String::deserialize(deserializer)?; - ::decode_base64(&s) - .map_err(|e| Error::custom(e.to_string())) + SuiKeyPair::decode_base64(&s).map_err(|e| Error::custom(e.to_string())) } } diff --git a/crates/sui-types/src/unit_tests/crypto_tests.rs b/crates/sui-types/src/unit_tests/crypto_tests.rs index 4d32c645acd0c..17dfd7e7c7d85 100644 --- a/crates/sui-types/src/unit_tests/crypto_tests.rs +++ b/crates/sui-types/src/unit_tests/crypto_tests.rs @@ -5,6 +5,18 @@ use crate::crypto::bcs_signable_test::Foo; use proptest::collection; use proptest::prelude::*; +#[test] +fn serde_keypair() { + let skp = SuiKeyPair::Ed25519(Ed25519KeyPair::generate(&mut StdRng::from_seed([0; 32]))); + let encoded = skp.encode().unwrap(); + assert_eq!( + encoded, + "suiprivkey1qzdlfxn2qa2lj5uprl8pyhexs02sg2wrhdy7qaq50cqgnffw4c2477kg9h3" + ); + let decoded = SuiKeyPair::decode(&encoded).unwrap(); + assert_eq!(skp, decoded); +} + #[test] fn serde_pubkey() { let skp = SuiKeyPair::Ed25519(get_key_pair().1); diff --git a/crates/sui-types/src/unit_tests/utils.rs b/crates/sui-types/src/unit_tests/utils.rs index d924a39c3639b..d9c897da335b2 100644 --- a/crates/sui-types/src/unit_tests/utils.rs +++ b/crates/sui-types/src/unit_tests/utils.rs @@ -160,7 +160,6 @@ pub fn mock_certified_checkpoint<'a>( } mod zk_login { - use fastcrypto::traits::EncodeDecodeBase64; use fastcrypto_zkp::bn254::{utils::big_int_str_to_bytes, zk_login::ZkLoginInputs}; use shared_crypto::intent::PersonalMessage; @@ -180,7 +179,7 @@ mod zk_login { let test_datum: Vec = serde_json::from_reader(file).unwrap(); let mut res = vec![]; for test in test_datum { - let kp = SuiKeyPair::decode_base64(&test.kp).unwrap(); + let kp = SuiKeyPair::decode(&test.kp).unwrap(); let inputs = ZkLoginInputs::from_json(&test.zklogin_inputs, &test.address_seed).unwrap(); let pk_zklogin = PublicKey::from_zklogin_inputs(&inputs).unwrap(); diff --git a/crates/sui-types/src/unit_tests/zk_login_authenticator_test.rs b/crates/sui-types/src/unit_tests/zk_login_authenticator_test.rs index a829d747dfd69..2023d68cb9b00 100644 --- a/crates/sui-types/src/unit_tests/zk_login_authenticator_test.rs +++ b/crates/sui-types/src/unit_tests/zk_login_authenticator_test.rs @@ -14,7 +14,7 @@ use crate::{ base_types::SuiAddress, signature::GenericSignature, zk_login_util::DEFAULT_JWK_BYTES, }; use fastcrypto::encoding::Base64; -use fastcrypto::traits::{EncodeDecodeBase64, ToFromBytes}; +use fastcrypto::traits::ToFromBytes; use fastcrypto_zkp::bn254::utils::big_int_str_to_bytes; use fastcrypto_zkp::bn254::zk_login::{parse_jwks, JwkId, OIDCProvider, ZkLoginInputs, JWK}; use fastcrypto_zkp::bn254::zk_login_api::ZkLoginEnv; @@ -50,9 +50,10 @@ fn zklogin_authenticator_jwk() { let test_datum: Vec = serde_json::from_reader(file).unwrap(); for test in test_datum { - let kp = SuiKeyPair::decode_base64(&test.kp).unwrap(); + let kp = SuiKeyPair::decode(&test.kp).unwrap(); let inputs = ZkLoginInputs::from_json(&test.zklogin_inputs, &test.address_seed).unwrap(); let pk_zklogin = PublicKey::from_zklogin_inputs(&inputs).unwrap(); + let addr = (&pk_zklogin).into(); let tx_data = make_transaction_data(addr); let msg = IntentMessage::new(Intent::sui_transaction(), tx_data); diff --git a/crates/sui-types/src/unit_tests/zklogin_test_vectors.json b/crates/sui-types/src/unit_tests/zklogin_test_vectors.json index cf2d0c5f1447d..dfc83ccb691d9 100644 --- a/crates/sui-types/src/unit_tests/zklogin_test_vectors.json +++ b/crates/sui-types/src/unit_tests/zklogin_test_vectors.json @@ -1,70 +1,70 @@ [ { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"6273992944282656241643249967546436415683538330073309038295090020437032734417\",\"13925131915383843099385555626272032961923726580226697015319197132189118605257\",\"1\"],\"b\":[[\"7704007614120244428203234032774689081078638869317243551253010726338510436127\",\"4733691566008191204012705799062275962647301524570212343732751171422663135752\"],[\"9632116537411472981464147640393917418420287809518621999342206768832847695896\",\"8436287150321216550921548884226038273194202440574729038196401403024384113898\"],[\"1\",\"0\"]],\"c\":[\"5172184205489148831058189142062767553723109372209872440130518436384175126524\",\"5151432066138747209120687522208554041564827080797050716927769643447422123270\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "AJv0mmoHVflTgR/OEl8mg9UEKcO7SeB0FH4AiaUurhVf", + "kp": "suiprivkey1qzdlfxn2qa2lj5uprl8pyhexs02sg2wrhdy7qaq50cqgnffw4c2477kg9h3", "pk_bigint": "84029355920633174015103288781128426107680789454168570548782290541079926444544", "randomness": "117598931443460319689073426599647316158", "address_seed": "20687642517630733273687795476247437743834601101566684967967705809985241065539" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"12020344873260331045349872664543266970265788758683035621827816148516742703945\",\"19286680082144884370284859727693762627064048159994264382318939930879400846428\",\"1\"],\"b\":[[\"19879287328867235832303278521933121369497653249523205953885742976335610935103\",\"11688858049752026126876304203912068708282222836316965500226819030155152697444\"],[\"1811642677710740464572160828904210324274401421268145051148479609140012535240\",\"14122639347822156220055803245273375401982911401216725143945528722373661855827\"],[\"1\",\"0\"]],\"c\":[\"10618630831451128453033243179908662096338507877450333093204399416196702696149\",\"4700731246539880448541516731316319778327575747210125571643977647104549281129\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "ADMB6NfnVNss9XsKTKc/JTxwU60rxTmHd7oDmyWOWa2d", + "kp": "suiprivkey1qqesr6xhua2dkt840v9yefely578q5ad90znnpmhhgpekfvwtxke6ef2xyg", "pk_bigint": "63474334245264100054858690294269280156015492289955642868240277397884359034564", "randomness": "218867963521134579131897699561469207020", "address_seed": "14130805180701430215782554716026838279704270408208765726436884679066607946819" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"15264503940130056110890541084591463677417491033143676424274085646610015071529\",\"21544275488871144711947576476590531632689575791211521174867284170016731962896\",\"1\"],\"b\":[[\"16289339821074223721696622573353383086553025979559138977395953882225499264068\",\"8315493139565130424050930092226981164241899423115877025378752357205300826791\"],[\"2693124842214023357756794089380889141819556754132943223316154166051259946765\",\"15176397568767666873208837013552130492111792071007803026542685821487053412551\"],[\"1\",\"0\"]],\"c\":[\"12242603005902998498721096603563679164125961104750350619380652951260529414359\",\"17718983547813086723095094534706058385382317633323582097017933631884148666249\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "AEYrYgdUUYHb063IcIrzW03bBBaKgOkgqVOJiOyS8fKG", + "kp": "suiprivkey1qprzkcs823gcrk7n4hy8pzhntdxakpqk32qwjg9f2wyc3myj78egvtw3ecr", "pk_bigint": "75812421556494218383181193737102779368268748635728793167976883795325330616458", "randomness": "176587745442750291046152198400506583285", "address_seed": "19895633505031320705368972239227849725478436929503586879388634108970411794238" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"17633733223397147948783338791602783389407660380011861949408773812640845827906\",\"20216892358238656927425523242681154125169368761640234292946231735803609065503\",\"1\"],\"b\":[[\"9368235122306573779364198873069099694942485548180690936229982070523720949957\",\"10222342712806483979922406819667135649922921336499752448949033938167232015978\"],[\"18223526242658366214030036256196019484424532416424601541372657100546409278864\",\"2624580525918676336718199344055775046780705622411341264350614178504402129763\"],[\"1\",\"0\"]],\"c\":[\"20710518213158318367331849208982455242691064095013540830911761932397081646246\",\"4992404109226659012494983525265424816779203574617688290983532812552607193149\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "AIZQIIJHhG7/Whxi7PKKxmYeKpnbRI7A7EVr5uK3yoZZ", + "kp": "suiprivkey1qzr9qgyzg7zxal66r33weu52cenpu25emdzgas8vg447dc4he2r9jczetul", "pk_bigint": "63229411998646057802711528553531152030949495808851454701083157966618591060088", "randomness": "303734606112163814770405020451962360540", "address_seed": "3582459548350436939165686905466588585305859026194819797848960992966035788616" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"8503409969354365749845238881846012870819096640524695706484295867789517043184\",\"4984075582819017225318941945987118760209405907625474753723209884770329550585\",\"1\"],\"b\":[[\"11018415898422093790929284948671577320468311641678995418023444289786196815593\",\"19624120193069526077000456672244871226271541678427245314845830312885391587609\"],[\"6093732639461995719309907394394754453680097596314771839781867787762141912530\",\"8576074130564706510760613673119922663249166860366179236933821822176033979361\"],[\"1\",\"0\"]],\"c\":[\"9136481661225789690325477604156035222697118877983171826140070125964783607224\",\"18077219714890499934749944696658172413555335274400042619754875865858480305864\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "AZv0mmoHVflTgR/OEl8mg9UEKcO7SeB0FH4AiaUurhVf", + "kp": "suiprivkey1qxdlfxn2qa2lj5uprl8pyhexs02sg2wrhdy7qaq50cqgnffw4c247yfa65x", "pk_bigint": "29944890766655829170200586829507612845990808972173483626575407156683039723788644", "randomness": "227344719118057691391866154883231454488", "address_seed": "7533672517636653992337612579969909906884356900904393785133446651799935517585" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"15154021911177344660885998814473428984382697880353532809373209097550432009522\",\"10956313470023435888432405604075856267293071004169971483963324257425640045401\",\"1\"],\"b\":[[\"12822440314609890863188750736153724289823387528483324446036781976640612454057\",\"7485431696345979687704506082673002129839126286840349466841782686649064981762\"],[\"4577258413482608048733031539242781204349895469554305149883814901861920586846\",\"9624617563206819044150750963463696541549789177101875820632981449145940754813\"],[\"1\",\"0\"]],\"c\":[\"15560212957989690736788609910395156564771278469932369667811198291711673485197\",\"6547682648222084792231774595465403484135184507032671041589735875295810186964\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "ATMB6NfnVNss9XsKTKc/JTxwU60rxTmHd7oDmyWOWa2d", + "kp": "suiprivkey1qyesr6xhua2dkt840v9yefely578q5ad90znnpmhhgpekfvwtxke6rkle8l", "pk_bigint": "30018465977241618867789562550368757971750861627710741207097686749635540763413090", "randomness": "260072221671936471650131080306245923913", "address_seed": "13138537574324499657441030888487328527990753362292375820678467583052474037912" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"1668510945466946962851803985107296535937325729933415303970626217700934618265\",\"7980125056110677326460522428259593874398921778539515186719039797514440884551\",\"1\"],\"b\":[[\"12806866692151831125311197775501455120740686396948421202824290830743338515231\",\"18766378170718033394841161154204406874034898605598183039636645657297523158855\"],[\"17625235047814864859655051925523616907860972378159556084324395418993287403943\",\"16471060229530226109943771577456230365395317344979006001155817398156732202713\"],[\"1\",\"0\"]],\"c\":[\"18557585152811993951664684058881844162685957010653226898929806056786323893043\",\"2446426488540770719522295567676957363621544050189901194167465417904696447405\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "AUYrYgdUUYHb063IcIrzW03bBBaKgOkgqVOJiOyS8fKG", + "kp": "suiprivkey1q9rzkcs823gcrk7n4hy8pzhntdxakpqk32qwjg9f2wyc3myj78egv33yxm5", "pk_bigint": "29935302485784446965418410676449965650864530796412535192117075817043889211746845", "randomness": "106090913826444400211898651826199046086", "address_seed": "8468967329029836542373232786777824091829359082737092472354108135909141382104" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"19504254193813379094053928925402012211888690039478342639989702082016660927881\",\"18102618546646670822292732200833549984011910250662482992338836055753325521533\",\"1\"],\"b\":[[\"5243059461254419347839312288496689246987221320710727767837774788283756831028\",\"12070203963036645666023504324374659601657664039583140566760119701072292047911\"],[\"16367645113170599118077568195220986567161489607242574688984472859431639834178\",\"5891483672251423838195414846105647980166028226703168156863936516959932183115\"],[\"1\",\"0\"]],\"c\":[\"6476104300881546597650894613552075488299608639168317283586827007153889225126\",\"4768364111563472402570841481203318304314504867139574430382670807019048114271\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "Apv0mmoHVflTgR/OEl8mg9UEKcO7SeB0FH4AiaUurhVf", + "kp": "suiprivkey1q2dlfxn2qa2lj5uprl8pyhexs02sg2wrhdy7qaq50cqgnffw4c247rptj3k", "pk_bigint": "59740465294956219006314303954462208761731869106882223046184311573972838785003116", "randomness": "321253446829058773372274760874992462788", "address_seed": "10743180842698163948497022900143786241109623320837617530697514053208056107834" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"10162261609844614259672079851627515426133359131794478780834075914798717337535\",\"16501135869964500956960167247803673924284245570228328780616187143663837042324\",\"1\"],\"b\":[[\"6022210978040091822450366366923152864030436668895537605391380079013232199965\",\"16106140687100773424225246127378225809211411602431203274362356109294992862386\"],[\"7404175874829574345954951544918668298449616330023183183701816109879009691711\",\"7300730894043634624720793421535551827458618759616913254868353815339887223109\"],[\"1\",\"0\"]],\"c\":[\"11406506752592849498372761822738206564878837517791566347494492442128011140357\",\"15699363809888077071635585106706214571027457819118515596208083853872937741473\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "AjMB6NfnVNss9XsKTKc/JTxwU60rxTmHd7oDmyWOWa2d", + "kp": "suiprivkey1qgesr6xhua2dkt840v9yefely578q5ad90znnpmhhgpekfvwtxke6y7f3z0", "pk_bigint": "59673879755937083223065362534050516461848668017752820848310235386909893117140670", "randomness": "212636522407275520796909175011015148278", "address_seed": "17572616848275893382217481793919006366376859817040546049835958187461298669270" }, { "zklogin_inputs": "{\"proofPoints\":{\"a\":[\"21343071864478785559559666874476843628416089488121982449648989879911251669067\",\"5362303941397745196141681609094028187781366877809506554707248370738970929360\",\"1\"],\"b\":[[\"19032924320018126727120632633369155860784046660723232541002428018088481273097\",\"193401384097267993438112688794619549957326751980140834630740672762172410885\"],[\"4117495762077661899669985515337607019358027625157804017452693516986505547217\",\"12703439356254753840970120700123927195592891408136974489039338298386084186276\"],[\"1\",\"0\"]],\"c\":[\"6025860882269515164672656579580159321268680356545919054907935383654607091228\",\"15517688064775830277006667169367016587994226751447898336840996580737844644115\",\"1\"]},\"issBase64Details\":{\"value\":\"wiaXNzIjoiaHR0cHM6Ly9pZC50d2l0Y2gudHYvb2F1dGgyIiw\",\"indexMod4\":2},\"headerBase64\":\"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEifQ\"}", - "kp": "AkYrYgdUUYHb063IcIrzW03bBBaKgOkgqVOJiOyS8fKG", + "kp": "suiprivkey1qfrzkcs823gcrk7n4hy8pzhntdxakpqk32qwjg9f2wyc3myj78egvkejw7y", "pk_bigint": "59590789386603400027887684510435888217604590338629673844644742645329247256474906", "randomness": "113343420344076434853711696004613047395", "address_seed": "2022367717686071279912060416557701223299757424068605037597512575340445416810" diff --git a/crates/sui/Cargo.toml b/crates/sui/Cargo.toml index 3cb2094736303..64b793905f1bb 100644 --- a/crates/sui/Cargo.toml +++ b/crates/sui/Cargo.toml @@ -83,6 +83,7 @@ test-cluster.workspace = true sui-macros.workspace = true sui-simulator.workspace = true sui-test-transaction-builder.workspace = true +serde_json.workspace = true [package.metadata.cargo-udeps.ignore] normal = ["jemalloc-ctl"] diff --git a/crates/sui/src/keytool.rs b/crates/sui/src/keytool.rs index ea247f776db66..35efda8875304 100644 --- a/crates/sui/src/keytool.rs +++ b/crates/sui/src/keytool.rs @@ -9,8 +9,6 @@ use fastcrypto::ed25519::Ed25519KeyPair; use fastcrypto::encoding::{Base64, Encoding, Hex}; use fastcrypto::hash::HashFunction; use fastcrypto::secp256k1::recoverable::Secp256k1Sig; -use fastcrypto::secp256k1::Secp256k1KeyPair; -use fastcrypto::secp256r1::Secp256r1KeyPair; use fastcrypto::traits::{KeyPair, ToFromBytes}; use fastcrypto_zkp::bn254::utils::{get_oidc_url, get_token_exchange_url}; use fastcrypto_zkp::bn254::zk_login::{fetch_jwks, OIDCProvider}; @@ -69,9 +67,13 @@ pub enum KeyToolCommand { /// The alias must start with a letter and can contain only letters, digits, dots, hyphens (-), or underscores (_). new_alias: Option, }, - /// Convert private key from wallet format (hex of 32 byte private key) to sui.keystore format - /// (base64 of 33 byte flag || private key) or vice versa. + /// Convert private key in Hex or Base64 to new format (Bech32 + /// encoded 33 byte flag || private key starting with "suiprivkey"). + /// Hex private key format import and export are both deprecated in + /// Sui Wallet and Sui CLI Keystore. Use `sui keytool import` if you + /// wish to import a key to Sui Keystore. Convert { value: String }, + /// Given a Base64 encoded transaction bytes, decode its components. DecodeTxBytes { #[clap(long)] @@ -92,20 +94,21 @@ pub enum KeyToolCommand { /// if not specified. /// /// The keypair file is output to the current directory. The content of the file is - /// a Base64 encoded string of 33-byte `flag || privkey`. Note: To generate and add keypair - /// to sui.keystore, use `sui client new-address`). + /// a Base64 encoded string of 33-byte `flag || privkey`. + /// + /// Use `sui client new-address` if you want to generate and save the key into sui.keystore. Generate { key_scheme: SignatureScheme, - word_length: Option, derivation_path: Option, + word_length: Option, }, - /// Add a new key to sui.keystore using either the input mnemonic phrase or a private key (from the Wallet), - /// the key scheme flag {ed25519 | secp256k1 | secp256r1} and an optional derivation path, - /// default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1 - /// or m/74'/784'/0'/0/0 for secp256r1. Supports mnemonic phrase of word length 12, 15, 18`, 21, 24. - /// Set an alias for the key with the --alias flag. If no alias is provided, - /// the tool will automatically generate one. + /// Add a new key to Sui CLI Keystore using either the input mnemonic phrase or a Bech32 encoded 33-byte + /// `flag || privkey` starting with "suiprivkey", the key scheme flag {ed25519 | secp256k1 | secp256r1} + /// and an optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 + /// for secp256k1 or m/74'/784'/0'/0/0 for secp256r1. Supports mnemonic phrase of word length 12, 15, + /// 18, 21, 24. Set an alias for the key with the --alias flag. If no alias is provided, the tool will + /// automatically generate one. Import { /// Sets an alias for this address. The alias must start with a letter and can contain only letters, digits, hyphens (-), or underscores (_). #[clap(long)] @@ -114,6 +117,13 @@ pub enum KeyToolCommand { key_scheme: SignatureScheme, derivation_path: Option, }, + /// Output the private key of the given address in Sui CLI Keystore as Bech32 encoded string starting + /// with `suiprivkey`. If the alias is provided, the private key for the given alias will be exported. + Export { + #[clap(long)] + alias: Option, + address: Option, + }, /// List all keys by its Sui address, Base64 encoded public key, key scheme name in /// sui.keystore. List { @@ -308,6 +318,13 @@ pub struct Key { peer_id: Option, } +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExportedKey { + exported_private_key: String, + key: Key, +} + #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct KeypairData { @@ -350,9 +367,11 @@ pub struct MultiSigOutput { #[derive(Serialize)] #[serde(rename_all = "camelCase")] -pub enum ConvertOutput { - Base64(String), - Hex(String), +pub struct ConvertOutput { + bech32_with_flag: String, // latest Sui Keystore and Sui Wallet import/export format + base64_with_flag: String, // Sui Keystore storage format + hex_without_flag: String, // Legacy Sui Wallet format + scheme: String, } #[derive(Serialize)] @@ -417,6 +436,7 @@ pub enum CommandOutput { Error(String), Generate(Key), Import(Key), + Export(ExportedKey), List(Vec), LoadKeypair(KeypairData), MultiSigAddress(MultiSigAddress), @@ -445,7 +465,7 @@ impl KeyToolCommand { }) } KeyToolCommand::Convert { value } => { - let result = convert_private_key_to_base64(value)?; + let result = convert_private_key_to_bech32(value)?; CommandOutput::Convert(result) } @@ -545,41 +565,56 @@ impl KeyToolCommand { key_scheme, derivation_path, } => { - // check if input is a private key -- should start with 0x - if input_string.starts_with("0x") { - let bytes: Vec = Hex::decode(&input_string).map_err(|_| { - anyhow!("Private key is malformed. Importing private key failed.") - })?; - let skp = match key_scheme { - SignatureScheme::ED25519 => { - let kp = Ed25519KeyPair::from_bytes(&bytes)?; - SuiKeyPair::Ed25519(kp) - } - SignatureScheme::Secp256k1 => { - let kp = Secp256k1KeyPair::from_bytes(&bytes)?; - SuiKeyPair::Secp256k1(kp) - } - SignatureScheme::Secp256r1 => { - let kp = Secp256r1KeyPair::from_bytes(&bytes)?; - SuiKeyPair::Secp256r1(kp) - } - _ => return Err(anyhow!("Unsupported scheme")), - }; - let key = Key::from(&skp); - keystore.add_key(alias, skp)?; - CommandOutput::Import(key) - } else { - let sui_address = keystore.import_from_mnemonic( - &input_string, - key_scheme, - derivation_path, - )?; - let skp = keystore.get_key(&sui_address)?; - let key = Key::from(skp); - CommandOutput::Import(key) + if Hex::decode(&input_string).is_ok() { + return Err(anyhow!( + "Sui Keystore and Sui Wallet no longer support importing + private key as Hex, if you are sure your private key is encoded in Hex, use + `sui keytool convert $HEX` to convert first then import the Bech32 encoded + private key starting with `suiprivkey`." + )); } - } + match SuiKeyPair::decode(&input_string) { + Ok(skp) => { + info!("Importing Bech32 encoded private key to keystore"); + let key = Key::from(&skp); + keystore.add_key(alias, skp)?; + CommandOutput::Import(key) + } + Err(_) => { + info!("Importing mneomonics to keystore"); + let sui_address = keystore.import_from_mnemonic( + &input_string, + key_scheme, + derivation_path, + )?; + let skp = keystore.get_key(&sui_address)?; + let key = Key::from(skp); + CommandOutput::Import(key) + } + } + } + KeyToolCommand::Export { alias, address } => { + let skp = match alias { + Some(a) => { + let address = keystore.get_address_by_alias(a)?; + keystore.get_key(address)? + } + None => match address { + Some(a) => keystore.get_key(&a)?, + None => { + return Err(anyhow!("Must provide either alias or address")); + } + }, + }; + let key = ExportedKey { + exported_private_key: skp + .encode() + .map_err(|_| anyhow!("Cannot decode keypair"))?, + key: Key::from(skp), + }; + CommandOutput::Export(key) + } KeyToolCommand::List { sort_by_alias } => { let mut keys = keystore .keys() @@ -817,8 +852,8 @@ impl KeyToolCommand { } KeyToolCommand::Unpack { keypair } => { - let keypair: SuiKeyPair = keypair.parse() - .expect("Expected a Base64 private key, but could not decode the input string to a SuiKeyPair"); + let keypair = SuiKeyPair::decode_base64(&keypair) + .map_err(|_| anyhow!("Invalid Base64 encode keypair"))?; let key = Key::from(&keypair); let path_str = format!("{}.key", key.sui_address).to_lowercase(); @@ -1087,15 +1122,15 @@ impl From<&SuiKeyPair> for Key { } impl From for Key { - fn from(key: PublicKey) -> Self { + fn from(pk: PublicKey) -> Self { Key { alias: None, // this is retrieved later - sui_address: SuiAddress::from(&key), - public_base64_key: key.encode_base64(), - key_scheme: key.scheme().to_string(), + sui_address: SuiAddress::from(&pk), + public_base64_key: pk.encode_base64(), + key_scheme: pk.scheme().to_string(), mnemonic: None, - flag: key.flag(), - peer_id: anemo_styling(&key), + flag: pk.flag(), + peer_id: anemo_styling(&pk), } } } @@ -1180,29 +1215,41 @@ impl Debug for CommandOutput { } } -fn convert_private_key_to_base64(value: String) -> Result { - match Base64::decode(&value) { - Ok(decoded) => { - if decoded.len() != 33 { - return Err(anyhow!(format!("Private key is malformed and cannot base64 decode it. Fed 33 length but got {}", decoded.len()))); - } - info!("Hex encode"); - Ok(ConvertOutput::Hex(Hex::encode(&decoded[1..]))) - } +/// Converts legacy formatted private key to 33 bytes bech32 encoded private key or vice versa. +/// It can handle: +/// 1) Hex encoded 32 byte private key (assumes scheme is Ed25519), this is the legacy wallet format +/// 2) Base64 encoded 32 bytes private key (assumes scheme is Ed25519) +/// 3) Base64 encoded 33 bytes private key with flag. +/// 4) Bech32 encoded 33 bytes private key with flag. +fn convert_private_key_to_bech32(value: String) -> Result { + let skp = match SuiKeyPair::decode(&value) { + Ok(s) => s, Err(_) => match Hex::decode(&value) { Ok(decoded) => { if decoded.len() != 32 { - return Err(anyhow!(format!("Private key is malformed and cannot hex decode it. Expected 32 length but got {}", decoded.len()))); + return Err(anyhow!(format!( + "Invalid private key length, expected 32 but got {}", + decoded.len() + ))); } - let mut res = Vec::new(); - res.extend_from_slice(&[SignatureScheme::ED25519.flag()]); - res.extend_from_slice(&decoded); - info!("Base64 encode"); - Ok(ConvertOutput::Base64(Base64::encode(&res))) + SuiKeyPair::Ed25519(Ed25519KeyPair::from_bytes(&decoded)?) } - Err(_) => Err(anyhow!("Invalid private key format".to_string())), + Err(_) => match SuiKeyPair::decode_base64(&value) { + Ok(skp) => skp, + Err(_) => match Ed25519KeyPair::decode_base64(&value) { + Ok(kp) => SuiKeyPair::Ed25519(kp), + Err(_) => return Err(anyhow!("Invalid private key encoding")), + }, + }, }, - } + }; + + Ok(ConvertOutput { + bech32_with_flag: skp.encode().map_err(|_| anyhow!("Cannot encode keypair"))?, + base64_with_flag: skp.encode_base64(), + hex_without_flag: Hex::encode(&skp.to_bytes()[1..]), + scheme: skp.public().scheme().to_string(), + }) } fn anemo_styling(pk: &PublicKey) -> Option { diff --git a/crates/sui/src/unit_tests/keytool_tests.rs b/crates/sui/src/unit_tests/keytool_tests.rs index 2165746948e1b..477174a1f5d69 100644 --- a/crates/sui/src/unit_tests/keytool_tests.rs +++ b/crates/sui/src/unit_tests/keytool_tests.rs @@ -6,12 +6,16 @@ use std::str::FromStr; use crate::key_identity::KeyIdentity; use crate::keytool::read_authority_keypair_from_file; use crate::keytool::read_keypair_from_file; +use crate::keytool::CommandOutput; use super::write_keypair_to_file; use super::KeyToolCommand; use anyhow::Ok; +use fastcrypto::ed25519::Ed25519KeyPair; use fastcrypto::encoding::Base64; use fastcrypto::encoding::Encoding; +use fastcrypto::encoding::Hex; +use fastcrypto::traits::ToFromBytes; use rand::rngs::StdRng; use rand::SeedableRng; use shared_crypto::intent::Intent; @@ -159,14 +163,15 @@ async fn test_sui_operations_config() { // This is the hardcoded keystore in sui-operation: https://github.com/MystenLabs/sui-operations/blob/af04c9d3b61610dbb36401aff6bef29d06ef89f8/docker/config/generate/static/sui.keystore // If this test fails, address hardcoded in sui-operations is likely needed be updated. let kp = SuiKeyPair::decode_base64("ANRj4Rx5FZRehqwrctiLgZDPrY/3tI5+uJLCdaXPCj6C").unwrap(); - let contents = kp.encode_base64(); - let res = std::fs::write(path, contents); + let contents = vec![kp.encode_base64()]; + let res = std::fs::write(path, serde_json::to_string_pretty(&contents).unwrap()); assert!(res.is_ok()); - let kp_read = read_keypair_from_file(path1); + let read = FileBasedKeystore::new(&path1); + assert!(read.is_ok()); assert_eq!( SuiAddress::from_str("7d20dcdb2bca4f508ea9613994683eb4e76e9c4ed371169677c1be02aaf0b58e") .unwrap(), - SuiAddress::from(&kp_read.unwrap().public()) + read.unwrap().addresses()[0] ); // This is the hardcoded keystore in sui-operation: https://github.com/MystenLabs/sui-operations/blob/af04c9d3b61610dbb36401aff6bef29d06ef89f8/docker/config/generate/static/sui-benchmark.keystore @@ -174,14 +179,14 @@ async fn test_sui_operations_config() { let path2 = temp_dir.path().join("sui-benchmark.keystore"); let path3 = path2.clone(); let kp = SuiKeyPair::decode_base64("APCWxPNCbgGxOYKeMfPqPmXmwdNVyau9y4IsyBcmC14A").unwrap(); - let contents = kp.encode_base64(); - let res = std::fs::write(path2, contents); + let contents = vec![kp.encode_base64()]; + let res = std::fs::write(path2, serde_json::to_string_pretty(&contents).unwrap()); assert!(res.is_ok()); - let kp_read = read_keypair_from_file(path3); + let read = FileBasedKeystore::new(&path3); assert_eq!( SuiAddress::from_str("160ef6ce4f395208a12119c5011bf8d8ceb760e3159307c819bd0197d154d384") .unwrap(), - SuiAddress::from(&kp_read.unwrap().public()) + read.unwrap().addresses()[0] ); } @@ -202,27 +207,30 @@ async fn test_load_keystore_err() { } #[test] -async fn test_private_keys_ed25519() -> Result<(), anyhow::Error> { - // private key, base64, address - const TEST_CASES: &[(&str, &str, &str)] = &[ +async fn test_private_keys_import_export() -> Result<(), anyhow::Error> { + // private key in Bech32, private key in Hex, private key in Base64, derived Sui address in Hex + const TEST_CASES: &[(&str, &str, &str, &str)] = &[ ( + "suiprivkey1qzwant3kaegmjy4qxex93s0jzvemekkjmyv3r2sjwgnv2y479pgsywhveae", "0x9dd9ae36ee51b912a0364c58c1f21333bcdad2d91911aa127226c512be285102", "AJ3ZrjbuUbkSoDZMWMHyEzO82tLZGRGqEnImxRK+KFEC", "0x90f3e6d73b5730f16974f4df1d3441394ebae62186baf83608599f226455afa7", ), ( + "suiprivkey1qrh2sjl88rze74hwjndw3l26dqyz63tea5u9frtwcsqhmfk9vxdlx8cpv0g", "0xeea84be738c59f56ee94dae8fd5a68082d4579ed38548d6ec4017da6c5619bf3", "AO6oS+c4xZ9W7pTa6P1aaAgtRXntOFSNbsQBfabFYZvz", "0xfd233cd9a5dd7e577f16fa523427c75fbc382af1583c39fdf1c6747d2ed807a3", ), ( + "suiprivkey1qzg73qyvfz0wpnyectkl08nrhe4pgnu0vqx8gydu96qx7uj4wyr8gcrjlh3", "0x91e8808c489ee0cc99c2edf79e63be6a144f8f600c7411bc2e806f7255710674", "AJHogIxInuDMmcLt955jvmoUT49gDHQRvC6Ab3JVcQZ0", "0x81aaefa4a883e72e8b6ccd3bec307e25fe3d79b14e43b778695c55dcec42f4f0", ), ]; // assert correctness - for (private_key, base64, address) in TEST_CASES { + for (private_key, private_key_hex, private_key_base64, address) in TEST_CASES { let mut keystore = Keystore::from(InMemKeystore::new_insecure_for_tests(0)); KeyToolCommand::Import { alias: None, @@ -232,18 +240,51 @@ async fn test_private_keys_ed25519() -> Result<(), anyhow::Error> { } .execute(&mut keystore) .await?; - let kp = SuiKeyPair::decode_base64(base64).unwrap(); + let kp = SuiKeyPair::decode(private_key).unwrap(); + let kp_from_hex = SuiKeyPair::Ed25519( + Ed25519KeyPair::from_bytes(&Hex::decode(private_key_hex).unwrap()).unwrap(), + ); + assert_eq!(kp, kp_from_hex); + + let kp_from_base64 = SuiKeyPair::decode_base64(&private_key_base64).unwrap(); + assert_eq!(kp, kp_from_base64); + let addr = SuiAddress::from_str(address).unwrap(); assert_eq!(SuiAddress::from(&kp.public()), addr); assert!(keystore.addresses().contains(&addr)); + + // Export output shows the private key in Bech32 + let output = KeyToolCommand::Export { + address: Some(addr), + alias: None, + } + .execute(&mut keystore) + .await?; + match output { + CommandOutput::Export(exported) => { + assert_eq!(exported.exported_private_key, private_key.to_string()); + } + _ => panic!("unexpected output"), + } } - // assert failure when private key is malformed - for (private_key, _, _) in TEST_CASES { + for (private_key, _, _, addr) in TEST_CASES { let mut keystore = Keystore::from(InMemKeystore::new_insecure_for_tests(0)); + // assert failure when private key is malformed let output = KeyToolCommand::Import { alias: None, - input_string: private_key[..25].to_string(), + input_string: private_key[1..].to_string(), + key_scheme: SignatureScheme::ED25519, + derivation_path: None, + } + .execute(&mut keystore) + .await; + assert!(output.is_err()); + + // importing an hex encoded string should fail + let output = KeyToolCommand::Import { + alias: None, + input_string: addr.to_string(), key_scheme: SignatureScheme::ED25519, derivation_path: None, } @@ -258,9 +299,9 @@ async fn test_private_keys_ed25519() -> Result<(), anyhow::Error> { #[test] async fn test_mnemonics_ed25519() -> Result<(), anyhow::Error> { // Test case matches with /mysten/sui/sdk/typescript/test/unit/cryptography/ed25519-keypair.test.ts - const TEST_CASES: [[&str; 3]; 3] = [["film crazy soon outside stand loop subway crumble thrive popular green nuclear struggle pistol arm wife phrase warfare march wheat nephew ask sunny firm", "AN0JMHpDum3BhrVwnkylH0/HGRHBQ/fO/8+MYOawO8j6", "a2d14fad60c56049ecf75246a481934691214ce413e6a8ae2fe6834c173a6133"], - ["require decline left thought grid priority false tiny gasp angle royal system attack beef setup reward aunt skill wasp tray vital bounce inflict level", "AJrA997C1eVz6wYIp7bO8dpITSRBXpvg1m70/P3gusu2", "1ada6e6f3f3e4055096f606c746690f1108fcc2ca479055cc434a3e1d3f758aa"], - ["organ crash swim stick traffic remember army arctic mesh slice swear summer police vast chaos cradle squirrel hood useless evidence pet hub soap lake", "AAEMSIQeqyz09StSwuOW4MElQcZ+4jHW4/QcWlJEf5Yk", "e69e896ca10f5a77732769803cc2b5707f0ab9d4407afb5e4b4464b89769af14"]]; + const TEST_CASES: [[&str; 3]; 3] = [["film crazy soon outside stand loop subway crumble thrive popular green nuclear struggle pistol arm wife phrase warfare march wheat nephew ask sunny firm", "suiprivkey1qrwsjvr6gwaxmsvxk4cfun99ra8uwxg3c9pl0nhle7xxpe4s80y05ctazer", "a2d14fad60c56049ecf75246a481934691214ce413e6a8ae2fe6834c173a6133"], + ["require decline left thought grid priority false tiny gasp angle royal system attack beef setup reward aunt skill wasp tray vital bounce inflict level", "suiprivkey1qzdvpa77ct272ultqcy20dkw78dysnfyg90fhcxkdm60el0qht9mvzlsh4j", "1ada6e6f3f3e4055096f606c746690f1108fcc2ca479055cc434a3e1d3f758aa"], + ["organ crash swim stick traffic remember army arctic mesh slice swear summer police vast chaos cradle squirrel hood useless evidence pet hub soap lake", "suiprivkey1qqqscjyyr64jea849dfv9cukurqj2swx0m3rr4hr7sw955jy07tzgcde5ut", "e69e896ca10f5a77732769803cc2b5707f0ab9d4407afb5e4b4464b89769af14"]]; for t in TEST_CASES { let mut keystore = Keystore::from(InMemKeystore::new_insecure_for_tests(0)); @@ -272,7 +313,7 @@ async fn test_mnemonics_ed25519() -> Result<(), anyhow::Error> { } .execute(&mut keystore) .await?; - let kp = SuiKeyPair::decode_base64(t[1]).unwrap(); + let kp = SuiKeyPair::decode(t[1]).unwrap(); let addr = SuiAddress::from_str(t[2]).unwrap(); assert_eq!(SuiAddress::from(&kp.public()), addr); assert!(keystore.addresses().contains(&addr)); @@ -283,9 +324,9 @@ async fn test_mnemonics_ed25519() -> Result<(), anyhow::Error> { #[test] async fn test_mnemonics_secp256k1() -> Result<(), anyhow::Error> { // Test case matches with /mysten/sui/sdk/typescript/test/unit/cryptography/secp256k1-keypair.test.ts - const TEST_CASES: [[&str; 3]; 3] = [["film crazy soon outside stand loop subway crumble thrive popular green nuclear struggle pistol arm wife phrase warfare march wheat nephew ask sunny firm", "AQA9EYZoLXirIahsXHQMDfdi5DPQ72wLA79zke4EY6CP", "9e8f732575cc5386f8df3c784cd3ed1b53ce538da79926b2ad54dcc1197d2532"], - ["require decline left thought grid priority false tiny gasp angle royal system attack beef setup reward aunt skill wasp tray vital bounce inflict level", "Ae+TTptXI6WaJfzplSrphnrbTD5qgftfMX5kTyca7unQ", "9fd5a804ed6b46d36949ff7434247f0fd594673973ece24aede6b86a7b5dae01"], - ["organ crash swim stick traffic remember army arctic mesh slice swear summer police vast chaos cradle squirrel hood useless evidence pet hub soap lake", "AY2iJpGSDMhvGILPjjpyeM1bV4Jky979nUenB5kvQeSj", "60287d7c38dee783c2ab1077216124011774be6b0764d62bd05f32c88979d5c5"]]; + const TEST_CASES: [[&str; 3]; 3] = [["film crazy soon outside stand loop subway crumble thrive popular green nuclear struggle pistol arm wife phrase warfare march wheat nephew ask sunny firm", "suiprivkey1qyqr6yvxdqkh32ep4pk9caqvphmk9epn6rhkczcrhaeermsyvwsg783y9am", "9e8f732575cc5386f8df3c784cd3ed1b53ce538da79926b2ad54dcc1197d2532"], + ["require decline left thought grid priority false tiny gasp angle royal system attack beef setup reward aunt skill wasp tray vital bounce inflict level", "suiprivkey1q8hexn5m2u36tx39ln5e22hfseadknp7d2qlkhe30ejy7fc6am5aqkqpqsj", "9fd5a804ed6b46d36949ff7434247f0fd594673973ece24aede6b86a7b5dae01"], + ["organ crash swim stick traffic remember army arctic mesh slice swear summer police vast chaos cradle squirrel hood useless evidence pet hub soap lake", "suiprivkey1qxx6yf53jgxvsmccst8cuwnj0rx4k4uzvn9aalvag7ns0xf0g8j2x246jst", "60287d7c38dee783c2ab1077216124011774be6b0764d62bd05f32c88979d5c5"]]; for t in TEST_CASES { let mut keystore = Keystore::from(InMemKeystore::new_insecure_for_tests(0)); @@ -297,7 +338,7 @@ async fn test_mnemonics_secp256k1() -> Result<(), anyhow::Error> { } .execute(&mut keystore) .await?; - let kp = SuiKeyPair::decode_base64(t[1]).unwrap(); + let kp = SuiKeyPair::decode(t[1]).unwrap(); let addr = SuiAddress::from_str(t[2]).unwrap(); assert_eq!(SuiAddress::from(&kp.public()), addr); assert!(keystore.addresses().contains(&addr)); @@ -311,35 +352,34 @@ async fn test_mnemonics_secp256r1() -> Result<(), anyhow::Error> { const TEST_CASES: [[&str; 3]; 3] = [ [ "act wing dilemma glory episode region allow mad tourist humble muffin oblige", - "AiWmZXUcFpUF75H082F2RVJAABS5kcrvb8o09IPH9yUw", + "suiprivkey1qgj6vet4rstf2p00j860xctkg4fyqqq5hxgu4mm0eg60fq787ujnqs5wc8q", "0x4a822457f1970468d38dae8e63fb60eefdaa497d74d781f581ea2d137ec36f3a", ], [ "flag rebel cabbage captain minimum purpose long already valley horn enrich salt", - "AjaB6aLp4fQabx4NglfGz2Bf01TGKArV80NEOnqDwqNN", + "suiprivkey1qgmgr6dza8slgxn0rcxcy47xeas9l565cc5q440ngdzr575rc2356gzlq7a", "0xcd43ecb9dd32249ff5748f5e4d51855b01c9b1b8bbe7f8638bb8ab4cb463b920", ], [ "area renew bar language pudding trial small host remind supreme cabbage era", - "AtSIEzVpJv+bJH3XptEq63vsuK+te1KRSY7JsiuJfcdK", + "suiprivkey1qt2gsye4dyn0lxey0ht6d5f2ada7ew9044a49y2f3mymy2uf0hr55jmfze3", "0x0d9047b7e7b698cc09c955ea97b0c68c2be7fb3aebeb59edcc84b1fb87e0f28e", ], ]; - for t in TEST_CASES { + for [mnemonics, sk, address] in TEST_CASES { let mut keystore = Keystore::from(InMemKeystore::new_insecure_for_tests(0)); KeyToolCommand::Import { alias: None, - input_string: t[0].to_string(), + input_string: mnemonics.to_string(), key_scheme: SignatureScheme::Secp256r1, derivation_path: None, } .execute(&mut keystore) .await?; - let kp = SuiKeyPair::decode_base64(t[1]).unwrap(); - println!("{:?}", kp.public().encode_base64()); - let addr = SuiAddress::from_str(t[2]).unwrap(); + let kp = SuiKeyPair::decode(sk).unwrap(); + let addr = SuiAddress::from_str(address).unwrap(); assert_eq!(SuiAddress::from(&kp.public()), addr); assert!(keystore.addresses().contains(&addr)); } diff --git a/docs/content/references/cli/keytool.mdx b/docs/content/references/cli/keytool.mdx index e2b2fc22c3ee6..0f5a25ffa6f1a 100644 --- a/docs/content/references/cli/keytool.mdx +++ b/docs/content/references/cli/keytool.mdx @@ -13,8 +13,7 @@ The Sui CLI `keytool` command provides several command-level access for the mana Usage: sui keytool [OPTIONS] Commands: - convert Convert private key from wallet format (hex of 32 byte private key) to sui.keystore format (base64 of 33 byte flag || private key) or - vice versa + convert Convert private key from legacy formats (e.g. Hex or Base64) to Bech32 encoded 33 byte `flag || private key` begins with `suiprivkey` decode-tx-bytes Given a Base64 encoded transaction bytes, decode its components decode-multi-sig Given a Base64 encoded MultiSig signature, decode its components. If tx_bytes is passed in, verify the multisig generate Generate a new keypair with key scheme flag {ed25519 | secp256k1 | secp256r1} with optional derivation path, default to diff --git a/sdk/typescript/package.json b/sdk/typescript/package.json index b15ecd3ef32a8..07a605c5dd848 100644 --- a/sdk/typescript/package.json +++ b/sdk/typescript/package.json @@ -131,6 +131,7 @@ "@scure/bip32": "^1.3.1", "@scure/bip39": "^1.2.1", "@suchipi/femver": "^1.0.0", + "bech32": "^2.0.0", "superstruct": "^1.0.3", "tweetnacl": "^1.0.3" } diff --git a/sdk/typescript/src/cryptography/keypair.ts b/sdk/typescript/src/cryptography/keypair.ts index b6af0f9c0f45e..58c40789ae937 100644 --- a/sdk/typescript/src/cryptography/keypair.ts +++ b/sdk/typescript/src/cryptography/keypair.ts @@ -1,18 +1,25 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { toB64 } from '@mysten/bcs'; +import { bcs, toB64 } from '@mysten/bcs'; import { blake2b } from '@noble/hashes/blake2b'; +import { bech32 } from 'bech32'; -import { bcs } from '../bcs/index.js'; import { IntentScope, messageWithIntent } from './intent.js'; import type { PublicKey } from './publickey.js'; +import { SIGNATURE_FLAG_TO_SCHEME, SIGNATURE_SCHEME_TO_FLAG } from './signature-scheme.js'; import type { SignatureScheme } from './signature-scheme.js'; import type { SerializedSignature } from './signature.js'; import { toSerializedSignature } from './signature.js'; export const PRIVATE_KEY_SIZE = 32; export const LEGACY_PRIVATE_KEY_SIZE = 64; +export const SUI_PRIVATE_KEY_PREFIX = 'suiprivkey'; + +export type ParsedKeypair = { + schema: SignatureScheme; + secretKey: Uint8Array; +}; export type ExportedKeypair = { schema: SignatureScheme; @@ -85,9 +92,57 @@ export abstract class BaseSigner { abstract getPublicKey(): PublicKey; } +export abstract class Keypair extends BaseSigner { + /** + * This returns the Bech32 secret key string for this keypair. + */ + abstract getSecretKey(): string; + + /** + * This returns an exported keypair object, schema is the signature + * scheme name, and the private key field is a Bech32 encoded string + * of 33-byte `flag || private_key` that starts with `suiprivkey`. + */ + export(): ExportedKeypair { + return { + schema: this.getKeyScheme(), + privateKey: this.getSecretKey(), + }; + } +} + /** - * TODO: Document + * This returns an ParsedKeypair object based by validating the + * 33-byte Bech32 encoded string starting with `suiprivkey`, and + * parse out the signature scheme and the private key in bytes. */ -export abstract class Keypair extends BaseSigner { - abstract export(): ExportedKeypair; +export function decodeSuiPrivateKey(value: string): ParsedKeypair { + const { prefix, words } = bech32.decode(value); + if (prefix !== SUI_PRIVATE_KEY_PREFIX) { + throw new Error('invalid private key prefix'); + } + const extendedSecretKey = new Uint8Array(bech32.fromWords(words)); + const secretKey = extendedSecretKey.slice(1); + const signatureScheme = + SIGNATURE_FLAG_TO_SCHEME[extendedSecretKey[0] as keyof typeof SIGNATURE_FLAG_TO_SCHEME]; + return { + schema: signatureScheme, + secretKey: secretKey, + }; +} + +/** + * This returns a Bech32 encoded string starting with `suiprivkey`, + * encoding 33-byte `flag || bytes` for the given the 32-byte private + * key and its signature scheme. + */ +export function encodeSuiPrivateKey(bytes: Uint8Array, scheme: SignatureScheme): string { + if (bytes.length !== PRIVATE_KEY_SIZE) { + throw new Error('Invalid bytes length'); + } + const flag = SIGNATURE_SCHEME_TO_FLAG[scheme]; + const privKeyBytes = new Uint8Array(bytes.length + 1); + privKeyBytes.set([flag]); + privKeyBytes.set(bytes, 1); + return bech32.encode(SUI_PRIVATE_KEY_PREFIX, bech32.toWords(privKeyBytes)); } diff --git a/sdk/typescript/src/keypairs/ed25519/keypair.ts b/sdk/typescript/src/keypairs/ed25519/keypair.ts index 54edf5646c40b..bada16f20643f 100644 --- a/sdk/typescript/src/keypairs/ed25519/keypair.ts +++ b/sdk/typescript/src/keypairs/ed25519/keypair.ts @@ -1,11 +1,9 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { toB64 } from '@mysten/bcs'; import nacl from 'tweetnacl'; -import type { ExportedKeypair } from '../../cryptography/keypair.js'; -import { Keypair, PRIVATE_KEY_SIZE } from '../../cryptography/keypair.js'; +import { encodeSuiPrivateKey, Keypair, PRIVATE_KEY_SIZE } from '../../cryptography/keypair.js'; import { isValidHardenedPath, mnemonicToSeedHex } from '../../cryptography/mnemonics.js'; import type { SignatureScheme } from '../../cryptography/signature-scheme.js'; import { derivePath } from './ed25519-hd-key.js'; @@ -63,18 +61,6 @@ export class Ed25519Keypair extends Keypair { * This is NOT the private scalar which is result of hashing and bit clamping of * the raw secret key. * - * The sui.keystore key is a list of Base64 encoded `flag || privkey`. To import - * a key from sui.keystore to typescript, decode from base64 and remove the first - * flag byte after checking it is indeed the Ed25519 scheme flag 0x00 (See more - * on flag for signature scheme: https://github.com/MystenLabs/sui/blob/818406c5abdf7de1b80915a0519071eec3a5b1c7/crates/sui-types/src/crypto.rs#L1650): - * ``` - * import { Ed25519Keypair, fromB64 } from '@mysten/sui.js'; - * const raw = fromB64(t[1]); - * if (raw[0] !== 0 || raw.length !== PRIVATE_KEY_SIZE + 1) { - * throw new Error('invalid key'); - * } - * const imported = Ed25519Keypair.fromSecretKey(raw.slice(1)) - * ``` * @throws error if the provided secret key is invalid and validation is not skipped. * * @param secretKey secret key byte array @@ -109,6 +95,16 @@ export class Ed25519Keypair extends Keypair { return new Ed25519PublicKey(this.keypair.publicKey); } + /** + * The Bech32 secret key string for this Ed25519 keypair + */ + getSecretKey(): string { + return encodeSuiPrivateKey( + this.keypair.secretKey.slice(0, PRIVATE_KEY_SIZE), + this.getKeyScheme(), + ); + } + async sign(data: Uint8Array) { return this.signData(data); } @@ -156,14 +152,4 @@ export class Ed25519Keypair extends Keypair { return Ed25519Keypair.fromSecretKey(key); } - - /** - * This returns an exported keypair object, the private key field is the pure 32-byte seed. - */ - export(): ExportedKeypair { - return { - schema: 'ED25519', - privateKey: toB64(this.keypair.secretKey.slice(0, PRIVATE_KEY_SIZE)), - }; - } } diff --git a/sdk/typescript/src/keypairs/secp256k1/keypair.ts b/sdk/typescript/src/keypairs/secp256k1/keypair.ts index c150a4e1c928a..787bcffac56c0 100644 --- a/sdk/typescript/src/keypairs/secp256k1/keypair.ts +++ b/sdk/typescript/src/keypairs/secp256k1/keypair.ts @@ -1,15 +1,13 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { toB64 } from '@mysten/bcs'; import { secp256k1 } from '@noble/curves/secp256k1'; import { blake2b } from '@noble/hashes/blake2b'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex } from '@noble/hashes/utils'; import { HDKey } from '@scure/bip32'; -import type { ExportedKeypair } from '../../cryptography/keypair.js'; -import { Keypair } from '../../cryptography/keypair.js'; +import { encodeSuiPrivateKey, Keypair } from '../../cryptography/keypair.js'; import { isValidBIP32Path, mnemonicToSeed } from '../../cryptography/mnemonics.js'; import type { PublicKey } from '../../cryptography/publickey.js'; import type { SignatureScheme } from '../../cryptography/signature-scheme.js'; @@ -109,6 +107,12 @@ export class Secp256k1Keypair extends Keypair { getPublicKey(): PublicKey { return new Secp256k1PublicKey(this.keypair.publicKey); } + /** + * The Bech32 secret key string for this Secp256k1 keypair + */ + getSecretKey(): string { + return encodeSuiPrivateKey(this.keypair.secretKey, this.getKeyScheme()); + } async sign(data: Uint8Array) { return this.signData(data); @@ -148,11 +152,4 @@ export class Secp256k1Keypair extends Keypair { secretKey: key.privateKey, }); } - - export(): ExportedKeypair { - return { - schema: 'Secp256k1', - privateKey: toB64(this.keypair.secretKey), - }; - } } diff --git a/sdk/typescript/src/keypairs/secp256r1/keypair.ts b/sdk/typescript/src/keypairs/secp256r1/keypair.ts index c0eacdce61ac7..151aa73851136 100644 --- a/sdk/typescript/src/keypairs/secp256r1/keypair.ts +++ b/sdk/typescript/src/keypairs/secp256r1/keypair.ts @@ -1,15 +1,13 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { toB64 } from '@mysten/bcs'; import { secp256r1 } from '@noble/curves/p256'; import { blake2b } from '@noble/hashes/blake2b'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex } from '@noble/hashes/utils'; import { HDKey } from '@scure/bip32'; -import type { ExportedKeypair } from '../../cryptography/keypair.js'; -import { Keypair } from '../../cryptography/keypair.js'; +import { encodeSuiPrivateKey, Keypair } from '../../cryptography/keypair.js'; import { isValidBIP32Path, mnemonicToSeed } from '../../cryptography/mnemonics.js'; import type { PublicKey } from '../../cryptography/publickey.js'; import type { SignatureScheme } from '../../cryptography/signature-scheme.js'; @@ -110,6 +108,13 @@ export class Secp256r1Keypair extends Keypair { return new Secp256r1PublicKey(this.keypair.publicKey); } + /** + * The Bech32 secret key string for this Secp256r1 keypair + */ + getSecretKey(): string { + return encodeSuiPrivateKey(this.keypair.secretKey, this.getKeyScheme()); + } + async sign(data: Uint8Array) { return this.signData(data); } @@ -143,11 +148,4 @@ export class Secp256r1Keypair extends Keypair { const privateKey = HDKey.fromMasterSeed(mnemonicToSeed(mnemonics)).derive(path).privateKey; return Secp256r1Keypair.fromSecretKey(privateKey!); } - - export(): ExportedKeypair { - return { - schema: 'Secp256r1', - privateKey: toB64(this.keypair.secretKey), - }; - } } diff --git a/sdk/typescript/test/unit/cryptography/ed25519-keypair.test.ts b/sdk/typescript/test/unit/cryptography/ed25519-keypair.test.ts index e32f4a4caafc0..d2e0b162a02bd 100644 --- a/sdk/typescript/test/unit/cryptography/ed25519-keypair.test.ts +++ b/sdk/typescript/test/unit/cryptography/ed25519-keypair.test.ts @@ -1,11 +1,12 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { fromB64, toB58, toB64 } from '@mysten/bcs'; +import { fromB64, toB58 } from '@mysten/bcs'; import nacl from 'tweetnacl'; import { describe, expect, it } from 'vitest'; import { TransactionBlock } from '../../../src/builder'; +import { decodeSuiPrivateKey } from '../../../src/cryptography/keypair'; import { Ed25519Keypair } from '../../../src/keypairs/ed25519'; import { verifyPersonalMessage, verifyTransactionBlock } from '../../../src/verify'; @@ -16,24 +17,21 @@ const PRIVATE_KEY_SIZE = 32; const TEST_CASES = [ [ 'film crazy soon outside stand loop subway crumble thrive popular green nuclear struggle pistol arm wife phrase warfare march wheat nephew ask sunny firm', - 'AN0JMHpDum3BhrVwnkylH0/HGRHBQ/fO/8+MYOawO8j6', + 'suiprivkey1qrwsjvr6gwaxmsvxk4cfun99ra8uwxg3c9pl0nhle7xxpe4s80y05ctazer', '0xa2d14fad60c56049ecf75246a481934691214ce413e6a8ae2fe6834c173a6133', ], [ 'require decline left thought grid priority false tiny gasp angle royal system attack beef setup reward aunt skill wasp tray vital bounce inflict level', - 'AJrA997C1eVz6wYIp7bO8dpITSRBXpvg1m70/P3gusu2', + 'suiprivkey1qzdvpa77ct272ultqcy20dkw78dysnfyg90fhcxkdm60el0qht9mvzlsh4j', '0x1ada6e6f3f3e4055096f606c746690f1108fcc2ca479055cc434a3e1d3f758aa', ], [ 'organ crash swim stick traffic remember army arctic mesh slice swear summer police vast chaos cradle squirrel hood useless evidence pet hub soap lake', - 'AAEMSIQeqyz09StSwuOW4MElQcZ+4jHW4/QcWlJEf5Yk', + 'suiprivkey1qqqscjyyr64jea849dfv9cukurqj2swx0m3rr4hr7sw955jy07tzgcde5ut', '0xe69e896ca10f5a77732769803cc2b5707f0ab9d4407afb5e4b4464b89769af14', ], ]; -const TEST_MNEMONIC = - 'result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss'; - describe('ed25519-keypair', () => { it('new keypair', () => { const keypair = new Ed25519Keypair(); @@ -51,21 +49,18 @@ describe('ed25519-keypair', () => { it('create keypair from secret key and mnemonics matches keytool', () => { for (const t of TEST_CASES) { - // Keypair derived from mnemonic + // Keypair derived from mnemonic. const keypair = Ed25519Keypair.deriveKeypair(t[0]); expect(keypair.getPublicKey().toSuiAddress()).toEqual(t[2]); - // Keypair derived from 32-byte secret key - const raw = fromB64(t[1]); - if (raw[0] !== 0 || raw.length !== PRIVATE_KEY_SIZE + 1) { - throw new Error('invalid key'); - } - const imported = Ed25519Keypair.fromSecretKey(raw.slice(1)); - expect(imported.getPublicKey().toSuiAddress()).toEqual(t[2]); - - // Exported secret key matches the 32-byte secret key. - const exported = imported.export(); - expect(exported.privateKey).toEqual(toB64(raw.slice(1))); + // Decode Sui private key from Bech32 string + const parsed = decodeSuiPrivateKey(t[1]); + const kp = Ed25519Keypair.fromSecretKey(parsed.secretKey); + expect(kp.getPublicKey().toSuiAddress()).toEqual(t[2]); + + // Exported keypair matches the Bech32 encoded secret key. + const exported = kp.export(); + expect(exported.privateKey).toEqual(t[1]); } }); @@ -90,7 +85,7 @@ describe('ed25519-keypair', () => { }); it('incorrect coin type node for ed25519 derivation path', () => { - const keypair = Ed25519Keypair.deriveKeypair(TEST_MNEMONIC, `m/44'/784'/0'/0'/0'`); + const keypair = Ed25519Keypair.deriveKeypair(TEST_CASES[0][0], `m/44'/784'/0'/0'/0'`); const signData = new TextEncoder().encode('hello world'); const signature = keypair.signData(signData); @@ -104,13 +99,13 @@ describe('ed25519-keypair', () => { it('incorrect coin type node for ed25519 derivation path', () => { expect(() => { - Ed25519Keypair.deriveKeypair(TEST_MNEMONIC, `m/44'/0'/0'/0'/0'`); + Ed25519Keypair.deriveKeypair(TEST_CASES[0][0], `m/44'/0'/0'/0'/0'`); }).toThrow('Invalid derivation path'); }); it('incorrect purpose node for ed25519 derivation path', () => { expect(() => { - Ed25519Keypair.deriveKeypair(TEST_MNEMONIC, `m/54'/784'/0'/0'/0'`); + Ed25519Keypair.deriveKeypair(TEST_CASES[0][0], `m/54'/784'/0'/0'/0'`); }).toThrow('Invalid derivation path'); }); diff --git a/sdk/typescript/test/unit/cryptography/secp256k1-keypair.test.ts b/sdk/typescript/test/unit/cryptography/secp256k1-keypair.test.ts index 7d0738a8dc2f1..1f16643fb95f3 100644 --- a/sdk/typescript/test/unit/cryptography/secp256k1-keypair.test.ts +++ b/sdk/typescript/test/unit/cryptography/secp256k1-keypair.test.ts @@ -7,6 +7,7 @@ import { sha256 } from '@noble/hashes/sha256'; import { describe, expect, it } from 'vitest'; import { TransactionBlock } from '../../../src/builder'; +import { decodeSuiPrivateKey } from '../../../src/cryptography/keypair'; import { DEFAULT_SECP256K1_DERIVATION_PATH, Secp256k1Keypair, @@ -37,17 +38,17 @@ export const INVALID_SECP256K1_PUBLIC_KEY = Uint8Array.from(Array(PRIVATE_KEY_SI const TEST_CASES = [ [ 'film crazy soon outside stand loop subway crumble thrive popular green nuclear struggle pistol arm wife phrase warfare march wheat nephew ask sunny firm', - 'AQA9EYZoLXirIahsXHQMDfdi5DPQ72wLA79zke4EY6CP', + 'suiprivkey1qyqr6yvxdqkh32ep4pk9caqvphmk9epn6rhkczcrhaeermsyvwsg783y9am', '0x9e8f732575cc5386f8df3c784cd3ed1b53ce538da79926b2ad54dcc1197d2532', ], [ 'require decline left thought grid priority false tiny gasp angle royal system attack beef setup reward aunt skill wasp tray vital bounce inflict level', - 'Ae+TTptXI6WaJfzplSrphnrbTD5qgftfMX5kTyca7unQ', + 'suiprivkey1q8hexn5m2u36tx39ln5e22hfseadknp7d2qlkhe30ejy7fc6am5aqkqpqsj', '0x9fd5a804ed6b46d36949ff7434247f0fd594673973ece24aede6b86a7b5dae01', ], [ 'organ crash swim stick traffic remember army arctic mesh slice swear summer police vast chaos cradle squirrel hood useless evidence pet hub soap lake', - 'AY2iJpGSDMhvGILPjjpyeM1bV4Jky979nUenB5kvQeSj', + 'suiprivkey1qxx6yf53jgxvsmccst8cuwnj0rx4k4uzvn9aalvag7ns0xf0g8j2x246jst', '0x60287d7c38dee783c2ab1077216124011774be6b0764d62bd05f32c88979d5c5', ], ]; @@ -135,18 +136,14 @@ describe('secp256k1-keypair', () => { const keypair = Secp256k1Keypair.deriveKeypair(t[0]); expect(keypair.getPublicKey().toSuiAddress()).toEqual(t[2]); - // Keypair derived from 32-byte secret key - const raw = fromB64(t[1]); - // The secp256k1 flag is 0x01. See more at [enum SignatureScheme]. - if (raw[0] !== 1 || raw.length !== PRIVATE_KEY_SIZE + 1) { - throw new Error('invalid key'); - } - const imported = Secp256k1Keypair.fromSecretKey(raw.slice(1)); - expect(imported.getPublicKey().toSuiAddress()).toEqual(t[2]); - - // Exported secret key matches the 32-byte secret key. - const exported = imported.export(); - expect(exported.privateKey).toEqual(toB64(raw.slice(1))); + // Keypair derived from Bech32 string. + const parsed = decodeSuiPrivateKey(t[1]); + const kp = Secp256k1Keypair.fromSecretKey(parsed.secretKey); + expect(kp.getPublicKey().toSuiAddress()).toEqual(t[2]); + + // Exported keypair matches the Bech32 encoded secret key. + const exported = kp.export(); + expect(exported.privateKey).toEqual(t[1]); } }); diff --git a/sdk/typescript/test/unit/cryptography/secp256r1-keypair.test.ts b/sdk/typescript/test/unit/cryptography/secp256r1-keypair.test.ts index 7382e1cf975b7..ac6fe2b7381c3 100644 --- a/sdk/typescript/test/unit/cryptography/secp256r1-keypair.test.ts +++ b/sdk/typescript/test/unit/cryptography/secp256r1-keypair.test.ts @@ -7,6 +7,7 @@ import { sha256 } from '@noble/hashes/sha256'; import { describe, expect, it } from 'vitest'; import { TransactionBlock } from '../../../src/builder'; +import { decodeSuiPrivateKey } from '../../../src/cryptography/keypair'; import { DEFAULT_SECP256R1_DERIVATION_PATH, Secp256r1Keypair, @@ -35,26 +36,21 @@ export const INVALID_SECP256R1_PUBLIC_KEY = Uint8Array.from(Array(PRIVATE_KEY_SI const TEST_CASES = [ [ 'act wing dilemma glory episode region allow mad tourist humble muffin oblige', - 'AiWmZXUcFpUF75H082F2RVJAABS5kcrvb8o09IPH9yUw', + 'suiprivkey1qgj6vet4rstf2p00j860xctkg4fyqqq5hxgu4mm0eg60fq787ujnqs5wc8q', '0x4a822457f1970468d38dae8e63fb60eefdaa497d74d781f581ea2d137ec36f3a', - 'AgLL1StURWGAemn/8rFn3FsRDVfO/Ssf+bbFaugGBtd70w==', ], [ 'flag rebel cabbage captain minimum purpose long already valley horn enrich salt', - 'AjaB6aLp4fQabx4NglfGz2Bf01TGKArV80NEOnqDwqNN', + 'suiprivkey1qgmgr6dza8slgxn0rcxcy47xeas9l565cc5q440ngdzr575rc2356gzlq7a', '0xcd43ecb9dd32249ff5748f5e4d51855b01c9b1b8bbe7f8638bb8ab4cb463b920', - 'AgM2aZKpmTrKs8HuyvOZQ2TCQ0s7ql5Agf4giTcu6FtPHA==', ], [ 'area renew bar language pudding trial small host remind supreme cabbage era', - 'AtSIEzVpJv+bJH3XptEq63vsuK+te1KRSY7JsiuJfcdK', + 'suiprivkey1qt2gsye4dyn0lxey0ht6d5f2ada7ew9044a49y2f3mymy2uf0hr55jmfze3', '0x0d9047b7e7b698cc09c955ea97b0c68c2be7fb3aebeb59edcc84b1fb87e0f28e', - 'AgJ0BrsxGK2gI3pl7m6L67IXusKo99w4tMDDZCwXhnUm/Q==', ], ]; -const TEST_MNEMONIC = 'open genre century trouble allow pioneer love task chat salt drive income'; - describe('secp256r1-keypair', () => { it('new keypair', () => { const keypair = new Secp256r1Keypair(); @@ -135,34 +131,26 @@ describe('secp256r1-keypair', () => { const keypair = Secp256r1Keypair.deriveKeypair(t[0]); expect(keypair.getPublicKey().toSuiAddress()).toEqual(t[2]); - // Keypair derived from 32-byte secret key - const raw = fromB64(t[1]); - expect(raw.length).toEqual(PRIVATE_KEY_SIZE + 1); - expect(keypair.export().privateKey).toEqual(toB64(raw.slice(1))); - - // The secp256r1 flag is 0x02. See more at [enum SignatureScheme]. - if (raw[0] !== 2 || raw.length !== PRIVATE_KEY_SIZE + 1) { - throw new Error('invalid key'); - } - - const imported = Secp256r1Keypair.fromSecretKey(raw.slice(1)); - expect(imported.getPublicKey().toSuiAddress()).toEqual(t[2]); + // Keypair derived from Bech32 string. + const parsed = decodeSuiPrivateKey(t[1]); + const kp = Secp256r1Keypair.fromSecretKey(parsed.secretKey); + expect(kp.getPublicKey().toSuiAddress()).toEqual(t[2]); - // Exported secret key matches the 32-byte secret key. - const exported = imported.export(); - expect(exported.privateKey).toEqual(toB64(raw.slice(1))); + // Exported keypair matches the Bech32 encoded secret key. + const exported = kp.export(); + expect(exported.privateKey).toEqual(t[1]); } }); it('incorrect purpose node for secp256r1 derivation path', () => { expect(() => { - Secp256r1Keypair.deriveKeypair(TEST_MNEMONIC, `m/54'/784'/0'/0'/0'`); + Secp256r1Keypair.deriveKeypair(TEST_CASES[0][0], `m/54'/784'/0'/0'/0'`); }).toThrow('Invalid derivation path'); }); it('incorrect hardened path for secp256k1 key derivation', () => { expect(() => { - Secp256r1Keypair.deriveKeypair(TEST_MNEMONIC, `m/44'/784'/0'/0'/0'`); + Secp256r1Keypair.deriveKeypair(TEST_CASES[0][0], `m/44'/784'/0'/0'/0'`); }).toThrow('Invalid derivation path'); }); From 9dd806a694ef570d93af105014c007154d4058f9 Mon Sep 17 00:00:00 2001 From: Joy Wang <108701016+joyqvq@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:11:46 +0800 Subject: [PATCH 02/11] address keytool comments Co-authored-by: stefan-mysten <135084671+stefan-mysten@users.noreply.github.com> --- crates/sui/src/keytool.rs | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/crates/sui/src/keytool.rs b/crates/sui/src/keytool.rs index 35efda8875304..d8c59491d13ed 100644 --- a/crates/sui/src/keytool.rs +++ b/crates/sui/src/keytool.rs @@ -120,6 +120,9 @@ pub enum KeyToolCommand { /// Output the private key of the given address in Sui CLI Keystore as Bech32 encoded string starting /// with `suiprivkey`. If the alias is provided, the private key for the given alias will be exported. Export { + #[clap(long)] + address: KeyIdentity, + }, #[clap(long)] alias: Option, address: Option, @@ -594,19 +597,9 @@ impl KeyToolCommand { } } } - KeyToolCommand::Export { alias, address } => { - let skp = match alias { - Some(a) => { - let address = keystore.get_address_by_alias(a)?; - keystore.get_key(address)? - } - None => match address { - Some(a) => keystore.get_key(&a)?, - None => { - return Err(anyhow!("Must provide either alias or address")); - } - }, - }; + KeyToolCommand::Export { address } => { + let address = get_identity_address_from_keystore(address, keystore)?; + let skp = keystore.get_key(&address)?; let key = ExportedKey { exported_private_key: skp .encode() From b6885b046898434a478987e15e634a9ee2eab61d Mon Sep 17 00:00:00 2001 From: Joy Wang <108701016+joyqvq@users.noreply.github.com> Date: Wed, 10 Jan 2024 23:23:34 +0800 Subject: [PATCH 03/11] lint and rebase --- .../ui/app/pages/accounts/ExportAccountPage.tsx | 2 -- crates/sui/src/client_commands.rs | 2 +- crates/sui/src/keytool.rs | 14 +++++--------- crates/sui/src/unit_tests/keytool_tests.rs | 5 ++--- sdk/enoki/src/EnokiKeypair.ts | 4 ++++ sdk/typescript/src/cryptography/keypair.ts | 4 ++-- 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx b/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx index 925aeaf8ec5eb..388a2ae8eeff9 100644 --- a/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx +++ b/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { useBackgroundClient } from '_src/ui/app/hooks/useBackgroundClient'; -import { fromB64 } from '@mysten/sui.js/utils'; -import { bytesToHex } from '@noble/hashes/utils'; import { useMutation } from '@tanstack/react-query'; import { Navigate, useNavigate, useParams } from 'react-router-dom'; diff --git a/crates/sui/src/client_commands.rs b/crates/sui/src/client_commands.rs index baa70d1190681..5e3d9cdbd48ce 100644 --- a/crates/sui/src/client_commands.rs +++ b/crates/sui/src/client_commands.rs @@ -1402,7 +1402,7 @@ impl SuiClientCommands { )); } - if let Some(address) = address.clone() { + if let Some(address) = address { let address = get_identity_address(Some(address), context)?; if !context.config.keystore.addresses().contains(&address) { return Err(anyhow!("Address {} not managed by wallet", address)); diff --git a/crates/sui/src/keytool.rs b/crates/sui/src/keytool.rs index d8c59491d13ed..a3f28e0edaf21 100644 --- a/crates/sui/src/keytool.rs +++ b/crates/sui/src/keytool.rs @@ -117,15 +117,11 @@ pub enum KeyToolCommand { key_scheme: SignatureScheme, derivation_path: Option, }, - /// Output the private key of the given address in Sui CLI Keystore as Bech32 encoded string starting - /// with `suiprivkey`. If the alias is provided, the private key for the given alias will be exported. + /// Output the private key of the given key identity in Sui CLI Keystore as Bech32 + /// encoded string starting with `suiprivkey`. Export { #[clap(long)] - address: KeyIdentity, - }, - #[clap(long)] - alias: Option, - address: Option, + key_identity: KeyIdentity, }, /// List all keys by its Sui address, Base64 encoded public key, key scheme name in /// sui.keystore. @@ -597,8 +593,8 @@ impl KeyToolCommand { } } } - KeyToolCommand::Export { address } => { - let address = get_identity_address_from_keystore(address, keystore)?; + KeyToolCommand::Export { key_identity } => { + let address = get_identity_address_from_keystore(key_identity, keystore)?; let skp = keystore.get_key(&address)?; let key = ExportedKey { exported_private_key: skp diff --git a/crates/sui/src/unit_tests/keytool_tests.rs b/crates/sui/src/unit_tests/keytool_tests.rs index 477174a1f5d69..28a4bd160f85a 100644 --- a/crates/sui/src/unit_tests/keytool_tests.rs +++ b/crates/sui/src/unit_tests/keytool_tests.rs @@ -246,7 +246,7 @@ async fn test_private_keys_import_export() -> Result<(), anyhow::Error> { ); assert_eq!(kp, kp_from_hex); - let kp_from_base64 = SuiKeyPair::decode_base64(&private_key_base64).unwrap(); + let kp_from_base64 = SuiKeyPair::decode_base64(private_key_base64).unwrap(); assert_eq!(kp, kp_from_base64); let addr = SuiAddress::from_str(address).unwrap(); @@ -255,8 +255,7 @@ async fn test_private_keys_import_export() -> Result<(), anyhow::Error> { // Export output shows the private key in Bech32 let output = KeyToolCommand::Export { - address: Some(addr), - alias: None, + key_identity: KeyIdentity::Address(addr), } .execute(&mut keystore) .await?; diff --git a/sdk/enoki/src/EnokiKeypair.ts b/sdk/enoki/src/EnokiKeypair.ts index 0d5fc61f49b0a..bfeeeb7129baf 100644 --- a/sdk/enoki/src/EnokiKeypair.ts +++ b/sdk/enoki/src/EnokiKeypair.ts @@ -95,4 +95,8 @@ export class EnokiKeypair extends Keypair { export(): never { throw new Error('EnokiKeypair does not support exporting'); } + + getSecretKey(): never { + throw new Error('EnokiKeypair does not support get secret key'); + } } diff --git a/sdk/typescript/src/cryptography/keypair.ts b/sdk/typescript/src/cryptography/keypair.ts index 58c40789ae937..2d812957b1eed 100644 --- a/sdk/typescript/src/cryptography/keypair.ts +++ b/sdk/typescript/src/cryptography/keypair.ts @@ -112,8 +112,8 @@ export abstract class Keypair extends BaseSigner { } /** - * This returns an ParsedKeypair object based by validating the - * 33-byte Bech32 encoded string starting with `suiprivkey`, and + * This returns an ParsedKeypair object based by validating the + * 33-byte Bech32 encoded string starting with `suiprivkey`, and * parse out the signature scheme and the private key in bytes. */ export function decodeSuiPrivateKey(value: string): ParsedKeypair { From ae61db52db32a18b5d3ccee462da2506293ad23d Mon Sep 17 00:00:00 2001 From: Joy Wang <108701016+joyqvq@users.noreply.github.com> Date: Thu, 11 Jan 2024 00:12:12 +0800 Subject: [PATCH 04/11] fix rust test --- crates/sui-keys/tests/tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/sui-keys/tests/tests.rs b/crates/sui-keys/tests/tests.rs index b20f655fabaa3..0484eae84bb69 100644 --- a/crates/sui-keys/tests/tests.rs +++ b/crates/sui-keys/tests/tests.rs @@ -5,6 +5,7 @@ use std::fs; use std::str::FromStr; use fastcrypto::hash::HashFunction; +use fastcrypto::traits::EncodeDecodeBase64; use sui_keys::key_derive::generate_new_key; use tempfile::TempDir; @@ -109,7 +110,7 @@ fn keystore_no_aliases() { let temp_dir = TempDir::new().unwrap(); let mut keystore_path = temp_dir.path().join("sui.keystore"); let (_, keypair, _, _) = generate_new_key(SignatureScheme::ED25519, None, None).unwrap(); - let private_keys = vec![keypair.encode().unwrap()]; + let private_keys = vec![keypair.encode_base64()]; let keystore_data = serde_json::to_string_pretty(&private_keys).unwrap(); fs::write(&keystore_path, keystore_data).unwrap(); From 7f8888cc5ed41b9f4a4de367efdfd4cb3a20bc13 Mon Sep 17 00:00:00 2001 From: Joy Wang <108701016+joyqvq@users.noreply.github.com> Date: Tue, 16 Jan 2024 09:39:29 +0800 Subject: [PATCH 05/11] update pnpm lockfile --- pnpm-lock.yaml | 144 ++++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 67 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46049353234b4..ef5f0dc5f3fa7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -622,6 +622,9 @@ importers: '@tanstack/react-query-persist-client': specifier: ^4.29.25 version: 4.29.25(@tanstack/react-query@5.0.0) + bech32: + specifier: ^2.0.0 + version: 2.0.0 bignumber.js: specifier: ^9.1.1 version: 9.1.1 @@ -1637,6 +1640,9 @@ importers: '@suchipi/femver': specifier: ^1.0.0 version: 1.0.0 + bech32: + specifier: ^2.0.0 + version: 2.0.0 superstruct: specifier: ^1.0.3 version: 1.0.3 @@ -3917,7 +3923,7 @@ packages: resolution: {integrity: sha512-dVPVVqQG0FixjM9CG/+8eHTsCAxRKqmNh6H69IpruolPlnEF1611f2AoLK8TijTSAsqBSclKd4WHs1KUb/LdJw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/selector-specificity': 3.0.0(postcss-selector-parser@6.0.13) postcss: 8.4.31 @@ -3928,7 +3934,7 @@ packages: resolution: {integrity: sha512-b1ptNkr1UWP96EEHqKBWWaV5m/0hgYGctgA/RVZhONeP1L3T/8hwoqDm9bB23yVCfOgE9U93KI9j06+pEkJTvw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-color-parser': 1.2.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -3941,7 +3947,7 @@ packages: resolution: {integrity: sha512-QGXjGugTluqFZWzVf+S3wCiRiI0ukXlYqCi7OnpDotP/zaVTyl/aqZujLFzTOXy24BoWnu89frGMc79ohY5eog==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-color-parser': 1.2.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -3954,7 +3960,7 @@ packages: resolution: {integrity: sha512-ntkGj+1uDa/u6lpjPxnkPcjJn7ChO/Kcy08YxctOZI7vwtrdYvFhmE476dq8bj1yna306+jQ9gzXIG/SWfOaRg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -3964,7 +3970,7 @@ packages: resolution: {integrity: sha512-jGSRoZmw+5ZQ8Y39YN4zc3LIfRYdoiz5vMQzgADOdn7Bc4VBueUMsmMn1gX4ED76Pp7/f+Xvi0WrCFiOM2hkyw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-color-parser': 1.2.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -3977,7 +3983,7 @@ packages: resolution: {integrity: sha512-a4gbFxgF6yJVGdXSAaDCZE4WMi7yu3PgPaBKpvqefyG1+R2zCwOboXYLzn2GVUyTAHij+ZRFDQUYUVODAQnf6g==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-color-parser': 1.2.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -3989,7 +3995,7 @@ packages: resolution: {integrity: sha512-FH3+zfOfsgtX332IIkRDxiYLmgwyNk49tfltpC6dsZaO4RV2zWY6x9VMIC5cjvmjlDO7DIThpzqaqw2icT8RbQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/postcss-progressive-custom-properties': 3.0.0(postcss@8.4.31) postcss: 8.4.31 @@ -4000,7 +4006,7 @@ packages: resolution: {integrity: sha512-0I6siRcDymG3RrkNTSvHDMxTQ6mDyYE8awkcaHNgtYacd43msl+4ZWDfQ1yZQ/viczVWjqJkLmPiRHSgxn5nZA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/selector-specificity': 3.0.0(postcss-selector-parser@6.0.13) postcss: 8.4.31 @@ -4011,7 +4017,7 @@ packages: resolution: {integrity: sha512-Wki4vxsF6icRvRz8eF9bPpAvwaAt0RHwhVOyzfoFg52XiIMjb6jcbHkGxwpJXP4DVrnFEwpwmrz5aTRqOW82kg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 dev: true @@ -4020,7 +4026,7 @@ packages: resolution: {integrity: sha512-lCQ1aX8c5+WI4t5EoYf3alTzJNNocMqTb+u1J9CINdDhFh1fjovqK+0aHalUHsNstZmzFPNzIkU4Mb3eM9U8SA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -4030,7 +4036,7 @@ packages: resolution: {integrity: sha512-KZIJXAvXqePyk2QHOYYy5YUVyjiqRTC5lgOjJJsjKIwNnGvOBqD4ypWUB94WlWO0yzNwIMs+JYnTP4jGEbKzhA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-tokenizer': 2.1.1 postcss: 8.4.31 @@ -4040,7 +4046,7 @@ packages: resolution: {integrity: sha512-gKwnAgX8wM3cNJ+nn2st8Cu25H/ZT43Z3CQE54rJPn4aD2gi4/ibXga+IZNwRUSGR7/zJtsoWrq9aHf4qXgYRg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-calc': 1.1.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -4053,7 +4059,7 @@ packages: resolution: {integrity: sha512-7gxwEFeKlzql44msYZp7hqxpyxRqE1rt/TcUnDgnqqeOZI5GVHUULIrrzVnMq0YiaQROw/ugy8hov4e8V46GHw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) '@csstools/css-tokenizer': 2.1.1 @@ -4065,7 +4071,7 @@ packages: resolution: {integrity: sha512-HsB66aDWAouOwD/GcfDTS0a7wCuVWaTpXcjl5VKP0XvFxDiU+r0T8FG7xgb6ovZNZ+qzvGIwRM+CLHhDgXrYgQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -4075,7 +4081,7 @@ packages: resolution: {integrity: sha512-6Nw55PRXEKEVqn3bzA8gRRPYxr5tf5PssvcE5DRA/nAxKgKtgNZMCHCSd1uxTCWeyLnkf6h5tYRSB0P1Vh/K/A==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -4085,7 +4091,7 @@ packages: resolution: {integrity: sha512-SQgh//VauJwat3qEwOw6t+Y9l8/dKooDnY3tD/o6qpcSjOvGqSsPeY+0QWWeAXYTtaddXSz4YmPohRRTsNlZGg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-color-parser': 1.2.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -4098,7 +4104,7 @@ packages: resolution: {integrity: sha512-Zd8ojyMlsL919TBExQ1I0CTpBDdyCpH/yOdqatZpuC3sd22K4SwC7+Yez3Q/vmXMWSAl+shjNeFZ7JMyxMjK+Q==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -4108,7 +4114,7 @@ packages: resolution: {integrity: sha512-2/D3CCL9DN2xhuUTP8OKvKnaqJ1j4yZUxuGLsCUOQ16wnDAuMLKLkflOmZF5tsPh/02VPeXRmqIN+U595WAulw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -4118,7 +4124,7 @@ packages: resolution: {integrity: sha512-2hz6pwJYgr/Uuj6657Ucphv8SIXLfH2IaBqg10g8+nrNrRYPA1Lfw9p4bDUhE+6M2cujhXy4Sx5NB77FcHUwuA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-color-parser': 1.2.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -4131,7 +4137,7 @@ packages: resolution: {integrity: sha512-GFNVsD97OuEcfHmcT0/DAZWAvTM/FFBDQndIOLawNc1Wq8YqpZwBdHa063Lq+Irk7azygTT+Iinyg3Lt76p7rg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -4141,7 +4147,7 @@ packages: resolution: {integrity: sha512-1+itpigiUemtdG2+pU3a36aQdpoFZbiKNZz0iW/s9H2mq0wCfqeRbXQmEQEStaqejEvlX+hLhbvWhb0WEuMKHQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-calc': 1.1.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -4153,7 +4159,7 @@ packages: resolution: {integrity: sha512-BAa1MIMJmEZlJ+UkPrkyoz3DC7kLlIl2oDya5yXgvUrelpwxddgz8iMp69qBStdXwuMyfPx46oZcSNx8Z0T2eA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/color-helpers': 3.0.0 postcss: 8.4.31 @@ -4164,7 +4170,7 @@ packages: resolution: {integrity: sha512-w00RYRPzvaCbpflgeDGBacZ8dJQwMi5driR+6JasOHh85MiF1e+muYZdjFYi6VWOIzM5XaqxwNiQlgQwdQvxgA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-calc': 1.1.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -4176,7 +4182,7 @@ packages: resolution: {integrity: sha512-P0JD1WHh3avVyKKRKjd0dZIjCEeaBer8t1BbwGMUDtSZaLhXlLNBqZ8KkqHzYWXOJgHleXAny2/sx8LYl6qhEA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 dev: true @@ -11733,7 +11739,7 @@ packages: engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.1.0 dependencies: browserslist: 4.21.9 caniuse-lite: 1.0.30001520 @@ -11968,6 +11974,10 @@ packages: tweetnacl: 0.14.5 dev: true + /bech32@2.0.0: + resolution: {integrity: sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==} + dev: false + /better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} @@ -13083,7 +13093,7 @@ packages: resolution: {integrity: sha512-VbfLlOWO7sBHBTn6pwDQzc07Z0SDydgDBfNfCE0nvrehdBNv9RKsuupIRa/qal0+fBZhAALyQDPMKz5lnvcchw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -13093,7 +13103,7 @@ packages: resolution: {integrity: sha512-X+r+JBuoO37FBOWVNhVJhxtSBUFHgHbrcc0CjFT28JEdOw1qaDwABv/uunyodUuSy2hMPe9j/HjssxSlvUmKjg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/selector-specificity': 3.0.0(postcss-selector-parser@6.0.13) postcss: 8.4.31 @@ -13122,7 +13132,7 @@ packages: resolution: {integrity: sha512-03QGAk/FXIRseDdLb7XAiu6gidQ0Nd8945xuM7VFVPpc6goJsG9uIO8xQjTxwbPdPIIV4o4AJoOJyt8gwDl67g==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 dev: true @@ -16711,7 +16721,7 @@ packages: resolution: {integrity: sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.1.0 dependencies: postcss: 8.4.31 dev: true @@ -20256,7 +20266,7 @@ packages: resolution: {integrity: sha512-IRuCwwAAQbgaLhxQdQcIIK0dCVXg3XDUnzgKD8iwdiYdwU4rMWRWyl/W9/0nA4ihVpq5pyALiHB2veBJ0292pw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -20266,7 +20276,7 @@ packages: resolution: {integrity: sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==} engines: {node: '>=7.6.0'} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4.6 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -20276,7 +20286,7 @@ packages: resolution: {integrity: sha512-kaWTgnhRKFtfMF8H0+NQBFxgr5CGg05WGe07Mc1ld6XHwwRWlqSbHOW0zwf+BtkBQpsdVUu7+gl9dtdvhWMedw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/postcss-progressive-custom-properties': 3.0.0(postcss@8.4.31) postcss: 8.4.31 @@ -20287,7 +20297,7 @@ packages: resolution: {integrity: sha512-SfPjgr//VQ/DOCf80STIAsdAs7sbIbxATvVmd+Ec7JvR8onz9pjawhq3BJM3Pie40EE3TyB0P6hft16D33Nlyg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -20297,7 +20307,7 @@ packages: resolution: {integrity: sha512-RmUFL+foS05AKglkEoqfx+KFdKRVmqUAxlHNz4jLqIi7046drIPyerdl4B6j/RA2BSP8FI8gJcHmLRrwJOMnHw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -20307,7 +20317,7 @@ packages: resolution: {integrity: sha512-NxDn7C6GJ7X8TsWOa8MbCdq9rLERRLcPfQSp856k1jzMreL8X9M6iWk35JjPRIb9IfRnVohmxAylDRx7n4Rv4g==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/cascade-layer-name-parser': 1.0.3(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -20320,7 +20330,7 @@ packages: resolution: {integrity: sha512-Z8UmzwVkRh8aITyeZoZnT4McSSPmS2EFl+OyPspfvx7v+N36V2UseMAODp3oBriZvcf/tQpzag9165x/VcC3kg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/cascade-layer-name-parser': 1.0.3(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -20333,7 +20343,7 @@ packages: resolution: {integrity: sha512-TU2xyUUBTlpiLnwyE2ZYMUIYB41MKMkBZ8X8ntkqRDQ8sdBLhFFsPgNcOliBd5+/zcK51C9hRnSE7hKUJMxQSw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/cascade-layer-name-parser': 1.0.3(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -20346,7 +20356,7 @@ packages: resolution: {integrity: sha512-Oy5BBi0dWPwij/IA+yDYj+/OBMQ9EPqAzTHeSNUYrUWdll/PRJmcbiUj0MNcsBi681I1gcSTLvMERPaXzdbvJg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -20356,7 +20366,7 @@ packages: resolution: {integrity: sha512-wR8npIkrIVUTicUpCWSSo1f/g7gAEIH70FMqCugY4m4j6TX4E0T2Q5rhfO0gqv00biBZdLyb+HkW8x6as+iJNQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/postcss-progressive-custom-properties': 3.0.0(postcss@8.4.31) postcss: 8.4.31 @@ -20367,7 +20377,7 @@ packages: resolution: {integrity: sha512-zA4TbVaIaT8npZBEROhZmlc+GBKE8AELPHXE7i4TmIUEQhw/P/mSJfY9t6tBzpQ1rABeGtEOHYrW4SboQeONMQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -20377,7 +20387,7 @@ packages: resolution: {integrity: sha512-E7+J9nuQzZaA37D/MUZMX1K817RZGDab8qw6pFwzAkDd/QtlWJ9/WTKmzewNiuxzeq6WWY7ATiRePVoDKp+DnA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -20386,7 +20396,7 @@ packages: /postcss-font-variant@5.0.0(postcss@8.4.31): resolution: {integrity: sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.1.0 dependencies: postcss: 8.4.31 dev: true @@ -20395,7 +20405,7 @@ packages: resolution: {integrity: sha512-YjsEEL6890P7MCv6fch6Am1yq0EhQCJMXyT4LBohiu87+4/WqR7y5W3RIv53WdA901hhytgRvjlrAhibhW4qsA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 dev: true @@ -20404,7 +20414,7 @@ packages: resolution: {integrity: sha512-bg58QnJexFpPBU4IGPAugAPKV0FuFtX5rHYNSKVaV91TpHN7iwyEzz1bkIPCiSU5+BUN00e+3fV5KFrwIgRocw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -20414,7 +20424,7 @@ packages: resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.0.0 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -20425,7 +20435,7 @@ packages: /postcss-initial@4.0.1(postcss@8.4.31): resolution: {integrity: sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.0.0 dependencies: postcss: 8.4.31 dev: true @@ -20434,7 +20444,7 @@ packages: resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4.21 dependencies: camelcase-css: 2.0.1 postcss: 8.4.31 @@ -20444,7 +20454,7 @@ packages: resolution: {integrity: sha512-bEKvKeoA0PPeqXdYfnIjU38NdkjrlqT4iENtIVMAcx9YAJz+9OrUvE2IRRK2jMZPcBM5RhyHj5zJqpzvR7KGtw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/css-color-parser': 1.2.2(@csstools/css-parser-algorithms@2.3.0)(@csstools/css-tokenizer@2.1.1) '@csstools/css-parser-algorithms': 2.3.0(@csstools/css-tokenizer@2.1.1) @@ -20457,7 +20467,7 @@ packages: resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} engines: {node: '>= 10'} peerDependencies: - postcss: '>=8.4.31' + postcss: '>=8.0.9' ts-node: '>=9.0.0' peerDependenciesMeta: postcss: @@ -20474,7 +20484,7 @@ packages: resolution: {integrity: sha512-vEJIc8RdiBRu3oRAI0ymerOn+7rPuMvRXslTvZUKZonDHFIczxztIyJ1urxM1x9JXEikvpWWTUUqal5j/8QgvA==} engines: {node: '>= 14'} peerDependencies: - postcss: '>=8.4.31' + postcss: '>=8.0.9' ts-node: '>=9.0.0' peerDependenciesMeta: postcss: @@ -20492,7 +20502,7 @@ packages: resolution: {integrity: sha512-YgO/yhtevGO/vJePCQmTxiaEwER94LABZN0ZMT4A0vsak9TpO+RvKRs7EmJ8peIlB9xfXCsS7M8LjqncsUZ5HA==} engines: {node: '>= 14.15.0'} peerDependencies: - postcss: '>=8.4.31' + postcss: ^7.0.0 || ^8.0.1 webpack: ^5.0.0 dependencies: cosmiconfig: 8.2.0 @@ -20506,7 +20516,7 @@ packages: resolution: {integrity: sha512-zYf3vHkoW82f5UZTEXChTJvH49Yl9X37axTZsJGxrCG2kOUwtaAoz9E7tqYg0lsIoJLybaL8fk/2mOi81zVIUw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -20516,7 +20526,7 @@ packages: resolution: {integrity: sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.1.0 dependencies: postcss: 8.4.31 dev: true @@ -20525,7 +20535,7 @@ packages: resolution: {integrity: sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.1.0 dependencies: icss-utils: 5.1.0(postcss@8.4.31) postcss: 8.4.31 @@ -20537,7 +20547,7 @@ packages: resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.1.0 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -20547,7 +20557,7 @@ packages: resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.1.0 dependencies: icss-utils: 5.1.0(postcss@8.4.31) postcss: 8.4.31 @@ -20557,7 +20567,7 @@ packages: resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.2.14 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -20567,7 +20577,7 @@ packages: resolution: {integrity: sha512-knqwW65kxssmyIFadRSimaiRyLVRd0MdwfabesKw6XvGLwSOCJ+4zfvNQQCOOYij5obwpZzDpODuGRv2PCyiUw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/selector-specificity': 3.0.0(postcss-selector-parser@6.0.13) postcss: 8.4.31 @@ -20578,7 +20588,7 @@ packages: resolution: {integrity: sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.2 dependencies: postcss: 8.4.31 dev: true @@ -20587,7 +20597,7 @@ packages: resolution: {integrity: sha512-2rlxDyeSics/hC2FuMdPnWiP9WUPZ5x7FTuArXLFVpaSQ2woPSfZS4RD59HuEokbZhs/wPUQJ1E3MT6zVv94MQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -20596,7 +20606,7 @@ packages: /postcss-page-break@3.0.4(postcss@8.4.31): resolution: {integrity: sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8 dependencies: postcss: 8.4.31 dev: true @@ -20605,7 +20615,7 @@ packages: resolution: {integrity: sha512-qLEPD9VPH5opDVemwmRaujODF9nExn24VOC3ghgVLEvfYN7VZLwJHes0q/C9YR5hI2UC3VgBE8Wkdp1TxCXhtg==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-value-parser: 4.2.0 @@ -20614,7 +20624,7 @@ packages: /postcss-prefix-selector@1.16.0(postcss@8.4.31): resolution: {integrity: sha512-rdVMIi7Q4B0XbXqNUEI+Z4E+pueiu/CS5E6vRCQommzdQ/sgsS4dK42U7GX8oJR+TJOtT+Qv3GkNo6iijUMp3Q==} peerDependencies: - postcss: '>=8.4.31' + postcss: '>4 <9' dependencies: postcss: 8.4.31 dev: true @@ -20623,7 +20633,7 @@ packages: resolution: {integrity: sha512-L0x/Nluq+/FkidIYjU7JtkmRL2/QmXuYkxuM3C5y9VG3iGLljF9PuBHQ7kzKRoVfwnca0VNN0Zb3a/bxVJ12vA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: '@csstools/postcss-cascade-layers': 4.0.0(postcss@8.4.31) '@csstools/postcss-color-function': 2.2.3(postcss@8.4.31) @@ -20688,7 +20698,7 @@ packages: resolution: {integrity: sha512-QNCYIL98VKFKY6HGDEJpF6+K/sg9bxcUYnOmNHJxZS5wsFDFaVoPeG68WAuhsqwbIBSo/b9fjEnTwY2mTSD+uA==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 @@ -20697,7 +20707,7 @@ packages: /postcss-replace-overflow-wrap@4.0.0(postcss@8.4.31): resolution: {integrity: sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.0.3 dependencies: postcss: 8.4.31 dev: true @@ -20706,7 +20716,7 @@ packages: resolution: {integrity: sha512-1zT5C27b/zeJhchN7fP0kBr16Cc61mu7Si9uWWLoA3Px/D9tIJPKchJCkUH3tPO5D0pCFmGeApAv8XpXBQJ8SQ==} engines: {node: ^14 || ^16 || >=18} peerDependencies: - postcss: '>=8.4.31' + postcss: ^8.4 dependencies: postcss: 8.4.31 postcss-selector-parser: 6.0.13 From 6bb1bceb00fba96a3c002f35c14d114be6a3ae72 Mon Sep 17 00:00:00 2001 From: Pavlos Chrysochoidis Date: Wed, 17 Jan 2024 13:59:17 +0200 Subject: [PATCH 06/11] support base64 exported keypairs * this allows supporting keys that are stored in base64 format already in the wallet storage --- .../src/background/accounts/ImportedAccount.ts | 2 +- .../src/background/legacy-accounts/LegacyVault.ts | 2 +- .../src/shared/utils/from-exported-keypair.ts | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/wallet/src/background/accounts/ImportedAccount.ts b/apps/wallet/src/background/accounts/ImportedAccount.ts index b05eee4656abc..d576181aad8bb 100644 --- a/apps/wallet/src/background/accounts/ImportedAccount.ts +++ b/apps/wallet/src/background/accounts/ImportedAccount.ts @@ -127,7 +127,7 @@ export class ImportedAccount async #getKeyPair() { const ephemeralData = await this.getEphemeralValue(); if (ephemeralData) { - return fromExportedKeypair(ephemeralData.keyPair); + return fromExportedKeypair(ephemeralData.keyPair, true); } return null; } diff --git a/apps/wallet/src/background/legacy-accounts/LegacyVault.ts b/apps/wallet/src/background/legacy-accounts/LegacyVault.ts index 36db5b7fff49a..189e314a867ee 100644 --- a/apps/wallet/src/background/legacy-accounts/LegacyVault.ts +++ b/apps/wallet/src/background/legacy-accounts/LegacyVault.ts @@ -53,7 +53,7 @@ export class LegacyVault { mnemonicSeedHex: storedMnemonicSeedHex, } = await decrypt(password, data.data); entropy = toEntropy(entropySerialized); - keypairs = importedKeypairs.map(fromExportedKeypair); + keypairs = importedKeypairs.map((aKeyPair) => fromExportedKeypair(aKeyPair, true)); if (storedTokens) { qredoTokens = new Map(Object.entries(storedTokens)); } diff --git a/apps/wallet/src/shared/utils/from-exported-keypair.ts b/apps/wallet/src/shared/utils/from-exported-keypair.ts index 1b7926ee88a3e..375c242722066 100644 --- a/apps/wallet/src/shared/utils/from-exported-keypair.ts +++ b/apps/wallet/src/shared/utils/from-exported-keypair.ts @@ -10,14 +10,24 @@ import { import { Ed25519Keypair } from '@mysten/sui.js/keypairs/ed25519'; import { Secp256k1Keypair } from '@mysten/sui.js/keypairs/secp256k1'; import { Secp256r1Keypair } from '@mysten/sui.js/keypairs/secp256r1'; +import { fromB64 } from '@mysten/sui.js/utils'; export function validateExportedKeypair(keypair: ExportedKeypair): ExportedKeypair { const _kp = decodeSuiPrivateKey(keypair.privateKey); return keypair; } -export function fromExportedKeypair(keypair: ExportedKeypair): Keypair { - const { schema, secretKey } = decodeSuiPrivateKey(keypair.privateKey); +export function fromExportedKeypair(keypair: ExportedKeypair, legacySupport = false): Keypair { + const { privateKey } = keypair; + let schema = keypair.schema; + let secretKey = null; + if (!legacySupport || privateKey.startsWith('suiprivkey')) { + const decoded = decodeSuiPrivateKey(keypair.privateKey); + schema = decoded?.schema; + secretKey = decoded.secretKey; + } else { + secretKey = fromB64(privateKey); + } switch (schema) { case 'ED25519': From ac94e1137f52a40a0d89dc7d4e6b3b9636579c62 Mon Sep 17 00:00:00 2001 From: Pavlos Chrysochoidis Date: Mon, 22 Jan 2024 14:43:35 +0000 Subject: [PATCH 07/11] deprecate ExportedKeyPair and Keypair.export --- .../wallet/src/background/accounts/Account.ts | 3 +- .../background/accounts/ImportedAccount.ts | 10 +++--- .../background/accounts/MnemonicAccount.ts | 10 +++--- .../accounts/zklogin/ZkLoginAccount.ts | 5 ++- .../legacy-accounts/storage-migration.ts | 2 +- .../messages/payloads/MethodPayload.ts | 6 ++-- .../src/shared/utils/from-exported-keypair.ts | 32 +++++++++---------- .../accounts/AccountsFormContext.tsx | 3 +- .../app/pages/accounts/ExportAccountPage.tsx | 15 +++++---- .../pages/accounts/ImportPrivateKeyPage.tsx | 7 +--- sdk/typescript/src/cryptography/keypair.ts | 2 ++ 11 files changed, 45 insertions(+), 50 deletions(-) diff --git a/apps/wallet/src/background/accounts/Account.ts b/apps/wallet/src/background/accounts/Account.ts index a3e91cf9174a9..39e6b0615c81d 100644 --- a/apps/wallet/src/background/accounts/Account.ts +++ b/apps/wallet/src/background/accounts/Account.ts @@ -4,7 +4,6 @@ import { type Serializable } from '_src/shared/cryptography/keystore'; import { toSerializedSignature, - type ExportedKeypair, type Keypair, type SerializedSignature, } from '@mysten/sui.js/cryptography'; @@ -186,7 +185,7 @@ export function isSigningAccount(account: any): account is SigningAccount { export interface KeyPairExportableAccount { readonly exportableKeyPair: true; - exportKeyPair(password: string): Promise; + exportKeyPair(password: string): Promise; } export function isKeyPairExportableAccount(account: any): account is KeyPairExportableAccount { diff --git a/apps/wallet/src/background/accounts/ImportedAccount.ts b/apps/wallet/src/background/accounts/ImportedAccount.ts index d576181aad8bb..21eb46464c1e8 100644 --- a/apps/wallet/src/background/accounts/ImportedAccount.ts +++ b/apps/wallet/src/background/accounts/ImportedAccount.ts @@ -14,8 +14,8 @@ import { type SigningAccount, } from './Account'; -type SessionStorageData = { keyPair: ExportedKeypair }; -type EncryptedData = { keyPair: ExportedKeypair }; +type SessionStorageData = { keyPair: ExportedKeypair | string }; +type EncryptedData = { keyPair: ExportedKeypair | string }; export interface ImportedAccountSerialized extends SerializedAccount { type: 'imported'; @@ -43,7 +43,7 @@ export class ImportedAccount readonly exportableKeyPair = true; static async createNew(inputs: { - keyPair: ExportedKeypair; + keyPair: string; password: string; }): Promise> { const keyPair = fromExportedKeypair(inputs.keyPair); @@ -118,10 +118,10 @@ export class ImportedAccount return this.generateSignature(data, keyPair); } - async exportKeyPair(password: string): Promise { + async exportKeyPair(password: string): Promise { const { encrypted } = await this.getStoredData(); const { keyPair } = await decrypt(password, encrypted); - return keyPair; + return fromExportedKeypair(keyPair, true).getSecretKey(); } async #getKeyPair() { diff --git a/apps/wallet/src/background/accounts/MnemonicAccount.ts b/apps/wallet/src/background/accounts/MnemonicAccount.ts index 5daf20f161610..a641a8980f803 100644 --- a/apps/wallet/src/background/accounts/MnemonicAccount.ts +++ b/apps/wallet/src/background/accounts/MnemonicAccount.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { fromExportedKeypair } from '_src/shared/utils/from-exported-keypair'; -import { type ExportedKeypair, type Keypair } from '@mysten/sui.js/cryptography'; +import { type Keypair } from '@mysten/sui.js/cryptography'; import { MnemonicAccountSource } from '../account-sources/MnemonicAccountSource'; import { @@ -34,7 +34,7 @@ export function isMnemonicSerializedUiAccount( return account.type === 'mnemonic-derived'; } -type SessionStorageData = { keyPair: ExportedKeypair }; +type SessionStorageData = { keyPair: string }; export class MnemonicAccount extends Account @@ -93,7 +93,7 @@ export class MnemonicAccount await mnemonicSource.unlock(password); } await this.setEphemeralValue({ - keyPair: (await mnemonicSource.deriveKeyPair(derivationPath)).export(), + keyPair: (await mnemonicSource.deriveKeyPair(derivationPath)).getSecretKey(), }); await this.onUnlocked(); } @@ -138,11 +138,11 @@ export class MnemonicAccount return this.getCachedData().then(({ sourceID }) => sourceID); } - async exportKeyPair(password: string): Promise { + async exportKeyPair(password: string): Promise { const { derivationPath } = await this.getStoredData(); const mnemonicSource = await this.#getMnemonicSource(); await mnemonicSource.unlock(password); - return (await mnemonicSource.deriveKeyPair(derivationPath)).export(); + return (await mnemonicSource.deriveKeyPair(derivationPath)).getSecretKey(); } async #getKeyPair() { diff --git a/apps/wallet/src/background/accounts/zklogin/ZkLoginAccount.ts b/apps/wallet/src/background/accounts/zklogin/ZkLoginAccount.ts index b03904a560f84..e2d5a70f523cc 100644 --- a/apps/wallet/src/background/accounts/zklogin/ZkLoginAccount.ts +++ b/apps/wallet/src/background/accounts/zklogin/ZkLoginAccount.ts @@ -7,7 +7,6 @@ import { deobfuscate, obfuscate } from '_src/shared/cryptography/keystore'; import { fromExportedKeypair } from '_src/shared/utils/from-exported-keypair'; import { toSerializedSignature, - type ExportedKeypair, type PublicKey, type SerializedSignature, } from '@mysten/sui.js/cryptography'; @@ -38,7 +37,7 @@ function serializeNetwork(network: NetworkEnvType): SerializedNetwork { } type CredentialData = { - ephemeralKeyPair: ExportedKeypair; + ephemeralKeyPair: string; proofs?: PartialZkLoginSignature; minEpoch: number; maxEpoch: number; @@ -278,7 +277,7 @@ export class ZkLoginAccount const ephemeralValue = (await this.getEphemeralValue()) || {}; const activeNetwork = await networkEnv.getActiveNetwork(); const credentialsData: CredentialData = { - ephemeralKeyPair: ephemeralKeyPair.export(), + ephemeralKeyPair: ephemeralKeyPair.getSecretKey(), minEpoch: Number(epoch), maxEpoch, network: activeNetwork, diff --git a/apps/wallet/src/background/legacy-accounts/storage-migration.ts b/apps/wallet/src/background/legacy-accounts/storage-migration.ts index 779485b37a74b..7c9905d2f4161 100644 --- a/apps/wallet/src/background/legacy-accounts/storage-migration.ts +++ b/apps/wallet/src/background/legacy-accounts/storage-migration.ts @@ -71,7 +71,7 @@ async function makeMnemonicAccounts(password: string, vault: LegacyVault) { async function makeImportedAccounts(password: string, vault: LegacyVault) { return Promise.all( vault.importedKeypairs.map((keyPair) => - ImportedAccount.createNew({ password, keyPair: keyPair.export() }), + ImportedAccount.createNew({ password, keyPair: keyPair.getSecretKey() }), ), ); } diff --git a/apps/wallet/src/shared/messaging/messages/payloads/MethodPayload.ts b/apps/wallet/src/shared/messaging/messages/payloads/MethodPayload.ts index b989f7ba4e2af..63204b3fb5361 100644 --- a/apps/wallet/src/shared/messaging/messages/payloads/MethodPayload.ts +++ b/apps/wallet/src/shared/messaging/messages/payloads/MethodPayload.ts @@ -5,7 +5,7 @@ import { type AccountSourceSerializedUI } from '_src/background/account-sources/ import { type SerializedUIAccount } from '_src/background/accounts/Account'; import { type ZkLoginProvider } from '_src/background/accounts/zklogin/providers'; import { type Status } from '_src/background/legacy-accounts/storage-migration'; -import { type ExportedKeypair, type SerializedSignature } from '@mysten/sui.js/cryptography'; +import { type SerializedSignature } from '@mysten/sui.js/cryptography'; import { isBasePayload } from './BasePayload'; import type { Payload } from './Payload'; @@ -32,7 +32,7 @@ type MethodPayloads = { unlockAccountSourceOrAccount: { id: string; password?: string }; createAccounts: | { type: 'mnemonic-derived'; sourceID: string } - | { type: 'imported'; keyPair: ExportedKeypair; password: string } + | { type: 'imported'; keyPair: string; password: string } | { type: 'ledger'; accounts: { publicKey: string; derivationPath: string; address: string }[]; @@ -61,7 +61,7 @@ type MethodPayloads = { setAutoLockMinutes: { minutes: number | null }; notifyUserActive: {}; getAccountKeyPair: { accountID: string; password: string }; - getAccountKeyPairResponse: { accountID: string; keyPair: ExportedKeypair }; + getAccountKeyPairResponse: { accountID: string; keyPair: string }; resetPassword: { password: string; recoveryData: PasswordRecoveryData[]; diff --git a/apps/wallet/src/shared/utils/from-exported-keypair.ts b/apps/wallet/src/shared/utils/from-exported-keypair.ts index 375c242722066..92b67ded6bbf3 100644 --- a/apps/wallet/src/shared/utils/from-exported-keypair.ts +++ b/apps/wallet/src/shared/utils/from-exported-keypair.ts @@ -12,23 +12,23 @@ import { Secp256k1Keypair } from '@mysten/sui.js/keypairs/secp256k1'; import { Secp256r1Keypair } from '@mysten/sui.js/keypairs/secp256r1'; import { fromB64 } from '@mysten/sui.js/utils'; -export function validateExportedKeypair(keypair: ExportedKeypair): ExportedKeypair { - const _kp = decodeSuiPrivateKey(keypair.privateKey); - return keypair; -} - -export function fromExportedKeypair(keypair: ExportedKeypair, legacySupport = false): Keypair { - const { privateKey } = keypair; - let schema = keypair.schema; - let secretKey = null; - if (!legacySupport || privateKey.startsWith('suiprivkey')) { - const decoded = decodeSuiPrivateKey(keypair.privateKey); - schema = decoded?.schema; - secretKey = decoded.secretKey; +export function fromExportedKeypair( + secret: ExportedKeypair | string, + legacySupport = false, +): Keypair { + let schema; + let secretKey; + if (typeof secret === 'object') { + if (!legacySupport) { + throw new Error('Invalid type of secret key. A string value was expected.'); + } + secretKey = fromB64(secret.privateKey); + schema = secret.schema; } else { - secretKey = fromB64(privateKey); + const decoded = decodeSuiPrivateKey(secret); + schema = decoded.schema; + secretKey = decoded.secretKey; } - switch (schema) { case 'ED25519': let pureSecretKey = secretKey; @@ -42,6 +42,6 @@ export function fromExportedKeypair(keypair: ExportedKeypair, legacySupport = fa case 'Secp256r1': return Secp256r1Keypair.fromSecretKey(secretKey); default: - throw new Error(`Invalid keypair schema ${keypair.schema}`); + throw new Error(`Invalid keypair schema ${schema}`); } } diff --git a/apps/wallet/src/ui/app/components/accounts/AccountsFormContext.tsx b/apps/wallet/src/ui/app/components/accounts/AccountsFormContext.tsx index ebe3efe7150b6..c4883135397bb 100644 --- a/apps/wallet/src/ui/app/components/accounts/AccountsFormContext.tsx +++ b/apps/wallet/src/ui/app/components/accounts/AccountsFormContext.tsx @@ -3,7 +3,6 @@ import { type ZkLoginProvider } from '_src/background/accounts/zklogin/providers'; import { type Wallet } from '_src/shared/qredo-api'; -import { type ExportedKeypair } from '@mysten/sui.js/cryptography'; import { createContext, useCallback, @@ -19,7 +18,7 @@ export type AccountsFormValues = | { type: 'new-mnemonic' } | { type: 'import-mnemonic'; entropy: string } | { type: 'mnemonic-derived'; sourceID: string } - | { type: 'imported'; keyPair: ExportedKeypair } + | { type: 'imported'; keyPair: string } | { type: 'ledger'; accounts: { publicKey: string; derivationPath: string; address: string }[]; diff --git a/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx b/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx index 388a2ae8eeff9..3bc59e118eecc 100644 --- a/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx +++ b/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx @@ -23,13 +23,14 @@ export function ExportAccountPage() { if (!account) { return null; } - const { - keyPair: { privateKey }, - } = await backgroundClient.exportAccountKeyPair({ - password, - accountID: account.id, - }); - return privateKey; + const key = ( + await backgroundClient.exportAccountKeyPair({ + password, + accountID: account.id, + }) + ).keyPair; + console.log(key); + return key; }, }); const navigate = useNavigate(); diff --git a/apps/wallet/src/ui/app/pages/accounts/ImportPrivateKeyPage.tsx b/apps/wallet/src/ui/app/pages/accounts/ImportPrivateKeyPage.tsx index ea896bcbf7c18..e8c46a7ac59b1 100644 --- a/apps/wallet/src/ui/app/pages/accounts/ImportPrivateKeyPage.tsx +++ b/apps/wallet/src/ui/app/pages/accounts/ImportPrivateKeyPage.tsx @@ -2,8 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { Text } from '_app/shared/text'; -import { validateExportedKeypair } from '_src/shared/utils/from-exported-keypair'; -import type { ExportedKeypair } from '@mysten/sui.js/cryptography'; import { useNavigate } from 'react-router-dom'; import { useAccountsFormContext } from '../../components/accounts/AccountsFormContext'; @@ -29,10 +27,7 @@ export function ImportPrivateKeyPage() { onSubmit={({ privateKey }) => { setAccountsFormValues({ type: 'imported', - keyPair: validateExportedKeypair({ - schema: 'ED25519', - privateKey: privateKey, - } as ExportedKeypair), + keyPair: privateKey, }); navigate('/accounts/protect-account?accountType=imported'); }} diff --git a/sdk/typescript/src/cryptography/keypair.ts b/sdk/typescript/src/cryptography/keypair.ts index 2d812957b1eed..daf6d99d7d8d6 100644 --- a/sdk/typescript/src/cryptography/keypair.ts +++ b/sdk/typescript/src/cryptography/keypair.ts @@ -21,6 +21,7 @@ export type ParsedKeypair = { secretKey: Uint8Array; }; +/** @deprecated use string instead. See {@link Keypair.getSecretKey} */ export type ExportedKeypair = { schema: SignatureScheme; privateKey: string; @@ -99,6 +100,7 @@ export abstract class Keypair extends BaseSigner { abstract getSecretKey(): string; /** + * @deprecated use {@link Keypair.getSecretKey} instead * This returns an exported keypair object, schema is the signature * scheme name, and the private key field is a Bech32 encoded string * of 33-byte `flag || private_key` that starts with `suiprivkey`. From 35149455fa2973077ca9acb3b4d25fac08bec59f Mon Sep 17 00:00:00 2001 From: Pavlos Chrysochoidis Date: Mon, 22 Jan 2024 15:58:14 +0000 Subject: [PATCH 08/11] lint --- apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx b/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx index 3bc59e118eecc..0cbc754f8219b 100644 --- a/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx +++ b/apps/wallet/src/ui/app/pages/accounts/ExportAccountPage.tsx @@ -23,14 +23,12 @@ export function ExportAccountPage() { if (!account) { return null; } - const key = ( + return ( await backgroundClient.exportAccountKeyPair({ password, accountID: account.id, }) ).keyPair; - console.log(key); - return key; }, }); const navigate = useNavigate(); From b777e211b39593ded44f4e876cb8de68e0252f85 Mon Sep 17 00:00:00 2001 From: Pavlos Chrysochoidis Date: Mon, 22 Jan 2024 16:09:13 +0000 Subject: [PATCH 09/11] changeset --- .changeset/angry-rocks-brush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/angry-rocks-brush.md diff --git a/.changeset/angry-rocks-brush.md b/.changeset/angry-rocks-brush.md new file mode 100644 index 0000000000000..9091d7351ba42 --- /dev/null +++ b/.changeset/angry-rocks-brush.md @@ -0,0 +1,5 @@ +--- +'@mysten/sui.js': patch +--- + +deprecate ExportedKeypair From 56fa875822d08ae92bfb66c922b49960ecd55f01 Mon Sep 17 00:00:00 2001 From: Pavlos Chrysochoidis Date: Mon, 22 Jan 2024 16:18:29 +0000 Subject: [PATCH 10/11] stop using ExportedKeyPair in wallet --- .../src/background/accounts/ImportedAccount.ts | 10 ++++++---- .../src/background/legacy-accounts/LegacyVault.ts | 9 ++++++--- .../src/shared/utils/from-exported-keypair.ts | 13 +++++++++++-- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/apps/wallet/src/background/accounts/ImportedAccount.ts b/apps/wallet/src/background/accounts/ImportedAccount.ts index 21eb46464c1e8..5b98fe0808d43 100644 --- a/apps/wallet/src/background/accounts/ImportedAccount.ts +++ b/apps/wallet/src/background/accounts/ImportedAccount.ts @@ -2,8 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 import { decrypt, encrypt } from '_src/shared/cryptography/keystore'; -import { fromExportedKeypair } from '_src/shared/utils/from-exported-keypair'; -import { type ExportedKeypair } from '@mysten/sui.js/cryptography'; +import { + fromExportedKeypair, + type LegacyExportedKeyPair, +} from '_src/shared/utils/from-exported-keypair'; import { Account, @@ -14,8 +16,8 @@ import { type SigningAccount, } from './Account'; -type SessionStorageData = { keyPair: ExportedKeypair | string }; -type EncryptedData = { keyPair: ExportedKeypair | string }; +type SessionStorageData = { keyPair: LegacyExportedKeyPair | string }; +type EncryptedData = { keyPair: LegacyExportedKeyPair | string }; export interface ImportedAccountSerialized extends SerializedAccount { type: 'imported'; diff --git a/apps/wallet/src/background/legacy-accounts/LegacyVault.ts b/apps/wallet/src/background/legacy-accounts/LegacyVault.ts index 189e314a867ee..ef187d72c1dfd 100644 --- a/apps/wallet/src/background/legacy-accounts/LegacyVault.ts +++ b/apps/wallet/src/background/legacy-accounts/LegacyVault.ts @@ -8,8 +8,11 @@ import { toEntropy, validateEntropy, } from '_shared/utils/bip39'; -import { fromExportedKeypair } from '_shared/utils/from-exported-keypair'; -import { mnemonicToSeedHex, type ExportedKeypair, type Keypair } from '@mysten/sui.js/cryptography'; +import { + fromExportedKeypair, + type LegacyExportedKeyPair, +} from '_shared/utils/from-exported-keypair'; +import { mnemonicToSeedHex, type Keypair } from '@mysten/sui.js/cryptography'; import { getFromLocalStorage } from '../storage-utils'; @@ -17,7 +20,7 @@ type StoredData = string | { v: 1 | 2; data: string }; type V2DecryptedDataType = { entropy: string; - importedKeypairs: ExportedKeypair[]; + importedKeypairs: LegacyExportedKeyPair[]; qredoTokens?: Record; mnemonicSeedHex?: string; }; diff --git a/apps/wallet/src/shared/utils/from-exported-keypair.ts b/apps/wallet/src/shared/utils/from-exported-keypair.ts index 92b67ded6bbf3..a3e7cd667de19 100644 --- a/apps/wallet/src/shared/utils/from-exported-keypair.ts +++ b/apps/wallet/src/shared/utils/from-exported-keypair.ts @@ -1,7 +1,7 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 -import { type ExportedKeypair, type Keypair } from '@mysten/sui.js/cryptography'; +import { type Keypair, type SignatureScheme } from '@mysten/sui.js/cryptography'; import { decodeSuiPrivateKey, LEGACY_PRIVATE_KEY_SIZE, @@ -12,8 +12,17 @@ import { Secp256k1Keypair } from '@mysten/sui.js/keypairs/secp256k1'; import { Secp256r1Keypair } from '@mysten/sui.js/keypairs/secp256r1'; import { fromB64 } from '@mysten/sui.js/utils'; +/** + * Wallet stored data might contain imported accounts with their keys stored in the previous format. + * Using this type to type-check it. + */ +export type LegacyExportedKeyPair = { + schema: SignatureScheme; + privateKey: string; +}; + export function fromExportedKeypair( - secret: ExportedKeypair | string, + secret: LegacyExportedKeyPair | string, legacySupport = false, ): Keypair { let schema; From 705ab6356d1d48e1a578cc45cacaf1e495451730 Mon Sep 17 00:00:00 2001 From: Pavlos Chrysochoidis Date: Fri, 26 Jan 2024 17:32:50 +0000 Subject: [PATCH 11/11] support importing hex private keys --- .../accounts/ImportPrivateKeyForm.tsx | 13 +++- .../validation/privateKeyValidation.ts | 64 ++++++++----------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/apps/wallet/src/ui/app/components/accounts/ImportPrivateKeyForm.tsx b/apps/wallet/src/ui/app/components/accounts/ImportPrivateKeyForm.tsx index cae7bc5f786df..b7e72a3031581 100644 --- a/apps/wallet/src/ui/app/components/accounts/ImportPrivateKeyForm.tsx +++ b/apps/wallet/src/ui/app/components/accounts/ImportPrivateKeyForm.tsx @@ -10,6 +10,7 @@ import { z } from 'zod'; import { privateKeyValidation } from '../../helpers/validation/privateKeyValidation'; import { Form } from '../../shared/forms/Form'; import { TextAreaField } from '../../shared/forms/TextAreaField'; +import Alert from '../alert'; const formSchema = z.object({ privateKey: privateKeyValidation, @@ -29,12 +30,20 @@ export function ImportPrivateKeyForm({ onSubmit }: ImportPrivateKeyFormProps) { const { register, formState: { isSubmitting, isValid }, + watch, } = form; const navigate = useNavigate(); - + const privateKey = watch('privateKey'); + const isHexadecimal = isValid && !privateKey.startsWith('suiprivkey'); return ( -
+ + {isHexadecimal ? ( + + Importing Hex encoded Private Key will soon be deprecated, please use Bech32 encoded + private key that starts with "suiprivkey" instead + + ) : null}