Skip to content

Commit

Permalink
feat: Add NostrKeyValidAttribute for validating Nostr Key format
Browse files Browse the repository at this point in the history
This commit adds a new file `NostrKeyValidAttribute.cs` which contains an attribute class for validating the Nostr Key format. The validation logic is implemented in the `IsValid` method of this class. This change also includes modifications to the `User` model to add properties for storing public and private keys, and updates to the `LoginViewModel` to validate the account key using this new attribute.

Additionally, there are changes made to localization files, database service, and view models related to user authentication.
  • Loading branch information
kfrancis committed May 17, 2023
1 parent de40978 commit 5fd6699
Show file tree
Hide file tree
Showing 11 changed files with 225 additions and 26 deletions.
76 changes: 76 additions & 0 deletions src/NuSocial/Core/Validation/NostrKeyValidAttribute.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using Microsoft.Extensions.Localization;
using Nostr.Client.Keys;
using NuSocial.Localization;
using System.ComponentModel.DataAnnotations;

namespace NuSocial.Core.Validation
{
/// <summary>
/// Attribute class for validating Nostr Key format.
/// </summary>
public sealed class NostrKeyValidAttribute : ValidationAttribute
{
/// <inheritdoc/>
protected override ValidationResult IsValid(object? value, ValidationContext validationContext)
{
if (validationContext is null)
{
throw new ArgumentNullException(nameof(validationContext));
}

var instance = validationContext.ObjectInstance;
var loc = (IStringLocalizer<NuSocialResource>?)instance.GetType().GetProperty("L")?.GetValue(instance);
var invalidMessage = loc?["InvalidNostrKey"] ?? "Invalid Key";

if (value is string valueString && !string.IsNullOrEmpty(valueString))
{
bool isValid = false;

if (valueString.StartsWith("npub", StringComparison.OrdinalIgnoreCase))
{
try
{
_ = NostrPublicKey.FromBech32(valueString);
isValid = true;
}
catch (ArgumentException) { }
}
else if (valueString.StartsWith("nsec", StringComparison.OrdinalIgnoreCase))
{
try
{
_ = NostrPrivateKey.FromBech32(valueString);
isValid = true;
}
catch (ArgumentException) { }
}
else
{
try
{
_ = NostrPublicKey.FromHex(valueString);
isValid = true;
}
catch (ArgumentException) { }

if (!isValid)
{
try
{
_ = NostrPrivateKey.FromHex(valueString);
isValid = true;
}
catch (ArgumentException) { }
}
}

if (!isValid)
{
return new(invalidMessage);
}
}

return ValidationResult.Success!;
}
}
}
3 changes: 2 additions & 1 deletion src/NuSocial/Localization/NuSocial/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"AboutPlaceholder": "Add something interesting about you ..",
"AccountId": "Account ID",
"AccountKeyPlaceholder": "nsec1...",
"NuSocial": "NuSocial"
"NuSocial": "NuSocial",
"TapToRegenerate": "Tap to regenerate"
}
}
26 changes: 24 additions & 2 deletions src/NuSocial/Models/User.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,41 @@
namespace NuSocial.Models;
using Nostr.Client.Keys;
using SQLite;

namespace NuSocial.Models;

public class User
{
[Ignore]
public NostrPublicKey? PublicKey { get; set; }

[Ignore]
public NostrPrivateKey? PrivateKey { get; set; }

public string PublicKeyString
{
get => PublicKey?.Bech32 ?? string.Empty;
set => PublicKey = NostrPublicKey.FromBech32(value);
}

public string PrivateKeyString
{
get => PrivateKey?.Bech32 ?? string.Empty;
set => PrivateKey = NostrPrivateKey.FromBech32(value);
}

public bool IsReadOnly => PublicKey != null && PrivateKey == null;
}

public class AuthenticateResult
{

public NostrKeyPair KeyPair { get; set; }
}

public class AuthenticateRequest
{

}

