2013年10月16日 星期三

Bluez的藍牙配對工具程式

藍牙配對時常會困擾使用者,特別是一般的使用者。其實藍牙組織(Bluetooth SIG)也知道這個情況,所以配對的方式也有改善,以前的方式(legacy pairing, 藍牙2.0+EDR及之前)使用PIN Code,配對雙方的設備都要輸入相同的PIN Code,通常是4碼數字(規格書定義可到16位數數字)。兩個設備檢查所持有PIN Code是否相同,相同才會配對成功。但是一般使用者可能會按錯鍵,所以常常都要試很多次才會成功,對一般使用者來說經驗是不太好的。近來新的藍牙設備都已改用新的配對方式(secure simple pairing),兩個設備會先溝通一個passkey,通常是6位數的數字,然後兩個設備都會顯示這個數字,接著只要求使用者確認雙方數字是否相同即可。這樣使用者原本必須按10個鍵(4個PIN Code X 2 + 2次確認),現在只要按兩個鍵(確認鍵)就可以了。不但大量降低按錯的機會,也加快了配對的速度。

不過對於Linux的使用者來說,許多藍牙設備因為市場考量的關係,並不見得原生會支援Linux作業系統。例如,Logitech K810的鍵盤要和ubuntu配對時,就要自己費一點功夫[1] 。這是因為設備廠商只會支援熱門的作業系統,這是很自然的事情;只是Linux系統近年來其實都有整合BlueZ這套藍牙管理程式,所以只要有依循藍牙規格書的設備,Linux就可以透過BlueZ來與之相連運作。反之,設備廠商也不需太費心專門為Linux系統做驅動,其設備就可以連上Linux系統。當然,專門做個驅動還是比較好的,會有助於使用經驗的提升。

BlueZ原生的配對工具,在Ubuntu中就是[1]提到的bluez-simple-agent這支程式,其實這是一支Python寫成的script,所以我們可以用more, cat, vi, gedit等指令直接看到它的內容。沒錯,它是命令列的指令用法,對大多數的使用者來說都是不易使用的。所以,其實在大部份的Linux套件中都會使用gnome-bluetooth[2]之類的圖形界面工具。這樣一來,使用者只要點選按就可以了,自然接受度會好很多。不過,這些工具都只是界面改善(當然有些有加入其他功能),其實主要的配對工作,還是丟給BlueZ去做的。就像我在[3]那篇文章中提到一樣,其實只要將bluez-simple-agent這支程式稍加改寫,就可以自己做個圖形界面工具來做配對的工作。我已經把這樣的程式寫好並提供下載,相關連結有興趣者可以自行參考該文。

所以重點在bluez-simple-agent這支程式了,也不完全是!先談談這支程式吧,正如我在[3]中所提及的,其實在BlueZ的原始碼中的test目錄下就附上了這支Python程式(叫做simple-agent)。大家都會問為什麼要放在test這個目錄。感覺好像不是正式的!我也這麼覺得,也許BlueZ真的有這個意思,他們只是要展示如何寫出配對程式。而且很簡單(simple)就可以寫出,但是功能還是很OK的。不過,還是希望有人可以寫個更好的工具,像gnome-bluetooth那樣。就像之前討論的,simple-agent這支程式很簡單,它沒做什麼事,主要處理命令列的參數,以決定要叫用BlueZ的那個方法。若使用者有輸入要配對設備的MAC ADDRESS則叫用adapter.CreatePairedDevice(),若使用者沒輸入MAC ADDRESS則叫用adapter.RegisterAgent()。如下程式碼片斷。而BlueZ在進行配對的過程中,會有需要使用者輸入的時候(可能是輸入PIN Code,也可能是要求確認),這時會叫用simple-agent來做這些工作(就是Agent類別負責的事)。
 if len(args) > 1:
  if len(args) > 2:
   device = adapter.FindDevice(args[1])
   adapter.RemoveDevice(device)

  agent.set_exit_on_release(False)
  adapter.CreatePairedDevice(args[1], path, capability,
     reply_handler=create_device_reply,
     error_handler=create_device_error)
 else:
  adapter.RegisterAgent(path, capability)
  print "Agent registered"
