Skip to main content

soil_cli/commands/
inspect_key.rs

1// This file is part of Soil.
2
3// Copyright (C) Soil contributors.
4// Copyright (C) Parity Technologies (UK) Ltd.
5// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
6
7//! Implementation of the `inspect` subcommand
8
9use crate::{
10	utils::{self, print_from_public, print_from_uri},
11	with_crypto_scheme, CryptoSchemeFlag, Error, KeystoreParams, NetworkSchemeFlag, OutputTypeFlag,
12};
13use clap::Parser;
14use std::str::FromStr;
15use subsoil::core::crypto::{ExposeSecret, SecretString, SecretUri, Ss58Codec};
16
17/// The `inspect` command
18#[derive(Debug, Parser)]
19#[command(
20	name = "inspect",
21	about = "Gets a public key and a SS58 address from the provided Secret URI"
22)]
23pub struct InspectKeyCmd {
24	/// A Key URI to be inspected. May be a secret seed, secret URI
25	/// (with derivation paths and password), SS58, public URI or a hex encoded public key.
26	/// If it is a hex encoded public key, `--public` needs to be given as argument.
27	/// If the given value is a file, the file content will be used
28	/// as URI.
29	/// If omitted, you will be prompted for the URI.
30	uri: Option<String>,
31
32	/// Is the given `uri` a hex encoded public key?
33	#[arg(long)]
34	public: bool,
35
36	#[allow(missing_docs)]
37	#[clap(flatten)]
38	pub keystore_params: KeystoreParams,
39
40	#[allow(missing_docs)]
41	#[clap(flatten)]
42	pub network_scheme: NetworkSchemeFlag,
43
44	#[allow(missing_docs)]
45	#[clap(flatten)]
46	pub output_scheme: OutputTypeFlag,
47
48	#[allow(missing_docs)]
49	#[clap(flatten)]
50	pub crypto_scheme: CryptoSchemeFlag,
51
52	/// Expect that `--uri` has the given public key/account-id.
53	/// If `--uri` has any derivations, the public key is checked against the base `uri`, i.e. the
54	/// `uri` without any derivation applied. However, if `uri` has a password or there is one
55	/// given by `--password`, it will be used to decrypt `uri` before comparing the public
56	/// key/account-id.
57	/// If there is no derivation in `--uri`, the public key will be checked against the public key
58	/// of `--uri` directly.
59	#[arg(long, conflicts_with = "public")]
60	pub expect_public: Option<String>,
61}
62
63impl InspectKeyCmd {
64	/// Run the command
65	pub fn run(&self) -> Result<(), Error> {
66		let uri = utils::read_uri(self.uri.as_ref())?;
67		let password = self.keystore_params.read_password()?;
68
69		if self.public {
70			with_crypto_scheme!(
71				self.crypto_scheme.scheme,
72				print_from_public(
73					&uri,
74					self.network_scheme.network,
75					self.output_scheme.output_type,
76				)
77			)?;
78		} else {
79			if let Some(ref expect_public) = self.expect_public {
80				with_crypto_scheme!(
81					self.crypto_scheme.scheme,
82					expect_public_from_phrase(expect_public, &uri, password.as_ref())
83				)?;
84			}
85
86			with_crypto_scheme!(
87				self.crypto_scheme.scheme,
88				print_from_uri(
89					&uri,
90					password,
91					self.network_scheme.network,
92					self.output_scheme.output_type,
93				)
94			);
95		}
96
97		Ok(())
98	}
99}
100
101/// Checks that `expect_public` is the public key of `suri`.
102///
103/// If `suri` has any derivations, `expect_public` is checked against the public key of the "bare"
104/// `suri`, i.e. without any derivations.
105///
106/// Returns an error if the public key does not match.
107fn expect_public_from_phrase<Pair: subsoil::core::Pair>(
108	expect_public: &str,
109	suri: &str,
110	password: Option<&SecretString>,
111) -> Result<(), Error> {
112	let secret_uri = SecretUri::from_str(suri).map_err(|e| format!("{:?}", e))?;
113	let expected_public = if let Some(public) = expect_public.strip_prefix("0x") {
114		let hex_public = array_bytes::hex2bytes(public)
115			.map_err(|_| format!("Invalid expected public key hex: `{}`", expect_public))?;
116		Pair::Public::try_from(&hex_public)
117			.map_err(|_| format!("Invalid expected public key: `{}`", expect_public))?
118	} else {
119		Pair::Public::from_string_with_version(expect_public)
120			.map_err(|_| format!("Invalid expected account id: `{}`", expect_public))?
121			.0
122	};
123
124	let pair = Pair::from_string_with_seed(
125		secret_uri.phrase.expose_secret().as_str(),
126		password
127			.or_else(|| secret_uri.password.as_ref())
128			.map(|p| p.expose_secret().as_str()),
129	)
130	.map_err(|_| format!("Invalid secret uri: {}", suri))?
131	.0;
132
133	if pair.public() == expected_public {
134		Ok(())
135	} else {
136		Err(format!("Expected public ({}) key does not match.", expect_public).into())
137	}
138}
139
140#[cfg(test)]
141mod tests {
142	use super::*;
143	use subsoil::core::crypto::{ByteArray, Pair};
144	use subsoil::runtime::traits::IdentifyAccount;
145
146	#[test]
147	fn inspect() {
148		let words =
149			"remember fiber forum demise paper uniform squirrel feel access exclude casual effort";
150		let seed = "0xad1fb77243b536b90cfe5f0d351ab1b1ac40e3890b41dc64f766ee56340cfca5";
151
152		let inspect = InspectKeyCmd::parse_from(&["inspect-key", words, "--password", "12345"]);
153		assert!(inspect.run().is_ok());
154
155		let inspect = InspectKeyCmd::parse_from(&["inspect-key", seed]);
156		assert!(inspect.run().is_ok());
157	}
158
159	#[test]
160	fn inspect_public_key() {
161		let public = "0x12e76e0ae8ce41b6516cce52b3f23a08dcb4cfeed53c6ee8f5eb9f7367341069";
162
163		let inspect = InspectKeyCmd::parse_from(&["inspect-key", "--public", public]);
164		assert!(inspect.run().is_ok());
165	}
166
167	#[test]
168	fn inspect_with_expected_public_key() {
169		let check_cmd = |seed, expected_public, success| {
170			let inspect = InspectKeyCmd::parse_from(&[
171				"inspect-key",
172				"--expect-public",
173				expected_public,
174				seed,
175			]);
176			let res = inspect.run();
177
178			if success {
179				assert!(res.is_ok());
180			} else {
181				assert!(res.unwrap_err().to_string().contains(&format!(
182					"Expected public ({}) key does not match.",
183					expected_public
184				)));
185			}
186		};
187
188		let seed =
189			"remember fiber forum demise paper uniform squirrel feel access exclude casual effort";
190		let invalid_public = "0x12e76e0ae8ce41b6516cce52b3f23a08dcb4cfeed53c6ee8f5eb9f7367341069";
191		let valid_public = subsoil::core::sr25519::Pair::from_string_with_seed(seed, None)
192			.expect("Valid")
193			.0
194			.public();
195		let valid_public_hex = array_bytes::bytes2hex("0x", valid_public.as_slice());
196		let valid_accountid = format!("{}", valid_public.into_account());
197
198		// It should fail with the invalid public key
199		check_cmd(seed, invalid_public, false);
200
201		// It should work with the valid public key & account id
202		check_cmd(seed, &valid_public_hex, true);
203		check_cmd(seed, &valid_accountid, true);
204
205		let password = "test12245";
206		let seed_with_password = format!("{}///{}", seed, password);
207		let valid_public_with_password = subsoil::core::sr25519::Pair::from_string_with_seed(
208			&seed_with_password,
209			Some(password),
210		)
211		.expect("Valid")
212		.0
213		.public();
214		let valid_public_hex_with_password =
215			array_bytes::bytes2hex("0x", valid_public_with_password.as_slice());
216		let valid_accountid_with_password =
217			format!("{}", &valid_public_with_password.into_account());
218
219		// Only the public key that corresponds to the seed with password should be accepted.
220		check_cmd(&seed_with_password, &valid_public_hex, false);
221		check_cmd(&seed_with_password, &valid_accountid, false);
222
223		check_cmd(&seed_with_password, &valid_public_hex_with_password, true);
224		check_cmd(&seed_with_password, &valid_accountid_with_password, true);
225
226		let seed_with_password_and_derivation = format!("{}//test//account///{}", seed, password);
227
228		let valid_public_with_password_and_derivation =
229			subsoil::core::sr25519::Pair::from_string_with_seed(
230				&seed_with_password_and_derivation,
231				Some(password),
232			)
233			.expect("Valid")
234			.0
235			.public();
236		let valid_public_hex_with_password_and_derivation =
237			array_bytes::bytes2hex("0x", valid_public_with_password_and_derivation.as_slice());
238
239		// They should still be valid, because we check the base secret key.
240		check_cmd(&seed_with_password_and_derivation, &valid_public_hex_with_password, true);
241		check_cmd(&seed_with_password_and_derivation, &valid_accountid_with_password, true);
242
243		// And these should be invalid.
244		check_cmd(&seed_with_password_and_derivation, &valid_public_hex, false);
245		check_cmd(&seed_with_password_and_derivation, &valid_accountid, false);
246
247		// The public of the derived account should fail.
248		check_cmd(
249			&seed_with_password_and_derivation,
250			&valid_public_hex_with_password_and_derivation,
251			false,
252		);
253	}
254}