-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsynocrypto_encrypter.go
More file actions
209 lines (176 loc) · 5.95 KB
/
synocrypto_encrypter.go
File metadata and controls
209 lines (176 loc) · 5.95 KB
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
package synocrypto
import (
"crypto/md5"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"hash"
"io"
"math/big"
"path"
"sync"
"github.com/maxlaverse/synocrypto/pkg/compression"
"github.com/maxlaverse/synocrypto/pkg/crypto"
"github.com/maxlaverse/synocrypto/pkg/encoding"
"github.com/maxlaverse/synocrypto/pkg/log"
)
type encrypter struct {
options EncrypterOptions
}
func (e *encrypter) Encrypt(in io.Reader, out io.Writer) error {
if e.options.DisableIntegrityCheck {
return e.encrypt(in, out)
}
// Run an integrity check by verifying that at least we
// can decrypt what we encrypted, in case CloudSync can't.
dec := NewDecrypter(DecrypterOptions{
PrivateKey: e.options.PrivateKey,
Password: e.options.Password,
IgnoreChecksumMismatch: false,
})
var wg sync.WaitGroup
var decryptionErr error
pr, pw := io.Pipe()
wg.Add(1)
go func() {
decryptionErr = dec.Decrypt(pr, io.Discard)
wg.Done()
}()
err := e.encrypt(in, io.MultiWriter(out, pw))
pw.Close()
if err != nil {
return err
}
wg.Wait()
if decryptionErr != nil {
return fmt.Errorf("integrity check failed: %w", decryptionErr)
}
return err
}
func (e *encrypter) encrypt(in io.Reader, out io.Writer) error {
encryptionSalt, err := crypto.RandomSalt(8)
if err != nil {
return fmt.Errorf("unable to generate random salt: %w", err)
}
sessionKey, err := randomSessionKey()
if err != nil {
return fmt.Errorf("unable to generate random session key: %w", err)
}
sessionKeyHex := bytesToHex(sessionKey)
metadata := map[string]interface{}{}
if !e.options.DisableCompression {
metadata[encoding.MetadataFieldCompress] = 1
}
metadata[encoding.MetadataFieldEncrypt] = 1
metadata[encoding.MetadataFieldSalt] = encryptionSalt
metadata[encoding.MetadataFieldVersion] = map[string]interface{}{"major": 3, "minor": 1}
saltedHash, err := crypto.RandomSaltedHash(string(sessionKeyHex))
if err != nil {
return fmt.Errorf("error generating random salt for session key hash: %w", err)
}
metadata[encoding.MetadataFieldSessionKeyHash] = saltedHash
if len(e.options.Password) > 0 {
passwordEncryptedSessionKey, passwordEncryptedSessionKeyHash, err := encryptedSessionKeyByPassword(sessionKeyHex, e.options.Password, encryptionSalt)
if err != nil {
return fmt.Errorf("error generating password encrypted key: %w", err)
}
metadata[encoding.MetadataFieldEncryptionKey1] = passwordEncryptedSessionKey
metadata[encoding.MetadataFieldEncryptionKey1Hash] = passwordEncryptedSessionKeyHash
}
if len(e.options.PrivateKey) > 0 {
privateKeyEncryptedSessionKey, privateKeyEncryptedSessionKeyHash, err := encryptedSessionKeyByPrivateKey(sessionKeyHex, e.options.PrivateKey)
if err != nil {
return fmt.Errorf("error generating private key encrypted key: %w", err)
}
metadata[encoding.MetadataFieldEncryptionKey2] = privateKeyEncryptedSessionKey
metadata[encoding.MetadataFieldEncryptionKey2Hash] = privateKeyEncryptedSessionKeyHash
}
if len(e.options.Password) == 0 || len(e.options.PrivateKey) == 0 {
log.Warning("Both password and private key are required in order for CloudSync to decrypt the data")
}
if e.options.Filename != "" {
metadata[encoding.MetadataFieldFilename] = path.Base(e.options.Filename)
}
var hasher hash.Hash
if !e.options.DisableDigestGeneration {
metadata[encoding.MetadataFieldDigest] = "md5"
hasher = md5.New()
in = io.TeeReader(in, hasher)
}
objWriter := encoding.NewWriter(out)
err = objWriter.WriteMetadata(metadata)
if err != nil {
return fmt.Errorf("error writing metadata: %w", err)
}
cryptoOut := crypto.NewEncrypterWithPasswordAndSalt(sessionKey, []byte{}, objWriter)
if !e.options.DisableCompression {
if e.options.UseExternalLz4Compressor {
log.Debug("Using external lz4 command for compression")
in, err = compression.NewLz4CompExternal(in)
} else {
log.Debug("Using builtin lz4 compressor")
log.Warning("The built-in lz4 compressor produces files which can be read only by this library, not CloudSync")
in, err = compression.NewLz4CompBuiltin(in)
}
if err != nil {
return fmt.Errorf("unable to initialize the compression: %w", err)
}
}
_, err = io.Copy(cryptoOut, in)
if err != nil {
return fmt.Errorf("error copying data: %w", err)
}
err = cryptoOut.Close()
if err != nil {
return fmt.Errorf("error closing data: %w", err)
}
if hasher != nil {
actualFileDigest := hex.EncodeToString(hasher.Sum(nil))
err = objWriter.WriteMetadata(map[string]interface{}{
encoding.MetadataFieldMd5Digest: actualFileDigest,
})
if err != nil {
return fmt.Errorf("error writing digest: %w", err)
}
}
return nil
}
func encryptedSessionKeyByPassword(sessionKey []byte, password, encryptionSalt string) (string, string, error) {
encryptedBytes, err := crypto.EncryptOnceWithPasswordAndSalt([]byte(password), []byte(encryptionSalt), sessionKey)
if err != nil {
return "", "", err
}
encrypted := base64.StdEncoding.EncodeToString(encryptedBytes)
saltedHash, err := crypto.RandomSaltedHash(password)
return encrypted, saltedHash, err
}
func encryptedSessionKeyByPrivateKey(sessionKey []byte, privateKey []byte) (string, string, error) {
publicKey, err := crypto.PublicKeyFromPrivateKey(privateKey)
if err != nil {
return "", "", fmt.Errorf("error extracting public key from private key: '%w'", err)
}
res, err := crypto.EncryptOnceWithPublicKey([]byte(publicKey), []byte(sessionKey))
if err != nil {
return "", "", fmt.Errorf("error encrypting private key: '%w'", err)
}
encrypted := base64.StdEncoding.EncodeToString([]byte(res))
saltedHash, err := crypto.RandomSaltedHash(publicKey)
return encrypted, saltedHash, err
}
func bytesToHex(str []byte) []byte {
data := make([]byte, hex.EncodedLen(len(str)))
hex.Encode(data, []byte(str))
return data
}
func randomSessionKey() ([]byte, error) {
res := make([]byte, 32)
for i := 0; i < 32; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(256)))
if err != nil {
return nil, err
}
res[i] = byte(num.Int64())
}
return res, nil
}