001/*
002 * Copyright (c) 2010-2024 Mark Allen, Norbert Bartels.
003 *
004 * Permission is hereby granted, free of charge, to any person obtaining a copy
005 * of this software and associated documentation files (the "Software"), to deal
006 * in the Software without restriction, including without limitation the rights
007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
008 * copies of the Software, and to permit persons to whom the Software is
009 * furnished to do so, subject to the following conditions:
010 *
011 * The above copyright notice and this permission notice shall be included in
012 * all copies or substantial portions of the Software.
013 *
014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
020 * THE SOFTWARE.
021 */
022package com.restfb.util;
023
024import java.nio.charset.StandardCharsets;
025import java.util.Base64;
026import java.util.Objects;
027import java.util.Optional;
028import java.util.stream.Collectors;
029import java.util.stream.IntStream;
030
031import javax.crypto.Mac;
032import javax.crypto.spec.SecretKeySpec;
033
034/**
035 * A collection of data-encoding utility methods.
036 * 
037 * @author Josef Gierbl
038 * @author Mikael Grev
039 * @author <a href="http://restfb.com">Mark Allen</a>
040 * @since 1.6.13
041 */
042public final class EncodingUtils {
043
044  /**
045   * Prevents instantiation.
046   */
047  private EncodingUtils() {}
048
049  private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();
050
051  /**
052   * Decodes a base64-encoded string, padding out if necessary.
053   * 
054   * @param base64
055   *          The base64-encoded string to decode.
056   * @return A decoded version of {@code base64}.
057   * @throws NullPointerException
058   *           If {@code base64} is {@code null}.
059   */
060  public static byte[] decodeBase64(String base64) {
061    return Base64.getDecoder().decode(Optional.ofNullable(base64).map(EncodingUtils::padBase64).orElseThrow(() -> new NullPointerException("Parameter 'base64' cannot be null.")));
062  }
063
064  private static String padBase64(String base64) {
065    String padding = "";
066    int remainder = base64.length() % 4;
067
068    if (remainder > 0) {
069      padding = IntStream.range(0, 4 - remainder).mapToObj(i -> "=").collect(Collectors.joining());
070    }
071
072    return base64 + padding;
073  }
074
075  /**
076   * Encodes a hex {@code byte[]} from given {@code byte[]}.
077   * <p>
078   * This function is equivalent to Apache commons-codec binary {@code new Hex().encode(byte[])}
079   * 
080   * @param data
081   *          The data to encode as hex.
082   * @return Hex-encoded {@code byte[]}
083   * @throws NullPointerException
084   *           If {@code data} is {@code null}.
085   */
086  public static byte[] encodeHex(final byte[] data) {
087    Objects.requireNonNull(data, "Parameter 'data' cannot be null.");
088    char[] out = new char[data.length << 1];
089    for (int j = 0; j < data.length; j++) {
090      int v = data[j] & 0xFF;
091      out[j * 2] = HEX_ARRAY[v >>> 4];
092      out[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
093    }
094    return new String(out).getBytes(StandardCharsets.UTF_8);
095  }
096
097  /**
098   * Generates an appsecret_proof for facebook.
099   * <p>
100   * See https://developers.facebook.com/docs/graph-api/securing-requests for more info
101   * 
102   * @param appSecret
103   *          The facebook application secret
104   * @param accessToken
105   *          The facebook access token
106   * @return A Hex encoded SHA256 Hash as a String
107   */
108  public static String encodeAppSecretProof(String appSecret, String accessToken) {
109    try {
110      byte[] key = appSecret.getBytes(StandardCharsets.UTF_8);
111      SecretKeySpec signingKey = new SecretKeySpec(key, "HmacSHA256");
112      Mac mac = Mac.getInstance("HmacSHA256");
113      mac.init(signingKey);
114      byte[] raw = mac.doFinal(accessToken.getBytes());
115      byte[] hex = encodeHex(raw);
116      return new String(hex, StandardCharsets.UTF_8);
117    } catch (Exception e) {
118      throw new IllegalStateException("Creation of appsecret_proof has failed", e);
119    }
120  }
121}