This commit is contained in:
Michele Balistreri 2019-11-01 16:09:08 +03:00
parent ae7c937d9e
commit 4cf6fba24f
No known key found for this signature in database
GPG Key ID: E9567DA33A4F791A
20 changed files with 211 additions and 145 deletions

View File

@ -1,24 +0,0 @@
package im.status.keycard.connect
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("im.status.keycard.connect", appContext.packageName)
}
}

View File

@ -16,7 +16,8 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".ui.InitActivity"></activity>
<activity android:name=".ui.ChangePINActivity"></activity>
<activity android:name=".ui.InitActivity" />
<activity android:name=".ui.PairingActivity" />
<activity android:name=".ui.PINActivity" />
<activity

View File

@ -0,0 +1,39 @@
package im.status.keycard.connect
import android.annotation.SuppressLint
import android.app.Activity
import android.nfc.NfcAdapter
import im.status.keycard.android.NFCCardManager
import im.status.keycard.connect.card.*
import im.status.keycard.connect.data.PINCache
import im.status.keycard.connect.data.PairingManager
object Registry {
lateinit var pinCache: PINCache
private set
lateinit var pairingManager: PairingManager
private set
@SuppressLint("StaticFieldLeak")
lateinit var scriptExecutor: CardScriptExecutor
private set
lateinit var cardManager: NFCCardManager
private set
lateinit var nfcAdapter: NfcAdapter
private set
fun init(activity: Activity) {
pairingManager = PairingManager(activity)
pinCache = PINCache()
nfcAdapter = NfcAdapter.getDefaultAdapter(activity)
scriptExecutor = CardScriptExecutor(activity)
cardManager = NFCCardManager()
cardManager.setCardListener(scriptExecutor)
cardManager.start()
}
}

View File

@ -2,11 +2,11 @@ package im.status.keycard.connect.card
import android.content.Intent
enum class CommandResult {
OK, CANCEL, RETRY, UX_ONGOING
}
interface CardCommand {
fun run(context: CardScriptExecutor.Context): CommandResult
enum class Result {
OK, CANCEL, RETRY, UX_ONGOING
}
fun run(context: CardScriptExecutor.ScriptContext): Result
fun onDataReceived(data: Intent?) {}
}

View File

