veracrypt

package module
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Nov 11, 2026 License: BSD-3-Clause Imports: 21 Imported by: 0

README

veracrypt

Read VeraCrypt and TrueCrypt volumes in pure Go.

dev, err := veracrypt.Open("volume.hc", veracrypt.Options{
	Password: []byte("password"),
	// Keyfiles: [][]byte{kf1, kf2}, PIM: 485, Hidden: true, …
})
if err != nil {
	log.Fatal(err)
}
defer dev.Close()
boot := make([]byte, 512)
_, err = dev.ReadAt(boot, 0) // the filesystem's first sector

OpenFrom(r, size, opts) reads from any io.ReaderAt, such as a partition or a virtual disk.

How a volume opens

A VeraCrypt header looks like random bytes: nothing on disk names the key derivation or the cipher. Opening a volume therefore works by trial, as in cryptsetup's tcrypt support:

  1. Every allowed KDF derives a key from the password and keyfiles.
  2. Every allowed cipher chain decrypts the header with that key.
  3. The trial stops when the header starts with VERA (TRUE for TrueCrypt) and its two CRC-32 checks match.

A KDF costs about half a second (500,000 PBKDF2 iterations, or Argon2id at 416 MiB). Naming the KDF and the cipher in Options skips the others.

What it reads

