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  }