Skip to content

Commit

Permalink
Merge pull request #163 from maxlaverse/argon2
Browse files Browse the repository at this point in the history
Fix issues with Argon2
  • Loading branch information
maxlaverse authored Oct 2, 2024
2 parents 97fb684 + 7565b63 commit b63ff64
Show file tree
Hide file tree
Showing 25 changed files with 1,043 additions and 837 deletions.
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

0 comments on commit b63ff64

Please sign in to comment.