migrate nkode-core

This commit is contained in:
2025-01-21 13:18:46 -06:00
parent 4dbb4c48c8
commit 1f10af0081
38 changed files with 4167 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
.idea
icons/
json_icon/
flaticon_colored_svgs/

6
Taskfile.yaml Normal file
View File

@@ -0,0 +1,6 @@
version: "3"
tasks:
sql:
cmds:
- sqlc generate

266
api/nkode_api.go Normal file
View File

@@ -0,0 +1,266 @@
package api
import (
"fmt"
"git.infra.nkode.tech/dkelly/nkode-core/config"
"git.infra.nkode.tech/dkelly/nkode-core/email"
"git.infra.nkode.tech/dkelly/nkode-core/entities"
"git.infra.nkode.tech/dkelly/nkode-core/repository"
"git.infra.nkode.tech/dkelly/nkode-core/security"
"github.com/google/uuid"
"github.com/patrickmn/go-cache"
"log"
"os"
"time"
)
const (
sessionExpiration = 5 * time.Minute
sessionCleanupInterval = 10 * time.Minute
)
type NKodeAPI struct {
Db repository.CustomerUserRepository
SignupSessionCache *cache.Cache
EmailQueue *email.Queue
}
func NewNKodeAPI(db repository.CustomerUserRepository, queue *email.Queue) NKodeAPI {
return NKodeAPI{
Db: db,
EmailQueue: queue,
SignupSessionCache: cache.New(sessionExpiration, sessionCleanupInterval),
}
}
func (n *NKodeAPI) CreateNewCustomer(nkodePolicy entities.NKodePolicy, id *entities.CustomerId) (*entities.CustomerId, error) {
newCustomer, err := entities.NewCustomer(nkodePolicy)
if id != nil {
newCustomer.Id = *id
}
if err != nil {
return nil, err
}
err = n.Db.CreateCustomer(*newCustomer)
if err != nil {
return nil, err
}
return &newCustomer.Id, nil
}
func (n *NKodeAPI) GenerateSignupResetInterface(userEmail entities.UserEmail, customerId entities.CustomerId, kp entities.KeypadDimension, reset bool) (*entities.SignupResetInterface, error) {
user, err := n.Db.GetUser(userEmail, customerId)
if err != nil {
return nil, err
}
if user != nil && !reset {
log.Printf("user %s already exists", string(userEmail))
return nil, config.ErrUserAlreadyExists
}
svgIdxInterface, err := n.Db.RandomSvgIdxInterface(kp)
if err != nil {
return nil, err
}
signupSession, err := entities.NewSignupResetSession(userEmail, kp, customerId, svgIdxInterface, reset)
if err != nil {
return nil, err
}
if err := n.SignupSessionCache.Add(signupSession.Id.String(), *signupSession, sessionExpiration); err != nil {
return nil, err
}
svgInterface, err := n.Db.GetSvgStringInterface(signupSession.LoginUserInterface.SvgId)
if err != nil {
return nil, err
}
resp := entities.SignupResetInterface{
UserIdxInterface: signupSession.SetIdxInterface,
SvgInterface: svgInterface,
SessionId: uuid.UUID(signupSession.Id).String(),
Colors: signupSession.Colors,
}
return &resp, nil
}
func (n *NKodeAPI) SetNKode(customerId entities.CustomerId, sessionId entities.SessionId, keySelection entities.KeySelection) (entities.IdxInterface, error) {
_, err := n.Db.GetCustomer(customerId)
if err != nil {
return nil, err
}
session, exists := n.SignupSessionCache.Get(sessionId.String())
if !exists {
log.Printf("session id does not exist %s", sessionId)
return nil, config.ErrSignupSessionDNE
}
userSession, ok := session.(entities.UserSignSession)
if !ok {
// handle the case where the type assertion fails
return nil, config.ErrSignupSessionDNE
}
confirmInterface, err := userSession.SetUserNKode(keySelection)
if err != nil {
return nil, err
}
n.SignupSessionCache.Set(sessionId.String(), userSession, sessionExpiration)
return confirmInterface, nil
}
func (n *NKodeAPI) ConfirmNKode(customerId entities.CustomerId, sessionId entities.SessionId, keySelection entities.KeySelection) error {
session, exists := n.SignupSessionCache.Get(sessionId.String())
if !exists {
log.Printf("session id does not exist %s", sessionId)
return config.ErrSignupSessionDNE
}
userSession, ok := session.(entities.UserSignSession)
if !ok {
// handle the case where the type assertion fails
return config.ErrSignupSessionDNE
}
customer, err := n.Db.GetCustomer(customerId)
if err != nil {
return err
}
passcode, err := userSession.DeducePasscode(keySelection)
if err != nil {
return err
}
if err = customer.IsValidNKode(userSession.Kp, passcode); err != nil {
return err
}
user, err := entities.NewUser(*customer, string(userSession.UserEmail), passcode, userSession.LoginUserInterface, userSession.Kp)
if err != nil {
return err
}
if userSession.Reset {
err = n.Db.UpdateUserNKode(*user)
} else {
err = n.Db.WriteNewUser(*user)
}
n.SignupSessionCache.Delete(userSession.Id.String())
return err
}
func (n *NKodeAPI) GetLoginInterface(userEmail entities.UserEmail, customerId entities.CustomerId) (*entities.LoginInterface, error) {
user, err := n.Db.GetUser(userEmail, customerId)
if err != nil {
return nil, err
}
if user == nil {
log.Printf("user %s for customer %s dne", userEmail, customerId)
return nil, config.ErrUserForCustomerDNE
}
svgInterface, err := n.Db.GetSvgStringInterface(user.Interface.SvgId)
if err != nil {
return nil, err
}
resp := entities.LoginInterface{
UserIdxInterface: user.Interface.IdxInterface,
SvgInterface: svgInterface,
NumbOfKeys: user.Kp.NumbOfKeys,
AttrsPerKey: user.Kp.AttrsPerKey,
Colors: entities.SetColors,
}
return &resp, nil
}
func (n *NKodeAPI) Login(customerId entities.CustomerId, userEmail entities.UserEmail, keySelection entities.KeySelection) (*security.AuthenticationTokens, error) {
customer, err := n.Db.GetCustomer(customerId)
if err != nil {
return nil, err
}
user, err := n.Db.GetUser(userEmail, customerId)
if err != nil {
return nil, err
}
if user == nil {
log.Printf("user %s for customer %s dne", userEmail, customerId)
return nil, config.ErrUserForCustomerDNE
}
passcode, err := entities.ValidKeyEntry(*user, *customer, keySelection)
if err != nil {
return nil, err
}
if user.Renew {
err = n.Db.RefreshUserPasscode(*user, passcode, customer.Attributes)
if err != nil {
return nil, err
}
}
jwtToken, err := security.NewAuthenticationTokens(string(user.Email), uuid.UUID(customerId))
if err != nil {
return nil, err
}
if err = n.Db.UpdateUserRefreshToken(user.Id, jwtToken.RefreshToken); err != nil {
return nil, err
}
if err = user.Interface.LoginShuffle(); err != nil {
return nil, err
}
if err = n.Db.UpdateUserInterface(user.Id, user.Interface); err != nil {
return nil, err
}
return &jwtToken, nil
}
func (n *NKodeAPI) RenewAttributes(customerId entities.CustomerId) error {
return n.Db.Renew(customerId)
}
func (n *NKodeAPI) RandomSvgInterface() ([]string, error) {
return n.Db.RandomSvgInterface(entities.KeypadMax)
}
func (n *NKodeAPI) RefreshToken(userEmail entities.UserEmail, customerId entities.CustomerId, refreshToken string) (string, error) {
user, err := n.Db.GetUser(userEmail, customerId)
if err != nil {
return "", err
}
if user == nil {
log.Printf("user %s for customer %s dne", userEmail, customerId)
return "", config.ErrUserForCustomerDNE
}
if user.RefreshToken != refreshToken {
return "", config.ErrRefreshTokenInvalid
}
refreshClaims, err := security.ParseRegisteredClaimToken(refreshToken)
if err != nil {
return "", err
}
if err = security.ClaimExpired(*refreshClaims); err != nil {
return "", err
}
newAccessClaims := security.NewAccessClaim(string(userEmail), uuid.UUID(customerId))
return security.EncodeAndSignClaims(newAccessClaims)
}
func (n *NKodeAPI) ResetNKode(userEmail entities.UserEmail, customerId entities.CustomerId) error {
user, err := n.Db.GetUser(userEmail, customerId)
if err != nil {
return fmt.Errorf("error getting user in rest nkode %v", err)
}
if user == nil {
return nil
}
nkodeResetJwt, err := security.ResetNKodeToken(string(userEmail), uuid.UUID(customerId))
if err != nil {
return err
}
frontendHost := os.Getenv("FRONTEND_HOST")
if frontendHost == "" {
frontendHost = config.FrontendHost
}
htmlBody := fmt.Sprintf("<h1>Hello!</h1><p>Click the link to reset your nKode.</p><a href=\"%s?token=%s\">Reset nKode</a>", frontendHost, nkodeResetJwt)
email := email.Email{
Sender: "no-reply@nkode.tech",
Recipient: string(userEmail),
Subject: "nKode Reset",
Content: htmlBody,
}
n.EmailQueue.AddEmail(email)
return nil
}

124
api/nkode_api_test.go Normal file
View File

@@ -0,0 +1,124 @@
package api
import (
"context"
"git.infra.nkode.tech/dkelly/nkode-core/email"
"git.infra.nkode.tech/dkelly/nkode-core/entities"
"git.infra.nkode.tech/dkelly/nkode-core/repository"
"git.infra.nkode.tech/dkelly/nkode-core/security"
"git.infra.nkode.tech/dkelly/nkode-core/sqlc"
"github.com/stretchr/testify/assert"
"log"
"os"
"testing"
)
func TestNKodeAPI(t *testing.T) {
//db1 := NewInMemoryDb()
//testNKodeAPI(t, &db1)
dbPath := os.Getenv("TEST_DB")
ctx := context.Background()
sqliteDb, err := sqlc.OpenSqliteDb(dbPath)
assert.NoError(t, err)
queue, err := sqlc.NewQueue(sqliteDb, ctx)
assert.NoError(t, err)
queue.Start()
defer func(queue *sqlc.Queue) {
if err := queue.Stop(); err != nil {
log.Fatal(err)
}
}(queue)
sqlitedb := repository.NewSqliteRepository(queue, ctx)
testNKodeAPI(t, &sqlitedb)
//if _, err := os.Stat(dbPath); err == nil {
// err = os.Remove(dbPath)
// assert.NoError(t, err)
//} else {
// assert.NoError(t, err)
//}
}
func testNKodeAPI(t *testing.T, db repository.CustomerUserRepository) {
bufferSize := 100
emailsPerSec := 14
testClient := email.TestEmailClient{}
queue := email.NewEmailQueue(bufferSize, emailsPerSec, &testClient)
queue.Start()
defer queue.Stop()
attrsPerKey := 5
numbOfKeys := 4
for idx := 0; idx < 1; idx++ {
userEmail := entities.UserEmail("test_username" + security.GenerateRandomString(12) + "@example.com")
passcodeLen := 4
nkodePolicy := entities.NewDefaultNKodePolicy()
keypadSize := entities.KeypadDimension{AttrsPerKey: attrsPerKey, NumbOfKeys: numbOfKeys}
nkodeApi := NewNKodeAPI(db, queue)
customerId, err := nkodeApi.CreateNewCustomer(nkodePolicy, nil)
assert.NoError(t, err)
signupResponse, err := nkodeApi.GenerateSignupResetInterface(userEmail, *customerId, keypadSize, false)
assert.NoError(t, err)
setInterface := signupResponse.UserIdxInterface
sessionIdStr := signupResponse.SessionId
sessionId, err := entities.SessionIdFromString(sessionIdStr)
assert.NoError(t, err)
keypadSize = entities.KeypadDimension{AttrsPerKey: numbOfKeys, NumbOfKeys: numbOfKeys}
userPasscode := setInterface[:passcodeLen]
setKeySelect, err := entities.SelectKeyByAttrIdx(setInterface, userPasscode, keypadSize)
assert.NoError(t, err)
confirmInterface, err := nkodeApi.SetNKode(*customerId, sessionId, setKeySelect)
assert.NoError(t, err)
confirmKeySelect, err := entities.SelectKeyByAttrIdx(confirmInterface, userPasscode, keypadSize)
err = nkodeApi.ConfirmNKode(*customerId, sessionId, confirmKeySelect)
assert.NoError(t, err)
keypadSize = entities.KeypadDimension{AttrsPerKey: attrsPerKey, NumbOfKeys: numbOfKeys}
loginInterface, err := nkodeApi.GetLoginInterface(userEmail, *customerId)
assert.NoError(t, err)
loginKeySelection, err := entities.SelectKeyByAttrIdx(loginInterface.UserIdxInterface, userPasscode, keypadSize)
assert.NoError(t, err)
_, err = nkodeApi.Login(*customerId, userEmail, loginKeySelection)
assert.NoError(t, err)
err = nkodeApi.RenewAttributes(*customerId)
assert.NoError(t, err)
loginInterface, err = nkodeApi.GetLoginInterface(userEmail, *customerId)
assert.NoError(t, err)
loginKeySelection, err = entities.SelectKeyByAttrIdx(loginInterface.UserIdxInterface, userPasscode, keypadSize)
assert.NoError(t, err)
_, err = nkodeApi.Login(*customerId, userEmail, loginKeySelection)
assert.NoError(t, err)
/// Reset nKode
attrsPerKey = 6
keypadSize = entities.KeypadDimension{AttrsPerKey: attrsPerKey, NumbOfKeys: numbOfKeys}
resetResponse, err := nkodeApi.GenerateSignupResetInterface(userEmail, *customerId, keypadSize, true)
assert.NoError(t, err)
setInterface = resetResponse.UserIdxInterface
sessionIdStr = resetResponse.SessionId
sessionId, err = entities.SessionIdFromString(sessionIdStr)
assert.NoError(t, err)
keypadSize = entities.KeypadDimension{AttrsPerKey: numbOfKeys, NumbOfKeys: numbOfKeys}
userPasscode = setInterface[:passcodeLen]
setKeySelect, err = entities.SelectKeyByAttrIdx(setInterface, userPasscode, keypadSize)
assert.NoError(t, err)
confirmInterface, err = nkodeApi.SetNKode(*customerId, sessionId, setKeySelect)
assert.NoError(t, err)
confirmKeySelect, err = entities.SelectKeyByAttrIdx(confirmInterface, userPasscode, keypadSize)
err = nkodeApi.ConfirmNKode(*customerId, sessionId, confirmKeySelect)
assert.NoError(t, err)
keypadSize = entities.KeypadDimension{AttrsPerKey: attrsPerKey, NumbOfKeys: numbOfKeys}
loginInterface2, err := nkodeApi.GetLoginInterface(userEmail, *customerId)
assert.NoError(t, err)
loginKeySelection, err = entities.SelectKeyByAttrIdx(loginInterface2.UserIdxInterface, userPasscode, keypadSize)
assert.NoError(t, err)
_, err = nkodeApi.Login(*customerId, userEmail, loginKeySelection)
assert.NoError(t, err)
signupResponse, err = nkodeApi.GenerateSignupResetInterface(userEmail, *customerId, keypadSize, false)
assert.Error(t, err)
}
}

258
cmd/nkode/nkode.go Normal file
View File

