🚀 feat(dependencies): add spatie/laravel-ciphersweet package for encryption support

 feat(profile): integrate email and fax fields in association profile

🆕 feat(migrations): create blind_indexes table for encrypted data

🔧 feat(model): implement CipherSweet in EinundzwanzigPleb for email encryption

🔧 config: add ciphersweet configuration file for encryption settings

🗄️ migration: add email field to einundzwanzig_plebs table for user data
This commit is contained in:
fsociety
2024-10-25 16:15:28 +02:00
parent e8817f5e71
commit f600c7983c
7 changed files with 395 additions and 21 deletions

View File

@@ -4,9 +4,14 @@ namespace App\Models;
use App\Enums\AssociationStatus;
use Illuminate\Database\Eloquent\Model;
use ParagonIE\CipherSweet\BlindIndex;
use ParagonIE\CipherSweet\EncryptedRow;
use Spatie\LaravelCipherSweet\Concerns\UsesCipherSweet;
use Spatie\LaravelCipherSweet\Contracts\CipherSweetEncrypted;
class EinundzwanzigPleb extends Model
class EinundzwanzigPleb extends Model implements CipherSweetEncrypted
{
use UsesCipherSweet;
protected $guarded = [];
@@ -17,6 +22,13 @@ class EinundzwanzigPleb extends Model
];
}
public static function configureCipherSweet(EncryptedRow $encryptedRow): void
{
$encryptedRow
->addOptionalTextField('email')
->addBlindIndex('email', new BlindIndex('email_index'));
}
public function profile()
{
return $this->hasOne(Profile::class, 'pubkey', 'pubkey');

View File

@@ -27,6 +27,7 @@
"sentry/sentry-laravel": "^4.9",
"simplesoftwareio/simple-qrcode": "^4.2",
"spatie/image": "^3.7",
"spatie/laravel-ciphersweet": "^1.6",
"spatie/laravel-google-fonts": "^1.4",
"spatie/laravel-markdown": "^2.5",
"spatie/laravel-medialibrary": "^11.9",

203
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bda4dccb94a2861dc6ff18410d22a5ec",
"content-hash": "e21a704ce71a5cc77ae5a1d48a8034bd",
"packages": [
{
"name": "akuechler/laravel-geoly",
@@ -3846,6 +3846,135 @@
],
"time": "2024-09-24T14:04:43+00:00"
},
{
"name": "paragonie/ciphersweet",
"version": "v4.7.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/ciphersweet.git",
"reference": "d7013e61f565c63213251222361ecbe060ec22de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/ciphersweet/zipball/d7013e61f565c63213251222361ecbe060ec22de",
"reference": "d7013e61f565c63213251222361ecbe060ec22de",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-openssl": "*",
"paragonie/constant_time_encoding": "^2|^3",
"paragonie/sodium_compat": "^1|^2",
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^9",
"vimeo/psalm": "^4"
},
"type": "library",
"autoload": {
"psr-4": {
"ParagonIE\\CipherSweet\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"ISC"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com"
}
],
"description": "Searchable field-level encryption library for relational databases",
"keywords": [
"FIPS 140-3",
"NIST cryptography",
"SQL encryption",
"crm",
"cryptography",
"database encryption",
"encrypt",
"encryption",
"field-level encryption",
"libsodium",
"queryable encryption",
"searchable encryption"
],
"support": {
"issues": "https://github.com/paragonie/ciphersweet/issues",
"source": "https://github.com/paragonie/ciphersweet/tree/v4.7.0"
},
"time": "2024-05-11T06:44:22+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512",
"reference": "df1e7fde177501eee2037dd159cf04f5f301a512",
"shasum": ""
},
"require": {
"php": "^8"
},
"require-dev": {
"phpunit/phpunit": "^9",
"vimeo/psalm": "^4|^5"
},
"type": "library",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"time": "2024-05-08T12:36:18+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v9.99.100",
@@ -6540,6 +6669,78 @@
},
"time": "2024-05-16T08:48:33+00:00"
},
{
"name": "spatie/laravel-ciphersweet",
"version": "1.6.2",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-ciphersweet.git",
"reference": "77b5cd8066858529ca611edf99288366efa62b61"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-ciphersweet/zipball/77b5cd8066858529ca611edf99288366efa62b61",
"reference": "77b5cd8066858529ca611edf99288366efa62b61",
"shasum": ""
},
"require": {
"illuminate/contracts": "^9.19|^10.0|^11.0",
"paragonie/ciphersweet": "^4.0.1",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.12.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.8",
"nunomaduro/collision": "^6.0|^8.0",
"nunomaduro/larastan": "^2.0.1",
"orchestra/testbench": "^7.0|^8.0|^9.0",
"pestphp/pest": "^1.21|^2.34",
"pestphp/pest-plugin-laravel": "^1.1|^2.3",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^9.5|^10.5",
"spatie/laravel-ray": "^1.26"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\LaravelCipherSweet\\LaravelCipherSweetServiceProvider"
],
"aliases": {
"LaravelCipherSweet": "Spatie\\LaravelCipherSweet\\Facades\\LaravelCipherSweet"
}
}
},
"autoload": {
"psr-4": {
"Spatie\\LaravelCipherSweet\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Rias Van der Veken",
"email": "rias@spatie.be",
"role": "Developer"
}
],
"description": "Use ciphersweet in your Laravel project",
"homepage": "https://github.com/spatie/laravel-ciphersweet",
"keywords": [
"laravel",
"laravel-ciphersweet",
"spatie"
],
"support": {
"source": "https://github.com/spatie/laravel-ciphersweet/tree/1.6.2"
},
"time": "2024-07-18T13:03:10+00:00"
},
{
"name": "spatie/laravel-google-fonts",
"version": "1.4.1",

54
config/ciphersweet.php Normal file
View File

@@ -0,0 +1,54 @@
<?php
return [
/**
* This controls which cryptographic backend will be used by CipherSweet.
* Unless you have specific compliance requirements, you should choose
* "nacl".
*
* Supported: "boring", "fips", "nacl", "custom"
*/
'backend' => env('CIPHERSWEET_BACKEND', 'nacl'),
/**
* Set backend-specific options here. "custom" points to a factory class that returns a
* backend from its `__invoke` method. Please see the docs for more details.
*/
'backends' => [
// 'custom' => CustomBackendFactory::class,
],
/**
* Select which key provider your application will use. The default option
* is to read a string literal out of .env, but it's also possible to
* provide the key in a file or use random keys for testing.
*
* Supported: "file", "random", "string", "custom"
*/
'provider' => env('CIPHERSWEET_PROVIDER', 'string'),
/**
* Set provider-specific options here. "string" will read the key directly
* from your .env file. "file" will read the contents of the specified file
* to use as your key. "custom" points to a factory class that returns a
* provider from its `__invoke` method. Please see the docs for more details.
*/
'providers' => [
'file' => [
'path' => env('CIPHERSWEET_FILE_PATH'),
],
'string' => [
'key' => env('CIPHERSWEET_KEY'),
],
// 'custom' => CustomKeyProviderFactory::class,
],
/*
* The provided code snippet checks whether the $permitEmpty property is set to false
* for a given field. If it is not set to false, it throws an EmptyFieldException indicating
* that the field is not defined in the row. This ensures that the code enforces the requirement for
* the field to have a value and alerts the user if it is empty or undefined.
* Supported: "true", "false"
*/
'permit_empty' => env('CIPHERSWEET_PERMIT_EMPTY', FALSE)
];

View File

@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('einundzwanzig_plebs', function (Blueprint $table) {
$table->string('email')->unique()->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('einundzwanzig_plebs', function (Blueprint $table) {
//
});
}
};

View File

@@ -0,0 +1,20 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('blind_indexes', function (Blueprint $table) {
$table->morphs('indexable');
$table->string('name');
$table->string('value');
$table->index(['name', 'value']);
$table->unique(['indexable_type', 'indexable_id', 'name']);
});
}
};

