diff options
author | Xin Li <delphij@google.com> | 2018-08-06 16:50:42 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2018-08-06 16:50:42 -0700 |
commit | 61a1a6d194606fb275cf1c1c07a28f6cbc762072 (patch) | |
tree | 308f9979f2ea7bc08fed5ee43022c3ec51e75688 | |
parent | 55a17365e016751e0f33f5d6b03e1d7ac67d4034 (diff) | |
parent | cbcf969a038b3ed9eb5c297ec30bc0b62436ab03 (diff) | |
download | rappor-61a1a6d194606fb275cf1c1c07a28f6cbc762072.tar.gz |
Merge Android Pie into master
Bug: 112104996
Change-Id: I659ca9d324029c4b37ebdd3e483941af68c447ec
-rw-r--r-- | Android.bp | 26 | ||||
-rw-r--r-- | Android.mk | 25 | ||||
-rw-r--r-- | LICENSE | 202 | ||||
-rw-r--r-- | MODULE_LICENSE_APACHE2 | 0 | ||||
-rw-r--r-- | NOTICE | 202 | ||||
-rw-r--r-- | README.android | 13 | ||||
-rw-r--r-- | README.md | 38 | ||||
-rw-r--r-- | README.version | 4 | ||||
-rw-r--r-- | client/README.md | 43 | ||||
-rw-r--r-- | client/java/com/google/android/rappor/Encoder.java | 613 | ||||
-rw-r--r-- | client/java/com/google/android/rappor/HmacDrbg.java | 264 | ||||
-rw-r--r-- | client/javatest/com/google/android/rappor/EncoderTest.java | 1044 | ||||
-rw-r--r-- | client/javatest/com/google/android/rappor/HmacDrbgTest.java | 653 |
13 files changed, 3127 insertions, 0 deletions
diff --git a/Android.bp b/Android.bp new file mode 100644 index 0000000..db6d520 --- /dev/null +++ b/Android.bp @@ -0,0 +1,26 @@ +// +// Copyright (C) 2017 The Android Open Source Project +// +// 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. +// + +java_library_static { + name: "rappor", + no_framework_libs: true, + java_version: "1.7", + sdk_version: "core_current", + libs: [ + "jsr305", + ], + srcs: ["client/java/**/*.java"], +} diff --git a/Android.mk b/Android.mk new file mode 100644 index 0000000..1fb2caf --- /dev/null +++ b/Android.mk @@ -0,0 +1,25 @@ +# +# Copyright (C) 2017 The Android Open Source Project +# +# 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. +# + +LOCAL_PATH := $(call my-dir) +include $(CLEAR_VARS) +LOCAL_MODULE := rappor-tests +LOCAL_JAVA_LANGUAGE_VERSION := 1.7 +LOCAL_STATIC_JAVA_LIBRARIES := hamcrest-library rappor guava junit +LOCAL_JAVA_LIBRARIES := jsr305 +LOCAL_SDK_VERISON := core_current +LOCAL_SRC_FILES := $(call all-java-files-under, client/javatest) +include $(BUILD_STATIC_JAVA_LIBRARY) @@ -0,0 +1,202 @@ + + 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/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/MODULE_LICENSE_APACHE2 @@ -0,0 +1,202 @@ + + 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/README.android b/README.android new file mode 100644 index 0000000..30aae43 --- /dev/null +++ b/README.android @@ -0,0 +1,13 @@ +This project only contains Rappor java client library. +If you want to get the full source or other components, please visit Rappor github page: +https://github.com/google/rappor + +Any Android specific modifications to upstream archive-patcher should be listed +here: + +- Removed all non-java client library files. + +- Modified client/java/com/google/rappor/HmacDrbg.java, client/java/com/google/rappor/Encoder.java + to remove Guava, javax dependency. + +- Moved package from com.google.rappor.* to com.google.android.rappor.* to avoid source conflict with 3rd party library. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8527287 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +RAPPOR +====== + +RAPPOR is a novel privacy technology that allows inferring statistics about +populations while preserving the privacy of individual users. + +This repository contains simulation and analysis code in Python and R. + +For a detailed description of the algorithms, see the +[paper](http://arxiv.org/abs/1407.6981) and links below. + +Feel free to send feedback to +[rappor-discuss@googlegroups.com][group]. + +------------- + +- [RAPPOR Data Flow](http://google.github.io/rappor/doc/data-flow.html) + +Publications +------------ + +- [RAPPOR: Randomized Aggregatable Privacy-Preserving Ordinal Response](http://arxiv.org/abs/1407.6981) +- [Building a RAPPOR with the Unknown: Privacy-Preserving Learning of Associations and Data Dictionaries](http://arxiv.org/abs/1503.01214) + +Links +----- + +- [Google Blog Post about RAPPOR](http://googleresearch.blogspot.com/2014/10/learning-statistics-with-privacy-aided.html) +- [RAPPOR implementation in Chrome](http://www.chromium.org/developers/design-documents/rappor) + - This is a production quality C++ implementation, but it's somewhat tied to + Chrome, and doesn't support all privacy parameters (e.g. only a few values + of p and q). On the other hand, the code in this repo is not yet + production quality, but supports experimentation with different parameters + and data sets. Of course, anyone is free to implement RAPPOR independently + as well. +- Mailing list: [rappor-discuss@googlegroups.com][group] + +[group]: https://groups.google.com/forum/#!forum/rappor-discuss diff --git a/README.version b/README.version new file mode 100644 index 0000000..19aa180 --- /dev/null +++ b/README.version @@ -0,0 +1,4 @@ +URL: https://github.com/google/rappor +Version: a13fa964edb7c576366c83f40ff58d7c8c1db759 +BugComponent: 315013 +Owners: rickywai, pvisontay, simonjw diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..bd79b89 --- /dev/null +++ b/client/README.md @@ -0,0 +1,43 @@ +RAPPOR Clients +============== + +This directory contains RAPPOR client implementations in various languages. + +The privacy of RAPPOR is based on the client "lying" about the true values -- +that is, not sending them over the network. + +The clients are typically small in terms of code size because the RAPPOR +client algorithm is simple. See the README.md in each subdirectory for details +on how to use the library. + +Common Test Protocol +-------------------- + +When implementing a new RAPPOR client, you can get for free! + +The `regtest.sh` script in the root of this repository does the following: + +1. Create test input data and feed it into your client as a CSV file +2. Preprocesses your client output (also CSV) +3. Runs the RAPPOR analysis, learning aggregate statistics from encoded values +4. Compares the analysis to the true client values, with metrics and plots. + +To have your client tested, you need a small executable wrapper, which reads +and write as CSV file in a specified format. + +Then add it to the `_run-one-instance` function in `regtest.sh`. + +<!-- + +TODO: +- more details about protocol + +--> + + + + + + + + diff --git a/client/java/com/google/android/rappor/Encoder.java b/client/java/com/google/android/rappor/Encoder.java new file mode 100644 index 0000000..a8fb57c --- /dev/null +++ b/client/java/com/google/android/rappor/Encoder.java @@ -0,0 +1,613 @@ +package com.google.android.rappor; + +// BEGIN android-changed: Removed guava dependency +// import static com.google.common.base.Preconditions.checkArgument; +// +// import com.google.common.base.Verify; +// END android-changed + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.BitSet; +// BEGIN android-changed +import java.util.Random; +// END android-changed + +// BEGIN android-changed: Remove javax +// import javax.annotation.Nullable; +// import javax.annotation.concurrent.GuardedBy; +// END android-changed + +/** + * Encodes reports using the RAPPOR differentially-private encoding algorithm. + * + * The RAPPOR algorithm is described at go/rappor and presented in detail at go/rappor-writeup. + * + * @author bonawitz@google.com Keith Bonawitz + */ +// TODO(bonawitz): Make encoder and interface and make this a final class implementing it. +// We can't just make this final now because there exist tests that need to Mock it. +public class Encoder { + /** + * A non-decreasing version number. + * + * <p>The version number should increase any time the Encoder has a user-visible functional change + * to any of encoding algorithms or the interpretation of the input parameters. + */ + public static final long VERSION = 3; + + /** + * Minimum length required for the user secret, in bytes. + */ + public static final int MIN_USER_SECRET_BYTES = HmacDrbg.ENTROPY_INPUT_SIZE_BYTES; + + /** + * Maximum number of bits in the RAPPOR-encoded report. + * + * Must be less than HmacDrbg.MAX_BYTES_TOTAL; + */ + public static final int MAX_BITS = 4096; + + /** + * Maximum number of Bloom filter hashes used for RAPPOR-encoded strings. + * + * <p>This is constrained in the current implementation by requiring 2 bytes from an MD5 value + * (which is 16 bytes long) for each Bloom filter hash. + */ + public static final int MAX_BLOOM_HASHES = 8; + + /** + * Maximum number of cohorts supported. + */ + public static final int MAX_COHORTS = 128; + + /** + * First (and only) byte of HMAC_DRBG personalization strings used to generate the cohort number. + */ + private static final byte HMAC_DRBG_TYPE_COHORT = 0x00; + + /** + * First byte of HMAC_DRBG personalization strings used to generate the PRR response. + */ + private static final byte HMAC_DRBG_TYPE_PRR = 0x01; + + /** + * A unique identifier for this Encoder, represented in UTF-8. + * + * <p>The (userSecret, encoderId) pair identify a the logical memoization table used for RAPPOR's + * Permanent Randomized Response stage. Therefore, for any userSecret, each Encoder must have a + * distinct identifier for Permanent Randomized Response to be effective. + * + * <p>In practice, "memoization" is achieved by generating deterministic pseudo-random bits using + * HMAC_DRBG. encoderIdBytes is used as part of the personalization string. + */ + private final byte[] encoderIdBytes; + + /** + * The RAPPOR "f" probability, on the range [0.0, 1.0]. + * + * <p>This it the probability of replacing a bit from the input with a uniform random bit when + * generating the permanent randomized response. + * + * <p>Setting probabilityF to 0 disables the PRR phase of RAPPOR. + */ + private final double probabilityF; + + /** + * The RAPPOR "p" probability, on the range [0.0, 1.0]. + * + * <p>This is the probability of emitting a '1' bit in the instantaneous randomized response, + * given that the corresponding bit in the permanent randomized response is '0'. + * + * <p>Setting probabilityP to 0.0 and probabilityQ to 1.0 disables the IRR phase of RAPPOR. + */ + private final double probabilityP; + + /** + * The RAPPOR "1" probability, on the range [0.0, 1.0]. + * + * <p>This is the probability of emitting a 1 bit in the instantaneous randomized response, given + * that the corresponding bit in the permanent randomized response is 1. + * + * <p>Setting probabilityP to 0.0 and probabilityQ to 1.0 disables the IRR phase of RAPPOR. + */ + private final double probabilityQ; + + /** + * The number of bits in the RAPPOR-encoded report. + * + * <p>Must satisfy 1 <= numBits <= MAX_BITS. + * + * <ul> + * <li>For encodeOrdinal, requires 0 <= ordinal < numBits. + * <li>For encodeString, uses a numBits-wide Bloom filter. + * <li>For encodeBits, only the low-order numBits may be non-zero. + * </ul> + */ + private final int numBits; + + /** + * The number of hash functions used forming the Bloom filter encoding of a string. + * + * <p>Must satisfy 1 <= numBloomHashes <= MAX_BLOOM_HASHES. + */ + private final int numBloomHashes; + + /** + * The number of cohorts used for cohort assignment. + */ + private final int numCohorts; + + /** + * The cohort this user is assigned to for Bloom filter string encoding. + * + * <p>This value is stable for a given (userSecret, numCohorts) pair. Furthermore, if two + * encoders use the same userSecret but have different numCohorts values, the cohort assignment + * for the encoder with the smaller numCohorts value is a bitwise suffix of the cohort assignment + * for the encoder with the larger numCohorts value. It follows that, if you know maximum cohort + * assignment across a set of encoders, and you know the numCohorts setting for each encoder, then + * you can deduce the cohort assignment for each encoder by taking the bitwise-and of the max + * cohort value and (numCohorts-1), noting that numCohorts is required to be a positive power of + * 2. + */ + private final int cohort; + + /** + * A bitmask with 1 bits in all the positions where a RAPPOR-encoded report could have a 1 bit. + * + * <p>The bitmask has the lowest order numBits set to 1 and the rest 0. + */ + private final BitSet inputMask; + + /** + * SHA-256 utility class instance. + * + * <p>This object is stateful; access must be synchronized. The reset method must be + * called before each use. + */ +// BEGIN android-changed: Remove javax +// @GuardedBy("this") +// END android-changed + private final MessageDigest sha256; + + /** + * MD5 utility class instance. + * + * <p>This object is stateful; access must be synchronized. The reset method must be + * called before each use. + */ +// BEGIN android-changed: Remove javax +// @GuardedBy("this") +// END android-changed + private final MessageDigest md5; + + /** + * A SecureRandom instance, initialized with a cryptographically secure random seed. + */ +// BEGIN android-changed +// private final SecureRandom random; + private final Random random; +// BEGIN android-changed + + /** + * Entropy input for constructing HmacDrbg objects. + */ + private final byte[] userSecret; + + /** + * Constructs a new RAPPOR message encoder. + * + * @param userSecret Stable secret randomly selected for this user. UserSecret must be at least + * MIN_USER_SECRET_BYTES bytes of high-quality entropy. Changing the user secret clears the + * memoized cohort assignments and permanent randomized responses. Be aware that resetting + * these memoizations has significant privacy risks -- consult documentation at go/rappor for + * more details. + * @param encoderId Uniquely identifies this encoder. Used to differentiate momoized + * cohort assignments and permanent randomized responses. + * @param numBits The number of bits in the RAPPOR-encoded report. + * @param probabilityF The RAPPOR "f" probability, on the range [0.0, 1.0]. This will be + * quantized to the nearest 1/128. + * @param probabilityP The RAPPOR "p" probability, on the range [0.0, 1.0]. + * @param probabilityQ The RAPPOR "1" probability, on the range [0.0, 1.0]. + * @param numCohorts Number of cohorts into which the user pool is randomly segmented. + * @param numBloomHashes The number of hash functions used forming the Bloom filter encoding of a + * string. + */ + public Encoder(byte[] userSecret, String encoderId, int numBits, + double probabilityF, double probabilityP, double probabilityQ, + int numCohorts, int numBloomHashes) { + this( + null, // random + null, // md5, + null, // sha256, + userSecret, + encoderId, + numBits, + probabilityF, + probabilityP, + probabilityQ, + numCohorts, + numBloomHashes); + } + + /** + * Constructs a new RAPPOR message encoder, using constructor-style dependency injection. + * + * @param random A cryptographically secure random number generator, or null (in which case a + * new SecureRandom will be constructed). + * @param md5 A configured MD5 hash algorithm, or null (in which case a new MessageDigest will be + * constructed). Note: MessageDigest objects are stateful, and that state must not be + * modified while calls to the Encoder are active. + * @param sha256 A configured SHA-256 hash algorithm, or null (in which case a new MessageDigest + * will be constructed). Note: MessageDigest objects are stateful, and that state must not + * be modified while calls to the Encoder are active. + * @param userSecret Stable secret randomly selected for this user. UserSecret must be at least + * 32 bytes of high-quality entropy. Changing the user secret clears the memoized cohort + * assignments and permanent randomized responses. Be aware that resetting these memoizations + * has significant privacy risks -- consult documentation at go/rappor for more details. + * @param encoderId Uniquely identifies this encoder. Used to differentiate momoized + * cohort assignments and permanent randomized responses. + * @param numBits The number of bits in the RAPPOR-encoded report. + * @param probabilityF The RAPPOR "f" probability, on the range [0.0, 1.0]. This will be + * quantized to the nearest 1/128. + * @param probabilityP The RAPPOR "p" probability, on the range [0.0, 1.0]. + * @param probabilityQ The RAPPOR "1" probability, on the range [0.0, 1.0]. + * @param numCohorts Number of cohorts into which the user pool is randomly segmented. + * @param numBloomHashes The number of hash functions used forming the Bloom filter encoding of a + * string. + */ + public Encoder( +// BEGIN android-changed: Remove javax +// @Nullable SecureRandom random, +// @Nullable MessageDigest md5, +// @Nullable MessageDigest sha256, +// SecureRandom random, + Random random, + MessageDigest md5, + MessageDigest sha256, +// END android-changed + byte[] userSecret, + String encoderId, + int numBits, + double probabilityF, + double probabilityP, + double probabilityQ, + int numCohorts, + int numBloomHashes) { + if (md5 != null) { + this.md5 = md5; + } else { + try { + this.md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException impossible) { + // This should never happen. Every implementation of the Java platform + // is required to support MD5. + throw new AssertionError(impossible); + } + } + this.md5.reset(); + + if (sha256 != null) { + this.sha256 = sha256; + } else { + try { + this.sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException impossible) { + // This should never happen. Every implementation of the Java platform + // is required to support 256. + throw new AssertionError(impossible); + } + } + this.sha256.reset(); + + this.encoderIdBytes = encoderId.getBytes(StandardCharsets.UTF_8); + + if (random != null) { + this.random = random; + } else { + this.random = new SecureRandom(); + } + + // android-changed: Removed guava dependency + // checkArgument( + // userSecret.length >= MIN_USER_SECRET_BYTES, + // "userSecret must be at least %s bytes of high-quality entropy.", + // MIN_USER_SECRET_BYTES); + checkArgument( + userSecret.length >= MIN_USER_SECRET_BYTES, + "userSecret must be at least " + MIN_USER_SECRET_BYTES + + " bytes of high-quality entropy."); + this.userSecret = userSecret; + + checkArgument( + probabilityF >= 0 && probabilityF <= 1, "probabilityF must be on range [0.0, 1.0]"); + this.probabilityF = Math.round(probabilityF * 128) / 128.0; + + checkArgument( + probabilityP >= 0 && probabilityP <= 1, "probabilityP must be on range [0.0, 1.0]"); + this.probabilityP = probabilityP; + + checkArgument( + probabilityQ >= 0 && probabilityQ <= 1, "probabilityQ must be on range [0.0, 1.0]"); + this.probabilityQ = probabilityQ; + + checkArgument( + numBits >= 1 && numBits <= MAX_BITS, "numBits must be on range [1, " + MAX_BITS + "]."); + this.numBits = numBits; + // Make a bitmask with the lowest-order numBits set to 1. + this.inputMask = new BitSet(numBits); + this.inputMask.set(0, numBits, true); + + checkArgument( + numBloomHashes >= 1 && numBloomHashes <= numBits, + "numBloomHashes must be on range [1, numBits)."); + this.numBloomHashes = numBloomHashes; + + checkArgument( + numCohorts >= 1 && numCohorts <= MAX_COHORTS, + "numCohorts must be on range [1, " + MAX_COHORTS + "]."); + + // Assuming numCohorts >= 1: + // + // If numCohorts is a power of 2, then numCohorts has one bit set and numCohorts - 1 has only + // the bits to the right of numCohorts's bit set. The bitwise-and of these is 0. + // + // If numCohorts is not a power of 2, then numCohorts has at least two bits set. It follows + // subtracting one from numCohorts keeps the most significant bit set to 1, and thus the + // bitwise-and has at least this non-zero bit. + boolean numCohortsIsPowerOfTwo = (numCohorts & (numCohorts - 1)) == 0; + checkArgument(numCohortsIsPowerOfTwo, "numCohorts must be a power of 2."); + this.numCohorts = numCohorts; + + // cohortMasterAssignment depends only on the userSecret. + HmacDrbg cohortDrbg = new HmacDrbg(userSecret, new byte[] {HMAC_DRBG_TYPE_COHORT}); + ByteBuffer cohortDrbgBytes = ByteBuffer.wrap(cohortDrbg.nextBytes(4)); + int cohortMasterAssignment = Math.abs(cohortDrbgBytes.getInt()) % MAX_COHORTS; + this.cohort = cohortMasterAssignment & (numCohorts - 1); + } + + public double getProbabilityF() { + return probabilityF; + } + + public double getProbabilityP() { + return probabilityP; + } + + public double getProbabilityQ() { + return probabilityQ; + } + + public int getNumBits() { + return numBits; + } + + public int getNumBloomHashes() { + return numBloomHashes; + } + + public int getNumCohorts() { + return numCohorts; + } + + public int getCohort() { + return cohort; + } + + /** + * Returns the Encoder ID as a UTF-8 formatted string. + */ + public String getEncoderId() { + return new String(encoderIdBytes, StandardCharsets.UTF_8); + } + + /** + * Encodes a boolean into a RAPPOR report. + * + * <p>The boolean is 0 or 1, then encoded using permanent and instantaneous randomized response. + * + * <p>In most cases, numBits should be 1 when using this method. + */ + public byte[] encodeBoolean(boolean bool) { + BitSet input = new BitSet(numBits); + input.set(0, bool); + return encodeBits(input); + } + + /** + * Encodes an ordinal into a RAPPOR report. + * + * <p>The ordinal is represented using a 1-hot binary representation, then encoded using permanent + * and instantaneous randomized response. + * + * @param ordinal A value on the range [0, numBits). + */ + public byte[] encodeOrdinal(int ordinal) { + checkArgument( + ordinal >= 0 && ordinal < numBits, "Ordinal value must be in range [0, numBits)."); + BitSet input = new BitSet(numBits); + input.set(ordinal, true); + return encodeBits(input); + } + + /** + * Encodes a string into a RAPPOR report. + * + * <p>The string is represented using a Bloom filter with numBloomHashes hash functions, then + * encoded using permanent and instantaneous randomized response. + * + * @param string An arbitrary string. + */ + public byte[] encodeString(String string) { + // Implements a Bloom filter by slicing a single MD5 hash into numBloomHashes bit indices. + byte[] stringInUtf8 = string.getBytes(StandardCharsets.UTF_8); + byte[] message = + ByteBuffer.allocate(4 + stringInUtf8.length) + .putInt(cohort) + .put(stringInUtf8) + .array(); + + byte[] digest; + synchronized (this) { + md5.reset(); + digest = md5.digest(message); + } + // android-changed: Removed guava dependency + // Verify.verify(digest.length == 16); + // Verify.verify(numBloomHashes <= digest.length / 2); + verify(digest.length == 16); + verify(numBloomHashes <= digest.length / 2); + + BitSet input = new BitSet(numBits); + for (int i = 0; i < numBloomHashes; i++) { + // Convert byte pairs to ints on [0, 65535]. + // Anding with 0xFF converts signed byte to unsigned int. + int digestWord = (digest[i * 2] & 0xFF) * 256 + (digest[i * 2 + 1] & 0xFF); + int chosenBit = digestWord % numBits; + input.set(chosenBit, true); + } + + return encodeBits(input); + } + + /** + * Encodes an arbitrary bitstring into a RAPPOR report. + * + * @param bits A bitstring in which only the least significant numBits bits may be 1. + */ + public byte[] encodeBits(byte[] bits) { + return encodeBits(BitSet.valueOf(bits)); + } + + /** + * Encodes an arbitrary bitstring into a RAPPOR report. + * + * @param bits A bitstring in which only the least significant numBits bits may be 1. + */ + private byte[] encodeBits(BitSet bits) { + BitSet permanentRandomizedResponse = computePermanentRandomizedResponse(bits); + BitSet encodedBitSet = computeInstantaneousRandomizedResponse(permanentRandomizedResponse); + + // BitSet.toByteArray only returns enough bytes to capture the most significant bit + // that is set. For example, a BitSet with no bits set could return a length-0 array. + // In contrast, we guarantee that our output is sized according to numBits. + byte[] encodedBytes = encodedBitSet.toByteArray(); + byte[] output = new byte[(numBits + 7) / 8]; + // android-changed: Removed guava dependency + // Verify.verify(encodedBytes.length <= output.length); + verify(encodedBytes.length <= output.length); + System.arraycopy( + encodedBytes, // src + 0, // srcPos + output, // dest + 0, // destPos + encodedBytes.length); // length + return output; + } + + /** + * Returns the permanent randomized response for the given bits. + * + * <p>The response for a particular bits input is guaranteed to always be the same for any encoder + * constructed with the same parameters (including the encoderId and the userSecret). + */ + private BitSet computePermanentRandomizedResponse(BitSet bits) { + // Ensures that the input only has bits set in the lowest + BitSet masked = new BitSet(); + masked.or(bits); + masked.andNot(inputMask); + checkArgument(masked.isEmpty(), "Input bits had bits set past Encoder's numBits limit."); + + if (probabilityF == 0.0) { + return bits; + } + + // Builds a personalization string that contains both the encoderId and input value (bits), + // and is no longer than HmacDrbg.MAX_PERSONALIZATION_STRING_LENGTH_BYTES. The first byte + // of the personalization string is always HMAC_DRBG_TYPE_PRR, to avoid collisions with the + // cohort-generation personalization string. + byte[] personalizationString; + synchronized (this) { + int personalizationStringLength = + Math.min(HmacDrbg.MAX_PERSONALIZATION_STRING_LENGTH_BYTES, 1 + sha256.getDigestLength()); + personalizationString = new byte[personalizationStringLength]; + personalizationString[0] = HMAC_DRBG_TYPE_PRR; + sha256.reset(); + sha256.update(encoderIdBytes); + sha256.update(new byte[] {0}); + sha256.update(bits.toByteArray()); + byte[] digest = sha256.digest(personalizationString); + System.arraycopy(digest, 0, personalizationString, 1, personalizationString.length - 1); + } + + HmacDrbg drbg = new HmacDrbg(userSecret, personalizationString); + byte[] pseudorandomStream = drbg.nextBytes(numBits); + // android-changed: Removed guava dependency + // Verify.verify(numBits <= pseudorandomStream.length); + verify(numBits <= pseudorandomStream.length); + + int probabilityFTimes128 = (int) Math.round(probabilityF * 128); + BitSet result = new BitSet(numBits); + for (int i = 0; i < numBits; i++) { + // Grabs a single byte from the pseudorandom stream. + // Anding with 0xFF converts a signed byte to an unsigned integer. + int pseudorandomByte = pseudorandomStream[i] & 0xFF; + + // Uses bits 1-7 to get a random number between 0 and 127. + int uniform0to127 = pseudorandomByte >> 1; + boolean shouldUseNoise = uniform0to127 < probabilityFTimes128; + + if (shouldUseNoise) { + // Uses bit 0 as a flip of a fair coin. + result.set(i, (pseudorandomByte & 0x01) > 0); + } else { + result.set(i, bits.get(i)); + } + } + return result; + } + + /** + * Returns the instantaneous randomized response for the given bits. + * + * <p>The instantaneous response is NOT memoized -- it is sampled randomly on + * every invocation. + */ + private BitSet computeInstantaneousRandomizedResponse(BitSet bits) { + // Ensures that the input only has bits set in the lowest + BitSet masked = new BitSet(); + masked.or(bits); + masked.andNot(inputMask); + checkArgument(masked.isEmpty(), "Input bits had bits set past Encoder's numBits limit."); + + if (probabilityP == 0.0 && probabilityQ == 1.0) { + return bits; + } + + BitSet response = new BitSet(numBits); + for (int i = 0; i < numBits; i++) { + boolean bit = bits.get(i); + double probability = bit ? probabilityQ : probabilityP; + boolean responseBit = random.nextFloat() < probability; + response.set(i, responseBit); + } + return response; + } + + // BEGIN android-changed: Added guava methods + private static void checkArgument(boolean expression, Object errorMessage) { + if (!expression) { + throw new IllegalArgumentException(String.valueOf(errorMessage)); + } + } + + private static void verify(boolean expression) { + if (!expression) { + throw new java.lang.IllegalStateException(); + } + } + // END android-changed +} diff --git a/client/java/com/google/android/rappor/HmacDrbg.java b/client/java/com/google/android/rappor/HmacDrbg.java new file mode 100644 index 0000000..db99700 --- /dev/null +++ b/client/java/com/google/android/rappor/HmacDrbg.java @@ -0,0 +1,264 @@ +package com.google.android.rappor; + +// BEGIN android-changed: Removed guava dependency +// import com.google.common.hash.HashFunction; +// import com.google.common.hash.Hashing; +// import com.google.common.primitives.Bytes; +// END android-changed + +import java.security.SecureRandom; +import java.util.Arrays; +import javax.annotation.concurrent.NotThreadSafe; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +/** + * Deterministic Random Bit Generator based on HMAC-SHA256. + * + * Also known as: HMAC_DRBG. + * See http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf for thorough specification. + * + * Reseeding is not supported. Instead, construct a new DRBG when reseeding is required. + * See http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf Section 8.6.8. + */ +@NotThreadSafe +public class HmacDrbg { + // "V" from the the spec. + private byte[] value; + + // BEGIN android-changed + // An instance of HMAC-SHA256 configured with "Key" from the spec. + // private HashFunction hmac; + private Mac hmac; + // END android-changed + + // The total number of bytes that have been generated from this DRBG so far. + private int bytesGenerated; + + // Assume maximum security strength for HMAC-256, which is 256. + // See: http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf D.2 #1. + public static final int SECURITY_STRENGTH = 256; + + /** + * Personalization strings should not exceed this many bytes in length. + * + * See: http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf D.2 #7. + */ + public static final int MAX_PERSONALIZATION_STRING_LENGTH_BYTES = 160 / 8; + + /** + * The constructor's entropyInput should contain this many high quality random bytes. + * HMAC_DRBG requires entropy input to be security_strength bits long, + * and nonce to be at least 1/2 security_strength bits long. We + * generate them both as a single "extra strong" entropy input. + * See: http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf + */ + public static final int ENTROPY_INPUT_SIZE_BYTES = (SECURITY_STRENGTH / 8) * 3 / 2; + + /** + * The maximum total number of bytes that can be generated from this DRBG. + * + * This is conservative releative to the suggestions in + * http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf section D.2, + * ensuring that reseeding is never triggered (each call to Generate produces at least one byte, + * therefore MAX_BYTES will be reached before RESEED_INTERAL=10000 is exceeded) + * and simplifying the interface (so that the client need not worry about MAX_BYTES_PER_REQUEST, + * below. + */ + public static final int MAX_BYTES_TOTAL = 10000; + + // See: http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf D.2 #2. + private static final int DIGEST_NUM_BYTES = 256 / 8; + + // floor(7500/8); see: http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf D.2 #5. + private static final int MAX_BYTES_PER_REQUEST = 937; + + private static final byte[] BYTE_ARRAY_0 = {0}; + private static final byte[] BYTE_ARRAY_1 = {1}; + + public HmacDrbg(byte[] entropyInput, byte[] personalizationString) { + // HMAC_DRBG Instantiate Process + // See: http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf 10.1.1.2 + + // 1. seed_material = entropy_input + nonce + personalization_string + // Note: We are using the 8.6.7 interpretation, where the entropy_input and + // nonce are acquired at the same time from the same source. + // BEGIN android-changed + // byte[] seedMaterial = Bytes.concat(entropyInput, emptyIfNull(personalizationString)); + byte[] seedMaterial = bytesConcat(entropyInput, emptyIfNull(personalizationString)); + // END android-changed + + // 2. Key = 0x00 00...00 + setKey(new byte[256 / 8]); + + // 3. V = 0x01 01...01 + value = new byte[DIGEST_NUM_BYTES]; + Arrays.fill(value, (byte) 0x01); + + // 4. (Key, V) = HMAC_DRBG_Update(seed_material, Key, V) + hmacDrbgUpdate(seedMaterial); + + bytesGenerated = 0; + } + + /** + * Returns an 0-length byte array if b is null, otherwise returns b. + */ + private static byte[] emptyIfNull(byte[] b) { + return b == null ? new byte[0] : b; + } + + /** + * Set's the "Key" state from the spec. + */ + private void setKey(byte[] key) { + // BEGIN android-changed + // hmac = Hashing.hmacSha256(key); + try { + hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secretKey = new SecretKeySpec(key, "HmacSHA256"); + hmac.init(secretKey); + } catch (Exception e) {} + // END android-changed + } + + /** + * Computes hmac("key" from the spec, x). + */ + private byte[] hash(byte[] x) { + // BEGIN android-changed + // return hmac.hashBytes(x).asBytes(); + try { + return hmac.doFinal(x); + } catch (Exception e) { + return null; + } + // END android-changed + } + + /** + * HMAC_DRBG Update Process + * + * See: http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf 10.1.2.2 + */ + private void hmacDrbgUpdate(byte[] providedData) { + + // 1. K = HMAC(K, V || 0x00 || provided_data) + // BEGIN android-changed + // setKey(hash(Bytes.concat(value, BYTE_ARRAY_0, emptyIfNull(providedData)))); + setKey(hash(bytesConcat(value, BYTE_ARRAY_0, emptyIfNull(providedData)))); + // END android-changed + + // 2. V = HMAC(K, V); + value = hash(value); + + // 3. If (provided_data = Null), then return K and V. + if (providedData == null) { + return; + } + + // 4. K = HMAC (K, V || 0x01 || provided_data). + // BEGIN android-changed + // setKey(hash(Bytes.concat(value, BYTE_ARRAY_1, providedData))); + setKey(hash(bytesConcat(value, BYTE_ARRAY_1, providedData))); + // END android-changed + + // 5. V = HMAC (K, V). + value = hash(value); + } + + /** + * HMAC_DRBG Generate Process + * + * See: http://csrc.nist.gov/publications/nistpubs/800-90A/SP800-90A.pdf 10.1.2.5 + * + * We do not support additional_input, assuming it to be always null. + * + * We guarantee that reseeding is never required through the use of MAX_BYTES_TOTAL + * rather than RESEED_INTERVAL. + */ + private void hmacDrbgGenerate(byte[] out, int start, int count) { + // 3. temp = Null. + int bytesWritten = 0; + + // 4. While (len (temp) < requested_number_of_bits) do: + while (bytesWritten < count) { + // 4.1 V = HMAC (Key, V). + value = hash(value); + + // 4.2 temp = temp || V. + // 5. returned_bits = Leftmost requested_number_of_bits of temp + int bytesToWrite = Math.min(count - bytesWritten, DIGEST_NUM_BYTES); + System.arraycopy(value, 0, out, start + bytesWritten, bytesToWrite); + bytesWritten += bytesToWrite; + } + + // 6. (Key, V) = HMAC_DRBG_Update (additional_input, Key, V). + hmacDrbgUpdate(null); + } + + /** + * Generates entropy byte-string suitable for use as the constructor's entropyInput. + * + * Uses SecureRandom to generate entropy. + */ + public static byte[] generateEntropyInput() { + byte result[] = new byte[ENTROPY_INPUT_SIZE_BYTES]; + new SecureRandom().nextBytes(result); + return result; + } + + /** + * Returns the next length pseudo-random bytes. + */ + public byte[] nextBytes(int length) { + byte result[] = new byte[length]; + nextBytes(result); + return result; + } + + /** + * Fills the output vector with pseudo-random bytes. + */ + public void nextBytes(byte[] out) { + nextBytes(out, 0, out.length); + } + + /** + * Fills out[start] through out[start+count-1] (inclusive) with pseudo-random bytes. + */ + public void nextBytes(byte[] out, int start, int count) { + if (count == 0) { + return; + } + if (bytesGenerated + count > MAX_BYTES_TOTAL) { + throw new IllegalStateException("Cannot generate more than a total of " + count + " bytes."); + } + try { + int bytesWritten = 0; + while (bytesWritten < count) { + int bytesToWrite = Math.min(count - bytesWritten, MAX_BYTES_PER_REQUEST); + hmacDrbgGenerate(out, start + bytesWritten, bytesToWrite); + bytesWritten += bytesToWrite; + } + } finally { + bytesGenerated += count; + } + } + + // BEGIN android-changed + private static byte[] bytesConcat(byte[]... arrays) { + int length = 0; + for (byte[] array : arrays) { + length += array.length; + } + byte[] result = new byte[length]; + int pos = 0; + for (byte[] array : arrays) { + System.arraycopy(array, 0, result, pos, array.length); + pos += array.length; + } + return result; + } + // END android-changed +} diff --git a/client/javatest/com/google/android/rappor/EncoderTest.java b/client/javatest/com/google/android/rappor/EncoderTest.java new file mode 100644 index 0000000..896322f --- /dev/null +++ b/client/javatest/com/google/android/rappor/EncoderTest.java @@ -0,0 +1,1044 @@ +package com.google.android.rappor; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.BlockJUnit4ClassRunner; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +// BEGIN android-changed +// import java.security.SecureRandom; +import java.util.Random; +// END android-changed + +/** + * Unit tests for {@link Encoder}. + */ +@RunWith(BlockJUnit4ClassRunner.class) +public class EncoderTest { + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + /** + * Convert a human readable string to a 32 byte userSecret for testing. + * + * <p>Do not use this in a production environment! For security, userSecret + * must be at least 32 bytes of high-quality entropy. + */ + private static byte[] makeTestingUserSecret(String testingSecret) throws Exception { + // We generate the fake user secret by concatenating three copies of the + // 16 byte MD5 hash of the testingSecret string encoded in UTF 8. + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digest = md5.digest(testingSecret.getBytes(StandardCharsets.UTF_8)); + assertEquals(16, digest.length); + return ByteBuffer.allocate(48).put(digest).put(digest).put(digest).array(); + } + + private static long toLong(byte[] bytes) { + assertThat(bytes.length, is(lessThanOrEqualTo(8))); + ByteBuffer buffer = ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).put(bytes); + buffer.rewind(); // can't chain rewind() because it returns Buffer, not ByteBuffer. + return buffer.getLong(); + } + + private static byte[] toBytes(long value) { + return ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(value).array(); + } + + @Test + public void testEncoderConstruction_goodArguments() throws Exception { + // Full RAPPOR + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + + // IRR-only (no PRR) + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + + // PRR-only (no IRR) + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.0, // probabilityP + 1.0, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_userSecretTooShort() throws Exception { + thrown.expect(IllegalArgumentException.class); + byte[] tooShortSecret = new byte[47]; + new Encoder(tooShortSecret, // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_userSecretMayBeLong() throws Exception { + byte[] tooLongSecret = new byte[49]; + new Encoder(tooLongSecret, // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_numBitsTooLow() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 0, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_numBitsTooHigh() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 4097, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_probabilityFTooLow() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + -0.01, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_probabilityFTooHigh() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 1.01, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_probabilityPTooLow() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + -0.01, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_probabilityPTooHigh() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 1.01, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_probabilityQTooLow() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + -0.01, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_probabilityQTooHigh() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.75, // probabilityP + 1.01, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_numCohortsTooLow() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 0, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_numCohortsTooHigh() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + Encoder.MAX_COHORTS + 1, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_numCohortsNotPowerOf2() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 3, // numCohorts + 2); // numBloomHashes + } + + @Test + public void testEncoderConstruction_numBloomHashesTooLow() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 0); // numBloomHashes + } + + @Test + public void testEncoderConstruction_numBloomHashesTooHigh() throws Exception { + thrown.expect(IllegalArgumentException.class); + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 9); // numBloomHashes + } + + @Test + public void testEncoderGetCohort() throws Exception { + // This is a stable, random cohort assignment. + assertEquals( + 3, + new Encoder(makeTestingUserSecret("Blotto"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 4, // numCohorts + 2) // numBloomHashes + .getCohort()); + + // With numCohorts == 1, the only possible cohort assigment is 0. + assertEquals( + 0, + new Encoder(makeTestingUserSecret("Blotto"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .getCohort()); + + // Changing the user secret changes the cohort. + assertEquals( + 3, + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 4, // numCohorts + 2) // numBloomHashes + .getCohort()); + + assertEquals( + 0, + new Encoder( + makeTestingUserSecret("Bar2"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 4, // numCohorts + 2) // numBloomHashes + .getCohort()); + + // Changing the encoder id does not changes the cohort. + assertEquals( + 3, + new Encoder(makeTestingUserSecret("Blotto"), // userSecret + "Foo1", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 4, // numCohorts + 2) // numBloomHashes + .getCohort()); + + assertEquals( + 3, + new Encoder(makeTestingUserSecret("Blotto"), // userSecret + "Foo2", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 4, // numCohorts + 2) // numBloomHashes + .getCohort()); + + assertEquals( + 3, + new Encoder(makeTestingUserSecret("Blotto"), // userSecret + "Foo3", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 4, // numCohorts + 2) // numBloomHashes + .getCohort()); + + // Cohort assignments are bit-wise subsets + int cohortAssignmentBig = + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + Encoder.MAX_COHORTS, // numCohorts + 2) // numBloomHashes + .getCohort(); + + int numCohortsSmall = Encoder.MAX_COHORTS / 2; + // Verify that numCohortsSmall is a power of 2. + assertEquals(0, numCohortsSmall & (numCohortsSmall - 1)); + int cohortAssignmentSmall = + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + numCohortsSmall, // numCohorts + 2) // numBloomHashes + .getCohort(); + + // This validates that the test case is well chosen. If it fails, select a different userSecret + // or encoderId. + assertNotEquals(cohortAssignmentBig, cohortAssignmentSmall); + + // Test that cohortAssignmentSmall is a suffix of cohortAssignmentBig when represented in + // binary. + assertEquals(cohortAssignmentBig & (numCohortsSmall - 1), cohortAssignmentSmall); + } + + @Test + public void testEncoderEncodeBits_identity() throws Exception { + assertEquals( + 0b11111101L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(0b11111101L)))); + + assertEquals( + 0xD56B8119L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 32, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(0xD56B8119L)))); + } + + @Test + public void testEncoderEncodeBits_tooHigh() throws Exception { + Encoder encoder = new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + thrown.expect(IllegalArgumentException.class); + encoder.encodeBits(toBytes(0x100)); // 9 bits + } + + @Test + public void testEncoderEncodeBoolean_identity() throws Exception { + assertEquals( + 0x1L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 1, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 1) // numBloomHashes + .encodeBoolean(true))); + + assertEquals( + 0x0L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 1, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 1) // numBloomHashes + .encodeBoolean(false))); + } + + @Test + public void testEncoderEncodeOrdinal_identity() throws Exception { + assertEquals( + 0b000000000001L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 1) // numBloomHashes + .encodeOrdinal(0))); + + assertEquals( + 0b100000000000L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 1) // numBloomHashes + .encodeOrdinal(11))); + } + + @Test + public void testEncoderEncodeOrdinal_tooLow() throws Exception { + Encoder encoder = new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 1); // numBloomHashes + thrown.expect(IllegalArgumentException.class); + encoder.encodeOrdinal(-1); + } + + @Test + public void testEncoderEncodeOrdinal_tooHigh() throws Exception { + Encoder encoder = new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 1); // numBloomHashes + thrown.expect(IllegalArgumentException.class); + encoder.encodeOrdinal(12); + } + + @Test + public void testEncoderEncodeString_identity() throws Exception { + assertEquals( + 0b000010000100L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts (so must be cohort 0) + 2) // numBloomHashes + .encodeString("Whizbang"))); + + // Changing the user but keeping the cohort the same (both cohort 0) + // results in the same encoding. + assertEquals( + 0b000010000100L, + toLong( + new Encoder( + makeTestingUserSecret("Blotto"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts (so must be cohort 0) + 2) // numBloomHashes + .encodeString("Whizbang"))); + + // When the user is in a different cohort, she gets a different encoding. + Encoder cohortProbeEncoder = + new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 4, // numCohorts + 2); // numBloomHashes + assertEquals(3, cohortProbeEncoder.getCohort()); + assertEquals(0b000011000000L, toLong(cohortProbeEncoder.encodeString("Whizbang"))); + + // Changing the string gets a different encoding. + assertEquals( + 0b001001000000L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts (so must be cohort 0) + 2) // numBloomHashes + .encodeString("Xyzzy"))); + + assertEquals( + 0b000000110000L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 12, // numBits, + 0, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts (so must be cohort 0) + 2) // numBloomHashes + .encodeString("Thud"))); + } + + + @Test + public void testEncoderEncodeBits_prrMemoizes() throws Exception { + assertEquals( + 0b01110101L, + toLong( + new Encoder( + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 0.25, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(0b11111101L)))); + + assertEquals( + 0b11111101L, + toLong( + new Encoder( + makeTestingUserSecret("Baz"), // userSecret + "Foo", // encoderId + 8, // numBits, + 0.25, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(0b11111101L)))); + } + + @Test + public void testEncoderEncodeBits_prrFlipProbability() throws Exception { + int numSamples = 10000; + int numBits = 8; + double probabilityF = 1.0 / 32.0; + long inputValue = 0b11111101L; + + int counts[] = new int[64]; + for (int iSample = 0; iSample < numSamples; iSample++) { + Encoder encoder = + new Encoder(makeTestingUserSecret("User" + iSample), // userSecret + "Foo", // encoderId + numBits, // numBits, + probabilityF, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + long encoded = toLong(encoder.encodeBits(toBytes(inputValue))); + assertEquals(encoded, toLong(encoder.encodeBits(toBytes(inputValue)))); + + for (int iBit = 0; iBit < numBits; iBit++) { + if ((encoded & (1L << iBit)) != 0) { + counts[iBit]++; + } + } + } + + assertEquals(9843, counts[0]); // input = 1, expectation = 9843.75 + assertEquals(173, counts[1]); // input = 0, expectation = 156.25 + assertEquals(9839, counts[2]); // input = 1, expectation = 9843.75 + assertEquals(9831, counts[3]); // input = 1, expectation = 9843.75 + assertEquals(9848, counts[4]); // input = 1, expectation = 9843.75 + assertEquals(9828, counts[5]); // input = 1, expectation = 9843.75 + assertEquals(9834, counts[6]); // input = 1, expectation = 9843.75 + assertEquals(9837, counts[7]); // input = 1, expectation = 9843.75 + + // Check that no high-order bit past numBits ever got set. + for (int iBit = numBits; iBit < 64; iBit++) { + assertEquals(0, counts[iBit]); + } + } + + @Test + public void testEncoderEncodeBits_irrFlipProbability() throws Exception { + int numBits = 8; + double probabilityP = 0.25; + double probabilityQ = 0.85; + long inputValue = 0b11111101L; + +// BEGIN android-changed +// SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + Random random = new Random(); +// END android-changed + random.setSeed(0x12345678L); + + int counts[] = new int[64]; + for (int iSample = 0; iSample < 10000; iSample++) { + Encoder encoder = + new Encoder( + random, + null, // md5 + null, // sha256 + makeTestingUserSecret("User" + iSample), // userSecret + "Foo", // encoderId + numBits, // numBits, + 0, // probabilityF + probabilityP, // probabilityP + probabilityQ, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + long encoded = toLong(encoder.encodeBits(toBytes(inputValue))); + + for (int iBit = 0; iBit < numBits; iBit++) { + if ((encoded & (1L << iBit)) != 0) { + counts[iBit]++; + } + } + } + +// BEGIN android-changed +// assertEquals(8481, counts[0]); // input = 1, 99.99% CI = [8358, 8636] +// assertEquals(2477, counts[1]); // input = 0, 99.99% CI = [2332, 2669] +// assertEquals(8486, counts[2]); // input = 1, 99.99% CI = [8358, 8636] +// assertEquals(8495, counts[3]); // input = 1, 99.99% CI = [8358, 8636] +// assertEquals(8563, counts[4]); // input = 1, 99.99% CI = [8358, 8636] +// assertEquals(8560, counts[5]); // input = 1, 99.99% CI = [8358, 8636] +// assertEquals(8481, counts[6]); // input = 1, 99.99% CI = [8358, 8636] +// assertEquals(8491, counts[7]); // input = 1, 99.99% CI = [8358, 8636] + assertEquals(8492, counts[0]); // input = 1, 99.99% CI = [8358, 8636] + assertEquals(2510, counts[1]); // input = 0, 99.99% CI = [2332, 2669] + assertEquals(8476, counts[2]); // input = 1, 99.99% CI = [8358, 8636] + assertEquals(8509, counts[3]); // input = 1, 99.99% CI = [8358, 8636] + assertEquals(8406, counts[4]); // input = 1, 99.99% CI = [8358, 8636] + assertEquals(8482, counts[5]); // input = 1, 99.99% CI = [8358, 8636] + assertEquals(8498, counts[6]); // input = 1, 99.99% CI = [8358, 8636] + assertEquals(8533, counts[7]); // input = 1, 99.99% CI = [8358, 8636] +// END android-changed + + // Check that no high-order bit past numBits ever got set. + for (int iBit = numBits; iBit < 64; iBit++) { + assertEquals(0, counts[iBit]); + } + } + + @Test + public void testEncoderEncodeBits_endToEnd() throws Exception { + int numBits = 8; + + long inputValue = 0b11111101L; + long prrValue = 0b01110101L; +// BEGIN android-changed +// long prrAndIrrValue = 0b01110110L; + long prrAndIrrValue = 0b00111101L; +// END android-changed + + // Verify that PRR is working as expected. + assertEquals( + prrValue, + toLong( + new Encoder( + null, + null, // md5 + null, // sha256 + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0, // probabilityP + 1, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(inputValue)))); + + // Verify that IRR is working as expected. +// BEGIN android-changed +// SecureRandom random1 = SecureRandom.getInstance("SHA1PRNG"); + Random random1 = new Random(); +// END android-changed + random1.setSeed(0x12345678L); + assertEquals( + prrAndIrrValue, + toLong( + new Encoder( + random1, + null, // md5 + null, // sha256 + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + numBits, // numBits, + 0, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(prrValue)))); + + // Test that end-to-end is the result of PRR + IRR. +// BEGIN android-changed +// SecureRandom random2 = SecureRandom.getInstance("SHA1PRNG"); + Random random2 = new Random(); +// END android-changed + random2.setSeed(0x12345678L); + assertEquals( + prrAndIrrValue, + toLong( + new Encoder( + random2, + null, // md5 + null, // sha256 + makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(inputValue)))); + } + + @Test + public void testEncoderEncodeBits_32BitValuesEncodeSuccessfully() throws Exception { + // Regression test for b/22035650. + int numBits = 32; + byte[] userSecret = makeTestingUserSecret("Bar"); + + // Explicitly spot-check the output for 2^0 and 2^31. + long inputValue0 = 1L; +// BEGIN android-changed +// long outputValue0 = 590349342L; +// SecureRandom random0 = SecureRandom.getInstance("SHA1PRNG"); + long outputValue0 = 1117977L; + Random random0 = new Random(); +// END android-changed + random0.setSeed(0x12345678L); + assertEquals( + outputValue0, + toLong( + new Encoder( + random0, + null, // md5 + null, // sha256 + userSecret, // userSecret + "MyEncoder", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(inputValue0)))); + + long inputValue31 = 1L << 31; +// BEGIN android-changed +// long outputValue31 = 2746482838L; +// SecureRandom random31 = SecureRandom.getInstance("SHA1PRNG"); + long outputValue31 = 9505692L; + Random random31 = new Random(); +// END android-changed + random31.setSeed(0x12345678L); + assertEquals( + outputValue31, + toLong( + new Encoder( + random31, + null, // md5 + null, // sha256 + userSecret, // userSecret + "MyEncoder", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(inputValue31)))); + + // Check the range 2^1 to 2^30, making sure no values produce exceptions. +// BEGIN android-changed +// SecureRandom randomRange = SecureRandom.getInstance("SHA1PRNG"); + Random randomRange = new Random(); +// END android-changed + randomRange.setSeed(0x12345678L); + for (int i = 1; i <= 30; i++) { + long inputValue = 1L << (i - 1); + new Encoder( + randomRange, + null, // md5 + null, // sha256 + userSecret, // userSecret + "MyEncoder", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(inputValue)); + } + } + + @Test + public void testEncoderEncodeBits_63BitValuesEncodeSuccessfully() throws Exception { + int numBits = 63; + byte[] userSecret = makeTestingUserSecret("Bar"); + + // Explicitly spot-check the output for 2^0 and 2^63. + long inputValue0 = 1L; +// BEGIN android-changed +// long outputValue0 = 867402030798341150L; +// SecureRandom random0 = SecureRandom.getInstance("SHA1PRNG"); + long outputValue0 = 5802301002133606169L; + Random random0 = new Random(); +// END android-changed + random0.setSeed(0x12345678L); + assertEquals( + outputValue0, + toLong( + new Encoder( + random0, + null, // md5 + null, // sha256 + userSecret, // userSecret + "MyEncoder", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(inputValue0)))); + + long inputValue63 = 1L << 62; +// BEGIN android-changed +// long outputValue63 = 5497102447743615126L; +// SecureRandom random63 = SecureRandom.getInstance("SHA1PRNG"); + long outputValue63 = 5874921546135456664L; + Random random63 = new Random(); +// END android-changed + random63.setSeed(0x12345678L); + assertEquals( + outputValue63, + toLong( + new Encoder( + random63, + null, // md5 + null, // sha256 + userSecret, // userSecret + "MyEncoder", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(inputValue63)))); + + // Check the range 2^1 to 2^62, making sure no values produce exceptions. +// BEGIN android-changed +// SecureRandom randomRange = SecureRandom.getInstance("SHA1PRNG"); + Random randomRange = new Random(); +// END android-changed + randomRange.setSeed(0x12345678L); + for (int i = 1; i <= 62; i++) { + long inputValue = 1L << (i - 1); + new Encoder( + randomRange, + null, // md5 + null, // sha256 + userSecret, // userSecret + "MyEncoder", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeBits(toBytes(inputValue)); + } + } + + @Test + public void testEncoderEncodeBits_4096BitValuesEncodeSuccessfully() throws Exception { + int numBits = 4096; + byte[] userSecret = makeTestingUserSecret("Bar"); + + // Check the range 2^1 to 2^4095, making sure no values produce exceptions. +// BEGIN android-changed +// SecureRandom randomRange = SecureRandom.getInstance("SHA1PRNG"); + Random randomRange = new Random(); +// END android-changed + randomRange.setSeed(0x12345678L); + // Stride is arbitrary, but chosen to be large enough to not cause too many probes (~40) and + // prime to explore well. + int stride = 97; + for (int i = 1; i < numBits; i += stride) { + new Encoder( + randomRange, + null, // md5 + null, // sha256 + userSecret, // userSecret + "MyEncoder", // encoderId + numBits, // numBits, + 0.25, // probabilityF + 0.3, // probabilityP + 0.7, // probabilityQ + 1, // numCohorts + 2) // numBloomHashes + .encodeOrdinal(i); + } + } + + @Test + public void testGetEncoderId() throws Exception { + Encoder encoder = new Encoder(makeTestingUserSecret("Bar"), // userSecret + "Foo", // encoderId + 8, // numBits, + 13.0 / 128.0, // probabilityF + 0.25, // probabilityP + 0.75, // probabilityQ + 1, // numCohorts + 2); // numBloomHashes + assertEquals("Foo", encoder.getEncoderId()); + } +} diff --git a/client/javatest/com/google/android/rappor/HmacDrbgTest.java b/client/javatest/com/google/android/rappor/HmacDrbgTest.java new file mode 100644 index 0000000..e8b0f49 --- /dev/null +++ b/client/javatest/com/google/android/rappor/HmacDrbgTest.java @@ -0,0 +1,653 @@ +package com.google.android.rappor; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import com.google.common.io.BaseEncoding; +import com.google.common.primitives.Bytes; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** + * Unit tests for {@link HmacDrbg}. + * + * Test vectors come from NIST's SHA-256 HMAC_DRBG no reseed known input test vectors at + * http://csrc.nist.gov/groups/STM/cavp/random-number-generation.html#drbgvs + */ +@RunWith(JUnit4.class) +public final class HmacDrbgTest { + + private byte[] hexToBytes(String s) { + return BaseEncoding.base16().decode(s.toUpperCase()); + } + + // ==== Test vectors for HMAC_DRBG with no personalization. ==== + @Test + public void testHmacDrbgNistCase0() { + byte[] entropy = + hexToBytes("ca851911349384bffe89de1cbdc46e6831e44d34a4fb935ee285dd14b71a7488"); + byte[] nonce = hexToBytes("659ba96c601dc69fc902940805ec0ca8"); + byte[] expected = hexToBytes( + "e528e9abf2dece54d47c7e75e5fe302149f817ea9fb4bee6f4199697d04d5b89" + + "d54fbb978a15b5c443c9ec21036d2460b6f73ebad0dc2aba6e624abf07745bc1" + + "07694bb7547bb0995f70de25d6b29e2d3011bb19d27676c07162c8b5ccde0668" + + "961df86803482cb37ed6d5c0bb8d50cf1f50d476aa0458bdaba806f48be9dcb8"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase1() { + byte[] entropy = + hexToBytes("79737479ba4e7642a221fcfd1b820b134e9e3540a35bb48ffae29c20f5418ea3"); + byte[] nonce = hexToBytes("3593259c092bef4129bc2c6c9e19f343"); + byte[] expected = hexToBytes( + "cf5ad5984f9e43917aa9087380dac46e410ddc8a7731859c84e9d0f31bd43655" + + "b924159413e2293b17610f211e09f770f172b8fb693a35b85d3b9e5e63b1dc25" + + "2ac0e115002e9bedfb4b5b6fd43f33b8e0eafb2d072e1a6fee1f159df9b51e6c" + + "8da737e60d5032dd30544ec51558c6f080bdbdab1de8a939e961e06b5f1aca37"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase2() { + byte[] entropy = + hexToBytes("b340907445b97a8b589264de4a17c0bea11bb53ad72f9f33297f05d2879d898d"); + byte[] nonce = hexToBytes("65cb27735d83c0708f72684ea58f7ee5"); + byte[] expected = hexToBytes( + "75183aaaf3574bc68003352ad655d0e9ce9dd17552723b47fab0e84ef903694a" + + "32987eeddbdc48efd24195dbdac8a46ba2d972f5808f23a869e71343140361f5" + + "8b243e62722088fe10a98e43372d252b144e00c89c215a76a121734bdc485486" + + "f65c0b16b8963524a3a70e6f38f169c12f6cbdd169dd48fe4421a235847a23ff"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase3() { + byte[] entropy = + hexToBytes("8e159f60060a7d6a7e6fe7c9f769c30b98acb1240b25e7ee33f1da834c0858e7"); + byte[] nonce = hexToBytes("c39d35052201bdcce4e127a04f04d644"); + byte[] expected = hexToBytes( + "62910a77213967ea93d6457e255af51fc79d49629af2fccd81840cdfbb491099" + + "1f50a477cbd29edd8a47c4fec9d141f50dfde7c4d8fcab473eff3cc2ee9e7cc9" + + "0871f180777a97841597b0dd7e779eff9784b9cc33689fd7d48c0dcd341515ac" + + "8fecf5c55a6327aea8d58f97220b7462373e84e3b7417a57e80ce946d6120db5"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase4() { + byte[] entropy = + hexToBytes("74755f196305f7fb6689b2fe6835dc1d81484fc481a6b8087f649a1952f4df6a"); + byte[] nonce = hexToBytes("c36387a544a5f2b78007651a7b74b749"); + byte[] expected = hexToBytes( + "b2896f3af4375dab67e8062d82c1a005ef4ed119d13a9f18371b1b8737744186" + + "84805fd659bfd69964f83a5cfe08667ddad672cafd16befffa9faed49865214f" + + "703951b443e6dca22edb636f3308380144b9333de4bcb0735710e4d926678634" + + "2fc53babe7bdbe3c01a3addb7f23c63ce2834729fabbd419b47beceb4a460236"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase5() { + byte[] entropy = + hexToBytes("4b222718f56a3260b3c2625a4cf80950b7d6c1250f170bd5c28b118abdf23b2f"); + byte[] nonce = hexToBytes("7aed52d0016fcaef0b6492bc40bbe0e9"); + byte[] expected = hexToBytes( + "a6da029b3665cd39fd50a54c553f99fed3626f4902ffe322dc51f0670dfe8742" + + "ed48415cf04bbad5ed3b23b18b7892d170a7dcf3ef8052d5717cb0c1a8b3010d" + + "9a9ea5de70ae5356249c0e098946030c46d9d3d209864539444374d8fbcae068" + + "e1d6548fa59e6562e6b2d1acbda8da0318c23752ebc9be0c1c1c5b3cf66dd967"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase6() { + byte[] entropy = + hexToBytes("b512633f27fb182a076917e39888ba3ff35d23c3742eb8f3c635a044163768e0"); + byte[] nonce = hexToBytes("e2c39b84629a3de5c301db5643af1c21"); + byte[] expected = hexToBytes( + "fb931d0d0194a97b48d5d4c231fdad5c61aedf1c3a55ac24983ecbf38487b1c9" + + "3396c6b86ff3920cfa8c77e0146de835ea5809676e702dee6a78100da9aa43d8" + + "ec0bf5720befa71f82193205ac2ea403e8d7e0e6270b366dc4200be26afd9f63" + + "b7e79286a35c688c57cbff55ac747d4c28bb80a2b2097b3b62ea439950d75dff"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase7() { + byte[] entropy = + hexToBytes("aae3ffc8605a975befefcea0a7a286642bc3b95fb37bd0eb0585a4cabf8b3d1e"); + byte[] nonce = hexToBytes("9504c3c0c4310c1c0746a036c91d9034"); + byte[] expected = hexToBytes( + "2819bd3b0d216dad59ddd6c354c4518153a2b04374b07c49e64a8e4d055575df" + + "bc9a8fcde68bd257ff1ba5c6000564b46d6dd7ecd9c5d684fd757df62d852115" + + "75d3562d7814008ab5c8bc00e7b5a649eae2318665b55d762de36eba00c2906c" + + "0e0ec8706edb493e51ca5eb4b9f015dc932f262f52a86b11c41e9a6d5b3bd431"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase8() { + byte[] entropy = + hexToBytes("b9475210b79b87180e746df704b3cbc7bf8424750e416a7fbb5ce3ef25a82cc6"); + byte[] nonce = hexToBytes("24baf03599c10df6ef44065d715a93f7"); + byte[] expected = hexToBytes( + "ae12d784f796183c50db5a1a283aa35ed9a2b685dacea97c596ff8c294906d1b" + + "1305ba1f80254eb062b874a8dfffa3378c809ab2869aa51a4e6a489692284a25" + + "038908a347342175c38401193b8afc498077e10522bec5c70882b7f760ea5946" + + "870bd9fc72961eedbe8bff4fd58c7cc1589bb4f369ed0d3bf26c5bbc62e0b2b2"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase9() { + byte[] entropy = + hexToBytes("27838eb44ceccb4e36210703ebf38f659bc39dd3277cd76b7a9bcd6bc964b628"); + byte[] nonce = hexToBytes("39cfe0210db2e7b0eb52a387476e7ea1"); + byte[] expected = hexToBytes( + "e5e72a53605d2aaa67832f97536445ab774dd9bff7f13a0d11fd27bf6593bfb5" + + "2309f2d4f09d147192199ea584503181de87002f4ee085c7dc18bf32ce531564" + + "7a3708e6f404d6588c92b2dda599c131aa350d18c747b33dc8eda15cf40e9526" + + "3d1231e1b4b68f8d829f86054d49cfdb1b8d96ab0465110569c8583a424a099a"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase10() { + byte[] entropy = + hexToBytes("d7129e4f47008ad60c9b5d081ff4ca8eb821a6e4deb91608bf4e2647835373a5"); + byte[] nonce = hexToBytes("a72882773f78c2fc4878295840a53012"); + byte[] expected = hexToBytes( + "0cbf48585c5de9183b7ff76557f8fc9ebcfdfde07e588a8641156f61b7952725" + + "bbee954f87e9b937513b16bba0f2e523d095114658e00f0f3772175acfcb3240" + + "a01de631c19c5a834c94cc58d04a6837f0d2782fa53d2f9f65178ee9c8372224" + + "94c799e64c60406069bd319549b889fa00a0032dd7ba5b1cc9edbf58de82bfcd"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase11() { + byte[] entropy = + hexToBytes("67fe5e300c513371976c80de4b20d4473889c9f1214bce718bc32d1da3ab7532"); + byte[] nonce = hexToBytes("e256d88497738a33923aa003a8d7845c"); + byte[] expected = hexToBytes( + "b44660d64ef7bcebc7a1ab71f8407a02285c7592d755ae6766059e894f694373" + + "ed9c776c0cfc8594413eefb400ed427e158d687e28da3ecc205e0f7370fb0896" + + "76bbb0fa591ec8d916c3d5f18a3eb4a417120705f3e2198154cd60648dbfcfc9" + + "01242e15711cacd501b2c2826abe870ba32da785ed6f1fdc68f203d1ab43a64f"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase12() { + byte[] entropy = + hexToBytes("de8142541255c46d66efc6173b0fe3ffaf5936c897a3ce2e9d5835616aafa2cb"); + byte[] nonce = hexToBytes("d01f9002c407127bc3297a561d89b81d"); + byte[] expected = hexToBytes( + "64d1020929d74716446d8a4e17205d0756b5264867811aa24d0d0da8644db25d" + + "5cde474143c57d12482f6bf0f31d10af9d1da4eb6d701bdd605a8db74fb4e77f" + + "79aaa9e450afda50b18d19fae68f03db1d7b5f1738d2fdce9ad3ee9461b58ee2" + + "42daf7a1d72c45c9213eca34e14810a9fca5208d5c56d8066bab1586f1513de7"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase13() { + byte[] entropy = + hexToBytes("4a8e0bd90bdb12f7748ad5f147b115d7385bb1b06aee7d8b76136a25d779bcb7"); + byte[] nonce = hexToBytes("7f3cce4af8c8ce3c45bdf23c6b181a00"); + byte[] expected = hexToBytes( + "320c7ca4bbeb7af977bc054f604b5086a3f237aa5501658112f3e7a33d2231f5" + + "536d2c85c1dad9d9b0bf7f619c81be4854661626839c8c10ae7fdc0c0b571be3" + + "4b58d66da553676167b00e7d8e49f416aacb2926c6eb2c66ec98bffae20864cf" + + "92496db15e3b09e530b7b9648be8d3916b3c20a3a779bec7d66da63396849aaf"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistCase14() { + byte[] entropy = + hexToBytes("451ed024bc4b95f1025b14ec3616f5e42e80824541dc795a2f07500f92adc665"); + byte[] nonce = hexToBytes("2f28e6ee8de5879db1eccd58c994e5f0"); + byte[] expected = hexToBytes( + "3fb637085ab75f4e95655faae95885166a5fbb423bb03dbf0543be063bcd4879" + + "9c4f05d4e522634d9275fe02e1edd920e26d9accd43709cb0d8f6e50aa54a5f3" + + "bdd618be23cf73ef736ed0ef7524b0d14d5bef8c8aec1cf1ed3e1c38a808b35e" + + "61a44078127c7cb3a8fd7addfa50fcf3ff3bc6d6bc355d5436fe9b71eb44f7fd"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + // ==== Test vectors for HMAC_DRBG with personalization. ==== + @Test + public void testHmacDrbgNistWithPersonalizationCase0() { + byte[] entropy = + hexToBytes("5cacc68165a2e2ee20812f35ec73a79dbf30fd475476ac0c44fc6174cdac2b55"); + byte[] nonce = hexToBytes("6f885496c1e63af620becd9e71ecb824"); + byte[] personalizationString = + hexToBytes("e72dd8590d4ed5295515c35ed6199e9d211b8f069b3058caa6670b96ef1208d0"); + byte[] expected = hexToBytes( + "f1012cf543f94533df27fedfbf58e5b79a3dc517a9c402bdbfc9a0c0f721f9d5" + + "3faf4aafdc4b8f7a1b580fcaa52338d4bd95f58966a243cdcd3f446ed4bc546d" + + "9f607b190dd69954450d16cd0e2d6437067d8b44d19a6af7a7cfa8794e5fbd72" + + "8e8fb2f2e8db5dd4ff1aa275f35886098e80ff844886060da8b1e7137846b23b"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase1() { + byte[] entropy = + hexToBytes("8df013b4d103523073917ddf6a869793059e9943fc8654549e7ab22f7c29f122"); + byte[] nonce = hexToBytes("da2625af2ddd4abcce3cf4fa4659d84e"); + byte[] personalizationString = + hexToBytes("b571e66d7c338bc07b76ad3757bb2f9452bf7e07437ae8581ce7bc7c3ac651a9"); + byte[] expected = hexToBytes( + "b91cba4cc84fa25df8610b81b641402768a2097234932e37d590b1154cbd23f9" + + "7452e310e291c45146147f0da2d81761fe90fba64f94419c0f662b28c1ed94da" + + "487bb7e73eec798fbcf981b791d1be4f177a8907aa3c401643a5b62b87b89d66" + + "b3a60e40d4a8e4e9d82af6d2700e6f535cdb51f75c321729103741030ccc3a56"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase2() { + byte[] entropy = + hexToBytes("565b2b77937ba46536b0f693b3d5e4a8a24563f9ef1f676e8b5b2ef17823832f"); + byte[] nonce = hexToBytes("4ef3064ec29f5b7f9686d75a23d170e3"); + byte[] personalizationString = + hexToBytes("3b722433226c9dba745087270ab3af2c909425ba6d39f5ce46f07256068319d9"); + byte[] expected = hexToBytes( + "d144ee7f8363d128872f82c15663fe658413cd42651098e0a7c51a970de75287" + + "ec943f9061e902280a5a9e183a7817a44222d198fbfab184881431b4adf35d3d" + + "1019da5a90b3696b2349c8fba15a56d0f9d010a88e3f9eeedb67a69bcaa71281" + + "b41afa11af576b765e66858f0eb2e4ec4081609ec81da81df0a0eb06787340ea"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase3() { + byte[] entropy = + hexToBytes("fc3832a91b1dcdcaa944f2d93cbceb85c267c491b7b59d017cde4add79a836b6"); + byte[] nonce = hexToBytes("d5e76ce9eabafed06e33a913e395c5e0"); + byte[] personalizationString = + hexToBytes("ffc5f6eefd51da64a0f67b5f0cf60d7ab43fc7836bca650022a0cee57a43c148"); + byte[] expected = hexToBytes( + "0e713c6cc9a4dbd4249201d12b7bf5c69c3e18eb504bf3252db2f43675e17d99" + + "b6a908400cea304011c2e54166dae1f20260008efe4e06a87e0ce525ca482bca" + + "223a902a14adcf2374a739a5dfeaf14cadd72efa4d55d15154c974d9521535bc" + + "b70658c5b6c944020afb04a87b223b4b8e5d89821704a9985bb010405ba8f3d4"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase4() { + byte[] entropy = + hexToBytes("8009eb2cb49fdf16403bcdfd4a9f952191062acb9cc111eca019f957fb9f4451"); + byte[] nonce = hexToBytes("355598866952394b1eddd85d59f81c9d"); + byte[] personalizationString = + hexToBytes("09ff1d4b97d83b223d002e05f754be480d13ba968e5aac306d71cc9fc49cc2dd"); + byte[] expected = hexToBytes( + "9550903c2f02cf77c8f9c9a37041d0040ee1e3ef65ba1a1fbbcf44fb7a2172bd" + + "6b3aaabe850281c3a1778277bacd09614dfefececac64338ae24a1bf150cbf9d" + + "9541173a82ecba08aa19b75abb779eb10efa4257d5252e8afcac414bc3bb5d30" + + "06b6f36fb9daea4c8c359ef6cdbeff27c1068571dd3c89dc87eda9190086888d"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase5() { + byte[] entropy = + hexToBytes("a6e4c9a8bd6da23b9c2b10a7748fd08c4f782fadbac7ea501c17efdc6f6087bd"); + byte[] nonce = hexToBytes("acdc47edf1d3b21d0aec7631abb6d7d5"); + byte[] personalizationString = + hexToBytes("c16ee0908a5886dccf332fbc61de9ec7b7972d2c4c83c477409ce8a15c623294"); + byte[] expected = hexToBytes( + "a52f93ccb363e2bdf0903622c3caedb7cffd04b726052b8d455744c71b76dee1" + + "b71db9880dc3c21850489cb29e412d7d80849cfa9151a151dcbf32a32b4a54ca" + + "c01d3200200ed66a3a5e5c131a49655ffbf1a8824ff7f265690dffb4054df46a" + + "707b9213924c631c5bce379944c856c4f7846e281ac89c64fad3a49909dfb92b"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase6() { + byte[] entropy = + hexToBytes("59d6307460a9bdd392dfc0904973991d585696010a71e52d590a5039b4849fa4"); + byte[] nonce = hexToBytes("34a0aafb95917cbf8c38fc5548373c05"); + byte[] personalizationString = + hexToBytes("0407b7c57bc11361747c3d67526c36e228028a5d0b145d66ab9a2fe4b07507a0"); + byte[] expected = hexToBytes( + "299aba0661315211b09d2861855d0b4b125ab24649461341af6abd903ed6f025" + + "223b3299f2126fcad44c675166d800619cf49540946b12138989417904324b0d" + + "dad121327211a297f11259c9c34ce4c70c322a653675f78d385e4e2443f8058d" + + "141195e17e0bd1b9d44bf3e48c376e6eb44ef020b11cf03eb141c46ecb43cf3d"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase7() { + byte[] entropy = + hexToBytes("9ae3506aadbc8358696ba1ba17e876e1157b7048235921503d36d9211b430342"); + byte[] nonce = hexToBytes("9abf7d66afee5d2b811cba358bbc527d"); + byte[] personalizationString = + hexToBytes("0d645f6238e9ceb038e4af9772426ca110c5be052f8673b8b5a65c4e53d2f519"); + byte[] expected = hexToBytes( + "5f032c7fec6320fe423b6f38085cbad59d826085afe915247b3d546c4c6b1745" + + "54dd4877c0d671de9554b505393a44e71f209b70f991ac8aa6e08f983fff2a4c" + + "817b0cd26c12b2c929378506489a75b2025b358cb5d0400821e7e252ac6376cd" + + "94a40c911a7ed8b6087e3de5fa39fa6b314c3ba1c593b864ce4ff281a97c325b"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase8() { + byte[] entropy = + hexToBytes("96ae3b8775b36da2a29b889ad878941f43c7d51295d47440cd0e3c4999193109"); + byte[] nonce = hexToBytes("1fe022a6fc0237b055d4d6a7036b18d5"); + byte[] personalizationString = + hexToBytes("1e40e97362d0a823d3964c26b81ab53825c56446c5261689011886f19b08e5c2"); + byte[] expected = hexToBytes( + "e707cd14b06ce1e6dbcceaedbf08d88891b03f44ad6a797bd12fdeb557d0151d" + + "f9346a028dec004844ca46adec3051dafb345895fa9f4604d8a13c8ff66ae093" + + "fa63c4d9c0816d55a0066d31e8404c841e87b6b2c7b5ae9d7afb6840c2f7b441" + + "bf2d3d8bd3f40349c1c014347c1979213c76103e0bece26ad7720601eff42275"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase9() { + byte[] entropy = + hexToBytes("33f5120396336e51ee3b0b619b5f873db05ca57cda86aeae2964f51480d14992"); + byte[] nonce = hexToBytes("6f1f6e9807ba5393edcf3cb4e4bb6113"); + byte[] personalizationString = + hexToBytes("3709605af44d90196867c927512aa8ba31837063337b4879408d91a05c8efa9f"); + byte[] expected = hexToBytes( + "8b8291126ded9acef12516025c99ccce225d844308b584b872c903c7bc646759" + + "9a1cead003dc4c70f6d519f5b51ce0da57f53da90dbe8f666a1a1dde297727fe" + + "e2d44cebd1301fc1ca75956a3fcae0d374e0df6009b668fd21638d2b733e6902" + + "d22d5bfb4af1b455975e08eef0ebe4dc87705801e7776583c8de11672729f723"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase10() { + byte[] entropy = + hexToBytes("ad300b799005f290fee7f930eebce158b98fb6cb449987fe433f955456b35300"); + byte[] nonce = hexToBytes("06aa2514e4bd114edf7ac105cfef2772"); + byte[] personalizationString = + hexToBytes("87ada711465e4169da2a74c931afb9b5a5b190d07b7af342aa99570401c3ee8a"); + byte[] expected = hexToBytes( + "80d7c606ff49415a3a92ba1f2943235c01339c8f9cd0b0511fbfdf3ef23c42ff" + + "ff008524193faaa4b7f2f2eb0cfa221d9df89bd373fe4e158ec06fad3ecf1eb4" + + "8b8239b0bb826ee69d773883a3e8edac66254610ff70b6609836860e39ea1f3b" + + "fa04596fee1f2baca6cebb244774c6c3eb4af1f02899eba8f4188f91776de16f"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase11() { + byte[] entropy = + hexToBytes("130b044e2c15ab89375e54b72e7baae6d4cad734b013a090f4df057e634f6ff0"); + byte[] nonce = hexToBytes("65fd6ac602cd44107d705dbc066e52b6"); + byte[] personalizationString = + hexToBytes("f374aba16f34d54aae5e494505b67d3818ef1c08ea24967a76876d4361379aec"); + byte[] expected = hexToBytes( + "5d179534fb0dba3526993ed8e27ec9f915183d967336bb24352c67f4ab5d7935" + + "d3168e57008da851515efbaecb69904b6d899d3bfa6e9805659aef2942c49038" + + "75b8fcbc0d1d24d1c075f0ff667c1fc240d8b410dff582fa71fa30878955ce2e" + + "d786ef32ef852706e62439b69921f26e84e0f54f62b938f04905f05fcd7c2204"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase12() { + byte[] entropy = + hexToBytes("716430e999964b35459c17921fe5f60e09bd9ab234cb8f4ba4932bec4a60a1d5"); + byte[] nonce = hexToBytes("9533b711e061b07d505da707cafbca03"); + byte[] personalizationString = + hexToBytes("372ae616d1a1fc45c5aecad0939c49b9e01c93bfb40c835eebd837af747f079d"); + byte[] expected = hexToBytes( + "a80d6a1b2d0ce01fe0d26e70fb73da20d45841cf01bfbd50b90d2751a46114c0" + + "e758cb787d281a0a9cf62f5c8ce2ee7ca74fefff330efe74926acca6d6f0646e" + + "4e3c1a1e52fce1d57b88beda4a5815896f25f38a652cc240deb582921c8b1d03" + + "a1da966dd04c2e7eee274df2cd1837096b9f7a0d89a82434076bc30173229a60"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase13() { + byte[] entropy = + hexToBytes("7679f154296e6d580854826539003a82d1c54e2e062c619d00da6c6ac820789b"); + byte[] nonce = hexToBytes("55d12941b0896462e7d888e5322a99a3"); + byte[] personalizationString = + hexToBytes("ba4d1ed696f58ef64596c76cee87cc1ca83069a79e7982b9a06f9d62f4209faf"); + byte[] expected = hexToBytes( + "10dc7cd2bb68c2c28f76d1b04ae2aa287071e04c3b688e1986b05cc1209f691d" + + "aa55868ebb05b633c75a40a32b49663185fe5bb8f906008347ef51590530948b" + + "87613920014802e5864e0758f012e1eae31f0c4c031ef823aecfb2f8a73aaa94" + + "6fc507037f9050b277bdeaa023123f9d22da1606e82cb7e56de34bf009eccb46"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgNistWithPersonalizationCase14() { + byte[] entropy = + hexToBytes("8ca4a964e1ff68753db86753d09222e09b888b500be46f2a3830afa9172a1d6d"); + byte[] nonce = hexToBytes("a59394e0af764e2f21cf751f623ffa6c"); + byte[] personalizationString = + hexToBytes("eb8164b3bf6c1750a8de8528af16cffdf400856d82260acd5958894a98afeed5"); + byte[] expected = hexToBytes( + "fc5701b508f0264f4fdb88414768e1afb0a5b445400dcfdeddd0eba67b4fea8c" + + "056d79a69fd050759fb3d626b29adb8438326fd583f1ba0475ce7707bd294ab0" + + "1743d077605866425b1cbd0f6c7bba972b30fbe9fce0a719b044fcc139435489" + + "5a9f8304a2b5101909808ddfdf66df6237142b6566588e4e1e8949b90c27fc1f"); + + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), personalizationString); + byte[] out1 = new byte[1024 / 8]; + drbg.nextBytes(out1); + byte[] out2 = new byte[1024 / 8]; + drbg.nextBytes(out2); + assertArrayEquals(expected, out2); + } + + @Test + public void testHmacDrbgGenerateEntropyInput() { + byte[] got = HmacDrbg.generateEntropyInput(); + assertEquals(got.length, HmacDrbg.ENTROPY_INPUT_SIZE_BYTES); + } + + @Test + public void testHmacDrbgZeroLengthOutput() { + byte[] entropy = + hexToBytes("8ca4a964e1ff68753db86753d09222e09b888b500be46f2a3830afa9172a1d6d"); + byte[] nonce = hexToBytes("a59394e0af764e2f21cf751f623ffa6c"); + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out = drbg.nextBytes(0); + assertArrayEquals(new byte[0], out); + } + + @Test + public void testHmacDrbgCanGenerateMaxBytesOutput() { + byte[] entropy = + hexToBytes("8ca4a964e1ff68753db86753d09222e09b888b500be46f2a3830afa9172a1d6d"); + byte[] nonce = hexToBytes("a59394e0af764e2f21cf751f623ffa6c"); + HmacDrbg drbg = new HmacDrbg(Bytes.concat(entropy, nonce), null); + byte[] out = drbg.nextBytes(HmacDrbg.MAX_BYTES_TOTAL); + assertEquals(HmacDrbg.MAX_BYTES_TOTAL, out.length); + } +} |