@@ -0,0 +1,258 @@
package main
import (
"database/sql"
"encoding/json"
"fmt"
_ "github.com/mattn/go-sqlite3" // Import the SQLite3 driver
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
)
type Icon struct {
Body string `json:"body"`
Width *int `json:"width,omitempty"`
}
// Root Define the Root struct to represent the entire JSON structure
type Root struct {
Prefix string `json:"prefix"`
Icons map[string]Icon `json:"icons"`
Height int `json:"height"`
}
func main() {
testDbPath := os.Getenv("TEST_DB_PATH")
dbPath := os.Getenv("DB_PATH")
dbPaths := []string{testDbPath, dbPath}
flaticonSvgDir := os.Getenv("SVG_DIR")
//dbPath := "/Users/donov/Desktop/nkode.db"
//dbPaths := []string{dbPath}
//outputStr := MakeSvgFiles()
for _, path := range dbPaths {
MakeTables(path)
FlaticonToSqlite(path, flaticonSvgDir)
//SvgToSqlite(path, outputStr)
}
}
func FlaticonToSqlite(dbPath string, svgDir string) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Open the directory
files, err := os.ReadDir(svgDir)
if err != nil {
log.Fatal(err)
}
for _, file := range files {
// Check if it is a regular file (not a directory) and has a .svg extension
if file.IsDir() || filepath.Ext(file.Name()) != ".svg" {
continue
}
filePath := filepath.Join(svgDir, file.Name())
// Read the file contents
content, err := os.ReadFile(filePath)
if err != nil {
log.Println("Error reading file:", filePath, err)
continue
}
// Print the file name and first few bytes of the file content
insertSql := `
INSERT INTO svg_icon (svg)
VALUES (?)
`
_, err = db.Exec(insertSql, string(content))
if err != nil {
log.Fatal(err)
}
}
}
func SvgToSqlite(dbPath string, outputStr string) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
log.Fatal(err)
}
defer db.Close()
lines := strings.Split(outputStr, "\n")
insertSql := `
INSERT INTO svg_icon (svg)
VALUES (?)
`
for _, line := range lines {
if line == "" {
continue
}
_, err := db.Exec(insertSql, line)
if err != nil {
log.Fatal(err)
}
}
}
func MakeSvgFiles() string {
jsonFiles, err := GetAllFiles("./core/sqlite-init/json")
if err != nil {
log.Fatalf("Error getting JSON files: %v", err)
}
if len(jsonFiles) == 0 {
log.Fatal("No JSON files found in ./json folder")
}
var outputStr string
strSet := make(map[string]struct{})
for _, filename := range jsonFiles {
fileData, err := LoadJson(filename)
if err != nil {
log.Print("Error loading JSON file: ", err)
continue
}
height := fileData.Height
for name, icon := range fileData.Icons {
width := height
parts := strings.Split(name, "-")
if len(parts) <= 0 {
log.Print(name, " has no parts")
continue
}
part0 := parts[0]
_, exists := strSet[part0]
if exists {
continue
}
if icon.Width != nil {
width = *icon.Width
}
strSet[part0] = struct{}{}
if icon.Body == "" {
continue
}
outputStr = fmt.Sprintf("%s<svg viewBox=\"0 0 %d %d\" xmlns=\"http://www.w3.org/2000/svg\">%s</svg>\n", outputStr, width, height, icon.Body)
}
}
return outputStr
}
func GetAllFiles(dir string) ([]string, error) {
// Use ioutil.ReadDir to list all files in the directory
files, err := ioutil.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("unable to read directory: %v", err)
}
// Create a slice to hold the JSON filenames
var jsonFiles []string
// Loop through the files and filter out JSON files
for _, file := range files {
if !file.IsDir() && filepath.Ext(file.Name()) == ".json" {
jsonFiles = append(jsonFiles, filepath.Join(dir, file.Name()))
}
}
return jsonFiles, nil
}
func LoadJson(filename string) (*Root, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %v", filename, err)
}
var root Root
err = json.Unmarshal(data, &root)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal JSON: %v", err)
}
return &root, nil
}
func MakeTables(dbPath string) {
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
log.Fatal(err)
}
defer db.Close()
createTable := `
PRAGMA journal_mode=WAL;
--PRAGMA busy_timeout = 5000; -- Wait up to 5 seconds
--PRAGMA synchronous = NORMAL; -- Reduce sync frequency for less locking
--PRAGMA cache_size = -16000; -- Increase cache size (16MB)PRAGMA
CREATE TABLE IF NOT EXISTS customer (
id TEXT NOT NULL PRIMARY KEY
,max_nkode_len INTEGER NOT NULL
,min_nkode_len INTEGER NOT NULL
,distinct_sets INTEGER NOT NULL
,distinct_attributes INTEGER NOT NULL
,lock_out INTEGER NOT NULL
,expiration INTEGER NOT NULL
,attribute_values BLOB NOT NULL
,set_values BLOB NOT NULL
,last_renew TEXT NOT NULL
,created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user (
id TEXT NOT NULL PRIMARY KEY
,email TEXT NOT NULL
-- first_name TEXT NOT NULL
-- last_name TEXT NOT NULL
,renew INT NOT NULL
,refresh_token TEXT
,customer_id TEXT NOT NULL
-- Enciphered Passcode
,code TEXT NOT NULL
,mask TEXT NOT NULL
-- Keypad Dimensions
,attributes_per_key INT NOT NULL
,number_of_keys INT NOT NULL
-- User Keys
,alpha_key BLOB NOT NULL
,set_key BLOB NOT NULL
,pass_key BLOB NOT NULL
,mask_key BLOB NOT NULL
,salt BLOB NOT NULL
,max_nkode_len INT NOT NULL
-- User Interface
,idx_interface BLOB NOT NULL
,svg_id_interface BLOB NOT NULL
,last_login TEXT NULL
,created_at TEXT
,FOREIGN KEY (customer_id) REFERENCES customer(id)
,UNIQUE(customer_id, email)
);
CREATE TABLE IF NOT EXISTS svg_icon (
id INTEGER PRIMARY KEY AUTOINCREMENT
,svg TEXT NOT NULL
);
`
_, err = db.Exec(createTable)
if err != nil {
log.Fatal(err)
}
}

5
config/config.go Normal file
View File

@@ -0,0 +1,5 @@
package config
const (
FrontendHost = "https://nkode.tech"
)

66
config/constants.go Normal file
View File

@@ -0,0 +1,66 @@
package config
import (
"errors"
"net/http"
)
var (
ErrInvalidNKodeLength = errors.New("invalid nKode length")
ErrInvalidNKodeIdx = errors.New("invalid passcode attribute index")
ErrTooFewDistinctSet = errors.New("too few distinct sets")
ErrTooFewDistinctAttributes = errors.New("too few distinct attributes")
ErrEmailAlreadySent = errors.New("email already sent")
ErrClaimExpOrNil = errors.New("claim expired or nil")
ErrInvalidJwt = errors.New("invalid jwt")
ErrInvalidKeypadDimensions = errors.New("keypad dimensions out of range")
ErrUserAlreadyExists = errors.New("user already exists")
ErrSignupSessionDNE = errors.New("signup session does not exist")
ErrUserForCustomerDNE = errors.New("user does not exist")
ErrRefreshTokenInvalid = errors.New("refresh token invalid")
ErrCustomerDne = errors.New("customer does not exist")
ErrSvgDne = errors.New("svg ")
ErrStoppingDatabase = errors.New("stopping database")
ErrSqliteTx = errors.New("sqlite begin, exec, query, or commit error. see logs")
ErrEmptySvgTable = errors.New("empty svg_icon table")
ErrKeyIndexOutOfRange = errors.New("one or more keys is out of range")
ErrAttributeIndexOutOfRange = errors.New("attribute index out of range")
ErrInternalValidKeyEntry = errors.New("internal validation error")
ErrUserMaskTooLong = errors.New("user mask length exceeds max nkode length")
ErrInterfaceNotDispersible = errors.New("interface is not dispersible")
ErrIncompleteUserSignupSession = errors.New("incomplete user signup session")
ErrSetConfirmSignupMismatch = errors.New("set and confirm nkode are not the same")
ErrKeypadIsNotDispersible = errors.New("keypad is not dispersible")
ErrInvalidNKode = errors.New("invalid nKode")
ErrStringIsNotAnSVG = errors.New("string is not an svg")
)
var HttpErrMap = map[error]int{
ErrInvalidNKodeLength: http.StatusBadRequest,
ErrInvalidNKodeIdx: http.StatusBadRequest,
ErrTooFewDistinctSet: http.StatusBadRequest,
ErrTooFewDistinctAttributes: http.StatusBadRequest,
ErrEmailAlreadySent: http.StatusBadRequest,
ErrClaimExpOrNil: http.StatusForbidden,
ErrInvalidJwt: http.StatusForbidden,
ErrInvalidKeypadDimensions: http.StatusBadRequest,
ErrUserAlreadyExists: http.StatusBadRequest,
ErrSignupSessionDNE: http.StatusBadRequest,
ErrUserForCustomerDNE: http.StatusBadRequest,
ErrRefreshTokenInvalid: http.StatusForbidden,
ErrCustomerDne: http.StatusBadRequest,
ErrSvgDne: http.StatusBadRequest,
ErrStoppingDatabase: http.StatusInternalServerError,
ErrSqliteTx: http.StatusInternalServerError,
ErrEmptySvgTable: http.StatusInternalServerError,
ErrKeyIndexOutOfRange: http.StatusBadRequest,
ErrAttributeIndexOutOfRange: http.StatusInternalServerError,
ErrInternalValidKeyEntry: http.StatusInternalServerError,
ErrUserMaskTooLong: http.StatusInternalServerError,
ErrInterfaceNotDispersible: http.StatusInternalServerError,
ErrIncompleteUserSignupSession: http.StatusBadRequest,
ErrSetConfirmSignupMismatch: http.StatusBadRequest,
ErrKeypadIsNotDispersible: http.StatusInternalServerError,
ErrInvalidNKode: http.StatusBadRequest,
ErrStringIsNotAnSVG: http.StatusInternalServerError,
}

150
email/queue.go Normal file
View File

@@ -0,0 +1,150 @@
package email
import (
"context"
"fmt"
config2 "git.infra.nkode.tech/dkelly/nkode-core/config"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/ses"
"github.com/aws/aws-sdk-go-v2/service/ses/types"
"github.com/patrickmn/go-cache"
"log"
"sync"
"time"
)
type Client interface {
SendEmail(Email) error
}
type Email struct {
Sender string
Recipient string
Subject string
Content string
}
type TestEmailClient struct{}
func (c *TestEmailClient) SendEmail(email Email) error {
fmt.Printf("Sending email to %s\n", email.Recipient)
return nil
}
type SESClient struct {
ResetCache *cache.Cache
}
const (
emailRetryExpiration = 5 * time.Minute
sesCleanupInterval = 10 * time.Minute
)
func NewSESClient() SESClient {
return SESClient{
ResetCache: cache.New(emailRetryExpiration, sesCleanupInterval),
}
}
func (s *SESClient) SendEmail(email Email) error {
if _, exists := s.ResetCache.Get(email.Recipient); exists {
log.Printf("email already sent to %s with subject %s", email.Recipient, email.Subject)
return config2.ErrEmailAlreadySent
}
cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-east-1"))
if err != nil {
errMsg := fmt.Sprintf("unable to load SDK config, %v", err)
log.Print(errMsg)
return err
}
sesClient := ses.NewFromConfig(cfg)
input := &ses.SendEmailInput{
Destination: &types.Destination{
ToAddresses: []string{email.Recipient},
},
Message: &types.Message{
Body: &types.Body{
Html: &types.Content{
Data: aws.String(email.Content),
},
},
Subject: &types.Content{
Data: aws.String(email.Subject),
},
},
Source: aws.String(email.Sender),
}
if err = s.ResetCache.Add(email.Recipient, nil, emailRetryExpiration); err != nil {
return err
}
resp, err := sesClient.SendEmail(context.TODO(), input)
if err != nil {
s.ResetCache.Delete(email.Recipient)
errMsg := fmt.Sprintf("failed to send email, %v", err)
log.Print(errMsg)
return err
}
fmt.Printf("UserEmail sent successfully, Message ID: %s\n", *resp.MessageId)
return nil
}
type Queue struct {
stop bool
emailQueue chan Email
rateLimit <-chan time.Time
client Client
wg sync.WaitGroup
FailedSendCount int
}
func NewEmailQueue(bufferSize int, emailsPerSecond int, client Client) *Queue {
rateLimit := time.Tick(time.Second / time.Duration(emailsPerSecond))
return &Queue{
stop: false,
emailQueue: make(chan Email, bufferSize),
rateLimit: rateLimit,
client: client,
FailedSendCount: 0,
}
}
func (q *Queue) AddEmail(email Email) {
if q.stop {
log.Printf("email %s with subject %s not add. Stopping queue", email.Recipient, email.Subject)
return
}
q.wg.Add(1)
q.emailQueue <- email
}
func (q *Queue) Start() {
q.stop = false
go func() {
for email := range q.emailQueue {
<-q.rateLimit
q.sendEmail(email)
q.wg.Done()
}
}()
}
func (q *Queue) sendEmail(email Email) {
if err := q.client.SendEmail(email); err != nil {
q.FailedSendCount += 1
log.Printf("Failed to send email to %s: %v\n", email.Recipient, err)
}
}
func (q *Queue) Stop() {
q.stop = true
q.wg.Wait()
close(q.emailQueue)
}

29
email/queue_test.go Normal file
View File

@@ -0,0 +1,29 @@
package email
import (
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)
func TestEmailQueue(t *testing.T) {
queue := NewEmailQueue(100, 14, &TestEmailClient{})
// Start the queue processing
queue.Start()
// Enqueue some emails
for i := 1; i <= 28; i++ {
email := Email{
Sender: "test@example.com",
Recipient: fmt.Sprintf("user%d@example.com", i),
Subject: "test subject",
Content: "This is a test email",
}
queue.AddEmail(email)
}
// Stop the queue after all emails are processed
queue.Stop()
assert.Equal(t, queue.FailedSendCount, 0)
}

102
entities/customer.go Normal file
View File

@@ -0,0 +1,102 @@
package entities
import (
"git.infra.nkode.tech/dkelly/nkode-core/config"
"git.infra.nkode.tech/dkelly/nkode-core/security"
"git.infra.nkode.tech/dkelly/nkode-core/sqlc"
"git.infra.nkode.tech/dkelly/nkode-core/utils"
"github.com/DonovanKelly/sugar-n-spice/set"
"github.com/google/uuid"
)
type Customer struct {
Id CustomerId
NKodePolicy NKodePolicy
Attributes CustomerAttributes
}
func NewCustomer(nkodePolicy NKodePolicy) (*Customer, error) {
customerAttrs, err := NewCustomerAttributes()
if err != nil {
return nil, err
}
customer := Customer{
Id: CustomerId(uuid.New()),
NKodePolicy: nkodePolicy,
Attributes: *customerAttrs,
}
return &customer, nil
}
func (c *Customer) IsValidNKode(kp KeypadDimension, passcodeAttrIdx []int) error {
nkodeLen := len(passcodeAttrIdx)
if nkodeLen < c.NKodePolicy.MinNkodeLen || nkodeLen > c.NKodePolicy.MaxNkodeLen {
return config.ErrInvalidNKodeLength
}
if validIdx := kp.ValidateAttributeIndices(passcodeAttrIdx); !validIdx {
return config.ErrInvalidNKodeIdx
}
passcodeSetVals := make(set.Set[uint64])
passcodeAttrVals := make(set.Set[uint64])
attrVals, err := c.Attributes.AttrValsForKp(kp)
if err != nil {
return err
}
for idx := 0; idx < nkodeLen; idx++ {
attrVal := attrVals[passcodeAttrIdx[idx]]
setVal, err := c.Attributes.GetAttrSetVal(attrVal, kp)
if err != nil {
return err
}
passcodeSetVals.Add(setVal)
passcodeAttrVals.Add(attrVal)
}
if passcodeSetVals.Size() < c.NKodePolicy.DistinctSets {
return config.ErrTooFewDistinctSet
}
if passcodeAttrVals.Size() < c.NKodePolicy.DistinctAttributes {
return config.ErrTooFewDistinctAttributes
}
return nil
}
func (c *Customer) RenewKeys() ([]uint64, []uint64, error) {
oldAttrs := make([]uint64, len(c.Attributes.AttrVals))
oldSets := make([]uint64, len(c.Attributes.SetVals))
copy(oldAttrs, c.Attributes.AttrVals)
copy(oldSets, c.Attributes.SetVals)
if err := c.Attributes.Renew(); err != nil {
return nil, nil, err
}
attrsXor, err := security.XorLists(oldAttrs, c.Attributes.AttrVals)
if err != nil {
return nil, nil, err
}
setXor, err := security.XorLists(oldSets, c.Attributes.SetVals)
if err != nil {
return nil, nil, err
}
return setXor, attrsXor, nil
}
func (c *Customer) ToSqlcCreateCustomerParams() sqlc.CreateCustomerParams {
return sqlc.CreateCustomerParams{
ID: uuid.UUID(c.Id).String(),
MaxNkodeLen: int64(c.NKodePolicy.MaxNkodeLen),
MinNkodeLen: int64(c.NKodePolicy.MinNkodeLen),
DistinctSets: int64(c.NKodePolicy.DistinctSets),
DistinctAttributes: int64(c.NKodePolicy.DistinctAttributes),
LockOut: int64(c.NKodePolicy.LockOut),
Expiration: int64(c.NKodePolicy.Expiration),
AttributeValues: c.Attributes.AttrBytes(),
SetValues: c.Attributes.SetBytes(),
LastRenew: utils.TimeStamp(),
CreatedAt: utils.TimeStamp(),
}
}

