Derive keys using HKDF with SHA256 in Go
HKDF (HMAC-based Key Derivation Function) is a cryptographic key derivation function that expands a source key material into multiple cryptographically strong keys. Here’s how to implement HKDF with SHA256 in Go using the standard library.
Basic HKDF Implementation
package main
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"golang.org/x/crypto/hkdf"
)
func main() {
// Generate a random source key material (IKM)
ikm := make([]byte, 32)
_, err := rand.Read(ikm)
if err != nil {
fmt.Println("error generating source key:", err)
return
}
// Optional salt (recommended for security)
salt := make([]byte, 32)
_, err = rand.Read(salt)
if err != nil {
fmt.Println("error generating salt:", err)
return
}
// Optional info parameter for context
info := []byte("key derivation context")
// Create HKDF reader
hkdf := hkdf.New(sha256.New, ikm, salt, info)
// Derive multiple keys of different lengths
key1 := make([]byte, 32) // 256-bit key
key2 := make([]byte, 16) // 128-bit key
key3 := make([]byte, 64) // 512-bit key
// Read the derived keys
_, err = io.ReadFull(hkdf, key1)
if err != nil {
fmt.Println("error deriving key1:", err)
return
}
_, err = io.ReadFull(hkdf, key2)
if err != nil {
fmt.Println("error deriving key2:", err)
return
}
_, err = io.ReadFull(hkdf, key3)
if err != nil {
fmt.Println("error deriving key3:", err)
return
}
fmt.Println("Source Key Material:", hex.EncodeToString(ikm))
fmt.Println("Salt:", hex.EncodeToString(salt))
fmt.Println("Derived Key 1 (32 bytes):", hex.EncodeToString(key1))
fmt.Println("Derived Key 2 (16 bytes):", hex.EncodeToString(key2))
fmt.Println("Derived Key 3 (64 bytes):", hex.EncodeToString(key3))
}
Practical Example: Deriving Encryption and MAC Keys
package main
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"golang.org/x/crypto/hkdf"
)
type DerivedKeys struct {
EncryptionKey []byte
MACKey []byte
IV []byte
}
func deriveKeys(masterKey []byte, salt []byte, info string) (*DerivedKeys, error) {
hkdf := hkdf.New(sha256.New, masterKey, salt, []byte(info))
keys := &DerivedKeys{
EncryptionKey: make([]byte, 32), // AES-256 key
MACKey: make([]byte, 32), // HMAC key
IV: make([]byte, 16), // AES IV
}
// Derive encryption key
_, err := io.ReadFull(hkdf, keys.EncryptionKey)
if err != nil {
return nil, fmt.Errorf("failed to derive encryption key: %w", err)
}
// Derive MAC key
_, err = io.ReadFull(hkdf, keys.MACKey)
if err != nil {
return nil, fmt.Errorf("failed to derive MAC key: %w", err)
}
// Derive IV
_, err = io.ReadFull(hkdf, keys.IV)
if err != nil {
return nil, fmt.Errorf("failed to derive IV: %w", err)
}
return keys, nil
}
func main() {
// Master key (could be from password-based key derivation)
masterKey := make([]byte, 32)
_, err := rand.Read(masterKey)
if err != nil {
fmt.Println("error generating master key:", err)
return
}
// Salt should be unique per derivation
salt := make([]byte, 32)
_, err = rand.Read(salt)
if err != nil {
fmt.Println("error generating salt:", err)
return
}
// Derive keys for encryption
keys, err := deriveKeys(masterKey, salt, "encryption-context-v1")
if err != nil {
fmt.Println("error deriving keys:", err)
return
}
fmt.Println("Master Key:", hex.EncodeToString(masterKey))
fmt.Println("Salt:", hex.EncodeToString(salt))
fmt.Println("Encryption Key:", hex.EncodeToString(keys.EncryptionKey))
fmt.Println("MAC Key:", hex.EncodeToString(keys.MACKey))
fmt.Println("IV:", hex.EncodeToString(keys.IV))
}
Usage Example
Here’s the expected output when running the basic HKDF implementation:
Source Key Material: a1b2c3d4e5f67890abcdef1234567890a1b2c3d4e5f67890abcdef1234567890
Salt: 1234567890abcdefa1b2c3d4e5f678901234567890abcdefa1b2c3d4e5f67890
Derived Key 1 (32 bytes): 4f2a8b3c7d9e1f5a6b8c2d4e7f0a1b3c4d5e6f8a9b0c1d2e3f4a5b6c7d8e9f0a
Derived Key 2 (16 bytes): 9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d
Derived Key 3 (64 bytes): 2f4e6d8c0b9a1f3e5d7c9b0a8f6e4d2c0b9a7f5e3d1c9b7a5f3e1d9c7b5a3f1e9d7c5b3a1f9e7d5c3b1a9f7e5d3c1b9a7f5e3d1c9b7a5f3e1d9c7b5a3f1e
For the practical encryption example:
Master Key: 5a4b3c2d1e0f9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b
Salt: f0e1d2c3b4a59687f0e1d2c3b4a59687f0e1d2c3b4a59687f0e1d2c3b4a59687
Encryption Key: 8f7e6d5c4b3a2910fedcba0987654321fedcba0987654321fedcba0987654321
MAC Key: 1a2b3c4d5e6f708192a3b4c5d6e7f809a1b2c3d4e5f6708192a3b4c5d6e7f809
IV: 9f8e7d6c5b4a39281f0e9d8c7b6a5948
Note: These are example outputs with sample values. In real usage, all values will be randomly generated and different each time.
Key Points
- Source Key Material (IKM): The input key material, should have good entropy
- Salt: Optional but recommended, adds randomness and prevents rainbow table attacks
- Info: Optional context information to bind the derived keys to a specific purpose
- Output Length: HKDF can generate up to 255 * HashLen bytes (8160 bytes for SHA256)
HKDF provides a standardized and secure way to derive multiple keys from a single source, making it ideal for protocols that need separate keys for encryption, authentication, and other cryptographic operations.