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


373

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

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

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

}

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



我有存储于图像/ PNG,通过为png作为BufferReader拿到钥匙
萨米特

我认为这是一个有效的问题,并在Firebase Android SDK github页面上发布了类似的问题:github.com/firebase/firebase-android-sdk/issues/1583。让我们看看是否可以解决。
grebulon

Answers:


348
  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. 您也许可以在自己的服务器上运行部分应用程序。如果您可以将钥匙放在那里,那么它们是安全的。

最后,这是一个经济上的折衷:密钥有多重要,您可以负担多少时间或软件,对密钥感兴趣的黑客有多熟练,他们想要多少时间?花费多少钱,才可以窃取密钥之前的延迟,成功的黑客将在多大程度上分配密钥,等等。像密钥之类的小信息比整个应用程序更难保护。从本质上讲,客户端上的任何内容都是坚不可摧的,但是您当然可以提高标准。

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


2
如果将私钥字符串存储在Java类中,而不是存储在String资源XML中,则@EricLafortune不会有所不同吗?
艾伦

1
@EricLafortune现在可以使用Android Keystore系统安全地存储密钥吗?(developer.android.com/training/articles/keystore.html
大卫·托马斯

1
@DavidThomas:您是否尝试过使用密钥库。我想混淆用Java类编写的API密钥。请回复
Sagar Trehan 2016年

1
Android KeyStore对此有用吗?(developer.android.com/training/articles/...
尉氏曾

1
我不明白#5。它不具有与原始问题相同的确切问题吗?
皮特

79

在我看来,只有很少的想法可以保证:

  1. 将您的秘密保存在Internet上的某个服务器上,并在需要时将其抓取并使用。如果用户将要使用保管箱,那么没有什么可以阻止您向您的站点发出请求并获取您的密钥。

  2. 将您的秘密放入jni代码中,添加一些可变代码以使您的库更大,更难于反编译。您也可以将密钥字符串分成几部分,并放在不同的位置。

  3. 使用混淆器,也将代码散列在秘密中,稍后在需要使用时对其进行散列。

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

如果您想快速了解一下阅读apk代码的难易程度,请抓住APKAnalyser:

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


46
如果用户可以反编译应用程序,尽管他们可能会确定对您自己的服务器的请求,然后简单执行以获取机密。这里没有灵丹妙药,但请采取一些步骤,我敢打赌,您会没事的!如果您的应用程序超级流行,尽管可能不是。
马特·沃尔夫

3
是的,数字1不能保证。
marcinj

41
我真的很喜欢在图像中隐藏键的想法。+1
伊戈尔·科尔达什(IgorČordaš)

1
@MarcinJędrzejewski您想对第四种解决方案进行更多解释(最好使用示例或代码片段)吗?谢谢。
Dr.jacky '16

2
@ Mr.Hyde称为隐写术,其方法过于复杂,无法在此处提供示例代码,您可以在google上找到示例。我在这里找到了一个:dreamincode.net/forums/topic/27950-steganography。这个主意很棒,但是由于可以反编译apk代码,因此破坏了它的美感。
marcinj '16

33

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

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

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

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

实际机密永远不会出现在设备上。实际上,该应用程序永远不会知道它是否有效,它会请求验证并传递生成的令牌。间接带来的好处是,如果您想更改机密,则可以这样做,而无需用户更新其已安装的应用程序。

因此,如果您想保护自己的秘密,那么首先不要在应用程序中拥有它是一个不错的方法。


5
这应该是公认的答案。
ortonomy

3
当您要访问身份验证服务时,问题仍然存在。它将为您提供客户端ID和客户端密码。我们应该在哪里保存它们?
阿什

不会解决您需要先通过api身份验证才能使用的私有api。您从哪里获得所有应用程序用户的凭据?
基博图

25

旧的不安全方式:

遵循3个简单的步骤来保护API /秘密密钥(旧答案

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

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

GoolgeAPIKey = "Your API/Secret Key"

2. build.gradle(模块:应用程序):在build.gradle中设置变量以在活动或片段中访问它。将以下代码添加到buildTypes {}中。

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

3.通过应用程序的BuildConfig在活动/片段中访问它:

BuildConfig.GoogleSecAPIKEY

更新:

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

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

解决方案:

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

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

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


4
将密钥存储在gradle文件中是否安全?
Google

4
@Google gradle.properties不应签入Git,因此,这至少是一种将秘​​密隐藏在已提交的源代码中的方法
David Rawson

4
尽管这绝对是管理不同API密钥(例如,在开放源代码项目中)的一个好主意,但这并不能防止将API密钥捆绑到结果文件中apk(它将添加到生成的BuildConfig文件中)
riyaz-ali

5
使用Java反编译器将允许某人查看BuildConfig文件和“ GoogleSecAPIKEY”
Matthew

3
您的BuildConfig.java文件将具有纯文本格式的密钥。这并不比OP已经在做的更好。
iceman '18

16

App-Secret密钥应保持私密性-但是在发布应用程序时,某些人可能会将其撤消。

对于那些家伙,它不会隐藏,请锁定任ProGuard一代码。这是一种重构,一些付费混淆器正在插入一些按位运算符以获取jk433g34hg3 String。如果您工作3天,则可以使黑客入侵时间延长5到15分钟:)

最好的方法是保持原样,恕我直言。

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

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


1
好吧-不是我希望得到的答案=)...我认为您可以达到高度的安全性:(
Basic Coder

抱歉,这不是您想要的出色,超安全的解决方案,但对于可以使用编译器的人来说,反编译器没有安全的Java代码:甚至可以使用hexa viewer查看原始代码并进行decrtyped。至少值得一试...

1
Proguard不会混淆实际的密钥。最好的办法是执行一些简单的加密/解密程序,以免混淆。
Doomsknight

3
它是“可见”的解密例程,很容易使反向,并且您具有原始字符串

14

一种可能的解决方案是对应用程序中的数据进行编码,然后在运行时使用解码(当您要使用该数据时)。我还建议使用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中稍作搜索,便可以找到这么多的加密器类。


13

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

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

此解决方案有什么不同?

  • 没有基础的firebase
  • Firebase访问受到保护,因此只有具有签名证书的应用才有权进行API调用
  • 加密/解密以防止中间人被拦截。但是已经调用https到firebase

所有人都尊重这一解决方案,我们仍然是第一个广场。建议不要使用证书,而建议使用证书。能够窃取您的凭据的任何人都可以窃取您的签名证书。
阿什

但是,利用建议的解决方案的一个优势是,我们在黑客面前增加了一个复杂的问题。
阿什

11

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

保护运输中的秘密

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

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

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

保护静止的秘密

第一步,使用诸如proguard之类的东西将有助于使其中保存任何秘密的地方变得不那么明显。您也可以使用NDK来存储密钥和机密并直接发送请求,这将大大减少具有适当技能以提取信息的人员。可以通过在任何时间长度内不直接将值存储在内存中来实现进一步的混淆,您可以按照其他答案的建议在使用前对其进行加密和解密。

更多高级选项

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

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

我应该怎么办?

借助任何此类安全建议,您都需要做出成本/收益决定,以决定让某人闯入它的难度。如果您是一家保护数百万客户的银行,则您的预算与支持他们的应用程序的人完全不同空余时间。阻止某人破坏您的安全性几乎是不可能的,但是实际上,很少有人需要所有的花招,并且通过一些基本的预防措施,您可以走很长一段路。


1
您只是从此处复制并粘贴了此内容:hackernoon.com/mobile-api-security-techniques-682a5da4fe10,但未确认其来源。
ortonomy

1
@ortonomy我同意他应该引用您链接的文章,但他可能已经忘记了,因为两者都在同一地方工作
Exadra37

2
Skip的文章以及它们所基于的博客文章也是在我回答后一周出现的。
ThePragmatist

8

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


32
问题是-要到达包含所有密钥的服务器,我应该使用另一个密钥-我想知道我将在哪里保存它?;)我想说的是-这也不是最佳解决方案(不要认为这里有理想的解决方案)
Mercury

事实并非如此,您的服务器完全在您的控制之下。它所需要的完全取决于您。
伯纳德·伊吉里

5
您能否在这里解释当密钥在服务器端时客户端如何加密他想发送到服务器的数据?并且如果您的答案是-服务器将密钥发送给客户端-因此也必须确保安全!所以再次没有魔术解决方案!你看不到吗?
水星

2
@BernardIgiri,然后再次回到正方形1。假设电话创建了一个随机登录,服务器接受了该登录并发送了一个密码(这就是我们正在谈论的所谓的私有服务器)。然后,反汇编您的应用程序的人会看到访问您的私有服务器所需的一切只是他可以自己创建的一些随机登录名。告诉我是什么阻止了他创建一个并访问您的服务器?实际上,您的解决方案与将登录名或api密钥实际存储到主服务器(我们希望将其凭据存储在私有服务器中)之间的区别是
Ken Ken

3
@ken将根据电话号码和对其短信的物理访问来验证随机数。如果有人欺骗您,您将获得他们的信息。如果还不够,请强迫他们创建完整的用户帐户和密码。如果那还不够好,那么也要获得信用卡。如果那还不够,请他们打电话。如果那还不够,请与他们面对面见面。您想要多安全/不便?
伯纳德·伊吉里

7

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


13
但是firebase的凭证呢?
the_joric

2
不幸的是,Firebase数据库在中国不起作用。
Konjengbam '18

9
没道理,攻击者可以从反编译代码中看到您的Firebase详细信息,并从您的datababse中获取任何数据
user924

3
我认为这是最好的解决方案,因为Firebase Apps使用SHA1允许访问服务器。反编译代码将无助于调​​用Firebase,因为黑客的新应用应使用准确的应用标记来访问Firebase。另外,存储的密钥在存储在Firebase DB中之前应先加密,并在收到后解密,以免中间人被截获。
艾曼·阿比

当您通过网络从Firebase数据库中获取机密时,与通过安全(https)渠道从另一个Web服务中获取相同机密相比,它的安全性如何?你可以解释吗?
Csaba Toth

6

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

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


5

年代久远,但仍然足够好。我认为使用NDK和C ++将其隐藏在.so库中会很好。.so文件可以在十六进制编辑器中查看,但很幸运,反编译:P


9
用户可以轻松地对共享库进行函数调用,并获取隐藏在其中的任何内容。无需拆装。
david72 '16

5
根据 androidauthority.com/…,目前尚无安全的方法在android中进行。
david72,2016年

1
@AhmedAwad不明白为什么要进行3次投票。任何人都可以轻松地反编译该应用程序,并查看ndk入口点是如何被调用的:/
Johny19'1

1
这个答案几乎是最好的选择之一,但是作者应该提到,非常重要的一点是您应该在NDK库中包含一个调用,以查看校验和是否与您的APK匹配,否则有人可以在外部调用NDK库您的应用程序
Stoycho Andreev

@Sniper会很棒,除非它有很大的问题。您如何知道哪个文件正在“调用”本机方法?如果您硬编码要检查的apk的名称,很好,但是如果我将“ hack” apk设置为与“ good” apk相同的文件夹怎么办?它将检查“好的” apk是否具有好的校验和,并允许我执行该本地方法。除非有从JNI / C ++方面了解调用者文件的方法,否则其他选项就毫无意义。
SocketByte

5

保持私有性的唯一真实方法是将它们保留在服务器上,并使应用程序将所有内容发送到服务器,然后服务器与Dropbox进行交互。这样,您就永远不会以任何格式分发私钥。


11
但是,如何防止世界其他地方呼叫服务器?
user2966445 '16

如果用“服务器”来表示凭据所在的Web服务器,则可以使用任何所需的方法。使用用户名/密码,oauth,活动目录等进行简单身份验证。确实取决于您的应用程序。
尼克

也许我遗漏了一些东西,但这是否还需要在应用程序内存储凭据?
user2966445

3
是的,但是您说该应用程序将首先与服务器进行身份验证。这是否意味着在应用程序中存储另一组凭据?我了解服务器将处理实际的保管箱调用。
user2966445

4
嗯,这可能意味着这个,但它是完全独立的授权。但是您不必。我正在谈论的用例是您的应用程序用户将登录到您的应用程序,例如使用facebook或twitter。您没有将他们的凭据存储在您的应用中,甚至不知道它们。该授权过程允许他们访问您的api服务器,该服务器具有dropbox的凭据,但是没有应用程序或用户可以直接访问它们。
尼克

0

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

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

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

3
您在哪里存储该服务器的凭据?
ZX9
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.