View File

@@ -0,0 +1,94 @@
package entities
import (
"git.infra.nkode.tech/dkelly/nkode-core/security"
"log"
)
type CustomerAttributes struct {
AttrVals []uint64
SetVals []uint64
}
func NewCustomerAttributes() (*CustomerAttributes, error) {
attrVals, err := security.GenerateRandomNonRepeatingUint64(KeypadMax.TotalAttrs())
if err != nil {
log.Print("unable to generate attribute vals: ", err)
return nil, err
}
setVals, err := security.GenerateRandomNonRepeatingUint64(KeypadMax.AttrsPerKey)
if err != nil {
log.Print("unable to generate set vals: ", err)
return nil, err
}
customerAttrs := CustomerAttributes{
AttrVals: attrVals,
SetVals: setVals,
}
return &customerAttrs, nil
}
func NewCustomerAttributesFromBytes(attrBytes []byte, setBytes []byte) CustomerAttributes {
return CustomerAttributes{
AttrVals: security.ByteArrToUint64Arr(attrBytes),
SetVals: security.ByteArrToUint64Arr(setBytes),
}
}
func (c *CustomerAttributes) Renew() error {
attrVals, err := security.GenerateRandomNonRepeatingUint64(KeypadMax.TotalAttrs())
if err != nil {
return err
}
setVals, err := security.GenerateRandomNonRepeatingUint64(KeypadMax.AttrsPerKey)
if err != nil {
return err
}
c.AttrVals = attrVals
c.SetVals = setVals
return nil
}
func (c *CustomerAttributes) IndexOfAttr(attrVal uint64) (int, error) {
// TODO: should this be mapped instead?
return security.IndexOf[uint64](c.AttrVals, attrVal)
}
func (c *CustomerAttributes) IndexOfSet(setVal uint64) (int, error) {
// TODO: should this be mapped instead?
return security.IndexOf[uint64](c.SetVals, setVal)
}
func (c *CustomerAttributes) GetAttrSetVal(attrVal uint64, userKeypad KeypadDimension) (uint64, error) {
indexOfAttr, err := c.IndexOfAttr(attrVal)
if err != nil {
return 0, err
}
setIdx := indexOfAttr % userKeypad.AttrsPerKey
return c.SetVals[setIdx], nil
}
func (c *CustomerAttributes) AttrValsForKp(userKp KeypadDimension) ([]uint64, error) {
err := userKp.IsValidKeypadDimension()
if err != nil {
return nil, err
}
return c.AttrVals[:userKp.TotalAttrs()], nil
}
func (c *CustomerAttributes) SetValsForKp(userKp KeypadDimension) ([]uint64, error) {
err := userKp.IsValidKeypadDimension()
if err != nil {
return nil, err
}
return c.SetVals[:userKp.AttrsPerKey], nil
}
func (c *CustomerAttributes) AttrBytes() []byte {
return security.Uint64ArrToByteArr(c.AttrVals)
}
func (c *CustomerAttributes) SetBytes() []byte {
return security.Uint64ArrToByteArr(c.SetVals)
}

57
entities/customer_test.go Normal file
View File

@@ -0,0 +1,57 @@
package entities
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestCustomer(t *testing.T) {
testNewCustomerAttributes(t)
testCustomerValidKeyEntry(t)
testCustomerIsValidNKode(t)
}
func testNewCustomerAttributes(t *testing.T) {
_, nil := NewCustomerAttributes()
assert.NoError(t, nil)
}
func testCustomerValidKeyEntry(t *testing.T) {
kp := KeypadDimension{AttrsPerKey: 10, NumbOfKeys: 9}
nkodePolicy := NewDefaultNKodePolicy()
customer, err := NewCustomer(nkodePolicy)
assert.NoError(t, err)
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
assert.NoError(t, err)
userEmail := "testing@example.com"
passcodeIdx := []int{0, 1, 2, 3}
user, err := NewUser(*customer, userEmail, passcodeIdx, *userInterface, kp)
assert.NoError(t, err)
userLoginInterface, err := user.GetLoginInterface()
assert.NoError(t, err)
selectedKeys, err := SelectKeyByAttrIdx(userLoginInterface, passcodeIdx, kp)
assert.NoError(t, err)
validatedPasscode, err := ValidKeyEntry(*user, *customer, selectedKeys)
assert.NoError(t, err)
assert.Equal(t, len(validatedPasscode), len(passcodeIdx))
for idx := range validatedPasscode {
assert.Equal(t, validatedPasscode[idx], passcodeIdx[idx])
}
}
func testCustomerIsValidNKode(t *testing.T) {
kp := KeypadDimension{AttrsPerKey: 10, NumbOfKeys: 7}
nkodePolicy := NewDefaultNKodePolicy()
customer, err := NewCustomer(nkodePolicy)
assert.NoError(t, err)
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
assert.NoError(t, err)
userEmail := "testing123@example.com"
passcodeIdx := []int{0, 1, 2, 3}
user, err := NewUser(*customer, userEmail, passcodeIdx, *userInterface, kp)
assert.NoError(t, err)
err = customer.IsValidNKode(user.Kp, passcodeIdx)
assert.NoError(t, err)
}

View File

@@ -0,0 +1,55 @@
package entities
import (
"git.infra.nkode.tech/dkelly/nkode-core/config"
"github.com/DonovanKelly/sugar-n-spice/all"
)
type KeypadDimension struct {
AttrsPerKey int `json:"attrs_per_key"`
NumbOfKeys int `json:"numb_of_keys"`
}
func (kp *KeypadDimension) TotalAttrs() int {
return kp.AttrsPerKey * kp.NumbOfKeys
}
func (kp *KeypadDimension) IsDispersable() bool {
return kp.AttrsPerKey <= kp.NumbOfKeys
}
func (kp *KeypadDimension) IsValidKeypadDimension() error {
if KeypadMin.AttrsPerKey > kp.AttrsPerKey || KeypadMax.AttrsPerKey < kp.AttrsPerKey || KeypadMin.NumbOfKeys > kp.NumbOfKeys || KeypadMax.NumbOfKeys < kp.NumbOfKeys {
return config.ErrInvalidKeypadDimensions
}
return nil
}
func (kp *KeypadDimension) ValidKeySelections(selectedKeys []int) bool {
return all.All[int](selectedKeys, func(idx int) bool {
return 0 <= idx && idx < kp.NumbOfKeys
})
}
func (kp *KeypadDimension) ValidateAttributeIndices(attrIndicies []int) bool {
return all.All[int](attrIndicies, func(i int) bool {
return i >= 0 && i < kp.TotalAttrs()
})
}
var (
KeypadMax = KeypadDimension{
AttrsPerKey: 16,
NumbOfKeys: 15,
}
KeypadMin = KeypadDimension{
AttrsPerKey: 5,
NumbOfKeys: 4,
}
KeypadDefault = KeypadDimension{
AttrsPerKey: 14,
NumbOfKeys: 7,
}
)

23
entities/keypad_utils.go Normal file
View File

@@ -0,0 +1,23 @@
package entities
import (
"errors"
"fmt"
"git.infra.nkode.tech/dkelly/nkode-core/security"
)
func SelectKeyByAttrIdx(interfaceUser []int, passcodeIdxs []int, keypadSize KeypadDimension) ([]int, error) {
selectedKeys := make([]int, len(passcodeIdxs))
for idx := range passcodeIdxs {
attrIdx, err := security.IndexOf[int](interfaceUser, passcodeIdxs[idx])
if err != nil {
return nil, err
}
keyNumb := attrIdx / keypadSize.AttrsPerKey
if keyNumb >= keypadSize.NumbOfKeys {
return nil, errors.New(fmt.Sprintf("index key number: %d out of range 0-%d", keyNumb, keypadSize.NumbOfKeys-1))
}
selectedKeys[idx] = keyNumb
}
return selectedKeys, nil
}

101
entities/models.go Normal file
View File

@@ -0,0 +1,101 @@
package entities
import (
"fmt"
"github.com/google/uuid"
"net/mail"
"strings"
)
type KeySelection []int
type CustomerId uuid.UUID
func CustomerIdToString(customerId CustomerId) string {
customerUuid := uuid.UUID(customerId)
return customerUuid.String()
}
type SessionId uuid.UUID
type UserId uuid.UUID
func UserIdFromString(userId string) UserId {
id, err := uuid.Parse(userId)
if err != nil {
fmt.Errorf("unable to parse user id %+v", err)
}
return UserId(id)
}
func (s *SessionId) String() string {
id := uuid.UUID(*s)
return id.String()
}
type UserEmail string
func ParseEmail(email string) (UserEmail, error) {
_, err := mail.ParseAddress(email)
if err != nil {
return "", err
}
return UserEmail(strings.ToLower(email)), err
}
type IdxInterface []int
type SvgIdInterface []int
func SessionIdFromString(sessionId string) (SessionId, error) {
id, err := uuid.Parse(sessionId)
if err != nil {
return SessionId{}, err
}
return SessionId(id), nil
}
type EncipheredNKode struct {
Code string
Mask string
}
type RGBColor struct {
Red int `json:"red"`
Green int `json:"green"`
Blue int `json:"blue"`
}
var SetColors = []RGBColor{
{0, 0, 0}, // Black
{255, 0, 0}, // Red
{0, 128, 0}, // Dark Green
{0, 0, 255}, // Blue
{244, 200, 60}, // Yellow
{255, 0, 255}, // Magenta
{0, 200, 200}, // Cyan
{127, 0, 127}, // Purple
{232, 92, 13}, // Orange
{0, 127, 127}, // Teal
{127, 127, 0}, // Olive
{127, 0, 0}, // Dark Red
{128, 128, 128}, // Gray
{228, 102, 102}, // Dark Purple
{185, 17, 240}, // Salmon
{16, 200, 100}, // Green
}
type SignupResetInterface struct {
SessionId string `json:"session_id"`
UserIdxInterface IdxInterface `json:"user_interface"`
SvgInterface []string `json:"svg_interface"`
Colors []RGBColor `json:"colors"`
}
type LoginInterface struct {
UserIdxInterface IdxInterface `json:"user_interface"`
SvgInterface []string `json:"svg_interface"`
AttrsPerKey int `json:"attrs_per_key"`
NumbOfKeys int `json:"numb_of_keys"`
Colors []RGBColor `json:"colors"`
}

34
entities/policy.go Normal file
View File

@@ -0,0 +1,34 @@
package entities
import "git.infra.nkode.tech/dkelly/nkode-core/config"
type NKodePolicy struct {
MaxNkodeLen int `json:"max_nkode_len"`
MinNkodeLen int `json:"min_nkode_len"`
DistinctSets int `json:"distinct_sets"`
DistinctAttributes int `json:"distinct_attributes"`
LockOut int `json:"lock_out"`
Expiration int `json:"expiration"` // seconds, -1 no expiration
}
func NewDefaultNKodePolicy() NKodePolicy {
return NKodePolicy{
MinNkodeLen: 4,
MaxNkodeLen: 4,
DistinctSets: 0,
DistinctAttributes: 4,
LockOut: 5,
Expiration: -1,
}
}
func (p *NKodePolicy) ValidLength(nkodeLen int) error {
if nkodeLen < p.MinNkodeLen || nkodeLen > p.MaxNkodeLen {
return config.ErrInvalidNKodeLength
}
// TODO: validate Max > Min
// Validate lockout
// Add Lockout To User
return nil
}

142
entities/user.go Normal file
View File

@@ -0,0 +1,142 @@
package entities
import (
"git.infra.nkode.tech/dkelly/nkode-core/config"
"git.infra.nkode.tech/dkelly/nkode-core/security"
"github.com/google/uuid"
"log"
)
type User struct {
Id UserId
CustomerId CustomerId
Email UserEmail
EncipheredPasscode EncipheredNKode
Kp KeypadDimension
CipherKeys UserCipherKeys
Interface UserInterface
Renew bool
RefreshToken string
}
func (u *User) DecipherMask(setVals []uint64, passcodeLen int) ([]uint64, error) {
return u.CipherKeys.DecipherMask(u.EncipheredPasscode.Mask, setVals, passcodeLen)
}
func (u *User) RenewKeys(setXor []uint64, attrXor []uint64) error {
u.Renew = true
var err error
u.CipherKeys.SetKey, err = security.XorLists(setXor[:u.Kp.AttrsPerKey], u.CipherKeys.SetKey)
if err != nil {
return err
}
u.CipherKeys.AlphaKey, err = security.XorLists(attrXor[:u.Kp.TotalAttrs()], u.CipherKeys.AlphaKey)
return err
}
func (u *User) RefreshPasscode(passcodeAttrIdx []int, customerAttributes CustomerAttributes) error {
setVals, err := customerAttributes.SetValsForKp(u.Kp)
if err != nil {
return err
}
newKeys, err := NewUserCipherKeys(&u.Kp, setVals, u.CipherKeys.MaxNKodeLen)
if err != nil {
return err
}
encipheredPasscode, err := newKeys.EncipherNKode(passcodeAttrIdx, customerAttributes)
if err != nil {
return err
}
u.CipherKeys = *newKeys
u.EncipheredPasscode = *encipheredPasscode
u.Renew = false
return nil
}
func (u *User) GetLoginInterface() ([]int, error) {
return u.Interface.IdxInterface, nil
}
func ValidKeyEntry(user User, customer Customer, selectedKeys []int) ([]int, error) {
if validKeys := user.Kp.ValidKeySelections(selectedKeys); !validKeys {
return nil, config.ErrKeyIndexOutOfRange
}
passcodeLen := len(selectedKeys)
if err := customer.NKodePolicy.ValidLength(passcodeLen); err != nil {
return nil, err
}
setVals, err := customer.Attributes.SetValsForKp(user.Kp)
if err != nil {
log.Printf("fatal error in validate key entry;invalid user keypad dimensions for user %s with error %v", user.Email, err)
return nil, config.ErrInternalValidKeyEntry
}
passcodeSetVals, err := user.DecipherMask(setVals, passcodeLen)
if err != nil {
log.Printf("fatal error in validate key entry;something when wrong deciphering mask;user email %s; error %v", user.Email, err)
return nil, config.ErrInternalValidKeyEntry
}
presumedAttrIdxVals := make([]int, passcodeLen)
for idx := range presumedAttrIdxVals {
keyNumb := selectedKeys[idx]
setIdx, err := customer.Attributes.IndexOfSet(passcodeSetVals[idx])
if err != nil {
log.Printf("fatal error in validate key entry;something when wrong getting the IndexOfSet;user email %s; error %v", user.Email, err)
return nil, config.ErrInternalValidKeyEntry
}
selectedAttrIdx, err := user.Interface.GetAttrIdxByKeyNumbSetIdx(setIdx, keyNumb)
if err != nil {
log.Printf("fatal error in validate key entry;something when wrong getting the GetAttrIdxByKeyNumbSetIdx;user email %s; error %v", user.Email, err)
return nil, config.ErrInternalValidKeyEntry
}
presumedAttrIdxVals[idx] = selectedAttrIdx
}
err = customer.IsValidNKode(user.Kp, presumedAttrIdxVals)
if err != nil {
return nil, err
}
attrVals, err := customer.Attributes.AttrValsForKp(user.Kp)
if err != nil {
return nil, err
}
err = user.CipherKeys.ValidPassword(user.EncipheredPasscode.Code, presumedAttrIdxVals, attrVals)
if err != nil {
return nil, err
}
return presumedAttrIdxVals, nil
}
func NewUser(customer Customer, userEmail string, passcodeIdx []int, ui UserInterface, kp KeypadDimension) (*User, error) {
_, err := ParseEmail(userEmail)
if err != nil {
return nil, err
}
setVals, err := customer.Attributes.SetValsForKp(kp)
if err != nil {
return nil, err
}
newKeys, err := NewUserCipherKeys(&kp, setVals, customer.NKodePolicy.MaxNkodeLen)
if err != nil {
return nil, err
}
encipheredNKode, err := newKeys.EncipherNKode(passcodeIdx, customer.Attributes)
if err != nil {
return nil, err
}
newUser := User{
Id: UserId(uuid.New()),
Email: UserEmail(userEmail),
EncipheredPasscode: *encipheredNKode,
CipherKeys: *newKeys,
Interface: ui,
Kp: kp,
CustomerId: customer.Id,
}
return &newUser, nil
}

