<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lucas Olopes</title>
    <description>The latest articles on DEV Community by Lucas Olopes (@lucasolopes).</description>
    <link>https://dev.to/lucasolopes</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4061437%2Fe22fd987-8027-4348-bd35-1f7261b7adce.png</url>
      <title>DEV Community: Lucas Olopes</title>
      <link>https://dev.to/lucasolopes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucasolopes"/>
    <language>en</language>
    <item>
      <title>arxid: keyed, non-enumerable ID obfuscation with a measured ARX-Feistel permutation</title>
      <dc:creator>Lucas Olopes</dc:creator>
      <pubDate>Mon, 03 Aug 2026 23:15:45 +0000</pubDate>
      <link>https://dev.to/lucasolopes/arxid-keyed-non-enumerable-id-obfuscation-with-a-measured-arx-feistel-permutation-3cia</link>
      <guid>https://dev.to/lucasolopes/arxid-keyed-non-enumerable-id-obfuscation-with-a-measured-arx-feistel-permutation-3cia</guid>
      <description>&lt;p&gt;If your API exposes &lt;code&gt;/users/1042&lt;/code&gt;, someone can request &lt;code&gt;/users/1043&lt;/code&gt;. And &lt;code&gt;/users/1044&lt;/code&gt;. Sequential integer IDs leak two things you probably didn't mean to leak: roughly how many records you have, and a trivial way to walk through all of them.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;arxid&lt;/code&gt; fixes that. Feed it an integer, get back an unpredictable-looking code; feed the code back with the same key, get the integer. Keyed, reversible, and byte-for-byte identical across every language it's ported to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Rust&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Arxid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="nf"&gt;.obfuscate_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// -&amp;gt; "7ZjcH7o"&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="nf"&gt;.deobfuscate_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// -&amp;gt; Some(1042)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// TypeScript&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Arxid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;              &lt;span class="c1"&gt;// key is a bigint&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;obfuscateStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1042&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// -&amp;gt; "7ZjcH7o"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deobfuscateStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// -&amp;gt; 1042 | null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Rust&lt;/span&gt;
cargo add arxid

&lt;span class="c"&gt;# TypeScript / JavaScript&lt;/span&gt;
npm i arxid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;arxid&lt;/code&gt; is a balanced Feistel network with an ARX (add-rotate-xor) round function, over a 40-bit domain that maps to a 7-character base62 code.&lt;/p&gt;

&lt;p&gt;A Feistel network is a bijection by construction. Split the input into two halves, and for each round, replace one half with itself XORed against some function of the other half. It doesn't matter whether that function is invertible: you can always run the network backward, because Feistel decryption reuses the same round function in reverse order. So &lt;code&gt;decode(encode(id)) == id&lt;/code&gt; for every id, always, with zero collision checks and no lookup table anywhere.&lt;/p&gt;

&lt;p&gt;The round function is ARX: three cheap integer operations, addition (mod 2^32), bit rotation, and XOR. No S-boxes, no hash calls, no lookup tables. SPECK and other lightweight ciphers use the same primitives. Each round is a handful of CPU instructions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;subkey&lt;/span&gt;        &lt;span class="c1"&gt;// wrapping add, u32&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="nf"&gt;rotl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;rotl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// wrapping add, u32&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="nf"&gt;rotl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four rounds of that. The result is a permutation that runs at roughly &lt;strong&gt;225M ops/sec&lt;/strong&gt; for the raw integer transform, about &lt;strong&gt;16x&lt;/strong&gt; faster than a structurally identical Feistel using HMAC-SHA256 as its round function. Same security shape, one round function swapped, an order of magnitude faster, because ARX rounds are integer ops instead of hash calls.&lt;/p&gt;

&lt;p&gt;These numbers come from &lt;strong&gt;a 13th Gen Intel Core i7-1355U, 16 GB RAM, Windows 11 Pro&lt;/strong&gt;, measured with criterion, single-threaded, reproducible with &lt;code&gt;cargo bench&lt;/code&gt;. The ratio against the HMAC-Feistel matters more than the absolute figure: both run on the same machine in the same harness, so the ~16x gap holds regardless of how fast your CPU is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the round count is 4, and how it was chosen
&lt;/h2&gt;

