-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdecrypter.go
128 lines (111 loc) · 3.57 KB
/
decrypter.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
// SPDX-FileCopyrightText: Winni Neessen <wn@neessen.dev>
//
// SPDX-License-Identifier: MIT
package iocrypter
import (
"bufio"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"errors"
"fmt"
"io"
"os"
)
// ErrTooLessRounds indicates that the provided number of rounds is smaller than the minimum
// required threshold.
var ErrTooLessRounds = errors.New("number of rounds too small")
func NewDecrypter(r io.Reader, password []byte) (io.ReadCloser, error) {
aesKey, hmacKey, iv, header, err := readParameters(r, password)
if err != nil {
return nil, fmt.Errorf("failed to read encryption parameters: %w", err)
}
hasher := hmac.New(hashFunc, hmacKey)
hasher.Write(header)
// We need to write the reader contents into a temporary file to authenticate the HMAC
tempFile, err := os.CreateTemp("", "iocrypter-*")
if err != nil {
return nil, fmt.Errorf("failed to create temporary file: %w", err)
}
defer func() {
_ = os.RemoveAll(tempFile.Name())
}()
block, err := aes.NewCipher(aesKey)
if err != nil {
return nil, fmt.Errorf("failed to create AES block cipher: %w", err)
}
decrypter := io.NopCloser(&cipher.StreamReader{
R: tempFile,
S: cipher.NewCTR(block, iv),
})
checksum := make([]byte, hmacSize)
writer := io.MultiWriter(hasher, tempFile)
buffer := bufio.NewReaderSize(r, chunkSize)
for {
data, err := buffer.Peek(chunkSize)
if err != nil && !errors.Is(err, io.EOF) {
return nil, fmt.Errorf("failed to read bytes from reader: %w", err)
}
// If we reached the end of the file, we read the rest of the buffered
// bytes, store them in the writer and read the HMAC into the checksum
// slice
if errors.Is(err, io.EOF) {
rest := buffer.Buffered()
if rest < hmacSize {
return nil, ErrMissingData
}
copy(checksum, data[rest-hmacSize:rest])
_, err = io.CopyN(writer, buffer, int64(rest-hmacSize))
if err != nil {
return nil, fmt.Errorf("failed to rest of buffered bytes: %w", err)
}
break
}
_, err = io.CopyN(writer, buffer, int64(chunkSize-hmacSize))
if err != nil {
return nil, err
}
}
// Authenticate the data
if !hmac.Equal(checksum, hasher.Sum(nil)) {
return nil, ErrFailedAuthentication
}
// Go back to the start of the file
if _, err = tempFile.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek to start of file: %w", err)
}
return decrypter, nil
}
// readParameters reads and deserializes the Argon2 settings, salt, IV, and derives keys from the provided
// reader and password.
func readParameters(r io.Reader, password []byte) ([]byte, []byte, []byte, []byte, error) {
if len(password) == 0 {
return nil, nil, nil, nil, ErrPassPhraseEmpty
}
settingsSerialized := make([]byte, 9)
if _, err := io.ReadFull(r, settingsSerialized); err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to read Argon2 settings: %w", err)
}
settings, err := DeserializeSettings(settingsSerialized)
if err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to deserialize Argon2 settings: %w", err)
}
salt := make([]byte, saltSize)
if _, err = io.ReadFull(r, salt); err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to read salt: %w", err)
}
iv := make([]byte, blockSize)
if _, err = io.ReadFull(r, iv); err != nil {
return nil, nil, nil, nil, fmt.Errorf("failed to read IV: %w", err)
}
header := bytes.NewBuffer(nil)
header.Write(settingsSerialized)
header.Write(salt)
header.Write(iv)
if settings.Time < 1 {
return nil, nil, nil, nil, ErrTooLessRounds
}
aesKey, hmacKey := DeriveKeys(password, salt, settings)
return aesKey, hmacKey, iv, header.Bytes(), nil
}