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}