operation_pack.go

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