1 WebSocket 原理
1.1 背景
WebSocket 是基於Http 協議的改進,Http 為無狀態協議,基於短連接,需要頻繁的發起請求,第二 Http 只能客戶端發起請求,服務端無法主動請求。
1.2 相同點
都是基於TCP的應用層協議。
都使用Request/Response模型進行連接的建立。
在連接的建立過程中對錯誤的處理方式相同,在這個階段WS可能返回和HTTP相同的返回碼。
都可以在網絡中傳輸數據。
1.3 不同點
WS使用HTTP來建立連接,但是定義了一系列新的header域,這些域在HTTP中並不會使用。
WS的連接不能通過中間人來轉發,它必須是一個直接連接。
WS連接建立之后,通信雙方都可以在任何時刻向另一方發送數據。
WS連接建立之后,數據的傳輸使用幀來傳遞,不再需要Request消息。
WS的數據幀有序。
WebSocket 分為握手和數據傳輸
1.4 握手
WebSocket 的握手基於http GET方法進行,
- 必須是有效的http request 格式
HTTP request method 必須是GET,協議應不小於1.1 如: Get /chat HTTP/1.1 - 必須包括Upgrade 頭域,並且其值為“websocket”,表明http 協議升級為WebSocket.
- 必須包括”Connection” 頭域,並且其值為 “Upgrade”
- 必須包括”Sec-WebSocket-Key”頭域,其值采用base64編碼的隨機16字節長的字符序列, 服務器端根據該域來判斷client 確實是websocket請求而不是冒充的,如http。響應方式是,首先要獲取到請求頭中的Sec-WebSocket-Key的值,再把這一段GUID “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”加到獲取到的Sec-WebSocket-Key的值的后面,然后拿這個字符串做SHA-1 hash計算,然后再把得到的結果通過base64加密,就得到了返回給客戶端的Sec-WebSocket-Accept的http響應頭的值。
- 必須包括”Sec-webSocket-Version” 頭域,當前值必須是13.
可能包括”Sec-WebSocket-Protocol”,表示client(應用程序)支持的協議列表,server選擇一個或者沒有可接受的協議響應之。
可能包括”Sec-WebSocket-Extensions”, 協議擴展, 某類協議可能支持多個擴展,通過它可以實現協議增強 - 可能包括任意其他域,如cookie
客戶端的握手如下:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 服務端的握手如下: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat 客戶端和服務端都發送了握手,並且成功,數據傳輸即可開始。
1.5 數據傳輸
通過Http握手之后,如果是http 協議的話,tcp 連接會斷開,這里在http 頭部指明了升級為 websocket, 所以tcp 連接不斷開。 WebSocket在握手后發送數據並象下層TCP協議那樣由用戶自定義,還是需要遵循對應的應用協議規范。 WebSocket 數據傳輸以數據幀的形式傳輸。
數據幀的結構如下圖:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
- FIN:1位,是否是消息的結束幀(分片)
- RSV1, RSV2, RSV3: 分別都是1位, 預留,用於約定自定義協議。 如果雙方之間沒有約定自定義協議,那么這幾位的值都必須為0,否則必須斷掉WebSocket連接;
- Opcode:4位操作碼,定義有效負載數據,如果收到了一個未知的操作碼,連接也必須斷掉,以下是定義的操作碼:
>
1. 0 表示連續消息分片
2. 1 表示文本消息分片
3. 2 表未二進制消息分片
4. 3-7 為將來的非控制消息片斷保留的操作碼
5. 8 表示連接關閉
6. 9 表示心跳檢查的ping
7. A 表示心跳檢查的pong, 當收到一個Ping幀時,一個端點必須在響應中發送一個Pong幀
8. B-F 為將來的控制消息片斷的保留操作碼
- Mask: 定義傳輸的數據是否有加掩碼,如果設置為1,掩碼鍵必須放在masking-key區域,客戶端發送給服務端的所有消息,此位的值都是1;
- Payload length: 傳輸數據的長度,以字節的形式表示:7位、7+16位、或者7+64位。如果這個值以字節表示是0-125這個范圍,那這個值就表示傳輸數據的長度;如果這個值是126,則隨后的兩個字節表示的是一個16進制無符號數,用來表示傳輸數據的長度;如果這個值是127,則隨后的是8個字節表示的一個64位無符合數,這個數用來表示傳輸數據的長度。多字節長度的數量是以網絡字節的順序表示。負載數據的長度為擴展數據及應用數據之和,擴展數據的長度可能為0,因而此時負載數據的長度就為應用數據的長度。注意Payload length不包括Masking-key在內。
- Masking-key: 0或4個字節,客戶端發送給服務端的數據,都是通過內嵌的一個32位值作為掩碼的;掩碼鍵只有在掩碼位設置為1的時候存在。 數據Mask方法是,第 i byte 數據 = orig-data \^ (i % 4)
2 AndroidSync 框架
AndroidAsync 是一個基於nio的異步socket ,http(客戶端服務器端),websocket,socket.io庫,AndroidAsync 是一個底層的網絡協議庫。AndroidAsync 適合用於一個未被封裝的Android的raw Socket, HTTP client/server, WebSocket, and Socket.IO。
>
特性
- 基於NIO,一個線程,回調驅動,高效
- 所有的操作返回一個Future,而且可以取消
- All operations return a Future that can be cancelled
- Socket client + socket server
- HTTP client + server
- WebSocket client + server
- Socket.IO 客戶端
2.1 啟動websocket 服務
void startWebSocketService(){ AsyncHttpServer server = new AsyncHttpServer(); server.setErrorCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { } }); server.listen(AsyncServer.getDefault(), 5000); server.websocket("/ws", new AsyncHttpServer.WebSocketRequestCallback() { @Override public void onConnected(final WebSocket webSocket, AsyncHttpServerRequest request) { mServiceSocket = webSocket; //Use this to clean up any references to your websocket webSocket.setClosedCallback(new CompletedCallback() { @Override public void onCompleted(Exception ex) { try { if (ex != null) { } } finally { } } }); webSocket.setStringCallback(new WebSocket.StringCallback() { @Override public void onStringAvailable(String s) { } }); } }); }
2.2 客戶端發起連接
void startWebSocketClient(){ AsyncHttpClient.getDefaultInstance().websocket(WSURL, null, new AsyncHttpClient.WebSocketConnectCallback() { @Override public void onCompleted(Exception ex, WebSocket webSocket) { if (ex != null) {; return; } mClientSocket = webSocket; webSocket.send("Hello Server"); webSocket.setStringCallback(new WebSocket.StringCallback() { public void onStringAvailable(String s) { } }); } }); }
3 SSL
3.1 SSL加密
wss 建立在HTTPS 的基礎上,在握手的時候使用HTTS 建立連接。HTTPS是HTTP over SSL/TLS,HTTP是應用層協議,TCP是傳輸層協議,在應用層和傳輸層之間,增加了一個安全套接層SSL/TLS。
HTTPS 連接過程如下圖:
-
客戶端發起一個https的請求,把自身支持的一系列Cipher Suite(密鑰算法套件,簡稱Cipher)發送給服務端 ClientHello
-
服務端接收到客戶端所有的Cipher后與自身支持的對比,如果不支持則連接斷開,反之則會從中選出一種加密算法和HASH算法,以證書的形式返回給客戶端。ServerHello Certificate ServerHelloDone
-
客戶端收到服務端響應的證書后. client_key_exchange+change_cipher_spec+encrypted_handshake_message
- 第一步、校驗證書的是否有效。關於客戶端校驗證書的是否有效已經做了詳細的介紹,這里就不贅述了。
- 第二步、生成隨機密碼。如果證書驗證通過,或者用戶接受了不授信的證書,此時瀏覽器會生成一串隨機密碼,然后用證書中的公鑰加密。
- 第三步、用最開始約定好的HASH方式,把握手消息取HASH值,把用
隨機數密碼
加密 “握手消息+握手消息HASH值(簽名)”和用公鑰加密的隨機密碼 一起發送給服務端。把握手消息做一個簽名,用於驗證握手消息在傳輸過程中沒有被篡改過。
-
服務端拿到客戶端傳來的密文,用自己的私鑰來解密,獲取隨機密碼,再用隨機數密碼 解密 握手消息與HASH值,並與傳過來的HASH值做對比確認是否一致。然后用隨機密碼加密一段握手消息(握手消息+握手消息的HASH值 )給客戶端。(此時服務器端已經獲取到了客戶端生成的隨機密碼了) 服務端用隨機密碼解密並計算握手消息的HASH,如果與客戶端發來的HASH一致,此時握手過程結束。
change_cipher_spec+encrypted_handshake_message
AndroidSync API
AndroidSync 對ssl 的支持如下:
KeyManagerFactory kmf = KeyManagerFactory.getInstance("X509"); KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(getContext().getResources().openRawResource(R.raw.keystore), "storepass".toCharArray()); kmf.init(ks, "storepass".toCharArray()); TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); KeyStore ts = KeyStore.getInstance(KeyStore.getDefaultType()); ts.load(getContext().getResources().openRawResource(R.raw.keystore), "storepass".toCharArray()); tmf.init(ts); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); AsyncHttpServer httpServer = new AsyncHttpServer(); httpServer.listenSecure(8888, sslContext); httpServer.get("/", new HttpServerRequestCallback() { @Override public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) { response.send("hello"); } }); Thread.sleep(1000); AsyncHttpClient.getDefaultInstance().getSSLSocketMiddleware().setSSLContext(sslContext); AsyncHttpClient.getDefaultInstance().getSSLSocketMiddleware().setTrustManagers(tmf.getTrustManagers()); AsyncHttpClient.getDefaultInstance().executeString(new AsyncHttpGet("https://localhost:8888/"), null).get();
3.2 自簽名證書驗證
ssl 連接需要證書,通常證書是CA機構發布,保證權威性。但是也可以使用自簽名證書。如12306.根據我們車機的特點,可以采用自簽名證書。
public SSLSocketFactory get12306SSLSocketFactory() {
try {
BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(CER_12306.getBytes())); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); Certificate cert = certificateFactory.generateCertificate(bis); Log.d("https", cert.toString()); KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(null); keyStore.setCertificateEntry("12306", cert); SSLContext sslContext = SSLContext.getInstance("TLS"); TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); trustManagerFactory.init(keyStore); sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom()); return sslContext.getSocketFactory(); } catch (Exception e) { e.printStackTrace(); } return null; } void httpConnect(){ try { URL url; url = new URL("https://kyfw.12306.cn/otn/"); HttpsURLConnection.setDefaultSSLSocketFactory(get12306SSLSocketFactory()); HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); conn.setDoOutput(true); conn.setDoInput(true); conn.connect(); if(conn.getResponseCode() == HttpURLConnection.HTTP_OK){ Log.e("https", "Https Certificate Success!"); Message msg = Message.obtain(); msg.what = HttpsActivity.HTTPS_SUCCES_12306; mViewHandler.sendMessage(msg); } } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
3.3 SSL 中間人攻擊
>
HTTPS ,是一種網絡安全傳輸協議,利用 SSL/TLS 來對數據包進行加密,以提供對網絡服務器的身份認證,保護交換數據的隱私與完整性。 中間人攻擊, Man-in-the-middle attack ,縮寫: MITM ,是指攻擊者與通訊的兩端分別創建獨立的聯系,並交換其所收到的數據,使通訊的兩端認為他們正在通過一個私密的連接與對方直接對話,但事實上整個會話都被攻擊者完全控制。
https 在理論上是可以抵御 MITM ,但是由於開發過程中的編碼不規范,導致 https 可能存在 MITM 攻擊風險,攻擊者可以解密、篡改 https 數據。 0X02 https 漏洞 Android https 的開發過程中常見的安全缺陷:
1)在自定義實現 X509TrustManager 時, checkServerTrusted 中沒有檢查證書是否可信,導致通信過程中可能存在中間人攻擊,造成敏感數據劫持危害。
2)在重寫 WebViewClient 的 onReceivedSslError 方法時,調用 proceed 忽略證書驗證錯誤信息繼續加載頁面,導致通信過程中可能存在中間人攻擊,造成敏感數據劫持危害。
3)在自定義實現 HostnameVerifier 時,沒有在 verify 中進行嚴格證書校驗,導致通信過程中可能存在中間人攻擊,造成敏感數據劫持危害。
4)在 setHostnameVerifier 方法中使用 ALLOW_ALL_HOSTNAME_VERIFIER ,信任所有 Hostname ,導致通信過程中可能存在中間人攻擊,造成敏感數據劫持危害。