diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index cd967fc..0000000 --- a/.dockerignore +++ /dev/null @@ -1,25 +0,0 @@ -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/.idea -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d6dc02a..0000000 --- a/Dockerfile +++ /dev/null @@ -1,21 +0,0 @@ -FROM mcr.microsoft.com/dotnet/runtime:9.0 AS base -USER $APP_UID -WORKDIR /app - -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["RelayCore.csproj", "./"] -RUN dotnet restore "RelayCore.csproj" -COPY . . -WORKDIR "/src/" -RUN dotnet build "./RelayCore.csproj" -c $BUILD_CONFIGURATION -o /app/build - -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "./RelayCore.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "RelayCore.dll"] diff --git a/E2EeHelper.cs b/E2EeHelper.cs deleted file mode 100644 index c4d5578..0000000 --- a/E2EeHelper.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Security.Cryptography; -using System.Text; - -namespace RelayCore; - -public static class E2EeHelper -{ - public static (string publicKey, string privateKey) GenerateRsaKeyPair() - { - using var rsa = RSA.Create(2048); - - var publicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo()); - var privateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey()); - - return (publicKey, privateKey); - } - - public static EncryptedMessagePayload EncryptForRecipient(string plainText, string recipientPublicKeyBase64) - { - var aesKey = RandomNumberGenerator.GetBytes(32); - var nonce = RandomNumberGenerator.GetBytes(12); - var plainBytes = Encoding.UTF8.GetBytes(plainText); - var cipherBytes = new byte[plainBytes.Length]; - var tag = new byte[16]; - - using (var aes = new AesGcm(aesKey, 16)) - { - aes.Encrypt(nonce, plainBytes, cipherBytes, tag); - } - - var recipientPublicKey = Convert.FromBase64String(recipientPublicKeyBase64); - byte[] encryptedKey; - - using (var rsa = RSA.Create()) - { - rsa.ImportSubjectPublicKeyInfo(recipientPublicKey, out _); - encryptedKey = rsa.Encrypt(aesKey, RSAEncryptionPadding.OaepSHA256); - } - - return new EncryptedMessagePayload - { - CipherText = Convert.ToBase64String(cipherBytes), - Nonce = Convert.ToBase64String(nonce), - Tag = Convert.ToBase64String(tag), - EncryptedKey = Convert.ToBase64String(encryptedKey) - }; - } - - public static string DecryptForRecipient(EncryptedMessagePayload payload, string recipientPrivateKeyBase64) - { - var encryptedKey = Convert.FromBase64String(payload.EncryptedKey); - var privateKey = Convert.FromBase64String(recipientPrivateKeyBase64); - - byte[] aesKey; - - using (var rsa = RSA.Create()) - { - rsa.ImportPkcs8PrivateKey(privateKey, out _); - aesKey = rsa.Decrypt(encryptedKey, RSAEncryptionPadding.OaepSHA256); - } - - var nonce = Convert.FromBase64String(payload.Nonce); - var tag = Convert.FromBase64String(payload.Tag); - var cipherBytes = Convert.FromBase64String(payload.CipherText); - var plainBytes = new byte[cipherBytes.Length]; - - using (var aes = new AesGcm(aesKey, 16)) - { - aes.Decrypt(nonce, cipherBytes, tag, plainBytes); - } - - return Encoding.UTF8.GetString(plainBytes); - } -} - -public class EncryptedMessagePayload -{ - public required string CipherText { get; set; } - public required string Nonce { get; set; } - public required string Tag { get; set; } - public required string EncryptedKey { get; set; } -} \ No newline at end of file diff --git a/PasswordHasher.cs b/PasswordHasher.cs deleted file mode 100644 index d8fbae5..0000000 --- a/PasswordHasher.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Text; -using Konscious.Security.Cryptography; - -namespace PasswordHasher -{ - /// - /// Provides secure password hashing functionality using Argon2id algorithm - /// - public class PasswordHasher - { - /// - /// Size of the salt in bytes - /// - private const int SaltSize = 16; - - /// - /// Size of the hash in bytes - /// - private const int HashSize = 32; - - /// - /// Number of threads to use for parallel computation - /// - private const int DegreeOfParallelism = 1; - - /// - /// Number of iterations for the Argon2id algorithm - /// - private const int Iterations = 2; - - /// - /// Memory size in KB to use - /// - private const int MemorySize = 19456; // 19 MB - - /// - /// Generates a secure hash of a password using Argon2id with a random salt - /// - /// The plain text password to hash - /// A Base64 string containing the combined salt and hash - /// Thrown when password is null - public string HashPassword(string password) - { - // Generate a random salt - byte[] salt = new byte[SaltSize]; - using (RandomNumberGenerator rng = RandomNumberGenerator.Create()) - { - rng.GetBytes(salt); - } - - // Create hash - byte[] hash = HashPassword(password, salt); - - // Combine salt and hash - var combinedBytes = new byte[salt.Length + hash.Length]; - Array.Copy(salt, 0, combinedBytes, 0, salt.Length); - Array.Copy(hash, 0, combinedBytes, salt.Length, hash.Length); - - // Convert to base64 for storage - return Convert.ToBase64String(combinedBytes); - } - - /// - /// Generates a password hash using Argon2id with a specific salt - /// - /// The plain text password - /// The salt to use for hashing - /// A byte array containing the password hash - private byte[] HashPassword(string password, byte[] salt) - { - var argon2 = new Argon2id(Encoding.UTF8.GetBytes(password)) - { - Salt = salt, - DegreeOfParallelism = DegreeOfParallelism, - Iterations = Iterations, - MemorySize = MemorySize - }; - return argon2.GetBytes(HashSize); - } - - /// - /// Verifies if a password matches a stored hash - /// - /// The plain text password to verify - /// The stored hash in Base64 format - /// True if the password matches the hash, false otherwise - /// Thrown when password or hashedPassword are null - /// Thrown when hashedPassword is not in valid Base64 format - public bool VerifyPassword(string password, string hashedPassword) - { - // Decode the stored hash - byte[] combinedBytes = Convert.FromBase64String(hashedPassword); - - // Extract salt and hash - byte[] salt = new byte[SaltSize]; - byte[] hash = new byte[HashSize]; - Array.Copy(combinedBytes, 0, salt, 0, SaltSize); - Array.Copy(combinedBytes, SaltSize, hash, 0, HashSize); - - // Compute hash for the input password - byte[] newHash = HashPassword(password, salt); - - // Compare the hashes - return CryptographicOperations.FixedTimeEquals(hash, newHash); - } - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs deleted file mode 100644 index 4b45823..0000000 --- a/Program.cs +++ /dev/null @@ -1,298 +0,0 @@ -using SurrealDb.Net; -using SurrealDb.Net.Models; -using SurrealDb.Net.Models.Auth; -using System.Text.Json; -using PasswordHasher; -using RelayCore; - -using var db = new SurrealDbClient("ws://127.0.0.1:8000/rpc"); - -await db.SignIn(new RootAuth { Username = "root", Password = "secret" }); -await db.Use("test", "test"); - -var keeper = await CreateUserAsync(db, "Keeper317", "Keeper317@gmail.com", "password"); -var kira = await CreateUserAsync(db, "Ru_Kira", "jduesling13@gmail.com", "password"); - -Console.WriteLine($"Keeper created: {ToJsonString(keeper)}"); -Console.WriteLine($"Kira created: {ToJsonString(kira)}"); - -var keeperKeys = E2EeHelper.GenerateRsaKeyPair(); -var kiraKeys = E2EeHelper.GenerateRsaKeyPair(); - -KeyStorage.SavePrivateKey("Keeper317", keeperKeys.privateKey); -KeyStorage.SavePrivateKey("Ru_Kira", kiraKeys.privateKey); - -await db.Create("user_keys", new UserKeys -{ - UserId = keeper.Id.ToString(), - PublicKey = keeperKeys.publicKey, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow -}); - -await db.Create("user_keys", new UserKeys -{ - UserId = kira.Id.ToString(), - PublicKey = kiraKeys.publicKey, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow -}); - -Console.WriteLine("Public keys stored for both users."); - -var conversation = await db.Create("conversations", new Conversations -{ - CreatedByUserId = keeper.Id.ToString(), - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow, - Title = "Keeper317 + Ru_Kira", - IsDirectMessage = true -}); - -Console.WriteLine($"Conversation created: {ToJsonString(conversation)}"); - -await db.Create("conversation_members", new ConversationMembers -{ - ConversationId = conversation.Id.ToString(), - UserId = keeper.Id.ToString(), - JoinedAt = DateTime.UtcNow -}); - -await db.Create("conversation_members", new ConversationMembers -{ - ConversationId = conversation.Id.ToString(), - UserId = kira.Id.ToString(), - JoinedAt = DateTime.UtcNow -}); - -Console.WriteLine("Conversation members added."); - -var encrypted = E2EeHelper.EncryptForRecipient("hello from Keeper317", kiraKeys.publicKey); - -var savedMessage = await db.Create("messages", new Messages -{ - ConversationId = conversation.Id.ToString(), - SenderUserId = keeper.Id.ToString(), - RecipientUserId = kira.Id.ToString(), - CipherText = encrypted.CipherText, - Nonce = encrypted.Nonce, - Tag = encrypted.Tag, - EncryptedKey = encrypted.EncryptedKey, - CreatedAt = DateTime.UtcNow -}); - -Console.WriteLine($"Encrypted message saved: {ToJsonString(savedMessage)}"); - -var decrypted = E2EeHelper.DecryptForRecipient(encrypted, kiraKeys.privateKey); -Console.WriteLine($"Decrypted for Ru_Kira: {decrypted}"); - -return; - -static string ToJsonString(object? o) -{ - return JsonSerializer.Serialize(o, new JsonSerializerOptions { WriteIndented = true }); -} - -static async Task CreateUserAsync(SurrealDbClient db, string username, string email, string rawPassword) -{ - var now = DateTime.UtcNow; - - var user = new Users - { - Username = username, - Email = email, - CreatedAt = now, - UpdatedAt = now, - LastLogin = now, - TwoFactorEnabled = false, - EmailVerified = false, - AccountStatus = (int)AccountStatuses.Active, - OnlineStatus = (int)OnlineStatuses.Online, - }; - - var created = await db.Create("users", user); - - var hasher = new PasswordHasher.PasswordHasher(); - var passwordHash = hasher.HashPassword(created.Id.ToString() + rawPassword); - - var updated = await db.Merge(new PasswordHash - { - Id = created.Id, - Password = passwordHash - }); - - return updated; -} - -public static class KeyStorage -{ - public static void SavePrivateKey(string username, string privateKey) - { - Directory.CreateDirectory("keys"); - File.WriteAllText(Path.Combine("keys", $"{username}.private.key"), privateKey); - } - - public static string LoadPrivateKey(string username) - { - return File.ReadAllText(Path.Combine("keys", $"{username}.private.key")); - } - - public static bool PrivateKeyExists(string username) - { - return File.Exists(Path.Combine("keys", $"{username}.private.key")); - } -} - -public class ResponsibilityMerge : Record -{ - public bool Marketing { get; set; } -} -public class Group -{ - public bool Marketing { get; set; } - public int Count { get; set; } -} - -public class Users : Record -{ - public required string Username { get; set; } - public string? Password { get; set; } - public required string Email { get; set; } - public required DateTime CreatedAt { get; set; } - public required DateTime UpdatedAt { get; set; } - public required DateTime LastLogin { get; set; } - public bool TwoFactorEnabled { get; set; } - public bool EmailVerified { get; set; } - public required int AccountStatus { get; set; } - public required int OnlineStatus { get; set; } -} - -public class PasswordHash : Record -{ - public string? Password { get; set; } -} - -public class Sessions : Record -{ - public required string UserId { get; set; } - public required string TokenHash { get; set; } - public required DateTime IssuedAt { get; set; } - public required DateTime ExpiresAt { get; set; } - public DateTime? LastUsedAt { get; set; } - public bool Revoked { get; set; } - public required string DeviceName { get; set; } - public required string IpAddress { get; set; } - public required string UserAgent { get; set; } -} - -public class PasswordReset : Record -{ - public required string UserId { get; set; } - public required string TokenHash { get; set; } - public required DateTime CreatedAt { get; set; } - public required DateTime ExpiresAt { get; set; } - public bool Revoked { get; set; } -} - -public class Licenses : Record -{ - public required string UserId { get; set; } - public required int LicenseType { get; set; } - public required int Status { get; set; } - public required DateTime CreatedAt { get; set; } - public required DateTime StartsAt { get; set; } - public required DateTime UpdatedAt { get; set; } - public required DateTime ExpiresAt { get; set; } - -} - -public class AuthAudits : Record -{ - public required string UserId { get; set; } - public required int EventType { get; set; } - public bool Success { get; set; } - public required string IpAddress { get; set; } - public required string UserAgent { get; set; } - public required string Details { get; set; } - public required DateTime CreatedAt { get; set; } -} - -public class UserKeys : Record -{ - public required string UserId { get; set; } - public required string PublicKey { get; set; } - public required DateTime CreatedAt { get; set; } - public required DateTime UpdatedAt { get; set; } -} - -public class Conversations : Record -{ - public required string CreatedByUserId { get; set; } - public required DateTime CreatedAt { get; set; } - public required DateTime UpdatedAt { get; set; } - public string? Title { get; set; } - public bool IsDirectMessage { get; set; } -} - -public class ConversationMembers : Record -{ - public required string ConversationId { get; set; } - public required string UserId { get; set; } - public required DateTime JoinedAt { get; set; } -} - -public class Messages : Record -{ - public required string ConversationId { get; set; } - public required string SenderUserId { get; set; } - public required string RecipientUserId { get; set; } - public required string CipherText { get; set; } - public required string Nonce { get; set; } - public required string Tag { get; set; } - public required string EncryptedKey { get; set; } - public required DateTime CreatedAt { get; set; } -} - -enum AccountStatuses -{ - Active, - Suspended, - Banned, - Deleted -} - -enum OnlineStatuses -{ - Online, - Busy, - DND, - Invisible, - Offline -} - -enum LicenseStatuses -{ - Active, - Expired, - Renewable, - Revoked -} - -enum LicenseType -{ - Free, - Basic, - Advanced, - Pro, - Enterprise -} - -enum LogEvents -{ - LoginSuccess, - LoginFailure, - LogoutSuccess, - LogoutFailure, - PasswordResetSuccess, - PasswordResetFailure, -} \ No newline at end of file diff --git a/Relay.sln b/Relay.sln new file mode 100644 index 0000000..add378e --- /dev/null +++ b/Relay.sln @@ -0,0 +1,62 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RelayCore", "RelayCore\RelayCore.csproj", "{346BE501-DE74-4E88-9787-4722FBC8BD0D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RelayClient", "RelayClient\RelayClient.csproj", "{AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RelayServer", "RelayServer\RelayServer.csproj", "{38995780-E9AA-44D6-B62D-07CCA45E4E4C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Debug|x64.ActiveCfg = Debug|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Debug|x64.Build.0 = Debug|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Debug|x86.ActiveCfg = Debug|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Debug|x86.Build.0 = Debug|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Release|Any CPU.Build.0 = Release|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Release|x64.ActiveCfg = Release|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Release|x64.Build.0 = Release|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Release|x86.ActiveCfg = Release|Any CPU + {346BE501-DE74-4E88-9787-4722FBC8BD0D}.Release|x86.Build.0 = Release|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Debug|x64.ActiveCfg = Debug|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Debug|x64.Build.0 = Debug|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Debug|x86.Build.0 = Debug|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Release|Any CPU.Build.0 = Release|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Release|x64.ActiveCfg = Release|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Release|x64.Build.0 = Release|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Release|x86.ActiveCfg = Release|Any CPU + {AB9DA5AB-55DC-4DE4-834C-E1E1BCD0C3CD}.Release|x86.Build.0 = Release|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Debug|x64.ActiveCfg = Debug|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Debug|x64.Build.0 = Debug|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Debug|x86.ActiveCfg = Debug|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Debug|x86.Build.0 = Debug|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Release|Any CPU.Build.0 = Release|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Release|x64.ActiveCfg = Release|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Release|x64.Build.0 = Release|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Release|x86.ActiveCfg = Release|Any CPU + {38995780-E9AA-44D6-B62D-07CCA45E4E4C}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/RelayClient/.gitignore b/RelayClient/.gitignore new file mode 100644 index 0000000..b59e83d --- /dev/null +++ b/RelayClient/.gitignore @@ -0,0 +1,93 @@ +############################################ +# .NET Build +############################################ + +bin/ +obj/ +out/ +publish/ + +############################################ +# Visual Studio +############################################ + +.vs/ +*.user +*.suo +*.userprefs +*.csproj.user +*.dbmdl +*.cache +*.pdb +*.opendb + +############################################ +# Rider / JetBrains +############################################ + +.idea/ +*.sln.iml + +############################################ +# VSCode +############################################ + +.vscode/ + +############################################ +# NuGet +############################################ + +*.nupkg +*.snupkg +packages/ +.nuget/ +.nuget/packages/ + +############################################ +# Logs +############################################ + +*.log +logs/ + +############################################ +# OS files +############################################ + +.DS_Store +Thumbs.db + +############################################ +# Local secrets / environment +############################################ + +.env +.env.* +secrets.json +appsettings.Development.json + +############################################ +# E2EE private keys +############################################ + +keys/* +!keys/.gitkeep + +############################################ +# Local test databases / data folders +############################################ + +data/ +*.db +*.sqlite +*.sqlite3 + +############################################ +# Temporary files +############################################ + +*.tmp +*.temp +*.bak +*.swp \ No newline at end of file diff --git a/RelayClient/App.xaml b/RelayClient/App.xaml new file mode 100644 index 0000000..0485623 --- /dev/null +++ b/RelayClient/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/RelayClient/App.xaml.cs b/RelayClient/App.xaml.cs new file mode 100644 index 0000000..3da581d --- /dev/null +++ b/RelayClient/App.xaml.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace RelayClient; + +public partial class App : Application +{ + public App() + { + InitializeComponent(); + } + + protected override Window CreateWindow(IActivationState? activationState) + { + return new Window(new AppShell()); + } +} \ No newline at end of file diff --git a/RelayClient/AppShell.xaml b/RelayClient/AppShell.xaml new file mode 100644 index 0000000..b205bde --- /dev/null +++ b/RelayClient/AppShell.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/RelayClient/AppShell.xaml.cs b/RelayClient/AppShell.xaml.cs new file mode 100644 index 0000000..b751b95 --- /dev/null +++ b/RelayClient/AppShell.xaml.cs @@ -0,0 +1,9 @@ +namespace RelayClient; + +public partial class AppShell : Shell +{ + public AppShell() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/RelayClient/MainPage.xaml b/RelayClient/MainPage.xaml new file mode 100644 index 0000000..f8b87d0 --- /dev/null +++ b/RelayClient/MainPage.xaml @@ -0,0 +1,36 @@ + + + + + + + +