Skip to content

Commit 513eed9

Browse files
authored
Validate that reserve bytes are set when uploading db with encryption (#1009)
fixes #1008 This patch validates that the db being uploaded has set the correct reserved bytes. If it isn't set, then it prints an error and instructions on how to set it: ``` $ turso db create --remote-encryption-key "gq2LqyS...DPOY=" --remote-encryption-cipher aegis256 --from-file my.db Error: database reserved bytes mismatch: found 12, but cipher 'aegis256' requires 48 reserved bytes. To fix this, run: $ sqlite3 my.db sqlite> .filectrl reserve_bytes 48 sqlite> VACUUM; ```
2 parents d356210 + 9791b4b commit 513eed9

2 files changed

Lines changed: 101 additions & 20 deletions

File tree

internal/cmd/db_create.go

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,29 @@ func showSchemaDeprecationNotice() {
2121

2222
const MaxDumpFileSizeBytes = 8 << 30
2323

24+
// cipherReservedBytes maps cipher names to their required reserved bytes
25+
var cipherReservedBytes = map[string]int{
26+
"aes256gcm": 28,
27+
"aes128gcm": 28,
28+
"chacha20poly1305": 28,
29+
"aegis128l": 32,
30+
"aegis128x2": 32,
31+
"aegis128x4": 32,
32+
"aegis256": 48,
33+
"aegis256x2": 48,
34+
"aegis256x4": 48,
35+
}
36+
37+
func isValidCipher(cipher string) bool {
38+
_, ok := cipherReservedBytes[cipher]
39+
return ok
40+
}
41+
42+
func getRequiredReservedBytes(cipher string) (int, bool) {
43+
bytes, ok := cipherReservedBytes[cipher]
44+
return bytes, ok
45+
}
46+
2447
func init() {
2548
dbCmd.AddCommand(createCmd)
2649
addGroupFlag(createCmd)
@@ -86,7 +109,12 @@ func CreateDatabase(name string) error {
86109
}
87110

88111
isAWS := strings.HasPrefix(group.Primary, "aws-")
89-
seed, err := parseDBSeedFlags(client, isAWS)
112+
113+
if err = validateEncryptionFlags(); err != nil {
114+
return err
115+
}
116+
117+
seed, err := parseDBSeedFlags(client, isAWS, remoteEncryptionCipherFlag)
90118
if err != nil {
91119
return err
92120
}
@@ -96,10 +124,6 @@ func CreateDatabase(name string) error {
96124
version = "canary"
97125
}
98126

99-
if err = validateEncryptionFlags(); err != nil {
100-
return err
101-
}
102-
103127
if err := ensureGroup(client, groupName, groups, location, version); err != nil {
104128
return err
105129
}
@@ -233,13 +257,18 @@ func validateEncryptionFlags() error {
233257
return fmt.Errorf("encryption key (%s) is not valid base64: %w", remoteEncryptionKey, err)
234258
}
235259

236-
if remoteEncryptionCipherFlag != "" {
237-
return nil
238-
}
239-
240260
// if cipher is empty, then it is only valid in case of forks and for everything else we need to have it set
241-
if fromDBFlag == "" {
261+
if remoteEncryptionCipherFlag == "" && fromDBFlag == "" {
242262
return fmt.Errorf("remote encryption cipher must be provided when remote encryption key is set")
243263
}
264+
265+
if !isValidCipher(remoteEncryptionCipherFlag) {
266+
validCiphers := make([]string, 0, len(cipherReservedBytes))
267+
for cipher := range cipherReservedBytes {
268+
validCiphers = append(validCiphers, cipher)
269+
}
270+
return fmt.Errorf("unknown encryption cipher: %s. Valid ciphers are: %s", remoteEncryptionCipherFlag, strings.Join(validCiphers, ", "))
271+
}
272+
244273
return nil
245274
}

internal/cmd/group_flag.go

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"log"
99
"os"
1010
"os/exec"
11+
"strconv"
1112
"strings"
1213
"time"
1314

@@ -64,7 +65,7 @@ func parseTimestampFlag() (*time.Time, error) {
6465
return &timestamp, nil
6566
}
6667

67-
func parseDBSeedFlags(client *turso.Client, isAWS bool) (*turso.DBSeed, error) {
68+
func parseDBSeedFlags(client *turso.Client, isAWS bool, cipher string) (*turso.DBSeed, error) {
6869
if countFlags(fromDBFlag, fromDumpFlag, fromFileFlag, fromDumpURLFlag, fromCSVFlag) > 1 {
6970
return nil, errors.New("only one of --from prefixed flags can be used at a time")
7071
}
@@ -85,7 +86,7 @@ func parseDBSeedFlags(client *turso.Client, isAWS bool) (*turso.DBSeed, error) {
8586
}
8687

8788
if fromFileFlag != "" {
88-
return handleDBFile(client, fromFileFlag, isAWS)
89+
return handleDBFile(client, fromFileFlag, isAWS, cipher)
8990
}
9091

9192
if fromDumpFlag != "" {
@@ -97,7 +98,7 @@ func parseDBSeedFlags(client *turso.Client, isAWS bool) (*turso.DBSeed, error) {
9798
if err != nil {
9899
return nil, err
99100
}
100-
return handleCSVFile(client, fromCSVFlag, csvTableNameFlag, csvSeparator)
101+
return handleCSVFile(client, fromCSVFlag, csvTableNameFlag, csvSeparator, cipher)
101102
}
102103
if fromDumpURLFlag != "" {
103104
return handleDumpURL(fromDumpURLFlag)
@@ -205,7 +206,50 @@ func checkIfDump(filename string) (bool, error) {
205206
}
206207
}
207208

208-
func sqliteFileIntegrityChecks(file string) error {
209+
// getReservedBytes retrieves the current reserved bytes setting from a SQLite database
210+
func getReservedBytes(dbPath string) (int, error) {
211+
output, err := exec.Command("sqlite3", dbPath, ".filectrl reserve_bytes").CombinedOutput()
212+
if err != nil {
213+
return 0, fmt.Errorf("failed to get reserved bytes: %w", err)
214+
}
215+
outputStr := strings.TrimSpace(string(output))
216+
217+
if strings.Contains(outputStr, ":") {
218+
parts := strings.Split(outputStr, ":")
219+
if len(parts) >= 2 {
220+
outputStr = strings.TrimSpace(parts[1])
221+
}
222+
}
223+
224+
reservedBytes, err := strconv.Atoi(outputStr)
225+
if err != nil {
226+
return 0, fmt.Errorf("failed to parse reserved bytes from output '%s': %w", string(output), err)
227+
}
228+
229+
return reservedBytes, nil
230+
}
231+
232+
// validateReservedBytes checks if the database has the required reserved bytes for the given cipher
233+
func validateReservedBytes(dbPath string, cipher string) error {
234+
requiredBytes, ok := getRequiredReservedBytes(cipher)
235+
if !ok {
236+
return nil
237+
}
238+
239+
currentBytes, err := getReservedBytes(dbPath)
240+
if err != nil {
241+
return err
242+
}
243+
244+
if currentBytes != requiredBytes {
245+
return fmt.Errorf("database reserved bytes mismatch: found %d, but cipher '%s' requires %d reserved bytes.\nTo fix this, run:\n\n $ sqlite3 %s\n sqlite> .filectrl reserve_bytes %d\n sqlite> VACUUM;",
246+
currentBytes, cipher, requiredBytes, dbPath, requiredBytes)
247+
}
248+
249+
return nil
250+
}
251+
252+
func sqliteFileIntegrityChecks(file string, cipher string) error {
209253
if flags.Debug() {
210254
log.Printf("Running integrity checks on database file %s", file)
211255
}
@@ -268,11 +312,19 @@ func sqliteFileIntegrityChecks(file string) error {
268312
return fmt.Errorf("integrity check on database failed: %w", err)
269313
}
270314

315+
// validate reserved bytes if encryption cipher is specified
316+
if cipher != "" {
317+
if flags.Debug() {
318+
log.Printf("Checking reserved bytes for cipher %s...", cipher)
319+
}
320+
return validateReservedBytes(file, cipher)
321+
}
322+
271323
return nil
272324
}
273325

274-
func handleDBFileAWS(file string) (*turso.DBSeed, error) {
275-
if err := sqliteFileIntegrityChecks(file); err != nil {
326+
func handleDBFileAWS(file string, cipher string) (*turso.DBSeed, error) {
327+
if err := sqliteFileIntegrityChecks(file, cipher); err != nil {
276328
return nil, err
277329
}
278330

@@ -284,7 +336,7 @@ func handleDBFileAWS(file string) (*turso.DBSeed, error) {
284336
return seed, nil
285337
}
286338

287-
func handleDBFile(client *turso.Client, file string, isAWS bool) (*turso.DBSeed, error) {
339+
func handleDBFile(client *turso.Client, file string, isAWS bool, cipher string) (*turso.DBSeed, error) {
288340
if err := checkFileExists(file); err != nil {
289341
return nil, err
290342
}
@@ -293,7 +345,7 @@ func handleDBFile(client *turso.Client, file string, isAWS bool) (*turso.DBSeed,
293345
}
294346

295347
if isAWS {
296-
return handleDBFileAWS(file)
348+
return handleDBFileAWS(file, cipher)
297349
}
298350

299351
if err := checkSQLiteFile(file); err != nil {
@@ -363,7 +415,7 @@ func dumpSQLiteDatabase(database string, dump *os.File) error {
363415
return nil
364416
}
365417

366-
func handleCSVFile(client *turso.Client, file, csvTableName string, separator rune) (*turso.DBSeed, error) {
418+
func handleCSVFile(client *turso.Client, file, csvTableName string, separator rune, cipher string) (*turso.DBSeed, error) {
367419
if err := checkFileExists(file); err != nil {
368420
return nil, err
369421
}
@@ -393,7 +445,7 @@ func handleCSVFile(client *turso.Client, file, csvTableName string, separator ru
393445
return nil, err
394446
}
395447

396-
seed, err := handleDBFile(client, tempDB.Name(), false)
448+
seed, err := handleDBFile(client, tempDB.Name(), false, cipher)
397449
if err != nil {
398450
return nil, err
399451
}

0 commit comments

Comments
 (0)