diff --git a/src/keepass2android/ChallengeXCKey.cs b/src/keepass2android/ChallengeXCKey.cs index ab3dfb34..3e342a75 100644 --- a/src/keepass2android/ChallengeXCKey.cs +++ b/src/keepass2android/ChallengeXCKey.cs @@ -33,7 +33,7 @@ namespace keepass2android } } - var chalIntent = Activity.TryGetYubichallengeIntentOrPrompt(challenge64, true); + var chalIntent = Activity.GetYubichallengeIntent(challenge64); if (chalIntent == null) throw new Exception("YubiChallenge not installed."); diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index fcefa04f..a835708f 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -218,14 +218,18 @@ namespace keepass2android } //update the Entry output in the App database and notify the CopyToClipboard service - App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.Set(key, new ProtectedString(isProtected, value)); - Intent updateKeyboardIntent = new Intent(this, typeof(CopyToClipboardService)); - updateKeyboardIntent.SetAction(Intents.UpdateKeyboard); - updateKeyboardIntent.PutExtra(KeyEntry, Entry.Uuid.ToHexString()); - StartService(updateKeyboardIntent); - //notify plugins - NotifyPluginsOnModification(Strings.PrefixString+key); + if (App.Kp2a.GetDb()?.LastOpenedEntry != null) + { + App.Kp2a.GetDb().LastOpenedEntry.OutputStrings.Set(key, new ProtectedString(isProtected, value)); + Intent updateKeyboardIntent = new Intent(this, typeof(CopyToClipboardService)); + updateKeyboardIntent.SetAction(Intents.UpdateKeyboard); + updateKeyboardIntent.PutExtra(KeyEntry, Entry.Uuid.ToHexString()); + StartService(updateKeyboardIntent); + + //notify plugins + NotifyPluginsOnModification(Strings.PrefixString + key); + } } private void AddPluginAction(string pluginPackage, string fieldId, string popupItemId, string displayText, int iconId, Bundle bundleExtra) diff --git a/src/keepass2android/LockingActivity.cs b/src/keepass2android/LockingActivity.cs index d883120e..1992978b 100644 --- a/src/keepass2android/LockingActivity.cs +++ b/src/keepass2android/LockingActivity.cs @@ -123,27 +123,12 @@ namespace keepass2android } - public Intent TryGetYubichallengeIntentOrPrompt(byte[] challenge, bool promptToInstall) + public Intent GetYubichallengeIntent(byte[] challenge) { - Intent chalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE"); + Intent chalIntent = new Intent(this, typeof(YubiChallengeActivity)); chalIntent.PutExtra("challenge", challenge); - chalIntent.PutExtra("slot", 2); - IList activities = PackageManager.QueryIntentActivities(chalIntent, 0); - bool isIntentSafe = activities.Count > 0; - if (isIntentSafe) - { - return chalIntent; - } - if (promptToInstall) - { - AlertDialog.Builder b = new AlertDialog.Builder(this); - b.SetMessage(Resource.String.YubiChallengeNotInstalled); - b.SetPositiveButton(Android.Resource.String.Ok, - delegate { Util.GotoUrl(this, GetString(Resource.String.MarketURL) + "com.yubichallenge"); }); - b.SetNegativeButton(Resource.String.cancel, delegate { }); - b.Create().Show(); - } - return null; + return chalIntent; + } } } diff --git a/src/keepass2android/PasswordActivity.cs b/src/keepass2android/PasswordActivity.cs index 2d85e2ea..5b00cc77 100644 --- a/src/keepass2android/PasswordActivity.cs +++ b/src/keepass2android/PasswordActivity.cs @@ -625,7 +625,7 @@ namespace keepass2android protected override void HandleSuccess() { - var chalIntent = Activity.TryGetYubichallengeIntentOrPrompt(Activity._chalInfo.Challenge, true); + var chalIntent = Activity.GetYubichallengeIntent(Activity._chalInfo.Challenge); if (chalIntent != null) { diff --git a/src/keepass2android/Properties/AndroidManifest_net.xml b/src/keepass2android/Properties/AndroidManifest_net.xml index 40b35a1f..c97f578b 100644 --- a/src/keepass2android/Properties/AndroidManifest_net.xml +++ b/src/keepass2android/Properties/AndroidManifest_net.xml @@ -139,6 +139,7 @@ + diff --git a/src/keepass2android/Resources/values-de/strings.xml b/src/keepass2android/Resources/values-de/strings.xml index af38e02c..9af0bff7 100644 --- a/src/keepass2android/Resources/values-de/strings.xml +++ b/src/keepass2android/Resources/values-de/strings.xml @@ -864,7 +864,6 @@ Erstes öffentliches Release Kennwort + OTP Secret (Recovery-Modus) Passwort + Challenge-Response Passwort + Challenge-Response-Secret (Recovery-Modus) - Password + Challenge-Response for KeepassXC database Fehler bei Zertifikatsvalidierung ignorieren diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index 09acef7a..fdef392f 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -554,7 +554,6 @@ Load OTP auxiliary file… Enter the next One-time-passwords (OTPs). Swipe your Yubikey NEO at the back of your device to enter via NFC. OTP %1$d - Could not find an app that can handle the challenge. Please install Yubichallenge from Google Play. Could not load auxiliary OTP file! Please use the OtpKeyProv plugin in KeePass 2.x (PC) to configure your database for use with OTPs! Please select database first. OTP is discarded for security reasons. @@ -567,6 +566,19 @@ Error updating OTP auxiliary file! Saving auxiliary OTP file… + NFC Challenge-Response + Challenging + Please swipe your YubiKey NEO + Please swipe and hold your YubiKey NEO + Failed getting response, is the YubiKey and/or YubiChallenge configured correctly? + YubiKey NEO lost during operation + Error communicating with YubiKey. + NFC seems to be off, turn on? + This device seems to not support NFC + YubiKey Error + The response from the YubiKey is incorrect + + The challenge response is incorrect. Could not load auxiliary challenge file! Please use the KeeChallenge plugin in KeePass 2.x (PC) to configure your database for use with challenge-response! @@ -986,7 +998,7 @@ * Improved determination of lock-state in some situations\n * Database files are loaded to memory while you are typing your password for increased loading speed (see settings)\n * Entries can be added to root group\n - * Bug fixes (resolving reference fields, problems with keyboard on Italian an Chinese devices) + * Bug fixes (resolving reference fields, problems with keyboard on Italian and Chinese devices) Version 0.8.4\n @@ -1086,7 +1098,6 @@ Initial public release Password + OTP secret (recovery mode) Password + Challenge-Response Password + Challenge-Response secret (recovery mode) - Password + Challenge-Response for KeepassXC database Ignore certificate validation failures diff --git a/src/keepass2android/YubiChallengeActivity.cs b/src/keepass2android/YubiChallengeActivity.cs new file mode 100644 index 00000000..404c963f --- /dev/null +++ b/src/keepass2android/YubiChallengeActivity.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.Nfc; +using Android.Nfc.Tech; +using Android.OS; +using Android.Preferences; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using Java.IO; + +namespace keepass2android +{ + [Activity(Label = "@string/yubichallenge_title_activity_nfc")] + [IntentFilter(new string[] { "android.nfc.action.NDEF_DISCOVERED" }, Categories = new string[]{Android.Content.Intent.CategoryDefault})] + public class YubiChallengeActivity : Activity + { + private byte[] challenge; + private int slot; + + private const byte SLOT_CHAL_HMAC1 = 0x30; + private const byte SLOT_CHAL_HMAC2 = 0x38; + private const byte CHAL_BYTES = 0x40; // 64 + private const byte RESP_BYTES = 20; + + private static byte[] selectCommand = { 0x00, (byte) 0xA4, 0x04, + 0x00, 0x07, (byte) 0xA0, 0x00, 0x00, 0x05, 0x27, 0x20, 0x01, 0x00 }; + private static byte[] chalCommand = { 0x00, 0x01, SLOT_CHAL_HMAC2, + 0x00, CHAL_BYTES }; + + private AlertDialog swipeDialog; + + private const int PERMISSIONS_REQUEST_NFC = 901234845; + + protected override void OnResume() + { + base.OnResume(); + + + challenge = Intent.GetByteArrayExtra("challenge"); + slot = PreferenceManager.GetDefaultSharedPreferences(this).GetInt("pref_Slot", 2); + if (challenge == null) return; + else if (challenge.Length != CHAL_BYTES) return; + else ChallengeYubiKey(); + } + + protected override void OnNewIntent(Intent intent) + { + base.OnNewIntent(intent); + + Tag tag = intent.GetParcelableExtra(NfcAdapter.ExtraTag) as Tag; + SetResult(Result.Canceled, intent); + if (tag != null) + { + IsoDep isoTag = IsoDep.Get(tag); + try + { + isoTag.Connect(); + byte[] resp = isoTag.Transceive(selectCommand); + int length = resp.Length; + if (resp[length - 2] == (byte)0x90 && resp[length - 1] == 0x00) + DoChallengeYubiKey(isoTag, slot, challenge); + else + { + Toast.MakeText(this, Resource.String.yubichallenge_tag_error, ToastLength.Long) + .Show(); + + } + + isoTag.Close(); + } + catch (TagLostException e) + { + Toast.MakeText(this, Resource.String.yubichallenge_lost_tag, ToastLength.Long) + .Show(); + + } + catch (IOException e) + { + Toast.MakeText(this, GetString(Resource.String.yubichallenge_tag_error) + e.Message, + ToastLength.Long).Show(); + } + } + else SetResult(Result.Canceled, intent); + Finish(); + } + + protected override void OnPause() + { + base.OnPause(); + if (swipeDialog != null) + { + swipeDialog.Dismiss(); + swipeDialog = null; + } + DisableDispatch(); + } + + private void DisableDispatch() + { + NfcAdapter adapter = NfcAdapter.GetDefaultAdapter(this); + if (adapter != null) + { + adapter.DisableForegroundDispatch(this); + } + } + + private void DoChallengeYubiKey(IsoDep isoTag, int slot, byte[] challenge) + { + if (challenge == null || challenge.Length != CHAL_BYTES) + return; + + byte[] apdu = new byte[chalCommand.Length + CHAL_BYTES]; + chalCommand.CopyTo(apdu, 0); + + if (slot == 1) + apdu[2] = SLOT_CHAL_HMAC1; + challenge.CopyTo(apdu, chalCommand.Length); + + byte[] respApdu = isoTag.Transceive(apdu); + if (respApdu.Length == 22 && respApdu[20] == (byte) 0x90 + && respApdu[21] == 0x00) + { + // Get the secret + byte[] resp = new byte[RESP_BYTES]; + Array.Copy(respApdu, 0, resp, 0, RESP_BYTES); + Intent data = Intent; + data.PutExtra("response", resp); + SetResult(Result.Ok, data); + } + else + { + Toast.MakeText(this, Resource.String.yubichallenge_challenge_failed, ToastLength.Long) + .Show(); + + } + } + + private void ChallengeYubiKey() + { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.SetTitle(Resource.String.yubichallenge_challenging); + builder.SetMessage(Resource.String.yubichallenge_swipe); + builder.SetNegativeButton(Android.Resource.String.Cancel, (obj, args) => Finish()); + builder.SetCancelable(false); + swipeDialog = builder.Show(); + + EnableDispatch(); + } + + public override void OnBackPressed() + { + base.OnBackPressed(); + Finish(); + } + + private void EnableDispatch() + { + Intent intent = Intent; + intent.AddFlags(ActivityFlags.SingleTop); + + PendingIntent tagIntent = PendingIntent.GetActivity( + this, 0, intent, 0); + + IntentFilter iso = new IntentFilter(NfcAdapter.ActionNdefDiscovered); + NfcAdapter adapter = NfcAdapter.GetDefaultAdapter(this); + if (adapter == null) + { + Toast.MakeText(this, Resource.String.yubichallenge_no_nfc, ToastLength.Long).Show(); + return; + } + if (adapter.IsEnabled) + { + // register for foreground dispatch so we'll receive tags according to our intent filters + adapter.EnableForegroundDispatch( + this, tagIntent, new IntentFilter[] { iso }, + new String[][] { new String[] { Java.Lang.Class.FromType(typeof(IsoDep)).Name } + } + ); + } else { + AlertDialog.Builder dialog = new AlertDialog.Builder(this); + dialog.SetTitle(Resource.String.yubichallenge_nfc_off); + dialog.SetPositiveButton(Android.Resource.String.Yes, (sender, args) => + { + Intent settings = new Intent(Android.Provider.Settings.ActionNfcSettings); + StartActivity(settings); + ((Dialog)sender).Dismiss(); + }); + dialog.SetNegativeButton(Android.Resource.String.No, (sender, args) => + { + ((Dialog)sender).Dismiss(); + }); + dialog.Show(); + } + } + + protected override void OnCreate(Bundle savedInstanceState) + { + base.OnCreate(savedInstanceState); + SetResult(Result.Canceled); + // Create your application here + } + } +} \ No newline at end of file diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index 586cf4a1..9c9b2a10 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -307,6 +307,7 @@ +