diff --git a/internal/bitwarden/crypto/keybuilder/prelogin_key.go b/internal/bitwarden/crypto/keybuilder/prelogin_key.go index 29351d6..3fee255 100644 --- a/internal/bitwarden/crypto/keybuilder/prelogin_key.go +++ b/internal/bitwarden/crypto/keybuilder/prelogin_key.go @@ -2,19 +2,27 @@ package keybuilder import ( "crypto/sha256" + "fmt" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/crypto/symmetrickey" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" + "golang.org/x/crypto/argon2" "golang.org/x/crypto/pbkdf2" ) -const ( - PBKDF2_SHA256 = 0 -) - -func BuildPreloginKey(masterPassword, email string, kdfIteration int) (*symmetrickey.Key, error) { - return buildKey(masterPassword, email, PBKDF2_SHA256, kdfIteration) +func BuildPreloginKey(masterPassword, email string, kdfConfig models.KdfConfiguration) (*symmetrickey.Key, error) { + return buildKey(masterPassword, email, kdfConfig) } -func buildKey(masterPassword, salt string, kdf, iterations int) (*symmetrickey.Key, error) { - return symmetrickey.NewFromRawBytes(pbkdf2.Key([]byte(masterPassword), []byte(salt), iterations, 32, sha256.New)) +func buildKey(masterPassword, salt string, kdfConfig models.KdfConfiguration) (*symmetrickey.Key, error) { + switch kdfConfig.KdfType { + case models.KdfTypePBKDF2_SHA256: + return symmetrickey.NewFromRawBytes(pbkdf2.Key([]byte(masterPassword), []byte(salt), kdfConfig.KdfIterations, 32, sha256.New)) + case models.KdfTypeArgon2: + hashedSalt := sha256.New() + hashedSalt.Write([]byte(salt)) + return symmetrickey.NewFromRawBytes(argon2.IDKey([]byte(masterPassword), hashedSalt.Sum(nil), uint32(kdfConfig.KdfIterations), uint32(kdfConfig.KdfMemory*1024), uint8(kdfConfig.KdfParallelism), 32)) + default: + return nil, fmt.Errorf("unsupported KDF: '%d'", kdfConfig.KdfType) + } } diff --git a/internal/bitwarden/embedded/models.go b/internal/bitwarden/embedded/models.go index e34d030..577d6cb 100644 --- a/internal/bitwarden/embedded/models.go +++ b/internal/bitwarden/embedded/models.go @@ -6,19 +6,17 @@ import ( "fmt" "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/crypto/symmetrickey" + "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models" ) type Account struct { - AccountUUID string `json:"accountUuid,omitempty"` - Email string `json:"email,omitempty"` - VaultFormat string `json:"vaultFormat,omitempty"` - KdfIterations int `json:"kdfIterations,omitempty"` - KdfMemory int `json:"kdfMemory,omitempty"` - KdfParallelism int `json:"kdfParallelism,omitempty"` - KdfType int `json:"kdfType,omitempty"` - ProtectedSymmetricKey string `json:"protectedSymmetricKey,omitempty"` - ProtectedRSAPrivateKey string `json:"protectedRSAPrivateKey,omitempty"` - Secrets AccountSecrets `json:"-"` + AccountUUID string `json:"accountUuid,omitempty"` + Email string `json:"email,omitempty"` + VaultFormat string `json:"vaultFormat,omitempty"` + KdfConfig models.KdfConfiguration `json:"kdfConfig,omitempty"` + ProtectedSymmetricKey string `json:"protectedSymmetricKey,omitempty"` + ProtectedRSAPrivateKey string `json:"protectedRSAPrivateKey,omitempty"` + Secrets AccountSecrets `json:"-"` } func (a *Account) PrivateKeyDecrypted() bool { diff --git a/internal/bitwarden/embedded/vault_base.go b/internal/bitwarden/embedded/vault_base.go index ccfdba8..cd0a64c 100644 --- a/internal/bitwarden/embedded/vault_base.go +++ b/internal/bitwarden/embedded/vault_base.go @@ -123,11 +123,7 @@ func (v *baseVault) storeObject(ctx context.Context, obj models.Object) { } func decryptAccountSecrets(account Account, password string) (*AccountSecrets, error) { - if account.KdfType != 0 { - return nil, fmt.Errorf("unsupported kdf type '%d'", account.KdfType) - } - - masterKey, err := keybuilder.BuildPreloginKey(password, account.Email, account.KdfIterations) + masterKey, err := keybuilder.BuildPreloginKey(password, account.Email, account.KdfConfig) if err != nil { return nil, fmt.Errorf("error building prelogin key: %w", err) } diff --git a/internal/bitwarden/embedded/vault_base_test.go b/internal/bitwarden/embedded/vault_base_test.go index c38a103..8f814a1 100644 --- a/internal/bitwarden/embedded/vault_base_test.go +++ b/internal/bitwarden/embedded/vault_base_test.go @@ -15,10 +15,13 @@ import ( var ( testAccount = Account{ - AccountUUID: "e8dababd-242e-4900-becf-e88bc021dda8", - Email: "test@laverse.net", - VaultFormat: "API", - KdfIterations: 600000, + AccountUUID: "e8dababd-242e-4900-becf-e88bc021dda8", + Email: "test@laverse.net", + VaultFormat: "API", + KdfConfig: models.KdfConfiguration{ + KdfType: models.KdfTypePBKDF2_SHA256, + KdfIterations: 600000, + }, ProtectedSymmetricKey: "2.lkAJiJtCKPHFPrZ96+j2Xg==|5XJtrKUndcGy28thFukrmgMcLp+BOVdkF+KcuOnfshq9AN1PFhna9Es96CVARCnjTcWuHuqvgnGmcOHTrf8fyfLv63VBsjLgLZk8rCXJoKE=|9dwgx4/13AD+elE2vE7vlSQoe8LbCGGlui345YrKvXY=", ProtectedRSAPrivateKey: "2.D2aLa8ne/DAkeSzctQISVw==|/xoGM5i5JGJTH/vohUuwTFrTx3hd/gt3kBD/FQdeLMtYjl1u96sh0ECmoERqGHeSfXj+iAb9kpTIOKG8LwmkZGUJBI90Mw0M7ODmf7E8eQ+aGF+bGqTSMQ1wtpunEyFVodlg92YN8Ddlb2V9J4uN8ykpHYNDmQiYLZ8bl6vCODRGPyzLvx5M8DbITVL5PhsjKDLLrVV8lFCgCcAL5YLfkghYhFELyX15zXA/KYEnwggDka3hG5+HHFOVZSeyk7Gi6M4TX2wADbTXz1/Wsho8oxFUrtNiOB3ZiY2cx9UWttpzMXoGfi2gJcP1db/nTfWenOLlzw6Od4VyRzsXsfyGwbqBqDnNFkjLvhjVw4JO+psF//xAMDs14101Tf2wFkB6toQ+zdnDphXUeKmiVPQ7gMnQlOWN5tWvjjmYOO4Y63sGpP24cDOdEScIdebZRSA8uOhTzadfKfOiH5zVYZzXs33FQ0li4nBrsj5xYa6PP4D1P7gqjxClPdguwkdLoZ7JvgIlyRIwEcORi5Ich8RWF/kqRBwk0QSzK1mTlHHU5xtSgi4MLNVx4qTYNaJVBtwL9d2MD9LeNn4Z2PL4A7qnszHqsERiQQDxNEgMxMBHDgSXqQbtQxRvsI6oY+yNbN7uVWw4o8AC3f5GBdxzIqcN1mgEM5ix5aDt15w3MhP2FHtf2neKI68TnL8WnT1fT8BVlbECIiUqK0tfq5aTjdSh3gCS15jvZ1H60h+K8+O/nDfquzVjY7UsTGwA+UtS8/JGiaUhc0VhxJo8P1V2VSCiu51d5q3De1vDg5R2VEgBmTchZyTIodC+3+7ACTOwkNCCdIJN7xKOcIGFA7QOuyJtBeXT4Rd9UGMHSL054IB/315WVDiwrP9W1aP0nHzFs+qAXbH5o1E+AmfMDyoHopjGgbUw22r837kHzf5Qe8QhRYPzQvDowfCPhdoy23cbN1VsNavNwTC9hcG5oYMwkK66xP3MEM9UfGD22pwxe7M8U4BRdLCCHbi95eklXE6Mg6DpWsAdMgokQbOvnlwgKfrlbltqXUE/vUQI8TB3AE1Nkt0ST4quTriMuuyiHdeeZV4UkV9jWt/5bTCfdrCYuGZP7g4shbfNcP7u1Zrdxv+EuUwGIOOTrNV5awmBnL3iHE5ya2MnqmRyfWiPIT5majZCk06yxj4XzyIPOpjYKFt0MOgLvG1GllmdtRqg7tMVvc5ZFo5KWIxLsIJD12UjA1GYYoFdX4+wsNbPjfnlE6D2PrtWUICnBFJzYpfyrKTe01k8G8hyyz+tVzBRfz8EA2ew1+hlVcAgSPCcBzhDgqPe+RSPi7ZSd66be1gDhGAftWFM8Z0MrMklXi2DyjjaKBNsZZD8qTcLcobm8nqHUQtnr5JCbmgP3rau8NY/fxeFHsvSiZQoB1aI/y+Sz/R4r+T9cg8hjmS/FUHDO+m6a6nuWNFwz8wIluM557oOTl+A9UGFF50Gpzmf97VdQjM3ZREazQ7la6AobzS3BHI6FNdxN9LTyMpYo+WODv52/VwU3ODH7wf5bz2OHZhk2NG5R7pSH7qg8jM+/MtJkFumENV0qMecozIkP6e4CyI9ua4YwI9n7G5OgKYMG1aj2PRSny2JSLS8aHF1TkRL8SD0nZFCox0=|muEtiwIuZxhuuLv0nouEdxHU2CO+I7JXKZuYHWiv/OE=", } diff --git a/internal/bitwarden/embedded/vault_mocked_webapi_test.go b/internal/bitwarden/embedded/vault_mocked_webapi_test.go index e50c55f..4eb4d4d 100644 --- a/internal/bitwarden/embedded/vault_mocked_webapi_test.go +++ b/internal/bitwarden/embedded/vault_mocked_webapi_test.go @@ -14,9 +14,9 @@ import ( "github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/webapi" ) -func NewWebAPIVaultWithMockedTransport(t *testing.T) webAPIVault { +func NewMockedWebAPIVault(t *testing.T, client webapi.Client) webAPIVault { vault := NewWebAPIVault("http://127.0.0.1:8081/").(*webAPIVault) - vault.client = mockedClient() + vault.client = client return *vault } @@ -524,9 +524,184 @@ func mockedClient() webapi.Client { return webapi.NewClient("http://127.0.0.1:8081/", webapi.WithCustomClient(client), webapi.DisableRetries()) } +func mockedClientArgon2() webapi.Client { + client := http.Client{Transport: httpmock.DefaultTransport} + httpmock.RegisterResponder("POST", "http://127.0.0.1:8081/identity/accounts/prelogin", + httpmock.NewStringResponder(200, `{"kdf":1,"kdfIterations":3,"kdfMemory":64,"kdfParallelism":4}`)) + + // Regexp match (could use httpmock.RegisterRegexpResponder instead) + httpmock.RegisterResponder("POST", `http://127.0.0.1:8081/identity/connect/token`, + httpmock.NewStringResponder(200, `{"ForcePasswordReset":false,"Kdf":1,"KdfIterations":3,"KdfMemory":64,"KdfParallelism":4,"Key":"2.VBpAJrIHYLv60UTYy5e7sA==|WWsozKnKPVvBYttXtlAzpmZPoffwlY3+8Wup9SBdGcO8T4Ybj808DubDhICTPMz9RliDSJ1OuaivBC0rh/7EcWV1s9KuRth5J3XFWJqmtXU=|l1ly+Uek1j4k2xQNT8iJ++GdwMTWRaSBP5mFvgJ+CGU=","MasterPasswordPolicy":{"object":"masterPasswordPolicy"},"PrivateKey":"2.0Xc+uV+6YVa+Zkg74C/MJA==|xTbZGy2r8/kHcXXy23YDHWDTHjasUUoHiWJ84rGEhuPD27GTF8DrFL+lbERo3+OH7MXvseAFygKArvSumlCQnMhT/dswJMH8ZEMuxQUZ8I7kg/7eNqxRndxyC8alZh3VM4FC7TsuVBe6BF4pbPQYX4etFL8yOHLpIhwkKcG7+IfQHQHwOXpsSiGpADQlk5er990snhAXwGpgROqoveO9klpNJXuEzJKMFMdoo9FCaRsJ4bB3BE/Y+8Ph7ek8mGyoUUqNFp8Z/T+XN+9kvxDFnVFtYp1p+sU8LpCSvaq3dAImQt6X7vAgfjbWVkTxB0HlBdMkjpg8BAK7qpscT5oH83SZqdNPRi9kkT9Y30xl0QvvJzXLcjqOS6je+i3vB1r4O8X3ZIj+th4cfZOfHKpzDLbGyAezxiiWwQ4xt0USQ9rLv+BU5YNDKnibmsIooKHuh8wV0qht8vr2CEVavXOmmBi/6bGIWJMOs2K52be5LJkYL+653dsKXBN4uahQDMdfs9vPUA7OoIFR9BZvQiGMDstFYVgJUulOkj2J4nieuTAmurQe6nS6U4v1swI7DIzdo8ZCJcAQfWYWk8IBwh6gQxSuPMg7+O2dntbPFkMP2KhoisUucoXNIDXmxwYAMeP01KMNSgFc6hL6WnLzyVcRlSLy7OC9qwTD6ZH6XA4D/MO9MINTTzw4+/pZvwTeuXMqB0HkcjTpWzTUi405uV6qh8CxYOaNEl6kNAMiuVNASToqYb/EBC0uGOybOzcNZOCiXmuiPVOstiz65KP1d3Dfl9al48hpxnDxFyFub5NDx8xADA3SzOlI9xMCsC/mcY0/fRlwoTDFCLzfdtPIdSn5N8/YySY10e/TXftKGV7bLvzGAlOntwoiWaLJbyHAnEXNUVFxJu5XVaVccXcxIKete42S291dCu4yk35KsIQU0jBaPB/hGXLJvvySl9/kARl0zIXJH+Pk0hlk+/IRH9HNZrru93WTgW9KjEeT2vaZ6UqULkkspIfoUrFxQfSyAxycDaqa4EHt1QJBKyC9+aWEOXNwtxkJTnhtvlqRPOGUpdkAyC5ebfdX2URZnd/2TR/PTviWaDKe/g51UWr8QepgwbJjkBeuZMSPCNHkcLdmwEZERisbZ7H50hDQhhK+qfmMOvkVrRRXKT2ICbQzchm5rDasdtMed7vIf3beS0ESIR8kZ1WmteI9dvWmagnXRlfsUZW0y0KWe9Ma/ISOa6QPb0Cc7T8LUg2lpREQpRTt119RX9Xugd2uZ+UzbGpqn9u5JY2+vddwv+zFL0g/Vu9h3kd02v+WTXeV8wLDJYkoc6XeTBzC0uQtNJt4oN7hWcMBrkYmnXxslIjGw/nE7eBnG02XWTCeTjADqXtz2j4xMFmzJod+4j4f6MQZBII9Sz5A2FfSbadUuukEYcFvkqLnjG/sMzcBQMIDP6vmy7VRnETahlNi7Pt+gRLHuhGN1BbScolW7a0YnWlI1T0MtKb4DBsjX9GfotxkG+ORg/i45YnG6KPQ6+jgXU3Xtf0tgjTqHVgc7TOCm9R2b4G24RSQXx3WaMP1slbSYsf/YmXIqdg/dVaCFrJdA1v2bDFymuPbA0P4Ey8c4ylInXGgK4yxS7nVC4qbbVtK5bWT3SRaBWw=|0r9TcWfl93tZln8+ZGrPwZTHBbrLiSuVXvoI+cQeSr0=","ResetMasterPassword":false,"UserDecryptionOptions":{"HasMasterPassword":true,"Object":"userDecryptionOptions"},"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3Mjc4MDcxNDUsImV4cCI6MTcyNzgxNDM0NSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdHxsb2dpbiIsInN1YiI6ImM2NGExZjhkLTIyM2MtNDBmZC1hZGRlLWE5MGQ1OGRiYzI2YyIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJ0ZXN0LTIwMjM0MSIsImVtYWlsIjoidGVzdC0yMDIzNDEtYXJnb24yQGxhdmVyc2UubmV0IiwiZW1haWxfdmVyaWZpZWQiOnRydWUsInNzdGFtcCI6IjZjYmY3Y2QxLWQyYTItNGM1YS05N2Q3LTkxMDQyMjg1YzQ5ZCIsImRldmljZSI6IiIsInNjb3BlIjpbImFwaSIsIm9mZmxpbmVfYWNjZXNzIl0sImFtciI6WyJBcHBsaWNhdGlvbiJdfQ.hJ5-aPuZFAovG_2q7im8xxO2GCjeHit7Hwhm-FKQsu-j7OJdY8F_2jSy7azPcrVasImboR99WvpydMYzbQLB3AlQ3OSPcqUUHJDlFqeDUIXzpi-NMb3CBkxyr8Q_fBojiWmRiwPY89oiOE6dXTvDxTEUIWpKv0xnd-iBBPwwx6vzfsSKKAIB1tbZM1Cy4aWgcbN0lC-cU-NGcIFI1UEVod9KzcuN65DouXfjB2d5NkAI-pic9pno42MF8strosezgWrbnvTk8h7CzT5BPj7ZCaqYVr_RKVciHAs3QwrFyFe_OZ4RoN_YikDApdbwQyIsJsIK_jNTraJzNMfo6bbZfA","expires_in":7200,"refresh_token":"JPxgcZSlj4KKfgZuyf8PnLF9KVXADYOYstA5hBreXGPwm7667dSeGPFtBYRWOyXRJxQryUr6BLEjXHBS_NqdIw==","scope":"api offline_access","token_type":"Bearer","unofficialServer":true}`)) + + httpmock.RegisterResponder("GET", `http://127.0.0.1:8081/api/sync?excludeDomains=true`, + httpmock.NewStringResponder(200, `{ + "ciphers": [ + { + "attachments": null, + "card": null, + "collectionIds": [], + "creationDate": "2024-10-01T18:25:26.092790Z", + "data": { + "autofillOnPageLoad": null, + "fields": [], + "name": "2.zD1jZaHjvJ0ZsD4OpfRHXg==|eekdaveWXtPM+Bo285ljIQ==|4pvvvQZzyX5HRCaY+jUrecLR0AlXx4oVf994NNTyHyE=", + "notes": null, + "password": null, + "passwordHistory": [], + "passwordRevisionDate": null, + "totp": null, + "uri": null, + "uris": [], + "username": null + }, + "deletedDate": null, + "edit": true, + "favorite": false, + "fields": [], + "folderId": null, + "id": "f01e68e9-3951-40ee-80ff-c4ff4517e159", + "identity": null, + "key": null, + "login": { + "autofillOnPageLoad": null, + "password": null, + "passwordRevisionDate": null, + "totp": null, + "uri": null, + "uris": [], + "username": null + }, + "name": "2.zD1jZaHjvJ0ZsD4OpfRHXg==|eekdaveWXtPM+Bo285ljIQ==|4pvvvQZzyX5HRCaY+jUrecLR0AlXx4oVf994NNTyHyE=", + "notes": null, + "object": "cipherDetails", + "organizationId": null, + "organizationUseTotp": true, + "passwordHistory": [], + "reprompt": 0, + "revisionDate": "2024-10-01T18:25:26.093577Z", + "secureNote": null, + "type": 1, + "viewPassword": true + } + ], + "collections": [], + "domains": null, + "folders": [], + "object": "sync", + "policies": [], + "profile": { + "_status": 0, + "avatarColor": null, + "culture": "en-US", + "email": "test-202341-argon2@laverse.net", + "emailVerified": true, + "forcePasswordReset": false, + "id": "c64a1f8d-223c-40fd-adde-a90d58dbc26c", + "key": "2.VBpAJrIHYLv60UTYy5e7sA==|WWsozKnKPVvBYttXtlAzpmZPoffwlY3+8Wup9SBdGcO8T4Ybj808DubDhICTPMz9RliDSJ1OuaivBC0rh/7EcWV1s9KuRth5J3XFWJqmtXU=|l1ly+Uek1j4k2xQNT8iJ++GdwMTWRaSBP5mFvgJ+CGU=", + "masterPasswordHint": null, + "name": "test-202341", + "object": "profile", + "organizations": [], + "premium": true, + "premiumFromOrganization": false, + "privateKey": "2.0Xc+uV+6YVa+Zkg74C/MJA==|xTbZGy2r8/kHcXXy23YDHWDTHjasUUoHiWJ84rGEhuPD27GTF8DrFL+lbERo3+OH7MXvseAFygKArvSumlCQnMhT/dswJMH8ZEMuxQUZ8I7kg/7eNqxRndxyC8alZh3VM4FC7TsuVBe6BF4pbPQYX4etFL8yOHLpIhwkKcG7+IfQHQHwOXpsSiGpADQlk5er990snhAXwGpgROqoveO9klpNJXuEzJKMFMdoo9FCaRsJ4bB3BE/Y+8Ph7ek8mGyoUUqNFp8Z/T+XN+9kvxDFnVFtYp1p+sU8LpCSvaq3dAImQt6X7vAgfjbWVkTxB0HlBdMkjpg8BAK7qpscT5oH83SZqdNPRi9kkT9Y30xl0QvvJzXLcjqOS6je+i3vB1r4O8X3ZIj+th4cfZOfHKpzDLbGyAezxiiWwQ4xt0USQ9rLv+BU5YNDKnibmsIooKHuh8wV0qht8vr2CEVavXOmmBi/6bGIWJMOs2K52be5LJkYL+653dsKXBN4uahQDMdfs9vPUA7OoIFR9BZvQiGMDstFYVgJUulOkj2J4nieuTAmurQe6nS6U4v1swI7DIzdo8ZCJcAQfWYWk8IBwh6gQxSuPMg7+O2dntbPFkMP2KhoisUucoXNIDXmxwYAMeP01KMNSgFc6hL6WnLzyVcRlSLy7OC9qwTD6ZH6XA4D/MO9MINTTzw4+/pZvwTeuXMqB0HkcjTpWzTUi405uV6qh8CxYOaNEl6kNAMiuVNASToqYb/EBC0uGOybOzcNZOCiXmuiPVOstiz65KP1d3Dfl9al48hpxnDxFyFub5NDx8xADA3SzOlI9xMCsC/mcY0/fRlwoTDFCLzfdtPIdSn5N8/YySY10e/TXftKGV7bLvzGAlOntwoiWaLJbyHAnEXNUVFxJu5XVaVccXcxIKete42S291dCu4yk35KsIQU0jBaPB/hGXLJvvySl9/kARl0zIXJH+Pk0hlk+/IRH9HNZrru93WTgW9KjEeT2vaZ6UqULkkspIfoUrFxQfSyAxycDaqa4EHt1QJBKyC9+aWEOXNwtxkJTnhtvlqRPOGUpdkAyC5ebfdX2URZnd/2TR/PTviWaDKe/g51UWr8QepgwbJjkBeuZMSPCNHkcLdmwEZERisbZ7H50hDQhhK+qfmMOvkVrRRXKT2ICbQzchm5rDasdtMed7vIf3beS0ESIR8kZ1WmteI9dvWmagnXRlfsUZW0y0KWe9Ma/ISOa6QPb0Cc7T8LUg2lpREQpRTt119RX9Xugd2uZ+UzbGpqn9u5JY2+vddwv+zFL0g/Vu9h3kd02v+WTXeV8wLDJYkoc6XeTBzC0uQtNJt4oN7hWcMBrkYmnXxslIjGw/nE7eBnG02XWTCeTjADqXtz2j4xMFmzJod+4j4f6MQZBII9Sz5A2FfSbadUuukEYcFvkqLnjG/sMzcBQMIDP6vmy7VRnETahlNi7Pt+gRLHuhGN1BbScolW7a0YnWlI1T0MtKb4DBsjX9GfotxkG+ORg/i45YnG6KPQ6+jgXU3Xtf0tgjTqHVgc7TOCm9R2b4G24RSQXx3WaMP1slbSYsf/YmXIqdg/dVaCFrJdA1v2bDFymuPbA0P4Ey8c4ylInXGgK4yxS7nVC4qbbVtK5bWT3SRaBWw=|0r9TcWfl93tZln8+ZGrPwZTHBbrLiSuVXvoI+cQeSr0=", + "providerOrganizations": [], + "providers": [], + "securityStamp": "6cbf7cd1-d2a2-4c5a-97d7-91042285c49d", + "twoFactorEnabled": false, + "usesKeyConnector": false + }, + "sends": [], + "unofficialServer": true +} +`)) + + httpmock.RegisterResponder("GET", `http://127.0.0.1:8081/api/accounts/profile`, + httpmock.NewStringResponder(200, `{"_status":0,"avatarColor":null,"culture":"en-US","email":"test-202341-argon2@laverse.net","emailVerified":true,"forcePasswordReset":false,"id":"c64a1f8d-223c-40fd-adde-a90d58dbc26c","key":"2.VBpAJrIHYLv60UTYy5e7sA==|WWsozKnKPVvBYttXtlAzpmZPoffwlY3+8Wup9SBdGcO8T4Ybj808DubDhICTPMz9RliDSJ1OuaivBC0rh/7EcWV1s9KuRth5J3XFWJqmtXU=|l1ly+Uek1j4k2xQNT8iJ++GdwMTWRaSBP5mFvgJ+CGU=","masterPasswordHint":null,"name":"test-202341","object":"profile","organizations":[],"premium":true,"premiumFromOrganization":false,"privateKey":"2.0Xc+uV+6YVa+Zkg74C/MJA==|xTbZGy2r8/kHcXXy23YDHWDTHjasUUoHiWJ84rGEhuPD27GTF8DrFL+lbERo3+OH7MXvseAFygKArvSumlCQnMhT/dswJMH8ZEMuxQUZ8I7kg/7eNqxRndxyC8alZh3VM4FC7TsuVBe6BF4pbPQYX4etFL8yOHLpIhwkKcG7+IfQHQHwOXpsSiGpADQlk5er990snhAXwGpgROqoveO9klpNJXuEzJKMFMdoo9FCaRsJ4bB3BE/Y+8Ph7ek8mGyoUUqNFp8Z/T+XN+9kvxDFnVFtYp1p+sU8LpCSvaq3dAImQt6X7vAgfjbWVkTxB0HlBdMkjpg8BAK7qpscT5oH83SZqdNPRi9kkT9Y30xl0QvvJzXLcjqOS6je+i3vB1r4O8X3ZIj+th4cfZOfHKpzDLbGyAezxiiWwQ4xt0USQ9rLv+BU5YNDKnibmsIooKHuh8wV0qht8vr2CEVavXOmmBi/6bGIWJMOs2K52be5LJkYL+653dsKXBN4uahQDMdfs9vPUA7OoIFR9BZvQiGMDstFYVgJUulOkj2J4nieuTAmurQe6nS6U4v1swI7DIzdo8ZCJcAQfWYWk8IBwh6gQxSuPMg7+O2dntbPFkMP2KhoisUucoXNIDXmxwYAMeP01KMNSgFc6hL6WnLzyVcRlSLy7OC9qwTD6ZH6XA4D/MO9MINTTzw4+/pZvwTeuXMqB0HkcjTpWzTUi405uV6qh8CxYOaNEl6kNAMiuVNASToqYb/EBC0uGOybOzcNZOCiXmuiPVOstiz65KP1d3Dfl9al48hpxnDxFyFub5NDx8xADA3SzOlI9xMCsC/mcY0/fRlwoTDFCLzfdtPIdSn5N8/YySY10e/TXftKGV7bLvzGAlOntwoiWaLJbyHAnEXNUVFxJu5XVaVccXcxIKete42S291dCu4yk35KsIQU0jBaPB/hGXLJvvySl9/kARl0zIXJH+Pk0hlk+/IRH9HNZrru93WTgW9KjEeT2vaZ6UqULkkspIfoUrFxQfSyAxycDaqa4EHt1QJBKyC9+aWEOXNwtxkJTnhtvlqRPOGUpdkAyC5ebfdX2URZnd/2TR/PTviWaDKe/g51UWr8QepgwbJjkBeuZMSPCNHkcLdmwEZERisbZ7H50hDQhhK+qfmMOvkVrRRXKT2ICbQzchm5rDasdtMed7vIf3beS0ESIR8kZ1WmteI9dvWmagnXRlfsUZW0y0KWe9Ma/ISOa6QPb0Cc7T8LUg2lpREQpRTt119RX9Xugd2uZ+UzbGpqn9u5JY2+vddwv+zFL0g/Vu9h3kd02v+WTXeV8wLDJYkoc6XeTBzC0uQtNJt4oN7hWcMBrkYmnXxslIjGw/nE7eBnG02XWTCeTjADqXtz2j4xMFmzJod+4j4f6MQZBII9Sz5A2FfSbadUuukEYcFvkqLnjG/sMzcBQMIDP6vmy7VRnETahlNi7Pt+gRLHuhGN1BbScolW7a0YnWlI1T0MtKb4DBsjX9GfotxkG+ORg/i45YnG6KPQ6+jgXU3Xtf0tgjTqHVgc7TOCm9R2b4G24RSQXx3WaMP1slbSYsf/YmXIqdg/dVaCFrJdA1v2bDFymuPbA0P4Ey8c4ylInXGgK4yxS7nVC4qbbVtK5bWT3SRaBWw=|0r9TcWfl93tZln8+ZGrPwZTHBbrLiSuVXvoI+cQeSr0=","providerOrganizations":[],"providers":[],"securityStamp":"6cbf7cd1-d2a2-4c5a-97d7-91042285c49d","twoFactorEnabled":false,"usesKeyConnector":false}`)) + + // We're changing the revisionDate on purpose in the response to mimick Bitwarden official server's behavior + httpmock.RegisterResponder("POST", `http://127.0.0.1:8081/api/ciphers`, + httpmock.NewStringResponder(200, ` + { + "attachments": null, + "card": null, + "collectionIds": [], + "creationDate": "2024-09-22T11:13:40.346903Z", + "data": { + "autofillOnPageLoad": null, + "fields": [ + { + "linkedId": null, + "name": "2.svtF3aK9R1MLHt2GwH7Ykw==|PqVDa02T0Vzx6ogTNz1Xyg==|g9/Gd5OOLkVtyVAk9vkRCY2reWTZjdY1D3XsVpBfQA4=", + "type": 0, + "value": "2.K49DvcymvaQ/+V/3soCb1Q==|r+b2dz0YqFeA8BrHii5a0DIjkwxdwlY+aEEH2d2NdC0=|KEJAjqImC0Jit9oaFN2rSZHyetxuD7jEjMt07HPFLvU=" + } + ], + "name": "2.LjR8NxRtCB1noDILxNKmPQ==|s4e2AO4I3HqmPRRsbsYl0XYSuWu7+sn2K3+jNiFjsW0=|3NQW64/3RmPxe3hhUGeMTrSy9Ruh5hYRlJxIhhWhhSI=", + "notes": "2.ke3IFCPe50UCM2XdJDS/VQ==|ksO+dyEuRBgrpAelfhxNhw==|JrbMU58V5QyiTpdJXyWB1g2l8jPcqyeWDMqjOnaa9UA=", + "password": null, + "passwordHistory": [], + "passwordRevisionDate": null, + "totp": "2.mScxVU7uCx3fiPXYlgzWVA==|yxqIjknG71Qdwxq1wyVufWyB1Hb6qWowPgGoZNV2d4s=|uY7hrn7Eow81A76/n4VUrJxSRtC9VDnUdaesO6y0ivY=", + "uri": "2.7ihoxwsJH4HBkTzjFBwFIA==|5EkQHm4IRExHH/LAU6D/jw==|Hs6YgBIFuTQNnH9M3ejgUAVzsGjxAyaVPi1pl1NU9wg=", + "uris": [ + { + "match": null, + "uri": "2.7ihoxwsJH4HBkTzjFBwFIA==|5EkQHm4IRExHH/LAU6D/jw==|Hs6YgBIFuTQNnH9M3ejgUAVzsGjxAyaVPi1pl1NU9wg=", + "uriChecksum": "2.WfCD+h+rQ49rJDiTM7ycjw==|QgYnnlKzeymjRXmW15SC7bnWWvWaU1iki7rABdi0+RjPgRVGLluT/lQCKXkr5fXH|vaMZaBCKZySaycxVXjpDatKjrN6mBaq2ffRsygLTGzA=" + } + ], + "username": "2.9hmypXsnAjVxJ2e5kZQLKA==|8Jif+MQgdTuAlaCn7i/xig==|xy7zJh4qXutESyC02aKSYb/79EmfbGsYlxsYfKqneLA=" + }, + "deletedDate": null, + "edit": true, + "favorite": false, + "fields": [ + { + "linkedId": null, + "name": "2.svtF3aK9R1MLHt2GwH7Ykw==|PqVDa02T0Vzx6ogTNz1Xyg==|g9/Gd5OOLkVtyVAk9vkRCY2reWTZjdY1D3XsVpBfQA4=", + "type": 0, + "value": "2.K49DvcymvaQ/+V/3soCb1Q==|r+b2dz0YqFeA8BrHii5a0DIjkwxdwlY+aEEH2d2NdC0=|KEJAjqImC0Jit9oaFN2rSZHyetxuD7jEjMt07HPFLvU=" + } + ], + "folderId": null, + "id": "24d1c150-5dfd-4008-964c-01317d1f6b23", + "identity": null, + "key": null, + "login": { + "autofillOnPageLoad": null, + "password": null, + "passwordRevisionDate": null, + "totp": "2.mScxVU7uCx3fiPXYlgzWVA==|yxqIjknG71Qdwxq1wyVufWyB1Hb6qWowPgGoZNV2d4s=|uY7hrn7Eow81A76/n4VUrJxSRtC9VDnUdaesO6y0ivY=", + "uri": "2.7ihoxwsJH4HBkTzjFBwFIA==|5EkQHm4IRExHH/LAU6D/jw==|Hs6YgBIFuTQNnH9M3ejgUAVzsGjxAyaVPi1pl1NU9wg=", + "uris": [ + { + "match": null, + "uri": "2.7ihoxwsJH4HBkTzjFBwFIA==|5EkQHm4IRExHH/LAU6D/jw==|Hs6YgBIFuTQNnH9M3ejgUAVzsGjxAyaVPi1pl1NU9wg=", + "uriChecksum": "2.WfCD+h+rQ49rJDiTM7ycjw==|QgYnnlKzeymjRXmW15SC7bnWWvWaU1iki7rABdi0+RjPgRVGLluT/lQCKXkr5fXH|vaMZaBCKZySaycxVXjpDatKjrN6mBaq2ffRsygLTGzA=" + } + ], + "username": "2.9hmypXsnAjVxJ2e5kZQLKA==|8Jif+MQgdTuAlaCn7i/xig==|xy7zJh4qXutESyC02aKSYb/79EmfbGsYlxsYfKqneLA=" + }, + "name": "2.LjR8NxRtCB1noDILxNKmPQ==|s4e2AO4I3HqmPRRsbsYl0XYSuWu7+sn2K3+jNiFjsW0=|3NQW64/3RmPxe3hhUGeMTrSy9Ruh5hYRlJxIhhWhhSI=", + "notes": "2.ke3IFCPe50UCM2XdJDS/VQ==|ksO+dyEuRBgrpAelfhxNhw==|JrbMU58V5QyiTpdJXyWB1g2l8jPcqyeWDMqjOnaa9UA=", + "object": "cipherDetails", + "organizationId": null, + "organizationUseTotp": true, + "passwordHistory": [], + "reprompt": 0, + "revisionDate": "2024-09-22T11:13:40.345356Z", + "secureNote": null, + "type": 1, + "viewPassword": true + }`)) + + return webapi.NewClient("http://127.0.0.1:8081/", webapi.WithCustomClient(client), webapi.DisableRetries()) +} + func createTestAccount(t *testing.T) { ctx := context.Background() - preloginKey, err := keybuilder.BuildPreloginKey(testPassword, testAccount.Email, testAccount.KdfIterations) + preloginKey, err := keybuilder.BuildPreloginKey(testPassword, testAccount.Email, testAccount.KdfConfig) if err != nil { t.Fatal(err) } @@ -563,7 +738,7 @@ func createTestAccount(t *testing.T) { Name: testAccount.Email, MasterPasswordHash: hashedPassword, Key: encryptedEncryptionKey, - KdfIterations: testAccount.KdfIterations, + KdfIterations: testAccount.KdfConfig.KdfIterations, Keys: webapi.KeyPair{ PublicKey: publicKey, EncryptedPrivateKey: encryptedPrivateKey, diff --git a/internal/bitwarden/embedded/vault_webapi.go b/internal/bitwarden/embedded/vault_webapi.go index 3c7ecd1..b8b91e5 100644 --- a/internal/bitwarden/embedded/vault_webapi.go +++ b/internal/bitwarden/embedded/vault_webapi.go @@ -28,7 +28,7 @@ type WebAPIVault interface { GetAttachment(ctx context.Context, itemId, attachmentId string) ([]byte, error) LoginWithAPIKey(ctx context.Context, password, clientId, clientSecret string) error LoginWithPassword(ctx context.Context, username, password string) error - RegisterUser(ctx context.Context, name, username, password string, kdfIterations int) error + RegisterUser(ctx context.Context, name, username, password string, kdfConfig models.KdfConfiguration) error Sync(ctx context.Context) error Unlock(ctx context.Context, password string) error } @@ -471,7 +471,12 @@ func (v *webAPIVault) LoginWithAPIKey(ctx context.Context, password, clientId, c return fmt.Errorf("error login with api key: %w", err) } - return v.continueLoginWithTokens(ctx, *tokenResp, password) + kdfConfig := models.KdfConfiguration{ + KdfType: tokenResp.Kdf, + KdfIterations: tokenResp.KdfIterations, + } + + return v.continueLoginWithTokens(ctx, *tokenResp, password, kdfConfig) } func (v *webAPIVault) LoginWithPassword(ctx context.Context, username, password string) error { @@ -480,16 +485,23 @@ func (v *webAPIVault) LoginWithPassword(ctx context.Context, username, password return fmt.Errorf("error prelogin with username/password: %w", err) } - tokenResp, err := v.client.LoginWithPassword(ctx, username, password, preResp.KdfIterations) + kdfConfig := models.KdfConfiguration{ + KdfType: preResp.Kdf, + KdfIterations: preResp.KdfIterations, + KdfMemory: preResp.KdfMemory, + KdfParallelism: preResp.KdfParallelism, + } + + tokenResp, err := v.client.LoginWithPassword(ctx, username, password, kdfConfig) if err != nil { return fmt.Errorf("error login with username/password: %w", err) } - return v.continueLoginWithTokens(ctx, *tokenResp, password) + return v.continueLoginWithTokens(ctx, *tokenResp, password, kdfConfig) } -func (v *webAPIVault) RegisterUser(ctx context.Context, name, username, password string, kdfIterations int) error { - preloginKey, err := keybuilder.BuildPreloginKey(password, username, kdfIterations) +func (v *webAPIVault) RegisterUser(ctx context.Context, name, username, password string, kdfConfig models.KdfConfiguration) error { + preloginKey, err := keybuilder.BuildPreloginKey(password, username, kdfConfig) if err != nil { return fmt.Errorf("error building prelogin key: %w", err) } @@ -511,7 +523,10 @@ func (v *webAPIVault) RegisterUser(ctx context.Context, name, username, password Name: name, MasterPasswordHash: hashedPassword, Key: encryptedEncryptionKey, - KdfIterations: kdfIterations, + Kdf: kdfConfig.KdfType, + KdfIterations: kdfConfig.KdfIterations, + KdfMemory: kdfConfig.KdfMemory, + KdfParallelism: kdfConfig.KdfParallelism, Keys: webapi.KeyPair{ PublicKey: publicKey, EncryptedPrivateKey: encryptedPrivateKey, @@ -567,11 +582,10 @@ func (v *webAPIVault) Unlock(ctx context.Context, password string) error { return nil } -func (v *webAPIVault) continueLoginWithTokens(ctx context.Context, tokenResp webapi.TokenResponse, password string) error { +func (v *webAPIVault) continueLoginWithTokens(ctx context.Context, tokenResp webapi.TokenResponse, password string, kdfConfig models.KdfConfiguration) error { v.loginAccount = Account{ VaultFormat: "API", - KdfIterations: tokenResp.KdfIterations, - KdfType: tokenResp.Kdf, + KdfConfig: kdfConfig, ProtectedRSAPrivateKey: tokenResp.PrivateKey, ProtectedSymmetricKey: tokenResp.Key, } diff --git a/internal/bitwarden/embedded/vault_webapi_test.go b/internal/bitwarden/embedded/vault_webapi_test.go index 31c6648..6ae757c 100644 --- a/internal/bitwarden/embedded/vault_webapi_test.go +++ b/internal/bitwarden/embedded/vault_webapi_test.go @@ -14,20 +14,41 @@ func TestLoginAsPasswordLoadsAccountInformation(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - vault := NewWebAPIVaultWithMockedTransport(t) + vault := NewMockedWebAPIVault(t, mockedClient()) err := vault.LoginWithPassword(ctx, "test@laverse.net", testPassword) if err != nil { t.Fatalf("vault unlock failed: %v", err) } - assert.Equal(t, vault.loginAccount.VaultFormat, "API") - assert.Equal(t, vault.loginAccount.Email, "test@laverse.net") - assert.Equal(t, vault.loginAccount.AccountUUID, "e8dababd-242e-4900-becf-e88bc021dda8") - assert.Equal(t, vault.loginAccount.KdfType, 0) - assert.Equal(t, vault.loginAccount.KdfIterations, 600000) - assert.Equal(t, vault.loginAccount.ProtectedRSAPrivateKey, "2.D2aLa8ne/DAkeSzctQISVw==|/xoGM5i5JGJTH/vohUuwTFrTx3hd/gt3kBD/FQdeLMtYjl1u96sh0ECmoERqGHeSfXj+iAb9kpTIOKG8LwmkZGUJBI90Mw0M7ODmf7E8eQ+aGF+bGqTSMQ1wtpunEyFVodlg92YN8Ddlb2V9J4uN8ykpHYNDmQiYLZ8bl6vCODRGPyzLvx5M8DbITVL5PhsjKDLLrVV8lFCgCcAL5YLfkghYhFELyX15zXA/KYEnwggDka3hG5+HHFOVZSeyk7Gi6M4TX2wADbTXz1/Wsho8oxFUrtNiOB3ZiY2cx9UWttpzMXoGfi2gJcP1db/nTfWenOLlzw6Od4VyRzsXsfyGwbqBqDnNFkjLvhjVw4JO+psF//xAMDs14101Tf2wFkB6toQ+zdnDphXUeKmiVPQ7gMnQlOWN5tWvjjmYOO4Y63sGpP24cDOdEScIdebZRSA8uOhTzadfKfOiH5zVYZzXs33FQ0li4nBrsj5xYa6PP4D1P7gqjxClPdguwkdLoZ7JvgIlyRIwEcORi5Ich8RWF/kqRBwk0QSzK1mTlHHU5xtSgi4MLNVx4qTYNaJVBtwL9d2MD9LeNn4Z2PL4A7qnszHqsERiQQDxNEgMxMBHDgSXqQbtQxRvsI6oY+yNbN7uVWw4o8AC3f5GBdxzIqcN1mgEM5ix5aDt15w3MhP2FHtf2neKI68TnL8WnT1fT8BVlbECIiUqK0tfq5aTjdSh3gCS15jvZ1H60h+K8+O/nDfquzVjY7UsTGwA+UtS8/JGiaUhc0VhxJo8P1V2VSCiu51d5q3De1vDg5R2VEgBmTchZyTIodC+3+7ACTOwkNCCdIJN7xKOcIGFA7QOuyJtBeXT4Rd9UGMHSL054IB/315WVDiwrP9W1aP0nHzFs+qAXbH5o1E+AmfMDyoHopjGgbUw22r837kHzf5Qe8QhRYPzQvDowfCPhdoy23cbN1VsNavNwTC9hcG5oYMwkK66xP3MEM9UfGD22pwxe7M8U4BRdLCCHbi95eklXE6Mg6DpWsAdMgokQbOvnlwgKfrlbltqXUE/vUQI8TB3AE1Nkt0ST4quTriMuuyiHdeeZV4UkV9jWt/5bTCfdrCYuGZP7g4shbfNcP7u1Zrdxv+EuUwGIOOTrNV5awmBnL3iHE5ya2MnqmRyfWiPIT5majZCk06yxj4XzyIPOpjYKFt0MOgLvG1GllmdtRqg7tMVvc5ZFo5KWIxLsIJD12UjA1GYYoFdX4+wsNbPjfnlE6D2PrtWUICnBFJzYpfyrKTe01k8G8hyyz+tVzBRfz8EA2ew1+hlVcAgSPCcBzhDgqPe+RSPi7ZSd66be1gDhGAftWFM8Z0MrMklXi2DyjjaKBNsZZD8qTcLcobm8nqHUQtnr5JCbmgP3rau8NY/fxeFHsvSiZQoB1aI/y+Sz/R4r+T9cg8hjmS/FUHDO+m6a6nuWNFwz8wIluM557oOTl+A9UGFF50Gpzmf97VdQjM3ZREazQ7la6AobzS3BHI6FNdxN9LTyMpYo+WODv52/VwU3ODH7wf5bz2OHZhk2NG5R7pSH7qg8jM+/MtJkFumENV0qMecozIkP6e4CyI9ua4YwI9n7G5OgKYMG1aj2PRSny2JSLS8aHF1TkRL8SD0nZFCox0=|muEtiwIuZxhuuLv0nouEdxHU2CO+I7JXKZuYHWiv/OE=") - assert.Equal(t, vault.loginAccount.ProtectedSymmetricKey, "2.lkAJiJtCKPHFPrZ96+j2Xg==|5XJtrKUndcGy28thFukrmgMcLp+BOVdkF+KcuOnfshq9AN1PFhna9Es96CVARCnjTcWuHuqvgnGmcOHTrf8fyfLv63VBsjLgLZk8rCXJoKE=|9dwgx4/13AD+elE2vE7vlSQoe8LbCGGlui345YrKvXY=") + assert.Equal(t, "API", vault.loginAccount.VaultFormat) + assert.Equal(t, "test@laverse.net", vault.loginAccount.Email) + assert.Equal(t, "e8dababd-242e-4900-becf-e88bc021dda8", vault.loginAccount.AccountUUID) + assert.Equal(t, models.KdfTypePBKDF2_SHA256, vault.loginAccount.KdfConfig.KdfType) + assert.Equal(t, 600000, vault.loginAccount.KdfConfig.KdfIterations) + assert.Equal(t, "2.D2aLa8ne/DAkeSzctQISVw==|/xoGM5i5JGJTH/vohUuwTFrTx3hd/gt3kBD/FQdeLMtYjl1u96sh0ECmoERqGHeSfXj+iAb9kpTIOKG8LwmkZGUJBI90Mw0M7ODmf7E8eQ+aGF+bGqTSMQ1wtpunEyFVodlg92YN8Ddlb2V9J4uN8ykpHYNDmQiYLZ8bl6vCODRGPyzLvx5M8DbITVL5PhsjKDLLrVV8lFCgCcAL5YLfkghYhFELyX15zXA/KYEnwggDka3hG5+HHFOVZSeyk7Gi6M4TX2wADbTXz1/Wsho8oxFUrtNiOB3ZiY2cx9UWttpzMXoGfi2gJcP1db/nTfWenOLlzw6Od4VyRzsXsfyGwbqBqDnNFkjLvhjVw4JO+psF//xAMDs14101Tf2wFkB6toQ+zdnDphXUeKmiVPQ7gMnQlOWN5tWvjjmYOO4Y63sGpP24cDOdEScIdebZRSA8uOhTzadfKfOiH5zVYZzXs33FQ0li4nBrsj5xYa6PP4D1P7gqjxClPdguwkdLoZ7JvgIlyRIwEcORi5Ich8RWF/kqRBwk0QSzK1mTlHHU5xtSgi4MLNVx4qTYNaJVBtwL9d2MD9LeNn4Z2PL4A7qnszHqsERiQQDxNEgMxMBHDgSXqQbtQxRvsI6oY+yNbN7uVWw4o8AC3f5GBdxzIqcN1mgEM5ix5aDt15w3MhP2FHtf2neKI68TnL8WnT1fT8BVlbECIiUqK0tfq5aTjdSh3gCS15jvZ1H60h+K8+O/nDfquzVjY7UsTGwA+UtS8/JGiaUhc0VhxJo8P1V2VSCiu51d5q3De1vDg5R2VEgBmTchZyTIodC+3+7ACTOwkNCCdIJN7xKOcIGFA7QOuyJtBeXT4Rd9UGMHSL054IB/315WVDiwrP9W1aP0nHzFs+qAXbH5o1E+AmfMDyoHopjGgbUw22r837kHzf5Qe8QhRYPzQvDowfCPhdoy23cbN1VsNavNwTC9hcG5oYMwkK66xP3MEM9UfGD22pwxe7M8U4BRdLCCHbi95eklXE6Mg6DpWsAdMgokQbOvnlwgKfrlbltqXUE/vUQI8TB3AE1Nkt0ST4quTriMuuyiHdeeZV4UkV9jWt/5bTCfdrCYuGZP7g4shbfNcP7u1Zrdxv+EuUwGIOOTrNV5awmBnL3iHE5ya2MnqmRyfWiPIT5majZCk06yxj4XzyIPOpjYKFt0MOgLvG1GllmdtRqg7tMVvc5ZFo5KWIxLsIJD12UjA1GYYoFdX4+wsNbPjfnlE6D2PrtWUICnBFJzYpfyrKTe01k8G8hyyz+tVzBRfz8EA2ew1+hlVcAgSPCcBzhDgqPe+RSPi7ZSd66be1gDhGAftWFM8Z0MrMklXi2DyjjaKBNsZZD8qTcLcobm8nqHUQtnr5JCbmgP3rau8NY/fxeFHsvSiZQoB1aI/y+Sz/R4r+T9cg8hjmS/FUHDO+m6a6nuWNFwz8wIluM557oOTl+A9UGFF50Gpzmf97VdQjM3ZREazQ7la6AobzS3BHI6FNdxN9LTyMpYo+WODv52/VwU3ODH7wf5bz2OHZhk2NG5R7pSH7qg8jM+/MtJkFumENV0qMecozIkP6e4CyI9ua4YwI9n7G5OgKYMG1aj2PRSny2JSLS8aHF1TkRL8SD0nZFCox0=|muEtiwIuZxhuuLv0nouEdxHU2CO+I7JXKZuYHWiv/OE=", vault.loginAccount.ProtectedRSAPrivateKey) + assert.Equal(t, "2.lkAJiJtCKPHFPrZ96+j2Xg==|5XJtrKUndcGy28thFukrmgMcLp+BOVdkF+KcuOnfshq9AN1PFhna9Es96CVARCnjTcWuHuqvgnGmcOHTrf8fyfLv63VBsjLgLZk8rCXJoKE=|9dwgx4/13AD+elE2vE7vlSQoe8LbCGGlui345YrKvXY=", vault.loginAccount.ProtectedSymmetricKey) +} + +func TestLoginAsPasswordLoadsAccountInformationForArgon2(t *testing.T) { + ctx := context.Background() + httpmock.Activate() + defer httpmock.DeactivateAndReset() + + vault := NewMockedWebAPIVault(t, mockedClientArgon2()) + + err := vault.LoginWithPassword(ctx, "test-202341-argon2@laverse.net", "test1234") + if err != nil { + t.Fatalf("vault unlock failed: %v", err) + } + + assert.Equal(t, "API", vault.loginAccount.VaultFormat) + assert.Equal(t, "test-202341-argon2@laverse.net", vault.loginAccount.Email) + assert.Equal(t, "c64a1f8d-223c-40fd-adde-a90d58dbc26c", vault.loginAccount.AccountUUID) + assert.Equal(t, models.KdfTypeArgon2, vault.loginAccount.KdfConfig.KdfType) + assert.Equal(t, 3, vault.loginAccount.KdfConfig.KdfIterations) + assert.Equal(t, "2.0Xc+uV+6YVa+Zkg74C/MJA==|xTbZGy2r8/kHcXXy23YDHWDTHjasUUoHiWJ84rGEhuPD27GTF8DrFL+lbERo3+OH7MXvseAFygKArvSumlCQnMhT/dswJMH8ZEMuxQUZ8I7kg/7eNqxRndxyC8alZh3VM4FC7TsuVBe6BF4pbPQYX4etFL8yOHLpIhwkKcG7+IfQHQHwOXpsSiGpADQlk5er990snhAXwGpgROqoveO9klpNJXuEzJKMFMdoo9FCaRsJ4bB3BE/Y+8Ph7ek8mGyoUUqNFp8Z/T+XN+9kvxDFnVFtYp1p+sU8LpCSvaq3dAImQt6X7vAgfjbWVkTxB0HlBdMkjpg8BAK7qpscT5oH83SZqdNPRi9kkT9Y30xl0QvvJzXLcjqOS6je+i3vB1r4O8X3ZIj+th4cfZOfHKpzDLbGyAezxiiWwQ4xt0USQ9rLv+BU5YNDKnibmsIooKHuh8wV0qht8vr2CEVavXOmmBi/6bGIWJMOs2K52be5LJkYL+653dsKXBN4uahQDMdfs9vPUA7OoIFR9BZvQiGMDstFYVgJUulOkj2J4nieuTAmurQe6nS6U4v1swI7DIzdo8ZCJcAQfWYWk8IBwh6gQxSuPMg7+O2dntbPFkMP2KhoisUucoXNIDXmxwYAMeP01KMNSgFc6hL6WnLzyVcRlSLy7OC9qwTD6ZH6XA4D/MO9MINTTzw4+/pZvwTeuXMqB0HkcjTpWzTUi405uV6qh8CxYOaNEl6kNAMiuVNASToqYb/EBC0uGOybOzcNZOCiXmuiPVOstiz65KP1d3Dfl9al48hpxnDxFyFub5NDx8xADA3SzOlI9xMCsC/mcY0/fRlwoTDFCLzfdtPIdSn5N8/YySY10e/TXftKGV7bLvzGAlOntwoiWaLJbyHAnEXNUVFxJu5XVaVccXcxIKete42S291dCu4yk35KsIQU0jBaPB/hGXLJvvySl9/kARl0zIXJH+Pk0hlk+/IRH9HNZrru93WTgW9KjEeT2vaZ6UqULkkspIfoUrFxQfSyAxycDaqa4EHt1QJBKyC9+aWEOXNwtxkJTnhtvlqRPOGUpdkAyC5ebfdX2URZnd/2TR/PTviWaDKe/g51UWr8QepgwbJjkBeuZMSPCNHkcLdmwEZERisbZ7H50hDQhhK+qfmMOvkVrRRXKT2ICbQzchm5rDasdtMed7vIf3beS0ESIR8kZ1WmteI9dvWmagnXRlfsUZW0y0KWe9Ma/ISOa6QPb0Cc7T8LUg2lpREQpRTt119RX9Xugd2uZ+UzbGpqn9u5JY2+vddwv+zFL0g/Vu9h3kd02v+WTXeV8wLDJYkoc6XeTBzC0uQtNJt4oN7hWcMBrkYmnXxslIjGw/nE7eBnG02XWTCeTjADqXtz2j4xMFmzJod+4j4f6MQZBII9Sz5A2FfSbadUuukEYcFvkqLnjG/sMzcBQMIDP6vmy7VRnETahlNi7Pt+gRLHuhGN1BbScolW7a0YnWlI1T0MtKb4DBsjX9GfotxkG+ORg/i45YnG6KPQ6+jgXU3Xtf0tgjTqHVgc7TOCm9R2b4G24RSQXx3WaMP1slbSYsf/YmXIqdg/dVaCFrJdA1v2bDFymuPbA0P4Ey8c4ylInXGgK4yxS7nVC4qbbVtK5bWT3SRaBWw=|0r9TcWfl93tZln8+ZGrPwZTHBbrLiSuVXvoI+cQeSr0=", vault.loginAccount.ProtectedRSAPrivateKey) + assert.Equal(t, "2.VBpAJrIHYLv60UTYy5e7sA==|WWsozKnKPVvBYttXtlAzpmZPoffwlY3+8Wup9SBdGcO8T4Ybj808DubDhICTPMz9RliDSJ1OuaivBC0rh/7EcWV1s9KuRth5J3XFWJqmtXU=|l1ly+Uek1j4k2xQNT8iJ++GdwMTWRaSBP5mFvgJ+CGU=", vault.loginAccount.ProtectedSymmetricKey) } func TestObjectCreation(t *testing.T) { @@ -35,7 +56,7 @@ func TestObjectCreation(t *testing.T) { httpmock.Activate() defer httpmock.DeactivateAndReset() - vault := NewWebAPIVaultWithMockedTransport(t) + vault := NewMockedWebAPIVault(t, mockedClient()) err := vault.LoginWithPassword(ctx, "test@laverse.net", testPassword) if err != nil { diff --git a/internal/bitwarden/models/models.go b/internal/bitwarden/models/models.go index ede7131..63a1ff7 100644 --- a/internal/bitwarden/models/models.go +++ b/internal/bitwarden/models/models.go @@ -19,6 +19,20 @@ const ( ItemTypeSecureNote ItemType = 2 ) +type KdfType int + +const ( + KdfTypePBKDF2_SHA256 KdfType = 0 + KdfTypeArgon2 KdfType = 1 +) + +type KdfConfiguration struct { + KdfIterations int `json:"kdfIterations,omitempty"` + KdfMemory int `json:"kdfMemory,omitempty"` + KdfParallelism int `json:"kdfParallelism,omitempty"` + KdfType KdfType `json:"kdfType,omitempty"` +} + type FieldType int const ( diff --git a/internal/bitwarden/webapi/client.go b/internal/bitwarden/webapi/client.go index c12cdbc..2286bda 100644 --- a/internal/bitwarden/webapi/client.go +++ b/internal/bitwarden/webapi/client.go @@ -41,7 +41,7 @@ type Client interface { GetContentFromURL(ctx context.Context, url string) ([]byte, error) GetObjectAttachment(ctx context.Context, itemId, attachmentId string) (*models.Attachment, error) LoginWithAPIKey(ctx context.Context, clientId, clientSecret string) (*TokenResponse, error) - LoginWithPassword(ctx context.Context, username, password string, kdfIterations int) (*TokenResponse, error) + LoginWithPassword(ctx context.Context, username, password string, kdfConfig models.KdfConfiguration) (*TokenResponse, error) PreLogin(context.Context, string) (*PreloginResponse, error) Profile(context.Context) (*Profile, error) RegisterUser(ctx context.Context, req SignupRequest) error @@ -256,8 +256,8 @@ func (c *client) GetCollections(ctx context.Context, orgID string) ([]Collection return resp.Data, nil } -func (c *client) LoginWithPassword(ctx context.Context, username, password string, kdfIterations int) (*TokenResponse, error) { - preloginKey, err := keybuilder.BuildPreloginKey(password, username, kdfIterations) +func (c *client) LoginWithPassword(ctx context.Context, username, password string, kdfConfig models.KdfConfiguration) (*TokenResponse, error) { + preloginKey, err := keybuilder.BuildPreloginKey(password, username, kdfConfig) if err != nil { return nil, fmt.Errorf("error building prelogin key: %w", err) } @@ -436,8 +436,8 @@ func doRequest[T any](ctx context.Context, httpClient *retryablehttp.Client, htt fmt.Printf("Body to unmarshall: %s\n", string(body)) return nil, fmt.Errorf("error unmarshalling response from '%s': %w", httpReq.URL, err) } - tflog.Trace(ctx, "Response from Bitwarden server", map[string]interface{}{"url": httpReq.URL.RequestURI(), "body": string(body)}) - + debugInfo := map[string]interface{}{"url": httpReq.URL.RequestURI(), "body": string(body)} + tflog.Trace(ctx, "Response from Bitwarden server", debugInfo) return &res, nil } diff --git a/internal/bitwarden/webapi/models.go b/internal/bitwarden/webapi/models.go index 78fdb7d..45205bc 100644 --- a/internal/bitwarden/webapi/models.go +++ b/internal/bitwarden/webapi/models.go @@ -8,13 +8,15 @@ import ( ) type SignupRequest struct { - Email string `json:"email"` - Name string `json:"name"` - MasterPasswordHash string `json:"masterPasswordHash"` - Key string `json:"key"` - Kdf int `json:"kdf"` - KdfIterations int `json:"kdfIterations"` - Keys KeyPair `json:"keys"` + Email string `json:"email"` + Name string `json:"name"` + MasterPasswordHash string `json:"masterPasswordHash"` + Key string `json:"key"` + Kdf models.KdfType `json:"kdf"` + KdfIterations int `json:"kdfIterations"` + KdfMemory int `json:"kdfMemory"` + KdfParallelism int `json:"kdfParallelism"` + Keys KeyPair `json:"keys"` } type KeyPair struct { @@ -61,23 +63,23 @@ type CreateOrganizationResponse struct { Id string `json:"id"` } type PreloginResponse struct { - Kdf int `json:"kdf"` - KdfIterations int `json:"kdfIterations"` - KdfMemory int `json:"kdfMemory"` - KdfParallelism int `json:"kdfParallelism"` + Kdf models.KdfType `json:"kdf"` + KdfIterations int `json:"kdfIterations"` + KdfMemory int `json:"kdfMemory"` + KdfParallelism int `json:"kdfParallelism"` } type TokenResponse struct { - Kdf int `json:"Kdf"` - KdfIterations int `json:"KdfIterations"` - Key string `json:"Key"` - PrivateKey string `json:"PrivateKey"` - ResetMasterPassword bool `json:"ResetMasterPassword"` - AccessToken string `json:"access_token"` - ExpireIn int `json:"expires_in"` - RefreshToken string `json:"refresh_token"` - Scope string `json:"scope"` - TokenType string `json:"token_type"` - UnofficialServer bool `json:"unofficialServer"` + Kdf models.KdfType `json:"Kdf"` + KdfIterations int `json:"KdfIterations"` + Key string `json:"Key"` + PrivateKey string `json:"PrivateKey"` + ResetMasterPassword bool `json:"ResetMasterPassword"` + AccessToken string `json:"access_token"` + ExpireIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` + TokenType string `json:"token_type"` + UnofficialServer bool `json:"unofficialServer"` RSAPrivateKey *rsa.PrivateKey } diff --git a/internal/provider/provider_utils_test.go b/internal/provider/provider_utils_test.go index ca7be95..1591e94 100644 --- a/internal/provider/provider_utils_test.go +++ b/internal/provider/provider_utils_test.go @@ -86,7 +86,7 @@ func ensureVaultwardenConfigured(t *testing.T) { } webapiClient := webapi.NewClient(testServerURL) - _, err = webapiClient.LoginWithPassword(ctx, testEmail, testPassword, kdfIterations) + _, err = webapiClient.LoginWithPassword(ctx, testEmail, testPassword, models.KdfConfiguration{KdfIterations: kdfIterations}) if err != nil { t.Fatal(err) } @@ -136,10 +136,29 @@ func ensureVaultwardenHasUser(t *testing.T) { client := embedded.NewWebAPIVault(testServerURL) testUsername = fmt.Sprintf("test-%s", testUniqueIdentifier) testEmail = fmt.Sprintf("test-%s@laverse.net", testUniqueIdentifier) - err := client.RegisterUser(context.Background(), testUsername, testEmail, testPassword, kdfIterations) + kdfConfig := models.KdfConfiguration{ + KdfType: models.KdfTypePBKDF2_SHA256, + KdfIterations: kdfIterations, + KdfMemory: 0, + KdfParallelism: 0, + } + err := client.RegisterUser(context.Background(), testUsername, testEmail, testPassword, kdfConfig) + if err != nil && !strings.Contains(strings.ToLower(err.Error()), "user already exists") { + t.Fatal(err) + } + + testEmail2 := fmt.Sprintf("test-%s-argon2@laverse.net", testUniqueIdentifier) + kdfConfig = models.KdfConfiguration{ + KdfType: models.KdfTypeArgon2, + KdfIterations: 3, + KdfMemory: 64, + KdfParallelism: 4, + } + err = client.RegisterUser(context.Background(), testUsername, testEmail2, testPassword, kdfConfig) if err != nil && !strings.Contains(strings.ToLower(err.Error()), "user already exists") { t.Fatal(err) } + t.Logf("Created test user (argon2) %s", testEmail2) isUserCreated = true }