embulk-filter-encrypt 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d7ccdad7bf5592cae007e69d377dd6d8fe729fbe
4
- data.tar.gz: f3a74ecd872841e10d0b740b117e7525c0aa156d
3
+ metadata.gz: 6de855d8ac0f1c315220c4907fe6eb8d7bd40043
4
+ data.tar.gz: b21751d55746b058abfef300e3927187fb8f6185
5
5
  SHA512:
6
- metadata.gz: 637dcbc1f0d2ac4c261ae7a1fcd43e9620e12e3668939429b1a88a93b4ba6ab655ce7e2f04e5fc2ea5c75cf803ebad1f633bb2700964a790a5fe46a9405afbc8
7
- data.tar.gz: 4971f179382b25ab01dbad3269d11f0b9e275d5f640262355da66157869c738025da408bd7cd395b0b95aaa5b8c36f483d85d9bdc5c572cccc55c854b07d5cb2
6
+ metadata.gz: 97fbc5a6e919eb2b053fd2b4862eebc4edd1e4b1a2245c8274fe0780fcab557610a2c8aa8feb3c67cd13ea04bba2ed06b69416b4d0ea3ce9e825366c2b6e2b1c
7
+ data.tar.gz: 80f240d5de8da536ad01c5a998ba273740cc5aa63c5b102dcb83d816666921e3c171204ebaa56ae8e102f434fe0214499872240fcc4f0ee241170855a8a8c738
data/ChangeLog CHANGED
@@ -1,3 +1,7 @@
1
+ Release 0.2.0 - 2018-06-13
2
+
3
+ * Add `output_encoding` config, supports "base64" and "hex".
4
+
1
5
  Release 0.1.0 - 2016-03-01
2
6
 
3
7
  * The first release.
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**: encyrption initialization vector (string, required if mode of the algorithm is CBC)
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.1.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.ConfigDiff;
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.type.StringType;
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.embulk.spi.time.Timestamp;
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", "AES-192", "AES-192-CBC"),
53
- AES_128_CBC("AES/CBC/PKCS5Padding", "AES", 128, true, "AES", "AES-128", "AES-128-CBC"),
54
- AES_256_ECB("AES/ECB/PKCS5Padding", "AES", 256, false, "AES", "AES-256", "AES-256-ECB"),
55
- AES_192_ECB("AES/ECB/PKCS5Padding", "AES", 192, false, "AES", "AES-192", "AES-192-ECB"),
56
- AES_128_ECB("AES/ECB/PKCS5Padding", "AES", 128, false, "AES", "AES-128", "AES-128-ECB"),
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
- @ConfigDefault("null")
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.getKeyHex().isPresent()) {
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
- throw new ConfigException("Algorithm '" + task.getAlgorithm() + "' doesn't use initialization vector. Please remove iv_hex option.");
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().get());
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 BaseEncoding base64 = BaseEncoding.base64();
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 = base64.encode(encrypted);
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.1.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: 2016-03-02 00:00:00.000000000 Z
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: '10.0'
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.1.0.jar
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