如何在没有第三方库的情况下加密 sqlite 文本 (Android)

A. 没有

我在我的 android 应用程序中使用了一个外部数据库,它在编译后直接嵌入到 apk 包中。因为我想在应用程序购买中实现以访问它的一些数据,所以我不想在没有任何加密的情况下离开它。我使用了 Sqlcipher 库,但它使应用程序太大且速度太慢。没有其他方法可以做到这一点吗?例如,一种加密字符串的算法,因此我将加密文本放入数据库并在应用程序代码中解密?

迈克

以下是加密部分数据的示例应用程序,然后可以打开。它基于我评论中的代码。

阶段 1 - 主数据库。

首先,您需要作为加密数据库基础的数据库(即不包含在应用程序中MASTER数据库,它的用途是创建加密数据库(或数据库,可能是一个库,每个数据库都有一个如果您想要更高的安全性,则唯一的密码/密钥))部分考虑这一点(在整个示例中使用):-

在此处输入图片说明

正如您所看到的,这将通过一个名为 FreeData 的表和另一个名为 PaidData 的表来工作。表定义是相同的,除了对于 PaidData 没有 ID 列(此方法的目的是在/如果请求的和 SecretKey(密码)有效时将 PaidData 中的行解密为 FreeData。)。

所以FreeData表看起来像:-

在此处输入图片说明

PaidData表如下所示: -

在此处输入图片说明

  • 因此,表之间的唯一区别是其中包含的实际数据以及缺少id列。
  • 当加密数据被从提取出的ID的将被生成PaidData表,解密并且插入FreeData表。因此,只需进行一次解密即可访问数据。

第 2 阶段 - 生成加密数据库以与应用程序一起分发

这是由一个应用程序为此目的使用 EncryptDecrypt 类完成的,该类与SQLite中的Encrypt data 中的类非常相似

根据EncryptDecrypt.java

class EncryptDecrypt {
    private Cipher cipher;
    private static SecretKeySpec secretKeySpec;
    private static IvParameterSpec ivParameterSpec;
    private boolean do_encrypt = true;

    /**
     * Construct EncryptDecrypt instance that does not check user login-in
     * mode, thus the assumption is that this user is NOT the special user
     * NOUSER that doesn't require a password to login; this constructor
     * is designed to ONLY be used when a user has been added by NOUSER,
     * and to then encrypt the data using the enccryptForced method solely
     * to encrypt any existing card data for the new user that has a password.
     *
     * @param context   The context, required for database usage (user)
     * @param skey      The secret key to be used to encrypt/decrypt
     */
    EncryptDecrypt(Context context, String skey) {
        //DBUsersMethods users = new DBUsersMethods(context);
        String saltasString = "there is no dark side of the moon it is all dark.";
        String paddedskey = (skey + saltasString).substring(0,16);
        secretKeySpec = new SecretKeySpec(paddedskey.getBytes(),"AES/CBC/PKCS5Padding");
        ivParameterSpec = new IvParameterSpec((saltasString.substring(0,16)).getBytes());
        try {
            cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        } catch (Exception e){
            //e.printStackTrace();
        }
    }

    /**
     * Normal encryption routine that will not encrypt data if the user is
     * the special case NOUSER (i.e LOGIN mode is NOUSER), otherwise data
     * is encrypted.
     *
     * @Param toEncrypt     The string to be encrypted
     * @return              The encryted (or not if NOUSER) data as a string
     */
    String encrypt(String toEncrypt) {
        if (!do_encrypt) {
            return toEncrypt;
        }
        byte[] encrypted;
        try {
            cipher.init(Cipher.ENCRYPT_MODE,secretKeySpec,ivParameterSpec);
            encrypted = cipher.doFinal(toEncrypt.getBytes());
        } catch (Exception e) {
            //e.printStackTrace();
            return null;
        }
        return Base64.encodeToString(encrypted, Base64.DEFAULT);
    }

