<?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: Bijan</title>
    <description>The latest articles on DEV Community by Bijan (@bijan53c).</description>
    <link>https://dev.to/bijan53c</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%2F3985731%2F41ac23dc-f551-4d29-a8a0-3fd79a35d893.jpeg</url>
      <title>DEV Community: Bijan</title>
      <link>https://dev.to/bijan53c</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bijan53c"/>
    <language>en</language>
    <item>
      <title>The oracle problem</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Sun, 11 Oct 2026 12:06:23 +0000</pubDate>
      <link>https://dev.to/bijan53c/the-oracle-problem-49gd</link>
      <guid>https://dev.to/bijan53c/the-oracle-problem-49gd</guid>
      <description>&lt;p&gt;Just faced the oracle problem in blockchain, and got different definitions. Which one do you think is true?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"If oracles allow corrupt data to be imported on-chain, auto-executing smart contracts can have disastrous affects" - from Ethereum stack exchange &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"The Oracle Problem is the challenge of securely and reliably bringing external data onto a blockchain, where smart contracts cannot independently verify that data and must rely on some mechanism to establish its trustworthiness."  - AI&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Blockchain doesn't understand real-world data, and you can't give the data to it from a centralized point cause the purpose of blockchain is decentralization. So we use oracles like Chainlink made out of multiple nodes, so they confirm the data validity and pass to the blockchain. Decentralized, valid, accurate and reliable." - What I learned&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What do you think? &lt;/p&gt;

</description>
      <category>programming</category>
      <category>crypto</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>I Gave My SOC Experience to AI. It Gave Me Sigma Rules.</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Thu, 08 Oct 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/i-gave-my-soc-experience-to-ai-it-gave-me-sigma-rules-52n0</link>
      <guid>https://dev.to/bijan53c/i-gave-my-soc-experience-to-ai-it-gave-me-sigma-rules-52n0</guid>
      <description>&lt;p&gt;After spending time in a SOC, you start noticing patterns.&lt;/p&gt;

&lt;p&gt;Different alerts. Different machines. Different incidents.&lt;/p&gt;

&lt;p&gt;But eventually, you realize that a lot of the attacks you're seeing aren't completely new.&lt;/p&gt;

&lt;p&gt;The same techniques keep coming back.&lt;/p&gt;

&lt;p&gt;An Office application spawning something it shouldn't.&lt;/p&gt;

&lt;p&gt;PowerShell being used in suspicious ways.&lt;/p&gt;

&lt;p&gt;Security controls being tampered with.&lt;/p&gt;

&lt;p&gt;Software quietly abusing system resources.&lt;/p&gt;

&lt;p&gt;You see these things, investigate them, stop them, and move on to the next alert.&lt;/p&gt;

&lt;p&gt;I started wondering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I took some of the attack scenarios and TTPs I've actually encountered and turned them into reusable detection rules?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That's how I started working on &lt;strong&gt;SigmaVault&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I provided AI with some of the attack scenarios and TTPs I'd encountered and stopped during my SOC work, then asked it to generate Sigma rules around those scenarios.&lt;/p&gt;

&lt;p&gt;The goal wasn't to blindly generate a pile of rules and call it detection engineering.&lt;/p&gt;

&lt;p&gt;I wanted to experiment with something more useful:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can practical SOC experience be turned into structured, reusable detections?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For example, instead of keeping an attack pattern in my head as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I've seen this behavior before."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I can turn it into something closer to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attack behavior
      ↓
Relevant telemetry
      ↓
Detection logic
      ↓
Sigma rule
      ↓
SIEM / detection pipeline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is one important disclaimer, though.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rules aren't tested yet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I'm treating SigmaVault as a work in progress rather than claiming these are production-ready detections.&lt;/p&gt;

&lt;p&gt;The next step is testing and tuning them against real telemetry, looking at false positives, and figuring out where the generated logic actually holds up.&lt;/p&gt;

&lt;p&gt;And I'm not planning to stop with the initial scenarios.&lt;/p&gt;

&lt;p&gt;I'd like to expand the project with more attack techniques and eventually add &lt;strong&gt;Linux-focused detections&lt;/strong&gt; as well.&lt;/p&gt;

&lt;p&gt;That's what makes this project interesting to me.&lt;/p&gt;

&lt;p&gt;It's not really about getting AI to write YAML.&lt;/p&gt;

&lt;p&gt;It's about taking things I've encountered in the SOC, turning them into detection hypotheses, and then testing whether those hypotheses can become useful detection logic.&lt;/p&gt;

&lt;p&gt;Maybe some rules will be wrong.&lt;/p&gt;

&lt;p&gt;Maybe some will be noisy.&lt;/p&gt;

&lt;p&gt;Maybe some will actually turn out to be useful.&lt;/p&gt;

&lt;p&gt;That's the part I want to find out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SigmaVault:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://github.com/bijan53c/SigmaVault" rel="noopener noreferrer"&gt;https://github.com/bijan53c/SigmaVault&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you spot something that could be improved, or have a detection scenario worth adding, let me know.&lt;/p&gt;

&lt;p&gt;Hope it helps someone else working on their own detection engineering journey.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>security</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>You don't know how to work!</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Sun, 04 Oct 2026 09:34:19 +0000</pubDate>
      <link>https://dev.to/bijan53c/you-dont-know-how-to-work-28ip</link>
      <guid>https://dev.to/bijan53c/you-dont-know-how-to-work-28ip</guid>
      <description>&lt;p&gt;I've been reading Slow Productivity by Cal Newport, and one idea in it made me stop and actually change something: working in a way that's aligned with how humans are wired to focus, instead of the always-on pace most jobs and corporations run on by default.&lt;/p&gt;

&lt;p&gt;I stopped tracking my work by time I've put, reduced the tasks and parallel projects I've been doing together; instead I'm tracking what tasks of each project I'm working on, setting complexity meter to each, and writing the output of each. &lt;/p&gt;

