Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
61b50b1465
8 changed files with 518 additions and 23 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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<PatternLockView.Dot> pattern) {
|
||||
if (PasswordManager.checkPassword(patternEncrypted,patternToString(pattern))){
|
||||
if (CrystalSecurityMonitor.getInstance(null).is2ndFactorSet()) {
|
||||
CrystalSecurityMonitor.getInstance(null).call2ndFactor(thisActivity);
|
||||
} else {
|
||||
thisActivity.finish();
|
||||
}
|
||||
} else {
|
||||
patternLockView.clearPattern();
|
||||
patternLockView.requestFocus();
|
||||
|
|
|
@ -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,9 +67,13 @@ public class PinRequestActivity extends AppCompatActivity {
|
|||
callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED)
|
||||
void afterPasswordChanged(Editable editable) {
|
||||
if (PasswordManager.checkPassword(passwordEncrypted, etPassword.getText().toString())) {
|
||||
if (CrystalSecurityMonitor.getInstance(null).is2ndFactorSet()) {
|
||||
CrystalSecurityMonitor.getInstance(null).call2ndFactor(this);
|
||||
} else {
|
||||
this.finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
String encodedSecret = "hola";
|
||||
Base32 decoder = new Base32();
|
||||
|
||||
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);
|
||||
|
||||
//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();
|
||||
/*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')
|
||||
}
|
||||
}*/
|
||||
|
||||
tagIsoDep.close();
|
||||
//ykOathApi.
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Toast.makeText(this, "Tag from nfc: "+tagFromIntent, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
//
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<String> {
|
||||
val resp = send(LIST_INS)
|
||||
|
||||
return mutableListOf<String>().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<ResponseData> {
|
||||
val resp = send(CALCULATE_ALL_INS, p2 = 1) {
|
||||
tlv(CHALLENGE_TAG, challenge)
|
||||
}
|
||||
|
||||
return mutableListOf<ResponseData>().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()))
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue