Search code examples
androidzipandroid-sqlitebackupdatabase-restore

Validate Backup ZIP File


Using this code, the user can zip my database, shared preferences and other internal app data as a backup file. The file looks like this:

zip file backup

content of backup file

The user has also the option the restore the backup file by choosing the zip file from the file manager. Here is were the "issue" arrises:

Although the restoring works, how can I prevent the user by restoring some "random" zip file which was not created by my app.

Few of my solutions were:

  • Checking if there is a database folder and if the database sqlite scheme match the app sqlite database scheme (it's a local database).
  • Adding some "hidden" META data which can't be seen or edited. (not sure if that is possible).
  • Checking if the ZIP-File is encrypted and if the passwords match and if the folder schemes generally match the backup folder scheme.
  • Generally trusting the user that he imports the correct folder, altough I wouldn't prefer that solution.

Solution

  • First, would be to check the file's (rather than database) header for the Magic header String. i.e. is it a valid SQLiteDatabase.

    Simply open the file and read the first 16 bytes, it must be SQLite format 3\000 or 53 51 4c 69 74 65 20 66 6f 72 6d 61 74 20 33 00 hex.

    Second, you could then check the user_version (offset 60 for 4 bytes) which should match the database version (thus protecting against restoring an outdated version). If using the SQLiteOpenHelper to access the database, then this value is maintained according to the version number used when compiling and producing the distribution.

    Adding some "hidden" META data which can't be seen or edited. (not sure if that is possible).

    Third, you could again use the header but this time the Application ID at offset 68 (4 bytes), which would be unused. This could be used in a similar way to the version number BUT you would have to implement it's maintenance (setting/updating).

    • The first two would require little and would protect against most accidental situations.

    • The third, the Application ID, offers a little more protection against the use of a valid SQLite database with a valid version number.

    • None would protect against intentional abuse (why such an intention would be questionable). However, it would then likely result in an exception.

    If the first 3 are insufficient, then you could open the database and interrogate sqlite_master to see if the schema is as expected.


    Perhaps consider the META DATA that Room uses.

    Room does it's schema checking based upon a hash of the schema expected according to the @Entity annotated classes and the hash stored in the database in the room_master_table. Which would equate to your META DATA methodology.

    e.g. when a Room prohject is compiled, in the generated java it will have code, in the createAllTables method like :-

    _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')");
    

    Now if the database doesn't exist then the hash is stored. If the database does exist then it checks that the hash stored in the room_master_table matches. If not then, if an appropriate migration is coded and the version number has changed, the appropriate migration(s) are called and then if the schema then matches the has is store otherwise and exception is raised. If the hash does not match and there are no appropriate migrations then an exception will be raised (migration required) or if fallbackToDestrictiveMigration is coded the database will be created from scratch.

    So a room database (as per the above) would include:-

    enter image description here

    An Alternative to the First would be to utilise/override the DatabaseErrorHandler's onCorruption method.

    Here's a method (albeit pretty long winded) that uses this technique and additionally checks to see if there are any tables (but not thoroughly thou ):-

      /**************************************************************************
         *
         * @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;
        }
    
    • note this includes logging IF logging is turned on, and message storage/retrieval, so many messages, if encountered, can be retrieved. Hence part of the long-windedness.