The Evolution of the OprfApi: A Story of Type Safety and Flexibility
Introduction
In the world of cryptography, type safety and flexibility are often at odds. A rigid type system can provide a strong contract, but it can also make the system difficult to extend or adapt. On the other hand, a flexible system can be easier to work with, but it can also introduce vulnerabilities or bugs. Let's explore how the OprfApi transitioned from a more rigid structure to a more flexible and type-safe design, improving both developer experience and security.
The Original API: Explicit but Cumbersome
The original was designed to be explicit, requiring the developers to manually manage various aspects like the cryptographic suite and the mode of operation. Here's how it looked:
// Original API example
export async function oprfExample() {
const suite = Oprf.Suite.P521_SHA512
const privateKey = await randomPrivateKey(suite)
const server = new OPRFServer(suite, privateKey)
const client = new OPRFClient(suite)
// ... rest of the code
}
While this design made it clear what the developer was working with, it also made the API somewhat cumbersome. For example, each new client and server instance had to be explicitly created with the correct suite.
The Proposed API: A Step Toward Abstraction
The proposed API aimed to reduce this rigidity by abstracting away some of the details:
However, this proposed approach ran into a problem: creating a generic makeClient interface became challenging due to the need to make the public key optional, which was not ideal for type safety.
The Final API: Type-Safe and Intuitive
The final version of the API solves these issues by leveraging TypeScript's conditional types and mapped types, making the API more flexible and type-safe at the same time.
The use of conditional types allows the Client interface to have different method signatures based on the mode of operation (M). This means that the finalize method can have different argument types and return types based on whether the mode is poprf or something else, without compromising type safety.
We did experiment with simply using advanced features to automatically append arguments, but then the IDE experience was not as nice, showing only generic argument names.
Why the CryptoProvider?
In the final API, the CryptoProvider is part of the OprfApi. This is an important design choice because it allows developers to inject their own cryptographic libraries or functionalities, making the API adaptable to different environments or constraints.
Initially, only one configuration could be allowed at a time, with runtime failure when an "old" OprfApi is used, but with some refactoring it will be possible to support more than one at a time.
No More Passing Around suiteID and modeID
One of the most significant improvements is that developers no longer need to pass around suiteID and modeID explicitly. Instead, they can simply create a Mode object:
makeMode<M extends ModeID, S extends SuiteID>(params: { mode: M; suite: S }): Mode<M, S>
Once this object is created, it knows how to operate on keys and create client and server instances, encapsulating the suite and mode information within itself.
By leveraging TypeScript's advanced type features, the API still provides a robust contract for developers while also being easier and more intuitive to use.
The Intermediary Pattern: A Flexible Yet Constrained Crypto Provider
At the heart of the cryptographic operations within the library is the CryptoProviderIntermediary class. This class serves as a flexible conduit that conforms to the CryptoProvider interface. It encapsulates an actual CryptoProvider instance, allowing the underlying provider to be switched at runtime.
// Example usage
import { CryptoImpl } from '@cloudflare/voprf-ts';
import { YourCryptoProvider } from './your-crypto-provider-file.js';
// Override the default crypto provider
CryptoImpl.provider = YourCryptoProvider;
// CryptoImpl now delegates to YourCryptoProvider
However, this flexibility comes with a caveat. All instances of OprfApi share this singleton CryptoProvider via CryptoImpl.provider, constraining them to use the same cryptographic provider at any given time.
By understanding this intermediary design pattern, developers can easily switch between cryptographic providers for all instances of OprfApi. Yet they should also be cautious, as changing the provider in one place will affect it globally across all OprfApi instances, due to the current reliance on the singleton pattern.
Implementation Details: Safeguarding Against Inconsistent Crypto Providers
The Challenge of Supporting Multiple Crypto Providers
The eventual goal of the OprfApi is to support multiple instances, each configured with a different CryptoProvider. However, the current implementation uses a facade pattern that wraps around the existing internals, which rely on a singleton pattern for accessing the cryptographic provider via CryptoImpl.provider. This creates a challenge: How do you prevent developers from inadvertently using multiple instances of OprfApi with different crypto providers, given that they would actually all use the same underlying provider?
Runtime Checks: A Temporary Safeguard
To mitigate this risk, the OprfApi implementation includes a runtime check to ensure that all instances are using the expected cryptographic provider. This is done using the errorIfCryptoChanged function, which wraps all methods in the API that rely on cryptographic operations.
Here's a simplified version of how it works:
export function errorIfCryptoChanged<T>(
bind: string,
val: T,
keys: AllFuncKeys<T>
) {
// Runtime check
if (bind !== CryptoImpl.name) {
throw new Error('Currently only one supported CryptoProvider at a time');
}
// ... (rest of the wrapping logic)
}
By applying this function to wrap all relevant methods, the API ensures that if the CryptoImpl.provider changes, an error is thrown to alert the developer. This effectively prevents a situation where different instances of OprfApi might be assumed to use different crypto providers but would actually all revert to the same one, thereby causing inconsistencies or even security vulnerabilities.
Toward a More Flexible Future
This runtime check is a temporary measure, meant to be in place until the internals of the OprfApi can be refactored to genuinely support multiple crypto providers. Once that is done, each instance of OprfApi will be able to operate independently with its own CryptoProvider, fulfilling the promise of both flexibility and robustness.
Conclusion
The use of runtime checks to enforce consistency in the cryptographic provider serves as a cautionary safeguard, ensuring that developers are aware of the current limitations while also signaling the intent for future flexibility. This approach maintains the integrity and security of the application, even as it evolves to offer more configurability.
(ChatGPT ... )
Examples Types Impl
The Evolution of the OprfApi: A Story of Type Safety and Flexibility
Introduction
In the world of cryptography, type safety and flexibility are often at odds. A rigid type system can provide a strong contract, but it can also make the system difficult to extend or adapt. On the other hand, a flexible system can be easier to work with, but it can also introduce vulnerabilities or bugs. Let's explore how the
OprfApi
transitioned from a more rigid structure to a more flexible and type-safe design, improving both developer experience and security.The Original API: Explicit but Cumbersome
The original was designed to be explicit, requiring the developers to manually manage various aspects like the cryptographic suite and the mode of operation. Here's how it looked:
While this design made it clear what the developer was working with, it also made the API somewhat cumbersome. For example, each new client and server instance had to be explicitly created with the correct
suite
.The Proposed API: A Step Toward Abstraction
The proposed API aimed to reduce this rigidity by abstracting away some of the details:
However, this proposed approach ran into a problem: creating a generic
makeClient
interface became challenging due to the need to make the public key optional, which was not ideal for type safety.The Final API: Type-Safe and Intuitive
The final version of the API solves these issues by leveraging TypeScript's conditional types and mapped types, making the API more flexible and type-safe at the same time.
The use of conditional types allows the
Client
interface to have different method signatures based on the mode of operation (M
). This means that thefinalize
method can have different argument types and return types based on whether the mode ispoprf
or something else, without compromising type safety.We did experiment with simply using advanced features to automatically append arguments, but then the IDE experience was not as nice, showing only generic argument names.
Why the CryptoProvider?
In the final API, the
CryptoProvider
is part of theOprfApi
. This is an important design choice because it allows developers to inject their own cryptographic libraries or functionalities, making the API adaptable to different environments or constraints.Initially, only one configuration could be allowed at a time, with runtime failure when an "old" OprfApi is used, but with some refactoring it will be possible to support more than one at a time.
No More Passing Around
suiteID
andmodeID
One of the most significant improvements is that developers no longer need to pass around
suiteID
andmodeID
explicitly. Instead, they can simply create aMode
object:Once this object is created, it knows how to operate on keys and create client and server instances, encapsulating the suite and mode information within itself.
Compare signatures:
vs
Conclusion
By leveraging TypeScript's advanced type features, the API still provides a robust contract for developers while also being easier and more intuitive to use.
The Intermediary Pattern: A Flexible Yet Constrained Crypto Provider
At the heart of the cryptographic operations within the library is the
CryptoProviderIntermediary
class. This class serves as a flexible conduit that conforms to theCryptoProvider
interface. It encapsulates an actualCryptoProvider
instance, allowing the underlying provider to be switched at runtime.However, this flexibility comes with a caveat. All instances of
OprfApi
share this singletonCryptoProvider
viaCryptoImpl.provider
, constraining them to use the same cryptographic provider at any given time.By understanding this intermediary design pattern, developers can easily switch between cryptographic providers for all instances of
OprfApi
. Yet they should also be cautious, as changing the provider in one place will affect it globally across allOprfApi
instances, due to the current reliance on the singleton pattern.Implementation Details: Safeguarding Against Inconsistent Crypto Providers
The Challenge of Supporting Multiple Crypto Providers
The eventual goal of the OprfApi is to support multiple instances, each configured with a different
CryptoProvider
. However, the current implementation uses a facade pattern that wraps around the existing internals, which rely on a singleton pattern for accessing the cryptographic provider viaCryptoImpl.provider
. This creates a challenge: How do you prevent developers from inadvertently using multiple instances ofOprfApi
with different crypto providers, given that they would actually all use the same underlying provider?Runtime Checks: A Temporary Safeguard
To mitigate this risk, the OprfApi implementation includes a runtime check to ensure that all instances are using the expected cryptographic provider. This is done using the
errorIfCryptoChanged
function, which wraps all methods in the API that rely on cryptographic operations.Here's a simplified version of how it works:
By applying this function to wrap all relevant methods, the API ensures that if the
CryptoImpl.provider
changes, an error is thrown to alert the developer. This effectively prevents a situation where different instances ofOprfApi
might be assumed to use different crypto providers but would actually all revert to the same one, thereby causing inconsistencies or even security vulnerabilities.Toward a More Flexible Future
This runtime check is a temporary measure, meant to be in place until the internals of the OprfApi can be refactored to genuinely support multiple crypto providers. Once that is done, each instance of
OprfApi
will be able to operate independently with its ownCryptoProvider
, fulfilling the promise of both flexibility and robustness.Conclusion
The use of runtime checks to enforce consistency in the cryptographic provider serves as a cautionary safeguard, ensuring that developers are aware of the current limitations while also signaling the intent for future flexibility. This approach maintains the integrity and security of the application, even as it evolves to offer more configurability.