View File

@@ -0,0 +1,193 @@
package entities
import (
"crypto/sha256"
"errors"
"git.infra.nkode.tech/dkelly/nkode-core/config"
"git.infra.nkode.tech/dkelly/nkode-core/security"
"golang.org/x/crypto/bcrypt"
)
type UserCipherKeys struct {
AlphaKey []uint64
SetKey []uint64
PassKey []uint64
MaskKey []uint64
Salt []byte
MaxNKodeLen int
Kp *KeypadDimension
}
func NewUserCipherKeys(kp *KeypadDimension, setVals []uint64, maxNKodeLen int) (*UserCipherKeys, error) {
err := kp.IsValidKeypadDimension()
if err != nil {
return nil, err
}
setKey, err := security.GenerateRandomNonRepeatingUint64(kp.AttrsPerKey)
if err != nil {
return nil, err
}
setKey, err = security.XorLists(setKey, setVals)
if err != nil {
return nil, err
}
alphaKey, _ := security.GenerateRandomNonRepeatingUint64(kp.TotalAttrs())
passKey, _ := security.GenerateRandomNonRepeatingUint64(maxNKodeLen)
maskKey, _ := security.GenerateRandomNonRepeatingUint64(maxNKodeLen)
salt, _ := security.RandomBytes(10)
userCipherKeys := UserCipherKeys{
AlphaKey: alphaKey,
PassKey: passKey,
MaskKey: maskKey,
SetKey: setKey,
Salt: salt,
MaxNKodeLen: maxNKodeLen,
Kp: kp,
}
return &userCipherKeys, nil
}
func (u *UserCipherKeys) PadUserMask(userMask []uint64, setVals []uint64) ([]uint64, error) {
if len(userMask) > u.MaxNKodeLen {
return nil, config.ErrUserMaskTooLong
}
paddedUserMask := make([]uint64, len(userMask))
copy(paddedUserMask, userMask)
for i := len(userMask); i < u.MaxNKodeLen; i++ {
paddedUserMask = append(paddedUserMask, setVals[i%len(setVals)])
}
return paddedUserMask, nil
}
func (u *UserCipherKeys) ValidPassword(hashedPassword string, passcodeAttrIdx []int, attrVals []uint64) error {
hashBytes := []byte(hashedPassword)
passcodeCipher := u.encipherCode(passcodeAttrIdx, attrVals)
passwordDigest := u.saltAndDigest(passcodeCipher)
err := bcrypt.CompareHashAndPassword(hashBytes, passwordDigest)
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return config.ErrInvalidNKode
}
return err
}
return nil
}
func (u *UserCipherKeys) EncipherSaltHashCode(passcodeAttrIdx []int, attrVals []uint64) (string, error) {
passcodeCipher := u.encipherCode(passcodeAttrIdx, attrVals)
passcodeDigest := u.saltAndDigest(passcodeCipher)
passcodeBytes, err := u.hashPasscode(passcodeDigest)
if err != nil {
return "", err
}
return string(passcodeBytes), nil
}
func (u *UserCipherKeys) encipherCode(passcodeAttrIdx []int, attrVals []uint64) []uint64 {
passcodeLen := len(passcodeAttrIdx)
passcodeCipher := make([]uint64, u.MaxNKodeLen)
for idx := 0; idx < passcodeLen; idx++ {
attrIdx := passcodeAttrIdx[idx]
alpha := u.AlphaKey[attrIdx]
attrVal := attrVals[attrIdx]
pass := u.PassKey[idx]
passcodeCipher[idx] = alpha ^ pass ^ attrVal
}
return passcodeCipher
}
func (u *UserCipherKeys) saltAndDigest(passcode []uint64) []byte {
passcodeBytes := security.Uint64ArrToByteArr(passcode)
passcodeBytes = append(passcodeBytes, u.Salt...)
h := sha256.New()
h.Write(passcodeBytes)
return h.Sum(nil)
}
func (u *UserCipherKeys) hashPasscode(passcodeDigest []byte) ([]byte, error) {
hashedPassword, err := bcrypt.GenerateFromPassword(passcodeDigest, bcrypt.DefaultCost)
if err != nil {
return nil, err
}
return hashedPassword, nil
}
func (u *UserCipherKeys) EncipherMask(passcodeSet []uint64, customerAttrs CustomerAttributes, userKp KeypadDimension) (string, error) {
setVals, err := customerAttrs.SetValsForKp(userKp)
if err != nil {
return "", err
}
paddedPasscodeSets, err := u.PadUserMask(passcodeSet, setVals)
if err != nil {
return "", err
}
cipheredMask := make([]uint64, len(paddedPasscodeSets))
for idx := range paddedPasscodeSets {
setIdx, err := customerAttrs.IndexOfSet(paddedPasscodeSets[idx])
if err != nil {
return "", err
}
setKeyVal := u.SetKey[setIdx]
maskKeyVal := u.MaskKey[idx]
setVal := paddedPasscodeSets[idx]
cipheredMask[idx] = setKeyVal ^ maskKeyVal ^ setVal
}
mask := security.EncodeBase64Str(cipheredMask)
return mask, nil
}
func (u *UserCipherKeys) DecipherMask(mask string, setVals []uint64, passcodeLen int) ([]uint64, error) {
decodedMask, err := security.DecodeBase64Str(mask)
if err != nil {
return nil, err
}
decipheredMask, err := security.XorLists(decodedMask, u.MaskKey)
if err != nil {
return nil, err
}
setKeyRandComponent, err := security.XorLists(setVals, u.SetKey)
if err != nil {
return nil, err
}
passcodeSet := make([]uint64, passcodeLen)
for idx, setCipher := range decipheredMask[:passcodeLen] {
setIdx, err := security.IndexOf(setKeyRandComponent, setCipher)
if err != nil {
return nil, err
}
passcodeSet[idx] = setVals[setIdx]
}
return passcodeSet, nil
}
func (u *UserCipherKeys) EncipherNKode(passcodeAttrIdx []int, customerAttrs CustomerAttributes) (*EncipheredNKode, error) {
attrVals, err := customerAttrs.AttrValsForKp(*u.Kp)
code, err := u.EncipherSaltHashCode(passcodeAttrIdx, attrVals)
if err != nil {
return nil, err
}
passcodeSet := make([]uint64, len(passcodeAttrIdx))
for idx := range passcodeSet {
passcodeAttr := attrVals[passcodeAttrIdx[idx]]
passcodeSet[idx], err = customerAttrs.GetAttrSetVal(passcodeAttr, *u.Kp)
if err != nil {
return nil, err
}
}
mask, err := u.EncipherMask(passcodeSet, customerAttrs, *u.Kp)
if err != nil {
return nil, err
}
encipheredCode := EncipheredNKode{
Code: code,
Mask: mask,
}
return &encipheredCode, nil
}

185
entities/user_interface.go Normal file
View File

@@ -0,0 +1,185 @@
package entities
import (
"git.infra.nkode.tech/dkelly/nkode-core/config"
"git.infra.nkode.tech/dkelly/nkode-core/security"
"github.com/DonovanKelly/sugar-n-spice/set"
"log"
)
type UserInterface struct {
IdxInterface IdxInterface
SvgId SvgIdInterface
Kp *KeypadDimension
}
func NewUserInterface(kp *KeypadDimension, svgId SvgIdInterface) (*UserInterface, error) {
idxInterface := security.IdentityArray(kp.TotalAttrs())
userInterface := UserInterface{
IdxInterface: idxInterface,
SvgId: svgId,
Kp: kp,
}
if err := userInterface.RandomShuffle(); err != nil {
return nil, err
}
return &userInterface, nil
}
func (u *UserInterface) RandomShuffle() error {
err := u.shuffleKeys()
keypadView, err := u.InterfaceMatrix()
if err != nil {
return err
}
setView, err := security.MatrixTranspose(keypadView)
if err != nil {
return err
}
for idx, set := range setView {
err := security.FisherYatesShuffle(&set)
if err != nil {
return nil
}
setView[idx] = set
}
keypadView, err = security.MatrixTranspose(setView)
if err != nil {
return err
}
u.IdxInterface = security.MatrixToList(keypadView)
return nil
}
func (u *UserInterface) InterfaceMatrix() ([][]int, error) {
return security.ListToMatrix(u.IdxInterface, u.Kp.AttrsPerKey)
}
func (u *UserInterface) SetViewMatrix() ([][]int, error) {
keypadView, err := u.InterfaceMatrix()
if err != nil {
return nil, err
}
return security.MatrixTranspose(keypadView)
}
func (u *UserInterface) DisperseInterface() error {
if !u.Kp.IsDispersable() {
return config.ErrInterfaceNotDispersible
}
err := u.shuffleKeys()
if err != nil {
return err
}
err = u.randomAttributeRotation()
if err != nil {
return err
}
return nil
}
func (u *UserInterface) shuffleKeys() error {
userInterfaceMatrix, err := security.ListToMatrix(u.IdxInterface, u.Kp.AttrsPerKey)
if err != nil {
return err
}
err = security.FisherYatesShuffle[[]int](&userInterfaceMatrix)
if err != nil {
return err
}
u.IdxInterface = security.MatrixToList(userInterfaceMatrix)
return nil
}
func (u *UserInterface) randomAttributeRotation() error {
userInterface, err := u.InterfaceMatrix()
if err != nil {
return err
}
transposeUserInterface, err := security.MatrixTranspose(userInterface)
attrRotation, err := security.RandomPermutation(len(transposeUserInterface))
if err != nil {
return err
}
for idx, attrSet := range transposeUserInterface {
rotation := attrRotation[idx]
transposeUserInterface[idx] = append(attrSet[rotation:], attrSet[:rotation]...)
}
userInterface, err = security.MatrixTranspose(transposeUserInterface)
if err != nil {
return err
}
u.IdxInterface = security.MatrixToList(userInterface)
return nil
}
func (u *UserInterface) AttributeAdjacencyGraph() (map[int]set.Set[int], error) {
interfaceKeypad, err := u.InterfaceMatrix()
if err != nil {
return nil, err
}
graph := make(map[int]set.Set[int])
for _, key := range interfaceKeypad {
keySet := set.NewSetFromSlice(key)
for _, attr := range key {
attrAdjacency := keySet.Copy()
attrAdjacency.Remove(attr)
graph[attr] = attrAdjacency
}
}
return graph, nil
}
func (u *UserInterface) LoginShuffle() error {
if err := u.shuffleKeys(); err != nil {
return err
}
keypadSet1, err := u.InterfaceMatrix()
if err = u.shuffleKeys(); err != nil {
return err
}
keypadSet2, err := u.InterfaceMatrix()
numbOfSelectedSets := u.Kp.AttrsPerKey / 2
setIdxs, err := security.RandomPermutation(u.Kp.AttrsPerKey)
if err != nil {
return err
}
selectedSets := set.NewSetFromSlice[int](setIdxs[:numbOfSelectedSets])
for keyIdx, key := range keypadSet1 {
for idx := range key {
if selectedSets.Contains(idx) {
keypadSet1[keyIdx][idx] = keypadSet2[keyIdx][idx]
}
}
}
u.IdxInterface = security.MatrixToList(keypadSet1)
return nil
}
func (u *UserInterface) GetAttrIdxByKeyNumbSetIdx(setIdx int, keyNumb int) (int, error) {
if keyNumb < 0 || u.Kp.NumbOfKeys <= keyNumb {
log.Printf("keyNumb %d is out of range 0-%d", keyNumb, u.Kp.NumbOfKeys)
return -1, config.ErrKeyIndexOutOfRange
}
if setIdx < 0 || u.Kp.AttrsPerKey <= setIdx {
log.Printf("setIdx %d is out of range 0-%d", setIdx, u.Kp.AttrsPerKey)
return -1, config.ErrAttributeIndexOutOfRange
}
keypadView, err := u.InterfaceMatrix()
if err != nil {
return -1, err
}
return keypadView[keyNumb][setIdx], nil
}

View File

