CodeKits.shopNew developer offer

Hashing, encoding and signatures for app developers

Distinguish Base64, file hashes, encryption and signatures with small Node tests, then choose the right tool for application security.

By Jonathan · 5 min read

Published

Different security jobs: represent data, detect change and verify a signature
In this article

A value that looks unreadable is not necessarily protected. Base64, a SHA-256 digest and an encrypted message can all look like random text while solving very different problems. Choosing by appearance is how secrets end up merely encoded.

This guide turns the cryptography distinctions in my study notes into a small, repeatable programming exercise. It corrects an important shorthand: hashing is not encryption. The examples use public test text and disposable in-memory keys, never customer information.

Decide which question you need to answer

Requirement Appropriate concept Important limitation
Represent bytes in JSON or text Encoding, such as Base64 Anyone can decode it
Compare exact file contents Cryptographic digest A digest alone does not identify the publisher
Keep recoverable data confidential Encryption with sound key management Anyone holding the decryption capability may recover it
Verify a publisher's signed bytes Digital signature You must trust the correct public key
Verify a login password Dedicated password hashing A fast file digest is not suitable

These mechanisms can work together. A signed download may also have a digest; encrypted bytes are often Base64-encoded for transport. The outer encoding says nothing about the inner design.

Start with a reversible representation

Requires Node.js 20 or later. Run locally, not in a production application's console:

base64-demo.mjs
const bytes = Buffer.from("public practice text", "utf8");
const encoded = bytes.toString("base64");
const recovered = Buffer.from(encoded, "base64").toString("utf8");
 
console.log(encoded);
console.log(recovered);

The second line of output is the original text. There was no key and no password. This is useful when a transport needs printable characters, not when a database value must remain confidential. Encoding is also not sanitisation: decoding untrusted data does not make it safe to insert into HTML, SQL or a shell command.

Hash the exact bytes you intend to compare

digest-demo.mjs
import { createHash } from "node:crypto";
 
const digest = text =>
  createHash("sha256").update(text, "utf8").digest("hex");
 
console.log(digest("hello"));
console.log(digest("hello\n"));

The values differ because the newline is part of the input. This matters when comparing a downloaded archive or a generated manifest: line endings, text encoding and reformatting can change bytes without changing the apparent meaning.

SHA-256 produces a fixed-size digest, not a compressed copy you can decrypt. Collisions are mathematically possible; the security goal is that finding useful collisions or preimages is computationally infeasible under the algorithm's assumptions. Do not describe a matching digest as absolute proof that two arbitrary files must be identical.

For release verification, obtain the expected digest through a trusted channel. If an attacker can replace both the download and its displayed checksum, recomputing the checksum does not authenticate the publisher.

Verify a signature, then test failures

The downloadable example uses Node's built-in Ed25519 support:

signature-demo.mjs
import { generateKeyPairSync, sign, verify } from "node:crypto";
 
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
const bytes = Buffer.from('{"release":"demo-1"}', "utf8");
const signature = sign(null, bytes, privateKey);
 
console.log(verify(null, bytes, publicKey, signature));
console.log(verify(
  null,
  Buffer.from('{"release":"demo-2"}', "utf8"),
  publicKey,
  signature
));

Expect true, then false. Ed25519 uses null for the algorithm argument in these Node calls; do not substitute a separate SHA-256 prehash casually. The Node crypto API documents the key-generation and sign/verify interfaces.

The signature is not encryption. Anyone with the signed bytes can read them. A valid signature means those bytes verify under that key, not that the text is truthful, the signer is trustworthy, or the public key belongs to the person named in a user interface. Production systems need trusted key distribution, rotation, revocation and replay/version rules as well as the primitive.

Do not use this digest as a password database

Passwords often have far less entropy than randomly generated keys. A fast SHA-256 operation is designed to be efficient, which is the wrong goal for resisting repeated password guesses.

Use a maintained password-hashing implementation with appropriate parameters, unique salts and an upgrade strategy. OWASP's password-storage guidance explains Argon2id and alternatives. A salt is not a secret encryption key; it prevents identical passwords from sharing identical stored hashes and frustrates precomputed attacks. Do not invent a home-made sequence of hashes.

Recoverable secrets such as API credentials have a different requirement. Use the existing security tutorials for application-specific storage patterns, and record where decryption keys live and who can access them.

Run the complete lab and understand its limits

Download file tree
hashing-encoding-signatures-explained/
├── README.md
└── test.mjs
Local terminal: run the tests
node --test test.mjs

The tests cover Base64 round-trip, a known SHA-256 result, newline sensitivity, a valid signature, changed content and an unrelated public key. They use only synthetic data and in-memory keys. Passing them is evidence that the demonstrations work, not certification of a secure messaging protocol or production key-management system.

Before choosing a security claim for your app, state the threat: stolen database, malicious server, intercepted connection or compromised device. Each changes what must be protected. Return to the production learning path to place these controls alongside access restrictions, updates and recovery.