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