koibumi-core 0.0.9

The core library for Koibumi, an experimental Bitmessage client
Documentation
//! Provides Bitmessage address types.

use std::{
    convert::TryInto,
    fmt,
    io::{self, Cursor, Read},
    str::FromStr,
};

use crate::{
    crypto::{PrivateKey, PrivateKeyError, PublicKey},
    hash::{double_sha512, ripemd160_sha512, sha512},
    io::{ReadFrom, WriteTo},
    object::Tag,
    priv_util::ToHexString,
};

pub use crate::stream::StreamNumber as Stream;

/// A version number of a Bitmessage address.
pub type Version = crate::object::ObjectVersion;

/// A hash of public keys of an address.
pub type Hash = crate::hash::Hash160;

/// This error indicates
/// that the construction from public keys failed.
#[derive(Clone, Debug)]
pub enum Error {
    /// Indicates that the version supplied was not supported.
    /// The actual version supplied is returned as a payload of this variant.
    UnsupportedVersion(Version),
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::UnsupportedVersion(version) => write!(f, "unsupported version: {}", version),
        }
    }
}

impl std::error::Error for Error {}

/// A Bitmessage address.
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct Address {
    version: Version,
    stream: Stream,
    hash: Hash,
}

pub(crate) fn count_zeros(bytes: impl AsRef<[u8]>) -> usize {
    let mut count = 0;
    for _ in bytes.as_ref().iter().take_while(|b| **b == 0) {
        count += 1;
    }
    count
}

pub(crate) const DEFAULT_ZEROS: usize = 1;

impl Address {
    /// Constructs a Bitmessage address from public keys.
    pub fn from_public_keys(
        version: Version,
        stream: Stream,
        public_signing_key: &PublicKey,
        public_encryption_key: &PublicKey,
    ) -> Result<Self, Error> {
        if version.as_u64() < 2 || version.as_u64() > 4 {
            return Err(Error::UnsupportedVersion(version));
        }
        let mut bytes = Vec::new();
        bytes.append(&mut public_signing_key.encode());
        bytes.append(&mut public_encryption_key.encode());
        let hash = ripemd160_sha512(bytes);
        Ok(Self {
            version,
            stream,
            hash,
        })
    }

    /// Returns the address version.
    pub fn version(&self) -> Version {
        self.version
    }

    /// Returns the stream number.
    pub fn stream(&self) -> Stream {
        self.stream
    }

    /// Returns the hash.
    pub fn hash(&self) -> &Hash {
        &self.hash
    }

    /// Returns the broadcast tag.
    pub fn broadcast_tag(&self) -> Tag {
        let mut bytes = Vec::new();
        self.version.write_to(&mut bytes).unwrap();
        self.stream.write_to(&mut bytes).unwrap();
        self.hash.as_ref().write_to(&mut bytes).unwrap();
        Tag::new(double_sha512(bytes)[32..].try_into().unwrap())
    }

    /// Returns the private encryption key for broadcast.
    pub fn broadcast_private_encryption_key(&self) -> Result<PrivateKey, PrivateKeyError> {
        let mut bytes = Vec::new();
        self.version.write_to(&mut bytes).unwrap();
        self.stream.write_to(&mut bytes).unwrap();
        self.hash.as_ref().write_to(&mut bytes).unwrap();
        let hash = if self.version.as_u64() <= 3 {
            sha512(bytes)[..32].try_into().unwrap()
        } else {
            double_sha512(bytes)[..32].try_into().unwrap()
        };
        PrivateKey::new(hash)
    }
}

impl fmt::Display for Address {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut bytes = Vec::new();
        self.version.write_to(&mut bytes).unwrap();
        self.stream.write_to(&mut bytes).unwrap();
        let hash = match self.version.as_u64() {
            1 => &self.hash[..],
            2 | 3 => &self.hash[std::cmp::min(count_zeros(&self.hash), 2)..],
            4 => &self.hash[count_zeros(&self.hash)..],
            _ => panic!("program error: unsupported version: {}", self.version),
        };
        hash.write_to(&mut bytes).unwrap();
        let checksum = &double_sha512(&bytes)[..4];
        checksum.write_to(&mut bytes).unwrap();
        write!(f, "BM-{}", koibumi_base58::encode(bytes))
    }
}

/// An error which can be returned
/// when parsing a Bitmessage address.
///
/// This error is used as the error type for the `FromStr` implementation
/// for [`Address`].
///
/// [`Address`]: enum.Address.html
#[derive(Debug)]
pub enum ParseError {
    /// An error was caught when parsing a Base58 string.
    /// The actual error caught is returned
    /// as a payload of this variant.
    Base58Error(koibumi_base58::InvalidCharacter),

    /// The length of the decoded bytes was too short to construct a Bitmessage address.
    /// The minimum length allowed and the actual length supplied
    /// are returned as payloads of this variant.
    TooShort {
        /// The minimum length allowed.
        min: usize,
        /// The actual length supplied.
        len: usize,
    },

