diff --git a/src/JavaFileStorageBindings/Jars/okhttp-3.4.1.jar b/src/JavaFileStorageBindings/Jars/okhttp-3.4.1.jar deleted file mode 100644 index e31f2486..00000000 Binary files a/src/JavaFileStorageBindings/Jars/okhttp-3.4.1.jar and /dev/null differ diff --git a/src/JavaFileStorageBindings/Jars/okhttp-3.9.0.jar b/src/JavaFileStorageBindings/Jars/okhttp-3.9.0.jar new file mode 100644 index 00000000..58ad4beb Binary files /dev/null and b/src/JavaFileStorageBindings/Jars/okhttp-3.9.0.jar differ diff --git a/src/JavaFileStorageBindings/JavaFileStorageBindings.csproj b/src/JavaFileStorageBindings/JavaFileStorageBindings.csproj index 64a279cb..796faa70 100644 --- a/src/JavaFileStorageBindings/JavaFileStorageBindings.csproj +++ b/src/JavaFileStorageBindings/JavaFileStorageBindings.csproj @@ -89,9 +89,6 @@ False - - - @@ -161,4 +158,7 @@ + + + \ No newline at end of file diff --git a/src/KeePassLib2Android/Keys/CompositeKey.cs b/src/KeePassLib2Android/Keys/CompositeKey.cs index 180734c0..a347fc11 100644 --- a/src/KeePassLib2Android/Keys/CompositeKey.cs +++ b/src/KeePassLib2Android/Keys/CompositeKey.cs @@ -163,11 +163,12 @@ namespace KeePassLib.Keys { return (T) GetUserKey(typeof (T)); } + /// /// Creates the composite key from the supplied user key sources (password, /// key file, user account, computer ID, etc.). /// - private byte[] CreateRawCompositeKey32() + private byte[] CreateRawCompositeKey32(byte[] mPbMasterSeed) { ValidateUserKeys(); @@ -176,6 +177,8 @@ namespace KeePassLib.Keys int cbData = 0; foreach(IUserKey pKey in m_vUserKeys) { + if (pKey is ISeedBasedUserKey) + ((ISeedBasedUserKey)pKey).SetParams(mPbMasterSeed); ProtectedBinary b = pKey.KeyData; if(b != null) { @@ -200,43 +203,15 @@ namespace KeePassLib.Keys return pbHash; } - public bool EqualsValue(CompositeKey ckOther) - { - if(ckOther == null) throw new ArgumentNullException("ckOther"); - - byte[] pbThis = CreateRawCompositeKey32(); - byte[] pbOther = ckOther.CreateRawCompositeKey32(); - bool bResult = MemUtil.ArraysEqual(pbThis, pbOther); - MemUtil.ZeroByteArray(pbOther); - MemUtil.ZeroByteArray(pbThis); - - return bResult; - } - - [Obsolete] - public ProtectedBinary GenerateKey32(byte[] pbKeySeed32, ulong uNumRounds) - { - Debug.Assert(pbKeySeed32 != null); - if(pbKeySeed32 == null) throw new ArgumentNullException("pbKeySeed32"); - Debug.Assert(pbKeySeed32.Length == 32); - if(pbKeySeed32.Length != 32) throw new ArgumentException("pbKeySeed32"); - - AesKdf kdf = new AesKdf(); - KdfParameters p = kdf.GetDefaultParameters(); - p.SetUInt64(AesKdf.ParamRounds, uNumRounds); - p.SetByteArray(AesKdf.ParamSeed, pbKeySeed32); - - return GenerateKey32(p); - } /// /// Generate a 32-byte (256-bit) key from the composite key. /// - public ProtectedBinary GenerateKey32(KdfParameters p) + public ProtectedBinary GenerateKey32(KdfParameters p, byte[] mPbMasterSeed) { if(p == null) { Debug.Assert(false); throw new ArgumentNullException("p"); } - byte[] pbRaw32 = CreateRawCompositeKey32(); + byte[] pbRaw32 = CreateRawCompositeKey32(mPbMasterSeed); if((pbRaw32 == null) || (pbRaw32.Length != 32)) { Debug.Assert(false); return null; } @@ -279,6 +254,11 @@ namespace KeePassLib.Keys } } + public interface ISeedBasedUserKey + { + void SetParams(byte[] masterSeed); + } + public sealed class InvalidCompositeKeyException : Exception { public override string Message diff --git a/src/KeePassLib2Android/Kp2aLog.cs b/src/KeePassLib2Android/Kp2aLog.cs index 84728988..b2a5a195 100644 --- a/src/KeePassLib2Android/Kp2aLog.cs +++ b/src/KeePassLib2Android/Kp2aLog.cs @@ -18,6 +18,9 @@ This file is part of Keepass2Android, Copyright 2013 Philipp Crocoll. using System; using System.Collections.Generic; using System.IO; +using Android; +using Android.App; +using Android.Content; using Android.Preferences; using KeePassLib.Serialization; @@ -57,7 +60,7 @@ namespace keepass2android private static string LogFilename { - get { return "/mnt/sdcard/keepass2android.log"; } + get { return Application.Context.FilesDir.CanonicalPath +"/keepass2android.log"; } } private static bool LogToFile @@ -77,5 +80,43 @@ namespace keepass2android if (OnUnexpectedError != null) OnUnexpectedError(null, exception); } + + public static void CreateLogFile() + { + if (!File.Exists(LogFilename)) + { + File.Create(LogFilename); + _logToFile = true; + } + + + } + + public static void FinishLogFile() + { + if (File.Exists(LogFilename)) + { + _logToFile = false; + int count = 0; + while (File.Exists(LogFilename + "." + count)) + count++; + File.Move(LogFilename, LogFilename + "." + count); + + } + + } + + public static void SendLog(Context ctx) + { + if (!File.Exists(LogFilename)) + return; + Intent sendIntent = new Intent(); + sendIntent.SetAction(Intent.ActionSend); + sendIntent.PutExtra(Intent.ExtraText, File.ReadAllText(LogFilename)); + sendIntent.PutExtra(Intent.ExtraEmail, "crocoapps@gmail.com"); + sendIntent.PutExtra(Intent.ExtraSubject, "Keepass2Android log"); + sendIntent.SetType("text/plain"); + ctx.StartActivity(Intent.CreateChooser(sendIntent, "Send log to...")); + } } } \ No newline at end of file diff --git a/src/KeePassLib2Android/Serialization/KdbxFile.cs b/src/KeePassLib2Android/Serialization/KdbxFile.cs index 261e4fd7..e61e2632 100644 --- a/src/KeePassLib2Android/Serialization/KdbxFile.cs +++ b/src/KeePassLib2Android/Serialization/KdbxFile.cs @@ -413,12 +413,15 @@ namespace KeePassLib.Serialization Debug.Assert(m_pbMasterSeed.Length == 32); if(m_pbMasterSeed.Length != 32) throw new FormatException(KLRes.MasterSeedLengthInvalid); - Array.Copy(m_pbMasterSeed, 0, pbCmp, 0, 32); + Debug.Assert(m_pwDatabase != null); Debug.Assert(m_pwDatabase.MasterKey != null); - ProtectedBinary pbinUser = m_pwDatabase.MasterKey.GenerateKey32( - m_pwDatabase.KdfParameters); + ProtectedBinary pbinUser = m_pwDatabase.MasterKey.GenerateKey32(m_pwDatabase.KdfParameters, + m_pbMasterSeed); + + Array.Copy(m_pbMasterSeed, 0, pbCmp, 0, 32); + Debug.Assert(pbinUser != null); if(pbinUser == null) throw new SecurityException(KLRes.InvalidCompositeKey); diff --git a/src/java/JavaFileStorage/app/build.gradle b/src/java/JavaFileStorage/app/build.gradle index 1893dec3..547c20c2 100644 --- a/src/java/JavaFileStorage/app/build.gradle +++ b/src/java/JavaFileStorage/app/build.gradle @@ -22,28 +22,14 @@ android { } dependencies { - /* - //compile files('libs/google-api-services-drive-v2-rev102-1.16.0-rc') - compile 'com.google.android.gms:play-services:6.5.+' - compile 'com.google.api-client:google-api-client-xml:1.18.0-rc' - compile 'com.google.http-client:google-http-client-gson:1.18.0-rc' - compile 'com.google.api-client:google-api-client-android:1.18.0-rc' - compile 'com.google.apis:google-api-services-drive:v2-rev155-1.19.0' - */ - compile 'com.squareup.okhttp3:okhttp:3.4.1' + compile 'com.squareup.okhttp3:okhttp:3.9.0' compile 'com.burgstaller:okhttp-digest:1.7' - // compile files('libs/dropbox-android-sdk-1.6.2.jar') compile 'com.google.android.gms:play-services:4.0.30' - /* compile('com.google.api-client:google-api-client-xml:1.16.0-rc') { - exclude group: 'com.google.android.google-play-services' - }*/ compile 'com.google.http-client:google-http-client-gson:1.16.0-rc' compile('com.google.api-client:google-api-client-android:1.16.0-rc') { exclude group: 'com.google.android.google-play-services' } compile 'com.google.apis:google-api-services-drive:v2-rev102-1.16.0-rc' - //compile 'com.dropbox.core:dropbox-core-sdk:2.0.1' - //compile group: 'com.dropbox.core', name: 'dropbox-core-sdk', version: '0-SNAPSHOT', changing: true compile 'com.dropbox.core:dropbox-core-sdk:3.0.3' //onedrive: compile('com.onedrive.sdk:onedrive-sdk-android:1.2+') { @@ -52,22 +38,5 @@ dependencies { compile 'com.google.code.gson:gson:2.3.1' compile 'com.microsoft.services.msa:msa-auth:0.8.+' compile 'com.microsoft.aad:adal:1.1.+' - /* compile 'com.google.http-client:google-http-client-gson:1.20.0' - compile 'com.google.code.gson:gson:2.1' - compile files('libs/commons-logging-1.1.1.jar') - compile files('libs/dropbox-android-sdk-1.6.2.jar') - compile files('libs/google-api-client-1.16.0-rc.jar') - compile files('libs/google-api-client-android-1.16.0-rc.jar') - compile files('libs/google-http-client-1.16.0-rc.jar') - compile files('libs/google-http-client-android-1.16.0-rc.jar') - compile files('libs/google-http-client-jackson-1.16.0-rc.jar') - compile files('libs/google-http-client-jackson2-1.16.0-rc.jar') - compile files('libs/google-oauth-client-1.16.0-rc.jar') - compile files('libs/httpclient-4.0.3.jar') - compile files('libs/httpcore-4.0.1.jar') - compile files('libs/httpmime-4.0.3.jar') - compile files('libs/jackson-core-2.1.3.jar') - compile files('libs/jackson-core-asl-1.9.11.jar') - compile files('libs/json_simple-1.1.jar') - compile files('libs/jsr305-1.3.9.jar')*/ + } diff --git a/src/java/KP2ASoftkeyboard_AS/app/app.iml b/src/java/KP2ASoftkeyboard_AS/app/app.iml deleted file mode 100644 index 68bc961f..00000000 --- a/src/java/KP2ASoftkeyboard_AS/app/app.iml +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/keepass2android/EntryActivity.cs b/src/keepass2android/EntryActivity.cs index 9ffdcfec..a3c1a621 100644 --- a/src/keepass2android/EntryActivity.cs +++ b/src/keepass2android/EntryActivity.cs @@ -34,6 +34,7 @@ using Android.Content.PM; using Android.Webkit; using Android.Graphics; using Java.IO; +using keepass2android.EntryActivityClasses; using KeePassLib; using KeePassLib.Security; using KeePassLib.Utility; @@ -623,6 +624,7 @@ namespace keepass2android var itemList = RegisterPopup(popupKey, valueViewContainer, valueViewContainer.FindViewById(Resource.Id.extra_vdots)); itemList.Add(new WriteBinaryToFilePopupItem(key, this)); itemList.Add(new OpenBinaryPopupItem(key, this)); + itemList.Add(new ViewImagePopupItem(key, this)); @@ -1115,5 +1117,19 @@ namespace keepass2android }, null, timeToWait, TimeSpan.FromMilliseconds(-1)); } + + public void ShowAttachedImage(string key) + { + ProtectedBinary pb = Entry.Binaries.Get(key); + System.Diagnostics.Debug.Assert(pb != null); + if (pb == null) + throw new ArgumentException(); + byte[] pbData = pb.ReadData(); + + Intent imageViewerIntent = new Intent(this, typeof(ImageViewActivity)); + imageViewerIntent.PutExtra("EntryId", Entry.Uuid.ToHexString()); + imageViewerIntent.PutExtra("EntryKey", key); + StartActivity(imageViewerIntent); + } } } \ No newline at end of file diff --git a/src/keepass2android/EntryActivityClasses/ViewImagePopupItem.cs b/src/keepass2android/EntryActivityClasses/ViewImagePopupItem.cs new file mode 100644 index 00000000..ec1f4f73 --- /dev/null +++ b/src/keepass2android/EntryActivityClasses/ViewImagePopupItem.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.Content.Res; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; + +namespace keepass2android.EntryActivityClasses +{ + internal class ViewImagePopupItem:IPopupMenuItem + { + private readonly string _key; + private readonly EntryActivity _entryActivity; + + public ViewImagePopupItem(string key, EntryActivity entryActivity) + { + _key = key; + _entryActivity = entryActivity; + } + public Drawable Icon + { + get + { + return _entryActivity.Resources.GetDrawable(Resource.Drawable.ic_picture); + } + } + + public string Text + { + get + { + return _entryActivity.Resources.GetString(Resource.String.ShowAttachedImage); + } + } + + public void HandleClick() + { + _entryActivity.ShowAttachedImage(_key); + + } + } +} \ No newline at end of file diff --git a/src/keepass2android/EntryEditActivity.cs b/src/keepass2android/EntryEditActivity.cs index 10c12f11..b860d0db 100644 --- a/src/keepass2android/EntryEditActivity.cs +++ b/src/keepass2android/EntryEditActivity.cs @@ -805,7 +805,7 @@ namespace keepass2android addBinaryButton.Enabled = !State.Entry.Binaries.Any(); addBinaryButton.Click += (sender, e) => { - Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false, false); + Util.ShowBrowseDialog(this, Intents.RequestCodeFileBrowseForBinary, false, true /*force OpenDocument if available, GetContent is not well support starting with Android 7 */); }; binariesGroup.AddView(addBinaryButton,layoutParams); diff --git a/src/keepass2android/ImageViewActivity.cs b/src/keepass2android/ImageViewActivity.cs new file mode 100644 index 00000000..f7b4433c --- /dev/null +++ b/src/keepass2android/ImageViewActivity.cs @@ -0,0 +1,330 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.Graphics; +using Android.Graphics.Drawables; +using Android.OS; +using Android.Runtime; +using Android.Util; +using Android.Views; +using Android.Widget; +using KeePassLib; +using KeePassLib.Utility; +using Object = Java.Lang.Object; + +namespace keepass2android +{ + + public class ZoomableImageView : ImageView + { + + private class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener + { + + private ZoomableImageView parent; + + public ScaleListener(ZoomableImageView parent) + { + this.parent = parent; + } + + public override bool OnScaleBegin(ScaleGestureDetector detector) + { + parent.mode = ZOOM; + return true; + } + + + public override bool OnScale(ScaleGestureDetector detector) + { + float scaleFactor = detector.ScaleFactor; + float newScale = parent.saveScale * scaleFactor; + if (newScale < parent.maxScale && newScale > parent.minScale) + { + parent.saveScale = newScale; + float width = parent.Width; + float height = parent.Height; + parent.right = (parent.originalBitmapWidth * parent.saveScale) - width; + parent.bottom = (parent.originalBitmapHeight * parent.saveScale) - height; + + float scaledBitmapWidth = parent.originalBitmapWidth * parent.saveScale; + float scaledBitmapHeight = parent.originalBitmapHeight * parent.saveScale; + + if (scaledBitmapWidth <= width || scaledBitmapHeight <= height) + { + parent.matrix.PostScale(scaleFactor, scaleFactor, width / 2, height / 2); + } + else + { + parent.matrix.PostScale(scaleFactor, scaleFactor, detector.FocusX, detector.FocusY); + } + } + return true; + } + + } + + const int NONE = 0; + const int DRAG = 1; + const int ZOOM = 2; + const int CLICK = 3; + + private int mode = NONE; + + private Matrix matrix = new Matrix(); + + private PointF last = new PointF(); + private PointF start = new PointF(); + private float minScale = 1.0f; + private float maxScale = 100.0f; + private float[] m; + + private float redundantXSpace, redundantYSpace; + private float saveScale = 1f; + private float right, bottom, originalBitmapWidth, originalBitmapHeight; + + private ScaleGestureDetector mScaleDetector; + protected ZoomableImageView(IntPtr javaReference, JniHandleOwnership transfer) + : base(javaReference, transfer) + { + } + public ZoomableImageView(Context context) + : base(context) + { + + init(context); + } + + public ZoomableImageView(Context context, IAttributeSet attrs) + : base(context, attrs) + { + init(context); + } + + public ZoomableImageView(Context context, IAttributeSet attrs, int defStyleAttr) + : base(context, attrs, defStyleAttr) + { + init(context); + } + + private void init(Context context) + { + base.Clickable = true; + mScaleDetector = new ScaleGestureDetector(context, new ScaleListener(this)); + m = new float[9]; + ImageMatrix = matrix; + SetScaleType(ScaleType.Matrix); + } + + + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + int bmHeight = getBmHeight(); + int bmWidth = getBmWidth(); + + float width = MeasuredWidth; + float height = MeasuredHeight; + //Fit to screen. + float scale = width > height ? height / bmHeight : width / bmWidth; + + matrix.SetScale(scale, scale); + saveScale = 1f; + + originalBitmapWidth = scale * bmWidth; + originalBitmapHeight = scale * bmHeight; + + // Center the image + redundantYSpace = (height - originalBitmapHeight); + redundantXSpace = (width - originalBitmapWidth); + + matrix.PostTranslate(redundantXSpace / 2, redundantYSpace / 2); + + ImageMatrix = matrix; + } + + + public override bool OnTouchEvent(MotionEvent event_) + { + mScaleDetector.OnTouchEvent(event_); + + matrix.GetValues(m); + float x = m[Matrix.MtransX]; + float y = m[Matrix.MtransY]; + PointF curr = new PointF(event_.GetX(), event_.GetY()); + Log.Debug("TOUCH", event_.Action.ToString(), " mode=" + mode); + switch (event_.Action) + { + //when one finger is touching + //set the mode to DRAG + case MotionEventActions.Down: + last.Set(event_.GetX(), event_.GetY()); + start.Set(last); + mode = DRAG; + break; + //when two fingers are touching + //set the mode to ZOOM + case MotionEventActions.Pointer2Down: + case MotionEventActions.PointerDown: + last.Set(event_.GetX(), event_.GetY()); + start.Set(last); + mode = ZOOM; + break; + //when a finger moves + //If mode is applicable move image + case MotionEventActions.Move: + //if the mode is ZOOM or + //if the mode is DRAG and already zoomed + if (mode == ZOOM || (mode == DRAG && saveScale > minScale)) + { + float deltaX = curr.X - last.X;// x difference + float deltaY = curr.Y - last.Y;// y difference + float scaleWidth = (float)System.Math.Round(originalBitmapWidth * saveScale);// width after applying current scale + float scaleHeight = (float)System.Math.Round(originalBitmapHeight * saveScale);// height after applying current scale + + bool limitX = false; + bool limitY = false; + + //if scaleWidth is smaller than the views width + //in other words if the image width fits in the view + //limit left and right movement + if (scaleWidth < Width && scaleHeight < Height) + { + // don't do anything + } + else if (scaleWidth < Width) + { + deltaX = 0; + limitY = true; + } + //if scaleHeight is smaller than the views height + //in other words if the image height fits in the view + //limit up and down movement + else if (scaleHeight < Height) + { + deltaY = 0; + limitX = true; + } + //if the image doesnt fit in the width or height + //limit both up and down and left and right + else + { + limitX = true; + limitY = true; + } + + if (limitY) + { + if (y + deltaY > 0) + { + deltaY = -y; + } + else if (y + deltaY < -bottom) + { + deltaY = -(y + bottom); + } + + } + + if (limitX) + { + if (x + deltaX > 0) + { + deltaX = -x; + } + else if (x + deltaX < -right) + { + deltaX = -(x + right); + } + + } + //move the image with the matrix + matrix.PostTranslate(deltaX, deltaY); + //set the last touch location to the current + last.Set(curr.X, curr.Y); + } + break; + //first finger is lifted + case MotionEventActions.Up: + mode = NONE; + int xDiff = (int)System.Math.Abs(curr.X - start.X); + int yDiff = (int)System.Math.Abs(curr.Y - start.Y); + if (xDiff < CLICK && yDiff < CLICK) + PerformClick(); + break; + // second finger is lifted + case MotionEventActions.Pointer2Up: + case MotionEventActions.PointerUp: + mode = NONE; + break; + } + ImageMatrix = matrix; + Invalidate(); + return true; + } + + public void setMaxZoom(float x) + { + maxScale = x; + } + + private int getBmWidth() + { + Drawable drawable = Drawable; + if (drawable != null) + { + return drawable.IntrinsicWidth; + } + return 0; + } + + private int getBmHeight() + { + Drawable drawable = Drawable; + if (drawable != null) + { + return drawable.IntrinsicHeight; + } + return 0; + } + } + [Activity(Label = "@string/app_name", ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.KeyboardHidden, + Theme = "@style/MyTheme_ActionBar")] + public class ImageViewActivity : LockCloseActivity + { + private ActivityDesign _activityDesign; + + public ImageViewActivity() + { + _activityDesign = new ActivityDesign(this); + } + + protected override void OnResume() + { + base.OnResume(); + _activityDesign.ReapplyTheme(); + } + + protected override void OnCreate(Bundle savedInstanceState) + { + _activityDesign.ApplyTheme(); + base.OnCreate(savedInstanceState); + SetContentView(Resource.Layout.ImageViewActivity); + var uuid = new PwUuid(MemUtil.HexStringToByteArray(Intent.GetStringExtra("EntryId"))); + string key = Intent.GetStringExtra("EntryKey"); + var binary = App.Kp2a.GetDb().Entries[uuid].Binaries.Get(key); + SupportActionBar.Title = key; + byte[] pbdata = binary.ReadData(); + + var bmp = BitmapFactory.DecodeByteArray(pbdata,0,pbdata.Length); + + FindViewById(Resource.Id.imageView).SetImageBitmap(bmp); + + } + } +} \ No newline at end of file diff --git a/src/keepass2android/PasswordActivity.cs b/src/keepass2android/PasswordActivity.cs index cf0335ef..b48a03d2 100644 --- a/src/keepass2android/PasswordActivity.cs +++ b/src/keepass2android/PasswordActivity.cs @@ -59,6 +59,8 @@ using Object = Java.Lang.Object; 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; @@ -67,6 +69,71 @@ 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", ConfigurationChanges = ConfigChanges.Orientation, LaunchMode = LaunchMode.SingleInstance, @@ -84,7 +151,8 @@ namespace keepass2android Otp = 2, OtpRecovery = 3, Challenge = 4, - ChalRecovery = 5 + ChalRecovery = 5, + ChallengeXC = 6 //KeepassXC compatible Challenge-Response } public const String KeyDefaultFilename = "defaultFileName"; @@ -102,6 +170,8 @@ namespace keepass2android private const string KeyProviderIdOtpRecovery = "KP2A-OTPSecret"; private const string KeyProviderIdChallenge = "KP2A-Chal"; private const string KeyProviderIdChallengeRecovery = "KP2A-ChalSecret"; + private const string KeyProviderIdChallengeXC = "KP2A-ChalXC"; + private const int RequestCodePrepareDbFile = 1000; private const int RequestCodePrepareOtpAuxFile = 1001; @@ -111,9 +181,11 @@ namespace keepass2android private const int RequestCodeSelectAuxFile = 1005; - private Task _loadDbTask; + private Task _loadDbFileTask; private bool _loadDbTaskOffline; //indicate if preloading was started with offline mode + private ChallengeXCKey _currentlyWaitingKey; + private IOConnectionInfo _ioConnection; private String _keyFileOrProvider; bool _showPassword; @@ -140,6 +212,8 @@ namespace keepass2android return KeyProviders.Challenge; if (_keyFileOrProvider == KeyProviderIdChallengeRecovery) return KeyProviders.ChalRecovery; + if (_keyFileOrProvider == KeyProviderIdChallengeXC) + return KeyProviders.ChallengeXC; return KeyProviders.KeyFile; } } @@ -303,8 +377,8 @@ namespace keepass2android Handler handler = new Handler(); OnFinish onFinish = new AfterLoad(handler, this); _performingLoad = true; - LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbTask, compositeKey, _keyFileOrProvider, onFinish); - _loadDbTask = null; // prevent accidental re-use + LoadDb task = new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, _keyFileOrProvider, onFinish); + _loadDbFileTask = null; // prevent accidental re-use new ProgressTask(App.Kp2a, this, task).Run(); } @@ -364,10 +438,19 @@ namespace keepass2android { try { - _challengeProv = new KeeChallengeProv(); byte[] challengeResponse = data.GetByteArrayExtra("response"); - _challengeSecret = _challengeProv.GetSecret(_chalInfo, challengeResponse); - Array.Clear(challengeResponse, 0, challengeResponse.Length); + 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) { @@ -1250,6 +1333,9 @@ namespace keepass2android case 5: _keyFileOrProvider = KeyProviderIdChallengeRecovery; break; + case 6: + _keyFileOrProvider = KeyProviderIdChallengeXC; + break; default: throw new Exception("Unexpected position " + args.Position + " / " + ((ICursor) ((AdapterView) sender).GetItemAtPosition(args.Position)).GetString(1)); @@ -1336,6 +1422,9 @@ namespace keepass2android case KeyProviders.ChalRecovery: enabled = FindViewById(Resource.Id.pass_otpsecret).Text != ""; break; + case KeyProviders.ChallengeXC: + enabled = true; + break; case KeyProviders.Challenge: enabled = _challengeSecret != null; break; @@ -1378,6 +1467,7 @@ namespace keepass2android private void PerformLoadDatabase() { + _currentlyWaitingKey = null; //put loading into background thread to allow loading the key file (potentially over network) new SimpleLoadingDialog(this, GetString(Resource.String.loading), true, () => @@ -1403,15 +1493,15 @@ namespace keepass2android if (App.Kp2a.OfflineMode != _loadDbTaskOffline) { - if (_loadDbTask == null) - throw new NullPointerException("_loadDbTask"); + if (_loadDbFileTask == null) + throw new NullPointerException("_loadDbFileTask"); if (App.Kp2a == null) throw new NullPointerException("App.Kp2a"); //keep the loading result if we loaded in online-mode (now offline) and the task is completed - if (!App.Kp2a.OfflineMode || !_loadDbTask.IsCompleted) + if (!App.Kp2a.OfflineMode || !_loadDbFileTask.IsCompleted) { //discard the pre-loading task - _loadDbTask = null; + _loadDbFileTask = null; } } @@ -1426,10 +1516,10 @@ namespace keepass2android OnFinish onFinish = new AfterLoad(handler, this); _performingLoad = true; LoadDb task = (KeyProviderType == KeyProviders.Otp) - ? new SaveOtpAuxFileAndLoadDb(App.Kp2a, _ioConnection, _loadDbTask, compositeKey, _keyFileOrProvider, + ? new SaveOtpAuxFileAndLoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, _keyFileOrProvider, onFinish, this) - : new LoadDb(App.Kp2a, _ioConnection, _loadDbTask, compositeKey, _keyFileOrProvider, onFinish); - _loadDbTask = null; // prevent accidental re-use + : new LoadDb(App.Kp2a, _ioConnection, _loadDbFileTask, compositeKey, _keyFileOrProvider, onFinish); + _loadDbFileTask = null; // prevent accidental re-use SetNewDefaultFile(); @@ -1511,6 +1601,11 @@ namespace keepass2android else if (KeyProviderType == KeyProviders.Challenge) { compositeKey.AddUserKey(new KcpCustomKey(KeeChallengeProv.Name, _challengeSecret, true)); + } + else if (KeyProviderType == KeyProviders.ChallengeXC) + { + _currentlyWaitingKey = new ChallengeXCKey(this, RequestCodeChallengeYubikey); + compositeKey.AddUserKey(_currentlyWaitingKey); } return true; } @@ -1776,7 +1871,7 @@ namespace keepass2android } //use !IsFinishing to make sure we're not starting another activity when we're already finishing (e.g. due to TaskComplete in OnActivityResult) - //use !performingLoad to make sure we're not already loading the database (after ActivityResult from File-Prepare-Activity; this would cause _loadDbTask to exist when we reload later!) + //use !performingLoad to make sure we're not already loading the database (after ActivityResult from File-Prepare-Activity; this would cause _loadDbFileTask to exist when we reload later!) if ( !IsFinishing && !_performingLoad) { if (App.Kp2a.DatabaseIsUnlocked) @@ -1802,10 +1897,10 @@ namespace keepass2android //database not yet loaded. //check if pre-loading is enabled but wasn't started yet: - if (_loadDbTask == null && _prefs.GetBoolean(GetString(Resource.String.PreloadDatabaseEnabled_key), true)) + if (_loadDbFileTask == null && _prefs.GetBoolean(GetString(Resource.String.PreloadDatabaseEnabled_key), true)) { // Create task to kick off file loading while the user enters the password - _loadDbTask = Task.Factory.StartNew(PreloadDbFile); + _loadDbFileTask = Task.Factory.StartNew(PreloadDbFile); _loadDbTaskOffline = App.Kp2a.OfflineMode; } } diff --git a/src/keepass2android/Resources/drawable-mdpi/ic_picture.png b/src/keepass2android/Resources/drawable-mdpi/ic_picture.png new file mode 100644 index 00000000..8c662588 Binary files /dev/null and b/src/keepass2android/Resources/drawable-mdpi/ic_picture.png differ diff --git a/src/keepass2android/Resources/drawable-xhdpi/ic_picture.png b/src/keepass2android/Resources/drawable-xhdpi/ic_picture.png new file mode 100644 index 00000000..c30ff898 Binary files /dev/null and b/src/keepass2android/Resources/drawable-xhdpi/ic_picture.png differ diff --git a/src/keepass2android/Resources/layout/ImageViewActivity.xml b/src/keepass2android/Resources/layout/ImageViewActivity.xml new file mode 100644 index 00000000..3eb2b69e --- /dev/null +++ b/src/keepass2android/Resources/layout/ImageViewActivity.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/src/keepass2android/Resources/values-de/strings.xml b/src/keepass2android/Resources/values-de/strings.xml index d599ad85..ef956307 100644 --- a/src/keepass2android/Resources/values-de/strings.xml +++ b/src/keepass2android/Resources/values-de/strings.xml @@ -847,6 +847,7 @@ Erstes öffentliches Release Kennwort + OTP Secret (Recovery-Modus) Passwort + Challenge-Response Passwort + Challenge-Response-Secret (Recovery-Modus) + Passwort + Challenge-Response für KeepassXC-Datenbank Fehler bei Zertifikatsvalidierung ignorieren diff --git a/src/keepass2android/Resources/values/config.xml b/src/keepass2android/Resources/values/config.xml index 1eef5c9f..f2351554 100644 --- a/src/keepass2android/Resources/values/config.xml +++ b/src/keepass2android/Resources/values/config.xml @@ -86,7 +86,9 @@ TrayTotp_SettingsField_key TrayTotp_SeedField_key TrayTotp_prefs_key - + DebugLog_key + DebugLog_prefs_key + DebugLog_send password_access_prefs_key security_prefs_key @@ -121,7 +123,7 @@ about_prefs market://details?id= - https://keepass2android.codeplex.com/workitem/list/basic + https://github.com/PhilippC/keepass2android/issues http://crowdin.net/project/keepass2android ShowCopyToClipboardNotification diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index e71321ce..c2a6cbca 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -6,7 +6,7 @@ Keepass2Android is a password manager providing read/write access to KeePass 2.x databases on Android. The User Interface is based on a port of KeepassDroid developed by Brian Pellin. Code for database operations is based on KeePass by Dominik Reichl. The Android robot is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons 3.0 Attribution License. SFTP support is implemented using the JSch library under BSD licence, created by JCraft, Inc. - The Hammer Icon is Created by John Caserta from the Noun Project. The Penguin Icon is Created by Adriano Emerick from the Noun Project. The Feather icon is Created by Jon Testa from the Noun Project. The Apple icon is Created by Ava Rowell from the Noun Project. + The Hammer Icon is Created by John Caserta from the Noun Project. The Penguin Icon is Created by Adriano Emerick from the Noun Project. The Feather icon is Created by Jon Testa from the Noun Project. The Apple icon is Created by Ava Rowell from the Noun Project. The Picture icon is from https://icons8.com/icon/5570/Picture. Accept Deny Add entry @@ -306,7 +306,9 @@ Please select where to save the attachment. Save to SD card Save to cache and open + Show with internal image viewer + Saved file to %1$s. Could not save attachment to %1$s. @@ -550,8 +552,16 @@ Enter the field name of the settings field for TrayTotp here. TrayTotp + Log-File for Debugging + Use log file + Write app output to a local log file + Send debug log... + + - Loading… + + + Loading… Plug-ins Package name: @@ -999,6 +1009,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/Resources/xml/preferences.xml b/src/keepass2android/Resources/xml/preferences.xml index 9786aeab..aaf82f28 100644 --- a/src/keepass2android/Resources/xml/preferences.xml +++ b/src/keepass2android/Resources/xml/preferences.xml @@ -510,10 +510,27 @@ android:title="@string/TrayTotp_SettingsField_title" android:key="@string/TrayTotp_SettingsField_key" /> - - - + + + + + + + + diff --git a/src/keepass2android/Totp/Base32.cs b/src/keepass2android/Totp/Base32.cs new file mode 100644 index 00000000..e5d83daa --- /dev/null +++ b/src/keepass2android/Totp/Base32.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace KeeTrayTOTP.Libraries +{ + /// + /// Utility to deal with Base32 encoding and decoding. + /// + /// + /// http://tools.ietf.org/html/rfc4648 + /// + public static class Base32 + { + /// + /// The number of bits in a base32 encoded character. + /// + private const int encodedBitCount = 5; + /// + /// The number of bits in a byte. + /// + private const int byteBitCount = 8; + /// + /// A string containing all of the base32 characters in order. + /// This allows a simple indexof or [index] to convert between + /// a numeric value and an encoded character and vice versa. + /// + private const string encodingChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + /// + /// Takes a block of data and converts it to a base 32 encoded string. + /// + /// Input data. + /// base 32 string. + public static string Encode(byte[] data) + { + if (data == null) + throw new ArgumentNullException(); + if (data.Length == 0) + throw new ArgumentNullException(); + + // The output character count is calculated in 40 bit blocks. That is because the least + // common blocks size for both binary (8 bit) and base 32 (5 bit) is 40. Padding must be used + // to fill in the difference. + int outputCharacterCount = (int)Math.Ceiling(data.Length / (decimal)encodedBitCount) * byteBitCount; + char[] outputBuffer = new char[outputCharacterCount]; + + byte workingValue = 0; + short remainingBits = encodedBitCount; + int currentPosition = 0; + + foreach (byte workingByte in data) + { + workingValue = (byte)(workingValue | (workingByte >> (byteBitCount - remainingBits))); + outputBuffer[currentPosition++] = encodingChars[workingValue]; + + if (remainingBits <= byteBitCount - encodedBitCount) + { + workingValue = (byte)((workingByte >> (byteBitCount - encodedBitCount - remainingBits)) & 31); + outputBuffer[currentPosition++] = encodingChars[workingValue]; + remainingBits += encodedBitCount; + } + + remainingBits -= byteBitCount - encodedBitCount; + workingValue = (byte)((workingByte << remainingBits) & 31); + } + + // If we didn't finish, write the last current working char. + if (currentPosition != outputCharacterCount) + outputBuffer[currentPosition++] = encodingChars[workingValue]; + + // RFC 4648 specifies that padding up to the end of the next 40 bit block must be provided + // Since the outputCharacterCount does account for the paddingCharacters, fill it out. + while (currentPosition < outputCharacterCount) + { + // The RFC defined paddinc char is '='. + outputBuffer[currentPosition++] = '='; + } + + return new string(outputBuffer); + } + + /// + /// Takes a base 32 encoded value and converts it back to binary data. + /// + /// Base 32 encoded string. + /// Binary data. + public static byte[] Decode(string base32) + { + if (string.IsNullOrEmpty(base32)) + throw new ArgumentNullException(); + + var unpaddedBase32 = base32.ToUpperInvariant().TrimEnd('='); + foreach (var c in unpaddedBase32) + { + if (encodingChars.IndexOf(c) < 0) + throw new ArgumentException("Base32 contains illegal characters."); + } + + // we have already removed the padding so this will tell us how many actual bytes there should be. + int outputByteCount = unpaddedBase32.Length * encodedBitCount / byteBitCount; + byte[] outputBuffer = new byte[outputByteCount]; + + byte workingByte = 0; + short bitsRemaining = byteBitCount; + int mask = 0; + int arrayIndex = 0; + + foreach (char workingChar in unpaddedBase32) + { + int encodedCharacterNumericValue = encodingChars.IndexOf(workingChar); + + if (bitsRemaining > encodedBitCount) + { + mask = encodedCharacterNumericValue << (bitsRemaining - encodedBitCount); + workingByte = (byte)(workingByte | mask); + bitsRemaining -= encodedBitCount; + } + else + { + mask = encodedCharacterNumericValue >> (encodedBitCount - bitsRemaining); + workingByte = (byte)(workingByte | mask); + outputBuffer[arrayIndex++] = workingByte; + workingByte = (byte)(encodedCharacterNumericValue << (byteBitCount - encodedBitCount + bitsRemaining)); + bitsRemaining += byteBitCount - encodedBitCount; + } + } + + return outputBuffer; + } + } +} diff --git a/src/keepass2android/Totp/KeeOtpPluginAdapter.cs b/src/keepass2android/Totp/KeeOtpPluginAdapter.cs index d6c19ae2..36d6db37 100644 --- a/src/keepass2android/Totp/KeeOtpPluginAdapter.cs +++ b/src/keepass2android/Totp/KeeOtpPluginAdapter.cs @@ -52,8 +52,8 @@ namespace PluginTOTP res.TotpSeed = parameters[KeyParameter]; - res.Duration = GetIntOrDefault(parameters, StepParameter, 30); - res.Length = GetIntOrDefault(parameters, SizeParameter, 6); + res.Duration = GetIntOrDefault(parameters, StepParameter, 30).ToString(); + res.Length = GetIntOrDefault(parameters, SizeParameter, 6).ToString(); res.IsTotpEnry = true; return res; diff --git a/src/keepass2android/Totp/TOTPEncoder.cs b/src/keepass2android/Totp/TOTPEncoder.cs new file mode 100644 index 00000000..cac3587b --- /dev/null +++ b/src/keepass2android/Totp/TOTPEncoder.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace KeeTrayTOTP.Libraries +{ + class TOTPEncoder + { + /// + /// Character set for authenticator code + /// + private static readonly char[] STEAMCHARS = new char[] { + '2', '3', '4', '5', '6', '7', '8', '9', 'B', 'C', + 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', + 'R', 'T', 'V', 'W', 'X', 'Y'}; + + + private static uint OTP2UInt(byte[] totp) + { + uint fullcode = BitConverter.ToUInt32(totp, 0) & 0x7fffffff; + + return fullcode; + } + + public static readonly Func rfc6238 = (byte[] bytes, int length) => + { + uint fullcode = TOTPEncoder.OTP2UInt(bytes); + uint mask = (uint)Math.Pow(10, length); + return (fullcode % mask).ToString(new string('0', length)); + + }; + + public static readonly Func steam = (byte[] bytes, int length) => + { + uint fullcode = TOTPEncoder.OTP2UInt(bytes); + + StringBuilder code = new StringBuilder(); + for (var i = 0; i < length; i++) + { + code.Append(STEAMCHARS[fullcode % STEAMCHARS.Length]); + fullcode /= (uint)STEAMCHARS.Length; + } + + return code.ToString(); + }; + + + } +} diff --git a/src/keepass2android/Totp/TOTPProvider.cs b/src/keepass2android/Totp/TOTPProvider.cs new file mode 100644 index 00000000..a74a4116 --- /dev/null +++ b/src/keepass2android/Totp/TOTPProvider.cs @@ -0,0 +1,264 @@ +using System; +using System.Security; +using System.Security.Cryptography; + +namespace KeeTrayTOTP.Libraries +{ + /// + /// Provides Time-based One Time Passwords RFC 6238. + /// + public class TOTPProvider + { + /// + /// Time reference for TOTP generation. + /// + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Duration of generation of each totp, in seconds. + /// + private int duration; + public int Duration + { + get + { + return this.duration; + } + set + { + if (!(value > 0)) throw new Exception("Invalid Duration."); //Throws an exception if the duration is invalid as the class cannot work without it. + this.duration = value; //Defines variable from argument. + } + } + + /// + /// Length of the generated totp. + /// + private int length; + public int Length + { + get + { + return this.length; + } + set + { + //Throws an exception if the length is invalid as the class cannot work without it. + if (value < 4 || value > 8) throw new Exception("Invalid Length."); + this.length = value; //Defines variable from argument. + } + + } + + + /// + /// TOTP Encoder. + /// + private Func encoder; + public Func Encoder + { + get + { + return this.encoder; + } + set + { + this.encoder = value; //Defines variable from argument. + } + } + + /// + /// Sets the time span that is used to match the server's UTC time to ensure accurate generation of Time-based One Time Passwords. + /// + private TimeSpan timeCorrection; + public TimeSpan TimeCorrection + { + get + { + return this.timeCorrection; + } + set + { + this.timeCorrection = value; //Defines variable from argument. + } + } + + private bool timeCorrectionError; + public bool TimeCorrectionError + { + get + { + return this.timeCorrectionError; + } + } + + /// + /// Instanciates a new TOTP_Generator. + /// + /// Duration of generation of each totp, in seconds. + /// Length of the generated totp. + /// The output encoder. + /*public TOTPProvider(int initDuration, int initLength, Func initEncoder) + { + this.Duration = initDuration; + this.Length = initLength; + this.encoder = initEncoder; + this.TimeCorrection = TimeSpan.Zero; + }*/ + + /// + /// Instanciates a new TOTP_Generator. + /// + /// Saved Settings. + public TOTPProvider(string[] Settings) + { + this.duration = Convert.ToInt16(Settings[0]); + + if (Settings[1] == "S") + { + this.length = 5; + this.encoder = TOTPEncoder.steam; + } + else + { + this.length = Convert.ToInt16(Settings[1]); + this.encoder = TOTPEncoder.rfc6238; + } + + if(Settings.Length > 2 && Settings[2] != String.Empty) + { + + { + this.TimeCorrection = TimeSpan.Zero; + this.timeCorrectionError = false; + } + } + else + { + this.TimeCorrection = TimeSpan.Zero; + } + + + } + + /// + /// Returns current time with correction int UTC format. + /// + public DateTime Now + { + get + { + return DateTime.UtcNow - timeCorrection; //Computes current time minus time correction giving the corrected time. + } + } + + /// + /// Returns the time remaining before counter incrementation. + /// + public int Timer + { + get + { + var n = (duration - (int)((Now - UnixEpoch).TotalSeconds % duration)); //Computes the seconds left before counter incrementation. + return n == 0 ? duration : n; //Returns timer value from 30 to 1. + } + } + + /// + /// Returns number of intervals that have elapsed. + /// + public long Counter + { + get + { + var ElapsedSeconds = (long)Math.Floor((Now - UnixEpoch).TotalSeconds); //Compute current counter for current time. + return ElapsedSeconds / duration; //Applies specified interval to computed counter. + } + } + + /// + /// Converts an unsigned integer to binary data. + /// + /// Unsigned Integer. + /// Binary data. + private byte[] GetBytes(ulong n) + { + byte[] b = new byte[8]; //Math. + b[0] = (byte)(n >> 56); //Math. + b[1] = (byte)(n >> 48); //Math. + b[2] = (byte)(n >> 40); //Math. + b[3] = (byte)(n >> 32); //Math. + b[4] = (byte)(n >> 24); //Math. + b[5] = (byte)(n >> 16); //Math. + b[6] = (byte)(n >> 8); //Math. + b[7] = (byte)(n); //Math. + return b; + } + + /// + /// Generate a TOTP using provided binary data. + /// + /// Binary data. + /// Time-based One Time Password encoded byte array. + public byte[] Generate(byte[] key) + { + System.Security.Cryptography.HMACSHA1 hmac = new System.Security.Cryptography.HMACSHA1(key, true); //Instanciates a new hash provider with a key. + byte[] hash = hmac.ComputeHash(GetBytes((ulong)Counter)); //Generates hash from key using counter. + hmac.Clear(); //Clear hash instance securing the key. + + /*int binary = //Math. + ((hash[offset] & 0x7f) << 24) //Math. + | ((hash[offset + 1] & 0xff) << 16) //Math. + | ((hash[offset + 2] & 0xff) << 8) //Math. + | (hash[offset + 3] & 0xff); //Math. + + int password = binary % (int)Math.Pow(10, length); //Math.*/ + + int offset = hash[hash.Length - 1] & 0x0f; //Math. + byte[] totp = { hash[offset + 3], hash[offset + 2], hash[offset + 1], hash[offset] }; + return totp; + + /* + return password.ToString(new string('0', length)); //Math.*/ + } + + /// + /// Generate a TOTP using provided binary data. + /// + /// Key in String Format. + /// Time-based One Time Password encoded byte array. + public string Generate(string key) + { + byte[] bkey = Base32.Decode(key); + return this.GenerateByByte(bkey); + } + + /// + /// Generate a TOTP using provided binary data. + /// + /// Binary data. + /// Time-based One Time Password encoded byte array. + public string GenerateByByte(byte[] key) + { + + HMACSHA1 hmac = new HMACSHA1(key, true); //Instanciates a new hash provider with a key. + + byte[] codeInterval = BitConverter.GetBytes((ulong)Counter); + + if (BitConverter.IsLittleEndian) + Array.Reverse(codeInterval); + + byte[] hash = hmac.ComputeHash(codeInterval); //Generates hash from key using counter. + hmac.Clear(); //Clear hash instance securing the key. + int start = hash[hash.Length - 1] & 0xf; + byte[] totp = new byte[4]; + + Array.Copy(hash, start, totp, 0, 4); + if (BitConverter.IsLittleEndian) + Array.Reverse(totp); + + return this.encoder(totp, length); + } + + } +} \ No newline at end of file diff --git a/src/keepass2android/Totp/TimeCorrectionProvider.cs b/src/keepass2android/Totp/TimeCorrectionProvider.cs new file mode 100644 index 00000000..8ffab4b3 --- /dev/null +++ b/src/keepass2android/Totp/TimeCorrectionProvider.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace KeeTrayTOTP.Libraries +{ + /// + /// Provides time correction for Time-based One Time Passwords that require accurate DateTime syncronisation with server. + /// + public class TimeCorrectionProvider + { + /// + /// Timer providing the delay between each time correction check. + /// + private System.Timers.Timer _Timer; + + /// + /// Thread which handles the time correction check. + /// + private System.Threading.Thread Task; + + private bool _Enable; + /// + /// Defines weither or not the class will attempt to get time correction from the server. + /// + public bool Enable { get { return _Enable; } set { _Enable = value; _Timer.Enabled = value; } } + + private static int _Interval = 60; + /// + /// Gets or sets the interval in minutes between each online checks for time correction. + /// + /// Time + public static int Interval { get { return _Interval; } set { _Interval = value; } } + private long _IntervalStretcher; + + private volatile string _Url; + /// + /// Returns the URL this instance is using to checks for time correction. + /// + public string Url { get { return _Url; } } + + private TimeSpan _TimeCorrection; + /// + /// Returns the time span between server UTC time and this computer's UTC time of the last check for time correction. + /// + public TimeSpan TimeCorrection { get { return _TimeCorrection; } } + + private DateTime _LastUpdateDateTime; + /// + /// Returns the date and time in universal format of the last online check for time correction. + /// + public DateTime LastUpdateDateTime { get { return _LastUpdateDateTime; } } + + private bool _LastUpdateSucceded = false; + /// + /// Returns true if the last check for time correction was successful. + /// + public bool LastUpdateSucceded { get { return _LastUpdateSucceded; } } + + /// + /// Instanciates a new TOTP_TimeCorrection using the specified URL to contact the server. + /// + /// URL of the server to get check. + /// Enable or disable the time correction check. + public TimeCorrectionProvider(string Url, bool Enable = true) + { + if (Url == string.Empty) throw new Exception("Invalid URL."); //Throws exception if the URL is invalid as the class cannot work without it. + _Url = Url; //Defines variable from argument. + _Enable = Enable; //Defines variable from argument. + _LastUpdateDateTime = DateTime.MinValue; //Defines variable from non-constant default value. + _TimeCorrection = TimeSpan.Zero; //Defines variable from non-constant default value. + _Timer = new System.Timers.Timer(); //Instanciates timer. + _Timer.Elapsed += Timer_Elapsed; //Handles the timer event + _Timer.Interval = 1000; //Defines the timer interval to 1 seconds. + _Timer.Enabled = _Enable; //Defines the timer to run if the class is initially enabled. + Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task. + if (_Enable) Task.Start(); //Starts the new thread if the class is initially enabled. + } + + /// + /// Task that occurs every time the timer's interval has elapsed. + /// + private void Timer_Elapsed(object sender, EventArgs e) + { + _IntervalStretcher++; //Increments timer. + if (_IntervalStretcher >= (60 * _Interval)) //Checks if the specified delay has been reached. + { + _IntervalStretcher = 0; //Resets the timer. + Task_Do(); //Attempts to run a new task + } + } + + /// + /// Instanciates a new task and starts it. + /// + /// Informs if reinstanciation of the task has succeeded or not. Will fail if the thread is still active from a previous time correction check. + private bool Task_Do() + { + if (!Task.IsAlive) //Checks if the task is still running. + { + Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task. + Task.Start(); //Starts the new thread. + return true; //Informs if successful + } + return false; //Informs if failed + } + + /// + /// Event that occurs when the timer has reached the required value. Attempts to get time correction from the server. + /// + private void Task_Thread() + { + try + { + var WebClient = new System.Net.WebClient(); //WebClient to connect to server. + WebClient.DownloadData(_Url); //Downloads the server's page using HTTP or HTTPS. + var DateHeader = WebClient.ResponseHeaders.Get("Date"); //Gets the date from the HTTP header of the downloaded page. + _TimeCorrection = DateTime.UtcNow - DateTime.Parse(DateHeader, System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat).ToUniversalTime(); //Compares the downloaded date to the systems date giving us a timespan. + _LastUpdateSucceded = true; //Informs that the date check has succeeded. + } + catch (Exception) + { + _LastUpdateSucceded = false; //Informs that the date check has failed. + } + _LastUpdateDateTime = DateTime.Now; //Informs when the last update has been attempted (succeeded or not). + } + + /// + /// Perform a time correction check, may a few seconds. + /// + /// Resets the timer to 0. Occurs even if the attempt to attempt a new time correction fails. + /// Attempts to get time correction even if disabled. + /// Informs if the time correction check was attempted or not. Will fail if the thread is still active from a previous time correction check. + public bool CheckNow(bool ResetTimer = true, bool ForceCheck = false) + { + if (ResetTimer) //Checks if the timer should be reset. + { + _IntervalStretcher = 0; //Resets the timer. + } + if (ForceCheck || _Enable) //Checks if this check is forced or if time correction is enabled. + { + return Task_Do(); //Attempts to run a new task and informs if attempt to attemp is a success of fail + } + return false; //Informs if not attempted to attempt + } + } +} diff --git a/src/keepass2android/Totp/TotpData.cs b/src/keepass2android/Totp/TotpData.cs index 43896512..84b34296 100644 --- a/src/keepass2android/Totp/TotpData.cs +++ b/src/keepass2android/Totp/TotpData.cs @@ -1,11 +1,24 @@ +using System.Collections.Generic; + namespace PluginTOTP { struct TotpData { public bool IsTotpEnry { get; set; } public string TotpSeed { get; set; } - public int Duration { get; set; } - public int Length { get; set; } + public string Duration { get; set; } + public string Length { get; set; } + public string Url { get; set; } + public string[] Settings + { + get + { + List settings = new List() { Duration, Length}; + if (Url != null) + settings.Add(Url); + return settings.ToArray(); + } + } } } \ No newline at end of file diff --git a/src/keepass2android/Totp/Totp_Client.cs b/src/keepass2android/Totp/Totp_Client.cs deleted file mode 100644 index 9ac5d8cc..00000000 --- a/src/keepass2android/Totp/Totp_Client.cs +++ /dev/null @@ -1,387 +0,0 @@ -using System; - -namespace OtpProviderClient -{ - /// - /// Provides Time-based One Time Passwords RFC 6238. - /// - public class Totp_Provider - { - /// - /// Time reference for TOTP generation. - /// - private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - /// - /// Duration of generation of each totp, in seconds. - /// - private int _Duration; - - /// - /// Length of the generated totp. - /// - private int _Length; - - private TimeSpan _TimeCorrection; - /// - /// Sets the time span that is used to match the server's UTC time to ensure accurate generation of Time-based One Time Passwords. - /// - public TimeSpan TimeCorrection { set { _TimeCorrection = value; } } - - /// - /// Instanciates a new Totp_Generator. - /// - /// Duration of generation of each totp, in seconds. - /// Length of the generated totp. - public Totp_Provider(int Duration, int Length) - { - if (!(Duration > 0)) throw new Exception("Invalid Duration."); //Throws an exception if the duration is invalid as the class cannot work without it. - _Duration = Duration; //Defines variable from argument. - if (!((Length > 5) && (Length < 9))) throw new Exception("Invalid Length."); //Throws an exception if the length is invalid as the class cannot work without it. - _Length = Length; //Defines variable from argument. - _TimeCorrection = TimeSpan.Zero; //Defines variable from non-constant default value. - } - - /// - /// Returns current time with correction int UTC format. - /// - public DateTime Now - { - get - { - return DateTime.UtcNow - _TimeCorrection; //Computes current time minus time correction giving the corrected time. - } - } - - /// - /// Returns the time remaining before counter incrementation. - /// - public int Timer - { - get - { - var n = (_Duration - (int)((Now - UnixEpoch).TotalSeconds % _Duration)); //Computes the seconds left before counter incrementation. - return n == 0 ? _Duration : n; //Returns timer value from 30 to 1. - } - } - - /// - /// Returns number of intervals that have elapsed. - /// - public long Counter - { - get - { - var ElapsedSeconds = (long)Math.Floor((Now - UnixEpoch).TotalSeconds); //Compute current counter for current time. - return ElapsedSeconds / _Duration; //Applies specified interval to computed counter. - } - } - - /// - /// Converts an unsigned integer to binary data. - /// - /// Unsigned Integer. - /// Binary data. - private byte[] GetBytes(ulong n) - { - byte[] b = new byte[8]; //Math. - b[0] = (byte)(n >> 56); //Math. - b[1] = (byte)(n >> 48); //Math. - b[2] = (byte)(n >> 40); //Math. - b[3] = (byte)(n >> 32); //Math. - b[4] = (byte)(n >> 24); //Math. - b[5] = (byte)(n >> 16); //Math. - b[6] = (byte)(n >> 8); //Math. - b[7] = (byte)(n); //Math. - return b; - } - - /// - /// Generate a Totp using provided binary data. - /// - /// Binary data. - /// Time-based One Time Password. - public string Generate(byte[] key) - { - System.Security.Cryptography.HMACSHA1 hmac = new System.Security.Cryptography.HMACSHA1(key, true); //Instanciates a new hash provider with a key. - byte[] hash = hmac.ComputeHash(GetBytes((ulong)Counter)); //Generates hash from key using counter. - hmac.Clear(); //Clear hash instance securing the key. - - int offset = hash[hash.Length - 1] & 0xf; //Math. - int binary = //Math. - ((hash[offset] & 0x7f) << 24) //Math. - | ((hash[offset + 1] & 0xff) << 16) //Math. - | ((hash[offset + 2] & 0xff) << 8) //Math. - | (hash[offset + 3] & 0xff); //Math. - - int password = binary % Convert.ToInt32(Math.Pow(10, _Length)); //Math. - return password.ToString(new string('0', _Length)); //Math. - } - } - - /// - /// Provides time correction for Time-based One Time Passwords that require accurate DateTime syncronisation with server. - /// - public class TimeCorrection_Provider - { - /// - /// Timer providing the delay between each time correction check. - /// - private System.Timers.Timer _Timer; - - /// - /// Thread which handles the time correction check. - /// - private System.Threading.Thread Task; - - private bool _Enable; - /// - /// Defines weither or not the class will attempt to get time correction from the server. - /// - public bool Enable { get { return _Enable; } set { _Enable = value; _Timer.Enabled = value; } } - - private static int _Interval = 60; - /// - /// Gets or sets the interval in minutes between each online checks for time correction. - /// - /// Time - public static int Interval { get { return _Interval; } set { _Interval = value; } } - private long _IntervalStretcher; - - private volatile string _Url; - /// - /// Returns the URL this instance is using to checks for time correction. - /// - public string Url { get { return _Url; } } - - private TimeSpan _TimeCorrection; - /// - /// Returns the time span between server UTC time and this computer's UTC time of the last check for time correction. - /// - public TimeSpan TimeCorrection { get { return _TimeCorrection; } } - - private DateTime _LastUpdateDateTime; - /// - /// Returns the date and time in universal format of the last online check for time correction. - /// - public DateTime LastUpdateDateTime { get { return _LastUpdateDateTime; } } - - private bool _LastUpdateSucceded = false; - /// - /// Returns true if the last check for time correction was successful. - /// - public bool LastUpdateSucceded { get { return _LastUpdateSucceded; } } - - /// - /// Instanciates a new Totp_TimeCorrection using the specified URL to contact the server. - /// - /// URL of the server to get check. - /// Enable or disable the time correction check. - public TimeCorrection_Provider(string Url, bool Enable = true) - { - if (Url == string.Empty) throw new Exception("Invalid URL."); //Throws exception if the URL is invalid as the class cannot work without it. - _Url = Url; //Defines variable from argument. - _Enable = Enable; //Defines variable from argument. - _LastUpdateDateTime = DateTime.MinValue; //Defines variable from non-constant default value. - _TimeCorrection = TimeSpan.Zero; //Defines variable from non-constant default value. - _Timer = new System.Timers.Timer(); //Instanciates timer. - _Timer.Elapsed += Timer_Elapsed; //Handles the timer event - _Timer.Interval = 1000; //Defines the timer interval to 1 seconds. - _Timer.Enabled = _Enable; //Defines the timer to run if the class is initially enabled. - Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task. - if (_Enable) Task.Start(); //Starts the new thread if the class is initially enabled. - } - - /// - /// Task that occurs every time the timer's interval has elapsed. - /// - private void Timer_Elapsed(object sender, EventArgs e) - { - _IntervalStretcher++; //Increments timer. - if (_IntervalStretcher >= (60 * _Interval)) //Checks if the specified delay has been reached. - { - _IntervalStretcher = 0; //Resets the timer. - Task_Do(); //Attempts to run a new task - } - } - - /// - /// Instanciates a new task and starts it. - /// - /// Informs if reinstanciation of the task has succeeded or not. Will fail if the thread is still active from a previous time correction check. - private bool Task_Do() - { - if (!Task.IsAlive) //Checks if the task is still running. - { - Task = new System.Threading.Thread(Task_Thread); //Instanciate a new task. - Task.Start(); //Starts the new thread. - return true; //Informs if successful - } - return false; //Informs if failed - } - - /// - /// Event that occurs when the timer has reached the required value. Attempts to get time correction from the server. - /// - private void Task_Thread() - { - try - { - var WebClient = new System.Net.WebClient(); //WebClient to connect to server. - WebClient.DownloadData(_Url); //Downloads the server's page using HTTP or HTTPS. - var DateHeader = WebClient.ResponseHeaders.Get("Date"); //Gets the date from the HTTP header of the downloaded page. - _TimeCorrection = DateTime.UtcNow - DateTime.Parse(DateHeader, System.Globalization.CultureInfo.InvariantCulture.DateTimeFormat).ToUniversalTime(); //Compares the downloaded date to the systems date giving us a timespan. - _LastUpdateSucceded = true; //Informs that the date check has succeeded. - } - catch (Exception) - { - _LastUpdateSucceded = false; //Informs that the date check has failed. - } - _LastUpdateDateTime = DateTime.Now; //Informs when the last update has been attempted (succeeded or not). - } - - /// - /// Perform a time correction check, may a few seconds. - /// - /// Resets the timer to 0. Occurs even if the attempt to attempt a new time correction fails. - /// Attempts to get time correction even if disabled. - /// Informs if the time correction check was attempted or not. Will fail if the thread is still active from a previous time correction check. - public bool CheckNow(bool ResetTimer = true, bool ForceCheck = false) - { - if (ResetTimer) //Checks if the timer should be reset. - { - _IntervalStretcher = 0; //Resets the timer. - } - if (ForceCheck || _Enable) //Checks if this check is forced or if time correction is enabled. - { - return Task_Do(); //Attempts to run a new task and informs if attempt to attemp is a success of fail - } - return false; //Informs if not attempted to attempt - } - } - - /// - /// Utility to deal with Base32 encoding and decoding. - /// - /// - /// http://tools.ietf.org/html/rfc4648 - /// - public static class Base32 - { - /// - /// The number of bits in a base32 encoded character. - /// - private const int encodedBitCount = 5; - /// - /// The number of bits in a byte. - /// - private const int byteBitCount = 8; - /// - /// A string containing all of the base32 characters in order. - /// This allows a simple indexof or [index] to convert between - /// a numeric value and an encoded character and vice versa. - /// - private const string encodingChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - /// - /// Takes a block of data and converts it to a base 32 encoded string. - /// - /// Input data. - /// base 32 string. - public static string Encode(byte[] data) - { - if (data == null) - throw new ArgumentNullException(); - if (data.Length == 0) - throw new ArgumentNullException(); - - // The output character count is calculated in 40 bit blocks. That is because the least - // common blocks size for both binary (8 bit) and base 32 (5 bit) is 40. Padding must be used - // to fill in the difference. - int outputCharacterCount = (int)Math.Ceiling(data.Length / (decimal)encodedBitCount) * byteBitCount; - char[] outputBuffer = new char[outputCharacterCount]; - - byte workingValue = 0; - short remainingBits = encodedBitCount; - int currentPosition = 0; - - foreach (byte workingByte in data) - { - workingValue = (byte)(workingValue | (workingByte >> (byteBitCount - remainingBits))); - outputBuffer[currentPosition++] = encodingChars[workingValue]; - - if (remainingBits <= byteBitCount - encodedBitCount) - { - workingValue = (byte)((workingByte >> (byteBitCount - encodedBitCount - remainingBits)) & 31); - outputBuffer[currentPosition++] = encodingChars[workingValue]; - remainingBits += encodedBitCount; - } - - remainingBits -= byteBitCount - encodedBitCount; - workingValue = (byte)((workingByte << remainingBits) & 31); - } - - // If we didn't finish, write the last current working char. - if (currentPosition != outputCharacterCount) - outputBuffer[currentPosition++] = encodingChars[workingValue]; - - // RFC 4648 specifies that padding up to the end of the next 40 bit block must be provided - // Since the outputCharacterCount does account for the paddingCharacters, fill it out. - while (currentPosition < outputCharacterCount) - { - // The RFC defined paddinc char is '='. - outputBuffer[currentPosition++] = '='; - } - - return new string(outputBuffer); - } - - /// - /// Takes a base 32 encoded value and converts it back to binary data. - /// - /// Base 32 encoded string. - /// Binary data. - public static byte[] Decode(string base32) - { - if (string.IsNullOrEmpty(base32)) - throw new ArgumentNullException(); - - var unpaddedBase32 = base32.ToUpperInvariant().TrimEnd('='); - foreach (var c in unpaddedBase32) - { - if (encodingChars.IndexOf(c) < 0) - throw new ArgumentException("Base32 contains illegal characters."); - } - - // we have already removed the padding so this will tell us how many actual bytes there should be. - int outputByteCount = unpaddedBase32.Length * encodedBitCount / byteBitCount; - byte[] outputBuffer = new byte[outputByteCount]; - - byte workingByte = 0; - short bitsRemaining = byteBitCount; - int mask = 0; - int arrayIndex = 0; - - foreach (char workingChar in unpaddedBase32) - { - int encodedCharacterNumericValue = encodingChars.IndexOf(workingChar); - - if (bitsRemaining > encodedBitCount) - { - mask = encodedCharacterNumericValue << (bitsRemaining - encodedBitCount); - workingByte = (byte)(workingByte | mask); - bitsRemaining -= encodedBitCount; - } - else - { - mask = encodedCharacterNumericValue >> (encodedBitCount - bitsRemaining); - workingByte = (byte)(workingByte | mask); - outputBuffer[arrayIndex++] = workingByte; - workingByte = (byte)(encodedCharacterNumericValue << (byteBitCount - encodedBitCount + bitsRemaining)); - bitsRemaining += byteBitCount - encodedBitCount; - } - } - - return outputBuffer; - } - } -} \ No newline at end of file diff --git a/src/keepass2android/Totp/TrayTotpPluginAdapter.cs b/src/keepass2android/Totp/TrayTotpPluginAdapter.cs index f3ace0cb..d85d9a5d 100644 --- a/src/keepass2android/Totp/TrayTotpPluginAdapter.cs +++ b/src/keepass2android/Totp/TrayTotpPluginAdapter.cs @@ -73,7 +73,7 @@ namespace PluginTOTP } try { - IsLengthValid = (Settings[1] == "6") || (Settings[1] == "8"); //Length + IsLengthValid = (Settings[1] == "6") || (Settings[1] == "8") || (Settings[1] == "S"); //Length } catch (Exception) { @@ -115,11 +115,12 @@ namespace PluginTOTP { bool NoTimeCorrection = false; string[] Settings = SettingsGet(entryFields); - res.Duration = Convert.ToInt16(Settings[0]); - res.Length = Convert.ToInt16(Settings[1]); + res.Duration = Settings[0]; + res.Length = Settings[1]; if (ValidUrl) { NoTimeCorrection = true; + res.Url = Settings[2]; /*var CurrentTimeCorrection = TimeCorrections[Settings[2]]; if (CurrentTimeCorrection != null) { diff --git a/src/keepass2android/Totp/UpdateTotpTimerTask.cs b/src/keepass2android/Totp/UpdateTotpTimerTask.cs index 08b970fb..840d82a2 100644 --- a/src/keepass2android/Totp/UpdateTotpTimerTask.cs +++ b/src/keepass2android/Totp/UpdateTotpTimerTask.cs @@ -6,8 +6,8 @@ using Java.Util; using KeePassLib.Security; using KeePassLib.Utility; using Keepass2android.Pluginsdk; -using OtpProviderClient; using keepass2android; +using KeeTrayTOTP.Libraries; namespace PluginTOTP { @@ -36,8 +36,8 @@ namespace PluginTOTP if (totpData.IsTotpEnry) { //generate a new totp - Totp_Provider prov = new Totp_Provider(totpData.Duration, totpData.Length); - string totp = prov.Generate(Base32.Decode(totpData.TotpSeed)); + TOTPProvider prov = new TOTPProvider(totpData.Settings); + string totp = prov.Generate(totpData.TotpSeed); //update entry and keyboard UpdateEntryData(totp); //broadcast new field value (update EntryActivity). this might result in another keyboard diff --git a/src/keepass2android/Utils/Util.cs b/src/keepass2android/Utils/Util.cs index 50c0cbf5..26e85bd0 100644 --- a/src/keepass2android/Utils/Util.cs +++ b/src/keepass2android/Utils/Util.cs @@ -200,7 +200,9 @@ namespace keepass2android /// is more for one-time access, but therefore allows possibly more available sources. public static void ShowBrowseDialog(Activity activity, int requestCodeBrowse, bool forSaving, bool tryGetPermanentAccess) { - var loadAction = IsKitKatOrLater ? + //even though GetContent is not well supported (since Android 7, see https://commonsware.com/Android/previews/appendix-b-android-70) + //we still offer it. + var loadAction = (tryGetPermanentAccess && IsKitKatOrLater) ? Intent.ActionOpenDocument : Intent.ActionGetContent; if ((!forSaving) && (IsIntentAvailable(activity, loadAction, "*/*", new List { Intent.CategoryOpenable}))) { diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index 0c32a663..2656b9de 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -128,6 +128,8 @@ + + @@ -194,11 +196,14 @@ + + - + + @@ -701,7 +706,9 @@ - + + Designer + @@ -716,7 +723,7 @@ Designer - + @@ -1709,6 +1716,15 @@ + + + + + + + + + diff --git a/src/keepass2android/settings/DatabaseSettingsActivity.cs b/src/keepass2android/settings/DatabaseSettingsActivity.cs index e4de490a..2fb17251 100644 --- a/src/keepass2android/settings/DatabaseSettingsActivity.cs +++ b/src/keepass2android/settings/DatabaseSettingsActivity.cs @@ -356,6 +356,9 @@ namespace keepass2android // Re-use the change handlers for the application settings FindPreference(GetString(Resource.String.keyfile_key)).PreferenceChange += OnRememberKeyFileHistoryChanged; FindPreference(GetString(Resource.String.ShowUnlockedNotification_key)).PreferenceChange += OnShowUnlockedNotificationChanged; + FindPreference(GetString(Resource.String.ShowUnlockedNotification_key)).PreferenceChange += OnShowUnlockedNotificationChanged; + FindPreference(GetString(Resource.String.DebugLog_key)).PreferenceChange += OnDebugLogChanged; + FindPreference(GetString(Resource.String.DebugLog_send_key)).PreferenceClick += OnSendDebug; PrepareNoDonatePreference(Activity, FindPreference(GetString(Resource.String.NoDonateOption_key))); PrepareNoDonationReminderPreference(Activity, ((PreferenceScreen)FindPreference(GetString(Resource.String.display_prefs_key))), FindPreference(GetString(Resource.String.NoDonationReminder_key))); @@ -447,6 +450,24 @@ namespace keepass2android } + private void OnSendDebug(object sender, Preference.PreferenceClickEventArgs e) + { + Kp2aLog.SendLog(this.Activity); + } + + private void OnDebugLogChanged(object sender, Preference.PreferenceChangeEventArgs e) + { + if ((bool)e.NewValue) + { + Kp2aLog.CreateLogFile(); + } + else + { + Kp2aLog.FinishLogFile(); + } + + } + private void AlgorithmPrefChange(object sender, Preference.PreferenceChangeEventArgs preferenceChangeEventArgs) { var db = App.Kp2a.GetDb();