&lt;p&gt;It's a small shift, but it's made my work in bug bounty hunting and personal projects noticeably less frantic and, honestly, more consistent.&lt;/p&gt;

&lt;p&gt;I don't think this is a one-person hack — I think most workplaces would benefit from applying even one or two of these principles deliberately, instead of optimizing purely for busyness.&lt;/p&gt;

&lt;p&gt;Have you read it? If so, what's the one idea from it that actually changed how you work — not just how you think about work?&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>tutorial</category>
      <category>career</category>
      <category>learning</category>
    </item>
    <item>
      <title>I Found an Access Control Bug in My First Solidity Contract</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Fri, 02 Oct 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/i-found-an-access-control-bug-in-my-first-solidity-contract-55a8</link>
      <guid>https://dev.to/bijan53c/i-found-an-access-control-bug-in-my-first-solidity-contract-55a8</guid>
      <description>&lt;p&gt;I revisited my first Solidity contract and found a simple access control bug — and learned why Solidity's address(0) default behavior mattered to the fix.&lt;/p&gt;

&lt;p&gt;I found an access control bug hiding in plain sight.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;SimpleStorage&lt;/code&gt; lets users save data by ID. Here's the version I originally wrote:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function AddData(uint256 _ItemID, string memory _StringData) public {
    DataIdToData[_ItemID] = EntryData(_StringData, msg.sender);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At a glance, it looks harmless.&lt;/p&gt;

&lt;p&gt;It isn't.&lt;/p&gt;

&lt;p&gt;There was &lt;strong&gt;no check for who already owned &lt;code&gt;_ItemID&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That meant anyone could call &lt;code&gt;AddData()&lt;/code&gt; with an ID that belonged to someone else and overwrite their entry.&lt;/p&gt;

&lt;p&gt;The caller would then become the new owner.&lt;/p&gt;

&lt;p&gt;No error.&lt;br&gt;
No revert.&lt;br&gt;
No access control.&lt;/p&gt;