public class UserConfiguration
{

Expand Down
5 changes: 5 additions & 0 deletions src/NuSocial/NuSocial.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@
<framework>Flyout</framework>
</PropertyGroup>

<PropertyGroup Condition="'$(TargetFramework)'=='net7.0-ios'">
<CodesignKey>Apple Development: Created via API (45692F9U2L)</CodesignKey>
<CodesignProvision>VS: WildCard Development</CodesignProvision>
</PropertyGroup>

<ItemGroup>
<Content Remove="C:\Users\byter\.nuget\packages\secp256k1.native\0.1.24-alpha\build\netstandard1.0\..\..\runtimes\win-x64\native\secp256k1.dll" />
</ItemGroup>
Expand Down
20 changes: 10 additions & 10 deletions src/NuSocial/Services/LocalStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ public interface IDatabase
{
Task DeleteAllDataAsync();

//Task<ObservableCollection<DataElement>> GetDataElementsAsync();
Task<ObservableCollection<User>> GetUsersAsync();

//Task UpdateDataElementsAsync(ObservableCollection<DataElement> dataElements);
Task UpdateUsersAsync(ObservableCollection<User> users);
}

/// <summary>
Expand Down Expand Up @@ -135,10 +135,10 @@ public async ValueTask DisposeAsync()
GC.SuppressFinalize(this);
}

///// <summary>
///// Gets data elements from the database.
///// </summary>
//public Task<ObservableCollection<DataElement>> GetDataElementsAsync() => GetItemsAsync<DataElement>();
/// <summary>
/// Gets data elements from the database.
/// </summary>
public Task<ObservableCollection<User>> GetUsersAsync() => GetItemsAsync<User>();

public async Task<ObservableCollection<T>> GetItemsAsync<T>(Action? reset = null) where T : class, new()
{
Expand All @@ -155,10 +155,10 @@ public async ValueTask DisposeAsync()
}
}

///// <summary>
///// Updates data elements in the database.
///// </summary>
//public Task UpdateDataElementsAsync(ObservableCollection<DataElement> dataElements) => UpdateIfPossible(dataElements);
/// <summary>
/// Updates data elements in the database.
/// </summary>
public Task UpdateUsersAsync(ObservableCollection<User> users) => UpdateIfPossible(users);

protected virtual async Task DisposeAsync(bool disposing)
{
Expand Down
42 changes: 39 additions & 3 deletions src/NuSocial/ViewModels/LoginViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,60 @@
using NuSocial.Core.ViewModel;
using CommunityToolkit.Mvvm.Messaging;
using Nostr.Client.Keys;
using NuSocial.Core.Validation;
using NuSocial.Core.ViewModel;
using NuSocial.Messages;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.DependencyInjection;

namespace NuSocial.ViewModels;

public partial class LoginViewModel : BaseFormModel, ITransientDependency
{
private readonly IDatabase _db;

[Required]
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsAccountKeyValid))]
[NostrKeyValid]
private string _accountKey = string.Empty;

public LoginViewModel(IDialogService dialogService, INavigationService navigationService) : base(dialogService, navigationService)
public LoginViewModel(IDialogService dialogService, INavigationService navigationService, IDatabase db) : base(dialogService, navigationService)
{
Title = L["Login"];
_db = db;
}

public bool IsAccountKeyValid => !string.IsNullOrEmpty(AccountKey);

[RelayCommand(CanExecute = nameof(IsNotBusy))]
private Task LoginAsync()
{
return SetBusyAsync(() => { return Task.CompletedTask; });
return SetBusyAsync(() =>
{
return ValidateAsync(async (isValid) =>
{
if (isValid)
{
if (AccountKey.StartsWith("npub", StringComparison.OrdinalIgnoreCase))
{
var pubKey = NostrPublicKey.FromBech32(AccountKey);
var user = new User() { PublicKey = pubKey };
await _db.UpdateUsersAsync(new ObservableCollection<User> { user });
await Navigation.NavigateTo("//main", user);
}
else
{
var privKey = NostrPrivateKey.FromBech32(AccountKey);
var pubKeyEc = privKey.Ec.CreateXOnlyPubKey();
var pubKey = NostrPublicKey.FromEc(pubKeyEc);
var user = new User() { PublicKey = pubKey, PrivateKey = privKey };
await _db.UpdateUsersAsync(new ObservableCollection<User> { user });
await Navigation.NavigateTo("//main", user);
}
}
}, async () => await ShowErrorsCommand.ExecuteAsync(null));
});
}
}
26 changes: 25 additions & 1 deletion src/NuSocial/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
using NuSocial.Core.ViewModel;
using CommunityToolkit.Mvvm.Messaging;
using NuSocial.Core.ViewModel;
using NuSocial.Messages;
using Volo.Abp.DependencyInjection;

