验证备份 ZIP 文件

Validate Backup ZIP File

使用此代码,用户可以将我的数据库、共享首选项和其他内部应用程序数据压缩为备份文件。 该文件如下所示:

用户还可以选择通过从文件管理器中选择 zip 文件来恢复备份文件。 这是“问题”出现的地方:

虽然恢复有效,但我如何通过恢复一些不是由我的应用程序创建的“随机”zip 文件来阻止用户。

我的一些解决方案是:

首先,将检查文件(而不是数据库)的header 是否有Magic header 字符串。即它是有效的 SQLiteDatabase 吗?

直接打开文件,读取前16个字节,必须是SQLite format 3[=12=]053 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.
  • 的一部分