說真的一開始,我只看懂CreatePairedDevice()部份,至於RegisterAgent()部份則一直執行失敗,老是說Agent已經存在,而我真的不知道為什麼如此,以及要如何去解。直到在看到Dicky[4]提及有一支passkey-agent其命令列參數中有帶一個0000的字串,而這個0000其實是配對的PIN Code時,才想到這兩個程式都是做配對的工作,其間有什麼關連呢?於是想到去看passkey-agent的程式碼。只是不知道程式碼在什麼地方,後來找到TI的Wiki中有一篇文章[5]提到編譯BlueZ的步驟,其中提到cp test/agent ...。這就很明白了,果然同樣地在BlueZ原始程式的test目錄下有一支agent.c的程式,這是使用DBus C API寫的C程式,雖然印出來有11頁之多,但稍加研讀就會發覺,其實這支程式和simple-agent根本是雙胞胎,只是一個為Python版,一個為C版。內容和邏輯基本上是一樣的。其主要程式邏輯片斷如下,很明顯地,除了語法不同外,和simple-agent是相同的。
 if (device) {
  if (create_paired_device(conn, adapter_path, agent_path,
      capabilities, device) < 0) {
   dbus_connection_unref(conn);
   exit(1);
  }
 } else {
  if (register_agent(conn, adapter_path, agent_path,
       capabilities) < 0) {
   dbus_connection_unref(conn);
   exit(1);
  }
 }
不過,在使用的時機有一點不同,所以程式還是有一些小差異。請看一下Fig.1,因為藍牙配對本來就是兩個設備之間在配。所以會有由誰發起配對的問題,如圖中所示,可能由A發起也可能由B發起。藍牙規格書應該沒有規範實際的產品必須要能主動發起配對,否則一堆藍牙滑鼠鍵盤都無法主動發起配對,就無法宣稱符合藍牙規範了。
Fig.1
規格書倒是為配對定義了四個結合模式(association models):

  1. Numeric Comparison: 例如手機和電腦配對,兩個設備都有顯示6位數字的能力,而且有可以輸入「是」或「否」的能力。
  2. Just Works: 例如手機和藍牙耳機,至少有一個設備沒有顯示6位數字的能力,而且也沒有輸入6位數字的能力。
  3. Out of Band: 例如使用NFC
  4. Passkey Entry: 例如電腦和鍵盤,有一個設備沒有顯示6位數字的能力,但是有輸入的能力。
以結合模式來看,有一些設備的能力比較差,例如耳機或鍵盤滑鼠,自然不可能要求它做太多事情來強化安全性。甚至連主動發起配對的能力也無法做到,通常都是被動被要求做配對。

