diff --git a/src/keepass2android/ChallengeInfo.cs b/src/keepass2android/ChallengeInfo.cs new file mode 100644 index 00000000..efcf1acc --- /dev/null +++ b/src/keepass2android/ChallengeInfo.cs @@ -0,0 +1,205 @@ +// +// ChallengeInfo.cs +// +// Author: +// Ben.Rush <> +// +// Copyright (c) 2014 Ben.Rush +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +// +using System; +using System.Xml; +using System.IO; +using keepass2android; +using KeePassLib.Serialization; +using System.Xml.Serialization; + +namespace KeeChallenge +{ + public class ChallengeInfo + { + public byte[] EncryptedSecret { + get; + private set; + } + + public byte[] IV { + get; + private set; + } + + public byte[] Challenge { + get; + private set; + } + + public byte[] Verification { + get; + private set; + } + + private ChallengeInfo() + { + } + + public ChallengeInfo(byte[] encryptedSecret, byte[] iv, byte[] challenge, byte[] verification) + { + EncryptedSecret = encryptedSecret; + IV = iv; + Challenge = challenge; + Verification = verification; + } + + public static ChallengeInfo Load(IOConnectionInfo ioc) + { + Stream sIn = null; + ChallengeInfo inf = new ChallengeInfo(); + try + { + sIn = App.Kp2a.GetOtpAuxFileStorage(ioc).OpenFileForRead(ioc); + + XmlSerializer xs = new XmlSerializer(typeof (ChallengeInfo)); + if (!inf.LoadStream(sIn)) return null; + } + catch (Exception e) + { + Kp2aLog.Log(e.ToString()); + } + finally + { + if(sIn != null) sIn.Close(); + } + + return inf; + } + + private bool LoadStream(Stream AuxFile) + { + //read file + XmlReader xml; + try + { + XmlReaderSettings settings = new XmlReaderSettings(); + settings.CloseInput = true; + xml = XmlReader.Create(AuxFile,settings); + } + catch (Exception) + { + return false; + } + + try + { + while (xml.Read()) + { + if (xml.IsStartElement()) + { + switch (xml.Name) + { + case "encrypted": + xml.Read(); + EncryptedSecret = Convert.FromBase64String(xml.Value.Trim()); + break; + case "iv": + xml.Read(); + IV = Convert.FromBase64String(xml.Value.Trim()); + break; + case "challenge": + xml.Read(); + Challenge = Convert.FromBase64String(xml.Value.Trim()); + break; + case "verification": + xml.Read(); + Verification = Convert.FromBase64String(xml.Value.Trim()); + break; + } + } + } + } + catch (Exception) + { + return false; + } + + xml.Close(); + //if failed, return false + return true; + } + + public bool Save(IOConnectionInfo ioc) + { + Stream sOut = null; + + try + { + using (var trans = App.Kp2a.GetOtpAuxFileStorage(ioc) + .OpenWriteTransaction(ioc, App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions))) + { + var stream = trans.OpenFile(); + if (SaveStream(sOut)) + { + trans.CommitWrite(); + } + else return false; + } + return true; + } + catch(Exception) { return false; } + finally + { + if(sOut != null) sOut.Close(); + } + + return false; + } + + private bool SaveStream(Stream file) + { + try + { + XmlWriterSettings settings = new XmlWriterSettings(); + settings.CloseOutput = true; + settings.Indent = true; + settings.IndentChars = "\t"; + settings.NewLineOnAttributes = true; + + XmlWriter xml = XmlWriter.Create(file,settings); + xml.WriteStartDocument(); + xml.WriteStartElement("data"); + + xml.WriteStartElement("aes"); + xml.WriteElementString("encrypted", Convert.ToBase64String(EncryptedSecret)); + xml.WriteElementString("iv", Convert.ToBase64String(IV)); + xml.WriteEndElement(); + + xml.WriteElementString("challenge", Convert.ToBase64String(Challenge)); + xml.WriteElementString("verification", Convert.ToBase64String(Verification)); + + xml.WriteEndElement(); + xml.WriteEndDocument(); + xml.Close(); + } + catch (Exception) + { + return false; + } + + return true; + } + + } +} + diff --git a/src/keepass2android/KeeChallenge.cs b/src/keepass2android/KeeChallenge.cs new file mode 100644 index 00000000..ca02fff0 --- /dev/null +++ b/src/keepass2android/KeeChallenge.cs @@ -0,0 +1,196 @@ +/* KeeChallenge--Provides Yubikey challenge-response capability to Keepass +* Copyright (C) 2014 Ben Rush +* +* This program is free software; you can redistribute it and/or +* modify it under the terms of the GNU General Public License +* as published by the Free Software Foundation; either version 2 +* of the License, or (at your option) any later version. +* +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with this program; if not, write to the Free Software +* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +*/ + +using System; +using System.IO; +using System.Collections.Generic; +using System.Text; +using System.Security.Cryptography; +using System.Diagnostics; +using System.Xml; + +using KeePassLib.Keys; +using KeePassLib.Utility; +using KeePassLib.Cryptography; +using KeePassLib.Serialization; + +using keepass2android; +using keepass2android.Io; + +namespace KeeChallenge +{ + public sealed class KeeChallengeProv + { + private const string m_name = "Yubikey challenge-response"; + + public static string Name { get { return m_name; } } + + public const int keyLenBytes = 20; + public const int challengeLenBytes = 64; + public const int responseLenBytes = 20; + public const int secretLenBytes = 20; + + private KeeChallengeProv() + { + } + + private static byte[] GenerateChallenge() + { + CryptoRandom rand = CryptoRandom.Instance; + return CryptoRandom.Instance.GetRandomBytes(challengeLenBytes); + } + + private static byte[] GenerateResponse(byte[] challenge, byte[] key) + { + HMACSHA1 hmac = new HMACSHA1(key); + byte[] resp = hmac.ComputeHash(challenge); + hmac.Clear(); + return resp; + } + + /// + /// A method for generating encrypted ChallengeInfo to be saved. For security, this method should + /// be called every time you get a successful challenge-response pair from the Yubikey. Failure to + /// do so will permit password re-use attacks. + /// + /// The un-encrypted secret + /// A fully populated ChallengeInfo object ready to be saved + public static ChallengeInfo Encrypt(byte[] secret) + { + //generate a random challenge for use next time + byte[] challenge = GenerateChallenge(); + + //generate the expected HMAC-SHA1 response for the challenge based on the secret + byte[] resp = GenerateResponse(challenge, secret); + + //use the response to encrypt the secret + SHA256 sha = SHA256Managed.Create(); + byte[] key = sha.ComputeHash(resp); // get a 256 bit key from the 160 bit hmac response + byte[] secretHash = sha.ComputeHash(secret); + + AesManaged aes = new AesManaged(); + aes.KeySize = key.Length * sizeof(byte) * 8; //pedantic, but foolproof + aes.Key = key; + aes.GenerateIV(); + aes.Padding = PaddingMode.PKCS7; + byte[] iv = aes.IV; + + byte[] encrypted; + ICryptoTransform enc = aes.CreateEncryptor(); + using (MemoryStream msEncrypt = new MemoryStream()) + { + using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, enc, CryptoStreamMode.Write)) + { + csEncrypt.Write(secret, 0, secret.Length); + csEncrypt.FlushFinalBlock(); + + encrypted = msEncrypt.ToArray(); + csEncrypt.Close(); + csEncrypt.Clear(); + } + msEncrypt.Close(); + } + + ChallengeInfo inf = new ChallengeInfo (encrypted, aes.IV, challenge, secretHash); + + sha.Clear(); + aes.Clear(); + + return inf; + } + + private static bool DecryptSecret(byte[] yubiResp, ChallengeInfo inf, out byte[] secret) + { + secret = new byte[keyLenBytes]; + + if (inf.IV == null) return false; + if (inf.Verification == null) return false; + + //use the response to decrypt the secret + SHA256 sha = SHA256Managed.Create(); + byte[] key = sha.ComputeHash(yubiResp); // get a 256 bit key from the 160 bit hmac response + + AesManaged aes = new AesManaged(); + aes.KeySize = key.Length * sizeof(byte) * 8; //pedantic, but foolproof + aes.Key = key; + aes.IV = inf.IV; + aes.Padding = PaddingMode.PKCS7; + + + ICryptoTransform dec = aes.CreateDecryptor(); + using (MemoryStream msDecrypt = new MemoryStream(inf.EncryptedSecret)) + { + using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, dec, CryptoStreamMode.Read)) + { + csDecrypt.Read(secret, 0, secret.Length); + csDecrypt.Close(); + csDecrypt.Clear(); + } + msDecrypt.Close(); + } + + byte[] secretHash = sha.ComputeHash(secret); + for (int i = 0; i < secretHash.Length; i++) + { + if (secretHash[i] != inf.Verification[i]) + { + //wrong response + Array.Clear(secret, 0, secret.Length); + return false; + } + } + + //return the secret + sha.Clear(); + aes.Clear(); + return true; + } + + /// + /// The primary access point for challenge-response utility functions. Accepts a pre-populated ChallengeInfo object + /// containing at least the IV, EncryptedSecret, and Verification fields. These fields are combined with the Yubikey response + /// to decrypt and verify the secret. + /// + /// A pre-populated object containing minimally the IV, EncryptedSecret and Verification fields. + /// This should be populated from the database.xml auxilliary file + /// The Yubikey's response to the issued challenge + /// The common secret, used as a composite key to encrypt a Keepass database + public static byte[] GetSecret(ChallengeInfo inf, byte[] resp) + { + if (resp.Length != responseLenBytes) + return null; + if (inf == null) + return null; + if (inf.Challenge == null || + inf.Verification == null) + return null; + + byte[] secret; + + if (DecryptSecret(resp, inf, out secret)) + { + return secret; + } + else + { + return null; + } + } + + } +} diff --git a/src/keepass2android/PasswordActivity.cs b/src/keepass2android/PasswordActivity.cs index e2c0eb23..b7d33873 100644 --- a/src/keepass2android/PasswordActivity.cs +++ b/src/keepass2android/PasswordActivity.cs @@ -45,6 +45,7 @@ using MemoryStream = System.IO.MemoryStream; using Object = Java.Lang.Object; using Process = Android.OS.Process; using String = System.String; +using KeeChallenge; namespace keepass2android { @@ -60,7 +61,9 @@ namespace keepass2android None = 0, KeyFile = 1, Otp = 2, - OtpRecovery = 3 + OtpRecovery = 3, + Chal = 4, + ChalRecovery = 5 } public const String KeyDefaultFilename = "defaultFileName"; @@ -76,9 +79,12 @@ namespace keepass2android private const string ShowpasswordKey = "ShowPassword"; private const string KeyProviderIdOtp = "KP2A-OTP"; private const string KeyProviderIdOtpRecovery = "KP2A-OTPSecret"; + private const string KeyProviderIdChallenge = "KP2A-Chal"; + private const string KeyProviderIdChallengeRecovery = "KP2A-ChalSecret"; private const int RequestCodePrepareDbFile = 1000; private const int RequestCodePrepareOtpAuxFile = 1001; + private const int RequestCodeChallengeYubikey = 1002; private Task _loadDbTask; @@ -104,6 +110,10 @@ namespace keepass2android return KeyProviders.Otp; if (_keyFileOrProvider == KeyProviderIdOtpRecovery) return KeyProviders.OtpRecovery; + if (_keyFileOrProvider == KeyProviderIdChallenge) + return KeyProviders.Chal; + if (_keyFileOrProvider == KeyProviderIdChallengeRecovery) + return KeyProviders.ChalRecovery; return KeyProviders.KeyFile; } } @@ -113,6 +123,8 @@ namespace keepass2android private bool _starting; private OtpInfo _otpInfo; + private ChallengeInfo _chalInfo; + private byte[] _challengeSecret; private readonly int[] _otpTextViewIds = new[] {Resource.Id.otp1, Resource.Id.otp2, Resource.Id.otp3, Resource.Id.otp4, Resource.Id.otp5, Resource.Id.otp6}; private const string OtpInfoKey = "OtpInfoKey"; private const string EnteredOtpsKey = "EnteredOtpsKey"; @@ -120,6 +132,7 @@ namespace keepass2android private const string PasswordKey = "PasswordKey"; private const string KeyFileOrProviderKey = "KeyFileOrProviderKey"; + private ActivityDesign _design; private bool _performingLoad; @@ -270,10 +283,60 @@ namespace keepass2android if (requestCode == RequestCodePrepareDbFile) PerformLoadDatabase(); if (requestCode == RequestCodePrepareOtpAuxFile) - LoadOtpFile(); - break; + { + if (_keyFileOrProvider == KeyProviderIdChallenge) + { + LoadChalFile(); + + } else { + LoadOtpFile (); + } + } + break; + } + if (requestCode == RequestCodeChallengeYubikey && resultCode == Result.Ok) + { + byte[] challengeResponse = data.GetByteArrayExtra("response"); + _challengeSecret = KeeChallengeProv.GetSecret(_chalInfo, challengeResponse); + UpdateOkButtonState(); + if (_challengeSecret != null) + { + new LoadingDialog(this, true, + //doInBackground + delegate + { + //save aux file + try + { + ChallengeInfo temp = KeeChallengeProv.Encrypt(_challengeSecret); + IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(_ioConnection); + IOConnectionInfo iocAux = fileStorage.GetFilePath(fileStorage.GetParentPath(_ioConnection), + fileStorage.GetFilenameWithoutPathAndExt(_ioConnection) + ".xml"); + if (!temp.Save(iocAux)) + { + Toast.MakeText(this, Resource.String.ErrorUpdatingChalAuxFile, ToastLength.Long).Show(); + return false; + } + Array.Clear(challengeResponse, 0, challengeResponse.Length); + } + catch (Exception e) + { + Kp2aLog.Log(e.ToString()); + } + return null; + } + , delegate + { + + }).Execute(); + + } + else + { + Toast.MakeText(this, Resource.String.bad_resp, ToastLength.Long).Show(); + return; + } } - } @@ -313,6 +376,61 @@ namespace keepass2android ).Execute(); } + private void LoadChalFile() + { + new LoadingDialog(this, true, + //doInBackground + delegate + { + + try + { + IFileStorage fileStorage = + App.Kp2a.GetOtpAuxFileStorage(_ioConnection); + IOConnectionInfo iocAux = + fileStorage.GetFilePath( + fileStorage.GetParentPath(_ioConnection), + fileStorage.GetFilenameWithoutPathAndExt(_ioConnection) + + ".xml"); + + _chalInfo = ChallengeInfo.Load(iocAux); + } + catch (Exception e) + { + Kp2aLog.Log(e.ToString()); + } + return null; + } + , delegate + { + if (_chalInfo == null) + { + Toast.MakeText(this, + GetString(Resource.String.CouldntLoadChalAuxFile) + + " " + + GetString( + Resource.String.CouldntLoadChalAuxFile_Hint) + , ToastLength.Long).Show(); + return; + + } + Intent chalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE"); + chalIntent.PutExtra("challenge", _chalInfo.Challenge); + chalIntent.PutExtra("slot", 2); + IList activities = PackageManager.QueryIntentActivities(chalIntent, 0); + bool isIntentSafe = activities.Count > 0; + if (isIntentSafe) + { + StartActivityForResult(chalIntent, RequestCodeChallengeYubikey); + } + else + { + //TODO message no plugin! + } + }).Execute(); + + } + private void ShowOtpEntry(IList prefilledOtps) { FindViewById(Resource.Id.otpInitView).Visibility = ViewStates.Gone; @@ -608,6 +726,12 @@ namespace keepass2android case 3: _keyFileOrProvider = KeyProviderIdOtpRecovery; break; + case 4: + _keyFileOrProvider = KeyProviderIdChallenge; + break; + case 5: + _keyFileOrProvider = KeyProviderIdChallengeRecovery; + break; default: throw new Exception("Unexpected position " + args.Position + " / " + ((ICursor) ((AdapterView) sender).GetItemAtPosition(args.Position)).GetString(1)); @@ -616,9 +740,9 @@ namespace keepass2android }; FindViewById(Resource.Id.init_otp).Click += (sender, args) => { - App.Kp2a.GetOtpAuxFileStorage(_ioConnection) - .PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, - RequestCodePrepareOtpAuxFile, false); + App.Kp2a.GetOtpAuxFileStorage(_ioConnection) + .PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, + RequestCodePrepareOtpAuxFile, false); }; } else @@ -692,6 +816,12 @@ namespace keepass2android case KeyProviders.OtpRecovery: FindViewById(Resource.Id.pass_ok).Enabled = FindViewById(Resource.Id.pass_otpsecret).Text != "" && _password != ""; break; + case KeyProviders.Chal: + FindViewById(Resource.Id.pass_ok).Enabled = _challengeSecret != null; + break; + case KeyProviders.ChalRecovery: + FindViewById(Resource.Id.pass_ok).Enabled = true; + break; default: throw new ArgumentOutOfRangeException(); } @@ -713,6 +843,12 @@ namespace keepass2android { FindViewById(Resource.Id.otps_pending).Visibility = _pendingOtps.Count > 0 ? ViewStates.Visible : ViewStates.Gone; } + + if (KeyProviderType == KeyProviders.Chal) + { + FindViewById (Resource.Id.otpView).Visibility = ViewStates.Visible; + FindViewById(Resource.Id.otps_pending).Visibility = ViewStates.Gone; + } UpdateOkButtonState(); } @@ -768,6 +904,11 @@ namespace keepass2android return; } } + else if (KeyProviderType == KeyProviders.Chal) + { + compositeKey.AddUserKey(new KcpCustomKey(KeeChallengeProv.Name, _challengeSecret, true)); + + } CheckBox cbQuickUnlock = (CheckBox) FindViewById(Resource.Id.enable_quickunlock); App.Kp2a.SetQuickUnlockEnabled(cbQuickUnlock.Checked); @@ -1136,12 +1277,8 @@ namespace keepass2android { if ( Success ) { - _act.SetEditText(Resource.Id.password, ""); - _act.SetEditText(Resource.Id.pass_otpsecret, ""); - foreach (int otpId in _act._otpTextViewIds) - { - _act.SetEditText(otpId, ""); - } + + _act.ClearEnteredPassword(); _act.LaunchNextActivity(); @@ -1155,6 +1292,18 @@ namespace keepass2android } } + private void ClearEnteredPassword() + { + SetEditText(Resource.Id.password, ""); + SetEditText(Resource.Id.pass_otpsecret, ""); + foreach (int otpId in _otpTextViewIds) + { + SetEditText(otpId, ""); + } + Array.Clear(_challengeSecret, 0, _challengeSecret.Length); + _challengeSecret = null; + } + class SaveOtpAuxFileAndLoadDb : LoadDb { private readonly PasswordActivity _act; diff --git a/src/keepass2android/Resources/values-de/strings.xml b/src/keepass2android/Resources/values-de/strings.xml index d836e74d..d47178dc 100644 --- a/src/keepass2android/Resources/values-de/strings.xml +++ b/src/keepass2android/Resources/values-de/strings.xml @@ -512,6 +512,8 @@ Erstes öffentliches Release Kennwort + Schlüsseldatei Kennwort + OTP Kennwort + OTP Secret (Recovery-Modus) + Password + Challenge-Response + Password + Challenge-Response secret (recovery mode) Fehler bei Zertifikatsvalidierung ignorieren diff --git a/src/keepass2android/Resources/values/strings.xml b/src/keepass2android/Resources/values/strings.xml index c1a9808c..d2c0e276 100644 --- a/src/keepass2android/Resources/values/strings.xml +++ b/src/keepass2android/Resources/values/strings.xml @@ -395,7 +395,7 @@ Error while adding the key file! - Load OTP auxiliary file… + Load auxiliary file… Enter the next One-time-passwords (OTPs). Swipe your Yubikey NEO at the back of your device to enter via NFC. OTP %1$d Could not load auxiliary OTP file! @@ -410,6 +410,11 @@ Error updating OTP auxiliary file! Saving auxiliary OTP file… + The challenge response is incorrect. + Could not load auxiliary challenge file! + Please use the KeeChallenge plugin in KeePass 2.x (PC) to configure your database for use with challenge-response! + Error updating OTP auxiliary file! + Loading… Plug-ins @@ -601,6 +606,8 @@ Initial public release Password + Key file Password + OTP Password + OTP secret (recovery mode) + Password + Challenge-Response + Password + Challenge-Response secret (recovery mode) Ignore certificate validation failures diff --git a/src/keepass2android/keepass2android.csproj b/src/keepass2android/keepass2android.csproj index 646ef7cb..8db79c6c 100644 --- a/src/keepass2android/keepass2android.csproj +++ b/src/keepass2android/keepass2android.csproj @@ -98,6 +98,7 @@ + @@ -121,6 +122,7 @@ +