From 7d119cf5f9b7445bd2ac530175c193d0aca7930b Mon Sep 17 00:00:00 2001
From: clearloop <26088946+clearloop@users.noreply.github.com>
Date: Fri, 2 Feb 2024 06:43:19 +0800
Subject: [PATCH] feat(keyring): introduce gring (#3619)
---
Cargo.lock | 22 +++
Cargo.toml | 1 +
Makefile | 3 +-
utils/crates-io/src/lib.rs | 3 +-
utils/gring/Cargo.toml | 39 ++++++
utils/gring/README.md | 7 +
utils/gring/res/pair.json | 14 ++
utils/gring/src/bin/gring.rs | 55 ++++++++
utils/gring/src/cmd.rs | 245 ++++++++++++++++++++++++++++++++++
utils/gring/src/keyring.rs | 177 ++++++++++++++++++++++++
utils/gring/src/keystore.rs | 229 +++++++++++++++++++++++++++++++
utils/gring/src/lib.rs | 32 +++++
utils/gring/src/pair.rs | 126 +++++++++++++++++
utils/gring/src/scrypt.rs | 116 ++++++++++++++++
utils/gring/src/ss58.rs | 117 ++++++++++++++++
utils/gring/tests/command.rs | 91 +++++++++++++
utils/gring/tests/keystore.rs | 34 +++++
17 files changed, 1309 insertions(+), 2 deletions(-)
create mode 100644 utils/gring/Cargo.toml
create mode 100644 utils/gring/README.md
create mode 100644 utils/gring/res/pair.json
create mode 100644 utils/gring/src/bin/gring.rs
create mode 100644 utils/gring/src/cmd.rs
create mode 100644 utils/gring/src/keyring.rs
create mode 100644 utils/gring/src/keystore.rs
create mode 100644 utils/gring/src/lib.rs
create mode 100644 utils/gring/src/pair.rs
create mode 100644 utils/gring/src/scrypt.rs
create mode 100644 utils/gring/src/ss58.rs
create mode 100644 utils/gring/tests/command.rs
create mode 100644 utils/gring/tests/keystore.rs
diff --git a/Cargo.lock b/Cargo.lock
index 491f807f6f3..7fafa6ec81a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4583,6 +4583,28 @@ dependencies = [
"syn 2.0.48",
]
+[[package]]
+name = "gring"
+version = "1.1.0"
+dependencies = [
+ "anyhow",
+ "base64 0.21.7",
+ "blake2",
+ "bs58 0.5.0",
+ "clap 4.4.18",
+ "colored",
+ "dirs",
+ "hex",
+ "nacl",
+ "once_cell",
+ "rand 0.8.5",
+ "schnorrkel 0.9.1",
+ "serde",
+ "serde_json",
+ "tracing",
+ "tracing-subscriber 0.3.18",
+]
+
[[package]]
name = "group"
version = "0.13.0"
diff --git a/Cargo.toml b/Cargo.toml
index 14c5a713f05..95707c5ea72 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -458,6 +458,7 @@ toml_edit = "0.21.0" # crat
scale-decode = "0.9.0" # gsdk
directories = "5.0.1" # utils/key-finder
num-traits = { version = "0.2", default-features = false } # gear-core
+blake2 = "0.10.6" # gring
# TODO: remove after wasmer bug is fixed:
# `misaligned pointer dereference: address must be a multiple of 0x8 but is...`
diff --git a/Makefile b/Makefile
index a48a07cf491..f4480f181ee 100644
--- a/Makefile
+++ b/Makefile
@@ -197,7 +197,8 @@ test-gear: # Crates except gclient, gcli, gsdk are excluded to significantly dec
--exclude gear-authorship \
--exclude pallet-gear-staking-rewards \
--exclude gear-wasm-gen \
- --exclude demo-stack-allocations
+ --exclude demo-stack-allocations \
+ --exclude gring
.PHONY: test-gear-release
test-gear-release:
diff --git a/utils/crates-io/src/lib.rs b/utils/crates-io/src/lib.rs
index a19b8739530..fbfb9335b3d 100644
--- a/utils/crates-io/src/lib.rs
+++ b/utils/crates-io/src/lib.rs
@@ -66,7 +66,8 @@ pub const STACKED_DEPENDENCIES: [&str; 13] = [
/// Packages need to be published.
///
/// NOTE: DO NOT change the order of this array.
-pub const PACKAGES: [&str; 6] = [
+pub const PACKAGES: [&str; 7] = [
+ "gring",
"gear-wasm-builder",
"gstd",
"gtest",
diff --git a/utils/gring/Cargo.toml b/utils/gring/Cargo.toml
new file mode 100644
index 00000000000..30f96b84aee
--- /dev/null
+++ b/utils/gring/Cargo.toml
@@ -0,0 +1,39 @@
+[package]
+name = "gring"
+description = "Substrate keystore implementation"
+keywords = [ "substrate", "gear", "keystore" ]
+version.workspace = true
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+homepage.workspace = true
+repository.workspace = true
+
+[[bin]]
+name = "gring"
+path = "src/bin/gring.rs"
+required-features = ["cli"]
+
+[dependencies]
+anyhow.workspace = true
+blake2.workspace = true
+bs58 = { workspace = true, features = ["alloc"] }
+base64.workspace = true
+nacl.workspace = true
+once_cell.workspace = true
+rand = { workspace = true, features = ["std", "std_rng"] }
+schnorrkel.workspace = true
+serde = { workspace = true, features = ["derive"] }
+serde_json.workspace = true
+tracing.workspace = true
+
+# Feature CLI
+clap = { workspace = true, features = ["derive"], optional = true }
+colored = { workspace = true, optional = true }
+dirs = { workspace = true, optional = true }
+hex = { workspace = true, features = ["std"], optional = true }
+tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
+
+[features]
+default = ["cli"]
+cli = ["clap", "colored", "dirs", "hex", "tracing-subscriber"]
diff --git a/utils/gring/README.md b/utils/gring/README.md
new file mode 100644
index 00000000000..58a39fb71da
--- /dev/null
+++ b/utils/gring/README.md
@@ -0,0 +1,7 @@
+# keyring
+
+This crate implements a sr25519 keyring which is synced from [the keystore implementation in polkadot-js][keystore-format].
+
+## Keystore
+
+[keystore-format]: https://github.com/polkadot-js/common/blob/6971012f4af62f453ba25d83d0ebbfd12eaf5709/packages/util-crypto/src/json/encryptFormat.ts#L9
diff --git a/utils/gring/res/pair.json b/utils/gring/res/pair.json
new file mode 100644
index 00000000000..3e2f3b2e4bf
--- /dev/null
+++ b/utils/gring/res/pair.json
@@ -0,0 +1,14 @@
+{
+ "encoded": "X/sAaS3pNejnqvbHk0lne8tcXXmTu2gPQgXvtbf3azgAgAAAAQAAAAgAAABxGGfnP+9PCbP7Gp0+7jxxl8twTthzIq4pLfC0m6NvA8hk557A4dkDapszVKhlyDhTvnQQE2WwhqzkfDwvq0XtFl9PDW6ShvVM/lSVLkZTF6QGnTzRZ2dwT7+X5v+gjFIJftI5z3vLFg7NM+NXy7kxU039iooVTxYDqzCnMSjXMBtnY2cqNedlGUcrbDGE0lNdWqu3MWT9J27kmysC",
+ "encoding": {
+ "content": ["pkcs8", "sr25519"],
+ "type": ["scrypt", "xsalsa20-poly1305"],
+ "version": "3"
+ },
+ "address": "5Hax9tpSjfiX1nYrqhFf8F3sLiaa2ZfPv2VeDQzPBLzKNjRq",
+ "meta": {
+ "genesisHash": "",
+ "name": "GEAR",
+ "whenCreated": 1659544420591
+ }
+}
diff --git a/utils/gring/src/bin/gring.rs b/utils/gring/src/bin/gring.rs
new file mode 100644
index 00000000000..a2f44ea54a4
--- /dev/null
+++ b/utils/gring/src/bin/gring.rs
@@ -0,0 +1,55 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use anyhow::Result;
+use clap::{CommandFactory, Parser};
+use gring::cmd::Command;
+use tracing_subscriber::filter::EnvFilter;
+
+/// Gear keyring.
+#[derive(Parser)]
+pub struct Opt {
+ /// The verbosity level.
+ #[arg(global = true, short, long, action = clap::ArgAction::Count)]
+ pub verbose: u8,
+
+ /// Sub commands.
+ #[command(subcommand)]
+ pub command: Command,
+}
+
+impl Opt {
+ /// Run the CLI with logger.
+ pub fn start() -> Result<()> {
+ let app = Self::parse();
+ let name = Self::command().get_name().to_string();
+ let env = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(match app.verbose {
+ 0 => format!("{name}=info"),
+ 1 => format!("{name}=debug"),
+ 2 => "debug".into(),
+ _ => "trace".into(),
+ }));
+
+ tracing_subscriber::fmt().with_env_filter(env).init();
+ app.command.run()
+ }
+}
+
+fn main() -> anyhow::Result<()> {
+ Opt::start()
+}
diff --git a/utils/gring/src/cmd.rs b/utils/gring/src/cmd.rs
new file mode 100644
index 00000000000..5ac74667027
--- /dev/null
+++ b/utils/gring/src/cmd.rs
@@ -0,0 +1,245 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+//! CLI implementation for gring.
+
+#![cfg(feature = "cli")]
+
+use crate::{ss58, Keyring, Keystore};
+use anyhow::{anyhow, Result};
+use clap::Parser;
+use colored::{ColoredString, Colorize};
+use schnorrkel::{PublicKey, Signature};
+use std::{fs, path::PathBuf};
+
+/// gring sub commands.
+#[derive(Parser)]
+pub enum Command {
+ /// Generate a new key.
+ New {
+ /// The name of the key.
+ name: String,
+ /// The passphrase of the key.
+ #[arg(short, long)]
+ passphrase: String,
+ /// If the key should be a vanity key.
+ #[arg(short, long)]
+ vanity: Option,
+ },
+ /// List all keys in keystore.
+ #[clap(visible_alias = "l")]
+ List {
+ /// If only list the primary key.
+ #[arg(short, long)]
+ primary: bool,
+ },
+ /// Use the provided key as primary key.
+ Use {
+ /// Set the key as the primary key.
+ key: String,
+ },
+ /// Sign a message.
+ Sign {
+ /// The singning context.
+ #[clap(short, long, default_value = "gring.vara")]
+ ctx: String,
+ /// The message to sign.
+ message: String,
+ /// the passphrase of the primary key.
+ #[clap(short, long)]
+ passphrase: String,
+ },
+ /// Verify a message.
+ Verify {
+ /// The singning context.
+ #[clap(short, long, default_value = "gring.vara")]
+ ctx: String,
+ /// The signed to message.
+ message: String,
+ /// The signature to verify.
+ signature: String,
+ /// The address used in the verification, supports hex
+ /// public key bytes and VARA ss58 address.
+ ///
+ /// NOTE: if not provided, the address of the primary
+ /// key will be used.
+ #[arg(short, long)]
+ address: Option,
+ },
+}
+
+impl Command {
+ /// The path of the keyring store.
+ ///
+ /// NOTE: This is currently not configurable.
+ pub fn store() -> Result {
+ let app = env!("CARGO_PKG_NAME");
+ let store = dirs::data_dir()
+ .ok_or_else(|| anyhow!("Failed to locate app directory."))?
+ .join(app);
+
+ fs::create_dir_all(&store).map_err(|e| {
+ tracing::error!("Failed to create keyring store at {store:?}");
+ e
+ })?;
+
+ tracing::info!(
+ "keyring store: {}",
+ store.display().to_string().underline().dimmed()
+ );
+ Ok(store)
+ }
+
+ /// Run the command.
+ pub fn run(self) -> Result<()> {
+ let mut keyring = Keyring::load(Command::store()?)?;
+ match self {
+ Command::New {
+ mut name,
+ vanity,
+ passphrase,
+ } => {
+ if name.len() > 16 {
+ return Err(anyhow!("Name must be less than 16 characters."));
+ }
+
+ let raw_name = name.clone();
+ let path = {
+ let mut path = keyring.store.join(&name).with_extension("json");
+ let mut count = 0;
+ while path.exists() {
+ name = format!("{}-{}", &raw_name, count);
+ path = keyring.store.join(&name).with_extension("json");
+ count += 1;
+ }
+
+ path
+ };
+
+ if name != raw_name {
+ tracing::info!(
+ "Key {} exists, auto switching to {}",
+ raw_name.underline(),
+ name.underline().cyan()
+ );
+ }
+
+ let (keystore, keypair) =
+ keyring.create(&name, vanity.as_deref(), Some(passphrase.as_ref()))?;
+
+ println!("{:<16}{}", "Name:", name.bold());
+ println!("{:<16}{}", "VARA Address: ", keystore.address);
+ println!("{:<16}0x{}", "Public Key:", hex::encode(keypair.public));
+ println!(
+ "Drag {} to the polkadot.js extension to import it.",
+ path.display().to_string().underline()
+ );
+ }
+ Command::List { primary } => {
+ let key = keyring.primary()?;
+ if primary {
+ Self::print_key(&key);
+ return Ok(());
+ }
+
+ println!("| {:<16} | {:<49} |", "Name".bold(), "Address".bold());
+ println!("| {} | {} |", "-".repeat(16), "-".repeat(49));
+
+ for key in keyring.list() {
+ let mut name: ColoredString = key.meta.name.clone().into();
+ let mut address: ColoredString = key.address.clone().into();
+ if key.meta.name == keyring.primary {
+ name = name.cyan();
+ address = address.cyan();
+ };
+
+ println!("| {name:<16} | {address} |");
+ }
+ }
+ Command::Use { key } => {
+ let key = keyring.set_primary(key)?;
+ println!("The primary key has been updated to:");
+ Self::print_key(&key);
+ }
+ Command::Sign {
+ ctx,
+ message,
+ passphrase,
+ } => {
+ let key = keyring.primary()?;
+ let pair = key.decrypt_scrypt(passphrase.as_ref()).map_err(|e| {
+ anyhow!("Incorrect passphrase, failed to decrypt keystore, {e}")
+ })?;
+ let sig = pair
+ .sign(schnorrkel::signing_context(ctx.as_bytes()).bytes(message.as_bytes()));
+ println!("{:<16}{}", "Key:", key.meta.name.green().bold());
+ println!("{:<16}{}", "SS58 Address:", key.address);
+ println!("{:<16}{ctx}", "Context:");
+ println!("{:<16}{message}", "Message:");
+ println!("{:<16}0x{}", "Signature:", hex::encode(sig.to_bytes()));
+ }
+ Command::Verify {
+ ctx,
+ message,
+ signature,
+ address,
+ } => {
+ let pk_bytes = if let Some(address) = address {
+ if let Some(encoded) = address.strip_prefix("0x") {
+ hex::decode(encoded).map_err(Into::into)
+ } else {
+ ss58::decode(address.as_bytes(), 32)
+ }
+ } else {
+ let key = keyring.primary()?;
+ ss58::decode(key.address.as_bytes(), 32)
+ }?;
+
+ let pk = PublicKey::from_bytes(&pk_bytes)
+ .map_err(|e| anyhow!("Failed to decode public key, {e}"))?;
+
+ let result = if pk
+ .verify(
+ schnorrkel::signing_context(ctx.as_bytes()).bytes(message.as_bytes()),
+ &Signature::from_bytes(&hex::decode(signature.trim_start_matches("0x"))?)
+ .map_err(|e| anyhow!("Failed to decode signature, {e}"))?,
+ )
+ .is_ok()
+ {
+ "Verified".green().bold()
+ } else {
+ "Not Verified".red().bold()
+ };
+
+ println!("{:<16}{result}", "Result:");
+ println!("{:<16}{ctx}", "Context:");
+ println!("{:<16}{message}", "Message:");
+ println!("{:<16}0x{signature}", "Signature:");
+ println!("{:<16}0x{}", "Public Key:", hex::encode(&pk_bytes));
+ println!("{:<16}{}", "SS58 Address:", ss58::encode(&pk_bytes));
+ }
+ }
+ Ok(())
+ }
+
+ /// Print a single key.
+ fn print_key(key: &Keystore) {
+ println!("Name: {}", key.meta.name.to_string().bold());
+ println!("VARA Address: {}", key.address.to_string().underline());
+ }
+}
diff --git a/utils/gring/src/keyring.rs b/utils/gring/src/keyring.rs
new file mode 100644
index 00000000000..cbc7da1e4ad
--- /dev/null
+++ b/utils/gring/src/keyring.rs
@@ -0,0 +1,177 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+//! Keyring implementation based on the polkadot-js keystore.
+
+use crate::{
+ ss58::{self, VARA_SS58_PREFIX},
+ Keystore,
+};
+use anyhow::Result;
+use colored::Colorize;
+use schnorrkel::Keypair;
+use serde::{Deserialize, Serialize};
+use std::{fs, path::PathBuf};
+
+const CONFIG: &str = "keyring.json";
+
+/// Gear keyring.
+#[derive(Default, Serialize, Deserialize)]
+pub struct Keyring {
+ /// Path to the store.
+ #[serde(skip)]
+ pub store: PathBuf,
+ /// A set of keystore instances.
+ #[serde(skip)]
+ ring: Vec,
+ /// The primary key.
+ pub primary: String,
+ /// The SS58 prefix.
+ #[serde(default = "ss58::default_ss58_version")]
+ pub ss58_version: u16,
+}
+
+impl Keyring {
+ /// Loads the keyring from the store.
+ ///
+ /// NOTE: For the store path, see [`STORE`].
+ pub fn load(store: PathBuf) -> Result {
+ let ring = fs::read_dir(&store)?
+ .filter_map(|entry| {
+ let path = entry.ok()?.path();
+ let content = fs::read(&path).ok()?;
+ if path.ends_with(CONFIG) {
+ return None;
+ }
+
+ serde_json::from_slice(&content)
+ .map_err(|err| {
+ tracing::warn!("Failed to load keystore at {path:?}: {err}");
+ err
+ })
+ .ok()
+ })
+ .collect::>();
+
+ let config = store.join(CONFIG);
+ let mut this = if config.exists() {
+ serde_json::from_slice(&fs::read(&config)?)?
+ } else {
+ Self::default()
+ };
+
+ if this.ss58_version != VARA_SS58_PREFIX {
+ ss58::set_default_ss58_version(this.ss58_version);
+ }
+
+ this.ring = ring;
+ this.store = store;
+
+ Ok(this)
+ }
+
+ /// Update and get the primary key.
+ pub fn primary(&mut self) -> Result {
+ if self.ring.is_empty() {
+ return Err(anyhow::anyhow!(
+ "No keys in keyring, run {} to create a new one.",
+ "`gring generate -p `"
+ .underline()
+ .cyan()
+ .bold()
+ ));
+ }
+
+ if let Some(key) = self
+ .ring
+ .iter()
+ .find(|k| k.meta.name == self.primary)
+ .cloned()
+ {
+ Ok(key)
+ } else {
+ self.primary = self.ring[0].meta.name.clone();
+ fs::write(self.store.join(CONFIG), serde_json::to_vec_pretty(&self)?)?;
+ Ok(self.ring[0].clone())
+ }
+ }
+
+ /// Set the primary key.
+ pub fn set_primary(&mut self, name: String) -> Result {
+ let key = self
+ .ring
+ .iter()
+ .find(|k| k.meta.name == name)
+ .cloned()
+ .ok_or_else(|| {
+ anyhow::anyhow!(
+ "Key with name {} not found, run {} to see all keys in keyring.",
+ name.underline().bold(),
+ "`gring list`".underline().cyan().bold()
+ )
+ })?;
+
+ self.primary = name;
+ fs::write(self.store.join(CONFIG), serde_json::to_vec_pretty(&self)?)?;
+ Ok(key)
+ }
+
+ /// Set the SS58 version.
+ pub fn set_ss58_version(&mut self, version: u16) -> Result<()> {
+ self.ss58_version = version;
+ fs::write(self.store.join(CONFIG), serde_json::to_vec_pretty(&self)?)?;
+ Ok(())
+ }
+
+ /// create a new key in keyring.
+ pub fn create(
+ &mut self,
+ name: &str,
+ vanity: Option<&str>,
+ passphrase: Option<&str>,
+ ) -> Result<(Keystore, Keypair)> {
+ let keypair = if let Some(vanity) = vanity {
+ tracing::info!("Generating vanity key with prefix {vanity}...");
+ let mut keypair = Keypair::generate();
+
+ while !ss58::encode(&keypair.public.to_bytes()).starts_with(vanity) {
+ keypair = Keypair::generate();
+ }
+
+ keypair
+ } else {
+ Keypair::generate()
+ };
+
+ let mut keystore = Keystore::encrypt(keypair.clone(), passphrase.map(|p| p.as_bytes()))?;
+ keystore.meta.name = name.into();
+
+ fs::write(
+ self.store.join(&keystore.meta.name).with_extension("json"),
+ serde_json::to_vec_pretty(&keystore)?,
+ )?;
+
+ self.ring.push(keystore.clone());
+ Ok((keystore, keypair))
+ }
+
+ /// List all keystores.
+ pub fn list(&self) -> &[Keystore] {
+ self.ring.as_ref()
+ }
+}
diff --git a/utils/gring/src/keystore.rs b/utils/gring/src/keystore.rs
new file mode 100644
index 00000000000..b027d944a6f
--- /dev/null
+++ b/utils/gring/src/keystore.rs
@@ -0,0 +1,229 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use crate::{ss58, KeypairInfo, Scrypt};
+use anyhow::{anyhow, Result};
+use base64::{engine::general_purpose::STANDARD, Engine as _};
+use rand::RngCore;
+use schnorrkel::Keypair;
+use serde::{Deserialize, Serialize};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+/// JSON keystore for storing sr25519 key pair.
+#[derive(Clone, Debug, Serialize, Deserialize, Default)]
+pub struct Keystore {
+ /// The encoded keypair in base64.
+ pub encoded: String,
+ /// Encoding format.
+ #[serde(default)]
+ pub encoding: Encoding,
+ /// The address of the keypair.
+ pub address: String,
+ /// The meta data of the keypair.
+ #[serde(default)]
+ pub meta: Meta,
+}
+
+impl Keystore {
+ /// The length of nonce.
+ const NONCE_LENGTH: usize = 24;
+
+ /// Encrypt the provided keypair with the given password.
+ pub fn encrypt(keypair: Keypair, passphrase: Option<&[u8]>) -> Result {
+ let info = KeypairInfo::from(keypair);
+ if let Some(passphrase) = passphrase {
+ Self::encrypt_scrypt(info, passphrase)
+ } else {
+ Ok(Self::encrypt_none(info))
+ }
+ }
+
+ /// Encrypt keypair info with scrypt.
+ pub fn encrypt_scrypt(info: KeypairInfo, passphrase: &[u8]) -> Result {
+ let mut encoded = Vec::new();
+
+ // 1. Get passwd from scrypt
+ let scrypt = Scrypt::default();
+ let passwd = scrypt.passwd(passphrase)?;
+ encoded.extend_from_slice(&scrypt.encode());
+
+ // 2. Generate random nonce
+ let mut nonce = [0; Self::NONCE_LENGTH];
+ rand::thread_rng().fill_bytes(&mut nonce);
+ encoded.extend_from_slice(&nonce);
+
+ // 3. Pack secret box
+ let encrypted = nacl::secret_box::pack(&info.encode(), &nonce, &passwd[..32])
+ .map_err(|e| anyhow!("{e:?}"))?;
+ encoded.extend_from_slice(&encrypted);
+
+ Ok(Self {
+ encoded: STANDARD.encode(&encoded),
+ address: ss58::encode(&info.public),
+ encoding: Encoding::scrypt(),
+ ..Default::default()
+ })
+ }
+
+ /// Encrypt keypair without encryption.
+ pub fn encrypt_none(info: KeypairInfo) -> Self {
+ Self {
+ encoded: STANDARD.encode(info.encode()),
+ address: ss58::encode(&info.public),
+ ..Default::default()
+ }
+ }
+
+ /// Decrypt keypair from encrypted data.
+ pub fn decrypt(&self, passphrase: Option<&[u8]>) -> Result {
+ if let Some(passphrase) = passphrase {
+ if !self.encoding.is_scrypt() {
+ return Err(anyhow!(
+ "unsupported key deriven function {}.",
+ self.encoding.ty[0]
+ ));
+ }
+
+ self.decrypt_scrypt(passphrase)
+ } else {
+ if self.encoding.is_xsalsa20_poly1305() {
+ return Err(anyhow!("password required to decode encrypted data."));
+ }
+
+ self.decrypt_none()
+ }
+ }
+
+ /// Decrypt keypair from encrypted data with scrypt.
+ pub fn decrypt_scrypt(&self, passphrase: &[u8]) -> Result {
+ let decoded = self.decoded()?;
+
+ // 1. Get passwd from scrypt
+ let mut encoded_scrypt = [0; Scrypt::ENCODED_LENGTH];
+ encoded_scrypt.copy_from_slice(&decoded[..Scrypt::ENCODED_LENGTH]);
+ let passwd = Scrypt::decode(encoded_scrypt).passwd(passphrase)?;
+
+ // 2. Decrypt the secret key with xsalsa20-poly1305
+ let encrypted = &decoded[Scrypt::ENCODED_LENGTH..];
+ let secret = nacl::secret_box::open(
+ &encrypted[Self::NONCE_LENGTH..],
+ &encrypted[..Self::NONCE_LENGTH],
+ &passwd[..32],
+ )
+ .map_err(|e| anyhow!("{e:?}"))?;
+
+ // 3. Decode the secret key to keypair
+ KeypairInfo::decode(&secret[..KeypairInfo::ENCODED_LENGTH])?.into_keypair()
+ }
+
+ /// Decrypt keypair from data without encryption.
+ pub fn decrypt_none(&self) -> Result {
+ KeypairInfo::decode(&self.decoded()?)?.into_keypair()
+ }
+
+ /// Returns self with the given name in meta.
+ pub fn with_name(mut self, name: &str) -> Self {
+ self.meta.name = name.to_owned();
+ self
+ }
+
+ /// Decode the encoded keypair info with base64.
+ fn decoded(&self) -> Result> {
+ STANDARD.decode(&self.encoded).map_err(Into::into)
+ }
+}
+
+/// Encoding format for the keypair.
+#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
+pub struct Encoding {
+ /// The content of the keystore.
+ ///
+ /// - The first element is the standard.
+ /// - The second element is the key algorithm.
+ pub content: (String, String),
+
+ /// The type of the keystore.
+ ///
+ /// - The first element is the key deriven function of the keystore.
+ /// - if the first element is `none`, there will be no cipher following.
+ /// - The second element is the encryption cipher of the keystore.
+ #[serde(rename = "type")]
+ pub ty: Vec,
+
+ /// The version of the keystore.
+ pub version: String,
+}
+
+impl Encoding {
+ /// None encoding format.
+ pub fn none() -> Self {
+ Self {
+ content: ("pkcs8".into(), "sr25519".into()),
+ ty: vec!["none".into()],
+ version: "3".to_string(),
+ }
+ }
+
+ /// Recommend encoding format.
+ pub fn scrypt() -> Self {
+ Self {
+ content: ("pkcs8".into(), "sr25519".into()),
+ ty: vec!["scrypt".into(), "xsalsa20-poly1305".into()],
+ ..Default::default()
+ }
+ }
+
+ /// Check if is encoding with scrypt.
+ pub fn is_scrypt(&self) -> bool {
+ self.ty.get(0) == Some(&"scrypt".into())
+ }
+
+ /// Check if the cipher is xsalsa20-poly1305.
+ pub fn is_xsalsa20_poly1305(&self) -> bool {
+ self.ty.get(1) == Some(&"xsalsa20-poly1305".into())
+ }
+}
+
+impl Default for Encoding {
+ fn default() -> Self {
+ Self::none()
+ }
+}
+
+/// The metadata of the key pair.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Meta {
+ /// The name of the key pair.
+ pub name: String,
+
+ /// The timestamp when the key pair is created in milliseconds.
+ #[serde(rename = "whenCreated")]
+ pub when_created: u128,
+}
+
+impl Default for Meta {
+ fn default() -> Self {
+ Self {
+ name: "".into(),
+ when_created: SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("time went backwards")
+ .as_millis(),
+ }
+ }
+}
diff --git a/utils/gring/src/lib.rs b/utils/gring/src/lib.rs
new file mode 100644
index 00000000000..32f52f6f656
--- /dev/null
+++ b/utils/gring/src/lib.rs
@@ -0,0 +1,32 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+mod keyring;
+mod keystore;
+mod pair;
+mod scrypt;
+
+pub mod cmd;
+pub mod ss58;
+
+pub use self::{
+ keyring::Keyring,
+ keystore::{Encoding, Keystore},
+ pair::KeypairInfo,
+ scrypt::Scrypt,
+};
diff --git a/utils/gring/src/pair.rs b/utils/gring/src/pair.rs
new file mode 100644
index 00000000000..abe3aa3b28c
--- /dev/null
+++ b/utils/gring/src/pair.rs
@@ -0,0 +1,126 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+use anyhow::{anyhow, Result};
+use schnorrkel::{Keypair, KEYPAIR_LENGTH, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH};
+
+/// Key info wrapped in pkcs8 format.
+///
+/// NOTE: the meaning of these bytes is ambiguous for now, see
+///
+///
+/// For the encoded data format of this implementation:
+///
+/// ENCODED(117) = HEADER(16) + SECRET_KEY_LENGTH(64) + DIVIDER(5) + PUBLIC_KEY_LENGTH(32)
+pub struct KeypairInfo {
+ /// Schnorrkel secret key.
+ pub secret: [u8; SECRET_KEY_LENGTH],
+ /// Schnorrkel public key.
+ pub public: [u8; PUBLIC_KEY_LENGTH],
+}
+
+impl KeypairInfo {
+ /// The length of the pkcs8 key info.
+ ///
+ /// NOTE: LENGTH(117) = HEADER(16) + SECRET_KEY_LENGTH + DIVIDER(5) + PUBLIC_KEY_LENGTH
+ pub const ENCODED_LENGTH: usize = 117;
+
+ /// The length of pkcs8 header in polkadot-js.
+ const PKCS8_HEADER_LENGTH: usize = 16;
+
+ /// The pkcs8 header used in polkadot-js.
+ const PKCS8_HEADER: [u8; Self::PKCS8_HEADER_LENGTH] =
+ [48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32];
+
+ /// The length of pkcs8 divider in polkadot-js.
+ const PKCS8_DIVIDER_LENGTH: usize = 5;
+
+ /// The pkcs8 divider used in polkadot-js.
+ const PKCS8_DIVIDER: [u8; Self::PKCS8_DIVIDER_LENGTH] = [161, 35, 3, 33, 0];
+
+ /// The offset of secret key in pkcs8 key info.
+ const SECRET_KEY_OFFSET: usize = Self::PKCS8_HEADER_LENGTH;
+
+ /// The offset of divider in pkcs8 key info.
+ const PKCS8_DIVIDER_OFFSET: usize = Self::PKCS8_HEADER_LENGTH + SECRET_KEY_LENGTH;
+
+ /// The offset of public key in pkcs8 key info.
+ const PUBLIC_KEY_OFFSET: usize =
+ Self::SECRET_KEY_OFFSET + SECRET_KEY_LENGTH + Self::PKCS8_DIVIDER_LENGTH;
+
+ /// Decode key info from fixed bytes.
+ pub fn decode(data: &[u8]) -> Result {
+ if data[..Self::PKCS8_HEADER_LENGTH] != Self::PKCS8_HEADER {
+ return Err(anyhow!("invalid pkcs8 header"));
+ }
+
+ if data[Self::PKCS8_DIVIDER_OFFSET..Self::PKCS8_DIVIDER_OFFSET + Self::PKCS8_DIVIDER_LENGTH]
+ != Self::PKCS8_DIVIDER
+ {
+ return Err(anyhow!("invalid pkcs8 divider"));
+ }
+
+ let mut encoded = [0; Self::ENCODED_LENGTH];
+ encoded.copy_from_slice(data);
+
+ let mut secret = [0u8; SECRET_KEY_LENGTH];
+ let mut public = [0u8; PUBLIC_KEY_LENGTH];
+
+ secret.copy_from_slice(
+ &encoded[Self::SECRET_KEY_OFFSET..Self::SECRET_KEY_OFFSET + SECRET_KEY_LENGTH],
+ );
+ public.copy_from_slice(
+ &encoded[Self::PUBLIC_KEY_OFFSET..Self::PUBLIC_KEY_OFFSET + PUBLIC_KEY_LENGTH],
+ );
+
+ Ok(Self { secret, public })
+ }
+
+ /// Encode self to fixed bytes.
+ pub fn encode(&self) -> [u8; Self::ENCODED_LENGTH] {
+ let mut encoded = [0; Self::ENCODED_LENGTH];
+
+ encoded[..Self::PKCS8_HEADER_LENGTH].copy_from_slice(&Self::PKCS8_HEADER);
+ encoded[Self::SECRET_KEY_OFFSET..Self::SECRET_KEY_OFFSET + SECRET_KEY_LENGTH]
+ .copy_from_slice(&self.secret);
+ encoded
+ [Self::PKCS8_DIVIDER_OFFSET..Self::PKCS8_DIVIDER_OFFSET + Self::PKCS8_DIVIDER_LENGTH]
+ .copy_from_slice(&Self::PKCS8_DIVIDER);
+ encoded[Self::PUBLIC_KEY_OFFSET..Self::PUBLIC_KEY_OFFSET + PUBLIC_KEY_LENGTH]
+ .copy_from_slice(&self.public);
+
+ encoded
+ }
+
+ /// Convert self to schnorrkel keypair.
+ pub fn into_keypair(self) -> Result {
+ let mut bytes = [0u8; KEYPAIR_LENGTH];
+ bytes[..SECRET_KEY_LENGTH].copy_from_slice(&self.secret);
+ bytes[SECRET_KEY_LENGTH..].copy_from_slice(&self.public);
+ Keypair::from_half_ed25519_bytes(&bytes)
+ .map_err(|e| anyhow!("Failed to create pair: {e:?}"))
+ }
+}
+
+impl From for KeypairInfo {
+ fn from(keypair: Keypair) -> Self {
+ let secret = keypair.secret.to_ed25519_bytes();
+ let public = keypair.public.to_bytes();
+ Self { secret, public }
+ }
+}
diff --git a/utils/gring/src/scrypt.rs b/utils/gring/src/scrypt.rs
new file mode 100644
index 00000000000..d443b356017
--- /dev/null
+++ b/utils/gring/src/scrypt.rs
@@ -0,0 +1,116 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+//! codec for keys.
+
+use anyhow::{anyhow, Result};
+use rand::RngCore;
+use schnorrkel::PUBLIC_KEY_LENGTH;
+
+/// Parameters of scrypt
+pub struct Scrypt {
+ /// Salt used for scrypt.
+ pub salt: [u8; Self::SALT_LENGTH],
+ /// CPU/memory cost parameter, must be power of 2 (e.g. 1024).
+ pub n: u32,
+ /// Block size parameter, which fine-tunes sequential memory
+ /// read size and performance ( 8 is commonly used ).
+ pub r: u32,
+ /// Parallelization parameter ( 1 .. 2^32 -1 * hLen/MFlen ).
+ pub p: u32,
+}
+
+impl Scrypt {
+ /// The length of encoded scrypt params.
+ ///
+ /// NOTE: SALT(32) + N(4) + R(4) + P(4)
+ pub const ENCODED_LENGTH: usize = 44;
+
+ /// The length of salt used for scrypt.
+ const SALT_LENGTH: usize = 32;
+
+ /// Read from encoded data.
+ pub fn decode(encoded: [u8; Self::ENCODED_LENGTH]) -> Self {
+ let mut salt = [0; Self::SALT_LENGTH];
+ salt.copy_from_slice(&encoded[..Self::SALT_LENGTH]);
+
+ let params = encoded[Self::SALT_LENGTH..]
+ .chunks(4)
+ .map(|bytes| {
+ let mut buf = [0; 4];
+ buf.copy_from_slice(bytes);
+ u32::from_le_bytes(buf)
+ })
+ .collect::>();
+
+ Self {
+ salt,
+ n: params[0].ilog2(),
+ r: params[2],
+ p: params[1],
+ }
+ }
+
+ /// Encode self to bytes.
+ pub fn encode(&self) -> [u8; Self::ENCODED_LENGTH] {
+ let mut buf = [0; Self::ENCODED_LENGTH];
+ let n = 1 << self.n;
+ buf[..Self::SALT_LENGTH].copy_from_slice(&self.salt);
+ buf[Self::SALT_LENGTH..].copy_from_slice(
+ [n, self.p, self.r]
+ .iter()
+ .flat_map(|n| n.to_le_bytes())
+ .collect::>()
+ .as_slice(),
+ );
+
+ buf
+ }
+
+ /// Get passwd from passphrase.
+ pub fn passwd(&self, passphrase: &[u8]) -> Result<[u8; 32]> {
+ let mut passwd = [0; 32];
+ let output = nacl::scrypt(
+ passphrase,
+ &self.salt,
+ self.n as u8,
+ self.r as usize,
+ self.p as usize,
+ PUBLIC_KEY_LENGTH,
+ &|_: u32| {},
+ )
+ .map_err(|e| anyhow!("{e:?}"))?;
+ passwd.copy_from_slice(&output[..32]);
+
+ Ok(passwd)
+ }
+}
+
+impl Default for Scrypt {
+ fn default() -> Self {
+ let mut salt = [0; Self::SALT_LENGTH];
+ rand::thread_rng().fill_bytes(&mut salt);
+
+ Self {
+ salt,
+ n: 15,
+ r: 8,
+ p: 1,
+ }
+ }
+}
diff --git a/utils/gring/src/ss58.rs b/utils/gring/src/ss58.rs
new file mode 100644
index 00000000000..3b9f3766eca
--- /dev/null
+++ b/utils/gring/src/ss58.rs
@@ -0,0 +1,117 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+
+//! SS58 encoding implementation
+//!
+//! This library is extracted from [ss58 codec][ss58-codec] in `sp-core`, not
+//! importing `sp-core` because it is super big (~300 dependencies).
+//!
+//! [ss58-codec]: https://paritytech.github.io/polkadot-sdk/master/sp_core/crypto/trait.Ss58Codec.html
+
+use anyhow::{anyhow, Result};
+use blake2::{Blake2b512, Digest};
+use core::sync::atomic::{AtomicU16, Ordering};
+
+/// The SS58 prefix of vara network.
+pub const VARA_SS58_PREFIX: u16 = 137;
+
+/// The default ss58 version.
+pub static DEFAULT_SS58_VERSION: AtomicU16 = AtomicU16::new(VARA_SS58_PREFIX);
+
+/// SS58 prefix
+const SS58_PREFIX: &[u8] = b"SS58PRE";
+
+/// The checksum length used in ss58 encoding
+const CHECKSUM_LENGTH: usize = 2;
+
+/// Encode data to SS58 format.
+pub fn encode(data: &[u8]) -> String {
+ let ident: u16 = default_ss58_version() & 0b0011_1111_1111_1111;
+ let mut v = match ident {
+ 0..=63 => vec![ident as u8],
+ 64..=16_383 => {
+ // upper six bits of the lower byte(!)
+ let first = ((ident & 0b0000_0000_1111_1100) as u8) >> 2;
+ // lower two bits of the lower byte in the high pos,
+ // lower bits of the upper byte in the low pos
+ let second = ((ident >> 8) as u8) | ((ident & 0b0000_0000_0000_0011) as u8) << 6;
+ vec![first | 0b01000000, second]
+ }
+ _ => unreachable!("masked out the upper two bits; qed"),
+ };
+
+ v.extend_from_slice(data);
+ let r = blake2b_512(&v);
+ v.extend(&r[0..CHECKSUM_LENGTH]);
+ bs58::encode(v).into_string()
+}
+
+/// Decode data from SS58 format.
+pub fn decode(encoded: &[u8], body_len: usize) -> Result> {
+ let data = bs58::decode(encoded)
+ .into_vec()
+ .map_err(|e| anyhow!("Invalid ss58 data: {}", e))?;
+ if data.len() < CHECKSUM_LENGTH {
+ return Err(anyhow!("Invalid length of encoded ss58 data."));
+ }
+
+ let (prefix_len, _) = match data[0] {
+ 0..=63 => (1, data[0] as u16),
+ 64..=127 => {
+ // weird bit manipulation owing to the combination of LE encoding and missing two
+ // bits from the left.
+ // d[0] d[1] are: 01aaaaaa bbcccccc
+ // they make the LE-encoded 16-bit value: aaaaaabb 00cccccc
+ // so the lower byte is formed of aaaaaabb and the higher byte is 00cccccc
+ let lower = (data[0] << 2) | (data[1] >> 6);
+ let upper = data[1] & 0b00111111;
+ (2, (lower as u16) | ((upper as u16) << 8))
+ }
+ _ => return Err(anyhow!("Invalid prefix of encoded ss58 data.")),
+ };
+
+ if data.len() != prefix_len + body_len + CHECKSUM_LENGTH {
+ return Err(anyhow!("Invalid length of encoded ss58 data."));
+ }
+
+ let hash = blake2b_512(&data[..prefix_len + body_len]);
+ let checksum = &hash[0..CHECKSUM_LENGTH];
+ if data[body_len + prefix_len..body_len + prefix_len + CHECKSUM_LENGTH] != *checksum {
+ return Err(anyhow!("Invalid checksum of encoded ss58 data."));
+ }
+
+ Ok(data[prefix_len..body_len + prefix_len].to_vec())
+}
+
+/// Get the default ss58 version.
+pub fn default_ss58_version() -> u16 {
+ DEFAULT_SS58_VERSION.load(Ordering::Relaxed)
+}
+
+/// Set the default ss58 version.
+pub fn set_default_ss58_version(version: u16) {
+ DEFAULT_SS58_VERSION.store(version, Ordering::Relaxed);
+}
+
+/// blake2b_512 hash
+fn blake2b_512(data: &[u8]) -> Vec {
+ let mut ctx = Blake2b512::new();
+ ctx.update(SS58_PREFIX);
+ ctx.update(data);
+ ctx.finalize().to_vec()
+}
diff --git a/utils/gring/tests/command.rs b/utils/gring/tests/command.rs
new file mode 100644
index 00000000000..0ca422c56ea
--- /dev/null
+++ b/utils/gring/tests/command.rs
@@ -0,0 +1,91 @@
+// This file is part of Gear.
+//
+// Copyright (C) 2021-2024 Gear Technologies Inc.
+// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see .
+#![cfg(feature = "cli")]
+
+use anyhow::{anyhow, Result};
+use gring::{cmd::Command, Keystore};
+use std::{path::PathBuf, process};
+
+fn bin() -> PathBuf {
+ let target = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target");
+ let profile = if cfg!(debug_assertions) {
+ "debug"
+ } else {
+ "release"
+ };
+
+ target.join(profile).join("gring")
+}
+
+fn gring(bin: &PathBuf, args: &[&str]) -> Result {
+ let output = process::Command::new(bin).args(args).output()?;
+ if output.stdout.is_empty() {
+ return Err(anyhow::anyhow!(
+ "stderr: {}",
+ String::from_utf8_lossy(&output.stderr)
+ ));
+ }
+
+ let stdout = output.stdout;
+ Ok(String::from_utf8_lossy(&stdout).to_string())
+}
+
+#[test]
+fn new() -> Result<()> {
+ let key = "_gring_test_new";
+ let passphrase = "test";
+ Command::New {
+ name: key.to_string(),
+ passphrase: passphrase.to_string(),
+ vanity: None,
+ }
+ .run()?;
+
+ let json = Command::store()?.join(format!("{key}.json"));
+ assert!(json.exists());
+
+ let keystore = serde_json::from_slice::(&std::fs::read(json)?)?;
+ assert!(keystore.decrypt_scrypt(passphrase.as_bytes()).is_ok());
+ Ok(())
+}
+
+#[test]
+fn sign_and_verify() -> Result<()> {
+ let key = "_gring_sig";
+ let key2 = "_gring_sig_2";
+ let message = "vara";
+ let bin = bin();
+
+ gring(&bin, &["new", key, "-p", "test"])?;
+ gring(&bin, &["use", key])?;
+ let sign = gring(&bin, &["sign", message, "-p", "test"])?;
+ let signature = sign
+ .lines()
+ .find(|line| line.contains("Signature"))
+ .ok_or_else(|| anyhow!("Signature not found in output: {}", sign))?
+ .split("Signature:")
+ .collect::>()[1]
+ .trim();
+ assert!(gring(&bin, &["verify", message, signature])?.contains("Verified"));
+
+ // `key2` can not verify this signature bcz it is signed by `key`.
+ gring(&bin, &["new", key2, "-p", "test"])?;
+ gring(&bin, &["use", key2])?;
+ assert!(gring(&bin, &["verify", message, signature])?.contains("Not Verified"));
+ Ok(())
+}
diff --git a/utils/gring/tests/keystore.rs b/utils/gring/tests/keystore.rs
new file mode 100644
index 00000000000..eaa7ef6bafe
--- /dev/null
+++ b/utils/gring/tests/keystore.rs
@@ -0,0 +1,34 @@
+use anyhow::Result;
+use gring::Keystore;
+use schnorrkel::Keypair;
+
+const POLKADOT_JS_PAIR: &[u8] = include_bytes!("../res/pair.json");
+
+#[test]
+fn polkadot_js() -> Result<()> {
+ let store = serde_json::from_slice::(POLKADOT_JS_PAIR)?;
+
+ assert!(store.decrypt(None).is_err());
+ assert!(store.decrypt(Some(b"42")).is_err());
+ assert!(store.decrypt(Some(b"000000")).is_ok());
+ Ok(())
+}
+
+#[test]
+fn scrypt() -> Result<()> {
+ let passphrase = b"42";
+ let pair = Keypair::generate();
+ let store = Keystore::encrypt_scrypt(pair.clone().into(), passphrase)?;
+
+ assert_eq!(pair.secret, store.decrypt_scrypt(b"42")?.secret);
+ Ok(())
+}
+
+#[test]
+fn nopasswd() -> Result<()> {
+ let pair = Keypair::generate();
+ let store = Keystore::encrypt_none(pair.clone().into());
+
+ assert_eq!(pair.secret, store.decrypt_none()?.secret);
+ Ok(())
+}