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