/
encdb.go
257 lines (240 loc) · 7.13 KB
/
encdb.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
// Copyright (c) 2015 Mute Communications Ltd.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package encdb defines an encrypted database used within Mute.
Such an encrypted database consists of two files for a given database file with
name "dbname":
dbname.db
dbname.key
The file "dbname.db" is an AES-256 encrypted sqlite3 file managed by the
package "github.com/mutecomm/go-sqlcipher/v4". The file named "dbname.key" is an
AES-256 encrypted text file which contains the (randomly generated) raw
encryption key for "dbname.db". To decrypt the key file the key derivation
function PBKDF2 is applied to a supplied passphrase (with a configurable number
of iterations) and the derived key is used as the AES-256 key for "dbname.key".
This design allows a very cheap rekey of the database, because only the key
file needs to be changed and the database file itself doesn't have to be
modified for a rekey operation.
*/
package encdb
import (
"database/sql"
"encoding/hex"
"errors"
"fmt"
"os"
"github.com/mutecomm/go-sqlcipher/v4"
)
// DBSuffix defines the suffix for database files.
const DBSuffix = ".db"
// KeySuffix defines the suffix for key files.
const KeySuffix = ".key"
// KDFIterations defines a default number of KDF iterations.
const KDFIterations = 64000
func createTables(db *sql.DB, createStmts []string) error {
for _, stmt := range createStmts {
if _, err := db.Exec(stmt); err != nil {
return fmt.Errorf("encdb: %q: %s", err, stmt)
}
}
return nil
}
// Create tries to create an encrypted database with the given passphrase and
// iter many KDF iterations. Thereby, dbname is the prefix of the following
// two database files which will be created and must not exist already:
//
// dbname.db
// dbname.key
//
// The SQL database is initialized with the statements given in createStmts.
// In case of error (for example, the database files do exist already or
// cannot be created) an error is returned.
func Create(dbname string, passphrase []byte, iter int, createStmts []string) error {
dbfile := dbname + DBSuffix
keyfile := dbname + KeySuffix
// make sure files do not exist already
exists, err := fileExists(dbfile)
if err != nil {
return err
}
if exists {
return fmt.Errorf("encdb: dbfile '%s' exists already", dbfile)
}
exists, err = fileExists(keyfile)
if err != nil {
return err
}
if exists {
return fmt.Errorf("encdb: keyfile '%s' exists already", keyfile)
}
// create keyfile
key, err := generateKeyfile(keyfile, passphrase, iter)
if err != nil {
return err
}
// create DB
dbfileWithDSN := dbfile +
fmt.Sprintf("?_pragma_key=x'%s'&_pragma_cipher_page_size=4096",
hex.EncodeToString(key))
db, err := sql.Open("sqlite3", dbfileWithDSN)
if err != nil {
return err
}
// set auto_vacuum mode to full
if _, err := db.Exec("PRAGMA auto_vacuum = full;"); err != nil {
db.Close()
return err
}
// create tables
if err := createTables(db, createStmts); err != nil {
db.Close()
return err
}
// close database
if err := db.Close(); err != nil {
return err
}
// make sure the database file is encrypted
encrypted, err := sqlite3.IsEncrypted(dbfile)
if err != nil {
return err
}
if !encrypted {
return fmt.Errorf("encdb: created dbfile '%s' is not encrypted", dbfile)
}
return nil
}
// Open tries to open an encrypted database with the given passphrase.
// Thereby, dbname is the prefix of the following two database files (which
// must already exist):
//
// dbname.db
// dbname.key
//
// In case of error (for example, the database files do not exist or the
// passphrase is wrong) an error is returned.
func Open(dbname string, passphrase []byte) (*sql.DB, error) {
dbfile := dbname + DBSuffix
keyfile := dbname + KeySuffix
// make sure files exists
if _, err := os.Stat(dbfile); err != nil {
return nil, err
}
if _, err := os.Stat(keyfile); err != nil {
return nil, err
}
// make sure the database file is encrypted
encrypted, err := sqlite3.IsEncrypted(dbfile)
if err != nil {
return nil, err
}
if !encrypted {
return nil, fmt.Errorf("encdb: dbfile '%s' is not encrypted", dbfile)
}
// get key from keyfile
key, err := ReadKeyfile(keyfile, passphrase)
if err != nil {
return nil, err
}
// open DB
dbfile += fmt.Sprintf("?_pragma_key=x'%s'&_pragma_cipher_page_size=4096",
hex.EncodeToString(key))
// enable foreign key support
dbfile += "&_foreign_keys=1"
db, err := sql.Open("sqlite3", dbfile)
if err != nil {
return nil, err
}
// test key
_, err = db.Exec("SELECT count(*) FROM sqlite_master;")
if err != nil {
return nil, err
}
return db, nil
}
// Rekey tries to rekey an encrypted database with the given newPassphrase and
// newIter many KDF iterations. The correct oldPassphrase must be supplied.
// Thereby, dbname is the prefix of the following two database files (which must
// already exist):
//
// dbname.db
// dbname.key
//
// Rekey replaces the dbname.key file and leaves the dbname.db file unmodified,
// allowing for very fast rekey operations. In case of error (for example, the
// database files do not exist or the oldPassphrase is wrong) an error is
// returned.
func Rekey(dbname string, oldPassphrase, newPassphrase []byte, newIter int) error {
encdb, err := Open(dbname, oldPassphrase)
if err != nil {
return err
}
defer encdb.Close()
keyfile := dbname + KeySuffix
return replaceKeyfile(keyfile, oldPassphrase, newPassphrase, newIter)
}
var autoVacuumModes = []string{
"NONE",
"FULL",
"INCREMENTAL",
}
// Status returns the autoVacuum and freelistCount of db.
func Status(db *sql.DB) (autoVacuum string, freelistCount int64, err error) {
var av int64
err = db.QueryRow("PRAGMA auto_vacuum;").Scan(&av)
if err != nil {
return "", 0, err
}
autoVacuum = autoVacuumModes[av]
err = db.QueryRow("PRAGMA freelist_count;").Scan(&freelistCount)
if err != nil {
return "", 0, err
}
return
}
// Vacuum executes VACUUM command in db. If autoVacuumMode is not nil and
// different from the current one, the auto_vacuum mode is changed before
// VACUUM is executed.
func Vacuum(db *sql.DB, autoVacuumMode string) error {
if autoVacuumMode != "" {
if !containsString(autoVacuumModes, autoVacuumMode) {
return fmt.Errorf("encdb: unknown auto_vacuum mode: %s", autoVacuumMode)
}
var av int64
err := db.QueryRow("PRAGMA auto_vacuum;").Scan(&av)
if err != nil {
return err
}
currentMode := autoVacuumModes[av]
if currentMode != autoVacuumMode {
_, err = db.Exec(fmt.Sprintf("PRAGMA auto_vacuum = %s;", autoVacuumMode))
if err != nil {
return err
}
}
}
_, err := db.Exec("VACUUM;")
if err != nil {
return err
}
return nil
}
// Incremental executes incremental_vacuum to free up to pages many pages. If
// pages is 0, all pages are freed. If the current auto_vacuum mode is not
// INCREMENTAL, an error is returned.
func Incremental(db *sql.DB, pages int64) error {
var av int64
err := db.QueryRow("PRAGMA auto_vacuum;").Scan(&av)
if err != nil {
return err
}
if autoVacuumModes[av] != "INCREMENTAL" {
return errors.New("encdb: current auto_vacuum mode is not INCREMENTAL")
}
_, err = db.Exec(fmt.Sprintf("PRAGMA incremental_vacuum(%d);", pages))
if err != nil {
return err
}
return nil
}