Internet-Draft Balloon Key Derivation Function (BKDF) December 2024
Lucas Expires 17 June 2025 [Page]
Workgroup:
Network Working Group
Internet-Draft:
draft-lucas-bkdf-latest
Published:
Intended Status:
Informational
Expires:
Author:
S. Lucas
Individual Contributor

Balloon Key Derivation Function (BKDF)

Abstract

This document specifies the Balloon key derivation function (BKDF) for password hashing and password-based key derivation. It is memory-hard, resistant to cache-timing attacks, easy to implement, and can be instantiated using any collision-resistant pseudorandom function (PRF), hash function, or extendable-output function (XOF).

About This Document

This note is to be removed before publishing as an RFC.

The latest revision of this draft can be found at https://samuel-lucas6.github.io/draft-lucas-bkdf/draft-lucas-bkdf.html. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-lucas-bkdf/.

Source for this draft and an issue tracker can be found at https://github.com/samuel-lucas6/draft-lucas-bkdf.

Status of This Memo

This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.

Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.

Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."

This Internet-Draft will expire on 17 June 2025.

Table of Contents

1. Introduction

BKDF is a memory-hard password hashing and password-based key derivation function based on Balloon [BCS16], an algorithm published shortly after the Password Hashing Competition (PHC). Here is how it compares to prior password hashing algorithms that are widely used in practice:

Table 1
  PBKDF2 bcrypt scrypt Argon2 BKDF
Memory-hard No No Yes Yes Yes
Cache-hard No Yes No No No
Parallelism No No Yes Yes Yes
Resists cache-timing N/A No No Depends Yes
Single primitive Yes Yes No No Yes
Mode of operation Yes No No No Yes
Intuitive design No No No No Yes
No variants Yes Yes Yes No Yes
Key derivation Yes No Yes Yes Yes
Separate memory/time No No No Yes Yes
Pepper No No No Yes Yes
Associated data No No No Yes Yes
Personalization No No No No Yes

In sum, it shares many features with Argon2 [RFC9106], the PHC winner, whilst being more of a spiritual successor to PBKDF2 [RFC8018]. Namely, the design is simple and supports any collision-resistant PRF, hash function, or XOF, allowing easy implementation with existing APIs and NIST approved functions to be used.

1.1. Comparison to Balloon

BKDF exists because the Balloon paper does not fully specify the algorithm, the performance is suboptimal, there are anonymity/security issues with the design, there are multiple variants, and functionality like support for key derivation is missing. These factors have limited its adoption and led to incompatible implementations.

This document rectifies these issues and more by improving the performance, specifying an encoding, preventing canonicalization attacks, improving domain separation, not computing the memory access pattern from the salt, fixing the modulo bias, making delta a constant, treating Balloon and Balloon-M as one algorithm, and adding support for key derivation, a pepper, associated data, a personalization string, and keyed hashing like HMAC [RFC2104].

Importantly, the approach to memory hardness remains the same, meaning such analyses of Balloon should transfer to BKDF. Furthermore, the security of collision-resistant hash functions that will be used in BKDF is well understood. Therefore, assuming these functions are being used correctly, the design is secure.

Note that this document is not an IETF product and is not a standard.

2. Conventions and Definitions

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “NOT RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.

Throughout this document, “byte” refers to the same unit as “octet”, namely an 8-bit sequence.

Operations:

Constants:

3. The BKDF Algorithm

BKDF(password, salt, personalization, spaceCost, timeCost, parallelism, length, pepper, associatedData)

BKDF calls an internal function that provides memory hardness in a way that supports parallelism and then produces a variable-length output. It can be divided into three steps:

  1. KDF Extract: a key is derived from the password, salt, personalization, pepper, and associated data.

  2. Parallelism: the internal function, which uses the derived key as well as other user-provided parameters, is called in parallel, and the outputs are XORed together.

  3. KDF Expand: a KDF in feedback mode is used to derive key material for the user from the derived key, context information, and the XORed outputs.

Note that the internal function calls SHOULD be implemented in parallel, not serially. Otherwise, the parallelism parameter essentially becomes another timeCost parameter. Therefore, if multithreading is not possible, it is RECOMMENDED to only support a parallelism of 1.

