1// OnionAddress.swift
2// SophaxChatCore
3//
4// Derives a Tor v3 .onion hostname from an Ed25519 public key.
5//
6// Tor v3 hidden service address spec (rend-spec-v3 §6):
7//
8// onion_address = base32(pubkey || checksum || version) + ".onion"
9// checksum = H(".onion checksum" || pubkey || version)[0:2]
10// version = 0x03
11// H = SHA3-256 (Keccak-256)
12//
13// The resulting address is 56 base32 characters + ".onion" = 62 chars total.
14// Because it's derived from the signing key, it is stable and tied to identity.
15
16import Foundation
17
18public enum OnionAddress {
19
20 public enum Error: Swift.Error {
21 case invalidKeyLength(Int)
22 }
23
24 private static let version: UInt8 = 0x03
25 private static let prefix = ".onion checksum"
26
27 /// Derives the Tor v3 `.onion` hostname (without port) from a 32-byte Ed25519 public key.
28 /// Throws `OnionAddress.Error.invalidKeyLength` if the key is not exactly 32 bytes.
29 public static func from(ed25519PublicKey pubkey: Data) throws -> String {
30 guard pubkey.count == 32 else {
31 throw Error.invalidKeyLength(pubkey.count)
32 }
33
34 // Checksum = SHA3-256(".onion checksum" || pubkey || 0x03)[0:2]
35 var checksumInput = Data(prefix.utf8)
36 checksumInput.append(pubkey)
37 checksumInput.append(version)
38 let checksum = Keccak.hash256(checksumInput).prefix(2)
39
40 // Encode: pubkey(32) || checksum(2) || version(1) = 35 bytes → 56 base32 chars
41 var payload = pubkey
42 payload.append(contentsOf: checksum)
43 payload.append(version)
44
45 return base32Encode(payload) + ".onion"
46 }
47
48 // MARK: - Base32 (RFC 4648, no padding, lowercase)
49
50 private static func base32Encode(_ data: Data) -> String {
51 let alphabet = Array("abcdefghijklmnopqrstuvwxyz234567")
52 var result = [Character]()
53 result.reserveCapacity((data.count * 8 + 4) / 5)
54
55 var buffer: UInt64 = 0
56 var bitsLeft = 0
57
58 for byte in data {
59 buffer = (buffer << 8) | UInt64(byte)
60 bitsLeft += 8
61 while bitsLeft >= 5 {
62 bitsLeft -= 5
63 let index = Int((buffer >> bitsLeft) & 0x1F)
64 result.append(alphabet[index])
65 }
66 }
67 if bitsLeft > 0 {
68 let index = Int((buffer << (5 - bitsLeft)) & 0x1F)
69 result.append(alphabet[index])
70 }
71 return String(result)
72 }
73}