referrerpolicy=no-referrer-when-downgrade

sc_cli/commands/
inspect_key.rs

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