离线下载
PDF版 ePub版

极客学院团队出品 · 更新于 2018-11-28 11:00:43

如何使用安卓密钥库存储密码和其他敏感信息

image

文章翻译:赵洁
发表时间:2015 年 7 月 10 日
原文作者:OBARO OGBO
文章分类:云计算与安全

关于本文

本文主要介绍如何使用安卓密钥库来轻松地管理应用程序密钥。有了密钥库,你就会发现,无论是存储密钥、还是删除密钥,以及加密和解密用户所提供的文本,都不再是难题。所以不妨来学习一下吧。

文章内容

几个月前,Godfrey Nolan 写了一篇超棒的文章,讨论了安卓应用程序的开发者如何存储用户的密码和敏感/私人信息安卓密钥库提供一个安全系统等级证书存储。有了密钥库,一个应用程序可以创建一个新的私人的/公共的密钥对,并在保存到私人存储文件夹之前就可以用于加密应用程序的秘密。在本文中,我们将要展示如何使用安卓密钥库来创建和删除密钥,以及如何使用这些密钥来加密和解密用户提供的文本。

准备

在我们编码之前,了解一点关于安卓密钥库的事情以及其性能是很有帮助的。密钥库不是直接用来存储应用程序的秘密的,比如说密码,而是提供一个安全容器,应用程序用其来存储私钥,在某种程度上对于恶意(未经授权)的用户和应用程序来说,检索是相当困难的。

正如其名,一个应用程序可以在密钥库里存储多样的密钥,但是只能查看和问询它自己的密钥。理想上,有了密钥库,应用程序可以生成或获取一个存储在密钥库的私人或公共的密钥对。公钥可用于加密应用程序的秘密,在其被存储在应用程序指定的文件夹之前。而私钥是在需要的时候,解密相同的信息。

尽管安卓密钥库供应商在 API 等级 18(安卓 4.3)中引进,密钥库本身是在 API 1 的时候可被使用,由 VPN 和 WiFi 系统限制使用。

密钥库本身是使用用户自身的锁屏 pin 或者密码来加密的,因此,当装置屏幕锁上的话,密钥库是不能使用的。谨记如果你有一个后台服务,可能需要访问你的应用程序的秘密。

布局

我们实例化应用程序的主要布局是一个 ListView,由应用程序所创建的所有密钥(实际上是密钥别名或名称)所组成的项目。保存为 layout/activity_main.xml。

<ListView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/listView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    tools:context="com.sample.foo.simplekeystoreapp.MainActivity">
</ListView>

列表上的每一个项目包含一个 TextView,代表了密钥的别名,一个删除密钥的按钮,以及加密和解密文本的按钮。这是我们项目中的 layout/list_item.xml。

image

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:card_view="http://schemas.android.com/apk/res-auto"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/cardBackground"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    card_view:cardCornerRadius="4dp"
    android:layout_margin="5dp">

    <TextView
        android:id="@+id/keyAlias"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:textSize="30dp"/>

    <Button
        android:id="@+id/deleteButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/keyAlias"
        android:layout_alignParentLeft="true"
        android:layout_centerHorizontal="true"
        android:text="@string/delete"
        style="@style/Base.Widget.AppCompat.Button.Borderless" />

    <Button
        android:id="@+id/encryptButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/keyAlias"
        android:layout_alignRight="@+id/keyAlias"
        android:text="@string/encrypt"
        style="@style/Base.Widget.AppCompat.Button.Borderless"/>

    <Button
        android:id="@+id/decryptButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/keyAlias"
        android:layout_toLeftOf="@+id/encryptButton"
        android:text="@string/decrypt"
        style="@style/Base.Widget.AppCompat.Button.Borderless"/>
</RelativeLayout>

列表标题

使用此方法将列表标题添加到 ListView。

View listHeader = View.inflate(this, R.layout.activity_main_header, null);
listView.addHeaderView(listHeader);

image