    /**
     * Encryption, irrespective of the USER type, noting that this should
     * only be used in conjunction with an EncryptDecrypt instance created
     * using the 2nd/extended constructor
     *
     * @param toEncrypt     The string to be encrypted
     * @return              The encrypted data as a string
     */
    String encryptForced(String toEncrypt) {
        byte[] encrypted;
        try {
            cipher.init(Cipher.ENCRYPT_MODE,secretKeySpec,ivParameterSpec);
            encrypted = cipher.doFinal(toEncrypt.getBytes());
        } catch (Exception e) {
            //e.printStackTrace();
            return null;
        }
        return Base64.encodeToString(encrypted,Base64.DEFAULT);
    }

    /**
     * Decrypt an encrypted string
     * @param toDecrypt     The encrypted string to be decrypted
     * @return              The decrypted string
     */
    String decrypt(String toDecrypt)  {
        if (!do_encrypt) {
            return toDecrypt;
        }
        byte[] decrypted;
        try {
            cipher.init(Cipher.DECRYPT_MODE,secretKeySpec,ivParameterSpec);
            decrypted = cipher.doFinal(Base64.decode(toDecrypt,Base64.DEFAULT));
        } catch (Exception e) {
            //e.printStackTrace();
            return null;
        }
        return new String(decrypted);
    }
}
  • 由于这是为用户登录和多个用户设计的,其中盐是数据库的一部分,盐已使用 :- 硬编码String saltasString = "there is no dark side of the moon it is all dark.";,只要长度至少为 16 个字符(仅前 16 个字节),就可以更改短语使用)。

一个类用于满足潜在的灵活性/扩展性,其中可以指定或不加密多个表,以及可以加密、复制或跳过的多个列(例如,可能会跳过 ID(在示例中它甚至没有定义为)一列))。

这个类是TableColumnConvertList.java并且是:-

public class TableColumnConvertList {
    private ArrayList<TableEntry> tables;

    public TableColumnConvertList() {
        this.tables = new ArrayList<>();
    }

    public String[] getTables() {
        String[] tableList = new String[tables.size()];
        int ix = 0;
        for (TableEntry te: this.tables) {
                tableList[ix++] = te.getSourceTableName();
        }
        return tableList;
    }

    public String[] getTableColumnNamesToEncrypt(String tableName) {
        String[] rv = null;
        for(TableEntry te: this.tables) {
            if (te.getSourceTableName().equals(tableName)) {
                rv = new String[te.getColumnNamesToEncrypt().size()];
                int ix=0;
                for (String s: te.getColumnNamesToEncrypt()) {
                    rv[ix++] = s;
                }
            }
        }
        return rv;
    }

    public String[] getTableColumnNamesToCopyAsis(String tableName) {
        String[] rv = null;
        for (TableEntry te: this.tables) {
            if (te.getSourceTableName().equals(tableName)) {
                rv = new String[te.getColumnNamesToCopyAsis().size()];
                int ix=0;
                for (String s: te.getColumnNamesToCopyAsis()) {
                    rv[ix++] = s;
                }
            }
        }
        return rv;
    }

    public String[] getTableColumnNamesToSkip(String tableName) {
        String[] rv = null;
        for (TableEntry te: this.tables) {
            if (te.sourceTableName.equals(tableName)) {
                rv = new String[te.getColumnNamesToSkip().size()];
                int ix =0;
                for (String s: te.getColumnNamesToSkip()) {
                    rv[ix++] = s;
                }
            }
        }
        return rv;
    }


    public void addTable(
            String sourceTableName,
            String destinationTableName,
            String[] columnNamesToEncrypt,
            String[] columnNamesToCopyAsis,
            String[] columnNamesToSkip
    ) {
        tables.add(
                new TableEntry(
                        sourceTableName,
                        destinationTableName,
                        columnNamesToEncrypt,
                        columnNamesToCopyAsis,
                        columnNamesToSkip
                )
        );
    }

