com.volcengine.tos.auth.SignV4 Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ve-tos-java-sdk Show documentation
Show all versions of ve-tos-java-sdk Show documentation
The VolcEngine TOS SDK for Java
package com.volcengine.tos.auth;
import com.volcengine.tos.internal.TosRequest;
import com.volcengine.tos.internal.util.TosUtils;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.volcengine.tos.internal.util.TosUtils.uriEncode;
@FunctionalInterface
interface signingHeader{
boolean isSigningHeader(String key, boolean isSigningQuery);
}
@FunctionalInterface
interface signKey{
byte[] signingKey(SignKeyInfo info);
}
public class SignV4 implements Signer {
private static final Logger LOG = LoggerFactory.getLogger(SignV4.class);
static final String emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
static final String unsignedPayload = "UNSIGNED-PAYLOAD";
static final String signPrefix = "TOS4-HMAC-SHA256";
static final String authorization = "Authorization";
static final DateTimeFormatter yyyyMMdd = DateTimeFormatter.ofPattern("yyyyMMdd");
static final DateTimeFormatter iso8601Layout = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
static final String v4Algorithm = "X-Tos-Algorithm";
static final String v4Credential = "X-Tos-Credential";
static final String v4Date = "X-Tos-Date";
static final String v4Expires = "X-Tos-Expires";
static final String v4SignedHeaders = "X-Tos-SignedHeaders";
static final String v4Signature = "X-Tos-Signature";
static final String v4SignatureLower = "x-tos-signature";
static final String v4ContentSHA256 = "X-Tos-Content-Sha256";
static final String v4SecurityToken = "X-Tos-Security-Token";
static final String v4Prefix = "x-tos";
private Credentials credentials;
private final String region;
private signingHeader signingHeader;
private Predicate signingQuery;
private Supplier now;
private signKey signKey;
public SignV4(Credentials credentials, String region) {
this.credentials = credentials;
this.region = region;
this.signingHeader = SignV4::defaultSigningHeaderV4;
this.signingQuery = SignV4::defaultSigningQueryV4;
this.now = SignV4::defaultUTCNow;
this.signKey = SignV4::signKey;
}
public Supplier getNow() {
return now;
}
public void setNow(Supplier date) {
this.now = date;
}
@Override
public Map signHeader(TosRequest req) {
Objects.requireNonNull(req.getHost(), "request host is null");
Map signed = new HashMap<>(4);
OffsetDateTime now = this.now.get().atOffset(ZoneOffset.UTC);
String date = now.format(iso8601Layout);
String contentSha256 = req.getHeaders().get(v4ContentSHA256);
Map header = req.getHeaders();
List> signedHeader = this.signedHeader(header, false);
signedHeader.add(new SimpleEntry<>(v4Date.toLowerCase(), date));
signedHeader.add(new SimpleEntry<>("date", date));
signedHeader.add(new SimpleEntry<>("host", req.getHost()));
Credential cred = this.credentials.credential();
if (StringUtils.isNotEmpty(cred.getSecurityToken())) {
signedHeader.add(new SimpleEntry<>(v4SecurityToken.toLowerCase(), cred.getSecurityToken()));
signed.put(v4SecurityToken, cred.getSecurityToken());
}
Collections.sort(signedHeader, new Comparator>() {
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
List> signedQuery = this.signedQuery(req.getQuery(), null);
String sign = this.doSign(req.getMethod(), req.getPath(), contentSha256, signedHeader, signedQuery, now, cred);
String credential = String.format("%s/%s/%s/tos/request", cred.getAccessKeyId(), now.format(yyyyMMdd), this.region);
String keys = signedHeader.stream().map(Map.Entry::getKey).sorted().collect(Collectors.joining(";"));
String auth = String.format("TOS4-HMAC-SHA256 Credential=%s,SignedHeaders=%s,Signature=%s", credential, keys, sign);
signed.put(authorization, auth);
signed.put(v4Date, date);
signed.put("Date", date);
return signed;
}
@Override
public Map signQuery(TosRequest req, Duration ttl) {
OffsetDateTime now = this.now.get().atOffset(ZoneOffset.UTC);
String date = now.format(iso8601Layout);
Map query = req.getQuery();
Map extra = new HashMap<>();
Credential cred = this.credentials.credential();
String credential = String.format("%s/%s/%s/tos/request", cred.getAccessKeyId(), now.format(yyyyMMdd), this.region);
extra.put(v4Algorithm, signPrefix);
extra.put(v4Credential, credential);
extra.put(v4Date, date);
extra.put(v4Expires, String.valueOf(ttl.toMillis() / 1000));
if (StringUtils.isNotEmpty(cred.getSecurityToken())) {
extra.put(v4SecurityToken, cred.getSecurityToken());
}
// extra.put(v4SignedHeaders, "host"); // 目前只有host
List> signedHeader = this.signedHeader(req.getHeaders(), true);
String host = req.getHost();
if (StringUtils.isEmpty(host)) {
throw new IllegalArgumentException("params.getHost() get null/empty");
}
signedHeader.add(new SimpleEntry<>("host", host));
Collections.sort(signedHeader, new Comparator>() {
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
String keys = signedHeader.stream().map(Map.Entry::getKey).sorted().collect(Collectors.joining(";"));
extra.put(v4SignedHeaders, keys);
List> signedQuery = this.signedQuery(query, extra);
String sign = this.doSign(req.getMethod(), req.getPath(), unsignedPayload, signedHeader, signedQuery, now, cred);
extra.put(v4Signature, sign);
return extra;
}
public SignV4 withSignKey(signKey signKey) {
this.signKey = signKey;
return this;
}
public static Instant defaultUTCNow() {
return Instant.now();
}
/**
* @param key header key
* @return boolean, 需要加入签名中 or not
*/
public static boolean defaultSigningHeaderV4(String key, boolean isSigningQuery) {
if (StringUtils.isEmpty(key)) {
return false;
}
return ("content-type".equals(key) && !isSigningQuery) || key.startsWith(v4Prefix);
}
/**
* @param key query key
* @return boolean, 需要加入签名中 or not
*/
public static boolean defaultSigningQueryV4(String key) {
return !v4SignatureLower.equals(key);
}
/**
* 返回的数据没有排序
*/
private List> signedHeader(Map header, boolean isSignedQuery) {
ArrayList> signed = new ArrayList<>(10);
if (header == null || header.isEmpty()) {
return signed;
}
for (Map.Entry entry : header.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
if (StringUtils.isNotEmpty(key)) {
String kk = key.toLowerCase();
if (this.signingHeader.isSigningHeader(kk, isSignedQuery)) {
value = value == null ? "" : value;
value = StringUtils.join(StringUtils.split(value), " "); // 目前只支持一个header value
signed.add(new SimpleEntry<>(kk, value));
}
}
}
return signed;
}
/**
* 返回的数据没有排序
*/
private List> signedQuery(Map query, Map extra) {
ArrayList> signed = new ArrayList<>(10);
if (query != null) {
query.forEach((k, v) -> {
if (this.signingQuery.test(k.toLowerCase())) {
signed.add(new SimpleEntry<>(k, v));
}
});
}
if (extra != null) {
extra.forEach((k, v) -> {
if (this.signingQuery.test(k.toLowerCase())) {
signed.add(new SimpleEntry<>(k, v));
}
});
}
return signed;
}
private String canonicalRequest(String method, String path, String contentSha256,
List> header,
List> query) {
final char split = '\n';
StringBuilder buf = new StringBuilder(512);
buf.append(method);
buf.append(split);
buf.append(encodePath(path));
buf.append(split);
buf.append(encodeQuery(query));
buf.append(split);
if (header == null) {
header = Collections.emptyList();
}
ArrayList keys = new ArrayList<>(header.size());
Collections.sort(header, new Comparator>() {
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
for (Map.Entry entry : header) {
String key = entry.getKey();
keys.add(key);
buf.append(key);
buf.append(':');
// 暂时只支持一个value
buf.append(entry.getValue() == null ? "" : entry.getValue());
buf.append('\n');
}
buf.append(split); // header
buf.append(StringUtils.join(keys, ";"));
buf.append(split);
if (StringUtils.isNotEmpty(contentSha256)) {
buf.append(contentSha256);
} else {
buf.append(emptySHA256);
}
return buf.toString();
}
static byte[] signKey(SignKeyInfo info) {
byte[] date = hmacSha256(info.getCredential().getAccessKeySecret().getBytes(StandardCharsets.UTF_8),
info.getDate().getBytes(StandardCharsets.UTF_8));
byte[] region = hmacSha256(date, info.getRegion().getBytes(StandardCharsets.UTF_8));
byte[] service = hmacSha256(region, "tos".getBytes(StandardCharsets.UTF_8));
return hmacSha256(service, "request".getBytes(StandardCharsets.UTF_8));
}
private String doSign(String method, String path, String contentSha256,
List> header,
List> query,
OffsetDateTime now, Credential cred) {
final char split = '\n';
String req = this.canonicalRequest(method, path, contentSha256, header, query);
LOG.debug("canonical request:\n{}", req);
StringBuilder buf = new StringBuilder(signPrefix.length() + 128);
buf.append(signPrefix);
buf.append(split);
buf.append(now.format(iso8601Layout));
buf.append(split);
String date = now.format(yyyyMMdd);
buf.append(date).append('/')
.append(this.region).append("/tos/request");
buf.append(split);
byte[] sum = sha256(req);
buf.append(toHex(sum));
LOG.debug("string to sign:\n {}", buf.toString());
byte[] signK = signKey(new SignKeyInfo(date, this.region, cred));
byte[] sign = hmacSha256(signK, buf.toString().getBytes(StandardCharsets.UTF_8));
return String.valueOf(toHex(sign));
}
private static String encodePath(String path) {
if (path == null || path.isEmpty()) {
return "/";
}
return uriEncode(path, false);
}
private static String encodeQuery(List> query) {
if (query == null || query.isEmpty()) {
return "";
}
StringBuilder buf = new StringBuilder(512);
Collections.sort(query, new Comparator>() {
@Override
public int compare(Map.Entry o1, Map.Entry o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
for (Map.Entry kv : query) {
String keyEscaped = uriEncode(kv.getKey(), true);
if (buf.length() > 0){
buf.append('&');
}
buf.append(keyEscaped);
buf.append('=');
buf.append(uriEncode(kv.getValue() == null ? "" : kv.getValue(), true));
}
return buf.toString();
}
static byte[] hmacSha256(byte[] key, byte[] value) {
try {
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(key, "HmacSHA256");
hmac.init(secretKey);
return hmac.doFinal(value);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
public static byte[] sha256(String data) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(data.getBytes(StandardCharsets.UTF_8));
return md.digest();
} catch (Exception e) {
throw new IllegalArgumentException(e.getMessage());
}
}
private static final char[] HEX = "0123456789abcdef".toCharArray();
private static char[] toHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = HEX[v >>> 4];
hexChars[j * 2 + 1] = HEX[v & 0x0F];
}
return hexChars;
}
}