use base64::Engine;
use crypto_secretbox::{
Key, Nonce, XSalsa20Poly1305,
aead::{Aead, KeyInit},
};
use serde::Deserialize;
use subxt_core::utils::AccountId32;
use thiserror::Error as DeriveError;
use crate::sr25519;
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)?)
}
#[derive(Debug, DeriveError)]
pub enum Error {
#[error("Invalid JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("Unsupported encoding.")]
UnsupportedEncoding,
#[error("Base64 decoding error: {0}")]
Base64(#[from] base64::DecodeError),
#[error("Unsupported Scrypt parameters: N: {n}, p: {p}, r: {r}")]
UnsupportedScryptParameters {
n: u32,
p: u32,
r: u32,
},
#[error("Decryption error: {0}")]
Secretbox(#[from] crypto_secretbox::Error),
#[error(transparent)]
Sr25519(#[from] sr25519::Error),
#[error("The decrypted keys are not valid.")]
InvalidKeys,
}
#[derive(Deserialize)]
struct EncryptionMetadata {
content: Vec<String>,
r#type: Vec<String>,
version: String,
}
#[derive(Deserialize)]
struct KeyringPairJson {
encoded: String,
encoding: EncryptionMetadata,
address: AccountId32,
}
fn slice_to_u32(slice: &[u8]) -> u32 {
u32::from_le_bytes(slice.try_into().expect("Slice should be 4 bytes."))
}
impl KeyringPairJson {
fn decrypt(self, password: &str) -> Result<sr25519::Keypair, Error> {
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);
}
let decoded = base64::engine::general_purpose::STANDARD.decode(self.encoded)?;
let params: [u8; 68] = decoded[..68]
.try_into()
.map_err(|_| Error::UnsupportedEncoding)?;
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]);
if n != 32768 || p != 1 || r != 8 {
return Err(Error::UnsupportedScryptParameters { n, p, r });
}
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.");
let cipher = XSalsa20Poly1305::new(&key);
let nonce = Nonce::from_slice(¶ms[44..68]);
let ciphertext = &decoded[68..];
let plaintext = cipher.decrypt(nonce, ciphertext)?;
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);
}
let keypair = sr25519::Keypair::from_ed25519_bytes(secret_key)?;
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();
}
}