@@ -0,0 +1,201 @@
package entities
import (
"git.infra.nkode.tech/dkelly/nkode-core/config"
"git.infra.nkode.tech/dkelly/nkode-core/security"
"github.com/DonovanKelly/sugar-n-spice/all"
"github.com/DonovanKelly/sugar-n-spice/set"
"github.com/google/uuid"
"log"
"sort"
)
type UserSignSession struct {
Id SessionId
CustomerId CustomerId
LoginUserInterface UserInterface
Kp KeypadDimension
SetIdxInterface IdxInterface
ConfirmIdxInterface IdxInterface
SetKeySelection KeySelection
UserEmail UserEmail
Reset bool
Expire int
Colors []RGBColor
}
func NewSignupResetSession(userEmail UserEmail, kp KeypadDimension, customerId CustomerId, svgInterface SvgIdInterface, reset bool) (*UserSignSession, error) {
loginInterface, err := NewUserInterface(&kp, svgInterface)
if err != nil {
return nil, err
}
signupInterface, colors, err := signupInterface(*loginInterface, kp)
if err != nil {
return nil, err
}
session := UserSignSession{
Id: SessionId(uuid.New()),
CustomerId: customerId,
LoginUserInterface: *loginInterface,
SetIdxInterface: signupInterface.IdxInterface,
ConfirmIdxInterface: nil,
SetKeySelection: nil,
UserEmail: userEmail,
Kp: kp,
Reset: reset,
Colors: colors,
}
return &session, nil
}
func (s *UserSignSession) DeducePasscode(confirmKeyEntry KeySelection) ([]int, error) {
validEntry := all.All[int](confirmKeyEntry, func(i int) bool {
return 0 <= i && i < s.Kp.NumbOfKeys
})
if !validEntry {
log.Printf("Invalid Key entry. One or more key index: %#v, not in range 0-%d", confirmKeyEntry, s.Kp.NumbOfKeys)
return nil, config.ErrKeyIndexOutOfRange
}
if s.SetIdxInterface == nil {
log.Print("signup session set interface is nil")
return nil, config.ErrIncompleteUserSignupSession
}
if s.ConfirmIdxInterface == nil {
log.Print("signup session confirm interface is nil")
return nil, config.ErrIncompleteUserSignupSession
}
if s.SetKeySelection == nil {
log.Print("signup session set key entry is nil")
return nil, config.ErrIncompleteUserSignupSession
}
if s.UserEmail == "" {
log.Print("signup session username is nil")
return nil, config.ErrIncompleteUserSignupSession
}
if len(confirmKeyEntry) != len(s.SetKeySelection) {
log.Printf("confirm and set key entry length mismatch %d != %d", len(confirmKeyEntry), len(s.SetKeySelection))
return nil, config.ErrSetConfirmSignupMismatch
}
passcodeLen := len(confirmKeyEntry)
setKeyVals, err := s.getSelectedKeyVals(s.SetKeySelection, s.SetIdxInterface)
if err != nil {
return nil, err
}
confirmKeyVals, err := s.getSelectedKeyVals(confirmKeyEntry, s.ConfirmIdxInterface)
passcode := make([]int, passcodeLen)
for idx := 0; idx < passcodeLen; idx++ {
setKey := set.NewSetFromSlice[int](setKeyVals[idx])
confirmKey := set.NewSetFromSlice[int](confirmKeyVals[idx])
intersection := setKey.Intersect(confirmKey)
if intersection.Size() < 1 {
log.Printf("set and confirm do not intersect at index %d", idx)
return nil, config.ErrSetConfirmSignupMismatch
}
if intersection.Size() > 1 {
log.Printf("set and confirm intersect at more than one point at index %d", idx)
return nil, config.ErrSetConfirmSignupMismatch
}
intersectionSlice := intersection.ToSlice()
passcode[idx] = intersectionSlice[0]
}
return passcode, nil
}
func (s *UserSignSession) SetUserNKode(keySelection KeySelection) (IdxInterface, error) {
validKeySelection := all.All[int](keySelection, func(i int) bool {
return 0 <= i && i < s.Kp.NumbOfKeys
})
if !validKeySelection {
log.Printf("one or key selection is out of range 0-%d", s.Kp.NumbOfKeys-1)
return nil, config.ErrKeyIndexOutOfRange
}
s.SetKeySelection = keySelection
setKp := s.SignupKeypad()
setInterface := UserInterface{IdxInterface: s.SetIdxInterface, Kp: &setKp}
err := setInterface.DisperseInterface()
if err != nil {
return nil, err
}
s.ConfirmIdxInterface = setInterface.IdxInterface
return s.ConfirmIdxInterface, nil
}
func (s *UserSignSession) getSelectedKeyVals(keySelections KeySelection, userInterface []int) ([][]int, error) {
signupKp := s.SignupKeypad()
keypadInterface, err := security.ListToMatrix(userInterface, signupKp.AttrsPerKey)
if err != nil {
return nil, err
}
keyVals := make([][]int, len(keySelections))
for idx, keyIdx := range keySelections {
keyVals[idx] = keypadInterface[keyIdx]
}
return keyVals, nil
}
func signupInterface(baseUserInterface UserInterface, kp KeypadDimension) (*UserInterface, []RGBColor, error) {
// This method randomly drops sets from the base user interface so it is a square and dispersable matrix
if kp.IsDispersable() {
return nil, nil, config.ErrKeypadIsNotDispersible
}
err := baseUserInterface.RandomShuffle()
if err != nil {
return nil, nil, err
}
// attributes are arranged by key interfaceMatrix
interfaceMatrix, err := baseUserInterface.InterfaceMatrix()
if err != nil {
return nil, nil, err
}
// attributes are arranged by set
attrSetView, err := security.MatrixTranspose(interfaceMatrix)
if err != nil {
return nil, nil, err
}
setIdxs := security.IdentityArray(kp.AttrsPerKey)
if err := security.FisherYatesShuffle[int](&setIdxs); err != nil {
return nil, nil, err
}
setIdxs = setIdxs[:kp.NumbOfKeys]
sort.Ints(setIdxs)
selectedSets := make([][]int, kp.NumbOfKeys)
selectedColors := make([]RGBColor, kp.NumbOfKeys)
for idx, setIdx := range setIdxs {
selectedSets[idx] = attrSetView[setIdx]
selectedColors[idx] = SetColors[setIdx]
}
// convert set view back into key view
selectedSets, err = security.MatrixTranspose(selectedSets)
if err != nil {
return nil, nil, err
}
signupUserInterface := UserInterface{
IdxInterface: security.MatrixToList(selectedSets),
Kp: &KeypadDimension{
AttrsPerKey: kp.NumbOfKeys,
NumbOfKeys: kp.NumbOfKeys,
},
}
return &signupUserInterface, selectedColors, nil
}
func (s *UserSignSession) SignupKeypad() KeypadDimension {
return KeypadDimension{
AttrsPerKey: s.Kp.NumbOfKeys,
NumbOfKeys: s.Kp.NumbOfKeys,
}
}

133
entities/user_test.go Normal file
View File

@@ -0,0 +1,133 @@
package entities
import (
"github.com/DonovanKelly/sugar-n-spice/all"
"github.com/stretchr/testify/assert"
"testing"
)
func TestUserCipherKeys_EncipherSaltHashCode(t *testing.T) {
kp := KeypadDimension{AttrsPerKey: 10, NumbOfKeys: 8}
maxNKodeLen := 10
customerAttrs, err := NewCustomerAttributes()
assert.NoError(t, err)
setVals, err := customerAttrs.SetValsForKp(kp)
assert.NoError(t, err)
attrVals, err := customerAttrs.AttrValsForKp(kp)
assert.NoError(t, err)
newUser, err := NewUserCipherKeys(&kp, setVals, maxNKodeLen)
assert.NoError(t, err)
passcodeIdx := []int{0, 1, 2, 3}
encipher0, err := newUser.EncipherSaltHashCode(passcodeIdx, attrVals)
assert.NoError(t, err)
err = newUser.ValidPassword(encipher0, passcodeIdx, attrVals)
assert.NoError(t, err)
passcodeIdxInvalid := []int{1, 0, 3, 2}
assert.NoError(t, err)
err = newUser.ValidPassword(encipher0, passcodeIdxInvalid, attrVals)
assert.Error(t, err)
}
func TestUserCipherKeys_EncipherDecipherMask(t *testing.T) {
kp := KeypadDimension{AttrsPerKey: 10, NumbOfKeys: 8}
maxNKodeLen := 10
customerAttrs, err := NewCustomerAttributes()
assert.NoError(t, err)
setVals, err := customerAttrs.SetValsForKp(kp)
assert.NoError(t, err)
attrVals, err := customerAttrs.AttrValsForKp(kp)
assert.NoError(t, err)
newUser, err := NewUserCipherKeys(&kp, setVals, maxNKodeLen)
assert.NoError(t, err)
passcodeIdx := []int{0, 1, 2, 3}
originalSetVals := make([]uint64, len(passcodeIdx))
for idx, val := range passcodeIdx {
attr := attrVals[val]
originalSetVals[idx], err = customerAttrs.GetAttrSetVal(attr, kp)
assert.NoError(t, err)
}
encipheredCode, err := newUser.EncipherNKode(passcodeIdx, *customerAttrs)
assert.NoError(t, err)
passcodeSetVals, err := newUser.DecipherMask(encipheredCode.Mask, setVals, len(passcodeIdx))
assert.NoError(t, err)
for idx, setVal := range passcodeSetVals {
assert.Equal(t, setVal, originalSetVals[idx])
}
}
func TestUserInterface_RandomShuffle(t *testing.T) {
kp := KeypadDimension{
AttrsPerKey: 10,
NumbOfKeys: 8,
}
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
assert.NoError(t, err)
userInterfaceCopy := make([]int, len(userInterface.IdxInterface))
copy(userInterfaceCopy, userInterface.IdxInterface)
err = userInterface.RandomShuffle()
assert.NoError(t, err)
assert.Equal(t, len(userInterface.IdxInterface), len(userInterfaceCopy))
equalCount := 0
for idx, val := range userInterface.IdxInterface {
if val == userInterfaceCopy[idx] {
equalCount++
}
}
assert.NotEqual(t, equalCount, len(userInterface.IdxInterface))
}
func TestUserInterface_DisperseInterface(t *testing.T) {
for idx := 0; idx < 10000; idx++ {
kp := KeypadDimension{AttrsPerKey: 7, NumbOfKeys: 10}
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
assert.NoError(t, err)
preDispersion, err := userInterface.AttributeAdjacencyGraph()
assert.NoError(t, err)
err = userInterface.DisperseInterface()
assert.NoError(t, err)
postDispersion, err := userInterface.AttributeAdjacencyGraph()
assert.Equal(t, len(postDispersion), len(preDispersion))
for attr, adjAttrs := range preDispersion {
postAdjAttrs := postDispersion[attr]
assert.Equal(t, adjAttrs.Size(), postAdjAttrs.Size())
assert.True(t, adjAttrs.IsDisjoint(postAdjAttrs))
}
}
}
func TestUserInterface_PartialInterfaceShuffle(t *testing.T) {
kp := KeypadDimension{AttrsPerKey: 7, NumbOfKeys: 10}
mockSvgInterface := make(SvgIdInterface, kp.TotalAttrs())
userInterface, err := NewUserInterface(&kp, mockSvgInterface)
assert.NoError(t, err)
preShuffle := userInterface.IdxInterface
err = userInterface.LoginShuffle()
assert.NoError(t, err)
postShuffle := userInterface.IdxInterface
shuffleCompare := make([]bool, len(postShuffle))
for idx, val := range preShuffle {
shuffleCompare[idx] = val == postShuffle[idx]
}
allTrue := all.All[bool](shuffleCompare, func(n bool) bool {
return n == true
})
assert.False(t, allTrue)
allFalse := all.All[bool](shuffleCompare, func(n bool) bool {
return n == false
})
assert.False(t, allFalse)
}

34
go.mod Normal file
View File

