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

时间:2013-01-28 20:50:33

标签: android reverse-engineering proguard api-key

大多数应用开发者会将一些第三方库集成到他们的应用中。如果要访问服务,例如Dropbox或YouTube,或者用于记录崩溃。第三方图书馆和服务的数量是惊人的。大多数这些库和服务都是以某种方式通过服务进行身份验证而集成的,大部分时间都是通过API密钥进行的。出于安全目的,服务通常生成公共和私有(通常也称为秘密密钥)。不幸的是,为了连接到服务,必须使用此私钥进行身份验证,因此可能是应用程序的一部分。 毋庸置疑,这面临着巨大的安全问题。公共和私人API密钥可以在几分钟内从APK中提取,并且可以轻松实现自动化。

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

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

}

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

14 个答案:

答案 0 :(得分:310)

  1. 实际上,您编译的应用程序包含键字符串,但也包含常量名称APP_KEY和APP_SECRET。从这种自我记录代码中提取密钥是微不足道的,例如使用标准的Android工具dx。

  2. 您可以申请ProGuard。它将保持关键字符串不变,但它将删除常量名称。它还将尽可能用简短无意义的名称重命名类和方法。然后,提取密钥会花费更多时间,以确定哪个字符串用于哪个目的。

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

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

    这是一种蛮力的方法;您可以在处理完的应用程序工作后优化此类配置。

  3. 您可以在代码中手动对字符串进行模糊处理,例如使用Base64编码,或者最好使用更复杂的内容;甚至可能是本机代码。然后,黑客必须对您的编码进行静态逆向工程,或者在适当的位置动态拦截解码。

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

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

  6. 最后,你必须做出经济上的权衡:关键是多么重要,你能负担多少时间或软件,对钥匙感兴趣的黑客有多复杂,多少时间会他们想要花钱,在密钥被黑客攻击之前延迟多少,任何成功的黑客会以什么规模分发密钥等等。像密钥这样的小信息比整个应用程序更难保护。从本质上讲,客户端没有任何东西是牢不可破的,但你肯定可以提高标准。

    (我是ProGuard和DexGuard的开发者)

答案 1 :(得分:68)

很少有想法,在我看来只有第一个给出一些保证:

  1. 将您的秘密保存在互联网上的某个服务器上,并在需要时抓住它们并使用。如果用户即将使用dropbox,则不会阻止您向您的网站发出请求并获取您的密钥。

  2. 将您的秘密置于jni代码中,添加一些变量代码以使您的库更大,更难以反编译。您也可以将键字串分成几个部分并将它们保存在不同的位置。

  3. 使用混淆器,也可以放入代码哈希秘密,稍后在需要使用时将其解开。

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

  5. 如果您想快速了解一下阅读apk代码是多么容易,然后抓住APKAnalyser:

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

答案 2 :(得分:20)

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

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

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

GoolgeAPIKey = "Your API/Secret Key"

<强> 2。 build.gradle(Module:app):在build.gradle中设置变量以在activity或fragment中访问它。将以下代码添加到buildTypes {}。

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

第3。通过应用程序的BuildConfig访问Activity / Fragment:

BuildConfig.GoogleSecAPIKEY

更新:

上述解决方案有助于开源项目提交Git。 (感谢David Rawson和riyaz-ali的评论)。

根据Matthew和Pablo Cegarra的评论,上述方式并不安全,Decompiler将允许某人使用我们的密钥查看BuildConfig。

解决方案:

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

请关注this博客,以使用NDK保护API密钥。

A follow-up on how to store tokens securely in Android

答案 3 :(得分:15)

另一种方法是首先在设备上没有秘密!见Mobile API Security Techniques(特别是第3部分)。

使用时间传承的间接传统,分享您的API端点和应用认证服务之间的秘密。

当您的客户想要进行 API调用时,它会要求应用auth服务对其进行身份验证(使用强大的远程证明技术),并且它会收到时间限制(通常 JWT < / strong>)由秘密签名的令牌。

令牌随每次 API调用一起发送,其中端点可以在对请求执行操作之前验证其签名。

