Help us shape the future of Go! Take the 2026 Go Developer Survey

luks

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: BSD-3-Clause Imports: 19 Imported by: 2

README

go-fde/luks

luks

ci Go Reference

Pure-Go read/write support for LUKS1 and LUKS2 full-disk encryption containers.

Features

LUKS version 1
Feature Status
Header parsing (592-byte phdr, 8 key slots) ✅
Key derivation: PBKDF2 ✅
Hash specs: sha1, sha256, sha512, ripemd160 ✅
Anti-forensic split/merge (AF) ✅
Cipher: aes-xts-plain64 ✅
Cipher: aes-cbc-essiv:sha256 ✅
Cipher: twofish-*, serpent-* ❌ (extremely rare; out of scope)
Hash specs: sha3-*, blake2b ❌ (very rare; out of scope)
LUKS version 2
Feature Status
Binary superblock + JSON metadata parsing ✅
Key derivation: PBKDF2 ✅
Key derivation: Argon2i ✅
Key derivation: Argon2id ✅
Cipher: aes-xts-plain64 ✅
Cipher: aes-cbc-essiv:sha256 ✅
Segment iv_tweak (non-zero starting sector number) ✅
Integrity (AEAD per-sector tags, --integrity flag) ❌ (complex; out of scope)
Hardware tokens (FIDO2, TPM, smart cards) ❌ (out of scope for passphrase library)
Detached headers ❌ (out of scope)
Backup header fallback (secondary copy at hdrSize/2) ❌ (valid devices always have a readable primary header)

Usage

import "github.com/go-fde/luks"

// Open and unlock a LUKS container.
dev, err := luks.Open("/dev/sdb", []byte("my passphrase"))
if err != nil {
    log.Fatal(err)
}
defer dev.Close()

// Read decrypted payload (offset is relative to the payload start).
buf := make([]byte, 4096)
n, err := dev.ReadAt(buf, 0)

// Write to the payload (encrypt-on-write).
_, err = dev.WriteAt([]byte("hello"), 0)

// Check whether a file looks like a LUKS container.
ok, err := luks.Detect("/path/to/disk.img")

dev.Size() is the payload size the header states. It is 0 when the payload runs to the end of the device, which is the case for every LUKS1 header and for a LUKS2 segment of size "dynamic", what cryptsetup writes. The payload is then deviceSize - dev.PayloadOffset() bytes.

Create a new LUKS1 container

Format and FormatOn initialise a fresh LUKS1 header on an existing file or block device, derive an encryption key from the passphrase, and return an open *Device ready for payload I/O. Container parameters:

Parameter Value
LUKS version 1
Master key size 32 bytes (AES-256)
Cipher aes-xts-plain64
Key derivation PBKDF2-SHA256, 1000 iterations
AF stripes 4000
Key material offset sector 8
Payload offset sector 258 (132 096 bytes from start)
// The file must exist before calling Format.
f, _ := os.Create("disk.luks")
f.Close()

dev, err := luks.Format("disk.luks", []byte("passphrase"))
if err != nil { log.Fatal(err) }
defer dev.Close()

// WriteAt offset is relative to payload start (sector 0 of plaintext).
dev.WriteAt(myData, 0)

FormatOn accepts the same blockRW interface as OpenFrom, allowing LUKS to be initialised directly inside a QCOW2 virtual disk or any other RW backend.

Opening an untrusted image: limits

