转载

OpenSSL Authenticated Android Accessory Protocal

前段时间在研究Android Auto(关于什么是Auto请自行google),里面涉及到两个比较关键的数据传输和加密协议: Android Accessory ProtocolOpenSSL 。具体来说,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 ,手机的角色为 DeviceAOA 协议主要作用是关于 AccessoryDevice 在初始化连接时候的互相识别,以及之后数据的传输。

关于 AccessoryDevice 的概念可以看 这篇博文 ,这里就不详述了。

AccessoryDevice 建立连接之后,两边就可以进行数据的传输了,但是由于一些隐私问题,传输的数据需要进行加密,因此就引入了 OpenSSL 协议。在OpenSSL协议中连接两端的实体被分为了 ServerClient ,这两个角色有什么区别会在之后提到。在这里我们只需要知道在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.FirstActivityonCreate() 函数,从而使得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:开始通信.

从这之后, AccessoryDevice 将通过Android Accessory协议进行通信, Accessory 首先获得该USB连接中的一些配置元数据,包括接口类型(UsbInterface),端点信息(UsbEndpoint)等,从而获得对应的bulk endpoints,进行之后的通信过程。

在数据通信的过程中, Accessory 通过 libusb 库提供的 libusb_control_transferlibusb_bulk_transfer 接口进行数据的传输,其中, libusb_control_transfer 用于传输一些指令数据,而 libusb_bulk_transfer 用于传输一些比较大的数据,比如音频数据,图像数据等; 而 Device 则通过Android USBManager 提供的 openAccessory 接口获得一个文件描述符,然后通过其对应的 FileInputStreamFileOutputStream 进行数据的读写:

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协议

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

OpenSSL握手协议

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

OpenSSL Authenticated Android Accessory Protocal

在这个过程中, Client 首先会对 Server 提供的证书(Certificate)进行验证, Server 也会对 Client 提供的证书进行验证。同时它们会用 Server 的公钥(包含在 Server 的证书中)和存在 Server 端的私钥进行秘钥的协商,最后通过这个协商好的秘钥(master key)对数据进行加解密。

这里推荐 StackOverflow的一个帖子 ,里面的前两个回答对OpenSSL握手协议进行了一个很棒的解释。

代码分析

在进行了背景介绍之后,我们开始来分析下如何实现这整个过程。

源码可以在 这里 下载。

里面有两个目录: aoa-dev-ssl-serveraoa-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 这个应用会被唤醒,进入 UalTraActivityonCreate() 函数。在该类中,会进行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 这个类中会通过 UsbManageropenAccessory 接口得到一个文件描述符 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);   } 

到目前为止, AccessoryDevice 的连接已经建立,之后的数据传输就可以进行了。

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 BIOwrite BIO

这有什么用呢?其实要解释清楚这个就需要先对OpenSSL的机制有一个初步的了解。

在SSL的所有操作中(比如证书验证,加密,解密等),说到底,就是从某段内存中读取数据,对其进行相应的操作,然后将结果写在另外一段内存中。因此这里的两段内存就分别对应了 read BIOwrite 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...