<?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: Luan Silveira Macea</title>
    <description>The latest articles on DEV Community by Luan Silveira Macea (@luanmacea).</description>
    <link>https://dev.to/luanmacea</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%2F4152736%2F6ff52d91-b4ea-4553-ba74-0dbaff581cca.jpg</url>
      <title>DEV Community: Luan Silveira Macea</title>
      <link>https://dev.to/luanmacea</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/luanmacea"/>
    <language>en</language>
    <item>
      <title>How a crypto library got my Tauri app flagged as a trojan (and how I found it by bisection)</title>
      <dc:creator>Luan Silveira Macea</dc:creator>
      <pubDate>Wed, 30 Sep 2026 16:57:50 +0000</pubDate>
      <link>https://dev.to/luanmacea/how-a-crypto-library-got-my-tauri-app-flagged-as-a-trojan-and-how-i-found-it-by-bisection-11fl</link>
      <guid>https://dev.to/luanmacea/how-a-crypto-library-got-my-tauri-app-flagged-as-a-trojan-and-how-i-found-it-by-bisection-11fl</guid>
      <description>&lt;p&gt;My desktop app started getting flagged by Microsoft as &lt;code&gt;Trojan:Win32/Wacatac.B!ml&lt;/code&gt;. The code hadn't learned any new tricks. The culprit turned out to be the encryption library I had just added, and I found it by bisecting builds against the VirusTotal API.&lt;/p&gt;

&lt;p&gt;This post covers how I tracked it down, how I replaced the library without breaking the files users already had on disk, and what I'd do differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/luanmacea/roblox-account-manager" rel="noopener noreferrer"&gt;RAM — Roblox Account Manager&lt;/a&gt; is an open-source (GPL-3.0) Windows app built with &lt;strong&gt;Rust + Tauri 2&lt;/strong&gt; and React. It manages many Roblox accounts, launches several game clients at once and automates things like rejoining servers.&lt;/p&gt;

&lt;p&gt;That last part matters. An app that stores session cookies, spawns processes, brings windows to the front and sends synthetic input already &lt;em&gt;looks&lt;/em&gt; suspicious to a heuristic engine. There is no margin for anything else to look off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;After one release, Windows started warning users about the &lt;code&gt;.exe&lt;/code&gt;, and VirusTotal showed &lt;strong&gt;1/75&lt;/strong&gt;: Microsoft, &lt;code&gt;Trojan:Win32/Wacatac.B!ml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;!ml&lt;/code&gt; suffix is the important part: it's a &lt;strong&gt;machine-learning verdict&lt;/strong&gt;, not a signature match. No known malware pattern was found. A model just decided the binary resembles malware.&lt;/p&gt;

&lt;h2&gt;
  
  
  First guess: wrong
&lt;/h2&gt;

