/* * identity.cpp — Antheos Protocol v9 Identity * * Base-32 encoding, BID/SID generation, and SID generator * per Antheos Protocol 1.0 Level 1, v9 specification (§5.4-5.6, §11.2). * * Copyright (c) 2025-2026 Are Bjørby * SPDX-License-Identifier: MIT */ #include "antheos.hpp" #include #include #include #include #include namespace antheos { /* ── FNV-1a 64-bit hash ── */ namespace { constexpr uint64_t FNV1A_OFFSET = UINT64_C(14695981039346656037); constexpr uint64_t FNV1A_PRIME = UINT64_C(1099511628211); uint64_t fnv1a_64(const uint8_t* data, size_t len) { uint64_t hash = FNV1A_OFFSET; for (size_t i = 0; i < len; i++) { hash ^= static_cast(data[i]); hash *= FNV1A_PRIME; } return hash; } bool read_urandom(uint8_t* buf, size_t len) { int fd = ::open("/dev/urandom", O_RDONLY); if (fd < 0) return false; ssize_t n = ::read(fd, buf, len); ::close(fd); return n == static_cast(len); } } // anonymous namespace /* ── id:: free functions ── */ namespace id { std::optional base32_encode(const uint8_t* data, size_t data_len) { if (data == nullptr || data_len == 0) return std::nullopt; size_t num_chars = (data_len * 8 + 4) / 5; std::string out; out.reserve(num_chars); uint32_t bits = 0; int nbits = 0; for (size_t i = 0; i < data_len; i++) { bits = (bits << 8) | static_cast(data[i]); nbits += 8; while (nbits >= 5) { nbits -= 5; out += BASE32_ALPHABET[(bits >> static_cast(nbits)) & 0x1Fu]; } } if (nbits > 0) { out += BASE32_ALPHABET[(bits << static_cast(5 - nbits)) & 0x1Fu]; } return out; } std::optional bid_generate(size_t len, const uint8_t* entropy, size_t entropy_len) { if (len < BID_MIN_LEN || len > BID_MAX_LEN) return std::nullopt; size_t nbytes = bid_entropy_needed(len); if (entropy == nullptr || entropy_len < nbytes) return std::nullopt; auto encoded = base32_encode(entropy, nbytes); if (!encoded) return std::nullopt; return encoded->substr(0, len); } std::optional bid_generate(size_t len) { if (len < BID_MIN_LEN || len > BID_MAX_LEN) return std::nullopt; size_t nbytes = bid_entropy_needed(len); uint8_t entropy[16]; /* BID_MAX_LEN=16 → max 10 bytes needed */ if (!read_urandom(entropy, nbytes)) return std::nullopt; return bid_generate(len, entropy, nbytes); } std::optional sid_generate( std::string_view oid, std::string_view did, std::string_view iid, uint32_t counter, size_t len, const uint8_t* entropy, size_t entropy_len) { if (oid.empty() || did.empty() || iid.empty()) return std::nullopt; if (len < SID_MIN_LEN || len > SID_MAX_LEN) return std::nullopt; /* Build hash input: "oid:did:iid:" + raw entropy bytes */ char input[256]; int n = std::snprintf(input, sizeof(input), "%.*s:%.*s:%.*s:%u", static_cast(oid.size()), oid.data(), static_cast(did.size()), did.data(), static_cast(iid.size()), iid.data(), counter); if (n < 0 || static_cast(n) >= sizeof(input)) return std::nullopt; /* Append entropy bytes directly to hash input */ size_t input_len = static_cast(n); if (entropy != nullptr && entropy_len > 0) { size_t avail = sizeof(input) - input_len; size_t copy = entropy_len < avail ? entropy_len : avail; std::memcpy(input + input_len, entropy, copy); input_len += copy; } /* Primary FNV-1a hash → 8 bytes, little-endian (least-significant byte * first). The hash input varies only in its trailing bytes (the counter), * and FNV-1a's final rounds move its LOW bits the most — so here the low * bits are the most-varying, and base-32 truncation from the low end is * near-uniform. Do NOT "tidy" this to big-endian: measured at len=4 over * 200k counters, LE gives 9.9% collisions (8.9% is the uniform ideal) while * BE gives 82.7% — catastrophic. The sensitivity is a short-length effect * (len=5 ≈ 2× LE; len=6 indistinguishable), which is why SID_MIN_LEN=6 * keeps generation clear of it. Spec §11.2 step 1. */ uint64_t h1 = fnv1a_64(reinterpret_cast(input), input_len); uint8_t hash_bytes[16]; for (int i = 0; i < 8; i++) { hash_bytes[i] = static_cast(h1 & 0xFFu); h1 >>= 8; } /* Secondary hash for extension — needed for len > 12 */ uint64_t h2 = fnv1a_64(hash_bytes, 8); for (int i = 0; i < 8; i++) { hash_bytes[8 + i] = static_cast(h2 & 0xFFu); h2 >>= 8; } auto encoded = base32_encode(hash_bytes, 16); if (!encoded) return std::nullopt; return encoded->substr(0, len); } } // namespace id /* ── SidPool ── */ struct SidPool::Impl { std::string oid; std::string did; std::string iid; uint32_t next_counter = 0; size_t initial_len = id::SID_MIN_LEN; size_t current_len = id::SID_MIN_LEN; size_t max_len = id::SID_MAX_LEN; Impl(std::string_view o, std::string_view d, std::string_view i) : oid(o), did(d), iid(i) {} }; SidPool::SidPool(std::string_view oid, std::string_view did, std::string_view iid) { if (oid.empty() || did.empty() || iid.empty()) throw std::invalid_argument("SidPool: OID, DID, IID must be non-empty"); impl_ = std::make_unique(oid, did, iid); } SidPool::~SidPool() = default; SidPool::SidPool(SidPool&&) noexcept = default; SidPool& SidPool::operator=(SidPool&&) noexcept = default; std::optional SidPool::acquire_unchecked() { /* Non-conformant escape hatch per spec §11.2 — see header comment. * Returns whatever sid_generate produces at the current length, with * no active-set collision check. Callers that need spec §11.2 * uniqueness MUST use acquire_unique() instead. */ auto& p = *impl_; uint8_t entropy[id::sid_entropy_needed()]; if (!read_urandom(entropy, sizeof(entropy))) return std::nullopt; auto sid = id::sid_generate(p.oid, p.did, p.iid, p.next_counter, p.current_len, entropy, sizeof(entropy)); if (!sid) return std::nullopt; p.next_counter++; return sid; } std::optional SidPool::acquire_unique(CollisionCheck check) { if (!check) return std::nullopt; auto& p = *impl_; /* Generate fresh per §11.2 — up to 3 full cycles */ size_t full_cycles = 0; while (full_cycles < 3) { uint8_t entropy[id::sid_entropy_needed()]; if (!read_urandom(entropy, sizeof(entropy))) return std::nullopt; auto sid = id::sid_generate(p.oid, p.did, p.iid, p.next_counter, p.current_len, entropy, sizeof(entropy)); if (!sid) return std::nullopt; if (!check(*sid)) { p.next_counter++; return sid; } /* Collision: grow length or roll over */ if (p.current_len < p.max_len) { p.current_len++; } else { p.next_counter++; p.current_len = p.initial_len; full_cycles++; } } return std::nullopt; } } // namespace antheos