From d0f4a94f7502ce37adf1f08065b07262b682c4bf Mon Sep 17 00:00:00 2001 From: Patryk Osmaczko Date: Thu, 17 Mar 2022 17:58:35 +0100 Subject: [PATCH] fix: move visual-identity service to protocol/identity It is required to be called before RPC server is running on client side --- mobile/status.go | 10 ++ node/get_status_node.go | 3 - node/status_node_services.go | 22 ---- protocol/identity/colorhash/colorhash.go | 73 +++++++++++ protocol/identity/colorhash/colorhash_test.go | 57 ++++++++ protocol/identity/emojihash/emojihash.go | 94 +++++++++++++ .../identity/emojihash/emojihash_test.go | 74 +++++------ protocol/identity/utils.go | 68 ++++++++++ protocol/identity/utils_test.go | 65 +++++++++ services/visual-identity/api.go | 92 ------------- services/visual-identity/service.go | 53 -------- services/visual-identity/utils.go | 114 ---------------- services/visual-identity/utils_test.go | 123 ------------------ 13 files changed, 403 insertions(+), 445 deletions(-) create mode 100644 protocol/identity/colorhash/colorhash.go create mode 100644 protocol/identity/colorhash/colorhash_test.go create mode 100644 protocol/identity/emojihash/emojihash.go rename services/visual-identity/api_test.go => protocol/identity/emojihash/emojihash_test.go (56%) create mode 100644 protocol/identity/utils.go create mode 100644 protocol/identity/utils_test.go delete mode 100644 services/visual-identity/api.go delete mode 100644 services/visual-identity/service.go delete mode 100644 services/visual-identity/utils.go delete mode 100644 services/visual-identity/utils_test.go diff --git a/mobile/status.go b/mobile/status.go index c24500466..026b7c572 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -26,6 +26,8 @@ import ( "github.com/status-im/status-go/profiling" protocol "github.com/status-im/status-go/protocol" "github.com/status-im/status-go/protocol/identity/alias" + "github.com/status-im/status-go/protocol/identity/colorhash" + "github.com/status-im/status-go/protocol/identity/emojihash" "github.com/status-im/status-go/server" "github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/typeddata" @@ -652,6 +654,14 @@ func Identicon(pk string) string { return identicon } +func EmojiHash(pk string) string { + return prepareJSONResponse(emojihash.GenerateFor(pk)) +} + +func ColorHash(pk string) string { + return prepareJSONResponse(colorhash.GenerateFor(pk)) +} + func ValidateMnemonic(mnemonic string) string { m := extkeys.NewMnemonic() err := m.ValidateMnemonic(mnemonic, extkeys.Language(0)) diff --git a/node/get_status_node.go b/node/get_status_node.go index 49a0c7ca6..34b65b846 100644 --- a/node/get_status_node.go +++ b/node/get_status_node.go @@ -45,7 +45,6 @@ import ( "github.com/status-im/status-go/services/status" "github.com/status-im/status-go/services/stickers" "github.com/status-im/status-go/services/subscriptions" - visualIdentity "github.com/status-im/status-go/services/visual-identity" "github.com/status-im/status-go/services/wakuext" "github.com/status-im/status-go/services/wakuv2ext" "github.com/status-im/status-go/services/wallet" @@ -118,7 +117,6 @@ type StatusNode struct { gifSrvc *gif.Service stickersSrvc *stickers.Service chatSrvc *chat.Service - visualIdentitySrvc *visualIdentity.Service } // New makes new instance of StatusNode. @@ -429,7 +427,6 @@ func (n *StatusNode) stop() error { n.wakuV2ExtSrvc = nil n.ensSrvc = nil n.stickersSrvc = nil - n.visualIdentitySrvc = nil n.publicMethods = make(map[string]bool) return nil diff --git a/node/status_node_services.go b/node/status_node_services.go index e70749d7a..ae18e68cc 100644 --- a/node/status_node_services.go +++ b/node/status_node_services.go @@ -38,7 +38,6 @@ import ( "github.com/status-im/status-go/services/status" "github.com/status-im/status-go/services/stickers" "github.com/status-im/status-go/services/subscriptions" - visualIdentity "github.com/status-im/status-go/services/visual-identity" "github.com/status-im/status-go/services/wakuext" "github.com/status-im/status-go/services/wakuv2ext" "github.com/status-im/status-go/services/wallet" @@ -79,12 +78,6 @@ func (b *StatusNode) initServices(config *params.NodeConfig) error { services = append(services, b.gifService()) services = append(services, b.ChatService()) - visualIdentitySrvc, err := b.visualIdentityService() - if err != nil { - return err - } - services = append(services, visualIdentitySrvc) - if config.WakuConfig.Enabled { wakuService, err := b.wakuService(&config.WakuConfig, &config.ClusterConfig) if err != nil { @@ -395,21 +388,6 @@ func (b *StatusNode) gifService() *gif.Service { return b.gifSrvc } -func (b *StatusNode) visualIdentityService() (*visualIdentity.Service, error) { - if b.visualIdentitySrvc != nil { - return b.visualIdentitySrvc, nil - } - - srvc := visualIdentity.NewService() - err := srvc.Init() - if err != nil { - return nil, err - } - b.visualIdentitySrvc = srvc - - return b.visualIdentitySrvc, nil -} - func (b *StatusNode) ChatService() *chat.Service { if b.chatSrvc == nil { b.chatSrvc = chat.NewService(b.appDB) diff --git a/protocol/identity/colorhash/colorhash.go b/protocol/identity/colorhash/colorhash.go new file mode 100644 index 000000000..9b544c661 --- /dev/null +++ b/protocol/identity/colorhash/colorhash.go @@ -0,0 +1,73 @@ +package colorhash + +import ( + "math/big" + + "github.com/status-im/status-go/protocol/identity" +) + +const ( + colorHashSegmentMaxLen = 5 + colorHashColorsCount = 32 +) + +var colorHashAlphabet [][]int + +func GenerateFor(pubkey string) (hash [][]int, err error) { + if len(colorHashAlphabet) == 0 { + colorHashAlphabet = makeColorHashAlphabet(colorHashSegmentMaxLen, colorHashColorsCount) + } + + compressedKey, err := identity.ToCompressedKey(pubkey) + if err != nil { + return nil, err + } + + slices, err := identity.Slices(compressedKey) + if err != nil { + return nil, err + } + + return toColorHash(new(big.Int).SetBytes(slices[2]), &colorHashAlphabet, colorHashColorsCount), nil +} + +// [[1 0] [1 1] [1 2] ... [units, colors-1]] +// [3 12] => 3 units length, 12 color index +func makeColorHashAlphabet(units, colors int) (res [][]int) { + res = make([][]int, units*colors) + idx := 0 + for i := 0; i < units; i++ { + for j := 0; j < colors; j++ { + res[idx] = make([]int, 2) + res[idx][0] = i + 1 + res[idx][1] = j + idx++ + } + } + return +} + +func toColorHash(value *big.Int, alphabet *[][]int, colorsCount int) (hash [][]int) { + alphabetLen := len(*alphabet) + indexes := identity.ToBigBase(value, uint64(alphabetLen)) + hash = make([][](int), len(indexes)) + for i, v := range indexes { + hash[i] = make([](int), 2) + hash[i][0] = (*alphabet)[v][0] + hash[i][1] = (*alphabet)[v][1] + } + + // colors can't repeat themselves + // this makes color hash not fully collision resistant + prevColorIdx := hash[0][1] + hashLen := len(hash) + for i := 1; i < hashLen; i++ { + colorIdx := hash[i][1] + if colorIdx == prevColorIdx { + hash[i][1] = (colorIdx + 1) % colorsCount + } + prevColorIdx = hash[i][1] + } + + return +} diff --git a/protocol/identity/colorhash/colorhash_test.go b/protocol/identity/colorhash/colorhash_test.go new file mode 100644 index 000000000..6dc8406f7 --- /dev/null +++ b/protocol/identity/colorhash/colorhash_test.go @@ -0,0 +1,57 @@ +package colorhash + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/status-im/status-go/protocol/identity" +) + +func TestGenerateFor(t *testing.T) { + checker := func(pubkey string, expected *[][](int)) { + colorhash, err := GenerateFor(pubkey) + require.NoError(t, err) + if !reflect.DeepEqual(colorhash, *expected) { + t.Fatalf("invalid emojihash %v != %v", colorhash, *expected) + } + } + + checker("0x04e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8", + &[][]int{{3, 30}, {2, 10}, {5, 5}, {3, 14}, {5, 4}, {4, 19}, {3, 16}, {4, 0}, {5, 28}, {4, 13}, {4, 15}}) +} + +func TestColorHashOfInvalidKey(t *testing.T) { + checker := func(pubkey string) { + _, err := GenerateFor(pubkey) + require.Error(t, err) + } + checker("abc") + checker("0x01") + checker("0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8") + checker("0x04425da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8") +} + +func TestColorHash(t *testing.T) { + alphabet := makeColorHashAlphabet(4, 4) + + checker := func(valueStr string, expected *[][](int)) { + value := identity.ToBigInt(t, valueStr) + res := toColorHash(value, &alphabet, 4) + if !reflect.DeepEqual(res, *expected) { + t.Fatalf("invalid colorhash conversion %v != %v", res, *expected) + } + } + + checker("0x0", &[][]int{{1, 0}}) + checker("0x1", &[][]int{{1, 1}}) + checker("0x4", &[][]int{{2, 0}}) + checker("0xF", &[][]int{{4, 3}}) + + // oops, collision + checker("0xFF", &[][]int{{4, 3}, {4, 0}}) + checker("0xFC", &[][]int{{4, 3}, {4, 0}}) + + checker("0xFFFF", &[][]int{{4, 3}, {4, 0}, {4, 3}, {4, 0}}) +} diff --git a/protocol/identity/emojihash/emojihash.go b/protocol/identity/emojihash/emojihash.go new file mode 100644 index 000000000..268632f3f --- /dev/null +++ b/protocol/identity/emojihash/emojihash.go @@ -0,0 +1,94 @@ +package emojihash + +import ( + "bufio" + "bytes" + "errors" + "math/big" + "strings" + + "github.com/status-im/status-go/protocol/identity" + "github.com/status-im/status-go/static" +) + +const ( + emojiAlphabetLen = 2757 // 20bytes of data described by 14 emojis requires at least 2757 length alphabet + emojiHashLen = 14 +) + +var emojisAlphabet []string + +func GenerateFor(pubkey string) ([]string, error) { + if len(emojisAlphabet) == 0 { + alphabet, err := loadAlphabet() + if err != nil { + return nil, err + } + emojisAlphabet = *alphabet + } + + compressedKey, err := identity.ToCompressedKey(pubkey) + if err != nil { + return nil, err + } + + slices, err := identity.Slices(compressedKey) + if err != nil { + return nil, err + } + + return toEmojiHash(new(big.Int).SetBytes(slices[1]), emojiHashLen, &emojisAlphabet) +} + +func loadAlphabet() (*[]string, error) { + data, err := static.Asset("emojis.txt") + if err != nil { + return nil, err + } + + alphabet := make([]string, 0, emojiAlphabetLen) + + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + alphabet = append(alphabet, strings.Replace(scanner.Text(), "\n", "", -1)) + } + + // current alphabet contains more emojis than needed, just in case some emojis needs to be removed + // make sure only necessary part is loaded + if len(alphabet) > emojiAlphabetLen { + alphabet = alphabet[:emojiAlphabetLen] + } + + return &alphabet, nil +} + +func toEmojiHash(value *big.Int, hashLen int, alphabet *[]string) (hash []string, err error) { + valueBitLen := value.BitLen() + alphabetLen := new(big.Int).SetInt64(int64(len(*alphabet))) + + indexes := identity.ToBigBase(value, alphabetLen.Uint64()) + if hashLen == 0 { + hashLen = len(indexes) + } else if hashLen > len(indexes) { + prependLen := hashLen - len(indexes) + for i := 0; i < prependLen; i++ { + indexes = append([](uint64){0}, indexes...) + } + } + + // alphabetLen^hashLen + possibleCombinations := new(big.Int).Exp(alphabetLen, new(big.Int).SetInt64(int64(hashLen)), nil) + + // 2^valueBitLen + requiredCombinations := new(big.Int).Exp(new(big.Int).SetInt64(2), new(big.Int).SetInt64(int64(valueBitLen)), nil) + + if possibleCombinations.Cmp(requiredCombinations) == -1 { + return nil, errors.New("alphabet or hash length is too short to encode given value") + } + + for _, v := range indexes { + hash = append(hash, (*alphabet)[v]) + } + + return hash, nil +} diff --git a/services/visual-identity/api_test.go b/protocol/identity/emojihash/emojihash_test.go similarity index 56% rename from services/visual-identity/api_test.go rename to protocol/identity/emojihash/emojihash_test.go index 6bdea2c9c..c9a46f759 100644 --- a/services/visual-identity/api_test.go +++ b/protocol/identity/emojihash/emojihash_test.go @@ -1,27 +1,17 @@ -package visualidentity +package emojihash import ( "reflect" "testing" "github.com/stretchr/testify/require" + + "github.com/status-im/status-go/protocol/identity" ) -func setupTestAPI(t *testing.T) *API { - api := NewAPI() - - alphabet, err := LoadAlphabet() - require.NoError(t, err) - - api.emojisAlphabet = alphabet - return api -} - -func TestEmojiHashOf(t *testing.T) { - api := setupTestAPI(t) - +func TestGenerateFor(t *testing.T) { checker := func(pubkey string, expected *[](string)) { - emojihash, err := api.EmojiHashOf(pubkey) + emojihash, err := GenerateFor(pubkey) require.NoError(t, err) if !reflect.DeepEqual(emojihash, *expected) { t.Fatalf("invalid emojihash %v != %v", emojihash, *expected) @@ -35,34 +25,15 @@ func TestEmojiHashOf(t *testing.T) { &[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀"}) checker("0x04000000000000000000000000000000000000000010000000000000000000000033600332D373318ECC2F212A30A5750D2EAC827B6A32B33D326CCF369B12B1BE", - &[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (*api.emojisAlphabet)[1]}) + &[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (emojisAlphabet)[1]}) checker("0x040000000000000000000000000000000000000000200000000000000000000000353050BFE33B724E60A0C600FBA565A9B62217B1BD35BF9848F2AB847C598B30", - &[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (*api.emojisAlphabet)[2]}) + &[](string){"😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", "😀", (emojisAlphabet)[2]}) } -func TestColorHashOf(t *testing.T) { - api := NewAPI() - - checker := func(pubkey string, expected *[][](int)) { - colorhash, err := api.ColorHashOf(pubkey) - require.NoError(t, err) - if !reflect.DeepEqual(colorhash, *expected) { - t.Fatalf("invalid emojihash %v != %v", colorhash, *expected) - } - } - - checker("0x04e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8", - &[][]int{{3, 30}, {2, 10}, {5, 5}, {3, 14}, {5, 4}, {4, 19}, {3, 16}, {4, 0}, {5, 28}, {4, 13}, {4, 15}}) -} - -func TestHashesOfInvalidKey(t *testing.T) { - api := setupTestAPI(t) - +func TestEmojiHashOfInvalidKey(t *testing.T) { checker := func(pubkey string) { - _, err := api.EmojiHashOf(pubkey) - require.Error(t, err) - _, err = api.ColorHashOf(pubkey) + _, err := GenerateFor(pubkey) require.Error(t, err) } checker("abc") @@ -70,3 +41,30 @@ func TestHashesOfInvalidKey(t *testing.T) { checker("0x01e25da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8") checker("0x04425da6994ea2dc4ac70727e07eca153ae92bf7609db7befb7ebdceaad348f4fc55bbe90abf9501176301db5aa103fc0eb3bc3750272a26c424a10887db2a7ea8") } + +func TestToEmojiHash(t *testing.T) { + alphabet := [](string){"😇", "🤐", "🥵", "🙊", "🤌"} + + checker := func(valueStr string, hashLen int, expected *[](string)) { + value := identity.ToBigInt(t, valueStr) + res, err := toEmojiHash(value, hashLen, &alphabet) + require.NoError(t, err) + if !reflect.DeepEqual(res, *expected) { + t.Fatalf("invalid emojihash conversion %v != %v", res, *expected) + } + } + + checker("777", 5, &[](string){"🤐", "🤐", "🤐", "😇", "🥵"}) + checker("777", 0, &[](string){"🤐", "🤐", "🤐", "😇", "🥵"}) + checker("777", 10, &[](string){"😇", "😇", "😇", "😇", "😇", "🤐", "🤐", "🤐", "😇", "🥵"}) + + // 20bytes of data described by 14 emojis requires at least 2757 length alphabet + alphabet = make([](string), 2757) + val := identity.ToBigInt(t, "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") // 20 bytes + _, err := toEmojiHash(val, 14, &alphabet) + require.NoError(t, err) + + alphabet = make([](string), 2757-1) + _, err = toEmojiHash(val, 14, &alphabet) + require.Error(t, err) +} diff --git a/protocol/identity/utils.go b/protocol/identity/utils.go new file mode 100644 index 000000000..e5090ba26 --- /dev/null +++ b/protocol/identity/utils.go @@ -0,0 +1,68 @@ +package identity + +import ( + "errors" + "fmt" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/crypto/secp256k1" +) + +func ToBigBase(value *big.Int, base uint64) (res [](uint64)) { + toBigBaseImpl(value, base, &res) + return +} + +func toBigBaseImpl(value *big.Int, base uint64, res *[](uint64)) { + bigBase := new(big.Int).SetUint64(base) + quotient := new(big.Int).Div(value, bigBase) + if quotient.Cmp(new(big.Int).SetUint64(0)) != 0 { + toBigBaseImpl(quotient, base, res) + } + + *res = append(*res, new(big.Int).Mod(value, bigBase).Uint64()) +} + +// compressedPubKey = |1.5 bytes chars cutoff|20 bytes emoji hash|10 bytes color hash|1.5 bytes chars cutoff| +func Slices(compressedPubkey []byte) (res [4][]byte, err error) { + if len(compressedPubkey) != 33 { + return res, errors.New("incorrect compressed pubkey") + } + + getSlice := func(low, high int, and string, rsh uint) []byte { + sliceValue := new(big.Int).SetBytes(compressedPubkey[low:high]) + andValue, _ := new(big.Int).SetString(and, 0) + andRes := new(big.Int).And(sliceValue, andValue) + return new(big.Int).Rsh(andRes, rsh).Bytes() + } + + res[0] = getSlice(0, 2, "0xFFF0", 4) + res[1] = getSlice(1, 22, "0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0", 4) + res[2] = getSlice(21, 32, "0x0FFFFFFFFFFFFFFFFFFFF0", 4) + res[3] = getSlice(31, 33, "0x0FFF", 0) + + return res, nil +} + +func ToCompressedKey(pubkey string) ([]byte, error) { + pubkeyValue, ok := new(big.Int).SetString(pubkey, 0) + if !ok { + return nil, fmt.Errorf("invalid pubkey: %s", pubkey) + } + + x, y := secp256k1.S256().Unmarshal(pubkeyValue.Bytes()) + if x == nil || !secp256k1.S256().IsOnCurve(x, y) { + return nil, fmt.Errorf("invalid pubkey: %s", pubkey) + } + + return secp256k1.CompressPubkey(x, y), nil +} + +func ToBigInt(t *testing.T, str string) *big.Int { + res, ok := new(big.Int).SetString(str, 0) + if !ok { + t.Errorf("invalid conversion to int from %s", str) + } + return res +} diff --git a/protocol/identity/utils_test.go b/protocol/identity/utils_test.go new file mode 100644 index 000000000..39539f68c --- /dev/null +++ b/protocol/identity/utils_test.go @@ -0,0 +1,65 @@ +package identity + +import ( + "math" + "math/big" + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestToBigBase(t *testing.T) { + checker := func(value *big.Int, base uint64, expected *[](uint64)) { + res := ToBigBase(value, base) + if !reflect.DeepEqual(res, *expected) { + t.Fatalf("invalid big base conversion %v != %v", res, *expected) + } + } + + lengthChecker := func(value *big.Int, base, expectedLength uint64) { + res := ToBigBase(value, base) + if len(res) != int(expectedLength) { + t.Fatalf("invalid big base conversion %d != %d", len(res), expectedLength) + } + } + + checker(new(big.Int).SetUint64(15), 16, &[](uint64){15}) + checker(new(big.Int).SetUint64(495), 16, &[](uint64){1, 14, 15}) + checker(new(big.Int).SetUint64(495), 30, &[](uint64){16, 15}) + checker(new(big.Int).SetUint64(495), 1024, &[](uint64){495}) + checker(new(big.Int).SetUint64(2048), 1024, &[](uint64){2, 0}) + + base := uint64(math.Pow(2, 7*4)) + checker(ToBigInt(t, "0xFFFFFFFFFFFFFF"), base, &[](uint64){base - 1, base - 1}) + + val := ToBigInt(t, "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + lengthChecker(val, 2757, 14) + lengthChecker(val, 2756, 15) +} + +func TestSlices(t *testing.T) { + checker := func(compressedKey, charsCutoffA, emojiHash, colorHash, charsCutoffB string) { + slices, err := Slices(ToBigInt(t, compressedKey).Bytes()) + require.NoError(t, err) + + sliceChecker := func(idx int, value *big.Int) { + if !reflect.DeepEqual(slices[idx], value.Bytes()) { + t.Fatalf("invalid slice (%d) %v != %v", idx, slices[idx], value.Bytes()) + } + } + + sliceChecker(0, ToBigInt(t, charsCutoffA)) + sliceChecker(1, ToBigInt(t, emojiHash)) + sliceChecker(2, ToBigInt(t, colorHash)) + sliceChecker(3, ToBigInt(t, charsCutoffB)) + } + + checker("0x03086138b210f21d41c757ae8a5d2a4cb29c1350f7389517608378ebd9efcf4a55", "0x030", "0x86138b210f21d41c757ae8a5d2a4cb29c1350f73", "0x89517608378ebd9efcf4", "0xa55") + checker("0x020000000000000000000000000000000000000000100000000000000000000000", "0x020", "0x0000000000000000000000000000000000000001", "0x00000000000000000000", "0x000") +} + +func TestSlicesInvalid(t *testing.T) { + _, err := Slices(ToBigInt(t, "0x01").Bytes()) + require.Error(t, err) +} diff --git a/services/visual-identity/api.go b/services/visual-identity/api.go deleted file mode 100644 index 0554804a6..000000000 --- a/services/visual-identity/api.go +++ /dev/null @@ -1,92 +0,0 @@ -package visualidentity - -import ( - "bufio" - "bytes" - "fmt" - "math/big" - "strings" - - "github.com/ethereum/go-ethereum/crypto/secp256k1" - "github.com/ethereum/go-ethereum/log" - "github.com/status-im/status-go/static" -) - -const ( - emojiAlphabetLen = 2757 // 20bytes of data described by 14 emojis requires at least 2757 length alphabet - emojiHashLen = 14 - colorHashSegmentMaxLen = 5 - colorHashColorsCount = 32 -) - -func NewAPI() *API { - colorHashAlphabet := MakeColorHashAlphabet(colorHashSegmentMaxLen, colorHashColorsCount) - return &API{ - emojisAlphabet: &[]string{}, - colorHashAlphabet: &colorHashAlphabet, - } -} - -type API struct { - emojisAlphabet *[]string - colorHashAlphabet *[][]int -} - -func (api *API) EmojiHashOf(pubkey string) (hash []string, err error) { - log.Info("[VisualIdentityAPI::EmojiHashOf]") - - slices, err := slices(pubkey) - if err != nil { - return nil, err - } - - return ToEmojiHash(new(big.Int).SetBytes(slices[1]), emojiHashLen, api.emojisAlphabet) -} - -func (api *API) ColorHashOf(pubkey string) (hash [][]int, err error) { - log.Info("[VisualIdentityAPI::ColorHashOf]") - - slices, err := slices(pubkey) - if err != nil { - return nil, err - } - - return ToColorHash(new(big.Int).SetBytes(slices[2]), api.colorHashAlphabet, colorHashColorsCount), nil -} - -func LoadAlphabet() (*[]string, error) { - data, err := static.Asset("emojis.txt") - if err != nil { - return nil, err - } - - alphabet := make([]string, 0, emojiAlphabetLen) - - scanner := bufio.NewScanner(bytes.NewReader(data)) - for scanner.Scan() { - alphabet = append(alphabet, strings.Replace(scanner.Text(), "\n", "", -1)) - } - - // current alphabet contains more emojis than needed, just in case some emojis needs to be removed - // make sure only necessary part is loaded - if len(alphabet) > emojiAlphabetLen { - alphabet = alphabet[:emojiAlphabetLen] - } - - return &alphabet, nil -} - -func slices(pubkey string) (res [4][]byte, err error) { - pubkeyValue, ok := new(big.Int).SetString(pubkey, 0) - if !ok { - return res, fmt.Errorf("invalid pubkey: %s", pubkey) - } - - x, y := secp256k1.S256().Unmarshal(pubkeyValue.Bytes()) - if x == nil || !secp256k1.S256().IsOnCurve(x, y) { - return res, fmt.Errorf("invalid pubkey: %s", pubkey) - } - compressedKey := secp256k1.CompressPubkey(x, y) - - return Slices(compressedKey) -} diff --git a/services/visual-identity/service.go b/services/visual-identity/service.go deleted file mode 100644 index d09de572b..000000000 --- a/services/visual-identity/service.go +++ /dev/null @@ -1,53 +0,0 @@ -package visualidentity - -import ( - "github.com/ethereum/go-ethereum/p2p" - "github.com/ethereum/go-ethereum/rpc" -) - -// Service represents out own implementation of Identity Visual Representation. -type Service struct { - api *API -} - -// New returns a new Service. -func NewService() *Service { - return &Service{ - api: NewAPI(), - } -} - -func (s *Service) Init() error { - alphabet, err := LoadAlphabet() - if err == nil { - s.api.emojisAlphabet = alphabet - } - return err -} - -// Protocols returns a new protocols list. In this case, there are none. -func (s *Service) Protocols() []p2p.Protocol { - return []p2p.Protocol{} -} - -// APIs returns a list of new APIs. -func (s *Service) APIs() []rpc.API { - return []rpc.API{ - { - Namespace: "visualIdentity", - Version: "0.1.0", - Service: s.api, - Public: true, - }, - } -} - -// Start is run when a service is started. -func (s *Service) Start() error { - return nil -} - -// Stop is run when a service is stopped. -func (s *Service) Stop() error { - return nil -} diff --git a/services/visual-identity/utils.go b/services/visual-identity/utils.go deleted file mode 100644 index 266befcc5..000000000 --- a/services/visual-identity/utils.go +++ /dev/null @@ -1,114 +0,0 @@ -package visualidentity - -import ( - "errors" - "math/big" -) - -func ToBigBase(value *big.Int, base uint64) (res [](uint64)) { - toBigBaseImpl(value, base, &res) - return -} - -func toBigBaseImpl(value *big.Int, base uint64, res *[](uint64)) { - bigBase := new(big.Int).SetUint64(base) - quotient := new(big.Int).Div(value, bigBase) - if quotient.Cmp(new(big.Int).SetUint64(0)) != 0 { - toBigBaseImpl(quotient, base, res) - } - - *res = append(*res, new(big.Int).Mod(value, bigBase).Uint64()) -} - -func ToEmojiHash(value *big.Int, hashLen int, alphabet *[]string) (hash []string, err error) { - valueBitLen := value.BitLen() - alphabetLen := new(big.Int).SetInt64(int64(len(*alphabet))) - - indexes := ToBigBase(value, alphabetLen.Uint64()) - if hashLen == 0 { - hashLen = len(indexes) - } else if hashLen > len(indexes) { - prependLen := hashLen - len(indexes) - for i := 0; i < prependLen; i++ { - indexes = append([](uint64){0}, indexes...) - } - } - - // alphabetLen^hashLen - possibleCombinations := new(big.Int).Exp(alphabetLen, new(big.Int).SetInt64(int64(hashLen)), nil) - - // 2^valueBitLen - requiredCombinations := new(big.Int).Exp(new(big.Int).SetInt64(2), new(big.Int).SetInt64(int64(valueBitLen)), nil) - - if possibleCombinations.Cmp(requiredCombinations) == -1 { - return nil, errors.New("alphabet or hash length is too short to encode given value") - } - - for _, v := range indexes { - hash = append(hash, (*alphabet)[v]) - } - - return hash, nil -} - -// compressedPubKey = |1.5 bytes chars cutoff|20 bytes emoji hash|10 bytes color hash|1.5 bytes chars cutoff| -func Slices(compressedPubkey []byte) (res [4][]byte, err error) { - if len(compressedPubkey) != 33 { - return res, errors.New("incorrect compressed pubkey") - } - - getSlice := func(low, high int, and string, rsh uint) []byte { - sliceValue := new(big.Int).SetBytes(compressedPubkey[low:high]) - andValue, _ := new(big.Int).SetString(and, 0) - andRes := new(big.Int).And(sliceValue, andValue) - return new(big.Int).Rsh(andRes, rsh).Bytes() - } - - res[0] = getSlice(0, 2, "0xFFF0", 4) - res[1] = getSlice(1, 22, "0x0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0", 4) - res[2] = getSlice(21, 32, "0x0FFFFFFFFFFFFFFFFFFFF0", 4) - res[3] = getSlice(31, 33, "0x0FFF", 0) - - return res, nil -} - -// [[1 0] [1 1] [1 2] ... [units, colors-1]] -// [3 12] => 3 units length, 12 color index -func MakeColorHashAlphabet(units, colors int) (res [][]int) { - res = make([][]int, units*colors) - idx := 0 - for i := 0; i < units; i++ { - for j := 0; j < colors; j++ { - res[idx] = make([]int, 2) - res[idx][0] = i + 1 - res[idx][1] = j - idx++ - } - } - return -} - -func ToColorHash(value *big.Int, alphabet *[][]int, colorsCount int) (hash [][]int) { - alphabetLen := len(*alphabet) - indexes := ToBigBase(value, uint64(alphabetLen)) - hash = make([][](int), len(indexes)) - for i, v := range indexes { - hash[i] = make([](int), 2) - hash[i][0] = (*alphabet)[v][0] - hash[i][1] = (*alphabet)[v][1] - } - - // colors can't repeat themselves - // this makes color hash not fully collision resistant - prevColorIdx := hash[0][1] - hashLen := len(hash) - for i := 1; i < hashLen; i++ { - colorIdx := hash[i][1] - if colorIdx == prevColorIdx { - hash[i][1] = (colorIdx + 1) % colorsCount - } - prevColorIdx = hash[i][1] - } - - return -} diff --git a/services/visual-identity/utils_test.go b/services/visual-identity/utils_test.go deleted file mode 100644 index 5688936cd..000000000 --- a/services/visual-identity/utils_test.go +++ /dev/null @@ -1,123 +0,0 @@ -package visualidentity - -import ( - "math" - "math/big" - "reflect" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestToBigBase(t *testing.T) { - checker := func(value *big.Int, base uint64, expected *[](uint64)) { - res := ToBigBase(value, base) - if !reflect.DeepEqual(res, *expected) { - t.Fatalf("invalid big base conversion %v != %v", res, *expected) - } - } - - lengthChecker := func(value *big.Int, base, expectedLength uint64) { - res := ToBigBase(value, base) - if len(res) != int(expectedLength) { - t.Fatalf("invalid big base conversion %d != %d", len(res), expectedLength) - } - } - - checker(new(big.Int).SetUint64(15), 16, &[](uint64){15}) - checker(new(big.Int).SetUint64(495), 16, &[](uint64){1, 14, 15}) - checker(new(big.Int).SetUint64(495), 30, &[](uint64){16, 15}) - checker(new(big.Int).SetUint64(495), 1024, &[](uint64){495}) - checker(new(big.Int).SetUint64(2048), 1024, &[](uint64){2, 0}) - - base := uint64(math.Pow(2, 7*4)) - checker(toBigInt(t, "0xFFFFFFFFFFFFFF"), base, &[](uint64){base - 1, base - 1}) - - val := toBigInt(t, "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") - lengthChecker(val, 2757, 14) - lengthChecker(val, 2756, 15) -} - -func TestToEmojiHash(t *testing.T) { - alphabet := [](string){"😇", "🤐", "🥵", "🙊", "🤌"} - - checker := func(valueStr string, hashLen int, expected *[](string)) { - value := toBigInt(t, valueStr) - res, err := ToEmojiHash(value, hashLen, &alphabet) - require.NoError(t, err) - if !reflect.DeepEqual(res, *expected) { - t.Fatalf("invalid emojihash conversion %v != %v", res, *expected) - } - } - - checker("777", 5, &[](string){"🤐", "🤐", "🤐", "😇", "🥵"}) - checker("777", 0, &[](string){"🤐", "🤐", "🤐", "😇", "🥵"}) - checker("777", 10, &[](string){"😇", "😇", "😇", "😇", "😇", "🤐", "🤐", "🤐", "😇", "🥵"}) - - // 20bytes of data described by 14 emojis requires at least 2757 length alphabet - alphabet = make([](string), 2757) - val := toBigInt(t, "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") // 20 bytes - _, err := ToEmojiHash(val, 14, &alphabet) - require.NoError(t, err) - - alphabet = make([](string), 2757-1) - _, err = ToEmojiHash(val, 14, &alphabet) - require.Error(t, err) -} - -func TestSlices(t *testing.T) { - checker := func(compressedKey, charsCutoffA, emojiHash, colorHash, charsCutoffB string) { - slices, err := Slices(toBigInt(t, compressedKey).Bytes()) - require.NoError(t, err) - - sliceChecker := func(idx int, value *big.Int) { - if !reflect.DeepEqual(slices[idx], value.Bytes()) { - t.Fatalf("invalid slice (%d) %v != %v", idx, slices[idx], value.Bytes()) - } - } - - sliceChecker(0, toBigInt(t, charsCutoffA)) - sliceChecker(1, toBigInt(t, emojiHash)) - sliceChecker(2, toBigInt(t, colorHash)) - sliceChecker(3, toBigInt(t, charsCutoffB)) - } - - checker("0x03086138b210f21d41c757ae8a5d2a4cb29c1350f7389517608378ebd9efcf4a55", "0x030", "0x86138b210f21d41c757ae8a5d2a4cb29c1350f73", "0x89517608378ebd9efcf4", "0xa55") - checker("0x020000000000000000000000000000000000000000100000000000000000000000", "0x020", "0x0000000000000000000000000000000000000001", "0x00000000000000000000", "0x000") -} - -func TestSlicesInvalid(t *testing.T) { - _, err := Slices(toBigInt(t, "0x01").Bytes()) - require.Error(t, err) -} - -func TestColorHash(t *testing.T) { - alphabet := MakeColorHashAlphabet(4, 4) - - checker := func(valueStr string, expected *[][](int)) { - value := toBigInt(t, valueStr) - res := ToColorHash(value, &alphabet, 4) - if !reflect.DeepEqual(res, *expected) { - t.Fatalf("invalid colorhash conversion %v != %v", res, *expected) - } - } - - checker("0x0", &[][]int{{1, 0}}) - checker("0x1", &[][]int{{1, 1}}) - checker("0x4", &[][]int{{2, 0}}) - checker("0xF", &[][]int{{4, 3}}) - - // oops, collision - checker("0xFF", &[][]int{{4, 3}, {4, 0}}) - checker("0xFC", &[][]int{{4, 3}, {4, 0}}) - - checker("0xFFFF", &[][]int{{4, 3}, {4, 0}, {4, 3}, {4, 0}}) -} - -func toBigInt(t *testing.T, str string) *big.Int { - res, ok := new(big.Int).SetString(str, 0) - if !ok { - t.Errorf("invalid conversion to int from %s", str) - } - return res -}