Skip to content

Commit

Permalink
Adiantum encrypting VFS. (#77)
Browse files Browse the repository at this point in the history
  • Loading branch information
ncruces committed Apr 18, 2024
1 parent e86789b commit ec1ed22
Show file tree
Hide file tree
Showing 20 changed files with 479 additions and 21 deletions.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ Go module `github.com/ncruces/go-sqlite3` is a `cgo`-free [SQLite](https://sqlit
It provides a [`database/sql`](https://pkg.go.dev/database/sql) compatible driver,
as well as direct access to most of the [C SQLite API](https://sqlite.org/cintro.html).

It wraps a [Wasm](https://webassembly.org/) build of SQLite, and uses [wazero](https://wazero.io/) as the runtime.\
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ runtime dependencies.
It wraps a [Wasm](https://webassembly.org/) [build](embed/) of SQLite,
and uses [wazero](https://wazero.io/) as the runtime.\
Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ runtime dependencies [^1].

### Packages

Expand Down Expand Up @@ -54,6 +55,8 @@ Go, wazero and [`x/sys`](https://pkg.go.dev/golang.org/x/sys) are the _only_ run
implements an in-memory VFS.
- [`github.com/ncruces/go-sqlite3/vfs/readervfs`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/readervfs)
implements a VFS for immutable databases.
- [`github.com/ncruces/go-sqlite3/vfs/adiantum`](https://pkg.go.dev/github.com/ncruces/go-sqlite3/vfs/adiantum)
wraps a VFS to offer encryption at rest.

### Advanced features

Expand Down Expand Up @@ -101,3 +104,6 @@ The Wasm and VFS layers are also tested by running SQLite's
- [`crawshaw.io/sqlite`](https://pkg.go.dev/crawshaw.io/sqlite)
- [`github.com/mattn/go-sqlite3`](https://pkg.go.dev/github.com/mattn/go-sqlite3)
- [`github.com/zombiezen/go-sqlite`](https://pkg.go.dev/github.com/zombiezen/go-sqlite)

[^1]: anything else you find in [`go.mod`](./go.mod) is either a test dependency,
or needed by one of the extensions.
3 changes: 3 additions & 0 deletions embed/exports.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ sqlite3_errmsg
sqlite3_error_offset
sqlite3_errstr
sqlite3_exec
sqlite3_filename_database
sqlite3_filename_journal
sqlite3_filename_wal
sqlite3_finalize
sqlite3_get_autocommit
sqlite3_get_auxdata
Expand Down
Binary file modified embed/sqlite3.wasm
Binary file not shown.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ require (
golang.org/x/sync v0.7.0
golang.org/x/sys v0.19.0
golang.org/x/text v0.14.0
lukechampine.com/adiantum v1.0.0
)

require github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect

retract v0.4.0 // tagged from the wrong branch
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
github.com/psanford/httpreadat v0.1.0 h1:VleW1HS2zO7/4c7c7zNl33fO6oYACSagjJIyMIwZLUE=
github.com/psanford/httpreadat v0.1.0/go.mod h1:Zg7P+TlBm3bYbyHTKv/EdtSJZn3qwbPwpfZ/I9GKCRE=
github.com/tetratelabs/wazero v1.7.1 h1:QtSfd6KLc41DIMpDYlJdoMc6k7QTN246DM2+n2Y/Dx8=
github.com/tetratelabs/wazero v1.7.1/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
lukechampine.com/adiantum v1.0.0 h1:xxLFgKHyno8ES1XiKLbQfU9DGiMaM2xsIJI2czgT7es=
lukechampine.com/adiantum v1.0.0/go.mod h1:kjMpBiZFjVX/FeEYcN81jyt3//7J3XjJgH9OkAXV4n0=
2 changes: 1 addition & 1 deletion internal/util/mmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (s *mmapState) init(ctx context.Context, enabled bool) context.Context {
return ctx
}

func CanMap(ctx context.Context) bool {
func CanMapFiles(ctx context.Context) bool {
s := ctx.Value(moduleKey{}).(*moduleState)
return s.mmapState.enabled
}
Expand Down
2 changes: 1 addition & 1 deletion internal/util/mmap_other.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ func (s *mmapState) init(ctx context.Context, _ bool) context.Context {
return ctx
}

func CanMap(ctx context.Context) bool {
func CanMapFiles(ctx context.Context) bool {
return false
}
8 changes: 8 additions & 0 deletions internal/util/unwrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package util

func Unwrap[T any](v T) T {
if u, ok := any(v).(interface{ Unwrap() T }); ok {
return u.Unwrap()
}
return v
}
11 changes: 10 additions & 1 deletion tests/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/ncruces/go-sqlite3"
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/tests/testcfg"
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
_ "github.com/ncruces/go-sqlite3/vfs/memdb"
)

Expand Down Expand Up @@ -56,10 +57,18 @@ func TestDB_utf16(t *testing.T) {
testDB(t, tmp)
}

func TestDB_vfs(t *testing.T) {
func TestDB_memdb(t *testing.T) {
t.Parallel()
testDB(t, "file:test.db?vfs=memdb")
}

func TestDB_adiantum(t *testing.T) {
t.Parallel()
testDB(t, "file:"+
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db"))+
"?vfs=adiantum&textkey=correct+horse+battery+staple")
}

func testDB(t testing.TB, name string) {
db, err := sqlite3.Open(name)
if err != nil {
Expand Down
24 changes: 20 additions & 4 deletions tests/parallel/parallel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
_ "github.com/ncruces/go-sqlite3/embed"
_ "github.com/ncruces/go-sqlite3/tests/testcfg"
"github.com/ncruces/go-sqlite3/vfs"
_ "github.com/ncruces/go-sqlite3/vfs/adiantum"
"github.com/ncruces/go-sqlite3/vfs/memdb"
)

func TestParallel(t *testing.T) {
func Test_parallel(t *testing.T) {
var iter int
if testing.Short() {
iter = 1000
Expand All @@ -34,7 +35,7 @@ func TestParallel(t *testing.T) {
testIntegrity(t, name)
}

func TestWAL(t *testing.T) {
func Test_wal(t *testing.T) {
if !vfs.SupportsSharedMemory {
t.Skip("skipping without shared memory")
}
Expand All @@ -48,7 +49,7 @@ func TestWAL(t *testing.T) {
testIntegrity(t, name)
}

func TestMemory(t *testing.T) {
func Test_memdb(t *testing.T) {
var iter int
if testing.Short() {
iter = 1000
Expand All @@ -61,6 +62,21 @@ func TestMemory(t *testing.T) {
testIntegrity(t, name)
}

func Test_adiantum(t *testing.T) {
var iter int
if testing.Short() {
iter = 1000
} else {
iter = 5000
}

name := "file:" +
filepath.ToSlash(filepath.Join(t.TempDir(), "test.db")) +
"?vfs=adiantum&textkey=correct+horse+battery+staple"
testParallel(t, name, iter)
testIntegrity(t, name)
}

func TestMultiProcess(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
Expand Down Expand Up @@ -112,7 +128,7 @@ func TestChildProcess(t *testing.T) {
testParallel(t, name, 1000)
}

func BenchmarkMemory(b *testing.B) {
func Benchmark_memdb(b *testing.B) {
memdb.Delete("test.db")
name := "file:/test.db?vfs=memdb"
testParallel(b, name, b.N)
Expand Down
1 change: 1 addition & 0 deletions util/ioutil/seek.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
// SeekingReaderAt implements [io.ReaderAt]
// through an underlying [io.ReadSeeker].
type SeekingReaderAt struct {
// +checklocks:l
r io.ReadSeeker
l sync.Mutex
}
Expand Down
29 changes: 29 additions & 0 deletions vfs/adiantum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Go `"adiantum"` SQLite VFS

This package wraps an SQLite VFS to offer encryption at rest.

> [!WARNING]
> This work was not certified by a cryptographer.
> If you need vetted encryption, you should purchase the
> [SQLite Encryption Extension](https://sqlite.org/see),
> and either wrap it, or seek assistance wrapping it.
The `"adiantum"` VFS wraps the default SQLite VFS using the
[Adiantum](https://github.com/lukechampine/adiantum)
tweakable and length-preserving encryption.

In general, any HBSH construction can be used to wrap any VFS.

The default Adiantum construction uses XChaCha12 for its stream cipher,
AES for its block cipher, and NH and Poly1305 for hashing.
It uses Argon2id to derive keys from plain text.

> [!IMPORTANT]
> Adiantum is typically used for disk encryption.
> The standard threat model for disk encryption considers an adversary
> that can read multiple snapshots of a disk.
> The security property that disk encryption provides is that
> the only information such an adversary can determine is
> whether the data in a sector has or has not changed over time.
The VFS encrypts database files, rollback and statement journals, and WAL files.
52 changes: 52 additions & 0 deletions vfs/adiantum/adiantum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package adiantum

import (
"sync"

"golang.org/x/crypto/argon2"
"lukechampine.com/adiantum"
"lukechampine.com/adiantum/hbsh"
)

const pepper = "github.com/ncruces/go-sqlite3/vfs/adiantum"

type adiantumCreator struct{}

func (adiantumCreator) HBSH(key []byte) *hbsh.HBSH {
return adiantum.New(key)
}

func (adiantumCreator) KDF(text string) []byte {
if key := keyCacheGet(text); key != nil {
return key[:]
}

key := argon2.IDKey([]byte(text), []byte(pepper), 1, 64*1024, 4, 32)
keyCachePut(text, (*[32]byte)(key))
return key
}

const keyCacheMaxEntries = 100

var (
// +checklocks:keyCacheMtx
keyCache = map[string]*[32]byte{}
keyCacheMtx sync.RWMutex
)

func keyCacheGet(text string) *[32]byte {
keyCacheMtx.RLock()
defer keyCacheMtx.RUnlock()
return keyCache[text]
}

func keyCachePut(text string, key *[32]byte) {
keyCacheMtx.Lock()
defer keyCacheMtx.Unlock()
if len(keyCache) >= keyCacheMaxEntries {
for k := range keyCache {
delete(keyCache, k)
}
}
keyCache[text] = key
}
50 changes: 50 additions & 0 deletions vfs/adiantum/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Package adiantum wraps an SQLite VFS to offer encryption at rest.
//
// The "adiantum" [vfs.VFS] wraps the default VFS using the
// Adiantum tweakable length-preserving encryption.
//
// Importing package adiantum registers that VFS.
//
// import _ "github.com/ncruces/go-sqlite3/vfs/adiantum"
//
// To open an encrypted database you need to provide key material.
// This is done through [URI] parameters:
//
// - key: key material in binary (32 bytes)
// - hexkey: key material in hex (64 hex digits)
// - textkey: key material in text (any length)
//
// [URI]: https://sqlite.org/uri.html
package adiantum

import (
"github.com/ncruces/go-sqlite3/vfs"
"lukechampine.com/adiantum/hbsh"
)

func init() {
Register("adiantum", vfs.Find(""), nil)
}

// 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.
func Register(name string, base vfs.VFS, cipher HBSHCreator) {
if cipher == nil {
cipher = adiantumCreator{}
}
vfs.Register("adiantum", &hbshVFS{
VFS: base,
hbsh: cipher,
})
}

// HBSHCreator creates an [hbsh.HBSH] cipher,
// given key material.
type HBSHCreator interface {
// KDF maps a secret (text) to a key of the appropriate size.
KDF(text string) (key []byte)

// HBSH creates an HBSH cipher given an appropriate key.
HBSH(key []byte) *hbsh.HBSH
}
Loading

0 comments on commit ec1ed22

Please sign in to comment.