-
Notifications
You must be signed in to change notification settings - Fork 30
/
encoder.go
167 lines (139 loc) · 4.5 KB
/
encoder.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
package gokeepasslib
import (
"bytes"
"compress/gzip"
"encoding/base64"
"encoding/xml"
"io"
)
// Header to be put before xml content in kdbx file
var xmlHeader = []byte(`<?xml version="1.0" encoding="utf-8" standalone="yes"?>` + "\n")
// Encoder is used to automaticaly encrypt and write a database to a file, network, etc
type Encoder struct {
w io.Writer
}
// NewEncoder creates a new encoder with writer w, identical to gokeepasslib.Encoder{w}
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{w: w}
}
// Encode writes db to e's internal writer
func (e *Encoder) Encode(db *Database) error {
db.cleanupBinaries()
// Unlock protected entries ensuring that we have them prepared in the order that is matching
// the xml unmarshalling order
err := db.UnlockProtectedEntries()
if err != nil {
return err
}
// Re-Lock the protected values mapping to ensure that they are locked in memory and
// follow the order in which they would be written again
err = db.LockProtectedEntries()
if err != nil {
return err
}
// ensure timestamps will be formatted correctly
db.ensureKdbxFormatVersion()
// Calculate transformed key to make HMAC and encrypt
transformedKey, err := db.getTransformedKey()
if err != nil {
return err
}
// Write header then hashes before decode content (necessary to update HeaderHash)
// db.Header writeTo will change its hash
if err = db.Header.writeTo(e.w); err != nil {
return err
}
// Update header hash into db.Hashes then write the data
hash := db.Header.GetSha256()
if db.Header.IsKdbx4() {
db.Hashes.Sha256 = hash
hmacKey := buildHmacKey(db, transformedKey)
hmacHash := db.Header.GetHmacSha256(hmacKey)
db.Hashes.Hmac = hmacHash
if err = db.Hashes.writeTo(e.w); err != nil {
return err
}
// Comment as taken from the original KDBX source:
// > The header hash is typically only stored in
// > KDBX <= 3.1 files, not in KDBX >= 4 files
// > (here, the header is verified via a HMAC),
// > but we also support it for KDBX >= 4 files
// > (i.e. if it's present, we check it)
// That means that for KDBXv4 files we can unset the hash to make sure that it is blank.
// Additionally it has to happen somewhere before `xml.MarshalIndent` and this is
// the perfect spot just before it specific to KDBXv4 files.
db.Content.Meta.HeaderHash = ""
} else {
db.Content.Meta.HeaderHash = base64.StdEncoding.EncodeToString(hash[:])
}
// Encode xml and append header to the top
rawContent, err := xml.MarshalIndent(db.Content, "", "\t")
if err != nil {
return err
}
rawContent = append(xmlHeader, rawContent...)
// Write InnerHeader (Kdbx v4)
if db.Header.IsKdbx4() {
var ih bytes.Buffer
if err = db.Content.InnerHeader.writeTo(&ih); err != nil {
return err
}
rawContent = append(ih.Bytes(), rawContent...)
}
// Encode raw content
encodedContent, err := encodeRawContent(db, rawContent, transformedKey)
if err != nil {
return err
}
// Writes the encrypted database content
_, err = e.w.Write(encodedContent)
return err
}
func encodeRawContent(
db *Database,
content []byte,
transformedKey []byte,
) ([]byte, error) {
// Compress if the header compression flag is 1 (gzip)
if db.Header.FileHeaders.CompressionFlags == GzipCompressionFlag {
b := new(bytes.Buffer)
w := gzip.NewWriter(b)
if _, err := w.Write(content); err != nil {
return nil, err
}
// Close() needs to be explicitly called to write Gzip stream footer,
// Flush() is not enough. some gzip decoders treat missing footer as error
// while some don't). internally Close() also does flush.
if err := w.Close(); err != nil {
return nil, err
}
content = b.Bytes()
}
// Compose blocks (Kdbx v3.1)
if !db.Header.IsKdbx4() {
var blocks bytes.Buffer
composeContentBlocks31(&blocks, content)
// Append blocks to StreamStartBytes
content = append(db.Header.FileHeaders.StreamStartBytes, blocks.Bytes()...)
}
// Always add padding, so that decoders that check for the last bytes can work correctly
padding := make([]byte, 16-(len(content)%16))
for i := 0; i < len(padding); i++ {
padding[i] = byte(len(padding))
}
content = append(content, padding...)
// Encrypt content
// Decrypt content
encrypter, err := db.GetEncrypterManager(transformedKey)
if err != nil {
return nil, err
}
encrypted := encrypter.Encrypt(content)
// Compose blocks (Kdbx v4)
if db.Header.IsKdbx4() {
var blocks bytes.Buffer
composeContentBlocks4(&blocks, encrypted, db.Header.FileHeaders.MasterSeed, transformedKey)
encrypted = blocks.Bytes()
}
return encrypted, nil
}