@@ -0,0 +1,34 @@
module git.infra.nkode.tech/dkelly/nkode-core
go 1.23.0
require (
github.com/DonovanKelly/sugar-n-spice v1.0.1
github.com/aws/aws-sdk-go-v2 v1.33.0
github.com/aws/aws-sdk-go-v2/config v1.29.1
github.com/aws/aws-sdk-go-v2/service/ses v1.29.6
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/mattn/go-sqlite3 v1.14.24
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.32.0
)
require (
github.com/aws/aws-sdk-go-v2/credentials v1.17.54 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

58
go.sum Normal file
View File

@@ -0,0 +1,58 @@
github.com/DonovanKelly/sugar-n-spice v1.0.1 h1:VsybiCHSziAqyPtbYF6GtkiJYYECWMHKN+EyEa6UVpA=
github.com/DonovanKelly/sugar-n-spice v1.0.1/go.mod h1:/HQWoablLFCwsa4gwfzVBu80cI5A3dyO1uCiB11sup0=
github.com/aws/aws-sdk-go-v2 v1.33.0 h1:Evgm4DI9imD81V0WwD+TN4DCwjUMdc94TrduMLbgZJs=
github.com/aws/aws-sdk-go-v2 v1.33.0/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
github.com/aws/aws-sdk-go-v2/config v1.29.1 h1:JZhGawAyZ/EuJeBtbQYnaoftczcb2drR2Iq36Wgz4sQ=
github.com/aws/aws-sdk-go-v2/config v1.29.1/go.mod h1:7bR2YD5euaxBhzt2y/oDkt3uNRb6tjFp98GlTFueRwk=
github.com/aws/aws-sdk-go-v2/credentials v1.17.54 h1:4UmqeOqJPvdvASZWrKlhzpRahAulBfyTJQUaYy4+hEI=
github.com/aws/aws-sdk-go-v2/credentials v1.17.54/go.mod h1:RTdfo0P0hbbTxIhmQrOsC/PquBZGabEPnCaxxKRPSnI=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24 h1:5grmdTdMsovn9kPZPI23Hhvp0ZyNm5cRO+IZFIYiAfw=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.24/go.mod h1:zqi7TVKTswH3Ozq28PkmBmgzG1tona7mo9G2IJg4Cis=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28 h1:igORFSiH3bfq4lxKFkTSYDhJEUCYo6C8VKiWJjYwQuQ=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.28/go.mod h1:3So8EA/aAYm36L7XIvCVwLa0s5N0P7o2b1oqnx/2R4g=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28 h1:1mOW9zAUMhTSrMDssEHS/ajx8JcAj/IcftzcmNlmVLI=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.28/go.mod h1:kGlXVIWDfvt2Ox5zEaNglmq0hXPHgQFNMix33Tw22jA=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9 h1:TQmKDyETFGiXVhZfQ/I0cCFziqqX58pi4tKJGYGFSz0=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.9/go.mod h1:HVLPK2iHQBUx7HfZeOQSEu3v2ubZaAY2YPbAm5/WUyY=
github.com/aws/aws-sdk-go-v2/service/ses v1.29.6 h1:uc9MwzkhjIjV5abWaG6Ird83IcSrNVt62BSXG7WRwAw=
github.com/aws/aws-sdk-go-v2/service/ses v1.29.6/go.mod h1:t1rqt5llPOnzPnfHpciQZ3dZgyCsgfR7RHZ2ZFfZEWs=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11 h1:kuIyu4fTT38Kj7YCC7ouNbVZSSpqkZ+LzIfhCr6Dg+I=
github.com/aws/aws-sdk-go-v2/service/sso v1.24.11/go.mod h1:Ro744S4fKiCCuZECXgOi760TiYylUM8ZBf6OGiZzJtY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10 h1:l+dgv/64iVlQ3WsBbnn+JSbkj01jIi+SM0wYsj3y/hY=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.10/go.mod h1:Fzsj6lZEb8AkTE5S68OhcbBqeWPsR8RnGuKPr8Todl8=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9 h1:BRVDbewN6VZcwr+FBOszDKvYeXY1kJ+GGMCcpghlw0U=
github.com/aws/aws-sdk-go-v2/service/sts v1.33.9/go.mod h1:f6vjfZER1M17Fokn0IzssOTMT2N8ZSq+7jnNF0tArvw=
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,20 @@
package repository
import (
"git.infra.nkode.tech/dkelly/nkode-core/entities"
)
type CustomerUserRepository interface {
GetCustomer(entities.CustomerId) (*entities.Customer, error)
GetUser(entities.UserEmail, entities.CustomerId) (*entities.User, error)
CreateCustomer(entities.Customer) error
WriteNewUser(entities.User) error
UpdateUserNKode(entities.User) error
UpdateUserInterface(entities.UserId, entities.UserInterface) error
UpdateUserRefreshToken(entities.UserId, string) error
Renew(entities.CustomerId) error
RefreshUserPasscode(entities.User, []int, entities.CustomerAttributes) error
RandomSvgInterface(entities.KeypadDimension) ([]string, error)
RandomSvgIdxInterface(entities.KeypadDimension) (entities.SvgIdInterface, error)
GetSvgStringInterface(entities.SvgIdInterface) ([]string, error)
}

View File

@@ -0,0 +1,401 @@
package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"git.infra.nkode.tech/dkelly/nkode-core/config"
"git.infra.nkode.tech/dkelly/nkode-core/entities"
"git.infra.nkode.tech/dkelly/nkode-core/security"
"git.infra.nkode.tech/dkelly/nkode-core/sqlc"
"git.infra.nkode.tech/dkelly/nkode-core/utils"
"github.com/google/uuid"
_ "github.com/mattn/go-sqlite3"
"log"
)
type SqliteRepository struct {
Queue *sqlc.Queue
ctx context.Context
}
func NewSqliteRepository(queue *sqlc.Queue, ctx context.Context) SqliteRepository {
return SqliteRepository{
Queue: queue,
ctx: ctx,
}
}
func (d *SqliteRepository) CreateCustomer(c entities.Customer) error {
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.CreateCustomerParams)
if !ok {
return fmt.Errorf("invalid argument type: expected CreateCustomerParams")
}
return q.CreateCustomer(ctx, params)
}
return d.Queue.EnqueueWriteTx(queryFunc, c.ToSqlcCreateCustomerParams())
}
func (d *SqliteRepository) WriteNewUser(u entities.User) error {
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.CreateUserParams)
if !ok {
return fmt.Errorf("invalid argument type: expected CreateUserParams")
}
return q.CreateUser(ctx, params)
}
// Use the wrapped function in EnqueueWriteTx
renew := 0
if u.Renew {
renew = 1
}
// Map entities.User to CreateUserParams
params := sqlc.CreateUserParams{
ID: uuid.UUID(u.Id).String(),
Email: string(u.Email),
Renew: int64(renew),
RefreshToken: sql.NullString{String: u.RefreshToken, Valid: u.RefreshToken != ""},
CustomerID: uuid.UUID(u.CustomerId).String(),
Code: u.EncipheredPasscode.Code,
Mask: u.EncipheredPasscode.Mask,
AttributesPerKey: int64(u.Kp.AttrsPerKey),
NumberOfKeys: int64(u.Kp.NumbOfKeys),
AlphaKey: security.Uint64ArrToByteArr(u.CipherKeys.AlphaKey),
SetKey: security.Uint64ArrToByteArr(u.CipherKeys.SetKey),
PassKey: security.Uint64ArrToByteArr(u.CipherKeys.PassKey),
MaskKey: security.Uint64ArrToByteArr(u.CipherKeys.MaskKey),
Salt: u.CipherKeys.Salt,
MaxNkodeLen: int64(u.CipherKeys.MaxNKodeLen),
IdxInterface: security.IntArrToByteArr(u.Interface.IdxInterface),
SvgIDInterface: security.IntArrToByteArr(u.Interface.SvgId),
CreatedAt: sql.NullString{String: utils.TimeStamp(), Valid: true},
}
return d.Queue.EnqueueWriteTx(queryFunc, params)
}
func (d *SqliteRepository) UpdateUserNKode(u entities.User) error {
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.UpdateUserParams)
if !ok {
return fmt.Errorf("invalid argument type: expected UpdateUserParams")
}
return q.UpdateUser(ctx, params)
}
// Use the wrapped function in EnqueueWriteTx
renew := 0
if u.Renew {
renew = 1
}
params := sqlc.UpdateUserParams{
Email: string(u.Email),
Renew: int64(renew),
RefreshToken: sql.NullString{String: u.RefreshToken, Valid: u.RefreshToken != ""},
CustomerID: uuid.UUID(u.CustomerId).String(),
Code: u.EncipheredPasscode.Code,
Mask: u.EncipheredPasscode.Mask,
AttributesPerKey: int64(u.Kp.AttrsPerKey),
NumberOfKeys: int64(u.Kp.NumbOfKeys),
AlphaKey: security.Uint64ArrToByteArr(u.CipherKeys.AlphaKey),
SetKey: security.Uint64ArrToByteArr(u.CipherKeys.SetKey),
PassKey: security.Uint64ArrToByteArr(u.CipherKeys.PassKey),
MaskKey: security.Uint64ArrToByteArr(u.CipherKeys.MaskKey),
Salt: u.CipherKeys.Salt,
MaxNkodeLen: int64(u.CipherKeys.MaxNKodeLen),
IdxInterface: security.IntArrToByteArr(u.Interface.IdxInterface),
SvgIDInterface: security.IntArrToByteArr(u.Interface.SvgId),
}
return d.Queue.EnqueueWriteTx(queryFunc, params)
}
func (d *SqliteRepository) UpdateUserInterface(id entities.UserId, ui entities.UserInterface) error {
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.UpdateUserInterfaceParams)
if !ok {
return fmt.Errorf("invalid argument type: expected UpdateUserInterfaceParams")
}
return q.UpdateUserInterface(ctx, params)
}
params := sqlc.UpdateUserInterfaceParams{
IdxInterface: security.IntArrToByteArr(ui.IdxInterface),
LastLogin: utils.TimeStamp(),
ID: uuid.UUID(id).String(),
}
return d.Queue.EnqueueWriteTx(queryFunc, params)
}
func (d *SqliteRepository) UpdateUserRefreshToken(id entities.UserId, refreshToken string) error {
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.UpdateUserRefreshTokenParams)
if !ok {
return fmt.Errorf("invalid argument type: expected UpdateUserRefreshToken")
}
return q.UpdateUserRefreshToken(ctx, params)
}
params := sqlc.UpdateUserRefreshTokenParams{
RefreshToken: sql.NullString{
String: refreshToken,
Valid: true,
},
ID: uuid.UUID(id).String(),
}
return d.Queue.EnqueueWriteTx(queryFunc, params)
}
func (d *SqliteRepository) RenewCustomer(renewParams sqlc.RenewCustomerParams) error {
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.RenewCustomerParams)
if !ok {
}
return q.RenewCustomer(ctx, params)
}
return d.Queue.EnqueueWriteTx(queryFunc, renewParams)
}
func (d *SqliteRepository) Renew(id entities.CustomerId) error {
setXor, attrXor, err := d.renewCustomer(id)
if err != nil {
return err
}
customerId := entities.CustomerIdToString(id)
userRenewRows, err := d.Queue.Queries.GetUserRenew(d.ctx, customerId)
if err != nil {
return err
}
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.RenewUserParams)
if !ok {
return fmt.Errorf("invalid argument type: expected RenewUserParams")
}
return q.RenewUser(ctx, params)
}
for _, row := range userRenewRows {
user := entities.User{
Id: entities.UserIdFromString(row.ID),
CustomerId: entities.CustomerId{},
Email: "",
EncipheredPasscode: entities.EncipheredNKode{},
Kp: entities.KeypadDimension{
AttrsPerKey: int(row.AttributesPerKey),
NumbOfKeys: int(row.NumberOfKeys),
},
CipherKeys: entities.UserCipherKeys{
AlphaKey: security.ByteArrToUint64Arr(row.AlphaKey),
SetKey: security.ByteArrToUint64Arr(row.SetKey),
},
Interface: entities.UserInterface{},
Renew: false,
}
if err = user.RenewKeys(setXor, attrXor); err != nil {
return err
}
params := sqlc.RenewUserParams{
AlphaKey: security.Uint64ArrToByteArr(user.CipherKeys.AlphaKey),
SetKey: security.Uint64ArrToByteArr(user.CipherKeys.SetKey),
Renew: 1,
ID: uuid.UUID(user.Id).String(),
}
if err = d.Queue.EnqueueWriteTx(queryFunc, params); err != nil {
return err
}
}
return nil
}
func (d *SqliteRepository) renewCustomer(id entities.CustomerId) ([]uint64, []uint64, error) {
customer, err := d.GetCustomer(id)
if err != nil {
return nil, nil, err
}
setXor, attrXor, err := customer.RenewKeys()
if err != nil {
return nil, nil, err
}
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.RenewCustomerParams)
if !ok {
return fmt.Errorf("invalid argument type: expected RenewCustomerParams")
}
return q.RenewCustomer(ctx, params)
}
params := sqlc.RenewCustomerParams{
AttributeValues: security.Uint64ArrToByteArr(customer.Attributes.AttrVals),
SetValues: security.Uint64ArrToByteArr(customer.Attributes.SetVals),
ID: uuid.UUID(customer.Id).String(),
}
if err = d.Queue.EnqueueWriteTx(queryFunc, params); err != nil {
return nil, nil, err
}
return setXor, attrXor, nil
}
func (d *SqliteRepository) RefreshUserPasscode(user entities.User, passcodeIdx []int, customerAttr entities.CustomerAttributes) error {
if err := user.RefreshPasscode(passcodeIdx, customerAttr); err != nil {
return err
}
queryFunc := func(q *sqlc.Queries, ctx context.Context, args any) error {
params, ok := args.(sqlc.RefreshUserPasscodeParams)
if !ok {
return fmt.Errorf("invalid argument type: expected RefreshUserPasscodeParams")
}
return q.RefreshUserPasscode(ctx, params)
}
params := sqlc.RefreshUserPasscodeParams{
Renew: 0,
Code: user.EncipheredPasscode.Code,
Mask: user.EncipheredPasscode.Mask,
AlphaKey: security.Uint64ArrToByteArr(user.CipherKeys.AlphaKey),
SetKey: security.Uint64ArrToByteArr(user.CipherKeys.SetKey),
PassKey: security.Uint64ArrToByteArr(user.CipherKeys.PassKey),
MaskKey: security.Uint64ArrToByteArr(user.CipherKeys.MaskKey),
Salt: user.CipherKeys.Salt,
ID: uuid.UUID(user.Id).String(),
}
return d.Queue.EnqueueWriteTx(queryFunc, params)
}
func (d *SqliteRepository) GetCustomer(id entities.CustomerId) (*entities.Customer, error) {
customer, err := d.Queue.Queries.GetCustomer(d.ctx, uuid.UUID(id).String())
if err != nil {
return nil, err
}
return &entities.Customer{
Id: id,
NKodePolicy: entities.NKodePolicy{
MaxNkodeLen: int(customer.MaxNkodeLen),
MinNkodeLen: int(customer.MinNkodeLen),
DistinctSets: int(customer.DistinctSets),
DistinctAttributes: int(customer.DistinctAttributes),
LockOut: int(customer.LockOut),
Expiration: int(customer.Expiration),
},
Attributes: entities.NewCustomerAttributesFromBytes(customer.AttributeValues, customer.SetValues),
}, nil
}
func (d *SqliteRepository) GetUser(email entities.UserEmail, customerId entities.CustomerId) (*entities.User, error) {
userRow, err := d.Queue.Queries.GetUser(d.ctx, sqlc.GetUserParams{
Email: string(email),
CustomerID: uuid.UUID(customerId).String(),
})
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
kp := entities.KeypadDimension{
AttrsPerKey: int(userRow.AttributesPerKey),
NumbOfKeys: int(userRow.NumberOfKeys),
}
renew := false
if userRow.Renew == 1 {
renew = true
}
user := entities.User{
Id: entities.UserIdFromString(userRow.ID),
CustomerId: customerId,
Email: email,
EncipheredPasscode: entities.EncipheredNKode{
Code: userRow.Code,
Mask: userRow.Mask,
},
Kp: kp,
CipherKeys: entities.UserCipherKeys{
AlphaKey: security.ByteArrToUint64Arr(userRow.AlphaKey),
SetKey: security.ByteArrToUint64Arr(userRow.SetKey),
PassKey: security.ByteArrToUint64Arr(userRow.PassKey),
MaskKey: security.ByteArrToUint64Arr(userRow.MaskKey),
Salt: userRow.Salt,
MaxNKodeLen: int(userRow.MaxNkodeLen),
Kp: &kp,
},
Interface: entities.UserInterface{
IdxInterface: security.ByteArrToIntArr(userRow.IdxInterface),
SvgId: security.ByteArrToIntArr(userRow.SvgIDInterface),
Kp: &kp,
},
Renew: renew,
RefreshToken: userRow.RefreshToken.String,
}
return &user, nil
}
func (d *SqliteRepository) RandomSvgInterface(kp entities.KeypadDimension) ([]string, error) {
ids, err := d.getRandomIds(kp.TotalAttrs())
if err != nil {
return nil, err
}
return d.getSvgsById(ids)
}
func (d *SqliteRepository) RandomSvgIdxInterface(kp entities.KeypadDimension) (entities.SvgIdInterface, error) {
return d.getRandomIds(kp.TotalAttrs())
}
func (d *SqliteRepository) GetSvgStringInterface(idxs entities.SvgIdInterface) ([]string, error) {
return d.getSvgsById(idxs)
}
func (d *SqliteRepository) getSvgsById(ids []int) ([]string, error) {
svgs := make([]string, len(ids))
for idx, id := range ids {
svg, err := d.Queue.Queries.GetSvgId(d.ctx, int64(id))
if err != nil {
return nil, err
}
svgs[idx] = svg
}
return svgs, nil
}
func (d *SqliteRepository) getRandomIds(count int) ([]int, error) {
tx, err := d.Queue.Db.Begin()
if err != nil {
log.Print(err)
return nil, config.ErrSqliteTx
}
rows, err := tx.Query("SELECT COUNT(*) as count FROM svg_icon;")
if err != nil {
log.Print(err)
return nil, config.ErrSqliteTx
}
var tableLen int
if !rows.Next() {
return nil, config.ErrEmptySvgTable
}
if err = rows.Scan(&tableLen); err != nil {
log.Print(err)
return nil, config.ErrSqliteTx
}
perm, err := security.RandomPermutation(tableLen)
if err != nil {
return nil, err
}
for idx := range perm {
perm[idx] += 1
}
if err = tx.Commit(); err != nil {
log.Print(err)
return nil, config.ErrSqliteTx
}
return perm[:count], nil
}

View File

@@ -0,0 +1,63 @@
package repository
import (
"context"
"git.infra.nkode.tech/dkelly/nkode-core/entities"
"git.infra.nkode.tech/dkelly/nkode-core/sqlc"
"github.com/stretchr/testify/assert"
"os"
"testing"
)
func TestNewSqliteDB(t *testing.T) {
dbPath := os.Getenv("TEST_DB")
// sql_driver.MakeTables(dbFile)
ctx := context.Background()
sqliteDb, err := sqlc.OpenSqliteDb(dbPath)
assert.NoError(t, err)
queue, err := sqlc.NewQueue(sqliteDb, ctx)
assert.NoError(t, err)
queue.Start()
defer queue.Stop()
db := NewSqliteRepository(queue, ctx)
assert.NoError(t, err)
testSignupLoginRenew(t, &db)
testSqliteDBRandomSvgInterface(t, &db)
}
func testSignupLoginRenew(t *testing.T, db CustomerUserRepository) {
nkodePolicy := entities.NewDefaultNKodePolicy()
customerOrig, err := entities.NewCustomer(nkodePolicy)
assert.NoError(t, err)
err = db.CreateCustomer(*customerOrig)
assert.NoError(t, err)
customer, err := db.GetCustomer(customerOrig.Id)
assert.NoError(t, err)
assert.Equal(t, customerOrig, customer)
username := "test_user@example.com"
kp := entities.KeypadDefault
passcodeIdx := []int{0, 1, 2, 3}
mockSvgInterface := make(entities.SvgIdInterface, kp.TotalAttrs())
ui, err := entities.NewUserInterface(&kp, mockSvgInterface)
assert.NoError(t, err)
userOrig, err := entities.NewUser(*customer, username, passcodeIdx, *ui, kp)
assert.NoError(t, err)
err = db.WriteNewUser(*userOrig)
assert.NoError(t, err)
user, err := db.GetUser(entities.UserEmail(username), customer.Id)
assert.NoError(t, err)
assert.Equal(t, userOrig, user)
err = db.Renew(customer.Id)
assert.NoError(t, err)
}
func testSqliteDBRandomSvgInterface(t *testing.T, db CustomerUserRepository) {
kp := entities.KeypadMax
svgs, err := db.RandomSvgInterface(kp)
assert.NoError(t, err)
assert.Len(t, svgs, kp.TotalAttrs())
}

123
security/jwt_claims.go Normal file
View File

