1package dag
2
3import (
4 "encoding/json"
5 "fmt"
6 "strconv"
7 "strings"
8
9 "github.com/ProtonMail/go-crypto/openpgp"
10 "github.com/ProtonMail/go-crypto/openpgp/packet"
11 "github.com/pkg/errors"
12
13 "github.com/MichaelMure/git-bug/entity"
14 "github.com/MichaelMure/git-bug/identity"
15 "github.com/MichaelMure/git-bug/repository"
16 "github.com/MichaelMure/git-bug/util/lamport"
17)
18
19const opsEntryName = "ops"
20const extraEntryName = "extra"
21const versionEntryPrefix = "version-"
22const createClockEntryPrefix = "create-clock-"
23const editClockEntryPrefix = "edit-clock-"
24
25// operationPack is a wrapper structure to store multiple operations in a single git blob.
26// Additionally, it holds and store the metadata for those operations.
27type operationPack struct {
28 // An identifier, taken from a hash of the serialized Operations.
29 id entity.Id
30
31 // The author of the Operations. Must be the same author for all the Operations.
32 Author identity.Interface
33 // The list of Operation stored in the operationPack
34 Operations []Operation
35 // Encode the entity's logical time of creation across all entities of the same type.
36 // Only exist on the root operationPack
37 CreateTime lamport.Time
38 // Encode the entity's logical time of last edition across all entities of the same type.
39 // Exist on all operationPack
40 EditTime lamport.Time
41}
42
43func (opp *operationPack) Id() entity.Id {
44 if opp.id == "" || opp.id == entity.UnsetId {
45 // This means we are trying to get the opp's Id *before* it has been stored.
46 // As the Id is computed based on the actual bytes written on the disk, we are going to predict
47 // those and then get the Id. This is safe as it will be the exact same code writing on disk later.
48
49 data, err := json.Marshal(opp)
50 if err != nil {
51 panic(err)
52 }
53 opp.id = entity.DeriveId(data)
54 }
55
56 return opp.id
57}
58
59func (opp *operationPack) MarshalJSON() ([]byte, error) {
60 return json.Marshal(struct {
61 Author identity.Interface `json:"author"`
62 Operations []Operation `json:"ops"`
63 }{
64 Author: opp.Author,
65 Operations: opp.Operations,
66 })
67}
68
69func (opp *operationPack) Validate() error {
70 if opp.Author == nil {
71 return fmt.Errorf("missing author")
72 }
73 for _, op := range opp.Operations {
74 if op.Author().Id() != opp.Author.Id() {
75 return fmt.Errorf("operation has different author than the operationPack's")
76 }
77 }
78 if opp.EditTime == 0 {
79 return fmt.Errorf("lamport edit time is zero")
80 }
81 return nil
82}
83
84// Write writes the OperationPack in git, with zero, one or more parent commits.
85// If the repository has a keypair able to sign (that is, with a private key), the resulting commit is signed with that key.
86// Return the hash of the created commit.
87func (opp *operationPack) Write(def Definition, repo repository.Repo, parentCommit ...repository.Hash) (repository.Hash, error) {
88 if err := opp.Validate(); err != nil {
89 return "", err
90 }
91
92 // For different reason, we store the clocks and format version directly in the git tree.
93 // Version has to be accessible before any attempt to decode to return early with a unique error.
94 // Clocks could possibly be stored in the git blob but it's nice to separate data and metadata, and
95 // we are storing something directly in the tree already so why not.
96 //
97 // To have a valid Tree, we point the "fake" entries to always the same value, the empty blob.
98 emptyBlobHash, err := repo.StoreData([]byte{})
99 if err != nil {
100 return "", err
101 }
102
103 // Write the Ops as a Git blob containing the serialized array of operations
104 data, err := json.Marshal(opp)
105 if err != nil {
106 return "", err
107 }
108
109 // compute the Id while we have the serialized data
110 opp.id = entity.DeriveId(data)
111
112 hash, err := repo.StoreData(data)
113 if err != nil {
114 return "", err
115 }
116
117 // Make a Git tree referencing this blob and encoding the other values:
118 // - format version
119 // - clocks
120 // - extra data
121 tree := []repository.TreeEntry{
122 {ObjectType: repository.Blob, Hash: emptyBlobHash,
123 Name: fmt.Sprintf(versionEntryPrefix+"%d", def.FormatVersion)},
124 {ObjectType: repository.Blob, Hash: hash,
125 Name: opsEntryName},
126 {ObjectType: repository.Blob, Hash: emptyBlobHash,
127 Name: fmt.Sprintf(editClockEntryPrefix+"%d", opp.EditTime)},
128 }
129 if opp.CreateTime > 0 {
130 tree = append(tree, repository.TreeEntry{
131 ObjectType: repository.Blob,
132 Hash: emptyBlobHash,
133 Name: fmt.Sprintf(createClockEntryPrefix+"%d", opp.CreateTime),
134 })
135 }
136 if extraTree := opp.makeExtraTree(); len(extraTree) > 0 {
137 extraTreeHash, err := repo.StoreTree(extraTree)
138 if err != nil {
139 return "", err
140 }
141 tree = append(tree, repository.TreeEntry{
142 ObjectType: repository.Tree,
143 Hash: extraTreeHash,
144 Name: extraEntryName,
145 })
146 }
147
148 // Store the tree
149 treeHash, err := repo.StoreTree(tree)
150 if err != nil {
151 return "", err
152 }
153
154 // Write a Git commit referencing the tree, with the previous commit as parent
155 // If we have keys, sign.
156 var commitHash repository.Hash
157
158 // Sign the commit if we have a key
159 signingKey, err := opp.Author.SigningKey(repo)
160 if err != nil {
161 return "", err
162 }
163
164 if signingKey != nil {
165 commitHash, err = repo.StoreSignedCommit(treeHash, signingKey.PGPEntity(), parentCommit...)
166 } else {
167 commitHash, err = repo.StoreCommit(treeHash, parentCommit...)
168 }
169
170 if err != nil {
171 return "", err
172 }
173
174 return commitHash, nil
175}
176
177func (opp *operationPack) makeExtraTree() []repository.TreeEntry {
178 var tree []repository.TreeEntry
179 counter := 0
180 added := make(map[repository.Hash]interface{})
181
182 for _, ops := range opp.Operations {
183 ops, ok := ops.(OperationWithFiles)
184 if !ok {
185 continue
186 }
187
188 for _, file := range ops.GetFiles() {
189 if _, has := added[file]; !has {
190 tree = append(tree, repository.TreeEntry{
191 ObjectType: repository.Blob,
192 Hash: file,
193 // The name is not important here, we only need to
194 // reference the blob.
195 Name: fmt.Sprintf("file%d", counter),
196 })
197 counter++
198 added[file] = struct{}{}
199 }
200 }
201 }
202
203 return tree
204}
205
206// readOperationPack read the operationPack encoded in git at the given Tree hash.
207//
208// Validity of the Lamport clocks is left for the caller to decide.
209func readOperationPack(def Definition, repo repository.RepoData, resolver identity.Resolver, commit repository.Commit) (*operationPack, error) {
210 entries, err := repo.ReadTree(commit.TreeHash)
211 if err != nil {
212 return nil, err
213 }
214
215 // check the format version first, fail early instead of trying to read something
216 var version uint
217 for _, entry := range entries {
218 if strings.HasPrefix(entry.Name, versionEntryPrefix) {
219 v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, versionEntryPrefix), 10, 64)
220 if err != nil {
221 return nil, errors.Wrap(err, "can't read format version")
222 }
223 if v > 1<<12 {
224 return nil, fmt.Errorf("format version too big")
225 }
226 version = uint(v)
227 break
228 }
229 }
230 if version == 0 {
231 return nil, entity.NewErrUnknownFormat(def.FormatVersion)
232 }
233 if version != def.FormatVersion {
234 return nil, entity.NewErrInvalidFormat(version, def.FormatVersion)
235 }
236
237 var id entity.Id
238 var author identity.Interface
239 var ops []Operation
240 var createTime lamport.Time
241 var editTime lamport.Time
242
243 for _, entry := range entries {
244 switch {
245 case entry.Name == opsEntryName:
246 data, err := repo.ReadData(entry.Hash)
247 if err != nil {
248 return nil, errors.Wrap(err, "failed to read git blob data")
249 }
250 ops, author, err = unmarshallPack(def, resolver, data)
251 if err != nil {
252 return nil, err
253 }
254 id = entity.DeriveId(data)
255
256 case strings.HasPrefix(entry.Name, createClockEntryPrefix):
257 v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, createClockEntryPrefix), 10, 64)
258 if err != nil {
259 return nil, errors.Wrap(err, "can't read creation lamport time")
260 }
261 createTime = lamport.Time(v)
262
263 case strings.HasPrefix(entry.Name, editClockEntryPrefix):
264 v, err := strconv.ParseUint(strings.TrimPrefix(entry.Name, editClockEntryPrefix), 10, 64)
265 if err != nil {
266 return nil, errors.Wrap(err, "can't read edit lamport time")
267 }
268 editTime = lamport.Time(v)
269 }
270 }
271
272 // Verify signature if we expect one
273 keys := author.ValidKeysAtTime(fmt.Sprintf(editClockPattern, def.Namespace), editTime)
274 if len(keys) > 0 {
275 keyring := PGPKeyring(keys)
276 _, err = openpgp.CheckDetachedSignature(keyring, commit.SignedData, commit.Signature, nil)
277 if err != nil {
278 return nil, fmt.Errorf("signature failure: %v", err)
279 }
280 }
281
282 return &operationPack{
283 id: id,
284 Author: author,
285 Operations: ops,
286 CreateTime: createTime,
287 EditTime: editTime,
288 }, nil
289}
290
291// unmarshallPack delegate the unmarshalling of the Operation's JSON to the decoding
292// function provided by the concrete entity. This gives access to the concrete type of each
293// Operation.
294func unmarshallPack(def Definition, resolver identity.Resolver, data []byte) ([]Operation, identity.Interface, error) {
295 aux := struct {
296 Author identity.IdentityStub `json:"author"`
297 Operations []json.RawMessage `json:"ops"`
298 }{}
299
300 if err := json.Unmarshal(data, &aux); err != nil {
301 return nil, nil, err
302 }
303
304 if aux.Author.Id() == "" || aux.Author.Id() == entity.UnsetId {
305 return nil, nil, fmt.Errorf("missing author")
306 }
307
308 author, err := resolver.ResolveIdentity(aux.Author.Id())
309 if err != nil {
310 return nil, nil, err
311 }
312
313 ops := make([]Operation, 0, len(aux.Operations))
314
315 for _, raw := range aux.Operations {
316 // delegate to specialized unmarshal function
317 op, err := def.OperationUnmarshaler(raw, resolver)
318 if err != nil {
319 return nil, nil, err
320 }
321 // Set the id from the serialized data
322 op.setId(entity.DeriveId(raw))
323 // Set the author, taken from the OperationPack
324 op.setAuthor(author)
325
326 ops = append(ops, op)
327 }
328
329 return ops, author, nil
330}
331
332var _ openpgp.KeyRing = &PGPKeyring{}
333
334// PGPKeyring implement a openpgp.KeyRing from an slice of Key
335type PGPKeyring []*identity.Key
336
337func (pk PGPKeyring) KeysById(id uint64) []openpgp.Key {
338 var result []openpgp.Key
339 for _, key := range pk {
340 if key.Public().KeyId == id {
341 result = append(result, openpgp.Key{
342 PublicKey: key.Public(),
343 PrivateKey: key.Private(),
344 SelfSignature: &packet.Signature{
345 IsPrimaryId: func() *bool { b := true; return &b }(),
346 },
347 })
348 }
349 }
350 return result
351}
352
353func (pk PGPKeyring) KeysByIdUsage(id uint64, requiredUsage byte) []openpgp.Key {
354 // the only usage we care about is the ability to sign, which all keys should already be capable of
355 return pk.KeysById(id)
356}
357
358func (pk PGPKeyring) DecryptionKeys() []openpgp.Key {
359 // result := make([]openpgp.Key, len(pk))
360 // for i, key := range pk {
361 // result[i] = openpgp.Key{
362 // PublicKey: key.Public(),
363 // PrivateKey: key.Private(),
364 // }
365 // }
366 // return result
367 panic("not implemented")
368}