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 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