mirror of
https://github.com/XFox111/SimpleOTP.git
synced 2026-04-22 08:00:45 +03:00
Initial commit
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Description
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
### Reproduction steps
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
### Expected behavior
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
### Screenshots
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
### Environment
|
||||
Please complete the following information:
|
||||
- Platform: [e.g. Xamarin.Forms 5]
|
||||
- OS: [e.g. Windows 10 1909 (10.0.18363)]
|
||||
- Package version: [e.g. 1.1]
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at opensource@xfox111.net. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
# Contribution Guidelines
|
||||
Welcome, and thank you for your interest in contributing to my project!
|
||||
|
||||
There are many ways in which you can contribute, beyond writing code. The goal of this document is to provide a high-level overview of how you can get involved.
|
||||
|
||||
## Table of Contents
|
||||
- [Contribution Guidelines](#contribution-guidelines)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Asking Questions](#asking-questions)
|
||||
- [Providing Feedback](#providing-feedback)
|
||||
- [Reporting Issues](#reporting-issues)
|
||||
- [Look For an Existing Issue](#look-for-an-existing-issue)
|
||||
- [Writing Good Bug Reports and Feature Requests](#writing-good-bug-reports-and-feature-requests)
|
||||
- [Final Checklist](#final-checklist)
|
||||
- [Follow Your Issue](#follow-your-issue)
|
||||
- [Contributing to codebase](#contributing-to-codebase)
|
||||
- [Build and run project](#build-and-run-project)
|
||||
- [Development workflow](#development-workflow)
|
||||
- [Release](#release)
|
||||
- [Coding guidelines](#coding-guidelines)
|
||||
- [Indentation](#indentation)
|
||||
- [Names](#names)
|
||||
- [Comments](#comments)
|
||||
- [Strings](#strings)
|
||||
- [Style](#style)
|
||||
- [Finding an issue to work on](#finding-an-issue-to-work-on)
|
||||
- [Contributing to translations](#contributing-to-translations)
|
||||
- [Submitting pull requests](#submitting-pull-requests)
|
||||
- [Spell check errors](#spell-check-errors)
|
||||
- [Thank You!](#thank-you)
|
||||
- [Attribution](#attribution)
|
||||
|
||||
## Asking Questions
|
||||
Have a question? Rather than opening an issue, please ask me directly on opensource@xfox111.net.
|
||||
|
||||
## Providing Feedback
|
||||
Your comments and feedback are welcome.
|
||||
You can leave your feedbak on feedback@xfox111.net or on [Feedbacks and reviews](https://github.com/XFox111/SimpleOTP/discussions/3) thread on [GitHub Discussions](https://github.com/XFox111/SimpleOTP/discussions/)
|
||||
|
||||
## Reporting Issues
|
||||
Have you identified a reproducible problem in the application? Have a feature request? I'd like to hear it! Here's how you can make reporting your issue as effective as possible.
|
||||
|
||||
### Look For an Existing Issue
|
||||
Before you create a new issue, please do a search in [open issues](https://github.com/xfox111/gutschedule/issues) to see if the issue or feature request has already been filed.
|
||||
|
||||
Be sure to scan through the [feature requests](https://github.com/XFox111/GUTSchedule/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement).
|
||||
|
||||
If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment:
|
||||
|
||||
* 👍 - upvote
|
||||
* 👎 - downvote
|
||||
|
||||
If you cannot find an existing issue that describes your bug or feature, create a new issue using the guidelines below.
|
||||
|
||||
### Writing Good Bug Reports and Feature Requests
|
||||
File a single issue per problem and feature request. Do not enumerate multiple bugs or feature requests in the same issue.
|
||||
|
||||
Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes.
|
||||
|
||||
The more information you can provide, the more likely someone will be successful at reproducing the issue and finding a fix.
|
||||
|
||||
Please include the following with each issue:
|
||||
- Current version of the package
|
||||
- Target platform info (name and version)
|
||||
- IDE you use
|
||||
- Reproducible steps (1... 2... 3...) that cause the issue
|
||||
- What you expected to see, versus what you actually saw
|
||||
- Images, animations, or a link to a video showing the issue occurring
|
||||
|
||||
### Final Checklist
|
||||
Please remember to do the following:
|
||||
- [X] Search the issue repository to ensure your report is a new issue
|
||||
- [X] Separate issues reports
|
||||
- [X] Include as much information as you can to your report
|
||||
|
||||
Don't feel bad if the developers can't reproduce the issue right away. They will simply ask for more information!
|
||||
|
||||
### Follow Your Issue
|
||||
Once your report is submitted, be sure to stay in touch with the devs in case they need more help from you.
|
||||
|
||||
## Contributing to codebase
|
||||
If you are interested in writing code to fix issues or implement new awesome features you can follow this guidelines to get a better result
|
||||
|
||||
### Build and run project
|
||||
1. Clone repository to local storage using [Git command prompt](https://guides.github.com/introduction/git-handbook/) or [Visual Studio](https://docs.microsoft.com/en-us/visualstudio/get-started/tutorial-open-project-from-repo?view=vs-2019)
|
||||
- Git clone command:
|
||||
```
|
||||
git clone https://github.com/xfox111/SimpleOTP.git
|
||||
```
|
||||
2. Open `SimpleOTP.sln` using [Microsoft Visual Studio](https://visualstudio.microsoft.com/) 2019 or later, or open repository folder with [Visual Studio Code](https://code.visualstudio.com/) (or with any other tool you use)
|
||||
- Make sure you have properly installed and congigured [.NET 5 SDK](https://dotnet.microsoft.com/)
|
||||
3. Press "Build Soulution" in Visual Studio or run `dotnet build` command from a terminal prompt if you are using VS Code
|
||||
4. Open "Test Explorer" in VS or run `dotnet test` in VS Code to run unit tests
|
||||
|
||||
### Development workflow
|
||||
This section represents how contributors should interact with codebase implementing features and fixing bugs
|
||||
1. Getting assigned to the issue
|
||||
2. Creating a repository fork
|
||||
3. Making changes to the codebase
|
||||
5. Creating a pull request to `master`
|
||||
6. Code review
|
||||
7. Completing PR
|
||||
8. Creating a release
|
||||
9. Done!
|
||||
|
||||
### Coding guidelines
|
||||
#### Indentation
|
||||
We use tabs, not spaces.
|
||||
|
||||
#### Names
|
||||
The project naming rules inherit [.NET Naming Guidelines](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/naming-guidelines). Nevertheless there're some distinctions with the guidelines as well as additions to those ones:
|
||||
- Use `camelCase` for fields instead of `CamelCase` stated in [Capitalization Conventions](https://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/capitalization-conventions#capitalization-rules-for-identifiers)
|
||||
- Private fields for properties should always start with underscore `_`
|
||||
- Wrong:
|
||||
```
|
||||
private int year = 1984;
|
||||
public int Year
|
||||
{
|
||||
get => year;
|
||||
set => year = value;
|
||||
}
|
||||
```
|
||||
- Correct:
|
||||
```
|
||||
private int _year = 1984;
|
||||
public int Year
|
||||
{
|
||||
get => _year;
|
||||
set => _year = value;
|
||||
}
|
||||
```
|
||||
> **Note:** underscores `_` before generic **private** fields are allowed but not recommended
|
||||
- Use `PascalCase` for file names
|
||||
|
||||
#### Comments
|
||||
Read [XML documentation comments](https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/xmldoc/) and try to use all stated methods. Remember: the more detailed documentation your code has the less programmers will curse you in the future
|
||||
|
||||
#### Strings
|
||||
Use "double quotes" wherever it's possible
|
||||
#### Style
|
||||
- Prefer to use lambda functions
|
||||
- Wrong:
|
||||
```
|
||||
button.Click += Button_Click;
|
||||
...
|
||||
private void Button_Click (object sender, RoutedEventArgs e)
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
}
|
||||
```
|
||||
```
|
||||
public void Main ()
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
}
|
||||
```
|
||||
- Correct:
|
||||
```
|
||||
button.Click += (s, e) => Console.WriteLine("Hello, World!");
|
||||
```
|
||||
```
|
||||
public void Main () =>
|
||||
Console.WriteLine("Hello, World!");
|
||||
```
|
||||
- Put curly braces on new lines
|
||||
- Wrong:
|
||||
```
|
||||
if (condition) {
|
||||
...
|
||||
}
|
||||
```
|
||||
- Correct:
|
||||
```
|
||||
if (condition)
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
- Put spaces between operators and before braces in methods declarations, conditionals and loops
|
||||
- Wrong:
|
||||
- `y=k*x+b`
|
||||
- `public void Main()`
|
||||
- Correct:
|
||||
- `y = k * x + b`
|
||||
- `public void Main ()`
|
||||
- Put `private` keyword even though it's unnecessary
|
||||
- Wrong: `void Main ()`
|
||||
- Correct: `private void Main ()`
|
||||
- Use interpolated strings and ternary conditionals wherever it's possible
|
||||
- Wrong:
|
||||
- `string s = a + "; " + b`, `string s = string.Format("{0}; {1:00}", a, b)`
|
||||
- ```
|
||||
string s;
|
||||
if (condition)
|
||||
s = "Life";
|
||||
else
|
||||
s = "Death"
|
||||
```
|
||||
- Correct:
|
||||
- `string s = $"{a}; {b:00}"`
|
||||
- `string s = condition ? "Life" : "Death"`
|
||||
- Do not surround loop and conditional bodies with curly braces if they can be avoided
|
||||
- Wrong:
|
||||
```
|
||||
if (condition)
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
```
|
||||
- Correct
|
||||
```
|
||||
if (condition)
|
||||
Console.WriteLine("Hello, World!");
|
||||
else
|
||||
return;
|
||||
```
|
||||
- Use `#region` tags to separate code blocks (e.g. properties, methods, contructors, etc.)
|
||||
|
||||
### Finding an issue to work on
|
||||
Check out the [full issues list](https://github.com/XFox111/GUTSchedule/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue) for a list of all potential areas for contributions. **Note** that just because an issue exists in the repository does not mean we will accept a contribution to the core editor for it. There are several reasons we may not accept a pull request like:
|
||||
|
||||
- Performance - One of the project's core values is to deliver a lightweight librayr, that means it should perform well in both real and perceived performance.
|
||||
- Architectural - Feature owner needs to agree with any architectural impact a change may make. Such things must be discussed with and agreed upon by the feature owner.
|
||||
|
||||
To improve the chances to get a pull request merged you should select an issue that is labelled with the `help-wanted` or `bug` labels. If the issue you want to work on is not labelled with `help-wanted` or `bug`, you can start a conversation with the issue owner asking whether an external contribution will be considered.
|
||||
|
||||
To avoid multiple pull requests resolving the same issue, let others know you are working on it by saying so in a comment.
|
||||
|
||||
### Submitting pull requests
|
||||
To enable us to quickly review and accept your pull requests, always create one pull request per issue and [link the issue in the pull request](https://github.com/blog/957-introducing-issue-mentions). Never merge multiple requests in one unless they have the same root cause. Be sure to follow our [Coding Guidelines](#coding-guidelines) and keep code changes as small as possible. Avoid pure formatting changes to code that has not been modified otherwise. Pull requests should contain tests whenever possible. Fill pull request content according to its template. Deviations from template are not recommended
|
||||
|
||||
#### Spell check errors
|
||||
Pull requests that fix spell check errors are welcomed but please make sure it doesn't touch multiple feature areas, otherwise it will be difficult to review.
|
||||
|
||||
## Thank You!
|
||||
Your contributions to open source, large or small, make great projects like this possible. Thank you for taking the time to contribute.
|
||||
|
||||
## Attribution
|
||||
This Contribution Guidelines are adapted from the [Contributing to VS Code](https://github.com/microsoft/vscode/blob/master/CONTRIBUTING.md)
|
||||
@@ -1,2 +1,96 @@
|
||||
# SimpleOTP
|
||||
[](https://github.com/xfox111/SimpleOTP/releases/latest)
|
||||
|
||||
[](https://github.com/xfox111/SimpleOTP/issues)
|
||||
[](https://github.com/xfox111/SimpleOTP/commits/master)
|
||||
[](https://github.com/xfox111/SimpleOTP)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
[](https://twitter.com/xfox111)
|
||||
[](https://github.com/xfox111)
|
||||
[](https://buymeacoffee.com/xfox111)
|
||||
|
||||
.NET library for TOTP/HOTP implementation on server (ASP.NET) or client (Xamarin) side
|
||||
|
||||
## Features
|
||||
- Generate and validate OTP codes
|
||||
- Support of [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_password) (RFC 6238) and [HOTP](https://en.wikipedia.org/wiki/HMAC-based_one-time_password) (RFC 4226) algorithms
|
||||
- Support of HMAC-SHA1, HMAC-SHA256 and HMAC-SHA512 hashing algorithms
|
||||
- Setup URI parser
|
||||
- Database-ready configuration models
|
||||
- Configuration generator for server-side implementation
|
||||
- QR code generator
|
||||
- No dependencies
|
||||
|
||||

|
||||
##### By Mateusz Adamowski, taken with Canon EOS. - Own work, CC BY-SA 1.0, https://commons.wikimedia.org/w/index.php?curid=142232
|
||||
|
||||
## Usage
|
||||
### Generate code
|
||||
```csharp
|
||||
string sample_config_uri = "otpauth://totp/FoxDev%20Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev%20Studio";
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration(sample_config_uri);
|
||||
// OTPModel { Id = af2358b0-3f69-4dd7-9537-32c07d6663aa, Type = TOTP, IssuerLabel = FoxDev Studio, AccountName = eugene@xfox111.net, Secret = ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH, Issuer = FoxDev Studio, Algorithm = SHA1, Digits = 6, Counter = 0, Period = 00:00:30 }
|
||||
|
||||
OTPCode code = OTPService.GenerateCode(ref config);
|
||||
// OTPasswordModel { Code = 350386, Expiring = 23-May-21 06:08:30 PM }
|
||||
```
|
||||
|
||||
### Validate code
|
||||
```csharp
|
||||
int codeToValidate = 350386;
|
||||
bool isValid = OTPService.ValidateCode(codeToValidate, config, TimeSpan.FromSeconds(30)); // True
|
||||
```
|
||||
|
||||
### Generate setup config
|
||||
```csharp
|
||||
OTPConfiguration config = OTPConfiguration.GenerateConfiguration("FoxDev Studio", "eugene@xfox111.net");
|
||||
// OTPModel { Id = af2358b0-3f69-4dd7-9537-32c07d6663aa, Type = TOTP, IssuerLabel = FoxDev Studio, AccountName = eugene@xfox111.net, Secret = ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH, Issuer = FoxDev Studio, Algorithm = SHA1, Digits = 6, Counter = 0, Period = 00:00:30 }
|
||||
|
||||
Uri uri = config.GetUri(); // otpauth://totp/FoxDev%20Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev%20Studio
|
||||
string qrCode = config.GetQrImage(300); // data:image/png;base64,...
|
||||
```
|
||||
|
||||
### Streamline code generation for client
|
||||
```csharp
|
||||
OTPFactory factory = new (config);
|
||||
|
||||
factory.CodeUpdated += (newCode) => Console.WriteLine(newCode);
|
||||
// OTPasswordModel { Code = 350386, Expiring = 23-May-21 06:08:30 PM }
|
||||
factory.PropertyChanged += (sender, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(factory.TimeLeft))
|
||||
Console.WriteLine(factory.TimeLeft);
|
||||
else
|
||||
Console.WriteLine(factory.Code);
|
||||
}
|
||||
...
|
||||
factory.Dispose();
|
||||
|
||||
```
|
||||
|
||||
## Download
|
||||
- [NuGet Gallery](https://www.nuget.org/packages/SimpleOTP)
|
||||
- [GitHub Releases](https://github.com/xfox111/SimpleOTP/releases/latest)
|
||||
|
||||
## Contributing
|
||||
There are many ways in which you can participate in the project, for example:
|
||||
- [Submit bugs and feature requests](https://github.com/xfox111/SimpleOTP/issues), and help us verify as they are checked in
|
||||
- Review [source code changes](https://github.com/xfox111/SimpleOTP/pulls)
|
||||
- Review documentation and make pull requests for anything from typos to new content
|
||||
|
||||
If you are interested in fixing issues and contributing directly to the code base, please see the [Contribution Guidelines](https://github.com/XFox111/SimpleOTP/blob/master/CONTRIBUTING.md), which covers the following:
|
||||
- [How to deploy the extension on your browser](https://github.com/XFox111/SimpleOTP/blob/master/CONTRIBUTING.md#deploy-test-version-on-your-browser)
|
||||
- [The development workflow](https://github.com/XFox111/SimpleOTP/blob/master/CONTRIBUTING.md#development-workflow), including debugging and running tests
|
||||
- [Coding guidelines](https://github.com/XFox111/SimpleOTP/blob/master/CONTRIBUTING.md#coding-guidelines)
|
||||
- [Submitting pull requests](https://github.com/XFox111/SimpleOTP/blob/master/CONTRIBUTING.md#submitting-pull-requests)
|
||||
- [Finding an issue to work on](https://github.com/XFox111/SimpleOTP/blob/master/CONTRIBUTING.md#finding-an-issue-to-work-on)
|
||||
- [Contributing to translations](https://github.com/XFox111/SimpleOTP/blob/master/CONTRIBUTING.md#contributing-to-translations)
|
||||
|
||||
## Code of Conduct
|
||||
This project has adopted the Contributor Covenant. For more information see the [Code of Conduct](https://github.com/XFox111/SimpleOTP/blob/master/CODE_OF_CONDUCT.md)
|
||||
|
||||
## Copyrights
|
||||
> ©2021 Michael "XFox" Gordeev
|
||||
|
||||
Licensed under [MIT License](https://opensource.org/licenses/MIT)
|
||||
@@ -0,0 +1,22 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* This file is used by Code Analysis to maintain SuppressMessage
|
||||
* attributes that are applied to this project.
|
||||
* Project-level suppressions either have no target or are given
|
||||
* a specific target and scoped to a namespace, type, member, etc.
|
||||
*/
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1625:Element documentation should not be copied and pasted", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1519:Braces should not be omitted from multi-line child statement", Justification = "Reviewd by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Reviewd by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP.Test")]
|
||||
@@ -0,0 +1,37 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Helpers;
|
||||
|
||||
namespace SimpleOTP.Test.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for Base32 encoder.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class Base32UnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Test overall work of the encoder.
|
||||
/// </summary>
|
||||
[TestMethod("Overall Base32 encoder test")]
|
||||
public void EncoderTest()
|
||||
{
|
||||
// byte[] bytes = new byte[new Random().Next(128, 161)]; // FIXME: See SimpleOTP.Helpers.Base32Encoder.Encode()
|
||||
byte[] bytes = new byte[160];
|
||||
new Random().NextBytes(bytes);
|
||||
string str = Base32Encoder.Encode(bytes);
|
||||
|
||||
bytes = Base32Encoder.Decode(str);
|
||||
string result = Base32Encoder.Encode(bytes);
|
||||
Assert.AreEqual(str, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Helpers;
|
||||
|
||||
namespace SimpleOTP.Test.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for key generator.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class SecretGeneratorUnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Overall test of key generator.
|
||||
/// </summary>
|
||||
[TestMethod("Overall generator tests")]
|
||||
public void Test_Generator()
|
||||
{
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => SecretGenerator.GenerateSecret(64));
|
||||
Assert.ThrowsException<ArgumentOutOfRangeException>(() => SecretGenerator.GenerateSecret(256));
|
||||
|
||||
string key = SecretGenerator.GenerateSecret();
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(key));
|
||||
|
||||
key = SecretGenerator.GenerateSecret(128);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for OTP URI parser.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class UriParserUnitTest
|
||||
{
|
||||
private static readonly Guid TestGuid = Guid.NewGuid();
|
||||
|
||||
private readonly string[] uriParts =
|
||||
{
|
||||
"otpauth",
|
||||
"totp|hotp",
|
||||
"FoxDev%20Studio",
|
||||
"eugene@xfox111.net",
|
||||
"secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
"issuer=FoxDev%20Studio%20Issuer",
|
||||
"algorithm=SHA1|algorithm=SHA512",
|
||||
"digits=8",
|
||||
"period=10",
|
||||
"counter=10000",
|
||||
};
|
||||
|
||||
private readonly OTPConfiguration[] configs =
|
||||
{
|
||||
new ()
|
||||
{
|
||||
AccountName = "eugene@xfox111.net",
|
||||
Algorithm = Enums.Algorithm.SHA512,
|
||||
Digits = 8,
|
||||
Type = Enums.OTPType.TOTP,
|
||||
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
Issuer = "FoxDev Studio Issuer",
|
||||
IssuerLabel = "FoxDev Studio",
|
||||
Id = TestGuid,
|
||||
Period = TimeSpan.FromSeconds(10),
|
||||
},
|
||||
new ()
|
||||
{
|
||||
AccountName = "eugene@xfox111.net",
|
||||
Algorithm = Enums.Algorithm.SHA512,
|
||||
Digits = 8,
|
||||
Type = Enums.OTPType.HOTP,
|
||||
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
Issuer = "FoxDev Studio Issuer",
|
||||
IssuerLabel = "FoxDev Studio",
|
||||
Id = TestGuid,
|
||||
Counter = 10000,
|
||||
},
|
||||
new ()
|
||||
{
|
||||
AccountName = "eugene@xfox111.net",
|
||||
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
Issuer = "FoxDev Studio",
|
||||
IssuerLabel = "FoxDev Studio",
|
||||
Id = TestGuid,
|
||||
},
|
||||
new ()
|
||||
{
|
||||
AccountName = "eugene@xfox111.net",
|
||||
Type = Enums.OTPType.HOTP,
|
||||
Secret = "ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH",
|
||||
Issuer = "FoxDev Studio",
|
||||
IssuerLabel = "FoxDev Studio",
|
||||
Id = TestGuid,
|
||||
Counter = 10000,
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with full TOTP URI.
|
||||
/// </summary>
|
||||
[TestMethod("Valid full URI (TOTP)")]
|
||||
public void ParseValidUri_FullFormed_Totp()
|
||||
{
|
||||
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[5]}&{uriParts[6].Split('|')[1]}&{uriParts[7]}&{uriParts[8]}";
|
||||
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[0], config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with full HOTP URI.
|
||||
/// </summary>
|
||||
[TestMethod("Valid full URI (HOTP)")]
|
||||
public void ParseValidUri_FullFormed_Hotp()
|
||||
{
|
||||
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[5]}&{uriParts[6].Split('|')[1]}&{uriParts[7]}&{uriParts[9]}";
|
||||
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[1], config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with TOTP URI. Minimal parameter set.
|
||||
/// </summary>
|
||||
[TestMethod("Valid minimal URI (TOTP)")]
|
||||
public void ParseValidUri_Minimal_Totp()
|
||||
{
|
||||
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}";
|
||||
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[2], config);
|
||||
|
||||
config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri($"otpauth://totp/eugene@xfox111.net?{uriParts[4]}&{uriParts[5]}&algorithm=SHA256"));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[2] with { IssuerLabel = "FoxDev Studio Issuer", Issuer = "FoxDev Studio Issuer", Algorithm = Enums.Algorithm.SHA256 }, config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with HOTP URI. Minimal parameter set.
|
||||
/// </summary>
|
||||
[TestMethod("Valid minimal URI (HOTP)")]
|
||||
public void ParseValidUri_Minimal_Hotp()
|
||||
{
|
||||
string uri = $"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}";
|
||||
var config = SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri));
|
||||
Assert.IsNotNull(config);
|
||||
config.Id = TestGuid;
|
||||
Assert.AreEqual(configs[3], config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test parser with invalid OTP URIs.
|
||||
/// </summary>
|
||||
[TestMethod("Invalid URI")]
|
||||
public void ParseInvalidUris()
|
||||
{
|
||||
string[] uris =
|
||||
{
|
||||
$"https://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://otp/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://{uriParts[2]}:{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[3]}?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/?{uriParts[4]}&{uriParts[9]}",
|
||||
$"{uriParts[0]}://{uriParts[1].Split('|')[0]}/{uriParts[2]}:{uriParts[3]}",
|
||||
$"{uriParts[0]}://{uriParts[1].Split('|')[1]}/{uriParts[2]}:{uriParts[3]}?{uriParts[4]}",
|
||||
};
|
||||
foreach (string uri in uris)
|
||||
Assert.ThrowsException<ArgumentException>(() => SimpleOTP.Helpers.UriParser.ParseUri(new Uri(uri)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for OTP code model.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class OTPCodeUnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Get formatted OTP code.
|
||||
/// </summary>
|
||||
[TestMethod("Code formatting")]
|
||||
public void Test_GetFullCode()
|
||||
{
|
||||
OTPCode code = new ()
|
||||
{
|
||||
Code = 123,
|
||||
Expiring = DateTime.UtcNow.AddSeconds(30),
|
||||
};
|
||||
|
||||
Assert.AreEqual("000123", code.GetCode());
|
||||
Assert.AreEqual("000 123", code.GetCode("000 000"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for OTP generator configuration model.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class OTPConfigurationUnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Get otpauth link from minimal configuration.
|
||||
/// </summary>
|
||||
[TestMethod("Link generator (short)")]
|
||||
public void TestShortLinkGenerator()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GenerateConfiguration("FoxDev Studio", "eugene@xfox111.net");
|
||||
System.Diagnostics.Debug.WriteLine(config.Id);
|
||||
Uri uri = config.GetUri();
|
||||
Assert.AreEqual($"otpauth://totp/FoxDev+Studio:eugene@xfox111.net?secret={config.Secret}&issuer=FoxDev+Studio", uri.AbsoluteUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get otpauth link from complete configurations (TOTP + HOTP).
|
||||
/// </summary>
|
||||
[TestMethod("Link generator (full)")]
|
||||
public void TestFullLinkGenerator()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration("otpauth://totp/FoxDev%20Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev%20Studio%20Issuer&algorithm=SHA512&digits=8&period=10");
|
||||
Assert.AreEqual($"otpauth://totp/FoxDev+Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev+Studio+Issuer&algorithm=SHA512&digits=8&period=10", config.GetUri().AbsoluteUri);
|
||||
config.Type = Enums.OTPType.HOTP;
|
||||
Assert.AreEqual($"otpauth://hotp/FoxDev+Studio:eugene@xfox111.net?secret=ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH&issuer=FoxDev+Studio+Issuer&algorithm=SHA512&digits=8&counter=0", config.GetUri().AbsoluteUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test user-friendly secret formatting.
|
||||
/// </summary>
|
||||
[TestMethod("Fancy secret")]
|
||||
public void GetFormattedSecret()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
|
||||
Assert.AreEqual($"ESQV TYRM 2CWZ C3NX 24GR RWIA UUWV HWQH", config.GetFancySecret());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test QR code generation for OTP config.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Task"/>.</returns>
|
||||
[TestMethod("QR code image")]
|
||||
public async Task GetQrImage()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
|
||||
string imageStr = await config.GetQrImage();
|
||||
Assert.AreEqual(
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEsCAIAAAD2HxkiAAAABmJLR0QA/wD/AP+gvaeTAAAHOklEQVR4nO3dwW4cKRRAUXs0///" +
|
||||
"LmZ03JVkMDVy6c846XZ04vkJ6ouD7z58/X0Dnn/ovAH87EUJMhBATIcRECDERQkyEEBMhxEQIMRFCTIQQEyHERAgxEUJMhBATIcRECDERQkyEEBMhxEQIMRFC" +
|
||||
"TIQQEyHERAgxEULs3+VP/P7+Xv7Mcc9T/Z9/n5GT/0f+FSPfNWLV32fkyat+GnO3J4w8Z+7PnLT85ggrIcRECDERQkyEEBMhxNZPR5/23UM6N8M8aWSyNzIPX" +
|
||||
"DWNnPvUvnnyKu3v2IushBATIcRECDERQkyEEDsxHX3at8dy1XeNfPvJJ6/aBfr0qRPUk79jL7ISQkyEEBMhxEQIMRFCrJmO3mbf2+Un30kfMTfjPfntq77rjV" +
|
||||
"gJISZCiIkQYiKEmAghZjr69bVzT2N7NunJueLcz/DjJ58jrIQQEyHERAgxEUJMhBBrpqMnZ2Kr3vg+eVvQbWelrvrUybfd32juaiWEmAghJkKIiRBiIoTYiel" +
|
||||
"oe8P43I7KfXs+993bfvJs0lXftern3P6OvchKCDERQkyEEBMhxEQIse832mK3yv33NM19+1M70V315I9nJYSYCCEmQoiJEGIihNj66ejJt8vvv9v9ad9cceRT" +
|
||||
"c27bmXnbZPhFVkKIiRBiIoSYCCEmQojdsne0nWWt+tRJq/6G+36Gq5y82SphJYSYCCEmQoiJEGIihNj6c0dPTq727R1tb5Zf9Wfad/+f9s0n5/6lJ/ff/sJKC" +
|
||||
"DERQkyEEBMhxEQIsVv2jj61b47fNmW97T36+08weLr2V91KCDERQkyEEBMhxEQIsRN31j+195uPuG3H6TvujTx5Au3ct588t/YXVkKIiRBiIoSYCCEmQoideL" +
|
||||
"N+bpp027x0zr5TT1c9Z9+n2vvo3+j3x0oIMRFCTIQQEyHERAix9dPRkyeI7vv7PL3jrUP7ziZddVbq3JNXueRdeyshxEQIMRFCTIQQEyHEmluZVk2l2r1/+3Z" +
|
||||
"mntw7OvLtq/5P2//3fXuYX2QlhJgIISZCiIkQYiKE2L17R/d96uST991HP+dT99/Ofbu9o8DXlwghJ0KIiRBiIoTYiTvrV80DT56rOfKcEe2dRyffbb//tNL2" +
|
||||
"9+cXVkKIiRBiIoSYCCEmQoitn46e3KG374TMVU6e4XnbzPnkbVP33xL1CyshxEQIMRFCTIQQEyHEbtk7uupTc89pZ30j9n3XvlnoyHednN+uYu8ofBoRQkyEE" +
|
||||
"BMhxEQIsfXnjj7tm2WtupN97kahfbcgPbU3Lq168qrvum3q+yIrIcRECDERQkyEEBMhxE5MR1ftlty3g3GVk/sVbztV4LY3/ecm5wkrIcRECDERQkyEEBMhxE" +
|
||||
"5MR5/afX3tPsz2HfmRJ18yM/xx8qb75KdhJYSYCCEmQoiJEGIihFjzZv3JN+JHZmsn528jTr7xvWrGO3c6wZwPmwNbCSEmQoiJEGIihJgIIdbcyvTU3nB08hz" +
|
||||
"LuU+dfCN+zm2nHDzddhbBDyshxEQIMRFCTIQQEyHE1k9Hb7vffOTJT/tulj/585mz6snt/Pbkz+dFVkKIiRBiIoSYCCEmQog1544+rbo1fu7JJ28UOnkr+qr7" +
|
||||
"jOaePOLkRPfk/uT/xUoIMRFCTIQQEyHERAixE3tHn9pb40/uHT15oun9b6nve86IfecevMhKCDERQkyEEBMhxEQIsfV7R/fdmNO+333yyau+fd/OzH37MJOTP" +
|
||||
"3/hznr4fCKEmAghJkKIiRBiza1M77jLceTJ7c1Nc59qz4Dd55L76EdYCSEmQoiJEGIihJgIIfZOtzKNfNeIVbsTT04IT94I354G0M7SE1ZCiIkQYiKEmAghJk" +
|
||||
"KIndg7OuK28ydv2+E596mTk8b2RviTM/nlrIQQEyHERAgxEUJMhBB77zfr23Ms798BO/ecpze6//1/uWTnqpUQYiKEmAghJkKIiRBit+wd3efaHYO/eMeb7vd" +
|
||||
"NUD/+BigrIcRECDERQkyEEBMhxNbfWX/bXTx/841LI59a5eRu0rl/+233Rv2wEkJMhBATIcRECDERQmz9dPTp/rMuV83o5rzjeaHtm/WrfhqX7Bm2EkJMhBAT" +
|
||||
"IcRECDERQuzEdPTp5ERu7tvbe5rmnvMZM9Xnk/ft8Gx/D39YCSEmQoiJEGIihJgIIdZMR0/aty90ZBp58u6kk/PSk2/EP+2bsiZv31sJISZCiIkQYiKEmAgh9" +
|
||||
"vnT0VVG5mbt7fOrrLrHatV+18/Yp/oLKyHERAgxEUJMhBATIcSa6egl5z3+mJvI7Zv17ZuX3rbfdd9z3FkPjBIhxEQIMRFCTIQQOzEdveRm8B8ndzDef9/TyL" +
|
||||
"fv2yk68pyn9n9nOSshxEQIMRFCTIQQEyHEvm/bxgl/GyshxEQIMRFCTIQQEyHERAgxEUJMhBATIcRECDERQkyEEBMhxEQIMRFCTIQQEyHERAgxEUJMhBATIcR" +
|
||||
"ECDERQkyEEBMhxEQIsf8A2z+aWL5SDQEAAAAASUVORK5CYII=", imageStr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test
|
||||
{
|
||||
/// <summary>
|
||||
/// OTP factory class unit-tests.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class OTPFactoryUnitTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Complex test of OTP factory.
|
||||
/// </summary>
|
||||
/// <returns><see cref="Task"/>.</returns>
|
||||
[TestMethod("Overall factory test")]
|
||||
public async Task TestFactory()
|
||||
{
|
||||
OTPConfiguration config = OTPConfiguration.GetConfiguration("ESQVTYRM2CWZC3NX24GRRWIAUUWVHWQH", "FoxDev Studio", "eugene@xfox111.net");
|
||||
config.Period = TimeSpan.FromSeconds(3);
|
||||
using OTPFactory factory = new (config, 1500);
|
||||
System.Diagnostics.Debug.WriteLine(factory.Configuration);
|
||||
var code = factory.CurrentCode;
|
||||
|
||||
factory.Configuration = config;
|
||||
factory.CurrentCode = code;
|
||||
|
||||
await Task.Delay(3500);
|
||||
Assert.AreNotEqual(code.Code, factory.CurrentCode.Code);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Test
|
||||
{
|
||||
/// <summary>
|
||||
/// Unit-tests for OTP generator.
|
||||
/// </summary>
|
||||
[TestClass]
|
||||
public class OTPServiceUnitTest
|
||||
{
|
||||
private readonly DateTime time = new (2021, 5, 28, 10, 47, 50, DateTimeKind.Utc);
|
||||
private OTPConfiguration totpConfig = OTPConfiguration.GetConfiguration(new Uri("otpauth://totp/FoxDev%20Studio:eugene@xfox111.net?secret=JBSWY3DPEHPK3PXP&issuer=FoxDev%20Studio"));
|
||||
private OTPConfiguration hotpConfig = OTPConfiguration.GetConfiguration(new Uri("otpauth://hotp/FoxDev%20Studio:eugene@xfox111.net?secret=JBSWY3DPEHPK3PXP&issuer=FoxDev%20Studio&counter=10000"));
|
||||
|
||||
/// <summary>
|
||||
/// Test time-based OTP generator with pre-calculated code.
|
||||
/// </summary>
|
||||
[TestMethod("TOTP code generation")]
|
||||
public void GenerateCode_Totp()
|
||||
{
|
||||
var config = totpConfig with { };
|
||||
var code = OTPService.GenerateCode(ref config, time);
|
||||
Assert.AreEqual(160102, code.Code);
|
||||
|
||||
config.Algorithm = Enums.Algorithm.SHA256;
|
||||
code = OTPService.GenerateCode(ref config, time);
|
||||
Assert.AreEqual(195671, code.Code);
|
||||
|
||||
config.Algorithm = Enums.Algorithm.SHA512;
|
||||
code = OTPService.GenerateCode(ref config, time);
|
||||
Assert.AreEqual(293657, code.Code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test HOTP generator with pre-calculated code.
|
||||
/// </summary>
|
||||
[TestMethod("HOTP code generation")]
|
||||
public void GenerateCode_Hotp()
|
||||
{
|
||||
var code = OTPService.GenerateCode(ref hotpConfig);
|
||||
Assert.AreEqual(457608, code.Code);
|
||||
Assert.AreEqual(10001, hotpConfig.Counter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test time-based OTP validator with series of codes.
|
||||
/// </summary>
|
||||
[TestMethod("TOTP code validation")]
|
||||
public void ValidateCode_Totp()
|
||||
{
|
||||
int[] codes =
|
||||
{
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(-45)).Code,
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(-15)).Code,
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(0)).Code,
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(15)).Code,
|
||||
OTPService.GenerateCode(ref totpConfig, DateTime.UtcNow.AddSeconds(45)).Code,
|
||||
};
|
||||
Assert.IsFalse(OTPService.ValidateCode(codes[0], totpConfig));
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[1], totpConfig));
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[2], totpConfig));
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[3], totpConfig));
|
||||
Assert.IsFalse(OTPService.ValidateCode(codes[4], totpConfig));
|
||||
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[0], totpConfig, TimeSpan.FromSeconds(60)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test HOTP validator with series of codes.
|
||||
/// </summary>
|
||||
[TestMethod("HOTP code validation")]
|
||||
public void ValidateCode_Hotp()
|
||||
{
|
||||
hotpConfig.Counter = 10000;
|
||||
int[] codes =
|
||||
{
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
OTPService.GenerateCode(ref hotpConfig).Code,
|
||||
};
|
||||
|
||||
hotpConfig.Counter = 10002;
|
||||
Assert.IsFalse(OTPService.ValidateCode(codes[0], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10002, hotpConfig.Counter);
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[1], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10001, hotpConfig.Counter);
|
||||
hotpConfig.Counter = 10002;
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[2], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10002, hotpConfig.Counter);
|
||||
hotpConfig.Counter = 10002;
|
||||
Assert.IsTrue(OTPService.ValidateCode(codes[3], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10003, hotpConfig.Counter);
|
||||
hotpConfig.Counter = 10002;
|
||||
Assert.IsFalse(OTPService.ValidateCode(codes[4], ref hotpConfig, 1, true));
|
||||
Assert.AreEqual(10002, hotpConfig.Counter);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<AssemblyName>SimpleOTP.Tests</AssemblyName>
|
||||
<PackageId>SimpleOTP.Tests</PackageId>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
|
||||
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
|
||||
<GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute>
|
||||
<GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.10.0-release-20210429-01" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.4-preview-20210513-02" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.4-preview-20210513-02" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.0.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.66">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SimpleOTP\SimpleOTP.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings":
|
||||
{
|
||||
"documentationRules":
|
||||
{
|
||||
"companyName": "Eugene Fox",
|
||||
"copyrightText": "------------------------------------------------------------\nCopyright ©{year} {companyName}. All rights reserved.\nCode by {author}\n\nLicensed under MIT license (https://opensource.org/licenses/MIT)\n------------------------------------------------------------",
|
||||
"xmlHeader": false,
|
||||
"variables":
|
||||
{
|
||||
"year": "2021",
|
||||
"author": "Eugene Fox (aka XFox)"
|
||||
},
|
||||
"headerDecoration": "------------------------------------------------------------",
|
||||
"fileNamingConvention": "stylecop",
|
||||
"documentationCulture": "en-US"
|
||||
},
|
||||
"indentation":
|
||||
{
|
||||
"useTabs": true
|
||||
},
|
||||
"layoutRules":
|
||||
{
|
||||
"newlineAtEndOfFile": "omit"
|
||||
},
|
||||
"maintainabilityRules":
|
||||
{
|
||||
"topLevelTypes": [ "class", "interface", "struct" ]
|
||||
},
|
||||
"namingRules":
|
||||
{
|
||||
"allowCommonHungarianPrefixes": false,
|
||||
"allowedNamespaceComponents": [],
|
||||
"tupleElementNameCasing": "PascalCase"
|
||||
},
|
||||
"orderingRules":
|
||||
{
|
||||
"blankLinesBetweenUsingGroups": "require",
|
||||
"systemUsingDirectivesFirst": true,
|
||||
"usingDirectivesPlacement": "outsideNamespace"
|
||||
},
|
||||
"readabilityRules":
|
||||
{
|
||||
"allowBuiltInTypeAliases": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.6.30114.105
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleOTP", "SimpleOTP\SimpleOTP.csproj", "{518EF6D5-DB32-4406-B289-6E11DB6E1D67}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SimpleOTP.Test", "SimpleOTP.Test\SimpleOTP.Test.csproj", "{54AC322C-6119-456B-BFE6-CCF3CC504C56}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
Debug|x86 = Debug|x86
|
||||
Release|Any CPU = Release|Any CPU
|
||||
Release|x64 = Release|x64
|
||||
Release|x86 = Release|x86
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Release|x64.Build.0 = Release|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{518EF6D5-DB32-4406-B289-6E11DB6E1D67}.Release|x86.Build.0 = Release|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Release|x64.Build.0 = Release|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{54AC322C-6119-456B-BFE6-CCF3CC504C56}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {247D25FB-623A-4CF6-ABD4-06B4CBF0AD3E}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,33 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace SimpleOTP.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// Available OTP encryption algorithms.
|
||||
/// </summary>
|
||||
public enum Algorithm
|
||||
{
|
||||
/// <summary>
|
||||
/// HMAC-SHA1 hasing algorithm (default)<br/>
|
||||
/// <a href="https://datatracker.ietf.org/doc/html/rfc3174">RFC 3174</a>
|
||||
/// </summary>
|
||||
SHA1 = 0,
|
||||
|
||||
/// <summary>
|
||||
/// HMAC-SHA256 hasing algorithm<br/>
|
||||
/// <a href="https://datatracker.ietf.org/doc/html/rfc4634">RFC 4634</a>
|
||||
/// </summary>
|
||||
SHA256 = 1,
|
||||
|
||||
/// <summary>
|
||||
/// HMAC-SHA512 hasing algorithm<br/>
|
||||
/// <a href="https://datatracker.ietf.org/doc/html/rfc4634">RFC 4634</a>
|
||||
/// </summary>
|
||||
SHA512 = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
namespace SimpleOTP.Enums
|
||||
{
|
||||
/// <summary>
|
||||
/// OTP algorithm types.
|
||||
/// </summary>
|
||||
public enum OTPType
|
||||
{
|
||||
/// <summary>
|
||||
/// Time-based One-Time Password<br/>
|
||||
/// <a href="https://datatracker.ietf.org/doc/html/rfc6238">RFC 6238</a>
|
||||
/// </summary>
|
||||
TOTP = 0,
|
||||
|
||||
/// <summary>
|
||||
/// HMAC-based One-Time Password<br/>
|
||||
/// /// <a href="https://datatracker.ietf.org/doc/html/rfc4226">RFC 4226</a>
|
||||
/// </summary>
|
||||
HOTP = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* This file is used by Code Analysis to maintain SuppressMessage
|
||||
* attributes that are applied to this project.
|
||||
* Project-level suppressions either have no target or are given
|
||||
* a specific target and scoped to a namespace, type, member, etc.
|
||||
*/
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1503:Braces should not be omitted", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1625:Element documentation should not be copied and pasted", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.LayoutRules", "SA1519:Braces should not be omitted from multi-line child statement", Justification = "Reviewd by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1101:Prefix local calls with this", Justification = "Reviewd by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.NamingRules", "SA1309:Field names should not begin with underscore", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1413:Use trailing comma in multi-line initializers", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP")]
|
||||
[assembly: SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Reviewed by E. Fox", Scope = "namespaceanddescendants", Target = "~N:SimpleOTP")]
|
||||
@@ -0,0 +1,64 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SimpleOTP.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class which contains methods for encoding and decoding Base32 bytes.
|
||||
/// </summary>
|
||||
internal static class Base32Encoder
|
||||
{
|
||||
private const string AllowedCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
|
||||
/// <summary>
|
||||
/// Encode byte array to Base32 string.
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array to encode.</param>
|
||||
/// <returns>Base32 string.</returns>
|
||||
internal static string Encode(byte[] data)
|
||||
{
|
||||
// FIXME: Encoder works correctly only with 160-bit keys
|
||||
string output = string.Empty;
|
||||
for (int bitIndex = 0; bitIndex < data.Length * 8; bitIndex += 5)
|
||||
{
|
||||
int dualbyte = data[bitIndex / 8] << 8;
|
||||
if ((bitIndex / 8) + 1 < data.Length)
|
||||
dualbyte |= data[(bitIndex / 8) + 1];
|
||||
dualbyte = 0x1f & (dualbyte >> (16 - (bitIndex % 8) - 5));
|
||||
output += AllowedCharacters[dualbyte];
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decode Base32 string into byte array.
|
||||
/// </summary>
|
||||
/// <param name="base32str">Base32-encoded string.</param>
|
||||
/// <returns>Initial byte array.</returns>
|
||||
internal static byte[] Decode(string base32str)
|
||||
{
|
||||
List<byte> output = new ();
|
||||
char[] bytes = base32str.ToCharArray();
|
||||
for (var bitIndex = 0; bitIndex < base32str.Length * 5; bitIndex += 8)
|
||||
{
|
||||
var dualbyte = AllowedCharacters.IndexOf(bytes[bitIndex / 5]) << 10;
|
||||
if ((bitIndex / 5) + 1 < bytes.Length)
|
||||
dualbyte |= AllowedCharacters.IndexOf(bytes[(bitIndex / 5) + 1]) << 5;
|
||||
if ((bitIndex / 5) + 2 < bytes.Length)
|
||||
dualbyte |= AllowedCharacters.IndexOf(bytes[(bitIndex / 5) + 2]);
|
||||
|
||||
dualbyte = 0xff & (dualbyte >> (15 - (bitIndex % 5) - 8));
|
||||
output.Add((byte)dualbyte);
|
||||
}
|
||||
|
||||
return output.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
namespace SimpleOTP.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class for OTP secret generation.
|
||||
/// </summary>
|
||||
public static class SecretGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generate OTP secret key.
|
||||
/// </summary>
|
||||
/// <param name="length">Length of the key in bits<br/>
|
||||
/// It should belong to [128-160] bit span<br/>
|
||||
/// Default is: 160 bits.</param>
|
||||
/// <remarks>CURRENTLY THIS GENERATOR WORKS CORRECTLY ONLY WITH 160-BIT LENGTHS. Set <paramref name="length"/> at your own risk.</remarks>
|
||||
/// <returns>Base32 encoded alphanumeric string with length form 16 to 20 characters.</returns>
|
||||
public static string GenerateSecret(int length = 160)
|
||||
{
|
||||
if (length > 160 || length < 128)
|
||||
throw new ArgumentOutOfRangeException(nameof(length), "Invalid key length. It should belong to [128-160] bits span");
|
||||
|
||||
byte[] key = new byte[length];
|
||||
new Random().NextBytes(key);
|
||||
|
||||
return Base32Encoder.Encode(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Web;
|
||||
|
||||
using SimpleOTP.Enums;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class which contains methods to parse OTP auth URIs.
|
||||
/// </summary>
|
||||
internal static class UriParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses OTP Auth URI and returns configuration object for further processing.
|
||||
/// URI should be correctly formed.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For more information please refer to <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key Uri Format</a>.
|
||||
/// </remarks>
|
||||
/// <param name="uri">OTP Auth URI. Should be correctly formed.
|
||||
/// For more information please refer to <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key Uri Format</a>.</param>
|
||||
/// <returns><see cref="OTPConfiguration"/> configuration object, which contains data for OTP generation.</returns>
|
||||
internal static OTPConfiguration ParseUri(Uri uri)
|
||||
{
|
||||
if (uri.Scheme != "otpauth")
|
||||
throw new ArgumentException("Malformed link: Invalid scheme");
|
||||
if (!new[] { "hotp", "totp" }.Contains(uri.Host))
|
||||
throw new ArgumentException("Malformed link: Invalid OTP type");
|
||||
if (string.IsNullOrWhiteSpace(uri.LocalPath[1..]))
|
||||
throw new ArgumentException("Malformed link: Invalid label");
|
||||
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(uri.Query);
|
||||
if (!query.AllKeys.Contains("secret"))
|
||||
throw new ArgumentException("Malformed link: No secret provided");
|
||||
if (uri.Host == "hotp" && !query.AllKeys.Contains("counter"))
|
||||
throw new ArgumentException("Malformed link: No counter provided for HOTP");
|
||||
if (!uri.LocalPath[1..].Contains(':') && !query.AllKeys.Contains("issuer"))
|
||||
throw new ArgumentException("Malformed link: No issuer provided");
|
||||
|
||||
string[] label = uri.LocalPath[1..].Split(':');
|
||||
OTPConfiguration item = new ()
|
||||
{
|
||||
Type = uri.Host == "totp" ? OTPType.TOTP : OTPType.HOTP,
|
||||
IssuerLabel = label.Length > 1 ? label[0] : query["issuer"],
|
||||
AccountName = label.Length > 1 ? label[1] : uri.LocalPath[1..],
|
||||
Secret = query["secret"],
|
||||
Issuer = query["issuer"] ?? label[0],
|
||||
Algorithm = query["algorithm"]?.ToUpperInvariant() switch
|
||||
{
|
||||
"SHA256" => Algorithm.SHA256,
|
||||
"SHA512" => Algorithm.SHA512,
|
||||
_ => Algorithm.SHA1
|
||||
},
|
||||
Digits = int.Parse(query["digits"] ?? "6"),
|
||||
Counter = int.Parse(query["counter"] ?? "0"),
|
||||
Period = TimeSpan.FromSeconds(int.Parse(query["period"] ?? "30"))
|
||||
};
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
|
||||
namespace SimpleOTP.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// OTP code object model.
|
||||
/// </summary>
|
||||
public record OTPCode
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets OTP code.
|
||||
/// </summary>
|
||||
public int Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets date-time until the code is valid.
|
||||
/// </summary>
|
||||
public DateTime? Expiring { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OTPCode"/> class.
|
||||
/// </summary>
|
||||
public OTPCode()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OTPCode"/> class.<br/>
|
||||
/// Use this contructor only for HOTP key. Otherwise, fill out all properties.
|
||||
/// </summary>
|
||||
/// <param name="code">OTP code.</param>
|
||||
public OTPCode(int code) =>
|
||||
Code = code;
|
||||
|
||||
/// <summary>
|
||||
/// Gets valid 6 digit or more OTP code.
|
||||
/// </summary>
|
||||
/// <param name="formatter">String formatter. Other variation:
|
||||
/// <code>"000 000"</code></param>
|
||||
/// <returns>Formatted OTP code string with 6 or more digits.</returns>
|
||||
public string GetCode(string formatter = "000000") =>
|
||||
Code.ToString(formatter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
|
||||
using SimpleOTP.Enums;
|
||||
using SimpleOTP.Helpers;
|
||||
|
||||
namespace SimpleOTP.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// OTP generator configuration object.
|
||||
/// </summary>
|
||||
public record OTPConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets unique identifier of current configuration instance.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets oTP algorithm type.
|
||||
/// </summary>
|
||||
public OTPType Type { get; set; } = OTPType.TOTP;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets name of config issuer/service.
|
||||
/// </summary>
|
||||
public string IssuerLabel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets username or email of current config.
|
||||
/// </summary>
|
||||
public string AccountName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets secret key for OTP code generation.
|
||||
/// </summary>
|
||||
public string Secret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets internal issuer name for additional identification. Currently should be the same with <see cref="IssuerLabel"/>.
|
||||
/// </summary>
|
||||
public string Issuer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets oTP hashing algorithm.
|
||||
/// </summary>
|
||||
public Algorithm Algorithm { get; set; } = Algorithm.SHA1;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets number of digits of OTP code.
|
||||
/// </summary>
|
||||
public int Digits { get; set; } = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets counter for HOTP generation. Update each time password has been generated.<br/>
|
||||
/// HOTP only.
|
||||
/// </summary>
|
||||
public long Counter { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets time of OTP validity interval. Used to calculate TOTP counter.
|
||||
/// TOTP only.
|
||||
/// </summary>
|
||||
public TimeSpan Period { get; set; } = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new OTP configuration to send it to client.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="table">
|
||||
/// <listheader>Algorithm parameters:</listheader>
|
||||
/// <item>
|
||||
/// <term>OTP algorithm</term>
|
||||
/// <description>Time-based OTP</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Key length</term>
|
||||
/// <description>160 bit (20 characters)</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Hashing algorithm</term>
|
||||
/// <description>HMAC-SHA-1</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Number of digits</term>
|
||||
/// <description>6</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Period</term>
|
||||
/// <description>30 seconds</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="issuer">Name of your application/service.</param>
|
||||
/// <param name="accountName">Username/email of the user.</param>
|
||||
/// <returns>Valid <see cref="OTPConfiguration"/> configuraion.</returns>
|
||||
public static OTPConfiguration GenerateConfiguration(string issuer, string accountName) =>
|
||||
new ()
|
||||
{
|
||||
Issuer = issuer,
|
||||
IssuerLabel = issuer,
|
||||
AccountName = accountName,
|
||||
Secret = SecretGenerator.GenerateSecret()
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Load OTP configuraiton with default parameters.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="table">
|
||||
/// <listheader>Algorithm parameters:</listheader>
|
||||
/// <item>
|
||||
/// <term>OTP algorithm</term>
|
||||
/// <description>Time-based OTP</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Hashing algorithm</term>
|
||||
/// <description>HMAC-SHA-1</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Number of digits</term>
|
||||
/// <description>6</description>
|
||||
/// </item>
|
||||
/// <item>
|
||||
/// <term>Period</term>
|
||||
/// <description>30 seconds</description>
|
||||
/// </item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
/// <param name="secret">OTP generator secret key (Base32 encoded string).</param>
|
||||
/// <param name="issuer">Name of your application/service.</param>
|
||||
/// <param name="accountName">Username/email of the user.</param>
|
||||
/// <returns>Valid <see cref="OTPConfiguration"/> configuraion.</returns>
|
||||
public static OTPConfiguration GetConfiguration(string secret, string issuer, string accountName) =>
|
||||
new ()
|
||||
{
|
||||
Issuer = issuer,
|
||||
IssuerLabel = issuer,
|
||||
AccountName = accountName,
|
||||
Secret = secret
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Loads OTP configuration from OTP AUTH URI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For more information please refer to <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key Uri Format</a>.
|
||||
/// </remarks>
|
||||
/// <param name="uri">OTP Auth URI. Should be correctly formed.</param>
|
||||
/// <returns>Valid <see cref="OTPConfiguration"/> configuraion.</returns>
|
||||
public static OTPConfiguration GetConfiguration(Uri uri) =>
|
||||
Helpers.UriParser.ParseUri(uri);
|
||||
|
||||
/// <summary>
|
||||
/// Loads OTP configuration from OTP AUTH URI.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// For more information please refer to <a href="https://github.com/google/google-authenticator/wiki/Key-Uri-Format">Key Uri Format</a>.
|
||||
/// </remarks>
|
||||
/// <param name="uri">OTP Auth URI. Should be correctly formed.</param>
|
||||
/// <returns>Valid <see cref="OTPConfiguration"/> configuraion.</returns>
|
||||
public static OTPConfiguration GetConfiguration(string uri) =>
|
||||
GetConfiguration(new Uri(uri));
|
||||
|
||||
/// <summary>
|
||||
/// Gets URI from current configuration to reuse it somewhere else.
|
||||
/// </summary>
|
||||
/// <returns>Valid OTP AUTH URI.</returns>
|
||||
public Uri GetUri()
|
||||
{
|
||||
string path = $"otpauth://{Type}/{HttpUtility.UrlEncode(IssuerLabel)}";
|
||||
if (!string.IsNullOrWhiteSpace(AccountName))
|
||||
path += $":{AccountName}";
|
||||
path += $"?secret={Secret}&issuer={HttpUtility.UrlEncode(Issuer)}";
|
||||
if (Algorithm != Algorithm.SHA1)
|
||||
path += $"&algorithm={Algorithm}";
|
||||
if (Digits != 6)
|
||||
path += $"&digits={Digits}";
|
||||
if (Type == OTPType.HOTP)
|
||||
path += $"&counter={Counter}";
|
||||
if (Type == OTPType.TOTP && Period.TotalSeconds != 30)
|
||||
path += $"&period={(int)Period.TotalSeconds}";
|
||||
|
||||
return new Uri(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns secret key separated with whitespaces on groups of 4.
|
||||
/// </summary>
|
||||
/// <returns>Formatted secret key string.</returns>
|
||||
public string GetFancySecret()
|
||||
{
|
||||
string secret = Secret;
|
||||
for (int k = 0; 4 + (5 * k) < secret.Length; k++)
|
||||
secret = secret.Insert(4 + (5 * k), " ");
|
||||
return secret;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates QR code image for current configuration with Google Chart API.
|
||||
/// </summary>
|
||||
/// <param name="qrCodeSize">QR code image size in pixels.</param>
|
||||
/// <param name="requestTimeout">Web request timeout in seconds.</param>
|
||||
/// <returns>string-encoded PNG image.</returns>
|
||||
public async Task<string> GetQrImage(int qrCodeSize = 300, int requestTimeout = 30)
|
||||
{
|
||||
HttpClient client = new () { Timeout = TimeSpan.FromSeconds(requestTimeout) };
|
||||
HttpResponseMessage response = client.GetAsync($"https://chart.googleapis.com/chart?cht=qr&chs={qrCodeSize}x{qrCodeSize}&chl={HttpUtility.UrlEncode(GetUri().AbsoluteUri)}").Result;
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new HttpRequestException("Response status code indicates that request has failed", null, response.StatusCode);
|
||||
|
||||
byte[] imageBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
string imageString = @$"data:image/png;base64,{Convert.ToBase64String(imageBytes)}";
|
||||
|
||||
return imageString;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Timers;
|
||||
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents method that will be called when new OTP code is generated.
|
||||
/// </summary>
|
||||
/// <param name="code">New OTP code instance.</param>
|
||||
public delegate void OTPCodeUpdatedEventHandler(OTPCode code);
|
||||
|
||||
/// <summary>
|
||||
/// Class used to streamline OTP code generation on client devices.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <code>
|
||||
/// var factory = new (config);<br/>
|
||||
/// factory.CodeUpdated += (newCode) => Console.WriteLine(newCode.Code);
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public class OTPFactory : INotifyPropertyChanged, IDisposable
|
||||
{
|
||||
private readonly Timer _timer = new (1000);
|
||||
|
||||
private OTPCode _currentCode;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets current valid OTP code instance.
|
||||
/// </summary>
|
||||
public OTPCode CurrentCode
|
||||
{
|
||||
get => _currentCode;
|
||||
set
|
||||
{
|
||||
if (_currentCode == value)
|
||||
return;
|
||||
_currentCode = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CurrentCode)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TimeLeft)));
|
||||
CodeUpdated?.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
private OTPConfiguration _configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets OTP configuration of the current instance.
|
||||
/// </summary>
|
||||
public OTPConfiguration Configuration
|
||||
{
|
||||
get => _configuration;
|
||||
set
|
||||
{
|
||||
if (_configuration == value)
|
||||
return;
|
||||
_configuration = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Configuration)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets time left before current OTP code expires.
|
||||
/// </summary>
|
||||
public TimeSpan? TimeLeft => CurrentCode?.Expiring - DateTime.UtcNow;
|
||||
|
||||
/// <summary>
|
||||
/// Event is fired when new OTP code is generated.
|
||||
/// </summary>
|
||||
public event OTPCodeUpdatedEventHandler CodeUpdated;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="OTPFactory"/> class.
|
||||
/// </summary>
|
||||
/// <param name="configuration">OTP configuration for codes producing.</param>
|
||||
/// <param name="timerUpdateInterval">Interval for timer updates in milliseconds.</param>
|
||||
public OTPFactory(OTPConfiguration configuration, int timerUpdateInterval = 1000)
|
||||
{
|
||||
Configuration = configuration;
|
||||
CurrentCode = OTPService.GenerateCode(ref configuration);
|
||||
|
||||
_timer.Interval = timerUpdateInterval;
|
||||
_timer.Elapsed += TimerElapsed;
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
_timer.Stop();
|
||||
_timer.Elapsed -= TimerElapsed;
|
||||
_timer.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void TimerElapsed(object sender, ElapsedEventArgs args)
|
||||
{
|
||||
if (TimeLeft.Value.TotalSeconds <= 0)
|
||||
CurrentCode = OTPService.GenerateCode(ref _configuration);
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TimeLeft)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
using SimpleOTP.Enums;
|
||||
using SimpleOTP.Helpers;
|
||||
using SimpleOTP.Models;
|
||||
|
||||
namespace SimpleOTP
|
||||
{
|
||||
/// <summary>
|
||||
/// Service class for generating and validating OTP codes.
|
||||
/// </summary>
|
||||
public static class OTPService
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a new OTP code with provided configuration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you're using HOTP algorithm, save <paramref name="target"/> after calling the function.
|
||||
/// </remarks>
|
||||
/// <param name="target">OTP configuration object.</param>
|
||||
/// <returns><see cref="OTPCode"/> instance with generated code.<br/>
|
||||
/// If OTP algorithm is HOTP, <paramref name="target"/> counter is increased by 1.
|
||||
/// </returns>
|
||||
public static OTPCode GenerateCode(ref OTPConfiguration target) =>
|
||||
GenerateCode(ref target, DateTime.UtcNow);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a new TOTP code with provided configuration and for specific interval.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you're using HOTP algorithm, save <paramref name="target"/> after calling the function.<br/>
|
||||
/// If you're using HOTP algorithm, <paramref name="date"/> will be ignored.
|
||||
/// </remarks>
|
||||
/// <param name="target">OTP configuration object.</param>
|
||||
/// <param name="date"><see cref="DateTime"/> for which the OTP should be generated.</param>
|
||||
/// <returns><see cref="OTPCode"/> instance with generated code.<br/>
|
||||
/// If OTP algorithm is HOTP, <paramref name="target"/> counter is increased by 1.
|
||||
/// </returns>
|
||||
public static OTPCode GenerateCode(ref OTPConfiguration target, DateTime date)
|
||||
{
|
||||
byte[] keyBytes = Base32Encoder.Decode(target.Secret);
|
||||
long counter = target.Type == OTPType.HOTP ? target.Counter : GetCurrentCounter(date.ToUniversalTime(), (int)target.Period.TotalSeconds);
|
||||
byte[] counterBytes = BitConverter.GetBytes(counter);
|
||||
|
||||
// IDK da fuk is this and at this point I'm too afraid to ask
|
||||
if (BitConverter.IsLittleEndian)
|
||||
Array.Reverse(counterBytes);
|
||||
|
||||
HMAC hmac = target.Algorithm switch
|
||||
{
|
||||
Algorithm.SHA256 => new HMACSHA256(keyBytes),
|
||||
Algorithm.SHA512 => new HMACSHA512(keyBytes),
|
||||
_ => new HMACSHA1(keyBytes)
|
||||
};
|
||||
|
||||
byte[] hash = hmac.ComputeHash(counterBytes);
|
||||
|
||||
// Not mine code, so fuck off
|
||||
int offset = hash[^1] & 0xf;
|
||||
|
||||
// Convert the 4 bytes into an integer, ignoring the sign.
|
||||
int binary =
|
||||
((hash[offset] & 0x7f) << 24)
|
||||
| (hash[offset + 1] << 16)
|
||||
| (hash[offset + 2] << 8)
|
||||
| hash[offset + 3];
|
||||
|
||||
OTPCode code = new (binary % (int)Math.Pow(10, target.Digits));
|
||||
|
||||
if (target.Type == OTPType.HOTP)
|
||||
target.Counter++; // Incrementing counter for HMAC OTP type
|
||||
else
|
||||
code.Expiring = DateTime.UnixEpoch.AddSeconds((counter + 1) * target.Period.TotalSeconds);
|
||||
|
||||
return code;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates provided HOTP code with provided parameters.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this method only with HOTP codes.
|
||||
/// </remarks>
|
||||
/// <param name="otp">HOTP code to validate.</param>
|
||||
/// <param name="target">OTP configuration for check codes generation.</param>
|
||||
/// <param name="toleranceSpan">Counter span from which OTP codes remain valid.</param>
|
||||
/// <param name="resyncCounter">Defines whether method should resync <see cref="OTPConfiguration.Counter"/> of the <paramref name="target"/> or not after successful validation.</param>
|
||||
/// <returns><c>True</c> if code is valid, <c>False</c> if it isn't.</returns>
|
||||
public static bool ValidateCode(int otp, ref OTPConfiguration target, int toleranceSpan, bool resyncCounter)
|
||||
{
|
||||
long currentCounter = target.Counter;
|
||||
List<(int Code, long Counter)> codes = new ();
|
||||
for (long i = currentCounter - toleranceSpan; i <= currentCounter + toleranceSpan; i++)
|
||||
{
|
||||
OTPConfiguration testTarget = target with { Counter = i };
|
||||
codes.Add((GenerateCode(ref testTarget).Code, testTarget.Counter - 1));
|
||||
}
|
||||
|
||||
bool isValid = codes.Any(i => i.Code == otp);
|
||||
if (isValid && resyncCounter)
|
||||
target.Counter = codes.Find(i => i.Code == otp).Counter;
|
||||
return isValid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates provided TOTP code with provided parameters.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this method only with Time-based OTP codes.
|
||||
/// </remarks>
|
||||
/// <param name="otp">OTP code to validate.</param>
|
||||
/// <param name="target">OTP configuration for check codes generation.</param>
|
||||
/// <param name="toleranceTime">Time span from which OTP codes remain valid.<br/>
|
||||
/// Default: 15 seconds.</param>
|
||||
/// <returns><c>True</c> if code is valid, <c>False</c> if it isn't.</returns>
|
||||
public static bool ValidateCode(int otp, OTPConfiguration target, TimeSpan? toleranceTime = null)
|
||||
{
|
||||
toleranceTime ??= TimeSpan.FromSeconds(15);
|
||||
DateTime now = DateTime.UtcNow;
|
||||
List<int> codes = new ();
|
||||
for (DateTime time = now - toleranceTime.Value; time <= now + toleranceTime; time += target.Period)
|
||||
codes.Add(GenerateCode(ref target, time).Code);
|
||||
|
||||
return codes.Any(i => i == otp);
|
||||
}
|
||||
|
||||
private static long GetCurrentCounter(DateTime date, int period) =>
|
||||
(long)(date - DateTime.UnixEpoch).TotalSeconds / period;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// ------------------------------------------------------------
|
||||
// Copyright ©2021 Eugene Fox. All rights reserved.
|
||||
// Code by Eugene Fox (aka XFox)
|
||||
//
|
||||
// Licensed under MIT license (https://opensource.org/licenses/MIT)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// 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("776914d2-d408-4755-894f-a230d372f07d")]
|
||||
[assembly: InternalsVisibleTo("SimpleOTP.Tests")]
|
||||
@@ -0,0 +1,49 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageId>SimpleOTP</PackageId>
|
||||
<AssemblyName>SimpleOTP</AssemblyName>
|
||||
<Version>1.0.0</Version>
|
||||
<Description>.NET library for TOTP/HOTP implementation on server (ASP.NET) or client (Xamarin) side</Description>
|
||||
<Authors>Eugene Fox</Authors>
|
||||
<Company>FoxDev Studio</Company>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
<Copyright>©2021 Eugene Fox</Copyright>
|
||||
<RepositoryType>Git</RepositoryType>
|
||||
<RepositoryUrl>https://github.com/XFox111/SimpleOTP</RepositoryUrl>
|
||||
<NeutralLanguage>en-US</NeutralLanguage>
|
||||
<PackageTags>otp;totp;dotnet;hotp;authenticator;2fa;mfa;security;oath</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="stylecop.json" />
|
||||
<None Include="..\LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="stylecop.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.66">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
|
||||
"settings":
|
||||
{
|
||||
"documentationRules":
|
||||
{
|
||||
"companyName": "Eugene Fox",
|
||||
"copyrightText": "------------------------------------------------------------\nCopyright ©{year} {companyName}. All rights reserved.\nCode by {author}\n\nLicensed under MIT license (https://opensource.org/licenses/MIT)\n------------------------------------------------------------",
|
||||
"xmlHeader": false,
|
||||
"variables":
|
||||
{
|
||||
"year": "2021",
|
||||
"author": "Eugene Fox (aka XFox)"
|
||||
},
|
||||
"headerDecoration": "------------------------------------------------------------",
|
||||
"fileNamingConvention": "stylecop",
|
||||
"documentationCulture": "en-US"
|
||||
},
|
||||
"indentation":
|
||||
{
|
||||
"useTabs": true
|
||||
},
|
||||
"layoutRules":
|
||||
{
|
||||
"newlineAtEndOfFile": "omit"
|
||||
},
|
||||
"maintainabilityRules":
|
||||
{
|
||||
"topLevelTypes": [ "class", "interface", "struct" ]
|
||||
},
|
||||
"namingRules":
|
||||
{
|
||||
"allowCommonHungarianPrefixes": false,
|
||||
"allowedNamespaceComponents": [],
|
||||
"tupleElementNameCasing": "PascalCase"
|
||||
},
|
||||
"orderingRules":
|
||||
{
|
||||
"blankLinesBetweenUsingGroups": "require",
|
||||
"systemUsingDirectivesFirst": true,
|
||||
"usingDirectivesPlacement": "outsideNamespace"
|
||||
},
|
||||
"readabilityRules":
|
||||
{
|
||||
"allowBuiltInTypeAliases": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user