rename folder keepass2android => keepass2android-app
This commit is contained in:
203
src/keepass2android-app/addons/CsvStreamReaderEx.cs
Normal file
203
src/keepass2android-app/addons/CsvStreamReaderEx.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
KeePass Password Safe - The Open-Source Password Manager
|
||||
Copyright (C) 2003-2018 Dominik Reichl <dominik.reichl@t-online.de>
|
||||
|
||||
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.Diagnostics;
|
||||
|
||||
using KeePassLib.Utility;
|
||||
|
||||
namespace KeePass.DataExchange
|
||||
{
|
||||
public sealed class CsvOptions
|
||||
{
|
||||
private char m_chFieldSep = ',';
|
||||
public char FieldSeparator
|
||||
{
|
||||
get { return m_chFieldSep; }
|
||||
set { m_chFieldSep = value; }
|
||||
}
|
||||
|
||||
private char m_chRecSep = '\n';
|
||||
public char RecordSeparator
|
||||
{
|
||||
get { return m_chRecSep; }
|
||||
set { m_chRecSep = value; }
|
||||
}
|
||||
|
||||
private char m_chTextQual = '\"';
|
||||
public char TextQualifier
|
||||
{
|
||||
get { return m_chTextQual; }
|
||||
set { m_chTextQual = value; }
|
||||
}
|
||||
|
||||
private bool m_bBackEscape = true;
|
||||
public bool BackslashIsEscape
|
||||
{
|
||||
get { return m_bBackEscape; }
|
||||
set { m_bBackEscape = value; }
|
||||
}
|
||||
|
||||
private bool m_bTrimFields = true;
|
||||
public bool TrimFields
|
||||
{
|
||||
get { return m_bTrimFields; }
|
||||
set { m_bTrimFields = value; }
|
||||
}
|
||||
|
||||
private string m_strNewLineSeq = "\r\n";
|
||||
public string NewLineSequence
|
||||
{
|
||||
get { return m_strNewLineSeq; }
|
||||
set
|
||||
{
|
||||
if (value == null) throw new ArgumentNullException("value");
|
||||
m_strNewLineSeq = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class CsvStreamReaderEx
|
||||
{
|
||||
private CharStream m_sChars;
|
||||
private CsvOptions m_opt;
|
||||
|
||||
public CsvStreamReaderEx(string strData)
|
||||
{
|
||||
Init(strData, null);
|
||||
}
|
||||
|
||||
public CsvStreamReaderEx(string strData, CsvOptions opt)
|
||||
{
|
||||
Init(strData, opt);
|
||||
}
|
||||
|
||||
private void Init(string strData, CsvOptions opt)
|
||||
{
|
||||
if (strData == null) throw new ArgumentNullException("strData");
|
||||
|
||||
m_opt = (opt ?? new CsvOptions());
|
||||
|
||||
string strInput = strData;
|
||||
|
||||
// Normalize to Unix "\n" right now; the new lines are
|
||||
// converted back in the <c>AddField</c> method
|
||||
strInput = StrUtil.NormalizeNewLines(strInput, false);
|
||||
|
||||
strInput = strInput.Trim(new char[] { (char)0 });
|
||||
|
||||
m_sChars = new CharStream(strInput);
|
||||
}
|
||||
|
||||
public string[] ReadLine()
|
||||
{
|
||||
char chFirst = m_sChars.PeekChar();
|
||||
if (chFirst == char.MinValue) return null;
|
||||
|
||||
List<string> v = new List<string>();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
bool bInText = false;
|
||||
|
||||
char chFS = m_opt.FieldSeparator, chRS = m_opt.RecordSeparator;
|
||||
char chTQ = m_opt.TextQualifier;
|
||||
|
||||
while (true)
|
||||
{
|
||||
char ch = m_sChars.ReadChar();
|
||||
if (ch == char.MinValue) break;
|
||||
|
||||
Debug.Assert(ch != '\r'); // Was normalized to Unix "\n"
|
||||
|
||||
if ((ch == '\\') && m_opt.BackslashIsEscape)
|
||||
{
|
||||
char chEsc = m_sChars.ReadChar();
|
||||
if (chEsc == char.MinValue) break;
|
||||
|
||||
if (chEsc == 'n') sb.Append('\n');
|
||||
else if (chEsc == 'r') sb.Append('\r');
|
||||
else if (chEsc == 't') sb.Append('\t');
|
||||
else if (chEsc == 'u')
|
||||
{
|
||||
char chNum1 = m_sChars.ReadChar();
|
||||
char chNum2 = m_sChars.ReadChar();
|
||||
char chNum3 = m_sChars.ReadChar();
|
||||
char chNum4 = m_sChars.ReadChar();
|
||||
if (chNum4 != char.MinValue) // Implies the others
|
||||
{
|
||||
StringBuilder sbNum = new StringBuilder();
|
||||
sbNum.Append(chNum3); // Little-endian
|
||||
sbNum.Append(chNum4);
|
||||
sbNum.Append(chNum1);
|
||||
sbNum.Append(chNum2);
|
||||
|
||||
byte[] pbNum = MemUtil.HexStringToByteArray(sbNum.ToString());
|
||||
ushort usNum = MemUtil.BytesToUInt16(pbNum);
|
||||
|
||||
sb.Append((char)usNum);
|
||||
}
|
||||
}
|
||||
else sb.Append(chEsc);
|
||||
}
|
||||
else if (ch == chTQ)
|
||||
{
|
||||
if (!bInText) bInText = true;
|
||||
else // bInText
|
||||
{
|
||||
char chNext = m_sChars.PeekChar();
|
||||
if (chNext == chTQ)
|
||||
{
|
||||
m_sChars.ReadChar();
|
||||
sb.Append(chTQ);
|
||||
}
|
||||
else bInText = false;
|
||||
}
|
||||
}
|
||||
else if ((ch == chRS) && !bInText) break;
|
||||
else if (bInText) sb.Append(ch);
|
||||
else if (ch == chFS)
|
||||
{
|
||||
AddField(v, sb.ToString());
|
||||
if (sb.Length > 0) sb.Remove(0, sb.Length);
|
||||
}
|
||||
else sb.Append(ch);
|
||||
}
|
||||
// Debug.Assert(!bInText);
|
||||
AddField(v, sb.ToString());
|
||||
|
||||
return v.ToArray();
|
||||
}
|
||||
|
||||
private void AddField(List<string> v, string strField)
|
||||
{
|
||||
// Escape characters might have been used to insert
|
||||
// new lines that might not conform to Unix "\n"
|
||||
strField = StrUtil.NormalizeNewLines(strField, false);
|
||||
|
||||
// Transform to final form of new lines
|
||||
strField = strField.Replace("\n", m_opt.NewLineSequence);
|
||||
|
||||
if (m_opt.TrimFields) strField = strField.Trim();
|
||||
|
||||
v.Add(strField);
|
||||
}
|
||||
}
|
||||
}
|
||||
187
src/keepass2android-app/addons/OtpKeyProv/EncodingUtil.cs
Normal file
187
src/keepass2android-app/addons/OtpKeyProv/EncodingUtil.cs
Normal file
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
OtpKeyProv Plugin
|
||||
Copyright (C) 2011-2012 Dominik Reichl <dominik.reichl@t-online.de>
|
||||
|
||||
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.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, FmtUtf8, FmtDec, FmtBase32
|
||||
};
|
||||
|
||||
public static OtpDataFmt? GetOtpDataFormat(String strFmt)
|
||||
{
|
||||
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";
|
||||
/// <summary>
|
||||
/// Decode base32 strings according to RFC 4648.
|
||||
/// </summary>
|
||||
private static byte[] ParseBase32(string str)
|
||||
{
|
||||
if((str == null) || ((str.Length % 8) != 0)) return null;
|
||||
|
||||
List<byte> l = new List<byte>();
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
179
src/keepass2android-app/addons/OtpKeyProv/OathHotpKeyProv.cs
Normal file
179
src/keepass2android-app/addons/OtpKeyProv/OathHotpKeyProv.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
/*
|
||||
This file was modified my Philipp Crocoll, 2013. Based on:
|
||||
|
||||
OtpKeyProv Plugin
|
||||
Copyright (C) 2011-2012 Dominik Reichl <dominik.reichl@t-online.de>
|
||||
|
||||
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 KeePassLib.Keys;
|
||||
using KeePassLib.Serialization;
|
||||
using KeePassLib.Utility;
|
||||
using keepass2android;
|
||||
using keepass2android.Io;
|
||||
|
||||
namespace OtpKeyProv
|
||||
{
|
||||
public sealed class OathHotpKeyProv
|
||||
/*removed base class KeyProvider because "synchronous" interface is not suitable on Android*/
|
||||
{
|
||||
public 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 static string Name
|
||||
{
|
||||
get { return "One-Time Passwords (OATH HOTP)"; }
|
||||
}
|
||||
|
||||
|
||||
public const string ShortProductName = "OtpKeyProv";
|
||||
public const string ProductName = "OtpKeyProv KeePass Plugin";
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
private static byte[] Open(KeyProviderQueryContext ctx, OtpInfo otpInfo)
|
||||
{
|
||||
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;
|
||||
}
|
||||
* */
|
||||
|
||||
/// <summary>
|
||||
/// Sets the "Secret" field in otpInfo based on the list of entered OTPs (lOtps) or the entered secret itself which is in format fmt
|
||||
/// </summary>
|
||||
/// based on the code in OtpKeyPromptForm.cs
|
||||
public void SetSecret(OtpInfo otpInfo, List<string> lOtps, string secret, OtpDataFmt? fmt)
|
||||
{
|
||||
byte[] pbSecret = EncodingUtil.ParseKey(secret,
|
||||
(fmt.HasValue ? fmt.Value : OtpDataFmt.Hex));
|
||||
if (pbSecret != null)
|
||||
{
|
||||
otpInfo.Secret = pbSecret;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(otpInfo.EncryptedSecret)) // < v2.0
|
||||
{
|
||||
byte[] pbKey32 = OtpUtil.KeyFromOtps(lOtps.ToArray(), 0,
|
||||
lOtps.Count, Convert.FromBase64String(
|
||||
otpInfo.TransformationKey), otpInfo.TransformationRounds);
|
||||
if (pbKey32 == null) throw new InvalidOperationException();
|
||||
|
||||
pbSecret = OtpUtil.DecryptData(otpInfo.EncryptedSecret,
|
||||
pbKey32, Convert.FromBase64String(otpInfo.EncryptionIV));
|
||||
if (pbSecret == null) throw new InvalidOperationException();
|
||||
|
||||
otpInfo.Secret = pbSecret;
|
||||
otpInfo.Counter += (ulong) otpInfo.OtpsRequired;
|
||||
}
|
||||
else // >= v2.0, supporting look-ahead
|
||||
{
|
||||
bool bSuccess = false;
|
||||
for (int i = 0; i < otpInfo.EncryptedSecrets.Count; ++i)
|
||||
{
|
||||
OtpEncryptedData d = otpInfo.EncryptedSecrets[i];
|
||||
pbSecret = OtpUtil.DecryptSecret(d, lOtps.ToArray(), 0,
|
||||
lOtps.Count);
|
||||
if (pbSecret != null)
|
||||
{
|
||||
otpInfo.Secret = pbSecret;
|
||||
otpInfo.Counter += ((ulong) otpInfo.OtpsRequired +
|
||||
(ulong) i);
|
||||
bSuccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bSuccess) throw new InvalidOperationException();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static bool CreateAuxFile(OtpInfo otpInfo,
|
||||
KeyProviderQueryContext ctx, IOConnectionInfo auxFileIoc)
|
||||
{
|
||||
otpInfo.Type = ProvType;
|
||||
otpInfo.Version = ProvVersion;
|
||||
otpInfo.Generator = ProductName;
|
||||
|
||||
otpInfo.EncryptSecret();
|
||||
|
||||
if(!OtpInfo.Save(auxFileIoc, otpInfo))
|
||||
{
|
||||
MessageService.ShowWarning("Failed to save auxiliary OTP info file:",
|
||||
UrlUtil.GetFileName(auxFileIoc.Path));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void CreateOtpSecret(List<string> lOtps, OtpInfo otpInfo)
|
||||
{
|
||||
|
||||
byte[] pbSecret;
|
||||
if (!string.IsNullOrEmpty(otpInfo.EncryptedSecret)) // < v2.0
|
||||
{
|
||||
byte[] pbKey32 = OtpUtil.KeyFromOtps(lOtps.ToArray(), 0,
|
||||
lOtps.Count, Convert.FromBase64String(
|
||||
otpInfo.TransformationKey), otpInfo.TransformationRounds);
|
||||
if (pbKey32 == null) throw new InvalidOperationException();
|
||||
|
||||
pbSecret = OtpUtil.DecryptData(otpInfo.EncryptedSecret,
|
||||
pbKey32, Convert.FromBase64String(otpInfo.EncryptionIV));
|
||||
if (pbSecret == null) throw new InvalidOperationException();
|
||||
|
||||
otpInfo.Secret = pbSecret;
|
||||
otpInfo.Counter += otpInfo.OtpsRequired;
|
||||
}
|
||||
else // >= v2.0, supporting look-ahead
|
||||
{
|
||||
bool bSuccess = false;
|
||||
for (int i = 0; i < otpInfo.EncryptedSecrets.Count; ++i)
|
||||
{
|
||||
OtpEncryptedData d = otpInfo.EncryptedSecrets[i];
|
||||
pbSecret = OtpUtil.DecryptSecret(d, lOtps.ToArray(), 0,
|
||||
lOtps.Count);
|
||||
if (pbSecret != null)
|
||||
{
|
||||
otpInfo.Secret = pbSecret;
|
||||
otpInfo.Counter += ((ulong) otpInfo.OtpsRequired +
|
||||
(ulong) i);
|
||||
bSuccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bSuccess) throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System.IO;
|
||||
using System.Xml.Serialization;
|
||||
using KeePassLib.Serialization;
|
||||
using OtpKeyProv;
|
||||
using keepass2android.Io;
|
||||
using System.Xml;
|
||||
using Android.Content;
|
||||
|
||||
namespace keepass2android.addons.OtpKeyProv
|
||||
{
|
||||
/// <summary>
|
||||
/// Class which provides caching for OtpInfo-files. This is an extension to CachingFileStorage required to handle conflicts directly when loading.
|
||||
/// </summary>
|
||||
class OtpAuxCachingFileStorage: CachingFileStorage
|
||||
{
|
||||
private readonly IOtpAuxCacheSupervisor _cacheSupervisor;
|
||||
|
||||
internal interface IOtpAuxCacheSupervisor: ICacheSupervisor
|
||||
{
|
||||
/// <summary>
|
||||
/// called when there was a conflict which was resolved by using the remote file.
|
||||
/// </summary>
|
||||
void ResolvedCacheConflictByUsingRemote(IOConnectionInfo ioc);
|
||||
|
||||
/// <summary>
|
||||
/// called when there was a conflict which was resolved by using the local file.
|
||||
/// </summary>
|
||||
void ResolvedCacheConflictByUsingLocal(IOConnectionInfo ioc);
|
||||
}
|
||||
|
||||
|
||||
public OtpAuxCachingFileStorage(IFileStorage cachedStorage, Context context, IOtpAuxCacheSupervisor cacheSupervisor)
|
||||
: base(cachedStorage, context, cacheSupervisor)
|
||||
{
|
||||
_cacheSupervisor = cacheSupervisor;
|
||||
}
|
||||
|
||||
protected override Stream OpenFileForReadWithConflict(IOConnectionInfo ioc, string cachedFilePath)
|
||||
{
|
||||
OtpInfo remoteOtpInfo, localOtpInfo;
|
||||
//load both files
|
||||
XmlSerializer xs = new XmlSerializer(typeof(OtpInfo));
|
||||
|
||||
using (var cacheStream = File.OpenRead(cachedFilePath))
|
||||
{
|
||||
XmlReaderSettings settings = new XmlReaderSettings() { XmlResolver = null, DtdProcessing = DtdProcessing.Ignore};
|
||||
var reader = XmlReader.Create(cacheStream, settings);
|
||||
|
||||
localOtpInfo = (OtpInfo) xs.Deserialize(reader);
|
||||
}
|
||||
using (Stream remoteStream = _cachedStorage.OpenFileForRead(ioc))
|
||||
{
|
||||
remoteOtpInfo = (OtpInfo)xs.Deserialize(remoteStream);
|
||||
}
|
||||
|
||||
//see which OtpInfo has the bigger Counter value and use this one:
|
||||
if (localOtpInfo.Counter > remoteOtpInfo.Counter)
|
||||
{
|
||||
//overwrite the remote file
|
||||
UpdateRemoteFile(File.OpenRead(cachedFilePath),
|
||||
ioc,
|
||||
App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions),
|
||||
GetBaseVersionHash(ioc)
|
||||
);
|
||||
|
||||
_cacheSupervisor.ResolvedCacheConflictByUsingRemote(ioc);
|
||||
}
|
||||
else
|
||||
{
|
||||
//overwrite the local file:
|
||||
UpdateCacheFromRemote(ioc, cachedFilePath);
|
||||
_cacheSupervisor.ResolvedCacheConflictByUsingLocal(ioc);
|
||||
}
|
||||
|
||||
//now return the local file in any way:
|
||||
return File.OpenRead(cachedFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
344
src/keepass2android-app/addons/OtpKeyProv/OtpInfo.cs
Normal file
344
src/keepass2android-app/addons/OtpKeyProv/OtpInfo.cs
Normal file
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
This file was modified my Philipp Crocoll, 2013. Based on:
|
||||
|
||||
OtpKeyProv Plugin
|
||||
Copyright (C) 2011-2012 Dominik Reichl <dominik.reichl@t-online.de>
|
||||
|
||||
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;
|
||||
using keepass2android;
|
||||
|
||||
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<OtpEncryptedData> m_lSecrets = new List<OtpEncryptedData>();
|
||||
[XmlArrayItem("EncryptedData")]
|
||||
public List<OtpEncryptedData> 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 = App.Kp2a.GetOtpAuxFileStorage(ioc).OpenFileForRead(ioc);
|
||||
|
||||
XmlSerializer xs = new XmlSerializer(typeof (OtpInfo));
|
||||
XmlReaderSettings settings = new XmlReaderSettings() { XmlResolver = null, DtdProcessing = DtdProcessing.Ignore };
|
||||
var reader = XmlReader.Create(sIn, settings);
|
||||
|
||||
return (OtpInfo) xs.Deserialize(reader);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Kp2aLog.LogUnexpectedError(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if(sIn != null) sIn.Close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool Save(IOConnectionInfo ioc, OtpInfo otpInfo)
|
||||
{
|
||||
Stream sOut = null;
|
||||
|
||||
try
|
||||
{
|
||||
using (var trans = App.Kp2a.GetOtpAuxFileStorage(ioc)
|
||||
.OpenWriteTransaction(ioc, App.Kp2a.GetBooleanPreference(PreferenceKey.UseFileTransactions)))
|
||||
{
|
||||
var stream = trans.OpenFile();
|
||||
WriteToStream(otpInfo, stream);
|
||||
trans.CommitWrite();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch(Exception) { Debug.Assert(false); }
|
||||
finally
|
||||
{
|
||||
if(sOut != null) sOut.Close();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public static void WriteToStream(OtpInfo otpInfo, Stream stream)
|
||||
{
|
||||
var xws = XmlWriterSettings();
|
||||
|
||||
XmlWriter xw = XmlWriter.Create(stream, xws);
|
||||
|
||||
XmlSerializer xs = new XmlSerializer(typeof (OtpInfo));
|
||||
xs.Serialize(xw, otpInfo);
|
||||
|
||||
xw.Close();
|
||||
}
|
||||
|
||||
public static XmlWriterSettings XmlWriterSettings()
|
||||
{
|
||||
XmlWriterSettings xws = new XmlWriterSettings
|
||||
{
|
||||
CloseOutput = true,
|
||||
Encoding = StrUtil.Utf8,
|
||||
Indent = true,
|
||||
IndentChars = "\t"
|
||||
};
|
||||
return xws;
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
174
src/keepass2android-app/addons/OtpKeyProv/OtpUtil.cs
Normal file
174
src/keepass2android-app/addons/OtpKeyProv/OtpUtil.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
/*
|
||||
OtpKeyProv Plugin
|
||||
Copyright (C) 2011-2012 Dominik Reichl <dominik.reichl@t-online.de>
|
||||
|
||||
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.Cryptography.KeyDerivation;
|
||||
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, 0, pbEnc.Length);
|
||||
|
||||
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, 0, pb.Length);
|
||||
|
||||
return pb;
|
||||
}
|
||||
|
||||
private static byte[] HashAndTransform(byte[] pbData, byte[] pbTrfKey32,
|
||||
ulong uTrfRounds)
|
||||
{
|
||||
SHA256Managed sha256 = new SHA256Managed();
|
||||
byte[] pbHash = sha256.ComputeHash(pbData);
|
||||
sha256.Clear();
|
||||
|
||||
if(!AesKdf.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user