为什么说在移动 App 中使用 OAuth API 密钥是不安全的?

阅读数:5345 2019 年 1 月 30 日

话题:安全移动最佳实践

对于移动应用程序来说,使用密钥来访问后端 API 服务来获取数据是非常常见的。那么,如何将密钥安全地包含在移动应用程序中呢?往短了说,你做不到。往长了说,请看这篇文章的剩余部分。

保存在本地和移动应用程序中的密钥被提取

从 JavaScript 应用程序中提取 API 密钥非常简单。只要你加载包含 JavaScript 应用程序的网页,浏览器就会下载整个源代码。你所要做的就是单击“查看源代码”,就可以看到整个源代码(包括 API 密钥)。JavaScript 代码通常会被压缩或缩小,看懂源代码可能并没有那么容易,但应用程序中定义的所有字符串都是可见的。

对于原生移动应用程序来说也是一样。这些应用程序在运行之前也会被下载到设备上,只是你下载的是二进制文件,而不是未编译的源代码。

让我们来做一个有趣的快速演示。如果你使用的是 Mac,并且安装了 1Password,请运行下面这个命令。你可以针对 Mac 上的任意一个二进制文件运行这个命令,但 1Password 应用程序中恰好有一些非常容易读懂的数据。

复制代码
strings /Applications/1Password\ 7.app/Contents/MacOS/1Password\ 7 | grep 1Password

strings 命令将会显示二进制文件的所有嵌入字符串,然后我们使用 grep 查找与 1Password 匹配的字符串,结果得到了在应用程序中使用的一堆文本!

复制代码
...
Restarting 1Password...
1Password failed to connect to 1Password
Please quit 1Password and start it again.
Add Account to 1Password
An update to Safari is required in order to use the 1Password extension.
Welcome to 1Password!
...

如果应用程序中嵌入了 API 密钥,strings 命令也会将它们显示出来。

让我们从头开始写一个简单的程序来证明这一点。我们将编写一个输出“hello world”的 C 语言程序。首先,创建一个叫作 hello.c 的文件,并写入以下内容:

复制代码
#include <stdio.h>
char hello[] = "hello";
char world[] = "world";
int main()
{
  printf("%s %s", hello, world);
  return 0;
}

在这段代码中,我们声明一个叫作 example 的字符串,值为“hello world”,然后在程序运行时打印它。你可以使用任何一个 C 语言编译器(如 gcc)编译它:

复制代码
$ gcc hello.c

结果会得到一个叫作 a.out 的二进制文件,然后你可以运行它:

复制代码
$ ./a.out
hello world

让我们针对这个二进制文件运行 strings 命令:

复制代码
$ strings a.out
%s %s
hello
world

它很容易就能发现二进制文件中的文本。

现在你可能会想:“如果我将 API 密钥分成几个部分并将它们分散在代码中会怎样?”这可能会为你赢得一些时间,但仍然会被一个真正有决心的人找出来。

即使是 Twitter 也无法阻止这种情况发生!2013 年,Twitter 官方应用程序的 API 密钥就是通过这种方式泄露的,让攻击者可以冒充合法的 Twitter 应用程序。

关键问题在于,一旦你将包含应用程序需要使用的字符串的内容发送给用户,总有人会提取它们。解决这个问题唯一的方法是使用“硬件安全模块”,秘钥被存储在微处理器中,微处理器无法通过编程的方式提取任密钥,它可以对数据进行加密签名,而不是发送密钥本身。

总的来说,如果你以未编译或二进制形式向用户发送代码,他们就有可能看到其中的内容。

HTTPS 请求在移动应用程序端被拦截

即使你认为你使用了最厉害的混淆技术,并且确信没有人能够从应用程序二进制文件中提取密钥,但总有人可以通过另一种方式找到密钥。

与运行在数据中心服务器上的应用程序不同,移动应用程序运行在用户手中的设备上,经过各种网络。用户可能是通过自家的网络安装应用程序,然后连接到咖啡店的网络并打开它,又或者在通过身份认证之前连接到酒店的网络。这些网络都是不可信任的,并且很有可能会出问题,或者攻击者会试图拦截数据!