A LUKS header is read from the image, so it says how much work unlocking costs. Without bounds, a crafted header could ask for 2³² PBKDF2 iterations (hours of CPU) or 4 TiB of Argon2 memory, or it could crash the program: zero Argon2 passes or threads make golang.org/x/crypto/argon2 panic. Every key slot and digest is therefore checked before any work is done for it.

  • Always: Argon2 1 to 4 threads, at most 4 GiB, and at least 8 KiB per thread (cryptsetup's crypt_pbkdf_get_limits and RFC 9106 §3.1). At least one iteration. An anti-forensic split of at most 4000 stripes of a key of at most 128 bytes.
  • The caller's Limits: PBKDF2 iterations, Argon2 passes and memory. Open and OpenFrom use DefaultLimits: 2²⁶ iterations, 1000 passes, 4 GiB. A program opening images from elsewhere (an upload, a browser) can tighten them with OpenLimited / OpenFromLimited.

A refused slot gives an error that wraps ErrLimit and names the slot. A wrong passphrase gives a different error.

dev, err := luks.OpenFromLimited(rw, passphrase, luks.Limits{
	MaxPBKDF2Iterations: 1 << 22,
	MaxArgon2Time:       16,
	MaxArgon2MemoryKiB:  1 << 20, // 1 GiB
})

IV sector number semantics

Sector numbers used for IV computation start at 0 (or the LUKS2 iv_tweak value) for the first payload sector and increment by one per sector, independently of where the payload sits in the file. This matches the behaviour of cryptsetup, making containers created by this library interoperable with the standard Linux tooling.

Compatibility

Containers created or opened by this library are fully interoperable with cryptsetup for the supported feature set above. The supported feature set covers the vast majority of LUKS containers found in practice:

  • LUKS1 with aes-xts-plain64 and sha256 (the cryptsetup default)
  • LUKS1 with aes-cbc-essiv:sha256 and sha1/ripemd160 (legacy containers)
  • LUKS2 with PBKDF2 or Argon2id (the cryptsetup LUKS2 default)

Layering on top of other block devices (raw, QCOW2, …)

OpenFrom allows layering LUKS on top of any read-write-closable block device instead of a plain file. This is useful when the LUKS container lives inside a virtual disk image format such as QCOW2.

The caller passes any value that satisfies:

interface {
    io.ReaderAt
    WriteAt([]byte, int64) (int, error)
    io.Closer
}

LUKS will use it as the underlying storage. When the returned *Device is closed it also closes the underlying block device.

Example: LUKS over QCOW2
import (
    luks      "github.com/go-fde/luks"
    image_qcow2 "github.com/go-diskimages/qcow2"
)

// 1. Open the QCOW2 container.
qdev, err := image_qcow2.OpenDevice("disk.qcow2")
if err != nil { log.Fatal(err) }

// 2. Unlock the LUKS container embedded in the QCOW2 virtual disk.
//    ldev.Close() will also close qdev.
ldev, err := luks.OpenFrom(qdev, []byte("passphrase"))
if err != nil {
    qdev.Close()
    log.Fatal(err)
}
defer ldev.Close()

// 3. Read/write the plaintext payload.
buf := make([]byte, 512)
_, err = ldev.ReadAt(buf, 0)
Creating a LUKS container inside a QCOW2 image

LUKS operates at the raw block level. To embed a LUKS container inside QCOW2:

  1. Create a raw file of the desired size: truncate -s 2G disk.raw
  2. Format with LUKS: cryptsetup luksFormat disk.raw
  3. Convert to QCOW2: qemu-img convert -f raw -O qcow2 disk.raw disk.qcow2

Alternatively, create the QCOW2 image first, write the LUKS header directly into the virtual disk using OpenDevice + WriteAt, then use OpenFrom to unlock it — as the diskimage package does for testing.

Documentation

Overview

Package luks provides pure-Go support for LUKS1 and LUKS2 full-disk encryption.

Supported:

  • LUKS version 1 (key derivation: PBKDF2; ciphers: aes-xts-plain64, aes-cbc-essiv:sha256)
  • LUKS version 2 (key derivation: PBKDF2 and Argon2id/i; ciphers: aes-xts-plain64)

Usage:

dev, err := luks.Open("/dev/sdb", []byte("passphrase"))
if err != nil { log.Fatal(err) }
defer dev.Close()
// use dev.ReadAt / dev.WriteAt to access plaintext payload

Index

Constants

This section is empty.

Variables

View Source
var DefaultLimits = Limits{
	MaxPBKDF2Iterations: 1 << 26,
	MaxArgon2Time:       1000,
	MaxArgon2MemoryKiB:  argon2MaxMemoryKiB,
}

DefaultLimits are what Open and OpenFrom use.

View Source
var ErrLimit = errors.New("luks: the header asks for more than the limits allow")

ErrLimit is a key slot or digest whose parameters a Limits refuses.

Functions

func Detect

func Detect(path string) (bool, error)

Detect returns true if the file at path begins with a LUKS header magic.

Types

type Device

type Device struct {
	// contains filtered or unexported fields
}

Device is an unlocked LUKS device. Its ReadAt and WriteAt methods transparently decrypt/encrypt the payload using the volume key.

func Format

func Format(path string, passphrase []byte) (*Device, error)

Format creates a new LUKS1 container at path, protecting it with passphrase. The file must already exist. Returns an opened Device ready for payload I/O.

func FormatOn

func FormatOn(rw blockRW, passphrase []byte) (*Device, error)

FormatOn creates a new LUKS1 container on rw, protecting it with passphrase. Returns an opened Device ready for payload I/O.

func Open

func Open(path string, passphrase []byte) (*Device, error)

Open opens the LUKS1 or LUKS2 container at path and unlocks it using passphrase. Returns a Device that exposes the plaintext payload.

func OpenFrom

func OpenFrom(rw blockRW, passphrase []byte) (*Device, error)

OpenFrom unlocks a LUKS container from any read-write-closable block device. This allows LUKS to be layered on top of e.g. a QCOW2 device. The caller is responsible for ensuring that rw remains valid for the lifetime of the returned Device; Close on the Device also closes rw.

func OpenFromLimited

func OpenFromLimited(rw blockRW, passphrase []byte, lim Limits) (*Device, error)

OpenFromLimited is OpenFrom with lim instead of DefaultLimits.

func OpenLimited

func OpenLimited(path string, passphrase []byte, lim Limits) (*Device, error)

OpenLimited is Open with lim instead of DefaultLimits.

func (*Device) Close

func (d *Device) Close() error

Close releases the underlying file descriptor.

func (*Device) PayloadOffset added in v0.2.0

func (d *Device) PayloadOffset() int64

PayloadOffset returns the byte offset of the payload in the device: the payload's offset 0 is the device's PayloadOffset.

func (*Device) ReadAt

func (d *Device) ReadAt(p []byte, off int64) (int, error)

ReadAt reads decrypted data from the plaintext payload starting at off. off is relative to the start of the payload (sector 0 of the plaintext).

func (*Device) Size

func (d *Device) Size() int64

Size returns the size of the plaintext payload in bytes, as the header states it. It is 0 when the payload runs to the end of the device -- every LUKS1 header, and a LUKS2 segment of size "dynamic", which is what cryptsetup writes -- and the payload is then the device's size minus PayloadOffset.

func (*Device) WriteAt

func (d *Device) WriteAt(p []byte, off int64) (int, error)

WriteAt encrypts p and writes it to the payload starting at off. off is relative to the start of the payload.

type Limits

type Limits struct {
	// MaxPBKDF2Iterations bounds a PBKDF2 key slot's iterations and a
	// master-key digest's.
	MaxPBKDF2Iterations uint32
	// MaxArgon2Time bounds an Argon2 key slot's passes.
	MaxArgon2Time uint32
	// MaxArgon2MemoryKiB bounds an Argon2 key slot's memory; at most
	// argon2MaxMemoryKiB whatever it says.
	MaxArgon2MemoryKiB uint32
}

Limits bound the work a header may make Open do. A header is read from the image, so it is untrusted: without bounds a key slot can ask for 2^32 PBKDF2 iterations (hours), 4 TiB of Argon2 memory (2^32 KiB), or zero Argon2 rounds or threads (a panic in golang.org/x/crypto/argon2).

Two kinds of bound:

  • Structural, always enforced, from cryptsetup's own limits for the KDFs it writes (lib/crypto_backend/pbkdf_check.c, crypt_pbkdf_get_limits): Argon2 memory at most 4 GiB and 1 to 4 threads (and Argon2's own floor of 8 KiB per thread); at least one iteration; and the anti-forensic split of a key slot at most 4000 stripes (LUKS1 and LUKS2 headers use 4000) of a key of at most 128 bytes.
  • Cost, chosen by the caller: cryptsetup accepts up to 2^32 iterations because it trusts headers it wrote; a program opening images from elsewhere -- an upload, a browser -- should not. DefaultLimits are generous enough for headers calibrated on fast machines.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL