Skip to content

Commit

Permalink
Add support for Steam OTP (#20)
Browse files Browse the repository at this point in the history
Allow adding Steam OTP data (compatible with KeePassXC)
Enable migration of Steam OTP settings from/to KeeTrayTotp
  • Loading branch information
Rookiestyle authored Oct 1, 2020
1 parent f6df454 commit 459871f
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 98 deletions.
4 changes: 2 additions & 2 deletions Translations/KeePassOTP.de.language.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Translation>
<TranslationVersion>9</TranslationVersion>
<TranslationVersion>10</TranslationVersion>
<item>
<key>OTPCopyTrayNoEntries</key>
<value>KPOTP - Keine Einträge vorhanden</value>
Expand Down Expand Up @@ -79,7 +79,7 @@
</item>
<item>
<key>TimeCorrection</key>
<value>Zeitversatz ausgleichen - TOTP spezifisch</value>
<value>Zeitversatz ausgleichen - Nur für TOTP && Steam</value>
</item>
<item>
<key>URL</key>
Expand Down
4 changes: 2 additions & 2 deletions Translations/KeePassOTP.fr.language.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Translation>
<TranslationVersion>2</TranslationVersion>
<TranslationVersion>3</TranslationVersion>
<item>
<key>OTPCopyTrayNoEntries</key>
<value>KPOTP - Aucune entrée disponible</value>
Expand Down Expand Up @@ -79,7 +79,7 @@
</item>
<item>
<key>TimeCorrection</key>
<value>Correction de temps - seulement pour TOTP</value>
<value>Correction de temps - seulement pour TOTP et Steam</value>
</item>
<item>
<key>URL</key>
Expand Down
2 changes: 1 addition & 1 deletion Translations/KeePassOTP.template.language.xml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
</item>
<item>
<key>TimeCorrection</key>
<value>Time correction - TOTP only</value>
<value>Time correction - TOTP && Steam only</value>
</item>
<item>
<key>URL</key>
Expand Down
2 changes: 1 addition & 1 deletion src/DAO/OTPDAO_DB.cs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ public override string GetReadableOTP(PwEntry pe)

if (otp.kpotp.Type == KPOTPType.HOTP) return otp.ReadableOTP;
int r = (otp.ValidTo - DateTime.UtcNow).Seconds + 1;
return otp.ReadableOTP + (r < 6 ? " (" + r.ToString() + ")" : string.Empty);
return otp.ReadableOTP + (r <= Config.TOTPSoonExpiring ? " (" + r.ToString() + ")" : string.Empty);
}

public override KPOTP GetOTP(PwEntry pe)
Expand Down
2 changes: 1 addition & 1 deletion src/DAO/OTPDAO_Entry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public override string GetReadableOTP(PwEntry pe)
else
{
int r = (otp.ValidTo - DateTime.UtcNow).Seconds + 1;
return otp.ReadableOTP + (r < 6 ? " (" + r.ToString() + ")" : string.Empty);
return otp.ReadableOTP + (r <= Config.TOTPSoonExpiring ? " (" + r.ToString() + ")" : string.Empty);
}
}

Expand Down
89 changes: 65 additions & 24 deletions src/KeePassOTP.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ namespace KeePassOTP
public enum KPOTPType : int
{
HOTP = 0,
TOTP
TOTP,
STEAM
}

public enum KPOTPHash : int
Expand All @@ -39,14 +40,29 @@ public class KPOTP
private static Dictionary<string, TimeSpan> m_timeCorrectionUrls = new Dictionary<string, TimeSpan>();

public KPOTPHash Hash = KPOTPHash.SHA1;
public KPOTPType Type = KPOTPType.TOTP;
private KPOTPType m_Type = KPOTPType.TOTP;
public KPOTPType Type
{
get { return m_Type; }
set
{
m_Type = value;
if (m_Type == KPOTPType.STEAM) Length = 5;
else Length = Length; // Ensure proper length (Steam = 5 digits)
}
}

public KPOTPEncoding Encoding = KPOTPEncoding.BASE32;

public int m_length = 6;
public int Length
{
get { return m_length; }
set { m_length = Math.Min(10, Math.Max(value, 6)); }
set
{
if (Type == KPOTPType.STEAM) m_length = 5;
else m_length = Math.Min(10, Math.Max(value, 6));
}
}

private int m_timestep = 30;
Expand Down Expand Up @@ -79,7 +95,7 @@ public string TimeCorrectionUrl
get { return m_url; }
set { SetURL(value); }
}

public string Issuer;
public string Label;
public ProtectedString OTPAuthString
Expand Down Expand Up @@ -113,10 +129,10 @@ public int RemainingSeconds

static KPOTP()
{
miConfigureWebClient = typeof(IOConnection).GetMethod("ConfigureWebClient",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic,
null,
new Type[] { typeof(System.Net.WebClient) },
miConfigureWebClient = typeof(IOConnection).GetMethod("ConfigureWebClient",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic,
null,
new Type[] { typeof(System.Net.WebClient) },
null);
}

Expand All @@ -131,18 +147,19 @@ public string GetOTP(bool ShowNext, bool CheckTime)
//List of time correction data is filled asynchronously
//Call it synchronously as it is required now!
if (CheckTime) GetTimeCorrection(m_url);
byte[] data =(Type == KPOTPType.TOTP) ? GetTOTPData(ShowNext) : GetHOTPData(ShowNext);
byte[] data = (Type == KPOTPType.HOTP) ? GetHOTPData(ShowNext) : GetTOTPData(ShowNext);
byte[] hash = ComputeHash(data);
MemUtil.ZeroByteArray(data);
int offset = hash[hash.Length - 1] & 0xF;

int binary = ((hash[offset] & 0x7F) << 24) |
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);

