<?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: nefu-dev</title>
    <description>The latest articles on DEV Community by nefu-dev (@nefu-dev).</description>
    <link>https://dev.to/nefu-dev</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%2F4157253%2Fe07d4120-8db3-4399-b2d4-0cdf481b3d0a.png</url>
      <title>DEV Community: nefu-dev</title>
      <link>https://dev.to/nefu-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nefu-dev"/>
    <language>en</language>
    <item>
      <title>Obfuscating AES Keys in Self-Contained Rust Desktop Apps</title>
      <dc:creator>nefu-dev</dc:creator>
      <pubDate>Wed, 07 Oct 2026 11:17:59 +0000</pubDate>
      <link>https://dev.to/nefu-dev/obfuscating-aes-keys-in-self-contained-rust-desktop-apps-1nmm</link>
      <guid>https://dev.to/nefu-dev/obfuscating-aes-keys-in-self-contained-rust-desktop-apps-1nmm</guid>
      <description>&lt;h2&gt;
  
  
  Obfuscating AES Keys in Self-Contained Rust Desktop Apps
&lt;/h2&gt;

&lt;p&gt;If you ship a desktop app that bundles encrypted resources (a game, an Electron-style web app, a document pack), you have probably hit this wall:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The key has to live in the binary, because the binary has to decrypt its own data at runtime.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So most of us just do the naive thing. And then someone runs &lt;code&gt;strings your.exe | grep -i key&lt;/code&gt;, or opens a hex editor, and the whole "encryption" collapses into a very expensive ZIP.&lt;/p&gt;

&lt;p&gt;This post shows a cheap, dependency-light upgrade: instead of storing the raw AES-256 key (and nonce) in the file, store a &lt;strong&gt;seed&lt;/strong&gt;, derive an &lt;strong&gt;XOR mask&lt;/strong&gt; from it with a deterministic PRNG, and only persist the masked bytes. The real key never appears in the packaged artifact.&lt;/p&gt;

&lt;p&gt;I implemented this in &lt;a href="https://github.com/nefu-dev/nefu" rel="noopener noreferrer"&gt;Nefu&lt;/a&gt;, a Rust tool that packages web projects (HTML/CSS/JS, plus a custom &lt;code&gt;.nc&lt;/code&gt; declarative UI language) into single-file desktop executables. All code below is real, tested code from the repo.&lt;/p&gt;




&lt;h2&gt;
  
  
  The threat model (be honest first)
&lt;/h2&gt;

&lt;p&gt;Let me be extremely clear about what this does and does not buy you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Stops the &lt;strong&gt;casual&lt;/strong&gt; extraction path: &lt;code&gt;strings&lt;/code&gt;, hexdump + copy-paste, "just grep for 32 random bytes".&lt;/li&gt;
&lt;li&gt;✅ Raises the bar enough that a curious user gives up.&lt;/li&gt;
&lt;li&gt;❌ Does &lt;strong&gt;NOT&lt;/strong&gt; stop a determined reverse engineer. Anyone with a debugger can dump the key from memory at runtime, or trace the AES round keys. True protection against that is white-box cryptography, which is an order of magnitude heavier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are building DRM, stop reading and go look at white-box AES or a licensing service. If you are building "I don't want my game's assets to be trivially unpackable," this is a sweet spot: ~60 lines of code, zero new dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The old layout (the problem)
&lt;/h2&gt;

