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!
This commit is contained in:
Philipp Crocoll
2018-04-10 07:15:19 +02:00
parent cdbb492f2c
commit f14aad0c50
10 changed files with 265 additions and 169 deletions

View File

@@ -55,7 +55,10 @@ namespace KeePassLib.Cryptography.KeyDerivation
get { return "AES-KDF"; } get { return "AES-KDF"; }
} }
public AesKdf() public override byte[] GetSeed(KdfParameters p)
{ return p.GetByteArray(ParamSeed); }
public AesKdf()
{ {
} }

View File

@@ -68,7 +68,10 @@ namespace KeePassLib.Cryptography.KeyDerivation
get { return "Argon2"; } get { return "Argon2"; }
} }
public Argon2Kdf() public override byte[] GetSeed(KdfParameters p)
{ return p.GetByteArray(ParamSalt); }
public Argon2Kdf()
{ {
} }

View File

@@ -36,7 +36,9 @@ namespace KeePassLib.Cryptography.KeyDerivation
get; get;
} }
public virtual KdfParameters GetDefaultParameters() public abstract byte[] GetSeed(KdfParameters p);
public virtual KdfParameters GetDefaultParameters()
{ {
return new KdfParameters(this.Uuid); return new KdfParameters(this.Uuid);
} }

View File

@@ -20,11 +20,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Text;
using KeePassLib.Cryptography; using KeePassLib.Cryptography;
using KeePassLib.Cryptography.KeyDerivation; using KeePassLib.Cryptography.KeyDerivation;
using KeePassLib.Native;
using KeePassLib.Resources; using KeePassLib.Resources;
using KeePassLib.Security; using KeePassLib.Security;
using KeePassLib.Utility; using KeePassLib.Utility;
@@ -168,7 +165,7 @@ namespace KeePassLib.Keys
/// Creates the composite key from the supplied user key sources (password, /// Creates the composite key from the supplied user key sources (password,
/// key file, user account, computer ID, etc.). /// key file, user account, computer ID, etc.).
/// </summary> /// </summary>
private byte[] CreateRawCompositeKey32(byte[] mPbMasterSeed) private byte[] CreateRawCompositeKey32(byte[] mPbMasterSeed, byte[] mPbKdfSeed)
{ {
ValidateUserKeys(); ValidateUserKeys();
@@ -178,7 +175,7 @@ namespace KeePassLib.Keys
foreach(IUserKey pKey in m_vUserKeys) foreach(IUserKey pKey in m_vUserKeys)
{ {
if (pKey is ISeedBasedUserKey) if (pKey is ISeedBasedUserKey)
((ISeedBasedUserKey)pKey).SetParams(mPbMasterSeed); ((ISeedBasedUserKey)pKey).SetParams(mPbMasterSeed, mPbKdfSeed);
ProtectedBinary b = pKey.KeyData; ProtectedBinary b = pKey.KeyData;
if(b != null) if(b != null)
{ {
@@ -211,15 +208,17 @@ namespace KeePassLib.Keys
{ {
if(p == null) { Debug.Assert(false); throw new ArgumentNullException("p"); } 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)) if((pbRaw32 == null) || (pbRaw32.Length != 32))
{ Debug.Assert(false); return null; } { 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); byte[] pbTrf32 = kdf.Transform(pbRaw32, p);
if(pbTrf32 == null) { Debug.Assert(false); return null; } if(pbTrf32 == null) { Debug.Assert(false); return null; }
@@ -256,7 +255,7 @@ namespace KeePassLib.Keys
public interface ISeedBasedUserKey public interface ISeedBasedUserKey
{ {
void SetParams(byte[] masterSeed); void SetParams(byte[] masterSeed, byte[] mPbKdfSeed);
} }
public sealed class InvalidCompositeKeyException : Exception public sealed class InvalidCompositeKeyException : Exception

View File

@@ -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;
}
}
}

View File

@@ -16,6 +16,10 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. This file
*/ */
using System; using System;
using System.Collections.Generic;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Runtime; using Android.Runtime;
namespace keepass2android 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<ChallengeXCKey>();
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<ChallengeXCKey>();
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(); base.OnPause();
TimeoutHelper.Pause(this); TimeoutHelper.Pause(this);
@@ -52,6 +87,30 @@ namespace keepass2android
TimeoutHelper.Resume(this); 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<ResolveInfo> 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;
}
}
} }