    /// The checksums did not match.
    /// The expected and the actual checksums are returned
    /// as payloads of this variant.
    InvalidChecksum {
        /// The expected checksum.
        expected: [u8; 4],
        /// The actual checksum.
        actual: [u8; 4],
    },

    /// A standard I/O error was caught during parsing a Bitmessage address.
    /// The actual error caught is returned as a payload of this variant.
    IoError(io::Error),

    /// Indicates that the version supplied was not supported.
    /// The actual version supplied is returned as a payload of this variant.
    UnsupportedVersion(Version),

    /// The length of the hash was invalid to construct a Bitmessage address.
    /// The minimum and the maximum lengths allowed and the actual length supplied
    /// are returned as payloads of this variant.
    InvalidHashLength {
        /// The minimum length allowed.
        min: usize,
        /// The maximum length allowed.
        max: usize,
        /// The actual length supplied.
        len: usize,
    },

    /// The hash starts with zero bytes.
    HashStartsWithZero,
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Base58Error(err) => err.fmt(f),
            Self::InvalidChecksum { expected, actual } => write!(
                f,
                "checksum should be {}, but {}",
                expected.as_ref().to_hex_string(),
                actual.as_ref().to_hex_string()
            ),
            Self::IoError(err) => err.fmt(f),
            Self::UnsupportedVersion(version) => write!(f, "unsupported version: {}", version),
            Self::TooShort { min, len } => write!(f, "length must be >={}, but {}", min, len),
            Self::InvalidHashLength { min, max, len } => write!(
                f,
                "invalid hash length: min={}, max={}, len={}",
                min, max, len
            ),
            Self::HashStartsWithZero => "hash starts with zero".fmt(f),
        }
    }
}

impl std::error::Error for ParseError {}

impl From<koibumi_base58::InvalidCharacter> for ParseError {
    fn from(err: koibumi_base58::InvalidCharacter) -> Self {
        Self::Base58Error(err)
    }
}

impl From<io::Error> for ParseError {
    fn from(err: io::Error) -> Self {
        Self::IoError(err)
    }
}

pub(crate) const VERSION: Version = Version::new(4);

fn pad_zeros(hash: Vec<u8>) -> Vec<u8> {
    const HASH_LEN: usize = 20;
    let mut bytes = [0; HASH_LEN];
    bytes[HASH_LEN - hash.len()..].copy_from_slice(&hash);
    bytes.to_vec()
}

impl FromStr for Address {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let s = if s.len() >= 3 && &s[..3] == "BM-" {
            &s[3..]
        } else {
            &s[..]
        };

        let bytes = koibumi_base58::decode(s)?;

        if bytes.len() < 4 {
            return Err(ParseError::TooShort {
                min: 4,
                len: bytes.len(),
            });
        }
        let checksum = &bytes[bytes.len() - 4..];
        let body = &bytes[..bytes.len() - 4];

        let double_hash = double_sha512(body);
        let actual_checksum = &double_hash[..4];
        if actual_checksum != checksum {
            return Err(ParseError::InvalidChecksum {
                expected: checksum.try_into().unwrap(),
                actual: actual_checksum.try_into().unwrap(),
            });
        }

        let mut bytes = Cursor::new(body);
        let version = Version::read_from(&mut bytes)?;

        // check version early
        if version.as_u64() < 1 || version.as_u64() > 4 {
            return Err(ParseError::UnsupportedVersion(version));
        }

        let stream = Stream::read_from(&mut bytes)?;

        let mut hash = Vec::new();
        bytes.read_to_end(&mut hash)?;

        match version.as_u64() {
            1 => {
                if hash.len() != 20 {
                    return Err(ParseError::InvalidHashLength {
                        min: 20,
                        max: 20,
                        len: hash.len(),
                    });
                }
                Ok(Address {
                    version,
                    stream,
                    hash: Hash::new(hash[..].try_into().unwrap()),
                })
            }
            2 | 3 => {
                if hash.len() < 18 || hash.len() > 20 {
                    return Err(ParseError::InvalidHashLength {
                        min: 18,
                        max: 20,
                        len: hash.len(),
                    });
                }
                let hash = pad_zeros(hash);
                Ok(Address {
                    version,
                    stream,
                    hash: Hash::new(hash[..].try_into().unwrap()),
                })
            }
            4 => {
                if hash.len() < 4 || hash.len() > 20 {
                    return Err(ParseError::InvalidHashLength {
                        min: 4,
                        max: 20,
                        len: hash.len(),
                    });
                }
                if hash[0] == 0 {
                    return Err(ParseError::HashStartsWithZero);
                }
                let hash = pad_zeros(hash);
                Ok(Address {
                    version,
                    stream,
                    hash: Hash::new(hash[..].try_into().unwrap()),
                })
            }
            _ => panic!("program error: unsupported version: {}", version),
        }
    }
}

#[test]
fn test_address_from_str() {
    let address = "BM-NC1qK8oQk3yya1ZfHt368uXQq9W6U2om"
        .parse::<Address>()
        .unwrap();
    assert_eq!(address.version(), 4.into());
    assert_eq!(address.stream(), 1.into());
    //assert_eq!(address.hash(), /* XXX */);
}