设备上永远不会出现实际的秘密;事实上,应用程序永远不知道它是否有效,它会发出请求身份验证并传递生成的令牌。作为间接的一个很好的好处,如果您想要更改秘密,您可以这样做,而无需用户更新其已安装的应用程序。

因此,如果您想保护自己的秘密,那么首先不要在您的应用中使用它是一个非常好的方法。

答案 4 :(得分:12)

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

// "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=="));
}

已编译应用程序的已解压缩源代码如下:

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

至少对我来说这很复杂。当我别无选择但在我的应用程序中存储值时,这就是我的方式。当然我们都知道这不是最好的方式,但它对我有用。

/**
 * @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";
}

反编译版本:

 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";
      }
    }
  }

你可以在google中找到很多加密类,只需要一点点搜索。

答案 5 :(得分:10)

  

App-Secret密钥应该保密 - 但是在发布应用程序时   他们可以被一些人逆转。

对于那些不会隐藏的人来说,锁定ProGuard代码。它是一个重构,一些付费的混淆器正在插入一些按位运算符来取回jk433g34hg3 串。如果你工作3天,你可以延长5-15分钟的黑客行为:)

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

即使您存储在服务器端(您的PC),密钥也可能被黑客入侵并打印出来。也许这花费的时间最长?无论如何,最好的情况是几分钟或几个小时。

普通用户不会反编译您的代码。

答案 6 :(得分:9)

这个例子有很多不同的方面。我将提到一些我不认为已在其他地方明确涵盖过的观点。

保护传输中的秘密

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

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

您可以pinning检查您从服务器收到的TLS证书是否是您期望的证书。这会向客户端添加一个检查,并使拦截流量变得更加困难。这将使得检查飞行中的流量变得更加困难,但是在客户端中发生了钉扎检查,因此可能仍然可以禁用钉扎测试。它确实让它变得更难。

保护休息时的秘密

作为第一步,使用像proguard这样的东西将有助于使任何秘密保持不太明显。您还可以使用NDK存储密钥和密钥并直接发送请求,这将大大减少具有提取信息的适当技能的人数。通过不将值直接存储在内存中任何时间长度,可以实现进一步的混淆,您可以在使用之前对其进行加密和解密,如另一个答案所示。

更多高级选项

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

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

我应该做多少?

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

答案 7 :(得分:7)

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

答案 8 :(得分:6)

添加到@Manohar Reddy解决方案中,可以使用firebase数据库或firebase RemoteConfig(默认值为Null):

  1. 密码输入
  2. 将其存储在firebase数据库中
  3. 在应用启动时或需要时获取它
  4. 解密密钥并使用它

此解决方案有什么不同?

  • firebase没有条件
  • firebase访问受到保护,因此只有具有签名证书的应用才具有 进行API调用的权限
  • 加密/解密以防止中间人被拦截。然而 呼叫已经https到firebase

答案 9 :(得分:4)

年老的帖子,但仍然足够好。我认为将它隐藏在.so库中会很棒,当然使用NDK和C ++。 .so文件可以在十六进制编辑器中查看,但好运反编译:P

答案 10 :(得分:4)

无论您采取什么措施保护您的密钥都不是真正的解决方案。如果开发人员可以反编译应用程序,则无法保护密钥,隐藏密钥只是隐蔽性的安全性,代码混淆也是如此。保护密钥的问题在于,为了保护密钥,您必须使用另一个密钥,并且还需要保护该密钥。想象一下隐藏在用钥匙锁定的盒子里的钥匙。你把一个盒子放在房间里并锁定房间。你剩下另一把钥匙了。而且该密钥仍将在您的应用程序中进行硬编码。

因此,除非用户输入PIN或短语,否则无法隐藏密钥。但要做到这一点,你必须有一个方案来管理带外发生的PIN,这意味着通过一个不同的渠道。对于保护Google API等服务的密钥,当然不实用。

答案 11 :(得分:4)

将密码保存在 firebase database 中,并在应用启动时从中获取, 它比调用Web服务要好得多。

答案 12 :(得分:3)

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

答案 13 :(得分:2)

基于痛苦的经验,并与IDA-Pro专家协商后,最好的解决方案是将代码的关键部分移至DLL / SharedObject,然后从服务器中获取并在运行时加载。

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

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