&lt;p&gt;The round count isn't a guess. It's calibrated with a harness that measures the Strict Avalanche Criterion: flip one bit of the input, and on a well-mixed permutation about half the output bits should flip, unpredictably. If flipping input bit 5 always flips the same 3 output bits, the code is enumerable. If it flips ~50% of them no matter which input bit you touch, the output looks like noise from outside.&lt;/p&gt;

&lt;p&gt;Sweeping 1 to 12 rounds over 200,000 samples each, the average avalanche hits 0.5001 at 4 rounds with full bit-coverage: every input bit can influence every output bit. Rounds beyond 4 buy nothing on that metric. So the count is fixed at 4, and frozen in the spec.&lt;/p&gt;

&lt;p&gt;One honest caveat, stated in the README and the security docs rather than buried: 4 rounds closes the &lt;em&gt;average&lt;/em&gt; avalanche, but the worst individual bit-pair is still 0.164 from ideal at 4 rounds and only fully closes at 5. The README's avalanche table has a dedicated "worst single pair" column showing that gap. Four rounds is a diffusion target calibrated for statistical non-enumerability, not a cryptographic safety margin. Which is exactly what &lt;code&gt;arxid&lt;/code&gt; is for, and exactly what it isn't (more below).&lt;/p&gt;

&lt;h2&gt;
  
  
  Same output in every language
&lt;/h2&gt;

&lt;p&gt;The point of a library like this is that a code obfuscated in one language decodes identically in another. You store an obfuscated ID somewhere, and the service that reads it might be written in something else entirely. That only works if every implementation produces byte-identical output.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;arxid&lt;/code&gt; guarantees it spec-first: a frozen specification pinning every parameter down to the byte (width, rounds, ARX constants, key schedule, endianness, the base62 alphabet and its ordering, padding), a reference implementation in Rust, and 61 canonical test vectors that act as the contract. Every port validates against the same vectors. Pass the vectors, and you're interoperable with every other implementation by definition. This is how Sqids reached more than 50 language implementations: one consistent spec, not one shared binary.&lt;/p&gt;

&lt;p&gt;The Rust reference is &lt;code&gt;no_std&lt;/code&gt;-capable and forbids unsafe. The TypeScript port is ESM with zero runtime dependencies. One detail worth knowing if you port it yourself: the 40-bit domain fits safely in a JavaScript &lt;code&gt;number&lt;/code&gt;, but the key schedule operates on 64 bits, so the subkey derivation needs &lt;code&gt;BigInt&lt;/code&gt; and a careful narrowing back to 32 bits. Get it wrong and the round-trip still passes locally while the output silently diverges. The vectors catch it: one vector uses a key with only the high bit set, so a port that truncates the key to 32 bits reads zero and fails loudly.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use it, and when not
&lt;/h2&gt;

&lt;p&gt;If you need multiple numbers bundled into one ID, an unbounded domain, or a profanity blocklist, use Sqids. It's mature, it has an ecosystem, and those are real features &lt;code&gt;arxid&lt;/code&gt; doesn't have.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;arxid&lt;/code&gt; when you want a keyed permutation with a real key schedule instead of a shuffled alphabet, over a fixed integer domain, that runs fast and produces the same output in every language.&lt;/p&gt;

&lt;p&gt;And use it knowing what it is. &lt;code&gt;arxid&lt;/code&gt; defeats casual enumeration. It is not encryption, it is not a MAC, and it has not been independently audited. Obfuscation is not access control: if a resource must stay secret, put real authorization in front of it. The obfuscated ID is a speed bump against enumeration, not a lock.&lt;/p&gt;

&lt;p&gt;The code, the spec, and the vectors are on GitHub: &lt;a href="https://github.com/lucasolopes/arxid" rel="noopener noreferrer"&gt;github.com/lucasolopes/arxid&lt;/a&gt;. Ports to other languages are the next step, and the contribution path is simple: implement the spec, pass the vectors, open a PR.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>typescript</category>
      <category>cryptography</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