View File

@@ -19,6 +19,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using System.Xml.Serialization; using System.Xml.Serialization;
@@ -60,7 +61,6 @@ using Process = Android.OS.Process;
using KeeChallenge; using KeeChallenge;
using KeePassLib.Cryptography.KeyDerivation; using KeePassLib.Cryptography.KeyDerivation;
using KeePassLib.Security;
using AlertDialog = Android.App.AlertDialog; using AlertDialog = Android.App.AlertDialog;
using Enum = System.Enum; using Enum = System.Enum;
using Exception = System.Exception; using Exception = System.Exception;
@@ -69,72 +69,7 @@ using Toolbar = Android.Support.V7.Widget.Toolbar;
namespace keepass2android namespace keepass2android
{ {
class ChallengeXCKey : IUserKey, ISeedBasedUserKey [Activity(Label = "@string/app_name",
{
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<ResolveInfo> 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",
ConfigurationChanges = ConfigChanges.Orientation, ConfigurationChanges = ConfigChanges.Orientation,
LaunchMode = LaunchMode.SingleInstance, LaunchMode = LaunchMode.SingleInstance,
WindowSoftInputMode = SoftInput.AdjustResize, WindowSoftInputMode = SoftInput.AdjustResize,
@@ -434,69 +369,91 @@ namespace keepass2android
GetAuxFileLoader().LoadAuxFile(false); GetAuxFileLoader().LoadAuxFile(false);
} }
if (requestCode == RequestCodeChallengeYubikey && resultCode == Result.Ok) if (requestCode == RequestCodeChallengeYubikey)
{ {
try if (resultCode == Result.Ok)
{ {
byte[] challengeResponse = data.GetByteArrayExtra("response");
if (_currentlyWaitingKey != null)
{ try
_currentlyWaitingKey.Response = challengeResponse; {
return; byte[] challengeResponse = data.GetByteArrayExtra("response");
} if (_currentlyWaitingKey != null)
else {
{ if ((challengeResponse != null) && (challengeResponse.Length > 0))
_challengeProv = new KeeChallengeProv(); {
_challengeSecret = _challengeProv.GetSecret(_chalInfo, challengeResponse); _currentlyWaitingKey.Response = challengeResponse;
Array.Clear(challengeResponse, 0, challengeResponse.Length); }
} else
_currentlyWaitingKey.Error = "Did not receive a valid response.";
}
catch (Exception e) return;
{
Kp2aLog.Log(e.ToString()); }
Toast.MakeText(this, "Error: " + e.Message, ToastLength.Long).Show(); else
return; {
} _challengeProv = new KeeChallengeProv();
_challengeSecret = _challengeProv.GetSecret(_chalInfo, challengeResponse);
UpdateOkButtonState(); Array.Clear(challengeResponse, 0, challengeResponse.Length);
FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone; }
if (_challengeSecret != null) }
{ catch (Exception e)
new LoadingDialog<object, object, object>(this, true, {
//doInBackground if (_currentlyWaitingKey != null)
delegate {
{ _currentlyWaitingKey.Error = e.Message;
//save aux file }
try Kp2aLog.Log(e.ToString());
{ Toast.MakeText(this, "Error: " + e.Message, ToastLength.Long).Show();
ChallengeInfo temp = _challengeProv.Encrypt(_challengeSecret); return;
if (!temp.Save(_otpAuxIoc)) }
{
Toast.MakeText(this, Resource.String.ErrorUpdatingChalAuxFile, ToastLength.Long).Show(); UpdateOkButtonState();
return false; FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone;
}
if (_challengeSecret != null)
} {
catch (Exception e) new LoadingDialog<object, object, object>(this, true,
{ //doInBackground
Kp2aLog.LogUnexpectedError(e); delegate
} {
return null; //save aux file
} try
, delegate {
{ ChallengeInfo temp = _challengeProv.Encrypt(_challengeSecret);
if (!temp.Save(_otpAuxIoc))
}).Execute(); {
Toast.MakeText(this, Resource.String.ErrorUpdatingChalAuxFile, ToastLength.Long)
} .Show();
else return false;
{ }
Toast.MakeText(this, Resource.String.bad_resp, ToastLength.Long).Show();
return; }
} 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() private AuxFileLoader GetAuxFileLoader()
@@ -690,27 +647,12 @@ namespace keepass2android
protected override void HandleSuccess() protected override void HandleSuccess()
{ {
Intent chalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE"); var chalIntent = Activity.TryGetYubichallengeIntentOrPrompt(Activity._chalInfo.Challenge, true);
chalIntent.PutExtra("challenge", Activity._chalInfo.Challenge);
chalIntent.PutExtra("slot", 2); if (chalIntent != null)
IList<ResolveInfo> activities = Activity.PackageManager.QueryIntentActivities(chalIntent, 0); {
bool isIntentSafe = activities.Count > 0; Activity.StartActivityForResult(chalIntent, RequestCodeChallengeYubikey);
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();
}
} }
protected override string GetErrorMessage() protected override string GetErrorMessage()
@@ -746,7 +688,8 @@ namespace keepass2android
} }
} }
private void ShowOtpEntry(IList<string> prefilledOtps)
private void ShowOtpEntry(IList<string> prefilledOtps)
{ {
FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone; FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone;
FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible; FindViewById(Resource.Id.otpEntry).Visibility = ViewStates.Visible;

View File

@@ -864,6 +864,7 @@ Erstes öffentliches Release</string>
<item>Kennwort + OTP Secret (Recovery-Modus)</item> <item>Kennwort + OTP Secret (Recovery-Modus)</item>
<item>Passwort + Challenge-Response</item> <item>Passwort + Challenge-Response</item>
<item>Passwort + Challenge-Response-Secret (Recovery-Modus)</item> <item>Passwort + Challenge-Response-Secret (Recovery-Modus)</item>
<item>Password + Challenge-Response for KeepassXC database</item>
</string-array> </string-array>
<string-array name="AcceptAllServerCertificates_options"> <string-array name="AcceptAllServerCertificates_options">
<item>Fehler bei Zertifikatsvalidierung ignorieren</item> <item>Fehler bei Zertifikatsvalidierung ignorieren</item>

View File

@@ -1069,6 +1069,7 @@ Initial public release
<item>Password + OTP secret (recovery mode)</item> <item>Password + OTP secret (recovery mode)</item>
<item>Password + Challenge-Response</item> <item>Password + Challenge-Response</item>
<item>Password + Challenge-Response secret (recovery mode)</item> <item>Password + Challenge-Response secret (recovery mode)</item>
<item>Password + Challenge-Response for KeepassXC database</item>
</string-array> </string-array>
<string-array name="AcceptAllServerCertificates_options"> <string-array name="AcceptAllServerCertificates_options">
<item>Ignore certificate validation failures</item> <item>Ignore certificate validation failures</item>

View File

@@ -171,6 +171,7 @@
</Reference> </Reference>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="ChallengeXCKey.cs" />
<Compile Include="EntryActivityClasses\ViewImagePopupItem.cs" /> <Compile Include="EntryActivityClasses\ViewImagePopupItem.cs" />
<Compile Include="ImageViewActivity.cs" /> <Compile Include="ImageViewActivity.cs" />
<Compile Include="addons\OtpKeyProv\EncodingUtil.cs" /> <Compile Include="addons\OtpKeyProv\EncodingUtil.cs" />