Skip to content

Commit 0768891

Browse files
committed
Improve test coverage for TransformedRecordSerializer*.java
It's hard to get to 100% because of some double checking at build and encrypt time.
1 parent 36b7869 commit 0768891

File tree

5 files changed

+270
-7
lines changed

5 files changed

+270
-7
lines changed

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/CipherPool.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,8 @@ public static Cipher borrowCipher(@Nonnull String cipherName) throws GeneralSecu
4747
public static void returnCipher(@Nonnull Cipher cipher) {
4848
MAPPED_POOL.offer(cipher.getAlgorithm(), cipher);
4949
}
50+
51+
public static void invalidateAll() {
52+
MAPPED_POOL.invalidateAll();
53+
}
5054
}

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/MappedPool.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,13 @@ public int getPoolSize(K key) {
103103
return queue == null ? 0 : queue.size();
104104
}
105105

106+
/**
107+
* Invalidate all entries in the pool.
108+
*/
109+
public void invalidateAll() {
110+
pool.invalidateAll();
111+
}
112+
106113
/**
107114
* Function with Exceptions to provide the pool.
108115
*

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/TransformedRecordSerializerJCE.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ protected void decrypt(@Nonnull TransformedRecordSerializerState state, @Nullabl
113113
}
114114
}
115115

116+
@Nonnull
117+
@Override
118+
public RecordSerializer<Message> widen() {
119+
return new TransformedRecordSerializerJCE<>(inner.widen(), compressWhenSerializing, compressionLevel, encryptWhenSerializing, writeValidationRatio, keyManager);
120+
}
121+
116122
/**
117123
* Creates a new {@link Builder TransformedRecordSerializerJCE.Builder} instance
118124
* that is backed by the default serializer for {@link Message}s, namely

fdb-record-layer-core/src/main/java/com/apple/foundationdb/record/provider/common/TransformedRecordSerializerPrefix.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,6 @@ class TransformedRecordSerializerPrefix {
7373
protected static final int TYPE_MASK = 0x07;
7474
protected static final int KEY_SHIFT = 3;
7575

76-
private TransformedRecordSerializerPrefix() {
77-
}
78-
7976
@SuppressWarnings("fallthrough")
8077
public static boolean decodePrefix(@Nonnull TransformedRecordSerializerState state, @Nonnull Tuple primaryKey) {
8178
final long prefix = readVarint(state, primaryKey);

fdb-record-layer-core/src/test/java/com/apple/foundationdb/record/provider/common/TransformedRecordSerializerTest.java

Lines changed: 253 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
import com.apple.foundationdb.record.logging.LogMessageKeys;
3030
import com.apple.foundationdb.record.metadata.RecordType;
3131
import com.apple.foundationdb.tuple.Tuple;
32+
import com.apple.test.BooleanSource;
33+
import com.apple.test.ParameterizedTestUtils;
3234
import com.google.common.base.Strings;
3335
import com.google.common.primitives.Bytes;
3436
import com.google.protobuf.Message;
@@ -38,6 +40,7 @@
3840
import org.junit.jupiter.params.ParameterizedTest;
3941
import org.junit.jupiter.params.provider.Arguments;
4042
import org.junit.jupiter.params.provider.MethodSource;
43+
import org.junit.jupiter.params.provider.ValueSource;
4144
import org.slf4j.Logger;
4245
import org.slf4j.LoggerFactory;
4346

@@ -47,6 +50,7 @@
4750
import javax.crypto.SecretKey;
4851
import java.nio.ByteBuffer;
4952
import java.nio.ByteOrder;
53+
import java.security.InvalidKeyException;
5054
import java.security.Key;
5155
import java.security.NoSuchAlgorithmException;
5256
import java.security.SecureRandom;
@@ -369,21 +373,26 @@ public void unrecognizedEncoding() {
369373
assertEquals(15L, e.getLogInfo().get("encoding"));
370374
}
371375

372-
@Test
373-
public void encryptWhenSerializing() throws Exception {
376+
@ParameterizedTest
377+
@BooleanSource
378+
public void encryptWhenSerializing(boolean compressToo) throws Exception {
374379
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
375380
keyGen.init(128);
376381
SecretKey key = keyGen.generateKey();
377382
TransformedRecordSerializer<Message> serializer = TransformedRecordSerializerJCE.newDefaultBuilder()
378383
.setEncryptWhenSerializing(true)
379384
.setEncryptionKey(key)
385+
.setCompressWhenSerializing(compressToo)
386+
.setCompressionLevel(9)
380387
.setWriteValidationRatio(1.0)
381388
.build();
382389

383390
MySimpleRecord mediumRecord = MySimpleRecord.newBuilder().setRecNo(1066L).setStrValueIndexed(SONNET_108).build();
384391
assertTrue(Bytes.indexOf(mediumRecord.toByteArray(), "brain".getBytes()) >= 0, "should contain clear text");
385392
byte[] serialized = serialize(serializer, mediumRecord);
386-
assertEquals(TransformedRecordSerializerPrefix.PREFIX_ENCRYPTED, serialized[0]);
393+
assertEquals(compressToo ? TransformedRecordSerializerPrefix.PREFIX_COMPRESSED_THEN_ENCRYPTED
394+
: TransformedRecordSerializerPrefix.PREFIX_ENCRYPTED,
395+
serialized[0]);
387396
assertFalse(Bytes.indexOf(serialized, "brain".getBytes()) >= 0, "should not contain clear text");
388397
Message deserialized = deserialize(serializer, Tuple.from(1066L), serialized);
389398
assertEquals(mediumRecord, deserialized);
@@ -457,8 +466,33 @@ public void corruptAnyBit() {
457466
}
458467
}
459468

469+
@ParameterizedTest
470+
@ValueSource(ints = {6, 10})
471+
public void malformedVarintEncoding(int length) {
472+
RecordSerializationException e = assertThrows(RecordSerializationException.class, () -> {
473+
TransformedRecordSerializer<Message> serializer = TransformedRecordSerializer.newDefaultBuilder().build();
474+
byte[] serialized = new byte[length];
475+
Arrays.fill(serialized, (byte)0xFF);
476+
deserialize(serializer, Tuple.from(1066L), serialized);
477+
});
478+
assertThat(e.getMessage(), containsString(length > 64 / 7 ? "transformation prefix too long"
479+
: "transformation prefix malformed"));
480+
}
481+
482+
@Test
483+
public void invalidKeyNumberEncoding() {
484+
RecordSerializationException e = assertThrows(RecordSerializationException.class, () -> {
485+
TransformedRecordSerializer<Message> serializer = TransformedRecordSerializer.newDefaultBuilder().build();
486+
byte[] serialized = new byte[10];
487+
TransformedRecordSerializerPrefix.writeVarint(serialized,
488+
TransformedRecordSerializerPrefix.PREFIX_ENCRYPTED + ((long)Integer.MAX_VALUE + 1 << 3));
489+
deserialize(serializer, Tuple.from(1066L), serialized);
490+
});
491+
assertThat(e.getMessage(), containsString("unrecognized transformation encoding"));
492+
}
493+
460494
@Test
461-
public void encryptDifferentKeys() throws Exception {
495+
public void encryptRollingKeys() throws Exception {
462496
RollingKeyManager keyManager = new RollingKeyManager();
463497
TransformedRecordSerializer<Message> serializer = TransformedRecordSerializerJCE.newDefaultBuilder()
464498
.setEncryptWhenSerializing(true)
@@ -490,6 +524,221 @@ public void encryptDifferentKeys() throws Exception {
490524
assertEquals(records, deserialized);
491525
}
492526

527+
@Test
528+
public void cannotDecryptUnknownKey() throws Exception {
529+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
530+
keyGen.init(128);
531+
SecretKey key = keyGen.generateKey();
532+
SecureRandom random = new SecureRandom();
533+
TransformedRecordSerializer<Message> serializer = TransformedRecordSerializerJCE.newDefaultBuilder()
534+
.setEncryptWhenSerializing(true)
535+
.setKeyManager(new TransformedRecordSerializerKeyManager() {
536+
@Override
537+
public int getSerializationKey() {
538+
return 2;
539+
}
540+
541+
@Override
542+
public Key getKey(final int keyNumber) {
543+
return key;
544+
}
545+
546+
@Override
547+
public String getCipher(final int keyNumber) {
548+
return CipherPool.DEFAULT_CIPHER;
549+
}
550+
551+
@Override
552+
public Random getRandom(final int keyNumber) {
553+
return random;
554+
}
555+
})
556+
.setWriteValidationRatio(1.0)
557+
.build();
558+
559+
MySimpleRecord simpleRecord = MySimpleRecord.newBuilder().setRecNo(1066L).setStrValueIndexed("Hello").build();
560+
RecordTypeUnion unionRecord = RecordTypeUnion.newBuilder().setMySimpleRecord(simpleRecord).build();
561+
byte[] serialized = serialize(serializer, simpleRecord);
562+
TransformedRecordSerializer<Message> deserializer = TransformedRecordSerializerJCE.newDefaultBuilder()
563+
.setEncryptionKey(key)
564+
.build();
565+
RecordSerializationException e = assertThrows(RecordSerializationException.class,
566+
() -> deserialize(deserializer, Tuple.from(1066L), serialized));
567+
assertThat(e.getMessage(), containsString("only provide key number 0"));
568+
}
569+
570+
@ParameterizedTest
571+
@BooleanSource
572+
public void cannotDecryptWithoutKey(boolean jce) throws Exception {
573+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
574+
keyGen.init(128);
575+
TransformedRecordSerializer<Message> serializer = TransformedRecordSerializerJCE.newDefaultBuilder()
576+
.setEncryptWhenSerializing(true)
577+
.setEncryptionKey(keyGen.generateKey())
578+
.setWriteValidationRatio(1.0)
579+
.build();
580+
MySimpleRecord simpleRecord = MySimpleRecord.newBuilder().setRecNo(1066L).setStrValueIndexed("Hello").build();
581+
RecordTypeUnion unionRecord = RecordTypeUnion.newBuilder().setMySimpleRecord(simpleRecord).build();
582+
byte[] serialized = serialize(serializer, simpleRecord);
583+
TransformedRecordSerializer<Message> deserializer;
584+
if (jce) {
585+
deserializer = TransformedRecordSerializerJCE.newDefaultBuilder()
586+
.setWriteValidationRatio(1.0)
587+
.build();
588+
} else {
589+
deserializer = TransformedRecordSerializer.newDefaultBuilder()
590+
.setWriteValidationRatio(1.0)
591+
.build();
592+
}
593+
RecordSerializationException e = assertThrows(RecordSerializationException.class,
594+
() -> deserialize(deserializer, Tuple.from(1066L), serialized));
595+
assertThat(e.getMessage(), containsString(jce ? "missing encryption key or provider during decryption"
596+
: "this serializer cannot decrypt"));
597+
}
598+
599+
@Test
600+
public void keyDoesNotMatchAlgorithm() throws Exception {
601+
KeyGenerator keyGen = KeyGenerator.getInstance("DES");
602+
keyGen.init(56);
603+
try {
604+
TransformedRecordSerializer<Message> serializer = TransformedRecordSerializerJCE.newDefaultBuilder()
605+
.setEncryptWhenSerializing(true)
606+
.setEncryptionKey(keyGen.generateKey())
607+
.setWriteValidationRatio(1.0)
608+
.build();
609+
MySimpleRecord simpleRecord = MySimpleRecord.newBuilder().setRecNo(1066L).setStrValueIndexed("Hello").build();
610+
RecordTypeUnion unionRecord = RecordTypeUnion.newBuilder().setMySimpleRecord(simpleRecord).build();
611+
RecordSerializationException e = assertThrows(RecordSerializationException.class,
612+
() -> serialize(serializer, simpleRecord));
613+
assertThat(e.getMessage(), containsString("encryption error"));
614+
assertThat(e.getCause(), instanceOf(InvalidKeyException.class));
615+
assertThat(e.getCause().getMessage(), containsString("Wrong algorithm"));
616+
} finally {
617+
// We have put something inconsistent in.
618+
CipherPool.invalidateAll();
619+
}
620+
}
621+
622+
@Test
623+
public void changeAlgorithm() throws Exception {
624+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
625+
keyGen.init(128);
626+
TransformedRecordSerializer<Message> serializer = TransformedRecordSerializerJCE.newDefaultBuilder()
627+
.setEncryptWhenSerializing(true)
628+
.setEncryptionKey(keyGen.generateKey())
629+
.setWriteValidationRatio(1.0)
630+
.build();
631+
MySimpleRecord simpleRecord = MySimpleRecord.newBuilder().setRecNo(1066L).setStrValueIndexed("Hello").build();
632+
RecordTypeUnion unionRecord = RecordTypeUnion.newBuilder().setMySimpleRecord(simpleRecord).build();
633+
byte[] serialized = serialize(serializer, simpleRecord);
634+
KeyGenerator keyGen2 = KeyGenerator.getInstance("DES");
635+
keyGen2.init(56);
636+
TransformedRecordSerializer<Message> deserializer = TransformedRecordSerializerJCE.newDefaultBuilder()
637+
.setEncryptWhenSerializing(true)
638+
.setCipherName("DES")
639+
.setEncryptionKey(keyGen2.generateKey())
640+
.setWriteValidationRatio(1.0)
641+
.build();
642+
RecordSerializationException e = assertThrows(RecordSerializationException.class,
643+
() -> deserialize(deserializer, Tuple.from(1066L), serialized));
644+
assertThat(e.getMessage(), containsString("decryption error"));
645+
}
646+
647+
public static Stream<Arguments> compressedAndOrEncrypted() {
648+
return ParameterizedTestUtils.cartesianProduct(
649+
ParameterizedTestUtils.booleans("compressed"),
650+
ParameterizedTestUtils.booleans("encrypted"));
651+
}
652+
653+
@ParameterizedTest
654+
@MethodSource("compressedAndOrEncrypted")
655+
public void typed(boolean compressed, boolean encrypted) throws Exception {
656+
RecordSerializer<MySimpleRecord> typedSerializer = new TypedRecordSerializer<>(
657+
TestRecords1Proto.RecordTypeUnion.getDescriptor().findFieldByNumber(TestRecords1Proto.RecordTypeUnion._MYSIMPLERECORD_FIELD_NUMBER),
658+
TestRecords1Proto.RecordTypeUnion::newBuilder,
659+
TestRecords1Proto.RecordTypeUnion::hasMySimpleRecord,
660+
TestRecords1Proto.RecordTypeUnion::getMySimpleRecord,
661+
TestRecords1Proto.RecordTypeUnion.Builder::setMySimpleRecord);
662+
MySimpleRecord record = MySimpleRecord.newBuilder().setRecNo(1066L).setStrValueIndexed(SONNET_108).build();
663+
664+
if (encrypted) {
665+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
666+
keyGen.init(128);
667+
SecretKey key = keyGen.generateKey();
668+
typedSerializer = TransformedRecordSerializerJCE.newBuilder(typedSerializer)
669+
.setEncryptWhenSerializing(true)
670+
.setEncryptionKey(key)
671+
.setCompressWhenSerializing(compressed)
672+
.setWriteValidationRatio(1.0)
673+
.build();
674+
} else if (compressed) {
675+
typedSerializer = TransformedRecordSerializer.newBuilder(typedSerializer)
676+
.setCompressWhenSerializing(true)
677+
.setWriteValidationRatio(1.0)
678+
.build();
679+
}
680+
681+
byte[] typedSerialized = serialize(typedSerializer, record);
682+
RecordSerializer<Message> untypedSerializer = typedSerializer.widen();
683+
byte[] untypedSerialized = serialize(untypedSerializer, record);
684+
685+
MySimpleRecord typedDeserialized = deserialize(typedSerializer, Tuple.from(1066L), typedSerialized);
686+
assertEquals(record, typedDeserialized);
687+
typedDeserialized = deserialize(typedSerializer, Tuple.from(1066L), untypedSerialized);
688+
assertEquals(record, typedDeserialized);
689+
690+
Message untypedDeserialized = deserialize(untypedSerializer, Tuple.from(1066L), typedSerialized);
691+
assertEquals(record, untypedDeserialized);
692+
untypedDeserialized = deserialize(untypedSerializer, Tuple.from(1066L), untypedSerialized);
693+
assertEquals(record, untypedDeserialized);
694+
}
695+
696+
@Test
697+
public void defaultKeyManagerKey() throws Exception {
698+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
699+
keyGen.init(128);
700+
TransformedRecordSerializerJCE<Message> serializer = TransformedRecordSerializerJCE.newDefaultBuilder()
701+
.setEncryptWhenSerializing(true)
702+
.setEncryptionKey(keyGen.generateKey())
703+
.setWriteValidationRatio(1.0)
704+
.build();
705+
TransformedRecordSerializerKeyManager keyManager = serializer.keyManager;
706+
assertEquals(0, keyManager.getSerializationKey());
707+
708+
RecordSerializationException e = assertThrows(RecordSerializationException.class,
709+
() -> keyManager.getKey(1));
710+
assertThat(e.getMessage(), containsString("only provide key number 0"));
711+
712+
e = assertThrows(RecordSerializationException.class,
713+
() -> keyManager.getCipher(1));
714+
assertThat(e.getMessage(), containsString("only provide key number 0"));
715+
716+
e = assertThrows(RecordSerializationException.class,
717+
() -> keyManager.getRandom(1));
718+
assertThat(e.getMessage(), containsString("only provide key number 0"));
719+
}
720+
721+
@Test
722+
public void invalidKeyManagerBuilder() throws Exception {
723+
TransformedRecordSerializerJCE.Builder<Message> builder = TransformedRecordSerializerJCE.newDefaultBuilder();
724+
builder.setEncryptWhenSerializing(true);
725+
726+
RecordCoreArgumentException e = assertThrows(RecordCoreArgumentException.class, builder::build);
727+
assertThat(e.getMessage(), containsString("cannot encrypt when serializing if encryption key is not set"));
728+
729+
RollingKeyManager keyManager = new RollingKeyManager();
730+
builder.setKeyManager(keyManager);
731+
732+
builder.setCipherName(CipherPool.DEFAULT_CIPHER);
733+
e = assertThrows(RecordCoreArgumentException.class, builder::build);
734+
assertThat(e.getMessage(), containsString("cannot specify both key manager and cipher name"));
735+
736+
builder.clearEncryption();
737+
builder.setEncryptionKey(keyManager.getKey(keyManager.getSerializationKey()));
738+
e = assertThrows(RecordCoreArgumentException.class, builder::build);
739+
assertThat(e.getMessage(), containsString("cannot specify both key manager and encryption key"));
740+
}
741+
493742
private boolean isCompressed(byte[] serialized) {
494743
byte headerByte = serialized[0];
495744
return headerByte == TransformedRecordSerializerPrefix.PREFIX_COMPRESSED ||

0 commit comments

Comments
 (0)