Skip to content

Commit

Permalink
Add support for Steam OTP
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 committed Sep 30, 2020
1 parent f6df454 commit 267ed70
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 267ed70

Please sign in to comment.