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

471

大多数应用程序开发人员都会将一些第三方库集成到他们的应用中,如果是为了访问服务,例如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

}

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


22
https://rammic.github.io/2015/07/28/hiding-secrets-in-android-apps/ - Patrick Dorn
1
我已经将存储在image/png中的数据读取为BufferReader,并获取了相应的密钥。 - Sumit
我认为这是一个有效的问题,并在Firebase Android SDK的github页面上发布了类似的问题:https://github.com/firebase/firebase-android-sdk/issues/1583。让我们看看是否会得到解决。 - grebulon
13个回答

415
  1. 目前你编译的应用程序包含关键字符串,但也包含常量名称APP_KEY和APP_SECRET。从这种自文档化的代码中提取密钥非常容易,例如使用标准的Android工具dx。

  2. 您可以应用ProGuard。它将保持关键字符串不变,但将删除常量名称。它还将重命名类和方法为短而无意义的名称,如有可能。然后需要更多时间来确定每个字符串所代表的目的。

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

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

    这是一种暴力方法;在处理的应用程序正常运行后,您可以对此类配置进行优化。

  3. 您可以手动混淆代码中的字符串,例如使用Base64编码或更复杂的内容(最好是本机代码)。然后,黑客必须静态地反向工程您的编码或在正确位置动态拦截解码。

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

  5. 您可能能够在自己的服务器上运行应用程序的部分内容。如果您能将密钥保存在那里,它们就是安全的。

最终,这是一个经济权衡:密钥有多重要,您能负担多少时间或软件成本,对密钥感兴趣的黑客有多复杂,他们想要花费多少时间,延迟密钥被破解的价值有多大,任何成功的黑客将以什么规模分发密钥等。与整个应用程序相比,密钥等小信息更难以保护。从本质上讲,客户端的任何内容都不是不可破解的,但您可以肯定地提高难度。

(我是ProGuard和DexGuard的开发者)


