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