github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/account/credentials.go (about) 1 package account 2 3 import ( 4 "bytes" 5 "crypto/rand" 6 "encoding/base64" 7 "encoding/binary" 8 "encoding/json" 9 "errors" 10 "io" 11 "strings" 12 13 "github.com/cozy/cozy-stack/pkg/config/config" 14 "github.com/cozy/cozy-stack/pkg/couchdb" 15 "github.com/cozy/cozy-stack/pkg/keyring" 16 "golang.org/x/crypto/nacl/box" 17 ) 18 19 const cipherHeader = "nacl" 20 const nonceLen = 24 21 const plainPrefixLen = 4 22 23 var ( 24 errCannotDecrypt = errors.New("accounts: cannot decrypt credentials") 25 errCannotEncrypt = errors.New("accounts: cannot encrypt credentials") 26 // ErrBadCredentials is used when an account credentials cannot be decrypted 27 ErrBadCredentials = errors.New("accounts: bad credentials") 28 ) 29 30 // EncryptCredentialsWithKey takes a login / password and encrypts their values using 31 // the vault public key. 32 func EncryptCredentialsWithKey(encryptorKey *keyring.NACLKey, login, password string) (string, error) { 33 if encryptorKey == nil { 34 return "", errCannotEncrypt 35 } 36 37 loginLen := len(login) 38 39 // make a buffer containing the length of the login in bigendian over 4 40 // bytes, followed by the login and password contatenated. 41 creds := make([]byte, plainPrefixLen+loginLen+len(password)) 42 43 // put the length of login in the first 4 bytes 44 binary.BigEndian.PutUint32(creds[0:], uint32(loginLen)) 45 46 // copy the concatenation of login + password in the end 47 copy(creds[plainPrefixLen:], login) 48 copy(creds[plainPrefixLen+loginLen:], password) 49 50 var nonce [nonceLen]byte 51 if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { 52 panic(err) 53 } 54 55 encryptedOut := make([]byte, len(cipherHeader)+len(nonce)) 56 copy(encryptedOut[0:], cipherHeader) 57 copy(encryptedOut[len(cipherHeader):], nonce[:]) 58 59 encryptedCreds := box.Seal(encryptedOut, creds, &nonce, encryptorKey.PublicKey(), encryptorKey.PrivateKey()) 60 return base64.StdEncoding.EncodeToString(encryptedCreds), nil 61 } 62 63 // EncryptCredentialsData takes any json encodable data and encode and encrypts 64 // it using the vault public key. 65 func EncryptCredentialsData(data interface{}) (string, error) { 66 encryptorKey := config.GetKeyring().CredentialsEncryptorKey() 67 if encryptorKey == nil { 68 return "", errCannotEncrypt 69 } 70 buf, err := json.Marshal(data) 71 if err != nil { 72 return "", err 73 } 74 cipher, err := EncryptBufferWithKey(encryptorKey, buf) 75 if err != nil { 76 return "", err 77 } 78 return base64.StdEncoding.EncodeToString(cipher), nil 79 } 80 81 // EncryptBufferWithKey encrypts the given bytee buffer with the specified encryption 82 // key. 83 func EncryptBufferWithKey(encryptorKey *keyring.NACLKey, buf []byte) ([]byte, error) { 84 var nonce [nonceLen]byte 85 if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { 86 panic(err) 87 } 88 89 encryptedOut := make([]byte, len(cipherHeader)+len(nonce)) 90 copy(encryptedOut[0:], cipherHeader) 91 copy(encryptedOut[len(cipherHeader):], nonce[:]) 92 93 encryptedCreds := box.Seal(encryptedOut, buf, &nonce, encryptorKey.PublicKey(), encryptorKey.PrivateKey()) 94 return encryptedCreds, nil 95 } 96 97 // EncryptCredentials encrypts the given credentials with the specified encryption 98 // key. 99 func EncryptCredentials(login, password string) (string, error) { 100 encryptorKey := config.GetKeyring().CredentialsEncryptorKey() 101 if encryptorKey == nil { 102 return "", errCannotEncrypt 103 } 104 return EncryptCredentialsWithKey(encryptorKey, login, password) 105 } 106 107 // DecryptCredentials takes an encrypted credentials, constiting of a login / 108 // password pair, and decrypts it using the vault private key. 109 func DecryptCredentials(encryptedData string) (login, password string, err error) { 110 decryptorKey := config.GetKeyring().CredentialsDecryptorKey() 111 if decryptorKey == nil { 112 return "", "", errCannotDecrypt 113 } 114 encryptedBuffer, err := base64.StdEncoding.DecodeString(encryptedData) 115 if err != nil { 116 return "", "", errCannotDecrypt 117 } 118 return DecryptCredentialsWithKey(decryptorKey, encryptedBuffer) 119 } 120 121 // DecryptCredentialsWithKey takes an encrypted credentials, constiting of a 122 // login / password pair, and decrypts it using the given private key. 123 func DecryptCredentialsWithKey(decryptorKey *keyring.NACLKey, encryptedCreds []byte) (login, password string, err error) { 124 // check the cipher text starts with the cipher header 125 if !bytes.HasPrefix(encryptedCreds, []byte(cipherHeader)) { 126 return "", "", ErrBadCredentials 127 } 128 // skip the cipher header 129 encryptedCreds = encryptedCreds[len(cipherHeader):] 130 131 // check the encrypted creds contains the space for the nonce as prefix 132 if len(encryptedCreds) < nonceLen { 133 return "", "", ErrBadCredentials 134 } 135 136 // extrct the nonce from the first 24 bytes 137 var nonce [nonceLen]byte 138 copy(nonce[:], encryptedCreds[:nonceLen]) 139 140 // skip the nonce 141 encryptedCreds = encryptedCreds[nonceLen:] 142 // decrypt the cipher text and check that the plain text is more the 4 bytes 143 // long, to contain the login length 144 creds, ok := box.Open(nil, encryptedCreds, &nonce, decryptorKey.PublicKey(), decryptorKey.PrivateKey()) 145 if !ok { 146 return "", "", ErrBadCredentials 147 } 148 149 // extract login length from 4 first bytes 150 loginLen := int(binary.BigEndian.Uint32(creds[0:])) 151 152 // skip login length 153 creds = creds[plainPrefixLen:] 154 155 // check credentials contains enough space to contain at least the login 156 if len(creds) < loginLen { 157 return "", "", ErrBadCredentials 158 } 159 160 // split the credentials into login / password 161 return string(creds[:loginLen]), string(creds[loginLen:]), nil 162 } 163 164 // DecryptCredentialsData takes an encryted buffer and decrypts and decode its 165 // content. 166 func DecryptCredentialsData(encryptedData string) (interface{}, error) { 167 decryptorKey := config.GetKeyring().CredentialsDecryptorKey() 168 if decryptorKey == nil { 169 return nil, errCannotDecrypt 170 } 171 encryptedBuffer, err := base64.StdEncoding.DecodeString(encryptedData) 172 if err != nil { 173 return nil, errCannotDecrypt 174 } 175 plainBuffer, err := DecryptBufferWithKey(decryptorKey, encryptedBuffer) 176 if err != nil { 177 return nil, err 178 } 179 var data interface{} 180 if err = json.Unmarshal(plainBuffer, &data); err != nil { 181 return nil, err 182 } 183 return data, nil 184 } 185 186 // DecryptBufferWithKey takes an encrypted buffer and decrypts it using the 187 // given private key. 188 func DecryptBufferWithKey(decryptorKey *keyring.NACLKey, encryptedBuffer []byte) ([]byte, error) { 189 // check the cipher text starts with the cipher header 190 if !bytes.HasPrefix(encryptedBuffer, []byte(cipherHeader)) { 191 return nil, ErrBadCredentials 192 } 193 194 // skip the cipher header 195 encryptedBuffer = encryptedBuffer[len(cipherHeader):] 196 197 // check the encrypted creds contains the space for the nonce as prefix 198 if len(encryptedBuffer) < nonceLen { 199 return nil, ErrBadCredentials 200 } 201 202 // extrct the nonce from the first 24 bytes 203 var nonce [nonceLen]byte 204 copy(nonce[:], encryptedBuffer[:nonceLen]) 205 206 // skip the nonce 207 encryptedBuffer = encryptedBuffer[nonceLen:] 208 209 // decrypt the cipher text and check that the plain text is more the 4 bytes 210 // long, to contain the login length 211 plainBuffer, ok := box.Open(nil, encryptedBuffer, &nonce, decryptorKey.PublicKey(), decryptorKey.PrivateKey()) 212 if !ok { 213 return nil, ErrBadCredentials 214 } 215 216 return plainBuffer, nil 217 } 218 219 // Encrypt encrypts sensitive fields inside the account. The document is 220 // modified in place. 221 func Encrypt(doc couchdb.JSONDoc) bool { 222 if config.GetKeyring().CredentialsEncryptorKey() != nil { 223 return encryptMap(doc.M) 224 } 225 return false 226 } 227 228 // Decrypt decrypts sensitive fields inside the account. The document is 229 // modified in place. 230 func Decrypt(doc couchdb.JSONDoc) bool { 231 if config.GetKeyring().CredentialsDecryptorKey() != nil { 232 return decryptMap(doc.M) 233 } 234 return false 235 } 236 237 func encryptMap(m map[string]interface{}) (encrypted bool) { 238 auth, ok := m["auth"].(map[string]interface{}) 239 if !ok { 240 return 241 } 242 login, _ := auth["login"].(string) 243 cloned := make(map[string]interface{}, len(auth)) 244 var encKeys []string 245 for k, v := range auth { 246 var err error 247 switch k { 248 case "password": 249 password, _ := v.(string) 250 cloned["credentials_encrypted"], err = EncryptCredentials(login, password) 251 if err == nil { 252 encrypted = true 253 } 254 case "secret", "dob", "code", "answer", "access_token", "refresh_token", "appSecret", "session": 255 cloned[k+"_encrypted"], err = EncryptCredentialsData(v) 256 if err == nil { 257 encrypted = true 258 } 259 default: 260 if strings.HasSuffix(k, "_encrypted") { 261 encKeys = append(encKeys, k) 262 } else { 263 cloned[k] = v 264 } 265 } 266 } 267 for _, key := range encKeys { 268 if _, ok := cloned[key]; !ok { 269 cloned[key] = auth[key] 270 } 271 } 272 m["auth"] = cloned 273 if data, ok := m["data"].(map[string]interface{}); ok { 274 if encryptMap(data) && !encrypted { 275 encrypted = true 276 } 277 } 278 return 279 } 280 281 func decryptMap(m map[string]interface{}) (decrypted bool) { 282 auth, ok := m["auth"].(map[string]interface{}) 283 if !ok { 284 return 285 } 286 cloned := make(map[string]interface{}, len(auth)) 287 for k, v := range auth { 288 if !strings.HasSuffix(k, "_encrypted") { 289 cloned[k] = v 290 continue 291 } 292 k = strings.TrimSuffix(k, "_encrypted") 293 var str string 294 str, ok = v.(string) 295 if !ok { 296 cloned[k] = v 297 continue 298 } 299 var err error 300 if k == "credentials" { 301 cloned["login"], cloned["password"], err = DecryptCredentials(str) 302 } else { 303 cloned[k], err = DecryptCredentialsData(str) 304 } 305 if !decrypted { 306 decrypted = err == nil 307 } 308 } 309 m["auth"] = cloned 310 if data, ok := m["data"].(map[string]interface{}); ok { 311 if decryptMap(data) && !decrypted { 312 decrypted = true 313 } 314 } 315 return 316 }