    private class TableEntry {
       private String sourceTableName;
       private String destinationTableName;
       private ArrayList<String> columnNamesToEncrypt;
       private ArrayList<String> columnNamesToCopyAsis;
       private ArrayList<String> columnNamesToSkip;

       private TableEntry() {}

       private TableEntry(String sourceTableName,
                          String destinationTableName,
                          String[] columnNamesToEncrypt,
                          String[] columnNamesToCopyAsis,
                          String[] columnNamesToSkip
       ) {
           this.sourceTableName = sourceTableName;
           this.destinationTableName = destinationTableName;
           this.columnNamesToEncrypt = new ArrayList<>();
           if (columnNamesToEncrypt != null && columnNamesToEncrypt.length > 0) {
               for (String s: columnNamesToEncrypt) {
                   addColumn(s);
               }
           }
       }

       private void addColumn(String s) {
           this.columnNamesToEncrypt.add(s);
       }

        private String getSourceTableName() {
            return sourceTableName;
        }

        public String getDestinationTableName() {
            return destinationTableName;
        }

        public void setSourceTableName(String sourceTableName) {
            this.sourceTableName = sourceTableName;
        }

        public void setDestinationTableName(String destinationTableName) {
            this.destinationTableName = destinationTableName;
        }

        private ArrayList<String> getColumnNamesToEncrypt() {
            return columnNamesToEncrypt;
        }

        public void setColumnNamesToEncrypt(ArrayList<String> columnNamesToEncrypt) {
            this.columnNamesToEncrypt = columnNamesToEncrypt;
        }

        private ArrayList<String> getColumnNamesToCopyAsis() {
            return columnNamesToCopyAsis;
        }

        public void setColumnNamesToCopyAsis(ArrayList<String> columnNamesToCopyAsis) {
            this.columnNamesToCopyAsis = columnNamesToCopyAsis;
        }

        public ArrayList<String> getColumnNamesToSkip() {
            return columnNamesToSkip;
        }

        public void setColumnNamesToSkip(ArrayList<String> columnNamesToSkip) {
            this.columnNamesToSkip = columnNamesToSkip;
        }
    }
}

目前,这个基本应用程序的其余部分都在一个使用两个输入(EditTexts)的活动中:-

  • 用于加密的密钥
  • 加密数据库的数据库名称(文件名)。
    • 应用程序中的代码防止使用与基础数据库相同的名称,需要将其复制到资产文件夹中。和一个按钮,如果输入良好则启动加密(对于时尚又名验证有限)。

这个布局 xml activiy_main.xml是:-

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="10dp"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Database EncryptTool" />

    <EditText
        android:id="@+id/secretkey"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:hint="Secret Key to use to Encrypt the Database."
        >
    </EditText>
    <EditText
        android:id="@+id/databasename"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="MyDatabase"
        android:hint="Database Name"
        >
    </EditText>

    <Button
        android:id="@+id/encrypt"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="ENCRYPT"
        android:visibility="gone"
        >
    </Button>
</LinearLayout>

完成工作的MainActivity.java是:-

public class MainActivity extends AppCompatActivity {

    public static final String ASSETDB_NAME = "basedb.db";
    public static final int ASSETDB_NOT_FOUND = -10;
    public static final int ASSETFILE_OPEN_ERROR = -11;
    public static final int ASSETDB_OPEN_ERROR = -12;
    public static final int ASSETDB_COPY_ERROR = -13;
    public static final int ASSETDB_FLUSH_ERROR = -14;
    public static final int ASSETDB_CLOSE_ERROR = -15;
    public static final int ASSETFILE_CLOSE_ERROR = -16;
    public static final int ASSETDB_CREATED_SUCCESSFULLY = 0;
    public static final int BUFFERSIZE = 1024 * 4;

    EditText mSecretKey, mDBName;
    Button mEncryptButton;
    TableColumnConvertList mTCCL = new TableColumnConvertList();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDBName = this.findViewById(R.id.databasename);
        mSecretKey = this.findViewById(R.id.secretkey);
        mEncryptButton = this.findViewById(R.id.encrypt);

