mirror of
https://github.com/moparisthebest/keepass2android
synced 2024-11-26 03:02:24 -05:00
Code updates from Ben Rush with Comments by Philipp
This commit is contained in:
parent
2cb6c79f0c
commit
8c15b38036
205
src/keepass2android/ChallengeInfo.cs
Normal file
205
src/keepass2android/ChallengeInfo.cs
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
196
src/keepass2android/KeeChallenge.cs
Normal file
196
src/keepass2android/KeeChallenge.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
@ -144,7 +144,7 @@ namespace keepass2android
|
|||||||
|
|
||||||
public PasswordActivity()
|
public PasswordActivity()
|
||||||
{
|
{
|
||||||
_design = new ActivityDesign(this);
|
_design = new ActivityDesign(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -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);
|
||||||
@ -653,7 +654,7 @@ namespace keepass2android
|
|||||||
UpdateKeyProviderUiState();
|
UpdateKeyProviderUiState();
|
||||||
};
|
};
|
||||||
FindViewById(Resource.Id.init_otp).Click += (sender, args) =>
|
FindViewById(Resource.Id.init_otp).Click += (sender, args) =>
|
||||||
{
|
{
|
||||||
App.Kp2a.GetOtpAuxFileStorage(_ioConnection)
|
App.Kp2a.GetOtpAuxFileStorage(_ioConnection)
|
||||||
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection,
|
.PrepareFileUsage(new FileStorageSetupInitiatorActivity(this, OnActivityResult, null), _ioConnection,
|
||||||
RequestCodePrepareOtpAuxFile, false);
|
RequestCodePrepareOtpAuxFile, false);
|
||||||
@ -764,7 +765,6 @@ namespace keepass2android
|
|||||||
}
|
}
|
||||||
UpdateOkButtonState();
|
UpdateOkButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private void PerformLoadDatabase()
|
private void PerformLoadDatabase()
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user