From f14aad0c503d1dec4cd81cbd16d4654a591ddb80 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 10 Apr 2018 07:15:19 +0200 Subject: [PATCH] implemented loading of files with KeepassXC Challenge (#4). requires write support, handling of Challenge/Response errors (or user cancels). Caution: saving corrupts the file at the moment! --- .../Cryptography/KeyDerivation/AesKdf.cs | 5 +- .../Cryptography/KeyDerivation/Argon2Kdf.cs | 5 +- .../Cryptography/KeyDerivation/KdfEngine.cs | 4 +- src/KeePassLib2Android/Keys/CompositeKey.cs | 23 +- src/keepass2android/ChallengeXCKey.cs | 84 ++++++ src/keepass2android/LockingActivity.cs | 63 ++++- src/keepass2android/PasswordActivity.cs | 247 +++++++----------- .../Resources/values-de/strings.xml | 1 + .../Resources/values/strings.xml | 1 + src/keepass2android/keepass2android.csproj | 1 + 10 files changed, 265 insertions(+), 169 deletions(-) create mode 100644 src/keepass2android/ChallengeXCKey.cs diff --git a/src/KeePassLib2Android/Cryptography/KeyDerivation/AesKdf.cs b/src/KeePassLib2Android/Cryptography/KeyDerivation/AesKdf.cs index c7a66a58..cf7bc634 100644 --- a/src/KeePassLib2Android/Cryptography/KeyDerivation/AesKdf.cs +++ b/src/KeePassLib2Android/Cryptography/KeyDerivation/AesKdf.cs @@ -55,7 +55,10 @@ namespace KeePassLib.Cryptography.KeyDerivation get { return "AES-KDF"; } } - public AesKdf() + public override byte[] GetSeed(KdfParameters p) + { return p.GetByteArray(ParamSeed); } + + public AesKdf() { } diff --git a/src/KeePassLib2Android/Cryptography/KeyDerivation/Argon2Kdf.cs b/src/KeePassLib2Android/Cryptography/KeyDerivation/Argon2Kdf.cs index bc858ca8..8663eb2c 100644 --- a/src/KeePassLib2Android/Cryptography/KeyDerivation/Argon2Kdf.cs +++ b/src/KeePassLib2Android/Cryptography/KeyDerivation/Argon2Kdf.cs @@ -68,7 +68,10 @@ namespace KeePassLib.Cryptography.KeyDerivation get { return "Argon2"; } } - public Argon2Kdf() + public override byte[] GetSeed(KdfParameters p) + { return p.GetByteArray(ParamSalt); } + + public Argon2Kdf() { } diff --git a/src/KeePassLib2Android/Cryptography/KeyDerivation/KdfEngine.cs b/src/KeePassLib2Android/Cryptography/KeyDerivation/KdfEngine.cs index 994e0c7e..7e4f5b5c 100644 --- a/src/KeePassLib2Android/Cryptography/KeyDerivation/KdfEngine.cs +++ b/src/KeePassLib2Android/Cryptography/KeyDerivation/KdfEngine.cs @@ -36,7 +36,9 @@ namespace KeePassLib.Cryptography.KeyDerivation get; } - public virtual KdfParameters GetDefaultParameters() + public abstract byte[] GetSeed(KdfParameters p); + + public virtual KdfParameters GetDefaultParameters() { return new KdfParameters(this.Uuid); } diff --git a/src/KeePassLib2Android/Keys/CompositeKey.cs b/src/KeePassLib2Android/Keys/CompositeKey.cs index a347fc11..7e935df0 100644 --- a/src/KeePassLib2Android/Keys/CompositeKey.cs +++ b/src/KeePassLib2Android/Keys/CompositeKey.cs @@ -20,11 +20,8 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using System.Text; - using KeePassLib.Cryptography; using KeePassLib.Cryptography.KeyDerivation; -using KeePassLib.Native; using KeePassLib.Resources; using KeePassLib.Security; using KeePassLib.Utility; @@ -168,7 +165,7 @@ namespace KeePassLib.Keys /// Creates the composite key from the supplied user key sources (password, /// key file, user account, computer ID, etc.). /// - private byte[] CreateRawCompositeKey32(byte[] mPbMasterSeed) + private byte[] CreateRawCompositeKey32(byte[] mPbMasterSeed, byte[] mPbKdfSeed) { ValidateUserKeys(); @@ -178,7 +175,7 @@ namespace KeePassLib.Keys foreach(IUserKey pKey in m_vUserKeys) { if (pKey is ISeedBasedUserKey) - ((ISeedBasedUserKey)pKey).SetParams(mPbMasterSeed); + ((ISeedBasedUserKey)pKey).SetParams(mPbMasterSeed, mPbKdfSeed); ProtectedBinary b = pKey.KeyData; if(b != null) { @@ -211,15 +208,17 @@ namespace KeePassLib.Keys { if(p == null) { Debug.Assert(false); throw new ArgumentNullException("p"); } - byte[] pbRaw32 = CreateRawCompositeKey32(mPbMasterSeed); + + KdfEngine kdf = KdfPool.Get(p.KdfUuid); + if (kdf == null) // CryptographicExceptions are translated to "file corrupted" + throw new Exception(KLRes.UnknownKdf + MessageService.NewParagraph + + KLRes.FileNewVerOrPlgReq + MessageService.NewParagraph + + "UUID: " + p.KdfUuid.ToHexString() + "."); + + byte[] pbRaw32 = CreateRawCompositeKey32(mPbMasterSeed, kdf.GetSeed(p)); if((pbRaw32 == null) || (pbRaw32.Length != 32)) { Debug.Assert(false); return null; } - KdfEngine kdf = KdfPool.Get(p.KdfUuid); - if(kdf == null) // CryptographicExceptions are translated to "file corrupted" - throw new Exception(KLRes.UnknownKdf + MessageService.NewParagraph + - KLRes.FileNewVerOrPlgReq + MessageService.NewParagraph + - "UUID: " + p.KdfUuid.ToHexString() + "."); byte[] pbTrf32 = kdf.Transform(pbRaw32, p); if(pbTrf32 == null) { Debug.Assert(false); return null; } @@ -256,7 +255,7 @@ namespace KeePassLib.Keys public interface ISeedBasedUserKey { - void SetParams(byte[] masterSeed); + void SetParams(byte[] masterSeed, byte[] mPbKdfSeed); } public sealed class InvalidCompositeKeyException : Exception diff --git a/src/keepass2android/ChallengeXCKey.cs b/src/keepass2android/ChallengeXCKey.cs new file mode 100644 index 00000000..8c224d01 --- /dev/null +++ b/src/keepass2android/ChallengeXCKey.cs @@ -0,0 +1,84 @@ +using Java.Lang; +using KeePassLib.Cryptography; +using KeePassLib.Keys; +using KeePassLib.Security; +using Exception = System.Exception; + +namespace keepass2android +{ + class ChallengeXCKey : IUserKey, ISeedBasedUserKey + { + private readonly int _requestCode; + + public ProtectedBinary KeyData + { + get + { + if (Activity == null) + throw new Exception("Need an active Keepass2Android activity to challenge Yubikey!"); + Activity.RunOnUiThread( + () => + { + byte[] challenge = _kdfSeed; + byte[] challenge64 = new byte[64]; + for (int i = 0; i < 64; i++) + { + if (i < challenge.Length) + { + challenge64[i] = challenge[i]; + } + else + { + challenge64[i] = (byte)(challenge64.Length - challenge.Length); + } + + } + var chalIntent = Activity.TryGetYubichallengeIntentOrPrompt(challenge64, true); + + if (chalIntent == null) + throw new Exception("YubiChallenge not installed."); + + Activity.StartActivityForResult(chalIntent, _requestCode); + + + + + + + }); + while ((Response == null) && (Error == null)) + { + Thread.Sleep(50); + } + if (Error != null) + throw new Exception("YubiChallenge failed: " + Error); + + return new ProtectedBinary(true, CryptoUtil.HashSha256(Response)); + } + } + + private byte[] _kdfSeed; + + public ChallengeXCKey(LockingActivity activity, int requestCode) + { + this.Activity = activity; + _requestCode = requestCode; + Response = null; + } + + public void SetParams(byte[] masterSeed, byte[] mPbKdfSeed) + { + _kdfSeed = mPbKdfSeed; + } + + public byte[] Response { get; set; } + + public string Error { get; set; } + + public LockingActivity Activity + { + get; + set; + } + } +} \ No newline at end of file diff --git a/src/keepass2android/LockingActivity.cs b/src/keepass2android/LockingActivity.cs index 7a588b14..d5feb517 100644 --- a/src/keepass2android/LockingActivity.cs +++ b/src/keepass2android/LockingActivity.cs @@ -16,6 +16,10 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file */ using System; +using System.Collections.Generic; +using Android.App; +using Android.Content; +using Android.Content.PM; using Android.Runtime; namespace keepass2android @@ -35,7 +39,38 @@ namespace keepass2android { } - protected override void OnPause() { + protected override void OnStart() + { + base.OnStart(); + + if (App.Kp2a.GetDb().Loaded) + { + var xcKey = App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(); + if (xcKey != null) + { + xcKey.Activity = this; + } + + } + + } + + protected override void OnStop() + { + base.OnStop(); + if (App.Kp2a.GetDb().Loaded) + { + var xcKey = App.Kp2a.GetDb().KpDatabase.MasterKey.GetUserKey(); + if (xcKey != null) + { + //don't store a pointer to this activity in the static database object to avoid memory leak + xcKey.Activity = null; + } + + } + } + + protected override void OnPause() { base.OnPause(); TimeoutHelper.Pause(this); @@ -52,6 +87,30 @@ namespace keepass2android TimeoutHelper.Resume(this); } - } + + + public Intent TryGetYubichallengeIntentOrPrompt(byte[] challenge, bool promptToInstall) + { + Intent chalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE"); + 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; + } + } } diff --git a/src/keepass2android/PasswordActivity.cs b/src/keepass2android/PasswordActivity.cs index 111260dc..5fb93842 100644 --- a/src/keepass2android/PasswordActivity.cs +++ b/src/keepass2android/PasswordActivity.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Threading.Tasks; using System.Xml; using System.Xml.Serialization; @@ -60,7 +61,6 @@ using Process = Android.OS.Process; using KeeChallenge; using KeePassLib.Cryptography.KeyDerivation; -using KeePassLib.Security; using AlertDialog = Android.App.AlertDialog; using Enum = System.Enum; using Exception = System.Exception; @@ -69,72 +69,7 @@ using Toolbar = Android.Support.V7.Widget.Toolbar; namespace keepass2android { - class ChallengeXCKey : IUserKey, ISeedBasedUserKey - { - private readonly Activity _activity; - private readonly int _requestCode; - - public ProtectedBinary KeyData - { - get - { - - _activity.RunOnUiThread( - () => - { - //TODO refactor to use code from PasswordActivity including notice to install Yubichallenge - Intent chalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE"); - byte[] challenge = _masterSeed; - byte[] challenge64 = new byte[64]; - for (int i = 0; i < 64; i++) - { - if (i < challenge.Length) - { - challenge64[i] = challenge[i]; - } - else - { - challenge64[i] = (byte) (challenge64.Length - challenge.Length); - } - - } - - Kp2aLog.Log(MemUtil.ByteArrayToHexString(challenge64)); - - chalIntent.PutExtra("challenge", challenge64); - chalIntent.PutExtra("slot", 2); - IList activities = _activity.PackageManager.QueryIntentActivities(chalIntent, 0); - bool isIntentSafe = activities.Count > 0; - if (isIntentSafe) - { - _activity.StartActivityForResult(chalIntent, _requestCode); - } - else throw new Exception("TODO implement: you need YubiChallenge"); - }); - while (Response == null) - Thread.Sleep(100); - - return new ProtectedBinary(true, Response); - } - } - - private byte[] _masterSeed; - - public ChallengeXCKey(Activity activity, int requestCode) - { - this._activity = activity; - _requestCode = requestCode; - Response = null; - } - - public void SetParams(byte[] masterSeed) - { - _masterSeed = masterSeed; - } - - public byte[] Response { get; set; } - } - [Activity(Label = "@string/app_name", + [Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation, LaunchMode = LaunchMode.SingleInstance, WindowSoftInputMode = SoftInput.AdjustResize, @@ -434,69 +369,91 @@ namespace keepass2android GetAuxFileLoader().LoadAuxFile(false); } - if (requestCode == RequestCodeChallengeYubikey && resultCode == Result.Ok) - { - try - { - byte[] challengeResponse = data.GetByteArrayExtra("response"); - if (_currentlyWaitingKey != null) - { - _currentlyWaitingKey.Response = challengeResponse; - return; - } - else - { - _challengeProv = new KeeChallengeProv(); - _challengeSecret = _challengeProv.GetSecret(_chalInfo, challengeResponse); - Array.Clear(challengeResponse, 0, challengeResponse.Length); - } - - } - catch (Exception e) - { - Kp2aLog.Log(e.ToString()); - Toast.MakeText(this, "Error: " + e.Message, ToastLength.Long).Show(); - return; - } - - UpdateOkButtonState(); - FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone; - - if (_challengeSecret != null) - { - new LoadingDialog(this, true, - //doInBackground - delegate - { - //save aux file - try - { - ChallengeInfo temp = _challengeProv.Encrypt(_challengeSecret); - if (!temp.Save(_otpAuxIoc)) - { - Toast.MakeText(this, Resource.String.ErrorUpdatingChalAuxFile, ToastLength.Long).Show(); - return false; - } - - } - catch (Exception e) - { - Kp2aLog.LogUnexpectedError(e); - } - return null; - } - , delegate - { - - }).Execute(); - - } - else - { - Toast.MakeText(this, Resource.String.bad_resp, ToastLength.Long).Show(); - return; - } - } + if (requestCode == RequestCodeChallengeYubikey) + { + if (resultCode == Result.Ok) + { + + + try + { + byte[] challengeResponse = data.GetByteArrayExtra("response"); + if (_currentlyWaitingKey != null) + { + if ((challengeResponse != null) && (challengeResponse.Length > 0)) + { + _currentlyWaitingKey.Response = challengeResponse; + } + else + _currentlyWaitingKey.Error = "Did not receive a valid response."; + + return; + + } + else + { + _challengeProv = new KeeChallengeProv(); + _challengeSecret = _challengeProv.GetSecret(_chalInfo, challengeResponse); + Array.Clear(challengeResponse, 0, challengeResponse.Length); + } + + } + catch (Exception e) + { + if (_currentlyWaitingKey != null) + { + _currentlyWaitingKey.Error = e.Message; + } + Kp2aLog.Log(e.ToString()); + Toast.MakeText(this, "Error: " + e.Message, ToastLength.Long).Show(); + return; + } + + UpdateOkButtonState(); + FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone; + + if (_challengeSecret != null) + { + new LoadingDialog(this, true, + //doInBackground + delegate + { + //save aux file + try + { + ChallengeInfo temp = _challengeProv.Encrypt(_challengeSecret); + if (!temp.Save(_otpAuxIoc)) + { + Toast.MakeText(this, Resource.String.ErrorUpdatingChalAuxFile, ToastLength.Long) + .Show(); + return false; + } + + } + catch (Exception e) + { + Kp2aLog.LogUnexpectedError(e); + } + return null; + } + , delegate + { + + }).Execute(); + + } + else + { + Toast.MakeText(this, Resource.String.bad_resp, ToastLength.Long).Show(); + return; + } + } + } + else + { + if (_currentlyWaitingKey != null) + _currentlyWaitingKey.Error = "Cancelled Yubichallenge."; + } } private AuxFileLoader GetAuxFileLoader() @@ -690,27 +647,12 @@ namespace keepass2android protected override void HandleSuccess() { - Intent chalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE"); - chalIntent.PutExtra("challenge", Activity._chalInfo.Challenge); - chalIntent.PutExtra("slot", 2); - IList activities = Activity.PackageManager.QueryIntentActivities(chalIntent, 0); - bool isIntentSafe = activities.Count > 0; - if (isIntentSafe) - { - Activity.StartActivityForResult(chalIntent, RequestCodeChallengeYubikey); - } - else - { - AlertDialog.Builder b = new AlertDialog.Builder(Activity); - b.SetMessage(Resource.String.YubiChallengeNotInstalled); - b.SetPositiveButton(Android.Resource.String.Ok, - delegate - { - Util.GotoUrl(Activity, Activity.GetString(Resource.String.MarketURL) + "com.yubichallenge"); - }); - b.SetNegativeButton(Resource.String.cancel, delegate { }); - b.Create().Show(); - } + var chalIntent = Activity.TryGetYubichallengeIntentOrPrompt(Activity._chalInfo.Challenge, true); + + if (chalIntent != null) + { + Activity.StartActivityForResult(chalIntent, RequestCodeChallengeYubikey); + } } protected override string GetErrorMessage() @@ -746,7 +688,8 @@ namespace keepass2android } } - private void ShowOtpEntry(IList prefilledOtps) + + private void ShowOtpEntry(IList prefilledOtps) { FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone; FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible; diff --git a/src/keepass2android/Resources/values-de/strings.xml b/src/keepass2android/Resources/values-de/strings.xml index 9af0bff7..af38e02c 100644 --- a/src/keepass2android/Resources/values-de/strings.xml +++ b/src/keepass2android/Resources/values-de/strings.xml @@ -864,6 +864,7 @@ 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 938e5e96..8c3ca733 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -1069,6 +1069,7 @@ 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/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index bd46cbfe..586cf4a1 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -171,6 +171,7 @@ +