diff --git a/plgxcreate.cmd b/plgxcreate.cmd index 67c256b..9e94d66 100644 --- a/plgxcreate.cmd +++ b/plgxcreate.cmd @@ -1,40 +1,11 @@ @echo off -set plgxnet=%1 -set plgxkp=%2 -set plgxos=%3 - -cls cd %~dp0 for %%* in (.) do set CurrDirName=%%~nx* echo Processing %CurrDirName% -echo Deleting existing PlgX folder -rmdir /s /q plgx - -echo Creating PlgX folder -mkdir plgx - -echo Copying files -xcopy src plgx /s /e /exclude:plgxexclude.txt > nul - -echo Compiling PlgX -cd.. -cd _KeePass_Release -KeePass.exe --plgx-create "%~dp0plgx" plgx-prereq-net:%plgxnet% -plgx-prereq-kp:%plgxkp% -plgx-prereq-os:%plgxos% -cd .. -cd %CurrDirName% - echo Copying PlgX to KeePass plugin folder -copy plgx.plgx "..\_KeePass_Release\Plugins\%CurrDirName%.plgx" +copy "src\bin\Release\%CurrDirName%.plgx" "..\_KeePass_Release\Plugins\%CurrDirName%.plgx" echo Releasing PlgX -move /y plgx.plgx "..\_Releases\%CurrDirName%.plgx" - -echo Cleaning up -rmdir /s /q plgx - -echo Compiled with following minimum requirements: -echo .NET = %plgxnet% -echo KeePass = %plgxkp% -echo OS = %plgxos% \ No newline at end of file +move /y "src\bin\Release\%CurrDirName%.plgx" "..\_Releases\%CurrDirName%.plgx" diff --git a/proto/GoogleAuthenticator_Import.proto b/proto/GoogleAuthenticator_Import.proto new file mode 100644 index 0000000..1a08100 --- /dev/null +++ b/proto/GoogleAuthenticator_Import.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; +package KeePassOTP; + +message GoogleAuthenticatorImport { + enum Algorithm { + ALGORITHM_UNSPECIFIED = 0; + ALGORITHM_SHA1 = 1; + ALGORITHM_SHA256 = 2; + ALGORITHM_SHA512 = 3; + ALGORITHM_MD5 = 4; + } + enum DigitCount { + DIGIT_COUNT_UNSPECIFIED = 0; + DIGIT_COUNT_SIX = 1; + DIGIT_COUNT_EIGHT = 2; + } + enum OtpType { + OTP_TYPE_UNSPECIFIED = 0; + OTP_TYPE_HOTP = 1; + OTP_TYPE_TOTP = 2; + } + message OtpParameters { + bytes secret = 1; + string name = 2; + string issuer = 3; + Algorithm algorithm = 4; + DigitCount digits = 5; + OtpType type = 6; + int64 counter = 7; + } + repeated OtpParameters otp_parameters = 1; + int32 version = 2; + int32 batch_size = 3; + int32 batch_index = 4; + int32 batch_id = 5; +} \ No newline at end of file diff --git a/src/GoogleAuthenticatorImport/GoogleAuthenticatorImport.cs b/src/GoogleAuthenticatorImport/GoogleAuthenticatorImport.cs new file mode 100644 index 0000000..d38f9d7 --- /dev/null +++ b/src/GoogleAuthenticatorImport/GoogleAuthenticatorImport.cs @@ -0,0 +1,127 @@ +// +// This file was generated by a tool; you should avoid making direct changes. +// Consider using 'partial classes' to extend these types +// Input: my.proto +// + +#region Designer generated code +#pragma warning disable 0612, 0618, 1591, 3021 +namespace KeePassOTP +{ + + [global::ProtoBuf.ProtoContract()] + public partial class GoogleAuthenticatorImport : global::ProtoBuf.IExtensible + { + private global::ProtoBuf.IExtension __pbn__extensionData; + global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing) + { + return global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing); + } + public GoogleAuthenticatorImport() + { + otp_parameters = new global::System.Collections.Generic.List(); + OnConstructor(); + } + + partial void OnConstructor(); + + [global::ProtoBuf.ProtoMember(1)] + public global::System.Collections.Generic.List otp_parameters { get; set; } + + [global::ProtoBuf.ProtoMember(2, Name = @"version")] + public int Version { get; set; } + + [global::ProtoBuf.ProtoMember(3, Name = @"batch_size")] + public int BatchSize { get; set; } + + [global::ProtoBuf.ProtoMember(4, Name = @"batch_index")] + public int BatchIndex { get; set; } + + [global::ProtoBuf.ProtoMember(5, Name = @"batch_id")] + public int BatchId { get; set; } + + [global::ProtoBuf.ProtoContract()] + public partial class OtpParameters : global::ProtoBuf.IExtensible + { + private global::ProtoBuf.IExtension __pbn__extensionData; + global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing) + { + return global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing); + } + public OtpParameters() + { + Name = ""; + Issuer = ""; + OnConstructor(); + } + + partial void OnConstructor(); + + [global::ProtoBuf.ProtoMember(1, Name = @"secret")] + public byte[] Secret { get; set; } + + [global::ProtoBuf.ProtoMember(2, Name = @"name")] + [global::System.ComponentModel.DefaultValue("")] + public string Name { get; set; } + + [global::ProtoBuf.ProtoMember(3, Name = @"issuer")] + [global::System.ComponentModel.DefaultValue("")] + public string Issuer { get; set; } + + [global::ProtoBuf.ProtoMember(4, Name = @"algorithm")] + public GoogleAuthenticatorImport.Algorithm Algorithm { get; set; } + + [global::ProtoBuf.ProtoMember(5, Name = @"digits")] + public GoogleAuthenticatorImport.DigitCount Digits { get; set; } + + [global::ProtoBuf.ProtoMember(6, Name = @"type")] + public GoogleAuthenticatorImport.OtpType Type { get; set; } + + [global::ProtoBuf.ProtoMember(7, Name = @"counter")] + public long Counter { get; set; } + + } + + [global::ProtoBuf.ProtoContract()] + public enum Algorithm + { + [global::ProtoBuf.ProtoEnum(Name = @"ALGORITHM_UNSPECIFIED")] + AlgorithmUnspecified = 0, + [global::ProtoBuf.ProtoEnum(Name = @"ALGORITHM_SHA1")] + AlgorithmSha1 = 1, + [global::ProtoBuf.ProtoEnum(Name = @"ALGORITHM_SHA256")] + AlgorithmSha256 = 2, + [global::ProtoBuf.ProtoEnum(Name = @"ALGORITHM_SHA512")] + AlgorithmSha512 = 3, + [global::ProtoBuf.ProtoEnum(Name = @"ALGORITHM_MD5")] + AlgorithmMd5 = 4, + } + + [global::ProtoBuf.ProtoContract()] + public enum DigitCount + { + [global::ProtoBuf.ProtoEnum(Name = @"DIGIT_COUNT_UNSPECIFIED")] + DigitCountUnspecified = 0, + [global::ProtoBuf.ProtoEnum(Name = @"DIGIT_COUNT_SIX")] + DigitCountSix = 1, + [global::ProtoBuf.ProtoEnum(Name = @"DIGIT_COUNT_EIGHT")] + DigitCountEight = 2, + } + + [global::ProtoBuf.ProtoContract()] + public enum OtpType + { + [global::ProtoBuf.ProtoEnum(Name = @"OTP_TYPE_UNSPECIFIED")] + OtpTypeUnspecified = 0, + [global::ProtoBuf.ProtoEnum(Name = @"OTP_TYPE_HOTP")] + OtpTypeHotp = 1, + [global::ProtoBuf.ProtoEnum(Name = @"OTP_TYPE_TOTP")] + OtpTypeTotp = 2, + } + + } + +} + +#pragma warning restore 0612, 0618, 1591, 3021 +#endregion diff --git a/src/KeePassOTP.csproj b/src/KeePassOTP.csproj index 9144909..eb37b59 100644 --- a/src/KeePassOTP.csproj +++ b/src/KeePassOTP.csproj @@ -1,14 +1,9 @@  - + 5 - - 2.42 - - - Debug AnyCPU @@ -19,7 +14,7 @@ Properties KeePassOTP KeePassOTP - v4.5 + v4.6.1 512 @@ -40,12 +35,21 @@ none true bin\Release\ - - prompt 4 false + + true + + + + + 2.42 + $(TargetFrameworkVersion.Replace('v', '')) + + + @@ -62,6 +66,7 @@ + @@ -117,17 +122,47 @@ {10938016-dee2-4a25-9a5a-8fd3444379ca} KeePass + + bin\Release\protobuf-net.dll + bin\Release\protobuf-net.Core.dll + bin\Release\System.Buffers.dll + bin\Release\System.Collections.Immutable.dll + bin\Release\System.Memory.dll + bin\Release\System.Numerics.Vectors.dll + bin\Release\System.Runtime.CompilerServices.Unsafe.dll + bin\Release\System.ServiceModel.Primitives.dll + + + 1.0.0 + + + + 3.0.52 + + + + + + - + + \ No newline at end of file diff --git a/src/KeePassOTPSetup.cs b/src/KeePassOTPSetup.cs index 13f1703..0706b09 100644 --- a/src/KeePassOTPSetup.cs +++ b/src/KeePassOTPSetup.cs @@ -3,6 +3,7 @@ using System.Windows.Forms; using KeePassLib.Security; using KeePassLib.Utility; +using PluginTools; using PluginTranslation; namespace KeePassOTP @@ -111,6 +112,24 @@ private void UpdatePreview() OTP.OTPAuthString = new ProtectedString(true, tbOTPSeed.Text); InitSettings(true); } + else if (tbOTPSeed.Text.ToLowerInvariant().StartsWith("otpauth-migration://")) + { + int iCount; + ProtectedString psGoogleAuth = PSConvert.ParseGoogleAuthExport(tbOTPSeed.Text, out iCount); + if ((iCount == 1) && (psGoogleAuth.Length > 0)) + { + //tbOTPSeed.Text = psGoogleAuth.ReadString(); + OTP.OTPAuthString = psGoogleAuth; + InitSettings(true); + return; + } + tbOTPSeed.Text = string.Empty; + + if (iCount > 1) Tools.ShowError(string.Format(PluginTranslate.ErrorGoogleAuthImportCount, iCount.ToString())); + else Tools.ShowError(PluginTranslate.ErrorGoogleAuthImport); + return; + } + else OTP.OTPSeed = new ProtectedString(true, tbOTPSeed.Text); if (cbOTPFormat.SelectedIndex == 0) OTP.Encoding = KPOTPEncoding.BASE32; @@ -254,7 +273,18 @@ private void pbQR_DragDrop(object sender, DragEventArgs e) if (e.Data.GetDataPresent(DataFormats.FileDrop)) { var f = e.Data.GetData(DataFormats.FileDrop) as string[]; - if (f != null) otp = ParseFromImageFile(f[0]); + if (f != null) + { + otp = ParseFromImageFile(f[0]); + if (otp.ReadString().ToLowerInvariant().StartsWith("otpauth-migration://")) + { + int iOTPCount = 0; + try { otp = PSConvert.ParseGoogleAuthExport(otp.ReadString(), out iOTPCount); } + catch { } + if (iOTPCount > 1) Tools.ShowError(string.Format(PluginTranslate.ErrorGoogleAuthImportCount, iOTPCount.ToString())); + else if (iOTPCount == 0) Tools.ShowError(PluginTranslate.ErrorGoogleAuthImport); + } + } } if (!IsValidOtpAuth(otp)) { diff --git a/src/PluginTranslation.cs b/src/PluginTranslation.cs index 374aa62..cece2d2 100644 --- a/src/PluginTranslation.cs +++ b/src/PluginTranslation.cs @@ -123,6 +123,11 @@ Open it anytime to check the previous content. Restoring the data needs to be done manually."; public static readonly string Placeholder = @"Placeholder:"; public static readonly string PlaceholderAutoSubmit = @"{0} + Enter"; + public static readonly string ErrorGoogleAuthImport = @"Error parsing data"; + public static readonly string ErrorGoogleAuthImportCount = @"Error parsing data + +Expected amount of OTP entries: 1 +Found amount of OTP entries: {0}"; #endregion #region NO changes in this area diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs index 86dbece..d492bf2 100644 --- a/src/Properties/AssemblyInfo.cs +++ b/src/Properties/AssemblyInfo.cs @@ -30,5 +30,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("0.16.1")] -[assembly: AssemblyFileVersion("0.16.1")] +[assembly: AssemblyVersion("0.17")] +[assembly: AssemblyFileVersion("0.17")] \ No newline at end of file diff --git a/src/Resources/Resources.resx b/src/Resources/Resources.resx index 18684d6..c3eb91e 100644 --- a/src/Resources/Resources.resx +++ b/src/Resources/Resources.resx @@ -112,12 +112,12 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + KeePassOTP_setup.png;System.Drawing.Bitmap, System.Drawing, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/src/Util.cs b/src/Util.cs index 1343dfe..8e8f579 100644 --- a/src/Util.cs +++ b/src/Util.cs @@ -266,6 +266,148 @@ internal static byte[] HexStringToByteArray(ProtectedString s) MemUtil.ZeroArray(strHex); return pb; } + + internal static ProtectedString ParseGoogleAuthExport(string s, out int iOTPCount) + { + iOTPCount = 0; + ProtectedString psResult = ProtectedString.Empty; + try + { + var u = new Uri(s); + var param = System.Web.HttpUtility.ParseQueryString(u.Query); + var b = Convert.FromBase64String(param["data"]); + GoogleAuthenticatorImport gi = DeserializeGoogleAuthMigrationData(b); + + iOTPCount = gi.otp_parameters.Count; + if (iOTPCount != 1) throw new ArgumentException("Expected exactly one OTP object, found: " + iOTPCount.ToString()); + var gAuthData = gi.otp_parameters[0]; + KPOTP otp = new KPOTP(); + switch (gAuthData.Algorithm) + { + case GoogleAuthenticatorImport.Algorithm.AlgorithmSha256: + otp.Hash = KPOTPHash.SHA256; + break; + case GoogleAuthenticatorImport.Algorithm.AlgorithmSha512: + otp.Hash = KPOTPHash.SHA512; + break; + default: + otp.Hash = KPOTPHash.SHA1; + break; + } + + switch (gAuthData.Type) + { + case GoogleAuthenticatorImport.OtpType.OtpTypeHotp: + otp.Type = KPOTPType.HOTP; + otp.HOTPCounter = (int)gAuthData.Counter; + break; + default: + otp.Type = KPOTPType.TOTP; + break; + } + + switch (gAuthData.Digits) + { + case GoogleAuthenticatorImport.DigitCount.DigitCountEight: + otp.Length = 8; + break; + default: + otp.Length = 6; + break; + } + + otp.Issuer = gAuthData.Issuer; + otp.Label = string.IsNullOrEmpty(gAuthData.Issuer) ? gAuthData.Name : gAuthData.Name.Remove(0, gAuthData.Issuer.Length + 1); + otp.Encoding = KPOTPEncoding.BASE32; + + byte[] bSeed = ConvertBase64ToBase32(gAuthData.Secret); + otp.OTPSeed = new ProtectedString(true, bSeed); + psResult = otp.OTPAuthString; + } + catch { } + return psResult; + } + + private static GoogleAuthenticatorImport DeserializeGoogleAuthMigrationData(byte[] b) + { + GoogleAuthenticatorImport gi = new GoogleAuthenticatorImport(); + using (var ms = new System.IO.MemoryStream()) + { + ms.Write(b, 0, b.Length); + ms.Position = 0; + gi = ProtoBuf.Serializer.Deserialize(ms); + } + return gi; + } + + private const string Base32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + private const int InByteSize = 8; + private const int OutByteSize = 5; + internal static byte[] ConvertBase64ToBase32(byte[] bytes) + { + List lBytes = new List(); + if ((bytes == null) || (bytes.Length == 0)) return lBytes.ToArray(); + + int iCurrentBytePosition = 0; + + // Offset inside a single byte that points to (from left to right) + // 0 - highest bit, 7 - lowest bit + int bytesSubPosition = 0; + //byte to look up in the base32 dictionary + byte outputBase32Byte = 0; + //The number of bits filled in the current output byte + int iCurrentOutputPosition = 0; + + // Iterate through input buffer until we reach past the end of it + while (iCurrentBytePosition < bytes.Length) + { + // Calculate the number of bits we can extract out of current input byte to fill missing bits in the output byte + int bitsAvailableInByte = Math.Min(InByteSize - bytesSubPosition, OutByteSize - iCurrentOutputPosition); + // Make space in the output byte + outputBase32Byte <<= bitsAvailableInByte; + // Extract the part of the input byte and move it to the output byte + outputBase32Byte |= (byte)(bytes[iCurrentBytePosition] >> (InByteSize - (bytesSubPosition + bitsAvailableInByte))); + // Update current sub-byte position + bytesSubPosition += bitsAvailableInByte; + + // Check overflow + if (bytesSubPosition >= InByteSize) + { + // Move to the next byte + iCurrentBytePosition++; + bytesSubPosition = 0; + } + + // Update current base32 byte completion + iCurrentOutputPosition += bitsAvailableInByte; + // Check overflow or end of input array + if (iCurrentOutputPosition >= OutByteSize) + { + // Drop the overflow bits + outputBase32Byte &= 0x1F; // 0x1F = 00011111 in binary + // Add current Base32 byte and convert it to character + lBytes.Add((byte)Base32Alphabet[outputBase32Byte]); + // Move to the next byte + iCurrentOutputPosition = 0; + } + } + + // Check if we have a remainder + if (iCurrentOutputPosition > 0) + { + // Move to the right bits + outputBase32Byte <<= (OutByteSize - iCurrentOutputPosition); + + // Drop the overflow bits + outputBase32Byte &= 0x1F; // 0x1F = 00011111 in binary + + // Add current Base32 byte and convert it to character + //builder.Append(Base32Alphabet[outputBase32Byte]); + lBytes.Add((byte)Base32Alphabet[outputBase32Byte]); + } + + return lBytes.ToArray(); + } } public static class EventHelper diff --git a/version.info b/version.info index 9f858f6..fc42afd 100644 --- a/version.info +++ b/version.info @@ -1,5 +1,5 @@ : -KeePassOTP:0.16.1 +KeePassOTP:0.17 KeePassOTP!de:10 KeePassOTP!fr:3 :