Employee Identity (EIAM)

Signature Verification and Signing Instructions

Identity Synchronization

When synchronizing data to enterprise applications, the enterprise application needs to identify and confirm the synchronization event to ensure the security and reliability of the event source, thereby guaranteeing data interaction in a secure environment.

# Signature Verification/Encryption and Decryption Terminology

Term Description
signature Message signature, used to verify whether the request originates from IDaaS, to prevent attackers from forging it. The signature algorithm is HMAC-SHA256 + Base64.
AESKey The key for the AES algorithm. Supported encryption algorithms are AES/GCM/NoPadding and AES/ECB/PKCS5Padding. AES/GCM/NoPadding is recommended.
msg Plaintext message body, formatted as JSON.
encrypt _msg Ciphertext obtained after encrypting the plaintext message msg and performing Base64 encoding.

# Signature Verification

To allow enterprise applications to confirm that event push notifications originate from IDaaS, when IDaaS pushes events to the enterprise application's callback service, the request body includes a request signature identified by the parameter signature. The enterprise application needs to verify the correctness of this parameter before decryption. The verification steps are as follows:

  1. Calculate the signature: It consists of five parts: the signature key, Nonce value, timestamp, event type, and encrypted message body, connected with &. The HMAC-SHA256 + Base64 algorithm is used for encryption. The following is a Java language signature example:
String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(签名秘钥.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(secretKey);
String newSignature = Base64.getEncoder().encodeToString(mac.doFinal(message.getBytes(StandardCharsets.UTF_8)));
1
2
3
4
5
  1. Compare the calculated signature cal_signature with the request parameter signature. If they are equal, the verification passes.

  2. The enterprise application returns the response message format as required.

# Plaintext Encryption Process

  1. Concatenate the plaintext string. The plaintext string is composed of a 16-byte random string and the plaintext msg, connected with &. The following is a Java language example:
String dataStr = RandomStringUtils.random(16, true, false) + "&" + data;
1
  1. Encrypt the concatenated plaintext string using the AESkey, then perform Base64 encoding to obtain the ciphertext encrypt_msg. The following are Java language examples demonstrating two different encryption algorithms:
  • AES/ECB/PKCS5Padding:

    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES");
    cipher.init(1, secretKey);
    byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8);
    String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
    
    1
    2
    3
    4
    5
  • AES/GCM/NoPadding:

    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    SecretKeySpec secretKey = new SecretKeySpec(加密密钥.getBytes(StandardCharsets.UTF_8), "AES");
    cipher.init(1, secretKey);
    byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8);
    //Generate a string of length 24
    String repIvStr = generate(24);
    byte[] iv =  Base64.getDecoder().decode(repIvStr);
    cipher.init(1, secretKey , new GCMParameterSpec(128, iv));
    String ecnryptStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

# Ciphertext Decryption Process

  1. Decode the ciphertext using BASE64.
byte[] encryptStr = Base64.getDecoder().decode(data);
1
  1. Decrypt using the AESKey.
  • AES/ECB/PKCS5Padding:

    Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
    SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(StandardCharsets.UTF_8), "AES");
    cipher.init(2, secretKey);
    byte[] bytes = cipher.doFinal(encryptStr);
    
    1
    2
    3
    4
  • AES/GCM/NoPadding:

    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    SecretKeySpec secretKey = new SecretKeySpec(encryptionKey.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
    byte[] iv = Base64.getDecoder().decode(data.substring(0, 24));
    data = data.substring(24);
    cipher.init(2, secretKey, new GCMParameterSpec(AES_KEY_SIZE, iv));
    byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(data));
    
    1
    2
    3
    4
    5
    6
  1. Obtain the plaintext content.
  • AES/ECB/PKCS5Padding: Need to remove the first 16 random bytes from the rand_msg header, the remaining part is the plaintext content msg

    String dataStr = new String(bytes, CHARSET).substring(17);
    
    1
  • AES/GCM/NoPadding:

    String dataStr = new String(bytes, CHARSET);
    
    1

