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` | | 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"