One goal of Tink is to discourage bad practices. Of particular interest in this section are two points:
- Tink encourages usage in such a way that users cannot access secret key material. Instead, secret keys should be stored in a KMS whenever possible using one of the pre-defined ways in which Tink supports such systems.
- Tink discourages users from accessing parts of keys, as doing so often results in compatibility bugs.
In practice, of course both of these principle have to be violated sometimes. For this, Tink provides different mechanisms.
Secret Key Access Tokens
In order to access secret key material, users have to have a token (which
typically just is an object of some class, without any functionality).
The token is typically provided by a method such as
Google, users are prevented from using this function using
Bazel BUILD visibility.
Outside of Google, we expect that security reviewers will search their code
base for usages of this function.
One useful feature of these tokens is that they can be passed on. For example, suppose we have a function which serializes an arbitrary Tink key, such as:
String serializeKey(Key key, Optional<SecretKeyAccess> secretKeyAccess);
For keys which have secret key material, this function requires the
secretKeyAccess optional object to have an actual
stored. For keys which do not have any secret
secretKeyAccess will be ignored.
Given such a function, it is possible to write a function which serializes a whole keyset:
String serializeKeyset(KeysetHandle keyset, Optional<SecretKeyAccess> secretKeyAccess);
This function will simply call
serializeKey for each key in the keyset
internally, and pass the given
secretKeyAccess to the underlying function.
Users who then call
serializeKeyset without the need to serialize
secret key material can use
Optional.empty() as the second argument. Users
which have the need to serialize secret key material need to use
Accessing Partial Keys
A relatively common security bug is a "key reuse attack". This can occur when
users reuse e.g. the modulus
n and exponents
e of a RSA key in two
different settings (e.g., to compute signatures and encryptions)1.
Another relatively common mistake when dealing with cryptographic keys is to specify part of the key, and then "assume" the metadata. For example, suppose a user want to export an RSASSA-PSS key public key from Tink for use with a different library. In Tink, these keys have the following parts:
- The modulus
- The public exponent
- The specification of the two hash functions used internally
- The length of the salt used internally in the algorithm.
When exporting such a key, it is easy to ignore the hash functions and the salt length. This can work fine, as often other libraries do not ask for the hash functions (and e.g. just assume SHA256), and the hash function used in Tink is coincidentally the same as in the other library (or maybe the hash functions were chosen specifically so it works together with the other library).
Nevertheless, ignoring the hash functions would be a potentially expensive mistake. To see this, suppose that later a new key with a different hash function is added to the Tink keyset. Suppose that then, the key is exported with the method, and given to a business partner, which uses it with the other library. Tink now assumes a different internal hash function, and hence will not be able to verify the signature.
In this case, the function exporting the key should fail if the hash function does not match what the other library expects: otherwise, the exported key will be useless.
To prevent such mistakes, Tink restricts functions which give access to the key material which is only partial, but could be mistaken as a full key. For example, in Java Tink uses RestrictedApi for this.
When a user uses such an annotation, they are responsible for preventing both key reuse attacks and incompatibilities.