Telegram-Android——端到端加密IM通訊詳解

????????本文主要是對 Telegram 7.9.3 的隨筆記載,在目前的版本上改動配置以正常編譯,簡單講解主界面UI流程、初始化、推送、加解密、前后端交互等核心功能,可能不夠準確,只是我目前所了解到的,歡迎指正錯誤和交流。
????????對于比較流行的IM開源項目,后續(xù)我會再針對性地重新整理一下Signalwire項目(曾經在這兩項目上二次開發(fā)過,這里是wire舊版本環(huán)境搭建+項目運行),針對最新版本再總結一下。
????????在打開項目之前,先看一下 gradle.properties文件,把 org.gradle.jvmargs 改到足夠大,建議6G以上,否則你會發(fā)現有些類在滑動代碼時一卡一卡的,下面先針對不同的2個AS版本修改配置以確保項目編譯成功。

在安裝了最新版本AS(Android Studio Arctic Fox | 2020.3.1 Patch)的情況下,你可能需要改動的地方如下:
  1. gradle/wrapper/gradle-wrapper.properties 文件:

distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
gradle 插件版本跟gradle版本對照

由于我本地安裝的SDK31的版本無法使用,于是將版本改為30,后續(xù)可能不需要做跟SDK版本相關的操作(下面所有操作):

TMessagesProj/build.gradle 文件:

compileSdkVersion 30
buildToolsVersion '30.0.0'

Dockerfile 文件:

ENV ANDROID_API_LEVEL android-30
ENV ANDROID_BUILD_TOOLS_VERSION 30.0.0
ENV ANDROID_VERSION 30
RUN cp $ANDROID_HOME/build-tools/30.0.3/dx $ANDROID_HOME/build-tools/30.0.0/dx
RUN cp $ANDROID_HOME/build-tools/30.0.3/lib/dx.jar $ANDROID_HOME/build-tools/30.0.0/lib/dx.jar

刪除目錄 values-v31/

chats_widget_info.xml 文件:

刪掉 targetCellWidth、targetCellHeightpreviewLayout、widgetFeatures

contacts_widget_info.xml 文件:

刪掉 targetCellWidth、targetCellHeightpreviewLayout、widgetFeatures、maxResizeHeight

啟動項目,可正常編譯通過。

在安裝較低版本的AS(以4.0.1為例)情況下,需要改動的地方如下:

build.gradle 文件:

classpath 'com.android.tools.build:gradle:4.0.1'

TMessagesProj/build.gradle 文件:

ndk.debugSymbolLevel = 'FULL'
4.1 之后 release 版本攜帶未精化的本地庫配置。全部屏蔽掉

關于SDK31的地方參照上面方法,另外需要額外修改 Dockerfile 文件:

FROM gradle:6.5-jdk11

至此項目可正常運行,如果res或xml有異常請按照提示來屏蔽或刪除。

????????第一次編譯可能比較耗時,慢慢等待。我個人推薦用最新版本的AS,用項目自帶的gradle插件版本(4.2.1)和gradle版本(6.7.1),因為不同gradle插件版本對應的語法可能有變動,最好跟著原項目來。

說一下個人對這個項目的感受

????????首先項目代碼量確實比較大,以前未見過一個類里面寫了5w+行代碼(這是個model類,我們一般不這么搞,這點得吐槽一下,把AS搞卡了),一個聊天界面可以寫2w+行代碼。估算了一下org.telegram包下代碼量大概50w行,這個包下沒有第三方代碼估算比較切合實際。做過一些項目之后,會發(fā)現一個功能齊全、穩(wěn)定迭代的IM項目可能只需要15w行java代碼就能搞定(估算了一下Signal的代碼大概19w行,如果把Signal代碼改成Kotlin可能只需要8w行左右)。之所以Telegram 的上層代碼量如此多,主要是因為其中幾乎所有的UI控件都是手動寫出來的,剩下的是因為他們手動寫不了(通知欄和widget)。Talegram debug版本給我的體驗已經非常好了,動效很炫酷、流暢,內存整體上不超過400M,單賬號登錄不再操作UI時一段時間后會降到170M以下,值得我們研究學習。接下來由淺入深,講一下具體功能。

UI 層次

????????簡而言之,在一個Activity中通過ActionBarLayout添加、移除不同視圖(BaseFragment——一個普通類)來實現UI切換,ActionBarLayout通過管理BaseFragment來讓其具有類似系統(tǒng)Fragment生命周期的功能。
????????當Activity需要展示某個BaseFragment時,流程如下:

ActionBarLayout#presentFragment-->BaseFragment#createView-->BaseFragment#onResume

????????當Activity需要移除某個BaseFragment時,流程如下:

ActionBarLayout#removeFragmentFromStackInternal、ActionBarLayout#closeLastFragment 
--> BaseFragment#onPause --> BaseFragment#onFragmentDestroy

下面是主要UI 圖層結構(手機版):


Telegram_UI.png

透過UI看本質,程序初始化做了些什么呢?先看上層GcmPushListenerService

????????為了避免誤解,我們先設置UserConfig.MAX_ACCOUNT_COUNT = 1 ,因為不論有多少賬號,推送入口只有一個,gcmPushToken 也只有一個。找到 ApplicationLoader#initPlayServices,在接收到Firebase返回的 gcmPushToken 之后,客戶端會將其上傳至IM服務器,這個過程結束之后,部分屬性會被賦值:

