You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by ab...@apache.org on 2019/06/10 09:58:19 UTC

[cayenne] branch STABLE-4.1 updated (8fe18f7 -> 42f57fc)

This is an automated email from the ASF dual-hosted git repository.

abulatski pushed a change to branch STABLE-4.1
in repository https://gitbox.apache.org/repos/asf/cayenne.git.


    from 8fe18f7  Update RELEASE-NOTES
     new 5c9b194  CAY-2584 Crypto: can't use ColumnSelect with encrypted columns
     new 42f57fc  CAY-2584 Crypto: can't use ColumnSelect with encrypted columns

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 RELEASE-NOTES.txt                                  |   1 +
 .../org/apache/cayenne/crypto/CryptoModule.java    |  39 ++---
 .../cayenne/crypto/CryptoServerModuleProvider.java |  11 +-
 .../reader/CryptoRowReaderFactoryDecorator.java    | 165 +++++++++++++++------
 .../apache/cayenne/crypto/Runtime_AES128_IT.java   | 100 +++++++++++++
 .../cayenne/access/jdbc/ColumnDescriptor.java      |   4 +
 .../jdbc/reader/DefaultRowReaderFactory.java       |  28 ++--
 .../translator/select/DefaultSelectTranslator.java |  26 ++--
 8 files changed, 275 insertions(+), 99 deletions(-)


[cayenne] 02/02: CAY-2584 Crypto: can't use ColumnSelect with encrypted columns

Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

abulatski pushed a commit to branch STABLE-4.1
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 42f57fc34a7973a9e61de9f8cd343a40674faf5f
Author: Arseni Bulatski <an...@gmail.com>
AuthorDate: Mon Jun 10 12:39:57 2019 +0300

    CAY-2584 Crypto: can't use ColumnSelect with encrypted columns
---
 .../org/apache/cayenne/crypto/CryptoModule.java    | 39 ++++++----------------
 .../cayenne/crypto/CryptoServerModuleProvider.java | 11 +++---
 .../reader/CryptoRowReaderFactoryDecorator.java    | 10 +++---
 .../jdbc/reader/DefaultRowReaderFactory.java       | 18 +++++-----
 4 files changed, 29 insertions(+), 49 deletions(-)

diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModule.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModule.java
index 5c75ad5..d2a5e6f 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModule.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoModule.java
@@ -18,6 +18,14 @@
  */
 package org.apache.cayenne.crypto;
 
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.sql.Types;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.Date;
+
 import org.apache.cayenne.access.jdbc.reader.RowReaderFactory;
 import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
 import org.apache.cayenne.crypto.batch.CryptoBatchTranslatorFactoryDecorator;
@@ -33,38 +41,11 @@ import org.apache.cayenne.crypto.transformer.TransformerFactory;
 import org.apache.cayenne.crypto.transformer.bytes.BytesTransformerFactory;
 import org.apache.cayenne.crypto.transformer.bytes.DefaultBytesTransformerFactory;
 import org.apache.cayenne.crypto.transformer.bytes.LazyBytesTransformerFactory;
-import org.apache.cayenne.crypto.transformer.value.Base64StringConverter;
-import org.apache.cayenne.crypto.transformer.value.BigDecimalConverter;
-import org.apache.cayenne.crypto.transformer.value.BigIntegerConverter;
-import org.apache.cayenne.crypto.transformer.value.BooleanConverter;
-import org.apache.cayenne.crypto.transformer.value.ByteConverter;
-import org.apache.cayenne.crypto.transformer.value.BytesConverter;
-import org.apache.cayenne.crypto.transformer.value.BytesToBytesConverter;
-import org.apache.cayenne.crypto.transformer.value.DefaultValueTransformerFactory;
-import org.apache.cayenne.crypto.transformer.value.DoubleConverter;
-import org.apache.cayenne.crypto.transformer.value.FloatConverter;
-import org.apache.cayenne.crypto.transformer.value.IntegerConverter;
-import org.apache.cayenne.crypto.transformer.value.LazyValueTransformerFactory;
-import org.apache.cayenne.crypto.transformer.value.LocalDateConverter;
-import org.apache.cayenne.crypto.transformer.value.LocalDateTimeConverter;
-import org.apache.cayenne.crypto.transformer.value.LocalTimeConverter;
-import org.apache.cayenne.crypto.transformer.value.LongConverter;
-import org.apache.cayenne.crypto.transformer.value.ShortConverter;
-import org.apache.cayenne.crypto.transformer.value.Utf8StringConverter;
-import org.apache.cayenne.crypto.transformer.value.UtilDateConverter;
-import org.apache.cayenne.crypto.transformer.value.ValueTransformerFactory;
+import org.apache.cayenne.crypto.transformer.value.*;
 import org.apache.cayenne.di.Binder;
 import org.apache.cayenne.di.MapBuilder;
 import org.apache.cayenne.di.Module;
 
