diff --git a/src/keepass2android/addons/OtpKeyProv/EncodingUtil.cs b/src/keepass2android/addons/OtpKeyProv/EncodingUtil.cs new file mode 100644 index 00000000..c0e6dada --- /dev/null +++ b/src/keepass2android/addons/OtpKeyProv/EncodingUtil.cs @@ -0,0 +1,189 @@ +/* + OtpKeyProv Plugin + Copyright (C) 2011-2012 Dominik Reichl + + 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 St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Diagnostics; + +using KeePassLib.Utility; + +namespace OtpKeyProv +{ + public static class EncodingUtil + { + private const string FmtHex = "Hex"; + private const string FmtBase64 = "Base64"; + private const string FmtBase32 = "Base32"; + private const string FmtUtf8 = "UTF-8"; + private const string FmtDec = "Dec"; + + public static readonly string[] Formats = new string[]{ + FmtHex, FmtBase64, FmtBase32, FmtUtf8, FmtDec + }; + + public static OtpDataFmt? GetOtpDataFormat(ComboBox cmb) + { + string strFmt = (cmb.SelectedItem as string); + if(strFmt == null) return null; // No assert + + if(strFmt == FmtHex) return OtpDataFmt.Hex; + if(strFmt == FmtBase64) return OtpDataFmt.Base64; + if(strFmt == FmtBase32) return OtpDataFmt.Base32; + if(strFmt == FmtUtf8) return OtpDataFmt.Utf8; + if(strFmt == FmtDec) return OtpDataFmt.Dec; + return null; + } + + public static byte[] ParseKey(string strKey, OtpDataFmt fmt) + { + if(strKey == null) { Debug.Assert(false); return null; } + + strKey = strKey.Trim(); + if(strKey.Length == 0) return null; // No assert + + if(fmt == OtpDataFmt.Hex) + { + strKey = strKey.Replace(" ", string.Empty); + strKey = strKey.Replace("\t", string.Empty); + strKey = strKey.Replace("\r", string.Empty); + strKey = strKey.Replace("\n", string.Empty); + + if((strKey.Length % 2) == 1) strKey = "0" + strKey; + return MemUtil.HexStringToByteArray(strKey); + } + else if(fmt == OtpDataFmt.Base64) + { + try { return Convert.FromBase64String(strKey); } + catch(Exception) { } + } + else if(fmt == OtpDataFmt.Base32) + return ParseBase32(strKey); + else if(fmt == OtpDataFmt.Utf8) + { + try { return StrUtil.Utf8.GetBytes(strKey); } + catch(Exception) { } + } + else if(fmt == OtpDataFmt.Dec) + { + ulong u; + if(ulong.TryParse(strKey, out u)) + { + byte[] pb = MemUtil.UInt64ToBytes(u); + Array.Reverse(pb); // Little endian -> big endian + return pb; + } + } + + return null; + } + + public static ulong? ParseCounter(string strCounter, OtpDataFmt fmt) + { + byte[] pb = ParseKey(strCounter, fmt); + if(pb == null) return null; + if(pb.Length > 8) return null; + + Array.Reverse(pb); // Big endian -> little endian + + byte[] pb8 = new byte[8]; + Array.Copy(pb, 0, pb8, 0, pb.Length); + return MemUtil.BytesToUInt64(pb8); // Little endian + } + + private const string Base32Alph = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + /// + /// Decode base32 strings according to RFC 4648. + /// + private static byte[] ParseBase32(string str) + { + if((str == null) || ((str.Length % 8) != 0)) return null; + + List l = new List(); + for(int i = 0; i < str.Length; i += 8) + { + ulong u = 0; + int nBits = 0; + + for(int j = 0; j < 8; ++j) + { + char ch = char.ToUpper(str[i + j]); + if(ch == '=') break; + + int iValue = Base32Alph.IndexOf(ch); + if(iValue < 0) return null; + + u <<= 5; + u += (ulong)iValue; + nBits += 5; + } + + int nBitsTooMany = (nBits % 8); + u >>= nBitsTooMany; + nBits -= nBitsTooMany; + Debug.Assert((nBits % 8) == 0); + + int idxNewBytes = l.Count; + while(nBits > 0) + { + l.Add((byte)(u & 0xFF)); + u >>= 8; + nBits -= 8; + } + l.Reverse(idxNewBytes, l.Count - idxNewBytes); + } + + return l.ToArray(); + } + + internal static void SelfTest() + { +#if DEBUG + byte[] pbRes = ParseBase32("MY======"); + byte[] pbExp = Encoding.ASCII.GetBytes("f"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-1"); + + pbRes = ParseBase32("MZXQ===="); + pbExp = Encoding.ASCII.GetBytes("fo"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-2"); + + pbRes = ParseBase32("MZXW6==="); + pbExp = Encoding.ASCII.GetBytes("foo"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-3"); + + pbRes = ParseBase32("MZXW6YQ="); + pbExp = Encoding.ASCII.GetBytes("foob"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-4"); + + pbRes = ParseBase32("MZXW6YTB"); + pbExp = Encoding.ASCII.GetBytes("fooba"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-5"); + + pbRes = ParseBase32("MZXW6YTBOI======"); + pbExp = Encoding.ASCII.GetBytes("foobar"); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-6"); + + pbRes = ParseBase32("JNSXSIDQOJXXM2LEMVZCAYTBONSWIIDPNYQG63TFFV2GS3LFEBYGC43TO5XXEZDTFY======"); + pbExp = Encoding.ASCII.GetBytes("Key provider based on one-time passwords."); + if(!MemUtil.ArraysEqual(pbRes, pbExp)) throw new Exception("Base32-7"); +#endif + } + } +} diff --git a/src/keepass2android/addons/OtpKeyProv/OathHotpKeyProv.cs b/src/keepass2android/addons/OtpKeyProv/OathHotpKeyProv.cs new file mode 100644 index 00000000..b588a867 --- /dev/null +++ b/src/keepass2android/addons/OtpKeyProv/OathHotpKeyProv.cs @@ -0,0 +1,132 @@ +/* + OtpKeyProv Plugin + Copyright (C) 2011-2012 Dominik Reichl + + 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 St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +using System; +using System.Collections.Generic; +using System.Text; +using System.Windows.Forms; +using System.Diagnostics; + +using OtpKeyProv.Forms; + +using KeePass.UI; + +using KeePassLib.Keys; +using KeePassLib.Serialization; +using KeePassLib.Utility; + +namespace OtpKeyProv +{ + public sealed class OathHotpKeyProv : KeyProvider + { + private const string AuxFileExt = ".otp.xml"; + private const string ProvType = "OATH HOTP / RFC 4226"; + private const string ProvVersion = "2.0"; // File version, not OtpKeyProv version + + public override string Name + { + get { return "One-Time Passwords (OATH HOTP)"; } + } + + public override bool SecureDesktopCompatible + { + get { return true; } + } + + public override byte[] GetKey(KeyProviderQueryContext ctx) + { + try + { + if(ctx.CreatingNewKey) return Create(ctx); + return Open(ctx); + } + catch(Exception ex) { MessageService.ShowWarning(ex.Message); } + + return null; + } + + private static IOConnectionInfo GetAuxFileIoc(KeyProviderQueryContext ctx) + { + IOConnectionInfo ioc = ctx.DatabaseIOInfo.CloneDeep(); + ioc.Path = UrlUtil.StripExtension(ioc.Path) + AuxFileExt; + return ioc; + } + + private static byte[] Create(KeyProviderQueryContext ctx) + { + IOConnectionInfo iocPrev = GetAuxFileIoc(ctx); + OtpInfo otpInfo = OtpInfo.Load(iocPrev); + if(otpInfo == null) otpInfo = new OtpInfo(); + + OtpKeyCreationForm dlg = new OtpKeyCreationForm(); + dlg.InitEx(otpInfo, ctx); + + if(UIUtil.ShowDialogAndDestroy(dlg) != DialogResult.OK) + return null; + + if(!CreateAuxFile(otpInfo, ctx)) return null; + return otpInfo.Secret; + } + + private static byte[] Open(KeyProviderQueryContext ctx) + { + IOConnectionInfo ioc = GetAuxFileIoc(ctx); + OtpInfo otpInfo = OtpInfo.Load(ioc); + if(otpInfo == null) + { + MessageService.ShowWarning("Failed to load auxiliary OTP info file:", + ioc.GetDisplayName()); + return null; + } + if(otpInfo.Type != ProvType) + { + MessageService.ShowWarning("Unknown OTP generator type!"); + return null; + } + + OtpKeyPromptForm dlg = new OtpKeyPromptForm(); + dlg.InitEx(otpInfo, ctx); + if(UIUtil.ShowDialogAndDestroy(dlg) != DialogResult.OK) + return null; + + if(!CreateAuxFile(otpInfo, ctx)) return null; + return otpInfo.Secret; + } + + private static bool CreateAuxFile(OtpInfo otpInfo, + KeyProviderQueryContext ctx) + { + otpInfo.Type = ProvType; + otpInfo.Version = ProvVersion; + otpInfo.Generator = OtpKeyProvExt.ProductName; + + otpInfo.EncryptSecret(); + + IOConnectionInfo ioc = GetAuxFileIoc(ctx); + if(!OtpInfo.Save(ioc, otpInfo)) + { + MessageService.ShowWarning("Failed to save auxiliary OTP info file:", + ioc.GetDisplayName()); + return false; + } + + return true; + } + } +} diff --git a/src/keepass2android/addons/OtpKeyProv/OtpInfo.cs b/src/keepass2android/addons/OtpKeyProv/OtpInfo.cs new file mode 100644 index 00000000..27f4445f --- /dev/null +++ b/src/keepass2android/addons/OtpKeyProv/OtpInfo.cs @@ -0,0 +1,317 @@ +/* + OtpKeyProv Plugin + Copyright (C) 2011-2012 Dominik Reichl + + 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 St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using System.ComponentModel; +using System.Security.Cryptography; +using System.Diagnostics; + +using KeePassLib.Cryptography; +using KeePassLib.Keys; +using KeePassLib.Serialization; +using KeePassLib.Utility; + +namespace OtpKeyProv +{ + public sealed class OtpInfo + { + private string m_strType = string.Empty; + public string Type + { + get { return m_strType; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strType = value; + } + } + + private string m_strVersion = string.Empty; + public string Version + { + get { return m_strVersion; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strVersion = value; + } + } + + private string m_strGen = string.Empty; + public string Generator + { + get { return m_strGen; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strGen = value; + } + } + + private byte[] m_pbSecret = null; + [XmlIgnore] + public byte[] Secret + { + get { return m_pbSecret; } + set { m_pbSecret = value; } + } + + private string m_strEncSecret = string.Empty; + [DefaultValue("")] + public string EncryptedSecret // Deprecated, < v2.0 + { + get { return m_strEncSecret; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strEncSecret = value; + } + } + + private List m_lSecrets = new List(); + [XmlArrayItem("EncryptedData")] + public List EncryptedSecrets + { + get { return m_lSecrets; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_lSecrets = value; + } + } + + private string m_strEncIV = string.Empty; + [DefaultValue("")] + public string EncryptionIV // Deprecated, < v2.0 + { + get { return m_strEncIV; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strEncIV = value; + } + } + + private string m_strTrfKey = string.Empty; + [DefaultValue("")] + public string TransformationKey // Deprecated, < v2.0 + { + get { return m_strTrfKey; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strTrfKey = value; + } + } + + private const ulong DefaultTrfRounds = 12000; + private ulong m_uTrfRounds = DefaultTrfRounds; + [DefaultValue(typeof(ulong), "12000")] + public ulong TransformationRounds // Deprecated, < v2.0 + { + get { return m_uTrfRounds; } + set { m_uTrfRounds = value; } + } + + private ulong m_uCounter = 0; + public ulong Counter + { + get { return m_uCounter; } + set { m_uCounter = value; } + } + + private uint m_uOtpLength = 8; + public uint OtpLength + { + get { return m_uOtpLength; } + set { m_uOtpLength = value; } + } + + private uint m_uOtpsReq = 4; + public uint OtpsRequired + { + get { return m_uOtpsReq; } + set { m_uOtpsReq = value; } + } + + private uint m_uLookAhead = 0; + public uint LookAheadCount + { + get { return m_uLookAhead; } + set { m_uLookAhead = value; } + } + + public static OtpInfo Load(IOConnectionInfo ioc) + { + Stream sIn = null; + + try + { + sIn = IOConnection.OpenRead(ioc); + + XmlSerializer xs = new XmlSerializer(typeof(OtpInfo)); + return (OtpInfo)xs.Deserialize(sIn); + } + catch(Exception) { } + finally + { + if(sIn != null) sIn.Close(); + } + + return null; + } + + public static bool Save(IOConnectionInfo ioc, OtpInfo otpInfo) + { + Stream sOut = null; + + try + { + sOut = IOConnection.OpenWrite(ioc); + + XmlWriterSettings xws = new XmlWriterSettings(); + xws.CloseOutput = true; + xws.Encoding = StrUtil.Utf8; + xws.Indent = true; + xws.IndentChars = "\t"; + + XmlWriter xw = XmlWriter.Create(sOut, xws); + + XmlSerializer xs = new XmlSerializer(typeof(OtpInfo)); + xs.Serialize(xw, otpInfo); + + xw.Close(); + return true; + } + catch(Exception) { Debug.Assert(false); } + finally + { + if(sOut != null) sOut.Close(); + } + + return false; + } + + public void EncryptSecret() + { + if(m_pbSecret == null) throw new InvalidOperationException(); + + string[] vOtps = new string[m_uOtpsReq + m_uLookAhead]; + ulong uCounter = m_uCounter; + for(int i = 0; i < vOtps.Length; ++i) + { + vOtps[i] = HmacOtp.Generate(m_pbSecret, uCounter, + m_uOtpLength, false, -1); + ++uCounter; + } + + m_strEncSecret = string.Empty; + m_strEncIV = string.Empty; + m_strTrfKey = string.Empty; + m_uTrfRounds = DefaultTrfRounds; + + m_lSecrets.Clear(); + for(int i = 0; i <= (int)m_uLookAhead; ++i) + m_lSecrets.Add(OtpUtil.EncryptSecret(m_pbSecret, vOtps, i, + (int)m_uOtpsReq)); + } + } + + public sealed class OtpEncryptedData + { + private string m_strCipherText = string.Empty; + [DefaultValue("")] + public string CipherText + { + get { return m_strCipherText; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strCipherText = value; + } + } + + private string m_strIV = string.Empty; + [DefaultValue("")] + public string IV + { + get { return m_strIV; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strIV = value; + } + } + + private string m_strTrfKey = string.Empty; + [DefaultValue("")] + public string TransformationKey + { + get { return m_strTrfKey; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strTrfKey = value; + } + } + + private ulong m_uTrfRounds = 10000; + public ulong TransformationRounds + { + get { return m_uTrfRounds; } + set { m_uTrfRounds = value; } + } + + private string m_strPlainHash = string.Empty; + [DefaultValue("")] + public string PlainTextHash + { + get { return m_strPlainHash; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strPlainHash = value; + } + } + + private string m_strPlainHashTrfKey = string.Empty; + [DefaultValue("")] + public string PlainTextHashTransformationKey + { + get { return m_strPlainHashTrfKey; } + set + { + if(value == null) throw new ArgumentNullException("value"); + m_strPlainHashTrfKey = value; + } + } + + private ulong m_uHashTrfRounds = 10000; + public ulong PlainTextHashTransformationRounds + { + get { return m_uHashTrfRounds; } + set { m_uHashTrfRounds = value; } + } + } +} diff --git a/src/keepass2android/addons/OtpKeyProv/OtpUtil.cs b/src/keepass2android/addons/OtpKeyProv/OtpUtil.cs new file mode 100644 index 00000000..e2f79fd8 --- /dev/null +++ b/src/keepass2android/addons/OtpKeyProv/OtpUtil.cs @@ -0,0 +1,173 @@ +/* + OtpKeyProv Plugin + Copyright (C) 2011-2012 Dominik Reichl + + 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 St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +using System; +using System.Collections.Generic; +using System.Text; +using System.IO; +using System.Security.Cryptography; +using System.Diagnostics; + +using KeePassLib.Cryptography; +using KeePassLib.Cryptography.Cipher; +using KeePassLib.Keys; +using KeePassLib.Utility; + +namespace OtpKeyProv +{ + public enum OtpDataFmt + { + Hex = 0, + Base64 = 1, + Base32 = 4, + Utf8 = 2, + Dec = 3 + } + + public static class OtpUtil + { + public static byte[] KeyFromOtps(string[] vOtps, int iOtpsOffset, + int iOtpsCount, byte[] pbTrfKey32, ulong uTrfRounds) + { + StringBuilder sb = new StringBuilder(); + for(int i = iOtpsOffset; i < (iOtpsOffset + iOtpsCount); ++i) + { + if(sb.Length > 0) sb.Append(':'); + sb.Append(vOtps[i].Trim()); + } + + string strKey = sb.ToString(); + byte[] pb = StrUtil.Utf8.GetBytes(strKey); + if(pb.Length == 0) return null; + + byte[] pbKey = HashAndTransform(pb, pbTrfKey32, uTrfRounds); + if(pbKey == null) throw new InvalidOperationException(); + + return pbKey; + } + + public static string EncryptData(byte[] pbData, byte[] pbKey32, + byte[] pbIV16) + { + byte[] pbIV8 = new byte[8]; + Array.Copy(pbIV16, 0, pbIV8, 0, 8); + + byte[] pbEnc = new byte[pbData.Length]; + Array.Copy(pbData, pbEnc, pbData.Length); + + Salsa20Cipher enc = new Salsa20Cipher(pbKey32, pbIV8); + enc.Encrypt(pbEnc, pbEnc.Length, true); + + return ("s20://" + Convert.ToBase64String(pbEnc, + Base64FormattingOptions.None)); + } + + public static byte[] DecryptData(string strData, byte[] pbKey32, + byte[] pbIV16) + { + if(!strData.StartsWith("s20://")) return null; + + string strEnc = strData.Substring(6); + byte[] pb = Convert.FromBase64String(strEnc); + + byte[] pbIV8 = new byte[8]; + Array.Copy(pbIV16, 0, pbIV8, 0, 8); + + Salsa20Cipher dec = new Salsa20Cipher(pbKey32, pbIV8); + dec.Encrypt(pb, pb.Length, true); + + return pb; + } + + private static byte[] HashAndTransform(byte[] pbData, byte[] pbTrfKey32, + ulong uTrfRounds) + { + SHA256Managed sha256 = new SHA256Managed(); + byte[] pbHash = sha256.ComputeHash(pbData); + sha256.Clear(); + + if(!CompositeKey.TransformKeyManaged(pbHash, pbTrfKey32, uTrfRounds)) + return null; + + sha256 = new SHA256Managed(); + pbHash = sha256.ComputeHash(pbHash); + sha256.Clear(); + + return pbHash; + } + + public static OtpEncryptedData EncryptSecret(byte[] pbSecret, string[] vOtps, + int iOtpsOffset, int iOtpsCount) + { + OtpEncryptedData d = new OtpEncryptedData(); + CryptoRandom r = CryptoRandom.Instance; + + byte[] pbIV16 = r.GetRandomBytes(16); + d.IV = Convert.ToBase64String(pbIV16, Base64FormattingOptions.None); + + byte[] pbTrfKey32 = r.GetRandomBytes(32); + d.TransformationKey = Convert.ToBase64String(pbTrfKey32, Base64FormattingOptions.None); + + byte[] pbKey32 = OtpUtil.KeyFromOtps(vOtps, iOtpsOffset, iOtpsCount, + pbTrfKey32, d.TransformationRounds); + + d.CipherText = OtpUtil.EncryptData(pbSecret, pbKey32, pbIV16); + + byte[] pbHashTrfKey32 = r.GetRandomBytes(32); + d.PlainTextHashTransformationKey = Convert.ToBase64String(pbHashTrfKey32, + Base64FormattingOptions.None); + + byte[] pbHash = HashAndTransform(pbSecret, pbHashTrfKey32, + d.PlainTextHashTransformationRounds); + d.PlainTextHash = Convert.ToBase64String(pbHash, Base64FormattingOptions.None); + + return d; + } + + public static byte[] DecryptSecret(OtpEncryptedData d, string[] vOtps, + int iOtpsOffset, int iOtpsCount) + { + try { return DecryptSecretPriv(d, vOtps, iOtpsOffset, iOtpsCount); } + catch(Exception) { Debug.Assert(false); } + return null; + } + + private static byte[] DecryptSecretPriv(OtpEncryptedData d, string[] vOtps, + int iOtpsOffset, int iOtpsCount) + { + if(d == null) throw new ArgumentNullException("d"); + + byte[] pbTrfKey32 = Convert.FromBase64String(d.TransformationKey); + byte[] pbKey32 = OtpUtil.KeyFromOtps(vOtps, iOtpsOffset, iOtpsCount, + pbTrfKey32, d.TransformationRounds); + byte[] pbIV = Convert.FromBase64String(d.IV); + + byte[] pbSecret = OtpUtil.DecryptData(d.CipherText, pbKey32, pbIV); + + byte[] pbHashTrfKey32 = Convert.FromBase64String(d.PlainTextHashTransformationKey); + byte[] pbHash = HashAndTransform(pbSecret, pbHashTrfKey32, + d.PlainTextHashTransformationRounds); + + if(!MemUtil.ArraysEqual(pbHash, Convert.FromBase64String(d.PlainTextHash))) + return null; + + return pbSecret; + } + } +}