Skip to content

Commit d6aebe6

Browse files
ncrucesben-krieger
andauthored
AES-XTS VFS (#171)
Co-authored-by: Ben Krieger <[email protected]>
1 parent 714ea0e commit d6aebe6

File tree

15 files changed

+660
-25
lines changed

15 files changed

+660
-25
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package adiantum
1+
package util
22

33
func abs(n int) int {
44
if n < 0 {
@@ -7,16 +7,16 @@ func abs(n int) int {
77
return n
88
}
99

10-
func gcd(m, n int) int {
10+
func GCD(m, n int) int {
1111
for n != 0 {
1212
m, n = n, m%n
1313
}
1414
return abs(m)
1515
}
1616

17-
func lcm(m, n int) int {
17+
func LCM(m, n int) int {
1818
if n == 0 {
1919
return 0
2020
}
21-
return abs(n) * (abs(m) / gcd(m, n))
21+
return abs(n) * (abs(m) / GCD(m, n))
2222
}

vfs/adiantum/math_test.go renamed to internal/util/math_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package adiantum
1+
package util
22

33
import (
44
"math"
@@ -25,7 +25,7 @@ func Test_abs(t *testing.T) {
2525
}
2626
}
2727

28-
func Test_gcd(t *testing.T) {
28+
func Test_GCD(t *testing.T) {
2929
tests := []struct {
3030
arg1 int
3131
arg2 int
@@ -46,14 +46,14 @@ func Test_gcd(t *testing.T) {
4646
}
4747
for _, tt := range tests {
4848
t.Run("", func(t *testing.T) {
49-
if got := gcd(tt.arg1, tt.arg2); got != tt.want {
49+
if got := GCD(tt.arg1, tt.arg2); got != tt.want {
5050
t.Errorf("gcd(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want)
5151
}
5252
})
5353
}
5454
}
5555

56-
func Test_lcm(t *testing.T) {
56+
func Test_LCM(t *testing.T) {
5757
tests := []struct {
5858
arg1 int
5959
arg2 int
@@ -74,7 +74,7 @@ func Test_lcm(t *testing.T) {
7474
}
7575
for _, tt := range tests {
7676
t.Run("", func(t *testing.T) {
77-
if got := lcm(tt.arg1, tt.arg2); got != tt.want {
77+
if got := LCM(tt.arg1, tt.arg2); got != tt.want {
7878
t.Errorf("lcm(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want)
7979
}
8080
})

vfs/adiantum/README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ The default Adiantum construction uses XChaCha12 for its stream cipher,
1111
AES for its block cipher, and NH and Poly1305 for hashing.\
1212
Additionally, we use [Argon2id](https://pkg.go.dev/golang.org/x/crypto/argon2#hdr-Argon2id)
1313
to derive 256-bit keys from plain text where needed.
14-
File contents are encrypted in 4K blocks, matching the
14+
File contents are encrypted in 4 KiB blocks, matching the
1515
[default](https://sqlite.org/pgszchng2016.html) SQLite page size.
1616

1717
The VFS encrypts all files _except_
@@ -53,6 +53,10 @@ and want to protect against forgery, you should sign your backups,
5353
and verify signatures before restoring them.
5454

5555
This is slightly weaker than other forms of SQLite encryption
56-
that include block-level [MACs](https://en.wikipedia.org/wiki/Message_authentication_code).
57-
Block-level MACs can protect against forging individual blocks,
56+
that include page-level [MACs](https://en.wikipedia.org/wiki/Message_authentication_code).
57+
Page-level MACs can protect against forging individual pages,
5858
but can't prevent them from being reverted to former versions of themselves.
59+
60+
> [!TIP]
61+
> The [`"xts"`](../xts/README.md) package also offers encryption at rest.
62+
> AES-XTS uses _only_ NIST and FIPS-140 approved cryptographic primitives.

vfs/adiantum/adiantum_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ func Test_fileformat(t *testing.T) {
4242
if version != 0xBADDB {
4343
t.Error(version)
4444
}
45+
46+
_, err = db.Exec(`PRAGMA integrity_check`)
47+
if err != nil {
48+
t.Error(err)
49+
}
4550
}
4651

4752
func Benchmark_nokey(b *testing.B) {
@@ -57,6 +62,7 @@ func Benchmark_nokey(b *testing.B) {
5762
db.Close()
5863
}
5964
}
65+
6066
func Benchmark_hexkey(b *testing.B) {
6167
tmp := filepath.Join(b.TempDir(), "test.db")
6268
sqlite3.Initialize()

vfs/adiantum/api.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,17 @@ func init() {
4545
// Register registers an encrypting VFS, wrapping a base VFS,
4646
// and possibly using a custom HBSH cipher construction.
4747
// To use the default Adiantum construction, set cipher to nil.
48+
//
49+
// The default construction uses a 32 byte key/hexkey.
50+
// If a textkey is provided, the default KDF is Argon2id
51+
// with 64 MiB of memory, 3 iterations, and 4 threads.
4852
func Register(name string, base vfs.VFS, cipher HBSHCreator) {
4953
if cipher == nil {
5054
cipher = adiantumCreator{}
5155
}
5256
vfs.Register(name, &hbshVFS{
5357
VFS: base,
54-
hbsh: cipher,
58+
init: cipher,
5559
})
5660
}
5761

vfs/adiantum/hbsh.go

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import (
1313

1414
type hbshVFS struct {
1515
vfs.VFS
16-
hbsh HBSHCreator
16+
init HBSHCreator
1717
}
1818

1919
func (h *hbshVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
@@ -39,35 +39,40 @@ func (h *hbshVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs
3939
} else {
4040
var key []byte
4141
if params := name.URIParameters(); name == nil {
42-
key = h.hbsh.KDF("") // Temporary files get a random key.
42+
key = h.init.KDF("") // Temporary files get a random key.
4343
} else if t, ok := params["key"]; ok {
4444
key = []byte(t[0])
4545
} else if t, ok := params["hexkey"]; ok {
4646
key, _ = hex.DecodeString(t[0])
47-
} else if t, ok := params["textkey"]; ok {
48-
key = h.hbsh.KDF(t[0])
47+
} else if t, ok := params["textkey"]; ok && len(t[0]) > 0 {
48+
key = h.init.KDF(t[0])
4949
} else if flags&vfs.OPEN_MAIN_DB != 0 {
5050
// Main datatabases may have their key specified as a PRAGMA.
51-
return &hbshFile{File: file, reset: h.hbsh}, flags, nil
51+
return &hbshFile{File: file, init: h.init}, flags, nil
5252
}
53-
hbsh = h.hbsh.HBSH(key)
53+
hbsh = h.init.HBSH(key)
5454
}
5555

5656
if hbsh == nil {
5757
return nil, flags, sqlite3.CANTOPEN
5858
}
59-
return &hbshFile{File: file, hbsh: hbsh, reset: h.hbsh}, flags, nil
59+
return &hbshFile{File: file, hbsh: hbsh, init: h.init}, flags, nil
6060
}
6161

62+
// Larger blocks improve both security (wide-block cipher)
63+
// and throughput (cheap hashes amortize the block cipher's cost).
64+
// Use the default SQLite page size;
65+
// smaller pages pay the cost of unaligned access.
66+
// https://sqlite.org/pgszchng2016.html
6267
const (
6368
tweakSize = 8
6469
blockSize = 4096
6570
)
6671

6772
type hbshFile struct {
6873
vfs.File
74+
init HBSHCreator
6975
hbsh *hbsh.HBSH
70-
reset HBSHCreator
7176
tweak [tweakSize]byte
7277
block [blockSize]byte
7378
}
@@ -80,15 +85,17 @@ func (h *hbshFile) Pragma(name string, value string) (string, error) {
8085
case "hexkey":
8186
key, _ = hex.DecodeString(value)
8287
case "textkey":
83-
key = h.reset.KDF(value)
88+
if len(value) > 0 {
89+
key = h.init.KDF(value)
90+
}
8491
default:
8592
if f, ok := h.File.(vfs.FilePragma); ok {
8693
return f.Pragma(name, value)
8794
}
8895
return "", sqlite3.NOTFOUND
8996
}
9097

91-
if h.hbsh = h.reset.HBSH(key); h.hbsh != nil {
98+
if h.hbsh = h.init.HBSH(key); h.hbsh != nil {
9299
return "ok", nil
93100
}
94101
return "", sqlite3.CANTOPEN
@@ -99,7 +106,7 @@ func (h *hbshFile) ReadAt(p []byte, off int64) (n int, err error) {
99106
// Only OPEN_MAIN_DB can have a missing key.
100107
if off == 0 && len(p) == 100 {
101108
// SQLite is trying to read the header of a database file.
102-
// Pretend the file is empty so the key may specified as a PRAGMA.
109+
// Pretend the file is empty so the key may be specified as a PRAGMA.
103110
return 0, io.EOF
104111
}
105112
return 0, sqlite3.CANTOPEN
@@ -187,7 +194,7 @@ func (h *hbshFile) Truncate(size int64) error {
187194
}
188195

189196
func (h *hbshFile) SectorSize() int {
190-
return lcm(h.File.SectorSize(), blockSize)
197+
return util.LCM(h.File.SectorSize(), blockSize)
191198
}
192199

193200
func (h *hbshFile) DeviceCharacteristics() vfs.DeviceCharacteristic {

vfs/adiantum/testdata/test.db

4 KB
Binary file not shown.

vfs/tests/mptest/mptest_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/ncruces/go-sqlite3/vfs"
2222
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
2323
"github.com/ncruces/go-sqlite3/vfs/memdb"
24+
_ "github.com/ncruces/go-sqlite3/vfs/xts"
2425
"github.com/tetratelabs/wazero"
2526
"github.com/tetratelabs/wazero/api"
2627
"github.com/tetratelabs/wazero/experimental"
@@ -293,6 +294,52 @@ func Test_crash01_adiantum_wal(t *testing.T) {
293294
mod.Close(ctx)
294295
}
295296

297+
func Test_crash01_xts(t *testing.T) {
298+
if testing.Short() {
299+
t.Skip("skipping in short mode")
300+
}
301+
if os.Getenv("CI") != "" {
302+
t.Skip("skipping in CI")
303+
}
304+
if !vfs.SupportsFileLocking {
305+
t.Skip("skipping without locks")
306+
}
307+
308+
ctx := util.NewContext(newContext(t))
309+
name := "file:" + filepath.Join(t.TempDir(), "test.db") +
310+
"?hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
311+
cfg := config(ctx).WithArgs("mptest", name, "crash01.test",
312+
"--vfs", "xts")
313+
mod, err := rt.InstantiateModule(ctx, module, cfg)
314+
if err != nil {
315+
t.Fatal(err)
316+
}
317+
mod.Close(ctx)
318+
}
319+
320+
func Test_crash01_xts_wal(t *testing.T) {
321+
if testing.Short() {
322+
t.Skip("skipping in short mode")
323+
}
324+
if os.Getenv("CI") != "" {
325+
t.Skip("skipping in CI")
326+
}
327+
if !vfs.SupportsSharedMemory {
328+
t.Skip("skipping without shared memory")
329+
}
330+
331+
ctx := util.NewContext(newContext(t))
332+
name := "file:" + filepath.Join(t.TempDir(), "test.db") +
333+
"?hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
334+
cfg := config(ctx).WithArgs("mptest", name, "crash01.test",
335+
"--vfs", "xts", "--journalmode", "wal")
336+
mod, err := rt.InstantiateModule(ctx, module, cfg)
337+
if err != nil {
338+
t.Fatal(err)
339+
}
340+
mod.Close(ctx)
341+
}
342+
296343
func newContext(t *testing.T) context.Context {
297344
return context.WithValue(context.Background(), logger{}, &testWriter{T: t})
298345
}

vfs/tests/speedtest1/speedtest1_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/ncruces/go-sqlite3/vfs"
2626
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
2727
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
28+
_ "github.com/ncruces/go-sqlite3/vfs/xts"
2829
)
2930

3031
//go:embed testdata/speedtest1.wasm.bz2
@@ -126,3 +127,22 @@ func Benchmark_adiantum(b *testing.B) {
126127
}
127128
mod.Close(ctx)
128129
}
130+
131+
func Benchmark_xts(b *testing.B) {
132+
output.Reset()
133+
ctx := util.NewContext(context.Background())
134+
name := "file:" + filepath.Join(b.TempDir(), "test.db") +
135+
"?hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
136+
args := append(options, "--vfs", "xts", "--size", strconv.Itoa(b.N), name)
137+
cfg := wazero.NewModuleConfig().
138+
WithArgs(args...).WithName("speedtest1").
139+
WithStdout(&output).WithStderr(&output).
140+
WithSysWalltime().WithSysNanotime().WithSysNanosleep().
141+
WithOsyield(runtime.Gosched).
142+
WithRandSource(rand.Reader)
143+
mod, err := rt.InstantiateModule(ctx, module, cfg)
144+
if err != nil {
145+
b.Fatal(err)
146+
}
147+
mod.Close(ctx)
148+
}

vfs/xts/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Go `xts` SQLite VFS
2+
3+
This package wraps an SQLite VFS to offer encryption at rest.
4+
5+
The `"xts"` VFS wraps the default SQLite VFS using the
6+
[AES-XTS](https://pkg.go.dev/golang.org/x/crypto/xts)
7+
tweakable and length-preserving encryption.\
8+
In general, any XTS construction can be used to wrap any VFS.
9+
10+
The default AES-XTS construction uses AES-128, AES-192, or AES-256
11+
for its block cipher.
12+
Additionally, we use [PBKDF2-HMAC-SHA512](https://pkg.go.dev/golang.org/x/crypto/pbkdf2)
13+
to derive AES-128 keys from plain text where needed.
14+
File contents are encrypted in 512 byte sectors, matching the
15+
[minimum](https://sqlite.org/fileformat.html#pages) SQLite page size.
16+
17+
The VFS encrypts all files _except_
18+
[super journals](https://sqlite.org/tempfiles.html#super_journal_files):
19+
these _never_ contain database data, only filenames,
20+
and padding them to the sector size is problematic.
21+
Temporary files _are_ encrypted with **random** AES-128 keys,
22+
as they _may_ contain database data.
23+
To avoid the overhead of encrypting temporary files,
24+
keep them in memory:
25+
26+
PRAGMA temp_store = memory;
27+
28+
> [!IMPORTANT]
29+
> XTS is a cipher mode typically used for disk encryption.
30+
> The standard threat model for disk encryption considers an adversary
31+
> that can read multiple snapshots of a disk.
32+
> The only security property that disk encryption provides
33+
> is that all information such an adversary can obtain
34+
> is whether the data in a sector has or has not changed over time.
35+
36+
The encryption offered by this package is fully deterministic.
37+
38+
This means that an adversary who can get ahold of multiple snapshots
39+
(e.g. backups) of a database file can learn precisely:
40+
which sectors changed, which ones didn't, which got reverted.
41+
42+
This is slightly weaker than other forms of SQLite encryption
43+
that include *some* nondeterminism; with limited nondeterminism,
44+
an adversary can't distinguish between
45+
sectors that actually changed, and sectors that got reverted.
46+
47+
> [!CAUTION]
48+
> This package does not claim protect databases against tampering or forgery.
49+
50+
The major practical consequence of the above point is that,
51+
if you're keeping `"xts"` encrypted backups of your database,
52+
and want to protect against forgery, you should sign your backups,
53+
and verify signatures before restoring them.
54+
55+
This is slightly weaker than other forms of SQLite encryption
56+
that include page-level [MACs](https://en.wikipedia.org/wiki/Message_authentication_code).
57+
Page-level MACs can protect against forging individual pages,
58+
but can't prevent them from being reverted to former versions of themselves.
59+
60+
> [!TIP]
61+
> The [`"adiantum"`](../adiantum/README.md) package also offers encryption at rest.
62+
> In general Adiantum performs significantly better,
63+
> and as a "wide-block" cipher, _may_ offer improved security.

0 commit comments

Comments
 (0)