转载

OpenSSL Authenticated Android Accessory Protocal

前段时间在研究Android Auto(关于什么是Auto请自行google),里面涉及到两个比较关键的数据传输和加密协议: Android Accessory Protocol 和 OpenSSL 。具体来说,auto和车载系统(之后称为headunit)之间数据的传输以及最初连接的建立是基于 Android Open Accessory (AOA) 协议的,而它们两个之间的认证过程以及数据的加密是基于 OpenSSL 的握手和加密协议。

在研究这两个协议的过程中,“如何将AOA协议和SSL协议结合起来”是一个很关键的问题。我在网上找了很多资料,但是并没有一个比较完整的教程,所以打算在这篇博客中做一个详细的介绍,并且将相关代码开源。

关于Android Auto

关于什么是 Android Auto ,可以到google的官方网站上去查询。简单来说,就是Google开发的一套机制,可以将手机上的应用(包括地图、音乐、通话等)和车载系统进行交互,使得车载系统的功能更加丰富。如果要描述它的机制的话,可以用下面一张图来表示:

OpenSSL Authenticated Android Accessory Protocal

其中,负责和headunit进行交互的是GMS (Google Mobile Service)的Car Service,然后它会和Google开发的Auto应用联系,Auto应用负责和其它第三方应用程序交互,现在支持Android Auto的第三方应用程序有 这些 ,可以看到大部分还是一些音乐和社交类的应用。这里有一个特殊的应用,那就是Google Map,它是直接整合在GMS里面的,可以直接和Car service进行交互,应该不需要经过Auto(当然这还仅仅是我的推测)。

关于两个协议

由于这篇博文主要介绍的是手机和车载的交互协议,因此我们主要关注的是GMS car service和headunit之间的交互,所以,我们把上面那张图简化一下:

OpenSSL Authenticated Android Accessory Protocal

其中 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 。因此我们的图又被抽象为如下:

OpenSSL Authenticated Android Accessory Protocal

好了,到现在为止,我们就完全和Auto撇清关系了,我们接下来要介绍的,就是两个实体,它们通过USB连接,一个作为AOA协议的 Accessory 和OpenSSL协议的 Client ,另一个作为AOA协议的 Device 和OpenSSL协议的 Server 。

Android Open Accessory(AOA)协议

当两个实体通过USB进行连接之后,最先做出反应的是 Accessory ,它会做以下几件事情:

步骤1:获得和它连接的 Device 的VendorID和ProductID;

步骤2:判断它们是否匹配相应的数字;

比如在Auto的例子中,headunit需要判断VendorID是否匹配 0x18D1 ,ProductID是否匹配 0x2D00 或者 0x2D01 ?)

  • 如果匹配,则表示该设备支持Android accessory模式,并且当前已经处于该模,所以 Accessory 可以直接和 Device 进行通信(直接跳到步骤5);
  • 否则,则表示该设备目前不处在Android accessory模式,但是不清楚其是否支持该模式,需要进行确认(继续执行步骤3~4)。

步骤3:Android accessory模式确认和重新连接;

  • 通过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

步骤4:重新检查;

步骤3结束之后, Device 会重新和 Accessory 进行连接,这时 Accessory 回到步骤1进行检查,如果检查通过,则进入步骤5,如果 Device 不支持Android accessory模式,或者没有匹配的应用程序,则 Device 会返回信息告诉 Accessory ,这时 Accessory 就只能等待下一个手机设备的接入。

步骤5:开始通信.

从这之后, 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 } 

具体的代码可以看 这里 。

OpenSSL协议

在 Accessory 和 Device 建立连接,并且可以传输数据之后,它们就要开始建立OpenSSL的连接,对数据进行加解密了。这里主要分为了两个过程:握手过程和数据加解密过程。这里简单介绍下握手协议:

OpenSSL握手协议

握手协议的作用是身份的认证,该过程由 Client 端发起,这个协议的过程如下:

OpenSSL Authenticated Android Accessory Protocal

在这个过程中, 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协议的实现

首先由 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协议基本就实现完成了。

OpenSSL握手协议

握手协议由 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

简单来说步骤是这样的:

  • 首先,我们需要配置OpenSSL的数据结构:
  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 中;
  • 最后,我们通过写USB对应的文件描述符就可以将这段加密的数据发送出去了。

因此,整个加密的逻辑就可以是这样的:

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); } 

类似的,解密的分解步骤是这样的:

  • 通过读USB对应的文件描述符读取一段长度为 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 。

正文到此结束
Loading...