Files
sqlite3/vfs/xts/xts.go
2024-10-21 15:07:30 +01:00

276 lines
6.6 KiB
Go

package xts
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/util/vfsutil"
"github.com/ncruces/go-sqlite3/vfs"
)
type xtsVFS struct {
vfs.VFS
init XTSCreator
}
func (x *xtsVFS) Open(name string, flags vfs.OpenFlag) (vfs.File, vfs.OpenFlag, error) {
// notest // OpenFilename is called instead
return nil, 0, sqlite3.CANTOPEN
}
func (x *xtsVFS) OpenFilename(name *vfs.Filename, flags vfs.OpenFlag) (file vfs.File, _ vfs.OpenFlag, err error) {
if hf, ok := x.VFS.(vfs.VFSFilename); ok {
file, flags, err = hf.OpenFilename(name, flags)
} else {
file, flags, err = x.VFS.Open(name.String(), flags)
}
// Encrypt everything except super journals and memory files.
if err != nil || flags&(vfs.OPEN_SUPER_JOURNAL|vfs.OPEN_MEMORY) != 0 {
return file, flags, err
}
var cipher *xts.Cipher
if f, ok := vfsutil.UnwrapFile[*xtsFile](name.DatabaseFile()); ok {
cipher = f.cipher
} else {
var key []byte
if params := name.URIParameters(); name == nil {
key = x.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 && 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.
return &xtsFile{File: file, init: x.init}, flags, nil
}
cipher = x.init.XTS(key)
}
if cipher == nil {
return nil, flags, sqlite3.CANTOPEN
}
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
// Ensure sectorSize is a power of two.
var _ [0]struct{} = [sectorSize & (sectorSize - 1)]struct{}{}
func roundDown(i int64) int64 {
return i &^ (sectorSize - 1)
}
func roundUp[T int | int64](i T) T {
return (i + (sectorSize - 1)) &^ (sectorSize - 1)
}
type xtsFile struct {
vfs.File
init XTSCreator
cipher *xts.Cipher
sector [sectorSize]byte
}
func (x *xtsFile) Pragma(name string, value string) (string, error) {
var key []byte
switch name {
case "key":
key = []byte(value)
case "hexkey":
key, _ = hex.DecodeString(value)
case "textkey":
if len(value) > 0 {
key = x.init.KDF(value)
}
default:
return vfsutil.WrapPragma(x.File, name, value)
}
if x.cipher = x.init.XTS(key); x.cipher != nil {
return "ok", nil
}
return "", sqlite3.CANTOPEN
}
func (x *xtsFile) ReadAt(p []byte, off int64) (n int, err error) {
if x.cipher == nil {
// 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 be specified as a PRAGMA.
return 0, io.EOF
}
return 0, sqlite3.CANTOPEN
}
min := roundDown(off)
max := roundUp(off + int64(len(p)))
// Read one block at a time.
for ; min < max; min += sectorSize {
m, err := x.File.ReadAt(x.sector[:], min)
if m != sectorSize {
return n, err
}
sectorNum := uint64(min / sectorSize)
x.cipher.Decrypt(x.sector[:], x.sector[:], sectorNum)
data := x.sector[:]
if off > 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 := roundDown(off)
max := roundUp(off + int64(len(p)))
// 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) DeviceCharacteristics() vfs.DeviceCharacteristic {
var _ [0]struct{} = [sectorSize - 512]struct{}{} // Ensure sectorSize is 512.
return x.File.DeviceCharacteristics() & (0 |
// The only safe flags are these:
vfs.IOCAP_ATOMIC512 |
vfs.IOCAP_UNDELETABLE_WHEN_OPEN |
vfs.IOCAP_IMMUTABLE |
vfs.IOCAP_BATCH_ATOMIC)
}
func (x *xtsFile) SectorSize() int {
return util.LCM(x.File.SectorSize(), sectorSize)
}
func (x *xtsFile) Truncate(size int64) error {
return x.File.Truncate(roundUp(size))
}
func (x *xtsFile) ChunkSize(size int) {
vfsutil.WrapChunkSize(x.File, roundUp(size))
}
func (x *xtsFile) SizeHint(size int64) error {
return vfsutil.WrapSizeHint(x.File, roundUp(size))
}
func (x *xtsFile) Unwrap() vfs.File {
return x.File
}
func (x *xtsFile) SharedMemory() vfs.SharedMemory {
return vfsutil.WrapSharedMemory(x.File)
}
// Wrap optional methods.
func (x *xtsFile) LockState() vfs.LockLevel {
return vfsutil.WrapLockState(x.File) // notest
}
func (x *xtsFile) PersistentWAL() bool {
return vfsutil.WrapPersistentWAL(x.File) // notest
}
func (x *xtsFile) SetPersistentWAL(keepWAL bool) {
vfsutil.WrapSetPersistentWAL(x.File, keepWAL) // notest
}
func (x *xtsFile) HasMoved() (bool, error) {
return vfsutil.WrapHasMoved(x.File) // notest
}
func (x *xtsFile) Overwrite() error {
return vfsutil.WrapOverwrite(x.File) // notest
}
func (x *xtsFile) CommitPhaseTwo() error {
return vfsutil.WrapCommitPhaseTwo(x.File) // notest
}
func (x *xtsFile) BeginAtomicWrite() error {
return vfsutil.WrapBeginAtomicWrite(x.File) // notest
}
func (x *xtsFile) CommitAtomicWrite() error {
return vfsutil.WrapCommitAtomicWrite(x.File) // notest
}
func (x *xtsFile) RollbackAtomicWrite() error {
return vfsutil.WrapRollbackAtomicWrite(x.File) // notest
}
func (x *xtsFile) CheckpointStart() {
vfsutil.WrapCheckpointStart(x.File) // notest
}
func (x *xtsFile) CheckpointDone() {
vfsutil.WrapCheckpointDone(x.File) // notest
}