diff --git a/README.md b/README.md
index 16acdb4..2af6ea7 100644
--- a/README.md
+++ b/README.md
@@ -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) |
| Immutable | `immutable` | `boolean` | For more information see [Immutable](https://www.sqlite.org/c3ref/open.html) |
| Journal Mode | `_journal_mode` \| `_journal` |
- DELETE
- TRUNCATE
- PERSIST
- MEMORY
- WAL
- OFF
| 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` | | For more information see [PRAGMA locking_mode](https://www.sqlite.org/pragma.html#pragma_locking_mode) |
| Mode | `mode` | | Access Mode of the database. For more information see [SQLite Open](https://www.sqlite.org/c3ref/open.html) |
| Mutex Locking | `_mutex` | | Specify mutex mode. |
diff --git a/go.mod b/go.mod
index 88c68ee..e7515d8 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/mattn/go-sqlite3
+module git.pablu.de/pablu/go-sqlite3
go 1.21
diff --git a/sqlite3.go b/sqlite3.go
index c30bb8f..68ac4b1 100644
--- a/sqlite3.go
+++ b/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.
func (c *SQLiteConn) Exec(query string, args []driver.Value) (driver.Result, error) {
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
// 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
// https://www.sqlite.org/pragma.html#pragma_case_sensitive_like
// Default or disabled the LIKE operation is case-insensitive.
@@ -1213,6 +1240,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
vfsName := ""
var cacheSize *int64
stmtCacheSize := 0
+ keyPragma := ""
pos := strings.IndexRune(dsn, '?')
if pos >= 1 {
@@ -1222,6 +1250,14 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
}
// 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 {
authCreate = true
}
@@ -1594,6 +1630,16 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
if db == nil {
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
conn := &SQLiteConn{db: db, loc: loc, txlock: txlock}
diff --git a/sqlite3_sqlcipher_test.go b/sqlite3_sqlcipher_test.go
new file mode 100644
index 0000000..4263b35
--- /dev/null
+++ b/sqlite3_sqlcipher_test.go
@@ -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)
+ }
+}
diff --git a/sqlite3_test.go b/sqlite3_test.go
index 10ecf7b..0f13a01 100644
--- a/sqlite3_test.go
+++ b/sqlite3_test.go
@@ -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) {
filename := t.Name() + ".sqlite"