// Copyright 2021 The Grin Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use data_encoding::BASE32; use ed25519_dalek::PublicKey as DalekPublicKey; use ed25519_dalek::SecretKey as DalekSecretKey; use grin_util::from_hex; use sha3::{Digest, Sha3_256}; use std::convert::TryFrom; use std::fmt; #[derive(Debug, Clone, Eq, PartialEq, thiserror::Error, Serialize, Deserialize)] /// OnionV3 Address Errors pub enum OnionV3Error { /// Error decoding an address from a string #[error("Address Decoding: {0}")] AddressDecoding(String), /// Error with given private key #[error("Invalid Private Key: {0}")] InvalidPrivateKey(String), } #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] /// Struct to hold an onion V3 address, represented internally as a raw /// ed25519 public key pub struct OnionV3Address([u8; 32]); impl OnionV3Address { /// from bytes pub fn from_bytes(bytes: [u8; 32]) -> Self { OnionV3Address(bytes) } /// as bytes pub fn as_bytes(&self) -> &[u8; 32] { &self.0 } /// populate from a private key pub fn from_private(key: &[u8; 32]) -> Result { let d_skey = match DalekSecretKey::from_bytes(key) { Ok(k) => k, Err(e) => { return Err(OnionV3Error::InvalidPrivateKey(format!( "Unable to create public key: {}", e ))); } }; let d_pub_key: DalekPublicKey = (&d_skey).into(); Ok(OnionV3Address(*d_pub_key.as_bytes())) } /// return dalek public key pub fn to_ed25519(&self) -> Result { let d_skey = match DalekPublicKey::from_bytes(&self.0) { Ok(k) => k, Err(e) => { return Err(OnionV3Error::InvalidPrivateKey(format!( "Unable to create dalek public key: {}", e ))); } }; Ok(d_skey) } /// Return as onion v3 address string pub fn to_ov3_str(&self) -> String { // calculate checksum let mut hasher = Sha3_256::new(); hasher.input(b".onion checksum"); hasher.input(self.0); hasher.input([0x03u8]); let checksum = hasher.result(); let mut address_bytes = self.0.to_vec(); address_bytes.push(checksum[0]); address_bytes.push(checksum[1]); address_bytes.push(0x03u8); let ret = BASE32.encode(&address_bytes); ret.to_lowercase() } /// return as http url pub fn to_http_str(&self) -> String { format!("http://{}.onion", self.to_ov3_str()) } } impl fmt::Display for OnionV3Address { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.to_ov3_str()) } } impl TryFrom<&str> for OnionV3Address { type Error = OnionV3Error; fn try_from(input: &str) -> Result { // First attempt to decode a pubkey from hex if let Ok(b) = from_hex(input) { if b.len() == 32 { let mut retval = OnionV3Address([0; 32]); retval.0.copy_from_slice(&b[0..32]); return Ok(retval); } else { return Err(OnionV3Error::AddressDecoding( "(Interpreted as Hex String) Public key is wrong length".to_owned(), )); } }; // Otherwise try to parse as onion V3 address let mut input = input.to_uppercase(); if input.starts_with("HTTP://") || input.starts_with("HTTPS://") { input = input.replace("HTTP://", ""); input = input.replace("HTTPS://", ""); } if input.ends_with(".ONION") { input = input.replace(".ONION", ""); } let orig_address_raw = input.clone(); // for now, just check input is the right length and try and decode from base32 if input.len() != 56 { return Err(OnionV3Error::AddressDecoding( "(Interpreted as Base32 String) Input address is wrong length".to_owned(), )); } let address = match BASE32.decode(input.as_bytes()) { Ok(a) => a, Err(_) => { return Err(OnionV3Error::AddressDecoding( "(Interpreted as Base32 String) Input address is not base 32".to_owned(), )); } }; let mut retval = OnionV3Address([0; 32]); retval.0.copy_from_slice(&address[0..32]); let test_v3 = retval.to_ov3_str(); if test_v3.to_uppercase() != orig_address_raw.to_uppercase() { return Err(OnionV3Error::AddressDecoding( "(Interpreted as Base32 String) Provided onion V3 address is invalid (no match)" .to_owned(), )); } Ok(retval) } } #[cfg(test)] mod test { use super::*; use std::convert::TryInto; #[test] fn onion_v3() -> Result<(), OnionV3Error> { let onion_address_str = "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyid"; let onion_address: OnionV3Address = onion_address_str.try_into()?; println!("Onion address: {:?}", onion_address); let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bb"; let onion_address_2: OnionV3Address = raw_pubkey_str.try_into()?; assert_eq!(onion_address, onion_address_2); // invalid hex string, should be interpreted as base32 and fail let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bx"; let ret: Result = raw_pubkey_str.try_into(); assert!(ret.is_err()); // wrong length hex string, should be interpreted as base32 and fail let raw_pubkey_str = "d03c09e9c19bb74aa9ea44e0fe5ae237a9bf40bddf0941064a80913a4459c8bbff"; let ret: Result = raw_pubkey_str.try_into(); assert!(ret.is_err()); // wrong length ov3 string let onion_address_str = "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyidx"; let ret: Result = onion_address_str.try_into(); assert!(ret.is_err()); // not base 32 ov3 string let onion_address_str = "2a6at2obto3uvkpkitqp4wxcg6u36qf534eucbskqciturczzc5suyi-"; let ret: Result = onion_address_str.try_into(); assert!(ret.is_err()); Ok(()) } }