开发符合GDPR的端到端加密聊天应用的技术指南
如何制作符合GDPR的聊天应用
介绍
你好,Back4app社区!
这是来自 Virgil Security, Inc.: 我们是 Twilio的端到端加密消息 背后的加密技术。我们的朋友@Back4app请我们向您展示如何在Back4app上构建一个端到端加密的聊天应用。
在这篇文章中,我们将带您了解如何使简单的Back4App Android Messenger应用实现端到端加密!你准备好了吗?PS:如果你不在乎细节,可以直接跳到文章末尾下载最终产品。
什么是端到端加密?
首先,让我们快速回顾一下什么是E2EE(端到端加密)以及它是如何工作的。E2EE很简单:当您在聊天中输入消息时,它会在您的移动设备(或浏览器)上加密,只有当您的聊天伙伴接收到它并想在聊天窗口中显示时才会解密。

消息在通过Wi-Fi和互联网、云/网络服务器、进入数据库以及返回到您的聊天伙伴的过程中始终保持加密。换句话说,网络或服务器都不知道你们在聊天内容。

端到端加密中困难的地方在于以一种方式管理加密密钥,使得只有参与聊天的用户可以访问它们,而其他人无法访问。当我说“其他人无法访问”时,我是认真的:即使是您的云服务提供商的内部人员,甚至您自己,开发者,也无法访问;没有意外错误或法律强制窥探都是不可能的。编写加密代码,尤其是针对多个平台的代码是困难的:生成真正的随机数、选择正确的算法和选择正确的加密模式只是一些让大多数开发者感到无能为力的例子,最终选择不去做。
这篇博客文章将向您展示如何忽略所有这些恼人的细节,并使用Virgil的SDK快速简单地进行端到端加密。
作为介绍,我们将如何将Back4app的消息应用程序升级为端到端加密:
- 在注册时:我们将为新用户生成单独的私钥和公钥(请记住:接收者的公钥加密消息,而匹配的接收者的私钥解密消息)。
- 在发送消息之前,您将使用接收者的公钥加密聊天消息。
- 在接收消息后,您将使用接收者的私钥解密聊天消息。

我们将把用户的公钥发布到Virgil的卡片服务,以便聊天用户能够相互查找并能够为彼此加密消息。私钥将保留在用户设备上。
保持简单
这是E2EE聊天的最简单实现,它非常适合两个用户之间的简单聊天应用, 在这种情况下,谈话是短暂的,如果设备丢失了私钥,丢失消息历史也是可以接受的。
好吧,够了!让我们开始编码吧。
- 我们将首先指导您完成Android应用的设置,
- 然后,我们将添加一些代码使应用实现端到端加密。
先决条件
要完成本教程,您需要:
- Android Studio。
- 在Back4App创建的应用。
让我们设置“干净”的 Back4app 消息应用
1: 设置您的应用服务器
让我们从部署云函数开始。为此,您需要:
- 找到 main.js 和 package.json 在 scripts 目录;
- 用您喜欢的编辑器打开 main.js。
1.1) 获取 Back4App 凭证
- 打开您的应用的 仪表板 > 应用设置 > 安全与密钥:

- 在 main.js, 替换 PARSE_APP_ID 为你的 应用程序 ID 和 PARSE_REST_API_KEY 为你的 REST API 密钥:
1.2) 获取 Virgil 凭证

- 打开您的新 Virgil 应用程序,导航到 E3Kit 部分并生成一个 .env 文件在左侧边栏的 E3Kit 部分

- 复制 APP_ID, APP_KEY, 和 APP_KEY_ID 从 .env 文件

- 在你的 main.js 文件中替换复制的值到相应的字段 (main.js 的 scripts 目录):
1.3) 部署云代码函数
- 打开你的应用的 Back4App “仪表板” -> “核心” -> 云代码函数;

- 点击 +添加,选择你的 main.js 和 package.json(来自脚本目录),然后将它们移动到云文件夹;
- 点击部署。
2: 启动干净的 Back4app Kotlin 演示应用
不要忘记先设置 Back4App 云代码功能。这是本演示的强制部分。之后,按照以下步骤进行操作:
2.1) 在 Android Studio 中导入项目
- 打开 Android Studio -> 文件 > 新建 > 从版本控制中导入项目 > Git
- Git 仓库 URL: https://github.com/VirgilSecurity/chat-back4app-android
- 检出 clean-chat-kt 分支

重要!
选择“项目”类型的文件树。它将在整个教程中使用:

