Skip to content

Commit

Permalink
Fix #1588: Implement per-record encryption in configuration table (#1614
Browse files Browse the repository at this point in the history
)

* Fix #1588: Implement per-record encryption in configuration table
  • Loading branch information
banterCZ authored Aug 6, 2024
1 parent 0b70406 commit 4511488
Show file tree
Hide file tree
Showing 24 changed files with 492 additions and 187 deletions.
13 changes: 7 additions & 6 deletions docs/Database-Structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,13 @@ Stores configurations for the applications stored in `pa_application` table.

#### Columns

| Name | Type | Info | Note |
|----------------|--------------|---------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| id | BIGINT(20) | primary key, autoincrement | Unique application configuration identifier. |
| application_id | BIGINT(20) | foreign key: pa\_application.id | Related application ID. |
| config_key | VARCHAR(255) | index | Configuration key names: `fido2_attestation_fmt_allowed`, `fido2_aaguids_allowed`, `fido2_root_ca_certs`, or `oauth2_providers` |
| config_values | TEXT | - | Configuration values serialized in JSON format. |
| Name | Type | Info | Note |
|------------------|--------------|------------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
| id | BIGINT(20) | primary key, autoincrement | Unique application configuration identifier. |
| application_id | BIGINT(20) | foreign key: pa\_application.id | Related application ID. |
| config_key | VARCHAR(255) | index | Configuration key names: `fido2_attestation_fmt_allowed`, `fido2_aaguids_allowed`, `fido2_root_ca_certs`, or `oauth2_providers` |
| config_values | TEXT | - | Configuration values serialized in JSON format. |
| encryption_mode | VARCHAR(255) | DEFAULT 'NO_ENCRYPTION' NOT NULL | Encryption of config values: `NO_ENCRYPTION` means plaintext, `AES_HMAC` for AES encryption with HMAC-based index. |
<!-- end -->

<!-- begin database table pa_activation -->
Expand Down
1 change: 1 addition & 0 deletions docs/Migration-Instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ This page contains PowerAuth Server migration instructions.
When updating across multiple versions, you need to perform all migration steps additively.
<!-- end -->

- [PowerAuth Server 1.9.0](./PowerAuth-Server-1.9.0.md)
- [PowerAuth Server 1.8.0](./PowerAuth-Server-1.8.0.md)
- [PowerAuth Server 1.7.0](./PowerAuth-Server-1.7.0.md)
- [PowerAuth Server 1.6.0](./PowerAuth-Server-1.6.0.md)
Expand Down
19 changes: 19 additions & 0 deletions docs/PowerAuth-Server-1.9.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Migration from 1.8.x to 1.9.0

This guide contains instructions for migration from PowerAuth Server version `1.8.x` to version `1.9.0`.


## Database Changes

For convenience, you can use liquibase for your database migration.

For manual changes use SQL scripts:

- [PostgreSQL script](./sql/postgresql/migration_1.8.0_1.9.0.sql)
- [Oracle script](./sql/oracle/migration_1.8.0_1.9.0.sql)
- [MSSQL script](./sql/mssql/migration_1.8.0_1.9.0.sql)


### Add encryption_mode Column

A new column `encryption_mode` has been added to the `pa_application_config` table to enable encryption of configuration values.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

<changeSet id="1" logicalFilePath="powerauth-java-server/1.9.x/20240723-configuration-encryption.xml" author="Lubos Racansky">
<preConditions onFail="MARK_RAN">
<not>
<columnExists tableName="pa_application_config" columnName="encryption_mode"/>
</not>
</preConditions>
<comment>Add encryption_mode column to pa_application_config table.</comment>
<addColumn tableName="pa_application_config">
<column name="encryption_mode" defaultValue="NO_ENCRYPTION" type="varchar(255)">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>

</databaseChangeLog>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd">

<include file="20240723-configuration-encryption.xml" relativeToChangelogFile="true" />

</databaseChangeLog>
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
<include file="1.6.x/db.changelog-version.xml" relativeToChangelogFile="true" />
<include file="1.7.x/db.changelog-version.xml" relativeToChangelogFile="true" />
<include file="1.8.x/db.changelog-version.xml" relativeToChangelogFile="true" />
<include file="1.9.x/db.changelog-version.xml" relativeToChangelogFile="true" />

