sealwd 0.5.0

Secure password and token management library for Rust, featuring hashing, encryption, and random generation.
Documentation
// Copyright 2026 Thomas Zuyev

// 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.

//! # Token Core Implementation
//!
//! This module provides uniform abstractions for generating, encoding, hashing, and
//! encrypting high-entropy secrets via the [`TokenMaterial`] trait.
//!
//! It offers two concrete underlying models:
//! - [`Token`]: Stack-allocated, fixed-size token using constant generics.
//! - [`GenericToken`]: Heap-allocated, dynamically-sized token.

pub mod fixed;
pub mod generic;
pub mod hash;

#[cfg(feature = "encryption")]
pub mod encrypted;

pub use fixed::Token;
pub use generic::GenericToken;
pub use hash::TokenHash;

#[cfg(feature = "encryption")]
pub use encrypted::EncryptedToken;

#[cfg(feature = "encryption")]
use crate::crypto::{CipherSuite, EncryptedSecret};

/// Errors encountered during token management, parsing, decoding, or encryption operations.
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// Errors originating from underlying cryptographic subsystems.
    #[cfg(feature = "encryption")]
    #[error("Crypto error: {0}")]
    Crypto(#[from] crate::crypto::Error),

    /// Errors originating from hardware random number generator systems.
    #[error("Random error: {0}")]
    Random(#[from] crate::random::Error),
    /// Given byte buffer or string container failed explicit length verification bounds.
    #[error("Length mismatch: expected = {expected}; provided = {provided}")]
    LengthMismatch {
        /// The exact expected size in bytes.
        expected: usize,
        /// The actual provided size in bytes.
        provided: usize,
    },
    /// Failed to properly decode expected Base64 payloads.
    #[error("Invalid base64: {0}")]
    Base64(#[from] base64::DecodeError),
    /// Missing prefix identifier when unpacking self-describing binary structures.
    #[error("Hash algorithm is missing")]
    HashAlgorithmMissing,
    /// Unrecognized prefix identifier received when evaluating versioning schemas.
    #[error("Hash algorithm is unknown or invalid: {0}")]
    HashAlgorithmInvalid(u8),
}

pub type Result<T> = std::result::Result<T, Error>;

/// Shared functionality for high-entropy secret containers.
///
/// Implementors are responsible for housing raw byte material safely (e.g., zeroizing raw states on destruction).
pub trait TokenMaterial: Sized {
    /// Extracts a view of the underlying raw binary payload.
    #[must_use]
    fn as_bytes(&self) -> &[u8];

    /// Reconstructs an instance directly from an explicit vector of bytes.
    ///
    /// # Errors
    /// Returns [`Error::LengthMismatch`] if the target implementor maintains tight internal size bounds
    /// that do not align with the provided vector's length.
    fn from_vec(bytes: Vec<u8>) -> Result<Self>;

    /// Hashes the token using the currently selected default hashing algorithm.
    ///
    /// This method exists as a stable indirection point:
    /// - New tokens should always be hashed using this function.
    /// - The underlying algorithm may change over time (e.g. when introducing
    ///   a new hash version), without requiring call-site changes.
    /// - Existing stored hashes remain verifiable via `TokenHash`.
    ///
    /// The pepper must be a secret value stored separately from the database,
    /// typically in configuration or a secret manager.
    /// Compromise of both database and pepper allows token forgery.
    ///
    /// The default algorithm MUST remain suitable only for high-entropy,
    /// randomly generated tokens.
    #[must_use]
    fn to_default_hash(&self, pepper: &[u8]) -> TokenHash {
        TokenHash::compute(self, pepper)
    }

    /// Hashes the token using BLAKE3 with an external pepper.
    ///
    /// # Warning
    /// This is NOT a password hashing function.
    /// BLAKE3 is intentionally fast and must only be used with
    /// high-entropy, randomly generated secrets.
    #[must_use]
    fn to_blake3(&self, pepper: &[u8]) -> TokenHash {
        TokenHash::compute_blake3_v1(self, pepper)
    }

    /// Encodes the token using URL-safe, unpadded base64.
    ///
    /// This matches the format expected by `from_base64`.
    #[must_use]
    fn to_base64(&self) -> String {
        crate::encoding::base64::encode(self.as_bytes())
    }

    /// Decodes an instance out of a URL-safe, unpadded base64 string slice.
    ///
    /// # Errors
    /// Returns [`Error::Base64`] if structural parsing fails, or [`Error::LengthMismatch`]
    /// if the parsed bytes do not fulfill implementor boundaries.
    fn from_base64(data: &str) -> Result<Self> {
        let decoded = crate::encoding::base64::decode(data)?;
        Self::from_vec(decoded)
    }

    /// Encrypts the raw token material into an authenticated, type-safe [`EncryptedToken`].
    ///
    /// # Errors
    /// Returns [`Error::Crypto`] if encryption procedures fail.
    #[cfg(feature = "encryption")]
    fn encrypt(&self, key: &[u8], algo: CipherSuite) -> Result<EncryptedToken<Self>> {
        let enc = EncryptedSecret::encrypt(self.as_bytes(), key, algo)?;
        Ok(EncryptedToken::from_secret(enc))
    }
}

// Testing default implementations of TokenMaterial
#[cfg(test)]
mod tests {
    use super::*;

    struct Test(Vec<u8>);

    impl TokenMaterial for Test {
        fn as_bytes(&self) -> &[u8] {
            &self.0
        }

        fn from_vec(bytes: Vec<u8>) -> Result<Self> {
            Ok(Self(bytes))
        }
    }

    #[test]
    fn base64_roundtrip() {
        let obj = Test(b"abcdef".into());
        let s = obj.to_base64();
        let deserialized = Test::from_base64(&s).unwrap();

        assert_eq!(obj.0, deserialized.0);
        assert_eq!(s, "YWJjZGVm");
    }

    #[test]
    fn reject_base64_wrong_length() {
        let obj = Test(b"abcdef".into());
        let mut s = obj.to_base64();

        s.pop(); // corrupt

        assert!(Test::from_base64(&s).is_err());
    }
}