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.

Frequently asked questions

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.

Nuovi articoli, una volta a settimana.

Guide pratiche per sviluppatori. Nessuno spam. Annulla l'iscrizione in qualsiasi momento.

Tools mentioned

Keep reading