        //<<<<<<<<< set what data to encrypt i.e. table(s) and the column(s) in the table >>>>>>>>>
        mTCCL.addTable(
                "PaidData",
                "FreeData",
                new String[]{"theData"},
                new String[]{},
                new String[]{"id"}
                );

        if (getDBFromAsset() >= 0) {
            mEncryptButton.setVisibility(View.VISIBLE);
            mEncryptButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mDBName.getText().toString().length() < 1) {
                        Toast.makeText(
                                v.getContext(),
                                "The Database Name cannot be blank.",
                                Toast.LENGTH_LONG
                        ).show();
                        mDBName.requestFocus();
                        return;
                    }
                    if (mDBName.getText().toString().equals(ASSETDB_NAME)) {
                        Toast.makeText(
                                v.getContext(),
                                "Database Name cannot be "
                                        + ASSETDB_NAME
                                        + ". Please change the name.",
                                Toast.LENGTH_LONG
                        ).show();
                        mDBName.requestFocus();
                        return;
                    }
                    if (mSecretKey.getText().toString().length() < 1) {
                        Toast.makeText(
                                v.getContext(),
                                "The Secret Key cannot be blank.",
                                Toast.LENGTH_LONG
                        ).show();
                        mSecretKey.requestFocus();
                        return;
                    }
                    if (createEncryptedDatabase(mTCCL,
                            mDBName.getText().toString(),
                            mSecretKey.getText().toString()
                    ) == 0) {
                        Toast.makeText(v.getContext(),"Successfully Encrypted Database " + mDBName + " using Secret Key " + mSecretKey,Toast.LENGTH_LONG).show();
                    }
                }
            });

        }
    }

    private boolean checkIfDataBaseExists(String databaseName) {
        File dbFile = new File(this.getDatabasePath(databaseName).getPath());
        if (dbFile.exists()) {
            return true;
        } else {
            if (!dbFile.getParentFile().exists()) {
                dbFile.getParentFile().mkdirs();
            }
        }
        return false;
    }

    private boolean checkIfAssetDBExists() {
        try {
            InputStream is = this.getAssets().open(ASSETDB_NAME);
            is.close();
            return true;
        } catch (IOException e) {
            return false;
        }
    }

    private int getDBFromAsset() {
        int rv = ASSETDB_NOT_FOUND;
        File dbFile = new File(this.getDatabasePath(ASSETDB_NAME).getPath());
        InputStream is;
        FileOutputStream os;
        int read_length;
        byte[] buffer = new byte[BUFFERSIZE];
        if (!checkIfAssetDBExists()) {
            return ASSETDB_NOT_FOUND;
        }
        if (checkIfDataBaseExists(ASSETDB_NAME)) {
            dbFile.delete();
        }
        try {
            rv = ASSETFILE_OPEN_ERROR;
            is = this.getAssets().open(ASSETDB_NAME);
            rv = ASSETDB_OPEN_ERROR;
            os = new FileOutputStream(dbFile);
            rv = ASSETDB_COPY_ERROR;
            while ((read_length = is.read(buffer)) > 0) {
                os.write(buffer,0,read_length);
            }
            rv = ASSETDB_FLUSH_ERROR;
            os.flush();
            rv = ASSETDB_CLOSE_ERROR;
            os.close();
            rv = ASSETFILE_CLOSE_ERROR;
            is.close();
            rv = ASSETDB_CREATED_SUCCESSFULLY;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return rv;
    }

    private int createEncryptedDatabase(TableColumnConvertList tableColumnConvertList, String databaseName, String key) {
        File copiedAssetDB = new File(this.getDatabasePath(ASSETDB_NAME).getPath());
        File encryptedDB = new File(this.getDatabasePath(databaseName).getPath());
        if (encryptedDB.exists()) {
            encryptedDB.delete();
        }
        try {
            byte[] buffer = new byte[BUFFERSIZE];
            int read_length;
            InputStream is = new FileInputStream(copiedAssetDB);
            OutputStream os = new FileOutputStream(encryptedDB);
            while ((read_length = is.read(buffer)) > 0) {
                os.write(buffer,0,read_length);
            }
            os.flush();
            os.close();
            is.close();
        } catch (IOException e) {
            e.printStackTrace();
            return -1;
        }

        SQLiteDatabase db = SQLiteDatabase.openDatabase(encryptedDB.getPath(),null,SQLiteDatabase.OPEN_READWRITE);
        EncryptDecrypt ed = new EncryptDecrypt(this,key);
        int errorcount = 0;

        db.beginTransaction();
        for (String t: tableColumnConvertList.getTables()) {
            ContentValues cv = new ContentValues();
            String[] columnsToEncrypt = tableColumnConvertList.getTableColumnNamesToEncrypt(t);
            String[] columnOriginalValues = new String[columnsToEncrypt.length];

            Cursor c = db.query(true,t,columnsToEncrypt,null,null,null,null,null, null);
            int totalRows = c.getCount();
            int updatedRows = 0;
            while (c.moveToNext()) {
                cv.clear();
                int ovix=0;
                StringBuilder whereClause = new StringBuilder();
                for (String s: c.getColumnNames()) {
                    for (String ec: columnsToEncrypt ) {
                        if (s.equals(ec)) {
                            cv.put(s,ed.encrypt(c.getString(c.getColumnIndex(s))));
                            columnOriginalValues[ovix++] = c.getString(c.getColumnIndex(s));
                            if (whereClause.length() > 0) {
                                whereClause.append(" AND ");
                            }
                            whereClause.append(s).append("=?");
                        }
                    }
                }
                updatedRows += db.update(t,cv,whereClause.toString(),columnOriginalValues);
            }
            c.close();
            Log.d("ENCRYPTRESULT","Read " + totalRows + " DISTINCT ROWS. Updated " + updatedRows);
            errorcount += totalRows - updatedRows;
        }
        if (errorcount == 0) {
            db.setTransactionSuccessful();
        } else  {
            Toast.makeText(
                    this,
                    "Errors encountered Encrypting Database. Rolled back (not changed)",
                    Toast.LENGTH_LONG
            ).show();
        }
        db.endTransaction();
        return errorcount;
    }
}