2.2) 在项目中设置 Back4App 凭据
- 打开 Back4App “仪表板” -> “应用设置” -> “安全性与密钥”;

前往 /app/src/main/res/values/strings.xml 文件在你的安卓项目中,并替换 your_back4app_app_id 为你的 应用程序 ID 和 your_back4app_client_key 为你的 客户端密钥:
2.3) 设置数据库
- 打开 Back4App “仪表板” -> “核心” -> “数据库浏览器” -> “创建一个类”,并创建 自定义 类型的类,命名为 消息 和 聊天线程;

2.4) 设置实时查询
- 按下你的应用程序上的 服务器设置 按钮
- 找到“网络托管和实时查询”块
- 打开实时查询设置并检查 激活托管 选项。
- 选择一个名称为你的子域名,以激活你创建的两个类的实时查询: 消息 和 聊天线程。
- 复制你的新子域名并点击保存按钮:

返回到 /app/src/main/res/values/strings.xml 并将你上面输入的“子域名”粘贴到 back4app_live_query_url 而不是“yourSubdomainName”:
完成这些步骤后,您将能够在 Android Studio 中点击运行按钮并使示例正常工作。使用模拟器或真实设备进行测试。
3: 运行干净的演示
要查看运行干净版本演示的结果,您需要:
- 注册 2 个用户;
- 在他们之间开始对话并发送几条消息;

3. 打开 Back4App “仪表板” -> “核心” -> “数据库浏览器” -> “消息”。
如果一切顺利,您应该会看到聊天消息应用程序弹出。注册两个用户并互相发送几条消息:您应该会看到新的数据出现在 消息 类中。
请注意,您可以在服务器上查看您的用户在聊天内容:

下一步: 关闭您的聊天界面,继续下一步 – 添加端到端加密。
现在,让我们对这些消息进行端到端加密!
到这一部分结束时,您的聊天消息在服务器上的样子是这样的:您能发现区别吗?

