- 投稿日:2020-05-17T19:55:02+09:00
AndroidStudioで辞書登録を共有する
はじめに
プロジェクト特有の言葉(日本語やビジネス用語)を使うときにAndroidStudioでは
丁寧に警告を出してくれるんですが、ミスリードなので警告を回避して、
チームで共有する方法を整理しました。ちなみに、開発プロジェクトでスペルチェック用辞書を共有する方法 が
すごく丁寧に書いてくれています。この記事は主に自身の備忘録にあたります...やったこと
1 typo対象外にしたいワードをAndroidStudioで辞書登録する
2 辞書ファイルはOSユーザー名で作成されるため、プロジェクトで共有できるファイル名に変更する
<dictionary name="os_user">
の name を変更するとファイル名自体も勝手に変更してくれるので便利です3 (必要であれば).gitignoreを編集してGitで共有できるようにする
プロジェクトで共有するファイルはgit登録、それ以外の辞書ファイルは無視する設定とかにしておくといいかもしれません。
あとは、GitにPushして終了です。まとめ
コーディングに集中しているときは後回しにしがちなのでチームで開発するときの環境整備として
早めにできているといいと思いました。
プロジェクトで共有できるファイル名にしておくことで、個別の設定も各自でできるので有用かなと..
(個別の設定は必要に応じてマージしてもらわないといけないですが..)
- 投稿日:2020-05-17T14:50:40+09:00
AndroidのTwilio VideoViewの形をCircleにする
はじめに
あるプロジェクトでTwilioのVideoライブラリを使ったのですがTwilioのVideoViewはデフォルトでは、四角形でカメラ映像がレンダリングされます。
これをよくある丸形にするのが意外と苦戦したのでメモしておきます。※ 今回の方法は下記のやり方を参考にしました。
https://github.com/twilio/video-quickstart-android/issues/291やり方
- 親のカスタムレイアウトを作成
CircleFrameLayout.ktpackage com.xxxxx import android.content.Context import android.graphics.Canvas import android.graphics.Path import android.graphics.Region import android.util.AttributeSet import android.widget.RelativeLayout open class CircleFrameLayout constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : RelativeLayout(context, attrs, defStyleAttr) { constructor(context: Context) : this(context, null, 0) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) private var circlePath = Path() override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { val radius = Math.min(w/2f, h/2f) circlePath.reset() circlePath.addCircle(w/2f, h/2f, radius, Path.Direction.CW) super.onSizeChanged(w, h, oldw, oldh) } override fun dispatchDraw(canvas: Canvas) { canvas.clipPath(circlePath, Region.Op.INTERSECT) super.dispatchDraw(canvas) } }
- twilioはVideoViewではなくVideoTextureViewを使います。
(VideoViewを使ってしまうと四角形の背景が残ったままになってしまうので注意)activity_main.xml<com.xxxxx.CircleFrameLayout android:layout_width="wrap_content" android:layout_height="wrap_content" app:layout_constraintTop_toBottomOf="@+id/menu_top" app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="@dimen/video_margin_small" android:layout_marginTop="@dimen/video_margin_small" > <com.twilio.video.VideoTextureView android:id="@+id/video_1" android:layout_width="@dimen/video_small" android:layout_height="@dimen/video_small" app:tviScaleType="fit" /> </com.xxxxx.CircleFrameLayout>これで最初に紹介したような丸形のViewに出来ます!
- 投稿日:2020-05-17T14:11:59+09:00
ExoPlayer Okhttp Extentionで ExoPlayerのネットワークをカスタマイズする
ExoPlayer Okhttp Extentionとは
ExoPlayerはAndroidで動画・音声を再生するフレームワークです。
ExoPlayerではたくさんの拡張ライブラリが用意されており、その中のひとつにOkhttp Extentionがあります。
Okhttp Extentionを使用すれば、OkHttpを使ってExoPlayerのネットワークをカスタマイズ出来ます。具体的には、ExoPlayerが受け取るレスポンスデータを書きかえたり、ステータスコードを書き換えたり... Okhttpで出来ること全てが可能です。
ただ、Httpタイムアウト時間・リクエストヘッダのカスタマイズ・ネットワークイベントなどはコアライブラリのみで対応可能です。導入
gradleのdependencyにokhttp extentionを追加します。extentionのバージョンはExoPlayer本体のバージョンと一致させます。
def ver_exoplayer = 'x.x.x' implementation "com.google.android.exoplayer:exoplayer-core:$ver_exoplayer" implementation "com.google.android.exoplayer:extension-okhttp:$ver_exoplayer"使い方
DefaultDataSource
,DefaultDataSourceFactory
の代わりにOkHttpDataSource
OkHttpDataSourceFactory
を用います。
例えば以下の例ではサーバー上の動画ファイルを再生・リクエスト時にログします。val client = OkHttpClient().newBuilder().addNetworkInterceptor(object : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { Log.d("LOG", "REQUEST INTERCEPTED") return chain.proceed(chain.request()) } }).build() val okHttpDataSourceFactory = OkHttpDataSourceFactory(client, "USER_AGENT") val mediaSource = ProgressiveMediaSource.Factory(okHttpDataSourceFactory) .createMediaSource("MP4_URL".toUri()) player.prepare(mediaSource) player.playWhenReady = trueOkHttpDataSource/OkHttpDataSourceFactoryにはカスタマイズしたOkhttp.Callを渡すことができるので、この部分で自由にカスタマイズできるという訳です。
リクエストヘッダ・タイムアウト時間をカスタムする場合はOkhttp Extentionは不要
リクエストヘッダ・タイムアウト時間・リダイレクトなどの設定なら、コアライブラリに含まれるDefaultHttpDataSourceFactoryで対応可能です。
Okhttp Extentionを使用せずに済みます。例
val factory = DefaultHttpDataSourceFactory( userAgent, connectTimeout, readTimeout, allowCrossProtocolRedirects/*HTTP<=>HTTPS間のリダイレクトを許容するかどうか*/ ) factory.defaultRequestProperties.set("headerName", "headerValue") val mediaSource = ProgressiveMediaSource.Factory(factory).createMediaSource("REMOTE_URL".toUri()) player.prepare(mediaSource) player.playWhenReady = trueネットワークイベントを作成する場合はOkhttp Extentionは不要
ダウンロードの進捗やリクエストの各パラメータを知りたいなら、コアライブラリのTransferListenerが便利です。
Okhttp Extentionを使用せずに済みます。例
// dataSpecからリクエストパラメータやストリーム中のポジションを取得できます // requestパラメータ: // dataSpec?.httpBody // dataSpec?.httpRequestHeaders // dataSpec?.httpMethod // ストリーム中のポジション: // dataSpec?.position // dataSpec?.absoluteStreamPosition val transferListener = object : TransferListener { override fun onTransferInitializing( source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean ) { } override fun onTransferStart( source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean ) { } override fun onTransferEnd( source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean ) { } override fun onBytesTransferred( source: DataSource?, dataSpec: DataSpec?, isNetwork: Boolean, bytesTransferred: Int ) { } } val factory = DefaultDataSourceFactory(getApplication(), userAgent, transferListener) val mediaSource = ProgressiveMediaSource.Factory(factory).createMediaSource("REMOTE_URL".toUri()) player.prepare(mediaSource) player.playWhenReady = trueさいごに
Okhttp Extentionは自由度が高く何でもできますが、基本的なことならコアライブラリのみで実装可能です。
レスポンスデータそのもの書きかえたい時など、使用する場面は割と限られてくるかと思います。
- 投稿日:2020-05-17T11:54:55+09:00
モバイルアプリに対する難読化/堅牢化
クラッキングの現状
近年様々の分野においてアプリのクラッキング被害が多く報告されています。これは、初心者にも簡単に使えるクラッキングツールが、インターネットで入手可能となり、攻撃者の数を急増したためです。
体表的になクラッキング被害は以下のような例を挙げられます。
- ゲームチート
- クラッキングにより不正ツールが作られ、課金回避や不正操作が横行します。
- 製造産業機器・ロボットのクローン
- 組込アプリのアルゴリズムを解析され、クローン製品を作られます。
- 金融・フィンテック 電子決済のなりますし
- アプリの改ざんにより、本人の認証の不正回避や、なりますしなどの原因となります。
- O2O 推薦者への報奨金のハッキング
- O2Oアプリの特性上、機密にすべきユーザの個人情報をアプリ内で扱うことがあります。ハッカーは、アプ リケーションの動作を理解し、簡単な操作でエミュレータから新しいユーザとして認識される方式を利用し、不当な利益を請求してきました。難読化/堅牢化とは
- 難読化
- プログラムにおいて、その内部的な動作の手続き内容・構造・データなどを人間が理解しにくい、あるいはそのようにるよう加工されたソースコードやマシンコードのこと- 堅牢化
- 改竄を検知してアプリを停止させる
- ロジック改竄検知、アプリ署名改竄検知、Root化/脱獄検知、デバッグモード検知、etc・・・サービス紹介(Msafe Technologyのseciron)
Msafe Technologyの堅牢化サービスはキャンペン実施しております。
ご希望のお客様はコーポレートサイト問い合わせフォームよりお問い合わせください。
Msafe Technology株式会社
- 投稿日:2020-05-17T00:08:21+09:00
AndroidとSesameとNFCで開けゴマ 最終版
動作結果
変更点
・Sesameの操作関係をクラス化
インスタンスで2台操作できそうに(未実験
10秒or1分で接続が切れることを利用して、それっぽく例外処理
・スマホとペアリングができていない場合は、createBondでペアリング
・マニフェストでActivityに対して”透明”テーマを適応
・Notificationでヘッドアップ通知で動作状況を把握
・BluetoothAdapterの初期化をdevelopersサイトのKotllin記載に修正manufactreDataMacDataStringの見つけ方
このプログラムではPrimarySesameMacDataの値について
BLEScannerのアドバタイズパケットの内容が見れる:RAWの部分の赤線部分。この資料の場合、[00-00-C1-02-02-02-02-02-02]になる。
成果物
MainActivity.kt
package jp.sakujira.opensesame //App import android.os.Bundle import androidx.appcompat.app.AppCompatActivity //NFC import android.nfc.NfcAdapter //Bluetooth import android.content.Context import android.bluetooth.BluetoothAdapter import android.bluetooth.BluetoothManager //Notification import android.os.Build import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat //SetUp-Config //NFCCardData val CardID : String = -鍵にしたいCardID- //Sesame UserData val UserID : String = -Sesameのメールアドレス- val Password : String = -公式APPから抜き出したパスワード- //Primary Sesame val PrimarySesameAddress : String = -SesameのBLEアドレスEx:FF:00:FF:00:FF:00- val PrimarySesameMacData : String = -Sesameのアドバタイズパケットの該当部分:0000C1020202020202- val PrimarySesameLockMinAngle : Int = 10 val PrimarySesameLockMaxAngle : Int = 270 //Secondary Sesame val SecondarySesameAddress : String = "" val SecondarySesameMacData : String = "" val SecondarySesameLockMinAngle : Int = 10 val SecondarySesameLockMaxAngle : Int = 270 class MainActivity : AppCompatActivity() { //SesameDevice private lateinit var PrimarySesame : SesameDevice private lateinit var SecondarySesame : SesameDevice private val HasSecondary : Boolean = (SecondarySesameAddress.length > 0) //BLE用のアダプター作成 private val BluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) { val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager bluetoothManager.adapter } //NFC-Certification private var isNFCLoad : Boolean = false @ExperimentalUnsignedTypes override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //SesameDeviceの初期化 PrimarySesame = SesameDevice(BluetoothAdapter.getRemoteDevice(PrimarySesameAddress), PrimarySesameMacData, PrimarySesameLockMinAngle, PrimarySesameLockMaxAngle, this) if(HasSecondary){ SecondarySesame = SesameDevice(BluetoothAdapter.getRemoteDevice(SecondarySesameAddress), SecondarySesameMacData, SecondarySesameLockMinAngle, SecondarySesameLockMaxAngle,this) } //このアプリを開く[起因]はNFC情報を読み取り //A1.[起因]は、AndroidManifest.xmlに規定した<intent-filter>に起因する if(!isNFCLoad && NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) { //カードのID情報を取得 var tagId: String = "" for (byte in intent.getByteArrayExtra(NfcAdapter.EXTRA_ID)) { tagId += String.format("%02X", byte) + ":" } //A2.読み込んだカードIDが一致すれば、BLE操作を開始 println("Btest:" + tagId) if (tagId == CardID + ":") { Notificate("ちょっとまってね!") isNFCLoad = true PrimarySesame.ConnectGatt() if (HasSecondary) { SecondarySesame.ConnectGatt() } } } finishAndRemoveTask() } //Sesameに対して操作を開始 @ExperimentalUnsignedTypes fun StartToggle(){ if(isNFCLoad){ isNFCLoad = false PrimarySesame.PrimaryStart() } } //SesameDeviceからの結果を受信 @ExperimentalUnsignedTypes fun SetCompleteState(mCommandState : Int, mLockState : Int){ var _LockState = mLockState if(HasSecondary && mCommandState == 4){ SecondarySesame.SecondayStart(mLockState) _LockState = 0 } when(_LockState){ 1->Notificate("いってらっしゃい!") 2->Notificate("おかえりなさい!") } } //通知処理 fun Notificate(mContent:String){ // Channelの取得と生成 val channelId = "KeyToggle" if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { //Android8 Oreo以上の場合 //通知チャンネルを作り val channel = NotificationChannel(channelId, "Beacon", NotificationManager.IMPORTANCE_HIGH).apply { this.description = "Sesame施錠・解錠通知" this.lockscreenVisibility = Notification.VISIBILITY_PUBLIC this.enableVibration(true) } // システムにチャンネルを登録する val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannel(channel) } val notification = NotificationCompat.Builder(this,channelId).apply { setContentTitle("Sesame施錠・解錠通知") setContentText(mContent) priority = NotificationCompat.PRIORITY_DEFAULT } with(NotificationManagerCompat.from(this)){ notify(1, notification.build()) } } }SesameDevice.kt
package jp.sakujira.opensesame import android.bluetooth.* import java.security.MessageDigest import java.util.* import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch class SesameDevice(val mDevice : BluetoothDevice, val mMadData :String, val mMinLockAngle : Int, val mMaxLockAngle : Int, val SesameService: MainActivity){ //Gattへの操作 private var mGatt: BluetoothGatt? = null //SesameがもつBLEのサービス検索詞 private val ServiceOperationUuid : UUID = UUID.fromString("00001523-1212-efde-1523-785feabcd123") private val CharacteristicCommandUuid : UUID = UUID.fromString("00001524-1212-efde-1523-785feabcd123") private val CharacteristicStatusUuid : UUID = UUID.fromString("00001526-1212-efde-1523-785feabcd123") private val CharacteristicAngleStatusUuid : UUID = UUID.fromString("00001525-1212-efde-1523-785feabcd123") //サービス検索結果:各サービスへの接続詞 var CharStatus: BluetoothGattCharacteristic? = null var CharCmd:BluetoothGattCharacteristic? = null var CharAngle:BluetoothGattCharacteristic? = null //状態管理用の変数 var CommandState : Int = 0 //次のコマンドを何を投げるかを管理 var SesameState : Int = 0 //Sesame側のカウンターを管理 var LockState : Int = 0 //開けるか・閉めるか・状態を聞くかを管理 var ConnectState : Int = 0 //Gatt接続状況を管理 //送信データ管理 var mSendData: ArrayList<ByteArray> = ArrayList() var mSendPointer : Int = 0 //Gattの接続を試みる @ExperimentalUnsignedTypes fun ConnectGatt(){ when(ConnectState){ 0->{//未接続状態 ConnectState = 1//接続テスト中へ変更 mGatt = mDevice.connectGatt(SesameService, false, mGattcallback) } 2->{//接続状態 ConnectedGatt() } } } //Gattの接続の成功 @ExperimentalUnsignedTypes fun ConnectedGatt(){ SesameService.StartToggle() } //Gattの接続終了処理 fun DisConnectGatt(){ CharStatus = null CharCmd = null CharAngle = null ConnectState = 0 SesameState = 0 LockState = 0 ConnectState = 0 } //ここから処理順番管理 @ExperimentalUnsignedTypes //For PrimaryDevice fun PrimaryStart(){ StateCommand() } //For SecondayDevice @ExperimentalUnsignedTypes fun SecondayStart(mLockState : Int){ LockState = mLockState ConnectState = 10 StateCommand() } //次処理実行 @ExperimentalUnsignedTypes fun NextState(){ CommandState += 1 //変更点:別スレッドで次のコマンドを実行 GlobalScope.launch{ StateCommand() } } //処理順番管理 @ExperimentalUnsignedTypes fun StateCommand(){ println("Btest:Start-" + CommandState) when (CommandState){ //For PrimaryDevice 0->GetSesameStatus() //B0.Sesameの状態取得:Sesameカウントを取得 1->SendStartData() //B1.LockState:0を送信しAngleを検知させる 2->GetSesameAngle() //B2.角度を取得し、施錠・解錠を取得 3->SendStartData() //B3.施錠・解錠コマンドを送信 4->ControlComplete() //B4.ループを終えてアプリを閉じる //For SecondaryDevice 10->GetSesameStatus() //B10.Sesameの状態取得:Sesameカウントを取得 11->SendStartData() //B11.施錠・解錠コマンドを送信 12->ControlComplete() //B12.ループを終えてアプリを閉じる } println("Btest:End-" + CommandState) } //ここまで処理順番管理 //ここから通信開始関数 //Sesameからステータスを取得-開始 fun GetSesameStatus(){ println("Btest:GetStatus") mGatt!!.readCharacteristic(CharStatus) } //Sesameに対してデータを送信-開始 @ExperimentalUnsignedTypes fun SendStartData(){ println("Btest:SendData:") //各パラメータから送信データを作成 val PayLoad = CreateSign(LockState,"", Password, mMadData, UserID, SesameState) //データをmtu:20byteごとに分割 mSendData = SplitByteArray(PayLoad) mSendPointer = 0 //送信データをセット println("Btest:SendData:Pointer"+ mSendPointer) println("Btest:SendData:" + ByteArrayToString(mSendData[mSendPointer])) CharCmd!!.setValue(mSendData[mSendPointer]) mGatt!!.writeCharacteristic(CharCmd) } //Sesameから角度情報取得-開始 fun GetSesameAngle(){ println("Btest:GetRange") mGatt!!.readCharacteristic(CharAngle) } //Sesameへの操作処理終了処理 @ExperimentalUnsignedTypes fun ControlComplete(){ println("Btest:EndConnect!") //BLEのペアリングを保存 if(mDevice.bondState == BluetoothDevice.BOND_NONE) { println("Btest:BOND-NONE") if(mDevice.createBond()){ println("Btest:BOND-Create") } } //Serviceに状態を報告 SesameService.SetCompleteState(CommandState,LockState) mGatt?.disconnect() } //ここまで通信開始関数 //ここから通信応答処理 @ExperimentalUnsignedTypes private val mGattcallback: BluetoothGattCallback = object : BluetoothGattCallback() { //B0.Sesameへの接続確認 override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { super.onConnectionStateChange(gatt, status, newState) //B0.接続確立を確認して if (newState == BluetoothProfile.STATE_CONNECTED) { //B0.サービスの検索を開始 println("Btest:GattSa-Search") gatt?.discoverServices() }else if (newState == BluetoothProfile.STATE_DISCONNECTED) { DisConnectGatt() } } //B0.サービスの検索完了:結果を分析 override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) { super.onServicesDiscovered(gatt, status) //B0.サービスの検索が成功を確認して if(status == BluetoothGatt.GATT_SUCCESS) { println("Btest:GattSa-OK!") //B0.サービスの一覧表を取得 val GattSList: List<BluetoothGattService> = gatt?.services as List<BluetoothGattService> //B0.サービスの一覧表を走査 for (GaService: BluetoothGattService in GattSList) { println("Btest:>" + GaService.uuid.toString()) //B0.事前にserviceOperationUuidと一致したものがあったら、 if (GaService.uuid.equals(ServiceOperationUuid)) { //B0.サービスが持っている機能・情報の一覧を取得 val GattCList: List<BluetoothGattCharacteristic> = GaService.characteristics //B0.機能・情報の一覧の走査 for (GaCharacteristic: BluetoothGattCharacteristic in GattCList) { println("Btest:>>" + GaCharacteristic.uuid.toString()) //B0.Sesameの状態情報取得の接続詞を取得 if (GaCharacteristic.uuid.equals(CharacteristicStatusUuid)){ CharStatus = GaCharacteristic } //B0.Sesameのコマンド情報取得の接続詞を取得 if (GaCharacteristic.uuid.equals(CharacteristicCommandUuid)){ CharCmd = GaCharacteristic } //B0.Sesameの角度情報取得の接続詞を取得 if (GaCharacteristic.uuid.equals(CharacteristicAngleStatusUuid)){ CharAngle = GaCharacteristic } } } } //B0.走査した結果が全てあるかどうかをチェックし、次の状態に移行 if(!(CharStatus == null || CharCmd == null || CharAngle == null)){ ConnectState = 2 ConnectedGatt() } } } //接続詞を使っての読み込み依頼した結果を分析 override fun onCharacteristicRead(gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) { super.onCharacteristicChanged(gatt, characteristic) println("Btest:" + characteristic!!.uuid.toString()) when (characteristic.uuid) { //B2.依頼内容が「AngleStatus」であれば CharacteristicAngleStatusUuid -> { val data: ByteArray = characteristic.value //データを取得 println("Btest:" + ByteArrayToString(data)) //データをFF:FF形式で表示 val angleRaw = ByteArrayToInt(data.slice(2..3).toByteArray()) //データを切り出して、Byte→Intへ val angle = Math.floor(angleRaw * 360 / 1024.0) //角度を計算 //B2.LockStateは、操作コマンドと兼ねているため 1:解錠(操作:施錠) 2:施錠(操作:解錠)と逆になる LockState = 1; if (angle < mMinLockAngle || angle > mMaxLockAngle) { LockState = 2; } println("Btest:Byte:" + ByteArrayToString(data) + ", Angle:" + angle + ", LockStatus:" + (LockState==2)) NextState()//次の状態に移行 } //B0.依頼内容が「Status」であれば CharacteristicStatusUuid -> { val data: ByteArray = characteristic.value //データの取得 val Sn: Int = ByteArrayToInt(data.slice(6..9).toByteArray()) + 1 //Sesameカウンターを取得 val Err: Int = ByteArrayToInt(data.slice(14..14).toByteArray()) + 1 //エラーコードを取得 //エラーコードリスト val errMsg = arrayOf( "Timeout", "Unsupported", "Success", "Operating", "ErrorDeviceMac", "ErrorUserId", "ErrorNumber", "ErrorSignature", "ErrorLevel", "ErrorPermission", "ErrorLength", "ErrorUnknownCmd", "ErrorBusy", "ErrorEncryption", "ErrorFormat", "ErrorBattery", "ErrorNotSend" ) println("Btest:Byte:" + ByteArrayToString(data) + ", Sn:" + Sn + ", Err:" + errMsg[Err]) SesameState = Sn //B0.Sesameカウンタを記録 NextState()//次の状態に移行 } } } //B1.B3.送信データの受領確認後、次パケットを送信 override fun onCharacteristicWrite( gatt: BluetoothGatt?, characteristic: BluetoothGattCharacteristic?, status: Int) { super.onCharacteristicWrite(gatt, characteristic, status) if(status == BluetoothGatt.GATT_SUCCESS){ //送信完了したので、ポインタを一つ進める mSendPointer += 1 //全部送信し終えたら if(mSendData.size <= mSendPointer){ println("Btest:SendData:Pointer-End") //送信データを綺麗にしてから mSendData = ArrayList() mSendPointer = 0 //次の処理へ NextState() }else { println("Btest:SendData:Pointer"+ mSendPointer) println("Btest:SendData:" + ByteArrayToString(mSendData[mSendPointer])) //ここも別スレッドで残りを送信! GlobalScope.launch { CharCmd?.setValue(mSendData[mSendPointer]) gatt?.writeCharacteristic(CharCmd) } } } } } //ここまで通信応答処理 //ここからデータ処理関数 //Sesameに対してデータを送信(BLEの20バイト制約のため、分割して送信) @ExperimentalUnsignedTypes fun SplitByteArray(pPayload : ByteArray): ArrayList<ByteArray>{ val Data : ArrayList<ByteArray> = ArrayList() //送信は20バイトごとに送信 //分割する際には、[先頭:01][中間:02][最後:04]と付ける必要がある //なので、一回の送信は19バイトごと for(i in 0..pPayload.size step 19){ val wSz = Math.min(pPayload.size-i,19) //送るデータが最後かどうか? var wCc : Int = 2 //初期値は[中間:02]とする var wBuf : ByteArray = ByteArray(wSz+1) //送信用データ場所を作成 //先頭・最後を判定 if(wSz < 19){ wCc = 4 }else if(i == 0){ wCc = 1 } //バイト列に分割詞をつける wBuf[0] = wCc.toByte() //送信データからバイト列を切り出し wBuf = ByteArrayCopy(wBuf, 1, pPayload, i,wSz) println("Btest:CutData:" + ByteArrayToString(wBuf)) Data.add(wBuf) } return Data } //認証用バイトデータを作成(普段Byteを使わないから、符号あり・なしに振り回された、、、) fun CreateSign(pCode:Int, pPayload: String, pPassword : String, pMacData : String, pUserid : String, pNonce: Int) : ByteArray{ //バイト配列の場所を作成 var wBufnonPw : ByteArray = ByteArray(59 - 32 + pPayload.toByteArray().size) //manufactreDataのデータをコピー val ByteMacData : ByteArray = HexStringToByteArray(pMacData) println("Btest:Mac:" + ByteArrayToString(ByteMacData.sliceArray(3..ByteMacData.size-1))) wBufnonPw = ByteArrayCopy(wBufnonPw, 0, ByteMacData.sliceArray(3..ByteMacData.size-1),0,6) //md5のデータをコピー val md5 = MessageDigest.getInstance("MD5").digest(pUserid.toByteArray()) println("Btest:md5:" + ByteArrayToString(md5)) wBufnonPw = ByteArrayCopy(wBufnonPw,6,md5,0,16) //Status(Nonce)をコピー println("Btest:Nonce:" + ByteArrayToString(InttoByteArrayUnsign(pNonce))) wBufnonPw = ByteArrayCopy(wBufnonPw,22,InttoByteArrayUnsign(pNonce),0,4) //Codeをコピー println("Btest:Code:" + ByteArrayToString(InttoByteArrayUnsign(pCode))) wBufnonPw = ByteArrayCopy(wBufnonPw,26,InttoByteArrayUnsign(pCode),0,1) //Payloadをコピー wBufnonPw = ByteArrayCopy(wBufnonPw, 27, pPayload.toByteArray(),0, pPayload.toByteArray().size) //パラメータの結果を確認 println("Btest:PrameterOK!:" + ByteArrayToString(wBufnonPw)) //「生成したパラメータ」を「パスワード」を使って暗号化 //「パスワード」を使用する暗号機を作成 val key = SecretKeySpec(HexStringToByteArray(pPassword), "HmacSHA256") val mac = Mac.getInstance("HmacSHA256") mac.init(key) val wBufKey = mac.doFinal(wBufnonPw) println("Btest:wBufkey:" + ByteArrayToString(wBufKey)) //全部を連結 var wBuf : ByteArray = ByteArray(pPayload.toByteArray().size + 59) wBuf = ByteArrayCopy(wBuf,0, wBufKey,0, 32) wBuf = ByteArrayCopy(wBuf,32, wBufnonPw,0, wBufnonPw.size) println("Btest:ALL:" + ByteArrayToString(wBuf)) return wBuf } //Intを符号なしのバイト列に変換 fun InttoByteArrayUnsign(num : Int): ByteArray{ val wHexString : String = num.toString(16).padStart(12,'0')//文字埋めを12桁にしているのはByteArrayCopyで参照値外がないようにするため val wResult = HexStringToByteArray(wHexString) return wResult.reversedArray() //1101→03F3となるが、送信データ上ではF303と逆にする必要がある } //HEX文字列をバイト配列にキャスト fun HexStringToByteArray(pHexString: String): ByteArray { val wBArray = ByteArray(pHexString.length / 2) for (index in 0 until wBArray.count()) { val pointer = index * 2 wBArray[index] = pHexString.substring(pointer, pointer + 2).toInt(16).toByte() } return wBArray } //Byte配列を指定位置にコピー fun ByteArrayCopy(pTarget: ByteArray, pPosition: Int, pCopy: ByteArray, pStart: Int, pLength : Int):ByteArray{ for(i in 0 until pLength){ pTarget[pPosition + i] = pCopy[pStart + i] } return pTarget } //Byte配列を文字列化 fun ByteArrayToString(pBytes: ByteArray): String{ var wRsult : String = "" for (b in pBytes) { wRsult += String.format("%02X", b) + ":" } return wRsult } //Byte配列を数値化 @ExperimentalUnsignedTypes fun ByteArrayToInt(pBytes: ByteArray): Int { var wResult: Int = 0 for (b in pBytes.reversed()){ wResult = wResult shl 8 wResult += b.toUByte().toInt() } return wResult } //ここまでデータ処理関数 }AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="jp.sakujira.opensesame"> <uses-permission android:name="android.permission.NFC" /> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name=".MainActivity" android:theme="@android:style/Theme.Translucent"><!--実行時にActivityが見えないように--> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> <intent-filter> <action android:name="android.nfc.action.NDEF_DISCOVERED" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="text/plain" /> </intent-filter> </activity> </application> </manifest>kotlinx.coroutinesを使うので、buid.gradle(app)に以下を追加
dependencies { ・・・・ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:X.X.X' }参考資料
アドバタイズパケットの分析とBeacon検討
aBeacon ~iBeacon を Android で受信する~
https://gaprot.jp/2014/03/27/abeacon/Android Beacon Library
https://altbeacon.github.io/android-beacon-library/index.html[Android][iBeacon] Android Beacon Library パラっと解説 その4 [距離観測]
https://dev.classmethod.jp/articles/android-beacon-library-introduction-4/Kotlinのサービス・レシーバーの書き方について
BLEMonitor
https://github.com/SergeiSafrigin/BLEMonitor/blob/master/src/kcg/ble/BleMonitorService.javaAndroidでServiceと通信する(Kotlinサンプル)
https://qiita.com/satken2/items/49dd76d848ceb208e937コジオニルク-プロセスが同一の場合の例
https://kojion.com/posts/649Notificationの書き方
OreoでNotificationを表示させる方法
https://qiita.com/naoi/items/367fc23e55292c50d459Androidで(ヘッドアップ)通知を表示するサンプル
https://qiita.com/gpsnmeajp/items/33392aac8d00b55bce75Activityを見せなくする方法
Theme.Translucentを継承せずにActivityの背景を透過にする
https://qiita.com/kgmyshin/items/a259f31b06ebab637044考察:このアプリのセキュリティについて
単純にこのアプリは
Sesame公式APP:ロック解除→アプリ起動→Sesame認識→ロック解除
を
このアプリ:ロック解除→NFCかざす→ロック解除
としているので、公式アプリと同じ位の危険性と考えています。
自宅や職場のwifiでロック解除するアプリを使っていたとしても、同じくらい、多少待ち時間が少ない分危険性は高くなるかなと考えています。仮に、NFCカード側を持っていかれても、Sesameに関する情報はなにもないので問題なし。
また、NFCカード、スマホ、スマホのロック解除が揃っているなら、公式アプリで解除するでしょうし、そもそも鍵を開けるためにはBLEの通信範囲にいないといけないので、「こんな事をしている」ことを知っている人に絞られるかなっと思ったり。まぁ、Sesameを着けているいる時点である程度は家の鍵にバックドアの可能性が増えていると思っているので、まぁいいかなっと自分自身は考えています。
(まぁ、アプリの情報抜かれたらと考えるのであれば、そもそもこのアプリ自体が抜いているので、、、、)編集後記
この話についてはココで区切り。
あとやるとしたら、施錠解錠の動画の撮影くらいかな~
最終版の書き方にして、かざす→施錠・解錠の時間が短くなったので、バックグランドサービスでSesameをScanLEで定期的に探そうかと思っていましたが、しなくていいかなっと感じています。
(その調査の結果、アドバタイズパケットからMacDataの位置が理解できたのですが、、、、楽しかったけど、疲れた、、、。