Public Key Cryptography 101: From Bits to Bitcoin Wallets
Abir Dutta
Published on Thursday, Sep 11, 2025
Okkay so here's the thing, I've been diving deep into blockchain development lately and realized that most developers (including me like 6 months ago) jump straight into writing smart contracts without understanding the cryptographic foundation that makes everything work. It's like trying to build a house without understanding why we need a foundation - sure, it might work for a while, but eventually things get shaky :))
After spending weeks reading papers, breaking my head over elliptic curves, and debugging wallet integrations, I thought let me write down everything I wish someone had explained to me when I started. This blog is for developers who want to understand the "why" behind the crypto magic, not just the "how."
So grab your favourite beverage and let's decode this together (pun intended)!
The "Why am I even learning this?" moment
Before we dive in, let me ask you something - have you ever wondered how your MetaMask wallet generates those 12 words that can control millions of dollars? Or why we can't just use regular passwords for blockchain transactions?
Understanding public key cryptography isn't just about showing off in developer meetups (though that's a nice bonus). It's the foundation that makes trustless systems possible. Without it, there's no Bitcoin, no Ethereum, no Solana, no DeFi, no NFTs - basically no decentralised internet.
Public and Private Keys: Your Digital Identity
Think of public key cryptography like having a magical mailbox. You give everyone the address (public key) so they can send you letters, but only you have the key (private key) to open it and read what's inside.
// This is what a typical key pair looks like (simplified example)
interface KeyPair {
publicKey: string; // Safe to share with the world
privateKey: string; // Keep this SECRET, seriously!
}
// Example key pair (don't use these in production!)
const myWallet: KeyPair = {
publicKey: "04a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd5b8dec5235a0fa8722476c7709c02559e3aa73aa03918ba2d492eea75abea235",
privateKey: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
Here's the mind-blowing part - anyone can encrypt a message using your public key, but ONLY your private key can decrypt it. It's like having a lock that anyone can close, but only you can open.
The Wallet Adapter Trust Issue
Now here's something that kept me up at night when I first learned this - most wallet adapters (like MetaMask browser extension, Phantom, etc.) actually have access to your private key. Yeah, I know, scary right?
This is why the crypto community is obsessed with open source wallets. When the code is public, thousands of developers can audit it and make sure there's no funny business going on. Trust, but verify - that's the motto.
Some wallets I trust (because of their open source nature and strong community):
MetaMask (open source, huge community)
Phantom (Solana ecosystem, good reputation)
Hardware wallets like Ledger (private keys never leave the device)
Bits vs Bytes: The Digital Foundation
Alright, let's get a bit (pun intended again) technical. Everything in computers eventually becomes 0s and 1s - bits. But when we're dealing with cryptographic operations, we work with bytes (8 bits each).
// A simple example of how data gets converted
const myMessage = "Hello Blockchain!";
// This string gets converted to bytes, then to bits
// "H" = 72 in ASCII = 01001000 in binary
// "e" = 101 in ASCII = 01100101 in binary
// and so on...
console.log(myMessage); // "Hello Blockchain!"
console.log(Buffer.from(myMessage)); // <Buffer 48 65 6c 6c 6f 20 42 6c 6f 63 6b 63 68 61 69 6e 21>
Numbers are straightforward - they're already mathematical, so converting them to binary is easy. But strings? That's where encodings come in, and it gets interesting (and sometimes frustrating).
Why Uint8Array instead of regular arrays?
Great question! I struggled with this too. Here's the deal:
// Regular JavaScript array - flexible but memory hungry
const regularArray = [1, 2, 3, 256, 1000, "hello"]; // Can store anything
console.log(regularArray); // [1, 2, 3, 256, 1000, "hello"]
// Uint8Array - fixed size, memory efficient, crypto-friendly
const uint8Array = new Uint8Array([1, 2, 3, 255]); // Only 0-255 values
console.log(uint8Array); // Uint8Array(4) [1, 2, 3, 255]
// Why use Uint8Array for crypto?
// 1. Fixed memory layout - predictable performance
// 2. Direct byte manipulation - perfect for crypto operations
// 3. No type coercion surprises - 255 + 1 = 0 (wraps around)
// 4. Works directly with crypto APIs
Pros of Uint8Array:
Memory efficient (exactly 1 byte per element)
Fast operations (no type checking)
Perfect for binary data
Direct crypto API compatibility
Cons:
Limited to 0-255 values only
Less flexible than regular arrays
Can't store different data types together
Encodings: The Rosetta Stone of Digital Communication
Encodings are like different languages for representing the same information. Let me break down the famous ones:
ASCII - The OG Encoding
// ASCII: Maps characters to numbers (0-127)
const asciiDemo = () => {
const char = 'A';
const asciiCode = char.charCodeAt(0); // 65
const backToChar = String.fromCharCode(65); // 'A'
console.log(`'${char}' in ASCII is ${asciiCode}`);
// Output: 'A' in ASCII is 65
}
ASCII is simple but limited - only English characters and basic symbols.
Base64 Encoding - The Email Friendly One
// Base64: Converts binary data to text using 64 characters
// Uses: A-Z, a-z, 0-9, +, / (and = for padding)
const base64Demo = () => {
const original = "Hello Crypto!";
const encoded = Buffer.from(original).toString('base64');
const decoded = Buffer.from(encoded, 'base64').toString();
console.log(`Original: ${original}`); // "Hello Crypto!"
console.log(`Base64: ${encoded}`); // "SGVsbG8gQ3J5cHRvIQ=="
console.log(`Decoded: ${decoded}`); // "Hello Crypto!"
}
// Why Base64?
// - Safe for email/URLs (no special characters that break things)
// - Increases size by ~33% (trade-off for safety)
// - Human readable (sort of)
Base16 (Hexadecimal) - The Developer's Friend
// Base16: Uses 0-9 and A-F (16 characters total)
const hexDemo = () => {
const original = "Crypto";
const hex = Buffer.from(original).toString('hex');
const backToOriginal = Buffer.from(hex, 'hex').toString();
console.log(`Original: ${original}`); // "Crypto"
console.log(`Hex: ${hex}`); // "437279707466"
console.log(`Back: ${backToOriginal}`); // "Crypto"
}
// Why Hex?
// - Compact representation (4 bits per character)
// - Easy to read for developers
// - Common in crypto (addresses, hashes)
Base58 - Bitcoin's Choice
// Base58: Like Base64 but removes confusing characters
// Removes: 0 (zero), O (capital o), I (capital i), l (lowercase L)
// Why? To avoid confusion when writing down addresses!
const base58Characters = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
// Bitcoin addresses use Base58Check (Base58 + checksum)
// Example Bitcoin address: 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
// Example Solana address: 11111111111111111111111111111112
console.log("Base58 makes addresses human-friendly and reduces errors!");
Hashing vs Encryption: The Great Confusion Cleared
This confused me for WEEKS when I started, so let me save you the headache:
Hashing = One-way street. You can't get the original data back. Encryption = Two-way street. You can decrypt to get original data back.
// HASHING - One way only!
import { createHash } from 'crypto';
const hashingDemo = () => {
const data = "My secret password";
const hash = createHash('sha256').update(data).digest('hex');
console.log(`Original: ${data}`);
console.log(`SHA256 Hash: ${hash}`);
// You CANNOT get "My secret password" back from the hash!
}
// ENCRYPTION - Two way process!
const encryptionDemo = () => {
const data = "My secret message";
const key = "mySecretKey123";
// Simplified example (don't use this in production)
const encrypted = simpleEncrypt(data, key);
const decrypted = simpleDecrypt(encrypted, key);
console.log(`Original: ${data}`);
console.log(`Encrypted: ${encrypted}`);
console.log(`Decrypted: ${decrypted}`); // Gets back original!
}
Famous Hash Functions
SHA-256: Bitcoin's choice, very secure
Keccak-256: Ethereum's choice
Blake2: Fast and secure
MD5: Old and broken, don't use!
Symmetric vs Asymmetric Encryption
Symmetric Encryption: Same key for encryption and decryption
Examples: AES, DES, ChaCha20
Fast but key sharing is a problem
Asymmetric Encryption: Different keys for encryption and decryption
Examples: RSA, ECC (Elliptic Curve), Ed25519
Slower but solves the key sharing problem
// Symmetric - Same key for both operations
const symmetricExample = {
key: "shared-secret-key",
encrypt: (data: string) => `encrypted_${data}_with_${symmetricExample.key}`,
decrypt: (encrypted: string) => encrypted.replace(`encrypted_`, '').replace(`_with_${symmetricExample.key}`, '')
}
// Asymmetric - Different keys
const asymmetricExample = {
publicKey: "public-key-123",
privateKey: "private-key-456",
encrypt: (data: string, pubKey: string) => `encrypted_${data}_with_${pubKey}`,
decrypt: (encrypted: string, privKey: string) => encrypted.replace('encrypted_', '').replace(`_with_${asymmetricExample.publicKey}`, '')
}
HD Wallets: One Seed to Rule Them All
HD (Hierarchical Deterministic) wallets blew my mind when I first understood them. Imagine having ONE master password that can generate infinite secure accounts. That's HD wallets for you!
// Conceptual example of how HD wallets work
interface HDWallet {
seedPhrase: string[]; // 12-24 words
masterSeed: Uint8Array; // Generated from seed phrase
// Can generate unlimited key pairs from master seed
generateKeyPair(derivationPath: string): KeyPair;
}
// Example seed phrase (NEVER use this in real applications!)
const exampleSeedPhrase = [
"abandon", "ability", "able", "about", "above", "absent",
"absorb", "abstract", "absurd", "abuse", "access", "accident"
];
console.log("This 12-word phrase can generate billions of wallets!");
BIP-39: The Magic Behind Seed Phrases
BIP-39 (Bitcoin Improvement Proposal 39) standardized how we go from human-readable words to cryptographic seeds. Here's the process:
// Step-by-step BIP-39 process (simplified)
class BIP39Demo {
// Step 1: Generate entropy (random data)
generateEntropy(): Uint8Array {
// 128 bits of randomness for 12-word phrase
// 256 bits of randomness for 24-word phrase
return crypto.getRandomValues(new Uint8Array(16)); // 128 bits
}
// Step 2: Add checksum to entropy
addChecksum(entropy: Uint8Array): string {
// Add SHA-256 hash bits as checksum
const hash = createHash('sha256').update(entropy).digest();
// Take first 4 bits of hash for 12-word phrase
return "entropy + checksum bits";
}
// Step 3: Convert to mnemonic words
entropyToMnemonic(entropyWithChecksum: string): string[] {
// Split into 11-bit groups
// Each group maps to word from 2048-word list
return ["word1", "word2", "...", "word12"];
}
// Step 4: Mnemonic to seed
mnemonicToSeed(mnemonic: string[], passphrase: string = ""): Uint8Array {
// PBKDF2 with 2048 iterations
// Salt = "mnemonic" + passphrase
return new Uint8Array(64); // 512-bit seed
}
// Step 5: Seed to master private key
seedToMasterKey(seed: Uint8Array): KeyPair {
// HMAC-SHA512 with "ed25519 seed" as key
return {
publicKey: "master_public_key",
privateKey: "master_private_key"
};
}
}
Derivation Paths: The Address Tree
This is where HD wallets get really clever. Instead of generating random keys, they use a tree structure where each "branch" represents a different account or purpose.
// Standard derivation paths
const derivationPaths = {
// Bitcoin Legacy: m/44'/0'/0'/0/0
bitcoinLegacy: "m/44'/0'/0'/0/0",
// Ethereum: m/44'/60'/0'/0/0
ethereum: "m/44'/60'/0'/0/0",
// Solana: m/44'/501'/0'/0'
solana: "m/44'/501'/0'/0'",
};
// Breaking down the path: m/44'/60'/0'/0/0
// m = master key
// 44' = purpose (BIP-44 standard)
// 60' = coin type (Ethereum's coin type)
// 0' = account (first account)
// 0 = change (external chain)
// 0 = address index (first address)
class DerivationExample {
generateWallet(derivationPath: string, accountIndex: number = 0): KeyPair {
// This would use the actual cryptographic derivation
// Each step in the path creates a new key pair
const pathSteps = derivationPath.split('/');
let currentKey = "master_seed";
pathSteps.forEach(step => {
if (step === 'm') return; // Skip master indicator
// Hardened derivation (') vs non-hardened
const isHardened = step.includes("'");
const index = parseInt(step.replace("'", ""));
// Cryptographic magic happens here
currentKey = `derived_key_${index}_${isHardened ? 'hardened' : 'normal'}`;
});
return {
publicKey: `pub_${currentKey}`,
privateKey: `priv_${currentKey}`
};
}
}
// The breakthrough: Same seed, unlimited wallets!
const wallet = new DerivationExample();
const account1 = wallet.generateWallet("m/44'/60'/0'/0/0"); // First Ethereum account
const account2 = wallet.generateWallet("m/44'/60'/0'/0/1"); // Second Ethereum account
const account3 = wallet.generateWallet("m/44'/60'/1'/0/0"); // Different account branch
console.log("All different wallets, same seed phrase! 🤯");
The Breakthrough Moment
The genius of derivation paths is standardization. Now:
All wallets can derive the same addresses from the same seed
You can have different accounts for different purposes
Hardware wallets can generate addresses without exposing the master seed
Recovery is universal - any BIP-44 compatible wallet can restore your funds
Putting It All Together: The Full Picture
Let me tie everything together with a real-world example of what happens when you create a new wallet:
class CryptoWalletCreation {
createNewWallet(): HDWallet {
// 1. Generate random entropy
const entropy = crypto.getRandomValues(new Uint8Array(16));
console.log("Generated random entropy:", entropy);
// 2. Convert to mnemonic (12 words)
const seedPhrase = this.entropyToMnemonic(entropy);
console.log("Seed phrase:", seedPhrase.join(' '));
// 3. Seed phrase to master seed
const masterSeed = this.mnemonicToSeed(seedPhrase);
console.log("Master seed generated (kept secret!)");
// 4. Generate first Ethereum account
const ethAccount = this.deriveAccount(masterSeed, "m/44'/60'/0'/0/0");
console.log("Ethereum address:", ethAccount.publicKey);
// 5. Generate first Solana account
const solAccount = this.deriveAccount(masterSeed, "m/44'/501'/0'/0'");
console.log("Solana address:", solAccount.publicKey);
return {
seedPhrase,
masterSeed,
generateKeyPair: (path: string) => this.deriveAccount(masterSeed, path)
};
}
// Helper methods (simplified for demo)
entropyToMnemonic(entropy: Uint8Array): string[] {
return ["abandon", "ability", "able", "about", "above", "absent", "absorb", "abstract", "absurd", "abuse", "access", "accident"];
}
mnemonicToSeed(mnemonic: string[]): Uint8Array {
return new Uint8Array(64); // 512-bit seed
}
deriveAccount(seed: Uint8Array, path: string): KeyPair {
return {
publicKey: `derived_public_key_for_${path}`,
privateKey: `derived_private_key_for_${path}`
};
}
}
// Create a new wallet
const walletCreator = new CryptoWalletCreation();
const myWallet = walletCreator.createNewWallet();
console.log("Congratulations! You now have a multi-chain HD wallet! 🎉");
Final Thoughts: The Rabbit Hole Never Ends
Understanding public key cryptography is like learning to see the Matrix code. Once you get it, you start noticing it everywhere - from HTTPS certificates to Git commits to blockchain transactions.
I've tried to cover the essentials here, but honestly, this is just scratching the surface. There's so much more to explore: elliptic curve mathematics, zero-knowledge proofs, post-quantum cryptography, and more.
The most important thing I learned in this journey? Don't try to understand everything at once. Pick one concept, play with it, break it, fix it, then move to the next. That's how you build real understanding, not just theoretical knowledge.
Also, one last piece of advice - always verify what you learn by implementing it. The code examples I've shown are simplified for understanding, but go ahead and build a real HD wallet, implement your own hash function, play with different encodings. That's when the "aha!" moments really happen :))
Keep the seed phrases safe, trust open source, and remember - in crypto, you are your own bank. With great power comes great responsibility!
By Abir
P.S. - If this helped you understand crypto better, drop me a message or share it with someone who's struggling with these concepts. Let's make the decentralized future accessible to everyone!