前情提要
之前升級(jí)Electron框架至5.x版本時(shí),因編譯報(bào)sqlite3相關(guān)錯(cuò),加上Sqlcipher官網(wǎng)上聲稱(chēng)4.x版本在性能和安全性上有了很大提升。因此,將項(xiàng)目的sqlite3庫(kù)做了基于4.x Sqlcipher的重編譯。重編譯后的庫(kù),只需要在連接原數(shù)據(jù)庫(kù)文件后,運(yùn)行PRAGMA cipher_migrate
語(yǔ)句,就能將數(shù)據(jù)庫(kù)文件升級(jí)為具有4.x特性(具體哪些特性沒(méi)在意...)。具體可參考作者的升級(jí)介紹:Upgrading to SQLCipher 4。
升級(jí)后,基于Electron 5.x的項(xiàng)目確實(shí)能運(yùn)行了。But!舊的sqlite庫(kù)無(wú)法打開(kāi)升級(jí)后的數(shù)據(jù)庫(kù)文件了。一時(shí)半會(huì)也沒(méi)那么多時(shí)間去解決Electron升級(jí)后的各種兼容問(wèn)題,只能想辦法怎么把數(shù)據(jù)庫(kù)恢復(fù)了。
如何降級(jí)加密數(shù)據(jù)庫(kù)文件?
需求
將數(shù)據(jù)庫(kù)文件降級(jí),支持原sqlcipher(3.x)打開(kāi)。
問(wèn)題
Sqlcipher提供了數(shù)據(jù)庫(kù)升級(jí)語(yǔ)句PRAGMA cipher_migrate
,但沒(méi)有降級(jí)的(倒是有個(gè)高版本庫(kù)以低版本特性運(yùn)行的語(yǔ)句:PRAGMA cipher_compatibility = 3;
)。這里還有個(gè)老哥提過(guò)類(lèi)似需求:Downgrade from version 4.2.0 to 3.5.9,然而作者明確回復(fù)沒(méi)有直接降級(jí)的方法。
分析
既然沒(méi)有現(xiàn)成的Downgrade方法,那只能另辟蹊徑了。在查看了官網(wǎng)的各種文檔和各種嘗試后,總結(jié)下可行的操作步驟:
- 使用升級(jí)后的Sqlcipher庫(kù),將數(shù)據(jù)庫(kù)文件解密轉(zhuǎn)化為非加密的純文本數(shù)據(jù)庫(kù)文件(plaintext db);
- 使用原Sqlcipher庫(kù),在plaintext db基礎(chǔ)上復(fù)制一份加密庫(kù)文件,然后進(jìn)行數(shù)據(jù)遷移。
主要用到的是數(shù)據(jù)遷移相關(guān)的關(guān)鍵字ATTACH
和sqlcipher_export()
函數(shù)。
實(shí)現(xiàn)降級(jí)的過(guò)程這樣看下來(lái)并不復(fù)雜,無(wú)非就是加密庫(kù)和非加密庫(kù)之間的來(lái)回?cái)?shù)據(jù)遷移。關(guān)鍵在于區(qū)分好前后使用的Sqlcipher版本。
降級(jí)的具體操作流程
-
將4.x版本的數(shù)據(jù)庫(kù)文件解密生成純文本數(shù)據(jù)庫(kù)文件
這一步需要用到4.x版本的Sqlcipher,之前重編譯Sqlite時(shí)有在本地全局安裝Sqlcipher。因此,這一步,直接在終端操作:
$ ./sqlcipher encrypted.db sqlite> PRAGMA key = 'testkey'; sqlite> ATTACH DATABASE 'plaintext.db' AS plaintext KEY ''; -- empty key will disable encryption sqlite> SELECT sqlcipher_export('plaintext'); sqlite> DETACH DATABASE plaintext;
成功執(zhí)行后,得到?jīng)]有加密的純文本數(shù)據(jù)庫(kù)文件
plaintext
。 -
使用舊版本Sqlcipher對(duì)純文本數(shù)據(jù)庫(kù)文件重加密
本地沒(méi)有舊版本的Sqlcipher,使用的是項(xiàng)目里已經(jīng)編譯好的加密版Sqlite。新建一個(gè)JS文件,執(zhí)行以下code:
const sqlcipher = require('cross-sqlcipher').verbose(); const sqlite3 = require('sqlite3').verbose(); const dbPath = path.resolve('./plaintext.db'); const plainDb = new sqlcipher.Database(dbPath, (err) => { if (!err) { console.log('open plain db success'); plainDb.serialize(() => { plainDb.run('ATTACH DATABASE \'encrypted.db\' AS encrypted KEY \'testkey\'', (err, res) => { console.log('attach finish'); }).run('SELECT sqlcipher_export(\'encrypted\')', (err, res) => { console.log('sqlcipher_export finish'); }).run('DETACH DATABASE encrypted'); }); } });
執(zhí)行完畢后,生成的encrypted.db即可以被原項(xiàng)目的Sqlite庫(kù)正常打開(kāi)。
引申:數(shù)據(jù)庫(kù)異常 - SQLITE_CORRUPT
// log
{ Error: SQLITE_CORRUPT: database disk image is malformed errno: 11, code: 'SQLITE_CORRUPT' }
參考:ERROR: SQLite database is malformed – SOLVED
之前升級(jí)了本地的sqlcipher版本,無(wú)法打開(kāi)用戶(hù)的數(shù)據(jù)庫(kù)文件,重新下載項(xiàng)目中的版本3.4.1
,復(fù)制到目錄(macos)/usr/local/Cellar/sqlcipher
, 使用bin目錄下sqlcipher程序打開(kāi)用戶(hù)數(shù)據(jù)庫(kù)文件。
解密后,導(dǎo)出完整的數(shù)據(jù)庫(kù)語(yǔ)句:
./bin/sqlcipher _data.db
PRAGMA key = 'yourkey';
.mode insert;
.output data_export.sql;
.dump
.exit
完成以上指令后,數(shù)據(jù)庫(kù)的所有數(shù)據(jù)已經(jīng)導(dǎo)出到sql文件中,通過(guò)該文件重新生成db文件。
// 備份
mv _data.db _data.db.original
sqlite3 _data.db < data_export.sql
校驗(yàn)新生成的數(shù)據(jù)庫(kù)是否能正常打開(kāi)。參考上面Sqlcipher加密純文本數(shù)據(jù)庫(kù)文件的步驟,完成數(shù)據(jù)庫(kù)加密。