-
Notifications
You must be signed in to change notification settings - Fork 0
/
cipher.go
256 lines (213 loc) · 7.99 KB
/
cipher.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
245
246
247
248
249
250
251
252
253
254
255
256
// ---------------------------------------------------------------------------------------------- //
// -- Copyright (c) 2024 Braden Hitchcock - MIT License (https://opensource.org/licenses/MIT) -- //
// ---------------------------------------------------------------------------------------------- //
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"strings"
"unicode"
"golang.org/x/crypto/bcrypt"
"golang.org/x/crypto/pbkdf2"
)
const (
// Iterations is the number of iterations used when generating a key with PBKDF2.
Iterations = 1000000
// KeyLength is the length of the key generated by PBKDF2 for encrypting data.
KeyLength = 32
// SaltLength is the length of the salt used by PBKDF2 when generating a key.
SaltLength = 32
// MinPasswordLength is the minimum number of characters that must be contained in a
// user-provided password.
MinPasswordLength = 16
// MaxPasswordLength is the maximum number of bytes a password can contain. This restriction is
// imposed by the Bcrypt algorithm used to hash the password.
MaxPasswordLength = 72
// HashCost is the cost used by the Bcrypt algorithm when hashing a password.
HashCost = 14
)
// Password is a user-provided string that has been validated and meets all criteria for a password.
type Password string
// Salt is the slice of saltlen bytes used when generating a key from a user-provided password.
type Salt []byte
// Key is the slice of keylen bytes used to encrypt data with AES-256.
type Key []byte
// PassHash is a hash of a password performed by Bcrypt represented as a slice of bytes.
type PassHash []byte
// DataHash is a hash of arbitrary data using SHA-256 represented as a 32-byte slice of bytes
type DataHash [32]byte
// NewPassword verifies the provided string value meets the criteria for a password and then wraps
// it in the Password type to indicate the string has been validated. If the provided string
// does not meet the password criteria for Kolob, then the function will return an error explaining
// which criteria failed.
//
// Note that although the function only returns a single error value, the message inside that error
// value is dynamic depending on which criteria for the password were not met.
func NewPassword(val string) (Password, error) {
count := 0
hasUpper := false
hasLower := false
hasNumber := false
hasSpecial := false
for _, c := range val {
switch {
case unicode.IsUpper(c):
hasUpper = true
case unicode.IsLower(c):
hasLower = true
case unicode.IsNumber(c):
hasNumber = true
case unicode.IsPunct(c) || unicode.IsSymbol(c):
hasSpecial = true
default:
}
count++
}
fails := make([]string, 0)
if count < MinPasswordLength {
fails = append(fails, fmt.Sprintf("at least %v characters", MinPasswordLength))
}
if count > MaxPasswordLength {
fails = append(fails, fmt.Sprintf("no more than %v characters", MaxPasswordLength))
}
if !hasUpper {
fails = append(fails, "one uppercase letter")
}
if !hasLower {
fails = append(fails, "one lowercase letter")
}
if !hasNumber {
fails = append(fails, "one number")
}
if !hasSpecial {
fails = append(fails, "one special character")
}
if len(fails) > 0 {
return "", fmt.Errorf("password must contain %v", strings.Join(fails, ", "))
}
return Password(val), nil
}
// HashPassword produces a byte hash of the provided password using the Bcrypt algorithm.
//
// See https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf for algorithm specifics.
func HashPassword(password Password) (PassHash, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), HashCost)
return bytes, err
}
// HashPassword compares a plain-text password against a byte hash of a password hashed using the
// Bcrypt algorithm. If using Bcrypt to hash the provided password would produce a string equal to
// the provided hash, then the function returns true. Otherwise the function returns false.
//
// See https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf for algorithm specifics.
func CheckPasswordHash(password Password, hash PassHash) bool {
err := bcrypt.CompareHashAndPassword(hash, []byte(password))
return err == nil
}
// HashData produces a byte hash of the provided data using the SHA-256 hashing algorithm.
func HashData(data []byte) DataHash {
return sha256.Sum256(data)
}
// CheckDataHash compares a slice of byte data with a hash using SHA-256. If using this algorithm to
// hash the provided data would produce the provided hash, then the function returns true. Otherwise
// the function returns false.
func CheckDataHash(data []byte, hash DataHash) bool {
return hash == sha256.Sum256(data)
}
// NewSalt creates a new slice containing saltlen bytes. The resulting salt is used when generating
// a key from a user-provided password.
func NewSalt() (Salt, error) {
salt := make([]byte, SaltLength)
_, err := rand.Read(salt)
if err != nil {
return nil, fmt.Errorf("failed to create salt: %v", err)
}
return salt, nil
}
// LoadSalt verifies that an existing byte slice only contains saltlen bytes. This ensures that a
// bytes loaded from authentication information can be used to recreate the original key used to
// encrypt data.
func LoadSalt(val []byte) (Salt, error) {
if len(val) != SaltLength {
return nil, errors.New("Salt is not the correct size")
}
return val, nil
}
// NewDerivedKey uses the PBKDF2 key derivation algorithm to create a 256-bit key that can be used
// by the AES algorithm for encrypting and decrypting data.
func NewDerivedKey(pass Password, salt Salt) Key {
return pbkdf2.Key([]byte(pass), salt, Iterations, KeyLength, sha256.New)
}
// NewRandomKey uses a cryptographically strong random generator to create a 256-bit key that can be
// used by the AES algorithm for encrypting and decrypting data.
func NewRandomKey() (Key, error) {
key := make([]byte, KeyLength)
_, err := rand.Read(key)
if err != nil {
return nil, fmt.Errorf("failed to create random key: %v", err)
}
return key, nil
}
// Encrypt uses AES-256 to encrypt the provided plaintext and produce a newly allocated byte slice
// of ciphertext. The byte slice is only valid if err is nil.
func Encrypt(key Key, plaintext []byte) (ciphertext []byte, err error) {
// Prepare the block cipher
block, err := aes.NewCipher(key)
if err != nil {
return
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return
}
// Create a new nonce and fill it with cryptographically strong random values.
nonce := make([]byte, gcm.NonceSize())
_, err = rand.Read(nonce)
if err != nil {
return
}
// Encrypt the plaintext data in the same byte sequence as the nonce. This will make it easy for
// us to extract the nonce from the ciphertext when we decrypt it later.
ciphertext = gcm.Seal(nonce, nonce, plaintext, nil)
return
}
// Decrypt uses AES-256 to decrypt the provided ciphertext and produce a newly allocated byte slice
// of the plaintext contents.
func Decrypt(key Key, ciphertext []byte) (plaintext []byte, err error) {
// Prepare the block cipher
block, err := aes.NewCipher(key)
if err != nil {
return
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return
}
// The ciphertext contains both the nonce and the encrypted data. We need to split the slice
// so that we can pass both to Open().
delim := gcm.NonceSize()
nonce, ciphertext := ciphertext[:delim], ciphertext[delim:]
// Decrypt
plaintext, err = gcm.Open(nil, nonce, ciphertext, nil)
return
}
// String returns a string containing a hexadecimal representation of the Salt receiver.
func (s Salt) String() string {
return hex.EncodeToString(s)
}
// String returns a string containing a hexadecimal representation of the Key receiver.
func (k Key) String() string {
return hex.EncodeToString(k)
}
// String returns a string containing a hexadecimal representation of the PassHash receiver.
func (h PassHash) String() string {
return hex.EncodeToString(h)
}
// String returns a string containing a hexadecimal representation of the DataHash receiver.
func (h DataHash) String() string {
return hex.EncodeToString(h[:])
}