验证备份 ZIP 文件
Validate Backup ZIP File
使用此代码,用户可以将我的数据库、共享首选项和其他内部应用程序数据压缩为备份文件。
该文件如下所示:
用户还可以选择通过从文件管理器中选择 zip 文件来恢复备份文件。
这是“问题”出现的地方:
虽然恢复有效,但我如何通过恢复一些不是由我的应用程序创建的“随机”zip 文件来阻止用户。
我的一些解决方案是:
- 检查是否有数据库文件夹以及数据库sqlite方案是否与app sqlite数据库方案匹配(它是本地数据库)。
- 添加一些无法查看或编辑的“隐藏”META 数据。 (不确定这是否可能)。
- 正在检查 ZIP 文件是否加密、密码是否匹配以及文件夹方案是否与备份文件夹方案大体匹配。
- 通常相信用户导入了正确的文件夹,尽管我不喜欢这种解决方案。
首先,将检查文件(而不是数据库)的header 是否有Magic header 字符串。即它是有效的 SQLiteDatabase 吗?
直接打开文件,读取前16个字节,必须是SQLite format 3[=12=]0
或53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00
十六进制。
Second,然后您可以检查应该与数据库版本匹配的 user_version(4 个字节的偏移量 60)(从而防止恢复过时的版本)。如果使用SQLiteOpenHelper访问数据库,那么这个值是根据编译和生产发行版时使用的版本号维护的。
Adding some "hidden" META data which can't be seen or edited. (not sure if that is possible).
Third,您可以再次使用 header,但这次应用程序 ID 位于偏移量 68(4 字节),将不会被使用。这可以以与版本号类似的方式使用,但您必须实施它的维护 (setting/updating)。
前两个几乎不需要,并且可以防止大多数意外情况。
第三个应用程序 ID 提供了更多保护,防止使用具有有效版本号的有效 SQLite 数据库。
None 可以防止故意滥用(为什么这样的意图值得怀疑)。但是,它可能会导致异常。
如果前 3 个不足,那么您可以打开数据库并询问 sqlite_master 以查看模式是否符合预期。
或许考虑一下 Room 使用的元数据。
Room 根据根据注释 类 的@Entity 预期的模式散列和存储在 room_master_table 中的数据库中的散列来进行模式检查。这等同于您的元数据方法。
例如编译 Room 项目时,在生成的 java 中会有代码,在 createAllTables
方法中,如 :-
_db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
_db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c9583474ce546ff5ead43c63fe049bc8')");
现在,如果数据库不存在,则存储散列。如果数据库确实存在,那么它会检查存储在 room_master_table 中的散列是否匹配。如果不是,则如果编码了适当的迁移并且版本号已更改,则调用适当的迁移,然后如果模式与 has 存储匹配,否则会引发异常。如果散列不匹配并且没有适当的迁移,那么将引发异常(需要迁移)或者如果对 fallbackToDestrictiveMigration 进行编码,则数据库将从头开始创建。
所以房间数据库(按照上述)将包括:-
第一个 的替代方法是 utilise/override DatabaseErrorHandler's onCorruption 方法。
这里有一个方法(虽然很冗长)使用这种技术并另外检查是否有任何表格(但不彻底):-
/**************************************************************************
*
* @return false if the backup file is invalid.
*
* determine by creating a differently name database (prefixed with IC),
* openeing it with it's own helper (does nothing) and then trying to
* check if there are tables in the database.
* No tables reflects that file is invalid type.
*
* Note! if an attempt to open an invalid database file then SQLite deletes the file.
*/
private boolean dataBaseIntegrityCheck() {
String methodname = new Object() {
}.getClass().getEnclosingMethod().getName();
LogMsg.LogMsg(LogMsg.LOGTYPE_INFORMATIONAL, LOGTAG, "Invoked", this, methodname);
@SuppressWarnings("UnusedAssignment") final String THIS_METHOD = "dataBaseIntegrityCheck";
//String sqlstr_mstr = "SELECT name FROM sqlite_master WHERE type = 'table' AND name!='android_metadata' ORDER by name;";
Cursor iccsr;
boolean rv = true;
//Note no use having the handler as it actually introduces problems as SQLite assumes that
// the handler will restore the database.
// No need to comment out as handler can be disabled by not not passing it as a parameter
// of the DBHelper
@SuppressWarnings("UnusedAssignment") DatabaseErrorHandler myerrorhandler = new DatabaseErrorHandler() {
@Override
public void onCorruption(SQLiteDatabase sqLiteDatabase) {
}
};
try {
FileInputStream bkp = new FileInputStream(backupfilename);
OutputStream ic = new FileOutputStream(icdbfilename);
while ((copylength = bkp.read(buffer)) > 0) {
ic.write(buffer, 0, copylength);
}
ic.close();
bkp.close();
icfile = new File(icdbfilename);
//Note SQLite will actually check for corruption and if so delete the file
//
IntegrityCheckDBHelper icdbh = new IntegrityCheckDBHelper(this, null, null, 1, null);
SQLiteDatabase icdb = icdbh.getReadableDatabase();
iccsr = icdb.query("sqlite_master",
new String[]{"name"},
"type=? AND name!=?",
new String[]{"table", "android_metadata"},
null, null,
"name"
);
//Check to see if there are any tables, if wrong file type shouldn't be any
//iccsr = icdb.rawQuery(sqlstr_mstr,null);
if (iccsr.getCount() < 1) {
errlist.add("Integrity Check extract from sqlite_master returned nothing - Propsoed file is corrupt or not a database file.");
rv = false;
}
iccsr.close();
icdb.close();
} catch (IOException e) {
e.printStackTrace();
errlist.add("Integrity Check Failed Error Message was " + e.getMessage());
}
if (!rv) {
AlertDialog.Builder notokdialog = new AlertDialog.Builder(this);
notokdialog.setTitle("Invalid Restore File.");
notokdialog.setCancelable(true);
String msg = "File " + backupfilename + " is an invalid file." +
"\n\nThe Restore cannot continue and will be canclled. " +
"\n\nPlease Use a Valid Database Backup File!";
notokdialog.setMessage(msg);
notokdialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
}).show();
}
// Delete the Integrity Check File (Database copy)
//noinspection ResultOfMethodCallIgnored
icfile.delete();
return rv;
}
- 请注意,这包括日志记录(如果日志记录已打开)和消息 storage/retrieval,如果遇到的话,可以检索到如此多的消息。因此 long-windedness.
的一部分
使用此代码,用户可以将我的数据库、共享首选项和其他内部应用程序数据压缩为备份文件。 该文件如下所示:
用户还可以选择通过从文件管理器中选择 zip 文件来恢复备份文件。 这是“问题”出现的地方:
虽然恢复有效,但我如何通过恢复一些不是由我的应用程序创建的“随机”zip 文件来阻止用户。
我的一些解决方案是:
- 检查是否有数据库文件夹以及数据库sqlite方案是否与app sqlite数据库方案匹配(它是本地数据库)。
- 添加一些无法查看或编辑的“隐藏”META 数据。 (不确定这是否可能)。
- 正在检查 ZIP 文件是否加密、密码是否匹配以及文件夹方案是否与备份文件夹方案大体匹配。
- 通常相信用户导入了正确的文件夹,尽管我不喜欢这种解决方案。
首先,将检查文件(而不是数据库)的header 是否有Magic header 字符串。即它是有效的 SQLiteDatabase 吗?
直接打开文件,读取前16个字节,必须是SQLite format 3[=12=]0
或53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00
十六进制。
Second,然后您可以检查应该与数据库版本匹配的 user_version(4 个字节的偏移量 60)(从而防止恢复过时的版本)。如果使用SQLiteOpenHelper访问数据库,那么这个值是根据编译和生产发行版时使用的版本号维护的。
Adding some "hidden" META data which can't be seen or edited. (not sure if that is possible).
Third,您可以再次使用 header,但这次应用程序 ID 位于偏移量 68(4 字节),将不会被使用。这可以以与版本号类似的方式使用,但您必须实施它的维护 (setting/updating)。
前两个几乎不需要,并且可以防止大多数意外情况。
第三个应用程序 ID 提供了更多保护,防止使用具有有效版本号的有效 SQLite 数据库。
None 可以防止故意滥用(为什么这样的意图值得怀疑)。但是,它可能会导致异常。
如果前 3 个不足,那么您可以打开数据库并询问 sqlite_master 以查看模式是否符合预期。
或许考虑一下 Room 使用的元数据。
Room 根据根据注释 类 的@Entity 预期的模式散列和存储在 room_master_table 中的数据库中的散列来进行模式检查。这等同于您的元数据方法。
例如编译 Room 项目时,在生成的 java 中会有代码,在 createAllTables
方法中,如 :-
_db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
_db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c9583474ce546ff5ead43c63fe049bc8')");
现在,如果数据库不存在,则存储散列。如果数据库确实存在,那么它会检查存储在 room_master_table 中的散列是否匹配。如果不是,则如果编码了适当的迁移并且版本号已更改,则调用适当的迁移,然后如果模式与 has 存储匹配,否则会引发异常。如果散列不匹配并且没有适当的迁移,那么将引发异常(需要迁移)或者如果对 fallbackToDestrictiveMigration 进行编码,则数据库将从头开始创建。
所以房间数据库(按照上述)将包括:-
第一个 的替代方法是 utilise/override DatabaseErrorHandler's onCorruption 方法。
这里有一个方法(虽然很冗长)使用这种技术并另外检查是否有任何表格(但不彻底):-
/**************************************************************************
*
* @return false if the backup file is invalid.
*
* determine by creating a differently name database (prefixed with IC),
* openeing it with it's own helper (does nothing) and then trying to
* check if there are tables in the database.
* No tables reflects that file is invalid type.
*
* Note! if an attempt to open an invalid database file then SQLite deletes the file.
*/
private boolean dataBaseIntegrityCheck() {
String methodname = new Object() {
}.getClass().getEnclosingMethod().getName();
LogMsg.LogMsg(LogMsg.LOGTYPE_INFORMATIONAL, LOGTAG, "Invoked", this, methodname);
@SuppressWarnings("UnusedAssignment") final String THIS_METHOD = "dataBaseIntegrityCheck";
//String sqlstr_mstr = "SELECT name FROM sqlite_master WHERE type = 'table' AND name!='android_metadata' ORDER by name;";
Cursor iccsr;
boolean rv = true;
//Note no use having the handler as it actually introduces problems as SQLite assumes that
// the handler will restore the database.
// No need to comment out as handler can be disabled by not not passing it as a parameter
// of the DBHelper
@SuppressWarnings("UnusedAssignment") DatabaseErrorHandler myerrorhandler = new DatabaseErrorHandler() {
@Override
public void onCorruption(SQLiteDatabase sqLiteDatabase) {
}
};
try {
FileInputStream bkp = new FileInputStream(backupfilename);
OutputStream ic = new FileOutputStream(icdbfilename);
while ((copylength = bkp.read(buffer)) > 0) {
ic.write(buffer, 0, copylength);
}
ic.close();
bkp.close();
icfile = new File(icdbfilename);
//Note SQLite will actually check for corruption and if so delete the file
//
IntegrityCheckDBHelper icdbh = new IntegrityCheckDBHelper(this, null, null, 1, null);
SQLiteDatabase icdb = icdbh.getReadableDatabase();
iccsr = icdb.query("sqlite_master",
new String[]{"name"},
"type=? AND name!=?",
new String[]{"table", "android_metadata"},
null, null,
"name"
);
//Check to see if there are any tables, if wrong file type shouldn't be any
//iccsr = icdb.rawQuery(sqlstr_mstr,null);
if (iccsr.getCount() < 1) {
errlist.add("Integrity Check extract from sqlite_master returned nothing - Propsoed file is corrupt or not a database file.");
rv = false;
}
iccsr.close();
icdb.close();
} catch (IOException e) {
e.printStackTrace();
errlist.add("Integrity Check Failed Error Message was " + e.getMessage());
}
if (!rv) {
AlertDialog.Builder notokdialog = new AlertDialog.Builder(this);
notokdialog.setTitle("Invalid Restore File.");
notokdialog.setCancelable(true);
String msg = "File " + backupfilename + " is an invalid file." +
"\n\nThe Restore cannot continue and will be canclled. " +
"\n\nPlease Use a Valid Database Backup File!";
notokdialog.setMessage(msg);
notokdialog.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
}
}).show();
}
// Delete the Integrity Check File (Database copy)
//noinspection ResultOfMethodCallIgnored
icfile.delete();
return rv;
}
- 请注意,这包括日志记录(如果日志记录已打开)和消息 storage/retrieval,如果遇到的话,可以检索到如此多的消息。因此 long-windedness. 的一部分