diff --git a/csharp/AppEncryption/AppEncryption.IntegrationTests/AppEncryption.IntegrationTests.csproj b/csharp/AppEncryption/AppEncryption.IntegrationTests/AppEncryption.IntegrationTests.csproj index 6fba144aa..bb567904f 100644 --- a/csharp/AppEncryption/AppEncryption.IntegrationTests/AppEncryption.IntegrationTests.csproj +++ b/csharp/AppEncryption/AppEncryption.IntegrationTests/AppEncryption.IntegrationTests.csproj @@ -21,6 +21,7 @@ + diff --git a/csharp/AppEncryption/AppEncryption.IntegrationTests/Regression/AppEncryptionParameterizedTest.cs b/csharp/AppEncryption/AppEncryption.IntegrationTests/Regression/AppEncryptionParameterizedTest.cs index c00b28cf9..3c177d119 100644 --- a/csharp/AppEncryption/AppEncryption.IntegrationTests/Regression/AppEncryptionParameterizedTest.cs +++ b/csharp/AppEncryption/AppEncryption.IntegrationTests/Regression/AppEncryptionParameterizedTest.cs @@ -205,7 +205,7 @@ public IEnumerator GetEnumerator() private object[] GenerateMocks(KeyState cacheIK, KeyState metaIK, KeyState cacheSK, KeyState metaSK) { - Partition partition = new Partition( + Partition partition = new DefaultPartition( cacheIK + "CacheIK_" + metaIK + "MetaIK_" + DateTimeUtils.GetCurrentTimeAsUtcIsoDateTimeOffset() + "_" + Random.Next(), cacheSK + "CacheSK_" + metaSK + "MetaSK_" + DateTimeUtils.GetCurrentTimeAsUtcIsoDateTimeOffset() + "_" + Random.Next(), diff --git a/csharp/AppEncryption/AppEncryption.IntegrationTests/Regression/DynamoDbCompatibilityTest.cs b/csharp/AppEncryption/AppEncryption.IntegrationTests/Regression/DynamoDbCompatibilityTest.cs new file mode 100644 index 000000000..324083014 --- /dev/null +++ b/csharp/AppEncryption/AppEncryption.IntegrationTests/Regression/DynamoDbCompatibilityTest.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; +using GoDaddy.Asherah.AppEncryption.Kms; +using GoDaddy.Asherah.AppEncryption.Persistence; +using GoDaddy.Asherah.AppEncryption.Tests; +using GoDaddy.Asherah.AppEncryption.Tests.AppEncryption.Persistence; +using GoDaddy.Asherah.Crypto; +using Xunit; + +namespace GoDaddy.Asherah.AppEncryption.IntegrationTests.Regression +{ + public class DynamoDbCompatibilityTest : IClassFixture, IClassFixture + { + private const string StaticMasterKey = "thisIsAStaticMasterKeyForTesting"; + private const string PartitionKey = "Id"; + private const string SortKey = "Created"; + private const string DefaultTableName = "EncryptionKey"; + + private readonly DynamoDbMetastoreImpl dynamoDbMetastoreImpl; + private readonly DynamoDbMetastoreImpl dynamoDbMetastoreImplWithKeySuffix; + + public DynamoDbCompatibilityTest(DynamoDBContainerFixture dynamoDbContainerFixture) + { + // Use AWS SDK to create client and initialize table + AmazonDynamoDBConfig amazonDynamoDbConfig = new AmazonDynamoDBConfig + { + ServiceURL = dynamoDbContainerFixture.ServiceUrl, + AuthenticationRegion = "us-west-2", + }; + IAmazonDynamoDB tempDynamoDbClient = new AmazonDynamoDBClient(amazonDynamoDbConfig); + CreateTableRequest request = new CreateTableRequest + { + TableName = DefaultTableName, + AttributeDefinitions = new List + { + new AttributeDefinition(PartitionKey, ScalarAttributeType.S), + new AttributeDefinition(SortKey, ScalarAttributeType.N), + }, + KeySchema = new List + { + new KeySchemaElement(PartitionKey, KeyType.HASH), + new KeySchemaElement(SortKey, KeyType.RANGE), + }, + ProvisionedThroughput = new ProvisionedThroughput(1L, 1L), + }; + tempDynamoDbClient.CreateTableAsync(request).Wait(); + + // Use a builder without the suffix + dynamoDbMetastoreImpl = DynamoDbMetastoreImpl.NewBuilder() + .WithEndPointConfiguration(dynamoDbContainerFixture.ServiceUrl, "us-west-2") + .Build(); + + // Connect to the same metastore but initialize it with a key suffix + dynamoDbMetastoreImplWithKeySuffix = DynamoDbMetastoreImpl.NewBuilder() + .WithEndPointConfiguration(dynamoDbContainerFixture.ServiceUrl, "us-west-2") + .WithKeySuffix("us-west-2") + .Build(); + } + + [Fact] + private void TestRegionSuffixBackwardCompatibility() + { + string dataRowString; + string originalPayloadString; + string decryptedPayloadString; + + // Encrypt originalPayloadString with metastore without key suffix + using (SessionFactory sessionFactory = SessionFactory.NewBuilder("productId", "reference_app") + .WithMetastore(dynamoDbMetastoreImpl) + .WithCryptoPolicy(new NeverExpiredCryptoPolicy()) + .WithKeyManagementService(new StaticKeyManagementServiceImpl(StaticMasterKey)) + .Build()) + { + using (Session sessionBytes = sessionFactory.GetSessionBytes("shopper123")) + { + originalPayloadString = "mysupersecretpayload"; + byte[] dataRowRecordBytes = + sessionBytes.Encrypt(Encoding.UTF8.GetBytes(originalPayloadString)); + + // Consider this us "persisting" the DRR + dataRowString = Convert.ToBase64String(dataRowRecordBytes); + } + } + + // Decrypt dataRowString with metastore with key suffix + using (SessionFactory sessionFactory = SessionFactory.NewBuilder("productId", "reference_app") + .WithMetastore(dynamoDbMetastoreImplWithKeySuffix) + .WithCryptoPolicy(new NeverExpiredCryptoPolicy()) + .WithKeyManagementService(new StaticKeyManagementServiceImpl(StaticMasterKey)) + .Build()) + { + using (Session sessionBytes = sessionFactory.GetSessionBytes("shopper123")) + { + byte[] newDataRowRecordBytes = Convert.FromBase64String(dataRowString); + + // Decrypt the payload + decryptedPayloadString = Encoding.UTF8.GetString(sessionBytes.Decrypt(newDataRowRecordBytes)); + } + } + + // Verify that we were able to decrypt with a suffixed builder + Assert.Equal(decryptedPayloadString, originalPayloadString); + } + } +} diff --git a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/PartitionTest.cs b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/DefaultPartitionTest.cs similarity index 92% rename from csharp/AppEncryption/AppEncryption.Tests/AppEncryption/PartitionTest.cs rename to csharp/AppEncryption/AppEncryption.Tests/AppEncryption/DefaultPartitionTest.cs index ea0be0a6c..958863d43 100644 --- a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/PartitionTest.cs +++ b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/DefaultPartitionTest.cs @@ -3,7 +3,7 @@ namespace GoDaddy.Asherah.AppEncryption.Tests.AppEncryption { [Collection("Logger Fixture collection")] - public class PartitionTest + public class DefaultPartitionTest { private const string TestPartitionId = "test_partition_id"; private const string TestServiceId = "test_service_id"; @@ -11,9 +11,9 @@ public class PartitionTest private readonly Partition partition; - public PartitionTest() + public DefaultPartitionTest() { - partition = new Partition(TestPartitionId, TestServiceId, TestProductId); + partition = new DefaultPartition(TestPartitionId, TestServiceId, TestProductId); } [Fact] diff --git a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Envelope/EnvelopeEncryptionJsonImplTest.cs b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Envelope/EnvelopeEncryptionJsonImplTest.cs index 0a3897854..6329f72c3 100644 --- a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Envelope/EnvelopeEncryptionJsonImplTest.cs +++ b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Envelope/EnvelopeEncryptionJsonImplTest.cs @@ -21,7 +21,7 @@ namespace GoDaddy.Asherah.AppEncryption.Tests.AppEncryption.Envelope public class EnvelopeEncryptionJsonImplTest : IClassFixture { private readonly Partition partition = - new Partition("shopper_123", "payments", "ecomm"); + new DefaultPartition("shopper_123", "payments", "ecomm"); // Setup DateTimeOffsets truncated to seconds and separated by hour to isolate overlap in case of interacting with multiple level keys private readonly DateTimeOffset drkDateTime = DateTimeOffset.UtcNow.Truncate(TimeSpan.FromSeconds(1)); @@ -188,7 +188,7 @@ private void TestWithIntermediateKeyForReadWithKeyCachedAndNotExpiredShouldUseCa byte[] actualBytes = envelopeEncryptionJsonImplSpy.Object.WithIntermediateKeyForRead( keyMetaMock.Object, functionWithIntermediateKey); Assert.Equal(expectedBytes, actualBytes); - envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(It.IsAny()), Times.Never); + envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(It.IsAny()), Times.Never); intermediateCryptoKeyMock.Verify(x => x.Dispose()); } @@ -206,7 +206,7 @@ private void TestWithIntermediateKeyForReadWithKeyCachedAndNotExpiredAndNotifyEx byte[] actualBytes = envelopeEncryptionJsonImplSpy.Object.WithIntermediateKeyForRead( keyMetaMock.Object, functionWithIntermediateKey); Assert.Equal(expectedBytes, actualBytes); - envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(It.IsAny()), Times.Never); + envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(It.IsAny()), Times.Never); // TODO : Add verify for notification not being called once implemented intermediateCryptoKeyMock.Verify(x => x.Dispose()); @@ -228,7 +228,7 @@ private void TestWithIntermediateKeyForReadWithKeyCachedAndExpiredAndNotifyExpir byte[] actualBytes = envelopeEncryptionJsonImplSpy.Object.WithIntermediateKeyForRead( keyMetaMock.Object, functionWithIntermediateKey); Assert.Equal(expectedBytes, actualBytes); - envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(It.IsAny()), Times.Never); + envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(It.IsAny()), Times.Never); // TODO : Add verify for notification not being called once implemented intermediateCryptoKeyMock.Verify(x => x.Dispose()); @@ -238,7 +238,7 @@ private void TestWithIntermediateKeyForReadWithKeyCachedAndExpiredAndNotifyExpir private void TestWithIntermediateKeyForReadWithKeyNotCachedAndCannotCacheAndNotExpiredShouldLookup() { keyMetaMock.Setup(x => x.Created).Returns(ikDateTime); - envelopeEncryptionJsonImplSpy.Setup(x => x.GetIntermediateKey(It.IsAny())) + envelopeEncryptionJsonImplSpy.Setup(x => x.GetIntermediateKey(It.IsAny())) .Returns(intermediateCryptoKeyMock.Object); byte[] expectedBytes = { 0, 1, 2, 3 }; @@ -247,7 +247,7 @@ private void TestWithIntermediateKeyForReadWithKeyNotCachedAndCannotCacheAndNotE byte[] actualBytes = envelopeEncryptionJsonImplSpy.Object.WithIntermediateKeyForRead( keyMetaMock.Object, functionWithIntermediateKey); Assert.Equal(expectedBytes, actualBytes); - envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(keyMetaMock.Object.Created)); + envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(keyMetaMock.Object)); intermediateKeyCacheMock.Verify(x => x.PutAndGetUsable(It.IsAny(), It.IsAny()), Times.Never); intermediateCryptoKeyMock.Verify(x => x.Dispose()); } @@ -256,7 +256,7 @@ private void TestWithIntermediateKeyForReadWithKeyNotCachedAndCannotCacheAndNotE private void TestWithIntermediateKeyForReadWithKeyNotCachedAndCanCacheAndNotExpiredShouldLookupAndCache() { keyMetaMock.Setup(x => x.Created).Returns(ikDateTime); - envelopeEncryptionJsonImplSpy.Setup(x => x.GetIntermediateKey(ikDateTime)) + envelopeEncryptionJsonImplSpy.Setup(x => x.GetIntermediateKey(keyMetaMock.Object)) .Returns(intermediateCryptoKeyMock.Object); cryptoPolicyMock.Setup(x => x.CanCacheIntermediateKeys()).Returns(true); intermediateCryptoKeyMock.Setup(x => x.GetCreated()).Returns(ikDateTime); @@ -270,7 +270,7 @@ private void TestWithIntermediateKeyForReadWithKeyNotCachedAndCanCacheAndNotExpi byte[] actualBytes = envelopeEncryptionJsonImplSpy.Object.WithIntermediateKeyForRead( keyMetaMock.Object, functionWithIntermediateKey); Assert.Equal(expectedBytes, actualBytes); - envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(keyMetaMock.Object.Created)); + envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(keyMetaMock.Object)); intermediateKeyCacheMock.Verify(x => x.PutAndGetUsable(intermediateCryptoKeyMock.Object.GetCreated(), intermediateCryptoKeyMock.Object)); intermediateCryptoKeyMock.Verify(x => x.Dispose()); } @@ -279,7 +279,7 @@ private void TestWithIntermediateKeyForReadWithKeyNotCachedAndCanCacheAndNotExpi private void TestWithIntermediateKeyForReadWithKeyNotCachedAndCanCacheAndCacheUpdateFailsShouldLookupAndFailAndDisposeKey() { keyMetaMock.Setup(x => x.Created).Returns(ikDateTime); - envelopeEncryptionJsonImplSpy.Setup(x => x.GetIntermediateKey(It.IsAny())) + envelopeEncryptionJsonImplSpy.Setup(x => x.GetIntermediateKey(It.IsAny())) .Returns(intermediateCryptoKeyMock.Object); cryptoPolicyMock.Setup(x => x.CanCacheIntermediateKeys()).Returns(true); intermediateKeyCacheMock.Setup(x => x.PutAndGetUsable(It.IsAny(), It.IsAny())) @@ -290,7 +290,7 @@ private void TestWithIntermediateKeyForReadWithKeyNotCachedAndCanCacheAndCacheUp Assert.Throws(() => envelopeEncryptionJsonImplSpy.Object.WithIntermediateKeyForRead( keyMetaMock.Object, functionWithIntermediateKey)); - envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(keyMetaMock.Object.Created)); + envelopeEncryptionJsonImplSpy.Verify(x => x.GetIntermediateKey(keyMetaMock.Object)); intermediateCryptoKeyMock.Verify(x => x.Dispose()); } @@ -1262,7 +1262,8 @@ private void TestGetIntermediateKeyWithParentKeyMetaShouldSucceed() envelopeEncryptionJsonImplSpy.Setup(x => x.DecryptKey(keyRecord, systemCryptoKeyMock.Object)) .Returns(intermediateCryptoKeyMock.Object); - CryptoKey actualIntermediateKey = envelopeEncryptionJsonImplSpy.Object.GetIntermediateKey(ikDateTime); + keyMetaMock.Setup(x => x.Created).Returns(ikDateTime); + CryptoKey actualIntermediateKey = envelopeEncryptionJsonImplSpy.Object.GetIntermediateKey(keyMetaMock.Object); Assert.Equal(intermediateCryptoKeyMock.Object, actualIntermediateKey); envelopeEncryptionJsonImplSpy.Verify(x => x.WithExistingSystemKey( (KeyMeta)keyRecord.ParentKeyMeta, false, It.IsAny>())); @@ -1279,7 +1280,7 @@ private void TestGetIntermediateKeyWithoutParentKeyMetaShouldFail() envelopeEncryptionJsonImplSpy.Setup(x => x.LoadKeyRecord(It.IsAny(), ikDateTime)) .Returns(keyRecord); - Assert.Throws(() => envelopeEncryptionJsonImplSpy.Object.GetIntermediateKey(ikDateTime)); + Assert.Throws(() => envelopeEncryptionJsonImplSpy.Object.GetIntermediateKey(keyMetaMock.Object)); } [Fact] diff --git a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Json/AppJsonEncryptionImplTest.cs b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Json/AppJsonEncryptionImplTest.cs index 6aeaa21dd..06e89a3d1 100644 --- a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Json/AppJsonEncryptionImplTest.cs +++ b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Json/AppJsonEncryptionImplTest.cs @@ -24,7 +24,7 @@ public class AppJsonEncryptionImplTest : IClassFixture public AppJsonEncryptionImplTest() { - partition = new Partition("PARTITION", "SYSTEM", "PRODUCT"); + partition = new DefaultPartition("PARTITION", "SYSTEM", "PRODUCT"); Dictionary memoryPersistence = new Dictionary(); dataPersistence = new AdhocPersistence( diff --git a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/AdoMetastoreImplTest.cs b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/AdoMetastoreImplTest.cs index 1b7e8c200..cd7432db2 100644 --- a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/AdoMetastoreImplTest.cs +++ b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/AdoMetastoreImplTest.cs @@ -339,5 +339,11 @@ private void TestDbConnectionClosedAfterStore() // Verify that DbConnection is closed at the end of the function call Assert.Equal(ConnectionState.Closed, dbConnection.State); } + + [Fact] + private void TestKeySuffixShouldReturnEmpty() + { + Assert.Equal(string.Empty, adoMetastoreImplSpy.Object.GetKeySuffix()); + } } } diff --git a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/DynamoDbMetastoreImplTest.cs b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/DynamoDbMetastoreImplTest.cs index f1e4e4dfd..e4e7fb077 100644 --- a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/DynamoDbMetastoreImplTest.cs +++ b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/DynamoDbMetastoreImplTest.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Amazon; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DocumentModel; using Amazon.DynamoDBv2.Model; @@ -16,8 +17,9 @@ namespace GoDaddy.Asherah.AppEncryption.Tests.AppEncryption.Persistence public class DynamoDbMetastoreImplTest : IClassFixture, IClassFixture, IDisposable { private const string TestKey = "some_key"; + private const string DynamoDbPort = "8000"; - private readonly AmazonDynamoDBClient amazonDynamoDbClient; + private readonly IAmazonDynamoDB amazonDynamoDbClient; private readonly Dictionary keyRecord = new Dictionary { @@ -38,30 +40,14 @@ public class DynamoDbMetastoreImplTest : IClassFixture public DynamoDbMetastoreImplTest(DynamoDBContainerFixture dynamoDbContainerFixture) { - AmazonDynamoDBConfig clientConfig = new AmazonDynamoDBConfig - { - ServiceURL = dynamoDbContainerFixture.ServiceUrl, - }; - amazonDynamoDbClient = new AmazonDynamoDBClient(clientConfig); + dynamoDbMetastoreImpl = NewBuilder() + .WithEndPointConfiguration(dynamoDbContainerFixture.ServiceUrl, "us-west-2") + .Build(); + amazonDynamoDbClient = dynamoDbMetastoreImpl.GetClient(); - CreateTableRequest request = new CreateTableRequest - { - TableName = TableName, - AttributeDefinitions = new List - { - new AttributeDefinition(PartitionKey, ScalarAttributeType.S), - new AttributeDefinition(SortKey, ScalarAttributeType.N), - }, - KeySchema = new List - { - new KeySchemaElement(PartitionKey, KeyType.HASH), - new KeySchemaElement(SortKey, KeyType.RANGE), - }, - ProvisionedThroughput = new ProvisionedThroughput(1L, 1L), - }; + CreateTableSchema(amazonDynamoDbClient, dynamoDbMetastoreImpl.GetTableName()); - CreateTableResponse createTableResponse = amazonDynamoDbClient.CreateTableAsync(request).Result; - table = Table.LoadTable(amazonDynamoDbClient, TableName); + table = Table.LoadTable(amazonDynamoDbClient, dynamoDbMetastoreImpl.GetTableName()); JObject jObject = JObject.FromObject(keyRecord); Document document = new Document @@ -71,16 +57,16 @@ public DynamoDbMetastoreImplTest(DynamoDBContainerFixture dynamoDbContainerFixtu [AttributeKeyRecord] = Document.FromJson(jObject.ToString()), }; - Document result = table.PutItemAsync(document).Result; - - dynamoDbMetastoreImpl = new DynamoDbMetastoreImpl(amazonDynamoDbClient); + table.PutItemAsync(document).Wait(); } public void Dispose() { try { - DeleteTableResponse deleteTableResponse = amazonDynamoDbClient.DeleteTableAsync(TableName).Result; + DeleteTableResponse deleteTableResponse = amazonDynamoDbClient + .DeleteTableAsync(dynamoDbMetastoreImpl.GetTableName()) + .Result; } catch (AggregateException) { @@ -88,6 +74,27 @@ public void Dispose() } } + private void CreateTableSchema(IAmazonDynamoDB client, string tableName) + { + CreateTableRequest request = new CreateTableRequest + { + TableName = tableName, + AttributeDefinitions = new List + { + new AttributeDefinition(PartitionKey, ScalarAttributeType.S), + new AttributeDefinition(SortKey, ScalarAttributeType.N), + }, + KeySchema = new List + { + new KeySchemaElement(PartitionKey, KeyType.HASH), + new KeySchemaElement(SortKey, KeyType.RANGE), + }, + ProvisionedThroughput = new ProvisionedThroughput(1L, 1L), + }; + + CreateTableResponse createTableResponse = client.CreateTableAsync(request).Result; + } + [Fact] private void TestLoadSuccess() { @@ -141,7 +148,7 @@ private void TestLoadLatestWithMultipleRecords() { "mytime", createdPlusOneHour }, }.ToString()), }; - Document resultPlusOneHour = table.PutItemAsync(documentPlusOneHour).Result; + table.PutItemAsync(documentPlusOneHour).Wait(); Document documentPlusOneDay = new Document { @@ -152,7 +159,7 @@ private void TestLoadLatestWithMultipleRecords() { "mytime", createdPlusOneDay }, }.ToString()), }; - Document resultPlusOneDay = table.PutItemAsync(documentPlusOneDay).Result; + table.PutItemAsync(documentPlusOneDay).Wait(); Document documentMinusOneHour = new Document { @@ -163,7 +170,7 @@ private void TestLoadLatestWithMultipleRecords() { "mytime", createdMinusOneHour }, }.ToString()), }; - Document resultMinusOneHour = table.PutItemAsync(documentMinusOneHour).Result; + table.PutItemAsync(documentMinusOneHour).Wait(); Document documentMinusOneDay = new Document { @@ -174,7 +181,7 @@ private void TestLoadLatestWithMultipleRecords() { "mytime", createdMinusOneDay }, }.ToString()), }; - Document resultMinusOneDay = table.PutItemAsync(documentMinusOneDay).Result; + table.PutItemAsync(documentMinusOneDay).Wait(); Option actualJsonObject = dynamoDbMetastoreImpl.LoadLatest(TestKey); @@ -226,16 +233,82 @@ private void TestStoreWithDuplicateShouldReturnFalse() Assert.False(secondAttempt); } - // This test is commented out since the constructor initializes the Table, which results in a network call. We decided - // it wasn't worth the effort of refactoring it with thread-safe lazy loading for slightly higher code coverage. -// [Fact] -// private void TestPrimaryBuilderPath() -// { -// AWSConfigs.AWSRegion = "us-west-2"; -// Builder dynamoDbMetastoreServicePrimaryBuilder = NewBuilder(); -// DynamoDbMetastoreImpl dynamoDbMetastoreImpl = -// dynamoDbMetastoreServicePrimaryBuilder.Build(); -// Assert.NotNull(dynamoDbMetastoreImpl); -// } + [Fact] + private void TestPrimaryBuilderPath() + { + // Hack to inject default region since we don't explicitly require one be specified as we do in KMS impl + AWSConfigs.AWSRegion = "us-west-2"; + DynamoDbMetastoreImpl dbMetastoreImpl = NewBuilder() + .Build(); + + Assert.NotNull(dbMetastoreImpl); + } + + [Fact] + private void TestBuilderPathWithEndPointConfiguration() + { + DynamoDbMetastoreImpl dbMetastoreImpl = NewBuilder() + .WithEndPointConfiguration("http://localhost:" + DynamoDbPort, "us-west-2") + .Build(); + + Assert.NotNull(dbMetastoreImpl); + } + + [Fact] + private void TestBuilderPathWithRegion() + { + DynamoDbMetastoreImpl dbMetastoreImpl = NewBuilder() + .WithRegion("us-west-2") + .Build(); + + Assert.NotNull(dbMetastoreImpl); + } + + [Fact] + private void TestBuilderPathWithKeySuffix() + { + DynamoDbMetastoreImpl dbMetastoreImpl = NewBuilder() + .WithKeySuffix("us-west-2") + .Build(); + + Assert.NotNull(dbMetastoreImpl); + } + + [Fact] + private void TestBuilderPathWithTableName() + { + const string tempTableName = "DummyTable"; + + // Use AWS SDK to create client + AmazonDynamoDBConfig amazonDynamoDbConfig = new AmazonDynamoDBConfig + { + ServiceURL = "http://localhost:8000", + AuthenticationRegion = "us-west-2", + }; + AmazonDynamoDBClient tempDynamoDbClient = new AmazonDynamoDBClient(amazonDynamoDbConfig); + CreateTableSchema(tempDynamoDbClient, tempTableName); + + // Put the object in temp table + Table tempTable = Table.LoadTable(tempDynamoDbClient, tempTableName); + JObject jObject = JObject.FromObject(keyRecord); + Document document = new Document + { + [PartitionKey] = TestKey, + [SortKey] = created.ToUnixTimeSeconds(), + [AttributeKeyRecord] = Document.FromJson(jObject.ToString()), + }; + tempTable.PutItemAsync(document).Wait(); + + // Create a metastore object using the withTableName step + DynamoDbMetastoreImpl dbMetastoreImpl = NewBuilder() + .WithEndPointConfiguration("http://localhost:" + DynamoDbPort, "us-west-2") + .WithTableName(tempTableName) + .Build(); + Option actualJsonObject = dbMetastoreImpl.Load(TestKey, created); + + // Verify that we were able to load and successfully decrypt the item from the metastore object created withTableName + Assert.True(actualJsonObject.IsSome); + Assert.True(JToken.DeepEquals(JObject.FromObject(keyRecord), (JObject)actualJsonObject)); + } } } diff --git a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/InMemoryMetastoreImplTest.cs b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/InMemoryMetastoreImplTest.cs index 1e61bc30d..9ee264e45 100644 --- a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/InMemoryMetastoreImplTest.cs +++ b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/Persistence/InMemoryMetastoreImplTest.cs @@ -91,5 +91,11 @@ private void TestStoreWithDuplicateKeyShouldReturnFalse() Assert.True(inMemoryMetastoreImpl.Store(keyId, created, value)); Assert.False(inMemoryMetastoreImpl.Store(keyId, created, value)); } + + [Fact] + private void TestKeySuffixShouldReturnEmpty() + { + Assert.Equal(string.Empty, inMemoryMetastoreImpl.GetKeySuffix()); + } } } diff --git a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/SessionFactoryTest.cs b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/SessionFactoryTest.cs index 04ccc4b84..b72e064c6 100644 --- a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/SessionFactoryTest.cs +++ b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/SessionFactoryTest.cs @@ -23,6 +23,7 @@ public class SessionFactoryTest private const string TestPartitionId = "test_partition_id"; private const string TestServiceId = "test_service_id"; private const string TestProductId = "test_product_id"; + private const string TestRegionSuffix = "test_region_suffix"; private const string TestStaticMasterKey = "thisIsAStaticMasterKeyForTesting"; private readonly Mock> metastoreMock; @@ -777,6 +778,18 @@ private void TestGetPartitionWithPartitionId() Assert.Equal(TestProductId, partition.ProductId); } + [Fact] + private void TestGetPartitionWithSuffixedPartition() + { + metastoreMock.Setup(x => x.GetKeySuffix()).Returns(TestRegionSuffix); + Partition partition = + sessionFactory.GetPartition(TestPartitionId); + + Assert.Equal(TestPartitionId, partition.PartitionId); + Assert.Equal(TestServiceId, partition.ServiceId); + Assert.Equal(TestProductId, partition.ProductId); + } + [Fact] private void TestDisposeSuccess() { diff --git a/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/SuffixedPartitionTest.cs b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/SuffixedPartitionTest.cs new file mode 100644 index 000000000..676feeaeb --- /dev/null +++ b/csharp/AppEncryption/AppEncryption.Tests/AppEncryption/SuffixedPartitionTest.cs @@ -0,0 +1,69 @@ +using Xunit; + +namespace GoDaddy.Asherah.AppEncryption.Tests.AppEncryption +{ + [Collection("Logger Fixture collection")] + public class SuffixedPartitionTest + { + private const string TestPartitionId = "test_partition_id"; + private const string TestServiceId = "test_service_id"; + private const string TestProductId = "test_product_id"; + private const string TestSuffixRegion = "test_suffix_region"; + + private readonly Partition partition; + + public SuffixedPartitionTest() + { + partition = new SuffixedPartition(TestPartitionId, TestServiceId, TestProductId, TestSuffixRegion); + } + + [Fact] + private void TestSuffixedPartitionCreation() + { + Assert.NotNull(partition); + } + + [Fact] + private void TestGetPartitionId() + { + string actualTestPartitionId = partition.PartitionId; + Assert.Equal(TestPartitionId, actualTestPartitionId); + } + + [Fact] + private void TestGetServiceId() + { + string actualServiceId = partition.ServiceId; + Assert.Equal(TestServiceId, actualServiceId); + } + + [Fact] + private void TestGetProductId() + { + string actualProductId = partition.ProductId; + Assert.Equal(TestProductId, actualProductId); + } + + [Fact] + private void TestGetSystemKeyId() + { + const string systemKeyIdString = "_SK_" + TestServiceId + "_" + TestProductId + "_" + TestSuffixRegion; + Assert.Equal(systemKeyIdString, partition.SystemKeyId); + } + + [Fact] + private void TestGetIntermediateKeyId() + { + const string intermediateKeyIdString = "_IK_" + TestPartitionId + "_" + TestServiceId + "_" + TestProductId + "_" + TestSuffixRegion; + Assert.Equal(intermediateKeyIdString, partition.IntermediateKeyId); + } + + [Fact] + private void TestToString() + { + string expectedToStringString = partition.GetType().Name + "[partitionId=" + TestPartitionId + + ", serviceId=" + TestServiceId + ", productId=" + TestProductId + ", regionSuffix=" + TestSuffixRegion + "]"; + Assert.Equal(expectedToStringString, partition.ToString()); + } + } +} diff --git a/csharp/AppEncryption/AppEncryption/DefaultPartition.cs b/csharp/AppEncryption/AppEncryption/DefaultPartition.cs new file mode 100644 index 000000000..85d717cfd --- /dev/null +++ b/csharp/AppEncryption/AppEncryption/DefaultPartition.cs @@ -0,0 +1,16 @@ +namespace GoDaddy.Asherah.AppEncryption +{ + public class DefaultPartition : Partition + { + public DefaultPartition(string partitionId, string serviceId, string productId) + : base(partitionId, serviceId, productId) + { + } + + public override string ToString() + { + return GetType().Name + "[partitionId=" + PartitionId + + ", serviceId=" + ServiceId + ", productId=" + ProductId + "]"; + } + } +} diff --git a/csharp/AppEncryption/AppEncryption/Envelope/EnvelopeEncryptionJsonImpl.cs b/csharp/AppEncryption/AppEncryption/Envelope/EnvelopeEncryptionJsonImpl.cs index f5d5acee5..f9ccabbe6 100644 --- a/csharp/AppEncryption/AppEncryption/Envelope/EnvelopeEncryptionJsonImpl.cs +++ b/csharp/AppEncryption/AppEncryption/Envelope/EnvelopeEncryptionJsonImpl.cs @@ -133,7 +133,7 @@ internal virtual T WithIntermediateKeyForRead( if (intermediateKey == null) { - intermediateKey = GetIntermediateKey(intermediateKeyMeta.Created); + intermediateKey = GetIntermediateKey(intermediateKeyMeta); // Put the key into our cache if allowed if (cryptoPolicy.CanCacheIntermediateKeys()) @@ -480,11 +480,11 @@ internal virtual CryptoKey GetLatestOrCreateSystemKey() /// /// The decrypted intermediate key. /// - /// creation time of intermediate key + /// intermediate key meta of intermediate key /// if the intermediate key is not found, or it has missing system key info - internal virtual CryptoKey GetIntermediateKey(DateTimeOffset intermediateKeyCreated) + internal virtual CryptoKey GetIntermediateKey(KeyMeta intermediateKeyMeta) { - EnvelopeKeyRecord intermediateKeyRecord = LoadKeyRecord(partition.IntermediateKeyId, intermediateKeyCreated); + EnvelopeKeyRecord intermediateKeyRecord = LoadKeyRecord(intermediateKeyMeta.KeyId, intermediateKeyMeta.Created); return WithExistingSystemKey( intermediateKeyRecord.ParentKeyMeta.IfNone(() => diff --git a/csharp/AppEncryption/AppEncryption/Partition.cs b/csharp/AppEncryption/AppEncryption/Partition.cs index f9ae5883d..058ef3842 100644 --- a/csharp/AppEncryption/AppEncryption/Partition.cs +++ b/csharp/AppEncryption/AppEncryption/Partition.cs @@ -1,28 +1,22 @@ namespace GoDaddy.Asherah.AppEncryption { - public class Partition + public abstract class Partition { - public Partition(string partitionId, string serviceId, string productId) + protected Partition(string partitionId, string serviceId, string productId) { PartitionId = partitionId; ServiceId = serviceId; ProductId = productId; } - public string SystemKeyId => "_SK_" + ServiceId + "_" + ProductId; + public virtual string SystemKeyId => "_SK_" + ServiceId + "_" + ProductId; - public string IntermediateKeyId => "_IK_" + PartitionId + "_" + ServiceId + "_" + ProductId; + public virtual string IntermediateKeyId => "_IK_" + PartitionId + "_" + ServiceId + "_" + ProductId; internal string PartitionId { get; } internal string ServiceId { get; } internal string ProductId { get; } - - public override string ToString() - { - return GetType().Name + "[partitionId=" + PartitionId + - ", serviceId=" + ServiceId + ", productId=" + ProductId + "]"; - } } } diff --git a/csharp/AppEncryption/AppEncryption/Persistence/AdoMetastoreImpl.cs b/csharp/AppEncryption/AppEncryption/Persistence/AdoMetastoreImpl.cs index 8987dd497..219ca2112 100644 --- a/csharp/AppEncryption/AppEncryption/Persistence/AdoMetastoreImpl.cs +++ b/csharp/AppEncryption/AppEncryption/Persistence/AdoMetastoreImpl.cs @@ -3,7 +3,6 @@ using System.Runtime.CompilerServices; using App.Metrics.Timer; using GoDaddy.Asherah.AppEncryption.Util; -using GoDaddy.Asherah.Crypto.Exceptions; using GoDaddy.Asherah.Logging; using LanguageExt; using Microsoft.Extensions.Logging; @@ -134,6 +133,11 @@ public bool Store(string keyId, DateTimeOffset created, JObject value) } } + public string GetKeySuffix() + { + return string.Empty; + } + internal virtual void AddParameter(DbCommand command, string name, object value) { DbParameter parameter = command.CreateParameter(); diff --git a/csharp/AppEncryption/AppEncryption/Persistence/DynamoDbMetastoreImpl.cs b/csharp/AppEncryption/AppEncryption/Persistence/DynamoDbMetastoreImpl.cs index 74bf2691b..1db29e465 100644 --- a/csharp/AppEncryption/AppEncryption/Persistence/DynamoDbMetastoreImpl.cs +++ b/csharp/AppEncryption/AppEncryption/Persistence/DynamoDbMetastoreImpl.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Amazon; using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.DocumentModel; using Amazon.DynamoDBv2.Model; @@ -20,7 +21,6 @@ namespace GoDaddy.Asherah.AppEncryption.Persistence { public class DynamoDbMetastoreImpl : IMetastore { - internal const string TableName = "EncryptionKey"; internal const string PartitionKey = "Id"; internal const string SortKey = "Created"; internal const string AttributeKeyRecord = "KeyRecord"; @@ -31,13 +31,61 @@ public class DynamoDbMetastoreImpl : IMetastore private static readonly ILogger Logger = LogManager.CreateLogger(); - // Note this instance is thread-safe + private readonly IAmazonDynamoDB dbClient; + private readonly string keySuffix; + private readonly string tableName; private readonly Table table; - internal DynamoDbMetastoreImpl(IAmazonDynamoDB dbClient) + private DynamoDbMetastoreImpl(Builder builder) { - // Note this results in a network call. For now, cleaner than refactoring w/ thread-safe lazy loading - table = Table.LoadTable(dbClient, TableName); + dbClient = builder.DbClient; + keySuffix = builder.KeySuffix; + tableName = builder.TableName; + Table.TryLoadTable(dbClient, tableName, out table); + } + + public interface IBuildStep + { + /// + /// Specifies whether key suffix should be enabled for DynamoDB. + /// + /// the region to be used as suffix. + /// The current IBuildStep instance. + IBuildStep WithKeySuffix(string suffix); + + /// + /// Specifies the name of the table. + /// + /// The name of the table. + /// The current IBuildStep instance. + IBuildStep WithTableName(string tableName); + + /// + /// Builds the finalized DynamoDbMetastoreImpl with the parameters specified in the builder. + /// + /// The fully instantiated DynamoDbMetastoreImpl. + DynamoDbMetastoreImpl Build(); + } + + private interface IEndPointStep + { + /// + /// Adds EndPoint config to the AWS DynamoDb client. + /// + /// the service endpoint either with or without the protocol. + /// the region to use for SigV4 signing of requests (e.g. us-west-1). + /// The current IBuildStep instance. + IBuildStep WithEndPointConfiguration(string endPoint, string signingRegion); + } + + private interface IRegionStep + { + /// + /// Specifies the region for the AWS DynamoDb client. + /// + /// The region for the DynamoDb client. + /// The current IBuildStep instance. + IBuildStep WithRegion(string region); } public static Builder NewBuilder() @@ -136,8 +184,7 @@ public bool Store(string keyId, DateTimeOffset created, JObject value) ConditionalExpression = expr, }; - // This a blocking call using Result because we need to wait for the call to complete before proceeding - Document result = table.PutItemAsync(document, config).Result; + table.PutItemAsync(document, config).Wait(); return true; } catch (AggregateException ae) @@ -157,12 +204,62 @@ public bool Store(string keyId, DateTimeOffset created, JObject value) } } - public class Builder + public IAmazonDynamoDB GetClient() + { + return dbClient; + } + + public string GetKeySuffix() + { + return keySuffix; + } + + public string GetTableName() { + return tableName; + } + + public class Builder : IBuildStep, IEndPointStep, IRegionStep + { + #pragma warning disable SA1401 + internal IAmazonDynamoDB DbClient; + internal string KeySuffix = DefaultKeySuffix; + internal string TableName = DefaultTableName; + #pragma warning restore SA1401 + + private const string DefaultTableName = "EncryptionKey"; + private static readonly string DefaultKeySuffix = string.Empty; + private readonly AmazonDynamoDBConfig dbConfig = new AmazonDynamoDBConfig(); + + public IBuildStep WithKeySuffix(string suffix) + { + KeySuffix = suffix; + return this; + } + + public IBuildStep WithTableName(string tableName) + { + TableName = tableName; + return this; + } + + public IBuildStep WithEndPointConfiguration(string endPoint, string signingRegion) + { + dbConfig.ServiceURL = endPoint; + dbConfig.AuthenticationRegion = signingRegion; + return this; + } + + public IBuildStep WithRegion(string region) + { + dbConfig.RegionEndpoint = RegionEndpoint.GetBySystemName(region); + return this; + } + public DynamoDbMetastoreImpl Build() { - IAmazonDynamoDB dbClient = new AmazonDynamoDBClient(); - return new DynamoDbMetastoreImpl(dbClient); + DbClient = new AmazonDynamoDBClient(dbConfig); + return new DynamoDbMetastoreImpl(this); } } } diff --git a/csharp/AppEncryption/AppEncryption/Persistence/IMetastore.cs b/csharp/AppEncryption/AppEncryption/Persistence/IMetastore.cs index 23b1ca951..88915fbe5 100644 --- a/csharp/AppEncryption/AppEncryption/Persistence/IMetastore.cs +++ b/csharp/AppEncryption/AppEncryption/Persistence/IMetastore.cs @@ -41,5 +41,14 @@ public interface IMetastore /// the created time to store /// the value to store bool Store(string keyId, DateTimeOffset created, T value); + + /// + /// Returns the key suffix or "" if key suffix option is disabled. + /// + /// + /// + /// The key suffix. + /// + string GetKeySuffix(); } } diff --git a/csharp/AppEncryption/AppEncryption/Persistence/InMemoryMetastoreImpl.cs b/csharp/AppEncryption/AppEncryption/Persistence/InMemoryMetastoreImpl.cs index a90c316c5..b19a6cde2 100644 --- a/csharp/AppEncryption/AppEncryption/Persistence/InMemoryMetastoreImpl.cs +++ b/csharp/AppEncryption/AppEncryption/Persistence/InMemoryMetastoreImpl.cs @@ -72,5 +72,10 @@ public virtual bool Store(string keyId, DateTimeOffset created, T value) return true; } } + + public string GetKeySuffix() + { + return string.Empty; + } } } diff --git a/csharp/AppEncryption/AppEncryption/SessionFactory.cs b/csharp/AppEncryption/AppEncryption/SessionFactory.cs index 7afa7f0ce..f6ca72943 100644 --- a/csharp/AppEncryption/AppEncryption/SessionFactory.cs +++ b/csharp/AppEncryption/AppEncryption/SessionFactory.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Threading; using App.Metrics; using App.Metrics.Concurrency; using GoDaddy.Asherah.AppEncryption.Envelope; @@ -157,7 +156,14 @@ internal IEnvelopeEncryption GetEnvelopeEncryptionBytes(string partition internal Partition GetPartition(string partitionId) { - return new Partition(partitionId, serviceId, productId); + string regionSuffix = metastore.GetKeySuffix(); + + if (!string.IsNullOrEmpty(regionSuffix)) + { + return new SuffixedPartition(partitionId, serviceId, productId, regionSuffix); + } + + return new DefaultPartition(partitionId, serviceId, productId); } /// diff --git a/csharp/AppEncryption/AppEncryption/SuffixedPartition.cs b/csharp/AppEncryption/AppEncryption/SuffixedPartition.cs new file mode 100644 index 000000000..c53524b47 --- /dev/null +++ b/csharp/AppEncryption/AppEncryption/SuffixedPartition.cs @@ -0,0 +1,23 @@ +namespace GoDaddy.Asherah.AppEncryption +{ + public class SuffixedPartition : Partition + { + private readonly string regionSuffix; + + public SuffixedPartition(string partitionId, string serviceId, string productId, string regionSuffix) + : base(partitionId, serviceId, productId) + { + this.regionSuffix = regionSuffix; + } + + public override string SystemKeyId => base.SystemKeyId + "_" + regionSuffix; + + public override string IntermediateKeyId => base.IntermediateKeyId + "_" + regionSuffix; + + public override string ToString() + { + return GetType().Name + "[partitionId=" + PartitionId + + ", serviceId=" + ServiceId + ", productId=" + ProductId + ", regionSuffix=" + regionSuffix + "]"; + } + } +} diff --git a/csharp/AppEncryption/Directory.Build.props b/csharp/AppEncryption/Directory.Build.props index 2d03965b1..f09ab663a 100644 --- a/csharp/AppEncryption/Directory.Build.props +++ b/csharp/AppEncryption/Directory.Build.props @@ -1,5 +1,5 @@ - 0.1.2 + 0.1.3