重要的是这一行/代码:-

TableColumnConvertList mTCCL = new TableColumnConvertList();

..........

    //<<<<<<<<< set what data to encrypt i.e. table(s) and the column(s) in the table >>>>>>>>>
    mTCCL.addTable(
            "PaidData",
            "FreeData",
            new String[]{"theData"},
            new String[]{},
            new String[]{"id"}
            );

这会将一个表添加到要加密的表列表中。它的参数是:-

  • 要包含在加密中的表的名称。
  • 要存储加密数据的表的名称。
    • 请注意,此功能不存在,但可以添加。因此,该值被忽略。
  • 要加密的列的列表。
  • 要复制的列的列表。
    • 此功能不存在,但可以添加。因此,该列表被忽略。
  • 要跳过的列的列表(例如 id 列)。
    • 尽管已编码,但不存在该功能。因此,该列表被忽略。

应用程序做什么。

最终结果是资产文件夹中的数据库(名为basedb.db)中的数据库,该数据库对PaidData表theData列中的数据进行了加密,但FreeData表没有变化。然后可以复制该数据库(例如使用设备浏览器),然后将其作为资产包含在要分发的应用程序中。该应用程序可以包括使用密钥和 EncryptDecrypt 类的解密部分的加密反转。

例如

FreeData 表:-

在此处输入图片说明

付费数据表:-

在此处输入图片说明

当应用程序启动时,如果从它存在的资产文件夹中复制数据库(硬编码为 basedb.db)并使加密按钮可见。

  • 如果加密按钮不可见,则资产文件未找到。所以是时候纠正这个问题了(提供正确的数据库文件)。
    • 请注意,这只是一个演示,为简洁起见,跳过了许多可以/应该完成或添加的检查/选项。