你可能会想:“HTTPS 会保护传输中的数据!”在正常情况下确实如此。只要应用程序在发出请求时正确验证 HTTPS 证书,处于手机和服务器之间的攻击者几乎不可能看到流量。

但是,我们担心的不是这个问题。如果有人愿意,他们可以为你的 API URL 提供自己的 HTTPS 证书,在请求离开手机之前拦截自己的 HTTPS 连接。网上有很多教程教你如何做到这一点!实际上,这一项很好的技术,在开发应用程序时,可以用它来测试自己的应用程序,也是人们对 Instagram 等私有 API 进行反向工程的常见方式。

如果你有兴趣尝试一下,可以看看 Charles Proxy(https://www.charlesproxy.com/)或者免费的 mitmproxy(https://mitmproxy.org/)。

在手机上安装了自己的证书授权程序后,它就可以为任何一个域颁发 HTTPS 证书,对于你的手机来说,一切看起来都很正常。只是你的手机实际上是在向运行在笔记本电脑上的软件发出 HTTPS 请求,然后你的笔记本电脑再向真实的 API 发起新的 HTTPS 请求。这样你的笔记本电脑就可以看到手机发送给 API 的所有内容。

当然,攻击者不会通过这种方式随机拦截用户的传输数据,但如果有人想要知道应用程序使用的密钥,他们就可以通过这种方式轻松查看应用程序通过网络发送的所有数据。这意味着尽管你尽最大努力在源代码中隐藏应用程序密钥,仍然会在通过网络传输时被拦截。

这与 OAuth 有什么关系?

我们已经看到了从移动应用中提取 API 密钥的两种方法,但这与 OAuth 有什么关系呢?

传统上,OAuth 2.0 应用程序在开发人员注册应用程序时会发出 client_id 和 client_secret。当应用程序在 Web 服务器上运行时,这没问题,因为应用程序用户永远无法访问源代码,因此无法查看密钥。但是,当我们在 JavaScript 或原生应用程序中使用 OAuth 2.0 时,显然会有问题,因为正如我们所看到的那样,它们没有保密的能力。

在 OAuth 1 中,每个 API 请求都需要使用一个密钥,这是它的主要缺点之一,也是它被 OAuth 2.0 取代的主要原因。OAuth 1 是在移动应用程序开始流行之前出现的,所以它并没有考虑到这些限制。

随着 OAuth 2.0 的出现,这种情况发生了变化,特别是在引入 PKCE(证明密钥交换)扩展之后。我喜欢把 PKCE 看作是一个“动态”的客户端密钥。PKCE 没有采用向移动应用程序传递 client_secret 的方式,而是在每次启动 OAuth 请求流时,应用程序都会创建一个新的随机密钥。这样就不存在需要提前传递的密钥,攻击者也没有什么东西可窃取。

OAuth 仍然会通过网络发送访问令牌,如果使用了 mitmproxy 之类的东西,那么它们对你仍然是可见的,但不同的是,这个令牌是动态发出的,并且特定于使用它的用户!这样一来,源代码中就没有密钥了,如果有人想从他们自己的设备上拦截流量,他们看到的只是一个访问令牌!他们无法访问应用程序已经无法访问的东西。

如何保护移动应用中的密钥

希望你现在已经了解为什么在移动应用程序中发布 API 密钥或其他密钥是不安全的。那么你会怎么做呢?

OAuth 解决了这个问题,它没有向移动应用程序传递任何密钥,而是让用户参与获取应用程序访问令牌的过程。这些访问令牌在用户每次登录时都是唯一的。PKCE 扩展提供了在移动应用程序上安全执行 OAuth 请求流的解决方案,即使没有使用预先准备的密钥。

如果你需要从移动应用程序中访问 API,但愿可以支持 OAuth 和 PKCE!幸运的是,有关 PKCE 的大部分繁琐的任务都是由像 AppAuth 这样的 SDK 来处理的,所以你不需要自己编写所有的代码。如果你使用的是像 Okta 这样的 API,那么 Okta 的 SDK 会自动执行 PKCE,你完全不需要操心。

英文原文:https://developer.okta.com/blog/2019/01/22/oauth-api-keys-arent-safe-in-mobile-apps