From 8c15b380366f0cd97f8f1005da5c3f61baceeed3 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Wed, 2 Apr 2014 06:57:04 +0200 Subject: [PATCH] Code updates from Ben Rush with Comments by Philipp --- src/keepass2android/ChallengeInfo.cs | 205 ++++++++++++++++++++++++ src/keepass2android/KeeChallenge.cs | 196 ++++++++++++++++++++++ src/keepass2android/PasswordActivity.cs | 24 +-- 3 files changed, 413 insertions(+), 12 deletions(-) create mode 100644 src/keepass2android/ChallengeInfo.cs create mode 100644 src/keepass2android/KeeChallenge.cs 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 479e5c4f..4d111ed0 100644 --- a/src/keepass2android/PasswordActivity.cs +++ b/src/keepass2android/PasswordActivity.cs @@ -84,7 +84,7 @@ namespace keepass2android private const int RequestCodePrepareDbFile = 1000; private const int RequestCodePrepareOtpAuxFile = 1001; - private const int RequestCodePrepareChalAuxFile = 1001; + private const int RequestCodeChallengeYubikey = 1002; private Task _loadDbTask; @@ -144,7 +144,7 @@ namespace keepass2android public PasswordActivity() { - _design = new ActivityDesign(this); + _design = new ActivityDesign(this); } @@ -277,19 +277,20 @@ namespace keepass2android if (_keyFileOrProvider == KeyProviderIdChallenge) { if (!LoadChalFile()) break; - Intent chalIntent = new Intent(this, typeof(NfcChalActivity)); + Intent chalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE"); chalIntent.PutExtra("challenge", _chalInfo.Challenge); - StartActivityForResult(chalIntent, 0); + chalIntent.PutExtra("slot", 2); + IList activities = PackageManager.QueryIntentActivities(chalIntent, 0); + bool isIntentSafe = activities.Count > 0; + if (isIntentSafe) + StartActivityForResult(chalIntent, RequestCodeChallengeYubikey); } else { LoadOtpFile (); } } - break; - case NfcChalActivity.SUCCESS: - _challengeResponse = data.GetByteArrayExtra("chalresp"); - break; + break; } - + if (requestCode == RequestCodeChallengeYubikey && resultCode == Result.Ok) _challengeResponse = data.GetByteArrayExtra("response"); } @@ -331,7 +332,7 @@ namespace keepass2android private bool LoadChalFile() { - + //TODO make async! try { IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(_ioConnection); @@ -653,7 +654,7 @@ namespace keepass2android UpdateKeyProviderUiState(); }; FindViewById(Resource.Id.init_otp).Click += (sender, args) => - { + { App.Kp2a.GetOtpAuxFileStorage(_ioConnection) .PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection, RequestCodePrepareOtpAuxFile, false); @@ -764,7 +765,6 @@ namespace keepass2android } UpdateOkButtonState(); } - private void PerformLoadDatabase() {