在上面的图片中,ListView 当前是空的,所以只有列表标题是可以看到的。列表标题相当明确,在顶端是一个 EditText,当创建一个密钥时需要一个字符串作为别名。生成新的密钥的按钮就在这个的下方。按钮后是三个 EditTexts,一个是需要输入的字符串被加密,另一个展示了加密的结果,然后第三个展示了解密字符串(一个成功的解密)。此文件保存在 layout/activity_main_header.xml。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_horizontal"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin">

    <EditText
        android:id="@+id/aliasText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:hint="@string/key_alias"/>

    <Button
        android:id="@+id/generateKeyPair"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/aliasText"
        android:layout_centerHorizontal="true"
        android:layout_alignParentRight="true"
        android:text="@string/generate"
        android:onClick="createNewKeys" />

    <EditText
        android:id="@+id/startText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/generateKeyPair"
        android:layout_centerHorizontal="true"
        android:hint="@string/initial_text"/>

    <EditText
        android:id="@+id/encryptedText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/startText"
        android:layout_centerHorizontal="true"
        android:editable="false"
        android:textIsSelectable="true"
        android:hint="@string/final_text"/>

    <EditText
        android:id="@+id/decryptedText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/encryptedText"
        android:layout_centerHorizontal="true"
        android:editable="false"
        android:textIsSelectable="true"
        android:hint="@string/decrypt_result"/>
</RelativeLayout>

主要活动

关于任何活动,我们开始都使用 onCreate() 方法。我们做的第一件事就是引用 AndroidKeyStore,然后对其初始化,使用:

Keystore.getInstance("AndroidKeyStore");
keystore.load(null)

然后我们调用 refreshKeys() 方法(接下来讨论)来列出我们应用程序存储在密钥库所有的密钥。这个保证了在密钥库中的任何的密钥在 ListView 初始化之后立即显示。

列出密钥库所有的密钥

image

要获取可在密钥库应用程序中所有密钥的列举,只需调用 aliases() 方法。在我们下方的 refreshKeys() 方法中,我们获取密钥库别名,然后把返回的字符串放置到一个 ArrayList(由我们 ListView 的适配器所使用)中。

    private void refreshKeys() {
        keyAliases = new ArrayList<>();
        try {
            Enumeration<String> aliases = keyStore.aliases();
            while (aliases.hasMoreElements()) {
                keyAliases.add(aliases.nextElement());
            }
        }
        catch(Exception e) {}

        if(listAdapter != null)
            listAdapter.notifyDataSetChanged();
    }

在密钥库中添加一个新密钥

image

每一个由应用程序创建的密钥必须有一个独特的别名,可以是任何字符串。我们使用一个 KeyPairGeneratorSpec 对象来创建我们所需密钥的规格。你可以设置密钥(setStartDate() 和 setEndDate())的有效期,设置别名和其他的主体(自签密钥)。该主体必须是一个 X500Principal 对象,解析到一个字符串的格式“CN=通用名称,O=组织,C=国家”。

要生成一个私人或公共的密钥对,我们需要一个 KeyPairGenerator 对象。我们获取 KeyPairGenerator 集合的实例来使用 “AndroidKeyStore” 的 RSA 算法。调用 generateKeyPair() 创建新的密钥对(私钥和相应的公钥),并将其添加到密钥库中。

      public void createNewKeys(View view) {
        String alias = aliasText.getText().toString();
        try {
            // Create new key if needed
            if (!keyStore.containsAlias(alias)) {
                Calendar start = Calendar.getInstance();
                Calendar end = Calendar.getInstance();
                end.add(Calendar.YEAR, 1);
                KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this)
                        .setAlias(alias)
                        .setSubject(new X500Principal("CN=Sample Name, O=Android Authority"))
                        .setSerialNumber(BigInteger.ONE)
                        .setStartDate(start.getTime())
                        .setEndDate(end.getTime())
                        .build();
                KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
                generator.initialize(spec);

                KeyPair keyPair = generator.generateKeyPair();
            }
        } catch (Exception e) {
            Toast.makeText(this, "Exception " + e.getMessage() + " occured", Toast.LENGTH_LONG).show();
            Log.e(TAG, Log.getStackTraceString(e));
        }
        refreshKeys();
    }

从密钥库中删除密钥

image

从密钥库中删除一个密钥相当简单。有密钥别名做准备,调用 keystore.deleteEntry(keyAlias)。没有办法重新存储一个删除了的密钥,所以在删除之前要确定。

      public void deleteKey(final String alias) {
        AlertDialog alertDialog =new AlertDialog.Builder(this)
                .setTitle("Delete Key")
                .setMessage("Do you want to delete the key \"" + alias + "\" from the keystore?")
                .setPositiveButton("Yes", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        try {
                            keyStore.deleteEntry(alias);
                            refreshKeys();
                        } catch (KeyStoreException e) {
                            Toast.makeText(MainActivity.this,
                                    "Exception " + e.getMessage() + " occured",
                                    Toast.LENGTH_LONG).show();
                            Log.e(TAG, Log.getStackTraceString(e));
                        }
                        dialog.dismiss();
                    }
                })
                .setNegativeButton("No", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .create();
        alertDialog.show();
    }

