Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ and a release's notes are its section. The procedure is in [`docs/RELEASING.md`]

### Fixed

- Abbreviated login commands such as `c name password` and `con name password` are hidden from local echo,
transcripts and credential-filtered command history, including when used inside a command stack.

- A password is no longer echoed into the output or written to the session log. When a game takes over echo at its
password prompt (telnet `WILL ECHO`), the line you send is not echoed locally or kept in command history, and echo
comes back when the game hands it back. A typed `connect <name> <password>`, including one inside a stacked line
Expand Down
4 changes: 3 additions & 1 deletion src/SharpMUTerm.Core/Input/HistorySecrets.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ public static class HistorySecrets
/// The MU* login verbs, which take <c>&lt;name&gt; &lt;password&gt;</c>. <c>cd</c> is connect-dark and
/// <c>ch</c> connect-hidden (PennMUSH/TinyMUX wizard variants), and both take the same two arguments;
/// <c>create</c> chooses a password rather than presenting one, which is no less a secret.
/// PennMUSH also accepts every nonempty prefix of <c>connect</c> at login.
/// </summary>
private static readonly string[] LoginVerbs = { "connect", "cd", "ch", "create" };
private static readonly string[] LoginVerbs =
{ "connect", "c", "co", "con", "conn", "conne", "connec", "cd", "ch", "create" };

/// <summary>
/// Verbs whose <em>first</em> argument is already the secret — <c>@password old=new</c>,
Expand Down
11 changes: 11 additions & 0 deletions tests/SharpMUTerm.Core.Tests/Input/HistorySecretsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ public class HistorySecretsTests
/// <summary>The lines that must never be recorded, in the spellings a user actually types.</summary>
[Test]
[Arguments("connect Corvid hunter2")]
[Arguments("c Corvid hunter2")]
[Arguments("co Corvid hunter2")]
[Arguments("Con Corvid hunter2")]
[Arguments("conn Corvid hunter2")]
[Arguments("conne Corvid hunter2")]
[Arguments("connec Corvid hunter2")]
[Arguments("CONNECT Corvid hunter2")] // MU* dispatch is case-insensitive; so is this
[Arguments("connect Corvid hunter2")] // runs of spaces are one separator
[Arguments("cd Corvid hunter2")] // connect-dark
Expand All @@ -33,6 +39,9 @@ public async Task ACredentialBearingLineIsRecognised(string command)
[Test]
[Arguments("connect")]
[Arguments("connect guest")]
[Arguments("c guest")]
[Arguments("con guest")]
[Arguments("connection Corvid hunter2")]
[Arguments("cd")]
[Arguments("create")]
[Arguments("@password")]
Expand Down Expand Up @@ -67,6 +76,8 @@ public async Task TheVerbsAreNamed()
[Arguments("look;connect Corvid hunter2", ";")]
[Arguments("look;#1 connect Corvid hunter2", ";")]
[Arguments("look;#101 connect Corvid hunter2", ";")] // a refused stack is still kept out
[Arguments("look;#1 con Corvid hunter2", ";")]
[Arguments("look;#101 con Corvid hunter2", ";")]
[Arguments("look | @password old=new", "|")]
[Arguments("look;;connect Corvid hunter2", ";;")]
public async Task AStackedOrMultiLineLoginCarriesACredential(string line, string? separator)
Expand Down
11 changes: 7 additions & 4 deletions tests/SharpMUTerm.Core.Tests/Session/PasswordLeakTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,19 @@ await Assert.That(session.Scrollback.Snapshot().Any(l => l.Text.Contains(Secret,
}

[Test]
[Arguments(true)]
[Arguments(false)]
public async Task ALoginCommandReachesTelnetButIsHiddenInTheEcho(bool typed)
[Arguments(true, "connect")]
[Arguments(false, "connect")]
[Arguments(true, "con")]
[Arguments(false, "con")]
[Arguments(true, "c")]
public async Task ALoginCommandReachesTelnetButIsHiddenInTheEcho(bool typed, string verb)
{
var log = new RecordingSink();
var telnet = new FakeTelnetSession();
var session = new WorldSession(World(), sessionFactory: _ => telnet, log: log);
await session.ConnectAsync();

var command = $"connect Corvid {Secret}";
var command = $"{verb} Corvid {Secret}";
if (typed)
{
await session.SendTypedAsync(command);
Expand Down
16 changes: 12 additions & 4 deletions tests/SharpMUTerm.Tui.Tests/HistorySearchEndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -471,19 +471,27 @@ public async Task ServerEchoKeepsStandalonePasswordOutOfHistory()
/// sent (<c>#1</c>) or refused (<c>#101</c>, past the repeat cap), and the echo and the wire behave as they would.
/// </summary>
[Test]
[Arguments("#1")]
[Arguments("#101")]
public async Task AStackedLoginIsKeptOutOfHistoryAndSearch(string repeat)
[Arguments("#1", "connect")]
[Arguments("#101", "connect")]
[Arguments("#1", "con")]
[Arguments("#101", "con")]
public async Task AStackedLoginIsKeptOutOfHistoryAndSearch(string repeat, string verb)
{
var (app, _) = await Demo(separator: ";");
const string secret = "stacked-secret";

Send(app, $"look;{repeat} connect Corvid {secret}");
Send(app, $"look;{repeat} {verb} Corvid {secret}");
Send(app, "inventory");

await Assert.That(app.HistoryEntries(InputBar.Primary).Any(e => e.Contains(secret, StringComparison.Ordinal)))
.IsFalse();
await Assert.That(app.HistoryEntries(InputBar.Primary)).Contains("inventory");
app.SimulateKey(Bare(ConsoleKey.UpArrow));
app.SimulateKey(Bare(ConsoleKey.UpArrow));
await Assert.That(app.ArmedInputText).IsEqualTo("inventory");
app.SimulateKey(Chord(ConsoleKey.R));
app.SimulateHistorySearchTyping(secret);
await Assert.That(app.HistorySearchRows).IsEmpty();
}

/// <summary>
Expand Down
Loading