tink-crypto / tink

Tink is a multi-language, cross-platform, open source library that provides cryptographic APIs that are secure, easy to use correctly, and hard(er) to misuse.
https://developers.google.com/tink
Apache License 2.0
13.47k stars 1.18k forks source link

encrypt & decrypt works in tink-awskms 1.5.0, but doesn't works in 1.6.0 or later. #530

Closed zaneli closed 3 years ago

zaneli commented 3 years ago

Help us help you

Please tell us more about ✅ your Tink deployment.

Describe the bug

encrypt & decrypt works in tink-awskms 1.5.0, but doesn't works in 1.6.0 or later.

To Reproduce

package com.zaneli.tink;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicSessionCredentials;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.services.securitytoken.AWSSecurityTokenService;
import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
import com.amazonaws.services.securitytoken.model.AssumeRoleRequest;
import com.amazonaws.services.securitytoken.model.Credentials;
import com.google.crypto.tink.*;
import com.google.crypto.tink.aead.AeadConfig;
import com.google.crypto.tink.aead.AeadKeyTemplates;
import com.google.crypto.tink.integration.awskms.AwsKmsClient;
import com.google.crypto.tink.proto.Keyset;
import com.google.crypto.tink.subtle.Base64;
import org.apache.commons.lang3.RandomStringUtils;

import java.nio.charset.StandardCharsets;

public class Example {
        private static final String url = "aws-kms://arn:aws:.....";

        public static void main(String[] args) throws Exception {
                new Example().execute();
        }

        public void execute() throws Exception {
                KmsClient client = new AwsKmsClient(url).withCredentialsProvider(createProvider());
                KmsClients.add(client);
                AeadConfig.register();

                String raw = "test value";
                Context encrypted = encrypt(raw);
                String decrypted = decrypt(encrypted);
                System.out.println(decrypted);
        }

        private Context encrypt(String raw) throws Exception {
                KeysetHandle keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.createKmsEnvelopeAeadKeyTemplate(url, AeadKeyTemplates.CHACHA20_POLY1305));
                Keyset keyset = CleartextKeysetHandle.getKeyset(keysetHandle);
                int primaryKeyId = keysetHandle.getKeysetInfo().getPrimaryKeyId();

                Aead aead = keysetHandle.getPrimitive(Aead.class);
                byte[] associatedData = RandomStringUtils.randomAlphanumeric(96).getBytes(StandardCharsets.UTF_8);

                String encrypted = Base64.urlSafeEncode(aead.encrypt(raw.getBytes(StandardCharsets.UTF_8), associatedData));
                String keyData = Base64.urlSafeEncode(keyset.getKey(0).getKeyData().getValue().toByteArray());

