偉大なnobleを使っていると、BLEペリフェラルとペアリングするときに、BLEペリフェラルが要求するPasskeyを入力したい場合があります。
nobleは、ペアリング方式としてJust Works固定となっているので、ちょっと改造する必要があります。
noble/noble
https://github.com/noble/noble
以下、そのときの改造個所を備忘録として残しておきます。
Just Works : 現実装
Just Worksは、ペアリングの際によく聞く言葉ですが、実際には認証に使う鍵を生成する方法です。
ペアリングの際の認証鍵をTKと呼びます。そして、TKは、Passkeyから派生させて生成します。
Just Worksは、Passkeyを0 とした場合のことです。
nobleでは、Just Works固定、すなわち、Passkeyが0固定で実装されています。
TKで認証が完了すると、鍵を共有します。この鍵をSTKと呼びます。BLE通信が切れると、STKも破棄されます。
認証方法の決定
上述の通り、認証方法は、Just Worksだけではなく、Passkeyを入力する方法があります。
しかしながら、Passkeyを入力する方法として、いくつかの種類があります。なぜならば、PasskeyをBLEペリフェラルとBLEセントラルの双方が互いに知っている必要があるためです。
何を言っているかというと、基本的にPasskeyをBLE通信プロトコル上に流してしまうと、盗聴されてしまうため、操作している人自身で、BLEペリフェラルとBLEセントラル双方で共通のPasskeyを交換してもらう必要があるのです。
たとえば、画面がないBLEペリフェラルの場合には、BLEセントラル側でPasskeyを決めて表示し、BLEペリフェラルにPasskeyを入力することになります。また、もしBLEペリフェラルに入力手段がなければ、BLEペリフェラル側でPasskeyを決めて表示し、BLEセントラルにPasskeyを入力することになります。さらに、BLEペリフェラルに入力装置も出力装置も両方ない場合は、固定のPasskeyをBLEペリフェラルに埋め込んでおいて、そのPasskeyをBLEセントラルが何らかの形であらかじめ知っている必要があります。
ペアリングとボンディング
ペアリングと一口に言っても、実際は、ペアリングとボンディングの2つに分けられます。
ペアリングは、Passkeyの交換による認証です。認証が完了するとSTKで通信が暗号化されます。
ボンディングは、再度ペアリングするときに毎度Passkeyを交換するのも面倒なので、後々のために、鍵を事前に共有しておくことで、次回の認証時はPasskeyの入力が不要となります。この鍵をLTKと呼びます。
noble改造個所
> npm install noble
をした後、node_modules/noble/lib/hci-socket にある、「smp.js」を修正します。
//<修正前>
Smp.prototype.sendPairingRequest = function() {
this._preq = new Buffer([
SMP_PAIRING_REQUEST,
0x03, // IO capability: NoInputNoOutput
0x00, // OOB data: Authentication data not present
0x01, // Authentication requirement: Bonding - No MITM
0x10, // Max encryption key size
0x00, // Initiator key distribution: <none>
0x01 // Responder key distribution: EncKey
]);
this.write(this._preq);
};
//<修正後>
var g_io_cap = 0x02;
// 0x00, // IO capability: DisplayOnly
// 0x01, // IO capability: DisplayYesNo
// 0x02, // IO capability: KeyboardOnly
// 0x03, // IO capability: NoInputNoOutput
// 0x04, // IO capability: KeyboardDisplay
Smp.prototype.sendPairingRequest = function() {
this._preq = new Buffer([
SMP_PAIRING_REQUEST,
// 0x03, // IO capability: NoInputNoOutput
g_io_cap, // IO capability
0x00, // OOB data: Authentication data not present
// 0x01, // Authentication requirement: Bonding - No MITM
0x04, // Authentication requirement: No Bonding - with MITM
0x10, // Max encryption key size
0x00, // Initiator key distribution: <none>
// 0x01 // Responder key distribution: EncKey
0x00 // Responder key distribution: <none>
]);
this.write(this._preq);
};
補足します。
sendPairingRequest の部分です。
IO capabilityは、BLEセントラル側がもつ表示装置と入力装置の有無です。
修正前は、NoInputNoOutput なので、Just Works専用となっていました。
また、Authentication requirement のところで、No Bonding(ボンディング無し)にしています。Node.js実行後、LTKを保存して次に使いまわすわけではないためです。
それにともない、Responder key distribution のところも、EncKeyも不要なので、無しにしています。
//<修正前>
Smp.prototype.handlePairingResponse = function(data) {
this._pres = data;
this._tk = new Buffer('00000000000000000000000000000000', 'hex');
this._r = crypto.r();
this.write(Buffer.concat([
new Buffer([SMP_PAIRING_CONFIRM]),
crypto.c1(this._tk, this._r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra)
]));
};
//<修正後>
var readline = require('readline-sync');
var g_passkey = 0;
var g_prompt_passkey = true;
Smp.prototype.handlePairingResponse = function(data) {
this._pres = data;
var input;
if( g_prompt_passkey )
input = readline.question('Input Passkey : ');
else
input = g_passkey;
var passkey = new Array(16);
for( var i = 0 ; i < 3 ; i++ )
passkey[i] = (input >> (i * 8)) & 0xff;
this._tk = Buffer.from(passkey);
// this._tk = new Buffer('00000000000000000000000000000000', 'hex');
this._r = crypto.r();
this.write(Buffer.concat([
new Buffer([SMP_PAIRING_CONFIRM]),
crypto.c1(this._tk, this._r, this._pres, this._preq, this._iat, this._ia, this._rat, this._ra)
]));
};
handlePairingResponse の部分です。
g_prompt_passkey がtrueだったら、標準入力からPasskeyを入力してもらいます。
標準入力には今回readline-syncを使いました。
> npm install readline-sync
g_prompt_passkey がfalse だったら、g_passkey をPasskeyとみなして処理を進めます。
Just Worksの場合にはこれが0固定でした。
必要に応じてペアリングする
普段は認証無しでアクセスしているが、認証が必要なアクセスがあったときにペアリングするようにします。
認証が必要かどうかは、BLEペリフェラル側が教えてくれます。必要な時には、Security Requestの通知がBLEセントラルに送られてきますので、そのハンドリングを追加します。
※なくてもうまくいくような気がしますが。。。
以下のところで、
var SMP_PAIRING_REQUEST = 0x01;
var SMP_PAIRING_RESPONSE = 0x02;
var SMP_PAIRING_CONFIRM = 0x03;
var SMP_PAIRING_RANDOM = 0x04;
var SMP_PAIRING_FAILED = 0x05;
var SMP_ENCRYPT_INFO = 0x06;
var SMP_MASTER_IDENT = 0x07;
以下を追加。
var SMP_SECURITY_REQUEST = 0x0b;
以下のところを
} else if (SMP_MASTER_IDENT === code) {
this.handleMasterIdent(data);
}
以下のように末尾に追加。
} else if (SMP_MASTER_IDENT === code) {
this.handleMasterIdent(data);
} else if( SMP_SECURITY_REQUEST == code ){
this.handleSecurityRequest(data);
}
以下がその実装。
Smp.prototype.handleSecurityRequest = function(data){
this.sendPairingRequest();
};
動作
こんな感じで、g_prompt_passkey =true にすると、Passkeyを求められます。
handlePairingResponse
Input Passkey : ★ここでPasskeyを入力します。
handlePairingConfirm
handlePairingRandom
pairing ok
Passkeyの入力では、BLEペリフェラル側から指定されていた場合にはその値をここで入力し、逆の場合は、ここで入力した値をBLEペリフェラル側にも入力してください。
以上