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
Original file line number Diff line number Diff line change
Expand Up @@ -30,41 +30,32 @@ client.UseTwoFactor();

#### Enrolling a user in two factor

To enroll a user in two factor, call [EnrollTwoFactor(DiscordUser.Id)](xref:DisCatSharp.Extensions.TwoFactorCommands.TwoFactorExtensionUtilities.EnrollTwoFactor*) on your [DiscordClient](xref:DisCatSharp.DiscordClient) instance.
To enroll a user in two factor, call [EnrollTwoFactor(DiscordUser.Id)](xref:DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands.EnrollTwoFactorAsync*) on your [InteractionContext](xref:DisCatSharp.ApplicationCommands.Context.InteractionContext).

```cs
using DisCatSharp.Extensions.TwoFactorCommands;
using DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands;

// ...
[SlashCommand("enroll", "Enroll in two factor")]
public static async Task EnrollTwoFactor(InteractionContext ctx)
{
// ...
var (Secret, QrCode) = ctx.Client.EnrollTwoFactor(ctx.User);

// Either send the QR code to the user, or the secret.
// QrCode is a MemoryStream you can use with DiscordWebhookBuilder.AddFile as example.
}
[SlashCommand("setup_two_factor", "Setup 2FA")]
public static async Task SetupTwoFactorAsync(InteractionContext ctx)
=> await ctx.EnrollTwoFactorAsync();
```

Example way to ask a user to register their two factor:

![Example Enroll](/images/two_factor_enrollment_message_example.png)
![Example Enroll (outdated)](/images/two_factor_enrollment_message_example.png)

#### Disenrolling a user in two factor

To disenroll a user from two factor, call [DisenrollTwoFactor(DiscordUser.Id)](xref:DisCatSharp.Extensions.TwoFactorCommands.TwoFactorExtensionUtilities.DisenrollTwoFactor*) on your [DiscordClient](xref:DisCatSharp.DiscordClient) instance.
To disenroll a user from two factor, call [DisenrollTwoFactor(DiscordUser.Id)](xref:DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands.UnenrollTwoFactorAsync*) on your [InteractionContext](xref:DisCatSharp.ApplicationCommands.Context.InteractionContext) instance.

```cs
using DisCatSharp.Extensions.TwoFactorCommands;
using DisCatSharp.Extensions.TwoFactorCommands.ApplicationCommands;

// ...
[SlashCommand("disenroll", "Disenroll from two factor"), ApplicationCommandRequireEnrolledTwoFactor]
public static async Task DisenrollTwoFactor(InteractionContext ctx)
{
// ...
ctx.Client.DisenrollTwoFactor(ctx.User.Id);
}
[SlashCommand("remove_two_factor", "Remove 2FA"), ApplicationCommandRequireEnrolledTwoFactor]
public static async Task RemoveTwoFactorAsync(InteractionContext ctx)
=> await ctx.UnenrollTwoFactorAsync();
```

#### Check if a user is enrolled in two factor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="QRCoder" Version="1.7.0" />
<PackageReference Include="System.Memory" Version="4.6.3" />
<PackageReference Include="TwoFactorAuth.Net" Version="1.4.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ public enum TwoFactorResult
/// </summary>
TimedOut = 4,

/// <summary>
/// Indicates that the enrollment process has been completed successfully.
/// </summary>
Enrolled = 5,

/// <summary>
/// Indicates that the unenrollment process has been completed successfully.
/// </summary>
Unenrolled = 6,

