blob: 4d7d4eef8a500a6762b786e7aa19679fef5ce5f9 [file] [log] [blame]
/*
* 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 java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.time.Instant;
import java.util.Optional;
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.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Represents an upload token returned by
* {@link DataRecordAccessProvider#initiateDataRecordUpload(long, int)} and
* used in subsequent calls to {@link
* DataRecordAccessProvider#completeDataRecordUpload(String)}. This class
* handles creation, signing, and parsing of the token and uses a provided
* secret key to sign the contents of the token and to validate contents of
* tokens.
*/
public class DataRecordUploadToken {
private static Logger LOG = LoggerFactory.getLogger(DataRecordUploadToken.class);
private String blobId;
private Optional<String> uploadId;
/**
* Create an upload token from the provided {@code blobId} and {@code
* uploadId}. At creation time the token is not encoded or signed; to do
* that call {@link #getEncodedToken(byte[])} after creating the token.
*
* @param blobId The blob ID, usually a {@link
* org.apache.jackrabbit.core.data.DataIdentifier}.
* @param uploadId A free-form string used to identify this upload. This
* may be provided by the service provider; if not a free-form
* upload ID generated by the implementation will suffice. May be
* {@code null} if no upload ID is available. However, some service
* providers will require an upload ID to complete the upload so be
* sure to check whether the service provider API provides one and
* use that if it is available.
*/
public DataRecordUploadToken(@NotNull String blobId, @Nullable String uploadId) {
this.blobId = blobId;
this.uploadId = Optional.ofNullable(uploadId);
}
/**
* Create an upload token instance from the provided encoded token string,
* using the provided secret key to verify the string. The encoded token
* string should have been created originally by a prior call to {@link
* #getEncodedToken(byte[])}.
* <p>
* This method will parse and validate the contents of the provided encoded
* token string. An instance of this class is returned if the parsing and
* validation is successful.
* <p>
* A secret key is required to verify the encoded token. You are strongly
* encouraged to use the secret key used by the data store backend
* implementation. This key can be obtained by calling {@link
* AbstractSharedBackend#getOrCreateReferenceKey()}.
*
* @param encoded The encoded, signed token string.
* @param secret The secret key to be used to verify the contents of the
* token string.
* @return A new instance containing the parsed upload token propreties.
* @throws IllegalArgumentException if the token string cannot be parsed or
* if validation fails.
*/
public static DataRecordUploadToken fromEncodedToken(@NotNull String encoded, @NotNull byte[] secret)
throws IllegalArgumentException {
final String[] parts = encoded.split("#", 2);
if (parts.length < 2) {
throw new IllegalArgumentException("Invalid upload token");
}
final String toBeDecoded = parts[0];
final String expectedSig = parts[1];
final String actualSig = getSignedString(toBeDecoded, secret);
if (!expectedSig.equals(actualSig)) {
throw new IllegalArgumentException("Invalid upload token");
}
String decoded = decodeBase64(toBeDecoded);
String decodedParts[] = decoded.split("#");
if (decodedParts.length < 2) {
throw new IllegalArgumentException("Invalid upload token");
}
return new DataRecordUploadToken(decodedParts[0], decodedParts.length > 2 ? decodedParts[2] : null);
}
/**
* Generate an encoded, signed token string from this instance. The
* resulting token can later be parsed and validated by {@link
* #fromEncodedToken(String, byte[])}.
* <p>
* A secret key is required to generate the encoded token. You are strongly
* encouraged to use the secret key used by the data store backend
* implementation. This key can be obtained by calling {@link
* AbstractSharedBackend#getOrCreateReferenceKey()}.
*
* @param secret The secret key used to sign the contents of the token.
* @return An encoded token string that can later be used to uniquely and
* securely identify an upload.
*/
public String getEncodedToken(@NotNull byte[] secret) {
String now = Instant.now().toString();
String toBeEncoded = uploadId.isPresent() ?
Joiner.on("#").join(blobId, now, uploadId.get()) :
Joiner.on("#").join(blobId, now);
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(StandardCharsets.UTF_8));
return encodeBase64(hash);
}
catch (NoSuchAlgorithmException | InvalidKeyException e) {
LOG.warn("Could not sign upload token", e);
}
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.
*
* @return The blob ID.
*/
public String getBlobId() {
return blobId;
}
/**
* Returns the upload ID of this instance.
*
* @return The upload ID.
*/
public Optional<String> getUploadId() {
return uploadId;
}
}