feistel/feistel.go
130 lines
1// Package feistel implements the seeded shuffle that pairs the registry.2//3// Permute is a bijection on [0, N) built from a six-round Feistel network on4// k = ceil(log2 N) bits with cycle walking for results ≥ N, exactly as in the5// specification:6//7// k = ceil(log2(N)); left_bits = floor(k/2); right_bits = k - left_bits8// loop:9// L = index >> right_bits; R = index & (2^right_bits - 1)10// for round = 0..5:11// mask = SHA256(seed ‖ uint8(round) ‖ uint64(R))[0..ceil(left_bits/8)]12// mask = mask >> (8*ceil(left_bits/8) - left_bits)13// R, L = L ⊕ mask, R14// result = (L << right_bits) | R15// if result < N: return result16// index = result17//18// The halves swap widths every round (the mask is always left_bits wide and19// is XORed into the low bits of whichever half is L); after the even number20// of rounds L is left_bits wide and R right_bits wide again, so the21// recombination is exact. Invert runs the rounds backwards.22//23// Pairs are adjacent positions in the shuffled order, 0↔1, 2↔3, …: the24// partner of index i is Invert(Permute(i) ⊕ 1). When N is odd the entry at25// shuffled position N−1 has no partner.26package feistel2728import (29"crypto/sha256"30"encoding/binary"31"math/bits"32)3334const rounds = 63536// widths returns k, left_bits and right_bits for a domain of size n.37func widths(n uint64) (k, left, right uint) {38if n <= 1 {39return 0, 0, 040}41k = uint(bits.Len64(n - 1)) // ceil(log2 n)42left = k / 243right = k - left44return45}4647// mask is the round function: the first ceil(left/8) bytes of48// SHA256(seed ‖ round ‖ half) as a big-endian integer, right-shifted to49// left bits.50func mask(seed *[32]byte, round uint8, half uint64, left uint) uint64 {51nbytes := (left + 7) / 852if nbytes == 0 {53return 054}55h := sha256.New()56h.Write(seed[:])57h.Write([]byte{round})58var b [8]byte59binary.BigEndian.PutUint64(b[:], half)60h.Write(b[:])61sum := h.Sum(nil)62var m uint6463for i := uint(0); i < nbytes; i++ {64m = m<<8 | uint64(sum[i])65}66return m >> (8*nbytes - left)67}6869// Permute maps index ∈ [0, n) to its shuffled position. It panics if70// index ≥ n.71func Permute(index, n uint64, seed [32]byte) uint64 {72if index >= n {73panic("feistel: index out of range")74}75k, left, right := widths(n)76if k == 0 {77return index78}79rmask := uint64(1)<<right - 180for {81l := index >> right82r := index & rmask83for round := uint8(0); round < rounds; round++ {84m := mask(&seed, round, r, left)85r, l = l^m, r86}87res := l<<right | r88if res < n {89return res90}91index = res92}93}9495// Invert maps a shuffled position ∈ [0, n) back to its index. It panics if96// position ≥ n.97func Invert(position, n uint64, seed [32]byte) uint64 {98if position >= n {99panic("feistel: position out of range")100}101k, left, right := widths(n)102if k == 0 {103return position104}105rmask := uint64(1)<<right - 1106for {107l := position >> right108r := position & rmask109for round := rounds - 1; round >= 0; round-- {110m := mask(&seed, uint8(round), l, left)111l, r = r^m, l112}113res := l<<right | r114if res < n {115return res116}117position = res118}119}120121// Partner returns the index paired with index this period, or false if it122// is the odd one out. It panics if index ≥ n.123func Partner(index, n uint64, seed [32]byte) (uint64, bool) {124p := Permute(index, n, seed) ^ 1125if p >= n {126return 0, false127}128return Invert(p, n, seed), true129}