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');