Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues with Argon2 #163

Merged
merged 3 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions internal/bitwarden/crypto/keybuilder/prelogin_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,31 @@ func BuildPreloginKey(masterPassword, email string, kdfConfig models.KdfConfigur
}

func buildKey(masterPassword, salt string, kdfConfig models.KdfConfiguration) (*symmetrickey.Key, error) {
var rawKey []byte
switch kdfConfig.KdfType {
case models.KdfTypePBKDF2_SHA256:
return symmetrickey.NewFromRawBytes(pbkdf2.Key([]byte(masterPassword), []byte(salt), kdfConfig.KdfIterations, 32, sha256.New))
rawKey = 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))

var err error
err = func() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("argon2.IDKey() panicked: %v", r)
}
}()

rawKey = argon2.IDKey([]byte(masterPassword), hashedSalt.Sum(nil), uint32(kdfConfig.KdfIterations), uint32(kdfConfig.KdfMemory*1024), uint8(kdfConfig.KdfParallelism), 32)
return
}()
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported KDF: '%d'", kdfConfig.KdfType)
}

return symmetrickey.NewFromRawBytes(rawKey)
}
32 changes: 32 additions & 0 deletions internal/bitwarden/crypto/keybuilder/prelogin_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package keybuilder

import (
"testing"

"github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models"
"github.com/stretchr/testify/assert"
)

func TestArgon2(t *testing.T) {
key, err := buildKey("test1234", "somesalt", models.KdfConfiguration{
KdfType: models.KdfTypeArgon2,
KdfIterations: 3,
KdfMemory: 64,
KdfParallelism: 4,
})

assert.NoError(t, err)
assert.Equal(t, "Key: CAgX8/OUnQXSAzipUdqaQ9CFCflNf2lowXvfbpzNmXU=\nEncryptionKey: CAgX8/OUnQXSAzipUdqaQ9CFCflNf2lowXvfbpzNmXU=\nMacKey: \n", key.Summary())
}

func TestArgon2WithTooLittleParallelism(t *testing.T) {
key, err := buildKey("test1234", "somesalt", models.KdfConfiguration{
KdfType: models.KdfTypeArgon2,
KdfIterations: 3,
KdfMemory: 64,
KdfParallelism: 0,
})

assert.Errorf(t, err, "panicked: argon2: parallelism degree too low")
assert.Nil(t, key)
}
125 changes: 125 additions & 0 deletions internal/bitwarden/embedded/fixtures/create_accounts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package fixtures

import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"net/http"
"strings"
"testing"

"github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/crypto"
"github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/crypto/keybuilder"
"github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/embedded"
"github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/models"
"github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/webapi"
)

// This is only used to generate test data
func TestCreateTestAccounts(t *testing.T) {
t.Skip()
createTestAccount(t, Pdkdf2Email, models.KdfConfiguration{
KdfType: models.KdfTypePBKDF2_SHA256,
KdfIterations: 600000,
})
createTestAccount(t, Argon2Email, models.KdfConfiguration{
KdfType: models.KdfTypeArgon2,
KdfIterations: 3,
KdfMemory: 64,
KdfParallelism: 4,
})
}

func createTestAccount(t *testing.T, accountEmail string, kdfConfig models.KdfConfiguration) {
ctx := context.Background()

mockName := strings.Split(accountEmail, "@")[0]

preloginKey, err := keybuilder.BuildPreloginKey(TestPassword, accountEmail, kdfConfig)
if err != nil {
t.Fatal(err)
}

hashedPassword := crypto.HashPassword(TestPassword, *preloginKey, false)

block, _ := pem.Decode([]byte(RsaPrivateKey))
if block == nil {
t.Fatal(err)
}

privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
t.Fatal(err)
}

encryptionKeyBytes, err := base64.StdEncoding.DecodeString(EncryptionKey)
if err != nil {
t.Fatal(err)
}

newEncryptionKey, encryptedEncryptionKey, err := keybuilder.EncryptEncryptionKey(*preloginKey, encryptionKeyBytes)
if err != nil {
t.Fatal(err)
}

publicKey, encryptedPrivateKey, err := keybuilder.EncryptRSAKeyPair(*newEncryptionKey, privateKey)
if err != nil {
t.Fatal(err)
}

signupRequest := webapi.SignupRequest{
Email: accountEmail,
Name: accountEmail,
MasterPasswordHash: hashedPassword,
Key: encryptedEncryptionKey,
Kdf: kdfConfig.KdfType,
KdfIterations: kdfConfig.KdfIterations,
KdfMemory: kdfConfig.KdfMemory,
KdfParallelism: kdfConfig.KdfParallelism,
Keys: webapi.KeyPair{
PublicKey: publicKey,
EncryptedPrivateKey: encryptedPrivateKey,
},
}

