/
storage.go
244 lines (224 loc) · 6.92 KB
/
storage.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
package core
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
bolt "go.etcd.io/bbolt"
"io"
"os"
)
// Header contains all information stored in the header of a fsverify partition.
type Header struct {
MagicNumber int
Signature string
FilesystemSize int
FilesystemUnit int
TableSize int
TableUnit int
}
// Node contains all information stored in a database node.
// If the Node is the first node in the database, PrevNodeSum should be set to Entrypoint.
type Node struct {
BlockStart int
BlockEnd int
BlockSum string
PrevNodeSum string
}
// GetHash returns the hash of all fields of a Node combined.
// The Node fields are combined in the order BlockStart, BlockEnd, BlockSum and PrevNodeSum
func (n *Node) GetHash() (string, error) {
return calculateStringHash(fmt.Sprintf("%d%d%s%s", n.BlockStart, n.BlockEnd, n.BlockSum, n.PrevNodeSum))
}
// parseUnitSpec parses the file size unit specified in the header and returns it as an according multiplier.
// In the case of an invalid Unit byte the function returns -1.
func parseUnitSpec(size []byte) int {
switch size[0] {
case 0:
return 1
case 1:
return 1000
case 2:
return 1000 * 1000
case 3:
return 1000 * 1000 * 10000
case 4:
return 100000000000000
case 5:
return 1000000000000000
default:
return -1
}
}
// ReadHeader reads the partition header and puts it in a variable of type Header.
// If any field fails to be read, the function returns an empty Header struct and the error.
func ReadHeader(partition string) (Header, error) {
_, exist := os.Stat(partition)
if os.IsNotExist(exist) {
return Header{}, fmt.Errorf("Cannot find partition %s", partition)
}
part, err := os.Open(partition)
if err != nil {
return Header{}, err
}
defer part.Close()
header := Header{}
reader := bufio.NewReader(part)
// Since the size of each field is already known
// it is best to hard code them, in the case
// that a field goes over its allocated size
// fsverify should (and will) fail
MagicNumber := make([]byte, 2)
UntrustedHash := make([]byte, 100)
TrustedHash := make([]byte, 88)
FilesystemSize := make([]byte, 4)
FilesystemUnit := make([]byte, 1)
TableSize := make([]byte, 4)
TableUnit := make([]byte, 1)
_, err = reader.Read(MagicNumber)
MagicNum := binary.BigEndian.Uint16(MagicNumber)
if MagicNum != 0xACAB { // The Silliest of magic numbers
return Header{}, err
}
header.MagicNumber = int(MagicNum)
_, err = reader.Read(UntrustedHash)
if err != nil {
return Header{}, err
}
_, err = reader.Read(TrustedHash)
if err != nil {
return Header{}, err
}
_, err = reader.Read(FilesystemSize)
if err != nil {
return Header{}, err
}
_, err = reader.Read(FilesystemUnit)
if err != nil {
return Header{}, err
}
_, err = reader.Read(TableSize)
if err != nil {
return Header{}, err
}
_, err = reader.Read(TableUnit)
if err != nil {
return Header{}, err
}
header.Signature = fmt.Sprintf("untrusted comment: fsverify\n%s\ntrusted comment: fsverify\n%s\n", string(UntrustedHash), string(TrustedHash))
header.FilesystemSize = int(binary.BigEndian.Uint32(FilesystemSize))
header.TableSize = int(binary.BigEndian.Uint32(TableSize))
header.FilesystemUnit = parseUnitSpec(FilesystemUnit)
header.TableUnit = parseUnitSpec(TableUnit)
if header.FilesystemUnit == -1 || header.TableUnit == -1 {
return Header{}, fmt.Errorf("unit size for Filesystem or Table invalid: fs: %x, table: %x", FilesystemUnit, TableUnit)
}
return header, nil
}
// ReadDB reads the database from a fsverify partition.
// It verifies the the size of the database with the size specified in the partition header and returns an error if the sizes do not match.
// Due to limitations with bbolt the database gets written to a temporary path and the function returns the path to the database.
func ReadDB(partition string) (string, error) {
_, exist := os.Stat(partition)
if os.IsNotExist(exist) {
return "", fmt.Errorf("Cannot find partition %s", partition)
}
part, err := os.Open(partition)
if err != nil {
return "", err
}
defer part.Close()
reader := bufio.NewReader(part)
// The area taken up by the header
// it is useless for this reader instance
// and will be skipped completely
_, err = reader.Read(make([]byte, 200))
if err != nil {
fmt.Println(err)
return "", err
}
header, err := ReadHeader(partition)
if err != nil {
fmt.Println(err)
return "", err
}
// Reading the specified table size allows for tamper protection
// in the case that the partition was tampered with "lazily"
// meaning that only the database was modified, and not the header
// if that is the case, the database would be lacking data, making it unusable
db := make([]byte, header.TableSize*header.TableUnit)
n, err := io.ReadFull(reader, db)
if err != nil {
return "", err
}
if n != header.TableSize*header.TableUnit {
return "", fmt.Errorf("Database is not expected size. Expected %d, got %d", header.TableSize*header.TableUnit, n)
}
fmt.Printf("db: %d\n", n)
// Write the database to a temporary directory
// to ensure that it disappears after the next reboot
temp, err := os.MkdirTemp("", "*-fsverify")
if err != nil {
return "", err
}
// The file permission is immediately set to 0700
// this ensures that the database is not modified
// after it has been written
err = os.WriteFile(temp+"/verify.db", db, 0700)
if err != nil {
return "", err
}
return temp + "/verify.db", nil
}
// OpenDB opens a bbolt database and returns a bbolt instance.
func OpenDB(dbpath string, readonly bool) (*bolt.DB, error) {
_, exist := os.Stat(dbpath)
if os.IsNotExist(exist) {
os.Create(dbpath)
}
db, err := bolt.Open(dbpath, 0777, &bolt.Options{ReadOnly: readonly})
if err != nil {
return nil, err
}
return db, nil
}
// GetNode retrieves a Node from the database based on the hash identifier.
// If db is set to nil, the function will open the database in read-only mode itself.
func GetNode(checksum string, db *bolt.DB) (Node, error) {
var err error
var deferDB bool
if db == nil {
db, err = OpenDB("my.db", true)
if err != nil {
return Node{}, err
}
deferDB = true
}
var node Node
err = db.View(func(tx *bolt.Tx) error {
nodes := tx.Bucket([]byte("Nodes"))
app := nodes.Get([]byte(checksum))
err := json.Unmarshal(app, &node)
return err
})
if deferDB {
defer db.Close()
}
return node, err
}
// CopyByteArea copies an area of bytes from a reader.
// It verifies that the reader reads the wanted amount of bytes, and returns an error if this is not the case.
func CopyByteArea(start int, end int, reader *bytes.Reader) ([]byte, error) {
if end-start < 0 {
return []byte{}, fmt.Errorf("tried creating byte slice with negative length. %d to %d total %d\n", start, end, end-start)
}
bytes := make([]byte, end-start)
n, err := reader.ReadAt(bytes, int64(start))
if err != nil {
return nil, err
} else if n != end-start {
return nil, fmt.Errorf("Unable to read requested size. Expected %d, got %d", end-start, n)
}
return bytes, nil
}