This is an automated email from the ASF dual-hosted git repository.
mridulpathak pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ofbiz-framework.git
The following commit(s) were added to refs/heads/trunk by this push:
new cae2bb65c1 Fixed: EntityCrypto uses AES in ECB mode for encrypt="true"
fields, producing deterministic ciphertext (OFBIZ-13599) (#2055)
cae2bb65c1 is described below
commit cae2bb65c1cdfdd26044e706a1d7f09bb445abe1
Author: Mridul Pathak <[email protected]>
AuthorDate: Wed Sep 23 15:06:47 2026 +0530
Fixed: EntityCrypto uses AES in ECB mode for encrypt="true" fields,
producing deterministic ciphertext (OFBIZ-13599) (#2055)
- Route EncryptMethod.TRUE through the same cipher default already used by
SALT, instead of forcing AES into ECB mode (no IV, deterministic ciphertext for
identical plaintext). Keep the old ECB cipher only as a decrypt fallback for
values written before this fix.
- Retire encrypt="salt" as a separate option, now redundant with
encrypt="true".
- Fix reencryptFields (OFBIZ-6694): select the primary key alongside the
target field so store() can find the row afterward, and skip view entities,
since storing through a view fails.
- Remove encrypt="true" from FinAccount.finAccountCode: the field is used
for exact-match lookup as a redemption code
(FinAccountHelper.getFinAccountFromCode), which is incompatible with
non-deterministic encryption. finAccountPin remains encrypted. Add
decryptFinAccountCodes, in the accounting component (not framework/entityext,
to avoid a framework-to-application dependency), to migrate existing ciphertext
values back to plain text.
- Remove the cardNumber search parameter from the findOrders service and
admin order search, since exact-match search against an encrypted field is no
longer possible.
- Document the required post-upgrade migration steps.
---
.../accounting/config/AccountingErrorUiLabels.xml | 3 ++
.../accounting/servicedef/services_finaccount.xml | 6 +++
.../accounting/finaccount/FinAccountServices.java | 46 ++++++++++++++++++
.../datamodel/entitydef/accounting-entitymodel.xml | 2 +-
applications/order/servicedef/services.xml | 1 -
.../ofbiz/order/order/OrderLookupServices.java | 13 +----
framework/entity/dtd/entitymodel.xsd | 1 -
framework/entity/entitydef/entitymodel_test.xml | 6 ---
.../org/apache/ofbiz/entity/model/ModelField.java | 3 +-
.../org/apache/ofbiz/entity/util/EntityCrypto.java | 55 +++++++++-------------
.../ofbiz/entity/test/EntityCryptoTestSuite.java | 48 +++++++------------
.../ofbiz/entityext/data/EntityDataServices.java | 17 ++++++-
.../_include/sy-entity-field-encryption.adoc | 39 +++++++++++++++
framework/security/src/docs/asciidoc/security.adoc | 1 +
14 files changed, 153 insertions(+), 88 deletions(-)
diff --git a/applications/accounting/config/AccountingErrorUiLabels.xml
b/applications/accounting/config/AccountingErrorUiLabels.xml
index 30c80a21e5..7d5496632f 100644
--- a/applications/accounting/config/AccountingErrorUiLabels.xml
+++ b/applications/accounting/config/AccountingErrorUiLabels.xml
@@ -239,6 +239,9 @@
<value xml:lang="zh">无法把标识为 ${parameters.finAccountTransId}
的财务账目交易提交到普通日记账中,无法找到 ${creditDebit} ${requiredField}</value>
<value xml:lang="zh-TW">無法把標識為 ${parameters.finAccountTransId}
的財務帳目交易提交到普通日記帳中,無法找到 ${creditDebit} ${requiredField}</value>
</property>
+ <property key="AccountingFinAccountCodeDecryptPermissionError">
+ <value xml:lang="en">You do not have permission to run the FinAccount
finAccountCode decryption migration</value>
+ </property>
<property key="AccountingFinAccountExpired">
<value xml:lang="ar">انتهت صلاحية الحساب منذ ${thruDate}</value>
<value xml:lang="de">Konto ist zum ${thruDate} abgelaufen</value>
diff --git a/applications/accounting/servicedef/services_finaccount.xml
b/applications/accounting/servicedef/services_finaccount.xml
index 5e204f0e5e..305b46b201 100644
--- a/applications/accounting/servicedef/services_finaccount.xml
+++ b/applications/accounting/servicedef/services_finaccount.xml
@@ -614,4 +614,10 @@ under the License.
<description>Delete a FinAccountTypeAttr</description>
<auto-attributes include="pk" mode="IN" optional="false"/>
</service>
+
+ <service name="decryptFinAccountCodes" engine="java" auth="true"
transaction-timeout="14400"
+
location="org.apache.ofbiz.accounting.finaccount.FinAccountServices"
invoke="decryptFinAccountCodes">
+ <description>One-time migration (OFBIZ-13599): decrypt existing
FinAccount.finAccountCode values now
+ that the field is no longer encrypted. Safe to run more than
once.</description>
+ </service>
</services>
diff --git
a/applications/accounting/src/main/java/org/apache/ofbiz/accounting/finaccount/FinAccountServices.java
b/applications/accounting/src/main/java/org/apache/ofbiz/accounting/finaccount/FinAccountServices.java
index 4b246d704a..86795f19ff 100644
---
a/applications/accounting/src/main/java/org/apache/ofbiz/accounting/finaccount/FinAccountServices.java
+++
b/applications/accounting/src/main/java/org/apache/ofbiz/accounting/finaccount/FinAccountServices.java
@@ -33,15 +33,18 @@ import org.apache.ofbiz.base.util.UtilMisc;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilValidate;
import org.apache.ofbiz.entity.Delegator;
+import org.apache.ofbiz.entity.EntityCryptoException;
import org.apache.ofbiz.entity.GenericEntityException;
import org.apache.ofbiz.entity.GenericValue;
import org.apache.ofbiz.entity.condition.EntityCondition;
import org.apache.ofbiz.entity.condition.EntityExpr;
import org.apache.ofbiz.entity.condition.EntityOperator;
+import org.apache.ofbiz.entity.model.ModelField;
import org.apache.ofbiz.entity.util.EntityListIterator;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.order.finaccount.FinAccountHelper;
import org.apache.ofbiz.product.store.ProductStoreWorker;
+import org.apache.ofbiz.security.Security;
import org.apache.ofbiz.service.DispatchContext;
import org.apache.ofbiz.service.GenericServiceException;
import org.apache.ofbiz.service.LocalDispatcher;
@@ -510,4 +513,47 @@ public class FinAccountServices {
return result;
}
+
+ /**
+ * One-time migration for OFBIZ-13599: FinAccount.finAccountCode is no
longer marked
+ * {@code encrypt="true"}, since the field needs exact-match lookup to
function as a redemption
+ * code (see FinAccountHelper.getFinAccountFromCode) and encryption never
provided real
+ * confidentiality for it in practice. Existing rows still hold ciphertext
from before this
+ * change; this decrypts them back to plaintext so they remain usable.
Safe to run more than
+ * once -- a row whose value is already plaintext fails to decrypt and is
left untouched.
+ */
+ public static Map<String, Object> decryptFinAccountCodes(DispatchContext
dctx, Map<String, Object> context) {
+ Delegator delegator = dctx.getDelegator();
+ Security security = dctx.getSecurity();
+ Locale locale = (Locale) context.get("locale");
+
+ GenericValue userLogin = (GenericValue) context.get("userLogin");
+ if (!security.hasPermission("ACCOUNTING_ADMIN", userLogin)) {
+ return
ServiceUtil.returnError(UtilProperties.getMessage(RES_ERROR,
"AccountingFinAccountCodeDecryptPermissionError", locale));
+ }
+
+ try {
+ List<GenericValue> rows =
EntityQuery.use(delegator).from("FinAccount")
+ .select("finAccountId", "finAccountCode").queryList();
+ for (GenericValue row: rows) {
+ String rawValue = row.getString("finAccountCode");
+ if (UtilValidate.isEmpty(rawValue)) {
+ continue;
+ }
+ String plainValue;
+ try {
+ plainValue = (String)
delegator.decryptFieldValue("FinAccount", ModelField.EncryptMethod.TRUE,
rawValue);
+ } catch (EntityCryptoException e) {
+ // Already plaintext -- either this migration already ran,
or the row was
+ // created after encrypt="true" was removed. Nothing to do.
+ continue;
+ }
+ row.setString("finAccountCode", plainValue);
+ row.store();
+ }
+ } catch (GenericEntityException gee) {
+ return ServiceUtil.returnError(gee.getMessage());
+ }
+ return ServiceUtil.returnSuccess();
+ }
}
diff --git a/applications/datamodel/entitydef/accounting-entitymodel.xml
b/applications/datamodel/entitydef/accounting-entitymodel.xml
index 87eede5cbb..cb28319597 100644
--- a/applications/datamodel/entitydef/accounting-entitymodel.xml
+++ b/applications/datamodel/entitydef/accounting-entitymodel.xml
@@ -341,7 +341,7 @@ under the License.
<field name="finAccountTypeId" type="id"></field>
<field name="statusId" type="id"></field>
<field name="finAccountName" type="name"></field>
- <field name="finAccountCode" type="long-varchar"
encrypt="true"></field>
+ <field name="finAccountCode" type="long-varchar"></field>
<field name="finAccountPin" type="long-varchar" encrypt="true"></field>
<field name="currencyUomId" type="id"/>
<field name="organizationPartyId" type="id"><description>The internal
organization Party that owns (or rather, is liable for) the
account.</description></field>
diff --git a/applications/order/servicedef/services.xml
b/applications/order/servicedef/services.xml
index b773973ef0..35381242c7 100644
--- a/applications/order/servicedef/services.xml
+++ b/applications/order/servicedef/services.xml
@@ -1111,7 +1111,6 @@ under the License.
<attribute name="billingAccountId" type="String" mode="IN"
optional="true"/>
<attribute name="finAccountId" type="String" mode="IN"
optional="true"/>
- <attribute name="cardNumber" type="String" mode="IN" optional="true"/>
<attribute name="accountNumber" type="String" mode="IN"
optional="true"/>
<attribute name="paymentStatusId" type="String" mode="IN"
optional="true"/>
diff --git
a/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderLookupServices.java
b/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderLookupServices.java
index cf32fd427b..ddb5856ede 100644
---
a/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderLookupServices.java
+++
b/applications/order/src/main/java/org/apache/ofbiz/order/order/OrderLookupServices.java
@@ -444,7 +444,6 @@ public class OrderLookupServices {
// payment preference fields
String billingAccountId = (String) context.get("billingAccountId");
String finAccountId = (String) context.get("finAccountId");
- String cardNumber = (String) context.get("cardNumber");
String accountNumber = (String) context.get("accountNumber");
String paymentStatusId = (String) context.get("paymentStatusId");
@@ -452,7 +451,7 @@ public class OrderLookupServices {
paramList.add("paymentStatusId=" + paymentStatusId);
conditions.add(makeExpr("paymentStatusId", paymentStatusId));
}
- if (finAccountId != null || cardNumber != null || accountNumber !=
null || paymentStatusId != null) {
+ if (finAccountId != null || accountNumber != null || paymentStatusId
!= null) {
dve.addMemberEntity("OP", "OrderPaymentPreference");
dve.addAlias("OP", "finAccountId");
dve.addAlias("OP", "paymentMethodId");
@@ -472,16 +471,6 @@ public class OrderLookupServices {
conditions.add(makeExpr("finAccountId", finAccountId));
}
- // search by card number
- if (UtilValidate.isNotEmpty(cardNumber)) {
- dve.addMemberEntity("CC", "CreditCard");
- dve.addAlias("CC", "cardNumber");
- dve.addViewLink("OP", "CC", Boolean.FALSE, UtilMisc.toList(new
ModelKeyMap("paymentMethodId", "paymentMethodId")));
-
- paramList.add("cardNumber=" + cardNumber);
- conditions.add(makeExpr("cardNumber", cardNumber));
- }
-
// search by eft account number
if (UtilValidate.isNotEmpty(accountNumber)) {
dve.addMemberEntity("EF", "EftAccount");
diff --git a/framework/entity/dtd/entitymodel.xsd
b/framework/entity/dtd/entitymodel.xsd
index 55c6eb4799..a082ab60c9 100644
--- a/framework/entity/dtd/entitymodel.xsd
+++ b/framework/entity/dtd/entitymodel.xsd
@@ -156,7 +156,6 @@ under the License.
<xs:restriction base="xs:token">
<xs:enumeration value="true"/>
<xs:enumeration value="false"/>
- <xs:enumeration value="salt"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
diff --git a/framework/entity/entitydef/entitymodel_test.xml
b/framework/entity/entitydef/entitymodel_test.xml
index 42bd4de849..f1cba2615b 100644
--- a/framework/entity/entitydef/entitymodel_test.xml
+++ b/framework/entity/entitydef/entitymodel_test.xml
@@ -223,7 +223,6 @@ under the License.
<field name="testingCryptoTypeId" type="id"/>
<field name="unencryptedValue" type="description"/>
<field name="encryptedValue" type="description" encrypt="true"/>
- <field name="saltedEncryptedValue" type="description" encrypt="salt"/>
<prim-key field="testingCryptoId"/>
</entity>
<view-entity entity-name="TestingCryptoRawView"
@@ -236,11 +235,6 @@ under the License.
<complex-alias-field entity-alias="TC" field="encryptedValue"/>
</complex-alias>
</alias>
- <alias name="rawSaltedEncryptedValue">
- <complex-alias operator="+">
- <complex-alias-field entity-alias="TC" field="saltedEncryptedValue"/>
- </complex-alias>
- </alias>
</view-entity>
<entity entity-name="TestingRemoveAll"
package-name="org.apache.ofbiz.entity.test"
diff --git
a/framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelField.java
b/framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelField.java
index e7411960d5..07f3e36a8b 100644
---
a/framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelField.java
+++
b/framework/entity/src/main/java/org/apache/ofbiz/entity/model/ModelField.java
@@ -41,8 +41,7 @@ public final class ModelField extends ModelChild {
public enum EncryptMethod {
FALSE,
- TRUE,
- SALT;
+ TRUE;
public boolean isEncrypted() {
return this != FALSE;
diff --git
a/framework/entity/src/main/java/org/apache/ofbiz/entity/util/EntityCrypto.java
b/framework/entity/src/main/java/org/apache/ofbiz/entity/util/EntityCrypto.java
index bae17e48a4..e52a2128f5 100644
---
a/framework/entity/src/main/java/org/apache/ofbiz/entity/util/EntityCrypto.java
+++
b/framework/entity/src/main/java/org/apache/ofbiz/entity/util/EntityCrypto.java
@@ -22,7 +22,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
-import java.security.SecureRandom;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -55,8 +54,6 @@ public final class EntityCrypto {
private final ConcurrentMap<String, byte[]> keyMap = new
ConcurrentHashMap<>();
private final StorageHandler[] handlers;
- private static final SecureRandom SECURE_RANDOM = new SecureRandom();
-
public EntityCrypto(Delegator delegator, String kekText) throws
EntityCryptoException {
this.delegator = delegator;
byte[] kek;
@@ -231,21 +228,24 @@ public final class EntityCrypto {
protected static final class ShiroStorageHandler extends StorageHandler {
private final HashService hashService;
private final AesCipherService cipherService;
- private final AesCipherService saltedCipherService;
+ private final AesCipherService legacyCipherService;
private final byte[] kek;
protected ShiroStorageHandler(byte[] kek) {
hashService = new DefaultHashService();
cipherService = new AesCipherService();
- cipherService.setMode(OperationMode.ECB);
- cipherService.setPaddingScheme(PaddingScheme.PKCS5);
- saltedCipherService = new AesCipherService();
+ // Kept only to decrypt values written before the fix for
OFBIZ-13599, which forced AES into ECB
+ // mode (no IV, deterministic ciphertext) instead of using
AesCipherService's safer default.
+ // Never used for new encryption; existing ECB values are migrated
via the reencryptFields service.
+ legacyCipherService = new AesCipherService();
+ legacyCipherService.setMode(OperationMode.ECB);
+ legacyCipherService.setPaddingScheme(PaddingScheme.PKCS5);
this.kek = kek;
}
@Override
protected Key generateNewKey() {
- return saltedCipherService.generateNewKey();
+ return cipherService.generateNewKey();
}
@Override
@@ -263,7 +263,7 @@ public final class EntityCrypto {
protected byte[] decodeKeyBytes(String keyText) throws
GeneralException {
byte[] keyBytes = Base64.decodeBase64(keyText);
if (kek != null) {
- keyBytes = saltedCipherService.decrypt(keyBytes,
kek).getClonedBytes();
+ keyBytes = cipherService.decrypt(keyBytes,
kek).getClonedBytes();
}
return keyBytes;
}
@@ -271,7 +271,7 @@ public final class EntityCrypto {
@Override
protected String encodeKey(byte[] key) throws GeneralException {
if (kek != null) {
- return saltedCipherService.encrypt(key, kek).toBase64();
+ return cipherService.encrypt(key, kek).toBase64();
} else {
return Base64.encodeBase64String(key);
}
@@ -279,22 +279,20 @@ public final class EntityCrypto {
@Override
protected byte[] decryptValue(byte[] key, EncryptMethod encryptMethod,
String encryptedString) throws GeneralException {
- switch (encryptMethod) {
- case SALT:
- return
saltedCipherService.decrypt(Base64.decodeBase64(encryptedString),
key).getClonedBytes();
- default:
- return
cipherService.decrypt(Base64.decodeBase64(encryptedString),
key).getClonedBytes();
+ byte[] encryptedBytes = Base64.decodeBase64(encryptedString);
+ try {
+ return cipherService.decrypt(encryptedBytes,
key).getClonedBytes();
+ } catch (Exception e) {
+ // Fall back to the legacy ECB decrypt for values written
before the fix for OFBIZ-13599;
+ // see the comment on legacyCipherService above.
+ Debug.logInfo("Decrypt with current cipher failed, trying
legacy ECB cipher", MODULE);
+ return legacyCipherService.decrypt(encryptedBytes,
key).getClonedBytes();
}
}
@Override
protected String encryptValue(EncryptMethod encryptMethod, byte[] key,
byte[] objBytes) throws GeneralException {
- switch (encryptMethod) {
- case SALT:
- return saltedCipherService.encrypt(objBytes, key).toBase64();
- default:
- return cipherService.encrypt(objBytes, key).toBase64();
- }
+ return cipherService.encrypt(objBytes, key).toBase64();
}
}
@@ -416,17 +414,10 @@ public final class EntityCrypto {
@Override
protected String encryptValue(EncryptMethod encryptMethod, byte[] key,
byte[] objBytes) throws GeneralException {
- byte[] saltBytes;
- switch (encryptMethod) {
- case SALT:
- // random length 5-16
- saltBytes = new byte[5 + SECURE_RANDOM.nextInt(11)];
- SECURE_RANDOM.nextBytes(saltBytes);
- break;
- default:
- saltBytes = new byte[0];
- break;
- }
+ // This legacy handler is never used for new encryption --
EntityCrypto.encrypt() always
+ // encrypts through the primary ShiroStorageHandler; this method
exists only so the class
+ // implements StorageHandler for its decrypt fallback role.
+ byte[] saltBytes = new byte[0];
byte[] allBytes = new byte[1 + saltBytes.length + objBytes.length];
allBytes[0] = (byte) saltBytes.length;
System.arraycopy(saltBytes, 0, allBytes, 1, saltBytes.length);
diff --git
a/framework/entity/src/test/java/org/apache/ofbiz/entity/test/EntityCryptoTestSuite.java
b/framework/entity/src/test/java/org/apache/ofbiz/entity/test/EntityCryptoTestSuite.java
index a43e4f12c2..b06a83443b 100644
---
a/framework/entity/src/test/java/org/apache/ofbiz/entity/test/EntityCryptoTestSuite.java
+++
b/framework/entity/src/test/java/org/apache/ofbiz/entity/test/EntityCryptoTestSuite.java
@@ -57,15 +57,12 @@ public class EntityCryptoTestSuite implements
JupiterTestHelper {
assertNull(entity.getString("encryptedValue"));
entity.setString("unencryptedValue", nanoTime);
entity.setString("encryptedValue", nanoTime);
- entity.setString("saltedEncryptedValue", nanoTime);
assertEquals(nanoTime, entity.getString("unencryptedValue"));
assertEquals(nanoTime, entity.getString("encryptedValue"));
- assertEquals(nanoTime, entity.getString("saltedEncryptedValue"));
entity.store();
entity.refresh();
assertEquals(nanoTime, entity.getString("unencryptedValue"));
assertEquals(nanoTime, entity.getString("encryptedValue"));
- assertEquals(nanoTime, entity.getString("saltedEncryptedValue"));
}
/**
@@ -86,32 +83,23 @@ public class EntityCryptoTestSuite implements
JupiterTestHelper {
GenericValue entity =
EntityQuery.use(delegator).from("TestingCrypto").where("testingCryptoId",
"1").queryOne();
assertNull(entity.getString("unencryptedValue"));
assertNull(entity.getString("encryptedValue"));
- assertNull(entity.getString("saltedEncryptedValue"));
GenericValue view =
EntityQuery.use(delegator).from("TestingCryptoRawView").where("testingCryptoId",
"1").queryOne();
assertNull(view.getString("unencryptedValue"));
assertNull(view.getString("encryptedValue"));
- assertNull(view.getString("saltedEncryptedValue"));
assertNull(view.getString("rawEncryptedValue"));
- assertNull(view.getString("rawSaltedEncryptedValue"));
// Verify that encryption is taking place
entity.setString("unencryptedValue", nanoTime);
entity.setString("encryptedValue", nanoTime);
- entity.setString("saltedEncryptedValue", nanoTime);
entity.store();
view.refresh();
assertEquals(nanoTime, view.getString("unencryptedValue"));
assertEquals(nanoTime, view.getString("encryptedValue"));
- assertEquals(nanoTime, view.getString("saltedEncryptedValue"));
String initialValue = view.getString("rawEncryptedValue");
- String initialSaltedValue = view.getString("rawSaltedEncryptedValue");
assertFalse(nanoTime.equals(initialValue));
- assertFalse(nanoTime.equals(initialSaltedValue));
- assertFalse(initialValue.equals(initialSaltedValue));
// Verify that the same value stored repeatedly gives different raw
encrypted values.
entity.setString("encryptedValue", nanoTime);
- entity.setString("saltedEncryptedValue", nanoTime);
entity.store();
//entity.refresh(); // this is a bug; store() ends up setting the
encrypted value *into* the entity
assertEquals(nanoTime, entity.getString("unencryptedValue"));
@@ -120,16 +108,13 @@ public class EntityCryptoTestSuite implements
JupiterTestHelper {
view.refresh();
assertEquals(nanoTime, view.getString("unencryptedValue"));
assertEquals(nanoTime, view.getString("encryptedValue"));
- assertEquals(nanoTime, view.getString("saltedEncryptedValue"));
String updatedValue = view.getString("rawEncryptedValue");
- String updatedSaltedValue = view.getString("rawSaltedEncryptedValue");
assertFalse(nanoTime.equals(updatedValue));
- assertFalse(nanoTime.equals(updatedSaltedValue));
- assertFalse(updatedValue.equals(updatedSaltedValue));
- assertEquals(initialValue, updatedValue);
- assertFalse(initialSaltedValue.equals(updatedSaltedValue));
+ // Regression guard for OFBIZ-13599: encryption must not be
deterministic. Before the fix, AES was
+ // forced into ECB mode (no IV), so the same plaintext always produced
the same raw ciphertext.
+ assertFalse(initialValue.equals(updatedValue));
}
/**
@@ -145,20 +130,17 @@ public class EntityCryptoTestSuite implements
JupiterTestHelper {
delegator.removeByAnd("TestingCrypto",
UtilMisc.toMap("testingCryptoTypeId", "LOOKUP"));
delegator.create("TestingCrypto", UtilMisc.toMap("testingCryptoId",
"lookup-null", "testingCryptoTypeId", "LOOKUP"));
delegator.create("TestingCrypto", UtilMisc.toMap("testingCryptoId",
"lookup-value", "testingCryptoTypeId", "LOOKUP",
- "encryptedValue", nanoTime, "saltedEncryptedValue", nanoTime));
+ "encryptedValue", nanoTime));
+ // Null values are not encrypted, so lookups against null still work
normally.
// This ends up using EntityExpr contained in EntityConditionList
assertEquals(1,
(EntityQuery.use(delegator).from("TestingCrypto").where("testingCryptoTypeId",
"LOOKUP",
"encryptedValue", null).queryList()).size());
- assertEquals(1,
(EntityQuery.use(delegator).from("TestingCrypto").where("testingCryptoTypeId",
"LOOKUP",
- "saltedEncryptedValue", null).queryList()).size());
- assertEquals(1,
(EntityQuery.use(delegator).from("TestingCrypto").where("testingCryptoTypeId",
"LOOKUP",
- "encryptedValue", nanoTime).queryList()).size());
+ // Regression guard for OFBIZ-13599: encrypted fields are no longer
exact-match lookupable, since
+ // encryption is non-deterministic -- a freshly-encrypted search value
never matches previously
+ // stored ciphertext for the same plaintext.
assertEquals(0,
(EntityQuery.use(delegator).from("TestingCrypto").where("testingCryptoTypeId",
"LOOKUP",
- "saltedEncryptedValue", nanoTime).queryList()).size());
-
- assertEquals(1,
EntityQuery.use(delegator).from("TestingCrypto").where("testingCryptoTypeId",
"LOOKUP",
- "encryptedValue", nanoTime).queryList().size());
+ "encryptedValue", nanoTime).queryList()).size());
}
/**
@@ -170,7 +152,9 @@ public class EntityCryptoTestSuite implements
JupiterTestHelper {
return EntityCondition.makeCondition(
EntityCondition.makeCondition("testingCryptoTypeId",
EntityOperator.IN, UtilMisc.toList("SUB_SELECT_1", "SUB_SELECT_3")),
EntityOperator.AND,
- EntityCondition.makeCondition("encryptedValue",
EntityOperator.EQUALS, nanoTime));
+ // Uses the unencrypted field here: encrypted fields are no longer
exact-match lookupable
+ // (OFBIZ-13599), so this exercises the sub-select
condition-building logic on its own.
+ EntityCondition.makeCondition("unencryptedValue",
EntityOperator.EQUALS, nanoTime));
}
/**
@@ -200,13 +184,13 @@ public class EntityCryptoTestSuite implements
JupiterTestHelper {
delegator.removeByAnd("TestingCrypto",
UtilMisc.toMap("testingCryptoTypeId", "SUB_SELECT_3"));
delegator.create("TestingCrypto", UtilMisc.toMap("testingCryptoId",
"SUB_1", "testingCryptoTypeId", "SUB_SELECT_1",
- "encryptedValue", nanoTime));
+ "unencryptedValue", nanoTime));
delegator.create("TestingCrypto", UtilMisc.toMap("testingCryptoId",
"SUB_2", "testingCryptoTypeId", "SUB_SELECT_2",
- "encryptedValue", nanoTime));
+ "unencryptedValue", nanoTime));
delegator.create("TestingCrypto", UtilMisc.toMap("testingCryptoId",
"SUB_3", "testingCryptoTypeId", "SUB_SELECT_3",
- "encryptedValue", "constant"));
+ "unencryptedValue", "constant"));
- results =
EntityQuery.use(delegator).from("TestingCrypto").where("encryptedValue",
nanoTime).orderBy("testingCryptoId").queryList();
+ results =
EntityQuery.use(delegator).from("TestingCrypto").where("unencryptedValue",
nanoTime).orderBy("testingCryptoId").queryList();
assertEquals(2, results.size());
assertEquals("SUB_1", results.get(0).get("testingCryptoId"));
assertEquals("SUB_2", results.get(1).get("testingCryptoId"));
diff --git
a/framework/entityext/src/main/java/org/apache/ofbiz/entityext/data/EntityDataServices.java
b/framework/entityext/src/main/java/org/apache/ofbiz/entityext/data/EntityDataServices.java
index 38faf2e024..a38f3c77cf 100644
---
a/framework/entityext/src/main/java/org/apache/ofbiz/entityext/data/EntityDataServices.java
+++
b/framework/entityext/src/main/java/org/apache/ofbiz/entityext/data/EntityDataServices.java
@@ -28,10 +28,12 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
+import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Set;
import org.apache.commons.codec.binary.Base64;
import org.apache.ofbiz.base.crypto.DesCrypt;
@@ -49,6 +51,7 @@ import org.apache.ofbiz.entity.datasource.GenericHelperInfo;
import org.apache.ofbiz.entity.jdbc.DatabaseUtil;
import org.apache.ofbiz.entity.model.ModelEntity;
import org.apache.ofbiz.entity.model.ModelField;
+import org.apache.ofbiz.entity.model.ModelViewEntity;
import org.apache.ofbiz.entity.util.EntityListIterator;
import org.apache.ofbiz.entity.util.EntityQuery;
import org.apache.ofbiz.security.Security;
@@ -526,11 +529,23 @@ public class EntityDataServices {
}
for (ModelEntity modelEntity: modelEntities.values()) {
+ if (modelEntity instanceof ModelViewEntity) {
+ // Skip view entities: an encrypted field exposed through a
view is really an alias of a
+ // real entity's field, which this loop also visits directly;
storing through a view here
+ // fails anyway unless the view happens to be uniquely
updatable.
+ continue;
+ }
List<ModelField> fields = modelEntity.getFieldsUnmodifiable();
for (ModelField field: fields) {
if (field.getEncryptMethod().isEncrypted()) {
try {
- List<GenericValue> rows =
EntityQuery.use(delegator).from(modelEntity.getEntityName()).select(field.getName()).queryList();
+ // Select the primary key alongside the encrypted
field: store() below needs the PK
+ // to find the row again, otherwise it fails with
GenericEntityNotFoundException
+ // (OFBIZ-6694).
+ Set<String> fieldsToSelect = new
LinkedHashSet<>(modelEntity.getPkFieldNames());
+ fieldsToSelect.add(field.getName());
+ List<GenericValue> rows =
EntityQuery.use(delegator).from(modelEntity.getEntityName())
+ .select(fieldsToSelect).queryList();
for (GenericValue row: rows) {
row.setString(field.getName(),
row.getString(field.getName()));
row.store();
diff --git
a/framework/security/src/docs/asciidoc/_include/sy-entity-field-encryption.adoc
b/framework/security/src/docs/asciidoc/_include/sy-entity-field-encryption.adoc
new file mode 100644
index 0000000000..fdd98bade5
--- /dev/null
+++
b/framework/security/src/docs/asciidoc/_include/sy-entity-field-encryption.adoc
@@ -0,0 +1,39 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements. See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership. The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License. You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied. See the License for the
+specific language governing permissions and limitations
+under the License.
+////
+
+= Entity field encryption (encrypt="true")
+
+Fields marked `encrypt="true"` in entitydef XML (for example
`CreditCard.cardNumber` and
+`PartyPrivateInformation.socialSecurityNumber`) are encrypted with AES through
+`org.apache.ofbiz.entity.util.EntityCrypto`, using whichever cipher
configuration is safe by
+default rather than a fixed mode (OFBIZ-13599). Values encrypted before this
change remain
+decryptable automatically, so upgrading does not make existing data unreadable.
+
+[CAUTION]
+Existing rows are not re-encrypted automatically. Run the `reencryptFields`
service (permission
+`ENTITY_MAINT`) after upgrading so that data written under the older cipher
configuration is
+rewritten under the current one. It walks every `encrypt="true"` field across
the entity group
+named by its `groupName` parameter (default `org.apache.ofbiz`) and is safe to
run more than once.
+
+`FinAccount.finAccountCode` is no longer marked `encrypt="true"`: it is used
as an exact-match
+redemption code (see `FinAccountHelper.getFinAccountFromCode`), which requires
the same input to
+always compare equal, a property incompatible with the safer, randomized
encryption above. Its
+sibling field `finAccountPin` remains encrypted. Existing `finAccountCode`
values written before
+this change are still ciphertext; run the `decryptFinAccountCodes` service
(permission
+`ACCOUNTING_ADMIN`, also safe to run more than once) to convert them back to
plain text.
diff --git a/framework/security/src/docs/asciidoc/security.adoc
b/framework/security/src/docs/asciidoc/security.adoc
index bb2d65f570..64ad2a4053 100644
--- a/framework/security/src/docs/asciidoc/security.adoc
+++ b/framework/security/src/docs/asciidoc/security.adoc
@@ -43,3 +43,4 @@ For that you may take as an example to follow
https://issues.apache.org/jira/bro
include::_include/sy-password-and-JWT.adoc[leveloffset=+1]
include::_include/sy-CSRF-defense.adoc[leveloffset=+1]
include::_include/sy-impersonation.adoc[leveloffset=+1]
+include::_include/sy-entity-field-encryption.adoc[leveloffset=+1]