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  }