Inputs:

Outputs:

Steps:

outputs = BlockArray(parallelism, HASH_LEN)

if pepper.Length == 0
    key = ByteArray(0)
else
    key = pepper
key = ZeroPad(key, KEY_LEN)

key = PRF(key, password || salt || personalization || associatedData || LE32(pepper.Length) || LE32(password.Length) || LE32(salt.Length) || LE32(personalization.Length) || LE32(associatedData.Length))
key = ZeroPad(key, KEY_LEN)

parallel for i = 0 to parallelism - 1
    outputs[i] = BalloonCore(key, personalization, spaceCost, timeCost, parallelism, i + 1)

hash = ByteArray(HASH_LEN)
foreach output in outputs
    for i = 0 to output.Length - 1
        hash[i] = hash[i] ^ output[i]

counter = 1
reps = Ceiling(length / HASH_LEN)
previous = hash
result = ByteArray(0)
for i = 0 to reps - 1
    previous = PRF(key, previous || UTF8("bkdf") || LE32(counter++))
    result = result || previous

return result.Slice(0, length)

4. The BalloonCore Function

BalloonCore(key, personalization, spaceCost, timeCost, parallelism, iteration)

The BalloonCore function is the internal function used by BKDF for memory hardness. It can be divided into four steps:

  1. Precompute: the user-provided parameters and domain separation are used to precompute pseudorandom bytes for the password-independent memory accesses performed in step 3.

  2. Expand: a large buffer is filled with pseudorandom bytes derived by hashing user-provided parameters and domain separation before repeatedly hashing the previous output.

  3. Mix: the buffer is mixed for the number of rounds specified by the user. Each hash-sized block becomes equal to the hash of the previous block, the current block, and delta other blocks pseudorandomly chosen from the buffer.

  4. Extract: the last block of the buffer is output for key derivation.

Inputs:

Outputs:

Steps:

spaceCost = 2 ** spaceCost
buffer = BlockArray(spaceCost, HASH_LEN)

counter = 0
emptyKey = ZeroPad(ByteArray(0), KEY_LEN)
pseudorandom = ByteArray(0)
reps = (spaceCost * timeCost * 3) / (HASH_LEN / 4)
for i = 0 to reps - 1
    pseudorandom = pseudorandom || PRF(emptyKey, LE32(VERSION) || personalization || LE32(spaceCost) || LE32(timeCost) || LE32(parallelism) || LE32(iteration) || LE64(counter++))

buffer[0] = PRF(key, LE32(VERSION) || LE32(spaceCost) || LE32(timeCost) || LE32(parallelism) || LE32(iteration) || LE64(counter++))
for m = 1 to spaceCost - 1
    buffer[m] = PRF(key, buffer[m - 1] || LE64(counter++))

offset = 0
previous = buffer[spaceCost - 1]
for t = 0 to timeCost - 1
    for m = 0 to spaceCost - 1
        other1 = ReadLE32(pseudorandom.Slice(offset, 4)) % spaceCost
        other2 = ReadLE32(pseudorandom.Slice(offset + 4, 4)) % spaceCost
        other3 = ReadLE32(pseudorandom.Slice(offset + 8, 4)) % spaceCost
        buffer[m] = PRF(key, previous || buffer[m] || buffer[other1] || buffer[other2] || buffer[other3] || LE64(counter++))
        previous = buffer[m]
        offset = offset + 12

return previous

5. Implementation Considerations

There are several ways to optimise the pseudocode, which is written for readability:

6. Choosing the Hash Function

The choice of collision-resistant hash function affects the performance and security of BKDF in three ways:

  1. For the same parameters, the attacker has an advantage if the algorithm is faster in hardware versus software. They will be able to do the computation in less time than the defender.

  2. For the same delay, the defender will be forced to use smaller parameters with a slower collision-resistant hash function in software. Using a faster algorithm in software means stronger parameters can be used.

  3. A smaller output length worsens the performance and memory-hardness.