&lt;p&gt;Nefu packages resources like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Original executable]
[AES-256-GCM encrypted ZIP]
[8 bytes: encrypted length (u64 LE)]
[32 bytes: SHA-256 checksum]
[8 bytes: magic "NEFUPACK"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the encrypted payload was assembled as:&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;// build_executable, before&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;encrypted_package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_capacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AES_KEY_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;NONCE_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&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;aes_key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// &amp;lt;-- raw key, in the file&lt;/span&gt;
&lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&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;nonce_bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// &amp;lt;-- raw nonce, in the file&lt;/span&gt;
&lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&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;ciphertext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a 32-byte AES key sitting in the binary &lt;strong&gt;in plaintext&lt;/strong&gt;, at a predictable offset right after the payload marker. Anybody can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the &lt;code&gt;NEFUPACK&lt;/code&gt; magic at the end of the file.&lt;/li&gt;
&lt;li&gt;Walk backwards to the payload start.&lt;/li&gt;
&lt;li&gt;Read 32 bytes of key, 12 bytes of nonce.&lt;/li&gt;
&lt;li&gt;Decrypt with 5 lines of Python.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;AES-256-GCM&lt;/code&gt; is doing all the real work, and we hand the attacker the key on a silver platter.&lt;/p&gt;




&lt;h2&gt;
  
  
  The obfuscation scheme
&lt;/h2&gt;

&lt;p&gt;The idea is boring and effective:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Generate the real random key + nonce as before.&lt;/li&gt;
&lt;li&gt;Also generate a random 16-byte &lt;strong&gt;obfuscation seed&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Derive an XOR mask from the seed using a &lt;strong&gt;deterministic PRNG&lt;/strong&gt;, with a domain separator so the key mask and nonce mask are different streams.&lt;/li&gt;
&lt;li&gt;Store &lt;code&gt;masked_key = key XOR mask&lt;/code&gt; and &lt;code&gt;masked_nonce = nonce XOR mask'&lt;/code&gt; instead of the originals.&lt;/li&gt;
&lt;li&gt;At runtime, re-derive the same mask from the seed (same seed + same PRNG = same mask) and XOR back.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The PRNG I used is &lt;strong&gt;SplitMix64&lt;/strong&gt; blended with two auxiliary mixing streams — an xorshift64* variant and a counter-fed avalanche PRNG. Three streams make the output look properly chaotic and make static analysis slightly less pleasant, at zero cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  The mask generator
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;generate_obfuscation_mask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Domain-separated seed diffusion&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;hasher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Sha256&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;hasher&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hasher&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"::"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hasher&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;hasher&lt;/span&gt;&lt;span class="nf"&gt;.update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"::nefu-obf-v1"&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;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hasher&lt;/span&gt;&lt;span class="nf"&gt;.finalize&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_le_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.try_into&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_le_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.try_into&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_le_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nf"&gt;.try_into&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_capacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// SplitMix64 main generator&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0x9E37_79B9_7F4A_7C15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0xBF58_476D_1CE4_E5B9&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0x94D0_49BB_1331_11EB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Auxiliary mixer: xorshift64* variant&lt;/span&gt;
        &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;mixer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mixer&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0x2545_F491_4F6C_DD1D&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Third stream: counter-fed avalanche PRNG&lt;/span&gt;
        &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0x9E37_79B9_7F4A_7C15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.wrapping_mul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0xFF51_AFD7_ED55_8CCD&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;^=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;33&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;blended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;mixer&lt;/span&gt;&lt;span class="nf"&gt;.rotate_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="nf"&gt;.rotate_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&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;blended&lt;/span&gt;&lt;span class="nf"&gt;.to_le_bytes&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="nf"&gt;.truncate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;mask&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The seed goes through SHA-256 with a domain separator before any PRNG state is derived, so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;same&lt;/strong&gt; &lt;code&gt;(seed, domain, length)&lt;/code&gt; always produces the &lt;strong&gt;same&lt;/strong&gt; mask (that is the whole point — packer and unpacker agree),&lt;/li&gt;
&lt;li&gt;different domains produce uncorrelated streams (key mask ≠ nonce mask),&lt;/li&gt;
&lt;li&gt;different seeds produce totally different masks.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Obfuscate / deobfuscate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;obfuscate_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_obfuscation_mask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.zip&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;mask&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.map&lt;/span&gt;&lt;span class="p"&gt;(|(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)|&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;^&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;deobfuscate_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;obfuscated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;obfuscate_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obfuscated&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;XOR is its own inverse, so one function serves both directions. (Yes, I keep both names in the codebase for readability — the compiler inlines them anyway.)&lt;/p&gt;




&lt;h2&gt;
  
  
  The new payload layout
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[key 32B][nonce 12B][ciphertext ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[OBS1][seed 16B][masked_key 32B][masked_nonce 12B][ciphertext ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;OBS1&lt;/code&gt; is a 4-byte format marker. It does two jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Lets &lt;code&gt;unpack&lt;/code&gt; detect which layout it is looking at.&lt;/li&gt;
&lt;li&gt;Gives us a cheap future migration path (&lt;code&gt;OBS2&lt;/code&gt;, ...).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Packing side
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Step 2: generate a random key, nonce and obfuscation seed&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;thread_rng&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;aes_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;AES_KEY_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;nonce_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;NONCE_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;obf_seed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;OBF_SEED_LEN&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="nf"&gt;.fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;aes_key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="nf"&gt;.fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;nonce_bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="nf"&gt;.fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;obf_seed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 2b: obfuscate key and nonce so the raw bytes never hit the file&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;obf_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;obfuscate_secret&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;obf_seed&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;aes_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;b"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;obf_nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;obfuscate_secret&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;obf_seed&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;nonce_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;b"nonce"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Step 3: AES-256-GCM encrypt&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Aes256Gcm&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_from_slice&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;aes_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&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;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Nonce&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_slice&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;nonce_bytes&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;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cipher&lt;/span&gt;&lt;span class="nf"&gt;.encrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;zip_data&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Assemble: [OBS1][seed][obf_key][obf_nonce][ciphertext]&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;encrypted_package&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;with_capacity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;FORMAT_MARKER&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;OBF_SEED_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AES_KEY_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;NONCE_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT_MARKER&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&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;obf_seed&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&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;obf_key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&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;obf_nonce&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="nf"&gt;.extend_from_slice&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;ciphertext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypted_package&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Unpacking side
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;encrypted_data&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;b"PK&lt;/span&gt;&lt;span class="se"&gt;\x03\x04&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// --no-encrypt debug artifact: plaintext ZIP, skip decryption&lt;/span&gt;
    &lt;span class="n"&gt;decompressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypted_data&lt;/span&gt;&lt;span class="nf"&gt;.to_vec&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;encrypted_data&lt;/span&gt;&lt;span class="nf"&gt;.starts_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FORMAT_MARKER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ---- v1 obfuscated layout ----&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;encrypted_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FORMAT_MARKER&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;..&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;obf_seed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;OBF_SEED_LEN&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;obf_key&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OBF_SEED_LEN&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;OBF_SEED_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AES_KEY_LEN&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;obf_nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OBF_SEED_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AES_KEY_LEN&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;OBF_SEED_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AES_KEY_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;NONCE_LEN&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;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;OBF_SEED_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;AES_KEY_LEN&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;NONCE_LEN&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="c1"&gt;// Recover the real key and nonce from the masked forms&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deobfuscate_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obf_seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obf_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;b"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;nonce_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deobfuscate_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obf_seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obf_nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;b"nonce"&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;cipher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Aes256Gcm&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new_from_slice&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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&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;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Nonce&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_slice&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;nonce_bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;decompressed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cipher&lt;/span&gt;&lt;span class="nf"&gt;.decrypt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ---- legacy layout (backward compatibility) ----&lt;/span&gt;
    &lt;span class="c1"&gt;// [key 32B][nonce 12B][ciphertext ...] — still supported&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the &lt;code&gt;PK\x03\x04&lt;/code&gt; check first: &lt;code&gt;--no-encrypt&lt;/code&gt; debug builds ship a plain ZIP, and that path is unchanged.&lt;/p&gt;




&lt;h2&gt;
  
  
  Backward compatibility
&lt;/h2&gt;

&lt;p&gt;This was the sneaky part. Existing users already shipped executables with the legacy &lt;code&gt;[key][nonce][ciphertext]&lt;/code&gt; layout. The new binary must still run those.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;OBS1&lt;/code&gt; marker makes this trivial — the unpacker branches on the first 4 bytes. The legacy branch is byte-for-byte the old code, and I added a regression test that hand-assembles a legacy payload (with a &lt;em&gt;real&lt;/em&gt; ZIP inside) and asserts it unpacks correctly.&lt;/p&gt;

&lt;p&gt;One caveat: I didn't bump the file magic, so &lt;code&gt;verify&lt;/code&gt;/&lt;code&gt;extract&lt;/code&gt; metadata tooling needed no changes. The trailer (&lt;code&gt;[len][checksum][magic]&lt;/code&gt;) is untouched.&lt;/p&gt;




&lt;h2&gt;
  
  
  The test suite
&lt;/h2&gt;

&lt;p&gt;Because this is security-adjacent code, I treated tests as part of the deliverable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_obfuscation_mask_deterministic        // same seed+domain =&amp;gt; same mask
test_obfuscation_mask_domain_separated     // "key" mask != "nonce" mask
test_obfuscation_mask_seed_sensitive       // different seed =&amp;gt; different mask
test_obfuscate_deobfuscate_roundtrip       // masked -&amp;gt; unmasked == original
test_unpack_legacy_layout_compatibility    // old binaries still unpack
test_build_then_unpack_obfuscated_key_not_plaintext // OBS1 marker + full roundtrip
test_build_then_unpack_roundtrip           // existing e2e regression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full suite: &lt;strong&gt;190 tests, all passing&lt;/strong&gt; (including these plus the pre-existing global shortcut, hot-reload injection, and packaging roundtrip tests).&lt;/p&gt;

&lt;p&gt;Two of those tests caught real issues during this change:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The legacy-layout test initially used a &lt;em&gt;fake&lt;/em&gt; ZIP body and failed on &lt;code&gt;failed to open ZIP archive&lt;/code&gt; — the test itself was wrong, not the code. Fixing it to encrypt a real ZIP made the compatibility guarantee actually meaningful.&lt;/li&gt;
&lt;li&gt;A pre-existing test in &lt;code&gt;global_shortcut.rs&lt;/code&gt; called &lt;code&gt;hotkey.modifiers()&lt;/code&gt;, which doesn't exist in &lt;code&gt;global-hotkey 0.6.4&lt;/code&gt; (the API moved to &lt;code&gt;matches()&lt;/code&gt;/&lt;code&gt;into_string()&lt;/code&gt;). It had been silently broken because &lt;code&gt;cargo test&lt;/code&gt; was never green on this crate. I fixed it so the suite is actually runnable and green.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Honest limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Memory dumping beats everything.&lt;/strong&gt; Attach a debugger, dump the process, find the AES key in the heap. Obfuscation-at-rest cannot prevent this. If you need protection against live extraction, you need white-box crypto and/or an external licensing service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The seed is in the binary.&lt;/strong&gt; The scheme is &lt;em&gt;obfuscation&lt;/em&gt;, not &lt;em&gt;hiding&lt;/em&gt;. A determined analyst will find the PRNG, reproduce the mask, and decrypt. We are raising the bar, not eliminating the problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't overclaim to your users.&lt;/strong&gt; "Encrypted and protected" is true against casual inspection; against a skilled reverser it is "mildly annoying."&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;If you embed a key next to the data it decrypts, you have a ZIP, not encryption. At minimum, XOR-mask it with a derived stream so it isn't sitting there in cleartext.&lt;/li&gt;
&lt;li&gt;Deterministic PRNG + domain separator is a zero-dependency, testable way to do this. SplitMix64 is 15 lines and good enough for mask generation (it's not being used as a cryptographic primitive — AES-GCM still provides the actual confidentiality).&lt;/li&gt;
&lt;li&gt;Format markers (&lt;code&gt;OBS1&lt;/code&gt;) make schema migration painless and keep backward compatibility testable.&lt;/li&gt;
&lt;li&gt;Write the tests &lt;em&gt;for the security property you actually care about&lt;/em&gt;: "the raw key never appears in the artifact" is a testable assertion, and "old artifacts still unpack" is too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full implementation lives in the &lt;a href="https://github.com/nefu-dev/nefu" rel="noopener noreferrer"&gt;Nefu repository&lt;/a&gt; (&lt;code&gt;src/pack.rs&lt;/code&gt;), with the complete test suite. If you ship self-contained desktop apps with bundled resources, stealing this ~60-line pattern is a strict upgrade over shipping plaintext keys.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>security</category>
      <category>encryption</category>
      <category>reverseengineering</category>
    </item>
    <item>
      <title>nefuOS: A from-scratch OS with a browser, WebGL, and a local LLM — built in one month</title>
      <dc:creator>nefu-dev</dc:creator>
      <pubDate>Fri, 02 Oct 2026 11:40:17 +0000</pubDate>
      <link>https://dev.to/nefu-dev/nefuos-a-from-scratch-os-with-a-browser-webgl-and-a-local-llm-built-in-one-month-l1b</link>
      <guid>https://dev.to/nefu-dev/nefuos-a-from-scratch-os-with-a-browser-webgl-and-a-local-llm-built-in-one-month-l1b</guid>
      <description>&lt;p&gt;One month ago, I saw someone's hobby OS and thought: &lt;em&gt;"I want to build one too."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One month later, nefuOS boots from a CD, switches into long mode, draws a browser window in software, compiles C++, installs packages, and answers questions with a local LLM. All of it written from scratch — one dev, one AI, one month.&lt;/p&gt;

&lt;p&gt;This post is the tour. I'll skip the "it's a hobby OS" humility and just show you how it's built.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;nefuOS is a full OS stack written from scratch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ncore&lt;/strong&gt; — custom kernel, boots via both BIOS (El Torito) and UEFI (GRUB multiboot2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nbrower&lt;/strong&gt; — custom browser with HTML/CSS/JS/WebGL2, own TLS 1.3 stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nefucpp&lt;/strong&gt; — C++ → .nefud compiler&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NPKG&lt;/strong&gt; — package manager over HTTPS, installs &lt;code&gt;.deb&lt;/code&gt;-style packages&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nshell&lt;/strong&gt; — 60+ Unix-like commands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ngame&lt;/strong&gt; — 34 built-in games&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NAI&lt;/strong&gt; — a fine-tuned Qwen2.5-1.5B that actually calls tools (writes files, runs commands)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NSAI&lt;/strong&gt; — a TinyStories-33M model running on bare metal: 24 tokens in ~90 seconds. Painfully slow, very much alive.&lt;/li&gt;
&lt;li&gt;Chinese IME, TTF rendering, 107 built-in apps&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The boot chain: from CD to long mode
&lt;/h2&gt;

&lt;p&gt;This is the part most people skip, and it's where the fun hides. The BIOS boot path is a real-mode assembly boot sector that loads the kernel from an El Torito no-emulation CD (2048-byte sectors, &lt;code&gt;DL=0xE0&lt;/code&gt;, real ATAPI via &lt;code&gt;int 13h AH=42h&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The loader can only reach about 512KB of contiguous real-mode memory (0x20000–0xA0000, just below the VGA/ROM hole) — but the kernel payload is over a megabyte. So it uses a &lt;strong&gt;ping-pong loader&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load a 512KB window from the CD into 0x20000.&lt;/li&gt;
&lt;li&gt;Switch to protected mode, let a small stub copy the window to a 0x300000 staging area.&lt;/li&gt;
&lt;li&gt;Return to real mode, reset the window, keep loading.&lt;/li&gt;
&lt;li&gt;On the final chunk, the stub inflates the kernel to 0x100000 and hands off to the long-mode entry point.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Boot sector state lives in a "safe zone" (0x7C00–0x7DAA) because BIOS &lt;code&gt;int 10h/13h&lt;/code&gt; clobbers the tail of the sector. Two separate DAPs — one for the boot menu, one for the payload — keep the menu round-trip from corrupting the loader state. A direct-register VBE setup (Bochs registers 0x1CE/0x1CF) gets a true 32bpp linear framebuffer on QEMU, where the BIOS VBE modes would otherwise hand you an unusable 24bpp layout.&lt;/p&gt;

&lt;p&gt;The UEFI path is a completely different beast — GRUB multiboot2, flat binary, same kernel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nbrower: a browser that actually renders
&lt;/h2&gt;

&lt;p&gt;Nbrower is the crown jewel. It does HTML/CSS/JS rendering, WebGL2, its own TLS 1.3 stack, a Chinese IME, and TTF font rendering — all inside the kernel.&lt;/p&gt;

&lt;p&gt;JavaScript runs on a QuickJS integration that lives in the kernel itself. The runtime is shared across &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; blocks (global state persists, like a real browser), with a 16MB memory limit and a 256KB stack cap tuned for bare metal (no per-thread guard pages). Script output and browser control share one protocol: DOM commands like &lt;code&gt;\x01setText|id|value\n&lt;/code&gt; get parsed by the browser and re-layout the page.&lt;/p&gt;

&lt;p&gt;WebGL is bridged the same way: JS-side GL calls are URL-encoded into control lines (&lt;code&gt;\x01gl&amp;lt;sub&amp;gt;|&amp;lt;cid&amp;gt;|&amp;lt;args&amp;gt;\n&lt;/code&gt;) and executed by the host on a hidden WGL context. Behind that bridge sits &lt;strong&gt;SWGL&lt;/strong&gt; — 9000 lines of software GL that run WebGL2 with no GPU driver at all.&lt;/p&gt;

&lt;p&gt;The old QuickJS 2021 build had a nasty bug: any second &lt;code&gt;JS_Eval&lt;/code&gt; in the same runtime threw a message-less exception. That's fixed now — the kernel runs on &lt;strong&gt;quickjs-ng&lt;/strong&gt;, stable and crash-free on bare metal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The toolchain: nefucpp &amp;amp; NPKG
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;nefucpp&lt;/strong&gt; is a C++ → .nefud compiler pipeline, so you can build native programs inside the OS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NPKG&lt;/strong&gt; fetches &lt;code&gt;.deb&lt;/code&gt;-style packages over HTTPS, with the browser's TLS stack doing the heavy lifting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Nshell, Ngame, and friends
&lt;/h2&gt;

&lt;p&gt;60+ Unix-like commands, 34 built-in games, and 107 built-in apps. The OS is meant to be &lt;em&gt;used&lt;/em&gt;, not just booted.&lt;/p&gt;

&lt;h2&gt;
  
  
  NAI &amp;amp; NSAI: two AIs, two personalities
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;NAI&lt;/strong&gt; is a fine-tuned Qwen2.5-1.5B running locally — and it's not just a chat box, it can call tools: write files, run commands. Real agent behavior inside a from-scratch OS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NSAI&lt;/strong&gt; is the other one: a TinyStories-33M model that runs directly on the bare-metal kernel. It generates 24 tokens in about 90 seconds. That's not a typo — 24 tokens, 90 seconds. It's brutally slow, and it's the most alive thing in the system: a neural network running on an OS written from scratch, no framework, no GPU, just raw CPU grinding out a story one token at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The workflow: one dev + AI
&lt;/h2&gt;

&lt;p&gt;This was the interesting part. I hand-wrote the first ~120k lines — kernel, VFS, IPC, the core architecture. Then I let AI fill in implementation details, about 80% of the rest.&lt;/p&gt;

&lt;p&gt;Architecture, decisions, and debugging are mine. AI just made the typing faster.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Self-developed: ~300k lines (mostly C/C++, plus Python tooling)&lt;/li&gt;
&lt;li&gt;With third-party: ~1.04M lines&lt;/li&gt;
&lt;li&gt;One month&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The hard parts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;BIOS + UEFI dual boot.&lt;/strong&gt; Two completely different boot protocols, one kernel, one codebase. The BIOS path is real-mode assembly with all the segment/offset pain; the UEFI path is multiboot2 with flat addressing. Getting both to land on the same long-mode entry took the longest of anything in the project.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLS 1.3 from scratch.&lt;/strong&gt; The browser talks to real sites, which means a real TLS handshake, real record layer, real certificate handling — implemented from the ground up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;QuickJS on bare metal.&lt;/strong&gt; Getting a JS engine into a kernel means no guard pages, no malloc assumptions, and a runtime that can't blow up your address space. The 2021 build's second-&lt;code&gt;JS_Eval&lt;/code&gt; crash forced a migration to quickjs-ng — now it runs all day without a hiccup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SWGL.&lt;/strong&gt; 9000 lines of software GL to run WebGL2 without a GPU driver. The canvas pipeline is what makes bloxd.io (a real 3D web game) almost render.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;bloxd.io WebGL rendering (canvas pipeline is almost there)&lt;/li&gt;
&lt;li&gt;Better CSS support (flexbox, descendant selectors)&lt;/li&gt;
&lt;li&gt;DeepSeek login fully wired into the browser&lt;/li&gt;
&lt;li&gt;Browser input widgets — just finished testing native range sliders across negative ranges, step values, and initial positions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/Nefu-Dev/nefuOS" rel="noopener noreferrer"&gt;https://github.com/Nefu-Dev/nefuOS&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the architecture, the AI-assisted workflow, or the dual-backend design.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>programming</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