@@ -0,0 +1,123 @@
package security
import (
"git.infra.nkode.tech/dkelly/nkode-core/config"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"log"
"os"
"time"
)
type AuthenticationTokens struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type ResetNKodeClaims struct {
Reset bool `json:"reset"`
jwt.RegisteredClaims
}
const (
accessTokenExp = 5 * time.Minute
refreshTokenExp = 30 * 24 * time.Hour
resetNKodeTokenExp = 5 * time.Minute
)
var secret = getJwtSecret()
func getJwtSecret() []byte {
jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
log.Fatal("No JWT_SECRET found")
}
jwtBytes, err := ParseHexString(jwtSecret)
if err != nil {
log.Fatalf("error parsing jwt secret %v", err)
}
return jwtBytes
}
func NewAuthenticationTokens(username string, customerId uuid.UUID) (AuthenticationTokens, error) {
accessClaims := NewAccessClaim(username, customerId)
refreshClaims := jwt.RegisteredClaims{
Subject: username,
Issuer: customerId.String(),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(refreshTokenExp)),
}
accessJwt, err := EncodeAndSignClaims(accessClaims)
if err != nil {
return AuthenticationTokens{}, err
}
refreshJwt, err := EncodeAndSignClaims(refreshClaims)
if err != nil {
return AuthenticationTokens{}, err
}
return AuthenticationTokens{
AccessToken: accessJwt,
RefreshToken: refreshJwt,
}, nil
}
func NewAccessClaim(username string, customerId uuid.UUID) jwt.RegisteredClaims {
return jwt.RegisteredClaims{
Subject: username,
Issuer: customerId.String(),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(accessTokenExp)),
}
}
func EncodeAndSignClaims(claims jwt.Claims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(secret)
}
func ParseRegisteredClaimToken(token string) (*jwt.RegisteredClaims, error) {
return parseJwt[*jwt.RegisteredClaims](token, &jwt.RegisteredClaims{})
}
func ParseRestNKodeToken(resetNKodeToken string) (*ResetNKodeClaims, error) {
return parseJwt[*ResetNKodeClaims](resetNKodeToken, &ResetNKodeClaims{})
}
func parseJwt[T *ResetNKodeClaims | *jwt.RegisteredClaims](tokenStr string, claim jwt.Claims) (T, error) {
token, err := jwt.ParseWithClaims(tokenStr, claim, func(token *jwt.Token) (interface{}, error) {
return secret, nil
})
if err != nil {
log.Printf("error parsing refresh token: %v", err)
return nil, config.ErrInvalidJwt
}
claims, ok := token.Claims.(T)
if !ok {
return nil, config.ErrInvalidJwt
}
return claims, nil
}
func ClaimExpired(claims jwt.RegisteredClaims) error {
if claims.ExpiresAt == nil {
return config.ErrClaimExpOrNil
}
if claims.ExpiresAt.Time.After(time.Now()) {
return nil
}
return config.ErrClaimExpOrNil
}
func ResetNKodeToken(userEmail string, customerId uuid.UUID) (string, error) {
resetClaims := ResetNKodeClaims{
true,
jwt.RegisteredClaims{
Subject: userEmail,
Issuer: customerId.String(),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(resetNKodeTokenExp)),
},
}
return EncodeAndSignClaims(resetClaims)
}

View File

@@ -0,0 +1,28 @@
package security
import (
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"testing"
)
func TestJwtClaims(t *testing.T) {
email := "testing@example.com"
customerId := uuid.New()
authTokens, err := NewAuthenticationTokens(email, customerId)
assert.NoError(t, err)
accessToken, err := ParseRegisteredClaimToken(authTokens.AccessToken)
assert.NoError(t, err)
assert.Equal(t, accessToken.Subject, email)
assert.NoError(t, ClaimExpired(*accessToken))
refreshToken, err := ParseRegisteredClaimToken(authTokens.RefreshToken)
assert.NoError(t, err)
assert.Equal(t, refreshToken.Subject, email)
assert.NoError(t, ClaimExpired(*refreshToken))
resetNKode, err := ResetNKodeToken(email, customerId)
assert.NoError(t, err)
resetToken, err := ParseRestNKodeToken(resetNKode)
assert.NoError(t, err)
assert.True(t, resetToken.Reset)
assert.Equal(t, resetToken.Subject, email)
}

289
security/random.go Normal file
View File

@@ -0,0 +1,289 @@
package security
import (
"crypto/rand"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"errors"
"github.com/DonovanKelly/sugar-n-spice/set"
"log"
"math/big"
r "math/rand"
"time"
)
var (
ErrFisherYatesShuffle = errors.New("unable to shuffle array")
ErrRandomBytes = errors.New("random bytes error")
ErrRandNonRepeatingUint64 = errors.New("list length must be less than 2^32")
ErrParseHexString = errors.New("parse hex string error")
ErrMatrixTranspose = errors.New("matrix cannot be nil or empty")
ErrListToMatrixNotDivisible = errors.New("list to matrix not possible")
ErrElementNotInArray = errors.New("element not in array")
ErrDecodeBase64Str = errors.New("decode base64 err")
ErrRandNonRepeatingInt = errors.New("list length must be less than 2^31")
ErrXorLengthMismatch = errors.New("xor length mismatch")
)
func fisherYatesShuffle[T any](b *[]T) error {
for i := len(*b) - 1; i > 0; i-- {
bigJ, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
if err != nil {
log.Print("fisher yates shuffle error: ", err)
return ErrFisherYatesShuffle
}
j := bigJ.Int64()
(*b)[i], (*b)[j] = (*b)[j], (*b)[i]
}
return nil
}
func FisherYatesShuffle[T any](b *[]T) error {
return fisherYatesShuffle(b)
}
func RandomBytes(n int) ([]byte, error) {
b := make([]byte, n)
_, err := rand.Read(b)
if err != nil {
log.Print("error in random bytes: ", err)
return nil, ErrRandomBytes
}
return b, nil
}
func RandomPermutation(n int) ([]int, error) {
perm := IdentityArray(n)
err := fisherYatesShuffle(&perm)
if err != nil {
return nil, err
}
return perm, nil
}
func GenerateRandomUInt64() (uint64, error) {
randBytes, err := RandomBytes(8)
if err != nil {
return 0, err
}
val := binary.LittleEndian.Uint64(randBytes)
return val, nil
}
func GenerateRandomInt() (int, error) {
randBytes, err := RandomBytes(8)
if err != nil {
return 0, err
}
val := int(binary.LittleEndian.Uint64(randBytes) & 0x7FFFFFFFFFFFFFFF) // Ensure it's positive
return val, nil
}
func GenerateRandomNonRepeatingUint64(listLen int) ([]uint64, error) {
if listLen > int(1)<<32 {
return nil, ErrRandNonRepeatingUint64
}
listSet := make(set.Set[uint64])
for {
if listSet.Size() == listLen {
break
}
randNum, err := GenerateRandomUInt64()
if err != nil {
return nil, err
}
listSet.Add(randNum)
}
data := listSet.ToSlice()
return data, nil
}
func GenerateRandomNonRepeatingInt(listLen int) ([]int, error) {
if listLen > int(1)<<31 {
return nil, ErrRandNonRepeatingInt
}
listSet := make(set.Set[int])
for {
if listSet.Size() == listLen {
break
}
randNum, err := GenerateRandomInt()
if err != nil {
return nil, err
}
listSet.Add(randNum)
}
data := listSet.ToSlice()
return data, nil
}
func XorLists(l0 []uint64, l1 []uint64) ([]uint64, error) {
if len(l0) != len(l1) {
log.Printf("list len mismatch %d, %d", len(l0), len(l1))
return nil, ErrXorLengthMismatch
}
xorList := make([]uint64, len(l0))
for idx := 0; idx < len(l0); idx++ {
xorList[idx] = l0[idx] ^ l1[idx]
}
return xorList, nil
}
func EncodeBase64Str(data []uint64) string {
dataBytes := Uint64ArrToByteArr(data)
encoded := base64.StdEncoding.EncodeToString(dataBytes)
return encoded
}
func DecodeBase64Str(encoded string) ([]uint64, error) {
dataBytes, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
log.Print("error decoding base64 str: ", err)
return nil, ErrDecodeBase64Str
}
data := ByteArrToUint64Arr(dataBytes)
return data, nil
}
func Uint64ArrToByteArr(intArr []uint64) []byte {
byteArr := make([]byte, len(intArr)*8)
for idx, val := range intArr {
startIdx := idx * 8
endIdx := (idx + 1) * 8
binary.LittleEndian.PutUint64(byteArr[startIdx:endIdx], val)
}
return byteArr
}
func IntArrToByteArr(intArr []int) []byte {
byteArr := make([]byte, len(intArr)*4)
for idx, val := range intArr {
uval := uint32(val)
startIdx := idx * 4
endIdx := (idx + 1) * 4
binary.LittleEndian.PutUint32(byteArr[startIdx:endIdx], uval)
}
return byteArr
}
func ByteArrToUint64Arr(byteArr []byte) []uint64 {
intArr := make([]uint64, len(byteArr)/8)
for idx := 0; idx < len(intArr); idx++ {
startIdx := idx * 8
endIdx := (idx + 1) * 8
intArr[idx] = binary.LittleEndian.Uint64(byteArr[startIdx:endIdx])
}
return intArr
}
func ByteArrToIntArr(byteArr []byte) []int {
intArr := make([]int, len(byteArr)/4)
for idx := 0; idx < len(intArr); idx++ {
startIdx := idx * 4
endIdx := (idx + 1) * 4
uval := binary.LittleEndian.Uint32(byteArr[startIdx:endIdx])
intArr[idx] = int(uval)
}
return intArr
}
func IndexOf[T uint64 | int](arr []T, el T) (int, error) {
for idx, val := range arr {
if val == el {
return idx, nil
}
}
return -1, ErrElementNotInArray
}
func IdentityArray(arrLen int) []int {
identityArr := make([]int, arrLen)
for idx := range identityArr {
identityArr[idx] = idx
}
return identityArr
}
func ListToMatrix(listArr []int, numbCols int) ([][]int, error) {
if len(listArr)%numbCols != 0 {
log.Printf("Array is not evenly divisible by number of columns: %d mod %d = %d", len(listArr), numbCols, len(listArr)%numbCols)
return nil, ErrListToMatrixNotDivisible
}
numbRows := len(listArr) / numbCols
matrix := make([][]int, numbRows)
for idx := range matrix {
startIdx := idx * numbCols
endIdx := (idx + 1) * numbCols
matrix[idx] = listArr[startIdx:endIdx]
}
return matrix, nil
}
func MatrixTranspose(matrix [][]int) ([][]int, error) {
if matrix == nil || len(matrix) == 0 {
log.Print("can't transpose nil or zero len matrix")
return nil, ErrMatrixTranspose
}
rows := len(matrix)
cols := len((matrix)[0])
// Check if the matrix is not rectangular
for _, row := range matrix {
if len(row) != cols {
log.Print("all rows must have the same number of columns")
return nil, ErrMatrixTranspose
}
}
transposed := make([][]int, cols)
for i := range transposed {
transposed[i] = make([]int, rows)
}
for i := 0; i < rows; i++ {
for j := 0; j < cols; j++ {
transposed[j][i] = (matrix)[i][j]
}
}
return transposed, nil
}
func MatrixToList(matrix [][]int) []int {
var flat []int
for _, row := range matrix {
flat = append(flat, row...)
}
return flat
}
func Choice[T any](items []T) T {
r.Seed(time.Now().UnixNano()) // Seed the random number generator
return items[r.Intn(len(items))]
}
// GenerateRandomString creates a random string of a specified length.
func GenerateRandomString(length int) string {
charset := []rune("abcdefghijklmnopqrstuvwxyz0123456789")
b := make([]rune, length)
for i := range b {
b[i] = Choice[rune](charset)
}
return string(b)
}
func ParseHexString(hexStr string) ([]byte, error) {
// Decode the hex string into bytes
bytes, err := hex.DecodeString(hexStr)
if err != nil {
log.Print("parse hex string err: ", err)
return nil, ErrParseHexString
}
return bytes, nil
}

62
security/random_test.go Normal file
View File

@@ -0,0 +1,62 @@
package security
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestGenerateRandomNonRepeatingUint64(t *testing.T) {
arrLen := 100000
randNumbs, err := GenerateRandomNonRepeatingUint64(arrLen)
assert.NoError(t, err)
assert.Equal(t, len(randNumbs), arrLen)
}
func TestGenerateRandomNonRepeatingInt(t *testing.T) {
arrLen := 100000
randNumbs, err := GenerateRandomNonRepeatingInt(arrLen)
assert.NoError(t, err)
assert.Equal(t, len(randNumbs), arrLen)
}
func TestEncodeDecode(t *testing.T) {
testArr := []uint64{1, 2, 3, 4, 5, 6}
testEncode := EncodeBase64Str(testArr)
testDecode, err := DecodeBase64Str(testEncode)
assert.NoError(t, err)
assert.Equal(t, len(testArr), len(testDecode))
for idx, val := range testArr {
assert.Equal(t, val, testDecode[idx])
}
}
func TestMatrixTranspose(t *testing.T) {
matrix := [][]int{
{0, 1, 2},
{3, 4, 5},
}
expectedMatrixT := [][]int{
{0, 3},
{1, 4},
{2, 5},
}
expectedFlatMat := MatrixToList(expectedMatrixT)
matrixT, err := MatrixTranspose(matrix)
assert.NoError(t, err)
flatMat := MatrixToList(matrixT)
assert.Equal(t, len(flatMat), len(expectedFlatMat))
for idx := range flatMat {
assert.Equal(t, expectedFlatMat[idx], flatMat[idx])
}
}
func TestIntToByteAndBack(t *testing.T) {
origIntArr := []int{1, 2, 3, 4, 5}
byteArr := IntArrToByteArr(origIntArr)
intArr := ByteArrToIntArr(byteArr)
assert.ElementsMatch(t, origIntArr, intArr)
}

9
sqlc.yaml Normal file
View File

@@ -0,0 +1,9 @@
version: "2"
sql:
- engine: "sqlite"
queries: "./sqlite/query.sql"
schema: "./sqlite/schema.sql"
gen:
go:
package: "sqlc"
out: "./pkg/nkode-core/sqlc"

31
sqlc/db.go Normal file
View File

@@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package sqlc
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

50
sqlc/models.go Normal file
View File

@@ -0,0 +1,50 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
package sqlc
import (
"database/sql"
)
type Customer struct {
ID string
MaxNkodeLen int64
MinNkodeLen int64
DistinctSets int64
DistinctAttributes int64
LockOut int64
Expiration int64
AttributeValues []byte
SetValues []byte
LastRenew string
CreatedAt string
}
type SvgIcon struct {
ID int64
Svg string
}
type User struct {
ID string
Email string
Renew int64
RefreshToken sql.NullString
CustomerID string
Code string
Mask string
AttributesPerKey int64
NumberOfKeys int64
AlphaKey []byte
SetKey []byte
PassKey []byte
MaskKey []byte
Salt []byte
MaxNkodeLen int64
IdxInterface []byte
SvgIDInterface []byte
LastLogin interface{}
CreatedAt sql.NullString
}

478
sqlc/query.sql.go Normal file
View File

