前段时间在研究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
进行数据的读写:
ParcelFileDescriptor mFD = mUSBManager.openAccessory(acc); if (mFD != null) { FileDescripter fd = mFD.getFileDescriptor(); mIS = new FileInputStream(fd); // use this to receive messages mOS = new FileOutputStream(fd); // use this to send commands }
具体的代码可以看 这里 。
在 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
文件中:
private void usb_acc_string_send(UsbDeviceConnection connection, int index, String string) { byte[] buffer = (string + "/0").getBytes(); int len = connection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, OAP_SEND_STRING, 0, index, buffer, buffer.length, 10000); } private void usb_acc_strings_send() { usb_acc_string_send(m_usb_dev_conn, OAP_STR_MANUFACTURE, "SJTU"); usb_acc_string_send(m_usb_dev_conn, OAP_STR_MODEL, "SJTU IPADS"); } private void acc_mode_switch() { int acc_ver = usb_acc_version_get(m_usb_dev_conn); usb_acc_strings_send(); m_usb_dev_conn.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, OAP_START, 0, 0, null, 0, 10000); } private void usb_connect(UsbDevice device) { if (usb_open(device) < 0) { usb_disconnect(); return; } int dev_vend_id = device.getVendorId(); int dev_prod_id = device.getProductId(); if (dev_vend_id == USB_VID_GOO && (dev_prod_id == USB_PID_OAP_NUL || dev_prod_id == USB_PID_OAP_ADB)) { int ret = acc_mode_connect(); ... return; } acc_mode_switch(); usb_disconnect(); }
这个可以参照我之前讲的AOA协议来对照,这里当调用 usb_acc_strings_send()
将两个字符串发送出去之后,在 Device
端就会有相应的应用被唤醒,因为在该应用中定义了如下内容(在 aoa-dev-ssl-server
目录的 res/xml/usb_accessory_filter
文件中):
<?xml version="1.0" encoding="utf-8"?> <resources> <usb-accessory manufacturer="SJTU" model="SJTU IPADS" /> </resources>
而在 aoa-dev-ssl-server
目录的 AndroidManifest.xml
文件中定义如下:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="cn.sjtu.ipads.ual"> <uses-feature android:name="android.hardware.usb.accessory" android:required="true"/> <application> <uses-library android:name="com.android.future.usb.accessory" /> ... <activity android:name="UalTraActivity"> <intent-filter> <action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"/> </intent-filter> <meta-data android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" android:resource="@xml/usb_accessory_filter"/> </activity> ... </application> </manifest>
所以, aoa-dev-ssl-server
这个应用会被唤醒,进入 UalTraActivity
的 onCreate()
函数。在该类中,会进行USB accessory的连接:
public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDeviceHandler = new Handler(this); mUSBManager = (UsbManager) getSystemService(Context.USB_SERVICE); connectToAccessory(); } public void connectToAccessory() { // bail out if we're already connected if (mConnection != null) return; Log.v(TAG, "connectToAccessory"); // assume only one accessory (currently safe assumption) UsbAccessory[] accessories = mUSBManager.getAccessoryList(); UsbAccessory accessory = (accessories == null ? null : accessories[0]); if (accessory != null) { if (mUSBManager.hasPermission(accessory)) { openAccessory(accessory); } else { Log.v(TAG, "no permission for accessory"); } } else { Log.d(TAG, "mAccessory is null"); } } private void openAccessory(UsbAccessory accessory) { Log.v(TAG, "openAccessory"); mConnection = new UsbConnection(this, mUSBManager, accessory); if (mConnection == null) { Log.d(TAG, "mConnection is null"); finish(); } performPostConnectionTasks(); }
在 UsbConnection
这个类中会通过 UsbManager
的 openAccessory
接口得到一个文件描述符 mFileDescriptor
,之后的数据传输就是通过对这个 mFileDescriptor
的读写来进行的:
public UsbConnection(Activity activity, UsbManager usbManager, UsbAccessory accessory) { mActivity = activity; mFileDescriptor = usbManager.openAccessory(accessory); if (mFileDescriptor != null) { Log.v("UsbConnection", "mFileDescriptor"); mAccessory = accessory; FileDescriptor fd = mFileDescriptor.getFileDescriptor(); mInputStream = new FileInputStream(fd); mOutputStream = new FileOutputStream(fd); } IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION); filter.addAction(UsbManager.ACTION_USB_ACCESSORY_DETACHED); mActivity.registerReceiver(mUsbReceiver, filter); }
到目前为止, 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
类的这个接口:
mConnection.getOutputStream().write(buffer, 0, bufferLength);
收数据类似:
mConnection.getInputStream().read(buffer, bufferUsed, buffer.length - bufferUsed);
AOA协议基本就实现完成了。
握手协议由 aoa-acc-ssl-client
发起,在文件 aoa-acc-ssl-client/jni/hu_aap.c
中:
int hu_aap_start (byte ep_in_addr, byte ep_out_addr) { ... ret = hu_ssl_handshake (); // Do SSL Client Handshake with AA SSL server ... }
之后就会经历上面提到的整个握手过程。
这里需要注意的是,这个OpenSSL握手和加解密过程的实现,和我们平时通过socket传输数据时所涉及到的过程有点不一样。
我们在网络编程的时候,一般会调用下面两个API:
SSL *ssl = SSL_new(ctx); /* get new SSL state with context */ SSL_set_fd(ssl, sockfd); /* set connection to SSL state */
之后的网络数据读写直接通过 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
简单来说步骤是这样的:
ual_ssl_ctx = SSL_CTX_new(ual_ssl_method); ret = SSL_CTX_use_certificate(ual_ssl_ctx, x509_cert); ret = SSL_CTX_use_PrivateKey(ual_ssl_ctx, priv_key); ual_ssl_ssl = SSL_new(ual_ssl_ctx); ual_ssl_rm_bio = BIO_new(BIO_s_mem()); ual_ssl_wm_bio = BIO_new(BIO_s_mem()); SSL_set_bio(ual_ssl_ssl, ual_ssl_rm_bio, ual_ssl_wm_bio);
我中间跳过了很多步,不过那些都不重要(可以去看源码),这里最重要的就是这句话:
SSL_set_bio(ual_ssl_ssl, ual_ssl_rm_bio, ual_ssl_wm_bio)
这里将 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
中; 因此,整个加密的逻辑就可以是这样的:
int ssl_encrypt_data(int len, char *plain_buf, char *cipher_buf) { bytes_written = SSL_write(ual_ssl_ssl, plain_buf, len); bytes_read = BIO_read(ual_ssl_wm_bio, cipher_buf, DEFBUF); return (bytes_read); } int length = ssl_encrypt_data(len, plain_buf, cipher_buf); send_to_usb_fd(cipher_buf, length); }
类似的,解密的分解步骤是这样的:
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
中; 其相应的逻辑就变成这样了:
int ssl_decrypt_data(int len, char *cipher_buf, char *plain_buf) { bytes_written = BIO_write(ual_ssl_rm_bio, cipher_buf, len); bytes_read = SSL_read(ual_ssl_ssl, plain_buf, DEFBUF); return (bytes_read); } len = recv_from_usb_fd(cipher_buf); ssl_decrypt_data(len, cipher_buf, plain_buf); process(plain_buf);
和加解密过程相比,握手的过程会比较复杂一些,但是相关原理是一样的。
不管在 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
接口。
整个逻辑大概是这样的:
int ssl_hs_data_enqueue(int len, char *buf) { ret = BIO_write(ual_ssl_rm_bio, &buf[2], len - 2); return ret; } int ssl_hs_data_dequeue(char *buf) { ret = BIO_read(ual_ssl_wm_bio, buf, DEFBUF - 6); return ret; } void ssl_handshake() { ret = SSL_do_handshake(ual_ssl_ssl); } While (handshake not finished) { len = recv_from_usb_fd(data); ssl_hs_data_enqueue(len, data); ssl_handshake(); length = ssl_hs_data_dequeue(result); send_to_usb_fd(result, length); }
讲到这里,OpenSSL的整个流程也基本介绍完了。最后需要说明的一点,在 aoa-acc-ssl-client
中,数据的传输和加密都是在JNI层完成的,所以代码比较简单。但是在 aoa-dev-ssl-server
中,数据的传输是在Java层完成的,而加密是在JNI层实现的,所以中间有一个JNI调用的过程,会显得比较复杂。不过整体的原理是一样的。
关于JNI如何调用,网上有很多教程,也可以直接参照源码,这里就不详述了。
最后,关于整个项目的编译和运行,可以参照github中的 README.md
。