operation_pack.go

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