-
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
#8: Catch COMException, added logging, extracted SecurityKeyChooser i…
…nto lots of small methods to make stack traces more useful, package PDB file in CI build artifacts, added more robust argument parsing, skip when foreground window is the Alt+Tab window
- Loading branch information
Showing
8 changed files
with
392 additions
and
201 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,75 +1,87 @@ | ||
using AuthenticatorChooser.Resources; | ||
using System.Collections.Frozen; | ||
using System.Globalization; | ||
using Workshell.PE; | ||
using Workshell.PE.Resources; | ||
using Workshell.PE.Resources.Strings; | ||
|
||
namespace AuthenticatorChooser; | ||
|
||
public static class I18N { | ||
|
||
public enum Key { | ||
|
||
SECURITY_KEY, | ||
SMARTPHONE, | ||
WINDOWS, | ||
SIGN_IN_WITH_YOUR_PASSKEY | ||
|
||
} | ||
|
||
private static readonly FrozenDictionary<Key, IList<string>> STRINGS; | ||
|
||
static I18N() { | ||
StringTableResource.Register(); | ||
|
||
string localizedFilesDir = Path.Combine(Environment.GetEnvironmentVariable("SystemRoot") ?? "C:\\Windows", "System32", CultureInfo.CurrentUICulture.Name); | ||
|
||
IList<string?> fidoCredProvStrings = getPeFileStrings(Path.Combine(localizedFilesDir, "fidocredprov.dll.mui"), [ | ||
(15, 230), // Security key | ||
(15, 231), // Smartphone; also appears in webauthn.dll.mui string table 4 entries 50 and 56 | ||
(15, 232) // Windows | ||
]); | ||
|
||
IList<string?> webauthnStrings = getPeFileStrings(Path.Combine(localizedFilesDir, "webauthn.dll.mui"), [ | ||
(4, 53) // Sign In With Your Passkey title; entry 63 has the same value, not sure which one is used | ||
]); | ||
|
||
STRINGS = new Dictionary<Key, IList<string>> { | ||
[Key.SECURITY_KEY] = getUniqueNonNullStrings(Strings.securityKey, fidoCredProvStrings[0]), | ||
[Key.SMARTPHONE] = getUniqueNonNullStrings(Strings.smartphone, fidoCredProvStrings[1]), | ||
[Key.WINDOWS] = getUniqueNonNullStrings(Strings.windows, fidoCredProvStrings[2]), | ||
[Key.SIGN_IN_WITH_YOUR_PASSKEY] = getUniqueNonNullStrings(Strings.signInWithYourPasskey, webauthnStrings[0]), | ||
}.ToFrozenDictionary(); | ||
|
||
static IList<string> getUniqueNonNullStrings(params string?[] strings) => strings.Compact().Distinct(StringComparer.CurrentCulture).ToList(); | ||
} | ||
|
||
public static IEnumerable<string> getStrings(Key key) => STRINGS[key]; | ||
|
||
private static IList<string?> getPeFileStrings(string peFile, IList<(int stringTableId, int stringTableEntryId)> queries) { | ||
try { | ||
using PortableExecutableImage file = PortableExecutableImage.FromFile(peFile); | ||
|
||
IDictionary<int, StringTable?> stringTableCache = new Dictionary<int, StringTable?>(); | ||
ResourceType? stringTables = ResourceCollection.Get(file).FirstOrDefault(type => type.Id == ResourceType.String); | ||
IList<string?> results = new List<string?>(queries.Count); | ||
|
||
foreach ((int stringTableId, int stringTableEntryId) in queries) { | ||
if (!stringTableCache.TryGetValue(stringTableId, out StringTable? stringTable)) { | ||
StringTableResource? stringTableResource = stringTables?.FirstOrDefault(resource => resource.Id == stringTableId) as StringTableResource; | ||
stringTable = stringTableResource?.GetTable(stringTableResource.Languages[0]); | ||
|
||
stringTableCache[stringTableId] = stringTable; | ||
} | ||
|
||
results.Add(stringTable?.FirstOrDefault(entry => entry.Id == stringTableEntryId)?.Value); | ||
} | ||
|
||
return results; | ||
} catch (FileNotFoundException) { } catch (DirectoryNotFoundException) { } catch (PortableExecutableImageException) { } | ||
|
||
return Enumerable.Repeat<string?>(null, queries.Count).ToList(); | ||
} | ||
|
||
using AuthenticatorChooser.Resources; | ||
using Microsoft.Win32; | ||
using System.Collections.Frozen; | ||
using System.Globalization; | ||
using Workshell.PE; | ||
using Workshell.PE.Resources; | ||
using Workshell.PE.Resources.Strings; | ||
|
||
namespace AuthenticatorChooser; | ||
|
||
public static class I18N { | ||
|
||
public enum Key { | ||
|
||
SECURITY_KEY, | ||
SMARTPHONE, | ||
WINDOWS, | ||
SIGN_IN_WITH_YOUR_PASSKEY | ||
|
||
} | ||
|
||
private static readonly FrozenDictionary<Key, IList<string>> STRINGS; | ||
|
||
static I18N() { | ||
StringTableResource.Register(); | ||
|
||
string systemRoot = Environment.GetEnvironmentVariable("SystemRoot") ?? "C:\\Windows"; | ||
|
||
// #2: CredentialUIBroker.exe runs as the current user | ||
IList<string?> fidoCredProvStrings = getPeFileStrings(Path.Combine(systemRoot, "System32", getUserLocaleId(true), "fidocredprov.dll.mui"), [ | ||
(15, 230), // Security key | ||
(15, 231), // Smartphone; also appears in webauthn.dll.mui string table 4 entries 50 and 56 | ||
(15, 232) // Windows | ||
]); | ||
|
||
// #2: CryptSvc runs as NETWORK SERVICE | ||
IList<string?> webauthnStrings = getPeFileStrings(Path.Combine(systemRoot, "System32", getUserLocaleId(false), "webauthn.dll.mui"), [ | ||
(4, 53) // Sign In With Your Passkey title; entry 63 has the same value, not sure which one is used | ||
]); | ||
|
||
STRINGS = new Dictionary<Key, IList<string>> { | ||
[Key.SECURITY_KEY] = getUniqueNonNullStrings(Strings.securityKey, fidoCredProvStrings[0]), | ||
[Key.SMARTPHONE] = getUniqueNonNullStrings(Strings.smartphone, fidoCredProvStrings[1]), | ||
[Key.WINDOWS] = getUniqueNonNullStrings(Strings.windows, fidoCredProvStrings[2]), | ||
[Key.SIGN_IN_WITH_YOUR_PASSKEY] = getUniqueNonNullStrings(Strings.signInWithYourPasskey, webauthnStrings[0]), | ||
}.ToFrozenDictionary(); | ||
|
||
static IList<string> getUniqueNonNullStrings(params string?[] strings) => strings.Compact().Distinct(StringComparer.CurrentCulture).ToList(); | ||
} | ||
|
||
public static IEnumerable<string> getStrings(Key key) => STRINGS[key]; | ||
|
||
private static IList<string?> getPeFileStrings(string peFile, IList<(int stringTableId, int stringTableEntryId)> queries) { | ||
try { | ||
using PortableExecutableImage file = PortableExecutableImage.FromFile(peFile); | ||
|
||
IDictionary<int, StringTable?> stringTableCache = new Dictionary<int, StringTable?>(queries.Count); | ||
ResourceType? stringTables = ResourceCollection.Get(file).FirstOrDefault(type => type.Id == ResourceType.String); | ||
IList<string?> results = new List<string?>(queries.Count); | ||
|
||
foreach ((int stringTableId, int stringTableEntryId) in queries) { | ||
if (!stringTableCache.TryGetValue(stringTableId, out StringTable? stringTable)) { | ||
StringTableResource? stringTableResource = stringTables?.FirstOrDefault(resource => resource.Id == stringTableId) as StringTableResource; | ||
stringTable = stringTableResource?.GetTable(stringTableResource.Languages[0]); // #2: use the table's language, not always English | ||
|
||
stringTableCache[stringTableId] = stringTable; | ||
} | ||
|
||
results.Add(stringTable?.FirstOrDefault(entry => entry.Id == stringTableEntryId)?.Value); | ||
} | ||
|
||
return results; | ||
} catch (FileNotFoundException) { } catch (DirectoryNotFoundException) { } catch (PortableExecutableImageException) { } | ||
|
||
return Enumerable.Repeat<string?>(null, queries.Count).ToList(); | ||
} | ||
|
||
/// <summary> | ||
/// Get the current locale tag of the user or computer. | ||
/// </summary> | ||
/// <param name="currentUser"><c>true</c> to get the current user's locale, or <c>false</c> to get the locale of the system — specifically, the <c>NETWORK SERVICE</c> user</param> | ||
/// <returns>locale name, such as <c>en-US</c></returns> | ||
public static string getUserLocaleId(bool currentUser) => currentUser | ||
? CultureInfo.CurrentUICulture.Name | ||
: (string) (Registry.GetValue(@"HKEY_USERS\S-1-5-20\Control Panel\International", "LocaleName", null) ?? string.Empty); | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
using NLog; | ||
using NLog.Config; | ||
using NLog.Layouts; | ||
using NLog.MessageTemplates; | ||
using NLog.Targets; | ||
using System.Text; | ||
|
||
namespace AuthenticatorChooser; | ||
|
||
internal static class Logging { | ||
|
||
private static readonly SimpleLayout MESSAGE_FORMAT = new( | ||
" ${level:format=FirstCharacter:lowercase=true} | ${date:format=yyyy-MM-dd HH\\:mm\\:ss.fff} | ${logger:shortName=true:padding=-18} | ${message:withException=true:exceptionSeparator=\n}"); | ||
|
||
public static void initialize(bool enableFileAppender, string? logFilename) { | ||
logFilename ??= Path.Combine(Path.GetTempPath(), Path.ChangeExtension(nameof(AuthenticatorChooser), ".log")); | ||
|
||
LoggingConfiguration logConfig = new(); | ||
ServiceRepository services = logConfig.LogFactory.ServiceRepository; | ||
services.RegisterService(typeof(IValueFormatter), new UnfuckedValueFormatter((IValueFormatter) services.GetService(typeof(IValueFormatter))!)); | ||
|
||
if (enableFileAppender) { | ||
logConfig.AddRule(LogLevel.Trace, LogLevel.Fatal, new FileTarget("fileAppender") { | ||
Layout = MESSAGE_FORMAT, | ||
FileName = logFilename, | ||
CleanupFileName = true | ||
}); | ||
} | ||
|
||
logConfig.AddRule(LogLevel.Trace, LogLevel.Fatal, new ConsoleTarget("consoleAppender") { | ||
Layout = MESSAGE_FORMAT, | ||
DetectConsoleAvailable = true | ||
}); | ||
|
||
LogManager.Configuration = logConfig; | ||
} | ||
|
||
/// <summary> | ||
/// When logging strings to NLog using structured logging, don't surround them with quotation marks, because it looks stupid | ||
/// </summary> | ||
/// <param name="parent">Built-in <see cref="ValueFormatter"/></param> | ||
private class UnfuckedValueFormatter(IValueFormatter parent): IValueFormatter { | ||
|
||
public bool FormatValue(object value, string format, CaptureType captureType, IFormatProvider formatProvider, StringBuilder builder) { | ||
switch (value) { | ||
case string s: | ||
builder.Append(s); | ||
return true; | ||
case StringBuilder s: | ||
builder.Append(s); | ||
return true; | ||
case ReadOnlyMemory<char> s: | ||
builder.Append(s); | ||
return true; | ||
default: | ||
return parent.FormatValue(value, format, captureType, formatProvider, builder); | ||
} | ||
} | ||
|
||
} | ||
|
||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.