@ -5,26 +5,21 @@ import android.content.Intent
import im.status.keycard.applet.KeycardCommandSet
import im.status.keycard.io.CardChannel
import im.status.keycard.io.CardListener
import java.util.*
class CardScriptExecutor(activity: Activity) : CardListener {
class Context(val mainActivity: Activity) {
var cardChannel: CardChannel? = null
var cmdSet: KeycardCommandSet? = null
}
class CardScriptExecutor(private val activity: Activity) : CardListener {
class ScriptContext(val activity: Activity, val cmdSet: KeycardCommandSet)
enum class State {
READY, UX_ONGOING, RUNNING
}
private var state = State.READY
private var executionContext = Context(activity)
private var script: List<CardCommand>? = null
private var defaultScript: List<CardCommand>? = null
private var waitingCmd: CardCommand? = null
override fun onConnected(channel: CardChannel) {
executionContext.cardChannel = channel
executionContext.cmdSet = KeycardCommandSet(executionContext.cardChannel)
val executionContext = ScriptContext(activity, KeycardCommandSet(channel))
if (state == State.READY) {
state = State.RUNNING
@ -32,22 +27,20 @@ class CardScriptExecutor(activity: Activity) : CardListener {
return
}
//TODO: replace with default script
val runningScript = script ?: LinkedList()
val runningScript = script ?: defaultScript ?: return
for (cmd in runningScript) {
when (cmd.run(executionContext)) {
CommandResult.OK -> {}
CommandResult.CANCEL -> { state = State.READY; return }
CommandResult.UX_ONGOING -> { waitingCmd = cmd; return }
CommandResult.RETRY -> { return }
CardCommand.Result.OK -> {}
CardCommand.Result.CANCEL -> { state = State.READY; return }
CardCommand.Result.UX_ONGOING -> { waitingCmd = cmd; return }
CardCommand.Result.RETRY -> { return }
}
}
}
override fun onDisconnected() {
executionContext.cardChannel = null
executionContext.cmdSet = null
}
fun onUserInteractionReturned(resultCode: Int, data: Intent?) {

View File

@ -2,6 +2,7 @@ package im.status.keycard.connect.card
import android.app.Activity
import android.content.Intent
import im.status.keycard.connect.Registry
import im.status.keycard.connect.data.*
import im.status.keycard.connect.ui.InitActivity
import im.status.keycard.io.APDUException
@ -12,37 +13,35 @@ class InitCommand : CardCommand {
private var initPUK: String? = null
private var initPairing: String? = null
private fun promptInit(mainActivity: Activity): CommandResult {
val intent = Intent(mainActivity, InitActivity::class.java)
mainActivity.startActivityForResult(intent, REQ_INTERACTIVE_SCRIPT)
return CommandResult.UX_ONGOING
private fun promptInit(activity: Activity): CardCommand.Result {
val intent = Intent(activity, InitActivity::class.java)
activity.startActivityForResult(intent, REQ_INTERACTIVE_SCRIPT)
return CardCommand.Result.UX_ONGOING
}
override fun run(context: CardScriptExecutor.Context): CommandResult {
val cmdSet = context.cmdSet ?: return CommandResult.CANCEL
if (cmdSet.applicationInfo.isInitializedCard) {
return CommandResult.OK
override fun run(context: CardScriptExecutor.ScriptContext): CardCommand.Result {
if (context.cmdSet.applicationInfo.isInitializedCard) {
return CardCommand.Result.OK
}
if (initPIN != null && initPUK != null && initPairing != null) {
try {
cmdSet.init(initPIN, initPUK, initPairing).checkOK()
cmdSet.select().checkOK()
cmdSet.autoPair(initPairing)
PairingManager.putPairing(cmdSet.applicationInfo.instanceUID, cmdSet.pairing)
return CommandResult.OK
context.cmdSet.init(initPIN, initPUK, initPairing).checkOK()
context.cmdSet.select().checkOK()
context.cmdSet.autoPair(initPairing)
Registry.pairingManager.putPairing(context.cmdSet.applicationInfo.instanceUID, context.cmdSet.pairing)
return CardCommand.Result.OK
} catch (e: IOException) {
return CommandResult.RETRY
return CardCommand.Result.RETRY
} catch (e: APDUException) {
return CommandResult.CANCEL
return CardCommand.Result.CANCEL
} finally {
initPIN = null
initPUK = null
initPairing = null
}
} else {
return promptInit(context.mainActivity)
return promptInit(context.activity)
}
}

View File

@ -3,8 +3,8 @@ package im.status.keycard.connect.card
import android.app.Activity
import android.content.Intent
import im.status.keycard.applet.KeycardCommandSet
import im.status.keycard.connect.Registry
import im.status.keycard.connect.data.PAIRING_ACTIVITY_PASSWORD
import im.status.keycard.connect.data.PairingManager
import im.status.keycard.connect.data.REQ_INTERACTIVE_SCRIPT
import im.status.keycard.connect.ui.PairingActivity
import java.io.IOException
@ -12,25 +12,25 @@ import java.io.IOException
class OpenSecureChannelCommand : CardCommand {
private var pairingPassword: String? = null
private fun openSecureChannel(cmdSet: KeycardCommandSet): CommandResult {
private fun openSecureChannel(cmdSet: KeycardCommandSet): CardCommand.Result {
try {
cmdSet.autoOpenSecureChannel()
} catch (e: IOException) {
//TODO: must distinguish real IOException from card exception (to fix in SDK)
return CommandResult.CANCEL
return CardCommand.Result.CANCEL
}
return CommandResult.OK
return CardCommand.Result.OK
}
private fun pair(mainActivity: Activity, cmdSet: KeycardCommandSet): CommandResult {
private fun pair(activity: Activity, cmdSet: KeycardCommandSet): CardCommand.Result {
if (pairingPassword != null) {
try {
//TODO: must distinguish real IOException from card exception (to fix in SDK)
cmdSet.autoPair(pairingPassword)
PairingManager.putPairing(cmdSet.applicationInfo.instanceUID, cmdSet.pairing)
Registry.pairingManager.putPairing(cmdSet.applicationInfo.instanceUID, cmdSet.pairing)
cmdSet.autoOpenSecureChannel()
return CommandResult.OK
return CardCommand.Result.OK
} catch(e: IOException) {
e.printStackTrace()
} finally {
@ -38,31 +38,29 @@ class OpenSecureChannelCommand : CardCommand {
}
}
return promptPairingPassword(mainActivity)
return promptPairingPassword(activity)
}
private fun promptPairingPassword(mainActivity: Activity): CommandResult {
private fun promptPairingPassword(mainActivity: Activity): CardCommand.Result {
val intent = Intent(mainActivity, PairingActivity::class.java)
mainActivity.startActivityForResult(intent, REQ_INTERACTIVE_SCRIPT)
return CommandResult.UX_ONGOING
return CardCommand.Result.UX_ONGOING
}
override fun run(context: CardScriptExecutor.Context): CommandResult {
val cmdSet = context.cmdSet ?: return CommandResult.CANCEL
val pairing = PairingManager.getPairing(cmdSet.applicationInfo.instanceUID)
override fun run(context: CardScriptExecutor.ScriptContext): CardCommand.Result {
val pairing = Registry.pairingManager.getPairing(context.cmdSet.applicationInfo.instanceUID)
if (pairing != null) {
cmdSet.pairing = pairing
if (openSecureChannel(cmdSet) == CommandResult.CANCEL) {
PairingManager.removePairing(cmdSet.applicationInfo.instanceUID)
context.cmdSet.pairing = pairing
if (openSecureChannel(context.cmdSet) == CardCommand.Result.CANCEL) {
Registry.pairingManager.removePairing(context.cmdSet.applicationInfo.instanceUID)
} else {
return CommandResult.OK
return CardCommand.Result.OK
}
}
return pair(context.mainActivity, cmdSet)
return pair(context.activity, context.cmdSet)
}
override fun onDataReceived(data: Intent?) {

View File

@ -4,16 +4,16 @@ import java.io.IOException
import java.lang.Exception
class SelectCommand : CardCommand {
override fun run(context: CardScriptExecutor.Context): CommandResult {
override fun run(context: CardScriptExecutor.ScriptContext): CardCommand.Result {
//TODO: handle not-installed-applet/not-a-keycard
try {
context.cmdSet!!.select().checkOK()
context.cmdSet.select().checkOK()
} catch(e: IOException) {
return CommandResult.RETRY
return CardCommand.Result.RETRY
} catch (e: Exception) {
return CommandResult.CANCEL
return CardCommand.Result.CANCEL
}
return CommandResult.OK
return CardCommand.Result.OK
}
}

View File

@ -2,8 +2,8 @@ package im.status.keycard.connect.card
import android.app.Activity
import android.content.Intent
import im.status.keycard.connect.Registry
import im.status.keycard.connect.ui.PINActivity
import im.status.keycard.connect.data.PINCache
import im.status.keycard.connect.data.PIN_ACTIVITY_ATTEMPTS
import im.status.keycard.connect.data.PIN_ACTIVITY_CARD_UID
import im.status.keycard.connect.data.REQ_INTERACTIVE_SCRIPT
@ -14,39 +14,37 @@ import java.io.IOException
class VerifyPINCommand : CardCommand {
private var retries = -1
private fun promptPIN(mainActivity: Activity, instanceUID: ByteArray): CommandResult {
val intent = Intent(mainActivity, PINActivity::class.java).apply {
private fun promptPIN(activity: Activity, instanceUID: ByteArray): CardCommand.Result {
val intent = Intent(activity, PINActivity::class.java).apply {
putExtra(PIN_ACTIVITY_ATTEMPTS, retries)
putExtra(PIN_ACTIVITY_CARD_UID, instanceUID)
}
mainActivity.startActivityForResult(intent, REQ_INTERACTIVE_SCRIPT)
activity.startActivityForResult(intent, REQ_INTERACTIVE_SCRIPT)
return CommandResult.UX_ONGOING
return CardCommand.Result.UX_ONGOING
}
override fun run(context: CardScriptExecutor.Context): CommandResult {
override fun run(context: CardScriptExecutor.ScriptContext): CardCommand.Result {
//TODO: handle retries == 0 with UNBLOCK PIN
val cmdSet = context.cmdSet ?: return CommandResult.CANCEL
val pin = PINCache.getPIN(cmdSet.applicationInfo.instanceUID)
val pin = Registry.pinCache.getPIN(context.cmdSet.applicationInfo.instanceUID)
if (pin != null) {
try {
cmdSet.verifyPIN(pin).checkAuthOK()
context.cmdSet.verifyPIN(pin).checkAuthOK()
retries = -1
return CommandResult.OK
return CardCommand.Result.OK
} catch (e: WrongPINException) {
PINCache.removePIN(cmdSet.applicationInfo.instanceUID)
Registry.pinCache.removePIN(context.cmdSet.applicationInfo.instanceUID)
retries = e.retryAttempts
} catch(e: IOException) {
return CommandResult.RETRY
return CardCommand.Result.RETRY
} catch(e: APDUException) {
return CommandResult.CANCEL
return CardCommand.Result.CANCEL
}
}
return promptPIN(context.mainActivity, cmdSet.applicationInfo.instanceUID)
return promptPIN(context.activity, context.cmdSet.applicationInfo.instanceUID)
}
}

View File

@ -10,3 +10,5 @@ const val INIT_ACTIVITY_PUK = "initPUK"
const val INIT_ACTIVITY_PAIRING = "initPairing"
const val REQ_INTERACTIVE_SCRIPT = 0x01
const val CACHE_VALIDITY = 15 * 60 * 1000

View File

@ -5,19 +5,17 @@ import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import kotlin.collections.HashMap
object PINCache {
const val CACHE_VALIDITY = 15 * 60 * 1000;
class PINCache {
//TODO: don't use Strings, the memory should be cleared before release. For this the entire
// chain from the EditText to the SDK should be controlled and never generate a String object.
// This will require extensions to the SDK.
private val pins: MutableMap<ByteArrayKey, String> = HashMap()
private val timestamps: MutableMap<Long, ByteArrayKey> = HashMap()
private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1);
private val scheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(1)
init {
scheduler.scheduleAtFixedRate(this::cleanCache, 1, 1, TimeUnit.MINUTES);
scheduler.scheduleAtFixedRate(this::cleanCache, 1, 1, TimeUnit.MINUTES)
}
private fun cleanCache() {

View File

@ -9,14 +9,14 @@ import im.status.keycard.applet.Pairing
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
object PairingManager {
class PairingManager(context: Context) {
private lateinit var sharedPreferences: SharedPreferences
private fun id(instanceUID: ByteArray) : String {
return Base64.encodeToString(instanceUID, NO_PADDING or NO_WRAP)
}
fun init(context: Context) {
init {
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
sharedPreferences = EncryptedSharedPreferences.create("pairings", masterKeyAlias, context, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)
}

View File

@ -0,0 +1,24 @@
package im.status.keycard.connect.ui
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.View
import im.status.keycard.connect.R
class ChangePINActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//TODO: pin validation and confirmation
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_change_pin)
}
fun ok(@Suppress("UNUSED_PARAMETER") view: View) {
finish()
}
fun cancel(@Suppress("UNUSED_PARAMETER") view: View) {
finish()
}
}

View File

@ -6,45 +6,34 @@ import androidx.appcompat.app.AppCompatActivity
import im.status.keycard.android.NFCCardManager
import android.content.Intent
import im.status.keycard.connect.R
import im.status.keycard.connect.Registry
import im.status.keycard.connect.card.*
import im.status.keycard.connect.data.PairingManager
import im.status.keycard.connect.data.REQ_INTERACTIVE_SCRIPT
class MainActivity : AppCompatActivity() {
private lateinit var cardManager: NFCCardManager
private lateinit var nfcAdapter: NfcAdapter
private lateinit var executor: CardScriptExecutor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
PairingManager.init(this)
nfcAdapter = NfcAdapter.getDefaultAdapter(this)
executor = CardScriptExecutor(this)
executor.setScript(listOf(SelectCommand(), InitCommand(), OpenSecureChannelCommand(), VerifyPINCommand()))
cardManager = NFCCardManager()
cardManager.setCardListener(executor)
cardManager.start()
Registry.init(this)
Registry.scriptExecutor.setScript(listOf(SelectCommand(), InitCommand(), OpenSecureChannelCommand(), VerifyPINCommand()))
}
override fun onResume() {
super.onResume()
nfcAdapter.enableReaderMode(this, this.cardManager,NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null)
Registry.nfcAdapter.enableReaderMode(this, Registry.cardManager,NfcAdapter.FLAG_READER_NFC_A or NfcAdapter.FLAG_READER_SKIP_NDEF_CHECK, null)
}
override fun onPause() {
super.onPause()
nfcAdapter.disableReaderMode(this)
Registry.nfcAdapter.disableReaderMode(this)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQ_INTERACTIVE_SCRIPT) {
executor.onUserInteractionReturned(resultCode, data)
Registry.scriptExecutor.onUserInteractionReturned(resultCode, data)
}
}
}

View File

@ -7,6 +7,7 @@ import android.view.View
import android.widget.EditText
import android.widget.TextView
import im.status.keycard.connect.R
import im.status.keycard.connect.Registry
import im.status.keycard.connect.data.PINCache
import im.status.keycard.connect.data.PIN_ACTIVITY_ATTEMPTS
import im.status.keycard.connect.data.PIN_ACTIVITY_CARD_UID
@ -34,7 +35,7 @@ class PINActivity : AppCompatActivity() {
fun ok(@Suppress("UNUSED_PARAMETER") view: View) {
val pinText = findViewById<EditText>(R.id.pinText)
PINCache.putPIN(cardUID, pinText.text.toString())
Registry.pinCache.putPIN(cardUID, pinText.text.toString())
setResult(Activity.RESULT_OK)
finish()
}

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.ChangePINActivity">
<TextView
android:id="@+id/newPinPrompt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="52dp"
android:text="@string/change_pin_prompt"
android:textAppearance="@style/TextAppearance.AppCompat.Display2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/okButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="88dp"
android:onClick="ok"
android:text="@android:string/ok"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/cancelButton" />
<Button
android:id="@+id/cancelButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="68dp"
android:layout_marginTop="76dp"
android:onClick="cancel"
android:text="@android:string/cancel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/newPINText" />
<EditText
android:id="@+id/newPINText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="88dp"
android:ems="10"
android:inputType="numberPassword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/newPinPrompt"
tools:text="123456" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -2,4 +2,5 @@
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="fab_margin">16dp</dimen>
</resources>

View File

@ -7,4 +7,6 @@
<string name="pin_label">PIN</string>
<string name="puk_label">PUK</string>
<string name="pairing_label">Pairing password</string>
<string name="title_activity_change_pin">ChangePINActivity</string>
<string name="change_pin_prompt">New PIN</string>
</resources>

View File

@ -8,4 +8,13 @@
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>

View File

@ -1,17 +0,0 @@
package im.status.keycard.connect
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}