initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.idea
|
||||||
1
customer/customer.go
Normal file
1
customer/customer.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package customer
|
||||||
70
customer/customer_attributes.go
Normal file
70
customer/customer_attributes.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
13
customer/customer_attributes_test.go
Normal file
13
customer/customer_attributes_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
14
go.mod
Normal file
14
go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
14
go.sum
Normal file
14
go.sum
Normal file
@@ -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=
|
||||||
53
hashset/hashset.go
Normal file
53
hashset/hashset.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
35
hashset/hashset_test.go
Normal file
35
hashset/hashset_test.go
Normal file
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
9
main.go
Normal file
9
main.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
a := 3
|
||||||
|
b := a / 2
|
||||||
|
fmt.Println(b)
|
||||||
|
}
|
||||||
28
models/models.go
Normal file
28
models/models.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
11
py-builtin/py-builtin.go
Normal file
11
py-builtin/py-builtin.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
1
users/user.go
Normal file
1
users/user.go
Normal file
@@ -0,0 +1 @@
|
|||||||
|
package users
|
||||||
181
users/user_cipher_keys.go
Normal file
181
users/user_cipher_keys.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
176
users/user_interface.go
Normal file
176
users/user_interface.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
120
users/user_test.go
Normal file
120
users/user_test.go
Normal file
@@ -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)
|
||||||
|
|
||||||
|
}
|
||||||
202
util/util.go
Normal file
202
util/util.go
Normal file
@@ -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))]
|
||||||
|
}
|
||||||
46
util/util_test.go
Normal file
46
util/util_test.go
Normal file
@@ -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])
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user