From 5ae8a132772385c903a62de2ceec02a97f108a01 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 8 Nov 2020 19:13:55 +0100 Subject: identity: Id from data, not git + hold multiple lamport clocks --- identity/version.go | 169 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 113 insertions(+), 56 deletions(-) (limited to 'identity/version.go') diff --git a/identity/version.go b/identity/version.go index bbf93575..bbf0a3f5 100644 --- a/identity/version.go +++ b/identity/version.go @@ -2,9 +2,11 @@ package identity import ( "crypto/rand" + "crypto/sha256" "encoding/json" "fmt" "strings" + "time" "github.com/pkg/errors" @@ -15,76 +17,133 @@ import ( ) // 1: original format -const formatVersion = 1 - -// Version is a complete set of information about an Identity at a point in time. -type Version struct { - // The lamport time at which this version become effective - // The reference time is the bug edition lamport clock - // It must be the first field in this struct due to https://github.com/golang/go/issues/599 - // - // TODO: BREAKING CHANGE - this need to actually be one edition lamport time **per entity** - // This is not a problem right now but will be when more entities are added (pull-request, config ...) - time lamport.Time - unixTime int64 +// 2: Identity Ids are generated from the first version serialized data instead of from the first git commit +const formatVersion = 2 + +// TODO ^^ +// version is a complete set of information about an Identity at a point in time. +type version struct { name string email string // as defined in git or from a bridge when importing the identity login string // from a bridge when importing the identity avatarURL string + // The lamport times of the other entities at which this version become effective + times map[string]lamport.Time + unixTime int64 + // The set of keys valid at that time, from this version onward, until they get removed // in a new version. This allow to have multiple key for the same identity (e.g. one per // device) as well as revoke key. keys []*Key - // This optional array is here to ensure a better randomness of the identity id to avoid collisions. + // mandatory random bytes to ensure a better randomness of the data of the first + // version of a bug, used to later generate the ID + // len(Nonce) should be > 20 and < 64 bytes // It has no functional purpose and should be ignored. - // It is advised to fill this array if there is not enough entropy, e.g. if there is no keys. + // TODO: optional? nonce []byte // A set of arbitrary key/value to store metadata about a version or about an Identity in general. metadata map[string]string + // Not serialized. Store the version's id in memory. + id entity.Id // Not serialized commitHash repository.Hash } -type VersionJSON struct { +func newVersion(repo repository.RepoClock, name string, email string, login string, avatarURL string, keys []*Key) (*version, error) { + clocks, err := repo.AllClocks() + if err != nil { + return nil, err + } + + times := make(map[string]lamport.Time) + for name, clock := range clocks { + times[name] = clock.Time() + } + + return &version{ + id: entity.UnsetId, + name: name, + email: email, + login: login, + avatarURL: avatarURL, + times: times, + unixTime: time.Now().Unix(), + keys: keys, + nonce: makeNonce(20), + }, nil +} + +type versionJSON struct { // Additional field to version the data FormatVersion uint `json:"version"` - Time lamport.Time `json:"time"` - UnixTime int64 `json:"unix_time"` - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` - Login string `json:"login,omitempty"` - AvatarUrl string `json:"avatar_url,omitempty"` - Keys []*Key `json:"pub_keys,omitempty"` - Nonce []byte `json:"nonce,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` + Times map[string]lamport.Time `json:"times"` + UnixTime int64 `json:"unix_time"` + Name string `json:"name,omitempty"` + Email string `json:"email,omitempty"` + Login string `json:"login,omitempty"` + AvatarUrl string `json:"avatar_url,omitempty"` + Keys []*Key `json:"pub_keys,omitempty"` + Nonce []byte `json:"nonce"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// Id return the identifier of the version +func (v *version) Id() entity.Id { + if v.id == "" { + // something went really wrong + panic("version's id not set") + } + if v.id == entity.UnsetId { + // This means we are trying to get the version's Id *before* it has been stored. + // As the Id is computed based on the actual bytes written on the disk, we are going to predict + // those and then get the Id. This is safe as it will be the exact same code writing on disk later. + data, err := json.Marshal(v) + if err != nil { + panic(err) + } + v.id = deriveId(data) + } + return v.id +} + +func deriveId(data []byte) entity.Id { + sum := sha256.Sum256(data) + return entity.Id(fmt.Sprintf("%x", sum)) } // Make a deep copy -func (v *Version) Clone() *Version { - clone := &Version{ - name: v.name, - email: v.email, - avatarURL: v.avatarURL, - keys: make([]*Key, len(v.keys)), +func (v *version) Clone() *version { + // copy direct fields + clone := *v + + clone.times = make(map[string]lamport.Time) + for name, t := range v.times { + clone.times[name] = t } + clone.keys = make([]*Key, len(v.keys)) for i, key := range v.keys { clone.keys[i] = key.Clone() } - return clone + clone.nonce = make([]byte, len(v.nonce)) + copy(clone.nonce, v.nonce) + + // not copying metadata + + return &clone } -func (v *Version) MarshalJSON() ([]byte, error) { - return json.Marshal(VersionJSON{ +func (v *version) MarshalJSON() ([]byte, error) { + return json.Marshal(versionJSON{ FormatVersion: formatVersion, - Time: v.time, + Times: v.times, UnixTime: v.unixTime, Name: v.name, Email: v.email, @@ -96,8 +155,8 @@ func (v *Version) MarshalJSON() ([]byte, error) { }) } -func (v *Version) UnmarshalJSON(data []byte) error { - var aux VersionJSON +func (v *version) UnmarshalJSON(data []byte) error { + var aux versionJSON if err := json.Unmarshal(data, &aux); err != nil { return err @@ -110,7 +169,8 @@ func (v *Version) UnmarshalJSON(data []byte) error { return entity.NewErrNewFormatVersion(aux.FormatVersion) } - v.time = aux.Time + v.id = deriveId(data) + v.times = aux.Times v.unixTime = aux.UnixTime v.name = aux.Name v.email = aux.Email @@ -123,23 +183,18 @@ func (v *Version) UnmarshalJSON(data []byte) error { return nil } -func (v *Version) Validate() error { +func (v *version) Validate() error { // time must be set after a commit if v.commitHash != "" && v.unixTime == 0 { return fmt.Errorf("unix time not set") } - if v.commitHash != "" && v.time == 0 { - return fmt.Errorf("lamport time not set") - } if text.Empty(v.name) && text.Empty(v.login) { return fmt.Errorf("either name or login should be set") } - if strings.Contains(v.name, "\n") { return fmt.Errorf("name should be a single line") } - if !text.Safe(v.name) { return fmt.Errorf("name is not fully printable") } @@ -147,7 +202,6 @@ func (v *Version) Validate() error { if strings.Contains(v.login, "\n") { return fmt.Errorf("login should be a single line") } - if !text.Safe(v.login) { return fmt.Errorf("login is not fully printable") } @@ -155,7 +209,6 @@ func (v *Version) Validate() error { if strings.Contains(v.email, "\n") { return fmt.Errorf("email should be a single line") } - if !text.Safe(v.email) { return fmt.Errorf("email is not fully printable") } @@ -167,6 +220,9 @@ func (v *Version) Validate() error { if len(v.nonce) > 64 { return fmt.Errorf("nonce is too big") } + if len(v.nonce) < 20 { + return fmt.Errorf("nonce is too small") + } for _, k := range v.keys { if err := k.Validate(); err != nil { @@ -177,9 +233,9 @@ func (v *Version) Validate() error { return nil } -// Write will serialize and store the Version as a git blob and return +// Write will serialize and store the version as a git blob and return // its hash -func (v *Version) Write(repo repository.Repo) (repository.Hash, error) { +func (v *version) Write(repo repository.Repo) (repository.Hash, error) { // make sure we don't write invalid data err := v.Validate() if err != nil { @@ -187,17 +243,18 @@ func (v *Version) Write(repo repository.Repo) (repository.Hash, error) { } data, err := json.Marshal(v) - if err != nil { return "", err } hash, err := repo.StoreData(data) - if err != nil { return "", err } + // make sure we set the Id when writing in the repo + v.id = deriveId(data) + return hash, nil } @@ -211,22 +268,22 @@ func makeNonce(len int) []byte { } // SetMetadata store arbitrary metadata about a version or an Identity in general -// If the Version has been commit to git already, it won't be overwritten. -func (v *Version) SetMetadata(key string, value string) { +// If the version has been commit to git already, it won't be overwritten. +// Beware: changing the metadata on a version will change it's ID +func (v *version) SetMetadata(key string, value string) { if v.metadata == nil { v.metadata = make(map[string]string) } - v.metadata[key] = value } -// GetMetadata retrieve arbitrary metadata about the Version -func (v *Version) GetMetadata(key string) (string, bool) { +// GetMetadata retrieve arbitrary metadata about the version +func (v *version) GetMetadata(key string) (string, bool) { val, ok := v.metadata[key] return val, ok } -// AllMetadata return all metadata for this Version -func (v *Version) AllMetadata() map[string]string { +// AllMetadata return all metadata for this version +func (v *version) AllMetadata() map[string]string { return v.metadata } -- cgit From b01aa18d3925a23ba0ad32a322617de7dc9a299e Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 8 Nov 2020 23:56:32 +0100 Subject: identity: PR fixes --- identity/version.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'identity/version.go') diff --git a/identity/version.go b/identity/version.go index bbf0a3f5..ae2474bf 100644 --- a/identity/version.go +++ b/identity/version.go @@ -18,10 +18,9 @@ import ( // 1: original format // 2: Identity Ids are generated from the first version serialized data instead of from the first git commit +// + Identity hold multiple lamport clocks from other entities, instead of just bug edit const formatVersion = 2 -// TODO ^^ - // version is a complete set of information about an Identity at a point in time. type version struct { name string @@ -42,7 +41,7 @@ type version struct { // version of a bug, used to later generate the ID // len(Nonce) should be > 20 and < 64 bytes // It has no functional purpose and should be ignored. - // TODO: optional? + // TODO: optional after first version? nonce []byte // A set of arbitrary key/value to store metadata about a version or about an Identity in general. @@ -122,6 +121,10 @@ func (v *version) Clone() *version { // copy direct fields clone := *v + // reset some fields + clone.commitHash = "" + clone.id = entity.UnsetId + clone.times = make(map[string]lamport.Time) for name, t := range v.times { clone.times[name] = t -- cgit From 2bf2b2d765c5003307544885b9321b32cc09d8bb Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Mon, 9 Nov 2020 00:34:48 +0100 Subject: entity: unique function to generate IDs --- identity/version.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) (limited to 'identity/version.go') diff --git a/identity/version.go b/identity/version.go index ae2474bf..ce5cc7d6 100644 --- a/identity/version.go +++ b/identity/version.go @@ -2,7 +2,6 @@ package identity import ( "crypto/rand" - "crypto/sha256" "encoding/json" "fmt" "strings" @@ -106,16 +105,11 @@ func (v *version) Id() entity.Id { if err != nil { panic(err) } - v.id = deriveId(data) + v.id = entity.DeriveId(data) } return v.id } -func deriveId(data []byte) entity.Id { - sum := sha256.Sum256(data) - return entity.Id(fmt.Sprintf("%x", sum)) -} - // Make a deep copy func (v *version) Clone() *version { // copy direct fields @@ -172,7 +166,7 @@ func (v *version) UnmarshalJSON(data []byte) error { return entity.NewErrNewFormatVersion(aux.FormatVersion) } - v.id = deriveId(data) + v.id = entity.DeriveId(data) v.times = aux.Times v.unixTime = aux.UnixTime v.name = aux.Name @@ -256,7 +250,7 @@ func (v *version) Write(repo repository.Repo) (repository.Hash, error) { } // make sure we set the Id when writing in the repo - v.id = deriveId(data) + v.id = entity.DeriveId(data) return hash, nil } -- cgit From 1ced77af1a4bdbaa212a74bf0c56b2b81cdc5bd2 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 14 Feb 2021 12:24:40 +0100 Subject: fix merge --- identity/version.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) (limited to 'identity/version.go') diff --git a/identity/version.go b/identity/version.go index ce5cc7d6..cbd56a98 100644 --- a/identity/version.go +++ b/identity/version.go @@ -159,11 +159,8 @@ func (v *version) UnmarshalJSON(data []byte) error { return err } - if aux.FormatVersion < formatVersion { - return entity.NewErrOldFormatVersion(aux.FormatVersion) - } - if aux.FormatVersion > formatVersion { - return entity.NewErrNewFormatVersion(aux.FormatVersion) + if aux.FormatVersion != formatVersion { + return entity.NewErrInvalidFormat(aux.FormatVersion, formatVersion) } v.id = entity.DeriveId(data) -- cgit From f1d4a19af81fcc05ae9d90e018ff141f6521335a Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 14 Mar 2021 18:39:04 +0100 Subject: bug: nonce on all operation to prevent id collision --- identity/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'identity/version.go') diff --git a/identity/version.go b/identity/version.go index cbd56a98..1c35831e 100644 --- a/identity/version.go +++ b/identity/version.go @@ -37,7 +37,7 @@ type version struct { keys []*Key // mandatory random bytes to ensure a better randomness of the data of the first - // version of a bug, used to later generate the ID + // version of an identity, used to later generate the ID // len(Nonce) should be > 20 and < 64 bytes // It has no functional purpose and should be ignored. // TODO: optional after first version? -- cgit