SharedConfig.pushAuthKey = random byte[256];
SharedConfig.pushString = gcmPushToken;
registeredForPush = true;
// 第一次收到推送消息時,下面數據會被賦值
SharedConfig.pushAuthKeyId = new byte[8];
byte[] authKeyHash = Utilities.computeSHA1(SharedConfig.pushAuthKey);
System.arraycopy(authKeyHash, authKeyHash.length - 8, SharedConfig.pushAuthKeyId, 0, 8);

有了這些條件,就可以解密后續(xù)推送過來的數據。
Gcm推送的整體流程如下:


Telegram_GcmPushListenerService.png

數據庫初始化

ApplicationLoader#getFilesDirFixed定義了項目中的緩存文件(下載的緩存文件,本地配置文件.dat,主題資源映射*.attheme,數據庫.db、文本資源等)。ApplicationLoader#onCreate --> MessagesController#getInstance --> BaseController#getMessagesStorage -->
MessagesStorage#getInstance --> MessagesStorage#openDatabase 即開始建庫建表。
一共 UserConfig.MAX_ACCOUNT_COUNT 個庫,路徑分別為 data/data/{pkg}/files、data/data//{pkg}/files/account1、data/data//{pkg}/files/account2 ……
至此,打開項目時就會創(chuàng)建UserConfig.MAX_ACCOUNT_COUNT個數據庫。

socket 初始化

????????由于 Telegram 的網絡層全部在 C++ 里面(主要涉及到 ConnectionsManager.cpp、Connection.cpp、ConnectionSocket、TgNetWrapper.cpp、Datacenter.cpp、Handshake.app、EventObject.cpp等文件),這里就從頭開始梳理一下底層初始化過程。在上層調用System.loadLibrary(xx_so)時,會走到對應 so 的 JNI_OnLoad 函數,本項目則是 jni::JNI_OnLoad,緊接著里面會執(zhí)行 registerNativeTgNetFunctions函數,我們找到它的實現:

extern "C" int registerNativeTgNetFunctions(JavaVM *vm, JNIEnv *env) {
// 檢查底層所需要的上層方法是否聲明(包括 ConnectionsManager#native_init),
// 主要是通過反射找方法(static 和 native 方法),找不到則報錯
}

socket的初始化分2步
1.檢查所需要的上層方法是否聲明:

ApplicationLoader#onCreate --> NativeLoader#initNativeLibs 
--> jni::JNI_OnLoad --> TgNetWrapper::registerNativeTgNetFunctions

至此底層函數跟上層方法映射完畢
2.初始化:

ApplicationLoader#onCreate --> ConnectionsManager#getInstance 
--> ConnectionsManager#init --> ConnectionsManager#native_init 
--> TgNetWrapper::init --> ConnectionsManager::init 
--> ConnectionsManager::ThreadProc --> ConnectionsManager::select

至此底層網絡模塊初始化完畢,一共會創(chuàng)建 UserConfig.MAX_ACCOUNT_COUNTConnectionsManager.cpp 對象 和ConnectionsManager.java 對象。ConnectionsManager::select 函數會給每個賬號創(chuàng)建一個死循環(huán)監(jiān)聽 socket fd 的狀態(tài),并分發(fā)數據。

Handshake過程——交換 nonce、 p、q ,前后端生成 aesKey、aesIv,為后續(xù)在線socket內容加解密。

????????首先是Client C 向 Server S發(fā)送隨機數 nonce ,然后S接收到 nonce后給C 發(fā)送隨機數 server_nonce 和大質數 pq,C分解 pq 得到 p、q,然后用RSA 公鑰加密 nonce 、server_nonce 、p、q并發(fā)送 ,S通過RSA 私鑰解密數據并返回 encrypted_answer 給C(雖然S 代碼未開源,但是可以根據C代碼推理出主要實功能),C 解密 encrypted_answer,本地計算 authKey 和臨時的 aesKey 、aesIv,用臨時 key 和 iv 加密數據發(fā)送給S,S 接收解密之后再次回復C后表示握手完畢。此時 C 的authKey 即為后續(xù)請求(消息)的AES 加密秘鑰的變量之一(另一個變量為請求數據的SHA256),所有上層數據(一般請求、消息發(fā)送和私密私聊)都會在底層加密一次。可以推理出 S 的 aesKey 和 aesIv 跟 C 的生成方式一樣。參考下圖:


Talegram_Handshake.png

SecretChatHelper

????????普通私聊的升級版,會話id為普通私聊的id左移32位,需要彼此確認才能建立,類似握手過程,創(chuàng)建上層數據加密的 authKey。

#sendRequestKeyMessage
#sendAcceptKeyMessage 
#updateEncryptedChat

加解密

????????關于底層加解密實現可以參考上層生成aesKey、aesIv過程MessageKeyData#generateMessageKeyData。底層代碼我暫時不方便解釋,避免誤導。下面是Socket發(fā)收消息的加解密相關代碼:

Telegram_Datacenter.png

???????? 聯(lián)想到一般的請求發(fā)給服務器之后,服務器接收需要解密,同時結合上圖解密過程和推送消息的解密過程,可以看出服務端轉發(fā)給客戶端的消息中,在線和離線的消息加密的秘鑰不一致,所以服務端解密了客戶端的socket消息后,會根據對方是否在線用握手時生成的秘鑰或者推送的pushAuthKey重新加密,然后發(fā)送(推送)給接收者。這次總結先寫到這,具體細節(jié)后續(xù)有空再慢慢研究。

Tips:
大家可以把這個宏——#define DEBUG_VERSION打開查看底層日志。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容