diff --git a/interfaces/contact/contact.go b/interfaces/contact/contact.go index c060a74a167e5d6377077ee38ccc5b49284c40c1..7dd6322c39971968d9731374e96792b36dd40677 100644 --- a/interfaces/contact/contact.go +++ b/interfaces/contact/contact.go @@ -8,15 +8,15 @@ package contact import ( - "encoding/json" + "bytes" + "encoding/binary" "github.com/pkg/errors" "gitlab.com/elixxir/crypto/cyclic" "gitlab.com/elixxir/primitives/fact" "gitlab.com/xx_network/primitives/id" ) -const factDelimiter = "," -const factBreak = ";" +const sizeByteLength = 2 // Contact implements the Contact interface defined in interface/contact.go, // in go, the structure is meant to be edited directly, the functions are for @@ -28,21 +28,103 @@ type Contact struct { Facts fact.FactList } +// Marshal saves the Contact in a compact byte slice. The byte slice has +// the following structure (not to scale). +// +// +----------+----------------+---------+----------+----------+----------------+----------+ +// | DhPubKey | OwnershipProof | Facts | ID | | | | +// | size | size | size | | DhPubKey | OwnershipProof | FactList | +// | 2 bytes | 2 bytes | 2 bytes | 33 bytes | | | | +// +----------+----------------+---------+----------+----------+----------------+----------+ func (c Contact) Marshal() ([]byte, error) { - return json.Marshal(&c) + var buff bytes.Buffer + b := make([]byte, sizeByteLength) + + // Write size of DhPubKey + var dhPubKey []byte + var err error + if c.DhPubKey != nil { + dhPubKey, err = c.DhPubKey.GobEncode() + if err != nil { + return nil, errors.Errorf("Failed to gob encode DhPubKey: %+v", err) + } + } + binary.PutVarint(b, int64(len(dhPubKey))) + buff.Write(b) + + // Write size of OwnershipProof + binary.PutVarint(b, int64(len(c.OwnershipProof))) + buff.Write(b) + + // Write length of Facts + factList := c.Facts.Stringify() + binary.PutVarint(b, int64(len(factList))) + buff.Write(b) + + // Write ID + if c.ID != nil { + buff.Write(c.ID.Marshal()) + } else { + emptyID := make([]byte, id.ArrIDLen) + buff.Write(emptyID) + } + + // Write DhPubKey + buff.Write(dhPubKey) + + // Write OwnershipProof + buff.Write(c.OwnershipProof) + + // Write fact list + buff.Write([]byte(factList)) + + return buff.Bytes(), nil } +// Unmarshal decodes the byte slice produced by Contact.Marshal into a Contact. func Unmarshal(b []byte) (Contact, error) { - c := Contact{} - err := json.Unmarshal(b, &c) + c := Contact{DhPubKey: &cyclic.Int{}} + var err error + buf := bytes.NewBuffer(b) + + // Get size (in bytes) of each field + dhPubKeySize, _ := binary.Varint(buf.Next(sizeByteLength)) + ownershipProofSize, _ := binary.Varint(buf.Next(sizeByteLength)) + factsSize, _ := binary.Varint(buf.Next(sizeByteLength)) + + // Get and unmarshal ID + c.ID, err = id.Unmarshal(buf.Next(id.ArrIDLen)) if err != nil { - return c, err + return c, errors.Errorf("Failed to unmarshal Contact ID: %+v", err) + } + + // Handle nil ID + if bytes.Equal(c.ID.Marshal(), make([]byte, id.ArrIDLen)) { + c.ID = nil } - for i, fact := range c.Facts { - if !fact.T.IsValid() { - return Contact{}, errors.Errorf("Fact %v/%v has invalid "+ - "type: %s", i, len(c.Facts), fact.T) + + // Get and decode DhPubKey + if dhPubKeySize == 0 { + // Handle nil key + c.DhPubKey = nil + } else { + err = c.DhPubKey.GobDecode(buf.Next(int(dhPubKeySize))) + if err != nil { + return c, errors.Errorf("Failed to gob decode Contact DhPubKey: %+v", err) } } + + // Get OwnershipProof + c.OwnershipProof = buf.Next(int(ownershipProofSize)) + if len(c.OwnershipProof) == 0 { + c.OwnershipProof = nil + } + + // Get and unstringify fact list + c.Facts, _, err = fact.UnstringifyFactList(string(buf.Next(int(factsSize)))) + if err != nil { + return c, errors.Errorf("Failed to unstringify Fact List: %+v", err) + } + return c, nil } diff --git a/interfaces/contact/contact_test.go b/interfaces/contact/contact_test.go new file mode 100644 index 0000000000000000000000000000000000000000..eb365acf79b9251419fd4fe8a042c31843234620 --- /dev/null +++ b/interfaces/contact/contact_test.go @@ -0,0 +1,113 @@ +package contact + +import ( + "encoding/json" + "gitlab.com/elixxir/crypto/cyclic" + "gitlab.com/elixxir/primitives/fact" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/crypto/large" + "gitlab.com/xx_network/primitives/id" + "math" + "math/rand" + "reflect" + "testing" +) + +// Tests marshaling and unmarshalling of a common contact. +func TestContact_Marshal_Unmarshal(t *testing.T) { + expectedContact := Contact{ + ID: id.NewIdFromUInt(rand.Uint64(), id.User, t), + DhPubKey: getCycInt(256), + Facts: fact.FactList{ + {Fact: "myUsername", T: fact.Username}, + {Fact: "devinputvalidation@elixxir.io", T: fact.Email}, + {Fact: "6502530000US", T: fact.Phone}, + {Fact: "6502530001US", T: fact.Phone}, + }, + } + + buff, err := expectedContact.Marshal() + if err != nil { + t.Errorf("Marshal() produced an error: %+v", err) + } + + testContact, err := Unmarshal(buff) + if err != nil { + t.Errorf("Unmarshal() produced an error: %+v", err) + } + + if !reflect.DeepEqual(expectedContact, testContact) { + t.Errorf("Unmarshaled Contact does not match expected."+ + "\nexpected: %#v\nreceived: %#v", expectedContact, testContact) + t.Errorf("DhPubKey."+ + "\nexpected: %+v\nreceived: %+v", expectedContact.DhPubKey.TextVerbose(10, math.MaxInt64), testContact.DhPubKey.TextVerbose(10, math.MaxInt64)) + } +} + +// Tests the size of marshaling and JSON marshaling of a Contact with a large +// amount of data. +func TestContact_Marshal_Size(t *testing.T) { + expectedContact := Contact{ + ID: id.NewIdFromUInt(rand.Uint64(), id.User, t), + DhPubKey: getCycInt(512), + OwnershipProof: make([]byte, 1024), + Facts: fact.FactList{ + {Fact: "myVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongUsername", T: fact.Username}, + {Fact: "myVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongEmail@elixxir.io", T: fact.Email}, + {Fact: "6502530000US", T: fact.Phone}, + }, + } + rand.Read(expectedContact.OwnershipProof) + + buff, err := expectedContact.Marshal() + if err != nil { + t.Errorf("Marshal() produced an error: %+v", err) + } + + marshalBuff, err := json.Marshal(expectedContact) + if err != nil { + t.Errorf("Marshal() produced an error: %+v", err) + } + + t.Logf("size of buff: %d", len(buff)) + t.Logf("size of marshalBuff: %d", len(marshalBuff)) + t.Logf("ratio: %.2f%%", float32(len(buff))/float32(len(marshalBuff))*100) + t.Logf("%s", marshalBuff) + + if len(marshalBuff) < len(buff) { + t.Errorf("JSON Contact smaller than marshaled contact."+ + "\nJSON: %d\nmarshal: %d", len(marshalBuff), len(buff)) + } +} + +func getCycInt(size int) *cyclic.Int { + var primeString = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" + + "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" + + "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" + + "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" + + "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D" + + "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F" + + "83655D23DCA3AD961C62F356208552BB9ED529077096966D" + + "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B" + + "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9" + + "DE2BCBF6955817183995497CEA956AE515D2261898FA0510" + + "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64" + + "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7" + + "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B" + + "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C" + + "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31" + + "43DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D7" + + "88719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA" + + "2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6" + + "287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED" + + "1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA9" + + "93B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199" + + "FFFFFFFFFFFFFFFF" + buff, err := csprng.GenerateInGroup([]byte(primeString), size, csprng.NewSystemRNG()) + if err != nil { + panic(err) + } + + grp := cyclic.NewGroup(large.NewIntFromString(primeString, 16), large.NewInt(2)).NewIntFromBytes(buff) + return grp +}