Skip to content

Commit 3b91075

Browse files
szymon-miezaldriftx
authored andcommitted
HCD-147: Add createsystemkey subcommand to nodetool (#1759)
This commit extends nodetool with createsystemkey command. The motivation is to allow users to create encryption keys that can be used for flat-file based encryption in TDE. CNDB PR: riptano/cndb#14205.
1 parent c594f9c commit 3b91075

File tree

6 files changed

+210
-19
lines changed

6 files changed

+210
-19
lines changed

src/java/org/apache/cassandra/crypto/LocalFileSystemKeyProvider.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ public class LocalFileSystemKeyProvider implements IKeyProvider
4747

4848
public LocalFileSystemKeyProvider(Path keyPath) throws IOException
4949
{
50+
if (keyPath.getParent() == null)
51+
{
52+
throw new IllegalArgumentException("The key path must be absolute");
53+
}
54+
5055
if (Files.exists(keyPath))
5156
{
5257
this.keyPath = keyPath;

src/java/org/apache/cassandra/tools/NodeTool.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ public int execute(String... args)
104104
Compact.class,
105105
CompactionHistory.class,
106106
CompactionStats.class,
107+
CreateSystemKey.class,
107108
DataPaths.class,
108109
Decommission.class,
109110
DescribeCluster.class,
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright DataStax, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.apache.cassandra.tools.nodetool;
17+
18+
import java.io.IOException;
19+
import java.io.PrintStream;
20+
import java.nio.file.Path;
21+
import java.security.InvalidParameterException;
22+
import java.security.NoSuchAlgorithmException;
23+
import java.util.ArrayList;
24+
import java.util.List;
25+
import javax.crypto.NoSuchPaddingException;
26+
27+
import io.airlift.airline.Arguments;
28+
import io.airlift.airline.Command;
29+
import io.airlift.airline.Option;
30+
import org.apache.cassandra.crypto.LocalSystemKey;
31+
import org.apache.cassandra.tools.NodeProbe;
32+
import org.apache.cassandra.tools.NodeTool.NodeToolCmd;
33+
34+
@Command(name = "createsystemkey", description = "Creates a system key for sstable encryption")
35+
public class CreateSystemKey extends NodeToolCmd
36+
{
37+
@Arguments(usage = "[<algorithm> <key strength> [<filename>]", description = "<algorithm[/mode/padding]>\n" +
38+
"<key strength>\n" +
39+
"[<file>]\n" +
40+
"Key strength not required for Hmac algorithms. <file> will be appended to the directory defined in system_key_directory.")
41+
private List<String> args = new ArrayList<>();
42+
43+
@Option(title = "directory", name = "-d", description = "Output directory")
44+
private String directoryOption = null;
45+
46+
@Override
47+
public void execute(NodeProbe probe)
48+
{
49+
if (args.size() < 2)
50+
{
51+
throw new RuntimeException("Usage: nodetool createsystemkey <algorithm> <key strength> [<file>]");
52+
}
53+
54+
String cipherName = args.get(0);
55+
int keyStrength = cipherName.startsWith("Hmac") ? 0 : Integer.parseInt(args.get(1));
56+
57+
Path directory = directoryOption != null ? Path.of(directoryOption) : null;
58+
String keyLocation = null;
59+
PrintStream out = probe.output().out;
60+
PrintStream err = probe.output().err;
61+
62+
try
63+
{
64+
keyLocation = args.size() > 2 ? args.get(2) : "system_key";
65+
Path keyPath = LocalSystemKey.createKey(directory, keyLocation, cipherName, keyStrength);
66+
67+
out.printf("Successfully created key %s%n", keyPath.toString());
68+
}
69+
catch (NoSuchAlgorithmException e)
70+
{
71+
err.printf("System key (%s %s) was not created at %s%n", cipherName, keyStrength, keyLocation);
72+
err.println(e.getMessage());
73+
err.println("Available algorithms are: AES, ARCFOUR, Blowfish, DES, DESede, HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512 and RC2");
74+
System.exit(1);
75+
}
76+
catch (InvalidParameterException | NoSuchPaddingException | IOException e)
77+
{
78+
err.printf("System key (%s %s) was not created at %s%n", cipherName, keyStrength, keyLocation);
79+
err.println(e.getMessage());
80+
System.exit(1);
81+
}
82+
}
83+
}

test/distributed/org/apache/cassandra/distributed/test/NodeToolTest.java

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,26 @@
1919
package org.apache.cassandra.distributed.test;
2020

2121
import java.io.IOException;
22+
import java.nio.file.Files;
23+
import java.nio.file.Path;
24+
import java.nio.file.Paths;
2225
import java.util.function.Consumer;
2326

2427
import org.junit.AfterClass;
2528
import org.junit.BeforeClass;
2629
import org.junit.Test;
2730

31+
import org.apache.cassandra.config.CassandraRelevantProperties;
2832
import org.apache.cassandra.config.DatabaseDescriptor;
33+
import org.apache.cassandra.crypto.TDEConfigurationProvider;
2934
import org.apache.cassandra.distributed.Cluster;
3035
import org.apache.cassandra.distributed.api.ICluster;
3136
import org.apache.cassandra.distributed.api.IInvokableInstance;
3237
import org.apache.cassandra.distributed.api.NodeToolResult;
3338

3439
import static org.junit.Assert.assertEquals;
40+
import static org.junit.Assert.assertFalse;
41+
import static org.junit.Assert.assertTrue;
3542

3643
public class NodeToolTest extends TestBaseImpl
3744
{
@@ -161,4 +168,85 @@ public void testCompactionStats() throws Throwable
161168
result.asserts().success().stdoutNotContains("system.peers");
162169
result.asserts().success().stdoutNotContains("system_schema.keyspaces");
163170
}
171+
172+
@Test
173+
public void testSuccesfulSystemKeyCreation() throws Throwable
174+
{
175+
try
176+
{
177+
// given a command with default key name and location
178+
Path testDir1 = Files.createTempDirectory("test_1");
179+
CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.setString(testDir1.toString());
180+
Path systemKey = Paths.get(TDEConfigurationProvider.getConfiguration().systemKeyDirectory).resolve("system_key");
181+
assertFalse(Files.exists(systemKey));
182+
// when
183+
NodeToolResult result = NODE.nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding", "128");
184+
// then should create a key
185+
result.asserts().success();
186+
result.asserts().stdoutContains("Successfully created key");
187+
assertTrue(Files.exists(systemKey));
188+
189+
// given a command with specified key name and default key location
190+
Path testDir2 = Files.createTempDirectory("test_2");
191+
CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.setString(testDir2.toString());
192+
systemKey = Paths.get(TDEConfigurationProvider.getConfiguration().systemKeyDirectory).resolve("system_key_2");
193+
assertFalse(Files.exists(systemKey));
194+
// when
195+
result = NODE.nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding", "128", "system_key_2");
196+
// then should create a key
197+
result.asserts().success();
198+
result.asserts().stdoutContains("Successfully created key");
199+
assertTrue(Files.exists(systemKey));
200+
201+
// given a command with specified key location and default key name
202+
Path testDir3 = Files.createTempDirectory("test_3");
203+
assertFalse(Files.exists(testDir3.resolve("system_key")));
204+
// when
205+
result = NODE.nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding", "128", "-d", testDir3.toString());
206+
// then should create a key
207+
result.asserts().success();
208+
result.asserts().stdoutContains("Successfully created key");
209+
assertTrue(Files.exists(testDir3.resolve("system_key")));
210+
}
211+
finally
212+
{
213+
CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.reset();
214+
}
215+
}
216+
217+
@Test
218+
public void testUnsuccesfulSystemKeyCreation()
219+
{
220+
try
221+
{
222+
// given a command without key type and strength
223+
NodeToolResult result = NODE.nodetoolResult("createsystemkey");
224+
// then should fail creation
225+
result.asserts().failure();
226+
result.asserts().stderrContains("Usage: nodetool createsystemkey <algorithm> <key strength> [<file>]");
227+
228+
// given a command without key strength
229+
result = NODE.nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding");
230+
// then should fail creation
231+
result.asserts().failure();
232+
result.asserts().stderrContains("Usage: nodetool createsystemkey <algorithm> <key strength> [<file>]");
233+
234+
// given a command with incorrect algorithm name
235+
result = NODE.nodetoolResult("createsystemkey", "INVALIDNAME", "128");
236+
// then should fail creation
237+
result.asserts().failure();
238+
result.asserts().stderrContains("System key (INVALIDNAME 128) was not created");
239+
result.asserts().stderrContains("Available algorithms are: AES, ARCFOUR, Blowfish, DES, DESede, HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512 and RC2");
240+
241+
// given a command with incorrect algorithm strength
242+
result = NODE.nodetoolResult("createsystemkey", "AES", "99");
243+
// then should fail creation
244+
result.asserts().failure();
245+
result.asserts().stderrContains("System key (AES 99) was not created");
246+
}
247+
finally
248+
{
249+
CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.reset();
250+
}
251+
}
164252
}

test/distributed/org/apache/cassandra/distributed/test/SSTableEncryptionTest.java

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
/*
2-
* Licensed to the Apache Software Foundation (ASF) under one
3-
* or more contributor license agreements. See the NOTICE file
4-
* distributed with this work for additional information
5-
* regarding copyright ownership. The ASF licenses this file
6-
* to you under the Apache License, Version 2.0 (the
7-
* "License"); you may not use this file except in compliance
8-
* with the License. You may obtain a copy of the License at
2+
* Copyright DataStax, Inc.
93
*
10-
* http://www.apache.org/licenses/LICENSE-2.0
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
119
*
1210
* Unless required by applicable law or agreed to in writing, software
1311
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -23,6 +21,7 @@
2321
import java.nio.charset.StandardCharsets;
2422
import java.nio.file.Files;
2523
import java.nio.file.Path;
24+
import java.nio.file.Paths;
2625
import java.security.NoSuchAlgorithmException;
2726
import java.util.List;
2827
import java.util.stream.Collectors;
@@ -34,11 +33,13 @@
3433
import org.junit.BeforeClass;
3534
import org.junit.Test;
3635

36+
import org.apache.cassandra.config.CassandraRelevantProperties;
3737
import org.apache.cassandra.crypto.LocalSystemKey;
3838
import org.apache.cassandra.crypto.TDEConfigurationProvider;
3939
import org.apache.cassandra.db.Keyspace;
4040
import org.apache.cassandra.distributed.Cluster;
4141
import org.apache.cassandra.distributed.api.ConsistencyLevel;
42+
import org.apache.cassandra.distributed.api.NodeToolResult;
4243
import org.apache.cassandra.io.sstable.Component;
4344
import org.apache.cassandra.io.sstable.format.SSTableFormat;
4445
import org.apache.cassandra.io.sstable.format.bti.BtiFormat;
@@ -60,22 +61,20 @@ public class SSTableEncryptionTest extends TestBaseImpl
6061
private static final String KEYSPACE_PREFIX = "ks";
6162
private static final String TABLE_PREFIX = "tbl";
6263
private static final String SENSITIVE_KEY = "Key with sensitive information";
63-
private static final int ROWS_COUNT = 10000;
64+
private static final int ROWS_COUNT = 20000;
6465

65-
private static String defaultSystemKeyDirectory;
6666

6767
@BeforeClass
6868
public static void beforeAll() throws IOException
6969
{
70-
defaultSystemKeyDirectory = TDEConfigurationProvider.getConfiguration().systemKeyDirectory;
7170
Path systemKeyDirectory = Files.createTempDirectory("system_key_directory");
72-
TDEConfigurationProvider.setSystemKeyDirectoryProperty(systemKeyDirectory.toString());
71+
CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.setString(systemKeyDirectory.toString());
7372
}
7473

7574
@AfterClass
7675
public static void tearDown()
7776
{
78-
TDEConfigurationProvider.setSystemKeyDirectoryProperty(defaultSystemKeyDirectory);
77+
CassandraRelevantProperties.SYSTEM_KEY_DIRECTORY.reset();
7978
}
8079

8180
@Test
@@ -87,7 +86,7 @@ public void shouldCreateQueryableEncryptedSSTables() throws Throwable
8786
{
8887
// given a table with data encrypted using local key
8988
String keyspace = createKeyspace(cluster);
90-
Path secretKey = createLocalSecretKey();
89+
Path secretKey = createLocalSecretKey(cluster);
9190
String table = createEncryptedTable(cluster, keyspace, secretKey);
9291
int numberOfRows = 10;
9392

@@ -141,7 +140,7 @@ public void shouldEncryptSensitiveData() throws Exception
141140
// given tables with and without encryption
142141
String keyspace = createKeyspace(cluster);
143142
TestTable nonEncryptedTable = createTableWithSampleData(cluster, keyspace, "");
144-
Path secretKey = createLocalSecretKey();
143+
Path secretKey = createLocalSecretKey(cluster);
145144
TestTable encryptedTable = createTableWithSampleData(cluster, keyspace, localSystemKeyEncryptionCompressionSuffix("Encryptor", secretKey.toAbsolutePath().toString()));
146145

147146
// then
@@ -156,6 +155,7 @@ public void shouldEncryptSensitiveData() throws Exception
156155

157156
// indexes with encryption should pass the checksum check
158157
assertThat(checkEncryptionCrc(encryptedTable.partitionIndexBytes)).isTrue();
158+
assertThat(encryptedTable.rowIndexBytes.length).isGreaterThan(0);
159159
assertThat(checkEncryptionCrc(encryptedTable.rowIndexBytes)).isTrue();
160160
// indexes without encryption should fail the checksum check
161161
assertThat(checkEncryptionCrc(nonEncryptedTable.partitionIndexBytes)).isFalse();
@@ -193,7 +193,7 @@ public void shouldNotReadRowsFromEncryptedTableWithoutTheSecretKey() throws Exce
193193

194194
// given a table with data encrypted using local key
195195
String keyspace = createKeyspace(cluster);
196-
Path secretKey = createLocalSecretKey();
196+
Path secretKey = createLocalSecretKey(cluster);
197197
String encryptedTableName = createEncryptedTable(cluster, keyspace, secretKey);
198198
String nonEncryptedTableName = createTable(cluster, keyspace);
199199
int numberOfRows = 10;
@@ -230,7 +230,7 @@ public void shouldFailWhenReadingWithDifferentKey() throws Exception
230230

231231
// given a table with data encrypted using local key
232232
String keyspace = createKeyspace(cluster);
233-
Path secretKey = createLocalSecretKey();
233+
Path secretKey = createLocalSecretKey(cluster);
234234
String encryptedTableName = createEncryptedTable(cluster, keyspace, secretKey);
235235
String nonEncryptedTableName = createTable(cluster, keyspace);
236236
int numberOfRows = 10;
@@ -342,10 +342,15 @@ private String createKeyspace(Cluster cluster)
342342
return randomKeyspaceName;
343343
}
344344

345-
private Path createLocalSecretKey() throws IOException, NoSuchAlgorithmException, NoSuchPaddingException
345+
private Path createLocalSecretKey(Cluster cluster)
346346
{
347347
String keyPath = "system_key_" + RandomStringUtils.random(10, true, true);
348-
return createLocalSecretKey(keyPath);
348+
Path keyFullPath = Paths.get(TDEConfigurationProvider.getConfiguration().systemKeyDirectory).resolve(keyPath);
349+
assertThat(Files.exists(keyFullPath)).isFalse();
350+
NodeToolResult result = cluster.get(1).nodetoolResult("createsystemkey", "AES/CBC/PKCS5Padding", "256", keyPath);
351+
result.asserts().success();
352+
assertThat(Files.exists(keyFullPath)).isTrue();
353+
return keyFullPath;
349354
}
350355

351356
private Path createLocalSecretKey(String keyPath) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException

test/unit/org/apache/cassandra/crypto/LocalFileSystemKeyProviderTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.junit.Test;
2828

2929
import org.apache.cassandra.io.util.FileUtils;
30+
import org.assertj.core.api.Assertions;
3031

3132
import static org.junit.Assert.assertEquals;
3233
import static org.junit.Assert.assertFalse;
@@ -234,4 +235,12 @@ public void testKeyGenerationExceptionForInvalidCipherMode() throws IOException
234235
Files.deleteIfExists(secretKeyPath);
235236
}
236237
}
238+
239+
@Test
240+
public void shouldDisallowNonAbsoluteKeyPath()
241+
{
242+
Assertions.assertThatThrownBy(() -> new LocalFileSystemKeyProvider(Path.of("foobar")))
243+
.isInstanceOf(IllegalArgumentException.class)
244+
.hasMessage("The key path must be absolute");
245+
}
237246
}

0 commit comments

Comments
 (0)