Skip to content
All posts

Hashing in 2026: SHA-256, HMAC and PBKDF2 across Node, Python, Go, Rust

Which stdlib function to reach for, which library to avoid, and the one-liner that gets you SHA-256 + HMAC-SHA256 in each of the big runtimes.

DDDev DeskDeveloper Tools EditorPublished April 27, 20261 min readintermediate

# The short version

SHA-256 is the 2026 default for general-purpose hashing. HMAC-SHA256 is the 2026 default for message authentication. Argon2id (or bcrypt) is the 2026 default for passwords. Below are the idiomatic one-liners for each.

<div class="callout callout-warning" role="note"><div class="callout-title">Warning</div><div class="callout-body"><p>Never use plain SHA-256 to hash passwords. SHA-2 is designed to be fast, which is exactly the opposite of what password hashing needs. Use argon2id or bcrypt — they're designed to resist GPU brute-force.</p></div></div>

# Node.js / JavaScript


import { createHash, createHmac, pbkdf2Sync } from "node:crypto";
// SHA-256 hex digest
createHash("sha256").update("hello").digest("hex");
// '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
// HMAC-SHA256
createHmac("sha256", "secret").update("hello").digest("hex");
// 'f31235...'
// PBKDF2 (not ideal for passwords, but stdlib)
pbkdf2Sync("password", "salt", 600_000, 32, "sha256").toString("hex");

For argon2, reach for @node-rs/argon2 or argon2:


import { hash, verify } from "@node-rs/argon2";
const h = await hash("password");  // stores salt + params inside
await verify(h, "password");       // → true

In the browser, SubtleCrypto is the equivalent — async, hex-awkward:


const bytes = new TextEncoder().encode("hello");
const digest = await crypto.subtle.digest("SHA-256", bytes);
const hex = [...new Uint8Array(digest)].map(b => b.toString(16).padStart(2, "0")).join("");

# Python

Stdlib via hashlib and hmac. Argon2 needs argon2-cffi.


import hashlib, hmac
hashlib.sha256(b"hello").hexdigest()
# '2cf24dba...'
hmac.new(b"secret", b"hello", hashlib.sha256).hexdigest()
# 'f31235...'
# PBKDF2 — stdlib
hashlib.pbkdf2_hmac("sha256", b"password", b"salt", 600_000).hex()

For passwords in 2026, use argon2-cffi:


from argon2 import PasswordHasher
ph = PasswordHasher()
h = ph.hash("password")  # stores salt + params
ph.verify(h, "password")  # raises on mismatch

# Go

Stdlib via crypto/sha256 and crypto/hmac. Argon2 lives in golang.org/x/crypto/argon2.


import (
  "crypto/hmac"
  "crypto/sha256"
  "encoding/hex"
)
// SHA-256
h := sha256.Sum256([]byte("hello"))
hex.EncodeToString(h[:])
// "2cf24dba..."
// HMAC-SHA256
mac := hmac.New(sha256.New, []byte("secret"))
mac.Write([]byte("hello"))
hex.EncodeToString(mac.Sum(nil))
// "f31235..."

<div class="callout callout-tip" role="note"><div class="callout-title">Tip</div><div class="callout-body"><p>Use <code>hmac.Equal</code> to compare MACs, not <code>==</code> or <code>bytes.Equal</code>. It's constant-time — defeats timing attacks that can leak the signature byte-by-byte.</p></div></div>

# Rust

Most ecosystems use the sha2 + hmac crates from the RustCrypto organisation.


use sha2::{Sha256, Digest};
use hmac::{Hmac, Mac};
// SHA-256
let mut h = Sha256::new();
h.update(b"hello");
let digest = h.finalize();
hex::encode(digest);
// "2cf24dba..."
// HMAC-SHA256
type HmacSha256 = Hmac<Sha256>;
let mut mac = HmacSha256::new_from_slice(b"secret").unwrap();
mac.update(b"hello");
let sig = mac.finalize().into_bytes();
hex::encode(sig);
// "f31235..."

For passwords, argon2:


use argon2::{Argon2, PasswordHasher, PasswordVerifier, password_hash::SaltString};
use rand::rngs::OsRng;
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2.hash_password(b"password", &salt).unwrap().to_string();

# Browser (no Node)

Only stdlib available is SubtleCrypto, async-only.


// SHA-256
const bytes = new TextEncoder().encode("hello");
const digest = await crypto.subtle.digest("SHA-256", bytes);
// HMAC-SHA256
const key = await crypto.subtle.importKey(
  "raw", new TextEncoder().encode("secret"),
  { name: "HMAC", hash: "SHA-256" }, false, ["sign"],
);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode("hello"));

Our Hash Generator and HMAC Generator use exactly this API — everything runs client-side, your secrets never touch a server.

# Quick comparison

| Task | Node | Python | Go | Rust |

|-------------------|-----------------|----------------|-----------------|---------------------|

| SHA-256 | createHash | hashlib.sha256| sha256.Sum256 | Sha256::new |

| HMAC-SHA256 | createHmac | hmac.new | hmac.New | Hmac::new_from_slice |

| PBKDF2 | pbkdf2Sync | pbkdf2_hmac | pbkdf2.Key | pbkdf2::pbkdf2 |

| Argon2 (preferred)| @node-rs/argon2| argon2-cffi | x/crypto/argon2| argon2 crate |

# Common bugs

1. Using === or == to compare HMACs. Timing-safe comparison only. Node's crypto.timingSafeEqual, Go's hmac.Equal, Python's hmac.compare_digest, Rust's subtle::ConstantTimeEq.

2. Forgetting to encode strings before hashing. Every SHA-256 implementation takes bytes, not strings. UTF-8 decode explicitly.

3. Reusing the HMAC key object between calls. Some implementations are stateful; always construct a fresh mac per message.

4. Hashing passwords with SHA-256. Fast = bad for passwords. A $500 GPU cracks billions of SHA-256 guesses per second.

# Verify it

Drop any text into our Hash Generator — SHA-1, 256, 384, 512 computed locally in your browser. For signed authentication, HMAC Generator takes a secret + message.

Common questions

Frequently asked.

Should I still use SHA-1?

Not for signatures or content-addressed IDs. For HMAC (which doesn't need collision resistance) it's fine but obsolete — SHA-256 is just as fast on modern CPUs. Git still uses SHA-1 internally, which is why it's pinned to strict enforcement mode.

Is MD5 ever okay?

As a non-security hash (cache keys, bloom filters, ETags) — yes, it's fast and fine. For passwords, signatures, or integrity checks against a motivated attacker — absolutely not.

What about bcrypt / argon2 for passwords?

Different category. Those are password-hashing functions designed to be slow and memory-hard. SHA-256 is a general-purpose hash — fast enough that an attacker with a GPU can brute-force your password hashes. Use argon2id for new code.

Haftada bir kez yeni gönderiler.

Pratik geliştirici rehberleri. Spam yok. İstediğiniz zaman abonelikten çıkabilirsiniz.

Tools mentioned

Pick up where the post leaves off.

Keep reading

More from the field notes.