Add SQLCipher _key DSN support
Some checks failed
Go / Test (1.24, macos-latest) (push) Has been cancelled
Go / Test (1.24, ubuntu-latest) (push) Has been cancelled
Go / Test (1.25, macos-latest) (push) Has been cancelled
Go / Test (1.25, ubuntu-latest) (push) Has been cancelled
Go / Test (1.26, macos-latest) (push) Has been cancelled
Go / Test (1.26, ubuntu-latest) (push) Has been cancelled
Go / Test for Windows (1.24) (push) Has been cancelled
Go / Test for Windows (1.25) (push) Has been cancelled
Go / Test for Windows (1.26) (push) Has been cancelled
dockerfile / Run Dockerfiles in examples (push) Has been cancelled

This commit is contained in:
2026-08-06 14:00:09 +02:00
parent cc41b8c876
commit 9fdc81b5a8
5 changed files with 168 additions and 1 deletions

View File

@@ -122,6 +122,7 @@ Boolean values can be one of:
| Ignore CHECK Constraints | `_ignore_check_constraints` | `boolean` | For more information see [PRAGMA ignore_check_constraints](https://www.sqlite.org/pragma.html#pragma_ignore_check_constraints) | | Ignore CHECK Constraints | `_ignore_check_constraints` | `boolean` | For more information see [PRAGMA ignore_check_constraints](https://www.sqlite.org/pragma.html#pragma_ignore_check_constraints) |
| Immutable | `immutable` | `boolean` | For more information see [Immutable](https://www.sqlite.org/c3ref/open.html) | | Immutable | `immutable` | `boolean` | For more information see [Immutable](https://www.sqlite.org/c3ref/open.html) |
| Journal Mode | `_journal_mode` \| `_journal` | <ul><li>DELETE</li><li>TRUNCATE</li><li>PERSIST</li><li>MEMORY</li><li>WAL</li><li>OFF</li></ul> | For more information see [PRAGMA journal_mode](https://www.sqlite.org/pragma.html#pragma_journal_mode) | | Journal Mode | `_journal_mode` \| `_journal` | <ul><li>DELETE</li><li>TRUNCATE</li><li>PERSIST</li><li>MEMORY</li><li>WAL</li><li>OFF</li></ul> | For more information see [PRAGMA journal_mode](https://www.sqlite.org/pragma.html#pragma_journal_mode) |
| SQLCipher Key | `_key` | passphrase or `x'<64 hexadecimal digits>'` | Applies `PRAGMA key` immediately after opening and before every driver initialization PRAGMA. Requires SQLCipher linked with the `libsqlite3` build tag. |
| Locking Mode | `_locking_mode` \| `_locking` | <ul><li>NORMAL</li><li>EXCLUSIVE</li></ul> | For more information see [PRAGMA locking_mode](https://www.sqlite.org/pragma.html#pragma_locking_mode) | | Locking Mode | `_locking_mode` \| `_locking` | <ul><li>NORMAL</li><li>EXCLUSIVE</li></ul> | For more information see [PRAGMA locking_mode](https://www.sqlite.org/pragma.html#pragma_locking_mode) |
| Mode | `mode` | <ul><li>ro</li><li>rw</li><li>rwc</li><li>memory</li></ul> | Access Mode of the database. For more information see [SQLite Open](https://www.sqlite.org/c3ref/open.html) | | Mode | `mode` | <ul><li>ro</li><li>rw</li><li>rwc</li><li>memory</li></ul> | Access Mode of the database. For more information see [SQLite Open](https://www.sqlite.org/c3ref/open.html) |
| Mutex Locking | `_mutex` | <ul><li>no</li><li>full</li></ul> | Specify mutex mode. | | Mutex Locking | `_mutex` | <ul><li>no</li><li>full</li></ul> | Specify mutex mode. |

2
go.mod
View File

@@ -1,4 +1,4 @@
module github.com/mattn/go-sqlite3 module git.pablu.de/pablu/go-sqlite3
go 1.21 go 1.21

View File

@@ -946,6 +946,28 @@ func lastError(db *C.sqlite3) error {
} }
} }
func sqlCipherKeyPragma(key string) (string, error) {
if strings.IndexByte(key, 0) >= 0 {
return "", errors.New("Invalid _key: contains NUL")
}
if isSQLCipherRawKey(key) {
return "PRAGMA key = \"" + key + "\";", nil
}
return "PRAGMA key = '" + strings.ReplaceAll(key, "'", "''") + "';", nil
}
func isSQLCipherRawKey(key string) bool {
if len(key) != 67 || (key[0] != 'x' && key[0] != 'X') || key[1] != '\'' || key[66] != '\'' {
return false
}
for _, c := range key[2:66] {
if !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') && !(c >= 'A' && c <= 'F') {
return false
}
}
return true
}
// Exec implements Execer. // Exec implements Execer.
func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, error) { func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, error) {
return c.exec(context.Background(), query, valueToNamedValue(args)) return c.exec(context.Background(), query, valueToNamedValue(args))
@@ -1137,6 +1159,11 @@ func (c *SQLiteConn) begin(ctx context.Context) (driver.Tx, error) {
// _busy_timeout=XXX"| _timeout=XXX // _busy_timeout=XXX"| _timeout=XXX
// Specify value for sqlite3_busy_timeout. // Specify value for sqlite3_busy_timeout.
// //
// _key=KEY
// Set the SQLCipher key before any driver initialization PRAGMA. KEY is a
// passphrase or a raw key expression of the form x'<64 hexadecimal digits>'.
// Requires a SQLCipher library linked with the libsqlite3 build tag.
//
// _case_sensitive_like=Boolean | _cslike=Boolean // _case_sensitive_like=Boolean | _cslike=Boolean
// https://www.sqlite.org/pragma.html#pragma_case_sensitive_like // https://www.sqlite.org/pragma.html#pragma_case_sensitive_like
// Default or disabled the LIKE operation is case-insensitive. // Default or disabled the LIKE operation is case-insensitive.
@@ -1213,6 +1240,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
vfsName := "" vfsName := ""
var cacheSize *int64 var cacheSize *int64
stmtCacheSize := 0 stmtCacheSize := 0
keyPragma := ""
pos := strings.IndexRune(dsn, '?') pos := strings.IndexRune(dsn, '?')
if pos >= 1 { if pos >= 1 {
@@ -1222,6 +1250,14 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
} }
// Authentication // Authentication
if _, ok := params["_key"]; ok {
var err error
keyPragma, err = sqlCipherKeyPragma(params.Get("_key"))
if err != nil {
return nil, err
}
}
if _, ok := params["_auth"]; ok { if _, ok := params["_auth"]; ok {
authCreate = true authCreate = true
} }
@@ -1594,6 +1630,16 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
if db == nil { if db == nil {
return nil, errors.New("sqlite succeeded without returning a database") return nil, errors.New("sqlite succeeded without returning a database")
} }
if keyPragma != "" {
cs := C.CString(keyPragma)
rv := C.sqlite3_exec(db, cs, nil, nil, nil)
C.free(unsafe.Pointer(cs))
if rv != C.SQLITE_OK {
err := lastError(db)
C.sqlite3_close_v2(db)
return nil, err
}
}
// Create connection to SQLite // Create connection to SQLite
conn := &SQLiteConn{db: db, loc: loc, txlock: txlock} conn := &SQLiteConn{db: db, loc: loc, txlock: txlock}

97
sqlite3_sqlcipher_test.go Normal file
View File

@@ -0,0 +1,97 @@
//go:build libsqlite3 && sqlcipher
// +build libsqlite3,sqlcipher
package sqlite3
import (
"database/sql"
"net/url"
"os"
"testing"
)
const sqlCipherTestRawKey = "x'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'"
func requireSQLCipher(t *testing.T, db *sql.DB) {
t.Helper()
var version string
if err := db.QueryRow("PRAGMA cipher_version").Scan(&version); err != nil || version == "" {
t.Fatalf("SQLCipher is required for this test; link it with -tags libsqlite3: %v", err)
}
}
func TestSQLCipherKeyDSN(t *testing.T) {
filename := TempFilename(t)
defer os.Remove(filename)
dsn := "file:" + filename + "?_key=" + url.QueryEscape(sqlCipherTestRawKey)
db, err := sql.Open("sqlite3", dsn)
if err != nil {
t.Fatal(err)
}
requireSQLCipher(t, db)
if _, err := db.Exec("CREATE TABLE test (value TEXT)"); err != nil {
t.Fatal(err)
}
if _, err := db.Exec("INSERT INTO test VALUES ('encrypted')"); err != nil {
t.Fatal(err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
db, err = sql.Open("sqlite3", dsn)
if err != nil {
t.Fatal(err)
}
var value string
if err := db.QueryRow("SELECT value FROM test").Scan(&value); err != nil {
t.Fatal(err)
}
if value != "encrypted" {
t.Fatalf("value = %q; want encrypted", value)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
for _, key := range []string{
"x'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'",
"",
} {
db, err = sql.Open("sqlite3", "file:"+filename+"?_key="+url.QueryEscape(key))
if err != nil {
t.Fatal(err)
}
var count int
err = db.QueryRow("SELECT count(*) FROM test").Scan(&count)
db.Close()
if err == nil {
t.Errorf("reading encrypted database with key %q succeeded", key)
}
}
}
func TestSQLCipherKeyAbsentPreservesSQLiteBehavior(t *testing.T) {
filename := TempFilename(t)
defer os.Remove(filename)
db, err := sql.Open("sqlite3", filename)
if err != nil {
t.Fatal(err)
}
defer db.Close()
if _, err := db.Exec("CREATE TABLE test (value TEXT)"); err != nil {
t.Fatal(err)
}
if _, err := db.Exec("INSERT INTO test VALUES ('plain')"); err != nil {
t.Fatal(err)
}
var value string
if err := db.QueryRow("SELECT value FROM test").Scan(&value); err != nil {
t.Fatal(err)
}
if value != "plain" {
t.Fatalf("value = %q; want plain", value)
}
}

View File

@@ -103,6 +103,29 @@ func TestOpen(t *testing.T) {
} }
} }
func TestSQLCipherKeyPragma(t *testing.T) {
rawKey := "x'0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'"
tests := []struct {
key string
want string
}{
{rawKey, "PRAGMA key = \"" + rawKey + "\";"},
{"pass'word; PRAGMA foreign_keys = OFF", "PRAGMA key = 'pass''word; PRAGMA foreign_keys = OFF';"},
}
for _, test := range tests {
got, err := sqlCipherKeyPragma(test.key)
if err != nil {
t.Fatalf("sqlCipherKeyPragma(%q): %v", test.key, err)
}
if got != test.want {
t.Errorf("sqlCipherKeyPragma(%q) = %q; want %q", test.key, got, test.want)
}
}
if _, err := sqlCipherKeyPragma("pass\x00word"); err == nil {
t.Error("sqlCipherKeyPragma accepted a key containing NUL")
}
}
func TestOpenWithVFS(t *testing.T) { func TestOpenWithVFS(t *testing.T) {
filename := t.Name() + ".sqlite" filename := t.Name() + ".sqlite"