From 47afd3cdea61b6092b1f0b014831e04bf0184df4 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Thu, 10 Oct 2024 13:23:53 +0100 Subject: [PATCH 1/6] XTS tests. --- vfs/adiantum/api.go | 2 +- vfs/adiantum/hbsh.go | 18 +- vfs/tests/mptest/mptest_test.go | 47 ++++ vfs/tests/speedtest1/speedtest1_test.go | 20 ++ vfs/xts/README.md | 3 + vfs/xts/aes.go | 34 +++ vfs/xts/aes_test.go | 88 ++++++++ vfs/xts/api.go | 36 ++++ vfs/xts/math.go | 22 ++ vfs/xts/math_test.go | 82 +++++++ vfs/xts/testdata/test.db | Bin 0 -> 4096 bytes vfs/xts/xts.go | 275 ++++++++++++++++++++++++ 12 files changed, 617 insertions(+), 10 deletions(-) create mode 100644 vfs/xts/README.md create mode 100644 vfs/xts/aes.go create mode 100644 vfs/xts/aes_test.go create mode 100644 vfs/xts/api.go create mode 100644 vfs/xts/math.go create mode 100644 vfs/xts/math_test.go create mode 100644 vfs/xts/testdata/test.db create mode 100644 vfs/xts/xts.go diff --git a/vfs/adiantum/api.go b/vfs/adiantum/api.go index c484ec98..11cf55fe 100644 --- a/vfs/adiantum/api.go +++ b/vfs/adiantum/api.go @@ -51,7 +51,7 @@ func Register(name string, base vfs.VFS, cipher HBSHCreator) { } vfs.Register(name, &hbshVFS{ VFS: base, - hbsh: cipher, + init: cipher, }) } diff --git a/vfs/adiantum/hbsh.go b/vfs/adiantum/hbsh.go index 4522104b..b84de1b2 100644 --- a/vfs/adiantum/hbsh.go +++ b/vfs/adiantum/hbsh.go @@ -13,7 +13,7 @@ import ( type hbshVFS struct { vfs.VFS - hbsh HBSHCreator + init HBSHCreator } func (h *hbshVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) { @@ -39,24 +39,24 @@ func (h *hbshVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs } else { var key []byte if params := name.URIParameters(); name == nil { - key = h.hbsh.KDF("") // Temporary files get a random key. + key = h.init.KDF("") // Temporary files get a random key. } else if t, ok := params["key"]; ok { key = []byte(t[0]) } else if t, ok := params["hexkey"]; ok { key, _ = hex.DecodeString(t[0]) } else if t, ok := params["textkey"]; ok { - key = h.hbsh.KDF(t[0]) + key = h.init.KDF(t[0]) } else if flags&vfs.OPEN_MAIN_DB != 0 { // Main datatabases may have their key specified as a PRAGMA. - return &hbshFile{File: file, reset: h.hbsh}, flags, nil + return &hbshFile{File: file, init: h.init}, flags, nil } - hbsh = h.hbsh.HBSH(key) + hbsh = h.init.HBSH(key) } if hbsh == nil { return nil, flags, sqlite3.CANTOPEN } - return &hbshFile{File: file, hbsh: hbsh, reset: h.hbsh}, flags, nil + return &hbshFile{File: file, hbsh: hbsh, init: h.init}, flags, nil } const ( @@ -66,8 +66,8 @@ const ( type hbshFile struct { vfs.File + init HBSHCreator hbsh *hbsh.HBSH - reset HBSHCreator tweak [tweakSize]byte block [blockSize]byte } @@ -80,7 +80,7 @@ func (h *hbshFile) Pragma(name string, value string) (string, error) { case "hexkey": key, _ = hex.DecodeString(value) case "textkey": - key = h.reset.KDF(value) + key = h.init.KDF(value) default: if f, ok := h.File.(vfs.FilePragma); ok { return f.Pragma(name, value) @@ -88,7 +88,7 @@ func (h *hbshFile) Pragma(name string, value string) (string, error) { return "", sqlite3.NOTFOUND } - if h.hbsh = h.reset.HBSH(key); h.hbsh != nil { + if h.hbsh = h.init.HBSH(key); h.hbsh != nil { return "ok", nil } return "", sqlite3.CANTOPEN diff --git a/vfs/tests/mptest/mptest_test.go b/vfs/tests/mptest/mptest_test.go index a47133e5..c8873637 100644 --- a/vfs/tests/mptest/mptest_test.go +++ b/vfs/tests/mptest/mptest_test.go @@ -21,6 +21,7 @@ import ( "github.com/ncruces/go-sqlite3/vfs" _ "github.com/ncruces/go-sqlite3/vfs/adiantum" "github.com/ncruces/go-sqlite3/vfs/memdb" + _ "github.com/ncruces/go-sqlite3/vfs/xts" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/experimental" @@ -294,6 +295,52 @@ func Test_crash01_adiantum_wal(t *testing.T) { mod.Close(ctx) } +func Test_crash01_xts(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + if os.Getenv("CI") != "" { + t.Skip("skipping in CI") + } + if !vfs.SupportsFileLocking { + t.Skip("skipping without locks") + } + + ctx := util.NewContext(newContext(t)) + name := "file:" + filepath.Join(t.TempDir(), "test.db") + + "?hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + cfg := config(ctx).WithArgs("mptest", name, "crash01.test", + "--vfs", "xts") + mod, err := rt.InstantiateModule(ctx, module, cfg) + if err != nil { + t.Fatal(err) + } + mod.Close(ctx) +} + +func Test_crash01_xts_wal(t *testing.T) { + if testing.Short() { + t.Skip("skipping in short mode") + } + if os.Getenv("CI") != "" { + t.Skip("skipping in CI") + } + if !vfs.SupportsSharedMemory { + t.Skip("skipping without shared memory") + } + + ctx := util.NewContext(newContext(t)) + name := "file:" + filepath.Join(t.TempDir(), "test.db") + + "?hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + cfg := config(ctx).WithArgs("mptest", name, "crash01.test", + "--vfs", "xts", "--journalmode", "wal") + mod, err := rt.InstantiateModule(ctx, module, cfg) + if err != nil { + t.Fatal(err) + } + mod.Close(ctx) +} + func newContext(t *testing.T) context.Context { return context.WithValue(context.Background(), logger{}, &testWriter{T: t}) } diff --git a/vfs/tests/speedtest1/speedtest1_test.go b/vfs/tests/speedtest1/speedtest1_test.go index 4aa0fd31..abd033b1 100644 --- a/vfs/tests/speedtest1/speedtest1_test.go +++ b/vfs/tests/speedtest1/speedtest1_test.go @@ -25,6 +25,7 @@ import ( "github.com/ncruces/go-sqlite3/vfs" _ "github.com/ncruces/go-sqlite3/vfs/adiantum" _ "github.com/ncruces/go-sqlite3/vfs/memdb" + _ "github.com/ncruces/go-sqlite3/vfs/xts" ) //go:embed testdata/speedtest1.wasm.bz2 @@ -125,3 +126,22 @@ func Benchmark_adiantum(b *testing.B) { } mod.Close(ctx) } + +func Benchmark_xts(b *testing.B) { + output.Reset() + ctx := util.NewContext(context.Background()) + name := "file:" + filepath.Join(b.TempDir(), "test.db") + + "?hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + args := append(options, "--vfs", "xts", "--size", strconv.Itoa(b.N), name) + cfg := wazero.NewModuleConfig(). + WithArgs(args...).WithName("speedtest1"). + WithStdout(&output).WithStderr(&output). + WithSysWalltime().WithSysNanotime().WithSysNanosleep(). + WithOsyield(runtime.Gosched). + WithRandSource(rand.Reader) + mod, err := rt.InstantiateModule(ctx, module, cfg) + if err != nil { + b.Fatal(err) + } + mod.Close(ctx) +} diff --git a/vfs/xts/README.md b/vfs/xts/README.md new file mode 100644 index 00000000..12120526 --- /dev/null +++ b/vfs/xts/README.md @@ -0,0 +1,3 @@ +# Go `xts` SQLite VFS + +This package wraps an SQLite VFS to offer encryption at rest. \ No newline at end of file diff --git a/vfs/xts/aes.go b/vfs/xts/aes.go new file mode 100644 index 00000000..b6b4c396 --- /dev/null +++ b/vfs/xts/aes.go @@ -0,0 +1,34 @@ +package xts + +import ( + "crypto/aes" + "crypto/rand" + "crypto/sha512" + + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/xts" +) + +// This variable can be replaced with -ldflags: +// +// go build -ldflags="-X github.com/ncruces/go-sqlite3/vfs/xts.pepper=xts" +var pepper = "github.com/ncruces/go-sqlite3/vfs/xts" + +type aesCreator struct{} + +func (aesCreator) XTS(key []byte) *xts.Cipher { + c, err := xts.NewCipher(aes.NewCipher, key) + if err != nil { + return nil + } + return c +} + +func (aesCreator) KDF(text string) []byte { + if text == "" { + key := make([]byte, 32) + n, _ := rand.Read(key) + return key[:n] + } + return pbkdf2.Key([]byte(text), []byte(pepper), 10_000, 32, sha512.New) +} diff --git a/vfs/xts/aes_test.go b/vfs/xts/aes_test.go new file mode 100644 index 00000000..d7185fd8 --- /dev/null +++ b/vfs/xts/aes_test.go @@ -0,0 +1,88 @@ +package xts_test + +import ( + _ "embed" + "path/filepath" + "strings" + "testing" + + "github.com/ncruces/go-sqlite3" + "github.com/ncruces/go-sqlite3/driver" + _ "github.com/ncruces/go-sqlite3/embed" + _ "github.com/ncruces/go-sqlite3/internal/testcfg" + "github.com/ncruces/go-sqlite3/util/ioutil" + "github.com/ncruces/go-sqlite3/vfs" + "github.com/ncruces/go-sqlite3/vfs/readervfs" + "github.com/ncruces/go-sqlite3/vfs/xts" +) + +//go:embed testdata/test.db +var testDB string + +func Test_fileformat(t *testing.T) { + readervfs.Create("test.db", ioutil.NewSizeReaderAt(strings.NewReader(testDB))) + xts.Register("rxts", vfs.Find("reader"), nil) + + db, err := driver.Open("file:test.db?vfs=rxts") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + _, err = db.Exec(`PRAGMA textkey='correct+horse+battery+staple'`) + if err != nil { + t.Fatal(err) + } + + var version uint32 + err = db.QueryRow(`PRAGMA user_version`).Scan(&version) + if err != nil { + t.Fatal(err) + } + if version != 0xBADDB { + t.Error(version) + } +} + +func Benchmark_nokey(b *testing.B) { + tmp := filepath.Join(b.TempDir(), "test.db") + sqlite3.Initialize() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1") + if err != nil { + b.Fatal(err) + } + db.Close() + } +} +func Benchmark_hexkey(b *testing.B) { + tmp := filepath.Join(b.TempDir(), "test.db") + sqlite3.Initialize() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1" + + "&vfs=xts&hexkey=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855") + if err != nil { + b.Fatal(err) + } + db.Close() + } +} + +func Benchmark_textkey(b *testing.B) { + tmp := filepath.Join(b.TempDir(), "test.db") + sqlite3.Initialize() + b.ResetTimer() + + for n := 0; n < b.N; n++ { + db, err := sqlite3.Open("file:" + filepath.ToSlash(tmp) + "?nolock=1" + + "&vfs=xts&textkey=correct+horse+battery+staple") + if err != nil { + b.Fatal(err) + } + db.Close() + } +} diff --git a/vfs/xts/api.go b/vfs/xts/api.go new file mode 100644 index 00000000..96f6d1ba --- /dev/null +++ b/vfs/xts/api.go @@ -0,0 +1,36 @@ +// Package xts wraps an SQLite VFS to offer encryption at rest. +package xts + +import ( + "github.com/ncruces/go-sqlite3/vfs" + "golang.org/x/crypto/xts" +) + +func init() { + Register("xts", vfs.Find(""), nil) +} + +// Register registers an encrypting VFS, wrapping a base VFS, +// and possibly using a custom XTS cipher construction. +// To use the default AES-XTS construction, set cipher to nil. +func Register(name string, base vfs.VFS, cipher XTSCreator) { + if cipher == nil { + cipher = aesCreator{} + } + vfs.Register(name, &xtsVFS{ + VFS: base, + init: cipher, + }) +} + +// XTSCreator creates an [xts.Cipher] +// given key material. +type XTSCreator interface { + // KDF derives an XTS key from a secret. + // If no secret is given, a random key is generated. + KDF(secret string) (key []byte) + + // XTS creates an XTS cipher given a key. + // If key is not appropriate, nil is returned. + XTS(key []byte) *xts.Cipher +} diff --git a/vfs/xts/math.go b/vfs/xts/math.go new file mode 100644 index 00000000..b6cf113c --- /dev/null +++ b/vfs/xts/math.go @@ -0,0 +1,22 @@ +package xts + +func abs(n int) int { + if n < 0 { + return -n + } + return n +} + +func gcd(m, n int) int { + for n != 0 { + m, n = n, m%n + } + return abs(m) +} + +func lcm(m, n int) int { + if n == 0 { + return 0 + } + return abs(n) * (abs(m) / gcd(m, n)) +} diff --git a/vfs/xts/math_test.go b/vfs/xts/math_test.go new file mode 100644 index 00000000..5502c2c3 --- /dev/null +++ b/vfs/xts/math_test.go @@ -0,0 +1,82 @@ +package xts + +import ( + "math" + "testing" +) + +func Test_abs(t *testing.T) { + tests := []struct { + arg int + want int + }{ + {0, 0}, + {1, 1}, + {-1, 1}, + {math.MaxInt, math.MaxInt}, + {math.MinInt, math.MinInt}, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + if got := abs(tt.arg); got != tt.want { + t.Errorf("abs(%d) = %d, want %d", tt.arg, got, tt.want) + } + }) + } +} + +func Test_gcd(t *testing.T) { + tests := []struct { + arg1 int + arg2 int + want int + }{ + {0, 0, 0}, + {0, 1, 1}, + {1, 0, 1}, + {1, 1, 1}, + {2, 3, 1}, + {42, 56, 14}, + {48, -18, 6}, + {1e9, 1e9, 1e9}, + {1e9, -1e9, 1e9}, + {-1e9, -1e9, 1e9}, + {math.MaxInt, math.MaxInt, math.MaxInt}, + {math.MinInt, math.MinInt, math.MinInt}, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + if got := gcd(tt.arg1, tt.arg2); got != tt.want { + t.Errorf("gcd(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want) + } + }) + } +} + +func Test_lcm(t *testing.T) { + tests := []struct { + arg1 int + arg2 int + want int + }{ + {0, 0, 0}, + {0, 1, 0}, + {1, 0, 0}, + {1, 1, 1}, + {2, 3, 6}, + {42, 56, 168}, + {48, -18, 144}, + {1e9, 1e9, 1e9}, + {1e9, -1e9, 1e9}, + {-1e9, -1e9, 1e9}, + {math.MaxInt, math.MaxInt, math.MaxInt}, + {math.MinInt, math.MinInt, math.MinInt}, + } + for _, tt := range tests { + t.Run("", func(t *testing.T) { + if got := lcm(tt.arg1, tt.arg2); got != tt.want { + t.Errorf("lcm(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want) + } + }) + } +} diff --git a/vfs/xts/testdata/test.db b/vfs/xts/testdata/test.db new file mode 100644 index 0000000000000000000000000000000000000000..774e59f4258b9913f9a59bfd27f37f3331f8e34f GIT binary patch literal 4096 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 min { + data = data[off-min:] + } + n += copy(p[n:], data) + } + + if n != len(p) { + panic(util.AssertErr()) + } + return n, nil +} + +func (x *xtsFile) WriteAt(p []byte, off int64) (n int, err error) { + if x.cipher == nil { + return 0, sqlite3.READONLY + } + + min := (off) &^ (sectorSize - 1) // round down + max := (off + int64(len(p)) + (sectorSize - 1)) &^ (sectorSize - 1) // round up + + // Write one block at a time. + for ; min < max; min += sectorSize { + sectorNum := uint64(min / sectorSize) + data := x.sector[:] + + if off > min || len(p[n:]) < sectorSize { + // Partial block write: read-update-write. + m, err := x.File.ReadAt(x.sector[:], min) + if m != sectorSize { + if err != io.EOF { + return n, err + } + // Writing past the EOF. + // We're either appending an entirely new block, + // or the final block was only partially written. + // A partially written block can't be decrypted, + // and is as good as corrupt. + // Either way, zero pad the file to the next block size. + clear(data) + } else { + x.cipher.Decrypt(data, data, sectorNum) + } + if off > min { + data = data[off-min:] + } + } + + t := copy(data, p[n:]) + x.cipher.Encrypt(x.sector[:], x.sector[:], sectorNum) + + m, err := x.File.WriteAt(x.sector[:], min) + if m != sectorSize { + return n, err + } + n += t + } + + if n != len(p) { + panic(util.AssertErr()) + } + return n, nil +} + +func (x *xtsFile) Truncate(size int64) error { + size = (size + (sectorSize - 1)) &^ (sectorSize - 1) // round up + return x.File.Truncate(size) +} + +func (x *xtsFile) SectorSize() int { + return lcm(x.File.SectorSize(), sectorSize) +} + +func (x *xtsFile) DeviceCharacteristics() vfs.DeviceCharacteristic { + return x.File.DeviceCharacteristics() & (0 | + // The only safe flags are these: + vfs.IOCAP_UNDELETABLE_WHEN_OPEN | + vfs.IOCAP_IMMUTABLE | + vfs.IOCAP_BATCH_ATOMIC) +} + +// Wrap optional methods. + +func (x *xtsFile) SharedMemory() vfs.SharedMemory { + if f, ok := x.File.(vfs.FileSharedMemory); ok { + return f.SharedMemory() + } + return nil +} + +func (x *xtsFile) ChunkSize(size int) { + if f, ok := x.File.(vfs.FileChunkSize); ok { + size = (size + (sectorSize - 1)) &^ (sectorSize - 1) // round up + f.ChunkSize(size) + } +} + +func (x *xtsFile) SizeHint(size int64) error { + if f, ok := x.File.(vfs.FileSizeHint); ok { + size = (size + (sectorSize - 1)) &^ (sectorSize - 1) // round up + return f.SizeHint(size) + } + return sqlite3.NOTFOUND +} + +func (x *xtsFile) HasMoved() (bool, error) { + if f, ok := x.File.(vfs.FileHasMoved); ok { + return f.HasMoved() + } + return false, sqlite3.NOTFOUND +} + +func (x *xtsFile) Overwrite() error { + if f, ok := x.File.(vfs.FileOverwrite); ok { + return f.Overwrite() + } + return sqlite3.NOTFOUND +} + +func (x *xtsFile) CommitPhaseTwo() error { + if f, ok := x.File.(vfs.FileCommitPhaseTwo); ok { + return f.CommitPhaseTwo() + } + return sqlite3.NOTFOUND +} + +func (x *xtsFile) BeginAtomicWrite() error { + if f, ok := x.File.(vfs.FileBatchAtomicWrite); ok { + return f.BeginAtomicWrite() + } + return sqlite3.NOTFOUND +} + +func (x *xtsFile) CommitAtomicWrite() error { + if f, ok := x.File.(vfs.FileBatchAtomicWrite); ok { + return f.CommitAtomicWrite() + } + return sqlite3.NOTFOUND +} + +func (x *xtsFile) RollbackAtomicWrite() error { + if f, ok := x.File.(vfs.FileBatchAtomicWrite); ok { + return f.RollbackAtomicWrite() + } + return sqlite3.NOTFOUND +} + +func (x *xtsFile) CheckpointDone() error { + if f, ok := x.File.(vfs.FileCheckpoint); ok { + return f.CheckpointDone() + } + return sqlite3.NOTFOUND +} + +func (x *xtsFile) CheckpointStart() error { + if f, ok := x.File.(vfs.FileCheckpoint); ok { + return f.CheckpointStart() + } + return sqlite3.NOTFOUND +} From 4360f555315d77a266adcdd83ace14ed16526e96 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 11 Oct 2024 18:13:23 +0100 Subject: [PATCH 2/6] Reuse math. --- {vfs/xts => internal/util}/math.go | 8 +-- {vfs/xts => internal/util}/math_test.go | 6 +- vfs/adiantum/hbsh.go | 2 +- vfs/adiantum/math.go | 22 ------- vfs/adiantum/math_test.go | 82 ------------------------- vfs/xts/xts.go | 2 +- 6 files changed, 9 insertions(+), 113 deletions(-) rename {vfs/xts => internal/util}/math.go (58%) rename {vfs/xts => internal/util}/math_test.go (91%) delete mode 100644 vfs/adiantum/math.go delete mode 100644 vfs/adiantum/math_test.go diff --git a/vfs/xts/math.go b/internal/util/math.go similarity index 58% rename from vfs/xts/math.go rename to internal/util/math.go index b6cf113c..ffbd0c2f 100644 --- a/vfs/xts/math.go +++ b/internal/util/math.go @@ -1,4 +1,4 @@ -package xts +package util func abs(n int) int { if n < 0 { @@ -7,16 +7,16 @@ func abs(n int) int { return n } -func gcd(m, n int) int { +func GCD(m, n int) int { for n != 0 { m, n = n, m%n } return abs(m) } -func lcm(m, n int) int { +func LCM(m, n int) int { if n == 0 { return 0 } - return abs(n) * (abs(m) / gcd(m, n)) + return abs(n) * (abs(m) / GCD(m, n)) } diff --git a/vfs/xts/math_test.go b/internal/util/math_test.go similarity index 91% rename from vfs/xts/math_test.go rename to internal/util/math_test.go index 5502c2c3..f403490c 100644 --- a/vfs/xts/math_test.go +++ b/internal/util/math_test.go @@ -1,4 +1,4 @@ -package xts +package util import ( "math" @@ -46,7 +46,7 @@ func Test_gcd(t *testing.T) { } for _, tt := range tests { t.Run("", func(t *testing.T) { - if got := gcd(tt.arg1, tt.arg2); got != tt.want { + if got := GCD(tt.arg1, tt.arg2); got != tt.want { t.Errorf("gcd(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want) } }) @@ -74,7 +74,7 @@ func Test_lcm(t *testing.T) { } for _, tt := range tests { t.Run("", func(t *testing.T) { - if got := lcm(tt.arg1, tt.arg2); got != tt.want { + if got := LCM(tt.arg1, tt.arg2); got != tt.want { t.Errorf("lcm(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want) } }) diff --git a/vfs/adiantum/hbsh.go b/vfs/adiantum/hbsh.go index b84de1b2..8fce7ee9 100644 --- a/vfs/adiantum/hbsh.go +++ b/vfs/adiantum/hbsh.go @@ -187,7 +187,7 @@ func (h *hbshFile) Truncate(size int64) error { } func (h *hbshFile) SectorSize() int { - return lcm(h.File.SectorSize(), blockSize) + return util.LCM(h.File.SectorSize(), blockSize) } func (h *hbshFile) DeviceCharacteristics() vfs.DeviceCharacteristic { diff --git a/vfs/adiantum/math.go b/vfs/adiantum/math.go deleted file mode 100644 index 17f7a291..00000000 --- a/vfs/adiantum/math.go +++ /dev/null @@ -1,22 +0,0 @@ -package adiantum - -func abs(n int) int { - if n < 0 { - return -n - } - return n -} - -func gcd(m, n int) int { - for n != 0 { - m, n = n, m%n - } - return abs(m) -} - -func lcm(m, n int) int { - if n == 0 { - return 0 - } - return abs(n) * (abs(m) / gcd(m, n)) -} diff --git a/vfs/adiantum/math_test.go b/vfs/adiantum/math_test.go deleted file mode 100644 index 2e2398a7..00000000 --- a/vfs/adiantum/math_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package adiantum - -import ( - "math" - "testing" -) - -func Test_abs(t *testing.T) { - tests := []struct { - arg int - want int - }{ - {0, 0}, - {1, 1}, - {-1, 1}, - {math.MaxInt, math.MaxInt}, - {math.MinInt, math.MinInt}, - } - for _, tt := range tests { - t.Run("", func(t *testing.T) { - if got := abs(tt.arg); got != tt.want { - t.Errorf("abs(%d) = %d, want %d", tt.arg, got, tt.want) - } - }) - } -} - -func Test_gcd(t *testing.T) { - tests := []struct { - arg1 int - arg2 int - want int - }{ - {0, 0, 0}, - {0, 1, 1}, - {1, 0, 1}, - {1, 1, 1}, - {2, 3, 1}, - {42, 56, 14}, - {48, -18, 6}, - {1e9, 1e9, 1e9}, - {1e9, -1e9, 1e9}, - {-1e9, -1e9, 1e9}, - {math.MaxInt, math.MaxInt, math.MaxInt}, - {math.MinInt, math.MinInt, math.MinInt}, - } - for _, tt := range tests { - t.Run("", func(t *testing.T) { - if got := gcd(tt.arg1, tt.arg2); got != tt.want { - t.Errorf("gcd(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want) - } - }) - } -} - -func Test_lcm(t *testing.T) { - tests := []struct { - arg1 int - arg2 int - want int - }{ - {0, 0, 0}, - {0, 1, 0}, - {1, 0, 0}, - {1, 1, 1}, - {2, 3, 6}, - {42, 56, 168}, - {48, -18, 144}, - {1e9, 1e9, 1e9}, - {1e9, -1e9, 1e9}, - {-1e9, -1e9, 1e9}, - {math.MaxInt, math.MaxInt, math.MaxInt}, - {math.MinInt, math.MinInt, math.MinInt}, - } - for _, tt := range tests { - t.Run("", func(t *testing.T) { - if got := lcm(tt.arg1, tt.arg2); got != tt.want { - t.Errorf("lcm(%d, %d) = %d, want %d", tt.arg1, tt.arg2, got, tt.want) - } - }) - } -} diff --git a/vfs/xts/xts.go b/vfs/xts/xts.go index fcd1321a..1c86d898 100644 --- a/vfs/xts/xts.go +++ b/vfs/xts/xts.go @@ -183,7 +183,7 @@ func (x *xtsFile) Truncate(size int64) error { } func (x *xtsFile) SectorSize() int { - return lcm(x.File.SectorSize(), sectorSize) + return util.LCM(x.File.SectorSize(), sectorSize) } func (x *xtsFile) DeviceCharacteristics() vfs.DeviceCharacteristic { From 3c4283e09e35f1dba41504203538fcc50288bfca Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Fri, 11 Oct 2024 18:23:20 +0100 Subject: [PATCH 3/6] Improved golden tests. --- vfs/adiantum/adiantum_test.go | 5 +++++ vfs/adiantum/testdata/test.db | Bin 4096 -> 8192 bytes vfs/xts/aes_test.go | 5 +++++ vfs/xts/testdata/test.db | Bin 4096 -> 8192 bytes 4 files changed, 10 insertions(+) diff --git a/vfs/adiantum/adiantum_test.go b/vfs/adiantum/adiantum_test.go index 1096a358..4d756dbf 100644 --- a/vfs/adiantum/adiantum_test.go +++ b/vfs/adiantum/adiantum_test.go @@ -42,6 +42,11 @@ func Test_fileformat(t *testing.T) { if version != 0xBADDB { t.Error(version) } + + _, err = db.Exec(`PRAGMA integrity_check`) + if err != nil { + t.Error(err) + } } func Benchmark_nokey(b *testing.B) { diff --git a/vfs/adiantum/testdata/test.db b/vfs/adiantum/testdata/test.db index 3f8ac937b373231a093b8fd47d7e6d7ba729cc6b..f778e70a809f9685720bbefe5359261efa386c13 100644 GIT binary patch literal 8192 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 mW!<*`N>_0-&0aO1=vQGOgWYMM`yfWIOwtMW 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 diff --git a/vfs/xts/aes_test.go b/vfs/xts/aes_test.go index d7185fd8..18974fe1 100644 --- a/vfs/xts/aes_test.go +++ b/vfs/xts/aes_test.go @@ -42,6 +42,11 @@ func Test_fileformat(t *testing.T) { if version != 0xBADDB { t.Error(version) } + + _, err = db.Exec(`PRAGMA integrity_check`) + if err != nil { + t.Error(err) + } } func Benchmark_nokey(b *testing.B) { diff --git a/vfs/xts/testdata/test.db b/vfs/xts/testdata/test.db index 774e59f4258b9913f9a59bfd27f37f3331f8e34f..98d2fb9961c6590a82a7495077a250620a251fa8 100644 GIT binary patch delta 4302 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 delta 174 zcmV;f08#&dK!6}XU**;;m#x{5Ex~lv*UCI*)j}S_@~Z)Rv5=gZf>2jhykmYTK-1PK zh4|?JgWTS27_0U!y#rFZp3vOs;41M(kw732nCzhda~)Q2h(H8;B#6{Dv2Z{SQ2#lt zgOyIg+FCUjeeuSkrbz#DI?^&`YYMn$Lj70!c@IRR Date: Thu, 17 Oct 2024 12:38:54 +0100 Subject: [PATCH 4/6] Docs. --- vfs/adiantum/README.md | 8 ++++-- vfs/xts/README.md | 62 +++++++++++++++++++++++++++++++++++++++++- vfs/xts/api.go | 32 ++++++++++++++++++++++ 3 files changed, 99 insertions(+), 3 deletions(-) diff --git a/vfs/adiantum/README.md b/vfs/adiantum/README.md index 508c6851..5fa0770f 100644 --- a/vfs/adiantum/README.md +++ b/vfs/adiantum/README.md @@ -53,6 +53,10 @@ and want to protect against forgery, you should sign your backups, and verify signatures before restoring them. This is slightly weaker than other forms of SQLite encryption -that include block-level [MACs](https://en.wikipedia.org/wiki/Message_authentication_code). -Block-level MACs can protect against forging individual blocks, +that include page-level [MACs](https://en.wikipedia.org/wiki/Message_authentication_code). +Page-level MACs can protect against forging individual pages, but can't prevent them from being reverted to former versions of themselves. + +> [!TIP] +> The [`"xts"`](../xts/README.md) package also offers encryption at rest. +> AES-XTS uses _only_ NIST and FIPS-140 approved cryptographic primitives. \ No newline at end of file diff --git a/vfs/xts/README.md b/vfs/xts/README.md index 12120526..b937f6c7 100644 --- a/vfs/xts/README.md +++ b/vfs/xts/README.md @@ -1,3 +1,63 @@ # Go `xts` SQLite VFS -This package wraps an SQLite VFS to offer encryption at rest. \ No newline at end of file +This package wraps an SQLite VFS to offer encryption at rest. + +The `"xts"` VFS wraps the default SQLite VFS using the +[AES-XTS](https://pkg.go.dev/golang.org/x/crypto/xts) +tweakable and length-preserving encryption.\ +In general, any XTS construction can be used to wrap any VFS. + +The default AES-XTS construction uses AES-128, AES-192 or AES-256 +for its block cipher. +Additionally, we use [PBKDF2-HMAC-SHA512](https://pkg.go.dev/golang.org/x/crypto/pbkdf2) +to derive AES-128 keys from plain text where needed. +File contents are encrypted in 512 byte sectors, matching the +[minimum](https://sqlite.org/fileformat.html#pages) SQLite page size. + +The VFS encrypts all files _except_ +[super journals](https://sqlite.org/tempfiles.html#super_journal_files): +these _never_ contain database data, only filenames, +and padding them to the sector size is problematic. +Temporary files _are_ encrypted with **random** AES-128 keys, +as they _may_ contain database data. +To avoid the overhead of encrypting temporary files, +keep them in memory: + + PRAGMA temp_store = memory; + +> [!IMPORTANT] +> XTS is a cipher mode typically used for disk encryption. +> The standard threat model for disk encryption considers an adversary +> that can read multiple snapshots of a disk. +> The only security property that disk encryption provides +> is that all information such an adversary can obtain +> is whether the data in a sector has or has not changed over time. + +The encryption offered by this package is fully deterministic. + +This means that an adversary who can get ahold of multiple snapshots +(e.g. backups) of a database file can learn precisely: +which sectors changed, which ones didn't, which got reverted. + +This is slightly weaker than other forms of SQLite encryption +that include *some* nondeterminism; with limited nondeterminism, +an adversary can't distinguish between +sectors that actually changed, and sectors that got reverted. + +> [!CAUTION] +> This package does not claim protect databases against tampering or forgery. + +The major practical consequence of the above point is that, +if you're keeping `"xts"` encrypted backups of your database, +and want to protect against forgery, you should sign your backups, +and verify signatures before restoring them. + +This is slightly weaker than other forms of SQLite encryption +that include page-level [MACs](https://en.wikipedia.org/wiki/Message_authentication_code). +Page-level MACs can protect against forging individual pages, +but can't prevent them from being reverted to former versions of themselves. + +> [!TIP] +> The [`"adiantum"`](../adiantum/README.md) package also offers encryption at rest. +> In general Adiantum performs significantly better, +> and as a "wide-block" cipher, _may_ offer improved security. \ No newline at end of file diff --git a/vfs/xts/api.go b/vfs/xts/api.go index 96f6d1ba..27250e48 100644 --- a/vfs/xts/api.go +++ b/vfs/xts/api.go @@ -1,4 +1,36 @@ // Package xts wraps an SQLite VFS to offer encryption at rest. +// +// The "xts" [vfs.VFS] wraps the default VFS using the +// AES-XTS tweakable, length-preserving encryption. +// +// Importing package xts registers that VFS: +// +// import _ "github.com/ncruces/go-sqlite3/vfs/xts" +// +// To open an encrypted database you need to provide key material. +// +// The simplest way to do that is to specify the key through an [URI] parameter: +// +// - key: key material in binary (32, 48 or 64 bytes) +// - hexkey: key material in hex (64, 96 or 128 hex digits) +// - textkey: key material in text (any length) +// +// However, this makes your key easily accessible to other parts of +// your application (e.g. through [vfs.Filename.URIParameters]). +// +// To avoid this, invoke any of the following PRAGMAs +// immediately after opening a connection: +// +// PRAGMA key='D41d8cD98f00b204e9800998eCf8427e'; +// PRAGMA hexkey='e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'; +// PRAGMA textkey='your-secret-key'; +// +// For an ATTACH-ed database, you must specify the schema name: +// +// ATTACH DATABASE 'demo.db' AS demo; +// PRAGMA demo.textkey='your-secret-key'; +// +// [URI]: https://sqlite.org/uri.html package xts import ( From 932438667c391b401c2eb110cdc3e21cf03ece10 Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Thu, 17 Oct 2024 22:59:14 +0100 Subject: [PATCH 5/6] Apply suggestions from code review Co-authored-by: Ben Krieger --- vfs/xts/aes_test.go | 1 + vfs/xts/xts.go | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/vfs/xts/aes_test.go b/vfs/xts/aes_test.go index 18974fe1..d7b78cc3 100644 --- a/vfs/xts/aes_test.go +++ b/vfs/xts/aes_test.go @@ -62,6 +62,7 @@ func Benchmark_nokey(b *testing.B) { db.Close() } } + func Benchmark_hexkey(b *testing.B) { tmp := filepath.Join(b.TempDir(), "test.db") sqlite3.Initialize() diff --git a/vfs/xts/xts.go b/vfs/xts/xts.go index 1c86d898..c01b85b2 100644 --- a/vfs/xts/xts.go +++ b/vfs/xts/xts.go @@ -4,10 +4,11 @@ import ( "encoding/hex" "io" + "golang.org/x/crypto/xts" + "github.com/ncruces/go-sqlite3" "github.com/ncruces/go-sqlite3/internal/util" "github.com/ncruces/go-sqlite3/vfs" - "golang.org/x/crypto/xts" ) type xtsVFS struct { @@ -94,7 +95,7 @@ func (x *xtsFile) ReadAt(p []byte, off int64) (n int, err error) { // Only OPEN_MAIN_DB can have a missing key. if off == 0 && len(p) == 100 { // SQLite is trying to read the header of a database file. - // Pretend the file is empty so the key may specified as a PRAGMA. + // Pretend the file is empty so the key may be specified as a PRAGMA. return 0, io.EOF } return 0, sqlite3.CANTOPEN From cb2f28dc1ebd5188f16646d4220e6ff24e3c8b0d Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Thu, 17 Oct 2024 23:50:19 +0100 Subject: [PATCH 6/6] Address code review. --- internal/util/math_test.go | 4 ++-- vfs/adiantum/README.md | 2 +- vfs/adiantum/adiantum_test.go | 1 + vfs/adiantum/api.go | 4 ++++ vfs/adiantum/hbsh.go | 13 ++++++++++--- vfs/xts/README.md | 2 +- vfs/xts/api.go | 5 +++++ vfs/xts/xts.go | 11 +++++++++-- 8 files changed, 33 insertions(+), 9 deletions(-) diff --git a/internal/util/math_test.go b/internal/util/math_test.go index f403490c..052314ca 100644 --- a/internal/util/math_test.go +++ b/internal/util/math_test.go @@ -25,7 +25,7 @@ func Test_abs(t *testing.T) { } } -func Test_gcd(t *testing.T) { +func Test_GCD(t *testing.T) { tests := []struct { arg1 int arg2 int @@ -53,7 +53,7 @@ func Test_gcd(t *testing.T) { } } -func Test_lcm(t *testing.T) { +func Test_LCM(t *testing.T) { tests := []struct { arg1 int arg2 int diff --git a/vfs/adiantum/README.md b/vfs/adiantum/README.md index 5fa0770f..bc3f094b 100644 --- a/vfs/adiantum/README.md +++ b/vfs/adiantum/README.md @@ -11,7 +11,7 @@ The default Adiantum construction uses XChaCha12 for its stream cipher, AES for its block cipher, and NH and Poly1305 for hashing.\ Additionally, we use [Argon2id](https://pkg.go.dev/golang.org/x/crypto/argon2#hdr-Argon2id) to derive 256-bit keys from plain text where needed. -File contents are encrypted in 4K blocks, matching the +File contents are encrypted in 4 KiB blocks, matching the [default](https://sqlite.org/pgszchng2016.html) SQLite page size. The VFS encrypts all files _except_ diff --git a/vfs/adiantum/adiantum_test.go b/vfs/adiantum/adiantum_test.go index 4d756dbf..dc327d1d 100644 --- a/vfs/adiantum/adiantum_test.go +++ b/vfs/adiantum/adiantum_test.go @@ -62,6 +62,7 @@ func Benchmark_nokey(b *testing.B) { db.Close() } } + func Benchmark_hexkey(b *testing.B) { tmp := filepath.Join(b.TempDir(), "test.db") sqlite3.Initialize() diff --git a/vfs/adiantum/api.go b/vfs/adiantum/api.go index 11cf55fe..bbfc89b2 100644 --- a/vfs/adiantum/api.go +++ b/vfs/adiantum/api.go @@ -45,6 +45,10 @@ func init() { // Register registers an encrypting VFS, wrapping a base VFS, // and possibly using a custom HBSH cipher construction. // To use the default Adiantum construction, set cipher to nil. +// +// The default construction uses a 32 byte key/hexkey. +// If a textkey is provided, the default KDF is Argon2id +// with 64 MiB of memory, 3 iterations, and 4 threads. func Register(name string, base vfs.VFS, cipher HBSHCreator) { if cipher == nil { cipher = adiantumCreator{} diff --git a/vfs/adiantum/hbsh.go b/vfs/adiantum/hbsh.go index 8fce7ee9..02cb63bd 100644 --- a/vfs/adiantum/hbsh.go +++ b/vfs/adiantum/hbsh.go @@ -44,7 +44,7 @@ func (h *hbshVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs key = []byte(t[0]) } else if t, ok := params["hexkey"]; ok { key, _ = hex.DecodeString(t[0]) - } else if t, ok := params["textkey"]; ok { + } else if t, ok := params["textkey"]; ok && len(t[0]) > 0 { key = h.init.KDF(t[0]) } else if flags&vfs.OPEN_MAIN_DB != 0 { // Main datatabases may have their key specified as a PRAGMA. @@ -59,6 +59,11 @@ func (h *hbshVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs return &hbshFile{File: file, hbsh: hbsh, init: h.init}, flags, nil } +// Larger blocks improve both security (wide-block cipher) +// and throughput (cheap hashes amortize the block cipher's cost). +// Use the default SQLite page size; +// smaller pages pay the cost of unaligned access. +// https://sqlite.org/pgszchng2016.html const ( tweakSize = 8 blockSize = 4096 @@ -80,7 +85,9 @@ func (h *hbshFile) Pragma(name string, value string) (string, error) { case "hexkey": key, _ = hex.DecodeString(value) case "textkey": - key = h.init.KDF(value) + if len(value) > 0 { + key = h.init.KDF(value) + } default: if f, ok := h.File.(vfs.FilePragma); ok { return f.Pragma(name, value) @@ -99,7 +106,7 @@ func (h *hbshFile) ReadAt(p []byte, off int64) (n int, err error) { // Only OPEN_MAIN_DB can have a missing key. if off == 0 && len(p) == 100 { // SQLite is trying to read the header of a database file. - // Pretend the file is empty so the key may specified as a PRAGMA. + // Pretend the file is empty so the key may be specified as a PRAGMA. return 0, io.EOF } return 0, sqlite3.CANTOPEN diff --git a/vfs/xts/README.md b/vfs/xts/README.md index b937f6c7..786435de 100644 --- a/vfs/xts/README.md +++ b/vfs/xts/README.md @@ -7,7 +7,7 @@ The `"xts"` VFS wraps the default SQLite VFS using the tweakable and length-preserving encryption.\ In general, any XTS construction can be used to wrap any VFS. -The default AES-XTS construction uses AES-128, AES-192 or AES-256 +The default AES-XTS construction uses AES-128, AES-192, or AES-256 for its block cipher. Additionally, we use [PBKDF2-HMAC-SHA512](https://pkg.go.dev/golang.org/x/crypto/pbkdf2) to derive AES-128 keys from plain text where needed. diff --git a/vfs/xts/api.go b/vfs/xts/api.go index 27250e48..ad30fb25 100644 --- a/vfs/xts/api.go +++ b/vfs/xts/api.go @@ -45,6 +45,11 @@ func init() { // Register registers an encrypting VFS, wrapping a base VFS, // and possibly using a custom XTS cipher construction. // To use the default AES-XTS construction, set cipher to nil. +// +// The default construction uses AES-128, AES-192, or AES-256 +// if the key/hexkey is 32, 48, or 64 bytes, respectively. +// If a textkey is provided, the default KDF is PBKDF2-HMAC-SHA512 +// with 10,000 iterations, always producing a 32 byte key. func Register(name string, base vfs.VFS, cipher XTSCreator) { if cipher == nil { cipher = aesCreator{} diff --git a/vfs/xts/xts.go b/vfs/xts/xts.go index c01b85b2..1d3107f2 100644 --- a/vfs/xts/xts.go +++ b/vfs/xts/xts.go @@ -44,7 +44,7 @@ func (x *xtsVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs. key = []byte(t[0]) } else if t, ok := params["hexkey"]; ok { key, _ = hex.DecodeString(t[0]) - } else if t, ok := params["textkey"]; ok { + } else if t, ok := params["textkey"]; ok && len(t[0]) > 0 { key = x.init.KDF(t[0]) } else if flags&vfs.OPEN_MAIN_DB != 0 { // Main datatabases may have their key specified as a PRAGMA. @@ -59,6 +59,11 @@ func (x *xtsVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs. return &xtsFile{File: file, cipher: cipher, init: x.init}, flags, nil } +// Larger sectors don't seem to significantly improve security, +// and don't affect perfomance. +// https://crossbowerbt.github.io/docs/crypto/pdf00086.pdf +// For flexibility, pick the minimum size of an SQLite page. +// https://sqlite.org/fileformat.html#pages const sectorSize = 512 type xtsFile struct { @@ -76,7 +81,9 @@ func (x *xtsFile) Pragma(name string, value string) (string, error) { case "hexkey": key, _ = hex.DecodeString(value) case "textkey": - key = x.init.KDF(value) + if len(value) > 0 { + key = x.init.KDF(value) + } default: if f, ok := x.File.(vfs.FilePragma); ok { return f.Pragma(name, value)