2026-06-02 23:57:32 +01:00
using System.Text.Json ;
using RemSound.Core ;
namespace RemSound.App ;
/// <summary>
/// The "password manager" view from Options: every saved profile listed with its password in a
/// plain, editable box. Type a new password against any profile and OK writes them all back to
/// disk. Plain (readable) boxes on purpose — a masked field reads as bullets to a screen reader,
/// which is useless; the security model already accepts the password is recoverable from the
/// profile file. Only the password field of each changed profile is rewritten; nothing else in
/// the profile is touched. Returns true if any password was changed. 2026-05-31.
/// </summary>
internal static class ProfilePasswordManagerDialog
{
public static bool Show ( IWin32Window owner , ProfileStore store )
2026-07-26 22:38:40 +01:00
{
var ( dialog , rows ) = Build ( store );
using ( dialog )
{
if ( dialog . ShowDialog ( owner ) != DialogResult . OK ) return false ;
return SaveChanges ( store , rows );
}
}
/// <summary>Construction split from ShowDialog so the accessibility audit can inspect the real
/// dialog (inline-built dialogs used to be invisible to the audit).</summary>
internal static ( Form Dialog , List <( string Title , string Original , TextBox Box )> Rows ) Build ( ProfileStore store )
2026-06-02 23:57:32 +01:00
{
var titles = store . ListProfileTitles ();
2026-07-26 22:38:40 +01:00
var dialog = new Form
2026-06-02 23:57:32 +01:00
{
Text = "Profile passwords" ,
StartPosition = FormStartPosition . CenterParent ,
FormBorderStyle = FormBorderStyle . Sizable ,
MinimizeBox = false ,
MaximizeBox = true ,
ShowInTaskbar = false ,
ClientSize = new Size ( 520 , 420 ),
MinimumSize = new Size ( 420 , 240 ),
AccessibleName = "Profile passwords" ,
};
var intro = new Label
{
Text = titles . Count == 0
? "You don't have any saved profiles yet. Create one with File → Save as, and it will ask you for a password."
: "Each profile has its own password. You and the person you connect to must use the same password. Edit any box and press OK to save." ,
Dock = DockStyle . Top ,
AutoSize = false ,
Height = 48 ,
Padding = new Padding ( 12 , 10 , 12 , 4 ),
};
var grid = new TableLayoutPanel
{
Dock = DockStyle . Fill ,
AutoScroll = true ,
ColumnCount = 2 ,
RowCount = titles . Count ,
Padding = new Padding ( 12 , 0 , 12 , 8 ),
};
grid . ColumnStyles . Add ( new ColumnStyle ( SizeType . AutoSize ));
grid . ColumnStyles . Add ( new ColumnStyle ( SizeType . Percent , 100 ));
var rows = new List <( string Title , string Original , TextBox Box )>();
foreach ( var title in titles )
{
string current ;
try { current = RemSoundCrypto . Deobfuscate ( store . Load ( title )?. Password ); }
catch { current = "" ; }
var label = new Label { Text = title , AutoSize = true , Anchor = AnchorStyles . Left , Padding = new Padding ( 0 , 6 , 10 , 6 ) };
2026-06-13 07:23:54 +01:00
var box = new TextBox { Text = current , Anchor = AnchorStyles . Left | AnchorStyles . Right , AccessibleName = $"Password for profile {title}" , Tag = KeyClickService . PasswordFieldTag };
2026-06-02 23:57:32 +01:00
grid . Controls . Add ( label );
grid . Controls . Add ( box );
rows . Add (( title , current , box ));
}
2026-07-27 08:19:20 +01:00
var okButton = new Button { Text = "&OK" , AutoSize = true };
// OK validates by hand (no auto-close DialogResult): every CHANGED, non-empty entry passes
// the same strength gate as the single-password dialog — one rule at every door. Unchanged
// entries always pass (an old weak password is grandfathered until the day it's changed).
okButton . Click += ( _ , _ ) =>
{
foreach ( var ( title , original , box ) in rows )
{
var entered = box . Text . Trim ();
if ( entered . Length > 0
&& ! string . Equals ( entered , original , StringComparison . Ordinal )
&& PasswordStrength . Critique ( entered ) is { } advice )
{
var page = new TaskDialogPage
{
Caption = "Choose a stronger password" ,
Heading = $"The new password for “{title}” is too easy to guess" ,
Text = advice ,
Icon = TaskDialogIcon . Warning ,
Buttons = { TaskDialogButton . OK },
DefaultButton = TaskDialogButton . OK ,
AllowCancel = true ,
};
TaskDialog . ShowDialog ( dialog , page );
box . Focus ();
box . SelectAll ();
return ;
}
}
dialog . DialogResult = DialogResult . OK ;
dialog . Close ();
};
2026-07-18 09:17:49 +01:00
var cancelButton = new Button { Text = "&Cancel" , AutoSize = true , DialogResult = DialogResult . Cancel };
2026-06-02 23:57:32 +01:00
var buttons = new FlowLayoutPanel { Dock = DockStyle . Bottom , FlowDirection = FlowDirection . RightToLeft , AutoSize = true , Padding = new Padding ( 8 ) };
buttons . Controls . Add ( okButton );
buttons . Controls . Add ( cancelButton );
dialog . Controls . Add ( grid );
dialog . Controls . Add ( buttons );
dialog . Controls . Add ( intro );
dialog . AcceptButton = okButton ;
dialog . CancelButton = cancelButton ;
2026-07-26 22:38:40 +01:00
return ( dialog , rows );
}
2026-06-02 23:57:32 +01:00
2026-07-26 22:38:40 +01:00
private static bool SaveChanges ( ProfileStore store , List <( string Title , string Original , TextBox Box )> rows )
{
2026-06-02 23:57:32 +01:00
var changedAny = false ;
foreach ( var ( title , original , box ) in rows )
{
var now = box . Text . Trim ();
if ( now == original ) continue ;
try
{
var path = store . PathFor ( title );
if (! File . Exists ( path )) continue ;
var profile = JsonSerializer . Deserialize < Profile >( File . ReadAllText ( path ));
if ( profile is null ) continue ;
profile . Password = RemSoundCrypto . Obfuscate ( now );
File . WriteAllText ( path , JsonSerializer . Serialize ( profile , new JsonSerializerOptions { WriteIndented = true }));
changedAny = true ;
}
catch
{
// Skip a profile we couldn't rewrite; the others still save.
}
}
return changedAny ;
}
}