client := webapi.NewClient(ServerURL)
err = client.RegisterUser(ctx, signupRequest)
if err != nil && !strings.Contains(err.Error(), "Registration not allowed or user already exists") {
t.Fatal(err)
}

httpClient := http.Client{
Transport: &diskTransport{
Prefix: mockName,
},
}
vault := embedded.NewWebAPIVault(ServerURL, embedded.WithHttpOptions(webapi.WithCustomClient(httpClient)))

err = vault.LoginWithPassword(ctx, accountEmail, TestPassword)
if err != nil {
t.Fatal(err)
}

apiKey, err := vault.GetAPIKey(ctx, accountEmail, TestPassword)
if err != nil {
t.Fatal(err)
}

err = vault.LoginWithAPIKey(ctx, TestPassword, apiKey.ClientID, apiKey.ClientSecret)
if err != nil {
t.Fatal(err)
}

_, err = vault.CreateObject(ctx, models.Object{
Object: models.ObjectTypeItem,
Type: models.ItemTypeLogin,
Name: "Item in own Vault",
Login: models.Login{
Username: "my-username",
},
})
if err != nil {
t.Fatal(err)
}
}
70 changes: 70 additions & 0 deletions internal/bitwarden/embedded/fixtures/disk_transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package fixtures

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
)

type diskTransport struct {
Transport http.RoundTripper
Prefix string
}

func (d *diskTransport) RoundTrip(req *http.Request) (*http.Response, error) {
transport := d.Transport
if transport == nil {
transport = http.DefaultTransport
}

resp, err := transport.RoundTrip(req)
if err != nil {
return nil, err
}

if err := d.saveResponseToFile(req, resp); err != nil {
return nil, fmt.Errorf("error saving response to file: %w", err)
}

return resp, nil
}

func (d *diskTransport) saveResponseToFile(req *http.Request, resp *http.Response) error {
filename := fmt.Sprintf("%s_%s%s.json", d.Prefix, req.Method, sanitizeFilename(resp.Request.URL.EscapedPath()))

data, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
resp.Body = io.NopCloser(bytes.NewReader(data))

var jsonData map[string]interface{}
if err := json.Unmarshal(data, &jsonData); err != nil {
return err
}

prettyData, err := json.MarshalIndent(jsonData, "", " ")
if err != nil {
return err
}

return os.WriteFile(filename, prettyData, 0644)
}

func sanitizeFilename(url string) string {
replacer := []string{
":", "_",
"/", "_",
"\\", "_",
"?", "_",
"&", "_",
"=", "_",
"%", "_",
}
replacerMap := strings.NewReplacer(replacer...)
return replacerMap.Replace(url)
}
51 changes: 51 additions & 0 deletions internal/bitwarden/embedded/fixtures/mocked_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package fixtures

import (
"fmt"
"net/http"
"os"
"strings"
"testing"

"github.com/jarcoal/httpmock"
"github.com/maxlaverse/terraform-provider-bitwarden/internal/bitwarden/webapi"
)

const (
mockedServerUrl = "http://127.0.0.1:8081"
)