It is RECOMMENDED to use a collision-resistant hash function with a larger output length that is fast in software but relatively slow in hardware, such as BLAKE2b-512 [RFC7693]. As another example, SHA-512 is preferable to SHA-256 [RFC6234].

SHA-3 [FIPS202] is NOT RECOMMENDED as it is slower in software compared to in hardware. HMAC [RFC2104] is also NOT RECOMMENDED because it increases the number of calls to the chosen hash function.

7. Choosing the Cost Parameters

The higher the spaceCost and timeCost, the longer it takes to compute an output. If these values are too small, security is unnecessarily reduced. If they are too large, there is a risk of user frustration and denial-of-service for different types of user devices and servers. To make matters even more complicated, these parameters may need to be increased over time as hardware gets faster/smaller.

The purpose of parallelism is to enable greater memory hardness without increasing the delay. This is because the mixing is done by multiple CPU cores simultaneously rather than serially. However, a poor choice of parallelism can also cause denial-of-service or give the attacker an advantage.

The following procedure can be used to choose parameters:

  1. For performing authentication on a server or running the algorithm on any type of user device, set the parallelism to 1. This avoids resource exhaustion attacks and slowdowns on machines with few CPU cores. Otherwise, set it to the maximum number of CPU cores the machine can dedicate to the computation (e.g. 4 cores).

  2. Establish the maximum acceptable delay for the user. For example, 100-500 ms for authentication, 250-1000 ms for file encryption, and 1000-5000 ms for disk encryption. On servers, you also need to factor in the maximum number of authentication attempts per second.

  3. Determine the maximum amount of memory available, taking into account different types of user devices and denial-of-service. For instance, mobile phones versus laptops/desktops.

  4. Convert the closest MiB/GiB memory size that is a power of 2 to bytes. Then set spaceCost to log2(bytes / HASH_LEN), which converts the number of BKDF blocks to an integer between MIN_SPACECOST and MAX_SPACECOST.

  5. Find the timeCost that brings you closest to the maximum acceptable delay or target number of authentication attempts per second by running benchmarks.

  6. If timeCost is only 1, reduce spaceCost to be able to increase timeCost. Performing multiple rounds is beneficial for security [AB17].

To cause an attacker to get < 10 kH/s on an RTX 4080 SUPER GPU, the parameters must be at least one of the following when using SHA-256, SHA-512, HMAC-SHA256, or HMAC-SHA512 as the collision-resistant PRF:

Note that these are example minimum parameters at the time of writing. They will not be appropriate minimums in the future, and you SHOULD use stronger parameters if you can afford to.

See Section 9 for guidance on the other parameters.

8. Encoding Password Hashes

To store BKDF hashes in a database as strings, the following format SHOULD be used:

$bkdf-hash$v=version$m=spaceCost,t=timeCost,p=parallelism$salt$hash

Here is an example encoded hash:

$bkdf-sha256$v=1$m=32,t=3,p=1$ZXhhbXBsZXNhbHQ$cWBD3/d3tEqnuI3LqxLAeKvs+snSicW1GVlnqmNEDfs

9. Security Considerations

9.1. Usage Guidelines

Technically, only preimage resistance is required for password hashing to prevent the attacker learning information about the password from the hash. However, hash functions that are not collision resistant (e.g. MD5 [RFC6151] and SHA-1 [RFC6194]) MUST NOT be used. Such functions are cryptographically weak and unsuitable for new protocols.

It is RECOMMENDED to either restrict password characters to ASCII [RFC20] or to apply Unicode Normalization Form C (NFC) [RFC8265] to the password prior to UTF-8 encoding [RFC3629]. The former eliminates and the latter reduces the risk of password characters being encoded differently depending on the keyboard, operating system, and so on. Without taking these steps, a user might not always be able to log in, decrypt a file/disk, etc.

If possible, store the password in protected memory and/or erase the password from memory once it is no longer required. Otherwise, an attacker may be able to recover the password from memory or the disk.

In all cases, it is RECOMMENDED to use a 128-bit salt. However, a 64-bit salt MAY be used if there are storage constraints. Regardless, the salt length SHOULD NOT vary in your protocol/application.

