diff --git a/Core/Helpers/IsolatedUserManager.cs b/Core/Helpers/IsolatedUserManager.cs index 7518460..c7442c8 100644 --- a/Core/Helpers/IsolatedUserManager.cs +++ b/Core/Helpers/IsolatedUserManager.cs @@ -23,15 +23,18 @@ namespace OpenStreamMS.Core.Helpers; /// - Sin driver kernel, sin licencias, sin binarios externos. /// /// SEGURIDAD: -/// - Contraseña aleatoria (24 chars, 4 clases) ROTADA en cada arranque y nunca -/// persistida en disco — solo vive en memoria durante el arranque de la sesión. +/// - Contraseña aleatoria (24 chars, 4 clases), guardada como secreto local de LSA +/// (, solo legible por SYSTEM) y reutilizada en cada +/// arranque. No se rota: restablecer la contraseña de una cuenta local deja inservibles +/// sus claves DPAPI, y con ellas el inicio de sesión guardado de Steam y las contraseñas +/// de los navegadores. Solo se restablece si no hay secreto o ya no vale. /// - Usuario oculto de la pantalla de login (SpecialAccounts\UserList). /// - Miembro únicamente de Users + Remote Desktop Users. Donde no existe ese grupo /// (Windows Home), recibe directamente el derecho de inicio de sesión remoto y /// acceso a la escucha RDP-Tcp; se le quitan al borrar la cuenta. /// /// CICLO DE VIDA: -/// - EnsureUser → crea el usuario (o rota la contraseña si ya existe). +/// - EnsureUser → crea el usuario (o reutiliza su contraseña si ya existe). /// - TryDeleteProfile → borra C:\Users\osms-... + hive de registro (modo efímero). /// - RemoveUser → perfil + cuenta + entrada de registro (al eliminar la sesión, /// salvo que se pida conservarlo para reutilizarlo en otra). @@ -71,6 +74,16 @@ static extern int NetUserGetLocalGroups(string? servername, string username, int [DllImport("netapi32.dll")] static extern int NetApiBufferFree(IntPtr buffer); + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern bool LogonUser(string user, string domain, string password, int logonType, int provider, out IntPtr token); + + [DllImport("kernel32.dll")] + static extern bool CloseHandle(IntPtr handle); + + const int LOGON32_LOGON_NETWORK = 3; + const int LOGON32_PROVIDER_DEFAULT = 0; + const int ERROR_LOGON_FAILURE = 1326; + [DllImport("userenv.dll", CharSet = CharSet.Unicode, SetLastError = true)] static extern bool DeleteProfile(string lpSidString, string? lpProfilePath, string? lpComputerName); @@ -128,9 +141,9 @@ internal static string GetUsername(Guid sessionId) } /// - /// Crea el usuario local dedicado de la sesión (o rota su contraseña si ya existe), - /// lo añade a Users + Remote Desktop Users y lo oculta de la pantalla de login. - /// Devuelve credenciales listas para FreeRDP. La contraseña no se persiste nunca. + /// Crea el usuario local dedicado de la sesión (o reutiliza su contraseña guardada si ya + /// existe), lo añade a Users + Remote Desktop Users y lo oculta de la pantalla de login. + /// Devuelve credenciales listas para FreeRDP. Ver . /// internal static IsolatedCredentials EnsureUser(string username, string sessionName) { @@ -153,15 +166,24 @@ internal static IsolatedCredentials EnsureUser(string username, string sessionNa { case NERR_Success: Logger.Log(BackendText.Get("isolatedUser.userCreated", username, sessionName)); + KeepPassword(username, password); break; case NERR_UserExists: + // La de la vez anterior, si sigue valiendo: así no se pierden las claves DPAPI. + if (TryGetSecret(username) is { } kept && PasswordWorks(username, kept)) + { + password = kept; + Logger.Log(BackendText.Get("isolatedUser.passwordKept", username)); + break; + } var reset = new USER_INFO_1003 { usri1003_password = password }; int rcSet = NetUserSetInfo(null, username, 1003, ref reset, out _); if (rcSet != NERR_Success) throw new InvalidOperationException( BackendText.Get("isolatedUser.passwordRotationFailed", username, rcSet)); Logger.Log(BackendText.Get("isolatedUser.passwordRotated", username)); + KeepPassword(username, password); break; default: @@ -180,6 +202,37 @@ internal static IsolatedCredentials EnsureUser(string username, string sessionNa return new IsolatedCredentials(username, password); } + static string? TryGetSecret(string username) + { + try { return IsolatedUserSecret.TryGet(username); } + catch (Exception ex) + { + Logger.Warning(BackendText.Get("isolatedUser.passwordStoreFailed", username, ex.Message)); + return null; + } + } + + static void KeepPassword(string username, string password) + { + try { IsolatedUserSecret.Store(username, password); } + catch (Exception ex) { Logger.Warning(BackendText.Get("isolatedUser.passwordStoreFailed", username, ex.Message)); } + } + + /// + /// La contraseña sigue siendo la de la cuenta. Inicio de sesión de red: no crea perfil ni + /// sesión. Solo "usuario o contraseña incorrectos" cuenta como no válida; otro error (por + /// ejemplo, que la directiva no permita ese tipo de inicio) ya implica que era correcta. + /// + static bool PasswordWorks(string username, string password) + { + if (LogonUser(username, ".", password, LOGON32_LOGON_NETWORK, LOGON32_PROVIDER_DEFAULT, out var token)) + { + CloseHandle(token); + return true; + } + return Marshal.GetLastWin32Error() != ERROR_LOGON_FAILURE; + } + /// /// Borra el perfil del usuario aislado (directorio + hive NTUSER.DAT) vía /// DeleteProfileW. Devuelve true si el perfil quedó eliminado o no existía. @@ -280,6 +333,8 @@ internal static void RemoveUser(string username) Logger.Warning(BackendText.Get("isolatedUser.netUserDelFailed", username, rc)); UnhideFromLoginScreen(username); + try { IsolatedUserSecret.Delete(username); } + catch (Exception ex) { Logger.Warning(BackendText.Get("isolatedUser.passwordStoreFailed", username, ex.Message)); } // Justo tras el logoff profsvc aún tiene el perfil cargado y DeleteProfile falla: // antes eso dejaba la carpeta C:\Users\osms-... para siempre. Se reintenta por SID, diff --git a/Core/Helpers/IsolatedUserSecret.cs b/Core/Helpers/IsolatedUserSecret.cs new file mode 100644 index 0000000..56a54a9 --- /dev/null +++ b/Core/Helpers/IsolatedUserSecret.cs @@ -0,0 +1,142 @@ +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace OpenStreamMS.Core.Helpers; + +/// +/// Contraseña de cada usuario aislado (osms-…), guardada como secreto local de LSA +/// (L$OpenStreamMS_<usuario>): Windows lo cifra y solo lo puede leer SYSTEM. +/// +/// Por qué se guarda: restablecer la contraseña de una cuenta local (lo que hacía cada arranque) +/// deja inservibles sus claves DPAPI, que dependen de la contraseña. Steam guarda su inicio de +/// sesión con DPAPI, así que recordaba la cuenta pero pedía QR o contraseña cada vez; lo mismo +/// pasaba con las contraseñas guardadas en los navegadores de la sesión. +/// +/// +internal static class IsolatedUserSecret +{ + [StructLayout(LayoutKind.Sequential)] + private struct LSA_UNICODE_STRING + { + public ushort Length; + public ushort MaximumLength; + public IntPtr Buffer; + } + + [StructLayout(LayoutKind.Sequential)] + private struct LSA_OBJECT_ATTRIBUTES + { + public int Length; + public IntPtr RootDirectory, ObjectName; + public int Attributes; + public IntPtr SecurityDescriptor, SecurityQualityOfService; + } + + [DllImport("advapi32.dll")] private static extern uint LsaOpenPolicy(IntPtr system, ref LSA_OBJECT_ATTRIBUTES attrs, int access, out IntPtr handle); + [DllImport("advapi32.dll")] private static extern uint LsaStorePrivateData(IntPtr policy, ref LSA_UNICODE_STRING key, IntPtr data); + [DllImport("advapi32.dll")] private static extern uint LsaRetrievePrivateData(IntPtr policy, ref LSA_UNICODE_STRING key, out IntPtr data); + [DllImport("advapi32.dll")] private static extern uint LsaFreeMemory(IntPtr buffer); + [DllImport("advapi32.dll")] private static extern uint LsaClose(IntPtr handle); + [DllImport("advapi32.dll")] private static extern int LsaNtStatusToWinError(uint status); + + private const int POLICY_CREATE_SECRET = 0x00000020; + private const int POLICY_GET_PRIVATE_INFORMATION = 0x00000004; + private const uint STATUS_OBJECT_NAME_NOT_FOUND = 0xC0000034; + + /// Nombre del secreto: "L$" lo hace local (nunca sale del equipo ni se replica). + internal static string KeyName(string username) => "L$OpenStreamMS_" + username.ToLowerInvariant(); + + /// La contraseña guardada para , o null si no hay. + internal static string? TryGet(string username) + { + return WithPolicy(policy => WithKey(username, (ref LSA_UNICODE_STRING key) => + { + uint st = LsaRetrievePrivateData(policy, ref key, out var data); + if (st == STATUS_OBJECT_NAME_NOT_FOUND) return null; + Check(st, "LsaRetrievePrivateData"); + try + { + if (data == IntPtr.Zero) return null; + var s = Marshal.PtrToStructure(data); + return s.Length == 0 ? null : Marshal.PtrToStringUni(s.Buffer, s.Length / 2); + } + finally + { + if (data != IntPtr.Zero) LsaFreeMemory(data); + } + })); + } + + /// Guarda (o sustituye) la contraseña de . + internal static void Store(string username, string password) + { + WithPolicy(policy => WithKey(username, (ref LSA_UNICODE_STRING key) => + { + var buffer = Marshal.StringToHGlobalUni(password); + var value = new LSA_UNICODE_STRING + { + Length = (ushort)(password.Length * 2), + MaximumLength = (ushort)(password.Length * 2), + Buffer = buffer, + }; + var valuePtr = Marshal.AllocHGlobal(Marshal.SizeOf()); + try + { + Marshal.StructureToPtr(value, valuePtr, false); + Check(LsaStorePrivateData(policy, ref key, valuePtr), "LsaStorePrivateData"); + return (string?)null; + } + finally + { + Marshal.FreeHGlobal(valuePtr); + Marshal.ZeroFreeGlobalAllocUnicode(buffer); + } + })); + } + + /// Borra el secreto de (si no existe, no hace nada). + internal static void Delete(string username) + { + WithPolicy(policy => WithKey(username, (ref LSA_UNICODE_STRING key) => + { + uint st = LsaStorePrivateData(policy, ref key, IntPtr.Zero); + if (st != STATUS_OBJECT_NAME_NOT_FOUND) Check(st, "LsaStorePrivateData"); + return (string?)null; + })); + } + + private delegate string? KeyAction(ref LSA_UNICODE_STRING key); + + private static string? WithKey(string username, KeyAction action) + { + var name = KeyName(username); + var buffer = Marshal.StringToHGlobalUni(name); + try + { + var key = new LSA_UNICODE_STRING + { + Length = (ushort)(name.Length * 2), + MaximumLength = (ushort)((name.Length + 1) * 2), + Buffer = buffer, + }; + return action(ref key); + } + finally + { + Marshal.FreeHGlobal(buffer); + } + } + + private static string? WithPolicy(Func action) + { + var attrs = new LSA_OBJECT_ATTRIBUTES { Length = Marshal.SizeOf() }; + Check(LsaOpenPolicy(IntPtr.Zero, ref attrs, POLICY_CREATE_SECRET | POLICY_GET_PRIVATE_INFORMATION, out var policy), "LsaOpenPolicy"); + try { return action(policy); } + finally { LsaClose(policy); } + } + + private static void Check(uint status, string what) + { + if (status != 0) throw new Win32Exception(LsaNtStatusToWinError(status), what); + } +} diff --git a/Services/Localization/Locales/bg.json b/Services/Localization/Locales/bg.json index ed2e108..fc3ca01 100644 --- a/Services/Localization/Locales/bg.json +++ b/Services/Localization/Locales/bg.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Не може да се изтрие профилът '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Останала папка '{0}' изтрита.", "isolatedUser.errorAddingToGroup": "Не може да се добави '{0}' към група '{1}': грешка {2}.", - "isolatedUser.errorRemovingRights": "Не може да се премахнат правата за '{0}': {1}." + "isolatedUser.errorRemovingRights": "Не може да се премахнат правата за '{0}': {1}.", + "isolatedUser.passwordKept": "Потребителят '{0}' вече съществуваше; паролата му се запазва (Steam и другите запазени влизания остават валидни).", + "isolatedUser.passwordStoreFailed": "Паролата на '{0}' не можа да бъде запазена или прочетена в LSA: {1}. Тя ще бъде нулирана при следващото стартиране." } \ No newline at end of file diff --git a/Services/Localization/Locales/cs.json b/Services/Localization/Locales/cs.json index f4310da..b48b0ed 100644 --- a/Services/Localization/Locales/cs.json +++ b/Services/Localization/Locales/cs.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Nelze smazat profil '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Zbylá složka '{0}' smazána.", "isolatedUser.errorAddingToGroup": "Nepodařilo se přidat '{0}' do skupiny '{1}': chyba {2}.", - "isolatedUser.errorRemovingRights": "Nepodařilo se odebrat práva pro '{0}': {1}." + "isolatedUser.errorRemovingRights": "Nepodařilo se odebrat práva pro '{0}': {1}.", + "isolatedUser.passwordKept": "Uživatel '{0}' již existoval; jeho heslo se zachovává (Steam a další uložená přihlášení zůstávají platná).", + "isolatedUser.passwordStoreFailed": "Heslo uživatele '{0}' nelze uložit ani přečíst v LSA: {1}. Při příštím spuštění bude obnoveno." } \ No newline at end of file diff --git a/Services/Localization/Locales/de.json b/Services/Localization/Locales/de.json index 9fb0956..cfc711f 100644 --- a/Services/Localization/Locales/de.json +++ b/Services/Localization/Locales/de.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Profil '{0}' konnte nicht gelöscht werden: {1}.", "isolatedUser.orphanFolderDeleted": "Verwaistes Verzeichnis '{0}' gelöscht.", "isolatedUser.errorAddingToGroup": "'{0}' konnte nicht zur Gruppe '{1}' hinzugefügt werden: Fehler {2}.", - "isolatedUser.errorRemovingRights": "Rechte für '{0}' konnten nicht entfernt werden: {1}." + "isolatedUser.errorRemovingRights": "Rechte für '{0}' konnten nicht entfernt werden: {1}.", + "isolatedUser.passwordKept": "Benutzer '{0}' war bereits vorhanden; sein Kennwort bleibt erhalten (Steam und andere gespeicherte Anmeldungen bleiben gültig).", + "isolatedUser.passwordStoreFailed": "Das Kennwort von '{0}' konnte in LSA nicht gespeichert oder gelesen werden: {1}. Es wird beim nächsten Start zurückgesetzt." } \ No newline at end of file diff --git a/Services/Localization/Locales/en.json b/Services/Localization/Locales/en.json index 9b044ad..4521f22 100644 --- a/Services/Localization/Locales/en.json +++ b/Services/Localization/Locales/en.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Could not delete profile '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Orphan folder '{0}' deleted.", "isolatedUser.errorAddingToGroup": "Could not add '{0}' to group '{1}': error {2}.", - "isolatedUser.errorRemovingRights": "Could not remove rights for '{0}': {1}." + "isolatedUser.errorRemovingRights": "Could not remove rights for '{0}': {1}.", + "isolatedUser.passwordKept": "User '{0}' already existed; its password is kept (Steam and other saved logins stay valid).", + "isolatedUser.passwordStoreFailed": "Could not save or read the password of '{0}' in LSA: {1}. It will be reset on the next start." } \ No newline at end of file diff --git a/Services/Localization/Locales/en_GB.json b/Services/Localization/Locales/en_GB.json index 4f86772..52ce867 100644 --- a/Services/Localization/Locales/en_GB.json +++ b/Services/Localization/Locales/en_GB.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Could not delete profile '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Orphan folder '{0}' deleted.", "isolatedUser.errorAddingToGroup": "Could not add '{0}' to group '{1}': error {2}.", - "isolatedUser.errorRemovingRights": "Could not remove rights for '{0}': {1}." + "isolatedUser.errorRemovingRights": "Could not remove rights for '{0}': {1}.", + "isolatedUser.passwordKept": "User '{0}' already existed; its password is kept (Steam and other saved logins stay valid).", + "isolatedUser.passwordStoreFailed": "Could not save or read the password of '{0}' in LSA: {1}. It will be reset on the next start." } \ No newline at end of file diff --git a/Services/Localization/Locales/en_US.json b/Services/Localization/Locales/en_US.json index d495a90..db4f032 100644 --- a/Services/Localization/Locales/en_US.json +++ b/Services/Localization/Locales/en_US.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Could not delete profile '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Orphan folder '{0}' deleted.", "isolatedUser.errorAddingToGroup": "Could not add '{0}' to group '{1}': error {2}.", - "isolatedUser.errorRemovingRights": "Could not remove rights for '{0}': {1}." + "isolatedUser.errorRemovingRights": "Could not remove rights for '{0}': {1}.", + "isolatedUser.passwordKept": "User '{0}' already existed; its password is kept (Steam and other saved logins stay valid).", + "isolatedUser.passwordStoreFailed": "Could not save or read the password of '{0}' in LSA: {1}. It will be reset on the next start." } \ No newline at end of file diff --git a/Services/Localization/Locales/es.json b/Services/Localization/Locales/es.json index afa1827..513c10b 100644 --- a/Services/Localization/Locales/es.json +++ b/Services/Localization/Locales/es.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "No se pudo borrar el perfil '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Carpeta huérfana '{0}' eliminada.", "isolatedUser.errorAddingToGroup": "No se pudo añadir '{0}' al grupo '{1}': error {2}.", - "isolatedUser.errorRemovingRights": "No se pudieron quitar los derechos de '{0}': {1}." + "isolatedUser.errorRemovingRights": "No se pudieron quitar los derechos de '{0}': {1}.", + "isolatedUser.passwordKept": "Usuario '{0}' ya existía; se conserva su contraseña (Steam y otros inicios de sesión guardados siguen valiendo).", + "isolatedUser.passwordStoreFailed": "No se pudo guardar o leer la contraseña de '{0}' en LSA: {1}. Se restablecerá en el siguiente arranque." } \ No newline at end of file diff --git a/Services/Localization/Locales/fr.json b/Services/Localization/Locales/fr.json index d80c20f..1053db0 100644 --- a/Services/Localization/Locales/fr.json +++ b/Services/Localization/Locales/fr.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "No se pudo borrar el perfil '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Carpeta huérfana '{0}' eliminada.", "isolatedUser.errorAddingToGroup": "No se pudo añadir '{0}' al grupo '{1}': error {2}.", - "isolatedUser.errorRemovingRights": "No se pudieron quitar los derechos de '{0}': {1}." + "isolatedUser.errorRemovingRights": "No se pudieron quitar los derechos de '{0}': {1}.", + "isolatedUser.passwordKept": "L'utilisateur '{0}' existait déjà ; son mot de passe est conservé (Steam et les autres connexions enregistrées restent valides).", + "isolatedUser.passwordStoreFailed": "Impossible d'enregistrer ou de lire le mot de passe de '{0}' dans LSA : {1}. Il sera réinitialisé au prochain démarrage." } \ No newline at end of file diff --git a/Services/Localization/Locales/hu.json b/Services/Localization/Locales/hu.json index 0c6f792..fb9d3dc 100644 --- a/Services/Localization/Locales/hu.json +++ b/Services/Localization/Locales/hu.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "A '{0}' profil nem törölhető: {1}.", "isolatedUser.orphanFolderDeleted": "Ártatlan mappa '{0}' törölve.", "isolatedUser.errorAddingToGroup": "A '{0}' nem adható hozzá a '{1}' csoporthoz: hiba {2}.", - "isolatedUser.errorRemovingRights": "A '{0}' jogai nem vonhatók ki: {1}." + "isolatedUser.errorRemovingRights": "A '{0}' jogai nem vonhatók ki: {1}.", + "isolatedUser.passwordKept": "A(z) '{0}' felhasználó már létezett; a jelszava megmarad (a Steam és más mentett bejelentkezések érvényesek maradnak).", + "isolatedUser.passwordStoreFailed": "Nem sikerült menteni vagy olvasni a(z) '{0}' jelszavát az LSA-ban: {1}. A következő indításkor alaphelyzetbe áll." } \ No newline at end of file diff --git a/Services/Localization/Locales/it.json b/Services/Localization/Locales/it.json index 5275fc9..e729f48 100644 --- a/Services/Localization/Locales/it.json +++ b/Services/Localization/Locales/it.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Impossibile eliminare il profilo '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Cartella orfana '{0}' eliminata.", "isolatedUser.errorAddingToGroup": "Impossibile aggiungere '{0}' al gruppo '{1}': errore {2}.", - "isolatedUser.errorRemovingRights": "Impossibile rimuovere i diritti per '{0}': {1}." + "isolatedUser.errorRemovingRights": "Impossibile rimuovere i diritti per '{0}': {1}.", + "isolatedUser.passwordKept": "L'utente '{0}' esisteva già; la sua password viene mantenuta (Steam e gli altri accessi salvati restano validi).", + "isolatedUser.passwordStoreFailed": "Impossibile salvare o leggere la password di '{0}' in LSA: {1}. Verrà reimpostata al prossimo avvio." } \ No newline at end of file diff --git a/Services/Localization/Locales/ja.json b/Services/Localization/Locales/ja.json index 19a3e59..cfa6bd3 100644 --- a/Services/Localization/Locales/ja.json +++ b/Services/Localization/Locales/ja.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "プロファイル '{0}' を削除できません: {1}。", "isolatedUser.orphanFolderDeleted": "孤立フォルダー '{0}' を削除しました。", "isolatedUser.errorAddingToGroup": "'{0}' をグループ '{1}' に追加できません: エラー {2}。", - "isolatedUser.errorRemovingRights": "'{0}' の権利を削除できません: {1}。" + "isolatedUser.errorRemovingRights": "'{0}' の権利を削除できません: {1}。", + "isolatedUser.passwordKept": "ユーザー '{0}' は既に存在していました。パスワードはそのまま保持されます (Steam などの保存済みログインは引き続き有効です)。", + "isolatedUser.passwordStoreFailed": "'{0}' のパスワードを LSA に保存または読み取りできませんでした: {1}。次回の起動時にリセットされます。" } \ No newline at end of file diff --git a/Services/Localization/Locales/ko.json b/Services/Localization/Locales/ko.json index d735333..da76c40 100644 --- a/Services/Localization/Locales/ko.json +++ b/Services/Localization/Locales/ko.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "프로필 '{0}'을(를) 삭제할 수 없습니다: {1}.", "isolatedUser.orphanFolderDeleted": "고아 폴더 '{0}'이(가) 삭제되었습니다.", "isolatedUser.errorAddingToGroup": "'{0}'을(를) 그룹 '{1}'에 추가할 수 없습니다: 오류 {2}.", - "isolatedUser.errorRemovingRights": "'{0}'의 권한을 제거할 수 없습니다: {1}." + "isolatedUser.errorRemovingRights": "'{0}'의 권한을 제거할 수 없습니다: {1}.", + "isolatedUser.passwordKept": "사용자 '{0}'이(가) 이미 있습니다. 암호가 유지됩니다(Steam 및 기타 저장된 로그인은 계속 유효합니다).", + "isolatedUser.passwordStoreFailed": "LSA에서 '{0}'의 암호를 저장하거나 읽을 수 없습니다: {1}. 다음 시작 시 재설정됩니다." } \ No newline at end of file diff --git a/Services/Localization/Locales/pl.json b/Services/Localization/Locales/pl.json index 7c5d86a..a1eb90c 100644 --- a/Services/Localization/Locales/pl.json +++ b/Services/Localization/Locales/pl.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Nie można usunąć profilu '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Pozostali folder '{0}' usunięty.", "isolatedUser.errorAddingToGroup": "Nie można dodać '{0}' do grupy '{1}': błąd {2}.", - "isolatedUser.errorRemovingRights": "Nie można usunąć uprawnień dla '{0}': {1}." + "isolatedUser.errorRemovingRights": "Nie można usunąć uprawnień dla '{0}': {1}.", + "isolatedUser.passwordKept": "Użytkownik '{0}' już istniał; jego hasło zostaje zachowane (Steam i inne zapisane logowania pozostają ważne).", + "isolatedUser.passwordStoreFailed": "Nie udało się zapisać ani odczytać hasła '{0}' w LSA: {1}. Zostanie zresetowane przy następnym uruchomieniu." } \ No newline at end of file diff --git a/Services/Localization/Locales/pt.json b/Services/Localization/Locales/pt.json index 28bdc41..65091cb 100644 --- a/Services/Localization/Locales/pt.json +++ b/Services/Localization/Locales/pt.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Não foi possível apagar o perfil '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Pasta órfã '{0}' eliminada.", "isolatedUser.errorAddingToGroup": "Não foi possível adicionar '{0}' ao grupo '{1}': erro {2}.", - "isolatedUser.errorRemovingRights": "Não foi possível remover os direitos de '{0}': {1}." + "isolatedUser.errorRemovingRights": "Não foi possível remover os direitos de '{0}': {1}.", + "isolatedUser.passwordKept": "O utilizador '{0}' já existia; a sua palavra-passe é mantida (o Steam e outros inícios de sessão guardados continuam válidos).", + "isolatedUser.passwordStoreFailed": "Não foi possível guardar ou ler a palavra-passe de '{0}' no LSA: {1}. Será reposta no próximo arranque." } \ No newline at end of file diff --git a/Services/Localization/Locales/pt_BR.json b/Services/Localization/Locales/pt_BR.json index 024fb1b..6830fc4 100644 --- a/Services/Localization/Locales/pt_BR.json +++ b/Services/Localization/Locales/pt_BR.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Não foi possível excluir o perfil '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Pasta órfã '{0}' excluída.", "isolatedUser.errorAddingToGroup": "Não foi possível adicionar '{0}' ao grupo '{1}': erro {2}.", - "isolatedUser.errorRemovingRights": "Não foi possível remover os direitos de '{0}': {1}." + "isolatedUser.errorRemovingRights": "Não foi possível remover os direitos de '{0}': {1}.", + "isolatedUser.passwordKept": "O usuário '{0}' já existia; a senha dele é mantida (o Steam e outros logins salvos continuam válidos).", + "isolatedUser.passwordStoreFailed": "Não foi possível salvar ou ler a senha de '{0}' no LSA: {1}. Ela será redefinida na próxima inicialização." } \ No newline at end of file diff --git a/Services/Localization/Locales/ru.json b/Services/Localization/Locales/ru.json index c94995e..84334b5 100644 --- a/Services/Localization/Locales/ru.json +++ b/Services/Localization/Locales/ru.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Не удалось удалить профиль '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Оставшаяся папка '{0}' удалена.", "isolatedUser.errorAddingToGroup": "Не удалось добавить '{0}' в группу '{1}': ошибка {2}.", - "isolatedUser.errorRemovingRights": "Не удалось удалить права для '{0}': {1}." + "isolatedUser.errorRemovingRights": "Не удалось удалить права для '{0}': {1}.", + "isolatedUser.passwordKept": "Пользователь '{0}' уже существовал; его пароль сохраняется (Steam и другие сохранённые входы остаются действительными).", + "isolatedUser.passwordStoreFailed": "Не удалось сохранить или прочитать пароль '{0}' в LSA: {1}. Он будет сброшен при следующем запуске." } \ No newline at end of file diff --git a/Services/Localization/Locales/sv.json b/Services/Localization/Locales/sv.json index e373cba..53607c7 100644 --- a/Services/Localization/Locales/sv.json +++ b/Services/Localization/Locales/sv.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Kunde inte ta bort profil '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Överbliven mapp '{0}' borttagen.", "isolatedUser.errorAddingToGroup": "Kunde inte lägga till '{0}' i grupp '{1}': fel {2}.", - "isolatedUser.errorRemovingRights": "Kunde inte ta bort rättigheter för '{0}': {1}." + "isolatedUser.errorRemovingRights": "Kunde inte ta bort rättigheter för '{0}': {1}.", + "isolatedUser.passwordKept": "Användaren '{0}' fanns redan; lösenordet behålls (Steam och andra sparade inloggningar fortsätter att gälla).", + "isolatedUser.passwordStoreFailed": "Det gick inte att spara eller läsa lösenordet för '{0}' i LSA: {1}. Det återställs vid nästa start." } \ No newline at end of file diff --git a/Services/Localization/Locales/tr.json b/Services/Localization/Locales/tr.json index e115d7c..4ff4bb1 100644 --- a/Services/Localization/Locales/tr.json +++ b/Services/Localization/Locales/tr.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Profil '{0}' silinemedi: {1}.", "isolatedUser.orphanFolderDeleted": "Kalıntı klasör '{0}' silindi.", "isolatedUser.errorAddingToGroup": "'{0}' '{1}' grubuna eklenemedi: hata {2}.", - "isolatedUser.errorRemovingRights": "'{0}' hakları kaldırılamadı: {1}." + "isolatedUser.errorRemovingRights": "'{0}' hakları kaldırılamadı: {1}.", + "isolatedUser.passwordKept": "'{0}' kullanıcısı zaten vardı; parolası korunuyor (Steam ve diğer kayıtlı oturum açmalar geçerli kalır).", + "isolatedUser.passwordStoreFailed": "'{0}' parolası LSA'ya kaydedilemedi veya okunamadı: {1}. Bir sonraki başlatmada sıfırlanacak." } \ No newline at end of file diff --git a/Services/Localization/Locales/uk.json b/Services/Localization/Locales/uk.json index c9c1268..db545c4 100644 --- a/Services/Localization/Locales/uk.json +++ b/Services/Localization/Locales/uk.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Не вдалося видалити профіль '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Залишкова папка '{0}' видалена.", "isolatedUser.errorAddingToGroup": "Не вдалося додати '{0}' до групи '{1}': помилка {2}.", - "isolatedUser.errorRemovingRights": "Не вдалося видалити права для '{0}': {1}." + "isolatedUser.errorRemovingRights": "Не вдалося видалити права для '{0}': {1}.", + "isolatedUser.passwordKept": "Користувач '{0}' уже існував; його пароль зберігається (Steam та інші збережені входи залишаються дійсними).", + "isolatedUser.passwordStoreFailed": "Не вдалося зберегти або прочитати пароль '{0}' в LSA: {1}. Його буде скинуто під час наступного запуску." } \ No newline at end of file diff --git a/Services/Localization/Locales/vi.json b/Services/Localization/Locales/vi.json index 2f1d8db..1eab2df 100644 --- a/Services/Localization/Locales/vi.json +++ b/Services/Localization/Locales/vi.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "Không thể xóa hồ sơ '{0}': {1}.", "isolatedUser.orphanFolderDeleted": "Thư mục mồ côi '{0}' đã bị xóa.", "isolatedUser.errorAddingToGroup": "Không thể thêm '{0}' vào nhóm '{1}': lỗi {2}.", - "isolatedUser.errorRemovingRights": "Không thể xóa quyền của '{0}': {1}." + "isolatedUser.errorRemovingRights": "Không thể xóa quyền của '{0}': {1}.", + "isolatedUser.passwordKept": "Người dùng '{0}' đã tồn tại; mật khẩu được giữ nguyên (Steam và các thông tin đăng nhập đã lưu khác vẫn hợp lệ).", + "isolatedUser.passwordStoreFailed": "Không thể lưu hoặc đọc mật khẩu của '{0}' trong LSA: {1}. Mật khẩu sẽ được đặt lại ở lần khởi động tiếp theo." } \ No newline at end of file diff --git a/Services/Localization/Locales/zh.json b/Services/Localization/Locales/zh.json index 57f7405..8f2e3e0 100644 --- a/Services/Localization/Locales/zh.json +++ b/Services/Localization/Locales/zh.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "无法删除配置文件 '{0}': {1}。", "isolatedUser.orphanFolderDeleted": "孤立文件夹 '{0}' 已删除。", "isolatedUser.errorAddingToGroup": "无法将 '{0}' 添加到组 '{1}': 错误 {2}。", - "isolatedUser.errorRemovingRights": "无法移除 '{0}' 的权限: {1}。" + "isolatedUser.errorRemovingRights": "无法移除 '{0}' 的权限: {1}。", + "isolatedUser.passwordKept": "用户 '{0}' 已存在;保留其密码(Steam 和其他已保存的登录仍然有效)。", + "isolatedUser.passwordStoreFailed": "无法在 LSA 中保存或读取 '{0}' 的密码:{1}。下次启动时将重置。" } \ No newline at end of file diff --git a/Services/Localization/Locales/zh_TW.json b/Services/Localization/Locales/zh_TW.json index 6f4c034..f6bb8de 100644 --- a/Services/Localization/Locales/zh_TW.json +++ b/Services/Localization/Locales/zh_TW.json @@ -82,5 +82,7 @@ "isolatedUser.profileDeleteError": "無法刪除設定檔 '{0}': {1}。", "isolatedUser.orphanFolderDeleted": "孤立資料夾 '{0}' 已刪除。", "isolatedUser.errorAddingToGroup": "無法將 '{0}' 新增到群組 '{1}': 錯誤 {2}。", - "isolatedUser.errorRemovingRights": "無法移除 '{0}' 的權限: {1}。" + "isolatedUser.errorRemovingRights": "無法移除 '{0}' 的權限: {1}。", + "isolatedUser.passwordKept": "使用者 '{0}' 已存在;保留其密碼(Steam 和其他已儲存的登入仍然有效)。", + "isolatedUser.passwordStoreFailed": "無法在 LSA 中儲存或讀取 '{0}' 的密碼:{1}。下次啟動時將重設。" } \ No newline at end of file diff --git a/tests/i18n-backend.test.cjs b/tests/i18n-backend.test.cjs index caf22a0..35d1293 100644 --- a/tests/i18n-backend.test.cjs +++ b/tests/i18n-backend.test.cjs @@ -195,11 +195,11 @@ const backendTextCode = fs.readFileSync(path.join(root, 'Services/Localization/B // We need to test the actual compiled code. Let's create a test that runs the actual .NET code. // For now, test the catalog integrity. -test('All 22 backend catalogs have exactly 84 keys', () => { +test('All 22 backend catalogs have exactly 86 keys', () => { const codes = Object.keys(backendCatalogs).sort(); assert.equal(codes.length, 22); for (const [lang, table] of Object.entries(backendCatalogs)) { - assert.equal(Object.keys(table).length, 84, lang); + assert.equal(Object.keys(table).length, 86, lang); for (const [key, value] of Object.entries(table)) { assert.equal(typeof value, 'string', lang + ':' + key); assert.ok(value.length > 0, lang + ':' + key + ' is empty');