关于android:在应用程序中存储和保护私有API密钥的最佳实践

Best practice for storing and protecting private API keys in applications

大多数应用程序开发人员将把一些第三方库集成到他们的应用程序中。如果要访问Dropbox或YouTube之类的服务,或者日志记录崩溃。第三方图书馆和服务的数量惊人。大多数库和服务都是通过某种方式与服务进行身份验证来集成的,大多数情况下,这是通过API密钥实现的。为了安全起见,服务通常生成一个公共的和私有的,通常也称为密钥。不幸的是,为了连接到服务,必须使用这个私钥进行身份验证,因此可能是应用程序的一部分。不用说,这面临着巨大的安全问题。公共和私有API密钥可以在几分钟内从APK中提取出来,并且可以很容易地实现自动化。

假设我有类似的东西,我如何保护密钥:

1
2
3
4
5
6
7
8
9
public class DropboxService  {

    private final static String APP_KEY ="jk433g34hg3";
    private final static String APP_SECRET ="987dwdqwdqw90";
    private final static AccessType ACCESS_TYPE = AccessType.DROPBOX;

    // SOME MORE CODE HERE

}

您认为存储私钥的最佳和最安全的方法是什么?混淆,加密,你怎么看?


  • 事实上,编译后的应用程序包含密钥字符串,还包含常量名称app_key和app_secret。从这样的自记录代码中提取密钥是很简单的,例如使用标准的android工具dx。

  • 您可以应用Proguard。它将保持键字符串不变,但它将删除常量名称。在可能的情况下,它还将用简短、无意义的名称重命名类和方法。然后提取键需要更多的时间,以确定哪个字符串用于哪个目的。

    请注意,设置Proguard不应该像您担心的那样困难。首先,您只需要启用proguard,如project.properties中所述。如果第三方库有任何问题,您可能需要在proguard-project.txt中抑制一些警告和/或防止它们被混淆。例如:

    1
    2
    -dontwarn com.dropbox.**
    -keep class com.dropbox.** { *; }

    这是一种蛮力的方法;一旦处理过的应用程序工作,您就可以对这种配置进行优化。

  • 您可以在代码中手动混淆字符串,例如使用base64编码或更复杂的代码,甚至可能是本机代码。黑客将不得不静态地反向工程您的编码或动态拦截解码在适当的地方。

  • 您可以应用商业模糊器,如Proguard的专业兄弟DexGuard。它还可以为您加密/混淆字符串和类。提取密钥需要更多的时间和专业知识。

  • 您可能能够在自己的服务器上运行部分应用程序。如果你能把钥匙放在那里,它们就安全了。

  • 最后,你必须做一个经济上的权衡:钥匙有多重要,你能负担多少时间或软件,对钥匙感兴趣的黑客有多复杂,他们想花多少时间,在钥匙被黑客攻击之前有多值得延迟,任何成功的黑客会在多大程度上分发Keys等。像密钥之类的小信息比整个应用程序更难保护。从本质上讲,客户端的任何东西都是牢不可破的,但您当然可以提高标准。

    (我是Proguard和DexGuard的开发人员)


    在我看来,没什么好主意,只有第一个能保证:

  • 把你的秘密保存在互联网上的某个服务器上,在需要的时候,只需抓住它们并使用。如果用户将要使用Dropbox,那么没有什么能阻止您向您的站点发出请求并获取您的密钥。

  • 把你的秘密放在JNI代码中,添加一些变量代码使你的库更大,更难反编译。您还可以将密钥字符串拆分为几个部分,并将它们保存在不同的位置。

  • 使用模糊器,也放入密码散列的秘密,稍后在需要使用时解开它。

  • 将您的密钥作为资产中某个图像的最后一个像素。然后在需要的时候在代码中读取它。模糊代码有助于隐藏将读取代码的代码。

  • 如果您想快速查看apk代码的易读性,请抓取apkanalyser:

    http://developer.sonymobile.com/knowledge-base/tool-guides/analyse-your-apks-with-apkanalyser/


    遵循3个简单的步骤来保护API/密钥

    我们可以使用Gradle来保护API密钥或密钥。

    1。gradle.properties(项目属性):使用键创建变量。

    1
    GoolgeAPIKey ="Your API/Secret Key"

    2。build.gradle(module:app):在build.gradle中设置变量以访问活动或片段中的变量。将下面的代码添加到buildTypes。

    1
    2
    3
    buildTypes.each {
        it.buildConfigField 'String', 'GoogleSecAPIKEY', GoolgeAPIKey
    }

    三。通过应用程序的buildconfig在activity/fragment中访问它:

    1
    BuildConfig.GoogleSecAPIKEY

    更新:

    上面的解决方案对于开源项目在Git上提交很有帮助。(感谢David Rawson和Riyaz Ali的评论)。

    根据Matthew和Pablo Cegarra的评论,上述方法不安全,反编译器将允许有人使用我们的秘密密钥查看buildconfig。

    解决方案:

    我们可以使用ndk来保护API密钥。我们可以在本地C/C++类中存储密钥,并在Java类中访问它们。

    请按照此日志使用ndk保护API密钥。

    如何在Android中安全存储令牌的后续行动


    另一种方法是首先不要在设备上有秘密!参见移动API安全技术(尤其是第3部分)。

    使用久负盛名的间接寻址传统,在您的API端点和应用程序身份验证服务之间共享秘密。

    当您的客户机想要进行API调用时,它会要求应用程序认证服务对其进行认证(使用强大的远程认证技术),并且它会收到一个由秘密签名的时间限制(通常是JWT)令牌。

    令牌随每个API调用一起发送,端点可以在处理请求之前验证其签名。

    实际的秘密永远不会出现在设备上;事实上,应用程序永远不知道它是否有效,它只需要请求身份验证并传递所得到的令牌。作为间接的一个好处,如果你想改变秘密,你可以这样做而不需要用户更新他们安装的应用程序。

    因此,如果你想保护你的秘密,一开始就不要把它放在你的应用程序中,这是一个很好的方法。


    一种可能的解决方案是对应用程序中的数据进行编码,并在运行时使用解码(当您想使用该数据时)。我还建议使用progoard使你的应用程序的解压源代码难以阅读和理解。例如,我在应用程序中放入一个编码的密钥,然后在应用程序中使用解码方法在运行时解码我的密钥:

    1
    2
    3
    4
    5
    6
    //"the real string is:"mypassword"";
    //encoded 2 times with an algorithm or you can encode with other algorithms too
    public String getClientSecret() {
        return Utils.decode(Utils
                .decode("Ylhsd1lYTnpkMjl5WkE9PQ=="));
    }

    程序化应用程序的反编译源代码如下:

    1
    2
    3
    4
     public String c()
     {
        return com.myrpoject.mypackage.g.h.a(com.myrpoject.mypackage.g.h.a("Ylhsd1lYTnpkMjl5WkE9PQ=="));
      }

    至少对我来说已经足够复杂了。当我别无选择,只能在应用程序中存储一个值时,这就是我的方法。当然,我们都知道这不是最好的方法,但它对我很有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    /**
     * @param input
     * @return decoded string
     */
    public static String decode(String input) {
        // Receiving side
        String text ="";
        try {
            byte[] data = Decoder.decode(input);
            text = new String(data,"UTF-8");
            return text;
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return"Error";
    }

    反编译版本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
     public static String a(String paramString)
      {
        try
        {
          str = new String(a.a(paramString),"UTF-8");
          return str;
        }
        catch (UnsupportedEncodingException localUnsupportedEncodingException)
        {
          while (true)
          {
            localUnsupportedEncodingException.printStackTrace();
            String str ="Error";
          }
        }
      }

    你可以在谷歌上搜索到这么多加密程序类。


    The App-Secret key should be kept private - but when releasing the app
    they can be reversed by some guys.

    对于那些不想隐藏的家伙,锁定ProGuard代码。这是一个重构,一些付费的模糊器正在插入一些位运算符以返回jk433g34hg3。字符串。如果你工作3天,你可以延长5-15分钟的黑客攻击时间:)

    最好的办法就是保持原样,imho。

    即使存储在服务器端(PC),密钥也可以被黑客破解并打印出来。也许这需要最长的时间?不管怎样,最好是几分钟或几小时。

    普通用户不会解压缩您的代码。


    这个例子有许多不同的方面。我将提到一些我认为在其他地方没有明确提及的要点。

    保护过境中的秘密

    首先要注意的是,使用Dropbox API的应用程序身份验证机制访问Dropbox API需要您传输密钥和秘密。连接是HTTPS,这意味着您无法在不知道TLS证书的情况下拦截流量。这是为了防止一个人在从移动设备到服务器的旅途中截取和读取数据包。对于普通用户来说,这是一种很好的方式来确保他们的流量隐私。

    它不擅长的是防止恶意用户下载应用程序并检查流量。对于所有进出移动设备的流量,使用中间人代理非常容易。由于Dropbox API的性质,在这种情况下,提取应用程序密钥和机密不需要对代码进行反汇编或逆向工程。

    您可以执行pinning,以检查从服务器接收的TLS证书是否是您期望的证书。这给客户机增加了一个检查,使得拦截流量更加困难。这将使检查飞行中的流量更加困难,但固定检查在客户机中进行,因此仍有可能禁用固定测试。不过,这确实让事情变得更难了。

    保守秘密

    作为第一步,使用Proguard之类的工具将有助于使任何秘密在何处都不那么明显。您还可以使用NDK来存储密钥和秘密,并直接发送请求,这将大大减少具有提取信息的适当技能的人员的数量。通过在内存中不直接存储任何时间长度的值,可以实现进一步的混淆,您可以在使用前按照另一个答案的建议对其进行加密和解密。

    更高级的选项

    如果你现在偏执于将秘密放在你的应用程序中的任何地方,并且你有时间和金钱来投资于更全面的解决方案,那么你可以考虑将凭证存储在你的服务器上(假设你有)。这将增加对API的任何调用的延迟,因为它必须通过您的服务器进行通信,并且可能由于数据吞吐量的增加而增加运行服务的成本。

    然后,您必须决定如何最好地与服务器通信,以确保它们受到保护。这对于防止内部API再次出现相同的问题很重要。我能给出的最好的经验法则是不要因为处于中间威胁的人而直接传递任何秘密。相反,您可以使用您的秘密对流量进行签名,并验证到达您的服务器的任何请求的完整性。实现这一点的一种标准方法是计算加密消息的HMAC。我在一家公司工作,这家公司有一个安全产品,也在这个领域运作,这就是为什么这类东西让我感兴趣。事实上,这是我的一个同事写的一篇博文,大部分内容都在上面。

    我该做多少?

    有了这样的安全建议,您需要做出一个成本/收益决策,决定您想让某人闯入有多困难。如果你是一家保护数百万客户的银行,你的预算与那些在业余时间支持应用程序的人完全不同。几乎不可能防止有人破坏你的安全,但实际上很少有人需要所有的铃声和口哨,而且有了一些基本的预防措施,你可以获得很长的路。


    添加到@manohar reddy解决方案中,可以使用firebase数据库或firebase remoteconfig(默认值为空):

  • 加密你的钥匙
  • 将其存储在FireBase数据库中
  • 在应用程序启动或需要时获取
  • 破译钥匙并使用它
  • 这个解决方案有什么不同?

    • 没有FireBase的凭据
    • FireBase访问受到保护,因此只有具有签名证书的应用程序进行API调用的权限
    • 加密/解密以防止中间人拦截。然而已将https调用到firebase

    最安全的解决方案是将密钥保存在服务器上,并通过服务器路由所有需要该密钥的请求。这样,密钥就不会离开服务器,只要服务器是安全的,那么密钥也是安全的。当然,这个解决方案会带来性能成本。


    firebase database中保守秘密,在应用程序启动时从中获取,它比调用Web服务要好得多。


    无论你做什么来保护你的秘密钥匙都不会是真正的解决方案。如果开发人员可以对应用程序进行反编译,那么就没有办法保护密钥,隐藏密钥只是由于不清楚而造成的安全问题,代码混淆也是如此。加密密钥的问题是,为了加密,您必须使用另一个密钥,而且该密钥也需要加密。想想藏在用钥匙锁着的盒子里的钥匙。你把盒子放在房间里,然后锁上房间。您还有另一把钥匙要保管。这个密钥仍将被硬编码在您的应用程序中。

    因此,除非用户输入一个密码或短语,否则无法隐藏密钥。但要做到这一点,您必须有一个管理带外发生的管脚的方案,这意味着通过一个不同的通道。当然,对于像google apis这样的服务的密钥安全来说,这是不实际的。


    老了,但还是够好的。我想把它藏在一个.so库中,当然可以使用NDK和C++。.so文件可以在十六进制编辑器中查看,但祝您好运,反编译它:p


    保持这些隐私的唯一真正方法是将它们保存在您的服务器上,并让应用程序将其发送到服务器,服务器与Dropbox交互。这样您就不会以任何格式分发您的私钥。


    基于痛苦的经验,在咨询了IDA专家后,最好的解决方案是将代码的一个关键部分移动到一个dll/sharedObject中,然后从服务器获取它并在运行时加载。

    必须对敏感数据进行编码,因为这样做非常容易:

    1
    2
    $ strings libsecretlib.so | grep My
      My_S3cr3t_P@$$W0rD