string result = (binary % (int)Math.Pow(10, Length)).ToString().PadLeft(Length, '0');
((hash[offset + 1] & 0xFF) << 16) |
((hash[offset + 2] & 0xFF) << 8) |
(hash[offset + 3] & 0xFF);

string result = string.Empty;
if (Type != KPOTPType.STEAM) result = (binary % (int)Math.Pow(10, Length)).ToString().PadLeft(Length, '0');
else result = GetSteamData(binary);

return result + (string.IsNullOrEmpty(m_url) || m_timeCorrectionUrls.ContainsKey(m_url) ? string.Empty : "*");
}
Expand All @@ -168,6 +185,24 @@ private byte[] GetTOTPData(bool showNext)
return GetOTPData(showNext, Ticks);
}

/// <summary>
/// Character set for authenticator code
/// </summary>
private static readonly char[] aSteamChars = new char[] { '2', '3', '4', '5',
'6', '7', '8', '9', 'B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P',
'Q', 'R', 'T', 'V', 'W', 'X', 'Y' };
private string GetSteamData(int binary)
{
string result = string.Empty;
for (int i = 0; i < Length; i++)
{
result += aSteamChars[binary % aSteamChars.Length];
binary /= aSteamChars.Length;
}

return result;
}

private byte[] GetOTPData(bool showNext, long value)
{
byte[] result = new byte[0];
Expand Down Expand Up @@ -215,6 +250,8 @@ private ProtectedString GetOTPAuthString()
otpSuffix += "&encoder=" + Encoding.ToString();
if (!string.IsNullOrEmpty(Issuer))
otpSuffix += "&issuer=" + Encode(Issuer, false);
if (Type == KPOTPType.STEAM)
otpSuffix += "&encoder=steam";
return new ProtectedString(true, otpPrefix) + m_seed + new ProtectedString(true, otpSuffix);
}

Expand Down Expand Up @@ -296,16 +333,20 @@ private void SetOTPAuthString(ProtectedString value)
else if (hash == "sha512") Hash = KPOTPHash.SHA512;
Length = MigrateInt(parameters.Get("digits"), 6);

string encoding = parameters.Get("encoding");
if (!string.IsNullOrEmpty(encoding)) encoding = encoding.ToLowerInvariant();
if (encoding == "base64") Encoding = KPOTPEncoding.BASE64;
else if (encoding == "hex") Encoding = KPOTPEncoding.HEX;
else if (encoding == "utf8") Encoding = KPOTPEncoding.UTF8;

string encoder = parameters.Get("encoder");
if (!string.IsNullOrEmpty(encoder)) encoder = encoder.ToLowerInvariant();
if (encoder == "base64") Encoding = KPOTPEncoding.BASE64;
else if (encoder == "hex") Encoding = KPOTPEncoding.HEX;
else if (encoder == "utf8") Encoding = KPOTPEncoding.UTF8;
KPOTPType tType = Type;
if (Enum.TryParse(encoder, true, out tType))
Type = tType;

string sIssuerParameter = parameters.Get("issuer");
if (!string.IsNullOrEmpty(sIssuerParameter)) Issuer = Decode(sIssuerParameter);


//Remove %3d / %3D at the end of the seed
c = seed.ReadChars();
idx = c.Length - 3;
Expand Down Expand Up @@ -453,7 +494,7 @@ private static TimeSpan GetTimeCorrection(string value)
{
if (!string.IsNullOrEmpty(value) && !bKeyFound)
PluginDebug.AddError("OTP time correction", 0, "Invalid URL: " + value, "Time correction: " + TimeSpan.Zero.ToString());
lock (m_timeCorrectionUrls) { m_timeCorrectionUrls[value] = TimeSpan.Zero; }
lock (m_timeCorrectionUrls) { m_timeCorrectionUrls[value] = TimeSpan.Zero; }
return m_timeCorrectionUrls[value];
}

Expand Down Expand Up @@ -551,10 +592,10 @@ public static bool Equals(KPOTP otp1, KPOTP otp2, string url, out bool OnlyCount

if (!otp1.OTPSeed.Equals(otp2.OTPSeed, false)) return false;
if (otp1.SanitizeChanged || otp2.SanitizeChanged) return false;
if (otp1.Encoding != otp2.Encoding) return false;
if (otp1.Hash != otp2.Hash) return false;
if (otp1.Type != otp2.Type) return false;
if (otp1.Length != otp2.Length) return false;
if (otp1.Encoding != otp2.Encoding) return false;
if (otp1.Hash != otp2.Hash) return false;
if (otp1.Type != otp2.Type) return false;
if (otp1.Length != otp2.Length) return false;
if ((otp1.Type == KPOTPType.TOTP) && (otp1.TOTPTimestep != otp2.TOTPTimestep)) return false;
if ((otp1.Type == KPOTPType.TOTP) && (otp1.TimeCorrectionUrlOwn != otp2.TimeCorrectionUrlOwn)) return false;
if (otp1.TimeCorrectionUrlOwn && (otp1.Type == KPOTPType.TOTP))
Expand Down
Loading

0 comments on commit 459871f

Please sign in to comment.