github.com/status-im/status-go@v1.1.0/sqlite/sqlite.go (about) 1 package sqlite 2 3 import ( 4 "database/sql" 5 "database/sql/driver" 6 "errors" 7 "fmt" 8 "net/url" 9 "os" 10 "runtime" 11 "strings" 12 13 sqlcipher "github.com/mutecomm/go-sqlcipher/v4" // We require go sqlcipher that overrides default implementation 14 15 "github.com/status-im/status-go/common/dbsetup" 16 ) 17 18 const ( 19 // The reduced number of kdf iterations (for performance reasons) which is 20 // used as the default value 21 // https://github.com/status-im/status-go/pull/1343 22 // https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA 23 ReducedKDFIterationsNumber = 3200 24 25 // WALMode for sqlite. 26 WALMode = "wal" 27 InMemoryPath = ":memory:" 28 V4CipherPageSize = 8192 29 V3CipherPageSize = 1024 30 sqlMainDatabase = "main" 31 ) 32 33 // DecryptDB completely removes the encryption from the db 34 func DecryptDB(oldPath string, newPath string, key string, kdfIterationsNumber int) error { 35 36 db, err := openDB(oldPath, key, kdfIterationsNumber, V4CipherPageSize) 37 if err != nil { 38 return err 39 } 40 41 _, err = db.Exec(`ATTACH DATABASE '` + newPath + `' AS plaintext KEY ''`) 42 if err != nil { 43 return err 44 } 45 46 _, err = db.Exec(`SELECT sqlcipher_export('plaintext')`) 47 if err != nil { 48 return err 49 } 50 _, err = db.Exec(`DETACH DATABASE plaintext`) 51 return err 52 } 53 54 func encryptDB(db *sql.DB, encryptedPath string, key string, kdfIterationsNumber int, onStart func(), onEnd func()) error { 55 if onStart != nil { 56 onStart() 57 } 58 if onEnd != nil { 59 defer onEnd() 60 } 61 62 attachedDbName := "encrypted" 63 err := attachDatabaseWithDefaultSettings(db, encryptedPath, attachedDbName, key, kdfIterationsNumber) 64 if err != nil { 65 return err 66 } 67 68 _, err = db.Exec(fmt.Sprintf(`SELECT sqlcipher_export('%s')`, attachedDbName)) 69 if err != nil { 70 return err 71 } 72 _, err = db.Exec(fmt.Sprintf(`DETACH DATABASE %s`, attachedDbName)) 73 return err 74 } 75 76 func attachDatabaseWithDefaultSettings(db *sql.DB, attachedDbPath string, attachedDbName string, key string, kdfIterationsNumber int) error { 77 _, err := db.Exec(fmt.Sprintf(`ATTACH DATABASE '%s' AS %s KEY '%s'`, attachedDbPath, attachedDbName, key)) 78 if err != nil { 79 return err 80 } 81 82 if kdfIterationsNumber <= 0 { 83 kdfIterationsNumber = dbsetup.ReducedKDFIterationsNumber 84 } 85 86 if _, err := db.Exec(fmt.Sprintf(`PRAGMA %s.busy_timeout = 60000`, attachedDbName)); err != nil { 87 return errors.New("failed to set `busy_timeout` pragma on attached db") 88 } 89 90 return setDatabaseCipherSettings(db, kdfIterationsNumber, attachedDbName) 91 } 92 93 func setDatabaseCipherSettings(db *sql.DB, kdfIterationsNumber int, dbNameOpt ...string) error { 94 dbName := sqlMainDatabase 95 if len(dbNameOpt) > 0 { 96 dbName = dbNameOpt[0] 97 } 98 99 _, err := db.Exec(fmt.Sprintf("PRAGMA %s.kdf_iter = '%d'", dbName, kdfIterationsNumber)) 100 if err != nil { 101 return err 102 } 103 104 if _, err := db.Exec(fmt.Sprintf("PRAGMA %s.cipher_page_size = %d", dbName, V4CipherPageSize)); err != nil { 105 fmt.Println("failed to set cipher_page_size pragma") 106 return err 107 } 108 if _, err := db.Exec(fmt.Sprintf("PRAGMA %s.cipher_hmac_algorithm = HMAC_SHA1", dbName)); err != nil { 109 fmt.Println("failed to set cipher_hmac_algorithm pragma") 110 return err 111 } 112 113 if _, err := db.Exec(fmt.Sprintf("PRAGMA %s.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1", dbName)); err != nil { 114 fmt.Println("failed to set cipher_kdf_algorithm pragma") 115 return err 116 } 117 118 return nil 119 } 120 121 // EncryptDB takes a plaintext database and adds encryption 122 func EncryptDB(unencryptedPath string, encryptedPath string, key string, kdfIterationsNumber int, onStart func(), onEnd func()) error { 123 _ = os.Remove(encryptedPath) 124 125 db, err := OpenUnecryptedDB(unencryptedPath) 126 if err != nil { 127 return err 128 } 129 return encryptDB(db, encryptedPath, key, kdfIterationsNumber, onStart, onEnd) 130 } 131 132 // Export takes an encrypted database and re-encrypts it in a new file, with a new key 133 func ExportDB(encryptedPath string, key string, kdfIterationsNumber int, newPath string, newKey string, onStart func(), onEnd func()) error { 134 db, err := openDB(encryptedPath, key, kdfIterationsNumber, V4CipherPageSize) 135 if err != nil { 136 return err 137 } 138 defer db.Close() 139 return encryptDB(db, newPath, newKey, kdfIterationsNumber, onStart, onEnd) 140 } 141 142 func buildSqlcipherDSN(path string) (string, error) { 143 if path == InMemoryPath { 144 return InMemoryPath, nil 145 } 146 147 // Adding sqlcipher query parameter to the DSN 148 queryOperator := "?" 149 150 if queryStart := strings.IndexRune(path, '?'); queryStart != -1 { 151 params, err := url.ParseQuery(path[queryStart+1:]) 152 if err != nil { 153 return "", err 154 } 155 156 if len(params) > 0 { 157 queryOperator = "&" 158 } 159 } 160 161 // We need to set txlock=immediate to avoid "database is locked" errors during concurrent write operations 162 // This could happen when a read transaction is promoted to write transaction 163 // https://www.sqlite.org/lang_transaction.html 164 return path + queryOperator + "_txlock=immediate", nil 165 } 166 167 func openDB(path string, key string, kdfIterationsNumber int, cipherPageSize int) (*sql.DB, error) { 168 driverName := fmt.Sprintf("sqlcipher_with_extensions-%d", len(sql.Drivers())) 169 sql.Register(driverName, &sqlcipher.SQLiteDriver{ 170 ConnectHook: func(conn *sqlcipher.SQLiteConn) error { 171 if _, err := conn.Exec("PRAGMA foreign_keys=ON", []driver.Value{}); err != nil { 172 return errors.New("failed to set `foreign_keys` pragma") 173 } 174 175 if _, err := conn.Exec(fmt.Sprintf("PRAGMA key = '%s'", key), []driver.Value{}); err != nil { 176 return errors.New("failed to set `key` pragma") 177 } 178 179 if kdfIterationsNumber <= 0 { 180 kdfIterationsNumber = dbsetup.ReducedKDFIterationsNumber 181 } 182 183 if _, err := conn.Exec(fmt.Sprintf("PRAGMA cipher_page_size = %d", cipherPageSize), nil); err != nil { 184 fmt.Println("failed to set cipher_page_size pragma") 185 return err 186 } 187 if _, err := conn.Exec("PRAGMA cipher_hmac_algorithm = HMAC_SHA1", nil); err != nil { 188 fmt.Println("failed to set cipher_hmac_algorithm pragma") 189 return err 190 } 191 192 if _, err := conn.Exec("PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1", nil); err != nil { 193 fmt.Println("failed to set cipher_kdf_algorithm pragma") 194 return err 195 } 196 197 if _, err := conn.Exec(fmt.Sprintf("PRAGMA kdf_iter = '%d'", kdfIterationsNumber), []driver.Value{}); err != nil { 198 return errors.New("failed to set `kdf_iter` pragma") 199 } 200 201 // readers do not block writers and faster i/o operations 202 if _, err := conn.Exec("PRAGMA journal_mode=WAL", []driver.Value{}); err != nil && path != InMemoryPath { 203 return fmt.Errorf("failed to set `journal_mode` pragma: %w", err) 204 } 205 206 // workaround to mitigate the issue of "database is locked" errors during concurrent write operations 207 if _, err := conn.Exec("PRAGMA busy_timeout=60000", []driver.Value{}); err != nil { 208 return errors.New("failed to set `busy_timeout` pragma") 209 } 210 211 return nil 212 }, 213 }) 214 215 dsn, err := buildSqlcipherDSN(path) 216 if err != nil { 217 return nil, err 218 } 219 220 db, err := sql.Open(driverName, dsn) 221 if err != nil { 222 return nil, err 223 } 224 225 if path == InMemoryPath { 226 db.SetMaxOpenConns(1) 227 } else { 228 nproc := func() int { 229 maxProcs := runtime.GOMAXPROCS(0) 230 numCPU := runtime.NumCPU() 231 if maxProcs < numCPU { 232 return maxProcs 233 } 234 return numCPU 235 }() 236 db.SetMaxOpenConns(nproc) 237 db.SetMaxIdleConns(nproc) 238 } 239 240 // Dummy select to check if the key is correct. Will return last error from initialization 241 if _, err := db.Exec("SELECT 'Key check'"); err != nil { 242 db.Close() 243 return nil, err 244 } 245 246 return db, nil 247 } 248 249 // OpenDB opens encrypted database. 250 func OpenDB(path string, key string, kdfIterationsNumber int) (*sql.DB, error) { 251 return openDB(path, key, kdfIterationsNumber, V4CipherPageSize) 252 } 253 254 // OpenUnecryptedDB opens database with setting PRAGMA key. 255 func OpenUnecryptedDB(path string) (*sql.DB, error) { 256 db, err := sql.Open("sqlite3", path) 257 if err != nil { 258 return nil, err 259 } 260 261 // Disable concurrent access as not supported by the driver 262 db.SetMaxOpenConns(1) 263 264 if _, err = db.Exec("PRAGMA foreign_keys=ON"); err != nil { 265 return nil, err 266 } 267 // readers do not block writers and faster i/o operations 268 // https://www.sqlite.org/draft/wal.html 269 // must be set after db is encrypted 270 if path != InMemoryPath { 271 var mode string 272 err = db.QueryRow("PRAGMA journal_mode=WAL").Scan(&mode) 273 if err != nil { 274 return nil, err 275 } 276 if mode != WALMode { 277 return nil, fmt.Errorf("unable to set journal_mode to WAL. actual mode %s", mode) 278 } 279 } 280 281 return db, nil 282 } 283 284 func ChangeEncryptionKey(path string, key string, kdfIterationsNumber int, newKey string, onStart func(), onEnd func()) error { 285 if onStart != nil { 286 onStart() 287 } 288 289 if onEnd != nil { 290 defer onEnd() 291 } 292 293 if kdfIterationsNumber <= 0 { 294 kdfIterationsNumber = dbsetup.ReducedKDFIterationsNumber 295 } 296 297 db, err := openDB(path, key, kdfIterationsNumber, V4CipherPageSize) 298 299 if err != nil { 300 return err 301 } 302 303 resetKeyString := fmt.Sprintf("PRAGMA rekey = '%s'", newKey) 304 if _, err = db.Exec(resetKeyString); err != nil { 305 return errors.New("failed to set rekey pragma") 306 } 307 308 return nil 309 } 310 311 // MigrateV3ToV4 migrates database from v3 to v4 format with encryption. 312 func MigrateV3ToV4(v3Path string, v4Path string, key string, kdfIterationsNumber int, onStart func(), onEnd func()) error { 313 314 db, err := openDB(v3Path, key, kdfIterationsNumber, V3CipherPageSize) 315 316 if err != nil { 317 fmt.Println("failed to open db", err) 318 return err 319 } 320 defer db.Close() 321 322 return encryptDB(db, v4Path, key, kdfIterationsNumber, onStart, onEnd) 323 }