前段时间在研究Android Auto(关于什么是Auto请自行google),里面涉及到两个比较关键的数据传输和加密协议:Android Accessory Protocol
和OpenSSL
。具体来说,auto和车载系统(之后称为headunit)之间数据的传输以及最初连接的建立是基于Android Open Accessory (AOA)协议的,而它们两个之间的认证过程以及数据的加密是基于OpenSSL的握手和加密协议。
在研究这两个协议的过程中,“如何将AOA协议和SSL协议结合起来”是一个很关键的问题。我在网上找了很多资料,但是并没有一个比较完整的教程,所以打算在这篇博客中做一个详细的介绍,并且将相关代码开源。
关于什么是Android Auto,可以到google的官方网站上去查询。简单来说,就是Google开发的一套机制,可以将手机上的应用(包括地图、音乐、通话等)和车载系统进行交互,使得车载系统的功能更加丰富。如果要描述它的机制的话,可以用下面一张图来表示:
其中,负责和headunit进行交互的是GMS (Google Mobile Service)的Car Service,然后它会和Google开发的Auto应用联系,Auto应用负责和其它第三方应用程序交互,现在支持Android Auto的第三方应用程序有这些,可以看到大部分还是一些音乐和社交类的应用。这里有一个特殊的应用,那就是Google Map,它是直接整合在GMS里面的,可以直接和Car service进行交互,应该不需要经过Auto(当然这还仅仅是我的推测)。
由于这篇博文主要介绍的是手机和车载的交互协议,因此我们主要关注的是GMS car service和headunit之间的交互,所以,我们把上面那张图简化一下:
其中Android Open Accessory(AOA)
协议发生在两台设备通过USB进行连接,其中一台作为Accessory
,一台作为Device
。在Auto的例子中,headunit的角色为Accessory
,手机的角色为Device
。AOA
协议主要作用是关于Accessory
和Device
在初始化连接时候的互相识别,以及之后数据的传输。
关于Accessory
和Device
的概念可以看这篇博文,这里就不详述了。
当Accessory
和Device
建立连接之后,两边就可以进行数据的传输了,但是由于一些隐私问题,传输的数据需要进行加密,因此就引入了OpenSSL
协议。在OpenSSL协议中连接两端的实体被分为了Server
和Client
,这两个角色有什么区别会在之后提到。在这里我们只需要知道在Auto的例子中,headunit的角色为Client
,手机的角色为Server
。因此我们的图又被抽象为如下:
好了,到现在为止,我们就完全和Auto撇清关系了,我们接下来要介绍的,就是两个实体,它们通过USB连接,一个作为AOA协议的Accessory
和OpenSSL协议的Client
,另一个作为AOA协议的Device
和OpenSSL协议的Server
。
当两个实体通过USB进行连接之后,最先做出反应的是Accessory
,它会做以下几件事情:
Device
的VendorID和ProductID;比如在Auto的例子中,headunit需要判断VendorID是否匹配0x18D1
,ProductID是否匹配0x2D00
或者0x2D01
?)
Accessory
可以直接和Device
进行通信(直接跳到步骤5);通过USB发送一个请求:
requestType: USB_DIR_IN | USB_TYPE_VENDOR
request: 51
value: 0
index: 0
data: protocol version number (16 bits little endian sent from the device to the accessory)
如果对方返回一个非零整数,则表示该设备支持Android accessory模式,该返回值表示支持的协议版本号;
发送另外的请求,该请求中包含一些字符串,用来表示Device
中哪些应用程序可以来和Accessory
进行交互:
requestType: USB_DIR_OUT | USB_TYPE_VENDOR
request: 52
value: 0
index: string ID
data zero terminated UTF8 string sent from accessory to device
有效的string ID包含以下几类:
manufacturer name: 0
model name: 1
description: 2
version: 3
URI: 4
serial number: 5
在Auto的例子中,headUnit在这个过程中会发送两个string ID:manufacturer name = "Android"
,model name = "Android Auto"
。该string ID会触发手机设备中com.google.android.gms.car.FirstActivity
的onCreate()
函数,从而使得GMS car service和headUnit进行accessory的连接;
Accessory
最后发送一个请求,告诉Device
开始进入Android accessory模式,并且重新建立连接:
requestType: USB_DIR_OUT | USB_TYPE_VENDOR
request: 53
value: 0
index: 0
data: none
步骤3结束之后,Device
会重新和Accessory
进行连接,这时Accessory
回到步骤1进行检查,如果检查通过,则进入步骤5,如果Device
不支持Android accessory模式,或者没有匹配的应用程序,则Device
会返回信息告诉Accessory
,这时Accessory
就只能等待下一个手机设备的接入。
从这之后,Accessory
和Device
将通过Android Accessory协议进行通信,Accessory
首先获得该USB连接中的一些配置元数据,包括接口类型(UsbInterface),端点信息(UsbEndpoint)等,从而获得对应的bulk endpoints,进行之后的通信过程。
在数据通信的过程中,Accessory
通过libusb
库提供的libusb_control_transfer
和libusb_bulk_transfer
接口进行数据的传输,其中,libusb_control_transfer
用于传输一些指令数据,而libusb_bulk_transfer
用于传输一些比较大的数据,比如音频数据,图像数据等;
而Device
则通过Android USBManager
提供的openAccessory
接口获得一个文件描述符,然后通过其对应的FileInputStream
和FileOutputStream
进行数据的读写:
1 2 3 4 5 6 |
|
在Accessory
和Device
建立连接,并且可以传输数据之后,它们就要开始建立OpenSSL的连接,对数据进行加解密了。这里主要分为了两个过程:握手过程和数据加解密过程。这里简单介绍下握手协议:
握手协议的作用是身份的认证,该过程由Client
端发起,这个协议的过程如下:
在这个过程中,Client
首先会对Server
提供的证书(Certificate)进行验证,Server
也会对Client
提供的证书进行验证。同时它们会用Server
的公钥(包含在Server
的证书中)和存在Server
端的私钥进行秘钥的协商,最后通过这个协商好的秘钥(master key)对数据进行加解密。
这里推荐StackOverflow的一个帖子,里面的前两个回答对OpenSSL握手协议进行了一个很棒的解释。
在进行了背景介绍之后,我们开始来分析下如何实现这整个过程。
源码可以在这里下载。
里面有两个目录:aoa-dev-ssl-server
和aoa-acc-ssl-client
,分别代表上面描述的两个实体。这两个目录是两个不同的Android应用,编译完之后可以通过adb install
安装在Android平台的手机或者平板上。
首先由aoa-acc-ssl-client
发起,代码在src/cn/sjtu/ipads/uas/UasTransport.java
文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
|
这个可以参照我之前讲的AOA协议来对照,这里当调用usb_acc_strings_send()
将两个字符串发送出去之后,在Device
端就会有相应的应用被唤醒,因为在该应用中定义了如下内容(在aoa-dev-ssl-server
目录的res/xml/usb_accessory_filter
文件中):
1 2 3 4 |
|
而在aoa-dev-ssl-server
目录的AndroidManifest.xml
文件中定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
所以,aoa-dev-ssl-server
这个应用会被唤醒,进入UalTraActivity
的onCreate()
函数。在该类中,会进行USB accessory的连接:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
在UsbConnection
这个类中会通过UsbManager
的openAccessory
接口得到一个文件描述符mFileDescriptor
,之后的数据传输就是通过对这个mFileDescriptor
的读写来进行的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
到目前为止,Accessory
和Device
的连接已经建立,之后的数据传输就可以进行了。
在Accessory
这端的数据读写是在jni层中,可以参阅aoa-acc-ssl-client/jni/hu_usb.c
这个文件。
发数据的流程是这样的:
hu_aap_usb_send() -> hu_usb_send() -> iusb_bulk_transfer(out)
接受数据的流程是这样的:
hu_aap_usb_recv() -> hu_usb_recv() -> iusb_bulk_transfer(in)
具体代码这里不贴了,有兴趣自己去看。
在Device
这端的数据读写是在java层,可以参阅aoa-dev-ssl-server/src/cn/sjtu/ipads/ual/UalTraActivity.java
这个文件。
发数据就是调用了之前获得的UsbConnection
类的这个接口:
1
|
|
收数据类似:
1
|
|
AOA协议基本就实现完成了。
握手协议由aoa-acc-ssl-client
发起,在文件aoa-acc-ssl-client/jni/hu_aap.c
中:
1 2 3 4 5 |
|
之后就会经历上面提到的整个握手过程。
这里需要注意的是,这个OpenSSL握手和加解密过程的实现,和我们平时通过socket传输数据时所涉及到的过程有点不一样。
我们在网络编程的时候,一般会调用下面两个API:
1 2 |
|
之后的网络数据读写直接通过SSL_write(ssl)
和SSL_read(ssl)
来做就行了。因为SSL和这个负责读写数据的文件描述符sockfd
已经绑定在一起了,在网络库的内部帮我们实现了网络buffer到SSL内部buffer的映射。
然而,当我们需要通过USB进行传输数据的时候就没有那么简单了。我们前面说过,我们同样可以通过对某个文件描述的读写操作来传送和接受USB数据,但是USB的库并没有帮我们实现其buffer到SSL内部buffer的映射。因此这步操作需要我们自己来实现。这里就用到了OpenSSL
+Memory BIO
的机制。
先提供一个参考资料:Using OpenSSL with Memory BIO
简单来说步骤是这样的:
1 2 3 4 5 6 7 8 9 10 11 |
|
我中间跳过了很多步,不过那些都不重要(可以去看源码),这里最重要的就是这句话:
1
|
|
这里将ual_ssl_ssl
这个数据结构和两段内存联系在一起,这两段内存分别是read BIO
和write BIO
。
这有什么用呢?其实要解释清楚这个就需要先对OpenSSL的机制有一个初步的了解。
在SSL的所有操作中(比如证书验证,加密,解密等),说到底,就是从某段内存中读取数据,对其进行相应的操作,然后将结果写在另外一段内存中。因此这里的两段内存就分别对应了read BIO
和write BIO
。
似乎还是有点晕,那么我们来举个例子:
如果我们要进行数据加密,分解步骤是这样的:
len
的明文数据plain_buf
;SSL_write(ual_ssl_ssl, plain_buf, len)
,这时OpenSSL内部的逻辑就会对这段数据进行加密,并且将结果保存在write BIO
中;BIO_read(ual_ssl_wm_bio, cipher_buf, DEFBUF)
,就可以将这段加密好的数据读出来保存在cipher_buf
中;因此,整个加密的逻辑就可以是这样的:
1 2 3 4 5 6 7 8 |
|
类似的,解密的分解步骤是这样的:
len
的密文数据cipher_buf
;BIO_write(ual_ssl_ssl, cipher_buf, len)
,将这段密文写入和SSL相关联的read BIO
的内存中;SSL_read(ual_ssl_ssl, plain_buf, DEFBUF)
,将read BIO
的数据进行解密,并将结果保存在plain_buf
中;其相应的逻辑就变成这样了:
1 2 3 4 5 6 7 8 9 |
|
和加解密过程相比,握手的过程会比较复杂一些,但是相关原理是一样的。
不管在Server
端还是在Client
端,都需要调用SSL_do_handshake(ual_ssl_ssl)
这个API,OpenSSL内部的逻辑就会根据当前的状态对ual_ssl_rm_bio
的数据进行处理,并将结果写到ual_ssl_wm_bio
中。在调用SSL_do_handshake
这个API前,需要将相关的数据写到read BIO
中(比如在Server
端,第一次调用SSL_do_handshake
前需要将Client Hello
的数据通过BIO_write
写进ual_ssl_rm_bio
中)。所以说,一般情况下需要手动调用大于一次的SSL_do_handshake
接口。
整个逻辑大概是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
讲到这里,OpenSSL的整个流程也基本介绍完了。最后需要说明的一点,在aoa-acc-ssl-client
中,数据的传输和加密都是在JNI层完成的,所以代码比较简单。但是在aoa-dev-ssl-server
中,数据的传输是在Java层完成的,而加密是在JNI层实现的,所以中间有一个JNI调用的过程,会显得比较复杂。不过整体的原理是一样的。
关于JNI如何调用,网上有很多教程,也可以直接参照源码,这里就不详述了。
最后,关于整个项目的编译和运行,可以参照github中的README.md
。