&lt;p&gt;Just a successful transaction.&lt;/p&gt;
&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;The fix was a simple ownership check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(
    DataIdToData[_ItemID].Owner == msg.sender ||
    DataIdToData[_ItemID].Owner == address(0),
    "this ID is assigned to someone else!"
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now a write is only allowed when:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The caller already owns the ID, &lt;strong&gt;or&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The ID hasn't been claimed yet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second condition is where something I previously found confusing in Solidity suddenly became useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;address(0)&lt;/code&gt; isn't just a default
&lt;/h3&gt;

&lt;p&gt;An address field that hasn't been explicitly initialized returns Solidity's zero address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;address(0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So an untouched entry can effectively act as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Owner = address(0)
        ↓
Nobody owns this ID yet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that gives the contract a simple way to distinguish an unclaimed ID from an owned one.&lt;/p&gt;

&lt;p&gt;A few days ago, I wrote about how confusing Solidity's default storage values were when I was first learning.&lt;/p&gt;

&lt;p&gt;Now I'm using that exact behavior to implement an ownership check.&lt;/p&gt;

&lt;p&gt;That's probably the part I find most interesting about learning Solidity:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The thing that confused me as a beginner can eventually become part of the security model.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'm still early in this.&lt;/p&gt;

&lt;p&gt;But I'm starting to find the gaps in my own contracts before someone else has to point them out.&lt;/p&gt;

</description>
      <category>web3</category>
      <category>programming</category>
      <category>beginners</category>
      <category>security</category>
    </item>
    <item>
      <title>The Line of Code That Makes Every Encryption Unique — Even With the Same Password</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Mon, 28 Sep 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/the-line-of-code-that-makes-every-encryption-unique-even-with-the-same-password-1bma</link>
      <guid>https://dev.to/bijan53c/the-line-of-code-that-makes-every-encryption-unique-even-with-the-same-password-1bma</guid>
      <description>&lt;p&gt;Same message. Same password. Same key. Two completely different ciphertexts. That's not a bug — it's a requirement, and it comes down to a single value that's easy to overlook: the nonce.&lt;/p&gt;

&lt;p&gt;This is the final post in a series documenting what I'm learning building &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why identical ciphertext for identical plaintext is a real problem
&lt;/h2&gt;

&lt;p&gt;If the same plaintext always produced the same ciphertext under a given key, an attacker watching encrypted traffic could spot patterns without ever breaking the encryption itself. "This exact ciphertext shows up every time this user logs in." "This ciphertext matches the one sent yesterday, so the underlying message is probably identical." None of that requires cracking the key — it's leaking information through metadata and repetition, which is its own category of failure separate from confidentiality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the nonce comes in
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;crypto.py&lt;/code&gt;, a fresh nonce is generated for every single call to &lt;code&gt;encrypt()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;NONCE_SIZE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;

&lt;span class="k"&gt;def&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;plaintext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&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="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NONCE_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AESGCM&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="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aes&lt;/span&gt;&lt;span class="p"&gt;.&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;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&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;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That 12-byte nonce gets mixed into the AES-GCM encryption process, and its presence is what guarantees the output is unique every time — even with the exact same key and the exact same message. It's returned alongside the ciphertext because it needs to be available again at decryption time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&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;ciphertext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&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="nb"&gt;bytes&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="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;aes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AESGCM&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;return&lt;/span&gt; &lt;span class="n"&gt;aes&lt;/span&gt;&lt;span class="p"&gt;.&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The nonce doesn't need to be secret — it needs to never repeat
&lt;/h2&gt;

&lt;p&gt;This mirrors the salt from earlier in this series, and it trips people up the same way. The nonce isn't hidden — it's stored right alongside the ciphertext, in the clear. Its job was never secrecy. Its only job is uniqueness: it must never repeat under the same key. That's it. That's the entire requirement, and it's also the requirement that's dangerously easy to violate without realizing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happens if you reuse one
&lt;/h2&gt;

&lt;p&gt;This is the part that makes nonce handling load-bearing rather than a formality. Reuse a nonce with AES-GCM under the same key — even once — and you don't just weaken the encryption slightly. You can fully break the confidentiality of &lt;em&gt;both&lt;/em&gt; messages encrypted with that repeated nonce, and depending on what an attacker can access, potentially recover the authentication key used for the integrity tag as well, undermining the tamper-detection property covered in the previous post too.&lt;/p&gt;

&lt;p&gt;This isn't a theoretical edge case in an academic paper. Nonce reuse under AES-GCM is one of the most common real-world implementation bugs in systems that otherwise look like they got everything else right — it's happened in production systems, including ones built by teams who clearly understood the rest of the AEAD design. The failure mode is specifically nasty because everything looks fine until the exact moment a nonce collides, at which point the damage isn't "slightly weaker encryption" — it's a full break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "generate it fresh, every call" is the whole mitigation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;os.urandom(12)&lt;/code&gt; on every single call to &lt;code&gt;encrypt()&lt;/code&gt; is the entire fix. A 12-byte (96-bit) random nonce, generated properly, makes accidental collision astronomically unlikely for any realistic volume of messages under a single key. The mitigation isn't complicated — it's just non-negotiable. Caching a nonce, deriving it predictably, or reusing one across calls "just this once" is exactly the kind of shortcut that looks harmless in a demo and catastrophic in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway across this whole series
&lt;/h2&gt;

&lt;p&gt;Looking back across all five posts: a derived key instead of a raw password, a random salt, a deliberately slow and memory-hard KDF, an AEAD cipher instead of plain AES, and a fresh nonce on every call. None of these individually are exotic. Every one of them is a single function call or a small config value. And every single one of them is a place where skipping the "obvious" step quietly turns working code into broken security.&lt;/p&gt;

&lt;p&gt;That's been the actual lesson of building this project: applied cryptography isn't hard because the concepts are exotic. It's hard because almost every step has a version that &lt;em&gt;looks&lt;/em&gt; correct, runs without errors, and is still wrong in a way that only shows up under attack.&lt;/p&gt;




&lt;p&gt;Full project (with all five concepts implemented) is at &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you seen a real nonce-reuse vulnerability in the wild?&lt;/strong&gt; I'd genuinely like to hear the story.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>python</category>
      <category>appsec</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>I Gave AI Hands on a Virtual Machine — Here's What Changed</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Mon, 28 Sep 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/i-gave-ai-hands-on-a-virtual-machine-heres-what-changed-176h</link>
      <guid>https://dev.to/bijan53c/i-gave-ai-hands-on-a-virtual-machine-heres-what-changed-176h</guid>
      <description>&lt;p&gt;I think AI is the closest thing to what Google search was when it first went public — it didn't replace anyone's job, it made a whole category of work faster, and "knowing how to use it well" became its own skill. Some people reach for the WordPress comparison instead, when talking about vibe coding and the worry it put into web developers a decade ago. Either analogy lands the same way for me: this is a new capability layer, not a replacement for the person using it.&lt;/p&gt;

&lt;p&gt;I'm an ethical hacker — I've worked both defense (SOC) and offense (red team, pentesting, bug hunting), which means my day covers everything from planning and scripting to configuring machines and finding and exploiting vulnerabilities, ethically. AI's role in that day has changed a lot over the past while, and the change is worth being specific about, because "I use AI" doesn't actually say much on its own.&lt;/p&gt;

&lt;p&gt;How I used to use it: five models, zero shared context&lt;/p&gt;

&lt;p&gt;For a while I ran different providers for different jobs, because each one genuinely had different strengths. Gemini for fast, deep search. Claude for coding. ChatGPT for translation. Grok and DeepSeek sitting in reserve for whenever I ran out of tokens on the primary set.&lt;/p&gt;

&lt;p&gt;It worked, but it had two real costs. First, models don't share context — handing code from Claude to another model to continue working on it could quietly break things, because the second model has no idea what assumptions the first one made. Second, every switch meant starting from zero: re-explaining the same context, constraints, and goals to a model that had no memory of the conversation you'd just had somewhere else.&lt;/p&gt;

&lt;p&gt;If you're running into the same problem, the practical fix is less exciting than it sounds: pick the provider whose pro tier actually covers most of what you need, and stop routing around token limits by hopping providers mid-task.&lt;/p&gt;

&lt;p&gt;How I use it now: an agent with hands, on a machine I can delete&lt;/p&gt;

&lt;p&gt;The bigger shift wasn't switching tools — it was giving one of them the ability to act directly. I run Claude Code on a virtual machine, which means I'm not copying output back and forth or manually following step-by-step instructions myself. The AI has hands on the machine. It's closer to a teammate than a tool at that point.&lt;/p&gt;

&lt;p&gt;In practice: I prompt once, define the skills a task needs, and reuse them across scenarios instead of re-explaining from scratch each time. That parallelism is the actual productivity gain — while I'm building a POC for one vulnerability, the agent can be running recon on a second target at the same time. Less of my own time spent context-switching, more happening concurrently.&lt;/p&gt;

&lt;p&gt;The guardrail matters as much as the capability. Full, unsupervised machine access for an AI agent is a genuinely bad idea — not hypothetically, just practically, given how easy it is to get lazy about permissions once something is working well. Running it on a disposable VM means if it does go sideways, the fix is deleting the machine, not cleaning up a production environment.&lt;/p&gt;

&lt;p&gt;The part most "AI in security" takes skip: it doesn't have the creative jump&lt;/p&gt;

&lt;p&gt;Permission friction is real — approving actions one at a time feels like fielding a constant stream of questions from a very capable but literal-minded assistant, and the alternative (broad access) risks burning through actions, and tokens, fast. Output still needs verifying every time; nothing here removes that step. And running this workflow well is as much a management skill as a technical one — you're now responsible for your own work and for checking someone else's, even if that someone else is an AI agent.&lt;/p&gt;

&lt;p&gt;But the sharpest limitation isn't about permissions or verification overhead. It's that AI doesn't have the creative jump a human hacker makes. It'll test a site for XSS, but only within the parameter space it's been given — it won't naturally go looking for the deep, unconventional payload structure a human would try specifically because it's unconventional. That's a real gap in offensive security work, where a meaningful chunk of what makes a finding interesting is exactly the part a rules-following process doesn't generate on its own. Trusting an agent's negative result — "nothing found" — without accounting for that gap is how you end up with false confidence, not just false positives.&lt;/p&gt;

&lt;p&gt;Where that leaves me&lt;/p&gt;

&lt;p&gt;AI is a tool, not a replacement — it speeds up real chunks of the work, from generating an image for a blog post to helping debug code, but the parts of this job that depend on human creativity and judgment aren't going anywhere. I built in the guardrail (isolated VM, gated permissions) because I don't fully trust unsupervised autonomy yet, and I don't think that trust should be assumed by default. The upside is real. So is the reason to keep a hand on the wheel.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>automation</category>
    </item>
    <item>
      <title>I Stopped Chasing OWASP Top 10. Here’s the Bug Bounty Method That Actually Finds Real Bugs</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Fri, 25 Sep 2026 14:19:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/i-stopped-chasing-owasp-top-10-heres-the-bug-bounty-method-that-actually-finds-real-bugs-401k</link>
      <guid>https://dev.to/bijan53c/i-stopped-chasing-owasp-top-10-heres-the-bug-bounty-method-that-actually-finds-real-bugs-401k</guid>
      <description>&lt;p&gt;A few days ago I jumped back into a bug bounty program after being away for a while.&lt;br&gt;
Like a lot of hunters, I disappear sometimes. When I return, I don’t reinvent the wheel. I open the methodology document I wrote for myself and follow it step by step — recon first, then deeper testing.&lt;br&gt;
The biggest shift in my approach over the years is this:&lt;br&gt;
I don’t treat OWASP vulnerabilities as the priority.&lt;br&gt;
I go after business logic flaws.&lt;br&gt;
Business logic is how the application actually operates — the real flow of value, the process that makes money for the company. Once you understand that flow, you can start looking for ways to break it, bypass steps, or manipulate parameters so the system works in the attacker’s favor instead of the owner’s.&lt;br&gt;
Because of this focus, my recon looks different from the usual “run tools and hope” approach.&lt;br&gt;
Directory brute-forcing rarely helps me. Instead I study the application by using it. I keep Burp Suite open, capture every request and response, and slowly build two things:&lt;/p&gt;

&lt;p&gt;A logical mind map of the application — which pages and inputs lead where, how the business process actually moves.&lt;br&gt;
Technical data — exact request structure, headers, parameters, and response behavior.&lt;/p&gt;

&lt;p&gt;I map both at the same time. After enough interaction, the potentially dangerous spots start to stand out on their own.&lt;br&gt;
I’ve had multiple cases where I found and reported valid bugs while still in the recon phase — simply because something in the flow looked wrong and I tested it on the spot.&lt;br&gt;
That’s the real value of treating recon as active study instead of passive scanning. You’re not just collecting endpoints. You’re learning how the business works and where it can be broken.&lt;/p&gt;

&lt;p&gt;If this way of thinking about bug bounty resonates with you, follow along. I’ll keep sharing the practical methods and mindsets that actually produce results.&lt;/p&gt;

</description>
      <category>tutorial</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>bugbounty</category>
    </item>
    <item>
      <title>Encryption Isn't Enough — You Also Need to Know If Someone Tampered With Your Data</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Mon, 21 Sep 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/encryption-isnt-enough-you-also-need-to-know-if-someone-tampered-with-your-data-gio</link>
      <guid>https://dev.to/bijan53c/encryption-isnt-enough-you-also-need-to-know-if-someone-tampered-with-your-data-gio</guid>
      <description>&lt;p&gt;Most people think encryption's job is to keep data secret. That's only half the job — and the half it doesn't cover is exactly where a lot of otherwise-careful implementations quietly break.&lt;/p&gt;

&lt;p&gt;This is part 4 of a series documenting what I'm learning building &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap plain encryption doesn't cover
&lt;/h2&gt;

&lt;p&gt;Encryption without a way to detect tampering means an attacker who can intercept your ciphertext can flip bits in it without ever knowing the key. They can't read your plaintext, but they don't need to — depending on the cipher mode, they can sometimes predictably corrupt it, and your system will happily decrypt the tampered ciphertext into garbage, or in worse cases, into something that looks structurally valid but isn't what was actually sent.&lt;/p&gt;

&lt;p&gt;Confidentiality answers "can someone read this." It says nothing about "can someone change this without me noticing." Those are two separate guarantees, and you need both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why AES-GCM instead of plain AES-CBC
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;crypto.py&lt;/code&gt; uses &lt;code&gt;AESGCM&lt;/code&gt; specifically, not plain AES in CBC mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cryptography.hazmat.primitives.ciphers.aead&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AESGCM&lt;/span&gt;

&lt;span class="k"&gt;def&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;plaintext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&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="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;nonce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&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;aes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AESGCM&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="n"&gt;ciphertext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aes&lt;/span&gt;&lt;span class="p"&gt;.&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;plaintext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&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;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ciphertext&lt;/span&gt;


&lt;span class="k"&gt;def&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;ciphertext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&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="nb"&gt;bytes&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="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;aes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AESGCM&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;return&lt;/span&gt; &lt;span class="n"&gt;aes&lt;/span&gt;&lt;span class="p"&gt;.&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GCM stands for Galois/Counter Mode, and it belongs to a category called AEAD — &lt;strong&gt;Authenticated Encryption with Associated Data&lt;/strong&gt;. In one pass, it does two things at once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Encrypts the data&lt;/strong&gt;, so it stays confidential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generates an authentication tag&lt;/strong&gt;, proving the ciphertext wasn't modified after encryption.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That tag is embedded in the output &lt;code&gt;AESGCM.encrypt()&lt;/code&gt; returns. When you call &lt;code&gt;decrypt()&lt;/code&gt;, the library recomputes and checks that tag before returning any plaintext at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when someone tampers with it
&lt;/h2&gt;

&lt;p&gt;Change even a single bit of the ciphertext after encryption, and &lt;code&gt;AESGCM.decrypt()&lt;/code&gt; doesn't return corrupted data — it raises an exception and refuses to return anything. That's the actual security property: failure is loud and immediate, not silent and exploitable. A system built on AES-GCM either gets the exact plaintext that was encrypted, or it gets nothing at all. There's no third outcome where it silently accepts tampered data.&lt;/p&gt;

&lt;p&gt;Plain AES-CBC doesn't give you this. It'll happily decrypt tampered ciphertext into whatever the tampering produces — corrupted plaintext, or in some poorly-designed protocols, plaintext an attacker had meaningful influence over. CBC gives you confidentiality. It gives you nothing for integrity, and bolting integrity on afterward (encrypt-then-MAC, done correctly) is exactly the kind of extra step that's easy to get subtly wrong if you're rolling it yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this distinction matters more than it sounds like it should
&lt;/h2&gt;

&lt;p&gt;"This is private" and "this is private, and I can trust it wasn't touched" sound like a minor wording difference. In practice they're the difference between a system that's vulnerable to bit-flipping and ciphertext manipulation attacks, and one that simply isn't — for free, as a property of the cipher mode you picked, not something you have to remember to bolt on separately.&lt;/p&gt;

&lt;p&gt;This is also why AEAD ciphers like AES-GCM (and alternatives like ChaCha20-Poly1305) have become the default recommendation over plain block cipher modes for new designs: they make the "did I remember to add integrity checking" question disappear by folding it into the encryption primitive itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Confidentiality and integrity are two different guarantees, and a lot of homegrown or legacy crypto quietly only provides one of them. Picking AES-GCM over plain AES-CBC isn't a performance or convenience choice — it's closing a specific, well-understood gap that shows up in real systems more often than it should.&lt;/p&gt;




&lt;p&gt;Next in this series, the last one: why a nonce has to be unique every single time, and what actually happens if you reuse one under AES-GCM — it's worse than you'd expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you run into a system that had encryption but no integrity check?&lt;/strong&gt; What ended up happening?&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>python</category>
      <category>security</category>
      <category>appsec</category>
    </item>
    <item>
      <title>Reading a Mapping That Lives on a Different Contract</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Mon, 21 Sep 2026 14:12:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/reading-a-mapping-that-lives-on-a-different-contract-4ck9</link>
      <guid>https://dev.to/bijan53c/reading-a-mapping-that-lives-on-a-different-contract-4ck9</guid>
      <description>&lt;p&gt;My last two entries in this series were both about &lt;code&gt;SimpleStorage.sol&lt;/code&gt; — a contract that saves data by ID, and a fix for a missing access control check on it. The next step was different in kind, not just in complexity: instead of writing one contract, I needed to write one that deploys and tracks &lt;em&gt;other&lt;/em&gt; contracts. A factory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: my data structure didn't match the pattern I was building on top of
&lt;/h2&gt;

&lt;p&gt;I followed a tutorial for the base factory structure, but I've been customizing every tutorial in this series rather than copying it directly — &lt;code&gt;SimpleStorage.sol&lt;/code&gt; uses a &lt;code&gt;mapping&lt;/code&gt; and a &lt;code&gt;struct&lt;/code&gt;, where the tutorial I originally learned from used an array. That decision paid off when it came to reading a single value out by ID, but it created a real problem once I needed to build a factory on top of it.&lt;/p&gt;

&lt;p&gt;The tutorial's factory pattern assumed each deployed contract exposed its data as an array, so pulling a value back out meant array-style indexing. My deployed contracts expose their data as a &lt;code&gt;mapping(uint256 =&amp;gt; EntryData)&lt;/code&gt;, keyed by an ID rather than a position. Reaching into a &lt;em&gt;different, already-deployed&lt;/em&gt; contract's mapping — not just my own — and pulling a specific entry back out wasn't something the tutorial covered, because it wasn't built around that data structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I actually worked through it
&lt;/h2&gt;

&lt;p&gt;Before asking for help, I told the AI I was working with directly that I didn't want generated code — I wanted to be walked toward the answer, not handed it. What followed was closer to a Socratic dialogue than a code request: guiding questions rather than a solution, similar to how I used to walk cybersecurity students toward an answer during training rather than giving it to them outright. That process is what produced this function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// reads from a deployed SimpleStorage contract
function SfGet(uint256 _SimpleStorageDataID, uint256 _ID) public view returns (string memory, address) {
    return ListOfSimpleStorageContracts[_SimpleStorageDataID].DataIdToData(_ID);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Breaking down what this actually does
&lt;/h2&gt;

&lt;p&gt;Two separate problems needed solving in this one function, and they're easy to conflate if you haven't hit this pattern before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First: which contract?&lt;/strong&gt; The factory can deploy many &lt;code&gt;SimpleStorage&lt;/code&gt; instances, tracked in a list — &lt;code&gt;ListOfSimpleStorageContracts&lt;/code&gt; — inside &lt;code&gt;factory.sol&lt;/code&gt;. &lt;code&gt;_SimpleStorageDataID&lt;/code&gt; is an index into that list, selecting &lt;em&gt;which deployed contract instance&lt;/em&gt; to talk to. This is a different concept from the ID used inside &lt;code&gt;SimpleStorage&lt;/code&gt; itself, even though both are &lt;code&gt;uint256&lt;/code&gt; — one picks a contract, the other picks an entry inside it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second: which entry, inside that contract?&lt;/strong&gt; Once you've selected a deployed instance, &lt;code&gt;_ID&lt;/code&gt; is the key into &lt;em&gt;that instance's&lt;/em&gt; mapping — the same &lt;code&gt;DataIdToData&lt;/code&gt; mapping from the earlier entries in this series. &lt;code&gt;.DataIdToData(_ID)&lt;/code&gt; calls the auto-generated getter Solidity creates for a &lt;code&gt;public&lt;/code&gt; mapping, but it's being called on a specific deployed contract reference (&lt;code&gt;ListOfSimpleStorageContracts[_SimpleStorageDataID]&lt;/code&gt;), not on &lt;code&gt;this&lt;/code&gt; contract.&lt;/p&gt;

&lt;p&gt;So the function needs two IDs because there are two levels of lookup happening — which contract, then which entry in it — and conflating them is exactly the kind of bug that would silently return the wrong data instead of failing loudly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the return type includes an address
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;SimpleStorage&lt;/code&gt;'s ownership check — the access control fix from the last entry in this series — depends entirely on &lt;code&gt;Owner&lt;/code&gt;, an address stored per entry. Reading data back out through the factory has to preserve that: the function returns &lt;code&gt;(string memory, address)&lt;/code&gt;, not just the string. Any caller or contract using this read function to make a decision — say, deciding whether to allow an action based on ownership — needs that address returned accurately alongside the data. Dropping it, or getting the ordering wrong, would quietly break the same access control logic the earlier fix depended on, just one layer further up the call chain.&lt;/p&gt;

&lt;p&gt;Check the full &lt;em&gt;factory.sol&lt;/em&gt; here&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this is headed
&lt;/h2&gt;

&lt;p&gt;Factories like this are a common building block once you get past single, standalone contracts — most DeFi protocols run on some version of "one contract deploys and tracks many instances of another." That's the direction I'm heading next: more DeFi-style projects, building on this pattern rather than starting from scratch each time.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>beginners</category>
      <category>web3</category>
    </item>
    <item>
      <title>Why I Chose Argon2id Over the \"Faster\" Alternatives</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Mon, 14 Sep 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/why-i-chose-argon2id-over-the-faster-alternatives-3dg1</link>
      <guid>https://dev.to/bijan53c/why-i-chose-argon2id-over-the-faster-alternatives-3dg1</guid>
      <description>&lt;p&gt;Argon2id is deliberately slow. That's not a limitation — it's the entire design goal. For key derivation, fast is a vulnerability, not a feature, and understanding why changed how I think about "performance" as a security property.&lt;/p&gt;

&lt;p&gt;This is part 3 of a series documenting what I'm learning building &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core trade-off: speed helps the attacker more than you
&lt;/h2&gt;

&lt;p&gt;Derive a key in one microsecond, and an attacker with the right hardware can try billions of password guesses per second against a stolen hash or ciphertext. Every guess is basically free for them. Make derivation slow and memory-hungry, and that same attack — still technically possible — becomes expensive enough to be impractical at scale.&lt;/p&gt;

&lt;p&gt;Here's the actual configuration in &lt;code&gt;crypto.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&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;master_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;hash_secret_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;master_password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;time_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# 64 MiB
&lt;/span&gt;        &lt;span class="n"&gt;parallelism&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hash_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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;memory_cost=65536&lt;/code&gt; means each single derivation attempt requires 64MB of memory. &lt;code&gt;time_cost=3&lt;/code&gt; adds iteration on top of that. Individually these numbers look arbitrary. Together, they're the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why memory cost specifically matters
&lt;/h2&gt;

&lt;p&gt;Older key derivation functions like PBKDF2 are fast to attack on GPUs, because GPUs are built for exactly the kind of math PBKDF2 does: massively parallel, low-memory operations. A single high-end GPU can run an enormous number of parallel PBKDF2 guesses simultaneously, because each guess needs almost no memory.&lt;/p&gt;

&lt;p&gt;Argon2 breaks that advantage on purpose. By forcing every single guess to allocate and use real memory (64MB, in this config), it caps how many guesses can run in parallel — GPUs have far less memory available per core than a CPU does, so a GPU that could run thousands of parallel PBKDF2 attempts can only run a small fraction of that many parallel Argon2 attempts before running out of memory. This is called being "memory-hard," and it specifically neutralizes the GPU cracking rigs that are the actual hardware attackers use at scale, not a theoretical edge case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "id" specifically, not "i" or "d"
&lt;/h2&gt;

&lt;p&gt;Argon2 actually comes in three variants:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Argon2d&lt;/strong&gt; — maximizes resistance to GPU cracking, but is vulnerable to certain side-channel timing attacks because memory access patterns depend on the secret input.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argon2i&lt;/strong&gt; — designed to resist those side-channel attacks by using data-independent memory access, at some cost to GPU resistance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argon2id&lt;/strong&gt; — a hybrid that runs Argon2i-style access for part of the process and Argon2d-style for the rest, aiming to get meaningful resistance to both attack classes at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That hybrid design is why Argon2id is the variant recommended by current guidance (including OWASP's password storage recommendations) over using Argon2i or Argon2d alone — it doesn't maximize either property individually, but it doesn't leave either one badly exposed either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning is a real trade-off, not a fixed answer
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;time_cost=3&lt;/code&gt;, &lt;code&gt;memory_cost=65536&lt;/code&gt;, &lt;code&gt;parallelism=4&lt;/code&gt; aren't universal constants — they're a balance between security and usability. Crank memory and time cost up, and you get stronger resistance to offline cracking, but every legitimate login or decryption also gets slower and heavier on the server or device doing the deriving. Set them too low, and you're back to a derivation cheap enough to attack at scale. There's no single correct number here — it depends on your threat model and what hardware your legitimate users are deriving keys on.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;Slow isn't a bug in a key derivation function — it's the mechanism. Argon2id spends real time and real memory on every single derivation specifically so that an attacker has to spend that same cost, multiplied by however many guesses they need to make. That's the trade a KDF is supposed to make, and it's a completely different goal from a general-purpose hash function like SHA-256, which is &lt;em&gt;meant&lt;/em&gt; to be fast.&lt;/p&gt;




&lt;p&gt;Next in this series: why encryption alone isn't enough, and what AES-GCM adds on top of confidentiality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you've tuned Argon2 or bcrypt cost parameters for a real production system, what trade-offs did you land on?&lt;/strong&gt; I'd like to hear how people are actually setting these in practice.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>python</category>
      <category>programming</category>
      <category>security</category>
    </item>
    <item>
      <title>What a Confusing Line of Solidity Taught Me About Defaults and Security</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Sun, 06 Sep 2026 12:27:03 +0000</pubDate>
      <link>https://dev.to/bijan53c/what-a-confusing-line-of-solidity-taught-me-about-defaults-and-security-2ie7</link>
      <guid>https://dev.to/bijan53c/what-a-confusing-line-of-solidity-taught-me-about-defaults-and-security-2ie7</guid>
      <description>&lt;p&gt;Last night I spent an hour confused about something that should've taken two minutes.&lt;/p&gt;

&lt;p&gt;I was writing my first Solidity storage contract — a simple one, &lt;code&gt;SimpleStorage&lt;/code&gt;, that saves entries by ID. Coming from years of web app pentesting, SOC work, and red/purple team ops, I'm used to thinking about how systems get broken, not how they're built. So when I declared:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8h1ogfq4dt6l0htpibtc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8h1ogfq4dt6l0htpibtc.png" alt=" " width="800" height="297"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I expected I'd need to explicitly tell Solidity this was permanent — the same way I had to write &lt;code&gt;string memory _StringData&lt;/code&gt; for the temporary parameter in my function. I didn't. State variables like that mapping are storage by default, no keyword required. It took a tutorial, a web search, and finally asking AI directly before that clicked.&lt;/p&gt;

&lt;p&gt;Small thing. But it's the kind of small thing that matters more here than in most languages I've worked in — because in Solidity, a misunderstanding like that isn't just a bug. It's the difference between "works in testing" and "exploitable in production."&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: two things that look similar, aren't
&lt;/h2&gt;

&lt;p&gt;In most languages I've touched, "where does this variable live" is either irrelevant to the syntax or explicit everywhere. Solidity splits the difference in a way that isn't obvious until it bites you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inside a function, if you're working with a &lt;code&gt;string&lt;/code&gt;, &lt;code&gt;struct&lt;/code&gt;, &lt;code&gt;array&lt;/code&gt;, or &lt;code&gt;mapping&lt;/code&gt;, you have to say whether it's &lt;code&gt;memory&lt;/code&gt; (temporary, wiped after the function call) or &lt;code&gt;storage&lt;/code&gt; (permanent, written to the blockchain).&lt;/li&gt;
&lt;li&gt;At the contract level — where you declare state variables — you don't say anything, because there's only one option. State variables are &lt;em&gt;always&lt;/em&gt; storage.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The inconsistency isn't a language flaw. It's that the "choice" only exists in one context. Outside a function, there's nothing to choose between — a state variable's whole purpose is to persist, so Solidity doesn't ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the default exists
&lt;/h2&gt;

&lt;p&gt;This maps directly to how the Ethereum Virtual Machine actually works. A smart contract has three places data can live:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Location&lt;/th&gt;
&lt;th&gt;Lifespan&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Where it's used&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;storage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Permanent, written to the blockchain&lt;/td&gt;
&lt;td&gt;Expensive (gas cost scales with writes)&lt;/td&gt;
&lt;td&gt;Contract state variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;memory&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temporary, exists only during function execution&lt;/td&gt;
&lt;td&gt;Cheap&lt;/td&gt;
&lt;td&gt;Local variables inside functions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;calldata&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temporary, read-only&lt;/td&gt;
&lt;td&gt;Cheapest&lt;/td&gt;
&lt;td&gt;Function input parameters (external calls)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;State variables exist specifically so a contract can remember something between transactions — a balance, an owner address, an entry in a mapping. There's no scenario where a state variable would need to be &lt;code&gt;memory&lt;/code&gt;, because a &lt;code&gt;memory&lt;/code&gt; variable disappears the moment the function returns. So Solidity doesn't make you spell out the obvious. Everything declared at the contract level, outside a function, is storage.&lt;/p&gt;

&lt;p&gt;Inside a function, though, the compiler can't guess your intent. You might want a &lt;code&gt;string&lt;/code&gt; that's just scratch space for a calculation — &lt;code&gt;memory&lt;/code&gt; — or you might be about to write it back into a state variable — &lt;code&gt;storage&lt;/code&gt;. That ambiguity is why the keyword is required there and only there.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it plays out in code
&lt;/h2&gt;

&lt;p&gt;Here's the shape of the confusion, side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// State variable — always storage, no keyword needed
mapping(uint256 =&amp;gt; string) public DataIdToData;

// Function parameter — location must be explicit
function addData(uint256 _id, string memory _StringData) public {
    DataIdToData[_id] = _StringData; // copied from memory into storage
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_StringData&lt;/code&gt; lives in memory for the duration of the function call. The moment you assign it into &lt;code&gt;DataIdToData[_id]&lt;/code&gt;, it's copied into permanent contract storage. Nothing about the syntax announces that copy is happening — you have to already know the mapping is &lt;code&gt;storage&lt;/code&gt; and the parameter is &lt;code&gt;memory&lt;/code&gt; to see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a security issue, not just a syntax quirk
&lt;/h2&gt;

&lt;p&gt;This is where the pentester instincts kicked back in. Getting &lt;code&gt;storage&lt;/code&gt; vs &lt;code&gt;memory&lt;/code&gt; wrong isn't cosmetic — it's a documented vulnerability class in Solidity, and it gets worse with structs and arrays.&lt;/p&gt;

&lt;p&gt;If you declare a local variable as &lt;code&gt;storage&lt;/code&gt; when you meant &lt;code&gt;memory&lt;/code&gt; (or the reverse), you're not creating a copy — you're creating a &lt;em&gt;reference&lt;/em&gt; to existing storage. Assign a &lt;code&gt;storage&lt;/code&gt; pointer to the wrong slot, or leave one uninitialized, and you can end up writing to state you never meant to touch. This general class of bug — storage layout confusion, particularly around delegatecall and library contracts — is widely cited in postmortems of real incidents, including the 2017 Parity multisig wallet freeze, where a storage layout issue tied to a library contract left funds permanently inaccessible. I'm citing that as a well-known industry example, not something from my own contract — but it's exactly the category of mistake that starts as "wait, which one is this again?"&lt;/p&gt;

&lt;p&gt;That's the pattern I keep running into three weeks in: things that would be a minor bug in a web app are a fund-draining or fund-freezing bug on-chain, because there's no "restart the server and it's fine" — the mistake is written to the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm taking from this
&lt;/h2&gt;

&lt;p&gt;I've been studying Solidity for about three weeks now, doing it in the opposite order from how I originally learned security: build first, then go back and audit. It's blockchain, smart contracts, tokens, and a syntax that reads like a mix of Python, C++, and PHP — simpler than I expected, but with sharper edges than I'm used to.&lt;/p&gt;

&lt;p&gt;The practical habit I'm building from this: whenever Solidity &lt;em&gt;doesn't&lt;/em&gt; make me declare something, I now ask why not, instead of assuming it's not important. The implicit defaults are often exactly where the design decisions — and the risk — are concentrated.&lt;/p&gt;

&lt;p&gt;If you've made the same shift — from finding vulnerabilities to writing the code that creates them — what tripped you up first? What resources actually moved the needle for you when you were starting out?&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>security</category>
      <category>blockchain</category>
      <category>beginners</category>
    </item>
    <item>
      <title>The One Line of Code That Stops Rainbow Table Attacks</title>
      <dc:creator>Bijan</dc:creator>
      <pubDate>Wed, 02 Sep 2026 17:00:00 +0000</pubDate>
      <link>https://dev.to/bijan53c/the-one-line-of-code-that-stops-rainbow-table-attacks-1mca</link>
      <guid>https://dev.to/bijan53c/the-one-line-of-code-that-stops-rainbow-table-attacks-1mca</guid>
      <description>&lt;p&gt;Someone steals a database of password hashes. Instead of cracking each one individually, they check it against a rainbow table — a massive precomputed list mapping hashes to the passwords that produced them. If a stolen hash shows up in that table, they're done in milliseconds. No cracking required, just a lookup.&lt;/p&gt;

&lt;p&gt;Salting kills this attack entirely, and it's a small enough fix that it's worth understanding exactly why it works rather than just knowing "you're supposed to do it."&lt;/p&gt;

&lt;p&gt;This is part 2 of a series where I'm documenting what I'm learning building &lt;a href="https://github.com/bijan53c/CryptoGraphy" rel="noopener noreferrer"&gt;CryptoGraphy&lt;/a&gt;, a small Python project I'm using to actually understand applied cryptography instead of just calling library functions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a salt actually does
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;main.py&lt;/code&gt;, before any key derivation happens, a fresh random salt is generated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;salt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&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="nf"&gt;derive_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That salt gets fed into Argon2id alongside the password:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&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;master_password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;hash_secret_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;master_password&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;time_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;memory_cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;65536&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;parallelism&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;hash_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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;The effect: two users with the identical password get two completely different derived keys, because their salts are different. Same input password, different salt, unrelated output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this specifically defeats rainbow tables
&lt;/h2&gt;

&lt;p&gt;A rainbow table only works because it's precomputed &lt;em&gt;once&lt;/em&gt; and reused against every hash in a stolen database. That only makes economic sense if the same password always produces the same hash — precompute "password123" → its hash one time, and that entry is useful forever, against any system, any user.&lt;/p&gt;

&lt;p&gt;Add a per-user salt, and that assumption breaks. Now "password123" with salt A produces a completely different output than "password123" with salt B. An attacker would need to precompute a separate rainbow table &lt;em&gt;for every possible salt value&lt;/em&gt;, which — with a 16-byte random salt — is computationally meaningless to attempt. You've turned one attack that scales across every victim into an attack that has to be redone, from scratch, per victim.&lt;/p&gt;

&lt;h2&gt;
  
  
  The salt isn't secret — and that's fine
&lt;/h2&gt;

&lt;p&gt;This is the detail that trips people up: the salt in &lt;code&gt;main.py&lt;/code&gt; isn't hidden anywhere. It's meant to sit right next to the ciphertext (or the stored hash) in plain sight, and that's by design.&lt;/p&gt;

&lt;p&gt;The salt's job was never to be a secret. Its job is to guarantee uniqueness — that no two derivations, even of the same password, ever produce the same result. Secrecy is the key's job. Uniqueness is the salt's job. They're different properties solving different problems, and a salt doesn't stop doing its job just because an attacker can see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is easy to skip and expensive to skip
&lt;/h2&gt;

&lt;p&gt;Skipping the salt costs you nothing while you're learning — the code still runs, the demo still works, the tests still pass. It's only expensive once real data is at stake, because "storing unsalted hashes" is one of those failures that's invisible until a breach happens, at which point it turns an already-bad leak into a catastrophic one: every user with a common password becomes crackable in bulk, instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;os.urandom(16)&lt;/code&gt; call, fed into key derivation, is the difference between "a leaked database is a slow, per-user cracking problem" and "a leaked database is a five-minute rainbow table lookup." It's a genuinely small piece of code carrying a genuinely large amount of the actual security guarantee.&lt;/p&gt;




&lt;p&gt;Next in this series: why I specifically chose Argon2id over faster alternatives like PBKDF2 — and why "slow" is the entire point of a key derivation function.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Have you dealt with a real system that turned out to be storing unsalted hashes?&lt;/strong&gt; Curious how that played out.&lt;/p&gt;

</description>
      <category>cryptography</category>
      <category>python</category>
      <category>security</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