-import java.math.BigDecimal;
-import java.math.BigInteger;
-import java.sql.Types;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.util.Date;
-
 /**
  * Contains cryptography extensions for Cayenne.
  *
@@ -135,7 +116,7 @@ public class CryptoModule implements Module {
         binder.bind(ColumnMapper.class).toInstance(new PatternColumnMapper(DEFAULT_COLUMN_MAPPER_PATTERN));
 
         binder.decorate(BatchTranslatorFactory.class).before(CryptoBatchTranslatorFactoryDecorator.class);
-        binder.decorate(RowReaderFactory.class).before(CryptoRowReaderFactoryDecorator.class);
+        binder.bind(RowReaderFactory.class).to(CryptoRowReaderFactoryDecorator.class);
 
         // decorate Crypto's own services to allow Cayenne to operate over plaintext entities even if crypto keys are
         // not available.
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoServerModuleProvider.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoServerModuleProvider.java
index 9fe8bc8..0e79c3a 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoServerModuleProvider.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/CryptoServerModuleProvider.java
@@ -18,12 +18,13 @@
  */
 package org.apache.cayenne.crypto;
 
-import org.apache.cayenne.configuration.server.CayenneServerModuleProvider;
-import org.apache.cayenne.di.Module;
-
 import java.util.Collection;
 import java.util.Collections;
 
+import org.apache.cayenne.configuration.server.CayenneServerModuleProvider;
+import org.apache.cayenne.configuration.server.ServerModule;
+import org.apache.cayenne.di.Module;
+
 /**
  * @since 4.0
  */
@@ -41,7 +42,7 @@ public class CryptoServerModuleProvider implements CayenneServerModuleProvider {
 
     @Override
     public Collection<Class<? extends Module>> overrides() {
-        // we don't override anything, we only decorate ServerModule services...
-        return Collections.emptyList();
+        Collection modules = Collections.singletonList(ServerModule.class);
+        return modules;
     }
 }
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
index e60600a..2b2f764 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
@@ -18,11 +18,13 @@
  ****************************************************************/
 package org.apache.cayenne.crypto.reader;
 
+import java.sql.ResultSet;
+import java.util.Map;
+
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.jdbc.RowDescriptor;
 import org.apache.cayenne.access.jdbc.reader.DefaultRowReaderFactory;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
-import org.apache.cayenne.access.jdbc.reader.RowReaderFactory;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.crypto.map.ColumnMapper;
@@ -41,9 +43,6 @@ import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.query.QueryMetadata;
 import org.apache.cayenne.query.ScalarResultSegment;
 