The salt MUST be unique each time you call the function unless verifying a password hash or deriving the same key. It SHOULD be randomly generated using a cryptographically secure pseudorandom number generator (CSPRNG). However, it MAY be deterministic and predictable if random generation is not possible.

The salt SHOULD be considered a non-secret value. It SHOULD be stored alongside the password hash for password hashing (see Section 8) or in something like a file header for key derivation. If you have a secret key, the password hash SHOULD be encrypted or the pepper parameter SHOULD be used, as described below.

The personalization parameter MUST be a hardcoded, globally unique, and application-specific string for your entire database/application. A good default format is UTF8("[application] [commit timestamp] [purpose]"). For instance, "example.com 2024-11-03 14:36:48 password hashing". This binds the buffer, memory access pattern, and output to your application even if multiple applications have the same name, which helps hinder attackers. It MUST NOT be unique per user in the database/application; that is the purpose of the salt.

The spaceCost, timeCost, and parallelism parameters MUST be carefully chosen to avoid denial-of-service and user frustration whilst ensuring adequate protection against password cracking. See Section 7 for more information about choosing these parameters.

Avoid using hardcoded spaceCost/timeCost/parallelism parameters when performing password hashing; these SHOULD be stored as part of the password hash, as described in Section 8. With key derivation, hardcoded parameters are acceptable if protocol versioning is used.

For password hashing, it is RECOMMENDED to use a length of 128 or 256 bits. For key derivation, it is RECOMMENDED to use a length of at least 128 bits.

If you want to derive multiple keys (e.g. for encryption and authentication), you MUST only run the algorithm once and use different portions of the output as separate keys. Otherwise, the attacker may have an advantage, like only needing to run the algorithm once instead of twice to check a password, and you will be forced to use weaker parameters.

For password hashing, it is RECOMMENDED to encrypt password hashes using an unauthenticated encryption algorithm or an authenticated encryption with associated data (AEAD) scheme [RFC5116] before storage. This forces an attacker to compromise the key, which is stored separately from the database, as well as the database before they can begin password cracking. If the key is compromised but the database is not, it can be rotated without having to reset any passwords. It is RECOMMENDED to use a 256-bit key.

For key derivation, one can feed a secret key into the pepper parameter for additional security. This forces an attacker to compromise the pepper before they can guess the password. It is RECOMMENDED to use a 256-bit pepper.

To bind context information to the output, like a user ID and server ID for password-authenticated key exchange (PAKE) algorithms, the associatedData parameter can be used. However, in most cases, this parameter is not required.

Importantly, if multiple values are concatenated to form the associatedData parameter, they MUST be unambiguously encoded. For example, by using fixed-length values or by prepending/appending the little-endian encoded length of each value.

9.2. Security Guarantees

The security properties of BKDF depend on the chosen collision-resistant hash function. For example, a 256-bit hash typically provides 128-bit collision resistance and 128- or 256-bit (second) preimage resistance.

Balloon has been proven sequentially memory-hard in the random-oracle model, making it resistant against sequential GPU/ASIC attacks [BCS16]. An adversary trying to save space pays a large penalty in computation time.

Balloon also uses a password-independent memory access pattern to prevent side-channel attacks leaking information about the password [BCS16]. This property is especially relevant in cloud computing environments where multiple users can share the same physical machine. However, no function that uses a password-independent memory access pattern can be optimally memory-hard in the parallel setting.

BKDF is not vulnerable to garbage-collector attacks since the internal state is overwritten [FLLW15]. However, it can be vulnerable to weak garbage-collector attacks because the key derived from the password is kept in memory throughout the algorithm. Even if you cache the hash function state after processing the key and zero the key, this attack is still possible. The only prevention is to use a pepper and zero that from memory immediately after processing it. With that said, the password is likely to remain in memory anyway, rendering this attack unnecessary.

The approach to parallelism is subject to a tradeoff, namely an adversary can do sequential calls to the BalloonCore function to avoid increasing the memory usage, keeping the time-area product constant. This is deemed acceptable because parallelism is often not used in practice and avoiding this would complicate the design.