@@ -0,0 +1,478 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.27.0
// source: query.sql
package sqlc
import (
"context"
"database/sql"
)
const createCustomer = `-- name: CreateCustomer :exec
INSERT INTO customer (
id
,max_nkode_len
,min_nkode_len
,distinct_sets
,distinct_attributes
,lock_out
,expiration
,attribute_values
,set_values
,last_renew
,created_at
)
VALUES (?,?,?,?,?,?,?,?,?,?,?)
`
type CreateCustomerParams struct {
ID string
MaxNkodeLen int64
MinNkodeLen int64
DistinctSets int64
DistinctAttributes int64
LockOut int64
Expiration int64
AttributeValues []byte
SetValues []byte
LastRenew string
CreatedAt string
}
func (q *Queries) CreateCustomer(ctx context.Context, arg CreateCustomerParams) error {
_, err := q.db.ExecContext(ctx, createCustomer,
arg.ID,
arg.MaxNkodeLen,
arg.MinNkodeLen,
arg.DistinctSets,
arg.DistinctAttributes,
arg.LockOut,
arg.Expiration,
arg.AttributeValues,
arg.SetValues,
arg.LastRenew,
arg.CreatedAt,
)
return err
}
const createUser = `-- name: CreateUser :exec
INSERT INTO user (
id
,email
,renew
,refresh_token
,customer_id
,code
,mask
,attributes_per_key
,number_of_keys
,alpha_key
,set_key
,pass_key
,mask_key
,salt
,max_nkode_len
,idx_interface
,svg_id_interface
,created_at
)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
`
type CreateUserParams struct {
ID string
Email string
Renew int64
RefreshToken sql.NullString
CustomerID string
Code string
Mask string
AttributesPerKey int64
NumberOfKeys int64
AlphaKey []byte
SetKey []byte
PassKey []byte
MaskKey []byte
Salt []byte
MaxNkodeLen int64
IdxInterface []byte
SvgIDInterface []byte
CreatedAt sql.NullString
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) error {
_, err := q.db.ExecContext(ctx, createUser,
arg.ID,
arg.Email,
arg.Renew,
arg.RefreshToken,
arg.CustomerID,
arg.Code,
arg.Mask,
arg.AttributesPerKey,
arg.NumberOfKeys,
arg.AlphaKey,
arg.SetKey,
arg.PassKey,
arg.MaskKey,
arg.Salt,
arg.MaxNkodeLen,
arg.IdxInterface,
arg.SvgIDInterface,
arg.CreatedAt,
)
return err
}
const getCustomer = `-- name: GetCustomer :one
SELECT
max_nkode_len
,min_nkode_len
,distinct_sets
,distinct_attributes
,lock_out
,expiration
,attribute_values
,set_values
FROM customer
WHERE id = ?
`
type GetCustomerRow struct {
MaxNkodeLen int64
MinNkodeLen int64
DistinctSets int64
DistinctAttributes int64
LockOut int64
Expiration int64
AttributeValues []byte
SetValues []byte
}
func (q *Queries) GetCustomer(ctx context.Context, id string) (GetCustomerRow, error) {
row := q.db.QueryRowContext(ctx, getCustomer, id)
var i GetCustomerRow
err := row.Scan(
&i.MaxNkodeLen,
&i.MinNkodeLen,
&i.DistinctSets,
&i.DistinctAttributes,
&i.LockOut,
&i.Expiration,
&i.AttributeValues,
&i.SetValues,
)
return i, err
}
const getSvgCount = `-- name: GetSvgCount :one
SELECT COUNT(*) as count FROM svg_icon
`
func (q *Queries) GetSvgCount(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, getSvgCount)
var count int64
err := row.Scan(&count)
return count, err
}
const getSvgId = `-- name: GetSvgId :one
SELECT svg
FROM svg_icon
WHERE id = ?
`
func (q *Queries) GetSvgId(ctx context.Context, id int64) (string, error) {
row := q.db.QueryRowContext(ctx, getSvgId, id)
var svg string
err := row.Scan(&svg)
return svg, err
}
const getUser = `-- name: GetUser :one
SELECT
id
,renew
,refresh_token
,code
,mask
,attributes_per_key
,number_of_keys
,alpha_key
,set_key
,pass_key
,mask_key
,salt
,max_nkode_len
,idx_interface
,svg_id_interface
FROM user
WHERE user.email = ? AND user.customer_id = ?
`
type GetUserParams struct {
Email string
CustomerID string
}
type GetUserRow struct {
ID string
Renew int64
RefreshToken sql.NullString
Code string
Mask string
AttributesPerKey int64
NumberOfKeys int64
AlphaKey []byte
SetKey []byte
PassKey []byte
MaskKey []byte
Salt []byte
MaxNkodeLen int64
IdxInterface []byte
SvgIDInterface []byte
}
func (q *Queries) GetUser(ctx context.Context, arg GetUserParams) (GetUserRow, error) {
row := q.db.QueryRowContext(ctx, getUser, arg.Email, arg.CustomerID)
var i GetUserRow
err := row.Scan(
&i.ID,
&i.Renew,
&i.RefreshToken,
&i.Code,
&i.Mask,
&i.AttributesPerKey,
&i.NumberOfKeys,
&i.AlphaKey,
&i.SetKey,
&i.PassKey,
&i.MaskKey,
&i.Salt,
&i.MaxNkodeLen,
&i.IdxInterface,
&i.SvgIDInterface,
)
return i, err
}
const getUserRenew = `-- name: GetUserRenew :many
SELECT
id
,alpha_key
,set_key
,attributes_per_key
,number_of_keys
FROM user
WHERE customer_id = ?
`
type GetUserRenewRow struct {
ID string
AlphaKey []byte
SetKey []byte
AttributesPerKey int64
NumberOfKeys int64
}
func (q *Queries) GetUserRenew(ctx context.Context, customerID string) ([]GetUserRenewRow, error) {
rows, err := q.db.QueryContext(ctx, getUserRenew, customerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserRenewRow
for rows.Next() {
var i GetUserRenewRow
if err := rows.Scan(
&i.ID,
&i.AlphaKey,
&i.SetKey,
&i.AttributesPerKey,
&i.NumberOfKeys,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const refreshUserPasscode = `-- name: RefreshUserPasscode :exec
UPDATE user
SET
renew = ?
,code = ?
,mask = ?
,alpha_key = ?
,set_key = ?
,pass_key = ?
,mask_key = ?
,salt = ?
WHERE id = ?
`
type RefreshUserPasscodeParams struct {
Renew int64
Code string
Mask string
AlphaKey []byte
SetKey []byte
PassKey []byte
MaskKey []byte
Salt []byte
ID string
}
func (q *Queries) RefreshUserPasscode(ctx context.Context, arg RefreshUserPasscodeParams) error {
_, err := q.db.ExecContext(ctx, refreshUserPasscode,
arg.Renew,
arg.Code,
arg.Mask,
arg.AlphaKey,
arg.SetKey,
arg.PassKey,
arg.MaskKey,
arg.Salt,
arg.ID,
)
return err
}
const renewCustomer = `-- name: RenewCustomer :exec
UPDATE customer
SET attribute_values = ?, set_values = ?
WHERE id = ?
`
type RenewCustomerParams struct {
AttributeValues []byte
SetValues []byte
ID string
}
func (q *Queries) RenewCustomer(ctx context.Context, arg RenewCustomerParams) error {
_, err := q.db.ExecContext(ctx, renewCustomer, arg.AttributeValues, arg.SetValues, arg.ID)
return err
}
const renewUser = `-- name: RenewUser :exec
UPDATE user
SET alpha_key = ?, set_key = ?, renew = ?
WHERE id = ?
`
type RenewUserParams struct {
AlphaKey []byte
SetKey []byte
Renew int64
ID string
}
func (q *Queries) RenewUser(ctx context.Context, arg RenewUserParams) error {
_, err := q.db.ExecContext(ctx, renewUser,
arg.AlphaKey,
arg.SetKey,
arg.Renew,
arg.ID,
)
return err
}
const updateUser = `-- name: UpdateUser :exec
UPDATE user
SET renew = ?
,refresh_token = ?
,code = ?
,mask = ?
,attributes_per_key = ?
,number_of_keys = ?
,alpha_key = ?
,set_key = ?
,pass_key = ?
,mask_key = ?
,salt = ?
,max_nkode_len = ?
,idx_interface = ?
,svg_id_interface = ?
WHERE email = ? AND customer_id = ?
`
type UpdateUserParams struct {
Renew int64
RefreshToken sql.NullString
Code string
Mask string
AttributesPerKey int64
NumberOfKeys int64
AlphaKey []byte
SetKey []byte
PassKey []byte
MaskKey []byte
Salt []byte
MaxNkodeLen int64
IdxInterface []byte
SvgIDInterface []byte
Email string
CustomerID string
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.ExecContext(ctx, updateUser,
arg.Renew,
arg.RefreshToken,
arg.Code,
arg.Mask,
arg.AttributesPerKey,
arg.NumberOfKeys,
arg.AlphaKey,
arg.SetKey,
arg.PassKey,
arg.MaskKey,
arg.Salt,
arg.MaxNkodeLen,
arg.IdxInterface,
arg.SvgIDInterface,
arg.Email,
arg.CustomerID,
)
return err
}
const updateUserInterface = `-- name: UpdateUserInterface :exec
UPDATE user SET idx_interface = ?, last_login = ? WHERE id = ?
`
type UpdateUserInterfaceParams struct {
IdxInterface []byte
LastLogin interface{}
ID string
}
func (q *Queries) UpdateUserInterface(ctx context.Context, arg UpdateUserInterfaceParams) error {
_, err := q.db.ExecContext(ctx, updateUserInterface, arg.IdxInterface, arg.LastLogin, arg.ID)
return err
}
const updateUserRefreshToken = `-- name: UpdateUserRefreshToken :exec
UPDATE user SET refresh_token = ? WHERE id = ?
`
type UpdateUserRefreshTokenParams struct {
RefreshToken sql.NullString
ID string
}
func (q *Queries) UpdateUserRefreshToken(ctx context.Context, arg UpdateUserRefreshTokenParams) error {
_, err := q.db.ExecContext(ctx, updateUserRefreshToken, arg.RefreshToken, arg.ID)
return err
}

93
sqlc/sqlite_queue.go Normal file
View File

@@ -0,0 +1,93 @@
package sqlc
import (
"context"
"database/sql"
"errors"
"fmt"
"sync"
)
const writeBufferSize = 100
type SqlcGeneric func(*Queries, context.Context, any) error
type WriteTx struct {
ErrChan chan error
Query SqlcGeneric
Args interface{}
}
type Queue struct {
Queries *Queries
Db *sql.DB
WriteQueue chan WriteTx
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
func NewQueue(sqlDb *sql.DB, ctx context.Context) (*Queue, error) {
ctx, cancel := context.WithCancel(context.Background())
sqldb := &Queue{
Queries: New(sqlDb),
Db: sqlDb,
WriteQueue: make(chan WriteTx, writeBufferSize),
ctx: ctx,
cancel: cancel,
}
return sqldb, nil
}
func (d *Queue) Start() {
d.wg.Add(1)
defer d.wg.Done()
go func() {
for {
select {
case <-d.ctx.Done():
return
case writeTx := <-d.WriteQueue:
err := writeTx.Query(d.Queries, d.ctx, writeTx.Args)
writeTx.ErrChan <- err
}
}
}()
}
func (d *Queue) Stop() error {
d.cancel()
d.wg.Wait()
close(d.WriteQueue)
return d.Db.Close()
}
func (d *Queue) EnqueueWriteTx(queryFunc SqlcGeneric, args any) error {
select {
case <-d.ctx.Done():
return errors.New("database is shutting down")
default:
}
errChan := make(chan error, 1)
writeTx := WriteTx{
Query: queryFunc,
Args: args,
ErrChan: errChan,
}
d.WriteQueue <- writeTx
return <-errChan
}
func OpenSqliteDb(dbPath string) (*sql.DB, error) {
sqliteDb, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
if err := sqliteDb.Ping(); err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
return sqliteDb, nil
}

136
sqlite/query.sql Normal file
View File

@@ -0,0 +1,136 @@
-- name: CreateCustomer :exec
INSERT INTO customer (
id
,max_nkode_len
,min_nkode_len
,distinct_sets
,distinct_attributes
,lock_out
,expiration
,attribute_values
,set_values
,last_renew
,created_at
)
VALUES (?,?,?,?,?,?,?,?,?,?,?);
-- name: CreateUser :exec
INSERT INTO user (
id
,email
,renew
,refresh_token
,customer_id
,code
,mask
,attributes_per_key
,number_of_keys
,alpha_key
,set_key
,pass_key
,mask_key
,salt
,max_nkode_len
,idx_interface
,svg_id_interface
,created_at
)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);
-- name: UpdateUser :exec
UPDATE user
SET renew = ?
,refresh_token = ?
,code = ?
,mask = ?
,attributes_per_key = ?
,number_of_keys = ?
,alpha_key = ?
,set_key = ?
,pass_key = ?
,mask_key = ?
,salt = ?
,max_nkode_len = ?
,idx_interface = ?
,svg_id_interface = ?
WHERE email = ? AND customer_id = ?;
-- name: UpdateUserInterface :exec
UPDATE user SET idx_interface = ?, last_login = ? WHERE id = ?;
-- name: UpdateUserRefreshToken :exec
UPDATE user SET refresh_token = ? WHERE id = ?;
-- name: RenewCustomer :exec
UPDATE customer
SET attribute_values = ?, set_values = ?
WHERE id = ?;
-- name: RenewUser :exec
UPDATE user
SET alpha_key = ?, set_key = ?, renew = ?
WHERE id = ?;
-- name: RefreshUserPasscode :exec
UPDATE user
SET
renew = ?
,code = ?
,mask = ?
,alpha_key = ?
,set_key = ?
,pass_key = ?
,mask_key = ?
,salt = ?
WHERE id = ?;
-- name: GetUserRenew :many
SELECT
id
,alpha_key
,set_key
,attributes_per_key
,number_of_keys
FROM user
WHERE customer_id = ?;
-- name: GetCustomer :one
SELECT
max_nkode_len
,min_nkode_len
,distinct_sets
,distinct_attributes
,lock_out
,expiration
,attribute_values
,set_values
FROM customer
WHERE id = ?;
-- name: GetUser :one
SELECT
id
,renew
,refresh_token
,code
,mask
,attributes_per_key
,number_of_keys
,alpha_key
,set_key
,pass_key
,mask_key
,salt
,max_nkode_len
,idx_interface
,svg_id_interface
FROM user
WHERE user.email = ? AND user.customer_id = ?;
-- name: GetSvgId :one
SELECT svg
FROM svg_icon
WHERE id = ?;
-- name: GetSvgCount :one
SELECT COUNT(*) as count FROM svg_icon;

57
sqlite/schema.sql Normal file
View File

@@ -0,0 +1,57 @@
PRAGMA journal_mode=WAL;
CREATE TABLE IF NOT EXISTS customer (
id TEXT NOT NULL PRIMARY KEY
,max_nkode_len INTEGER NOT NULL
,min_nkode_len INTEGER NOT NULL
,distinct_sets INTEGER NOT NULL
,distinct_attributes INTEGER NOT NULL
,lock_out INTEGER NOT NULL
,expiration INTEGER NOT NULL
,attribute_values BLOB NOT NULL
,set_values BLOB NOT NULL
,last_renew TEXT NOT NULL
,created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user (
id TEXT NOT NULL PRIMARY KEY
,email TEXT NOT NULL
-- first_name TEXT NOT NULL
-- last_name TEXT NOT NULL
,renew INT NOT NULL
,refresh_token TEXT
,customer_id TEXT NOT NULL
-- Enciphered Passcode
,code TEXT NOT NULL
,mask TEXT NOT NULL
-- Keypad Dimensions
,attributes_per_key INT NOT NULL
,number_of_keys INT NOT NULL
-- User Keys
,alpha_key BLOB NOT NULL
,set_key BLOB NOT NULL
,pass_key BLOB NOT NULL
,mask_key BLOB NOT NULL
,salt BLOB NOT NULL
,max_nkode_len INT NOT NULL
-- User Interface
,idx_interface BLOB NOT NULL
,svg_id_interface BLOB NOT NULL
,last_login TEXT NULL
,created_at TEXT
,FOREIGN KEY (customer_id) REFERENCES customer(id)
,UNIQUE(customer_id, email)
);
CREATE TABLE IF NOT EXISTS svg_icon (
id INTEGER PRIMARY KEY AUTOINCREMENT
,svg TEXT NOT NULL
);

7
utils/timestamp.go Normal file
View File

@@ -0,0 +1,7 @@
package utils
import "time"
func TimeStamp() string {
return time.Now().Format(time.RFC3339)
}