Key Expansion and Stretching through Key Derivation Function

Introduction

When handling multiple crypto keys I wondered: “More keys to manage means more to defend,” and “How can we prevent users from using very simple keys that make cracking easy?” There’s a dedicated algorithms for these problems: Key Derivation Functions (KDFs).

Key Derivation Function (KDF)

Transform a secret (for example a user-entered password or a single master key) into one or more cryptographically strong keys

Easy to crack key

Key Derivation Function

A Hard to crack key

B Hard to crack key

C Hard to crack key

Key Expansion

Avoid large-scale key management by deriving many keys from a single key via key expansion

By using a master key plus different “info”, you can dynamically derive independent subkeys for each purpose. Even if one subkey is leaked, that does not imply the master key or other-purpose keys can be recovered. This addresses the following problems caused by having many keys:

  • Complex management costs (key rotation, permissions, deployment synchronization)
  • Increased risk of leakage (maintaining consistency in practice)
package main
import (
"crypto/sha256"
"fmt"
"io"
"golang.org/x/crypto/hkdf"
)
func deriveKey(masterKey []byte, info string) ([]byte, error) {
hkdfReader := hkdf.New(
sha256.New,
masterKey,
nil,
[]byte(info), // domain separation, to prevent keys with the same purpose from being generated in different scenarios.
)
key := make([]byte, 32)
_, err := io.ReadFull(hkdfReader, key)
if err != nil {
return nil, err
}
return key, nil
}
func main() {
masterKey := []byte("super-secret-master-key")
jwtKey, _ := deriveKey(masterKey, "jwt-sign")
aesKey, _ := deriveKey(masterKey, "aes-encryption")
fmt.Printf("JWT Key: %x\n", jwtKey)
fmt.Printf("AES Key: %x\n", aesKey)
}

Key Stretching

Prevent weak keys (such as user passwords) from being easily cracked by increasing the cost of cracking through key stretching

Add a “salt” during derivation and make the computation to be very time-consuming or highly memory-intensive. Even if an attacker steals the database, they cannot easily use rainbow tables or brute-force attacks.

package main
import (
"crypto/rand"
"encoding/hex"
"fmt"
"log"
"golang.org/x/crypto/argon2"
)
// GenerateSalt generates a random salt of the specified length
// The salt value does not need to be kept secret, but it should be unique for each user or for each derivative.
func GenerateSalt(size int) ([]byte, error) {
salt := make([]byte, size)
_, err := rand.Read(salt)
if err != nil {
return nil, err
}
return salt, nil
}
func main() {
// Scenario: The user set a very weak password
userPassword := "iloveyou123"
fmt.Printf("Original password entered: %s\n", userPassword)
// 1. Generate a 16-byte random Salt
// In practical case, this Salt will be stored in the database along with the encrypted data or hash.
salt, err := GenerateSalt(16)
if err != nil {
log.Fatalf("Failed to generate Salt: %v", err)
}
// 2. Setting Argon2id parameters (determines cracking difficulty)
// These parameters directly affect computation time and memory consumption, and can be adjusted according to server performance.
time := uint32(1) // Time cost
memory := uint32(64 * 1024) // Memory cost, 64MB
threads := uint8(4) // Degree of parallelism
keyLength := uint32(32) // We expect the output key length to be 32 bytes = 256 bits (suitable for AES-256).
// 3. Execution key derivation
derivedKey := argon2.IDKey([]byte(userPassword), salt, time, memory, threads, keyLength)
// 4. Output Results
fmt.Println("--------------------------------------------------")
fmt.Printf("Generated Salt (Hex) : %s\n", hex.EncodeToString(salt))
fmt.Printf("Derived 256-bit Key: %s\n", hex.EncodeToString(derivedKey))
fmt.Println("--------------------------------------------------")
}

Note that increasing the cost of cracking does not mean a password becomes absolutely secure; a weak password is still a weak password — only the cost of guessing it can be controlled and raised.

Conclusion

KDFs mainly mitigate human shortcomings in key handling: “weak memory” and “not following best practices,” by using algorithms to reduce those risks.

  • Key Derivation Functions
    • Key Expansion: derive multiple child keys from an already-strong master key
    • Key Stretching: turn an easily brute-forced weak password into a high-cost key that significantly raises cracking effort

In practice, it is possible and often necessary to use two rounds of key derivation: first to convert a weak password into a high-cost master key, and then to expand that master key into multiple purpose-specific high-cost keys.

User Password

Key Stretch

Master Key

Key Expansion

Key A

Key B

Key C