Skip to content

Commit

Permalink
Adds support for otpauth:// URIs
Browse files Browse the repository at this point in the history
  • Loading branch information
geoffodonnell committed May 25, 2024
1 parent 34d1fb1 commit 7aef6fa
Show file tree
Hide file tree
Showing 7 changed files with 167 additions and 18 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
shell: pwsh
run: |-
$version = "${{ github.ref_name }}"
$result = $version | Select-String -Pattern "v([0-9]+).([0-9]+).([0-9]+)(-)?([A-Za-z]+)?"
$result = $version | Select-String -Pattern "v([0-9]+).([0-9]+).([0-9]+)(-)?([A-Za-z0-9]+)?"
if ($result.Matches.Success) {
$majorVersion = $result.Matches.Groups[1].Value
Expand Down Expand Up @@ -183,7 +183,7 @@ jobs:
shell: pwsh
run: |-
$version = "${{ github.ref_name }}"
$result = $version | Select-String -Pattern "v([0-9]+).([0-9]+).([0-9]+)(-)?([A-Za-z]+)?"
$result = $version | Select-String -Pattern "v([0-9]+).([0-9]+).([0-9]+)(-)?([A-Za-z0-9]+)?"
if ($result.Matches.Success -and $result.Matches.Groups.Length -eq 6) {
$prerelease = $result.Matches.Groups[5].Value
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# powershell-otpauth-module

[![CI/CD](https://github.com/geoffodonnell/powershell-otpauth-module/actions/workflows/ci-cd.yml/badge.svg?branch=develop&event=push)](https://github.com/geoffodonnell/powershell-otpauth-module/actions/workflows/ci-cd.yml)
[![CI/CD](https://github.com/geoffodonnell/powershell-otpauth-module/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/geoffodonnell/powershell-otpauth-module/actions/workflows/ci-cd.yml)
[![PSGallery version](https://img.shields.io/powershellgallery/v/OtpAuth?include_prereleases)](https://www.powershellgallery.com/packages/OtpAuth)

# Overview
Expand Down Expand Up @@ -81,7 +81,7 @@ PS C:\Users\admin> Get-OtpAuthCredential -Issuer "Example" | Get-OtpAuthCode
* PowerShell 7.4

## Local
Clone this repository and execute `build-and-load-local.ps1` in a PowerShell window to build the module and import it into the current session. By default, when building locally the module is named `OtpAuth.Local`.
Clone this repository and execute `build-and-load.ps1` in a PowerShell window to build the module and import it into the current session. By default, when building locally the module is named `OtpAuth.Local`.

## Pipelines
powershell-outauth-module build pipelines use GitHub Actions workflows.
Expand Down
3 changes: 2 additions & 1 deletion src/OtpAuth.PowerShell/Cmdlet/Code/Get_OtpAuthCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ protected override void ProcessRecord() {

var size = GetSize(Credential);
var mode = GetMode(Credential);
var period = Credential.Period != 0 ? Credential.Period : CredentialModel.DefaultPeriod;
var keyAsB64 = Credential.Secret.ReadChars();
var key = Convert.FromBase64CharArray(keyAsB64, 0, keyAsB64.Length);

Expand All @@ -32,7 +33,7 @@ protected override void ProcessRecord() {

} else if (Credential.Type == Model.OtpType.TOTP) {

var totp = new Totp(key, 30, mode, size);
var totp = new Totp(key, period, mode, size);
var code = totp.ComputeTotp();

WriteObject(GetResult(code));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using SkiaSharp;
using System;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Web;
using ZXing;
Expand All @@ -13,6 +14,8 @@ namespace OtpAuth.PowerShell.Cmdlet.Credential {
[Cmdlet(VerbsData.Import, "OtpAuthCredential")]
public class Import_OtpAuthCredential : CmdletBase {

private static readonly StringComparison mCmp = StringComparison.InvariantCultureIgnoreCase;

[Parameter(Mandatory = true, ValueFromPipeline = true)]
public string Path { get; set; }

Expand Down Expand Up @@ -41,19 +44,52 @@ protected override void ProcessRecord() {
}

var uri = new Uri(result?.Text);
var credentials = GetCredentialModels(uri);

foreach (var entry in credentials) {
WriteObject(entry);
}
}

private static CredentialModel[] GetCredentialModels(Uri uri) {

if (String.Equals(uri.Scheme, "otpauth-migration", mCmp)) {
return ProcessMigrationUri(uri);
}

if (String.Equals(uri.Scheme, "otpauth", mCmp)) {
return ProcessOtpAuthUri(uri);
}

return null;
}

private static CredentialModel[] ProcessMigrationUri(Uri uri) {

// SEE: https://alexbakker.me/post/parsing-google-auth-export-qr-code.html

var args = HttpUtility.ParseQueryString(uri.Query);
var payload = args.Get("data").Replace(' ', '+');
var payloadAsBytes = Convert.FromBase64String(payload);
var data = args.Get("data")?.Replace(' ', '+');
var payloadAsBytes = Convert.FromBase64String(data);

OtpMigrationPayload model = null;
OtpMigrationPayload payload = null;

using (var stream = new MemoryStream(payloadAsBytes)) {
model = Serializer.Deserialize<OtpMigrationPayload>(stream);
payload = Serializer.Deserialize<OtpMigrationPayload>(stream);
}

foreach (var entry in model.OtpParameters) {
WriteObject(CredentialModel.FromParameters(entry));
}
return payload
.OtpParameters
.Select(CredentialModel.FromParameters)
.ToArray();
}

private static CredentialModel[] ProcessOtpAuthUri(Uri uri) {

var payload = OtpAuthPayload.FromUri(uri);
var result = CredentialModel.FromAuthPayload(payload);

return [result];
}

private static Result Decode(string fullPath) {
Expand Down Expand Up @@ -81,7 +117,5 @@ private static Result Decode(string fullPath) {

return reader.Decode(bitmap);
}

}

}
}
41 changes: 38 additions & 3 deletions src/OtpAuth.PowerShell/Model/CredentialModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ namespace OtpAuth.PowerShell.Model {

public class CredentialModel {

public const int DefaultPeriod = 30;

public string Id { get; set; }

public string Name { get; set; }
Expand All @@ -22,25 +24,50 @@ public class CredentialModel {

public long Counter { get; set; }

public int Period { get; set; }

public DateTimeOffset Created { get; set; }

public DateTimeOffset Updated { get; set; }

public CredentialModel() {

Id = Guid.NewGuid().ToString();
}

public static CredentialModel FromParameters(OtpMigrationParameters parameters) {

var id = Guid.NewGuid().ToString();
var now = DateTimeOffset.UtcNow;
var secret = Convert.ToBase64String(parameters.Secret);

return new CredentialModel {
Id = id,
Name = parameters.Name ?? "(undefined)",
Secret = new ProtectedString(true, secret),
Issuer = parameters.Issuer ?? "(undefined)",
Algorithm = parameters.Algorithm,
Digits = parameters.Digits,
Type = parameters.Type,
Counter = parameters.Counter,
Period = DefaultPeriod,
Created = now,
Updated = now
};
}

public static CredentialModel FromAuthPayload(OtpAuthPayload payload) {

var now = DateTimeOffset.UtcNow;
var secret = Convert.ToBase64String(payload.Secret);

return new CredentialModel {
Name = payload.Name ?? "(undefined)",
Secret = new ProtectedString(true, secret),
Issuer = payload.Issuer ?? "(undefined)",
Algorithm = payload.Algorithm,
Digits = payload.Digits,
Type = payload.Type,
Counter = payload.Counter,
Period = payload.Period,
Created = now,
Updated = now
};
Expand All @@ -61,7 +88,7 @@ public OtpMigrationParameters ToParameters() {

public static CredentialModel FromKeePassEntry(PwEntry entry) {

return new CredentialModel {
var result = new CredentialModel {
Id = entry.Strings.ReadSafe("Id").Trim(),
Name = entry.Strings.ReadSafe("Title").Trim(),
Secret = entry.Strings.Get("Password"),
Expand All @@ -70,9 +97,16 @@ public static CredentialModel FromKeePassEntry(PwEntry entry) {
Digits = Enum.Parse<OtpDigitCount>(entry.Strings.ReadSafe("Digits").Trim()),
Type = Enum.Parse<OtpType>(entry.Strings.ReadSafe("Type").Trim()),
Counter = Convert.ToInt64(entry.Strings.ReadSafe("Counter").Trim()),
Period = DefaultPeriod,
Created = DateTimeOffset.Parse(entry.Strings.ReadSafe("Created").Trim()),
Updated = DateTimeOffset.Parse(entry.Strings.ReadSafe("Updated").Trim())
};

if (entry.Strings.Exists("Period")) {
result.Period = Convert.ToInt32(entry.Strings.ReadSafe("Period").Trim());
}

return result;
}

public PwEntry ToKeePassEntry() {
Expand All @@ -87,6 +121,7 @@ public PwEntry ToKeePassEntry() {
result.Strings.Set("Digits", new ProtectedString(true, Convert.ToString((int)Digits)));
result.Strings.Set("Type", new ProtectedString(true, Convert.ToString((int)Type)));
result.Strings.Set("Counter", new ProtectedString(true, Convert.ToString(Counter)));
result.Strings.Set("Period", new ProtectedString(true, Convert.ToString(Period)));
result.Strings.Set("Created", new ProtectedString(true, Created.ToString("O")));
result.Strings.Set("Updated", new ProtectedString(true, Updated.ToString("O")));

Expand Down
78 changes: 78 additions & 0 deletions src/OtpAuth.PowerShell/Model/OtpAuthPayload.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using OtpNet;
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Web;

namespace OtpAuth.PowerShell.Model {
public class OtpAuthPayload {

public byte[] Secret { get; set; }

public string Name { get; set; }

public string Issuer { get; set; }

public OtpAlgorithm Algorithm { get; set; }

public OtpDigitCount Digits { get; set; }

public OtpType Type { get; set; }

public long Counter { get; set; }

public int Period { get; set; }

public static OtpAuthPayload FromUri(Uri uri) {

//SEE: https://docs.yubico.com/yesdk/users-manual/application-oath/uri-string-format.html

var type = String.Equals(uri.Host, "totp", StringComparison.InvariantCultureIgnoreCase) ? OtpType.TOTP : OtpType.HOTP;
var issuerAndAccountName = uri.LocalPath.Split(':');
var issuer = issuerAndAccountName?.FirstOrDefault()?.Substring(1);
var accountName = issuerAndAccountName?.LastOrDefault();

var args = HttpUtility.ParseQueryString(uri.Query);

var secret = args.Get("secret");
var digitsAsNumber = GetNumberOrDefaultValue(args, "digits", 6);
var counter = GetNumberOrDefaultValue(args, "counter", 0);
var period = GetNumberOrDefaultValue(args, "period", 30);

var digits = digitsAsNumber == 6 ? OtpDigitCount.Six
: digitsAsNumber == 7 ? OtpDigitCount.Seven
: digitsAsNumber == 8 ? OtpDigitCount.Eight
: OtpDigitCount.Unspecified;

if (!Enum.TryParse<OtpAlgorithm>(args.Get("algorithm"), out var algorithm)) {
algorithm = OtpAlgorithm.SHA1;
}

return new OtpAuthPayload {
Secret = Base32Encoding.ToBytes(secret),
Name = accountName,
Issuer = issuer,
Algorithm = algorithm,
Digits = digits,
Type = type,
Counter = counter,
Period = period
};
}

private static int GetNumberOrDefaultValue(NameValueCollection args, string key, int defaultValue) {

var value = args.Get(key);

if (String.IsNullOrWhiteSpace(value)) {
return defaultValue;
}

if (Int32.TryParse(value, out var result)) {
return result;
}

return defaultValue;
}
}
}
1 change: 1 addition & 0 deletions src/OtpAuth.PowerShell/Model/OtpDigitCount.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
public enum OtpDigitCount {
Unspecified = 0,
Six = 1,
Seven = 3,
Eight = 2
}
}

0 comments on commit 7aef6fa

Please sign in to comment.