4
私钥字符串存储在Java类中还是String资源XML中,是否有区别? - Alan
2
@EricLafortune现在可以使用Android Keystore系统安全地存储密钥吗?(http://developer.android.com/training/articles/keystore.html) - David Thomas
2
@DavidThomas:你尝试过使用密钥库吗?我想混淆一个写在Java类中的API密钥。请回复。 - Sagar Trehan
3
我不理解第五个。它难道不和原问题有完全相同的问题吗? - pete
6
@BartvanIngenSchenau,我该如何向服务器发送请求以验证我的身份是否被确认?我可以想到一种解决方法...我会发送一个私钥凭据...但这不是我们试图解决的根本问题吗? - sudocoder
显示剩余11条评论

97

以下是我个人的一些想法,但在我看来只有第一个选项比较可靠:

  1. 将您的秘密保存在互联网上的某个服务器上,在需要时只需获取并使用。如果用户要使用Dropbox,则没有任何东西会阻止您向您的站点发出请求并获取您的秘钥。

  2. 将您的秘密放入jni代码中,添加一些变量代码使您的库变得更大且更难被反编译。您还可以将关键字符串分成几个部分并将它们保留在各种位置。

  3. 使用混淆器,同时在代码中添加哈希秘密,以后需要时解除哈希以使用。

  4. 将您的秘钥作为图像资源的最后一个像素。然后在需要时在代码中读取它。混淆您的代码应该有助于隐藏将读取它的代码。

如果您想快速了解有多容易阅读您的apk代码,则可以使用APKAnalyser:

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


53
如果用户可以反编译应用程序,他们很可能能够确定向您自己的服务器发送了哪个请求,并简单地执行该请求以获取秘密。这里没有什么灵丹妙药,但只需采取几个步骤,我敢打赌您将不会有问题!不过,如果您的应用程序非常受欢迎,情况可能会有所不同...好的想法! - Matt Wolfe
3
好的,号码1没有任何保证。 - marcinj
64
我非常喜欢在图片中隐藏钥匙的想法。+1 - Igor Čordaš
1
@MarcinJędrzejewski,您是否可以详细解释一下第四个解决方案(最好附带示例或代码片段)?谢谢。 - Dr.jacky
2
@Mr.Hyde 这被称为隐写术,这里给出样例代码过于复杂,你可以在谷歌上找到例子。我在这里找到了一个:http://www.dreamincode.net/forums/topic/27950-steganography/。这个想法很棒,但是由于APK代码可以反编译,所以破坏了它的美感。 - marcinj
显示剩余10条评论

61

另一种方法是根本不在设备上存储密钥!请参见移动API安全技术(特别是第三部分)。

利用间接传递的惯例,在API端点和应用程序身份验证服务之间共享密钥。

当客户端想要发起API调用时,它会请求应用程序身份验证服务进行身份验证(使用强大的远程认证技术),然后收到由密钥签名的、有时限的(通常为JWT)令牌。

每次API调用都会发送这个令牌,端点会在处理请求之前验证其签名。

实际密钥从未存在于设备上;事实上,应用程序甚至不知道它是否有效,只是请求身份验证并传递生成的令牌。通过间接传递的好处之一是,如果您想要更改密钥,可以这样做而无需要求用户更新已安装的应用程序。

因此,如果您想保护自己的密钥,首先将其从应用程序中删除是一个相当好的方法。


26
当您想要访问认证服务时,问题仍然存在。它会给您一个客户端ID和客户端密码。我们应该将它们保存在哪里? - Ashi
3
仅翻译文本内容:这并不能解决需要先进行身份验证才能使用私有API的问题。您从哪里获取所有应用程序用户的凭据? - Kibotu
@Ashi客户端ID以某种方式混淆,只有API终端知道如何从混淆的数据中提取数据,例如,如果仅有一些客户端ID的字符(其中客户端ID是实际客户端ID + 更多数据以创建混淆字符串),只表示实际数据,但如果黑客试图更改或修改客户端ID,则不知道哪些数据实际上代表客户端ID,因为只有API端点知道客户端ID如何被混淆以及如何从客户端ID中提取有用的数据才能表示客户端ID...希望你明白我的意思。 - Simranjeet Singh

36

旧的不安全方式:

按照以下3个简单步骤来保护API/Secret key (旧答案)

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

1. gradle.properties(项目属性): 创建具有密钥的变量。

GoogleAPIKey = "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的评论,上述方法不安全,反编译器将允许某人查看包含我们秘密密钥的BuildConfig文件。

解决方法:

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

请参考博客以使用NDK来保护API密钥。

关于如何在Android中安全存储令牌的后续


6
将密钥存储在Gradle文件中是否安全? - Google
4
gradle.properties不应该被提交到Git,这是一种避免秘密信息泄露在已提交的源代码中的方法。 - David Rawson
4
这并不能防止将API密钥捆绑到生成的“apk”中(它将添加到生成的“BuildConfig”文件中),但对于管理不同的API密钥(例如在开源项目中),这绝对是一个好主意。 - riyaz-ali
6
使用Java反编译器可以让某人查看BuildConfig文件和"GoogleSecAPIKEY"。 - Matthew
3
你的回答顶部非常大而粗的文本,如果解决方案明显不安全,就不应该说“安全”,正如指出的那样。我知道你有一个“更新”部分,但很多人可能不会阅读它。 - mtrewartha
显示剩余8条评论

21

除了 @Manohar Reddy 的解决方案之外,可以使用firebase数据库或firebase RemoteConfig(具有Null默认值):

  1. 对您的密钥进行加密
  2. 将其存储在firebase数据库中
  3. 在应用程序启动时或需要时获取它
  4. 解密密钥并使用它

这个解决方案有什么不同之处?

  • 无需firebase凭据
  • firebase访问受保护,因此只有带有签名证书的应用程序才能调用API
  • 进行加密/解密以防止中间人拦截。但是,调用已经通过https到达firebase

3
尊重地说,使用这个解决方案后我们仍然是第一个正方形。与其使用凭据,你建议使用证书。任何一个能够窃取你凭据的人也有能力窃取你签名的证书。 - Ashi
1
然而,使用建议的解决方案有一个优点,即我们在黑客面前增加了一个更复杂的障碍。 - Ashi
1
我们从不将私有证书保存在源代码中,这样就不会被盗取了,对吧? - Code Name Jack

17

一种可能的解决方案是在应用程序中对数据进行编码,并在运行时(当您想要使用该数据时)进行解码。我还建议使用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";
      }
    }
  }

