I’m using the encryption built-into Laravel by casting an attribute as ‘encrypted’ in a model. My understanding is that the attribute will be encrypted when storing (create and update) and decrypted when retrieved.
When I seed, encrypted attributes are indeed encrypted in the database. And when I dump a model, encrypted attributes are encrypted in the dumped output.
Retrieving the encrypted attribute (by doing $model->attribute) appears to only work in actual code scenarios, and throws a decryption exception when I do so in a test (also database assertion using the decrypted value of the attribute passes when I expect it should fail).
Model creation, dump and attribute retrieval in a controller method:
$setupSaving = $financialGoal->setupSaving()->create([
'goal_name' => $financialGoal->goal_name,
'amount' => $validated['amount'],
'start_date' => $validated['start_date'],
'transfer_auth_code' => $result['data']['auth_code'], // encrypted attribute
'is_authorised' => true,
'is_active' => true,
]);
dump($setupSaving, $setupSaving->transfer_auth_code);
AppModelsSetupSaving {#15762
#connection: "mysql"
#table: "setup_savings"
#primaryKey: "id"
#keyType: "int"
+incrementing: true
#with: []
#withCount: []
+preventsLazyLoading: false
#perPage: 15
+exists: true
+wasRecentlyCreated: true
#escapeWhenCastingToString: false
#attributes: array:10 [
"goal_name" => "fake goal"
"amount" => 15000.0
"start_date" => "2024-08-06 00:00:00"
"transfer_auth_code" => "mYjAzODA5N2VhYTM3MmRiMTRjOCIsInRhZyI6IiJ9" //truncated
"is_authorised" => true
"is_active" => true
"financial_goal_id" => "9cb34536-9775-4a75-89e7-b0bd469009fa"
"id" => "9cb34538-204b-4a37-9051-659bb57e2a93"
"updated_at" => "2024-08-06 10:49:03"
"created_at" => "2024-08-06 10:49:03"
]
#original: array:10 [
"goal_name" => "fake goal"
"amount" => 15000.0
"start_date" => "2024-08-06 00:00:00"
"transfer_auth_code" => "mYjAzODA5N2VhYTM3MmRiMTRjOCIsInRhZyI6IiJ9" //truncated
"is_authorised" => true
"is_active" => true
"financial_goal_id" => "9cb34536-9775-4a75-89e7-b0bd469009fa"
"id" => "9cb34538-204b-4a37-9051-659bb57e2a93"
"updated_at" => "2024-08-06 10:49:03"
"created_at" => "2024-08-06 10:49:03"
]
#changes: []
#casts: array:7 [
"amount" => "float"
"start_date" => "date"
"next_date" => "date"
"is_active" => "boolean"
"is_authorised" => "boolean"
"loan_repayments" => "json"
"transfer_auth_code" => "encrypted"
]
#classCastCache: []
#attributeCastCache: []
#dateFormat: null
#appends: []
#dispatchesEvents: []
#observables: []
#relations: []
#touches: []
+timestamps: true
+usesUniqueIds: true
#hidden: array:1 [
0 => "transfer_auth_code"
]
#visible: []
#fillable: array:11 [
0 => "financial_goal_id"
1 => "goal_name"
2 => "transfer_auth_code"
3 => "amount"
4 => "start_date"
5 => "savings_frequency"
6 => "next_date"
7 => "is_authorised"
8 => "is_active"
9 => "administrator"
10 => "loan_repayments"
]
#guarded: array:1 [
0 => "*"
]
}
"12345tyrfdgy6" // retrieved value
Using the stored attribute at a later point also works:
$token = $financialGoal->setupSaving->transfer_auth_code;
dump($token);
"12345tyrfdgy6" // retrieved value used as payload in an API call
// API response dump
array:3 [
"status" => "success"
"message" => "test message"
"data" => array:9 [
"status" => "successful"
"amount" => 15000
"tx_ref" => "4373cc8426d62d140ae2d9c79b2a77a3cfcb116c897421771146cfbe9a756accc5f90e78bca7a7e2"
"payment_type" => "card"
"created_at" => "2024-08-06T09:49:03.974961Z"
"ip_address" => null
"processor_response" => "successful"
"card" => array:4 [
"reusable" => true
"exp_year" => "2026"
"exp_month" => "07"
"token" => "12345tyrfdgy6" //retrieved attribute used in API call
]
"meta" => array:13 [
"benefit_id" => null
"goal_id" => "9cb34536-9775-4a75-89e7-b0bd469009fa"
"user_id" => "d94e81cb-4028-3b64-aefc-65175842c3fd"
"setupBenefit_id" => null
"investment_service_provider_id" => null
"loan_id" => null
"loanProvider_id" => null
"reward_id" => null
"providerAuth_id" => null
"rewardProvider" => null
"transfer_description" => "Savings"
"bill_type" => null
"beneficiaries" => array:1 [
0 => array:6 [
"id" => "9cb34536-8158-4d76-b9fd-5afbfd669ae6"
"name" => "Oluwashina, Ekwueme and Olasunkanmi-fasayo"
"bank_name" => "Saheed, Israel and Akintade"
"account_name" => "Oluwashina, Ekwueme and Olasunkanmi-fasayo"
"account_number" => "846928981594103"
"amount" => 15000
]
]
]
]
]
My understanding is that this is the expected behaviour.
The issue then is my test. I get an decryption exception error when I try to retrieve the encrypted attribute like this:
dump($goal->fresh()->setupSaving->transfer_auth_code); // should return decrypted value
expect($goal->fresh()->setupSaving->transfer_auth_code)->toBe('12345tyrfdgy6'); // should fail because I'm comparing unlike strings but not give a decryption exception error
//Error
that… DecryptException
The payload is invalid.
at vendorlaravelframeworksrcIlluminateEncryptionEncrypter.php:245
241▕ // If the payload is not valid JSON or does not have the proper keys set we will
242▕ // assume it is invalid and bail out of the routine since we will not be able
243▕ // to decrypt the given value. We'll also check the MAC for this encryption.
244▕ if (! $this->validPayload($payload)) {
➜ 245▕ throw new DecryptException('The payload is invalid.');
246▕ }
247▕
248▕ return $payload;
249▕ }
1 vendorlaravelframeworksrcIlluminateEncryptionEncrypter.php:245
2 vendorlaravelframeworksrcIlluminateEncryptionEncrypter.php:158
but when I comment these out, my test passes and curiously, the dump of $goal->fresh()->setupSaving shows decrypted value of ‘transfer_auth_code’ when I would expect it to be encrypted as was the case when I dumped after model creation:
AppModelsSetupSaving {#16989
#connection: "mysql"
#table: "setup_savings"
#primaryKey: "id"
#keyType: "int"
+incrementing: true
#with: []
#withCount: []
+preventsLazyLoading: false
#perPage: 15
+exists: true
+wasRecentlyCreated: false
#escapeWhenCastingToString: false
#attributes: array:14 [
"id" => "9cb350fb-ec5c-416f-a511-277cbba30803"
"financial_goal_id" => "9cb350fa-f0e7-41d9-8d7a-7ba0eeb467a3"
"goal_name" => "fake goal"
"transfer_auth_code" => "12345tyrfdgy6" // not encrypted
"amount" => "15000.00"
"start_date" => "2024-08-06"
"savings_frequency" => "Monthly"
"next_date" => "2024-09-06"
"is_authorised" => 1
"is_active" => 1
"administrator" => "SmartWealth"
"loan_repayments" => "[]"
"created_at" => "2024-08-06 11:21:57"
"updated_at" => "2024-08-06 11:21:57"
]
#original: array:14 [
"id" => "9cb350fb-ec5c-416f-a511-277cbba30803"
"financial_goal_id" => "9cb350fa-f0e7-41d9-8d7a-7ba0eeb467a3"
"goal_name" => "fake goal"
"transfer_auth_code" => "12345tyrfdgy6"
"amount" => "15000.00"
"start_date" => "2024-08-06"
"savings_frequency" => "Monthly"
"next_date" => "2024-09-06"
"is_authorised" => 1
"is_active" => 1
"administrator" => "SmartWealth"
"loan_repayments" => "[]"
"created_at" => "2024-08-06 11:21:57"
"updated_at" => "2024-08-06 11:21:57"
]
#changes: []
#casts: array:7 [
"amount" => "float"
"start_date" => "date"
"next_date" => "date"
"is_active" => "boolean"
"is_authorised" => "boolean"
"loan_repayments" => "json"
"transfer_auth_code" => "encrypted"
]
#classCastCache: []
#attributeCastCache: []
#dateFormat: null
#appends: []
#dispatchesEvents: []
#observables: []
#relations: []
#touches: []
+timestamps: true
+usesUniqueIds: true
#hidden: array:1 [
0 => "transfer_auth_code"
]
#visible: []
#fillable: array:11 [
0 => "financial_goal_id"
1 => "goal_name"
2 => "transfer_auth_code"
3 => "amount"
4 => "start_date"
5 => "savings_frequency"
6 => "next_date"
7 => "is_authorised"
8 => "is_active"
9 => "administrator"
10 => "loan_repayments"
]
#guarded: array:1 [
0 => "*"
]
}
It appears that the stored attribute is no longer encrypted at this point when it should be. I expected this assertion to fail but it passes:
$this->assertDatabaseHas('setup_savings', [
'financial_goal_id' => $goal->id,
'transfer_auth_code' => '12345tyrfdgy6', // should be encrypted and this should fail but it passes
]);
Is this the expected behaviour or I’m I missing something?