P2FA/go/totp.go

200 lines
4.0 KiB
Go

package main
import (
"C"
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"github.com/creachadair/otp"
"hash"
"log"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"unsafe"
)
var token string
var homeDir string
var configs map[string]otp.Config
func getSha1() func() hash.Hash { return sha1.New }
//export loadConfigs
func loadConfigs() {
osVar := runtime.GOOS
if osVar == "windows" {
homeDir = os.Getenv("USERPROFILE")
} else {
homeDir = os.Getenv("HOME")
}
configs = make(map[string]otp.Config)
files, err := os.ReadDir(homeDir + "/.totp")
if err != nil {
log.Fatal(err)
}
for _, fileEntry := range files {
fileBytes, err := os.ReadFile(homeDir + "/.totp/" + fileEntry.Name())
if err != nil {
log.Fatal(err)
}
fileStr := string(fileBytes)
lineSplt := strings.Split(fileStr, "\n")
var key string
var hashVar func() hash.Hash
var timeStep func() uint64
var counter uint64
var digits int
var format func(hash []byte, length int) string
for _, line := range lineSplt {
kv := strings.Split(line, ":")
if len(kv) < 2 {
continue
}
kv[1] = strings.TrimSpace(kv[1])
switch strings.ToLower(kv[0]) {
case "key":
key = kv[1]
case "hash":
{
switch kv[1] {
case "sha1":
hashVar = getSha1()
case "sha256":
hashVar = func() hash.Hash { return sha256.New() }
case "sha512":
hashVar = func() hash.Hash { return sha512.New() }
case "hmac":
{
keyBytes, err := otp.ParseKey(key)
if err != nil {
log.Fatal(err)
}
hashVar = func() hash.Hash { return hmac.New(getSha1(), keyBytes) } // Probably not proper implementation but who uses HMAC anyway
}
}
}
case "timestep":
{
ts, err := strconv.Atoi(kv[1])
if err != nil {
log.Fatal(err)
}
timeStep = otp.TimeWindow(ts)
}
case "counter":
{
cNum, err := strconv.ParseUint(kv[1], 10, 0)
if err != nil {
log.Fatal(err)
}
if cNum < 6 || cNum > 10 {
log.Fatal("Counter not in range 6-10")
}
counter = cNum
}
case "digits":
{
dNum, err := strconv.Atoi(kv[1])
if err != nil {
log.Fatal(err)
}
if dNum < 1 || dNum > 10 {
log.Fatal("Digits is not in range 1-10")
}
digits = dNum
}
case "format":
{
} //TODO implement
}
}
config := otp.Config{
Hash: hashVar,
TimeStep: timeStep,
Counter: counter,
Digits: digits,
Format: format,
}
err = config.ParseKey(key)
if err != nil {
log.Fatal(err)
}
fileName := fileEntry.Name()
ext := filepath.Ext(fileName)
configs[strings.TrimSuffix(fileName, ext)] = config
}
}
//export getConfigNames
func getConfigNames() (**C.char, C.int) {
configsLen := len(configs)
cArray := C.malloc(C.size_t(configsLen) * C.size_t(unsafe.Sizeof(uintptr(0))))
ar := unsafe.Slice((**C.char)(cArray), configsLen)
i := 0
for name := range configs {
ar[i] = C.CString(name)
i++
}
return (**C.char)(cArray), C.int(configsLen)
}
//export getConfig
func getConfig(configName string) (*C.char, C.int) {
files, err := os.ReadDir(homeDir + "/.totp")
if err != nil {
log.Fatal(err)
}
for _, fileEntry := range files {
if strings.HasPrefix(fileEntry.Name(), configName) {
fileBytes, err := os.ReadFile(homeDir + "/.totp/" + fileEntry.Name())
if err != nil {
log.Fatal(err)
}
fileStr := string(fileBytes)
cStr := C.CString(fileStr)
runtime.KeepAlive(fileStr)
return (*C.char)(cStr), (C.int)(len(fileStr))
}
}
return nil, -1
}
//export getCode
func getCode(configName string) *C.char {
code := configs[configName].TOTP()
cCode := C.CString(code)
runtime.KeepAlive(code)
return cCode
}
//export getTimeRemaining
func getTimeRemaining(n int) uint64 {
return uint64(n) - (uint64(time.Now().Unix()) % uint64(n))
}
//export getTimeRemainingMS
func getTimeRemainingMS(n int) uint64 {
return (uint64(n) * 1000) - (uint64(time.Now().UnixNano()/1e6) % (uint64(n) * 1000))
}
func main() {}