我们如何到达那里?显然,我们需要实现端到端加密,这意味着我们的应用需要:
- 在注册时生成私钥和公钥对
- 将私钥存储在用户设备的密钥存储中
- 将公钥作为“Virgil Card”发布到Virgil的卡片服务中,以便其他用户下载并使用它加密消息
- 使用公钥加密消息并用私钥签名;使用私钥解密消息并用公钥验证
为此,我们需要将E3Kit添加到我们的干净演示应用程序中,并添加一些代码以实现上述所有内容。
但在我们开始之前,让我们为您澄清两个重要术语:什么是Virgil Card和私钥?
- Virgil Card - Virgil卡携带用户的私钥。Virgil卡发布到Virgil的卡片服务中(想象这个服务就像一本电话簿),供其他用户检索:爱丽丝需要检索鲍勃的公钥,以便使用该公钥加密给鲍勃的消息。
- 私钥 - 加密密钥的私有部分。请记住,私钥可以解密使用匹配公钥加密的数据。
1: 将E3Kit添加到干净的E3Kit Back4App Kotlin演示中
- 在应用级别 (模块: app) 的 gradle 在 /app/build.gradle 添加(但暂时不要同步 gradle 脚本):
- 将以下内容添加到项目级别的 /build.gradle (项目: chat-back4app-android):
- 现在你可以同步 gradle 脚本;
- 通过添加新字段更新你的 AppVirgil 类:
- 按 Alt+ Enter 将必要的库导入到类中。
2: 使用 Back4App Cloud Code 进行用户身份验证
- 在 ../virgilsecurity/virgilback4app/model/ 目录中,创建数据类 AuthenticateResponse 和 VirgilJwtResponse,它们表示来自 Cloud Code 函数的响应:
- 在 ../virgilsecurity/virgilback4app/util/ 创建 AuthRx 对象,该对象实现对 Cloud Code 函数的调用(不要忘记在之后导入所有必要的库):
1 object AuthRx {
2
3 /**
4 * You can call it only after successful [authenticate]
5 */
6 fun virgilJwt(sessionToken: String) = Single.create<String> { emitter ->
7 val requestParams = mutableMapOf<String, String>().apply {
8 put("sessionToken", sessionToken)
9 }
10
11 ParseCloud.callFunctionInBackground<Map<String, Any>>(
12 KEY_VIRGIL_JWT,
13 requestParams
14 ) { virgilJwt, exception ->
15 if (exception == null)
16 emitter.onSuccess(virgilJwt[KEY_TOKEN].toString())
17 else
18 emitter.onError(exception)
19
20 }
21 }
22
23 private const val KEY_VIRGIL_JWT = "virgil-jwt"
24 private const val KEY_TOKEN = "token"
25 }3: 本地存储Virgil JWT
从Cloud Code函数接收到的Virgil令牌需要本地存储。让我们更新 Preferences 类在 ../virgilsecurity/virgilback4app/util/:
- 定义常量:
- 添加函数 setVirgilToken, virgilToken 和 clearVirgilToken:
~~~kotlin
fun setVirgilToken(virgilToken: String) { with(sharedPreferences.edit()) { putString(KEY_VIRGIL_TOKEN, virgilToken) apply() } }
fun virgilToken(): String? { with(sharedPreferences) { return getString(KEY_VIRGIL_TOKEN, null) } }
fun clearVirgilToken() { with(sharedPreferences.edit()) { remove(KEY_VIRGIL_TOKEN) apply() } }
1 Virgil token should be reset on logout. Let's add `Preferences.instance(this).clearVirgilToken()` line into `initDrawer` function of `ThreadsListActivity` class (in `../virgilsecurity/virgilback4app/chat/contactsList/`):
2 ~~~kotlin
3 private fun initDrawer() {
4 ...
5 nvNavigation.setNavigationItemSelectedListener { item ->
6 R.id.itemLogOut -> {
7 dlDrawer.closeDrawer(GravityCompat.START)
8 presenter.disposeAll()
9 showBaseLoading(true)
10 // New code >>
11 Preferences.instance(this).clearVirgilToken()
12 // << New code
13 ...
14 }
15 }
16 }4: 修改用户注册
E3Kit 处理您的私钥和公钥。在注册过程中生成它们,我们需要做以下事情:
- 在 ../virgilsecurity/virgilback4app/util/ 创建 RxEthree 类:
- 现在,添加 initEthree 函数,该函数在 RxEthree 类中初始化 E3Kit 实例:
1 import com.virgilsecurity.android.common.model.EThreeParams
2 import com.virgilsecurity.android.ethree.interaction.EThree
3
4
5 fun initEthree(identity: String, verifyPrivateKey: Boolean = false): 6Single<EThree> = Single.create { e ->
6 val params = EThreeParams(identity, {preferences.virgilToken()!!}, context)
7 val ethree = EThree(params)
8 if (verifyPrivateKey) {
9 if (ethree.hasLocalPrivateKey()) {
10 e.onSuccess(ethree)
11 } else {
12 e.onError(KeyEntryNotFoundException())
13 }
14 } else {
15 e.onSuccess(ethree)
16 }
17 }- 添加 registerEthree 函数,该函数将新用户注册到 RxEthree 类。 E3Kit 在注册期间生成一对密钥。生成的私钥存储在本地存储中,公钥作为 Virgil 卡发布到 Virgil 服务。
1 import com.android.virgilsecurity.virgilback4app.AppVirgil
2 import com.virgilsecurity.common.callback.OnCompleteListener
3 import io.reactivex.Completable
4
5
6 fun registerEthree(): Completable = Completable.create { e ->
7 AppVirgil.eThree.register().addCallback(object : OnCompleteListener {
8 override fun onError(throwable: Throwable) {
9 e.onError(throwable)
10 }
11
12 override fun onSuccess() {
13 e.onComplete()
14 }
15
16 })
17 }- 让我们对 LogInPresenter 类进行一些更新(在 ../virgilsecurity/virgilback4app/auth/).
- 添加 rxEthree 字段:
1 private val rxEthree = RxEthree(context)更新 requestSignUp 函数以使用 E3Kit 进行注册
1 fun requestSignUp(identity: String, onSuccess: () -> Unit, onError: (Throwable) -> Unit) {
2 val password = generatePassword(identity.toByteArray())
3
4 val disposable = RxParse.signUp(identity, password)
5 .subscribeOn(Schedulers.io())
6 .observeOn(Schedulers.io())
7 // New code >>
8 .toSingle { ParseUser.getCurrentUser() }
9 .flatMap { AuthRx.virgilJwt(it.sessionToken) }
10 .map { preferences.setVirgilToken(it) }
11 .flatMap { rxEthree.initEthree(identity) }
12 .map { AppVirgil.eThree = it }
13 .flatMap { rxEthree.registerEthree().toSingle { Unit } }
14 // << New code
15 .observeOn(AndroidSchedulers.mainThread())
16 // Updated code >>
17 .subscribeBy(
18 onSuccess = {
19 onSuccess()
20 },
21 onError = {
22 onError(it)
23 }
24 )
25 // << Updated code
26
27 compositeDisposable += disposable
28 }5: 修改登录功能
现在,让我们对 requestSignIn 方法进行更改,属于 LogInPresenter 类(在 ../virgilsecurity/virgilback4app/auth/):
1 fun requestSignIn(identity: String,
2 onSuccess: () -> Unit,
3 onError: (Throwable) -> Unit) {
4
5 val password = generatePassword(identity.toByteArray())
6
7 val disposable = RxParse.logIn(identity, password)
8 .subscribeOn(Schedulers.io())
9 .observeOn(Schedulers.io())
10 // New code >>
11 .flatMap { AuthRx.virgilJwt(it.sessionToken) }
12 .map { preferences.setVirgilToken(it) }
13 .flatMap { rxEthree.initEthree(identity, true) }
14 .map { AppVirgil.eThree = it }
15 // << New code
16 .observeOn(AndroidSchedulers.mainThread())
17 // Updated code >>
18 .subscribeBy(
19 onSuccess = {
20 onSuccess()
21 },
22 onError = {
23 onError(it)
24 }
25 )
26 // << Updated code
27
28 compositeDisposable += disposable
29 }6: 获取现有聊天列表
接下来,将处理 E3Kit 初始化的函数添加到 ThreadsListFragment 类(在 ../virgilsecurity/virgilback4app/chat/contactsList/):
1 private fun onInitEthreeSuccess() {
2 presenter.requestThreads(ParseUser.getCurrentUser(),
3 20,
4 page,
5 Const.TableNames.CREATED_AT_CRITERIA,
6 ::onGetThreadsSuccess,
7 ::onGetThreadsError)
8 }
9
10 private fun onInitEthreeError(throwable: Throwable) {
11 showProgress(false)
12 if (adapter.itemCount == 0)
13 tvError.visibility = View.VISIBLE
14
15 Utils.toast(activity, Utils.resolveError(throwable))
16 }更新 postCreateInit 函数以初始化 E3Kit:
1 override fun postCreateInit() {
2 ...
3 presenter = ThreadsListFragmentPresenter(activity)
4
5 showProgress(true)
6 // Updated code >>
7 if (AppVirgil.isEthreeInitialized()) {
8 presenter.requestThreads(ParseUser.getCurrentUser(),
9 20,
10 page,
11 Const.TableNames.CREATED_AT_CRITERIA,
12 ::onGetThreadsSuccess,
13 ::onGetThreadsError)
14 } else {
15 presenter.requestEthreeInit(ParseUser.getCurrentUser(), ::onInitEthreeSuccess, ::onInitEthreeError)
16 }
17 // << Updated code
18 }并将以下代码添加到 ThreadsListFragmentPresenter 类中 virgilsecurity.virgilback4app.chat.contactsList/:
- 添加字段:
1 private val rxEthree = RxEthree(context)- 和函数
1 fun requestEthreeInit(currentUser: ParseUser, onSuccess: () -> Unit, onError: (Throwable) -> Unit) {
2 val disposable = rxEthree.initEthree(currentUser.username)
3 .subscribeOn(Schedulers.io())
4 .observeOn(AndroidSchedulers.mainThread())
5 .subscribeBy(
6 onSuccess = {
7 AppVirgil.eThree = it
8 onSuccess()
9 },
10 onError = {
11 onError(it)
12 }
13 )
14
15 compositeDisposable += disposable
16 }此时我们能够注册/登录用户并与其他用户创建新聊天.
现在让我们为我们的消息添加加密.
7: 消息加密和解密
让我们添加 findCard 函数到 RxEthree 类(在 ../virgilsecurity/virgilback4app/util/),这将帮助我们通过用户名获取最新的Virgil卡片:
1 fun findCard(identity: String): Single<Card> = Single.create { e ->
2 AppVirgil.eThree.findUser(identity).addCallback(object : OnResultListener<Card> {
3 override fun onError(throwable: Throwable) {
4 e.onError(throwable)
5 }
6
7 override fun onSuccess(result: Card) {
8 e.onSuccess(result)
9 }
10
11 })
12 }当聊天被打开时,我们应该获取收件人的Virgil卡。编辑 postCreateInit 的 ChatThreadFragment 类(在 ../virgilsecurity/virgilback4app/chat/thread/)通过替换
1 presenter.requestMessages(thread,
2 50,
3 page,
4 Const.TableNames.CREATED_AT_CRITERIA,
5 ::onGetMessagesSuccess,
6 ::onGetMessagesError)用一个新的代码替换
1 presenter.requestCard(recipientId,
2 ::onGetCardSuccess,
3 ::onGetCardError)并添加两个函数:
1 private fun onGetCardSuccess(card: Card) {
2 showProgress(false)
3 adapter.interlocutorCard = card
4 presenter.requestMessages(thread,
5 50,
6 page,
7 Const.TableNames.CREATED_AT_CRITERIA,
8 ::onGetMessagesSuccess,
9 ::onGetMessagesError)
10 }
11
12 private fun onGetCardError(t: Throwable) {
13 if (t is VirgilCardIsNotFoundException || t is VirgilCardServiceException) {
14 Utils.toast(this,
15 "Virgil Card is not found.\nYou can not chat with user without Virgil Card")
16 activity.onBackPressed()
17 }
18 showProgress(false)
19 srlRefresh.isRefreshing = false
20 lockSendUi(lock = false, lockInput = false)
21
22 Utils.toast(this, Utils.resolveError(t))
23 }现在让我们更改 ChatThreadPresenter:
- 添加字段:
1 private val eThree = AppVirgil.eThree
2 private lateinit var userCard: Card
3 private val rxEthree = RxEthree(context)- 添加一个获取收件人 Virgil 卡片的函数:
1 fun requestCard(identity: String,
2 onSuccess: (Card) -> Unit,
3 onError: (Throwable) -> Unit) {
4
5 val disposable = rxEthree.findCard(identity)
6 .subscribeOn(Schedulers.io())
7 .observeOn(AndroidSchedulers.mainThread())
8 .subscribeBy(
9 onSuccess = {
10 userCard = it
11 onSuccess(it)
12 },
13 onError = {
14 onError(it)
15 }
16 )
17
18 compositeDisposable += disposable
19 }- 在 requestSendMessage 函数中添加出站消息的加密:
1 fun requestSendMessage(text: String,
2 thread: ChatThread,
3 onSuccess: () -> Unit,
4 onError: (Throwable) -> Unit) {
5
6 val encryptedText = eThree.authEncrypt(text, userCard)
7 val disposable = RxParse.sendMessage(encryptedText, thread)
8 ...
9 }将所有传入消息的解密添加到 ChatThreadRVAdapter 类中(在 ../virgilsecurity/virgilback4app/chat/thread/):
- 添加字段:
1 private var eThree: EThree = AppVirgil.eThree
2 lateinit var interlocutorCard: Card- 在 onBindViewHolder 函数中实现消息解密
1 override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
2 when (viewHolder) {
3 is HolderMessageMe -> {
4 val decryptedText = eThree.authDecrypt(items[position].body)
5 viewHolder.bind(decryptedText)
6 }
7 is HolderMessageYou -> {
8 val decryptedText = eThree.authDecrypt(items[position].body, interlocutorCard)
9 viewHolder.bind(decryptedText)
10 }
11 }
12 }8: 运行完整的端到端加密演示
现在要查看我们完全端到端加密演示的结果,请再次按照以下步骤进行操作:
- 注销当前用户
- 注册两个新用户;
- 在他们之间开始对话并发送几条消息;
- 打开 Back4App “仪表板” -> “核心” -> “数据库浏览器” -> “消息”。
重要! 您必须 注销 当前用户并注册两个新用户,之后您可以与这两个新用户开始 E2EE 聊天。原因是您的前两个用户没有 Virgil 卡片,因此您无法对他们使用加密\解密。{: .blockquote-tip}
完成!现在您可以看到用户的消息是加密的,并且只能由用户自己在应用中访问。
HIPAA 和 GDPR 合规性:
端到端加密是一种满足 HIPAA(美国 1996 年健康保险流通与问责法)和 GDPR(欧盟通用数据保护条例)技术要求的方法。如果您需要更多详细信息,请注册一个免费的 Virgil 账户,加入我们的 Slack 社区并在那儿联系我:我们很乐意讨论您自己的隐私情况,并帮助您了解满足技术 HIPAA 和 GDPR 要求所需的内容。
接下来该去哪里?
最终项目。如果您错过了拼图中的某些部分,请打开 E2EE 项目分支。您可以在此代码中插入您的应用凭据(就像您在文章中所做的那样)并构建项目。
您可以在 这里 找到有关您可以使用 Virgil Security 构建的更多信息。