Copybara import of the project: - 42ec5f97f6af3f8c91822fcc0da1f19bc9b5b9b1 Initial commit. by Marvin S. Addison <marvin.addison@gmail.com> - 9504c0d6bdd17b8834f66e37547c5bd5f870a7e4 Test coverage for CipherUtil class. by Marvin S. Addison <marvin.addison@gmail.com> - 716bbb13d9b158b4ee66809e4b374569032ebb8d Add javadocs. by Marvin S. Addison <marvin.addison@gmail.com> - 6deadce5cc7f2a9aff748d2d8347cc70d7553f0d Test coverage for HashUtil. by Marvin S. Addison <marvin.addison@gmail.com> - 1afd88805535fb50ff722bfea618a17cea30ff8e Add support for symmetric encryption of streams. by Marvin S. Addison <marvin.addison@gmail.com> - 05b880fbb0b443fde86678777d341d5aa6099596 Add support for hashing streams. by Marvin S. Addison <marvin.addison@gmail.com> - 99d9a61c747f871b6c40cdf077652f18489c66f7 Create encoding components that support chunked IO. by Marvin S. Addison <marvin.addison@gmail.com> - 9c897d95863b2f33855f29b9793314e7052d8e2a Reorganize utility components in util package. by Marvin S. Addison <marvin.addison@gmail.com> - 04f8093f2aa29105bdd58fcfacc6cdd673be1229 Port vt-crypt key pair generation and utils. by Marvin S. Addison <marvin.addison@gmail.com> - 814deb93446382133fc8a191f733ba3d77c451b1 Add support for colon-delimited hex strings. by Marvin S. Addison <marvin.addison@gmail.com> - 60bc9af5b4ab0a64213feb80e43dc1b8504fc586 Add X.509 utilities. by Marvin S. Addison <marvin.addison@gmail.com> - c2c93e69e32770ad151486cb1c186d553b57b981 Fix base64 encoding bugs. by Marvin S. Addison <marvin.addison@gmail.com> - 9e78590891bb436e9f5bd88e3d4a397953e70c8c Preliminary port of private key reading/decoding. by Marvin S. Addison <marvin.addison@gmail.com> - e18447085bb287965abd3bbdfa12eb01352d58a8 Fix invalid data passed to overridden constructor. by Marvin S. Addison <marvin.addison@gmail.com> - a1f8aecb963f6e3b70e6a5aea406bc3937d32f77 Add CodecSpec for declarative creation of encoders/decode... by Marvin S. Addison <marvin.addison@gmail.com> - 1d4d51114c0a705127d69311684ed2f8d97d381c Complete private key read/asn.1 decode support. by Marvin S. Addison <marvin.addison@gmail.com> - 2aefc3d59cc4207471c0d3374b744402eb80ae2c Refactor priv key decoding, add pub key reading. by Marvin S. Addison <marvin.addison@gmail.com> - b23ac7f50bb9bde959fe79139d3847ea6f47a3ff Disable base64 encoding line breaks by default. by Marvin S. Addison <marvin.addison@gmail.com> - 884a80deb3cd9c669f5bedd7985e47f9af48eba2 Platform-specific line breaks in base64 encoding. by Marvin S. Addison <marvin.addison@gmail.com> - 9c09ff03a11bd4d16ae0f5f92e3632a8052ca345 Add nonce generator classes. by Marvin S. Addison <marvin.addison@gmail.com> - 6e59f80d8b9acd4038ea729afd442d8abace1231 Add convenience constructors to CounterNonce. by Marvin S. Addison <marvin.addison@gmail.com> - ca677a69f3ac7a0b04bab7b3a3bc4303b1812c36 Add field getters to DigestSpec. by Marvin S. Addison <marvin.addison@gmail.com> - 8a2f8f08d197d76fafe495178fe28a34a71ec062 Convert CodecSpec to class for consistency. by Marvin S. Addison <marvin.addison@gmail.com> - 26c57aaeef7cd036290e405a16a056d60dc84001 Add Nonce parameter to CipherUtil#encrypt() methods. by Marvin S. Addison <marvin.addison@gmail.com> - deb97ef4b9de39f476d46757d0422ad2ffad80f5 Use RBGNonce in implementation of NonceUtil#nist80038d(). by Marvin S. Addison <marvin.addison@gmail.com> - 9d3def585fe35cd4284f9b3ac7456513e2c56dc4 Complete test coverage for CertUtil. by Marvin S. Addison <marvin.addison@gmail.com> - 92ef86eebe66e13b16a336187b80dc87b53afa47 Add encrypted nonce generation strategy. by Marvin S. Addison <marvin.addison@gmail.com> - 8ab1893ce74e444e35db76b05582b35a1853133f Refactor crypotographic specification components. by Marvin S. Addison <marvin.addison@gmail.com> - 39b3f3488ede456c7674d92e4c29e6e399f5ef77 Use Spec interface where possible. by Marvin S. Addison <marvin.addison@gmail.com> - d3cc9cbf7b0d4d2fec57d201900ae2c3378cce2f Add facility to specify block cipher as algorith/mode/pad... by Marvin S. Addison <marvin.addison@gmail.com> - d70b5e4255fdf4fd56734a62b82cc8e097ab316f Move key generators to generator package. by Marvin S. Addison <marvin.addison@gmail.com> - c89e2fb88a0339b06295c120dbd0cfd3f33d016c Start work on bean components. by Marvin S. Addison <marvin.addison@gmail.com> - e8ddbd0c4f701db6a5163354550ddfda1b4de762 Add secret key factory beans. by Marvin S. Addison <marvin.addison@gmail.com> - 455d6cd7d2122de0b52537f3391d64167a670894 Update HashUtil to apply _salt_ before hashing data. by Marvin S. Addison <marvin.addison@gmail.com> - 4d771b244c721be431c48077beaf0d48601168fe Add hash bean components. by Marvin S. Addison <marvin.addison@gmail.com> - c82c38dd2a2586e091b1ee53e5e80a8e07d8617e Use Spec interface for greater extensibility. by Marvin S. Addison <marvin.addison@gmail.com> - 3174291dc84afff81ec2b9c8b655f6ccf600499b Fix case sensitivity of KeyStoreFactoryBean source file. by Marvin S. Addison <marvin.addison@gmail.com> - 3be2316a14fe2dd0bb7568336a30035f35800c2e Implement cipher beans. by Marvin S. Addison <marvin.addison@gmail.com> - 5baf0e79b462d5fa588f1410053d82fba323a341 Refactor packaging of adapter classes. by Marvin S. Addison <marvin.addison@gmail.com> - 272a3dd24f406a3626a4d28bc68d34f421e0f1ae Remove PKCS#12 PBE encryption scheme -- no use case. by Marvin S. Addison <marvin.addison@gmail.com> - cf6ce5bd350817a8687dc8dd2d7910b95d4295cc Replace inner classes with new BlockCipherAdapter compone... by Marvin S. Addison <marvin.addison@gmail.com> - 696e9d57d9fb6420bf8d090faf9563e831404961 Refactor nonces to support more use cases. by Marvin S. Addison <marvin.addison@gmail.com> - 2dc499d5f7039c7966236eaaaf48857479eac4db Implement checkstyle rules in build and format accordingly. by Marvin S. Addison <marvin.addison@gmail.com> - 78df5f1dea7460f079a9c34b8c32c690343e52de Javadoc fix. by Marvin S. Addison <marvin.addison@gmail.com> - 3f4cfbce99fb9120f57d235308e7fb6d7e3ad62b Add hash comparison methods to HashUtil. by Marvin S. Addison <marvin.addison@gmail.com> - 91c722d07050eb075ece3728d51302f8254ad655 Add HashBean#compare() for hash comparisons. by Marvin S. Addison <marvin.addison@gmail.com> - a8d18859b20fb54502c516a7cf6e96760d208303 Add getters for bean fields. by Marvin S. Addison <marvin.addison@gmail.com> - 6f27ee5078c9377fb0e38e21c7a7f0bbdec46f8f Rename project to cryptacular. by Marvin S. Addison <marvin.addison@gmail.com> - 58f8a0d56a879d46095bf04a9fcf394f1afb6e32 Update README for cryptacular. by Marvin S. Addison <marvin.addison@gmail.com> - 917cd45b05427ee8cb564745b50e8b2bf9b9927a Refactor salted hashing strategy. by Marvin S. Addison <marvin.addison@gmail.com> - 0e581bd99939ee30dd0679efc4d2cc3f57df7bbe Merge branch 'master' of github.com:vt-middleware/cryptac... by Marvin S. Addison <marvin.addison@gmail.com> - a38881d1ea08fe8df00acf44c236a37afa35d0d8 Restore compare method on HashBean. by Marvin S. Addison <marvin.addison@gmail.com> - 284ce4cbabb755acbe7980d4962d026253ec507f Add all-in-one constructors to beans. by Marvin S. Addison <marvin.addison@gmail.com> - 3bbec1a0bbc67433c5c9cbc226a5279e1e36997a More methods for reading certs. by Marvin S. Addison <marvin.addison@gmail.com> - c8f9799c5dd4caeca410220e4adc229584af7996 Add missing test certificate chain files. by Marvin S. Addison <marvin.addison@gmail.com> - af2e6b108c99cf4a13fdf79f5760750f83d1bc24 Add IdGenerator components for random string ID generation. by Marvin S. Addison <marvin.addison@gmail.com> - 2bf17a2cce6116ae9c4385f702e5acc04d320e63 Conformity changes. by Daniel Fisher <dfisher@vt.edu> - 862be57c4cb9e45905713ff8ee427ae55ab3e43c Remove some usages of this. by Daniel Fisher <dfisher@vt.edu> - b44b6f458d8b6b99cc8fc9e2ce62fe387af47214 Intellij source cleanup. by Daniel Fisher <dfisher@vt.edu> - 35a5e2562045d58406374b0e31d7ebacde1d4e2b Jalopy source code cleanup. by Daniel Fisher <dfisher@vt.edu> - de6b5a56b145765d7eb9d08eea8457d669c7f2ed Update BC version to 1.50. by Daniel Fisher <dfisher@vt.edu> - dd503a1f0e898ec2c0871ce87ff56dd624d0d0f6 Must use a header module or header is ignored. by Daniel Fisher <dfisher@vt.edu> - 0768c5d88be374811ef21bf83d0c6083189563cf Add public/private key factory beans. by Marvin S. Addison <marvin.addison@gmail.com> - 352588aff48c0358bd9fcbe95cd506c7377067ad Add maven release plugin config. by Marvin S. Addison <marvin.addison@gmail.com> - 39c5be22a5b9f094531c590fd16bf1736ea58e85 [maven-release-plugin] prepare release v1.0-RC1 by Marvin S. Addison <marvin.addison@gmail.com> - 216d22be9eb7ac65013d99ef7f1fdadc6bc6daa2 [maven-release-plugin] prepare for next development itera... by Marvin S. Addison <marvin.addison@gmail.com> - 06fde2b5350439e03dc048e3acee911a4c770b50 Merge branch 'otp' by Marvin S. Addison <marvin.addison@gmail.com> - 10045a7f8712d3c9f6a446b667cae7ee18ff1ae8 [maven-release-plugin] prepare release v1.0-RC2 by Marvin S. Addison <marvin.addison@gmail.com> - 8714f65702b915726ffee4cc83d7fcd8731c4c3a [maven-release-plugin] prepare for next development itera... by Marvin S. Addison <marvin.addison@gmail.com> - 4579f12e1ad3503eb588478e7effa31316a2ada7 Expose getter for numberOfDigits. by Marvin S. Addison <marvin.addison@gmail.com> - 22cc3c13d29b4e3ce9435d0c2d74ce459dc5321b Add base 32 encoding/decoding support. by Marvin S. Addison <marvin.addison@gmail.com> - 50560a286719bc3420d891ab2443c8f454e82666 [maven-release-plugin] prepare release v1.0-RC3 by Marvin S. Addison <marvin.addison@gmail.com> - c0f65941dbd068f81ae2c6f42328fa55cfb59b1d [maven-release-plugin] prepare for next development itera... by Marvin S. Addison <marvin.addison@gmail.com> - d2a0a9957b166a3e95cb8cb44f493987a667c0a9 Add a null check when reading subject alt names. by Daniel Fisher <dfisher@gmail.com> - 6c15305e06756c2299a37e8fbce14a7f9ee0f849 Merge pull request #2 from serac/issue-1 by Daniel Fisher <dfisher@gmail.com> - 296a6926aa715e80573fcdce5c6438fdb4229d3e Merge pull request #4 from serac/issue-3 by Daniel Fisher <dfisher@gmail.com> - a3fa4b9a64c419c986b778367c4f99d3d6c1363e Fix version -- never released RC4. by Marvin S. Addison <marvin.addison@gmail.com> - 4983df707bc7b6d0f4afdd75e89d6dce5f286417 [maven-release-plugin] prepare release v1.0-RC4 by Marvin S. Addison <marvin.addison@gmail.com> - 8fdf26670c457b0b2a44e4e7b92123dd4af93a7f [maven-release-plugin] prepare for next development itera... by Marvin S. Addison <marvin.addison@gmail.com> - bc73d5491da4e4346dc0be940a5210aa88e30c84 Update plugin and dependency versions. by Daniel Fisher <dfisher@vt.edu> - 1b739f9b30992c8b5dadf2828a1692ba2d57175b Don't attach the assembly to the project. by Daniel Fisher <dfisher@vt.edu> - faca124004da3994b0af9ebfabc139753f5387b7 Add multi-value RDN test vectors. by Marvin S. Addison <marvin.addison@gmail.com> - 59824a405a58b9c31a2facbbe28a64071fbb8b79 Add LdapNameFormatterTest multi-value RDN test cases. by Marvin S. Addison <marvin.addison@gmail.com> - be723161bb8ce44706348d8ead20dedd77fff2b2 Merge pull request #9 from vt-middleware/multi-value-rdn by Daniel Fisher <dfisher@gmail.com> - 2abfc83720749ea5ebc6c8f42f716a32a24de2e7 Merge pull request #8 from vt-middleware/generalize-ksbskfb by Daniel Fisher <dfisher@gmail.com> - 3f290eacf0ed0b2b291fd51b984ff3d4426e5498 Merge pull request #11 from vt-middleware/rfc2253-escape by Daniel Fisher <dfisher@gmail.com> - 85ba61657a6de75e6eca3c56496e77e1ddde5d7f Fix issue URL. by Daniel Fisher <dfisher@vt.edu> - 8ad916cbdfae9ff87c3df38b1ae2cb90e186a81a Intellij cleanup. by Daniel Fisher <dfisher@vt.edu> - 5795da7b9daad0086e72f1db8276b15e73bf4d32 Merge pull request #13 from vt-middleware/non-standard-dn... by Daniel Fisher <dfisher@gmail.com> - caeef021fb83f53a844bd1f439c02a313976dd60 Merge pull request #14 from vt-middleware/jalopy-cleanup by Marvin S. Addison <marvin.addison@gmail.com> - 1563671d0f7da76ad6927057fc2d2bfe64d3d96d Update version for release. by Daniel Fisher <dfisher@vt.edu> - 6fb9b7c6c4602e6ed7e87b745ed94d07c11c4f57 Set master back to snapshot. by Daniel Fisher <dfisher@vt.edu> - 5503c5b34c048916de0e7e938f1b2a895862d138 Tagging 1.0 release. by Daniel Fisher <dfisher@vt.edu> - c0b84c9aa8e2035001af25f540794600ea105a0d Bump version for next snapshot. by Daniel Fisher <dfisher@vt.edu> - 2eeafd1b65e8b0574985b4567ce04546ca610e80 Update plugin and dependency versions. by Daniel Fisher <dfisher@vt.edu> - 604272b06700664cf80a278200a9f29f8ce8a1a3 Remove inheritDoc for @Override methods. by Daniel Fisher <dfisher@vt.edu> - 215259028f85816845e2919cc87942ef834cba0c Update copyright year. by Daniel Fisher <dfisher@vt.edu> - b112a9b302a543cd3b5ab63ad1655e204747fdee Issue #18 Fix thread safety bug. by Marvin S. Addison <marvin.addison@gmail.com> - f61d0d1539f7374e6298700ef47a75ca79e72414 Merge pull request #21 from vt-middleware/salted-encoding... by Marvin S. Addison <marvin.addison@gmail.com> - 37fbcef6c7b6e8177caf69f8391c04a6374fec37 Update dependency and plugin versions. by Daniel Fisher <dfisher@vt.edu> (And 106 more changes) GitOrigin-RevId: a070bf4723761dac9c72c0f0333336b829fef525 Change-Id: I69f14464e5a5999125a71501793f1ed95ac644c0
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9d08a1a --- /dev/null +++ b/.editorconfig
@@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8905eb7 --- /dev/null +++ b/.gitattributes
@@ -0,0 +1 @@ +src/test/resources/plaintexts/*.txt text eol=lf
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6240411 --- /dev/null +++ b/.gitignore
@@ -0,0 +1,3 @@ +*.iml +.idea +target
diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7213ae0 --- /dev/null +++ b/LICENSE
@@ -0,0 +1,2 @@ +This product is dual licensed under a choice of either the Apache-2.0 or the LGPL-3.0 license. +See LICENSE-apache2 and LICENSE-lgpl for the full text of the licenses.
diff --git a/LICENSE-apache2 b/LICENSE-apache2 new file mode 100644 index 0000000..6b0b127 --- /dev/null +++ b/LICENSE-apache2
@@ -0,0 +1,203 @@ + + 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 [yyyy] [name of copyright owner] + + 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. +
diff --git a/LICENSE-lgpl b/LICENSE-lgpl new file mode 100644 index 0000000..cca7fc2 --- /dev/null +++ b/LICENSE-lgpl
@@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library.
diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..c261dd6 --- /dev/null +++ b/NOTICE
@@ -0,0 +1,6 @@ +Cryptacular Java Library +Copyright (C) 2003-2024 Virginia Tech. +All rights reserved. + +This product includes software developed at +Virginia Tech (http://www.vt.edu).
diff --git a/README.md b/README.md new file mode 100644 index 0000000..2e205aa --- /dev/null +++ b/README.md
@@ -0,0 +1,11 @@ +# Cryptacular [](https://maven-badges.herokuapp.com/maven-central/org.cryptacular/cryptacular) + +The spectacular complement to the Bouncy Castle crypto API for Java. + +Cryptacular in a nutshell: + +* Utilities to perform common crypto operations (hash, encrypt, encode). +* Stateful, thread-safe bean components. +* Components to facilitate strict adherence to standards. +* Comprehensive documentation and examples. +
diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..58fe26e --- /dev/null +++ b/pom.xml
@@ -0,0 +1,417 @@ +<?xml version='1.0' encoding='UTF-8'?> +<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> + <groupId>org.cryptacular</groupId> + <artifactId>cryptacular</artifactId> + <packaging>jar</packaging> + <version>1.2.7</version> + <name>Cryptacular Library</name> + <description>The spectacular complement to the Bouncy Castle crypto API for Java.</description> + <url>https://www.cryptacular.org</url> + <issueManagement> + <system>GitHub</system> + <url>https://github.com/vt-middleware/cryptacular/issues</url> + </issueManagement> + <licenses> + <license> + <name>Apache 2</name> + <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url> + </license> + <license> + <name>GNU Lesser General Public License</name> + <url>https://www.gnu.org/licenses/lgpl-3.0.txt</url> + </license> + </licenses> + <scm> + <connection>scm:git:git@github.com:vt-middleware/cryptacular.git</connection> + <url>scm:git:git@github.com:vt-middleware/cryptacular.git</url> + <tag>HEAD</tag> + </scm> + <developers> + <developer> + <id>dfisher</id> + <name>Daniel Fisher</name> + <email>dfisher@vt.edu</email> + <organization>Virginia Tech</organization> + <organizationUrl>https://www.vt.edu</organizationUrl> + <roles> + <role>developer</role> + </roles> + </developer> + <developer> + <id>serac</id> + <name>Marvin S. Addison</name> + <email>serac@vt.edu</email> + <organization>Virginia Tech</organization> + <organizationUrl>https://www.vt.edu</organizationUrl> + <roles> + <role>developer</role> + </roles> + </developer> + </developers> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> + <checkstyle.dir>${basedir}/src/main/checkstyle</checkstyle.dir> + <spotbugs.dir>${basedir}/src/main/spotbugs</spotbugs.dir> + <assembly.dir>${basedir}/src/main/assembly</assembly.dir> + <testng.verbosity>0</testng.verbosity> + <japicmp.enabled>true</japicmp.enabled> + <japicmp.oldVersion>1.2.0</japicmp.oldVersion> + <bc.version>1.78.1</bc.version> + </properties> + + <dependencies> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcprov-jdk18on</artifactId> + <version>${bc.version}</version> + </dependency> + <dependency> + <groupId>org.bouncycastle</groupId> + <artifactId>bcpkix-jdk18on</artifactId> + <version>${bc.version}</version> + </dependency> + <!-- Remain on version 7.5 until source moves to JDK 11 --> + <dependency> + <groupId>org.testng</groupId> + <artifactId>testng</artifactId> + <version>7.5.1</version> + <scope>test</scope> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-enforcer-plugin</artifactId> + <version>3.5.0</version> + <executions> + <execution> + <id>enforce-maven</id> + <goals> + <goal>enforce</goal> + </goals> + <configuration> + <rules> + <requireMavenVersion> + <version>[3.8.0,)</version> + </requireMavenVersion> + </rules> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-resources-plugin</artifactId> + <version>3.3.1</version> + <executions> + <execution> + <id>copy-info</id> + <phase>validate</phase> + <goals> + <goal>copy-resources</goal> + </goals> + <configuration> + <outputDirectory>${basedir}/target/package-info</outputDirectory> + <resources> + <resource> + <directory>${basedir}</directory> + <filtering>false</filtering> + <includes> + <include>README*</include> + <include>LICENSE*</include> + <include>NOTICE*</include> + <include>pom.xml</include> + </includes> + </resource> + </resources> + </configuration> + </execution> + <execution> + <id>copy-scripts</id> + <phase>validate</phase> + <goals> + <goal>copy-resources</goal> + </goals> + <configuration> + <outputDirectory>${basedir}/target/bin</outputDirectory> + <resources> + <resource> + <directory>bin</directory> + <filtering>true</filtering> + </resource> + </resources> + </configuration> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-clean-plugin</artifactId> + <version>3.4.0</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-deploy-plugin</artifactId> + <version>3.1.2</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-install-plugin</artifactId> + <version>3.1.2</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-site-plugin</artifactId> + <version>3.12.1</version> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>3.13.0</version> + <configuration> + <fork>true</fork> + <debug>true</debug> + <showDeprecation>true</showDeprecation> + <showWarnings>true</showWarnings> + <compilerArgument>-Xlint:unchecked</compilerArgument> + <source>1.8</source> + <target>1.8</target> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <version>3.4.0</version> + <dependencies> + <dependency> + <groupId>com.puppycrawl.tools</groupId> + <artifactId>checkstyle</artifactId> + <!-- version 10 requires >= java 11 --> + <version>9.3</version> + </dependency> + </dependencies> + <configuration> + <configLocation>${checkstyle.dir}/checks.xml</configLocation> + <headerLocation>${checkstyle.dir}/header.txt</headerLocation> + <suppressionsLocation>${checkstyle.dir}/suppressions.xml</suppressionsLocation> + <includeTestSourceDirectory>true</includeTestSourceDirectory> + <failsOnError>true</failsOnError> + <outputFileFormat>plain</outputFileFormat> + <outputFile>${project.build.directory}/checkstyle-result.txt</outputFile> + </configuration> + <executions> + <execution> + <id>checkstyle</id> + <phase>compile</phase> + <goals> + <goal>checkstyle</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>com.github.spotbugs</groupId> + <artifactId>spotbugs-maven-plugin</artifactId> + <version>4.8.6.2</version> + <configuration> + <effort>Max</effort> + <threshold>Medium</threshold> + <xmlOutput>true</xmlOutput> + <xmlOutputDirectory>${project.build.directory}/spotbugs</xmlOutputDirectory> + <excludeFilterFile>${spotbugs.dir}/exclude.xml</excludeFilterFile> + </configuration> + <executions> + <execution> + <goals> + <goal>check</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <version>3.3.1</version> + <configuration> + <threadCount>10</threadCount> + <properties> + <property> + <name>surefire.testng.verbose</name> + <value>${testng.verbosity}</value> + </property> + </properties> + </configuration> + </plugin> + <plugin> + <groupId>com.github.siom79.japicmp</groupId> + <artifactId>japicmp-maven-plugin</artifactId> + <version>0.22.0</version> + <configuration> + <oldVersion> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>${project.artifactId}</artifactId> + <version>${japicmp.oldVersion}</version> + <type>jar</type> + </dependency> + </oldVersion> + <newVersion> + <file> + <path>${project.build.directory}/${project.artifactId}-${project.version}.${project.packaging}</path> + </file> + </newVersion> + <parameter> + <ignoreNonResolvableArtifacts>false</ignoreNonResolvableArtifacts> + <onlyBinaryIncompatible>true</onlyBinaryIncompatible> + <breakBuildOnBinaryIncompatibleModifications>${japicmp.enabled}</breakBuildOnBinaryIncompatibleModifications> + <packagingSupporteds> + <packagingSupported>jar</packagingSupported> + </packagingSupporteds> + </parameter> + </configuration> + <executions> + <execution> + <phase>verify</phase> + <goals> + <goal>cmp</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>3.8.0</version> + <configuration> + <source>8</source> + <links> + <link>https://download.oracle.com/javase/8/docs/api</link> + </links> + <bottom><![CDATA[<i>Copyright © 2003-2024 Virginia Tech. All Rights Reserved.</i>]]></bottom> + </configuration> + <executions> + <execution> + <id>javadoc</id> + <phase>package</phase> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-source-plugin</artifactId> + <version>3.3.1</version> + <executions> + <execution> + <id>source</id> + <phase>package</phase> + <goals> + <goal>jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-jar-plugin</artifactId> + <version>3.4.2</version> + <configuration> + <archive> + <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile> + </archive> + </configuration> + <executions> + <execution> + <id>jar</id> + <phase>package</phase> + <goals> + <goal>test-jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.felix</groupId> + <artifactId>maven-bundle-plugin</artifactId> + <version>5.1.9</version> + <executions> + <execution> + <id>bundle-manifest</id> + <phase>process-classes</phase> + <goals> + <goal>manifest</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <version>3.7.1</version> + <configuration> + <appendAssemblyId>true</appendAssemblyId> + <attach>false</attach> + <descriptors> + <descriptor>${assembly.dir}/cryptacular.xml</descriptor> + </descriptors> + <archiverConfig> + <defaultDirectoryMode>0755</defaultDirectoryMode> + </archiverConfig> + </configuration> + <executions> + <execution> + <id>assembly</id> + <phase>package</phase> + <goals> + <goal>single</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-release-plugin</artifactId> + <version>3.1.1</version> + <configuration> + <tagNameFormat>v@{project.version}</tagNameFormat> + </configuration> + </plugin> + </plugins> + </build> + <profiles> + <profile> + <id>sign-artifacts</id> + <activation> + <property> + <name>sign</name> + <value>true</value> + </property> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-gpg-plugin</artifactId> + <version>3.2.5</version> + <executions> + <execution> + <id>sign-artifacts</id> + <phase>package</phase> + <goals> + <goal>sign</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> +</project>
diff --git a/publish-snapshot b/publish-snapshot new file mode 100755 index 0000000..710ca19 --- /dev/null +++ b/publish-snapshot
@@ -0,0 +1,45 @@ +#!/bin/bash + +function user_continue() { + read -p "Do you want to continue? [y/n]" -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +} + +if [ "$#" -ne 1 ]; then + echo "USAGE: `basename $0` <repo-path>" + exit +fi + +REPO_PATH="${1}" +SHA=`git rev-parse --verify HEAD` +echo "=================================================================" +echo "BEGIN PUBLISH SNAPSHOT for revision ${SHA} at ${REPO_PATH}" +echo "=================================================================" +user_continue + +# update pom to release version +if ! mvn clean; then + echo "maven clean command failed, check your environment" + exit +fi +mvn package -Dmaven.javadoc.skip=true -B -V + +# Clone the maven repo +pushd ${REPO_PATH} +git pull +popd + +# Deploy the artifact to the maven repo +mvn deploy -DskipTests -DaltDeploymentRepository=snapshot::file://${REPO_PATH} + +# Push changes to the maven repo +pushd ${REPO_PATH} +git add . +git commit -a -m "Publish snapshot: ${SHA}" +git push origin master +popd + +echo "Successfully published SNAPSHOT artifacts for ${SHA}"
diff --git a/release b/release new file mode 100755 index 0000000..26ee1cd --- /dev/null +++ b/release
@@ -0,0 +1,138 @@ +#!/bin/bash + +set -e + +function user_continue() { + read -p "Do you want to continue? [y/n]" -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 1 + fi +} + +if [ "$#" -ne 5 ]; then + echo "USAGE: `basename $0` <branch> <release-version> <next-version> <sonatype-user> <sonatype-passwd>" + exit +fi + +PROJECT="cryptacular" +BRANCH="${1}" +if [ ! $(git rev-parse --abbrev-ref HEAD) = "${BRANCH}" ]; then + echo "The current branch must be ${BRANCH}" + exit +fi +RELEASE_VERSION="${2}" +if [[ ! "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "<release-version> must be of the form 'MAJOR.MINOR.REVISION'" + exit +fi +NEXT_VERSION="${3}" +if [[ ! "${NEXT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-SNAPSHOT$ ]]; then + echo "<next-version> must be of the form 'MAJOR.MINOR.REVISION-SNAPSHOT'" + exit +fi +SONATYPE_USER="${4}" +SONATYPE_PASSWORD="${5}" + +if [ -z $(git config --get user.signingkey) ]; then + echo "Git signing must be enabled. Add user.signingkey to ~/.gitconfig" + exit +fi + +if [ $(git tag -l | grep "$RELEASE_VERSION") ]; then + echo "Tag ${RELEASE_VERSION} already exists" + exit +fi + +echo "=================================================================" +echo "BEGIN RELEASE" +echo "PROJECT: ${PROJECT}" +echo "BRANCH TO TAG: ${BRANCH}" +echo "RELEASE VERSION: ${RELEASE_VERSION}" +echo "NEXT VERSION: ${NEXT_VERSION}" +echo "=================================================================" +user_continue + +# update pom to release version +if ! mvn clean; then + echo "maven clean command failed, check your environment" + exit +fi +mvn versions:set -DnewVersion=${RELEASE_VERSION} -DgenerateBackupPoms=false +echo "Updated pom to release version ${RELEASE_VERSION}" + +POM_VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout) +if [ "${POM_VERSION}" != "${RELEASE_VERSION}" ]; then + echo "POM version ${POM_VERSION} does not equal ${RELEASE_VERSION}" + exit +fi +user_continue + +# commit pom changes +git commit pom.xml -m "Update version for ${RELEASE_VERSION} release." + +# tag the release version +git tag -s v${RELEASE_VERSION} -m "Tagging ${RELEASE_VERSION} release." +echo "Tagged release ${RELEASE_VERSION}" + +# update pom to the next version +mvn versions:set -DnewVersion=${NEXT_VERSION} -DgenerateBackupPoms=false +echo "Updated pom to next version ${NEXT_VERSION}" + +# commit pom changes +git commit pom.xml -m "Bump version to ${NEXT_VERSION}." + +# push commits +git push origin ${BRANCH} + +# push release tag +git push origin v${RELEASE_VERSION} + +# checkout the release tag +git checkout v${RELEASE_VERSION} +echo "Switched to the tag version ${RELEASE_VERSION}" + +# build the release distribution +mvn -Dsign=true repository:bundle-create +gpg --armor --detach-sign target/${PROJECT}-${RELEASE_VERSION}-dist.tar.gz +gpg --armor --detach-sign target/${PROJECT}-${RELEASE_VERSION}-dist.zip + +# update the javadocs +echo "Updating javadocs" +user_continue + +git checkout gh-pages +git pull origin gh-pages +# remove root directory javadocs +git rm -r javadocs/org javadocs/*.html javadocs/*.css javadocs/*.js javadocs/package-list +# add new javadocs to root directory +cp -Rp target/apidocs/ javadocs +# add new javadocs to release version directory +cp -Rp target/apidocs/ javadocs/${RELEASE_VERSION} +git add javadocs +git commit -a -m "Updated javadocs for ${RELEASE_VERSION} release." +echo "Committed new javadocs" + +# add new binaries +echo "Adding release binaries" +user_continue + +mkdir downloads/${RELEASE_VERSION} +cp target/*-dist* downloads/${RELEASE_VERSION} +git add downloads/${RELEASE_VERSION} +git commit -a -m "Added binaries for ${RELEASE_VERSION} release." +echo "Committed new release binaries" + +# push changes to the server +git push origin gh-pages + +# upload bundle jar to sonatype +echo "Uploading bundle jar to sonatype" +user_continue + +curl -i -u ${SONATYPE_USER}:${SONATYPE_PASSWORD} \ + -F "file=@target/${PROJECT}-"${RELEASE_VERSION}"-bundle.jar" \ + "https://oss.sonatype.org/service/local/staging/bundle_upload" + +echo "Finished release ${RELEASE_VERSION} for ${PROJECT}" +
diff --git a/src/main/assembly/cryptacular.xml b/src/main/assembly/cryptacular.xml new file mode 100644 index 0000000..43486c8 --- /dev/null +++ b/src/main/assembly/cryptacular.xml
@@ -0,0 +1,64 @@ +<?xml version='1.0' encoding='UTF-8'?> +<assembly> + <id>dist</id> + <formats> + <format>tar.gz</format> + <format>zip</format> + </formats> + <dependencySets> + <dependencySet> + <useProjectArtifact>false</useProjectArtifact> + <outputDirectory>lib</outputDirectory> + <scope>runtime</scope> + <includes> + <include>*:jar</include> + </includes> + </dependencySet> + </dependencySets> + <fileSets> + <fileSet> + <directory>target/package-info</directory> + <outputDirectory>/</outputDirectory> + <includes> + <include>**</include> + </includes> + <useDefaultExcludes>true</useDefaultExcludes> + </fileSet> + <fileSet> + <directory>src</directory> + <useDefaultExcludes>true</useDefaultExcludes> + </fileSet> + <fileSet> + <directory>target</directory> + <outputDirectory>jars</outputDirectory> + <includes> + <include>${project.build.finalName}*.jar</include> + </includes> + <useDefaultExcludes>true</useDefaultExcludes> + </fileSet> + <fileSet> + <directory>target/site/apidocs</directory> + <outputDirectory>docs/apidocs</outputDirectory> + <useDefaultExcludes>true</useDefaultExcludes> + </fileSet> + <fileSet> + <directory>target/bin</directory> + <outputDirectory>bin</outputDirectory> + <useDefaultExcludes>true</useDefaultExcludes> + <includes> + <include>*.bat</include> + <include>*.config</include> + </includes> + </fileSet> + <fileSet> + <directory>target/bin</directory> + <outputDirectory>bin</outputDirectory> + <fileMode>0755</fileMode> + <useDefaultExcludes>true</useDefaultExcludes> + <excludes> + <exclude>*.bat</exclude> + <exclude>*.config</exclude> + </excludes> + </fileSet> + </fileSets> +</assembly>
diff --git a/src/main/checkstyle/checks.xml b/src/main/checkstyle/checks.xml new file mode 100644 index 0000000..59bb109 --- /dev/null +++ b/src/main/checkstyle/checks.xml
@@ -0,0 +1,222 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE module PUBLIC + "-//Puppy Crawl//DTD Check Configuration 1.2//EN" + "http://www.puppycrawl.com/dtds/configuration_1_2.dtd"> + +<module name="Checker"> + + <!-- limit source file size --> + <module name="FileLength"> + <property name="max" value="2000"/> + </module> + + <!-- require a header --> + <module name="RegexpHeader"> + <property name="headerFile" value="${checkstyle.header.file}"/> + <property name="fileExtensions" value="java"/> + </module> + + <!-- forbid tab characters in source files --> + <module name="FileTabCharacter"/> + + <module name="NewlineAtEndOfFile"> + <property name="fileExtensions" value="java"/> + </module> + + <module name="LineLength"> + <property name="ignorePattern" value="(^.*\$.*\$.*$)|(^import .*)"/> + <property name="max" value="120"/> + </module> + + <module name="TreeWalker"> + <property name="tabWidth" value="2"/> + + <module name="SuppressionCommentFilter"> + <property name="offCommentFormat" value="CheckStyle\:([\w\|]+) OFF"/> + <property name="onCommentFormat" value="CheckStyle\:([\w\|]+) ON"/> + <property name="checkFormat" value="$1"/> + </module> + + <!-- annotations --> + <module name="AnnotationUseStyle"/> + <module name="MissingDeprecated"/> + <module name="MissingOverride"/> + <module name="SuppressWarnings"/> + + <!-- javadocs --> + <module name="JavadocType"> + <property name="authorFormat" value="\S+"/> + </module> + <module name="JavadocMethod"/> + <module name="JavadocVariable"/> + <module name="JavadocStyle"> + <property name="checkFirstSentence" value="false"/> + </module> + + <!-- naming --> + <module name="AbstractClassName"> + <property name="format" value="^Abstract.*$"/> + </module> + <module name="ClassTypeParameterName"/> + <module name="ConstantName"/> + <module name="LocalFinalVariableName"/> + <module name="LocalVariableName"/> + <module name="MemberName"/> + <module name="MethodName"/> + <module name="MethodTypeParameterName"/> + <module name="PackageName"/> + <module name="ParameterName"/> + <module name="StaticVariableName"/> + <module name="TypeName"/> + + <!-- imports --> + <module name="AvoidStarImport"/> + <module name="AvoidStaticImport"/> + <module name="IllegalImport"/> + <module name="RedundantImport"/> + <module name="UnusedImports"/> + <module name="ImportOrder"> + <property name="groups" value="java,javax"/> + <property name="ordered" value="true"/> + <property name="option" value="bottom"/> + </module> + + <!-- sizes --> + <module name="MethodLength"> + <property name="max" value="300"/> + </module> + <module name="AnonInnerLength"> + <property name="max" value="30"/> + </module> + <module name="ParameterNumber"/> + <module name="OuterTypeNumber"/> + + <!-- whitespace --> + <module name="GenericWhitespace"/> + <module name="EmptyForInitializerPad"/> + <module name="EmptyForIteratorPad"/> + <module name="MethodParamPad"/> + <module name="NoWhitespaceAfter"/> + <module name="NoWhitespaceBefore"> + <property name="allowLineBreaks" value="true"/> + <property name="tokens" + value="SEMI,DOT,POST_DEC,POST_INC"/> + </module> + <module name="OperatorWrap"> + <property name="option" value="eol"/> + <property name="tokens" + value="BAND,BOR,BSR,BXOR,DIV,EQUAL,GE,GT,LAND,LE, + LITERAL_INSTANCEOF,LOR,LT,MINUS,MOD,NOT_EQUAL,PLUS, + SL,SR,STAR"/> + </module> + <module name="ParenPad"/> + <module name="TypecastParenPad"/> + <module name="WhitespaceAfter"/> + <module name="WhitespaceAround"> + <property name="tokens" + value="ASSIGN,BAND,BAND_ASSIGN,BOR,BOR_ASSIGN,BSR,BSR_ASSIGN, + BXOR,BXOR_ASSIGN,COLON,DIV,DIV_ASSIGN,EQUAL,GE,GT,LAND, + LE,LITERAL_ASSERT,LITERAL_CATCH,LITERAL_DO, + LITERAL_ELSE,LITERAL_FINALLY,LITERAL_FOR,LITERAL_IF, + LITERAL_RETURN,LITERAL_SYNCHRONIZED,LITERAL_TRY, + LITERAL_WHILE,LOR,LT,MINUS,MINUS_ASSIGN,MOD,MOD_ASSIGN, + NOT_EQUAL,PLUS_ASSIGN,QUESTION,SL, + SL_ASSIGN,SR,SR_ASSIGN,STAR,STAR_ASSIGN"/> + </module> + + <!-- modifiers --> + <module name="ModifierOrder"/> + <module name="RedundantModifier"/> + + <!-- blocks --> + <module name="EmptyBlock"/> + <module name="LeftCurly"> + <property name="option" value="nl"/> + <property name="tokens" + value="CLASS_DEF,CTOR_DEF,INTERFACE_DEF,METHOD_DEF"/> + </module> + <module name="NeedBraces"/> + <module name="RightCurly"/> + <module name="AvoidNestedBlocks"/> + + <!-- coding --> + <module name="ArrayTrailingComma"/> + <module name="CovariantEquals"/> + <module name="EmptyStatement"/> + <module name="EqualsAvoidNull"/> + <module name="EqualsHashCode"/> + <module name="FinalLocalVariable"/> + <module name="IllegalInstantiation"> + <property name="classes" value="java.lang.Boolean"/> + </module> + <module name="InnerAssignment"/> + <module name="MissingSwitchDefault"/> + <module name="SimplifyBooleanExpression"/> + <module name="SimplifyBooleanReturn"/> + <module name="StringLiteralEquality"/> + <module name="NestedIfDepth"> + <property name="max" value="5"/> + </module> + <module name="NestedTryDepth"> + <property name="max" value="5"/> + </module> + <module name="SuperClone"/> + <module name="SuperFinalize"/> + <module name="PackageDeclaration"/> + <module name="ReturnCount"> + <property name="max" value="3"/> + </module> + <module name="IllegalType"> + <property name="illegalClassNames" + value="java.util.GregorianCalendar, java.util.Hashtable, + java.util.HashSet, java.util.HashMap, + java.util.ArrayList, java.util.LinkedList, + java.util.LinkedHashMap, java.util.LinkedHashSet, + java.util.TreeSet, java.util.TreeMap, java.util.Vector"/> + </module> + <module name="DeclarationOrder"/> + <module name="ParameterAssignment"/> + <module name="ExplicitInitialization"/> + <module name="DefaultComesLast"/> + <module name="FallThrough"/> + <module name="MultipleVariableDeclarations"/> + <module name="RequireThis"> + <property name="checkFields" value="false"/> + <property name="checkMethods" value="false"/> + </module> + <module name="UnnecessaryParentheses"/> + + <!-- design --> + <module name="VisibilityModifier"> + <property name="protectedAllowed" value="true"/> + </module> + <module name="FinalClass"/> + <module name="InterfaceIsType"/> + <module name="HideUtilityClassConstructor"/> + <module name="MutableException"/> + <module name="ThrowsCount"> + <property name="max" value="4"/> + </module> + + <!-- prevent trailing whitespace --> + <module name="Regexp"> + <property name="format" value="[ \t]+$"/> + <property name="illegalPattern" value="true"/> + <property name="message" value="Trailing whitespace"/> + </module> + + <!-- misc --> + <module name="UpperEll"/> + <module name="ArrayTypeStyle"/> + <module name="FinalParameters"/> + <module name="Indentation"> + <property name="basicOffset" value="2"/> + <property name="caseIndent" value="0"/> + <property name="throwsIndent" value="2"/> + <property name="arrayInitIndent" value="2"/> + <property name="lineWrappingIndentation" value="2"/> + </module> + <module name="TrailingComment"/> + + </module> +</module>
diff --git a/src/main/checkstyle/header.txt b/src/main/checkstyle/header.txt new file mode 100644 index 0000000..0ce0ff7 --- /dev/null +++ b/src/main/checkstyle/header.txt
@@ -0,0 +1 @@ +/\* See LICENSE for licensing and NOTICE for copyright. \*/
diff --git a/src/main/checkstyle/suppressions.xml b/src/main/checkstyle/suppressions.xml new file mode 100644 index 0000000..5eaf9df --- /dev/null +++ b/src/main/checkstyle/suppressions.xml
@@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE suppressions PUBLIC + "-//Puppy Crawl//DTD Suppressions 1.1//EN" + "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd"> + +<suppressions> + <suppress checks="AvoidStaticImport|Javadoc.*" + files=".*Test\.java" /> +</suppressions>
diff --git a/src/main/java/org/cryptacular/CiphertextHeader.java b/src/main/java/org/cryptacular/CiphertextHeader.java new file mode 100644 index 0000000..9b18454 --- /dev/null +++ b/src/main/java/org/cryptacular/CiphertextHeader.java
@@ -0,0 +1,263 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.cryptacular.util.ByteUtil; + +/** + * Cleartext header prepended to ciphertext providing data required for decryption. + * + * <p>Data format:</p> + * + * <pre> + +-----+----------+-------+------------+---------+ + | Len | NonceLen | Nonce | KeyNameLen | KeyName | + +-----+----------+-------+------------+---------+ + * </pre> + * + * <p>Where fields are defined as follows:</p> + * + * <ul> + * <li>Len - Total header length in bytes (4-byte integer)</li> + * <li>NonceLen - Nonce length in bytes (4-byte integer)</li> + * <li>Nonce - Nonce bytes (variable length)</li> + * <li>KeyNameLen (OPTIONAL) - Key name length in bytes (4-byte integer)</li> + * <li>KeyName (OPTIONAL) - Key name encoded as bytes in platform-specific encoding (variable length)</li> + * </ul> + * + * <p>The last two fields are optional and provide support for multiple keys at the encryption provider. A common case + * for multiple keys is key rotation; by tagging encrypted data with a key name, an old key may be retrieved by name to + * decrypt outstanding data which will be subsequently re-encrypted with a new key.</p> + * + * @author Middleware Services + * + * @deprecated Superseded by {@link CiphertextHeaderV2} + */ +@Deprecated +public class CiphertextHeader +{ + /** Maximum nonce length in bytes. */ + protected static final int MAX_NONCE_LEN = 255; + + /** Maximum key name length in bytes. */ + protected static final int MAX_KEYNAME_LEN = 500; + + /** Header nonce field value. */ + protected final byte[] nonce; + + /** Header key name field value. */ + protected String keyName; + + /** Header length in bytes. */ + protected int length; + + + /** + * Creates a new instance with only a nonce. + * + * @param nonce Nonce bytes. + */ + public CiphertextHeader(final byte[] nonce) + { + this(nonce, null); + } + + + /** + * Creates a new instance with a nonce and named key. + * + * @param nonce Nonce bytes. + * @param keyName Key name. + */ + public CiphertextHeader(final byte[] nonce, final String keyName) + { + if (nonce.length > MAX_NONCE_LEN) { + throw new IllegalArgumentException("Nonce exceeds size limit in bytes (" + MAX_NONCE_LEN + ")"); + } + if (keyName != null) { + if (ByteUtil.toBytes(keyName).length > MAX_KEYNAME_LEN) { + throw new IllegalArgumentException("Key name exceeds size limit in bytes (" + MAX_KEYNAME_LEN + ")"); + } + } + this.nonce = nonce; + this.keyName = keyName; + length = computeLength(); + } + + /** + * Gets the header length in bytes. + * + * @return Header length in bytes. + */ + public int getLength() + { + return this.length; + } + + /** + * Gets the bytes of the nonce/IV. + * + * @return Nonce bytes. + */ + public byte[] getNonce() + { + return this.nonce; + } + + /** + * Gets the encryption key name stored in the header. + * + * @return Encryption key name. + */ + public String getKeyName() + { + return this.keyName; + } + + + /** + * Encodes the header into bytes. + * + * @return Byte representation of header. + */ + public byte[] encode() + { + final ByteBuffer bb = ByteBuffer.allocate(length); + bb.order(ByteOrder.BIG_ENDIAN); + bb.putInt(length); + bb.putInt(nonce.length); + bb.put(nonce); + if (keyName != null) { + final byte[] b = keyName.getBytes(); + bb.putInt(b.length); + bb.put(b); + } + return bb.array(); + } + + + /** + * @return Length of this header encoded as bytes. + */ + protected int computeLength() + { + int len = 8 + nonce.length; + if (keyName != null) { + len += 4 + keyName.getBytes().length; + } + return len; + } + + + /** + * Creates a header from encrypted data containing a cleartext header prepended to the start. + * + * @param data Encrypted data with prepended header data. + * + * @return Decoded header. + * + * @throws EncodingException when ciphertext header cannot be decoded. + */ + public static CiphertextHeader decode(final byte[] data) throws EncodingException + { + final ByteBuffer bb = ByteBuffer.wrap(data); + bb.order(ByteOrder.BIG_ENDIAN); + + final int length = bb.getInt(); + if (length < 0) { + throw new EncodingException("Bad ciphertext header"); + } + + final byte[] nonce; + final int nonceLen; + try { + nonceLen = bb.getInt(); + if (nonceLen > MAX_NONCE_LEN) { + throw new EncodingException("Bad ciphertext header: maximum nonce length exceeded"); + } + nonce = new byte[nonceLen]; + bb.get(nonce); + } catch (IndexOutOfBoundsException | BufferUnderflowException e) { + throw new EncodingException("Bad ciphertext header"); + } + + String keyName = null; + if (length > nonce.length + 8) { + final byte[] b; + final int keyLen; + try { + keyLen = bb.getInt(); + if (keyLen > MAX_KEYNAME_LEN) { + throw new EncodingException("Bad ciphertext header: maximum key length exceeded"); + } + b = new byte[keyLen]; + bb.get(b); + keyName = new String(b); + } catch (IndexOutOfBoundsException | BufferUnderflowException e) { + throw new EncodingException("Bad ciphertext header"); + } + } + + return new CiphertextHeader(nonce, keyName); + } + + + /** + * Creates a header from encrypted data containing a cleartext header prepended to the start. + * + * @param input Input stream that is positioned at the start of ciphertext header data. + * + * @return Decoded header. + * + * @throws EncodingException when ciphertext header cannot be decoded. + * @throws StreamException on stream IO errors. + */ + public static CiphertextHeader decode(final InputStream input) throws EncodingException, StreamException + { + final int length = ByteUtil.readInt(input); + if (length < 0) { + throw new EncodingException("Bad ciphertext header"); + } + + final byte[] nonce; + final int nonceLen; + try { + nonceLen = ByteUtil.readInt(input); + if (nonceLen > MAX_NONCE_LEN) { + throw new EncodingException("Bad ciphertext header: maximum nonce size exceeded"); + } + nonce = new byte[nonceLen]; + input.read(nonce); + } catch (ArrayIndexOutOfBoundsException e) { + throw new EncodingException("Bad ciphertext header"); + } catch (IOException e) { + throw new StreamException(e); + } + + String keyName = null; + if (length > nonce.length + 8) { + final byte[] b; + final int keyLen; + try { + keyLen = ByteUtil.readInt(input); + if (keyLen > MAX_KEYNAME_LEN) { + throw new EncodingException("Bad ciphertext header: maximum key length exceeded"); + } + b = new byte[keyLen]; + input.read(b); + } catch (ArrayIndexOutOfBoundsException e) { + throw new EncodingException("Bad ciphertext header"); + } catch (IOException e) { + throw new StreamException(e); + } + keyName = new String(b); + } + + return new CiphertextHeader(nonce, keyName); + } + +}
diff --git a/src/main/java/org/cryptacular/CiphertextHeaderV2.java b/src/main/java/org/cryptacular/CiphertextHeaderV2.java new file mode 100644 index 0000000..bb9d566 --- /dev/null +++ b/src/main/java/org/cryptacular/CiphertextHeaderV2.java
@@ -0,0 +1,311 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.BufferUnderflowException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.function.BiConsumer; +import java.util.function.Function; +import javax.crypto.SecretKey; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.macs.HMac; +import org.cryptacular.util.ByteUtil; + +/** + * Cleartext header prepended to ciphertext providing data required for decryption. + * + * <p>Data format:</p> + * + * <pre> + +---------+---------+---+----------+-------+------+ + | Version | KeyName | 0 | NonceLen | Nonce | HMAC | + +---------+---------+---+----------+-------+------+ + | | + +--- 4 ---+--- x ---+ 1 +--- 1 ----+-- y --+- 32 -+ + * </pre> + * + * <p>Where fields are defined as follows:</p> + * + * <ul> + * <li>Version - Header version format as a negative number (4-byte integer). Current version is -2.</li> + * <li>KeyName - Symbolic key name encoded as UTF-8 bytes (variable length)</li> + * <li>0 - Null byte signifying the end of the symbolic key name</li> + * <li>NonceLen - Nonce length in bytes (1-byte unsigned integer)</li> + * <li>Nonce - Nonce bytes (variable length)</li> + * <li>HMAC - HMAC-256 over preceding fields (32 bytes)</li> + * </ul> + * + * <p>The last two fields provide support for multiple keys at the encryption provider. A common case for multiple + * keys is key rotation; by tagging encrypted data with a key name, an old key may be retrieved by name to decrypt + * outstanding data which will be subsequently re-encrypted with a new key.</p> + * + * @author Middleware Services + */ +public class CiphertextHeaderV2 extends CiphertextHeader +{ + /** Header version format. */ + private static final int VERSION = -2; + + /** Size of HMAC algorithm output in bytes. */ + private static final int HMAC_SIZE = 32; + + /** Function to resolve a key from a symbolic key name. */ + private Function<String, SecretKey> keyLookup; + + + /** + * Creates a new instance with a nonce and named key. + * + * @param nonce Nonce bytes. + * @param keyName Key name. + */ + public CiphertextHeaderV2(final byte[] nonce, final String keyName) + { + super(nonce, keyName); + if (keyName == null || keyName.isEmpty()) { + throw new IllegalArgumentException("Key name is required"); + } + } + + + /** + * Sets the function to resolve keys from {@link #keyName}. + * + * @param keyLookup Key lookup function. + */ + public void setKeyLookup(final Function<String, SecretKey> keyLookup) + { + this.keyLookup = keyLookup; + } + + + @Override + public byte[] encode() + { + final SecretKey key = keyLookup != null ? keyLookup.apply(keyName) : null; + if (key == null) { + throw new IllegalStateException("Could not resolve secret key to generate header HMAC"); + } + return encode(key); + } + + + /** + * Encodes the header into bytes. + * + * @param hmacKey Key used to generate header HMAC. + * + * @return Byte representation of header. + */ + public byte[] encode(final SecretKey hmacKey) + { + if (hmacKey == null) { + throw new IllegalArgumentException("Secret key cannot be null"); + } + final ByteBuffer bb = ByteBuffer.allocate(length); + bb.order(ByteOrder.BIG_ENDIAN); + bb.putInt(VERSION); + bb.put(ByteUtil.toBytes(keyName)); + bb.put((byte) 0); + bb.put(ByteUtil.toUnsignedByte(nonce.length)); + bb.put(nonce); + bb.put(hmac(bb.array(), 0, bb.limit() - HMAC_SIZE)); + return bb.array(); + } + + + /** + * @return Length of this header encoded as bytes. + */ + protected int computeLength() + { + return 4 + ByteUtil.toBytes(keyName).length + 2 + nonce.length + HMAC_SIZE; + } + + + /** + * Creates a header from encrypted data containing a cleartext header prepended to the start. + * + * @param data Encrypted data with prepended header data. + * @param keyLookup Function used to look up the secret key from the symbolic key name in the header. + * + * @return Decoded header. + * + * @throws EncodingException when ciphertext header cannot be decoded. + */ + public static CiphertextHeaderV2 decode(final byte[] data, final Function<String, SecretKey> keyLookup) + throws EncodingException + { + final ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN); + return decodeInternal( + ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN), + keyLookup, + ByteBuffer -> bb.getInt(), + ByteBuffer -> bb.get(), + (ByteBuffer, output) -> bb.get(output)); + } + + + /** + * Creates a header from encrypted data containing a cleartext header prepended to the start. + * + * @param input Input stream that is positioned at the start of ciphertext header data. + * @param keyLookup Function used to look up the secret key from the symbolic key name in the header. + * + * @return Decoded header. + * + * @throws EncodingException when ciphertext header cannot be decoded. + * @throws StreamException on stream IO errors. + */ + public static CiphertextHeaderV2 decode(final InputStream input, final Function<String, SecretKey> keyLookup) + throws EncodingException, StreamException + { + return decodeInternal( + input, keyLookup, ByteUtil::readInt, CiphertextHeaderV2::readByte, CiphertextHeaderV2::readInto); + } + + + /** + * Internal header decoding routine. + * + * @param <T> Type of input source. + * @param source Source of header data (input stream or byte buffer). + * @param keyLookup Function to look up key from symbolic key name in header. + * @param readIntFn Function that produces a 4-byte integer from the input source. + * @param readByteFn Function that produces a byte from the input source. + * @param readBytesConsumer Function that fills a byte array from the input source. + * + * @return Decoded header. + */ + private static <T> CiphertextHeaderV2 decodeInternal( + final T source, + final Function<String, SecretKey> keyLookup, + final Function<T, Integer> readIntFn, + final Function<T, Byte> readByteFn, + final BiConsumer<T, byte[]> readBytesConsumer) + { + final SecretKey key; + final String keyName; + final byte[] nonce; + final byte[] hmac; + try { + final int version = readIntFn.apply(source); + if (version != VERSION) { + throw new EncodingException("Unsupported ciphertext header version"); + } + final ByteArrayOutputStream out = new ByteArrayOutputStream(100); + byte b; + int count = 0; + while ((b = readByteFn.apply(source)) != 0) { + out.write(b); + if (out.size() > MAX_KEYNAME_LEN) { + throw new EncodingException("Bad ciphertext header: maximum nonce length exceeded"); + } + count++; + } + keyName = ByteUtil.toString(out.toByteArray(), 0, count); + key = keyLookup.apply(keyName); + if (key == null) { + throw new IllegalStateException("Symbolic key name mentioned in header was not found"); + } + final int nonceLen = ByteUtil.toInt(readByteFn.apply(source)); + nonce = new byte[nonceLen]; + readBytesConsumer.accept(source, nonce); + hmac = new byte[HMAC_SIZE]; + readBytesConsumer.accept(source, hmac); + } catch (IndexOutOfBoundsException | BufferUnderflowException e) { + throw new EncodingException("Bad ciphertext header"); + } + final CiphertextHeaderV2 header = new CiphertextHeaderV2(nonce, keyName); + final byte[] encoded = header.encode(key); + if (!arraysEqual(hmac, 0, encoded, encoded.length - HMAC_SIZE, HMAC_SIZE)) { + throw new EncodingException("Ciphertext header HMAC verification failed"); + } + header.setKeyLookup(keyLookup); + return header; + } + + + /** + * Generates an HMAC-256 over the given input byte array. + * + * @param input Input bytes. + * @param offset Starting position in input byte array. + * @param length Number of bytes in input to consume. + * + * @return HMAC as byte array. + */ + private static byte[] hmac(final byte[] input, final int offset, final int length) + { + final HMac hmac = new HMac(new SHA256Digest()); + final byte[] output = new byte[HMAC_SIZE]; + hmac.update(input, offset, length); + hmac.doFinal(output, 0); + return output; + } + + + /** + * Read <code>output.length</code> bytes from the input stream into the output buffer. + * + * @param input Input stream. + * @param output Output buffer. + * + * @return number of bytes read + * + * @throws StreamException on stream IO errors. + */ + private static int readInto(final InputStream input, final byte[] output) + { + try { + return input.read(output); + } catch (IOException e) { + throw new StreamException(e); + } + } + + + /** + * Read a single byte from the input stream. + * + * @param input Input stream. + * + * @return Byte read from input stream. + */ + private static byte readByte(final InputStream input) + { + try { + return (byte) input.read(); + } catch (IOException e) { + throw new StreamException(e); + } + } + + + /** + * Determines if two byte array ranges are equal bytewise. + * + * @param a First array to compare. + * @param aOff Offset into first array. + * @param b Second array to compare. + * @param bOff Offset into second array. + * @param length Number of bytes to compare. + * + * @return True if every byte in the given range is equal, false otherwise. + */ + private static boolean arraysEqual(final byte[] a, final int aOff, final byte[] b, final int bOff, final int length) + { + if (length + aOff > a.length || length + bOff > b.length) { + return false; + } + for (int i = 0; i < length; i++) { + if (a[i + aOff] != b[i + bOff]) { + return false; + } + } + return true; + } +}
diff --git a/src/main/java/org/cryptacular/CryptoException.java b/src/main/java/org/cryptacular/CryptoException.java new file mode 100644 index 0000000..94756b2 --- /dev/null +++ b/src/main/java/org/cryptacular/CryptoException.java
@@ -0,0 +1,32 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +/** + * Runtime error describing a generic cryptographic problem (e.g. bad padding, unsupported cipher). + * + * @author Middleware Services + */ +public class CryptoException extends RuntimeException +{ + /** + * Creates a new instance with the given error message. + * + * @param message Error message. + */ + public CryptoException(final String message) + { + super(message); + } + + + /** + * Creates a new instance with the given error message and cause. + * + * @param message Error message. + * @param cause Error cause. + */ + public CryptoException(final String message, final Throwable cause) + { + super(message, cause); + } +}
diff --git a/src/main/java/org/cryptacular/EncodingException.java b/src/main/java/org/cryptacular/EncodingException.java new file mode 100644 index 0000000..641cf81 --- /dev/null +++ b/src/main/java/org/cryptacular/EncodingException.java
@@ -0,0 +1,32 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +/** + * Runtime error describing an encoding problem of a cryptographic primitive (e.g. private key, X.509 certificate). + * + * @author Middleware Services + */ +public class EncodingException extends RuntimeException +{ + /** + * Creates a new instance with the given error message. + * + * @param message Error message. + */ + public EncodingException(final String message) + { + super(message); + } + + + /** + * Creates a new instance with the given error message and cause. + * + * @param message Error message. + * @param cause Error cause. + */ + public EncodingException(final String message, final Throwable cause) + { + super(message, cause); + } +}
diff --git a/src/main/java/org/cryptacular/SaltedHash.java b/src/main/java/org/cryptacular/SaltedHash.java new file mode 100644 index 0000000..b09d78c --- /dev/null +++ b/src/main/java/org/cryptacular/SaltedHash.java
@@ -0,0 +1,120 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +import org.cryptacular.codec.Encoder; +import org.cryptacular.util.CodecUtil; + +/** + * Container for the output of a salted hash operation that includes both the digest output and salt value. + * + * @author Middleware Services + */ +public class SaltedHash +{ + + /** Digest output. */ + private final byte[] hash; + + /** Salt value. */ + private final byte[] salt; + + + /** + * Creates a new instance with digest and salt data. + * + * @param hash Digest output. + * @param salt Salt value used to compute salt. + */ + public SaltedHash(final byte[] hash, final byte[] salt) + { + this.hash = hash; + this.salt = salt; + } + + + /** + * Creates a new instance from byte input that contains the concatenation of digest output and salt. + * + * @param hashWithSalt Concatenation of hash and salt. + * @param digestLength Number of bytes in digest output. + * @param toEnd True if salt is appended to end of hash, false if salt is prepended to hash. + */ + public SaltedHash(final byte[] hashWithSalt, final int digestLength, final boolean toEnd) + { + this.hash = new byte[digestLength]; + this.salt = new byte[hashWithSalt.length - digestLength]; + if (toEnd) { + System.arraycopy(hashWithSalt, 0, hash, 0, hash.length); + System.arraycopy(hashWithSalt, hash.length, salt, 0, salt.length); + } else { + System.arraycopy(hashWithSalt, 0, salt, 0, salt.length); + System.arraycopy(hashWithSalt, salt.length, hash, 0, hash.length); + } + } + + + /** @return Digest output. */ + public byte[] getHash() + { + return hash; + } + + + /** @return Salt value. */ + public byte[] getSalt() + { + return salt; + } + + /** + * Gets N bytes of salt. + * + * @param n Number of bytes of salt; must be less than or equal to salt size. + * + * @return First N bytes of salt. + */ + public byte[] getSalt(final int n) + { + if (n > salt.length) { + throw new IllegalArgumentException("Requested size exceeded length: " + n + ">" + salt.length); + } + final byte[] bytes = new byte[n]; + System.arraycopy(salt, 0, bytes, 0, n); + return bytes; + } + + + /** + * Gets an encoded string of the concatenation of digest output and salt. + * + * @param toEnd True to append salt to end of hash, false to prefix hash with salt. + * @param encoder Encodes concatenated bytes to a string. + * + * @return Salt concatenated to hash encoded as a string. + */ + public String concatenateSalt(final boolean toEnd, final Encoder encoder) + { + return CodecUtil.encode(encoder, concatenateSalt(toEnd)); + } + + + /** + * Gets a byte array containing the concatenation of digest output and salt. + * + * @param toEnd True to append salt to end of hash, false to prefix hash with salt. + * + * @return Salt concatenated to hash. + */ + public byte[] concatenateSalt(final boolean toEnd) + { + final byte[] output = new byte[hash.length + salt.length]; + if (toEnd) { + System.arraycopy(hash, 0, output, 0, hash.length); + System.arraycopy(salt, 0, output, hash.length, salt.length); + } else { + System.arraycopy(salt, 0, output, 0, salt.length); + System.arraycopy(hash, 0, output, salt.length, hash.length); + } + return output; + } +}
diff --git a/src/main/java/org/cryptacular/StreamException.java b/src/main/java/org/cryptacular/StreamException.java new file mode 100644 index 0000000..9aabea7 --- /dev/null +++ b/src/main/java/org/cryptacular/StreamException.java
@@ -0,0 +1,33 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +import java.io.IOException; + +/** + * Runtime exception thrown on stream IO errors. Effectively a runtime equivalent of {@link java.io.IOException}. + * + * @author Middleware Services + */ +public class StreamException extends RuntimeException +{ + /** + * Creates a new instance with the given error message. + * + * @param message Error message. + */ + public StreamException(final String message) + { + super(message); + } + + + /** + * Creates a new instance with causing IO exception. + * + * @param cause IO exception to wrap. + */ + public StreamException(final IOException cause) + { + super("IO error", cause); + } +}
diff --git a/src/main/java/org/cryptacular/adapter/AEADBlockCipherAdapter.java b/src/main/java/org/cryptacular/adapter/AEADBlockCipherAdapter.java new file mode 100644 index 0000000..27bc4aa --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/AEADBlockCipherAdapter.java
@@ -0,0 +1,78 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.cryptacular.CryptoException; + +/** + * Adapts a {@link AEADBlockCipherAdapter}. + * + * @author Middleware Services + */ +public class AEADBlockCipherAdapter implements BlockCipherAdapter +{ + + /** All methods delegate to this instance. */ + private final AEADBlockCipher cipherDelegate; + + + /** + * Creates a new instance that delegates to the given cipher. + * + * @param delegate Adapted cipher. + */ + public AEADBlockCipherAdapter(final AEADBlockCipher delegate) + { + cipherDelegate = delegate; + } + + + @Override + public int getOutputSize(final int len) + { + return cipherDelegate.getOutputSize(len); + } + + + @Override + public void init(final boolean forEncryption, final CipherParameters params) throws CryptoException + { + try { + cipherDelegate.init(forEncryption, params); + } catch (RuntimeException e) { + throw new CryptoException("Cipher initialization error", e); + } + } + + + @Override + public int processBytes(final byte[] in, final int inOff, final int len, final byte[] out, final int outOff) + throws CryptoException + { + try { + return cipherDelegate.processBytes(in, inOff, len, out, outOff); + } catch (RuntimeException e) { + throw new CryptoException("Cipher processing error", e); + } + } + + + @Override + public int doFinal(final byte[] out, final int outOff) throws CryptoException + { + try { + return cipherDelegate.doFinal(out, outOff); + } catch (InvalidCipherTextException e) { + throw new CryptoException("Error finalizing cipher", e); + } + } + + + @Override + public void reset() + { + cipherDelegate.reset(); + } +}
diff --git a/src/main/java/org/cryptacular/adapter/AbstractWrappedDSAKey.java b/src/main/java/org/cryptacular/adapter/AbstractWrappedDSAKey.java new file mode 100644 index 0000000..e10ba48 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/AbstractWrappedDSAKey.java
@@ -0,0 +1,63 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.math.BigInteger; +import java.security.interfaces.DSAParams; +import org.bouncycastle.crypto.params.DSAKeyParameters; + +/** + * Base class for DSA wrapped keys. + * + * @param <T> DSA key parameters type. + * + * @author Middleware Services + */ +public abstract class AbstractWrappedDSAKey<T extends DSAKeyParameters> extends AbstractWrappedKey<T> +{ + + /** DSA algorithm name. */ + private static final String ALGORITHM = "DSA"; + + + /** + * Creates a new instance that wraps the given key. + * + * @param wrappedKey Key to wrap. + */ + public AbstractWrappedDSAKey(final T wrappedKey) + { + super(wrappedKey); + } + + + /** @return DSA key parameters. */ + public DSAParams getParams() + { + return new DSAParams() { + @Override + public BigInteger getP() + { + return delegate.getParameters().getP(); + } + + @Override + public BigInteger getQ() + { + return delegate.getParameters().getQ(); + } + + @Override + public BigInteger getG() + { + return delegate.getParameters().getG(); + } + }; + } + + + @Override + public String getAlgorithm() + { + return ALGORITHM; + } +}
diff --git a/src/main/java/org/cryptacular/adapter/AbstractWrappedECKey.java b/src/main/java/org/cryptacular/adapter/AbstractWrappedECKey.java new file mode 100644 index 0000000..46d0c96 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/AbstractWrappedECKey.java
@@ -0,0 +1,55 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.jcajce.provider.asymmetric.util.EC5Util; + +/** + * Base class for wrapped EC keys. + * + * @param <T> EC key parameters type. + * + * @author Middleware Services + */ +public abstract class AbstractWrappedECKey<T extends ECKeyParameters> extends AbstractWrappedKey<T> +{ + + /** Elliptic curve algorithm name. */ + private static final String ALGORITHM = "EC"; + + + /** + * Creates a new instance that wraps the given key. + * + * @param wrappedKey Key to wrap. + */ + public AbstractWrappedECKey(final T wrappedKey) + { + super(wrappedKey); + } + + + /** @return EC domain parameters. */ + public ECParameterSpec getParams() + { + final ECDomainParameters params = delegate.getParameters(); + return + new ECParameterSpec( + EC5Util.convertCurve(params.getCurve(), params.getSeed()), + new ECPoint( + params.getG().normalize().getXCoord().toBigInteger(), + params.getG().normalize().getYCoord().toBigInteger()), + params.getN(), + params.getH().intValue()); + } + + + @Override + public String getAlgorithm() + { + return ALGORITHM; + } +}
diff --git a/src/main/java/org/cryptacular/adapter/AbstractWrappedKey.java b/src/main/java/org/cryptacular/adapter/AbstractWrappedKey.java new file mode 100644 index 0000000..15d65bd --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/AbstractWrappedKey.java
@@ -0,0 +1,73 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.io.IOException; +import java.security.Key; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.PrivateKeyInfoFactory; +import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory; +import org.cryptacular.EncodingException; + +/** + * JCE/JDK key base class that wraps a BC native private key. + * + * @param <T> Asymmetric key parameters type wrapped by this class. + * + * @author Middleware Services + */ +public abstract class AbstractWrappedKey<T extends AsymmetricKeyParameter> implements Key +{ + + /** PKCS#8 format identifier used with private keys. */ + public static final String PKCS8_FORMAT = "PKCS#8"; + + /** X.509 format identifier used with private keys. */ + public static final String X509_FORMAT = "X.509"; + + + /** Wrapped key. */ + protected final T delegate; + + + /** + * Creates a new instance that wraps the given BC key. + * + * @param wrappedKey BC key to wrap. + */ + public AbstractWrappedKey(final T wrappedKey) + { + if (wrappedKey == null) { + throw new IllegalArgumentException("Wrapped key cannot be null."); + } + delegate = wrappedKey; + } + + + /** @return {@value #PKCS8_FORMAT} in the case of a private key, otherwise {@link #X509_FORMAT}. */ + @Override + public String getFormat() + { + if (delegate.isPrivate()) { + return PKCS8_FORMAT; + } + return X509_FORMAT; + } + + + /** + * @return Encoded PrivateKeyInfo structure in the case of a private key, otherwise an encoded SubjectPublicKeyInfo + * structure. + */ + @Override + public byte[] getEncoded() + { + try { + if (delegate.isPrivate()) { + return PrivateKeyInfoFactory.createPrivateKeyInfo(delegate).getEncoded(); + } + return SubjectPublicKeyInfoFactory.createSubjectPublicKeyInfo(delegate).getEncoded(); + } catch (IOException e) { + throw new EncodingException("Key encoding error", e); + } + } +}
diff --git a/src/main/java/org/cryptacular/adapter/AbstractWrappedRSAKey.java b/src/main/java/org/cryptacular/adapter/AbstractWrappedRSAKey.java new file mode 100644 index 0000000..4d50a55 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/AbstractWrappedRSAKey.java
@@ -0,0 +1,44 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.math.BigInteger; +import org.bouncycastle.crypto.params.RSAKeyParameters; + +/** + * Base class for RSA wrapped keys. + * + * @param <T> RSA key parameters type handled by this class. + * + * @author Middleware Services + */ +public abstract class AbstractWrappedRSAKey<T extends RSAKeyParameters> extends AbstractWrappedKey<T> +{ + + /** RSA algorithm name. */ + private static final String ALGORITHM = "RSA"; + + + /** + * Creates a new instance that wraps the given key. + * + * @param wrappedKey Key to wrap. + */ + public AbstractWrappedRSAKey(final T wrappedKey) + { + super(wrappedKey); + } + + + /** @return Gets the RSA modulus. */ + public BigInteger getModulus() + { + return delegate.getModulus(); + } + + + @Override + public String getAlgorithm() + { + return ALGORITHM; + } +}
diff --git a/src/main/java/org/cryptacular/adapter/BlockCipherAdapter.java b/src/main/java/org/cryptacular/adapter/BlockCipherAdapter.java new file mode 100644 index 0000000..c24a718 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/BlockCipherAdapter.java
@@ -0,0 +1,35 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import org.cryptacular.CryptoException; + +/** + * Adapter for all block cipher types. + * + * @author Middleware Services + */ +public interface BlockCipherAdapter extends CipherAdapter +{ + + /** + * Gets the size of the output buffer required to hold the output of an input buffer of the given size. + * + * @param len Length of input buffer. + * + * @return Size of output buffer. + */ + int getOutputSize(int len); + + + /** + * Finish the encryption/decryption operation (e.g. apply padding). + * + * @param out Output buffer to receive final processing output. + * @param outOff Offset into output buffer where processed data should start. + * + * @return Number of bytes written to output buffer. + * + * @throws CryptoException on underlying cipher finalization errors. + */ + int doFinal(byte[] out, int outOff) throws CryptoException; +}
diff --git a/src/main/java/org/cryptacular/adapter/BufferedBlockCipherAdapter.java b/src/main/java/org/cryptacular/adapter/BufferedBlockCipherAdapter.java new file mode 100644 index 0000000..2877ac1 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/BufferedBlockCipherAdapter.java
@@ -0,0 +1,78 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.cryptacular.CryptoException; + +/** + * Adapts a {@link BufferedBlockCipher}. + * + * @author Middleware Services + */ +public class BufferedBlockCipherAdapter implements BlockCipherAdapter +{ + + /** All methods delegate to this instance. */ + private final BufferedBlockCipher cipherDelegate; + + + /** + * Creates a new instance that delegates to the given cipher. + * + * @param delegate Adapted cipher. + */ + public BufferedBlockCipherAdapter(final BufferedBlockCipher delegate) + { + cipherDelegate = delegate; + } + + + @Override + public int getOutputSize(final int len) + { + return cipherDelegate.getOutputSize(len); + } + + + @Override + public void init(final boolean forEncryption, final CipherParameters params) throws CryptoException + { + try { + cipherDelegate.init(forEncryption, params); + } catch (RuntimeException e) { + throw new CryptoException("Cipher initialization error", e); + } + } + + + @Override + public int processBytes(final byte[] in, final int inOff, final int len, final byte[] out, final int outOff) + throws CryptoException + { + try { + return cipherDelegate.processBytes(in, inOff, len, out, outOff); + } catch (RuntimeException e) { + throw new CryptoException("Cipher processing error", e); + } + } + + + @Override + public int doFinal(final byte[] out, final int outOff) throws CryptoException + { + try { + return cipherDelegate.doFinal(out, outOff); + } catch (InvalidCipherTextException e) { + throw new CryptoException("Error finalizing cipher", e); + } + } + + + @Override + public void reset() + { + cipherDelegate.reset(); + } +}
diff --git a/src/main/java/org/cryptacular/adapter/CipherAdapter.java b/src/main/java/org/cryptacular/adapter/CipherAdapter.java new file mode 100644 index 0000000..34a6217 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/CipherAdapter.java
@@ -0,0 +1,46 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import org.bouncycastle.crypto.CipherParameters; +import org.cryptacular.CryptoException; + +/** + * Provides a consistent interface for cipher operations against dissimilar BC cipher types. + * + * @author Middleware Services + */ +public interface CipherAdapter +{ + + /** + * Initialize the underlying cipher. + * + * @param forEncryption True for encryption mode, false for decryption mode. + * @param params Cipher initialization parameters. + * + * @throws CryptoException on underlying cipher initialization errors. + */ + void init(boolean forEncryption, CipherParameters params) throws CryptoException; + + + /** + * Process an array of bytes, producing output if necessary. + * + * @param in Input data. + * @param inOff Offset at which the input data starts. + * @param len The number of bytes in the input data to process. + * @param out Array to receive any data produced by cipher. + * @param outOff Offset into output array. + * + * @return The number of bytes produced by the cipher. + * + * @throws CryptoException on underlying cipher data handling errors. + */ + int processBytes(byte[] in, int inOff, int len, byte[] out, int outOff) throws CryptoException; + + + /** + * Reset the cipher. After resetting the cipher is in the same state as it was after the last init (if there was one). + */ + void reset(); +}
diff --git a/src/main/java/org/cryptacular/adapter/Converter.java b/src/main/java/org/cryptacular/adapter/Converter.java new file mode 100644 index 0000000..d096ddc --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/Converter.java
@@ -0,0 +1,78 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.security.PrivateKey; +import java.security.PublicKey; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.DSAPrivateKeyParameters; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; + +/** + * Static factory with methods to convert from BC type to the corresponding JCE type. + * + * @author Middleware Services + */ +public final class Converter +{ + + /** Private constructor of utility class. */ + private Converter() {} + + + /** + * Produces a {@link PrivateKey} from a BC private key type. + * + * @param bcKey BC private key. + * + * @return JCE private key. + */ + public static PrivateKey convertPrivateKey(final AsymmetricKeyParameter bcKey) + { + if (!bcKey.isPrivate()) { + throw new IllegalArgumentException("AsymmetricKeyParameter is not a private key: " + bcKey); + } + + final PrivateKey key; + if (bcKey instanceof DSAPrivateKeyParameters) { + key = new WrappedDSAPrivateKey((DSAPrivateKeyParameters) bcKey); + } else if (bcKey instanceof ECPrivateKeyParameters) { + key = new WrappedECPrivateKey((ECPrivateKeyParameters) bcKey); + } else if (bcKey instanceof RSAPrivateCrtKeyParameters) { + key = new WrappedRSAPrivateCrtKey((RSAPrivateCrtKeyParameters) bcKey); + } else { + throw new IllegalArgumentException("Unsupported private key " + bcKey); + } + return key; + } + + + /** + * Produces a {@link PublicKey} from a BC public key type. + * + * @param bcKey BC public key. + * + * @return JCE public key. + */ + public static PublicKey convertPublicKey(final AsymmetricKeyParameter bcKey) + { + if (bcKey.isPrivate()) { + throw new IllegalArgumentException("AsymmetricKeyParameter is not a public key: " + bcKey); + } + + final PublicKey key; + if (bcKey instanceof DSAPublicKeyParameters) { + key = new WrappedDSAPublicKey((DSAPublicKeyParameters) bcKey); + } else if (bcKey instanceof ECPublicKeyParameters) { + key = new WrappedECPublicKey((ECPublicKeyParameters) bcKey); + } else if (bcKey instanceof RSAKeyParameters) { + key = new WrappedRSAPublicKey((RSAKeyParameters) bcKey); + } else { + throw new IllegalArgumentException("Unsupported public key " + bcKey); + } + return key; + } +}
diff --git a/src/main/java/org/cryptacular/adapter/WrappedDSAPrivateKey.java b/src/main/java/org/cryptacular/adapter/WrappedDSAPrivateKey.java new file mode 100644 index 0000000..16b9950 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/WrappedDSAPrivateKey.java
@@ -0,0 +1,35 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.math.BigInteger; +import java.security.interfaces.DSAPrivateKey; +import org.bouncycastle.crypto.params.DSAPrivateKeyParameters; + +/** + * JCE/JDK DSA private key that wraps the corresponding BC DSA private key type, {@link DSAPrivateKeyParameters}. + * + * @author Middleware Services + */ +public class WrappedDSAPrivateKey extends AbstractWrappedDSAKey<DSAPrivateKeyParameters> implements DSAPrivateKey +{ + + /** serialVersionUID. */ + private static final long serialVersionUID = 8393283358287883368L; + + /** + * Creates a new instance that wraps the given BC DSA private key. + * + * @param parameters BC DSA private key. + */ + public WrappedDSAPrivateKey(final DSAPrivateKeyParameters parameters) + { + super(parameters); + } + + + @Override + public BigInteger getX() + { + return delegate.getX(); + } +}
diff --git a/src/main/java/org/cryptacular/adapter/WrappedDSAPublicKey.java b/src/main/java/org/cryptacular/adapter/WrappedDSAPublicKey.java new file mode 100644 index 0000000..dbf3522 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/WrappedDSAPublicKey.java
@@ -0,0 +1,36 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.math.BigInteger; +import java.security.interfaces.DSAPublicKey; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; + +/** + * JCE/JDK DSA public key that wraps the corresponding BC DSA public key type, {@link DSAPublicKeyParameters}. + * + * @author Middleware Services + */ +public class WrappedDSAPublicKey extends AbstractWrappedDSAKey<DSAPublicKeyParameters> implements DSAPublicKey +{ + + /** serialVersionUID. */ + private static final long serialVersionUID = -3349509056520420431L; + + /** + * Creates a new instance that wraps the given key. + * + * @param wrappedKey DSA key to wrap. + */ + public WrappedDSAPublicKey(final DSAPublicKeyParameters wrappedKey) + { + super(wrappedKey); + } + + + @Override + public BigInteger getY() + { + return delegate.getY(); + } + +}
diff --git a/src/main/java/org/cryptacular/adapter/WrappedECPrivateKey.java b/src/main/java/org/cryptacular/adapter/WrappedECPrivateKey.java new file mode 100644 index 0000000..e0ac1d1 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/WrappedECPrivateKey.java
@@ -0,0 +1,35 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.math.BigInteger; +import java.security.interfaces.ECPrivateKey; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; + +/** + * JCE/JDK EC private key that wraps the corresponding BC EC private key type, {@link ECPrivateKeyParameters}. + * + * @author Middleware Services + */ +public class WrappedECPrivateKey extends AbstractWrappedECKey<ECPrivateKeyParameters> implements ECPrivateKey +{ + + /** serialVersionUID. */ + private static final long serialVersionUID = -2383997830074646642L; + + /** + * Creates a new instance that wraps the given key. + * + * @param wrappedKey EC key to wrap. + */ + public WrappedECPrivateKey(final ECPrivateKeyParameters wrappedKey) + { + super(wrappedKey); + } + + + @Override + public BigInteger getS() + { + return delegate.getD(); + } +}
diff --git a/src/main/java/org/cryptacular/adapter/WrappedECPublicKey.java b/src/main/java/org/cryptacular/adapter/WrappedECPublicKey.java new file mode 100644 index 0000000..a49fa73 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/WrappedECPublicKey.java
@@ -0,0 +1,39 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECPoint; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; + +/** + * JCE/JDK EC public key that wraps the corresponding BC EC public key type, {@link ECPublicKeyParameters}. + * + * @author Middleware Services + */ +public class WrappedECPublicKey extends AbstractWrappedECKey<ECPublicKeyParameters> implements ECPublicKey +{ + + /** serialVersionUID. */ + private static final long serialVersionUID = -8218654577692012657L; + + /** + * Creates a new instance that wraps the given key. + * + * @param wrappedKey EC key to wrap. + */ + public WrappedECPublicKey(final ECPublicKeyParameters wrappedKey) + { + super(wrappedKey); + } + + + @Override + public ECPoint getW() + { + return + new ECPoint( + delegate.getQ().normalize().getXCoord().toBigInteger(), + delegate.getQ().normalize().getYCoord().toBigInteger()); + } + +}
diff --git a/src/main/java/org/cryptacular/adapter/WrappedRSAPrivateCrtKey.java b/src/main/java/org/cryptacular/adapter/WrappedRSAPrivateCrtKey.java new file mode 100644 index 0000000..4dc9274 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/WrappedRSAPrivateCrtKey.java
@@ -0,0 +1,80 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.math.BigInteger; +import java.security.interfaces.RSAPrivateCrtKey; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; + +/** + * JCE/JDK RSA private key that wraps the corresponding BC RSA private key type, {@link RSAPrivateCrtKeyParameters}. + * + * @author Middleware Services + */ +public class WrappedRSAPrivateCrtKey extends AbstractWrappedRSAKey<RSAPrivateCrtKeyParameters> + implements RSAPrivateCrtKey +{ + + /** serialVersionUID. */ + private static final long serialVersionUID = 99555083744578278L; + + /** + * Creates a new instance that wraps the given BC RSA private key. + * + * @param parameters BC RSA private (certificate) key. + */ + public WrappedRSAPrivateCrtKey(final RSAPrivateCrtKeyParameters parameters) + { + super(parameters); + } + + + @Override + public BigInteger getPublicExponent() + { + return delegate.getPublicExponent(); + } + + + @Override + public BigInteger getPrimeP() + { + return delegate.getP(); + } + + + @Override + public BigInteger getPrimeQ() + { + return delegate.getQ(); + } + + + @Override + public BigInteger getPrimeExponentP() + { + return delegate.getDP(); + } + + + @Override + public BigInteger getPrimeExponentQ() + { + return delegate.getDQ(); + } + + + @Override + public BigInteger getCrtCoefficient() + { + return delegate.getQInv(); + } + + + @Override + public BigInteger getPrivateExponent() + { + return delegate.getExponent(); + } + + +}
diff --git a/src/main/java/org/cryptacular/adapter/WrappedRSAPublicKey.java b/src/main/java/org/cryptacular/adapter/WrappedRSAPublicKey.java new file mode 100644 index 0000000..f9becd7 --- /dev/null +++ b/src/main/java/org/cryptacular/adapter/WrappedRSAPublicKey.java
@@ -0,0 +1,35 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.math.BigInteger; +import java.security.interfaces.RSAPublicKey; +import org.bouncycastle.crypto.params.RSAKeyParameters; + +/** + * JCE/JDK RSA public key that wraps the corresponding BC RSA public key type, {@link RSAKeyParameters}. + * + * @author Middleware Services + */ +public class WrappedRSAPublicKey extends AbstractWrappedRSAKey<RSAKeyParameters> implements RSAPublicKey +{ + + /** serialVersionUID. */ + private static final long serialVersionUID = -5733201361124222309L; + + /** + * Creates a new instance that wraps the given key. + * + * @param wrappedKey RSA key to wrap. + */ + public WrappedRSAPublicKey(final RSAKeyParameters wrappedKey) + { + super(wrappedKey); + } + + + @Override + public BigInteger getPublicExponent() + { + return delegate.getExponent(); + } +}
diff --git a/src/main/java/org/cryptacular/asn/ASN1Decoder.java b/src/main/java/org/cryptacular/asn/ASN1Decoder.java new file mode 100644 index 0000000..b2a8f2b --- /dev/null +++ b/src/main/java/org/cryptacular/asn/ASN1Decoder.java
@@ -0,0 +1,27 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.asn; + +import org.cryptacular.EncodingException; + +/** + * Strategy interface for converting encoded ASN.1 bytes to an object. + * + * @param <T> Type of object to produce on decode. + * + * @author Middleware Services + */ +public interface ASN1Decoder<T> +{ + + /** + * Produces an object from an encoded representation. + * + * @param encoded ASN.1 encoded data. + * @param args Additional data required to perform decoding. + * + * @return Decoded object. + * + * @throws EncodingException on encoding errors. + */ + T decode(byte[] encoded, Object... args) throws EncodingException; +}
diff --git a/src/main/java/org/cryptacular/asn/AbstractPrivateKeyDecoder.java b/src/main/java/org/cryptacular/asn/AbstractPrivateKeyDecoder.java new file mode 100644 index 0000000..0e4d376 --- /dev/null +++ b/src/main/java/org/cryptacular/asn/AbstractPrivateKeyDecoder.java
@@ -0,0 +1,73 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.asn; + +import org.cryptacular.EncodingException; +import org.cryptacular.util.PemUtil; + +/** + * Base class for all private key decoders. + * + * @param <T> Type produced by decode operation. + * + * @author Middleware Services + */ +public abstract class AbstractPrivateKeyDecoder<T> implements ASN1Decoder<T> +{ + + @Override + public T decode(final byte[] encoded, final Object... args) throws EncodingException + { + try { + final byte[] asn1Bytes; + if (args != null && args.length > 0 && args[0] instanceof char[]) { + asn1Bytes = decryptKey(encoded, (char[]) args[0]); + } else { + asn1Bytes = tryConvertPem(encoded); + } + return decodeASN1(asn1Bytes); + } catch (EncodingException e) { + throw e; + } catch (RuntimeException e) { + throw new EncodingException("Key encoding error", e); + } + } + + + /** + * Tests the given encoded input and converts it to PEM if it is detected, stripping out any header/footer data in the + * process. + * + * @param input Encoded data that may be PEM encoded. + * + * @return Decoded data if PEM encoding detected, otherwise original data. + */ + protected byte[] tryConvertPem(final byte[] input) + { + if (PemUtil.isPem(input)) { + return PemUtil.decode(input); + } + return input; + } + + + /** + * Decrypts an encrypted key in either PKCS#8 or OpenSSL "traditional" format. Both PEM and DER encodings are + * supported. + * + * @param encrypted Encoded encrypted key data. + * @param password Password to decrypt key. + * + * @return Decrypted key. + */ + protected abstract byte[] decryptKey(byte[] encrypted, char[] password); + + + /** + * Decodes the given raw ASN.1 encoded data into a private key of the type supported by this class. + * + * @param encoded Encoded ASN.1 data. + * + * @return Private key object. + */ + protected abstract T decodeASN1(byte[] encoded); +}
diff --git a/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java b/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java new file mode 100644 index 0000000..5934d85 --- /dev/null +++ b/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java
@@ -0,0 +1,135 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.asn; + +import java.math.BigInteger; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1TaggedObject; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.DSAParameters; +import org.bouncycastle.crypto.params.DSAPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import org.cryptacular.EncodingException; +import org.cryptacular.pbe.OpenSSLAlgorithm; +import org.cryptacular.pbe.OpenSSLEncryptionScheme; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.CodecUtil; +import org.cryptacular.util.PemUtil; + +/** + * Decrypts PEM-encoded OpenSSL "traditional" format private keys. + * + * @author Middleware Services + */ +public class OpenSSLPrivateKeyDecoder extends AbstractPrivateKeyDecoder<AsymmetricKeyParameter> +{ + + @Override + protected byte[] decryptKey(final byte[] encrypted, final char[] password) + { + final String pem = new String(encrypted, ByteUtil.ASCII_CHARSET); + final int start = pem.indexOf(PemUtil.DEK_INFO); + final int eol = pem.indexOf('\n', start); + final String[] dekInfo = pem.substring(start + 10, eol).split(","); + final String alg = dekInfo[0]; + final byte[] iv = CodecUtil.hex(dekInfo[1]); + final byte[] bytes = PemUtil.decode(encrypted); + return new OpenSSLEncryptionScheme(OpenSSLAlgorithm.fromAlgorithmId(alg), iv, password).decrypt(bytes); + } + + + @Override + protected AsymmetricKeyParameter decodeASN1(final byte[] encoded) + { + final ASN1InputStream stream = new ASN1InputStream(encoded); + final ASN1Object o; + try { + o = stream.readObject(); + } catch (Exception e) { + throw new EncodingException("Invalid encoded key", e); + } + + final AsymmetricKeyParameter key; + if (o instanceof ASN1ObjectIdentifier) { + // EC private key with named curve in the default OpenSSL format emitted + // by openssl ecparam -name xxxx -genkey + try { + key = parseECPrivateKey(ASN1Sequence.getInstance(stream.readObject())); + } catch (Exception e) { + throw new EncodingException("Invalid encoded key", e); + } + } else { + // OpenSSL "traditional" format is an ASN.1 sequence of key parameters + + // Detect key type based on number and types of parameters: + // RSA -> {version, mod, pubExp, privExp, prime1, prime2, exp1, exp2, c} + // DSA -> {version, p, q, g, pubExp, privExp} + // EC -> {version, privateKey, parameters, publicKey} + final ASN1Sequence sequence = ASN1Sequence.getInstance(o); + if (sequence.size() == 9) { + // RSA private certificate key + key = new RSAPrivateCrtKeyParameters( + ASN1Integer.getInstance(sequence.getObjectAt(1)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(2)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(3)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(4)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(5)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(6)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(7)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(8)).getValue()); + } else if (sequence.size() == 6) { + // DSA private key + key = new DSAPrivateKeyParameters( + ASN1Integer.getInstance(sequence.getObjectAt(5)).getValue(), + new DSAParameters( + ASN1Integer.getInstance(sequence.getObjectAt(1)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(2)).getValue(), + ASN1Integer.getInstance(sequence.getObjectAt(3)).getValue())); + } else if (sequence.size() == 4) { + // EC private key with explicit curve + key = parseECPrivateKey(sequence); + } else { + throw new EncodingException("Invalid OpenSSL traditional private key format."); + } + } + return key; + } + + + /** + * Parses an EC private key as defined in RFC 5915. + * <pre> + * ECPrivateKey ::= SEQUENCE { + * version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + * privateKey OCTET STRING, + * parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + * publicKey [1] BIT STRING OPTIONAL + * } + * </pre> + * + * @param seq ASN1 sequence to parse + * + * @return EC private key + */ + private ECPrivateKeyParameters parseECPrivateKey(final ASN1Sequence seq) + { + final ASN1TaggedObject asn1Params = ASN1TaggedObject.getInstance(seq.getObjectAt(2)); + final X9ECParameters params; + if (asn1Params.getBaseObject() instanceof ASN1ObjectIdentifier) { + params = ECUtil.getNamedCurveByOid(ASN1ObjectIdentifier.getInstance(asn1Params.getBaseObject())); + } else { + params = X9ECParameters.getInstance(asn1Params.getBaseObject()); + } + return new ECPrivateKeyParameters( + new BigInteger(1, ASN1OctetString.getInstance(seq.getObjectAt(1)).getOctets()), + new ECDomainParameters(params.getCurve(), params.getG(), params.getN(), params.getH(), params.getSeed())); + } +}
diff --git a/src/main/java/org/cryptacular/asn/PKCS8PrivateKeyDecoder.java b/src/main/java/org/cryptacular/asn/PKCS8PrivateKeyDecoder.java new file mode 100644 index 0000000..289fbdb --- /dev/null +++ b/src/main/java/org/cryptacular/asn/PKCS8PrivateKeyDecoder.java
@@ -0,0 +1,54 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.asn; + +import java.io.IOException; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo; +import org.bouncycastle.asn1.pkcs.PBEParameter; +import org.bouncycastle.asn1.pkcs.PBES2Parameters; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.cryptacular.EncodingException; +import org.cryptacular.pbe.EncryptionScheme; +import org.cryptacular.pbe.PBES1Algorithm; +import org.cryptacular.pbe.PBES1EncryptionScheme; +import org.cryptacular.pbe.PBES2EncryptionScheme; + +/** + * Decodes PEM or DER-encoded PKCS#8 private keys. + * + * @author Middleware Services + */ +public class PKCS8PrivateKeyDecoder extends AbstractPrivateKeyDecoder<AsymmetricKeyParameter> +{ + + @Override + protected byte[] decryptKey(final byte[] encrypted, final char[] password) + { + final EncryptionScheme scheme; + final EncryptedPrivateKeyInfo ki = EncryptedPrivateKeyInfo.getInstance(tryConvertPem(encrypted)); + final AlgorithmIdentifier alg = ki.getEncryptionAlgorithm(); + if (PKCSObjectIdentifiers.id_PBES2.equals(alg.getAlgorithm())) { + scheme = new PBES2EncryptionScheme(PBES2Parameters.getInstance(alg.getParameters()), password); + } else { + scheme = new PBES1EncryptionScheme( + PBES1Algorithm.fromOid(alg.getAlgorithm().getId()), + PBEParameter.getInstance(alg.getParameters()), + password); + } + return scheme.decrypt(ki.getEncryptedData()); + } + + + @Override + protected AsymmetricKeyParameter decodeASN1(final byte[] encoded) + { + try (ASN1InputStream is = new ASN1InputStream(encoded)) { + return PrivateKeyFactory.createKey(is.readObject().getEncoded()); + } catch (IOException e) { + throw new EncodingException("ASN.1 decoding error", e); + } + } +}
diff --git a/src/main/java/org/cryptacular/asn/PublicKeyDecoder.java b/src/main/java/org/cryptacular/asn/PublicKeyDecoder.java new file mode 100644 index 0000000..cdeb4d7 --- /dev/null +++ b/src/main/java/org/cryptacular/asn/PublicKeyDecoder.java
@@ -0,0 +1,33 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.asn; + +import java.io.IOException; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.cryptacular.EncodingException; +import org.cryptacular.util.PemUtil; + +/** + * Decodes public keys formatted in an X.509 SubjectPublicKeyInfo structure in either PEM or DER encoding. + * + * @author Middleware Services + */ +public class PublicKeyDecoder implements ASN1Decoder<AsymmetricKeyParameter> +{ + + @Override + public AsymmetricKeyParameter decode(final byte[] encoded, final Object... args) + { + try { + if (PemUtil.isPem(encoded)) { + return PublicKeyFactory.createKey(PemUtil.decode(encoded)); + } + try (ASN1InputStream is = new ASN1InputStream(encoded)) { + return PublicKeyFactory.createKey(is.readObject().getEncoded()); + } + } catch (IOException e) { + throw new EncodingException("ASN.1 decoding error", e); + } + } +}
diff --git a/src/main/java/org/cryptacular/bean/AEADBlockCipherBean.java b/src/main/java/org/cryptacular/bean/AEADBlockCipherBean.java new file mode 100644 index 0000000..542513b --- /dev/null +++ b/src/main/java/org/cryptacular/bean/AEADBlockCipherBean.java
@@ -0,0 +1,107 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyStore; +import javax.crypto.SecretKey; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.cryptacular.CiphertextHeader; +import org.cryptacular.adapter.AEADBlockCipherAdapter; +import org.cryptacular.generator.Nonce; +import org.cryptacular.spec.Spec; + +/** + * Cipher bean that performs encryption with a block cipher in AEAD mode (e.g. GCM, CCM). + * + * @author Middleware Services + */ +public class AEADBlockCipherBean extends AbstractBlockCipherBean +{ + + /** Mac size in bits. */ + public static final int MAC_SIZE_BITS = 128; + + /** AEAD block cipher specification (algorithm, mode, padding). */ + private Spec<AEADBlockCipher> blockCipherSpec; + + + /** Creates a new instance. */ + public AEADBlockCipherBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param blockCipherSpec Block cipher specification. + * @param keyStore Key store containing encryption key. + * @param keyAlias Name of encryption key entry in key store. + * @param keyPassword Password used to decrypt key entry in keystore. + * @param nonce Nonce/IV generator. + */ + public AEADBlockCipherBean( + final Spec<AEADBlockCipher> blockCipherSpec, + final KeyStore keyStore, + final String keyAlias, + final String keyPassword, + final Nonce nonce) + { + super(keyStore, keyAlias, keyPassword, nonce); + setBlockCipherSpec(blockCipherSpec); + } + + + /** @return Block cipher specification. */ + public Spec<AEADBlockCipher> getBlockCipherSpec() + { + return blockCipherSpec; + } + + + /** + * Sets the AEAD block cipher specification. + * + * @param blockCipherSpec Describes a block cipher in terms of algorithm, mode, and padding. + */ + public void setBlockCipherSpec(final Spec<AEADBlockCipher> blockCipherSpec) + { + this.blockCipherSpec = blockCipherSpec; + } + + + @Override + public void encrypt(final InputStream input, final OutputStream output) + { + if (blockCipherSpec.toString().endsWith("CCM")) { + throw new UnsupportedOperationException("CCM mode ciphers do not support chunked encryption."); + } + super.encrypt(input, output); + } + + + @Override + public void decrypt(final InputStream input, final OutputStream output) + { + if (blockCipherSpec.toString().endsWith("CCM")) { + throw new UnsupportedOperationException("CCM mode ciphers do not support chunked decryption."); + } + super.decrypt(input, output); + } + + + @Override + protected AEADBlockCipherAdapter newCipher(final CiphertextHeader header, final boolean mode) + { + final AEADBlockCipher cipher = blockCipherSpec.newInstance(); + final SecretKey key = lookupKey(header.getKeyName()); + final AEADParameters params = new AEADParameters( + new KeyParameter(key.getEncoded()), + MAC_SIZE_BITS, + header.getNonce(), + header.encode()); + cipher.init(mode, params); + return new AEADBlockCipherAdapter(cipher); + } +}
diff --git a/src/main/java/org/cryptacular/bean/AbstractBlockCipherBean.java b/src/main/java/org/cryptacular/bean/AbstractBlockCipherBean.java new file mode 100644 index 0000000..0d06b32 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/AbstractBlockCipherBean.java
@@ -0,0 +1,115 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyStore; +import org.cryptacular.CiphertextHeader; +import org.cryptacular.StreamException; +import org.cryptacular.adapter.BlockCipherAdapter; +import org.cryptacular.generator.Nonce; +import org.cryptacular.util.StreamUtil; + +/** + * Base class for all cipher beans that use block cipher. + * + * @author Middleware Services + */ +public abstract class AbstractBlockCipherBean extends AbstractCipherBean +{ + + /** Creates a new instance. */ + public AbstractBlockCipherBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param keyStore Key store containing encryption key. + * @param keyAlias Name of encryption key entry in key store. + * @param keyPassword Password used to decrypt key entry in keystore. + * @param nonce Nonce/IV generator. + */ + public AbstractBlockCipherBean( + final KeyStore keyStore, + final String keyAlias, + final String keyPassword, + final Nonce nonce) + { + super(keyStore, keyAlias, keyPassword, nonce); + } + + + @Override + protected byte[] process(final CiphertextHeader header, final boolean mode, final byte[] input) + { + final BlockCipherAdapter cipher = newCipher(header, mode); + int outOff; + final int inOff; + final int length; + final byte[] output; + if (mode) { + final byte[] headerBytes = header.encode(); + final int outSize = headerBytes.length + cipher.getOutputSize(input.length); + output = new byte[outSize]; + System.arraycopy(headerBytes, 0, output, 0, headerBytes.length); + inOff = 0; + outOff = headerBytes.length; + length = input.length; + } else { + outOff = 0; + inOff = header.getLength(); + length = input.length - inOff; + + final int outSize = cipher.getOutputSize(length); + output = new byte[outSize]; + } + outOff += cipher.processBytes(input, inOff, length, output, outOff); + outOff += cipher.doFinal(output, outOff); + if (outOff < output.length) { + final byte[] copy = new byte[outOff]; + System.arraycopy(output, 0, copy, 0, outOff); + return copy; + } + return output; + } + + + @Override + protected void process( + final CiphertextHeader header, + final boolean mode, + final InputStream input, + final OutputStream output) + { + final BlockCipherAdapter cipher = newCipher(header, mode); + final int outSize = cipher.getOutputSize(StreamUtil.CHUNK_SIZE); + final byte[] outBuf = new byte[Math.max(outSize, StreamUtil.CHUNK_SIZE)]; + StreamUtil.pipeAll( + input, + output, + (in, inOff, len, out) -> { + final int n = cipher.processBytes(in, inOff, len, outBuf, 0); + out.write(outBuf, 0, n); + }); + + final int n = cipher.doFinal(outBuf, 0); + try { + output.write(outBuf, 0, n); + } catch (IOException e) { + throw new StreamException(e); + } + } + + + /** + * Creates a new cipher adapter instance suitable for the block cipher used by this class. + * + * @param header Ciphertext header. + * @param mode True for encryption; false for decryption. + * + * @return Block cipher adapter that wraps an initialized block cipher that is ready for use in the given mode. + */ + protected abstract BlockCipherAdapter newCipher(CiphertextHeader header, boolean mode); +}
diff --git a/src/main/java/org/cryptacular/bean/AbstractCipherBean.java b/src/main/java/org/cryptacular/bean/AbstractCipherBean.java new file mode 100644 index 0000000..fd73763 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/AbstractCipherBean.java
@@ -0,0 +1,219 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.KeyStore; +import javax.crypto.SecretKey; +import org.cryptacular.CiphertextHeader; +import org.cryptacular.CiphertextHeaderV2; +import org.cryptacular.CryptoException; +import org.cryptacular.EncodingException; +import org.cryptacular.StreamException; +import org.cryptacular.generator.Nonce; +import org.cryptacular.util.CipherUtil; + +/** + * Base class for all cipher beans. The base class assumes all ciphertext output will contain a prepended {@link + * CiphertextHeaderV2} containing metadata that facilitates decryption. + * + * @author Middleware Services + */ +public abstract class AbstractCipherBean implements CipherBean +{ + + /** Keystore containing symmetric key(s). */ + private KeyStore keyStore; + + /** Keystore entry for alias of current key. */ + private String keyAlias; + + /** Password on private key entry. */ + private String keyPassword; + + /** Nonce generator. */ + private Nonce nonce; + + + /** Creates a new instance. */ + public AbstractCipherBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param keyStore Key store containing encryption key. + * @param keyAlias Name of encryption key entry in key store. + * @param keyPassword Password used to decrypt key entry in keystore. + * @param nonce Nonce/IV generator. + */ + public AbstractCipherBean(final KeyStore keyStore, final String keyAlias, final String keyPassword, final Nonce nonce) + { + setKeyStore(keyStore); + setKeyAlias(keyAlias); + setKeyPassword(keyPassword); + setNonce(nonce); + } + + + /** @return Keystore that contains the {@link SecretKey}. */ + public KeyStore getKeyStore() + { + return keyStore; + } + + + /** + * Sets the keystore containing encryption/decryption key(s). The keystore must contain a {@link SecretKey} entry + * whose alias is given by {@link #setKeyAlias(String)}, which will be used at the encryption key. It may contain + * additional symmetric keys to support, for example, key rollover where some existing ciphertexts have headers + * specifying a different key. In general all keys used for outstanding ciphertexts should be contained in the + * keystore. + * + * @param keyStore Keystore containing encryption key(s). + */ + public void setKeyStore(final KeyStore keyStore) + { + this.keyStore = keyStore; + } + + + /** @return Alias that specifies the {@link KeyStore} entry containing the {@link SecretKey}. */ + public String getKeyAlias() + { + return keyAlias; + } + + + /** + * Sets the keystore entry alias used to locate the current encryption key. + * + * @param keyAlias Alias of {@link SecretKey} used for encryption. + */ + public void setKeyAlias(final String keyAlias) + { + this.keyAlias = keyAlias; + } + + + /** + * Sets the password used to access the encryption key. + * + * @param keyPassword Encryption key password. + */ + public void setKeyPassword(final String keyPassword) + { + this.keyPassword = keyPassword; + } + + + /** @return Nonce/IV generation strategy. */ + public Nonce getNonce() + { + return nonce; + } + + + /** + * Sets the nonce/IV generation strategy. + * + * @param nonce Nonce generator. + */ + public void setNonce(final Nonce nonce) + { + this.nonce = nonce; + } + + + @Override + public byte[] encrypt(final byte[] input) throws CryptoException + { + return process(header(), true, input); + } + + + @Override + public void encrypt(final InputStream input, final OutputStream output) throws CryptoException, StreamException + { + final CiphertextHeaderV2 header = header(); + try { + output.write(header.encode()); + } catch (IOException e) { + throw new StreamException(e); + } + process(header, true, input, output); + } + + + @Override + public byte[] decrypt(final byte[] input) throws CryptoException, EncodingException + { + return process(CipherUtil.decodeHeader(input, this::lookupKey), false, input); + } + + + @Override + public void decrypt(final InputStream input, final OutputStream output) + throws CryptoException, EncodingException, StreamException + { + process(CipherUtil.decodeHeader(input, this::lookupKey), false, input, output); + } + + + /** + * Looks up secret key entry in the {@link #keyStore}. + * + * @param alias Name of secret key entry. + * + * @return Secret key. + */ + protected SecretKey lookupKey(final String alias) + { + final Key key; + try { + key = keyStore.getKey(alias, keyPassword.toCharArray()); + } catch (Exception e) { + throw new CryptoException("Error accessing keystore entry " + alias, e); + } + if (key instanceof SecretKey) { + return (SecretKey) key; + } + throw new CryptoException(alias + " is not a secret key"); + } + + + /** + * Processes the given data under the action of the cipher. + * + * @param header Ciphertext header. + * @param mode True for encryption; false for decryption. + * @param input Data to process by cipher. + * + * @return Ciphertext data under encryption, plaintext data under decryption. + */ + protected abstract byte[] process(CiphertextHeader header, boolean mode, byte[] input); + + + /** + * Processes the given data under the action of the cipher. + * + * @param header Ciphertext header. + * @param mode True for encryption; false for decryption. + * @param input Stream containing input data. + * @param output Stream that receives output of cipher. + */ + protected abstract void process(CiphertextHeader header, boolean mode, InputStream input, OutputStream output); + + + /** + * @return New ciphertext header for a pending encryption or decryption operation performed by this instance. + */ + private CiphertextHeaderV2 header() + { + final CiphertextHeaderV2 header = new CiphertextHeaderV2(nonce.generate(), keyAlias); + header.setKeyLookup(this::lookupKey); + return header; + } +}
diff --git a/src/main/java/org/cryptacular/bean/AbstractHashBean.java b/src/main/java/org/cryptacular/bean/AbstractHashBean.java new file mode 100644 index 0000000..23be306 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/AbstractHashBean.java
@@ -0,0 +1,106 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import org.bouncycastle.crypto.Digest; +import org.cryptacular.spec.Spec; +import org.cryptacular.util.HashUtil; + +/** + * Abstract base class for all hash beans. + * + * @author Middleware Services + */ +public abstract class AbstractHashBean +{ + + /** Digest specification. */ + private Spec<Digest> digestSpec; + + /** Number of hash rounds. */ + private int iterations = 1; + + + /** Creates a new instance. */ + public AbstractHashBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param digestSpec Digest specification. + * @param iterations Number of hash rounds. + */ + public AbstractHashBean(final Spec<Digest> digestSpec, final int iterations) + { + setDigestSpec(digestSpec); + setIterations(iterations); + } + + + /** @return Digest specification that determines the instance of {@link Digest} used to compute the hash. */ + public Spec<Digest> getDigestSpec() + { + return digestSpec; + } + + + /** + * Sets the digest specification that determines the instance of {@link Digest} used to compute the hash. + * + * @param digestSpec Digest algorithm specification. + */ + public void setDigestSpec(final Spec<Digest> digestSpec) + { + this.digestSpec = digestSpec; + } + + + /** @return Number of iterations the digest function is applied to the input data. */ + public int getIterations() + { + return iterations; + } + + + /** + * Sets the number of iterations the digest function is applied to the input data. + * + * @param iterations Number of hash rounds. Default value is 1. + */ + public void setIterations(final int iterations) + { + if (iterations < 1) { + throw new IllegalArgumentException("Iterations must be positive"); + } + this.iterations = iterations; + } + + + /** + * Hashes the given data. + * + * @param data Data to hash. + * + * @return Digest output. + */ + protected byte[] hashInternal(final Object... data) + { + return HashUtil.hash(digestSpec.newInstance(), iterations, data); + } + + + /** + * Compares the hash of the given data against a known hash output. + * + * @param hash Known hash value. If the length of the array is greater than the length of the digest output, + * anything beyond the digest length is considered salt data that is hashed <strong>after</strong> the + * input data. + * @param data Data to hash. + * + * @return True if hashed data equals known hash output, false otherwise. + */ + protected boolean compareInternal(final byte[] hash, final Object... data) + { + return HashUtil.compareHash(digestSpec.newInstance(), hash, iterations, data); + } +}
diff --git a/src/main/java/org/cryptacular/bean/BCryptHashBean.java b/src/main/java/org/cryptacular/bean/BCryptHashBean.java new file mode 100644 index 0000000..a77b144 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/BCryptHashBean.java
@@ -0,0 +1,330 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import org.bouncycastle.crypto.generators.BCrypt; +import org.cryptacular.CryptoException; +import org.cryptacular.StreamException; +import org.cryptacular.codec.Base64Decoder; +import org.cryptacular.codec.Base64Encoder; +import org.cryptacular.codec.Decoder; +import org.cryptacular.codec.Encoder; +import org.cryptacular.util.ByteUtil; + +/** + * {@link HashBean} implementation that uses the <em>bcrypt</em> algorithm for hashing. Hash strings of the following + * format are supported: + * <br> + * <code> + * $2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + * + * where: + * n is an optional bcrypt algorithm version (typically "a" or "b") + * 4 ≤ cost ≤ 31 + * x is 22 characters of encoded salt + * y is 31 characters of encoded hash bytes + * </code> + * <p> + * The encoding for salt and hash bytes is a variant of base-64 encoding without padding in the following alphabet: + * </p> + * <br> + * <code>./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789</code> + * + * @author Middleware Services + */ +public class BCryptHashBean implements HashBean<String> +{ + /** Custom base-64 alphabet. */ + private static final String ALPHABET = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + /** BCrypt cost factor in the range [4, 31]. Default value is {@value}. */ + private int cost = 12; + + /** BCrypt version used when computing hashes. Default value is {@value}. */ + private String version = "2b"; + + + /** Creates a new instance. */ + public BCryptHashBean() {} + + + /** + * Creates a new instance that uses the given cost factor when hashing. + * + * @param costFactor BCrypt cost in the range [4, 31]. + */ + public BCryptHashBean(final int costFactor) + { + setCost(costFactor); + } + + + /** + * Sets the bcrypt cost factor. + * + * @param costFactor BCrypt cost in the range [4, 31]. + */ + public void setCost(final int costFactor) + { + if (costFactor < 4 || costFactor > 31) { + throw new IllegalArgumentException("Cost must be in the range [4, 31]."); + } + cost = costFactor; + } + + + /** + * Sets the bcrypt version. + * + * @param ver Bcrypt version, e.g. "2b" + */ + public void setVersion(final String ver) + { + if (!ver.startsWith("2") && ver.length() <= 2) { + throw new IllegalArgumentException("Invalid version: " + ver); + } + version = ver; + } + + /** + * Compute a bcrypt hash of the form <code>$2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</code> + * given a salt and a password. + * @param data A 2-element array containing salt and password. The salt may be encoded per the bcrypt standard + * or raw bytes. + * + * @return An encoded bcrypt hash, <code>yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</code> in the specification above. + * + * @throws CryptoException on bcrypt algorithm errors. + */ + @Override + public String hash(final Object... data) throws CryptoException + { + if (data.length != 2) { + throw new IllegalArgumentException("Expected exactly two elements in data array but got " + data.length); + } + return encode(BCrypt.generate(password(version, data[1]), salt(data[0]), cost), 23); + } + + + /** + * Compares a bcrypt hash of the form <code>$2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</code> + * with the computed hash from the given password. The bcrypt algorithm parameters are derived from the reference + * bcrypt hash string. + * + * @param data A 1-element array containing password. + * + * @return True if the computed hash is exactly equal to the reference hash, false otherwise. + * + * @throws CryptoException on bcrypt algorithm errors. + */ + @Override + public boolean compare(final String hash, final Object... data) throws CryptoException, StreamException + { + if (data.length != 1) { + throw new IllegalArgumentException("Expected exactly one element in data array but got " + data.length); + } + final BCryptParameters params = new BCryptParameters(hash); + final byte[] computed = BCrypt.generate(password(params.getVersion(), data[0]), params.getSalt(), params.getCost()); + for (int i = 0; i < 23; i++) { + if (params.getHash()[i] != computed[i]) { + return false; + } + } + return true; + } + + + /** + * Encodes an input byte array into a string using the configured encoder. + * + * @param bytes Input bytes to encode. + * @param length Number of bytes of input to encode. + * + * @return Input encoded as a string. + */ + private static String encode(final byte[] bytes, final int length) + { + final Encoder encoder = new Base64Encoder.Builder().setAlphabet(ALPHABET).setPadding(false).build(); + // Only want 184 bits (23 bytes) of the output + final ByteBuffer input = ByteBuffer.wrap(bytes, 0, length); + final CharBuffer output = CharBuffer.allocate(encoder.outputSize(length)); + encoder.encode(input, output); + encoder.finalize(output); + return output.flip().toString(); + } + + + /** + * Decodes an input string into a byte array using the configured decoder. + * + * @param input Input string to decode. + * @param length Desired output size in bytes. + * + * @return Input decoded as a byte array. + */ + private static byte[] decode(final String input, final int length) + { + final Decoder decoder = new Base64Decoder.Builder().setAlphabet(ALPHABET).setPadding(false).build(); + final ByteBuffer output = ByteBuffer.allocate(decoder.outputSize(input.length())); + decoder.decode(CharBuffer.wrap(input), output); + decoder.finalize(output); + output.flip(); + if (output.limit() != length) { + throw new IllegalArgumentException("Input is not of the expected size: " + output.limit() + "!=" + length); + } + return ByteUtil.toArray(output); + } + + + /** + * Converts an input object into a salt as an array of bytes. + * + * @param data Input salt as a byte array or encoded string. + * + * @return Salt as byte array. + */ + private static byte[] salt(final Object data) + { + if (data instanceof byte[]) { + return (byte[]) data; + } else if (data instanceof String) { + return decode((String) data, 16); + } + throw new IllegalArgumentException("Expected byte array or base-64 string."); + } + + + /** + * Converts an input object into a password as an array of UTF-8 bytes. A null terminator is added if the supplied + * data does not end with one. + * + * @param version Bcrypt version, e.g. "2a". + * @param data Input password. + * + * @return Null terminated password as UTF-8 byte array. + */ + private static byte[] password(final String version, final Object data) + { + if (data instanceof byte[]) { + final byte[] origData = (byte[]) data; + final byte[] newData; + if (origData[origData.length - 1] != 0x00) { + newData = new byte[origData.length + 1]; + System.arraycopy(origData, 0, newData, 0, origData.length); + newData[newData.length - 1] = 0x00; + } else { + newData = origData; + } + return newData; + } + final StringBuilder sb = new StringBuilder(); + if (data instanceof char[]) { + sb.append((char[]) data); + } else if (data instanceof String) { + sb.append((String) data); + } else { + throw new IllegalArgumentException("Expected byte array or string."); + } + if (sb.charAt(sb.length() - 1) != '\0') { + // Version 2a and later requires null terminator on password + sb.append('\0'); + } + return sb.toString().getBytes(StandardCharsets.UTF_8); + } + + + /** + * Handles encoding and decoding a bcrypt hash of the form + * <code>$2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</code>. + */ + public static class BCryptParameters + { + /** bcrypt version. */ + private final String version; + + /** bcrypt cost. */ + private final int cost; + + /** bcrypt salt. */ + private final byte[] salt; + + /** bcrypt hash. */ + private final byte[] hash; + + + /** + * Decodes bcrypt parameters from a string. + * + * @param bCryptString bcrypt hash of the form + * <code>$2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</code> + */ + protected BCryptParameters(final String bCryptString) + { + if (!bCryptString.startsWith("$2")) { + throw new IllegalArgumentException("Expected bcrypt hash of the form $2n$cost$salthash"); + } + final String[] parts = bCryptString.split("\\$"); + if (parts.length != 4) { + throw new IllegalArgumentException("Invalid bcrypt hash"); + } + version = parts[1]; + cost = Integer.parseInt(parts[2]); + salt = decode(parts[3].substring(0, 22), 16); + hash = decode(parts[3].substring(22), 23); + } + + + /** @return bcrypt version. */ + public String getVersion() + { + return version; + } + + + /** @return bcrypt cost in the range [4, 31]. */ + public int getCost() + { + return cost; + } + + + /** @return bcrypt salt. */ + public byte[] getSalt() + { + return salt; + } + + + /** @return bcrypt hash. */ + public byte[] getHash() + { + return hash; + } + + + /** + * Produces an encoded bcrypt hash string from bcrypt parameter data. + * + * @return Bcrypt hash of the form <code>$2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</code>. + */ + public String encode() + { + return '$' + version + '$' + cost + '$' + BCryptHashBean.encode(salt, 16) + BCryptHashBean.encode(hash, 23); + } + + + /** + * Produces an encoded bcrypt hash string from bcrypt parameters and a provided hash string. + * + * @param hash Encoded bcrypt hash bytes; e.g. the value produced from {@link #hash(Object...)}. + * + * @return Bcrypt hash of the form <code>$2n$cost$xxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</code>. + */ + public String encode(final String hash) + { + return '$' + version + '$' + cost + '$' + BCryptHashBean.encode(salt, 16) + hash; + } + } +}
diff --git a/src/main/java/org/cryptacular/bean/BufferedBlockCipherBean.java b/src/main/java/org/cryptacular/bean/BufferedBlockCipherBean.java new file mode 100644 index 0000000..55d2f18 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/BufferedBlockCipherBean.java
@@ -0,0 +1,82 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.security.KeyStore; +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.cryptacular.CiphertextHeader; +import org.cryptacular.adapter.BufferedBlockCipherAdapter; +import org.cryptacular.generator.Nonce; +import org.cryptacular.spec.Spec; + +/** + * Cipher bean that performs symmetric encryption/decryption using a standard block cipher in a standard mode (e.g. CBC, + * OFB) with padding to support processing inputs of arbitrary length. + * + * @author Middleware Services + */ +public class BufferedBlockCipherBean extends AbstractBlockCipherBean +{ + + /** Block cipher specification (algorithm, mode, padding). */ + private Spec<BufferedBlockCipher> blockCipherSpec; + + + /** Creates a new instance. */ + public BufferedBlockCipherBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param blockCipherSpec Block cipher specification. + * @param keyStore Key store containing encryption key. + * @param keyAlias Name of encryption key entry in key store. + * @param keyPassword Password used to decrypt key entry in keystore. + * @param nonce Nonce/IV generator. + */ + public BufferedBlockCipherBean( + final Spec<BufferedBlockCipher> blockCipherSpec, + final KeyStore keyStore, + final String keyAlias, + final String keyPassword, + final Nonce nonce) + { + super(keyStore, keyAlias, keyPassword, nonce); + setBlockCipherSpec(blockCipherSpec); + } + + + /** @return Block cipher specification. */ + public Spec<BufferedBlockCipher> getBlockCipherSpec() + { + return blockCipherSpec; + } + + + /** + * Sets the block cipher specification. + * + * @param blockCipherSpec Describes a block cipher in terms of algorithm, mode, and padding. + */ + public void setBlockCipherSpec(final Spec<BufferedBlockCipher> blockCipherSpec) + { + this.blockCipherSpec = blockCipherSpec; + } + + + @Override + protected BufferedBlockCipherAdapter newCipher(final CiphertextHeader header, final boolean mode) + { + final BufferedBlockCipher cipher = blockCipherSpec.newInstance(); + CipherParameters params = new KeyParameter(lookupKey(header.getKeyName()).getEncoded()); + final String algName = cipher.getUnderlyingCipher().getAlgorithmName(); + if (algName.endsWith("CBC") || algName.endsWith("OFB") || algName.endsWith("CFB")) { + params = new ParametersWithIV(params, header.getNonce()); + } + cipher.init(mode, params); + return new BufferedBlockCipherAdapter(cipher); + } +}
diff --git a/src/main/java/org/cryptacular/bean/CipherBean.java b/src/main/java/org/cryptacular/bean/CipherBean.java new file mode 100644 index 0000000..9429a3f --- /dev/null +++ b/src/main/java/org/cryptacular/bean/CipherBean.java
@@ -0,0 +1,67 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.InputStream; +import java.io.OutputStream; +import org.cryptacular.CryptoException; +import org.cryptacular.StreamException; + +/** + * Bean that performs encryption/decryption using a symmetric cipher. + * + * @author Middleware Services + */ +public interface CipherBean +{ + + /** + * Encrypts the input data using a symmetric cipher. + * + * @param input Plaintext data to encrypt. + * + * @return Ciphertext output. + * + * @throws CryptoException on underlying cipher data handling errors. + */ + byte[] encrypt(byte[] input) throws CryptoException; + + + /** + * Encrypts the data from the input stream onto the output stream using a symmetric cipher. + * + * <p>The caller is responsible for providing and managing the streams (e.g. closing them when finished).</p> + * + * @param input Input stream containing plaintext data to encrypt. + * @param output Output stream containing ciphertext produced by cipher in encryption mode. + * + * @throws CryptoException on underlying cipher data handling errors. + * @throws StreamException on stream IO errors. + */ + void encrypt(InputStream input, OutputStream output) throws CryptoException, StreamException; + + + /** + * Decrypts the input data using a block cipher. + * + * @param input Ciphertext data to encrypt. + * + * @return Plaintext output. + * + * @throws CryptoException on underlying cipher data handling errors. + */ + byte[] decrypt(byte[] input) throws CryptoException; + + + /** + * Decrypts the data from the input stream onto the output stream using a symmetric cipher. + * + * <p>The caller is responsible for providing and managing the streams (e.g. closing them when finished).</p> + * + * @param input Input stream containing ciphertext data to decrypt. + * @param output Output stream containing plaintext produced by cipher in decryption mode. + * + * @throws CryptoException on underlying cipher data handling errors. + * @throws StreamException on stream IO errors. + */ + void decrypt(InputStream input, OutputStream output) throws CryptoException, StreamException; +}
diff --git a/src/main/java/org/cryptacular/bean/EncodingHashBean.java b/src/main/java/org/cryptacular/bean/EncodingHashBean.java new file mode 100644 index 0000000..c1f9a65 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/EncodingHashBean.java
@@ -0,0 +1,165 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.util.Arrays; +import org.cryptacular.CryptoException; +import org.cryptacular.EncodingException; +import org.cryptacular.StreamException; +import org.cryptacular.codec.Codec; +import org.cryptacular.spec.Spec; +import org.cryptacular.util.CodecUtil; + +/** + * Computes a hash in an encoded format, e.g. hex, base64. + * + * @author Middleware Services + */ +public class EncodingHashBean extends AbstractHashBean implements HashBean<String> +{ + + /** Determines kind of encoding. */ + private Spec<Codec> codecSpec; + + /** Whether data provided to this bean includes a salt. */ + private boolean salted; + + + /** Creates a new instance. */ + public EncodingHashBean() {} + + + /** + * Creates a new instance that will not be salted. Delegates to {@link #EncodingHashBean(Spec, Spec, int, boolean)}. + * + * @param codecSpec Digest specification. + * @param digestSpec Digest specification. + */ + public EncodingHashBean(final Spec<Codec> codecSpec, final Spec<Digest> digestSpec) + { + this(codecSpec, digestSpec, 1, false); + } + + + /** + * Creates a new instance that will not be salted. Delegates to {@link #EncodingHashBean(Spec, Spec, int, boolean)}. + * + * @param codecSpec Digest specification. + * @param digestSpec Digest specification. + * @param iterations Number of hash rounds. + */ + public EncodingHashBean(final Spec<Codec> codecSpec, final Spec<Digest> digestSpec, final int iterations) + { + this(codecSpec, digestSpec, iterations, false); + } + + + /** + * Creates a new instance by specifying all properties. + * + * @param codecSpec Digest specification. + * @param digestSpec Digest specification. + * @param iterations Number of hash rounds. + * @param salted Whether hash data will be salted. + */ + public EncodingHashBean( + final Spec<Codec> codecSpec, + final Spec<Digest> digestSpec, + final int iterations, + final boolean salted) + { + super(digestSpec, iterations); + setCodecSpec(codecSpec); + setSalted(salted); + } + + + /** @return Codec specification that determines the encoding applied to the hash output bytes. */ + public Spec<Codec> getCodecSpec() + { + return codecSpec; + } + + + /** + * Sets the codec specification that determines the encoding applied to the hash output bytes. + * + * @param codecSpec Codec specification, e.g. {@link org.cryptacular.spec.CodecSpec#BASE64}, {@link + * org.cryptacular.spec.CodecSpec#HEX}. + */ + public void setCodecSpec(final Spec<Codec> codecSpec) + { + this.codecSpec = codecSpec; + } + + + /** + * Whether data provided to {@link #hash(Object...)} includes a salt as the last parameter. + * + * @return whether hash data includes a salt + */ + public boolean isSalted() + { + return salted; + } + + + /** + * Sets whether {@link #hash(Object...)} should expect a salt as the last parameter. + * + * @param salted whether hash data includes a salt + */ + public void setSalted(final boolean salted) + { + this.salted = salted; + } + + + /** + * Hashes the given data. If {@link #isSalted()} is true then the last parameter MUST be a byte array containing the + * salt. The salt value will be appended to the encoded hash that is returned. + * + * @param data Data to hash. + * + * @return Encoded digest output, including a salt if provided. + * + * @throws CryptoException on hash computation errors. + * @throws EncodingException on encoding errors. + * @throws StreamException on stream IO errors. + */ + @Override + public String hash(final Object... data) throws CryptoException, EncodingException, StreamException + { + if (salted) { + if (data.length < 2 || !(data[data.length - 1] instanceof byte[])) { + throw new IllegalArgumentException("Last parameter must be a salt of type byte[]"); + } + + final byte[] hashSalt = (byte[]) data[data.length - 1]; + return CodecUtil.encode(codecSpec.newInstance().getEncoder(), Arrays.concatenate(hashInternal(data), hashSalt)); + } + return CodecUtil.encode(codecSpec.newInstance().getEncoder(), hashInternal(data)); + } + + + /** + * Compares a known hash value with the hash of the given data. + * + * @param hash Known encoded hash value. If the length of the hash bytes after decoding is greater than the length + * of the digest output, anything beyond the digest length is considered salt data that is hashed + * <strong>after</strong> the input data. + * @param data Data to hash. + * + * @return True if the hashed data matches the given hash, false otherwise. + * + * @throws CryptoException on hash computation errors. + * @throws EncodingException on encoding errors. + * @throws StreamException on stream IO errors. + */ + @Override + public boolean compare(final String hash, final Object... data) + throws CryptoException, EncodingException, StreamException + { + return compareInternal(CodecUtil.decode(codecSpec.newInstance().getDecoder(), hash), data); + } +}
diff --git a/src/main/java/org/cryptacular/bean/FactoryBean.java b/src/main/java/org/cryptacular/bean/FactoryBean.java new file mode 100644 index 0000000..ff2fee8 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/FactoryBean.java
@@ -0,0 +1,16 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +/** + * Factory bean strategy interface. + * + * @param <T> Type produced by factory. + * + * @author Middleware Services + */ +public interface FactoryBean<T> +{ + + /** @return New instance of the type handled by this factory. */ + T newInstance(); +}
diff --git a/src/main/java/org/cryptacular/bean/HashBean.java b/src/main/java/org/cryptacular/bean/HashBean.java new file mode 100644 index 0000000..b851cc4 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/HashBean.java
@@ -0,0 +1,46 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import org.cryptacular.CryptoException; +import org.cryptacular.StreamException; + +/** + * Strategy interface to support beans that produce hash outputs in various formats, e.g. raw bytes, hex output, etc. + * + * @param <T> Type of output (e.g. byte[], string) produced by hash bean. + * + * @author Middleware Services + */ +public interface HashBean<T> +{ + + /** + * Hashes the given data. + * + * @param data Data to hash. Callers should expect support for at least the following types: <code>byte[]</code>, + * {@link CharSequence}, {@link java.io.InputStream}, and {@link org.cryptacular.io.Resource}. Unless + * otherwise noted, character data is processed in the <code>UTF-8</code> character set; if another + * character set is desired, the caller should convert to <code>byte[]</code> and provide the resulting + * bytes. + * + * @return Digest output. + * + * @throws CryptoException on hash computation errors. + * @throws StreamException on stream IO errors. + */ + T hash(Object... data) throws CryptoException, StreamException; + + + /** + * Compares a known hash value with the hash of the given data. + * + * @param hash Known hash value. + * @param data Data to hash. + * + * @return True if the hashed data matches the given hash, false otherwise. + * + * @throws CryptoException on hash computation errors. + * @throws StreamException on stream IO errors. + */ + boolean compare(T hash, Object... data) throws CryptoException, StreamException; +}
diff --git a/src/main/java/org/cryptacular/bean/KeyStoreBasedKeyFactoryBean.java b/src/main/java/org/cryptacular/bean/KeyStoreBasedKeyFactoryBean.java new file mode 100644 index 0000000..73fa3e6 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/KeyStoreBasedKeyFactoryBean.java
@@ -0,0 +1,111 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import org.cryptacular.CryptoException; + +/** + * Factory that produces either a {@link javax.crypto.SecretKey} or {@link java.security.PrivateKey}. + * + * <p>from a {@link KeyStore}.</p> + * + * @param <T> Type of key, either {@link javax.crypto.SecretKey} or {@link java.security.PrivateKey}. + * + * @author Middleware Services + */ +public class KeyStoreBasedKeyFactoryBean<T extends Key> implements FactoryBean<T> +{ + + /** Keystore containing secret key. */ + private KeyStore keyStore; + + /** Alias of keystore entry containing secret key. */ + private String alias; + + /** Password required to read key entry. */ + private String password; + + + /** Creates a new instance. */ + public KeyStoreBasedKeyFactoryBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param keyStore Key store containing encryption key. + * @param alias Name of encryption key entry in key store. + * @param password Password used to decrypt key entry in keystore. + */ + public KeyStoreBasedKeyFactoryBean(final KeyStore keyStore, final String alias, final String password) + { + setKeyStore(keyStore); + setAlias(alias); + setPassword(password); + } + + + /** @return Keystore that contains the {@link #keyStore}. */ + public KeyStore getKeyStore() + { + return keyStore; + } + + + /** + * Sets the keystore that contains the key. + * + * @param keyStore Non-null keystore. + */ + public void setKeyStore(final KeyStore keyStore) + { + this.keyStore = keyStore; + } + + + /** @return Alias that specifies the {@link KeyStore} entry containing the key. */ + public String getAlias() + { + return alias; + } + + + /** + * Sets the alias that specifies the {@link KeyStore} entry containing the key. + * + * @param alias Keystore alias of key entry. + */ + public void setAlias(final String alias) + { + this.alias = alias; + } + + + /** + * Sets the password used to access the key entry. + * + * @param password Key entry password. + */ + public void setPassword(final String password) + { + this.password = password; + } + + + @Override + @SuppressWarnings("unchecked") + public T newInstance() + { + final Key key; + try { + key = keyStore.getKey(alias, password.toCharArray()); + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) { + throw new CryptoException("Error accessing keystore entry " + alias, e); + } + return (T) key; + } +}
diff --git a/src/main/java/org/cryptacular/bean/KeyStoreFactoryBean.java b/src/main/java/org/cryptacular/bean/KeyStoreFactoryBean.java new file mode 100644 index 0000000..d692f38 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/KeyStoreFactoryBean.java
@@ -0,0 +1,127 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import org.cryptacular.CryptoException; +import org.cryptacular.StreamException; +import org.cryptacular.io.Resource; + +/** + * Factory bean that produces a {@link KeyStore} from a file or URI. + * + * @author Middleware Services + */ +public class KeyStoreFactoryBean implements FactoryBean<KeyStore> +{ + + /** Default keystore type, {@value}. */ + public static final String DEFAULT_TYPE = "JCEKS"; + + /** Keystore type, e.g. JKS, JCEKS, BKS. */ + private String type = DEFAULT_TYPE; + + /** Resource that provides encoded keystore data. */ + private Resource resource; + + /** Keystore password. */ + private String password; + + + /** Creates a new instance. */ + public KeyStoreFactoryBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param resource Resource containing encoded keystore data. + * @param type Keystore type, e.g. JCEKS. + * @param password Password used to decrypt key entry in keystore. + */ + public KeyStoreFactoryBean(final Resource resource, final String type, final String password) + { + setResource(resource); + setType(type); + setPassword(password); + } + + + /** @return Keystore type. */ + public String getType() + { + return type; + } + + + /** + * Sets the keystore type. + * + * @param type JCEKS (default), JKS, PKCS12, or BKS. <strong>NOTE:</strong> BKS type is supported only when BC + * provider is installed. + */ + public void setType(final String type) + { + this.type = type; + } + + + /** @return Resource that provides encoded keystore data. */ + public Resource getResource() + { + return resource; + } + + + /** + * Sets the resource that provides encoded keystore data. + * + * @param resource Keystore resource. + */ + public void setResource(final Resource resource) + { + this.resource = resource; + } + + + /** + * Sets the keystore password required to decrypt an encrypted keystore. + * + * @param password Keystore password. + */ + public void setPassword(final String password) + { + this.password = password; + } + + + @Override + public KeyStore newInstance() + { + if (resource == null) { + throw new IllegalStateException("Must provide resource."); + } + + final KeyStore store; + try { + store = KeyStore.getInstance(type); + } catch (KeyStoreException e) { + String message = "Unsupported keystore type " + type; + if ("BKS".equalsIgnoreCase(type)) { + message += ". Is BC provider installed?"; + } + throw new CryptoException(message, e); + } + try { + store.load(resource.getInputStream(), password.toCharArray()); + } catch (CertificateException | NoSuchAlgorithmException e) { + throw new CryptoException("Error loading keystore", e); + } catch (IOException e) { + throw new StreamException(e); + } + return store; + } +}
diff --git a/src/main/java/org/cryptacular/bean/PemBasedPrivateKeyFactoryBean.java b/src/main/java/org/cryptacular/bean/PemBasedPrivateKeyFactoryBean.java new file mode 100644 index 0000000..05a4123 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/PemBasedPrivateKeyFactoryBean.java
@@ -0,0 +1,67 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.security.PrivateKey; +import org.cryptacular.EncodingException; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.KeyPairUtil; +import org.cryptacular.util.PemUtil; + +/** + * Factory for creating a public key from a PEM-encoded private key in any format supported by {@link + * KeyPairUtil#decodePrivateKey(byte[])}. Note that this component does not support encrypted private keys; see {@link + * ResourceBasedPrivateKeyFactoryBean} for encryption support. + * + * @author Middleware Services + * @see org.cryptacular.util.KeyPairUtil#decodePrivateKey(byte[]) + * @see ResourceBasedPrivateKeyFactoryBean + */ +public class PemBasedPrivateKeyFactoryBean implements FactoryBean<PrivateKey> +{ + + /** PEM-encoded public key data. */ + private String encodedKey; + + + /** Creates a new instance. */ + public PemBasedPrivateKeyFactoryBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param pemEncodedKey PEM-encoded private key data. + */ + public PemBasedPrivateKeyFactoryBean(final String pemEncodedKey) + { + setEncodedKey(pemEncodedKey); + } + + + /** @return PEM-encoded private key data. */ + public String getEncodedKey() + { + return encodedKey; + } + + + /** + * Sets the PEM-encoded private key data. + * + * @param pemEncodedKey PEM-encoded private key data. + */ + public void setEncodedKey(final String pemEncodedKey) + { + if (!PemUtil.isPem(ByteUtil.toBytes(pemEncodedKey))) { + throw new IllegalArgumentException("Data is not PEM encoded."); + } + this.encodedKey = pemEncodedKey; + } + + + @Override + public PrivateKey newInstance() throws EncodingException + { + return KeyPairUtil.decodePrivateKey(PemUtil.decode(encodedKey)); + } +}
diff --git a/src/main/java/org/cryptacular/bean/PemBasedPublicKeyFactoryBean.java b/src/main/java/org/cryptacular/bean/PemBasedPublicKeyFactoryBean.java new file mode 100644 index 0000000..31c305b --- /dev/null +++ b/src/main/java/org/cryptacular/bean/PemBasedPublicKeyFactoryBean.java
@@ -0,0 +1,77 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.security.PublicKey; +import org.cryptacular.EncodingException; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.KeyPairUtil; +import org.cryptacular.util.PemUtil; + +/** + * Factory for creating a public key from a PEM-encoded string: + * + * <pre>-----BEGIN PUBLIC KEY----- + MIIBtzCCASsGByqGSM44BAEwggEeAoGBAOulifG+AGGBVGWEjunG4661rydB7eFy + RfHzbwVAVaPU0H3zFcOY35z1l6Pk4ZANVHq7hCbViJBR7XyrkYKaUcaB0nSPLgg3 + vWWOmvGqhuR6tWRGbz4fyHl1urCRk9mrJum4mAJd3OlLugCyuIqozsYUtvJ5mlGe + vir1zmxinKd7AhUA7fBEySYP53g7FLOlcEyuhIjvQAECgYBJ9baoGzn0zKpeteC4 + jfbGVuKrFksr2eeY0AFJOeTtyFkCnVqrNnF674eN1RAOwA2tzzhWZ96G0AGux8ah + mGsNRbj/qaUTDNRWr7BPBIvDd+8LpMin4Cb5j4c/A7uOY+5WxhUm3TNifueBRohw + h1NnexYQqpclcuTRA/ougLX48gOBhQACgYEA6Tw2khtb1g0vcHu6JRgggWPZVTuj + /HOH3FyjufsfHogWKrlKebZ6hnQ73qAcEgLLYKctPdCX6wnpXN+BsQGYdTkc0FsU + NZD4VW5L5kaWRiLVfE8x55wXdMZtXKWqg1vL6aXYZw7RFe9U9Ck+/AG90knThDC+ + xrX2FTDm6uC25rk= + -----END PUBLIC KEY-----</pre> + * + * @author Middleware Services + * @see KeyPairUtil#decodePublicKey(byte[]) + */ +public class PemBasedPublicKeyFactoryBean implements FactoryBean<PublicKey> +{ + + /** PEM-encoded public key data. */ + private String encodedKey; + + + /** Creates a new instance. */ + public PemBasedPublicKeyFactoryBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param pemEncodedKey PEM-encoded public key data. + */ + public PemBasedPublicKeyFactoryBean(final String pemEncodedKey) + { + setEncodedKey(pemEncodedKey); + } + + + /** @return PEM-encoded public key data. */ + public String getEncodedKey() + { + return encodedKey; + } + + + /** + * Sets the PEM-encoded public key data. + * + * @param pemEncodedKey PEM-encoded public key data. + */ + public void setEncodedKey(final String pemEncodedKey) + { + if (!PemUtil.isPem(ByteUtil.toBytes(pemEncodedKey))) { + throw new IllegalArgumentException("Data is not PEM encoded."); + } + this.encodedKey = pemEncodedKey; + } + + + @Override + public PublicKey newInstance() throws EncodingException + { + return KeyPairUtil.decodePublicKey(PemUtil.decode(encodedKey)); + } +}
diff --git a/src/main/java/org/cryptacular/bean/ResourceBasedPrivateKeyFactoryBean.java b/src/main/java/org/cryptacular/bean/ResourceBasedPrivateKeyFactoryBean.java new file mode 100644 index 0000000..ad8e821 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/ResourceBasedPrivateKeyFactoryBean.java
@@ -0,0 +1,98 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.IOException; +import java.security.PrivateKey; +import org.cryptacular.EncodingException; +import org.cryptacular.StreamException; +import org.cryptacular.io.Resource; +import org.cryptacular.util.KeyPairUtil; + +/** + * Factory for reading a private from a {@link Resource} containing data in any of the formats supported by {@link + * KeyPairUtil#readPrivateKey(java.io.InputStream, char[])}. + * + * @author Middleware Services + * @see KeyPairUtil#readPrivateKey(java.io.InputStream, char[]) + * @see KeyPairUtil#readPrivateKey(java.io.InputStream) + */ +public class ResourceBasedPrivateKeyFactoryBean implements FactoryBean<PrivateKey> +{ + + /** Resource containing key data. */ + private Resource resource; + + /** Password required to decrypt an encrypted private key. */ + private String password; + + + /** Creates a new instance. */ + public ResourceBasedPrivateKeyFactoryBean() {} + + + /** + * Creates a new instance capable of reading an unencrypted private key. + * + * @param resource Resource containing encoded key data. + */ + public ResourceBasedPrivateKeyFactoryBean(final Resource resource) + { + setResource(resource); + } + + + /** + * Creates a new instance of reading an encrypted private key. + * + * @param resource Resource containing encoded key data. + * @param decryptionPassword Password-based encryption key. + */ + public ResourceBasedPrivateKeyFactoryBean(final Resource resource, final String decryptionPassword) + { + setResource(resource); + setPassword(decryptionPassword); + } + + + /** @return Resource containing key data. */ + public Resource getResource() + { + return resource; + } + + + /** + * Sets the resource containing key data. + * + * @param resource Resource containing key bytes. + */ + public void setResource(final Resource resource) + { + this.resource = resource; + } + + + /** + * Sets the password-based key used to decrypt an encrypted private key. + * + * @param decryptionPassword Password-based encryption key. + */ + public void setPassword(final String decryptionPassword) + { + this.password = decryptionPassword; + } + + + @Override + public PrivateKey newInstance() throws EncodingException, StreamException + { + try { + if (password != null) { + return KeyPairUtil.readPrivateKey(resource.getInputStream(), password.toCharArray()); + } + return KeyPairUtil.readPrivateKey(resource.getInputStream()); + } catch (IOException e) { + throw new StreamException(e); + } + } +}
diff --git a/src/main/java/org/cryptacular/bean/ResourceBasedPublicKeyFactoryBean.java b/src/main/java/org/cryptacular/bean/ResourceBasedPublicKeyFactoryBean.java new file mode 100644 index 0000000..a8c9034 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/ResourceBasedPublicKeyFactoryBean.java
@@ -0,0 +1,67 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.IOException; +import java.security.PublicKey; +import org.cryptacular.EncodingException; +import org.cryptacular.StreamException; +import org.cryptacular.io.Resource; +import org.cryptacular.util.KeyPairUtil; + +/** + * Factory for creating a public key from a {@link Resource} containing data in any of the formats supported by {@link + * KeyPairUtil#readPublicKey(java.io.InputStream)}. + * + * @author Middleware Services + * @see KeyPairUtil#readPublicKey(java.io.InputStream) + */ +public class ResourceBasedPublicKeyFactoryBean implements FactoryBean<PublicKey> +{ + + /** Resource containing key data. */ + private Resource resource; + + + /** Creates a new instance. */ + public ResourceBasedPublicKeyFactoryBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param resource Resource containing encoded key data. + */ + public ResourceBasedPublicKeyFactoryBean(final Resource resource) + { + setResource(resource); + } + + + /** @return Resource containing key data. */ + public Resource getResource() + { + return resource; + } + + + /** + * Sets the resource containing key data. + * + * @param resource Resource containing key bytes. + */ + public void setResource(final Resource resource) + { + this.resource = resource; + } + + + @Override + public PublicKey newInstance() throws EncodingException, StreamException + { + try { + return KeyPairUtil.readPublicKey(resource.getInputStream()); + } catch (IOException e) { + throw new StreamException(e); + } + } +}
diff --git a/src/main/java/org/cryptacular/bean/ResourceBasedSecretKeyFactoryBean.java b/src/main/java/org/cryptacular/bean/ResourceBasedSecretKeyFactoryBean.java new file mode 100644 index 0000000..b80c0b1 --- /dev/null +++ b/src/main/java/org/cryptacular/bean/ResourceBasedSecretKeyFactoryBean.java
@@ -0,0 +1,88 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.IOException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.cryptacular.StreamException; +import org.cryptacular.io.Resource; +import org.cryptacular.util.StreamUtil; + +/** + * Factory that produces a {@link SecretKey} from a {@link Resource}. + * + * @author Middleware Services + */ +public class ResourceBasedSecretKeyFactoryBean implements FactoryBean<SecretKey> +{ + + /** Key algorithm. */ + private String algorithm; + + /** Resource containing key data. */ + private Resource resource; + + + /** Creates a new instance. */ + public ResourceBasedSecretKeyFactoryBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param resource Resource containing encoded key data. + * @param algorithm Algorithm name of cipher with which key will be used. + */ + public ResourceBasedSecretKeyFactoryBean(final Resource resource, final String algorithm) + { + setResource(resource); + setAlgorithm(algorithm); + } + + + /** @return Key algorithm name, e.g. AES. */ + public String getAlgorithm() + { + return algorithm; + } + + + /** + * Sets the key algorithm. + * + * @param algorithm Secret key algorithm, e.g. AES. + */ + public void setAlgorithm(final String algorithm) + { + this.algorithm = algorithm; + } + + + /** @return Resource containing key data. */ + public Resource getResource() + { + return resource; + } + + + /** + * Sets the resource containing key data. + * + * @param resource Resource containing key bytes. + */ + public void setResource(final Resource resource) + { + this.resource = resource; + } + + + @Override + public SecretKey newInstance() throws StreamException + { + try { + return new SecretKeySpec(StreamUtil.readAll(resource.getInputStream()), algorithm); + } catch (IOException e) { + throw new StreamException(e); + } + } +}
diff --git a/src/main/java/org/cryptacular/bean/SimpleHashBean.java b/src/main/java/org/cryptacular/bean/SimpleHashBean.java new file mode 100644 index 0000000..e5f75be --- /dev/null +++ b/src/main/java/org/cryptacular/bean/SimpleHashBean.java
@@ -0,0 +1,58 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import org.bouncycastle.crypto.Digest; +import org.cryptacular.CryptoException; +import org.cryptacular.StreamException; +import org.cryptacular.spec.Spec; + +/** + * Computes a hash using an instance of {@link Digest} specified by {@link #setDigestSpec(org.cryptacular.spec.Spec)}. + * + * @author Middleware Services + */ +public class SimpleHashBean extends AbstractHashBean implements HashBean<byte[]> +{ + + /** Creates a new instance. */ + public SimpleHashBean() {} + + + /** + * Creates a new instance by specifying all properties. + * + * @param digestSpec Digest specification. + * @param iterations Number of hash rounds. + */ + public SimpleHashBean(final Spec<Digest> digestSpec, final int iterations) + { + super(digestSpec, iterations); + } + + + @Override + public byte[] hash(final Object... data) throws CryptoException, StreamException + { + return hashInternal(data); + } + + + /** + * Compares a known hash value with the hash of the given data. + * + * @param hash Known hash value. If the length of the array is greater than the length of the digest output, + * anything beyond the digest length is considered salt data that is hashed <strong>after</strong> the + * input data. + * @param data Data to hash. + * + * @return True if the hashed data matches the given hash, false otherwise. + * + * @throws CryptoException on hash computation errors. + * @throws StreamException on stream IO errors. + */ + @Override + public boolean compare(final byte[] hash, final Object... data) throws CryptoException, StreamException + { + return compareInternal(hash, data); + } +}
diff --git a/src/main/java/org/cryptacular/codec/AbstractBaseNDecoder.java b/src/main/java/org/cryptacular/codec/AbstractBaseNDecoder.java new file mode 100644 index 0000000..1a8d23f --- /dev/null +++ b/src/main/java/org/cryptacular/codec/AbstractBaseNDecoder.java
@@ -0,0 +1,155 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.EncodingException; + +/** + * Base decoder class for encoding schemes described in RFC 3548. + * + * @author Middleware Services + */ +public abstract class AbstractBaseNDecoder implements Decoder +{ + + /** Block of encoded characters. */ + private final char[] block = new char[getBlockLength() / getBitsPerChar()]; + + /** Decoding table. */ + private final byte[] table; + + /** Current position in character block. */ + private int blockPos; + + /** Flag indicating whether input is padded. True by default. */ + private boolean paddedInput = true; + + + /** + * Creates a new instance with given parameters. + * + * @param decodingTable Byte array indexed by characters in the character set encoding. + */ + public AbstractBaseNDecoder(final byte[] decodingTable) + { + table = decodingTable; + } + + + /** @return True if padded input is accepted (default), false otherwise. */ + public boolean isPaddedInput() + { + return paddedInput; + } + + + /** + * Determines whether padded input is accepted. + * + * @param enabled True to enable support for padded input, false otherwise. + */ + public void setPaddedInput(final boolean enabled) + { + this.paddedInput = enabled; + } + + + @Override + public void decode(final CharBuffer input, final ByteBuffer output) throws EncodingException + { + char current; + while (input.hasRemaining()) { + current = input.get(); + if (Character.isWhitespace(current) || current == '=') { + continue; + } + block[blockPos++] = current; + if (blockPos == block.length) { + writeOutput(output, block.length); + } + } + } + + + @Override + public void finalize(final ByteBuffer output) throws EncodingException + { + if (blockPos > 0) { + writeOutput(output, blockPos); + } + } + + + @Override + public int outputSize(final int inputSize) + { + final int size; + if (paddedInput) { + size = inputSize; + } else { + // For unpadded input, add the maximum number of padding characters to get worst-case estimate + size = inputSize + getBlockLength() / 8 - 1; + } + return size * getBitsPerChar() / 8; + } + + + /** @return Number of bits in a block of encoded characters. */ + protected abstract int getBlockLength(); + + + /** @return Number of bits encoding a single character. */ + protected abstract int getBitsPerChar(); + + + /** + * Converts the given alphabet into a base-N decoding table. + * + * @param alphabet Decoding alphabet to use. + * @param n Encoding base. + * + * @return Decoding table of 128 elements. + */ + protected static byte[] decodingTable(final String alphabet, final int n) + { + if (alphabet.length() != n) { + throw new IllegalArgumentException("Alphabet must be exactly " + n + " characters long"); + } + final byte[] decodingTable = new byte[128]; + for (int i = 0; i < n; i++) { + decodingTable[alphabet.charAt(i)] = (byte) i; + } + return decodingTable; + } + + + /** + * Writes bytes in the current encoding block to the output buffer. + * + * @param output Output buffer. + * @param len Number of characters to decode in current block. + */ + private void writeOutput(final ByteBuffer output, final int len) + { + long b; + long value = 0; + int shift = getBlockLength(); + for (int i = 0; i < len; i++) { + b = table[block[i] & 0x7F]; + if (b < 0) { + throw new EncodingException("Invalid character " + block[i]); + } + shift -= getBitsPerChar(); + value |= b << shift; + } + + final int stop = shift + getBitsPerChar() - 1; + int offset = getBlockLength(); + while (offset > stop) { + offset -= 8; + output.put((byte) ((value & (0xffL << offset)) >> offset)); + } + blockPos = 0; + } +}
diff --git a/src/main/java/org/cryptacular/codec/AbstractBaseNEncoder.java b/src/main/java/org/cryptacular/codec/AbstractBaseNEncoder.java new file mode 100644 index 0000000..c0c6858 --- /dev/null +++ b/src/main/java/org/cryptacular/codec/AbstractBaseNEncoder.java
@@ -0,0 +1,184 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.EncodingException; + +/** + * Base encoder class for encoding schemes described in RFC 3548. + * + * @author Middleware Services + */ +public abstract class AbstractBaseNEncoder implements Encoder +{ + + /** Platform-specific line terminator string, e.g. LF (Unix), CRLF (Windows). */ + private static final String NEWLINE = System.lineSeparator(); + + /** Number of base64 characters per line. */ + protected final int lineLength; + + /** Encoding character set. */ + private final char[] charset; + + /** Number of bits in a block. */ + private final int blockLength = getBlockLength(); + + /** Number of bits encoding a single character. */ + private final int bitsPerChar = getBitsPerChar(); + + /** Initial bit mask for selecting characters in a block. */ + private final long initialBitMask; + + /** Holds a block of bytes to encode. */ + private long block; + + /** Number of bits in encode block remaining. */ + private int remaining = blockLength; + + /** Number of characters written. */ + private int outCount; + + /** Flag indicating whether output is padded. True by default. */ + private boolean paddedOutput = true; + + + /** + * Creates a new instance with given parameters. + * + * @param characterSet Encoding character set. + * @param charactersPerLine Number of characters per line. + */ + public AbstractBaseNEncoder(final char[] characterSet, final int charactersPerLine) + { + charset = characterSet; + + long mask = 0; + for (int i = 1; i <= bitsPerChar; i++) { + mask |= 1L << (blockLength - i); + } + initialBitMask = mask; + lineLength = charactersPerLine; + } + + + /** + * @return True if padded output is enabled (default), false otherwise. + */ + public boolean isPaddedOutput() + { + return paddedOutput; + } + + + /** + * Sets the output padding mode. + * + * @param enabled True to enable padded output, false otherwise. + */ + public void setPaddedOutput(final boolean enabled) + { + this.paddedOutput = enabled; + } + + + @Override + public void encode(final ByteBuffer input, final CharBuffer output) throws EncodingException + { + while (input.hasRemaining()) { + remaining -= 8; + block |= (input.get() & 0xffL) << remaining; + if (remaining == 0) { + writeOutput(output, 0); + } + } + } + + + @Override + public void finalize(final CharBuffer output) throws EncodingException + { + if (remaining < blockLength) { + // Floor division + final int stop = remaining / bitsPerChar * bitsPerChar; + writeOutput(output, stop); + if (paddedOutput) { + for (int i = stop; i > 0; i -= bitsPerChar) { + output.put('='); + } + } + } + // Append trailing newline to make consistent with OpenSSL base64 output + if (lineLength > 0 && output.position() > 0) { + output.append(NEWLINE); + } + outCount = 0; + } + + + @Override + public int outputSize(final int inputSize) + { + int len = (inputSize + (blockLength / 8) - 1) * 8 / bitsPerChar; + if (lineLength > 0) { + len += (len / lineLength + 1) * NEWLINE.length(); + } + return len; + } + + + /** @return Number of bits in a block of encoded characters. */ + protected abstract int getBlockLength(); + + + /** @return Number of bits encoding a single character. */ + protected abstract int getBitsPerChar(); + + + /** + * Converts the given alphabet into a base-N encoding table. + * + * @param alphabet Encoding alphabet to use. + * @param n Encoding base. + * + * @return Encoding table of N elements. + */ + protected static char[] encodingTable(final String alphabet, final int n) + { + if (alphabet.length() != n) { + throw new IllegalArgumentException("Alphabet must be exactly " + n + " characters long"); + } + final char[] encodingTable = new char[n]; + for (int i = 0; i < n; i++) { + encodingTable[i] = alphabet.charAt(i); + } + return encodingTable; + } + + + /** + * Writes bytes in the current encoding block to the output buffer. + * + * @param output Output buffer. + * @param stop Bit shift stop position where data in current encoding block ends. + */ + private void writeOutput(final CharBuffer output, final int stop) + { + int shift = blockLength; + long mask = initialBitMask; + int index; + while (shift > stop) { + shift -= bitsPerChar; + index = (int) ((block & mask) >> shift); + output.put(charset[index]); + outCount++; + if (lineLength > 0 && outCount % lineLength == 0) { + output.put(NEWLINE); + } + mask >>= bitsPerChar; + } + block = 0; + remaining = blockLength; + } +}
diff --git a/src/main/java/org/cryptacular/codec/Base32Codec.java b/src/main/java/org/cryptacular/codec/Base32Codec.java new file mode 100644 index 0000000..632e58b --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Base32Codec.java
@@ -0,0 +1,103 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +/** + * Base 32 encoder/decoder pair. + * + * @author Middleware Services + */ +public class Base32Codec implements Codec +{ + + /** Encoder. */ + private final Encoder encoder; + + /** Decoder. */ + private final Decoder decoder; + + /** Custom alphabet to use. */ + private final String customAlphabet; + + /** Whether input/output padding is supported. */ + private final boolean padding; + + + /** + * Creates a new instance using the RFC 4328 alphabet, <code>ABCDEFGHIJKLMNOPQRSTUVWXYZ234567</code>. + */ + public Base32Codec() + { + encoder = new Base32Encoder(); + decoder = new Base32Decoder(); + customAlphabet = null; + padding = true; + } + + + /** + * Creates a new instance using the given 32-character alphabet. + * + * @param alphabet 32-character alphabet to use. + */ + public Base32Codec(final String alphabet) + { + this(alphabet, true); + } + + + /** + * Creates a new instance using the given 32-character alphabet with option to enable/disable padding. + * + * @param alphabet 32-character alphabet to use. + * @param inputOutputPadding True to enable support for padding, false otherwise. + */ + public Base32Codec(final String alphabet, final boolean inputOutputPadding) + { + customAlphabet = alphabet; + padding = inputOutputPadding; + encoder = newEncoder(); + decoder = newDecoder(); + } + + + @Override + public Encoder getEncoder() + { + return encoder; + } + + + @Override + public Decoder getDecoder() + { + return decoder; + } + + + @Override + public Encoder newEncoder() + { + final Base32Encoder encoder; + if (customAlphabet != null) { + encoder = new Base32Encoder(customAlphabet); + } else { + encoder = new Base32Encoder(); + } + encoder.setPaddedOutput(padding); + return encoder; + } + + + @Override + public Decoder newDecoder() + { + final Base32Decoder decoder; + if (customAlphabet != null) { + decoder = new Base32Decoder(customAlphabet); + } else { + decoder = new Base32Decoder(); + } + decoder.setPaddedInput(padding); + return decoder; + } +}
diff --git a/src/main/java/org/cryptacular/codec/Base32Decoder.java b/src/main/java/org/cryptacular/codec/Base32Decoder.java new file mode 100644 index 0000000..4299f48 --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Base32Decoder.java
@@ -0,0 +1,54 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +/** + * Stateful base 32 decoder with support for line breaks. + * + * @author Middleware Services + */ +public class Base32Decoder extends AbstractBaseNDecoder +{ + + /** Base-32 character decoding table. */ + private static final byte[] DECODING_TABLE; + + + /* Initializes the character decoding table. */ + static + { + DECODING_TABLE = decodingTable("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", 32); + } + + /** + * Creates a new instance using the RFC 4648 alphabet, <code>ABCDEFGHIJKLMNOPQRSTUVWXYZ234567</code>, for decoding. + */ + public Base32Decoder() + { + super(DECODING_TABLE); + } + + + /** + * Creates a new instance using the given 32-character alphabet for decoding. + * + * @param alphabet 32-character alphabet to use. + */ + public Base32Decoder(final String alphabet) + { + super(decodingTable(alphabet, 32)); + } + + + @Override + protected int getBlockLength() + { + return 40; + } + + + @Override + protected int getBitsPerChar() + { + return 5; + } +}
diff --git a/src/main/java/org/cryptacular/codec/Base32Encoder.java b/src/main/java/org/cryptacular/codec/Base32Encoder.java new file mode 100644 index 0000000..6dcba61 --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Base32Encoder.java
@@ -0,0 +1,83 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +/** + * Stateful base 32 encoder with support for configurable line breaks. + * + * @author Middleware Services + */ +public class Base32Encoder extends AbstractBaseNEncoder +{ + + /** Base 32 character encoding table. */ + private static final char[] ENCODING_TABLE; + + + /* Initializes the default character encoding table. */ + static + { + ENCODING_TABLE = encodingTable("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", 32); + } + + + /** + * Creates a new instance that produces base 32-encoded output in the RFC 4648 alphabet, + * <code>ABCDEFGHIJKLMNOPQRSTUVWXYZ234567</code>, with no line breaks in the output. + */ + public Base32Encoder() + { + // Default to no line breaks. + this(-1); + } + + + /** + * Creates a new instance that produces base 32-encoded output in the RFC 4648 alphabet, + * <code>ABCDEFGHIJKLMNOPQRSTUVWXYZ234567</code>, with the given number of characters per line in the output. + * + * @param charactersPerLine Number of characters per line. A zero or negative value disables line breaks. + */ + public Base32Encoder(final int charactersPerLine) + { + super(ENCODING_TABLE, charactersPerLine); + } + + + /** + * Creates a new instance that produces base 32-encoded output in the given 32-character alphabet with no line + * breaks in the output. + * + * @param alphabet 32-character alphabet to use. + */ + public Base32Encoder(final String alphabet) + { + this(alphabet, -1); + } + + + /** + * Creates a new instance that produces base 32-encoded output in the given 32-character alphabet + * with the given number of characters per line in the output. + * + * @param alphabet 32-character alphabet to use. + * @param charactersPerLine Number of characters per line. A zero or negative value disables line breaks. + */ + public Base32Encoder(final String alphabet, final int charactersPerLine) + { + super(encodingTable(alphabet, 32), charactersPerLine); + } + + + @Override + protected int getBlockLength() + { + return 40; + } + + + @Override + protected int getBitsPerChar() + { + return 5; + } +}
diff --git a/src/main/java/org/cryptacular/codec/Base64Codec.java b/src/main/java/org/cryptacular/codec/Base64Codec.java new file mode 100644 index 0000000..14d9e7a --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Base64Codec.java
@@ -0,0 +1,103 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +/** + * Base 64 encoder/decoder pair. + * + * @author Middleware Services + */ +public class Base64Codec implements Codec +{ + + /** Encoder. */ + private final Encoder encoder; + + /** Decoder. */ + private final Decoder decoder; + + /** Custom alphabet to use. */ + private final String customAlphabet; + + /** Whether input/output padding is supported. */ + private final boolean padding; + + + /** + * Creates a new instance using the base-64 alphabet defined in RFC 4648. + */ + public Base64Codec() + { + encoder = new Base64Encoder(); + decoder = new Base64Decoder(); + customAlphabet = null; + padding = true; + } + + + /** + * Creates a new instance using the given 64-character alphabet. + * + * @param alphabet 64-character alphabet to use. + */ + public Base64Codec(final String alphabet) + { + this(alphabet, true); + } + + + /** + * Creates a new instance using the given 64-character alphabet with option to enable/disable padding. + * + * @param alphabet 64-character alphabet to use. + * @param inputOutputPadding True to enable support for padding, false otherwise. + */ + public Base64Codec(final String alphabet, final boolean inputOutputPadding) + { + customAlphabet = alphabet; + padding = inputOutputPadding; + encoder = newEncoder(); + decoder = newDecoder(); + } + + + @Override + public Encoder getEncoder() + { + return encoder; + } + + + @Override + public Decoder getDecoder() + { + return decoder; + } + + + @Override + public Encoder newEncoder() + { + final Base64Encoder encoder; + if (customAlphabet != null) { + encoder = new Base64Encoder(customAlphabet); + } else { + encoder = new Base64Encoder(); + } + encoder.setPaddedOutput(padding); + return encoder; + } + + + @Override + public Decoder newDecoder() + { + final Base64Decoder decoder; + if (customAlphabet != null) { + decoder = new Base64Decoder(customAlphabet); + } else { + decoder = new Base64Decoder(); + } + decoder.setPaddedInput(padding); + return decoder; + } +}
diff --git a/src/main/java/org/cryptacular/codec/Base64Decoder.java b/src/main/java/org/cryptacular/codec/Base64Decoder.java new file mode 100644 index 0000000..57bc791 --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Base64Decoder.java
@@ -0,0 +1,144 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +/** + * Stateful base 64 decoder with support for line breaks. + * + * @author Middleware Services + */ +public class Base64Decoder extends AbstractBaseNDecoder +{ + + /** Default base-64 character decoding table. */ + private static final byte[] DEFAULT_DECODING_TABLE; + + /** URL and filesystem-safe base-64 character decoding table. */ + private static final byte[] URLSAFE_DECODING_TABLE; + + + /* Initializes the character decoding table. */ + static + { + DEFAULT_DECODING_TABLE = decodingTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", 64); + URLSAFE_DECODING_TABLE = decodingTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", 64); + } + + + /** Creates a new instance that decodes base 64-encoded input in the default character set. */ + public Base64Decoder() + { + this(false); + } + + + /** + * Creates a new instance that decodes base 64-encoded input in the optional URL-safe character set. + * + * @param urlSafe True to use URL and filesystem-safe character set, false otherwise. + */ + public Base64Decoder(final boolean urlSafe) + { + super(urlSafe ? URLSAFE_DECODING_TABLE : DEFAULT_DECODING_TABLE); + } + + + /** + * Creates a new instance that decodes base-64 character data encoded in the given alphabet. + * + * @param alphabet Base-64 alphabet to use for decoding + */ + public Base64Decoder(final String alphabet) + { + super(decodingTable(alphabet, 64)); + } + + + @Override + protected int getBlockLength() + { + return 24; + } + + + @Override + protected int getBitsPerChar() + { + return 6; + } + + + /** + * Builder for base-64 decoders. + */ + public static class Builder + { + /** URL-safe alphabet flag. */ + private boolean urlSafe; + + /** Arbitrary alphbet. */ + private String alphabet; + + /** Padding flag. */ + private boolean padding; + + + /** + * Sets the URL-safe alphabet flag. + * + * @param safe True for URL-safe alphabet, false otherwise. + * + * @return This instance. + */ + public Builder setUrlSafe(final boolean safe) + { + urlSafe = safe; + return this; + } + + + /** + * Sets an arbitrary 64-character alphabet for decoding. + * + * @param alpha Alternative alphabet. + * + * @return This instance. + */ + public Builder setAlphabet(final String alpha) + { + alphabet = alpha; + return this; + } + + + /** + * Sets padding flag on the decoder. + * + * @param pad True for base-64 padding, false otherwise. + * + * @return This instance. + */ + public Builder setPadding(final boolean pad) + { + padding = pad; + return this; + } + + + /** + * Builds a base-64 decoder with the given options. + * + * @return New base-64 decoder instance. + */ + public Base64Decoder build() + { + final Base64Decoder decoder; + if (alphabet != null) { + decoder = new Base64Decoder(alphabet); + } else { + decoder = new Base64Decoder(urlSafe); + } + decoder.setPaddedInput(padding); + return decoder; + } + } +}
diff --git a/src/main/java/org/cryptacular/codec/Base64Encoder.java b/src/main/java/org/cryptacular/codec/Base64Encoder.java new file mode 100644 index 0000000..a397f8c --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Base64Encoder.java
@@ -0,0 +1,201 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +/** + * Stateful base 64 encoder with support for configurable line breaks. + * + * @author Middleware Services + */ +public class Base64Encoder extends AbstractBaseNEncoder +{ + + /** Default base 64 character encoding table. */ + private static final char[] DEFAULT_ENCODING_TABLE; + + /** Filesystem and URL-safe base 64 character encoding table. */ + private static final char[] URLSAFE_ENCODING_TABLE; + + + /* Initializes the default character encoding tables. */ + static + { + DEFAULT_ENCODING_TABLE = encodingTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", 64); + URLSAFE_ENCODING_TABLE = encodingTable("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_", 64); + } + + + /** Creates a new instance that produces base 64-encoded output with no line breaks in the default character set. */ + public Base64Encoder() + { + // Default to no line breaks. + this(-1); + } + + + /** + * Creates a new instance that produces base 64-encoded output with no line breaks and optional URL-safe character + * set. + * + * @param urlSafe True to use URL and filesystem-safe character set, false otherwise. + */ + public Base64Encoder(final boolean urlSafe) + { + this(urlSafe, -1); + } + + + /** + * Creates a new instance that produces base 64-encoded output with the given number of characters per line in the + * default character set. + * + * @param charactersPerLine Number of characters per line. A zero or negative value disables line breaks. + */ + public Base64Encoder(final int charactersPerLine) + { + this(false, charactersPerLine); + } + + + /** + * Creates a new instance that produces base 64-encoded output with the given number of characters per line with the + * option of URL-safe character set. + * + * @param urlSafe True to use URL and filesystem-safe character set, false otherwise. + * @param charactersPerLine Number of characters per line. A zero or negative value disables line breaks. + */ + public Base64Encoder(final boolean urlSafe, final int charactersPerLine) + { + super(urlSafe ? URLSAFE_ENCODING_TABLE : DEFAULT_ENCODING_TABLE, charactersPerLine); + } + + + /** + * Creates a new instance that produces base 64-encoded output with the given 64-character alphabet. + * + * @param alphabet 64-character alphabet to use. + */ + public Base64Encoder(final String alphabet) + { + this(alphabet, -1); + } + + + /** + * Creates a new instance that produces base 64-encoded output with the given 64-character alphabet with line + * wrapping at the specified line length; + * + * @param alphabet 64-character alphabet to use. + * @param charactersPerLine Number of characters per line. A zero or negative value disables line breaks. + */ + public Base64Encoder(final String alphabet, final int charactersPerLine) + { + super(encodingTable(alphabet, 64), charactersPerLine); + } + + + @Override + protected int getBlockLength() + { + return 24; + } + + + @Override + protected int getBitsPerChar() + { + return 6; + } + + + /** + * Builder for base-64 encoders. + */ + public static class Builder + { + /** URL-safe alphabet flag. */ + private boolean urlSafe; + + /** Arbitrary alphbet. */ + private String alphabet; + + /** Padding flag. */ + private boolean padding; + + /** Number of base-64 characters per line in output. */ + private int charactersPerLine = -1; + + + /** + * Sets the URL-safe alphabet flag. + * + * @param safe True for URL-safe alphabet, false otherwise. + * + * @return This instance. + */ + public Base64Encoder.Builder setUrlSafe(final boolean safe) + { + urlSafe = safe; + return this; + } + + + /** + * Sets an arbitrary 64-character alphabet for encoding. + * + * @param alpha Alternative alphabet. + * + * @return This instance. + */ + public Base64Encoder.Builder setAlphabet(final String alpha) + { + alphabet = alpha; + return this; + } + + + /** + * Sets padding flag on the encoder. + * + * @param pad True for base-64 padding, false otherwise. + * + * @return This instance. + */ + public Base64Encoder.Builder setPadding(final boolean pad) + { + padding = pad; + return this; + } + + + /** + * Sets the number of characters per line in output produced by the encoder. + * + * @param lineLength Number of characters per line. Set to <code>-1</code> to suppress line breaks. + * + * @return This instance. + */ + public Base64Encoder.Builder setCharactersPerLine(final int lineLength) + { + charactersPerLine = lineLength; + return this; + } + + + /** + * Builds a base-64 encoder with the given options. + * + * @return New base-64 encoder instance. + */ + public Base64Encoder build() + { + final Base64Encoder decoder; + if (alphabet != null) { + decoder = new Base64Encoder(alphabet, charactersPerLine); + } else { + decoder = new Base64Encoder(urlSafe, charactersPerLine); + } + decoder.setPaddedOutput(padding); + return decoder; + } + } +}
diff --git a/src/main/java/org/cryptacular/codec/Codec.java b/src/main/java/org/cryptacular/codec/Codec.java new file mode 100644 index 0000000..1c97226 --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Codec.java
@@ -0,0 +1,27 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +/** + * Container for an encoder/decoder pair. + * + * @author Middleware Services + */ +public interface Codec +{ + + + /** @return The byte-to-char encoder of the codec pair. */ + Encoder getEncoder(); + + + /** @return The char-to-byte decoder of the codec pair. */ + Decoder getDecoder(); + + + /** @return A new instance of the byte-to-char encoder of the codec pair. */ + Encoder newEncoder(); + + + /** @return A new instance of the char-to-byte decoder of the codec pair. */ + Decoder newDecoder(); +}
diff --git a/src/main/java/org/cryptacular/codec/Decoder.java b/src/main/java/org/cryptacular/codec/Decoder.java new file mode 100644 index 0000000..fbf759a --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Decoder.java
@@ -0,0 +1,46 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.EncodingException; + +/** + * Describes a potentially stateful character-to-byte decoder. + * + * @author Middleware Services + */ +public interface Decoder +{ + + /** + * Decodes characters in input buffer into bytes placed in the output buffer. This method may be called multiple + * times, followed by {@link #finalize(ByteBuffer)}. after all input bytes have been provided. + * + * @param input Input character buffer. + * @param output Output byte buffer. + * + * @throws EncodingException on decoding errors. + */ + void decode(CharBuffer input, ByteBuffer output) throws EncodingException; + + + /** + * Performs final output decoding (e.g. padding) after all input characters have been provided. + * + * @param output Output byte buffer. + * + * @throws EncodingException on decoding errors. + */ + void finalize(ByteBuffer output) throws EncodingException; + + + /** + * Expected number of bytes in the output buffer for an input buffer of the given size. + * + * @param inputSize Size of input buffer in characters. + * + * @return Minimum byte buffer size required to store all decoded characters in input buffer. + */ + int outputSize(int inputSize); +}
diff --git a/src/main/java/org/cryptacular/codec/Encoder.java b/src/main/java/org/cryptacular/codec/Encoder.java new file mode 100644 index 0000000..ac53606 --- /dev/null +++ b/src/main/java/org/cryptacular/codec/Encoder.java
@@ -0,0 +1,46 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.EncodingException; + +/** + * Describes a potentially stateful byte-to-character encoder. + * + * @author Middleware Services + */ +public interface Encoder +{ + + /** + * Encodes bytes in input buffer into characters placed in the output buffer. This method may be called multiple + * times, followed by {@link #finalize(java.nio.CharBuffer)} after all input bytes have been provided. + * + * @param input Input byte buffer. + * @param output Output character buffer. + * + * @throws EncodingException on encoding errors. + */ + void encode(ByteBuffer input, CharBuffer output) throws EncodingException; + + + /** + * Performs final output encoding (e.g. padding) after all input bytes have been provided. + * + * @param output Output character buffer. + * + * @throws EncodingException on encoding errors. + */ + void finalize(CharBuffer output) throws EncodingException; + + + /** + * Expected number of characters in the output buffer for an input buffer of the given size. + * + * @param inputSize Size of input buffer in bytes. + * + * @return Minimum character buffer size required to store all encoded input bytes. + */ + int outputSize(int inputSize); +}
diff --git a/src/main/java/org/cryptacular/codec/HexCodec.java b/src/main/java/org/cryptacular/codec/HexCodec.java new file mode 100644 index 0000000..005f468 --- /dev/null +++ b/src/main/java/org/cryptacular/codec/HexCodec.java
@@ -0,0 +1,69 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +/** + * Hexadecimal encoder/decoder pair. + * + * @author Middleware Services + */ +public class HexCodec implements Codec +{ + + /** Encoder. */ + private final Encoder encoder; + + /** Decoder. */ + private final Decoder decoder = new HexDecoder(); + + /** True to encode in uppercase characters, false otherwise. */ + private final boolean uppercase; + + + /** + * Creates a new instance that outputs lowercase hex characters and supports decoding in either case. + */ + public HexCodec() + { + this(false); + } + + + /** + * Creates a new instance that optionally outputs uppercase hex characters and supports decoding in either case. + * + * @param uppercaseOutput True to output uppercase alphabetic characters, false for lowercase. + */ + public HexCodec(final boolean uppercaseOutput) + { + uppercase = uppercaseOutput; + encoder = new HexEncoder(false, uppercase); + } + + + @Override + public Encoder getEncoder() + { + return encoder; + } + + + @Override + public Decoder getDecoder() + { + return decoder; + } + + + @Override + public Encoder newEncoder() + { + return new HexEncoder(false, uppercase); + } + + + @Override + public Decoder newDecoder() + { + return new HexDecoder(); + } +}
diff --git a/src/main/java/org/cryptacular/codec/HexDecoder.java b/src/main/java/org/cryptacular/codec/HexDecoder.java new file mode 100644 index 0000000..175f691 --- /dev/null +++ b/src/main/java/org/cryptacular/codec/HexDecoder.java
@@ -0,0 +1,91 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.util.Arrays; +import org.cryptacular.EncodingException; + +/** + * Stateful hexadecimal character-to-byte decoder. + * + * @author Middleware Services + */ +public class HexDecoder implements Decoder +{ + + /** Hex character decoding table. */ + private static final byte[] DECODING_TABLE = new byte[128]; + + /* Initializes the character decoding table. */ + static { + Arrays.fill(DECODING_TABLE, (byte) -1); + for (int i = 0; i < 10; i++) { + DECODING_TABLE[i + 48] = (byte) i; + } + for (int i = 0; i < 6; i++) { + DECODING_TABLE[i + 65] = (byte) (10 + i); + DECODING_TABLE[i + 97] = (byte) (10 + i); + } + } + + /** Number of encoded characters processed. */ + private int count; + + + @Override + public void decode(final CharBuffer input, final ByteBuffer output) throws EncodingException + { + // Ignore leading 0x characters if present + if (input.get(0) == '0' && input.get(1) == 'x') { + input.position(input.position() + 2); + } + + byte hi = 0; + byte lo; + char current; + while (input.hasRemaining()) { + current = input.get(); + if (current == ':' || Character.isWhitespace(current)) { + continue; + } + if ((count++ & 0x01) == 0) { + hi = lookup(current); + } else { + lo = lookup(current); + output.put((byte) ((hi << 4) | lo)); + } + } + } + + + @Override + public void finalize(final ByteBuffer output) throws EncodingException + { + count = 0; + } + + + @Override + public int outputSize(final int inputSize) + { + return inputSize / 2; + } + + + /** + * Looks up the byte that corresponds to the given character. + * + * @param c Encoded character. + * + * @return Decoded byte. + */ + private static byte lookup(final char c) + { + final byte b = DECODING_TABLE[c & 0x7F]; + if (b < 0) { + throw new EncodingException("Invalid hex character " + c); + } + return b; + } +}
diff --git a/src/main/java/org/cryptacular/codec/HexEncoder.java b/src/main/java/org/cryptacular/codec/HexEncoder.java new file mode 100644 index 0000000..b259bec --- /dev/null +++ b/src/main/java/org/cryptacular/codec/HexEncoder.java
@@ -0,0 +1,109 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.EncodingException; + +/** + * Stateless hexadecimal byte-to-character encoder. + * + * @author Middleware Services + */ +public class HexEncoder implements Encoder +{ + + /** Lowercase hex character encoding table. */ + private static final char[] LC_ENCODING_TABLE = new char[16]; + + /** Uppercase hex character encoding table. */ + private static final char[] UC_ENCODING_TABLE = new char[16]; + + + + /* Initializes the encoding character table. */ + static { + initTable("0123456789abcdef", LC_ENCODING_TABLE); + initTable("0123456789ABCDEF", UC_ENCODING_TABLE); + } + + /** Flag indicating whether to delimit every two characters with ':' as in key fingerprints, etc. */ + private final boolean delimit; + + /** Encoding table to use. */ + private final char[] table; + + + /** Creates a new instance that does not delimit bytes in the output hex string. */ + public HexEncoder() + { + this(false, false); + } + + /** + * Creates a new instance with optional colon-delimiting of bytes. + * + * @param delimitBytes True to delimit every two characters (i.e. every byte) with ':' character. + */ + public HexEncoder(final boolean delimitBytes) + { + this(delimitBytes, false); + } + + + /** + * Creates a new instance with optional colon-delimiting of bytes and uppercase output. + * + * @param delimitBytes True to delimit every two characters (i.e. every byte) with ':' character. + * @param uppercase True to output uppercase alphabetic characters, false for lowercase. + */ + public HexEncoder(final boolean delimitBytes, final boolean uppercase) + { + delimit = delimitBytes; + table = uppercase ? UC_ENCODING_TABLE : LC_ENCODING_TABLE; + } + + + @Override + public void encode(final ByteBuffer input, final CharBuffer output) throws EncodingException + { + byte current; + while (input.hasRemaining()) { + current = input.get(); + output.put(table[(current & 0xf0) >> 4]); + output.put(table[current & 0x0f]); + if (delimit && input.hasRemaining()) { + output.put(':'); + } + } + } + + + @Override + public void finalize(final CharBuffer output) throws EncodingException {} + + + @Override + public int outputSize(final int inputSize) + { + int size = inputSize * 2; + if (delimit) { + size += inputSize - 1; + } + return size; + } + + + /** + * Initializes the encoding table for the given character set. + * + * @param charset Character set. + * @param table Encoding table. + */ + private static void initTable(final String charset, final char[] table) + { + for (int i = 0; i < charset.length(); i++) { + table[i] = charset.charAt(i); + } + } +}
diff --git a/src/main/java/org/cryptacular/generator/AESP12Generator.java b/src/main/java/org/cryptacular/generator/AESP12Generator.java new file mode 100644 index 0000000..e0c8d28 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/AESP12Generator.java
@@ -0,0 +1,202 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.io.OutputStream; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERNull; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.EncryptionScheme; +import org.bouncycastle.asn1.pkcs.KeyDerivationFunc; +import org.bouncycastle.asn1.pkcs.PBES2Parameters; +import org.bouncycastle.asn1.pkcs.PBKDF2Params; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.crypto.util.PBKDF2Config; +import org.bouncycastle.operator.GenericKey; +import org.bouncycastle.operator.OutputEncryptor; +import org.cryptacular.pbe.PBES2Algorithm; +import org.cryptacular.pbe.PBES2EncryptionScheme; +import org.cryptacular.spec.DigestSpec; + + +/** + * Generates PKCS12 containers using the PBES2 algorithm with the AES-256-CBC cipher for encryption, which is the + * most portable and secure algorithm in use with PKCS12 at this time. + * + * @author Marvin S. Addison + */ +public class AESP12Generator extends AbstractP12Generator +{ + /** Set of supported digest algorithms. */ + public static final Set<ASN1ObjectIdentifier> SUPPORTED_DIGEST_ALGORITHMS = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList( + NISTObjectIdentifiers.id_sha256, + NISTObjectIdentifiers.id_sha512, + NISTObjectIdentifiers.id_sha3_256, + NISTObjectIdentifiers.id_sha3_384, + NISTObjectIdentifiers.id_sha3_512 + ))); + + /** Map of digest algorithm identifiers to digest specifications. */ + private static final Map<ASN1ObjectIdentifier, DigestSpec> DIGEST_ID_TO_DIGEST_SPEC_MAP = new HashMap<>(); + + /** Map of digest algorithm identifiers to HMAC algorithm IDs. */ + private static final Map<ASN1ObjectIdentifier, ASN1ObjectIdentifier> DIGEST_ID_TO_HMAC_ID_MAP = new HashMap<>(); + + /** Digest algorithm used for all HMAC operations. */ + private final ASN1ObjectIdentifier digestAlgorithm; + + /** PBKDF2 configuration. */ + private final PBKDF2Config pbkdf2Config; + + /** Produces encryptors that use the PBES2 algorithm. */ + private final PBES2OutputEncryptorBuilder outputEncryptorBuilder; + + + static + { + DIGEST_ID_TO_DIGEST_SPEC_MAP.put(NISTObjectIdentifiers.id_sha256, new DigestSpec("SHA256")); + DIGEST_ID_TO_DIGEST_SPEC_MAP.put(NISTObjectIdentifiers.id_sha512, new DigestSpec("SHA512")); + DIGEST_ID_TO_DIGEST_SPEC_MAP.put(NISTObjectIdentifiers.id_sha3_256, new DigestSpec("SHA3", 256)); + DIGEST_ID_TO_DIGEST_SPEC_MAP.put(NISTObjectIdentifiers.id_sha3_384, new DigestSpec("SHA3", 384)); + DIGEST_ID_TO_DIGEST_SPEC_MAP.put(NISTObjectIdentifiers.id_sha3_512, new DigestSpec("SHA3", 512)); + DIGEST_ID_TO_HMAC_ID_MAP.put(NISTObjectIdentifiers.id_sha256, PKCSObjectIdentifiers.id_hmacWithSHA256); + DIGEST_ID_TO_HMAC_ID_MAP.put(NISTObjectIdentifiers.id_sha512, PKCSObjectIdentifiers.id_hmacWithSHA512); + DIGEST_ID_TO_HMAC_ID_MAP.put(NISTObjectIdentifiers.id_sha3_256, NISTObjectIdentifiers.id_hmacWithSHA3_256); + DIGEST_ID_TO_HMAC_ID_MAP.put(NISTObjectIdentifiers.id_sha3_384, NISTObjectIdentifiers.id_hmacWithSHA3_384); + DIGEST_ID_TO_HMAC_ID_MAP.put(NISTObjectIdentifiers.id_sha3_512, NISTObjectIdentifiers.id_hmacWithSHA3_512); + } + + + /** + * Creates a new instance that encrypts with AES-256-CBC and SHA256 using 2048 rounds of hashing. + */ + public AESP12Generator() + { + this(NISTObjectIdentifiers.id_sha256, 2048); + } + + /** + * Creates a new instance that encrypts with AES-256-CBC and SHA256 using the given number of hashing rounds. + * + * @param iterations Number of rounds of encryption. + */ + public AESP12Generator(final int iterations) + { + this(NISTObjectIdentifiers.id_sha256, iterations); + } + + /** + * Creates a new instances that uses AES-256-CBC and the given digest algorithm to encrypt data. + * + * @param digestAlgId Digest algorithm identifier. + * @param iterations Number of rounds of hashing. + */ + public AESP12Generator(final ASN1ObjectIdentifier digestAlgId, final int iterations) + { + if (!SUPPORTED_DIGEST_ALGORITHMS.contains(digestAlgId)) { + throw new IllegalArgumentException("Unsupported digest algorithm"); + } + digestAlgorithm = digestAlgId; + final ASN1ObjectIdentifier hmacAlgId = DIGEST_ID_TO_HMAC_ID_MAP.get(digestAlgId); + // The default behavior of the builder is to select salt size based on HMAC algorithm, + // which is the desirable behavior here + pbkdf2Config = new PBKDF2Config.Builder() + .withIterationCount(iterations) + .withPRF(new AlgorithmIdentifier(hmacAlgId, DERNull.INSTANCE)) + .build(); + outputEncryptorBuilder = new PBES2OutputEncryptorBuilder(PBES2Algorithm.AES256, pbkdf2Config); + } + + @Override + public int getIterations() + { + return pbkdf2Config.getIterationCount(); + } + + @Override + protected ASN1ObjectIdentifier getDigestAlgorithmId() + { + return digestAlgorithm; + } + + @Override + protected DigestSpec getDigestSpec() + { + return DIGEST_ID_TO_DIGEST_SPEC_MAP.get(digestAlgorithm); + } + + @Override + protected OutputEncryptor keyOutputEncryptor(final char[] password) + { + return outputEncryptorBuilder.build(password); + } + + @Override + protected OutputEncryptor dataOutputEncryptor(final char[] password) + { + return outputEncryptorBuilder.build(password); + } + + /** + * Builds an output encryptor based on PBKDF2 domain parameters. + */ + static class PBES2OutputEncryptorBuilder + { + /** Source of cryptographically-strong randomness. */ + private final SecureRandom random = new SecureRandom(); + + /** PBES2 encryption algorithm. */ + private final PBES2Algorithm encryptionAlg; + + /** PBKDF2 domain parameters. */ + private final PBKDF2Config pbkdf2Config; + + + PBES2OutputEncryptorBuilder(final PBES2Algorithm encAlg, final PBKDF2Config config) + { + this.encryptionAlg = encAlg; + this.pbkdf2Config = config; + } + + OutputEncryptor build(final char[] password) + { + final byte[] salt = new byte[pbkdf2Config.getSaltLength()]; + random.nextBytes(salt); + final byte[] iv = new byte[encryptionAlg.getBlockSize() / 8]; + random.nextBytes(iv); + final ASN1ObjectIdentifier encryptionAlgId = new ASN1ObjectIdentifier(encryptionAlg.getOid()); + final EncryptionScheme encryptionScheme = new EncryptionScheme(encryptionAlgId, new DEROctetString(iv)); + final PBKDF2Params pbkdf2Params = new PBKDF2Params(salt, pbkdf2Config.getIterationCount(), pbkdf2Config.getPRF()); + final PBES2Parameters pbes2Parameters = new PBES2Parameters( + new KeyDerivationFunc(PKCSObjectIdentifiers.id_PBKDF2, pbkdf2Params), + encryptionScheme); + final PBES2EncryptionScheme scheme = new PBES2EncryptionScheme(pbes2Parameters, password); + return new OutputEncryptor() + { + public AlgorithmIdentifier getAlgorithmIdentifier() + { + return new AlgorithmIdentifier(PKCSObjectIdentifiers.id_PBES2, pbes2Parameters); + } + + public OutputStream getOutputStream(final OutputStream out) + { + return scheme.wrap(true, out); + } + + public GenericKey getKey() + { + return null; + } + }; + } + } +}
diff --git a/src/main/java/org/cryptacular/generator/AbstractOTPGenerator.java b/src/main/java/org/cryptacular/generator/AbstractOTPGenerator.java new file mode 100644 index 0000000..6260207 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/AbstractOTPGenerator.java
@@ -0,0 +1,99 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.macs.HMac; +import org.bouncycastle.crypto.params.KeyParameter; +import org.cryptacular.util.ByteUtil; + +/** + * Abstract base class for <a href="https://tools.ietf.org/html/rfc4226">HOTP</a> and <a + * href="https://tools.ietf.org/html/rfc6238">TOTP</a> OTP generation schemes. + * + * @author Middleware Services + */ +public abstract class AbstractOTPGenerator +{ + + /** Array of modulus values indexed per number of digits in OTP output. */ + private static final int[] MODULUS = new int[] { + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000, + 1000000000, + }; + + /** Number of digits in generated OTP. */ + private int numberOfDigits = 6; + + + /** @return Number of digits in generated OTP. */ + public int getNumberOfDigits() + { + return numberOfDigits; + } + + + /** + * Sets the numbers in the generated OTP. + * + * @param digits Number of digits in generated OTP. MUST be in the range 6 - 9. Default is 6. + */ + public void setNumberOfDigits(final int digits) + { + if (digits < 6 || digits > 9) { + throw new IllegalArgumentException("Number of generated digits must be in range 6-9."); + } + this.numberOfDigits = digits; + } + + + /** + * Internal OTP generation method. + * + * @param key Per-user key. + * @param count Counter moving factor. + * + * @return Integer OTP. + */ + protected int generateInternal(final byte[] key, final long count) + { + final HMac hmac = new HMac(getDigest()); + final byte[] output = new byte[hmac.getMacSize()]; + hmac.init(new KeyParameter(key)); + hmac.update(ByteUtil.toBytes(count), 0, 8); + hmac.doFinal(output, 0); + return truncate(output) % MODULUS[numberOfDigits]; + } + + + /** @return Digest algorithm used for HMAC operation. */ + protected abstract Digest getDigest(); + + + /** + * Truncates HMAC output onto an unsigned (i.e. 31-bit) integer using the strategy discussed in RFC 4226, + * section 5.3. + * + * @param hmac HMAC output. + * + * @return Truncated output. + */ + private int truncate(final byte[] hmac) + { + // Offset is the lowest 4 bits of the computed hash + final int offset = hmac[hmac.length - 1] & 0xf; + return + (hmac[offset] & 0x7f) << 24 | + (hmac[offset + 1] & 0xff) << 16 | + (hmac[offset + 2] & 0xff) << 8 | + (hmac[offset + 3] & 0xff); + } + +}
diff --git a/src/main/java/org/cryptacular/generator/AbstractP12Generator.java b/src/main/java/org/cryptacular/generator/AbstractP12Generator.java new file mode 100644 index 0000000..e73dd46 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/AbstractP12Generator.java
@@ -0,0 +1,116 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERBMPString; +import org.bouncycastle.asn1.DERNull; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.crypto.ExtendedDigest; +import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.pkcs.PKCS12MacCalculatorBuilder; +import org.bouncycastle.pkcs.PKCS12PfxPdu; +import org.bouncycastle.pkcs.PKCS12PfxPduBuilder; +import org.bouncycastle.pkcs.PKCS12SafeBag; +import org.bouncycastle.pkcs.PKCS12SafeBagBuilder; +import org.bouncycastle.pkcs.PKCSException; +import org.bouncycastle.pkcs.bc.BcPKCS12MacCalculatorBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS12SafeBagBuilder; +import org.cryptacular.CryptoException; +import org.cryptacular.spec.DigestSpec; + +/** + * Base class for all PKCS12 generation components. + * + * @author Marvin S. Addison + */ +public abstract class AbstractP12Generator implements P12Generator +{ + + @Override + public PKCS12PfxPdu generate(final char[] password, final PrivateKey key, final String alias, + final X509Certificate... certificates) + { + String label; + if (certificates.length < 1) { + throw new IllegalArgumentException("At least one certificate must be provided"); + } + if (password == null || password.length == 0) { + throw new IllegalArgumentException("Password cannot be null or empty"); + } + final PKCS12PfxPduBuilder pfxPduBuilder = new PKCS12PfxPduBuilder(); + final PKCS12SafeBag[] certBags = new PKCS12SafeBag[certificates.length]; + final JcaX509ExtensionUtils extUtils; + try { + extUtils = new JcaX509ExtensionUtils(); + final PKCS12SafeBagBuilder keyBagBuilder = new JcaPKCS12SafeBagBuilder( + key, keyOutputEncryptor(password)); + keyBagBuilder.addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, new DERBMPString(alias)); + keyBagBuilder.addBagAttribute( + PKCSObjectIdentifiers.pkcs_9_at_localKeyId, + extUtils.createSubjectKeyIdentifier(certificates[0].getPublicKey())); + certBags[0] = new JcaPKCS12SafeBagBuilder(certificates[0]) + .addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, new DERBMPString(alias)) + .addBagAttribute( + PKCSObjectIdentifiers.pkcs_9_at_localKeyId, + extUtils.createSubjectKeyIdentifier(certificates[0].getPublicKey())) + .build(); + for (int i = 1; i < certificates.length; i++) { + label = "ca-cert-" + i; + certBags[i] = new JcaPKCS12SafeBagBuilder(certificates[i]) + .addBagAttribute(PKCSObjectIdentifiers.pkcs_9_at_friendlyName, new DERBMPString(label)) + .build(); + } + // Add certificates before private key as is the usual ordering produced by OpenSSL + pfxPduBuilder.addEncryptedData(dataOutputEncryptor(password), certBags); + pfxPduBuilder.addData(keyBagBuilder.build()); + final DigestSpec digestSpec = getDigestSpec(); + final PKCS12MacCalculatorBuilder macCalculatorBuilder = new BcPKCS12MacCalculatorBuilder( + (ExtendedDigest) digestSpec.newInstance(), + new AlgorithmIdentifier(getDigestAlgorithmId(), DERNull.INSTANCE) + ).setIterationCount(getIterations()); + return pfxPduBuilder.build(macCalculatorBuilder, password); + } catch (IOException | NoSuchAlgorithmException | PKCSException e) { + throw new CryptoException("P12 generation failed", e); + } + } + + @Override + public PKCS12PfxPdu generate(final char[] password, final PrivateKey key, final X509Certificate... certificates) + { + return generate(password, key, "end-entity-cert", certificates); + } + + /** @return Number of hashing rounds. */ + public abstract int getIterations(); + + /** @return Digest algorithm object identifier. */ + protected abstract ASN1ObjectIdentifier getDigestAlgorithmId(); + + /** @return Digest specification. */ + protected abstract DigestSpec getDigestSpec(); + + /** + * Builds a new output encryptor that performs password-based encryption on keys in the P12 file. + * + * @param password Password tha will the basis of an encryption key. + * + * @return Output encryptor. + */ + protected abstract OutputEncryptor keyOutputEncryptor(char[] password); + + + /** + * Builds a new output encryptor that performs password-based encryption on encrypted data in the P12 file. + * + * @param password Password tha will the basis of an encryption key. + * + * @return Output encryptor. + */ + protected abstract OutputEncryptor dataOutputEncryptor(char[] password); +}
diff --git a/src/main/java/org/cryptacular/generator/HOTPGenerator.java b/src/main/java/org/cryptacular/generator/HOTPGenerator.java new file mode 100644 index 0000000..7de1330 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/HOTPGenerator.java
@@ -0,0 +1,35 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; + +/** + * OTP generator component that implements the HOTP scheme described in <a href="https://tools.ietf.org/html/rfc4226"> + * RFC 4226</a>. + * + * @author Middleware Services + */ +public class HOTPGenerator extends AbstractOTPGenerator +{ + + /** + * Generates the OTP given a per-user key and invocation count. + * + * @param key Per-user key. + * @param count Counter moving factor. + * + * @return Integer OTP. + */ + public int generate(final byte[] key, final long count) + { + return generateInternal(key, count); + } + + + @Override + protected Digest getDigest() + { + return new SHA1Digest(); + } +}
diff --git a/src/main/java/org/cryptacular/generator/IdGenerator.java b/src/main/java/org/cryptacular/generator/IdGenerator.java new file mode 100644 index 0000000..122f225 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/IdGenerator.java
@@ -0,0 +1,18 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +/** + * Generation strategy for random identifiers. + * + * @author Middleware Services + */ +public interface IdGenerator +{ + + /** + * Generates a random identifier. + * + * @return Random identifier. + */ + String generate(); +}
diff --git a/src/main/java/org/cryptacular/generator/KeyPairGenerator.java b/src/main/java/org/cryptacular/generator/KeyPairGenerator.java new file mode 100644 index 0000000..6e92db4 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/KeyPairGenerator.java
@@ -0,0 +1,91 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyPair; +import java.security.SecureRandom; +import org.bouncycastle.jce.spec.ECNamedCurveGenParameterSpec; + +/** + * Static factory that generates various types of asymmetric key pairs. + * + * @author Middleware Services + */ +public final class KeyPairGenerator +{ + + /** Private constructor of static factory. */ + private KeyPairGenerator() {} + + + /** + * Generates a DSA key pair. + * + * @param random Random source required for key generation. + * @param bitLength Desired key size in bits. + * + * @return DSA key pair of desired size. + */ + public static KeyPair generateDSA(final SecureRandom random, final int bitLength) + { + final org.bouncycastle.jcajce.provider.asymmetric.dsa.KeyPairGeneratorSpi generator = + new org.bouncycastle.jcajce.provider.asymmetric.dsa.KeyPairGeneratorSpi(); + generator.initialize(bitLength, random); + return generator.generateKeyPair(); + } + + + /** + * Generates an RSA key pair. + * + * @param random Random source required for key generation. + * @param bitLength Desired key size in bits. + * + * @return RSA key pair of desired size. + */ + public static KeyPair generateRSA(final SecureRandom random, final int bitLength) + { + final org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi generator = + new org.bouncycastle.jcajce.provider.asymmetric.rsa.KeyPairGeneratorSpi(); + generator.initialize(bitLength, random); + return generator.generateKeyPair(); + } + + + /** + * Generates an EC key pair. + * + * @param random Random source required for key generation. + * @param bitLength Desired key size in bits. + * + * @return EC key pair of desired size. + */ + public static KeyPair generateEC(final SecureRandom random, final int bitLength) + { + final org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC generator = + new org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC(); + generator.initialize(bitLength, random); + return generator.generateKeyPair(); + } + + + /** + * Generates an EC key pair. + * + * @param random Random source required for key generation. + * @param namedCurve Well-known elliptic curve name that includes domain parameters including key size. + * + * @return EC key pair according to named curve. + */ + public static KeyPair generateEC(final SecureRandom random, final String namedCurve) + { + final org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC generator = + new org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi.EC(); + try { + generator.initialize(new ECNamedCurveGenParameterSpec(namedCurve), random); + } catch (InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException("Invalid EC curve " + namedCurve, e); + } + return generator.generateKeyPair(); + } +}
diff --git a/src/main/java/org/cryptacular/generator/LegacyP12Generator.java b/src/main/java/org/cryptacular/generator/LegacyP12Generator.java new file mode 100644 index 0000000..415fc6c --- /dev/null +++ b/src/main/java/org/cryptacular/generator/LegacyP12Generator.java
@@ -0,0 +1,86 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.oiw.OIWObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.crypto.engines.DESedeEngine; +import org.bouncycastle.crypto.engines.RC2Engine; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.operator.OutputEncryptor; +import org.bouncycastle.pkcs.bc.BcPKCS12PBEOutputEncryptorBuilder; +import org.cryptacular.spec.DigestSpec; + +/** + * Generates PKCS12 containers using DES3+SHA1 for private keys and 40-bit RC2+SHA1 for encrypted data. + * These algorithms are considered unsecure by today's standards (2024), but are needed for interoperability + * in some cases. Importing a keypair into the Mac keychain is a notable use case. + * + * @author Marvin S. Addison + */ +public class LegacyP12Generator extends AbstractP12Generator +{ + /** Number of hashing rounds. */ + private final int iterations; + + /** Key encryptor builder. */ + private final BcPKCS12PBEOutputEncryptorBuilder keyEncryptorBuilder; + + /** Data encryptor builder. */ + private final BcPKCS12PBEOutputEncryptorBuilder dataEncryptorBuilder; + + + /** + * Creates a new instance that encrypts with 1024 rounds of hashing. + */ + public LegacyP12Generator() + { + this(1024); + } + + /** + * Creates a new instance that encrypts with the given number of hashing rounds. + * + * @param iterations Number of hashing rounds. + */ + public LegacyP12Generator(final int iterations) + { + this.iterations = iterations; + this.keyEncryptorBuilder = new BcPKCS12PBEOutputEncryptorBuilder( + PKCSObjectIdentifiers.pbeWithSHAAnd3_KeyTripleDES_CBC, CBCBlockCipher.newInstance(new DESedeEngine())) + .setIterationCount(iterations); + this.dataEncryptorBuilder = new BcPKCS12PBEOutputEncryptorBuilder( + PKCSObjectIdentifiers.pbeWithSHAAnd40BitRC2_CBC, CBCBlockCipher.newInstance(new RC2Engine())) + .setIterationCount(iterations); + } + + @Override + public int getIterations() + { + return iterations; + } + + @Override + protected ASN1ObjectIdentifier getDigestAlgorithmId() + { + return OIWObjectIdentifiers.idSHA1; + } + + @Override + protected DigestSpec getDigestSpec() + { + return new DigestSpec("SHA1"); + } + + @Override + protected OutputEncryptor keyOutputEncryptor(final char[] password) + { + return keyEncryptorBuilder.build(password); + } + + @Override + protected OutputEncryptor dataOutputEncryptor(final char[] password) + { + return dataEncryptorBuilder.build(password); + } +}
diff --git a/src/main/java/org/cryptacular/generator/LimitException.java b/src/main/java/org/cryptacular/generator/LimitException.java new file mode 100644 index 0000000..a4d98b5 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/LimitException.java
@@ -0,0 +1,22 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +/** + * Runtime exception that describes a condition where some fundamental limit imposed by the implementation or + * specification of a generator has been exceeded. + * + * @author Middleware Services + */ +public class LimitException extends RuntimeException +{ + + /** + * Creates a new instance with the given error description. + * + * @param message Error message. + */ + public LimitException(final String message) + { + super(message); + } +}
diff --git a/src/main/java/org/cryptacular/generator/Nonce.java b/src/main/java/org/cryptacular/generator/Nonce.java new file mode 100644 index 0000000..e51c6ff --- /dev/null +++ b/src/main/java/org/cryptacular/generator/Nonce.java
@@ -0,0 +1,25 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +/** + * Nonce generation strategy. + * + * @author Middleware Services + */ +public interface Nonce +{ + + /** + * Generates a nonce value. + * + * @return Nonce bytes. + * + * @throws LimitException When a limit imposed by the nonce generation strategy, if any, is exceeded. + */ + byte[] generate() + throws LimitException; + + + /** @return Length in bytes of generated nonce values. */ + int getLength(); +}
diff --git a/src/main/java/org/cryptacular/generator/P12Generator.java b/src/main/java/org/cryptacular/generator/P12Generator.java new file mode 100644 index 0000000..262bc56 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/P12Generator.java
@@ -0,0 +1,39 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import org.bouncycastle.pkcs.PKCS12PfxPdu; + +/** + * Provides a simple interface for generating PKCS12 containers. + * + * @author Marvin S. Addison + */ +public interface P12Generator +{ + /** + * Generates a PKCS12 container object that contains the given private key and certificates. + * + * @param password PKCS12 encryption password. This secret is also used to encrypt the inner private key. + * @param key Private key. + * @param certificates One or more certificates. If more than one certificate is provided, the first is taken as the + * end-entity certificate. + * + * @return Bouncy Castle PKCS12 container object. + */ + PKCS12PfxPdu generate(char[] password, PrivateKey key, X509Certificate... certificates); + + /** + * Generates a PKCS12 container object that contains the given private key and certificates with the given alias. + * + * @param password PKCS12 encryption password. This secret is also used to encrypt the inner private key. + * @param key Private key. + * @param alias Keystore alias. + * @param certificates One or more certificates. If more than one certificate is provided, the first is taken as the + * end-entity certificate. + * + * @return Bouncy Castle PKCS12 container object. + */ + PKCS12PfxPdu generate(char[] password, PrivateKey key, String alias, X509Certificate... certificates); +}
diff --git a/src/main/java/org/cryptacular/generator/RandomIdGenerator.java b/src/main/java/org/cryptacular/generator/RandomIdGenerator.java new file mode 100644 index 0000000..aa77cdc --- /dev/null +++ b/src/main/java/org/cryptacular/generator/RandomIdGenerator.java
@@ -0,0 +1,72 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.security.SecureRandom; + +/** + * Generates random identifiers with an alphanumeric character set by default. + * + * @author Middleware Services + */ +public class RandomIdGenerator implements IdGenerator +{ + + /** Default character set. */ + public static final String DEFAULT_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + /** Size of generated identifiers. */ + private final int length; + + /** Identifier character set. */ + private final String charset; + + /** Source of randomness. */ + private final SecureRandom secureRandom; + + /** + * Creates a new instance with the default character set. + * + * @param length Number of characters in generated identifiers. + */ + public RandomIdGenerator(final int length) + { + this(length, DEFAULT_CHARSET); + } + + + /** + * Creates a new instance with a defined character set. + * + * @param length Number of characters in generated identifiers. + * @param charset Character set. + */ + public RandomIdGenerator(final int length, final String charset) + { + if (length < 1) { + throw new IllegalArgumentException("Length must be positive"); + } + this.length = length; + if (charset == null || charset.length() < 2 || charset.length() > 128) { + throw new IllegalArgumentException("Charset length must be in the range 2 - 128"); + } + this.charset = charset; + secureRandom = new SecureRandom(); + // Call nextBytes to force seeding via default process + secureRandom.nextBytes(new byte[1]); + } + + + @Override + public String generate() + { + final StringBuilder id = new StringBuilder(length); + final byte[] output = new byte[length]; + secureRandom.nextBytes(output); + int index; + for (int i = 0; i < output.length && id.length() < length; i++) { + index = 0x7F & output[i]; + id.append(charset.charAt(index % charset.length())); + } + return id.toString(); + } +}
diff --git a/src/main/java/org/cryptacular/generator/SecretKeyGenerator.java b/src/main/java/org/cryptacular/generator/SecretKeyGenerator.java new file mode 100644 index 0000000..8e6681b --- /dev/null +++ b/src/main/java/org/cryptacular/generator/SecretKeyGenerator.java
@@ -0,0 +1,69 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.security.SecureRandom; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.prng.SP800SecureRandomBuilder; +import org.cryptacular.util.NonceUtil; + +/** + * Factory class with static methods for generating {@link SecretKey}s. + * + * @author Middleware Services + */ +public final class SecretKeyGenerator +{ + + /** Private constructor of static class. */ + private SecretKeyGenerator() {} + + + /** + * Generates a symmetric encryption key whose size is equal to the cipher block size. + * + * @param cipher Cipher with key will be used. + * + * @return Symmetric encryption key. + */ + public static SecretKey generate(final BlockCipher cipher) + { + return generate(cipher.getBlockSize() * 8, cipher); + } + + + /** + * Generates a symmetric encryption key of the given length. + * + * @param bitLength Desired key length in bits. + * @param cipher Cipher with key will be used. + * + * @return Symmetric encryption key. + */ + public static SecretKey generate(final int bitLength, final BlockCipher cipher) + { + // Want as much nonce data as key bits + final byte[] nonce = NonceUtil.randomNonce((bitLength + 7) / 8); + return generate(bitLength, cipher, new SP800SecureRandomBuilder().buildHash(new SHA256Digest(), nonce, false)); + } + + + /** + * Generates a symmetric encryption key of the given length. + * + * @param bitLength Desired key length in bits. + * @param cipher Cipher with key will be used. + * @param random Randomness provider for key generation. + * + * @return Symmetric encryption key. + */ + public static SecretKey generate(final int bitLength, final BlockCipher cipher, final SecureRandom random) + { + // Round up for bit lengths that are not a multiple of 8 + final byte[] key = new byte[(bitLength + 7) / 8]; + random.nextBytes(key); + return new SecretKeySpec(key, cipher.getAlgorithmName()); + } +}
diff --git a/src/main/java/org/cryptacular/generator/TOTPGenerator.java b/src/main/java/org/cryptacular/generator/TOTPGenerator.java new file mode 100644 index 0000000..3443297 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/TOTPGenerator.java
@@ -0,0 +1,140 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import org.bouncycastle.crypto.Digest; +import org.cryptacular.spec.DigestSpec; +import org.cryptacular.spec.Spec; + +/** + * OTP generator component that implements the TOTP scheme described in <a href="https://tools.ietf.org/html/rfc6238"> + * RFC 6238</a>. + * + * @author Middleware Services + */ +public class TOTPGenerator extends AbstractOTPGenerator +{ + + /** Digest algorithm specification. */ + private Spec<Digest> digestSpecification = new DigestSpec("SHA1"); + + /** + * Current system time in seconds since the start of the epoch, 1970-01-01T00:00:00. + * This value is used if and only if it is a non-negative value; otherwise the current system time is used. + */ + private long currentTime = -1; + + /** Reference start time, T0. Default 0, i.e. 1970-01-01T00:00:00. */ + private int startTime; + + /** Time step in seconds, X. Default is 30 seconds. */ + private int timeStep = 30; + + + /** @return Digest algorithm used with the HMAC function. */ + public Spec<Digest> getDigestSpecification() + { + return digestSpecification; + } + + + /** + * Sets the digest algorithm used with the HMAC function. + * + * @param specification SHA-1, SHA-256, or SHA-512 digest specification. + */ + public void setDigestSpecification(final Spec<Digest> specification) + { + if ("SHA1".equalsIgnoreCase(specification.getAlgorithm()) || + "SHA-1".equalsIgnoreCase(specification.getAlgorithm()) || + "SHA256".equalsIgnoreCase(specification.getAlgorithm()) || + "SHA-256".equalsIgnoreCase(specification.getAlgorithm()) || + "SHA512".equalsIgnoreCase(specification.getAlgorithm()) || + "SHA-512".equalsIgnoreCase(specification.getAlgorithm())) { + this.digestSpecification = specification; + return; + } + throw new IllegalArgumentException("Unsupported digest algorithm " + specification); + } + + + /** @return Reference start time. */ + public int getStartTime() + { + return startTime; + } + + + /** + * Sets the reference start time, T0. Default 0, i.e. 1970-01-01T00:00:00. + * + * @param seconds Start time in seconds. + */ + public void setStartTime(final int seconds) + { + this.startTime = seconds; + } + + + /** @return Time step in seconds. */ + public int getTimeStep() + { + return timeStep; + } + + + /** + * Sets the time step, X. + * + * @param seconds Time step in seconds. Default is 30. This value determines the validity window of generated OTP + * values. + */ + public void setTimeStep(final int seconds) + { + this.timeStep = seconds; + } + + + /** + * Generates the OTP given a per-user key. + * + * @param key Per-user key. + * + * @return Integer OTP. + */ + public int generate(final byte[] key) + { + final long t = (currentTime() - startTime) / timeStep; + return generateInternal(key, t); + } + + + @Override + protected Digest getDigest() + { + return digestSpecification.newInstance(); + } + + + /** + * Sets the current time (supports testing). This value is used if and only if it is a non-negative value; otherwise + * the current system time is used. + * + * @param epochSeconds Seconds since the start of the epoch, 1970-01-01T00:00:00. + */ + protected void setCurrentTime(final long epochSeconds) + { + currentTime = epochSeconds; + } + + + /** + * @return Current system time in seconds since the start of epoch, 1970-01-01T00:00:00. + */ + protected long currentTime() + { + if (currentTime >= 0) { + return currentTime; + } + return System.currentTimeMillis() / 1000; + } +}
diff --git a/src/main/java/org/cryptacular/generator/sp80038a/BigIntegerCounterNonce.java b/src/main/java/org/cryptacular/generator/sp80038a/BigIntegerCounterNonce.java new file mode 100644 index 0000000..2df8d0b --- /dev/null +++ b/src/main/java/org/cryptacular/generator/sp80038a/BigIntegerCounterNonce.java
@@ -0,0 +1,71 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator.sp80038a; + +import java.math.BigInteger; +import java.util.Arrays; +import org.cryptacular.generator.LimitException; +import org.cryptacular.generator.Nonce; + +/** + * Uses a {@link BigInteger} to back a counter in order to produce nonces of arbitrary length. + * + * <p>A common use case for this component is creation of IVs for ciphers with 16-byte block size, e.g. AES.</p> + * + * <p>Instances of this class are thread safe.</p> + * + * @author Middleware Services + */ +public class BigIntegerCounterNonce implements Nonce +{ + + /** Counter. */ + private BigInteger counter; + + /** Length of generated counter nonce values in bytes. */ + private final int length; + + + /** + * Creates a new instance with given parameters. + * + * @param counter Initial counter value. + * @param length Maximum length of generated counter values in bytes. + */ + public BigIntegerCounterNonce(final BigInteger counter, final int length) + { + if (length < 1) { + throw new IllegalArgumentException("Length must be positive"); + } + this.length = length; + this.counter = counter; + } + + + @Override + public byte[] generate() + throws LimitException + { + final byte[] value; + synchronized (this) { + counter = counter.add(BigInteger.ONE); + value = counter.toByteArray(); + } + if (value.length > length) { + throw new LimitException("Counter value exceeded max byte length " + length); + } + if (value.length < length) { + final byte[] temp = new byte[length]; + Arrays.fill(temp, (byte) 0); + System.arraycopy(value, 0, temp, temp.length - value.length, value.length); + return temp; + } + return value; + } + + + @Override + public int getLength() + { + return length; + } +}
diff --git a/src/main/java/org/cryptacular/generator/sp80038a/EncryptedNonce.java b/src/main/java/org/cryptacular/generator/sp80038a/EncryptedNonce.java new file mode 100644 index 0000000..60e198b --- /dev/null +++ b/src/main/java/org/cryptacular/generator/sp80038a/EncryptedNonce.java
@@ -0,0 +1,76 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator.sp80038a; + +import javax.crypto.SecretKey; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.cryptacular.generator.LimitException; +import org.cryptacular.generator.Nonce; +import org.cryptacular.spec.Spec; +import org.cryptacular.util.NonceUtil; + +/** + * Nonce generation strategy that produces a random value according to NIST <a href="http://goo.gl/S9z8qF"> + * SP-800-38a</a>, appendix C, method 1 (encrypted nonce), suitable for use with any block cipher mode described in that + * standard except OFB. + * + * <p>Instances of this class are thread safe.</p> + * + * @author Middleware Services + */ +public class EncryptedNonce implements Nonce +{ + + /** Block cipher. */ + private final BlockCipher cipher; + + /** Encryption key. */ + private final SecretKey key; + + + /** + * Creates a new instance. + * + * @param cipherSpec Block cipher specification. + * @param key Symmetric key. + */ + public EncryptedNonce(final Spec<BlockCipher> cipherSpec, final SecretKey key) + { + this(cipherSpec.newInstance(), key); + } + + + /** + * Creates a new instance. + * + * @param cipher Block cipher to use. + * @param key Symmetric key. + */ + public EncryptedNonce(final BlockCipher cipher, final SecretKey key) + { + this.cipher = cipher; + this.key = key; + } + + + @Override + public byte[] generate() + throws LimitException + { + final byte[] result = new byte[cipher.getBlockSize()]; + final byte[] nonce = NonceUtil.randomNonce(result.length); + synchronized (cipher) { + cipher.init(true, new KeyParameter(key.getEncoded())); + cipher.processBlock(nonce, 0, result, 0); + cipher.reset(); + } + return result; + } + + + @Override + public int getLength() + { + return cipher.getBlockSize(); + } +}
diff --git a/src/main/java/org/cryptacular/generator/sp80038a/LongCounterNonce.java b/src/main/java/org/cryptacular/generator/sp80038a/LongCounterNonce.java new file mode 100644 index 0000000..5c47faf --- /dev/null +++ b/src/main/java/org/cryptacular/generator/sp80038a/LongCounterNonce.java
@@ -0,0 +1,56 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator.sp80038a; + +import java.util.concurrent.atomic.AtomicLong; +import org.cryptacular.generator.LimitException; +import org.cryptacular.generator.Nonce; +import org.cryptacular.util.ByteUtil; + +/** + * Simple counter nonce that uses a long integer counter internally and produces 8-byte nonces. Note that this component + * is suitable exclusively for ciphers with block length 8, e.g. Blowfish. + * + * <p>Instances of this class are thread safe.</p> + * + * @author Middleware Services + * @see BigIntegerCounterNonce + */ +public class LongCounterNonce implements Nonce +{ + + /** Counter. */ + private final AtomicLong counter; + + + /** Creates a new instance whose counter values start at 1. */ + public LongCounterNonce() + { + this(0); + } + + + /** + * Creates a new instance whose counter values start above the given value. + * + * @param start Start value. + */ + public LongCounterNonce(final long start) + { + counter = new AtomicLong(start); + } + + + @Override + public byte[] generate() + throws LimitException + { + return ByteUtil.toBytes(counter.incrementAndGet()); + } + + + @Override + public int getLength() + { + return 8; + } +}
diff --git a/src/main/java/org/cryptacular/generator/sp80038a/RBGNonce.java b/src/main/java/org/cryptacular/generator/sp80038a/RBGNonce.java new file mode 100644 index 0000000..4d2e2af --- /dev/null +++ b/src/main/java/org/cryptacular/generator/sp80038a/RBGNonce.java
@@ -0,0 +1,67 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator.sp80038a; + +import org.bouncycastle.crypto.prng.drbg.SP80090DRBG; +import org.cryptacular.generator.LimitException; +import org.cryptacular.generator.Nonce; +import org.cryptacular.util.NonceUtil; + +/** + * Nonce generation strategy that produces a random value according to NIST <a href="http://goo.gl/S9z8qF"> + * SP-800-38a</a>, appendix C, method 2 (random number generator), suitable for use with any block cipher mode described + * in that standard except OFB. + * + * <p>Instances of this class are thread safe.</p> + * + * @author Middleware Services + */ +public class RBGNonce implements Nonce +{ + + /** Length of generated nonces. */ + private final int length; + + /** Random bit generator. */ + private final SP80090DRBG rbg; + + + /** Creates a new instance that produces 16-bytes (128-bits) of random data. */ + public RBGNonce() + { + this(16); + } + + + /** + * Creates a new instance that produces length bytes of random data. + * + * @param length Number of bytes in generated nonce values. + */ + public RBGNonce(final int length) + { + if (length < 1) { + throw new IllegalArgumentException("Length must be positive"); + } + this.length = length; + this.rbg = NonceUtil.newRBG(length); + } + + + @Override + public byte[] generate() + throws LimitException + { + final byte[] random = new byte[length]; + synchronized (rbg) { + rbg.generate(random, null, false); + } + return random; + } + + + @Override + public int getLength() + { + return length; + } +}
diff --git a/src/main/java/org/cryptacular/generator/sp80038d/CounterNonce.java b/src/main/java/org/cryptacular/generator/sp80038d/CounterNonce.java new file mode 100644 index 0000000..1ed9732 --- /dev/null +++ b/src/main/java/org/cryptacular/generator/sp80038d/CounterNonce.java
@@ -0,0 +1,135 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator.sp80038d; + +import java.util.concurrent.atomic.AtomicLong; +import org.cryptacular.generator.LimitException; +import org.cryptacular.generator.Nonce; +import org.cryptacular.util.ByteUtil; + +/** + * Deterministic nonce generation strategy that uses a counter for the invocation field as described in NIST <a + * href="http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf">SP-800-38D</a>, section 8.2.1. The + * invocation part of the sequence is always 64 bits (8 bytes) due to the use of a <code>long</code>, thus the length of + * the nonce is determined by the length of the fixed part: <code>length = 8 + fixed.length</code>. + * + * <p><strong>NOTE:</strong> users of this class are responsible for maintaining the invocation count in order to + * support enforcement of constraints described in section 8.3; namely the following:</p> + * + * <blockquote>The total number of invocations of the authenticated encryption function shall not exceed 2<sup>32</sup>, + * including all IV lengths and all instances of the authenticated encryption function with the given key.</blockquote> + * + * <p>Instances of this class enforce this constraint by considering the nonce length, which determines whether the + * constraint applies, and the invocation count. The invocation count is incremented upon every invocation of {@link + * #generate()} method. The current invocation count is accessible via {@link #getInvocations()}.</p> + * + * <p>Instances of this class are thread safe.</p> + * + * @author Middleware Services + */ +public class CounterNonce implements Nonce +{ + + /** Default nonce getLength is {@value} bytes. */ + public static final int DEFAULT_LENGTH = 12; + + /** + * Maximum invocations is 2<sup>32</sup>. Does not apply to nonces with default getLength, {@value #DEFAULT_LENGTH}. + */ + public static final long MAX_INVOCATIONS = 0xFFFFFFFFL; + + /** Fixed field value. */ + private final byte[] fixed; + + /** Invocation count. */ + private final AtomicLong count; + + + /** + * Creates a new instance. + * + * @param fixed User-defined fixed field value. + * @param invocations Initial invocation count. The invocations field is incremented _before_ use in {@link + * #generate()}. + */ + public CounterNonce(final String fixed, final long invocations) + { + this(ByteUtil.toBytes(fixed), invocations); + } + + + /** + * Creates a new instance. Instances of this method produces nonces of the default length, {@value #DEFAULT_LENGTH}, + * and are not subject to constraints on the number of invocations. + * + * @param fixed User-defined fixed field value. + * @param invocations Initial invocation count. The invocations field is incremented _before_ use in {@link + * #generate()}. + */ + public CounterNonce(final int fixed, final long invocations) + { + this(ByteUtil.toBytes(fixed), invocations); + } + + + /** + * Creates a new instance. + * + * @param fixed User-defined fixed field value. + * @param invocations Initial invocation count. The invocations field is incremented _before_ use in {@link + * #generate()}. + */ + public CounterNonce(final long fixed, final long invocations) + { + this(ByteUtil.toBytes(fixed), invocations); + } + + + /** + * Creates a new instance. + * + * @param fixed User-defined fixed field value. + * @param invocations Initial invocation count. The invocations field is incremented _before_ use in {@link + * #generate()}. + */ + public CounterNonce(final byte[] fixed, final long invocations) + { + if (fixed == null || fixed.length == 0) { + throw new IllegalArgumentException("Fixed part cannot be null or empty."); + } + this.count = new AtomicLong(invocations); + this.fixed = fixed; + } + + + @Override + public byte[] generate() + throws LimitException + { + final byte[] value = new byte[getLength()]; + System.arraycopy(fixed, 0, value, 0, fixed.length); + + final long next = count.incrementAndGet(); + if (value.length != DEFAULT_LENGTH) { + // Enforce constraints described in section 8.3 + if (next > MAX_INVOCATIONS) { + throw new LimitException("Exceeded 2^32 invocations."); + } + } + ByteUtil.toBytes(next, value, fixed.length); + return value; + } + + + @Override + public int getLength() + { + return fixed.length + 8; + } + + + /** @return Current invocation count. */ + public long getInvocations() + { + return count.get(); + } +}
diff --git a/src/main/java/org/cryptacular/generator/sp80038d/RBGNonce.java b/src/main/java/org/cryptacular/generator/sp80038d/RBGNonce.java new file mode 100644 index 0000000..ae9f1fb --- /dev/null +++ b/src/main/java/org/cryptacular/generator/sp80038d/RBGNonce.java
@@ -0,0 +1,117 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator.sp80038d; + +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.prng.drbg.HashSP800DRBG; +import org.bouncycastle.crypto.prng.drbg.SP80090DRBG; +import org.cryptacular.generator.LimitException; +import org.cryptacular.generator.Nonce; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.NonceUtil; + +/** + * RBG-based nonce generation strategy that uses a RBG component to produce values for the invocation field as described + * in NIST <a href="http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf">SP-800-38D</a>, section 8.2.2. + * + * <p><strong>NOTE:</strong> users of this class are responsible for counting number of invocations and enforcing the + * constraints described in section 8.3; namely the following:</p> + * + * <blockquote>The total number of invocations of the authenticated encryption function shall not exceed 2<sup>32</sup>, + * including all IV lengths and all instances of the authenticated encryption function with the given key.</blockquote> + * + * <p>Instances of this class are thread safe.</p> + * + * @author Middleware Services + */ +public class RBGNonce implements Nonce +{ + + /** Fixed field value. */ + private final byte[] fixed; + + /** Number of bytes of random data in invocation field. */ + private final int randomLength; + + /** Random bit generator. */ + private final SP80090DRBG rbg; + + + /** + * Creates a new instance that produces 12-bytes (96-bits) of random data; that is, the fixed field of the nonce is + * null. + */ + public RBGNonce() + { + this(12); + } + + + /** + * Creates a new instance that produces length bytes of random data; that is, the fixed field of the nonce is null. + * + * @param randomLength Number of bytes in the random part of the nonce. MUST be at least 12. + */ + public RBGNonce(final int randomLength) + { + this(null, randomLength); + } + + + /** + * Creates a new instance using the given fixed field value. + * + * @param fixed User-defined fixed field value. + * @param randomLength Number of bytes in the random part of the nonce. MUST be at least 12. + */ + public RBGNonce(final String fixed, final int randomLength) + { + if (randomLength < 12) { + throw new IllegalArgumentException("Must specify at least 12 bytes (96 bits) for random part."); + } + this.randomLength = randomLength; + if (fixed != null) { + this.fixed = ByteUtil.toBytes(fixed); + } else { + this.fixed = new byte[0]; + } + this.rbg = newRBG(this.randomLength, this.fixed); + } + + + @Override + public byte[] generate() + throws LimitException + { + final byte[] random = new byte[randomLength]; + synchronized (rbg) { + rbg.generate(random, null, false); + } + + final byte[] value = new byte[getLength()]; + System.arraycopy(fixed, 0, value, 0, fixed.length); + System.arraycopy(random, 0, value, fixed.length, random.length); + return value; + } + + + @Override + public int getLength() + { + return fixed.length + randomLength; + } + + + /** + * Creates a new DRBG instance. + * + * @param length Length in bits of values produced by DRBG. + * @param domain Domain qualifier. + * + * @return New DRBG instance. + */ + private static SP80090DRBG newRBG(final int length, final byte[] domain) + { + return new HashSP800DRBG( + new SHA256Digest(), length, NonceUtil.randomEntropySource(length), domain, NonceUtil.timestampNonce(8)); + } +}
diff --git a/src/main/java/org/cryptacular/io/ChunkHandler.java b/src/main/java/org/cryptacular/io/ChunkHandler.java new file mode 100644 index 0000000..8e64ab1 --- /dev/null +++ b/src/main/java/org/cryptacular/io/ChunkHandler.java
@@ -0,0 +1,27 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Callback interface that supports arbitrary processing of data chunks read from an input stream. + * + * @author Middleware Services + */ +public interface ChunkHandler +{ + + /** + * Processes the given chunk of data and writes it to the output stream. + * + * @param input Chunk of input data to process. + * @param offset Offset into input array where data to process starts. + * @param count Number of bytes of input data to process. + * @param output Output stream where processed data is written. + * + * @throws IOException On IO errors. + */ + void handle(byte[] input, int offset, int count, OutputStream output) + throws IOException; +}
diff --git a/src/main/java/org/cryptacular/io/ClassPathResource.java b/src/main/java/org/cryptacular/io/ClassPathResource.java new file mode 100644 index 0000000..c3ed037 --- /dev/null +++ b/src/main/java/org/cryptacular/io/ClassPathResource.java
@@ -0,0 +1,58 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.InputStream; + +/** + * Resource that produces a {@link InputStream} from a classpath resource. + * + * @author Middleware Services + */ +public class ClassPathResource implements Resource +{ + + /** Classpath location of resource. */ + private final String classPath; + + /** Class loader used to get input streams on classpath locations. */ + private final ClassLoader classLoader; + + + /** + * Creates a new resource that reads from the given classpath location. <code> + * Thread.currentThread().getContextClassLoader()</code> is used to obtain the class loader used to obtain an input + * stream on the given classpath. + * + * @param path Classpath location. + */ + public ClassPathResource(final String path) + { + this(path, Thread.currentThread().getContextClassLoader()); + } + + + /** + * Creates a new resource that reads from the given classpath location. + * + * @param path Classpath location. + * @param loader Class loader used to obtain an input stream on the given classpath location. + */ + public ClassPathResource(final String path, final ClassLoader loader) + { + // Strip leading / since absolute paths are not supported by + // ClassLoader#getResourceAsStream(...) + if (path.startsWith("/")) { + this.classPath = path.substring(1); + } else { + this.classPath = path; + } + this.classLoader = loader; + } + + + @Override + public InputStream getInputStream() + { + return classLoader.getResourceAsStream(classPath); + } +}
diff --git a/src/main/java/org/cryptacular/io/DecodingInputStream.java b/src/main/java/org/cryptacular/io/DecodingInputStream.java new file mode 100644 index 0000000..0b6794a --- /dev/null +++ b/src/main/java/org/cryptacular/io/DecodingInputStream.java
@@ -0,0 +1,140 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.codec.Base64Decoder; +import org.cryptacular.codec.Decoder; +import org.cryptacular.codec.HexDecoder; + +/** + * Filters read bytes through a {@link Decoder} such that consumers obtain raw (decoded) bytes from read operations. + * + * @author Middleware Services + */ +public class DecodingInputStream extends FilterInputStream +{ + + /** Performs decoding. */ + private final Decoder decoder; + + /** Wraps the input stream to convert bytes to characters. */ + private final InputStreamReader reader; + + /** Holds input bytes as characters. */ + private CharBuffer input; + + /** Receives decoding result. */ + private ByteBuffer output; + + + /** + * Creates a new instance that wraps the given stream and performs decoding using the given encoder component. + * + * @param in Input stream to wrap. + * @param d Decoder that provides on-the-fly decoding. + */ + public DecodingInputStream(final InputStream in, final Decoder d) + { + super(in); + if (d == null) { + throw new IllegalArgumentException("Decoder cannot be null."); + } + decoder = d; + reader = new InputStreamReader(in); + } + + + @Override + public int read() + throws IOException + { + return read(new byte[1]); + } + + + @Override + public int read(final byte[] b) + throws IOException + { + return read(b, 0, b.length); + } + + + @Override + public int read(final byte[] b, final int off, final int len) + throws IOException + { + prepareInputBuffer(len - off); + prepareOutputBuffer(); + if (reader.read(input) < 0) { + decoder.finalize(output); + if (output.position() == 0) { + return -1; + } + } else { + input.flip(); + decoder.decode(input, output); + } + output.flip(); + output.get(b, off, output.limit()); + return output.position(); + } + + + /** + * Creates a new instance that decodes base64 input from the given stream. + * + * @param in Wrapped input stream. + * + * @return Decoding input stream that decodes base64 output. + */ + public static DecodingInputStream base64(final InputStream in) + { + return new DecodingInputStream(in, new Base64Decoder()); + } + + + /** + * Creates a new instance that decodes hexadecimal input from the given stream. + * + * @param in Wrapped input stream. + * + * @return Decoding input stream that decodes hexadecimal output. + */ + public static DecodingInputStream hex(final InputStream in) + { + return new DecodingInputStream(in, new HexDecoder()); + } + + + /** + * Prepares the input buffer to receive the given number of bytes. + * + * @param required Number of bytes required. + */ + private void prepareInputBuffer(final int required) + { + if (input == null || input.capacity() < required) { + input = CharBuffer.allocate(required); + } else { + input.clear(); + } + } + + + /** Prepares the output buffer based on input buffer capacity. */ + private void prepareOutputBuffer() + { + final int required = decoder.outputSize(input.capacity()); + if (output == null || output.capacity() < required) { + output = ByteBuffer.allocate(required); + } else { + output.clear(); + } + } +}
diff --git a/src/main/java/org/cryptacular/io/DirectByteArrayOutputStream.java b/src/main/java/org/cryptacular/io/DirectByteArrayOutputStream.java new file mode 100644 index 0000000..9477f2d --- /dev/null +++ b/src/main/java/org/cryptacular/io/DirectByteArrayOutputStream.java
@@ -0,0 +1,41 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.ByteArrayOutputStream; + +/** + * Extends {@link ByteArrayOutputStream} by allowing direct access to the internal byte buffer. + * + * @author Middleware Services + */ +public class DirectByteArrayOutputStream extends ByteArrayOutputStream +{ + + /** Creates a new instance with a buffer of the default size. */ + public DirectByteArrayOutputStream() + { + super(); + } + + + /** + * Creates a new instance with a buffer of the given initial capacity. + * + * @param capacity Initial capacity of internal buffer. + */ + public DirectByteArrayOutputStream(final int capacity) + { + super(capacity); + } + + + /** + * Gets the internal byte buffer. + * + * @return Internal buffer that holds written bytes. + */ + public byte[] getBuffer() + { + return buf; + } +}
diff --git a/src/main/java/org/cryptacular/io/EncodingOutputStream.java b/src/main/java/org/cryptacular/io/EncodingOutputStream.java new file mode 100644 index 0000000..cc8e985 --- /dev/null +++ b/src/main/java/org/cryptacular/io/EncodingOutputStream.java
@@ -0,0 +1,153 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.codec.Base64Encoder; +import org.cryptacular.codec.Encoder; +import org.cryptacular.codec.HexEncoder; + +/** + * Filters written bytes through an {@link Encoder} such that encoded data is written to the underlying output stream. + * + * @author Middleware Services + */ +public class EncodingOutputStream extends FilterOutputStream +{ + + /** Performs decoding. */ + private final Encoder encoder; + + /** Wraps the output stream to convert characters to bytes. */ + private final OutputStreamWriter writer; + + /** Receives encoding result. */ + private CharBuffer output; + + + /** + * Creates a new instance that wraps the given stream and performs encoding using the given encoder component. + * + * @param out Output stream to wrap. + * @param e Encoder that provides on-the-fly encoding. + */ + public EncodingOutputStream(final OutputStream out, final Encoder e) + { + super(out); + if (e == null) { + throw new IllegalArgumentException("Encoder cannot be null."); + } + encoder = e; + writer = new OutputStreamWriter(out); + } + + + @Override + public void write(final int b) + throws IOException + { + write(new byte[] {(byte) b}); + } + + + @Override + public void write(final byte[] b) + throws IOException + { + write(b, 0, b.length); + } + + + @Override + public void write(final byte[] b, final int off, final int len) + throws IOException + { + final ByteBuffer input = ByteBuffer.wrap(b, off, len); + final int required = encoder.outputSize(len - off); + if (output == null || output.capacity() < required) { + output = CharBuffer.allocate(required); + } else { + output.clear(); + } + encoder.encode(input, output); + output.flip(); + writer.write(output.toString()); + writer.flush(); + } + + + @Override + public void flush() + throws IOException + { + writer.flush(); + } + + + @Override + public void close() + throws IOException + { + if (output == null) { + output = CharBuffer.allocate(8); + } else { + output.clear(); + } + encoder.finalize(output); + output.flip(); + writer.write(output.toString()); + writer.flush(); + writer.close(); + } + + + /** + * Creates a new instance that produces base64 output in the given stream. + * + * <p><strong>NOTE:</strong> there are no line breaks in the output with this version.</p> + * + * @param out Wrapped output stream. + * + * @return Encoding output stream that produces base64 output. + */ + public static EncodingOutputStream base64(final OutputStream out) + { + return base64(out, -1); + } + + + /** + * Creates a new instance that produces base64 output in the given stream. + * + * <p><strong>NOTE:</strong> this version supports output with configurable line breaks.</p> + * + * @param out Wrapped output stream. + * @param lineLength Length of each base64-encoded line in output. A zero or negative value disables line breaks. + * + * @return Encoding output stream that produces base64 output. + */ + public static EncodingOutputStream base64(final OutputStream out, final int lineLength) + { + return new EncodingOutputStream(out, new Base64Encoder(lineLength)); + } + + + /** + * Creates a new instance that produces hexadecimal output in the given stream. + * + * <p><strong>NOTE:</strong> there are no line breaks in the output.</p> + * + * @param out Wrapped output stream. + * + * @return Encoding output stream that produces hexadecimal output. + */ + public static EncodingOutputStream hex(final OutputStream out) + { + return new EncodingOutputStream(out, new HexEncoder()); + } + +}
diff --git a/src/main/java/org/cryptacular/io/FileResource.java b/src/main/java/org/cryptacular/io/FileResource.java new file mode 100644 index 0000000..3c04ea8 --- /dev/null +++ b/src/main/java/org/cryptacular/io/FileResource.java
@@ -0,0 +1,50 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Resource that produces a buffered {@link FileInputStream} from a file. + * + * @author Middleware Services + */ +public class FileResource implements Resource +{ + + /** Underlying file resource. */ + private final File file; + + + /** + * Creates a new file resource. + * + * @param file Non-null file. + */ + public FileResource(final File file) + { + if (file == null) { + throw new IllegalArgumentException("File cannot be null."); + } + this.file = file; + } + + + @Override + public InputStream getInputStream() + throws IOException + { + return new BufferedInputStream(new FileInputStream(file)); + } + + + @Override + public String toString() + { + return file.toString(); + } +}
diff --git a/src/main/java/org/cryptacular/io/Resource.java b/src/main/java/org/cryptacular/io/Resource.java new file mode 100644 index 0000000..9858d62 --- /dev/null +++ b/src/main/java/org/cryptacular/io/Resource.java
@@ -0,0 +1,28 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Resource descriptor that provides a strategy to get an {@link InputStream} to read bytes. + * + * @author Middleware Services + */ +public interface Resource +{ + + /** + * Gets an input stream around the resource. Callers of this method are responsible for resource cleanup; it should be + * sufficient to simply call {@link java.io.InputStream#close()} unless otherwise noted. + * + * <p>Implementers should produce a new instance on every call to this method to provide for thread-safe usage + * patterns on a shared resource.</p> + * + * @return Input stream around underlying resource, e.g. file, remote resource (URI), etc. + * + * @throws IOException On IO errors. + */ + InputStream getInputStream() + throws IOException; +}
diff --git a/src/main/java/org/cryptacular/io/URLResource.java b/src/main/java/org/cryptacular/io/URLResource.java new file mode 100644 index 0000000..31d43dd --- /dev/null +++ b/src/main/java/org/cryptacular/io/URLResource.java
@@ -0,0 +1,47 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +/** + * Describes a (presumably remote) resource accessible via URL. + * + * @author Middleware Services + */ +public class URLResource implements Resource +{ + + /** Location of resource. */ + private final URL url; + + + /** + * Creates a new URL resource. + * + * @param url Non-null URL where resource is located. + */ + public URLResource(final URL url) + { + if (url == null) { + throw new IllegalArgumentException("URL cannot be null."); + } + this.url = url; + } + + + @Override + public InputStream getInputStream() + throws IOException + { + return url.openStream(); + } + + + @Override + public String toString() + { + return url.toString(); + } +}
diff --git a/src/main/java/org/cryptacular/pbe/AbstractEncryptionScheme.java b/src/main/java/org/cryptacular/pbe/AbstractEncryptionScheme.java new file mode 100644 index 0000000..ce8ea81 --- /dev/null +++ b/src/main/java/org/cryptacular/pbe/AbstractEncryptionScheme.java
@@ -0,0 +1,124 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.pbe; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.io.CipherOutputStream; +import org.bouncycastle.util.io.Streams; +import org.cryptacular.CryptoException; + +/** + * Abstract base class for password-based encryption schemes based on salt data and iterated hashing as the basis of the + * key derivation function. + * + * <p>NOTE: Classes derived from this class are not thread safe. In particular, care should be take to prevent multiple + * threads from performing encryption and/or decryption concurrently.</p> + * + * @author Middleware Services + * @version $Revision: 2744 $ + */ +public abstract class AbstractEncryptionScheme implements EncryptionScheme +{ + + /** Cipher used for encryption and decryption. */ + private BufferedBlockCipher cipher; + + /** Cipher initialization parameters. */ + private CipherParameters parameters; + + + @Override + public byte[] encrypt(final byte[] plaintext) + { + cipher.init(true, parameters); + return process(plaintext); + } + + + @Override + public void encrypt(final InputStream in, final OutputStream out) + throws IOException + { + cipher.init(true, parameters); + Streams.pipeAll(in, new CipherOutputStream(out, cipher)); + } + + + @Override + public byte[] decrypt(final byte[] ciphertext) + { + cipher.init(false, parameters); + return process(ciphertext); + } + + + @Override + public void decrypt(final InputStream in, final OutputStream out) + throws IOException + { + cipher.init(false, parameters); + Streams.pipeAll(new CipherInputStream(in, cipher), out); + } + + + @Override + public OutputStream wrap(final boolean encryptionFlag, final OutputStream out) + { + cipher.init(encryptionFlag, parameters); + return new CipherOutputStream(out, cipher); + } + + + /** + * Sets the block cipher used for encryption/decryption. + * + * @param bufferedBlockCipher Buffered block cipher. + */ + protected void setCipher(final BufferedBlockCipher bufferedBlockCipher) + { + if (bufferedBlockCipher == null) { + throw new IllegalArgumentException("Block cipher cannot be null"); + } + this.cipher = bufferedBlockCipher; + } + + + /** + * Sets block cipher initialization parameters. + * + * @param parameters Cipher-specific init params. + */ + protected void setCipherParameters(final CipherParameters parameters) + { + if (parameters == null) { + throw new IllegalArgumentException("Cipher parameters cannot be null"); + } + this.parameters = parameters; + } + + + /** + * Run the given data through the initialized underlying cipher and return the result. + * + * @param input Input data. + * + * @return Result of cipher acting on input. + */ + private byte[] process(final byte[] input) + { + final byte[] output = new byte[cipher.getOutputSize(input.length)]; + int processed = cipher.processBytes(input, 0, input.length, output, 0); + try { + processed += cipher.doFinal(output, processed); + } catch (InvalidCipherTextException e) { + throw new CryptoException("Cipher error", e); + } + return Arrays.copyOfRange(output, 0, processed); + } +}
diff --git a/src/main/java/org/cryptacular/pbe/EncryptionScheme.java b/src/main/java/org/cryptacular/pbe/EncryptionScheme.java new file mode 100644 index 0000000..38bff71 --- /dev/null +++ b/src/main/java/org/cryptacular/pbe/EncryptionScheme.java
@@ -0,0 +1,73 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.pbe; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Describes a password-based encryption scheme. + * + * @author Middleware Services + * @version $Revision: 2744 $ + */ +public interface EncryptionScheme +{ + + /** + * Encrypts the given plaintext bytes into a byte array of ciphertext using the derived key. + * + * @param plaintext Input plaintext bytes. + * + * @return Ciphertext resulting from plaintext encryption. + */ + byte[] encrypt(byte[] plaintext); + + + /** + * Encrypts the data in the given plaintext input stream into ciphertext in the output stream. Use {@link + * org.cryptacular.io.EncodingOutputStream} to produce ciphertext bytes that encoded as a string data in the output + * stream. + * + * @param in Input stream of plaintext. + * @param out Output stream of ciphertext. + * + * @throws IOException On stream read/write errors. + */ + void encrypt(InputStream in, OutputStream out) + throws IOException; + + + /** + * Decrypts the given ciphertext into plaintext using the derived key. + * + * @param ciphertext Input ciphertext bytes. + * + * @return Plaintext resulting from ciphertext decryption. + */ + byte[] decrypt(byte[] ciphertext); + + + /** + * Decrypts ciphertext from an input stream into plaintext in the output stream. Use {@link + * org.cryptacular.io.DecodingInputStream} to handle input ciphertext encoded as string data. + * + * @param in Input stream of ciphertext. + * @param out Output stream of plaintext. + * + * @throws IOException On stream read/write errors. + */ + void decrypt(InputStream in, OutputStream out) + throws IOException; + + + /*** + * Wraps an output stream with one that performs encryption or decryption on the fly. + * + * @param encryptionFlag True to signal encryption, false for decryption. + * @param out Output stream to wrap + * + * @return Wrapped output steam. + */ + OutputStream wrap(boolean encryptionFlag, OutputStream out); +}
diff --git a/src/main/java/org/cryptacular/pbe/OpenSSLAlgorithm.java b/src/main/java/org/cryptacular/pbe/OpenSSLAlgorithm.java new file mode 100644 index 0000000..7f83da1 --- /dev/null +++ b/src/main/java/org/cryptacular/pbe/OpenSSLAlgorithm.java
@@ -0,0 +1,85 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.pbe; + +import org.cryptacular.spec.KeyedBlockCipherSpec; + +/** + * Describes block ciphers allowed with the OpenSSL password-based encryption scheme. + * + * @author Middleware Services + */ +public enum OpenSSLAlgorithm { + + /** AES-128 in CBC mode. */ + AES_128_CBC("aes-128-cbc", new KeyedBlockCipherSpec("AES", "CBC", "PKCS5", 128)), + + /** AES-192 in CBC mode. */ + AES_192_CBC("aes-192-cbc", new KeyedBlockCipherSpec("AES", "CBC", "PKCS5", 192)), + + /** AES-256 in CBC mode. */ + AES_256_CBC("aes-256-cbc", new KeyedBlockCipherSpec("AES", "CBC", "PKCS5", 256)), + + /** DES in CBC mode. */ + DES_CBC("des-cbc", new KeyedBlockCipherSpec("DES", "CBC", "PKCS5", 64)), + + /** Triple DES in CBC mode. */ + DES_EDE3_CBC("des-ede3-cbc", new KeyedBlockCipherSpec("DESede", "CBC", "PKCS5", 192)), + + /** 128-bit RC2 in CBC mode. */ + RC2_CBC("rc2-cbc", new KeyedBlockCipherSpec("RC2", "CBC", "PKCS5", 128)), + + /** 40-bit RC2 in CBC mode. */ + RC2_40_CBC("rc2-40-cbc", new KeyedBlockCipherSpec("RC2", "CBC", "PKCS5", 40)), + + /** 64-bit RC2 in CBC mode. */ + RC2_64_CBC("rc2-64-cbc", new KeyedBlockCipherSpec("RC2", "CBC", "PKCS5", 64)); + + + /** Algorithm identifier, e.g. aes-128-cbc. */ + private final String algorithmId; + + /** Cipher algorithm specification. */ + private final KeyedBlockCipherSpec cipherSpec; + + /** + * Creates a new instance with given parameters. + * + * @param algId Algorithm identifier, e.g. aes-128-cbc. + * @param cipherSpec Block cipher specification that corresponds to algorithm ID. + */ + OpenSSLAlgorithm(final String algId, final KeyedBlockCipherSpec cipherSpec) + { + this.algorithmId = algId; + this.cipherSpec = cipherSpec; + } + + /** @return OpenSSL algorithm identifier, e.g. aes-128-cbc. */ + public String getAlgorithmId() + { + return algorithmId; + } + + /** @return Cipher algorithm specification. */ + public KeyedBlockCipherSpec getCipherSpec() + { + return cipherSpec; + } + + + /** + * Converts an OID to the corresponding algorithm specification. + * + * @param algorithmId Algorithm OID. + * + * @return Algorithm spec. + */ + public static OpenSSLAlgorithm fromAlgorithmId(final String algorithmId) + { + for (OpenSSLAlgorithm alg : values()) { + if (alg.getAlgorithmId().equalsIgnoreCase(algorithmId)) { + return alg; + } + } + throw new IllegalArgumentException("Unsupported algorithm " + algorithmId); + } +}
diff --git a/src/main/java/org/cryptacular/pbe/OpenSSLEncryptionScheme.java b/src/main/java/org/cryptacular/pbe/OpenSSLEncryptionScheme.java new file mode 100644 index 0000000..c3f5fb2 --- /dev/null +++ b/src/main/java/org/cryptacular/pbe/OpenSSLEncryptionScheme.java
@@ -0,0 +1,60 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.pbe; + +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.PBEParametersGenerator; +import org.bouncycastle.crypto.generators.OpenSSLPBEParametersGenerator; +import org.bouncycastle.crypto.params.ParametersWithIV; + +/** + * Password-based encryption scheme used by OpenSSL for encrypting private keys. + * + * @author Middleware Services + * @version $Revision: 2744 $ + */ +public class OpenSSLEncryptionScheme extends AbstractEncryptionScheme +{ + + /** + * Creates a new instance using the given parameters. + * + * @param cipher Buffered block cipher algorithm. + * @param salt Salt data for key generation function. + * @param keyBitLength Size of derived keys in bits. + * @param password Password used to derive key. + */ + public OpenSSLEncryptionScheme( + final BufferedBlockCipher cipher, + final byte[] salt, + final int keyBitLength, + final char[] password) + { + final OpenSSLPBEParametersGenerator generator = new OpenSSLPBEParametersGenerator(); + generator.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password), salt); + setCipher(cipher); + setCipherParameters(generator.generateDerivedParameters(keyBitLength)); + } + + + /** + * Creates a new instance from an algorithm and salt data. + * + * @param algorithm OpenSSL key encryption algorithm. + * @param iv Explicit IV; first 8 bytes also used for salt in PBE key generation. + * @param password Password used to derive key. + */ + public OpenSSLEncryptionScheme(final OpenSSLAlgorithm algorithm, final byte[] iv, final char[] password) + { + byte[] salt = iv; + if (iv.length > 8) { + salt = new byte[8]; + System.arraycopy(iv, 0, salt, 0, 8); + } + + final OpenSSLPBEParametersGenerator generator = new OpenSSLPBEParametersGenerator(); + generator.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password), salt); + setCipher(algorithm.getCipherSpec().newInstance()); + setCipherParameters( + new ParametersWithIV(generator.generateDerivedParameters(algorithm.getCipherSpec().getKeyLength()), iv)); + } +}
diff --git a/src/main/java/org/cryptacular/pbe/PBES1Algorithm.java b/src/main/java/org/cryptacular/pbe/PBES1Algorithm.java new file mode 100644 index 0000000..2c33685 --- /dev/null +++ b/src/main/java/org/cryptacular/pbe/PBES1Algorithm.java
@@ -0,0 +1,116 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.pbe; + +import org.cryptacular.spec.BufferedBlockCipherSpec; +import org.cryptacular.spec.DigestSpec; + +/** + * Password-based encryption algorithms defined in PKCS#5 for PBES1 scheme. + * + * @author Middleware Services + * @version $Revision: 2745 $ + */ +public enum PBES1Algorithm { + + /** PBES1 encryption method with MD2 hash and DES CBC cipher. */ + PbeWithMD2AndDES_CBC( + "1.2.840.113549.1.5.1", + new BufferedBlockCipherSpec("DES", "CBC", "PKCS5"), + new DigestSpec("MD2")), + + /** PBES1 encryption method with MD2 hash and RC2 CBC cipher. */ + PbeWithMD2AndRC2_CBC( + "1.2.840.113549.1.5.4", + new BufferedBlockCipherSpec("RC2", "CBC", "PKCS5"), + new DigestSpec("MD2")), + + /** PBES1 encryption method with MD5 hash and DES CBC cipher. */ + PbeWithMD5AndDES_CBC( + "1.2.840.113549.1.5.3", + new BufferedBlockCipherSpec("DES", "CBC", "PKCS5"), + new DigestSpec("MD5")), + + /** PBES1 encryption method with MD5 hash and RC2 CBC cipher. */ + PbeWithMD5AndRC2_CBC( + "1.2.840.113549.1.5.6", + new BufferedBlockCipherSpec("RC2", "CBC", "PKCS5"), + new DigestSpec("MD5")), + + /** PBES1 encryption method with SHA1 hash and DES CBC cipher. */ + PbeWithSHA1AndDES_CBC( + "1.2.840.113549.1.5.10", + new BufferedBlockCipherSpec("DES", "CBC", "PKCS5"), + new DigestSpec("SHA1")), + + /** PBES1 encryption method with SHA1 hash and RC2 CBC cipher. */ + PbeWithSHA1AndRC2_CBC( + "1.2.840.113549.1.5.11", + new BufferedBlockCipherSpec("RC2", "CBC", "PKCS5"), + new DigestSpec("SHA1")); + + + /** Algorithm identifier OID. */ + private final String oid; + + /** Cipher algorithm specification. */ + private final BufferedBlockCipherSpec cipherSpec; + + /** Pseudorandom function digest specification. */ + private final DigestSpec digestSpec; + + + /** + * Creates a new instance with given parameters. + * + * @param id Algorithm OID. + * @param cipherSpec Cipher algorithm specification. + * @param digestSpec Digest specification used for pseudorandom function. + */ + PBES1Algorithm(final String id, final BufferedBlockCipherSpec cipherSpec, final DigestSpec digestSpec) + { + this.oid = id; + this.cipherSpec = cipherSpec; + this.digestSpec = digestSpec; + } + + + /** + * Gets the PBE algorithm for the given object identifier. + * + * @param oid PBE algorithm OID. + * + * @return Algorithm whose identifier equals given value. + * + * @throws IllegalArgumentException If no matching algorithm found. + */ + public static PBES1Algorithm fromOid(final String oid) + { + for (PBES1Algorithm a : PBES1Algorithm.values()) { + if (a.getOid().equals(oid)) { + return a; + } + } + throw new IllegalArgumentException("Unknown PBES1Algorithm for OID " + oid); + } + + + /** @return the oid */ + public String getOid() + { + return oid; + } + + + /** @return Cipher algorithm specification. */ + public BufferedBlockCipherSpec getCipherSpec() + { + return cipherSpec; + } + + + /** @return Digest algorithm. */ + public DigestSpec getDigestSpec() + { + return digestSpec; + } +}
diff --git a/src/main/java/org/cryptacular/pbe/PBES1EncryptionScheme.java b/src/main/java/org/cryptacular/pbe/PBES1EncryptionScheme.java new file mode 100644 index 0000000..9d7555a --- /dev/null +++ b/src/main/java/org/cryptacular/pbe/PBES1EncryptionScheme.java
@@ -0,0 +1,40 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.pbe; + +import org.bouncycastle.asn1.pkcs.PBEParameter; +import org.bouncycastle.crypto.PBEParametersGenerator; +import org.bouncycastle.crypto.generators.PKCS5S1ParametersGenerator; + +/** + * Implements the PBES1 encryption scheme defined in PKCS#5v2. + * + * @author Middleware Services + * @version $Revision: 2744 $ + */ +public class PBES1EncryptionScheme extends AbstractEncryptionScheme +{ + + /** Number of bits in derived key. */ + public static final int KEY_LENGTH = 64; + + /** Number of bits IV. */ + public static final int IV_LENGTH = 64; + + + /** + * Creates a new instance with the given parameters. + * + * @param alg Describes hash/algorithm pair suitable for PBES1 scheme. + * @param params Key generation function salt and iteration count. + * @param password Password used to derive key. + */ + public PBES1EncryptionScheme(final PBES1Algorithm alg, final PBEParameter params, final char[] password) + { + final byte[] salt = params.getSalt(); + final int iterations = params.getIterationCount().intValue(); + final PKCS5S1ParametersGenerator generator = new PKCS5S1ParametersGenerator(alg.getDigestSpec().newInstance()); + generator.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password), salt, iterations); + setCipher(alg.getCipherSpec().newInstance()); + setCipherParameters(generator.generateDerivedParameters(KEY_LENGTH, IV_LENGTH)); + } +}
diff --git a/src/main/java/org/cryptacular/pbe/PBES2Algorithm.java b/src/main/java/org/cryptacular/pbe/PBES2Algorithm.java new file mode 100644 index 0000000..1fc0671 --- /dev/null +++ b/src/main/java/org/cryptacular/pbe/PBES2Algorithm.java
@@ -0,0 +1,113 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.pbe; + +import org.cryptacular.spec.BufferedBlockCipherSpec; + +/** + * Supported password-based encryption algorithms for PKCS#5 PBES2 encryption scheme. The ciphers mentioned in PKCS#5 + * are supported as well as others in common use or of presumed value. + * + * @author Middleware Services + * @version $Revision: 2745 $ + */ +public enum PBES2Algorithm { + + /** DES CBC cipher. */ + DES("1.3.14.3.2.7", new BufferedBlockCipherSpec("DES", "CBC", "PKCS5"), 64, 64), + + /** 3-DES CBC cipher. */ + DESede("1.2.840.113549.3.7", new BufferedBlockCipherSpec("DESede", "CBC", "PKCS5"), 64, 192), + + /** RC2 CBC cipher. */ + RC2("1.2.840.113549.3.2", new BufferedBlockCipherSpec("RC2", "CBC", "PKCS5"), 0, 64), + + /** RC5 CBC cipher. */ + RC5("1.2.840.113549.3.9", new BufferedBlockCipherSpec("RC5", "CBC", "PKCS5"), 0, 128), + + /** AES-128 CBC cipher. */ + AES128("2.16.840.1.101.3.4.1.2", new BufferedBlockCipherSpec("AES", "CBC", "PKCS5"), 128, 128), + + /** AES-192 CBC cipher. */ + AES192("2.16.840.1.101.3.4.1.22", new BufferedBlockCipherSpec("AES", "CBC", "PKCS5"), 128, 192), + + /** AES-256 CBC cipher. */ + AES256("2.16.840.1.101.3.4.1.42", new BufferedBlockCipherSpec("AES", "CBC", "PKCS5"), 128, 256); + + + /** Algorithm identifier OID. */ + private final String oid; + + /** Cipher algorithm specification. */ + private final BufferedBlockCipherSpec cipherSpec; + + /** Cipher block size in bits. */ + private final int blockSize; + + /** Cipher key size in bits. */ + private final int keySize; + + + /** + * Creates a new instance with given parameters. + * + * @param id Algorithm OID. + * @param cipherSpec Cipher algorithm specification. + * @param cipherBlockSize Block cipher size in bits. + * @param keySizeBits Size of derived key in bits to be used with cipher. + */ + PBES2Algorithm( + final String id, final BufferedBlockCipherSpec cipherSpec, final int cipherBlockSize, final int keySizeBits) + { + this.oid = id; + this.cipherSpec = cipherSpec; + this.blockSize = cipherBlockSize; + this.keySize = keySizeBits; + } + + + /** + * Gets the PBE algorithm for the given object identifier. + * + * @param oid PBE algorithm OID. + * + * @return Algorithm whose identifier equals given value. + * + * @throws IllegalArgumentException If no matching algorithm found. + */ + public static PBES2Algorithm fromOid(final String oid) + { + for (PBES2Algorithm a : PBES2Algorithm.values()) { + if (a.getOid().equals(oid)) { + return a; + } + } + throw new IllegalArgumentException("Unknown PBES1Algorithm for OID " + oid); + } + + + /** @return the oid */ + public String getOid() + { + return oid; + } + + + /** @return Cipher algorithm specification. */ + public BufferedBlockCipherSpec getCipherSpec() + { + return cipherSpec; + } + + + /** @return Cipher block size. */ + public int getBlockSize() + { + return blockSize; + } + + /** @return Size of derived key in bits or -1 if algorithm does not define a key size. */ + public int getKeySize() + { + return keySize; + } +}
diff --git a/src/main/java/org/cryptacular/pbe/PBES2EncryptionScheme.java b/src/main/java/org/cryptacular/pbe/PBES2EncryptionScheme.java new file mode 100644 index 0000000..da8ad18 --- /dev/null +++ b/src/main/java/org/cryptacular/pbe/PBES2EncryptionScheme.java
@@ -0,0 +1,136 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.pbe; + +import java.util.HashMap; +import java.util.Map; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.asn1.pkcs.PBES2Parameters; +import org.bouncycastle.asn1.pkcs.PBKDF2Params; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.PBEParametersGenerator; +import org.bouncycastle.crypto.engines.RC532Engine; +import org.bouncycastle.crypto.engines.RC564Engine; +import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.bouncycastle.crypto.params.RC2Parameters; +import org.bouncycastle.crypto.params.RC5Parameters; +import org.cryptacular.spec.DigestSpec; + +/** + * Implements the PBES2 encryption scheme defined in PKCS#5v2. + * + * @author Middleware Services + * @version $Revision: 2744 $ + */ +public class PBES2EncryptionScheme extends AbstractEncryptionScheme +{ + /** Map of HMAC algorithm identifiers to digest specifications. */ + private static final Map<ASN1ObjectIdentifier, DigestSpec> HMAC_ID_TO_DIGEST_SPEC_MAP = new HashMap<>(); + + /** Size of derived key in bits. */ + private int keyLength; + + + static + { + HMAC_ID_TO_DIGEST_SPEC_MAP.put(PKCSObjectIdentifiers.id_hmacWithSHA1, new DigestSpec("SHA1")); + HMAC_ID_TO_DIGEST_SPEC_MAP.put(PKCSObjectIdentifiers.id_hmacWithSHA256, new DigestSpec("SHA256")); + HMAC_ID_TO_DIGEST_SPEC_MAP.put(PKCSObjectIdentifiers.id_hmacWithSHA512, new DigestSpec("SHA512")); + HMAC_ID_TO_DIGEST_SPEC_MAP.put(NISTObjectIdentifiers.id_hmacWithSHA3_256, new DigestSpec("SHA3", 256)); + HMAC_ID_TO_DIGEST_SPEC_MAP.put(NISTObjectIdentifiers.id_hmacWithSHA3_384, new DigestSpec("SHA3", 384)); + HMAC_ID_TO_DIGEST_SPEC_MAP.put(NISTObjectIdentifiers.id_hmacWithSHA3_512, new DigestSpec("SHA3", 512)); + } + + + /** + * Creates a new instance with the given parameters. + * + * @param params PBES2 parameters describing the key derivation function and encryption scheme. + * @param password Password used to derive key. + */ + public PBES2EncryptionScheme(final PBES2Parameters params, final char[] password) + { + final PBKDF2Params kdfParams = PBKDF2Params.getInstance(params.getKeyDerivationFunc().getParameters()); + final byte[] salt = kdfParams.getSalt(); + final int iterations = kdfParams.getIterationCount().intValue(); + if (kdfParams.getKeyLength() != null) { + keyLength = kdfParams.getKeyLength().intValue() * 8; + } + final DigestSpec digestSpec = HMAC_ID_TO_DIGEST_SPEC_MAP.get(kdfParams.getPrf().getAlgorithm()); + if (digestSpec == null) { + throw new IllegalArgumentException("Unsupported PBKDF2 PRF HMAC algorithm"); + } + final PKCS5S2ParametersGenerator generator = new PKCS5S2ParametersGenerator(digestSpec.newInstance()); + generator.init(PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password), salt, iterations); + initCipher(generator, params.getEncryptionScheme()); + } + + + /** + * Initializes the block cipher and sets up its initialization parameters. + * + * @param generator Derived key generator. + * @param scheme PKCS#5 encryption scheme. + */ + private void initCipher( + final PKCS5S2ParametersGenerator generator, + final org.bouncycastle.asn1.pkcs.EncryptionScheme scheme) + { + final PBES2Algorithm alg = PBES2Algorithm.fromOid(scheme.getAlgorithm().getId()); + if (keyLength == 0) { + keyLength = alg.getKeySize(); + } + + byte[] iv = null; + CipherParameters cipherParameters = generator.generateDerivedParameters(keyLength); + switch (alg) { + + case RC2: + setCipher(alg.getCipherSpec().newInstance()); + + final ASN1Sequence rc2Params = ASN1Sequence.getInstance(scheme.getParameters()); + if (rc2Params.size() > 1) { + cipherParameters = new RC2Parameters( + ((KeyParameter) cipherParameters).getKey(), + ASN1Integer.getInstance(rc2Params.getObjectAt(0)).getValue().intValue()); + iv = ASN1OctetString.getInstance(rc2Params.getObjectAt(0)).getOctets(); + } + break; + + case RC5: + + final ASN1Sequence rc5Params = ASN1Sequence.getInstance(scheme.getParameters()); + final int rounds = ASN1Integer.getInstance(rc5Params.getObjectAt(1)).getValue().intValue(); + final int blockSize = ASN1Integer.getInstance(rc5Params.getObjectAt(2)).getValue().intValue(); + if (blockSize == 64) { + setCipher(new PaddedBufferedBlockCipher(new CBCBlockCipher(new RC564Engine()), new PKCS7Padding())); + } else if (blockSize == 32) { + setCipher(new PaddedBufferedBlockCipher(new CBCBlockCipher(new RC532Engine()), new PKCS7Padding())); + } else { + throw new IllegalArgumentException("Invalid RC5 block size: " + blockSize); + } + cipherParameters = new RC5Parameters(((KeyParameter) cipherParameters).getKey(), rounds); + if (rc5Params.size() > 3) { + iv = ASN1OctetString.getInstance(rc5Params.getObjectAt(3)).getOctets(); + } + break; + + default: + setCipher(alg.getCipherSpec().newInstance()); + iv = ASN1OctetString.getInstance(scheme.getParameters()).getOctets(); + } + if (iv != null) { + cipherParameters = new ParametersWithIV(cipherParameters, iv); + } + setCipherParameters(cipherParameters); + } +}
diff --git a/src/main/java/org/cryptacular/spec/AEADBlockCipherSpec.java b/src/main/java/org/cryptacular/spec/AEADBlockCipherSpec.java new file mode 100644 index 0000000..f163118 --- /dev/null +++ b/src/main/java/org/cryptacular/spec/AEADBlockCipherSpec.java
@@ -0,0 +1,121 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.spec; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.CCMBlockCipher; +import org.bouncycastle.crypto.modes.EAXBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.modes.OCBBlockCipher; + +/** + * Describes an AEAD block cipher in terms of a (algorithm, mode) tuple and provides a facility to create a new instance + * of the cipher via the {@link #newInstance()} method. + * + * @author Middleware Services + * @version $Revision: 2744 $ + */ +public class AEADBlockCipherSpec implements Spec<AEADBlockCipher> +{ + + /** String specification format, <code>algorithm/mode</code>. */ + public static final Pattern FORMAT = Pattern.compile("(?<alg>[A-Za-z0-9_-]+)/(?<mode>\\w+)"); + + /** Cipher algorithm. */ + private final String algorithm; + + /** Cipher mode, e.g. GCM, CCM. */ + private final String mode; + + + /** + * Creates a new instance from a cipher algorithm and mode. + * + * @param algName Cipher algorithm name. + * @param cipherMode Cipher mode, e.g. GCM, CCM. + */ + public AEADBlockCipherSpec(final String algName, final String cipherMode) + { + this.algorithm = algName; + this.mode = cipherMode; + } + + + @Override + public String getAlgorithm() + { + return algorithm; + } + + + /** + * Gets the cipher mode. + * + * @return Cipher mode, e.g. CBC, OFB. + */ + public String getMode() + { + return mode; + } + + + /** + * Creates a new AEAD block cipher from the specification in this instance. + * + * @return New AEAD block cipher instance. + */ + @Override + public AEADBlockCipher newInstance() + { + final BlockCipher blockCipher = new BlockCipherSpec(algorithm).newInstance(); + final AEADBlockCipher aeadBlockCipher; + switch (mode) { + + case "GCM": + aeadBlockCipher = new GCMBlockCipher(blockCipher); + break; + + case "CCM": + aeadBlockCipher = new CCMBlockCipher(blockCipher); + break; + + case "OCB": + aeadBlockCipher = new OCBBlockCipher(blockCipher, new BlockCipherSpec(algorithm).newInstance()); + break; + + case "EAX": + aeadBlockCipher = new EAXBlockCipher(blockCipher); + break; + + default: + throw new IllegalStateException("Unsupported mode " + mode); + } + return aeadBlockCipher; + } + + + @Override + public String toString() + { + return algorithm + '/' + mode; + } + + + /** + * Parses a string representation of a AEAD block cipher specification into an instance of this class. + * + * @param specification AEAD block cipher specification of the form <code>algorithm/mode</code>. + * + * @return Buffered block cipher specification instance. + */ + public static AEADBlockCipherSpec parse(final String specification) + { + final Matcher m = FORMAT.matcher(specification); + if (!m.matches()) { + throw new IllegalArgumentException("Invalid specification " + specification); + } + return new AEADBlockCipherSpec(m.group("alg"), m.group("mode")); + } +}
diff --git a/src/main/java/org/cryptacular/spec/BlockCipherSpec.java b/src/main/java/org/cryptacular/spec/BlockCipherSpec.java new file mode 100644 index 0000000..05e82f4 --- /dev/null +++ b/src/main/java/org/cryptacular/spec/BlockCipherSpec.java
@@ -0,0 +1,107 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.spec; + +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.engines.BlowfishEngine; +import org.bouncycastle.crypto.engines.CAST5Engine; +import org.bouncycastle.crypto.engines.CAST6Engine; +import org.bouncycastle.crypto.engines.CamelliaEngine; +import org.bouncycastle.crypto.engines.DESEngine; +import org.bouncycastle.crypto.engines.DESedeEngine; +import org.bouncycastle.crypto.engines.GOST28147Engine; +import org.bouncycastle.crypto.engines.NoekeonEngine; +import org.bouncycastle.crypto.engines.RC2Engine; +import org.bouncycastle.crypto.engines.RC564Engine; +import org.bouncycastle.crypto.engines.RC6Engine; +import org.bouncycastle.crypto.engines.SEEDEngine; +import org.bouncycastle.crypto.engines.SerpentEngine; +import org.bouncycastle.crypto.engines.SkipjackEngine; +import org.bouncycastle.crypto.engines.TEAEngine; +import org.bouncycastle.crypto.engines.TwofishEngine; +import org.bouncycastle.crypto.engines.XTEAEngine; + +/** + * Block cipher specification. + * + * @author Middleware Services + */ +public class BlockCipherSpec implements Spec<BlockCipher> +{ + + /** Cipher algorithm. */ + private final String algorithm; + + + /** + * Creates a new instance that describes the given block cipher algorithm. + * + * @param algName Block cipher algorithm. + */ + public BlockCipherSpec(final String algName) + { + this.algorithm = algName; + } + + + @Override + public String getAlgorithm() + { + return algorithm; + } + + + @Override + public BlockCipher newInstance() + { + final BlockCipher cipher; + if ("AES".equalsIgnoreCase(algorithm)) { + cipher = new AESEngine(); + } else if ("Blowfish".equalsIgnoreCase(algorithm)) { + cipher = new BlowfishEngine(); + } else if ("Camellia".equalsIgnoreCase(algorithm)) { + cipher = new CamelliaEngine(); + } else if ("CAST5".equalsIgnoreCase(algorithm)) { + cipher = new CAST5Engine(); + } else if ("CAST6".equalsIgnoreCase(algorithm)) { + cipher = new CAST6Engine(); + } else if ("DES".equalsIgnoreCase(algorithm)) { + cipher = new DESEngine(); + } else if ("DESede".equalsIgnoreCase(algorithm) || "DES3".equalsIgnoreCase(algorithm)) { + cipher = new DESedeEngine(); + } else if ("GOST".equalsIgnoreCase(algorithm) || "GOST28147".equals(algorithm)) { + cipher = new GOST28147Engine(); + } else if ("Noekeon".equalsIgnoreCase(algorithm)) { + cipher = new NoekeonEngine(); + } else if ("RC2".equalsIgnoreCase(algorithm)) { + cipher = new RC2Engine(); + } else if ("RC5".equalsIgnoreCase(algorithm)) { + cipher = new RC564Engine(); + } else if ("RC6".equalsIgnoreCase(algorithm)) { + cipher = new RC6Engine(); + } else if ("SEED".equalsIgnoreCase(algorithm)) { + cipher = new SEEDEngine(); + } else if ("Serpent".equalsIgnoreCase(algorithm)) { + cipher = new SerpentEngine(); + } else if ("Skipjack".equalsIgnoreCase(algorithm)) { + cipher = new SkipjackEngine(); + } else if ("TEA".equalsIgnoreCase(algorithm)) { + cipher = new TEAEngine(); + } else if ("Twofish".equalsIgnoreCase(algorithm)) { + cipher = new TwofishEngine(); + } else if ("XTEA".equalsIgnoreCase(algorithm)) { + cipher = new XTEAEngine(); + } else { + throw new IllegalStateException("Unsupported cipher algorithm " + algorithm); + } + return cipher; + } + + + @Override + public String toString() + { + return algorithm; + } + +}
diff --git a/src/main/java/org/cryptacular/spec/BufferedBlockCipherSpec.java b/src/main/java/org/cryptacular/spec/BufferedBlockCipherSpec.java new file mode 100644 index 0000000..c1e5366 --- /dev/null +++ b/src/main/java/org/cryptacular/spec/BufferedBlockCipherSpec.java
@@ -0,0 +1,220 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.spec; + +import java.io.Serializable; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.BufferedBlockCipher; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.modes.CFBBlockCipher; +import org.bouncycastle.crypto.modes.OFBBlockCipher; +import org.bouncycastle.crypto.paddings.BlockCipherPadding; +import org.bouncycastle.crypto.paddings.ISO10126d2Padding; +import org.bouncycastle.crypto.paddings.ISO7816d4Padding; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.paddings.TBCPadding; +import org.bouncycastle.crypto.paddings.X923Padding; +import org.bouncycastle.crypto.paddings.ZeroBytePadding; + +/** + * Describes a block cipher in terms of a (algorithm, mode, padding) tuple and provides a facility to create a new + * instance of the cipher via the {@link #newInstance()} method. + * + * @author Middleware Services + * @version $Revision: 2744 $ + */ +public class BufferedBlockCipherSpec implements Spec<BufferedBlockCipher>, Serializable +{ + + /** String specification format, <code>algorithm/mode/padding</code>. */ + public static final Pattern FORMAT = Pattern.compile("(?<alg>[A-Za-z0-9_-]+)/(?<mode>\\w+)/(?<padding>\\w+)"); + + /** serialVersionUID. */ + private static final long serialVersionUID = 2900237827716742605L; + + /** Cipher algorithm. */ + private final String algorithm; + + /** Cipher mode, e.g. CBC, OFB. */ + private final String mode; + + /** Cipher padding scheme, e.g. PKCS5Padding. */ + private final String padding; + + + /** + * Creates a new instance from an algorithm name. + * + * @param algName Cipher algorithm name. + */ + public BufferedBlockCipherSpec(final String algName) + { + this(algName, null, null); + } + + + /** + * Creates a new instance from a cipher algorithm and mode. + * + * @param algName Cipher algorithm name. + * @param cipherMode Cipher mode. + */ + public BufferedBlockCipherSpec(final String algName, final String cipherMode) + { + this(algName, cipherMode, null); + } + + + /** + * Creates a new instance from the given cipher specifications. + * + * @param algName Cipher algorithm name. + * @param cipherMode Cipher mode. + * @param cipherPadding Cipher padding scheme algorithm. + */ + public BufferedBlockCipherSpec(final String algName, final String cipherMode, final String cipherPadding) + { + this.algorithm = algName; + this.mode = cipherMode; + this.padding = cipherPadding; + } + + + @Override + public String getAlgorithm() + { + return algorithm; + } + + + /** + * Gets the cipher mode. + * + * @return Cipher mode, e.g. CBC, OFB. + */ + public String getMode() + { + return mode; + } + + + /** + * Gets the cipher padding scheme. + * + * @return Padding scheme algorithm, e.g. PKCS5Padding. The following names are equivalent for no padding: NULL, + * Zero, None. + */ + public String getPadding() + { + return padding; + } + + + /** + * Gets the simple block cipher specification corresponding to this instance. + * + * @return Simple block cipher specification. + */ + public BlockCipherSpec getBlockCipherSpec() + { + return new BlockCipherSpec(this.algorithm); + } + + + /** + * Creates a new buffered block cipher from the specification in this instance. + * + * @return New buffered block cipher instance. + */ + @Override + public BufferedBlockCipher newInstance() + { + BlockCipher cipher = getBlockCipherSpec().newInstance(); + + switch (mode) { + + case "CBC": + cipher = new CBCBlockCipher(cipher); + break; + + case "OFB": + cipher = new OFBBlockCipher(cipher, cipher.getBlockSize()); + break; + + case "CFB": + cipher = new CFBBlockCipher(cipher, cipher.getBlockSize()); + break; + + default: + break; + } + + if (padding != null) { + return new PaddedBufferedBlockCipher(cipher, getPadding(padding)); + } + return new BufferedBlockCipher(cipher); + } + + + @Override + public String toString() + { + return algorithm + '/' + mode + '/' + padding; + } + + + /** + * Parses a string representation of a buffered block cipher specification into an instance of this class. + * + * @param specification Block cipher specification of the form <code>algorithm/mode/padding</code>. + * + * @return Buffered block cipher specification instance. + */ + public static BufferedBlockCipherSpec parse(final String specification) + { + final Matcher m = FORMAT.matcher(specification); + if (!m.matches()) { + throw new IllegalArgumentException("Invalid specification " + specification); + } + return new BufferedBlockCipherSpec(m.group("alg"), m.group("mode"), m.group("padding")); + } + + + /** + * Gets an instance of block cipher padding from a padding name string. + * + * @param padding Name of padding algorithm. + * + * @return Block cipher padding instance. + */ + private static BlockCipherPadding getPadding(final String padding) + { + final String name; + final int pIndex = padding.indexOf("Padding"); + if (pIndex > -1) { + name = padding.substring(0, pIndex); + } else { + name = padding; + } + + final BlockCipherPadding blockCipherPadding; + if ("ISO7816d4".equalsIgnoreCase(name) || "ISO7816".equalsIgnoreCase(name)) { + blockCipherPadding = new ISO7816d4Padding(); + } else if ("ISO10126".equalsIgnoreCase(name) || "ISO10126-2".equalsIgnoreCase(name)) { + blockCipherPadding = new ISO10126d2Padding(); + } else if ("PKCS7".equalsIgnoreCase(name) || "PKCS5".equalsIgnoreCase(name)) { + blockCipherPadding = new PKCS7Padding(); + } else if ("TBC".equalsIgnoreCase(name)) { + blockCipherPadding = new TBCPadding(); + } else if ("X923".equalsIgnoreCase(name)) { + blockCipherPadding = new X923Padding(); + } else if ("NULL".equalsIgnoreCase(name) || "Zero".equalsIgnoreCase(name) || "None".equalsIgnoreCase(name)) { + blockCipherPadding = new ZeroBytePadding(); + } else { + throw new IllegalArgumentException("Invalid padding " + padding); + } + return blockCipherPadding; + } +}
diff --git a/src/main/java/org/cryptacular/spec/CodecSpec.java b/src/main/java/org/cryptacular/spec/CodecSpec.java new file mode 100644 index 0000000..5c99141 --- /dev/null +++ b/src/main/java/org/cryptacular/spec/CodecSpec.java
@@ -0,0 +1,100 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.spec; + + +import org.cryptacular.codec.Base32Codec; +import org.cryptacular.codec.Base64Codec; +import org.cryptacular.codec.Codec; +import org.cryptacular.codec.HexCodec; + +/** + * Describes a string-to-byte encoding provides a means to create a new instance of the coed via the {@link + * #newInstance()} method. + * + * @author Middleware Services + */ +public class CodecSpec implements Spec<Codec> +{ + + /** Hexadecimal encoding specification. */ + public static final CodecSpec HEX = new CodecSpec("Hex"); + + /** Lowercase hexadecimal encoding specification. */ + public static final CodecSpec HEX_LOWER = new CodecSpec("Hex-Lower"); + + /** Uppercase hexadecimal encoding specification. */ + public static final CodecSpec HEX_UPPER = new CodecSpec("Hex-Upper"); + + /** Base32 encoding specification. */ + public static final CodecSpec BASE32 = new CodecSpec("Base32"); + + /** Unpadded base32 encoding specification. */ + public static final CodecSpec BASE32_UNPADDED = new CodecSpec("Base32-Unpadded"); + + /** Base64 encoding specification. */ + public static final CodecSpec BASE64 = new CodecSpec("Base64"); + + /** URL-safe base64 encoding specification. */ + public static final CodecSpec BASE64_URLSAFE = new CodecSpec("Base64-URLSafe"); + + /** Unpadded base64 encoding specification. */ + public static final CodecSpec BASE64_UNPADDED = new CodecSpec("Base64-Unpadded"); + + + /** Name of encoding, e.g. "Hex, "Base64". */ + private final String encoding; + + + /** + * Creates a new instance of the given encoding. + * + * @param encoding Name of encoding. + */ + public CodecSpec(final String encoding) + { + if (encoding == null) { + throw new IllegalArgumentException("Encoding cannot be null."); + } + this.encoding = encoding; + } + + + /** @return The name of the encoding, e.g. "Hex", "Base32", "Base64". */ + @Override + public String getAlgorithm() + { + return encoding; + } + + + @Override + public Codec newInstance() + { + final Codec codec; + if ("Hex".equalsIgnoreCase(encoding) || "Hex-Lower".equalsIgnoreCase(encoding)) { + codec = new HexCodec(); + } else if ("Hex-Upper".equalsIgnoreCase(encoding)) { + codec = new HexCodec(true); + } else if ("Base32".equalsIgnoreCase(encoding) || "Base-32".equalsIgnoreCase(encoding)) { + codec = new Base32Codec(); + } else if ("Base32-Unpadded".equalsIgnoreCase(encoding)) { + codec = new Base32Codec("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", true); + } else if ("Base64".equalsIgnoreCase(encoding) || "Base-64".equalsIgnoreCase(encoding)) { + codec = new Base64Codec(); + } else if ("Base64-URLSafe".equalsIgnoreCase(encoding)) { + codec = new Base64Codec("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"); + } else if ("Base64-Unpadded".equalsIgnoreCase(encoding)) { + codec = new Base64Codec("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", false); + } else { + throw new IllegalArgumentException("Invalid encoding."); + } + return codec; + } + + + @Override + public String toString() + { + return encoding; + } +}
diff --git a/src/main/java/org/cryptacular/spec/DigestSpec.java b/src/main/java/org/cryptacular/spec/DigestSpec.java new file mode 100644 index 0000000..4d0c4b7 --- /dev/null +++ b/src/main/java/org/cryptacular/spec/DigestSpec.java
@@ -0,0 +1,139 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.spec; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.GOST3411Digest; +import org.bouncycastle.crypto.digests.MD2Digest; +import org.bouncycastle.crypto.digests.MD4Digest; +import org.bouncycastle.crypto.digests.MD5Digest; +import org.bouncycastle.crypto.digests.RIPEMD128Digest; +import org.bouncycastle.crypto.digests.RIPEMD160Digest; +import org.bouncycastle.crypto.digests.RIPEMD256Digest; +import org.bouncycastle.crypto.digests.RIPEMD320Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA224Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA384Digest; +import org.bouncycastle.crypto.digests.SHA3Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.bouncycastle.crypto.digests.TigerDigest; +import org.bouncycastle.crypto.digests.WhirlpoolDigest; + +/** + * Describes a message digest function by name and provides a means to create a new instance of the digest via the + * {@link #newInstance()} method. + * + * @author Middleware Services + */ +public class DigestSpec implements Spec<Digest> +{ + + /** Digest algorithm name. */ + private final String algorithm; + + /** Requested size of variable-size hash algorithms, e.g. SHA-3. -1 for hashes with fixed size outputs. */ + private final int size; + + + /** + * Creates a new instance from the given algorithm name. + * + * @param algName Digest algorithm name. + */ + public DigestSpec(final String algName) + { + if (algName == null) { + throw new IllegalArgumentException("Algorithm name is required."); + } + this.algorithm = algName; + this.size = -1; + } + + + /** + * Constructor for digests that have variable output size, e.g. SHA3. + * + * @param algName Digest algorithm name. + * @param digestSize Size of resultant digest in bits. + */ + public DigestSpec(final String algName, final int digestSize) + { + if (algName == null) { + throw new IllegalArgumentException("Algorithm name is required."); + } + this.algorithm = algName; + if (digestSize < 0) { + throw new IllegalArgumentException("Digest size must be positive."); + } + this.size = digestSize; + } + + + @Override + public String getAlgorithm() + { + return algorithm; + } + + + /** @return Size of digest output in bytes, or -1 if the digest does not support variable size output. */ + public int getSize() + { + return size; + } + + + /** + * Creates a new digest instance. + * + * @return Digest instance. + */ + @Override + public Digest newInstance() + { + final Digest digest; + if ("GOST3411".equalsIgnoreCase(algorithm)) { + digest = new GOST3411Digest(); + } else if ("MD2".equalsIgnoreCase(algorithm)) { + digest = new MD2Digest(); + } else if ("MD4".equalsIgnoreCase(algorithm)) { + digest = new MD4Digest(); + } else if ("MD5".equalsIgnoreCase(algorithm)) { + digest = new MD5Digest(); + } else if ("RIPEMD128".equalsIgnoreCase(algorithm) || "RIPEMD-128".equalsIgnoreCase(algorithm)) { + digest = new RIPEMD128Digest(); + } else if ("RIPEMD160".equalsIgnoreCase(algorithm) || "RIPEMD-160".equalsIgnoreCase(algorithm)) { + digest = new RIPEMD160Digest(); + } else if ("RIPEMD256".equalsIgnoreCase(algorithm) || "RIPEMD-256".equalsIgnoreCase(algorithm)) { + digest = new RIPEMD256Digest(); + } else if ("RIPEMD320".equalsIgnoreCase(algorithm) || "RIPEMD-320".equalsIgnoreCase(algorithm)) { + digest = new RIPEMD320Digest(); + } else if ("SHA1".equalsIgnoreCase(algorithm) || "SHA-1".equalsIgnoreCase(algorithm)) { + digest = new SHA1Digest(); + } else if ("SHA224".equalsIgnoreCase(algorithm) || "SHA-224".equalsIgnoreCase(algorithm)) { + digest = new SHA224Digest(); + } else if ("SHA256".equalsIgnoreCase(algorithm) || "SHA-256".equalsIgnoreCase(algorithm)) { + digest = new SHA256Digest(); + } else if ("SHA384".equalsIgnoreCase(algorithm) || "SHA-384".equalsIgnoreCase(algorithm)) { + digest = new SHA384Digest(); + } else if ("SHA512".equalsIgnoreCase(algorithm) || "SHA-512".equalsIgnoreCase(algorithm)) { + digest = new SHA512Digest(); + } else if ("SHA3".equalsIgnoreCase(algorithm) || "SHA-3".equalsIgnoreCase(algorithm)) { + digest = new SHA3Digest(size); + } else if ("Tiger".equalsIgnoreCase(algorithm)) { + digest = new TigerDigest(); + } else if ("Whirlpool".equalsIgnoreCase(algorithm)) { + digest = new WhirlpoolDigest(); + } else { + throw new IllegalStateException("Unsupported digest algorithm " + algorithm); + } + return digest; + } + + + @Override + public String toString() + { + return algorithm; + } +}
diff --git a/src/main/java/org/cryptacular/spec/KeyedBlockCipherSpec.java b/src/main/java/org/cryptacular/spec/KeyedBlockCipherSpec.java new file mode 100644 index 0000000..15c42eb --- /dev/null +++ b/src/main/java/org/cryptacular/spec/KeyedBlockCipherSpec.java
@@ -0,0 +1,50 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.spec; + +/** + * Describes a block cipher algorithm with a known key size. + * + * @author Middleware Services + */ +public class KeyedBlockCipherSpec extends BufferedBlockCipherSpec +{ + + /** serialVersionUID. */ + private static final long serialVersionUID = -7623413862633189082L; + + /** Key length in bits. */ + private final int keyLength; + + + /** + * Creates a new instance from the given cipher specifications. + * + * @param algName Cipher algorithm name. + * @param cipherMode Cipher mode. + * @param cipherPadding Cipher padding scheme algorithm. + * @param keyBitLength Key length in bits. + */ + public KeyedBlockCipherSpec( + final String algName, + final String cipherMode, + final String cipherPadding, + final int keyBitLength) + { + super(algName, cipherMode, cipherPadding); + if (keyBitLength < 0) { + throw new IllegalArgumentException("Key length must be non-negative"); + } + this.keyLength = keyBitLength; + } + + + /** + * Gets the cipher key length in bits. + * + * @return Key length in bits. + */ + public int getKeyLength() + { + return keyLength; + } +}
diff --git a/src/main/java/org/cryptacular/spec/Spec.java b/src/main/java/org/cryptacular/spec/Spec.java new file mode 100644 index 0000000..e3307b0 --- /dev/null +++ b/src/main/java/org/cryptacular/spec/Spec.java
@@ -0,0 +1,24 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.spec; + +/** + * Specification for a cryptographic primitive, e.g. block cipher, message digest, etc. + * + * @param <T> Type of specification. + * + * @author Middleware Services + */ +public interface Spec<T> +{ + + /** @return Cryptographic algorithm name. */ + String getAlgorithm(); + + + /** + * Creates a new instance of the cryptographic primitive described by this specification. + * + * @return New instance of cryptographic primitive. + */ + T newInstance(); +}
diff --git a/src/main/java/org/cryptacular/spec/StreamCipherSpec.java b/src/main/java/org/cryptacular/spec/StreamCipherSpec.java new file mode 100644 index 0000000..55e9296 --- /dev/null +++ b/src/main/java/org/cryptacular/spec/StreamCipherSpec.java
@@ -0,0 +1,75 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.spec; + +import org.bouncycastle.crypto.StreamCipher; +import org.bouncycastle.crypto.engines.Grain128Engine; +import org.bouncycastle.crypto.engines.HC128Engine; +import org.bouncycastle.crypto.engines.HC256Engine; +import org.bouncycastle.crypto.engines.ISAACEngine; +import org.bouncycastle.crypto.engines.RC4Engine; +import org.bouncycastle.crypto.engines.Salsa20Engine; +import org.bouncycastle.crypto.engines.VMPCEngine; + +/** + * Stream cipher specification. + * + * @author Middleware Services + */ +public class StreamCipherSpec implements Spec<StreamCipher> +{ + + /** Cipher algorithm. */ + private final String algorithm; + + + /** + * Creates a new instance that describes the given stream cipher algorithm. + * + * @param algName Stream cipher algorithm. + */ + public StreamCipherSpec(final String algName) + { + this.algorithm = algName; + } + + + @Override + public String getAlgorithm() + { + return algorithm; + } + + + @Override + public StreamCipher newInstance() + { + final StreamCipher cipher; + if ("Grainv1".equalsIgnoreCase(algorithm) || "Grain-v1".equalsIgnoreCase(algorithm)) { + cipher = new ISAACEngine(); + } else if ("Grain128".equalsIgnoreCase(algorithm) || "Grain-128".equalsIgnoreCase(algorithm)) { + cipher = new Grain128Engine(); + } else if ("ISAAC".equalsIgnoreCase(algorithm)) { + cipher = new ISAACEngine(); + } else if ("HC128".equalsIgnoreCase(algorithm)) { + cipher = new HC128Engine(); + } else if ("HC256".equalsIgnoreCase(algorithm)) { + cipher = new HC256Engine(); + } else if ("RC4".equalsIgnoreCase(algorithm)) { + cipher = new RC4Engine(); + } else if ("Salsa20".equalsIgnoreCase(algorithm)) { + cipher = new Salsa20Engine(); + } else if ("VMPC".equalsIgnoreCase(algorithm)) { + cipher = new VMPCEngine(); + } else { + throw new IllegalStateException("Unsupported cipher algorithm " + algorithm); + } + return cipher; + } + + + @Override + public String toString() + { + return algorithm; + } +}
diff --git a/src/main/java/org/cryptacular/util/ByteUtil.java b/src/main/java/org/cryptacular/util/ByteUtil.java new file mode 100644 index 0000000..7525c8a --- /dev/null +++ b/src/main/java/org/cryptacular/util/ByteUtil.java
@@ -0,0 +1,291 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import org.cryptacular.StreamException; + +/** + * Utilities for working with bytes. + * + * @author Middleware Services + */ +public final class ByteUtil +{ + + /** Default character set for bytes is UTF-8. */ + public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + /** ASCII character set. */ + public static final Charset ASCII_CHARSET = StandardCharsets.US_ASCII; + + /** Private constructor of utilty class. */ + private ByteUtil() {} + + + /** + * Converts the big-endian representation of a 32-bit integer to the equivalent integer value. + * + * @param data 4-byte array in big-endian format. + * + * @return Integer value. + */ + public static int toInt(final byte[] data) + { + return (data[0] << 24) | ((data[1] & 0xff) << 16) | ((data[2] & 0xff) << 8) | (data[3] & 0xff); + } + + + /** + * Converts an unsigned byte into an integer. + * + * @param unsigned Unsigned byte. + * + * @return Integer value. + */ + public static int toInt(final byte unsigned) + { + return 0x000000FF & unsigned; + } + + + /** + * Reads 4-bytes from the input stream and converts to a 32-bit integer. + * + * @param in Stream from which to read 4 bytes. + * + * @return Integer value. + * + * @throws StreamException on stream IO errors. + */ + public static int readInt(final InputStream in) throws StreamException + { + try { + return (in.read() << 24) | ((in.read() & 0xff) << 16) | ((in.read() & 0xff) << 8) | (in.read() & 0xff); + } catch (IOException e) { + throw new StreamException(e); + } + } + + + /** + * Converts the big-endian representation of a 64-bit integer to the equivalent long value. + * + * @param data 8-byte array in big-endian format. + * + * @return Long integer value. + */ + public static long toLong(final byte[] data) + { + return + ((long) data[0] << 56) | (((long) data[1] & 0xff) << 48) | + (((long) data[2] & 0xff) << 40) | (((long) data[3] & 0xff) << 32) | + (((long) data[4] & 0xff) << 24) | (((long) data[5] & 0xff) << 16) | + (((long) data[6] & 0xff) << 8) | ((long) data[7] & 0xff); + } + + + /** + * Reads 8-bytes from the input stream and converts to a 64-bit long integer. + * + * @param in Stream from which to read 8 bytes. + * + * @return Long integer value. + * + * @throws StreamException on stream IO errors. + */ + public static long readLong(final InputStream in) throws StreamException + { + try { + return + ((long) in.read() << 56) | (((long) in.read() & 0xff) << 48) | + (((long) in.read() & 0xff) << 40) | (((long) in.read() & 0xff) << 32) | + (((long) in.read() & 0xff) << 24) | (((long) in.read() & 0xff) << 16) | + (((long) in.read() & 0xff) << 8) | ((long) in.read() & 0xff); + } catch (IOException e) { + throw new StreamException(e); + } + } + + + /** + * Converts an integer into a 4-byte big endian array. + * + * @param value Integer value to convert. + * + * @return 4-byte big-endian representation of integer value. + */ + public static byte[] toBytes(final int value) + { + final byte[] bytes = new byte[4]; + toBytes(value, bytes, 0); + return bytes; + } + + + /** + * Converts an integer into a 4-byte big endian array. + * + * @param value Integer value to convert. + * @param output Array into which bytes are placed. + * @param offset Offset into output array at which output bytes start. + */ + public static void toBytes(final int value, final byte[] output, final int offset) + { + int shift = 24; + for (int i = 0; i < 4; i++) { + output[offset + i] = (byte) (value >> shift); + shift -= 8; + } + } + + + /** + * Converts a long integer into an 8-byte big endian array. + * + * @param value Long integer value to convert. + * + * @return 8-byte big-endian representation of long integer value. + */ + public static byte[] toBytes(final long value) + { + final byte[] bytes = new byte[8]; + toBytes(value, bytes, 0); + return bytes; + } + + + /** + * Converts an integer into an 8-byte big endian array. + * + * @param value Long value to convert. + * @param output Array into which bytes are placed. + * @param offset Offset into output array at which output bytes start. + */ + public static void toBytes(final long value, final byte[] output, final int offset) + { + int shift = 56; + for (int i = 0; i < 8; i++) { + output[offset + i] = (byte) (value >> shift); + shift -= 8; + } + } + + + /** + * Converts a byte array into a string in the UTF-8 character set. + * + * @param bytes Byte array to convert. + * + * @return UTF-8 string representation of bytes. + */ + public static String toString(final byte[] bytes) + { + return new String(bytes, DEFAULT_CHARSET); + } + + + /** + * Converts a portion of a byte array into a string in the UTF-8 character set. + * + * @param bytes Byte array to convert. + * @param offset Offset into byte array where string content begins. + * @param length Total number of bytes to convert. + * + * @return UTF-8 string representation of bytes. + */ + public static String toString(final byte[] bytes, final int offset, final int length) + { + return new String(bytes, offset, length, DEFAULT_CHARSET); + } + + + /** + * Converts a byte buffer into a string in the UTF-8 character set. + * + * @param buffer Byte buffer to convert. + * + * @return UTF-8 string representation of bytes. + */ + public static String toString(final ByteBuffer buffer) + { + return toCharBuffer(buffer).toString(); + } + + /** + * Converts a byte buffer into a character buffer. + * + * @param buffer Byte buffer to convert. + * + * @return Character buffer containing UTF-8 string representation of bytes. + */ + public static CharBuffer toCharBuffer(final ByteBuffer buffer) + { + return DEFAULT_CHARSET.decode(buffer); + } + + + /** + * Converts a string into bytes in the UTF-8 character set. + * + * @param s String to convert. + * + * @return Byte buffer containing byte representation of string. + */ + public static ByteBuffer toByteBuffer(final String s) + { + return DEFAULT_CHARSET.encode(CharBuffer.wrap(s)); + } + + + /** + * Converts a string into bytes in the UTF-8 character set. + * + * @param s String to convert. + * + * @return Byte array containing byte representation of string. + */ + public static byte[] toBytes(final String s) + { + return s.getBytes(DEFAULT_CHARSET); + } + + + /** + * Converts an integer into an unsigned byte. All bits above 1 byte are truncated. + * + * @param b Integer value. + * + * @return Unsigned byte as a byte. + */ + public static byte toUnsignedByte(final int b) + { + return (byte) (0x000000FF & b); + } + + + /** + * Converts a byte buffer into a byte array. + * + * @param buffer Byte buffer to convert. + * + * @return Byte array corresponding to bytes of buffer from current position to limit. + */ + public static byte[] toArray(final ByteBuffer buffer) + { + final int size = buffer.limit() - buffer.position(); + if (buffer.hasArray() && size == buffer.capacity()) { + return buffer.array(); + } + + final byte[] array = new byte[size]; + buffer.get(array); + return array; + } + + +}
diff --git a/src/main/java/org/cryptacular/util/CertUtil.java b/src/main/java/org/cryptacular/util/CertUtil.java new file mode 100644 index 0000000..5f70797 --- /dev/null +++ b/src/main/java/org/cryptacular/util/CertUtil.java
@@ -0,0 +1,712 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.RFC4519Style; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.GeneralNamesBuilder; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.cert.CertIOException; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.cryptacular.EncodingException; +import org.cryptacular.StreamException; +import org.cryptacular.codec.Base64Encoder; +import org.cryptacular.x509.ExtensionReader; +import org.cryptacular.x509.GeneralNameType; +import org.cryptacular.x509.KeyUsageBits; +import org.cryptacular.x509.dn.NameReader; +import org.cryptacular.x509.dn.StandardAttributeType; + +/** + * Utility class providing convenience methods for common operations on X.509 certificates. + * + * @author Middleware Services + */ +public final class CertUtil +{ + + /** Private constructor of utility class. */ + private CertUtil() {} + + + /** + * Gets the common name attribute (CN) of the certificate subject distinguished name. + * + * @param cert Certificate to examine. + * + * @return Subject CN or null if no CN attribute is defined in the subject DN. + * + * @throws EncodingException on cert field extraction. + */ + public static String subjectCN(final X509Certificate cert) throws EncodingException + { + return new NameReader(cert).readSubject().getValue(StandardAttributeType.CommonName); + } + + + /** + * Gets all subject alternative names defined on the given certificate. + * + * @param cert X.509 certificate to examine. + * + * @return List of subject alternative names or null if no subject alt names are defined. + * + * @throws EncodingException on cert field extraction. + */ + public static GeneralNames subjectAltNames(final X509Certificate cert) throws EncodingException + { + return new ExtensionReader(cert).readSubjectAlternativeName(); + } + + + /** + * Gets all subject alternative names of the given type(s) on the given cert. + * + * @param cert X.509 certificate to examine. + * @param types One or more subject alternative name types to fetch. + * + * @return List of subject alternative names of the matching type(s) or null if none found. + * + * @throws EncodingException on cert field extraction. + */ + public static GeneralNames subjectAltNames(final X509Certificate cert, final GeneralNameType... types) + throws EncodingException + { + final GeneralNamesBuilder builder = new GeneralNamesBuilder(); + final GeneralNames altNames = subjectAltNames(cert); + if (altNames != null) { + for (GeneralName name : altNames.getNames()) { + for (GeneralNameType type : types) { + if (type.ordinal() == name.getTagNo()) { + builder.addName(name); + } + } + } + } + + final GeneralNames names = builder.build(); + if (names.getNames().length == 0) { + return null; + } + return names; + } + + + /** + * Gets a list of all subject names defined for the given certificate. The list includes the first common name (CN) + * specified in the subject distinguished name (if defined) and all subject alternative names. + * + * @param cert X.509 certificate to examine. + * + * @return List of subject names. + * + * @throws EncodingException on cert field extraction. + */ + public static List<String> subjectNames(final X509Certificate cert) throws EncodingException + { + final List<String> names = new ArrayList<>(); + final String cn = subjectCN(cert); + if (cn != null) { + names.add(cn); + } + + final GeneralNames altNames = subjectAltNames(cert); + if (altNames == null) { + return names; + } + for (GeneralName name : altNames.getNames()) { + names.add(name.getName().toString()); + } + return names; + } + + + /** + * Gets a list of subject names defined for the given certificate. The list includes the first common name (CN) + * specified in the subject distinguished name (if defined) and all subject alternative names of the given type. + * + * @param cert X.509 certificate to examine. + * @param types One or more subject alternative name types to fetch. + * + * @return List of subject names. + * + * @throws EncodingException on cert field extraction. + */ + public static List<String> subjectNames(final X509Certificate cert, final GeneralNameType... types) + throws EncodingException + { + final List<String> names = new ArrayList<>(); + final String cn = subjectCN(cert); + if (cn != null) { + names.add(cn); + } + + final GeneralNames altNames = subjectAltNames(cert, types); + if (altNames == null) { + return names; + } + for (GeneralName name : altNames.getNames()) { + names.add(name.getName().toString()); + } + return names; + } + + + /** + * Finds a certificate whose public key is paired with the given private key. + * + * @param key Private key used to find matching public key. + * @param candidates Array of candidate certificates. + * + * @return Certificate whose public key forms a keypair with the private key or null if no match is found. + * + * @throws EncodingException on cert field extraction. + */ + public static X509Certificate findEntityCertificate(final PrivateKey key, final X509Certificate... candidates) + throws EncodingException + { + return findEntityCertificate(key, Arrays.asList(candidates)); + } + + + /** + * Finds a certificate whose public key is paired with the given private key. + * + * @param key Private key used to find matching public key. + * @param candidates Collection of candidate certificates. + * + * @return Certificate whose public key forms a keypair with the private key or null if no match is found. + * + * @throws EncodingException on cert field extraction. + */ + public static X509Certificate findEntityCertificate( + final PrivateKey key, + final Collection<X509Certificate> candidates) + throws EncodingException + { + for (X509Certificate candidate : candidates) { + if (KeyPairUtil.isKeyPair(candidate.getPublicKey(), key)) { + return candidate; + } + } + return null; + } + + + /** + * Reads an X.509 certificate from ASN.1 encoded format in the file at the given location. + * + * @param path Path to file containing an DER or PEM encoded X.509 certificate. + * + * @return Certificate. + * + * @throws EncodingException on cert parsing errors. + * @throws StreamException on IO errors. + */ + public static X509Certificate readCertificate(final String path) throws EncodingException, StreamException + { + return readCertificate(StreamUtil.makeStream(new File(path))); + } + + + /** + * Reads an X.509 certificate from ASN.1 encoded format from the given file. + * + * @param file File containing an DER or PEM encoded X.509 certificate. + * + * @return Certificate. + * + * @throws EncodingException on cert parsing errors. + * @throws StreamException on IO errors. + */ + public static X509Certificate readCertificate(final File file) throws EncodingException, StreamException + { + return readCertificate(StreamUtil.makeStream(file)); + } + + + /** + * Reads an X.509 certificate from ASN.1 encoded data in the given stream. + * + * @param in Input stream containing PEM or DER encoded X.509 certificate. + * + * @return Certificate. + * + * @throws EncodingException on cert parsing errors. + * @throws StreamException on IO errors. + */ + public static X509Certificate readCertificate(final InputStream in) throws EncodingException, StreamException + { + try { + final CertificateFactory factory = CertificateFactory.getInstance("X.509"); + return (X509Certificate) factory.generateCertificate(in); + } catch (CertificateException e) { + if (e.getCause() instanceof IOException) { + throw new StreamException((IOException) e.getCause()); + } + throw new EncodingException("Cannot decode certificate", e); + } + } + + + /** + * Creates an X.509 certificate from its ASN.1 encoded form. + * + * @param encoded PEM or DER encoded ASN.1 data. + * + * @return Certificate. + * + * @throws EncodingException on cert parsing errors. + */ + public static X509Certificate decodeCertificate(final byte[] encoded) throws EncodingException + { + return readCertificate(new ByteArrayInputStream(encoded)); + } + + + /** + * Reads an X.509 certificate chain from ASN.1 encoded format in the file at the given location. + * + * @param path Path to file containing a sequence of PEM or DER encoded certificates or PKCS#7 certificate chain. + * + * @return Certificate. + * + * @throws EncodingException on cert parsing errors. + * @throws StreamException on IO errors. + */ + public static X509Certificate[] readCertificateChain(final String path) throws EncodingException, StreamException + { + return readCertificateChain(StreamUtil.makeStream(new File(path))); + } + + + /** + * Reads an X.509 certificate chain from ASN.1 encoded format from the given file. + * + * @param file File containing a sequence of PEM or DER encoded certificates or PKCS#7 certificate chain. + * + * @return Certificate. + * + * @throws EncodingException on cert parsing errors. + * @throws StreamException on IO errors. + */ + public static X509Certificate[] readCertificateChain(final File file) throws EncodingException, StreamException + { + return readCertificateChain(StreamUtil.makeStream(file)); + } + + + /** + * Reads an X.509 certificate chain from ASN.1 encoded data in the given stream. + * + * @param in Input stream containing a sequence of PEM or DER encoded certificates or PKCS#7 certificate chain. + * + * @return Certificate. + * + * @throws EncodingException on cert parsing errors. + * @throws StreamException on IO errors. + */ + public static X509Certificate[] readCertificateChain(final InputStream in) throws EncodingException, StreamException + { + try { + final CertificateFactory factory = CertificateFactory.getInstance("X.509"); + final Collection<? extends Certificate> certs = factory.generateCertificates(in); + return certs.toArray(new X509Certificate[0]); + } catch (CertificateException e) { + if (e.getCause() instanceof IOException) { + throw new StreamException((IOException) e.getCause()); + } + throw new EncodingException("Cannot decode certificate", e); + } + } + + + /** + * Creates an X.509 certificate chain from its ASN.1 encoded form. + * + * @param encoded Sequence of PEM or DER encoded certificates or PKCS#7 certificate chain. + * + * @return Certificate. + * + * @throws EncodingException on cert parsing errors. + */ + public static X509Certificate[] decodeCertificateChain(final byte[] encoded) throws EncodingException + { + return readCertificateChain(new ByteArrayInputStream(encoded)); + } + + + /** + * Determines whether the certificate allows the given basic key usages. + * + * @param cert Certificate to check. + * @param bits One or more basic key usage types to check. + * + * @return True if certificate allows all given usage types, false otherwise. + * + * @throws EncodingException on cert field extraction. + */ + public static boolean allowsUsage(final X509Certificate cert, final KeyUsageBits... bits) throws EncodingException + { + final KeyUsage usage = new ExtensionReader(cert).readKeyUsage(); + for (KeyUsageBits bit : bits) { + if (!bit.isSet(usage)) { + return false; + } + } + return true; + } + + + /** + * Determines whether the certificate allows the given extended key usages. + * + * @param cert Certificate to check. + * @param purposes One or more extended key usage purposes to check. + * + * @return True if certificate allows all given purposes, false otherwise. + * + * @throws EncodingException on cert field extraction. + */ + public static boolean allowsUsage(final X509Certificate cert, final KeyPurposeId... purposes) throws EncodingException + { + final List<KeyPurposeId> allowedUses = new ExtensionReader(cert).readExtendedKeyUsage(); + for (KeyPurposeId purpose : purposes) { + if (allowedUses == null || !allowedUses.contains(purpose)) { + return false; + } + } + return true; + } + + + /** + * Determines whether the certificate defines all the given certificate policies. + * + * @param cert Certificate to check. + * @param policyOidsToCheck One or more certificate policy OIDs to check. + * + * @return True if certificate defines all given policy OIDs, false otherwise. + * + * @throws EncodingException on cert field extraction. + */ + public static boolean hasPolicies(final X509Certificate cert, final String... policyOidsToCheck) + throws EncodingException + { + final List<PolicyInformation> policies = new ExtensionReader(cert).readCertificatePolicies(); + boolean hasPolicy; + for (String policyOid : policyOidsToCheck) { + hasPolicy = false; + if (policies != null) { + for (PolicyInformation policy : policies) { + if (policy.getPolicyIdentifier().getId().equals(policyOid)) { + hasPolicy = true; + break; + } + } + } + if (!hasPolicy) { + return false; + } + } + return true; + } + + + /** + * Gets the subject key identifier of the given certificate in delimited hexadecimal format, e.g. <code> + * 25:48:2f:28:ec:5d:19:bb:1d:25:ae:94:93:b1:7b:b5:35:96:24:66</code>. + * + * @param cert Certificate to process. + * + * @return Subject key identifier in colon-delimited hex format. + * + * @throws EncodingException on cert field extraction. + */ + public static String subjectKeyId(final X509Certificate cert) throws EncodingException + { + return CodecUtil.hex(new ExtensionReader(cert).readSubjectKeyIdentifier().getKeyIdentifier(), true); + } + + + /** + * Gets the authority key identifier of the given certificate in delimited hexadecimal format, e.g. <code> + * 25:48:2f:28:ec:5d:19:bb:1d:25:ae:94:93:b1:7b:b5:35:96:24:66</code>. + * + * @param cert Certificate to process. + * + * @return Authority key identifier in colon-delimited hex format. + * + * @throws EncodingException on cert field extraction. + */ + public static String authorityKeyId(final X509Certificate cert) throws EncodingException + { + return CodecUtil.hex(new ExtensionReader(cert).readAuthorityKeyIdentifier().getKeyIdentifier(), true); + } + + + /** + * PEM encodes the given certificate with the provided encoding type. + * + * @param <T> type of encoding + * + * @param certificate X.509 certificate. + * @param encodeType Type of encoding. {@link EncodeType#X509} or {@link EncodeType#PKCS7} + * + * @return either DER encoded certificate or PEM-encoded certificate header and footer defined by {@link EncodeType} + * and data wrapped at 64 characters per line. + * + * @throws RuntimeException if a certificate encoding error occurs + */ + public static <T> T encodeCert(final X509Certificate certificate, final EncodeType<T> encodeType) + { + try { + return encodeType.encode(certificate); + } catch (CertificateEncodingException e) { + throw new RuntimeException("Error getting encoded X.509 certificate data", e); + } + } + + /** + * Retrieves the subject distinguished name (DN) of the provided X.509 certificate. + * + * The subject DN represents the identity of the certificate holder and typically includes information + * such as the common name (CN), organizational unit (OU), organization (O), locality (L), state (ST), + * country (C), and other attributes. + * + * @param cert The X.509 certificate from which to extract the subject DN. + * @param format Controls whether the output contains spaces between attributes in the DN. + * Use {@link X500PrincipalFormat#READABLE} to generate a DN with spaces after the commas separating + * attribute-value pairs, {@link X500PrincipalFormat#RFC2253} for no spaces. + * @return The subject DN string of the X.509 certificate. + * + * @throws NullPointerException If the provided certificate is null. + */ + public static String subjectDN(final X509Certificate cert, final X500PrincipalFormat format) + { + final X500Principal subjectX500Principal = cert.getSubjectX500Principal(); + return X500PrincipalFormat.READABLE.equals(format) ? + subjectX500Principal.toString() : subjectX500Principal.getName(X500Principal.RFC2253); + } + + /** + * Generates a self-signed certificate. + * + * @param keyPair used for signing the certificate + * @param dn Subject dn + * @param duration Validity period of the certificate. The <em>notAfter</em> field is set to {@code now} + * plus this value. + * @param signatureAlgo the signature algorithm identifier to use + * + * @return a self-signed X509Certificate + */ + public static X509Certificate generateX509Certificate(final KeyPair keyPair, final String dn, + final Duration duration, final String signatureAlgo) + { + final Instant now = Instant.now(); + final Date notBefore = Date.from(now); + final Date notAfter = Date.from(now.plus(duration)); + return generateX509Certificate(keyPair, dn, notBefore, notAfter, signatureAlgo); + } + + /** + * Generates a self-signed certificate. + * + * @param keyPair used for signing the certificate + * @param dn Subject dn + * @param notBefore the date and time when the certificate validity period starts + * @param notAfter the date and time when the certificate validity period ends + * @param signatureAlgo the signature algorithm identifier to use + * + * @return a self-signed X509Certificate + */ + public static X509Certificate generateX509Certificate(final KeyPair keyPair, final String dn, + final Date notBefore, final Date notAfter, final String signatureAlgo) + { + final Instant now = Instant.now(); + final BigInteger serial = BigInteger.valueOf(now.toEpochMilli()); + + try { + final ContentSigner contentSigner = new JcaContentSignerBuilder(signatureAlgo) + .build(keyPair.getPrivate()); + final X500Name x500Name = new X500Name(RFC4519Style.INSTANCE, dn); + final X509v3CertificateBuilder certificateBuilder = + new JcaX509v3CertificateBuilder(x500Name, + serial, + notBefore, + notAfter, + x500Name, + keyPair.getPublic()) + .addExtension(Extension.basicConstraints, true, new BasicConstraints(true)); + + return new JcaX509CertificateConverter() + .setProvider(new BouncyCastleProvider()).getCertificate(certificateBuilder.build(contentSigner)); + } catch (OperatorCreationException | CertIOException | CertificateException e) { + throw new RuntimeException("Certificate generation error", e); + } + } + + /** + * Describes the behavior of string formatting of X.500 distinguished names. + */ + public enum X500PrincipalFormat + { + /** The format described in RFC2253 (without spaces). */ + RFC2253, + + /** Similar to RFC2253, but with spaces. */ + READABLE + } + + /** + * Marker interface for encoding types. + * + * @param <T> type of encoding + */ + public interface EncodeType<T> + { + + /** DER encode type.*/ + EncodeType<byte[]> DER = new DEREncodeType(); + + /** X509 encode type. */ + EncodeType<String> X509 = new X509EncodeType(); + + /** PKCS7 encode type. */ + EncodeType<String> PKCS7 = new PKCS7EncodeType(); + + /** + * Returns the type of encoding. + * + * @return type + */ + String getType(); + + /** + * Encodes the supplied certificate. + * + * @param cert to encode + * + * @return encoded certificate + * + * @throws CertificateEncodingException if an error occurs encoding the certificate + */ + T encode(X509Certificate cert) throws CertificateEncodingException; + } + + /** + * Base implementation for PEM encoded types. + */ + private abstract static class AbstractPemEncodeType implements EncodeType<String> + { + + /** + * Returns a PEM encoding of the supplied DER bytes. + * + * @param der to encode + * + * @return PEM encoded certificate + */ + protected String encodePem(final byte[] der) + { + final Base64Encoder encoder = new Base64Encoder(64); + final ByteBuffer input = ByteBuffer.wrap(der); + // Space for Base64-encoded data + header, footer, line breaks, and potential padding + final CharBuffer output = CharBuffer.allocate(encoder.outputSize(der.length) + 100); + output.append("-----BEGIN ").append(getType()).append("-----"); + output.append(System.lineSeparator()); + encoder.encode(input, output); + encoder.finalize(output); + output.flip(); + return output.toString().trim() + .concat(System.lineSeparator()).concat("-----END ").concat(getType()).concat("-----"); + } + } + + /** DER encode type. */ + private static class DEREncodeType implements EncodeType<byte[]> + { + + @Override + public String getType() + { + return "DER"; + } + + @Override + public byte[] encode(final X509Certificate cert) + throws CertificateEncodingException + { + return cert.getEncoded(); + } + } + + /** X509 encode type. */ + private static final class X509EncodeType extends AbstractPemEncodeType + { + + @Override + public String getType() + { + return "CERTIFICATE"; + } + + @Override + public String encode(final X509Certificate cert) + throws CertificateEncodingException + { + return encodePem(cert.getEncoded()); + } + } + + /** PKCS7 encode type. */ + private static final class PKCS7EncodeType extends AbstractPemEncodeType + { + + @Override + public String getType() + { + return "PKCS7"; + } + + @Override + public String encode(final X509Certificate cert) + throws CertificateEncodingException + { + return encodePem(cert.getEncoded()); + } + } +}
diff --git a/src/main/java/org/cryptacular/util/CipherUtil.java b/src/main/java/org/cryptacular/util/CipherUtil.java new file mode 100644 index 0000000..3e71015 --- /dev/null +++ b/src/main/java/org/cryptacular/util/CipherUtil.java
@@ -0,0 +1,394 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.function.Function; +import javax.crypto.SecretKey; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.paddings.PKCS7Padding; +import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; +import org.bouncycastle.crypto.params.ParametersWithIV; +import org.cryptacular.CiphertextHeader; +import org.cryptacular.CiphertextHeaderV2; +import org.cryptacular.CryptoException; +import org.cryptacular.EncodingException; +import org.cryptacular.StreamException; +import org.cryptacular.adapter.AEADBlockCipherAdapter; +import org.cryptacular.adapter.BlockCipherAdapter; +import org.cryptacular.adapter.BufferedBlockCipherAdapter; +import org.cryptacular.generator.Nonce; + +/** + * Utility class that performs encryption and decryption operations using a block cipher. + * + * @author Middleware Services + */ +public final class CipherUtil +{ + + /** Mac size in bits. */ + private static final int MAC_SIZE_BITS = 128; + + /** Private constructor of utility class. */ + private CipherUtil() {} + + + /** + * Encrypts data using an AEAD cipher. A {@link CiphertextHeaderV2} is prepended to the resulting ciphertext and + * used as AAD (Additional Authenticated Data) passed to the AEAD cipher. + * + * @param cipher AEAD cipher. + * @param key Encryption key. + * @param nonce Nonce generator. + * @param data Plaintext data to be encrypted. + * + * @return Concatenation of encoded {@link CiphertextHeaderV2} and encrypted data that completely fills the returned + * byte array. + * + * @throws CryptoException on encryption errors. + */ + public static byte[] encrypt(final AEADBlockCipher cipher, final SecretKey key, final Nonce nonce, final byte[] data) + throws CryptoException + { + final byte[] iv = nonce.generate(); + final byte[] header = new CiphertextHeaderV2(iv, "1").encode(key); + cipher.init(true, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, iv, header)); + return encrypt(new AEADBlockCipherAdapter(cipher), header, data); + } + + + /** + * Encrypts data using an AEAD cipher. A {@link CiphertextHeaderV2} is prepended to the resulting ciphertext and used + * as AAD (Additional Authenticated Data) passed to the AEAD cipher. + * + * @param cipher AEAD cipher. + * @param key Encryption key. + * @param nonce Nonce generator. + * @param input Input stream containing plaintext data. + * @param output Output stream that receives a {@link CiphertextHeaderV2} followed by ciphertext data produced by + * the AEAD cipher in encryption mode. + * + * @throws CryptoException on encryption errors. + * @throws StreamException on IO errors. + */ + public static void encrypt( + final AEADBlockCipher cipher, + final SecretKey key, + final Nonce nonce, + final InputStream input, + final OutputStream output) + throws CryptoException, StreamException + { + final byte[] iv = nonce.generate(); + final byte[] header = new CiphertextHeaderV2(iv, "1").encode(key); + cipher.init(true, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, iv, header)); + writeHeader(header, output); + process(new AEADBlockCipherAdapter(cipher), input, output); + } + + + /** + * Decrypts data using an AEAD cipher. + * + * @param cipher AEAD cipher. + * @param key Encryption key. + * @param data Ciphertext data containing a prepended {@link CiphertextHeaderV2}. The header is treated as AAD input + * to the cipher that is verified during decryption. + * + * @return Decrypted data that completely fills the returned byte array. + * + * @throws CryptoException on encryption errors. + * @throws EncodingException on decoding cyphertext header. + */ + public static byte[] decrypt(final AEADBlockCipher cipher, final SecretKey key, final byte[] data) + throws CryptoException, EncodingException + { + final CiphertextHeader header = decodeHeader(data, String -> key); + final byte[] nonce = header.getNonce(); + final byte[] hbytes = header.encode(); + cipher.init(false, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, nonce, hbytes)); + return decrypt(new AEADBlockCipherAdapter(cipher), data, header.getLength()); + } + + + /** + * Decrypts data using an AEAD cipher. + * + * @param cipher AEAD cipher. + * @param key Encryption key. + * @param input Input stream containing a {@link CiphertextHeaderV2} followed by ciphertext data. The header is + * treated as AAD input to the cipher that is verified during decryption. + * @param output Output stream that receives plaintext produced by block cipher in decryption mode. + * + * @throws CryptoException on encryption errors. + * @throws EncodingException on decoding cyphertext header. + * @throws StreamException on IO errors. + */ + public static void decrypt( + final AEADBlockCipher cipher, + final SecretKey key, + final InputStream input, + final OutputStream output) + throws CryptoException, EncodingException, StreamException + { + final CiphertextHeader header = decodeHeader(input, String -> key); + final byte[] nonce = header.getNonce(); + final byte[] hbytes = header.encode(); + cipher.init(false, new AEADParameters(new KeyParameter(key.getEncoded()), MAC_SIZE_BITS, nonce, hbytes)); + process(new AEADBlockCipherAdapter(cipher), input, output); + } + + + /** + * Encrypts data using the given block cipher with PKCS5 padding. A {@link CiphertextHeaderV2} is prepended to the + * resulting ciphertext. + * + * @param cipher Block cipher. + * @param key Encryption key. + * @param nonce IV generator. Callers must take care to ensure that the length of generated IVs is equal to the + * cipher block size. + * @param data Plaintext data to be encrypted. + * + * @return Concatenation of encoded {@link CiphertextHeaderV2} and encrypted data that completely fills the returned + * byte array. + * + * @throws CryptoException on encryption errors. + */ + public static byte[] encrypt(final BlockCipher cipher, final SecretKey key, final Nonce nonce, final byte[] data) + throws CryptoException + { + final byte[] iv = nonce.generate(); + final byte[] header = new CiphertextHeaderV2(iv, "1").encode(key); + final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding()); + padded.init(true, new ParametersWithIV(new KeyParameter(key.getEncoded()), iv)); + return encrypt(new BufferedBlockCipherAdapter(padded), header, data); + } + + + /** + * Encrypts data using the given block cipher with PKCS5 padding. A {@link CiphertextHeader} is prepended to the + * resulting ciphertext. + * + * @param cipher Block cipher. + * @param key Encryption key. + * @param nonce IV generator. Callers must take care to ensure that the length of generated IVs is equal to the + * cipher block size. + * @param input Input stream containing plaintext data. + * @param output Output stream that receives ciphertext produced by block cipher in encryption mode. + * + * @throws CryptoException on encryption errors. + * @throws StreamException on IO errors. + */ + public static void encrypt( + final BlockCipher cipher, + final SecretKey key, + final Nonce nonce, + final InputStream input, + final OutputStream output) + throws CryptoException, StreamException + { + final byte[] iv = nonce.generate(); + final byte[] header = new CiphertextHeaderV2(iv, "1").encode(key); + final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding()); + padded.init(true, new ParametersWithIV(new KeyParameter(key.getEncoded()), iv)); + writeHeader(header, output); + process(new BufferedBlockCipherAdapter(padded), input, output); + } + + + /** + * Decrypts data using the given block cipher with PKCS5 padding. + * + * @param cipher Block cipher. + * @param key Encryption key. + * @param data Ciphertext data containing a prepended {@link CiphertextHeader}. + * + * @return Decrypted data that completely fills the returned byte array. + * + * @throws CryptoException on encryption errors. + * @throws EncodingException on decoding cyphertext header. + */ + public static byte[] decrypt(final BlockCipher cipher, final SecretKey key, final byte[] data) + throws CryptoException, EncodingException + { + final CiphertextHeader header = decodeHeader(data, String -> key); + final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding()); + padded.init(false, new ParametersWithIV(new KeyParameter(key.getEncoded()), header.getNonce())); + return decrypt(new BufferedBlockCipherAdapter(padded), data, header.getLength()); + } + + + /** + * Decrypts data using the given block cipher with PKCS5 padding. + * + * @param cipher Block cipher. + * @param key Encryption key. + * @param input Input stream containing a {@link CiphertextHeader} followed by ciphertext data. + * @param output Output stream that receives plaintext produced by block cipher in decryption mode. + * + * @throws CryptoException on encryption errors. + * @throws EncodingException on decoding cyphertext header. + * @throws StreamException on IO errors. + */ + public static void decrypt( + final BlockCipher cipher, + final SecretKey key, + final InputStream input, + final OutputStream output) + throws CryptoException, EncodingException, StreamException + { + final CiphertextHeader header = decodeHeader(input, String -> key); + final PaddedBufferedBlockCipher padded = new PaddedBufferedBlockCipher(cipher, new PKCS7Padding()); + padded.init(false, new ParametersWithIV(new KeyParameter(key.getEncoded()), header.getNonce())); + process(new BufferedBlockCipherAdapter(padded), input, output); + } + + + /** + * Decodes the ciphertext header at the start of the given byte array. + * Supports both original (deprecated) and v2 formats. + * + * @param data Ciphertext data with prepended header. + * @param keyLookup Decryption key lookup function. + * + * @return Ciphertext header instance. + */ + public static CiphertextHeader decodeHeader(final byte[] data, final Function<String, SecretKey> keyLookup) + { + try { + return CiphertextHeaderV2.decode(data, keyLookup); + } catch (EncodingException e) { + return CiphertextHeader.decode(data); + } + } + + + /** + * Decodes the ciphertext header at the start of the given input stream. + * Supports both original (deprecated) and v2 formats. + * + * @param in Ciphertext stream that is positioned at the start of the ciphertext header. + * @param keyLookup Decryption key lookup function. + * + * @return Ciphertext header instance. + */ + public static CiphertextHeader decodeHeader(final InputStream in, final Function<String, SecretKey> keyLookup) + { + CiphertextHeader header; + try { + // Mark the stream start position, so we can try again with old format header + if (in.markSupported()) { + in.mark(4); + } + header = CiphertextHeaderV2.decode(in, keyLookup); + } catch (EncodingException e) { + try { + in.reset(); + } catch (IOException ioe) { + throw new StreamException("Stream error trying to process old header format: " + ioe.getMessage()); + } + header = CiphertextHeader.decode(in); + } + return header; + } + + + /** + * Encrypts the given data. + * + * @param cipher Adapter for either a block or AEAD cipher. + * @param header Encoded ciphertext header. + * @param data Plaintext data to encrypt. + * + * @return Concatenation of encoded header and encrypted data that completely fills the returned byte array. + */ + private static byte[] encrypt(final BlockCipherAdapter cipher, final byte[] header, final byte[] data) + { + final int outSize = header.length + cipher.getOutputSize(data.length); + final byte[] output = new byte[outSize]; + System.arraycopy(header, 0, output, 0, header.length); + + int outOff = header.length; + outOff += cipher.processBytes(data, 0, data.length, output, outOff); + cipher.doFinal(output, outOff); + cipher.reset(); + return output; + } + + + /** + * Decrypts the given data. + * + * @param cipher Adapter for either a block or AEAD cipher. + * @param data Ciphertext data containing prepended header bytes. + * @param inOff Offset into ciphertext at which encrypted data starts (i.e. after header). + * + * @return Decrypted data that completely fills the returned byte array. + */ + private static byte[] decrypt(final BlockCipherAdapter cipher, final byte[] data, final int inOff) + { + final int len = data.length - inOff; + final int outSize = cipher.getOutputSize(len); + final byte[] output = new byte[outSize]; + int outOff = cipher.processBytes(data, inOff, len, output, 0); + outOff += cipher.doFinal(output, outOff); + cipher.reset(); + if (outOff < output.length) { + final byte[] temp = new byte[outOff]; + System.arraycopy(output, 0, temp, 0, outOff); + return temp; + } + return output; + } + + + /** + * Performs encryption or decryption on the given input stream based on the underlying cipher mode and writes the + * result to the given output stream. + * + * @param cipher Adapter for either a block or AEAD cipher. + * @param input Input stream containing data to be processed by the cipher. + * @param output Output stream that receives the output of the cipher acting on the input. + */ + private static void process(final BlockCipherAdapter cipher, final InputStream input, final OutputStream output) + { + final int inSize = 1024; + final int outSize = cipher.getOutputSize(inSize); + final byte[] inBuf = new byte[inSize]; + final byte[] outBuf = new byte[outSize > inSize ? outSize : inSize]; + int readLen; + int writeLen; + try { + while ((readLen = input.read(inBuf)) > 0) { + writeLen = cipher.processBytes(inBuf, 0, readLen, outBuf, 0); + output.write(outBuf, 0, writeLen); + } + writeLen = cipher.doFinal(outBuf, 0); + output.write(outBuf, 0, writeLen); + } catch (IOException e) { + throw new StreamException(e); + } + } + + + /** + * Writes a ciphertext header to the output stream. + * + * @param header Ciphertext header bytes. + * @param output Output stream. + */ + private static void writeHeader(final byte[] header, final OutputStream output) + { + try { + output.write(header, 0, header.length); + } catch (IOException e) { + throw new StreamException(e); + } + } + +}
diff --git a/src/main/java/org/cryptacular/util/CodecUtil.java b/src/main/java/org/cryptacular/util/CodecUtil.java new file mode 100644 index 0000000..10c9eeb --- /dev/null +++ b/src/main/java/org/cryptacular/util/CodecUtil.java
@@ -0,0 +1,205 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.EncodingException; +import org.cryptacular.codec.Base32Decoder; +import org.cryptacular.codec.Base32Encoder; +import org.cryptacular.codec.Base64Decoder; +import org.cryptacular.codec.Base64Encoder; +import org.cryptacular.codec.Decoder; +import org.cryptacular.codec.Encoder; +import org.cryptacular.codec.HexDecoder; +import org.cryptacular.codec.HexEncoder; + + +/** + * Utility class for common encoding conversions. + * + * @author Middleware Services + */ +public final class CodecUtil +{ + + /** Private constructor of utility class. */ + private CodecUtil() {} + + + /** + * Encodes raw bytes to the equivalent hexadecimal encoded string. + * + * @param raw Raw bytes to encode. + * + * @return Hexadecimal encoded string. + * + * @throws EncodingException on encoding errors. + */ + public static String hex(final byte[] raw) throws EncodingException + { + return encode(new HexEncoder(), raw); + } + + + /** + * Encodes raw bytes to the equivalent hexadecimal encoded string with optional delimiting of output. + * + * @param raw Raw bytes to encode. + * @param delimit True to delimit every two characters (i.e. every byte) of output with ':' character, false + * otherwise. + * + * @return Hexadecimal encoded string. + * + * @throws EncodingException on encoding errors. + */ + public static String hex(final byte[] raw, final boolean delimit) throws EncodingException + { + return encode(new HexEncoder(delimit), raw); + } + + + /** + * Decodes a hexadecimal encoded string to raw bytes. + * + * @param encoded Hex encoded character data. + * + * @return Raw bytes of hex string. + * + * @throws EncodingException on decoding errors. + */ + public static byte[] hex(final CharSequence encoded) throws EncodingException + { + return decode(new HexDecoder(), encoded); + } + + + /** + * Encodes bytes into base 64-encoded string. + * + * @param raw Raw bytes to encode. + * + * @return Base64-encoded string. + * + * @throws EncodingException on encoding errors. + */ + public static String b64(final byte[] raw) throws EncodingException + { + return encode(new Base64Encoder(), raw); + } + + + /** + * Decodes a base64-encoded string into raw bytes. + * + * @param encoded Base64-encoded character data. + * + * @return Base64-decoded bytes. + * + * @throws EncodingException on decoding errors. + */ + public static byte[] b64(final CharSequence encoded) throws EncodingException + { + return decode(new Base64Decoder(), encoded); + } + + + /** + * Encodes bytes into base64-encoded string. + * + * @param raw Raw bytes to encode. + * @param lineLength Length of each base64-encoded line in output. + * + * @return Base64-encoded string. + * + * @throws EncodingException on encoding errors. + */ + public static String b64(final byte[] raw, final int lineLength) throws EncodingException + { + return encode(new Base64Encoder(lineLength), raw); + } + + + /** + * Encodes bytes into base 32-encoded string. + * + * @param raw Raw bytes to encode. + * + * @return Base32-encoded string. + * + * @throws EncodingException on encoding errors. + */ + public static String b32(final byte[] raw) throws EncodingException + { + return encode(new Base32Encoder(), raw); + } + + + /** + * Decodes a base32-encoded string into raw bytes. + * + * @param encoded Base32-encoded character data. + * + * @return Base64-decoded bytes. + * + * @throws EncodingException on decoding errors. + */ + public static byte[] b32(final CharSequence encoded) throws EncodingException + { + return decode(new Base32Decoder(), encoded); + } + + + /** + * Encodes bytes into base32-encoded string. + * + * @param raw Raw bytes to encode. + * @param lineLength Length of each base32-encoded line in output. + * + * @return Base32-encoded string. + * + * @throws EncodingException on encoding errors. + */ + public static String b32(final byte[] raw, final int lineLength) throws EncodingException + { + return encode(new Base32Encoder(lineLength), raw); + } + + + /** + * Encodes raw bytes using the given encoder. + * + * @param encoder Encoder to perform byte-to-char conversion. + * @param raw Raw bytes to encode. + * + * @return Encoded data as a string. + * + * @throws EncodingException on encoding errors. + */ + public static String encode(final Encoder encoder, final byte[] raw) throws EncodingException + { + final CharBuffer output = CharBuffer.allocate(encoder.outputSize(raw.length)); + encoder.encode(ByteBuffer.wrap(raw), output); + encoder.finalize(output); + return output.flip().toString(); + } + + + /** + * Decodes the given encoded data using the given char-to-byte decoder. + * + * @param decoder Decoder to perform char-to-byte conversion. + * @param encoded Encoded character data. + * + * @return Decoded data as raw bytes. + * + * @throws EncodingException on decoding errors. + */ + public static byte[] decode(final Decoder decoder, final CharSequence encoded) throws EncodingException + { + final ByteBuffer output = ByteBuffer.allocate(decoder.outputSize(encoded.length())); + decoder.decode(CharBuffer.wrap(encoded), output); + decoder.finalize(output); + output.flip(); + return ByteUtil.toArray(output); + } +}
diff --git a/src/main/java/org/cryptacular/util/CsrUtil.java b/src/main/java/org/cryptacular/util/CsrUtil.java new file mode 100644 index 0000000..f548e00 --- /dev/null +++ b/src/main/java/org/cryptacular/util/CsrUtil.java
@@ -0,0 +1,273 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1Set; +import org.bouncycastle.asn1.pkcs.Attribute; +import org.bouncycastle.asn1.pkcs.CertificationRequest; +import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.Extensions; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.GeneralNamesBuilder; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.util.PublicKeyFactory; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.AlgorithmNameFinder; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultAlgorithmNameFinder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder; +import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder; +import org.cryptacular.CryptoException; +import org.cryptacular.EncodingException; +import org.cryptacular.x509.dn.NameReader; +import org.cryptacular.x509.dn.RDNSequence; +import org.cryptacular.x509.dn.StandardAttributeType; + +/** + * PKCS#10 certificate signing request (CSR) utilities. + * + * @author Marvin S. Addison + */ +public final class CsrUtil +{ + /** Maps algorithm OIDs onto typical algorithm names like "SHA256withRSA". */ + private static final AlgorithmNameFinder ALG_NAME_FINDER = new DefaultAlgorithmNameFinder(); + + + /** + * Private constructor of utility class. + */ + private CsrUtil() {} + + /** + * Encodes a PKCS#10 certificate signing request to PEM-encoded string format. + * + * @param csr Certificate signing request. + * + * @return PEM-encoded CSR. + * + * @throws EncodingException on errors writing PEM-encoded data. + */ + public static String encodeCsr(final PKCS10CertificationRequest csr) + { + final StringWriter writer = new StringWriter(); + try (JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) { + pemWriter.writeObject(csr); + pemWriter.close(); + return writer.toString(); + } catch (IOException e) { + throw new EncodingException("CSR encoding error", e); + } + } + + /** + * Decodes PEM-encoded PKCS#10 certificate signing request into a structured object. + * + * @param csr PEM-encoded CSR. + * + * @return Decoded CSR. + * + * @throws IllegalArgumentException if input does not appear to be PEM-encoded data. + */ + public static CertificationRequest decodeCsr(final String csr) + { + byte[] csrBytes = csr.getBytes(StandardCharsets.US_ASCII); + if (!PemUtil.isPem(csrBytes)) { + throw new IllegalArgumentException("Input is not PEM-encoded as required"); + } + csrBytes = PemUtil.decode(csrBytes); + return CertificationRequest.getInstance(csrBytes); + } + + /** + * Decodes DER-encoded PKCS#10 certificate signing request into a structured object. + * + * @param csr Bytes of a DER-encoded CSR. + * + * @return Decoded CSR. + */ + public static CertificationRequest decodeCsr(final byte[] csr) + { + return CertificationRequest.getInstance(csr); + } + + /** + * Decodes either a PEM or DER-encoded PKCS#10 certificate signing request from a file into a structured object. + * + * @param file File containing PEM or DER-encoded data. + * + * @return Decoded CSR. + */ + public static CertificationRequest readCsr(final File file) + { + return readCsr(StreamUtil.makeStream(file)); + } + + /** + * Decodes either a PEM or DER-encoded PKCS#10 certificate signing request from a stream into a structured object. + * + * @param in Input stream containing PEM or DER-encoded data. + * + * @return Decoded CSR. + */ + public static CertificationRequest readCsr(final InputStream in) + { + final byte[] data = StreamUtil.readAll(in); + if (PemUtil.isPem(data)) { + return decodeCsr(PemUtil.decode(data)); + } + return decodeCsr(data); + } + + /** + * Gets all the common names from the subject of the certificate request. + * + * @param csr Certificate request. + * + * @return List of zero or more common names. + */ + public static List<String> commonNames(final CertificationRequest csr) + { + final RDNSequence sequence = NameReader.readX500Name(csr.getCertificationRequestInfo().getSubject()); + return sequence.getValues(StandardAttributeType.CommonName); + } + + /** + * Gets all subject alternative names mentioned on the certificate request. + * + * @param csr Certificate request. + * + * @return List of subject alternative names. + */ + public static List<String> subjectAltNames(final CertificationRequest csr) + { + final List<String> names = new ArrayList<>(); + final ASN1Set attributeSet = csr.getCertificationRequestInfo().getAttributes(); + if (attributeSet == null) { + return names; + } + for (ASN1Encodable item : attributeSet) + { + final Attribute attr = Attribute.getInstance(item); + if (attr.getAttrType().equals(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest)) { + final Extensions extensions = Extensions.getInstance(attr.getAttributeValues()[0]); + final GeneralNames subjAltNames = GeneralNames.fromExtensions(extensions, Extension.subjectAlternativeName); + if (subjAltNames != null) { + for (GeneralName gn : subjAltNames.getNames()) { + names.add(gn.getName().toString().toLowerCase(Locale.ROOT)); + } + } + } + } + return names; + } + + /** + * Gets the name of the signature algorithm mentioned in the CSR. + * + * @param csr Certificate request. + * + * @return Signature algorithm name, e.g. "SHA256withRSA" + */ + public static String sigAlgName(final CertificationRequest csr) + { + return ALG_NAME_FINDER.getAlgorithmName(csr.getSignatureAlgorithm()).replace("WITH", "with"); + } + + /** + * Gets the size in bits of the public key in the CSR. + * + * @param csr Certificate request. + * + * @return Public key size in bits. + * + * @throws IllegalArgumentException if CSR specifies a key algorithm other than RSA or EC. + * @throws CryptoException on errors creating a public key from data in the CSR. + */ + public static int keyLength(final CertificationRequest csr) + { + final AsymmetricKeyParameter pubKeyParam; + try { + pubKeyParam = PublicKeyFactory.createKey( + csr.getCertificationRequestInfo().getSubjectPublicKeyInfo()); + } catch (IOException e) { + throw new CryptoException("Error creating public key parameters", e); + } + final int length; + if (pubKeyParam instanceof RSAKeyParameters) { + length = ((RSAKeyParameters) pubKeyParam).getModulus().bitLength(); + } else if (pubKeyParam instanceof ECKeyParameters) { + length = ((ECPublicKeyParameters) pubKeyParam).getQ().getXCoord().getFieldSize(); + } else { + throw new IllegalArgumentException("Unsupported key algorithm"); + } + return length; + } + + /** + * Generates a CSR given a key pair, subject DN, and optional subject alternative names. + * + * @param keyPair Key pair. + * @param subjectDN Subject distinguished name, e.g. "CN=host.example.org, DC=example, DC=org". + * @param subjectAltNames Zero or more DNS subject alternative names. + * + * @return PKCS#10 certification request. Use {@link PKCS10CertificationRequest#toASN1Structure()} to get the + * underlying {@link CertificationRequest} that may be used with other helper methods. + * + * @throws IllegalArgumentException if CSR specifies a key algorithm other than RSA or EC. + * @throws CryptoException on errors generating the CSR from data provided. + */ + public static PKCS10CertificationRequest generateCsr( + final KeyPair keyPair, final String subjectDN, final String ... subjectAltNames) + { + final String keyAlg = keyPair.getPublic().getAlgorithm(); + final String sigAlg; + if ("RSA".equals(keyAlg)) { + sigAlg = "SHA256withRSA"; + } else if ("EC".equals(keyAlg)) { + sigAlg = "SHA256withECDSA"; + } else { + throw new IllegalArgumentException("Unsupported key algorithm"); + } + final PKCS10CertificationRequestBuilder p10Builder = new JcaPKCS10CertificationRequestBuilder( + new X500Principal(subjectDN), keyPair.getPublic()); + if (subjectAltNames != null && subjectAltNames.length > 0) { + final GeneralNamesBuilder namesBuilder = new GeneralNamesBuilder(); + for (String subjectAltName : subjectAltNames) { + namesBuilder.addName(new GeneralName(GeneralName.dNSName, subjectAltName)); + } + final GeneralNames names = namesBuilder.build(); + try { + final Extension sanExtension = Extension.create(Extension.subjectAlternativeName, false, names); + p10Builder.addAttribute(PKCSObjectIdentifiers.pkcs_9_at_extensionRequest, new Extensions(sanExtension)); + } catch (IOException e) { + throw new CryptoException("Error adding subject alt names to CSR", e); + } + } + final JcaContentSignerBuilder csBuilder = new JcaContentSignerBuilder(sigAlg); + try { + final ContentSigner signer = csBuilder.build(keyPair.getPrivate()); + return p10Builder.build(signer); + } catch (OperatorCreationException e) { + throw new CryptoException("Failed generating CSR", e); + } + } +}
diff --git a/src/main/java/org/cryptacular/util/HashUtil.java b/src/main/java/org/cryptacular/util/HashUtil.java new file mode 100644 index 0000000..b028bc9 --- /dev/null +++ b/src/main/java/org/cryptacular/util/HashUtil.java
@@ -0,0 +1,269 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA3Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.cryptacular.CryptoException; +import org.cryptacular.SaltedHash; +import org.cryptacular.StreamException; +import org.cryptacular.io.Resource; + +/** + * Utility class for computing cryptographic hashes. + * + * @author Middleware Services + */ +public final class HashUtil +{ + + /** Private constructor of utility class. */ + private HashUtil() {} + + + /** + * Computes the hash of the given data using the given algorithm. A salted hash may be produced as follows: + * + * <pre> + // data is a byte array containing raw data to digest + final byte[] salt = new RBGNonce(16).generate(); + final byte[] hash = HashUtil.hash(new SHA1Digest(), data, salt); + * </pre> + * + * @param digest Hash algorithm. + * @param data Data to hash. Supported types are <code>byte[]</code>, {@link CharSequence} ,{@link InputStream}, and + * {@link Resource}. Character data is processed in the <code>UTF-8</code> character set; if another + * character set is desired, the caller should convert to <code>byte[]</code> and provide the resulting + * bytes. + * + * @return Byte array of length {@link Digest#getDigestSize()} containing hash output. + * + * @throws CryptoException on hash computation errors. + * @throws StreamException on stream IO errors. + */ + public static byte[] hash(final Digest digest, final Object... data) throws CryptoException, StreamException + { + for (Object o : data) { + if (o instanceof byte[]) { + final byte[] bytes = (byte[]) o; + digest.update(bytes, 0, bytes.length); + } else if (o instanceof String) { + final byte[] bytes = ByteUtil.toBytes((String) o); + digest.update(bytes, 0, bytes.length); + } else if (o instanceof InputStream) { + hashStream(digest, (InputStream) o); + } else if (o instanceof Resource) { + final InputStream in; + try { + in = ((Resource) o).getInputStream(); + } catch (IOException e) { + throw new StreamException(e); + } + hashStream(digest, in); + } else { + throw new IllegalArgumentException("Invalid input data type " + o); + } + } + + final byte[] output = new byte[digest.getDigestSize()]; + try { + digest.doFinal(output, 0); + } catch (RuntimeException e) { + throw new CryptoException("Hash computation error", e); + } + return output; + } + + + /** + * Computes the iterated hash of the given data using the given algorithm. The following example demonstrates a + * typical usage pattern, a salted hash with 10 rounds: + * + * <pre> + // data is a byte array containing raw data to digest + final byte[] salt = new RBGNonce(16).generate(); + final byte[] hash = HashUtil.hash(new SHA1Digest(), 10, data, salt); + * </pre> + * + * @param digest Hash algorithm. + * @param iterations Number of hash rounds. Must be positive value. + * @param data Data to hash. Supported types are <code>byte[]</code>, {@link CharSequence} ,{@link InputStream}, and + * {@link Resource}. Character data is processed in the <code>UTF-8</code> character set; if another + * character set is desired, the caller should convert to <code>byte[]</code> and provide the resulting + * bytes. + * + * @return Byte array of length {@link Digest#getDigestSize()} containing hash output. + * + * @throws CryptoException on hash computation errors. + * @throws StreamException on stream IO errors. + */ + public static byte[] hash(final Digest digest, final int iterations, final Object... data) + throws CryptoException, StreamException + { + if (iterations < 1) { + throw new IllegalArgumentException("Iterations must be positive"); + } + + final byte[] output = hash(digest, data); + try { + for (int i = 1; i < iterations; i++) { + digest.update(output, 0, output.length); + digest.doFinal(output, 0); + } + } catch (RuntimeException e) { + throw new CryptoException("Hash computation error", e); + } + return output; + } + + + /** + * Determines whether the hash of the given input equals a known value. + * + * @param digest Hash algorithm. + * @param hash Hash to compare with. If the length of the array is greater than the length of the digest output, + * anything beyond the digest length is considered salt data that is hashed <strong>after</strong> the + * input data. + * @param iterations Number of hash rounds. + * @param data Data to hash. + * + * @return True if the hash of the data under the given digest is equal to the hash, false otherwise. + * + * @throws CryptoException on hash computation errors. + * @throws StreamException on stream IO errors. + */ + public static boolean compareHash(final Digest digest, final byte[] hash, final int iterations, final Object... data) + throws CryptoException, StreamException + { + if (hash.length > digest.getDigestSize()) { + final byte[] hashPart = Arrays.copyOfRange(hash, 0, digest.getDigestSize()); + final byte[] saltPart = Arrays.copyOfRange(hash, digest.getDigestSize(), hash.length); + final Object[] dataWithSalt = Arrays.copyOf(data, data.length + 1); + dataWithSalt[data.length] = saltPart; + return Arrays.equals(hash(digest, iterations, dataWithSalt), hashPart); + } + return Arrays.equals(hash(digest, iterations, data), hash); + } + + + /** + * Determines whether the salted hash of the given input equals a known hash value. + * + * @param digest Hash algorithm. + * @param hash Salted hash data. + * @param iterations Number of hash rounds. + * @param saltAfterData True to apply salt after data, false to apply salt before data. + * @param data Data to hash, which should NOT include the salt value. + * + * @return True if the hash of the data under the given digest is equal to the hash, false otherwise. + * + * @throws CryptoException on hash computation errors. + * @throws StreamException on stream IO errors. + */ + public static boolean compareHash( + final Digest digest, + final SaltedHash hash, + final int iterations, + final boolean saltAfterData, + final Object... data) + throws CryptoException, StreamException + { + final Object[] dataWithSalt; + if (saltAfterData) { + dataWithSalt = Arrays.copyOf(data, data.length + 1); + dataWithSalt[data.length] = hash.getSalt(); + } else { + dataWithSalt = new Object[data.length + 1]; + dataWithSalt[0] = hash.getSalt(); + System.arraycopy(data, 0, dataWithSalt, 1, data.length); + } + return Arrays.equals(hash(digest, iterations, dataWithSalt), hash.getHash()); + } + + + /** + * Produces the SHA-1 hash of the given data. + * + * @param data Data to hash. See {@link #hash(Digest, Object...)} for supported inputs. + * + * @return 20-byte array containing hash output. + * + * @see #hash(Digest, Object...) + */ + public static byte[] sha1(final Object... data) + { + return hash(new SHA1Digest(), data); + } + + + /** + * Produces the SHA-256 hash of the given data. + * + * @param data Data to hash. See {@link #hash(Digest, Object...)} for supported inputs. + * + * @return 32-byte array containing hash output. + * + * @see #hash(Digest, Object...) + */ + public static byte[] sha256(final Object... data) + { + return hash(new SHA256Digest(), data); + } + + + /** + * Produces the SHA-512 hash of the given data. + * + * @param data Data to hash. See {@link #hash(Digest, Object...)} for supported inputs. + * + * @return 64-byte array containing hash output. + * + * @see #hash(Digest, Object...) + */ + public static byte[] sha512(final Object... data) + { + return hash(new SHA512Digest(), data); + } + + + /** + * Produces the SHA-3 hash of the given data. + * + * @param bitLength One of the supported SHA-3 output bit lengths: 224, 256, 384, or 512. + * @param data Data to hash. See {@link #hash(Digest, Object...)} for supported inputs. + * + * @return Byte array of size <code>bitLength</code> containing hash output. + * + * @see #hash(Digest, Object...) + */ + public static byte[] sha3(final int bitLength, final Object... data) + { + return hash(new SHA3Digest(bitLength), data); + } + + + /** + * Digests the data in the given stream. Note this method does not finalize the digest process by calling {@link + * Digest#doFinal(byte[], int)}. + * + * @param digest Digest algorithm. + * @param in Input stream containing data to hash. + */ + private static void hashStream(final Digest digest, final InputStream in) + { + final byte[] buffer = new byte[StreamUtil.CHUNK_SIZE]; + int length; + try { + while ((length = in.read(buffer)) > 0) { + digest.update(buffer, 0, length); + } + } catch (IOException e) { + throw new StreamException(e); + } + } +}
diff --git a/src/main/java/org/cryptacular/util/KeyPairUtil.java b/src/main/java/org/cryptacular/util/KeyPairUtil.java new file mode 100644 index 0000000..3bec562 --- /dev/null +++ b/src/main/java/org/cryptacular/util/KeyPairUtil.java
@@ -0,0 +1,484 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.DSAParameters; +import org.bouncycastle.crypto.params.DSAPrivateKeyParameters; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.signers.DSASigner; +import org.bouncycastle.crypto.signers.ECDSASigner; +import org.bouncycastle.crypto.signers.RSADigestSigner; +import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; +import org.cryptacular.EncodingException; +import org.cryptacular.StreamException; +import org.cryptacular.adapter.Converter; +import org.cryptacular.asn.OpenSSLPrivateKeyDecoder; +import org.cryptacular.asn.PKCS8PrivateKeyDecoder; +import org.cryptacular.asn.PublicKeyDecoder; + +/** + * Utility methods for public/private key pairs used for asymmetric encryption. + * + * @author Middleware Services + */ +public final class KeyPairUtil +{ + + /** Data used to verify key pairs. */ + private static final byte[] SIGN_BYTES = ByteUtil.toBytes("Mr. Watson--come here--I want to see you."); + + + /** Private constructor of utility class. */ + private KeyPairUtil() {} + + + /** + * Gets the length in bits of a public key where key size is dependent on the particulars of the algorithm. + * + * <ul> + * <li>DSA - length of p</li> + * <li>EC - length of p for prime fields, m for binary fields</li> + * <li>RSA - length of modulus</li> + * </ul> + * + * @param pubKey Public key. + * + * @return Size of the key in bits. + */ + public static int length(final PublicKey pubKey) + { + final int size; + if (pubKey instanceof DSAPublicKey) { + size = ((DSAPublicKey) pubKey).getParams().getP().bitLength(); + } else if (pubKey instanceof RSAPublicKey) { + size = ((RSAPublicKey) pubKey).getModulus().bitLength(); + } else if (pubKey instanceof ECPublicKey) { + size = ((ECPublicKey) pubKey).getParams().getCurve().getField().getFieldSize(); + } else { + throw new IllegalArgumentException(pubKey + " not supported."); + } + return size; + } + + + /** + * Gets the length in bits of a private key where key size is dependent on the particulars of the algorithm. + * + * <ul> + * <li>DSA - length of q in bits</li> + * <li>EC - length of p for prime fields, m for binary fields</li> + * <li>RSA - modulus length in bits</li> + * </ul> + * + * @param privKey Private key. + * + * @return Size of the key in bits. + */ + public static int length(final PrivateKey privKey) + { + final int size; + if (privKey instanceof DSAPrivateKey) { + size = ((DSAPrivateKey) privKey).getParams().getQ().bitLength(); + } else if (privKey instanceof RSAPrivateKey) { + size = ((RSAPrivateKey) privKey).getModulus().bitLength(); + } else if (privKey instanceof ECPrivateKey) { + size = ((ECPrivateKey) privKey).getParams().getCurve().getField().getFieldSize(); + } else { + throw new IllegalArgumentException(privKey + " not supported."); + } + return size; + } + + + /** + * Determines whether the given public and private keys form a proper key pair by computing and verifying a digital + * signature with the keys. + * + * @param pubKey DSA, RSA or EC public key. + * @param privKey DSA, RSA, or EC private key. + * + * @return True if the keys form a functioning keypair, false otherwise. Errors during signature verification are + * treated as false. + * + * @throws org.cryptacular.CryptoException on key validation errors. + */ + public static boolean isKeyPair(final PublicKey pubKey, final PrivateKey privKey) + throws org.cryptacular.CryptoException + { + final String alg = pubKey.getAlgorithm(); + if (!alg.equals(privKey.getAlgorithm())) { + return false; + } + + // Dispatch onto the algorithm-specific method + final boolean result; + switch (alg) { + + case "DSA": + result = isKeyPair((DSAPublicKey) pubKey, (DSAPrivateKey) privKey); + break; + + case "RSA": + result = isKeyPair((RSAPublicKey) pubKey, (RSAPrivateKey) privKey); + break; + + case "EC": + result = isKeyPair((ECPublicKey) pubKey, (ECPrivateKey) privKey); + break; + + default: + throw new IllegalArgumentException(alg + " not supported."); + } + return result; + } + + + /** + * Determines whether the given DSA public and private keys form a proper key pair by computing and verifying a + * digital signature with the keys. + * + * @param pubKey DSA public key. + * @param privKey DSA private key. + * + * @return True if the keys form a functioning keypair, false otherwise. Errors during signature verification are + * treated as false. + * + * @throws org.cryptacular.CryptoException on key validation errors. + */ + public static boolean isKeyPair(final DSAPublicKey pubKey, final DSAPrivateKey privKey) + throws org.cryptacular.CryptoException + { + final DSASigner signer = new DSASigner(); + final DSAParameters params = new DSAParameters( + privKey.getParams().getP(), + privKey.getParams().getQ(), + privKey.getParams().getG()); + + try { + signer.init(true, new DSAPrivateKeyParameters(privKey.getX(), params)); + final BigInteger[] sig = signer.generateSignature(SIGN_BYTES); + signer.init(false, new DSAPublicKeyParameters(pubKey.getY(), params)); + return signer.verifySignature(SIGN_BYTES, sig[0], sig[1]); + } catch (RuntimeException e) { + throw new org.cryptacular.CryptoException("Signature computation error", e); + } + } + + + /** + * Determines whether the given RSA public and private keys form a proper key pair by computing and verifying a + * digital signature with the keys. + * + * @param pubKey RSA public key. + * @param privKey RSA private key. + * + * @return True if the keys form a functioning keypair, false otherwise. Errors during signature verification are + * treated as false. + * + * @throws org.cryptacular.CryptoException on key validation errors. + */ + public static boolean isKeyPair(final RSAPublicKey pubKey, final RSAPrivateKey privKey) + throws org.cryptacular.CryptoException + { + final RSADigestSigner signer = new RSADigestSigner(new SHA256Digest()); + try { + signer.init(true, new RSAKeyParameters(true, privKey.getModulus(), privKey.getPrivateExponent())); + signer.update(SIGN_BYTES, 0, SIGN_BYTES.length); + final byte[] sig = signer.generateSignature(); + signer.init(false, new RSAKeyParameters(false, pubKey.getModulus(), pubKey.getPublicExponent())); + signer.update(SIGN_BYTES, 0, SIGN_BYTES.length); + return signer.verifySignature(sig); + } catch (CryptoException e) { + throw new org.cryptacular.CryptoException("Signature computation error", e); + } + } + + + /** + * Determines whether the given EC public and private keys form a proper key pair by computing and verifying a digital + * signature with the keys. + * + * @param pubKey EC public key. + * @param privKey EC private key. + * + * @return True if the keys form a functioning keypair, false otherwise. Errors during signature verification are + * treated as false. + * + * @throws org.cryptacular.CryptoException on key validation errors. + */ + public static boolean isKeyPair(final ECPublicKey pubKey, final ECPrivateKey privKey) + throws org.cryptacular.CryptoException + { + final ECDSASigner signer = new ECDSASigner(); + try { + signer.init(true, ECUtil.generatePrivateKeyParameter(privKey)); + + final BigInteger[] sig = signer.generateSignature(SIGN_BYTES); + signer.init(false, ECUtil.generatePublicKeyParameter(pubKey)); + return signer.verifySignature(SIGN_BYTES, sig[0], sig[1]); + } catch (Exception e) { + throw new org.cryptacular.CryptoException("Signature computation error", e); + } + } + + + /** + * Reads an encoded private key from a file at the given path. Both PKCS#8 and OpenSSL "traditional" formats are + * supported in DER or PEM encoding. See {@link #decodePrivateKey(byte[])} for supported asymmetric algorithms. + * + * @param path Path to private key file. + * + * @return Private key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors reading data from file. + */ + public static PrivateKey readPrivateKey(final String path) throws EncodingException, StreamException + { + return readPrivateKey(new File(path)); + } + + + /** + * Reads an encoded private key from a file. Both PKCS#8 and OpenSSL "traditional" formats are supported in DER or PEM + * encoding. See {@link #decodePrivateKey(byte[])} for supported asymmetric algorithms. + * + * @param file Private key file. + * + * @return Private key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors reading data from file. + */ + public static PrivateKey readPrivateKey(final File file) throws EncodingException, StreamException + { + try { + return readPrivateKey(new FileInputStream(file)); + } catch (FileNotFoundException e) { + throw new StreamException("File not found: " + file); + } + } + + + /** + * Reads an encoded private key from an input stream. Both PKCS#8 and OpenSSL "traditional" formats are supported in + * DER or PEM encoding. See {@link #decodePrivateKey(byte[])} for supported asymmetric algorithms. The {@link + * InputStream} parameter is closed by this method. + * + * @param in Input stream containing private key data. + * + * @return Private key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors reading data from file. + */ + public static PrivateKey readPrivateKey(final InputStream in) throws EncodingException, StreamException + { + return decodePrivateKey(StreamUtil.readAll(in)); + } + + + /** + * Reads an encrypted private key from a file at the given path. Both PKCS#8 and OpenSSL "traditional" formats are + * supported in DER or PEM encoding. See {@link #decodePrivateKey(byte[])} for supported asymmetric algorithms. + * + * @param path Path to private key file. + * @param password Password used to encrypt private key. + * + * @return Private key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors. + */ + public static PrivateKey readPrivateKey(final String path, final char[] password) + throws EncodingException, StreamException + { + return readPrivateKey(new File(path), password); + } + + + /** + * Reads an encrypted private key from a file. Both PKCS#8 and OpenSSL "traditional" formats are supported in DER or + * PEM encoding. See {@link #decodePrivateKey(byte[])} for supported asymmetric algorithms. + * + * @param file Private key file. + * @param password Password used to encrypt private key. + * + * @return Private key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors. + */ + public static PrivateKey readPrivateKey(final File file, final char[] password) + throws EncodingException, StreamException + { + try { + return readPrivateKey(new FileInputStream(file), password); + } catch (FileNotFoundException e) { + throw new StreamException("File not found: " + file); + } + } + + + /** + * Reads an encrypted private key from an input stream. Both PKCS#8 and OpenSSL "traditional" formats are supported in + * DER or PEM encoding. See {@link #decodePrivateKey(byte[])} for supported asymmetric algorithms. The {@link + * InputStream} parameter is closed by this method. + * + * @param in Input stream containing private key data. + * @param password Password used to encrypt private key. + * + * @return Private key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors. + */ + public static PrivateKey readPrivateKey(final InputStream in, final char[] password) + throws EncodingException, StreamException + { + return decodePrivateKey(StreamUtil.readAll(in), password); + } + + + /** + * Decodes an encoded private key in either PKCS#8 or OpenSSL "traditional" format in either DER or PEM encoding. Keys + * from the following asymmetric algorithms are supported: + * + * <ul> + * <li>DSA</li> + * <li>RSA</li> + * <li>Elliptic curve</li> + * </ul> + * + * @param encodedKey Encoded private key data. + * + * @return Private key. + * + * @throws EncodingException on key encoding errors. + */ + public static PrivateKey decodePrivateKey(final byte[] encodedKey) throws EncodingException + { + return decodePrivateKey(encodedKey, null); + } + + + /** + * Decodes an encrypted private key. The following formats are supported: + * + * <ul> + * <li>DER or PEM encoded PKCS#8 format</li> + * <li>PEM encoded OpenSSL "traditional" format</li> + * </ul> + * + * <p>Keys from the following asymmetric algorithms are supported:</p> + * + * <ul> + * <li>DSA</li> + * <li>RSA</li> + * <li>Elliptic curve</li> + * </ul> + * + * @param encryptedKey Encrypted private key data. + * @param password Password used to encrypt private key. + * + * @return Private key. + * + * @throws EncodingException on key encoding errors. + */ + public static PrivateKey decodePrivateKey(final byte[] encryptedKey, final char[] password) throws EncodingException + { + AsymmetricKeyParameter key; + try { + final PKCS8PrivateKeyDecoder decoder = new PKCS8PrivateKeyDecoder(); + key = decoder.decode(encryptedKey, password); + } catch (RuntimeException e) { + final OpenSSLPrivateKeyDecoder decoder = new OpenSSLPrivateKeyDecoder(); + key = decoder.decode(encryptedKey, password); + } + return Converter.convertPrivateKey(key); + } + + + /** + * Reads a DER or PEM-encoded public key from a file. + * + * @param path Path to DER or PEM-encoded public key file. + * + * @return Public key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors. + */ + public static PublicKey readPublicKey(final String path) throws EncodingException, StreamException + { + return readPublicKey(new File(path)); + } + + + /** + * Reads a DER or PEM-encoded public key from a file. + * + * @param file DER or PEM-encoded public key file. + * + * @return Public key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors. + */ + public static PublicKey readPublicKey(final File file) throws EncodingException, StreamException + { + try { + return readPublicKey(new FileInputStream(file)); + } catch (FileNotFoundException e) { + throw new StreamException("File not found: " + file); + } + } + + + /** + * Reads a DER or PEM-encoded public key from data in the given stream. The {@link InputStream} parameter is closed by + * this method. + * + * @param in Input stream containing an encoded key. + * + * @return Public key. + * + * @throws EncodingException on key encoding errors. + * @throws StreamException on IO errors. + */ + public static PublicKey readPublicKey(final InputStream in) throws EncodingException, StreamException + { + return decodePublicKey(StreamUtil.readAll(in)); + } + + + /** + * Decodes public keys formatted in an X.509 SubjectPublicKeyInfo structure in either PEM or DER encoding. + * + * @param encoded Encoded public key bytes. + * + * @return Public key. + * + * @throws EncodingException on key encoding errors. + */ + public static PublicKey decodePublicKey(final byte[] encoded) throws EncodingException + { + return Converter.convertPublicKey(new PublicKeyDecoder().decode(encoded)); + } +}
diff --git a/src/main/java/org/cryptacular/util/NonceUtil.java b/src/main/java/org/cryptacular/util/NonceUtil.java new file mode 100644 index 0000000..da14f56 --- /dev/null +++ b/src/main/java/org/cryptacular/util/NonceUtil.java
@@ -0,0 +1,240 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.lang.reflect.Method; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import javax.crypto.SecretKey; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.prng.EntropySource; +import org.bouncycastle.crypto.prng.SP800SecureRandom; +import org.bouncycastle.crypto.prng.drbg.HashSP800DRBG; +import org.bouncycastle.crypto.prng.drbg.SP80090DRBG; +import org.cryptacular.generator.sp80038a.EncryptedNonce; +import org.cryptacular.generator.sp80038d.RBGNonce; + +/** + * Utility class for generating secure nonce and initialization vectors. + * + * @author Middleware Services + */ +public final class NonceUtil +{ + /** Class-wide random source. */ + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + + /* Seed random source. */ + static + { + // Call nextBytes to force seeding via default process + SECURE_RANDOM.nextBytes(new byte[1]); + } + + /** Private constructor of utility class. */ + private NonceUtil() {} + + + /** + * Generates a nonce of the given size by repetitively concatenating system timestamps (i.e. {@link + * System#nanoTime()}) up to the required size. + * + * @param length Positive number of bytes in nonce. + * + * @return Nonce bytes. + */ + public static byte[] timestampNonce(final int length) + { + if (length <= 0) { + throw new IllegalArgumentException(length + " is invalid. Length must be positive."); + } + + final byte[] nonce = new byte[length]; + int count = 0; + long timestamp; + while (count < length) { + timestamp = System.nanoTime(); + for (int i = 0; i < 8 && count < length; i++) { + nonce[count++] = (byte) (timestamp & 0xFF); + timestamp >>= 8; + } + } + return nonce; + } + + + /** + * Generates a random nonce of the given length in bytes. + * + * @param length Positive number of bytes in nonce. + * + * @return Nonce bytes. + */ + public static byte[] randomNonce(final int length) + { + if (length <= 0) { + throw new IllegalArgumentException(length + " is invalid. Length must be positive."); + } + final byte[] nonce = new byte[length]; + SECURE_RANDOM.nextBytes(nonce); + return nonce; + } + + + /** + * Creates a new entropy source that wraps a {@link SecureRandom} to produce random bytes. + * + * @param length Size of entropy blocks. + * + * @return New random entropy source. + */ + public static EntropySource randomEntropySource(final int length) + { + return new EntropySource() { + @Override + public boolean isPredictionResistant() + { + return true; + } + + @Override + public byte[] getEntropy() + { + final byte[] bytes = new byte[length]; + SECURE_RANDOM.nextBytes(bytes); + return bytes; + } + + @Override + public int entropySize() + { + return length; + } + }; + } + + + /** + * Generates a nonce/IV using the strategy described in NIST <a + * href="http://csrc.nist.gov/publications/nistpubs/800-38D/SP-800-38D.pdf">SP-800-38d</a>, section 8.2.2, "RBG-based + * Construction". The implementation uses a hash-based DRBG based on a SHA-256 digest, and uses random data for all + * bits of the nonce; that is, the fixed field is null. + * + * <p>This nonce generation strategy is suitable for GCM ciphers.</p> + * + * @param length Number of bytes in nonce; MUST be 12 or more. + * + * @return Nonce bytes. + */ + public static byte[] nist80038d(final int length) + { + return new RBGNonce(length).generate(); + } + + + /** + * Generates a random IV according to NIST <a href="http://goo.gl/S9z8qF">SP-800-63a</a>, appendix C, method 1 + * (encrypted nonce), suitable for use with any block cipher mode described in that standard. This method uses an + * instance of {@link EncryptedNonce} for the implementation. + * + * @param cipher Block cipher. + * @param key Encryption key intended for use with IV. + * + * @return Cipher block size number of random bytes. + * + * @see EncryptedNonce + */ + public static byte[] nist80063a(final BlockCipher cipher, final SecretKey key) + { + BlockCipher raw = cipher; + // Get the underlying cipher if there is one + final Method method = ReflectUtil.getMethod(cipher.getClass(), "getUnderlyingCipher"); + if (method != null) { + raw = (BlockCipher) ReflectUtil.invoke(cipher, method); + } + return new EncryptedNonce(raw, key).generate(); + } + + + /** + * Generates a random IV according to NIST <a href="http://goo.gl/S9z8qF">SP-800-63a</a>, appendix C, method 2 + * (pseudorandom), suitable for use with any block cipher mode described in that standard. + * + * @param prng NIST SP800-63a approved pseudorandom number generator. + * @param blockSize Cipher block size in bytes. + * + * @return Cipher block size number of random bytes. + */ + public static byte[] nist80063a(final SP800SecureRandom prng, final int blockSize) + { + prng.setSeed(randomNonce(blockSize)); + + final byte[] iv = new byte[blockSize]; + prng.nextBytes(iv); + return iv; + } + + + /** + * Generates a random IV according to NIST <a href="http://goo.gl/S9z8qF">SP-800-63a</a>, appendix C, method 2 + * (pseudorandom), suitable for use with any block cipher mode described in that standard. Uses an instance of {@link + * RBGNonce} internally with length equal to block size of given cipher. + * + * @param cipher Block cipher. + * + * @return Cipher block size number of random bytes. + * + * @see RBGNonce + */ + public static byte[] nist80063a(final BlockCipher cipher) + { + return new RBGNonce(cipher.getBlockSize()).generate(); + } + + + /** + * Creates a new DRBG instance based on a SHA-256 digest. + * + * @param length Length in bits of values to be produced by DRBG instance. + * + * @return New DRGB instance. + */ + public static SP80090DRBG newRBG(final int length) + { + return newRBG(new SHA256Digest(), length); + } + + + /** + * Creates a new hash-based DRBG instance that uses the given digest as the pseudorandom source. + * + * @param digest Digest algorithm. + * @param length Length in bits of values to be produced by DRBG instance. + * + * @return New DRGB instance. + */ + public static SP80090DRBG newRBG(final Digest digest, final int length) + { + return newRBG(digest, length, randomEntropySource(length)); + } + + /** + * Creates a new hash-based DRBG instance that uses the given digest as the pseudorandom source. + * + * @param digest Digest algorithm. + * @param length Length in bits of values to be produced by DRBG instance. + * @param es Entropy source. + * + * @return New DRGB instance. + */ + public static SP80090DRBG newRBG(final Digest digest, final int length, final EntropySource es) + { + return new HashSP800DRBG( + digest, + length, + es, + Thread.currentThread().getName().getBytes(StandardCharsets.UTF_8), + NonceUtil.timestampNonce(8)); + } +}
diff --git a/src/main/java/org/cryptacular/util/PemUtil.java b/src/main/java/org/cryptacular/util/PemUtil.java new file mode 100644 index 0000000..7f4ea60 --- /dev/null +++ b/src/main/java/org/cryptacular/util/PemUtil.java
@@ -0,0 +1,134 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.util.regex.Pattern; +import org.cryptacular.codec.Base64Decoder; + +/** + * Utility class with helper methods for common PEM encoding operations. + * + * @author Middleware Services + */ +public final class PemUtil +{ + + /** Line length. */ + public static final int LINE_LENGTH = 64; + + /** PEM encoding header start string. */ + public static final String HEADER_BEGIN = "-----BEGIN"; + + /** PEM encoding footer start string. */ + public static final String FOOTER_END = "-----END"; + + /** Procedure type tag for PEM-encoded private key in OpenSSL format. */ + public static final String PROC_TYPE = "Proc-Type:"; + + /** Decryption infor tag for PEM-encoded private key in OpenSSL format. */ + public static final String DEK_INFO = "DEK-Info:"; + + /** Pattern used to split multiple PEM-encoded objects in a single file. */ + private static final Pattern PEM_SPLITTER = Pattern.compile("-----(?:BEGIN|END) [A-Z ]+-----"); + + /** Pattern used to a file by line terminator. */ + private static final Pattern LINE_SPLITTER = Pattern.compile("[\r\n]+"); + + + + /** Private constructor of utility class. */ + private PemUtil() {} + + + /** + * Determines whether the data in the given byte array is base64-encoded data of PEM encoding. The determination is + * made using as little data from the given array as necessary to make a reasonable determination about encoding. + * + * @param data Data to test for PEM encoding + * + * @return True if data appears to be PEM encoded, false otherwise. + */ + public static boolean isPem(final byte[] data) + { + final String start = new String(data, 0, 10, ByteUtil.ASCII_CHARSET).trim(); + if (!start.startsWith(HEADER_BEGIN) && !start.startsWith(PROC_TYPE)) { + // Check all bytes in first line to make sure they are in the range + // of base64 character set encoding + for (int i = 0; i < LINE_LENGTH; i++) { + if (!isBase64Char(data[i])) { + // Last two bytes may be padding character '=' (61) + if (i > LINE_LENGTH - 3) { + if (data[i] != 61) { + return false; + } + } else { + return false; + } + } + } + } + return true; + } + + + /** + * Determines whether the given byte represents an ASCII character in the character set for base64 encoding. + * + * @param b Byte to test. + * + * @return True if the byte represents an ASCII character in the set of valid characters for base64 encoding, false + * otherwise. The padding character '=' is not considered valid since it may only appear at the end of a + * base64 encoded value. + */ + public static boolean isBase64Char(final byte b) + { + return !(b < 47 || b > 122 || b > 57 && b < 65 || b > 90 && b < 97) || b == 43; + } + + + /** + * Decodes a PEM-encoded cryptographic object into the raw bytes of its ASN.1 encoding. Header/footer data and + * metadata info, e.g. Proc-Type, are ignored. + * + * @param pem Bytes of PEM-encoded data to decode. + * + * @return ASN.1 encoded bytes. + */ + public static byte[] decode(final byte[] pem) + { + return decode(new String(pem, ByteUtil.ASCII_CHARSET)); + } + + + /** + * Decodes one or more PEM-encoded cryptographic objects into the raw bytes of their ASN.1 encoding. All header and + * metadata, e.g. Proc-Type, are ignored. If multiple cryptographic objects are represented, the decoded bytes of + * each object are concatenated together and returned. + * + * @param pem PEM-encoded data to decode. + * + * @return ASN.1 encoded bytes. + */ + public static byte[] decode(final String pem) + { + final Base64Decoder decoder = new Base64Decoder(); + final CharBuffer buffer = CharBuffer.allocate(pem.length()); + final ByteBuffer output = ByteBuffer.allocate(pem.length() * 3 / 4); + // There may be multiple PEM-encoded objects in the input + for (String object : PEM_SPLITTER.split(pem)) { + buffer.clear(); + for (String line : LINE_SPLITTER.split(object)) { + if (line.startsWith(DEK_INFO) || line.startsWith(PROC_TYPE)) { + continue; + } + buffer.append(line); + } + buffer.flip(); + decoder.decode(buffer, output); + decoder.finalize(output); + } + output.flip(); + return ByteUtil.toArray(output); + } +}
diff --git a/src/main/java/org/cryptacular/util/ReflectUtil.java b/src/main/java/org/cryptacular/util/ReflectUtil.java new file mode 100644 index 0000000..e56d651 --- /dev/null +++ b/src/main/java/org/cryptacular/util/ReflectUtil.java
@@ -0,0 +1,66 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + * Reflection utilities. + * + * @author Middleware Services + */ +public final class ReflectUtil +{ + + /** Method cache. */ + private static final Map<String, Method> METHOD_CACHE = new HashMap<>(); + + /** Private constructor of utility class. */ + private ReflectUtil() {} + + + /** + * Gets the method defined on the target class. The method is cached to speed up subsequent lookups. + * + * @param target Target class that contains method. + * @param name Method name. + * @param parameters Method parameters. + * + * @return Method if found, otherwise null. + */ + public static Method getMethod(final Class<?> target, final String name, final Class<?>... parameters) + { + final String key = target.getName() + '.' + name; + Method method = METHOD_CACHE.get(key); + if (method != null) { + return method; + } + try { + method = target.getMethod(name, parameters); + METHOD_CACHE.put(key, method); + return method; + } catch (NoSuchMethodException e) { + return null; + } + } + + + /** + * Invokes the method on the target object with the given parameters. + * + * @param target Target class that contains method. + * @param method Method to invoke on target. + * @param parameters Method parameters. + * + * @return Method return value. A void method returns null. + */ + public static Object invoke(final Object target, final Method method, final Object... parameters) + { + try { + return method.invoke(target, parameters); + } catch (Exception e) { + throw new RuntimeException("Failed invoking " + method, e); + } + } +}
diff --git a/src/main/java/org/cryptacular/util/StreamUtil.java b/src/main/java/org/cryptacular/util/StreamUtil.java new file mode 100644 index 0000000..bece540 --- /dev/null +++ b/src/main/java/org/cryptacular/util/StreamUtil.java
@@ -0,0 +1,277 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import org.bouncycastle.util.io.Streams; +import org.cryptacular.StreamException; +import org.cryptacular.io.ChunkHandler; + +/** + * Utility methods for stream handling. + * + * @author Middleware Services + */ +public final class StreamUtil +{ + + /** + * Buffer size of chunked operations, e.g. {@link #pipeAll(java.io.InputStream, java.io.OutputStream, + * org.cryptacular.io.ChunkHandler)}. + */ + public static final int CHUNK_SIZE = 1024; + + /** Private method of utility class. */ + private StreamUtil() {} + + + /** + * Reads all the data from the file at the given path. + * + * @param path Path to file. + * + * @return Byte array of data read from file. + * + * @throws StreamException on stream IO errors. + */ + public static byte[] readAll(final String path) throws StreamException + { + return readAll(new File(path)); + } + + + /** + * Reads all the data from the given file. + * + * @param file File to read. + * + * @return Byte array of data read from file. + * + * @throws StreamException on stream IO errors. + */ + public static byte[] readAll(final File file) throws StreamException + { + final InputStream input = makeStream(file); + try { + return readAll(input, (int) file.length()); + } finally { + closeStream(input); + } + } + + + /** + * Reads all the data from the given input stream. + * + * @param input Input stream to read. + * + * @return Byte array of data read from stream. + * + * @throws StreamException on stream IO errors. + */ + public static byte[] readAll(final InputStream input) throws StreamException + { + return readAll(input, 1024); + } + + + /** + * Reads all the data from the given input stream. + * + * @param input Input stream to read. + * @param sizeHint Estimate of amount of data to be read in bytes. + * + * @return Byte array of data read from stream. + * + * @throws StreamException on stream IO errors. + */ + public static byte[] readAll(final InputStream input, final int sizeHint) throws StreamException + { + final ByteArrayOutputStream output = new ByteArrayOutputStream(sizeHint); + try { + Streams.pipeAll(input, output); + } catch (IOException e) { + throw new StreamException(e); + } finally { + closeStream(input); + closeStream(output); + } + return output.toByteArray(); + } + + + /** + * Reads all data from the given reader. + * + * @param reader Reader over character data. + * + * @return Data read from reader. + * + * @throws StreamException on stream IO errors. + */ + public static String readAll(final Reader reader) throws StreamException + { + return readAll(reader, 1024); + } + + + /** + * Reads all data from the given reader. + * + * @param reader Reader over character data. + * @param sizeHint Estimate of amount of data to be read in number of characters. + * + * @return Data read from reader. + * + * @throws StreamException on stream IO errors. + */ + public static String readAll(final Reader reader, final int sizeHint) throws StreamException + { + final CharArrayWriter writer = new CharArrayWriter(sizeHint); + final char[] buffer = new char[CHUNK_SIZE]; + int len; + try { + while ((len = reader.read(buffer)) > 0) { + writer.write(buffer, 0, len); + } + } catch (IOException e) { + throw new StreamException(e); + } finally { + closeReader(reader); + closeWriter(writer); + } + return writer.toString(); + } + + + /** + * Pipes an input stream into an output stream with chunked processing. + * + * @param in Input stream providing data to process. + * @param out Output stream holding processed data. + * @param handler Arbitrary handler for processing input stream. + * + * @throws StreamException on stream IO errors. + */ + public static void pipeAll(final InputStream in, final OutputStream out, final ChunkHandler handler) + throws StreamException + { + final byte[] buffer = new byte[CHUNK_SIZE]; + int count; + try { + while ((count = in.read(buffer)) > 0) { + handler.handle(buffer, 0, count, out); + } + } catch (IOException e) { + throw new StreamException(e); + } + } + + + /** + * Creates an input stream around the given file. + * + * @param file Input stream source. + * + * @return Input stream around file. + * + * @throws StreamException on stream IO errors. + */ + public static InputStream makeStream(final File file) throws StreamException + { + try { + return new BufferedInputStream(new FileInputStream(file)); + } catch (FileNotFoundException e) { + throw new StreamException(file + " does not exist"); + } + } + + + /** + * Creates a reader around the given file that presumably contains character data. + * + * @param file Reader source. + * + * @return Reader around file. + * + * @throws StreamException on stream IO errors. + */ + public static Reader makeReader(final File file) throws StreamException + { + try { + return new InputStreamReader(new BufferedInputStream(new FileInputStream(file))); + } catch (FileNotFoundException e) { + throw new StreamException(file + " does not exist"); + } + } + + + /** + * Closes the given stream and swallows exceptions that may arise during the process. + * + * @param in Input stream to close. + */ + public static void closeStream(final InputStream in) + { + try { + in.close(); + } catch (IOException e) { + System.err.println("Error closing " + in + ": " + e); + } + } + + + /** + * Closes the given stream and swallows exceptions that may arise during the process. + * + * @param out Output stream to close. + */ + public static void closeStream(final OutputStream out) + { + try { + out.close(); + } catch (IOException e) { + System.err.println("Error closing " + out + ": " + e); + } + } + + + /** + * Closes the given reader and swallows exceptions that may arise during the process. + * + * @param reader Reader to close. + */ + public static void closeReader(final Reader reader) + { + try { + reader.close(); + } catch (IOException e) { + System.err.println("Error closing " + reader + ": " + e); + } + } + + + /** + * Closes the given writer and swallows exceptions that may arise during the process. + * + * @param writer Writer to close. + */ + public static void closeWriter(final Writer writer) + { + try { + writer.close(); + } catch (IOException e) { + System.err.println("Error closing " + writer + ": " + e); + } + } +}
diff --git a/src/main/java/org/cryptacular/x509/ExtensionReader.java b/src/main/java/org/cryptacular/x509/ExtensionReader.java new file mode 100644 index 0000000..0b75dc8 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/ExtensionReader.java
@@ -0,0 +1,311 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509; + +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import org.bouncycastle.asn1.ASN1Encodable; +import org.bouncycastle.asn1.ASN1OctetString; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.x509.AccessDescription; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.BasicConstraints; +import org.bouncycastle.asn1.x509.DistributionPoint; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.cryptacular.EncodingException; + +/** + * Reads X.509v3 extended properties from an {@link java.security.cert.X509Certificate} object. The available properties + * are described in section 4.2 of RFC 2459, http://www.faqs.org/rfcs/rfc2459.html. + * + * @author Middleware Services + */ +public final class ExtensionReader +{ + + /** The X509Certificate whose extension fields will be read. */ + private final X509Certificate certificate; + + + /** + * Creates a new instance that can read extension fields from the given X.509 certificate. + * + * @param cert Certificate to read. + */ + public ExtensionReader(final X509Certificate cert) + { + certificate = cert; + } + + + /** + * Reads the value of the extension given by OID or name as defined in section 4.2 of RFC 2459. + * + * @param extensionOidOrName OID or extension name, e.g. 2.5.29.14 orSubjectK eyIdentifier. In the case of extension + * name, the name is case-sensitive and follows the conventions in RFC 2459. + * + * @return Extension type containing data from requested extension field. + * + * @throws EncodingException On certificate field parse errors. + */ + public ASN1Encodable read(final String extensionOidOrName) throws EncodingException + { + if (extensionOidOrName == null) { + throw new IllegalArgumentException("extensionOidOrName cannot be null."); + } + if (extensionOidOrName.contains(".")) { + return read(ExtensionType.fromOid(extensionOidOrName)); + } else { + return read(ExtensionType.fromName(extensionOidOrName)); + } + } + + + /** + * Reads the value of the given certificate extension field. + * + * @param extension Extension to read from certificate. + * + * @return Extension type containing data from requested extension field. + * + * @throws EncodingException On certificate field parse errors. + */ + public ASN1Encodable read(final ExtensionType extension) + { + byte[] data = certificate.getExtensionValue(extension.getOid()); + if (data == null) { + return null; + } + try { + ASN1Encodable der = ASN1Primitive.fromByteArray(data); + if (der instanceof ASN1OctetString) { + // Strip off octet string "wrapper" + data = ((ASN1OctetString) der).getOctets(); + der = ASN1Primitive.fromByteArray(data); + } + return der; + } catch (Exception e) { + throw new EncodingException("ASN.1 parse error", e); + } + } + + + /** + * Reads the value of the SubjectAlternativeName extension field of the certificate. + * + * @return Collection of subject alternative names or null if the certificate does not define this extension field. + * Note that an empty collection of names is different from a null return value; in the former case the field + * is defined but empty, whereas in the latter the field is not defined on the certificate. + * + * @throws EncodingException On certificate field parse errors. + */ + public GeneralNames readSubjectAlternativeName() throws EncodingException + { + try { + return GeneralNames.getInstance(read(ExtensionType.SubjectAlternativeName)); + } catch (RuntimeException e) { + throw new EncodingException("GeneralNames parse error", e); + } + } + + + /** + * Reads the value of the <code>IssuerAlternativeName</code> extension field of the certificate. + * + * @return Collection of issuer alternative names or null if the certificate does not define this extension field. + * Note that an empty collection of names is different from a null return value; in the former case the field + * is defined but empty, whereas in the latter the field is not defined on the certificate. + * + * @throws EncodingException On certificate field parse errors. + */ + public GeneralNames readIssuerAlternativeName() throws EncodingException + { + try { + return GeneralNames.getInstance(read(ExtensionType.IssuerAlternativeName)); + } catch (RuntimeException e) { + throw new EncodingException("GeneralNames parse error", e); + } + } + + + /** + * Reads the value of the <code>BasicConstraints</code> extension field of the certificate. + * + * @return Basic constraints defined on certificate or null if the certificate does not define the field. + * + * @throws EncodingException On certificate field parse errors. + */ + public BasicConstraints readBasicConstraints() throws EncodingException + { + try { + return BasicConstraints.getInstance(read(ExtensionType.BasicConstraints)); + } catch (RuntimeException e) { + throw new EncodingException("BasicConstraints parse error", e); + } + } + + + /** + * Reads the value of the <code>CertificatePolicies</code> extension field of the certificate. + * + * @return List of certificate policies defined on certificate or null if the certificate does not define the field. + * + * @throws EncodingException On certificate field parse errors. + */ + public List<PolicyInformation> readCertificatePolicies() throws EncodingException + { + final ASN1Encodable data = read(ExtensionType.CertificatePolicies); + if (data == null) { + return null; + } + + try { + final ASN1Sequence sequence = ASN1Sequence.getInstance(data); + final List<PolicyInformation> list = new ArrayList<>(sequence.size()); + for (int i = 0; i < sequence.size(); i++) { + list.add(PolicyInformation.getInstance(sequence.getObjectAt(i))); + } + return list; + } catch (RuntimeException e) { + throw new EncodingException("PolicyInformation parse error", e); + } + } + + + /** + * Reads the value of the <code>SubjectKeyIdentifier</code> extension field of the certificate. + * + * @return Subject key identifier. + * + * @throws EncodingException On certificate field parse errors. + */ + public SubjectKeyIdentifier readSubjectKeyIdentifier() throws EncodingException + { + try { + return SubjectKeyIdentifier.getInstance(read(ExtensionType.SubjectKeyIdentifier)); + } catch (RuntimeException e) { + throw new EncodingException("SubjectKeyIdentifier parse error", e); + } + } + + + /** + * Reads the value of the <code>AuthorityKeyIdentifier</code> extension field of the certificate. + * + * @return Authority key identifier. + * + * @throws EncodingException On certificate field parse errors. + */ + public AuthorityKeyIdentifier readAuthorityKeyIdentifier() throws EncodingException + { + try { + return AuthorityKeyIdentifier.getInstance(read(ExtensionType.AuthorityKeyIdentifier)); + } catch (RuntimeException e) { + throw new EncodingException("AuthorityKeyIdentifier parse error", e); + } + } + + + /** + * Reads the value of the <code>KeyUsage</code> extension field of the certificate. + * + * @return Key usage data or null if extension field is not defined. + * + * @throws EncodingException On certificate field parse errors. + */ + public KeyUsage readKeyUsage() throws EncodingException + { + try { + return KeyUsage.getInstance(read(ExtensionType.KeyUsage)); + } catch (RuntimeException e) { + throw new EncodingException("KeyUsage parse error", e); + } + } + + + /** + * Reads the value of the <code>ExtendedKeyUsage</code> extension field of the certificate. + * + * @return List of supported extended key usages or null if extension is not defined. + * + * @throws EncodingException On certificate field parse errors. + */ + public List<KeyPurposeId> readExtendedKeyUsage() throws EncodingException + { + final ASN1Encodable data = read(ExtensionType.ExtendedKeyUsage); + if (data == null) { + return null; + } + + try { + final ASN1Sequence sequence = ASN1Sequence.getInstance(data); + final List<KeyPurposeId> list = new ArrayList<>(sequence.size()); + for (int i = 0; i < sequence.size(); i++) { + list.add(KeyPurposeId.getInstance(sequence.getObjectAt(i))); + } + return list; + } catch (RuntimeException e) { + throw new EncodingException("KeyPurposeId parse error", e); + } + } + + + /** + * Reads the value of the <code>CRLDistributionPoints</code> extension field of the certificate. + * + * @return List of CRL distribution points or null if extension is not defined. + * + * @throws EncodingException On certificate field parse errors. + */ + public List<DistributionPoint> readCRLDistributionPoints() throws EncodingException + { + final ASN1Encodable data = read(ExtensionType.CRLDistributionPoints); + if (data == null) { + return null; + } + + try { + final ASN1Sequence sequence = ASN1Sequence.getInstance(data); + final List<DistributionPoint> list = new ArrayList<>(sequence.size()); + for (int i = 0; i < sequence.size(); i++) { + list.add(DistributionPoint.getInstance(sequence.getObjectAt(i))); + } + return list; + } catch (RuntimeException e) { + throw new EncodingException("DistributionPoint parse error", e); + } + } + + + /** + * Reads the value of the <code>AuthorityInformationAccess</code> extension field of the certificate. + * + * @return List of access descriptions or null if extension is not defined. + * + * @throws EncodingException On certificate field parse errors. + */ + public List<AccessDescription> readAuthorityInformationAccess() throws EncodingException + { + final ASN1Encodable data = read(ExtensionType.AuthorityInformationAccess); + if (data == null) { + return null; + } + + try { + final ASN1Sequence sequence = ASN1Sequence.getInstance(data); + final List<AccessDescription> list = new ArrayList<>(sequence.size()); + for (int i = 0; i < sequence.size(); i++) { + list.add(AccessDescription.getInstance(sequence.getObjectAt(i))); + } + return list; + } catch (RuntimeException e) { + throw new EncodingException("AccessDescription parse error", e); + } + } + +}
diff --git a/src/main/java/org/cryptacular/x509/ExtensionType.java b/src/main/java/org/cryptacular/x509/ExtensionType.java new file mode 100644 index 0000000..a7d4074 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/ExtensionType.java
@@ -0,0 +1,134 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509; + +/** + * Enumeration of X.509v3 extension fields defined in section 4.2 of RFC 2459. + * + * @author Middleware Services + * @version $Revision: 2745 $ + */ +public enum ExtensionType { + + /** AuthorityInfoAccess extension field. */ + AuthorityInformationAccess("1.3.6.1.5.5.7.1.1", false), + + /** AuthorityKeyIdentifier extension field. */ + AuthorityKeyIdentifier("2.5.29.35", false), + + /** BasicConstraints extension field. */ + BasicConstraints("2.5.29.19", true), + + /** CertificatePolicies extension field. */ + CertificatePolicies("2.5.29.32", false), + + /** CRLDistributionPoints extension field. */ + CRLDistributionPoints("2.5.29.31", false), + + /** ExtendedKeyUsage extension field. */ + ExtendedKeyUsage("2.5.29.37", false), + + /** IssuerAlternativeName extension field. */ + IssuerAlternativeName("2.5.29.18", false), + + /** KeyUsage extension field. */ + KeyUsage("2.5.29.15", true), + + /** NameConstraints extension field. */ + NameConstraints("2.5.29.30", true), + + /** PolicyConstraints extension field. */ + PolicyConstraints("2.5.29.36", false), + + /** PolicyMappings extension field. */ + PolicyMappings("2.5.29.33", false), + + /** PrivateKeyUsage extension field. */ + PrivateKeyUsagePeriod("2.5.29.16", false), + + /** SubjectAlternativeName extension field. */ + SubjectAlternativeName("2.5.29.17", false), + + /** SubjectKeyIdentifier extension field. */ + SubjectKeyIdentifier("2.5.29.14", false), + + /** SubjectDirectoryAttributes extension field. */ + SubjectDirectoryAttributes("2.5.29.9", false); + + + /** Oid value. */ + private final String oid; + + /** Whether this extension is critical according to RFC 2459. */ + private final boolean critical; + + + /** + * Creates a new type with the given OID value. + * + * @param oidString Extension OID value. + * @param criticality True if extension MUST or SHOULD be marked critical under general circumstances, false + * otherwise. + */ + ExtensionType(final String oidString, final boolean criticality) + { + oid = oidString; + critical = criticality; + } + + + /** + * Gets the extension by OID. + * + * @param oid Extension OID value. + * + * @return Extension with given OID value. + * + * @throws IllegalArgumentException If no extension with given OID exists. + */ + public static ExtensionType fromOid(final String oid) + { + for (ExtensionType ext : values()) { + if (ext.getOid().equals(oid)) { + return ext; + } + } + throw new IllegalArgumentException("Invalid X.509v3 extension OID " + oid); + } + + + /** + * Gets the extension by name. + * + * @param name Case-sensitive X.509v3 extension name. The acceptable case of extension names is governed by + * conventions in RFC 2459. + * + * @return Extension with given name. + * + * @throws IllegalArgumentException If no extension with given name exists. + */ + public static ExtensionType fromName(final String name) + { + try { + return ExtensionType.valueOf(ExtensionType.class, name); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid X.509v3 extension name " + name); + } + } + + + /** + * @return True if extension MUST or SHOULD be marked critical under general circumstances according to RFC 2459, + * false otherwise. + */ + public boolean isCritical() + { + return critical; + } + + + /** @return OID value of extension field. */ + public String getOid() + { + return oid; + } +}
diff --git a/src/main/java/org/cryptacular/x509/GeneralNameType.java b/src/main/java/org/cryptacular/x509/GeneralNameType.java new file mode 100644 index 0000000..7129887 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/GeneralNameType.java
@@ -0,0 +1,68 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509; + +/** + * Representation of the options in the CHOICE element describing various categories of the <code>GeneralName</code> + * type defined in section 4.2.1.7 of RFC 2459. + * + * @author Middleware Services + */ +public enum GeneralNameType { + + /** otherName choice element. */ + OtherName, + + /** rfc822Name choice element. */ + RFC822Name, + + /** dNSName choice element. */ + DNSName, + + /** x400Address choice element. */ + X400Address, + + /** directoryName choice element. */ + DirectoryName, + + /** ediPartyName choice element. */ + EdiPartyName, + + /** uniformResourceIdentifier choice element. */ + UniformResourceIdentifier, + + /** iPAddress choice element. */ + IPAddress, + + /** registeredID choice element. */ + RegisteredID; + + + /** Minimum tag number for items in CHOICE definition. */ + public static final int MIN_TAG_NUMBER = 0; + + /** Maximum tag number for items in CHOICE definition. */ + public static final int MAX_TAG_NUMBER = 8; + + + /** + * Gets a name type from the value of the tag in the CHOICE element definition. + * + * @param tagNo Ordinal position of type in CHOICE definition in RFC 2459. + * + * @return Type corresponding to given tag number. + * + * @throws IllegalArgumentException If there is not a general name type corresponding to the given tag number. + */ + public static GeneralNameType fromTagNumber(final int tagNo) + { + if (tagNo < MIN_TAG_NUMBER || tagNo > MAX_TAG_NUMBER) { + throw new IllegalArgumentException("Invalid tag number " + tagNo); + } + for (GeneralNameType type : values()) { + if (type.ordinal() == tagNo) { + return type; + } + } + throw new IllegalArgumentException("Invalid tag number " + tagNo); + } +}
diff --git a/src/main/java/org/cryptacular/x509/KeyUsageBits.java b/src/main/java/org/cryptacular/x509/KeyUsageBits.java new file mode 100644 index 0000000..83782e6 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/KeyUsageBits.java
@@ -0,0 +1,120 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509; + +import java.util.BitSet; +import org.bouncycastle.asn1.x509.KeyUsage; + +/** + * Representation of the bit meanings in the <code>KeyUsage</code> BIT STRING type defined in section 4.2.1.3 of RFC + * 2459. + * + * @author Middleware Services + * @version $Revision: 2745 $ + */ +public enum KeyUsageBits { + + /** digitalSignature bit. */ + DigitalSignature(7), + + /** nonRepudiation bit. */ + NonRepudiation(6), + + /** keyEncipherment bit. */ + KeyEncipherment(5), + + /** dataEncipherment bit. */ + DataEncipherment(4), + + /** keyAgreement bit. */ + KeyAgreement(3), + + /** keyCertSign bit. */ + KeyCertSign(2), + + /** cRLSign bit. */ + CRLSign(1), + + /** encipherOnly bit. */ + EncipherOnly(0), + + /** decipherOnly bit. */ + DecipherOnly(15); + + + /** Bit mask offset. */ + private final int offset; + + + /** + * Creates a bit flag with the given bit mask offset. + * + * @param offset Bit mask offset. + */ + KeyUsageBits(final int offset) + { + this.offset = offset; + } + + + /** @return Bit mask value. */ + public int getMask() + { + return 1 << offset; + } + + + /** + * Determines whether this key usage bit is set in the given key usage value. + * + * @param keyUsage BC key usage object. + * + * @return True if bit is set, false otherwise. + */ + public boolean isSet(final KeyUsage keyUsage) + { + return isSet(keyUsage.getBytes()); + } + + + /** + * Determines whether this key usage bit is set in the given key usage bit string. + * + * @param bitString Key usage bit string as a byte array. + * + * @return True if bit is set, false otherwise. + */ + public boolean isSet(final byte[] bitString) + { + return BitSet.valueOf(bitString).get(offset); + } + + + /** + * Determines whether this key usage bit is set in the given key usage bit string. + * + * @param bitString Key usage bit string as a big endian integer. + * + * @return True if bit is set, false otherwise. + */ + public boolean isSet(final int bitString) + { + return (bitString & getMask()) >> offset == 1; + } + + + /** + * Computes the key usage value from one or more key usage bits. + * + * @param bits One ore more key usage bits. + * + * @return Key usage bit string as an integer. + */ + public static int usage(final KeyUsageBits... bits) + { + int usage = 0; + for (KeyUsageBits bit : bits) { + usage |= bit.getMask(); + } + return usage; + } +}
diff --git a/src/main/java/org/cryptacular/x509/dn/Attribute.java b/src/main/java/org/cryptacular/x509/dn/Attribute.java new file mode 100644 index 0000000..d6e7738 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/Attribute.java
@@ -0,0 +1,51 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +/** + * Simple implementation of the X.501 AttributeTypeAndValue that makes up the RelativeDistinguishedName type described + * in section 4.1.2.4 of RFC 2459. + * + * @author Middleware Services + */ +public class Attribute +{ + + /** Attribute type. */ + private final AttributeType type; + + /** Attribute value. */ + private final String value; + + + /** + * Creates a new instance of the given type and value. + * + * @param type Attribute type. + * @param value Attribute value. + */ + public Attribute(final AttributeType type, final String value) + { + if (type == null) { + throw new IllegalArgumentException("Type cannot be null."); + } + this.type = type; + if (value == null) { + throw new IllegalArgumentException("Value cannot be null."); + } + this.value = value; + } + + + /** @return Attribute type. */ + public AttributeType getType() + { + return type; + } + + + /** @return Attribute value. */ + public String getValue() + { + return value; + } +}
diff --git a/src/main/java/org/cryptacular/x509/dn/AttributeType.java b/src/main/java/org/cryptacular/x509/dn/AttributeType.java new file mode 100644 index 0000000..2d53d11 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/AttributeType.java
@@ -0,0 +1,18 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +/** + * Describes values of AttributeType that may appear in a RelativeDistinguishedName (RDN) as defined in section 2 of RFC + * 2253. + * + * @author Middleware Services + */ +public interface AttributeType +{ + + /** @return Attribute OID. */ + String getOid(); + + /** @return Attribute name. */ + String getName(); +}
diff --git a/src/main/java/org/cryptacular/x509/dn/Attributes.java b/src/main/java/org/cryptacular/x509/dn/Attributes.java new file mode 100644 index 0000000..a4eef35 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/Attributes.java
@@ -0,0 +1,117 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Ordered list of {@link Attribute}s. + * + * @author Middleware Services + */ +public class Attributes implements Iterable<Attribute> +{ + + /** Underlying attributes. */ + private final List<Attribute> attributes = new ArrayList<>(5); + + + /** + * Adds an attribute by type and value to the end of the attribute list. + * + * @param typeOid OID of attribute type. + * @param value Attribute value. + */ + public void add(final String typeOid, final String value) + { + final StandardAttributeType type = StandardAttributeType.fromOid(typeOid); + if (type != null) { + add(new Attribute(type, value)); + } else { + add(new Attribute(new UnknownAttributeType(typeOid), value)); + } + } + + + /** + * Adds the given attribute to the end of the attribute list. + * + * @param attr Non-null attribute. + */ + public void add(final Attribute attr) + { + if (attr == null) { + throw new IllegalArgumentException("Attribute cannot be null"); + } + attributes.add(attr); + } + + + /** + * Gets the number of attributes contained in this instance. + * + * @return Number of attributes. + */ + public int size() + { + return attributes.size(); + } + + + /** + * Gets an immutable list of attributes. + * + * @return Non-null immutable attribute list. + */ + public List<Attribute> getAll() + { + return Collections.unmodifiableList(attributes); + } + + + /** + * Gets an immutable list of all attributes of the given type. The order of the returned list reflects the ordering of + * the underlying attributes. + * + * @param type Attribute type. + * + * @return Non-null list of attributes of given type. An empty list is returned if there are no attributes of the + * given type. + */ + public List<String> getValues(final AttributeType type) + { + final List<String> values = new ArrayList<>(attributes.size()); + values.addAll( + attributes.stream().filter( + attr -> attr.getType().equals(type)).map(Attribute::getValue).collect(Collectors.toList())); + return Collections.unmodifiableList(values); + } + + + /** + * Gets the first value of the given type that appears in the attribute list. + * + * @param type Attribute type. + * + * @return Value of first attribute of given type or null if no attributes of given type exist. + */ + public String getValue(final AttributeType type) + { + for (Attribute attr : attributes) { + if (attr.getType().equals(type)) { + return attr.getValue(); + } + } + return null; + } + + + @Override + public Iterator<Attribute> iterator() + { + return attributes.iterator(); + } +}
diff --git a/src/main/java/org/cryptacular/x509/dn/LdapNameFormatter.java b/src/main/java/org/cryptacular/x509/dn/LdapNameFormatter.java new file mode 100644 index 0000000..632d031 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/LdapNameFormatter.java
@@ -0,0 +1,110 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import javax.security.auth.x500.X500Principal; +import org.cryptacular.codec.HexEncoder; + +/** + * Produces a string representation of an X.500 distinguished name using the process described in section 2 of RFC 2253, + * LADPv3 Distinguished Names. + * + * @author Middleware Services + */ +public class LdapNameFormatter implements NameFormatter +{ + + /** Separator character between RDN components. */ + public static final char RDN_SEPARATOR = ','; + + /** Separator character between ATV components in the same RDN element. */ + public static final char ATV_SEPARATOR = '+'; + + /** Escape character. */ + public static final char ESCAPE_CHAR = '\\'; + + /** String of characters that need to be escaped. */ + public static final String RESERVED_CHARS = ",+\"\\<>;"; + + /** Handles hex encoding. */ + private static final HexEncoder ENCODER = new HexEncoder(); + + + @Override + public String format(final X500Principal dn) + { + final StringBuilder builder = new StringBuilder(); + final RDNSequence sequence = NameReader.readX500Principal(dn); + int i = 0; + for (RDN rdn : sequence.backward()) { + if (i++ > 0) { + builder.append(RDN_SEPARATOR); + } + + int j = 0; + for (Attribute attr : rdn.getAttributes()) { + if (j++ > 0) { + builder.append(ATV_SEPARATOR); + } + builder.append(attr.getType()).append('='); + + final AttributeType type = attr.getType(); + if (type instanceof StandardAttributeType) { + escape(attr.getValue(), builder); + } else { + encode(attr.getValue(), builder); + } + } + } + return builder.toString(); + } + + + /** + * Appends the given value to the output with proper character escaping. + * + * @param value Value to escape. + * @param output String builder where escaped value is written. + */ + private static void escape(final String value, final StringBuilder output) + { + char c = value.charAt(0); + if (c == ' ' || c == '#') { + output.append(ESCAPE_CHAR); + } + output.append(c); + + final int nmax = value.length() - 1; + for (int n = 1; n < nmax; n++) { + c = value.charAt(n); + if (RESERVED_CHARS.indexOf(c) > -1) { + output.append(ESCAPE_CHAR); + } + output.append(c); + } + c = value.charAt(nmax); + if (c == ' ') { + output.append(ESCAPE_CHAR); + } + output.append(c); + } + + + /** + * Appends the given value to the output using the HEX encoding method described in section 2.4. + * + * @param value Value to encode. + * @param output String builder where encoded value is written. + */ + private static void encode(final String value, final StringBuilder output) + { + output.append('#'); + + final byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + final CharBuffer out = CharBuffer.allocate(bytes.length * 2); + ENCODER.encode(ByteBuffer.wrap(bytes), out); + output.append(out.flip()); + } +}
diff --git a/src/main/java/org/cryptacular/x509/dn/NameFormatter.java b/src/main/java/org/cryptacular/x509/dn/NameFormatter.java new file mode 100644 index 0000000..af045a8 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/NameFormatter.java
@@ -0,0 +1,22 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +import javax.security.auth.x500.X500Principal; + +/** + * Strategy pattern interface for producing a string representation of an X.500 distinguished name. + * + * @author Middleware Services + */ +public interface NameFormatter +{ + + /** + * Produces a string representation of the given X.500 principal. + * + * @param dn Distinguished name as an X.500 principal. + * + * @return String representation of DN. + */ + String format(X500Principal dn); +}
diff --git a/src/main/java/org/cryptacular/x509/dn/NameReader.java b/src/main/java/org/cryptacular/x509/dn/NameReader.java new file mode 100644 index 0000000..7564aa5 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/NameReader.java
@@ -0,0 +1,91 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +import java.security.cert.X509Certificate; +import javax.security.auth.x500.X500Principal; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.X500Name; + +/** + * Reads X.509 subject and issuer DNs as a raw sequence of attributes to facilitate precise handling of name parsing. + * + * @author Middleware Services + */ +public class NameReader +{ + + /** Certificate to read. */ + private final X509Certificate certificate; + + + /** + * Creates a new instance to support reading subject and issuer information on the given certificate. + * + * @param cert Certificate to read. + */ + public NameReader(final X509Certificate cert) + { + if (cert == null) { + throw new IllegalArgumentException("Certificate cannot be null."); + } + this.certificate = cert; + } + + + /** + * Reads the subject field from the certificate. + * + * @return Subject DN as an RDN sequence. + */ + public RDNSequence readSubject() + { + return readX500Principal(certificate.getSubjectX500Principal()); + } + + + /** + * Reads the issuer field from the certificate. + * + * @return Issuer DN as an RDN sequence. + */ + public RDNSequence readIssuer() + { + return readX500Principal(certificate.getIssuerX500Principal()); + } + + + /** + * Converts the given X.500 principal to a list of relative distinguished names that contains the attributes + * comprising the DN. + * + * @param principal Principal to convert. + * + * @return X500 principal as an RDN sequence. + */ + public static RDNSequence readX500Principal(final X500Principal principal) + { + return readX500Name(X500Name.getInstance(principal.getEncoded())); + } + + + /** + * Converts the given X.500 name to a list of relative distinguished names that contains the attributes + * comprising the DN. + * + * @param name X.500 name. + * + * @return X.500 name as an RDN sequence. + */ + public static RDNSequence readX500Name(final X500Name name) + { + final RDNSequence sequence = new RDNSequence(); + for (org.bouncycastle.asn1.x500.RDN rdn : name.getRDNs()) { + final Attributes attributes = new Attributes(); + for (AttributeTypeAndValue tv : rdn.getTypesAndValues()) { + attributes.add(tv.getType().getId(), tv.getValue().toString()); + } + sequence.add(new RDN(attributes)); + } + return sequence; + } +}
diff --git a/src/main/java/org/cryptacular/x509/dn/RDN.java b/src/main/java/org/cryptacular/x509/dn/RDN.java new file mode 100644 index 0000000..be2a3e3 --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/RDN.java
@@ -0,0 +1,35 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +/** + * Simple implementation of the X.501 RelativeDistinguishedName type described in section 4.1.2.4 of RFC 2459. + * + * @author Middleware Services + */ +public class RDN +{ + + /** RDN attributes. */ + private final Attributes attributes; + + + /** + * Creates a new instance with given attributes. + * + * @param attributes Container for one or more AttributeTypeAndValues. + */ + public RDN(final Attributes attributes) + { + if (attributes == null) { + throw new IllegalArgumentException("Attributes cannot be null"); + } + this.attributes = attributes; + } + + + /** @return RDN attributes. */ + public Attributes getAttributes() + { + return attributes; + } +}
diff --git a/src/main/java/org/cryptacular/x509/dn/RDNSequence.java b/src/main/java/org/cryptacular/x509/dn/RDNSequence.java new file mode 100644 index 0000000..53ba5aa --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/RDNSequence.java
@@ -0,0 +1,126 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; + +/** + * Simple implementation of the X.501 RDNSequence type described in section 4.1.2.4 of RFC 2459. + * + * @author Middleware Services + */ +public class RDNSequence implements Iterable<RDN> +{ + + /** Maintains the list/sequence of RDNs. */ + private final List<RDN> rdns = new ArrayList<>(10); + + + /** + * Adds an RDN to the sequence. + * + * @param rdn RDN to add. + */ + public void add(final RDN rdn) + { + rdns.add(rdn); + } + + + @Override + public Iterator<RDN> iterator() + { + return rdns.iterator(); + } + + + /** @return Iterable that moves backward over the RDN sequence. */ + public Iterable<RDN> backward() + { + return + () -> new Iterator<RDN>() { + + /** List iterator. */ + private final ListIterator<RDN> it = rdns.listIterator(rdns.size()); + + @Override + public boolean hasNext() + { + return it.hasPrevious(); + } + + @Override + public RDN next() + { + return it.previous(); + } + + @Override + public void remove() + { + throw new UnsupportedOperationException("Remove not supported"); + } + }; + } + + + /** + * Gets an immutable list of all attributes of the given type. The order of the returned list reflects the ordering of + * the RDNs and their attributes. + * + * @param type Attribute type. + * + * @return Non-null list of attributes of given type. An empty list is returned if there are no attributes of the + * given type. + */ + public List<String> getValues(final AttributeType type) + { + final List<String> values = new ArrayList<>(rdns.size()); + for (RDN rdn : rdns) { + values.addAll(rdn.getAttributes().getValues(type)); + } + return Collections.unmodifiableList(values); + } + + + /** + * Gets the first value of the given type that appears in the attribute list of any RDN in the sequence. + * + * @param type Attribute type. + * + * @return Value of first attribute of given type or null if no attributes of given type exist. + */ + public String getValue(final AttributeType type) + { + final List<String> values = getValues(type); + if (!values.isEmpty()) { + return values.get(0); + } + return null; + } + + /** + * Creates a comma-separated list of TYPE=VALUE tokens from the attributes in the list in order. + * + * @return String representation that resembles an X.509 distinguished name, e.g. <code>CN=foo, OU=Bar, dc=example, + * dc=com</code>. + */ + @Override + public String toString() + { + final StringBuilder builder = new StringBuilder(); + int i = 0; + for (RDN rdn : this) { + for (Attribute attr : rdn.getAttributes()) { + if (i++ > 0) { + builder.append(", "); + } + builder.append(attr.getType()).append('=').append(attr.getValue()); + } + } + return builder.toString(); + } +}
diff --git a/src/main/java/org/cryptacular/x509/dn/StandardAttributeType.java b/src/main/java/org/cryptacular/x509/dn/StandardAttributeType.java new file mode 100644 index 0000000..c9f81da --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/StandardAttributeType.java
@@ -0,0 +1,179 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +/** + * Describes the registered values of AttributeType that may appear in a RelativeDistinguishedName (RDN) as defined in + * section 2 of RFC 2253. + * + * <p>Enumeration values include attributes likely to appear in an X.509 RDN, which were obtained from the following + * sources:</p> + * + * <ul> + * <li>RFC 4519 Lightweight Directory Access Protocol (LDAP): Schema for User Applications</li> + * <li>RFC 4524 COSINE LDAP/X.500 Schema</li> + * <li>PKCS #9 v2.0: Selected Object Classes and Attribute Types</li> + * </ul> + * + * @author Middleware Services + * @version $Revision: 2745 $ + */ +public enum StandardAttributeType implements AttributeType { + + /** CN - RFC 4519 section 2.3. */ + CommonName("2.5.4.3", "CN"), + + /** C - RFC 4519 section 2.2. */ + CountryName("2.5.4.6", "C"), + + /** DESCRIPTION - RFC 4519 section 2.5. */ + Description("2.5.4.13", "DESCRIPTION"), + + /** DNQUALIFIER - RFC 4519 section 2.8. */ + DnQualifier("2.5.4.46", "DNQUALIFIER"), + + /** DC - RFC 4519 section 2.4. */ + DomainComponent("0.9.2342.19200300.100.1.25", "DC"), + + /** Email address - PKCS#9 v2.0 section B.3.5. */ + EmailAddress("1.2.840.113549.1.9.1", "EMAILADDRESS"), + + /** GenerationQualifier - RFC 4519 section 2.11. */ + GenerationQualifier("2.5.4.44", "GENERATIONQUALIFIER"), + + /** GIVENNAME - RFC 4519 section 2.12. */ + GivenName("2.5.4.42", "GIVENNAME"), + + /** INITIALS - RFC 4519 section 2.14. */ + Initials("2.5.4.43", "INITIALS"), + + /** L - RFC 4519 section 2.16. */ + LocalityName("2.5.4.7", "L"), + + /** MAIL - RFC 4524 section 2.16. */ + Mail("0.9.2342.19200300.100.1.3", "MAIL"), + + /** NAME - RFC 4519 section 2.18. */ + Name("2.5.4.41", "NAME"), + + /** O - RFC 4519 section 2.19. */ + OrganizationName("2.5.4.10", "O"), + + /** OU - RFC 4519 section 2.20. */ + OrganizationalUnitName("2.5.4.11", "OU"), + + /** POSTALADDRESS - RFC 4519 section 2.23. */ + PostalAddress("2.5.4.16", "POSTALADDRESS"), + + /** POSTALCODE - RFC 4519 section 2.24. */ + PostalCode("2.5.4.17", "POSTALCODE"), + + /** POSTOFFICEBOX - RFC 4519 section 2.25. */ + PostOfficeBox("2.5.4.18", "POSTOFFICEBOX"), + + /** SERIALNUMBER - RFC 4519 section 2.31. */ + SerialNumber("2.5.4.5", "SERIALNUMBER"), + + /** ST - RFC 4519 section 2.33. */ + StateOrProvinceName("2.5.4.8", "ST"), + + /** STREET - RFC 4519 section 2.34. */ + StreetAddress("2.5.4.9", "STREET"), + + /** SN - RFC 4519 section 2.32. */ + Surname("2.5.4.4", "SN"), + + /** TELEPHONENUMBER - RFC 4519 section 2.35. */ + TelephoneNumber("2.5.4.20", "TELEPHONENUMBER"), + + /** TITLE - RFC 4519 section 2.38. */ + Title("2.5.4.12", "TITLE"), + + /** UNIQUEIDENTIFIER - RFC 4524 section 2.24. */ + UniqueIdentifier("0.9.2342.19200300.100.1.44", "UNIQUEIDENTIFIER"), + + /** UID - RFC 4519 section 2.39. */ + UserId("0.9.2342.19200300.100.1.1", "UID"); + + + /** OID of RDN attribute type. */ + private final String oid; + + /** Display string of the type in an RDN. */ + private final String name; + + + /** + * Creates a new type for the given OID. + * + * @param attributeTypeOid OID of attribute type. + * @param shortName Registered short name for the attribute type. + */ + StandardAttributeType(final String attributeTypeOid, final String shortName) + { + oid = attributeTypeOid; + name = shortName; + } + + + /** @return OID of attribute type. */ + @Override + public String getOid() + { + return oid; + } + + + /** @return Registered short name of attribute type. */ + @Override + public String getName() + { + return name; + } + + + /** @return Attribute name. */ + @Override + public String toString() + { + return name; + } + + + /** + * Gets the attribute type whose OID is the given string. + * + * @param oid OID of attribute type to get. + * + * @return Attribute type whose OID matches given value or none if there is no standard attribute type matching the + * given OID. + */ + public static StandardAttributeType fromOid(final String oid) + { + for (StandardAttributeType t : StandardAttributeType.values()) { + if (t.getOid().equals(oid)) { + return t; + } + } + return null; + } + + + /** + * Gets the attribute type whose name is the given string. + * + * @param name Name of attribute to get, where the name is the all-caps RFC/standard name that would be returned by + * {@link #getName()} for the desired attribute. + * + * @return Attribute type whose {@link #getName()} property matches the given value or null if there is no standard + * attribute with the given name. + */ + public static AttributeType fromName(final String name) + { + for (AttributeType t : StandardAttributeType.values()) { + if (t.getName().equals(name)) { + return t; + } + } + return null; + } +}
diff --git a/src/main/java/org/cryptacular/x509/dn/UnknownAttributeType.java b/src/main/java/org/cryptacular/x509/dn/UnknownAttributeType.java new file mode 100644 index 0000000..d9d5f3e --- /dev/null +++ b/src/main/java/org/cryptacular/x509/dn/UnknownAttributeType.java
@@ -0,0 +1,52 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +import java.util.regex.Pattern; + +/** + * Describes a non-standard AttributeType in dotted decimal form that may appear in a RelativeDistinguishedName (RDN) as + * defined in section 2 of RFC 2253. + * + * @author Middleware Services + */ +public class UnknownAttributeType implements AttributeType +{ + + /** Dotted decimal OID pattern. */ + private static final Pattern PATTERN = Pattern.compile("[0-9]+(.[0-9]+)*"); + + /** Attribute type OID. */ + private final String oid; + + + /** + * Creates a new instance from the given oid. + * + * @param attributeTypeOid Attribute type OID. + */ + public UnknownAttributeType(final String attributeTypeOid) + { + if (!PATTERN.matcher(attributeTypeOid).matches()) { + throw new IllegalArgumentException(attributeTypeOid + " is not an OID"); + } + this.oid = attributeTypeOid; + } + + @Override + public String getOid() + { + return oid; + } + + @Override + public String getName() + { + return oid; + } + + @Override + public String toString() + { + return oid; + } +}
diff --git a/src/main/spotbugs/exclude.xml b/src/main/spotbugs/exclude.xml new file mode 100644 index 0000000..b614caa --- /dev/null +++ b/src/main/spotbugs/exclude.xml
@@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="UTF-8"?> +<FindBugsFilter + xmlns="https://github.com/spotbugs/filter/4.8.4" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="https://github.com/spotbugs/filter/4.8.4 + https://raw.githubusercontent.com/spotbugs/spotbugs/4.8.4/spotbugs/etc/findbugsfilter.xsd"> + + <!-- See https://spotbugs.readthedocs.io/en/latest/bugDescriptions.html --> + + <!-- Allow platform specific encoding --> + <Match> + <Class name="org.cryptacular.CiphertextHeader" /> + <Bug pattern="DM_DEFAULT_ENCODING" /> + </Match> + + <!-- Result of InputStream#read is ignored --> + <Match> + <Class name="org.cryptacular.CiphertextHeader" /> + <Method name="decode" /> + <Bug pattern="RR_NOT_CHECKED" /> + </Match> + + <!-- Allow constructors to throw exceptions --> + <Match> + <Bug pattern="CT_CONSTRUCTOR_THROW" /> + </Match> + + <!-- Allow platform specific encoding when reading and writing files --> + <Match> + <Or> + <Class name="org.cryptacular.util.StreamUtil" /> + <Class name="org.cryptacular.io.DecodingInputStream" /> + <Class name="org.cryptacular.io.EncodingOutputStream" /> + </Or> + <Bug pattern="DM_DEFAULT_ENCODING" /> + </Match> + + <!-- This check appears broken, SecureRandom is not discarded --> + <Match> + <Or> + <Class name="org.cryptacular.generator.RandomIdGenerator" /> + <Class name="org.cryptacular.util.NonceUtil" /> + </Or> + <Bug pattern="DMI_RANDOM_USED_ONLY_ONCE" /> + </Match> + + <!-- Byte array streams do not need to be closed --> + <Match> + <Class name="org.cryptacular.asn.OpenSSLPrivateKeyDecoder" /> + <Method name="decodeASN1" /> + <Bug pattern="OS_OPEN_STREAM" /> + </Match> + + <!-- Internal representation is exposed throughout the API --> + <Match> + <Or> + <Bug pattern="EI_EXPOSE_REP" /> + <Bug pattern="EI_EXPOSE_REP2" /> + </Or> + </Match> + +</FindBugsFilter>
diff --git a/src/openssl/gen-test-cert.sh b/src/openssl/gen-test-cert.sh new file mode 100755 index 0000000..360c511 --- /dev/null +++ b/src/openssl/gen-test-cert.sh
@@ -0,0 +1,11 @@ +#!/bin/sh + +if [ $# -lt 1 ]; then + echo "USAGE: `basename $0` path/to/cert/file" + exit +fi +CSR=request.csr +openssl req -config openssl.cnf -new -key test-key.pem -out $CSR +openssl ca -config openssl.cnf -days 10000 -in request.csr -out $1 +rm -f $CSR +
diff --git a/src/openssl/openssl.cnf b/src/openssl/openssl.cnf new file mode 100644 index 0000000..15b2756 --- /dev/null +++ b/src/openssl/openssl.cnf
@@ -0,0 +1,257 @@ +# +# OpenSSL example configuration file. +# This is mostly being used for generation of certificate requests. +# + +# This definition stops the following lines choking if HOME isn't +# defined. +HOME = . +RANDFILE = $ENV::HOME/.rnd + +# Extra OBJECT IDENTIFIER info: +#oid_file = $ENV::HOME/.oid +oid_section = new_oids + +# To use this configuration file with the "-extfile" option of the +# "openssl x509" utility, name here the section containing the +# X.509v3 extensions to use: +# extensions = +# (Alternatively, use a configuration file that has only +# X.509v3 extensions in its main [= default] section.) + +[ new_oids ] + +# We can add new OIDs in here for use by 'ca' and 'req'. +# Add a simple OID like this: +# testoid1=1.2.3.4 +# Or use config file substitution like this: +# testoid2=${testoid1}.5.6 + +#################################################################### +[ ca ] +default_ca = CA_default # The default ca section + +#################################################################### +[ CA_default ] + +dir = ./testCA # Where everything is kept +certs = $dir/certs # Where the issued certs are kept +crl_dir = $dir/crl # Where the issued crl are kept +database = $dir/index.txt # database index file. +new_certs_dir = $dir/newcerts # default place for new certs. + +certificate = $dir/ca/cacert.pem # The CA certificate +serial = $dir/serial # The current serial number +crl = $dir/crl.pem # The current CRL +private_key = $dir/ca/cakey.pem # The private key +RANDFILE = $dir/private/.rand # private random number file + +x509_extensions = usr_cert # The extentions to add to the cert + +# Comment out the following two lines for the "traditional" +# (and highly broken) format. +name_opt = ca_default # Subject Name options +cert_opt = ca_default # Certificate field options + +# Extension copying option: use with caution. +# copy_extensions = copy + +# Extensions to add to a CRL. Note: Netscape communicator chokes on V2 CRLs +# so this is commented out by default to leave a V1 CRL. +# crl_extensions = crl_ext + +default_days = 10000 # how long to certify for +default_crl_days= 30 # how long before next CRL +default_md = sha256 # which md to use. +preserve = no # keep passed DN ordering + +# A few difference way of specifying how similar the request should look +# For type CA, the listed attributes must be the same, and the optional +# and supplied fields are just that :-) +policy = policy_match + +# For the CA policy +[ policy_match ] +countryName = match +stateOrProvinceName = match +organizationName = match +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +# For the 'anything' policy +# At this point in time, you must list all acceptable 'object' +# types. +[ policy_anything ] +countryName = optional +stateOrProvinceName = optional +localityName = optional +organizationName = optional +organizationalUnitName = optional +commonName = supplied +emailAddress = optional + +#################################################################### +[ req ] +default_bits = 2048 +default_keyfile = serverkey.pem +distinguished_name = req_distinguished_name +attributes = req_attributes +x509_extensions = v3_ca # The extentions to add to the self signed cert + +# Passwords for private keys if not present they will be prompted for +# input_password = secret +# output_password = secret + +# This sets a mask for permitted string types. There are several options. +# default: PrintableString, T61String, BMPString. +# pkix : PrintableString, BMPString. +# utf8only: only UTF8Strings. +# nombstr : PrintableString, T61String (no BMPStrings or UTF8Strings). +# MASK:XXXX a literal mask value. +# WARNING: current versions of Netscape crash on BMPStrings or UTF8Strings +# so use this option with caution! +string_mask = nombstr + +# req_extensions = v3_req # The extensions to add to a certificate request + +[ req_distinguished_name ] + +0.domainComponent = Domain Component 1 (eg, edu, com) +0.domainComponent_default = com + +1.domainComponent = Domain Component 2 (eg, vt) +1.domainComponent_default = example + +countryName = Country Name (2 letter code) +countryName_default = US +countryName_min = 2 +countryName_max = 2 + +stateOrProvinceName = State or Province Name (full name) +stateOrProvinceName_default = New York + +localityName = Locality Name (eg, city) +localityName_default = New York + +0.organizationName = Organization Name (eg, company) +0.organizationName_default = Snake Oil Unlimited + +commonName = Common Name (eg, YOUR server DNS name eg. server.dept.vt.edu must be less than 64 charactors) +commonName_max = 64 +commonName_default = something.example.com + +#emailAddress = Email Address +#emailAddress_max = 64 + +# SET-ex3 = SET extension number 3 + +[ req_attributes ] +#challengePassword = A challenge password +#challengePassword_min = 4 +#challengePassword_max = 20 + +#unstructuredName = An optional company name + +[ usr_cert ] + +# These extensions are added when 'ca' signs a request. + +# This goes against PKIX guidelines but some CAs do it and some software +# requires this to avoid interpreting an end user certificate as a CA. + +basicConstraints=CA:FALSE + +# Here are some examples of the usage of nsCertType. If it is omitted +# the certificate can be used for anything *except* object signing. + +# This is OK for an SSL server. +# nsCertType = server + +# For an object signing certificate this would be used. +# nsCertType = objsign + +# For normal client use this is typical +# nsCertType = client, email + +# and for everything including object signing: +# nsCertType = client, email, objsign + +# This is typical in keyUsage for a client certificate. +# keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +# This will be displayed in Netscape's comment listbox. +nsComment = "OpenSSL Generated Certificate" + +# PKIX recommendations harmless if included in all certificates. +subjectKeyIdentifier=hash +authorityKeyIdentifier=keyid,issuer:always + +# This stuff is for subjectAltName and issuerAltname. +# Import the email address. +# subjectAltName=email:copy +# An alternative to produce certificates that aren't +# deprecated according to PKIX. +# subjectAltName=email:move + +# Copy subject details +issuerAltName=DNS:snake-1.example.com, DNS:snake-2.example.com + +nsCaRevocationUrl=http://crl.example.com/ca-crl.pem +#nsBaseUrl +#nsRevocationUrl +#nsRenewalUrl +#nsCaPolicyUrl +#nsSslServerName + +[ v3_req ] + +# Extensions to add to a certificate request + +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment + +[ v3_ca ] + + +# Extensions for a typical CA + + +# PKIX recommendation. + +subjectKeyIdentifier=hash + +authorityKeyIdentifier=keyid:always,issuer:always + +# This is what PKIX recommends but some broken software chokes on critical +# extensions. +#basicConstraints = critical,CA:true +# So we do this instead. +basicConstraints = CA:true + +# Key usage: this is typical for a CA certificate. However since it will +# prevent it being used as an test self-signed certificate it is best +# left out by default. +# keyUsage = cRLSign, keyCertSign + +# Some might want this also +# nsCertType = sslCA, emailCA + +# Include email address in subject alt name: another PKIX recommendation +# subjectAltName=email:copy +# Copy issuer details +# issuerAltName=issuer:copy + +# DER hex encoding of an extension: beware experts only! +# obj=DER:02:03 +# Where 'obj' is a standard or added object +# You can even override a supported extension: +# basicConstraints= critical, DER:30:03:01:01:FF + +[ crl_ext ] + +# CRL extensions. +# Only issuerAltName and authorityKeyIdentifier make any sense in a CRL. + +# issuerAltName=issuer:copy +authorityKeyIdentifier=keyid:always,issuer:always
diff --git a/src/openssl/test-key.pem b/src/openssl/test-key.pem new file mode 100644 index 0000000..ff29adf --- /dev/null +++ b/src/openssl/test-key.pem
@@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA3J6aPv2Zr+PvkUCxb1BL03wE6zTL/Jkqbfz+dItiCpDQ0395 +W/D2/nY67p8J5z47WKbdkH8W2E2FZB3PRH8D9Vd0hlXGw9J3Fgg/AIa4s44SHuub +/rNXeAERviYHVAS+/PesQoxivhJsP50pX0P92AOot9WlcR/nYlo5TAvhDw4xSNg4 +Ya5IviTy9DX9x9qqv6PSIkY1wvALOAB6tnHq/xCwFsaWZJ9VjP7LyaKaeZmOgQ6v +DZKUwJtiflVKsQYfUvChsFTJHbKZS3QUgLsuY7Ik5McgP8Ks5LpZZ+xF/R4SfTy1 +2qxkI6qPcoWyKcdh8/fQO1R61FNqYhOSkWYodQIDAQABAoIBAQCHd9Qa7bnbKUlH +lcPeKB4HZFXY33iKSLqnAvx0L8op1raDx/iLHjFsGskhEQMRvULPstbGDWPHugI4 +cZ1938hcdDEW88CzKZ76JmIZPqBXkNtLpT0KbrE8/NsaOVuymZ900dgynOVc9Q8H +GMf4uVU7uTN2fneyOPbpi6E3MuwlQ1urgo02WSyOfkMALtPBOvM/qajLv1LvIKxf +lp7JfOrewZokMNX8NUraiGJmOOZsidgd66HLaxqjCALk/7UWTBh9vjur4jPn+qOm +zukQSpmvoFMjtaoNSQXL7qqYTFKoLqfNNUhq/Ok533JHWSQhOM37YmfmfCEChxIE +Qdlc0SSZAoGBAPbBRxbOOY6/55Qq5/uuAlMPDMpYIA8nas7P1XQhPd9Ai7RSkN9S +wATZkOvr/KT07c0maFpWddIAxbgv27GN9VTF0G+HULItRRQ2C8LK/BhhnEVokMyl +U4mnc3NTQCY1zIbaYJ0i/PEREOyR69WzIDJKl446SAqEgaTyVF6RGCHPAoGBAOTi +pk92VJuc1vqDaxFoCUw1sUZtyrTCTX/jXxyEQhO8gZfq3m8dTVzW+3AKDlwvFNfj +WPbJh5H36eHzXJBOomn4yIz9OV/+XmhBux8XESZODfeVF+UFepTc1V4q0TsrBUIf +luK7ZFtaGWmho3Hn5RA1h8e0Z6c+4nKZ5i0Z3/Z7AoGANPEA/J6gcMUxvXN7NF+A +Nivbdap0rmupmdezl2fua3DgyH6SgKezdRbs5gFKwmWeg86CwycbvkPWKA90lmK7 +yUVr1BH3OVNHJ+/0lAWTEvQWYDnwH0g1ZSpdNdgdwlT2ndRKuEwicuJTfD5OmBoH +hWLFo4lTnZYSbr5jZarBv7cCgYA+i+Uwr8BdKdXhbUoz3n8z8TQ5b8VF8hbljMev +7kB0Tj4HuqoAKTy70w+wxT65WDBU8o6cGeRPMjUahrtTv/lIBjEfvg8QuV0pFlVB +ILeSBSBx+K8n6YBe753q9r5ocdAlCqbb3KOHBy8Mm5wjg2AoNsic/SKaJGgTMxUg +XALEwwKBgQCpMQMlKkJniqwjdxqYVfsHNmnTZVWBlu7I5Pph2Os2VGGW+w10XWcX +h0P3ys6abPQQjzh7STbXGnFYJH1oK1X98jahjI5iMwwpI+4KfcRThbn9nC8qftVL +ZvD1krEMJbkvke7PnUX/idBmI7VNXhq4538WL47w29jmnmkYNQQFTA== +-----END RSA PRIVATE KEY-----
diff --git a/src/openssl/testCA/ca/cacert.pem b/src/openssl/testCA/ca/cacert.pem new file mode 100644 index 0000000..b2ee317 --- /dev/null +++ b/src/openssl/testCA/ca/cacert.pem
@@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEvTCCA6WgAwIBAgIJALk9c4BcQN5DMA0GCSqGSIb3DQEBBQUAMIGaMRMwEQYK +CZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTELMAkGA1UE +BhMCVVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9yazEcMBoG +A1UEChMTU25ha2UgT2lsIFVubGltaXRlZDEZMBcGA1UEAxMQcm9vdC5leGFtcGxl +LmNvbTAeFw0xMzEyMDQxNjM3MjBaFw00MTA0MjExNjM3MjBaMIGaMRMwEQYKCZIm +iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTELMAkGA1UEBhMC +VVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9yazEcMBoGA1UE +ChMTU25ha2UgT2lsIFVubGltaXRlZDEZMBcGA1UEAxMQcm9vdC5leGFtcGxlLmNv +bTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANhHTFSyNtSs3AwtbvUe +x3DNySOxmIXU3C5HUBNLRvRvW5uVhXg6xUd/vJkuy6vNIkXJUVqgft+NHI3OmyNX +W0ElD4d5wK24/HuuA7iUkOtRQ2oqxrdQQQ+u12y1yLOE/+Dbt0wMaxxdU8LL/VEu +oj3eRRoaWJsWLsctR/Y3b6nddhG1pAeB/S1x0BGq+k8EB09ijYQ6Lmxi4WaPnZqy +vHXgbBgCT5K7pY0AxMy2Nctam71O+2D8W3m5DSx9eEPpxhqXBfp+fM/HYeG7lMX7 +sURD+6CZ5RaoHCdGFVh3TWzjs32WjV047QdwRQGzcJ6GIxfXjBjMECLwqKAyMMpb +Bf0CAwEAAaOCAQIwgf8wHQYDVR0OBBYEFH1agQiTu1LPLGbN18FHsxFLAUNsMIHP +BgNVHSMEgccwgcSAFH1agQiTu1LPLGbN18FHsxFLAUNsoYGgpIGdMIGaMRMwEQYK +CZImiZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTELMAkGA1UE +BhMCVVMxETAPBgNVBAgTCE5ldyBZb3JrMREwDwYDVQQHEwhOZXcgWW9yazEcMBoG +A1UEChMTU25ha2UgT2lsIFVubGltaXRlZDEZMBcGA1UEAxMQcm9vdC5leGFtcGxl +LmNvbYIJALk9c4BcQN5DMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEB +AHtnPxtwM5wOu1utXlnwzTOepeM10OVIOIrJRNfGaz9cwxMEvlFp5PdhNBdlV460 +loAe9NtWDz6IieWJJXNx+h+CeMwOG9jnSYtCKpd2dfICyNypD3PUtHwyx81mxExz +/fK87G6Wfo53j4kuUBL+wzI2qQfaPZ95oDpLySQ/i/l6nzD2+lDczYUCaynwPcRq +MzMLyvpt6Km3gAZHXKB8VNtR+Sr655oMysOB0jcHetMnVvKm87ByZP/ErnCMWQZy +rEZClf93OaTQCcMcOJSrnVWOgi0o31m90MQjRwdh09ZtikuRSutkA4xrU3rPOgmV +c/9bYt2L/wzfV1v3JaBJEQs= +-----END CERTIFICATE-----
diff --git a/src/openssl/testCA/ca/cakey.pem b/src/openssl/testCA/ca/cakey.pem new file mode 100644 index 0000000..9f2764c --- /dev/null +++ b/src/openssl/testCA/ca/cakey.pem
@@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEA2EdMVLI21KzcDC1u9R7HcM3JI7GYhdTcLkdQE0tG9G9bm5WF +eDrFR3+8mS7Lq80iRclRWqB+340cjc6bI1dbQSUPh3nArbj8e64DuJSQ61FDairG +t1BBD67XbLXIs4T/4Nu3TAxrHF1Twsv9US6iPd5FGhpYmxYuxy1H9jdvqd12EbWk +B4H9LXHQEar6TwQHT2KNhDoubGLhZo+dmrK8deBsGAJPkruljQDEzLY1y1qbvU77 +YPxbebkNLH14Q+nGGpcF+n58z8dh4buUxfuxREP7oJnlFqgcJ0YVWHdNbOOzfZaN +XTjtB3BFAbNwnoYjF9eMGMwQIvCooDIwylsF/QIDAQABAoIBAEsVdYpx1FdBK6OO +ola2uMaQqqOZpDnSDB6E42fPWnLBtivtXMjAnnyT/AWyGUMrlBpmKbgsv98cPi18 +7J74VNXo59tAiYPGFOFbKC+MZENNkvnon9REKFIpgOBcu7CXG74UiS39obHXNJ0L +9IWaivivkY3eV6R/rv222qS/2iQ9+p8Y3vqCGIzRtPCfNq3mPK9/nzyKPnwq3sJX +tq6hJuBaKuoJV6omCZG8FWK4EZXUVAaUXWXeCoA+cWuz1gmXe9mY4Xh1JOCfwzTh +wVkWcg7CqSgjsBvrq8twVU47EF68Rqo6g/ntGuyb4Qe6UPjlDEliakYV+os8hNbs +p6DLUAECgYEA9JUKXnHHUkSqQCsYLcr3h0PZ93///jomyc/t8vtdbE5f0T6e46d+ +KFoVsdJ6H9rFd2TGh+FythQ91Mbz0YQeEdIEK+dS9IVjBgXKaAgQC0XW3VtMWYa5 +hWoTNEy0y4eYxCuRDfIIkteiJwwabLLeLA0MFRKSol89Wd5D7M6dI90CgYEA4mAB +WM9TeiJF7AyDO/xXogF+VrCWayxeWsUvm228xG+AemWVFxYc3LTVwC2SRU+SJpA/ +UKM8DMB5lNEip9u6XDzRjqDHG08FQshxdvizttugsn4MGiYfeDv7bAs3Lm6xIUuG +9UmC7j2XPCnbtTZMz/5jL/9hY08ki2NmLjqJ2KECgYABwBNL67qGbzFctjI9Gae9 +0xF7QPI/CoF+jjtgssXPYZwz7iPK80bm2QYwuJXhJnqlSRZWoJlmjiyHGkliZXSl +ogAfpE8mqtGzmFUDe5NJ0V0hRmb8eQdY2hJ7HFVq43SHatxl4iaHjn19lAuYXYtT +e2Brwi9EdDQHMZ0A09WyDQKBgDCVLBTUQfUXP+xd7xhDmscRDP0r3sxXdFSEtyfj +UDzUNT2PaYTP4RfY03rwDNFFN3eBQ6VZsvyFnlI64/YkaQV8o/i5NqH8voNLo1ZG +H8OhtQY5mP1PqzdRoC7a5VfYt7kOjYM86JWasEdgMF/erHODA+R8KXl3tb8YcQiA +1a6hAoGAdYwuHLeH+Nwr2q5+m3QEkCmfuAMo3oWCGccdOetrCUVViRuOM6IR8L4s +46bG8cbnOQ90SWqyIwLH8RPEkMykcRpLw/1XGG5tmpXgvPgNSwPsZj2a33yJBAW1 +JtGXcSMQPfzROahNKBFlV9qcF+3FL0mf3B/y24rEyRKFuIiAvhQ= +-----END RSA PRIVATE KEY-----
diff --git a/src/openssl/testCA/index.txt b/src/openssl/testCA/index.txt new file mode 100644 index 0000000..edf5c7a --- /dev/null +++ b/src/openssl/testCA/index.txt
@@ -0,0 +1 @@ +V 410421165056Z 01 unknown /C=US/ST=New York/O=Snake Oil Unlimited/CN=test.example.com
diff --git a/src/openssl/testCA/index.txt.attr b/src/openssl/testCA/index.txt.attr new file mode 100644 index 0000000..8f7e63a --- /dev/null +++ b/src/openssl/testCA/index.txt.attr
@@ -0,0 +1 @@ +unique_subject = yes
diff --git a/src/openssl/testCA/index.txt.old b/src/openssl/testCA/index.txt.old new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/openssl/testCA/index.txt.old
diff --git a/src/openssl/testCA/newcerts/01.pem b/src/openssl/testCA/newcerts/01.pem new file mode 100644 index 0000000..a7fb6b2 --- /dev/null +++ b/src/openssl/testCA/newcerts/01.pem
@@ -0,0 +1,94 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: DC=com, DC=example, C=US, ST=New York, L=New York, O=Snake Oil Unlimited, CN=root.example.com + Validity + Not Before: Dec 4 16:50:56 2013 GMT + Not After : Apr 21 16:50:56 2041 GMT + Subject: C=US, ST=New York, O=Snake Oil Unlimited, CN=test.example.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (2048 bit) + Modulus (2048 bit): + 00:dc:9e:9a:3e:fd:99:af:e3:ef:91:40:b1:6f:50: + 4b:d3:7c:04:eb:34:cb:fc:99:2a:6d:fc:fe:74:8b: + 62:0a:90:d0:d3:7f:79:5b:f0:f6:fe:76:3a:ee:9f: + 09:e7:3e:3b:58:a6:dd:90:7f:16:d8:4d:85:64:1d: + cf:44:7f:03:f5:57:74:86:55:c6:c3:d2:77:16:08: + 3f:00:86:b8:b3:8e:12:1e:eb:9b:fe:b3:57:78:01: + 11:be:26:07:54:04:be:fc:f7:ac:42:8c:62:be:12: + 6c:3f:9d:29:5f:43:fd:d8:03:a8:b7:d5:a5:71:1f: + e7:62:5a:39:4c:0b:e1:0f:0e:31:48:d8:38:61:ae: + 48:be:24:f2:f4:35:fd:c7:da:aa:bf:a3:d2:22:46: + 35:c2:f0:0b:38:00:7a:b6:71:ea:ff:10:b0:16:c6: + 96:64:9f:55:8c:fe:cb:c9:a2:9a:79:99:8e:81:0e: + af:0d:92:94:c0:9b:62:7e:55:4a:b1:06:1f:52:f0: + a1:b0:54:c9:1d:b2:99:4b:74:14:80:bb:2e:63:b2: + 24:e4:c7:20:3f:c2:ac:e4:ba:59:67:ec:45:fd:1e: + 12:7d:3c:b5:da:ac:64:23:aa:8f:72:85:b2:29:c7: + 61:f3:f7:d0:3b:54:7a:d4:53:6a:62:13:92:91:66: + 28:75 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 44:88:EF:79:2B:51:0B:16:31:FE:62:C3:2E:D3:26:F6:A8:EF:CD:A3 + X509v3 Authority Key Identifier: + keyid:7D:5A:81:08:93:BB:52:CF:2C:66:CD:D7:C1:47:B3:11:4B:01:43:6C + DirName:/DC=com/DC=example/C=US/ST=New York/L=New York/O=Snake Oil Unlimited/CN=root.example.com + serial:B9:3D:73:80:5C:40:DE:43 + + X509v3 Issuer Alternative Name: + DNS:snake-1.example.com, DNS:snake-2.example.com + Netscape CA Revocation Url: + http://crl.example.com/ca-crl.pem + Signature Algorithm: sha256WithRSAEncryption + a3:f0:4a:d0:e6:2a:b2:f2:ab:4e:68:05:89:9d:d4:95:cc:7d: + 7c:01:39:44:f0:9a:52:18:3c:49:78:3b:1b:95:00:7a:25:f0: + c7:e6:25:a0:41:20:5f:86:8d:e3:c5:59:79:fe:6d:99:81:23: + 40:a7:52:ed:b8:18:dd:f8:37:b9:5b:99:39:c2:6f:0f:8f:4a: + 5f:c3:dd:b4:fd:ca:be:a2:5d:f5:56:8a:d6:f2:cd:ce:e7:92: + 01:0f:4c:9c:a8:63:5a:2a:53:ca:ce:fe:88:87:f1:76:7e:6f: + 0d:d0:55:b3:c2:db:03:13:f2:ea:88:0a:1b:a7:0e:cf:54:a9: + 02:63:fc:1a:0f:94:40:68:46:f5:e2:4a:77:d1:fa:a7:35:d3: + 0e:ba:17:1c:55:08:ca:e4:30:39:0f:c9:39:0b:e6:a7:f9:f9: + 25:2f:8e:0f:88:81:5c:16:04:e0:0f:69:9b:21:87:4f:92:dd: + ed:37:f6:a6:01:5d:7d:af:1d:fb:9f:53:67:2f:d2:8c:10:dd: + d7:fb:16:ea:18:7f:47:28:d0:91:d7:1e:d7:25:a0:ed:0b:8c: + 29:94:d5:a8:43:e8:74:f4:bf:f6:bd:d4:78:fe:c5:bd:5a:87: + 53:27:ad:70:2c:77:61:4c:98:50:c9:c6:db:c8:d6:74:0e:1b: + a9:04:2a:db +-----BEGIN CERTIFICATE----- +MIIFBjCCA+6gAwIBAgIBATANBgkqhkiG9w0BAQsFADCBmjETMBEGCgmSJomT8ixk +ARkWA2NvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxCzAJBgNVBAYTAlVTMREw +DwYDVQQIEwhOZXcgWW9yazERMA8GA1UEBxMITmV3IFlvcmsxHDAaBgNVBAoTE1Nu +YWtlIE9pbCBVbmxpbWl0ZWQxGTAXBgNVBAMTEHJvb3QuZXhhbXBsZS5jb20wHhcN +MTMxMjA0MTY1MDU2WhcNNDEwNDIxMTY1MDU2WjBZMQswCQYDVQQGEwJVUzERMA8G +A1UECBMITmV3IFlvcmsxHDAaBgNVBAoTE1NuYWtlIE9pbCBVbmxpbWl0ZWQxGTAX +BgNVBAMTEHRlc3QuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDcnpo+/Zmv4++RQLFvUEvTfATrNMv8mSpt/P50i2IKkNDTf3lb8Pb+ +djrunwnnPjtYpt2QfxbYTYVkHc9EfwP1V3SGVcbD0ncWCD8AhrizjhIe65v+s1d4 +ARG+JgdUBL7896xCjGK+Emw/nSlfQ/3YA6i31aVxH+diWjlMC+EPDjFI2Dhhrki+ +JPL0Nf3H2qq/o9IiRjXC8As4AHq2cer/ELAWxpZkn1WM/svJopp5mY6BDq8NkpTA +m2J+VUqxBh9S8KGwVMkdsplLdBSAuy5jsiTkxyA/wqzkulln7EX9HhJ9PLXarGQj +qo9yhbIpx2Hz99A7VHrUU2piE5KRZih1AgMBAAGjggGVMIIBkTAJBgNVHRMEAjAA +MCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAd +BgNVHQ4EFgQURIjveStRCxYx/mLDLtMm9qjvzaMwgc8GA1UdIwSBxzCBxIAUfVqB +CJO7Us8sZs3XwUezEUsBQ2yhgaCkgZ0wgZoxEzARBgoJkiaJk/IsZAEZFgNjb20x +FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMQswCQYDVQQGEwJVUzERMA8GA1UECBMI +TmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMRwwGgYDVQQKExNTbmFrZSBPaWwg +VW5saW1pdGVkMRkwFwYDVQQDExByb290LmV4YW1wbGUuY29tggkAuT1zgFxA3kMw +MwYDVR0SBCwwKoITc25ha2UtMS5leGFtcGxlLmNvbYITc25ha2UtMi5leGFtcGxl +LmNvbTAwBglghkgBhvhCAQQEIxYhaHR0cDovL2NybC5leGFtcGxlLmNvbS9jYS1j +cmwucGVtMA0GCSqGSIb3DQEBCwUAA4IBAQCj8ErQ5iqy8qtOaAWJndSVzH18ATlE +8JpSGDxJeDsblQB6JfDH5iWgQSBfho3jxVl5/m2ZgSNAp1LtuBjd+De5W5k5wm8P +j0pfw920/cq+ol31VorW8s3O55IBD0ycqGNaKlPKzv6Ih/F2fm8N0FWzwtsDE/Lq +iAobpw7PVKkCY/waD5RAaEb14kp30fqnNdMOuhccVQjK5DA5D8k5C+an+fklL44P +iIFcFgTgD2mbIYdPkt3tN/amAV19rx37n1NnL9KMEN3X+xbqGH9HKNCR1x7XJaDt +C4wplNWoQ+h09L/2vdR4/sW9WodTJ61wLHdhTJhQycbbyNZ0DhupBCrb +-----END CERTIFICATE-----
diff --git a/src/openssl/testCA/serial b/src/openssl/testCA/serial new file mode 100644 index 0000000..9e22bcb --- /dev/null +++ b/src/openssl/testCA/serial
@@ -0,0 +1 @@ +02
diff --git a/src/openssl/testCA/serial.old b/src/openssl/testCA/serial.old new file mode 100644 index 0000000..8a0f05e --- /dev/null +++ b/src/openssl/testCA/serial.old
@@ -0,0 +1 @@ +01
diff --git a/src/test/java/org/cryptacular/CiphertextHeaderTest.java b/src/test/java/org/cryptacular/CiphertextHeaderTest.java new file mode 100644 index 0000000..51abfae --- /dev/null +++ b/src/test/java/org/cryptacular/CiphertextHeaderTest.java
@@ -0,0 +1,55 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +import java.util.Arrays; +import org.cryptacular.util.CodecUtil; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link CiphertextHeader}. + * + * @author Middleware Services + */ +public class CiphertextHeaderTest +{ + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Nonce exceeds size limit in bytes.*") + public void testNonceLimitConstructor() + { + new CiphertextHeader(new byte[256], "key2"); + } + + @Test + public void testEncodeDecodeSuccess() + { + final byte[] nonce = new byte[255]; + Arrays.fill(nonce, (byte) 7); + final CiphertextHeader expected = new CiphertextHeader(nonce, "aleph"); + final byte[] encoded = expected.encode(); + assertEquals(expected.getLength(), encoded.length); + final CiphertextHeader actual = CiphertextHeader.decode(encoded); + assertEquals(expected.getNonce(), actual.getNonce()); + assertEquals(expected.getKeyName(), actual.getKeyName()); + assertEquals(expected.getLength(), actual.getLength()); + } + + @Test( + expectedExceptions = EncodingException.class, + expectedExceptionsMessageRegExp = "Bad ciphertext header: maximum nonce length exceeded") + public void testDecodeFailNonceLengthExceeded() + { + // https://github.com/vt-middleware/cryptacular/issues/52 + CiphertextHeader.decode(CodecUtil.hex("000000347ffffffd")); + } + + @Test( + expectedExceptions = EncodingException.class, + expectedExceptionsMessageRegExp = "Bad ciphertext header: maximum key length exceeded") + public void testDecodeFailKeyLengthExceeded() + { + CiphertextHeader.decode(CodecUtil.hex("000000F300000004DEADBEEF00FFFFFF")); + } +}
diff --git a/src/test/java/org/cryptacular/CiphertextHeaderV2Test.java b/src/test/java/org/cryptacular/CiphertextHeaderV2Test.java new file mode 100644 index 0000000..7313d35 --- /dev/null +++ b/src/test/java/org/cryptacular/CiphertextHeaderV2Test.java
@@ -0,0 +1,67 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +import java.util.Arrays; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import org.cryptacular.generator.sp80038a.RBGNonce; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link CiphertextHeaderV2}. + * + * @author Middleware Services + */ +public class CiphertextHeaderV2Test +{ + /** Test HMAC key. */ + private final SecretKey key = new SecretKeySpec(new RBGNonce().generate(), "AES"); + + @Test( + expectedExceptions = IllegalArgumentException.class, + expectedExceptionsMessageRegExp = "Nonce exceeds size limit in bytes.*") + public void testNonceLimitConstructor() + { + new CiphertextHeaderV2(new byte[256], "key2"); + } + + @Test + public void testEncodeDecodeSuccess() + { + final byte[] nonce = new byte[255]; + Arrays.fill(nonce, (byte) 7); + final CiphertextHeaderV2 expected = new CiphertextHeaderV2(nonce, "aleph"); + expected.setKeyLookup(this::getKey); + final byte[] encoded = expected.encode(); + assertEquals(expected.getLength(), encoded.length); + final CiphertextHeaderV2 actual = CiphertextHeaderV2.decode(encoded, this::getKey); + assertEquals(expected.getNonce(), actual.getNonce()); + assertEquals(expected.getKeyName(), actual.getKeyName()); + assertEquals(expected.getLength(), actual.getLength()); + } + + @Test( + expectedExceptions = EncodingException.class, + expectedExceptionsMessageRegExp = "Ciphertext header HMAC verification failed") + public void testEncodeDecodeFailBadHMAC() + { + final byte[] nonce = new byte[16]; + Arrays.fill(nonce, (byte) 3); + final CiphertextHeaderV2 expected = new CiphertextHeaderV2(nonce, "aleph"); + // Tamper with computed HMAC + final byte[] encoded = expected.encode(key); + final int index = encoded.length - 3; + final byte b = encoded[index]; + encoded[index] = (byte) (b + 1); + CiphertextHeaderV2.decode(encoded, this::getKey); + } + + private SecretKey getKey(final String alias) + { + if ("aleph".equals(alias)) { + return key; + } + return null; + } +}
diff --git a/src/test/java/org/cryptacular/FailListener.java b/src/test/java/org/cryptacular/FailListener.java new file mode 100644 index 0000000..41e3768 --- /dev/null +++ b/src/test/java/org/cryptacular/FailListener.java
@@ -0,0 +1,22 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +import org.testng.ITestResult; +import org.testng.TestListenerAdapter; + +/** + * TestNG listener that converts skipped results to failures when the cause of skip is an error. + * A common use case for this listener is triggering failures on <code>@DataProvider</code> errors. + * + * @author Middleware Services + */ +public class FailListener extends TestListenerAdapter +{ + @Override + public void onTestSkipped(final ITestResult tr) + { + if (tr.getThrowable() != null) { + tr.setStatus(ITestResult.FAILURE); + } + } +}
diff --git a/src/test/java/org/cryptacular/adapter/WrappedKeyTest.java b/src/test/java/org/cryptacular/adapter/WrappedKeyTest.java new file mode 100644 index 0000000..339b77b --- /dev/null +++ b/src/test/java/org/cryptacular/adapter/WrappedKeyTest.java
@@ -0,0 +1,82 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.adapter; + +import java.io.FileOutputStream; +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import org.cryptacular.FailListener; +import org.cryptacular.util.KeyPairUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.AssertJUnit.assertTrue; + +/** + * Test for {@link AbstractWrappedKey} classes. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class WrappedKeyTest +{ + private static final String KEY_PATH = "src/test/resources/keys/"; + + @DataProvider(name = "keypairs") + public Object[][] getKeyPairs() + { + return + new Object[][] { + {"DSA", KEY_PATH + "dsa-pub.der", KEY_PATH + "dsa-pkcs8-nopass.der", }, + {"RSA", KEY_PATH + "rsa-pub.der", KEY_PATH + "rsa-pkcs8-nopass.der", }, + // TODO: enable once BC gets support for writing EC named curves + // As of bcprov 1.50 only raw EC params can be written + // SunJCE only understands named curves + // { + // "EC", + // KEY_PATH + "ec-prime256v1-named-pub.der", + // KEY_PATH + "ec-pkcs8-prime256v1-named-nopass.der", + // }, + }; + } + + + @Test(dataProvider = "keypairs") + public void testKeyEquivalence(final String algorithm, final String pubKeyPath, final String privKeyPath) + throws Exception + { + final KeyPair wrappedPair = new KeyPair( + KeyPairUtil.readPublicKey(pubKeyPath), + KeyPairUtil.readPrivateKey(privKeyPath)); + final String bcPubKeyPath = String.format("target/%s-%s.key", algorithm, "pub"); + final String bcPrivKeyPath = String.format("target/%s-%s.key", algorithm, "priv"); + writeFile(bcPubKeyPath, wrappedPair.getPublic().getEncoded()); + writeFile(bcPrivKeyPath, wrappedPair.getPrivate().getEncoded()); + + final KeyPair jcePair = readJCEKeyPair(algorithm, bcPubKeyPath, bcPrivKeyPath); + + assertTrue(KeyPairUtil.isKeyPair(wrappedPair.getPublic(), jcePair.getPrivate())); + assertTrue(KeyPairUtil.isKeyPair(jcePair.getPublic(), wrappedPair.getPrivate())); + } + + + private static void writeFile(final String path, final byte[] data) + throws IOException + { + try (FileOutputStream out = new FileOutputStream(path)) { + out.write(data); + } + } + + private static KeyPair readJCEKeyPair(final String algorithm, final String pubKeyPath, final String privKeyPath) + throws Exception + { + final PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(StreamUtil.readAll(privKeyPath)); + final X509EncodedKeySpec pubSpec = new X509EncodedKeySpec(StreamUtil.readAll(pubKeyPath)); + final KeyFactory factory = KeyFactory.getInstance(algorithm); + return new KeyPair(factory.generatePublic(pubSpec), factory.generatePrivate(privSpec)); + } +}
diff --git a/src/test/java/org/cryptacular/bean/AEADBlockCipherBeanTest.java b/src/test/java/org/cryptacular/bean/AEADBlockCipherBeanTest.java new file mode 100644 index 0000000..a26f341 --- /dev/null +++ b/src/test/java/org/cryptacular/bean/AEADBlockCipherBeanTest.java
@@ -0,0 +1,151 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.security.KeyStore; +import org.cryptacular.FailListener; +import org.cryptacular.generator.sp80038d.CounterNonce; +import org.cryptacular.io.FileResource; +import org.cryptacular.spec.AEADBlockCipherSpec; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.CodecUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link AEADBlockCipherBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class AEADBlockCipherBeanTest +{ + + @DataProvider(name = "test-arrays") + public Object[][] getTestArrays() + { + return + new Object[][] { + new Object[] { + // Plaintext is NOT multiple of block size + "Able was I ere I saw elba.", + "AES/GCM", + }, + // Plaintext is multiple of block size + new Object[] { + "Four score and seven years ago, our forefathers ", + "Twofish/CCM", + }, + // OCB + new Object[] { + "Have you passed through this night?", + "Twofish/OCB", + }, + // EAX + new Object[] { + "I went to the woods because I wished to live deliberately, to front only the essential facts of life", + "AES/EAX", + }, + }; + } + + @DataProvider(name = "test-streams") + public Object[][] getTestStreams() + { + return + new Object[][] { + new Object[] { + "src/test/resources/plaintexts/lorem-5000.txt", + "Twofish/GCM", + }, + new Object[] { + "src/test/resources/plaintexts/lorem-1200.txt", + "AES/OCB", + }, + new Object[] { + "src/test/resources/plaintexts/lorem-1200.txt", + "AES/EAX", + }, + }; + } + + + @Test(dataProvider = "test-arrays") + public void testEncryptDecryptArray(final String input, final String cipherSpecString) + throws Exception + { + final AEADBlockCipherBean cipherBean = newCipherBean(AEADBlockCipherSpec.parse(cipherSpecString)); + final byte[] ciphertext = cipherBean.encrypt(ByteUtil.toBytes(input)); + assertEquals(ByteUtil.toString(cipherBean.decrypt(ciphertext)), input); + } + + + @Test(dataProvider = "test-streams") + public void testEncryptDecryptStream(final String path, final String cipherSpecString) + throws Exception + { + final AEADBlockCipherBean cipherBean = newCipherBean(AEADBlockCipherSpec.parse(cipherSpecString)); + final ByteArrayOutputStream tempOut = new ByteArrayOutputStream(8192); + cipherBean.encrypt(StreamUtil.makeStream(new File(path)), tempOut); + + final ByteArrayInputStream tempIn = new ByteArrayInputStream(tempOut.toByteArray()); + final ByteArrayOutputStream finalOut = new ByteArrayOutputStream(8192); + cipherBean.decrypt(tempIn, finalOut); + assertEquals(ByteUtil.toString(finalOut.toByteArray()), ByteUtil.toString(StreamUtil.readAll(path))); + } + + + @Test + public void testDecryptArrayBackwardCompatibleHeader() + { + final AEADBlockCipherBean cipherBean = newCipherBean(new AEADBlockCipherSpec("Twofish", "OCB")); + final String expected = "Have you passed through this night?"; + final String v1CiphertextHex = + "0000001f0000000c76746d770002ba17043672d900000007767463727970745a38dee735266e3f5f7aafec8d1c9ed8a0830a2ff9" + + "c3a46c25f89e69b6eb39dbb82fd13da50e32b2544a73f1a4476677b377e6"; + final byte[] plaintext = cipherBean.decrypt(CodecUtil.hex(v1CiphertextHex)); + assertEquals(expected, ByteUtil.toString(plaintext)); + } + + + @Test + public void testDecryptStreamBackwardCompatibleHeader() + { + final AEADBlockCipherBean cipherBean = newCipherBean(new AEADBlockCipherSpec("Twofish", "OCB")); + final String expected = "Have you passed through this night?"; + final String v1CiphertextHex = + "0000001f0000000c76746d770002ba17043672d900000007767463727970745a38dee735266e3f5f7aafec8d1c9ed8a0830a2ff9" + + "c3a46c25f89e69b6eb39dbb82fd13da50e32b2544a73f1a4476677b377e6"; + final ByteArrayInputStream in = new ByteArrayInputStream(CodecUtil.hex(v1CiphertextHex)); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + cipherBean.decrypt(in, out); + assertEquals(expected, ByteUtil.toString(out.toByteArray())); + } + + + private static KeyStore getTestKeyStore() + { + final KeyStoreFactoryBean bean = new KeyStoreFactoryBean(); + bean.setPassword("vtcrypt"); + bean.setResource(new FileResource(new File("src/test/resources/keystores/cipher-bean.jceks"))); + bean.setType("JCEKS"); + return bean.newInstance(); + } + + + private static AEADBlockCipherBean newCipherBean(final AEADBlockCipherSpec cipherSpec) + { + final AEADBlockCipherBean cipherBean = new AEADBlockCipherBean(); + cipherBean.setNonce(new CounterNonce("vtmw", System.nanoTime())); + cipherBean.setKeyAlias("vtcrypt"); + cipherBean.setKeyPassword("vtcrypt"); + cipherBean.setKeyStore(getTestKeyStore()); + cipherBean.setBlockCipherSpec(cipherSpec); + return cipherBean; + } +}
diff --git a/src/test/java/org/cryptacular/bean/BCryptHashBeanTest.java b/src/test/java/org/cryptacular/bean/BCryptHashBeanTest.java new file mode 100644 index 0000000..0192062 --- /dev/null +++ b/src/test/java/org/cryptacular/bean/BCryptHashBeanTest.java
@@ -0,0 +1,41 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link BCryptHashBean} class. + * + * @author Middleware Services + */ +public class BCryptHashBeanTest +{ + @DataProvider(name = "hashes") + public Object[][] getHashData() + { + return + new Object[][] { + {"password", "$2a$5$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe"}, + {"x", "$2a$12$w6IdiZTAckGirKaH8LU8VOxEvP97cFLEW5ePVJzhZilSa5c.V/uMK"}, + {"abcdefghijklmnopqrstuvwxyz", "$2a$6$.rCVZVOThsIa97pEDOxvGuRRgzG64bvtJ0938xuqzv18d3ZpQhstC"}, + {"abcdefghijklmnopqrstuvwxyz", "$2a$8$aTsUwsyowQuzRrDqFflhgekJ8d9/7Z3GV3UcgvzQW3J5zMyrTvlz."}, + }; + } + + @Test(dataProvider = "hashes") + public void testHash(final String password, final String expected) + { + final BCryptHashBean.BCryptParameters params = new BCryptHashBean.BCryptParameters(expected); + final String hash = new BCryptHashBean(params.getCost()).hash(params.getSalt(), password); + assertEquals(params.encode(hash), expected); + } + + @Test(dataProvider = "hashes") + public void testCompare(final String password, final String expected) + { + assertTrue(new BCryptHashBean(10).compare(expected, password)); + } +}
diff --git a/src/test/java/org/cryptacular/bean/BufferedBlockCipherBeanTest.java b/src/test/java/org/cryptacular/bean/BufferedBlockCipherBeanTest.java new file mode 100644 index 0000000..0a42a10 --- /dev/null +++ b/src/test/java/org/cryptacular/bean/BufferedBlockCipherBeanTest.java
@@ -0,0 +1,138 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.math.BigInteger; +import java.security.KeyStore; +import org.cryptacular.FailListener; +import org.cryptacular.generator.Nonce; +import org.cryptacular.generator.sp80038a.BigIntegerCounterNonce; +import org.cryptacular.generator.sp80038a.LongCounterNonce; +import org.cryptacular.generator.sp80038a.RBGNonce; +import org.cryptacular.io.FileResource; +import org.cryptacular.spec.BufferedBlockCipherSpec; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link BufferedBlockCipherBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class BufferedBlockCipherBeanTest +{ + @DataProvider(name = "test-arrays") + public Object[][] getTestArrays() + { + return + new Object[][] { + new Object[] { + // Plaintext is NOT multiple of block size + "Able was I ere I saw elba.", + "AES/CBC/PKCS5", + new RBGNonce(16), + }, + // Plaintext is multiple of block size + new Object[] { + "Four score and seven years ago, our forefathers ", + "Blowfish/CBC/None", + new RBGNonce(8), + }, + // OFB + new Object[] { + "Have you passed through this night?", + "Blowfish/OFB/PKCS5Padding", + new LongCounterNonce(), + }, + // CFB + new Object[] { + "I went to the woods because I wished to live deliberately, to front only the essential facts of life", + "AES/CFB/PKCS5Padding", + new RBGNonce(16), + }, + }; + } + + @DataProvider(name = "test-streams") + public Object[][] getTestStreams() + { + return + new Object[][] { + new Object[] { + "src/test/resources/plaintexts/lorem-5000.txt", + "AES/CBC/PKCS7", + new RBGNonce(16), + }, + new Object[] { + "src/test/resources/plaintexts/lorem-1200.txt", + "Twofish/OFB/NULL", + new BigIntegerCounterNonce(BigInteger.ONE, 16), + }, + new Object[] { + "src/test/resources/plaintexts/lorem-1200.txt", + "AES/CFB/PKCS5", + new RBGNonce(16), + }, + new Object[] { + "src/test/resources/plaintexts/lorem-1200.txt", + "AES/ECB/PKCS5", + new RBGNonce(16), + }, + }; + } + + + @Test(dataProvider = "test-arrays") + public void testEncryptDecryptArray(final String input, final String cipherSpecString, final Nonce nonce) + throws Exception + { + final BufferedBlockCipherBean cipherBean = new BufferedBlockCipherBean(); + final BufferedBlockCipherSpec cipherSpec = BufferedBlockCipherSpec.parse(cipherSpecString); + cipherBean.setNonce(nonce); + cipherBean.setKeyAlias("vtcrypt"); + cipherBean.setKeyPassword("vtcrypt"); + cipherBean.setKeyStore(getTestKeyStore()); + cipherBean.setBlockCipherSpec(cipherSpec); + + final byte[] ciphertext = cipherBean.encrypt(ByteUtil.toBytes(input)); + assertEquals(ByteUtil.toString(cipherBean.decrypt(ciphertext)), input); + } + + + @Test(dataProvider = "test-streams") + public void testEncryptDecryptStream(final String path, final String cipherSpecString, final Nonce nonce) + throws Exception + { + final BufferedBlockCipherBean cipherBean = new BufferedBlockCipherBean(); + final BufferedBlockCipherSpec cipherSpec = BufferedBlockCipherSpec.parse(cipherSpecString); + cipherBean.setNonce(nonce); + cipherBean.setKeyAlias("vtcrypt"); + cipherBean.setKeyPassword("vtcrypt"); + cipherBean.setKeyStore(getTestKeyStore()); + cipherBean.setBlockCipherSpec(cipherSpec); + + final ByteArrayOutputStream tempOut = new ByteArrayOutputStream(8192); + cipherBean.encrypt(StreamUtil.makeStream(new File(path)), tempOut); + + final ByteArrayInputStream tempIn = new ByteArrayInputStream(tempOut.toByteArray()); + final ByteArrayOutputStream finalOut = new ByteArrayOutputStream(8192); + cipherBean.decrypt(tempIn, finalOut); + assertEquals(ByteUtil.toString(finalOut.toByteArray()), ByteUtil.toString(StreamUtil.readAll(path))); + } + + private static KeyStore getTestKeyStore() + { + final KeyStoreFactoryBean bean = new KeyStoreFactoryBean(); + bean.setPassword("vtcrypt"); + bean.setResource(new FileResource(new File("src/test/resources/keystores/cipher-bean.jceks"))); + bean.setType("JCEKS"); + return bean.newInstance(); + } +}
diff --git a/src/test/java/org/cryptacular/bean/EncodingHashBeanTest.java b/src/test/java/org/cryptacular/bean/EncodingHashBeanTest.java new file mode 100644 index 0000000..1653809 --- /dev/null +++ b/src/test/java/org/cryptacular/bean/EncodingHashBeanTest.java
@@ -0,0 +1,98 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import org.cryptacular.FailListener; +import org.cryptacular.spec.CodecSpec; +import org.cryptacular.spec.DigestSpec; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.CodecUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link EncodingHashBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class EncodingHashBeanTest +{ + @DataProvider(name = "hash-data") + public Object[][] getHashData() + { + return + new Object[][] { + { + new EncodingHashBean(CodecSpec.BASE64, new DigestSpec("SHA1"), 1, false), + new Object[] { + CodecUtil.b64("7FHsteHnm6XQsJT1TTKbxw=="), + CodecUtil.b64("ehp6PCnojSegFpRvStqQ9A=="), + }, + "Oadnuuj7QsRPUuMBiu+dmlT6qzU=", + }, + { + new EncodingHashBean(CodecSpec.BASE64, new DigestSpec("SHA1"), 1, true), + new Object[] { + CodecUtil.b64("7FHsteHnm6XQsJT1TTKbxw=="), + CodecUtil.b64("ehp6PCnojSegFpRvStqQ9A=="), + CodecUtil.b64("/siCJIPstwM="), + }, + "uRt+VlmPzfGOPjSGoZLTxpvd1dP+yIIkg+y3Aw==", + }, + { + new EncodingHashBean(CodecSpec.HEX, new DigestSpec("SHA256"), 3, false), + new Object[] { + CodecUtil.b64("7FHsteHnm6XQsJT1TTKbxw=="), + CodecUtil.b64("ehp6PCnojSegFpRvStqQ9A=="), + }, + "3a1edec6aef6d1736bec63130755690c07f04d7e7139d8fd685cc2d989961b79", + }, + { + new EncodingHashBean(CodecSpec.HEX, new DigestSpec("SHA256"), 3, true), + new Object[] { + CodecUtil.b64("7FHsteHnm6XQsJT1TTKbxw=="), + CodecUtil.b64("ehp6PCnojSegFpRvStqQ9A=="), + CodecUtil.b64("DH9M1lDibNU="), + }, + "79f2868e7f72ed18cd67858e8ffe589c6090d696f7ff298e021faf5855fd41a10c7f4cd650e26cd5", + }, + }; + } + + @DataProvider(name = "compare-data") + public Object[][] getCompareData() + { + return + new Object[][] { + { + new EncodingHashBean(CodecSpec.BASE64, new DigestSpec("SHA1"), 1, false), + "7fyOZXGp+gKMziV/2Px7RIMkxyI2O1H8", + new Object[] {ByteUtil.toBytes("password"), }, + }, + { + new EncodingHashBean(CodecSpec.BASE64, new DigestSpec("SHA1"), 1, true), + "lrb+YkKHqoGbFtxYd0B5567N6ZYwqwvWQwvoSg==", + new Object[] {ByteUtil.toBytes("password"), }, + }, + }; + } + + + @Test(dataProvider = "hash-data") + public void testHash(final EncodingHashBean bean, final Object[] input, final String expected) + throws Exception + { + assertEquals(bean.hash(input), expected); + } + + + @Test(dataProvider = "compare-data") + public void testCompare(final EncodingHashBean bean, final String hash, final Object[] input) + throws Exception + { + assertTrue(bean.compare(hash, input)); + } +}
diff --git a/src/test/java/org/cryptacular/bean/KeyStoreBasedKeyFactoryBeanTest.java b/src/test/java/org/cryptacular/bean/KeyStoreBasedKeyFactoryBeanTest.java new file mode 100644 index 0000000..e7c995d --- /dev/null +++ b/src/test/java/org/cryptacular/bean/KeyStoreBasedKeyFactoryBeanTest.java
@@ -0,0 +1,75 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.File; +import java.security.Key; +import java.security.interfaces.RSAPrivateKey; +import javax.crypto.SecretKey; +import org.cryptacular.FailListener; +import org.cryptacular.io.FileResource; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link KeyStoreBasedKeyFactoryBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class KeyStoreBasedKeyFactoryBeanTest +{ + private static final String KS_PATH = "src/test/resources/keystores/"; + + @DataProvider(name = "keys") + public Object[][] getKeys() + { + return + new Object[][] { + { + KS_PATH + "factory-bean.jceks", + "JCEKS", + "aes256", + "AES", + 32, + }, + { + KS_PATH + "factory-bean.jceks", + "JCEKS", + "rsa2048", + "RSA", + 2048, + }, + }; + } + + + @Test(dataProvider = "keys") + public void testNewInstance( + final String keyStorePath, + final String keyStoreType, + final String alias, + final String expectedAlg, + final int expectedSize) + throws Exception + { + final KeyStoreFactoryBean keyStoreFactory = new KeyStoreFactoryBean(); + keyStoreFactory.setResource(new FileResource(new File(keyStorePath))); + keyStoreFactory.setPassword("vtcrypt"); + keyStoreFactory.setType(keyStoreType); + + final KeyStoreBasedKeyFactoryBean<? extends Key> secretKeyFactory = new KeyStoreBasedKeyFactoryBean<>(); + secretKeyFactory.setKeyStore(keyStoreFactory.newInstance()); + secretKeyFactory.setAlias(alias); + secretKeyFactory.setPassword("vtcrypt"); + + final Key key = secretKeyFactory.newInstance(); + assertEquals(key.getAlgorithm(), expectedAlg); + if (key instanceof SecretKey) { + assertEquals(key.getEncoded().length, expectedSize); + } else if (key instanceof RSAPrivateKey) { + assertEquals(((RSAPrivateKey) key).getModulus().bitLength(), expectedSize); + } + } +}
diff --git a/src/test/java/org/cryptacular/bean/KeyStoreFactoryBeanTest.java b/src/test/java/org/cryptacular/bean/KeyStoreFactoryBeanTest.java new file mode 100644 index 0000000..f1c782a --- /dev/null +++ b/src/test/java/org/cryptacular/bean/KeyStoreFactoryBeanTest.java
@@ -0,0 +1,57 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.File; +import org.cryptacular.FailListener; +import org.cryptacular.io.FileResource; +import org.cryptacular.io.Resource; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link KeyStoreFactoryBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class KeyStoreFactoryBeanTest +{ + private static final String KS_PATH = "src/test/resources/keystores/"; + + @DataProvider(name = "keystore-data") + public Object[][] getKeyStoreData() + { + return + new Object[][] { + new Object[] { + "JCEKS", + new FileResource(new File(KS_PATH + "keystore.jceks")), + 1, + }, + new Object[] { + "JKS", + new FileResource(new File(KS_PATH + "keystore.jks")), + 1, + }, + new Object[] { + "PKCS12", + new FileResource(new File(KS_PATH + "keystore.p12")), + 1, + }, + }; + } + + + @Test(dataProvider = "keystore-data") + public void testNewInstance(final String type, final Resource resource, final int expectedSize) + throws Exception + { + final KeyStoreFactoryBean factory = new KeyStoreFactoryBean(); + factory.setType(type); + factory.setResource(resource); + factory.setPassword("vtcrypt"); + assertEquals(factory.newInstance().size(), expectedSize); + } +}
diff --git a/src/test/java/org/cryptacular/bean/PemBasedPrivateKeyFactoryBeanTest.java b/src/test/java/org/cryptacular/bean/PemBasedPrivateKeyFactoryBeanTest.java new file mode 100644 index 0000000..5bd07c1 --- /dev/null +++ b/src/test/java/org/cryptacular/bean/PemBasedPrivateKeyFactoryBeanTest.java
@@ -0,0 +1,45 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.File; +import java.security.PrivateKey; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link PemBasedPrivateKeyFactoryBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class PemBasedPrivateKeyFactoryBeanTest +{ + private static final String KEY_PATH = "src/test/resources/keys/"; + + @DataProvider(name = "keys") + public Object[][] getKeys() + { + return + new Object[][] { + new Object[] {KEY_PATH + "dsa-pkcs8-nopass.pem"}, + new Object[] {KEY_PATH + "dsa-openssl-nopass.pem"}, + new Object[] {KEY_PATH + "rsa-pkcs8-nopass.pem"}, + new Object[] {KEY_PATH + "rsa-openssl-nopass.pem"}, + new Object[] {KEY_PATH + "ec-openssl-sect571r1-explicit-nopass.pem"}, + }; + } + + @Test(dataProvider = "keys") + public void testNewInstance(final String path) + throws Exception + { + final String pem = ByteUtil.toString(StreamUtil.readAll(new File(path))); + final PemBasedPrivateKeyFactoryBean factory = new PemBasedPrivateKeyFactoryBean(pem); + assertTrue(factory.newInstance() instanceof PrivateKey); + } +}
diff --git a/src/test/java/org/cryptacular/bean/PemBasedPublicKeyFactoryBeanTest.java b/src/test/java/org/cryptacular/bean/PemBasedPublicKeyFactoryBeanTest.java new file mode 100644 index 0000000..806690d --- /dev/null +++ b/src/test/java/org/cryptacular/bean/PemBasedPublicKeyFactoryBeanTest.java
@@ -0,0 +1,43 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.File; +import java.security.PublicKey; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link PemBasedPublicKeyFactoryBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class PemBasedPublicKeyFactoryBeanTest +{ + private static final String KEY_PATH = "src/test/resources/keys/"; + + @DataProvider(name = "keys") + public Object[][] getKeys() + { + return + new Object[][] { + new Object[] {KEY_PATH + "dsa-pub.pem"}, + new Object[] {KEY_PATH + "rsa-pub.pem"}, + new Object[] {KEY_PATH + "ec-secp224k1-explicit-pub.pem"}, + }; + } + + @Test(dataProvider = "keys") + public void testNewInstance(final String path) + throws Exception + { + final String pem = ByteUtil.toString(StreamUtil.readAll(new File(path))); + final PemBasedPublicKeyFactoryBean factory = new PemBasedPublicKeyFactoryBean(pem); + assertTrue(factory.newInstance() instanceof PublicKey); + } +}
diff --git a/src/test/java/org/cryptacular/bean/ResourceBasedPrivateKeyFactoryBeanTest.java b/src/test/java/org/cryptacular/bean/ResourceBasedPrivateKeyFactoryBeanTest.java new file mode 100644 index 0000000..4ade3fd --- /dev/null +++ b/src/test/java/org/cryptacular/bean/ResourceBasedPrivateKeyFactoryBeanTest.java
@@ -0,0 +1,60 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.File; +import java.security.PrivateKey; +import org.cryptacular.FailListener; +import org.cryptacular.io.FileResource; +import org.cryptacular.io.Resource; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link ResourceBasedPrivateKeyFactoryBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class ResourceBasedPrivateKeyFactoryBeanTest +{ + private static final String KEY_PATH = "src/test/resources/keys/"; + + @DataProvider(name = "keys") + public Object[][] getKeys() + { + return + new Object[][] { + new Object[] {KEY_PATH + "dsa-pkcs8-nopass.pem", null}, + new Object[] {KEY_PATH + "dsa-openssl-nopass.pem", null}, + new Object[] {KEY_PATH + "rsa-pkcs8-nopass.pem", null}, + new Object[] {KEY_PATH + "rsa-openssl-nopass.pem", null}, + new Object[] { + KEY_PATH + "ec-openssl-sect571r1-explicit-nopass.pem", + null, + }, + new Object[] {KEY_PATH + "dsa-openssl-des3.pem", "vtcrypt"}, + new Object[] {KEY_PATH + "dsa-pkcs8-v2-des3.der", "vtcrypt"}, + new Object[] { + KEY_PATH + "ec-pkcs8-sect571r1-explicit-v2-aes128.pem", + "vtcrypt", + }, + new Object[] { + KEY_PATH + "ec-pkcs8-sect571r1-named-v1-sha1-rc2-64.der", + "vtcrypt", + }, + new Object[] {KEY_PATH + "rsa-openssl-des.pem", "vtcrypt"}, + new Object[] {KEY_PATH + "rsa-pkcs8-v2-aes256.der", "vtcrypt"}, + }; + } + + @Test(dataProvider = "keys") + public void testNewInstance(final String path, final String password) + throws Exception + { + final Resource resource = new FileResource(new File(path)); + final ResourceBasedPrivateKeyFactoryBean factory = new ResourceBasedPrivateKeyFactoryBean(resource, password); + assertTrue(factory.newInstance() instanceof PrivateKey); + } +}
diff --git a/src/test/java/org/cryptacular/bean/ResourceBasedPublicKeyFactoryBeanTest.java b/src/test/java/org/cryptacular/bean/ResourceBasedPublicKeyFactoryBeanTest.java new file mode 100644 index 0000000..1bf7ef1 --- /dev/null +++ b/src/test/java/org/cryptacular/bean/ResourceBasedPublicKeyFactoryBeanTest.java
@@ -0,0 +1,42 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.security.PublicKey; +import org.cryptacular.FailListener; +import org.cryptacular.io.ClassPathResource; +import org.cryptacular.io.Resource; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link ResourceBasedPublicKeyFactoryBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class ResourceBasedPublicKeyFactoryBeanTest +{ + private static final String KEY_PATH = "/keys/"; + + @DataProvider(name = "keys") + public Object[][] getKeys() + { + return + new Object[][] { + new Object[] {KEY_PATH + "dsa-pub.pem"}, + new Object[] {KEY_PATH + "rsa-pub.pem"}, + new Object[] {KEY_PATH + "ec-secp224k1-explicit-pub.pem"}, + }; + } + + @Test(dataProvider = "keys") + public void testNewInstance(final String path) + throws Exception + { + final Resource resource = new ClassPathResource(path); + final ResourceBasedPublicKeyFactoryBean factory = new ResourceBasedPublicKeyFactoryBean(resource); + assertTrue(factory.newInstance() instanceof PublicKey); + } +}
diff --git a/src/test/java/org/cryptacular/bean/ResourceBasedSecretKeyFactoryBeanTest.java b/src/test/java/org/cryptacular/bean/ResourceBasedSecretKeyFactoryBeanTest.java new file mode 100644 index 0000000..d01fa12 --- /dev/null +++ b/src/test/java/org/cryptacular/bean/ResourceBasedSecretKeyFactoryBeanTest.java
@@ -0,0 +1,45 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import java.io.File; +import org.cryptacular.FailListener; +import org.cryptacular.io.FileResource; +import org.cryptacular.io.Resource; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link ResourceBasedSecretKeyFactoryBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class ResourceBasedSecretKeyFactoryBeanTest +{ + private static final String KEY_PATH = "src/test/resources/keys/"; + + @DataProvider(name = "keys") + public Object[][] getKeys() + { + return new Object[][] { + new Object[] { + "AES", + new FileResource(new File(KEY_PATH + "aes-128.key")), + 16, + }, + }; + } + + + @Test(dataProvider = "keys") + public void testNewInstance(final String algorithm, final Resource resource, final int expectedSize) + throws Exception + { + final ResourceBasedSecretKeyFactoryBean factory = new ResourceBasedSecretKeyFactoryBean(); + factory.setAlgorithm(algorithm); + factory.setResource(resource); + assertEquals(factory.newInstance().getEncoded().length, expectedSize); + } +}
diff --git a/src/test/java/org/cryptacular/bean/SimpleHashBeanTest.java b/src/test/java/org/cryptacular/bean/SimpleHashBeanTest.java new file mode 100644 index 0000000..3f19a30 --- /dev/null +++ b/src/test/java/org/cryptacular/bean/SimpleHashBeanTest.java
@@ -0,0 +1,55 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.bean; + +import org.cryptacular.FailListener; +import org.cryptacular.spec.DigestSpec; +import org.cryptacular.util.CodecUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link SimpleHashBean}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class SimpleHashBeanTest +{ + @DataProvider(name = "test-data") + public Object[][] getTestData() + { + return + new Object[][] { + { + new DigestSpec("SHA1"), + new Object[] { + CodecUtil.b64("7FHsteHnm6XQsJT1TTKbxw=="), + CodecUtil.b64("ehp6PCnojSegFpRvStqQ9A=="), + }, + 1, + "Oadnuuj7QsRPUuMBiu+dmlT6qzU=", + }, + { + new DigestSpec("SHA256"), + new Object[] { + CodecUtil.b64("7FHsteHnm6XQsJT1TTKbxw=="), + CodecUtil.b64("ehp6PCnojSegFpRvStqQ9A=="), + }, + 3, + "Oh7exq720XNr7GMTB1VpDAfwTX5xOdj9aFzC2YmWG3k=", + }, + }; + } + + @Test(dataProvider = "test-data") + public void testHash(final DigestSpec digest, final Object[] input, final int iterations, final String expectedBase64) + throws Exception + { + final SimpleHashBean bean = new SimpleHashBean(); + bean.setDigestSpec(digest); + bean.setIterations(iterations); + assertEquals(CodecUtil.b64(bean.hash(input)), expectedBase64); + } +}
diff --git a/src/test/java/org/cryptacular/codec/Base32DecoderTest.java b/src/test/java/org/cryptacular/codec/Base32DecoderTest.java new file mode 100644 index 0000000..de382ff --- /dev/null +++ b/src/test/java/org/cryptacular/codec/Base32DecoderTest.java
@@ -0,0 +1,74 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.CodecUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link Base64Decoder}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class Base32DecoderTest +{ + @DataProvider(name = "encoded-data") + public Object[][] getEncodedData() + { + final Base32Decoder unpadded = new Base32Decoder(); + unpadded.setPaddedInput(false); + return + new Object[][] { + // Multiple of 40 bits + new Object[] { + new Base32Decoder(), + "TQSN7XJ4", + CodecUtil.hex("9c24dfdd3c"), + }, + // Final quantum of encoding input is exactly 8 bits + new Object[] { + unpadded, + "43H7CNN2EI", + CodecUtil.hex("e6cff135ba22"), + }, + // Final quantum of encoding input is exactly 16 bits + new Object[] { + new Base32Decoder(), + "2NEK2FDJHXDQ====", + CodecUtil.hex("d348ad14693dc7"), + }, + // Final quantum of encoding input is exactly 24 bits + new Object[] { + new Base32Decoder(), + "LVVECZIT6F3MU===", + CodecUtil.hex("5d6a416513f176ca"), + }, + // Final quantum of encoding input is exactly 32 bits + new Object[] { + new Base32Decoder(), + "QN5Z7HN4PBY4G5Q=", + CodecUtil.hex("837b9f9dbc7871c376"), + }, + }; + } + + + @Test(dataProvider = "encoded-data") + public void testDecode(final Base32Decoder decoder, final String data, final byte[] expected) + throws Exception + { + final CharBuffer input = CharBuffer.wrap(data); + final ByteBuffer output = ByteBuffer.allocate(decoder.outputSize(input.length())); + decoder.decode(input, output); + decoder.finalize(output); + output.flip(); + assertEquals(ByteUtil.toArray(output), expected); + } +}
diff --git a/src/test/java/org/cryptacular/codec/Base32EncoderTest.java b/src/test/java/org/cryptacular/codec/Base32EncoderTest.java new file mode 100644 index 0000000..2738c43 --- /dev/null +++ b/src/test/java/org/cryptacular/codec/Base32EncoderTest.java
@@ -0,0 +1,72 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.FailListener; +import org.cryptacular.util.CodecUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link Base64Encoder}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class Base32EncoderTest +{ + @DataProvider(name = "byte-data") + public Object[][] getByteData() + { + final Base32Encoder unpadded = new Base32Encoder(); + unpadded.setPaddedOutput(false); + return + new Object[][] { + // Multiple of 40 bits + new Object[] { + new Base32Encoder(), + CodecUtil.hex("9c24dfdd3c"), + "TQSN7XJ4", + }, + // Final quantum of encoding input is exactly 8 bits + new Object[] { + new Base32Encoder(), + CodecUtil.hex("e6cff135ba22"), + "43H7CNN2EI======", + }, + // Final quantum of encoding input is exactly 16 bits + new Object[] { + new Base32Encoder(), + CodecUtil.hex("d348ad14693dc7"), + "2NEK2FDJHXDQ====", + }, + // Final quantum of encoding input is exactly 24 bits + new Object[] { + unpadded, + CodecUtil.hex("5d6a416513f176ca"), + "LVVECZIT6F3MU", + }, + // Final quantum of encoding input is exactly 32 bits + new Object[] { + new Base32Encoder(), + CodecUtil.hex("837b9f9dbc7871c376"), + "QN5Z7HN4PBY4G5Q=", + }, + }; + } + + @Test(dataProvider = "byte-data") + public void testEncode(final Base32Encoder encoder, final byte[] inBytes, final String expected) + throws Exception + { + final ByteBuffer input = ByteBuffer.wrap(inBytes); + final CharBuffer output = CharBuffer.allocate(encoder.outputSize(input.limit())); + encoder.encode(input, output); + encoder.finalize(output); + assertEquals(output.flip().toString(), expected); + } + +}
diff --git a/src/test/java/org/cryptacular/codec/Base64DecoderTest.java b/src/test/java/org/cryptacular/codec/Base64DecoderTest.java new file mode 100644 index 0000000..641ed39 --- /dev/null +++ b/src/test/java/org/cryptacular/codec/Base64DecoderTest.java
@@ -0,0 +1,119 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.io.File; +import java.io.Reader; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.CodecUtil; +import org.cryptacular.util.HashUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link Base64Decoder} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class Base64DecoderTest +{ + + @DataProvider(name = "encoded-data") + public Object[][] getEncodedData() + { + return + new Object[][] { + new Object[] { + new Base64Decoder(), + "QWJsZSB3YXMgSSBlcmUgSSBzYXcgZWxiYQ==", + ByteUtil.toBytes("Able was I ere I saw elba"), + }, + new Object[] { + new Base64Decoder(), + "QWJsZSB3YXMgSSBlcmUgSSBzYXcgZWxiYS4=", + ByteUtil.toBytes("Able was I ere I saw elba."), + }, + new Object[] { + new Base64Decoder(), + "safx/LW8+SsSy/o3PmCNy4VEm5s=", + HashUtil.sha1(ByteUtil.toBytes("t3stUs3r01")), + }, + new Object[] { + new Base64Decoder.Builder().setUrlSafe(true).build(), + "safx_LW8-SsSy_o3PmCNy4VEm5s=", + HashUtil.sha1(ByteUtil.toBytes("t3stUs3r01")), + }, + new Object[] { + new Base64Decoder.Builder().setUrlSafe(true).setPadding(false).build(), + "FPu_A9l-", + CodecUtil.hex("14FBBF03D97E"), + }, + new Object[] { + new Base64Decoder.Builder().setUrlSafe(true).setPadding(false).build(), + "FPu_A9k", + CodecUtil.hex("14FBBF03D9"), + }, + }; + } + + + @DataProvider(name = "plaintext-files") + public Object[][] getPlaintextFiles() + { + return + new Object[][] { + new Object[] {"src/test/resources/plaintexts/lorem-1190.txt"}, + new Object[] {"src/test/resources/plaintexts/lorem-1200.txt"}, + new Object[] {"src/test/resources/plaintexts/lorem-5000.txt"}, + }; + } + + + @Test(dataProvider = "encoded-data") + public void testDecode(final Base64Decoder decoder, final String data, final byte[] expected) + throws Exception + { + final CharBuffer input = CharBuffer.wrap(data); + final ByteBuffer output = ByteBuffer.allocate(decoder.outputSize(input.length())); + decoder.decode(input, output); + decoder.finalize(output); + output.flip(); + assertEquals(ByteUtil.toArray(output), expected); + } + + + @Test(dataProvider = "plaintext-files") + public void testDecodeFile(final String path) + throws Exception + { + final String expected = StreamUtil.readAll(StreamUtil.makeReader(new File(path))); + final File file = new File(path + ".b64"); + final StringBuilder actual = new StringBuilder(expected.length()); + final Reader reader = StreamUtil.makeReader(file); + final Base64Decoder decoder = new Base64Decoder(); + try { + final CharBuffer bufIn = CharBuffer.allocate(1024); + final ByteBuffer bufOut = ByteBuffer.allocate(decoder.outputSize(bufIn.capacity())); + while (reader.read(bufIn) > 0) { + bufIn.flip(); + decoder.decode(bufIn, bufOut); + bufOut.flip(); + actual.append(ByteUtil.toCharBuffer(bufOut)); + bufOut.clear(); + bufIn.clear(); + } + decoder.finalize(bufOut); + bufOut.flip(); + actual.append(ByteUtil.toCharBuffer(bufOut)); + } finally { + StreamUtil.closeReader(reader); + } + assertEquals(actual.toString(), expected); + } +}
diff --git a/src/test/java/org/cryptacular/codec/Base64EncoderTest.java b/src/test/java/org/cryptacular/codec/Base64EncoderTest.java new file mode 100644 index 0000000..201a0bd --- /dev/null +++ b/src/test/java/org/cryptacular/codec/Base64EncoderTest.java
@@ -0,0 +1,133 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.io.File; +import java.io.FileInputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.channels.FileChannel; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.CodecUtil; +import org.cryptacular.util.HashUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link Base64Encoder} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class Base64EncoderTest +{ + @DataProvider(name = "byte-data") + public Object[][] getByteData() + { + final Base64Encoder unpadded = new Base64Encoder(); + unpadded.setPaddedOutput(false); + return + new Object[][] { + new Object[] { + new Base64Encoder(), + ByteUtil.toBytes("Able was I ere I saw elba"), + "QWJsZSB3YXMgSSBlcmUgSSBzYXcgZWxiYQ==", + }, + new Object[] { + new Base64Encoder.Builder().setPadding(false).build(), + ByteUtil.toBytes("Able was I ere I saw elba"), + "QWJsZSB3YXMgSSBlcmUgSSBzYXcgZWxiYQ", + }, + new Object[] { + new Base64Encoder(), + ByteUtil.toBytes("Able was I ere I saw elba."), + "QWJsZSB3YXMgSSBlcmUgSSBzYXcgZWxiYS4=", + }, + new Object[] { + new Base64Encoder(), + HashUtil.sha1(ByteUtil.toBytes("t3stUs3r01")), + "safx/LW8+SsSy/o3PmCNy4VEm5s=", + }, + new Object[] { + new Base64Encoder(true), + HashUtil.sha1(ByteUtil.toBytes("t3stUs3r01")), + "safx_LW8-SsSy_o3PmCNy4VEm5s=", + }, + new Object[] { + new Base64Encoder(), + CodecUtil.hex("3f1c435a244f7a8be1572a1bf2a196f4958cc00c17b96e"), + "PxxDWiRPeovhVyob8qGW9JWMwAwXuW4=", + }, + new Object[] { + new Base64Encoder.Builder().setUrlSafe(true).setPadding(false).build(), + CodecUtil.hex("14FBBF03D97E"), + "FPu_A9l-", + }, + new Object[] { + new Base64Encoder.Builder().setUrlSafe(true).setPadding(false).build(), + CodecUtil.hex("14FBBF03D9"), + "FPu_A9k", + }, + }; + } + + + @DataProvider(name = "plaintext-files") + public Object[][] getPlaintextFiles() + { + return + new Object[][] { + new Object[] {"src/test/resources/plaintexts/lorem-1190.txt"}, + new Object[] {"src/test/resources/plaintexts/lorem-1200.txt"}, + new Object[] {"src/test/resources/plaintexts/lorem-5000.txt"}, + }; + } + + + @Test(dataProvider = "byte-data") + public void testEncode(final Base64Encoder encoder, final byte[] inBytes, final String expected) + throws Exception + { + final ByteBuffer input = ByteBuffer.wrap(inBytes); + final CharBuffer output = CharBuffer.allocate(encoder.outputSize(input.limit())); + encoder.encode(input, output); + encoder.finalize(output); + assertEquals(output.flip().toString(), expected); + } + + + @Test(dataProvider = "plaintext-files") + public void testEncodeFile(final String path) + throws Exception + { + final File file = new File(path); + String expectedPath = path + ".b64"; + if ("\r\n".equals(System.lineSeparator())) { + expectedPath += ".crlf"; + } + + final String expected = new String(StreamUtil.readAll(expectedPath)); + final StringBuilder actual = new StringBuilder(expected.length()); + final Base64Encoder encoder = new Base64Encoder(64); + try (FileInputStream input = new FileInputStream(file)) { + final ByteBuffer bufIn = ByteBuffer.allocate(512); + final CharBuffer bufOut = CharBuffer.allocate(encoder.outputSize(512)); + final FileChannel chIn = input.getChannel(); + while (chIn.read(bufIn) > 0) { + bufIn.flip(); + encoder.encode(bufIn, bufOut); + bufOut.flip(); + actual.append(bufOut); + bufOut.clear(); + bufIn.clear(); + } + encoder.finalize(bufOut); + bufOut.flip(); + actual.append(bufOut); + } + assertEquals(actual.toString(), expected); + } +}
diff --git a/src/test/java/org/cryptacular/codec/HexDecoderTest.java b/src/test/java/org/cryptacular/codec/HexDecoderTest.java new file mode 100644 index 0000000..17246a6 --- /dev/null +++ b/src/test/java/org/cryptacular/codec/HexDecoderTest.java
@@ -0,0 +1,72 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link HexDecoder} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class HexDecoderTest +{ + @DataProvider(name = "hex-data") + public Object[][] getHexData() + { + return + new Object[][] { + new Object[] { + "41626C652077617320492065726520492073617720656C6261", + "Able was I ere I saw elba", + }, + new Object[] { + "41626c652 077617320492065726520492073617720656c626\n1", + "Able was I ere I saw elba", + }, + new Object[] { + "41626c652 077617320492065726520492073617720656c626\n", + "Able was I ere I saw elb", + }, + new Object[] { + "41:62:6c:65:20:77:61:73:20:49:20:65:72:65:20:49:20:73:61:77:20:65:6c:62:61", + "Able was I ere I saw elba", + }, + new Object[] { + "9c63b0547798b60d5e04", + ByteUtil.toString( + new byte[] { + (byte) -100, + (byte) 99, + (byte) -80, + (byte) 84, + (byte) 119, + (byte) -104, + (byte) -74, + (byte) 13, + (byte) 94, + (byte) 4, + }), + }, + }; + } + + @Test(dataProvider = "hex-data") + public void testDecode(final String encoded, final String expected) + throws Exception + { + final HexDecoder decoder = new HexDecoder(); + final ByteBuffer output = ByteBuffer.allocate(decoder.outputSize(encoded.length())); + decoder.decode(CharBuffer.wrap(encoded), output); + decoder.finalize(output); + output.flip(); + assertEquals(ByteUtil.toString(output), expected); + } +}
diff --git a/src/test/java/org/cryptacular/codec/HexEncoderTest.java b/src/test/java/org/cryptacular/codec/HexEncoderTest.java new file mode 100644 index 0000000..c30a02c --- /dev/null +++ b/src/test/java/org/cryptacular/codec/HexEncoderTest.java
@@ -0,0 +1,74 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.codec; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link HexEncoder} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class HexEncoderTest +{ + @DataProvider(name = "text-data") + public Object[][] getTextData() + { + return + new Object[][] { + new Object[] { + new HexEncoder(false), + ByteUtil.toBytes("Able was I ere I saw elba"), + "41626c652077617320492065726520492073617720656c6261", + }, + new Object[] { + new HexEncoder(false, true), + ByteUtil.toBytes("Able was I ere I saw elba\n"), + "41626C652077617320492065726520492073617720656C62610A", + }, + new Object[] { + new HexEncoder(true), + ByteUtil.toBytes("Able was I ere I saw elba"), + "41:62:6c:65:20:77:61:73:20:49:20:65:72:65:20:49:20:73:61:77:20:65:6c:62:61", + }, + new Object[] { + new HexEncoder(true, true), + ByteUtil.toBytes("Able was I ere I saw elba"), + "41:62:6C:65:20:77:61:73:20:49:20:65:72:65:20:49:20:73:61:77:20:65:6C:62:61", + }, + new Object[] { + new HexEncoder(), + new byte[] { + (byte) -100, + (byte) 99, + (byte) -80, + (byte) 84, + (byte) 119, + (byte) -104, + (byte) -74, + (byte) 13, + (byte) 94, + (byte) 4, + }, + "9c63b0547798b60d5e04", + }, + }; + } + + @Test(dataProvider = "text-data") + public void testEncode(final HexEncoder encoder, final byte[] data, final String expected) + throws Exception + { + final CharBuffer output = CharBuffer.allocate(encoder.outputSize(data.length)); + encoder.encode(ByteBuffer.wrap(data), output); + encoder.finalize(output); + assertEquals(output.flip().toString(), expected); + } +}
diff --git a/src/test/java/org/cryptacular/generator/AESP12GeneratorTest.java b/src/test/java/org/cryptacular/generator/AESP12GeneratorTest.java new file mode 100644 index 0000000..e5a496a --- /dev/null +++ b/src/test/java/org/cryptacular/generator/AESP12GeneratorTest.java
@@ -0,0 +1,75 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.io.File; +import java.io.FileOutputStream; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; +import org.bouncycastle.pkcs.PKCS12PfxPdu; +import org.cryptacular.bean.KeyStoreFactoryBean; +import org.cryptacular.io.ClassPathResource; +import org.cryptacular.io.FileResource; +import org.cryptacular.io.Resource; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * Unit test for {@link AESP12Generator} class. + * + * @author Marvin S. Addison + */ +public class AESP12GeneratorTest +{ + @DataProvider(name = "p12-params") + public Object[][] getP12Params() + { + return new Object[][] { + new Object[] {"aes256-sha256-2048", "/keystores/alpha.p12", NISTObjectIdentifiers.id_sha256, 2048}, + new Object[] {"aes256-sha512-4196", "/keystores/alpha.p12", NISTObjectIdentifiers.id_sha512, 4196}, + }; + } + + @Test(dataProvider = "p12-params") + public void testGenerate( + final String testCaseName, + final String keystorePath, + final ASN1ObjectIdentifier digestAlgId, + final int iterations) throws Exception + { + final String password = "vtcrypt"; + final char[] passwordChars = password.toCharArray(); + final KeyStore keyStore = loadP12KeyStore(new ClassPathResource(keystorePath), password); + final RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey("1", passwordChars); + final X509Certificate cert = (X509Certificate) keyStore.getCertificate("1"); + final AESP12Generator generator = new AESP12Generator(digestAlgId, iterations); + final PKCS12PfxPdu p12 = generator.generate(passwordChars, privateKey, cert); + Assert.assertEquals(p12.getContentInfos().length, 2); + // Encrypted bag (certificate) + Assert.assertEquals(p12.getContentInfos()[0].getContentType().toString(), "1.2.840.113549.1.7.6"); + // Shrouded bag (key) + Assert.assertEquals(p12.getContentInfos()[1].getContentType().toString(), "1.2.840.113549.1.7.1"); + final File outFile = new File("target/keystores/" + testCaseName + ".p12"); + outFile.getParentFile().mkdirs(); + try (FileOutputStream out = new FileOutputStream(outFile)) { + out.write(p12.getEncoded()); + } + final KeyStore generated = loadP12KeyStore(new FileResource(outFile), password); + final RSAPrivateKey genKey = (RSAPrivateKey) generated.getKey("end-entity-cert", passwordChars); + Assert.assertEquals(genKey.getPrivateExponent(), privateKey.getPrivateExponent()); + final X509Certificate genCert = (X509Certificate) generated.getCertificate("end-entity-cert"); + Assert.assertEquals(genCert.getSubjectX500Principal(), cert.getSubjectX500Principal()); + } + + private KeyStore loadP12KeyStore(final Resource resource, final String password) + { + final KeyStoreFactoryBean factory = new KeyStoreFactoryBean(); + factory.setResource(resource); + factory.setType("PKCS12"); + factory.setPassword(password); + return factory.newInstance(); + } +}
diff --git a/src/test/java/org/cryptacular/generator/HOTPGeneratorTest.java b/src/test/java/org/cryptacular/generator/HOTPGeneratorTest.java new file mode 100644 index 0000000..7183176 --- /dev/null +++ b/src/test/java/org/cryptacular/generator/HOTPGeneratorTest.java
@@ -0,0 +1,45 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import org.cryptacular.FailListener; +import org.cryptacular.util.CodecUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link HOTPGenerator}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class HOTPGeneratorTest +{ + @DataProvider(name = "test-data") + public Object[][] getTestData() + { + return + new Object[][] { + {"0x3132333435363738393031323334353637383930", 0, 755224}, + {"0x3132333435363738393031323334353637383930", 1, 287082}, + {"0x3132333435363738393031323334353637383930", 2, 359152}, + {"0x3132333435363738393031323334353637383930", 3, 969429}, + {"0x3132333435363738393031323334353637383930", 4, 338314}, + {"0x3132333435363738393031323334353637383930", 5, 254676}, + {"0x3132333435363738393031323334353637383930", 6, 287922}, + {"0x3132333435363738393031323334353637383930", 7, 162583}, + {"0x3132333435363738393031323334353637383930", 8, 399871}, + {"0x3132333435363738393031323334353637383930", 9, 520489}, + }; + } + + + @Test(dataProvider = "test-data") + public void testGenerate(final String hexKey, final int count, final int expected) + throws Exception + { + final HOTPGenerator generator = new HOTPGenerator(); + assertEquals(generator.generate(CodecUtil.hex(hexKey), count), expected); + } +}
diff --git a/src/test/java/org/cryptacular/generator/LegacyP12GeneratorTest.java b/src/test/java/org/cryptacular/generator/LegacyP12GeneratorTest.java new file mode 100644 index 0000000..4d93c08 --- /dev/null +++ b/src/test/java/org/cryptacular/generator/LegacyP12GeneratorTest.java
@@ -0,0 +1,66 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.io.File; +import java.io.FileOutputStream; +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.security.interfaces.RSAPrivateKey; +import org.bouncycastle.pkcs.PKCS12PfxPdu; +import org.cryptacular.bean.KeyStoreFactoryBean; +import org.cryptacular.io.ClassPathResource; +import org.cryptacular.io.Resource; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * Unit test for {@link LegacyP12Generator}. + * + * @author Marvin S. Addison + */ +public class LegacyP12GeneratorTest +{ + @DataProvider(name = "p12-params") + public Object[][] getP12Params() + { + return new Object[][] { + new Object[] {"legacy-1024", "/keystores/alpha.p12", 1024}, + new Object[] {"legacy-2048", "/keystores/alpha.p12", 2048}, + }; + } + + @Test(dataProvider = "p12-params") + public void testGenerate( + final String testCaseName, + final String keystorePath, + final int iterations) throws Exception + { + final String password = "vtcrypt"; + final char[] passwordChars = password.toCharArray(); + final KeyStore keyStore = loadP12KeyStore(new ClassPathResource(keystorePath), password); + final RSAPrivateKey privateKey = (RSAPrivateKey) keyStore.getKey("1", passwordChars); + final X509Certificate cert = (X509Certificate) keyStore.getCertificate("1"); + final LegacyP12Generator generator = new LegacyP12Generator(iterations); + final PKCS12PfxPdu p12 = generator.generate(passwordChars, privateKey, cert); + Assert.assertEquals(p12.getContentInfos().length, 2); + // Encrypted bag (certificate) + Assert.assertEquals(p12.getContentInfos()[0].getContentType().toString(), "1.2.840.113549.1.7.6"); + // Shrouded bag (key) + Assert.assertEquals(p12.getContentInfos()[1].getContentType().toString(), "1.2.840.113549.1.7.1"); + final File outFile = new File("target/keystores/" + testCaseName + ".p12"); + outFile.getParentFile().mkdirs(); + try (FileOutputStream out = new FileOutputStream(outFile)) { + out.write(p12.getEncoded()); + } + } + + private KeyStore loadP12KeyStore(final Resource resource, final String password) + { + final KeyStoreFactoryBean factory = new KeyStoreFactoryBean(); + factory.setResource(resource); + factory.setType("PKCS12"); + factory.setPassword(password); + return factory.newInstance(); + } +}
diff --git a/src/test/java/org/cryptacular/generator/RandomIdGeneratorTest.java b/src/test/java/org/cryptacular/generator/RandomIdGeneratorTest.java new file mode 100644 index 0000000..d511d29 --- /dev/null +++ b/src/test/java/org/cryptacular/generator/RandomIdGeneratorTest.java
@@ -0,0 +1,113 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.cryptacular.FailListener; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link RandomIdGenerator}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class RandomIdGeneratorTest +{ + @DataProvider(name = "generators") + public Object[][] getGenerators() + { + return + new Object[][] { + { + new RandomIdGenerator(10), + Pattern.compile("\\w{10}"), + }, + { + new RandomIdGenerator(128), + Pattern.compile("\\w{128}"), + }, + { + new RandomIdGenerator(20, "abcdefg"), + Pattern.compile("[abcdefg]{20}"), + }, + }; + } + + @Test(dataProvider = "generators") + public void testGenerate(final RandomIdGenerator generator, final Pattern expected) + { + for (int i = 0; i < 100; i++) { + final Matcher m = expected.matcher(generator.generate()); + assertTrue(m.matches()); + } + } + + /** + * Test concurrent random ID generation on a shared instance. + * + * @throws Exception on test errors + */ + @Test + public void testConcurrentGeneration() + throws Exception + { + final int poolSize = 100; + final ExecutorService executor = Executors.newFixedThreadPool(poolSize); + final RandomIdGenerator generator = new RandomIdGenerator(50); + final Collection<Callable<String>> tasks = new ArrayList<>(); + for (int i = 0; i < poolSize; i++) { + tasks.add(generator::generate); + } + // Ensure all generated IDs are unique + final Set<String> identifiers = new HashSet<>(poolSize); + final List<Future<String>> results = executor.invokeAll(tasks); + for (Future<String> result : results) { + final String id = result.get(1, TimeUnit.SECONDS); + assertNotNull(id); + identifiers.add(id); + } + assertEquals(poolSize, identifiers.size()); + } + + /** + * Test creating new instances and calling generate on them concurrently. + * + * @throws Exception on test errors + */ + @Test + public void testConcurrentGeneration2() + throws Exception + { + final int poolSize = 100; + final ExecutorService executor = Executors.newFixedThreadPool(poolSize); + final Collection<Callable<String>> tasks = new ArrayList<>(); + for (int i = 0; i < poolSize; i++) { + tasks.add(() -> new RandomIdGenerator(50).generate()); + } + // Ensure all generated IDs are unique + final Set<String> identifiers = new HashSet<>(poolSize); + final List<Future<String>> results = executor.invokeAll(tasks); + for (Future<String> result : results) { + final String id = result.get(1, TimeUnit.SECONDS); + assertNotNull(id); + identifiers.add(id); + } + assertEquals(poolSize, identifiers.size()); + } +}
diff --git a/src/test/java/org/cryptacular/generator/TOTPGeneratorTest.java b/src/test/java/org/cryptacular/generator/TOTPGeneratorTest.java new file mode 100644 index 0000000..298420d --- /dev/null +++ b/src/test/java/org/cryptacular/generator/TOTPGeneratorTest.java
@@ -0,0 +1,77 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator; + +import java.nio.charset.StandardCharsets; +import org.cryptacular.FailListener; +import org.cryptacular.spec.DigestSpec; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link HOTPGenerator}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class TOTPGeneratorTest +{ + /** Test vectors from RFC 6238, appendix B. */ + @DataProvider(name = "test-data-rfc6238") + public Object[][] getTestDataRfc6238() + { + // Key size is equal to hash length for test vectors in RFC-6238 + // (via careful review of the main method in the reference implementation under Appendix A) + final String sha1Key = "12345678901234567890"; + final String sha256Key = "12345678901234567890123456789012"; + final String sha512Key = "1234567890123456789012345678901234567890123456789012345678901234"; + return + new Object[][] { + {new DigestSpec("SHA1"), sha1Key, 59, 8, 94287082}, + {new DigestSpec("SHA256"), sha256Key, 59, 8, 46119246}, + {new DigestSpec("SHA512"), sha512Key, 59, 8, 90693936}, + {new DigestSpec("SHA1"), sha1Key, 1111111109, 8, 7081804}, + {new DigestSpec("SHA256"), sha256Key, 1111111109, 8, 68084774}, + {new DigestSpec("SHA512"), sha512Key, 1111111109, 8, 25091201}, + {new DigestSpec("SHA1"), sha1Key, 1111111111, 8, 14050471}, + {new DigestSpec("SHA256"), sha256Key, 1111111111, 8, 67062674}, + {new DigestSpec("SHA512"), sha512Key, 1111111111, 8, 99943326}, + {new DigestSpec("SHA1"), sha1Key, 1234567890, 8, 89005924}, + {new DigestSpec("SHA256"), sha256Key, 1234567890, 8, 91819424}, + {new DigestSpec("SHA512"), sha512Key, 1234567890, 8, 93441116}, + {new DigestSpec("SHA1"), sha1Key, 2000000000, 8, 69279037}, + {new DigestSpec("SHA256"), sha256Key, 2000000000, 8, 90698825}, + {new DigestSpec("SHA512"), sha512Key, 2000000000, 8, 38618901}, + {new DigestSpec("SHA1"), sha1Key, 20000000000L, 8, 65353130}, + {new DigestSpec("SHA256"), sha256Key, 20000000000L, 8, 77737706}, + {new DigestSpec("SHA512"), sha512Key, 20000000000L, 8, 47863826}, + }; + } + + + @Test(dataProvider = "test-data-rfc6238") + public void testGenerate( + final DigestSpec digestSpec, final String asciiKey, final long currentTime, final int otpSize, final int expected) + { + final TOTPGenerator generator = new TOTPGenerator(); + generator.setDigestSpecification(digestSpec); + generator.setStartTime(0); + generator.setTimeStep(30); + generator.setCurrentTime(currentTime); + generator.setNumberOfDigits(otpSize); + assertEquals(generator.generate(asciiKey.getBytes(StandardCharsets.US_ASCII)), expected); + } + + /** Ensure the system time is used by default. */ + @Test + public void testTimeBehavior() throws Exception + { + final TOTPGenerator generator = new TOTPGenerator(); + final long t1 = generator.currentTime(); + Thread.sleep(1001); + final long t2 = generator.currentTime(); + assertTrue(t2 > t1); + } +}
diff --git a/src/test/java/org/cryptacular/generator/sp80038a/BigIntegerCounterNonceTest.java b/src/test/java/org/cryptacular/generator/sp80038a/BigIntegerCounterNonceTest.java new file mode 100644 index 0000000..118abec --- /dev/null +++ b/src/test/java/org/cryptacular/generator/sp80038a/BigIntegerCounterNonceTest.java
@@ -0,0 +1,40 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.generator.sp80038a; + +import java.math.BigInteger; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link BigIntegerCounterNonce}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class BigIntegerCounterNonceTest +{ + @DataProvider(name = "test-data") + public Object[][] getTestData() + { + return new Object[][] { + new Object[] {1, 8}, + new Object[] {2199023255552L, 16}, + }; + } + + @Test(dataProvider = "test-data") + public void testGenerate(final long start, final int expectedLength) + throws Exception + { + final BigIntegerCounterNonce nonce = new BigIntegerCounterNonce( + new BigInteger(ByteUtil.toBytes(start)), + expectedLength); + final byte[] value = nonce.generate(); + assertEquals(value.length, expectedLength); + assertEquals(new BigInteger(value), new BigInteger(ByteUtil.toBytes(start + 1))); + } +}
diff --git a/src/test/java/org/cryptacular/io/DecodingInputStreamTest.java b/src/test/java/org/cryptacular/io/DecodingInputStreamTest.java new file mode 100644 index 0000000..ce727ab --- /dev/null +++ b/src/test/java/org/cryptacular/io/DecodingInputStreamTest.java
@@ -0,0 +1,49 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import org.bouncycastle.util.io.Streams; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link DecodingInputStream} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class DecodingInputStreamTest +{ + @DataProvider(name = "plaintext-files") + public Object[][] getPlaintextFiles() + { + return + new Object[][] { + new Object[] {"src/test/resources/plaintexts/lorem-1200.txt"}, + new Object[] {"src/test/resources/plaintexts/lorem-5000.txt"}, + }; + } + + @Test(dataProvider = "plaintext-files") + public void testDecode(final String path) + throws Exception + { + final String expected = StreamUtil.readAll(StreamUtil.makeReader(new File(path))); + final File file = new File(path + ".b64"); + final DecodingInputStream input = DecodingInputStream.base64(StreamUtil.makeStream(file)); + final ByteArrayOutputStream output = new ByteArrayOutputStream(expected.length()); + try { + Streams.pipeAll(input, output); + } finally { + StreamUtil.closeStream(input); + StreamUtil.closeStream(output); + } + assertEquals(ByteUtil.toString(output.toByteArray()), expected); + } +}
diff --git a/src/test/java/org/cryptacular/io/EncodingOutputStreamTest.java b/src/test/java/org/cryptacular/io/EncodingOutputStreamTest.java new file mode 100644 index 0000000..28ba857 --- /dev/null +++ b/src/test/java/org/cryptacular/io/EncodingOutputStreamTest.java
@@ -0,0 +1,53 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.io; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import org.bouncycastle.util.io.Streams; +import org.cryptacular.FailListener; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.StreamUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link EncodingOutputStream} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class EncodingOutputStreamTest +{ + @DataProvider(name = "plaintext-files") + public Object[][] getPlaintextFiles() + { + return + new Object[][] { + new Object[] {"src/test/resources/plaintexts/lorem-1200.txt"}, + new Object[] {"src/test/resources/plaintexts/lorem-5000.txt"}, + }; + } + + @Test(dataProvider = "plaintext-files") + public void testEncode(final String path) + throws Exception + { + final File file = new File(path); + String expectedPath = path + ".b64"; + if ("\r\n".equals(System.lineSeparator())) { + expectedPath += ".crlf"; + } + + final String expected = new String(StreamUtil.readAll(expectedPath)); + final ByteArrayOutputStream bufOut = new ByteArrayOutputStream((int) file.length() * 4 / 3); + final EncodingOutputStream output = EncodingOutputStream.base64(bufOut, 64); + try { + Streams.pipeAll(StreamUtil.makeStream(file), output); + } finally { + StreamUtil.closeStream(output); + } + assertEquals(ByteUtil.toString(bufOut.toByteArray()), expected); + } +}
diff --git a/src/test/java/org/cryptacular/util/ByteUtilTest.java b/src/test/java/org/cryptacular/util/ByteUtilTest.java new file mode 100644 index 0000000..d7956f2 --- /dev/null +++ b/src/test/java/org/cryptacular/util/ByteUtilTest.java
@@ -0,0 +1,55 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import org.cryptacular.FailListener; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** @author Middleware Services */ +@Listeners(FailListener.class) +public class ByteUtilTest +{ + @DataProvider(name = "integers") + public Object[][] getIntegers() + { + return + new Object[][] { + new Object[] {64}, + new Object[] {-89}, + new Object[] {255}, + new Object[] {256}, + new Object[] {210983498}, + new Object[] {-417234198}, + }; + } + + @DataProvider(name = "longs") + public Object[][] getLongs() + { + return new Object[][] { + new Object[] {128}, + new Object[] {110374187198L}, + new Object[] {-8987189751341L}, + }; + } + + @Test(dataProvider = "integers") + public void testIntToBytesAndBack(final int value) + throws Exception + { + final byte[] bytes = new byte[4]; + ByteUtil.toBytes(value, bytes, 0); + assertEquals(ByteUtil.toInt(bytes), value); + } + + @Test(dataProvider = "longs") + public void testLongToBytesAndBack(final long value) + throws Exception + { + final byte[] bytes = new byte[8]; + ByteUtil.toBytes(value, bytes, 0); + assertEquals(ByteUtil.toLong(bytes), value); + } +}
diff --git a/src/test/java/org/cryptacular/util/CertUtilTest.java b/src/test/java/org/cryptacular/util/CertUtilTest.java new file mode 100644 index 0000000..1978c0d --- /dev/null +++ b/src/test/java/org/cryptacular/util/CertUtilTest.java
@@ -0,0 +1,503 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.File; +import java.nio.file.Files; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Date; +import java.util.List; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.cryptacular.FailListener; +import org.cryptacular.generator.KeyPairGenerator; +import org.cryptacular.x509.GeneralNameType; +import org.cryptacular.x509.KeyUsageBits; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link CertUtil} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class CertUtilTest +{ + private static final String CRT_PATH = "src/test/resources/certs/"; + + @DataProvider(name = "subject-cn") + public Object[][] getSubjectCommonNames() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + "ed.middleware.vt.edu", + }, + }; + } + + @DataProvider(name = "subject-dn") + public Object[][] getSubjectDN() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + "C=US,DC=edu,DC=vt,ST=Virginia,L=Blacksburg,O=Virginia Polytechnic Institute and State University," + + "OU=Middleware-Server-with-saltr,OU=Middleware Services,CN=ed.middleware.vt.edu", + }, + }; + } + + @DataProvider(name = "subject-dn-spaces") + public Object[][] getSubjectDNWithSpaces() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + "C=US, DC=edu, DC=vt, ST=Virginia, L=Blacksburg, O=Virginia Polytechnic Institute and State University, " + + "OU=Middleware-Server-with-saltr, OU=Middleware Services, CN=ed.middleware.vt.edu", + }, + }; + } + + + @DataProvider(name = "encode-cert-p7") + public Object[][] getP7EncodedCert() throws Exception + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + new String(Files.readAllBytes(new File(CRT_PATH + "ed.middleware.vt.edu.p7b").toPath())), + }, + }; + } + + @DataProvider(name = "encode-cert-x509") + public Object[][] getX509Cert() throws Exception + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + new String(Files.readAllBytes(new File(CRT_PATH + "ed.middleware.vt.edu.crt").toPath())), + }, + }; + } + + @DataProvider(name = "encode-cert-der") + public Object[][] getDERCert() throws Exception + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + Files.readAllBytes(new File(CRT_PATH + "ed.middleware.vt.edu.der").toPath()), + }, + }; + } + + @DataProvider(name = "subject-alt-names") + public Object[][] getSubjectAltNames() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + new String[] { + "ed.middleware.vt.edu", + "directory.vt.edu", + "id.directory.vt.edu", + "authn.directory.vt.edu", + "ldap.vt.edu", + }, + }, + }; + } + + @DataProvider(name = "subject-alt-names-by-type") + public Object[][] getSubjectAltNamesByType() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + new GeneralNameType[] {GeneralNameType.DNSName}, + new String[] { + "ed.middleware.vt.edu", + "directory.vt.edu", + "id.directory.vt.edu", + "authn.directory.vt.edu", + "ldap.vt.edu", + }, + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + new GeneralNameType[] {GeneralNameType.RFC822Name}, + new String[0], + }, + }; + } + + @DataProvider(name = "subject-names") + public Object[][] getSubjectNames() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new String[] {"Marvin S Addison", "eprov@vt.edu"}, + }, + }; + } + + @DataProvider(name = "subject-names-by-type") + public Object[][] getSubjectNamesByType() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new GeneralNameType[] {GeneralNameType.RFC822Name}, + new String[] {"Marvin S Addison", "eprov@vt.edu"}, + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new GeneralNameType[] {GeneralNameType.OtherName}, + new String[] {"Marvin S Addison"}, + }, + }; + } + + @DataProvider(name = "entity-certificate") + public Object[][] getEntityCertificates() + throws Exception + { + return + new Object[][] { + new Object[] { + KeyPairUtil.readPrivateKey(CRT_PATH + "entity.key"), + new X509Certificate[] { + CertUtil.readCertificate(CRT_PATH + "glider.cc.vt.edu.crt"), + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + CertUtil.readCertificate(CRT_PATH + "entity.crt"), + }, + CertUtil.readCertificate(CRT_PATH + "entity.crt"), + }, + }; + } + + @DataProvider(name = "basic-usage") + public Object[][] getBasicUsage() + throws Exception + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new KeyUsageBits[] { + KeyUsageBits.DigitalSignature, + KeyUsageBits.NonRepudiation, + }, + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + new KeyUsageBits[] { + KeyUsageBits.DigitalSignature, + KeyUsageBits.KeyEncipherment, + }, + }, + }; + } + + @DataProvider(name = "extended-usage") + public Object[][] getExtendedUsage() + throws Exception + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new KeyPurposeId[] { + KeyPurposeId.id_kp_clientAuth, + KeyPurposeId.id_kp_emailProtection, + KeyPurposeId.id_kp_smartcardlogon, + }, + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + new KeyPurposeId[] { + KeyPurposeId.id_kp_clientAuth, + KeyPurposeId.id_kp_serverAuth, + }, + }, + }; + } + + @DataProvider(name = "has-policies") + public Object[][] getHasPolicies() + throws Exception + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new String[] { + "1.3.6.1.4.1.6760.5.2.2.1.1", + "1.3.6.1.4.1.6760.5.2.2.2.1", + "1.3.6.1.4.1.6760.5.2.2.3.1", + "1.3.6.1.4.1.6760.5.2.2.4.1", + }, + }, + }; + } + + @DataProvider(name = "subject-keyid") + public Object[][] getSubjectKeyId() + throws Exception + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + "25:48:2F:28:EC:5D:19:BB:1D:25:AE:94:93:B1:7B:B5:35:96:24:66", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + "31:AE:F1:7C:98:67:E9:1F:19:69:A2:A7:84:1E:67:5C:AA:C3:6B:75", + }, + }; + } + + @DataProvider(name = "authority-keyid") + public Object[][] getAuthorityKeyId() + throws Exception + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + "38:E0:6F:AE:48:ED:5E:23:F6:22:9B:1E:E7:9C:19:16:47:B8:7E:92", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + "FC:8A:50:BA:9E:B9:25:5A:7B:55:85:4F:95:00:63:8F:E9:58:6B:43", + }, + }; + } + + @DataProvider(name = "cert-chains") + public Object[][] getCertificateChains() + throws Exception + { + return new Object[][] { + {CRT_PATH + "vtgsca_chain.pem", 4}, + {CRT_PATH + "vtuca_chain.p7b", 2}, + }; + } + + + @Test(dataProvider = "subject-cn") + public void testSubjectCN(final X509Certificate cert, final String expected) + { + assertEquals(CertUtil.subjectCN(cert), expected); + } + + @Test(dataProvider = "subject-alt-names") + public void testSubjectAltNames(final X509Certificate cert, final String[] expected) + throws Exception + { + final GeneralNames names = CertUtil.subjectAltNames(cert); + if (expected.length == 0) { + assertNull(names); + return; + } + assertEquals(names.getNames().length, expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(names.getNames()[i].getName().toString(), expected[i]); + } + } + + @Test(dataProvider = "subject-alt-names-by-type") + public void testSubjectAltNamesByType( + final X509Certificate cert, + final GeneralNameType[] types, + final String[] expected) + throws Exception + { + final GeneralNames names = CertUtil.subjectAltNames(cert, types); + if (expected.length == 0) { + assertNull(names); + return; + } + assertEquals(names.getNames().length, expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(names.getNames()[i].getName().toString(), expected[i]); + } + } + + @Test(dataProvider = "subject-names") + public void testSubjectNames(final X509Certificate cert, final String[] expected) + throws Exception + { + final List<String> names = CertUtil.subjectNames(cert); + assertEquals(names.size(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(names.get(i), expected[i]); + } + } + + @Test(dataProvider = "subject-names-by-type") + public void testSubjectNamesByType(final X509Certificate cert, final GeneralNameType[] types, final String[] expected) + throws Exception + { + final List<String> names = CertUtil.subjectNames(cert, types); + assertEquals(names.size(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(names.get(i), expected[i]); + } + } + + @Test(dataProvider = "entity-certificate") + public void testFindEntityCertificate( + final PrivateKey key, + final X509Certificate[] candidates, + final X509Certificate expected) + throws Exception + { + assertEquals(CertUtil.findEntityCertificate(key, candidates), expected); + } + + @Test(dataProvider = "basic-usage") + public void testAllowsBasicUsage(final X509Certificate cert, final KeyUsageBits[] expectedUses) + throws Exception + { + assertTrue(CertUtil.allowsUsage(cert, expectedUses)); + } + + @Test(dataProvider = "extended-usage") + public void testAllowsExtendedUsage(final X509Certificate cert, final KeyPurposeId[] expectedPurposes) + throws Exception + { + assertTrue(CertUtil.allowsUsage(cert, expectedPurposes)); + } + + @Test(dataProvider = "has-policies") + public void testHasPolicies(final X509Certificate cert, final String[] expectedPolicies) + throws Exception + { + assertTrue(CertUtil.hasPolicies(cert, expectedPolicies)); + } + + @Test(dataProvider = "subject-keyid") + public void testSubjectKeyId(final X509Certificate cert, final String expectedKeyId) + throws Exception + { + assertEquals(CertUtil.subjectKeyId(cert).toUpperCase(), expectedKeyId); + } + + @Test(dataProvider = "authority-keyid") + public void testAuthorityKeyId(final X509Certificate cert, final String expectedKeyId) + throws Exception + { + assertEquals(CertUtil.authorityKeyId(cert).toUpperCase(), expectedKeyId); + } + + + @Test(dataProvider = "cert-chains") + public void testReadCertificateChains(final String path, final int expectedCount) + throws Exception + { + assertEquals(CertUtil.readCertificateChain(path).length, expectedCount); + } + + @Test(dataProvider = "encode-cert-p7") + public void certEncodedAsPkcs7(final X509Certificate certificate, final String expectedEncodedCert) + { + final String actualEncodedCertString = CertUtil.encodeCert(certificate, CertUtil.EncodeType.PKCS7); + final X509Certificate decodedCert = CertUtil.decodeCertificate(CertUtil.encodeCert(certificate, + CertUtil.EncodeType.PKCS7).getBytes()); + assertEquals(actualEncodedCertString, expectedEncodedCert); + assertEquals(certificate, decodedCert); + } + + @Test(dataProvider = "encode-cert-x509") + public void certEncodedAsX509(final X509Certificate certificate, final String x509Cert) + { + final String encodedCert = CertUtil.encodeCert(certificate, CertUtil.EncodeType.X509); + assertEquals(encodedCert, x509Cert); + } + + @Test(dataProvider = "encode-cert-der") + public void certEncodedAsDER(final X509Certificate certificate, final byte[] derCert) + { + final byte[] encodedCert = CertUtil.encodeCert(certificate, CertUtil.EncodeType.DER); + assertEquals(encodedCert, derCert); + } + + @Test(dataProvider = "subject-dn") + public void testSubjectDN(final X509Certificate certificate, final String expectedResponse) + { + assertEquals(CertUtil.subjectDN(certificate, CertUtil.X500PrincipalFormat.RFC2253), expectedResponse); + } + + @Test(dataProvider = "subject-dn-spaces") + public void testSubjectDNWithSpaces(final X509Certificate certificate, final String expectedResponse) + { + assertEquals(CertUtil.subjectDN(certificate, CertUtil.X500PrincipalFormat.READABLE), expectedResponse); + } + + @Test + public void testGenX509() + { + final KeyPair keyPair = KeyPairGenerator.generateRSA(new SecureRandom(), 2048); + final String dn = "C=US, DC=edu, DC=vt, ST=Virginia, " + + "L=Blacksburg, O=Virginia Polytechnic Institute and State University, OU=Middleware-Server-with-saltr, " + + "OU=Middleware Services, CN=ed.middleware.vt.edu"; + + final Instant expectedNotBefore = Instant.now(); + final Instant expectedNotAfter = Instant.now().plus(Duration.ofDays(365)); + + final X509Certificate x509Certificate = CertUtil.generateX509Certificate(keyPair, dn, + Date.from(expectedNotBefore), Date.from(expectedNotAfter), "SHA256WithRSA"); + + assertEquals(truncateToSeconds(expectedNotBefore), truncateToSeconds(x509Certificate.getNotBefore().toInstant())); + assertEquals(truncateToSeconds(expectedNotAfter), truncateToSeconds(x509Certificate.getNotAfter().toInstant())); + } + + @Test(expectedExceptions = RuntimeException.class, + expectedExceptionsMessageRegExp = "Unknown signature type requested: UNSUPPORTEDALGO") + public void testGenX509UnSupportedAlgo() + { + final KeyPair keyPair = KeyPairGenerator.generateRSA(new SecureRandom(), 2048); + final String dn = "C=US, DC=edu, DC=vt, ST=Virginia, " + + "L=Blacksburg, O=Virginia Polytechnic Institute and State University, OU=Middleware-Server-with-saltr, " + + "OU=Middleware Services, CN=ed.middleware.vt.edu"; + + final Instant expectedNotBefore = Instant.now(); + final Instant expectedNotAfter = Instant.now().plus(Duration.ofDays(365)); + + CertUtil.generateX509Certificate(keyPair, dn, + Date.from(expectedNotBefore), Date.from(expectedNotAfter), "UNSUPPORTEDALGO"); + } + + + private OffsetDateTime truncateToSeconds(final Instant instant) + { + return instant.atOffset(ZoneOffset.UTC).withNano(0); + } +}
diff --git a/src/test/java/org/cryptacular/util/CipherUtilTest.java b/src/test/java/org/cryptacular/util/CipherUtilTest.java new file mode 100644 index 0000000..8c5e10c --- /dev/null +++ b/src/test/java/org/cryptacular/util/CipherUtilTest.java
@@ -0,0 +1,215 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import javax.crypto.SecretKey; +import org.bouncycastle.crypto.BlockCipher; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.engines.BlowfishEngine; +import org.bouncycastle.crypto.engines.TwofishEngine; +import org.bouncycastle.crypto.modes.AEADBlockCipher; +import org.bouncycastle.crypto.modes.CBCBlockCipher; +import org.bouncycastle.crypto.modes.CCMBlockCipher; +import org.bouncycastle.crypto.modes.CFBBlockCipher; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.modes.OCBBlockCipher; +import org.bouncycastle.crypto.modes.OFBBlockCipher; +import org.cryptacular.FailListener; +import org.cryptacular.bean.KeyStoreBasedKeyFactoryBean; +import org.cryptacular.bean.KeyStoreFactoryBean; +import org.cryptacular.generator.Nonce; +import org.cryptacular.generator.SecretKeyGenerator; +import org.cryptacular.generator.sp80038a.LongCounterNonce; +import org.cryptacular.generator.sp80038a.RBGNonce; +import org.cryptacular.generator.sp80038d.CounterNonce; +import org.cryptacular.io.FileResource; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link CipherUtil} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class CipherUtilTest +{ + /** Static key derived from keystore on resource classpath. */ + private static final SecretKey STATIC_KEY; + + static + { + final KeyStoreFactoryBean keyStoreFactory = new KeyStoreFactoryBean(); + keyStoreFactory.setPassword("vtcrypt"); + keyStoreFactory.setResource(new FileResource(new File("src/test/resources/keystores/cipher-bean.jceks"))); + keyStoreFactory.setType("JCEKS"); + final KeyStoreBasedKeyFactoryBean<SecretKey> keyFactory = new KeyStoreBasedKeyFactoryBean<>(); + keyFactory.setKeyStore(keyStoreFactory.newInstance()); + keyFactory.setAlias("vtcrypt"); + keyFactory.setPassword("vtcrypt"); + STATIC_KEY = keyFactory.newInstance(); + } + + @DataProvider(name = "block-cipher") + public Object[][] getBlockCipherData() + { + return + new Object[][] { + new Object[] { + // Plaintext is NOT multiple of block size + "Able was I ere I saw elba.", + new CBCBlockCipher(new AESEngine()), + new RBGNonce(16), + }, + // Plaintext is multiple of block size + new Object[] { + "Four score and seven years ago, our forefathers ", + new CBCBlockCipher(new BlowfishEngine()), + new RBGNonce(8), + }, + // OFB + new Object[] { + "Have you passed through this night?", + new OFBBlockCipher(new BlowfishEngine(), 64), + new LongCounterNonce(), + }, + // CFB + new Object[] { + "I went to the woods because I wished to live deliberately, to front only the essential facts of life", + new CFBBlockCipher(new AESEngine(), 128), + new RBGNonce(16), + }, + }; + } + + + @DataProvider(name = "aead-block-cipher") + public Object[][] getAeadBlockCipherData() + { + return + new Object[][] { + new Object[] { + // Plaintext is NOT multiple of block size + "I never picked cotton like my mother did", + new GCMBlockCipher(new AESEngine()), + }, + new Object[] { + // Plaintext is multiple of block size + "Cogito ergo sum.", + new GCMBlockCipher(new AESEngine()), + }, + // CCM + new Object[] { + "Thousands of candles can be lit from a single candle and the life of the candle will not be shortened.", + new CCMBlockCipher(new TwofishEngine()), + }, + // OCB + new Object[] { + "I slept and dreamt life was joy. I awoke and saw that life was service. " + + "I acted and behold: service was joy.", + new OCBBlockCipher(new AESEngine(), new AESEngine()), + }, + }; + } + + + @DataProvider(name = "plaintext-files") + public Object[][] getPlaintextFiles() + { + return + new Object[][] { + new Object[] {"src/test/resources/plaintexts/lorem-1200.txt"}, + new Object[] {"src/test/resources/plaintexts/lorem-5000.txt"}, + }; + } + + + @Test(dataProvider = "block-cipher") + public void testBlockCipherEncryptDecrypt(final String plaintext, final BlockCipher cipher, final Nonce nonce) + { + final SecretKey key = SecretKeyGenerator.generate(cipher); + final byte[] ciphertext = CipherUtil.encrypt(cipher, key, nonce, plaintext.getBytes()); + final byte[] result = CipherUtil.decrypt(cipher, key, ciphertext); + assertEquals(new String(result), plaintext); + } + + + @Test(dataProvider = "aead-block-cipher") + public void testAeadBlockCipherEncryptDecrypt(final String plaintext, final AEADBlockCipher cipher) + { + final BlockCipher under = cipher.getUnderlyingCipher(); + final SecretKey key = SecretKeyGenerator.generate(under); + final byte[] ciphertext = CipherUtil.encrypt(cipher, key, new RBGNonce(12), plaintext.getBytes()); + final byte[] result = CipherUtil.decrypt(cipher, key, ciphertext); + assertEquals(new String(result), plaintext); + } + + + @Test(dataProvider = "plaintext-files") + public void testBlockCipherEncryptDecryptStream(final String path) + throws Exception + { + final BlockCipher cipher = new CBCBlockCipher(new AESEngine()); + final SecretKey key = SecretKeyGenerator.generate(cipher); + final Nonce nonce = new CounterNonce("vt-crypt", 1); + final File file = new File(path); + final String expected = new String(StreamUtil.readAll(file)); + final ByteArrayOutputStream tempOut = new ByteArrayOutputStream(); + CipherUtil.encrypt(cipher, key, nonce, StreamUtil.makeStream(file), tempOut); + + final ByteArrayInputStream tempIn = new ByteArrayInputStream(tempOut.toByteArray()); + final ByteArrayOutputStream actual = new ByteArrayOutputStream(); + CipherUtil.decrypt(cipher, key, tempIn, actual); + assertEquals(actual.toString(), expected); + } + + + @Test(dataProvider = "plaintext-files") + public void testAeadBlockCipherEncryptDecryptStream(final String path) + throws Exception + { + final AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); + final SecretKey key = SecretKeyGenerator.generate(cipher.getUnderlyingCipher()); + final File file = new File(path); + final String expected = new String(StreamUtil.readAll(file)); + final ByteArrayOutputStream tempOut = new ByteArrayOutputStream(); + CipherUtil.encrypt(cipher, key, new RBGNonce(), StreamUtil.makeStream(file), tempOut); + + final ByteArrayInputStream tempIn = new ByteArrayInputStream(tempOut.toByteArray()); + final ByteArrayOutputStream actual = new ByteArrayOutputStream(); + CipherUtil.decrypt(cipher, key, tempIn, actual); + assertEquals(actual.toString(), expected); + } + + + @Test + public void testDecryptArrayBackwardCompatibleHeader() + { + final AEADBlockCipher cipher = new OCBBlockCipher(new TwofishEngine(), new TwofishEngine()); + final String expected = "Have you passed through this night?"; + final String v1CiphertextHex = + "0000001f0000000c76746d770002ba17043672d900000007767463727970745a38dee735266e3f5f7aafec8d1c9ed8a0830a2ff9" + + "c3a46c25f89e69b6eb39dbb82fd13da50e32b2544a73f1a4476677b377e6"; + final byte[] plaintext = CipherUtil.decrypt(cipher, STATIC_KEY, CodecUtil.hex(v1CiphertextHex)); + assertEquals(expected, ByteUtil.toString(plaintext)); + } + + + @Test + public void testDecryptStreamBackwardCompatibleHeader() + { + final AEADBlockCipher cipher = new OCBBlockCipher(new TwofishEngine(), new TwofishEngine()); + final String expected = "Have you passed through this night?"; + final String v1CiphertextHex = + "0000001f0000000c76746d770002ba17043672d900000007767463727970745a38dee735266e3f5f7aafec8d1c9ed8a0830a2ff9" + + "c3a46c25f89e69b6eb39dbb82fd13da50e32b2544a73f1a4476677b377e6"; + final ByteArrayInputStream in = new ByteArrayInputStream(CodecUtil.hex(v1CiphertextHex)); + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + CipherUtil.decrypt(cipher, STATIC_KEY, in, out); + assertEquals(expected, ByteUtil.toString(out.toByteArray())); + } +}
diff --git a/src/test/java/org/cryptacular/util/CsrUtilTest.java b/src/test/java/org/cryptacular/util/CsrUtilTest.java new file mode 100644 index 0000000..3e9743e --- /dev/null +++ b/src/test/java/org/cryptacular/util/CsrUtilTest.java
@@ -0,0 +1,126 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.Arrays; +import java.util.List; +import org.bouncycastle.asn1.pkcs.CertificationRequest; +import org.bouncycastle.pkcs.PKCS10CertificationRequest; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Test class for {@link CsrUtil}. + * + * @author Marvin S. Addison + */ +public class CsrUtilTest +{ + + @DataProvider(name = "csr-files") + public Object[][] getCsrFiles() + { + return new Object[][] { + new Object[] {"/csrs/simple-ec-prime256v1.csr"}, + new Object[] {"/csrs/simple-ec-secp384r1.csr"}, + new Object[] {"/csrs/simple-rsa-1024.csr"}, + new Object[] {"/csrs/with-sans-rsa-2048.csr"}, + }; + } + + @DataProvider(name = "key-lengths") + public Object[][] getKeyLengths() + { + return new Object[][] { + new Object[] {"/csrs/simple-ec-prime256v1.csr", 256}, + new Object[] {"/csrs/simple-ec-secp384r1.csr", 384}, + new Object[] {"/csrs/simple-rsa-1024.csr", 1024}, + new Object[] {"/csrs/with-sans-rsa-2048.csr", 2048}, + }; + } + + @DataProvider(name = "sig-alg-names") + public Object[][] getSigAlgNames() + { + return new Object[][] { + new Object[] {"/csrs/simple-ec-secp384r1.csr", "SHA256withECDSA"}, + new Object[] {"/csrs/simple-ec-prime256v1.csr", "SHA256withECDSA"}, + new Object[] {"/csrs/simple-rsa-1024.csr", "SHA256withRSA"}, + new Object[] {"/csrs/with-sans-rsa-2048.csr", "SHA256withRSA"}, + }; + } + + @DataProvider(name = "names") + public Object[][] getNames() + { + return new Object[][] { + new Object[] {"/csrs/simple-ec-prime256v1.csr", "simple.example.com"}, + new Object[] {"/csrs/simple-ec-secp384r1.csr", "simple.example.com"}, + new Object[] {"/csrs/simple-rsa-1024.csr", "simple.example.com"}, + new Object[] { + "/csrs/with-sans-rsa-2048.csr", + "host.example.com", + "dev.host.example.com", + "pprd.host.example.com", + }, + }; + } + + @DataProvider(name = "key-algs") + public Object[][] getKeyAlgs() + { + return new Object[][] { + new Object[] {"RSA"}, + new Object[] {"EC"}, + }; + } + + @Test(dataProvider = "csr-files") + public void testEncodeCsr(final String classPath) throws IOException + { + final CertificationRequest csr1 = CsrUtil.readCsr(getClass().getResourceAsStream(classPath)); + final String encoded = CsrUtil.encodeCsr(new PKCS10CertificationRequest(csr1)); + final CertificationRequest csr2 = CsrUtil.decodeCsr(encoded); + assertEquals(csr1.getEncoded(), csr2.getEncoded()); + } + + @Test(dataProvider = "names") + public void testNames(final String classPath, final String... names) + { + final CertificationRequest csr = CsrUtil.readCsr(getClass().getResourceAsStream(classPath)); + assertEquals(CsrUtil.commonNames(csr).get(0), names[0]); + final List<String> sans = CsrUtil.subjectAltNames(csr); + for (int i = 1; i < names.length; i++) { + assertEquals(sans.get(i - 1), names[i]); + } + } + + @Test(dataProvider = "key-lengths") + public void testKeyLength(final String classPath, final int length) + { + final CertificationRequest csr = CsrUtil.readCsr(getClass().getResourceAsStream(classPath)); + assertEquals(CsrUtil.keyLength(csr), length); + } + + @Test(dataProvider = "sig-alg-names") + public void testSigAlgName(final String classPath, final String sigAlgName) + { + final CertificationRequest csr = CsrUtil.readCsr(getClass().getResourceAsStream(classPath)); + assertEquals(CsrUtil.sigAlgName(csr), sigAlgName); + } + + @Test(dataProvider = "key-algs") + public void testGenerateCsr(final String keyAlg) throws Exception + { + final KeyPair keyPair = KeyPairGenerator.getInstance(keyAlg).generateKeyPair(); + final String hostname = keyAlg.toLowerCase() + ".example.org"; + final String dn = "CN=" + hostname + ",DC=example,DC=org"; + final String[] sans = {"dev." + hostname, "pprd." + hostname}; + final CertificationRequest csr = CsrUtil.generateCsr(keyPair, dn, sans).toASN1Structure(); + assertEquals(CsrUtil.commonNames(csr).get(0), hostname); + assertEquals(CsrUtil.subjectAltNames(csr), Arrays.asList(sans)); + } +}
diff --git a/src/test/java/org/cryptacular/util/HashUtilTest.java b/src/test/java/org/cryptacular/util/HashUtilTest.java new file mode 100644 index 0000000..16b8f40 --- /dev/null +++ b/src/test/java/org/cryptacular/util/HashUtilTest.java
@@ -0,0 +1,150 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.File; +import java.io.InputStream; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.util.encoders.Hex; +import org.cryptacular.FailListener; +import org.cryptacular.SaltedHash; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link HashUtil}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class HashUtilTest +{ + private static final byte[] SALT = new byte[] {0, 1, 2, 3, 4, 5, 6, 7}; + + @DataProvider(name = "salted-hashes") + public Object[][] getSaltedHashData() + { + return + new Object[][] { + { + new SHA1Digest(), + new Object[] {ByteUtil.toBytes("deoxyribonucleic acid"), }, + 1, + "0aDM5g/qqfVV/8MIqkTKQaklWSg=", + }, + { + new SHA1Digest(), + new Object[] { + ByteUtil.toBytes("protoporphyrin-9"), + SALT, + }, + 1, + "6SafHIoTusYN6dnK1pxx7udaBLA=", + }, + { + new SHA256Digest(), + new Object[] { + SALT, + ByteUtil.toBytes("N-arachidonoylethanolamine"), + }, + 5, + "RWIg3BIXdqZPI9C7PFvSn62miU3L9ponVZLvKmC9XlQ=", + }, + }; + } + + @DataProvider(name = "hash-compare") + public Object[][] getHashCompareData() + { + return + new Object[][] { + { + new SHA1Digest(), + CodecUtil.b64("7fyOZXGp+gKMziV/2Px7RIMkxyI2O1H8"), + 1, + ByteUtil.toBytes("password"), + }, + { + new SHA1Digest(), + CodecUtil.b64("0aDM5g/qqfVV/8MIqkTKQaklWSg="), + 1, + ByteUtil.toBytes("deoxyribonucleic acid"), + }, + }; + } + + @DataProvider(name = "salted-hash-compare") + public Object[][] getSaltedHashCompareData() + { + return + new Object[][] { + { + new SHA1Digest(), + new SaltedHash(CodecUtil.b64("7fyOZXGp+gKMziV/2Px7RIMkxyI2O1H8"), 20, true), + 1, + true, + ByteUtil.toBytes("password"), + }, + }; + } + + + @DataProvider(name = "file-hashes") + public Object[][] getFileHashes() + { + return + new Object[][] { + new Object[] { + "src/test/resources/plaintexts/lorem-1200.txt", + "f0746e8978b3eccca05284dd12f098fdea32c8bc", + }, + new Object[] { + "src/test/resources/plaintexts/lorem-5000.txt", + "1142d7a2661760624fa41b002be6c66c23b50602", + }, + }; + } + + + @Test(dataProvider = "salted-hashes") + public void testSaltedHash(final Digest digest, final Object[] data, final int iterations, final String expected) + throws Exception + { + assertEquals(CodecUtil.b64(HashUtil.hash(digest, iterations, data)), expected); + } + + + @Test(dataProvider = "hash-compare") + public void testCompareHash(final Digest digest, final byte[] hash, final int iterations, final byte[] data) + throws Exception + { + assertTrue(HashUtil.compareHash(digest, hash, iterations, data)); + } + + + @Test(dataProvider = "salted-hash-compare") + public void testCompareSaltedHash( + final Digest digest, + final SaltedHash saltedHash, + final int iterations, + final boolean saltAfterData, + final byte[] data) + throws Exception + { + assertTrue(HashUtil.compareHash(digest, saltedHash, iterations, saltAfterData, data)); + } + + + @Test(dataProvider = "file-hashes") + public void testHashStream(final String path, final String expected) + throws Exception + { + try (InputStream in = StreamUtil.makeStream(new File(path))) { + assertEquals(Hex.toHexString(HashUtil.sha1(in)), expected); + } + } +}
diff --git a/src/test/java/org/cryptacular/util/KeyPairUtilTest.java b/src/test/java/org/cryptacular/util/KeyPairUtilTest.java new file mode 100644 index 0000000..7a28e78 --- /dev/null +++ b/src/test/java/org/cryptacular/util/KeyPairUtilTest.java
@@ -0,0 +1,433 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.util; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.KeyPair; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPublicKey; +import org.cryptacular.FailListener; +import org.cryptacular.generator.KeyPairGenerator; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * Unit test for {@link KeyPairUtil} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class KeyPairUtilTest +{ + private static final String KEY_PATH = "src/test/resources/keys/"; + + private final SecureRandom random = new SecureRandom(); + + private final KeyPair rsa512 = KeyPairGenerator.generateRSA(random, 512); + + private final KeyPair dsa1024 = KeyPairGenerator.generateDSA(random, 1024); + + private final KeyPair ec256 = KeyPairGenerator.generateEC(random, 256); + + private final KeyPair ec224 = KeyPairGenerator.generateEC(random, "P-224"); + + + @DataProvider(name = "public-keys") + public Object[][] getPublicKeys() + { + return + new Object[][] { + new Object[] {dsa1024.getPublic(), 1024}, + new Object[] {rsa512.getPublic(), 512}, + new Object[] {ec256.getPublic(), 256}, + }; + } + + @DataProvider(name = "private-keys") + public Object[][] getPrivateKeys() + { + return + new Object[][] { + new Object[] {dsa1024.getPrivate(), 160}, + new Object[] {rsa512.getPrivate(), 512}, + new Object[] {ec224.getPrivate(), 224}, + }; + } + + @DataProvider(name = "key-pairs") + public Object[][] getKeyPairs() + { + final KeyPair rsa512p2 = KeyPairGenerator.generateRSA(random, 512); + return + new Object[][] { + new Object[] {rsa512.getPublic(), rsa512.getPrivate(), true}, + new Object[] {rsa512p2.getPublic(), rsa512p2.getPrivate(), true}, + new Object[] {rsa512.getPublic(), rsa512p2.getPrivate(), false}, + new Object[] {ec256.getPublic(), ec256.getPrivate(), true}, + new Object[] {ec224.getPublic(), ec224.getPrivate(), true}, + new Object[] { + KeyPairUtil.readPublicKey(KEY_PATH + "ec-openssl-prime256v1-named-pub.pem"), + KeyPairUtil.readPrivateKey(KEY_PATH + "ec-openssl-prime256v1-named-nopass.pem"), + true, + }, + new Object[] { + KeyPairUtil.readPublicKey(KEY_PATH + "ec-openssl-secp112r1-named-pub.pem"), + KeyPairUtil.readPrivateKey(KEY_PATH + "ec-openssl-secp112r1-named-nopass.pem"), + true, + }, + new Object[] { + KeyPairUtil.readPublicKey(KEY_PATH + "ec-openssl-secp224k1-explicit-pub.pem"), + KeyPairUtil.readPrivateKey(KEY_PATH + "ec-openssl-secp224k1-explicit-nopass.pem"), + true, + }, + new Object[] { + KeyPairUtil.readPublicKey(KEY_PATH + "ec-openssl-secp256k1-explicit-pub.pem"), + KeyPairUtil.readPrivateKey(KEY_PATH + "ec-openssl-secp256k1-explicit-nopass.pem"), + true, + }, + new Object[] { + KeyPairUtil.readPublicKey(KEY_PATH + "ec-openssl-sect409k1-named-pub.pem"), + KeyPairUtil.readPrivateKey(KEY_PATH + "ec-openssl-sect409k1-named-nopass.pem"), + true, + }, + new Object[] { + KeyPairUtil.readPublicKey(KEY_PATH + "ec-openssl-sect571r1-explicit-pub.pem"), + KeyPairUtil.readPrivateKey(KEY_PATH + "ec-openssl-sect571r1-explicit-nopass.pem"), + true, + }, + new Object[] { + KeyPairUtil.readPublicKey(KEY_PATH + "ec-openssl-sect571r1-explicit-pub.pem"), + KeyPairUtil.readPrivateKey(KEY_PATH + "ec-openssl-sect571r1-explicit-nopass.pem"), + true, + }, + }; + } + + @DataProvider(name = "private-key-files") + public Object[][] getPrivateKeyFiles() + { + return + new Object[][] { + new Object[] {KEY_PATH + "dsa-openssl-nopass.der", DSAPrivateKey.class}, + new Object[] {KEY_PATH + "dsa-openssl-nopass.pem", DSAPrivateKey.class}, + new Object[] { + KEY_PATH + "rsa-openssl-nopass.der", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-openssl-nopass.pem", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-prime256v1-named-nopass.der", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-prime256v1-named-nopass.pem", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-secp112r1-named-nopass.der", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-secp112r1-named-nopass.pem", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-secp224k1-explicit-nopass.der", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-secp224k1-explicit-nopass.pem", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-sect571r1-explicit-nopass.der", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-sect571r1-explicit-nopass.pem", + ECPrivateKey.class, + }, + new Object[] {KEY_PATH + "dsa-pkcs8-nopass.der", DSAPrivateKey.class}, + new Object[] {KEY_PATH + "dsa-pkcs8-nopass.pem", DSAPrivateKey.class}, + new Object[] { + KEY_PATH + "rsa-pkcs8-nopass.der", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-pkcs8-nopass.pem", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-pkcs8-nopass-noheader.pem", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "ec-pkcs8-secp224k1-explicit-nopass.der", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-pkcs8-secp224k1-explicit-nopass.pem", + ECPrivateKey.class, + }, + }; + } + + @DataProvider(name = "encrypted-private-key-files") + public Object[][] getEncryptedPrivateKeyFiles() + { + return + new Object[][] { + new Object[] { + KEY_PATH + "dsa-openssl-des3.pem", + "vtcrypt", + DSAPrivateKey.class, + }, + new Object[] { + KEY_PATH + "rsa-openssl-des.pem", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-openssl-des-noheader.pem", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-openssl-des3.pem", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-secp224k1-explicit-des.pem", + "vtcrypt", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-openssl-sect571r1-explicit-des.pem", + "vtcrypt", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "dsa-pkcs8-priv.der", + "vtcrypt", + DSAPrivateKey.class, + }, + new Object[] { + KEY_PATH + "dsa-pkcs8-priv.pem", + "vtcrypt", + DSAPrivateKey.class, + }, + new Object[] { + KEY_PATH + "dsa-pkcs8-v2-des3.der", + "vtcrypt", + DSAPrivateKey.class, + }, + new Object[] { + KEY_PATH + "dsa-pkcs8-v2-des3.pem", + "vtcrypt", + DSAPrivateKey.class, + }, + new Object[] { + KEY_PATH + "rsa-pkcs8-v1-md5-des.der", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-pkcs8-v1-md5-des.pem", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-pkcs8-v1-md5-rc2-64.der", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-pkcs8-v2-aes256.der", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-pkcs8-v2-aes256.pem", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "rsa-pkcs8-v2-aes256-noheader.pem", + "vtcrypt", + RSAPrivateCrtKey.class, + }, + new Object[] { + KEY_PATH + "ec-pkcs8-secp224k1-explicit-sha1-rc4-128.der", + "vtcrypt", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-pkcs8-secp224k1-explicit-v1-sha1-rc2-64.der", + "vtcrypt", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-pkcs8-secp224k1-explicit-v2-des3.pem", + "vtcrypt", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-pkcs8-sect571r1-explicit-v2-aes128.pem", + "vtcrypt", + ECPrivateKey.class, + }, + new Object[] { + KEY_PATH + "ec-pkcs8-sect571r1-named-v1-sha1-rc2-64.der", + "vtcrypt", + ECPrivateKey.class, + }, + }; + } + + @DataProvider(name = "public-key-files") + public Object[][] getPublicKeyFiles() + { + return + new Object[][] { + new Object[] {KEY_PATH + "dsa-pub.der", DSAPublicKey.class}, + new Object[] {KEY_PATH + "dsa-pub.pem", DSAPublicKey.class}, + new Object[] { + KEY_PATH + "ec-secp224k1-explicit-pub.der", + ECPublicKey.class, + }, + new Object[] { + KEY_PATH + "ec-secp224k1-explicit-pub.pem", + ECPublicKey.class, + }, + new Object[] {KEY_PATH + "rsa-pub.der", RSAPublicKey.class}, + new Object[] {KEY_PATH + "rsa-pub.pem", RSAPublicKey.class}, + }; + } + + + @Test(dataProvider = "public-keys") + public void testLengthPublicKey(final PublicKey key, final int expectedLength) + throws Exception + { + assertEquals(KeyPairUtil.length(key), expectedLength); + } + + @Test(dataProvider = "private-keys") + public void testLengthPrivateKey(final PrivateKey key, final int expectedLength) + throws Exception + { + assertEquals(KeyPairUtil.length(key), expectedLength); + } + + @Test(dataProvider = "key-pairs") + public void testIsKeyPair(final PublicKey pubKey, final PrivateKey privKey, final boolean expected) + throws Exception + { + assertEquals(KeyPairUtil.isKeyPair(pubKey, privKey), expected); + } + + @Test(dataProvider = "private-key-files") + public void testReadPrivateKey(final String path, final Class<?> expectedType) + throws Exception + { + final PrivateKey key = KeyPairUtil.readPrivateKey(path); + assertNotNull(key); + assertTrue(expectedType.isAssignableFrom(key.getClass())); + } + + @Test(dataProvider = "encrypted-private-key-files") + public void testReadEncryptedPrivateKey(final String path, final String password, final Class<?> expectedType) + throws Exception + { + final PrivateKey key = KeyPairUtil.readPrivateKey(path, password.toCharArray()); + assertNotNull(key); + assertTrue(expectedType.isAssignableFrom(key.getClass())); + } + + @Test(dataProvider = "public-key-files") + public void testReadPublicKey(final String path, final Class<?> expectedType) + throws Exception + { + final PublicKey key = KeyPairUtil.readPublicKey(path); + assertNotNull(key); + assertTrue(expectedType.isAssignableFrom(key.getClass())); + } + + @Test(dataProvider = "private-key-files") + public void testClosePrivateKey(final String path, final Class<?> expectedType) + throws Exception + { + final TestableFileInputStream is = new TestableFileInputStream(path); + final PrivateKey key = KeyPairUtil.readPrivateKey(is); + assertNotNull(key); + assertTrue(is.isClosed()); + } + + @Test(dataProvider = "public-key-files") + public void testClosePublicKey(final String path, final Class<?> expectedType) + throws Exception + { + final TestableFileInputStream is = new TestableFileInputStream(path); + final PublicKey key = KeyPairUtil.readPublicKey(is); + assertNotNull(key); + assertTrue(is.isClosed()); + } + + + /** + * Class for testing usage of {@link FileInputStream}. + */ + private static class TestableFileInputStream extends FileInputStream + { + + /** Whether {@link #close()} has been invoked. */ + private boolean isClosed; + + /** + * Default constructor. + * + * @param name of the file to open + * + * @throws FileNotFoundException if an error occurs + */ + TestableFileInputStream(final String name) + throws FileNotFoundException + { + super(name); + } + + @Override + public void close() + throws IOException + { + super.close(); + isClosed = true; + } + + /** + * Returns whether {@link #close()} has been invoked. + * + * @return whether {@link #close()} has been invoked + */ + public boolean isClosed() + { + return isClosed; + } + } +}
diff --git a/src/test/java/org/cryptacular/x509/ExtensionReaderTest.java b/src/test/java/org/cryptacular/x509/ExtensionReaderTest.java new file mode 100644 index 0000000..f75e15b --- /dev/null +++ b/src/test/java/org/cryptacular/x509/ExtensionReaderTest.java
@@ -0,0 +1,352 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509; + +import java.security.cert.X509Certificate; +import java.util.List; +import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.asn1.x509.AccessDescription; +import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier; +import org.bouncycastle.asn1.x509.DistributionPoint; +import org.bouncycastle.asn1.x509.DistributionPointName; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.asn1.x509.GeneralNames; +import org.bouncycastle.asn1.x509.KeyPurposeId; +import org.bouncycastle.asn1.x509.KeyUsage; +import org.bouncycastle.asn1.x509.PolicyInformation; +import org.bouncycastle.asn1.x509.PolicyQualifierInfo; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.cryptacular.FailListener; +import org.cryptacular.util.CertUtil; +import org.cryptacular.util.CodecUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link ExtensionReader}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class ExtensionReaderTest +{ + private static final String CRT_PATH = "src/test/resources/certs/"; + + @DataProvider(name = "subject-alt-name") + public Object[][] getSubjectAltNames() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new String[] {"eprov@vt.edu"}, + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "ed.middleware.vt.edu.crt"), + new String[] { + "ed.middleware.vt.edu", + "directory.vt.edu", + "id.directory.vt.edu", + "authn.directory.vt.edu", + "ldap.vt.edu", + }, + }, + }; + } + + @DataProvider(name = "issuer-alt-name") + public Object[][] getIssuerAltNames() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "test.example.com.crt"), + new String[] {"snake-1.example.com", "snake-2.example.com"}, + }, + }; + } + + @DataProvider(name = "basic-constraints") + public Object[][] getBasicConstraints() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "thawte-premium-server-ca.crt"), + true, + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + false, + }, + }; + } + + @DataProvider(name = "certificate-policies") + public Object[][] getCertificatePolicies() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new PolicyInformation[] { + new PolicyInformation(new ASN1ObjectIdentifier("1.3.6.1.4.1.6760.5.2.2.2.1")), + new PolicyInformation(new ASN1ObjectIdentifier("1.3.6.1.4.1.6760.5.2.2.1.1")), + new PolicyInformation( + new ASN1ObjectIdentifier("1.3.6.1.4.1.6760.5.2.2.4.1"), + new DERSequence(new PolicyQualifierInfo("http://www.pki.vt.edu/vtuca/cps/index.html"))), + new PolicyInformation(new ASN1ObjectIdentifier("1.3.6.1.4.1.6760.5.2.2.3.1")), + }, + }, + }; + } + + @DataProvider(name = "subject-key-id") + public Object[][] getSubjectKeyIds() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + "25:48:2F:28:EC:5D:19:BB:1D:25:AE:94:93:B1:7B:B5:35:96:24:66", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + "31:AE:F1:7C:98:67:E9:1F:19:69:A2:A7:84:1E:67:5C:AA:C3:6B:75", + }, + }; + } + + @DataProvider(name = "authority-key-id") + public Object[][] getAuthorityKeyIds() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + "38:E0:6F:AE:48:ED:5E:23:F6:22:9B:1E:E7:9C:19:16:47:B8:7E:92", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + "FC:8A:50:BA:9E:B9:25:5A:7B:55:85:4F:95:00:63:8F:E9:58:6B:43", + }, + }; + } + + @DataProvider(name = "key-usage") + public Object[][] getKeyUsage() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + KeyUsageBits.usage(KeyUsageBits.DigitalSignature, KeyUsageBits.NonRepudiation), + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + KeyUsageBits.usage(KeyUsageBits.DigitalSignature, KeyUsageBits.KeyEncipherment), + }, + }; + } + + @DataProvider(name = "extended-key-usage") + public Object[][] getExtendedKeyUsage() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + new KeyPurposeId[] { + KeyPurposeId.id_kp_clientAuth, + KeyPurposeId.id_kp_emailProtection, + KeyPurposeId.id_kp_smartcardlogon, + }, + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + new KeyPurposeId[] { + KeyPurposeId.id_kp_serverAuth, + KeyPurposeId.id_kp_clientAuth, + }, + }, + }; + } + + @DataProvider(name = "crl-distribution-points") + public Object[][] getCrlDistributionPoints() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + new DistributionPoint[] { + new DistributionPoint( + new DistributionPointName(new GeneralNames(uri("http://EVSecure-crl.verisign.com/EVSecure2006.crl"))), + null, + null), + }, + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "glider.cc.vt.edu.crt"), + new DistributionPoint[] { + new DistributionPoint( + new DistributionPointName( + new GeneralNames( + uri( + "http://vtca-p.eprov.seti.vt.edu:8080/ejbca/publicweb/" + + "webdist/certdist?cmd=crl&" + + "issuer=CN=Virginia+Tech+Middleware+CA,O=Virginia+" + + "Polytechnic+Institute+and+State+University," + + "DC=vt,DC=edu,C=US"))), + null, + new GeneralNames( + dirName( + "CN=Virginia Tech Middleware CA,O=Virginia Polytechnic " + + "Institute and State University,DC=vt,DC=edu,C=US"))), + }, + }, + }; + } + + @DataProvider(name = "authority-information-access") + public Object[][] getAuthorityInformationAccess() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "login.live.com.crt"), + new AccessDescription[] { + new AccessDescription(AccessDescription.id_ad_ocsp, uri("http://EVSecure-ocsp.verisign.com")), + new AccessDescription( + AccessDescription.id_ad_caIssuers, + uri("http://EVSecure-aia.verisign.com/EVSecure2006.cer")), + }, + }, + }; + } + + + @Test(dataProvider = "subject-alt-name") + public void testReadSubjectAlternativeName(final X509Certificate cert, final String[] expected) + throws Exception + { + final GeneralNames names = new ExtensionReader(cert).readSubjectAlternativeName(); + assertEquals(names.getNames().length, expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(names.getNames()[i].getName().toString(), expected[i]); + } + } + + @Test(dataProvider = "issuer-alt-name") + public void testReadIssuerAlternativeName(final X509Certificate cert, final String[] expected) + throws Exception + { + final GeneralNames names = new ExtensionReader(cert).readIssuerAlternativeName(); + assertEquals(names.getNames().length, expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(names.getNames()[i].getName().toString(), expected[i]); + } + } + + @Test(dataProvider = "basic-constraints") + public void testReadBasicConstraints(final X509Certificate cert, final boolean expected) + throws Exception + { + assertEquals(new ExtensionReader(cert).readBasicConstraints().isCA(), expected); + } + + @Test(dataProvider = "certificate-policies") + public void testReadCertificatePolicies(final X509Certificate cert, final PolicyInformation[] expected) + throws Exception + { + final List<PolicyInformation> policies = new ExtensionReader(cert).readCertificatePolicies(); + assertEquals(policies.size(), expected.length); + + PolicyInformation current; + for (int i = 0; i < expected.length; i++) { + current = policies.get(i); + assertEquals(current.getPolicyIdentifier(), expected[i].getPolicyIdentifier()); + if (expected[i].getPolicyQualifiers() != null) { + for (int j = 0; j < expected[i].getPolicyQualifiers().size(); j++) { + assertEquals(current.getPolicyQualifiers().getObjectAt(j), expected[i].getPolicyQualifiers().getObjectAt(j)); + } + } + } + } + + @Test(dataProvider = "subject-key-id") + public void testReadSubjectKeyIdentifier(final X509Certificate cert, final String expected) + throws Exception + { + final SubjectKeyIdentifier keyId = new ExtensionReader(cert).readSubjectKeyIdentifier(); + assertEquals(CodecUtil.hex(keyId.getKeyIdentifier(), true).toUpperCase(), expected); + } + + @Test(dataProvider = "authority-key-id") + public void testReadAuthorityKeyIdentifier(final X509Certificate cert, final String expected) + throws Exception + { + final AuthorityKeyIdentifier keyId = new ExtensionReader(cert).readAuthorityKeyIdentifier(); + assertEquals(CodecUtil.hex(keyId.getKeyIdentifier(), true).toUpperCase(), expected); + } + + @Test(dataProvider = "key-usage") + public void testReadKeyUsage(final X509Certificate cert, final int expected) + throws Exception + { + final KeyUsage usage = new ExtensionReader(cert).readKeyUsage(); + final byte[] bytes = usage.getBytes(); + final int result; + if (bytes.length == 1) { + result = bytes[0] & 0xff; + } else { + result = (bytes[1] & 0xff) << 8 | (bytes[0] & 0xff); + } + assertEquals(result, expected); + } + + @Test(dataProvider = "extended-key-usage") + public void testReadExtendedKeyUsage(final X509Certificate cert, final KeyPurposeId[] expected) + throws Exception + { + final List<KeyPurposeId> purposes = new ExtensionReader(cert).readExtendedKeyUsage(); + assertEquals(purposes.size(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(purposes.get(i), expected[i]); + } + } + + @Test(dataProvider = "crl-distribution-points") + public void testReadCRLDistributionPoints(final X509Certificate cert, final DistributionPoint[] expected) + throws Exception + { + final List<DistributionPoint> points = new ExtensionReader(cert).readCRLDistributionPoints(); + assertEquals(points.size(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(points.get(i), expected[i]); + } + } + + @Test(dataProvider = "authority-information-access") + public void testReadAuthorityInformationAccess(final X509Certificate cert, final AccessDescription[] expected) + throws Exception + { + final List<AccessDescription> descriptions = new ExtensionReader(cert).readAuthorityInformationAccess(); + assertEquals(descriptions.size(), expected.length); + for (int i = 0; i < expected.length; i++) { + assertEquals(descriptions.get(i), expected[i]); + } + } + + private GeneralName uri(final String uri) + { + return new GeneralName(GeneralName.uniformResourceIdentifier, uri); + } + + private GeneralName dirName(final String dn) + { + return new GeneralName(GeneralName.directoryName, dn); + } +}
diff --git a/src/test/java/org/cryptacular/x509/dn/LdapNameFormatterTest.java b/src/test/java/org/cryptacular/x509/dn/LdapNameFormatterTest.java new file mode 100644 index 0000000..b6546a3 --- /dev/null +++ b/src/test/java/org/cryptacular/x509/dn/LdapNameFormatterTest.java
@@ -0,0 +1,83 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +import javax.security.auth.x500.X500Principal; +import org.cryptacular.FailListener; +import org.cryptacular.util.CertUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link LdapNameFormatter} class. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class LdapNameFormatterTest +{ + private static final String CRT_PATH = "src/test/resources/certs/"; + + @DataProvider(name = "distinguished-names") + public Object[][] getDistinguishedNames() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt").getSubjectX500Principal(), + "C=US,DC=vt,DC=edu,O=Virginia Polytechnic Institute and State University,CN=Marvin S Addison,UID=1145718", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt").getIssuerX500Principal(), + "SERIALNUMBER=12,CN=DEV Virginia Tech Class 1 Server CA,O=Virginia " + + "Polytechnic Institute and State University,C=US,DC=vt,DC=edu", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "glider.cc.vt.edu.crt").getSubjectX500Principal(), + "C=US,DC=edu,DC=vt,ST=Virginia,L=Blacksburg,O=Virginia Polytechnic Institute and State University," + + "OU=Middleware-Client,OU=SETI,SERIALNUMBER=1248110657961,CN=glider.cc.vt.edu", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "glider.cc.vt.edu.crt").getIssuerX500Principal(), + "CN=Virginia Tech Middleware CA,O=Virginia Polytechnic Institute and State University,C=US,DC=vt,DC=edu", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "multi-value-rdn-1.crt").getSubjectX500Principal(), + "CN=b.foo.com,CN=a.foo.com,DC=ldaptive,DC=org", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "multi-value-rdn-2.crt").getSubjectX500Principal(), + "DC=org,DC=ldaptive,CN=a.foo.com+CN=b.foo.com", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "needs-escaping-1.crt").getSubjectX500Principal(), + "CN=DC=example\\, DC=com,O=VPI&SU,L=Blacksburg,ST=Virginia,C=US,DC=vt,DC=edu", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "needs-escaping-2.crt").getSubjectX500Principal(), + "CN=\\#DEADBEEF,O=VPI&SU,L=Blacksburg,ST=Virginia,C=US,DC=vt,DC=edu", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "needs-escaping-3.crt").getSubjectX500Principal(), + "CN=\\ space,O=VPI&SU,L=Blacksburg,ST=Virginia,C=US,DC=vt,DC=edu", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "needs-escaping-4.crt").getSubjectX500Principal(), + "CN=space2 \\ ,O=VPI&SU,L=Blacksburg,ST=Virginia,C=US,DC=vt,DC=edu", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "unknown-dn-attr.crt").getSubjectX500Principal(), + "DC=org,DC=example,1.2.3.4.5=#6e6f6e73656e7365,CN=marzipan", + }, + }; + } + + + @Test(dataProvider = "distinguished-names") + public void testFormat(final X500Principal dn, final String expected) + throws Exception + { + assertEquals(new LdapNameFormatter().format(dn), expected); + } +}
diff --git a/src/test/java/org/cryptacular/x509/dn/NameReaderTest.java b/src/test/java/org/cryptacular/x509/dn/NameReaderTest.java new file mode 100644 index 0000000..3ed0fc2 --- /dev/null +++ b/src/test/java/org/cryptacular/x509/dn/NameReaderTest.java
@@ -0,0 +1,97 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.x509.dn; + +import java.security.cert.X509Certificate; +import org.cryptacular.FailListener; +import org.cryptacular.util.CertUtil; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; +import static org.testng.Assert.assertEquals; + +/** + * Unit test for {@link NameReader}. + * + * @author Middleware Services + */ +@Listeners(FailListener.class) +public class NameReaderTest +{ + private static final String CRT_PATH = "src/test/resources/certs/"; + + @DataProvider(name = "subjects") + public Object[][] getSubjects() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + "UID=1145718, CN=Marvin S Addison, O=Virginia Polytechnic " + + "Institute and State University, DC=edu, DC=vt, C=US", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "glider.cc.vt.edu.crt"), + "CN=glider.cc.vt.edu, SERIALNUMBER=1248110657961, OU=SETI, " + + "OU=Middleware-Client, O=Virginia Polytechnic Institute and " + + "State University, L=Blacksburg, ST=Virginia, DC=vt, DC=edu, C=US", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "multi-value-rdn-1.crt"), + "DC=org, DC=ldaptive, CN=a.foo.com, CN=b.foo.com", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "multi-value-rdn-2.crt"), + "CN=a.foo.com, CN=b.foo.com, DC=ldaptive, DC=org", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "scantor-dn-description.crt"), + "DESCRIPTION=6MtpJS1dcC7t254v, CN=cantor.2@osu.edu, EMAILADDRESS=cantor.2@osu.edu", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "unknown-dn-attr.crt"), + "CN=marzipan, 1.2.3.4.5=nonsense, DC=example, DC=org", + }, + }; + } + + @DataProvider(name = "issuers") + public Object[][] getIssuers() + { + return + new Object[][] { + new Object[] { + CertUtil.readCertificate(CRT_PATH + "serac-dev-test.crt"), + "DC=edu, DC=vt, C=US, O=Virginia Polytechnic Institute and State " + + "University, CN=DEV Virginia Tech Class 1 Server CA, SERIALNUMBER=12", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "glider.cc.vt.edu.crt"), + "DC=edu, DC=vt, C=US, O=Virginia Polytechnic Institute and State University, CN=Virginia Tech Middleware CA", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "multi-value-rdn-1.crt"), + "DC=org, DC=ldaptive, CN=a.foo.com, CN=b.foo.com", + }, + new Object[] { + CertUtil.readCertificate(CRT_PATH + "multi-value-rdn-2.crt"), + "CN=a.foo.com, CN=b.foo.com, DC=ldaptive, DC=org", + }, + }; + } + + @Test(dataProvider = "subjects") + public void testReadSubject(final X509Certificate cert, final String expected) + throws Exception + { + final RDNSequence sequence = new NameReader(cert).readSubject(); + assertEquals(sequence.toString(), expected); + } + + @Test(dataProvider = "issuers") + public void testReadIssuer(final X509Certificate cert, final String expected) + throws Exception + { + final RDNSequence sequence = new NameReader(cert).readIssuer(); + assertEquals(sequence.toString(), expected); + } +}
diff --git a/src/test/resources/certs/ed.middleware.vt.edu.crt b/src/test/resources/certs/ed.middleware.vt.edu.crt new file mode 100644 index 0000000..446eaeb --- /dev/null +++ b/src/test/resources/certs/ed.middleware.vt.edu.crt
@@ -0,0 +1,42 @@ +-----BEGIN CERTIFICATE----- +MIIHTjCCBTagAwIBAgIITROLEwW3lyYwDQYJKoZIhvcNAQEFBQAwgZoxEzARBgoJ +kiaJk/IsZAEZEwNlZHUxEjAQBgoJkiaJk/IsZAEZEwJ2dDELMAkGA1UEBhMCVVMx +PDA6BgNVBAoTM1ZpcmdpbmlhIFBvbHl0ZWNobmljIEluc3RpdHV0ZSBhbmQgU3Rh +dGUgVW5pdmVyc2l0eTEkMCIGA1UEAxMbVmlyZ2luaWEgVGVjaCBNaWRkbGV3YXJl +IENBMB4XDTEyMDUyNTE2NTMxN1oXDTE0MDUyNTE2NTMxN1owggEAMR0wGwYDVQQD +DBRlZC5taWRkbGV3YXJlLnZ0LmVkdTEcMBoGA1UECwwTTWlkZGxld2FyZSBTZXJ2 +aWNlczElMCMGA1UECwwcTWlkZGxld2FyZS1TZXJ2ZXItd2l0aC1zYWx0cjE8MDoG +A1UECgwzVmlyZ2luaWEgUG9seXRlY2huaWMgSW5zdGl0dXRlIGFuZCBTdGF0ZSBV +bml2ZXJzaXR5MRMwEQYDVQQHDApCbGFja3NidXJnMREwDwYDVQQIDAhWaXJnaW5p +YTESMBAGCgmSJomT8ixkARkWAnZ0MRMwEQYKCZImiZPyLGQBGRYDZWR1MQswCQYD +VQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2+UJtj+Bhd +um+GQe9zciP6nZKBgNWDLPLHo2s+beL0R9n9qIVapqUaB6Rqssgaf0K3WegX+ULR +9m6Dz0ljh9FOgfOtzDrPTAtqkGvz+HQZtKaAZzHW4WFAtjUrhhY1JI4zIvJiEkSg +db26IhSqb0+CB15XPZDE+ra5j4QlygOqfUs8fF+SrjkynAQ1ASWsnT+aHplg2nEY +VJPaN2LhxWjr6Uo2iVCuzc1vR3Tua/veZACztb2x8QK6sgUcsrH/OY02yFcJ7qyo +NhgxnMJSpLB/Mo6BLFjMn2fetePt96RgsO4FDwzDT7h1iGf5vXBBiId9QDk7fvZw +Z9wcJkLa1R0CAwEAAaOCAi0wggIpMB0GA1UdDgQWBBSrVN4+XHdiFIwgwiAFIWRX +UFRxPzAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFP02QCKa1fxb8ATJkOZPT4hLRPhO +MHgGA1UdIARxMG8wDgYMKwYBBAG0aAUCAgIBMA4GDCsGAQQBtGgFAgIBATA9Bgwr +BgEEAbRoBQICBAEwLTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5wa2kudnQuZWR1 +L3Z0bXcvY3BzLzAOBgwrBgEEAbRoBQICAwEwgdIGA1UdHwSByjCBxzCBxKCBwaCB +voaBu2h0dHA6Ly92dGNhLXAuZXByb3Yuc2V0aS52dC5lZHU6ODA4MC9lamJjYS9w +dWJsaWN3ZWIvd2ViZGlzdC9jZXJ0ZGlzdD9jbWQ9Y3JsJmlzc3Vlcj1DTj1WaXJn +aW5pYStUZWNoK01pZGRsZXdhcmUrQ0EsTz1WaXJnaW5pYStQb2x5dGVjaG5pYytJ +bnN0aXR1dGUrYW5kK1N0YXRlK1VuaXZlcnNpdHksREM9dnQsREM9ZWR1LEM9VVMw +CwYDVR0PBAQDAgTwMBMGA1UdJQQMMAoGCCsGAQUFBwMBMGsGA1UdEQRkMGKCFGVk +Lm1pZGRsZXdhcmUudnQuZWR1ghBkaXJlY3RvcnkudnQuZWR1ghNpZC5kaXJlY3Rv +cnkudnQuZWR1ghZhdXRobi5kaXJlY3RvcnkudnQuZWR1ggtsZGFwLnZ0LmVkdTAN +BgkqhkiG9w0BAQUFAAOCAgEABs6vLEbm28l3tpZOy1iWJEZbaXsKwVdMZXQKlQWx +QzXNe99ktfzsq1Rf99YhNefcxpwqIb4TKxc9e8hjSH39ySLso3PpjjywSjZzFkVn +I/gC+R8Lq6tgFJnEGeRfu6z699ej7YaX21SKqy4+qh+pXTKV9yppch8Wiz440Pbx +qTg0/422nFjBoDcfSby9g4GaFcQKsx26MZ3cY+bFZUSVZ9skFlv8hXQ60NnHL23L +TzYTje+/7gZAYMRWKQpbBvXGj/cu7cpX56qYNRV9My+xmDxkhv2aJaEyY6kePIY4 +4Ziu8PUfSLLK7CZexOyNTbVbbyL31s7ao07v1iXiY8rIJieCOrzEpqMHZ7qaGZVJ +26WL4fgVfGQu8mIWnlcE7R2sa6RtmLBqAHttXozYCuGVxMYYsyoFLg+I8p1wnFAF +4dS6rARiI/dbOrz5z8UokDDaAAfP8FfpyTBzi0mZo85XPZeB2Vy0zQjiZFAuzq8f +9XShx3VnLNz7FIvy2wNQwmLM3LXijL4NzCKo5GAE9KniFr4x9d+iAdzcOyZe5a9i +6KtfZD+9Z7HyV45gzKWR6WmNL9lS44afYNW1dTxQCY7dOo+27nW3eS59rcxHlifc +maJA9izvFEMxGDBzu5isUWyj+qp7VFpAm63uHKwy8+wmemm7C8xF04f+QnHpw5MM +Bl0= +-----END CERTIFICATE----- \ No newline at end of file
diff --git a/src/test/resources/certs/ed.middleware.vt.edu.der b/src/test/resources/certs/ed.middleware.vt.edu.der new file mode 100644 index 0000000..eec76f3 --- /dev/null +++ b/src/test/resources/certs/ed.middleware.vt.edu.der Binary files differ
diff --git a/src/test/resources/certs/ed.middleware.vt.edu.p7b b/src/test/resources/certs/ed.middleware.vt.edu.p7b new file mode 100644 index 0000000..074bc4b --- /dev/null +++ b/src/test/resources/certs/ed.middleware.vt.edu.p7b
@@ -0,0 +1,42 @@ +-----BEGIN PKCS7----- +MIIHTjCCBTagAwIBAgIITROLEwW3lyYwDQYJKoZIhvcNAQEFBQAwgZoxEzARBgoJ +kiaJk/IsZAEZEwNlZHUxEjAQBgoJkiaJk/IsZAEZEwJ2dDELMAkGA1UEBhMCVVMx +PDA6BgNVBAoTM1ZpcmdpbmlhIFBvbHl0ZWNobmljIEluc3RpdHV0ZSBhbmQgU3Rh +dGUgVW5pdmVyc2l0eTEkMCIGA1UEAxMbVmlyZ2luaWEgVGVjaCBNaWRkbGV3YXJl +IENBMB4XDTEyMDUyNTE2NTMxN1oXDTE0MDUyNTE2NTMxN1owggEAMR0wGwYDVQQD +DBRlZC5taWRkbGV3YXJlLnZ0LmVkdTEcMBoGA1UECwwTTWlkZGxld2FyZSBTZXJ2 +aWNlczElMCMGA1UECwwcTWlkZGxld2FyZS1TZXJ2ZXItd2l0aC1zYWx0cjE8MDoG +A1UECgwzVmlyZ2luaWEgUG9seXRlY2huaWMgSW5zdGl0dXRlIGFuZCBTdGF0ZSBV +bml2ZXJzaXR5MRMwEQYDVQQHDApCbGFja3NidXJnMREwDwYDVQQIDAhWaXJnaW5p +YTESMBAGCgmSJomT8ixkARkWAnZ0MRMwEQYKCZImiZPyLGQBGRYDZWR1MQswCQYD +VQQGEwJVUzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN2+UJtj+Bhd +um+GQe9zciP6nZKBgNWDLPLHo2s+beL0R9n9qIVapqUaB6Rqssgaf0K3WegX+ULR +9m6Dz0ljh9FOgfOtzDrPTAtqkGvz+HQZtKaAZzHW4WFAtjUrhhY1JI4zIvJiEkSg +db26IhSqb0+CB15XPZDE+ra5j4QlygOqfUs8fF+SrjkynAQ1ASWsnT+aHplg2nEY +VJPaN2LhxWjr6Uo2iVCuzc1vR3Tua/veZACztb2x8QK6sgUcsrH/OY02yFcJ7qyo +NhgxnMJSpLB/Mo6BLFjMn2fetePt96RgsO4FDwzDT7h1iGf5vXBBiId9QDk7fvZw +Z9wcJkLa1R0CAwEAAaOCAi0wggIpMB0GA1UdDgQWBBSrVN4+XHdiFIwgwiAFIWRX +UFRxPzAJBgNVHRMEAjAAMB8GA1UdIwQYMBaAFP02QCKa1fxb8ATJkOZPT4hLRPhO +MHgGA1UdIARxMG8wDgYMKwYBBAG0aAUCAgIBMA4GDCsGAQQBtGgFAgIBATA9Bgwr +BgEEAbRoBQICBAEwLTArBggrBgEFBQcCARYfaHR0cDovL3d3dy5wa2kudnQuZWR1 +L3Z0bXcvY3BzLzAOBgwrBgEEAbRoBQICAwEwgdIGA1UdHwSByjCBxzCBxKCBwaCB +voaBu2h0dHA6Ly92dGNhLXAuZXByb3Yuc2V0aS52dC5lZHU6ODA4MC9lamJjYS9w +dWJsaWN3ZWIvd2ViZGlzdC9jZXJ0ZGlzdD9jbWQ9Y3JsJmlzc3Vlcj1DTj1WaXJn +aW5pYStUZWNoK01pZGRsZXdhcmUrQ0EsTz1WaXJnaW5pYStQb2x5dGVjaG5pYytJ +bnN0aXR1dGUrYW5kK1N0YXRlK1VuaXZlcnNpdHksREM9dnQsREM9ZWR1LEM9VVMw +CwYDVR0PBAQDAgTwMBMGA1UdJQQMMAoGCCsGAQUFBwMBMGsGA1UdEQRkMGKCFGVk +Lm1pZGRsZXdhcmUudnQuZWR1ghBkaXJlY3RvcnkudnQuZWR1ghNpZC5kaXJlY3Rv +cnkudnQuZWR1ghZhdXRobi5kaXJlY3RvcnkudnQuZWR1ggtsZGFwLnZ0LmVkdTAN +BgkqhkiG9w0BAQUFAAOCAgEABs6vLEbm28l3tpZOy1iWJEZbaXsKwVdMZXQKlQWx +QzXNe99ktfzsq1Rf99YhNefcxpwqIb4TKxc9e8hjSH39ySLso3PpjjywSjZzFkVn +I/gC+R8Lq6tgFJnEGeRfu6z699ej7YaX21SKqy4+qh+pXTKV9yppch8Wiz440Pbx +qTg0/422nFjBoDcfSby9g4GaFcQKsx26MZ3cY+bFZUSVZ9skFlv8hXQ60NnHL23L +TzYTje+/7gZAYMRWKQpbBvXGj/cu7cpX56qYNRV9My+xmDxkhv2aJaEyY6kePIY4 +4Ziu8PUfSLLK7CZexOyNTbVbbyL31s7ao07v1iXiY8rIJieCOrzEpqMHZ7qaGZVJ +26WL4fgVfGQu8mIWnlcE7R2sa6RtmLBqAHttXozYCuGVxMYYsyoFLg+I8p1wnFAF +4dS6rARiI/dbOrz5z8UokDDaAAfP8FfpyTBzi0mZo85XPZeB2Vy0zQjiZFAuzq8f +9XShx3VnLNz7FIvy2wNQwmLM3LXijL4NzCKo5GAE9KniFr4x9d+iAdzcOyZe5a9i +6KtfZD+9Z7HyV45gzKWR6WmNL9lS44afYNW1dTxQCY7dOo+27nW3eS59rcxHlifc +maJA9izvFEMxGDBzu5isUWyj+qp7VFpAm63uHKwy8+wmemm7C8xF04f+QnHpw5MM +Bl0= +-----END PKCS7----- \ No newline at end of file
diff --git a/src/test/resources/certs/entity.crt b/src/test/resources/certs/entity.crt new file mode 100644 index 0000000..5577edd --- /dev/null +++ b/src/test/resources/certs/entity.crt
@@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFSzCCBDOgAwIBAgIJAJlvk9MRIyGCMA0GCSqGSIb3DQEBBQUAMIHJMQswCQYD +VQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExEzARBgNVBAcTCkJsYWNrc2J1cmcx +PDA6BgNVBAoTM1ZpcmdpbmlhIFBvbHl0ZWNobmljIEluc3RpdHV0ZSBhbmQgU3Rh +dGUgVW5pdmVyc2l0eTETMBEGA1UECxMKTWlkZGxld2FyZTEeMBwGA1UEAxMVQmVh +dXJlZ2FyZCBCdW1ibGVmb290MR8wHQYJKoZIhvcNAQkBFhBiZWF1QGV4YW1wbGUu +Y29tMB4XDTEyMDIyMzE1MDc0NloXDTM5MDcxMTE1MDc0NlowgckxCzAJBgNVBAYT +AlVTMREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxhY2tzYnVyZzE8MDoG +A1UEChMzVmlyZ2luaWEgUG9seXRlY2huaWMgSW5zdGl0dXRlIGFuZCBTdGF0ZSBV +bml2ZXJzaXR5MRMwEQYDVQQLEwpNaWRkbGV3YXJlMR4wHAYDVQQDExVCZWF1cmVn +YXJkIEJ1bWJsZWZvb3QxHzAdBgkqhkiG9w0BCQEWEGJlYXVAZXhhbXBsZS5jb20w +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJqxu5wkvp2xIViqPYaqPI +LIojitnspRsPHBe7JjPW+rDHx68RNzkht/uCOTvvDPYvKbf7aZyaFQnTQBQMaWqX +1FkwcMRoVHeJpvi8pS5pyuvcu3kpS3U3aT7t6Joi/qhPBZMc9iu8Pe4jIcf8CPQU +SCMJoSl1llbM0EBxqxTa9vKTsryt1xkVPM0Of1i5RpC4Y9gipSyKffWH8M0RX2p2 +iVu79te0GAwDBmKw2lNLkPodDj8yF78CRnJJw07R8kMVoS0Hi91Sx6NEhbmvc+zi +jpF2/RFsOjDBh3Im6V6ejnDwFu8+5P/kuL04nL1I0lG5Urv0js3Jlgn36DuYomXf +AgMBAAGjggEyMIIBLjAdBgNVHQ4EFgQUOOaPw5TKHJs6XEi1u13t0oHrVNAwgf4G +A1UdIwSB9jCB84AUOOaPw5TKHJs6XEi1u13t0oHrVNChgc+kgcwwgckxCzAJBgNV +BAYTAlVTMREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxhY2tzYnVyZzE8 +MDoGA1UEChMzVmlyZ2luaWEgUG9seXRlY2huaWMgSW5zdGl0dXRlIGFuZCBTdGF0 +ZSBVbml2ZXJzaXR5MRMwEQYDVQQLEwpNaWRkbGV3YXJlMR4wHAYDVQQDExVCZWF1 +cmVnYXJkIEJ1bWJsZWZvb3QxHzAdBgkqhkiG9w0BCQEWEGJlYXVAZXhhbXBsZS5j +b22CCQCZb5PTESMhgjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQAS +WHk3oP64Uat1gBodTpOEPTUvvemEvVvjGGBDZbg+SOA2OYvMrfJTIEYlVcFlGLHU +es82gCFdG7BMrqnZlssMNnvGdSS1SIarsfdv82wizBzuqVCHfMQXJ3ar5XfbY5hJ +PgnJs/LIRLncB8u0UdmZ2xcUDR5RbFtreXplMboRmzv2MXK7m0PhoctLSDFoMAld +RLxXOfD+Geq/ZJC8LLnNaSD483sTltDh4bu1RaQueZ5sb84a2agVnP//LkHwGvxE +MfctVFDTeHng7oCA55Jel8nSoPunp4MG5fUwfMb3yp0Dc+C/EJejCpH5dqO5lflw +76/LBZIpF8V3s1jWNbYL +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/entity.key b/src/test/resources/certs/entity.key new file mode 100644 index 0000000..987e03d --- /dev/null +++ b/src/test/resources/certs/entity.key
@@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAyasbucJL6dsSFYqj2GqjyCyKI4rZ7KUbDxwXuyYz1vqwx8ev +ETc5Ibf7gjk77wz2Lym3+2mcmhUJ00AUDGlql9RZMHDEaFR3iab4vKUuacrr3Lt5 +KUt1N2k+7eiaIv6oTwWTHPYrvD3uIyHH/Aj0FEgjCaEpdZZWzNBAcasU2vbyk7K8 +rdcZFTzNDn9YuUaQuGPYIqUsin31h/DNEV9qdolbu/bXtBgMAwZisNpTS5D6HQ4/ +Mhe/AkZyScNO0fJDFaEtB4vdUsejRIW5r3Ps4o6Rdv0RbDowwYdyJuleno5w8Bbv +PuT/5Li9OJy9SNJRuVK79I7NyZYJ9+g7mKJl3wIDAQABAoIBADwQMUbHHpL9A0rV +Ku1m/Xa+BTqGvVck6YU7ibncq+3oZkRqLbMD7okjYc4sO7R7+MqdM0W288RUZcO8 +PvxfXTbxMMsjmuuz1JJz33tX+xXZMRxh3bk11yh0uSBkeZvYmspGT8V9cBM1orpl +8kkXZZKw1XalwFJcP2fq0nbITILWKC25vc5QSesuOcpP9rs2f+W/d80eF11EPLXw +/cBgqHc9YCk/IAX1vZnXrPep27OzrOmwT7XIQUWQvGA0VjwT3jGE3fF4ZgEbIz3g +gZz346tHkIQzjibKiMjxnq1RO+f0leqfaTyomLD0YeSIg14giwS0i3OwvCOhC4bB +S0Q7k5ECgYEA9fe3ZDKjfKukYRhEwfoJmD/z4ea7mA+9U4N6Ax/WUa1eRGkLFXDf +FCfEIt3/HWW90QN3R2jC7Cc2fZ+QWRzBSuVdvdUZPxEDAvvlWYbIDwblR1rvWeqX +wVw2fudKse/1Sr5iAPw+d1vSSUHFkexsxTP0v3iCJQZppe64JeysTAcCgYEA0eTW +zJCu9WGh55FSbp/nArk1q3Ezqaf+KFs9VY1oMTigE1DHhHLO8ueBRXwtwRWczVpP +uD2xHb9FSKgkyhSXb88rS8E3v/uww8l2bRJDfFe3j87a8T/ChWJSEenH0JA3aisv +IQQhqAfh/uj/l6jjEyJqvtojFr5bjUKiwKaPUWkCgYByT/AhVw94D2VT4q2B4Sy4 +X3B+2nbw0s/QklgQP6mhSAt5i8Ak9NIYUerrsXSxOumezBeRTnTYv9ipRZEWeTC0 +GCka4oDbOJLHvj32/5bWtQO1x+NZTJe+u5ZwIBos3DKJzDVL8+8sFbaDaVfi25gp +hl4G5oDFqFdNUMawiXAB3QKBgBTUiRy0HyjrD45TtcKUy/BRQSpKib4ElgybQXME +HZsE653/Hk3etvsUTpf+wuuuoWkf1VmLhdBV8yJKzZvgf0bxYHKcMlQzPk+v5rjc +XyYv7l+vP7tBgKSMJWjxsorYRSecMYktR8nNPnh11yfN8vsrJzzZmTHgomVaf5xu +6zpBAoGBANyebxHmQwIINzQ8N5zhVo/+jYfLJslNzxg9nb9t2mbO5OAmuZWPrLKt +iw17lA6aLc3YORqs0UntQKRRY2Jqv643Uluo9XvHFpITNzpSkjkcGc1TEfuZJD7m +k782JevhsWkEaMtiQflAOq3lFH3Go1u+qP/A2klt9jbdexfNu4v2 +-----END RSA PRIVATE KEY-----
diff --git a/src/test/resources/certs/glider.cc.vt.edu.crt b/src/test/resources/certs/glider.cc.vt.edu.crt new file mode 100644 index 0000000..0c77bdb --- /dev/null +++ b/src/test/resources/certs/glider.cc.vt.edu.crt
@@ -0,0 +1,43 @@ +-----BEGIN CERTIFICATE----- +MIIHgTCCBWmgAwIBAgIIM9zF+iOD1NIwDQYJKoZIhvcNAQEFBQAwgZoxEzARBgoJ +kiaJk/IsZAEZEwNlZHUxEjAQBgoJkiaJk/IsZAEZEwJ2dDELMAkGA1UEBhMCVVMx +PDA6BgNVBAoTM1ZpcmdpbmlhIFBvbHl0ZWNobmljIEluc3RpdHV0ZSBhbmQgU3Rh +dGUgVW5pdmVyc2l0eTEkMCIGA1UEAxMbVmlyZ2luaWEgVGVjaCBNaWRkbGV3YXJl +IENBMB4XDTA5MDcyMDE4MTYzOVoXDTExMDcyMDE4MTYzOVowgfoxGTAXBgNVBAMM +EGdsaWRlci5jYy52dC5lZHUxFjAUBgNVBAUTDTEyNDgxMTA2NTc5NjExDTALBgNV +BAsMBFNFVEkxGjAYBgNVBAsMEU1pZGRsZXdhcmUtQ2xpZW50MTwwOgYDVQQKDDNW +aXJnaW5pYSBQb2x5dGVjaG5pYyBJbnN0aXR1dGUgYW5kIFN0YXRlIFVuaXZlcnNp +dHkxEzARBgNVBAcMCkJsYWNrc2J1cmcxETAPBgNVBAgMCFZpcmdpbmlhMRIwEAYK +CZImiZPyLGQBGRYCdnQxEzARBgoJkiaJk/IsZAEZFgNlZHUxCzAJBgNVBAYTAlVT +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs5UbcNcLgGVmyzzrz/rR +jnvelOjb6KZVYH+7twTNQ8nrjfKz09xeRGUyYEeeJd4Ma0l+i0kW6i68LsI8Mshm +mKQTbMXdGodXrGmqw468+Az9nbp/fsn6N/PYRJedar9i1IqiVzu638RZpvVuo3Vi +uZjuj0mHC89NNhUQ0NJaefgsKQ3ksAvvyhHtmk6UZyx4S5bQWQ8rVmGuBRAqUjjT +HLde4k2Rid/TVjqT7bAjsIMUKnWx2+tVrx19ePJ2c4XUB9aBIwZ/IMmhVwtpQESn +c32G5t8tY7W/vPiHK1dupYkeOhiyLXAClHIipbOBdUnvIzEmuBmv1Ln1t2Z64O0D +zQIDAQABo4ICZzCCAmMwHQYDVR0OBBYEFMc2newZJGEJXQtUFUlg6d4+MFpnMAkG +A1UdEwQCMAAwHwYDVR0jBBgwFoAU/TZAIprV/FvwBMmQ5k9PiEtE+E4weAYDVR0g +BHEwbzAOBgwrBgEEAbRoBQICAgEwDgYMKwYBBAG0aAUCAgEBMD0GDCsGAQQBtGgF +AgIEATAtMCsGCCsGAQUFBwIBFh9odHRwOi8vd3d3LnBraS52dC5lZHUvdnRtdy9j +cHMvMA4GDCsGAQQBtGgFAgIDATCCAXgGA1UdHwSCAW8wggFrMIIBZ6CBwaCBvoaB +u2h0dHA6Ly92dGNhLXAuZXByb3Yuc2V0aS52dC5lZHU6ODA4MC9lamJjYS9wdWJs +aWN3ZWIvd2ViZGlzdC9jZXJ0ZGlzdD9jbWQ9Y3JsJmlzc3Vlcj1DTj1WaXJnaW5p +YStUZWNoK01pZGRsZXdhcmUrQ0EsTz1WaXJnaW5pYStQb2x5dGVjaG5pYytJbnN0 +aXR1dGUrYW5kK1N0YXRlK1VuaXZlcnNpdHksREM9dnQsREM9ZWR1LEM9VVOigaCk +gZ0wgZoxJDAiBgNVBAMMG1ZpcmdpbmlhIFRlY2ggTWlkZGxld2FyZSBDQTE8MDoG +A1UECgwzVmlyZ2luaWEgUG9seXRlY2huaWMgSW5zdGl0dXRlIGFuZCBTdGF0ZSBV +bml2ZXJzaXR5MRIwEAYKCZImiZPyLGQBGRYCdnQxEzARBgoJkiaJk/IsZAEZFgNl +ZHUxCzAJBgNVBAYTAlVTMAsGA1UdDwQEAwIE8DATBgNVHSUEDDAKBggrBgEFBQcD +AjANBgkqhkiG9w0BAQUFAAOCAgEAoTqj6tu88MnnQ5FzjWnUw4qVtLF9A6Cm/Ku/ +g49/LNSvccGoe55OazE1fYjLLGmPkwICey8ijkvj+kzYEHqPKcNCSqPEW2ALOclF +obTpUQvxSWieYoLSsJ8oBCjQ4RY03uSVnVvnSx0mhXQZffW0q7qfx8+OhDe4JFKo +4/iizuC8OZuvWvUwV3CkufVUpFNQJZ5ClXqkhad265sw8fTGkqZ8e0ccfzTdlomk +UUhsykFagVM071nQ4FtxdCS+Wml5e36ysXqraMVWBv3XGWWI0SiFY1Ol/cewIdZt +elHyZYOxBzBLXqfep2GjEVqhqqkBQLiw1WqDJ6FXlz7Lt3V6AQHtT1QT/lcjD7c4 +Stj25/Xh89tWVV5ApccdDGT83JHN6dI2rTTkNwFS+PuDY1ogjUyM4O52olGWY7Ws +5xmOf+cDJAzGCdF94OY7c+Z9uyso65r+QpGhCsLnC/dKw6YXrPKz4LC4vJ2g2Cc1 +Gb3ZqOCkSt9OHcqLfzaNul7tJx82b7sDdgoTjyVroY5M0lsXEsKh1gFwp3LWiz5N +BHI8sSqwAuQP2R5xPij8Lp/0hasWONZNULURisQKFsswyE+6UTbFoEjkFEMrx/pM +OcWT490JNHRxcb+fb0UGXZ0Wdm9NKIy1L98EHpkPnNDuiaXs/lhpu56LipUAWdiL +4swBcCA= +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/login.live.com.crt b/src/test/resources/certs/login.live.com.crt new file mode 100644 index 0000000..108311d --- /dev/null +++ b/src/test/resources/certs/login.live.com.crt
@@ -0,0 +1,33 @@ +-----BEGIN CERTIFICATE----- +MIIFxzCCBK+gAwIBAgIQWfjHqoYW0XMXfFBcAW44GzANBgkqhkiG9w0BAQUFADCB +ujELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2Ug +YXQgaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykwNjE0MDIGA1UEAxMr +VmVyaVNpZ24gQ2xhc3MgMyBFeHRlbmRlZCBWYWxpZGF0aW9uIFNTTCBDQTAeFw0w +OTA2MTYwMDAwMDBaFw0xMDA2MTYyMzU5NTlaMIIBDzETMBEGCysGAQQBgjc8AgED +EwJVUzEbMBkGCysGAQQBgjc8AgECEwpXYXNoaW5ndG9uMRswGQYDVQQPExJWMS4w +LCBDbGF1c2UgNS4oYikxEjAQBgNVBAUTCTYwMDQxMzQ4NTELMAkGA1UEBhMCVVMx +DjAMBgNVBBEUBTk4MDUyMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHFAdS +ZWRtb25kMRowGAYDVQQJFBFPbmUgTWljcm9zb2Z0IFdheTEeMBwGA1UEChQVTWlj +cm9zb2Z0IENvcnBvcmF0aW9uMREwDwYDVQQLFAhQYXNzcG9ydDEXMBUGA1UEAxQO +bG9naW4ubGl2ZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKuYQtRg ++njn4QEUtC2TkeJj+LWH2rgmkpEWUE4df1j8/eFzKPnJmdsviZA+KLslQp9EALTM +Y6G2wKwDxR9hRz5xmOlvnZB5z4uUfcXYsONETqm7HtWqHzHTyKHJKDv88Vva4eGr +1AgS+M3hzFAaUl/DnpG8sI0Lz2X7dx5xSwSfAgMBAAGjggHzMIIB7zAJBgNVHRME +AjAAMB0GA1UdDgQWBBQxrvF8mGfpHxlpoqeEHmdcqsNrdTALBgNVHQ8EBAMCBaAw +QgYDVR0fBDswOTA3oDWgM4YxaHR0cDovL0VWU2VjdXJlLWNybC52ZXJpc2lnbi5j +b20vRVZTZWN1cmUyMDA2LmNybDBEBgNVHSAEPTA7MDkGC2CGSAGG+EUBBxcGMCow +KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LnZlcmlzaWduLmNvbS9ycGEwHQYDVR0l +BBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB8GA1UdIwQYMBaAFPyKULqeuSVae1WF +T5UAY4/pWGtDMHwGCCsGAQUFBwEBBHAwbjAtBggrBgEFBQcwAYYhaHR0cDovL0VW +U2VjdXJlLW9jc3AudmVyaXNpZ24uY29tMD0GCCsGAQUFBzAChjFodHRwOi8vRVZT +ZWN1cmUtYWlhLnZlcmlzaWduLmNvbS9FVlNlY3VyZTIwMDYuY2VyMG4GCCsGAQUF +BwEMBGIwYKFeoFwwWjBYMFYWCWltYWdlL2dpZjAhMB8wBwYFKw4DAhoEFEtruSiW +Bgy70FI4mymsSweLIQUYMCYWJGh0dHA6Ly9sb2dvLnZlcmlzaWduLmNvbS92c2xv +Z28xLmdpZjANBgkqhkiG9w0BAQUFAAOCAQEAdjzxBfnaVmlbOWci+BnXY9hZZqSK +sqk0CrsdBQXot+PcPpzfi4cdUc8/8Pr6SvLAqi/koHc6ez0SxK1QycfuwAUVCKsd +/np79v2K94UHuKb+NTzHUStBg3KoSe7/mK3gP1CSSpgr9u+wHjQhjdIYrozHJD2C +T/cjX4A1EZy5ChV719L7UiganmT6jOY2+ogP3ppd1WgZHnCGHciUaiR6kjw9p6UD +jM19Lg3DHnw9zxjFHTj+MFc9qLBb3m7QEdEOO1hwQdWYS08bjEXvuuISO0tEr28X +5ir3zGxe66Ge0Koi2AM1y9gq8+bOVqDudU2poyIXfFXcQ13HRNtgvnH3fQ== +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/multi-value-rdn-1.crt b/src/test/resources/certs/multi-value-rdn-1.crt new file mode 100644 index 0000000..1c510c2 --- /dev/null +++ b/src/test/resources/certs/multi-value-rdn-1.crt
@@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC2zCCAkSgAwIBAgIDAVJ9MA0GCSqGSIb3DQEBBQUAMFcxEzARBgoJkiaJk/Is +ZAEZFgNvcmcxGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2ZTESMBAGA1UEAxMJYS5m +b28uY29tMRIwEAYDVQQDEwliLmZvby5jb20wHhcNMTQwODI5MTk1MTE5WhcNMTQw +OTI4MTk1MTE5WjBXMRMwEQYKCZImiZPyLGQBGRYDb3JnMRgwFgYKCZImiZPyLGQB +GRYIbGRhcHRpdmUxEjAQBgNVBAMTCWEuZm9vLmNvbTESMBAGA1UEAxMJYi5mb28u +Y29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrFV0ARzYvBJLXMLo8yex7 +aNATrAANh4S3utE/ce+xj2qTi+hl9xm0EU6Zal+iYGpsKqnpTPfNE8HVMbzOrrPB +6fRMGS1AyRV3WOy+2mgdzi1P068PqpTkm+MjXF6El8OBnuGaIwLzvFMno0rV7lse +UOLDYcEIl3BdVsIlH27KpQIDAQABo4G0MIGxMB0GA1UdDgQWBBSHRs4AN3PGdL/i +OkPq/Cjjc6f8EDCBgQYDVR0jBHoweIAUh0bOADdzxnS/4jpD6vwo43On/BChW6RZ +MFcxEzARBgoJkiaJk/IsZAEZFgNvcmcxGDAWBgoJkiaJk/IsZAEZFghsZGFwdGl2 +ZTESMBAGA1UEAxMJYS5mb28uY29tMRIwEAYDVQQDEwliLmZvby5jb22CAwFSfTAM +BgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAC++ms/hrIOiY4Gdyie8qiIW +FAU/IZLkbFSPwFpVQrYLdN7m+xCIcq2+viaZdXG6QYOC8dYr2URoEoVm+DPfx2Hj +TokXEIsNS7ODx8r/sBmJ2UHvRdPROtqwY4tCgYlf7LWD/s27eRVYCTZbcwMF1hBf +aNe1VTBZ5MLkzyewZ6tW +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/multi-value-rdn-2.crt b/src/test/resources/certs/multi-value-rdn-2.crt new file mode 100644 index 0000000..29e71f5 --- /dev/null +++ b/src/test/resources/certs/multi-value-rdn-2.crt
@@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC1DCCAj2gAwIBAgIDAVJ9MA0GCSqGSIb3DQEBBQUAMFUxJDAQBgNVBAMTCWEu +Zm9vLmNvbTAQBgNVBAMTCWIuZm9vLmNvbTEYMBYGCgmSJomT8ixkARkWCGxkYXB0 +aXZlMRMwEQYKCZImiZPyLGQBGRYDb3JnMB4XDTE0MDgyOTE5MjY1OVoXDTE0MDky +ODE5MjY1OVowVTEkMBAGA1UEAxMJYS5mb28uY29tMBAGA1UEAxMJYi5mb28uY29t +MRgwFgYKCZImiZPyLGQBGRYIbGRhcHRpdmUxEzARBgoJkiaJk/IsZAEZFgNvcmcw +gZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOC2KBN8MDiKHWEuv1pnIEcWYjHb +D+NgAGVnZh7i8jEDRIVpWUzFj7FxNROEsAitZAanzpwo6jYeGmT60Vl4DpliuoVu +Vt1Reem96Dp9/J7BL0QBv0fJErv/YRhNor4wSOuWI96TWHvCDEL4oDNuxEK46Nsn +dAw10DFBRMWt1VcFAgMBAAGjgbEwga4wHQYDVR0OBBYEFB7EPqv9y/GqxBrJAMS4 +pEh2ktyTMH8GA1UdIwR4MHaAFB7EPqv9y/GqxBrJAMS4pEh2ktyToVmkVzBVMSQw +EAYDVQQDEwlhLmZvby5jb20wEAYDVQQDEwliLmZvby5jb20xGDAWBgoJkiaJk/Is +ZAEZFghsZGFwdGl2ZTETMBEGCgmSJomT8ixkARkWA29yZ4IDAVJ9MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADgYEALU4SluqREjvyztZDZRsVnKn0Wy5kQqh3 +wVN/U2Sv82+N6ulzqOttmEY/dq8UGH5QbIioGUTgWxycidYwzWCIT/+Gg+pwBcmz +oTYxJY0aUKfvfy4p25dcaG360DMycUpmZHM+HpgEGOrMsLCewKshuR+D03pE9eH5 +AK1FbieXQtM= +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/needs-escaping-1.crt b/src/test/resources/certs/needs-escaping-1.crt new file mode 100644 index 0000000..0e92a3f --- /dev/null +++ b/src/test/resources/certs/needs-escaping-1.crt
@@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDjTCCAvagAwIBAgIJAJ9OE8QRbtsRMA0GCSqGSIb3DQEBBQUAMIGMMRMwEQYK +CZImiZPyLGQBGRYDZWR1MRIwEAYKCZImiZPyLGQBGRYCdnQxCzAJBgNVBAYTAlVT +MREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxhY2tzYnVyZzEPMA0GA1UE +ChQGVlBJJlNVMRswGQYDVQQDExJEQz1leGFtcGxlLCBEQz1jb20wHhcNMTQxMDI5 +MTE0MzUzWhcNNDIwMzE2MTE0MzUzWjCBjDETMBEGCgmSJomT8ixkARkWA2VkdTES +MBAGCgmSJomT8ixkARkWAnZ0MQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2lu +aWExEzARBgNVBAcTCkJsYWNrc2J1cmcxDzANBgNVBAoUBlZQSSZTVTEbMBkGA1UE +AxMSREM9ZXhhbXBsZSwgREM9Y29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB +gQDkYYrsisNd/+lNvWR3ridkbPg0VJKc9juQAnSrdh3rV99wBtg0SQeao5MUPwsm +LG3/aQmzNhaFNPeWHcQZDmqZO1QLTB7rFX2xZn3t4uD2+yn764lnL10qliZmGCbs +6wB4LLrnR50hWYx3MgisdeHe4uOc18FVGvGs8+2k0Im/yQIDAQABo4H0MIHxMB0G +A1UdDgQWBBQVpddz37M6vpI2uk5lLDByhWTNxTCBwQYDVR0jBIG5MIG2gBQVpddz +37M6vpI2uk5lLDByhWTNxaGBkqSBjzCBjDETMBEGCgmSJomT8ixkARkWA2VkdTES +MBAGCgmSJomT8ixkARkWAnZ0MQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2lu +aWExEzARBgNVBAcTCkJsYWNrc2J1cmcxDzANBgNVBAoUBlZQSSZTVTEbMBkGA1UE +AxMSREM9ZXhhbXBsZSwgREM9Y29tggkAn04TxBFu2xEwDAYDVR0TBAUwAwEB/zAN +BgkqhkiG9w0BAQUFAAOBgQDDyLykXoVgJroG5iSbebWrZALT0XzEN/gfxYxAYEh7 +YUrjM3GUHpF+dKroh/lreWn+fbO1oALuqSODnDmYPIYGeemS22P798ab8ar7ltNt +pVGjaQk5w+GPvXA1LTAFWlb/2kHQAFEE0+oKz6PEZxtJy0StDQvfc4bQ3dW7ddOC +Kg== +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/needs-escaping-2.crt b/src/test/resources/certs/needs-escaping-2.crt new file mode 100644 index 0000000..898ec8d --- /dev/null +++ b/src/test/resources/certs/needs-escaping-2.crt
@@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDcjCCAtugAwIBAgIJALpZTTXEfhcxMA0GCSqGSIb3DQEBBQUAMIGDMRMwEQYK +CZImiZPyLGQBGRYDZWR1MRIwEAYKCZImiZPyLGQBGRYCdnQxCzAJBgNVBAYTAlVT +MREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxhY2tzYnVyZzEPMA0GA1UE +ChQGVlBJJlNVMRIwEAYDVQQDFAkjREVBREJFRUYwHhcNMTQxMDI5MTMxNDIxWhcN +NDIwMzE2MTMxNDIxWjCBgzETMBEGCgmSJomT8ixkARkWA2VkdTESMBAGCgmSJomT +8ixkARkWAnZ0MQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExEzARBgNV +BAcTCkJsYWNrc2J1cmcxDzANBgNVBAoUBlZQSSZTVTESMBAGA1UEAxQJI0RFQURC +RUVGMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkYYrsisNd/+lNvWR3ridk +bPg0VJKc9juQAnSrdh3rV99wBtg0SQeao5MUPwsmLG3/aQmzNhaFNPeWHcQZDmqZ +O1QLTB7rFX2xZn3t4uD2+yn764lnL10qliZmGCbs6wB4LLrnR50hWYx3MgisdeHe +4uOc18FVGvGs8+2k0Im/yQIDAQABo4HrMIHoMB0GA1UdDgQWBBQVpddz37M6vpI2 +uk5lLDByhWTNxTCBuAYDVR0jBIGwMIGtgBQVpddz37M6vpI2uk5lLDByhWTNxaGB +iaSBhjCBgzETMBEGCgmSJomT8ixkARkWA2VkdTESMBAGCgmSJomT8ixkARkWAnZ0 +MQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExEzARBgNVBAcTCkJsYWNr +c2J1cmcxDzANBgNVBAoUBlZQSSZTVTESMBAGA1UEAxQJI0RFQURCRUVGggkAullN +NcR+FzEwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQAOxmoFF9gRDiuA +l+z0/stcxChGAkdKYGXL62U/L6POvwxLUnpvWgBX21KcIub2wICCMo/JA81s2QFk +LwaxBpXp6awCN0H+H5HH60hr9Q3optPxFxzeAb8L/TlP4ClcsnKrpY9Ge69Hp8GD +yqBVXG9iHAz3RpE10pJA4f/NFD7ZLQ== +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/needs-escaping-3.crt b/src/test/resources/certs/needs-escaping-3.crt new file mode 100644 index 0000000..151247a --- /dev/null +++ b/src/test/resources/certs/needs-escaping-3.crt
@@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDaTCCAtKgAwIBAgIJALRAX7xeRlE+MA0GCSqGSIb3DQEBBQUAMIGAMRMwEQYK +CZImiZPyLGQBGRYDZWR1MRIwEAYKCZImiZPyLGQBGRYCdnQxCzAJBgNVBAYTAlVT +MREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxhY2tzYnVyZzEPMA0GA1UE +ChQGVlBJJlNVMQ8wDQYDVQQDEwYgc3BhY2UwHhcNMTQxMDI5MTMxNjM4WhcNNDIw +MzE2MTMxNjM4WjCBgDETMBEGCgmSJomT8ixkARkWA2VkdTESMBAGCgmSJomT8ixk +ARkWAnZ0MQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExEzARBgNVBAcT +CkJsYWNrc2J1cmcxDzANBgNVBAoUBlZQSSZTVTEPMA0GA1UEAxMGIHNwYWNlMIGf +MA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkYYrsisNd/+lNvWR3ridkbPg0VJKc +9juQAnSrdh3rV99wBtg0SQeao5MUPwsmLG3/aQmzNhaFNPeWHcQZDmqZO1QLTB7r +FX2xZn3t4uD2+yn764lnL10qliZmGCbs6wB4LLrnR50hWYx3MgisdeHe4uOc18FV +GvGs8+2k0Im/yQIDAQABo4HoMIHlMB0GA1UdDgQWBBQVpddz37M6vpI2uk5lLDBy +hWTNxTCBtQYDVR0jBIGtMIGqgBQVpddz37M6vpI2uk5lLDByhWTNxaGBhqSBgzCB +gDETMBEGCgmSJomT8ixkARkWA2VkdTESMBAGCgmSJomT8ixkARkWAnZ0MQswCQYD +VQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExEzARBgNVBAcTCkJsYWNrc2J1cmcx +DzANBgNVBAoUBlZQSSZTVTEPMA0GA1UEAxMGIHNwYWNlggkAtEBfvF5GUT4wDAYD +VR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQB14f+JlFDdZPIxYpNXGCDNaemw +zPDUs7YDzXAfVgkbw/mt36155d7jXnmzkPnDZbCeIc/CGmgOURmY5WOS5m1eVywu +cliBoYA5xQBfNUVMa+68wvLzY7jQbGyKm9CFIwNjNMhDF5uenpyv/DxwvMd7Wneq +XluBRl3MN4DV9aULOg== +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/needs-escaping-4.crt b/src/test/resources/certs/needs-escaping-4.crt new file mode 100644 index 0000000..533c191 --- /dev/null +++ b/src/test/resources/certs/needs-escaping-4.crt
@@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbzCCAtigAwIBAgIJALu4vu81OOfsMA0GCSqGSIb3DQEBBQUAMIGCMRMwEQYK +CZImiZPyLGQBGRYDZWR1MRIwEAYKCZImiZPyLGQBGRYCdnQxCzAJBgNVBAYTAlVT +MREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxhY2tzYnVyZzEPMA0GA1UE +ChQGVlBJJlNVMREwDwYDVQQDEwhzcGFjZTIgIDAeFw0xNDEwMjkxMzE3MzJaFw00 +MjAzMTYxMzE3MzJaMIGCMRMwEQYKCZImiZPyLGQBGRYDZWR1MRIwEAYKCZImiZPy +LGQBGRYCdnQxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UE +BxMKQmxhY2tzYnVyZzEPMA0GA1UEChQGVlBJJlNVMREwDwYDVQQDEwhzcGFjZTIg +IDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA5GGK7IrDXf/pTb1kd64nZGz4 +NFSSnPY7kAJ0q3Yd61ffcAbYNEkHmqOTFD8LJixt/2kJszYWhTT3lh3EGQ5qmTtU +C0we6xV9sWZ97eLg9vsp++uJZy9dKpYmZhgm7OsAeCy650edIVmMdzIIrHXh3uLj +nNfBVRrxrPPtpNCJv8kCAwEAAaOB6jCB5zAdBgNVHQ4EFgQUFaXXc9+zOr6SNrpO +ZSwwcoVkzcUwgbcGA1UdIwSBrzCBrIAUFaXXc9+zOr6SNrpOZSwwcoVkzcWhgYik +gYUwgYIxEzARBgoJkiaJk/IsZAEZFgNlZHUxEjAQBgoJkiaJk/IsZAEZFgJ2dDEL +MAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYDVQQHEwpCbGFja3Ni +dXJnMQ8wDQYDVQQKFAZWUEkmU1UxETAPBgNVBAMTCHNwYWNlMiAgggkAu7i+7zU4 +5+wwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQUFAAOBgQC5fERd6NiAzDWY/Rlc +YETf6pHrWrGsnB29Mj/PNuSj04pEJ00/LuVEzV56q7vFjjrPh7PkaOH7dAedtDRZ +g4YEJY4hS8WZa5NavBtDjidQUQknoL6vQDi4rHEAXoJF/Ol2Zb+XCJCPyJjoR+Ae +28EAHhfJKxysA9t16aI7UN/MqA== +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/scantor-dn-description.crt b/src/test/resources/certs/scantor-dn-description.crt new file mode 100644 index 0000000..ee771bd --- /dev/null +++ b/src/test/resources/certs/scantor-dn-description.crt
@@ -0,0 +1,36 @@ +-----BEGIN CERTIFICATE----- +MIIGOTCCBSGgAwIBAgIDC9q0MA0GCSqGSIb3DQEBBQUAMIGMMQswCQYDVQQGEwJJ +TDEWMBQGA1UEChMNU3RhcnRDb20gTHRkLjErMCkGA1UECxMiU2VjdXJlIERpZ2l0 +YWwgQ2VydGlmaWNhdGUgU2lnbmluZzE4MDYGA1UEAxMvU3RhcnRDb20gQ2xhc3Mg +MSBQcmltYXJ5IEludGVybWVkaWF0ZSBDbGllbnQgQ0EwHhcNMTQxMTEwMDI1MzE2 +WhcNMTUxMTExMTM0ODI4WjBXMRkwFwYDVQQNExA2TXRwSlMxZGNDN3QyNTR2MRkw +FwYDVQQDDBBjYW50b3IuMkBvc3UuZWR1MR8wHQYJKoZIhvcNAQkBFhBjYW50b3Iu +MkBvc3UuZWR1MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyIP4AzEq +67t3w/K3qKRp+8DYzPUFCA2tkuxBHEZRGeOK2xx2mZm9anw0lO1ihNm2Td6xaOnP +RKeIdl2eMX7AEzVazMaDYAB8n+qxQLC5+2R98wrqsp685YjE0siczBDTKkReXtea +yhxP6vuJTtnAfSf7caa+U5RqWxfPf9bGAb378P9eNQ4xXpaR3VjVBL+Bnh5pSYzT +bxj2fpGzNUAh5pi/QD6pxZ+cXyc3SB2hmX1QoFPDlEjuXBrOJKW2rBJ/58Mah+gS +V0cCt9YVgeONNytpr4IbjcdZj356VJav07gfrhwdlIDOhFsrq/1aHvwttUeaUQ+E +AWuRwR5XfOrU6QIDAQABo4IC1jCCAtIwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAw +HQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMEMB0GA1UdDgQWBBTE8bu/7pUs +M37CLGlLPPfH/tTtcDAfBgNVHSMEGDAWgBRTcu2SnODaywFcfH6WNU7y1LhRgjAb +BgNVHREEFDASgRBjYW50b3IuMkBvc3UuZWR1MIIBTAYDVR0gBIIBQzCCAT8wggE7 +BgsrBgEEAYG1NwECAzCCASowLgYIKwYBBQUHAgEWImh0dHA6Ly93d3cuc3RhcnRz +c2wuY29tL3BvbGljeS5wZGYwgfcGCCsGAQUFBwICMIHqMCcWIFN0YXJ0Q29tIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MAMCAQEagb5UaGlzIGNlcnRpZmljYXRlIHdh +cyBpc3N1ZWQgYWNjb3JkaW5nIHRvIHRoZSBDbGFzcyAxIFZhbGlkYXRpb24gcmVx +dWlyZW1lbnRzIG9mIHRoZSBTdGFydENvbSBDQSBwb2xpY3ksIHJlbGlhbmNlIG9u +bHkgZm9yIHRoZSBpbnRlbmRlZCBwdXJwb3NlIGluIGNvbXBsaWFuY2Ugb2YgdGhl +IHJlbHlpbmcgcGFydHkgb2JsaWdhdGlvbnMuMDYGA1UdHwQvMC0wK6ApoCeGJWh0 +dHA6Ly9jcmwuc3RhcnRzc2wuY29tL2NydHUxLWNybC5jcmwwgY4GCCsGAQUFBwEB +BIGBMH8wOQYIKwYBBQUHMAGGLWh0dHA6Ly9vY3NwLnN0YXJ0c3NsLmNvbS9zdWIv +Y2xhc3MxL2NsaWVudC9jYTBCBggrBgEFBQcwAoY2aHR0cDovL2FpYS5zdGFydHNz +bC5jb20vY2VydHMvc3ViLmNsYXNzMS5jbGllbnQuY2EuY3J0MCMGA1UdEgQcMBqG +GGh0dHA6Ly93d3cuc3RhcnRzc2wuY29tLzANBgkqhkiG9w0BAQUFAAOCAQEAnk+/ +4X15nDhpqiLPkfH0jg4QMkWsniANJtfPoLcbjlhpTxRgIpEHlqY2hdw/gwcMDL0y +CjCs2/yhZINITU6YmOnOkalXqKkgy7RxGRYIpjb5XYJbQv+RR+qpzl5S38HQzUDu ++Z3r83lYrZFM5scqvl2bzK+2jUchQoM3iClr4ZmmgdJyBM5SEVgHjeQOQI5p7NhO +B4eLZ5USBFLK5gVQEkqJjAxZTz72uCHsjPaoiyraZnuZyrct75g/adSfyoJvXuHw +rd/RrbJJcx96Eqf1v26StKcU0eXTc7hIkSshNef3LSKqPbBOojp0JaPJ0AIyvQcs +rZoJ0wA8Ifva+t2QvA== +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/serac-dev-test.crt b/src/test/resources/certs/serac-dev-test.crt new file mode 100644 index 0000000..c2cbb6c --- /dev/null +++ b/src/test/resources/certs/serac-dev-test.crt
@@ -0,0 +1,117 @@ +Bag Attributes + localKeyID: 01 00 00 00 +subject=/UID=1145718/CN=Marvin S Addison/O=Virginia Polytechnic Institute and State University/DC=edu/DC=vt/C=US +issuer=/DC=edu/DC=vt/C=US/O=Virginia Polytechnic Institute and State University/CN=DEV Virginia Tech Class 1 Server CA/serialNumber=12 +-----BEGIN CERTIFICATE----- +MIIFfzCCA2egAwIBAgIIL1b8rywTgSMwDQYJKoZIhvcNAQEFBQAwga8xEzARBgoJ +kiaJk/IsZAEZFgNlZHUxEjAQBgoJkiaJk/IsZAEZFgJ2dDELMAkGA1UEBhMCVVMx +PDA6BgNVBAoTM1ZpcmdpbmlhIFBvbHl0ZWNobmljIEluc3RpdHV0ZSBhbmQgU3Rh +dGUgVW5pdmVyc2l0eTEsMCoGA1UEAxMjREVWIFZpcmdpbmlhIFRlY2ggQ2xhc3Mg +MSBTZXJ2ZXIgQ0ExCzAJBgNVBAUTAjEyMB4XDTA5MDEyNzE0MzkwMloXDTExMDEy +NzE0MzkwMlowgagxFzAVBgoJkiaJk/IsZAEBDAcxMTQ1NzE4MRkwFwYDVQQDDBBN +YXJ2aW4gUyBBZGRpc29uMTwwOgYDVQQKDDNWaXJnaW5pYSBQb2x5dGVjaG5pYyBJ +bnN0aXR1dGUgYW5kIFN0YXRlIFVuaXZlcnNpdHkxEzARBgoJkiaJk/IsZAEZFgNl +ZHUxEjAQBgoJkiaJk/IsZAEZFgJ2dDELMAkGA1UEBhMCVVMwgZ8wDQYJKoZIhvcN +AQEBBQADgY0AMIGJAoGBAJqNBYMDpUcDTAI9Srrj473GzqUPB2GFyJMNUy8ebWPE +iwsMJlLZRHpC2igDRvDuS/70Jm5YpuVdqr5rnotCgVpczGCP8mE0QBziktLTuLdf +Vzbu06/F77GHGUkSJ5TNL0iUynk+zxVvyZA5nAZVpaPqCkKG1xs+Y1Mm34F4EA5t +AgMBAAGjggEmMIIBIjAdBgNVHQ4EFgQUJUgvKOxdGbsdJa6Uk7F7tTWWJGYwCQYD +VR0TBAIwADAfBgNVHSMEGDAWgBQ44G+uSO1eI/Yimx7nnBkWR7h+kjCBgwYDVR0g +BHwwejAOBgwrBgEEAbRoBQICAgEwDgYMKwYBBAG0aAUCAgEBMEgGDCsGAQQBtGgF +AgIEATA4MDYGCCsGAQUFBwIBFipodHRwOi8vd3d3LnBraS52dC5lZHUvdnR1Y2Ev +Y3BzL2luZGV4Lmh0bWwwDgYMKwYBBAG0aAUCAgMBMAsGA1UdDwQEAwIGwDApBgNV +HSUEIjAgBggrBgEFBQcDAgYIKwYBBQUHAwQGCisGAQQBgjcUAgIwFwYDVR0RBBAw +DoEMZXByb3ZAdnQuZWR1MA0GCSqGSIb3DQEBBQUAA4ICAQBzUOeoIl4dN89hx5c1 +rCQ0zxRn1y3MGWvTLuJnAItnruTbqZWXlMjlGMlW7PY4ZacLcsDC8O2dUYojXiDr +vjxykMNf+upCRUpKdlwQNeE7NaB99GnZCK2tavVVXF8B1hfoucvLghN9jPtI0mtI +NoSVpUAaU81fTgkkEDP4P8TeBFokj7ygahRGBq6+6ogXQovZqn1jqH+GG/rUUO21 +flHw0fQ4MsRWOPXSkiubEn/JbRXsN4dznaoxpAsudl8r3qCf+A3ka6Q1P1ITQxLS +aL6dP5GdipBB1Wq+cx/wZ4W2ZdAj9sFY1M8WXDo92i1KsobaRXtbuKgJMTSS8I54 +sRdpNzTxVnfrm5ZKm3IoLIqGbaV7HBs26qj+LMbb9XaDVnubRp+cLD4moTcEbEFI +jOmykuTsfvZryfZF3Db82GZ0lrLdVr+fnc16PY/943cX7ZhjBUPTjlklwzQs7/VK +6tAlzkfk6Qi0yJlZXGLp2+KZP0eTICB6h2/8udoqI6t6fVxm92JqWNfvFURt2m9t +ayrNXSbTXq/KY2DcrMoYk3slqMpGnKlD+iIJQsbjOMmK8a1BMgEAZEDhzCabtQt6 +TtJq/v9XFvRPzDiHHmAMkbrJLA0DzpIKICdtovVsskPGov5zv7MalSFJ3+1Cwi0w +phTkNox9q4S8XAaRUPS7swqefQ== +-----END CERTIFICATE----- +Bag Attributes: <Empty Attributes> +subject=/C=US/ST=Virginia /L=Blacksburg/O=DEV Virginia Tech Root CA +issuer=/C=US/ST=Virginia /L=Blacksburg/O=DEV Virginia Tech Root CA +-----BEGIN CERTIFICATE----- +MIIGQzCCBCugAwIBAgIBADANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJVUzES +MBAGA1UECBMJVmlyZ2luaWEgMRMwEQYDVQQHEwpCbGFja3NidXJnMSIwIAYDVQQK +ExlERVYgVmlyZ2luaWEgVGVjaCBSb290IENBMB4XDTAzMDgyNzE5Mzk0M1oXDTMz +MDgxOTE5Mzk0M1owWjELMAkGA1UEBhMCVVMxEjAQBgNVBAgTCVZpcmdpbmlhIDET +MBEGA1UEBxMKQmxhY2tzYnVyZzEiMCAGA1UEChMZREVWIFZpcmdpbmlhIFRlY2gg +Um9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANnl4lsx9H3P +ktcm1B6b4nRaRZ1MiBjr3BVLH/GQbhC7rS9hq5B03j9LbN0/ld1aWa+ot6gibBvF +cCGweAHkU878CVTEUr/CrvesY0FOggaj7ucCWPOxCxv6Dob+FT1RLO1Bd7YUVLN7 +WlMHwi/KX4ADytbNyu7DBDG9PQE4ZmI/9JKBIjpiY6EfjDDIMyRfqK9Gz1mpsVlo +kYJBZDa37bNsZ+tIjdm61n2ramdjQxnH9DN1BPvQsm1IoRFD2JeVmYq6jJcmV+Wy +Gy1U4WNUe2MxX/y5IEhUmerOnJs1tz7e9qoPJ5MqwSC/ujZ+J/3NmrZK1kc/o3AF +5dZSejFZBIL2hdXVzFeblBYgwMYiLEqdykYo5Y/fFPe6k9VVDNAMraJsFpR7/2ks +fENyS6bTZgX3bHeq2WJiQ92BDC5F32QZKFsCArdP8Cdy2J1O2BnFjVB9s3vlLy6X +sPjtDtdyCsbuxYHRZT2wWDKKDcIrcSs+3fai5j0MpKPGdwjoK/uxBqjcwAai5XAW +NG9ZY2K1+l0oLcw3exi3RUXLLWCu4KjIwQXh0dA1O0hSp+iHaCgW4S7s7EA37zG5 +/dOpNVFyZlJB4zOYxmrKt1XtOubOZ+GubeuQiS2xjvZKgyhulV6hdORO7VAgTgKA +NMbKnbGCXMW7nU4InkXnPtD0X7j6/HnvAgMBAAGjggESMIIBDjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTwC6Mn1xo3SrY1FN+8p796K1cdCzCBggYDVR0jBHsw +eYAU8AujJ9caN0q2NRTfvKe/eitXHQuhXqRcMFoxCzAJBgNVBAYTAlVTMRIwEAYD +VQQIEwlWaXJnaW5pYSAxEzARBgNVBAcTCkJsYWNrc2J1cmcxIjAgBgNVBAoTGURF +ViBWaXJnaW5pYSBUZWNoIFJvb3QgQ0GCAQAwCwYDVR0PBAQDAgEGMEoGA1UdIARD +MEEwPwYMKwYBBAG0aAUCAQEBMC8wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucGtp +LnZ0LmVkdS9yb290Y2EvY3BzLzANBgkqhkiG9w0BAQUFAAOCAgEAE1YKsusecsvV +igS4vIlxuSVjxNVA2nyr25G6+jW5ZGwn4osLGh3wRGi6omPWRufctmXcj1K47Scl +/wdUmYgprKNlQ3ZwnC03aDFrMBePdT3Iyc6tdS4s4WTH+YxUvHDOht5hGd8yjOEP +1Kh3Iv+WKA3Y4Lt3k5dvYVyz/5Z1vnGFZ/0WU6ICz8rPewKHB0oGiGR2vuJ6g/pH +QgSeR0bcUEubo3rBEb0E5GOGrjG4JIzZfH08tq82dGRfxODRgAZV8mMLfbkUluBV +ilafwXHLWSgD0CooqmnIDnQJrmsZQssG5wjI2cclwdRZJvVB+Gxy9W7LvLQcRPvf +8ojL6BCC7qkd3zXCdRcpBITTKf1xz1JcUmit2NJHFhmgv1JWwsmGRjGRCjtgLhkG +4fEKP8eY8WeiOGsRzmcZzxe8jWjeUO6ghF3T2R1vdcMCUdn21DO/l62lhR2qC+Ji +EDQZvciLZbltTxc0kbHNLXiET1wMHrq50WVM6F6pG7OzEwjXiQPXTPZqRU54ZCuA +x3IIJ7pJTzmYGQSISn8QEFDJMmeX8/lWdUJuCAUf1ERO958VD6bSvgK1TasPHtzb +xdPfyWvXAvetCNPJLBQAWs4V8SKx71brRn5/ZXBCMd8tqdtwhUyDQqlPpe7kBeGA +STDp+xbGH4LUxg/5gUXPiYrqaVCM7GA= +-----END CERTIFICATE----- +Bag Attributes: <Empty Attributes> +subject=/DC=edu/DC=vt/C=US/O=Virginia Polytechnic Institute and State University/CN=DEV Virginia Tech Class 1 Server CA/serialNumber=12 +issuer=/C=US/ST=Virginia /L=Blacksburg/O=DEV Virginia Tech Root CA +-----BEGIN CERTIFICATE----- +MIIG1DCCBLygAwIBAgIBDDANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJVUzES +MBAGA1UECBMJVmlyZ2luaWEgMRMwEQYDVQQHEwpCbGFja3NidXJnMSIwIAYDVQQK +ExlERVYgVmlyZ2luaWEgVGVjaCBSb290IENBMB4XDTA1MDEwMzE0Mjg0MVoXDTE1 +MDEwMTE0Mjg0MVowga8xEzARBgoJkiaJk/IsZAEZFgNlZHUxEjAQBgoJkiaJk/Is +ZAEZFgJ2dDELMAkGA1UEBhMCVVMxPDA6BgNVBAoTM1ZpcmdpbmlhIFBvbHl0ZWNo +bmljIEluc3RpdHV0ZSBhbmQgU3RhdGUgVW5pdmVyc2l0eTEsMCoGA1UEAxMjREVW +IFZpcmdpbmlhIFRlY2ggQ2xhc3MgMSBTZXJ2ZXIgQ0ExCzAJBgNVBAUTAjEyMIIC +IDANBgkqhkiG9w0BAQEFAAOCAg0AMIICCAKCAgEA6Gbe+Q+Lx5uphTbuXVAriztF +OOLU8/MUtidl1O7aifyiliUq7YqYbZ4Ma1twNEYxRSkrO9L+ljihOBPDOlEwgnAl +yXxXIeRobyWxKuDwzC2DggLx1ufmj45iM0TrJ/+XAyo0jgnIxE4cvbFxMgA4AQYP +YcxNmPtMdGSdLS5PgEp2DekA5a3K6dD4G6RxSW1YHfZMAzz7k3Mm30usHOw0qKgJ +Hvdm50wBXZ+Epd+UUOvHvH8IuDyezvW11Ci/skRrvRYV2aYABy5WFZxVPNMUC5lI +gHYuE0Q+gaVp9NJrHi0M/Ks6EFPeMlgKFoevxqluQHzHdOcfgOHD0Hzi5Urqspju +mg+KJ5RzzsohLYsjXO0c0nMyzI3aaviZJ9vux7UhsJC8+sxX1hc/C5QubSi1NCEA +t1GbH4tsWzhZwja++fTEz8cdOQ2M04PdZj2AltqmbKzse0Mn54XKg5h58hyxxx5z +VEURnjV6lYVSxmWlVPviXkYO4i1+/UGmtAgNXdDCajS8PQGHCm4yukbhGFQxiwKQ +xi+UaylVK3djr2+emEjGfHIRC5gLAyI687YExUIRbTM8UUfwirieGlEbEqYK9TQw +8jDKp9S/P+hU/AA42QouM3jso1VV63J27cXZSUMsIcwUov6MIUccj/Vaf1PN9K/h +Dz3ebY66xX9eT/VrXqcCAQOjggFPMIIBSzAPBgNVHRMBAf8EBTADAQH/MAsGA1Ud +DwQEAwIBBjAdBgNVHQ4EFgQUOOBvrkjtXiP2Ipse55wZFke4fpIwgYIGA1UdIwR7 +MHmAFPALoyfXGjdKtjUU37ynv3orVx0LoV6kXDBaMQswCQYDVQQGEwJVUzESMBAG +A1UECBMJVmlyZ2luaWEgMRMwEQYDVQQHEwpCbGFja3NidXJnMSIwIAYDVQQKExlE +RVYgVmlyZ2luaWEgVGVjaCBSb290IENBggEAMDsGA1UdHwQ0MDIwMKAuoCyGKmh0 +dHA6Ly93d3cucGtpLnZ0LmVkdS9yb290Y2EvY3JsL2NhY3JsLmNybDBKBgNVHSAE +QzBBMD8GDCsGAQQBtGgFAgEBATAvMC0GCCsGAQUFBwIBFiFodHRwOi8vd3d3LnBr +aS52dC5lZHUvcm9vdGNhL2Nwcy8wDQYJKoZIhvcNAQEFBQADggIBAIylwmlnOevb +PGL6TuRsHtXfN9Ext+HarJ+MZrH0zoDCpC3yOytNiuazDJA4fwD8Q3Km5OrYF2SL +KAy6AaeTffEB9w4/MoNmr4qNMc0yx4IxcQuGuNq47L/jDCp332WNFkmR4LwmLYfo +nb8O7KulYZxu0dqxY8PeglSbgAc8jff+xx41+V5bhxdMZUT/oSTMv70Xs+ovmqX7 +sBK5mPc4zcz28PmRxf1gQ5a69ouTFg+7xBm6+wluKjC5UItNSkYsUeXBvI6Kjf8X +kjeO9t6eQ85iqR1SxD6KLhIE4eQ/gu3tCNCiUUTonA8kxJyHWtNmMjLs5Q7NtnmB +h3MJo0Bjwf609ztIpAxp7+gRbkjRfLEWAsKyDrYzF2R9EQ06nxGwj05YsEYYXncw +NnFutRNF/+CT0LYhEgGmbtrbnCcA3ZV6HmilsLHcDTPcdD1UyIhsx/qFxXD6Jawb +2MO8wyDuaHy171EXN9elUBLqG7+64mSuphj7fuHRKEf+ntvcao7CZpwyuVO1NA9m +L84JltJ5MuK14Bm/bIJQE/0pduRJtMZkuKoInRuFtnT7vqAt03rb6SaKUwurjzIE +2nns1A0ddGcKQsn+zq0jyWAEPRVnLJTfk/hXjbUqk3Tw++h8r5GtvbdDrRU74r4h +/1+7GR+cUUXCGG7HpuwthXCiXmhA9Vmz +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/test.example.com.crt b/src/test/resources/certs/test.example.com.crt new file mode 100644 index 0000000..a7fb6b2 --- /dev/null +++ b/src/test/resources/certs/test.example.com.crt
@@ -0,0 +1,94 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 1 (0x1) + Signature Algorithm: sha256WithRSAEncryption + Issuer: DC=com, DC=example, C=US, ST=New York, L=New York, O=Snake Oil Unlimited, CN=root.example.com + Validity + Not Before: Dec 4 16:50:56 2013 GMT + Not After : Apr 21 16:50:56 2041 GMT + Subject: C=US, ST=New York, O=Snake Oil Unlimited, CN=test.example.com + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + RSA Public Key: (2048 bit) + Modulus (2048 bit): + 00:dc:9e:9a:3e:fd:99:af:e3:ef:91:40:b1:6f:50: + 4b:d3:7c:04:eb:34:cb:fc:99:2a:6d:fc:fe:74:8b: + 62:0a:90:d0:d3:7f:79:5b:f0:f6:fe:76:3a:ee:9f: + 09:e7:3e:3b:58:a6:dd:90:7f:16:d8:4d:85:64:1d: + cf:44:7f:03:f5:57:74:86:55:c6:c3:d2:77:16:08: + 3f:00:86:b8:b3:8e:12:1e:eb:9b:fe:b3:57:78:01: + 11:be:26:07:54:04:be:fc:f7:ac:42:8c:62:be:12: + 6c:3f:9d:29:5f:43:fd:d8:03:a8:b7:d5:a5:71:1f: + e7:62:5a:39:4c:0b:e1:0f:0e:31:48:d8:38:61:ae: + 48:be:24:f2:f4:35:fd:c7:da:aa:bf:a3:d2:22:46: + 35:c2:f0:0b:38:00:7a:b6:71:ea:ff:10:b0:16:c6: + 96:64:9f:55:8c:fe:cb:c9:a2:9a:79:99:8e:81:0e: + af:0d:92:94:c0:9b:62:7e:55:4a:b1:06:1f:52:f0: + a1:b0:54:c9:1d:b2:99:4b:74:14:80:bb:2e:63:b2: + 24:e4:c7:20:3f:c2:ac:e4:ba:59:67:ec:45:fd:1e: + 12:7d:3c:b5:da:ac:64:23:aa:8f:72:85:b2:29:c7: + 61:f3:f7:d0:3b:54:7a:d4:53:6a:62:13:92:91:66: + 28:75 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Netscape Comment: + OpenSSL Generated Certificate + X509v3 Subject Key Identifier: + 44:88:EF:79:2B:51:0B:16:31:FE:62:C3:2E:D3:26:F6:A8:EF:CD:A3 + X509v3 Authority Key Identifier: + keyid:7D:5A:81:08:93:BB:52:CF:2C:66:CD:D7:C1:47:B3:11:4B:01:43:6C + DirName:/DC=com/DC=example/C=US/ST=New York/L=New York/O=Snake Oil Unlimited/CN=root.example.com + serial:B9:3D:73:80:5C:40:DE:43 + + X509v3 Issuer Alternative Name: + DNS:snake-1.example.com, DNS:snake-2.example.com + Netscape CA Revocation Url: + http://crl.example.com/ca-crl.pem + Signature Algorithm: sha256WithRSAEncryption + a3:f0:4a:d0:e6:2a:b2:f2:ab:4e:68:05:89:9d:d4:95:cc:7d: + 7c:01:39:44:f0:9a:52:18:3c:49:78:3b:1b:95:00:7a:25:f0: + c7:e6:25:a0:41:20:5f:86:8d:e3:c5:59:79:fe:6d:99:81:23: + 40:a7:52:ed:b8:18:dd:f8:37:b9:5b:99:39:c2:6f:0f:8f:4a: + 5f:c3:dd:b4:fd:ca:be:a2:5d:f5:56:8a:d6:f2:cd:ce:e7:92: + 01:0f:4c:9c:a8:63:5a:2a:53:ca:ce:fe:88:87:f1:76:7e:6f: + 0d:d0:55:b3:c2:db:03:13:f2:ea:88:0a:1b:a7:0e:cf:54:a9: + 02:63:fc:1a:0f:94:40:68:46:f5:e2:4a:77:d1:fa:a7:35:d3: + 0e:ba:17:1c:55:08:ca:e4:30:39:0f:c9:39:0b:e6:a7:f9:f9: + 25:2f:8e:0f:88:81:5c:16:04:e0:0f:69:9b:21:87:4f:92:dd: + ed:37:f6:a6:01:5d:7d:af:1d:fb:9f:53:67:2f:d2:8c:10:dd: + d7:fb:16:ea:18:7f:47:28:d0:91:d7:1e:d7:25:a0:ed:0b:8c: + 29:94:d5:a8:43:e8:74:f4:bf:f6:bd:d4:78:fe:c5:bd:5a:87: + 53:27:ad:70:2c:77:61:4c:98:50:c9:c6:db:c8:d6:74:0e:1b: + a9:04:2a:db +-----BEGIN CERTIFICATE----- +MIIFBjCCA+6gAwIBAgIBATANBgkqhkiG9w0BAQsFADCBmjETMBEGCgmSJomT8ixk +ARkWA2NvbTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxCzAJBgNVBAYTAlVTMREw +DwYDVQQIEwhOZXcgWW9yazERMA8GA1UEBxMITmV3IFlvcmsxHDAaBgNVBAoTE1Nu +YWtlIE9pbCBVbmxpbWl0ZWQxGTAXBgNVBAMTEHJvb3QuZXhhbXBsZS5jb20wHhcN +MTMxMjA0MTY1MDU2WhcNNDEwNDIxMTY1MDU2WjBZMQswCQYDVQQGEwJVUzERMA8G +A1UECBMITmV3IFlvcmsxHDAaBgNVBAoTE1NuYWtlIE9pbCBVbmxpbWl0ZWQxGTAX +BgNVBAMTEHRlc3QuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw +ggEKAoIBAQDcnpo+/Zmv4++RQLFvUEvTfATrNMv8mSpt/P50i2IKkNDTf3lb8Pb+ +djrunwnnPjtYpt2QfxbYTYVkHc9EfwP1V3SGVcbD0ncWCD8AhrizjhIe65v+s1d4 +ARG+JgdUBL7896xCjGK+Emw/nSlfQ/3YA6i31aVxH+diWjlMC+EPDjFI2Dhhrki+ +JPL0Nf3H2qq/o9IiRjXC8As4AHq2cer/ELAWxpZkn1WM/svJopp5mY6BDq8NkpTA +m2J+VUqxBh9S8KGwVMkdsplLdBSAuy5jsiTkxyA/wqzkulln7EX9HhJ9PLXarGQj +qo9yhbIpx2Hz99A7VHrUU2piE5KRZih1AgMBAAGjggGVMIIBkTAJBgNVHRMEAjAA +MCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAd +BgNVHQ4EFgQURIjveStRCxYx/mLDLtMm9qjvzaMwgc8GA1UdIwSBxzCBxIAUfVqB +CJO7Us8sZs3XwUezEUsBQ2yhgaCkgZ0wgZoxEzARBgoJkiaJk/IsZAEZFgNjb20x +FzAVBgoJkiaJk/IsZAEZFgdleGFtcGxlMQswCQYDVQQGEwJVUzERMA8GA1UECBMI +TmV3IFlvcmsxETAPBgNVBAcTCE5ldyBZb3JrMRwwGgYDVQQKExNTbmFrZSBPaWwg +VW5saW1pdGVkMRkwFwYDVQQDExByb290LmV4YW1wbGUuY29tggkAuT1zgFxA3kMw +MwYDVR0SBCwwKoITc25ha2UtMS5leGFtcGxlLmNvbYITc25ha2UtMi5leGFtcGxl +LmNvbTAwBglghkgBhvhCAQQEIxYhaHR0cDovL2NybC5leGFtcGxlLmNvbS9jYS1j +cmwucGVtMA0GCSqGSIb3DQEBCwUAA4IBAQCj8ErQ5iqy8qtOaAWJndSVzH18ATlE +8JpSGDxJeDsblQB6JfDH5iWgQSBfho3jxVl5/m2ZgSNAp1LtuBjd+De5W5k5wm8P +j0pfw920/cq+ol31VorW8s3O55IBD0ycqGNaKlPKzv6Ih/F2fm8N0FWzwtsDE/Lq +iAobpw7PVKkCY/waD5RAaEb14kp30fqnNdMOuhccVQjK5DA5D8k5C+an+fklL44P +iIFcFgTgD2mbIYdPkt3tN/amAV19rx37n1NnL9KMEN3X+xbqGH9HKNCR1x7XJaDt +C4wplNWoQ+h09L/2vdR4/sW9WodTJ61wLHdhTJhQycbbyNZ0DhupBCrb +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/thawte-premium-server-ca.crt b/src/test/resources/certs/thawte-premium-server-ca.crt new file mode 100644 index 0000000..51285e3 --- /dev/null +++ b/src/test/resources/certs/thawte-premium-server-ca.crt
@@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJzCCApCgAwIBAgIBATANBgkqhkiG9w0BAQQFADCBzjELMAkGA1UEBhMCWkEx +FTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTESMBAGA1UEBxMJQ2FwZSBUb3duMR0wGwYD +VQQKExRUaGF3dGUgQ29uc3VsdGluZyBjYzEoMCYGA1UECxMfQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcyBEaXZpc2lvbjEhMB8GA1UEAxMYVGhhd3RlIFByZW1pdW0gU2Vy +dmVyIENBMSgwJgYJKoZIhvcNAQkBFhlwcmVtaXVtLXNlcnZlckB0aGF3dGUuY29t +MB4XDTk2MDgwMTAwMDAwMFoXDTIwMTIzMTIzNTk1OVowgc4xCzAJBgNVBAYTAlpB +MRUwEwYDVQQIEwxXZXN0ZXJuIENhcGUxEjAQBgNVBAcTCUNhcGUgVG93bjEdMBsG +A1UEChMUVGhhd3RlIENvbnN1bHRpbmcgY2MxKDAmBgNVBAsTH0NlcnRpZmljYXRp +b24gU2VydmljZXMgRGl2aXNpb24xITAfBgNVBAMTGFRoYXd0ZSBQcmVtaXVtIFNl +cnZlciBDQTEoMCYGCSqGSIb3DQEJARYZcHJlbWl1bS1zZXJ2ZXJAdGhhd3RlLmNv +bTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA0jY2aovXwlue2oFBYo847kkE +VdbQ7xwblRZH7xhINTpS9CtqBo87L+pW46+GjZ4X9560ZXUCTe/LCaIhUdib0GfQ +ug2SBhRz1JPLlyoAnFxODLz6FVL88kRu2hFKbgifLy3j+ao6hnO2RlNYyIkFvYMR +uHM/qgeN9EJN50CdHDcCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG +9w0BAQQFAAOBgQAmSCwWwlj66BZ0DKqqX1Q/8tfJeGBeXm43YyJ3Nn6yF8Q0ufUI +hfzJATj/Tb7yFkJD57taRvvBxhEf8UqwKEbJw8RCfbz6q1lu1bdRiBHjpIUZa4JM +pAwSremkrj/xw0llmozFyD4lt5SZu5IycQfwhl7tUCemDaYj+bvLpgcUQg== +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/unknown-dn-attr.crt b/src/test/resources/certs/unknown-dn-attr.crt new file mode 100644 index 0000000..c3caa2e --- /dev/null +++ b/src/test/resources/certs/unknown-dn-attr.crt
@@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIID5jCCAs6gAwIBAgIJAKezwvQ9w/frMA0GCSqGSIb3DQEBBQUAMFUxETAPBgNV +BAMTCG1hcnppcGFuMRIwEAYEKgMEBRMIbm9uc2Vuc2UxFzAVBgoJkiaJk/IsZAEZ +FgdleGFtcGxlMRMwEQYKCZImiZPyLGQBGRYDb3JnMB4XDTE0MTExOTE2Mzg1MVoX +DTE0MTIxOTE2Mzg1MVowVTERMA8GA1UEAxMIbWFyemlwYW4xEjAQBgQqAwQFEwhu +b25zZW5zZTEXMBUGCgmSJomT8ixkARkWB2V4YW1wbGUxEzARBgoJkiaJk/IsZAEZ +FgNvcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJqxu5wkvp2xIV +iqPYaqPILIojitnspRsPHBe7JjPW+rDHx68RNzkht/uCOTvvDPYvKbf7aZyaFQnT +QBQMaWqX1FkwcMRoVHeJpvi8pS5pyuvcu3kpS3U3aT7t6Joi/qhPBZMc9iu8Pe4j +Icf8CPQUSCMJoSl1llbM0EBxqxTa9vKTsryt1xkVPM0Of1i5RpC4Y9gipSyKffWH +8M0RX2p2iVu79te0GAwDBmKw2lNLkPodDj8yF78CRnJJw07R8kMVoS0Hi91Sx6NE +hbmvc+zijpF2/RFsOjDBh3Im6V6ejnDwFu8+5P/kuL04nL1I0lG5Urv0js3Jlgn3 +6DuYomXfAgMBAAGjgbgwgbUwHQYDVR0OBBYEFDjmj8OUyhybOlxItbtd7dKB61TQ +MIGFBgNVHSMEfjB8gBQ45o/DlMocmzpcSLW7Xe3SgetU0KFZpFcwVTERMA8GA1UE +AxMIbWFyemlwYW4xEjAQBgQqAwQFEwhub25zZW5zZTEXMBUGCgmSJomT8ixkARkW +B2V4YW1wbGUxEzARBgoJkiaJk/IsZAEZFgNvcmeCCQCns8L0PcP36zAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQB7gDp+vpIFNoAACztjrr5UXeRwdHYS +L1J5FepfBcsjvlx1rwDWS7B0GiHxO7RRG4H/NzErRX6Jnoks4qz3pdX4sT358hn+ +7YeSnQ8pnl8WRWqrk968YJtn4GKdVIZcH2szuzJ+cuvsnfSBb9Hn9OpLe7PU8naS +b7xWclvWXS87nWvR4i8Yq3uhBNIl70NON3es7SledzdvySWJ8HpKW2zxdFqAGKOY +VGUROtBKMrjc8Or2A+ocApAVMfm5HpGrVJvipRCenb9VgLTlBDA32lZVnPDapHtP +RhBSt1uuroqcofUod+MHoy2LDtYIch66iz9CGs4gZ9xmvFSdBvsMp07I +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/vtgsca_chain.pem b/src/test/resources/certs/vtgsca_chain.pem new file mode 100644 index 0000000..5d90c5a --- /dev/null +++ b/src/test/resources/certs/vtgsca_chain.pem
@@ -0,0 +1,117 @@ +-----BEGIN CERTIFICATE----- +MIIG3jCCBMagAwIBAgIIPHK7keZq/ZUwDQYJKoZIhvcNAQEFBQAwZTELMAkGA1UE +BhMCVVMxFjAUBgNVBAoTDVZpcmdpbmlhIFRlY2gxFzAVBgNVBAsTDkdsb2JhbCBS +b290IENBMSUwIwYDVQQDExxWaXJnaW5pYSBUZWNoIEdsb2JhbCBSb290IENBMB4X +DTExMDExODE2MzYyMVoXDTIwMTExODEwMDAwMFowaTEnMCUGA1UEAwweVmlyZ2lu +aWEgVGVjaCBHbG9iYWwgU2VydmVyIENBMRkwFwYDVQQLDBBHbG9iYWwgU2VydmVy +IENBMRYwFAYDVQQKDA1WaXJnaW5pYSBUZWNoMQswCQYDVQQGEwJVUzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBALZZbbiCLqUuFETVAoVvKlVqoGq4Ex7l +gWR3lhZrDJo2XN7Q8rE6O9R0u53Tb39UuFiG+rJFgevykZq/bk06xRxtvpo0dzEn +0Xt7e2RjLLWAwDMb6WJjCJ9XvHJZfP6z1XXSpcohQetrUf7oYP2twpOmQbRBqvjz +RkCcz3u5iRUgGn3f75LWkFPjeNEdLoWWF8iTp0QcTbQ8uPiSJFkQMxXkZdTsqUsF +Kq54tEh75Lt5OHLrpcbwe2Rq3J8NYKVp6mGlpVRL2hmbZaEIvZkhj52Am1uPb5lX +h1zpLfwk6vZwuo+Kmsv9sWi331DUEEWagDvtP2AhQUFmfGphMx78me0eMqASOk8I +2FN0EnrXl5p4wGiXw2tK65UKqZUz8wwDgNbbA/UqNqgxC0OFaZwjkMei3pI8p4uA +rgdZDhyL3V6zA1byQP3HAmEdKNXqvAE8j30I4xwdyu3m14jjMzmvTSZcn2HTqESi +6UC5yuDEC8pSmCdUK5VdbbI1s63uQvkzLUxXHhhgiUcOY9PzVLT9edw+ymwoYoVC +IlqrG7i/o+CIy5t7lE7yQ9We8XnR5d7cbtxHgeGe/dfCmzqvZpAxiV8f0bFMdB+c +zdawPVaoL4DySDmVMO7iY8jBtk0vB05xPgOmhFJkkMMp2eVYcNVIzxTqJAVM45Gu +dBq8uNm8xUY9AgMBAAGjggGMMIIBiDBSBggrBgEFBQcBAQRGMEQwQgYIKwYBBQUH +MAKGNmh0dHA6Ly93d3cucGtpLnZ0LmVkdS9nbG9iYWxyb290L2NhY2VydC9nbG9i +YWxyb290LmNydDAdBgNVHQ4EFgQUCyOn9eYxh71E5K/HEMx0mh1H8fYwEgYDVR0T +AQH/BAgwBgEB/wIBADAfBgNVHSMEGDAWgBR4w+1F/pkMoulXrlhSWS/yr8TAYjCB +jAYDVR0gBIGEMIGBMA4GDCsGAQQBtGgFAgICATAOBgwrBgEEAbRoBQICAQEwDgYM +KwYBBAG0aAUCAgQBMD8GDCsGAQQBtGgFAgIFATAvMC0GCCsGAQUFBwIBFiFodHRw +Oi8vd3d3LnBraS52dC5lZHUvcm9vdGNhL2Nwcy8wDgYMKwYBBAG0aAUCAgMBMD8G +A1UdHwQ4MDYwNKAyoDCGLmh0dHA6Ly93d3cucGtpLnZ0LmVkdS9nbG9iYWxyb290 +L2NybC9jYWNybC5jcmwwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBBQUAA4IC +AQBgU3s9/+KRBFdjEUbWcMWUAhEqWWWCkuvSwyZzEoWnkwbisTBwXzvkiiuKPdH6 +BhSu/IL8CFaRyovsjU+Qh/TknYUA8E9NmqHssuf5QA2haHI72mum7MY9CXPq0HNy +tOesWKk6Ci5L59DdwrQ/DZ9kkwzYEzb3noUSOGmiTqwGHF9+hiJdlDPEqq4Az33B +E1HOUH/YLjvBFx6HXFa0ZdqJWJ9sLrX2odTdHmLt9rAOTXCZOvowCud1Vw2BIdGU +qnJo1f3oZmvPH0zkgEzwK3HYWEwz6nrIMlDDN2NqIoyqdAd0+R+3I+N0y9ceh/0A +BAzR4fvTTSEexVr+ZQIU78E8PNt4GuQ5usFaOElrn/sivWXWRj53g+efJlEUcdRD +AZqEZl5YuSRn2GTIFNRaOkASggyGzTTdRinWiqUWfxZu4C5WxNZztmWaUKp2VCvQ +u9Bxsgr8ZTZy/rPNvy985jLbm4n8LB6J590xb/oAzWdp02Ge5PVLJqRLVQjcEUtg +8EPxtXj1h1eKTVDdxkfQGaXbAOLQTyO4IsDmYbpPoAtKTpNBeflZo9qDjUSekY4b +Y/zZpp//4TqVgVvpNLsA+KY4PHWk4iLwgzE6EZtjeLUln+nTck9xlGzwYjJQy0De +bKEd1GaYa6ofxDz/IJU8zu5P0aSi6Bd1iA0jJTl+aF80CQ== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIF3TCCBMWgAwIBAgILBAAAAAABLF5/HXYwDQYJKoZIhvcNAQEFBQAwcTEoMCYG +A1UEAxMfR2xvYmFsU2lnbiBSb290U2lnbiBQYXJ0bmVycyBDQTEdMBsGA1UECxMU +Um9vdFNpZ24gUGFydG5lcnMgQ0ExGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +CzAJBgNVBAYTAkJFMB4XDTEwMTExODEwMDAwMFoXDTIwMTExODEwMDAwMFowZTEL +MAkGA1UEBhMCVVMxFjAUBgNVBAoTDVZpcmdpbmlhIFRlY2gxFzAVBgNVBAsTDkds +b2JhbCBSb290IENBMSUwIwYDVQQDExxWaXJnaW5pYSBUZWNoIEdsb2JhbCBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAot80+Dl5XoWNCsPy +MG0JTJonOCeDwh92+XJ9mza+n+gMCjmXzmqzoDSgI0RwVMPMAizEyRZc044GBvw6 +blQHIlTTAWmxr904ABfgRTPnt5o9gGk55VcezfYZ0zT2I8OPEbPjOyNiJkNhKc7j +ibef9C5DAxqYV2+kcwnx3edqnl5zHRK3EFAvHHoHVvrkfgd92hPqJO4XiIF4LSaX +ZDDwSnHlJfpjJS3Qez2fl2skp6IV08Za3T5wZ8et4bPXpQiNsMTyOtPpGff9Qd2M +wzlecgXYPqa117m2l4RGtapHQ4ei3mjjpbz6yEaT8O1wqeCItsG4roSDyb865LUX +G719T0UZkr8HPyWPtglMURERsc03x/gLOE/1oSnoHU+9z0g6wMyOioBrkXKHdJoL +oZklV3CddVJF+3AbjXUw9MyYGpKonJ6KC2M6OyvzMKpgvo3W3VIr7YdyBtQRsF4Z +bQHSkTi3lce+HmXxGB03/w0oyxSbC31wQaE7lcDzq8TIcEl5w6YrR1KfaqvruO4g +8a+SnzeA2rz8K8ya19Y/0WWK8SsWLzLe/TnNO0sOt5MqynBbjuiQzxyT5nJQiz3O +Enm1yS4ipImrtmINTpMv+/dWHcRkqCjDLe5nhphF3WaE9T2ELsqQUES5mYanEG3/ +1I0hUdxOx1hnsvuxsvjWTCVbJdUCAwEAAaOCAYAwggF8MA4GA1UdDwEB/wQEAwIB +BjASBgNVHRMBAf8ECDAGAQH/AgEBMB0GA1UdDgQWBBR4w+1F/pkMoulXrlhSWS/y +r8TAYjB8BgNVHSAEdTBzMHEGCSsGAQQBoDIBPDBkMDEGCCsGAQUFBwIBFiVodHRw +Oi8vd3d3Lmdsb2JhbHNpZ24uY29tL3JlcG9zaXRvcnkvMC8GCCsGAQUFBwICMCMa +IWh0dHA6Ly93d3cucGtpLnZ0LmVkdS9yb290Y2EvY3BzLzA/BgNVHR8EODA2MDSg +MqAwhi5odHRwOi8vY3JsLmdsb2JhbHNpZ24ubmV0L1Jvb3RTaWduUGFydG5lcnMu +Y3JsMFcGCCsGAQUFBwEBBEswSTBHBggrBgEFBQcwAoY7aHR0cDovL3NlY3VyZS5n +bG9iYWxzaWduLm5ldC9jYWNlcnQvUm9vdFNpZ25QYXJ0bmVycy1SMS5jcnQwHwYD +VR0jBBgwFoAUVoTstXGl52PY21EE1vrm8EhSSc4wDQYJKoZIhvcNAQEFBQADggEB +AHyR8iWGLs9Wzs+vELu8d0O1X0NYSpfupMP/TpkpyuAyFGl52TnVJvi7p46D4U/H +sm7WhEnFm9g24Kv7OCoccS5ST41QZR56AivFXcJh0zXuWpJ8lJGFnvtDrzK2vo9A +whFnZHiq99gfBOZmpgxqi5+/B5nt4c7bX2fD+JQrUATjuyji+32qdKb88otOtmY3 +G2ymLw+t33WdpBi+E3TSsELbDrBoJSJRHxGrArDRXZBdSQyKcOJINH6o3d2gAnyU +idbweTuj7eftXM5VBJpQ/r4TMtfSx8RQOnmftEXWLxVv7xCpy+DegB7Jmz6xgvZ9 +LYeKy8z9lQXkXBBpnStGGsg= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIID5zCCAs+gAwIBAgILBAAAAAABFUtaxacwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw0wMzEyMTYxMzAw +MDBaFw0yODAxMjgxMTAwMDBaMHExKDAmBgNVBAMTH0dsb2JhbFNpZ24gUm9vdFNp +Z24gUGFydG5lcnMgQ0ExHTAbBgNVBAsTFFJvb3RTaWduIFBhcnRuZXJzIENBMRkw +FwYDVQQKExBHbG9iYWxTaWduIG52LXNhMQswCQYDVQQGEwJCRTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL3vMPEw8TSpiWV3TUanjZD9rk+OyigXulnj +qJIKRQMqio/lCVBVUoHwo5Gx2RIqgfbCAxw8gsByzfGnANf1VJwKR+6alUGSjqCt +CT3T66J0rZ8ZIAm2faZeNZ9POWoDtYqtH5Ziaxe5q4dg1V1t2ZLJ0BOu1IjZUKhE +kQSw6kfqX7LtBMHXAXwh+MRxI/xrTGVEM8ONHebSZhxSKUbEBucLNfBZAWYAic+c +43t4qlPi7qw1lef9XddClJXTGm4xVUfX663HTJ9UcYMaF8j5585YAfQ2v64/WZ9l +fEAHXHMgNKISw0n0aEBpHonghek6t5dju0ewOWtBAH71S7h/4yECAwEAAaOBmTCB +ljAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVoTs +tXGl52PY21EE1vrm8EhSSc4wMwYDVR0fBCwwKjAooCagJIYiaHR0cDovL2NybC5n +bG9iYWxzaWduLm5ldC9Sb290LmNybDAfBgNVHSMEGDAWgBRge2YaRQ2XyolQL30E +zTSo//z9SzANBgkqhkiG9w0BAQUFAAOCAQEAzeN0ltyXbKD5p2QRBKst9isZPl/R +xbiw5Ijr9WLDCfJXza7b2o2kUOxkJ9ivz/BE+0DJc1i5G02aZf5bVq/CltFmJyS7 +lToQ+cO3jmPhA1++FLfHUz/C19/6NisbYPNo4713x0CDcqpOhWu7M8CcdF7gu3JT +66Ra4nqoWPZJugW97gBm8cThETDunuYnd6I2fA3FoNdMhs1CdiVkOy1xFKIZS/ZK +IV159Qmxa8xiP6uTrJ28t4lqRr0Ewf6DFpzuDSm3t6Rm4OvMIrfE0uDaJDl5E5Px +4dTMhbD6kRoWFMpj+z63jhFxBPwF2DbBKv5UAlLuOWCQwmnjYEa6AyJzCw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE-----
diff --git a/src/test/resources/certs/vtuca_chain.p7b b/src/test/resources/certs/vtuca_chain.p7b new file mode 100644 index 0000000..bf00ed4 --- /dev/null +++ b/src/test/resources/certs/vtuca_chain.p7b
@@ -0,0 +1,74 @@ +-----BEGIN PKCS7----- +MIINVwYJKoZIhvcNAQcCoIINSDCCDUQCAQExADALBgkqhkiG9w0BBwGggg0qMIIG +8TCCBNmgAwIBAgIBBTANBgkqhkiG9w0BAQUFADBVMQswCQYDVQQGEwJVUzERMA8G +A1UECBMIVmlyZ2luaWExEzARBgNVBAcTCkJsYWNrc2J1cmcxHjAcBgNVBAoTFVZp +cmdpbmlhIFRlY2ggUm9vdCBDQTAeFw0wNjA5MjAxNjI4MzZaFw0xNjA5MTcxNjI4 +MzZaMIGUMRMwEQYKCZImiZPyLGQBGRMDZWR1MRIwEAYKCZImiZPyLGQBGRMCdnQx +CzAJBgNVBAYTAlVTMTwwOgYDVQQKEzNWaXJnaW5pYSBQb2x5dGVjaG5pYyBJbnN0 +aXR1dGUgYW5kIFN0YXRlIFVuaXZlcnNpdHkxHjAcBgNVBAMTFVZpcmdpbmlhIFRl +Y2ggVXNlciBDQTCCAiAwDQYJKoZIhvcNAQEBBQADggINADCCAggCggIBALtuDADJ +vOwqLAdiorNhINDADF6T3/xUGKGRiIGmjgy2COLGlxFHAgq8Hi8pPk3CzhUbBDia +r1sFo0BfzozoticTzlF2tIuftSTMCbQWPB0bPZlYp5zSt7WBgHhfVeBnWrgn5eVk +CJEfiwPjtOtlNvCykFKgM+4OTfC+ocCZwWmLR1PBmJ+CAf12xBbKoDIhPDOmTWOg +lu85oJXPTPwd8caLHR4cKeznuxq+i8Y0uqgtXc6+nhWZ6zUcXO65Q0y5aRwJgt2L +8Q6Pr34qFWR3GF+e6iIAY4UT951X5UdyfvSSNjN5BUhJv8n1mzHOqpdyiZf5qDEt +DNFYGx/o9sXVcqaLR0S61Z7UkcZQKZLZQDwZVthT9i69+N2D1ckaME9dto0ApIgr +gz0sUneg3hVs8un1bpXtJcsGfuUgWIp+qyjVZZQYN71wYXZ+HPEkmbRuXdrXEvC5 +63bL7gT/2/m5UqWoW3u8yaAO+DcuY9w0eh37mapsF2IuD3q4htFAM4cVUSCa0AQl +PdXnhu2JM4VR4S4aKkHCQQMS1lTr9A3DEdEvoqAeNNOLhP430CrULChYf1/0FGiu +PV1SADe6vrXf8y87W7RsWhuTFLyHxRq4eBHzBVnPrHk1ac8XslxcXz1qPm+Nshfa +sGX26wHSbXJ8lddnkUPQKiOFrGnCtNGoHdntAgEDo4IBjDCCAYgwDwYDVR0TAQH/ +BAUwAwEB/zALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFN67bqLJNHXY8VeVeRD5A8DU +aQqMMH0GA1UdIwR2MHSAFESRFC84mijaPkomPJiAo/z3iG2noVmkVzBVMQswCQYD +VQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWExEzARBgNVBAcTCkJsYWNrc2J1cmcx +HjAcBgNVBAoTFVZpcmdpbmlhIFRlY2ggUm9vdCBDQYIBADA7BgNVHR8ENDAyMDCg +LqAshipodHRwOi8vd3d3LnBraS52dC5lZHUvcm9vdGNhL2NybC9jYWNybC5jcmww +gYwGA1UdIASBhDCBgTAOBgwrBgEEAbRoBQICAQEwDgYMKwYBBAG0aAUCAgIBMA4G +DCsGAQQBtGgFAgIDATAOBgwrBgEEAbRoBQICBAEwPwYMKwYBBAG0aAUCAgUBMC8w +LQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucGtpLnZ0LmVkdS9yb290Y2EvY3BzLzAN +BgkqhkiG9w0BAQUFAAOCAgEAY1QvqZ9ozwQPrPQmqz2XhXCCDZxriy0dapk537wE +SYq+D4Ptkpuu+P8HDSi0j6QaksM6h7otm4k1znD1fcGt7uo2YzDoO1hyORjan4ZK +FgsR906LgexvTreYGSi1+4VKcfALDkqd/snkoPMFI3/KQnEoKbqwCXVQL908WrD6 +PH8gdNb5M8Es+TzH49iA4J+jQrE6CqVMAq2e/QwyC7EJ3yUHknBV0hpaWEvkUtty +XxFaF3ZVfOo0j2xxXcCihR4PGyUHlkPj5U3ggjyDZ7TTS0mtwmluNuhaJva2trW6 +NbscmZ0vdoyQLxjGVkBeeEFyNcm4OPMEc3uMzvhacxvN4NJF6Mhyhj+Sc/97KTYU +CZNPiP9fE8Ev6klgjbf6AA9ARHARDWpQ/F7CbN710Ft2iHZb0uldzZRMBevW3M4y +9Q62YQtr7WhuJ+FZFA3JnjJjOfYaR8c6zOXsWiyPFQ8Eq3qMdcO9V4UmDKeYK0hD +YI6BHpoKXNEikf7ygwKVVmOjLzSJI1G/knj9UffiusmlSQ7Cx5nz5HwciBtAxYp1 +6IyL0YNi1Con8YbB0EKlW8LS9+4d1R+cKtx/tPTQLJv9jjqlp/sw+NdDEkJ0Q4YA +TGJXklx3Q1QpyuA1TiI+7CXEyO9zkPQVptXAzB+kePb/TvwtaDI71LwYjg52d7nb +/wgwggYxMIIEGaADAgECAgEAMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNVBAYTAlVT +MREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxhY2tzYnVyZzEeMBwGA1UE +ChMVVmlyZ2luaWEgVGVjaCBSb290IENBMB4XDTAzMDQxMDE5Mjc0OVoXDTMzMDQw +MjE5Mjc0OVowVTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYD +VQQHEwpCbGFja3NidXJnMR4wHAYDVQQKExVWaXJnaW5pYSBUZWNoIFJvb3QgQ0Ew +ggIgMA0GCSqGSIb3DQEBAQUAA4ICDQAwggIIAoICAQCgRDSdz9FCKV9+KKohQDcO +tcANOrvANxnCPJMsykQCRvGTKlhoSxhjApRkwJn4a60130Alm4AUD3iQANUYUnFq +mCbjmmhieFTNGZkMBMVOIRIsupcMZLZsbYqSJSykxonejb9/WlTDEahVXh87p349 +9Pc/m/pZjfUjYAYrBzR/QwuFNq8eLdOlDcwlZS/PXfhtL5LjWm07DrAzKwUiUUCS ++IZrQqbquHNYUB+16IwtKv9ceInMwN+8nIBmhYPWQjKaX27Zvrj1x+FgpkXdv9A/ +b3ilioJZafiLx7/q2V9A01zb6uU18Sjxj3bqPlXXl7cK7BenQ8dDEwV/5DcCnaoQ +ZJDT1PYYXiqdUoGgsFypZbuMZ0f2+6pKGI3nmiz5vOpmKvrE7+aX70gyNa0irQJM +7OYailArddrOd7ostPZRMX1wcRrBoXh6IvTTMohrSo+yKVd9TIW60IebN0GPJZa/ +P9JUbsfYH1RjUOUOtvH7cTAYr9mNPaKiW4NpHVEz8o+RE24kifa46c2+cMe5PFR9 +mVXsiwdaFEJ4BhF4DRTMfGU7R19hDR7vr5FiqPU36i5jLGTgAmmzrJWPTmi42l79 +65UxNyL4GjdJPPZzFilKw2Yk/B/7Kky+fkpdyBqnwtHRr5X/u7gW1w3eYnqIUa/5 +askh5S5yqD8u64e1lLBXCQIBA6OCAQwwggEIMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFESRFC84mijaPkomPJiAo/z3iG2nMH0GA1UdIwR2MHSAFESRFC84mija +PkomPJiAo/z3iG2noVmkVzBVMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2lu +aWExEzARBgNVBAcTCkJsYWNrc2J1cmcxHjAcBgNVBAoTFVZpcmdpbmlhIFRlY2gg +Um9vdCBDQYIBADALBgNVHQ8EBAMCAQYwSgYDVR0gBEMwQTA/BgwrBgEEAbRoBQIB +AQEwLzAtBggrBgEFBQcCARYhaHR0cDovL3d3dy5wa2kudnQuZWR1L3Jvb3RjYS9j +cHMvMA0GCSqGSIb3DQEBBQUAA4ICAQB9k2F2LPLGh0BN9l2ER3sYSpWwEGEfWCrB +Nl2Hhh75qmIFhOGHLiEdnbG87v/1fv2phIiFkSliq2s8LZJJJb6uMpKjKWmovSaf +iQWXMfzm/266M6W7cxwSwymuaVZLIDqpfC0BsiIRixbuWB6qIy3wcBjTC4t4mBpV +b//zjpk18TrbFbw5ee/ISuI/LMw86XbvKlhfU6Q4l4WGOyhUPw2lpVcmaKNkiVKe +AG1CSa9L4XTCz7VJ0dm/kJXwfzw4cnRerZRjRBTKOa1geMvP5eG7mdx5bLwEIC4W +qpxQ0a4S63NkMZShMzouXOiwwZk//DHOKrPTAtbEfrQ0cowL0cwmq5rrGmcqQQbK +Dlu9GJO9aFSlVz0EpKtn4RazRe9aJwVb1m8u/uoZUQQDKDgVp4SNQnMlgYexyYRP +E4RBRv6azJvJw19izG9m18iCaAb3oCHcmuAHCKr8/3nqsvdoWqrDMETuuxaLW2F4 +E/rWDHECGYmAbh8IMmoLY1cIB5hH1Qy6yjM4jfiz9bMQdIZ7vhM1JCSkrfUnBbOV +6VipDH/YaMPnnmZ5Ay8gPRMm/iS7c5SQP/HcYcmvWAmKY/JTUeBTnSAF34hPBl0G +6YwvGUgcbd556rqzS6498+5mEVTWE/jC0NzdHzrI2uTFiGFZMhaJgHMatOZixCB9 +IOE8OVlB9qEAMQA= +-----END PKCS7-----
diff --git a/src/test/resources/csrs/simple-ec-prime256v1.csr b/src/test/resources/csrs/simple-ec-prime256v1.csr new file mode 100644 index 0000000..b44f13a --- /dev/null +++ b/src/test/resources/csrs/simple-ec-prime256v1.csr
@@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBBjCBrQIBADBLMRswGQYDVQQDExJzaW1wbGUuZXhhbXBsZS5jb20xFzAVBgoJ +kiaJk/IsZAEZFgdleGFtcGxlMRMwEQYKCZImiZPyLGQBGRYDY29tMFkwEwYHKoZI +zj0CAQYIKoZIzj0DAQcDQgAEAdnqPEISFMGKKZG5/y1vfZ7m+nQ3JPEdoFeg6uef +ChzF2ds7yNR/e9nCl+vf4HT3m4S35oYvK1vI9uZDDrfaNKAAMAoGCCqGSM49BAMC +A0gAMEUCIChj+FrmNlYWK/8ywpSzdLzPUMoOLSBQxXX6pKMDNsYoAiEAgybN/k5c +9ZqMlpTQ+szfl4UlYuxeAZ9BL7uzMPgqjjY= +-----END CERTIFICATE REQUEST-----
diff --git a/src/test/resources/csrs/simple-ec-secp384r1.csr b/src/test/resources/csrs/simple-ec-secp384r1.csr new file mode 100644 index 0000000..4abe51b --- /dev/null +++ b/src/test/resources/csrs/simple-ec-secp384r1.csr
@@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBRDCBygIBADBLMRswGQYDVQQDExJzaW1wbGUuZXhhbXBsZS5jb20xFzAVBgoJ +kiaJk/IsZAEZFgdleGFtcGxlMRMwEQYKCZImiZPyLGQBGRYDY29tMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEI/OEPzzsE8kS5u2+CEVi4pWLM41B6jMJ00VKG4bR0mvK +MQpmJmOAnJ13pxU308GX6kGqhBYD63ZwsJuTUyOHNUDli2bqrtDc3zi/Skwb41YO +cMzLqxrzcA4OJKuYwV5ooAAwCgYIKoZIzj0EAwIDaQAwZgIxALRCHUNt6ualwJt7 +zfmkfIo66+ZNyIu67KugJcT+LhoBYgjgbrjubYHeN5YmjlUHEwIxAICo8t02z14r +LGztxBFHTbZlMDtx/8PsK14Lx7wDQi0GhWNHQiqQKkmt8iigzAuBRA== +-----END CERTIFICATE REQUEST-----
diff --git a/src/test/resources/csrs/simple-rsa-1024.csr b/src/test/resources/csrs/simple-rsa-1024.csr new file mode 100644 index 0000000..d33424b --- /dev/null +++ b/src/test/resources/csrs/simple-rsa-1024.csr
@@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIBijCB9AIBADBLMRswGQYDVQQDExJzaW1wbGUuZXhhbXBsZS5jb20xFzAVBgoJ +kiaJk/IsZAEZFgdleGFtcGxlMRMwEQYKCZImiZPyLGQBGRYDY29tMIGfMA0GCSqG +SIb3DQEBAQUAA4GNADCBiQKBgQC7BZZm8prhDaplqevZs+8Le8IVQ/0H4Rz2rC5E +p0lF6o/w0KddvLZkVh6ONKtwxf+oVFT/Aa87/Uib9fjR/XKrU1ektL9cabL9qiCC +0bdvFlPJJvZL5ftPMaVAFlZh7ZqQ07tf4MOtPAhbCuwJbV9I0TEJBjM/4cDl6sgK +d54xYQIDAQABoAAwDQYJKoZIhvcNAQELBQADgYEAQZ+j3jBhC4XSNYGx7bWovRPM +O7v85X7oovjR0y0aqrYiIbajGG/AsiorwYuvfsM1ielPW3Jd1YZXN2SrFIvQUrre +B4Pa6kAAAK7eWVFG7n5xZKyUi43JUIpRC+Q2LvRnREWYCLBggxgRBdz1cB/HNbOq +LirPEroYI/+W6An6zKE= +-----END CERTIFICATE REQUEST-----
diff --git a/src/test/resources/csrs/with-sans-rsa-2048.csr b/src/test/resources/csrs/with-sans-rsa-2048.csr new file mode 100644 index 0000000..cd9eab4 --- /dev/null +++ b/src/test/resources/csrs/with-sans-rsa-2048.csr
@@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC1zCCAb8CAQAwSTEZMBcGA1UEAxMQaG9zdC5leGFtcGxlLmNvbTEXMBUGCgmS +JomT8ixkARkWB2V4YW1wbGUxEzARBgoJkiaJk/IsZAEZFgNjb20wggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC5CZnRypPvyo2tN1uU143tYj7LG0Ymn3gN +s8FlZfpRAqJc12W8FfPDeK1xCcuWtxtLjwQw7wH11I4XCX8/wfGEpLUkrvd3Ve+D +3XXwi9ei/8vljJBVPzcLQcd+Kgm0jfvGT7UczZGVkcpHD1h3TGnajKmwN8hfcm9K +9O97/NSyYrgS9Tdb8epsX0gH4VuUKcLLB7t4LMfwXZUFV8gxfQpGQ99VN0xmj+54 +34RhEbTsNbZxQchi9DJ3sDF7YDo5yOAbqWQTqfwaSHkjQIMettuy331IJ2lF3ju/ +1s9hlL/naZOq9Vp+R1MWhACc/kX6mVU23n8vF1V5MrOTNjgYjj21AgMBAAGgSTBH +BgkqhkiG9w0BCQ4xOjA4MDYGA1UdEQQvMC2CFGRldi5ob3N0LmV4YW1wbGUuY29t +ghVwcHJkLmhvc3QuZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBAII2xRyP +Gu8cEDUfjYOSEq7kTzKvajj/DQaBeEjsW1tU2dEFZndK/LoZy9IY/F3H7lYB5I9W +AtmmoE0lmIlAZC1XFYTB++kY8Yh65l831Cie17FY1Z6NaqVmHyYZWXbzJRVLxLvf +tczqXCLwC4dpXnhB5qEPwQ2jZbwHIohsMViK/L/DApgKDgtxqtxDqOv32Yo0g6xO +r5KctgCpH4fr5zaZ4I2IjB9ZanEzBQP6w689vJGUiQOrTKoaDDMH/WaxEHoJsQjg +0KxKrlKvORZGhK3voOh6maRhgiPCOLagK3DGxcXY6a/b4eS0giLX+Y6pGXfpR/Ps +arHLrOFnzGfVfb0= +-----END CERTIFICATE REQUEST-----
diff --git a/src/test/resources/keys/aes-128.key b/src/test/resources/keys/aes-128.key new file mode 100644 index 0000000..db24e71 --- /dev/null +++ b/src/test/resources/keys/aes-128.key
@@ -0,0 +1 @@ +Ü|âÑË¢oU•2ÿõ'>õ \ No newline at end of file
diff --git a/src/test/resources/keys/dsa-openssl-des3.pem b/src/test/resources/keys/dsa-openssl-des3.pem new file mode 100644 index 0000000..4a85bb1 --- /dev/null +++ b/src/test/resources/keys/dsa-openssl-des3.pem
@@ -0,0 +1,15 @@ +-----BEGIN DSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,5E562BC405BB14E8 + +SUa/8UvU4GuanQJU8F2HQpizTuDdn93kk/h0HlQL8d9YFF1eHqxRfwzTWbw99/n3 +VZ6WC5xaP3w0nxD034Za56ujKL5GcmCQsNuVljFlOUqoFxDIUfxkFx5lX/zXIC8w +3jSnYA2ga29bA+bUTVGuCBuzyqumh3zdpMXx2LpJGtTh7DGcDbCu5GBPF3SPHUhi +iCXubJJJLuB15siChYVx4+w+74n2Y1Ga+FN6CL1svX+cpnMIH8jhlPQNmNySlEbW +yBaaXUWoCFUeZZXt4rvzkTnXm5idKr0zYwuCp871rxQj8g2zqy8WGoJmjIj08vjD +hSO0lRSlg8LQY2zNiv/D+PyXoODoS5AcSExjFsHs8RGynndgiGV2HpmgJ3d3Db/M +ZdvCsHf8ldwmHhIAwjVbO30b93syaVhV+/O+pQ16XjEawgS9Rzz1POrAOr3/C8as +WsYxT+5LDI6TYq+ng2JTiHx10VfSNbHZddXgt0E2utIt6kd7GKDbGBGKes8DJjqI +8LrmVafdfBSea2R5i03YAMzOIpTxgyeJsiK0THm+XtjpTZ1yLSYN5ee9uvOz9TQd +0MlWsFwB+9tFlLAV7J0yjKM4kzykrVHS +-----END DSA PRIVATE KEY-----
diff --git a/src/test/resources/keys/dsa-openssl-nopass.der b/src/test/resources/keys/dsa-openssl-nopass.der new file mode 100644 index 0000000..5be1a0d --- /dev/null +++ b/src/test/resources/keys/dsa-openssl-nopass.der Binary files differ
diff --git a/src/test/resources/keys/dsa-openssl-nopass.pem b/src/test/resources/keys/dsa-openssl-nopass.pem new file mode 100644 index 0000000..ed096e1 --- /dev/null +++ b/src/test/resources/keys/dsa-openssl-nopass.pem
@@ -0,0 +1,12 @@ +-----BEGIN DSA PRIVATE KEY----- +MIIBvAIBAAKBgQDrpYnxvgBhgVRlhI7pxuOuta8nQe3hckXx828FQFWj1NB98xXD +mN+c9Zej5OGQDVR6u4Qm1YiQUe18q5GCmlHGgdJ0jy4IN71ljprxqobkerVkRm8+ +H8h5dbqwkZPZqybpuJgCXdzpS7oAsriKqM7GFLbyeZpRnr4q9c5sYpynewIVAO3w +RMkmD+d4OxSzpXBMroSI70ABAoGASfW2qBs59MyqXrXguI32xlbiqxZLK9nnmNAB +STnk7chZAp1aqzZxeu+HjdUQDsANrc84VmfehtABrsfGoZhrDUW4/6mlEwzUVq+w +TwSLw3fvC6TIp+Am+Y+HPwO7jmPuVsYVJt0zYn7ngUaIcIdTZ3sWEKqXJXLk0QP6 +LoC1+PICgYEA6Tw2khtb1g0vcHu6JRgggWPZVTuj/HOH3FyjufsfHogWKrlKebZ6 +hnQ73qAcEgLLYKctPdCX6wnpXN+BsQGYdTkc0FsUNZD4VW5L5kaWRiLVfE8x55wX +dMZtXKWqg1vL6aXYZw7RFe9U9Ck+/AG90knThDC+xrX2FTDm6uC25rkCFQDi5v1v +S/zsr0up+AudMj9WvQTIuw== +-----END DSA PRIVATE KEY-----
diff --git a/src/test/resources/keys/dsa-pkcs8-nopass.der b/src/test/resources/keys/dsa-pkcs8-nopass.der new file mode 100644 index 0000000..8e7d2a8 --- /dev/null +++ b/src/test/resources/keys/dsa-pkcs8-nopass.der Binary files differ
diff --git a/src/test/resources/keys/dsa-pkcs8-nopass.pem b/src/test/resources/keys/dsa-pkcs8-nopass.pem new file mode 100644 index 0000000..9da8e84 --- /dev/null +++ b/src/test/resources/keys/dsa-pkcs8-nopass.pem
@@ -0,0 +1,9 @@ +-----BEGIN PRIVATE KEY----- +MIIBSwIBADCCASsGByqGSM44BAEwggEeAoGBAOulifG+AGGBVGWEjunG4661rydB +7eFyRfHzbwVAVaPU0H3zFcOY35z1l6Pk4ZANVHq7hCbViJBR7XyrkYKaUcaB0nSP +Lgg3vWWOmvGqhuR6tWRGbz4fyHl1urCRk9mrJum4mAJd3OlLugCyuIqozsYUtvJ5 +mlGevir1zmxinKd7AhUA7fBEySYP53g7FLOlcEyuhIjvQAECgYBJ9baoGzn0zKpe +teC4jfbGVuKrFksr2eeY0AFJOeTtyFkCnVqrNnF674eN1RAOwA2tzzhWZ96G0AGu +x8ahmGsNRbj/qaUTDNRWr7BPBIvDd+8LpMin4Cb5j4c/A7uOY+5WxhUm3TNifueB +Rohwh1NnexYQqpclcuTRA/ougLX48gQXAhUA4ub9b0v87K9LqfgLnTI/Vr0EyLs= +-----END PRIVATE KEY-----
diff --git a/src/test/resources/keys/dsa-pkcs8-priv.der b/src/test/resources/keys/dsa-pkcs8-priv.der new file mode 100644 index 0000000..5456df7 --- /dev/null +++ b/src/test/resources/keys/dsa-pkcs8-priv.der Binary files differ
diff --git a/src/test/resources/keys/dsa-pkcs8-priv.pem b/src/test/resources/keys/dsa-pkcs8-priv.pem new file mode 100644 index 0000000..5c72a2c --- /dev/null +++ b/src/test/resources/keys/dsa-pkcs8-priv.pem
@@ -0,0 +1,10 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIBcTAbBgkqhkiG9w0BBQMwDgQIcn2fFCBFuV8CAggABIIBUG8ceCq/p5CYZBt8 +8a7KNx2FmPt5LHrSuonpS6WbB8eZJWRl7QiAL3Ui7ooFoDkl+aa1vvolBpGjXvdd +24hLgJCCSsAw6zIfcEaWkogu9wNOlOffL5CZgkFCg/JPrBnSJ/IoQ489hA4DWBDV +CLc0ZYkFI1h0LmmjjU56uvfTU5hH5Ew+cIMCiBkFfaWP9micPqjXkl4h2jP0FoQ9 +25m99SNxX3ORtyA5PMTLVTWxlAoFdf91B/f36sYJzB95uDulz/sOhAEZyYuKuYi4 +KLqtdfV5jL9b/K7hFRBsHlnGSjzf7bQ5GloBeUtdZjmMs1Cg7wR+38GIp2xJMvJB +T3Ja/XlDYzmYVYg31BTbIwQgsfDwdCFrsPJWUgjhLF4TR+DQRVGREuHCw/KWEyBz +1ktXBYkZtnl5NzK6WxapCwaMn4v4UEvvLGLgaaqHErGFmkZS2A== +-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/keys/dsa-pkcs8-v2-des3.der b/src/test/resources/keys/dsa-pkcs8-v2-des3.der new file mode 100644 index 0000000..9a5a6ef --- /dev/null +++ b/src/test/resources/keys/dsa-pkcs8-v2-des3.der Binary files differ
diff --git a/src/test/resources/keys/dsa-pkcs8-v2-des3.pem b/src/test/resources/keys/dsa-pkcs8-v2-des3.pem new file mode 100644 index 0000000..d19fc4a --- /dev/null +++ b/src/test/resources/keys/dsa-pkcs8-v2-des3.pem
@@ -0,0 +1,11 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIBljBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIe5NvINrToPcCAggA +MBQGCCqGSIb3DQMHBAiBg4mBYvzq1QSCAVDzYM/oK1flkdze+6mW0JF5f8EFRU9O +5bn94oEoum0EWz43OFEW9y8lBTuErzUUxXBZ3CXK5IT8AtiVTTUMnPzpMtnkSiWu +iLR8bujoyfSEIQ4kewkZG4DFT2tUPOqE1O6kiiOzCiuCD1wRHH+tznGQb+mj4Afj +GSD7jRyX9l5TWYWjQw3+w9Fiu12LHEDTXc7I5IotXOyOWJ+A6G4iLg7q4r7L4gJU +RX4LiI7MX3Dqv1tyed9yN9xsC8ORIGG1h+F6F8yua+UErqGlfSxlvTYPI8W8O5Wr +nv44CmAQOwZdVto5pkQ61YX7csrgaJHLcBtZdxVHEt7ua/887pLuHq4Fq9u+0usG +3OwPNJ/zJinTww8X1ZazVmYXcDGJK/d04URmUYuo8AGc2XFY3lCjMr4t45saQydK +Du1telBhT7x2R8w6vi425AkD3QE8t531oc4= +-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/keys/dsa-pub.der b/src/test/resources/keys/dsa-pub.der new file mode 100644 index 0000000..c14ea0a --- /dev/null +++ b/src/test/resources/keys/dsa-pub.der Binary files differ
diff --git a/src/test/resources/keys/dsa-pub.pem b/src/test/resources/keys/dsa-pub.pem new file mode 100644 index 0000000..b8e8c2f --- /dev/null +++ b/src/test/resources/keys/dsa-pub.pem
@@ -0,0 +1,12 @@ +-----BEGIN PUBLIC KEY----- +MIIBtzCCASsGByqGSM44BAEwggEeAoGBAOulifG+AGGBVGWEjunG4661rydB7eFy +RfHzbwVAVaPU0H3zFcOY35z1l6Pk4ZANVHq7hCbViJBR7XyrkYKaUcaB0nSPLgg3 +vWWOmvGqhuR6tWRGbz4fyHl1urCRk9mrJum4mAJd3OlLugCyuIqozsYUtvJ5mlGe +vir1zmxinKd7AhUA7fBEySYP53g7FLOlcEyuhIjvQAECgYBJ9baoGzn0zKpeteC4 +jfbGVuKrFksr2eeY0AFJOeTtyFkCnVqrNnF674eN1RAOwA2tzzhWZ96G0AGux8ah +mGsNRbj/qaUTDNRWr7BPBIvDd+8LpMin4Cb5j4c/A7uOY+5WxhUm3TNifueBRohw +h1NnexYQqpclcuTRA/ougLX48gOBhQACgYEA6Tw2khtb1g0vcHu6JRgggWPZVTuj +/HOH3FyjufsfHogWKrlKebZ6hnQ73qAcEgLLYKctPdCX6wnpXN+BsQGYdTkc0FsU +NZD4VW5L5kaWRiLVfE8x55wXdMZtXKWqg1vL6aXYZw7RFe9U9Ck+/AG90knThDC+ +xrX2FTDm6uC25rk= +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/ec-openssl-prime256v1-named-nopass.der b/src/test/resources/keys/ec-openssl-prime256v1-named-nopass.der new file mode 100644 index 0000000..71fe542 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-prime256v1-named-nopass.der Binary files differ
diff --git a/src/test/resources/keys/ec-openssl-prime256v1-named-nopass.pem b/src/test/resources/keys/ec-openssl-prime256v1-named-nopass.pem new file mode 100644 index 0000000..35cb960 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-prime256v1-named-nopass.pem
@@ -0,0 +1,8 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIKtRX2H01Z5Pu3O53fQJfctXqU/3/n6p5kzKTiQ/XSMxoAoGCCqGSM49 +AwEHoUQDQgAEV0bfrxH9VzCilRisNe/yynt2n1+hc/JktcTD5PK5eHFrAk3nUb7Q +QClQFWMrrfbB9mmWhKuMqym8uLDsbLtrow== +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-openssl-prime256v1-named-pub.pem b/src/test/resources/keys/ec-openssl-prime256v1-named-pub.pem new file mode 100644 index 0000000..e76a244 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-prime256v1-named-pub.pem
@@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV0bfrxH9VzCilRisNe/yynt2n1+h +c/JktcTD5PK5eHFrAk3nUb7QQClQFWMrrfbB9mmWhKuMqym8uLDsbLtrow== +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/ec-openssl-secp112r1-named-nopass.der b/src/test/resources/keys/ec-openssl-secp112r1-named-nopass.der new file mode 100644 index 0000000..cac8d2e --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp112r1-named-nopass.der Binary files differ
diff --git a/src/test/resources/keys/ec-openssl-secp112r1-named-nopass.pem b/src/test/resources/keys/ec-openssl-secp112r1-named-nopass.pem new file mode 100644 index 0000000..639129f --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp112r1-named-nopass.pem
@@ -0,0 +1,7 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQABg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MD4CAQEEDk5WBCiBT8eE8yf/y6dRoAcGBSuBBAAGoSADHgAEa8bhnjsvOSDXsxED +mAjM7YymrVrLJTYchIM+Bw== +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-openssl-secp112r1-named-pub.pem b/src/test/resources/keys/ec-openssl-secp112r1-named-pub.pem new file mode 100644 index 0000000..624de16 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp112r1-named-pub.pem
@@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MDIwEAYHKoZIzj0CAQYFK4EEAAYDHgAEa8bhnjsvOSDXsxEDmAjM7YymrVrLJTYc +hIM+Bw== +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/ec-openssl-secp224k1-explicit-des.pem b/src/test/resources/keys/ec-openssl-secp224k1-explicit-des.pem new file mode 100644 index 0000000..549f656 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp224k1-explicit-des.pem
@@ -0,0 +1,11 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-CBC,36232933C6E4A23A + +iFYAONnfgUQvmXFtoDsb/hKXjTEhsfW+gT2rzpp0MOS2Ygidbiu9JxwZ2anT1vbV +SwTfDQ/EPQTRoGft1EY4Cb6CmUEpj02ywo+U7c/smUDRTScOvh7budcuhYoNVzQL +sifvukhV25nDEWydl8DPiCnVtQYNGIBh8wj1TBsbn1ucHddc0VwaGXZeYoHCbymS +jAPHE+xHOrFXwAWot2FbcO6tnV7Dqd6Tw8DA1XCux9DQFfA6jxED3l7Wd79NwuKf +zzpcqV5Xvcxkud5cu3WW3T8aHicKXW/dZHDKkQtet2uBnJ4nNRZErsKtFxorqimH +cvvZCECsiDUyMdS67Za2PA== +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-openssl-secp224k1-explicit-nopass.der b/src/test/resources/keys/ec-openssl-secp224k1-explicit-nopass.der new file mode 100644 index 0000000..ccaf8fd --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp224k1-explicit-nopass.der Binary files differ
diff --git a/src/test/resources/keys/ec-openssl-secp224k1-explicit-nopass.pem b/src/test/resources/keys/ec-openssl-secp224k1-explicit-nopass.pem new file mode 100644 index 0000000..76bdd2a --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp224k1-explicit-nopass.pem
@@ -0,0 +1,8 @@ +-----BEGIN EC PRIVATE KEY----- +MIH3AgEBBBw3HL0pymjIYdndGvIRdEwFOCYipTJyZZDb8oiFoIGVMIGSAgEBMCgG +ByqGSM49AQECHQD///////////////////////////////7//+VtMAYEAQAEAQUE +OQShRVszTfCZ3zD8KKFppGfp5HB1qQ9+ZQ62t6Rcfgif7X+6NEKCyvvW9+MZ98Cw +vVniykvbVW1hpQIdAQAAAAAAAAAAAAAAAAAB3OjS7GGEyvCpcXafsfcCAQGhPAM6 +AASYYFXr2TWWJSCu9HwUmNH0yvYPmFeEox3Ut4fBntgBo+kINaoSVfTVbj6sAfeo +kgWQUadcoXNMEw== +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-openssl-secp224k1-explicit-pub.pem b/src/test/resources/keys/ec-openssl-secp224k1-explicit-pub.pem new file mode 100644 index 0000000..3563fd8 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp224k1-explicit-pub.pem
@@ -0,0 +1,7 @@ +-----BEGIN PUBLIC KEY----- +MIHdMIGeBgcqhkjOPQIBMIGSAgEBMCgGByqGSM49AQECHQD///////////////// +//////////////7//+VtMAYEAQAEAQUEOQShRVszTfCZ3zD8KKFppGfp5HB1qQ9+ +ZQ62t6Rcfgif7X+6NEKCyvvW9+MZ98CwvVniykvbVW1hpQIdAQAAAAAAAAAAAAAA +AAAB3OjS7GGEyvCpcXafsfcCAQEDOgAEmGBV69k1liUgrvR8FJjR9Mr2D5hXhKMd +1LeHwZ7YAaPpCDWqElX01W4+rAH3qJIFkFGnXKFzTBM= +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/ec-openssl-secp256k1-explicit-nopass.pem b/src/test/resources/keys/ec-openssl-secp256k1-explicit-nopass.pem new file mode 100644 index 0000000..585789b --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp256k1-explicit-nopass.pem
@@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHQCAQEEIIS3dzDcULU/YxSdkYjQnnnJMTFW3xUQb9rTjVuD6KzHoAcGBSuBBAAK +oUQDQgAExo65hLAAfoKDrGFzflBulms4hLyntjaUdaoJTnBRt7P3E/d1xtERSUm3 +GTWrelNA9tSw/OPq4jcPz70u4VJtAw== +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-openssl-secp256k1-explicit-pub.pem b/src/test/resources/keys/ec-openssl-secp256k1-explicit-pub.pem new file mode 100644 index 0000000..f08d594 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-secp256k1-explicit-pub.pem
@@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAExo65hLAAfoKDrGFzflBulms4hLyntjaU +daoJTnBRt7P3E/d1xtERSUm3GTWrelNA9tSw/OPq4jcPz70u4VJtAw== +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/ec-openssl-sect409k1-named-nopass.pem b/src/test/resources/keys/ec-openssl-sect409k1-named-nopass.pem new file mode 100644 index 0000000..b5deb5c --- /dev/null +++ b/src/test/resources/keys/ec-openssl-sect409k1-named-nopass.pem
@@ -0,0 +1,9 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAJA== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIGvAgEBBDNhfrS2YEmpljk4skviH8iSWr7fKoy7Dpe33DqVtjr8KoX05dYrIaSq +PXpcXzAylaqsCiSgBwYFK4EEACShbANqAAQBen76TxFvShf5SFOx5KtdhrLbkB9Y +DS4cEUUld3jrQCkJBY7clKBKSsLob/S4CA5j/+mWABIK2M216wcUz9NVk3HLSnd0 +b02d/XAMt9E8V5vcKmVyjo5HQ0LKXnOzZs0h3SIKz9vhWQ== +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-openssl-sect409k1-named-pub.pem b/src/test/resources/keys/ec-openssl-sect409k1-named-pub.pem new file mode 100644 index 0000000..4cea543 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-sect409k1-named-pub.pem
@@ -0,0 +1,5 @@ +-----BEGIN PUBLIC KEY----- +MH4wEAYHKoZIzj0CAQYFK4EEACQDagAEAXp++k8Rb0oX+UhTseSrXYay25AfWA0u +HBFFJXd460ApCQWO3JSgSkrC6G/0uAgOY//plgASCtjNtesHFM/TVZNxy0p3dG9N +nf1wDLfRPFeb3Cplco6OR0NCyl5zs2bNId0iCs/b4Vk= +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/ec-openssl-sect571r1-explicit-des.pem b/src/test/resources/keys/ec-openssl-sect571r1-explicit-des.pem new file mode 100644 index 0000000..5cbc267 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-sect571r1-explicit-des.pem
@@ -0,0 +1,18 @@ +-----BEGIN EC PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-CBC,A1B708E785CED928 + +nxXWURBdsbW4WyN25MmBNwWUjVEegjJo/XPwACRsibhAV+diUcbwTCde3qGlvVHs +Z4BZVtBPP4c7Qcqvo1jAtgA4nFETptSt3iX4JFeZ4CVpmuMDpeJ9QvSHc4xh0rtj +rBWhmZfmwSIiegzUA1KV3AavK3mYsKoPaW2uqhfD/nLhZewXEwSo8Fna+7XL4w8L +0qZoj7cm3Xa6CiYit6LOckVwnztPQ2S9lVYo+OQoZu79AETzoPctUL/5XozPkC8m +m3Kip6Azj3SlcZCBu4HTi3YRnnxi0xtVaB5cgUICOsnY+yIJhuTC1NwqoUn3E622 +2VlBHpIe2/zA3vylMmriM2dPRkgZN75Z//qE4om8ka0Auff8IIeCisLKB3QKWgGY +hDdoE/Q6r9CxtfNudyZtMGt25idBfKp+toAPyH4GWWc21IKrDBDLmcEbLXGZKKjO +eU/ohpjhOY7ojunSC/TcSaHKCLwW/9PCkkOb+h3Ze+PbfKLr7nRA6Sr0X8GV8e+h +1XZwMfkHCaG/u8t3+ZoVHDvogja7IjA7rSfHrLNp/YGSqwsijYwvvaU1flt8cO7K +9qDpdRULYmBLmiAI5M6W9C768ZQ/qLhzuQ0Bmy+FwSwlt3lOx9XgNkbMCNcMAuv3 +6pI54RVsbXtv3vaW+Rac1O7QQOmH5L2nBYtamZ+7k4AZOmc70QrnUyHU+JxDvWTs +xhAARVngIt71kDibZZmCGHneZvonXGd1PCAPkXA2C4dudhcaGk5JpkQrGt/FtazS +C4HXA0/ZVBglYz4EhiLy5WNjUbxOI9KHPkqzDvww/fQ8nzP6s4aklg== +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-openssl-sect571r1-explicit-nopass.der b/src/test/resources/keys/ec-openssl-sect571r1-explicit-nopass.der new file mode 100644 index 0000000..fcaf19c --- /dev/null +++ b/src/test/resources/keys/ec-openssl-sect571r1-explicit-nopass.der Binary files differ
diff --git a/src/test/resources/keys/ec-openssl-sect571r1-explicit-nopass.pem b/src/test/resources/keys/ec-openssl-sect571r1-explicit-nopass.pem new file mode 100644 index 0000000..7d92f3e --- /dev/null +++ b/src/test/resources/keys/ec-openssl-sect571r1-explicit-nopass.pem
@@ -0,0 +1,15 @@ +-----BEGIN EC PRIVATE KEY----- +MIICXQIBAQRHq9Hu/6ik+nkgja8ol2qgR063MpyVCiX6yaXXWnBvLZOmIslBgatF +MzaOGfekjsWx6JuHUBZ+9ZKP4r0mCGP/FpZB/uFYtdmgggF1MIIBcQIBATAlBgcq +hkjOPQECMBoCAgI7BgkqhkjOPQECAwMwCQIBAgIBBQIBCjBkBAEBBEgC9A5+IiHy +ld4pcRe389YvXGqX/8uM7/HNa6jOSpoYrYT/q72O+lkzK+etZ1ambilK/RhaeP8S +qlIOTec5usoMf/7/fylVcnoDFQAqoFj3Og4zq0hrD2EEEMU6fxMjEASBkQQDAwAd +NLhWKWwWwNQNPNd1CpPR0pVfqAql9A/I23sqvb3lOVD0wNKTzdcRo1tn+xSZrmAD +hhTxOUq/o7TIUNkn4ed2nI7sLRkDe/JzQtpjm23M//63PWnXjGwnpgCcu8oZgPhT +OSHopoRCPkO6sIpXYpGvj0YbsqizUx0vBIXBmxbi8VFuI908GkgnrxuKwVsCSAP/ +/////////////////////////////////////////////+Zhzhj/VZhzCAWbGGgj +hR7H3ZyhFh3pPVF01m6Dgum7L+hORwIBAqGBlQOBkgAEAJwZpe20C5w+yTMUKjet +DfROAC4ydb9Dy25G4PDKewVeoyM4EeDWAU25q5A91ipvCIe0GqeS7Va40HH3VjGg +Q0yRFh+BKVuTAle0b7VyOg52P8pIGqovQd3ZEk4vmh9uH8yldBYrM3+0hpTXsqcP +pdoHJFZW93bpu7T+Q9tXTzLQ9wjvQJ8dAo8wqcPN4OOX +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-openssl-sect571r1-explicit-pub.pem b/src/test/resources/keys/ec-openssl-sect571r1-explicit-pub.pem new file mode 100644 index 0000000..261fdc0 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-sect571r1-explicit-pub.pem
@@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICFzCCAX4GByqGSM49AgEwggFxAgEBMCUGByqGSM49AQIwGgICAjsGCSqGSM49 +AQIDAzAJAgECAgEFAgEKMGQEAQEESAL0Dn4iIfKV3ilxF7fz1i9capf/y4zv8c1r +qM5KmhithP+rvY76WTMr561nVqZuKUr9GFp4/xKqUg5N5zm6ygx//v9/KVVyegMV +ACqgWPc6DjOrSGsPYQQQxTp/EyMQBIGRBAMDAB00uFYpbBbA1A0813UKk9HSlV+o +CqX0D8jbeyq9veU5UPTA0pPN1xGjW2f7FJmuYAOGFPE5Sr+jtMhQ2Sfh53acjuwt +GQN78nNC2mObbcz//rc9adeMbCemAJy7yhmA+FM5IeimhEI+Q7qwildika+PRhuy +qLNTHS8EhcGbFuLxUW4j3TwaSCevG4rBWwJIA/////////////////////////// +////////////////////5mHOGP9VmHMIBZsYaCOFHsfdnKEWHek9UXTWboOC6bsv +6E5HAgECA4GSAAQAnBml7bQLnD7JMxQqN60N9E4ALjJ1v0PLbkbg8Mp7BV6jIzgR +4NYBTbmrkD3WKm8Ih7Qap5LtVrjQcfdWMaBDTJEWH4EpW5MCV7RvtXI6DnY/ykga +qi9B3dkSTi+aH24fzKV0Fiszf7SGlNeypw+l2gckVlb3dum7tP5D21dPMtD3CO9A +nx0CjzCpw83g45c= +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/ec-openssl-sect571r1-named-nopass.der b/src/test/resources/keys/ec-openssl-sect571r1-named-nopass.der new file mode 100644 index 0000000..fcaf19c --- /dev/null +++ b/src/test/resources/keys/ec-openssl-sect571r1-named-nopass.der Binary files differ
diff --git a/src/test/resources/keys/ec-openssl-sect571r1-named-nopass.pem b/src/test/resources/keys/ec-openssl-sect571r1-named-nopass.pem new file mode 100644 index 0000000..8bc53d2 --- /dev/null +++ b/src/test/resources/keys/ec-openssl-sect571r1-named-nopass.pem
@@ -0,0 +1,10 @@ +-----BEGIN EC PARAMETERS----- +BgUrgQQAJw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MIHtAgEBBEer0e7/qKT6eSCNryiXaqBHTrcynJUKJfrJpddacG8tk6YiyUGBq0Uz +No4Z96SOxbHom4dQFn71ko/ivSYIY/8WlkH+4Vi12aAHBgUrgQQAJ6GBlQOBkgAE +AJwZpe20C5w+yTMUKjetDfROAC4ydb9Dy25G4PDKewVeoyM4EeDWAU25q5A91ipv +CIe0GqeS7Va40HH3VjGgQ0yRFh+BKVuTAle0b7VyOg52P8pIGqovQd3ZEk4vmh9u +H8yldBYrM3+0hpTXsqcPpdoHJFZW93bpu7T+Q9tXTzLQ9wjvQJ8dAo8wqcPN4OOX +-----END EC PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-pkcs8-prime256v1-named-nopass.der b/src/test/resources/keys/ec-pkcs8-prime256v1-named-nopass.der new file mode 100644 index 0000000..009a3b4 --- /dev/null +++ b/src/test/resources/keys/ec-pkcs8-prime256v1-named-nopass.der Binary files differ
diff --git a/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-nopass.der b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-nopass.der new file mode 100644 index 0000000..003e355 --- /dev/null +++ b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-nopass.der Binary files differ
diff --git a/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-nopass.pem b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-nopass.pem new file mode 100644 index 0000000..dac54af --- /dev/null +++ b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-nopass.pem
@@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIIBBwIBADCBngYHKoZIzj0CATCBkgIBATAoBgcqhkjOPQEBAh0A//////////// +///////////////////+///lbTAGBAEABAEFBDkEoUVbM03wmd8w/CihaaRn6eRw +dakPfmUOtrekXH4In+1/ujRCgsr71vfjGffAsL1Z4spL21VtYaUCHQEAAAAAAAAA +AAAAAAAAAdzo0uxhhMrwqXF2n7H3AgEBBGEwXwIBAQQcNxy9KcpoyGHZ3RryEXRM +BTgmIqUycmWQ2/KIhaE8AzoABJhgVevZNZYlIK70fBSY0fTK9g+YV4SjHdS3h8Ge +2AGj6Qg1qhJV9NVuPqwB96iSBZBRp1yhc0wT +-----END PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-sha1-rc4-128.der b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-sha1-rc4-128.der new file mode 100644 index 0000000..69ece65 --- /dev/null +++ b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-sha1-rc4-128.der Binary files differ
diff --git a/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-v1-sha1-rc2-64.der b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-v1-sha1-rc2-64.der new file mode 100644 index 0000000..60ccc1b --- /dev/null +++ b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-v1-sha1-rc2-64.der Binary files differ
diff --git a/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-v2-des3.pem b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-v2-des3.pem new file mode 100644 index 0000000..98ef49f --- /dev/null +++ b/src/test/resources/keys/ec-pkcs8-secp224k1-explicit-v2-des3.pem
@@ -0,0 +1,10 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIBVjBABgkqhkiG9w0BBQ0wMzAbBgkqhkiG9w0BBQwwDgQIxeg91cPvDU8CAggA +MBQGCCqGSIb3DQMHBAiuY7aTDDWyWQSCARA4/DvOJkuMZ1IbSS5/pyqgbGSC+fOu +6ZuBSYQqUrawNhIDkNt4rvpOlvre6PNh3RAu43wJLVrgYcSMM5ZlJI74na3ru9zd +c5iZ17/rnk2ooGNRNj0mqx+fAj3Tu4d97mRizRboWAJpsQeTC4Br4uDdx8ttZzIP +zp1oVMLy9Yj3HsIwwNxGu9jbgBaHiHjIzkJ+lRqza35+9sdpQwcpz2NMUQIXw34S +JjUhXLaufy91d0HSfmbo8h9a0zSHiRRmHrBUBTUkBYMbLnEkZf5fAlhT21FFafSc +1v4BzuVZfvGLisjWzOZzW199G+XrYOxNmjUkvLDRyWMJYxL74YK6m6bHnZ3U+wjm +N2QeMohF0rB8wA== +-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-pkcs8-sect571r1-explicit-v2-aes128.pem b/src/test/resources/keys/ec-pkcs8-sect571r1-explicit-v2-aes128.pem new file mode 100644 index 0000000..0d822c0 --- /dev/null +++ b/src/test/resources/keys/ec-pkcs8-sect571r1-explicit-v2-aes128.pem
@@ -0,0 +1,18 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQIDONm+4mH/+cCAggA +MB0GCWCGSAFlAwQBAgQQdQBCHbXJepgl9MClxApnqgSCAoAph2G4i+iZzPSWxObp +66uQ5n1tNOJE6pLdbcHinwPHVYoHwp24SAel1hjX8enNuQAJaOAJvoZo9C/2rYwN +7JL7/hH+aBwojPRasnkTTBD50VYaEQa/DtTSRS/AddJ9T2DC9zwXbCzMkHJuUbtw +y0ofj/RJZaMIlL0oDtQ0IdssQzxKFdEOII44r9j3lTw3bT24rKC8gbgkqHmnmO86 +R3v0nnhVAj/JZFluy1PxNnsnCTcWj4mB9aHoTBuBZRiE5Qsuoq6OKTlqNQMx0Seg +dbScAf6fb0cj7Cx/b7xKgR+oLO9QhyhB4oICfmPCnhJVgYlXLi9A2MsTeON0suXi +7tGIR14HsBZ8qjW9nk/PXnAnZX7PM/GJ1Md80paPOk3ffpTPrEdB6vCxmyYn7Q2m +O2jcDTDUxb4847TX5EBP2fp3eX6pvbAJ6PC+RTMwXDOCYsfDDjAYypTrJZgatzqq +M+kprdhvThWr4NVddjnJrGYY0QYL5dFDGXJE5Czfxp+agZdjAyAJ9kZ9TDogJNQV +UrpY04er8TGieGUwphqn4kBqngoOn2PcNluc/qbLOs0fYoIqqvJzWpqlwQKGevsO +x5cmot6NWVamarV7IOHanJ59ECu4Na5KxyReLSFgFHx/HCp5PM27+5FjxAZv+ZUP +C0aWqG020r00k7e1RxA9sBjf7M3UXPYpY0Ip3ec4HEhvtjMjtuBtE35wBXotz8Cf +6WhTsiabQsiYcRrqZw4RZICU9v4nkJLvAyGDo0K+fI39EhtbzIJBW22QTlv03Zlj +lK03vgB0ZFyLe1RjKVBjyvJzM3c3BV3cEcuEeDpu7pLQApnCK0u+YunA0pDR0n1J +40MA +-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/keys/ec-pkcs8-sect571r1-named-v1-sha1-rc2-64.der b/src/test/resources/keys/ec-pkcs8-sect571r1-named-v1-sha1-rc2-64.der new file mode 100644 index 0000000..f968392 --- /dev/null +++ b/src/test/resources/keys/ec-pkcs8-sect571r1-named-v1-sha1-rc2-64.der Binary files differ
diff --git a/src/test/resources/keys/ec-prime256v1-named-pub.der b/src/test/resources/keys/ec-prime256v1-named-pub.der new file mode 100644 index 0000000..061fee3 --- /dev/null +++ b/src/test/resources/keys/ec-prime256v1-named-pub.der Binary files differ
diff --git a/src/test/resources/keys/ec-prime256v1-named-pub.pem b/src/test/resources/keys/ec-prime256v1-named-pub.pem new file mode 100644 index 0000000..e76a244 --- /dev/null +++ b/src/test/resources/keys/ec-prime256v1-named-pub.pem
@@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV0bfrxH9VzCilRisNe/yynt2n1+h +c/JktcTD5PK5eHFrAk3nUb7QQClQFWMrrfbB9mmWhKuMqym8uLDsbLtrow== +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/ec-secp224k1-explicit-pub.der b/src/test/resources/keys/ec-secp224k1-explicit-pub.der new file mode 100644 index 0000000..61ffe8d --- /dev/null +++ b/src/test/resources/keys/ec-secp224k1-explicit-pub.der Binary files differ
diff --git a/src/test/resources/keys/ec-secp224k1-explicit-pub.pem b/src/test/resources/keys/ec-secp224k1-explicit-pub.pem new file mode 100644 index 0000000..3563fd8 --- /dev/null +++ b/src/test/resources/keys/ec-secp224k1-explicit-pub.pem
@@ -0,0 +1,7 @@ +-----BEGIN PUBLIC KEY----- +MIHdMIGeBgcqhkjOPQIBMIGSAgEBMCgGByqGSM49AQECHQD///////////////// +//////////////7//+VtMAYEAQAEAQUEOQShRVszTfCZ3zD8KKFppGfp5HB1qQ9+ +ZQ62t6Rcfgif7X+6NEKCyvvW9+MZ98CwvVniykvbVW1hpQIdAQAAAAAAAAAAAAAA +AAAB3OjS7GGEyvCpcXafsfcCAQEDOgAEmGBV69k1liUgrvR8FJjR9Mr2D5hXhKMd +1LeHwZ7YAaPpCDWqElX01W4+rAH3qJIFkFGnXKFzTBM= +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keys/rsa-openssl-des-noheader.pem b/src/test/resources/keys/rsa-openssl-des-noheader.pem new file mode 100644 index 0000000..5cb574f --- /dev/null +++ b/src/test/resources/keys/rsa-openssl-des-noheader.pem
@@ -0,0 +1,16 @@ +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-CBC,C65E6ED4FD6921EF + +w9CNDiUxGP8ve2L/4M0vvfIwvwXmEfAYdVt2jFv5waqeZjrwq0eG8Xup1kSAHgaX +NOX/J3GVH0F8rlMdFDdOIT3+DgDFr5hV1Pcy8e5aI+8tP0VICXGeh8tPttFj5m7k +RupYtbDF6e71odLwe+9JvoWknUQYl/t7nQx/1GTa7ZKPdTKWq9r8HUJCCzAaG7xQ +e44C+G1r3C9TVoI/QpPUwWgiDs9scjQRnli83OTBQKHXzUceIKY2EdpukZdIlxUa +hRRXKCu+ZOxys4FDJzWgd/FEP9V6asIdVn07rDtIZX011hJLNmYaxw6/zls+gwSp +Q8zRs/lohWnEKELDOJXG6dfWzWsE4/kYZsD5Z/6+w5uOIMCynd+IhG+HLb1Cyu2M +gIi2o3EuWj7+RMcctNe55xs/Vm9sSlO+5cdXsfDEPrwwF69Gd4ArHRjTojQj+X0h +vXzKdgjEPsEqgaa/Ln0h7MT6R9dh2feHmDEFRb1eyA4v/79IhbGypdy639SdiRAI +//FpDrrZm+zlpoVtOpd6vj2YcPSIsMCHQ5osw7hlwt57o3pEyzqlR1O2zHGzngWk +09PzcVPm94Tza0pXxChJpk3Sn0cxpGgTpzLcjSOIj7ZWHZ/4kXGy5njs1/lHAMcY +mioZOKuIVNZVC92bbp/uiNQP4NoaGVTGyX0qwFo81+Nb6Z0xdYsNHjp1gW0lD7m/ +hhlMQT83WgApkEUcMhf9jjuN3EYQI8D07r2TbhJZ9Rc5DDlwFq04jkTrEMgDhULK +JzkncgTsrpWKC0bi4MB6ky7/gcUBTsPr7/zF2czstru8LSqc3Q61lg==
diff --git a/src/test/resources/keys/rsa-openssl-des.pem b/src/test/resources/keys/rsa-openssl-des.pem new file mode 100644 index 0000000..4551e02 --- /dev/null +++ b/src/test/resources/keys/rsa-openssl-des.pem
@@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-CBC,C65E6ED4FD6921EF + +w9CNDiUxGP8ve2L/4M0vvfIwvwXmEfAYdVt2jFv5waqeZjrwq0eG8Xup1kSAHgaX +NOX/J3GVH0F8rlMdFDdOIT3+DgDFr5hV1Pcy8e5aI+8tP0VICXGeh8tPttFj5m7k +RupYtbDF6e71odLwe+9JvoWknUQYl/t7nQx/1GTa7ZKPdTKWq9r8HUJCCzAaG7xQ +e44C+G1r3C9TVoI/QpPUwWgiDs9scjQRnli83OTBQKHXzUceIKY2EdpukZdIlxUa +hRRXKCu+ZOxys4FDJzWgd/FEP9V6asIdVn07rDtIZX011hJLNmYaxw6/zls+gwSp +Q8zRs/lohWnEKELDOJXG6dfWzWsE4/kYZsD5Z/6+w5uOIMCynd+IhG+HLb1Cyu2M +gIi2o3EuWj7+RMcctNe55xs/Vm9sSlO+5cdXsfDEPrwwF69Gd4ArHRjTojQj+X0h +vXzKdgjEPsEqgaa/Ln0h7MT6R9dh2feHmDEFRb1eyA4v/79IhbGypdy639SdiRAI +//FpDrrZm+zlpoVtOpd6vj2YcPSIsMCHQ5osw7hlwt57o3pEyzqlR1O2zHGzngWk +09PzcVPm94Tza0pXxChJpk3Sn0cxpGgTpzLcjSOIj7ZWHZ/4kXGy5njs1/lHAMcY +mioZOKuIVNZVC92bbp/uiNQP4NoaGVTGyX0qwFo81+Nb6Z0xdYsNHjp1gW0lD7m/ +hhlMQT83WgApkEUcMhf9jjuN3EYQI8D07r2TbhJZ9Rc5DDlwFq04jkTrEMgDhULK +JzkncgTsrpWKC0bi4MB6ky7/gcUBTsPr7/zF2czstru8LSqc3Q61lg== +-----END RSA PRIVATE KEY-----
diff --git a/src/test/resources/keys/rsa-openssl-des3.pem b/src/test/resources/keys/rsa-openssl-des3.pem new file mode 100644 index 0000000..3337dcd --- /dev/null +++ b/src/test/resources/keys/rsa-openssl-des3.pem
@@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,030694C414709D5A + +9RGwZOuXp57EMb+PNOH80HX3iprxbFkKH6y1wIddC1VmyohyVY3JMY2sCy0RfU+Z +/LSahlmq5fwmP9LdFvnxa9jj0A/9fbytUvg6AN3cHHalgFDEKXs/vXAu3katirNa +t0wVqWKPARJSHHpNy7Zd2XyUN8i+dyWxRU/K/324K9fcRr51oiiEpl2H3DDlBqJE +VY1a/odRUKm1YGrsN1KvZw91iCv2QfeziqCw/HQ/ehAiEqXXGpxi2zw2tPCuJM1S +Qi7VAPXmyORInm0YWS0kLkNMUKjJW8uka9Mv7Fz23pQvTipHIul9XTEZWe46tMTv +ueWdkqWBpmgP9is9NOEdZx+iJigJjgSeNApZjLF9HKkf9/45lcbGQS/FtUXnp1Qf +MMT8V+CR5wH87kLOAJEkkpQkYKWOw52xlI+WQZrtZe7GsVo6uXRS+Yp1xyGHYYok +AbTh0z+JSuViDXE80G0zn2hMaAcC/zk4UXcmCbXNDhv4uIcT6pE6n/HCbzbMUVYF +rOhT/XhA4wZGeGp26J4COqsJHPTBgHbsmKrONwCogm3yCx5BndRlE+tKykBqUFke +/AliECal85HfoW+VL5ojukB7C+Wy62ioG2qYepfEXKJI+j14GkQrd4bbn7huj14S +5FdXxjoWe8Ld6ZOJO8QKF8vMpnNp87hWaXXRYrfvKyEBfvUcnU3f8OBVdsDy3LX8 +HPyCGNWBsuRdhxsuFVSgxP1COQ6FdOGYvZx36JXC+nRFxSd1n9BxZf5qqEdTAH2w +nQiwJVbVtw/8d+vqpaavWn9/v9c+9sTrYUwoajBBOoEWiSOId+JHkdOnuAgE2OvL +DtlzeB64VK6e4AU+6OSZzufZW1XiCRjYqEB+W0s8hYj5EB+W9hRIaEfA1NMMfsyG +irCsIZYQD7lP+yGUICcvk2eNiU4ZwcViBq1Rv8JLimFn1X+6je2dcRmtmAby+OWA +cx3WpnZ04mbI9AnrILa7qnrE7GqLtOpFDwtsQ5kc0//7zadPCg34kf96WEVHNCfW +EZ0FKno0PtYbxh8bU0YnUpi3IczpbCW1e2BjvIskR80MQaz2ZSEkbsV0+uNBzOia +8Cjmo1JrdpfeE5NSiEZo5p7QqOEP3t3kL2I0mAZ8qhhN/NUTuAQAP6Lu1bncRyUT +MXiHhdeOJ6LvWZ0+YehlG5sOpOfVgRKeyGI8HjTVFaff9RdTuYPEZAlDkxgwsHuF +3isenNHhH3LMRzlcjTzy19q2DUxIOxq1Nm8fkf5448fD9bNlXHmVuLBUe8m2JfAv +JcP9a0PnCGPuvcSyQqKbtXkydk+4MJrB/55gEb/uClkttrCJXUKI6LvrqAH9+CNI +Jn9YZ3G/ApcTsA3h/kQNoZcIPUli/bRr6wIBs1Q3VBoUEJK1f27K2LlmqsdWVqwu +gMBJwIGtUmp32nVAjy+Tkb2Xexe6V7r8Rd1oiy/ewlpU6xTo68UpATeb8PmsCk3d +DNpqA/ykaWz90UsSxfhadxocRxNKjZQhJC/xkDmG1x7EUGzPR3ESrMsTsP+WGLWl +vORgbbVMakP6bomJLAREkfVmtQnLtSuR4ZaU8HkBKiX0Rw10n7TROQ== +-----END RSA PRIVATE KEY-----
diff --git a/src/test/resources/keys/rsa-openssl-nopass.der b/src/test/resources/keys/rsa-openssl-nopass.der new file mode 100644 index 0000000..f4da603 --- /dev/null +++ b/src/test/resources/keys/rsa-openssl-nopass.der Binary files differ
diff --git a/src/test/resources/keys/rsa-openssl-nopass.pem b/src/test/resources/keys/rsa-openssl-nopass.pem new file mode 100644 index 0000000..c635117 --- /dev/null +++ b/src/test/resources/keys/rsa-openssl-nopass.pem
@@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDkYYrsisNd/+lNvWR3ridkbPg0VJKc9juQAnSrdh3rV99wBtg0 +SQeao5MUPwsmLG3/aQmzNhaFNPeWHcQZDmqZO1QLTB7rFX2xZn3t4uD2+yn764ln +L10qliZmGCbs6wB4LLrnR50hWYx3MgisdeHe4uOc18FVGvGs8+2k0Im/yQIDAQAB +AoGAbGEx6XEsp6YzlHXlgLo/7XOdElB0R1K/D2dq8JecOTw5R9OntKBXoHYx7TDB +3LrG9KdnOAnaBBsPx8iWcAGcafezh9xvKiLANOju3lAgfg+6c8AE941zehfqMk3V +nxstWYdMT5G5MmgxMEcwDU1iQYIHQKqHQKsDLBywRd858MUCQQD9YNC9D5qxq9WD +ClJxD2AkAVnG7IKylnfNX9QfPxkkhCyzMAQTHsbGBi14FHbLlzqskcAS3o2qg+9R +wY7ldd5nAkEA5r6C3uDFFcKRbW8NpRv0/NZPCVY7Fby3timVAmWVsE6KLNIF5kQG +sD0M/COAmVBrsbERRyuys9XuTDoCHjYyTwJAdwBNiT4W7XNC6DSk26zY/pAT1jWm +fLHmunJTcgl0iY36YH4gq8o8mSy1ljwmPyBb0kjx6OrVpkwozkEWF/bv6QJBALg1 +n4UPLEgS7MbQwbPufcbb0H1DuifAYWmsCKnBL59xFB50DQGnjS9ljdg8/41mBpP1 +KDYJTMEFKRjxtn8oBUECQEq9Rhvw4hmi0grQMnftbYAtGcRxEbngAZTnNtSC7Dyy +5453+by1CM1cI6FE39rCFW1FPHM36BW7GFaVqjQMk3M= +-----END RSA PRIVATE KEY-----
diff --git a/src/test/resources/keys/rsa-pkcs8-nopass-noheader.pem b/src/test/resources/keys/rsa-pkcs8-nopass-noheader.pem new file mode 100644 index 0000000..48dbfa4 --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-nopass-noheader.pem
@@ -0,0 +1,14 @@ +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAORhiuyKw13/6U29 +ZHeuJ2Rs+DRUkpz2O5ACdKt2HetX33AG2DRJB5qjkxQ/CyYsbf9pCbM2FoU095Yd +xBkOapk7VAtMHusVfbFmfe3i4Pb7KfvriWcvXSqWJmYYJuzrAHgsuudHnSFZjHcy +CKx14d7i45zXwVUa8azz7aTQib/JAgMBAAECgYBsYTHpcSynpjOUdeWAuj/tc50S +UHRHUr8PZ2rwl5w5PDlH06e0oFegdjHtMMHcusb0p2c4CdoEGw/HyJZwAZxp97OH +3G8qIsA06O7eUCB+D7pzwAT3jXN6F+oyTdWfGy1Zh0xPkbkyaDEwRzANTWJBggdA +qodAqwMsHLBF3znwxQJBAP1g0L0PmrGr1YMKUnEPYCQBWcbsgrKWd81f1B8/GSSE +LLMwBBMexsYGLXgUdsuXOqyRwBLejaqD71HBjuV13mcCQQDmvoLe4MUVwpFtbw2l +G/T81k8JVjsVvLe2KZUCZZWwToos0gXmRAawPQz8I4CZUGuxsRFHK7Kz1e5MOgIe +NjJPAkB3AE2JPhbtc0LoNKTbrNj+kBPWNaZ8sea6clNyCXSJjfpgfiCryjyZLLWW +PCY/IFvSSPHo6tWmTCjOQRYX9u/pAkEAuDWfhQ8sSBLsxtDBs+59xtvQfUO6J8Bh +aawIqcEvn3EUHnQNAaeNL2WN2Dz/jWYGk/UoNglMwQUpGPG2fygFQQJASr1GG/Di +GaLSCtAyd+1tgC0ZxHERueABlOc21ILsPLLnjnf5vLUIzVwjoUTf2sIVbUU8czfo +FbsYVpWqNAyTcw==
diff --git a/src/test/resources/keys/rsa-pkcs8-nopass.der b/src/test/resources/keys/rsa-pkcs8-nopass.der new file mode 100644 index 0000000..5a7f1d4 --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-nopass.der Binary files differ
diff --git a/src/test/resources/keys/rsa-pkcs8-nopass.pem b/src/test/resources/keys/rsa-pkcs8-nopass.pem new file mode 100644 index 0000000..d58631b --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-nopass.pem
@@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAORhiuyKw13/6U29 +ZHeuJ2Rs+DRUkpz2O5ACdKt2HetX33AG2DRJB5qjkxQ/CyYsbf9pCbM2FoU095Yd +xBkOapk7VAtMHusVfbFmfe3i4Pb7KfvriWcvXSqWJmYYJuzrAHgsuudHnSFZjHcy +CKx14d7i45zXwVUa8azz7aTQib/JAgMBAAECgYBsYTHpcSynpjOUdeWAuj/tc50S +UHRHUr8PZ2rwl5w5PDlH06e0oFegdjHtMMHcusb0p2c4CdoEGw/HyJZwAZxp97OH +3G8qIsA06O7eUCB+D7pzwAT3jXN6F+oyTdWfGy1Zh0xPkbkyaDEwRzANTWJBggdA +qodAqwMsHLBF3znwxQJBAP1g0L0PmrGr1YMKUnEPYCQBWcbsgrKWd81f1B8/GSSE +LLMwBBMexsYGLXgUdsuXOqyRwBLejaqD71HBjuV13mcCQQDmvoLe4MUVwpFtbw2l +G/T81k8JVjsVvLe2KZUCZZWwToos0gXmRAawPQz8I4CZUGuxsRFHK7Kz1e5MOgIe +NjJPAkB3AE2JPhbtc0LoNKTbrNj+kBPWNaZ8sea6clNyCXSJjfpgfiCryjyZLLWW +PCY/IFvSSPHo6tWmTCjOQRYX9u/pAkEAuDWfhQ8sSBLsxtDBs+59xtvQfUO6J8Bh +aawIqcEvn3EUHnQNAaeNL2WN2Dz/jWYGk/UoNglMwQUpGPG2fygFQQJASr1GG/Di +GaLSCtAyd+1tgC0ZxHERueABlOc21ILsPLLnjnf5vLUIzVwjoUTf2sIVbUU8czfo +FbsYVpWqNAyTcw== +-----END PRIVATE KEY-----
diff --git a/src/test/resources/keys/rsa-pkcs8-v1-md5-des.der b/src/test/resources/keys/rsa-pkcs8-v1-md5-des.der new file mode 100644 index 0000000..4a34039 --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-v1-md5-des.der Binary files differ
diff --git a/src/test/resources/keys/rsa-pkcs8-v1-md5-des.pem b/src/test/resources/keys/rsa-pkcs8-v1-md5-des.pem new file mode 100644 index 0000000..936e555 --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-v1-md5-des.pem
@@ -0,0 +1,17 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICoTAbBgkqhkiG9w0BBQMwDgQIGhFw3dgb3b8CAggABIICgO7i7wykRw78u/Nx +swPSeJqV4Gvw38t4jGQr5arL8+z5T85URGRI102+pGBf7NtJP6U10rf24+KZL3jb +9ZhhKDr90dTZPl5XhCFygUNTsusenoW53Tf1Nh2ZbNPB72keckG3or79qgFD5ddp +ZUq9HhOXksyqZm+SV8qgewXFct+4+PQMK2/CN7qF05Uw9LraTZburh2TnlA5tdwT +RbPPnj+vYBLJ6liJtGnTxVq6oS9CpGK00NgvFn4OvsW8+nGrTKCvOACvdIrxHiOk +XCS5Inii58p/bo/v90eajxza+1fWlhDEi0I2+UZrxn1GpcZUKUGsjSMze8bLtUa4 +AMzYKi1nds1bg4FlgLUS58B2x4qwlLcliGYg9bR2iuiHKp21Yi9jILo+Rqc38/a+ +kqI1Gks0RA2ifOrATEogZr2ivK3hf2vgzgIM0UUBQJaddFStIxndOV4c3RDJX0lM +5SbjvS6HHa6SF2lfLcrV6d4G3VzMIIhiwquREmy1t1CiYM3YhiEVBnB7EgLf2Hm6 +5ul/FmDqg1cdie8nToyTvDlOSC2Qeql3Zo7RW88dupedkrAOOIW739yoFS7Oo1Rd +e3wh7P1GRxfvrJGZWugG1X4u44zis4iA0sxoAoCXo0qbYinb7iCr8W7JLvN9HB/G +Mr+/hIuNKGVfTZqbP0Lcq978kCT/BZuH706K1enVL392lsTN3fUcdhHR+0o/Ym61 +SNsdc0wC5jLJ0G8hDJ/o/iUAIvj0iN/cJQ+kJiKaQiDMLgkDtDlgi4x8WaqB+ZWH +oZtrQ2VcWBy4Octr9w/0qkbis3NXUJHZ3lCe5srU/4ofOCZzPx2jdJ5GvHqr8MOe +mye2GBQ= +-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/keys/rsa-pkcs8-v1-md5-rc2-64.der b/src/test/resources/keys/rsa-pkcs8-v1-md5-rc2-64.der new file mode 100644 index 0000000..20dddb1 --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-v1-md5-rc2-64.der Binary files differ
diff --git a/src/test/resources/keys/rsa-pkcs8-v2-aes256-noheader.pem b/src/test/resources/keys/rsa-pkcs8-v2-aes256-noheader.pem new file mode 100644 index 0000000..712fad2 --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-v2-aes256-noheader.pem
@@ -0,0 +1,16 @@ +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQICbSpN5NE6iYCAggA +MB0GCWCGSAFlAwQBKgQQsTIY+PyJiePgRY0nL+SD1wSCAoCmvaOpL9cSSYtpLD/S +3Xc21VU/lu6O0FkT5fs5gxFkbnrp5ykPpdhAvlPGVAtoGDN/WnDBrYET+JCDxB12 +8PsrAW5vd9sXs4YjQfdIrUDuqYllEbNo6Rp/8hvSroxT+x7RDtwAIB2eJMOjEZqN +onAIHhCMf6zz+K/zXONxnCw0APgr6gBMlNn7JvElfOsCTOWjk7x3BPid7qoR4pBt +EAtjsGpo6xhhc+8yQt4t1M6o8476Th/pqm1WB03LMyjPkXJXnLoP0GHXhVvLYg5y ++EH9S3FLHz4SiIYM2O1uG7ji/4ByoPOE/xvuNgtNLVtNrRdgMrGQukntBbbiUZL6 +qjr4Qa6EHrivXov8w5YAVrqSjfA7S7cW+qgKbpmnB+IDvClkpu24gvsqKdCThgLN +HI+z9VnoBVbj+We+iKbQEytNXGiH8kls2aY5U0okii8ZWnTu1OXwyzl+vVOnQ5qU +MYPmzEVPpmmArZTc1gF7udC4jwJcPnzzqFAzoc6ttQ2lX3JsZ9Ms30/WmQqEFEXj +DJABY2r5Qkli+7ViGbXC9AB6+/yW1RahNe9kenSDUcZuGGlWlXc2Nv8Mx73FeYVc +o24JfTiIzHlTGlOF3DAB/sOghDgXDkOd/C0+ooslih4hnfdE/0P0DGCgnuYWkk0/ +7Xz/ztRMBijn6kfm2q6EN4RDH98Tro1i+CCyh2T6LX1gC8jiJKcBcKPTuZ5Kd7U6 +1coUyluOdb9wJ2sCvan/oKbJPaTVp+AdTLLtGKtg8sCeuw73AdSv2mgRryL18dOw +6oOWwXsiSQ8m+mFJgNW36BWmDIJGxej9SoO7gv+Qvqr2Ip72on82EOtgy3cUwZ9l +w1oB
diff --git a/src/test/resources/keys/rsa-pkcs8-v2-aes256.der b/src/test/resources/keys/rsa-pkcs8-v2-aes256.der new file mode 100644 index 0000000..ef50081 --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-v2-aes256.der Binary files differ
diff --git a/src/test/resources/keys/rsa-pkcs8-v2-aes256.pem b/src/test/resources/keys/rsa-pkcs8-v2-aes256.pem new file mode 100644 index 0000000..3f7a018 --- /dev/null +++ b/src/test/resources/keys/rsa-pkcs8-v2-aes256.pem
@@ -0,0 +1,18 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIICzzBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQICbSpN5NE6iYCAggA +MB0GCWCGSAFlAwQBKgQQsTIY+PyJiePgRY0nL+SD1wSCAoCmvaOpL9cSSYtpLD/S +3Xc21VU/lu6O0FkT5fs5gxFkbnrp5ykPpdhAvlPGVAtoGDN/WnDBrYET+JCDxB12 +8PsrAW5vd9sXs4YjQfdIrUDuqYllEbNo6Rp/8hvSroxT+x7RDtwAIB2eJMOjEZqN +onAIHhCMf6zz+K/zXONxnCw0APgr6gBMlNn7JvElfOsCTOWjk7x3BPid7qoR4pBt +EAtjsGpo6xhhc+8yQt4t1M6o8476Th/pqm1WB03LMyjPkXJXnLoP0GHXhVvLYg5y ++EH9S3FLHz4SiIYM2O1uG7ji/4ByoPOE/xvuNgtNLVtNrRdgMrGQukntBbbiUZL6 +qjr4Qa6EHrivXov8w5YAVrqSjfA7S7cW+qgKbpmnB+IDvClkpu24gvsqKdCThgLN +HI+z9VnoBVbj+We+iKbQEytNXGiH8kls2aY5U0okii8ZWnTu1OXwyzl+vVOnQ5qU +MYPmzEVPpmmArZTc1gF7udC4jwJcPnzzqFAzoc6ttQ2lX3JsZ9Ms30/WmQqEFEXj +DJABY2r5Qkli+7ViGbXC9AB6+/yW1RahNe9kenSDUcZuGGlWlXc2Nv8Mx73FeYVc +o24JfTiIzHlTGlOF3DAB/sOghDgXDkOd/C0+ooslih4hnfdE/0P0DGCgnuYWkk0/ +7Xz/ztRMBijn6kfm2q6EN4RDH98Tro1i+CCyh2T6LX1gC8jiJKcBcKPTuZ5Kd7U6 +1coUyluOdb9wJ2sCvan/oKbJPaTVp+AdTLLtGKtg8sCeuw73AdSv2mgRryL18dOw +6oOWwXsiSQ8m+mFJgNW36BWmDIJGxej9SoO7gv+Qvqr2Ip72on82EOtgy3cUwZ9l +w1oB +-----END ENCRYPTED PRIVATE KEY-----
diff --git a/src/test/resources/keys/rsa-pub.der b/src/test/resources/keys/rsa-pub.der new file mode 100644 index 0000000..f62dba8 --- /dev/null +++ b/src/test/resources/keys/rsa-pub.der Binary files differ
diff --git a/src/test/resources/keys/rsa-pub.pem b/src/test/resources/keys/rsa-pub.pem new file mode 100644 index 0000000..bd22ce2 --- /dev/null +++ b/src/test/resources/keys/rsa-pub.pem
@@ -0,0 +1,6 @@ +-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDkYYrsisNd/+lNvWR3ridkbPg0 +VJKc9juQAnSrdh3rV99wBtg0SQeao5MUPwsmLG3/aQmzNhaFNPeWHcQZDmqZO1QL +TB7rFX2xZn3t4uD2+yn764lnL10qliZmGCbs6wB4LLrnR50hWYx3MgisdeHe4uOc +18FVGvGs8+2k0Im/yQIDAQAB +-----END PUBLIC KEY-----
diff --git a/src/test/resources/keystores/alpha.p12 b/src/test/resources/keystores/alpha.p12 new file mode 100644 index 0000000..0245c8c --- /dev/null +++ b/src/test/resources/keystores/alpha.p12 Binary files differ
diff --git a/src/test/resources/keystores/cipher-bean.jceks b/src/test/resources/keystores/cipher-bean.jceks new file mode 100644 index 0000000..1723ccb --- /dev/null +++ b/src/test/resources/keystores/cipher-bean.jceks Binary files differ
diff --git a/src/test/resources/keystores/factory-bean.jceks b/src/test/resources/keystores/factory-bean.jceks new file mode 100644 index 0000000..305bcc3 --- /dev/null +++ b/src/test/resources/keystores/factory-bean.jceks Binary files differ
diff --git a/src/test/resources/keystores/keystore.bks b/src/test/resources/keystores/keystore.bks new file mode 100644 index 0000000..b5bc01a --- /dev/null +++ b/src/test/resources/keystores/keystore.bks Binary files differ
diff --git a/src/test/resources/keystores/keystore.jceks b/src/test/resources/keystores/keystore.jceks new file mode 100644 index 0000000..aaddfd7 --- /dev/null +++ b/src/test/resources/keystores/keystore.jceks Binary files differ
diff --git a/src/test/resources/keystores/keystore.jks b/src/test/resources/keystores/keystore.jks new file mode 100644 index 0000000..3ca2902 --- /dev/null +++ b/src/test/resources/keystores/keystore.jks Binary files differ
diff --git a/src/test/resources/keystores/keystore.p12 b/src/test/resources/keystores/keystore.p12 new file mode 100644 index 0000000..2e4447a --- /dev/null +++ b/src/test/resources/keystores/keystore.p12 Binary files differ
diff --git a/src/test/resources/plaintexts/lorem-1190.txt b/src/test/resources/plaintexts/lorem-1190.txt new file mode 100644 index 0000000..d018f3d --- /dev/null +++ b/src/test/resources/plaintexts/lorem-1190.txt
@@ -0,0 +1,12 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nunc leo, adipiscing et ultrices a, fermentum eget +quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum mattis, risus tempus ultricies luctus, nisi +elit consectetur velit, lacinia fringilla urna lectus sit amet nunc. Nunc nec dictum arcu. Duis id aliquet nulla. In ut +augue elit. Phasellus semper ut lacus ac sagittis. Nunc elementum, dui nec ornare bibendum, ligula ante vulputate nisi, +in imperdiet lectus eros rhoncus augue. Maecenas non interdum metus, vitae tempor mi. + +Sed rutrum, lorem quis varius tincidunt, augue elit accumsan nisl, sit amet gravida odio massa ultrices urna. Morbi +suscipit mauris nec eros iaculis, vel tempus mauris bibendum. Phasellus tempor enim eget commodo gravida. Sed et risus +quis orci eleifend ullamcorper. Ut et nisi et sem convallis sodales. Proin nec nibh non metus condimentum vehicula. +Maecenas ut felis tellus. Cras id diam diam. Nam eu ultricies nunc, in fermentum tellus. Donec et libero a augue +fermentum congue. Nulla vitae augue arcu. Donec mollis faucibus est, ac consectetur lectus aliquet at. Class aptent +taciti sociosqu ad litora torquent per conubia nost \ No newline at end of file
diff --git a/src/test/resources/plaintexts/lorem-1190.txt.b64 b/src/test/resources/plaintexts/lorem-1190.txt.b64 new file mode 100644 index 0000000..540b45b --- /dev/null +++ b/src/test/resources/plaintexts/lorem-1190.txt.b64
@@ -0,0 +1,25 @@ +TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2Np +bmcgZWxpdC4gQWVuZWFuIG51bmMgbGVvLCBhZGlwaXNjaW5nIGV0IHVsdHJpY2Vz +IGEsIGZlcm1lbnR1bSBlZ2V0IApxdWFtLiBMb3JlbSBpcHN1bSBkb2xvciBzaXQg +YW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzY2luZyBlbGl0LiBWZXN0aWJ1bHVtIG1h +dHRpcywgcmlzdXMgdGVtcHVzIHVsdHJpY2llcyBsdWN0dXMsIG5pc2kgCmVsaXQg +Y29uc2VjdGV0dXIgdmVsaXQsIGxhY2luaWEgZnJpbmdpbGxhIHVybmEgbGVjdHVz +IHNpdCBhbWV0IG51bmMuIE51bmMgbmVjIGRpY3R1bSBhcmN1LiBEdWlzIGlkIGFs +aXF1ZXQgbnVsbGEuIEluIHV0IAphdWd1ZSBlbGl0LiBQaGFzZWxsdXMgc2VtcGVy +IHV0IGxhY3VzIGFjIHNhZ2l0dGlzLiBOdW5jIGVsZW1lbnR1bSwgZHVpIG5lYyBv +cm5hcmUgYmliZW5kdW0sIGxpZ3VsYSBhbnRlIHZ1bHB1dGF0ZSBuaXNpLCAKaW4g +aW1wZXJkaWV0IGxlY3R1cyBlcm9zIHJob25jdXMgYXVndWUuIE1hZWNlbmFzIG5v +biBpbnRlcmR1bSBtZXR1cywgdml0YWUgdGVtcG9yIG1pLgoKU2VkIHJ1dHJ1bSwg +bG9yZW0gcXVpcyB2YXJpdXMgdGluY2lkdW50LCBhdWd1ZSBlbGl0IGFjY3Vtc2Fu +IG5pc2wsIHNpdCBhbWV0IGdyYXZpZGEgb2RpbyBtYXNzYSB1bHRyaWNlcyB1cm5h +LiBNb3JiaSAKc3VzY2lwaXQgbWF1cmlzIG5lYyBlcm9zIGlhY3VsaXMsIHZlbCB0 +ZW1wdXMgbWF1cmlzIGJpYmVuZHVtLiBQaGFzZWxsdXMgdGVtcG9yIGVuaW0gZWdl +dCBjb21tb2RvIGdyYXZpZGEuIFNlZCBldCByaXN1cyAKcXVpcyBvcmNpIGVsZWlm +ZW5kIHVsbGFtY29ycGVyLiBVdCBldCBuaXNpIGV0IHNlbSBjb252YWxsaXMgc29k +YWxlcy4gUHJvaW4gbmVjIG5pYmggbm9uIG1ldHVzIGNvbmRpbWVudHVtIHZlaGlj +dWxhLiAKTWFlY2VuYXMgdXQgZmVsaXMgdGVsbHVzLiBDcmFzIGlkIGRpYW0gZGlh +bS4gTmFtIGV1IHVsdHJpY2llcyBudW5jLCBpbiBmZXJtZW50dW0gdGVsbHVzLiBE +b25lYyBldCBsaWJlcm8gYSBhdWd1ZSAKZmVybWVudHVtIGNvbmd1ZS4gTnVsbGEg +dml0YWUgYXVndWUgYXJjdS4gRG9uZWMgbW9sbGlzIGZhdWNpYnVzIGVzdCwgYWMg +Y29uc2VjdGV0dXIgbGVjdHVzIGFsaXF1ZXQgYXQuIENsYXNzIGFwdGVudCAKdGFj +aXRpIHNvY2lvc3F1IGFkIGxpdG9yYSB0b3JxdWVudCBwZXIgY29udWJpYSBub3N0
diff --git a/src/test/resources/plaintexts/lorem-1190.txt.b64.crlf b/src/test/resources/plaintexts/lorem-1190.txt.b64.crlf new file mode 100644 index 0000000..d4e0736 --- /dev/null +++ b/src/test/resources/plaintexts/lorem-1190.txt.b64.crlf
@@ -0,0 +1,25 @@ +TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2Np +bmcgZWxpdC4gQWVuZWFuIG51bmMgbGVvLCBhZGlwaXNjaW5nIGV0IHVsdHJpY2Vz +IGEsIGZlcm1lbnR1bSBlZ2V0IApxdWFtLiBMb3JlbSBpcHN1bSBkb2xvciBzaXQg +YW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzY2luZyBlbGl0LiBWZXN0aWJ1bHVtIG1h +dHRpcywgcmlzdXMgdGVtcHVzIHVsdHJpY2llcyBsdWN0dXMsIG5pc2kgCmVsaXQg +Y29uc2VjdGV0dXIgdmVsaXQsIGxhY2luaWEgZnJpbmdpbGxhIHVybmEgbGVjdHVz +IHNpdCBhbWV0IG51bmMuIE51bmMgbmVjIGRpY3R1bSBhcmN1LiBEdWlzIGlkIGFs +aXF1ZXQgbnVsbGEuIEluIHV0IAphdWd1ZSBlbGl0LiBQaGFzZWxsdXMgc2VtcGVy +IHV0IGxhY3VzIGFjIHNhZ2l0dGlzLiBOdW5jIGVsZW1lbnR1bSwgZHVpIG5lYyBv +cm5hcmUgYmliZW5kdW0sIGxpZ3VsYSBhbnRlIHZ1bHB1dGF0ZSBuaXNpLCAKaW4g +aW1wZXJkaWV0IGxlY3R1cyBlcm9zIHJob25jdXMgYXVndWUuIE1hZWNlbmFzIG5v +biBpbnRlcmR1bSBtZXR1cywgdml0YWUgdGVtcG9yIG1pLgoKU2VkIHJ1dHJ1bSwg +bG9yZW0gcXVpcyB2YXJpdXMgdGluY2lkdW50LCBhdWd1ZSBlbGl0IGFjY3Vtc2Fu +IG5pc2wsIHNpdCBhbWV0IGdyYXZpZGEgb2RpbyBtYXNzYSB1bHRyaWNlcyB1cm5h +LiBNb3JiaSAKc3VzY2lwaXQgbWF1cmlzIG5lYyBlcm9zIGlhY3VsaXMsIHZlbCB0 +ZW1wdXMgbWF1cmlzIGJpYmVuZHVtLiBQaGFzZWxsdXMgdGVtcG9yIGVuaW0gZWdl +dCBjb21tb2RvIGdyYXZpZGEuIFNlZCBldCByaXN1cyAKcXVpcyBvcmNpIGVsZWlm +ZW5kIHVsbGFtY29ycGVyLiBVdCBldCBuaXNpIGV0IHNlbSBjb252YWxsaXMgc29k +YWxlcy4gUHJvaW4gbmVjIG5pYmggbm9uIG1ldHVzIGNvbmRpbWVudHVtIHZlaGlj +dWxhLiAKTWFlY2VuYXMgdXQgZmVsaXMgdGVsbHVzLiBDcmFzIGlkIGRpYW0gZGlh +bS4gTmFtIGV1IHVsdHJpY2llcyBudW5jLCBpbiBmZXJtZW50dW0gdGVsbHVzLiBE +b25lYyBldCBsaWJlcm8gYSBhdWd1ZSAKZmVybWVudHVtIGNvbmd1ZS4gTnVsbGEg +dml0YWUgYXVndWUgYXJjdS4gRG9uZWMgbW9sbGlzIGZhdWNpYnVzIGVzdCwgYWMg +Y29uc2VjdGV0dXIgbGVjdHVzIGFsaXF1ZXQgYXQuIENsYXNzIGFwdGVudCAKdGFj +aXRpIHNvY2lvc3F1IGFkIGxpdG9yYSB0b3JxdWVudCBwZXIgY29udWJpYSBub3N0
diff --git a/src/test/resources/plaintexts/lorem-1200.txt b/src/test/resources/plaintexts/lorem-1200.txt new file mode 100644 index 0000000..0eb4195 --- /dev/null +++ b/src/test/resources/plaintexts/lorem-1200.txt
@@ -0,0 +1,12 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean nunc leo, adipiscing et ultrices a, fermentum eget +quam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum mattis, risus tempus ultricies luctus, nisi +elit consectetur velit, lacinia fringilla urna lectus sit amet nunc. Nunc nec dictum arcu. Duis id aliquet nulla. In ut +augue elit. Phasellus semper ut lacus ac sagittis. Nunc elementum, dui nec ornare bibendum, ligula ante vulputate nisi, +in imperdiet lectus eros rhoncus augue. Maecenas non interdum metus, vitae tempor mi. + +Sed rutrum, lorem quis varius tincidunt, augue elit accumsan nisl, sit amet gravida odio massa ultrices urna. Morbi +suscipit mauris nec eros iaculis, vel tempus mauris bibendum. Phasellus tempor enim eget commodo gravida. Sed et risus +quis orci eleifend ullamcorper. Ut et nisi et sem convallis sodales. Proin nec nibh non metus condimentum vehicula. +Maecenas ut felis tellus. Cras id diam diam. Nam eu ultricies nunc, in fermentum tellus. Donec et libero a augue +fermentum congue. Nulla vitae augue arcu. Donec mollis faucibus est, ac consectetur lectus aliquet at. Class aptent +taciti sociosqu ad litora torquent per conubia nostra posuere. \ No newline at end of file
diff --git a/src/test/resources/plaintexts/lorem-1200.txt.b64 b/src/test/resources/plaintexts/lorem-1200.txt.b64 new file mode 100644 index 0000000..94079e4 --- /dev/null +++ b/src/test/resources/plaintexts/lorem-1200.txt.b64
@@ -0,0 +1,26 @@ +TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2Np +bmcgZWxpdC4gQWVuZWFuIG51bmMgbGVvLCBhZGlwaXNjaW5nIGV0IHVsdHJpY2Vz +IGEsIGZlcm1lbnR1bSBlZ2V0IApxdWFtLiBMb3JlbSBpcHN1bSBkb2xvciBzaXQg +YW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzY2luZyBlbGl0LiBWZXN0aWJ1bHVtIG1h +dHRpcywgcmlzdXMgdGVtcHVzIHVsdHJpY2llcyBsdWN0dXMsIG5pc2kgCmVsaXQg +Y29uc2VjdGV0dXIgdmVsaXQsIGxhY2luaWEgZnJpbmdpbGxhIHVybmEgbGVjdHVz +IHNpdCBhbWV0IG51bmMuIE51bmMgbmVjIGRpY3R1bSBhcmN1LiBEdWlzIGlkIGFs +aXF1ZXQgbnVsbGEuIEluIHV0IAphdWd1ZSBlbGl0LiBQaGFzZWxsdXMgc2VtcGVy +IHV0IGxhY3VzIGFjIHNhZ2l0dGlzLiBOdW5jIGVsZW1lbnR1bSwgZHVpIG5lYyBv +cm5hcmUgYmliZW5kdW0sIGxpZ3VsYSBhbnRlIHZ1bHB1dGF0ZSBuaXNpLCAKaW4g +aW1wZXJkaWV0IGxlY3R1cyBlcm9zIHJob25jdXMgYXVndWUuIE1hZWNlbmFzIG5v +biBpbnRlcmR1bSBtZXR1cywgdml0YWUgdGVtcG9yIG1pLgoKU2VkIHJ1dHJ1bSwg +bG9yZW0gcXVpcyB2YXJpdXMgdGluY2lkdW50LCBhdWd1ZSBlbGl0IGFjY3Vtc2Fu +IG5pc2wsIHNpdCBhbWV0IGdyYXZpZGEgb2RpbyBtYXNzYSB1bHRyaWNlcyB1cm5h +LiBNb3JiaSAKc3VzY2lwaXQgbWF1cmlzIG5lYyBlcm9zIGlhY3VsaXMsIHZlbCB0 +ZW1wdXMgbWF1cmlzIGJpYmVuZHVtLiBQaGFzZWxsdXMgdGVtcG9yIGVuaW0gZWdl +dCBjb21tb2RvIGdyYXZpZGEuIFNlZCBldCByaXN1cyAKcXVpcyBvcmNpIGVsZWlm +ZW5kIHVsbGFtY29ycGVyLiBVdCBldCBuaXNpIGV0IHNlbSBjb252YWxsaXMgc29k +YWxlcy4gUHJvaW4gbmVjIG5pYmggbm9uIG1ldHVzIGNvbmRpbWVudHVtIHZlaGlj +dWxhLiAKTWFlY2VuYXMgdXQgZmVsaXMgdGVsbHVzLiBDcmFzIGlkIGRpYW0gZGlh +bS4gTmFtIGV1IHVsdHJpY2llcyBudW5jLCBpbiBmZXJtZW50dW0gdGVsbHVzLiBE +b25lYyBldCBsaWJlcm8gYSBhdWd1ZSAKZmVybWVudHVtIGNvbmd1ZS4gTnVsbGEg +dml0YWUgYXVndWUgYXJjdS4gRG9uZWMgbW9sbGlzIGZhdWNpYnVzIGVzdCwgYWMg +Y29uc2VjdGV0dXIgbGVjdHVzIGFsaXF1ZXQgYXQuIENsYXNzIGFwdGVudCAKdGFj +aXRpIHNvY2lvc3F1IGFkIGxpdG9yYSB0b3JxdWVudCBwZXIgY29udWJpYSBub3N0 +cmEgcG9zdWVyZS4=
diff --git a/src/test/resources/plaintexts/lorem-1200.txt.b64.crlf b/src/test/resources/plaintexts/lorem-1200.txt.b64.crlf new file mode 100644 index 0000000..f9f5bc9 --- /dev/null +++ b/src/test/resources/plaintexts/lorem-1200.txt.b64.crlf
@@ -0,0 +1,26 @@ +TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2Np +bmcgZWxpdC4gQWVuZWFuIG51bmMgbGVvLCBhZGlwaXNjaW5nIGV0IHVsdHJpY2Vz +IGEsIGZlcm1lbnR1bSBlZ2V0IApxdWFtLiBMb3JlbSBpcHN1bSBkb2xvciBzaXQg +YW1ldCwgY29uc2VjdGV0dXIgYWRpcGlzY2luZyBlbGl0LiBWZXN0aWJ1bHVtIG1h +dHRpcywgcmlzdXMgdGVtcHVzIHVsdHJpY2llcyBsdWN0dXMsIG5pc2kgCmVsaXQg +Y29uc2VjdGV0dXIgdmVsaXQsIGxhY2luaWEgZnJpbmdpbGxhIHVybmEgbGVjdHVz +IHNpdCBhbWV0IG51bmMuIE51bmMgbmVjIGRpY3R1bSBhcmN1LiBEdWlzIGlkIGFs +aXF1ZXQgbnVsbGEuIEluIHV0IAphdWd1ZSBlbGl0LiBQaGFzZWxsdXMgc2VtcGVy +IHV0IGxhY3VzIGFjIHNhZ2l0dGlzLiBOdW5jIGVsZW1lbnR1bSwgZHVpIG5lYyBv +cm5hcmUgYmliZW5kdW0sIGxpZ3VsYSBhbnRlIHZ1bHB1dGF0ZSBuaXNpLCAKaW4g +aW1wZXJkaWV0IGxlY3R1cyBlcm9zIHJob25jdXMgYXVndWUuIE1hZWNlbmFzIG5v +biBpbnRlcmR1bSBtZXR1cywgdml0YWUgdGVtcG9yIG1pLgoKU2VkIHJ1dHJ1bSwg +bG9yZW0gcXVpcyB2YXJpdXMgdGluY2lkdW50LCBhdWd1ZSBlbGl0IGFjY3Vtc2Fu +IG5pc2wsIHNpdCBhbWV0IGdyYXZpZGEgb2RpbyBtYXNzYSB1bHRyaWNlcyB1cm5h +LiBNb3JiaSAKc3VzY2lwaXQgbWF1cmlzIG5lYyBlcm9zIGlhY3VsaXMsIHZlbCB0 +ZW1wdXMgbWF1cmlzIGJpYmVuZHVtLiBQaGFzZWxsdXMgdGVtcG9yIGVuaW0gZWdl +dCBjb21tb2RvIGdyYXZpZGEuIFNlZCBldCByaXN1cyAKcXVpcyBvcmNpIGVsZWlm +ZW5kIHVsbGFtY29ycGVyLiBVdCBldCBuaXNpIGV0IHNlbSBjb252YWxsaXMgc29k +YWxlcy4gUHJvaW4gbmVjIG5pYmggbm9uIG1ldHVzIGNvbmRpbWVudHVtIHZlaGlj +dWxhLiAKTWFlY2VuYXMgdXQgZmVsaXMgdGVsbHVzLiBDcmFzIGlkIGRpYW0gZGlh +bS4gTmFtIGV1IHVsdHJpY2llcyBudW5jLCBpbiBmZXJtZW50dW0gdGVsbHVzLiBE +b25lYyBldCBsaWJlcm8gYSBhdWd1ZSAKZmVybWVudHVtIGNvbmd1ZS4gTnVsbGEg +dml0YWUgYXVndWUgYXJjdS4gRG9uZWMgbW9sbGlzIGZhdWNpYnVzIGVzdCwgYWMg +Y29uc2VjdGV0dXIgbGVjdHVzIGFsaXF1ZXQgYXQuIENsYXNzIGFwdGVudCAKdGFj +aXRpIHNvY2lvc3F1IGFkIGxpdG9yYSB0b3JxdWVudCBwZXIgY29udWJpYSBub3N0 +cmEgcG9zdWVyZS4=
diff --git a/src/test/resources/plaintexts/lorem-5000.txt b/src/test/resources/plaintexts/lorem-5000.txt new file mode 100644 index 0000000..dcfac44 --- /dev/null +++ b/src/test/resources/plaintexts/lorem-5000.txt
@@ -0,0 +1,55 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent tempus sagittis erat et facilisis. In quis eleifend +dolor. Duis sagittis porttitor risus non aliquam. Etiam malesuada purus et felis feugiat, et condimentum augue +sagittis. Donec posuere semper lacus eget sollicitudin. Ut feugiat diam ipsum, ut tempor turpis vehicula sed. Duis +euismod euismod rhoncus. Praesent vitae leo egestas neque pellentesque ultricies tincidunt et purus. Suspendisse +interdum blandit urna imperdiet euismod. Etiam et metus molestie nisl imperdiet cursus. + +Donec ullamcorper purus sed leo rutrum tincidunt. Pellentesque iaculis tristique pellentesque. Pellentesque cursus +porta nisl, at iaculis ligula dapibus quis. Morbi mattis, eros porta blandit ullamcorper, arcu dolor fermentum nunc, ac +cursus magna arcu id nulla. Quisque urna urna, dapibus et blandit vitae, placerat quis nibh. Sed ac est eget nisl +ultrices gravida. Aliquam id suscipit ligula. Suspendisse sollicitudin fermentum turpis. Donec auctor consectetur purus +eu bibendum. Pellentesque eget elit vitae augue bibendum lacinia. Quisque convallis tristique turpis id viverra. Mauris +aliquet bibendum bibendum. Morbi volutpat id dolor non aliquam. + +Aliquam ultrices dapibus diam, vel porttitor magna elementum et. Fusce eu dui quis dui malesuada tempor vel rhoncus +orci. Vestibulum id vestibulum augue. Aenean turpis felis, auctor vel neque eget, fermentum semper ante. Proin +imperdiet quis tellus vitae pulvinar. Phasellus est metus, molestie eget diam vel, consequat hendrerit erat. Donec +laoreet vitae quam eu sagittis. Vivamus quis tincidunt turpis. Integer venenatis turpis velit. + +Morbi tempor elit mi, ac lacinia ligula interdum vel. Donec commodo erat vitae porta tincidunt. Quisque ullamcorper, +est non lacinia consequat, turpis nunc feugiat metus, quis tincidunt lectus augue et ipsum. Curabitur lobortis +facilisis luctus. Sed placerat ligula enim, at dignissim urna condimentum eget. Etiam tempus facilisis nibh, lobortis +tincidunt ante ultrices at. Integer sed purus nec dolor aliquet dictum ut id mi. Nulla vel sem ipsum. Aliquam nec +tincidunt metus. Interdum et malesuada fames ac ante ipsum primis in faucibus. Sed suscipit ipsum a varius commodo. +Vestibulum bibendum at lectus sit amet vehicula. Pellentesque ullamcorper est eu purus iaculis laoreet. Nam nec neque +eget massa faucibus elementum eu quis eros. + +Ut id laoreet dolor. Donec eget gravida elit, ac rutrum elit. Quisque lacus neque, fringilla in turpis et, gravida +volutpat orci. Integer in nisl eget lorem ullamcorper condimentum nec venenatis lorem. Curabitur eget magna eu dui +fermentum fringilla sed ut dolor. Nulla vestibulum nunc et pharetra imperdiet. Morbi tristique porttitor odio, nec +pharetra orci lacinia vitae. Duis vestibulum diam ullamcorper massa vestibulum, ut auctor lorem imperdiet. Vestibulum +et venenatis tellus. Suspendisse convallis, odio ac dictum lacinia, neque quam tincidunt risus, non ornare dolor lacus +nec tellus. Mauris rhoncus fermentum neque sit amet sollicitudin. Pellentesque nulla magna, commodo vel tincidunt sed, +hendrerit quis justo. + +In hac habitasse platea dictumst. Nunc et est augue. Vivamus pretium, odio vel sodales semper, risus ante elementum +urna, ac ullamcorper massa neque non ligula. Cras id tellus quis urna adipiscing elementum. Donec adipiscing magna vel +suscipit vulputate. Quisque in mi eget orci viverra scelerisque nec et magna. Cras posuere, urna lacinia dictum +rhoncus, justo orci suscipit metus, ut gravida orci nulla in turpis. Interdum et malesuada fames ac ante ipsum primis +in faucibus. In volutpat feugiat turpis, non faucibus nunc interdum ut. Praesent ornare sagittis urna, id lacinia +tortor suscipit et. Pellentesque tempor mi ac nisi varius, vitae ullamcorper urna suscipit. + +Aliquam pellentesque vulputate turpis vestibulum eleifend. Donec faucibus eu felis eu blandit. Pellentesque pulvinar, +elit non lacinia porttitor, orci nibh iaculis augue, ac scelerisque mi tellus quis tortor. Interdum et malesuada fames +ac ante ipsum primis in faucibus. Cras nec suscipit risus, ut ultricies elit. Suspendisse et fringilla urna. Donec +luctus et nisi quis bibendum. Quisque sed augue lacinia, pellentesque diam vitae, convallis metus. Nam vehicula metus +eget orci sagittis sodales. + +Curabitur egestas congue nibh sed luctus. Nam luctus tellus at nulla vestibulum, rutrum fermentum est vulputate. Nulla +condimentum dui a dolor lobortis, eu aliquet odio pretium. Phasellus id pretium nulla. Donec eu tincidunt lectus, in +ornare orci. Pellentesque mollis est eget turpis aliquet, vel tempor velit fringilla. Curabitur a leo elementum elit +pellentesque tincidunt vestibulum sit amet mi. Vestibulum varius pulvinar nisi sit amet dignissim. Vestibulum dui +velit, commodo vel augue non, dapibus imperdiet mi. + +Etiam at turpis velit. Suspendisse tempor, nisl a congue pellentesque, erat tellus gravida sapien, et sodales nulla +nisi at mauris. Nunc consectetur ligula vitae posuere pharetra. Integer id suscipit sem. Praesent cras amet. \ No newline at end of file
diff --git a/src/test/resources/plaintexts/lorem-5000.txt.b64 b/src/test/resources/plaintexts/lorem-5000.txt.b64 new file mode 100644 index 0000000..6259704 --- /dev/null +++ b/src/test/resources/plaintexts/lorem-5000.txt.b64
@@ -0,0 +1,106 @@ +TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2Np +bmcgZWxpdC4gUHJhZXNlbnQgdGVtcHVzIHNhZ2l0dGlzIGVyYXQgZXQgZmFjaWxp +c2lzLiBJbiBxdWlzIGVsZWlmZW5kIApkb2xvci4gRHVpcyBzYWdpdHRpcyBwb3J0 +dGl0b3IgcmlzdXMgbm9uIGFsaXF1YW0uIEV0aWFtIG1hbGVzdWFkYSBwdXJ1cyBl +dCBmZWxpcyBmZXVnaWF0LCBldCBjb25kaW1lbnR1bSBhdWd1ZSAKc2FnaXR0aXMu +IERvbmVjIHBvc3VlcmUgc2VtcGVyIGxhY3VzIGVnZXQgc29sbGljaXR1ZGluLiBV +dCBmZXVnaWF0IGRpYW0gaXBzdW0sIHV0IHRlbXBvciB0dXJwaXMgdmVoaWN1bGEg +c2VkLiBEdWlzIApldWlzbW9kIGV1aXNtb2QgcmhvbmN1cy4gUHJhZXNlbnQgdml0 +YWUgbGVvIGVnZXN0YXMgbmVxdWUgcGVsbGVudGVzcXVlIHVsdHJpY2llcyB0aW5j +aWR1bnQgZXQgcHVydXMuIFN1c3BlbmRpc3NlIAppbnRlcmR1bSBibGFuZGl0IHVy +bmEgaW1wZXJkaWV0IGV1aXNtb2QuIEV0aWFtIGV0IG1ldHVzIG1vbGVzdGllIG5p +c2wgaW1wZXJkaWV0IGN1cnN1cy4KCkRvbmVjIHVsbGFtY29ycGVyIHB1cnVzIHNl +ZCBsZW8gcnV0cnVtIHRpbmNpZHVudC4gUGVsbGVudGVzcXVlIGlhY3VsaXMgdHJp +c3RpcXVlIHBlbGxlbnRlc3F1ZS4gUGVsbGVudGVzcXVlIGN1cnN1cyAKcG9ydGEg +bmlzbCwgYXQgaWFjdWxpcyBsaWd1bGEgZGFwaWJ1cyBxdWlzLiBNb3JiaSBtYXR0 +aXMsIGVyb3MgcG9ydGEgYmxhbmRpdCB1bGxhbWNvcnBlciwgYXJjdSBkb2xvciBm +ZXJtZW50dW0gbnVuYywgYWMgCmN1cnN1cyBtYWduYSBhcmN1IGlkIG51bGxhLiBR +dWlzcXVlIHVybmEgdXJuYSwgZGFwaWJ1cyBldCBibGFuZGl0IHZpdGFlLCBwbGFj +ZXJhdCBxdWlzIG5pYmguIFNlZCBhYyBlc3QgZWdldCBuaXNsIAp1bHRyaWNlcyBn +cmF2aWRhLiBBbGlxdWFtIGlkIHN1c2NpcGl0IGxpZ3VsYS4gU3VzcGVuZGlzc2Ug +c29sbGljaXR1ZGluIGZlcm1lbnR1bSB0dXJwaXMuIERvbmVjIGF1Y3RvciBjb25z +ZWN0ZXR1ciBwdXJ1cyAKZXUgYmliZW5kdW0uIFBlbGxlbnRlc3F1ZSBlZ2V0IGVs +aXQgdml0YWUgYXVndWUgYmliZW5kdW0gbGFjaW5pYS4gUXVpc3F1ZSBjb252YWxs +aXMgdHJpc3RpcXVlIHR1cnBpcyBpZCB2aXZlcnJhLiBNYXVyaXMgCmFsaXF1ZXQg +YmliZW5kdW0gYmliZW5kdW0uIE1vcmJpIHZvbHV0cGF0IGlkIGRvbG9yIG5vbiBh +bGlxdWFtLgoKQWxpcXVhbSB1bHRyaWNlcyBkYXBpYnVzIGRpYW0sIHZlbCBwb3J0 +dGl0b3IgbWFnbmEgZWxlbWVudHVtIGV0LiBGdXNjZSBldSBkdWkgcXVpcyBkdWkg +bWFsZXN1YWRhIHRlbXBvciB2ZWwgcmhvbmN1cyAKb3JjaS4gVmVzdGlidWx1bSBp +ZCB2ZXN0aWJ1bHVtIGF1Z3VlLiBBZW5lYW4gdHVycGlzIGZlbGlzLCBhdWN0b3Ig +dmVsIG5lcXVlIGVnZXQsIGZlcm1lbnR1bSBzZW1wZXIgYW50ZS4gUHJvaW4gCmlt +cGVyZGlldCBxdWlzIHRlbGx1cyB2aXRhZSBwdWx2aW5hci4gUGhhc2VsbHVzIGVz +dCBtZXR1cywgbW9sZXN0aWUgZWdldCBkaWFtIHZlbCwgY29uc2VxdWF0IGhlbmRy +ZXJpdCBlcmF0LiBEb25lYyAKbGFvcmVldCB2aXRhZSBxdWFtIGV1IHNhZ2l0dGlz +LiBWaXZhbXVzIHF1aXMgdGluY2lkdW50IHR1cnBpcy4gSW50ZWdlciB2ZW5lbmF0 +aXMgdHVycGlzIHZlbGl0LgoKTW9yYmkgdGVtcG9yIGVsaXQgbWksIGFjIGxhY2lu +aWEgbGlndWxhIGludGVyZHVtIHZlbC4gRG9uZWMgY29tbW9kbyBlcmF0IHZpdGFl +IHBvcnRhIHRpbmNpZHVudC4gUXVpc3F1ZSB1bGxhbWNvcnBlciwgCmVzdCBub24g +bGFjaW5pYSBjb25zZXF1YXQsIHR1cnBpcyBudW5jIGZldWdpYXQgbWV0dXMsIHF1 +aXMgdGluY2lkdW50IGxlY3R1cyBhdWd1ZSBldCBpcHN1bS4gQ3VyYWJpdHVyIGxv +Ym9ydGlzIApmYWNpbGlzaXMgbHVjdHVzLiBTZWQgcGxhY2VyYXQgbGlndWxhIGVu +aW0sIGF0IGRpZ25pc3NpbSB1cm5hIGNvbmRpbWVudHVtIGVnZXQuIEV0aWFtIHRl +bXB1cyBmYWNpbGlzaXMgbmliaCwgbG9ib3J0aXMgCnRpbmNpZHVudCBhbnRlIHVs +dHJpY2VzIGF0LiBJbnRlZ2VyIHNlZCBwdXJ1cyBuZWMgZG9sb3IgYWxpcXVldCBk +aWN0dW0gdXQgaWQgbWkuIE51bGxhIHZlbCBzZW0gaXBzdW0uIEFsaXF1YW0gbmVj +IAp0aW5jaWR1bnQgbWV0dXMuIEludGVyZHVtIGV0IG1hbGVzdWFkYSBmYW1lcyBh +YyBhbnRlIGlwc3VtIHByaW1pcyBpbiBmYXVjaWJ1cy4gU2VkIHN1c2NpcGl0IGlw +c3VtIGEgdmFyaXVzIGNvbW1vZG8uIApWZXN0aWJ1bHVtIGJpYmVuZHVtIGF0IGxl +Y3R1cyBzaXQgYW1ldCB2ZWhpY3VsYS4gUGVsbGVudGVzcXVlIHVsbGFtY29ycGVy +IGVzdCBldSBwdXJ1cyBpYWN1bGlzIGxhb3JlZXQuIE5hbSBuZWMgbmVxdWUgCmVn +ZXQgbWFzc2EgZmF1Y2lidXMgZWxlbWVudHVtIGV1IHF1aXMgZXJvcy4KClV0IGlk +IGxhb3JlZXQgZG9sb3IuIERvbmVjIGVnZXQgZ3JhdmlkYSBlbGl0LCBhYyBydXRy +dW0gZWxpdC4gUXVpc3F1ZSBsYWN1cyBuZXF1ZSwgZnJpbmdpbGxhIGluIHR1cnBp +cyBldCwgZ3JhdmlkYSAKdm9sdXRwYXQgb3JjaS4gSW50ZWdlciBpbiBuaXNsIGVn +ZXQgbG9yZW0gdWxsYW1jb3JwZXIgY29uZGltZW50dW0gbmVjIHZlbmVuYXRpcyBs +b3JlbS4gQ3VyYWJpdHVyIGVnZXQgbWFnbmEgZXUgZHVpIApmZXJtZW50dW0gZnJp +bmdpbGxhIHNlZCB1dCBkb2xvci4gTnVsbGEgdmVzdGlidWx1bSBudW5jIGV0IHBo +YXJldHJhIGltcGVyZGlldC4gTW9yYmkgdHJpc3RpcXVlIHBvcnR0aXRvciBvZGlv +LCBuZWMgCnBoYXJldHJhIG9yY2kgbGFjaW5pYSB2aXRhZS4gRHVpcyB2ZXN0aWJ1 +bHVtIGRpYW0gdWxsYW1jb3JwZXIgbWFzc2EgdmVzdGlidWx1bSwgdXQgYXVjdG9y +IGxvcmVtIGltcGVyZGlldC4gVmVzdGlidWx1bSAKZXQgdmVuZW5hdGlzIHRlbGx1 +cy4gU3VzcGVuZGlzc2UgY29udmFsbGlzLCBvZGlvIGFjIGRpY3R1bSBsYWNpbmlh +LCBuZXF1ZSBxdWFtIHRpbmNpZHVudCByaXN1cywgbm9uIG9ybmFyZSBkb2xvciBs +YWN1cyAKbmVjIHRlbGx1cy4gTWF1cmlzIHJob25jdXMgZmVybWVudHVtIG5lcXVl +IHNpdCBhbWV0IHNvbGxpY2l0dWRpbi4gUGVsbGVudGVzcXVlIG51bGxhIG1hZ25h +LCBjb21tb2RvIHZlbCB0aW5jaWR1bnQgc2VkLCAKaGVuZHJlcml0IHF1aXMganVz +dG8uCgpJbiBoYWMgaGFiaXRhc3NlIHBsYXRlYSBkaWN0dW1zdC4gTnVuYyBldCBl +c3QgYXVndWUuIFZpdmFtdXMgcHJldGl1bSwgb2RpbyB2ZWwgc29kYWxlcyBzZW1w +ZXIsIHJpc3VzIGFudGUgZWxlbWVudHVtIAp1cm5hLCBhYyB1bGxhbWNvcnBlciBt +YXNzYSBuZXF1ZSBub24gbGlndWxhLiBDcmFzIGlkIHRlbGx1cyBxdWlzIHVybmEg +YWRpcGlzY2luZyBlbGVtZW50dW0uIERvbmVjIGFkaXBpc2NpbmcgbWFnbmEgdmVs +IApzdXNjaXBpdCB2dWxwdXRhdGUuIFF1aXNxdWUgaW4gbWkgZWdldCBvcmNpIHZp +dmVycmEgc2NlbGVyaXNxdWUgbmVjIGV0IG1hZ25hLiBDcmFzIHBvc3VlcmUsIHVy +bmEgbGFjaW5pYSBkaWN0dW0gCnJob25jdXMsIGp1c3RvIG9yY2kgc3VzY2lwaXQg +bWV0dXMsIHV0IGdyYXZpZGEgb3JjaSBudWxsYSBpbiB0dXJwaXMuIEludGVyZHVt +IGV0IG1hbGVzdWFkYSBmYW1lcyBhYyBhbnRlIGlwc3VtIHByaW1pcyAKaW4gZmF1 +Y2lidXMuIEluIHZvbHV0cGF0IGZldWdpYXQgdHVycGlzLCBub24gZmF1Y2lidXMg +bnVuYyBpbnRlcmR1bSB1dC4gUHJhZXNlbnQgb3JuYXJlIHNhZ2l0dGlzIHVybmEs +IGlkIGxhY2luaWEgCnRvcnRvciBzdXNjaXBpdCBldC4gUGVsbGVudGVzcXVlIHRl +bXBvciBtaSBhYyBuaXNpIHZhcml1cywgdml0YWUgdWxsYW1jb3JwZXIgdXJuYSBz +dXNjaXBpdC4KCkFsaXF1YW0gcGVsbGVudGVzcXVlIHZ1bHB1dGF0ZSB0dXJwaXMg +dmVzdGlidWx1bSBlbGVpZmVuZC4gRG9uZWMgZmF1Y2lidXMgZXUgZmVsaXMgZXUg +YmxhbmRpdC4gUGVsbGVudGVzcXVlIHB1bHZpbmFyLCAKZWxpdCBub24gbGFjaW5p +YSBwb3J0dGl0b3IsIG9yY2kgbmliaCBpYWN1bGlzIGF1Z3VlLCBhYyBzY2VsZXJp +c3F1ZSBtaSB0ZWxsdXMgcXVpcyB0b3J0b3IuIEludGVyZHVtIGV0IG1hbGVzdWFk +YSBmYW1lcyAKYWMgYW50ZSBpcHN1bSBwcmltaXMgaW4gZmF1Y2lidXMuIENyYXMg +bmVjIHN1c2NpcGl0IHJpc3VzLCB1dCB1bHRyaWNpZXMgZWxpdC4gU3VzcGVuZGlz +c2UgZXQgZnJpbmdpbGxhIHVybmEuIERvbmVjIApsdWN0dXMgZXQgbmlzaSBxdWlz +IGJpYmVuZHVtLiBRdWlzcXVlIHNlZCBhdWd1ZSBsYWNpbmlhLCBwZWxsZW50ZXNx +dWUgZGlhbSB2aXRhZSwgY29udmFsbGlzIG1ldHVzLiBOYW0gdmVoaWN1bGEgbWV0 +dXMgCmVnZXQgb3JjaSBzYWdpdHRpcyBzb2RhbGVzLgoKQ3VyYWJpdHVyIGVnZXN0 +YXMgY29uZ3VlIG5pYmggc2VkIGx1Y3R1cy4gTmFtIGx1Y3R1cyB0ZWxsdXMgYXQg +bnVsbGEgdmVzdGlidWx1bSwgcnV0cnVtIGZlcm1lbnR1bSBlc3QgdnVscHV0YXRl +LiBOdWxsYSAKY29uZGltZW50dW0gZHVpIGEgZG9sb3IgbG9ib3J0aXMsIGV1IGFs +aXF1ZXQgb2RpbyBwcmV0aXVtLiBQaGFzZWxsdXMgaWQgcHJldGl1bSBudWxsYS4g +RG9uZWMgZXUgdGluY2lkdW50IGxlY3R1cywgaW4gCm9ybmFyZSBvcmNpLiBQZWxs +ZW50ZXNxdWUgbW9sbGlzIGVzdCBlZ2V0IHR1cnBpcyBhbGlxdWV0LCB2ZWwgdGVt +cG9yIHZlbGl0IGZyaW5naWxsYS4gQ3VyYWJpdHVyIGEgbGVvIGVsZW1lbnR1bSBl +bGl0IApwZWxsZW50ZXNxdWUgdGluY2lkdW50IHZlc3RpYnVsdW0gc2l0IGFtZXQg +bWkuIFZlc3RpYnVsdW0gdmFyaXVzIHB1bHZpbmFyIG5pc2kgc2l0IGFtZXQgZGln +bmlzc2ltLiBWZXN0aWJ1bHVtIGR1aSAKdmVsaXQsIGNvbW1vZG8gdmVsIGF1Z3Vl +IG5vbiwgZGFwaWJ1cyBpbXBlcmRpZXQgbWkuCgpFdGlhbSBhdCB0dXJwaXMgdmVs +aXQuIFN1c3BlbmRpc3NlIHRlbXBvciwgbmlzbCBhIGNvbmd1ZSBwZWxsZW50ZXNx +dWUsIGVyYXQgdGVsbHVzIGdyYXZpZGEgc2FwaWVuLCBldCBzb2RhbGVzIG51bGxh +IApuaXNpIGF0IG1hdXJpcy4gTnVuYyBjb25zZWN0ZXR1ciBsaWd1bGEgdml0YWUg +cG9zdWVyZSBwaGFyZXRyYS4gSW50ZWdlciBpZCBzdXNjaXBpdCBzZW0uIFByYWVz +ZW50IGNyYXMgYW1ldC4=
diff --git a/src/test/resources/plaintexts/lorem-5000.txt.b64.crlf b/src/test/resources/plaintexts/lorem-5000.txt.b64.crlf new file mode 100644 index 0000000..9d6ed26 --- /dev/null +++ b/src/test/resources/plaintexts/lorem-5000.txt.b64.crlf
@@ -0,0 +1,106 @@ +TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIGFkaXBpc2Np +bmcgZWxpdC4gUHJhZXNlbnQgdGVtcHVzIHNhZ2l0dGlzIGVyYXQgZXQgZmFjaWxp +c2lzLiBJbiBxdWlzIGVsZWlmZW5kIApkb2xvci4gRHVpcyBzYWdpdHRpcyBwb3J0 +dGl0b3IgcmlzdXMgbm9uIGFsaXF1YW0uIEV0aWFtIG1hbGVzdWFkYSBwdXJ1cyBl +dCBmZWxpcyBmZXVnaWF0LCBldCBjb25kaW1lbnR1bSBhdWd1ZSAKc2FnaXR0aXMu +IERvbmVjIHBvc3VlcmUgc2VtcGVyIGxhY3VzIGVnZXQgc29sbGljaXR1ZGluLiBV +dCBmZXVnaWF0IGRpYW0gaXBzdW0sIHV0IHRlbXBvciB0dXJwaXMgdmVoaWN1bGEg +c2VkLiBEdWlzIApldWlzbW9kIGV1aXNtb2QgcmhvbmN1cy4gUHJhZXNlbnQgdml0 +YWUgbGVvIGVnZXN0YXMgbmVxdWUgcGVsbGVudGVzcXVlIHVsdHJpY2llcyB0aW5j +aWR1bnQgZXQgcHVydXMuIFN1c3BlbmRpc3NlIAppbnRlcmR1bSBibGFuZGl0IHVy +bmEgaW1wZXJkaWV0IGV1aXNtb2QuIEV0aWFtIGV0IG1ldHVzIG1vbGVzdGllIG5p +c2wgaW1wZXJkaWV0IGN1cnN1cy4KCkRvbmVjIHVsbGFtY29ycGVyIHB1cnVzIHNl +ZCBsZW8gcnV0cnVtIHRpbmNpZHVudC4gUGVsbGVudGVzcXVlIGlhY3VsaXMgdHJp +c3RpcXVlIHBlbGxlbnRlc3F1ZS4gUGVsbGVudGVzcXVlIGN1cnN1cyAKcG9ydGEg +bmlzbCwgYXQgaWFjdWxpcyBsaWd1bGEgZGFwaWJ1cyBxdWlzLiBNb3JiaSBtYXR0 +aXMsIGVyb3MgcG9ydGEgYmxhbmRpdCB1bGxhbWNvcnBlciwgYXJjdSBkb2xvciBm +ZXJtZW50dW0gbnVuYywgYWMgCmN1cnN1cyBtYWduYSBhcmN1IGlkIG51bGxhLiBR +dWlzcXVlIHVybmEgdXJuYSwgZGFwaWJ1cyBldCBibGFuZGl0IHZpdGFlLCBwbGFj +ZXJhdCBxdWlzIG5pYmguIFNlZCBhYyBlc3QgZWdldCBuaXNsIAp1bHRyaWNlcyBn +cmF2aWRhLiBBbGlxdWFtIGlkIHN1c2NpcGl0IGxpZ3VsYS4gU3VzcGVuZGlzc2Ug +c29sbGljaXR1ZGluIGZlcm1lbnR1bSB0dXJwaXMuIERvbmVjIGF1Y3RvciBjb25z +ZWN0ZXR1ciBwdXJ1cyAKZXUgYmliZW5kdW0uIFBlbGxlbnRlc3F1ZSBlZ2V0IGVs +aXQgdml0YWUgYXVndWUgYmliZW5kdW0gbGFjaW5pYS4gUXVpc3F1ZSBjb252YWxs +aXMgdHJpc3RpcXVlIHR1cnBpcyBpZCB2aXZlcnJhLiBNYXVyaXMgCmFsaXF1ZXQg +YmliZW5kdW0gYmliZW5kdW0uIE1vcmJpIHZvbHV0cGF0IGlkIGRvbG9yIG5vbiBh +bGlxdWFtLgoKQWxpcXVhbSB1bHRyaWNlcyBkYXBpYnVzIGRpYW0sIHZlbCBwb3J0 +dGl0b3IgbWFnbmEgZWxlbWVudHVtIGV0LiBGdXNjZSBldSBkdWkgcXVpcyBkdWkg +bWFsZXN1YWRhIHRlbXBvciB2ZWwgcmhvbmN1cyAKb3JjaS4gVmVzdGlidWx1bSBp +ZCB2ZXN0aWJ1bHVtIGF1Z3VlLiBBZW5lYW4gdHVycGlzIGZlbGlzLCBhdWN0b3Ig +dmVsIG5lcXVlIGVnZXQsIGZlcm1lbnR1bSBzZW1wZXIgYW50ZS4gUHJvaW4gCmlt +cGVyZGlldCBxdWlzIHRlbGx1cyB2aXRhZSBwdWx2aW5hci4gUGhhc2VsbHVzIGVz +dCBtZXR1cywgbW9sZXN0aWUgZWdldCBkaWFtIHZlbCwgY29uc2VxdWF0IGhlbmRy +ZXJpdCBlcmF0LiBEb25lYyAKbGFvcmVldCB2aXRhZSBxdWFtIGV1IHNhZ2l0dGlz +LiBWaXZhbXVzIHF1aXMgdGluY2lkdW50IHR1cnBpcy4gSW50ZWdlciB2ZW5lbmF0 +aXMgdHVycGlzIHZlbGl0LgoKTW9yYmkgdGVtcG9yIGVsaXQgbWksIGFjIGxhY2lu +aWEgbGlndWxhIGludGVyZHVtIHZlbC4gRG9uZWMgY29tbW9kbyBlcmF0IHZpdGFl +IHBvcnRhIHRpbmNpZHVudC4gUXVpc3F1ZSB1bGxhbWNvcnBlciwgCmVzdCBub24g +bGFjaW5pYSBjb25zZXF1YXQsIHR1cnBpcyBudW5jIGZldWdpYXQgbWV0dXMsIHF1 +aXMgdGluY2lkdW50IGxlY3R1cyBhdWd1ZSBldCBpcHN1bS4gQ3VyYWJpdHVyIGxv +Ym9ydGlzIApmYWNpbGlzaXMgbHVjdHVzLiBTZWQgcGxhY2VyYXQgbGlndWxhIGVu +aW0sIGF0IGRpZ25pc3NpbSB1cm5hIGNvbmRpbWVudHVtIGVnZXQuIEV0aWFtIHRl +bXB1cyBmYWNpbGlzaXMgbmliaCwgbG9ib3J0aXMgCnRpbmNpZHVudCBhbnRlIHVs +dHJpY2VzIGF0LiBJbnRlZ2VyIHNlZCBwdXJ1cyBuZWMgZG9sb3IgYWxpcXVldCBk +aWN0dW0gdXQgaWQgbWkuIE51bGxhIHZlbCBzZW0gaXBzdW0uIEFsaXF1YW0gbmVj +IAp0aW5jaWR1bnQgbWV0dXMuIEludGVyZHVtIGV0IG1hbGVzdWFkYSBmYW1lcyBh +YyBhbnRlIGlwc3VtIHByaW1pcyBpbiBmYXVjaWJ1cy4gU2VkIHN1c2NpcGl0IGlw +c3VtIGEgdmFyaXVzIGNvbW1vZG8uIApWZXN0aWJ1bHVtIGJpYmVuZHVtIGF0IGxl +Y3R1cyBzaXQgYW1ldCB2ZWhpY3VsYS4gUGVsbGVudGVzcXVlIHVsbGFtY29ycGVy +IGVzdCBldSBwdXJ1cyBpYWN1bGlzIGxhb3JlZXQuIE5hbSBuZWMgbmVxdWUgCmVn +ZXQgbWFzc2EgZmF1Y2lidXMgZWxlbWVudHVtIGV1IHF1aXMgZXJvcy4KClV0IGlk +IGxhb3JlZXQgZG9sb3IuIERvbmVjIGVnZXQgZ3JhdmlkYSBlbGl0LCBhYyBydXRy +dW0gZWxpdC4gUXVpc3F1ZSBsYWN1cyBuZXF1ZSwgZnJpbmdpbGxhIGluIHR1cnBp +cyBldCwgZ3JhdmlkYSAKdm9sdXRwYXQgb3JjaS4gSW50ZWdlciBpbiBuaXNsIGVn +ZXQgbG9yZW0gdWxsYW1jb3JwZXIgY29uZGltZW50dW0gbmVjIHZlbmVuYXRpcyBs +b3JlbS4gQ3VyYWJpdHVyIGVnZXQgbWFnbmEgZXUgZHVpIApmZXJtZW50dW0gZnJp +bmdpbGxhIHNlZCB1dCBkb2xvci4gTnVsbGEgdmVzdGlidWx1bSBudW5jIGV0IHBo +YXJldHJhIGltcGVyZGlldC4gTW9yYmkgdHJpc3RpcXVlIHBvcnR0aXRvciBvZGlv +LCBuZWMgCnBoYXJldHJhIG9yY2kgbGFjaW5pYSB2aXRhZS4gRHVpcyB2ZXN0aWJ1 +bHVtIGRpYW0gdWxsYW1jb3JwZXIgbWFzc2EgdmVzdGlidWx1bSwgdXQgYXVjdG9y +IGxvcmVtIGltcGVyZGlldC4gVmVzdGlidWx1bSAKZXQgdmVuZW5hdGlzIHRlbGx1 +cy4gU3VzcGVuZGlzc2UgY29udmFsbGlzLCBvZGlvIGFjIGRpY3R1bSBsYWNpbmlh +LCBuZXF1ZSBxdWFtIHRpbmNpZHVudCByaXN1cywgbm9uIG9ybmFyZSBkb2xvciBs +YWN1cyAKbmVjIHRlbGx1cy4gTWF1cmlzIHJob25jdXMgZmVybWVudHVtIG5lcXVl +IHNpdCBhbWV0IHNvbGxpY2l0dWRpbi4gUGVsbGVudGVzcXVlIG51bGxhIG1hZ25h +LCBjb21tb2RvIHZlbCB0aW5jaWR1bnQgc2VkLCAKaGVuZHJlcml0IHF1aXMganVz +dG8uCgpJbiBoYWMgaGFiaXRhc3NlIHBsYXRlYSBkaWN0dW1zdC4gTnVuYyBldCBl +c3QgYXVndWUuIFZpdmFtdXMgcHJldGl1bSwgb2RpbyB2ZWwgc29kYWxlcyBzZW1w +ZXIsIHJpc3VzIGFudGUgZWxlbWVudHVtIAp1cm5hLCBhYyB1bGxhbWNvcnBlciBt +YXNzYSBuZXF1ZSBub24gbGlndWxhLiBDcmFzIGlkIHRlbGx1cyBxdWlzIHVybmEg +YWRpcGlzY2luZyBlbGVtZW50dW0uIERvbmVjIGFkaXBpc2NpbmcgbWFnbmEgdmVs +IApzdXNjaXBpdCB2dWxwdXRhdGUuIFF1aXNxdWUgaW4gbWkgZWdldCBvcmNpIHZp +dmVycmEgc2NlbGVyaXNxdWUgbmVjIGV0IG1hZ25hLiBDcmFzIHBvc3VlcmUsIHVy +bmEgbGFjaW5pYSBkaWN0dW0gCnJob25jdXMsIGp1c3RvIG9yY2kgc3VzY2lwaXQg +bWV0dXMsIHV0IGdyYXZpZGEgb3JjaSBudWxsYSBpbiB0dXJwaXMuIEludGVyZHVt +IGV0IG1hbGVzdWFkYSBmYW1lcyBhYyBhbnRlIGlwc3VtIHByaW1pcyAKaW4gZmF1 +Y2lidXMuIEluIHZvbHV0cGF0IGZldWdpYXQgdHVycGlzLCBub24gZmF1Y2lidXMg +bnVuYyBpbnRlcmR1bSB1dC4gUHJhZXNlbnQgb3JuYXJlIHNhZ2l0dGlzIHVybmEs +IGlkIGxhY2luaWEgCnRvcnRvciBzdXNjaXBpdCBldC4gUGVsbGVudGVzcXVlIHRl +bXBvciBtaSBhYyBuaXNpIHZhcml1cywgdml0YWUgdWxsYW1jb3JwZXIgdXJuYSBz +dXNjaXBpdC4KCkFsaXF1YW0gcGVsbGVudGVzcXVlIHZ1bHB1dGF0ZSB0dXJwaXMg +dmVzdGlidWx1bSBlbGVpZmVuZC4gRG9uZWMgZmF1Y2lidXMgZXUgZmVsaXMgZXUg +YmxhbmRpdC4gUGVsbGVudGVzcXVlIHB1bHZpbmFyLCAKZWxpdCBub24gbGFjaW5p +YSBwb3J0dGl0b3IsIG9yY2kgbmliaCBpYWN1bGlzIGF1Z3VlLCBhYyBzY2VsZXJp +c3F1ZSBtaSB0ZWxsdXMgcXVpcyB0b3J0b3IuIEludGVyZHVtIGV0IG1hbGVzdWFk +YSBmYW1lcyAKYWMgYW50ZSBpcHN1bSBwcmltaXMgaW4gZmF1Y2lidXMuIENyYXMg +bmVjIHN1c2NpcGl0IHJpc3VzLCB1dCB1bHRyaWNpZXMgZWxpdC4gU3VzcGVuZGlz +c2UgZXQgZnJpbmdpbGxhIHVybmEuIERvbmVjIApsdWN0dXMgZXQgbmlzaSBxdWlz +IGJpYmVuZHVtLiBRdWlzcXVlIHNlZCBhdWd1ZSBsYWNpbmlhLCBwZWxsZW50ZXNx +dWUgZGlhbSB2aXRhZSwgY29udmFsbGlzIG1ldHVzLiBOYW0gdmVoaWN1bGEgbWV0 +dXMgCmVnZXQgb3JjaSBzYWdpdHRpcyBzb2RhbGVzLgoKQ3VyYWJpdHVyIGVnZXN0 +YXMgY29uZ3VlIG5pYmggc2VkIGx1Y3R1cy4gTmFtIGx1Y3R1cyB0ZWxsdXMgYXQg +bnVsbGEgdmVzdGlidWx1bSwgcnV0cnVtIGZlcm1lbnR1bSBlc3QgdnVscHV0YXRl +LiBOdWxsYSAKY29uZGltZW50dW0gZHVpIGEgZG9sb3IgbG9ib3J0aXMsIGV1IGFs +aXF1ZXQgb2RpbyBwcmV0aXVtLiBQaGFzZWxsdXMgaWQgcHJldGl1bSBudWxsYS4g +RG9uZWMgZXUgdGluY2lkdW50IGxlY3R1cywgaW4gCm9ybmFyZSBvcmNpLiBQZWxs +ZW50ZXNxdWUgbW9sbGlzIGVzdCBlZ2V0IHR1cnBpcyBhbGlxdWV0LCB2ZWwgdGVt +cG9yIHZlbGl0IGZyaW5naWxsYS4gQ3VyYWJpdHVyIGEgbGVvIGVsZW1lbnR1bSBl +bGl0IApwZWxsZW50ZXNxdWUgdGluY2lkdW50IHZlc3RpYnVsdW0gc2l0IGFtZXQg +bWkuIFZlc3RpYnVsdW0gdmFyaXVzIHB1bHZpbmFyIG5pc2kgc2l0IGFtZXQgZGln +bmlzc2ltLiBWZXN0aWJ1bHVtIGR1aSAKdmVsaXQsIGNvbW1vZG8gdmVsIGF1Z3Vl +IG5vbiwgZGFwaWJ1cyBpbXBlcmRpZXQgbWkuCgpFdGlhbSBhdCB0dXJwaXMgdmVs +aXQuIFN1c3BlbmRpc3NlIHRlbXBvciwgbmlzbCBhIGNvbmd1ZSBwZWxsZW50ZXNx +dWUsIGVyYXQgdGVsbHVzIGdyYXZpZGEgc2FwaWVuLCBldCBzb2RhbGVzIG51bGxh +IApuaXNpIGF0IG1hdXJpcy4gTnVuYyBjb25zZWN0ZXR1ciBsaWd1bGEgdml0YWUg +cG9zdWVyZSBwaGFyZXRyYS4gSW50ZWdlciBpZCBzdXNjaXBpdCBzZW0uIFByYWVz +ZW50IGNyYXMgYW1ldC4=