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