# Data Signing & Encryption/Decryption Example

  • The following is a Java language example for data signing & encryption/decryption, demonstrating two algorithms respectively:
    • AES/ECB/PKCS5Padding:
    
    package BOOT-INF.classes.com.example.demo.controller;
      import com.alibaba.fastjson.JSON;
      import com.alibaba.fastjson.JSONObject;
      import org.apache.commons.lang3.RandomStringUtils;
      import org.apache.commons.lang3.StringUtils;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.RequestBody;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.ResponseBody;
    
      import javax.crypto.Cipher;
      import javax.crypto.Mac;
      import javax.crypto.spec.SecretKeySpec;
      import javax.servlet.http.HttpServletRequest;
      import java.nio.charset.Charset;
      import java.nio.charset.StandardCharsets;
      import java.util.Base64;
      import java.util.UUID;
    
      @RequestMapping
      @Controller
      public class DemoController {
      private static final Logger log = LoggerFactory.getLogger(DemoController.class);
      
          private static final String SEPARATOR = "&";
      
          private static final String SIGN_KEY = "wGt9VxV2qqLbgRrs";
      
          private static final String ENCRYPTION_KEY = "ZJIXSHUdo8WK7FQo";
      
          private static final String TOKEN = "4JVImwu3GdM3zNCE4JVImwu3GdM3zNCE";
      
          private static final String HMAC_SHA256 = "HmacSHA256";
      
          private static final String ENCRYPT_ALGORITHM = "AES/ECB/PKCS5Padding";
      
          private static final String ENCRYPT_KEY_ALGORITHM = "AES";
      
          private static final Charset CHARSET = StandardCharsets.UTF_8;
      
          @ResponseBody
          @RequestMapping({"/callback"})
          public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) {
              try {
                  // Verify authorization information
                  // Get the Authorization header information
                  String authorization = httpRequest.getHeader("Authorization");
                  assertAuthorizationMessage(authorization);
      
                  //Note: To determine if the received event callback message is a duplicate, it can be verified through the first 16 characters of the decrypted data. (i.e., the string before & is used to check if the message is a duplicate, the string after & is the object attribute data)
      
                  JSONObject messageBody = JSON.parseObject(body);
      
                  //Verify if the message body source is legitimate
                  assertMessageSourceValidity(messageBody);
      
                  //Parse the encrypted message body
                  String encryptMessage = messageBody.getString("data");
                  String plaintMessage = parseEncryptedMessage(encryptMessage);
      
                  // Process data
                  JSONObject eventData = JSON.parseObject(plaintMessage);
                  String eventType = messageBody.getString("eventType");
                  JSONObject processResultData = processEventData(eventData, eventType);
      
                  String processResultDataStr = RandomStringUtils.random(16, true, false)+ "&" + processResultData.toJSONString(); ;
                  String encryptResultData =encryptResultData(processResultDataStr);
                  JSONObject result = new JSONObject();
                  result.put("code", "200");
                  result.put("message", "success");
                  result.put("data", encryptResultData);
                  return result;
              } catch (Exception e) {
                  JSONObject result = new JSONObject();
                  result.put("code", "400");
                  String message = e.getMessage();
                  result.put("message", StringUtils.isNotEmpty(message) ? message : e);
                  return result;
              }
          }
    
          private String encryptResultData(String dataStr) {
              try {
                  Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                  SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                  cipher.init(1, secretKey);
                  byte[] bytes = dataStr.getBytes(CHARSET);
                  return Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
              } catch (Exception e) {
                  throw new RuntimeException("error encrypt Result Data", e);
              }
          }
      
          private void assertAuthorizationMessage(String authorization) {
              if (!"Bearer ".concat(TOKEN).equals(authorization)) {
                  throw new RuntimeException("error authorization.");
              }
          }
      
          private void assertMessageSourceValidity(JSONObject messageBody) {
              try {
                  String nonce = messageBody.getString("nonce");
                  String timestamp = messageBody.getString("timestamp");
                  String eventType = messageBody.getString("eventType");
                  String encryptData = messageBody.getString("data");
                  String signature = messageBody.getString("signature");
      
                  String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
                  Mac mac = Mac.getInstance(HMAC_SHA256);
                  SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
                  mac.init(secretKey);
                  byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
                  String cal_signature = Base64.getEncoder().encodeToString(bytes);
      
                  if (!cal_signature.equals(signature)) {
                      throw new RuntimeException("signature validation failed.");
                  }
              } catch (Exception e) {
                  throw new RuntimeException("error validating message signature", e);
              }
          }
    
          private String parseEncryptedMessage(String encryptMessage) {
              try {
                  Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
                  SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
                  cipher.init(2, secretKey);
                  byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptMessage));
                  return new String(bytes, CHARSET).substring(17);
              } catch (Exception e) {
                  throw new RuntimeException("error parsing encrypted message", e);
              }
          }
      
          private JSONObject processEventData(JSONObject eventData, String eventType) {
              if (eventType.equals("CREATE_USER")) {
                  return processCreateUser(eventData);
              } else if (eventType.equals("CREATE_ORGANIZATION")) {
                  return processCreateOrganization(eventData);
              } else if (eventType.equals("UPDATE_USER")) {
                  return processUpdateUser(eventData);
              } else if (eventType.equals("UPDATE_ORGANIZATION")) {
                  return processUpdateOrganization(eventData);
              } else if (eventType.equals("DELETE_ORGANIZATION")) {
                  // No data to return
                  processDeleteUser(eventData);
                  return new JSONObject();
              } else if (eventType.equals("DELETE_USER")) {
                  // No data to return
                  processDeleteOrganization(eventData);
                  return new JSONObject();
              } else if (eventType.equals("CHECK_URL")) {
                  return processCheckUrl();
              } else {
                  throw new RuntimeException("the event type is not supported");
              }
          }
      
          private JSONObject processCreateUser(JSONObject eventData) {
              //TODO Business logic to be implemented by the client
              JSONObject returnData = new JSONObject(1);
              returnData.put("id", eventData.getString("username"));
              return returnData;
          }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    private JSONObject processCreateOrganization(JSONObject eventData) {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        returnData.put("id", eventData.getString("code"));
        return returnData;
    }

    private JSONObject processUpdateUser(JSONObject eventData) {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        returnData.put("id", eventData.getString("id"));
        return returnData;
    }

    private JSONObject processUpdateOrganization(JSONObject eventData) {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        returnData.put("id", eventData.getString("id"));
        return returnData;
    }

    private void processDeleteUser(JSONObject eventData) {
        //TODO Business logic to be implemented
    }

    private void processDeleteOrganization(JSONObject eventData) {
        //TODO Business logic to be implemented
    }

    private JSONObject processCheckUrl() {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        String randomStr = UUID.randomUUID().toString().replaceAll("-", "");
        returnData.put("randomStr", randomStr);
        return returnData;
    }
}



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
+ AES/GCM/NoPadding: 
```java
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.UUID;

@RequestMapping
@Controller
public class DemoController {
private static final Logger log = LoggerFactory.getLogger(com.example.demo.controller.DemoController.class);

    private static final String SEPARATOR = "&";

    private static final String SIGN_KEY = "wGt9VxV2qqLbgRrs";

    private static final String ENCRYPTION_KEY = "ZJIXSHUdo8WK7FQo";

    private static final String TOKEN = "4JVImwu3GdM3zNCE";

    private static final String HMAC_SHA256 = "HmacSHA256";

    private static final String ENCRYPT_ALGORITHM = "AES/GCM/NoPadding";

    private static final String ENCRYPT_KEY_ALGORITHM = "AES";

    private static final Charset CHARSET = StandardCharsets.UTF_8;

    @ResponseBody
    @RequestMapping({"/callback"})
    public JSONObject demo(HttpServletRequest httpRequest, @RequestBody String body) {
        try {
            // Verify authorization information
            // Get the Authorization header information
            String authorization = httpRequest.getHeader("Authorization");
            assertAuthorizationMessage(authorization);

            //Note: If it is necessary to determine whether the received event callback message is duplicated, it can be verified through the first 16 characters of the decrypted data. (i.e., the string before & is used to verify if the message is duplicated, and the string after & is the object property data)

            JSONObject messageBody = JSON.parseObject(body);

            // Verify whether the message body source is legitimate
            assertMessageSourceValidity(messageBody);

            // Parse the encrypted message body
            String encryptMessage = messageBody.getString("data");
            String plaintMessage = parseEncryptedMessage(encryptMessage);

            // Process the data
            JSONObject eventData = JSON.parseObject(plaintMessage);
            String eventType = messageBody.getString("eventType");
            JSONObject processResultData = processEventData(eventData, eventType);

            String encryptResultData = encryptResultData(processResultData);
            JSONObject result = new JSONObject();
            result.put("code", "200");
            result.put("message", "success");
            result.put("data", encryptResultData);
            return result;
        } catch (Exception e) {
            JSONObject result = new JSONObject();
            result.put("code", "400");
            String message = e.getMessage();
            result.put("message", StringUtils.isNotEmpty(message) ? message : e);
            return result;
        }
    }

    private String encryptResultData(JSONObject processResultData) {
        try {
            String dataStr = processResultData.toJSONString();
            Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
            SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
            String ivRandom = RandomStringUtils.random(24, true, true);
            byte[] iv = Base64.getDecoder().decode(ivRandom);
            cipher.init(1, secretKey, new GCMParameterSpec(128, iv));
            byte[] bytes = dataStr.getBytes(CHARSET);
            dataStr = Base64.getEncoder().encodeToString(cipher.doFinal(bytes));
            return ivRandom + dataStr;
        } catch (Exception e) {
            throw new RuntimeException("error encrypt Result Data", e);
        }
    }

    private void assertAuthorizationMessage(String authorization) {
        if (!"Bearer ".concat(TOKEN).equals(authorization)) {
            throw new RuntimeException("error authorization.");
        }
    }

    private void assertMessageSourceValidity(JSONObject messageBody) {
        try {
            String nonce = messageBody.getString("nonce");
            String timestamp = messageBody.getString("timestamp");
            String eventType = messageBody.getString("eventType");
            String encryptData = messageBody.getString("data");
            String signature = messageBody.getString("signature");

            String message = nonce + "&" + timestamp + "&" + eventType + "&" + encryptData;
            Mac mac = Mac.getInstance(HMAC_SHA256);
            SecretKeySpec secretKey = new SecretKeySpec(SIGN_KEY.getBytes(StandardCharsets.UTF_8), HMAC_SHA256);
            mac.init(secretKey);
            byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
            String cal_signature = Base64.getEncoder().encodeToString(bytes);

            if (!cal_signature.equals(signature)) {
                throw new RuntimeException("signature validation failed.");
            }
        } catch (Exception e) {
            throw new RuntimeException("error validating message signature", e);
        }
    }

    private String parseEncryptedMessage(String encryptMessage) {
        try {
            Cipher cipher = Cipher.getInstance(ENCRYPT_ALGORITHM);
            SecretKeySpec secretKey = new SecretKeySpec(ENCRYPTION_KEY.getBytes(CHARSET), ENCRYPT_KEY_ALGORITHM);
            byte[] iv = Base64.getDecoder().decode(encryptMessage.substring(0, 24));
            String encryptMsg = encryptMessage.substring(24);
            cipher.init(2, secretKey, new GCMParameterSpec(128, iv));
            byte[] bytes = cipher.doFinal(Base64.getDecoder().decode(encryptMsg));
            return new String(bytes, CHARSET);
        } catch (Exception e) {
            throw new RuntimeException("error parsing encrypted message", e);
        }
    }

    private JSONObject processEventData(JSONObject eventData, String eventType) {
        if (eventType.equals("CREATE_USER")) {
            return processCreateUser(eventData);
        } else if (eventType.equals("CREATE_ORGANIZATION")) {
            return processCreateOrganization(eventData);
        } else if (eventType.equals("UPDATE_USER")) {
            return processUpdateUser(eventData);
        } else if (eventType.equals("UPDATE_ORGANIZATION")) {
            return processUpdateOrganization(eventData);
        } else if (eventType.equals("DELETE_ORGANIZATION")) {
            // No data to return
            processDeleteUser(eventData);
            return new JSONObject();
        } else if (eventType.equals("DELETE_USER")) {
            // No data to return
            processDeleteOrganization(eventData);
            return new JSONObject();
        } else if (eventType.equals("CHECK_URL")) {
            return processCheckUrl();
        } else {
            throw new RuntimeException("the event type is not supported");
        }
    }

    private JSONObject processCreateUser(JSONObject eventData) {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        returnData.put("id", eventData.getString("username"));
        return returnData;
    }

    private JSONObject processCreateOrganization(JSONObject eventData) {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        returnData.put("id", eventData.getString("code"));
        return returnData;
    }

    private JSONObject processUpdateUser(JSONObject eventData) {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        returnData.put("id", eventData.getString("id"));
        return returnData;
    }

    private JSONObject processUpdateOrganization(JSONObject eventData) {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        returnData.put("id", eventData.getString("id"));
        return returnData;
    }

    private void processDeleteUser(JSONObject eventData) {
        //TODO Business logic to be implemented
    }

    private void processDeleteOrganization(JSONObject eventData) {
        //TODO Business logic to be implemented
    }

    private JSONObject processCheckUrl() {
        //TODO Business logic to be implemented
        JSONObject returnData = new JSONObject(1);
        String randomStr = UUID.randomUUID().toString().replaceAll("-", "");
        returnData.put("randomStr", randomStr);
        return returnData;
    }
}



```