commit 84d48b7c9dc78801751c61086f894106f81cf276 Author: Alexander Klimetschek Date: Wed Aug 8 16:14:35 2018 -0700 OAK-7692 - [DirectBinaryAccess] Upload token HMAC signature must be base64 encoded diff --git a/oak-blob-plugins/src/main/java/org/apache/jackrabbit/oak/plugins/blob/datastore/directaccess/DataRecordUploadToken.java b/oak-blob-plugins/src/main/java/org/apache/jackrabbit/oak/plugins/blob/datastore/directaccess/DataRecordUploadToken.java index 2b02f06e10..4d7d4eef8a 100644 --- a/oak-blob-plugins/src/main/java/org/apache/jackrabbit/oak/plugins/blob/datastore/directaccess/DataRecordUploadToken.java +++ b/oak-blob-plugins/src/main/java/org/apache/jackrabbit/oak/plugins/blob/datastore/directaccess/DataRecordUploadToken.java @@ -18,6 +18,7 @@ */ package org.apache.jackrabbit.oak.plugins.blob.datastore.directaccess; +import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Instant; @@ -27,8 +28,9 @@ import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import com.google.common.base.Joiner; + +import org.apache.commons.codec.binary.Base64; import org.apache.jackrabbit.oak.spi.blob.AbstractSharedBackend; -import org.apache.jackrabbit.util.Base64; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; @@ -93,22 +95,22 @@ public class DataRecordUploadToken { */ public static DataRecordUploadToken fromEncodedToken(@NotNull String encoded, @NotNull byte[] secret) throws IllegalArgumentException { - String[] parts = encoded.split("#", 2); + final String[] parts = encoded.split("#", 2); if (parts.length < 2) { - throw new IllegalArgumentException("Encoded string is missing the signature"); + throw new IllegalArgumentException("Invalid upload token"); } - String toBeDecoded = parts[0]; - String expectedSig = Base64.decode(parts[1]); - String actualSig = getSignedString(toBeDecoded, secret); + final String toBeDecoded = parts[0]; + final String expectedSig = parts[1]; + final String actualSig = getSignedString(toBeDecoded, secret); if (!expectedSig.equals(actualSig)) { - throw new IllegalArgumentException("Upload token signature does not match"); + throw new IllegalArgumentException("Invalid upload token"); } - String decoded = Base64.decode(toBeDecoded); + String decoded = decodeBase64(toBeDecoded); String decodedParts[] = decoded.split("#"); if (decodedParts.length < 2) { - throw new IllegalArgumentException("Not all upload token parts provided"); + throw new IllegalArgumentException("Invalid upload token"); } return new DataRecordUploadToken(decodedParts[0], decodedParts.length > 2 ? decodedParts[2] : null); @@ -133,19 +135,20 @@ public class DataRecordUploadToken { String toBeEncoded = uploadId.isPresent() ? Joiner.on("#").join(blobId, now, uploadId.get()) : Joiner.on("#").join(blobId, now); - String toBeSigned = Base64.encode(toBeEncoded); + String toBeSigned = encodeBase64(toBeEncoded); String sig = getSignedString(toBeSigned, secret); return sig != null ? Joiner.on("#").join(toBeSigned, sig) : toBeSigned; } + /** Returns the base64 encoded HMAC signature */ private static String getSignedString(String toBeSigned, byte[] secret) { try { final String algorithm = "HmacSHA1"; Mac mac = Mac.getInstance(algorithm); mac.init(new SecretKeySpec(secret, algorithm)); - byte[] hash = mac.doFinal(toBeSigned.getBytes()); - return new String(hash); + byte[] hash = mac.doFinal(toBeSigned.getBytes(StandardCharsets.UTF_8)); + return encodeBase64(hash); } catch (NoSuchAlgorithmException | InvalidKeyException e) { LOG.warn("Could not sign upload token", e); @@ -153,6 +156,18 @@ public class DataRecordUploadToken { return null; } + private static String encodeBase64(String string) { + return Base64.encodeBase64String(string.getBytes(StandardCharsets.UTF_8)); + } + + private static String encodeBase64(byte[] bytes) { + return Base64.encodeBase64String(bytes); + } + + private static String decodeBase64(String encodedString) { + return new String(Base64.decodeBase64(encodedString), StandardCharsets.UTF_8); + } + /** * Returns the blob ID of this instance. * diff --git a/oak-blob-plugins/src/test/java/org/apache/jackrabbit/oak/plugins/blob/datastore/directaccess/DataRecordUploadTokenTest.java b/oak-blob-plugins/src/test/java/org/apache/jackrabbit/oak/plugins/blob/datastore/directaccess/DataRecordUploadTokenTest.java new file mode 100644 index 0000000000..97ad87ee9e --- /dev/null +++ b/oak-blob-plugins/src/test/java/org/apache/jackrabbit/oak/plugins/blob/datastore/directaccess/DataRecordUploadTokenTest.java @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.jackrabbit.oak.plugins.blob.datastore.directaccess; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import java.nio.charset.StandardCharsets; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +public class DataRecordUploadTokenTest { + + private static final String BLOB_ID = "blob"; + private static final String UPLOAD_ID = "upload"; + private static final byte[] SECRET = "1234567890".getBytes(StandardCharsets.UTF_8); + + @Test + public void testUploadToken() { + String encodedToken = new DataRecordUploadToken(BLOB_ID, UPLOAD_ID).getEncodedToken(SECRET); + + // also check token can be parsed and is valid + DataRecordUploadToken parsedToken = DataRecordUploadToken.fromEncodedToken(encodedToken, SECRET); + assertEquals(BLOB_ID, parsedToken.getBlobId()); + assertTrue(parsedToken.getUploadId().isPresent()); + assertEquals(UPLOAD_ID, parsedToken.getUploadId().get()); + } + + @Test + public void testUploadTokenIsAscii() { + + // run a few times to rule out the (low) chance it is ascii just by chance; the seed will change regularly + for (int i = 0; i < 1000; i++) { + String encodedToken = new DataRecordUploadToken(BLOB_ID, UPLOAD_ID).getEncodedToken(SECRET); + assertTrue("upload token is not ascii: " + encodedToken, StringUtils.isAsciiPrintable(encodedToken)); + + // also check token can be parsed and is valid + DataRecordUploadToken parsedToken = DataRecordUploadToken.fromEncodedToken(encodedToken, SECRET); + assertEquals(BLOB_ID, parsedToken.getBlobId()); + assertTrue(parsedToken.getUploadId().isPresent()); + assertEquals(UPLOAD_ID, parsedToken.getUploadId().get()); + } + } + + @Test + public void testUploadTokenSignature() { + // simple test to check the signature is present and validated + String spoofedToken = Base64.encodeBase64String((BLOB_ID + "#" + UPLOAD_ID).getBytes(StandardCharsets.UTF_8)); + + try { + DataRecordUploadToken.fromEncodedToken(spoofedToken, SECRET); + } catch (IllegalArgumentException expected) { + } + } +}