Unlike password hashing algorithms such as bcrypt [PM99], which perform many small and fast pseudorandom reads, BKDF is not cache-hard. Whilst there are no known publications on cache-hardness at the time of writing, it is reported to provide better GPU resistance than memory-hardness for shorter delays (e.g. < 1000 ms). This is because such algorithms force GPUs to use less memory bandwidth because of their large bus width (typically 256 to 1024 bits). Assuming GPUs are primarily used for password cracking, this makes cache-hard algorithms ideal for authentication scenarios especially.

Third-party analysis of Balloon can be found in [RD16], [AB17], [ABP17], and [RD17]. However, note that there are multiple versions of Balloon, and none of these papers have analysed BKDF.

10. IANA Considerations

This document has no IANA actions.

11. References

11.1. Normative References

[BLAKE3]
O'Connor, J., Aumasson, J.-P., Neves, S., and Z. Wilcox-O’Hearn, "BLAKE3: one function, fast everywhere", , <https://github.com/BLAKE3-team/BLAKE3-specs/blob/master/blake3.pdf>.
[FIPS202]
National Institute of Standards and Technology, "FIPS PUB 202 - SHA-3 Standard: Permutation-Based Hash and Extendable-Output Functions", , <https://doi.org/10.6028/NIST.FIPS.202>.
[RFC2104]
Krawczyk, H., Bellare, M., and R. Canetti, "HMAC: Keyed-Hashing for Message Authentication", RFC 2104, DOI 10.17487/RFC2104, , <https://www.rfc-editor.org/rfc/rfc2104>.
[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/rfc/rfc2119>.
[RFC3629]
Yergeau, F., "UTF-8, a transformation format of ISO 10646", STD 63, RFC 3629, DOI 10.17487/RFC3629, , <https://www.rfc-editor.org/rfc/rfc3629>.
[RFC4648]
Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, DOI 10.17487/RFC4648, , <https://www.rfc-editor.org/rfc/rfc4648>.
[RFC6234]
Eastlake 3rd, D. and T. Hansen, "US Secure Hash Algorithms (SHA and SHA-based HMAC and HKDF)", RFC 6234, DOI 10.17487/RFC6234, , <https://www.rfc-editor.org/rfc/rfc6234>.
[RFC7693]
Saarinen, M., Ed. and J. Aumasson, "The BLAKE2 Cryptographic Hash and Message Authentication Code (MAC)", RFC 7693, DOI 10.17487/RFC7693, , <https://www.rfc-editor.org/rfc/rfc7693>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/rfc/rfc8174>.

11.2. Informative References

[AB16]
Alwen, J. and J. Blocki, "Efficiently Computing Data-Independent Memory-Hard Functions", Advances in Cryptology – CRYPTO 2016. CRYPTO 2016. Lecture Notes in Computer Science(), vol 9815, pp. 241–271, , <https://doi.org/10.1007/978-3-662-53008-5_9>.
[AB17]
Alwen, J. and J. Blocki, "Towards Practical Attacks on Argon2i and Balloon Hashing", 2017 IEEE European Symposium on Security and Privacy (EuroS&P), Paris, France, 2017, pp. 142-157, , <https://doi.org/10.1109/EuroSP.2017.47>.
[ABP17]
Alwen, J., Blocki, J., and K. Pietrzak, "Depth-Robust Graphs and Their Cumulative Memory Complexity", Advances in Cryptology – EUROCRYPT 2017. EUROCRYPT 2017. Lecture Notes in Computer Science(), vol 10212, pp. 3–32, , <https://doi.org/10.1007/978-3-319-56617-7_1>.
[BCS16]
Boneh, D., Corrigan-Gibbs, H., and S. Schechter, "Balloon Hashing: A Memory-Hard Function Providing Provable Protection Against Sequential Attacks", Cryptology ePrint Archive, Paper 2016/027, , <https://eprint.iacr.org/2016/027>.
[FLLW15]
Forler, C., List, E., Lucks, S., and J. Wenzel, "Overview of the Candidates for the Password Hashing Competition", Technology and Practice of Passwords. PASSWORDS 2014. Lecture Notes in Computer Science(), vol 9393, pp. 3–18, , <https://doi.org/10.1007/978-3-319-24192-0_1>.
[PM99]
Provos, N. and D. Mazières, "A Future-Adaptable Password Scheme", Proceedings of the 1999 USENIX Annual Technical Conference, , <https://www.usenix.org/legacy/publications/library/proceedings/usenix99/provos/provos.pdf>.
[RD16]
Ren, L. and S. Devadas, "Proof of Space from Stacked Expanders", Theory of Cryptography. TCC 2016. Lecture Notes in Computer Science(), vol 9985, pp. 262–285, , <https://doi.org/10.1007/978-3-662-53641-4_11>.
[RD17]
Ren, L. and S. Devadas, "Bandwidth Hard Functions for ASIC Resistance", Theory of Cryptography. TCC 2017. Lecture Notes in Computer Science(), vol 10677, pp. 466–492, , <https://doi.org/10.1007/978-3-319-70500-2_16>.
[RFC20]
Cerf, V., "ASCII format for network interchange", STD 80, RFC 20, DOI 10.17487/RFC0020, , <https://www.rfc-editor.org/rfc/rfc20>.
[RFC5116]
McGrew, D., "An Interface and Algorithms for Authenticated Encryption", RFC 5116, DOI 10.17487/RFC5116, , <https://www.rfc-editor.org/rfc/rfc5116>.
[RFC6151]
Turner, S. and L. Chen, "Updated Security Considerations for the MD5 Message-Digest and the HMAC-MD5 Algorithms", RFC 6151, DOI 10.17487/RFC6151, , <https://www.rfc-editor.org/rfc/rfc6151>.
[RFC6194]
Polk, T., Chen, L., Turner, S., and P. Hoffman, "Security Considerations for the SHA-0 and SHA-1 Message-Digest Algorithms", RFC 6194, DOI 10.17487/RFC6194, , <https://www.rfc-editor.org/rfc/rfc6194>.
[RFC8018]
Moriarty, K., Ed., Kaliski, B., and A. Rusch, "PKCS #5: Password-Based Cryptography Specification Version 2.1", RFC 8018, DOI 10.17487/RFC8018, , <https://www.rfc-editor.org/rfc/rfc8018>.
[RFC8265]
Saint-Andre, P. and A. Melnikov, "Preparation, Enforcement, and Comparison of Internationalized Strings Representing Usernames and Passwords", RFC 8265, DOI 10.17487/RFC8265, , <https://www.rfc-editor.org/rfc/rfc8265>.
[RFC9106]
Biryukov, A., Dinu, D., Khovratovich, D., and S. Josefsson, "Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications", RFC 9106, DOI 10.17487/RFC9106, , <https://www.rfc-editor.org/rfc/rfc9106>.

Appendix A. Test Vectors

A.1. BKDF-SHA256

NOTE: These test vectors are out of date and will be updated when the new design has been finalised.

A.1.1. Test Vector 1

password: 70617373776f7264

salt: 73616c74

spaceCost: 1

timeCost: 1

parallelism: 1

hash: 97a11df9382a788c781929831d409d3599e0b67ab452ef834718114efdcd1c6d

A.1.2. Test Vector 2

password: 70617373776f7264

salt: 73616c74

spaceCost: 1

timeCost: 1

parallelism: 16

hash: a67b383bb88a282aef595d98697f90820adf64582a4b3627c76b7da3d8bae915

A.1.3. Test Vector 3

password: 68756e7465723432

salt: 6578616d706c6573616c74

spaceCost: 1024

timeCost: 3

parallelism: 4

hash: 1832bd8e5cbeba1cb174a13838095e7e66508e9bf04c40178990adbc8ba9eb6f

A.1.4. Test Vector 4

password:

salt: 73616c74

spaceCost: 3

timeCost: 3

parallelism: 2

hash: f8767fe04059cef67b4427cda99bf8bcdd983959dbd399a5e63ea04523716c23

Acknowledgments

The original version of Balloon was designed by Dan Boneh, Henry Corrigan-Gibbs, and Stuart Schechter.

We would like to thank the following individuals for their contributions:

Author's Address

Samuel Lucas
Individual Contributor