Skip to content

Commit

Permalink
Rework the library
Browse files Browse the repository at this point in the history
Areas of improvement:
- `FromStr` and `TryFrom<&str>` impls for each struct
- `IdentityType` (always 0)
- clear, operation-specific error types that allow for robust error handling;
inspired by https://mmapped.blog/posts/12-rust-error-handling
- docstrings for all public APIs
- separation of concerns
  • Loading branch information
lukateras committed Jan 5, 2025
1 parent 33c51b9 commit 5fdfac0
Show file tree
Hide file tree
Showing 8 changed files with 586 additions and 177 deletions.
13 changes: 6 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
[package]
name = "zerotier-crypto"
version = "0.1.0"
version = "0.2.0-SNAPSHOT"
authors = ["Luka Teras <luka@sdf.org>"]
edition = "2021"
description = "Library for interfacing with ZeroTier"
description = "ZeroTier-compatible cryptography library"
readme = "README.md"
repository = "https://github.com/lukateras/rust-zerotier-crypto"
license = "BSD-2-Clause-Patent"

[dependencies]
arrayref = "0.3.9"
failure = "0.1.8"
generic-array = "1"
hex = "0.4.3"
salsa20 = "0.10.2"
serde = { version = "1", features = ["derive"] }
sha2 = "0.10.8"
hex = "0.4"
salsa20 = "0.10"
sha2 = "0.10"
thiserror = "2"

[dependencies.ed25519-dalek]
version = "2"
Expand Down
167 changes: 120 additions & 47 deletions src/address.rs
Original file line number Diff line number Diff line change
@@ -1,54 +1,65 @@
use crate::{InternalError, PublicKey, PUBLIC_KEY_LENGTH};

use arrayref::{array_mut_ref, array_ref};
use failure::Error;
use serde::*;
use std::{convert::TryFrom, mem};

use hex::{FromHex, FromHexError};
use salsa20::cipher::{KeyIvInit, StreamCipher};
use salsa20::Salsa20;

use sha2::{Digest, Sha512};

use std::convert::TryFrom;
use std::mem;
use std::str::FromStr;

use crate::{PublicKey, PUBLIC_KEY_LENGTH};

/// [`Address`](struct.Address.html) length in bytes.
pub const ADDRESS_LENGTH: usize = 5;

const BLOCK_SIZE: usize = 64;
const BLOCK_SIZE: usize = 1 << 6; // 64
const MEMORY_SIZE: usize = 1 << 21; // 2 MB
const U64_SIZE: usize = mem::size_of::<u64>();

/// 40-bit node ID derived from [`PublicKey`](struct.PublicKey.html).
const HASHCASH_MAX_FIRST_BYTE: u8 = 0x10; // 16

/// ZeroTier address derived from [`PublicKey`](struct.PublicKey.html).
///
/// Address is derived by taking last five bytes of memory-hard hash.
/// Address is valid unless:
/// The address is derived by taking the last five bytes of ZeroTier hashcash based on Salsa20 with
/// memory size of 2 MB.
///
/// - first byte of memory-hard hash is greater than `0x10`
/// - first byte of address is `0xFF`
/// - every byte of address is `0x00`
/// The hashcash is valid if the first byte is at most `0x10`.
///
/// The address is reserved if the first byte is `0xFF` or all bytes are `0x00`.
#[derive(Clone, Debug, PartialEq)]
pub struct Address([u8; ADDRESS_LENGTH]);

impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&hex::encode(self.0))
/// Error in [`Address`](struct.Address.html).
#[derive(Debug)]
pub enum AddressError {
/// Expected the first byte of hashcash to be at most `0x10`, got this byte instead.
InvalidHashcash(u8),
/// This address is reserved.
Reserved(Address),
}

impl TryFrom<&[u8; ADDRESS_LENGTH]> for Address {
type Error = AddressError;

fn try_from(bytes: &[u8; ADDRESS_LENGTH]) -> Result<Self, Self::Error> {
if bytes[0] != 0xFF && bytes[..] != [0, 0, 0, 0, 0] {
Ok(Self(bytes.clone()))
} else {
Err(Self::Error::Reserved(Self(bytes.clone())))
}
}
}