</databaseChangeLog>
Binary file modified docs/images/arch_db_structure.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions docs/sql/mssql/migration_1.8.0_1.9.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Changeset powerauth-java-server/1.9.x/20240723-configuration-encryption.xml::1::Lubos Racansky
-- Add encryption_mode column to pa_application_config table.
ALTER TABLE pa_application_config ADD encryption_mode varchar(255) CONSTRAINT DF_pa_application_config_encryption_mode DEFAULT 'NO_ENCRYPTION' NOT NULL;
GO
3 changes: 3 additions & 0 deletions docs/sql/oracle/migration_1.8.0_1.9.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Changeset powerauth-java-server/1.9.x/20240723-configuration-encryption.xml::1::Lubos Racansky
-- Add encryption_mode column to pa_application_config table.
ALTER TABLE pa_application_config ADD encryption_mode VARCHAR2(255) DEFAULT 'NO_ENCRYPTION' NOT NULL;
3 changes: 3 additions & 0 deletions docs/sql/postgresql/migration_1.8.0_1.9.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Changeset powerauth-java-server/1.9.x/20240723-configuration-encryption.xml::1::Lubos Racansky
-- Add encryption_mode column to pa_application_config table.
ALTER TABLE pa_application_config ADD encryption_mode VARCHAR(255) DEFAULT 'NO_ENCRYPTION' NOT NULL;
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
package com.wultra.security.powerauth.client.model.request;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;
Expand All @@ -30,12 +32,16 @@
* @author Roman Strobl, [email protected]
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CreateApplicationConfigRequest {

@NotBlank
private String applicationId;

@NotBlank
private String key;

private List<Object> values = new ArrayList<>();

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,16 @@
*/
package io.getlime.security.powerauth.app.server.converter;

import java.util.Base64;
import java.util.List;

import org.springframework.stereotype.Component;

import io.getlime.security.powerauth.app.server.database.model.RecoveryPrivateKey;
import io.getlime.security.powerauth.app.server.service.encryption.Encryptable;
import io.getlime.security.powerauth.app.server.service.encryption.EncryptableData;
import io.getlime.security.powerauth.app.server.service.encryption.EncryptionService;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Base64;
import java.util.List;

/**
* Converter for recovery postcard private key which handles key encryption and decryption in case it is configured.
Expand Down Expand Up @@ -67,8 +66,8 @@ public String fromDBValue(final RecoveryPrivateKey recoveryPrivateKey, long appl
* @throws GenericServiceException Thrown when recovery postcard private key encryption fails.
*/
public RecoveryPrivateKey toDBValue(byte[] recoveryPrivateKey, long applicationRid) throws GenericServiceException {
final Encryptable encryptable = encryptionService.encrypt(recoveryPrivateKey, createSecretKeyDerivationInput(applicationRid));
return new RecoveryPrivateKey(encryptable.getEncryptionMode(), convert(encryptable.getEncryptedData()));
final EncryptableData encryptable = encryptionService.encrypt(recoveryPrivateKey, createSecretKeyDerivationInput(applicationRid));
return new RecoveryPrivateKey(encryptable.encryptionMode(), convert(encryptable.encryptedData()));
}

private static String convert(final byte[] source) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,15 @@
*/
package io.getlime.security.powerauth.app.server.converter;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.List;

import org.springframework.stereotype.Component;

import io.getlime.security.powerauth.app.server.database.model.RecoveryPuk;
import io.getlime.security.powerauth.app.server.database.model.enumeration.EncryptionMode;
import io.getlime.security.powerauth.app.server.service.encryption.Encryptable;
import io.getlime.security.powerauth.app.server.service.encryption.EncryptableString;
import io.getlime.security.powerauth.app.server.service.encryption.EncryptionService;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.List;

/**
* Converter for recovery PUK which handles record encryption and decryption in case it is configured.
Expand All @@ -56,9 +52,7 @@ public class RecoveryPukConverter {
* @throws GenericServiceException In case recovery PUK hash decryption fails.
*/
public String fromDBValue(final RecoveryPuk recoveryPuk, final long applicationRid, final String userId, final String recoveryCode, final long pukIndex) throws GenericServiceException {
final byte[] data = convert(recoveryPuk);
final byte[] decrypted = encryptionService.decrypt(data, recoveryPuk.encryptionMode(), createSecretKeyDerivationInput(applicationRid, userId, recoveryCode, pukIndex));
return convert(decrypted);
return encryptionService.decrypt(recoveryPuk.pukHash(), recoveryPuk.encryptionMode(), createSecretKeyDerivationInput(applicationRid, userId, recoveryCode, pukIndex));
}

/**
Expand All @@ -75,33 +69,8 @@ public String fromDBValue(final RecoveryPuk recoveryPuk, final long applicationR
* @throws GenericServiceException Thrown when server private key encryption fails.
*/
public RecoveryPuk toDBValue(final String pukHash, final long applicationRid, final String userId, final String recoveryCode, final long pukIndex) throws GenericServiceException {
final byte[] data = convert(pukHash);
final Encryptable encryptable = encryptionService.encrypt(data, createSecretKeyDerivationInput(applicationRid, userId, recoveryCode, pukIndex));
return new RecoveryPuk(encryptable.getEncryptionMode(), convert(encryptable));
}

private static byte[] convert(final RecoveryPuk source) {
if (source.encryptionMode() == EncryptionMode.NO_ENCRYPTION) {
return source.pukHash().getBytes(StandardCharsets.UTF_8);
} else {
return Base64.getDecoder().decode(source.pukHash());
}
}

private static byte[] convert(final String source) {
return source.getBytes(StandardCharsets.UTF_8);
}

private static String convert(final Encryptable source) {
if (source.getEncryptionMode() == EncryptionMode.NO_ENCRYPTION) {
return convert(source.getEncryptedData());
} else {
return Base64.getEncoder().encodeToString(source.getEncryptedData());
}
}

private static String convert(final byte[] source) {
return new String(source, StandardCharsets.UTF_8);
final EncryptableString encryptable = encryptionService.encrypt(pukHash, createSecretKeyDerivationInput(applicationRid, userId, recoveryCode, pukIndex));
return new RecoveryPuk(encryptable.encryptionMode(), encryptable.encryptedData());
}

private static List<String> createSecretKeyDerivationInput(final long applicationRid, final String userId, final String recoveryCode, final long pukIndex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,16 @@
*/
package io.getlime.security.powerauth.app.server.converter;

import java.util.Base64;
import java.util.List;

import org.springframework.stereotype.Component;

import io.getlime.security.powerauth.app.server.database.model.ServerPrivateKey;
import io.getlime.security.powerauth.app.server.service.encryption.Encryptable;
import io.getlime.security.powerauth.app.server.service.encryption.EncryptableData;
import io.getlime.security.powerauth.app.server.service.encryption.EncryptionService;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.Base64;
import java.util.List;

/**
* Converter for server private key which handles key encryption and decryption in case it is configured.
Expand Down Expand Up @@ -69,8 +68,8 @@ public String fromDBValue(final ServerPrivateKey serverPrivateKey, final String
* @throws GenericServiceException Thrown when server private key encryption fails.
*/
public ServerPrivateKey toDBValue(final byte[] serverPrivateKey, final String userId, final String activationId) throws GenericServiceException {
final Encryptable encryptable = encryptionService.encrypt(serverPrivateKey, createSecretKeyDerivationInput(userId, activationId));
return new ServerPrivateKey(encryptable.getEncryptionMode(), convert(encryptable.getEncryptedData()));
final EncryptableData encryptable = encryptionService.encrypt(serverPrivateKey, createSecretKeyDerivationInput(userId, activationId));
return new ServerPrivateKey(encryptable.encryptionMode(), convert(encryptable.encryptedData()));
}

private static String convert(final byte[] source) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,14 @@
*/
package io.getlime.security.powerauth.app.server.database.model.entity;

import io.getlime.security.powerauth.app.server.database.model.converter.ListToJsonConverter;
import io.getlime.security.powerauth.app.server.database.model.enumeration.EncryptionMode;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.util.ProxyUtils;

import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
Expand Down Expand Up @@ -57,8 +55,11 @@ public class ApplicationConfigEntity implements Serializable {
private String key;

@Column(name = "config_values", columnDefinition = "CLOB")
@Convert(converter = ListToJsonConverter.class)
private List<Object> values = new ArrayList<>();
private String values = "[]";

@Enumerated(EnumType.STRING)
@Column(name = "encryption_mode", nullable = false, columnDefinition = "varchar(255) default 'NO_ENCRYPTION'")
private EncryptionMode encryptionMode;

@Override
public boolean equals(Object o) {
Expand All @@ -83,6 +84,7 @@ public String toString() {
", appId='" + application.getId() + '\'' +
", key=" + key +
", values=" + values +
", encryptionMode=" + encryptionMode +
'}';
}
}
Loading

0 comments on commit 4511488

Please sign in to comment.