IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    OpenSSL Authenticated Android Accessory Protocal

    Liu Yutao发表于 2015-12-05 11:09:00
    love 0

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

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

    github上的源码

    关于Android Auto

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

    Androi Auto Mechanism

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

    关于两个协议

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

    AA and SSL Protocols

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

    AA and SSL Protocols 2

    好了,到现在为止,我们就完全和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进行数据的读写:

    1
    2
    3
    4
    5
    6
    
    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 Handshake Protocol

    在这个过程中,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文件中:

    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
    
      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文件中):

    1
    2
    3
    4
    
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <usb-accessory manufacturer="SJTU" model="SJTU IPADS" />
    </resources>
    

    而在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
    
    <?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的连接:

    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
    
      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的读写来进行的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
      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类的这个接口:

    1
    
    mConnection.getOutputStream().write(buffer, 0, bufferLength);
    

    收数据类似:

    1
    
    mConnection.getInputStream().read(buffer, bufferUsed, buffer.length - bufferUsed);
    

    AOA协议基本就实现完成了。

    OpenSSL握手协议

    握手协议由aoa-acc-ssl-client发起,在文件aoa-acc-ssl-client/jni/hu_aap.c中:

    1
    2
    3
    4
    5
    
    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:

    1
    2
    
    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的数据结构:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      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);
    

    我中间跳过了很多步,不过那些都不重要(可以去看源码),这里最重要的就是这句话:

    1
    
    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对应的文件描述符就可以将这段加密的数据发送出去了。

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

    1
    2
    3
    4
    5
    6
    7
    8
    
    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中;
    • 最后,我们就可以对这段明文数据进行处理了。

    其相应的逻辑就变成这样了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    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接口。

    整个逻辑大概是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    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。



沪ICP备19023445号-2号
友情链接