Initial commit of the PACE library.

This commit is contained in:
Scott Ruoti committed 2017-05-31 17:36:58 -04:00
commit ccf94c8203
266 files changed
+24194

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
# Copyright 2016 MIT Lincoln Laboratory
#
# Licensed under the Apache License, Version 2.0 (the "License")#
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Maven ignores
/target/
# IDE ignores
/.settings/
/.project
/.classpath
/.pydevproject
/.idea
/*.iml
/nbproject/
/nbactions.xml
/nb-configuration.xml
+20
View File
@@ -0,0 +1,20 @@
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
PACE depends on artifacts which can be found in Maven Central.
Each module has its own dependencies. Please refer to the individual
modules' pom.xml files for a comprehensive listing.
+226
View File
@@ -0,0 +1,226 @@
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
Confidentiality
---------------
The encryption code allows clients to encrypt data before storing it at the
server. This prevents that data from being read by the server administrator.
Additionally, it can be used to cryptographically enforce visibility labels.
The encryption code is designed as a drop-in replacement for Accumulo's
BatchWriter and BatchScanner classes. It can be configured programmatically
or through the use of configuration files (preferred). Cryptographic keys
are provided by the developer (see [key management][key-management]).
API
---
Classes for encryption functionality are found in `edu.mit.ll.pace.encryption`.
#### `EncryptedBatchWriter`
The encrypted batch writer is created similarly to a `BatchWriter`. After
creation, it is used identically to the Accumulo `BatchWriter`.
```java
BatchWriter writer = new EncryptedBatchWriter(
instance.getConnector(),
TABLE_NAME,
batchWriterConfig,
encryptionConfig,
encryptionKeys);
```
#### `EncryptedBatchScanner`
The encrypted batch scanner is created similarly to a `BatchScanner`. After
creation, it is used identically to the Accumulo `BatchScanner`.
```java
BatchScanner scanner = new EncryptedBatchScanner(
instance.getConnector(),
TABLE_NAME,
authorizations,
numThreads,
encryptionConfig,
encryptionKeys);
```
Configuration
-------------
The encryption configuration defines which fields will be encrypted, and with
which modes of operation. The encryption configuration of each field can be set
separately from the other fields. This configuration is given by an
`EncryptionConfig` object. These configuration objects can be created
programmatically or loaded from a configuration file.
The configuration file is split into sections, one for each field that will
store encrypted data. The layout of these sections is as follows:
```ini
[{row, columnFamily, columnQualifier, value}]
cipher = {AES_CTR, AES_CFB, AES_CBC, AES_OFB, AES_GCM, AES_SIV_DETERMINISTIC}
provider = {SunJCE,BC,...}
useVisibility = {true,false}
keyId = {keyId}
keyLength = {16, 24, 32, ...}
sources = {row, columnFamily, columnQualifier, columnVisibility, value}
```
##### Section Header
The section header defines which field will store the encrypted data.
##### cipher
Dictates the algorithm, mode, and padding to use for encryption. The various
modes fall into the following
three categories:
1. Semantically secure encryption—Encrypted data reveals no partial information
about the plaintext data (other than length). In particular, encryption does not
reveal equalities of plaintexts; for a given key, encrypting the same value
repeatedly will produce different ciphertexts. This prevents a server
administrator from detecting patterns in the data.
2. Authenticated encryption—In addition to providing confidentiality with
semantic security, authenticated encryption provides integrity and authenticity,
meaning that any unauthorized changes to the data will be detected.
3. Deterministic encryption—For a given key, encrypting the same value
repeatedly will always produce the same ciphertext. This allows for encrypted
data to be searched for server-side but also allows the server administrator to
detect patterns in the data (see https://cs.brown.edu/~seny/pubs/edb.pdf).
**For this reason, care should be taken when using this type of encryption**,
and it should only be used when the performance gains are more important than
the loss of security.
The following ciphers are available for use in encryption.
* **AES_CTR**—Semantically secure—Encrypts data using AES in CTR mode with no
padding.
* **AES_CFB**—Semantically secure—Encrypts data using AES in CFB mode with no
padding.
* **AES_CBC**—Semantically secure—Encrypts data using AES in CBC mode with PKCS5
padding.
* **AES_OFB**—Semantically secure—Encrypts data using AES in OFB mode with no
padding.
* **AES_GCM**—Authenticated—Encrypts data using AES in GCM mode with no padding.
* **AES_SIV_DETERMINISTIC**—Deterministic—Encrypts data using AES in SIV mode
with no padding.
If unsure, we recommend the use of AES_GCM. It is very important to note that
the GCM encryption key must be refreshed to limit the number of GCM encryptions
with a given key in order to prevent IV reuse; similarly for CTR mode. The NIST
recommendation (SP 800-38 D) is to limit the number of invocations of encryption
with GCM for any given key to 2^32.
##### provider (Optional)
The Java cryptographic provider to use. If not set, will let the system decide
the appropriate cryptographic provider.
##### useVisibility (Optional, default=false)
Whether to use encryption to enforce the column visibility field. If this is set
to true, only users that have encryption keys corresponding to the appropriate
labels in the column visibility field will be able to decrypt the data. For
example, if the visibility is `doctor | (nurse & admin)`, a user with only the
doctor attribute key will be able to decrypt the row, but a user with only the
nurse key (and neither the doctor key nor the admin key) will not be able to
decrypt.
When writing data, there is currently a limitation that requires the client to
have keys for all labels in the visibility field. In the above example, the
user would need to have the doctor, nurse, **and** admin attribute keys to
encrypt the row. We are looking to address this limitation in future versions of
the code base.
##### keyId (Optional, default=cipher)
An identifier for the keys used to encrypt the field. This value is passed to
the key management interface to specify which key to retrieve. If not set, will
use the value given in the cipher field.
##### keyLength (Optional, default=16|32)
The length of the key to use for encryption, in bytes. This is one of {16, 24,
32} for non-deterministic encryption ciphers, and {32, 48, 64} for deterministic
encryption ciphers. If not set, will use the smallest appropriate key length.
##### sources (Optional, default=section header)
The source of the data that should be encrypted into the field. If not set, the
encrypted data will be drawn from the same field as the encrypted.
Example configurations
----------------------
Within the core library's jar resources there are three example configurations
(`edu/mit/ll/place/encryption`).
These demonstrate some configurations that should support a large number of use
cases.
* **encrypt-value**—Encrypts the value using AES in GCM mode, with the
encryption cryptographically enforcing the column visibility.
* **encrypt-entry**—Encrypts each field in the entry (row, columnFamily,
columnQualifier, value) individually using AES in GCM mode, with the encryption
cryptographically enforcing the column visibility.
* **searchable**—Encrypts each field in the entry key (row, columnFamily,
columnQualifier) individually using AES in SIV mode (deterministic encryption),
with the encryption not being used to enforce column visibility. The value is
encrypted using AES in GCM mode, with the encryption cryptographically enforcing
the column visibility. Since the key is encrypted deterministically, the system
can still search for records server-side.
Limitations
-----------
1. If any field in the key is encrypted with non-deterministic encryption,
Accumulo versioning on the table will be broken.
1. If any field in the key is encrypted with non-deterministic encryption, it is
not possible for the `EncryptedBatchWriter` to delete values. Instead it is
necessary to delete keys using an `ItemProcessingIterator`:
```java
EncryptedBatchScanner encryptedBatchScanner = ...;
BatchWriter writer = ...;
ItemProcessingIterator<Entry<Key,Value>> iterator = encryptedBatchScanner.iterator();
while (iterator.hasNext()) {
if (isKeyToDelete(iterator.next())) {
Entry<Key,Value> unprocessed = iterator.unprocessed();
Mutation delete = new Mutation(unprocessed.getKey().getRow());
delete.putDelete(unprocessed.getKey().getColumnFamily(), unprocessed.getKey().getColumnQualifier(), unprocessed.getKey().getColumnVisibilityParsed(),
unprocessed.getKey().getTimestamp());
}
}
writer.close();
```
1. If the entry key is encrypted deterministically and one of the encryption
keys is revoked—resulting in a new version of the encryption key—then entries
created with the new key will not version correctly with entries created with
the old key. Both sets of entries will show up in the scanner.
1. When attempting to filter results returned by `EncryptedBatchScanner`, if
possible, the filtering will happen server side. In several situations it is
necessary to still filter some of the data client side:
1. If any of the search fields (e.g., row, column family) are encrypted
non-deterministially, than those field must be filtered client side.
2. If a contents of a search field are encrypted into another field (e.g.,
row encrypts both the row and column family, leaving the column family
zeroed out), then the field can only be filtered server side if the
encrypted field containing it is also part of the search (e.g., searching
using a range will work, but not searching using `fetchColumnFamily`).
3. For a deterministically encrypted field, the search must be a single
value and not a range of values. Similarly, fields preceding the encrypted
field must be a single value and not a range of values. Fields after the
encrypted field can be searched over a range. (e.g., if the column family is
encrypted, you can only create ranges that have the same row and column
family, but the column qualifier can be multiple values.)
[key-management]: KEY_MANAGEMENT.md
+151
View File
@@ -0,0 +1,151 @@
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
Examples
--------
In the `examples/simple` module we have included several examples that show
how to use the PACE library for encryption and signatures:
* Encryption
* GenerateEncryptionKeys—Demonstrates how encryption keys can be managed from
the command line.
* EncryptedReadWriteExample—Modified
`org.apache.accumulo.examples.simple.client.ReadWriteExample`; demonstrates
how to write, then read, encrypted data.
* EncryptedConverterExample—Converts an existing unencrypted table into an
encrypted table.
* Signature
* GenerateSignatureKeys—Demonstrates how signature keys can be managed from
the command line.
* SignedReadWriteExample—Modified
`org.apache.accumulo.examples.simple.client.ReadWriteExample`; demonstrates
how to write, then, read, signed data.
* SignedConverterExample—Converts an existing unsigned table into a signed
table.
We describe below how to use these samples to generate keys, convert an existing
table to one that is signed/encrypted, and read data from that table.
To prepare to run the following examples, first set up the environment as
follows:
```bash
cd examples/simple
mvn clean compile
mkdir -p target/keys
```
Encryption
----------
### Key generation
First we will generate three encryption keys and put them into a key store for
later use:
1. An encryption key for AES_GCM.
2. An encryption key for AES_GCM when the visibility label is `secret`.
3. An encryption key for AES_GCM when the visibility label is `default`.
```
mvn exec:java -Dexec.mainClass=edu.mit.ll.pace.examples.simple.GenerateEncryptionKeys -Dexec.args="--master-key-file target/keys/master.keys --write-master-key --key AES_GCM --key AES_GCM|secret --key AES_GCM|default --store target/keys/enc.keys"
```
The master secret file stores a randomly generated master secret that is used
in the generation of all encryption keys. The master secret file is needed in
order to generate more keys later. For example, the following command creates a
second encryption key store that has keys 1 and 3 from the first key store, but
not key 2.
```
mvn exec:java -Dexec.mainClass=edu.mit.ll.pace.examples.simple.GenerateEncryptionKeys -Dexec.args="--master-key-file target/keys/master.keys --read-master-key --key AES_GCM --key AES_GCM|default --store target/keys/enc3.keys"
```
### Converting Data
First we will use an example from Accumulo to create data in a table:
```bash
$ACCUMULO_HOME/bin/accumulo org.apache.accumulo.examples.simple.client.ReadWriteExample -i instance -z zookeepers -u user -p password --table test --createtable --create --auths secret,default
```
Now we will convert that data into encrypted form:
```
mvn exec:java -Dexec.mainClass=edu.mit.ll.pace.examples.simple.EncryptedConverterExample -Dexec.args="-i instance -z zookeepers -u user -p password --source test --destination test2 --createtable --encryption-config ../../core/src/main/resources/edu/mit/ll/pace/encryption/encrypt-value.ini --encryption-keys target/keys/enc.keys --auths secret,default"
```
In the Accumulo shell you can view the encrypted data:
```bash
table test2
scan
```
### Reading Data
Next we will read the data in its unencrypted form:
```
mvn exec:java -Dexec.mainClass=edu.mit.ll.pace.examples.simple.EncryptedReadWriteExample -Dexec.args="-i instance -z zookeepers -u user -p password --table test2 --read --encryption-config ../../core/src/main/resources/edu/mit/ll/pace/encryption/encrypt-value.ini --encryption-keys target/keys/enc.keys --auths secret,default"
```
Signatures
----------
### Key generation
First we will generate an RSA signature and verification keys for two users,
Alice and Bob.
The following command will create a key store for Alice (containing Alice's
generated private and public RSA key):
```
mvn exec:java -Dexec.mainClass=edu.mit.ll.pace.examples.simple.GenerateSignatureKeys -Dexec.args="--key Alice|RSA|2048 --key-dir target/keys --public-key-file target/keys/public.keys"
```
Later Bob's key store is created (containing Bob's generated public and private
keys, and Alice's public key). Alice's key store is also updated to included
Bob's public key:
```
mvn exec:java -Dexec.mainClass=edu.mit.ll.pace.examples.simple.GenerateSignatureKeys -Dexec.args="--key Bob|RSA|2048 --key-dir target/keys --public-key-file target/keys/public.keys --update"
```
### Converting Data
First we will use an example from Accumulo to create data in a table:
```bash
$ACCUMULO_HOME/bin/accumulo org.apache.accumulo.examples.simple.client.ReadWriteExample -i instance -z zookeepers -u user -p password --table test3 --createtable --create --auths secret,default
```
Now we will convert that data into signed form:
```
mvn exec:java -Dexec.mainClass=edu.mit.ll.pace.examples.simple.SignatureConverterExample -Dexec.args="-i instance -z zookeepers -u user -p password --source test3 --destination test4 --createtable --signature-config ../../core/src/main/resources/edu/mit/ll/pace/signature/rsa.ini --signature-keys target/keys/Alice_RSA.keys --auths secret,default"
```
In the Accumulo shell you can check that the data is signed:
```bash
table test4
scan
```
### Reading Data
Next we will read the data, verifying its signature. We use Bob's key store to
demonstrate that different users can verify signatures, as long as they have the
appropriate public key.
```
mvn exec:java -Dexec.mainClass=edu.mit.ll.pace.examples.simple.SignedReadWriteExample -Dexec.args="-i instance -z zookeepers -u user -p password --table test4 --read --signature-config ../../core/src/main/resources/edu/mit/ll/pace/signature/rsa.ini --signature-keys target/keys/Bob_RSA.keys --auths secret,default"
```
+39
View File
@@ -0,0 +1,39 @@
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
Key Management
--------------
In PACE there are two types of keys, symmetric encryption keys (e.g., AES) and
asymmetric signature keys (e.g., RSA). While PACE defines the structure of these
keys (e.g., key length) it is left to the developers to generate, distribute,
and manage the keys.
While we plan to provide an example key management server
and client in the future, these are not currently implemented. In the meantime,
the [examples][examples], which provide basic key management operations, can be
used as templates to define your own key management framework. These
examples make use of locally stored key containers, which can be used
by importing the following artifact:
```xml
<dependency>
<groupId>edu.mit.ll.pace</groupId>
<artifactId>pace-keymanagement-common</artifactId>
<version>1.0.0</version>
</dependency>
```
[examples]: EXAMPLES.md
+212
View File
@@ -0,0 +1,212 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
**********
PACE SUBCOMPONENTS:
The PACE project contains subcomponents with separate copyright
notices and license terms. Your use of the source code for the these
subcomponents is subject to the terms and conditions of the following licenses.
**********
+2
View File
@@ -0,0 +1,2 @@
MIT Lincoln Laboratory
Copyright 2016 MIT Lincoln Laboratory
+177
View File
@@ -0,0 +1,177 @@
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
[Proactively-secure Accumulo with Cryptographic Enforcement (PACE)][pace]
--
The [PACE][pace] library adds cryptographic protections to
[Apache Accumulo][accumulo], with the goal of protecting data from a potentially
malicious Accumulo server administrator. These include encrypting data
([encryption]) and signing data ([signature]) stored in Accumulo.
Installation
------------
To use PACE, include the core library as a maven dependency:
```xml
<dependency>
<groupId>edu.mit.ll.pace</groupId>
<artifactId>pace-core</artifactId>
<version>1.0.0</version>
</dependency>
```
Documentation
-------------
There are three main components to PACE:
1. [Encryption][encryption]—Ensuring that data can be read by authorized
parties.
2. [Signatures][signature]—Ensuring that data can only be modified by authorized
parties.
3. [Key management][key-management]—Managing the cryptographic keys necessary
for encryption and signatures to function properly.
Currently, developers can choose to either encrypt **or** sign data. In the
future, we plan to allow users to do both simultaneously.
API
---
The public PACE API is composed of all public types in the following packages:
* edu.mit.ll.pace
* edu.mit.ll.pace.encryption
* edu.mit.ll.pace.signature
* edu.mit.ll.pace.keymanagement.common
A type is a class, interface, or enum. Anything with public or protected
acccess in an API type is in the API. This includes, but is not limited to:
methods, members, classes, interfaces, and enums. Package-private types in
the above packages are *not* considered public API.
The PACE project maintains binary compatibility across this API within a
major release, as defined in the Java Language Specification 3rd ed. All
API changes will follow [semver 2.0][semver].
Examples
--------
Example code can be found in the examples/simple project. The examples
demonstrate how to generate keys, read and write encrypted/signed data, and
migrate existing tables to use PACE. Descriptions of these examples can be found
in the [examples README][examples].
Building
--------
Accumulo uses [Maven] to compile, test, and package its source.
In addition to the standard Maven lifecycle, the following commands are
supported:
* **apilyzer:analyze**—Analyze the public PACE API, ensuring that it is
[well structured][apilyzer].
* **cobertura:cobertura**–Generates test coverage report in the site directory.
* **javadoc:javadoc**–Generates JavaDoc for the PACE project.
Future Work
-----------
* Key Management—While the [example code][examples] demonstrates how to manually
maintain cryptographic keys, it is not automated, nor does it scale well.
In the future, we plan to add a key management client and server that will
simplify the process of key management.
Troubleshooting
---------------
If you get an `InvalidKeyException: Illegal key size or default parameters`
message, you most likely need to install the Java Cryptography Extension
(JCE) Unlimited Strength Jurisdiction Policy Files. They can be found on
Oracle's website.
Export Control
--------------
This distribution includes cryptographic software. The country in which you
currently reside may have restrictions on the import, possession, use, and/or
re-export to another country, of encryption software. BEFORE using any
encryption software, please check your country's laws, regulations and
policies concerning the import, possession, or use, and re-export of encryption
software, to see if this is permitted. See <http://www.wassenaar.org/> for more
information.
<!--
The U.S. Government Department of Commerce, Bureau of Industry and Security
(BIS), has classified this software as Export Commodity Control Number (ECCN)
5D002.C.1, which includes information security software using or performing
cryptographic functions with asymmetric algorithms. The form and manner of this
Apache Software Foundation distribution makes it eligible for export under the
License Exception ENC Technology Software Unrestricted (TSU) exception (see the
BIS Export Administration Regulations, Section 740.13) for both object code and
source code.
-->
The following provides more details on the included cryptographic software:
PACE uses the built-in Java cryptography libraries in its
encryption implementation. See [Oracle's export-regulations doc][java-export]
for more details on Java's cryptography features. Apache Accumulo also uses
the Bouncy Castle library for some cryptographic technology as well. See
[the Bouncy Castle FAQ][bouncy-faq] for
more details on Bouncy Castle's cryptography features.
Distribution Statement
----------------------
DISTRIBUTION STATEMENT A. Approved for public release: distribution unlimited.
This material is based upon work supported by the Department of Defense under Air Force Contract No. FA8721-05-C-0002 and/or FA8702-15-D-0001. Any opinions, findings, conclusions or recommendations expressed in this material are those of the author(s) and do not necessarily reflect the views of the Department of Defense.
&copy; 2017 Massachusetts Institute of Technology.
The software/firmware is provided to you on an As-Is basis
Delivered to the U.S. Government with Unlimited Rights, as defined in DFARS Part 252.227-7013 or 7014 (Feb 2014). Notwithstanding any copyright notice, U.S. Government rights in this work are defined by DFARS 252.227-7013 or DFARS 252.227-7014 as detailed above.
License
-------
Copyright 2017 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
[pace]: https://github.com/mit-ll/pace
[accumulo]: https://accumulo.apache.org
[encryption]: ENCRYPTION.md
[signature]: SIGNATURE.md
[examples]: EXAMPLES.md
[key-management]: KEY_MANAGEMENT.md
[Maven]: https://maven.apache.org
[semver]: http://semver.org/spec/v2.0.0
[java-export]: http://www.oracle.com/us/products/export/export-regulations-345813.html
[bouncy-faq]: http://www.bouncycastle.org/wiki/display/JA1/Frequently+Asked+Questions
[apache]: http://www.apache.org/licenses/
[apilyzer]: https://github.com/revelc/apilyzer-maven-plugin
+173
View File
@@ -0,0 +1,173 @@
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
Integrity
---------
The signature code allows clients to sign data before storing it at the server.
This prevents data from being modified by the server administrator.
The signature code is designed as a drop-in replacement for Accumulo's
BatchWriter, BatchScanner, and Scanner classes. It can be configured
programmatically or through the use of configuration files (preferred).
Cryptographic keys are provided by the developer (see
[key management][key-management]).
API
---
Classes for signature functionality are found in
`edu.mit.ll.pace.signature`.
#### `SignedBatchWriter`
The signed batch writer is created similarly to a `BatchWriter`. After creation,
it is used identically to the Accumulo `BatchWriter`.
```java
BatchWriter writer = new SignedBatchWriter(
instance.getConnector(),
TABLE_NAME,
batchWriterConfig,
signatureConfig,
signatureKey);
```
#### `SignedBatchScanner`
The signed batch scanner is created similarly to a `BatchScanner`. After
creation, it is used identically to the Accumulo `BatchScanner`.
```java
BatchScanner scanner = new SignedBatchScanner(
instance.getConnector(),
TABLE_NAME,
authorizations,
numThreads,
signatureConfig,
verificationKeys);
```
#### `SignedScanner`
The signed batch scanner is created similarly to a `Scanner`. After creation,
it is used identically to the Accumulo `Scanner`.
```java
Scanner scanner = new SignedScanner(
instance.getConnector(),
TABLE_NAME,
authorizations,
signatureConfig,
verificationKeys);
```
Configuration
-------------
The configuration for the signature code is given by a `SignatureConfig` object.
This configuration object can be created programmatically or loaded from a
configuration file.
The configuration files specify the signature algorithm that will be used to
sign entries.
```ini
[Signature]
algorithm = {RSA-PKCS1,RSA-PSS,DSA,ECDSA}
provider = {SunJCE,BC,...}
```
Example config files can be found in the resources folder
(`edu.mit.ll.pace.signature`).
#### algorithm
Dictates the algorithm to use for signing and verifying records. The following
algorithms are available for signing and verifying data.
* **RSA-PKCS1**–RSA signature using the old PKCS #1 v1.5 padding.
* **RSA-PSS**–RSA signature using the more modern PSS padding scheme.
* **DSA**—DSA signature.
* **ECDSA**—ECDSA signature.
All algorithms sign the SHA256 hash of the data. BouncyCastle is required for
using RSA-PSS and DSA.
##### provider (Optional)
The Java cryptographic provider to use. If not set, will let the system decide
the appropriate cryptographic provider.
Compatibility Options (Use with Care)
-------------------------------------
By default, the signature is stored in the value field. When using the PACE
classes, this signature is verified and stripped before the value is returned.
In some cases, it may be necessary to sign data, while still allowing older,
non-PACE aware clients to continue accessing the data. In this case, we have
provided two alternative locations to store the signature.
##### Storing signatures in the columnVisibility field
The signature can be stored in the column visibility field by adding it as a
disjunctive clause (i.e., `(originalVisibility)|encodedSignature`). This is
done by adding the following two lines to the configuration file:
```ini
[Signature]
destination = colVis
defaultVisibility = ...
```
As with the default behavior, PACE clients will strip the signature from the
column visibility field before returning the data. Non-PACE aware clients will
see the signature, but will ignore it as it will be a visibility label for which
no user will have access.
The `defaultVisibility` option specifies what the default visibility field is
for the table where signatures will be stored. This is used when the entry being
signed has an empty visibility field.
**Limitations:**
1. Storing signatures in the column visibility field breaks versioning. As such,
it should only be used with tables that do not rely on Accumulo to version
entries.
2. The `SignedBatchWriter` is unable to delete entries when the signature is
stored in the column visibility field.
##### Storing signatures in a separate table
The signature can also be stored in a separate table. This is done by adding the
following two lines to the configuration file:
```ini
destination = table
table = ...
```
In this case, signatures are stored in a second, duplicate table, where the
values are replaced with signatures. The name of this second table is given in
the configuration.
**Limitations:**
1. This approach is much slower. First, it requires two tables to be scanned.
Second, if the data in the two tables is retrieved out of order
(i.e., batch scanning), an internal copy of all signatures that have not yet
been matched must be kept, increasing the memory requirements.
2. This option requires that both tables are versioned, and cannot be used if
versioning is disabled.
[key-management]: KEY_MANAGEMENT.md
+306
View File
@@ -0,0 +1,306 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<profiles version="12">
<profile kind="CodeFormatterProfile" name="integration" version="12">
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.disabling_tag" value="@formatter:off"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_block_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_field" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.use_on_off_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_ellipsis" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_multiple_fields" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_conditional_expression" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_binary_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_array_initializer" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_package" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_binary_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_package" value="0"/>
<setting id="org.eclipse.jdt.core.compiler.source" value="1.7"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_line_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.join_wrapped_lines" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_member_type" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.align_type_members_on_columns" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_parameter_description" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.lineSplit" value="160"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_method" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indentation.size" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.enabling_tag" value="@formatter:on"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_assignment" value="0"/>
<setting id="org.eclipse.jdt.core.compiler.problem.assertIdentifier" value="error"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.char" value="space"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_try_resources" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_body" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_method" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_method_declaration" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_switch" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_try" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.compiler.problem.enumIdentifier" value="error"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_ellipsis" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_method_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.compact_else_if" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_field" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_constant" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.comment.indent_root_tags" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.tabulation.size" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_empty_lines" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_block_in_case" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.compiler.compliance" value="1.7"/>
<setting id="org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer" value="2"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_unary_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_binary_expression" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_type" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while" value="do not insert"/>
<setting id="org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode" value="enabled"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_try" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_label" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_javadoc_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.comment.line_length" value="160"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_import_groups" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body" value="0"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.wrap_before_binary_operator" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_statements_compare_to_block" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.join_lines_in_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_compact_if" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_before_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_html" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_source_code" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.compiler.codegen.targetPlatform" value="1.7"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_resources_in_try" value="80"/>
<setting id="org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation" value="16"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_header" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.comment.format_block_comments" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.alignment_for_enum_constants" value="48"/>
<setting id="org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.brace_position_for_type_declaration" value="end_of_line"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.blank_lines_after_imports" value="1"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header" value="true"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for" value="insert"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_try_resources" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments" value="do not insert"/>
<setting id="org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column" value="false"/>
<setting id="org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line" value="false"/>
</profile>
</profiles>
+107
View File
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<templates>
<template autoinsert="true" context="gettercomment_context" deleted="false" description="Comment for getter method" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.gettercomment" name="gettercomment">/**
* @return the ${bare_field_name}
*/
</template>
<template autoinsert="true" context="settercomment_context" deleted="false" description="Comment for setter method" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.settercomment" name="settercomment">/**
* @param ${param} the ${bare_field_name} to set
*/
</template>
<template autoinsert="true" context="constructorcomment_context" deleted="false" description="Comment for created constructors" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.constructorcomment" name="constructorcomment">/**
* ${tags}
*/
</template>
<template autoinsert="false" context="filecomment_context" deleted="false" description="Comment for created Java files" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.filecomment" name="filecomment">/*&#13;
* Copyright 2016 MIT Lincoln Laboratory&#13;
*&#13;
* Licensed under the Apache License, Version 2.0 (the "License");&#13;
* you may not use this file except in compliance with the License.&#13;
* You may obtain a copy of the License at&#13;
* &#13;
* http://www.apache.org/licenses/LICENSE-2.0&#13;
* &#13;
* Unless required by applicable law or agreed to in writing, software&#13;
* distributed under the License is distributed on an "AS IS" BASIS,&#13;
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.&#13;
* See the License for the specific language governing permissions and&#13;
* limitations under the License.&#13;
*/
</template>
<template autoinsert="false" context="typecomment_context" deleted="false" description="Comment for created types" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.typecomment" name="typecomment">/**
* ${tags}
*/
</template>
<template autoinsert="false" context="fieldcomment_context" deleted="false" description="Comment for fields" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.fieldcomment" name="fieldcomment"/>
<template autoinsert="true" context="methodcomment_context" deleted="false" description="Comment for non-overriding methods" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.methodcomment" name="methodcomment">/**
* ${tags}
*/
</template>
<template autoinsert="false" context="overridecomment_context" deleted="false" description="Comment for overriding methods" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.overridecomment" name="overridecomment"/>
<template autoinsert="true" context="delegatecomment_context" deleted="false" description="Comment for delegate methods" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.delegatecomment" name="delegatecomment">/**
* ${tags}
* ${see_to_target}
*/
</template>
<template autoinsert="true" context="newtype_context" deleted="false" description="Newly created files" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.newtype" name="newtype">${filecomment}
${package_declaration}
${typecomment}
${type_declaration}
</template>
<template autoinsert="true" context="classbody_context" deleted="false" description="Code in new class type bodies" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.classbody" name="classbody">
</template>
<template autoinsert="true" context="interfacebody_context" deleted="false" description="Code in new interface type bodies" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.interfacebody" name="interfacebody">
</template>
<template autoinsert="true" context="enumbody_context" deleted="false" description="Code in new enum type bodies" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.enumbody" name="enumbody">
</template>
<template autoinsert="true" context="annotationbody_context" deleted="false" description="Code in new annotation type bodies" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.annotationbody" name="annotationbody">
</template>
<template autoinsert="true" context="catchblock_context" deleted="false" description="Code in new catch blocks" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.catchblock" name="catchblock">// ${todo} Auto-generated catch block
${exception_var}.printStackTrace();
</template>
<template autoinsert="true" context="methodbody_context" deleted="false" description="Code in created method stubs" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.methodbody" name="methodbody">// ${todo} Auto-generated method stub
${body_statement}
</template>
<template autoinsert="true" context="constructorbody_context" deleted="false" description="Code in created constructor stubs" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.constructorbody" name="constructorbody">${body_statement}
// ${todo} Auto-generated constructor stub
</template>
<template autoinsert="true" context="getterbody_context" deleted="false" description="Code in created getters" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.getterbody" name="getterbody">return ${field};
</template>
<template autoinsert="true" context="setterbody_context" deleted="false" description="Code in created setters" enabled="true"
id="org.eclipse.jdt.ui.text.codetemplates.setterbody" name="setterbody">${field} = ${param};
</template>
</templates>
+23
View File
@@ -0,0 +1,23 @@
# Copyright 2016 MIT Lincoln Laboratory
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
To generate a Findbugs report, invoke the following Maven command from the root
of the project:
`mvn clean package site site:stage`
Activating the package lifecycle phase is necessary as recompilation of the
classes is necessary to get instrumentation from Findbugs. This will ultimately
generate a local HTML site in `target/staging` which you can point your web
browser to. Each submodule of the project will contain its own Findbugs report.
+27
View File
@@ -0,0 +1,27 @@
# Copyright 2016 MIT Lincoln Laboratory
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Maven ignores
/target/
# IDE ignores
/.settings/
/.project
/.classpath
/.pydevproject
/.idea
/*.iml
/nbproject/
/nbactions.xml
/nb-configuration.xml
+132
View File
@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>edu.mit.ll.pace</groupId>
<artifactId>pace-project</artifactId>
<version>1.0-beta</version>
</parent>
<artifactId>pace-core</artifactId>
<name>Core</name>
<description>Core libraries.</description>
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>org.apache.accumulo</groupId>
<artifactId>accumulo-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.hadoop</groupId>
<artifactId>hadoop-common</artifactId>
</dependency>
<dependency>
<groupId>org.cryptomator</groupId>
<artifactId>siv-mode</artifactId>
</dependency>
<dependency>
<groupId>org.ini4j</groupId>
<artifactId>ini4j</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>net.revelc.code</groupId>
<artifactId>apilyzer-maven-plugin</artifactId>
<configuration>
<includes>
<include>edu[.]mit[.]ll[.]pace[.].*</include>
</includes>
<excludes>
<exclude>.*Impl</exclude>
<exclude>.*[.]impl[.].*</exclude>
<eclude>edu[.]mit[.]ll[.]pace[.]internal[.].*</eclude>
</excludes>
<allows>
<allow>org[.]apache[.]accumulo[.].*</allow>
<allow>org[.]apache[.]hadoop[.]io[.]Text</allow>
</allows>
</configuration>
<executions>
<execution>
<id>apilyzer</id>
<goals>
<goal>analyze</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.7</version>
<configuration>
<aggregate>true</aggregate>
<formats>
<format>xml</format>
<format>html</format>
</formats>
<instrumentation>
<excludes>
<exclude>edu/mit/ll/pace/IllegalKeyRequestException.class</exclude>
<exclude>edu/mit/ll/pace/encryption/EncryptionException.class</exclude>
<exclude>edu/mit/ll/pace/signature/SignatureException.class</exclude>
</excludes>
<ignoreTrivial>true</ignoreTrivial>
</instrumentation>
</configuration>
</plugin>
</plugins>
</build>
</project>
+46
View File
@@ -0,0 +1,46 @@
<!--
Copyright 2016 MIT Lincoln Laboratory
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<FindBugsFilter>
<Match>
<!-- The purpose of the method is to test a null argument -->
<Or>
<Class name="edu.mit.ll.pace.encryption.EncryptedBatchScannerTest"/>
<Class name="edu.mit.ll.pace.encryption.EntryEncryptorTest"/>
<Class name="edu.mit.ll.pace.signature.EntrySignerTest"/>
</Or>
<Bug code="NP" pattern="NP_NULL_PARAM_DEREF_ALL_TARGETS_DANGEROUS"/>
</Match>
<Match>
<!-- The purpose of the method is to test a null argument -->
<Or>
<Class name="edu.mit.ll.pace.encryption.EncryptedBatchScannerTest"/>
<Class name="edu.mit.ll.pace.encryption.EncryptedBatchWriterTest"/>
<Class name="edu.mit.ll.pace.encryption.EntryEncryptorTest"/>
<Class name="edu.mit.ll.pace.encryption.FieldEncryptorTest"/>
<Class name="edu.mit.ll.pace.signature.SignedBatchScannerTest"/>
<Class name="edu.mit.ll.pace.signature.SignedBatchWriterTest"/>
<Class name="edu.mit.ll.pace.signature.SignedScannerTest"/>
<Class name="edu.mit.ll.pace.signature.ValueSignerTest"/>
<Class name="edu.mit.ll.pace.internal.UtilsTest"/>
</Or>
<Bug code="NP" pattern="NP_NULL_PARAM_DEREF_NONVIRTUAL"/>
</Match>
<Match>
<!-- We are intending to search for an array on a single byte[] -->
<Class name="edu.mit.ll.pace.encryption.EntryEncryptorTest"/>
<Bug code="VA" pattern="VA_PRIMITIVE_ARRAY_PASSED_TO_OBJECT_VARARG"/>
</Match>
</FindBugsFilter>
@@ -0,0 +1,81 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace;
import java.util.Map;
import java.util.TreeMap;
/**
* Enumeration for the fields in an entry.
*/
public enum EntryField {
ROW("row"),
COLUMN_FAMILY("colFamily"),
COLUMN_QUALIFIER("colQualifier"),
COLUMN_VISIBILITY("colVisibility"),
TIMESTAMP("timestamp"),
DELETE("delete"),
VALUE("value");
/**
* Set of EntryField names mapped to the EntryField enum object. Used to speed up name lookups.
*/
private static final Map<String,EntryField> fields;
static {
fields = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (EntryField field : values()) {
fields.put(field.name, field);
}
}
/**
* A string representation of the EntryField.
*/
private final String name;
/**
* Create an EntryField with the given name.
*
* @param name
* Name of the entry field. Used in the configuration INI.
*/
EntryField(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
/**
* Get the entry from its {@link String} representation.
* <p>
* This method is paired with {@link #toString()}, and will parse strings generated by that method.
*
* @param value
* Value to parse.
* @return EntryField for the value.
*/
public static EntryField fromString(String value) {
EntryField field = fields.get(value);
if (field == null) {
throw new IllegalArgumentException("invalid name");
}
return field;
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace;
/**
* Exception for when a user lacks the permissions to request a key from the key infrastructure.
*/
public class IllegalKeyRequestException extends RuntimeException {
private static final long serialVersionUID = 6652704411535999467L;
/**
* Constructs a new illegal key request exception with null as its detail message.
*/
public IllegalKeyRequestException() {
super();
}
/**
* Constructs a new illegal key request exception with the specified detail message.
*
* @param message
* the detail message. The detail message is saved for later retrieval by the {@link Throwable#getMessage()} method.
*/
public IllegalKeyRequestException(String message) {
super(message);
}
/**
* Constructs a new illegal key request exception with the specified cause and a detail message of (cause==null ? null : cause.toString()) (which typically
* contains the class and detail message of cause). This constructor is useful for runtime exceptions that are little more than wrappers for other throwables.
*
* @param cause
* the cause (which is saved for later retrieval by the {@link Throwable#getCause()} method). (A null value is permitted, and indicates that the
* cause is nonexistent or unknown.)
*/
public IllegalKeyRequestException(Throwable cause) {
super(cause);
}
/**
* Constructs a new illegal key request exception with the specified detail message and cause.
* <p>
* Note that the detail message associated with cause is not automatically incorporated in this runtime exception's detail message.
*
* @param message
* the detail message. The detail message is saved for later retrieval by the {@link Throwable#getMessage()} method.
* @param cause
* the cause (which is saved for later retrieval by the {@link Throwable#getCause()} method). (A null value is permitted, and indicates that the
* cause is nonexistent or unknown.)
*/
public IllegalKeyRequestException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace;
import java.util.Iterator;
/**
* An iterator that iterates over items both post- and pre- processing.
* <p>
* More concretely, {@link #next()} returns the processed entry, and {@link #unprocessed()} returns the unprocessed entry that resulted in the value returned by
* {@link #next()}.
*
* @param <E>
* Type parameter.
*/
public interface ItemProcessingIterator<E> extends Iterator<E> {
/**
* Get the unprocessed element that returned the next item.
* <p>
* Requires that {@link #next()} has been successfully called.
*
* @return Returns the next item.
*/
E unprocessed();
}
@@ -0,0 +1,138 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.io.ByteArrayInputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.security.AlgorithmParameters;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.output.ByteArrayOutputStream;
import org.apache.hadoop.io.WritableUtils;
/**
* Defines the contract for a {@link ValueEncryptorBase} that performs semantic or authenticated encryption using AES.
*/
final class AESValueEncryptor extends ValueEncryptorBase {
/**
* The algorithm string to use when creating a key spec.
*/
private final static String AES = "AES";
/**
* The cipher to use for encryption.
*/
private final Cipher cipher;
/**
* Tracks whether this encryptor is a SunJCE provided instance of GCM. This is necessary as SunJCE uses "GCM" instead of "AES" for the algorithm name of this
* cipher.
*/
private final boolean isInstanceOfSunProvidedGCM;
/**
* Create the value encryptor.
*
* @param transformation
* ValueEncryptor transformation. Passed to {@link Cipher#getInstance(String, String)}.
* <p>
* The transformation defines the algorithm used, the mode it is used in, and any padding.
* @param provider
* ValueEncryptor provider. Passed to {@link Cipher#getInstance(String, String)}.
* @throws EncryptionException
* Thrown when the encryptor can't be instantiated.
*/
AESValueEncryptor(String transformation, String provider) {
try {
if (provider == null) {
cipher = Cipher.getInstance(transformation);
} else {
cipher = Cipher.getInstance(transformation, provider);
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | NoSuchProviderException e) {
throw new EncryptionException(e);
}
isInstanceOfSunProvidedGCM = transformation.toUpperCase().startsWith("AES/GCM") && cipher.getProvider().getName().equals("SunJCE");
}
@Override
byte[] encrypt(byte[] key, byte[] data) {
try {
SecretKeySpec keySpec = new SecretKeySpec(key, AES);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] ciphertext = cipher.doFinal(data);
// Write out the metadata.
ByteArrayOutputStream stream = new ByteArrayOutputStream();
DataOutput out = new DataOutputStream(stream);
byte[] params = cipher.getParameters().getEncoded();
WritableUtils.writeVInt(out, params.length);
out.write(params);
// Write the original ciphertext and return the new ciphertext.
out.write(ciphertext);
return stream.toByteArray();
} catch (InvalidKeyException | IllegalBlockSizeException | BadPaddingException | IOException e) {
throw new EncryptionException(e);
}
}
@Override
byte[] decrypt(byte[] key, byte[] data) {
try {
// Sun's impelemntation of GCM uses a custom algorithm name. We handle this odd (potentially incorrect) behavior here.
AlgorithmParameters params = AlgorithmParameters.getInstance(isInstanceOfSunProvidedGCM ? "GCM" : "AES");
// Read the metadata.
ByteArrayInputStream stream = new ByteArrayInputStream(data);
DataInput in = new DataInputStream(stream);
int metadataLength = WritableUtils.readVInt(in);
byte[] metadata = new byte[metadataLength];
in.readFully(metadata);
params.init(metadata);
// Decrypt the remaining data.
SecretKeySpec keySpec = new SecretKeySpec(key, AES);
cipher.init(Cipher.DECRYPT_MODE, keySpec, params);
byte[] ciphertext = new byte[stream.available()];
in.readFully(ciphertext);
cipher.init(Cipher.DECRYPT_MODE, keySpec, params);
return cipher.doFinal(ciphertext);
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException
| IOException e) {
throw new EncryptionException(e);
}
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.util.Arrays;
import javax.crypto.IllegalBlockSizeException;
import org.cryptomator.siv.SivMode;
import org.cryptomator.siv.UnauthenticCiphertextException;
/**
* Defines the contract for classes that supports symmetric encryption and decryption of data.
*/
final class DeterministicSIVValueEncryptor extends ValueEncryptorBase {
/**
* SIV cipher to use.
*/
private final SivMode siv;
/**
* Creates an SIV encryptor to use for deterministic encryption.
*/
DeterministicSIVValueEncryptor() {
siv = new SivMode();
}
@Override
byte[] encrypt(byte[] key, byte[] data) {
// The key passed here contains both the AES and IV generation keys.
byte[] encKey = Arrays.copyOfRange(key, 0, key.length / 2);
byte[] macKey = Arrays.copyOfRange(key, key.length / 2, key.length);
return siv.encrypt(encKey, macKey, data);
}
@Override
byte[] decrypt(byte[] key, byte[] data) {
// The key passed here contains both the AES and IV generation keys.
byte[] encKey = Arrays.copyOfRange(key, 0, key.length / 2);
byte[] macKey = Arrays.copyOfRange(key, key.length / 2, key.length);
try {
return siv.decrypt(encKey, macKey, data);
} catch (IllegalBlockSizeException | UnauthenticCiphertextException e) {
throw new EncryptionException(e);
}
}
}
@@ -0,0 +1,289 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.client.BatchScanner;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.sample.SamplerConfiguration;
import org.apache.accumulo.core.data.Column;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.accumulo.core.util.TextUtil;
import org.apache.hadoop.io.Text;
import edu.mit.ll.pace.ItemProcessingIterator;
import edu.mit.ll.pace.encryption.EntryEncryptor.ColumnFilterResult;
/**
* Reads encrypted entries from Accumulo. As is the case for all {@link BatchScanner} instances, data is not guaranteed to be sorted.
* <p>
* It should be noted that while server side scan iterators can be set from this scanner ({@link #addScanIterator(IteratorSetting)},
* {@link #clearScanIterators()}, {@link #removeScanIterator(String)}, and {@link #updateScanIteratorOption(String, String, String)}), the data will not be
* decrypted on the server side. As such these server side will only be able to operate on unencrypted data and ciphertext, not decrypted plaintext.
*/
public final class EncryptedBatchScanner implements BatchScanner {
/**
* The underlying {@link BatchScanner} used to retrieve data from Accumulo.
*/
private final BatchScanner scanner;
/**
* The {@link EntryEncryptor} that will handle decryption for this instance.
*/
private final EntryEncryptor encryptor;
/**
* The set of ranges that an entry must match to be returned.
* <p>
* These are handled client side, as it was not possible to push this filtering to the server.
*/
private List<Range> clientSideRanges = new ArrayList<>();
/**
* The set of columns that an entry must match to be returned.
* <p>
* These are handled client side, as it was not possible to push this filtering to the server.
*/
private SortedSet<Column> clientSideColumnFilters = new TreeSet<>();
/**
* Create an encrypted batch scanner.
*
* @param connector
* The connector for the Accumulo instance.
* @param tableName
* Name of the table to write to.
* @param authorizations
* The authorizations this user has for querying Accumulo.
* @param numQueryThreads
* Maximimum number of query threads to use for this scanner.
* @param cryptoConfig
* Configuration for the decryption.
* @param keys
* Container with the keys to use for decryption.
* @throws TableNotFoundException
* Thrown if the table name is not found in the Accumulo instance.
*/
public EncryptedBatchScanner(Connector connector, String tableName, Authorizations authorizations, int numQueryThreads, EncryptionConfig cryptoConfig,
EncryptionKeyContainer keys) throws TableNotFoundException {
checkArgument(connector != null, "connection is null");
checkArgument(tableName != null, "tableName is null");
checkArgument(authorizations != null, "authorizations is null");
checkArgument(cryptoConfig != null, "config is null");
checkArgument(keys != null, "keys is null");
this.scanner = connector.createBatchScanner(tableName, authorizations, numQueryThreads);
this.encryptor = new EntryEncryptor(cryptoConfig, keys);
}
/**
* {@inheritDoc}
* <p>
* Automatically creates a set of server and client side ranges that most efficiently filter for the given ranges.
*
* @throws EncryptionException
* The reason for the failure can be retrieved by calling {@link EncryptionException#getCause()}.
*/
@Override
public void setRanges(Collection<Range> collection) {
checkArgument(collection != null && collection.size() > 0, "ranges must be non null and contain at least 1 range");
List<Range> serverSideRanges = new ArrayList<>();
clientSideRanges = new ArrayList<>();
// Transform the ranges as needed to deal with different encryption configurations.
for (Range range : collection) {
if (encryptor.transformRange(range, serverSideRanges)) {
clientSideRanges.add(range);
}
}
scanner.setRanges(serverSideRanges);
}
/**
* {@inheritDoc}
* <p>
* Handles encryption of the search values as needed, filtering for the given column family on the server or client side as appropriate.
*
* @throws EncryptionException
* The reason for the failure can be retrieved by calling {@link EncryptionException#getCause()}.
*/
@Override
public void fetchColumnFamily(Text col) {
checkArgument(col != null, "col is null");
ColumnFilterResult search = encryptor.getColumnFamilyFilter(col);
for (Column filter : search.serverSideFilters) {
scanner.fetchColumnFamily(new Text(filter.getColumnFamily()));
}
if (search.needsClientSideFiltering) {
clientSideColumnFilters.add(new Column(TextUtil.getBytes(col), null, null));
}
}
/**
* {@inheritDoc}
* <p>
* Handles encryption of the search values as needed, filtering for the given column on the server or client side as appropriate.
*
* @throws EncryptionException
* The reason for the failure can be retrieved by calling {@link EncryptionException#getCause()}.
*/
@Override
public void fetchColumn(Text colFam, Text colQual) {
checkArgument(colFam != null, "colFam is null");
checkArgument(colQual != null, "colQual is null");
ColumnFilterResult search = encryptor.getColumnFilter(colFam, colQual);
for (Column column : search.serverSideFilters) {
if (column.getColumnQualifier() == null) {
scanner.fetchColumnFamily(new Text(column.getColumnFamily()));
} else {
scanner.fetchColumn(new Text(column.getColumnFamily()), new Text(column.getColumnQualifier()));
}
}
if (search.needsClientSideFiltering) {
clientSideColumnFilters.add(new Column(TextUtil.getBytes(colFam), TextUtil.getBytes(colQual), null));
}
}
/**
* {@inheritDoc}
*
* @throws EncryptionException
* The reason for the failure can be retrieved by calling {@link EncryptionException#getCause()}.
*/
@Override
public void fetchColumn(IteratorSetting.Column column) {
checkArgument(column != null, "Column is null");
fetchColumn(column.getColumnFamily(), column.getColumnQualifier());
}
@Override
public void clearColumns() {
scanner.clearColumns();
clientSideColumnFilters.clear();
}
/**
* {@inheritDoc}
* <p>
* Returns a {@link edu.mit.ll.pace.ItemProcessingIterator}. This can be used to get the unprocessed items, allowing them to be deleted by a regular accumulo
* writer.
*/
@Override
public ItemProcessingIterator<Entry<Key,Value>> iterator() {
return new EncryptedScannerIterator(scanner.iterator(), encryptor, clientSideRanges, clientSideColumnFilters);
}
/* What remains are simply wrappers around BatchScanner. */
@Override
public void clearScanIterators() {
scanner.clearScanIterators();
}
@Override
public void close() {
scanner.close();
}
@Override
public Authorizations getAuthorizations() {
return scanner.getAuthorizations();
}
@Override
public void setSamplerConfiguration(SamplerConfiguration samplerConfiguration) {
scanner.setSamplerConfiguration(samplerConfiguration);
}
@Override
public SamplerConfiguration getSamplerConfiguration() {
return scanner.getSamplerConfiguration();
}
@Override
public void clearSamplerConfiguration() {
scanner.clearSamplerConfiguration();
}
@Override
public void setBatchTimeout(long l, TimeUnit timeUnit) {
scanner.setBatchTimeout(l, timeUnit);
}
@Override
public long getBatchTimeout(TimeUnit timeUnit) {
return scanner.getBatchTimeout(timeUnit);
}
@Override
public void setClassLoaderContext(String s) {
scanner.setClassLoaderContext(s);
}
@Override
public void clearClassLoaderContext() {
scanner.clearClassLoaderContext();
}
@Override
public String getClassLoaderContext() {
return scanner.getClassLoaderContext();
}
@Override
public void addScanIterator(IteratorSetting iteratorSetting) {
scanner.addScanIterator(iteratorSetting);
}
@Override
public void removeScanIterator(String s) {
scanner.removeScanIterator(s);
}
@Override
public void updateScanIteratorOption(String s, String s1, String s2) {
scanner.updateScanIteratorOption(s, s1, s2);
}
@Override
public void setTimeout(long l, TimeUnit timeUnit) {
scanner.setTimeout(l, timeUnit);
}
@Override
public long getTimeout(TimeUnit timeUnit) {
return scanner.getTimeout(timeUnit);
}
}
@@ -0,0 +1,141 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static com.google.common.base.Preconditions.checkArgument;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.MutationsRejectedException;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.data.ColumnUpdate;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.security.ColumnVisibility;
import edu.mit.ll.pace.internal.MutableEntry;
/**
* Writes encrypted entries to Accumulo.
*/
public final class EncryptedBatchWriter implements BatchWriter {
/**
* A {@link BatchWriter} to use to write the data to Accumulo.
*/
private final BatchWriter writer;
/**
* The {@link EntryEncryptor} that will handle encryption for this instance.
*/
private final EntryEncryptor encryptor;
/**
* Tracks whether entries written by this encrypted batch writer can be updated (i.e., deleted or changed).
*/
private final boolean supportsDelete;
/**
* Create an encrypted batch writer.
*
* @param connector
* The connector for the Accumulo instance.
* @param tableName
* Name of the table to write to.
* @param batchConfig
* Configuration for a {@link BatchWriter}.
* @param cryptoConfig
* Configuration for the encryption.
* @param keys
* Container with the keys to use for encryption.
* @throws TableNotFoundException
* Thrown if the table name is not found in the Accumulo instance.
*/
public EncryptedBatchWriter(Connector connector, String tableName, BatchWriterConfig batchConfig, EncryptionConfig cryptoConfig, EncryptionKeyContainer keys)
throws TableNotFoundException {
checkArgument(connector != null, "connector is null");
checkArgument(tableName != null, "tableName is null");
checkArgument(cryptoConfig != null, "config is null");
checkArgument(keys != null, "keys is null");
this.writer = connector.createBatchWriter(tableName, batchConfig);
this.encryptor = new EntryEncryptor(cryptoConfig, keys);
this.supportsDelete = this.encryptor.canBeDeleteServerSide();
}
/**
* Encrypt the given mutation and then write it to Accumulo.
*
* @param mutation
* The mutation to encrypt.
* @throws EncryptionException
* The reason for the failure can be retrieved by calling {@link EncryptionException#getCause()}.
*/
@Override
public void addMutation(Mutation mutation) throws MutationsRejectedException {
for (ColumnUpdate update : mutation.getUpdates()) {
MutableEntry entry = new MutableEntry(mutation.getRow(), update);
if (update.isDeleted()) {
if (!supportsDelete) {
throw new IllegalArgumentException("cannot delete entries when there are fields encrypted using non-deterministic encryption");
}
// Many keys might need to be deleted, one for each combination of key versions.
for (Key deleteKey : encryptor.getDeleteKeys(entry.toKey())) {
MutableEntry encryptedKey = new MutableEntry(deleteKey);
Mutation encryptedMutation = new Mutation(encryptedKey.row);
if (update.hasTimestamp()) {
encryptedMutation.putDelete(encryptedKey.colF, encryptedKey.colQ, new ColumnVisibility(encryptedKey.colVis), encryptedKey.timestamp);
} else {
encryptedMutation.putDelete(encryptedKey.colF, encryptedKey.colQ, new ColumnVisibility(encryptedKey.colVis));
}
writer.addMutation(encryptedMutation);
}
} else {
MutableEntry encryptedEntry = new MutableEntry(encryptor.encrypt(new MutableEntry(mutation.getRow(), update).toEntry()));
Mutation encryptedMutation = new Mutation(encryptedEntry.row);
if (update.hasTimestamp()) {
encryptedMutation.put(encryptedEntry.colF, encryptedEntry.colQ, new ColumnVisibility(encryptedEntry.colVis), encryptedEntry.timestamp,
encryptedEntry.value);
} else {
encryptedMutation.put(encryptedEntry.colF, encryptedEntry.colQ, new ColumnVisibility(encryptedEntry.colVis), encryptedEntry.value);
}
writer.addMutation(encryptedMutation);
}
}
}
@Override
public void addMutations(Iterable<Mutation> iterable) throws MutationsRejectedException {
for (Mutation mutation : iterable) {
addMutation(mutation);
}
}
@Override
public void flush() throws MutationsRejectedException {
writer.flush();
}
@Override
public void close() throws MutationsRejectedException {
writer.close();
}
}
@@ -0,0 +1,175 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.SortedSet;
import org.apache.accumulo.core.data.Column;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import edu.mit.ll.pace.ItemProcessingIterator;
/**
* Reads encrypted entries from Accumulo.
*/
final class EncryptedScannerIterator implements ItemProcessingIterator<Entry<Key,Value>> {
/**
* The underlying iterator that contains the data that will be decrypted.
*/
private final Iterator<Entry<Key,Value>> iterator;
/**
* The {@link EntryEncryptor} to use to decrypt values.
*/
private final EntryEncryptor encryptor;
/**
* The set of ranges that an entry must match to be returned.
* <p>
* These are handled client side, as it was not possible to push this filtering to the server.
*/
private final List<Range> clientSideRanges;
/**
* The set of columns that an entry must match to be returned.
* <p>
* These are handled client side, as it was not possible to push this filtering to the server.
*/
private final SortedSet<Column> clientSideColumnFilters;
/**
* Used to track the next element to return. Needed as the underlying iterator's {@link Iterator#hasNext()} doesn't match this iterator's {@link #hasNext()}.
*/
private Entry<Key,Value> next = null;
/**
* Keep track of the encrypted entry that resulted in
*/
private Entry<Key,Value> unprocessedEntry = null;
/**
* Wrap the given iterator, decrypting and filtering entries as appropriate.
*
* @param iterator
* The underlying iterator that contains the data that will be decrypted.
* @param encryptor
* The {@link EntryEncryptor} to use to decrypt values.
* @param clientSideRanges
* The set of ranges that an entry must match to be returned.
* @param clientSideColumnFilters
* The set of columns that an entry must match to be returned.
*/
EncryptedScannerIterator(Iterator<Entry<Key,Value>> iterator, EntryEncryptor encryptor, List<Range> clientSideRanges,
SortedSet<Column> clientSideColumnFilters) {
this.iterator = iterator;
this.encryptor = encryptor;
this.clientSideRanges = clientSideRanges;
this.clientSideColumnFilters = clientSideColumnFilters;
}
@Override
public boolean hasNext() {
if (next == null) {
advance();
}
return next != null;
}
@Override
public Entry<Key,Value> next() {
if (next == null) {
advance();
}
if (next == null) {
throw new NoSuchElementException();
}
// Clear next, as it has now been consumed.
Entry<Key,Value> value = next;
next = null;
return value;
}
/**
* Advance to the next entry that can be decrypted. This element is stored in next. If no more entries are left, next is set to null.
*/
private void advance() {
next = null;
unprocessedEntry = null;
while (iterator.hasNext()) {
Entry<Key,Value> original = iterator.next();
Entry<Key,Value> entry = encryptor.decrypt(original);
// Check to see if the decrypted entry matches one of the set clientSideRanges.
if (clientSideRanges.size() > 0) {
boolean matches = false;
for (Range range : clientSideRanges) {
if (range.contains(entry.getKey())) {
matches = true;
break;
}
}
if (!matches) {
continue;
}
}
// Check to see if the decrypted entry matches one of the filtered columns.
if (clientSideColumnFilters.size() > 0) {
boolean matches = false;
for (Column column : clientSideColumnFilters) {
// Column family always set and must be checked. Column qualifier may not be set.
if (Arrays.equals(column.getColumnFamily(), entry.getKey().getColumnFamilyData().getBackingArray())
&& (column.getColumnQualifier() == null || Arrays.equals(column.getColumnQualifier(), entry.getKey().getColumnQualifierData().getBackingArray()))) {
matches = true;
break;
}
}
if (!matches) {
continue;
}
}
// We've found the next entry and can end the loop.
next = entry;
unprocessedEntry = original;
break;
}
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public Entry<Key,Value> unprocessed() {
if (next != null || unprocessedEntry == null) {
throw new NoSuchElementException();
}
return unprocessedEntry;
}
}
@@ -0,0 +1,94 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static com.google.common.base.Preconditions.checkArgument;
import java.io.IOException;
import java.io.Writer;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.ini4j.Ini;
import edu.mit.ll.pace.EntryField;
/**
* Holds configuration data for encrypting Accumulo entries.
*
* <p>
* The actual configuration is a list of {@link FieldEncryptorConfig} objects, that specify how data will be encrypted and written to a given field. As having
* two {@link FieldEncryptorConfig} with the same destination field would lead to conflicts, only a single {@link FieldEncryptorConfig} is allowed for a
* destination field.
*/
public final class EncryptionConfig {
/**
* Read-only of {@link FieldEncryptorConfig} that define how encryption of an entry will happen.
*/
final List<FieldEncryptorConfig> fieldEncryptorConfigs;
/**
* Creates an EncryptionConfig from a set of field encryptor configurations.
*
* @param fieldEncryptorConfigs
* The encryptor configs that define how encryption of an entry will happen.
*/
EncryptionConfig(List<FieldEncryptorConfig> fieldEncryptorConfigs) {
checkArgument(fieldEncryptorConfigs != null, "fieldEncryptorConfigs is null");
checkArgument(fieldEncryptorConfigs.size() != 0, "fieldEncryptorConfigs is empty");
// Scan for duplicate destinations.
Set<EntryField> destinations = new HashSet<>();
Set<EntryField> sources = new HashSet<>();
for (FieldEncryptorConfig config : fieldEncryptorConfigs) {
checkArgument(!destinations.contains(config.destination), "duplicate destination in fieldEncryptorConfigs");
destinations.add(config.destination);
sources.addAll(config.sources);
}
// Scan for duplicate
for (EntryField destination : destinations) {
checkArgument(sources.contains(destination), destination.toString() + " was a destination, but it was not a source.");
}
/*
* TODO: Behavior with duplicate sources is undefined. This could lead to problems on decrypt. Consider the following situations: Encrypt with config:
* colFamily = E(row) Update table: row = "new" Encrypt with config: colQualifier = E(row) Decrypt with config: row = D(colFamily), row = D(colQualifier) In
* this case, row will have different values encrypted into it. This can only arise if different configs are allowed on a single table, but right now there
* is nothing that stops that.
*/
this.fieldEncryptorConfigs = Collections.unmodifiableList(fieldEncryptorConfigs);
}
/**
* Write the object to a Writer.
*
* @param out
* Stream to write object out to.
*/
public void write(Writer out) throws IOException {
Ini configIni = new Ini();
for (FieldEncryptorConfig config : fieldEncryptorConfigs) {
config.write(configIni);
}
configIni.store(out);
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.ini4j.Ini;
import org.ini4j.Profile.Section;
/**
* Builder for configuration of entry encryptor objects.
*/
public class EncryptionConfigBuilder {
// For definitions of these values see FieldEncryptorConfig.
private List<FieldEncryptorConfig> fieldEncryptorConfigs = new ArrayList<>();
/**
* Add a field encryptor config.
*
* @param fieldEncryptorConfig
* Field encryptor config to add.
* @return Builder.
*/
public EncryptionConfigBuilder addFieldEncryptorConfig(FieldEncryptorConfig fieldEncryptorConfig) {
this.fieldEncryptorConfigs.add(fieldEncryptorConfig);
return this;
}
/**
* Set the field encryptor configs.
*
* @param fieldEncryptorConfigs
* Field encryptor config to use.
* @return Builder.
*/
public EncryptionConfigBuilder setFieldEncryptorConfigs(Collection<FieldEncryptorConfig> fieldEncryptorConfigs) {
this.fieldEncryptorConfigs.clear();
if (fieldEncryptorConfigs != null) {
this.fieldEncryptorConfigs.addAll(fieldEncryptorConfigs);
}
return this;
}
/**
* Read the configuration from a Reader.
*
* @param in
* Stream to read from.
* @return Builder.
*/
public EncryptionConfigBuilder readFromFile(Reader in) throws IOException {
Ini configIni = new Ini(in);
for (Section section : configIni.values()) {
addFieldEncryptorConfig(new FieldEncryptorConfigBuilder().readFromIni(section).build());
}
return this;
}
/**
* Build the {@link EncryptionConfig}.
*
* @return Built config.
*/
public EncryptionConfig build() {
return new EncryptionConfig(fieldEncryptorConfigs);
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
/**
* Runtime wrapper for exceptions that occur during encryption.
*/
public class EncryptionException extends RuntimeException {
private static final long serialVersionUID = -6473669625720101487L;
/**
* Constructs a new encryption exception with null as its detail message.
*/
public EncryptionException() {
super();
}
/**
* Constructs a new encryption exception with the specified detail message.
*
* @param message
* the detail message. The detail message is saved for later retrieval by the {@link Throwable#getMessage()} method.
*/
public EncryptionException(String message) {
super(message);
}
/**
* Constructs a new encryption exception with the specified cause and a detail message of (cause==null ? null : cause.toString()) (which typically contains
* the class and detail message of cause). This constructor is useful for runtime exceptions that are little more than wrappers for other throwables.
*
* @param cause
* the cause (which is saved for later retrieval by the {@link Throwable#getCause()} method). (A null value is permitted, and indicates that the
* cause is nonexistent or unknown.)
*/
public EncryptionException(Throwable cause) {
super(cause);
}
/**
* Constructs a new encryption exception with the specified detail message and cause.
* <p>
* Note that the detail message associated with cause is not automatically incorporated in this runtime exception's detail message.
*
* @param message
* the detail message. The detail message is saved for later retrieval by the {@link Throwable#getMessage()} method.
* @param cause
* the cause (which is saved for later retrieval by the {@link Throwable#getCause()} method). (A null value is permitted, and indicates that the
* cause is nonexistent or unknown.)
*/
public EncryptionException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,151 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Arrays;
import java.util.Collection;
import java.util.Objects;
import edu.mit.ll.pace.IllegalKeyRequestException;
/**
* Interface defining operations that must be supported by a key container for it to work with this module.
*/
public interface EncryptionKeyContainer {
/**
* Key with its version.
*/
final class KeyWithVersion {
/**
* Encryption key.
*/
public final byte[] key;
/**
* Version of the key.
*/
public final int version;
/**
* Key with its version.
*
* @param key
* Key.
* @param version
* Version.
*/
public KeyWithVersion(byte[] key, int version) {
checkArgument(key != null, "key is null");
checkArgument(key.length != 0, "key is empty");
checkArgument(version >= 0, "version is negative");
this.key = key;
this.version = version;
}
@Override
public int hashCode() {
return Objects.hash(key, version);
}
@Override
public boolean equals(Object obj) {
if (null == obj || !(obj instanceof KeyWithVersion)) {
return false;
}
KeyWithVersion other = (KeyWithVersion) obj;
return Arrays.equals(key, other.key) && version == other.version;
}
}
/**
* Get all the versioned keys for the given id.
*
* @param id
* Id of the key to retrieve.
* @param length
* Length of the key to return in bits.
* @return Requested key of the desired length and the version of that key.
* @throws IllegalKeyRequestException
* User lacks the permissions to obtain the desired key.
*/
Collection<KeyWithVersion> getKeys(String id, int length) throws IllegalKeyRequestException;
/**
* Get the key for the given id.
*
* @param id
* Id of the key to retrieve.
* @param length
* Length of the key to return in bits.
* @return Requested key of the desired length and the version of that key.
* @throws IllegalKeyRequestException
* User lacks the permissions to obtain the desired key.
*/
KeyWithVersion getKey(String id, int length) throws IllegalKeyRequestException;
/**
* Get the key for the given id and version.
*
* @param id
* Id of the key to retrieve.
* @param version
* Version of the key to retrieve.
* @param length
* Length of the key to return in bits.
* @return byte[] with requested key.
* @throws IllegalKeyRequestException
* User lacks the permissions to obtain the desired key.
*/
byte[] getKey(String id, int version, int length) throws IllegalKeyRequestException;
/**
* Get the encryption key for the given attribute.
*
* @param attribute
* Name of the attribute whose key is being retrieved.
* @param id
* Id of the key to retrieve.
* @param length
* Length of the key to return in bits.
* @return Requested encryption key of the desired length and the version of those keys.
* @throws IllegalKeyRequestException
* User lacks the permissions to obtain the desired key.
*/
KeyWithVersion getAttributeKey(String attribute, String id, int length) throws IllegalKeyRequestException;
/**
* Get the encryption key for the given attribute.
*
* @param attribute
* Name of the attribute whose key is being retrieved.
* @param id
* Id of the key to retrieve.
* @param version
* Version of the key to retrieve.
* @param length
* Length of the key to return in bits.
* @return byte[] with the requested encryption key.
* @throws IllegalKeyRequestException
* User lacks the permissions to obtain the desired key.
*/
byte[] getAttributeKey(String attribute, String id, int version, int length) throws IllegalKeyRequestException;
}
@@ -0,0 +1,554 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static com.google.common.base.Preconditions.checkArgument;
import static edu.mit.ll.pace.internal.Utils.EMPTY;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.accumulo.core.data.Column;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.PartialKey;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.ColumnVisibility;
import org.apache.accumulo.core.util.TextUtil;
import org.apache.hadoop.io.Text;
import com.google.common.collect.ImmutableSet;
import edu.mit.ll.pace.EntryField;
import edu.mit.ll.pace.internal.MutableEntry;
/**
* Encrypts an Accumulo {@literal Entry<Key,Value>} based on the supplied configuration.
*/
public final class EntryEncryptor {
/**
* Result of encrypted filter operation.
*/
public static final class ColumnFilterResult {
/**
* Server side filters.
*/
public final Collection<Column> serverSideFilters;
/**
* Whether filtering also needs to happen client side.
*/
public final boolean needsClientSideFiltering;
/**
* Constructor.
*
* @param serverSideFilters
* Server side filters.
* @param needsClientSideFiltering
* Whether filtering also needs to happen client side.
*/
private ColumnFilterResult(Collection<Column> serverSideFilters, boolean needsClientSideFiltering) {
this.serverSideFilters = serverSideFilters;
this.needsClientSideFiltering = needsClientSideFiltering;
}
}
/**
* A list of the {@link FieldEncryptor} objects that make up this entry encryptor.
*/
private final List<FieldEncryptor> encryptors;
/**
* A map of destination fields to their respective {@link FieldEncryptor}.
*/
private final Map<EntryField,FieldEncryptor> destinationMap;
/**
* A set of the fields used as sources by this encryptor.
*/
private final Set<EntryField> sources;
/**
* Create an entry encryptor.
*
* @param config
* Configuration for the encryption.
* @param keys
* Container with the keys to use for encryption.
*/
public EntryEncryptor(EncryptionConfig config, EncryptionKeyContainer keys) {
checkArgument(config != null, "config is null");
checkArgument(keys != null, "keys is null");
// Create the encryptors and their reverse-lookup.
encryptors = new ArrayList<>(config.fieldEncryptorConfigs.size());
destinationMap = new HashMap<>(config.fieldEncryptorConfigs.size());
sources = new HashSet<>();
for (FieldEncryptorConfig fieldEncryptorConfig : config.fieldEncryptorConfigs) {
FieldEncryptor fieldEncryptor = new FieldEncryptor(fieldEncryptorConfig, keys);
encryptors.add(fieldEncryptor);
destinationMap.put(fieldEncryptorConfig.destination, fieldEncryptor);
sources.addAll(fieldEncryptorConfig.sources);
}
}
/**
* Encrypt the given entry.
*
* @param entry
* Entry to encrypt.
* @return Encrypted entry.
*/
public Entry<Key,Value> encrypt(Entry<Key,Value> entry) {
checkArgument(entry != null, "entry is null");
MutableEntry wrapped = new MutableEntry(entry);
MutableEntry result = new MutableEntry(entry);
ColumnVisibility visibility = entry.getKey().getColumnVisibilityParsed();
// Remove source fields that are encrypted but not replaced with encrypted data.
// Ignore column visibility, timestamp, and delete as these are needed for correct parsing by Accumulo.
for (EntryField source : sources) {
switch (source) {
case ROW:
case COLUMN_FAMILY:
case COLUMN_QUALIFIER:
case VALUE:
result.setBytes(source, EMPTY);
break;
}
}
// Encrypt the various fields.
try {
for (FieldEncryptor fieldEncryptor : encryptors) {
fieldEncryptor.encrypt(wrapped, result, visibility);
}
} catch (IOException e) { // IO exceptions won't be thrown in practice as we are operating on in-memory streams.
throw new EncryptionException(e);
}
return result.toEntry();
}
/**
* Decrypt the given entry.
*
* @param entry
* entry to decrypt.
* @return Decrypted entry.
*/
public Entry<Key,Value> decrypt(Entry<Key,Value> entry) {
checkArgument(entry != null, "entry is null");
MutableEntry wrapped = new MutableEntry(entry);
MutableEntry result = new MutableEntry(entry);
ColumnVisibility visibility = entry.getKey().getColumnVisibilityParsed();
// Decrypt the various fields.
try {
for (FieldEncryptor fieldEncryptor : encryptors) {
fieldEncryptor.decrypt(wrapped, result, visibility);
}
} catch (IOException e) { // IO exceptions won't be thrown in practice as we are operating on in-memory streams.
throw new EncryptionException(e);
}
return result.toEntry();
}
/**
* Checks whether this entry encryptor results in an encrypted key that is searchable server-side.
*
* @return Whether entried can be updated.
*/
public boolean canBeDeleteServerSide() {
for (EntryField field : FieldEncryptorConfig.KEY_DESTINATION_FIELDS) {
if (destinationMap.containsKey(field) && !destinationMap.get(field).canBeFilteredServerSide(FieldEncryptorConfig.KEY_SOURCE_FIELDS)) {
return false;
}
}
return true;
}
/**
* Given a key for deletion, generate the appropriate server side keys to delete.
*
* @param key
* Key to delete.
* @return Set of encrypted keys to delete.
*/
public Collection<Key> getDeleteKeys(Key key) {
checkArgument(canBeDeleteServerSide(), "canBeDeleteServerSide is false");
checkArgument(key != null, "key is null");
MutableEntry searchKey = new MutableEntry(key);
// Pair up the correct answers.
Collection<byte[]> rowValues = getDeleteValues(searchKey, EntryField.ROW);
Collection<byte[]> colFValues = getDeleteValues(searchKey, EntryField.COLUMN_FAMILY);
Collection<byte[]> colQValues = getDeleteValues(searchKey, EntryField.COLUMN_QUALIFIER);
return rowValues
.stream()
.flatMap(
r -> colFValues.stream().flatMap(
cf -> colQValues.stream().map(cq -> new Key(r, cf, cq, key.getColumnVisibilityData().getBackingArray(), key.getTimestamp(), true, true))))
.collect(Collectors.toList());
}
/**
* Get the delete values for the given field.
*
* @param key
* Key to pull values from.
* @param field
* Field to get delete values for.
* @return Values that needed to be deleted server side to delete the given key.
*/
private Collection<byte[]> getDeleteValues(MutableEntry key, EntryField field) {
if (!destinationMap.containsKey(field)) {
if (!sources.contains(field)) {
return Collections.singletonList(key.getBytes(field));
} else {
return Collections.singletonList(EMPTY);
}
} else {
return destinationMap.get(field).getServerSideFilterValues(key, false);
}
}
/**
* Get the filter for the given value.
*
* @param col
* Column family to filter.
* @return Results with columns to filter server ide and whether client side filtering is needed.
*/
public ColumnFilterResult getColumnFamilyFilter(Text col) {
checkArgument(col != null, "col is null");
MutableEntry key = new MutableEntry();
key.colF = TextUtil.getBytes(col);
return getColumnFamilyFilter(key, ImmutableSet.of(EntryField.COLUMN_FAMILY));
}
/**
* Get the filter for the given value.
*
* @param key
* Data to use to generate the possibly encrypted column family.
* @param fields
* Set of fields available for encryption.
* @return Results with columns to filter server ide and whether client side filtering is needed.
*/
private ColumnFilterResult getColumnFamilyFilter(MutableEntry key, Set<EntryField> fields) {
if (!destinationMap.containsKey(EntryField.COLUMN_FAMILY)) {
if (!sources.contains(EntryField.COLUMN_FAMILY)) {
return new ColumnFilterResult(Collections.singletonList(new Column(key.colF, null, null)), false);
} else {
return new ColumnFilterResult(Collections.singletonList(new Column(EMPTY, null, null)), true);
}
} else {
FieldEncryptor encryptor = destinationMap.get(EntryField.COLUMN_FAMILY);
if (encryptor.canBeFilteredServerSide(fields)) {
return new ColumnFilterResult(encryptor.getServerSideFilterValues(key).stream().map(f -> new Column(f, null, null)).collect(Collectors.toList()), false);
} else {
return new ColumnFilterResult(new ArrayList<>(0), true);
}
}
}
/**
* Get the filter for the given value.
*
* @param colFam
* Column family to filter.
* @param colQual
* Column qualifier to filter.
* @return A pair, with the left side being the set of columns to filter on the server side, and the right whether it is necessary to also filter on the
* client side.
*/
public ColumnFilterResult getColumnFilter(Text colFam, Text colQual) {
checkArgument(colFam != null, "colFam is null");
checkArgument(colQual != null, "colQual is null");
Set<EntryField> fields = ImmutableSet.of(EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER);
MutableEntry key = new MutableEntry();
key.colF = TextUtil.getBytes(colFam);
key.colQ = TextUtil.getBytes(colQual);
ColumnFilterResult familySearch = getColumnFamilyFilter(key, fields);
if (!destinationMap.containsKey(EntryField.COLUMN_QUALIFIER)) {
if (!sources.contains(EntryField.COLUMN_QUALIFIER)) {
return new ColumnFilterResult(familySearch.serverSideFilters.stream().map(f -> new Column(f.getColumnFamily(), key.colQ, null))
.collect(Collectors.toList()), familySearch.needsClientSideFiltering);
} else {
boolean serverSideOnly = destinationMap.containsKey(EntryField.COLUMN_FAMILY)
&& destinationMap.get(EntryField.COLUMN_FAMILY).canSearchFor(ImmutableSet.of(EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER));
return new ColumnFilterResult(familySearch.serverSideFilters.stream().map(f -> new Column(f.getColumnFamily(), EMPTY, null))
.collect(Collectors.toList()), !serverSideOnly);
}
} else {
FieldEncryptor encryptor = destinationMap.get(EntryField.COLUMN_QUALIFIER);
if (encryptor.canBeFilteredServerSide(fields)) {
boolean serverSideOnly = !familySearch.needsClientSideFiltering
|| (!destinationMap.containsKey(EntryField.COLUMN_FAMILY) && sources.contains(EntryField.COLUMN_FAMILY) && encryptor.canSearchFor(ImmutableSet.of(
EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER)));
return new ColumnFilterResult(familySearch.serverSideFilters.stream()
.flatMap(f -> encryptor.getServerSideFilterValues(key).stream().map(v -> new Column(f.getColumnFamily(), v, null))).collect(Collectors.toList()),
!serverSideOnly);
} else {
boolean serverSideOnly = destinationMap.containsKey(EntryField.COLUMN_FAMILY)
&& destinationMap.get(EntryField.COLUMN_FAMILY).canSearchFor(ImmutableSet.of(EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER));
return new ColumnFilterResult(familySearch.serverSideFilters.stream().map(f -> new Column(f.getColumnFamily(), null, null))
.collect(Collectors.toList()), !serverSideOnly);
}
}
}
/**
* Converts the range to a set of encrypted server side range queries.
*
* @param range
* Range to convert.
* @param serverSideRanges
* List of server side filters.
* @return Whether this range still needs to be searched for client side.
*/
public boolean transformRange(Range range, Collection<Range> serverSideRanges) {
checkArgument(range != null, "range is null");
checkArgument(serverSideRanges != null, "serverSideRanges is null");
// If only the value is encrypted, then the key can be searched server side.
if (destinationMap.size() == 1 && destinationMap.containsKey(EntryField.VALUE)) {
serverSideRanges.add(range);
return false;
}
// If the range includes everything, it can be sent server side.
if (range.isInfiniteStartKey() && range.isInfiniteStopKey()) {
serverSideRanges.add(range);
return false;
}
MutableEntry startKey = range.isInfiniteStartKey() ? new MutableEntry() : new MutableEntry(range.getStartKey());
MutableEntry endKey = range.isInfiniteStopKey() ? new MutableEntry() : new MutableEntry(range.getEndKey());
// Determine which set of fields are equal in both the start and end keys; these keys can be used as source values for deterministically encrypted fields.
Set<EntryField> equalFields = new HashSet<>();
EntryField followingKey = null;
if (!range.isInfiniteStartKey() && !range.isInfiniteStopKey()) {
for (EntryField field : FieldEncryptorConfig.KEY_SOURCE_FIELDS) {
byte[] start = startKey.getBytes(field);
byte[] end = endKey.getBytes(field);
if (Arrays.equals(start, end)) {
equalFields.add(field);
} else if (!range.isEndKeyInclusive() && range.getStartKey().followingKey(toPartialKey(field)).equals(range.getEndKey())) {
// TODO: This may actually be buggy. I'm not sure if it is possible to have this true twice, and if so how I should handle that.
followingKey = field;
break;
}
}
if (followingKey != null) {
equalFields.add(followingKey);
}
}
// Generate server side filters.
List<MutableEntry> startKeys = new ArrayList<>(), endKeys = new ArrayList<>();
startKeys.add(new MutableEntry());
endKeys.add(new MutableEntry());
PartialKey prefix = null;
boolean prefixHasVariance = range.isInfiniteStartKey() || range.isInfiniteStopKey(); // Infinite keys always give variance.
for (EntryField field : EntryField.values()) {
if (field == EntryField.VALUE) {
continue;
}
if (!destinationMap.containsKey(field)) {
if (!sources.contains(field)) {
// Only plaintext fields that are not wiped (i.e., used as a source) will contribute to the filter.
if (range.isInfiniteStartKey()) {
for (MutableEntry entry : endKeys) {
entry.setBytes(field, endKey.getBytes(field));
}
} else if (range.isInfiniteStopKey()) {
for (MutableEntry entry : startKeys) {
entry.setBytes(field, startKey.getBytes(field));
}
} else {
if (!equalFields.contains(field)) {
prefixHasVariance = true;
}
Iterator<MutableEntry> startKeyIterator = startKeys.iterator();
Iterator<MutableEntry> endKeyIterator = endKeys.iterator();
while (startKeyIterator.hasNext()) {
switch (field) {
case TIMESTAMP:
startKeyIterator.next().timestamp = startKey.timestamp;
endKeyIterator.next().timestamp = endKey.timestamp;
break;
case DELETE:
startKeyIterator.next().delete = startKey.delete;
endKeyIterator.next().delete = endKey.delete;
break;
default:
startKeyIterator.next().setBytes(field, startKey.getBytes(field));
endKeyIterator.next().setBytes(field, endKey.getBytes(field));
}
}
}
}
} else { // Encrypted field.
if (prefixHasVariance || !destinationMap.get(field).canBeFilteredServerSide(equalFields)) {
appendServerSideRanges(serverSideRanges, range, startKeys, endKeys, prefix);
return true;
} else {
startKeys = getServerSideFilterKeys(startKeys, field, startKey, false);
endKeys = getServerSideFilterKeys(endKeys, field, startKey, field == followingKey);
}
}
prefix = toPartialKey(field);
}
// Doesn't need a client side filter, as everything can be done server-side.
Iterator<MutableEntry> startKeyIterator = startKeys.iterator();
Iterator<MutableEntry> endKeyIterator = endKeys.iterator();
while (startKeyIterator.hasNext()) {
serverSideRanges.add(new Range(startKeyIterator.next().toKey(), range.isStartKeyInclusive(), endKeyIterator.next().toKey(), range.isEndKeyInclusive()));
}
return false;
}
/**
* Append the server side filter keys for the given field to the existing filters.
*
* @param existingFilters
* Filters that are being added to.
* @param field
* Field to add encrypted search for.
* @param key
* Data to use for generated encrypted search.
* @param followingKey
* Should the returned value be generated as in {@link Key#followingArray(byte[])}.
* @return New list of server side filters.
*/
private List<MutableEntry> getServerSideFilterKeys(List<MutableEntry> existingFilters, EntryField field, MutableEntry key, boolean followingKey) {
List<MutableEntry> entries = new ArrayList<>();
List<byte[]> values = destinationMap.get(field).getServerSideFilterValues(key, followingKey);
for (MutableEntry entry : existingFilters) {
for (byte[] value : values) {
MutableEntry newItem = entry.cloneEntry();
newItem.setBytes(field, value);
entries.add(newItem);
}
}
return entries;
}
/**
* @param serverSideRanges
* List of server side filters.
* @param originalRange
* Range to convert.
* @param startKeys
* Set of start key filters.
* @param endKeys
* Set of end key filters.
* @param prefix
* What portion of the key are we filtering?
*/
private void appendServerSideRanges(Collection<Range> serverSideRanges, Range originalRange, List<MutableEntry> startKeys, List<MutableEntry> endKeys,
PartialKey prefix) {
if (prefix == null) {
serverSideRanges.add(new Range());
return;
}
if (originalRange.isInfiniteStartKey()) {
for (MutableEntry endKey : endKeys) {
serverSideRanges.add(new Range(null, true, endKey.toKey().followingKey(prefix), false));
}
} else if (originalRange.isInfiniteStopKey()) {
for (MutableEntry startKey : startKeys) {
serverSideRanges.add(new Range(startKey.toKey(), true, null, true));
}
} else {
Iterator<MutableEntry> startKeyIterator = startKeys.iterator();
Iterator<MutableEntry> endKeyIterator = endKeys.iterator();
while (startKeyIterator.hasNext()) {
serverSideRanges.add(new Range(startKeyIterator.next().toKey(), true, endKeyIterator.next().toKey().followingKey(prefix), false));
}
}
}
/**
* Converts the field to a partial key.
*
* @param field
* Field to transform.
* @return The partial key that ends with this field.
*/
private static PartialKey toPartialKey(EntryField field) {
switch (field) {
case ROW:
return PartialKey.ROW;
case COLUMN_FAMILY:
return PartialKey.ROW_COLFAM;
case COLUMN_QUALIFIER:
return PartialKey.ROW_COLFAM_COLQUAL;
case COLUMN_VISIBILITY:
return PartialKey.ROW_COLFAM_COLQUAL_COLVIS;
case TIMESTAMP:
return PartialKey.ROW_COLFAM_COLQUAL_COLVIS_TIME;
case DELETE:
return PartialKey.ROW_COLFAM_COLQUAL_COLVIS_TIME_DEL;
default:
throw new IllegalArgumentException();
}
}
}
@@ -0,0 +1,484 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static com.google.common.base.Preconditions.checkArgument;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static edu.mit.ll.pace.internal.Utils.xor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.security.ColumnVisibility;
import org.apache.accumulo.core.security.ColumnVisibility.Node;
import org.apache.accumulo.core.security.ColumnVisibility.NodeType;
import org.apache.hadoop.io.WritableUtils;
import edu.mit.ll.pace.EntryField;
import edu.mit.ll.pace.IllegalKeyRequestException;
import edu.mit.ll.pace.encryption.EncryptionKeyContainer.KeyWithVersion;
import edu.mit.ll.pace.internal.MutableEntry;
/**
* Encrypts an Accumulo field based on the supplied configuration.
*/
final class FieldEncryptor {
/**
* Secure random number generator.
*/
private static final SecureRandom random = new SecureRandom();
/**
* Configuration for this encryptor.
*/
private final FieldEncryptorConfig config;
/**
* The keys available to this encryptor.
*/
private final EncryptionKeyContainer keys;
/**
* The value encryptor to use to encrypt data.
*/
private final ValueEncryptorBase encryptor;
/**
* Creates a field encryptor.
*
* @param config
* Configuration for the encryption.
* @param keys
* Container with the keys to use for encryption.
*/
FieldEncryptor(FieldEncryptorConfig config, EncryptionKeyContainer keys) {
checkArgument(config != null, "config is null");
checkArgument(keys != null, "keys is null");
this.config = config;
this.keys = keys;
this.encryptor = config.valueEncryptor.getInstance(config.provider);
}
/**
* Encrypt the given entry.
*
* @param entry
* {@link MutableEntry} to encrypt.
* @param result
* {@link MutableEntry} to write result to.
* @param columnVisibility
* The parsed column visibility.
* @throws IOException
* Not actually thrown.
*/
void encrypt(MutableEntry entry, MutableEntry result, ColumnVisibility columnVisibility) throws IOException {
// Put the data in a single byte array.
byte[] data = concatData(entry);
// Gets the key used to encrypt the data. Also write to the front of the ciphertext the metadata necessary to retrieve the key.
ByteArrayOutputStream ciphertextStream = new ByteArrayOutputStream();
DataOutput ciphertextOut = new DataOutputStream(ciphertextStream);
// Encrypt the data, and write it to the result (first wrapping it if writing to the column visibility field).
byte[] key = getKey(columnVisibility, ciphertextOut);
ciphertextOut.write(encryptor.encrypt(key, data));
byte[] ciphertext = ciphertextStream.toByteArray();
result.setBytes(config.destination, ciphertext);
}
/**
* Concat the data in preparation for it to be encrypted.
*
* @param entry
* Entry to pull data from.
* @return Concatenated data.
* @throws IOException
* Not actually thrown.
*/
private byte[] concatData(MutableEntry entry) throws IOException {
ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
DataOutput dataOut = new DataOutputStream(dataStream);
for (EntryField source : config.sources) {
switch (source) {
case ROW:
case COLUMN_FAMILY:
case COLUMN_QUALIFIER:
case COLUMN_VISIBILITY:
case VALUE:
byte[] bytes = entry.getBytes(source);
WritableUtils.writeVInt(dataOut, bytes.length);
dataOut.write(bytes);
break;
default:
throw new UnsupportedOperationException();
}
}
return dataStream.toByteArray();
}
/**
* Decrypt the given entry.
*
* @param entry
* {@link MutableEntry} to encrypt.
* @param result
* {@link MutableEntry} to write result to.
* @param columnVisibility
* The parsed column visibility.
*
* @throws IOException
* Not actually thrown.
*/
void decrypt(MutableEntry entry, MutableEntry result, ColumnVisibility columnVisibility) throws IOException {
ByteArrayInputStream ciphertextStream = new ByteArrayInputStream(entry.getBytes(config.destination));
DataInput ciphertextIn = new DataInputStream(ciphertextStream);
byte[] key = getKey(columnVisibility, ciphertextIn);
byte[] ciphertext = new byte[ciphertextStream.available()];
ciphertextIn.readFully(ciphertext);
byte[] decryptedData = encryptor.decrypt(key, ciphertext);
// Break apart the decrypted data.
ByteArrayInputStream dataStream = new ByteArrayInputStream(decryptedData);
DataInput dataIn = new DataInputStream(dataStream);
for (EntryField source : config.sources) {
switch (source) {
case ROW:
case COLUMN_FAMILY:
case COLUMN_QUALIFIER:
case COLUMN_VISIBILITY:
case VALUE:
int length = WritableUtils.readVInt(dataIn);
byte[] bytes = new byte[length];
dataIn.readFully(bytes);
result.setBytes(source, bytes);
break;
// case TIMESTAMP:
// result.timestamp = WritableUtils.readVLong(dataIn);
// break;
// case DELETE:
// result.delete = dataIn.readBoolean();
// break;
default:
throw new UnsupportedOperationException();
}
}
}
/**
* Get a field encryption key for use in <strong>encrypting</strong> the field.
* <p>
* Any metadata needed to retrieve this key later should be written to the DataOuput object.
*
* @param visibility
* Visibility expression for the field.
* @param out
* DataOutput object to write metadata to.
* @return Field encryption key.
* @throws IOException
* Not actually thrown.
*/
private byte[] getKey(ColumnVisibility visibility, DataOutput out) throws IOException {
if (config.encryptUsingVisibility) {
byte[] key = new byte[config.keyLength];
if (visibility.getParseTree().getType() != NodeType.EMPTY) {
random.nextBytes(key);
writeVisibilityShare(key, visibility.getParseTree(), visibility.getExpression(), out);
}
return key;
} else {
KeyWithVersion keyData = keys.getKey(config.keyId, config.keyLength);
WritableUtils.writeVInt(out, keyData.version); // Write the version of the key being used as meta-data.
return keyData.key;
}
}
/**
* Retrieve a field encryption key to use in <strong>decrypting</strong> the field.
* <p>
* Metadata can be read from the DataInput object. All meta-data that was written to the stream should be read out, regardless if it is used.
*
* @param visibility
* Visibility expression for the field.
* @param in
* Stream from which metadata is read.
* @return Field encryption key.
* @throws IOException
* Not actually thrown.
*/
private byte[] getKey(ColumnVisibility visibility, DataInput in) throws IOException {
if (config.encryptUsingVisibility) {
if (visibility.getParseTree().getType() != NodeType.EMPTY) {
// Rebuild the key from the shares created based on the visibility expression.
byte[] key = readVisibilityShare(visibility.getParseTree(), visibility.getExpression(), in, false);
if (key == null) {
throw new IllegalKeyRequestException();
}
return key;
} else {
return new byte[config.keyLength];
}
} else {
int version = WritableUtils.readVInt(in);
return keys.getKey(config.keyId, version, config.keyLength);
}
}
/**
* Encrypt the given share based on the current visibility node.
* <p>
* Metadata needed to later decrypt the share and the encrypted share are written to the DataOutput object.
*
* @param share
* Share of an encryption key to encrypt based on the visilbity expression.
* @param node
* Visibility node to use in encrypting the share.
* @param expression
* Visibility expression.
* @param out
* Stream to which metadata is written.
* @throws IOException
* Not actually thrown.
*/
private void writeVisibilityShare(byte[] share, Node node, byte[] expression, DataOutput out) throws IOException {
switch (node.getType()) {
case TERM:
// This is the only case we actually write to the stream. Encrypt the share with the attribute share.
// The output format is "version || length || encrypted data"
KeyWithVersion keyData = keys.getAttributeKey(new String(node.getTerm(expression).toArray(), VISIBILITY_CHARSET), config.keyId, config.keyLength);
WritableUtils.writeVInt(out, keyData.version); // Key version is written to the metadata.
byte[] encrypted;
encrypted = encryptor.encrypt(keyData.key, share);
WritableUtils.writeVInt(out, encrypted.length);
out.write(encrypted);
break;
case AND:
// Create random shares, with the final share being the original share xor'ed with each of the random shares.
byte[] mask = new byte[share.length];
for (int i = 0; i < node.getChildren().size(); i++) {
if (i == (node.getChildren().size() - 1)) {
writeVisibilityShare(xor(mask, share), node.getChildren().get(i), expression, out);
} else {
byte[] randomMask = new byte[share.length];
random.nextBytes(randomMask);
writeVisibilityShare(randomMask, node.getChildren().get(i), expression, out);
xor(mask, randomMask);
}
}
break;
case OR:
// Write the same share with each attribute share.
for (Node child : node.getChildren()) {
writeVisibilityShare(share, child, expression, out);
}
break;
default:
throw new UnsupportedOperationException();
}
}
/**
* Decrypt the given share based on the current visibility node.
* <p>
* This method must read all metadata that the corresponding call to {@link #writeVisibilityShare(byte[], Node, byte[], DataOutput)} wrote. This is necessary
* regardless of whether the key retrieval is ongoing, successful, or failed.
*
* @param node
* Visibility node to use in decrypting the share.
* @param expression
* Visibility expression.
* @param in
* Stream from which metadata is read.
* @param skipDecryption
* Tracks whether the call to this method is trying to regenerate the share.
* <p>
* This will be false if the share was already generated (i.e., from a different branch in an OR expression) or if there was an error in this branch
* (i.e., lacked the attribute key to decrypt a different share of an AND expression).
* @return The decrypted share, or null if it could not be obtained.
* @throws IOException
* Not actually thrown.
*/
private byte[] readVisibilityShare(Node node, byte[] expression, DataInput in, boolean skipDecryption) throws IOException {
byte[] share = null;
switch (node.getType()) {
case TERM:
// This is the only case we actually read from the stream. Decrypt the share with the attribute share.
// The input format is "version || length || encrypted data"
int version = WritableUtils.readVInt(in);
byte[] encrypted = new byte[WritableUtils.readVInt(in)];
in.readFully(encrypted);
if (!skipDecryption) {
try {
byte[] key = keys.getAttributeKey(new String(node.getTerm(expression).toArray(), VISIBILITY_CHARSET), config.keyId, version, config.keyLength);
share = encryptor.decrypt(key, encrypted);
} catch (IllegalKeyRequestException e) {
// Swallow this error. The user does not have access to decrypt this sub-share, but it still may be possible for the user to decrypt another
// sub-share that will give the same data. This error will be re-thrown if at the end of reading all shares the users does not have enough
// data to reconstruct the original share.
}
}
break;
case AND:
// Read random shares, with the final share being the original share xor'ed with each of the random shares.
for (Node child : node.getChildren()) {
byte[] mask = readVisibilityShare(child, expression, in, skipDecryption);
// A single failure means this whole AND is a failure.
if (!skipDecryption) {
if (mask == null) {
share = null;
skipDecryption = true;
} else {
if (share == null) {
share = mask;
} else {
xor(share, mask);
}
}
}
}
break;
case OR:
// Read the share from multiple possible attribute shares. Only one share is needed.
for (Node child : node.getChildren()) {
byte[] tempKey = readVisibilityShare(child, expression, in, skipDecryption);
if (!skipDecryption && tempKey != null) {
share = tempKey;
skipDecryption = true;
}
}
break;
default:
throw new UnsupportedOperationException();
}
return share;
}
/**
* Checks whether the field can be filtered server side.
* <p>
* A field can be filtered server side if two things hold:
* <ol>
* <li>It is encrypted deterministically.</li>
* <li>All of this field encryptor's field sources are available for use.</li>
* </ol>
*
* @param availableSources
* Fields available during encryption to pull data from.
* @return Whether it is possible to generate a server side encryption for this item.
*/
boolean canBeFilteredServerSide(Collection<EntryField> availableSources) {
return config.valueEncryptor.isDeterministic() && availableSources.containsAll(config.sources);
}
/**
* Checks the encrypted value created by this encryptor can be used to search for the given fields.
* <p>
* The fields can only be searched for if they are the same set as the sources for this field encryptor.
*
* @param fields
* Fields that need to be searched.
* @return Whether this field's encrypted value can be used to search for the given fields.
*/
boolean canSearchFor(Set<EntryField> fields) {
return config.valueEncryptor.isDeterministic() && fields.equals(config.sources);
}
/**
* Transform a value into a set of server side search terms. This method assumes that {@link #canBeFilteredServerSide(Collection)} was called, and that this
* field is suitable for server side filtering.
* <p>
* This method will encrypt the data in the given key using ever version of that key available. For example, if key "ROW_KEY" has 3 versions, than this method
* will encrypt the key with all 3 versions, resulting in 3 possible server side filter values for this encrypted field.
*
* @param key
* Data used to create the filter.
* @return Keys containing the data for the server side filters.
*/
List<byte[]> getServerSideFilterValues(MutableEntry key) {
return getServerSideFilterValues(key, false);
}
/**
* Transform a value into a set of server side search terms. This method assumes that {@link #canBeFilteredServerSide(Collection)} was called, and that this
* field is suitable for server side filtering.
* <p>
* This method will encrypt the data in the given key using ever version of that key available. For example, if key "ROW_KEY" has 3 versions, than this method
* will encrypt the key with all 3 versions, resulting in 3 possible server side filter values for this encrypted field.
*
* @param key
* Data used to create the filter.
* @param followingKey
* Should the returned value be generated as in {@link Key#followingArray(byte[])}.
* @return Keys containing the data for the server side filters.
*/
List<byte[]> getServerSideFilterValues(MutableEntry key, boolean followingKey) {
List<byte[]> filterValues = new ArrayList<>();
for (KeyWithVersion keyData : keys.getKeys(config.keyId, config.keyLength)) {
ByteArrayOutputStream ciphertextStream = new ByteArrayOutputStream();
DataOutput ciphertextOut = new DataOutputStream(ciphertextStream);
try {
WritableUtils.writeVInt(ciphertextOut, keyData.version);
ciphertextOut.write(encryptor.encrypt(keyData.key, concatData(key)));
if (followingKey) {
ciphertextOut.writeByte(0);
}
} catch (IOException e) { // IO exceptions won't be thrown in practice as we are operating on in-memory streams.
throw new EncryptionException(e);
}
filterValues.add(ciphertextStream.toByteArray());
}
return filterValues;
}
}
@@ -0,0 +1,158 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Collections;
import java.util.List;
import java.util.SortedSet;
import org.apache.commons.lang3.StringUtils;
import org.ini4j.Ini;
import org.ini4j.Profile.Section;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import edu.mit.ll.pace.EntryField;
/**
* Holds configuration data for encrypting an Accumulo field.
* <p>
* This class defines how data will be encrypted to a specific field. This includes defining which encryption scheme will be used, where the the data to be
* encrypted is pulled from, and where it is to be written.
*/
public final class FieldEncryptorConfig {
/**
* The set of fields that can be the destination of encrypted content in the key.
*/
static final List<EntryField> KEY_DESTINATION_FIELDS = ImmutableList.of(EntryField.ROW, EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER);
/**
* Set of fields that can be sources for data when encrypting a field in the key.
*/
static final List<EntryField> KEY_SOURCE_FIELDS = ImmutableList.of(EntryField.ROW, EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER,
EntryField.COLUMN_VISIBILITY);
/**
* Set of fields that can be sources for data when encrypting the value.
*/
static final List<EntryField> VALUE_SOURCE_FIELDS = ImmutableList.of(EntryField.VALUE);
/**
* The {@link ValueEncryptor} used to encrypt the field.
*/
final ValueEncryptor valueEncryptor;
/**
* The provider to use when getting an instance of the {@link ValueEncryptor}.
*/
final String provider;
/**
* Will this encryption encrypt the field using the visibility.
*/
final boolean encryptUsingVisibility;
/**
* Id of the key that will be used for encryption.
*/
final String keyId;
/**
* The length of keys to use for encryption.
*/
final int keyLength;
/**
* The field that the encrypted data will be written to.
*/
final EntryField destination;
/**
* The fields that plaintext data is drawn from.
*/
final SortedSet<EntryField> sources;
/**
* Creates a configuration for an {@link FieldEncryptor}.
*
* @param valueEncryptor
* The {@link ValueEncryptor} used to encrypt the field.
* @param provider
* The provider to use when getting an instance of the {@link ValueEncryptor}. If this is null, will search for the appropriate providers as defined
* by the system.
* @param encryptUsingVisibility
* Will this encryption encrypt the field using the visibility.
* @param keyId
* Id of the key that will be used for encryption.
* @param keyLength
* The length of the key to use.
* @param destination
* The field where encrypted data will be written to by the encryptor.
* @param sources
* The fields that plaintext data is drawn from.
*/
public FieldEncryptorConfig(ValueEncryptor valueEncryptor, String provider, boolean encryptUsingVisibility, String keyId, int keyLength,
EntryField destination, SortedSet<EntryField> sources) {
checkArgument(valueEncryptor != null, "valueEncryptor is null");
checkArgument(!encryptUsingVisibility || !valueEncryptor.isDeterministic(),
"Cannot deterministically encrypt when encrypting fields using the visibility expression");
checkArgument(!Strings.isNullOrEmpty(keyId), "keyId is null or empty");
checkArgument(valueEncryptor.isValidKeyLength(keyLength), "invalid key length for the value encryptor");
checkArgument(destination != null, "destination is null");
checkArgument(KEY_DESTINATION_FIELDS.contains(destination) || destination == EntryField.VALUE, "invalid destination");
checkArgument(sources != null, "sources is null");
checkArgument(sources.size() > 0, "sources is empty");
if (KEY_SOURCE_FIELDS.contains(destination)) {
for (EntryField source : sources) {
checkArgument(KEY_SOURCE_FIELDS.contains(source), source.toString() + " cannot be encrypted into a key field");
}
} else {
for (EntryField source : sources) {
checkArgument(VALUE_SOURCE_FIELDS.contains(source), source.toString() + " cannot be encrypted into the value field");
}
}
this.valueEncryptor = valueEncryptor;
this.provider = provider;
this.encryptUsingVisibility = encryptUsingVisibility;
this.keyLength = keyLength;
this.keyId = keyId;
this.destination = destination;
this.sources = Collections.unmodifiableSortedSet(sources);
}
/**
* Write's this FieldEncryptorBase configuration to the given INI.
*
* @param configIni
* INI to write to.
*/
void write(Ini configIni) {
Section section = configIni.add(destination.toString());
section.put("cipher", valueEncryptor.toString());
section.put("provider", provider);
section.put("useVisibility", encryptUsingVisibility);
section.put("keyId", keyId);
section.put("keyLength", Integer.toString(keyLength));
section.put("sources", StringUtils.join(sources, ','));
}
}
@@ -0,0 +1,177 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.util.Collection;
import java.util.SortedSet;
import java.util.TreeSet;
import org.apache.commons.lang3.StringUtils;
import org.ini4j.Profile.Section;
import edu.mit.ll.pace.EntryField;
/**
* Builder for configuration of field encryptor objects.
*/
public class FieldEncryptorConfigBuilder {
// For definitions of these values see FieldEncryptorConfig.
private ValueEncryptor valueEncryptor;
private String provider;
private boolean encryptUsingVisibility;
private int keyLength;
private String keyId;
private EntryField destination;
private SortedSet<EntryField> sources = new TreeSet<>();
/**
* Set the value encryptor.
*
* @param valueEncryptor
* The {@link ValueEncryptor} used to encrypt the field.
* @return Builder.
*/
public FieldEncryptorConfigBuilder setValueEncryptor(ValueEncryptor valueEncryptor) {
this.valueEncryptor = valueEncryptor;
return this;
}
/**
* Set the provider to use.
*
* @param provider
* The provider to use when getting an instance of the {@link ValueEncryptor}. If this is null, will search for the appropriate providers as defined
* by the system.
* @return Builder.
*/
public FieldEncryptorConfigBuilder setProvider(String provider) {
this.provider = provider;
return this;
}
/**
* Set the whether to encrypt using the visibility.
*
* @param encryptUsingVisibility
* Will this encryption encrypt the field using the visibility.
* @return Builder.
*/
public FieldEncryptorConfigBuilder setEncryptUsingVisibility(boolean encryptUsingVisibility) {
this.encryptUsingVisibility = encryptUsingVisibility;
return this;
}
/**
* Set the key length.
*
* @param keyLength
* The length of the key to use.
* @return Builder.
*/
public FieldEncryptorConfigBuilder setKeyLength(int keyLength) {
this.keyLength = keyLength;
return this;
}
/**
* Set the key ID.
*
* @param keyId
* Id of the key that will be used for encryption.
* @return Builder.
*/
public FieldEncryptorConfigBuilder setKeyId(String keyId) {
this.keyId = keyId;
return this;
}
/**
* Set the destination field.
*
* @param destination
* The field where encrypted data will be written to by the encryptor.
* @return Builder.
*/
public FieldEncryptorConfigBuilder setDestination(EntryField destination) {
this.destination = destination;
return this;
}
/**
* Add a source to the list of sources for this encryptor.
*
* @param source
* A field that will be encrypted and stored in the destination.
* @return Builder.
*/
public FieldEncryptorConfigBuilder addSource(EntryField source) {
this.sources.add(source);
return this;
}
/**
* Set the sources.
*
* @param sources
* The fields that will be encrypted and stored in the destination.
* @return Builder.
*/
public FieldEncryptorConfigBuilder setSources(Collection<EntryField> sources) {
this.sources.clear();
if (sources != null) {
this.sources.addAll(sources);
}
return this;
}
/**
* Read the configuration from a Reader.
*
* @param section
* Ini section to read from.
* @return BUilder
*/
FieldEncryptorConfigBuilder readFromIni(Section section) {
ValueEncryptor valueEncryptor = ValueEncryptor.fromString(section.get("cipher"));
setValueEncryptor(valueEncryptor);
setProvider(section.get("provider"));
setEncryptUsingVisibility(section.containsKey("useVisibility") ? Boolean.parseBoolean(section.get("useVisibility")) : false);
setKeyId(section.getOrDefault("keyId", valueEncryptor.toString()));
setKeyLength(section.containsKey("keyLength") ? Integer.parseInt(section.get("keyLength")) : valueEncryptor.getDefaultKeyLength());
setDestination(EntryField.fromString(section.getName()));
if (!section.containsKey("sources")) {
addSource(EntryField.fromString(section.getName()));
} else {
for (String source : StringUtils.split(section.get("sources"), ',')) {
addSource(EntryField.fromString(source.trim()));
}
}
return this;
}
/**
* Build the {@link FieldEncryptorConfig}.
*
* @return Built config.
*/
public FieldEncryptorConfig build() {
return new FieldEncryptorConfig(valueEncryptor, provider, encryptUsingVisibility, keyId, keyLength, destination, sources);
}
}
@@ -0,0 +1,169 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.util.Map;
import java.util.TreeMap;
import com.google.common.collect.ImmutableList;
/**
* Defines the set of supported instantiations for {@link ValueEncryptorBase}.
*/
public enum ValueEncryptor {
AES_SIV_DETERMINISTIC("AES_SIV_DETERMINISTIC", true, true, null), AES_CTR("AES_CTR", false, false, "AES/CTR/NoPadding"), AES_CFB("AES_CFB", false, false,
"AES/CFB/NoPadding"), AES_CBC("AES_CBC", false, false, "AES/CBC/PKCS5Padding"), AES_OFB("AES_OFB", false, false, "AES/OFB/NoPadding"), AES_GCM("AES_GCM",
false, false, "AES/GCM/NoPadding");
/**
* Valid key sizes for generic AES.
*/
final static ImmutableList<Integer> VALID_KEY_LENGTHS = ImmutableList.of(16, 24, 32);
/**
* Valid key sizes for AES algorithms that need both an aes and an IV-generation key.
*/
final static ImmutableList<Integer> VALID_DOUBLE_KEY_LENGTHS = ImmutableList.of(32, 48, 64);
/**
* Set of ValueEncryptor names mapped to the ValueEncryptor enum object. Used to speed up name lookups.
*/
private static final Map<String,ValueEncryptor> encryptors;
static {
encryptors = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (ValueEncryptor encryptor : values()) {
encryptors.put(encryptor.name, encryptor);
}
}
/**
* Name of the encryptor.
*/
private final String name;
/**
* Whether the encryptor is deterministic in its encryption of ciphertext (i.e., same value produces the same encrypted ciphertext).
*/
private final boolean deterministic;
/**
* Whether the encryptor needs two keys, an encryption and iv-generation key.
*/
private final boolean needsTwoKeys;
/**
* Transformation used by this encryptor, or null if the encryptor's implementation does not rely on {@link javax.crypto.Cipher#getInstance(String, String)}.
* <p>
* The transformation defines the algorithm used, the mode it is used in, and any padding.
*/
private final String transformation;
/**
* Create the value encryptor.
*
* @param name
* Name of the encryptor.
* @param deterministic
* Whether the encryptor is deterministic in its encryption of ciphertext (i.e., same value produces the same encrypted ciphertext).
* @param needsTwoKeys
* Whether the encryptor needs two keys, an encryption and iv-generation key.
* @param transformation
* Transformation used by this encryptor, or null if the encryptor's implementation does not rely on
* {@link javax.crypto.Cipher#getInstance(String, String)}.
*/
ValueEncryptor(String name, boolean deterministic, boolean needsTwoKeys, String transformation) {
this.name = name;
this.deterministic = deterministic;
this.needsTwoKeys = needsTwoKeys;
this.transformation = transformation;
}
@Override
public String toString() {
return this.name;
}
/**
* Get the value encryptor from its {@link String} representation.
* <p>
* This method is paired with {@link #toString()}, and will parse strings generated by that method.
*
* @param value
* Value to parse.
* @return ValueEncryptor for the value.
*/
static ValueEncryptor fromString(String value) {
ValueEncryptor encryptor = encryptors.get(value);
if (encryptor == null) {
throw new IllegalArgumentException("invalid name");
}
return encryptor;
}
/**
* Gets whether this encryptor is deterministic.
*
* @return Whether this encryptor is deterministic.
*/
boolean isDeterministic() {
return deterministic;
}
/**
* Gets the default key length for this encryptor.
*
* @return The default key length of this encryptor.
*/
int getDefaultKeyLength() {
return (needsTwoKeys ? VALID_DOUBLE_KEY_LENGTHS : VALID_KEY_LENGTHS).get(0);
}
/**
* Gets whether the key length is valid for this encryptor.
*
* @param keyLength
* Length of the key to check for validity.
* @return Whether the given key length is valid for this encryptor.
*/
boolean isValidKeyLength(int keyLength) {
return (needsTwoKeys ? VALID_DOUBLE_KEY_LENGTHS : VALID_KEY_LENGTHS).contains(keyLength);
}
/**
* Gets a new {@link ValueEncryptorBase} instance for the encryptor.
*
* @param provider
* Provider to use when creating the instance.
* @return The created instance.
*/
ValueEncryptorBase getInstance(String provider) {
switch (this) {
case AES_SIV_DETERMINISTIC:
return new DeterministicSIVValueEncryptor();
case AES_CTR:
case AES_CFB:
case AES_CBC:
case AES_OFB:
case AES_GCM:
return new AESValueEncryptor(transformation, provider);
default:
throw new UnsupportedOperationException();
}
}
}
@@ -0,0 +1,55 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.io.IOException;
/**
* Defines the contract for classes that supports symmetric encryption and decryption of data.
*/
abstract class ValueEncryptorBase {
/**
* Encrypt the given data with the given key.
*
* @param key
* Key to encrypt with.
* @param data
* Data to encrypt.
* @return The encrypted data, encoded with any metadata needed to decrypt the data.
* @throws EncryptionException
* Thrown if an error happens during encryption.
* @throws IOException
* Not actually thrown.
*/
abstract byte[] encrypt(byte[] key, byte[] data) throws EncryptionException, IOException;
/**
* Decrypt the given data with the given key.
*
* @param key
* Key to decrypt with.
* @param data
* Data to decrypt, including the encoded metadata needed to decrypt the data.
* @return The decrypted data.
* @throws EncryptionException
* Thrown if an error happens during encryption
* @throws IOException
* Not actually thrown.
*/
abstract byte[] decrypt(byte[] key, byte[] data) throws EncryptionException, IOException;
}
@@ -0,0 +1,187 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.internal;
import static edu.mit.ll.pace.internal.Utils.EMPTY;
import java.util.AbstractMap;
import java.util.Map;
import org.apache.accumulo.core.data.ColumnUpdate;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import edu.mit.ll.pace.EntryField;
/**
* Mutable container representing an Accumulo entry.
* <p>
* This class is not part of the PACE API, and should not be used.
* </p>
*/
public final class MutableEntry implements Cloneable {
/**
* The byte-based values in an entry.
*/
public byte[] row = EMPTY, colF = EMPTY, colQ = EMPTY, colVis = EMPTY, value = EMPTY;
/**
* The timestamp for the entry.
*/
public long timestamp = 0L;
/**
* Whether the entry has been deleted.
*/
public boolean delete = false;
/**
* Creates an empty mutable entry.
*/
public MutableEntry() {}
/**
* Creates a mutable entry for the given key.
*
* @param key
* Key to wrap.
*/
public MutableEntry(Key key) {
row = key.getRowData().getBackingArray();
colF = key.getColumnFamilyData().getBackingArray();
colQ = key.getColumnQualifierData().getBackingArray();
colVis = key.getColumnVisibilityData().getBackingArray();
timestamp = key.getTimestamp();
delete = key.isDeleted();
}
/**
* Creates a mutable entry for the given entry.
*
* @param entry
* Entry to wrap.
*/
public MutableEntry(Map.Entry<Key,Value> entry) {
this(entry.getKey());
value = entry.getValue().get();
}
/**
* Creates a mutable entry for the given update.
*
* @param row
* The row that is being updated.
* @param update
* Update to wrap.
*/
public MutableEntry(byte[] row, ColumnUpdate update) {
this.row = row;
this.colF = update.getColumnFamily();
this.colQ = update.getColumnQualifier();
this.colVis = update.getColumnVisibility();
this.timestamp = update.getTimestamp();
this.delete = update.isDeleted();
this.value = update.getValue();
}
/**
* Wrap the data in this class in a cannonical {@link Key}.
*
* @return An Accumulo key.
*/
public Key toKey() {
return new Key(row, colF, colQ, colVis, timestamp, delete, false);
}
/**
* Wrap the data in this class in a cannonical {@literal Entry<Key,Value>}.
*
* @return An Accumulo entry.
*/
public Map.Entry<Key,Value> toEntry() {
return new AbstractMap.SimpleImmutableEntry<>(toKey(), new Value(value, false));
}
/**
* Clone this object.
* <p>
* Cloning this object will never throw an exception.
*
* @return Shallow clone of this object.
*/
public MutableEntry cloneEntry() {
try {
return (MutableEntry) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException(e);
}
}
/**
* Get the bytes for the given field.
*
* @param field
* Field for which to retrieve data.
* @return Requested bytes.
*/
public byte[] getBytes(EntryField field) {
switch (field) {
case ROW:
return row;
case COLUMN_FAMILY:
return colF;
case COLUMN_QUALIFIER:
return colQ;
case COLUMN_VISIBILITY:
return colVis;
case VALUE:
return value;
default:
throw new IllegalArgumentException("invalid field");
}
}
/**
* Set the bytes for the given field.
*
* @param field
* Field for which to set data.
* @param bytes
* Bytes to set.
*/
public void setBytes(EntryField field, byte[] bytes) {
switch (field) {
case ROW:
row = bytes;
break;
case COLUMN_FAMILY:
colF = bytes;
break;
case COLUMN_QUALIFIER:
colQ = bytes;
break;
case COLUMN_VISIBILITY:
colVis = bytes;
break;
case VALUE:
value = bytes;
break;
default:
throw new IllegalArgumentException("invalid field");
}
}
}
@@ -0,0 +1,63 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.internal;
import static com.google.common.base.Preconditions.checkArgument;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
/**
* Utility functions used by PACE.
*/
public final class Utils {
/**
* The empty byte array.
*/
public static final byte[] EMPTY = new byte[0];
/**
* Charset to use when changing column visibility bytes into a String and vice versa.
*/
public static final Charset VISIBILITY_CHARSET = StandardCharsets.US_ASCII;
/**
* Static class.
*/
private Utils() {}
/**
* XOR first and second, storing the result in first.
*
* @param first
* First argument.
* @param second
* Second argument.
* @return XOR'ed first argument.
*/
public static byte[] xor(byte[] first, byte[] second) {
checkArgument(first != null, "first is null");
checkArgument(second != null, "second is null");
checkArgument(first.length == second.length, "first and second must be the same length");
for (int i = 0; i < first.length; i++) {
first[i] = (byte) (first[i] ^ second[i]);
}
return first;
}
}
@@ -0,0 +1,381 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static com.google.common.base.Preconditions.checkArgument;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.Signature;
import java.util.Map.Entry;
import org.apache.accumulo.core.data.ArrayByteSequence;
import org.apache.accumulo.core.data.ByteSequence;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.ColumnVisibility;
import org.apache.accumulo.core.security.VisibilityEvaluator;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.io.WritableUtils;
import com.google.common.primitives.Longs;
import edu.mit.ll.pace.internal.MutableEntry;
import edu.mit.ll.pace.signature.SignatureConfig.Destination;
import edu.mit.ll.pace.signature.SignatureKeyContainer.PrivateKeyWithId;
/**
* Signs an Accumulo {@literal Entry<Key,Value>} based on the supplied configuration.
*/
public final class EntrySigner {
/**
* Signature configuration.
*/
private final SignatureConfig config;
/**
* Keys to use for signing and verifying.
*/
private final SignatureKeyContainer keys;
/**
* Verifier for this instance.
*/
private Signature signer;
/**
* The id of the signer.
*/
private byte[] signerId;
/**
* Signer for this instance.
*/
private Signature verifier;
/**
* Create an entry singer.
*
* @param config
* Configuration for the signing.
* @param keys
* Container with the keys to use for signing.
*/
public EntrySigner(SignatureConfig config, SignatureKeyContainer keys) {
checkArgument(config != null, "config is null");
checkArgument(keys != null, "keys is null");
this.config = config;
this.keys = keys;
}
/**
* Sign the given entry.
*
* @param entry
* Entry to sign.
* @return Signed update.
* @throws SignatureException
* Thrown in signature creation fails.
*/
public Entry<Key,Value> sign(Entry<Key,Value> entry, boolean hasTimestamp) {
checkArgument(entry != null, "entry is null");
checkArgument(!entry.getKey().isDeleted(), "cannot sign deleted entries");
// Defer initialization of the signing algorithm until needed.
if (signer == null) {
signer = config.algorithm.getInstance(config.provider);
try {
PrivateKeyWithId keyData = keys.getSigningKey();
signer.initSign(keyData.key);
signerId = keyData.id;
} catch (InvalidKeyException e) {
throw new SignatureException(e);
}
}
// If we are writing to the visibility field, we will need to replace the empty visibility with the default visibility.
MutableEntry wrapped = new MutableEntry(entry);
if (config.destination == Destination.COLUMN_VISIBILITY && wrapped.colVis.length == 0) {
wrapped.colVis = config.defaultVisibility;
}
// Sign the entry.
byte[] signature;
try {
signer.update(wrapped.row);
signer.update(wrapped.colF);
signer.update(wrapped.colQ);
signer.update(wrapped.colVis);
signer.update(hasTimestamp ? (byte) 0 : (byte) 1);
if (hasTimestamp) {
signer.update(Longs.toByteArray(wrapped.timestamp));
}
signer.update(wrapped.value);
signature = signer.sign();
} catch (java.security.SignatureException e) {
throw new SignatureException(e);
}
// Create a stream with the signature data.
try {
ByteArrayOutputStream stream = new ByteArrayOutputStream();
DataOutput out = new DataOutputStream(stream);
WritableUtils.writeVInt(out, signerId.length);
out.write(signerId);
out.writeBoolean(hasTimestamp);
WritableUtils.writeVInt(out, signature.length);
out.write(signature);
switch (config.destination) {
case VALUE:
out.write(wrapped.value);
wrapped.value = stream.toByteArray();
break;
case COLUMN_VISIBILITY:
wrapped.colVis = wrapVisibility(wrapped.colVis, stream.toByteArray());
break;
case SEPARATE_TABLE:
wrapped.value = stream.toByteArray();
break;
default:
throw new UnsupportedOperationException();
}
} catch (IOException e) { // IO exceptions won't be thrown in practice as we are operating on in-memory streams.
throw new SignatureException(e);
}
return wrapped.toEntry();
}
/**
* Verify the given entry.
*
* @param entry
* entry with signature to verify.
* @return Verified entry with signature data removed.
* @throws SignatureException
* Thrown if signature verification fails.
*/
public Entry<Key,Value> verify(Entry<Key,Value> entry) {
return verify(entry, null);
}
/**
* Verify the given entry.
*
* @param entry
* entry to verify, potentially with the signature too.
* @param signedEntry
* If the signature is stored in an entry in another table, then this entry is that signature entry, otherwise it is null.
* @return Verified entry, with signature data removed as necessary.
* @throws SignatureException
* Thrown if signature verification fails.
*/
public Entry<Key,Value> verify(Entry<Key,Value> entry, Entry<Key,Value> signedEntry) {
checkArgument(entry != null, "entry is null");
checkArgument(!entry.getKey().isDeleted(), "cannot verify deleted entries");
if (config.destination == SignatureConfig.Destination.SEPARATE_TABLE) {
checkArgument(signedEntry != null, "signature is in a separate table, but signature entry is null");
} else {
checkArgument(signedEntry == null, "signature is not in a separate table, but separate signature entry given");
}
if (verifier == null) {
verifier = config.algorithm.getInstance(config.provider);
}
MutableEntry wrapped = new MutableEntry(entry);
ByteArrayInputStream stream;
switch (config.destination) {
case VALUE:
stream = new ByteArrayInputStream(wrapped.value);
break;
case COLUMN_VISIBILITY:
Pair<byte[],byte[]> signatureData = unwrapVisibility(new ColumnVisibility(wrapped.colVis));
wrapped.colVis = signatureData.getLeft();
stream = new ByteArrayInputStream(signatureData.getRight());
break;
case SEPARATE_TABLE:
stream = new ByteArrayInputStream(signedEntry.getValue().get());
break;
default:
throw new UnsupportedOperationException();
}
// Read the signature from the stream.
byte[] signature;
boolean hasTimestamp;
try {
DataInput in = new DataInputStream(stream);
byte[] signerId = new byte[WritableUtils.readVInt(in)];
in.readFully(signerId);
verifier.initVerify(keys.getVerifyingKey(signerId));
hasTimestamp = in.readBoolean();
signature = new byte[WritableUtils.readVInt(in)];
in.readFully(signature);
// If we wrote to the value, extract the original value.
if (config.destination == Destination.VALUE) {
wrapped.value = new byte[stream.available()];
in.readFully(wrapped.value);
}
} catch (IOException | InvalidKeyException e) { // IO exceptions won't be thrown in practice as we are operating on in-memory streams.
throw new SignatureException(e);
}
// Generate and verify the signature.
try {
verifier.update(wrapped.row);
verifier.update(wrapped.colF);
verifier.update(wrapped.colQ);
verifier.update(wrapped.colVis);
verifier.update(hasTimestamp ? (byte) 0 : (byte) 1);
if (hasTimestamp) {
verifier.update(Longs.toByteArray(wrapped.timestamp));
}
verifier.update(wrapped.value);
if (!verifier.verify(signature)) {
throw new SignatureException("invalid signature found");
}
} catch (java.security.SignatureException e) {
throw new SignatureException(e);
}
return wrapped.toEntry();
}
/**
* Wrap the visibility field.
* <p>
* The resulting field will have the value {@literal colVis = (<originalColVis>)|data}. If the original column visibility value is empty, then the deafult
* visibility value will be used in place of originalColVis.
*
* @param visibility
* The visibility data being wrapped.
* @param data
* data to wrap into visibility.
* @return Wrapped visibility.
*/
private static byte[] wrapVisibility(byte[] visibility, byte[] data) throws IOException {
ByteArrayOutputStream colVisStream = new ByteArrayOutputStream();
DataOutput colVisOut = new DataOutputStream(colVisStream);
colVisOut.writeByte((byte) '(');
colVisOut.write(visibility);
colVisOut.writeByte((byte) ')');
colVisOut.writeByte((byte) '|');
colVisOut.write(VisibilityEvaluator.escape(data, true));
return colVisStream.toByteArray();
}
/**
* Unwrap the visibility field.
* <p>
* Assumes the visibility field had previously been wrapped by a call to {@link #wrapVisibility(byte[], byte[])}. If the visibility is not in the form
*
*
* @param visibility
* The visibility data.
* @return The unwrapped visibility expression and data.
*/
private static Pair<byte[],byte[]> unwrapVisibility(ColumnVisibility visibility) {
ColumnVisibility.Node node = visibility.getParseTree();
if (node.getType() != ColumnVisibility.NodeType.OR || node.getChildren().size() != 2) {
throw new SignatureException("Invalid signature in the column visibility");
}
StringBuilder builder = new StringBuilder();
ColumnVisibility.stringify(node.getChildren().get(0), visibility.getExpression(), builder);
byte[] visibilityData = builder.toString().getBytes(VISIBILITY_CHARSET);
byte[] wrappedData = unescape(node.getChildren().get(1).getTerm(visibility.getExpression())).toArray();
return Pair.of(visibilityData, wrappedData);
}
/**
* Unescape a visibility term.
* <p>
* Pulled from {@link VisibilityEvaluator} (line 49). This method is not public and could not be used directly.
*
* @param auth
* Term to transform.
* @return The unescaped term.
*/
private static ByteSequence unescape(ByteSequence auth) {
int escapeCharCount = 0;
for (int i = 0; i < auth.length(); i++) {
byte b = auth.byteAt(i);
if (b == '"' || b == '\\') {
escapeCharCount++;
}
}
if (escapeCharCount > 0) {
if (escapeCharCount % 2 == 1) {
throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth);
}
byte[] unescapedCopy = new byte[auth.length() - escapeCharCount / 2];
int pos = 0;
for (int i = 0; i < auth.length(); i++) {
byte b = auth.byteAt(i);
if (b == '\\') {
i++;
b = auth.byteAt(i);
if (b != '"' && b != '\\') {
throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth);
}
} else if (b == '"') {
// should only see quote after a slash
throw new IllegalArgumentException("Illegal escape sequence in auth : " + auth);
}
unescapedCopy[pos++] = b;
}
return new ArrayByteSequence(unescapedCopy);
} else {
return auth;
}
}
}
@@ -0,0 +1,200 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static com.google.common.base.Preconditions.checkArgument;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import java.util.TreeMap;
import org.ini4j.Ini;
import org.ini4j.Profile.Section;
import com.google.common.base.Strings;
/**
* Configuration for signing entries.
*/
public final class SignatureConfig {
/**
* INI section to write data to.
*/
static final String SECTION_NAME = "Signature";
/**
* Where the signature should be written.
*/
public enum Destination {
VALUE("value"), COLUMN_VISIBILITY("colVis"), SEPARATE_TABLE("table");
/**
* Set of EntryField names mapped to the EntryField enum object. Used to speed up name lookups.
*/
private static final Map<String,Destination> destinations;
static {
destinations = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (Destination destination : values()) {
destinations.put(destination.name, destination);
}
}
/**
* A string representation of the Destination.
*/
private final String name;
/**
* Create an Destination with the given name.
*
* @param name
* Name of the entry field. Used in the configuration INI.
*/
Destination(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
/**
* Get the destination from its {@link String} representation.
* <p>
* This method is paired with {@link #toString()}, and will parse strings generated by that method.
*
* @param value
* Value to parse.
* @return Destination for the value.
*/
static Destination fromString(String value) {
Destination destination = destinations.get(value);
if (destination == null) {
throw new IllegalArgumentException("invalid name");
}
return destination;
}
}
/**
* The algorithm to use to sign entries.
*/
final ValueSigner algorithm;
/**
* The provider to use for creating the {@link java.security.Signature}.
*/
final String provider;
/**
* The destination where the signature is written to or read from.
*/
final Destination destination;
/**
* The table that will store the signatures.
*/
final String destinationTable;
/**
* Default visibility to use when wrapping data in the column visibility field.
*/
final byte[] defaultVisibility;
/**
* Create a configuration for signing entries.
*
* @param signer
* The algorithm to use to sign entries.
* @param provider
* The provider to use for creating the {@link java.security.Signature}.
* @param destination
* The destination where the signature is written to or read from.
* @param destinationTable
* The table that will store the signatures.
* @param defaultVisibility
* Default visibility to use when wrapping signature in a visibility field.
*/
SignatureConfig(ValueSigner signer, String provider, Destination destination, String destinationTable, byte[] defaultVisibility) {
checkArgument(signer != null, "signer is null");
checkArgument(destination != null, "destination is null");
if (destination == Destination.SEPARATE_TABLE) {
checkArgument(!Strings.isNullOrEmpty(destinationTable), "destination table is null or empty");
} else {
checkArgument(destinationTable == null, "destination table is not null, but not needed");
}
if (destination == Destination.COLUMN_VISIBILITY) {
checkArgument(defaultVisibility != null && defaultVisibility.length != 0, "defaultVisibility is null or empty, but needed");
} else {
checkArgument(defaultVisibility == null, "default is not null, but should be");
}
this.algorithm = signer;
this.provider = provider;
this.destination = destination;
this.destinationTable = destinationTable;
this.defaultVisibility = defaultVisibility;
}
/**
* Write the object to a Writer.
*
* @param out
* Stream to write object out to.
*/
public void write(Writer out) throws IOException {
Ini configIni = new Ini();
Section section = configIni.add(SECTION_NAME);
section.put("algorithm", algorithm.toString());
section.put("provider", provider);
section.put("destination", destination.toString());
if (destinationTable != null) {
section.put("table", destinationTable);
}
if (defaultVisibility != null) {
section.put("defaultVisibility", new String(defaultVisibility, VISIBILITY_CHARSET));
}
configIni.store(out);
}
/**
* Gets whether this config write to a separate signature table.
*/
public boolean isSignatureInSeparateTable() {
return this.destination == Destination.SEPARATE_TABLE;
}
/**
* Gets the algorithm for this signer.
* <p>
* The key supplied to the {@link EntrySigner} must match this algorithm.
*
* @return Algorithm Algorithm defined by this config.
*/
public ValueSigner getAlgorithm() {
return this.algorithm;
}
}
@@ -0,0 +1,128 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import java.io.IOException;
import java.io.Reader;
import org.ini4j.Ini;
import org.ini4j.Profile.Section;
import edu.mit.ll.pace.signature.SignatureConfig.Destination;
/**
* Builder for configuration of signer objects.
*/
public class SignatureConfigBuilder {
// Variables from SignatureConfig.
private ValueSigner signer;
private String provider;
private Destination destination;
private String destinationTable;
private byte[] defaultVisibility;
/**
* Set the algorithm to use to sign entries.
*
* @param signer
* The algorithm to use to sign entries.
* @return Builder.
*/
public SignatureConfigBuilder setSigner(ValueSigner signer) {
this.signer = signer;
return this;
}
/**
* Set the provider to use when creating the signature.
*
* @param provider
* The provider to use for creating the {@link java.security.Signature}. If null, will allow the system to select the appropriate provider.
* @return Builder.
*/
public SignatureConfigBuilder setProvider(String provider) {
this.provider = provider;
return this;
}
/**
* Set the destination for the signature.
*
* @param destination
* The destination where the signature is written to or read from.
* @return Builder.
*/
public SignatureConfigBuilder setDestination(Destination destination) {
this.destination = destination;
return this;
}
/**
* Set the destination table.
*
* @param destinationTable
* The table that will store the signatures.
* @return Builder.
*/
public SignatureConfigBuilder setDestinationTable(String destinationTable) {
this.destinationTable = destinationTable;
return this;
}
/**
* Set the default visibility.
*
* @param defaultVisibility
* Default visibility to use when wrapping data in the column visibility field.
* @return Builder.
*/
public SignatureConfigBuilder setDefaultVisibility(byte[] defaultVisibility) {
this.defaultVisibility = defaultVisibility;
return this;
}
/**
* Read the configuration from a Reader.
*
* @param in
* Stream to read from.
* @return builder.
*/
public SignatureConfigBuilder readFromFile(Reader in) throws IOException {
Ini configIni = new Ini(in);
Section section = configIni.get(SignatureConfig.SECTION_NAME);
setSigner(ValueSigner.fromString(section.get("algorithm")));
setDestination(section.containsKey("destination") ? Destination.fromString(section.get("destination")) : Destination.VALUE);
setProvider(section.get("provider")).setDestinationTable(section.get("table"));
setDefaultVisibility(section.containsKey("defaultVisibility") ? section.get("defaultVisibility").getBytes(VISIBILITY_CHARSET) : null);
return this;
}
/**
* Build the signature config.
*
* @return The build signature config.
*/
public SignatureConfig build() {
return new SignatureConfig(signer, provider, destination, destinationTable, defaultVisibility);
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
/**
* Runtime wrapper for exceptions that occur during signing.
*/
public class SignatureException extends RuntimeException {
private static final long serialVersionUID = 3546537933578657569L;
/**
* Constructs a new signature exception with null as its detail message.
*/
public SignatureException() {
super();
}
/**
* Constructs a new signature exception with the specified detail message.
*
* @param message
* the detail message. The detail message is saved for later retrieval by the {@link Throwable#getMessage()} method.
*/
public SignatureException(String message) {
super(message);
}
/**
* Constructs a new signature exception with the specified cause and a detail message of (cause==null ? null : cause.toString()) (which typically contains the
* class and detail message of cause). This constructor is useful for runtime exceptions that are little more than wrappers for other throwables.
*
* @param cause
* the cause (which is saved for later retrieval by the {@link Throwable#getCause()} method). (A null value is permitted, and indicates that the
* cause is nonexistent or unknown.)
*/
public SignatureException(Throwable cause) {
super(cause);
}
/**
* Constructs a new signature exception with the specified detail message and cause.
* <p>
* Note that the detail message associated with cause is not automatically incorporated in this runtime exception's detail message.
*
* @param message
* the detail message. The detail message is saved for later retrieval by the {@link Throwable#getMessage()} method.
* @param cause
* the cause (which is saved for later retrieval by the {@link Throwable#getCause()} method). (A null value is permitted, and indicates that the
* cause is nonexistent or unknown.)
*/
public SignatureException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,100 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static com.google.common.base.Preconditions.checkArgument;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Objects;
import edu.mit.ll.pace.IllegalKeyRequestException;
/**
* Interface for the key container.
*/
public interface SignatureKeyContainer {
/**
* Private key with an id.
*/
final class PrivateKeyWithId {
/**
* Private key.
*/
public final PrivateKey key;
/**
* Id associated with the private key.
*/
public final byte[] id;
/**
* Constructor.
*
* @param privateKey
* Private key.
* @param id
* Id.
*/
public PrivateKeyWithId(PrivateKey privateKey, byte[] id) {
checkArgument(privateKey != null, "key is null");
checkArgument(id != null, "id ßis null");
checkArgument(id.length != 0, "id is empty");
this.key = privateKey;
this.id = id;
}
@Override
public int hashCode() {
return Objects.hash(key, id);
}
@Override
public boolean equals(Object obj) {
if (null == obj || !(obj instanceof PrivateKeyWithId)) {
return false;
}
PrivateKeyWithId other = (PrivateKeyWithId) obj;
return key.equals(other.key) && Arrays.equals(id, other.id);
}
}
/**
* Get the signing key to use in signing entries.
*
* @return The signing key and an identifier used to retrieve the appropriate verifier key.
* @throws IllegalKeyRequestException
* User lacks the permissions to obtain the desired key.
*/
PrivateKeyWithId getSigningKey();
/**
* Get the verifier key for the given signature key.
*
* @param id
* Id of the signing key used to sign the entry.
* @return The verifier key to use to verify data signed by the identified signing key.
* @throws IllegalKeyRequestException
* User lacks the permissions to obtain the desired key.
*/
PublicKey getVerifyingKey(byte[] id);
}
@@ -0,0 +1,265 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Collection;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.client.BatchScanner;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.IteratorSetting.Column;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.sample.SamplerConfiguration;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.hadoop.io.Text;
import edu.mit.ll.pace.ItemProcessingIterator;
/**
* Scanner that checks signatures on entries.
*/
public final class SignedBatchScanner implements BatchScanner {
/**
* The verifier to use.
*/
private final EntrySigner verifier;
/**
* The underlying scanner used to retrieve data from Accumulo.
*/
private final BatchScanner valueScanner;
/**
* The scanner to use to retrieve signatures.
*/
private final BatchScanner signatureScanner;
/**
* Create an encrypted batch scanner.
*
* @param connector
* The connector for the Accumulo instance.
* @param tableName
* Name of the table to write to.
* @param authorizations
* The authorizations this user has for querying Accumulo.
* @param numQueryThreads
* Maximimum number of query threads to use for this scanner.
* @param signatureConfig
* Configuration for the decryption.
* @param keys
* Container with the keys to use for decryption.
*/
public SignedBatchScanner(Connector connector, String tableName, Authorizations authorizations, int numQueryThreads, SignatureConfig signatureConfig,
SignatureKeyContainer keys) throws TableNotFoundException {
checkArgument(connector != null, "connection is null");
checkArgument(tableName != null, "tableName is null");
checkArgument(authorizations != null, "authorizations is null");
checkArgument(signatureConfig != null, "config is null");
checkArgument(keys != null, "keys is null");
this.valueScanner = connector.createBatchScanner(tableName, authorizations, numQueryThreads);
this.verifier = new EntrySigner(signatureConfig, keys);
if (signatureConfig.destination == SignatureConfig.Destination.SEPARATE_TABLE) {
this.signatureScanner = connector.createBatchScanner(signatureConfig.destinationTable, authorizations, numQueryThreads);
} else {
this.signatureScanner = null;
}
}
/**
* {@inheritDoc}
* <p>
* Returns a {@link edu.mit.ll.pace.ItemProcessingIterator}. This can be used to get the unprocessed items, allowing them to be deleted by a regular accumulo
* writer.
*/
@Override
public ItemProcessingIterator<Entry<Key,Value>> iterator() {
if (signatureScanner != null) {
return new SignedExternalScannerIterator(valueScanner.iterator(), signatureScanner.iterator(), verifier, false);
} else {
return new SignedInlineScannerIterator(valueScanner.iterator(), verifier);
}
}
/**
* {@inheritDoc}
* <p>
* This iterator is applied to both the table and the signature table. Changing the value will also affect the signature.
*/
@Override
public void addScanIterator(IteratorSetting cfg) {
valueScanner.addScanIterator(cfg);
if (signatureScanner != null) {
signatureScanner.addScanIterator(cfg);
}
}
@Override
public void clearColumns() {
valueScanner.clearColumns();
if (signatureScanner != null) {
signatureScanner.clearColumns();
}
}
@Override
public void clearScanIterators() {
valueScanner.clearScanIterators();
if (signatureScanner != null) {
signatureScanner.clearScanIterators();
}
}
@Override
public void setRanges(Collection<Range> collection) {
valueScanner.setRanges(collection);
if (signatureScanner != null) {
signatureScanner.setRanges(collection);
}
}
@Override
public void close() {
valueScanner.close();
if (signatureScanner != null) {
signatureScanner.close();
}
}
@Override
public void fetchColumn(Column column) {
valueScanner.fetchColumn(column);
if (signatureScanner != null) {
signatureScanner.fetchColumn(column);
}
}
@Override
public void fetchColumn(Text colFam, Text colQual) {
valueScanner.fetchColumn(colFam, colQual);
if (signatureScanner != null) {
signatureScanner.fetchColumn(colFam, colQual);
}
}
@Override
public void fetchColumnFamily(Text col) {
valueScanner.fetchColumnFamily(col);
if (signatureScanner != null) {
signatureScanner.fetchColumnFamily(col);
}
}
@Override
public Authorizations getAuthorizations() {
return valueScanner.getAuthorizations();
}
@Override
public void setSamplerConfiguration(SamplerConfiguration samplerConfiguration) {
valueScanner.setSamplerConfiguration(samplerConfiguration);
if (signatureScanner != null) {
signatureScanner.setSamplerConfiguration(samplerConfiguration);
}
}
@Override
public SamplerConfiguration getSamplerConfiguration() {
return valueScanner.getSamplerConfiguration();
}
@Override
public void clearSamplerConfiguration() {
valueScanner.clearSamplerConfiguration();
if (signatureScanner != null) {
signatureScanner.clearSamplerConfiguration();
}
}
@Override
public void setBatchTimeout(long l, TimeUnit timeUnit) {
valueScanner.setBatchTimeout(l, timeUnit);
if (signatureScanner != null) {
signatureScanner.setBatchTimeout(l, timeUnit);
}
}
@Override
public long getBatchTimeout(TimeUnit timeUnit) {
return valueScanner.getBatchTimeout(timeUnit);
}
@Override
public void setClassLoaderContext(String s) {
valueScanner.setClassLoaderContext(s);
if (signatureScanner != null) {
signatureScanner.setClassLoaderContext(s);
}
}
@Override
public void clearClassLoaderContext() {
valueScanner.clearClassLoaderContext();
if (signatureScanner != null) {
signatureScanner.clearClassLoaderContext();
}
}
@Override
public String getClassLoaderContext() {
return valueScanner.getClassLoaderContext();
}
@Override
public long getTimeout(TimeUnit timeUnit) {
return valueScanner.getTimeout(timeUnit);
}
@Override
public void removeScanIterator(String iteratorName) {
valueScanner.removeScanIterator(iteratorName);
if (signatureScanner != null) {
signatureScanner.removeScanIterator(iteratorName);
}
}
@Override
public void updateScanIteratorOption(String iteratorName, String key, String value) {
valueScanner.updateScanIteratorOption(iteratorName, key, value);
if (signatureScanner != null) {
signatureScanner.updateScanIteratorOption(iteratorName, key, value);
}
}
@Override
public void setTimeout(long timeout, TimeUnit timeUnit) {
valueScanner.setTimeout(timeout, timeUnit);
if (signatureScanner != null) {
signatureScanner.setTimeout(timeout, timeUnit);
}
}
}
@@ -0,0 +1,160 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Map.Entry;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.MutationsRejectedException;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.data.ColumnUpdate;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Mutation;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.ColumnVisibility;
import edu.mit.ll.pace.internal.MutableEntry;
import edu.mit.ll.pace.signature.SignatureConfig.Destination;
/**
* Writes signed entries to Accumulo.
*/
public final class SignedBatchWriter implements BatchWriter {
/**
* A {@link BatchWriter} to use to write the data to Accumulo.
*/
private final BatchWriter tableWriter;
/**
* A {@link BatchWriter} to use to write the data to Accumulo when signatures are stored in a separate table.
*/
private final BatchWriter signatureTableWriter;
/**
* The {@link EntrySigner} that will handle signing for this instance.
*/
private final EntrySigner signer;
/**
* The {@link EntrySigner} that will handle signing for this instance.
*/
private final SignatureConfig signatureConfig;
/**
* Create an signed batch tableWriter.
*
* @param connector
* The connector for the Accumulo instance.
* @param tableName
* Name of the table to write to.
* @param batchConfig
* Configuration for a {@link BatchWriter}.
* @param signatureConfig
* Configuration for the signatures.
* @param keys
* Container with the keys to use for signatures.
*/
public SignedBatchWriter(Connector connector, String tableName, BatchWriterConfig batchConfig, SignatureConfig signatureConfig, SignatureKeyContainer keys)
throws TableNotFoundException {
checkArgument(connector != null, "connector is null");
checkArgument(tableName != null, "tableName is null");
checkArgument(signatureConfig != null, "signatureConfig is null");
checkArgument(keys != null, "keys is null");
this.tableWriter = connector.createBatchWriter(tableName, batchConfig);
this.signer = new EntrySigner(signatureConfig, keys);
this.signatureConfig = signatureConfig;
if (signatureConfig.destination == SignatureConfig.Destination.SEPARATE_TABLE) {
this.signatureTableWriter = connector.createBatchWriter(signatureConfig.destinationTable, batchConfig);
} else {
this.signatureTableWriter = null;
}
}
/**
* Signs the given mutation and then write it to Accumulo.
*
* @param mutation
* The mutation to sign.
*/
@Override
public void addMutation(Mutation mutation) throws MutationsRejectedException {
Mutation signedMutation = new Mutation(mutation.getRow());
// Sign the entries.
for (ColumnUpdate update : mutation.getUpdates()) {
if (update.isDeleted()) {
if (signatureConfig.destination == Destination.COLUMN_VISIBILITY) {
throw new IllegalArgumentException("cannot delete entries when the signature is stored in the column visibility");
}
if (update.hasTimestamp()) {
signedMutation.putDelete(update.getColumnFamily(), update.getColumnQualifier(), new ColumnVisibility(update.getColumnVisibility()),
update.getTimestamp());
} else {
signedMutation.putDelete(update.getColumnFamily(), update.getColumnQualifier(), new ColumnVisibility(update.getColumnVisibility()));
}
} else {
Entry<Key,Value> signedEntry = signer.sign(new MutableEntry(mutation.getRow(), update).toEntry(), update.hasTimestamp());
Key signedKey = signedEntry.getKey();
if (update.hasTimestamp()) {
signedMutation.put(signedKey.getColumnFamily(), signedKey.getColumnQualifier(), signedKey.getColumnVisibilityParsed(), signedKey.getTimestamp(),
signedEntry.getValue());
} else {
signedMutation.put(signedKey.getColumnFamily(), signedKey.getColumnQualifier(), signedKey.getColumnVisibilityParsed(), signedEntry.getValue());
}
}
}
// Write the signed mutations.
if (signatureTableWriter != null) {
tableWriter.addMutation(mutation);
signatureTableWriter.addMutation(signedMutation);
} else {
tableWriter.addMutation(signedMutation);
}
}
@Override
public void addMutations(Iterable<Mutation> iterable) throws MutationsRejectedException {
for (Mutation mutation : iterable) {
addMutation(mutation);
}
}
@Override
public void flush() throws MutationsRejectedException {
tableWriter.flush();
if (signatureTableWriter != null) {
signatureTableWriter.flush();
}
}
@Override
public void close() throws MutationsRejectedException {
tableWriter.close();
if (signatureTableWriter != null) {
signatureTableWriter.close();
}
}
}
@@ -0,0 +1,142 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.SortedMap;
import java.util.TreeMap;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.PartialKey;
import org.apache.accumulo.core.data.Value;
import edu.mit.ll.pace.ItemProcessingIterator;
/**
* Read signed Accumulo entries.
*/
final class SignedExternalScannerIterator implements ItemProcessingIterator<Entry<Key,Value>> {
/**
* The iterator with the Accumulo data.
*/
private final Iterator<Entry<Key,Value>> valueIterator;
/**
* The iterator with the signature data.
*/
private final Iterator<Entry<Key,Value>> signatureIterator;
/**
* The verifier to use.
*/
private final EntrySigner verifier;
/**
* Whether the signatures are in the same order as the values.
*/
private final boolean inOrder;
/**
* If we are processing out of order, then this map will store signatures that we have consumed, but for which the appropriate value is yet to be consumed.
*/
private final SortedMap<Key,Entry<Key,Value>> bufferedSignatures = new TreeMap<>();
/**
* Keep track of the encrypted entry that resulted in
*/
private Entry<Key,Value> unprocessedEntry = null;
/**
* Read signed Accumulo data where the signature is stored in a different entry than the value.
*
* @param valueIterator
* The iterator with the accumulo data.
* @param signatureIterator
* The iterator with the signature data.
* @param verifier
* The signer to use in verifying signatures.
* @param inOrder
* Whether the signatures are in the same order as the values.
*/
SignedExternalScannerIterator(Iterator<Entry<Key,Value>> valueIterator, Iterator<Entry<Key,Value>> signatureIterator, EntrySigner verifier, boolean inOrder) {
this.valueIterator = valueIterator;
this.signatureIterator = signatureIterator;
this.verifier = verifier;
this.inOrder = inOrder;
}
@Override
public boolean hasNext() {
return valueIterator.hasNext();
}
@Override
public Entry<Key,Value> next() {
unprocessedEntry = null;
// Get the next signature.
Entry<Key,Value> entry = valueIterator.next();
Entry<Key,Value> signature = bufferedSignatures.get(entry.getKey());
if (signature == null) {
while (true) {
if (!signatureIterator.hasNext()) {
throw new SignatureException("no signature found");
}
signature = signatureIterator.next();
// Timestamps might be slightly inconsistent, so don't include them in the compare. This requires that both of these tables be versioned.
// TODO: Check that the tables are versioned, and if not throw an exception.
int cmp = entry.getKey().compareTo(signature.getKey(), PartialKey.ROW_COLFAM_COLQUAL_COLVIS);
if (cmp == 0) { // Found the signature
break;
} else if (cmp < 0) { // Entry is before the next signature.
if (inOrder) {
throw new SignatureException("no signature found for entry");
} else {
bufferedSignatures.put(signature.getKey(), signature);
}
} else { // Entry is after the next signature.
if (!inOrder) {
bufferedSignatures.put(signature.getKey(), signature);
}
}
}
}
Entry<Key,Value> processedEntry = verifier.verify(entry, signature);
unprocessedEntry = entry;
return processedEntry;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public Entry<Key,Value> unprocessed() {
if (unprocessedEntry == null) {
throw new NoSuchElementException("next() has not been called");
}
return unprocessedEntry;
}
}
@@ -0,0 +1,88 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import edu.mit.ll.pace.ItemProcessingIterator;
/**
* Read signed Accumulo entries.
*/
final class SignedInlineScannerIterator implements ItemProcessingIterator<Entry<Key,Value>> {
/**
* The iterator with the Accumulo data.
*/
private final Iterator<Entry<Key,Value>> iterator;
/**
* The verifier to use.
*/
private final EntrySigner verifier;
/**
* Keep track of the encrypted entry that resulted in
*/
private Entry<Key,Value> unprocessedEntry = null;
/**
* Read signed Accumulo data.
* <p>
* This only works when the signature is stored in the same entry as the value.
*
* @param iterator
* The iterator with the accumulo data.
* @param verifier
* The signer to use in verifying signatures.
*/
SignedInlineScannerIterator(Iterator<Entry<Key,Value>> iterator, EntrySigner verifier) {
this.iterator = iterator;
this.verifier = verifier;
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public Entry<Key,Value> next() {
Entry<Key,Value> original = iterator.next();
Entry<Key,Value> processedEntry = verifier.verify(original);
unprocessedEntry = original;
return processedEntry;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public Entry<Key,Value> unprocessed() {
if (unprocessedEntry == null) {
throw new NoSuchElementException("next() has not been called");
}
return unprocessedEntry;
}
}
@@ -0,0 +1,323 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static com.google.common.base.Preconditions.checkArgument;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.IteratorSetting.Column;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.TableNotFoundException;
import org.apache.accumulo.core.client.sample.SamplerConfiguration;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.hadoop.io.Text;
import edu.mit.ll.pace.ItemProcessingIterator;
/**
* Accumulo Scanner that also checks signatures on entries.
*/
public final class SignedScanner implements Scanner {
/**
* The signer to use to verify signatures.
*/
private final EntrySigner verifier;
/**
* The underlying scanner used to retrieve data from Accumulo.
*/
private final Scanner valueScanner;
/**
* The scanner to use to retrieve signatures.
*/
private final Scanner signatureScanner;
/**
* Create an signed scanner.
*
* @param connector
* The connector for the Accumulo instance.
* @param tableName
* Name of the table to write to.
* @param authorizations
* The authorizations this user has for querying Accumulo.
* @param signatureConfig
* Configuration for the verification.
* @param keys
* Container with the keys to use for signatures.
*/
public SignedScanner(Connector connector, String tableName, Authorizations authorizations, SignatureConfig signatureConfig, SignatureKeyContainer keys)
throws TableNotFoundException {
checkArgument(connector != null, "connection is null");
checkArgument(tableName != null, "tableName is null");
checkArgument(authorizations != null, "authorizations is null");
checkArgument(signatureConfig != null, "config is null");
checkArgument(keys != null, "keys is null");
this.valueScanner = connector.createScanner(tableName, authorizations);
this.verifier = new EntrySigner(signatureConfig, keys);
if (signatureConfig.destination == SignatureConfig.Destination.SEPARATE_TABLE) {
this.signatureScanner = connector.createScanner(signatureConfig.destinationTable, authorizations);
} else {
this.signatureScanner = null;
}
}
/**
* {@inheritDoc}
* <p>
* Returns a {@link edu.mit.ll.pace.ItemProcessingIterator}. This can be used to get the unprocessed items, allowing them to be deleted by a regular accumulo
* writer.
*/
@Override
public ItemProcessingIterator<Entry<Key,Value>> iterator() {
if (signatureScanner != null) {
return new SignedExternalScannerIterator(valueScanner.iterator(), signatureScanner.iterator(), verifier, true);
} else {
return new SignedInlineScannerIterator(valueScanner.iterator(), verifier);
}
}
/**
* {@inheritDoc}
* <p>
* This iterator is applied to both the table and the signature table. Changing the value will also affect the signature.
*/
@Override
public void addScanIterator(IteratorSetting cfg) {
valueScanner.addScanIterator(cfg);
if (signatureScanner != null) {
signatureScanner.addScanIterator(cfg);
}
}
@Override
public void clearColumns() {
valueScanner.clearColumns();
if (signatureScanner != null) {
signatureScanner.clearColumns();
}
}
@Override
public void clearScanIterators() {
valueScanner.clearScanIterators();
if (signatureScanner != null) {
signatureScanner.clearScanIterators();
}
}
@Override
public void close() {
valueScanner.close();
if (signatureScanner != null) {
signatureScanner.close();
}
}
@Override
public void fetchColumn(Column column) {
valueScanner.fetchColumn(column);
if (signatureScanner != null) {
signatureScanner.fetchColumn(column);
}
}
@Override
public void fetchColumn(Text colFam, Text colQual) {
valueScanner.fetchColumn(colFam, colQual);
if (signatureScanner != null) {
signatureScanner.fetchColumn(colFam, colQual);
}
}
@Override
public void fetchColumnFamily(Text col) {
valueScanner.fetchColumnFamily(col);
if (signatureScanner != null) {
signatureScanner.fetchColumnFamily(col);
}
}
@Override
public Authorizations getAuthorizations() {
return valueScanner.getAuthorizations();
}
@Override
public void setSamplerConfiguration(SamplerConfiguration samplerConfiguration) {
valueScanner.setSamplerConfiguration(samplerConfiguration);
if (signatureScanner != null) {
signatureScanner.setSamplerConfiguration(samplerConfiguration);
}
}
@Override
public SamplerConfiguration getSamplerConfiguration() {
return valueScanner.getSamplerConfiguration();
}
@Override
public void clearSamplerConfiguration() {
valueScanner.clearSamplerConfiguration();
if (signatureScanner != null) {
signatureScanner.clearSamplerConfiguration();
}
}
@Override
public void setBatchTimeout(long l, TimeUnit timeUnit) {
valueScanner.setBatchTimeout(l, timeUnit);
if (signatureScanner != null) {
signatureScanner.setBatchTimeout(l, timeUnit);
}
}
@Override
public long getBatchTimeout(TimeUnit timeUnit) {
return valueScanner.getBatchTimeout(timeUnit);
}
@Override
public void setClassLoaderContext(String s) {
valueScanner.setClassLoaderContext(s);
if (signatureScanner != null) {
signatureScanner.setClassLoaderContext(s);
}
}
@Override
public void clearClassLoaderContext() {
valueScanner.clearClassLoaderContext();
if (signatureScanner != null) {
signatureScanner.clearClassLoaderContext();
}
}
@Override
public String getClassLoaderContext() {
return valueScanner.getClassLoaderContext();
}
@Override
public long getTimeout(TimeUnit timeUnit) {
return valueScanner.getTimeout(timeUnit);
}
@Override
public void removeScanIterator(String iteratorName) {
valueScanner.removeScanIterator(iteratorName);
if (signatureScanner != null) {
signatureScanner.removeScanIterator(iteratorName);
}
}
@Override
public void updateScanIteratorOption(String iteratorName, String key, String value) {
valueScanner.updateScanIteratorOption(iteratorName, key, value);
if (signatureScanner != null) {
signatureScanner.updateScanIteratorOption(iteratorName, key, value);
}
}
@Override
public void setTimeout(long timeout, TimeUnit timeUnit) {
valueScanner.setTimeout(timeout, timeUnit);
if (signatureScanner != null) {
signatureScanner.setTimeout(timeout, timeUnit);
}
}
@Override
public void disableIsolation() {
valueScanner.disableIsolation();
if (signatureScanner != null) {
signatureScanner.disableIsolation();
}
}
@Override
public void enableIsolation() {
valueScanner.enableIsolation();
if (signatureScanner != null) {
signatureScanner.enableIsolation();
}
}
@Override
public int getBatchSize() {
return valueScanner.getBatchSize();
}
@Override
public Range getRange() {
return valueScanner.getRange();
}
@Override
public long getReadaheadThreshold() {
return valueScanner.getReadaheadThreshold();
}
@Override
@SuppressWarnings("deprecation")
public int getTimeOut() {
return valueScanner.getTimeOut();
}
@Override
public void setBatchSize(int size) {
valueScanner.setBatchSize(size);
if (signatureScanner != null) {
signatureScanner.setBatchSize(size);
}
}
@Override
public void setRange(Range range) {
valueScanner.setRange(range);
if (signatureScanner != null) {
signatureScanner.setRange(range);
}
}
@Override
public void setReadaheadThreshold(long batches) {
valueScanner.setReadaheadThreshold(batches);
if (signatureScanner != null) {
signatureScanner.setReadaheadThreshold(batches);
}
}
@Override
@SuppressWarnings("deprecation")
public void setTimeOut(int timeOut) {
valueScanner.setTimeOut(timeOut);
if (signatureScanner != null) {
signatureScanner.setTimeOut(timeOut);
}
}
}
@@ -0,0 +1,123 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.Signature;
import java.util.Map;
import java.util.TreeMap;
/**
* Set of objects that can sign values.
*/
public enum ValueSigner {
RSA_PKCS1("RSA-PKCS1", "SHA256withRSA", "RSA"), RSA_PSS("RSA-PSS", "SHA256withRSA/PSS", "RSA"), DSA("DSA", "SHA256withDSA", "DSA"), ECDSA("ECDSA",
"SHA256withECDSA", "ECDSA");
/**
* Set of ValueSigner names mapped to the ValueSigner enum object. Used to speed up name lookups.
*/
private static final Map<String,ValueSigner> signers;
static {
signers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (ValueSigner signer : values()) {
signers.put(signer.name, signer);
}
}
/**
* Name of the signer.
*/
private final String name;
/**
* Algorithm used for the singer.
*/
private final String algorithm;
/**
* Algorithm that can be used to generate keys for use with this cipher.
*/
private final String keyGenerationAlgorithm;
/**
* Create the value signer.
*
* @param name
* Name of the algorithm.
* @param algorithm
* Algorithm used by this signer.
* @param keyGenerationAlgorithm
* Algorithm that can be used to generate keys for use with this cipher.
*/
ValueSigner(String name, String algorithm, String keyGenerationAlgorithm) {
this.name = name;
this.algorithm = algorithm;
this.keyGenerationAlgorithm = keyGenerationAlgorithm;
}
@Override
public String toString() {
return this.name;
}
/**
* Get the value signer from its {@link String} representation.
* <p>
* This method is paired with {@link #toString()}, and will parse strings generated by that method.
*
* @param value
* Value to parse.
* @return ValueSigner for the value.
*/
static ValueSigner fromString(String value) {
ValueSigner signer = signers.get(value);
if (signer == null) {
throw new IllegalArgumentException("invalid name");
}
return signer;
}
/**
* Get the algorithm that can be used to generate keys for use with this cipher.
*
* @return Key generation algorithm.
*/
public String getKeyGenerationAlgorithm() {
return keyGenerationAlgorithm;
}
/**
* Gets a new {@link Signature} instance for the signer.
*
* @param provider
* Provider to use when creating the instance, or null to allow the system to decide which provider to use.
* @return The created instance.
*/
Signature getInstance(String provider) {
try {
if (provider == null) {
return Signature.getInstance(algorithm);
} else {
return Signature.getInstance(algorithm, provider);
}
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
throw new SignatureException(e);
}
}
}
@@ -0,0 +1,29 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_GCM
useVisibility = true
[colFamily]
cipher = AES_GCM
useVisibility = true
[colQualifier]
cipher = AES_GCM
useVisibility = true
[value]
cipher = AES_GCM
useVisibility = true
@@ -0,0 +1,17 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[value]
cipher = AES_GCM
useVisibility = true
@@ -0,0 +1,29 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_row
[colFamily]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colFamily
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
[value]
cipher = AES_GCM
useVisibility = true
@@ -0,0 +1,16 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[Signature]
algorithm = DSA
@@ -0,0 +1,16 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[Signature]
algorithm = ECDSA
@@ -0,0 +1,16 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[Signature]
algorithm = RSA-PSS
@@ -0,0 +1,60 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import org.junit.Test;
/**
* Test {@link EntryField}.
*/
public final class EntryFieldTest {
@Test
public void validEnumTest() {
assertThat("should have seven values", 7, is(EntryField.values().length));
}
@Test
public void toStringTest() {
assertThat("toString should return correct value", "row", is(EntryField.ROW.toString()));
assertThat("toString should return correct value", "colFamily", is(EntryField.COLUMN_FAMILY.toString()));
assertThat("toString should return correct value", "colQualifier", is(EntryField.COLUMN_QUALIFIER.toString()));
assertThat("toString should return correct value", "colVisibility", is(EntryField.COLUMN_VISIBILITY.toString()));
assertThat("toString should return correct value", "timestamp", is(EntryField.TIMESTAMP.toString()));
assertThat("toString should return correct value", "delete", is(EntryField.DELETE.toString()));
assertThat("toString should return correct value", "value", is(EntryField.VALUE.toString()));
}
@Test
public void fromStringTest() {
assertThat("fromString should return correct enum value", EntryField.fromString("row"), is(EntryField.ROW));
assertThat("fromString should return correct enum value", EntryField.fromString("colFamily"), is(EntryField.COLUMN_FAMILY));
assertThat("fromString should return correct enum value", EntryField.fromString("colQualifier"), is(EntryField.COLUMN_QUALIFIER));
assertThat("fromString should return correct enum value", EntryField.fromString("colVisibility"), is(EntryField.COLUMN_VISIBILITY));
assertThat("fromString should return correct enum value", EntryField.fromString("timestamp"), is(EntryField.TIMESTAMP));
assertThat("fromString should return correct enum value", EntryField.fromString("delete"), is(EntryField.DELETE));
assertThat("fromString should return correct enum value", EntryField.fromString("value"), is(EntryField.VALUE));
}
@Test(expected = IllegalArgumentException.class)
public void fromStringFailureTest() {
EntryField.fromString("bad");
}
}
@@ -0,0 +1,112 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static edu.mit.ll.pace.test.TestUtils.wrap;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.isA;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.arrayContaining;
import static org.junit.Assert.assertThat;
import java.security.Security;
import javax.crypto.AEADBadTagException;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
* Test {@link AESValueEncryptor}.
*/
public class AESValueEncryptorTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
@BeforeClass
public static void registerBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void semanticEncryptionTest() throws Exception {
ValueEncryptorBase encryptor = ValueEncryptor.AES_CBC.getInstance(null);
byte[] plaintext = "1234".getBytes(VISIBILITY_CHARSET);
byte[] key = "aabbccddaabbccdd".getBytes(VISIBILITY_CHARSET);
byte[] ciphertext1 = encryptor.encrypt(key, plaintext);
assertThat("ciphertext should not contain the original plaintext", wrap(ciphertext1), is(not(arrayContaining(wrap(plaintext)))));
byte[] plaintext1 = encryptor.decrypt(key, ciphertext1);
assertThat("decrypting the ciphertext should give the plaintext", plaintext1, is(equalTo(plaintext)));
byte[] ciphertext2 = encryptor.encrypt(key, plaintext);
assertThat("ciphertext should be different when identical values are encrypted", ciphertext2, is(not(equalTo(ciphertext1))));
byte[] ciphertext3 = encryptor.encrypt(key, "1235".getBytes(VISIBILITY_CHARSET));
assertThat("ciphertext should be not be the same when different values are encrypted", ciphertext3, is(not(equalTo(ciphertext1))));
}
@Test
public void authenticatedEncryptionTest() throws Exception {
for (String provider : new String[] {null, "BC", "SunJCE"}) {
ValueEncryptorBase encryptor = ValueEncryptor.AES_GCM.getInstance(provider);
byte[] plaintext = "1234".getBytes(VISIBILITY_CHARSET);
byte[] key = "aabbccddaabbccddaabbccddaabbccdd".getBytes(VISIBILITY_CHARSET);
byte[] ciphertext1 = encryptor.encrypt(key, plaintext);
assertThat("ciphertext should not contain the original plaintext", wrap(ciphertext1), is(not(arrayContaining(wrap(plaintext)))));
byte[] plaintext1 = encryptor.decrypt(key, ciphertext1);
assertThat("decrypting the ciphertext should give the plaintext", plaintext1, is(equalTo(plaintext)));
byte[] ciphertext2 = encryptor.encrypt(key, plaintext);
assertThat("ciphertext should be different when identical values are encrypted", ciphertext2, is(not(equalTo(ciphertext1))));
byte[] ciphertext3 = encryptor.encrypt(key, "1235".getBytes(VISIBILITY_CHARSET));
assertThat("ciphertext should be not be the same when different values are encrypted", ciphertext3, is(not(equalTo(ciphertext1))));
}
}
@Test
public void authenticatedEncryptionMacFailureTest() throws Exception {
for (String provider : new String[] {null, "BC", "SunJCE"}) {
ValueEncryptorBase encryptor = ValueEncryptor.AES_GCM.getInstance(provider);
byte[] plaintext = "1234567890123457".getBytes(VISIBILITY_CHARSET);
byte[] key = "aabbccddaabbccddaabbccddaabbccdd".getBytes(VISIBILITY_CHARSET);
byte[] ciphertext = encryptor.encrypt(key, plaintext);
if (ciphertext[20] == 0) {
ciphertext[20] = (byte) 1;
} else {
ciphertext[20] = (byte) 0;
}
thrown.expect(EncryptionException.class);
thrown.expectCause(isA(AEADBadTagException.class));
encryptor.decrypt(key, ciphertext);
}
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static edu.mit.ll.pace.test.TestUtils.wrap;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.isA;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.arrayContaining;
import static org.junit.Assert.assertThat;
import org.cryptomator.siv.UnauthenticCiphertextException;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
* Test {@link DeterministicSIVValueEncryptor}.
*/
public class DeterministicSIVValueEncryptorTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void deterministicSivEncryptionTest() throws Exception {
ValueEncryptorBase encryptor = ValueEncryptor.AES_SIV_DETERMINISTIC.getInstance(null);
byte[] plaintext = "1234".getBytes(VISIBILITY_CHARSET);
byte[] key = "aabbccddaabbccddaabbccddaabbccdd".getBytes(VISIBILITY_CHARSET);
byte[] ciphertext1 = encryptor.encrypt(key, plaintext);
assertThat("ciphertext should not contain the original plaintext", wrap(ciphertext1), is(not(arrayContaining(wrap(plaintext)))));
byte[] plaintext1 = encryptor.decrypt(key, ciphertext1);
assertThat("decrypting the ciphertext should give the plaintext", plaintext1, is(equalTo(plaintext)));
byte[] ciphertext2 = encryptor.encrypt(key, plaintext);
assertThat("ciphertext should be the same when identical values are encrypted", ciphertext2, is(equalTo(ciphertext1)));
byte[] ciphertext3 = encryptor.encrypt(key, "1235".getBytes(VISIBILITY_CHARSET));
assertThat("ciphertext should be not be the same when different values are encrypted", ciphertext3, is(not(equalTo(ciphertext1))));
}
@Test
public void authenticatedEncryptionMacFailureTest() throws Exception {
ValueEncryptorBase encryptor = ValueEncryptor.AES_SIV_DETERMINISTIC.getInstance(null);
byte[] plaintext = "1234567890123457".getBytes(VISIBILITY_CHARSET);
byte[] key = "aabbccddaabbccddaabbccddaabbccdd".getBytes(VISIBILITY_CHARSET);
byte[] ciphertext = encryptor.encrypt(key, plaintext);
ciphertext[20] = (byte) 0;
thrown.expect(EncryptionException.class);
thrown.expectCause(isA(UnauthenticCiphertextException.class));
encryptor.decrypt(key, ciphertext);
}
}
@@ -0,0 +1,524 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.InputStreamReader;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.client.BatchScanner;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.sample.SamplerConfiguration;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.io.Text;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import edu.mit.ll.pace.internal.Utils;
import edu.mit.ll.pace.test.Matchers;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test {@link EncryptedBatchScanner}.
*/
public class EncryptedBatchScannerTest {
/**
* The default testing table.
*/
private final static String TEST_TABLE = "test";
/**
* Authorizations.
*/
private final static Authorizations authorizations = new Authorizations("admin", "secret", "top secret");
/**
* Encryption keys.
*/
private final static MockEncryptionKeyContainer KEYS = new MockEncryptionKeyContainer(Pair.of("AES_GCM", 1), Pair.of("searchable_row", 2), Pair.of(
"searchable_colFamily", 2), Pair.of("searchable_colQualifier", 2), Pair.of("secret", 1));
@Mock
private Connector mockConnector;
@Mock
private BatchScanner mockScanner;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Captor
public ArgumentCaptor<Collection<Range>> captor;
@Test
public void badConstructorTest() throws Exception {
try {
new EncryptedBatchScanner(null, TEST_TABLE, authorizations, 0, getConfig("encrypt-value.ini"), KEYS);
fail("null connector not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptedBatchScanner(mockConnector, null, authorizations, 0, getConfig("encrypt-value.ini"), KEYS);
fail("null table name not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptedBatchScanner(mockConnector, TEST_TABLE, null, 0, getConfig("encrypt-value.ini"), KEYS);
fail("null authorizations not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 0, null, KEYS);
fail("null crypto config not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 0, getConfig("encrypt-value.ini"), null);
fail("null key container not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void constructorTest() throws Exception {
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS);
verify(mockConnector).createBatchScanner(TEST_TABLE, authorizations, 1);
}
@Test
public void setRangesSemanticEncryptionTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("encrypt-key.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-key.ini"), KEYS);
assertThat("has correct number of elements", scanner, iterableWithSize(2));
scanner.setRanges(Collections.singletonList(new Range(new Text(new byte[] {1}))));
assertThat("has correct number of elements", scanner, iterableWithSize(1));
scanner.setRanges(Collections.singletonList(new Range(new Text(new byte[] {1}), new Text(new byte[] {5}))));
assertThat("has correct number of elements", scanner, iterableWithSize(2));
// Should not have been handled server side.
verify(mockScanner, times(2)).setRanges(captor.capture());
for (Collection<Range> ranges : captor.getAllValues()) {
assertThat("has the infinite range", ranges, hasSize(1));
assertThat("has the infinite range", ranges.iterator().next(), equalTo(new Range()));
}
}
@Test
public void setRangesSearchableTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("searchable-row.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("searchable-row.ini"), KEYS);
scanner.setRanges(Collections.singletonList(new Range(new Text(new byte[] {1}))));
assertThat("has correct number of elements", scanner, iterableWithSize(2));
verify(mockScanner).setRanges(captor.capture());
assertThat("correct number of ranges", captor.getValue(), hasSize(2));
}
@Test
public void setRangesException() throws Exception {
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-key.ini"), KEYS);
try {
scanner.setRanges(null);
fail("null ranges not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
scanner.setRanges(new ArrayList<>());
fail("empty ranges not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void fetchColumnFamilySemanticEncryptionTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("encrypt-key.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-key.ini"), KEYS);
assertThat("has correct number of elements", scanner, iterableWithSize(2));
scanner.fetchColumnFamily(new Text(new byte[] {2}));
assertThat("has correct number of elements", scanner, iterableWithSize(1));
scanner.fetchColumnFamily(new Text(new byte[] {6}));
assertThat("has correct number of elements", scanner, iterableWithSize(2));
// Should not have been handled server side.
verify(mockScanner, never()).fetchColumn(any());
}
@Test
public void fetchColumnFamilySearchableTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("searchable.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("searchable.ini"), KEYS);
scanner.fetchColumnFamily(new Text(new byte[] {2}));
assertThat("filtering is not happening client-side", scanner, iterableWithSize(2));
verify(mockScanner, times(2)).fetchColumnFamily(any());
}
@Test
public void fetchColumnFamilyException() throws Exception {
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-key.ini"), KEYS);
try {
scanner.fetchColumnFamily(null);
fail("null column family not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void fetchColumnSemanticEncryptionTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("encrypt-key.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-key.ini"), KEYS);
assertThat("has correct number of elements", scanner, iterableWithSize(2));
scanner.fetchColumn(new IteratorSetting.Column(new Text(new byte[] {2}), new Text(new byte[] {3})));
assertThat("has correct number of elements", scanner, iterableWithSize(1));
scanner.fetchColumn(new IteratorSetting.Column(new Text(new byte[] {6}), new Text(new byte[] {7})));
assertThat("has correct number of elements", scanner, iterableWithSize(2));
// Should not have been handled server side.
verify(mockScanner, never()).fetchColumn(any());
}
@Test
public void fetchColumnSearchableTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("searchable.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("searchable.ini"), KEYS);
scanner.fetchColumn(new Text(new byte[] {2}), new Text(new byte[] {3}));
assertThat("filtering is not happening client-side", scanner, iterableWithSize(2));
verify(mockScanner, times(4)).fetchColumn(any(), any());
}
@Test
public void fetchColumnPartiallySearchableTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("partially-searchable.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("partially-searchable.ini"), KEYS);
scanner.fetchColumn(new Text(new byte[] {2}), new Text(new byte[] {3}));
assertThat("filtering is partially happening client-side", scanner, iterableWithSize(1));
verify(mockScanner, times(2)).fetchColumnFamily(any());
}
@Test
public void fetchColumnException() throws Exception {
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-key.ini"), KEYS);
try {
scanner.fetchColumn(null);
fail("null column not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
scanner.fetchColumn(null, new Text(Utils.EMPTY));
fail("null column family not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
scanner.fetchColumn(new Text(Utils.EMPTY), null);
fail("null column qualifier not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void clearColumnsTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("encrypt-key.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-key.ini"), KEYS);
scanner.fetchColumn(new Text(new byte[] {2}), new Text(new byte[] {2}));
assertThat("has correct number of elements", scanner, iterableWithSize(0));
scanner.clearColumns();
assertThat("has correct number of elements", scanner, iterableWithSize(2));
}
@Test
public void iteratorTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("encrypt-value.ini"), KEYS);
List<Map.Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS);
assertThat("has correct number of elements", scanner, iterableWithSize(2));
Iterator<Entry<Key,Value>> iterator = scanner.iterator();
assertThat("correct item", iterator.next(), Matchers.equalTo(entry));
assertThat("correct item", iterator.next(), Matchers.equalTo(entry2));
}
@Test
public void clearScanIteratorsTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).clearScanIterators();
verify(mockScanner).clearScanIterators();
}
@Test
public void closeTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).close();
verify(mockScanner).close();
}
@Test
public void getAuthorizationsTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockScanner.getAuthorizations()).thenReturn(authorizations);
Authorizations auths = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).getAuthorizations();
verify(mockScanner).getAuthorizations();
assertThat("correct authorizations returned", auths, equalTo(authorizations));
}
@Test
public void setSamplerConfigurationTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
SamplerConfiguration config = new SamplerConfiguration("test");
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).setSamplerConfiguration(config);
verify(mockScanner).setSamplerConfiguration(config);
}
@Test
public void getSamplerConfigurationTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
SamplerConfiguration config = new SamplerConfiguration("test");
when(mockScanner.getSamplerConfiguration()).thenReturn(config);
SamplerConfiguration value = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS)
.getSamplerConfiguration();
verify(mockScanner).getSamplerConfiguration();
assertThat("correct config returned", value, equalTo(config));
}
@Test
public void clearSamplerConfigurationTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).clearSamplerConfiguration();
verify(mockScanner).clearSamplerConfiguration();
}
@Test
public void setBatchTimeoutTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).setBatchTimeout(5L, TimeUnit.DAYS);
verify(mockScanner).setBatchTimeout(5L, TimeUnit.DAYS);
}
@Test
public void getBatchTimeoutTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockScanner.getBatchTimeout(TimeUnit.DAYS)).thenReturn(5L);
long value = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).getBatchTimeout(TimeUnit.DAYS);
verify(mockScanner).getBatchTimeout(TimeUnit.DAYS);
assertThat("correct timeout returned", value, is(5L));
}
@Test
public void setClassLoaderContextTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).setClassLoaderContext("test");
verify(mockScanner).setClassLoaderContext("test");
}
@Test
public void clearClassLoaderContextTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).clearClassLoaderContext();
verify(mockScanner).clearClassLoaderContext();
}
@Test
public void getClassLoaderContextTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockScanner.getClassLoaderContext()).thenReturn("test");
String value = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).getClassLoaderContext();
verify(mockScanner).getClassLoaderContext();
assertThat("correct class loader context returned", value, is("test"));
}
@Test
public void addScanIteratorTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
IteratorSetting test = new IteratorSetting(10, "test", "test2");
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).addScanIterator(test);
verify(mockScanner).addScanIterator(test);
}
@Test
public void removeScanIteratorTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).removeScanIterator("test");
verify(mockScanner).removeScanIterator("test");
}
@Test
public void updateScanIteratorOptionTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).updateScanIteratorOption("test", "a", "b");
verify(mockScanner).updateScanIteratorOption("test", "a", "b");
}
@Test
public void setTimeoutTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).setTimeout(5L, TimeUnit.DAYS);
verify(mockScanner).setTimeout(5L, TimeUnit.DAYS);
}
@Test
public void getTimeoutTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockScanner.getTimeout(TimeUnit.DAYS)).thenReturn(5L);
Long value = new EncryptedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("encrypt-value.ini"), KEYS).getTimeout(TimeUnit.DAYS);
verify(mockScanner).getTimeout(TimeUnit.DAYS);
assertThat("correct timeout returned", value, is(5L));
}
/**
* Get an encryptor config.
*
* @param resource
* Resource file containing the configuration.
* @return EncryptionConfig.
*/
private EncryptionConfig getConfig(String resource) throws Exception {
return new EncryptionConfigBuilder().readFromFile(new InputStreamReader(TestUtils.getResourceAsStream(this.getClass(), resource))).build();
}
}
@@ -0,0 +1,248 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.data.ColumnUpdate;
import org.apache.accumulo.core.data.Mutation;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import edu.mit.ll.pace.internal.MutableEntry;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test {@link EncryptedBatchWriter}.
*/
public class EncryptedBatchWriterTest {
/**
* The default testing table.
*/
private final static String TEST_TABLE = "table";
/**
* Encryption keys.
*/
private final static MockEncryptionKeyContainer KEYS = new MockEncryptionKeyContainer(Pair.of("AES_GCM", 1), Pair.of("deterministic", 2));
@Mock
private Connector mockConnector;
@Mock
private BatchWriter mockWriter;
@Captor
private ArgumentCaptor<Mutation> captor;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
@Test
public void badConstructorTest() throws Exception {
try {
new EncryptedBatchWriter(null, TEST_TABLE, null, getConfig("encrypt-value.ini"), KEYS);
fail("null connector not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptedBatchWriter(mockConnector, null, null, getConfig("encrypt-value.ini"), KEYS);
fail("null table name not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, null, KEYS);
fail("null crypto config not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("encrypt-value.ini"), null);
fail("null key container not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void constructorTest() throws Exception {
BatchWriterConfig config = new BatchWriterConfig();
new EncryptedBatchWriter(mockConnector, TEST_TABLE, config, getConfig("encrypt-value.ini"), KEYS);
verify(mockConnector).createBatchWriter(TEST_TABLE, config);
}
@Test
public void addMutationTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("encrypt-value.ini"), KEYS);
EntryEncryptor encryptor = new EntryEncryptor(getConfig("encrypt-value.ini"), KEYS);
Mutation mutation = new Mutation("row".getBytes());
mutation.put("colF".getBytes(), "colQ".getBytes(), "val1".getBytes());
mutation.put("colF".getBytes(), "colQ".getBytes(), 0, "val2".getBytes());
writer.addMutation(mutation);
verify(mockWriter, times(2)).addMutation(captor.capture());
List<Mutation> mutations = captor.getAllValues();
assertThat("two mutations", mutations, iterableWithSize(2));
Mutation encrypted = mutations.get(0);
assertThat("row is unchanged", encrypted.getRow(), equalTo("row".getBytes()));
List<ColumnUpdate> updates = encrypted.getUpdates();
assertThat("has 1 update", updates, hasSize(1));
ColumnUpdate update = updates.get(0);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnQualifier(), equalTo("colQ".getBytes()));
assertThat("timestamp not set", update.hasTimestamp(), is(false));
assertThat("value is encrypted", update.getColumnFamily(), not(equalTo("val1".getBytes())));
MutableEntry decrypted = new MutableEntry(encryptor.decrypt(new MutableEntry("row".getBytes(), update).toEntry()));
assertThat("value was encrypted correctly", decrypted.value, equalTo("val1".getBytes()));
encrypted = mutations.get(1);
assertThat("row is unchanged", encrypted.getRow(), equalTo("row".getBytes()));
updates = encrypted.getUpdates();
assertThat("has 1 update", updates, hasSize(1));
update = updates.get(0);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnQualifier(), equalTo("colQ".getBytes()));
assertThat("timestamp is set", update.hasTimestamp(), is(true));
assertThat("timestamp is correct", update.getTimestamp(), is(0L));
assertThat("value is encrypted", update.getColumnFamily(), not(equalTo("val2".getBytes())));
decrypted = new MutableEntry(encryptor.decrypt(new MutableEntry("row".getBytes(), update).toEntry()));
assertThat("value was encrypted correctly", decrypted.value, equalTo("val2".getBytes()));
}
@Test
public void addMutationDeterministicTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("deterministic.ini"), KEYS);
Mutation mutation = new Mutation("row".getBytes());
mutation.put("colF1".getBytes(), "colQ".getBytes(), "val".getBytes());
mutation.put("colF2".getBytes(), "colQ".getBytes(), 0, "val".getBytes());
mutation.put("colF1".getBytes(), "colQ".getBytes(), 0, "val2".getBytes());
writer.addMutation(mutation);
verify(mockWriter, times(3)).addMutation(captor.capture());
Iterable<Mutation> mutations = captor.getAllValues();
assertThat("should have 2 mutations", mutations, iterableWithSize(3));
}
@Test
public void deleteTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("encrypt-entry.ini"), KEYS);
Mutation mutation = new Mutation("row".getBytes());
mutation.putDelete("colF1".getBytes(), "colQ".getBytes());
try {
writer.addMutation(mutation);
fail("encrypted deletes are not allowed on non-deterministically encrypted data");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void deleteDeterministicTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("deterministic.ini"), KEYS);
Mutation mutation = new Mutation("row".getBytes());
mutation.putDelete("colF1".getBytes(), "colQ".getBytes());
mutation.putDelete("colF2".getBytes(), "colQ".getBytes(), 0);
writer.addMutation(mutation);
verify(mockWriter, times(4)).addMutation(captor.capture());
Iterable<Mutation> mutations = captor.getAllValues();
assertThat("should have 4 mutations", mutations, iterableWithSize(4));
}
@Test
public void addMutationsTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("encrypt-value.ini"), KEYS);
List<Mutation> mutations = new ArrayList<>();
Mutation mutation = new Mutation("row");
mutation.put("colF", "colQ", "val");
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(1)).addMutation(any()); // 1 time
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(3)).addMutation(any()); // 1 + 2 times
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(6)).addMutation(any()); // 1 + 2 + 3 times
}
@Test
public void flushTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("encrypt-value.ini"), KEYS).flush();
verify(mockWriter).flush();
}
@Test
public void closeTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
new EncryptedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("encrypt-value.ini"), KEYS).close();
verify(mockWriter).close();
}
/**
* Get an encryptor config.
*
* @param resource
* Resource file containing the configuration.
* @return EncryptionConfig.
*/
private EncryptionConfig getConfig(String resource) throws Exception {
return new EncryptionConfigBuilder().readFromFile(new InputStreamReader(TestUtils.getResourceAsStream(this.getClass(), resource))).build();
}
}
@@ -0,0 +1,238 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.InputStreamReader;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.TreeSet;
import org.apache.accumulo.core.data.Column;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.io.Text;
import org.junit.Test;
import com.google.common.collect.Lists;
import edu.mit.ll.pace.internal.Utils;
import edu.mit.ll.pace.test.Matchers;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test {@link EncryptedScannerIterator}.
*/
public class EncryptedScannerIteratorTest {
/**
* Encryption keys.
*/
private final static MockEncryptionKeyContainer KEYS = new MockEncryptionKeyContainer(Pair.of("AES_GCM", 1));
@Test
public void hasNextTest() throws Exception {
EntryEncryptor encryptor = new EntryEncryptor(getConfig("config.ini"), KEYS);
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<Key,Value>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
entries.add(encryptor.encrypt(entry));
EncryptedScannerIterator iterator = new EncryptedScannerIterator(entries.iterator(), encryptor, Collections.singletonList(new Range()),
new TreeSet<Column>());
assertThat("has next item", iterator.hasNext(), is(true));
assertThat("has next item", iterator.hasNext(), is(true));
iterator.next();
assertThat("does not have a next item", iterator.hasNext(), is(false));
}
@Test
public void nextTest() throws Exception {
EntryEncryptor encryptor = new EntryEncryptor(getConfig("config.ini"), KEYS);
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<Key,Value>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
entries.add(encryptor.encrypt(entry));
EncryptedScannerIterator iterator = new EncryptedScannerIterator(entries.iterator(), encryptor, Collections.singletonList(new Range()),
new TreeSet<Column>());
assertThat("next item is correct", iterator.next(), Matchers.equalTo(entry));
try {
iterator.next();
fail("no items should be left");
} catch (NoSuchElementException e) { /* expected */}
}
@Test
public void hasNextThenNextTest() throws Exception {
EntryEncryptor encryptor = new EntryEncryptor(getConfig("config.ini"), KEYS);
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<Key,Value>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
entries.add(encryptor.encrypt(entry));
EncryptedScannerIterator iterator = new EncryptedScannerIterator(entries.iterator(), encryptor, Collections.singletonList(new Range()),
new TreeSet<Column>());
assertThat("hasNext is true", iterator.hasNext(), is(true));
assertThat("next item is correct", iterator.next(), Matchers.equalTo(entry));
}
@Test
public void matchRangeTest() throws Exception {
EntryEncryptor encryptor = new EntryEncryptor(getConfig("config.ini"), KEYS);
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(Utils.VISIBILITY_CHARSET), 0,
false, false), new Value(new byte[] {4}));
Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7}, "secret".getBytes(Utils.VISIBILITY_CHARSET),
0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
EncryptedScannerIterator iterator = new EncryptedScannerIterator(entries.iterator(), new EntryEncryptor(getConfig("config.ini"), KEYS),
Collections.singletonList(new Range()), new TreeSet<>());
assertThat("correct number of items", Lists.newArrayList(iterator), hasSize(2));
iterator = new EncryptedScannerIterator(entries.iterator(), new EntryEncryptor(getConfig("config.ini"), KEYS), new ArrayList<Range>(),
new TreeSet<Column>());
assertThat("correct number of items", Lists.newArrayList(iterator), hasSize(2));
iterator = getIteratorForRange(entries, (byte) 1);
assertThat("correct number of items", Lists.newArrayList(iterator), hasSize(1));
iterator = getIteratorForRange(entries, (byte) 3);
assertThat("correct number of items", Lists.newArrayList(iterator), hasSize(0));
}
private EncryptedScannerIterator getIteratorForRange(List<Entry<Key,Value>> entries, byte row) throws Exception {
return new EncryptedScannerIterator(entries.iterator(), new EntryEncryptor(getConfig("config.ini"), KEYS), Collections.singletonList(new Range(new Text(
new byte[] {row}))), new TreeSet<Column>());
}
@Test
public void matchColumnFilters() throws Exception {
EntryEncryptor encryptor = new EntryEncryptor(getConfig("config.ini"), KEYS);
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(Utils.VISIBILITY_CHARSET), 0,
false, false), new Value(new byte[] {4}));
Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {2}, new byte[] {7}, "secret".getBytes(Utils.VISIBILITY_CHARSET),
0, false, false), new Value(new byte[] {8}));
entries.add(encryptor.encrypt(entry));
entries.add(encryptor.encrypt(entry2));
EncryptedScannerIterator iterator = new EncryptedScannerIterator(entries.iterator(), new EntryEncryptor(getConfig("config.ini"), KEYS),
Collections.singletonList(new Range()), new TreeSet<>());
assertThat("correct number of items", Lists.newArrayList(iterator), hasSize(2));
iterator = getIteratorForColumn(entries, new byte[] {2}, null);
assertThat("correct number of items", Lists.newArrayList(iterator), hasSize(2));
iterator = getIteratorForColumn(entries, new byte[] {3}, null);
assertThat("correct number of items", Lists.newArrayList(iterator), hasSize(0));
iterator = getIteratorForColumn(entries, new byte[] {2}, new byte[] {7});
assertThat("correct number of items", Lists.newArrayList(iterator), hasSize(1));
}
private EncryptedScannerIterator getIteratorForColumn(List<Entry<Key,Value>> entries, byte[] colF, byte[] colQ) throws Exception {
TreeSet<Column> columns = new TreeSet<>();
columns.add(new Column(colF, colQ, null));
return new EncryptedScannerIterator(entries.iterator(), new EntryEncryptor(getConfig("config.ini"), KEYS), Collections.singletonList(new Range()), columns);
}
@Test
public void unprocessedTest() throws Exception {
EntryEncryptor encryptor = new EntryEncryptor(getConfig("config.ini"), KEYS);
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<Key,Value>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
entries.add(encryptor.encrypt(entry));
EncryptedScannerIterator iterator = new EncryptedScannerIterator(entries.iterator(), encryptor, Collections.singletonList(new Range()),
new TreeSet<Column>());
iterator.next();
assertThat("unprocessed item is correct", iterator.unprocessed(), Matchers.equalTo(entries.get(0)));
}
@Test
public void unprocessedException() throws Exception {
EntryEncryptor encryptor = new EntryEncryptor(getConfig("config.ini"), KEYS);
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<Key,Value>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
entries.add(encryptor.encrypt(entry));
EncryptedScannerIterator iterator = new EncryptedScannerIterator(entries.iterator(), encryptor, Collections.singletonList(new Range()), new TreeSet<>());
try {
iterator.unprocessed();
fail("cannot call unprocessed before calling next()");
} catch (NoSuchElementException e) { /* expected */}
try {
iterator.hasNext();
iterator.unprocessed();
fail("cannot call unprocessed before calling next()");
} catch (NoSuchElementException e) { /* expected */}
}
@Test
public void removeException() throws Exception {
EntryEncryptor encryptor = new EntryEncryptor(getConfig("config.ini"), KEYS);
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<Key,Value>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
entries.add(encryptor.encrypt(entry));
EncryptedScannerIterator iterator = new EncryptedScannerIterator(entries.iterator(), encryptor, Collections.singletonList(new Range()), new TreeSet<>());
try {
iterator.remove();
fail("remove not supported");
} catch (UnsupportedOperationException e) { /* expected */}
}
/**
* Get an encryptor config.
*
* @param resource
* Resource file containing the configuration.
* @return EncryptionConfig.
*/
private EncryptionConfig getConfig(String resource) throws Exception {
return new EncryptionConfigBuilder().readFromFile(new InputStreamReader(TestUtils.getResourceAsStream(this.getClass(), resource))).build();
}
}
@@ -0,0 +1,201 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static edu.mit.ll.pace.test.Matchers.hasSameFieldsAs;
import static edu.mit.ll.pace.test.TestUtils.getResourceAsStream;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.TreeMap;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.TypeSafeMatcher;
import org.ini4j.Ini;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import edu.mit.ll.pace.EntryField;
import edu.mit.ll.pace.test.Matchers;
/**
* Test {@link EncryptionConfig}.
*/
public class EncryptionConfigTest {
/**
* In memory representation of the config.
*/
private static final EncryptionConfig config1 = new EncryptionConfigBuilder()
.addFieldEncryptorConfig(
new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_CBC).setProvider("BC").setKeyLength(16).setEncryptUsingVisibility(false)
.setKeyId("keyId").setDestination(EntryField.COLUMN_FAMILY)
.setSources(ImmutableSortedSet.of(EntryField.ROW, EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER)).build())
.addFieldEncryptorConfig(
new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_SIV_DETERMINISTIC).setKeyLength(32).setEncryptUsingVisibility(false)
.setKeyId("keyId2").setDestination(EntryField.COLUMN_QUALIFIER)
.setSources(ImmutableSortedSet.of(EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER)).build())
.addFieldEncryptorConfig(
new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_GCM).setKeyLength(24).setEncryptUsingVisibility(true).setKeyId("AES_GCM")
.setDestination(EntryField.VALUE).setSources(ImmutableSortedSet.of(EntryField.VALUE)).build()).build();
@Rule
public TemporaryFolder testFolder = new TemporaryFolder();
@Test
public void constructorExceptionTests() throws IOException {
try {
new EncryptionConfigBuilder().setFieldEncryptorConfigs(null).build();
fail("null fieldEncryptionConfigs should not be allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptionConfigBuilder().readFromFile(getReader("emptyConfig.ini")).build();
fail("empty fieldEncryptionConfigs should not be allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new EncryptionConfigBuilder().setFieldEncryptorConfigs(
ImmutableList.of(
new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_GCM).setProvider("BC").setKeyLength(32).setEncryptUsingVisibility(true)
.setKeyId("AES_GCM").setDestination(EntryField.VALUE).setSources(ImmutableSortedSet.of(EntryField.VALUE)).build(),
new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_GCM).setProvider("BC").setKeyLength(32).setEncryptUsingVisibility(true)
.setKeyId("AES_GCM").setDestination(EntryField.VALUE).setSources(ImmutableSortedSet.of(EntryField.VALUE)).build())).build();
fail("duplicate fieldEncryptionConfigs should not be allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void readTest() throws IOException {
EncryptionConfig actual = new EncryptionConfigBuilder().readFromFile(getReader("config1.ini")).build();
assertThat("reading the ini produces the correct configuration", actual, is(equalTo(config1)));
}
@Test
public void writeTest() throws IOException {
File iniFile = testFolder.newFile();
FileWriter out = new FileWriter(iniFile);
config1.write(out);
out.close();
Ini actual = new Ini(new FileReader(iniFile));
Ini expected = getIni("config1.ini");
assertThat("reading the ini produces the correct configuration", actual, is(Matchers.equalTo(expected)));
}
@Test
public void writeReadTest() throws IOException {
File ini = testFolder.newFile();
FileWriter out = new FileWriter(ini);
config1.write(out);
out.close();
FileReader in = new FileReader(ini);
EncryptionConfig config2 = new EncryptionConfigBuilder().readFromFile(in).build();
in.close();
assertThat("writing then reading the config should produce an equivalent configuration", config2, is(equalTo(config1)));
}
/**
* Gets a hamcrest matcher that tests whether two {@link EncryptionConfig} are equal.
*
* @param expectedValue
* The expected value to core against.
* @return Method safe matcher that will core field equality.
*/
@Factory
private static TypeSafeMatcher<EncryptionConfig> equalTo(final EncryptionConfig expectedValue) {
final TreeMap<EntryField,TypeSafeMatcher<FieldEncryptorConfig>> configMatchers = new TreeMap<>();
for (FieldEncryptorConfig config : expectedValue.fieldEncryptorConfigs) {
configMatchers.put(config.destination, hasSameFieldsAs(config));
}
return new TypeSafeMatcher<EncryptionConfig>() {
@Override
protected boolean matchesSafely(EncryptionConfig actualValue) {
if (actualValue.fieldEncryptorConfigs.size() != configMatchers.size()) {
return false;
}
for (FieldEncryptorConfig config : actualValue.fieldEncryptorConfigs) {
TypeSafeMatcher<FieldEncryptorConfig> matcher = configMatchers.get(config.destination);
if (matcher == null || !matcher.matches(config)) {
return false;
}
}
return true;
}
@Override
protected void describeMismatchSafely(EncryptionConfig actualValue, Description description) {
description.appendText("was [\n");
for (FieldEncryptorConfig config : actualValue.fieldEncryptorConfigs) {
description.appendText("\t\t{").appendText(ReflectionToStringBuilder.toString(config, ToStringStyle.SIMPLE_STYLE)).appendText("}\n");
}
description.appendText("\t]");
}
@Override
public void describeTo(Description description) {
description.appendText("[\n");
for (FieldEncryptorConfig config : expectedValue.fieldEncryptorConfigs) {
description.appendText("\t\t{").appendText(ReflectionToStringBuilder.toString(config, ToStringStyle.SIMPLE_STYLE)).appendText("}\n");
}
description.appendText("\t]");
}
};
}
/**
* Get the requested INI.
*
* @param resource
* INI file to get.
* @return The INI file.
*/
private Ini getIni(String resource) throws IOException {
return new Ini(getResourceAsStream(this.getClass(), resource));
}
/**
* Get the requested INI.
*
* @param resource
* INI file to get.
* @return The INI file.
*/
private Reader getReader(String resource) throws IOException {
return new InputStreamReader(getResourceAsStream(this.getClass(), resource));
}
}
@@ -0,0 +1,422 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static edu.mit.ll.pace.internal.Utils.EMPTY;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static edu.mit.ll.pace.test.TestUtils.getResourceAsStream;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.InputStreamReader;
import java.security.Security;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map.Entry;
import org.apache.accumulo.core.data.Column;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.hadoop.io.Text;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import edu.mit.ll.pace.encryption.EntryEncryptor.ColumnFilterResult;
import edu.mit.ll.pace.internal.MutableEntry;
import edu.mit.ll.pace.test.Matchers;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test {@link EntryEncryptor}.
*/
public class EntryEncryptorTest {
/**
* The mock key container, filled with several keys.
*/
private static EncryptionKeyContainer keys = new MockEncryptionKeyContainer(Pair.of("searchable_row", 2), Pair.of("searchable_colFamily", 2), Pair.of(
"searchable_colQualifier", 2), Pair.of("searchable_row", 2), Pair.of("secret", 2), Pair.of("top secret", 1), Pair.of("admin", 1), Pair.of("AES_GCM", 2),
Pair.of("deterministic", 2));
/**
* Ensure that BouncyCastleProvider is registered.
*/
@BeforeClass
public static void setup() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void constructorExceptionTests() throws Exception {
try {
new EntryEncryptor(null, keys);
fail("config must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getEncryptor("encrypt-value.ini", null); // Implicitly tests a good config, but a null key set.
fail("keys must not be null");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void nullEntryTest() throws Exception {
EntryEncryptor encryptor = getEncryptor("encrypt-entry.ini");
try {
encryptor.encrypt(null);
fail("cannot call encrypt with a null value");
} catch (IllegalArgumentException e) { /* expected */}
try {
encryptor.decrypt(null);
fail("cannot call decrypt with a null value");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void encryptDecryptTest() throws Exception {
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET),
(long) 5, false), new Value(new byte[] {6}));
Entry<Key,Value> encrypted, decrypted;
for (EntryEncryptor encryptor : getEncryptors()) { // Test all the various configs.
encrypted = encryptor.encrypt(entry);
assertThat("encrypting should change the data", encrypted, not(Matchers.equalTo(entry)));
decrypted = encryptor.decrypt(encrypted);
assertThat("encrypting than decrypting should return the original value", decrypted, Matchers.equalTo(entry));
}
}
@Test
public void canBeDeletedServerSideTest() throws Exception {
assertThat("non-encrypted key can be updated", getEncryptor("encrypt-value.ini").canBeDeleteServerSide(), is(true));
assertThat("non-deterministically encrypted key cannot be updated", getEncryptor("encrypt-entry.ini").canBeDeleteServerSide(), is(false));
assertThat("searchable key can be updated", getEncryptor("searchable.ini").canBeDeleteServerSide(), is(true));
}
@Test
public void getDeleteKeysTest() throws Exception {
EntryEncryptor encryptor = getEncryptor("deterministic.ini");
MutableEntry searchKey = new MutableEntry(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), (long) 5, false));
Collection<Key> keys = encryptor.getDeleteKeys(searchKey.toKey());
assertThat("has correct number of delete keys", keys, hasSize(2));
for (Key deleteKey : keys) {
MutableEntry key = new MutableEntry(deleteKey);
assertThat("row is encrypted", key.row, not(equalTo(searchKey.row)));
assertThat("colF is zeroes", key.colF, equalTo(EMPTY));
assertThat("colQ is plaintext", key.colQ, equalTo(searchKey.colQ));
assertThat("colQ is plaintext", key.colVis, equalTo(searchKey.colVis));
assertThat("colQ is plaintext", key.timestamp, equalTo(searchKey.timestamp));
assertThat("delete is true", key.delete, is(true));
}
}
@Test
public void getDeleteKeysExceptionTest() throws Exception {
EntryEncryptor encryptor = getEncryptor("encrypt-entry.ini");
try {
encryptor.getDeleteKeys(null);
fail("cannot call delete keys with a null value");
} catch (IllegalArgumentException e) { /* expected */}
encryptor = getEncryptor("encrypt-entry.ini");
try {
encryptor.getDeleteKeys(new Key());
fail("cannot call delete keys when the encryptor does not allow server side delete");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void getColumnFamilyFilterTest() throws Exception {
byte[] columnFamily = new byte[] {2};
ColumnFilterResult result = getEncryptor("encrypt-value.ini").getColumnFamilyFilter(new Text(columnFamily));
assertThat("unencrypted column is searchable server side", result.serverSideFilters, hasSize(1));
assertThat("unencrypted column is searchable server side", result.serverSideFilters, containsInAnyOrder(new Column(columnFamily, null, null)));
assertThat("unencrypted column does not need client-side search", result.needsClientSideFiltering, is(false));
result = getEncryptor("zeroed3.ini").getColumnFamilyFilter(new Text(columnFamily));
assertThat("zeroed column is searchable server side", result.serverSideFilters, hasSize(1));
assertThat("zeroed column is searchable server side", result.serverSideFilters, containsInAnyOrder(new Column(EMPTY, null, null)));
assertThat("zeroed column needs client-side search", result.needsClientSideFiltering, is(true));
result = getEncryptor("encrypt-entry.ini").getColumnFamilyFilter(new Text(columnFamily));
assertThat("non-deterministically encrypted column is not searchable server side", result.serverSideFilters, hasSize(0));
assertThat("non-deterministically encrypted column needs client-side search", result.needsClientSideFiltering, is(true));
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(EMPTY, columnFamily, EMPTY, EMPTY, (long) 0, false), new Value(EMPTY));
MutableEntry encrypted1 = new MutableEntry(getEncryptor("filterable.ini",
new MockEncryptionKeyContainer(Pair.of("searchable_colFamily", 1), Pair.of("searchable_colQualifier", 1))).encrypt(entry));
MutableEntry encrypted2 = new MutableEntry(getEncryptor("filterable.ini",
new MockEncryptionKeyContainer(Pair.of("searchable_colFamily", 2), Pair.of("searchable_colQualifier", 2))).encrypt(entry));
result = getEncryptor("filterable.ini").getColumnFamilyFilter(new Text(columnFamily));
assertThat("deterministically encrypted column is searchable server side", result.serverSideFilters, hasSize(2));
assertThat("deterministically encrypted column is searchable server side", result.serverSideFilters,
containsInAnyOrder(new Column(encrypted1.colF, null, null), new Column(encrypted2.colF, null, null)));
assertThat("deterministically column does not needs client-side search", result.needsClientSideFiltering, is(false));
}
@Test
public void getColumnFamilyFilterExceptionTest() throws Exception {
EntryEncryptor encryptor = getEncryptor("encrypt-entry.ini");
try {
encryptor.getColumnFamilyFilter(null);
fail("cannot call filter with a null value");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void getColumnFilterTest() throws Exception {
byte[] columnFamily = new byte[] {2};
byte[] columnQualifier = new byte[] {3};
// Unencrypted column qualifier
ColumnFilterResult result = getEncryptor("encrypt-value.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("unencrypted column is searchable server side", result.serverSideFilters, hasSize(1));
assertThat("unencrypted column is searchable server side", result.serverSideFilters, containsInAnyOrder(new Column(columnFamily, columnQualifier, null)));
assertThat("unencrypted column does not need client-side search", result.needsClientSideFiltering, is(false));
// Zeroed column qualifier
result = getEncryptor("zeroed1.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("zeroed column is searchable server side", result.serverSideFilters, hasSize(2));
assertThat("zeroed column needs client-side search", result.needsClientSideFiltering, is(false));
result = getEncryptor("zeroed2.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("zeroed column is searchable server side", result.serverSideFilters, hasSize(2));
assertThat("zeroed column needs client-side search", result.needsClientSideFiltering, is(true));
result = getEncryptor("zeroed3.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("zeroed column is searchable server side", result.serverSideFilters, hasSize(2));
assertThat("zeroed column needs client-side search", result.needsClientSideFiltering, is(false));
result = getEncryptor("zeroed4.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("zeroed column is searchable server side", result.serverSideFilters, hasSize(2));
assertThat("zeroed column needs client-side search", result.needsClientSideFiltering, is(true));
result = getEncryptor("zeroed5.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("zeroed column is searchable server side", result.serverSideFilters, hasSize(1));
assertThat("zeroed column needs client-side search", result.needsClientSideFiltering, is(true));
// Non-deterministic encrypted column qualifier
result = getEncryptor("encrypt-entry.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("non-deterministically encrypted column is not searchable server side", result.serverSideFilters, hasSize(0));
assertThat("non-deterministically encrypted column needs client-side search", result.needsClientSideFiltering, is(true));
result = getEncryptor("filterable2.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("non-deterministically encrypted column is not searchable server side", result.serverSideFilters, hasSize(2));
assertThat("non-deterministically encrypted column needs client-side search", result.needsClientSideFiltering, is(false));
// Deterministically encrypted column qualifier
result = getEncryptor("filterable.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("deterministically encrypted column is searchable server side", result.serverSideFilters, hasSize(4));
assertThat("deterministically column does not needs client-side search", result.needsClientSideFiltering, is(false));
result = getEncryptor("filterable3.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("deterministically encrypted column is searchable server side", result.serverSideFilters, hasSize(2));
assertThat("deterministically column does not needs client-side search", result.needsClientSideFiltering, is(false));
result = getEncryptor("filterable4.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("deterministically encrypted column is searchable server side", result.serverSideFilters, hasSize(2));
assertThat("deterministically column does not needs client-side search", result.needsClientSideFiltering, is(true));
result = getEncryptor("filterable5.ini").getColumnFilter(new Text(columnFamily), new Text(columnQualifier));
assertThat("deterministically encrypted column is searchable server side", result.serverSideFilters, hasSize(0));
assertThat("deterministically column does not needs client-side search", result.needsClientSideFiltering, is(true));
}
@Test
public void getColumnFilterExceptionTest() throws Exception {
EntryEncryptor encryptor = getEncryptor("encrypt-entry.ini");
try {
encryptor.getColumnFilter(null, new Text());
fail("cannot call filter with a null value");
} catch (IllegalArgumentException e) { /* expected */}
try {
encryptor.getColumnFilter(new Text(), null);
fail("cannot call filter with a null value");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void transformRangeTest() throws Exception {
// only value encrypted
Range range = new Range("A");
Collection<Range> serverSideRanges = new ArrayList<>();
boolean result = getEncryptor("encrypt-value.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(1));
assertThat("should be filtered server side", serverSideRanges, contains(range));
assertThat("should not be filtered client side", result, is(false));
// all fields encrypted
range = new Range("A");
serverSideRanges.clear();
result = getEncryptor("encrypt-entry.ini", keys).transformRange(range, serverSideRanges);
assertThat("should not be filtered server side", serverSideRanges, hasSize(1));
assertThat("should not be filtered server side", serverSideRanges, contains(new Range()));
assertThat("should be filtered client side", result, is(true));
// infinite range
range = new Range();
serverSideRanges.clear();
result = getEncryptor("searchable.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(1));
assertThat("should be filtered server side", serverSideRanges, contains(range));
assertThat("should not be filtered client side", result, is(false));
// single value search
range = new Range(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 0, false), new Key(new byte[] {1},
new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 0, false));
serverSideRanges.clear();
result = getEncryptor("searchable.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(8)); // 2 different key versions for each of the three search fields.
assertThat("should not be filtered client side", result, is(false));
// single value search, change unimportant variables
range = new Range(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 1, false), new Key(new byte[] {1},
new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 0, true));
serverSideRanges.clear();
result = getEncryptor("searchable.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(8)); // 2 different key versions for each of the three search fields.
assertThat("should not be filtered client side", result, is(false));
// range search, one searchable field
range = new Range(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 0, false), new Key(new byte[] {1},
new byte[] {4}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 0, false));
serverSideRanges.clear();
result = getEncryptor("searchable.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(2)); // 2 different key versions for the one searchable field.
assertThat("should not be filtered client side", result, is(true));
// range search, two searchable fields
range = new Range(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 0, false), new Key(new byte[] {1},
new byte[] {2}, new byte[] {4}, "secret".getBytes(VISIBILITY_CHARSET), 0, false));
serverSideRanges.clear();
result = getEncryptor("searchable.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(4)); // 2 different key versions for the two searchable field.
assertThat("should not be filtered client side", result, is(true));
// range search, involves infinite key
range = new Range(null, new Key(new byte[] {1}, new byte[] {2}, new byte[] {4}, "secret".getBytes(VISIBILITY_CHARSET), 0, false));
serverSideRanges.clear();
result = getEncryptor("searchable2.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(1)); // a range that is the first part of the search.
assertThat("should not be filtered client side", result, is(true));
range = new Range(new Key(new byte[] {1}, new byte[] {2}, new byte[] {4}, "secret".getBytes(VISIBILITY_CHARSET), 0, false), null);
serverSideRanges.clear();
result = getEncryptor("searchable2.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(1)); // a range that is the first part of the search.
assertThat("should not be filtered client side", result, is(true));
// single point search, non-deterministic encryption
range = new Range(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 0, false), new Key(new byte[] {1},
new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET), 0, false));
serverSideRanges.clear();
result = getEncryptor("searchable3.ini", keys).transformRange(range, serverSideRanges);
assertThat("should be filtered server side", serverSideRanges, hasSize(2)); // 2 different key versions for the one searchable field.
assertThat("should not be filtered client side", result, is(true));
}
@Test
public void transformRangeExceptionTest() throws Exception {
Range range = new Range();
Collection<Range> serverSideRanges = new ArrayList<>();
EntryEncryptor encryptor = getEncryptor("searchable.ini");
try {
encryptor.transformRange(null, serverSideRanges);
fail("cannot call with a null range");
} catch (IllegalArgumentException e) { /* expected */}
try {
encryptor.transformRange(range, null);
fail("cannot call with a null serverSideRanges");
} catch (IllegalArgumentException e) { /* expected */}
}
/**
* Get an encryptor. The default set of keys will be used.
*
* @param resource
* Resource file containing the configuration.
* @return EntryEncryptor.
*/
private EntryEncryptor getEncryptor(String resource) throws Exception {
return getEncryptor(resource, keys);
}
/**
* Get an encryptor.
*
* @param resource
* Resource file containing the configuration.
* @param keys
* Keys to use for the encryptor.
* @return EntryEncryptor.
*/
private EntryEncryptor getEncryptor(String resource, EncryptionKeyContainer keys) throws Exception {
return new EntryEncryptor(new EncryptionConfigBuilder().readFromFile(new InputStreamReader(getResourceAsStream(this.getClass(), resource))).build(), keys);
}
/**
* Get the value encryptors for this test class. The default set of keys will be used.
*
* @return Entry encryptors.
*/
private Collection<EntryEncryptor> getEncryptors() throws Exception {
List<EntryEncryptor> encryptors = new ArrayList<>();
for (String line : IOUtils.readLines(this.getClass().getResourceAsStream(this.getClass().getSimpleName()))) {
encryptors.add(new EntryEncryptor(new EncryptionConfigBuilder().readFromFile(new InputStreamReader(TestUtils.getResourceAsStream(this.getClass(), line)))
.build(), keys));
}
return encryptors;
}
}
@@ -0,0 +1,175 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static edu.mit.ll.pace.test.Matchers.hasSameFieldsAs;
import static edu.mit.ll.pace.test.TestUtils.getResourceAsStream;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.util.TreeSet;
import org.ini4j.Ini;
import org.junit.Test;
import com.google.common.collect.ImmutableSortedSet;
import edu.mit.ll.pace.EntryField;
import edu.mit.ll.pace.test.Matchers;
/**
* Test {@link FieldEncryptorConfig}
*/
public class FieldEncryptorConfigTest {
@Test
public void constructorExceptionTests() throws IOException {
try {
getValidBuilder().setValueEncryptor(null).build();
fail("valueEnctypor must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setKeyLength(1000).build();
fail("keyLength must be appropriate for the encryptor");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setKeyId(null).build();
fail("keyId must be set");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setKeyId("").build();
fail("keyId must be set");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setValueEncryptor(ValueEncryptor.AES_SIV_DETERMINISTIC).setEncryptUsingVisibility(true).build();
fail("deterministic encryption cannot be used when keys are created using the visibility");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(null).build();
fail("destination must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(EntryField.COLUMN_VISIBILITY).build();
fail("destination must not be column visibility");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(EntryField.TIMESTAMP).build();
fail("destination must not be timestamp");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(EntryField.DELETE).build();
fail("destination must not be delete");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setSources(null).build();
fail("sources must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setSources(new TreeSet<>()).build();
fail("sources must not be empty");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void readTest() throws IOException {
FieldEncryptorConfig expectedConfig = new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_CBC).setProvider("SunJCE").setKeyLength(24)
.setEncryptUsingVisibility(false).setKeyId("keyId").setDestination(EntryField.COLUMN_FAMILY)
.setSources(ImmutableSortedSet.of(EntryField.ROW, EntryField.COLUMN_QUALIFIER, EntryField.COLUMN_FAMILY)).build();
FieldEncryptorConfig actualConfig = new FieldEncryptorConfigBuilder().readFromIni(getIni("config1.ini").get(EntryField.COLUMN_FAMILY.toString())).build();
assertThat("reading the ini file produces the correct configuration", actualConfig, hasSameFieldsAs(expectedConfig));
expectedConfig = new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_CBC).setProvider("BC").setKeyLength(16)
.setEncryptUsingVisibility(true).setKeyId("AES_CBC").setDestination(EntryField.ROW).setSources(ImmutableSortedSet.of(EntryField.ROW)).build();
actualConfig = new FieldEncryptorConfigBuilder().readFromIni(getIni("config2.ini").get(EntryField.ROW.toString())).build();
assertThat("reading the ini file produces the correct configuration", actualConfig, hasSameFieldsAs(expectedConfig));
expectedConfig = new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_CBC).setKeyLength(16).setEncryptUsingVisibility(true)
.setKeyId("AES_CBC").setDestination(EntryField.ROW).setSources(ImmutableSortedSet.of(EntryField.ROW)).build();
actualConfig = new FieldEncryptorConfigBuilder().readFromIni(getIni("config2WithoutDefaults.ini").get(EntryField.ROW.toString())).build();
assertThat("default values are correctly set", actualConfig, hasSameFieldsAs(expectedConfig));
expectedConfig = new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_CBC).setProvider(null).setKeyLength(16)
.setEncryptUsingVisibility(false).setKeyId(ValueEncryptor.AES_CBC.toString()).setDestination(EntryField.VALUE)
.setSources(ImmutableSortedSet.of(EntryField.VALUE)).build();
actualConfig = new FieldEncryptorConfigBuilder().readFromIni(getIni("config3.ini").get(EntryField.VALUE.toString())).build();
assertThat("reading the ini file produces the correct configuration", actualConfig, hasSameFieldsAs(expectedConfig));
}
@Test
public void writeTest() throws IOException {
FieldEncryptorConfig config = new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_CBC).setProvider("SunJCE").setKeyLength(24)
.setEncryptUsingVisibility(false).setKeyId("keyId").setDestination(EntryField.COLUMN_FAMILY)
.setSources(ImmutableSortedSet.of(EntryField.ROW, EntryField.COLUMN_QUALIFIER, EntryField.COLUMN_FAMILY)).build();
Ini ini = new Ini();
config.write(ini);
assertThat("the configuration is properly written to an ini", ini, is(Matchers.equalTo(getIni("config1.ini"))));
config = new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_CBC).setProvider("BC").setKeyLength(16).setEncryptUsingVisibility(true)
.setKeyId("AES_CBC").setDestination(EntryField.ROW).setSources(ImmutableSortedSet.of(EntryField.ROW)).build();
ini = new Ini();
config.write(ini);
assertThat("empty fields are not included in the output ini", ini, is(Matchers.equalTo(getIni("config2.ini"))));
}
@Test
public void writeReadTest() {
FieldEncryptorConfig config = new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_CBC).setProvider("BC").setKeyLength(32)
.setEncryptUsingVisibility(false).setKeyId("keyId").setDestination(EntryField.COLUMN_FAMILY)
.setSources(ImmutableSortedSet.of(EntryField.ROW, EntryField.COLUMN_FAMILY, EntryField.COLUMN_QUALIFIER)).build();
Ini ini = new Ini();
config.write(ini);
FieldEncryptorConfig config2 = new FieldEncryptorConfigBuilder().readFromIni(ini.values().iterator().next()).build();
assertThat("writing then reading the config should produce an equivalent configuration", config2, hasSameFieldsAs(config));
}
/**
* Get a valid config builder.
*
* @return Valid builder.
*/
private static FieldEncryptorConfigBuilder getValidBuilder() {
return new FieldEncryptorConfigBuilder().setValueEncryptor(ValueEncryptor.AES_GCM).setKeyLength(ValueEncryptor.AES_GCM.getDefaultKeyLength())
.setEncryptUsingVisibility(true).setDestination(EntryField.VALUE).setSources(ImmutableSortedSet.of(EntryField.VALUE));
}
/**
* Get the requested INI.
*
* @param resource
* INI file to get.
* @return The INI file.
*/
private Ini getIni(String resource) throws IOException {
return new Ini(getResourceAsStream(this.getClass(), resource));
}
}
@@ -0,0 +1,395 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static edu.mit.ll.pace.test.TestUtils.getResourceAsStream;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.security.Security;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.List;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.ColumnVisibility;
import org.apache.commons.lang3.tuple.Pair;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.ini4j.Ini;
import org.junit.BeforeClass;
import org.junit.Test;
import com.google.common.collect.ImmutableSet;
import edu.mit.ll.pace.EntryField;
import edu.mit.ll.pace.IllegalKeyRequestException;
import edu.mit.ll.pace.internal.MutableEntry;
/**
* Test {@link FieldEncryptor}.
*/
public class FieldEncryptorTest {
/**
* The mock key container, filled with several keys.
*/
private static EncryptionKeyContainer keys = new MockEncryptionKeyContainer(Pair.of("SIV", 2), Pair.of("GCM", 2), Pair.of("identity1", 1), Pair.of(
"identity2", 2), Pair.of("secret", 2), Pair.of("top secret", 1), Pair.of("admin", 1));
@BeforeClass
public static void registerBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void constructorExceptionTests() throws Exception {
try {
new FieldEncryptor(null, keys);
fail("config must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getEncryptor("gcm.ini", null); // Implicitly tests a good config, but a null key set.
fail("keys must not be null");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void encryptTest() throws Exception {
MutableEntry original = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6})));
MutableEntry empty = new MutableEntry();
ColumnVisibility visibility = new ColumnVisibility("secret");
// gcmEncryptor
MutableEntry encrypted = new MutableEntry();
FieldEncryptor encryptor = getEncryptor("gcm.ini");
encryptor.encrypt(original, encrypted, visibility);
assertThat("row was not encrypted and should not be set", encrypted.row, is(empty.row));
assertThat("colFamily was not encrypted and should not be set", encrypted.colF, is(empty.colF));
assertThat("colQualifier was not encrypted and should not be set", encrypted.colQ, is(empty.colQ));
assertThat("colVisibility was not encrypted and should not be set", encrypted.colVis, is(empty.colVis));
assertThat("timestamp was not encrypted and should not be set", encrypted.timestamp, is(empty.timestamp));
assertThat("delete was not encrypted and should not be set", encrypted.delete, is(empty.delete));
assertThat("value was encrypted and should be different", encrypted.value, is(not(original.value)));
// gcmCEABACEncryptor
encrypted = new MutableEntry();
encryptor = getEncryptor("gcmCEABAC.ini");
encryptor.encrypt(original, encrypted, visibility);
assertThat("row was not encrypted and should not be set", encrypted.row, is(empty.row));
assertThat("colFamily was not encrypted and should not be set", encrypted.colF, is(empty.colF));
assertThat("colQualifier was not encrypted and should not be set", encrypted.colQ, is(empty.colQ));
assertThat("colVisibility was not encrypted and should not be set", encrypted.colVis, is(empty.colVis));
assertThat("timestamp was not encrypted and should not be set", encrypted.timestamp, is(empty.timestamp));
assertThat("delete was not encrypted and should not be set", encrypted.delete, is(empty.delete));
assertThat("value was encrypted and should be different", encrypted.value, is(not(original.value)));
// sivEncryptor
encrypted = new MutableEntry();
encryptor = getEncryptor("siv.ini");
encryptor.encrypt(original, encrypted, visibility);
assertThat("row was encrypted and should be different", encrypted.row, is(not(original.row)));
assertThat("colFamily was not encrypted and should not be set", encrypted.colF, is(empty.colF));
assertThat("colQualifier was not encrypted and should not be set", encrypted.colQ, is(empty.colQ));
assertThat("colVisibility was not encrypted and should not be set", encrypted.colVis, is(empty.colVis));
assertThat("timestamp was not encrypted and should not be set", encrypted.timestamp, is(empty.timestamp));
assertThat("delete was not encrypted and should not be set", encrypted.delete, is(empty.delete));
assertThat("value was not encrypted and should not be set", encrypted.value, is(empty.value));
// allEncryptor
encrypted = new MutableEntry();
encryptor = getEncryptor("all.ini");
encryptor.encrypt(original, encrypted, visibility);
assertThat("row was encrypted and should not be different", encrypted.row, is(not(original.row)));
assertThat("colFamily was encrypted and should not be different", encrypted.colF, is(not(original.colF)));
assertThat("colQualifier was encrypted and should not be different", encrypted.colQ, is(not(original.colQ)));
assertThat("colVisibility was encrypted and should be different", encrypted.colVis, is(not(original.colVis)));
assertThat("timestamp was encrypted and should not be different", encrypted.timestamp, is(empty.timestamp));
assertThat("delete was encrypted, but and should not be different", encrypted.delete, is(empty.delete));
assertThat("value was encrypted and should not be different", encrypted.value, is(not(original.value)));
}
@Test
public void encryptedFieldStructureTest() throws Exception {
MutableEntry original1 = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6, 7, 8})));
MutableEntry original2 = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {9}, new byte[] {10, 11}, new byte[] {12, 13, 14},
"abcde".getBytes(VISIBILITY_CHARSET), (long) 15, true), new Value(new byte[] {16, 17})));
ColumnVisibility visibility = new ColumnVisibility("secret");
// Encrypt value with version 1 key.
MutableEntry encrypted = new MutableEntry();
FieldEncryptor encryptor = getEncryptor("identity1.ini"); // Uses a key with version = 1.
IdentityEncryptor.replaceValueEncryptorWithIdentityFunction(encryptor);
encryptor.encrypt(original1, encrypted, visibility);
assertThat("encrypted structure is (key version, length, data)", encrypted.value, is(new byte[] {1, 3, 6, 7, 8}));
encryptor.encrypt(original2, encrypted, visibility);
assertThat("encrypted structure is (key version, length, data)", encrypted.value, is(new byte[] {1, 2, 16, 17}));
// Encrypt value with version 2 key.
encrypted = new MutableEntry();
encryptor = getEncryptor("identity2.ini"); // Uses a key with version = 2.
IdentityEncryptor.replaceValueEncryptorWithIdentityFunction(encryptor);
encryptor.encrypt(original1, encrypted, visibility);
assertThat("encrypted structure is (key version, length, data)", encrypted.value, is(new byte[] {2, 3, 6, 7, 8}));
// Encrypt all values into single field.
encrypted = new MutableEntry();
encryptor = getEncryptor("identityAll.ini");
IdentityEncryptor.replaceValueEncryptorWithIdentityFunction(encryptor);
encryptor.encrypt(original1, encrypted, visibility);
assertThat("encrypted structure is (key version, foreach field [length, data])", encrypted.row, is(new byte[] {2, 1, 1, 1, 2, 1, 3, 6, 115, 101, 99, 114,
101, 116}));
encryptor.encrypt(original2, encrypted, visibility);
assertThat("encrypted structure is (key version, foreach field [length, data])", encrypted.row, is(new byte[] {2, 1, 9, 2, 10, 11, 3, 12, 13, 14, 5, 97,
98, 99, 100, 101}));
}
@Test
public void encryptDecryptTest() throws Exception {
MutableEntry original = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6})));
MutableEntry empty = new MutableEntry();
ColumnVisibility visibility = new ColumnVisibility("secret");
// gcmEncryptor
MutableEntry encrypted = new MutableEntry();
MutableEntry decrypted = new MutableEntry();
FieldEncryptor encryptor = getEncryptor("gcm.ini");
encryptor.encrypt(original, encrypted, visibility);
encryptor.decrypt(encrypted, decrypted, visibility);
assertThat("row was not encrypted and should not be set", decrypted.row, is(empty.row));
assertThat("colFamily was not encrypted and should not be set", decrypted.colF, is(empty.colF));
assertThat("colQualifier was not encrypted and should not be set", decrypted.colQ, is(empty.colQ));
assertThat("colVisibility was not encrypted and should not be set", decrypted.colVis, is(empty.colVis));
assertThat("timestamp was not encrypted and should not be set", decrypted.timestamp, is(empty.timestamp));
assertThat("delete was not encrypted and should not be set", decrypted.delete, is(empty.delete));
assertThat("value was encrypted and should be set", decrypted.value, is(original.value));
// gcmCEABACEncryptor
encrypted = new MutableEntry();
decrypted = new MutableEntry();
encryptor = getEncryptor("gcmCEABAC.ini");
encryptor.encrypt(original, encrypted, visibility);
encryptor.decrypt(encrypted, decrypted, visibility);
assertThat("row was not encrypted and should not be set", decrypted.row, is(empty.row));
assertThat("colFamily was not encrypted and should not be set", decrypted.colF, is(empty.colF));
assertThat("colQualifier was not encrypted and should not be set", decrypted.colQ, is(empty.colQ));
assertThat("colVisibility was not encrypted and should not be set", decrypted.colVis, is(empty.colVis));
assertThat("timestamp was not encrypted and should not be set", decrypted.timestamp, is(empty.timestamp));
assertThat("delete was not encrypted and should not be set", decrypted.delete, is(empty.delete));
assertThat("value was encrypted and should be set", decrypted.value, is(original.value));
// sivEncryptor
encrypted = new MutableEntry();
decrypted = new MutableEntry();
encryptor = getEncryptor("siv.ini");
encryptor.encrypt(original, encrypted, visibility);
encryptor.decrypt(encrypted, decrypted, visibility);
assertThat("row was encrypted and should be set", decrypted.row, is(original.row));
assertThat("colFamily was not encrypted and should not be set", decrypted.colF, is(empty.colF));
assertThat("colQualifier was not encrypted and should not be set", decrypted.colQ, is(empty.colQ));
assertThat("colVisibility was not encrypted and should not be set", decrypted.colVis, is(empty.colVis));
assertThat("timestamp was not encrypted and should not be set", decrypted.timestamp, is(empty.timestamp));
assertThat("delete was not encrypted and should not be set", decrypted.delete, is(empty.delete));
assertThat("value was not encrypted and should not be set", decrypted.value, is(empty.value));
// allEncryptor
encrypted = new MutableEntry();
decrypted = new MutableEntry();
encryptor = getEncryptor("all.ini");
encryptor.encrypt(original, encrypted, visibility);
encryptor.decrypt(encrypted, decrypted, visibility);
assertThat("row was encrypted and should be set", decrypted.row, is(original.row));
assertThat("colFamily was encrypted and should be set", decrypted.colF, is(original.colF));
assertThat("colQualifier was encrypted and should be set", decrypted.colQ, is(original.colQ));
assertThat("colVisibility was encrypted and should be set", decrypted.colVis, is(original.colVis));
assertThat("timestamp was not encrypted and should not be set", decrypted.timestamp, is(empty.timestamp));
assertThat("delete was not encrypted and should not be set", decrypted.delete, is(empty.delete));
assertThat("value was not encrypted and should not be set", decrypted.value, is(empty.value));
}
@Test
public void encryptDecryptCEABACTest() throws Exception {
MutableEntry original = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6})));
MutableEntry encrypted = new MutableEntry();
MutableEntry decrypted = new MutableEntry();
FieldEncryptor encryptor = getEncryptor("gcmCEABAC.ini");
MockEncryptionKeyContainer empty = new MockEncryptionKeyContainer();
MockEncryptionKeyContainer secretOnly = new MockEncryptionKeyContainer(Pair.of("secret", 2));
MockEncryptionKeyContainer topSecretOnly = new MockEncryptionKeyContainer(Pair.of("top secret", 1));
MockEncryptionKeyContainer secretAndAdminOnly = new MockEncryptionKeyContainer(Pair.of("secret", 2), Pair.of("admin", 1));
// empty
ColumnVisibility visibility = new ColumnVisibility("");
original.colVis = visibility.getExpression();
encryptor.encrypt(original, encrypted, visibility);
getEncryptor("gcmCEABAC.ini", empty).decrypt(encrypted, decrypted, visibility);
assertThat("no keys should be needed to decrypt the value", decrypted.value, is(original.value));
// secret | top secret
visibility = new ColumnVisibility("secret|\"top secret\"");
original.colVis = visibility.getExpression();
encryptor.encrypt(original, encrypted, visibility);
try {
getEncryptor("gcmCEABAC.ini", empty).decrypt(encrypted, decrypted, visibility);
fail("empty keys should not decrypt data");
} catch (IllegalKeyRequestException e) { /* expected */}
getEncryptor("gcmCEABAC.ini", secretOnly).decrypt(encrypted, decrypted, visibility);
assertThat("secret should be sufficient", decrypted.value, is(original.value));
getEncryptor("gcmCEABAC.ini", topSecretOnly).decrypt(encrypted, decrypted, visibility);
assertThat("top secret should be sufficient", decrypted.value, is(original.value));
// secret & top secret
visibility = new ColumnVisibility("secret&\"top secret\"");
original.colVis = visibility.getExpression();
encryptor.encrypt(original, encrypted, visibility);
try {
getEncryptor("gcmCEABAC.ini", empty).decrypt(encrypted, decrypted, visibility);
fail("empty keys should not decrypt data");
} catch (IllegalKeyRequestException e) { /* expected */}
try {
getEncryptor("gcmCEABAC.ini", secretOnly).decrypt(encrypted, decrypted, visibility);
fail("secret keys should not decrypt data");
} catch (IllegalKeyRequestException e) { /* expected */}
try {
getEncryptor("gcmCEABAC.ini", topSecretOnly).decrypt(encrypted, decrypted, visibility);
fail("top secret keys should not decrypt data");
} catch (IllegalKeyRequestException e) { /* expected */}
getEncryptor("gcmCEABAC.ini", keys).decrypt(encrypted, decrypted, visibility);
assertThat("secret and topSecret should be sufficient", decrypted.value, is(original.value));
// top secret | (secret & admin)
visibility = new ColumnVisibility("\"top secret\"|(secret&admin)");
original.colVis = visibility.getExpression();
encryptor.encrypt(original, encrypted, visibility);
getEncryptor("gcmCEABAC.ini", topSecretOnly).decrypt(encrypted, decrypted, visibility);
assertThat("topSecret should be sufficient", decrypted.value, is(original.value));
try {
getEncryptor("gcmCEABAC.ini", secretOnly).decrypt(encrypted, decrypted, visibility);
fail("secret keys should not decrypt data");
} catch (IllegalKeyRequestException e) { /* expected */}
getEncryptor("gcmCEABAC.ini", secretAndAdminOnly).decrypt(encrypted, decrypted, visibility);
assertThat("secret and admin should be sufficient", decrypted.value, is(original.value));
}
@Test
public void keyVersioningTest() throws Exception {
MutableEntry original = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6})));
ColumnVisibility visibility = new ColumnVisibility("secret");
MutableEntry encrypted = new MutableEntry();
MutableEntry decrypted = new MutableEntry();
getEncryptor("gcm.ini", new MockEncryptionKeyContainer(Pair.of("GCM", 1))).encrypt(original, encrypted, visibility);
getEncryptor("gcm.ini", new MockEncryptionKeyContainer(Pair.of("GCM", 2))).decrypt(encrypted, decrypted, visibility);
assertThat("value was decrypted with proper key", decrypted.value, is(original.value));
}
@Test
public void canBeFilteredServerSideTest() throws Exception {
assertThat("non-deterministic algorithms can't be filtered server side", getEncryptor("gcm.ini").canBeFilteredServerSide(ImmutableSet.of(EntryField.ROW)),
is(false));
assertThat("Visibility can't be filtered server side", getEncryptor("gcmCEABAC.ini").canBeFilteredServerSide(ImmutableSet.of(EntryField.ROW)), is(false));
assertThat("appropriate columns must be available", getEncryptor("siv.ini").canBeFilteredServerSide(ImmutableSet.of(EntryField.COLUMN_FAMILY)), is(false));
assertThat("deterministic encryption with correct fields should work", getEncryptor("siv.ini").canBeFilteredServerSide(ImmutableSet.of(EntryField.ROW)),
is(true));
}
@Test
public void canSearchForTest() throws Exception {
assertThat("non-deterministic algorithms can't be searched server side", getEncryptor("gcm.ini").canSearchFor(ImmutableSet.of(EntryField.ROW)), is(false));
assertThat("Visibility can't be filtered server side", getEncryptor("gcmCEABAC.ini").canSearchFor(ImmutableSet.of(EntryField.ROW)), is(false));
assertThat("appropriate columns must be available", getEncryptor("siv.ini").canSearchFor(ImmutableSet.of(EntryField.ROW, EntryField.COLUMN_FAMILY)),
is(false));
assertThat("deterministic encryption with correct fields should work", getEncryptor("siv.ini").canSearchFor(ImmutableSet.of(EntryField.ROW)), is(true));
}
@Test
public void getServerSideFilterValuesTest() throws Exception {
MutableEntry original = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6})));
ColumnVisibility visibility = new ColumnVisibility("secret");
MutableEntry encryptedV1 = new MutableEntry();
getEncryptor("siv.ini", new MockEncryptionKeyContainer(Pair.of("SIV", 1))).encrypt(original, encryptedV1, visibility);
MutableEntry encryptedV2 = new MutableEntry();
getEncryptor("siv.ini", new MockEncryptionKeyContainer(Pair.of("SIV", 2))).encrypt(original, encryptedV2, visibility);
List<byte[]> searchKeys = getEncryptor("siv.ini", new MockEncryptionKeyContainer(Pair.of("SIV", 2))).getServerSideFilterValues(original);
assertThat("encrypted values for each version available", searchKeys, containsInAnyOrder(encryptedV1.row, encryptedV2.row));
}
/**
* Get an encryptor. The default set of keys will be used.
*
* @param resource
* Resource file containing the configuration.
* @return FieldEncryptor.
*/
private FieldEncryptor getEncryptor(String resource) throws Exception {
return getEncryptor(resource, keys);
}
/**
* Get an encryptor.
*
* @param resource
* Resource file containing the configuration.
* @param keys
* Keys to use for the encryptor.
* @return FieldEncryptor.
*/
private FieldEncryptor getEncryptor(String resource, EncryptionKeyContainer keys) throws Exception {
Ini ini = new Ini(getResourceAsStream(this.getClass(), resource));
return new FieldEncryptor(new FieldEncryptorConfigBuilder().readFromIni(ini.values().iterator().next()).build(), keys);
}
}
@@ -0,0 +1,94 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.lang.reflect.Field;
import java.util.Collection;
/**
* Encryptor that returns data instead of encrypting it.
*/
public class IdentityEncryptor extends ValueEncryptorBase {
/**
* Field accesser for {@link EntryEncryptor#encryptors}.
*/
private static Field encryptorsField;
/**
* Field accesser for {@link FieldEncryptor#encryptor}.
*/
private static Field encryptorField;
static {
try {
encryptorsField = EntryEncryptor.class.getDeclaredField("encryptors");
encryptorsField.setAccessible(true);
encryptorField = FieldEncryptor.class.getDeclaredField("encryptor");
encryptorField.setAccessible(true);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
/**
* Replace the internal {@link ValueEncryptorBase} for each field encryptor that makes up this entry encryptor.
* <p>
* Useful for testing or demoes.
*
* @param entryEncryptor
* Entry encryptor to modify.
*/
static void replaceValueEncryptorsWithIdentityFunction(EntryEncryptor entryEncryptor) {
try {
@SuppressWarnings("unchecked")
Collection<FieldEncryptor> fieldEncryptors = (Collection<FieldEncryptor>) encryptorsField.get(entryEncryptor);
for (FieldEncryptor fieldEncryptor : fieldEncryptors) {
replaceValueEncryptorWithIdentityFunction(fieldEncryptor);
}
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* Replace field encryptors internal {@link ValueEncryptorBase} to use an identity function instead of actual encryption.
* <p>
* Useful for testing or demoes.
*
* @param fieldEncryptor
* Field encryptor to modify.
*/
static void replaceValueEncryptorWithIdentityFunction(FieldEncryptor fieldEncryptor) {
try {
encryptorField.set(fieldEncryptor, new IdentityEncryptor());
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
@Override
byte[] encrypt(byte[] key, byte[] data) {
return data;
}
@Override
byte[] decrypt(byte[] key, byte[] data) {
return data;
}
}
@@ -0,0 +1,173 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.apache.commons.lang3.tuple.Pair;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.HKDFBytesGenerator;
import org.bouncycastle.crypto.params.HKDFParameters;
import com.google.common.primitives.Ints;
import edu.mit.ll.pace.IllegalKeyRequestException;
/**
* Mock {@link EncryptionKeyContainer} to use for testing the encryption code.
*/
public final class MockEncryptionKeyContainer implements EncryptionKeyContainer {
/**
* Key to use in the HKDF function.
*/
private static byte[] HKDF_KEY = new byte[] {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
/**
* The keys that will be issued by this container, along with their current version.
*/
private Map<String,Integer> keys = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
/**
* An HKDF to use for key generation.
*/
private HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA512Digest());
/**
* Create the mock container with the supplied keys and versions.
*
* @param keys
* Keys to add to the mock container.
*/
@SafeVarargs
MockEncryptionKeyContainer(Pair<String,Integer>... keys) {
for (Pair<String,Integer> pair : keys) {
this.keys.put(pair.getLeft(), pair.getRight());
}
}
@Override
public Collection<KeyWithVersion> getKeys(String id, int length) throws IllegalKeyRequestException {
if (!keys.containsKey(id)) {
throw new IllegalKeyRequestException("getKey: " + id);
}
List<KeyWithVersion> versionedKeys = new ArrayList<>();
int maxVersion = keys.get(id);
// Get a key for each version, past and future.
for (int i = 1; i <= maxVersion; i++) {
versionedKeys.add(new KeyWithVersion(generateKey(id, i, length), i));
}
return versionedKeys;
}
@Override
public KeyWithVersion getKey(String id, int length) throws IllegalKeyRequestException {
if (!keys.containsKey(id)) {
throw new IllegalKeyRequestException("getKey: " + id);
}
int version = keys.get(id);
return new KeyWithVersion(generateKey(id, version, length), version);
}
@Override
public byte[] getKey(String id, int version, int length) throws IllegalKeyRequestException {
if (!keys.containsKey(id)) {
throw new IllegalKeyRequestException("getKey: " + id);
}
int maxVersion = keys.get(id);
if (version > maxVersion) {
throw new IllegalArgumentException("invalid version");
}
return generateKey(id, version, length);
}
@Override
public KeyWithVersion getAttributeKey(String attribute, String id, int length) throws IllegalKeyRequestException {
if (!keys.containsKey(attribute)) {
throw new IllegalKeyRequestException("getAttributeKey: " + attribute);
}
int version = keys.get(attribute);
return new KeyWithVersion(generateKey(attribute, id, version, length), version);
}
@Override
public byte[] getAttributeKey(String attribute, String id, int version, int length) throws IllegalKeyRequestException {
if (!keys.containsKey(attribute)) {
throw new IllegalKeyRequestException("getAttributeKey: " + attribute);
}
int maxVersion = keys.get(attribute);
if (version > maxVersion) {
throw new IllegalArgumentException("invalid version");
}
return generateKey(attribute, id, version, length);
}
/**
* Generate a key from the given data.
*
* @param id
* Id of the key to generate.
* @param version
* Version of the key to generate.
* @param length
* Length of the key to generate.
* @return Generated key.
*/
private byte[] generateKey(String id, int version, int length) {
return generateKey("keyIdOnly", id, version, length);
}
/**
* Generate a key from the given data.
*
* @param attribute
* Attribute of the key to generate.
* @param id
* Id of the key to generate.
* @param version
* Version of the key to generate.
* @param length
* Length of the key to generate.
* @return Generated key.
*/
private byte[] generateKey(String attribute, String id, int version, int length) {
ByteArrayOutputStream metadata = new ByteArrayOutputStream();
try {
metadata.write(id.getBytes(StandardCharsets.UTF_8));
metadata.write(Ints.toByteArray(version));
metadata.write(Ints.toByteArray(length));
} catch (IOException e) { /* won't be thrown */}
byte[] key = new byte[length];
hkdf.init(new HKDFParameters(HKDF_KEY, attribute.getBytes(StandardCharsets.UTF_8), metadata.toByteArray()));
hkdf.generateBytes(key, 0, key.length);
return key;
}
}
@@ -0,0 +1,146 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.encryption;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.junit.Assert.assertThat;
import java.security.Security;
import java.util.Arrays;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test {@link ValueEncryptor}.
*/
public final class ValueEncryptorTest {
@BeforeClass
public static void registerBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void validEnumTest() {
assertThat("should have six values", ValueEncryptor.values(), is(arrayWithSize(6)));
}
@Test
public void toStringTest() {
assertThat("toString should return correct value", ValueEncryptor.AES_SIV_DETERMINISTIC.toString(), is("AES_SIV_DETERMINISTIC"));
assertThat("toString should return correct value", ValueEncryptor.AES_CTR.toString(), is("AES_CTR"));
assertThat("toString should return correct value", ValueEncryptor.AES_CFB.toString(), is("AES_CFB"));
assertThat("toString should return correct value", ValueEncryptor.AES_CBC.toString(), is("AES_CBC"));
assertThat("toString should return correct value", ValueEncryptor.AES_OFB.toString(), is("AES_OFB"));
assertThat("toString should return correct value", ValueEncryptor.AES_GCM.toString(), is("AES_GCM"));
}
@Test
public void fromStringTest() {
assertThat("fromString should return correct enum value", ValueEncryptor.fromString("AES_SIV_DETERMINISTIC"), is(ValueEncryptor.AES_SIV_DETERMINISTIC));
assertThat("fromString should return correct enum value", ValueEncryptor.fromString("AES_CTR"), is(ValueEncryptor.AES_CTR));
assertThat("fromString should return correct enum value", ValueEncryptor.fromString("AES_CFB"), is(ValueEncryptor.AES_CFB));
assertThat("fromString should return correct enum value", ValueEncryptor.fromString("AES_CBC"), is(ValueEncryptor.AES_CBC));
assertThat("fromString should return correct enum value", ValueEncryptor.fromString("AES_OFB"), is(ValueEncryptor.AES_OFB));
assertThat("fromString should return correct enum value", ValueEncryptor.fromString("AES_GCM"), is(ValueEncryptor.AES_GCM));
}
@Test(expected = IllegalArgumentException.class)
public void fromStringFailureTest() {
ValueEncryptor.fromString("bad");
}
@Test
public void isDeterministicTest() {
assertThat("isDeterministic is true", ValueEncryptor.AES_SIV_DETERMINISTIC.isDeterministic(), is(true));
assertThat("isDeterministic is false", ValueEncryptor.AES_CTR.isDeterministic(), is(false));
assertThat("isDeterministic is false", ValueEncryptor.AES_CFB.isDeterministic(), is(false));
assertThat("isDeterministic is false", ValueEncryptor.AES_CBC.isDeterministic(), is(false));
assertThat("isDeterministic is false", ValueEncryptor.AES_OFB.isDeterministic(), is(false));
assertThat("isDeterministic is false", ValueEncryptor.AES_GCM.isDeterministic(), is(false));
}
@Test
public void getDefaultKeyLengthTest() {
assertThat("correct default key (32)", ValueEncryptor.AES_SIV_DETERMINISTIC.getDefaultKeyLength(), is(32));
assertThat("correct default key (16)", ValueEncryptor.AES_CTR.getDefaultKeyLength(), is(16));
assertThat("correct default key (16)", ValueEncryptor.AES_CFB.getDefaultKeyLength(), is(16));
assertThat("correct default key (16)", ValueEncryptor.AES_CBC.getDefaultKeyLength(), is(16));
assertThat("correct default key (16)", ValueEncryptor.AES_OFB.getDefaultKeyLength(), is(16));
assertThat("correct default key (16)", ValueEncryptor.AES_GCM.getDefaultKeyLength(), is(16));
}
@Test
public void isValidKeyLengthTest() {
for (int keyLength : Arrays.asList(16, 24, 32)) {
for (ValueEncryptor encryptor : ValueEncryptor.values()) {
switch (encryptor) {
case AES_SIV_DETERMINISTIC:
assertThat("isValidKeyLength should accept good key lengths", encryptor.isValidKeyLength(keyLength * 2), is(true));
assertThat("isValidKeyLength should accept good key lengths", encryptor.isValidKeyLength(keyLength * 2 + 1), is(false));
break;
default:
assertThat("isValidKeyLength should accept good key lengths", encryptor.isValidKeyLength(keyLength), is(true));
assertThat("isValidKeyLength should accept good key lengths", encryptor.isValidKeyLength(keyLength + 1), is(false));
}
}
}
}
@Test
public void getInstanceTest() throws Exception {
assertThat("should return a valid ValueEncryptorBase instance", ValueEncryptor.AES_SIV_DETERMINISTIC.getInstance(null),
is(instanceOf(ValueEncryptorBase.class)));
assertThat("should return a valid ValueEncryptorBase instance", ValueEncryptor.AES_CTR.getInstance(null), is(instanceOf(ValueEncryptorBase.class)));
assertThat("should return a valid ValueEncryptorBase instance", ValueEncryptor.AES_CFB.getInstance("BC"), is(instanceOf(ValueEncryptorBase.class)));
assertThat("should return a valid ValueEncryptorBase instance", ValueEncryptor.AES_CBC.getInstance("BC"), is(instanceOf(ValueEncryptorBase.class)));
assertThat("should return a valid ValueEncryptorBase instance", ValueEncryptor.AES_OFB.getInstance(null), is(instanceOf(ValueEncryptorBase.class)));
assertThat("should return a valid ValueEncryptorBase instance", ValueEncryptor.AES_GCM.getInstance(null), is(instanceOf(ValueEncryptorBase.class)));
}
@Test
public void encryptionTest() throws Exception {
// Test each enum value and ensure it can properly encrypt data.
for (ValueEncryptor encryptorEnum : ValueEncryptor.values()) {
ValueEncryptorBase encryptor = encryptorEnum.getInstance(null);
byte[] plaintext = "1234".getBytes(VISIBILITY_CHARSET);
byte[] key = "aabbccddaabbccddaabbccddaabbccdd".getBytes(VISIBILITY_CHARSET); // 256-bit key valid for all encryptors.
byte[] ciphertext1 = encryptor.encrypt(key, plaintext);
assertThat("ciphertext should not contain the original plaintext", TestUtils.wrap(ciphertext1), is(not(arrayContaining(TestUtils.wrap(plaintext)))));
byte[] plaintext1 = encryptor.decrypt(key, ciphertext1);
assertThat("decrypting the ciphertext should give the plaintext", plaintext1, is(equalTo(plaintext)));
byte[] ciphertext2 = encryptor.encrypt(key, "1235".getBytes(VISIBILITY_CHARSET));
assertThat("ciphertext should be not be the same when different values are encrypted", ciphertext2, is(not(equalTo(ciphertext1))));
}
}
}
@@ -0,0 +1,164 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.internal;
import static edu.mit.ll.pace.internal.Utils.EMPTY;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.Map.Entry;
import org.apache.accumulo.core.data.ColumnUpdate;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.junit.Before;
import org.junit.Test;
import edu.mit.ll.pace.EntryField;
/**
* Test {@link MutableEntry}.
*/
public final class MutableEntryTest {
/**
* Testing values.
*/
private byte[] row, colF, colQ, colVis, value;
private long timestamp;
private boolean delete;
private Key key;
private Entry<Key,Value> entry;
/**
* Ensure that the core values are always in a good state.
*/
@Before
public void setupTestValues() {
row = new byte[] {(byte) 1};
colF = new byte[] {(byte) 2};
colQ = new byte[] {(byte) 3};
colVis = "A".getBytes(VISIBILITY_CHARSET);
value = new byte[] {(byte) 4};
timestamp = 4;
delete = true;
key = new Key(row, colF, colQ, colVis, timestamp, delete);
entry = new SimpleImmutableEntry<>(key, new Value(value));
}
@Test
public void constructorWithKeyTest() {
MutableEntry mutableKey = new MutableEntry(key);
assertThat("row should be unmodified", mutableKey.row, is(equalTo(row)));
assertThat("colF should be unmodified", mutableKey.colF, is(equalTo(colF)));
assertThat("colQ should be unmodified", mutableKey.colQ, is(equalTo(colQ)));
assertThat("colVis should be unmodified", mutableKey.colVis, is(equalTo(colVis)));
assertThat("timestamp should be unmodified", mutableKey.timestamp, is(equalTo(timestamp)));
assertThat("delete should be unmodified", mutableKey.delete, is(equalTo(delete)));
assertThat("value should not be set", mutableKey.value, is(equalTo(EMPTY)));
}
@Test
public void constructorWithEntryTest() {
MutableEntry mutableEntry = new MutableEntry(entry);
assertThat("row should be unmodified", mutableEntry.row, is(equalTo(row)));
assertThat("colF should be unmodified", mutableEntry.colF, is(equalTo(colF)));
assertThat("colQ should be unmodified", mutableEntry.colQ, is(equalTo(colQ)));
assertThat("colVis should be unmodified", mutableEntry.colVis, is(equalTo(colVis)));
assertThat("timestamp should be unmodified", mutableEntry.timestamp, is(equalTo(timestamp)));
assertThat("delete should be unmodified", mutableEntry.delete, is(equalTo(delete)));
assertThat("value should be unmodified", mutableEntry.value, is(equalTo(value)));
}
@Test
public void constructorWithUpdateTest() {
ColumnUpdate update = new ColumnUpdate(colF, colQ, colVis, true, timestamp, delete, value);
MutableEntry mutableEntry = new MutableEntry(row, update);
assertThat("row should be unmodified", mutableEntry.row, is(equalTo(row)));
assertThat("colF should be unmodified", mutableEntry.colF, is(equalTo(colF)));
assertThat("colQ should be unmodified", mutableEntry.colQ, is(equalTo(colQ)));
assertThat("colVis should be unmodified", mutableEntry.colVis, is(equalTo(colVis)));
assertThat("timestamp should be unmodified", mutableEntry.timestamp, is(equalTo(timestamp)));
assertThat("delete should be unmodified", mutableEntry.delete, is(equalTo(delete)));
assertThat("value should be unmodified", mutableEntry.value, is(equalTo(value)));
}
@Test
public void toKeyTest() {
MutableEntry mutableKey = new MutableEntry(key);
assertThat("returned key should be the same as the original key", mutableKey.toKey(), is(equalTo(key)));
}
@Test
public void toEntryTest() {
MutableEntry mutableEntry = new MutableEntry(entry);
assertThat("returned entry should be the same as the original entry", mutableEntry.toEntry(), is(equalTo(entry)));
}
@Test
public void cloneTest() {
MutableEntry mutableEntry = new MutableEntry(entry);
MutableEntry mutableEntry2 = mutableEntry.cloneEntry();
assertThat("returned entry should be the same as the original entry", mutableEntry2.toEntry(), is(equalTo(mutableEntry.toEntry())));
}
@Test
public void getBytesTest() {
MutableEntry mutableEntry = new MutableEntry(entry);
assertThat("correct bytes returned", mutableEntry.getBytes(EntryField.ROW), is(equalTo(row)));
assertThat("correct bytes returned", mutableEntry.getBytes(EntryField.COLUMN_FAMILY), is(equalTo(colF)));
assertThat("correct bytes returned", mutableEntry.getBytes(EntryField.COLUMN_QUALIFIER), is(equalTo(colQ)));
assertThat("correct bytes returned", mutableEntry.getBytes(EntryField.COLUMN_VISIBILITY), is(equalTo(colVis)));
assertThat("correct bytes returned", mutableEntry.getBytes(EntryField.VALUE), is(equalTo(value)));
}
@Test(expected = IllegalArgumentException.class)
public void getBytesException() {
MutableEntry mutableEntry = new MutableEntry(entry);
mutableEntry.getBytes(EntryField.TIMESTAMP);
}
@Test
public void setBytesTest() {
MutableEntry mutableEntry = new MutableEntry(new Key()); // Create an empty mutable entry.
mutableEntry.setBytes(EntryField.ROW, row);
mutableEntry.setBytes(EntryField.COLUMN_FAMILY, colF);
mutableEntry.setBytes(EntryField.COLUMN_QUALIFIER, colQ);
mutableEntry.setBytes(EntryField.COLUMN_VISIBILITY, colVis);
mutableEntry.setBytes(EntryField.VALUE, value);
mutableEntry.timestamp = timestamp;
mutableEntry.delete = delete;
assertThat("constructed entry should be the same as the testing entry", mutableEntry.toEntry(), is(equalTo(entry)));
}
@Test(expected = IllegalArgumentException.class)
public void setBytesException() {
MutableEntry mutableEntry = new MutableEntry(entry);
mutableEntry.setBytes(EntryField.TIMESTAMP, EMPTY);
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.internal;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.nio.charset.StandardCharsets;
import org.apache.accumulo.core.data.Key;
import org.junit.Test;
/**
* Test {@link Utils}.
*/
public final class UtilsTest {
@Test
public void visibilityCharsetTest() {
assertThat("correct visibility charset", Utils.VISIBILITY_CHARSET, is(StandardCharsets.US_ASCII));
}
@Test
public void emptyTest() {
assertThat("correct empty array", Utils.EMPTY, is(equalTo(new Key().getRow().getBytes())));
}
@Test
public void xorTest() {
byte[] first = new byte[] {0, 1, 2};
byte[] second = new byte[] {0, 1, 1};
byte[] result = Utils.xor(first, second);
assertThat("the return value is the same as the first argument", result == first, is(true));
assertThat("the correct result was achieved", result, is(equalTo(new byte[] {0, 0, 3})));
}
@Test
public void xorExceptionTest() {
byte[] first = new byte[] {0, 1, 2};
byte[] second = new byte[] {0, 1, 2, 3};
try {
Utils.xor(null, second);
fail("null first variable not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
Utils.xor(first, null);
fail("null second variable not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
Utils.xor(first, second);
fail("arrays with unequal length not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
}
@@ -0,0 +1,280 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static edu.mit.ll.pace.internal.Utils.EMPTY;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static edu.mit.ll.pace.test.TestUtils.getResourceAsStream;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import edu.mit.ll.pace.internal.MutableEntry;
import edu.mit.ll.pace.internal.Utils;
import edu.mit.ll.pace.test.Matchers;
/**
* Tests for {@link EntrySigner}
*/
public class EntrySignerTest {
private Map<ValueSigner,SignatureKeyContainer> aliceKeyContainers = new HashMap<>();
private Map<ValueSigner,SignatureKeyContainer> bobKeyContainers = new HashMap<>();
public EntrySignerTest() throws NoSuchAlgorithmException {
aliceKeyContainers = MockSignatureKeyContainer.getContainers("alice", "alice", "bob");
bobKeyContainers = MockSignatureKeyContainer.getContainers("bob", "alice", "bob");
}
@BeforeClass
public static void setupBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void constructorExceptionTests() throws Exception {
try {
new EntrySigner(null, aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("config must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getSigner("config1.ini", null); // Implicitly tests a good config, but a null key set.
fail("keys must not be null");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void nullEntryTest() throws Exception {
EntrySigner signer = getSigner("config1.ini", aliceKeyContainers.get(ValueSigner.RSA_PSS));
try {
signer.sign(null, true);
fail("cannot call sign with a null entry");
} catch (IllegalArgumentException e) { /* expected */}
try {
signer.verify(null);
fail("cannot call verify with a null value");
} catch (IllegalArgumentException e) { /* expected */}
try {
signer.verify(null, null);
fail("cannot call verify with a null value");
} catch (IllegalArgumentException e) { /* expected */}
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(VISIBILITY_CHARSET),
(long) 5, false), new Value(new byte[] {6}));
try {
getSigner("config2.ini", aliceKeyContainers.get(ValueSigner.RSA_PKCS1)).verify(entry, entry);
fail("cannot call verify with a non-null signature entry if not needed");
} catch (IllegalArgumentException e) { /* expected */}
try {
getSigner("config3.ini", aliceKeyContainers.get(ValueSigner.ECDSA)).verify(entry, null);
fail("cannot call verify with a null signature value when needed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void signVerifyInValueTest() throws Exception {
MutableEntry entry = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6})));
MutableEntry signed;
Entry<Key,Value> verified;
EntrySigner signer = getSigner("config1.ini", aliceKeyContainers.get(ValueSigner.RSA_PSS));
EntrySigner verifier = getSigner("config1.ini", bobKeyContainers.get(ValueSigner.RSA_PSS));
signed = new MutableEntry(signer.sign(entry.toEntry(), true));
assertThat("row should not have changed", signed.row, is(entry.row));
assertThat("colFamily should not have changed", signed.colF, is(entry.colF));
assertThat("colQualifier should not have changed", signed.colQ, is(entry.colQ));
assertThat("colVisibility should not have changed", signed.colVis, is(entry.colVis));
assertThat("timestamp should not have changed", signed.timestamp, is(entry.timestamp));
assertThat("delete should not have changed", signed.delete, is(entry.delete));
assertThat("value should have changed", signed.value, is(not(entry.value)));
verified = verifier.verify(signed.toEntry());
assertThat("original and verified records are the same.", verified, Matchers.equalTo(entry.toEntry()));
}
@Test
public void signVerifyInVisibilityTest() throws Exception {
MutableEntry entry = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6})));
MutableEntry signed;
Entry<Key,Value> verified;
// Sign to column visibility
EntrySigner signer = getSigner("config2.ini", aliceKeyContainers.get(ValueSigner.RSA_PKCS1));
EntrySigner verifier = getSigner("config2.ini", bobKeyContainers.get(ValueSigner.RSA_PKCS1));
signed = new MutableEntry(signer.sign(entry.toEntry(), true));
assertThat("row should not have changed", signed.row, is(entry.row));
assertThat("colFamily should not have changed", signed.colF, is(entry.colF));
assertThat("colQualifier should not have changed", signed.colQ, is(entry.colQ));
assertThat("colVisibility should have changed", signed.colVis, is(not(entry.colVis)));
assertThat("colVisibility is wrapped", new String(signed.colVis, Utils.VISIBILITY_CHARSET), startsWith("(secret)"));
assertThat("timestamp should not have changed", signed.timestamp, is(entry.timestamp));
assertThat("delete should not have changed", signed.delete, is(entry.delete));
assertThat("value should not have changed", signed.value, is(entry.value));
verified = verifier.verify(signed.toEntry());
assertThat("original and verified records are the same.", verified, Matchers.equalTo(entry.toEntry()));
// Sign to column visibility with default visibility
MutableEntry entry2 = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, EMPTY, (long) 5, false),
new Value(new byte[] {6})));
signed = new MutableEntry(signer.sign(entry2.toEntry(), true));
assertThat("row should not have changed", signed.row, is(entry2.row));
assertThat("colFamily should not have changed", signed.colF, is(entry.colF));
assertThat("colQualifier should not have changed", signed.colQ, is(entry.colQ));
assertThat("colVisibility should have changed", signed.colVis, is(not(entry.colVis)));
assertThat("colVisibility uses the default visibility", new String(signed.colVis, Utils.VISIBILITY_CHARSET), startsWith("(default)"));
assertThat("timestamp should not have changed", signed.timestamp, is(entry.timestamp));
assertThat("delete should not have changed", signed.delete, is(entry.delete));
assertThat("value should not have changed", signed.value, is(entry.value));
MutableEntry verifiedEntry = new MutableEntry(verifier.verify(signed.toEntry()));
assertThat("row should not have changed", verifiedEntry.row, is(entry2.row));
assertThat("colFamily should not have changed", verifiedEntry.colF, is(entry.colF));
assertThat("colQualifier should not have changed", verifiedEntry.colQ, is(entry.colQ));
assertThat("colVisibility should have changed to the default visibility", verifiedEntry.colVis, is("default".getBytes(Utils.VISIBILITY_CHARSET)));
assertThat("timestamp should not have changed", verifiedEntry.timestamp, is(entry.timestamp));
assertThat("delete should not have changed", verifiedEntry.delete, is(entry.delete));
assertThat("value should not have changed", verifiedEntry.value, is(entry.value));
}
@Test
public void signVerifyInSeparateTableTest() throws Exception {
MutableEntry entry = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 5, false), new Value(new byte[] {6})));
MutableEntry signed;
Entry<Key,Value> verified;
EntrySigner signer = getSigner("config3.ini", aliceKeyContainers.get(ValueSigner.ECDSA));
EntrySigner verifier = getSigner("config3.ini", bobKeyContainers.get(ValueSigner.ECDSA));
signed = new MutableEntry(signer.sign(entry.toEntry(), true));
assertThat("row should not have changed", signed.row, is(entry.row));
assertThat("colFamily should not have changed", signed.colF, is(entry.colF));
assertThat("colQualifier should not have changed", signed.colQ, is(entry.colQ));
assertThat("colVisibility should not have changed", signed.colVis, is(entry.colVis));
assertThat("timestamp should not have changed", signed.timestamp, is(entry.timestamp));
assertThat("delete should not have changed", signed.delete, is(entry.delete));
assertThat("value should have changed", signed.value, is(not(entry.value)));
verified = verifier.verify(entry.toEntry(), signed.toEntry());
assertThat("original and verified records are the same.", verified, Matchers.equalTo(entry.toEntry()));
}
@Test
public void hasNoTimestampTest() throws Exception {
MutableEntry entry = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 0, false), new Value(new byte[] {6})));
MutableEntry signed;
EntrySigner signer = getSigner("config1.ini", aliceKeyContainers.get(ValueSigner.RSA_PSS));
EntrySigner verifier = getSigner("config1.ini", bobKeyContainers.get(ValueSigner.RSA_PSS));
try {
signed = new MutableEntry(signer.sign(entry.toEntry(), true));
signed.timestamp = 1000L;
verifier.verify(signed.toEntry());
fail("changing the timestamp should cause the signature to fail");
} catch (SignatureException e) { /* expected */}
signed = new MutableEntry(signer.sign(entry.toEntry(), false));
signed.timestamp = 1000L;
verifier.verify(signed.toEntry());
}
@Test
public void badSignatureTest() throws Exception {
MutableEntry entry = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 0, false), new Value(new byte[] {6})));
Entry<Key,Value> signed;
// Sign to value;
EntrySigner signer = getSigner("config3.ini", aliceKeyContainers.get(ValueSigner.ECDSA));
EntrySigner verifier = getSigner("config3.ini", bobKeyContainers.get(ValueSigner.ECDSA));
signed = signer.sign(entry.toEntry(), true);
entry.value = new byte[] {7};
try {
verifier.verify(entry.toEntry(), signed);
fail("bad signature should thrown an exception");
} catch (SignatureException e) { /* expected */}
}
@Test
public void signVerifyDeleteException() throws Exception {
MutableEntry entry = new MutableEntry(new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(VISIBILITY_CHARSET), (long) 0, true), new Value(new byte[] {6})));
EntrySigner signer = getSigner("config1.ini", aliceKeyContainers.get(ValueSigner.RSA_PSS));
EntrySigner verifier = getSigner("config1.ini", bobKeyContainers.get(ValueSigner.RSA_PSS));
try {
signer.sign(entry.toEntry(), true);
fail("cannot sign deleted entries");
} catch (IllegalArgumentException e) { /* expected */}
try {
verifier.verify(entry.toEntry());
fail("cannot verify deleted entries");
} catch (IllegalArgumentException e) { /* expected */}
}
/**
* Get a signer.
*
* @param resource
* Resource file containing the configuration.
* @param keys
* Keys to use for the signer.
* @return EntrySigner.
*/
private EntrySigner getSigner(String resource, SignatureKeyContainer keys) throws Exception {
return new EntrySigner(new SignatureConfigBuilder().readFromFile(new InputStreamReader(getResourceAsStream(this.getClass(), resource))).build(), keys);
}
}
@@ -0,0 +1,175 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
/**
* Mock {@link SignatureKeyContainer} to use for testing the signature code.
*/
public class MockSignatureKeyContainer implements SignatureKeyContainer {
/**
* Class for key lookup in key container map.
*/
private static final class KeyLookup {
private final ValueSigner signer;
private final int keyLength;
private final String id;
KeyLookup(ValueSigner signer, int keyLength, String id) {
this.signer = signer;
this.keyLength = keyLength;
this.id = id;
}
@Override
public int hashCode() {
return Objects.hash(signer, keyLength, id);
}
@Override
public boolean equals(Object obj) {
if (null == obj || !(obj instanceof KeyLookup)) {
return false;
}
KeyLookup other = (KeyLookup) obj;
return signer.equals(other.signer) && keyLength == other.keyLength && id.equals(other.id);
}
}
/**
* Set of keys generated by this class.
*/
private static final Map<KeyLookup,KeyPair> generatedKeys = new HashMap<>();
/**
* The signing key id.
*/
private final String signingKeyId;
/**
* The signing key wrapped by this container.
*/
private final PrivateKey signingKey;
/**
* The public key wrapped by this container.
*/
private final Map<String,PublicKey> verifyingKeys = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
/**
* Create a mock container with the given signature and verifying keys.
*
* @param signer
* Signer to generate keys for.
* @param keyLength
* Length of the keys.
* @param signingKeyId
* Id of the user the signing key is for.
* @param verifyingKeyIds
* Id of the users that we will verify signatures for.
*/
private MockSignatureKeyContainer(ValueSigner signer, int keyLength, String signingKeyId, String... verifyingKeyIds) throws NoSuchAlgorithmException {
this.signingKeyId = String.format("%s_%d_%s", signer.getKeyGenerationAlgorithm(), keyLength, signingKeyId);
this.signingKey = getKeyPair(signer, keyLength, signingKeyId).getPrivate();
for (String keyId : verifyingKeyIds) {
this.verifyingKeys.put(String.format("%s_%d_%s", signer.getKeyGenerationAlgorithm(), keyLength, keyId), getKeyPair(signer, keyLength, keyId).getPublic());
}
}
@Override
public PrivateKeyWithId getSigningKey() {
return new PrivateKeyWithId(signingKey, signingKeyId.getBytes(StandardCharsets.UTF_8));
}
@Override
public PublicKey getVerifyingKey(byte[] id) {
String keyId = new String(id, StandardCharsets.UTF_8);
return verifyingKeys.get(keyId);
}
/**
* Get a key pair of given length for the given ID.
*
* @param signer
* Signer that the key is being generated for.
* @param keyLength
* Length of the key.
* @param id
* Id of the user the key is for.
* @return KeyPair for the requested key.
*/
private static KeyPair getKeyPair(ValueSigner signer, int keyLength, String id) throws NoSuchAlgorithmException {
KeyLookup mapKey = new KeyLookup(signer, keyLength, id);
if (!generatedKeys.containsKey(mapKey)) {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(signer.getKeyGenerationAlgorithm());
keyGen.initialize(keyLength);
generatedKeys.put(mapKey, keyGen.generateKeyPair());
}
return generatedKeys.get(mapKey);
}
/**
* Get the key length for a given signer.
*
* @param signer
* The singer to get the length for.
* @return The length for generating keys.
*/
private static int getKeyGenLength(ValueSigner signer) {
switch (signer) {
case RSA_PKCS1:
case RSA_PSS:
case DSA:
return 1024;
case ECDSA:
return 256;
default:
throw new UnsupportedOperationException();
}
}
/**
* Create mock containers for each value signer for the given signing key id and verifying key id.
*
* @param signingKeyId
* Id of the user the signing key is for.
* @param verifyingKeyIds
* Id of the users that we will verify signatures for.
*/
static Map<ValueSigner,SignatureKeyContainer> getContainers(String signingKeyId, String... verifyingKeyIds) throws NoSuchAlgorithmException {
Map<ValueSigner,SignatureKeyContainer> containers = new HashMap<>();
for (ValueSigner signer : ValueSigner.values()) {
containers.put(signer, new MockSignatureKeyContainer(signer, getKeyGenLength(signer), signingKeyId, verifyingKeyIds));
}
return containers;
}
}
@@ -0,0 +1,59 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import org.junit.Test;
import edu.mit.ll.pace.signature.SignatureConfig.Destination;
/**
* Test for {@link Destination}.
*/
public class SignatureConfigDestinationTest {
@Test
public void validEnumTest() {
assertThat("should have three values", Destination.values(), is(arrayWithSize(3)));
}
@Test
public void toStringTest() {
assertThat("toString should return correct value", Destination.VALUE.toString(), is("value"));
assertThat("toString should return correct value", Destination.COLUMN_VISIBILITY.toString(), is("colVis"));
assertThat("toString should return correct value", Destination.SEPARATE_TABLE.toString(), is("table"));
}
@Test
public void fromStringTest() {
assertThat("fromString should return correct enum value", Destination.fromString("value"), is(Destination.VALUE));
assertThat("fromString should return correct enum value", Destination.fromString("colVis"), is(Destination.COLUMN_VISIBILITY));
assertThat("fromString should return correct enum value", Destination.fromString("table"), is(Destination.SEPARATE_TABLE));
}
@Test
public void fromStringExceptionTest() {
try {
Destination.fromString("bad");
fail("only valid names should be allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
}
@@ -0,0 +1,208 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static edu.mit.ll.pace.test.Matchers.equalTo;
import static edu.mit.ll.pace.test.Matchers.hasSameFieldsAs;
import static edu.mit.ll.pace.test.TestUtils.getResourceAsStream;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import org.hamcrest.Matchers;
import org.ini4j.Ini;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import edu.mit.ll.pace.internal.Utils;
import edu.mit.ll.pace.signature.SignatureConfig.Destination;
/**
* Test for {@link SignatureConfig} and {@link SignatureConfigBuilder}.
*/
public class SignatureConfigTest {
@Rule
public TemporaryFolder temp = new TemporaryFolder();
// Configurations matching the ini files in the resource directory.
private static final SignatureConfig config1 = new SignatureConfigBuilder().setSigner(ValueSigner.RSA_PSS).setProvider("BC")
.setDestination(Destination.VALUE).build();
private static final SignatureConfig config2 = new SignatureConfigBuilder().setSigner(ValueSigner.RSA_PKCS1).setDestination(Destination.COLUMN_VISIBILITY)
.setDefaultVisibility("default".getBytes(Utils.VISIBILITY_CHARSET)).build();
private static final SignatureConfig config3 = new SignatureConfigBuilder().setSigner(ValueSigner.ECDSA).setDestination(Destination.SEPARATE_TABLE)
.setDestinationTable("sigs").build();
@Test
public void constructorExceptionTest() {
try {
getValidBuilder().setSigner(null).build();
fail("signer must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(null).build();
fail("destination must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(Destination.SEPARATE_TABLE).setDestinationTable(null).build();
fail("if storing to a separate table, destination table must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(Destination.SEPARATE_TABLE).setDestinationTable("").build();
fail("if storing to a separate table, destination table must not be empty");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(Destination.VALUE).setDestinationTable("something").build();
fail("if not storing to a separate table, destination table must not be set");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(Destination.COLUMN_VISIBILITY).setDefaultVisibility(null).build();
fail("if storing to the column visibility field, default visibility must not be null");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(Destination.COLUMN_VISIBILITY).setDefaultVisibility(Utils.EMPTY).build();
fail("if storing to the column visibility field, default visibility must not be empty");
} catch (IllegalArgumentException e) { /* expected */}
try {
getValidBuilder().setDestination(Destination.VALUE).setDefaultVisibility("something".getBytes(Utils.VISIBILITY_CHARSET)).build();
fail("if not storing to the column visibility field, default visibility must not be set");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void readTest() throws Exception {
SignatureConfig actualConfig = new SignatureConfigBuilder().readFromFile(getReader("config1.ini")).build();
assertThat("reading the ini file produces the correct configuration", actualConfig, hasSameFieldsAs(config1));
actualConfig = new SignatureConfigBuilder().readFromFile(getReader("config1WithoutDefault.ini")).build();
assertThat("reading the ini file produces the correct configuration", actualConfig, hasSameFieldsAs(config1));
actualConfig = new SignatureConfigBuilder().readFromFile(getReader("config2.ini")).build();
assertThat("reading the ini file produces the correct configuration", actualConfig, hasSameFieldsAs(config2));
actualConfig = new SignatureConfigBuilder().readFromFile(getReader("config3.ini")).build();
assertThat("reading the ini file produces the correct configuration", actualConfig, hasSameFieldsAs(config3));
}
@Test
public void writeTest() throws Exception {
File file = writeConfigToFile(config1);
Ini expectedIni = getIni("config1.ini");
Ini actualIni = new Ini(file);
assertThat("writing the ini file produces the correct configuration", expectedIni, equalTo(actualIni));
file = writeConfigToFile(config2);
expectedIni = getIni("config2.ini");
actualIni = new Ini(file);
assertThat("writing the ini file produces the correct configuration", expectedIni, equalTo(actualIni));
file = writeConfigToFile(config3);
expectedIni = getIni("config3.ini");
actualIni = new Ini(file);
assertThat("writing the ini file produces the correct configuration", expectedIni, equalTo(actualIni));
}
@Test
public void writeReadTest() throws Exception {
File file = writeConfigToFile(config1);
SignatureConfig actualConfig = new SignatureConfigBuilder().readFromFile(new FileReader(file)).build();
assertThat("writing then reading the config produces an equivalent configuration", actualConfig, hasSameFieldsAs(config1));
file = writeConfigToFile(config2);
actualConfig = new SignatureConfigBuilder().readFromFile(new FileReader(file)).build();
assertThat("writing then reading the config produces an equivalent configuration", actualConfig, hasSameFieldsAs(config2));
file = writeConfigToFile(config3);
actualConfig = new SignatureConfigBuilder().readFromFile(new FileReader(file)).build();
assertThat("writing then reading the config produces an equivalent configuration", actualConfig, hasSameFieldsAs(config3));
}
@Test
public void getAlgorithmTest() {
assertThat("correct algorithm", config1.getAlgorithm(), Matchers.equalTo(config1.algorithm));
assertThat("correct algorithm", config2.getAlgorithm(), Matchers.equalTo(config2.algorithm));
assertThat("correct algorithm", config3.getAlgorithm(), Matchers.equalTo(config3.algorithm));
}
@Test
public void isSignatureInSeparateTableTest() {
assertThat("correct return value", config1.isSignatureInSeparateTable(), is(false));
assertThat("correct return value", config2.isSignatureInSeparateTable(), is(false));
assertThat("correct return value", config3.isSignatureInSeparateTable(), is(true));
}
/**
* Write the given configuration to a file.
*
* @param config
* Configuration to write.
* @return The file that was written to.
*/
private File writeConfigToFile(SignatureConfig config) throws IOException {
File file = temp.newFile();
FileWriter writer = new FileWriter(file);
config.write(writer);
writer.close();
return file;
}
/**
* Get a builder wrapping a valid configuration.
*
* @return Valid builder.
*/
private SignatureConfigBuilder getValidBuilder() {
return new SignatureConfigBuilder().setSigner(ValueSigner.RSA_PKCS1).setDestination(Destination.VALUE);
}
/**
* Get the requested INI.
*
* @param resource
* INI file to get.
* @return The INI file.
*/
private Ini getIni(String resource) throws IOException {
return new Ini(getResourceAsStream(this.getClass(), resource));
}
/**
* Get the requested INI.
*
* @param resource
* INI file to get.
* @return The INI file.
*/
private Reader getReader(String resource) throws IOException {
return new InputStreamReader(getResourceAsStream(this.getClass(), resource));
}
}
@@ -0,0 +1,552 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.client.BatchScanner;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.IteratorSetting.Column;
import org.apache.accumulo.core.client.sample.SamplerConfiguration;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.hadoop.io.Text;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import edu.mit.ll.pace.internal.Utils;
import edu.mit.ll.pace.test.Matchers;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test {@link SignedBatchScanner}.
*/
public class SignedBatchScannerTest {
private final static String TEST_TABLE = "test";
private final static String SIG_TABLE = "sigs";
private final static Authorizations authorizations = new Authorizations("admin", "secret", "top secret");
private Map<ValueSigner,SignatureKeyContainer> aliceKeyContainers = new HashMap<>();
@Mock
private Connector mockConnector;
@Mock
private BatchScanner mockScanner;
@Mock
private BatchScanner mockSignatureScanner;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
public SignedBatchScannerTest() throws NoSuchAlgorithmException {
aliceKeyContainers = MockSignatureKeyContainer.getContainers("alice", "alice");
}
@BeforeClass
public static void setupBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void badConstructorTest() throws Exception {
try {
new SignedBatchScanner(null, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null connector not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedBatchScanner(mockConnector, null, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null table name not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedBatchScanner(mockConnector, TEST_TABLE, null, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null authorizations not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, null, aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null crypto config not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), null);
fail("null key container not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void constructorTest() throws Exception {
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
verify(mockConnector).createBatchScanner(TEST_TABLE, authorizations, 1);
verify(mockConnector, never()).createBatchScanner(SIG_TABLE, authorizations, 1);
}
@Test
public void constructorExternalTest() throws Exception {
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
verify(mockConnector).createBatchScanner(TEST_TABLE, authorizations, 1);
verify(mockConnector).createBatchScanner(SIG_TABLE, authorizations, 1);
}
@Test
public void iteratorTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
EntrySigner signer = new EntrySigner(getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
List<Entry<Key,Value>> entries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(Utils.VISIBILITY_CHARSET), 0,
false, false), new Value(new byte[] {4}));
Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7}, "secret".getBytes(Utils.VISIBILITY_CHARSET),
0, false, false), new Value(new byte[] {8}));
entries.add(signer.sign(entry, true));
entries.add(signer.sign(entry2, true));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
BatchScanner scanner = new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"),
aliceKeyContainers.get(ValueSigner.RSA_PSS));
assertThat("has correct number of elements", scanner, iterableWithSize(2));
Iterator<Entry<Key,Value>> iterator = scanner.iterator();
assertThat("correct item", iterator.next(), Matchers.equalTo(entry));
assertThat("correct item", iterator.next(), Matchers.equalTo(entry2));
}
@Test
public void iteratorExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
EntrySigner signer = new EntrySigner(getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA));
List<Entry<Key,Value>> entries = new ArrayList<>();
List<Entry<Key,Value>> signedEntries = new ArrayList<>();
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3}, "secret".getBytes(Utils.VISIBILITY_CHARSET), 0,
false, false), new Value(new byte[] {4}));
Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7}, "secret".getBytes(Utils.VISIBILITY_CHARSET),
0, false, false), new Value(new byte[] {8}));
entries.add(entry);
entries.add(entry2);
signedEntries.add(signer.sign(entry, true));
signedEntries.add(signer.sign(entry2, true));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
when(mockSignatureScanner.iterator()).thenReturn(signedEntries.iterator()).thenReturn(signedEntries.iterator());
BatchScanner scanner = new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"),
aliceKeyContainers.get(ValueSigner.ECDSA));
assertThat("has correct number of elements", scanner, iterableWithSize(2));
Iterator<Entry<Key,Value>> iterator = scanner.iterator();
assertThat("correct item", iterator.next(), Matchers.equalTo(entry));
assertThat("correct item", iterator.next(), Matchers.equalTo(entry2));
}
@Test
public void addScanIteratorTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
IteratorSetting test = new IteratorSetting(10, "test", "test2");
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.addScanIterator(test);
verify(mockScanner).addScanIterator(test);
}
@Test
public void addScanIteratorExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
IteratorSetting test = new IteratorSetting(10, "test", "test2");
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.addScanIterator(test);
verify(mockScanner).addScanIterator(test);
verify(mockSignatureScanner).addScanIterator(test);
}
@Test
public void clearColumns() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).clearColumns();
verify(mockScanner).clearColumns();
}
@Test
public void clearExternalColumns() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).clearColumns();
verify(mockScanner).clearColumns();
verify(mockSignatureScanner).clearColumns();
}
@Test
public void clearScanIteratorsTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearScanIterators();
verify(mockScanner).clearScanIterators();
}
@Test
public void clearScanIteratorsExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearScanIterators();
verify(mockScanner).clearScanIterators();
verify(mockSignatureScanner).clearScanIterators();
}
@Test
public void setRangesTest() throws Exception {
Collection<Range> ranges = Collections.singletonList(new Range("test"));
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setRanges(ranges);
verify(mockScanner).setRanges(ranges);
}
@Test
public void setRangesExternalTest() throws Exception {
Collection<Range> ranges = Collections.singletonList(new Range("test"));
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setRanges(ranges);
verify(mockScanner).setRanges(ranges);
verify(mockSignatureScanner).setRanges(ranges);
}
@Test
public void closeTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).close();
verify(mockScanner).close();
}
@Test
public void closeExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).close();
verify(mockScanner).close();
verify(mockSignatureScanner).close();
}
@Test
public void fetchColumnTest() throws Exception {
Column column = new Column(new Text(new byte[] {1}), new Text(new byte[] {2}));
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.fetchColumn(column);
verify(mockScanner).fetchColumn(column);
}
@Test
public void fetchColumnExternalTest() throws Exception {
Column column = new Column(new Text(new byte[] {1}), new Text(new byte[] {2}));
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.fetchColumn(column);
verify(mockScanner).fetchColumn(column);
verify(mockSignatureScanner).fetchColumn(column);
}
@Test
public void fetchColumn2Test() throws Exception {
Text colF = new Text(new byte[] {1}), colQ = new Text(new byte[] {2});
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).fetchColumn(
colF, colQ);
verify(mockScanner).fetchColumn(colF, colQ);
}
@Test
public void fetchColumn2ExternalTest() throws Exception {
Text colF = new Text(new byte[] {1}), colQ = new Text(new byte[] {2});
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).fetchColumn(
colF, colQ);
verify(mockScanner).fetchColumn(colF, colQ);
verify(mockSignatureScanner).fetchColumn(colF, colQ);
}
@Test
public void fetchColumnFamilyTest() throws Exception {
Text colF = new Text(new byte[] {1});
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.fetchColumnFamily(colF);
verify(mockScanner).fetchColumnFamily(colF);
}
@Test
public void fetchColumnFamilyExternalTest() throws Exception {
Text colF = new Text(new byte[] {1});
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.fetchColumnFamily(colF);
verify(mockScanner).fetchColumnFamily(colF);
verify(mockSignatureScanner).fetchColumnFamily(colF);
}
@Test
public void getAuthorizationsTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockScanner.getAuthorizations()).thenReturn(authorizations);
Authorizations auths = new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"),
aliceKeyContainers.get(ValueSigner.RSA_PSS)).getAuthorizations();
verify(mockScanner).getAuthorizations();
assertThat("correct authorizations returned", auths, equalTo(authorizations));
}
@Test
public void getSamplerConfigurationTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
SamplerConfiguration config = new SamplerConfiguration("test");
when(mockScanner.getSamplerConfiguration()).thenReturn(config);
SamplerConfiguration value = new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"),
aliceKeyContainers.get(ValueSigner.RSA_PSS)).getSamplerConfiguration();
verify(mockScanner).getSamplerConfiguration();
assertThat("correct config returned", value, equalTo(config));
}
@Test
public void setSamplerConfigurationTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
SamplerConfiguration config = new SamplerConfiguration("test");
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setSamplerConfiguration(config);
verify(mockScanner).setSamplerConfiguration(config);
}
@Test
public void setSamplerConfigurationExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
SamplerConfiguration config = new SamplerConfiguration("test");
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setSamplerConfiguration(config);
verify(mockScanner).setSamplerConfiguration(config);
verify(mockSignatureScanner).setSamplerConfiguration(config);
}
@Test
public void clearSamplerConfigurationTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearSamplerConfiguration();
verify(mockScanner).clearSamplerConfiguration();
}
@Test
public void clearSamplerConfigurationExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearSamplerConfiguration();
verify(mockScanner).clearSamplerConfiguration();
verify(mockSignatureScanner).clearSamplerConfiguration();
}
@Test
public void setBatchTimeoutTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setBatchTimeout(5L, TimeUnit.DAYS);
verify(mockScanner).setBatchTimeout(5L, TimeUnit.DAYS);
}
@Test
public void setBatchTimeoutExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setBatchTimeout(5L, TimeUnit.DAYS);
verify(mockScanner).setBatchTimeout(5L, TimeUnit.DAYS);
verify(mockSignatureScanner).setBatchTimeout(5L, TimeUnit.DAYS);
}
@Test
public void getBatchTimeoutTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockScanner.getBatchTimeout(TimeUnit.DAYS)).thenReturn(5L);
long value = new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getBatchTimeout(TimeUnit.DAYS);
verify(mockScanner).getBatchTimeout(TimeUnit.DAYS);
assertThat("correct timeout returned", value, is(5L));
}
@Test
public void setClassLoaderContextTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setClassLoaderContext("test");
verify(mockScanner).setClassLoaderContext("test");
}
@Test
public void setClassLoaderContextExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setClassLoaderContext("test");
verify(mockScanner).setClassLoaderContext("test");
verify(mockSignatureScanner).setClassLoaderContext("test");
}
@Test
public void clearClassLoaderContextTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearClassLoaderContext();
verify(mockScanner).clearClassLoaderContext();
}
@Test
public void clearClassLoaderContextExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearClassLoaderContext();
verify(mockScanner).clearClassLoaderContext();
verify(mockSignatureScanner).clearClassLoaderContext();
}
@Test
public void getClassLoaderContextTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockScanner.getClassLoaderContext()).thenReturn("test");
String value = new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getClassLoaderContext();
verify(mockScanner).getClassLoaderContext();
assertThat("correct class loader context returned", value, is("test"));
}
@Test
public void getTimeoutTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockScanner.getTimeout(TimeUnit.DAYS)).thenReturn(5L);
Long value = new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getTimeout(TimeUnit.DAYS);
verify(mockScanner).getTimeout(TimeUnit.DAYS);
assertThat("correct timeout returned", value, is(5L));
}
@Test
public void removeScanIteratorTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.removeScanIterator("test");
verify(mockScanner).removeScanIterator("test");
}
@Test
public void removeScanIteratorExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.removeScanIterator("test");
verify(mockScanner).removeScanIterator("test");
verify(mockSignatureScanner).removeScanIterator("test");
}
@Test
public void updateScanIteratorOptionTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.updateScanIteratorOption("test", "a", "b");
verify(mockScanner).updateScanIteratorOption("test", "a", "b");
}
@Test
public void updateScanIteratorOptionExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.updateScanIteratorOption("test", "a", "b");
verify(mockScanner).updateScanIteratorOption("test", "a", "b");
verify(mockSignatureScanner).updateScanIteratorOption("test", "a", "b");
}
@Test
public void setTimeoutTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setTimeout(5L,
TimeUnit.DAYS);
verify(mockScanner).setTimeout(5L, TimeUnit.DAYS);
}
@Test
public void setTimeoutExternalTest() throws Exception {
when(mockConnector.createBatchScanner(TEST_TABLE, authorizations, 1)).thenReturn(mockScanner);
when(mockConnector.createBatchScanner(SIG_TABLE, authorizations, 1)).thenReturn(mockSignatureScanner);
new SignedBatchScanner(mockConnector, TEST_TABLE, authorizations, 1, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setTimeout(5L,
TimeUnit.DAYS);
verify(mockScanner).setTimeout(5L, TimeUnit.DAYS);
verify(mockSignatureScanner).setTimeout(5L, TimeUnit.DAYS);
}
/**
* Get an signor config.
*
* @param resource
* Resource file containing the configuration.
* @return EncryptionConfig.
*/
private SignatureConfig getConfig(String resource) throws Exception {
return new SignatureConfigBuilder().readFromFile(new InputStreamReader(TestUtils.getResourceAsStream(this.getClass(), resource))).build();
}
}
@@ -0,0 +1,507 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.accumulo.core.client.BatchWriter;
import org.apache.accumulo.core.client.BatchWriterConfig;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.data.ColumnUpdate;
import org.apache.accumulo.core.data.Mutation;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import edu.mit.ll.pace.internal.MutableEntry;
import edu.mit.ll.pace.internal.Utils;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test {@link SignedBatchWriter}.
*/
public class SignedBatchWriterTest {
private final static String TEST_TABLE = "test";
private final static String SIG_TABLE = "sigs";
private Map<ValueSigner,SignatureKeyContainer> aliceKeyContainers = new HashMap<>();
private Map<ValueSigner,SignatureKeyContainer> bobKeyContainers = new HashMap<>();
@Mock
private Connector mockConnector;
@Mock
private BatchWriter mockWriter;
@Mock
private BatchWriter mockSignatureWriter;
@Captor
private ArgumentCaptor<Mutation> captor;
@Captor
private ArgumentCaptor<Mutation> signatureCaptor;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
public SignedBatchWriterTest() throws NoSuchAlgorithmException {
aliceKeyContainers = MockSignatureKeyContainer.getContainers("alice", "alice", "bob");
bobKeyContainers = MockSignatureKeyContainer.getContainers("bob", "alice", "bob");
}
@BeforeClass
public static void setupBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void badConstructorTest() throws Exception {
try {
new SignedBatchWriter(null, TEST_TABLE, null, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null connector not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedBatchWriter(mockConnector, null, null, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null table name not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedBatchWriter(mockConnector, TEST_TABLE, null, null, aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null crypto config not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config1.ini"), null);
fail("null key container not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void constructorTest() throws Exception {
BatchWriterConfig config = new BatchWriterConfig();
new SignedBatchWriter(mockConnector, TEST_TABLE, config, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
verify(mockConnector).createBatchWriter(TEST_TABLE, config);
}
@Test
public void constructorExternalTest() throws Exception {
BatchWriterConfig config = new BatchWriterConfig();
new SignedBatchWriter(mockConnector, TEST_TABLE, config, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA));
verify(mockConnector).createBatchWriter(TEST_TABLE, config);
verify(mockConnector).createBatchWriter(SIG_TABLE, config);
}
@Test
public void addMutationValueTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
EntrySigner signer = new EntrySigner(getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
Mutation mutation = new Mutation("row".getBytes());
mutation.put("colF".getBytes(), "colQ".getBytes(), "val1".getBytes());
mutation.put("colF".getBytes(), "colQ".getBytes(), 0, "val2".getBytes());
writer.addMutation(mutation);
verify(mockWriter).addMutation(captor.capture());
verify(mockSignatureWriter, never()).addMutation(any());
List<Mutation> mutations = captor.getAllValues();
assertThat("only a single mutation", mutations, hasSize(1));
Mutation signed = mutations.get(0);
assertThat("row is unchanged", signed.getRow(), equalTo("row".getBytes()));
List<ColumnUpdate> updates = signed.getUpdates();
assertThat("has 2 updates", updates, hasSize(2));
ColumnUpdate update = updates.get(0);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp not set", update.hasTimestamp(), is(false));
assertThat("signature in value", update.getColumnFamily(), not(equalTo("val1".getBytes())));
MutableEntry verified = new MutableEntry(signer.verify(new MutableEntry("row".getBytes(), update).toEntry()));
assertThat("value was correctly unwrapped", verified.value, equalTo("val1".getBytes()));
update = updates.get(1);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp is set", update.hasTimestamp(), is(true));
assertThat("timestamp is correct", update.getTimestamp(), is(0L));
assertThat("signature in value", update.getColumnFamily(), not(equalTo("val2".getBytes())));
verified = new MutableEntry(signer.verify(new MutableEntry("row".getBytes(), update).toEntry()));
assertThat("value was correctly unwrapped", verified.value, equalTo("val2".getBytes()));
}
@Test
public void addMutationColVisTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config2.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
EntrySigner signer = new EntrySigner(getConfig("config2.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
Mutation mutation = new Mutation("row".getBytes());
mutation.put("colF".getBytes(), "colQ".getBytes(), "val1".getBytes());
mutation.put("colF".getBytes(), "colQ".getBytes(), 0, "val2".getBytes());
writer.addMutation(mutation);
verify(mockWriter).addMutation(captor.capture());
verify(mockSignatureWriter, never()).addMutation(any());
List<Mutation> mutations = captor.getAllValues();
assertThat("only a single mutation", mutations, iterableWithSize(1));
Mutation signed = mutations.get(0);
assertThat("row is unchanged", signed.getRow(), equalTo("row".getBytes()));
List<ColumnUpdate> updates = signed.getUpdates();
assertThat("has 2 updates", updates, hasSize(2));
ColumnUpdate update = updates.get(0);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnQualifier(), equalTo("colQ".getBytes()));
assertThat("colQualifier has the default visibility", new String(update.getColumnVisibility(), Utils.VISIBILITY_CHARSET), startsWith("(default)"));
assertThat("timestamp not set", update.hasTimestamp(), is(false));
assertThat("value is unchanged", update.getValue(), equalTo("val1".getBytes()));
MutableEntry verified = new MutableEntry(signer.verify(new MutableEntry("row".getBytes(), update).toEntry()));
assertThat("value was correctly unwrapped", verified.value, equalTo("val1".getBytes()));
update = updates.get(1);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnQualifier(), equalTo("colQ".getBytes()));
assertThat("colQualifier has the default visibility", new String(update.getColumnVisibility(), Utils.VISIBILITY_CHARSET), startsWith("(default)"));
assertThat("timestamp is set", update.hasTimestamp(), is(true));
assertThat("timestamp is correct", update.getTimestamp(), is(0L));
assertThat("value is unchanged", update.getValue(), equalTo("val2".getBytes()));
verified = new MutableEntry(signer.verify(new MutableEntry("row".getBytes(), update).toEntry()));
assertThat("value was correctly unwrapped", verified.value, equalTo("val2".getBytes()));
}
@Test
public void addMutationSeparateTableTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
when(mockConnector.createBatchWriter(SIG_TABLE, null)).thenReturn(mockSignatureWriter);
BatchWriter writer = new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA));
EntrySigner signer = new EntrySigner(getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA));
Mutation mutation = new Mutation("row".getBytes());
mutation.put("colF".getBytes(), "colQ".getBytes(), "val1".getBytes());
mutation.put("colF".getBytes(), "colQ".getBytes(), 0, "val2".getBytes());
writer.addMutation(mutation);
verify(mockWriter).addMutation(captor.capture());
verify(mockSignatureWriter).addMutation(signatureCaptor.capture());
// Check entry
List<Mutation> mutations = captor.getAllValues();
assertThat("only a single mutation", mutations, hasSize(1));
Mutation signed = mutations.get(0);
assertThat("row is unchanged", signed.getRow(), equalTo("row".getBytes()));
List<ColumnUpdate> updates = signed.getUpdates();
assertThat("has 2 updates", updates, hasSize(2));
ColumnUpdate update = updates.get(0);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp not set", update.hasTimestamp(), is(false));
assertThat("value is unchanged", update.getValue(), equalTo("val1".getBytes()));
update = updates.get(1);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp is set", update.hasTimestamp(), is(true));
assertThat("timestamp is correct", update.getTimestamp(), is(0L));
assertThat("value is unchanged", update.getValue(), equalTo("val2".getBytes()));
// Check signature.
mutations = signatureCaptor.getAllValues();
assertThat("only a single mutation", mutations, hasSize(1));
signed = mutations.get(0);
assertThat("row is unchanged", signed.getRow(), equalTo("row".getBytes()));
updates = signed.getUpdates();
assertThat("has 2 updates", updates, hasSize(2));
update = updates.get(0);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp not set", update.hasTimestamp(), is(false));
assertThat("signature in value", update.getColumnFamily(), not(equalTo("val1".getBytes())));
update = updates.get(1);
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp is set", update.hasTimestamp(), is(true));
assertThat("timestamp is correct", update.getTimestamp(), is(0L));
assertThat("signature in value", update.getColumnFamily(), not(equalTo("val2".getBytes())));
// Verify unwrapping of entry.
MutableEntry verified = new MutableEntry(signer.verify(new MutableEntry("row".getBytes(), captor.getAllValues().get(0).getUpdates().get(0)).toEntry(),
new MutableEntry("row".getBytes(), signatureCaptor.getAllValues().get(0).getUpdates().get(0)).toEntry()));
assertThat("value was correctly unwrapped", verified.value, equalTo("val1".getBytes()));
}
@Test
public void deleteTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
Mutation mutation = new Mutation("row".getBytes());
mutation.putDelete("colF".getBytes(), "colQ".getBytes());
mutation.putDelete("colF".getBytes(), "colQ".getBytes(), 0L);
writer.addMutation(mutation);
verify(mockWriter).addMutation(captor.capture());
verify(mockSignatureWriter, never()).addMutation(any());
List<Mutation> mutations = captor.getAllValues();
assertThat("only a single mutation", mutations, iterableWithSize(1));
Mutation signed = mutations.get(0);
assertThat("row is unchanged", signed.getRow(), equalTo("row".getBytes()));
List<ColumnUpdate> updates = signed.getUpdates();
assertThat("has 2 updates", updates, hasSize(2));
ColumnUpdate update = updates.get(0);
assertThat("is delete operation", update.isDeleted(), is(true));
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnQualifier(), equalTo("colQ".getBytes()));
assertThat("timestamp is not set", update.hasTimestamp(), is(false));
update = updates.get(1);
assertThat("is delete operation", update.isDeleted(), is(true));
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnQualifier(), equalTo("colQ".getBytes()));
assertThat("timestamp is set", update.hasTimestamp(), is(true));
assertThat("timestamp is correct", update.getTimestamp(), is(0L));
}
@Test
public void deleteColVisTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config2.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
Mutation mutation = new Mutation("row".getBytes());
mutation.putDelete("colF1".getBytes(), "colQ".getBytes());
mutation.putDelete("colF1".getBytes(), "colQ".getBytes(), 0L);
try {
writer.addMutation(mutation);
fail("signed deletes are not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void deleteExternalTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
when(mockConnector.createBatchWriter(SIG_TABLE, null)).thenReturn(mockSignatureWriter);
BatchWriter writer = new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA));
Mutation mutation = new Mutation("row".getBytes());
mutation.putDelete("colF".getBytes(), "colQ".getBytes());
mutation.putDelete("colF".getBytes(), "colQ".getBytes(), 0);
writer.addMutation(mutation);
verify(mockWriter).addMutation(captor.capture());
verify(mockSignatureWriter).addMutation(signatureCaptor.capture());
// Check entry
List<Mutation> mutations = captor.getAllValues();
assertThat("only a single mutation", mutations, hasSize(1));
Mutation signed = mutations.get(0);
assertThat("row is unchanged", signed.getRow(), equalTo("row".getBytes()));
List<ColumnUpdate> updates = signed.getUpdates();
assertThat("has 2 updates", updates, hasSize(2));
ColumnUpdate update = updates.get(0);
assertThat("is delete operation", update.isDeleted(), is(true));
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp not set", update.hasTimestamp(), is(false));
update = updates.get(1);
assertThat("is delete operation", update.isDeleted(), is(true));
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp is set", update.hasTimestamp(), is(true));
assertThat("timestamp is correct", update.getTimestamp(), is(0L));
// Check signature.
mutations = signatureCaptor.getAllValues();
assertThat("only a single mutation", mutations, hasSize(1));
signed = mutations.get(0);
assertThat("row is unchanged", signed.getRow(), equalTo("row".getBytes()));
updates = signed.getUpdates();
assertThat("has 2 updates", updates, hasSize(2));
update = updates.get(0);
assertThat("is delete operation", update.isDeleted(), is(true));
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp not set", update.hasTimestamp(), is(false));
update = updates.get(1);
assertThat("is delete operation", update.isDeleted(), is(true));
assertThat("column family is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("column qualifier is unchanged", update.getColumnFamily(), equalTo("colF".getBytes()));
assertThat("timestamp is set", update.hasTimestamp(), is(true));
assertThat("timestamp is correct", update.getTimestamp(), is(0L));
}
@Test
public void addMutationsTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
BatchWriter writer = new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
List<Mutation> mutations = new ArrayList<>();
Mutation mutation = new Mutation("row");
mutation.put("colF", "colQ", "val");
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(1)).addMutation(any()); // 1 time
verify(mockSignatureWriter, never()).addMutation(any());
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(3)).addMutation(any()); // 1 + 2 times
verify(mockSignatureWriter, never()).addMutation(any());
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(6)).addMutation(any()); // 1 + 2 + 3 times
verify(mockSignatureWriter, never()).addMutation(any());
}
@Test
public void addMutationsExternalTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
when(mockConnector.createBatchWriter(SIG_TABLE, null)).thenReturn(mockSignatureWriter);
BatchWriter writer = new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA));
List<Mutation> mutations = new ArrayList<>();
Mutation mutation = new Mutation("row");
mutation.put("colF", "colQ", "val");
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(1)).addMutation(any()); // 1 time
verify(mockSignatureWriter, times(1)).addMutation(any()); // 1 time
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(3)).addMutation(any()); // 1 + 2 times
verify(mockSignatureWriter, times(3)).addMutation(any()); // 1 + 2 times
mutations.add(mutation);
writer.addMutations(mutations);
verify(mockWriter, times(6)).addMutation(any()); // 1 + 2 + 3 times
verify(mockSignatureWriter, times(6)).addMutation(any()); // 1 + 2 + 3 times
}
@Test
public void flushTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).flush();
verify(mockWriter).flush();
}
@Test
public void flushExternalTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
when(mockConnector.createBatchWriter(SIG_TABLE, null)).thenReturn(mockSignatureWriter);
new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA)).flush();
verify(mockWriter).flush();
verify(mockSignatureWriter).flush();
}
@Test
public void closeTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).close();
verify(mockWriter).close();
}
@Test
public void closeExternalTest() throws Exception {
when(mockConnector.createBatchWriter(TEST_TABLE, null)).thenReturn(mockWriter);
when(mockConnector.createBatchWriter(SIG_TABLE, null)).thenReturn(mockSignatureWriter);
new SignedBatchWriter(mockConnector, TEST_TABLE, null, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA)).close();
verify(mockWriter).close();
verify(mockSignatureWriter).close();
}
/**
* Get an encryptor config.
*
* @param resource
* Resource file containing the configuration.
* @return EncryptionConfig.
*/
private SignatureConfig getConfig(String resource) throws Exception {
return new SignatureConfigBuilder().readFromFile(new InputStreamReader(TestUtils.getResourceAsStream(this.getClass(), resource))).build();
}
}
@@ -0,0 +1,352 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static edu.mit.ll.pace.internal.Utils.VISIBILITY_CHARSET;
import static edu.mit.ll.pace.test.Matchers.equalTo;
import static edu.mit.ll.pace.test.TestUtils.getResourceAsStream;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NoSuchElementException;
import java.util.Random;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import edu.mit.ll.pace.internal.Utils;
import edu.mit.ll.pace.test.Matchers;
/**
* Tests for {@link SignedInlineScannerIterator}
*/
public class SignedIteratorTest {
private Map<ValueSigner,SignatureKeyContainer> aliceKeyContainers = new HashMap<>();
private Map<ValueSigner,SignatureKeyContainer> bobKeyContainers = new HashMap<>();
public SignedIteratorTest() throws NoSuchAlgorithmException {
aliceKeyContainers = MockSignatureKeyContainer.getContainers("alice", "alice", "bob");
bobKeyContainers = MockSignatureKeyContainer.getContainers("bob", "alice", "bob");
}
@BeforeClass
public static void setupBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void iteratorValueTest() throws Exception {
Random random = new Random();
List<Entry<Key,Value>> entries = new ArrayList<>();
for (int i = 0; i < 100; i++) {
byte[] bytes = new byte[32 * 4];
random.nextBytes(bytes);
entries.add(new SimpleImmutableEntry<>(new Key(Arrays.copyOfRange(bytes, 0, 32), Arrays.copyOfRange(bytes, 32, 64), Arrays.copyOfRange(bytes, 64, 96),
"secret".getBytes(VISIBILITY_CHARSET), (long) 0, false), new Value(Arrays.copyOfRange(bytes, 96, 128))));
}
EntrySigner signer = getSigner("config1.ini", aliceKeyContainers.get(ValueSigner.RSA_PSS));
List<Entry<Key,Value>> signedEntries = new ArrayList<>(entries.size());
for (Entry<Key,Value> entry : entries) {
signedEntries.add(signer.sign(entry, true));
}
EntrySigner verifier = getSigner("config1.ini", bobKeyContainers.get(ValueSigner.RSA_PSS));
Iterator<Entry<Key,Value>> entriesIterator = entries.iterator();
Iterator<Entry<Key,Value>> signedIterator = new SignedInlineScannerIterator(signedEntries.iterator(), verifier);
while (entriesIterator.hasNext()) {
assertThat("should return all entries", signedIterator.hasNext(), is(true));
assertThat("should return same entry", entriesIterator.next(), equalTo(signedIterator.next()));
}
assertThat("should not return any more entries", signedIterator.hasNext(), is(false));
}
@Test
public void iteratorColVisTest() throws Exception {
Random random = new Random();
List<Entry<Key,Value>> entries = new ArrayList<>();
for (int i = 0; i < 100; i++) {
byte[] bytes = new byte[32 * 4];
random.nextBytes(bytes);
entries.add(new SimpleImmutableEntry<>(new Key(Arrays.copyOfRange(bytes, 0, 32), Arrays.copyOfRange(bytes, 32, 64), Arrays.copyOfRange(bytes, 64, 96),
"secret".getBytes(VISIBILITY_CHARSET), (long) 0, false), new Value(Arrays.copyOfRange(bytes, 96, 128))));
}
EntrySigner signer = getSigner("config2.ini", aliceKeyContainers.get(ValueSigner.RSA_PKCS1));
List<Entry<Key,Value>> signedEntries = new ArrayList<>(entries.size());
for (Entry<Key,Value> entry : entries) {
signedEntries.add(signer.sign(entry, true));
}
EntrySigner verifier = getSigner("config2.ini", bobKeyContainers.get(ValueSigner.RSA_PKCS1));
Iterator<Entry<Key,Value>> entriesIterator = entries.iterator();
SignedInlineScannerIterator signedIterator = new SignedInlineScannerIterator(signedEntries.iterator(), verifier);
while (entriesIterator.hasNext()) {
assertThat("should return all entries", signedIterator.hasNext(), is(true));
assertThat("should return same entry", entriesIterator.next(), equalTo(signedIterator.next()));
}
assertThat("should not return any more entries", signedIterator.hasNext(), is(false));
}
@Test
public void iteratorTableTest() throws Exception {
Random random = new Random();
List<Entry<Key,Value>> entries = new ArrayList<>();
for (int i = 0; i < 100; i++) {
byte[] bytes = new byte[32 * 4];
random.nextBytes(bytes);
entries.add(new SimpleImmutableEntry<>(new Key(Arrays.copyOfRange(bytes, 0, 32), Arrays.copyOfRange(bytes, 32, 64), Arrays.copyOfRange(bytes, 64, 96),
"secret".getBytes(VISIBILITY_CHARSET), (long) 0, false), new Value(Arrays.copyOfRange(bytes, 96, 128))));
}
EntrySigner signer = getSigner("config3.ini", aliceKeyContainers.get(ValueSigner.ECDSA));
List<Entry<Key,Value>> signedEntries = new ArrayList<>(entries.size());
for (Entry<Key,Value> entry : entries) {
signedEntries.add(signer.sign(entry, true));
}
EntrySigner verifier = getSigner("config3.ini", bobKeyContainers.get(ValueSigner.ECDSA));
Iterator<Entry<Key,Value>> entriesIterator = entries.iterator();
SignedExternalScannerIterator signedIterator = new SignedExternalScannerIterator(entries.iterator(), signedEntries.iterator(), verifier, true);
while (entriesIterator.hasNext()) {
assertThat("should return all entries", signedIterator.hasNext(), is(true));
assertThat("should return same entry", entriesIterator.next(), equalTo(signedIterator.next()));
}
assertThat("should not return any more entries", signedIterator.hasNext(), is(false));
// Sign to separate table, with signatures in a different order than the entries.
signedEntries = new ArrayList<>(entries.size());
for (Entry<Key,Value> entry : entries) {
signedEntries.add(signer.sign(entry, true));
}
Collections.shuffle(signedEntries);
entriesIterator = entries.iterator();
signedIterator = new SignedExternalScannerIterator(entries.iterator(), signedEntries.iterator(), verifier, false);
while (entriesIterator.hasNext()) {
assertThat("should return all entries", signedIterator.hasNext(), is(true));
assertThat("should return same entry", entriesIterator.next(), equalTo(signedIterator.next()));
}
assertThat("should not return any more entries", signedIterator.hasNext(), is(false));
}
@Test
public void internalUnprocessedTest() throws Exception {
EntrySigner signer = getSigner("config1.ini", aliceKeyContainers.get(ValueSigner.RSA_PSS));
EntrySigner verifier = getSigner("config1.ini", bobKeyContainers.get(ValueSigner.RSA_PSS));
List<Entry<Key,Value>> entries = new ArrayList<>();
byte[] row = new byte[] {1};
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(row, new byte[] {2}, new byte[] {3}, "secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false,
false), new Value(new byte[] {4}));
entries.add(signer.sign(entry, true));
SignedInlineScannerIterator iterator = new SignedInlineScannerIterator(entries.iterator(), verifier);
iterator.next();
assertThat("unprocessed item is correct", iterator.unprocessed(), Matchers.equalTo(entries.get(0)));
}
@Test
public void internalUnprocessedException() throws Exception {
EntrySigner signer = getSigner("config1.ini", aliceKeyContainers.get(ValueSigner.RSA_PSS));
EntrySigner verifier = getSigner("config1.ini", bobKeyContainers.get(ValueSigner.RSA_PSS));
List<Entry<Key,Value>> entries = new ArrayList<>();
byte[] row = new byte[] {1};
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(row, new byte[] {2}, new byte[] {3}, "secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false,
false), new Value(new byte[] {4}));
entries.add(signer.sign(entry, true));
SignedInlineScannerIterator iterator = new SignedInlineScannerIterator(entries.iterator(), verifier);
try {
iterator.unprocessed();
fail("cannot call unprocessed before calling next()");
} catch (NoSuchElementException e) { /* expected */}
try {
iterator.hasNext();
iterator.unprocessed();
fail("cannot call unprocessed before calling next()");
} catch (NoSuchElementException e) { /* expected */}
}
@Test
public void externalUnprocessedTest() throws Exception {
EntrySigner signer = getSigner("config3.ini", aliceKeyContainers.get(ValueSigner.ECDSA));
EntrySigner verifier = getSigner("config3.ini", bobKeyContainers.get(ValueSigner.ECDSA));
List<Entry<Key,Value>> entries = new ArrayList<>();
List<Entry<Key,Value>> signedEntries = new ArrayList<>();
byte[] row = new byte[] {1};
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(row, new byte[] {2}, new byte[] {3}, "secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false,
false), new Value(new byte[] {4}));
entries.add(entry);
signedEntries.add(signer.sign(entry, true));
SignedExternalScannerIterator iterator = new SignedExternalScannerIterator(entries.iterator(), signedEntries.iterator(), verifier, true);
iterator.next();
assertThat("unprocessed item is correct", iterator.unprocessed(), Matchers.equalTo(entries.get(0)));
}
@Test
public void externalUnprocessedException() throws Exception {
EntrySigner signer = getSigner("config3.ini", aliceKeyContainers.get(ValueSigner.ECDSA));
EntrySigner verifier = getSigner("config3.ini", bobKeyContainers.get(ValueSigner.ECDSA));
List<Entry<Key,Value>> entries = new ArrayList<>();
List<Entry<Key,Value>> signedEntries = new ArrayList<>();
byte[] row = new byte[] {1};
Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(row, new byte[] {2}, new byte[] {3}, "secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false,
false), new Value(new byte[] {4}));
entries.add(entry);
signedEntries.add(signer.sign(entry, true));
SignedExternalScannerIterator iterator = new SignedExternalScannerIterator(entries.iterator(), signedEntries.iterator(), verifier, true);
try {
iterator.unprocessed();
fail("cannot call unprocessed before calling next()");
} catch (NoSuchElementException e) { /* expected */}
try {
iterator.hasNext();
iterator.unprocessed();
fail("cannot call unprocessed before calling next()");
} catch (NoSuchElementException e) { /* expected */}
}
@Test
public void iteratorRemoveFailsTest() {
try {
new SignedInlineScannerIterator(null, null).remove();
fail("removal is not allowed");
} catch (UnsupportedOperationException e) { /* expected */}
try {
new SignedExternalScannerIterator(null, null, null, true).remove();
fail("removal is not allowed");
} catch (UnsupportedOperationException e) { /* expected */}
}
@Test
public void missingSignatureTest() throws Exception {
Random random = new Random();
List<Entry<Key,Value>> entries = new ArrayList<>();
for (int i = 0; i < 100; i++) {
byte[] bytes = new byte[32 * 4];
random.nextBytes(bytes);
entries.add(new SimpleImmutableEntry<>(new Key(Arrays.copyOfRange(bytes, 0, 32), Arrays.copyOfRange(bytes, 32, 64), Arrays.copyOfRange(bytes, 64, 96),
"secret".getBytes(VISIBILITY_CHARSET), (long) 0, false), new Value(Arrays.copyOfRange(bytes, 96, 128))));
}
EntrySigner signer = getSigner("config3.ini", aliceKeyContainers.get(ValueSigner.ECDSA));
List<Entry<Key,Value>> signedEntries = new ArrayList<>(entries.size());
for (Entry<Key,Value> entry : entries) {
signedEntries.add(signer.sign(entry, true));
}
EntrySigner verifier = getSigner("config3.ini", bobKeyContainers.get(ValueSigner.ECDSA));
signedEntries.remove(9);
SignedExternalScannerIterator signedIterator = new SignedExternalScannerIterator(entries.iterator(), signedEntries.iterator(), verifier, true);
try {
while (signedIterator.hasNext()) {
signedIterator.next();
}
fail("missing signature should cause an exception");
} catch (SignatureException e) { /* expected */}
// Missing signature is the first signature.
signedEntries = new ArrayList<>(entries.size());
for (Entry<Key,Value> entry : entries) {
signedEntries.add(signer.sign(entry, true));
}
signedEntries.remove(0);
signedIterator = new SignedExternalScannerIterator(entries.iterator(), signedEntries.iterator(), verifier, true);
try {
while (signedIterator.hasNext()) {
signedIterator.next();
}
fail("missing signature should cause an exception");
} catch (SignatureException e) { /* expected */}
// Missing signature is in the middle of the table.
signedEntries = new ArrayList<>(entries.size());
for (Entry<Key,Value> entry : entries) {
signedEntries.add(signer.sign(entry, true));
}
signedEntries.remove(5);
Collections.shuffle(signedEntries);
signedIterator = new SignedExternalScannerIterator(entries.iterator(), signedEntries.iterator(), verifier, false);
try {
while (signedIterator.hasNext()) {
signedIterator.next();
}
fail("missing signature should cause an exception");
} catch (SignatureException e) { /* expected */}
}
/**
* Get a signer.
*
* @param resource
* Resource file containing the configuration.
* @param keys
* Keys to use for the signer.
* @return EntrySigner.
*/
private EntrySigner getSigner(String resource, SignatureKeyContainer keys) throws Exception {
return new EntrySigner(new SignatureConfigBuilder().readFromFile(new InputStreamReader(getResourceAsStream(this.getClass(), resource))).build(), keys);
}
}
@@ -0,0 +1,662 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.iterableWithSize;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.InputStreamReader;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.apache.accumulo.core.client.Connector;
import org.apache.accumulo.core.client.IteratorSetting;
import org.apache.accumulo.core.client.IteratorSetting.Column;
import org.apache.accumulo.core.client.Scanner;
import org.apache.accumulo.core.client.sample.SamplerConfiguration;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Range;
import org.apache.accumulo.core.data.Value;
import org.apache.accumulo.core.security.Authorizations;
import org.apache.hadoop.io.Text;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import edu.mit.ll.pace.internal.Utils;
import edu.mit.ll.pace.test.Matchers;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test {@link SignedScanner}.
*/
public class SignedScannerTest {
private final static String TEST_TABLE = "test";
private final static String SIG_TABLE = "sigs";
private final static Authorizations authorizations = new Authorizations("admin", "secret", "top secret");
private Map<ValueSigner,SignatureKeyContainer> aliceKeyContainers = new HashMap<>();
@Mock
private Connector mockConnector;
@Mock
private Scanner mockScanner;
@Mock
private Scanner mockSignatureScanner;
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule();
public SignedScannerTest() throws NoSuchAlgorithmException {
aliceKeyContainers = MockSignatureKeyContainer.getContainers("alice", "alice");
}
@BeforeClass
public static void setupBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void badConstructorTest() throws Exception {
try {
new SignedScanner(null, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null connector not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedScanner(mockConnector, null, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null table name not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedScanner(mockConnector, TEST_TABLE, null, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null authorizations not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedScanner(mockConnector, TEST_TABLE, authorizations, null, aliceKeyContainers.get(ValueSigner.RSA_PSS));
fail("null crypto config not allowed");
} catch (IllegalArgumentException e) { /* expected */}
try {
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), null);
fail("null key container not allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void constructorTest() throws Exception {
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
verify(mockConnector).createScanner(TEST_TABLE, authorizations);
verify(mockConnector, never()).createScanner(SIG_TABLE, authorizations);
}
@Test
public void constructorExternalTest() throws Exception {
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
verify(mockConnector).createScanner(TEST_TABLE, authorizations);
verify(mockConnector).createScanner(SIG_TABLE, authorizations);
}
@Test
public void iteratorTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
EntrySigner signer = new EntrySigner(getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
List<Entry<Key,Value>> entries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(signer.sign(entry, true));
entries.add(signer.sign(entry2, true));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
Scanner scanner = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS));
assertThat("has correct number of elements", scanner, iterableWithSize(2));
Iterator<Entry<Key,Value>> iterator = scanner.iterator();
assertThat("correct item", iterator.next(), Matchers.equalTo(entry));
assertThat("correct item", iterator.next(), Matchers.equalTo(entry2));
}
@Test
public void iteratorExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
EntrySigner signer = new EntrySigner(getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA));
List<Entry<Key,Value>> entries = new ArrayList<>();
List<Entry<Key,Value>> signedEntries = new ArrayList<>();
Map.Entry<Key,Value> entry = new SimpleImmutableEntry<>(new Key(new byte[] {1}, new byte[] {2}, new byte[] {3},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {4}));
Map.Entry<Key,Value> entry2 = new SimpleImmutableEntry<>(new Key(new byte[] {5}, new byte[] {6}, new byte[] {7},
"secret".getBytes(Utils.VISIBILITY_CHARSET), 0, false, false), new Value(new byte[] {8}));
entries.add(entry);
entries.add(entry2);
signedEntries.add(signer.sign(entry, true));
signedEntries.add(signer.sign(entry2, true));
when(mockScanner.iterator()).thenReturn(entries.iterator()).thenReturn(entries.iterator());
when(mockSignatureScanner.iterator()).thenReturn(signedEntries.iterator()).thenReturn(signedEntries.iterator());
Scanner scanner = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.ECDSA));
assertThat("has correct number of elements", scanner, iterableWithSize(2));
Iterator<Entry<Key,Value>> iterator = scanner.iterator();
assertThat("correct item", iterator.next(), Matchers.equalTo(entry));
assertThat("correct item", iterator.next(), Matchers.equalTo(entry2));
}
@Test
public void addScanIteratorTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
IteratorSetting test = new IteratorSetting(10, "test", "test2");
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).addScanIterator(test);
verify(mockScanner).addScanIterator(test);
}
@Test
public void addScanIteratorExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
IteratorSetting test = new IteratorSetting(10, "test", "test2");
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).addScanIterator(test);
verify(mockScanner).addScanIterator(test);
verify(mockSignatureScanner).addScanIterator(test);
}
@Test
public void clearColumns() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).clearColumns();
verify(mockScanner).clearColumns();
}
@Test
public void clearExternalColumns() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).clearColumns();
verify(mockScanner).clearColumns();
verify(mockSignatureScanner).clearColumns();
}
@Test
public void clearScanIteratorsTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).clearScanIterators();
verify(mockScanner).clearScanIterators();
}
@Test
public void clearScanIteratorsExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).clearScanIterators();
verify(mockScanner).clearScanIterators();
verify(mockSignatureScanner).clearScanIterators();
}
@Test
public void closeTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).close();
verify(mockScanner).close();
}
@Test
public void closeExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).close();
verify(mockScanner).close();
verify(mockSignatureScanner).close();
}
@Test
public void fetchColumnTest() throws Exception {
Column column = new Column(new Text(new byte[] {1}), new Text(new byte[] {2}));
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).fetchColumn(column);
verify(mockScanner).fetchColumn(column);
}
@Test
public void fetchColumnExternalTest() throws Exception {
Column column = new Column(new Text(new byte[] {1}), new Text(new byte[] {2}));
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).fetchColumn(column);
verify(mockScanner).fetchColumn(column);
verify(mockSignatureScanner).fetchColumn(column);
}
@Test
public void fetchColumn2Test() throws Exception {
Text colF = new Text(new byte[] {1}), colQ = new Text(new byte[] {2});
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).fetchColumn(colF, colQ);
verify(mockScanner).fetchColumn(colF, colQ);
}
@Test
public void fetchColumn2ExternalTest() throws Exception {
Text colF = new Text(new byte[] {1}), colQ = new Text(new byte[] {2});
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).fetchColumn(colF, colQ);
verify(mockScanner).fetchColumn(colF, colQ);
verify(mockSignatureScanner).fetchColumn(colF, colQ);
}
@Test
public void fetchColumnFamilyTest() throws Exception {
Text colF = new Text(new byte[] {1});
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).fetchColumnFamily(colF);
verify(mockScanner).fetchColumnFamily(colF);
}
@Test
public void fetchColumnFamilyExternalTest() throws Exception {
Text colF = new Text(new byte[] {1});
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).fetchColumnFamily(colF);
verify(mockScanner).fetchColumnFamily(colF);
verify(mockSignatureScanner).fetchColumnFamily(colF);
}
@Test
public void getAuthorizationsTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockScanner.getAuthorizations()).thenReturn(authorizations);
Authorizations auths = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getAuthorizations();
verify(mockScanner).getAuthorizations();
assertThat("correct authorizations returned", auths, equalTo(authorizations));
}
@Test
public void getSamplerConfigurationTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
SamplerConfiguration config = new SamplerConfiguration("test");
when(mockScanner.getSamplerConfiguration()).thenReturn(config);
SamplerConfiguration value = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"),
aliceKeyContainers.get(ValueSigner.RSA_PSS)).getSamplerConfiguration();
verify(mockScanner).getSamplerConfiguration();
assertThat("correct config returned", value, equalTo(config));
}
@Test
public void setSamplerConfigurationTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
SamplerConfiguration config = new SamplerConfiguration("test");
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setSamplerConfiguration(config);
verify(mockScanner).setSamplerConfiguration(config);
}
@Test
public void setSamplerConfigurationExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
SamplerConfiguration config = new SamplerConfiguration("test");
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setSamplerConfiguration(config);
verify(mockScanner).setSamplerConfiguration(config);
verify(mockSignatureScanner).setSamplerConfiguration(config);
}
@Test
public void clearSamplerConfigurationTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearSamplerConfiguration();
verify(mockScanner).clearSamplerConfiguration();
}
@Test
public void clearSamplerConfigurationExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearSamplerConfiguration();
verify(mockScanner).clearSamplerConfiguration();
verify(mockSignatureScanner).clearSamplerConfiguration();
}
@Test
public void setBatchTimeoutTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setBatchTimeout(5L,
TimeUnit.DAYS);
verify(mockScanner).setBatchTimeout(5L, TimeUnit.DAYS);
}
@Test
public void setBatchTimeoutExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setBatchTimeout(5L,
TimeUnit.DAYS);
verify(mockScanner).setBatchTimeout(5L, TimeUnit.DAYS);
verify(mockSignatureScanner).setBatchTimeout(5L, TimeUnit.DAYS);
}
@Test
public void getBatchTimeoutTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockScanner.getBatchTimeout(TimeUnit.DAYS)).thenReturn(5L);
long value = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getBatchTimeout(TimeUnit.DAYS);
verify(mockScanner).getBatchTimeout(TimeUnit.DAYS);
assertThat("correct timeout returned", value, is(5L));
}
@Test
public void setClassLoaderContextTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setClassLoaderContext("test");
verify(mockScanner).setClassLoaderContext("test");
}
@Test
public void setClassLoaderContextExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setClassLoaderContext("test");
verify(mockScanner).setClassLoaderContext("test");
verify(mockSignatureScanner).setClassLoaderContext("test");
}
@Test
public void clearClassLoaderContextTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearClassLoaderContext();
verify(mockScanner).clearClassLoaderContext();
}
@Test
public void clearClassLoaderContextExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.clearClassLoaderContext();
verify(mockScanner).clearClassLoaderContext();
verify(mockSignatureScanner).clearClassLoaderContext();
}
@Test
public void getClassLoaderContextTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockScanner.getClassLoaderContext()).thenReturn("test");
String value = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getClassLoaderContext();
verify(mockScanner).getClassLoaderContext();
assertThat("correct class loader context returned", value, is("test"));
}
@Test
public void getTimeoutTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockScanner.getTimeout(TimeUnit.DAYS)).thenReturn(5L);
Long value = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getTimeout(TimeUnit.DAYS);
verify(mockScanner).getTimeout(TimeUnit.DAYS);
assertThat("correct timeout returned", value, is(5L));
}
@Test
public void removeScanIteratorTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.removeScanIterator("test");
verify(mockScanner).removeScanIterator("test");
}
@Test
public void removeScanIteratorExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.removeScanIterator("test");
verify(mockScanner).removeScanIterator("test");
verify(mockSignatureScanner).removeScanIterator("test");
}
@Test
public void updateScanIteratorOptionTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.updateScanIteratorOption("test", "a", "b");
verify(mockScanner).updateScanIteratorOption("test", "a", "b");
}
@Test
public void updateScanIteratorOptionExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.updateScanIteratorOption("test", "a", "b");
verify(mockScanner).updateScanIteratorOption("test", "a", "b");
verify(mockSignatureScanner).updateScanIteratorOption("test", "a", "b");
}
@Test
public void setTimeoutTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setTimeout(5L,
TimeUnit.DAYS);
verify(mockScanner).setTimeout(5L, TimeUnit.DAYS);
}
@Test
public void setTimeoutExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setTimeout(5L,
TimeUnit.DAYS);
verify(mockScanner).setTimeout(5L, TimeUnit.DAYS);
verify(mockSignatureScanner).setTimeout(5L, TimeUnit.DAYS);
}
@Test
public void disableIsolationTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).disableIsolation();
verify(mockScanner).disableIsolation();
}
@Test
public void disableIsolationExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).disableIsolation();
verify(mockScanner).disableIsolation();
verify(mockSignatureScanner).disableIsolation();
}
@Test
public void enableIsolationTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).enableIsolation();
verify(mockScanner).enableIsolation();
}
@Test
public void enableIsolationExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).enableIsolation();
verify(mockScanner).enableIsolation();
verify(mockSignatureScanner).enableIsolation();
}
@Test
public void setBatchSizeTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setBatchSize(5);
verify(mockScanner).setBatchSize(5);
}
@Test
public void setBatchSizeExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setBatchSize(5);
verify(mockScanner).setBatchSize(5);
verify(mockSignatureScanner).setBatchSize(5);
}
@Test
public void getBatchSizeTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockScanner.getBatchSize()).thenReturn(5);
int value = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getBatchSize();
verify(mockScanner).getBatchSize();
assertThat("correct batch size returned", value, is(5));
}
@Test
public void setRangeTest() throws Exception {
Range range = new Range("test");
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setRange(range);
verify(mockScanner).setRange(range);
}
@Test
public void setRangeExternalTest() throws Exception {
Range range = new Range("test");
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setRange(range);
verify(mockScanner).setRange(range);
verify(mockSignatureScanner).setRange(range);
}
@Test
public void getRangeTest() throws Exception {
Range range = new Range("test");
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockScanner.getRange()).thenReturn(range);
Range value = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getRange();
verify(mockScanner).getRange();
assertThat("correct range returned", value, equalTo(range));
}
@Test
public void setReadaheadThresholdTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setReadaheadThreshold(5L);
verify(mockScanner).setReadaheadThreshold(5L);
}
@Test
public void setReadaheadThresholdExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.setReadaheadThreshold(5L);
verify(mockScanner).setReadaheadThreshold(5L);
verify(mockSignatureScanner).setReadaheadThreshold(5L);
}
@Test
public void getReadaheadThresholdTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockScanner.getReadaheadThreshold()).thenReturn(5L);
long value = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getReadaheadThreshold();
verify(mockScanner).getReadaheadThreshold();
assertThat("correct threshold returned", value, is(5L));
}
@Test
@SuppressWarnings("deprecation")
public void setTimeOutTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setTimeOut(5);
verify(mockScanner).setTimeOut(5);
}
@Test
@SuppressWarnings("deprecation")
public void setTimeOutExternalTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockConnector.createScanner(SIG_TABLE, authorizations)).thenReturn(mockSignatureScanner);
new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config3.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS)).setTimeOut(5);
verify(mockScanner).setTimeOut(5);
verify(mockSignatureScanner).setTimeOut(5);
}
@Test
@SuppressWarnings("deprecation")
public void getTimeOutTest() throws Exception {
when(mockConnector.createScanner(TEST_TABLE, authorizations)).thenReturn(mockScanner);
when(mockScanner.getTimeOut()).thenReturn(5);
int value = new SignedScanner(mockConnector, TEST_TABLE, authorizations, getConfig("config1.ini"), aliceKeyContainers.get(ValueSigner.RSA_PSS))
.getTimeOut();
verify(mockScanner).getTimeOut();
assertThat("correct timeout returned", value, is(5));
}
/**
* Get an signor config.
*
* @param resource
* Resource file containing the configuration.
* @return EncryptionConfig.
*/
private SignatureConfig getConfig(String resource) throws Exception {
return new SignatureConfigBuilder().readFromFile(new InputStreamReader(TestUtils.getResourceAsStream(this.getClass(), resource))).build();
}
}
@@ -0,0 +1,143 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.signature;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.emptyArray;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import java.security.Signature;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.BeforeClass;
import org.junit.Test;
import edu.mit.ll.pace.test.TestUtils;
/**
* Test for {@link ValueSigner}.
*/
public class ValueSignerTest {
@BeforeClass
public static void setupBouncyCastle() {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@Test
public void validEnumTest() {
assertThat("should have four values", ValueSigner.values(), is(arrayWithSize(4)));
}
@Test
public void toStringTest() {
assertThat("toString should return correct value", ValueSigner.RSA_PKCS1.toString(), is("RSA-PKCS1"));
assertThat("toString should return correct value", ValueSigner.RSA_PSS.toString(), is("RSA-PSS"));
assertThat("toString should return correct value", ValueSigner.DSA.toString(), is("DSA"));
assertThat("toString should return correct value", ValueSigner.ECDSA.toString(), is("ECDSA"));
}
@Test
public void fromStringTest() {
assertThat("fromString should return correct enum value", ValueSigner.fromString("RSA-PKCS1"), is(ValueSigner.RSA_PKCS1));
assertThat("fromString should return correct enum value", ValueSigner.fromString("RSA-PSS"), is(ValueSigner.RSA_PSS));
assertThat("fromString should return correct enum value", ValueSigner.fromString("DSA"), is(ValueSigner.DSA));
assertThat("fromString should return correct enum value", ValueSigner.fromString("ECDSA"), is(ValueSigner.ECDSA));
}
@Test
public void fromStringExceptionTest() {
try {
ValueSigner.fromString("bad");
fail("only valid names should be allowed");
} catch (IllegalArgumentException e) { /* expected */}
}
@Test
public void getKeyGenerationAlgorithmTest() throws Exception {
assertThat("should return correct algorithm", ValueSigner.RSA_PKCS1.getKeyGenerationAlgorithm(), is("RSA"));
assertThat("should return correct algorithm", ValueSigner.RSA_PSS.getKeyGenerationAlgorithm(), is("RSA"));
assertThat("should return correct algorithm", ValueSigner.DSA.getKeyGenerationAlgorithm(), is("DSA"));
assertThat("should return correct algorithm", ValueSigner.ECDSA.getKeyGenerationAlgorithm(), is("ECDSA"));
}
@Test
public void getInstanceTest() throws Exception {
assertThat("should return a Signature instance", ValueSigner.RSA_PKCS1.getInstance(null), is(instanceOf(Signature.class)));
assertThat("should return a Signature instance", ValueSigner.RSA_PSS.getInstance("BC"), is(instanceOf(Signature.class)));
assertThat("should return a Signature instance", ValueSigner.DSA.getInstance("BC"), is(instanceOf(Signature.class)));
assertThat("should return a Signature instance", ValueSigner.ECDSA.getInstance(null), is(instanceOf(Signature.class)));
}
@Test
public void signatureTest() throws Exception {
for (ValueSigner signer : ValueSigner.values()) {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(signer.getKeyGenerationAlgorithm());
switch (signer) {
case RSA_PKCS1:
case RSA_PSS:
keyGen.initialize(1024);
break;
case DSA:
keyGen.initialize(1024);
break;
case ECDSA:
keyGen.initialize(256);
break;
}
signatureTest(signer, keyGen);
}
}
private void signatureTest(ValueSigner signer, KeyPairGenerator generator) throws Exception {
byte[] data = "HELLO".getBytes();
Signature signature = signer.getInstance(null);
KeyPair pair = generator.generateKeyPair();
signature.initSign(pair.getPrivate());
signature.update(data);
byte[] digest = signature.sign();
assertThat("digest has a value", TestUtils.wrap(digest), is(not(emptyArray())));
signature.initVerify(pair.getPublic());
signature.update(data);
assertThat("verification succeeds", signature.verify(digest), is(true));
switch (signer) {
case RSA_PKCS1:
break;
default:
signature.initSign(pair.getPrivate());
signature.update(data);
assertThat("signatures correctly use random padding", signature.sign(), not(equalTo(digest)));
}
}
}
@@ -0,0 +1,141 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.test;
import java.util.Map.Entry;
import org.apache.accumulo.core.data.Key;
import org.apache.accumulo.core.data.Value;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.TypeSafeMatcher;
import org.ini4j.Ini;
import org.ini4j.Profile.Section;
/**
* Hamcrest matchers for use in testing.
*/
public class Matchers {
/**
* Gets a hamcrest matcher that tests whether two objects have the same fields.
* <p>
* This check uses reflection.
*
* @param expectedValue
* The expected value to core against.
* @return Method safe matcher that will core field equality.
*/
@Factory
public static <T> TypeSafeMatcher<T> hasSameFieldsAs(final T expectedValue) {
return new TypeSafeMatcher<T>() {
@Override
protected boolean matchesSafely(T actualValue) {
return EqualsBuilder.reflectionEquals(expectedValue, actualValue, false);
}
@Override
protected void describeMismatchSafely(T actualValue, Description description) {
description.appendText("was {").appendText(ReflectionToStringBuilder.toString(actualValue, ToStringStyle.SIMPLE_STYLE)).appendText("}");
}
@Override
public void describeTo(Description description) {
description.appendText(" {").appendText(ReflectionToStringBuilder.toString(expectedValue, ToStringStyle.SIMPLE_STYLE)).appendText("}");
}
};
}
/**
* Gets a hamcrest matcher that tests whether two Ini objects are equal.
*
* @param expectedValue
* The expected value to core against.
* @return Method safe matcher that will core field equality.
*/
@Factory
public static TypeSafeMatcher<Ini> equalTo(final Ini expectedValue) {
return new TypeSafeMatcher<Ini>() {
@Override
protected boolean matchesSafely(Ini actualValue) {
if (expectedValue.size() != actualValue.size()) {
return false;
}
for (Section expectedSection : expectedValue.values()) {
Section actualSection = actualValue.get(expectedSection.getName());
if (actualSection == null) {
return false;
}
if (expectedSection.size() != actualSection.size()) {
return false;
}
for (Entry<String,String> expectedEntry : expectedSection.entrySet()) {
if (!actualSection.containsKey(expectedEntry.getKey())) {
return false;
}
String actual = actualSection.get(expectedEntry.getKey());
String expected = expectedEntry.getValue();
if ((actual == null && expected != null) || (actual != null && !actual.equals(expected))) {
return false;
}
}
}
return true;
}
@Override
public void describeTo(Description description) {
description.appendValue(expectedValue);
}
};
}
/**
* Gets a hamcrest matcher that tests whether two entires are the same.
*
* @param expectedValue
* The expected value to core against.
* @return Method safe matcher that will core equality.
*/
@Factory
public static TypeSafeMatcher<Entry<Key,Value>> equalTo(final Entry<Key,Value> expectedValue) {
return new TypeSafeMatcher<Entry<Key,Value>>() {
@Override
protected boolean matchesSafely(Entry<Key,Value> actualValue) {
return expectedValue.getKey().equals(actualValue.getKey()) && expectedValue.getValue().equals(actualValue.getValue());
}
@Override
protected void describeMismatchSafely(Entry<Key,Value> actualValue, Description description) {
description.appendText("was {key=").appendValue(actualValue.getKey()).appendText(", value=").appendValue(actualValue.getValue()).appendText("}");
}
@Override
public void describeTo(Description description) {
description.appendText(" {key=").appendValue(expectedValue.getKey()).appendText(", value=").appendValue(expectedValue.getValue()).appendText("}");
}
};
}
}
@@ -0,0 +1,64 @@
/*
* Copyright 2016 MIT Lincoln Laboratory
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edu.mit.ll.pace.test;
import java.io.IOException;
import java.io.InputStream;
/**
* Functions utilized across the tests.
*/
public final class TestUtils {
/**
* Get a resource for the given class.
*
* @param clazz
* Class to get the resource for.
* @param resourceName
* THe name of the resource.
* @return An stream to the resource.
* @throws IOException
* Thrown if the resource name does not exist.
*/
public static InputStream getResourceAsStream(Class<?> clazz, String resourceName) throws IOException {
if (resourceName.startsWith("/")) {
return clazz.getResourceAsStream(resourceName);
}
InputStream resource = clazz.getResourceAsStream(clazz.getSimpleName() + "/" + resourceName);
if (resource == null) {
resource = clazz.getResourceAsStream(resourceName);
}
return resource;
}
/**
* Wrap a byte[] in a Byte[] so that it can be used with {@link org.hamcrest.Matchers#arrayContaining(Object[])} )}.
*
* @param data
* Data to be wrapped.
* @return Wrapped array.
*/
public static Byte[] wrap(byte[] data) {
Byte[] newData = new Byte[data.length];
for (int i = 0; i < data.length; i++) {
newData[i] = data[i];
}
return newData;
}
}
@@ -0,0 +1,22 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_GCM
[colFamily]
cipher = AES_GCM
[colQualifier]
cipher = AES_GCM
@@ -0,0 +1,30 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_row
sources = row
[colFamily]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colFamily
sources = colFamily
[colQualifier]
cipher = AES_GCM
[value]
cipher = AES_GCM
useVisibility = true
@@ -0,0 +1,22 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_row
sources = row
[value]
cipher = AES_GCM
useVisibility = true
@@ -0,0 +1,18 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = deterministic
sources = row,colFamily
@@ -0,0 +1,16 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[value]
cipher = AES_GCM
@@ -0,0 +1,35 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colFamily]
cipher = AES_CBC
provider = BC
useVisibility = false
keyId = keyId
keyLength = 16
sources = row,colFamily,colQualifier
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
useVisibility = false
keyId = keyId2
keyLength = 32
sources = colFamily,colQualifier
[value]
cipher = AES_GCM
useVisibility = true
keyId = AES_GCM
keyLength = 24
sources = value
@@ -0,0 +1,14 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
@@ -0,0 +1,18 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = deterministic
sources = row,colFamily
@@ -0,0 +1,23 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colFamily]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colFamily
sources = colFamily
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
sources = colQualifier
@@ -0,0 +1,18 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colFamily]
cipher = AES_GCM
keyId = searchable_colFamily
sources = colFamily
@@ -0,0 +1,23 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colFamily]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colFamily
sources = colFamily,colQualifier
[colQualifier]
cipher = AES_GCM
keyId = AES_GCM
sources = colQualifier
@@ -0,0 +1,23 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_GCM
keyId = AES_GCM
sources = row,colFamily
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
sources = colFamily,colQualifier
@@ -0,0 +1,23 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_GCM
keyId = AES_GCM
sources = row,colFamily
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
sources = colQualifier
@@ -0,0 +1,23 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colFamily]
cipher = AES_GCM
keyId = AES_GCM
sources = colFamily
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
sources = colQualifier
@@ -0,0 +1,18 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
sources = colQualifier
@@ -0,0 +1,29 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_row
sources = row
[colFamily]
cipher = AES_GCM
useVisibility = true
keyId = AES_GCM
sources = colFamily
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
sources = colQualifier
@@ -0,0 +1,18 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colFamily]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colFamily
sources = colFamily,colQualifier
@@ -0,0 +1,23 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_row
sources = row,colQualifier
[colFamily]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colFamily
sources = colFamily
@@ -0,0 +1,18 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
sources = colFamily,colQualifier
@@ -0,0 +1,23 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_row
sources = row,colFamily
[colQualifier]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_colQualifier
sources = colQualifier
@@ -0,0 +1,18 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[row]
cipher = AES_SIV_DETERMINISTIC
keyId = searchable_row
sources = row,colFamily,colQualifier
@@ -0,0 +1,21 @@
; Copyright 2016 MIT Lincoln Laboratory
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; http://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
[colFamily]
cipher = AES_CBC
provider = SunJCE
useVisibility = false
keyId = keyId
keyLength = 24
sources = row,colFamily,colQualifier
Loaded 100 of 266 files, more files were not shown because too many files have changed in this diff. Show more