github.com/minio/madmin-go/v2@v2.2.1/encrypt.go (about) 1 // 2 // Copyright (c) 2015-2022 MinIO, Inc. 3 // 4 // This file is part of MinIO Object Storage stack 5 // 6 // This program is free software: you can redistribute it and/or modify 7 // it under the terms of the GNU Affero General Public License as 8 // published by the Free Software Foundation, either version 3 of the 9 // License, or (at your option) any later version. 10 // 11 // This program is distributed in the hope that it will be useful, 12 // but WITHOUT ANY WARRANTY; without even the implied warranty of 13 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 // GNU Affero General Public License for more details. 15 // 16 // You should have received a copy of the GNU Affero General Public License 17 // along with this program. If not, see <http://www.gnu.org/licenses/>. 18 // 19 20 package madmin 21 22 import ( 23 "bytes" 24 "crypto/sha256" 25 "errors" 26 "io" 27 28 "github.com/secure-io/sio-go" 29 "github.com/secure-io/sio-go/sioutil" 30 "golang.org/x/crypto/argon2" 31 "golang.org/x/crypto/pbkdf2" 32 ) 33 34 // IsEncrypted reports whether data is encrypted. 35 func IsEncrypted(data []byte) bool { 36 if len(data) <= 32 { 37 return false 38 } 39 b := data[32] 40 return b == pbkdf2AESGCM || b == argon2idAESGCM || b == argon2idChaCHa20Poly1305 41 } 42 43 // EncryptData encrypts the data with an unique key 44 // derived from password using the Argon2id PBKDF. 45 // 46 // The returned ciphertext data consists of: 47 // 48 // salt | AEAD ID | nonce | encrypted data 49 // 32 1 8 ~ len(data) 50 func EncryptData(password string, data []byte) ([]byte, error) { 51 salt := sioutil.MustRandom(32) 52 53 var ( 54 id byte 55 err error 56 stream *sio.Stream 57 ) 58 if FIPSEnabled() { 59 key := pbkdf2.Key([]byte(password), salt, pbkdf2Cost, 32, sha256.New) 60 stream, err = sio.AES_256_GCM.Stream(key) 61 if err != nil { 62 return nil, err 63 } 64 id = pbkdf2AESGCM 65 } else { 66 key := argon2.IDKey([]byte(password), salt, argon2idTime, argon2idMemory, argon2idThreads, 32) 67 if sioutil.NativeAES() { 68 stream, err = sio.AES_256_GCM.Stream(key) 69 if err != nil { 70 return nil, err 71 } 72 id = argon2idAESGCM 73 } else { 74 stream, err = sio.ChaCha20Poly1305.Stream(key) 75 if err != nil { 76 return nil, err 77 } 78 id = argon2idChaCHa20Poly1305 79 } 80 } 81 82 nonce := sioutil.MustRandom(stream.NonceSize()) 83 84 // ciphertext = salt || AEAD ID | nonce | encrypted data 85 cLen := int64(len(salt)+1+len(nonce)+len(data)) + stream.Overhead(int64(len(data))) 86 ciphertext := bytes.NewBuffer(make([]byte, 0, cLen)) // pre-alloc correct length 87 88 // Prefix the ciphertext with salt, AEAD ID and nonce 89 ciphertext.Write(salt) 90 ciphertext.WriteByte(id) 91 ciphertext.Write(nonce) 92 93 w := stream.EncryptWriter(ciphertext, nonce, nil) 94 if _, err = w.Write(data); err != nil { 95 return nil, err 96 } 97 if err = w.Close(); err != nil { 98 return nil, err 99 } 100 return ciphertext.Bytes(), nil 101 } 102 103 // ErrMaliciousData indicates that the stream cannot be 104 // decrypted by provided credentials. 105 var ErrMaliciousData = sio.NotAuthentic 106 107 // ErrUnexpectedHeader indicates that the data stream returned unexpected header 108 var ErrUnexpectedHeader = errors.New("unexpected header") 109 110 // DecryptData decrypts the data with the key derived 111 // from the salt (part of data) and the password using 112 // the PBKDF used in EncryptData. DecryptData returns 113 // the decrypted plaintext on success. 114 // 115 // The data must be a valid ciphertext produced by 116 // EncryptData. Otherwise, the decryption will fail. 117 func DecryptData(password string, data io.Reader) ([]byte, error) { 118 // Parse the stream header 119 var hdr [32 + 1 + 8]byte 120 if _, err := io.ReadFull(data, hdr[:]); err != nil { 121 if errors.Is(err, io.EOF) { 122 // Incomplete header, return unexpected header 123 return nil, ErrUnexpectedHeader 124 } 125 return nil, err 126 } 127 salt, id, nonce := hdr[0:32], hdr[32:33], hdr[33:41] 128 129 var ( 130 err error 131 stream *sio.Stream 132 ) 133 switch { 134 case id[0] == argon2idAESGCM: 135 key := argon2.IDKey([]byte(password), salt, argon2idTime, argon2idMemory, argon2idThreads, 32) 136 stream, err = sio.AES_256_GCM.Stream(key) 137 case id[0] == argon2idChaCHa20Poly1305: 138 key := argon2.IDKey([]byte(password), salt, argon2idTime, argon2idMemory, argon2idThreads, 32) 139 stream, err = sio.ChaCha20Poly1305.Stream(key) 140 case id[0] == pbkdf2AESGCM: 141 key := pbkdf2.Key([]byte(password), salt, pbkdf2Cost, 32, sha256.New) 142 stream, err = sio.AES_256_GCM.Stream(key) 143 default: 144 err = errors.New("madmin: invalid encryption algorithm ID") 145 } 146 if err != nil { 147 return nil, err 148 } 149 150 return io.ReadAll(stream.DecryptReader(data, nonce, nil)) 151 } 152 153 const ( 154 argon2idAESGCM = 0x00 155 argon2idChaCHa20Poly1305 = 0x01 156 pbkdf2AESGCM = 0x02 157 ) 158 159 const ( 160 argon2idTime = 1 161 argon2idMemory = 64 * 1024 162 argon2idThreads = 4 163 pbkdf2Cost = 8192 164 )