tink-crypto / tink-java

Java implementation of Tink
https://developers.google.com/tink
Apache License 2.0
117 stars 16 forks source link

How to get prefix for tink wire format of `prefix || encapsulated_key || encrypted_data` from a server that doesn't use tink #28

Closed ColtonIdle closed 5 months ago

ColtonIdle commented 6 months ago

I'm using hpke with a server that already exists (the server does not use tink). What I'm working on, is a java desktop app that uses tink. Everything is going smoothly except I can't decrypt messages from the server. When the server sends me a message it sends me the key and the message in json format, and i grab the key and message from that.

{ "key" = "OHzZgmNwayS5vyE\/1hj+eTJ1ebrakGg7ZVA2vENMFAQ=", "message="MZxGIq9CZJZ74mf2caSFxHh6Oo+z" }

if I base64 decode those two values then I get a byteArray of size 32 for the key, and size 21 for the message. (This seems to be correct because it's the exact size I generate when I send a message from my desktop app) except that my desktop app uses tink, and so it adds the 5 extra bytes to the start of the data (that makes sense because thats what tink wire format docs state:)

prefix || encapsulated_key || encrypted_data 5 bytes + 32 bytes + 21 bytes

so basically everything seems good to go, BUT when I try to decrypt I get a failure


val createTinkWireFormatFromServer: ByteArray = byteArrayOf(0,0,0,0,0) + Base64.decode(key) + Base64.decode(message)
val decryptedData: ByteArray = decryptor.decrypt(createTinkWireFormatFromServer, ByteArray(0))

I'm assuming I just can't use 5 empty bytes for the prefix... but any idea how to generate the prefix?

juergw commented 5 months ago

The prefix depends on the key ID, which is chosen at random. (This makes decryption faster, because Tink can find the correct key in the keyset faster.) It is not a good idea to add the prefix yourself to the ciphertext. Instead, you should use a key that doesn't require this prefix, using NO_PREFIX: https://github.com/tink-crypto/tink-java/blob/main/src/main/java/com/google/crypto/tink/hybrid/HpkeParameters.java#L34

ColtonIdle commented 5 months ago

This is what I'm doing now. I guess this is bad?

val hpkeParams = com.google.crypto.tink.proto.HpkeParams.newBuilder()
    .setKem(com.google.crypto.tink.proto.HpkeKem.DHKEM_X25519_HKDF_SHA256)
    .setKdf(com.google.crypto.tink.proto.HpkeKdf.HKDF_SHA256)
    .setAead(com.google.crypto.tink.proto.HpkeAead.AES_256_GCM)
    .build()

val hpkeKeyFormat = com.google.crypto.tink.proto.HpkeKeyFormat.newBuilder()
    .setParams(hpkeParams)
    .build()

val a = KeyTemplate.create(
    "type.googleapis.com/google.crypto.tink.HpkePrivateKey",
    hpkeKeyFormat.toByteArray(),
    OutputPrefixType.RAW
)
tholenst commented 5 months ago

We do not recommend using protos. They aren't really in the public API (though it's slightly unclear). However, more importantly they do not have an API which we can design since they are tied to the serialization of the key.

Instead, do something like this:

HpkeParameters.Builder params =
    HpkeParameters.builder()
        .setVariant(HpkeParameters.Variant.NO_PREFIX)
        .setKemId(HpkeParameters.KemId.DHKEM_X25519_HKDF_SHA256)
        .setKdfId(HpkeParameters.KdfId.HKDF_SHA256)
        .setAeadId(HpkeParameters.AeadId.AES_256_GCM).build();
byte[] publicKeyByteArray = ...
Bytes publicKeyBytes = Bytes.copyFrom(publicKeyByteArray);
HpkePublicKey publicKey1 =
    HpkePublicKey.create(params, publicKeyBytes, /* idRequirement= */ 123);

By using NO_PREFIX you ensure that Tink doesn't use the 5 byte prefix above.

ColtonIdle commented 5 months ago

Thanks for that snippet.

The last line of my snippet should have shown (my apologies that I missed it the first time)

 KeysetHandle.generateNew(a)

In your snippet it's creating from an existing publicKeyByteArray. But what if I want to use tink to actually generate a public/private keypair (keyset?) instead of importing? (well I actually want to know how to import into tink and generate with tink, but now I know how to import into tink. thanks to you!) If I use the typical way to generate from tink then it'll give me a prefix, which I would like to opt out of/use OutputPrefixType.RAW)

tholenst commented 5 months ago

If you want to create a new one, you can e.g. do

KeysetHandle.generateNew(params)

after you defined params in my snippet.

OutputPrefixType.RAW is a proto thing, I would recommend HpkeParameters.Variant.NO_PREFIX.

ColtonIdle commented 5 months ago

Wow! Can't believe I missed that. Thank you @tholenst . This has been insanely helpful!