而agent.c這支程式的設計,就是在這種情境下使用的,以結合模式來說,應該是Just Works這個模式。以Fig.1來說明,就是B的設備(例如滑鼠),當A設備(例如電腦)主動發起配對時,因為B設備能力差,無法顯示也無法輸入6位數數字,所以agent.c會在收到passkey要求時,直接回應預設的passkey(在agent的命令列參數設定之),或是收到確認要求時,直接回應確認。或者要求Pin Code時,如下程式片斷之request_pincode_message()函數,回應預設的key值。那麼在被動配對時,BlueZ如何提供彈性,讓我們可以設定預計的passkey呢?就是讓我們叫用DBus上的adapter.RegisterAgent()方法,透過這個方式來告訴BlueZ,要問passkey時,去叫用那個DBus上的方法(當然這個部份是我們要寫的,不過agent.c已經為我們寫好了)。我們只要在這些特定的方法中提供回應就好了。
static DBusHandlerResult request_pincode_message(DBusConnection *conn,
      DBusMessage *msg, void *data)
{
 DBusMessage *reply;
 const char *path;

 if (!passkey_value)
  return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;

 if (!dbus_message_get_args(msg, NULL, DBUS_TYPE_OBJECT_PATH, &path,
       DBUS_TYPE_INVALID)) {
  fprintf(stderr, "Invalid arguments for RequestPinCode method");
  return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
 }

 if (do_reject) {
  reply = dbus_message_new_error(msg, "org.bluez.Error.Rejected", "");
  goto send;
 }

 reply = dbus_message_new_method_return(msg);
 if (!reply) {
  fprintf(stderr, "Can't create reply message\n");
  return DBUS_HANDLER_RESULT_NEED_MEMORY;
 }

 printf("Pincode request for device %s\n", path);

 if (passkey_delay) {
  printf("Waiting for %d seconds\n", passkey_delay);
  sleep(passkey_delay);
 }

 dbus_message_append_args(reply, DBUS_TYPE_STRING, &passkey_value,
       DBUS_TYPE_INVALID);

send:
 dbus_connection_send(conn, reply, NULL);

 dbus_connection_flush(conn);

 dbus_message_unref(reply);

 return DBUS_HANDLER_RESULT_HANDLED;
}
所以原本的agent程式其實不太合適用來主動發起配對,特别是新的Secure Simple Pairing模式中,因為它會在背後幫使用者把確認動作做完。而使用者不會看到任何提示,也不用做任何事就配對好了。

反之simple-agent這支程式的預設比較偏向Numeric Comparison結合模式,因為它在配對過程中,程式預設是可以顯示和輸入的,所以它會要求輸入PIN Code或者顯示passkey後要求確認。如下程式片斷所示,都會詢問使用者,要求輸入確認。而主動發起配對的方式在BlueZ,就是叫用它在DBus中的adapter.CreatePairedDevice()方法。簡單説,這兩支程式(agent.c, simple-agent)是一様的,他們在同一支程式内,試著讓使用者可以主動發起配對,或者可以被動做配對。但有時主動配對和被動配對的情境不同,程式仍然要做不同的修改才合適。
 @dbus.service.method("org.bluez.Agent",
     in_signature="o", out_signature="s")
 def RequestPinCode(self, device):
  print "RequestPinCode (%s)" % (device)
  return raw_input("Enter PIN Code: ")

 @dbus.service.method("org.bluez.Agent",
     in_signature="o", out_signature="u")
 def RequestPasskey(self, device):
  print "RequestPasskey (%s)" % (device)
  passkey = raw_input("Enter passkey: ")
  return dbus.UInt32(passkey)

 @dbus.service.method("org.bluez.Agent",
     in_signature="ou", out_signature="")
 def DisplayPasskey(self, device, passkey):
  print "DisplayPasskey (%s, %d)" % (device, passkey)

 @dbus.service.method("org.bluez.Agent",
     in_signature="ou", out_signature="")
 def RequestConfirmation(self, device, passkey):
  print "RequestConfirmation (%s, %d)" % (device, passkey)
  confirm = raw_input("Confirm passkey (yes/no): ")
  if (confirm == "yes"):
   return
  raise Rejected("Passkey doesn't match")

但是在實際使用情況中,例如Ubuntu的環境,大都有所修改。正如前文提到的,大都會提供像gnome-bluetooth之類的圖形工具,基本上不太會去使用simple-agent了。同樣地,gnome-bluetooth之類的工具也都已使用adapter.RegisterAgent()方法向BlueZ註冊了被動配對時要叫用的方法。這樣當別台電腦主動發起配對我們的電腦時,才會在螢幕上顯示合適的視窗要求passkey或要求確認。也因此,當我使用simple-agent執行到adapter.RegisterAgent()時,都會出現Agent已經存在的錯誤訊息了。



參考來源:
[1] carl, Ubuntu 12.04 persistent bluetooth pairing, 2012/11/30
[2] gnome-bluetooth手冊, GNOME LIBRARY
[3] 小白和小叮叮, python gtk+ glade and dbus work together, 2013/9/11
[4] Dicky Chiang, BT porting總整理, 2009/8/8
[5] WL127x Bluetooth API Information, Texas Instruments Wiki
張貼留言