Added project

This commit is contained in:
Gabriel Tofvesson 2018-03-31 00:57:10 +02:00
parent e44ae49ceb
commit d17f4842d3
48 changed files with 7100 additions and 287 deletions

300
.gitignore vendored
View File

@ -1,288 +1,14 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
################################################################################
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
################################################################################
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Typescript v1 declaration files
typings/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
/.vs/Bank/v15
/Bank/obj/Debug
/Client/bin
/Client/obj
/Common/bin
/Common/obj
/keys
/Server/Resources/0x200.d
/Server/bin
/Server/obj

37
Bank.sln Normal file
View File

@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.2020
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Server", "Server\Server.csproj", "{B458552A-5884-4B27-BA6B-826BC5590106}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{2236D5D4-7816-4630-8C86-0F0BDD46D7D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{23EB87D4-E310-48C4-A931-0961C83892D7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B458552A-5884-4B27-BA6B-826BC5590106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B458552A-5884-4B27-BA6B-826BC5590106}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B458552A-5884-4B27-BA6B-826BC5590106}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B458552A-5884-4B27-BA6B-826BC5590106}.Release|Any CPU.Build.0 = Release|Any CPU
{2236D5D4-7816-4630-8C86-0F0BDD46D7D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2236D5D4-7816-4630-8C86-0F0BDD46D7D8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2236D5D4-7816-4630-8C86-0F0BDD46D7D8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2236D5D4-7816-4630-8C86-0F0BDD46D7D8}.Release|Any CPU.Build.0 = Release|Any CPU
{23EB87D4-E310-48C4-A931-0961C83892D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{23EB87D4-E310-48C4-A931-0961C83892D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{23EB87D4-E310-48C4-A931-0961C83892D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{23EB87D4-E310-48C4-A931-0961C83892D7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E0FD1BD7-4DD9-46C6-A710-6E5371F96DE7}
EndGlobalSection
EndGlobal

6
Bank/App.config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
</configuration>

52
Bank/Bank.csproj Normal file
View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{ACD33936-2A7E-48F0-B7E5-8AA818C309D5}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>Bank</RootNamespace>
<AssemblyName>Bank</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

15
Bank/Program.cs Normal file
View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Bank
{
class Program
{
static void Main(string[] args)
{
}
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Bank")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Bank")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("acd33936-2a7e-48f0-b7e5-8aa818c309d5")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

1
BankProject Submodule

@ -0,0 +1 @@
Subproject commit e44ae49cebfda90b5394073c04798a79b1f427e2

6
Client/App.config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
</configuration>

85
Client/Client.csproj Normal file
View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{2236D5D4-7816-4630-8C86-0F0BDD46D7D8}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>Client</RootNamespace>
<AssemblyName>Client</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="ConsoleForms.cs" />
<Compile Include="NetContext.cs" />
<Compile Include="Networking.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="SessionContext.cs" />
<Compile Include="WelcomeContext.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="Resources\0x200.e" />
<None Include="Resources\0x200.n" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj">
<Project>{23eb87d4-e310-48c4-a931-0961c83892d7}</Project>
<Name>Common</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Content Include="Session.xml" />
<Content Include="Setup.xml">
<SubType>Designer</SubType>
</Content>
<Content Include="Networking.xml">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

1370
Client/ConsoleForms.cs Normal file

File diff suppressed because it is too large Load Diff

90
Client/NetContext.cs Normal file
View File

@ -0,0 +1,90 @@
using ConsoleForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tofvesson.Collections;
namespace Client
{
public class NetContext : Context
{
public NetContext(ContextManager manager) : base(manager, "Networking")
{
// Just close when anything is selected and "submitted"
RegisterSelectListeners((s, i, v) => controller.CloseView(s), "EmptyFieldError", "IPError", "PortError", "ConnectionError");
((InputTextBox)views.GetNamed("NetConnect")).SubmissionsListener = i =>
{
bool
ip = ParseIP(i.Inputs[0].Text) != null,
port = short.TryParse(i.Inputs[1].Text, out short prt) && prt > 0;
if (ip && port)
{
// Connect to server here
BankNetInteractor ita = new BankNetInteractor(i.Inputs[0].Text, prt, false); // Don't do identity check for now
try
{
var t = ita.Connect();
while (!t.IsCompleted)
if (t.IsCanceled || t.IsFaulted)
{
controller.AddView(views.GetNamed("ConnectError"));
return;
}
}
catch
{
controller.AddView(views.GetNamed("ConnectionError"));
return;
}
manager.LoadContext(new WelcomeContext(manager, ita));
}
else if (i.Inputs[0].Text.Length == 0 || i.Inputs[1].Text.Length == 0) controller.AddView(views.GetNamed("EmptyFieldError"));
else if (!ip) controller.AddView(views.GetNamed("IPError"));
else controller.AddView(views.GetNamed("PortError"));
};
}
public override void OnCreate()
{
controller.AddView(views.GetNamed("NetConnect"));
}
public override void OnDestroy()
{
foreach (var view in views)
controller.CloseView(view.Item2);
}
//int gtrack = 0;
public override bool Update(ConsoleController.KeyEvent keypress, bool hasKeypress = true)
{
/*
var connectBox = (TextBox)views.GetNamed("NetConnect");
if (++gtrack == 10)
{
connectBox.BorderColor = (ConsoleColor)((int)(connectBox.BorderColor + 1) % 16);
gtrack = 0;
}
connectBox.Dirty = true;
*/
return base.Update(keypress, hasKeypress);
}
private static byte[] ParseIP(string ip)
{
if (!ip.ContainsExactly('.', 3)) return null;
string[] vals = ip.Split('.');
byte[] parts = new byte[4];
for(int i = 0; i<4; ++i)
if (!byte.TryParse(vals[i], out parts[i])) return null;
return parts;
}
}
}

182
Client/Networking.cs Normal file
View File

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tofvesson.Crypto;
namespace Client
{
public class BankNetInteractor
{
protected static readonly CryptoRandomProvider provider = new CryptoRandomProvider();
protected static readonly Dictionary<long, OnClientConnectStateChanged> changeListeners = new Dictionary<long, OnClientConnectStateChanged>();
protected Dictionary<long, Promise> promises = new Dictionary<long, Promise>();
protected NetClient client;
private bool authenticating = true, authenticated = false;
public bool Authenticating { get => authenticating; }
public bool PeerIsAuthenticated { get => authenticated; }
public RSA AuthenticatedKeys { get; private set; }
public bool IsAlive { get => client.IsAlive; }
public BankNetInteractor(string address, short port, bool checkIdentity = true)
{
if(checkIdentity)
new Task(() =>
{
AuthenticatedKeys = NetClient.CheckServerIdentity(address, port, provider);
authenticating = false;
authenticated = AuthenticatedKeys != null;
}).Start();
else
{
authenticating = false;
authenticated = false;
}
var addr = System.Net.IPAddress.Parse(address);
client = new NetClient(
new Rijndael128(
Convert.ToBase64String(provider.GetBytes(64)), // 64-byte key (converted to base64)
Convert.ToBase64String(provider.GetBytes(64)) // 64-byte salt (converted to base64)
),
addr,
port,
MessageRecievedHandler,
ClientConnectionHandler,
65536); // 64 KiB buffer
}
public virtual Task Connect()
{
client.Connect();
Task t = new Task(() =>
{
while (!client.IsAlive) System.Threading.Thread.Sleep(125);
});
t.Start();
return t;
}
public async virtual Task<object> Disconnect() => await client.Disconnect();
public long RegisterListener(OnClientConnectStateChanged stateListener)
{
long tkn;
changeListeners[tkn = GetListenerToken()] = stateListener;
return tkn;
}
public void UnregisterListener(long tkn) => changeListeners.Remove(tkn);
protected virtual string MessageRecievedHandler(string msg, Dictionary<string, string> associated, ref bool keepAlive)
{
string response = HandleResponse(msg, out long pID, out bool err);
if (err || !promises.ContainsKey(pID)) return null;
Promise p = promises[pID];
promises.Remove(pID);
p.Value = response;
p.HasValue = true;
p.Subscribe?.Invoke(p);
return null;
}
protected virtual void ClientConnectionHandler(NetClient client, bool connect)
{
foreach (var listener in changeListeners.Values)
listener(client, connect);
}
public virtual Promise CheckAccountAvailability(string username)
{
if (username.Length > 60)
return new Promise
{
HasValue = true,
Value = "ERROR"
};
client.Send(CreateCommandMessage("Avail", username, out long pID));
Promise p = new Promise();
promises[pID] = p;
return p;
}
public virtual Promise Authenticate(string username, string password)
{
if (username.Length > 60)
return new Promise
{
HasValue = true,
Value = "ERROR"
};
client.Send(CreateCommandMessage("Auth", username+":"+password, out long pID));
Promise p = new Promise();
promises[pID] = p;
return p;
}
public virtual Promise Register(string username, string password)
{
if (username.Length > 60)
return new Promise
{
HasValue = true,
Value = "ERROR"
};
client.Send(CreateCommandMessage("Reg", username + ":" + password, out long pID));
Promise p = new Promise();
promises[pID] = p;
return p;
}
public virtual void Logout(string sessionID)
=> client.Send(CreateCommandMessage("Logout", sessionID, out long _));
protected long GetNewPromiseUID()
{
long l;
do l = provider.NextLong();
while (promises.ContainsKey(l));
return l;
}
protected long GetListenerToken()
{
long l;
do l = provider.NextLong();
while (changeListeners.ContainsKey(l));
return l;
}
protected static void PostPromise(Promise p, string value)
{
p.Value = value;
p.HasValue = true;
p.Subscribe?.Invoke(p);
}
protected static string HandleResponse(string response, out long promiseID, out bool error)
{
error = !long.TryParse(response.Substring(0, Math.Max(0, response.IndexOf(':'))), out promiseID);
return response.Substring(Math.Max(0, response.IndexOf(':') + 1));
}
protected string CreateCommandMessage(string command, string message, out long promiseID) => command + ":" + (promiseID = GetNewPromiseUID()) + ":" + message;
}
public delegate void Event(Promise p);
public class Promise
{
private Event evt;
public string Value { get; internal set; }
public bool HasValue { get; internal set; }
public Event Subscribe
{
get => evt;
set
{
evt = value;
if (HasValue)
evt(this);
}
}
}
}

85
Client/Networking.xml Normal file
View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="ConsoleForms">
<!-- Networking context -->
<InputTextBox id="NetConnect"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1">
<Fields>
<Field input_type="decimal" max_length="15">Server IP:</Field>
<Field default="80" input_type="integer" max_length="5">Port:</Field>
</Fields>
<Text>Server connection configuration</Text>
</InputTextBox>
<DialogBox id="SecurityIssue"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
select="1">
<Options>
<Option>Continue</Option>
<Option>Cancel</Option>
</Options>
<Text>The identity of the server could not be verified. Continue?</Text>
</DialogBox>
<DialogBox id="Connecting"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1">
<Options>
<Option>Cancel</Option>
</Options>
<Text>Connecting to server...</Text>
</DialogBox>
<!-- Generic dialog boxes -->
<DialogBox id="EmptyFieldError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1">
<Options>
<Option>Ok</Option>
</Options>
<Text>One or more required input field is empty</Text>
</DialogBox>
<DialogBox id="IPError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1">
<Options>
<Option>Ok</Option>
</Options>
<Text>The supplied IP-address is not valid</Text>
</DialogBox>
<DialogBox id="PortError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1">
<Options>
<Option>Ok</Option>
</Options>
<Text>The supplied port is not valid</Text>
</DialogBox>
<DialogBox id="ConnectionError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="4">
<Options>
<Option>Ok</Option>
</Options>
<Text>Could not connect to server</Text>
</DialogBox>
</Elements>

62
Client/Program.cs Normal file
View File

@ -0,0 +1,62 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Client;
using Client.Properties;
using Common;
using Tofvesson.Collections;
namespace ConsoleForms
{
class Program
{
public static TextWriter DebugStream = new DebugAdapterWriter();
private static ConsoleController controller = ConsoleController.singleton;
static void Main(string[] args)
{
// Set up timestamps in debug output
DebugStream = new TimeStampWriter(DebugStream, "HH:mm:ss.fff");
Padding p = new AbsolutePadding(2, 2, 1, 1);
Console.CursorVisible = false;
Console.Title = "Tofvesson Enterprises"; // Set console title
// Start with the networking context
ContextManager manager = new ContextManager();
manager.LoadContext(new NetContext(manager));
// Start input listener loop. Graphics happen here too (triggered by keystrokes)
ConsoleController.KeyEvent info = new ConsoleController.KeyEvent(default(ConsoleKeyInfo))
{
ValidEvent = false
};
bool first = true;
do
{
if (first) first = false;
else info = controller.ReadKey();
bool b = manager.Update(info), haskey = false;
while (b)
{
System.Threading.Thread.Sleep(25);
haskey = _kbhit() != 0;
if (haskey) info = controller.ReadKey(false);
b = manager.Update(info, haskey);
controller.Draw();
}
} while (!info.ValidEvent || info.Event.Key != ConsoleKey.Escape);
}
[DllImport("msvcrt")]
public static extern int _kbhit();
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Client")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Client")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("2236d5d4-7816-4630-8c86-0f0bdd46d7d8")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

149
Client/Properties/Resources.Designer.cs generated Normal file
View File

@ -0,0 +1,149 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Client.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Client.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] e_0x200 {
get {
object obj = ResourceManager.GetObject("e_0x200", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] n_0x200 {
get {
object obj = ResourceManager.GetObject("n_0x200", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized string similar to &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
///&lt;Elements xmlns=&quot;ConsoleForms&quot;&gt;
/// &lt;!-- Networking context --&gt;
/// &lt;InputTextBox id=&quot;NetConnect&quot;
/// padding_left=&quot;2&quot;
/// padding_right=&quot;2&quot;
/// padding_top=&quot;1&quot;
/// padding_bottom=&quot;1&quot;&gt;
/// &lt;Fields&gt;
/// &lt;Field input_type=&quot;decimal&quot; max_length=&quot;15&quot;&gt;Server IP:&lt;/Field&gt;
/// &lt;Field default=&quot;80&quot; input_type=&quot;integer&quot; max_length=&quot;5&quot;&gt;Port:&lt;/Field&gt;
/// &lt;/Fields&gt;
/// &lt;Text&gt;Server connection configuration&lt;/Text&gt;
/// &lt;/Input [rest of string was truncated]&quot;;.
/// </summary>
internal static string Networking {
get {
return ResourceManager.GetString("Networking", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
///&lt;Elements xmlns=&quot;ConsoleForms&quot;&gt;
/// &lt;DialogBox id=&quot;Success&quot;
/// padding_left=&quot;2&quot;
/// padding_right=&quot;2&quot;
/// padding_top=&quot;1&quot;
/// padding_bottom=&quot;1&quot;&gt;
/// &lt;Options&gt;
/// &lt;Option&gt;Quit&lt;/Option&gt;
/// &lt;/Options&gt;
/// &lt;Text&gt;Login succeeded!&lt;/Text&gt;
/// &lt;/DialogBox&gt;
///&lt;/Elements&gt;.
/// </summary>
internal static string Session {
get {
return ResourceManager.GetString("Session", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to &lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&gt;
///&lt;Elements xmlns=&quot;ConsoleForms&quot;&gt;
///
/// &lt;!-- Welcome screen --&gt;
/// &lt;DialogBox id=&quot;WelcomeScreen&quot;
/// padding_left=&quot;2&quot;
/// padding_right=&quot;2&quot;
/// padding_top=&quot;1&quot;
/// padding_bottom=&quot;1&quot;
/// width=&quot;42&quot;&gt;
/// &lt;Options&gt;
/// &lt;Option event=&quot;Setup:Login&quot; close=&quot;true&quot;&gt;Login&lt;/Option&gt;
/// &lt;Option event=&quot;Setup:Register&quot; close=&quot;true&quot;&gt;Register&lt;/Option&gt;
/// &lt;/Options&gt;
/// &lt;Text&gt;Welcome to the Tofvesson banking system! To conti [rest of string was truncated]&quot;;.
/// </summary>
internal static string Setup {
get {
return ResourceManager.GetString("Setup", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="e_0x200" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\0x200.e;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Networking" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Networking.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="n_0x200" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\0x200.n;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="Setup" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Setup.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
<data name="Session" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Session.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8</value>
</data>
</root>

BIN
Client/Resources/0x200.e Normal file

Binary file not shown.

BIN
Client/Resources/0x200.n Normal file

Binary file not shown.

13
Client/Session.xml Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="ConsoleForms">
<DialogBox id="Success"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1">
<Options>
<Option>Quit</Option>
</Options>
<Text>Login succeeded!</Text>
</DialogBox>
</Elements>

39
Client/SessionContext.cs Normal file
View File

@ -0,0 +1,39 @@
using ConsoleForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tofvesson.Collections;
namespace Client
{
public sealed class SessionContext : Context
{
private readonly BankNetInteractor interactor;
private readonly string sessionID;
public SessionContext(ContextManager manager, BankNetInteractor interactor, string sessionID) : base(manager, "Session")
{
this.interactor = interactor;
this.sessionID = sessionID;
((DialogBox)views.GetNamed("Success")).RegisterSelectListener((v, i, s) =>
{
interactor.Logout(sessionID);
manager.LoadContext(new NetContext(manager));
});
}
public override void OnCreate()
{
controller.AddView(views.GetNamed("Success"));
}
public override void OnDestroy()
{
controller.CloseView(views.GetNamed("Success"));
interactor.Disconnect();
}
}
}

128
Client/Setup.xml Normal file
View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="ConsoleForms">
<!-- Welcome screen -->
<DialogBox id="WelcomeScreen"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
width="42">
<Options>
<Option event="Setup:Login" close="true">Login</Option>
<Option event="Setup:Register" close="true">Register</Option>
</Options>
<Text>Welcome to the Tofvesson banking system! To continue, press [ENTER] To go back, press [ESCAPE]</Text>
</DialogBox>
<!-- Register-context views -->
<InputTextBox id="Register"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
width="35"
back="Setup:WelcomeScreen">
<Fields>
<Field>Username:</Field>
<Field hide="true">Password:</Field>
<Field hide="true">Repeat password:</Field>
</Fields>
<Text>Register Account</Text>
</InputTextBox>
<TextBox id="RegWait"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="3">
<Text>Registering...</Text>
</TextBox>
<DialogBox id="DuplicateAccountError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="4">
<Options>
<Option>Ok</Option>
</Options>
<Text>An account with this username already exists!</Text>
</DialogBox>
<DialogBox id="PasswordMismatchError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="4">
<Options>
<Option>Ok</Option>
</Options>
<Text>The entered passwords don't match! </Text>
</DialogBox>
<DialogBox id="WeakPasswordWarning"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="6">
<Options>
<Option>Yes</Option>
<Option>No</Option>
</Options>
<Text>The password you have supplied has been deemed to be weak. Are you sure you want to continue?</Text>
</DialogBox>
<!-- Login-context views -->
<InputTextBox id="Login"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
width="35"
back="Setup:WelcomeScreen">
<Fields>
<Field>Username:</Field>
<Field hide="true">Password:</Field>
</Fields>
<Text>Log in</Text>
</InputTextBox>
<TextBox id="AuthWait"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="3">
<Text>Authenticating...</Text>
</TextBox>
<!-- Generic dialog boxes -->
<DialogBox id="EmptyFieldError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="4">
<Options>
<Option>Ok</Option>
</Options>
<Text>One or more required input field is empty</Text>
</DialogBox>
<DialogBox id="AuthError"
padding_left="2"
padding_right="2"
padding_top="1"
padding_bottom="1"
border="4">
<Options>
<Option>Ok</Option>
</Options>
<Text>The given username or password was incorrect</Text>
</DialogBox>
</Elements>

159
Client/WelcomeContext.cs Normal file
View File

@ -0,0 +1,159 @@
using ConsoleForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tofvesson.Collections;
namespace Client
{
public sealed class WelcomeContext : Context
{
private readonly BankNetInteractor interactor;
private long token;
private Promise promise;
private bool forceDestroy = true;
public WelcomeContext(ContextManager manager, BankNetInteractor connection) : base(manager, "Setup")
{
this.interactor = connection;
// Prepare events and stuff
// Just close when anything is selected and "submitted"
RegisterSelectListeners((s, i, v) => controller.CloseView(s), "DuplicateAccountError", "EmptyFieldError", "IPError", "PortError", "AuthError", "PasswordMismatchError");
((InputTextBox)views.GetNamed("Login")).SubmissionsListener = i =>
{
bool success = true;
foreach (var input in i.Inputs)
{
if (input.Text.Length == 0)
{
success = false;
input.SelectBackgroundColor = ConsoleColor.Red;
input.BackgroundColor = ConsoleColor.DarkRed;
}
}
if (success)
{
// Authenticate against server here
controller.AddView(views.GetNamed("AuthWait"));
promise = interactor.Authenticate(i.Inputs[0].Text, i.Inputs[1].Text);
promise.Subscribe =
response =>
{
controller.CloseView(views.GetNamed("AuthWait"));
if (response.Value.Equals("ERROR"))
controller.AddView(views.GetNamed("AuthError"));
else
{
forceDestroy = false;
manager.LoadContext(new SessionContext(manager, interactor, response.Value));
}
};
}
else controller.AddView(views.GetNamed("EmptyFieldError"));
};
// For a smooth effect
((InputTextBox)views.GetNamed("Login")).InputListener = (v, c, i) =>
{
c.BackgroundColor = v.DefaultBackgroundColor;
c.SelectBackgroundColor = v.DefaultSelectBackgroundColor;
return true;
};
((InputTextBox)views.GetNamed("Register")).SubmissionsListener = i =>
{
bool success = true, mismatch = false;
foreach (var input in i.Inputs)
{
if (input.Text.Length == 0)
{
success = false;
input.SelectBackgroundColor = ConsoleColor.Red;
input.BackgroundColor = ConsoleColor.DarkRed;
}
}
mismatch = !i.Inputs[1].Text.Equals(i.Inputs[2].Text);
if (success && !mismatch)
{
void a()
{
controller.AddView(views.GetNamed("RegWait"));
promise = interactor.Register(i.Inputs[0].Text, i.Inputs[1].Text);
promise.Subscribe =
response =>
{
controller.CloseView(views.GetNamed("RegWait"));
if (response.Value.Equals("ERROR"))
controller.AddView(views.GetNamed("DuplicateAccountError"));
else
{
forceDestroy = false;
manager.LoadContext(new SessionContext(manager, interactor, response.Value));
}
};
}
if (i.Inputs[1].Text.Length < 5 || i.Inputs[1].Text.StartsWith("asdfasdf") || i.Inputs[1].Text.StartsWith("asdf1234"))
{
var warning = (DialogBox)views.GetNamed("WeakPasswordWarning");
warning.RegisterSelectListener((wrn, idx, sel) =>
{
controller.CloseView(warning);
if (idx == 0) a();
});
controller.AddView(warning);
}
else a();
}
else if (mismatch) controller.AddView(views.GetNamed("PasswordMismatchError"));
else controller.AddView(views.GetNamed("EmptyFieldError"));
};
((InputTextBox)views.GetNamed("Register")).InputListener = (v, c, i) =>
{
c.BackgroundColor = v.DefaultBackgroundColor;
c.SelectBackgroundColor = v.DefaultSelectBackgroundColor;
return true;
};
}
public override void OnCreate()
{
token = interactor.RegisterListener((c, s) =>
{
if(!s) controller.Popup("The connection to the server was severed! ", 4500, ConsoleColor.DarkRed, () => manager.LoadContext(new NetContext(manager)));
});
// Add the initial view
controller.AddView(views.GetNamed("WelcomeScreen"));
}
public override void OnDestroy()
{
// TODO: Save state
// Close views
foreach (var view in views)
controller.CloseView(view.Item2);
// Unsubscribe from events
if (promise != null && !promise.HasValue) promise.Subscribe = null;
// Stop listening
interactor.UnregisterListener(token);
if (forceDestroy) interactor.Disconnect();
}
}
}

668
Common/AES.cs Normal file
View File

@ -0,0 +1,668 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
namespace Tofvesson.Crypto
{
public class Rijndael128 : BlockCipher
{
protected readonly byte[] roundKeys;
protected readonly byte[] key;
public Rijndael128(string key, string salt = "PlsNoRainbowz") : base(16)
{
// Derive a proper key
var t = DeriveKey(key, salt);
this.key = t.Item1;
// Expand the derived key
roundKeys = KeySchedule(this.key, BitMode.Bit128);
}
protected Rijndael128(byte[] key) : base(16)
{
this.key = key;
// Expand the derived key
roundKeys = KeySchedule(this.key, BitMode.Bit128);
}
// Encrypt/Decrypt a string by just converting it to bytes and passing it along to the byte-based encryption/decryption methods
public byte[] EncryptString(string message) => Encrypt(Encoding.UTF8.GetBytes(message));
public string DecryptString(byte[] message, int length) => new string(Encoding.UTF8.GetChars(Decrypt(message, length, false))).Substring(0, length);
// Encrypt a message (this one just splits the message into blocks and passes it along)
public override byte[] Encrypt(byte[] message)
{
byte[] result = new byte[message.Length + ((16 - (message.Length % 16))%16)];
Array.Copy(message, result, message.Length);
for(int i = 0; i<result.Length/16; ++i)
Array.Copy(AES128_Encrypt(result.SubArray(i * 16, i * 16 + 16)), 0, result, i * 16, 16);
return result;
}
// Decrypt a message (these just pass the message along)
public override byte[] Decrypt(byte[] ciphertext) => Decrypt(ciphertext, -1, false);
public byte[] Decrypt(byte[] message, int messageLength) => Decrypt(message, messageLength, true);
protected byte[] Decrypt(byte[] message, int messageLength, bool doTruncate)
{
if (message.Length % 16 != 0) throw new SystemException("Invalid encrypted message length!");
byte[] result = new byte[message.Length];
Array.Copy(message, result, message.Length);
for (int i = 0; i < result.Length / 16; ++i)
Array.Copy(AES128_Decrypt(result.SubArray(i * 16, i * 16 + 16)), 0, result, i * 16, 16);
return doTruncate ? result.SubArray(0, messageLength) : result;
}
// The actual AES encryption implementation
protected virtual byte[] AES128_Encrypt(byte[] input)
{
// The "state" is the name given the the 4x4 matrix that AES encrypts. The state is known as the "state" no matter what stage of AES it has gone through or how many left it has
byte[] state = new byte[16];
Array.Copy(input, state, 16);
// Initial round. Just just xor the key for this round input the input
state = AddRoundKey(state, roundKeys, 0);
// Rounds 1 - 9
for (int rounds = 1; rounds < 10; ++rounds)
{
state = ShiftRows(SubBytes(state, false)); // Shift the rows of the column-major matrix
if (rounds != 9) state = MixColumns(state, true); // Mix the columns (gonna be honest, I don't remember what this does, but it has something to do with galois fields, so just check Galois2 out)
state = AddRoundKey(state, roundKeys, rounds * 16); // Xor the key into the mess
}
// Now this matrix is encrypted!
return state;
}
// Literally just the inverse functions of the Encrypt-process run in reverse.
protected virtual byte[] AES128_Decrypt(byte[] input)
{
byte[] state = new byte[16];
Array.Copy(input, state, 16);
for (int rounds = 9; rounds > 0; --rounds)
{
state = AddRoundKey(state, roundKeys, rounds * 16);
if (rounds != 9) state = MixColumns(state, false);
state = SubBytes(UnShiftRows(state), true);
}
return AddRoundKey(state, roundKeys, 0);
}
// Save the key to a file
public void Save(string baseName, bool force = false)
{
if (force || !File.Exists(baseName + ".key")) File.WriteAllBytes(baseName + ".key", key);
}
// Load the key from a file (gonna be honest, I think I just copy-pasted this from the RSA file and renamed some stuff)
public static Rijndael128 Load(string baseName)
{
if (!File.Exists(baseName + ".key")) throw new SystemException("Required files could not be located");
return new Rijndael128(File.ReadAllBytes(baseName + ".key"));
}
// De/-serializes the key (the method is just here for compatibility)
public byte[] Serialize() => Support.SerializeBytes(new byte[][] { key });
public static Rijndael128 Deserialize(byte[] message, out int read)
{
byte[][] output = Support.DeserializeBytes(message, 1);
read = output[0].Length + 8;
return new Rijndael128(output[0]);
}
// Internal methods for encryption :)
private static uint KSchedCore(uint input, int iteration)
{
input = Rotate(input);
byte[] bytes = Support.WriteToArray(new byte[4], input, 0);
for (int i = 0; i < bytes.Length; ++i) bytes[i] = SBox(bytes[i]);
bytes[bytes.Length - 1] ^= RCON(iteration);
return (uint)Support.ReadInt(bytes, 0);
}
// Rijndael key schedule: implemented for the three common implementations because I'm thorough or something
public enum BitMode { Bit128, Bit192, Bit256 }
private static byte[] KeySchedule(byte[] key, BitMode mode)
{
int n = mode == BitMode.Bit128 ? 16 : mode == BitMode.Bit192 ? 24 : 32;
int b = mode == BitMode.Bit128 ? 176 : mode == BitMode.Bit192 ? 208 : 240;
byte[] output = new byte[b];
Array.Copy(key, output, n);
int rcon_iter = 1;
int accruedBytes = n;
while (accruedBytes < b)
{
// Generate 4 new bytes of extended key
byte[] t = Support.WriteToArray(new byte[4], KSchedCore((uint)Support.ReadInt(output, accruedBytes - 4), rcon_iter), 0);
++rcon_iter;
for (int i = 0; i < 4; ++i) t[i] ^= output[accruedBytes - n + i];
Array.Copy(t, 0, output, accruedBytes, 4);
accruedBytes += 4;
// Generate 12 new bytes of extended key
for (int i = 0; i < 3; ++i)
{
Array.Copy(output, accruedBytes - 4, t, 0, 4);
for (int j = 0; j < 4; ++j) t[j] ^= output[accruedBytes - n + j];
Array.Copy(t, 0, output, accruedBytes, 4);
accruedBytes += 4;
}
// Special processing for 256-bit key schedule
if (mode == BitMode.Bit256)
{
Array.Copy(output, accruedBytes - 4, t, 0, 4);
for (int j = 0; j < 4; ++j) t[j] = (byte)(SBox(t[j]) ^ output[accruedBytes - n + j]);
Array.Copy(t, 0, output, accruedBytes, 4);
accruedBytes += 4;
}
// Special processing for 192-bit key schedule
if (mode != BitMode.Bit128)
for (int i = mode == BitMode.Bit192 ? 1 : 2; i >= 0; --i)
{
Array.Copy(output, accruedBytes - 4, t, 0, 4);
for (int j = 0; j < 4; ++j) t[j] ^= output[accruedBytes - n + j];
Array.Copy(t, 0, output, accruedBytes, 4);
accruedBytes += 4;
}
}
return output;
}
// MixColumns matrix basis. Used for multiplication over the rijndael field
private static readonly byte[] mix_matrix = new byte[] { 2, 3, 1, 1 };
private static readonly byte[] unmix_matrix = new byte[] { 14, 11, 13, 9 };
/// <summary>
/// Rijndael substitution step in the encryption (first thing that happens). This supplies confusion for the algorithm
/// </summary>
/// <param name="b">The value (most likely from the AES state) that should be substituted</param>
/// <returns>The substituted byte</returns>
private static byte SBox(byte b) => Affine(new Galois2(new byte[] { b }).InvMul().ToByteArray()[0]);
// Inverse SBox-function
private static byte ISBox(byte b) => new Galois2(new byte[] { Rffine(b) }).InvMul().ToByteArray()[0];
// Replaces GF(2^8) matrix multiplication for the affine and reverse affine functions
private static byte Affine(byte value) => (byte)(value ^ Rot(value, 1) ^ Rot(value, 2) ^ Rot(value, 3) ^ Rot(value, 4) ^ 0b0110_0011);
private static byte Rffine(byte value) => (byte)(Rot(value, 1) ^ Rot(value, 3) ^ Rot(value, 6) ^ 0b0000_0101);
// Rotate bits
private static byte Rot(byte value, int by) => (byte)((value << by) | (value >> (8 - by)));
private delegate byte SBOXFunc(byte b);
private static byte[] SubBytes(byte[] state, bool reverse)
{
SBOXFunc v;
if (reverse) v = ISBox;
else v = SBox;
for (int i = 0; i < state.Length; ++i) state[i] = v(state[i]);
return state;
}
// The AES state is a column-major 4x4 matrix (for AES-128). Demonstrated below are the decimal indices, as would be represented in the state:
// 00 04 08 12
// 01 05 09 13
// 02 06 10 14
// 03 07 11 15
// Shiftrows applied to state above:
// 00 04 08 12 - No change
// 05 09 13 01 - Shifted 1 to the left
// 10 14 02 06 - Shifted 2 to the left
// 15 03 07 11 - Shifted 3 to the left
/// <summary>
/// Shifts the rows of the column-major matrix
/// </summary>
/// <param name="state"></param>
/// <returns>The shifted matrix</returns>
public static byte[] ShiftRows(byte[] state)
{
for (int i = 1; i < 4; ++i)
{
uint value = GetRow(state, i);
for (int j = 0; j < i; ++j) value = Rotate(value);
WriteToRow(value, state, i);
}
return state;
}
// Reverse ShiftRows
private static byte[] UnShiftRows(byte[] state)
{
for (int i = 1; i < 4; ++i)
{
uint value = GetRow(state, i);
for (int j = 3; j >= i; --j) value = Rotate(value);
WriteToRow(value, state, i);
}
return state;
}
// Helper method, really
private static void WriteToRow(uint value, byte[] to, int row)
{
to[row] = (byte)(value & 255);
to[row + 4] = (byte)((value >> 8) & 255);
to[row + 8] = (byte)((value >> 16) & 255);
to[row + 12] = (byte)((value >> 24) & 255);
}
// Boring helper method
private static uint GetRow(byte[] from, int row) => (uint)(from[row] | (from[row + 4] << 8) | (from[row + 8] << 16) | (from[row + 12] << 24));
/// <summary>
/// MixColumns adds diffusion to the algorithm. Performs matrix multiplication under GF(2^8) with the irreducible prime 0x11B (x^8 + x^4 + x^3 + x + 1)
/// </summary>
/// <param name="state"></param>
/// <returns>A matrix-multiplied and limited state (mixed)</returns>
private static byte[] MixColumns(byte[] state, bool mix)
{
byte[] res = new byte[16];
byte[] rowGenerator = mix ? mix_matrix : unmix_matrix;
// Simplified matrix multiplication under GF(2^8)
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
for (int k = 0; k < 4; ++k)
{
int idx = 4 - j;
Galois2 g = Galois2.FromValue(state[k + i * 4]);
res[j + i * 4] ^= g.Multiply(Galois2.FromValue(rowGenerator[(k + idx) % 4])).ToByteArray()[0];
//int r = ((state[k + i * 4] * (mix_matrix[(k + idx) % 4] & 1)) ^ ((state[k + i * 4] << 1) * ((mix_matrix[(k + idx) % 4]>>1)&1)));
//if (r > 0b100011011) r ^= 0b100011011;
//res[j + i * 4] ^= (byte) r;
}
}
}
return res;
}
/// <summary>
/// Introduces the subkey for this round to the state
/// </summary>
/// <param name="state">The state to introduce the roundkey to</param>
/// <param name="subkey">The subkey</param>
/// <returns>The state where the roundkey has been added</returns>
private static byte[] AddRoundKey(byte[] state, byte[] subkey, int offset)
{
for (int i = 0; i < state.Length; ++i) state[i] ^= subkey[i + offset];
return state;
}
/// <summary>
/// Rotate bits to the left by 8 bits. This means that, for example, "0F AB 09 16" becomes "AB 09 16 0F"
/// </summary>
/// <param name="i"></param>
/// <returns>Rotated value</returns>
private static uint Rotate(uint i) => (uint)(((i >> 24) & 255) | ((i << 8) & ~255));
/// <summary>
/// KDF for a given input string.
/// </summary>
/// <param name="message">Input string to derive key from</param>
/// <returns>A key and an IV</returns>
private static Tuple<byte[], byte[]> DeriveKey(string message, string salt)
{
byte[] key = KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(message), salt.ToUTF8Bytes(), 4096, 16); // Generate a 16-byte (128-bit) key from salt over 4096 iterations of HMAC-SHA1
return new Tuple<byte[], byte[]>(key, salt.ToUTF8Bytes());
}
private static byte RCON(int i) => i <= 0 ? (byte)0x8d : new Galois2(i - 1).ToByteArray()[0];
}
// If you genuinely care what this does, to which I would under regular circumstances call you a nerd but alas I researched this, soooooo here:
// https://en.wikipedia.org/wiki/Finite_field_arithmetic
// http://www.cs.utsa.edu/~wagner/laws/FFM.html
// https://www.wolframalpha.com/examples/math/algebra/finite-fields/
// https://www.youtube.com/watch?v=x1v2tX4_dkQ
// That YouTube link explains it the best imo, but the others really help and solidify the concept.
// Essentially, if you're genuinely going to torture yourself by trying to learn this stuff, at least do yourself a favour and start with the video.
// Also, I'm not gonna comment the code down there because it's really annoying and it's late and I have a headache and ooooooooohhhhh I don't want to spend more time than I already have on that stuff.
// You get the comments that I put there when I made this; no more than that! Honestly, they're still plenty so just make do!
/// <summary>
/// Object representation of a Galois Field with characteristic 2
/// </summary>
public class Galois2
{
private static readonly byte[] ZERO = new byte[1] { 0 };
private static readonly byte[] ONE = new byte[1] { 1 };
public static byte[] RijndaelIP
{ get { return new byte[] { 0b0001_1011, 0b0000_0001 }; } }
protected readonly byte[] value;
protected readonly byte[] ip;
/// <summary>
/// Create a new Galois2 instance representing the given polynomial using the given irreducible polynomial. The given value will be reduced if possible
/// </summary>
/// <param name="value">Value to represent</param>
/// <param name="ip">Irreducible polynomial</param>
public Galois2(byte[] value, byte[] ip)
{
this.value = _ClipZeroes(_FieldMod(value, this.ip = ip));
}
public Galois2(int pow, byte[] ip) : this(_FlipBit(new byte[0], pow), ip)
{ }
public Galois2(byte[] value) : this(value, RijndaelIP)
{ }
public Galois2(int pow) : this(pow, RijndaelIP)
{ }
public static Galois2 FromValue(int value, byte[] ip) => new Galois2(Support.WriteToArray(new byte[4], value, 0), ip);
public static Galois2 FromValue(int value) => FromValue(value, Galois2.RijndaelIP);
public Galois2 Multiply(Galois2 factor) => new Galois2(_Mul(value, factor.value), ip);
public Galois2 Add(Galois2 val) => new Galois2(_Add(value, val.value), ip);
public Galois2 Subtract(Galois2 val) => new Galois2(_Sub(value, val.value), ip);
public Galois2 XOR(Galois2 val) => new Galois2(_XOR(value, val.value), ip);
/// <summary>
/// Perform inverse multiplication on this Galois2 object. This is done by performing the extended euclidean algorithm (two-variable linear diophantine equations).
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public Galois2 InvMul()
{
if (_ArraysEquals(value, ZERO)) return FromValue(0, ip);
Stack<byte[]> factors = new Stack<byte[]>();
byte[] val = value;
byte[] mod = ip;
ModResult res;
while (!_ArraysEquals((res = _Mod(val, mod)).rem, ZERO))
{
factors.Push(res.div);
val = mod;
mod = res.rem;
}
// Values are not coprime. There is no solution!
if (!_ArraysEquals(mod, ONE)) return new Galois2(new byte[0], ip);
byte[] useful = new byte[1] { 1 };
byte[] theOtherOne = factors.Pop();
byte[] tmp;
while (factors.Count > 0)
{
tmp = theOtherOne;
theOtherOne = _Add(useful, _Mul(theOtherOne, factors.Pop()));
useful = tmp;
}
return new Galois2(useful, ip);
}
public byte[] ToByteArray() => (byte[])value.Clone();
public override string ToString()
{
StringBuilder builder = new StringBuilder();
for (int i = _GetFirstSetBit(value); i >= 0; --i)
if (_BitAt(value, i))
builder.Append("x^").Append(i).Append(" + ");
if (builder.Length == 0) builder.Append("0 ");
else builder.Remove(builder.Length - 2, 2);
builder.Append("(mod ");
int j;
for(int i = j = _GetFirstSetBit(ip); i>=0; --i)
if (_BitAt(ip, i))
builder.Append("x^").Append(i).Append(" + ");
if (j == -1) builder.Append('0');
else builder.Remove(builder.Length - 3, 3);
return builder.Append(')').ToString();
}
// Overrides
public override bool Equals(object obj)
{
if (obj == null || !(obj is Galois2 || obj is byte[])) return false;
byte[] val = obj is Galois2 ? ((Galois2)obj).value : (byte[])obj;
bool cmp = val.Length > value.Length;
byte[] bigger = cmp ? val : value;
byte[] smaller = cmp ? value : val;
for (int i = bigger.Length - 1; i >= 0; --i)
if (i >= smaller.Length)
{
if (bigger[i] != 0) return false;
}
else if (bigger[i] != smaller[i]) return false;
// If the value supplied was a byte array, ignore the irreducible prime, otherwise, make sure the irreducible primes are the same
return obj is byte[] || ((Galois2)obj).ip.Equals(ip);
}
public override int GetHashCode()
{
var hashCode = -579181322;
hashCode = hashCode * -1521134295 + EqualityComparer<byte[]>.Default.GetHashCode(value);
hashCode = hashCode * -1521134295 + EqualityComparer<byte[]>.Default.GetHashCode(ip);
return hashCode;
}
// Just internal stuff from here on out boiiiiiiis!
protected static bool _ArraysEquals(byte[] v1, byte[] v2)
{
bool cmp = v1.Length > v2.Length;
byte[] bigger = cmp ? v1 : v2;
byte[] smaller = cmp ? v2 : v1;
for (int i = bigger.Length - 1; i >= 0; --i)
if (i >= smaller.Length)
{
if (bigger[i] != 0) return false;
}
else if (bigger[i] != smaller[i]) return false;
return true;
}
// Internal methods for certain calculations
protected static byte[] _FieldMod(byte[] applyTo, byte[] fieldIP)
{
byte[] CA_l;
int fsb = _GetFirstSetBit(fieldIP);
while (_GetFirstSetBit(applyTo) >= fsb) // In GF(2^8), polynomials may not exceed x^7. This means that a value containing a bit representing x^8 or higher is invalid
{
CA_l = _GetFirstSetBit(applyTo) >= _GetFirstSetBit(fieldIP) ? _Align((byte[])fieldIP.Clone(), applyTo) : fieldIP;
byte[] res = new byte[CA_l.Length];
for (int i = 0; i < CA_l.Length; ++i) res[i] = (byte)(applyTo[i] ^ CA_l[i]);
applyTo = _ClipZeroes(res);
}
return applyTo;
}
/// <summary>
/// Remove preceding zero-bytes
/// </summary>
/// <param name="val">Value to remove preceding zeroes from</param>
/// <returns>Truncated value (if truncation was necessary)</returns>
protected static byte[] _ClipZeroes(byte[] val)
{
int i = 0;
for (int j = val.Length - 1; j >= 0; --j) if (val[j] != 0) { i = j; break; }
byte[] res = new byte[i + 1];
Array.Copy(val, res, res.Length);
return res;
}
/// <summary>
/// Get the bit index of the highest bit. This will get the value of the exponent, i.e. index 8 represents x^8
/// </summary>
/// <param name="b">Value to get the highest set bit from</param>
/// <returns>Index of the highest set bit. -1 if no bits are set</returns>
protected static int _GetFirstSetBit(byte[] b)
{
for (int i = (b.Length * 8) - 1; i >= 0; --i)
if (b[i / 8] == 0) i -= i % 8; // Speeds up searches through blank bytes
else if ((b[i / 8] & (1 << (i % 8))) != 0)
return i;
return -1;
}
/// <summary>
/// Get the state of a bit in the supplied value.
/// </summary>
/// <param name="value">Value to get bit from</param>
/// <param name="index">Bit index to get bit from. (Not byte index)</param>
/// <returns></returns>
protected static bool _BitAt(byte[] value, int index) => (value[index / 8] & (1 << (index % 8))) != 0;
protected static byte _ShiftedBitmask(int start)
{
byte res = 0;
for (int i = start; i > 0; --i) res = (byte)((res >> 1) | 128);
return res;
}
protected static byte[] _Align(byte[] value, byte[] to) => _SHL(value, _GetFirstSetBit(to) - _GetFirstSetBit(value));
protected static bool _NeedsAlignment(byte[] value, byte[] comp) => _GetFirstSetBit(value) > _GetFirstSetBit(comp);
protected static bool _GT(byte[] v1, byte[] v2, bool eq)
{
byte[] bigger = v1.Length > v2.Length ? v1 : v2;
byte[] smaller = v1.Length > v2.Length ? v2 : v1;
for (int i = bigger.Length - 1; i >= 0; --i)
if (i >= smaller.Length && bigger[i] != 0)
return bigger == v1;
else if (i < smaller.Length && bigger[i] != smaller[i])
return (bigger[i] > smaller[i]) ^ (bigger != v1);
return eq;
}
/// <summary>
/// Shifts bit in the array by 'shift' bits to the left. This means that 0b0010_0000_1000_1111 shited by 2 becomes 0b1000_0010_0011_1100.
/// Note: A shift of 0 just acts like a slow value.Clone()
/// </summary>
/// <param name="value"></param>
/// <param name="shift"></param>
/// <returns></returns>
protected static byte[] _SHL(byte[] value, int shift)
{
int set = shift / 8;
int sub = shift % 8;
byte bm = _ShiftedBitmask(sub);
byte ibm = (byte)~bm;
byte carry = 0;
int fsb1 = _GetFirstSetBit(value);
if (fsb1 == -1) return value;
byte fsb = (byte)(fsb1 % 8);
byte[] create = new byte[value.Length + set + (fsb + sub >= 7 ? 1 : 0)];
for (int i = set; i - set < value.Length; ++i)
{
create[i] = (byte)(((value[i - set] & ibm) << sub) | carry);
carry = (byte)((value[i - set] & bm) >> (8 - sub));
}
create[create.Length - 1] |= carry;
return create;
}
/// <summary>
/// Flips the bit at the given binary index in the supplied value. For example, flipping bit 5 in the number 0b0010_0011 would result in 0b0000_0011, whereas flipping index 7 would result in 0b1010_0011.
/// </summary>
/// <param name="value">Value to manipulate bits of</param>
/// <param name="bitIndex">Index (in bits) of the bit to flip.</param>
/// <returns>An array (may be the same object as the one given) with a bit flipped.</returns>
protected static byte[] _FlipBit(byte[] value, int bitIndex)
{
if (bitIndex >= value.Length * 8)
{
byte[] intermediate = new byte[(bitIndex / 8) + 1];
Array.Copy(value, intermediate, value.Length);
value = intermediate;
}
value[bitIndex / 8] ^= (byte)(1 << (bitIndex % 8));
return value;
}
// Addition, Subtraction and XOR are all equivalent under GF(2^8) due to the modular nature of the field
protected static byte[] _Add(byte[] v1, byte[] v2) => _XOR(v1, v2);
protected static byte[] _Sub(byte[] v1, byte[] v2) => _XOR(v1, v2);
protected static byte[] _XOR(byte[] v1, byte[] v2)
{
bool size = v1.Length > v2.Length;
byte[] bigger = size ? v1 : v2;
byte[] smaller = size ? v2 : v1;
byte[] res = new byte[bigger.Length];
Array.Copy(bigger, res, bigger.Length);
for (int i = 0; i < smaller.Length; ++i) res[i] ^= smaller[i];
return _ClipZeroes(res);
}
/// <summary>
/// Perform polynomial multiplication under a galois field with characteristic 2
/// </summary>
/// <param name="value">Factor to multiply</param>
/// <param name="by">Factor to multiply other value by</param>
/// <returns>The product of the multiplication</returns>
protected static byte[] _Mul(byte[] value, byte[] by)
{
byte[] result = new byte[0];
for (int i = _GetFirstSetBit(by); i >= 0; --i)
if (_BitAt(by, i))
result = _Add(result, _SHL(value, i));
return result;
}
/// <summary>
/// Performs modulus on a given value by a certain value (mod) over a Galois Field with characteristic 2. This method performs both modulus and division.
/// </summary>
/// <param name="value">Value to perform modular aithmetic on</param>
/// <param name="mod">Modular value</param>
/// <returns>The result of the polynomial division and the result of the modulus</returns>
protected static ModResult _Mod(byte[] value, byte[] mod)
{
byte[] divRes = new byte[1];
while (_GT(value, mod, true))
{
divRes = _FlipBit(divRes, _GetFirstSetBit(value) - _GetFirstSetBit(mod)); // Notes the bit shift in the division tracker
value = _Sub(value, _Align(mod, value));
}
return new ModResult(divRes, value);
}
/// <summary>
/// Used to store the result of a polynomial division/modulus in GF(2^m)
/// </summary>
protected struct ModResult
{
public ModResult(byte[] div, byte[] rem)
{
this.div = div;
this.rem = rem;
}
public byte[] div;
public byte[] rem;
}
}
}

116
Common/AccountInfo.cs Normal file
View File

@ -0,0 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Tofvesson.Crypto;
namespace Common
{
public class User
{
public bool ProblematicTransactions { get; private set; }
public string Name { get; private set; }
public long Balance { get; set; }
public bool IsAdministrator { get; set; }
public string PasswordHash { get; private set; }
public string Salt { get; private set; }
public List<Transaction> History { get; }
private User()
{
Name = "";
History = new List<Transaction>();
}
public User(string name, string passHash, string salt, long balance, bool generatePass = false, List<Transaction> transactionHistory = null, bool admin = false)
: this(name, passHash, Encoding.UTF8.GetBytes(salt), balance, generatePass, transactionHistory, admin)
{ }
public User(string name, string passHash, byte[] salt, long balance, bool generatePass = false, List<Transaction> transactionHistory = null, bool admin = false)
{
History = transactionHistory ?? new List<Transaction>();
Balance = balance;
Name = name;
IsAdministrator = admin;
Salt = Convert.ToBase64String(salt);
PasswordHash = generatePass ? Convert.ToBase64String(KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(passHash), Encoding.UTF8.GetBytes(Salt), 8192, 320)) : passHash;
}
public bool Authenticate(string password)
=> Convert.ToBase64String(KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(Salt), 8192, 320)).Equals(PasswordHash);
public User AddTransaction(Transaction tx)
{
History.Add(tx);
return this;
}
public Transaction CreateTransaction(User recipient, long amount, string message = null) => new Transaction(this.Name, recipient.Name, amount, message);
public override bool Equals(object obj) => obj is User && ((User)obj).Name.Equals(Name);
public override int GetHashCode()
=> 539060726 + EqualityComparer<string>.Default.GetHashCode(Name);
/*
public string Serialize()
{
}
public static User Deserialize(string ser)
{
}
*/
}
public class Transaction
{
public string from;
public string to;
public long amount;
public string meta;
public Transaction(string from, string to, long amount, string meta)
{
this.from = from;
this.to = to;
this.amount = amount;
this.meta = meta;
}
public string Serialize()
{
XmlDocument doc = new XmlDocument();
XmlElement el = doc.CreateElement("Transaction");
XmlElement to = doc.CreateElement("To");
to.InnerText = this.to;
XmlElement from = doc.CreateElement("From");
from.InnerText = this.from;
XmlElement amount = doc.CreateElement("Balance");
amount.InnerText = amount.ToString();
el.AppendChild(to).AppendChild(from).AppendChild(amount);
if (meta != null)
{
XmlElement msg = doc.CreateElement("Meta");
msg.InnerText = meta;
el.AppendChild(msg);
}
return el.ToString();
}
public static Transaction Deserialize(string ser)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(ser);
return null;
}
}
}

6
Common/App.config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
</configuration>

227
Common/CBC.cs Normal file
View File

@ -0,0 +1,227 @@
using System;
using System.Linq;
using System.Reflection;
namespace Tofvesson.Crypto
{
// Specifies the basic structure of a block cipher
public abstract class BlockCipher
{
public Int32 BlockSize { get; private set; }
public BlockCipher(int blockSize) { this.BlockSize = blockSize; }
public abstract byte[] Encrypt(byte[] message);
public abstract byte[] Decrypt(byte[] ciphertext);
}
// Base structure of a cipher block chaining algorithm
// For an in-depth explanation of what this is (as well as visuals of the implementations that are a bit futher down), visit https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
public abstract class GenericCBC : BlockCipher
{
private static readonly byte[] splitter = ":".ToUTF8Bytes();
private readonly byte[] iv_e;
public byte[] IV { get => (byte[])iv_e.Clone(); }
protected readonly byte[] currentIV_e;
protected readonly byte[] currentIV_d;
protected readonly BlockCipher cipher;
public GenericCBC(BlockCipher cipher, RandomProvider provider) : base(cipher.BlockSize)
{
this.cipher = cipher;
// Generate initialization vector and set it as the current iv
iv_e = provider.GetBytes(new byte[cipher.BlockSize]);
currentIV_e = new byte[cipher.BlockSize];
currentIV_d = new byte[cipher.BlockSize];
Array.Copy(iv_e, currentIV_e, iv_e.Length);
Array.Copy(iv_e, currentIV_d, iv_e.Length);
}
public GenericCBC(BlockCipher cipher, byte[] iv_e) : base(cipher.BlockSize)
{
this.iv_e = iv_e;
this.cipher = cipher;
currentIV_e = new byte[cipher.BlockSize];
currentIV_d = new byte[cipher.BlockSize];
Array.Copy(iv_e, currentIV_e, iv_e.Length);
Array.Copy(iv_e, currentIV_d, iv_e.Length);
}
// Separate a given messae into blocks for processing
protected byte[][] SplitBlocks(byte[] message)
{
byte[][] blocks = new byte[(message.Length / cipher.BlockSize) + (message.Length % cipher.BlockSize == 0 ? 0 : 1)][];
for (int i = 0; i < blocks.Length; ++i)
{
blocks[i] = message.SubArray(i * cipher.BlockSize, Math.Min((i + 1) * cipher.BlockSize, message.Length));
if (blocks[i].Length != cipher.BlockSize)
{
byte[] res = new byte[cipher.BlockSize];
Array.Copy(blocks[i], res, blocks[i].Length);
blocks[i] = res;
}
}
return blocks;
}
// Recombine blocks that have been split back into a single string of bytes
protected byte[] CollectBlocks(byte[][] result)
{
byte[] collected = new byte[result.Length * cipher.BlockSize];
for (int i = 0; i < result.Length; ++i) Array.Copy(result[i], 0, collected, cipher.BlockSize * i, cipher.BlockSize);
return collected;
}
// Resets the state of this CBC instance
public virtual void Reset()
{
Array.Copy(iv_e, currentIV_e, iv_e.Length);
Array.Copy(iv_e, currentIV_d, iv_e.Length);
}
}
/// <summary>
/// Standard cipher block chaining implementation (not recommended, but available nonetheless)
/// </summary>
public sealed class CBC : GenericCBC
{
public CBC(BlockCipher cipher, RandomProvider provider) : base(cipher, provider)
{ }
public CBC(BlockCipher cipher, byte[] iv_e) : base(cipher, iv_e)
{ }
// This entire method is pretty self-explanatory. All you need to know is: currentIV_e represents the IV currently being used for encryption
public override byte[] Encrypt(byte[] message)
{
byte[][] blocks = SplitBlocks(message);
for (int i = 0; i < blocks.Length; ++i)
{
byte[] enc_result = cipher.Encrypt(blocks[i].XOR(currentIV_e));
Array.Copy(enc_result, currentIV_e, enc_result.Length);
Array.Copy(enc_result, blocks[i], cipher.BlockSize);
}
return CollectBlocks(blocks);
}
public override byte[] Decrypt(byte[] ciphertext)
{
// Split ciphertext into encrypted blocks
byte[][] blocks = SplitBlocks(ciphertext);
for(int i = 0; i<blocks.Length; ++i)
{
// Decrypt block
Array.Copy(cipher.Decrypt(blocks[i]).XOR(currentIV_d), blocks[i], cipher.BlockSize);
// Set the next iv to be this iteration's decrypted block
Array.Copy(blocks[i], currentIV_d, cipher.BlockSize);
}
return CollectBlocks(blocks);
}
}
// Propogating CBC
public sealed class PCBC : GenericCBC
{
public PCBC(BlockCipher cipher, RandomProvider provider) : base(cipher, provider)
{ }
public PCBC(BlockCipher cipher, byte[] iv_e) : base(cipher, iv_e)
{ }
public override byte[] Encrypt(byte[] message)
{
byte[][] blocks = SplitBlocks(message);
for (int i = 0; i < blocks.Length; ++i)
{
// Store the unmodified input text block
byte[] before = (byte[])blocks[i].Clone();
// Compute the encrypted value for this block
byte[] enc_result = cipher.Encrypt(blocks[i].XOR(currentIV_e));
// Store/compute new IV
Array.Copy(enc_result, currentIV_e, enc_result.Length);
currentIV_e.XOR(before);
// Store encryption result
Array.Copy(enc_result, 0, blocks[i], i * 16, 16);
}
return CollectBlocks(blocks);
}
public override byte[] Decrypt(byte[] ciphertext)
{
// Split ciphertext into blocks (ciphertext should ahve a length that is a multiple of cipher.BlockSize)
byte[][] blocks = SplitBlocks(ciphertext);
// Decrypt each block
for (int i = 0; i < blocks.Length; ++i)
{
// Temporarily store a copy of the encrypted block
byte[] before = (byte[])blocks[i].Clone();
// Decrypt the block
Array.Copy(cipher.Decrypt(before).XOR(currentIV_d), blocks[i], cipher.BlockSize);
// Compute the next IV
Array.Copy(currentIV_d, before.XOR(blocks[i]), cipher.BlockSize);
}
return CollectBlocks(blocks);
}
}
public sealed class CFB : GenericCBC
{
public CFB(BlockCipher cipher, RandomProvider provider) : base(cipher, provider)
{ }
public CFB(BlockCipher cipher, byte[] iv_e) : base(cipher, iv_e)
{ }
public override byte[] Encrypt(byte[] message)
{
byte[][] blocks = SplitBlocks(message);
for (int i = 0; i < blocks.Length; ++i)
// Encrypt IV and compute the XOR of the result with the plaintext. Finally, set the ciphertext as the IV for the next iteration
Array.Copy(blocks[i] = blocks[i].XOR(cipher.Encrypt(currentIV_e)), currentIV_e, cipher.BlockSize);
return CollectBlocks(blocks);
}
public override byte[] Decrypt(byte[] ciphertext)
{
// Split ciphertext into blocks (ciphertext should ahve a length that is a multiple of cipher.BlockSize)
byte[][] blocks = SplitBlocks(ciphertext);
for (int i = 0; i < blocks.Length; ++i)
{
// Store unmodified copy
byte[] before = (byte[])blocks[i].Clone();
// Decrypt
blocks[i] = blocks[i].XOR(cipher.Encrypt(currentIV_d));
// Set the ciphertext a the iv for the next iteration
Array.Copy(before, currentIV_d, cipher.BlockSize);
}
return CollectBlocks(blocks);
}
}
}

253
Common/Collections.cs Normal file
View File

@ -0,0 +1,253 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Collections.ObjectModel;
namespace Tofvesson.Collections
{
public class BoundedList<T> : IEnumerable<T>
{
protected const float GROW_FACTOR = 1.75f;
protected const float SHRINK_FACTOR = 1.25f;
protected readonly int maxCapacity;
protected T[] values;
public int Count { get; private set; }
public T this[int i]
{
get => ElementAt(i);
set
{
DoRangeCheck(i);
values[i] = value;
}
}
public BoundedList(int maxCapacity = -1, int initialCapacity = 10)
{
this.maxCapacity = maxCapacity < 0 ? -1 : maxCapacity;
values = new T[maxCapacity == -1 ? Max(initialCapacity, 0) : Min(maxCapacity, Max(initialCapacity, 0))];
}
private static int Min(int i1, int i2) => i1 > i2 ? i2 : i1;
private static int Max(int i1, int i2) => i1 > i2 ? i1 : i2;
public BoundedList(int maxCapacity, IEnumerable<T> collection) : this(maxCapacity, collection.Count())
{
int track = 0;
IEnumerator<T> enumerator = collection.GetEnumerator();
while(enumerator.MoveNext() && track < maxCapacity)
{
Add(enumerator.Current);
++track;
}
}
public virtual bool Add(T t)
{
if (Count == maxCapacity) return false;
if (Count == values.Length) Resize(Count * GROW_FACTOR);
values[Count] = t;
++Count;
return true;
}
public virtual bool Remove(T t) => RemoveIf(t1 => (t == null && t1 == null) || (t != null && t.Equals(t1))) > 0;
public int RemoveIf(Predicate<T> p)
{
int removed = 0;
for (int c = 0; c < Count; ++c)
if (p(values[c]))
{
_RemoveAt(c);
++removed;
}
if (values.Length >= Count * SHRINK_FACTOR) Resize(Count * SHRINK_FACTOR);
return removed;
}
public virtual void RemoveAt(int i)
{
_RemoveAt(i);
if (values.Length >= Count * SHRINK_FACTOR) Resize(Count * SHRINK_FACTOR);
}
public virtual T ElementAt(int i)
{
DoRangeCheck(i);
return values[i];
}
public virtual T[] ToArray()
{
T[] t = new T[Count];
Array.Copy(values, t, Count);
return t;
}
protected virtual void _RemoveAt(int i)
{
DoRangeCheck(i);
for (int j = i + 1; j < Count; ++j) values[j - 1] = values[j];
values[Count - 1] = default(T); // Don't keep references in case GC needs to claim the object
--Count;
}
protected virtual void Resize(float targetSize_f)
{
int targetSize = maxCapacity == -1 ? Math.Max((int)Math.Round(targetSize_f), 0) : Math.Max(0, Math.Min((int)Math.Round(targetSize_f), maxCapacity));
T[] surrogate = new T[targetSize];
Array.Copy(values, surrogate, Math.Min(targetSize, Count));
values = surrogate;
}
protected void DoRangeCheck(int i)
{
if (i < 0 || i >= Count) throw new IndexOutOfRangeException();
}
public virtual IEnumerator<T> GetEnumerator() => new BoundedListEnumerator<T>(this);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
private sealed class BoundedListEnumerator<K> : IEnumerator<K>
{
public K Current => list.values[current];
object IEnumerator.Current => Current;
private readonly BoundedList<K> list;
private int current = -1;
public BoundedListEnumerator(BoundedList<K> list)
{
this.list = list;
}
public void Dispose() { }
public bool MoveNext() => ++current < list.Count;
public void Reset() => current = 0;
}
}
public class EvictionList<T> : BoundedList<T>
{
public EvictionList(int maxCapacity = -1, int initialCapacity = 10) : base(maxCapacity, initialCapacity)
{ }
public override bool Add(T t)
{
if (Count == maxCapacity) RemoveAt(0);
return base.Add(t);
}
}
public static class Collections
{
public static M Get<M, K, T>(this Dictionary<K, T> dict, K key)
{
object d;
if (dict.ContainsKey(key) && dict[key] != null && dict[key] is M) d = dict[key];
else d = default(M);
return (M)d;
}
public static Dictionary<K, T> Replace<K, T>(this Dictionary<K, T> dict, K key, T replace)
{
dict[key] = replace;
return dict;
}
public static int CollectiveLength(this Tuple<string, string>[] values, bool first)
{
int len = 0;
foreach (var val in values)
len += (first ? val.Item1 : val.Item2)?.Length ?? 0;
return len;
}
public static T[] Collect<T>(this Tuple<T, T>[] values, bool first)
{
T[] collect = new T[values.Length];
for (int i = 0; i < values.Length; ++i) collect[i] = (first ? values[i].Item1 : values[i].Item2);
return collect;
}
public static V GetNamed<T, V>(this List<Tuple<T, V>> list, T key)
{
foreach (var element in list)
if (element != null && ObjectEquals(key, element.Item1))
return element.Item2;
return default(V);
}
public static V GetNamed<T, V>(this ReadOnlyCollection<Tuple<T, V>> list, T key)
{
foreach (var element in list)
if (element != null && ObjectEquals(key, element.Item1))
return element.Item2;
return default(V);
}
public static V GetFirst<V>(this List<V> v, Predicate<V> p)
{
foreach (var v1 in v)
if (p(v1))
return v1;
return default(V);
}
public static bool ContainsExactly(this string s, char c, int count = 1)
{
int ctr = 0;
for (int i = 0; i < s.Length; ++i)
if (s[i] == c && ++ctr > count)
return false;
return ctr == count;
}
public static bool ObjectEquals(object o1, object o2) => (o1==null && o2==null) || (o1!=null && o1.Equals(o2));
public static int CollectiveLength<T>(this Tuple<string, T>[] t, bool redundant = true)
{
int len = 0;
foreach (var val in t)
len += val?.Item1?.Length ?? 0;
return len;
}
public static int CollectiveLength<T>(this Tuple<T, string>[] t)
{
int len = 0;
foreach (var val in t)
len += val?.Item2?.Length ?? 0;
return len;
}
public static List<T> Filter<T>(this List<T> t, Predicate<T> p)
{
List<T> l1 = new List<T>();
foreach (var l in t)
if (p(l))
l1.Add(l);
return l1;
}
public delegate T Transformation<T, V>(V v);
public static List<T> Transform<T, V>(this List<V> l, Transformation<T, V> t)
{
List<T> l1 = new List<T>();
foreach (var l2 in l)
l1.Add(t(l2));
return l1;
}
public static T[] Transform<T, V>(this V[] l, Transformation<T, V> t)
{
T[] l1 = new T[l.Length];
for (int i = 0; i < l.Length; ++i)
l1[i] = t(l[i]);
return l1;
}
}
}

94
Common/Common.csproj Normal file
View File

@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{23EB87D4-E310-48C4-A931-0961C83892D7}</ProjectGuid>
<OutputType>Library</OutputType>
<RootNamespace>Common</RootNamespace>
<AssemblyName>Common</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject />
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Numerics" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="AccountInfo.cs" />
<Compile Include="AES.cs" />
<Compile Include="CBC.cs" />
<Compile Include="Collections.cs" />
<Compile Include="KDF.cs" />
<Compile Include="Net.cs" />
<Compile Include="Padding.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RSA.cs" />
<Compile Include="SHA.cs" />
<Compile Include="Streams.cs" />
<Compile Include="Support.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.6.1">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.6.1 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

13
Common/Common.csproj.user Normal file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishUrlHistory>publish\</PublishUrlHistory>
<InstallUrlHistory />
<SupportUrlHistory />
<UpdateUrlHistory />
<BootstrapperUrlHistory />
<ErrorReportUrlHistory />
<FallbackCulture>en-US</FallbackCulture>
<VerifyUploadedFiles>false</VerifyUploadedFiles>
</PropertyGroup>
</Project>

72
Common/KDF.cs Normal file
View File

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Tofvesson.Crypto
{
// Class for key derivation
public static class KDF
{
public delegate byte[] HashFunction(byte[] message);
// Hash-based Message Authentication Codes: generates a code for verifying the sender of a message and the like
public static byte[] HMAC(byte[] key, byte[] message, HashFunction func, int blockSizeBytes)
{
if (key.Length > blockSizeBytes) key = func(key);
else if (key.Length < blockSizeBytes)
{
byte[] b = new byte[blockSizeBytes];
Array.Copy(key, b, key.Length);
key = b;
}
byte[] o_key_pad = new byte[blockSizeBytes]; // Outer padding
byte[] i_key_pad = new byte[blockSizeBytes]; // Inner padding
for (int i = 0; i < blockSizeBytes; ++i)
{
// Combine padding with key
o_key_pad[i] = (byte)(key[i] ^ 0x5c);
i_key_pad[i] = (byte)(key[i] ^ 0x36);
}
return func(Support.Concatenate(o_key_pad, func(Support.Concatenate(message, i_key_pad))));
}
// Perform HMAC using SHA1
public static byte[] HMAC_SHA1(byte[] key, byte[] message) => HMAC(key, message, SHA.SHA1, 20);
// Pseudorandom function delegate
public delegate byte[] PRF(byte[] key, byte[] salt);
// Password-Based Key Derivation Function 2. Used to generate "pseudorandom" keys from a given password and salt using a certain PRF applied a certain amount of times (iterations).
// dklen specified the "derived key length" in bytes. It is recommended to use a high number for the iterations variable (somewhere around 4096 is the standard for SHA1 currently)
public static byte[] PBKDF2(PRF function, byte[] password, byte[] salt, int iterations, int dklen)
{
byte[] dk = new byte[0]; // Create a placeholder for the derived key
uint iter = 1; // Track the iterations
while (dk.Length < dklen)
{
// F-function
// The F-function (PRF) takes the amount of iterations performed in the opposite endianness format from what C# uses, so we have to swap the endianness
byte[] u = function(password, Support.Concatenate(salt, Support.WriteToArray(new byte[4], Support.SwapEndian(iter), 0)));
byte[] ures = new byte[u.Length];
Array.Copy(u, ures, u.Length);
for(int i = 1; i<iterations; ++i)
{
// Iteratively apply the PRF
u = function(password, u);
for (int j = 0; j < u.Length; ++j) ures[j] ^= u[j];
}
// Concatenate the result to the dk
dk = Support.Concatenate(dk, ures);
++iter;
}
// Clip aby bytes past what we needed (yes, that's really what the standard is)
return dk.ToLength(dklen);
}
}
}

505
Common/Net.cs Normal file
View File

@ -0,0 +1,505 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Tofvesson.Crypto
{
public delegate string OnMessageRecieved(string request, Dictionary<string, string> associations, ref bool stayAlive);
public delegate void OnClientConnectStateChanged(NetClient client, bool connect);
public sealed class NetServer
{
private readonly short port;
private readonly object state_lock = new object();
private readonly List<ClientStateObject> clients = new List<ClientStateObject>();
private readonly OnMessageRecieved callback;
private readonly OnClientConnectStateChanged onConn;
private readonly IPAddress ipAddress;
private Socket listener;
private readonly RSA crypto;
private readonly byte[] ser_cache;
private readonly int bufSize;
private bool state_running = false;
private Thread listenerThread;
public int Count
{
get
{
return clients.Count;
}
}
public bool Running
{
get
{
lock (state_lock) return state_running;
}
private set
{
lock (state_lock) state_running = value;
}
}
public NetServer(RSA crypto, short port, OnMessageRecieved callback, OnClientConnectStateChanged onConn, int bufSize = 16384)
{
this.callback = callback;
this.onConn = onConn;
this.bufSize = bufSize;
this.crypto = crypto;
this.port = port;
this.ser_cache = crypto.Serialize(); // Keep this here so we don't wastefully re-serialize every time we get a new client
IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
this.ipAddress = ipHostInfo.GetIPV4();
if (ipAddress == null)
ipAddress = IPAddress.Parse("127.0.0.1"); // If there was no IPv4 result in dns lookup, use loopback address
}
public void StartListening()
{
bool isAlive = false;
object lock_await = new object();
if(!Running && (listenerThread==null || !listenerThread.IsAlive))
{
Running = true;
listenerThread = new Thread(() =>
{
this.listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp)
{
Blocking = false // When calling Accept() with no queued sockets, listener throws an exception
};
IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);
listener.Bind(localEndPoint);
listener.Listen(100);
byte[] buffer = new byte[bufSize];
lock (lock_await) isAlive = true;
Stopwatch limiter = new Stopwatch();
while (Running)
{
limiter.Start();
// Accept clients
try
{
Socket s = listener.Accept();
s.Blocking = false;
clients.Add(new ClientStateObject(new NetClient(s, crypto, callback, onConn), buffer));
}
catch (Exception)
{
if(clients.Count==0)
Thread.Sleep(25); // Wait a bit before trying to accept another client
}
// Update clients
foreach (ClientStateObject cli in clients.ToArray())
// Ensure we are still connected to client
if (!(cli.IsConnected() && !cli.Update()))
{
cli.client.onConn(cli.client, false);
clients.Remove(cli);
continue;
}
limiter.Stop();
if (limiter.ElapsedMilliseconds < 125) Thread.Sleep(250); // If loading data wasn't heavy, take a break
limiter.Reset();
}
})
{
Priority = ThreadPriority.Highest,
Name = $"NetServer-${port}"
};
listenerThread.Start();
}
bool rd;
do
{
Thread.Sleep(25);
lock (lock_await) rd = isAlive;
} while (!rd);
}
public Task<object> StopRunning()
{
Running = false;
return new TaskFactory().StartNew<object>(() =>
{
listenerThread.Join();
return null;
});
}
private class ClientStateObject
{
internal NetClient client;
private bool hasCrypto = false; // Whether or not encrypted communication has been etablished
private Queue<byte> buffer = new Queue<byte>(); // Incoming data buffer
private int expectedSize = 0; // Expected size of next message
private readonly byte[] buf;
public ClientStateObject(NetClient client, byte[] buf)
{
this.client = client;
this.buf = buf;
}
public bool Update()
{
bool stop = client.SyncListener(ref hasCrypto, ref expectedSize, out bool read, buffer, buf);
return stop;
}
public bool IsConnected() => client.IsConnected;
}
}
public class NetClient
{
private static readonly RandomProvider rp = new CryptoRandomProvider();
// Thread state lock for primitive values
private readonly object state_lock = new object();
// Primitive state values
private bool state_running = false;
// Socket event listener
private Thread eventListener;
// Communication parameters
protected readonly Queue<byte[]> messageBuffer = new Queue<byte[]>();
public readonly Dictionary<string, string> assignedValues = new Dictionary<string, string>();
protected readonly OnMessageRecieved handler;
protected internal readonly OnClientConnectStateChanged onConn;
protected readonly IPAddress target;
protected readonly int bufSize;
protected readonly RSA decrypt;
protected internal long lastComm = DateTime.Now.Ticks; // Latest comunication event (in ticks)
public RSA RemoteCrypto { get => decrypt; }
// Connection to peer
protected Socket Connection { get; private set; }
// State/connection parameters
protected Rijndael128 Crypto { get; private set; }
protected GenericCBC CBC { get; private set; }
public short Port { get; }
protected bool Running
{
get
{
lock (state_lock) return state_running;
}
private set
{
lock (state_lock) state_running = value;
}
}
protected internal bool IsConnected
{
get
{
return Connection != null && Connection.Connected && !(Connection.Poll(1, SelectMode.SelectRead) && Connection.Available == 0);
}
}
public bool IsAlive
{
get
{
return Running || (Connection != null && Connection.Connected) || (eventListener != null && eventListener.IsAlive);
}
}
protected bool ServerSide { get; private set; }
public NetClient(Rijndael128 crypto, IPAddress target, short port, OnMessageRecieved handler, OnClientConnectStateChanged onConn, int bufSize = 16384)
{
#pragma warning disable CS0618 // Type or member is obsolete
if (target.AddressFamily==AddressFamily.InterNetwork && target.Address == 16777343)
#pragma warning restore CS0618 // Type or member is obsolete
{
IPAddress addr = Dns.GetHostEntry(Dns.GetHostName()).GetIPV4();
if (addr != null) target = addr;
}
this.target = target;
Crypto = crypto;
if(crypto!=null) CBC = new PCBC(crypto, rp);
this.bufSize = bufSize;
this.handler = handler;
this.onConn = onConn;
Port = port;
ServerSide = false;
}
internal NetClient(Socket sock, RSA crypto, OnMessageRecieved handler, OnClientConnectStateChanged onConn)
: this(null, ((IPEndPoint)sock.RemoteEndPoint).Address, (short) ((IPEndPoint)sock.RemoteEndPoint).Port, handler, onConn, -1)
{
decrypt = crypto;
Connection = sock;
Running = true;
ServerSide = true;
// Initiate crypto-handshake by sending public keys
Connection.Send(NetSupport.WithHeader(crypto.Serialize()));
}
public virtual void Connect()
{
if (ServerSide) throw new SystemException("Serverside socket cannot connect to a remote peer!");
NetSupport.DoStateCheck(IsAlive || (eventListener != null && eventListener.IsAlive), false);
Connection = new Socket(SocketType.Stream, ProtocolType.Tcp);
Connection.Connect(target, Port);
Running = true;
eventListener = new Thread(() =>
{
bool cryptoEstablished = false;
int mLen = 0;
Queue<byte> ibuf = new Queue<byte>();
byte[] buffer = new byte[bufSize];
Stopwatch limiter = new Stopwatch();
while (Running)
{
limiter.Start();
if (SyncListener(ref cryptoEstablished, ref mLen, out bool _, ibuf, buffer))
break;
if (cryptoEstablished && DateTime.Now.Ticks >= lastComm + (5 * TimeSpan.TicksPerSecond))
try
{
Connection.Send(NetSupport.WithHeader(new byte[0])); // Send a test packet. (Will just send an empty header to the peer)
lastComm = DateTime.Now.Ticks;
}
catch
{
break; // Connection died
}
limiter.Stop();
if (limiter.ElapsedMilliseconds < 125) Thread.Sleep(250); // If loading data wasn't heavy, take a break
limiter.Reset();
}
if (ibuf.Count != 0) Debug.WriteLine("Client socket closed with unread data!");
onConn(this, false);
})
{
Priority = ThreadPriority.Highest,
Name = $"NetClient-${target}:${Port}"
};
eventListener.Start();
}
protected internal bool SyncListener(ref bool cryptoEstablished, ref int mLen, out bool acceptedData, Queue<byte> ibuf, byte[] buffer)
{
if (cryptoEstablished)
{
lock (messageBuffer)
{
foreach (byte[] message in messageBuffer) Connection.Send(NetSupport.WithHeader(message));
if(messageBuffer.Count > 0) lastComm = DateTime.Now.Ticks;
messageBuffer.Clear();
}
}
if (acceptedData = Connection.Available > 0)
{
int read = Connection.Receive(buffer);
ibuf.EnqueueAll(buffer, 0, read);
if (read > 0) lastComm = DateTime.Now.Ticks;
}
if (mLen == 0 && ibuf.Count >= 4)
mLen = Support.ReadInt(ibuf.Dequeue(4), 0);
if (mLen != 0 && ibuf.Count >= mLen)
{
// Got a full message. Parse!
byte[] message = ibuf.Dequeue(mLen);
lastComm = DateTime.Now.Ticks;
if (!cryptoEstablished)
{
if (ServerSide)
{
var nonceText = new string(Encoding.UTF8.GetChars(message));
byte[] sign;
if(nonceText.StartsWith("Nonce:") && BigInteger.TryParse(nonceText.Substring(6), out BigInteger parse) && (sign=parse.ToByteArray()).Length <= 512)
{
Connection.Send(NetSupport.WithHeader(decrypt.Encrypt(parse.ToByteArray(), null, true)));
Disconnect();
return true;
}
if (Crypto == null)
{
byte[] m = decrypt.Decrypt(message);
if (m.Length == 0) return false;
Crypto = Rijndael128.Deserialize(m, out int _);
}
else
{
byte[] m = decrypt.Decrypt(message);
if (m.Length == 0) return false;
CBC = new PCBC(Crypto, m);
onConn(this, true);
}
}
else
{
// Reconstruct RSA object from remote public keys and use it to encrypt our serialized AES key/iv
RSA asymm = RSA.Deserialize(message, out int _);
Connection.Send(NetSupport.WithHeader(asymm.Encrypt(Crypto.Serialize())));
Connection.Send(NetSupport.WithHeader(asymm.Encrypt(CBC.IV)));
onConn(this, true);
}
if (CBC != null)
cryptoEstablished = true;
}
else
{
// Decrypt the incoming message
byte[] read = Crypto.Decrypt(message);
// Read the decrypted message length
int mlenInner = Support.ReadInt(read, 0);
if (mlenInner == 0) return false; // Got a ping packet
// Send the message to the handler and get a response
bool live = true;
string response = handler(read.SubArray(4, 4+mlenInner).ToUTF8String(), assignedValues, ref live);
// Send the response (if given one) and drop the connection if the handler tells us to
if (response != null) Connection.Send(NetSupport.WithHeader(Crypto.Encrypt(NetSupport.WithHeader(response.ToUTF8Bytes()))));
if (!live)
{
Running = false;
try
{
Connection.Close();
}
catch (Exception) { }
return true;
}
}
// Reset expexted message length
mLen = 0;
}
return false;
}
/// <summary>
/// Disconnect from server
/// </summary>
/// <returns></returns>
public virtual async Task<object> Disconnect()
{
NetSupport.DoStateCheck(IsAlive, true);
Running = false;
return await new TaskFactory().StartNew<object>(() => { eventListener.Join(); return null; });
}
// Methods for sending data to the server
public bool TrySend(string message) => TrySend(Encoding.UTF8.GetBytes(message));
public bool TrySend(byte[] message)
{
try
{
Send(message);
return true;
}
catch (InvalidOperationException) { return false; }
}
public virtual void Send(string message) => Send(Encoding.UTF8.GetBytes(message));
public virtual void Send(byte[] message) {
NetSupport.DoStateCheck(IsAlive, true);
lock (messageBuffer) messageBuffer.Enqueue(Crypto.Encrypt(NetSupport.WithHeader(message)));
}
public static RSA CheckServerIdentity(string host, short port, RandomProvider provider, long timeout = 10000)
{
Socket sock = new Socket(SocketType.Stream, ProtocolType.Tcp)
{
ReceiveTimeout = 5000,
SendTimeout = 5000
};
sock.Blocking = false;
sock.Connect(host, port);
List<byte> read = new List<byte>();
byte[] buf = new byte[1024];
if (!Read(sock, read, buf, timeout)) return null;
read.RemoveRange(0, 4);
RSA remote;
try
{
remote = RSA.Deserialize(read.ToArray(), out int _);
}
catch { return null; }
BigInteger cmp;
sock.Send(NetSupport.WithHeader(Encoding.UTF8.GetBytes("Nonce:"+(cmp=BigInteger.Abs(new BigInteger(provider.GetBytes(128)))))));
Thread.Sleep(250); // Give the server ample time to compute the signature
read.Clear();
if (!Read(sock, read, buf, timeout)) return null;
read.RemoveRange(0, 4);
try
{
if (!cmp.Equals(new BigInteger(remote.Encrypt(read.ToArray())))) return null;
}
catch { return null; }
return remote; // Passed signature check
}
private static bool Read(Socket sock, List<byte> read, byte[] buf, long timeout)
{
Stopwatch sw = new Stopwatch();
int len = -1;
sw.Start();
while ((len == -1 || read.Count < 4) && (sw.ElapsedTicks / 10000) < timeout)
{
if (len == -1 && read.Count > 4)
len = Support.ReadInt(read, 0);
try
{
int r = sock.Receive(buf);
read.AddRange(buf.SubArray(0, r));
}
catch { }
}
sw.Stop();
return read.Count - 4 == len && len>0;
}
}
// Helper methods. WithHeader() should really just be in Support.cs
public static class NetSupport
{
public static byte[] WithHeader(string message) => WithHeader(Encoding.UTF8.GetBytes(message));
public static byte[] WithHeader(byte[] message)
{
byte[] nmsg = new byte[message.Length + 4];
Support.WriteToArray(nmsg, message.Length, 0);
Array.Copy(message, 0, nmsg, 4, message.Length);
return nmsg;
}
public static byte[] FromHeaded(byte[] msg, int offset) => msg.SubArray(offset + 4, offset + 4 + Support.ReadInt(msg, offset));
internal static void DoStateCheck(bool state, bool target) {
if (state != target) throw new InvalidOperationException("Bad state!");
}
}
}

376
Common/Padding.cs Normal file
View File

@ -0,0 +1,376 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
namespace Tofvesson.Crypto
{
public interface CryptoPadding
{
byte[] Pad(byte[] message);
byte[] Unpad(byte[] message);
PaddingIdentifier GetParameters();
}
public sealed class PaddingIdentifier
{
private readonly Dictionary<string, Tuple<ParameterTypes, string>> attributes = new Dictionary<string, Tuple<ParameterTypes, string>>();
private readonly Dictionary<string, PaddingIdentifier> nests = new Dictionary<string, PaddingIdentifier>();
public string Name { get; private set; }
public List<string> AttributeKeys
{
get
{
List<string> keys = new List<string>();
keys.AddRange(attributes.Keys);
return keys;
}
}
public List<string> NestedKeys
{
get
{
List<string> keys = new List<string>();
keys.AddRange(nests.Keys);
return keys;
}
}
public PaddingIdentifier(string name) { this.Name = name; }
public void AddAttribute(string attr, byte[] data) => attributes.Add(attr, new Tuple<ParameterTypes, string>(ParameterTypes.BYTES, Support.ArrayToString(data)));
public void AddAttribute(string attr, int data) => attributes.Add(attr, new Tuple<ParameterTypes, string>(ParameterTypes.NUMBER, data.ToString()));
public void AddAttribute(string attr, PaddingIdentifier data) => nests.Add(attr, data);
public Tuple<ParameterTypes, string> GetAttribute(string key)
{
if (attributes.ContainsKey(key)) return attributes[key];
return null;
}
public PaddingIdentifier GetNested(string key)
{
if (nests.ContainsKey(key)) return nests[key];
return null;
}
public XmlElement Compile(XmlDocument writeTo)
{
XmlElement root = writeTo.CreateElement(Name);
foreach (string key in attributes.Keys)
{
XmlElement attr = writeTo.CreateElement(key);
attr.SetAttribute("type", attributes[key].Item1.ToString());
attr.InnerText = attributes[key].Item2;
root.AppendChild(attr);
}
foreach (string key in nests.Keys) root.AppendChild(nests[key].Compile(writeTo));
return root;
}
}
public enum ParameterTypes { NUMBER, BYTES, NESTED }
public sealed class RandomLengthPadding : CryptoPadding
{
private const ushort DEFAULT_MAX = 12;
private readonly RandomProvider provider;
private readonly byte[] delimiter;
private readonly ushort maxLen;
public RandomLengthPadding(RandomProvider provider, byte[] delimiter, ushort maxLen = DEFAULT_MAX)
{
this.provider = provider;
this.delimiter = delimiter;
this.maxLen = maxLen;
}
public RandomLengthPadding(byte[] delimiter, ushort maxLen = DEFAULT_MAX)
: this(new RegularRandomProvider(), delimiter, maxLen)
{ }
public byte[] Pad(byte[] message)
{
// Generate padding
byte[] prepadding = GenerateSequence();
byte[] postpadding = GenerateSequence();
// Allocate output array
byte[] result = new byte[message.Length + prepadding.Length + postpadding.Length + delimiter.Length * 2];
// Assemble padding
int index = 0;
Array.Copy(prepadding, 0, result, 0, -(index - (index += prepadding.Length)));
Array.Copy(delimiter, 0, result, index, -(index - (index += delimiter.Length)));
Array.Copy(message, 0, result, index, -(index - (index += message.Length)));
Array.Copy(delimiter, 0, result, index, -(index - (index += delimiter.Length)));
Array.Copy(postpadding, 0, result, index, -(index - (index += postpadding.Length)));
return result;
}
public byte[] Unpad(byte[] message)
{
int index = Support.ArrayContains(message, delimiter);
if (index == -1) throw new InvalidPaddingException("Preceding delimiter could not be found");
byte[] result_stage1 = new byte[message.Length - 1 - index];
Array.Copy(message, index + 1, result_stage1, 0, message.Length - 1 - index);
index = Support.ArrayContains(result_stage1, delimiter, false);
if (index == -1) throw new InvalidPaddingException("Trailing delimeter could not be found");
byte[] result_stage2 = new byte[index];
Array.Copy(result_stage1, 0, result_stage2, 0, index);
return result_stage2;
}
private byte[] GenerateSequence()
{
// Generate between 0 and maxLen random bytes to be used as padding
byte[] padding = provider.GetBytes(provider.NextUShort((ushort)(maxLen + 1)));
// Remove instances of the delimiter sequence from the padding
int idx;
while ((idx = Support.ArrayContains(padding, delimiter)) != -1)
foreach (byte val in provider.GetBytes(delimiter.Length))
padding[idx++] = val;
return padding;
}
public PaddingIdentifier GetParameters()
{
PaddingIdentifier id = new PaddingIdentifier("R");
id.AddAttribute("delimiter", delimiter);
id.AddAttribute("maxLen", maxLen);
return id;
}
}
public sealed class IncrementalPadding : CryptoPadding
{
private const int DEFAULT_INCREMENT = 12;
private readonly RandomProvider provider;
private readonly int increments;
private readonly int determiner;
public IncrementalPadding(RandomProvider provider, int determiner, int increments = DEFAULT_INCREMENT)
{
this.provider = provider;
this.increments = increments * determiner;
this.determiner = determiner;
if (increments < 0) throw new InvalidPaddingException("Increments cannot be negative!");
if (determiner <= 1) throw new InvalidPaddingException("Determiner must be a positive value larger than 1!");
if (increments * determiner < 0) throw new InvalidPaddingException("Increment-Delimiter pair is too large!");
}
public byte[] Pad(byte[] message)
{
if (message.Length % determiner != 0)
{
byte[] result = new byte[message.Length + increments];
Array.Copy(message, result, message.Length);
Array.Copy(provider.GetBytes(increments), 0, result, message.Length, increments);
return result;
}
else return message;
}
public byte[] Unpad(byte[] message)
{
if (message.Length % determiner == 0) return message;
byte[] result = new byte[message.Length - increments];
Array.Copy(message, result, result.Length);
return result;
}
public PaddingIdentifier GetParameters()
{
PaddingIdentifier id = new PaddingIdentifier("I");
id.AddAttribute("increments", increments / determiner);
id.AddAttribute("determiner", determiner);
return id;
}
}
public sealed class SequentialPadding : CryptoPadding
{
private readonly List<CryptoPadding> pads = new List<CryptoPadding>();
public SequentialPadding WithPadding(CryptoPadding padding)
{
pads.Add(padding);
return this;
}
public byte[] Pad(byte[] message)
{
for (int i = 0; i < pads.Count; ++i) message = pads[i].Pad(message);
return message;
}
public byte[] Unpad(byte[] message)
{
for (int i = pads.Count - 1; i >= 0; --i) message = pads[i].Unpad(message);
return message;
}
public PaddingIdentifier GetParameters()
{
PaddingIdentifier id = new PaddingIdentifier("S");
for (int i = 0; i < pads.Count; ++i) id.AddAttribute(i.ToString(), pads[i].GetParameters());
return id;
}
}
public sealed class PassthroughPadding : CryptoPadding
{
public byte[] Pad(byte[] message) => message;
public byte[] Unpad(byte[] message) => message;
public PaddingIdentifier GetParameters() => new PaddingIdentifier("P");
}
public static class PaddingSupport
{
private static readonly Regex byteFinder = new Regex("(\\d{0,3})[,\\]]");
public static string SerializePadding(CryptoPadding padding)
{
XmlDocument doc = new XmlDocument();
doc.AppendChild(padding.GetParameters().Compile(doc));
string output;
using (var stream = new MemoryStream())
{
XmlTextWriter writer = new XmlTextWriter(stream, Encoding.UTF8);
doc.WriteTo(writer);
writer.Flush();
stream.Position = 0;
using (var reader = new StreamReader(stream))
{
output = reader.ReadToEnd();
}
}
return output;
}
// WIP
public static string NetSerialize(CryptoPadding padding) => NetSerialize(padding.GetParameters(), new StringBuilder()).ToString();
public static string NetSerialize(PaddingIdentifier padding) => NetSerialize(padding, new StringBuilder()).ToString();
public static StringBuilder NetSerialize(PaddingIdentifier id, StringBuilder builder)
{
builder.Append(id.Name).Append('{');
foreach (string key in id.AttributeKeys) builder.Append(id.GetAttribute(key).Item2).Append(',');
foreach (string key in id.NestedKeys) NetSerialize(id.GetNested(key), builder).Append(',');
if (id.AttributeKeys.Count > 0 || id.NestedKeys.Count > 0) builder.Remove(builder.Length - 1, 1); // Remove last ','
builder.Append('}');
return builder;
}
// Works but is really large
public static CryptoPadding DeserializePadding(string ser) => DeserializePadding(ser, new DummyRandomProvider());
public static CryptoPadding DeserializePadding(string ser, RandomProvider provider)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml(ser);
XmlNodeList lst = doc.ChildNodes;
if (lst.Count != 1) throw new XMLCryptoParseException("Cannot have more than one root node!");
return ParseNode(lst.Item(0), provider);
}
private static CryptoPadding ParseNode(XmlNode el, RandomProvider provider)
{
XmlNodeList lst;
switch (el.Name)
{
case "P":
return new PassthroughPadding();
case "S":
{
SequentialPadding seq = new SequentialPadding();
if (el.HasChildNodes)
{
lst = el.ChildNodes;
foreach (XmlNode subNode in lst) seq.WithPadding(ParseNode(subNode, provider));
}
return seq;
}
case "I":
{
if (el.HasChildNodes && (lst = el.ChildNodes).Count == 2)
{
int increments;
if (!TryParseNumberNode("increments", lst, out increments))
throw new XMLCryptoParseException("Invalid parameter supplied");
int determiner;
if (!TryParseNumberNode("determiner", lst, out determiner))
throw new XMLCryptoParseException("Invalid parameter supplied");
return new IncrementalPadding(provider, determiner, increments);
}
else throw new XMLCryptoParseException("No parameters supplied");
}
case "R":
{
if (el.HasChildNodes && (lst = el.ChildNodes).Count == 2)
{
byte[] delimiter = TryParseByteNode("delimiter", lst);
if (delimiter == null) throw new XMLCryptoParseException("Invalid parameter supplied");
int maxLen;
if (!TryParseNumberNode("maxLen", lst, out maxLen))
throw new XMLCryptoParseException("Invalid parameter supplied");
return new RandomLengthPadding(provider, delimiter, (ushort)maxLen);
}
else throw new XMLCryptoParseException("No parameters supplied");
}
default:
throw new XMLCryptoParseException($"Unrecognized padding algorithm \"{el.Name}\"");
}
}
private static bool TryParseNumberNode(string name, XmlNodeList from, out int val)
{
XmlNode node = Support.ContainsNamedNode(name, from);
val = 0;
return node != null && int.TryParse(node.InnerText, out val);
}
public static byte[] TryParseByteNode(string name, XmlNodeList from)
{
XmlNode node = Support.ContainsNamedNode(name, from);
if (node == null) return null;
List<byte> collect = new List<byte>();
Match m = byteFinder.Match(node.InnerText);
while (m.Success)
{
collect.Add(byte.Parse(m.Groups[1].Value));
m = m.NextMatch();
}
return collect.ToArray();
}
public class XMLCryptoParseException : SystemException
{
public XMLCryptoParseException() { }
public XMLCryptoParseException(string message) : base(message) { }
public XMLCryptoParseException(string message, Exception innerException) : base(message, innerException) { }
}
}
// Exception related to padding errors
public class InvalidPaddingException : SystemException
{
public InvalidPaddingException() { }
public InvalidPaddingException(string message) : base(message) { }
public InvalidPaddingException(string message, Exception innerException) : base(message, innerException) { }
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Common")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Common")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("23eb87d4-e310-48c4-a931-0961c83892d7")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

182
Common/RSA.cs Normal file
View File

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Text;
namespace Tofvesson.Crypto
{
public class RSA
{
private static readonly PassthroughPadding NO_PADDING = new PassthroughPadding();
private static readonly Encoding DEFAULT_ENCODING = Encoding.UTF8;
private readonly RandomProvider provider = new CryptoRandomProvider();
private readonly BigInteger e;
private readonly BigInteger n;
private readonly BigInteger d;
public bool CanEncrypt { get; private set; }
public bool CanDecrypt { get; private set; }
public RSA(int byteSize, int margin, int threads, int certainty)
{
// Choose primes
BigInteger p = Support.GeneratePrime(threads, byteSize, margin, certainty, provider);
BigInteger q = Support.GeneratePrime(threads, byteSize, margin, certainty, provider);
// For optimization
BigInteger p_1 = p - 1;
BigInteger q_1 = q - 1;
// Calculate needed values
n = p * q;
BigInteger lcm = (p_1 * q_1) / Support.GCD(p_1, q_1);
// Generate e such that is is less than and coprime to lcm
do
{
e = RandomSupport.GenerateBoundedRandom(lcm, provider);
} while (e == lcm || Support.GCD(e, lcm) != 1);
// Generate the modular multiplicative inverse
d = Support.Dio(e, lcm).Key + lcm;
CanEncrypt = true;
CanDecrypt = true;
}
// Load necessary values from files
public RSA(string e_file, string n_file, string d_file) : this(File.ReadAllBytes(e_file), File.ReadAllBytes(n_file), File.ReadAllBytes(d_file))
{ }
public RSA(byte[] e, byte[] n, byte[] d = null)
{
this.e = new BigInteger(e);
this.n = new BigInteger(n);
this.d = new BigInteger(d ?? new byte[0]);
CanEncrypt = true;
CanDecrypt = d!=null;
}
// Create a shallow copy of the given object because it's really just wasteful to perform a deep copy
// unless the person modifying this code is a madman, in which case I highly doubt they'd willfully leave this code alone anyway...
public RSA(RSA copy)
{
e = copy.e;
n = copy.n;
d = copy.d;
CanEncrypt = copy.CanEncrypt;
CanDecrypt = copy.CanDecrypt;
}
// Create a "remote" instance of the rsa object. This means that we do not know the private exponent
private RSA(byte[] e, byte[] n)
{
this.e = new BigInteger(e);
this.n = new BigInteger(n);
this.d = BigInteger.Zero;
CanEncrypt = true;
CanDecrypt = false;
}
// Encrypt (duh)
public byte[] EncryptString(string message, Encoding encoding = null, CryptoPadding padding = null, bool sign = false) => Encrypt((encoding ?? DEFAULT_ENCODING).GetBytes(message), padding, sign);
public byte[] Encrypt(byte[] message, CryptoPadding padding = null, bool sign = false)
{
// Apply dynamic padding
message = (padding ?? NO_PADDING).Pad(message);
// Apply fixed padding
byte[] b1 = new byte[message.Length + 1];
Array.Copy(message, b1, message.Length);
b1[message.Length] = 1;
message = b1;
// Represent message as a number
BigInteger m = new BigInteger(message);
// Encrypt message
BigInteger cryptomessage = Support.ModExp(m, sign ? d : e, n);
// Convert encrypted message back to bytes
return cryptomessage.ToByteArray();
}
// Decrypt (duh)
public string DecryptString(byte[] message, Encoding encoding = null, CryptoPadding padding = null, bool checkSign = false) => new string((encoding ?? DEFAULT_ENCODING).GetChars(Decrypt(message, padding, checkSign)));
public byte[] Decrypt(byte[] message, CryptoPadding padding = null, bool checkSign = false)
{
// Reinterpret encrypted message as a number
BigInteger cryptomessage = new BigInteger(message);
// Reverse encryption
message = Support.ModExp(cryptomessage, checkSign ? e : d, n).ToByteArray();
// Remove fixed padding
byte[] b1 = new byte[message.Length - 1];
Array.Copy(message, b1, message.Length - 1);
message = b1;
// Remove dynamic padding
message = (padding ?? NO_PADDING).Unpad(message);
return message;
}
// Gives you the public key
public byte[] GetPubK() => e.ToByteArray();
// Save this RSA instance to correspondingly named files
public void Save(string fileNameBase, bool force = false)
{
if (force || !File.Exists(fileNameBase + ".e")) File.WriteAllBytes(fileNameBase + ".e", e.ToByteArray());
if (force || !File.Exists(fileNameBase + ".n")) File.WriteAllBytes(fileNameBase + ".n", n.ToByteArray());
if (force || !File.Exists(fileNameBase + ".d")) File.WriteAllBytes(fileNameBase + ".d", d.ToByteArray());
}
// Serialize (for public key distribution)
public byte[] Serialize() => Support.SerializeBytes(new byte[][] { e.ToByteArray(), n.ToByteArray() });
// Deserialize RSA data (for key distribution (but the other end (how many parentheses deep can I go?)))
public static RSA Deserialize(byte[] function, out int read)
{
byte[][] rd = Support.DeserializeBytes(function, 2);
read = rd[0].Length + rd[1].Length + 8;
return new RSA(rd[0], rd[1]);
}
// Check if the data we want to convert into an RSA-instance will cause a crash if we try to parse it
public static bool CanDeserialize(IEnumerable<byte> data)
{
try
{
int size = Support.ReadInt(data, 0), size2;
if (size >= data.Count() - 8) return false;
size2 = Support.ReadInt(data, 4 + size);
if (size2 > data.Count() - size - 8) return false;
return true;
}
catch (Exception) { }
return false;
}
// Safely attempt to load RSA keys from files
public static RSA TryLoad(string fileNameBase) => TryLoad(fileNameBase + ".e", fileNameBase + ".n", fileNameBase + ".d");
public static RSA TryLoad(string e_file, string n_file, string d_file)
{
try
{
return new RSA(e_file, n_file, d_file);
}
catch (Exception) { }
return null;
}
public override bool Equals(object obj)
=> obj is RSA && ((RSA)obj).CanDecrypt == CanDecrypt && ((RSA)obj).e.Equals(e) && ((RSA)obj).n.Equals(n) && (!CanDecrypt || ((RSA)obj).d.Equals(d));
}
}

90
Common/SHA.cs Normal file
View File

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Tofvesson.Crypto
{
/// <summary>
/// Secure Hashing Alorithm implementations
/// </summary>
public static class SHA
{
public static byte[] SHA1(byte[] message)
{
// Initialize buffers
uint h0 = 0x67452301;
uint h1 = 0xEFCDAB89;
uint h2 = 0x98BADCFE;
uint h3 = 0x10325476;
uint h4 = 0xC3D2E1F0;
// Pad message
int ml = message.Length + 1;
byte[] msg = new byte[ml + ((960 - (ml*8 % 512)) % 512)/8 + 8];
Array.Copy(message, msg, message.Length);
msg[message.Length] = 0x80;
long len = message.Length * 8;
for (int i = 0; i < 8; ++i) msg[msg.Length - 1 - i] = (byte)((len >> (i*8)) & 255);
//Support.WriteToArray(msg, message.Length * 8, msg.Length - 8);
//for (int i = 0; i <4; ++i) msg[msg.Length - 5 - i] = (byte)(((message.Length*8) >> (i * 8)) & 255);
int chunks = msg.Length / 64;
// Perform hashing for each 512-bit block
for(int i = 0; i<chunks; ++i)
{
// Split block into words
uint[] w = new uint[80];
for(int j = 0; j<16; ++j)
w[j] |= (uint) ((msg[i * 64 + j * 4] << 24) | (msg[i * 64 + j * 4 + 1] << 16) | (msg[i * 64 + j * 4 + 2] << 8) | (msg[i * 64 + j * 4 + 3] << 0));
// Expand words
for(int j = 16; j<80; ++j)
w[j] = Rot(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1);
// Initialize chunk-hash
uint
a = h0,
b = h1,
c = h2,
d = h3,
e = h4;
// Do hash rounds
for (int t = 0; t<80; ++t)
{
uint tmp = Rot(a, 5) + func(t, b, c, d) + e + K(t) + w[t];
e = d;
d = c;
c = Rot(b, 30);
b = a;
a = tmp;
}
h0 += a;
h1 += b;
h2 += c;
h3 += d;
h4 += e;
}
return Support.WriteContiguous(new byte[20], 0, Support.SwapEndian(h0), Support.SwapEndian(h1), Support.SwapEndian(h2), Support.SwapEndian(h3), Support.SwapEndian(h4));
}
private static uint func(int t, uint b, uint c, uint d) =>
t < 20 ? (b & c) | ((~b) & d) :
t < 40 ? b ^ c ^ d :
t < 60 ? (b & c) | (b & d) | (c & d) :
/*t<80*/ b ^ c ^ d;
private static uint K(int t) =>
t < 20 ? 0x5A827999 :
t < 40 ? 0x6ED9EBA1 :
t < 60 ? 0x8F1BBCDC :
/*t<80*/ 0xCA62C1D6 ;
private static uint Rot(uint val, int by) => (val << by) | (val >> (32 - by));
}
}

57
Common/Streams.cs Normal file
View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace Common
{
public sealed class TimeStampWriter : TextWriter
{
private readonly DateTime time = DateTime.Now;
private readonly string dateFormat;
private readonly TextWriter underlying;
private bool triggered;
public TimeStampWriter(TextWriter underlying, string dateFormat, bool emulateNL = true)
{
this.dateFormat = dateFormat;
this.underlying = underlying;
triggered = emulateNL;
}
public TimeStampWriter(TextWriter underlying, string dateFormat, IFormatProvider formatProvider, bool emulateNL = true) : base(formatProvider)
{
this.dateFormat = dateFormat;
this.underlying = underlying;
triggered = emulateNL;
}
public override Encoding Encoding => underlying.Encoding;
public override void Write(char value)
{
if (triggered)
{
StringBuilder s = new StringBuilder();
s.Append('[').Append(time.ToString(dateFormat)).Append("] ");
foreach (var c in s.ToString()) underlying.Write(c);
}
underlying.Write(value);
triggered = value == '\n';
}
}
// A TextWriter wrapper for the Debug output
public sealed class DebugAdapterWriter : TextWriter
{
public override Encoding Encoding => throw new NotImplementedException();
public override void Write(char value)
{
Debug.Write(value);
}
}
}

582
Common/Support.cs Normal file
View File

@ -0,0 +1,582 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Numerics;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace Tofvesson.Crypto
{
// Just a ton of support methods to make life easier. Almost aboslutely nothing of notable value here
// Honestly, just continue on to the next file of whatever, unless you have some unbearable desire to give yourself a headache and be completely disappointed by the end of reading this
public static class Support
{
// -- Math --
public static BigInteger Invert(BigInteger b)
{
byte[] arr = b.ToByteArray();
for (int i = 0; i < arr.Length; ++i) arr[i] ^= 255;
BigInteger integer = new BigInteger(arr);
integer += 1;
return integer;
}
public static BigInteger ModExp(BigInteger b, BigInteger e, BigInteger m)
{
int count = e.ToByteArray().Length * 8;
BigInteger result = BigInteger.One;
b = b % m;
while (count>0)
{
if (e % 2 != 0) result = (result * b) % m;
b = (b * b) % m;
e >>= 1;
--count;
}
return result;
}
/// <summary>
/// Uses the fermat test a given amount of times to test whether or not a supplied interger is probably prime.
/// </summary>
/// <param name="b">Value to test primality of</param>
/// <param name="provider">Random provider used to generate values to test b against</param>
/// <param name="certainty">How many times the test should be performed. More iterations means higher certainty, but at the cost of performance!</param>
/// <returns>Whether or not the given value is probably prime or not</returns>
public static bool IsProbablePrime(BigInteger b, RandomProvider provider, int certainty)
{
BigInteger e = b - 1;
byte[] b1 = b.ToByteArray();
byte last = b1[b1.Length-1];
int len = b1.Length - 1;
for (int i = 0; i < certainty; ++i)
{
byte[] gen = new byte[provider.NextInt(len)+1];
provider.GetBytes(gen);
if (last != 0 && gen.Length==len+1) gen[gen.Length - 1] %= last;
else gen[gen.Length - 1] &= 127;
BigInteger test = new BigInteger(gen);
if (ModExp(test, e, b) != 1) return false;
}
return true;
}
/// <summary>
/// Calculate the greatest common divisor for two values.
/// </summary>
/// <param name="b1">First value</param>
/// <param name="b2">Second value</param>
/// <returns>The greatest common divisor</returns>
public static BigInteger GCD(BigInteger b1, BigInteger b2)
{
BigInteger tmp;
while ((tmp = b1 % b2) != 0)
{
b1 = b2;
b2 = tmp;
}
return b2;
}
public static int CollectiveLength(this string[] s)
{
int i = 0;
foreach (var s1 in s) i += s1.Length;
return i;
}
/// <summary>
/// Linear diophantine equations. Calculates the modular multiplicative inverse for a given value and a given modulus.
/// For: ax + by = 1
/// Where 'a' and 'b' are known factors
/// </summary>
/// <param name="in1">First known factor (a)</param>
/// <param name="in2">Second known factor (b)</param>
/// <returns>A pair of factors that fulfill the aforementioned equations (if possible), where Item1 corresponds to 'x' and Item2 corresponds to 'y'. If the two supplied known factors are not coprime, both factors will be 0</returns>
public static KeyValuePair<BigInteger, BigInteger> Dio(BigInteger in1, BigInteger in2)
{
// Euclidean algorithm
BigInteger tmp;
var i1 = in1;
var i2 = in2;
if (i1 <= BigInteger.Zero || i2 <= BigInteger.Zero || i1 == i2 || i1 % i2 == BigInteger.Zero || i2 % i1 == BigInteger.Zero)
{
return new KeyValuePair<BigInteger, BigInteger>(BigInteger.Zero, BigInteger.Zero);
}
var minusOne = new BigInteger(-1);
var e_m = new BigInteger(-1L);
var collect = new Stack<BigInteger>();
while ((e_m = i1 % i2) != BigInteger.Zero)
{
collect.Push(i1 / i2 * minusOne);
i1 = i2;
i2 = e_m;
}
// There are no solutions because 'a' and 'b' are not coprime
if (i2 != BigInteger.One)
return new KeyValuePair<BigInteger, BigInteger>(BigInteger.Zero, BigInteger.Zero);
// Extended euclidean algorithm
var restrack_first = BigInteger.One;
var restrack_second = collect.Pop();
while (collect.Count > 0)
{
tmp = restrack_second;
restrack_second = restrack_first + restrack_second * collect.Pop();
restrack_first = tmp;
}
return new KeyValuePair<BigInteger, BigInteger>(restrack_first, restrack_second);
}
/// <summary>
/// Generate a prime number using with a given approximate length and byte length margin
/// </summary>
/// <param name="threads">How many threads to use to generate primes</param>
/// <param name="approximateByteCount">The byte array length around which the prime generator will select lengths</param>
/// <param name="byteMargin">Allowed deviation of byte length from approximateByteCount</param>
/// <param name="certainty">How many iterations of the fermat test should be run to test primailty for each generated number</param>
/// <param name="provider">Random provider that will be used to generate random primes</param>
/// <returns>A prime number that is aproximately approximateByteCount long</returns>
public static BigInteger GeneratePrime(int threads, int approximateByteCount, int byteMargin, int certainty, RandomProvider provider)
{
var found = false;
BigInteger result = BigInteger.Zero;
for(int i = 0; i<threads; ++i)
Task.Factory.StartNew(() =>
{
char left = '\0';
byte rand = 0;
BigInteger b = BigInteger.Zero;
while (!found)
{
if (left == 0)
{
rand = provider.GetBytes(1)[0];
left = (char)8;
}
byte[] b1 = provider.GetBytes(approximateByteCount + (provider.GetBytes(1)[0] % byteMargin) * (rand % 2 == 1 ? 1 : -1));
b1[0] |= 1; // Always odd
b1[b1.Length - 1] &= 127; // Always positive
b = new BigInteger(b1);
rand >>= 1;
--left;
if (IsProbablePrime(b, provider, certainty))
{
found = true;
result = b;
}
}
});
while (!found) System.Threading.Thread.Sleep(125);
return result;
}
// -- Net --
/// <summary>
/// Finds an IPv4a address in the address list.
/// </summary>
/// <param name="entry">IPHostEntry to get the address from</param>
/// <returns>An IPv4 address if available, otherwise null</returns>
public static IPAddress GetIPV4(this IPHostEntry entry)
{
foreach (IPAddress addr in entry.AddressList)
if (addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
return addr;
return null;
}
// -- Arrays/Collections --
/// <summary>
/// Pad or truncate this array to the specified length. Padding is performed by filling the new indicies with 0's. Truncation removes bytes from the end.
/// </summary>
/// <typeparam name="T">The array type</typeparam>
/// <param name="t">The array to resize</param>
/// <param name="length">Target length</param>
/// <returns>A resized array</returns>
public static T[] ToLength<T>(this T[] t, int length)
{
var t1 = new T[length];
Array.Copy(t, t1, Math.Min(length, t.Length));
return t1;
}
/// <summary>
/// Reads a serialized 32-bit integer from the byte collection
/// </summary>
/// <param name="data"></param>
/// <param name="offset"></param>
/// <returns></returns>
public static int ReadInt(IEnumerable<byte> data, int offset)
{
int result = 0;
for (int i = 0; i < 4; ++i)
result |= data.ElementAt(i + offset) << (i * 8);
return result;
}
public static int ArrayContains(byte[] b, byte[] seq, bool fromStart = true)
{
int track = 0;
for (int i = fromStart ? 0 : b.Length - 1; (fromStart && i < b.Length) || (!fromStart && i >= 0); i+=fromStart?1:-1)
if (b[i] == seq[fromStart?track:seq.Length - 1 - track])
{
if (++track == seq.Length) return i;
}
else track = 0;
return -1;
}
public static byte[] WriteToArray(byte[] target, int data, int offset)
{
for (int i = 0; i < 4; ++i)
target[i + offset] = (byte)((data >> (i * 8))&255);
return target;
}
public static byte[] WriteContiguous(byte[] target, int offset, params int[] data)
{
for (int i = 0; i < data.Length; ++i) WriteToArray(target, data[i], offset + i * 4);
return target;
}
public static byte[] WriteToArray(byte[] target, uint data, int offset)
{
for (int i = 0; i < 4; ++i)
target[i + offset] = (byte)((data >> (i * 8)) & 255);
return target;
}
public static byte[] WriteContiguous(byte[] target, int offset, params uint[] data)
{
for (int i = 0; i < data.Length; ++i) WriteToArray(target, data[i], offset + i * 4);
return target;
}
public static byte[] Concatenate(params byte[][] bytes)
{
int alloc = 0;
foreach (byte[] b in bytes) alloc += b.Length;
byte[] result = new byte[alloc];
alloc = 0;
for(int i = 0; i<bytes.Length; ++i)
{
Array.Copy(bytes[i], 0, result, alloc, bytes[i].Length);
alloc += bytes[i].Length;
}
return result;
}
public static void ArrayCopy<T>(IEnumerable<T> source, int sourceOffset, T[] destination, int offset, int length)
{
for (int i = 0; i < length; ++i) destination[i + offset] = source.ElementAt<T>(i+sourceOffset);
}
public static string ArrayToString(byte[] array)
{
StringBuilder builder = new StringBuilder().Append('[');
for (int i = 0; i < array.Length; ++i)
{
builder.Append(array[i]);
if (i != array.Length - 1) builder.Append(", ");
}
return builder.Append(']').ToString();
}
public static bool ArraysEqual<T>(T[] t1, T[] t2)
{
if (t1 == t2) return true;
else if (t1 == null) return false;
else if (t1.Length != t2.Length) return false;
for (int i = 0; i < t1.Length; ++i)
if (!ObjectsEqual(t1[i], t2[i]))
return false;
return true;
}
public static bool ObjectsEqual(object o1, object o2) => (o1 == null && o2 == null) || (o1 != null && o1.Equals(o2));
public static void EnqueueAll<T>(this Queue<T> q, IEnumerable<T> items, int offset, int length)
{
for (int i = 0; i < length; ++i) q.Enqueue(items.ElementAt(i+offset));
}
public static T[]Dequeue<T>(this Queue<T> q, int count)
{
T[] t = new T[count];
for (int i = 0; i < count; ++i) t[i] = q.Dequeue();
return t;
}
public static byte[] SerializeBytes(byte[][] bytes)
{
int collectSize = 0;
for (int i = 0; i < bytes.Length; ++i) collectSize += bytes[i].Length;
byte[] output = new byte[collectSize + 4*bytes.Length];
collectSize = 0;
for(int i = 0; i<bytes.Length; ++i)
{
WriteToArray(output, bytes[i].Length, collectSize);
Array.Copy(bytes[i], 0, output, collectSize + 4, bytes[i].Length);
collectSize += bytes[i].Length + 4;
}
return output;
}
public static byte[][] DeserializeBytes(byte[] message, int messageCount)
{
byte[][] output = new byte[messageCount][];
int offset = 0;
for(int i = 0; i< messageCount; ++i)
{
int size = ReadInt(message, offset);
if (size > message.Length - offset - 4 || (i!=messageCount-1 && size==message.Length-offset-4))
throw new IndexOutOfRangeException("Attempted to read more bytes than are available");
offset += 4;
output[i] = new byte[size];
Array.Copy(message, offset, output[i], 0, size);
offset += size;
}
return output;
}
public static T[] SubArray<T>(this T[] array, int start, int end)
{
T[] res = new T[end-start];
for (int i = start; i < end; ++i) res[i - start] = array[i];
return res;
}
public static byte[] XOR(this byte[] array, byte[] xor)
{
for (int i = Math.Min(array.Length, xor.Length) - 1; i >= 0; --i) array[i] ^= xor[i];
return array;
}
public static string ToUTF8String(this byte[] b) => new string(Encoding.UTF8.GetChars(b));
public static byte[] ToUTF8Bytes(this string s) => Encoding.UTF8.GetBytes(s);
// -- Misc --
// Allows deconstruction when iterating over a collection of Tuples
public static void Deconstruct<T1, T2>(this Tuple<T1, T2> tuple, out T1 key, out T2 value)
{
key = tuple.Item1;
value = tuple.Item2;
}
public static XmlNode ContainsNamedNode(string name, XmlNodeList lst)
{
for (int i = lst.Count - 1; i >= 0; --i)
if (lst.Item(i).Name.Equals(name))
return lst.Item(i);
return null;
}
public static bool IsNumber(this char c) => c > 47 && c < 58;
public static bool IsAlphabetical(this char c) => (c > 64 && c < 91) || (c > 96 && c < 123);
public static bool IsAlphaNumeric(this char c) => c.IsNumber() || c.IsAlphabetical();
public static bool IsDecimal(this char c) => c == '.' || c.IsNumber();
// Swap endianness of a given integer
public static uint SwapEndian(uint value) => (uint)(((value >> 24) & (255 << 0)) | ((value >> 8) & (255 << 8)) | ((value << 8) & (255 << 16)) | ((value << 24) & (255 << 24)));
public static string ToHexString(byte[] value)
{
StringBuilder builder = new StringBuilder();
foreach(byte b in value)
{
builder.Append((char)((((b >> 4) < 10) ? 48 : 87) + (b >> 4)));
builder.Append((char)((((b & 15) < 10) ? 48 : 87) + (b & 15)));
}
return builder.ToString();
}
public static bool ReadYNBool(this TextReader reader, string nonDefault) => reader.ReadLine().ToLower().Equals(nonDefault);
public static string SerializeStrings(string[] data)
{
StringBuilder builder = new StringBuilder();
foreach (var datum in data) builder.Append(datum.Replace("&", "&amp;").Replace("\n", "&nl;")).Append("&nm;");
if (builder.Length > 0) builder.Remove(builder.Length - 4, 4);
return builder.ToString();
}
public static string[] DeserializeString(string message)
{
List<string> collect = new List<string>();
const string target = "&nm;";
int found = 0;
int prev = 0;
for(int i = 0; i<message.Length; ++i)
{
if (message[i] == target[found])
{
if (++found == target.Length)
{
collect.Add(message.Substring(prev, (-prev) + (prev = i + 1) - target.Length));
found = 0;
}
}
else found = 0;
}
collect.Add(message.Substring(prev));
string[] data = collect.ToArray();
for (int i = 0; i < data.Length; ++i) data[i] = data[i].Replace("&nl;", "\n").Replace("&amp;", "&");
return data;
}
public static int Accepts(this ParameterInfo[] info, Type parameterType, int pastFirst = 0)
{
int found = -1;
for(int i = 0; i<info.Length; ++i)
if (parameterType.IsAssignableFrom(info[i].ParameterType) && ++found >= pastFirst)
return i;
return -1;
}
}
public abstract class RandomProvider
{
public abstract byte[] GetBytes(int count);
public abstract byte[] GetBytes(byte[] buffer);
// Randomly generates a shortinteger bounded by the supplied integer. If bounding value is <= 0, it will be ignored
public ushort NextUShort(ushort bound = 0)
{
byte[] raw = GetBytes(2);
ushort result = 0;
for (byte s = 0; s < 2; ++s)
{
result <<= 8;
result |= raw[s];
}
return (ushort)(bound > 0 ? result % bound : result);
}
// Randomly generates an integer bounded by the supplied integer. If bounding value is <= 0, it will be ignored
public uint NextUInt(uint bound = 0)
{
byte[] raw = GetBytes(4);
uint result = 0;
for (byte s = 0; s < 4; ++s)
{
result <<= 8;
result |= raw[s];
}
return bound > 0 ? result % bound : result;
}
// Randomly generates a long integer bounded by the supplied integer. If bounding value is <= 0, it will be ignored
public ulong NextULong(ulong bound = 0)
{
byte[] raw = GetBytes(8);
ulong result = 0;
for (byte s = 0; s < 8; ++s)
{
result <<= 8;
result |= raw[s];
}
return bound > 0 ? result % bound : result;
}
public char NextChar(bool alphanumeric = false)
{
char c = (char) GetBytes(1)[0];
if (alphanumeric)
{
c %= (char)62;
c += (char)(c < 10 ? 48 : c < 36 ? 55 : 61);
}
return c;
}
public string NextString(int length)
{
byte[] b = GetBytes(length);
StringBuilder builder = new StringBuilder(length);
foreach(var b1 in b)
{
char c = (char)(b1%62);
builder.Append((char)(c+(c < 10 ? 48 : c < 36 ? 55 : 61)));
}
return builder.ToString();
}
public short NextShort(short bound = 0) => (short)NextUInt((ushort)bound);
public int NextInt(int bound = 0) => (int)NextUInt((uint)bound);
public long NextLong(long bound = 0) => (long)NextULong((ulong)bound);
}
public sealed class RegularRandomProvider : RandomProvider
{
private Random rand;
public RegularRandomProvider(Random rand) { this.rand = rand; }
public RegularRandomProvider() : this(new Random(Environment.TickCount)) {}
// Copy our random reference to the other provider: share a random object
public void share(RegularRandomProvider provider) => provider.rand = this.rand;
public override byte[] GetBytes(int count) => GetBytes(new byte[count]);
public override byte[] GetBytes(byte[] buffer)
{
rand.NextBytes(buffer);
return buffer;
}
}
public sealed class CryptoRandomProvider : RandomProvider
{
private RNGCryptoServiceProvider rand;
public CryptoRandomProvider(RNGCryptoServiceProvider rand) { this.rand = rand; }
public CryptoRandomProvider() : this(new RNGCryptoServiceProvider()) { }
// Copy our random reference to the other provider: share a random object
public void share(CryptoRandomProvider provider) => provider.rand = this.rand;
public override byte[] GetBytes(int count) => GetBytes(new byte[count]);
public override byte[] GetBytes(byte[] buffer)
{
rand.GetBytes(buffer);
return buffer;
}
}
public sealed class DummyRandomProvider : RandomProvider
{
public override byte[] GetBytes(int count) => new byte[count];
public override byte[] GetBytes(byte[] buffer)
{
for (int i = 0; i < buffer.Length; ++i) buffer[i] = 0;
return buffer;
}
}
public static class RandomSupport
{
public static BigInteger GenerateBoundedRandom(BigInteger max, RandomProvider provider)
{
byte[] b = max.ToByteArray();
byte maxLast = b[b.Length - 1];
provider.GetBytes(b);
if (maxLast != 0) b[b.Length - 1] %= maxLast;
b[b.Length - 1] |= 127;
return new BigInteger(b);
}
}
}

6
Server/App.config Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
</configuration>

542
Server/Database.cs Normal file
View File

@ -0,0 +1,542 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Tofvesson.Crypto;
using Tofvesson.Collections;
namespace Server
{
public sealed class Database
{
private static readonly RandomProvider random = new RegularRandomProvider();
public string[] MasterEntry { get; }
public string DatabaseName { get; }
public bool LoadFull { get; set; }
// Cached changes
private readonly List<User> changeList = new List<User>();
private readonly List<User> toRemove = new List<User>();
private readonly EvictionList<User> loadedUsers = new EvictionList<User>(40);
public Database(string dbName, string master, bool loadFullDB = false)
{
dbName += ".xml";
MasterEntry = master.Split('/');
if (!File.Exists(dbName))
{
FileStream strm = File.Create(dbName);
byte[] b;
strm.Write(b = Encoding.UTF8.GetBytes($"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"), 0, b.Length);
// Generate root element for users
for (int i = 0; i < MasterEntry.Length; ++i) strm.Write(b = Encoding.UTF8.GetBytes($"<{MasterEntry[i]}>"), 0, b.Length);
for (int i = MasterEntry.Length - 1; i >= 0; --i) strm.Write(b = Encoding.UTF8.GetBytes($"</{MasterEntry[i]}>"), 0, b.Length);
strm.Close();
}
DatabaseName = dbName;
LoadFull = loadFullDB;
}
// Flush before deletion
~Database() { Flush(false); }
// UpdateUser is just another name for AddUser
public void UpdateUser(User entry) => AddUser(entry, true);
public void AddUser(User entry) => AddUser(entry, true);
private void AddUser(User entry, bool withFlush)
{
entry = ToEncoded(entry);
for (int i = 0; i < loadedUsers.Count; ++i)
if (entry.Equals(loadedUsers[i]))
loadedUsers[i] = entry;
for (int i = toRemove.Count - 1; i >= 0; --i)
if (toRemove[i].Equals(entry.Name))
toRemove.RemoveAt(i);
for (int i = 0; i < changeList.Count; ++i)
if (changeList[i].Equals(entry.Name))
return;
changeList.Add(entry);
if(withFlush) Flush(true);
}
public void RemoveUser(User entry) => RemoveUser(entry, true);
private void RemoveUser(User entry, bool withFlush)
{
entry = ToEncoded(entry);
for (int i = 0; i < loadedUsers.Count; ++i)
if (entry.Equals(loadedUsers[i]))
loadedUsers.RemoveAt(i);
for (int i = changeList.Count - 1; i >= 0; --i)
if (changeList[i].Equals(entry.Name))
changeList.RemoveAt(i);
for (int i = toRemove.Count - 1; i >= 0; --i)
if (toRemove[i].Equals(entry.Name))
return;
toRemove.Add(entry);
if(withFlush) Flush(true);
}
// Triggers a forceful flush
public void Flush() => Flush(false);
// Permissive (cache-dependent) flush
private void Flush(bool optional)
{
if(!(optional || changeList.Count > 30 || toRemove.Count > 30)) return; // No need to flush
string temp = GenerateTempFileName("tmp_", ".xml");
using(var writer = XmlWriter.Create(temp))
{
using(var reader = XmlReader.Create(DatabaseName))
{
int masterDepth = 0;
bool trigger = false, wn = false, recent = false;
while (wn || reader.Read())
{
wn = false;
if (trigger)
{
foreach (var user in changeList)
WriteUser(writer, user);
bool wroteNode = false;
while ((wroteNode || reader.Name.Equals("User") || reader.Read()) && reader.NodeType != XmlNodeType.EndElement)
{
wroteNode = false;
if (reader.Name.Equals("User"))
{
User u = User.Parse(ReadEntry(reader), this);
if (u != null)
{
bool shouldWrite = true;
foreach (var toChange in changeList)
if (toChange.Name.Equals(u.Name))
{
shouldWrite = false;
break;
}
if (shouldWrite)
foreach (var remove in toRemove)
if (remove.Name.Equals(u.Name))
{
shouldWrite = false;
break;
}
if (shouldWrite) WriteUser(writer, u);
}
}
else
{
wroteNode = true;
writer.WriteNode(reader, true);
}
}
trigger = false;
recent = true;
writer.WriteEndElement();
toRemove.Clear();
changeList.Clear();
}
if (masterDepth != MasterEntry.Length && reader.Name.Equals(MasterEntry[masterDepth]))
{
trigger = reader.NodeType == XmlNodeType.Element && ++masterDepth == MasterEntry.Length;
reader.MoveToContent();
writer.WriteStartElement(MasterEntry[masterDepth - 1]);
}
else if (masterDepth == MasterEntry.Length && recent)
{
if(masterDepth!=1) writer.WriteEndElement();
recent = false;
}
else
{
wn = true;
writer.WriteNode(reader, true);
}
}
}
writer.Flush();
}
File.Delete(DatabaseName);
File.Move(temp, DatabaseName);
}
private static void WriteUser(XmlWriter writer, User u)
{
writer.WriteStartElement("User");
if (u.IsAdministrator) writer.WriteAttributeString("admin", "", "true");
writer.WriteElementString("Name", u.Name);
writer.WriteElementString("Balance", u.Balance.ToString());
writer.WriteElementString("Password", u.PasswordHash);
writer.WriteElementString("Salt", u.Salt);
foreach (var tx in u.History)
{
writer.WriteStartElement("Transaction");
writer.WriteElementString(tx.to.Equals(u.Name) ? "From" : "To", tx.to.Equals(u.Name) ? tx.from : tx.to);
writer.WriteElementString("Balance", tx.amount.ToString());
if (tx.meta != null && tx.meta.Length != 0) writer.WriteElementString("Meta", tx.meta);
writer.WriteEndElement();
}
writer.WriteEndElement();
}
private static string GenerateTempFileName(string prefix, string suffix)
{
string s;
do s = prefix + random.NextString((Math.Abs(random.NextInt())%16)+8) + suffix;
while (File.Exists(s));
return s;
}
private Entry ReadEntry(XmlReader reader)
{
Entry e = new Entry(reader.Name);
if (reader.HasAttributes)
{
reader.MoveToAttribute(0);
do e.Attributes.Add(reader.Name, reader.Value);
while (reader.MoveToNextAttribute());
}
reader.MoveToContent();
while (reader.Read() && reader.NodeType != XmlNodeType.EndElement)
{
if (reader.NodeType == XmlNodeType.Element)
using (var subRead = reader.ReadSubtree())
{
SkipSpaces(subRead);
e.NestedEntries.Add(ReadEntry(subRead));
}
else if (reader.NodeType == XmlNodeType.Text) e.Text = reader.Value;
}
reader.Read();
return e;
}
public User GetUser(string name) => FirstUser(u => u.Name.Equals(name));
public User FirstUser(Predicate<User> p)
{
User u;
foreach (var entry in loadedUsers)
if (p(u=FromEncoded(entry)))
return u;
foreach (var entry in changeList)
if (p(u=FromEncoded(entry)))
{
if (!loadedUsers.Contains(entry)) loadedUsers.Add(u);
return u;
}
using (var reader = XmlReader.Create(DatabaseName))
{
if (!Traverse(reader, MasterEntry)) return null;
while (reader.Read() && reader.NodeType != XmlNodeType.EndElement)
{
if (reader.Name.Equals("User"))
{
User n = User.Parse(ReadEntry(reader), this);
if (n != null && p(n=FromEncoded(n)))
{
if (!loadedUsers.Contains(n)) loadedUsers.Add(n);
return n;
}
}
}
}
return null;
}
public bool AddTransaction(string sender, string recipient, long amount, string message = null)
{
User from = FirstUser(u => u.Name.Equals(sender));
User to = FirstUser(u => u.Name.Equals(recipient));
if (to == null || (from == null && !to.IsAdministrator)) return false;
Transaction tx = new Transaction(from == null ? "System" : from.Name, to.Name, amount, message);
to.History.Add(tx);
AddUser(to);
if (from != null)
{
from.History.Add(tx);
AddUser(from);
}
return true;
}
public User[] Users(Predicate<User> p)
{
List<User> l = new List<User>();
User u;
foreach (var entry in changeList)
if (p(u=FromEncoded(entry)))
l.Add(entry);
using (var reader = XmlReader.Create(DatabaseName))
{
if (!Traverse(reader, MasterEntry)) return null;
while (SkipSpaces(reader) && reader.NodeType != XmlNodeType.EndElement)
{
if (reader.NodeType == XmlNodeType.EndElement) break;
User e = User.Parse(ReadEntry(reader), this);
if (e!=null && p(e=FromEncoded(e))) l.Add(e);
}
}
return l.ToArray();
}
public bool ContainsUser(string user) => FirstUser(u => u.Name.Equals(user)) != null;
public bool ContainsUser(User user) => FirstUser(u => u.Name.Equals(user.Name)) != null;
private bool Traverse(XmlReader reader, params string[] downTo)
{
for(int i = 0; i<downTo.Length; ++i)
{
while (reader.Read() && !downTo[i].Equals(reader.Name)) ;
if (!downTo[i].Equals(reader.Name)) return false;
reader.MoveToContent();
}
return true;
}
private bool SkipSpaces(XmlReader reader)
{
bool b;
while ((b = reader.Read()) && reader.NodeType == XmlNodeType.Whitespace) ;
return b;
}
private static User ToEncoded(User entry)
{
User u = new User(entry);
u.Name = Encode(u.Name);
for (int i = 0; i < u.History.Count; ++i)
{
u.History[i].to = Encode(u.History[i].to);
u.History[i].from = Encode(u.History[i].from);
u.History[i].meta = Encode(u.History[i].meta);
}
return u;
}
private static User FromEncoded(User entry)
{
User u = new User(entry);
u.Name = Decode(u.Name);
for (int i = 0; i < u.History.Count; ++i)
{
u.History[i].to = Decode(u.History[i].to);
u.History[i].from = Decode(u.History[i].from);
u.History[i].meta = Decode(u.History[i].meta);
}
return u;
}
private static string Encode(string s) => Convert.ToBase64String(Encoding.UTF8.GetBytes(s));
private static string Decode(string s) => Convert.FromBase64String(s).ToUTF8String();
internal class Entry
{
public string Text { get; set; }
public string Name { get; set; }
public Dictionary<string, string> Attributes { get; } // Transient properties in comparison
public List<Entry> NestedEntries { get; } // Semi-transient comparison properties
public Entry(string name, string text = "")
{
Name = name;
Text = text;
Attributes = new Dictionary<string, string>();
NestedEntries = new List<Entry>();
}
public Entry GetNestedEntry(Predicate<Entry> p)
{
foreach (var entry in NestedEntries)
if (p(entry))
return entry;
return null;
}
public bool BoolAttribute(string attrName, bool def = false, bool ignoreCase = true)
=> Attributes.ContainsKey(attrName) && bool.TryParse(ignoreCase ? Attributes[attrName].ToLower() :Attributes[attrName], out bool b) ? b : def;
public Entry AddNested(Entry e)
{
NestedEntries.Add(e);
return this;
}
public Entry AddAttribute(string key, string value)
{
Attributes[key] = value;
return this;
}
public override bool Equals(object obj)
{
if (!(obj is Entry)) return false;
Entry cmp = (Entry)obj;
if (cmp.Attributes.Count != Attributes.Count || cmp.NestedEntries.Count != NestedEntries.Count || !Text.Equals(cmp.Text) || !Name.Equals(cmp.Name)) return false;
foreach(var entry in NestedEntries)
{
if (entry.BoolAttribute("omit")) goto Next;
foreach (var cmpEntry in cmp.NestedEntries)
if (cmpEntry.BoolAttribute("omit")) continue;
else if (cmpEntry.Equals(entry)) goto Next;
return false;
Next: { }
}
return true;
}
}
public class User
{
public bool ProblematicTransactions { get; internal set; }
public string Name { get; internal set; }
public long Balance { get; set; }
public bool IsAdministrator { get; set; }
public string PasswordHash { get; internal set; }
public string Salt { get; internal set; }
public List<Transaction> History { get; }
private User()
{
Name = "";
History = new List<Transaction>();
}
public User(User copy) : this()
{
this.ProblematicTransactions = copy.ProblematicTransactions;
this.Name = copy.Name;
this.Balance = copy.Balance;
this.IsAdministrator = copy.IsAdministrator;
this.PasswordHash = copy.PasswordHash;
this.Salt = copy.Salt;
this.History.AddRange(copy.History);
}
public User(string name, string passHash, string salt, long balance, bool generatePass = false, List<Transaction> transactionHistory = null, bool admin = false)
: this(name, passHash, Encoding.UTF8.GetBytes(salt), balance, generatePass, transactionHistory, admin)
{ }
public User(string name, string passHash, byte[] salt, long balance, bool generatePass = false, List<Transaction> transactionHistory = null, bool admin = false)
{
History = transactionHistory ?? new List<Transaction>();
Balance = balance;
Name = name;
IsAdministrator = admin;
Salt = Convert.ToBase64String(salt);
PasswordHash = generatePass ? Convert.ToBase64String(KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(passHash), Encoding.UTF8.GetBytes(Salt), 8192, 320)) : passHash;
}
public bool Authenticate(string password)
=> Convert.ToBase64String(KDF.PBKDF2(KDF.HMAC_SHA1, Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(Salt), 8192, 320)).Equals(PasswordHash);
public User AddTransaction(Transaction tx)
{
History.Add(tx);
return this;
}
private Entry Serialize()
{
Entry root = new Entry("User")
.AddNested(new Entry("Name", Name))
.AddNested(new Entry("Balance", Balance.ToString()).AddAttribute("omit", "true"));
foreach (var transaction in History)
{
Entry tx =
new Entry("Transaction")
.AddAttribute("omit", "true")
.AddNested(new Entry(transaction.to.Equals(Name) ? "From" : "To", transaction.to.Equals(Name) ? transaction.from : transaction.to))
.AddNested(new Entry("Balance", transaction.amount.ToString()));
if (transaction.meta != null) tx.AddNested(new Entry("Meta", transaction.meta));
root.AddNested(tx);
}
return root;
}
internal static User Parse(Entry e, Database db)
{
if (!e.Name.Equals("User")) return null;
User user = new User();
foreach (var entry in e.NestedEntries)
{
if (entry.Name.Equals("Name")) user.Name = entry.Text;
else if (entry.Name.Equals("Balance")) user.Balance = long.TryParse(entry.Text, out long l) ? l : 0;
else if (entry.Name.Equals("Transaction"))
{
string from = null;
string to = null;
long amount = -1;
string meta = "";
foreach (var e1 in entry.NestedEntries)
{
if (e1.Name.Equals("To")) to = e1.Text;
else if (e1.Name.Equals("From")) from = e1.Text;
else if (e1.Name.Equals("Balance")) amount = long.TryParse(e1.Text, out amount) ? amount : 0;
else if (e1.Name.Equals("Meta")) meta = e1.Text;
}
if ((from == null && to == null) || (from != null && to != null) || amount <= 0) user.ProblematicTransactions = true;
else user.History.Add(new Transaction(from, to, amount, meta));
}
else if (entry.Name.Equals("Password")) user.PasswordHash = entry.Text;
else if (entry.Name.Equals("Salt")) user.Salt = entry.Text;
}
if (user.Name == null || user.Name.Length == 0 || user.PasswordHash == null || user.Salt == null || user.PasswordHash.Length==0 || user.Salt.Length==0) return null;
if (user.Balance < 0) user.Balance = 0;
// Populate transaction names
foreach (var transaction in user.History)
if (transaction.from == null) transaction.from = user.Name;
else if (transaction.to == null) transaction.to = user.Name;
return user;
}
public Transaction CreateTransaction(User recipient, long amount, string message = null) => new Transaction(this.Name, recipient.Name, amount, message);
public override bool Equals(object obj) => obj is User && ((User)obj).Name.Equals(Name);
public override int GetHashCode()
{
return 539060726 + EqualityComparer<string>.Default.GetHashCode(Name);
}
}
public class Transaction
{
public string from;
public string to;
public long amount;
public string meta;
public Transaction(string from, string to, long amount, string meta)
{
this.from = from;
this.to = to;
this.amount = amount;
this.meta = meta;
}
public User GetTxToUser(Database db) => db.FirstUser(u => u.Name.Equals(to));
public User GetTxFromUser(Database db) => db.FirstUser(u => u.Name.Equals(from));
}
}
}

122
Server/Program.cs Normal file
View File

@ -0,0 +1,122 @@
using Common;
using Server.Properties;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Tofvesson.Crypto;
namespace Server
{
class Program
{
static void Main(string[] args)
{
Console.SetError(new TimeStampWriter(Console.Error, "HH:mm:ss.fff"));
Console.SetOut(new TimeStampWriter(Console.Out, "HH:mm:ss.fff"));
SessionManager manager = new SessionManager(120 * TimeSpan.TicksPerSecond, 20);
Database db = new Database("BankDB", "Resources");
//Database.User me = db.GetUser("Gabriel Tofvesson");//new Database.User("Gabriel Tofvesson", "Hello, World", "NoRainbow", 1337, true, null, true);
CryptoRandomProvider random = new CryptoRandomProvider();
RSA rsa = null;// new RSA(Resources.e_0x200, Resources.n_0x200, Resources.d_0x200);
if (rsa == null)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine("No RSA keys available! Server identity will not be verifiable!");
Console.ForegroundColor = ConsoleColor.Gray;
Console.WriteLine("Generating session-specific RSA-keys...");
rsa = new RSA(64, 8, 8, 5);
Console.WriteLine("Done!");
}
NetServer server = new NetServer(
rsa,
80,
(string r, Dictionary<string, string> associations, ref bool s) =>
{
string[] cmd = ParseCommand(r, out long id);
// Perform a signature verification by signing a nonce
switch (cmd[0])
{
case "Auth":
{
int idx = cmd[1].IndexOf(':');
if (idx == -1) return GenerateResponse(id, "ERROR");
string user = cmd[1].Substring(0, idx);
string pass = cmd[1].Substring(idx + 1);
Database.User usr = db.GetUser(user);
if (usr == null || !usr.Authenticate(pass))
{
Console.WriteLine("Authentcation failure for user: "+user);
return GenerateResponse(id, "ERROR");
}
string sess = manager.GetSession(usr, "ERROR");
Console.WriteLine("Authentication success for user: "+user+"\nSession: "+sess);
associations["session"] = sess;
return GenerateResponse(id, sess);
}
case "Logout":
manager.Expire(cmd[1]);
Console.WriteLine("Prematurely expired session: "+cmd[1]);
break;
case "Reg":
{
int idx = cmd[1].IndexOf(':');
if (idx == -1) return GenerateResponse(id, "ERROR");
string user = cmd[1].Substring(0, idx);
string pass = cmd[1].Substring(idx + 1);
if (db.ContainsUser(user)) return GenerateResponse(id, "ERROR");
Database.User u = new Database.User(user, pass, random.GetBytes(Math.Abs(random.NextShort() % 60) + 20), 0, true);
db.AddUser(u);
string sess = manager.GetSession(u, "ERROR");
Console.WriteLine("Registered account: " + u.Name + "\nSession: "+sess);
associations["session"] = sess;
return GenerateResponse(id, sess);
}
default:
return GenerateResponse(id, "ERROR");
}
return null;
},
(c, b) =>
{
Console.WriteLine($"Client has {(b ? "C" : "Disc")}onnected");
//if(!b && c.assignedValues.ContainsKey("session"))
// manager.Expire(c.assignedValues["session"]);
});
server.StartListening();
Console.ReadLine();
server.StopRunning();
}
private static string[] ParseCommand(string cmd, out long id)
{
int idx = cmd.IndexOf(':'), idx1;
string sub;
if (idx == -1 || !(sub = cmd.Substring(idx + 1)).Contains(':') || !long.TryParse(sub.Substring(0, idx1 = sub.IndexOf(':')), out id))
{
id = 0;
return null;
}
return new string[] { cmd.Substring(0, idx), sub.Substring(idx1 + 1) };
}
private static string GenerateResponse(long id, bool b) => GenerateResponse(id, b.ToString());
private static string GenerateResponse(long id, int b) => GenerateResponse(id, b.ToString());
private static string GenerateResponse(long id, long b) => GenerateResponse(id, b.ToString());
private static string GenerateResponse(long id, float b) => GenerateResponse(id, b.ToString());
private static string GenerateResponse(long id, double b) => GenerateResponse(id, b.ToString());
private static string GenerateResponse(long id, string response) => id + ":" + response;
}
}

View File

@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Server")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Server")]
[assembly: AssemblyCopyright("Copyright © 2018")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("b458552a-5884-4b27-ba6b-826bc5590106")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]

93
Server/Properties/Resources.Designer.cs generated Normal file
View File

@ -0,0 +1,93 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Server.Properties {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Server.Properties.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] d_0x200 {
get {
object obj = ResourceManager.GetObject("d_0x200", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] e_0x200 {
get {
object obj = ResourceManager.GetObject("e_0x200", resourceCulture);
return ((byte[])(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] n_0x200 {
get {
object obj = ResourceManager.GetObject("n_0x200", resourceCulture);
return ((byte[])(obj));
}
}
}
}

View File

@ -0,0 +1,130 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="d_0x200" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\0x200.d;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="e_0x200" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\0x200.e;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
<data name="n_0x200" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>..\Resources\0x200.n;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>

BIN
Server/Resources/0x200.e Normal file

Binary file not shown.

BIN
Server/Resources/0x200.n Normal file

Binary file not shown.

74
Server/Server.csproj Normal file
View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B458552A-5884-4B27-BA6B-826BC5590106}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>Server</RootNamespace>
<AssemblyName>Server</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="Database.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="SessionManager.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="Resources\0x200.d" />
<None Include="Resources\0x200.e" />
<None Include="Resources\0x200.n" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj">
<Project>{23eb87d4-e310-48c4-a931-0961c83892d7}</Project>
<Name>Common</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>

120
Server/SessionManager.cs Normal file
View File

@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using Tofvesson.Crypto;
namespace Server
{
public sealed class SessionManager
{
private static readonly RandomProvider random = new RegularRandomProvider();
private readonly List<Session> sessions = new List<Session>();
private readonly long timeout;
private readonly int sidLength;
public SessionManager(long timeout, int sidLength = 10)
{
this.timeout = timeout;
this.sidLength = sidLength < 10 ? 10 : sidLength;
}
public string GetSession(Database.User user, string invalidSID)
{
Update();
for (int i = 0; i < sessions.Count; ++i)
if (sessions[i].user.Equals(user))
return sessions[i].sessionID;
Session s = new Session
{
sessionID = GenerateRandomSID(invalidSID),
user = user,
expiry = DateTime.Now.Ticks + timeout
};
sessions.Add(s);
return s.sessionID;
}
public bool Refresh(Database.User user)
{
Update();
for (int i = sessions.Count - 1; i >= 0; --i)
if (sessions[i].user.Equals(user))
{
Session s = sessions[i];
s.expiry = DateTime.Now.Ticks + timeout;
sessions[i] = s;
return true;
}
return false;
}
public void Expire(Database.User user)
{
Update();
for (int i = sessions.Count - 1; i >= 0; --i)
if (sessions[i].user.Equals(user))
{
sessions.RemoveAt(i);
return;
}
return;
}
public bool Refresh(string sid)
{
Update();
for (int i = sessions.Count - 1; i >= 0; --i)
if (sessions[i].sessionID.Equals(sid))
{
Session s = sessions[i];
s.expiry = DateTime.Now.Ticks + timeout;
sessions[i] = s;
return true;
}
return false;
}
public void Expire(string sid)
{
Update();
for (int i = sessions.Count - 1; i >= 0; --i)
if (sessions[i].sessionID.Equals(sid))
{
sessions.RemoveAt(i);
return;
}
return;
}
public bool CheckSession(string sid, Database.User user)
{
foreach (var session in sessions)
if (session.sessionID.Equals(sid) && session.user.Equals(user))
return true;
return false;
}
public void Update()
{
for(int i = sessions.Count - 1; i>=0; --i)
if (sessions[i].expiry < DateTime.Now.Ticks)
sessions.RemoveAt(i);
}
private string GenerateRandomSID(string invalid)
{
string res;
do res = random.NextString(sidLength);
while (res.Equals(invalid));
return res;
}
}
public struct Session
{
public string sessionID;
public Database.User user;
public long expiry; // Measured in ticks
}
}