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