From c4f0e9ef41f76ba134111e5bb5c49746c87dd5db Mon Sep 17 00:00:00 2001 From: Javier Varona Date: Thu, 2 Aug 2018 22:59:48 -0400 Subject: [PATCH] - Added functionality to PocketRequestActivity to verify the yubikey code (still in progress...) --- .../activities/IntroActivity.java | 4 +- .../activities/PatternRequestActivity.java | 7 +- .../activities/PinRequestActivity.java | 7 +- .../activities/PocketRequestActivity.java | 58 ++-- .../application/CrystalSecurityMonitor.java | 18 ++ .../fragments/SecuritySettingsFragment.java | 4 +- .../crystalwallet/util/yubikey/TOTP.java | 175 ++++++++++++ .../crystalwallet/util/yubikey/YkOathApi.kt | 268 ++++++++++++++++++ 8 files changed, 518 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/cy/agorise/crystalwallet/util/yubikey/TOTP.java create mode 100644 app/src/main/java/cy/agorise/crystalwallet/util/yubikey/YkOathApi.kt diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/IntroActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/IntroActivity.java index 9d4eaf1..c557bde 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/activities/IntroActivity.java +++ b/app/src/main/java/cy/agorise/crystalwallet/activities/IntroActivity.java @@ -94,8 +94,8 @@ public class IntroActivity extends AppCompatActivity { //startActivity(intent); } else { //Intent intent = new Intent(this, CreateSeedActivity.class); - Intent intent = new Intent(this, BoardActivity.class); - //Intent intent = new Intent(this, PocketRequestActivity.class); + //Intent intent = new Intent(this, BoardActivity.class); + Intent intent = new Intent(this, PocketRequestActivity.class); startActivity(intent); } diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/PatternRequestActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/PatternRequestActivity.java index 283578e..0ecb4f8 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/activities/PatternRequestActivity.java +++ b/app/src/main/java/cy/agorise/crystalwallet/activities/PatternRequestActivity.java @@ -18,6 +18,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnTextChanged; import cy.agorise.crystalwallet.R; +import cy.agorise.crystalwallet.application.CrystalSecurityMonitor; import cy.agorise.crystalwallet.models.GeneralSetting; import cy.agorise.crystalwallet.util.PasswordManager; import cy.agorise.crystalwallet.viewmodels.GeneralSettingListViewModel; @@ -69,7 +70,11 @@ public class PatternRequestActivity extends AppCompatActivity { @Override public void onComplete(List pattern) { if (PasswordManager.checkPassword(patternEncrypted,patternToString(pattern))){ - thisActivity.finish(); + if (CrystalSecurityMonitor.getInstance(null).is2ndFactorSet()) { + CrystalSecurityMonitor.getInstance(null).call2ndFactor(thisActivity); + } else { + thisActivity.finish(); + } } else { patternLockView.clearPattern(); patternLockView.requestFocus(); diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/PinRequestActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/PinRequestActivity.java index a95210a..a4f4a5a 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/activities/PinRequestActivity.java +++ b/app/src/main/java/cy/agorise/crystalwallet/activities/PinRequestActivity.java @@ -18,6 +18,7 @@ import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.OnTextChanged; import cy.agorise.crystalwallet.R; +import cy.agorise.crystalwallet.application.CrystalSecurityMonitor; import cy.agorise.crystalwallet.models.AccountSeed; import cy.agorise.crystalwallet.models.GeneralSetting; import cy.agorise.crystalwallet.util.PasswordManager; @@ -66,7 +67,11 @@ public class PinRequestActivity extends AppCompatActivity { callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED) void afterPasswordChanged(Editable editable) { if (PasswordManager.checkPassword(passwordEncrypted, etPassword.getText().toString())) { - this.finish(); + if (CrystalSecurityMonitor.getInstance(null).is2ndFactorSet()) { + CrystalSecurityMonitor.getInstance(null).call2ndFactor(this); + } else { + this.finish(); + } } } } diff --git a/app/src/main/java/cy/agorise/crystalwallet/activities/PocketRequestActivity.java b/app/src/main/java/cy/agorise/crystalwallet/activities/PocketRequestActivity.java index 6a78b70..9eb3919 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/activities/PocketRequestActivity.java +++ b/app/src/main/java/cy/agorise/crystalwallet/activities/PocketRequestActivity.java @@ -26,15 +26,19 @@ import com.andrognito.patternlockview.listener.PatternLockViewListener; import org.apache.commons.codec.binary.Base32; import java.io.IOException; +import java.math.BigInteger; +import java.nio.ByteBuffer; import java.util.List; import butterknife.BindView; import butterknife.ButterKnife; import cy.agorise.crystalwallet.R; +import cy.agorise.crystalwallet.application.CrystalSecurityMonitor; import cy.agorise.crystalwallet.models.GeneralSetting; import cy.agorise.crystalwallet.util.PasswordManager; import cy.agorise.crystalwallet.util.yubikey.Algorithm; import cy.agorise.crystalwallet.util.yubikey.OathType; +import cy.agorise.crystalwallet.util.yubikey.TOTP; import cy.agorise.crystalwallet.util.yubikey.YkOathApi; import cy.agorise.crystalwallet.viewmodels.GeneralSettingListViewModel; @@ -60,6 +64,26 @@ public class PocketRequestActivity extends AppCompatActivity { mNfcAdapter = NfcAdapter.getDefaultAdapter(this); this.configureForegroundDispatch(); + + + String clave = "12345678901234567890"; + + char[] ch = clave.toCharArray(); + + StringBuilder builder = new StringBuilder(); + for (char c : ch) { + String hexCode=String.format("%H", c); + builder.append(hexCode); + } + String claveHex = String.format("%040x", new BigInteger(1, clave.getBytes())); + + + long time = 1111111109/30; + String steps = Long.toHexString(time).toUpperCase(); + while(steps.length() < 16) steps = "0" + steps; + Log.i("TEST", TOTP.generateTOTP( + claveHex, + steps, "6", "HmacSHA1")); } public void configureForegroundDispatch(){ @@ -100,29 +124,29 @@ public class PocketRequestActivity extends AppCompatActivity { Tag tagFromIntent = intent.getParcelableExtra(NfcAdapter.EXTRA_TAG); IsoDep tagIsoDep = IsoDep.get(tagFromIntent); Log.i("Tag from nfc","New Intent"); + String yubikeySecret = CrystalSecurityMonitor.getInstance(null).get2ndFactorValue(); + try { + tagIsoDep.connect(); + YkOathApi ykOathApi = new YkOathApi(tagIsoDep); - String encodedSecret = "hola"; - Base32 decoder = new Base32(); + /*long unixTime = System.currentTimeMillis() / 1000L; + byte[] timeStep = ByteBuffer.allocate(8).putLong(unixTime / 30L).array(); + byte[] response; + response = ykOathApi.calculate("cy.agorise.crystalwallet",timeStep,true); + response[0]. + private fun formatTruncated(data: ByteArray): String { + return with(ByteBuffer.wrap(data)) { + val digits = get().toInt() + int.toString().takeLast(digits).padStart(digits, '0') + } + }*/ - if ((encodedSecret != null) && (!encodedSecret.equals("")) && decoder.isInAlphabet(encodedSecret)) { - byte[] secret = decoder.decode(encodedSecret); - YkOathApi ykOathApi = new YkOathApi(); - tagIsoDep.connect(); - tagIsoDep.setTimeout(15000); - - //byte[] keyBytes = {0x68,0x6f,0x6c,0x61}; - ykOathApi.putCode(tagIsoDep,"prueba",secret, OathType.TOTP, Algorithm.SHA256,(byte)6,0,false); - tagIsoDep.close(); - - Toast.makeText(this, "Credential saved!", Toast.LENGTH_LONG).show(); - } else { - Toast.makeText(this, "Invalid password for credential", Toast.LENGTH_LONG).show(); - } + tagIsoDep.close(); + //ykOathApi. } catch (IOException e) { e.printStackTrace(); } - Toast.makeText(this, "Tag from nfc: "+tagFromIntent, Toast.LENGTH_LONG).show(); } } diff --git a/app/src/main/java/cy/agorise/crystalwallet/application/CrystalSecurityMonitor.java b/app/src/main/java/cy/agorise/crystalwallet/application/CrystalSecurityMonitor.java index 63668f2..e027107 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/application/CrystalSecurityMonitor.java +++ b/app/src/main/java/cy/agorise/crystalwallet/application/CrystalSecurityMonitor.java @@ -15,6 +15,7 @@ import java.util.List; import cy.agorise.crystalwallet.activities.PatternRequestActivity; import cy.agorise.crystalwallet.activities.PinRequestActivity; +import cy.agorise.crystalwallet.activities.PocketRequestActivity; import cy.agorise.crystalwallet.models.GeneralSetting; import cy.agorise.crystalwallet.notifiers.CrystalWalletNotifier; import cy.agorise.crystalwallet.viewmodels.GeneralSettingListViewModel; @@ -111,6 +112,10 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall return ""; } + public boolean is2ndFactorSet(){ + return !this.yubikeyOathTotpPasswordEncrypted.equals(""); + } + public void setYubikeyOathTotpSecurity(String name, String password){ this.yubikeyOathTotpPasswordEncrypted = password; GeneralSetting yubikeyOathTotpSetting = new GeneralSetting(); @@ -155,6 +160,19 @@ public class CrystalSecurityMonitor implements Application.ActivityLifecycleCall } } + public void call2ndFactor(Activity activity){ + Intent intent = null; + if ((this.yubikeyOathTotpPasswordEncrypted != null) && (!this.yubikeyOathTotpPasswordEncrypted.equals(""))) { + intent = new Intent(activity, PocketRequestActivity.class); + //intent.putExtra("ACTIVITY_TYPE", "PASSWORD_REQUEST"); + activity.startActivity(intent); + } + } + + public String get2ndFactorValue(){ + return this.yubikeyOathTotpPasswordEncrypted; + } + @Override public void onActivityCreated(Activity activity, Bundle bundle) { // diff --git a/app/src/main/java/cy/agorise/crystalwallet/fragments/SecuritySettingsFragment.java b/app/src/main/java/cy/agorise/crystalwallet/fragments/SecuritySettingsFragment.java index 18623ef..86b201b 100644 --- a/app/src/main/java/cy/agorise/crystalwallet/fragments/SecuritySettingsFragment.java +++ b/app/src/main/java/cy/agorise/crystalwallet/fragments/SecuritySettingsFragment.java @@ -201,12 +201,12 @@ public class SecuritySettingsFragment extends Fragment { if ((encodedSecret != null) && (!encodedSecret.equals("")) && decoder.isInAlphabet(encodedSecret)) { byte[] secret = decoder.decode(encodedSecret); - YkOathApi ykOathApi = new YkOathApi(); tagIsoDep.connect(); tagIsoDep.setTimeout(15000); + YkOathApi ykOathApi = new YkOathApi(tagIsoDep); try { - ykOathApi.putCode(tagIsoDep, serviceName, secret, OathType.TOTP, Algorithm.SHA256, (byte) 6, 0, false); + ykOathApi.putCode(serviceName, secret, OathType.TOTP, Algorithm.SHA256, (byte) 6, 0, false); CrystalSecurityMonitor.getInstance(null).setYubikeyOathTotpSecurity(CrystalSecurityMonitor.getServiceName(),encodedSecret); } catch(IOException e) { Toast.makeText(this.getContext(), "There's no space for new credentials!", Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/TOTP.java b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/TOTP.java new file mode 100644 index 0000000..5b92530 --- /dev/null +++ b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/TOTP.java @@ -0,0 +1,175 @@ +package cy.agorise.crystalwallet.util.yubikey; + +import java.lang.reflect.UndeclaredThrowableException; +import java.security.GeneralSecurityException; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.math.BigInteger; +import java.util.TimeZone; + +/* +* Source of this class: https://tools.ietf.org/html/rfc6238 +* */ + +public class TOTP { + + private TOTP() {} + + /** + * This method uses the JCE to provide the crypto algorithm. + * HMAC computes a Hashed Message Authentication Code with the + * crypto hash algorithm as a parameter. + * + * @param crypto: the crypto algorithm (HmacSHA1, HmacSHA256, + * HmacSHA512) + * @param keyBytes: the bytes to use for the HMAC key + * @param text: the message or text to be authenticated + */ + + private static byte[] hmac_sha(String crypto, byte[] keyBytes, + byte[] text){ + try { + Mac hmac; + hmac = Mac.getInstance(crypto); + SecretKeySpec macKey = + new SecretKeySpec(keyBytes, "RAW"); + hmac.init(macKey); + return hmac.doFinal(text); + } catch (GeneralSecurityException gse) { + throw new UndeclaredThrowableException(gse); + } + } + + + /** + * This method converts a HEX string to Byte[] + * + * @param hex: the HEX string + * + * @return: a byte array + */ + + private static byte[] hexStr2Bytes(String hex){ + // Adding one byte to get the right conversion + // Values starting with "0" can be converted + byte[] bArray = new BigInteger("10" + hex,16).toByteArray(); + + // Copy all the REAL bytes, not the "first" + byte[] ret = new byte[bArray.length - 1]; + for (int i = 0; i < ret.length; i++) + ret[i] = bArray[i+1]; + return ret; + } + + private static final int[] DIGITS_POWER + // 0 1 2 3 4 5 6 7 8 + = {1,10,100,1000,10000,100000,1000000,10000000,100000000 }; + + /** + * This method generates a TOTP value for the given + * set of parameters. + * + * @param key: the shared secret, HEX encoded + * @param time: a value that reflects a time + * @param returnDigits: number of digits to return + * + * @return: a numeric String in base 10 that includes + * {@link truncationDigits} digits + */ + + public static String generateTOTP(String key, + String time, + String returnDigits){ + return generateTOTP(key, time, returnDigits, "HmacSHA1"); + } + + + /** + * This method generates a TOTP value for the given + * set of parameters. + * + * @param key: the shared secret, HEX encoded + * @param time: a value that reflects a time + * @param returnDigits: number of digits to return + * + * @return: a numeric String in base 10 that includes + * {@link truncationDigits} digits + */ + + public static String generateTOTP256(String key, + String time, + String returnDigits){ + return generateTOTP(key, time, returnDigits, "HmacSHA256"); + } + + /** + * This method generates a TOTP value for the given + * set of parameters. + * + * @param key: the shared secret, HEX encoded + * @param time: a value that reflects a time + * @param returnDigits: number of digits to return + * + * @return: a numeric String in base 10 that includes + * {@link truncationDigits} digits + */ + + public static String generateTOTP512(String key, + String time, + String returnDigits){ + return generateTOTP(key, time, returnDigits, "HmacSHA512"); + } + + + /** + * This method generates a TOTP value for the given + * set of parameters. + * + * @param key: the shared secret, HEX encoded + * @param time: a value that reflects a time + * @param returnDigits: number of digits to return + * @param crypto: the crypto function to use + * + * @return: a numeric String in base 10 that includes + * {@link truncationDigits} digits + */ + + public static String generateTOTP(String key, + String time, + String returnDigits, + String crypto){ + int codeDigits = Integer.decode(returnDigits).intValue(); + String result = null; + + // Using the counter + // First 8 bytes are for the movingFactor + // Compliant with base RFC 4226 (HOTP) + while (time.length() < 16 ) + time = "0" + time; + + // Get the HEX in a Byte[] + byte[] msg = hexStr2Bytes(time); + byte[] k = hexStr2Bytes(key); + byte[] hash = hmac_sha(crypto, k, msg); + + // put selected bytes into result int + int offset = hash[hash.length - 1] & 0xf; + + int binary = + ((hash[offset] & 0x7f) << 24) | + ((hash[offset + 1] & 0xff) << 16) | + ((hash[offset + 2] & 0xff) << 8) | + (hash[offset + 3] & 0xff); + + int otp = binary % DIGITS_POWER[codeDigits]; + + result = Integer.toString(otp); + while (result.length() < codeDigits) { + result = "0" + result; + } + return result; + } +} diff --git a/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/YkOathApi.kt b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/YkOathApi.kt new file mode 100644 index 0000000..4919240 --- /dev/null +++ b/app/src/main/java/cy/agorise/crystalwallet/util/yubikey/YkOathApi.kt @@ -0,0 +1,268 @@ +package cy.agorise.crystalwallet.util.yubikey + +import android.nfc.tech.IsoDep +import android.util.Base64 +//import com.yubico.yubioath.exc.AppletMissingException +//import com.yubico.yubioath.exc.AppletSelectException +//import com.yubico.yubioath.exc.StorageFullException +//import com.yubico.yubioath.transport.ApduError +//import com.yubico.yubioath.transport.Backend +import java.io.Closeable +import java.io.IOException +import java.nio.ByteBuffer +import java.security.MessageDigest + + +class YkOathApi @Throws(IOException::class/*, AppletSelectException::class*/) +constructor(private var tag: IsoDep) : Closeable { + val deviceSalt: ByteArray + val deviceInfo: DeviceInfo + private var challenge = byteArrayOf() + + init { + //try { + val resp = send(0xa4.toByte(), p1 = 0x04) { put(AID) } + val version = Version.parse(resp.parseTlv(VERSION_TAG)) + deviceSalt = resp.parseTlv(NAME_TAG) + val id = getDeviceId(deviceSalt) + + if (resp.hasRemaining()) { + challenge = resp.parseTlv(CHALLENGE_TAG) + } + + deviceInfo = DeviceInfo(id, false, version, challenge.isNotEmpty()) + /*} catch (e: ApduError) { + throw AppletMissingException() + }*/ + } + + /* fun isLocked(): Boolean = challenge.isNotEmpty() + + fun reselect() { + send(0xa4.toByte(), p1 = 0x04) { put(AID) }.apply { + parseTlv(VERSION_TAG) + parseTlv(NAME_TAG) + challenge = if (hasRemaining()) { + parseTlv(CHALLENGE_TAG) + } else byteArrayOf() + } + } + + fun unlock(signer: ChallengeSigner): Boolean { + val response = signer.sign(challenge) + val myChallenge = ByteArray(8) + val random = SecureRandom() + random.nextBytes(myChallenge) + val myResponse = signer.sign(myChallenge) + + return try { + val resp = send(VALIDATE_INS) { + tlv(RESPONSE_TAG, response) + tlv(CHALLENGE_TAG, myChallenge) + } + Arrays.equals(myResponse, resp.parseTlv(RESPONSE_TAG)) + } catch (e: ApduError) { + false + } + } + + fun setLockCode(secret: ByteArray) { + val challenge = ByteArray(8) + val random = SecureRandom() + random.nextBytes(challenge) + val response = Mac.getInstance("HmacSHA1").apply { + init(SecretKeySpec(secret, algorithm)) + }.doFinal(challenge) + + send(SET_CODE_INS) { + tlv(KEY_TAG, byteArrayOf(OathType.TOTP.byteVal or Algorithm.SHA1.byteVal) + secret) + tlv(CHALLENGE_TAG, challenge) + tlv(RESPONSE_TAG, response) + } + deviceInfo.hasPassword = true + } + + fun unsetLockCode() { + send(SET_CODE_INS) { tlv(KEY_TAG) } + deviceInfo.hasPassword = false + }*/ + + fun listCredentials(): List { + val resp = send(LIST_INS) + + return mutableListOf().apply { + while (resp.hasRemaining()) { + val nameBytes = resp.parseTlv(NAME_LIST_TAG) + add(String(nameBytes, 1, nameBytes.size - 1)) //First byte is algorithm + } + } + } + + @Throws(IOException::class) + fun putCode(name: String, key: ByteArray, type: OathType, algorithm: Algorithm, digits: Byte, imf: Int, touch: Boolean) { + //send(tag, 0xa4.toByte(), p1 = 0x04) { put(AID) } + //if (touch && deviceInfo.version.major < 4) { + // throw IllegalArgumentException("Require touch requires YubiKey 4") + //} + //try { + send(PUT_INS) { + tlv(NAME_TAG, name.toByteArray()) + tlv(KEY_TAG, byteArrayOf(type.byteVal or algorithm.byteVal, digits) + algorithm.prepareKey(key)) + if (touch) put(PROPERTY_TAG).put(REQUIRE_TOUCH_PROP) + if (type == OathType.HOTP && imf > 0) put(IMF_TAG).put(4).putInt(imf) + } + //} catch (e: ApduError) { + // throw if (e.status == APDU_FILE_FULL) StorageFullException("No more room for OATH credentials!") else e + //} + } + + /*fun deleteCode(name: String) { + send(DELETE_INS) { tlv(NAME_TAG, name.toByteArray()) } + }*/ + + fun calculate(name: String, challenge: ByteArray, truncate: Boolean = true): ByteArray { + val resp = send(CALCULATE_INS, p2 = if (truncate) 1 else 0) { + tlv(NAME_TAG, name.toByteArray()) + tlv(CHALLENGE_TAG, challenge) + } + return resp.parseTlv(resp.slice().get()) + } + + /*fun calculateAll(challenge: ByteArray): List { + val resp = send(CALCULATE_ALL_INS, p2 = 1) { + tlv(CHALLENGE_TAG, challenge) + } + + return mutableListOf().apply { + while (resp.hasRemaining()) { + val name = String(resp.parseTlv(NAME_TAG)) + val respType = resp.slice().get() // Peek + val hashBytes = resp.parseTlv(respType) + val oathType = if (respType == NO_RESPONSE_TAG) OathType.HOTP else OathType.TOTP + val touch = respType == TOUCH_TAG + + add(ResponseData(name, oathType, touch, hashBytes)) + } + } + }*/ + @Throws(IOException::class) + private fun send(ins: Byte, p1: Byte = 0, p2: Byte = 0, data: ByteBuffer.() -> Unit = {}): ByteBuffer { + val apdu = ByteBuffer.allocate(256).put(0).put(ins).put(p1).put(p2).put(0).apply(data).let { + it.put(4, (it.position() - 5).toByte()).array().copyOfRange(0, it.position()) + } + + return ByteBuffer.allocate(4096).apply { + var resp = splitApduResponse(tag.transceive(apdu)) + while (resp.status != APDU_OK) { + if ((resp.status shr 8).toByte() == APDU_DATA_REMAINING_SW1) { + put(resp.data) + resp = splitApduResponse(tag.transceive(byteArrayOf(0, SEND_REMAINING_INS, 0, 0))) + } else { + throw IOException(""+resp.status) + } + } + put(resp.data).limit(position()).rewind() + } + } + + override fun close() { + /*backend.close() + backend = object : Backend { + override val persistent: Boolean = false + override fun sendApdu(apdu: ByteArray): ByteArray = throw IOException("SENDING APDU ON CLOSED BACKEND!") + override fun close() = throw IOException("Backend already closed!") + }*/ + } + + data class Version(val major: Int, val minor: Int, val micro: Int) { + companion object { + fun parse(data: ByteArray): Version = Version(data[0].toInt(), data[1].toInt(), data[2].toInt()) + } + + override fun toString(): String = "%d.%d.%d".format(major, minor, micro) + + fun compare(major: Int, minor: Int, micro: Int): Int { + return if (major > this.major || (major == this.major && (minor > this.minor || minor == this.minor && micro > this.micro))) { + -1 + } else if (major == this.major && minor == this.minor && micro == this.micro) { + 0 + } else { + 1 + } + } + + fun compare(version: Version): Int = compare(version.major, version.minor, version.micro) + } + + class DeviceInfo(val id: String, val persistent: Boolean, val version: Version, initialHasPassword: Boolean) { + var hasPassword = initialHasPassword + internal set + } + + class ResponseData(val key: String, val oathType: OathType, val touch: Boolean, val data: ByteArray) + + private infix fun Byte.or(b: Byte): Byte = (toInt() or b.toInt()).toByte() + + companion object { + const private val APDU_OK = 0x9000 + const private val APDU_FILE_FULL = 0x6a84 + const private val APDU_DATA_REMAINING_SW1 = 0x61.toByte() + + const private val NAME_TAG: Byte = 0x71 + const private val NAME_LIST_TAG: Byte = 0x72 + const private val KEY_TAG: Byte = 0x73 + const private val CHALLENGE_TAG: Byte = 0x74 + const private val RESPONSE_TAG: Byte = 0x75 + const private val T_RESPONSE_TAG: Byte = 0x76 + const private val NO_RESPONSE_TAG: Byte = 0x77 + const private val PROPERTY_TAG: Byte = 0x78 + const private val VERSION_TAG: Byte = 0x79 + const private val IMF_TAG: Byte = 0x7a + const private val TOUCH_TAG: Byte = 0x7c + + const private val ALWAYS_INCREASING_PROP: Byte = 0x01 + const private val REQUIRE_TOUCH_PROP: Byte = 0x02 + + const private val PUT_INS: Byte = 0x01 + const private val DELETE_INS: Byte = 0x02 + const private val SET_CODE_INS: Byte = 0x03 + const private val RESET_INS: Byte = 0x04 + + const private val LIST_INS = 0xa1.toByte() + const private val CALCULATE_INS = 0xa2.toByte() + const private val VALIDATE_INS = 0xa3.toByte() + const private val CALCULATE_ALL_INS = 0xa4.toByte() + const private val SEND_REMAINING_INS = 0xa5.toByte() + + private val AID = byteArrayOf(0xa0.toByte(), 0x00, 0x00, 0x05, 0x27, 0x21, 0x01, 0x01) + + private fun getDeviceId(id: ByteArray): String { + val digest = MessageDigest.getInstance("SHA256").apply { + update(id) + }.digest() + + return Base64.encodeToString(digest.sliceArray(0 until 16), Base64.NO_PADDING or Base64.NO_WRAP) + } + + @Throws(IOException::class) + private fun ByteBuffer.parseTlv(tag: Byte): ByteArray { + val readTag = get() + if (readTag != tag) { + throw IOException("Required tag: %02x, got %02x".format(tag, readTag)) + } + return ByteArray(0xff and get().toInt()).apply { get(this) } + } + + private fun ByteBuffer.tlv(tag: Byte, data: ByteArray = byteArrayOf()): ByteBuffer { + return put(tag).put(data.size.toByte()).put(data) + } + + private data class Response(val data: ByteArray, val status: Int) + + private fun splitApduResponse(resp: ByteArray): Response { + return Response( + resp.copyOfRange(0, resp.size - 2), + ((0xff and resp[resp.size - 2].toInt()) shl 8) or (0xff and resp[resp.size - 1].toInt())) + } + } +}