initial commit
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
#include "scramble/fibonacci_scrambler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
|
||||
namespace radiantwave {
|
||||
namespace scramble {
|
||||
|
||||
namespace {
|
||||
|
||||
// A single-round affine map over Z_N^2, written as
|
||||
// p(x, y) = (a*x + b*y + e, c*x + d*y + f) (mod N).
|
||||
// Entries a..f and coordinates are reduced mod N; coordinate products are
|
||||
// accumulated in int64_t, so the representation is exact for N <= 2^30.
|
||||
struct AffineMap
|
||||
{
|
||||
int a;
|
||||
int b;
|
||||
int c;
|
||||
int d;
|
||||
int e;
|
||||
int f;
|
||||
};
|
||||
|
||||
// splitmix64 (public domain, Sebastiano Vigna). Deliberately pure integer
|
||||
// arithmetic: the offset vectors it feeds are bit-identical on every platform,
|
||||
// which is what makes a compile-time seed produce identical scrambling
|
||||
// everywhere. No libc rand(), no floating point.
|
||||
uint64_t splitmix64(uint64_t& state)
|
||||
{
|
||||
uint64_t z = (state += 0x9E3779B97F4A7C15ULL);
|
||||
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
|
||||
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
|
||||
return z ^ (z >> 31);
|
||||
}
|
||||
|
||||
// T(x, y) = (y + b0, x + y + b1) (mod N), the Fibonacci affine transformation
|
||||
// T(x) = F.x + b with F = [[0,1],[1,1]] (Definitions 4.1 and 2.2). The offset
|
||||
// (b0, b1) arrives already reduced mod N from deriveOffsets().
|
||||
AffineMap forwardMap(int b0, int b1)
|
||||
{
|
||||
return {0, 1, 1, 1, b0, b1};
|
||||
}
|
||||
|
||||
// T^-1(y) = F^-1.(y - b) (mod N). F^-1 = [[-1,1],[1,0]] (algorithm.md
|
||||
// line 498) follows from the adjoint formula for the inverse of a 2x2 matrix.
|
||||
// The affine inverse of x -> A.x + b is x -> A^-1.x - A^-1.b, so the offset
|
||||
// is -F^-1.b:
|
||||
// F^-1.b = (b1 - b0, b0) => offset = ((b0 - b1) mod N, (-b0) mod N).
|
||||
// The implementation expands T^-1 to p(x, y) = (a*x + b*y + e, c*x + d*y + f)
|
||||
// with a = -1 mod N, b = 1, c = 1, d = 0, e = (b0 - b1) mod N, f = (-b0) mod N:
|
||||
// -x + y + (b0 - b1) and x - b0, which is F^-1.(y - b) as required.
|
||||
AffineMap inverseMap(int b0, int b1, int n)
|
||||
{
|
||||
int e = (b0 - b1) % n;
|
||||
if (e < 0)
|
||||
{
|
||||
e += n;
|
||||
}
|
||||
int f = (-b0) % n;
|
||||
if (f < 0)
|
||||
{
|
||||
f += n;
|
||||
}
|
||||
// a = -1 mod N is N - 1 (which is 0 when N == 1, correctly the identity).
|
||||
return {n - 1, 1, 1, 0, e, f};
|
||||
}
|
||||
|
||||
// Gather-form permutation, one pass of the algorithm: every output pixel
|
||||
// (oy, ox) is copied from the single source pixel at p(ox, oy), so exactly
|
||||
// one input pixel maps into each output pixel -- no holes, no collisions --
|
||||
// and the padding convention is filled directly where the preimage lies
|
||||
// outside the source. `p` is an affine map reduced mod N; its products are
|
||||
// accumulated in int64_t, which is exact for N <= 2^30.
|
||||
//
|
||||
// srcW x srcH is the source bound used by the bounds check:
|
||||
// - scramble round 1 reads the unpadded W x H source, so preimages outside
|
||||
// [0, W) x [0, H) become transparent-black padding;
|
||||
// - every later round reads an already-padded N x N canvas, so callers pass
|
||||
// srcW == srcH == N and every coordinate is valid.
|
||||
// Both buffers are tightly packed RGBA8888 (row stride = width * 4).
|
||||
void applyGather(const uint8_t* src, int srcW, int srcH,
|
||||
uint8_t* dst, int dstW, int dstH,
|
||||
int n, const AffineMap& p)
|
||||
{
|
||||
for (int oy = 0; oy < dstH; ++oy)
|
||||
{
|
||||
for (int ox = 0; ox < dstW; ++ox)
|
||||
{
|
||||
const int64_t xv = static_cast<int64_t>(p.a) * ox +
|
||||
static_cast<int64_t>(p.b) * oy + p.e;
|
||||
const int64_t yv = static_cast<int64_t>(p.c) * ox +
|
||||
static_cast<int64_t>(p.d) * oy + p.f;
|
||||
int sx = static_cast<int>(xv % n);
|
||||
if (sx < 0)
|
||||
{
|
||||
sx += n;
|
||||
}
|
||||
int sy = static_cast<int>(yv % n);
|
||||
if (sy < 0)
|
||||
{
|
||||
sy += n;
|
||||
}
|
||||
|
||||
uint8_t* out = dst + (static_cast<size_t>(oy) * dstW + ox) * 4;
|
||||
if (sx < srcW && sy < srcH)
|
||||
{
|
||||
const uint8_t* in = src + (static_cast<size_t>(sy) * srcW + sx) * 4;
|
||||
out[0] = in[0];
|
||||
out[1] = in[1];
|
||||
out[2] = in[2];
|
||||
out[3] = in[3];
|
||||
}
|
||||
else
|
||||
{
|
||||
out[0] = 0;
|
||||
out[1] = 0;
|
||||
out[2] = 0;
|
||||
out[3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The key schedule: the per-round offset vectors b_i = (b0, b1) for `rounds`
|
||||
// rounds, derived deterministically from `seed` as two consecutive splitmix64
|
||||
// draws per round, each reduced mod N. The same (seed, rounds, N) triple
|
||||
// always produces the same sequence, so scramble() and unscramble() derive the
|
||||
// exact same offsets and a fixed seed yields fixed scrambling.
|
||||
std::vector<int> deriveOffsets(uint64_t seed, int rounds, int n)
|
||||
{
|
||||
std::vector<int> offsets;
|
||||
offsets.reserve(static_cast<size_t>(rounds) * 2);
|
||||
uint64_t state = seed;
|
||||
for (int i = 0; i < rounds; ++i)
|
||||
{
|
||||
offsets.push_back(static_cast<int>(splitmix64(state) % static_cast<uint64_t>(n)));
|
||||
offsets.push_back(static_cast<int>(splitmix64(state) % static_cast<uint64_t>(n)));
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int canvasSize(int width, int height)
|
||||
{
|
||||
if (width < 1 || height < 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return std::max(width, height);
|
||||
}
|
||||
|
||||
bool scramble(const uint8_t* src, uint8_t* dst, int width, int height,
|
||||
const ScrambleConfig& config)
|
||||
{
|
||||
const int n = canvasSize(width, height);
|
||||
if (n == 0 || src == nullptr || dst == nullptr || n > (1 << 30))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int rounds = std::max(1, config.rounds);
|
||||
const std::vector<int> offsets = deriveOffsets(config.seed, rounds, n);
|
||||
|
||||
// Ping-pong scratch so a gather pass never reads the buffer it writes. A
|
||||
// gather is not safe in place: within one pass, dst[y] = src[p(y)] reads
|
||||
// source position p(y) after that position may already hold the value
|
||||
// written at output p(y) (whenever p(y) < y in iteration order). Two
|
||||
// alternating buffers keep read and write distinct across every round.
|
||||
std::vector<uint8_t> scratchA(static_cast<size_t>(n) * n * 4);
|
||||
std::vector<uint8_t> scratchB(static_cast<size_t>(n) * n * 4);
|
||||
|
||||
// Round 1 reads the unpadded W x H source and fills out-of-bounds
|
||||
// preimages with transparent-black padding; later rounds read the fully
|
||||
// padded N x N scratch, where every coordinate is in bounds. The last
|
||||
// round writes directly to `dst`, the N x N canvas.
|
||||
const uint8_t* read = src;
|
||||
int readW = width;
|
||||
int readH = height;
|
||||
for (int i = 0; i < rounds; ++i)
|
||||
{
|
||||
uint8_t* write = (i == rounds - 1) ? dst
|
||||
: (i % 2 == 0 ? scratchA.data() : scratchB.data());
|
||||
const AffineMap inv = inverseMap(offsets[2 * i], offsets[2 * i + 1], n);
|
||||
applyGather(read, readW, readH, write, n, n, n, inv);
|
||||
if (i < rounds - 1)
|
||||
{
|
||||
read = (i % 2 == 0) ? scratchA.data() : scratchB.data();
|
||||
readW = n;
|
||||
readH = n;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool unscramble(const uint8_t* src, uint8_t* dst, int width, int height,
|
||||
const ScrambleConfig& config)
|
||||
{
|
||||
const int n = canvasSize(width, height);
|
||||
if (n == 0 || src == nullptr || dst == nullptr || n > (1 << 30))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const int rounds = std::max(1, config.rounds);
|
||||
const std::vector<int> offsets = deriveOffsets(config.seed, rounds, n);
|
||||
|
||||
// Ping-pong scratch; the same in-place gather hazard as scramble() applies,
|
||||
// so read and write buffers never alias.
|
||||
std::vector<uint8_t> scratchA(static_cast<size_t>(n) * n * 4);
|
||||
std::vector<uint8_t> scratchB(static_cast<size_t>(n) * n * 4);
|
||||
|
||||
// Unscramble applies the forward maps in reverse order: round t first (it
|
||||
// was the last round applied while scrambling), round 1 last. This
|
||||
// reverses the scrambled composition T1^-1 o ... o Tt^-1 by telescoping:
|
||||
// T1^-1( T2^-1( ... Tt^-1( Tt( ... T2( T1(y) ) ... ) ) ... ) ) = y
|
||||
// so the W x H source is recovered exactly in `rounds` passes.
|
||||
// Intermediate rounds write the full N x N scratch; the final round writes
|
||||
// only the W x H output region, since `dst` is tightly packed at W x H.
|
||||
const uint8_t* read = src;
|
||||
for (int i = rounds - 1; i >= 0; --i)
|
||||
{
|
||||
uint8_t* write = (i == 0) ? dst
|
||||
: (i % 2 == 0 ? scratchA.data() : scratchB.data());
|
||||
const int outW = (i == 0) ? width : n;
|
||||
const int outH = (i == 0) ? height : n;
|
||||
const AffineMap fwd = forwardMap(offsets[2 * i], offsets[2 * i + 1]);
|
||||
applyGather(read, n, n, write, outW, outH, n, fwd);
|
||||
if (i > 0)
|
||||
{
|
||||
read = (i % 2 == 0) ? scratchA.data() : scratchB.data();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace scramble
|
||||
} // namespace radiantwave
|
||||
@@ -0,0 +1,191 @@
|
||||
#pragma once
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fibonacci affine image scrambling (RadiantWave scroller text obfuscation)
|
||||
//
|
||||
// This module implements the Fibonacci affine transformation as a reversible
|
||||
// image permutation. The RadiantWave scroller uses it to render proclamation
|
||||
// text as scrambled glyphs: every code point's glyph is permuted by the map
|
||||
// below so letterforms are destroyed and the text is never readable on screen,
|
||||
// while the exact inverse recovers the original glyph given the same key.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// [Published: Williams 2007] -- concepts from the published research
|
||||
//
|
||||
// Ben Williams, "Some properties of digital image scrambling methods using
|
||||
// Fibonacci numbers", MATH491 Summer Research Project 2006-2007, Department
|
||||
// of Mathematics and Statistics, University of Canterbury, 2007 (mirrored in
|
||||
// this repository as algorithm.md).
|
||||
//
|
||||
// Mathematical foundations:
|
||||
// - The Fibonacci matrix (Definition 2.2) is
|
||||
// F = [ 0 1 ]
|
||||
// [ 1 1 ]
|
||||
// so consecutive Fibonacci numbers satisfy u(n+1) = F.u(n). It squares to
|
||||
// the Arnold matrix F^2 = [[1,1],[1,2]] (Remark 3).
|
||||
// - The Fibonacci affine transformation (Definition 4.1) acts on the square
|
||||
// grid Z_m^2 = {0..m-1}^2 with modular arithmetic (Section 3):
|
||||
// T(x) = F.x + b (mod m), b in Z_m^2.
|
||||
// - An affine map x -> A.x + b is a bijection of Z_m^2 iff gcd(det A, m) = 1
|
||||
// (Theorem 3.1 / Corollary 3.2), and it is periodic under the same
|
||||
// condition (Theorem 3.4).
|
||||
// - det F = -1, which is coprime to every m. By Proposition 4.1 the
|
||||
// Fibonacci affine transformation is therefore a permutation of Z_m^2 for
|
||||
// EVERY modulus m: no constraint whatsoever on the grid size.
|
||||
// - The paper decrypts by re-applying T until the period recurs. Section 5
|
||||
// notes the period of F mod m can reach 6m, so that route is slow; the
|
||||
// paper never uses the inverse transform.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// [Adapted beyond the paper] -- generalization from algorithm.md's appendix
|
||||
// (lines 491-500)
|
||||
//
|
||||
// Two extensions turn the published square-only transform into an
|
||||
// arbitrary-size scrambler with a fast, exact round trip:
|
||||
//
|
||||
// 1. Arbitrary image sizes. Any W x H image is embedded in a square canvas
|
||||
// of side
|
||||
// N = canvasSize(W, H) = max(W, H)
|
||||
// and T acts on Z_N^2. Because det F = -1 is invertible for every N, no
|
||||
// Fibonacci-number, power-of-two, or other size constraint applies. (The
|
||||
// size restriction in the paper was specific to Zou's scaled transform,
|
||||
// Definition 4.3, not to the plain Fibonacci affine map.)
|
||||
//
|
||||
// 2. Exact inverse instead of period iteration. Since
|
||||
// F^-1 = [ -1 1 ]
|
||||
// [ 1 0 ]
|
||||
// exists for every modulus, decryption applies the inverse directly and
|
||||
// costs exactly as many rounds as encryption -- not up to 6N applications
|
||||
// of T. This also turns the key into (round count, offset sequence):
|
||||
// scramble applies the per-round offsets b1..bt in order, and unscramble
|
||||
// applies the inverse in reverse order.
|
||||
//
|
||||
// ---------------------------------------------------------------------------
|
||||
// [This implementation] -- project-specific engineering decisions
|
||||
//
|
||||
// Gather-form permutation. Each round is a single gather pass:
|
||||
// scramble: dst[y] = src[preimage(y)], preimage(y) = T^-1(y)
|
||||
// unscramble: dst[y] = src[image(y)], image(y) = T(y)
|
||||
// Every output pixel is produced from exactly one input pixel, so there are
|
||||
// never holes or collisions, and the padding convention is filled directly
|
||||
// where a preimage falls outside the source. (The paper describes the
|
||||
// forward scatter -- source pixel at x moves to T(x); gather is the
|
||||
// equivalent output-directed formulation.)
|
||||
//
|
||||
// Composition semantics. Scrambling applies the inverse maps in round order
|
||||
// (T1^-1, T2^-1, ..., Tt^-1); unscrambling applies the forward maps in
|
||||
// reverse order (Tt, ..., T1). The composition telescopes:
|
||||
// T1^-1( T2^-1( ... Tt^-1( Tt( ... T2( T1(y) ) ... ) ) ... ) ) = y
|
||||
// so the round trip is exact in exactly `rounds` passes, regardless of the
|
||||
// transform's period. Padding is created in round 1 and then permuted like
|
||||
// any other pixel; because the composition is a bijection, each W x H output
|
||||
// position of the final reverse pass maps to a canvas position holding a real
|
||||
// source pixel, so no padding value ever reaches the recovered W x H region.
|
||||
// (Intermediate reverse passes do read padding pixels -- they move the full
|
||||
// N x N canvas -- but they are routed out of the W x H output.)
|
||||
//
|
||||
// Key derivation. The key is the (seed, rounds) pair. Each round derives its
|
||||
// offset vector b = (b0, b1) from `seed` via splitmix64 -- two consecutive
|
||||
// pure-integer draws reduced mod N:
|
||||
// b0 = splitmix64(seed) mod N, b1 = splitmix64(seed) mod N.
|
||||
// Pure integer arithmetic makes the offsets bit-identical on every platform,
|
||||
// so a compile-time seed produces identical scrambling everywhere. The
|
||||
// scroller fixes the seed at compile time (kDefaultSeed), meaning the same
|
||||
// glyph scrambles identically on every device and every run, and no key
|
||||
// state is ever persisted or transmitted.
|
||||
//
|
||||
// Threat model. This is deliberate obfuscation, not encryption: the seed is
|
||||
// compiled into the binary and the algorithm is public, so anyone with the
|
||||
// source and the key can recover the text. The goal is only to stop casual
|
||||
// on-screen reading, not to withstand an adversary. Future work should treat
|
||||
// (seed, rounds) as formatting parameters, not as secrets.
|
||||
//
|
||||
// Padding convention. During scramble the W x H source sits in the natural
|
||||
// top-left embedding of the N x N canvas. Canvas positions whose preimage
|
||||
// falls outside [0, W) x [0, H) are written as transparent black
|
||||
// (0, 0, 0, 0), which is invisible on the scroller's black background.
|
||||
// Because the map is a bijection, the W x H source pixels land on W x H
|
||||
// distinct canvas positions, and the remaining N^2 - W*H positions are
|
||||
// padding. unscramble() reads the whole N x N canvas and writes only the
|
||||
// W x H region, reproducing the source exactly. Display uses the matching
|
||||
// crop-window convention: render the top-left W x H sub-rectangle of the
|
||||
// scrambled canvas.
|
||||
//
|
||||
// Whole pixels. Each RGBA8888 pixel, alpha included, is permuted as a unit,
|
||||
// so a round trip preserves alpha exactly.
|
||||
//
|
||||
// In-place hazard / scratch. A gather pass is not safe in place: within one
|
||||
// pass, dst[y] = src[p(y)] reads source position p(y) after an earlier write
|
||||
// at that same position may have replaced it (whenever p(y) < y in iteration
|
||||
// order). Every pass therefore reads and writes distinct buffers. Two
|
||||
// ping-pong scratch buffers of N*N*4 bytes are reused across rounds, so read
|
||||
// and write never alias while memory stays bounded.
|
||||
//
|
||||
// Numerics. All coordinate products are accumulated in int64_t and reduced
|
||||
// mod N, which is exact for any N whose squares fit comfortably in int64_t
|
||||
// (N <= 2^30). Rounds below 1 are clamped to 1; N == 1 (a 1x1 image) yields
|
||||
// the identity map, which is correct. In practice N is the side of a glyph
|
||||
// tile, at most a few hundred pixels.
|
||||
//
|
||||
// Format & portability. The module is SDL-free and pure C++17: it operates
|
||||
// on tightly packed RGBA8888 pixels (4 bytes per pixel, row stride = width *
|
||||
// 4) and keeps no state between calls. Any caller can adopt it; the scroller
|
||||
// uses it per glyph (see scroller_page.cpp).
|
||||
//
|
||||
// Why CPU. Glyph tiles are tiny and every distinct code point's tile is
|
||||
// scrambled at most once per font size and then cached, so the total cost is
|
||||
// a one-time, amortized O(N^2 * rounds) integer permute per character. A GPU
|
||||
// path would need per-texture compute passes or shader support that buys
|
||||
// nothing here.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace radiantwave {
|
||||
namespace scramble {
|
||||
|
||||
// Fixed compile-time seed so every device renders the same glyph scrambling.
|
||||
// The value is splitmix64's golden-ratio constant; any constant would do, the
|
||||
// requirement is only that it never changes across runs and platforms.
|
||||
inline constexpr uint64_t kDefaultSeed = 0x9E3779B97F4A7C15ULL;
|
||||
|
||||
// Default round count. Each round applies the affine map once; more rounds mix
|
||||
// pixels across a larger fraction of the canvas. Values below 1 are clamped to
|
||||
// 1 by the API.
|
||||
inline constexpr int kDefaultRounds = 4;
|
||||
|
||||
// Per-call configuration: how many rounds and which key (seed).
|
||||
struct ScrambleConfig
|
||||
{
|
||||
int rounds = kDefaultRounds;
|
||||
uint64_t seed = kDefaultSeed;
|
||||
};
|
||||
|
||||
// Square canvas side N for a W x H image: N = max(W, H).
|
||||
// Returns 0 when width or height is < 1 (the scramble/unscramble entry points
|
||||
// reject N == 0).
|
||||
int canvasSize(int width, int height);
|
||||
|
||||
// Scramble a W x H RGBA8888 image (tightly packed, stride = width * 4) into
|
||||
// an N x N RGBA8888 canvas where N = canvasSize(width, height). `dst` must
|
||||
// hold at least N*N*4 bytes. Out-of-canvas source coordinates are written as
|
||||
// transparent black per the padding convention above. `src` and `dst` must not
|
||||
// alias.
|
||||
//
|
||||
// Returns false (writing nothing to `dst`) when width/height < 1, when either
|
||||
// pointer is null, or when N exceeds the int64-safe bound (2^30).
|
||||
bool scramble(const uint8_t* src, uint8_t* dst, int width, int height,
|
||||
const ScrambleConfig& config);
|
||||
|
||||
// Exact inverse of scramble(): reads the N x N RGBA8888 canvas (stride =
|
||||
// N * 4) and writes back the top-left W x H RGBA8888 region (stride = width *
|
||||
// 4). `dst` must hold at least W*H*4 bytes. Must be called with the same
|
||||
// (width, height, config) as the matching scramble() -- the key is the same
|
||||
// tuple. `src` and `dst` must not alias.
|
||||
//
|
||||
// Returns false under the same conditions as scramble().
|
||||
bool unscramble(const uint8_t* src, uint8_t* dst, int width, int height,
|
||||
const ScrambleConfig& config);
|
||||
|
||||
} // namespace scramble
|
||||
} // namespace radiantwave
|
||||
Reference in New Issue
Block a user