1package identity
2
3import (
4 "crypto/rand"
5 "encoding/json"
6 "fmt"
7 "time"
8
9 "github.com/pkg/errors"
10
11 bootstrap "github.com/MichaelMure/git-bug/entity/boostrap"
12 "github.com/MichaelMure/git-bug/repository"
13 "github.com/MichaelMure/git-bug/util/lamport"
14 "github.com/MichaelMure/git-bug/util/text"
15)
16
17// 1: original format
18// 2: Identity Ids are generated from the first version serialized data instead of from the first git
19// commit + Identity hold multiple lamport clocks from other entities, instead of just bug edit
20const formatVersion = 2
21
22// version is a complete set of information about an Identity at a point in time.
23type version struct {
24 name string
25 email string // as defined in git or from a bridge when importing the identity
26 login string // from a bridge when importing the identity
27 avatarURL string
28
29 // The lamport times of the other entities at which this version become effective
30 times map[string]lamport.Time
31 unixTime int64
32
33 // The set of keys valid at that time, from this version onward, until they get removed
34 // in a new version. This allows to have multiple key for the same identity (e.g. one per
35 // device) as well as revoke key.
36 keys []bootstrap.Key
37
38 // mandatory random bytes to ensure a better randomness of the data of the first
39 // version of an identity, used to later generate the ID
40 // len(Nonce) should be > 20 and < 64 bytes
41 // It has no functional purpose and should be ignored.
42 // TODO: optional after first version?
43 nonce []byte
44
45 // A set of arbitrary key/value to store metadata about a version or about an Identity in general.
46 metadata map[string]string
47
48 // Not serialized. Store the version's id in memory.
49 id bootstrap.Id
50 // Not serialized
51 commitHash repository.Hash
52}
53
54func newVersion(repo repository.RepoClock, name string, email string, login string, avatarURL string, keys []bootstrap.Key) (*version, error) {
55 clocks, err := repo.AllClocks()
56 if err != nil {
57 return nil, err
58 }
59
60 times := make(map[string]lamport.Time)
61 for name, clock := range clocks {
62 times[name] = clock.Time()
63 }
64
65 return &version{
66 id: bootstrap.UnsetId,
67 name: name,
68 email: email,
69 login: login,
70 avatarURL: avatarURL,
71 times: times,
72 unixTime: time.Now().Unix(),
73 keys: keys,
74 nonce: makeNonce(20),
75 }, nil
76}
77
78type versionJSON struct {
79 // Additional field to version the data
80 FormatVersion uint `json:"version"`
81
82 Times map[string]lamport.Time `json:"times"`
83 UnixTime int64 `json:"unix_time"`
84 Name string `json:"name,omitempty"`
85 Email string `json:"email,omitempty"`
86 Login string `json:"login,omitempty"`
87 AvatarUrl string `json:"avatar_url,omitempty"`
88 Keys []*Key `json:"pub_keys,omitempty"`
89 Nonce []byte `json:"nonce"`
90 Metadata map[string]string `json:"metadata,omitempty"`
91}
92
93// Id return the identifier of the version
94func (v *version) Id() bootstrap.Id {
95 if v.id == "" {
96 // something went really wrong
97 panic("version's id not set")
98 }
99 if v.id == bootstrap.UnsetId {
100 // This means we are trying to get the version's Id *before* it has been stored.
101 // As the Id is computed based on the actual bytes written on the disk, we are going to predict
102 // those and then get the Id. This is safe as it will be the exact same code writing on disk later.
103 data, err := json.Marshal(v)
104 if err != nil {
105 panic(err)
106 }
107 v.id = bootstrap.DeriveId(data)
108 }
109 return v.id
110}
111
112// Make a deep copy
113func (v *version) Clone() *version {
114 // copy direct fields
115 clone := *v
116
117 // reset some fields
118 clone.commitHash = ""
119 clone.id = bootstrap.UnsetId
120
121 clone.times = make(map[string]lamport.Time)
122 for name, t := range v.times {
123 clone.times[name] = t
124 }
125
126 clone.keys = make([]bootstrap.Key, len(v.keys))
127 for i, key := range v.keys {
128 clone.keys[i] = key.Clone()
129 }
130
131 clone.nonce = make([]byte, len(v.nonce))
132 copy(clone.nonce, v.nonce)
133
134 // not copying metadata
135
136 return &clone
137}
138
139func (v *version) MarshalJSON() ([]byte, error) {
140 return json.Marshal(versionJSON{
141 FormatVersion: formatVersion,
142 Times: v.times,
143 UnixTime: v.unixTime,
144 Name: v.name,
145 Email: v.email,
146 Login: v.login,
147 AvatarUrl: v.avatarURL,
148 Keys: v.keys,
149 Nonce: v.nonce,
150 Metadata: v.metadata,
151 })
152}
153
154func (v *version) UnmarshalJSON(data []byte) error {
155 var aux versionJSON
156
157 if err := json.Unmarshal(data, &aux); err != nil {
158 return err
159 }
160
161 if aux.FormatVersion != formatVersion {
162 return bootstrap.NewErrInvalidFormat(aux.FormatVersion, formatVersion)
163 }
164
165 v.id = bootstrap.DeriveId(data)
166 v.times = aux.Times
167 v.unixTime = aux.UnixTime
168 v.name = aux.Name
169 v.email = aux.Email
170 v.login = aux.Login
171 v.avatarURL = aux.AvatarUrl
172 v.keys = aux.Keys
173 v.nonce = aux.Nonce
174 v.metadata = aux.Metadata
175
176 return nil
177}
178
179func (v *version) Validate() error {
180 // time must be set after a commit
181 if v.commitHash != "" && v.unixTime == 0 {
182 return fmt.Errorf("unix time not set")
183 }
184
185 if text.Empty(v.name) && text.Empty(v.login) {
186 return fmt.Errorf("either name or login should be set")
187 }
188 if !text.SafeOneLine(v.name) {
189 return fmt.Errorf("name has unsafe characters")
190 }
191
192 if !text.SafeOneLine(v.login) {
193 return fmt.Errorf("login has unsafe characters")
194 }
195
196 if !text.SafeOneLine(v.email) {
197 return fmt.Errorf("email has unsafe characters")
198 }
199
200 if v.avatarURL != "" && !text.ValidUrl(v.avatarURL) {
201 return fmt.Errorf("avatarUrl is not a valid URL")
202 }
203
204 if len(v.nonce) > 64 {
205 return fmt.Errorf("nonce is too big")
206 }
207 if len(v.nonce) < 20 {
208 return fmt.Errorf("nonce is too small")
209 }
210
211 for _, k := range v.keys {
212 if err := k.Validate(); err != nil {
213 return errors.Wrap(err, "invalid key")
214 }
215 }
216
217 return nil
218}
219
220// Write will serialize and store the version as a git blob and return
221// its hash
222func (v *version) Write(repo repository.Repo) (repository.Hash, error) {
223 // make sure we don't write invalid data
224 err := v.Validate()
225 if err != nil {
226 return "", errors.Wrap(err, "validation error")
227 }
228
229 data, err := json.Marshal(v)
230 if err != nil {
231 return "", err
232 }
233
234 hash, err := repo.StoreData(data)
235 if err != nil {
236 return "", err
237 }
238
239 // make sure we set the Id when writing in the repo
240 v.id = bootstrap.DeriveId(data)
241
242 return hash, nil
243}
244
245func makeNonce(len int) []byte {
246 result := make([]byte, len)
247 _, err := rand.Read(result)
248 if err != nil {
249 panic(err)
250 }
251 return result
252}
253
254// SetMetadata store arbitrary metadata about a version or an Identity in general
255// If the version has been commit to git already, it won't be overwritten.
256// Beware: changing the metadata on a version will change it's ID
257func (v *version) SetMetadata(key string, value string) {
258 if v.metadata == nil {
259 v.metadata = make(map[string]string)
260 }
261 v.metadata[key] = value
262}
263
264// GetMetadata retrieve arbitrary metadata about the version
265func (v *version) GetMetadata(key string) (string, bool) {
266 val, ok := v.metadata[key]
267 return val, ok
268}
269
270// AllMetadata return all metadata for this version
271func (v *version) AllMetadata() map[string]string {
272 return v.metadata
273}