Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge #1

Open
wants to merge 46 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
52a093b
Add functionality to create compliant OTP URIs (#34)
mees- Dec 23, 2022
01999d7
refactor OtpUri some
kspearrin Dec 23, 2022
d363a57
rename OtpUri
kspearrin Dec 23, 2022
9c361d3
OtpUri tests
kspearrin Dec 23, 2022
2d5dffd
.net 5 support + small refactoring (#33)
Dec 23, 2022
c0d4b5a
refactoring
kspearrin Dec 23, 2022
b4a15ac
build status
kspearrin Dec 23, 2022
9b2e079
bump version
kspearrin Dec 23, 2022
2bd90dd
Update README.md
kspearrin Dec 23, 2022
a9f3111
accept string secret for otp uri
kspearrin Dec 23, 2022
6bfb02a
Merge branch 'master' of github.com:kspearrin/Otp.NET
kspearrin Dec 23, 2022
347afe7
snk signing key
kspearrin Dec 23, 2022
92ab19d
AssemblyOriginatorKeyFile only on release
kspearrin Dec 23, 2022
2778bd3
update license file reference
kspearrin Dec 23, 2022
be11196
package icon
kspearrin Dec 23, 2022
168f5cf
change to EscapeDataString
kspearrin Dec 23, 2022
6c05715
fix OTP uri example
kspearrin Dec 23, 2022
d84651e
package readme
kspearrin Dec 23, 2022
159b4c7
update dist path
kspearrin Dec 23, 2022
505c3e8
Update README.md
kspearrin Jan 19, 2023
675daf5
Add support for .NET 7 (#42)
DSC-bdavis Apr 8, 2023
6a62bf1
trim end of secret, resolves #40
kspearrin Apr 8, 2023
e3b5613
update target frameworks
kspearrin Apr 8, 2023
af9e4bf
move to latest c# lang features
kspearrin Apr 10, 2023
7af3a19
update test project lang syntax
kspearrin Apr 10, 2023
d2afcc6
OTP properties (#49)
miloush Jul 20, 2023
0ac3c31
accessible TOTP window start (#50)
miloush Jul 24, 2023
887f3f1
update to .net 8
kspearrin Jan 22, 2024
924d771
appveyor fixes
kspearrin Jan 22, 2024
14a0a1c
Update README.md (#51)
doggy8088 Jan 23, 2024
7b7da32
remove forced truncation to 6 digits (#31)
tmiranda Feb 18, 2024
df2abdd
added 6 digit tests
kspearrin Feb 18, 2024
8ed6a96
add base32 comments
kspearrin Feb 18, 2024
4f22810
update libs
kspearrin Feb 18, 2024
48475df
cleanup syntax
kspearrin Feb 18, 2024
8246823
bump version
kspearrin Apr 12, 2024
1f63a82
GeneratePackageOnBuild
kspearrin Apr 12, 2024
a5b437f
Create action workflow
kspearrin Apr 12, 2024
65758d7
workflows
kspearrin Apr 12, 2024
78ee49e
rename nuget workflow
kspearrin Apr 12, 2024
04e1b8c
NUGET_AUTH_TOKEN env
kspearrin Apr 12, 2024
08f8fab
restore signing key
kspearrin Apr 12, 2024
16f92e2
remove appveyor
kspearrin Apr 12, 2024
bcffc90
github action badge
kspearrin Apr 12, 2024
c7545fa
link badge
kspearrin Apr 12, 2024
16e2a5e
Use long for counter in OtpUri (#61)
kikaragyozov Apr 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: .NET Build

on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
- name: Upload package
uses: actions/upload-artifact@v4
with:
name: otpnet-nuget-package
path: src/Otp.NET/bin/Debug/*.nupkg
39 changes: 39 additions & 0 deletions .github/workflows/nuget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Upload NuGet package

on:
release:
types: [created]

jobs:
deploy:

runs-on: ubuntu-latest

permissions:
packages: write
contents: read

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Restore signing key
env:
SIGNING_KEY: ${{ secrets.VS_SIGNING_KEY }}
run: |
echo $SIGNING_KEY | base64 --decode > src/Otp.NET/vs-signing-key.snk
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore --configuration Release
- name: Publish to nuget.org
run: dotnet nuget push src/Otp.NET/bin/Release/*.nupkg -k $NUGET_AUTH_TOKEN -s https://api.nuget.org/v3/index.json
env:
NUGET_AUTH_TOKEN: ${{ secrets.NUGET_TOKEN }}
- name: Upload package
uses: actions/upload-artifact@v4
with:
name: otpnet-nuget-package
path: src/Otp.NET/bin/Release/*.nupkg
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,5 @@ paket-files/
*.sln.iml

.nuget/
node_modules/
node_modules/
*.snk
9 changes: 6 additions & 3 deletions Otp.NET.sln
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29411.108
# Visual Studio Version 17
VisualStudioVersion = 17.4.33110.190
MinimumVisualStudioVersion = 15.0.26124.0
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D0E9D08A-7F66-426A-8645-C2D92DB4D200}"
EndProject
Expand All @@ -12,7 +12,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{81C89CBC-2
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2A4A70D0-4968-4E0C-9E42-4AF2C076D38B}"
ProjectSection(SolutionItems) = preProject
appveyor.yml = appveyor.yml
.gitignore = .gitignore
.github\workflows\dotnet.yml = .github\workflows\dotnet.yml
LICENSE.txt = LICENSE.txt
.github\workflows\nuget.yml = .github\workflows\nuget.yml
README.md = README.md
EndProjectSection
EndProject
Expand Down
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@

An implementation TOTP [RFC 6238](http://tools.ietf.org/html/rfc6238) and HOTP [RFC 4226](http://tools.ietf.org/html/rfc4226) in C#.

[![.NET build status (master)](https://github.com/kspearrin/Otp.NET/actions/workflows/dotnet.yml/badge.svg?branch=master)](https://github.com/kspearrin/Otp.NET/actions/workflows/dotnet.yml)

## Get it on NuGet

https://www.nuget.org/packages/Otp.NET

```powershell
Install-Package Otp.NET
```
PM> Install-Package Otp.NET

or

```bash
dotnet add package Otp.NET
```

## Documentation

- [TOTP (Timed One Time Password)](#totp-timed-one-time-password)
- [HOTP (HMAC-based One Time Password)](#hotp-hmac-based-one-time-password)
- [OTP Uri](#otp-uri)
- [Base32 Encoding](#base32-encoding)

### TOTP (Timed One Time Password)
Expand Down Expand Up @@ -184,7 +193,7 @@ var hotp = new Hotp(secretKey, mode: OtpHashMode.Sha512);
Finally the truncation level can be specified. Basically this is how many digits do you want your HOTP code to be. The tests in the RFC specify 8, but 6 has become a de-facto standard if not an actual one. For this reason the default is 6 but you can set it to something else. There aren't a lot of tests around this either so use at your own risk (other than the fact that the RFC test table uses HOTP values that are 8 digits).

```c#
var hotp = new Hotp(secretKey, totpSize: 8);
var hotp = new Hotp(secretKey, hotpSize: 8);
```

#### Verification
Expand All @@ -195,6 +204,15 @@ The HOTP implementation provides a mechanism for verifying HOTP codes that are p
public bool VerifyHotp(string totp, long counter);
```

### OTP Uri

You can use the OtpUri class to generate OTP style uris in the "Key Uri Format" as defined here: https://github.com/google/google-authenticator/wiki/Key-Uri-Format

```c#
var uriString = new OtpUri(OtpType.Totp, "JBSWY3DPEHPK3PXP", "alice@google.com", "ACME Co").ToString();
// uriString is otpauth://totp/ACME%20Co:alice%40google.com?secret=JBSWY3DPEHPK3PXP&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
```

### Base32 Encoding

Also included is a Base32 helper.
Expand Down
14 changes: 0 additions & 14 deletions appveyor.yml

This file was deleted.

Binary file added icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
184 changes: 96 additions & 88 deletions src/Otp.NET/Base32Encoding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,128 +6,136 @@

using System;

namespace OtpNet
namespace OtpNet;

public class Base32Encoding
{
public class Base32Encoding
public static byte[] ToBytes(string input)
{
public static byte[] ToBytes(string input)
if (string.IsNullOrEmpty(input))
{
if(string.IsNullOrEmpty(input))
{
throw new ArgumentNullException("input");
}
throw new ArgumentNullException(nameof(input));
}

// remove padding characters
input = input.TrimEnd('=');
// this must be TRUNCATED
var byteCount = input.Length * 5 / 8;
var returnArray = new byte[byteCount];

input = input.TrimEnd('='); //remove padding characters
int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
byte[] returnArray = new byte[byteCount];
byte curByte = 0, bitsRemaining = 8;
var arrayIndex = 0;

byte curByte = 0, bitsRemaining = 8;
int mask = 0, arrayIndex = 0;
foreach (var c in input)
{
var cValue = CharToValue(c);

foreach(char c in input)
int mask;
if (bitsRemaining > 5)
{
int cValue = CharToValue(c);

if(bitsRemaining > 5)
{
mask = cValue << (bitsRemaining - 5);
curByte = (byte)(curByte | mask);
bitsRemaining -= 5;
}
else
{
mask = cValue >> (5 - bitsRemaining);
curByte = (byte)(curByte | mask);
returnArray[arrayIndex++] = curByte;
curByte = (byte)(cValue << (3 + bitsRemaining));
bitsRemaining += 3;
}
mask = cValue << (bitsRemaining - 5);
curByte = (byte)(curByte | mask);
bitsRemaining -= 5;
}

//if we didn't end with a full byte
if(arrayIndex != byteCount)
else
{
returnArray[arrayIndex] = curByte;
mask = cValue >> (5 - bitsRemaining);
curByte = (byte)(curByte | mask);
returnArray[arrayIndex++] = curByte;
curByte = (byte)(cValue << (3 + bitsRemaining));
bitsRemaining += 3;
}

return returnArray;
}

public static string ToString(byte[] input)
// if we didn't end with a full byte
if (arrayIndex != byteCount)
{
if(input == null || input.Length == 0)
{
throw new ArgumentNullException("input");
}
returnArray[arrayIndex] = curByte;
}

int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
char[] returnArray = new char[charCount];
return returnArray;
}

byte nextChar = 0, bitsRemaining = 5;
int arrayIndex = 0;
public static string ToString(byte[] input)
{
if (input == null || input.Length == 0)
{
throw new ArgumentNullException(nameof(input));
}

foreach(byte b in input)
{
nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
returnArray[arrayIndex++] = ValueToChar(nextChar);
var charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
var returnArray = new char[charCount];

if(bitsRemaining < 4)
{
nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
returnArray[arrayIndex++] = ValueToChar(nextChar);
bitsRemaining += 5;
}
byte nextChar = 0, bitsRemaining = 5;
var arrayIndex = 0;

bitsRemaining -= 3;
nextChar = (byte)((b << bitsRemaining) & 31);
}
foreach (byte b in input)
{
nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
returnArray[arrayIndex++] = ValueToChar(nextChar);

//if we didn't end with a full char
if(arrayIndex != charCount)
if (bitsRemaining < 4)
{
nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
returnArray[arrayIndex++] = ValueToChar(nextChar);
while(arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
bitsRemaining += 5;
}

return new string(returnArray);
bitsRemaining -= 3;
nextChar = (byte)((b << bitsRemaining) & 31);
}

private static int CharToValue(char c)
// if we didn't end with a full char
if (arrayIndex != charCount)
{
int value = (int)c;

//65-90 == uppercase letters
if(value < 91 && value > 64)
returnArray[arrayIndex++] = ValueToChar(nextChar);
// padding
while (arrayIndex != charCount)
{
return value - 65;
}
//50-55 == numbers 2-7
if(value < 56 && value > 49)
{
return value - 24;
}
//97-122 == lowercase letters
if(value < 123 && value > 96)
{
return value - 97;
returnArray[arrayIndex++] = '=';
}
}

return new string(returnArray);
}

private static int CharToValue(char c)
{
int value = c;

throw new ArgumentException("Character is not a Base32 character.", "c");
// 65-90 == uppercase letters
if (value < 91 && value > 64)
{
return value - 65;
}

private static char ValueToChar(byte b)
// 50-55 == numbers 2-7
if (value < 56 && value > 49)
{
if(b < 26)
{
return (char)(b + 65);
}
return value - 24;
}

if(b < 32)
{
return (char)(b + 24);
}
// 97-122 == lowercase letters
if (value < 123 && value > 96)
{
return value - 97;
}

throw new ArgumentException("Byte is not a Base32 value.", "b");
throw new ArgumentException("Character is not a Base32 character.", nameof(c));
}

private static char ValueToChar(byte b)
{
if (b < 26)
{
return (char)(b + 65);
}

if (b < 32)
{
return (char)(b + 24);
}

throw new ArgumentException("Byte is not a Base32 value.", nameof(b));
}
}
Loading