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
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:
@@ -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
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/mattn/go-sqlite3
|
module git.pablu.de/pablu/go-sqlite3
|
||||||
|
|
||||||
go 1.21
|
go 1.21
|
||||||
|
|
||||||
|
|||||||
46
sqlite3.go
46
sqlite3.go
@@ -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
97
sqlite3_sqlcipher_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user