View File

@@ -19,13 +19,17 @@ use function Laravel\Folio\{middleware, name};
name('association.profile');
state(['yearsPaid' => []]);
state(['events' => []]);
state(['payments' => []]);
state(['amountToPay' => config('app.env') === 'production' ? 21000 : 1]);
state(['currentYearIsPaid' => false]);
state(['currentPubkey' => null]);
state(['currentPleb' => null]);
state([
'fax' => '',
'email' => '',
'yearsPaid' => [],
'events' => [],
'payments' => [],
'amountToPay' => config('app.env') === 'production' ? 21000 : 1,
'currentYearIsPaid' => false,
'currentPubkey' => null,
'currentPleb' => null,
]);
form(\App\Livewire\Forms\ApplicationForm::class);
@@ -38,6 +42,7 @@ on([
=> $query->where('year', date('Y')),
])
->where('pubkey', $pubkey)->first();
$this->email = $this->currentPleb->email;
if ($this->currentPleb->association_status === \App\Enums\AssociationStatus::ACTIVE) {
$this->amountToPay = config('app.env') === 'production' ? 21000 : 1;
}
@@ -60,6 +65,23 @@ on([
},
]);
updated([
'fax' => function () {
$this->js('alert("Markus Turm wird sich per Fax melden!")');
},
]);
$saveEmail = function () {
$this->validate([
'email' => 'required|email',
]);
$this->currentPleb->update([
'email' => $this->email,
]);
$notification = new Notification($this);
$notification->success('E-Mail Adresse gespeichert.');
};
$pay = function ($comment) {
$paymentEvent = $this->currentPleb
->paymentEvents()
@@ -413,18 +435,55 @@ $loadEvents = function () {
<section>
@if($currentPleb && $currentPleb->association_status->value > 1)
<div
class="inline-flex flex-col w-full max-w-lg px-4 py-2 rounded-lg text-sm bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700/60 text-gray-600 dark:text-gray-400">
<div class="flex w-full justify-between items-start">
<div class="flex">
<svg class="shrink-0 fill-current text-yellow-500 mt-[3px] mr-3" width="16"
height="16" viewBox="0 0 16 16">
<path
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 12c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zm1-3H7V4h2v5z"></path>
</svg>
<div>
<div class="font-medium text-gray-800 dark:text-gray-100 mb-1">
Du bist derzeit ein Mitglied des Vereins.
<div class="flex flex-col space-y-4">
<div
class="inline-flex flex-col w-full max-w-lg px-4 py-2 rounded-lg text-sm bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700/60 text-gray-600 dark:text-gray-400">
<div class="flex w-full justify-between items-start">
<div class="flex">
<svg class="shrink-0 fill-current text-yellow-500 mt-[3px] mr-3"
width="16"
height="16" viewBox="0 0 16 16">
<path
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 12c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zm1-3H7V4h2v5z"></path>
</svg>
<div>
<div class="font-medium text-gray-800 dark:text-gray-100 mb-1">
Du bist derzeit ein Mitglied des Vereins.
</div>
</div>
</div>
</div>
</div>
<div
class="inline-flex flex-col w-full px-4 py-2 rounded-lg text-sm bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700/60 text-gray-600 dark:text-gray-400">
<div class="flex w-full justify-between items-start">
<div class="flex w-full">
<svg class="shrink-0 fill-current text-yellow-500 mt-[3px] mr-3"
width="16"
height="16" viewBox="0 0 16 16">
<path
d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 12c-.6 0-1-.4-1-1s.4-1 1-1 1 .4 1 1-.4 1-1 1zm1-3H7V4h2v5z"></path>
</svg>
<div class="w-full">
<div
class="w-full font-medium text-gray-800 dark:text-gray-100 mb-1">
Falls du möchtest, kannst du hier eine E-Mail Adresse
hinterlegen,
damit der Verein dich darüber informieren kann, wenn es
Neuigkeiten
gibt.<br><br>
Am besten eine anynomisierte E-Mail Adresse verwenden. Wir
sichern
diese Adresse AES-256 verschlüsselt in der Datenbank ab.
</div>
<div class="flex space-x-2">
<x-input wire:model.live.debounce="fax" label="Fax-Nummer"/>
<x-input wire:model.live.debounce="email"
label="E-Mail Adresse"/>
</div>
<div class="flex space-x-2 mt-2">
<x-button wire:click="saveEmail" label="Speichern"/>
</div>
</div>
</div>
</div>