加密文本块

image

加密一个文本块由密钥对的公钥执行。我们检索公钥,请求一个密码,使用我们更喜欢的加密或解密转换(“RSA/ECB/PKCS1Padding”),然后初始化密码,使用检索到的公钥来执行加密(Cipher.ENCRYPT_MODE)。密码操作(和返回)一个字节 []。我们将密码包含在 CipherOutputStream 中,和 ByteArrayOutputStream 一起来处理加密复杂性。加密进程的结果就是转化成一个显示为 Base64 的字符串。

    public void encryptString(String alias) {
        try {
            KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);
            RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey();

            // Encrypt the text
            String initialText = startText.getText().toString();
            if(initialText.isEmpty()) {
                Toast.makeText(this, "Enter text in the 'Initial Text' widget", Toast.LENGTH_LONG).show();
                return;
            }

            Cipher input = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
            input.init(Cipher.ENCRYPT_MODE, publicKey);

            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            CipherOutputStream cipherOutputStream = new CipherOutputStream(
                    outputStream, input);
            cipherOutputStream.write(initialText.getBytes("UTF-8"));
            cipherOutputStream.close();

            byte [] vals = outputStream.toByteArray();
            encryptedText.setText(Base64.encodeToString(vals, Base64.DEFAULT));
        } catch (Exception e) {
            Toast.makeText(this, "Exception " + e.getMessage() + " occured", Toast.LENGTH_LONG).show();
            Log.e(TAG, Log.getStackTraceString(e));
        }
    }

解密

解密基本上是加密的逆过程。解密是通过使用密钥对的私钥完成的。然后我们使用和加密相同的转化算法来初始化一个密码,但是设置 Cipher.DECRYPT_MODE。Base64 字符串解码为一个字节[],然后放置在 ByteArrayInputStream 中。然后我们使用一个 CipherInputStream 来解密数据为一个字节[]。然后显示为一个字符串。

    public void decryptString(String alias) {
        try {
            KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);
            RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey();

            Cipher output = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");
            output.init(Cipher.DECRYPT_MODE, privateKey);

            String cipherText = encryptedText.getText().toString();
            CipherInputStream cipherInputStream = new CipherInputStream(
                    new ByteArrayInputStream(Base64.decode(cipherText, Base64.DEFAULT)), output);
            ArrayList<Byte> values = new ArrayList<>();
            int nextByte;
            while ((nextByte = cipherInputStream.read()) != -1) {
                values.add((byte)nextByte);
            }

            byte[] bytes = new byte[values.size()];
            for(int i = 0; i < bytes.length; i++) {
                bytes[i] = values.get(i).byteValue();
            }

            String finalText = new String(bytes, 0, bytes.length, "UTF-8");
            decryptedText.setText(finalText);

        } catch (Exception e) {
            Toast.makeText(this, "Exception " + e.getMessage() + " occured", Toast.LENGTH_LONG).show();
            Log.e(TAG, Log.getStackTraceString(e));
        }
    }

安卓开发者简报

你想知道更多吗?订阅我们的安卓开发者简报。仅在下方输入你的电子邮件地址,就可以每周一次在你的收件箱里获取所有顶级的开发者信息、提示和链接。

PS. 永远不会有垃圾邮件。你的邮箱地址只用于安卓开发周报。

综述

安卓密钥库使创建和管理应用程序密钥变得轻而易举,并为应用程序提供了一个相对安全的库来存储加密密钥。当然公钥也可以发送到你的服务器上,服务器的公钥可以发送到你的应用程序上,来确保应用程序和服务器之前的安全通信。按往常来说,完整的源代码在 github 上可供你使用。想要补充、改正或讨论,请在下方留下评论,我们非常期待听到你的声音。

更多IT技术干货: wiki.jikexueyuan.com
加入极客星球翻译团队: http://wiki.jikexueyuan.com/project/wiki-editors-guidelines/translators.html

版权声明:
本译文仅用于学习和交流目的。非商业转载请注明译者、出处,并保留文章在极客学院的完整链接
商业合作请联系 wiki@jikexueyuan.com
原文地址:http://www.androidauthority.com/use-android-keystore-store-passwords-sensitive-information-623779/