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/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}