feat: add initial implementation of Go utilities and CI workflows

This commit is contained in:
2025-06-18 16:46:01 -04:00
commit c588ade142
12 changed files with 475 additions and 0 deletions

114
Yubikey/otp.go Executable file
View File

@@ -0,0 +1,114 @@
package Yubikey
import (
"errors"
"fmt"
"log"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
)
type Response struct {
h string
t time.Time
otp string
presentedNonce string
nonce string
sl uint8
timestamp uint32
sessionCounter uint32
sessionUse uint32
status string
}
const keyLen = 40
func (response *Response) Time() time.Time {
return response.t
}
func (response *Response) TimeStamp() uint32 {
return response.timestamp
}
func (response *Response) Valid() bool {
return response.status == "OK" && response.nonce == response.presentedNonce
}
//goland:noinspection GoUnusedExportedFunction // library function
func GetVerification(otp string, clientId string) (*Response, error) {
nonce := randSeq(keyLen)
url := fmt.Sprintf(
"https://api.yubico.com/wsapi/2.0/verify?id=%s&otp=%s&timestamp=1&nonce=%s",
clientId,
otp,
nonce,
)
response, _ := http.Get(url)
defer func() {
_ = response.Body.Close()
}()
body := make([]byte, response.ContentLength)
_, err := response.Body.Read(body)
if err != nil {
log.Fatal(err)
}
parts := strings.Split(string(body), "\r\n")
var status string
for k, part := range parts {
if strings.Contains(part, "status=") {
status = strings.Replace(parts[k], "status=", "", 1)
break
}
}
if status != "OK" {
return nil, errors.New("invalid response status: " + status)
}
timeString := strings.Replace(parts[1], "t=", "", 1)[0:20]
signedTime, err := time.Parse(time.RFC3339, timeString)
if err != nil {
println(err)
}
sl, _ := strconv.ParseInt(strings.Replace(parts[4], "sl=", "", 1), 10, 8)
timestamp, _ := strconv.ParseInt(strings.Replace(parts[5], "timestamp=", "", 1), 10, 32)
sessionCounter, _ := strconv.ParseInt(strings.Replace(parts[6], "sessioncounter=", "", 1), 10, 32)
sessionUse, _ := strconv.ParseInt(strings.Replace(parts[7], "sessionuse=", "", 1), 10, 32)
keyResponse := Response{
h: strings.Replace(parts[0], "h=", "", 1),
t: signedTime,
otp: strings.Replace(parts[2], "otp=", "", 1),
presentedNonce: nonce,
nonce: strings.Replace(parts[3], "nonce=", "", 1),
sl: uint8(sl),
timestamp: uint32(timestamp),
sessionCounter: uint32(sessionCounter),
sessionUse: uint32(sessionUse),
status: status,
}
return &keyResponse, nil
}
func randSeq(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune, n)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}

44
Yubikey/otp_test.go Normal file
View File

@@ -0,0 +1,44 @@
package Yubikey
import (
"testing"
"time"
)
// Test the Valid method of Response.
func TestResponse_Valid(t *testing.T) {
resp := &Response{
status: "OK",
nonce: "abc123",
presentedNonce: "abc123",
}
if !resp.Valid() {
t.Errorf("Expected Valid() to return true")
}
resp.status = "BAD"
if resp.Valid() {
t.Errorf("Expected Valid() to return false when status is not OK")
}
resp.status = "OK"
resp.nonce = "xyz"
if resp.Valid() {
t.Errorf("Expected Valid() to return false when nonce does not match presentedNonce")
}
}
// Test the Time and TimeStamp methods.
func TestResponse_TimeAndTimeStamp(t *testing.T) {
now := time.Now()
resp := &Response{
t: now,
timestamp: 12345,
}
if !resp.Time().Equal(now) {
t.Errorf("Expected Time() to return the correct time")
}
if resp.TimeStamp() != 12345 {
t.Errorf("Expected TimeStamp() to return 12345")
}
}