而且你可以通过简单的谷歌搜索找到很多加密器类。


4
我认为这个方案已经接近最佳解决方案,但结合静态链接的NDK代码来对“运行应用程序名称”进行哈希处理,并使用该结果哈希值来解密秘密信息。 - c.fogelklou

16
App-Secret key应该保密,但在发布应用程序时,一些人可能会将其反向。对于那些人来说,无论是隐藏还是锁定ProGuard代码都不起作用。进行重构并插入一些位运算符以获取jk433g34hg3字符串的付费混淆器。如果您工作了3天,可以使黑客攻击时间延长5-15分钟:)最好的方法是保持原样,我个人认为。

即使您将密钥存储在服务器端(您的PC),密钥也可能被黑客攻击并打印出来。也许这需要最长时间?无论如何,在最好的情况下,这只需要几分钟或几小时。

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


1
好吧 - 不是我希望得到的答案 =) ... 我以为你可以实现很高的安全性 :( - Basic Coder
抱歉,这并不是您想要的卓越、超安全的解决方案,但对于那些能够使用编译器和反编译器的人来说,没有安全的Java代码:即使本地代码也可以通过十六进制查看器进行查看和解密。至少值得一试... - user529543
1
Proguard不会混淆实际密钥,对此最好的做法是使用一些简单的加密/解密例程,以便混淆可以隐藏。 - IAmGroot
3
“visible” 的解密程序很容易使反向操作,从而得到原始字符串。 - user529543
即使你可以在设备上保护它,你肯定无法防止用户查看自己的网络日志,在那里可能会传递秘密。 - sunnyX

14

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

保护传输中的密钥

首先要注意的是,使用Dropbox API时,使用他们的应用程序认证机制需要传输您的密钥和密码。 连接是HTTPS,这意味着你无法拦截流量而不知道TLS证书。这是为了防止人在移动设备到服务器的旅程中拦截并阅读数据包。 对于普通用户来说,这是确保其流量隐私的一种非常好的方法。

但是,它不能有效防止恶意人下载应用程序并检查流量。很容易使用中间人代理处理进出移动设备的所有流量。 由于Dropbox API的本质,无需反汇编或逆向工程代码即可提取应用程序密钥和密码。

您可以进行证书和公钥固定,以检查从服务器接收到的TLS证书是否是您期望的证书。这向客户端添加了检查,使拦截流量更加困难。这将使现场检查更加困难,但固定检查发生在客户端,因此仍然可能有可能关闭固定测试。不过,这确实增加了防护层次。

保护静态密钥

作为第一步,使用类似proguard这样的工具可以帮助减少保存任何密钥的明显位置。您还可以使用NDK存储密钥和密码并直接发送请求,从而大大减少拥有提取信息所需技能的人数。通过不在内存中直接存储这些值来进一步混淆,您可以在使用之前对它们进行加密和解密,如其他答案建议的那样。

更高级的选项

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

然后,您必须决定如何最好地与您的服务器通信,以确保它们受到保护。这很重要,以防止所有内部API出现相同问题。我可以给出的最佳经验法则是不直接传输任何秘密信息,以避免中间人攻击的威胁。相反,您可以使用您的秘密签署流量,并验证发往您的服务器的任何请求的完整性。计算消息的HMAC并以秘密为键进行签名是一种标准方法。我在一家安全产品公司工作,该公司也在此领域运营,这就是为什么这些东西使我感兴趣的原因。实际上,这是我同事撰写的一篇博客文章,其中涵盖了大部分内容。

我应该做多少?

对于任何这样的安全建议,您需要做出成本效益决策,以确定要让入侵者攻破安全措施的难度。如果您是保护数百万客户的银行,您的预算完全不同于在业余时间支持应用程序的人。实际上,几乎不可能防止有人攻破您的安全措施,但是通过一些基本预防措施,您可以走得很远。


2
你刚才从这里复制粘贴了这段内容:https://hackernoon.com/mobile-api-security-techniques-682a5da4fe10,却没有注明出处。 - ortonomy
1
@ortonomy 我同意他应该引用你链接的文章,但是他可能忘记了,因为他们在同一个地方工作... - Exadra37
9
Skip的文章和他们所基于的博客文章是在我的回答一周后发布的。 - ThePragmatist

9
无论你采取什么措施来保护你的密钥,都不会是一个真正的解决方案。如果开发者可以反编译应用程序,那么就没有办法保护密钥,隐藏密钥只是安全性靠模糊和代码混淆来保护。保护秘密密钥的问题在于为了保护它,你必须使用另一个密钥,并且该密钥也需要得到保护。想象一下一个隐藏在一个锁着钥匙的盒子里的密钥。你把盒子放在房间里并锁上门。你还剩下另一个要保护的密钥。而这个密钥仍然会被硬编码在你的应用程序中。
因此,除非用户输入PIN或短语,否则没有办法隐藏密钥。但是要做到这一点,你需要有一个管理PIN的方案,通过另一个渠道进行,而不是通过应用程序本身。对于像Google APIs这样的服务来说,这显然不是保护密钥的实际方法。

6

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


48
问题是 - 要连接包含所有秘密的服务器,我需要使用另一个秘钥 - 我不知道该放在哪里? ;) 我的意思是 - 这也不是最好的解决方案(我认为在这里没有理想的解决方案)。 - Mercury
6
你能否在这里解释一下客户端如何加密他想要发送到服务器的数据,而密钥在服务器端?如果你的答案是——服务器向客户端发送密钥——那么也必须确保这一点!所以没有什么魔法解决方案!你看不见吗?!可以使用公钥加密来解决这个问题。服务器生成一对公钥和私钥,并将公钥发送给客户端。客户端使用服务器的公钥来加密要发送的数据,只有服务器持有与该公钥匹配的私钥才能解密该数据。因此,即使公钥被截获,攻击者也无法解密数据,因为攻击者没有私钥。注意:公钥加密是非常安全的,但它比对称加密算法慢得多。 - Mercury
1
@Ken,你想解决的问题是防止他人使用你的服务器吗?我只知道一种解决方案,那就是身份验证。用户必须创建帐户并登录才能访问服务器。如果您不希望人们输入自己的信息,您可以让应用程序自动化处理。应用程序可以在手机上生成一个随机的登录令牌,将请求发送到服务器,并将电话号码和随机密码发送回手机。验证密码后,帐户将被创建,然后只需要令牌即可访问。 - Bernard Igiri
2
@BernardIgiri 然后我们又回到了原点。假设手机创建了一个随机登录名,服务器接受并发送一个 PIN(这就是我们所谓的“私有服务器”)。然后,拆解您的应用程序的人会发现,访问您的“私有”服务器所需的只是他自己可以创建的一些随机登录名。请告诉我,什么阻止他创建一个并访问您的服务器?实际上,您的解决方案与将登录名或 API 密钥存储到主服务器(我们想要将其凭据存储在私有服务器中)之间有什么区别? - Ken
4
@ken,随机数将与电话号码和短信物理访问权限进行身份验证。如果有人欺骗您,您将获得他们的信息。如果这还不够好,请强制他们创建完整的用户帐户和密码。如果这还不够好,请获取信用卡。如果这还不够好,请要求他们打电话确认。如果这还不够好,请面对面见面。您想要多安全/繁琐? - Bernard Igiri
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接