func MockedClient(t *testing.T, name string) webapi.Client {
client := http.Client{Transport: httpmock.DefaultTransport}

files, err := os.ReadDir("fixtures")
if err != nil {
t.Fatal(err)
}

t.Logf("Found %d responders", len(files))
for _, file := range files {
if !strings.HasSuffix(file.Name(), ".json") || !strings.HasPrefix(file.Name(), fmt.Sprintf("%s_", name)) {
continue
}

data, err := os.ReadFile(fmt.Sprintf("%s/%s", "fixtures", file.Name()))
if err != nil {
t.Fatal(err)
}

tmp := strings.Split(file.Name(), "_")
name = tmp[0]
method := strings.ToUpper(tmp[1])
mockUrl := strings.Join(tmp[2:], "/")

mockUrl, _ = strings.CutSuffix(mockUrl, ".json")
mockUrl = fmt.Sprintf("%s/%s", mockedServerUrl, mockUrl)
t.Logf("Registering responder for %s %s", method, mockUrl)

httpmock.RegisterResponder(method, mockUrl,
httpmock.NewStringResponder(200, string(data)))
}

return webapi.NewClient(mockedServerUrl, webapi.WithCustomClient(client), webapi.DisableRetries())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"_status": 0,
"avatarColor": null,
"culture": "en-US",
"email": "[email protected]",
"emailVerified": true,
"forcePasswordReset": false,
"id": "aaf15bd1-4f51-4ba0-ade8-9dc2ec0fd2c3",
"key": "2.fl6CmmK/o/THYg8Y3Z5fJw==|sEjr1DMSe+Hgg7DRU8Z2wqZiZWIySrY63E8ISwQ7Q9vpxUPDQPifTW9oW0ZjcnKE4Xl4fxP89LO5xce+y+yR1yAQpnVcm2wMBx0OXM1xaAU=|9wNu2sNXt7ECSXCYsDL1ctWw02GGT1y7Pkx2NxIyD0A=",
"masterPasswordHint": null,
"name": "[email protected]",
"object": "profile",
"organizations": [],
"premium": true,
"premiumFromOrganization": false,
"privateKey": "2.11j1IvphccY0UkZov7ZbmQ==|4J34a16NM+krEXyDUkHVUNL2sSA7dph4EBCLWoDkmDoljqMZh5IxA7M51yZTNB6/uqGgJZJX0uU+B4RSn2s2JoFG/VizMmgSIJkTIr6WuA7taEzyIFRi7sY980W02pnLeb8fGV8EXrZk1Bj0UzAFukNGoeS2CHWbm0bZKRvDxl5Vw40V3Lt6GzHTb6X+4k4Ovg0UKgm2mqMWzxxjmCfy+G1R0vlsAtqg14n2ax5f3Jn4ajGDvT2BQia9EFYFkie/SfqgsYbtvou4DvyCTEWh0j9Cre1QIJQ9AZ4AHAzGwIKFhpN7sWjsfw6yTGmvxAfflCMFzCdleBBiuCefaAKNe5Uf1QASC9TzIiHknl2+sK9lOFZ+XZJ41HCU2sMvNBu1EHuBPNRIHodCEHdXHyRqBaC+vgWO6RcTWU4K5ZctBKmElAYst7FFCuzM+liUcGp3I1a7wxOJTPraxeFw8z69rYFiNMOng/HkV/G/VUQXzWalC5gtjaR35dMp35Mf1lMlGnWhy/qdLr0NVdl69qUDKt+kCG9+kiXO7Eq5PTbV+tj4AxBzxE1DroAh0jTPFLb7p1205+OQqqrCEq6tzpGHKTFLoyHZCVoxdQvxgFNX6+fNi3+MfbDfLle7msQAwOuaUl6rxcb2JjdgRQQUd4/GQbvKeLjFRIr7OtcVHWm3251jqRLBgFJcQ235SFgdjLnKATqN4BRe0WLyYOMuMQEmfQXaebjzMo7L2JMJeOar9QutuV+Acle7kJZZU4N1XPiUmborUksAcrmLGXvQITn1QLmadou0jX/oov+lkrKDIFTJC5MIutVBNmExS6FqW978viF1ZNgRQxG5OULHoT5tgfyjrl2u74VFf0ttbLC/JSsi1k8me2LA2vi2Sx7DNOzd6fLx0f8+mYmz2VTRw6fnoFh99d8HktPUNyKcGNZyZpsfkkhFSFV/U88tZ9gROWayLbRXS6g801Mi0tBg0RJp/Flo9ZX9Y3qKVWLCDZT6t3l1NR2npkGmi16yqslXSs9hadIvd6lxyYqyW6cskuJjsacHPOCyvz1xZFH6txtzmb46UrKUU7Cjo/09Kmtb740rXzhH/0nPuvJUjlk0UN77LqzvnvVWAe2IJVEjsbhTspLw3Z1fmCQxMKvc3yPA36/KP2uk/wOlHmBnuX/eebQmI22PFOzskl1d0ScNItJsw5D6fkEhybxEY3oPWYG5y+Oitqj1dQy/TNKXeJHMP+MC1PxzvaE3jq/+cb0IwVBnMVblbaHCJM6JAllV/p3Z6/Ho3Ps4hDHRflKZjP2HyB5wvvTtYhX5XpDoA8s9e6TpDXYnoE+XLN4Thzu30GL3WBeI7koYuNEpRhD5++oT2CElpEqo0v/TFKGSmnV+qohWTL8I4WnHSZeKDLC5alqIMYgRkN6qUlVJLzILOFzeJcL9zeXsAeHemd3VZWe0OYiJRxVwBCYGHk6W7WA4pZ+Wq/C0JHaxNJhr9gx0kyFlMPBkwN/LEyUP+PxsqIuW+09f70qZdGE1r9bNjUSKmb53I6nqxwQhIwvpZZDGAOZjaxrfC8PE9rOOTkM3v6zsSWgjl4EJMPwJubHJb63w5hvW2DwS4h3t7lduTab9ykKInvfBsaWxLuBhJuX+fesGTM0KGXs=|BXz0QzgtArYQXICHbYvGqOeFVi9YGZRP0MASNlVwzzY=",
"providerOrganizations": [],
"providers": [],
"securityStamp": "aa485463-c258-40b5-b2cf-bf22bf705258",
"twoFactorEnabled": false,
"usesKeyConnector": false
}
Loading
Loading