-import java.sql.ResultSet;
-import java.util.Map;
-
 public class CryptoRowReaderFactoryDecorator extends DefaultRowReaderFactory {
 
     private TransformerFactory transformerFactory;
@@ -51,8 +50,7 @@ public class CryptoRowReaderFactoryDecorator extends DefaultRowReaderFactory {
     private BytesTransformerFactory bytesTransformerFactory;
     private ValueTransformerFactory valueTransformerFactory;
 
-    public CryptoRowReaderFactoryDecorator(@Inject RowReaderFactory delegate,
-                                           @Inject TransformerFactory transformerFactory,
+    public CryptoRowReaderFactoryDecorator(@Inject TransformerFactory transformerFactory,
                                            @Inject ColumnMapper columnMapper,
                                            @Inject BytesTransformerFactory bytesTransformerFactory,
                                            @Inject ValueTransformerFactory valueTransformerFactory) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
index 507cbbb..4d90adf 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
@@ -18,6 +18,13 @@
  ****************************************************************/
 package org.apache.cayenne.access.jdbc.reader;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.jdbc.RowDescriptor;
@@ -33,13 +40,6 @@ import org.apache.cayenne.query.QueryMetadata;
 import org.apache.cayenne.query.ScalarResultSegment;
 import org.apache.cayenne.reflect.ClassDescriptor;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
 /**
  * @since 4.0
  */
@@ -108,7 +108,7 @@ public class DefaultRowReaderFactory implements RowReaderFactory {
 		}
 	}
 
-	private RowReader<?> createFullRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
+	protected RowReader<?> createFullRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
 			PostprocessorFactory postProcessorFactory) {
 
 		if (queryMetadata.getPageSize() > 0) {
@@ -120,7 +120,7 @@ public class DefaultRowReaderFactory implements RowReaderFactory {
 		}
 	}
 
-	private class PostprocessorFactory {
+	protected class PostprocessorFactory {
 
 		private QueryMetadata queryMetadata;
 		private ExtendedTypeMap extendedTypes;


[cayenne] 01/02: CAY-2584 Crypto: can't use ColumnSelect with encrypted columns

Posted by ab...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

abulatski pushed a commit to branch STABLE-4.1
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit 5c9b194d0ca7c11b56fe5f387947cfc74efdb835
Author: Nikita Timofeev <st...@gmail.com>
AuthorDate: Thu Jun 6 18:09:47 2019 +0300

    CAY-2584 Crypto: can't use ColumnSelect with encrypted columns
---
 RELEASE-NOTES.txt                                  |   1 +
 .../reader/CryptoRowReaderFactoryDecorator.java    | 155 ++++++++++++++++-----
 .../apache/cayenne/crypto/Runtime_AES128_IT.java   | 100 +++++++++++++
 .../cayenne/access/jdbc/ColumnDescriptor.java      |   4 +
 .../jdbc/reader/DefaultRowReaderFactory.java       |  10 +-
 .../translator/select/DefaultSelectTranslator.java |  26 ++--
 6 files changed, 246 insertions(+), 50 deletions(-)

diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index d0ac1c6..144aa21 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -16,6 +16,7 @@ Bug Fixes:
 
 CAY-2573 DI field injection is triggered when creating sql Driver
 CAY-2582 Double insert of manyToMany relationship mapped to Set
+CAY-2584 Crypto: can't use ColumnSelect with encrypted columns
 
 ----------------------------------
 Release: 4.1.B2
diff --git a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
index ab2c08d..e60600a 100644
--- a/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
+++ b/cayenne-crypto/src/main/java/org/apache/cayenne/crypto/reader/CryptoRowReaderFactoryDecorator.java
@@ -20,6 +20,7 @@ package org.apache.cayenne.crypto.reader;
 
 import org.apache.cayenne.access.jdbc.ColumnDescriptor;
 import org.apache.cayenne.access.jdbc.RowDescriptor;
+import org.apache.cayenne.access.jdbc.reader.DefaultRowReaderFactory;
 import org.apache.cayenne.access.jdbc.reader.RowReader;
 import org.apache.cayenne.access.jdbc.reader.RowReaderFactory;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -27,68 +28,70 @@ import org.apache.cayenne.access.types.ExtendedTypeMap;
 import org.apache.cayenne.crypto.map.ColumnMapper;
 import org.apache.cayenne.crypto.transformer.MapTransformer;
 import org.apache.cayenne.crypto.transformer.TransformerFactory;
+import org.apache.cayenne.crypto.transformer.bytes.BytesDecryptor;
+import org.apache.cayenne.crypto.transformer.bytes.BytesTransformerFactory;
+import org.apache.cayenne.crypto.transformer.value.ValueDecryptor;
+import org.apache.cayenne.crypto.transformer.value.ValueTransformerFactory;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.query.EntityResultSegment;
 import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.query.ScalarResultSegment;
 
 import java.sql.ResultSet;
 import java.util.Map;
 
-public class CryptoRowReaderFactoryDecorator implements RowReaderFactory {
+public class CryptoRowReaderFactoryDecorator extends DefaultRowReaderFactory {
 
-    private RowReaderFactory delegate;
     private TransformerFactory transformerFactory;
     private ColumnMapper columnMapper;
+    private BytesTransformerFactory bytesTransformerFactory;
+    private ValueTransformerFactory valueTransformerFactory;
 
     public CryptoRowReaderFactoryDecorator(@Inject RowReaderFactory delegate,
                                            @Inject TransformerFactory transformerFactory,
-                                           @Inject ColumnMapper columnMapper) {
-        this.delegate = delegate;
+                                           @Inject ColumnMapper columnMapper,
+                                           @Inject BytesTransformerFactory bytesTransformerFactory,
+                                           @Inject ValueTransformerFactory valueTransformerFactory) {
         this.transformerFactory = transformerFactory;
         this.columnMapper = columnMapper;
+        this.bytesTransformerFactory = bytesTransformerFactory;
+        this.valueTransformerFactory = valueTransformerFactory;
     }
 
     @Override
-    public RowReader<?> rowReader(final RowDescriptor descriptor, QueryMetadata queryMetadata, DbAdapter adapter,
+    public RowReader<?> rowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, DbAdapter adapter,
                                   Map<ObjAttribute, ColumnDescriptor> attributeOverrides) {
+        RowDescriptor encryptedRowDescriptor = encryptedRowDescriptor(descriptor, adapter.getExtendedTypes());
+        return super.rowReader(encryptedRowDescriptor, queryMetadata, adapter, attributeOverrides);
+    }
 
-        final RowReader<?> delegateReader = delegate.rowReader(encryptedRowDescriptor(descriptor, adapter.getExtendedTypes()),
-                queryMetadata,
-                adapter,
-                attributeOverrides);
-
-        return new RowReader<Object>() {
-
-            private boolean decryptorCompiled;
-            private MapTransformer decryptor;
-
-            private void ensureDecryptorCompiled(Object row) {
-                if (!decryptorCompiled) {
-                    decryptor = transformerFactory.decryptor(descriptor.getColumns(), row);
-                    decryptorCompiled = true;
-                }
-            }
-
-            @Override
-            public Object readRow(ResultSet resultSet) {
-                Object row = delegateReader.readRow(resultSet);
-
-                ensureDecryptorCompiled(row);
-
-                if (decryptor != null) {
-
-                    @SuppressWarnings({"unchecked", "rawtypes"})
-                    Map<String, Object> map = (Map) row;
+    @Override
+    protected RowReader<?> createScalarRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
+                                                 ScalarResultSegment segment) {
+        RowReader<?> scalarRowReader = super
+                .createScalarRowReader(descriptor, queryMetadata, segment);
+        return new DecoratedScalarRowReader(descriptor.getColumns()[segment.getColumnOffset()], scalarRowReader);
+    }
 
-                    decryptor.transform(map);
-                }
+    @Override
+    protected RowReader<?> createEntityRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
+                                                 EntityResultSegment resultMetadata,
+                                                 PostprocessorFactory postProcessorFactory) {
+        RowReader<?> entityRowReader = super
+                .createEntityRowReader(descriptor, queryMetadata, resultMetadata, postProcessorFactory);
+        return new DecoratedFullRowReader(descriptor, entityRowReader);
+    }
 
-                return row;
-            }
-        };
+    @Override
+    protected RowReader<?> createFullRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
+                                               PostprocessorFactory postProcessorFactory) {
+        RowReader<?> fullRowReader = super
+                .createFullRowReader(descriptor, queryMetadata, postProcessorFactory);
+        return new DecoratedFullRowReader(descriptor, fullRowReader);
     }
 
     protected RowDescriptor encryptedRowDescriptor(RowDescriptor descriptor, ExtendedTypeMap typeMap) {
@@ -121,6 +124,82 @@ public class CryptoRowReaderFactoryDecorator implements RowReaderFactory {
             encryptedConverters[i] = t;
         }
 
-        return new RowDescriptor(originalColumns, encryptedConverters);
+        return new DecoratedRowDescriptor(descriptor, originalColumns, encryptedConverters);
+    }
+
+    private static class DecoratedRowDescriptor extends RowDescriptor {
+
+        private final RowDescriptor original;
+
+        DecoratedRowDescriptor(RowDescriptor rowDescriptor, ColumnDescriptor[] columns, ExtendedType[] converters) {
+            this.original = rowDescriptor;
+            this.columns = columns;
+            this.converters = converters;
+        }
+
+        public RowDescriptor unwrap() {
+            return original;
+        }
+    }
+
+    private class DecoratedScalarRowReader implements RowReader<Object> {
+        private final RowReader<?> delegateReader;
+        private final ValueDecryptor valueDecryptor;
+        private final BytesDecryptor bytesDecryptor;
+
+        DecoratedScalarRowReader(ColumnDescriptor descriptor, RowReader<?> delegateReader) {
+            this.delegateReader = delegateReader;
+            if(descriptor.getAttribute() != null && columnMapper.isEncrypted(descriptor.getAttribute())) {
+                this.valueDecryptor = valueTransformerFactory.decryptor(descriptor.getAttribute());
+                this.bytesDecryptor = bytesTransformerFactory.decryptor();
+            } else {
+                this.valueDecryptor = null;
+                this.bytesDecryptor = null;
+            }
+        }
+
+        @Override
+        public Object readRow(ResultSet resultSet) {
+            Object value = delegateReader.readRow(resultSet);
+            if(valueDecryptor == null) {
+                return value;
+            }
+            return valueDecryptor.decrypt(bytesDecryptor, value);
+        }
+    }
+
+    private class DecoratedFullRowReader implements RowReader<Object> {
+
+        private final RowDescriptor descriptor;
+        private final RowReader<?> delegateReader;
+        private boolean decryptorCompiled;
+        private MapTransformer decryptor;
+
+        DecoratedFullRowReader(RowDescriptor descriptor, RowReader<?> delegateReader) {
+            this.descriptor = descriptor;
+            this.delegateReader = delegateReader;
+        }
+
+        private void ensureDecryptorCompiled(Object row) {
+            if (!decryptorCompiled) {
+                decryptor = transformerFactory.decryptor(descriptor.getColumns(), row);
+                decryptorCompiled = true;
+            }
+        }
+
+        @Override
+        public Object readRow(ResultSet resultSet) {
+            Object row = delegateReader.readRow(resultSet);
+
+            ensureDecryptorCompiled(row);
+
+            if (decryptor != null) {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> map = (Map<String, Object>) row;
+                decryptor.transform(map);
+            }
+
+            return row;
+        }
     }
 }
diff --git a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java
index 1e4b4bc..b6f75d3 100644
--- a/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java
+++ b/cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_IT.java
@@ -23,6 +23,8 @@ import org.apache.cayenne.crypto.db.Table1;
 import org.apache.cayenne.crypto.db.Table2;
 import org.apache.cayenne.crypto.transformer.value.IntegerConverter;
 import org.apache.cayenne.crypto.unit.CryptoUnitUtils;
+import org.apache.cayenne.exp.Property;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.SelectQuery;
 import org.junit.Before;
 import org.junit.Test;
@@ -156,4 +158,102 @@ public class Runtime_AES128_IT extends Runtime_AES128_Base {
         assertEquals(61, result.get(0).getCryptoInt());
     }
 
+    @Test
+    public void test_ColumnQueryObject() {
+
+        ObjectContext context = runtime.newContext();
+
+        Table1 t1 = context.newObject(Table1.class);
+        t1.setCryptoInt(1);
+        t1.setCryptoString("test");
+        context.commitChanges();
+
+        List<Table1> result = ObjectSelect
+                .columnQuery(Table1.class, Property.createSelf(Table1.class))
+                .select(context);
+
+        assertEquals(1, result.size());
+        assertEquals(1, result.get(0).getCryptoInt());
+        assertEquals("test", result.get(0).getCryptoString());
+    }
+
+    @Test
+    public void test_ColumnQueryObjectWithPlainScalar() {
+
+        ObjectContext context = runtime.newContext();
+
+        Table1 t1 = context.newObject(Table1.class);
+        t1.setCryptoInt(1);
+        t1.setPlainInt(2);
+        t1.setCryptoString("test");
+        context.commitChanges();
+
+        List<Object[]> result = ObjectSelect
+                .columnQuery(Table1.class, Property.createSelf(Table1.class), Table1.PLAIN_INT)
+                .select(context);
+
+        assertEquals(1, result.size());
+        assertEquals(1, ((Table1)result.get(0)[0]).getCryptoInt());
+        assertEquals("test", ((Table1)result.get(0)[0]).getCryptoString());
+        assertEquals(2, result.get(0)[1]);
+    }
+
+    @Test
+    public void test_ColumnQueryObjectWithEncryptedScalar() {
+
+        ObjectContext context = runtime.newContext();
+
+        Table1 t1 = context.newObject(Table1.class);
+        t1.setCryptoInt(1);
+        t1.setPlainInt(2);
+        t1.setCryptoString("test");
+        context.commitChanges();
+
+        List<Object[]> result = ObjectSelect
+                .columnQuery(Table1.class, Property.createSelf(Table1.class), Table1.CRYPTO_INT)
+                .select(context);
+
+        assertEquals(1, result.size());
+        assertEquals(1, ((Table1)result.get(0)[0]).getCryptoInt());
+        assertEquals("test", ((Table1)result.get(0)[0]).getCryptoString());
+        assertEquals(1, result.get(0)[1]);
+    }
+
+    @Test
+    public void test_ColumnQuerySingleScalar() {
+        ObjectContext context = runtime.newContext();
+
+        Table1 t1 = context.newObject(Table1.class);
+        t1.setCryptoInt(1);
+        t1.setCryptoString("test");
+        context.commitChanges();
+
+        List<String> result = ObjectSelect
+                .columnQuery(Table1.class, Table1.CRYPTO_STRING)
+                .select(context);
+
+        assertEquals(1, result.size());
+        assertEquals("test", result.get(0));
+    }
+
+    @Test
+    public void test_ColumnQueryMultipleScalars() {
+        ObjectContext context = runtime.newContext();
+
+        Table1 t1 = context.newObject(Table1.class);
+        t1.setCryptoInt(1);
+        t1.setCryptoString("test");
+        t1.setPlainInt(2);
+        context.commitChanges();
+
+        List<Object[]> result = ObjectSelect
+                .columnQuery(Table1.class, Table1.CRYPTO_STRING, Table1.CRYPTO_INT, Table1.PLAIN_INT)
+                .select(context);
+
+        assertEquals(1, result.size());
+        assertEquals("test", result.get(0)[0]);
+        assertEquals(1, result.get(0)[1]);
+        assertEquals(2, result.get(0)[2]);
+    }
+
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
index 632f5e3..cd8d1fc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/ColumnDescriptor.java
@@ -149,6 +149,10 @@ public class ColumnDescriptor {
         return name;
     }
 
+    public void setAttribute(DbAttribute attribute) {
+        this.attribute = attribute;
+    }
+
     /**
      * Returns a DbAttribute for this column. Since columns descriptors can be
      * initialized in a context where a DbAttribite is unknown, this method may
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
index 36c7027..507cbbb 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/reader/DefaultRowReaderFactory.java
@@ -70,7 +70,7 @@ public class DefaultRowReaderFactory implements RowReaderFactory {
 				return createEntityRowReader(descriptor, queryMetadata, (EntityResultSegment) segment,
 						postProcessorFactory);
 			} else {
-				return new ScalarRowReader<>(descriptor, (ScalarResultSegment) segment);
+				return createScalarRowReader(descriptor, queryMetadata, (ScalarResultSegment) segment);
 			}
 		} else {
 			CompoundRowReader reader = new CompoundRowReader(resultWidth);
@@ -84,7 +84,7 @@ public class DefaultRowReaderFactory implements RowReaderFactory {
 							createEntityRowReader(descriptor, queryMetadata, (EntityResultSegment) segment,
 									postProcessorFactory));
 				} else {
-					reader.addRowReader(i, new ScalarRowReader<>(descriptor, (ScalarResultSegment) segment));
+					reader.addRowReader(i, createScalarRowReader(descriptor, queryMetadata, (ScalarResultSegment) segment));
 				}
 			}
 
@@ -92,7 +92,11 @@ public class DefaultRowReaderFactory implements RowReaderFactory {
 		}
 	}
 
-	private RowReader<?> createEntityRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
+	protected RowReader<?> createScalarRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata, ScalarResultSegment segment) {
+		return new ScalarRowReader<Object>(descriptor, segment);
+	}
+
+	protected RowReader<?> createEntityRowReader(RowDescriptor descriptor, QueryMetadata queryMetadata,
 			EntityResultSegment resultMetadata, PostprocessorFactory postProcessorFactory) {
 
 		if (queryMetadata.getPageSize() > 0) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
index 4a2c591..2eefd81 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
@@ -437,7 +437,8 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 
 		for(Property<?> property : query.getColumns()) {
 			joinTableAliasForProperty[0] = null;
-			int expressionType = property.getExpression().getType();
+			Expression propertyExpression = property.getExpression();
+			int expressionType = propertyExpression.getType();
 
 			// forbid direct selection of toMany relationships columns
 			if(property.getType() != null && (expressionType == Expression.OBJ_PATH || expressionType == Expression.DB_PATH)
@@ -456,7 +457,7 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 			// Qualifier Translator in case of Object Columns have side effect -
 			// it will create required joins, that we catch with listener above.
 			// And we force created join alias for all columns of Object we select.
-			qualifierTranslator.setQualifier(property.getExpression());
+			qualifierTranslator.setQualifier(propertyExpression);
 			qualifierTranslator.setForceJoinForRelations(objectProperty);
 			StringBuilder builder = qualifierTranslator.appendPart(new StringBuilder());
 
@@ -481,13 +482,15 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 					builder.append(" AS ").append(alias);
 				}
 
-				int type = getJdbcTypeForProperty(property);
+				DbAttribute attribute = getAttributeForProperty(propertyExpression);
+				int type = attribute == null ? getJdbcTypeForProperty(property) : attribute.getType();
 				ColumnDescriptor descriptor;
 				if(property.getType() != null) {
 					descriptor = new ColumnDescriptor(builder.toString(), type, property.getType().getCanonicalName());
 				} else {
 					descriptor = new ColumnDescriptor(builder.toString(), type);
 				}
+				descriptor.setAttribute(attribute);
 				descriptor.setDataRowKey(alias);
 				descriptor.setIsExpression(true);
 				columns.add(descriptor);
@@ -508,18 +511,18 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 		return columns;
 	}
 
-	private int getJdbcTypeForProperty(Property<?> property) {
-		int expressionType = property.getExpression().getType();
+	private DbAttribute getAttributeForProperty(Expression propertyExpression) {
+		int expressionType = propertyExpression.getType();
 		if(expressionType == Expression.OBJ_PATH) {
 			// Scan obj path, stop as soon as DbAttribute found
 			for (PathComponent<ObjAttribute, ObjRelationship> component :
-					getQueryMetadata().getObjEntity().resolvePath(property.getExpression(), getPathAliases())) {
+					getQueryMetadata().getObjEntity().resolvePath(propertyExpression, getPathAliases())) {
 				if(component.getAttribute() != null) {
 					Iterator<CayenneMapEntry> dbPathIterator = component.getAttribute().getDbPathIterator();
 					while (dbPathIterator.hasNext()) {
 						Object pathPart = dbPathIterator.next();
 						if (pathPart instanceof DbAttribute) {
-							return ((DbAttribute) pathPart).getType();
+							return ((DbAttribute) pathPart);
 						}
 					}
 				}
@@ -527,12 +530,17 @@ public class DefaultSelectTranslator extends QueryAssembler implements SelectTra
 		} else if(expressionType == Expression.DB_PATH) {
 			// Scan db path, stop as soon as DbAttribute found
 			for (PathComponent<DbAttribute, DbRelationship> component :
-					getQueryMetadata().getDbEntity().resolvePath(property.getExpression(), getPathAliases())) {
+					getQueryMetadata().getDbEntity().resolvePath(propertyExpression, getPathAliases())) {
 				if(component.getAttribute() != null) {
-					return component.getAttribute().getType();
+					return component.getAttribute();
 				}
 			}
 		}
+
+		return null;
+	}
+
+	private int getJdbcTypeForProperty(Property<?> property) {
 		// NOTE: If no attribute found or expression have some other type
 	 	// return JDBC type based on Java type of the property.
 		// This can lead to incorrect behavior in case we deal with some custom type