oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1package docs
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8
9 "github.com/google/uuid"
10 "gorm.io/gorm"
11)
12
13var errContentState = errors.New("docs: invalid content sealing state")
14var contentKeyCheck = []byte("oficina/docs/content-key-check/v1")
15
16type contentSealing struct {
17 ID int
18 Encoding int
19 KeyFingerprint string
20 KeyCheck []byte
21 Scrubbed bool
22}
23
24func (contentSealing) TableName() string { return "content_sealing" }
25
26func readContentSealing(ctx context.Context, g *gorm.DB, c *contentCipher) (*contentSealing, error) {
27 var state contentSealing
28 err := g.WithContext(ctx).First(&state, 1).Error
29 if errors.Is(err, gorm.ErrRecordNotFound) {
30 var count int64
31 if err := g.WithContext(ctx).Raw("SELECT (SELECT COUNT(*) FROM doc_updates WHERE content_encoding != 0) + (SELECT COUNT(*) FROM doc_snapshots WHERE content_encoding != 0)").Scan(&count).Error; err != nil {
32 return nil, err
33 }
34 if count != 0 {
35 return nil, errContentState
36 }
37 return nil, nil
38 }
39 if err != nil {
40 return nil, err
41 }
42 if c == nil || state.KeyFingerprint != c.fingerprint {
43 return nil, errContentKeyMismatch
44 }
45 if state.Encoding != contentEncoding {
46 return nil, errContentState
47 }
48 plain, err := c.open(contentKeyCheckKind, uuid.Nil.String(), 0, uint32(state.Encoding), state.KeyCheck)
49 if err != nil || !bytes.Equal(plain, contentKeyCheck) {
50 return nil, errContentAuthentication
51 }
52 return &state, nil
53}
54
55func convertContent(ctx context.Context, g *gorm.DB, c *contentCipher) error {
56 if c == nil {
57 return errContentKey
58 }
59 return g.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
60 if err := tx.Exec("PRAGMA secure_delete=ON").Error; err != nil {
61 return err
62 }
63 state, err := readContentSealing(ctx, tx, c)
64 if err != nil {
65 return err
66 }
67 var hadLegacy bool
68 if state != nil {
69 if err := tx.Raw("SELECT EXISTS(SELECT 1 FROM doc_updates WHERE content_encoding = 0 LIMIT 1) OR EXISTS(SELECT 1 FROM doc_snapshots WHERE content_encoding = 0 LIMIT 1)").Scan(&hadLegacy).Error; err != nil {
70 return err
71 }
72 }
73 for _, snapshot := range []bool{false, true} {
74 if err := convertContentRows(ctx, tx, c, snapshot, state != nil); err != nil {
75 return err
76 }
77 }
78 if state != nil {
79 if hadLegacy {
80 return tx.Model(&contentSealing{}).Where("id = 1").Update("scrubbed", false).Error
81 }
82 return nil
83 }
84 check, err := c.seal(contentKeyCheckKind, uuid.Nil.String(), 0, contentEncoding, contentKeyCheck)
85 if err != nil {
86 return err
87 }
88 return tx.Create(&contentSealing{ID: 1, Encoding: contentEncoding, KeyFingerprint: c.fingerprint, KeyCheck: check}).Error
89 })
90}
91
92type contentMigrationRow struct {
93 ID int64
94 DocID int64
95 Seq int64
96 ABIVersion uint32
97 ContentEncoding int
98 Body []byte
99 GUID string
100}
101
102func convertContentRows(ctx context.Context, tx *gorm.DB, c *contentCipher, snapshot, active bool) error {
103 table, column, abi, kind := "doc_updates", "update_bin", "0", contentUpdateKind
104 if snapshot {
105 table, column, abi, kind = "doc_snapshots", "state", "r.abi_version", contentSnapshotKind
106 }
107 last := int64(0)
108 first := true
109 for {
110 var rows []contentMigrationRow
111 query := "SELECT r.id, r.doc_id, r.seq, " + abi + " AS abi_version, r.content_encoding, r." + column + " AS body, d.guid FROM " + table + " r LEFT JOIN documents d ON d.id=r.doc_id WHERE (? OR r.id > ?) ORDER BY r.id LIMIT 100"
112 if err := tx.WithContext(ctx).Raw(query, first, last).Scan(&rows).Error; err != nil {
113 return err
114 }
115 if len(rows) == 0 {
116 return nil
117 }
118 first = false
119 for _, row := range rows {
120 if err := ctx.Err(); err != nil {
121 return err
122 }
123 last = row.ID
124 if active && row.ContentEncoding == contentEncoding {
125 if _, err := c.open(kind, row.GUID, row.Seq, row.ABIVersion, row.Body); err != nil {
126 return err
127 }
128 continue
129 }
130 if row.ContentEncoding != 0 {
131 return errContentEncoding
132 }
133 sealed, err := c.seal(kind, row.GUID, row.Seq, row.ABIVersion, row.Body)
134 if err != nil {
135 return err
136 }
137 result := tx.Table(table).Where("id = ? AND content_encoding = 0", row.ID).
138 Updates(map[string]any{column: sealed, "content_encoding": contentEncoding})
139 if result.Error != nil {
140 return result.Error
141 }
142 if result.RowsAffected != 1 {
143 return fmt.Errorf("%w: content row %d", errContentState, row.ID)
144 }
145 }
146 }
147}
148