Ciphers (XTS) every XTS chain VeraCrypt offers: AES, Camellia, Kuznyechik, Serpent, Twofish, and the cascades AES-Twofish, AES-Twofish-Serpent, Camellia-Kuznyechik, Camellia-Serpent, Kuznyechik-AES, Kuznyechik-Serpent-Camellia, Kuznyechik-Twofish, Serpent-AES, Serpent-Twofish-AES, Twofish-Serpent (cryptsetup's names list them in reverse: twofish-aes, kuznyechik-camellia, …)
KDFs PBKDF2 with SHA-512, SHA-256, BLAKE2s-256 or RIPEMD-160; Argon2id; TrueCrypt's PBKDF2-SHA-1 with Legacy
Options PIM, keyfiles (up to 1 MiB each), passwords up to 128 bytes
Volumes normal, hidden, backup headers; VeraCrypt and TrueCrypt 5.0 and later

The following are not supported yet. A volume that uses them fails like a wrong password, because nothing on disk can tell the two apart.

  • The Whirlpool and Streebog KDFs (go-fde/fde#28).
  • TrueCrypt's LRW and CBC modes, and headers older than version 3.

These are refused with ErrUnsupported:

  • system encryption;
  • sector sizes other than 512 bytes.

Writing is not implemented: WriteAt returns ErrReadOnly.

How it is checked

The tests open the images in cryptsetup's tests/tcrypt-images.tar.xz, which VeraCrypt and TrueCrypt made, with the passwords, PIMs and keyfiles of tests/tcrypt-compat-test. The FAT filesystem inside must have the UUID that test checks: DEAD-BABE, or CAFE-BABE in a hidden volume.

  • 59 volumes open and match, covering:
    • VeraCrypt and TrueCrypt;
    • SHA-512, SHA-256, BLAKE2s, RIPEMD-160 and Argon2id;
    • PIM, keyfiles, and a 72-byte password;
    • hidden volumes, including TrueCrypt 5's;
    • every cipher alone, and seven of the ten cascades.
  • The 31 volumes using primitives not supported yet must be refused rather than misread.

Reversing the cascade's decryption order fails all 22 cascade volumes, so the order is checked. Serpent's and Kuznyechik's byte orders are confirmed by the same images.

Three cascades have no volume in the corpus: Camellia-Serpent, Kuznyechik-AES and Kuznyechik-Twofish. They are checked only by the test writer's round trip, with cryptsetup's key offsets.

The corpus file names do not list the ciphers in one order. Most use VeraCrypt's display order, but the Kuznyechik cascades use cryptsetup's. Two chains are each other's reverse (serpent-twofish-aes and aes-twofish-serpent), so the test tries every chain a name can mean, and the FAT UUID decides.

The archive is GPL-2.0+, so it is fetched at test time and never committed. Pinned by commit and SHA-256, it is read through TCRYPT_IMAGES:

TCRYPT_IMAGES=/path/to/tcrypt-images.tar.xz go test ./...

Without it the interoperability test skips, and says so. A test-only writer builds volumes for the unit tests, which reach every refusal and every limit. CI requires 100 % coverage.

Untrusted images

Trial decryption is a cost an attacker controls: a large PIM multiplies PBKDF2's iterations and Argon2id's memory. Limits bounds the number of derivations, the PIM (2000 by default), the PBKDF2 iterations and the Argon2id memory (1 GiB by default). An open is refused with ErrLimit before any derivation that would exceed them.

Once decrypted, the header is checked before use:

  • both CRC-32s;
  • the data area lies inside the device;
  • every offset is aligned to the sector size.

FuzzParse fuzzes those checks.

Performance

  • AES volumes decrypt through go-encryptions/xts v0.2.0 DecryptSectors, which is ahead of OpenSSL on one core.
  • Twofish goes through golang.org/x/crypto/twofish one block at a time. It has not been measured against a reference implementation yet (veracrypt#3).
  • Serpent goes through go-encryptions/serpent v0.1.0, bitsliced pure Go, one block at a time. It is 2.1× (encryption) and 2.5× (decryption) slower than Botan on one core, a gap tracked in go-encryptions/serpent#2.
  • Camellia goes through go-encryptions/camellia v0.1.0, pure Go, one block at a time. On one core it is level with Botan's scalar build, and 1.12× faster than its default build.
  • Kuznyechik goes through go-encryptions/kuznyechik v0.1.0, pure Go with lookup tables. On one core it is 1.14× (encryption) and 1.16× (decryption) faster than Botan 3.14.1.
  • The KDFs cost what the format specifies; that cost is the point of the format.

References

  • cryptsetup lib/tcrypt/tcrypt.c: the KDF and cipher tables, the key offsets, the header layout and the data offsets.
  • The VeraCrypt documentation (veracrypt.io): Volume Format Specification, Header Key Derivation, PIM, Encryption Algorithms, Cascades.

Licence

BSD-3-Clause. The test corpus is cryptsetup's (GPL-2.0+) and is not part of this repository.

Documentation

Overview

Package veracrypt reads VeraCrypt and TrueCrypt volumes in pure Go.

A VeraCrypt header is indistinguishable from random bytes: nothing on disk names the key derivation or the cipher. Opening a volume therefore tries every allowed KDF on the password (and keyfiles), and every allowed cipher chain on each derived key, until the decrypted header starts with "VERA" (or TrueCrypt's "TRUE") and its CRC-32s match, as cryptsetup's tcrypt support does. Naming the KDF and the cipher in Options skips the others, which matters: one KDF costs about half a second.

dev, err := veracrypt.Open("volume.hc", veracrypt.Options{Password: []byte("pw")})

The decrypted volume starts at its filesystem's first byte. This version reads AES and Twofish (and VeraCrypt's AES-Twofish cascade) in XTS mode, with PBKDF2 over SHA-512, SHA-256, BLAKE2s-256 or RIPEMD-160, or Argon2id; PIM, keyfiles, hidden volumes and backup headers. It does not write.

Untrusted images: the trial costs as much as the caller allows; Limits bounds the number of key derivations, the PBKDF2 iterations and the Argon2id memory a PIM can ask for, and refuses before deriving.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrWrongSecret = errors.New("veracrypt: no KDF and cipher open the header with this secret")
	ErrCorrupt     = errors.New("veracrypt: corrupt volume")
	ErrUnsupported = errors.New("veracrypt: unsupported volume")
	ErrLimit       = errors.New("veracrypt: limit exceeded")
	ErrReadOnly    = errors.New("veracrypt: read-only")
)

Errors. Every refusal of a malformed image wraps ErrCorrupt.

Functions

This section is empty.

Types

type Cipher

type Cipher string

Cipher names an XTS cipher or cascade, as cryptsetup names it: the ciphers in the order of its table, which is the REVERSE of VeraCrypt's display name (VeraCrypt's "AES-Twofish" is "twofish-aes" here).

const (
	AES                       Cipher = "aes"
	Serpent                   Cipher = "serpent"
	Twofish                   Cipher = "twofish"
	TwofishAES                Cipher = "twofish-aes"
	SerpentTwofishAES         Cipher = "serpent-twofish-aes"
	AESSerpent                Cipher = "aes-serpent"
	AESTwofishSerpent         Cipher = "aes-twofish-serpent"
	SerpentTwofish            Cipher = "serpent-twofish"
	Camellia                  Cipher = "camellia"
	Kuznyechik                Cipher = "kuznyechik"
	KuznyechikCamellia        Cipher = "kuznyechik-camellia"
	TwofishKuznyechik         Cipher = "twofish-kuznyechik"
	SerpentCamellia           Cipher = "serpent-camellia"
	AESKuznyechik             Cipher = "aes-kuznyechik"
	CamelliaSerpentKuznyechik Cipher = "camellia-serpent-kuznyechik"
)

The ciphers this version decrypts: every XTS chain of cryptsetup's table.

type Device

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

Device is an unlocked volume. ReadAt is safe for concurrent use.

func Open

func Open(path string, o Options) (*Device, error)

Open opens the volume at path read-only.

func OpenFrom

func OpenFrom(r ReadAtCloser, size int64, o Options) (*Device, error)

OpenFrom unlocks the volume of size bytes in r. Close on the returned Device closes r.

func (*Device) Close

func (d *Device) Close() error

Close closes the underlying reader.

func (*Device) Header

func (d *Device) Header() *Header

Header returns what the volume header records.

func (*Device) ReadAt

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

ReadAt reads the decrypted volume at off: the whole-sector span with one read, decrypted in one pass per cipher.

func (*Device) Size

func (d *Device) Size() int64

Size is the decrypted volume's length.

func (*Device) WriteAt

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

WriteAt is not implemented.

type Header struct {
	VeraCrypt  bool // magic "VERA"; false for TrueCrypt's "TRUE"
	Version    uint16
	MinVersion uint16
	HiddenSize uint64
	VolumeSize uint64
	MKOffset   uint64 // the data area's start on the device
	MKSize     uint64
	Flags      uint32
	SectorSize uint32
	KDF        string // the variant that opened it, e.g. pbkdf2-sha512-500000
	Cipher     Cipher
	// contains filtered or unexported fields
}

Header is what the decrypted header records, big-endian on disk (cryptsetup TCRYPT_hdr_from_disk).

func (*Header) System

func (h *Header) System() bool

System reports the system-encryption flag (bit 0).

type KDF

type KDF string

KDF names a header key derivation, as cryptsetup names them.

const (
	SHA512    KDF = "sha512"
	SHA256    KDF = "sha256"
	BLAKE2s   KDF = "blake2s-256"
	RIPEMD160 KDF = "ripemd160"
	Argon2id  KDF = "argon2id"
	SHA1      KDF = "sha1" // TrueCrypt legacy only
)

The KDFs this version derives with. Whirlpool and Streebog are not implemented yet (no Go implementation in the fleet); a volume made with them fails like a wrong password.

type Limits

type Limits struct {
	// MaxKeyDerivations is how many KDFs may run; default: all of them.
	MaxKeyDerivations int
	// MaxPIM bounds the PIM; default 2000 (2,015,000 PBKDF2 iterations).
	MaxPIM int
	// MaxIterations bounds the PBKDF2 iterations of one derivation;
	// default 2,015,000.
	MaxIterations int
	// MaxArgon2MemKiB bounds Argon2id's memory; default 1 GiB, the most a
	// VeraCrypt PIM asks for.
	MaxArgon2MemKiB uint32
}

Limits bounds the work an open may do. Zero fields take the defaults.

type Options

type Options struct {
	Password []byte
	// Keyfiles are the contents of the keyfiles, in the order VeraCrypt
	// was given them; only the first MiB of each counts.
	Keyfiles [][]byte
	// PIM is VeraCrypt's Personal Iterations Multiplier; 0 is the
	// default. A non-zero PIM tries only VeraCrypt KDFs.
	PIM int
	// KDF and Cipher, when set, are the only ones tried.
	KDF    KDF
	Cipher Cipher
	// Hidden opens the hidden volume (its header at 64 KiB).
	Hidden bool
	// Backup reads the backup header at the end of the device instead.
	Backup bool
	// TrueCryptOnly refuses VeraCrypt's "VERA" volumes and KDFs.
	TrueCryptOnly bool
	// Legacy also tries TrueCrypt's oldest KDF (PBKDF2-SHA-1).
	Legacy bool
	Limits Limits
}

Options says how to open a volume.

type ReadAtCloser

type ReadAtCloser interface {
	io.ReaderAt
	io.Closer
}

ReadAtCloser is what OpenFrom reads.

Jump to

Keyboard shortcuts

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