/// <summary>
/// This function is not implemented.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ public ApplicationCommandRequireEnrolledTwoFactorAttribute()
/// Runs checks.
/// </summary>
public override Task<bool> ExecuteChecksAsync(BaseContext ctx)
=> Task.FromResult(ctx.Client.GetTwoFactor().IsEnrolled(ctx.User.Id));
=> Task.FromResult(ctx.Client.GetTwoFactor()?.IsEnrolled(ctx.User.Id) ?? false);
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@ public CommandRequireEnrolledTwoFactorAttribute()
/// Runs checks.
/// </summary>
public override Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help)
=> Task.FromResult(ctx.Client.GetTwoFactor().IsEnrolled(ctx.User.Id));
=> Task.FromResult(ctx.Client.GetTwoFactor()?.IsEnrolled(ctx.User.Id) ?? false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ public TwoFactorResponseConfiguration()
/// </summary>
public string AuthenticationFailureMessage { internal get; set; } = "Code invalid..";

/// <summary>
/// <para>Sets the message when an user successfully enrolled into two factor auth.</para>
/// <para>Defaults to: You successfully enrolled in two factor.</para>
/// </summary>
public string AuthenticationEnrolledMessage { internal get; set; } = "You successfully enrolled in two factor.";

/// <summary>
/// <para>Sets the message when an user successfully unenrolled from two factor auth.</para>
/// <para>Defaults to: You successfully unenrolled from two factor.</para>
/// </summary>
public string AuthenticationUnenrolledMessage { internal get; set; } = "You successfully unenrolled from two factor.";

/// <summary>
/// <para>Sets the message when an user is not yet enrolled into two factor auth.</para>
/// <para>Defaults to: You are not enrolled in two factor.</para>
Expand Down
12 changes: 6 additions & 6 deletions DisCatSharp.Extensions.TwoFactorCommands/TwoFactorExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ protected internal override void Setup(DiscordClient client)
else
{
var v = a.GetName().Version;
var vs = v.ToString(3);
var vs = v!.ToString(3);

if (v.Revision > 0)
this.VersionString = $"{vs}, CI build {v.Revision}";
Expand Down Expand Up @@ -144,7 +144,7 @@ private bool HasData(ulong user)
/// </summary>
/// <param name="user">The user id to get data for.</param>
private string GetSecretFor(ulong user)
=> this.DatabaseClient.Select(this._tableName, null, 1, null, new(this._userField, OperatorEnum.Equals, user.ToString())).Rows[0].ItemArray[1].ToString();
=> this.DatabaseClient.Select(this._tableName, null, 1, null, new(this._userField, OperatorEnum.Equals, user.ToString())).Rows[0].ItemArray[1]!.ToString()!;

/// <summary>
/// Adds a secret for the given user id to the database.
Expand Down Expand Up @@ -174,29 +174,29 @@ private void RemoveSecret(ulong user)
/// <param name="user">The user id entering the code.</param>
/// <param name="code">The code to check.</param>
/// <returns>Whether the code is valid.</returns>
internal bool IsValidCode(ulong user, string code)
public bool IsValidCode(ulong user, string code)
=> this.HasData(user) && this.TwoFactorClient.VerifyCode(this.GetSecretFor(user), code);

/// <summary>
/// Checks whether given user id is enrolled in two factor auth.
/// </summary>
/// <param name="user">User id to check for enrollment.</param>
/// <returns>Whether the user is enrolled.</returns>
internal bool IsEnrolled(ulong user)
public bool IsEnrolled(ulong user)
=> this.HasData(user);

/// <summary>
/// Enrolls given user id with two factor auth.
/// </summary>
/// <param name="user">User id to enroll.</param>
/// <param name="secret">Secret to use.</param>
internal void EnrollUser(ulong user, string secret)
public void EnrollUser(ulong user, string secret)
=> this.AddSecretFor(user, secret);

/// <summary>
/// Unenrolls given user id from two factor auth.
/// </summary>
/// <param name="user">User id to unenroll.</param>
internal void DisenrollUser(ulong user)
public void DisenrollUser(ulong user)
=> this.RemoveSecret(user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,16 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Web;

using DisCatSharp.Entities;

using QRCoder;

using TwoFactorAuthNet;

namespace DisCatSharp.Extensions.TwoFactorCommands;
Expand All @@ -38,28 +43,28 @@ public static class TwoFactorExtensionUtilities
/// <param name="user">The user id to check.</param>
/// <returns>Whether the user is enrolled.</returns>
public static bool CheckTwoFactorEnrollmentFor(this DiscordClient client, ulong user)
=> client.GetTwoFactor().IsEnrolled(user);
=> client.GetTwoFactor()?.IsEnrolled(user) ?? false;

/// <summary>
/// Removes the two factor registration for the given user id.
/// </summary>
/// <param name="client">The discord client.</param>
/// <param name="user">The user id to check.</param>
public static void DisenrollTwoFactor(this DiscordClient client, ulong user)
=> client.GetTwoFactor().DisenrollUser(user);
=> client.GetTwoFactor()?.DisenrollUser(user);

/// <summary>
/// Registers two factor for the given user.
/// </summary>
/// <param name="client">The discord client.</param>
/// <param name="user">The user to check.</param>
/// <returns>
/// A <see cref="System.Tuple{T1, T2}" /> where <c>Secret</c> is a <see cref="System.String">string</see> with the
/// A <see cref="System.Tuple{T1, T2}" /> where <c>Secret</c> is a <see cref="string">string</see> with the
/// secret itself and <c>QrCode</c> a <see cref="System.IO.MemoryStream">MemoryStream</see> with the qr code image.
/// </returns>
public static (string Secret, MemoryStream QrCode) EnrollTwoFactor(this DiscordClient client, DiscordUser user)
{
var ext = client.GetTwoFactor();
var ext = client.GetTwoFactor() ?? throw new InvalidOperationException("Two factor extension is not registered on this client.");
var secret = ext.TwoFactorClient.CreateSecret(160, CryptoSecureRequirement.RequireSecure);
ext.EnrollUser(user.Id, secret);
var label = $"{ext.Configuration.ResponseConfiguration.AuthenticatorAccountPrefix}: {HttpUtility.UrlEncode(user.UsernameWithDiscriminator)}";
Expand All @@ -71,4 +76,99 @@ public static (string Secret, MemoryStream QrCode) EnrollTwoFactor(this DiscordC
};
return (secret, ms);
}

/// <summary>
/// Converts the specified payload into a QR code represented as block text.
/// </summary>
/// <param name="payload">The payload to encode.</param>
/// <param name="quietZoneModules">Number of quiet-zone modules.</param>
/// <returns>A string representing the QR code.</returns>
public static string ToBlockText(this string payload, int quietZoneModules = 4)
{
using var generator = new QRCodeGenerator();
using var data = generator.CreateQrCode(payload, QRCodeGenerator.ECCLevel.M);

var modules = data.ModuleMatrix;
var size = modules.Count;

var total = size + (quietZoneModules * 2);

bool Get(int x, int y)
{
x -= quietZoneModules;
y -= quietZoneModules;

return x >= 0 && x < size &&
y >= 0 && y < size &&
modules[y][x];
}

var sb = new StringBuilder();

for (var y = 0; y < total; y += 2)
{
for (var x = 0; x < total; x++)
{
var top = Get(x, y);
var bottom = Get(x, y + 1);

sb.Append((top, bottom) switch
{
(true, true) => '█',
(true, false) => '▀',
(false, true) => '▄',
_ => ' '
});
}

sb.AppendLine();
}

return TrimHorizontalWhitespace(sb.ToString());
}

/// <summary>
/// Trims horizontal whitespace from each line of the input string, preserving a specified minimum padding on both sides.
/// </summary>
/// <param name="input">The input string containing one or more lines to be trimmed of horizontal whitespace.</param>
/// <param name="minPadding">The minimum number of characters to retain as padding on both the left and right sides of each trimmed line. Must
/// be zero or greater.</param>
/// <returns>A string in which each line has been trimmed of leading and trailing horizontal whitespace, with the specified
/// minimum padding preserved. Returns the original input if no lines are present.</returns>
private static string TrimHorizontalWhitespace(string input, int minPadding = 2)
{
var lines = input.Replace("\r", "").Split('\n');
if (lines.Length == 0)
return input;

var width = lines.Max(l => l.Length);

bool IsEmptyColumn(int col)
{
foreach (var line in lines)
{
if (col < line.Length && line[col] != ' ')
return false;
}
return true;
}

var left = 0;
while (left < width && IsEmptyColumn(left)) left++;

var right = width - 1;
while (right >= 0 && IsEmptyColumn(right)) right--;

left = Math.Max(0, left - minPadding);
right = Math.Min(width - 1, right + minPadding);

var trimmed = lines.Select(line =>
{
if (line.Length <= left) return "";
var len = Math.Min(right - left + 1, line.Length - left);
return line.Substring(left, len).TrimEnd();
});

return string.Join("\n", trimmed);
}
}
Loading