mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-11 17:41:08 +00:00
Support loading keys from Polkadot-JS accounts. (#1661)
* Import key pair from JSON. * Get secret. * Fix JSON decryption. * Fix error handling. * Fix warnings. * Add polkadot-js links. * Fix packages. * Fix json feature. * Add copyright message. * Update Cargo.toml Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com> * Improve error handling. * Expect that provided parameters are valid. * Add Scrypt parameters comment from JS implementation. * Fix expect() message * Make from_ed25519_bytes() pub(crate) * Rename json feature to polkadot-js-compat * Add comment about polkadot-js-compat dependencies. Co-authored-by: James Wilson <james@jsdw.me> * Add decrypt_json() public method. * json.rs -> polkadot_js_compat.rs * Simplify JSON structs. * Only declare from_ed25519_bytes() with polkadot-js-compat * Move decrypt_json() to top of file. * Don't enable new crates on std feature * Avoid enabling a couple of existing optional crates on std feature --------- Co-authored-by: Jonathan Brown <jbrown@acuity.network> Co-authored-by: Niklas Adolfsson <niklasadolfsson1@gmail.com> Co-authored-by: James Wilson <james@jsdw.me>
This commit is contained in:
Generated
+49
-6
@@ -541,9 +541,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.0"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
@@ -853,6 +853,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
|
||||
dependencies = [
|
||||
"crypto-common",
|
||||
"inout",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1174,6 +1175,21 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto_secretbox"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"cipher",
|
||||
"generic-array",
|
||||
"poly1305",
|
||||
"salsa20",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "3.2.0"
|
||||
@@ -2470,7 +2486,7 @@ version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08163edd8bcc466c33d79e10f695cdc98c00d1e6ddfb95cec41b6b0279dd5432"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"base64 0.22.1",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"gloo-net",
|
||||
@@ -2523,7 +2539,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2d90064e04fb9d7282b1c71044ea94d0bbc6eff5621c66f1a0bce9e9de7cf3ac"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.0",
|
||||
"base64 0.22.1",
|
||||
"http-body",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
@@ -3060,6 +3076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
|
||||
dependencies = [
|
||||
"digest 0.10.7",
|
||||
"hmac 0.12.1",
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
@@ -3689,7 +3706,7 @@ version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"base64 0.22.1",
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
@@ -3772,6 +3789,15 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
|
||||
[[package]]
|
||||
name = "salsa20"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
@@ -3981,6 +4007,18 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "scrypt"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
|
||||
dependencies = [
|
||||
"password-hash",
|
||||
"pbkdf2",
|
||||
"salsa20",
|
||||
"sha2 0.10.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
@@ -4377,7 +4415,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37468c595637c10857701c990f93a40ce0e357cedb0953d1c26c8d8027f9bb53"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"futures",
|
||||
"httparse",
|
||||
@@ -5019,9 +5057,11 @@ dependencies = [
|
||||
name = "subxt-signer"
|
||||
version = "0.37.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bip32",
|
||||
"bip39",
|
||||
"cfg-if",
|
||||
"crypto_secretbox",
|
||||
"getrandom",
|
||||
"hex",
|
||||
"hex-literal",
|
||||
@@ -5032,8 +5072,11 @@ dependencies = [
|
||||
"proptest",
|
||||
"regex",
|
||||
"schnorrkel",
|
||||
"scrypt",
|
||||
"secp256k1",
|
||||
"secrecy",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.8",
|
||||
"sp-core",
|
||||
"sp-crypto-hashing",
|
||||
|
||||
@@ -162,6 +162,9 @@ keccak-hash = { version = "0.10.0", default-features = false }
|
||||
secrecy = "0.8.0"
|
||||
sha2 = { version = "0.10.8", default-features = false }
|
||||
zeroize = { version = "1", default-features = false }
|
||||
base64 = { version = "0.22.1", default-features = false }
|
||||
scrypt = { version = "0.11.0", default-features = false }
|
||||
crypto_secretbox = { version = "0.1.1", default-features = false }
|
||||
|
||||
[profile.dev.package.smoldot-light]
|
||||
opt-level = 2
|
||||
|
||||
+18
-3
@@ -23,8 +23,13 @@ std = [
|
||||
"sha2/std",
|
||||
"hmac/std",
|
||||
"bip39/std",
|
||||
"schnorrkel/std",
|
||||
"secp256k1/std",
|
||||
"schnorrkel?/std",
|
||||
"secp256k1?/std",
|
||||
"serde?/std",
|
||||
"serde_json?/std",
|
||||
"base64?/std",
|
||||
"scrypt?/std",
|
||||
"crypto_secretbox?/std",
|
||||
]
|
||||
|
||||
# Pick the signer implementation(s) you need by enabling the
|
||||
@@ -35,6 +40,9 @@ sr25519 = ["schnorrkel"]
|
||||
ecdsa = ["secp256k1"]
|
||||
unstable-eth = ["keccak-hash", "ecdsa", "secp256k1", "bip32"]
|
||||
|
||||
# Enable support for loading key pairs from polkadot-js json.
|
||||
polkadot-js-compat = ["std", "subxt", "sr25519", "base64", "scrypt", "crypto_secretbox", "serde", "serde_json"]
|
||||
|
||||
# Make the keypair algorithms here compatible with Subxt's Signer trait,
|
||||
# so that they can be used to sign transactions for compatible chains.
|
||||
subxt = ["dep:subxt-core"]
|
||||
@@ -66,6 +74,13 @@ secp256k1 = { workspace = true, optional = true, features = [
|
||||
] }
|
||||
keccak-hash = { workspace = true, optional = true }
|
||||
|
||||
# These are used if the polkadot-js-compat feature is enabled
|
||||
serde = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
base64 = { workspace = true, optional = true, features = ["alloc"] }
|
||||
scrypt = { workspace = true, default-features = false, optional = true }
|
||||
crypto_secretbox = { workspace = true, optional = true, features = ["alloc", "salsa20"] }
|
||||
|
||||
# We only pull this in to enable the JS flag for schnorrkel to use.
|
||||
getrandom = { workspace = true, optional = true }
|
||||
|
||||
@@ -86,4 +101,4 @@ rustdoc-args = ["--cfg", "docsrs"]
|
||||
defalt-features = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
workspace = true
|
||||
|
||||
@@ -37,6 +37,11 @@ pub mod ecdsa;
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "unstable-eth")))]
|
||||
pub mod eth;
|
||||
|
||||
/// A polkadot-js account json loader.
|
||||
#[cfg(feature = "polkadot-js-compat")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "polkadot-js-compat")))]
|
||||
pub mod polkadot_js_compat;
|
||||
|
||||
// Re-export useful bits and pieces for generating a Pair from a phrase,
|
||||
// namely the Mnemonic struct.
|
||||
pub use bip39;
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright 2019-2024 Parity Technologies (UK) Ltd.
|
||||
// This file is dual-licensed as Apache-2.0 or GPL-3.0.
|
||||
// see LICENSE for license details.
|
||||
|
||||
//! A Polkadot-JS account loader.
|
||||
|
||||
use base64::Engine;
|
||||
use core::fmt::Display;
|
||||
use crypto_secretbox::{
|
||||
aead::{Aead, KeyInit},
|
||||
Key, Nonce, XSalsa20Poly1305,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use subxt_core::utils::AccountId32;
|
||||
|
||||
use crate::sr25519;
|
||||
|
||||
/// Given a JSON keypair as exported from Polkadot-JS, this returns an [`sr25519::Keypair`]
|
||||
pub fn decrypt_json(json: &str, password: &str) -> Result<sr25519::Keypair, Error> {
|
||||
let pair_json: KeyringPairJson = serde_json::from_str(json)?;
|
||||
Ok(pair_json.decrypt(password)?)
|
||||
}
|
||||
|
||||
/// Error
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Error decoding JSON.
|
||||
Json(serde_json::Error),
|
||||
/// The keypair has an unsupported encoding.
|
||||
UnsupportedEncoding,
|
||||
/// Base64 decoding error.
|
||||
Base64(base64::DecodeError),
|
||||
/// Wrong Scrypt parameters
|
||||
UnsupportedScryptParameters {
|
||||
/// N
|
||||
n: u32,
|
||||
/// p
|
||||
p: u32,
|
||||
/// r
|
||||
r: u32,
|
||||
},
|
||||
/// Decryption error.
|
||||
Secretbox(crypto_secretbox::Error),
|
||||
/// sr25519 keypair error.
|
||||
Sr25519(sr25519::Error),
|
||||
/// The decrypted keys are not valid.
|
||||
InvalidKeys,
|
||||
}
|
||||
|
||||
impl_from!(serde_json::Error => Error::Json);
|
||||
impl_from!(base64::DecodeError => Error::Base64);
|
||||
impl_from!(crypto_secretbox::Error => Error::Secretbox);
|
||||
impl_from!(sr25519::Error => Error::Sr25519);
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Error::Json(e) => write!(f, "Invalid JSON: {e}"),
|
||||
Error::UnsupportedEncoding => write!(f, "Unsupported encoding."),
|
||||
Error::Base64(e) => write!(f, "Base64 decoding error: {e}"),
|
||||
Error::UnsupportedScryptParameters { n, p, r } => {
|
||||
write!(f, "Unsupported Scrypt parameters: N: {n}, p: {p}, r: {r}")
|
||||
}
|
||||
Error::Secretbox(e) => write!(f, "Decryption error: {e}"),
|
||||
Error::Sr25519(e) => write!(f, "{e}"),
|
||||
Error::InvalidKeys => write!(f, "The decrypted keys are not valid."),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct EncryptionMetadata {
|
||||
/// Descriptor for the content
|
||||
content: Vec<String>,
|
||||
/// The encoding (in current/latest versions this is always an array)
|
||||
r#type: Vec<String>,
|
||||
/// The version of encoding applied
|
||||
version: String,
|
||||
}
|
||||
|
||||
/// https://github.com/polkadot-js/common/blob/37fa211fdb141d4f6eb32e8f377a4651ed2d9068/packages/keyring/src/types.ts#L67
|
||||
#[derive(Deserialize)]
|
||||
struct KeyringPairJson {
|
||||
/// The encoded string
|
||||
encoded: String,
|
||||
/// The encoding used
|
||||
encoding: EncryptionMetadata,
|
||||
/// The ss58 encoded address or the hex-encoded version (the latter is for ETH-compat chains)
|
||||
address: AccountId32,
|
||||
}
|
||||
|
||||
// This can be removed once split_array is stabilized.
|
||||
fn slice_to_u32(slice: &[u8]) -> u32 {
|
||||
u32::from_le_bytes(slice.try_into().expect("Slice should be 4 bytes."))
|
||||
}
|
||||
|
||||
impl KeyringPairJson {
|
||||
/// Decrypt JSON keypair.
|
||||
fn decrypt(self, password: &str) -> Result<sr25519::Keypair, Error> {
|
||||
// Check encoding.
|
||||
// https://github.com/polkadot-js/common/blob/37fa211fdb141d4f6eb32e8f377a4651ed2d9068/packages/keyring/src/keyring.ts#L166
|
||||
if self.encoding.version != "3"
|
||||
|| !self.encoding.content.contains(&"pkcs8".to_owned())
|
||||
|| !self.encoding.content.contains(&"sr25519".to_owned())
|
||||
|| !self.encoding.r#type.contains(&"scrypt".to_owned())
|
||||
|| !self
|
||||
.encoding
|
||||
.r#type
|
||||
.contains(&"xsalsa20-poly1305".to_owned())
|
||||
{
|
||||
return Err(Error::UnsupportedEncoding);
|
||||
}
|
||||
|
||||
// Decode from Base64.
|
||||
let decoded = base64::engine::general_purpose::STANDARD.decode(self.encoded)?;
|
||||
let params: [u8; 68] = decoded[..68]
|
||||
.try_into()
|
||||
.map_err(|_| Error::UnsupportedEncoding)?;
|
||||
|
||||
// Extract scrypt parameters.
|
||||
// https://github.com/polkadot-js/common/blob/master/packages/util-crypto/src/scrypt/fromU8a.ts
|
||||
let salt = ¶ms[0..32];
|
||||
let n = slice_to_u32(¶ms[32..36]);
|
||||
let p = slice_to_u32(¶ms[36..40]);
|
||||
let r = slice_to_u32(¶ms[40..44]);
|
||||
|
||||
// FIXME At this moment we assume these to be fixed params, this is not a great idea
|
||||
// since we lose flexibility and updates for greater security. However we need some
|
||||
// protection against carefully-crafted params that can eat up CPU since these are user
|
||||
// inputs. So we need to get very clever here, but atm we only allow the defaults
|
||||
// and if no match, bail out.
|
||||
if n != 32768 || p != 1 || r != 8 {
|
||||
return Err(Error::UnsupportedScryptParameters { n, p, r });
|
||||
}
|
||||
|
||||
// Hash password.
|
||||
let scrypt_params =
|
||||
scrypt::Params::new(15, 8, 1, 32).expect("Provided parameters should be valid.");
|
||||
let mut key = Key::default();
|
||||
scrypt::scrypt(password.as_bytes(), salt, &scrypt_params, &mut key)
|
||||
.expect("Key should be 32 bytes.");
|
||||
|
||||
// Decrypt keys.
|
||||
// https://github.com/polkadot-js/common/blob/master/packages/util-crypto/src/json/decryptData.ts
|
||||
let cipher = XSalsa20Poly1305::new(&key);
|
||||
let nonce = Nonce::from_slice(¶ms[44..68]);
|
||||
let ciphertext = &decoded[68..];
|
||||
let plaintext = cipher.decrypt(nonce, ciphertext)?;
|
||||
|
||||
// https://github.com/polkadot-js/common/blob/master/packages/keyring/src/pair/decode.ts
|
||||
if plaintext.len() != 117 {
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
let header = &plaintext[0..16];
|
||||
let secret_key = &plaintext[16..80];
|
||||
let div = &plaintext[80..85];
|
||||
let public_key = &plaintext[85..117];
|
||||
|
||||
if header != [48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]
|
||||
|| div != [161, 35, 3, 33, 0]
|
||||
{
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
// Generate keypair.
|
||||
let keypair = sr25519::Keypair::from_ed25519_bytes(secret_key)?;
|
||||
|
||||
// Ensure keys are correct.
|
||||
if keypair.public_key().0 != public_key
|
||||
|| keypair.public_key().to_account_id() != self.address
|
||||
{
|
||||
return Err(Error::InvalidKeys);
|
||||
}
|
||||
|
||||
Ok(keypair)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_keypair_sr25519() {
|
||||
let json = r#"
|
||||
{
|
||||
"encoded": "DumgApKCTqoCty1OZW/8WS+sgo6RdpHhCwAkA2IoDBMAgAAAAQAAAAgAAAB6IG/q24EeVf0JqWqcBd5m2tKq5BlyY84IQ8oamLn9DZe9Ouhgunr7i36J1XxUnTI801axqL/ym1gil0U8440Qvj0lFVKwGuxq38zuifgoj0B3Yru0CI6QKEvQPU5xxj4MpyxdSxP+2PnTzYao0HDH0fulaGvlAYXfqtU89xrx2/z9z7IjSwS3oDFPXRQ9kAdDebtyCVreZ9Otw9v3",
|
||||
"encoding": {
|
||||
"content": [
|
||||
"pkcs8",
|
||||
"sr25519"
|
||||
],
|
||||
"type": [
|
||||
"scrypt",
|
||||
"xsalsa20-poly1305"
|
||||
],
|
||||
"version": "3"
|
||||
},
|
||||
"address": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
|
||||
"meta": {
|
||||
"genesisHash": "",
|
||||
"name": "Alice",
|
||||
"whenCreated": 1718265838755
|
||||
}
|
||||
}
|
||||
"#;
|
||||
decrypt_json(json, "whoisalice").unwrap();
|
||||
}
|
||||
}
|
||||
@@ -122,6 +122,18 @@ impl Keypair {
|
||||
Ok(Keypair(keypair))
|
||||
}
|
||||
|
||||
/// Construct a keypair from a slice of bytes, corresponding to
|
||||
/// an Ed25519 expanded secret key.
|
||||
#[cfg(feature = "polkadot-js-compat")]
|
||||
pub(crate) fn from_ed25519_bytes(bytes: &[u8]) -> Result<Self, Error> {
|
||||
let secret_key = schnorrkel::SecretKey::from_ed25519_bytes(bytes)?;
|
||||
|
||||
Ok(Keypair(schnorrkel::Keypair {
|
||||
public: secret_key.to_public(),
|
||||
secret: secret_key,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Derive a child key from this one given a series of junctions.
|
||||
///
|
||||
/// # Example
|
||||
@@ -199,10 +211,13 @@ pub enum Error {
|
||||
Phrase(bip39::Error),
|
||||
/// Invalid hex.
|
||||
Hex(hex::FromHexError),
|
||||
/// Signature error.
|
||||
Signature(schnorrkel::SignatureError),
|
||||
}
|
||||
|
||||
impl_from!(bip39::Error => Error::Phrase);
|
||||
impl_from!(hex::FromHexError => Error::Hex);
|
||||
impl_from!(schnorrkel::SignatureError => Error::Signature);
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
@@ -210,6 +225,7 @@ impl Display for Error {
|
||||
Error::InvalidSeed => write!(f, "Invalid seed (was it the wrong length?)"),
|
||||
Error::Phrase(e) => write!(f, "Cannot parse phrase: {e}"),
|
||||
Error::Hex(e) => write!(f, "Cannot parse hex string: {e}"),
|
||||
Error::Signature(e) => write!(f, "Signature error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user