如果出现加密按钮,那么加密只是点击按钮的问题。

点击按钮后调用createEncryptedDatabase方法。

这将创建一个副本,这将是加密数据库,通过将文件复制到它的新名称(根据给定的数据库名称,该名称必须与资产的文件名不同)从资产文件夹复制的数据库。

它使用复制的数据库查询在mTCCLTableColumnConvertList的实例)中定义的表

该查询将仅提取已指定为要加密的列的数据。查询仅获取不同的行(即,如果存在多行且列中具有相同数据,则仅提取行之一)。

对于每个提取的行:-

  • 常用的 ContentValues 实例被清除。
  • whereClause StringBuilder 被清除。
  • 检查 Cursor 中的每一列以查看它是否是在正在处理的表中定义的列(它应该是因为只有列 t 被提取)。
    • 如果不是,则跳过它。
  • 原始值保存在字符串数组columnOriginalValues的适当元素中(这将用作 UPDATE 的 WHERE 子句的绑定参数)
  • ContentValues 实例的一个元素添加了当前列名和加密数据。
    • 这是按照 cv.put(s,ed.encrypt(c.getString(c.getColumnIndex(s))));
  • 如果 whereClause 的长度大于 0 则 AND 被添加到 whereClause,那么列名后缀为 =? 添加到正在构建的 whereClause 中。在处理完所有列之后,调用SQLiteDatabase更新方法来更新列,将值设置为加密值,其中所有列都与原始数据匹配。
  • 处理完所有行后,关闭 Cursor 并处理下一个表。
  • 如果在处理完所有表后错误计数为 0,则事务被设置为成功,否则将显示消息为遇到加密数据库的Toasted Errors。回滚(未更改)
  • 然后结束事务(如果未设置为成功,则数据不会更新而是回滚)。

数据库将在 data/data/package_name/databases 文件夹中,例如:-

在此处输入图片说明

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章

Android如何在没有第三方的情况下发送帖子请求

如何在没有任何第三方库的情况下在 Android 上制作 RTSP 客户端?

如何为没有第三方pkg的文本设置颜色

如何在Android中集成Adtech第三方广告库

如何在Android cmake外部构建系统中链接第三方库?

如何在Flutter中将第三方Android库导入Java文件

如何使用ADB Wireless连接Android设备?没有任何第三方工具

如何在不使用任何第三方应用程序的情况下在Android中读取或解码QR码?

如何在没有第三方库的情况下为雪人制作动画?

在Eclipse for Android中使用第三方库

在没有第三方路由库的情况下路由PUT请求

在没有任何第三方库的情况下从MySQL触发Shell脚本

Android Proguard-保留所有第三方库是否是最佳实践?

Android:我可以覆盖具有自己布局的第三方类(库)吗?

Flutter:如何在没有android底部导航栏聚焦文本字段的情况下显示屏幕键盘?

如何在没有虚线大写字母“I”的情况下显示android按钮文本?

如何从Android上包含的第三方库中删除未使用的资源?

如何从第三方共享库中查找导致Android本机崩溃的指令?

第三方android权限如何工作?

没有第三方解析器的情况下,如何在Kotlin中解析JSON?

没有任何第三方模块的情况下,如何在Node Js中进行https发布?

如何在没有第三方工具的情况下向后(向左)扩展硬盘分区?

如何在没有第三方软件的情况下将 Apache Druid 连接到 Power BI?

如何在没有d.ts文件的情况下导入第三方软件包?

如何在没有第三方的情况下使用Angular / Typescript对HTML表进行排序?

如何在没有第三方软件的情况下更改Windows 10上的MAC地址?

如何在没有第三方软件的情况下远程访问我的电脑?

如何在android中的sqlite db中插入带有单引号的文本?

Android SQLite查询连续文本?