Class: Rodauth::Tools::AccountIdCipher
- Inherits:
-
Object
- Object
- Rodauth::Tools::AccountIdCipher
- Defined in:
- lib/rodauth/tools/account_id_cipher.rb
Overview
Keyed format-preserving obfuscator for integer primary keys.
Turns a numeric account id (e.g. 2) into a fixed-width, URL-safe,
non-sequential token (e.g. "E946V4SD7Z7RV") and back again, WITHOUT
changing the database schema. The mapping is a keyed pseudo-random
permutation (a bijection) over the 64-bit domain, so:
- every id maps to exactly one token and vice-versa (no collisions),
- the token reveals nothing about the id or about neighbouring ids to anyone who does not hold the secret,
decode(encode(id)) == idfor all ids in range, anddecoderejects any well-formed-but-non-canonical token (see #decode).
It is a 4-round Feistel network keyed with HMAC-SHA256. This is genuine keyed encryption over a small domain (format-preserving encryption), not a reversible "encoder" like Hashids/Sqids: without the secret the permutation is hard to invert and, for a bounded number of observed tokens, hard to distinguish from random. As with any small-block Feistel construction, that only holds up to the birthday bound of the 32-bit half-block (roughly 2^16 tokens observed under one secret) — it is not an unconditional guarantee at unbounded query volume.
The output alphabet is Crockford Base32, which deliberately excludes the
_ character so encoded ids never collide with Rodauth's token_separator.
This class is a pure Integer <-> 13-char bijection: it knows nothing
about versions, prefixes or legacy formats. The account_id_obfuscation
feature adds a one-character non-digit version prefix on top (which is what
makes obfuscated tokens deterministically distinguishable from legacy
decimal ids and drives key rotation), keeping this primitive reusable and
trivially testable in isolation.
Constant Summary collapse
- MASK32 =
0xFFFF_FFFF- MASK64 =
0xFFFF_FFFF_FFFF_FFFF- ROUNDS =
4- ALPHABET =
Crockford Base32 (no I, L, O, U; no underscore). 13 chars encodes 65 bits, covering the full 64-bit domain with a fixed, padding-free width.
'0123456789ABCDEFGHJKMNPQRSTVWXYZ'- WIDTH =
13- MIN_SECRET_BYTES =
Minimum secret length. HMAC-SHA256's block size is 64 bytes; 32 bytes (256 bits) is the smallest key we accept as cryptographically adequate.
32
Instance Method Summary collapse
-
#decode(token) ⇒ Integer?
Decode an obfuscated token back into its integer id.
-
#encode(id) ⇒ String
Encode an integer id into its fixed-width obfuscated token.
-
#initialize(secret) ⇒ AccountIdCipher
constructor
A new instance of AccountIdCipher.
Constructor Details
#initialize(secret) ⇒ AccountIdCipher
Returns a new instance of AccountIdCipher.
64 65 66 67 68 69 |
# File 'lib/rodauth/tools/account_id_cipher.rb', line 64 def initialize(secret) secret = secret.to_s raise ArgumentError, "secret must be at least #{MIN_SECRET_BYTES} bytes" if secret.bytesize < MIN_SECRET_BYTES @secret = secret end |
Instance Method Details
#decode(token) ⇒ Integer?
Decode an obfuscated token back into its integer id.
Returns nil for anything that is not a well-formed 13-char token (wrong
length, illegal character, non-string), so callers can pass foreign input
through without raising. Also returns nil for a well-formed-but-
non-canonical token: 13 Crockford chars encode 65 bits for a 64-bit
block, so each id has exactly one canonical 13-char token but a second,
non-canonical encoding that differs only in the (discarded) 65th bit of
its first character. Re-encoding the decrypted id and comparing against
the input rejects that non-canonical form, keeping #decode a strict
inverse of #encode rather than a 2-to-1 mapping.
97 98 99 100 101 102 103 |
# File 'lib/rodauth/tools/account_id_cipher.rb', line 97 def decode(token) number = unbase32(token) return nil unless number id = feistel(number, :decrypt) encode(id) == token ? id : nil end |
#encode(id) ⇒ String
Encode an integer id into its fixed-width obfuscated token.
78 79 80 |
# File 'lib/rodauth/tools/account_id_cipher.rb', line 78 def encode(id) base32(feistel(Integer(id) & MASK64, :encrypt)) end |