Mobile ID / RSSP — PIN · OTP · SAD

Sơ đồ bảng DB · code sinh/verify (trích từ mã nguồn) · luồng API. Code là tái dựng trung thực từ bytecode; tên SP_REST_*, tham số, thuật toán, tên bảng/thuộc tính là hằng trích trực tiếp. Phần so khớp/đếm/khóa nằm trong stored procedure MySQL (⚠️).

1 · Sơ đồ liên kết bảng DB

erDiagram CLOUD_CERTIFICATE_OWNER ||--o{ CLOUD_CERTIFICATE : "1 owner - N credential" CLOUD_CERTIFICATE_OWNER ||--o{ CLOUD_CERTIFICATE_OWNER_ATTR : "OTP OWNER (login/2FA)" CLOUD_CERTIFICATE_OWNER ||--o{ CLOUD_AGREEMENT : "cloudCertificateOwnerID" RELYING_PARTY ||--o{ CLOUD_AGREEMENT : "relyingPartyID" CLOUD_CERTIFICATE ||--o{ CLOUD_AGREEMENT : "cloudCertificateID" RELYING_PARTY ||--o{ CLOUD_CERTIFICATE : "createdRelyingPartyID" CLOUD_CERTIFICATE ||--o{ CLOUD_CERTIFICATE_ATTR : "PIN=3 - OTP=22 - counters" CLOUD_CERTIFICATE_ATTR_TYPE ||--o{ CLOUD_CERTIFICATE_ATTR : "ATTR_TYPE_ID - ten" CLOUD_CERTIFICATE ||--o{ SAD_MANAGER : "cloudCertificateID" RELYING_PARTY ||--o{ SAD_MANAGER : "relyingPartyID" CLOUD_LOG ||--o{ SAD_MANAGER : "cloudLogID" CLOUD_CERTIFICATE_OWNER { long OWNER_ID PK string USERNAME int TWO_FACTOR_METHOD_ID } RELYING_PARTY { int RELYING_PARTY_ID PK string NAME } CLOUD_AGREEMENT { long CLOUD_AGREEMENT_ID PK int CLOUD_CERTIFICATE_OWNER_ID FK int RELYING_PARTY_ID FK int CLOUD_CERTIFICATE_ID FK } CLOUD_CERTIFICATE { long CLOUD_CERTIFICATE_ID PK long CLOUD_CERTIFICATE_OWNER_ID FK int CREATED_RELYING_PARTY_ID FK int AUTH_MODE_ID int SCAL } CLOUD_CERTIFICATE_ATTR_TYPE { int ID PK string NAME } CLOUD_CERTIFICATE_ATTR { long CLOUD_CERTIFICATE_ID FK int CLOUD_CERTIFICATE_ATTR_TYPE_ID FK string VALUE } SAD_MANAGER { long ID PK long CLOUD_CERTIFICATE_ID FK int RELYING_PARTY_ID FK long CLOUD_LOG_ID FK long CLIENT_INFO string SAD int REMAINING_COUNTER datetime EXPIRED_DT int NUM_SIGNATURE string HASH_TOBE_SIGN_PROPERTIES }
PIN & OTP = các dòng trong CLOUD_CERTIFICATE_ATTR (phân biệt bằng ATTR_TYPE_ID: PASSCODE=3, OTP_PASSCODE=22), lưu thô.  SAD = bảng riêng SAD_MANAGER, cột SAD lưu bản băm.  OTP của owner (login) khác PIN/OTP của credential — ở CLOUD_CERTIFICATE_OWNER_ATTR. Nối nhau qua CLOUD_CERTIFICATE_ID; CLOUD_AGREEMENT nối owner↔RP↔cert.

2 · PIN & OTP — sinh & verify

2.1 Bộ sinh dùng chung — Utils.generateOneTimePassword

public static String generateOneTimePassword(int len) {
    String digits = "0123456789";
    Random r = new Random();                    // ⚠️ java.util.Random (không phải SecureRandom)
    char[] buf = new char[len];
    for (int i = 0; i < len; i++)
        buf[i] = digits.charAt(r.nextInt(digits.length()));
    return new String(buf);                     // chuỗi SỐ
}

2.2 Sinh PIN — một lần, lúc phát hành (credentials/issue → eSignCloud)

// vn.mobileid.esigncloud.kernel.PrepareCertificateForSignCloud (chỉ với authMode = EXPLICIT_PIN)
int len = getPasscodeLength();                              // cấu hình LENGHT_OF_OTP_PASSCODE
String pin = Utils.generateOneTimePassword(len);           // sinh PIN
database.updateCloudCertificateAttributes(
        cloudCertificateID, CloudCertificateAttributeType.PASSCODE.id, pin, null, modifiedBy);  // lưu attr PASSCODE(3)
EmailReq req = new EmailReq(smtpProps, ownerEmail, subject, bodyChuaPin);   // EMAIL PIN + CTS
sendEndpoint(req);
PIN là mật khẩu tĩnh: phát một lần, đổi qua changePassphrase/resetPassphrase.

2.3 Sinh OTP — mỗi phiên ký (credentials/sendOTP)

// vn.mobileid.rssp.restful.api.proccess.API_SendOTP
int len = rsspProps.getOtpPassCodeLength(resource);
String otp = Utils.generateOneTimePassword(len);           // sinh OTP mỗi lần

Attribute attr = new Attribute(cloudCertificateID, CloudCertificateAttributeType.OTP_PASSCODE.id);
attr.setValue(otp);                                        // ⚠️ lưu PLAINTEXT, KHÔNG băm — attr OTP_PASSCODE(22)
resource.updateCloudCertificateAttribute(attr);

String body = Utils.makeNotificationMessage(template, otp, rsspProps.getOtpPassCodeTimeOutDuration());
EmailReq req = new EmailReq(rp.getSmtpProperties(), cert.getAuthorisationEmail(), subject, body);
API_Utils.sendEndpointRequest(resource, req, endpointUuid, EndpointFunction.SEND_EMAIL);  // hoặc SMS
OTP là mã động, có hạn (otpPassCodeTimeOutDuration) — sinh lại mỗi lần ký.

2.4 Verify PIN/OTP — trong credentials/authorize (API_Authorize.verify)

AuthMode mode = AuthMode.valueOf(cloudCertificate.getAuthModeID());
switch (mode) {
    case EXPLICIT_PIN: {
        String code = ((RequestAuthorize) request).getAuthorizeCode();   // PIN client nộp (field authorizeCode)
        DatabaseResponse dbResp = resource.authorizePassphrase(cloudCertificateID, code, modifiedBy);
        if (dbResp.getResponseCode() != ResponseCodeType.SUCCESS.code) {
            // PASSPHRASE_INVALID  -> setRemainingCounter + throw "Passphrase is invalid"
            // BLOCKED             -> setTempLockoutDuration(getOtpPassCodeUnblockDuration) + throw "...has been blocked..."
            // NOT_AVAILABLE       -> throw "The passphrase ... is not avaialble ..."
        }
        break;
    }
    case EXPLICIT_OTP_SMS: case EXPLICIT_OTP_EMAIL:
        // resource.authorizeOtpPassCode(...) -> "OTP is invalid" / "OTP was expired" / blocked
        break;
    case IMPLICIT_TSE:
        API_Utils.pushNotification(...);                     // đẩy app, chờ xác nhận
        return ResponseCodeType.REQUEST_WAITING_USER_CONFIRM;
}

2.5 Verify PIN — gọi stored procedure (nơi so khớp thật)

// vn.mobileid.rssp.database.DatabaseResourceStorage
public DatabaseResponse authorizePassphrase(long cloudCertificateID, String passphrase,
                                            int maxCounter, int modifiedBy) {
    CallableStatement cs = conn("SQL_WRITE_ONLY_POOL")
        .prepareCall("{ call SP_REST_CLOUD_CERTIFICATE_CHECK_PASSCODE(?,?,?,?,?,?) }");
    cs.setLong  (1, cloudCertificateID);
    cs.setString(2, passphrase);                // PIN thô
    cs.setInt   (3, maxCounter);
    cs.setInt   (4, modifiedBy);
    cs.registerOutParameter(5, ...);            // pRESPONSE_CODE_NAME
    cs.registerOutParameter(6, ...);            // pREMAINING_COUNTER
    cs.execute();
    res.setResponseCode(Integer.parseInt(cs.getString("pRESPONSE_CODE_NAME")));
    res.setRemainingCounter(cs.getInt("pREMAINING_COUNTER"));
    return res;                                 // ⚠️ so khớp + đếm + khóa nằm TRONG SP
}

Verify OTP tương tự: { call SP_REST_CLOUD_CERTIFICATE_CHECK_OTP_PASSCODE(?,?,?,?,?,?,?) } — IN: pCLOUD_CERTIFICATE_ID, pOTP_PASSCODE, pMAX_COUNTER_FOR_PASSCODE, pTIMEOUT_DURATION_PASSCODE, pMODIFIED_BY; OUT: pREMAINING_COUNTER, pRESPONSE_CODE_NAME.

3 · SAD — sinh & xử lý (verify)

3.1 Sinh token gốc của SAD — Utils.randomToken

// vn.mobileid.rssp.common.utils.Utils — đây là "token ban đầu" của SAD
public static String randomToken(int len) {
    String A = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+/";
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < len; i++)
        sb.append(A.charAt((int)(Math.random() * A.length())));   // ⚠️ Math.random() (không phải SecureRandom)
    return sb.toString();
}
// Đây chính là chuỗi TRẢ VỀ CLIENT trong ResponseAuthorize.SAD (vd "y7B0UAgXMnPfi+Cg...").
// DB KHÔNG lưu chuỗi này — chỉ lưu bản băm của nó (mục 3.4).

3.2 Công thức băm SAD — RSSPUtils.hashSAD

public static String hashSAD(long clientInfo, int relyingPartyID, long cloudCertificateID, String sad) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    baos.write(Long.toHexString(clientInfo).getBytes());            // hex(clientInfo) = RequesterInfo.hashCode()
    baos.write(Long.toHexString((long) relyingPartyID).getBytes()); // hex(relyingPartyID)
    baos.write(Long.toHexString(cloudCertificateID).getBytes());    // hex(cloudCertificateID)
    baos.write(sad.getBytes());                                     // token gốc (từ mục 3.1)
    byte[] digest = Crypto.hashData(baos.toByteArray(), "SHA-256");
    return Utils.base64Encode(digest);
}
// SAD_luuDB = Base64( SHA-256( hex(clientInfo) ‖ hex(relyingPartyID) ‖ hex(cloudCertificateID) ‖ token ) )

3.3 Cấp SAD — trong credentials/authorize (sau khi verify PIN/OTP)

// khung processRequest() ở API_ExtendTransaction, dùng lại cho API_Authorize
this.preVerify(request);
ResponseCodeType rc = this.verify(request, responseUnauthorized);   // authorize -> check PIN/OTP (mục 2.4)
if (rc == ResponseCodeType.SUCCESS) {
    String token = Utils.randomToken(len);                          // TOKEN GỐC (mục 3.1)
    API_Utils.checkAndStorageSad(resource, relyingParty, cert, requesterInfo,
                                 cloudLogID, numSignature, token, hashTobeSignProperties);  // lưu SAD (băm)
    ((ResponseAuthorize) response).setSAD(token);                   // TRẢ TOKEN GỐC cho client
    ((ResponseAuthorize) response).setExpiresIn(rsspProps.getSadExpiresIn());
}

3.4 Lưu SAD (băm) xuống DB — insertSADManagerByID

CallableStatement cs = conn.prepareCall("{ call SP_REST_SAD_MANAGER_INSERT(?,?,?,?,?,?,?,?,?,?,?) }");
cs.setLong  (1,  sm.getRelyingPartyID());
cs.setLong  (2,  sm.getCloudCertificateID());
cs.setLong  (3,  sm.getClientInfo());               // = RequesterInfo.hashCode()
cs.setString(4,  sm.getClientInfoValue()...);       // RequesterInfo (JSON)
cs.setLong  (5,  sm.getCloudLogID());
cs.setString(6,  RSSPUtils.hashSAD(sm));            // ★ CỘT SAD = BĂM (không lưu token gốc)
cs.setInt   (7,  sm.getRemainingCounter());         // = sadMaxCounter
cs.setTimestamp(8, sm.getExpiredDt());              // = now + sadExpiresIn
cs.setInt   (9,  sm.getNumSignature());
cs.setString(10, sm.getHashTobeSignProperties()...);// DTBS/R (JSON) — ràng buộc SCAL2
cs.setInt   (11, modifiedBy);

3.5 Verify SAD — trong signatures/signHash (authorizeSAD)

public DatabaseResponse authorizeSAD(long cloudCertificateID, long clientInfo, int relyingPartyID,
                                     String token, int maxCounter, int modifiedBy) {
    String pSad = RSSPUtils.hashSAD(clientInfo, relyingPartyID, cloudCertificateID, token);  // băm lại token client nộp
    CallableStatement cs = conn("SQL_WRITE_ONLY_POOL")
        .prepareCall("{ call SP_REST_CLOUD_CERTIFICATE_CHECK_SAD(?,?,?,?,?,?,?,?,?,?) }");
    cs.setLong  ("pCLOUD_CERTIFICATE_ID", cloudCertificateID);
    cs.setLong  (2, clientInfo);
    cs.setLong  (3, relyingPartyID);
    cs.setString("pSAD", pSad);                     // ★ đưa HASH cho SP (không phải token gốc)
    cs.setInt   ("pMAX_COUNTER_FOR_SAD", maxCounter);
    cs.setInt   ("pMODIFIED_BY", modifiedBy);
    cs.registerOutParameter("pSAD_REMAINING_COUNTER", ...);
    cs.registerOutParameter("pRESPONSE_CODE_NAME", ...);
    cs.registerOutParameter("pNUM_SIGNATURE", ...);
    cs.registerOutParameter("pHASH_TOBE_SIGN_PROPERTIES", ...);   // DTBS/R đã ghim lúc authorize
    cs.execute();
    // đọc responseCode, remaining, numSignature, hashTobeSign ... (⚠️ so bằng+đếm+hết hạn trong SP)
}
// sau đó API_SignHash ràng buộc tài liệu (SCAL2):
if (!Utils.checkByteArrayTwoEquals(hashTobeSign.getHash(), request.getDocumentDigests().getHashes()))
    throw new RSSP_Exception(..., "Hash array in credentials/authorize it not matched with Hash array in signatures/signHash");
// hợp lệ -> HsmCryptoFactory.create(...).restore(...).sign(...)  -> ký ở HSM
Client cầm token gốc (mục 3.1); DB chỉ giữ bản băm (salt = clientInfo + relyingPartyID + cloudCertificateID). Lộ DB không dựng lại được vé, và salt trói SAD vào đúng máy + RP + khóa. ⚠️ Token gốc sinh bằng Math.random() — nên đổi SecureRandom khi tự xây.

4 · Luồng API liên quan

4.1 Cấp CTS + sinh PIN (một lần)

owner/create → agreements/assign → credentials/issue
   └─ API_CertificateIssue → API_Utils.enrollCertificate
        └─ [eSignCloud] PrepareCertificateForSignCloud
             → generateOneTimePassword (PIN) → lưu attr PASSCODE(3) → EMAIL PIN + CTS

4.2 Ký SCAL1 — EXPLICIT_PIN

credentials/authorize  { credentialID, numSignatures, authorizeCode = PIN, [documentDigests] }
   → API_Authorize.verify: authorizePassphrase → SP_REST_CLOUD_CERTIFICATE_CHECK_PASSCODE   (verify PIN)
   → processRequest: randomToken → checkAndStorageSad(hashSAD → SAD_MANAGER_INSERT) → setSAD(token)
   ⇐ { SAD = token gốc, expiresIn }
signatures/signHash    { credentialID, SAD, documentDigests, signAlgo }
   → authorizeSAD: hashSAD(token) → SP_REST_CLOUD_CERTIFICATE_CHECK_SAD   (verify SAD)
   → checkByteArrayTwoEquals (bind hash) → HSM RsspKey.sign
   ⇐ { signatures[], remainingSigningCounter }

4.3 Ký SCAL1 — EXPLICIT_OTP_SMS / OTP_EMAIL

credentials/sendOTP   { credentialID }
   → API_SendOTP: generateOneTimePassword (OTP) → lưu attr OTP_PASSCODE(22) → gửi SMS/email
credentials/authorize { credentialID, numSignatures, authorizeCode = OTP, [documentDigests] }
   → API_Authorize.verify: authorizeOtpPassCode → SP_REST_CLOUD_CERTIFICATE_CHECK_OTP_PASSCODE
   → (giống 4.2) cấp SAD
signatures/signHash   → (giống 4.2)

4.4 Quản lý PIN

credentials/changePassphrase { oldPassphrase, newPassphrase }      → SP_REST_CLOUD_CERTIFICATE_CHANGE_PASSCODE
credentials/resetPassphrase  { requestType=request→gửi OTP; confirm→OTP+PIN mới }  → SP ..._RESET_PASSCODE
credentials/verifyPassphrase { authorizeCode = PIN }               → authorizePassphrase (chỉ kiểm, không ký)

5 · Sơ đồ xử lý tổng quan từng API

Actor: Client (SCA) · SSA (app RSSP) · DB (stored procedure MySQL) · HSM. Khối tô xanh = chỗ client nhận token gốc.

5.1 credentials/authorize — EXPLICIT_PIN (nhận SAD token gốc)

sequenceDiagram autonumber participant C as Client (SCA) participant A as SSA (API_Authorize) participant DB as MySQL (Stored Proc) participant H as HSM C->>A: credentials/authorize (credentialID, numSignatures, authorizeCode = PIN, hashes) A->>DB: authorizePassphrase → SP CHECK_PASSCODE (PIN thô) DB-->>A: responseCode + remainingCounter Note over A: Sai PIN → trả lỗi "Passphrase is invalid" (+ remaining / lockout) A->>A: token = randomToken() [TOKEN GỐC] A->>DB: checkAndStorageSad → SP SAD_MANAGER_INSERT (cột SAD = hashSAD(token) = BĂM) A->>H: RsspKey.authorize → NodeHSMProperties rect rgb(224, 244, 233) A-->>C: 200 — SAD = TOKEN GỐC (client nhận token ban đầu) + expiresIn end Note over C,DB: Client GIỮ token gốc · DB chỉ giữ bản BĂM

5.2 credentials/authorize — EXPLICIT_OTP (SMS/EMAIL)

sequenceDiagram autonumber participant C as Client (SCA) participant A as SSA (API) participant DB as MySQL (Stored Proc) participant G as SMS/Email GW C->>A: credentials/sendOTP (credentialID) A->>A: otp = generateOneTimePassword() A->>DB: updateCloudCertificateAttribute (OTP_PASSCODE = otp, PLAINTEXT) A->>G: gửi OTP (SMS/email) A-->>C: 200 — OTP đã gửi tới SĐT/email C->>A: credentials/authorize (authorizeCode = OTP, hashes) A->>DB: authorizeOtpPassCode → SP CHECK_OTP_PASSCODE DB-->>A: responseCode + remainingCounter A->>A: token = randomToken() [TOKEN GỐC] A->>DB: checkAndStorageSad → SAD_MANAGER_INSERT (SAD = BĂM) rect rgb(224, 244, 233) A-->>C: 200 — SAD = TOKEN GỐC + expiresIn end

5.3 credentials/authorize — IMPLICIT_TSE (blocking + push app)

sequenceDiagram autonumber participant C as Client (SCA) participant A as SSA (API) participant P as Push (Firebase) participant M as App EasyCA (KAK) participant DB as MySQL C->>A: credentials/authorize (SCAL2, hashes, MobileDisplayTemplate) [gọi CHẶN] A->>P: pushNotification → đẩy giao dịch xuống app P->>M: thông báo ký M->>A: confirmTransaction (KAK xác nhận trên thiết bị) A->>A: token = randomToken() [TOKEN GỐC] A->>DB: checkAndStorageSad → SAD_MANAGER_INSERT (SAD = BĂM) rect rgb(224, 244, 233) A-->>C: 200 — SAD = TOKEN GỐC + expiresIn (sau khi user xác nhận) end Note over C,A: Không poll — request bị giữ tới khi app xác nhận hoặc timeout

5.4 signatures/signHash — dùng lại token gốc để ký

sequenceDiagram autonumber participant C as Client (SCA) participant A as SSA (API_SignHash) participant DB as MySQL (Stored Proc) participant H as HSM C->>A: signatures/signHash (credentialID, SAD = TOKEN GỐC, hashes, signAlgo) A->>A: pSad = hashSAD(token) [băm lại token client nộp] A->>DB: authorizeSAD → SP CHECK_SAD (so pSad == cột SAD) DB-->>A: responseCode + numSignature + HASH_TOBE_SIGN + remaining Note over A: so hashes request == hash đã ghim (checkByteArrayTwoEquals) — bind DTBS/R A->>H: RsspKey.sign(hash) H-->>A: signature[] A-->>C: 200 — signatures[] + remainingSigningCounter

5.5 credentials/issue — phát hành CTS + sinh PIN

sequenceDiagram autonumber participant C as Client (RA) participant A as SSA (API_CertificateIssue) participant E as eSignCloud (PrepareCertificate) participant RA as RA/CA (EJBCA/TMS-RA) participant H as HSM participant DB as MySQL participant G as Email GW C->>A: credentials/issue (user, certificateProfile, authMode = EXPLICIT_PIN, ...) A->>H: sinh khóa (genRSA/ECDSA) + CSR A->>RA: enrollCertificate → phát hành CTS A->>E: PrepareCertificateForSignCloud E->>E: pin = generateOneTimePassword() E->>DB: updateCloudCertificateAttributes (PASSCODE = pin) E->>G: EMAIL: PIN + thông tin CTS A-->>C: 200 — credentialID + certificates

6 · SAD — tổng quan (pseudo-code)

Sinh SAD — credentials/authorize (sau khi verify PIN/OTP OK)

// randomToken() — sinh token gốc (Utils.randomToken)
String randomToken(int len) {
    String A = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._+/";
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < len; i++)
        sb.append(A.charAt((int)(Math.random() * A.length())));   // ⚠️ Math.random()
    return sb.toString();
}
token = randomToken()                                          // token gốc, ngẫu nhiên -> TRẢ CHO CLIENT
sad_hash = SHA256( hex(clientInfo) + hex(rpID) + hex(certID) + token )   // băm có salt

DB.insert(SAD_MANAGER) {
    SAD          = sad_hash        // ★ chỉ lưu BĂM (không lưu token gốc)
    EXPIRED_DT   = now + sadExpiresIn      // ~300s
    REMAINING    = sadMaxCounter
    NUM_SIGNATURE, HASH_TOBE_SIGN          // hash tài liệu (SCAL2)
}
return { SAD: token }              // client nhận TOKEN GỐC

Verify SAD — signatures/signHash

token = request.SAD                                            // client gửi lại token gốc
sad_hash = SHA256( hex(clientInfo) + hex(rpID) + hex(certID) + token )   // băm LẠI y hệt

row = DB.check(SAD_MANAGER, sad_hash)     // SP_CHECK_SAD: so bằng + hạn + lượt + khóa
if !row.ok            -> throw  invalid / expired / blocked / not_exist
if SCAL2 && row.HASH_TOBE_SIGN != request.hashes -> throw "hash not matched"

HSM.sign(request.hashes)                   // hợp lệ -> ký
row.REMAINING--                            // giảm lượt
Client giữ token gốc; DB chỉ giữ bản băm (salt = clientInfo + rpID + certID) → trói vé vào đúng máy + RP + khóa. ⚠️ So khớp/đếm/khóa chạy trong stored procedure; token gốc nên đổi sang SecureRandom.

Phụ lục — nơi đọc trong mã

Chủ đềJAR → class → method
Sinh PIN/OTPRSSP_Restful_CommonUtils.generateOneTimePassword; PIN: eSignCloudPrepareCertificateForSignCloud; OTP: RSSP_Restful_APIAPI_SendOTP
Sinh token SADRSSP_Restful_CommonUtils.randomToken
Verify PIN/OTPRSSP_Restful_APIv2.API_Authorize.verify; RSSP_WebApp_own_classesDatabaseResourceStorage.authorizePassphrase / authorizeOtpPassCode
Sinh/băm SADRSSP_Restful_APIv2.API_ExtendTransaction.processRequest + API_Utils.checkAndStorageSad; RSSP_Restful_CoreRSSPUtils.hashSAD
Lưu/Verify SADRSSP_WebApp_own_classesDatabaseResourceStorage.insertSADManagerByID / authorizeSAD; RSSP_Restful_APIv2.API_SignHash
⚠️ Mọi { call SP_... } là hết phần Java; logic so khớp/đếm/khóa ở stored procedure MySQL — cần dump SP để xác nhận 100%.