From 7711fc14edb0316d3246da24392545d88047057d Mon Sep 17 00:00:00 2001 From: Donovan Date: Sat, 17 Aug 2024 10:26:02 -0500 Subject: [PATCH] initial commit --- .gitignore | 1 + customer/customer.go | 1 + customer/customer_attributes.go | 70 ++++++++++ customer/customer_attributes_test.go | 13 ++ go.mod | 14 ++ go.sum | 14 ++ hashset/hashset.go | 53 +++++++ hashset/hashset_test.go | 35 +++++ main.go | 9 ++ models/models.go | 28 ++++ py-builtin/py-builtin.go | 11 ++ users/user.go | 1 + users/user_cipher_keys.go | 181 ++++++++++++++++++++++++ users/user_interface.go | 176 +++++++++++++++++++++++ users/user_test.go | 120 ++++++++++++++++ util/util.go | 202 +++++++++++++++++++++++++++ util/util_test.go | 46 ++++++ 17 files changed, 975 insertions(+) create mode 100644 .gitignore create mode 100644 customer/customer.go create mode 100644 customer/customer_attributes.go create mode 100644 customer/customer_attributes_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 hashset/hashset.go create mode 100644 hashset/hashset_test.go create mode 100644 main.go create mode 100644 models/models.go create mode 100644 py-builtin/py-builtin.go create mode 100644 users/user.go create mode 100644 users/user_cipher_keys.go create mode 100644 users/user_interface.go create mode 100644 users/user_test.go create mode 100644 util/util.go create mode 100644 util/util_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/customer/customer.go b/customer/customer.go new file mode 100644 index 0000000..943abb2 --- /dev/null +++ b/customer/customer.go @@ -0,0 +1 @@ +package customer diff --git a/customer/customer_attributes.go b/customer/customer_attributes.go new file mode 100644 index 0000000..52f2037 --- /dev/null +++ b/customer/customer_attributes.go @@ -0,0 +1,70 @@ +package customer + +import ( + "errors" + "fmt" + "go-nkode/models" + "go-nkode/util" +) + +// TODO: make generic +type CustomerAttributes struct { + AttrVals []uint64 + SetVals []uint64 + keypadSize models.KeypadSize +} + +func NewCustomerAttributes(keypadSize models.KeypadSize) (*CustomerAttributes, error) { + if keypadSize.IsDispersable() { + return nil, errors.New("number of keys must be less than the number of attributes per key to be dispersion resistant") + } + + attrVals, errAttr := util.GenerateRandomNonRepeatingUint64(keypadSize.TotalAttrs()) + if errAttr != nil { + return nil, errAttr + } + setVals, errSet := util.GenerateRandomNonRepeatingUint64(keypadSize.AttrsPerKey) + if errSet != nil { + return nil, errSet + } + + customerAttrs := CustomerAttributes{ + AttrVals: attrVals, + SetVals: setVals, + keypadSize: keypadSize, + } + return &customerAttrs, nil +} + +func (c *CustomerAttributes) Renew() error { + attrVals, errAttr := util.GenerateRandomNonRepeatingUint64(c.keypadSize.TotalAttrs()) + if errAttr != nil { + return errAttr + } + setVals, errSet := util.GenerateRandomNonRepeatingUint64(c.keypadSize.AttrsPerKey) + if errSet != nil { + return errSet + } + c.AttrVals = attrVals + c.SetVals = setVals + return nil +} + +func (c *CustomerAttributes) IndexOfAttr(attrVal uint64) int { + // TODO: should this be mapped instead? + return util.IndexOf[uint64](c.AttrVals, attrVal) +} + +func (c *CustomerAttributes) IndexOfSet(setVal uint64) int { + // TODO: should this be mapped instead? + return util.IndexOf[uint64](c.SetVals, setVal) +} + +func (c *CustomerAttributes) GetAttrSetVal(attrVal uint64) (uint64, error) { + indexOfAttr := c.IndexOfAttr(attrVal) + if indexOfAttr == -1 { + return 0, errors.New(fmt.Sprintf("No attribute %d", attrVal)) + } + setIdx := indexOfAttr % c.keypadSize.AttrsPerKey + return c.SetVals[setIdx], nil +} diff --git a/customer/customer_attributes_test.go b/customer/customer_attributes_test.go new file mode 100644 index 0000000..e30874c --- /dev/null +++ b/customer/customer_attributes_test.go @@ -0,0 +1,13 @@ +package customer + +import ( + "github.com/stretchr/testify/assert" + "go-nkode/models" + "testing" +) + +func TestNewCustomerAttributes(t *testing.T) { + keypad := models.KeypadSize{AttrsPerKey: 10, NumbOfKeys: 5} + _, nil := NewCustomerAttributes(keypad) + assert.NoError(t, nil) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..19e9492 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module go-nkode + +go 1.19 + +require ( + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.26.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fc8de62 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +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/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +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/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hashset/hashset.go b/hashset/hashset.go new file mode 100644 index 0000000..a7e15a2 --- /dev/null +++ b/hashset/hashset.go @@ -0,0 +1,53 @@ +package hashset + +type Set[T comparable] map[T]struct{} + +func (s *Set[T]) Add(element T) { + (*s)[element] = struct{}{} +} + +func (s *Set[T]) Remove(element T) { + delete(*s, element) +} + +func (s *Set[T]) Contains(element T) bool { + _, exists := (*s)[element] + return exists +} + +func (s *Set[T]) Size() int { + return len(*s) +} + +func (s *Set[T]) ToSlice() []T { + list := make([]T, 0, len(*s)) + for key := range *s { + list = append(list, key) + } + return list +} + +func NewSetFromSlice[T comparable](slice []T) Set[T] { + set := make(Set[T]) + for _, val := range slice { + set.Add(val) + } + return set +} + +func (s *Set[T]) Copy() Set[T] { + newSet := make(Set[T]) + for key, val := range *s { + newSet[key] = val + } + return newSet +} + +func (s *Set[T]) IsDisjoint(otherSet Set[T]) bool { + for attr, _ := range *s { + if otherSet.Contains(attr) { + return false + } + } + return true +} diff --git a/hashset/hashset_test.go b/hashset/hashset_test.go new file mode 100644 index 0000000..7912f84 --- /dev/null +++ b/hashset/hashset_test.go @@ -0,0 +1,35 @@ +package hashset + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSet(t *testing.T) { + intSet := make(Set[int]) + intSet.Add(1) + intSet.Add(2) + assert.EqualValues(t, intSet.Size(), 2) + intSet.Add(3) + intSet.Add(3) + assert.EqualValues(t, intSet.Size(), 3) + intSet.Remove(2) + assert.EqualValues(t, intSet.Size(), 2) + assert.False(t, intSet.Contains(2)) + assert.True(t, intSet.Contains(1)) + + list := intSet.ToSlice() + assert.Contains(t, list, 1) + assert.Contains(t, list, 3) +} + +func TestSet_Copy(t *testing.T) { + intSet := NewSetFromSlice[int]([]int{1, 2, 3}) + + copySet := intSet.Copy() + + intSet.Remove(1) + assert.Equal(t, intSet.Size(), 2) + assert.Equal(t, copySet.Size(), 3) + +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fb6b1eb --- /dev/null +++ b/main.go @@ -0,0 +1,9 @@ +package main + +import "fmt" + +func main() { + a := 3 + b := a / 2 + fmt.Println(b) +} diff --git a/models/models.go b/models/models.go new file mode 100644 index 0000000..79fd451 --- /dev/null +++ b/models/models.go @@ -0,0 +1,28 @@ +package models + +type KeypadSize struct { + AttrsPerKey int + NumbOfKeys int +} + +func (kp *KeypadSize) TotalAttrs() int { + return kp.AttrsPerKey * kp.NumbOfKeys +} + +func (kp *KeypadSize) IsDispersable() bool { + return kp.AttrsPerKey <= kp.NumbOfKeys +} + +type EncipheredNKode struct { + Code string + Mask string +} + +type NKodePolicy struct { + MaxNkodeLen int + MinNkodeLen int + DistinctSets int + DistinctAttributes int + LockOut int + Expiration int // seconds, -1 no expiration +} diff --git a/py-builtin/py-builtin.go b/py-builtin/py-builtin.go new file mode 100644 index 0000000..3c2cac9 --- /dev/null +++ b/py-builtin/py-builtin.go @@ -0,0 +1,11 @@ +package py_builtin + +func All[T comparable](slice []T, condition func(T) bool) bool { + + for _, v := range slice { + if !condition(v) { + return false + } + } + return true +} diff --git a/users/user.go b/users/user.go new file mode 100644 index 0000000..82abcb9 --- /dev/null +++ b/users/user.go @@ -0,0 +1 @@ +package users diff --git a/users/user_cipher_keys.go b/users/user_cipher_keys.go new file mode 100644 index 0000000..45d2b23 --- /dev/null +++ b/users/user_cipher_keys.go @@ -0,0 +1,181 @@ +package users + +import ( + "crypto/sha256" + "errors" + "fmt" + "go-nkode/customer" + "go-nkode/models" + "go-nkode/util" + "golang.org/x/crypto/bcrypt" +) + +type UserCipherKeys struct { + AlphaKey []uint64 + SetKey []uint64 + PassKey []uint64 + MaskKey []uint64 + Salt []byte + MaxNKodeLen int +} + +func NewUserCipherKeys(keypadSize models.KeypadSize, setVals []uint64, maxNKodeLen int) (*UserCipherKeys, error) { + if len(setVals) != keypadSize.AttrsPerKey { + return nil, errors.New(fmt.Sprintf("setVals len != attrsPerKey, %d, %d", len(setVals), keypadSize.AttrsPerKey)) + } + + setKey, err := util.GenerateRandomNonRepeatingUint64(keypadSize.AttrsPerKey) + if err != nil { + return nil, err + } + setKey, err = util.XorLists(setKey, setVals) + if err != nil { + return nil, err + } + + alphakey, _ := util.GenerateRandomNonRepeatingUint64(keypadSize.AttrsPerKey) + passKey, _ := util.GenerateRandomNonRepeatingUint64(maxNKodeLen) + maskKey, _ := util.GenerateRandomNonRepeatingUint64(maxNKodeLen) + salt, _ := util.RandomBytes(10) + userCipherKeys := UserCipherKeys{ + AlphaKey: alphakey, + PassKey: passKey, + MaskKey: maskKey, + SetKey: setKey, + Salt: salt, + MaxNKodeLen: maxNKodeLen, + } + return &userCipherKeys, nil +} + +func (u *UserCipherKeys) PadUserMask(userMask []uint64, setVals []uint64) ([]uint64, error) { + if len(userMask) > u.MaxNKodeLen { + return nil, errors.New("user mask length exceeds max nkode length") + } + 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 []byte, passcodeAttrIdx []int, customerAttrs customer.CustomerAttributes) error { + passcodeCipher := u.encipherCode(passcodeAttrIdx, customerAttrs) + passwordDigest, err := u.saltAndDigest(passcodeCipher) + if err != nil { + return err + } + err = bcrypt.CompareHashAndPassword(hashedPassword, passwordDigest) + if err != nil { + return err + } + return nil +} + +func (u *UserCipherKeys) EncipherSaltHashCode(passcodeAttrIdx []int, customerAttrs customer.CustomerAttributes) ([]byte, error) { + passcodeCipher := u.encipherCode(passcodeAttrIdx, customerAttrs) + + passcodeDigest, err := u.saltAndDigest(passcodeCipher) + if err != nil { + return nil, err + } + return u.hashPasscode(passcodeDigest) +} + +func (u *UserCipherKeys) encipherCode(passcodeAttrIdx []int, customerAttrs customer.CustomerAttributes) []uint64 { + passcodeLen := len(passcodeAttrIdx) + + passcodeCipher := make([]uint64, u.MaxNKodeLen) + + for idx := 0; idx < passcodeLen; idx++ { + attrIdx := passcodeAttrIdx[idx] + alpha := u.AlphaKey[attrIdx] + attrVal := customerAttrs.AttrVals[idx] + pass := u.PassKey[idx] + passcodeCipher[idx] = alpha ^ pass ^ attrVal + } + return passcodeCipher +} + +func (u *UserCipherKeys) saltAndDigest(passcode []uint64) ([]byte, error) { + passcodeBytes := util.Uint64ArrToByteArr(passcode) + passcodeBytes = append(passcodeBytes, u.Salt...) + h := sha256.New() + passcodeSha, err := h.Write(passcodeBytes) + if err != nil { + return nil, err + } + passcodeDigest := uint64(passcodeSha) + return util.Uint64ArrToByteArr([]uint64{passcodeDigest}), 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 customer.CustomerAttributes) (string, error) { + paddedPasscodeSets, err := u.PadUserMask(passcodeSet, customerAttrs.SetVals) + if err != nil { + return "", err + } + + cipheredMask := make([]uint64, len(paddedPasscodeSets)) + for idx := range paddedPasscodeSets { + setIdx := customerAttrs.IndexOfSet(paddedPasscodeSets[idx]) + setKeyVal := u.SetKey[setIdx] + maskKeyVal := u.MaskKey[idx] + setVal := paddedPasscodeSets[idx] + cipheredMask[idx] = setKeyVal ^ maskKeyVal ^ setVal + } + mask := util.EncodeBase64Str(cipheredMask) + return mask, nil +} + +func (u *UserCipherKeys) DecipherMask(mask string, setVals []uint64, passcodeLen int) ([]uint64, error) { + decodedMask, err := util.DecodeBase64Str(mask) + if err != nil { + return nil, err + } + decipheredMask, err := util.XorLists(decodedMask, u.MaskKey) + if err != nil { + return nil, err + } + setKeyRandComponent, err := util.XorLists(setVals, u.SetKey) + if err != nil { + return nil, err + } + + passcodeSet := make([]uint64, passcodeLen) + for idx, setCipher := range decipheredMask[:passcodeLen] { + setIdx := util.IndexOf(setKeyRandComponent, setCipher) + passcodeSet[idx] = setVals[setIdx] + } + return passcodeSet, nil +} + +func (u *UserCipherKeys) EncipherNKode(passcodeAttrIdx []int, customerAttrs customer.CustomerAttributes) (*models.EncipheredNKode, error) { + code, err := u.EncipherSaltHashCode(passcodeAttrIdx, customerAttrs) + if err != nil { + return nil, err + } + passcodeSet := make([]uint64, len(passcodeAttrIdx)) + + for idx := range passcodeSet { + passcodeAttr := customerAttrs.AttrVals[passcodeAttrIdx[idx]] + passcodeSet[idx], err = customerAttrs.GetAttrSetVal(passcodeAttr) + if err != nil { + return nil, err + } + } + mask, err := u.EncipherMask(passcodeSet, customerAttrs) + encipheredCode := models.EncipheredNKode{ + Code: string(code), + Mask: mask, + } + return &encipheredCode, nil +} diff --git a/users/user_interface.go b/users/user_interface.go new file mode 100644 index 0000000..077ab09 --- /dev/null +++ b/users/user_interface.go @@ -0,0 +1,176 @@ +package users + +import ( + "errors" + "go-nkode/hashset" + "go-nkode/models" + "go-nkode/util" +) + +type UserInterface struct { + IdxInterface []int + KeypadSize models.KeypadSize +} + +func NewUserInterface(keypadSize models.KeypadSize) (*UserInterface, error) { + idxInterface := util.IdentityArray(keypadSize.TotalAttrs()) + userInterface := UserInterface{ + IdxInterface: idxInterface, + KeypadSize: keypadSize, + } + err := userInterface.RandomShuffle() + if 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 := util.MatrixTranspose(keypadView) + if err != nil { + return err + } + + for idx, set := range setView { + err := util.FisherYatesShuffle(&set) + if err != nil { + return nil + } + setView[idx] = set + } + + keypadView, err = util.MatrixTranspose(setView) + if err != nil { + return err + } + u.IdxInterface = util.MatrixToList(keypadView) + return nil +} + +func (u *UserInterface) InterfaceMatrix() ([][]int, error) { + return util.ListToMatrix(u.IdxInterface, u.KeypadSize.AttrsPerKey) +} + +func (u *UserInterface) SetViewMatrix() ([][]int, error) { + keypadView, err := u.InterfaceMatrix() + if err != nil { + return nil, err + } + return util.MatrixTranspose(keypadView) +} + +func (u *UserInterface) DisperseInterface() error { + if !u.KeypadSize.IsDispersable() { + return errors.New("interface is not dispersable") + } + + 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 := util.ListToMatrix(u.IdxInterface, u.KeypadSize.AttrsPerKey) + if err != nil { + return err + } + err = util.FisherYatesShuffle[[]int](&userInterfaceMatrix) + if err != nil { + return err + } + u.IdxInterface = util.MatrixToList(userInterfaceMatrix) + return nil +} + +func (u *UserInterface) randomAttributeRotation() error { + userInterface, err := u.InterfaceMatrix() + if err != nil { + return err + } + + transposeUserInterface, err := util.MatrixTranspose(userInterface) + + attrRotation, err := util.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 = util.MatrixTranspose(transposeUserInterface) + if err != nil { + return err + } + u.IdxInterface = util.MatrixToList(userInterface) + return nil +} + +func (u *UserInterface) AttributeAdjacencyGraph() (map[int]hashset.Set[int], error) { + interfaceKeypad, err := u.InterfaceMatrix() + if err != nil { + return nil, err + } + graph := make(map[int]hashset.Set[int]) + + for _, key := range interfaceKeypad { + keySet := hashset.NewSetFromSlice(key) + for _, attr := range key { + attrAdjacency := keySet.Copy() + attrAdjacency.Remove(attr) + graph[attr] = attrAdjacency + } + } + return graph, nil +} + +func (u *UserInterface) PartialInterfaceShuffle() error { + err := u.shuffleKeys() + if err != nil { + return err + } + numbOfSelectedSets := u.KeypadSize.AttrsPerKey / 2 + if u.KeypadSize.AttrsPerKey&1 == 1 { + numbOfSelectedSets += util.Choice[int]([]int{0, 1}) + } + setIdxs, err := util.RandomPermutation(u.KeypadSize.AttrsPerKey) + if err != nil { + return err + } + selectedSets := hashset.NewSetFromSlice[int](setIdxs[:numbOfSelectedSets]) + + keypadSetView, err := u.SetViewMatrix() + if err != nil { + return err + } + interfaceBySet := make([][]int, u.KeypadSize.AttrsPerKey) + for idx, attrs := range keypadSetView { + if selectedSets.Contains(idx) { + err = util.FisherYatesShuffle[int](&attrs) + if err != nil { + return err + } + } + interfaceBySet[idx] = attrs + } + keypadView, err := util.MatrixTranspose(interfaceBySet) + if err != nil { + return err + } + u.IdxInterface = util.MatrixToList(keypadView) + return nil +} diff --git a/users/user_test.go b/users/user_test.go new file mode 100644 index 0000000..57821ef --- /dev/null +++ b/users/user_test.go @@ -0,0 +1,120 @@ +package users + +import ( + "github.com/stretchr/testify/assert" + "go-nkode/customer" + "go-nkode/models" + py_builtin "go-nkode/py-builtin" + "testing" +) + +func TestUserCipherKeys_EncipherSaltHashCode(t *testing.T) { + keypadSize := models.KeypadSize{AttrsPerKey: 10, NumbOfKeys: 5} + maxNKodeLen := 10 + customerAttrs, err := customer.NewCustomerAttributes(keypadSize) + assert.NoError(t, err) + newUser, err := NewUserCipherKeys(keypadSize, customerAttrs.SetVals, maxNKodeLen) + assert.NoError(t, err) + passcodeIdx := []int{0, 1, 2, 3} + encipher0, err := newUser.EncipherSaltHashCode(passcodeIdx, *customerAttrs) + assert.NoError(t, err) + err = newUser.ValidPassword(encipher0, passcodeIdx, *customerAttrs) + assert.NoError(t, err) +} + +func TestUserCipherKeys_EncipherDecipherMask(t *testing.T) { + keypadSize := models.KeypadSize{AttrsPerKey: 10, NumbOfKeys: 5} + maxNKodeLen := 10 + + customerAttrs, err := customer.NewCustomerAttributes(keypadSize) + assert.NoError(t, err) + newUser, err := NewUserCipherKeys(keypadSize, customerAttrs.SetVals, maxNKodeLen) + assert.NoError(t, err) + passcodeIdx := []int{0, 1, 2, 3} + originalSetVals := make([]uint64, len(passcodeIdx)) + + for idx, val := range passcodeIdx { + attr := customerAttrs.AttrVals[val] + originalSetVals[idx], err = customerAttrs.GetAttrSetVal(attr) + assert.NoError(t, err) + } + encipheredCode, err := newUser.EncipherNKode(passcodeIdx, *customerAttrs) + assert.NoError(t, err) + passcodeSetVals, err := newUser.DecipherMask(encipheredCode.Mask, customerAttrs.SetVals, len(passcodeIdx)) + assert.NoError(t, err) + + for idx, setVal := range passcodeSetVals { + assert.Equal(t, setVal, originalSetVals[idx]) + } +} + +func TestUserInterface_RandomShuffle(t *testing.T) { + keypadSize := models.KeypadSize{ + AttrsPerKey: 10, + NumbOfKeys: 5, + } + userInterface, err := NewUserInterface(keypadSize) + 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++ { + keypadSize := models.KeypadSize{AttrsPerKey: 7, NumbOfKeys: 10} + + userInterface, err := NewUserInterface(keypadSize) + 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) { + keypadSize := models.KeypadSize{AttrsPerKey: 7, NumbOfKeys: 10} + userInterface, err := NewUserInterface(keypadSize) + assert.NoError(t, err) + preShuffle := userInterface.IdxInterface + err = userInterface.PartialInterfaceShuffle() + assert.NoError(t, err) + postShuffle := userInterface.IdxInterface + + shuffleCompare := make([]bool, len(postShuffle)) + for idx, val := range preShuffle { + shuffleCompare[idx] = val == postShuffle[idx] + } + + allTrue := py_builtin.All[bool](shuffleCompare, func(n bool) bool { + return n == true + }) + assert.False(t, allTrue) + + allFalse := py_builtin.All[bool](shuffleCompare, func(n bool) bool { + return n == false + }) + + assert.False(t, allFalse) + +} diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..63cff2d --- /dev/null +++ b/util/util.go @@ -0,0 +1,202 @@ +package util + +import ( + "crypto/rand" + "encoding/base64" + "encoding/binary" + "errors" + "fmt" + "go-nkode/hashset" + "math/big" + r "math/rand" + "time" +) + +type ShuffleTypes interface { + []int | int | []uint64 | uint64 +} + +// fisherYatesShuffle shuffles a slice of bytes in place using the Fisher-Yates algorithm. +func fisherYatesShuffle[T ShuffleTypes](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 { + return err + } + j := bigJ.Int64() + (*b)[i], (*b)[j] = (*b)[j], (*b)[i] + } + return nil +} + +func FisherYatesShuffle[T ShuffleTypes](b *[]T) error { + return fisherYatesShuffle(b) +} + +func RandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + 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 GenerateRandomNonRepeatingUint64(listLen int) ([]uint64, error) { + if listLen > int(1)<<32 { + return nil, errors.New("list length must be less than 2^32") + } + listSet := make(hashset.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 XorLists(l0 []uint64, l1 []uint64) ([]uint64, error) { + if len(l0) != len(l1) { + return nil, errors.New(fmt.Sprintf("list len mismatch %d, %d", len(l0), len(l1))) + } + + 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 { + return nil, err + } + 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 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 IndexOf[T uint64 | int](arr []T, el T) int { + for idx, val := range arr { + if val == el { + return idx + } + } + return -1 +} + +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 { + return nil, errors.New(fmt.Sprintf("Array is not evenly divisible by number of columns: %d mod %d = %d", len(listArr), numbCols, len(listArr)%numbCols)) + } + 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 { + return nil, fmt.Errorf("matrix cannot be nil or empty") + } + + rows := len(matrix) + cols := len((matrix)[0]) + + // Check if the matrix is not rectangular + for _, row := range matrix { + if len(row) != cols { + return nil, fmt.Errorf("all rows must have the same number of columns") + } + } + + 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))] +} diff --git a/util/util_test.go b/util/util_test.go new file mode 100644 index 0000000..76bae04 --- /dev/null +++ b/util/util_test.go @@ -0,0 +1,46 @@ +package util + +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 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]) + } +}