&lt;p&gt;My first instinct was to compare the last clean build with the flagged one. I dumped both with &lt;a href="https://github.com/erocarrera/pefile" rel="noopener noreferrer"&gt;&lt;code&gt;pefile&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same 16 "heuristically heavy" imports (&lt;code&gt;SendInput&lt;/code&gt;, &lt;code&gt;SetForegroundWindow&lt;/code&gt;, &lt;code&gt;TerminateProcess&lt;/code&gt;, &lt;code&gt;DuplicateHandle&lt;/code&gt;, &lt;code&gt;RegSetValueExW&lt;/code&gt;…)&lt;/li&gt;
&lt;li&gt;same section entropy, same linker&lt;/li&gt;
&lt;li&gt;only four new imports, all boring (&lt;code&gt;FlushFileBuffers&lt;/code&gt;, &lt;code&gt;OpenEventW&lt;/code&gt;, &lt;code&gt;RemoveDirectoryW&lt;/code&gt;, &lt;code&gt;MessageBoxW&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Conclusion at the time: &lt;em&gt;"the behavior profile is identical, the model just changed its mind, nothing to fix."&lt;/em&gt; It sounded reasonable, and it was wrong. The import table isn't everything the model looks at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bisecting against VirusTotal
&lt;/h2&gt;

&lt;p&gt;So I stopped guessing and bisected. The VirusTotal API is free (4 requests/minute, 500/day). A small script hashes the file, looks up an existing report and uploads it only if the hash is new:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CryptoHasher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/files/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;headers&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// already analyzed: read last_analysis_stats&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;// upload, then poll /analyses/{id} until "completed"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then plain &lt;code&gt;git bisect&lt;/code&gt;: build the release &lt;code&gt;.exe&lt;/code&gt; at each step, scan it, mark it &lt;code&gt;good&lt;/code&gt; (0/75) or &lt;code&gt;bad&lt;/code&gt; (1/75). Each step takes a few minutes, mostly the Rust release build.&lt;/p&gt;

&lt;p&gt;(A tip: my first attempt bisected &lt;em&gt;by date&lt;/em&gt; and broke on merge commits. Use real &lt;code&gt;git bisect&lt;/code&gt;.)&lt;/p&gt;

&lt;p&gt;The first bad commit was the one that &lt;strong&gt;integrated encryption for the account file&lt;/strong&gt;, using &lt;a href="https://crates.io/crates/sodiumoxide" rel="noopener noreferrer"&gt;&lt;code&gt;sodiumoxide&lt;/code&gt;&lt;/a&gt;, the Rust bindings to &lt;strong&gt;libsodium&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why libsodium?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;sodiumoxide&lt;/code&gt; statically links the libsodium &lt;strong&gt;C library&lt;/strong&gt; into your binary. A blob of optimized native crypto primitives, sitting inside an app that also stores credentials and injects input, is apparently close enough to what ransomware and stealers ship for the model to fire.&lt;/p&gt;

&lt;p&gt;Nothing is wrong with libsodium itself. It's the &lt;em&gt;combination&lt;/em&gt; the classifier learned to dislike.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing it without locking anyone out
&lt;/h2&gt;

&lt;p&gt;Users already had encrypted files on disk, and some had a password on them. The replacement had to read those files &lt;strong&gt;byte for byte&lt;/strong&gt;. I went with pure-Rust &lt;a href="https://github.com/RustCrypto" rel="noopener noreferrer"&gt;RustCrypto&lt;/a&gt; crates and matched every libsodium parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;argon2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.6"&lt;/span&gt;
&lt;span class="py"&gt;crypto_secretbox&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1"&lt;/span&gt;  &lt;span class="c"&gt;# XSalsa20-Poly1305&lt;/span&gt;
&lt;span class="py"&gt;sha2&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.10"&lt;/span&gt;             &lt;span class="c"&gt;# SHA-512 of the password, as before&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// Same as libsodium's crypto_pwhash (argon2i13, OPSLIMIT/MEMLIMIT_MODERATE):&lt;/span&gt;
&lt;span class="cd"&gt;/// Argon2i v0x13, t_cost = 6, m_cost = 128 MiB, parallelism 1, 32-byte output.&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;derive_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password_hash&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;salt&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;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CryptoError&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Params&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="mi"&gt;131072&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;CryptoError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InvalidData&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;argon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Argon2&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="nn"&gt;Algorithm&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Argon2i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;V0x13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;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="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;argon&lt;/span&gt;&lt;span class="nf"&gt;.hash_password_into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;salt&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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;CryptoError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;InvalidPassword&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="nf"&gt;Ok&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;crypto_secretbox&lt;/code&gt; from RustCrypto already produces libsodium's layout: the 16-byte Poly1305 MAC in &lt;strong&gt;front&lt;/strong&gt; of the ciphertext.&lt;/p&gt;

&lt;p&gt;What made me trust the swap was a test. Before removing libsodium, I encrypted a tiny payload with the &lt;strong&gt;old implementation&lt;/strong&gt;, pasted the bytes into the test as hex, and the new code must decrypt them exactly:&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="nd"&gt;#[test]&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;decrypts_a_libsodium_fixture&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Bytes produced by the OLD libsodium/sodiumoxide implementation.&lt;/span&gt;
    &lt;span class="c1"&gt;// If this fails, existing users' files would be locked. Never relax it.&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fixture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hex_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;concat!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s"&gt;"526f626c6f78204163636f756e74204d616e6167657220637265617465"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// …the rest of the header, salt, nonce, MAC and ciphertext&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;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;hash_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"test-password"&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;plain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decrypt&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;fixture&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;hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"fixture must decrypt"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nd"&gt;assert_eq!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;b"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s"&gt;example&lt;/span&gt;&lt;span class="se"&gt;\"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any parameter is off by one — Argon2 version, memory cost, MAC position — this test fails. That's exactly the kind of bug that locks users out of their data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side effect: tests got 17x slower
&lt;/h2&gt;

&lt;p&gt;Pure-Rust Argon2 in a &lt;strong&gt;debug&lt;/strong&gt; build is painfully slow: &lt;strong&gt;5.4 s per derivation&lt;/strong&gt;, against &lt;strong&gt;0.3 s&lt;/strong&gt; optimized. libsodium never had this problem, because it's precompiled C.&lt;/p&gt;

&lt;p&gt;Since &lt;code&gt;cargo test&lt;/code&gt; runs in debug, the suite went from minutes to most of the CI time. The fix is one block in &lt;code&gt;Cargo.toml&lt;/code&gt;: optimize &lt;em&gt;dependencies&lt;/em&gt; even in dev builds, keep your own crate unoptimized so it still compiles fast.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profile.dev.package."*"]&lt;/span&gt;
&lt;span class="py"&gt;opt-level&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Rust suite went from &lt;strong&gt;230 s to 41 s&lt;/strong&gt;, faster than it was with libsodium.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App &lt;code&gt;.exe&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;1/75 (&lt;code&gt;Wacatac.B!ml&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0/75&lt;/strong&gt;, Defender clean&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MSI installer&lt;/td&gt;
&lt;td&gt;0/61, but required admin&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;0/75&lt;/strong&gt;, per-user, no admin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NSIS setup&lt;/td&gt;
&lt;td&gt;3/71&lt;/td&gt;
&lt;td&gt;1/75 (one generic ML engine, on the packager)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No code signing, and no removing the encryption.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone shipping an unsigned desktop app
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;!ml&lt;/code&gt; verdicts are a moving target.&lt;/strong&gt; The same source built with different feature flags got different verdicts from the same model. You can't "win" once; you measure every release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VirusTotal's Microsoft engine and local Defender disagree.&lt;/strong&gt; A build passed local &lt;code&gt;MpCmdRun&lt;/code&gt; and still got flagged on VirusTotal. Check both, because users see both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bisect, don't theorize.&lt;/strong&gt; My careful import-table analysis pointed at the wrong conclusion. A few rounds of &lt;code&gt;git bisect&lt;/code&gt; pointed at the right commit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make your scanner fail loudly.&lt;/strong&gt; My own scan script once reported "all clean" while the Defender step had crashed (a UTF-8 em dash in a BOM-less &lt;code&gt;.ps1&lt;/code&gt; breaks Windows PowerShell 5.1) and the VirusTotal step exited 0 on &lt;em&gt;any&lt;/em&gt; verdict. A scanner that didn't run must never count as clean. That rule now has a test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native C dependencies are a heuristic risk.&lt;/strong&gt; If a pure-Rust equivalent exists and you can prove format compatibility with a fixture, it's worth considering.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The durable fix is still code signing (there are free options for open-source projects, like SignPath Foundation). Until then, measuring every release and keeping the recommended download clean is what I can do.&lt;/p&gt;




&lt;p&gt;The project is open source, and I'd love feedback on the Rust/Tauri side: &lt;a href="https://github.com/luanmacea/roblox-account-manager" rel="noopener noreferrer"&gt;github.com/luanmacea/roblox-account-manager&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I wrote this post with the help of an AI assistant; the investigation, numbers and code are from the project's history.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>security</category>
      <category>windows</category>
    </item>
  </channel>
</rss>
