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