migrate nkode-core
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
.idea
|
||||
icons/
|
||||
json_icon/
|
||||
flaticon_colored_svgs/
|
||||
6
Taskfile.yaml
Normal file
6
Taskfile.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
version: "3"
|
||||
|
||||
tasks:
|
||||
sql:
|
||||
cmds:
|
||||
- sqlc generate
|
||||
266
api/nkode_api.go
Normal file
266
api/nkode_api.go
Normal 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
124
api/nkode_api_test.go
Normal 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
258
cmd/nkode/nkode.go
Normal 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
5
config/config.go
Normal file
@@ -0,0 +1,5 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
FrontendHost = "https://nkode.tech"
|
||||
)
|
||||
66
config/constants.go
Normal file
66
config/constants.go
Normal 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
150
email/queue.go
Normal 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
29
email/queue_test.go
Normal 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
102
entities/customer.go
Normal 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(),
|
||||
}
|
||||
}
|
||||
94
entities/customer_attributes.go
Normal file
94
entities/customer_attributes.go
Normal 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
57
entities/customer_test.go
Normal 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)
|
||||
}
|
||||
55
entities/keypad_dimension.go
Normal file
55
entities/keypad_dimension.go
Normal 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
23
entities/keypad_utils.go
Normal 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
101
entities/models.go
Normal 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
34
entities/policy.go
Normal 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
142
entities/user.go
Normal 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
|
||||
}
|
||||
193
entities/user_cipher_keys.go
Normal file
193
entities/user_cipher_keys.go
Normal 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
185
entities/user_interface.go
Normal 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
|
||||
}
|
||||
201
entities/user_signup_session.go
Normal file
201
entities/user_signup_session.go
Normal 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
133
entities/user_test.go
Normal 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
34
go.mod
Normal 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
58
go.sum
Normal 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=
|
||||
20
repository/customer_user_repository.go
Normal file
20
repository/customer_user_repository.go
Normal 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)
|
||||
}
|
||||
401
repository/sqlite_repository.go
Normal file
401
repository/sqlite_repository.go
Normal 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
|
||||
}
|
||||
63
repository/sqlite_repository_test.go
Normal file
63
repository/sqlite_repository_test.go
Normal 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
123
security/jwt_claims.go
Normal 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)
|
||||
}
|
||||
28
security/jwt_claims_test.go
Normal file
28
security/jwt_claims_test.go
Normal 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
289
security/random.go
Normal 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
62
security/random_test.go
Normal 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
9
sqlc.yaml
Normal 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
31
sqlc/db.go
Normal 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
50
sqlc/models.go
Normal 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
478
sqlc/query.sql.go
Normal 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
93
sqlc/sqlite_queue.go
Normal 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
136
sqlite/query.sql
Normal 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
57
sqlite/schema.sql
Normal 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
7
utils/timestamp.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package utils
|
||||
|
||||
import "time"
|
||||
|
||||
func TimeStamp() string {
|
||||
return time.Now().Format(time.RFC3339)
|
||||
}
|
||||
Reference in New Issue
Block a user