Code updates from Ben Rush with Comments by Philipp

This commit is contained in:
Philipp Crocoll 2014-04-02 06:57:04 +02:00
parent 2cb6c79f0c
commit 8c15b38036
3 changed files with 413 additions and 12 deletions

View File

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

View File

@ -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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="secret">The un-encrypted secret</param>
/// <returns>A fully populated ChallengeInfo object ready to be saved</returns>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <param name="inf">A pre-populated object containing minimally the IV, EncryptedSecret and Verification fields.
/// This should be populated from the database.xml auxilliary file</param>
/// <param name="resp">The Yubikey's response to the issued challenge</param>
/// <returns>The common secret, used as a composite key to encrypt a Keepass database</returns>
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;
}
}
}
}

View File

@ -84,7 +84,7 @@ namespace keepass2android
private const int RequestCodePrepareDbFile = 1000; private const int RequestCodePrepareDbFile = 1000;
private const int RequestCodePrepareOtpAuxFile = 1001; private const int RequestCodePrepareOtpAuxFile = 1001;
private const int RequestCodePrepareChalAuxFile = 1001; private const int RequestCodeChallengeYubikey = 1002;
private Task<MemoryStream> _loadDbTask; private Task<MemoryStream> _loadDbTask;
@ -277,19 +277,20 @@ namespace keepass2android
if (_keyFileOrProvider == KeyProviderIdChallenge) if (_keyFileOrProvider == KeyProviderIdChallenge)
{ {
if (!LoadChalFile()) break; if (!LoadChalFile()) break;
Intent chalIntent = new Intent(this, typeof(NfcChalActivity)); Intent chalIntent = new Intent("com.yubichallenge.NFCActivity.CHALLENGE");
chalIntent.PutExtra("challenge", _chalInfo.Challenge); chalIntent.PutExtra("challenge", _chalInfo.Challenge);
StartActivityForResult(chalIntent, 0); chalIntent.PutExtra("slot", 2);
IList<ResolveInfo> activities = PackageManager.QueryIntentActivities(chalIntent, 0);
bool isIntentSafe = activities.Count > 0;
if (isIntentSafe)
StartActivityForResult(chalIntent, RequestCodeChallengeYubikey);
} else { } else {
LoadOtpFile (); LoadOtpFile ();
} }
} }
break; break;
case NfcChalActivity.SUCCESS:
_challengeResponse = data.GetByteArrayExtra("chalresp");
break;
} }
if (requestCode == RequestCodeChallengeYubikey && resultCode == Result.Ok) _challengeResponse = data.GetByteArrayExtra("response");
} }
@ -331,7 +332,7 @@ namespace keepass2android
private bool LoadChalFile() private bool LoadChalFile()
{ {
//TODO make async!
try try
{ {
IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(_ioConnection); IFileStorage fileStorage = App.Kp2a.GetOtpAuxFileStorage(_ioConnection);
@ -765,7 +766,6 @@ namespace keepass2android
UpdateOkButtonState(); UpdateOkButtonState();
} }
private void PerformLoadDatabase() private void PerformLoadDatabase()
{ {