/// Ad-hoc memory-hard hash function used to derive address from ZeroTier public key.
fn memory_hard_hash(public_key: &PublicKey) -> Result<[u8; BLOCK_SIZE], Error> {
fn hashcash(public_key: &PublicKey) -> Result<[u8; BLOCK_SIZE], AddressError> {
let mut buf = [0u8; BLOCK_SIZE];
let mut mem = vec![0u8; MEMORY_SIZE];

let public_key_bytes: [u8; PUBLIC_KEY_LENGTH] = public_key.into();
buf.copy_from_slice(&Sha512::digest(&public_key_bytes));

let mut cipher = Salsa20::new(
buf[0..32].into(),
buf[32..40].into(),
);
let mut cipher = Salsa20::new(buf[0..32].into(), buf[32..40].into());

cipher.apply_keystream(&mut mem[..BLOCK_SIZE]);

Expand All @@ -63,50 +74,112 @@ fn memory_hard_hash(public_key: &PublicKey) -> Result<[u8; BLOCK_SIZE], Error> {
let n1 = u64::from_be_bytes(*array_ref!(mem, i, U64_SIZE));
let n2 = u64::from_be_bytes(*array_ref!(mem, i + U64_SIZE, U64_SIZE));

let i1 = usize::try_from(n1)? % (BLOCK_SIZE / U64_SIZE) * U64_SIZE;
let i2 = usize::try_from(n2)? % (MEMORY_SIZE / U64_SIZE) * U64_SIZE;
let i1 = n1 % (BLOCK_SIZE as u64 / U64_SIZE as u64) * U64_SIZE as u64;
let i2 = n2 % (MEMORY_SIZE as u64 / U64_SIZE as u64) * U64_SIZE as u64;

mem::swap(
array_mut_ref!(buf, i1, U64_SIZE),
array_mut_ref!(mem, i2, U64_SIZE),
array_mut_ref![buf, i1 as usize, U64_SIZE],
array_mut_ref![mem, i2 as usize, U64_SIZE],
);

cipher.apply_keystream(&mut buf[..]);
}

if buf[0] >= 17 {
Err(InternalError::InvalidHashcash.into())
} else {
if buf[0] <= HASHCASH_MAX_FIRST_BYTE {
Ok(buf)
} else {
Err(AddressError::InvalidHashcash(buf[0]))
}
}

/// Tries to derive address from [`PublicKey`](struct.PublicKey.html). Throws
/// [`InternalError`](enum.InternalError.html) for invalid addresses.
impl TryFrom<&PublicKey> for Address {
type Error = Error;
type Error = AddressError;

fn try_from(public_key: &PublicKey) -> Result<Self, Error> {
let hash = memory_hard_hash(public_key)?;
let addr = array_ref!(hash, BLOCK_SIZE - ADDRESS_LENGTH, ADDRESS_LENGTH).clone();
fn try_from(public_key: &PublicKey) -> Result<Self, Self::Error> {
let hash = hashcash(public_key)?;
let addr_bytes = array_ref![hash, BLOCK_SIZE - ADDRESS_LENGTH, ADDRESS_LENGTH];

if addr[0] == 0xff || addr[..] == [0, 0, 0, 0, 0] {
Err(InternalError::ReservedAddress.into())
} else {
Ok(Address(addr))
}
Address::try_from(addr_bytes)
}
}

/// Error while reading [`Address`](struct.Address.html) from a byte slice.
#[derive(Debug)]
pub enum FromSliceAddressError {
/// Expected the slice to be [`ADDRESS_LENGTH`](constant.ADDRESS_LENGTH.html)
/// bytes long, got this length instead.
InvalidLength(usize),
/// Error in [`Address`](struct.Address.html).
AddressError(AddressError),
}

impl From<AddressError> for FromSliceAddressError {
fn from(err: AddressError) -> Self {
Self::AddressError(err)
}
}

/// Tries to construct an address from a slice of bytes. Fails if `len(bytes) != 5`.
impl TryFrom<&[u8]> for Address {
type Error = Error;
type Error = FromSliceAddressError;

fn try_from(bytes: &[u8]) -> Result<Self, Error> {
if bytes.len() != ADDRESS_LENGTH {
Err(InternalError::BytesLengthError.into())
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
let length = bytes.len();

if length == ADDRESS_LENGTH {
Ok(Self::try_from(array_ref![bytes, 0, ADDRESS_LENGTH])?)
} else {
Ok(Self(*array_ref!(bytes, 0, ADDRESS_LENGTH)))
Err(Self::Error::InvalidLength(length))
}
}
}

/// Error while parsing [`Address`](struct.Address.html) from a string.
#[derive(Debug)]
pub enum FromStrAddressError {
/// Error while decoding the string.
DecodeError(FromHexError),
/// Expected the string to encode [`ADDRESS_LENGTH`](constant.ADDRESS_LENGTH.html)
/// bytes, got this many bytes instead.
InvalidByteLength(usize),
/// Error in [`Address`](struct.Address.html).
AddressError(AddressError),
}

impl From<FromHexError> for FromStrAddressError {
fn from(err: FromHexError) -> Self {
Self::DecodeError(err)
}
}

impl From<AddressError> for FromStrAddressError {
fn from(err: AddressError) -> Self {
Self::AddressError(err)
}
}

impl From<FromSliceAddressError> for FromStrAddressError {
fn from(err: FromSliceAddressError) -> Self {
match err {
FromSliceAddressError::InvalidLength(len) => Self::InvalidByteLength(len),
FromSliceAddressError::AddressError(err) => err.into(),
}
}
}

impl FromHex for Address {
type Error = FromStrAddressError;

fn from_hex<T: AsRef<[u8]>>(hex: T) -> Result<Self, Self::Error> {
Ok(Self::try_from(&<[u8; ADDRESS_LENGTH]>::from_hex(hex)?)?)
}
}

impl FromStr for Address {
type Err = FromStrAddressError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_hex(s)
}
}
13 changes: 0 additions & 13 deletions src/error.rs

This file was deleted.

Loading

0 comments on commit 5fdfac0

Please sign in to comment.