embulk-filter-encrypt 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ChangeLog +4 -0
- data/README.md +4 -1
- data/build.gradle +2 -1
- data/src/main/java/org/embulk/filter/encrypt/EncryptFilterPlugin.java +80 -40
- data/src/test/java/org/embulk/filter/encrypt/TestEncryptFilterPlugin.java +535 -0
- metadata +13 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6de855d8ac0f1c315220c4907fe6eb8d7bd40043
|
4
|
+
data.tar.gz: b21751d55746b058abfef300e3927187fb8f6185
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97fbc5a6e919eb2b053fd2b4862eebc4edd1e4b1a2245c8274fe0780fcab557610a2c8aa8feb3c67cd13ea04bba2ed06b69416b4d0ea3ce9e825366c2b6e2b1c
|
7
|
+
data.tar.gz: 80f240d5de8da536ad01c5a998ba273740cc5aa63c5b102dcb83d816666921e3c171204ebaa56ae8e102f434fe0214499872240fcc4f0ee241170855a8a8c738
|
data/ChangeLog
CHANGED
data/README.md
CHANGED
@@ -23,7 +23,8 @@ You can apply encryption to password column and get following outputs:
|
|
23
23
|
- **algorithm**: encryption algorithm (see below) (enum, required)
|
24
24
|
- **column_names**: names of string columns to encrypt (array of string, required)
|
25
25
|
- **key_hex**: encryption key (string, required)
|
26
|
-
- **iv_hex**:
|
26
|
+
- **iv_hex**: encryption initialization vector (string, required if mode of the algorithm is CBC)
|
27
|
+
- **output_encoding**: the encoding of encrypted value, can be either "base64" or "hex" (base16)
|
27
28
|
|
28
29
|
## Algorithms
|
29
30
|
|
@@ -117,9 +118,11 @@ You can use Hive's `aes_decrypt(input binary, key binary)` function (available s
|
|
117
118
|
```yaml
|
118
119
|
filters:
|
119
120
|
- type: encrypt
|
121
|
+
algorithm: AES-256-CBC
|
120
122
|
column_names: [password, ip]
|
121
123
|
key_hex: 098F6BCD4621D373CADE4E832627B4F60A9172716AE6428409885B8B829CCB05
|
122
124
|
iv_hex: C9DD4BB33B827EB1FBA1B16A0074D460
|
125
|
+
output_encoding: hex
|
123
126
|
```
|
124
127
|
|
125
128
|
## Build
|
data/build.gradle
CHANGED
@@ -13,7 +13,7 @@ configurations {
|
|
13
13
|
provided
|
14
14
|
}
|
15
15
|
|
16
|
-
version = "0.
|
16
|
+
version = "0.2.0"
|
17
17
|
|
18
18
|
sourceCompatibility = 1.7
|
19
19
|
targetCompatibility = 1.7
|
@@ -23,6 +23,7 @@ dependencies {
|
|
23
23
|
provided "org.embulk:embulk-core:0.8.6"
|
24
24
|
// compile "YOUR_JAR_DEPENDENCY_GROUP:YOUR_JAR_DEPENDENCY_MODULE:YOUR_JAR_DEPENDENCY_VERSION"
|
25
25
|
testCompile "junit:junit:4.+"
|
26
|
+
testCompile "org.embulk:embulk-core:0.8.18:tests"
|
26
27
|
}
|
27
28
|
|
28
29
|
task classpath(type: Copy, dependsOn: ["jar"]) {
|
@@ -1,47 +1,43 @@
|
|
1
1
|
package org.embulk.filter.encrypt;
|
2
2
|
|
3
|
-
import java.util.List;
|
4
|
-
import java.util.Set;
|
5
|
-
import java.util.HashSet;
|
6
|
-
import java.util.EnumSet;
|
7
|
-
import javax.crypto.Cipher;
|
8
|
-
import javax.crypto.SecretKey;
|
9
|
-
import javax.crypto.SecretKeyFactory;
|
10
|
-
import javax.crypto.spec.SecretKeySpec;
|
11
|
-
import javax.crypto.spec.IvParameterSpec;
|
12
|
-
import javax.crypto.spec.PBEKeySpec;
|
13
|
-
import javax.crypto.NoSuchPaddingException;
|
14
|
-
import javax.crypto.BadPaddingException;
|
15
|
-
import javax.crypto.IllegalBlockSizeException;
|
16
|
-
import java.security.AlgorithmParameters;
|
17
|
-
import java.security.InvalidKeyException;
|
18
|
-
import java.security.NoSuchAlgorithmException;
|
19
|
-
import java.security.InvalidAlgorithmParameterException;
|
20
|
-
import java.security.spec.KeySpec;
|
21
3
|
import com.fasterxml.jackson.annotation.JsonCreator;
|
22
4
|
import com.fasterxml.jackson.annotation.JsonValue;
|
23
5
|
import com.google.common.base.Optional;
|
24
6
|
import com.google.common.io.BaseEncoding;
|
25
7
|
import org.embulk.config.Config;
|
26
8
|
import org.embulk.config.ConfigDefault;
|
27
|
-
import org.embulk.config.
|
9
|
+
import org.embulk.config.ConfigException;
|
28
10
|
import org.embulk.config.ConfigSource;
|
29
11
|
import org.embulk.config.Task;
|
30
12
|
import org.embulk.config.TaskSource;
|
31
|
-
import org.embulk.config.ConfigException;
|
32
13
|
import org.embulk.spi.Column;
|
33
|
-
import org.embulk.spi.DataException;
|
34
14
|
import org.embulk.spi.ColumnVisitor;
|
35
|
-
import org.embulk.spi.
|
15
|
+
import org.embulk.spi.DataException;
|
36
16
|
import org.embulk.spi.Exec;
|
37
17
|
import org.embulk.spi.FilterPlugin;
|
38
18
|
import org.embulk.spi.Page;
|
39
19
|
import org.embulk.spi.PageBuilder;
|
40
|
-
import org.embulk.spi.PageReader;
|
41
20
|
import org.embulk.spi.PageOutput;
|
21
|
+
import org.embulk.spi.PageReader;
|
42
22
|
import org.embulk.spi.Schema;
|
43
|
-
import org.
|
23
|
+
import org.slf4j.Logger;
|
24
|
+
|
25
|
+
import javax.crypto.BadPaddingException;
|
26
|
+
import javax.crypto.Cipher;
|
27
|
+
import javax.crypto.IllegalBlockSizeException;
|
28
|
+
import javax.crypto.NoSuchPaddingException;
|
29
|
+
import javax.crypto.spec.IvParameterSpec;
|
30
|
+
import javax.crypto.spec.SecretKeySpec;
|
31
|
+
|
32
|
+
import java.security.InvalidAlgorithmParameterException;
|
33
|
+
import java.security.InvalidKeyException;
|
34
|
+
import java.security.NoSuchAlgorithmException;
|
35
|
+
import java.util.EnumSet;
|
36
|
+
import java.util.List;
|
37
|
+
|
38
|
+
import static java.lang.String.format;
|
44
39
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
40
|
+
import static org.apache.commons.lang3.StringUtils.join;
|
45
41
|
|
46
42
|
public class EncryptFilterPlugin
|
47
43
|
implements FilterPlugin
|
@@ -49,12 +45,11 @@ public class EncryptFilterPlugin
|
|
49
45
|
public static enum Algorithm
|
50
46
|
{
|
51
47
|
AES_256_CBC("AES/CBC/PKCS5Padding", "AES", 256, true, "AES", "AES-256", "AES-256-CBC"),
|
52
|
-
AES_192_CBC("AES/CBC/PKCS5Padding", "AES", 192, true, "AES
|
53
|
-
AES_128_CBC("AES/CBC/PKCS5Padding", "AES", 128, true, "AES
|
54
|
-
AES_256_ECB("AES/ECB/PKCS5Padding", "AES", 256, false, "AES
|
55
|
-
AES_192_ECB("AES/ECB/PKCS5Padding", "AES", 192, false, "AES
|
56
|
-
AES_128_ECB("AES/ECB/PKCS5Padding", "AES", 128, false, "AES
|
57
|
-
;
|
48
|
+
AES_192_CBC("AES/CBC/PKCS5Padding", "AES", 192, true, "AES-192", "AES-192-CBC"),
|
49
|
+
AES_128_CBC("AES/CBC/PKCS5Padding", "AES", 128, true, "AES-128", "AES-128-CBC"),
|
50
|
+
AES_256_ECB("AES/ECB/PKCS5Padding", "AES", 256, false, "AES-256-ECB"),
|
51
|
+
AES_192_ECB("AES/ECB/PKCS5Padding", "AES", 192, false, "AES-192-ECB"),
|
52
|
+
AES_128_ECB("AES/ECB/PKCS5Padding", "AES", 128, false, "AES-128-ECB");
|
58
53
|
|
59
54
|
private final String javaName;
|
60
55
|
private final String javaKeySpecName;
|
@@ -112,15 +107,60 @@ public class EncryptFilterPlugin
|
|
112
107
|
}
|
113
108
|
}
|
114
109
|
|
110
|
+
public enum Encoder
|
111
|
+
{
|
112
|
+
BASE64("base64", BaseEncoding.base64()),
|
113
|
+
HEX("hex", BaseEncoding.base16());
|
114
|
+
|
115
|
+
private final BaseEncoding encoding;
|
116
|
+
private final String name;
|
117
|
+
|
118
|
+
Encoder(String name, BaseEncoding encoding)
|
119
|
+
{
|
120
|
+
this.name = name;
|
121
|
+
this.encoding = encoding;
|
122
|
+
}
|
123
|
+
|
124
|
+
public String encode(byte[] bytes)
|
125
|
+
{
|
126
|
+
return encoding.encode(bytes);
|
127
|
+
}
|
128
|
+
|
129
|
+
@JsonCreator
|
130
|
+
public static Encoder fromName(String name)
|
131
|
+
{
|
132
|
+
EnumSet<Encoder> encoders = EnumSet.allOf(Encoder.class);
|
133
|
+
for (Encoder encoder : encoders) {
|
134
|
+
if (encoder.name.equals(name)) {
|
135
|
+
return encoder;
|
136
|
+
}
|
137
|
+
}
|
138
|
+
throw new ConfigException(
|
139
|
+
format("Unsupported output encoding '%s'. Supported encodings are %s.",
|
140
|
+
name,
|
141
|
+
join(encoders, ", ")));
|
142
|
+
}
|
143
|
+
|
144
|
+
@JsonValue
|
145
|
+
@Override
|
146
|
+
public String toString()
|
147
|
+
{
|
148
|
+
return name;
|
149
|
+
}
|
150
|
+
}
|
151
|
+
|
115
152
|
public interface PluginTask
|
116
153
|
extends Task
|
117
154
|
{
|
118
155
|
@Config("algorithm")
|
119
156
|
public Algorithm getAlgorithm();
|
120
157
|
|
158
|
+
@Config("output_encoding")
|
159
|
+
@ConfigDefault("\"base64\"")
|
160
|
+
public Encoder getOutputEncoding();
|
161
|
+
|
121
162
|
@Config("key_hex")
|
122
|
-
|
123
|
-
public Optional<String> getKeyHex();
|
163
|
+
public String getKeyHex();
|
124
164
|
|
125
165
|
@Config("iv_hex")
|
126
166
|
@ConfigDefault("null")
|
@@ -130,19 +170,19 @@ public class EncryptFilterPlugin
|
|
130
170
|
public List<String> getColumnNames();
|
131
171
|
}
|
132
172
|
|
173
|
+
private static final Logger log = Exec.getLogger(EncryptFilterPlugin.class);
|
174
|
+
|
133
175
|
@Override
|
134
176
|
public void transaction(ConfigSource config, Schema inputSchema,
|
135
177
|
FilterPlugin.Control control)
|
136
178
|
{
|
137
179
|
PluginTask task = config.loadConfig(PluginTask.class);
|
138
180
|
|
139
|
-
if (!task.
|
140
|
-
}
|
141
|
-
else if (task.getAlgorithm().useIv() && !task.getIvHex().isPresent()) {
|
181
|
+
if (task.getAlgorithm().useIv() && !task.getIvHex().isPresent()) {
|
142
182
|
throw new ConfigException("Algorithm '" + task.getAlgorithm() + "' requires initialization vector. Please generate one and set it to iv_hex option.");
|
143
183
|
}
|
144
184
|
else if (!task.getAlgorithm().useIv() && task.getIvHex().isPresent()) {
|
145
|
-
|
185
|
+
log.warn("Algorithm '" + task.getAlgorithm() + "' doesn't use initialization vector. Please remove iv_hex option.");
|
146
186
|
}
|
147
187
|
|
148
188
|
// validate configuration
|
@@ -166,7 +206,7 @@ public class EncryptFilterPlugin
|
|
166
206
|
{
|
167
207
|
Algorithm algo = task.getAlgorithm();
|
168
208
|
|
169
|
-
byte[] keyData = BaseEncoding.base16().decode(task.getKeyHex()
|
209
|
+
byte[] keyData = BaseEncoding.base16().decode(task.getKeyHex());
|
170
210
|
SecretKeySpec key = new SecretKeySpec(keyData, algo.getJavaKeySpecName());
|
171
211
|
|
172
212
|
if (algo.useIv()) {
|
@@ -188,7 +228,7 @@ public class EncryptFilterPlugin
|
|
188
228
|
public PageOutput open(TaskSource taskSource, final Schema inputSchema,
|
189
229
|
final Schema outputSchema, final PageOutput output)
|
190
230
|
{
|
191
|
-
PluginTask task = taskSource.loadTask(PluginTask.class);
|
231
|
+
final PluginTask task = taskSource.loadTask(PluginTask.class);
|
192
232
|
|
193
233
|
final Cipher cipher;
|
194
234
|
try {
|
@@ -207,7 +247,7 @@ public class EncryptFilterPlugin
|
|
207
247
|
return new PageOutput() {
|
208
248
|
private final PageReader pageReader = new PageReader(inputSchema);
|
209
249
|
private final PageBuilder pageBuilder = new PageBuilder(Exec.getBufferAllocator(), outputSchema, output);
|
210
|
-
private final
|
250
|
+
private final Encoder encoder = task.getOutputEncoding();
|
211
251
|
|
212
252
|
@Override
|
213
253
|
public void finish()
|
@@ -291,7 +331,7 @@ public class EncryptFilterPlugin
|
|
291
331
|
// this must not happen because always doFinal is called
|
292
332
|
throw new DataException(ex);
|
293
333
|
}
|
294
|
-
String encoded =
|
334
|
+
String encoded = encoder.encode(encrypted);
|
295
335
|
pageBuilder.setString(column, encoded);
|
296
336
|
}
|
297
337
|
else {
|
@@ -1,5 +1,540 @@
|
|
1
1
|
package org.embulk.filter.encrypt;
|
2
2
|
|
3
|
+
import com.google.common.collect.ImmutableList;
|
4
|
+
import org.embulk.EmbulkTestRuntime;
|
5
|
+
import org.embulk.config.ConfigException;
|
6
|
+
import org.embulk.config.ConfigSource;
|
7
|
+
import org.embulk.config.TaskSource;
|
8
|
+
import org.embulk.filter.encrypt.EncryptFilterPlugin.PluginTask;
|
9
|
+
import org.embulk.spi.Column;
|
10
|
+
import org.embulk.spi.ColumnVisitor;
|
11
|
+
import org.embulk.spi.FilterPlugin;
|
12
|
+
import org.embulk.spi.PageOutput;
|
13
|
+
import org.embulk.spi.PageReader;
|
14
|
+
import org.embulk.spi.Schema;
|
15
|
+
import org.embulk.spi.TestPageBuilderReader.MockPageOutput;
|
16
|
+
import org.embulk.spi.type.Types;
|
17
|
+
import org.junit.Before;
|
18
|
+
import org.junit.Rule;
|
19
|
+
import org.junit.Test;
|
20
|
+
import org.junit.rules.ExpectedException;
|
21
|
+
|
22
|
+
import javax.crypto.BadPaddingException;
|
23
|
+
import javax.crypto.Cipher;
|
24
|
+
import javax.crypto.IllegalBlockSizeException;
|
25
|
+
import javax.crypto.NoSuchPaddingException;
|
26
|
+
import javax.crypto.spec.IvParameterSpec;
|
27
|
+
import javax.crypto.spec.SecretKeySpec;
|
28
|
+
|
29
|
+
import java.security.GeneralSecurityException;
|
30
|
+
import java.security.InvalidAlgorithmParameterException;
|
31
|
+
import java.security.InvalidKeyException;
|
32
|
+
import java.security.NoSuchAlgorithmException;
|
33
|
+
import java.util.Arrays;
|
34
|
+
import java.util.List;
|
35
|
+
|
36
|
+
import static com.google.common.base.Charsets.UTF_8;
|
37
|
+
import static com.google.common.io.BaseEncoding.base16;
|
38
|
+
import static com.google.common.io.BaseEncoding.base64;
|
39
|
+
import static java.lang.String.format;
|
40
|
+
import static java.util.Collections.emptyList;
|
41
|
+
import static java.util.Objects.requireNonNull;
|
42
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Algorithm;
|
43
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Algorithm.AES_128_CBC;
|
44
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Algorithm.AES_128_ECB;
|
45
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Algorithm.AES_192_CBC;
|
46
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Algorithm.AES_192_ECB;
|
47
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Algorithm.AES_256_CBC;
|
48
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Algorithm.AES_256_ECB;
|
49
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Encoder;
|
50
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Encoder.BASE64;
|
51
|
+
import static org.embulk.filter.encrypt.EncryptFilterPlugin.Encoder.HEX;
|
52
|
+
import static org.embulk.spi.PageTestUtils.buildPage;
|
53
|
+
import static org.junit.Assert.assertEquals;
|
54
|
+
import static org.junit.Assert.assertNotEquals;
|
55
|
+
import static org.junit.Assert.fail;
|
56
|
+
|
3
57
|
public class TestEncryptFilterPlugin
|
4
58
|
{
|
59
|
+
@Rule
|
60
|
+
public EmbulkTestRuntime runtime = new EmbulkTestRuntime();
|
61
|
+
|
62
|
+
@Rule
|
63
|
+
public ExpectedException expectedException = ExpectedException.none();
|
64
|
+
|
65
|
+
private EncryptFilterPlugin plugin;
|
66
|
+
|
67
|
+
private ConfigSource defaultConfig()
|
68
|
+
{
|
69
|
+
return runtime.getExec().newConfigSource()
|
70
|
+
.set("type", "encrypt")
|
71
|
+
.set("algorithm", "AES-256-CBC")
|
72
|
+
.set("key_hex", "D0867C9310D061F17ACD11EB30DE68265DCB79849BE5FB2BE157919D19BF2F42")
|
73
|
+
.set("iv_hex", "2A1D6BD59D2DB50A59364BAD3B9B6544");
|
74
|
+
}
|
75
|
+
|
76
|
+
@Before
|
77
|
+
public void setup()
|
78
|
+
{
|
79
|
+
plugin = new EncryptFilterPlugin();
|
80
|
+
}
|
81
|
+
|
82
|
+
@Test(expected = GeneralSecurityException.class)
|
83
|
+
public void encrypt_with_AES_256_CBC() throws Exception
|
84
|
+
{
|
85
|
+
ConfigSource config = defaultConfig()
|
86
|
+
.set("algorithm", "AES-256-CBC")
|
87
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
88
|
+
Schema schema = Schema.builder()
|
89
|
+
.add("should_be_encrypted", Types.STRING)
|
90
|
+
.build();
|
91
|
+
|
92
|
+
List rawRecord = ImmutableList.of("My super secret");
|
93
|
+
List filteredRecord = applyFilter(config, schema, rawRecord);
|
94
|
+
|
95
|
+
String plaintext = (String) rawRecord.get(0);
|
96
|
+
String ciphertext = (String) filteredRecord.get(0);
|
97
|
+
|
98
|
+
assertNotEquals(plaintext, ciphertext);
|
99
|
+
assertEquals(plaintext, decrypt(ciphertext, AES_256_CBC, config));
|
100
|
+
|
101
|
+
// Apparently it should fail when decrypt with a different algorithm
|
102
|
+
decrypt(ciphertext, AES_128_ECB, config);
|
103
|
+
}
|
104
|
+
|
105
|
+
@Test
|
106
|
+
public void encrypt_with_AES_256_CBC__alias_should_work_too() throws Exception
|
107
|
+
{
|
108
|
+
ConfigSource config = defaultConfig()
|
109
|
+
.set("algorithm", "AES")
|
110
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
111
|
+
Schema schema = Schema.builder()
|
112
|
+
.add("should_be_encrypted", Types.STRING)
|
113
|
+
.build();
|
114
|
+
|
115
|
+
String plaintext = "My super secret!";
|
116
|
+
|
117
|
+
assertEquals(
|
118
|
+
plaintext,
|
119
|
+
decrypt((String) applyFilter(config, schema, ImmutableList.of(plaintext)).get(0),
|
120
|
+
AES_256_CBC,
|
121
|
+
config));
|
122
|
+
}
|
123
|
+
|
124
|
+
@Test
|
125
|
+
public void encrypt_with_AES_192_CBC() throws Exception
|
126
|
+
{
|
127
|
+
ConfigSource config = defaultConfig()
|
128
|
+
.set("algorithm", "AES-192-CBC")
|
129
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
130
|
+
Schema schema = Schema.builder()
|
131
|
+
.add("should_be_encrypted", Types.STRING)
|
132
|
+
.build();
|
133
|
+
|
134
|
+
String plaintext = "My super secret!";
|
135
|
+
|
136
|
+
assertEquals(
|
137
|
+
plaintext,
|
138
|
+
decrypt((String) applyFilter(config, schema, ImmutableList.of(plaintext)).get(0),
|
139
|
+
AES_192_CBC,
|
140
|
+
config));
|
141
|
+
}
|
142
|
+
|
143
|
+
@Test
|
144
|
+
public void encrypt_with_AES_128_CBC() throws Exception
|
145
|
+
{
|
146
|
+
ConfigSource config = defaultConfig()
|
147
|
+
.set("algorithm", "AES-128-CBC")
|
148
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
149
|
+
Schema schema = Schema.builder()
|
150
|
+
.add("should_be_encrypted", Types.STRING)
|
151
|
+
.build();
|
152
|
+
|
153
|
+
String plaintext = "My super secret!";
|
154
|
+
|
155
|
+
assertEquals(
|
156
|
+
plaintext,
|
157
|
+
decrypt((String) applyFilter(config, schema, ImmutableList.of(plaintext)).get(0),
|
158
|
+
AES_128_CBC,
|
159
|
+
config));
|
160
|
+
}
|
161
|
+
|
162
|
+
@Test
|
163
|
+
public void encrypt_with_AES_256_ECB() throws Exception
|
164
|
+
{
|
165
|
+
ConfigSource config = defaultConfig()
|
166
|
+
.set("algorithm", "AES-256-ECB")
|
167
|
+
.remove("iv_hex")
|
168
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
169
|
+
Schema schema = Schema.builder()
|
170
|
+
.add("should_be_encrypted", Types.STRING)
|
171
|
+
.build();
|
172
|
+
|
173
|
+
String plaintext = "My super secret!";
|
174
|
+
|
175
|
+
assertEquals(
|
176
|
+
plaintext,
|
177
|
+
decrypt((String) applyFilter(config, schema, ImmutableList.of(plaintext)).get(0),
|
178
|
+
AES_256_ECB,
|
179
|
+
config));
|
180
|
+
}
|
181
|
+
|
182
|
+
@Test
|
183
|
+
public void encrypt_with_AES_192_ECB() throws Exception
|
184
|
+
{
|
185
|
+
ConfigSource config = defaultConfig()
|
186
|
+
.set("algorithm", "AES-192-ECB")
|
187
|
+
.remove("iv_hex")
|
188
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
189
|
+
Schema schema = Schema.builder()
|
190
|
+
.add("should_be_encrypted", Types.STRING)
|
191
|
+
.build();
|
192
|
+
|
193
|
+
String plaintext = "My super secret!";
|
194
|
+
|
195
|
+
assertEquals(
|
196
|
+
plaintext,
|
197
|
+
decrypt((String) applyFilter(config, schema, ImmutableList.of(plaintext)).get(0),
|
198
|
+
AES_192_ECB,
|
199
|
+
config));
|
200
|
+
}
|
201
|
+
|
202
|
+
@Test
|
203
|
+
public void encrypt_with_AES_128_ECB() throws Exception
|
204
|
+
{
|
205
|
+
ConfigSource config = defaultConfig()
|
206
|
+
.set("algorithm", "AES-128-ECB")
|
207
|
+
.remove("iv_hex")
|
208
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
209
|
+
Schema schema = Schema.builder()
|
210
|
+
.add("should_be_encrypted", Types.STRING)
|
211
|
+
.build();
|
212
|
+
|
213
|
+
String plaintext = "My super secret!";
|
214
|
+
|
215
|
+
assertEquals(
|
216
|
+
plaintext,
|
217
|
+
decrypt((String) applyFilter(config, schema, ImmutableList.of(plaintext)).get(0),
|
218
|
+
AES_128_ECB,
|
219
|
+
config));
|
220
|
+
}
|
221
|
+
|
222
|
+
@Test
|
223
|
+
public void encrypt_selective_columns() throws Exception
|
224
|
+
{
|
225
|
+
ConfigSource config = defaultConfig()
|
226
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
227
|
+
Schema schema = Schema.builder()
|
228
|
+
.add("should_be_encrypted", Types.STRING)
|
229
|
+
.add("should_be_unencrypted", Types.STRING)
|
230
|
+
.build();
|
231
|
+
|
232
|
+
List raw = ImmutableList.of("My super secret!", "Hey yo!");
|
233
|
+
List filtered = applyFilter(config, schema, raw);
|
234
|
+
|
235
|
+
// Encrypted column
|
236
|
+
assertNotEquals(raw.get(0), filtered.get(0));
|
237
|
+
assertEquals(raw.get(0), decrypt((String) filtered.get(0), config));
|
238
|
+
|
239
|
+
// Unencrypted column
|
240
|
+
assertEquals(raw.get(1), filtered.get(1));
|
241
|
+
}
|
242
|
+
|
243
|
+
@Test
|
244
|
+
public void nonstring_is_not_intact_whatsoever() throws Exception
|
245
|
+
{
|
246
|
+
ConfigSource config = defaultConfig()
|
247
|
+
.set("column_names", ImmutableList.of("attempt_to_encrypt"));
|
248
|
+
Schema schema = Schema.builder()
|
249
|
+
.add("attempt_to_encrypt", Types.LONG)
|
250
|
+
.build();
|
251
|
+
|
252
|
+
List raw = ImmutableList.of(1L);
|
253
|
+
List filtered = applyFilter(config, schema, raw);
|
254
|
+
|
255
|
+
assertEquals(raw, filtered);
|
256
|
+
}
|
257
|
+
|
258
|
+
@Test
|
259
|
+
public void base64_encoding() throws Exception
|
260
|
+
{
|
261
|
+
ConfigSource config = defaultConfig()
|
262
|
+
.set("output_encoding", "base64")
|
263
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
264
|
+
Schema schema = Schema.builder()
|
265
|
+
.add("should_be_encrypted", Types.STRING)
|
266
|
+
.build();
|
267
|
+
|
268
|
+
String plaintext = "a_plaintext";
|
269
|
+
|
270
|
+
String ciphertext = (String) applyFilter(config, schema, ImmutableList.of(plaintext)).get(0);
|
271
|
+
|
272
|
+
PluginTask task = config.loadConfig(PluginTask.class);
|
273
|
+
|
274
|
+
assertEquals(
|
275
|
+
plaintext,
|
276
|
+
decrypt(ciphertext,
|
277
|
+
task.getAlgorithm(),
|
278
|
+
task.getKeyHex(),
|
279
|
+
task.getIvHex().orNull(),
|
280
|
+
BASE64));
|
281
|
+
try {
|
282
|
+
decrypt(ciphertext,
|
283
|
+
task.getAlgorithm(),
|
284
|
+
task.getKeyHex(),
|
285
|
+
task.getIvHex().orNull(),
|
286
|
+
HEX);
|
287
|
+
}
|
288
|
+
catch (IllegalArgumentException ex) {
|
289
|
+
return;
|
290
|
+
}
|
291
|
+
fail("Expected an IllegalArgumentException for mismatch encoding!");
|
292
|
+
}
|
293
|
+
|
294
|
+
@Test
|
295
|
+
public void hex_encoding() throws Exception
|
296
|
+
{
|
297
|
+
ConfigSource config = defaultConfig()
|
298
|
+
.set("output_encoding", "hex")
|
299
|
+
.set("column_names", ImmutableList.of("should_be_encrypted"));
|
300
|
+
Schema schema = Schema.builder()
|
301
|
+
.add("should_be_encrypted", Types.STRING)
|
302
|
+
.build();
|
303
|
+
|
304
|
+
String plaintext = "a_plaintext";
|
305
|
+
|
306
|
+
String ciphertext = (String) applyFilter(config, schema, ImmutableList.of(plaintext)).get(0);
|
307
|
+
|
308
|
+
PluginTask task = config.loadConfig(PluginTask.class);
|
309
|
+
|
310
|
+
assertEquals(
|
311
|
+
plaintext,
|
312
|
+
decrypt(ciphertext,
|
313
|
+
task.getAlgorithm(),
|
314
|
+
task.getKeyHex(),
|
315
|
+
task.getIvHex().orNull(),
|
316
|
+
HEX));
|
317
|
+
try {
|
318
|
+
decrypt(ciphertext,
|
319
|
+
task.getAlgorithm(),
|
320
|
+
task.getKeyHex(),
|
321
|
+
task.getIvHex().orNull(),
|
322
|
+
BASE64);
|
323
|
+
}
|
324
|
+
// Since hex/base16 is a totally valid subset of base64, this won't yield
|
325
|
+
// an IllegalArgumentException when encoding, but a decrypting exception instead.
|
326
|
+
catch (GeneralSecurityException ex) {
|
327
|
+
return;
|
328
|
+
}
|
329
|
+
fail("Expected an IllegalArgumentException for mismatch encoding!");
|
330
|
+
}
|
331
|
+
|
332
|
+
@Test
|
333
|
+
public void default_output_encoding_should_be_base64()
|
334
|
+
{
|
335
|
+
ConfigSource config = defaultConfig()
|
336
|
+
.remove("output_encoding")
|
337
|
+
.set("column_names", emptyList());
|
338
|
+
PluginTask task = config.loadConfig(PluginTask.class);
|
339
|
+
assertEquals(task.getOutputEncoding(), BASE64);
|
340
|
+
}
|
341
|
+
|
342
|
+
@Test
|
343
|
+
// Previously, missing key_hex does throw a ConfigException but doesn't pinpoint the problematic field
|
344
|
+
public void absence_of_encryption_key_should_yell_a_meaningful_ConfigException() throws Exception
|
345
|
+
{
|
346
|
+
ConfigSource config = defaultConfig()
|
347
|
+
.remove("key_hex")
|
348
|
+
.set("column_names", ImmutableList.of("attempt_to_encrypt"));
|
349
|
+
Schema schema = Schema.builder()
|
350
|
+
.add("attempt_to_encrypt", Types.STRING)
|
351
|
+
.build();
|
352
|
+
|
353
|
+
expectedException.expect(ConfigException.class);
|
354
|
+
expectedException.expectMessage("key_hex");
|
355
|
+
applyFilter(config, schema, ImmutableList.of("Try to encrypt me buddy!"));
|
356
|
+
}
|
357
|
+
|
358
|
+
@Test
|
359
|
+
public void absence_of_iv_on_a_required_iv_algorithm_should_yell_a_meaningful_ConfigException() throws Exception
|
360
|
+
{
|
361
|
+
ConfigSource config = defaultConfig()
|
362
|
+
.remove("iv_hex")
|
363
|
+
.set("algorithm", "AES-256-CBC")
|
364
|
+
.set("column_names", ImmutableList.of("attempt_to_encrypt"));
|
365
|
+
Schema schema = Schema.builder()
|
366
|
+
.add("attempt_to_encrypt", Types.STRING)
|
367
|
+
.build();
|
368
|
+
|
369
|
+
expectedException.expect(ConfigException.class);
|
370
|
+
expectedException.expectMessage("iv_hex");
|
371
|
+
applyFilter(config, schema, ImmutableList.of("Try to encrypt me buddy!"));
|
372
|
+
}
|
373
|
+
|
374
|
+
@Test
|
375
|
+
// Previously, this will throw
|
376
|
+
public void presence_of_iv_on_a_non_iv_algorithm_should_be_silent() throws Exception
|
377
|
+
{
|
378
|
+
ConfigSource config = defaultConfig()
|
379
|
+
.remove("iv_hex")
|
380
|
+
.set("algorithm", "AES-128-ECB")
|
381
|
+
.set("column_names", ImmutableList.of("attempt_to_encrypt"));
|
382
|
+
Schema schema = Schema.builder()
|
383
|
+
.add("attempt_to_encrypt", Types.STRING)
|
384
|
+
.build();
|
385
|
+
|
386
|
+
applyFilter(config, schema, ImmutableList.of("Try to encrypt me buddy!"));
|
387
|
+
}
|
388
|
+
|
389
|
+
/** Apply the filter to a single record */
|
390
|
+
private PageReader applyFilter(ConfigSource config, final Schema schema, final Object... rawRecord)
|
391
|
+
{
|
392
|
+
if (rawRecord.length > schema.getColumnCount()) {
|
393
|
+
throw new UnsupportedOperationException("applyFilter() only supports a single record, " +
|
394
|
+
"number of supplied values exceed the schema column size.");
|
395
|
+
}
|
396
|
+
final PluginTask task = config.loadConfig(PluginTask.class);
|
397
|
+
|
398
|
+
final MockPageOutput filteredOutput = new MockPageOutput();
|
399
|
+
|
400
|
+
plugin.transaction(config, schema, new FilterPlugin.Control()
|
401
|
+
{
|
402
|
+
@Override
|
403
|
+
public void run(TaskSource taskSource, Schema outputSchema)
|
404
|
+
{
|
405
|
+
PageOutput originalOutput = plugin.open(task.dump(), schema, outputSchema, filteredOutput);
|
406
|
+
originalOutput.add(buildPage(runtime.getBufferAllocator(), schema, rawRecord).get(0));
|
407
|
+
originalOutput.finish();
|
408
|
+
originalOutput.close();
|
409
|
+
}
|
410
|
+
});
|
411
|
+
assert filteredOutput.pages.size() == 1;
|
412
|
+
|
413
|
+
PageReader reader = new PageReader(schema);
|
414
|
+
reader.setPage(filteredOutput.pages.get(0));
|
415
|
+
reader.nextRecord();
|
416
|
+
|
417
|
+
return reader;
|
418
|
+
}
|
419
|
+
|
420
|
+
/** Conveniently returning a List after apply a filter over the original list */
|
421
|
+
private List applyFilter(ConfigSource config, Schema schema, List rawRecord)
|
422
|
+
{
|
423
|
+
try (PageReader reader = applyFilter(config, schema, rawRecord.toArray())) {
|
424
|
+
return readToList(reader, schema);
|
425
|
+
}
|
426
|
+
}
|
427
|
+
|
428
|
+
private static List readToList(final PageReader reader, Schema schema)
|
429
|
+
{
|
430
|
+
final Object[] filtered = new Object[schema.getColumnCount()];
|
431
|
+
schema.visitColumns(new ColumnVisitor()
|
432
|
+
{
|
433
|
+
@Override
|
434
|
+
public void booleanColumn(Column column)
|
435
|
+
{
|
436
|
+
filtered[column.getIndex()] = reader.getBoolean(column);
|
437
|
+
}
|
438
|
+
|
439
|
+
@Override
|
440
|
+
public void longColumn(Column column)
|
441
|
+
{
|
442
|
+
filtered[column.getIndex()] = reader.getLong(column);
|
443
|
+
}
|
444
|
+
|
445
|
+
@Override
|
446
|
+
public void doubleColumn(Column column)
|
447
|
+
{
|
448
|
+
filtered[column.getIndex()] = reader.getDouble(column);
|
449
|
+
}
|
450
|
+
|
451
|
+
@Override
|
452
|
+
public void stringColumn(Column column)
|
453
|
+
{
|
454
|
+
filtered[column.getIndex()] = reader.getString(column);
|
455
|
+
}
|
456
|
+
|
457
|
+
@Override
|
458
|
+
public void timestampColumn(Column column)
|
459
|
+
{
|
460
|
+
filtered[column.getIndex()] = reader.getTimestamp(column);
|
461
|
+
}
|
462
|
+
|
463
|
+
@Override
|
464
|
+
public void jsonColumn(Column column)
|
465
|
+
{
|
466
|
+
filtered[column.getIndex()] = reader.getJson(column);
|
467
|
+
}
|
468
|
+
});
|
469
|
+
return Arrays.asList(filtered);
|
470
|
+
}
|
471
|
+
|
472
|
+
private static String decrypt(String ciphertext, Algorithm algo, String keyHex, String ivHex, Encoder encoder)
|
473
|
+
throws NoSuchPaddingException,
|
474
|
+
NoSuchAlgorithmException,
|
475
|
+
InvalidAlgorithmParameterException,
|
476
|
+
InvalidKeyException,
|
477
|
+
BadPaddingException,
|
478
|
+
IllegalBlockSizeException
|
479
|
+
{
|
480
|
+
Cipher cipher = Cipher.getInstance(algo.getJavaName());
|
481
|
+
SecretKeySpec key = new SecretKeySpec(base16().decode(keyHex), algo.getJavaKeySpecName());
|
482
|
+
if (algo.useIv()) {
|
483
|
+
requireNonNull(ivHex, format("IV is required for this algorithm (%s)", algo));
|
484
|
+
IvParameterSpec iv = new IvParameterSpec(base16().decode(ivHex));
|
485
|
+
cipher.init(Cipher.DECRYPT_MODE, key, iv);
|
486
|
+
}
|
487
|
+
else {
|
488
|
+
cipher.init(Cipher.DECRYPT_MODE, key);
|
489
|
+
}
|
490
|
+
return new String(cipher.doFinal(decode(ciphertext, encoder)), UTF_8);
|
491
|
+
}
|
492
|
+
|
493
|
+
private static String decrypt(String ciphertext, ConfigSource config)
|
494
|
+
throws NoSuchPaddingException,
|
495
|
+
InvalidKeyException,
|
496
|
+
NoSuchAlgorithmException,
|
497
|
+
IllegalBlockSizeException,
|
498
|
+
BadPaddingException,
|
499
|
+
InvalidAlgorithmParameterException
|
500
|
+
{
|
501
|
+
PluginTask task = config.loadConfig(PluginTask.class);
|
502
|
+
return decrypt(
|
503
|
+
ciphertext,
|
504
|
+
task.getAlgorithm(),
|
505
|
+
task.getKeyHex(),
|
506
|
+
task.getIvHex().orNull(),
|
507
|
+
task.getOutputEncoding());
|
508
|
+
}
|
509
|
+
|
510
|
+
/** Just to be explicit about the algorithm in used */
|
511
|
+
private static String decrypt(String ciphertext, Algorithm algo, ConfigSource config)
|
512
|
+
throws NoSuchPaddingException,
|
513
|
+
InvalidKeyException,
|
514
|
+
NoSuchAlgorithmException,
|
515
|
+
IllegalBlockSizeException,
|
516
|
+
BadPaddingException,
|
517
|
+
InvalidAlgorithmParameterException
|
518
|
+
{
|
519
|
+
PluginTask task = config.loadConfig(PluginTask.class);
|
520
|
+
return decrypt(
|
521
|
+
ciphertext,
|
522
|
+
algo,
|
523
|
+
task.getKeyHex(),
|
524
|
+
task.getIvHex().orNull(),
|
525
|
+
task.getOutputEncoding());
|
526
|
+
}
|
527
|
+
|
528
|
+
/** Decoding by reversing the originalEncoder */
|
529
|
+
private static byte[] decode(String encoded, Encoder originalEncoder)
|
530
|
+
{
|
531
|
+
switch (originalEncoder) {
|
532
|
+
case BASE64:
|
533
|
+
return base64().decode(encoded);
|
534
|
+
case HEX:
|
535
|
+
return base16().decode(encoded);
|
536
|
+
default:
|
537
|
+
throw new UnsupportedOperationException("Unrecognized encoder: " + originalEncoder);
|
538
|
+
}
|
539
|
+
}
|
5
540
|
}
|
metadata
CHANGED
@@ -1,43 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: embulk-filter-encrypt
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sadayuki Furuhashi
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-06-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name: bundler
|
15
|
-
version_requirements: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ~>
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.0'
|
20
14
|
requirement: !ruby/object:Gem::Requirement
|
21
15
|
requirements:
|
22
16
|
- - ~>
|
23
17
|
- !ruby/object:Gem::Version
|
24
18
|
version: '1.0'
|
19
|
+
name: bundler
|
25
20
|
prerelease: false
|
26
21
|
type: :development
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: rake
|
29
22
|
version_requirements: !ruby/object:Gem::Requirement
|
30
23
|
requirements:
|
31
|
-
- -
|
24
|
+
- - ~>
|
32
25
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
34
28
|
requirement: !ruby/object:Gem::Requirement
|
35
29
|
requirements:
|
36
30
|
- - '>='
|
37
31
|
- !ruby/object:Gem::Version
|
38
32
|
version: '10.0'
|
33
|
+
name: rake
|
39
34
|
prerelease: false
|
40
35
|
type: :development
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
41
|
description: Encrypt
|
42
42
|
email:
|
43
43
|
- frsyuki@gmail.com
|
@@ -62,7 +62,7 @@ files:
|
|
62
62
|
- lib/embulk/filter/encrypt.rb
|
63
63
|
- src/main/java/org/embulk/filter/encrypt/EncryptFilterPlugin.java
|
64
64
|
- src/test/java/org/embulk/filter/encrypt/TestEncryptFilterPlugin.java
|
65
|
-
- classpath/embulk-filter-encrypt-0.
|
65
|
+
- classpath/embulk-filter-encrypt-0.2.0.jar
|
66
66
|
homepage: https://github.com/embulk/embulk-filter-encrypt
|
67
67
|
licenses:
|
68
68
|
- Apache 2.0
|