-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Description
Environment:
Go: go version: go1.23.8 linux/amd64
mattn/go-sqlite3: github.com/mattn/go-sqlite3 v1.14.28
OS/Environment: Debian Bookworm (via mcr.microsoft.com/devcontainers/go:1.23 Docker image)
SQLCipher: 3.44.2 2023-11-24 11:41:44 ebead0e7230cd33bcec9f95d2183069565b9e709bf745c9b5db65cc0cbf9alt1 (64-bit) (SQLCipher 4.5.6 community) Built from source
Build Tool: go test
Problem:
Attempts to create or open an encrypted SQLite database using mattn/go-sqlite3 with SQLCipher support enabled result in an unencrypted database file, even when SQLCipher v4.5.6 is correctly built, installed, and discoverable by the linker. Both DSN parameters (_pragma_key=) and explicit PRAGMA key = '...' execution after opening fail to enable encryption.
Code & CGO Flags:
File: internal/sqlite/securedb.go
//go:build cgo
package sqlite
/*
#cgo CFLAGS: -DSQLITE_HAS_CODEC
#cgo LDFLAGS: -lsqlcipher
*/
import (
"database/sql"
"fmt"
_ "github.com/mattn/go-sqlite3"
)
// Open returns an encrypted handle. Relies on PRAGMA key.
func Open(path string, key []byte) (*sql.DB, error) {
// DSN without key parameter.
dsn := fmt.Sprintf(
"file:%s?_pragma_cipher_page_size=4096&_busy_timeout=10000",
path,
)
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, fmt.Errorf("sql open failed: %w", err)
}
// --- Explicitly execute PRAGMA key AFTER open ---
pragmaKeySQL := fmt.Sprintf("PRAGMA key = \"x'%x'\";", key)
if _, err := db.Exec(pragmaKeySQL); err != nil {
// If *this* fails, it might indicate an issue with the key or encryption setup
_ = db.Close() // Close on error
return nil, fmt.Errorf("failed to execute PRAGMA key: %w", err)
}
// --- End Explicit PRAGMA ---
// --- Minimal Check: Ping AFTER setting key ---
// If PRAGMA key worked, Ping should still succeed.
// If PRAGMA key failed silently earlier but encryption *was* active, Ping might fail here.
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, fmt.Errorf("db ping failed after setting key: %w", err)
}
// Assume success if PRAGMA key and Ping didn't error.
// Let the caller perform actual read/write checks.
return db, nil
}
File: internal/sqlite/securedb_test.go
package sqlite
import (
"bytes"
"fmt"
"os" // Import os for file reading
"path/filepath"
"testing"
"github.com/n1/n1/internal/crypto"
"github.com/stretchr/testify/require"
)
// --- Keep your existing Open function in securedb.go (Simplified version above) ---
func TestEncryptionEndToEnd(t *testing.T) {
tmpDir := t.TempDir()
dbPath := filepath.Join(tmpDir, "vault_e2e.db")
mk, _ := crypto.Generate(32)
t.Logf("Database path: %s", dbPath)
const testData = "this is secret"
const testID = 1
// --- 1. Create DB, Write Data with Correct Key ---
dbWrite, err := Open(dbPath, mk)
require.NoError(t, err, "E2E: Initial open with correct key failed")
_, err = dbWrite.Exec(`CREATE TABLE secrets (id INTEGER PRIMARY KEY, data TEXT)`)
require.NoError(t, err, "E2E: Create table failed")
_, err = dbWrite.Exec(`INSERT INTO secrets (id, data) VALUES (?, ?)`, testID, testData)
require.NoError(t, err, "E2E: Insert data failed")
err = dbWrite.Close()
require.NoError(t, err, "E2E: Close after write failed")
// --- DIAGNOSTIC: Check file header by reading raw bytes ---
t.Logf("--- Reading file header directly ---")
fileBytes, readErr := os.ReadFile(dbPath)
if readErr != nil {
t.Logf("!!! Error reading database file: %v", readErr)
} else if len(fileBytes) < 16 {
t.Logf("!!! Database file is too short (< 16 bytes) !!!")
} else {
header := fileBytes[:16]
expectedHeader := []byte("SQLite format 3\000") // Note the null terminator \000
// Log the actual header bytes found
t.Logf("Actual file header bytes: %x", header)
if bytes.Equal(header, expectedHeader) {
t.Logf("!!! DIAGNOSTIC: File header matches 'SQLite format 3' - DATABASE IS NOT ENCRYPTED !!!")
} else {
t.Logf("--- DIAGNOSTIC: File header does not match 'SQLite format 3' - Likely Encrypted (as expected) ---")
}
}
t.Logf("--- Finished file header check ---")
// --- End DIAGNOSTIC ---
// --- 2. Attempt to Open with WRONG Key and READ Data ---
wrongKey, _ := crypto.Generate(32)
dbReadWrong, openErrWrong := Open(dbPath, wrongKey)
var readDataWrong string
var queryErrWrong error
if dbReadWrong != nil {
queryErrWrong = dbReadWrong.QueryRow(`SELECT data FROM secrets WHERE id = ?`, testID).Scan(&readDataWrong)
_ = dbReadWrong.Close()
} else {
queryErrWrong = fmt.Errorf("E2E: Open itself failed with wrong key: %w", openErrWrong)
}
require.Error(t, queryErrWrong, "E2E: Reading data with WRONG key succeeded, expected an error!")
t.Logf("E2E: Successfully failed to read with wrong key (as required by test logic): %v", queryErrWrong)
// --- 3. Open with CORRECT Key and READ Data ---
dbReadCorrect, err := Open(dbPath, mk)
require.NoError(t, err, "E2E: Reopen with correct key failed")
var readDataCorrect string
err = dbReadCorrect.QueryRow(`SELECT data FROM secrets WHERE id = ?`, testID).Scan(&readDataCorrect)
require.NoError(t, err, "E2E: Reading data with CORRECT key failed")
require.Equal(t, testData, readDataCorrect, "E2E: Data read with correct key does not match original data")
err = dbReadCorrect.Close()
require.NoError(t, err, "E2E: Close after correct read failed")
}
Steps to Reproduce:
Set up a Debian Bookworm environment (e.g., using mcr.microsoft.com/devcontainers/go:1.23).
Install build dependencies and build+install SQLCipher v4.5.6 from source (ensuring ldconfig registers /usr/local/lib).
Simplified example from devcontainer postCreateCommand
sudo apt-get update && sudo apt-get install -y build-essential tclsh libssl-dev git sqlite3
cd /tmp && git clone --depth 1 --branch v4.5.6 https://github.com/sqlcipher/sqlcipher.git
cd sqlcipher && ./configure CFLAGS="-DSQLITE_HAS_CODEC" LDFLAGS="-lcrypto" --with-crypto-lib=openssl && make -j$(nproc) && sudo make install && sudo ldconfig
Use the Go code provided above (internal/sqlite/securedb.go and internal/sqlite/securedb_test.go).
Run go test -v ./... in the project root.
Actual Results:
The test fails at the require.Error assertion in step 2.
Reading the header of the database file created in step 1 confirms it starts with SQLite format 3\0, indicating it is not encrypted. (Diagnostic check added to test code confirms this).
The PRAGMA key = ... command in the Open function does not return an error but does not result in an encrypted database or prevent reads with an incorrect key.
Expected Results:
The database file created in step 1 should have an encrypted header (not starting with SQLite format 3\0).
The call to dbReadWrong.QueryRow(...).Scan(...) in step 2 should return an error (e.g., "file is not a database", "bad parameter or other API misuse", etc.) because the wrong key was used.
The require.Error(t, queryErrWrong, ...) assertion should pass.
The overall test TestEncryptionEndToEnd should pass.
Troubleshooting Already Attempted:
Verified SQLCipher v4.5.6 installation (sqlcipher --version) and linker path (ldconfig -p).
Verified CGO flags (-DSQLITE_HAS_CODEC, -lsqlcipher) are present in verbose build logs (go test -x).
Tried both DSN _pragma_key and explicit PRAGMA key after sql.Open.
Attempted static linking (-tags=sqlite_static with appropriate LDFLAGS), which also resulted in an unencrypted database.
Downgraded to v1.14.22 and v1.14.17 with no change in test results.
Any guidance on why encryption might not be activating under these conditions would be appreciated.