namespace NuSocial.ViewModels;

public partial class MainViewModel : BaseViewModel, ITransientDependency
{
[ObservableProperty]
private User? _user;

public MainViewModel(IDialogService dialogService, INavigationService navigationService) : base(dialogService, navigationService)
{
}

public override Task OnFirstAppear()
{
WeakReferenceMessenger.Default.Send<ResetNavMessage>(new());
return Task.CompletedTask;
}

public override Task OnParameterSet()
{
return SetBusyAsync(() =>
{
if (NavigationParameter is User user)
{
User = user;
}
return Task.CompletedTask;
});
}
}
26 changes: 25 additions & 1 deletion src/NuSocial/ViewModels/RegisterViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.Messaging;
using Nostr.Client.Keys;
using NuSocial.Core.ViewModel;
using NuSocial.Messages;
using System.ComponentModel.DataAnnotations;
Expand All @@ -24,14 +25,27 @@ public RegisterViewModel(IDialogService dialogService, INavigationService naviga
private string? _about;

[ObservableProperty]
private string _accountId = "123";
private string _publicKey = string.Empty;

[ObservableProperty]
private string _privateKey = string.Empty;

public override Task OnFirstAppear()
{
GenerateKeyPair();

WeakReferenceMessenger.Default.Send<ResetNavMessage>(new("//start"));
return Task.CompletedTask;
}

private void GenerateKeyPair()
{
var keyPair = NostrKeyPair.GenerateNew();

PublicKey = keyPair.PublicKey.Bech32;
PrivateKey = keyPair.PrivateKey.Bech32;
}

[RelayCommand(CanExecute = nameof(IsNotBusy))]
private Task CreateAsync()
{
Expand All @@ -44,4 +58,14 @@ private Task CreateAsync()
}, async () => await ShowErrorsCommand.ExecuteAsync(null));
});
}

[RelayCommand(CanExecute = nameof(IsNotBusy))]
private Task RegenerateAsync()
{
return SetBusyAsync(() =>
{
GenerateKeyPair();
return Task.CompletedTask;
});
}
}
3 changes: 2 additions & 1 deletion src/NuSocial/Views/LoginView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
<Button
Grid.Row="2"
Margin="0,10"
Command="{Binding LoginCommand}"
HeightRequest="50"
MaximumWidthRequest="200"
Text="Login"
Text="{loc:Translate Login}"
VerticalOptions="Start" />
</Grid>
</base:ContentPageFormBase>
4 changes: 1 addition & 3 deletions src/NuSocial/Views/MainView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,8 @@
xmlns:base="clr-namespace:NuSocial.Core.View"
xmlns:loc="clr-namespace:NuSocial.Helpers"
xmlns:viewModels="clr-namespace:NuSocial.ViewModels"
Title="{Binding Title}"
x:DataType="viewModels:MainViewModel"
x:TypeArguments="viewModels:MainViewModel"
Shell.NavBarIsVisible="False">
x:TypeArguments="viewModels:MainViewModel">
<VerticalStackLayout>
<Label
HorizontalOptions="Center"
Expand Down
20 changes: 16 additions & 4 deletions src/NuSocial/Views/RegisterView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<toolkit:GravatarImageSource
CacheValidity="1"
CachingEnabled="True"
Email="{Binding AccountId}"
Email="{Binding PublicKey}"
Image="Robohash" />
</Image.Source>
</Image>
Expand Down Expand Up @@ -123,11 +123,23 @@
Padding="10,0,0,0"
RowDefinitions="Auto,Auto"
RowSpacing="5">
<Label FontAttributes="Bold" Text="{loc:Translate AccountId}" />
<HorizontalStackLayout>
<Label FontAttributes="Bold" Text="{loc:Translate AccountId}" />
<Label
Margin="5,0"
FontSize="10"
Text="{loc:Translate TapToRegenerate,
StringFormat='({0})'}"
VerticalTextAlignment="Center" />
</HorizontalStackLayout>
<Label
Grid.Row="1"
Text="{Binding AccountId}"
VerticalOptions="Center" />
Text="{Binding PublicKey}"
VerticalOptions="Center">
<Label.GestureRecognizers>
<TapGestureRecognizer Command="{Binding RegenerateCommand}" />
</Label.GestureRecognizers>
</Label>
</Grid>

<Button
Expand Down

0 comments on commit 5fd6699

Please sign in to comment.