                return new Context(primaryKeyId, encrypted, keyData, associatedData);
        }

        private String decrypt(Context context) throws Exception {
                JsonKeysetReader keysetReader = JsonKeysetReader.withString(context.makeJsonKeysetString());
                KeysetHandle keysetHandle = CleartextKeysetHandle.read(keysetReader);
                Aead aead = keysetHandle.getPrimitive(Aead.class);
                return new String(aead.decrypt(Base64.urlSafeDecode(context.encryptedValue), context.associatedData), StandardCharsets.UTF_8);
        }

        private static class Context {
                private final int keyId;
                private final String encryptedValue;
                private final String keyData;
                private final byte[] associatedData;

                Context(int keyId, String encryptedValue, String keyData, byte[] associatedData) {
                        this.keyId = keyId;
                        this.encryptedValue = encryptedValue;
                        this.keyData = keyData;
                        this.associatedData = associatedData;
                }

                /**
                 * example
                 * {
                 *    "primaryKeyId":12345678,
                 *    "key":[
                 *       {
                 *          "keyData":{
                 *             "typeUrl":"type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey",
                 *             "value":"xxxxx",
                 *             "keyMaterialType":"REMOTE"
                 *          },
                 *          "outputPrefixType":"TINK",
                 *          "keyId": 12345678,
                 *          "status":"ENABLED"
                 *       }
                 *    ]
                 * }
                 */
                String makeJsonKeysetString() {
                        return "{\"primaryKeyId\":"+ keyId + ",\"key\":[{\"keyData\":{\"typeUrl\":\"type.googleapis.com/google.crypto.tink.KmsEnvelopeAeadKey\",\"value\":\"" + keyData + "\",\"keyMaterialType\":\"REMOTE\"},\"outputPrefixType\":\"TINK\",\"keyId\":" + keyId + ",\"status\":\"ENABLED\"}]}";
                }
        }

        private AWSCredentialsProvider createProvider() {
                AWSSecurityTokenService tokenService = AWSSecurityTokenServiceClientBuilder
                        .standard()
                        .withCredentials(new ProfileCredentialsProvider(.....))
                        .withRegion(.....)
                        .build();

                AssumeRoleRequest roleRequest = new AssumeRoleRequest()
                        .withRoleArn(.....)
                        .withRoleSessionName(.....);

                Credentials credentials = tokenService.assumeRole(roleRequest).getCredentials();

                return new AWSStaticCredentialsProvider(new BasicSessionCredentials(
                        credentials.getAccessKeyId(),
                        credentials.getSecretAccessKey(),
                        credentials.getSessionToken()
                ));
        }
}
plugins {
    id 'java'
}

group 'com.zaneli.tink'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation group: 'com.google.crypto.tink', name: 'tink-awskms', version: '1.6.1'
    implementation group: 'com.amazonaws', name: 'aws-java-sdk-sts', version: '1.12.26'
    implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0'
}

test {
    useJUnitPlatform()
}

Expected behavior

output test value.

Error messages, stack traces, etc.

Exception in thread "main" java.security.GeneralSecurityException: decryption failed
    at com.google.crypto.tink.aead.AeadWrapper$WrappedAead.decrypt(AeadWrapper.java:83)
    at com.zaneli.tink.Example.decrypt(Example.java:58)
    at com.zaneli.tink.Example.execute(Example.java:36)
    at com.zaneli.tink.Example.main(Example.java:26)

Version information

Additional context

in 1.5.0, this entries size is 1, and return entry.getPrimitive().decrypt(ciphertextNoPrefix, associatedData); succeed. in 1.6.1, this entries size is 0. https://github.com/google/tink/blob/7c93a224b8fa6a3babfaf71c18c5610052dcbd61/java_src/src/main/java/com/google/crypto/tink/aead/AeadWrapper.java#L62

encrypt using 1.5.0, and decrypt using 1.6.1 works fine. So I guess encrypt implementation in 1.6.0 is something wrong.

thaidn commented 3 years ago

In makeJsonKeysetString can you change outputPrefixType: TINK to outputPrefixType: RAW and try again?

If that works, you might want to consider one of the following better approaches:

1/ To serialize/deserialize a keyset, use JsonKeysetReader/JsonKeysetWriter, instead of hardcoding it like this.

2/ You don't have to serialize the keyset at all. If you do this in decrypt it should also work:

private String decrypt(Context context) throws Exception {
                KeysetHandle keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.createKmsEnvelopeAeadKeyTemplate(url, AeadKeyTemplates.CHACHA20_POLY1305));
                Aead aead = keysetHandle.getPrimitive(Aead.class);
                return new String(aead.decrypt(Base64.urlSafeDecode(context.encryptedValue), context.associatedData), StandardCharsets.UTF_8);
        }
zaneli commented 3 years ago

Thank you.

In makeJsonKeysetString can you change outputPrefixType: TINK to outputPrefixType: RAW and try again?

I tried it and succeeded, but can you tell me the reason? Is outputPrefixType: TINK no longer usable? Or, originally I do not have to explicitly specify hard coded outputPrefixType?

thaidn commented 3 years ago

You should use JsonKeysetReader and JsonKeysetWriter to serialize and deserialize keysets. Don't hardcode the output.