All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.volcengine.tos.auth.SignV4 Maven / Gradle / Ivy

There is a newer version: 2.8.3
Show newest version
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;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy