Skip to content

vtrc-mobile — Scorecard

Legend:

  • Critical = active security / data-loss / availability risk
  • High = correctness / maintainability hazard with real impact
  • Medium = friction / hygiene — ยังไม่เป็นอันตรายทันที

vtrc-mobile (branch: master) เป็นแอป production จริง (versionName 1.9.12, versionCode 139 — android/app/build.gradle:154-155) มีฟีเจอร์กว้างที่สุดในกลุ่ม Mobile แต่สะสมหนี้ด้าน secret-in-bundle, host drift, และ stack EOL


สรุปคะแนนภาพรวม

มิติระดับเหตุผลสั้น
SecurityCriticalRSA private PEM + keystore passwords + apiKey + biometric private key ใน AsyncStorage
CorrectnessHighMeeting API ค้าง UAT; HostApi path drift; HOSTAPIDEV import พัง
MaintainabilityHighRN 0.63 / React 16 / router-flux EOL; App.js 1071 บรรทัด flat scenes
ObservabilityMediumไม่มี Crashlytics/Sentry; catch หลายจุดเงียบ
TestMediumมีแค่ template __tests__/App-test.js

Debt table

IDSeverityคำอธิบายหลักฐานการแก้ไขที่เสนอ
SEC-MOBILE-01CriticalRSA private key (PEM เต็ม) hardcode ใน JS bundle ที่ใช้สร้าง csId — แตก APK/IPA ถอดกุญแจได้; คีย์ชุดเดียวถูก copy ไป new-vtrc-mobilevtrc-mobile/src/utils/login.js:17-42ย้าย keypair ไป server เท่านั้น; client ถือแค่ public ที่ rotate ได้ผ่าน API; rotate คีย์ที่หลุดแล้วทั้งหมด
SEC-MOBILE-02CriticalAndroid release signingConfigs ใส่ path เครื่อง dev (/Users/bosspj/…/I3Keystore) + storePassword / keyPassword / keyAlias เป็น plain text ใน gitvtrc-mobile/android/app/build.gradle:174-179ย้ายไป CI secret / keystore.properties ที่ gitignore; วางแผน rotate signing key ร่วมทีม release (กระทบ update path ของแอปที่ publish แล้ว)
SEC-MOBILE-03HighProduction device apiKey hardcode ซ้ำ 3 จุด พร้อม UAT key ใน comment คู่กัน — ใครแตก bundle เห็นทั้งสอง envsrc/utils/fetch.js:43-45, src/constants/Authen.js:1-5, src/utils/login.js:49-52inject จาก native BuildConfig / XCConfig ตอน CI; อย่าใส่ JS string; rotate key ที่หลุด
SEC-MOBILE-04HighBiometric RSA private key เก็บใน AsyncStorage key Encode (ไม่ใช่ keychain) หลัง RSA.generateKeys ใน Settingsrc/containers/Setting.js:36-43, src/utils/setting.js:90-105, ใช้ถอดใน authSlice.js:960-967เก็บ private material ใน sensitive-info / Keystore เท่านั้น; พิจารณาใช้ biometrics-bound key ของ react-native-biometrics เป็นหลักแทน PEM ใน JS
SEC-MOBILE-05HighRECRUITMENT_KEY hardcode ในซอร์สsrc/constants/PrivateKey.js:1ย้ายไป build-time secret; rotate
SEC-MOBILE-06MediumDev mode ปิด SSL pinning ทั้งก้อน (disableAllSecurity: Config.isDevMode) — ถ้าระวัง NODE_ENV ผิดตอน bundle จะปิด pin ใน build ที่คิดว่าเป็น prodsrc/utils/fetch.js:33, src/config/index.js:14ผูก pinning กับ build flavor ชัดเจน ไม่ใช่แค่ NODE_ENV; ตรวจ release bundle ว่า pin ยังทำงาน
LEG-MOBILE-01HighReact Native 0.63.2 + React 16.13.1 — เก่ากว่าหลายปี ไม่ได้ security patch จาก upstreampackage.json:63-64phase upgrade (0.63 → 0.71 LTS ก่อน) สอดคล้อง roadmap new-vtrc-mobile
LEG-MOBILE-02Highไม่มี runtime endpoint resolver; host ประกาศซ้ำ 3 ไฟล์ที่ path ไม่ตรง (Config.API = /api/graphql, HOSTGRAHQL = /api, hostUrl.HOST = origin อย่างเดียว)src/config/index.js:1-15, src/constants/HostApi.js:1-38, src/utils/hostUrl.js:1-6รวมเป็น single config + Android productFlavor / iOS scheme; เลิก comment-switch
LEG-MOBILE-03Mediumreact-native-router-flux + imperative Actions.* — library ไม่มีทิศทางใหม่; typed routes/deep-link ทดสอบยาก; ผูก scene ทั้งแอปในไฟล์เดียวpackage.json:99, App.js:227-1062migrate ทีละ cluster ไป @react-navigation (แบบที่ new-vtrc-mobile ใช้)
LEG-MOBILE-04MediumHermes ปิด; เสียโอกาส perf/bytecode ของ RN รุ่นใหม่android/app/build.gradle:81-82เปิดหลัง upgrade RN ที่รองรับ Hermes นิ่ง
CORR-MOBILE-01HighConfig.MEETING_API hardcode UAT ไม่มี branch prod — meeting-management ทั้งก้อน + redirectByPass หลัง login ในแอป production ยิง UATsrc/config/index.js:12, src/utils/axios.js:4-5, src/services/meetingService.js:327-338เพิ่มค่า prod และสลับคู่กับ Config.API; ตรวจว่า UAT meeting มีข้อมูล/สิทธิ์รั่วข้าม env หรือไม่
CORR-MOBILE-02HighfetchRestFull.js import HOSTAPIDEV จาก HostApi แต่ HostApi.js ไม่ได้ประกาศ/export สัญลักษณ์นี้src/utils/fetchRestFull.js:3, src/constants/HostApi.js:38ลบ import ที่ไม่ใช้ หรือเพิ่ม export ให้ครบ; กัน Metro/runtime error ตาม path ที่ถูกโหลด
CORR-MOBILE-03MediumScene key ซ้ำ: listallleave สองครั้ง (App.js:609-615); delegete สองครั้ง (App.js:664-670) — พฤติกรรม router-flux ขึ้นกับตัวหลังทับApp.js:609-615, 664-670ลบประกาศซ้ำ; rename typo keys พร้อมอัปเดต deep-link
CORR-MOBILE-04MediumqueryProfileData dispatch bankAccount สองครั้งติดกันsrc/features/authSlice.js:777,780ลบ duplicate เพื่อลดโหลดหลัง login
CORR-MOBILE-05MediumcreateAuthSession เรียก ReactNativeBiometrics.createKeys แล้วไม่ใช้ publicKey จากผลลัพธ์ — keypair จริงถูกสร้างด้วย RSA.generateKeys ใน Setting ส่งขึ้น server คนละชุดกับ biometrics keysauthSlice.js:854-857, Setting.js:36-43รวมเป็นหนึ่งโมเดลกุญแจ; เอกสาร/ลบ dead call
QUAL-MOBILE-01MediumTypo ฝังชื่อไฟล์/scene: Merdical, witchdraw, Cancle, delegete, DEATIL ใน query namecontainers Merdical*, components/witchdraw/, App.js scene keys, gql/query/withdraw.js study leave DEATILยอมรับสั้น ๆ หรือ rename พร้อม notification payload + deep-link matrix
QUAL-MOBILE-02MediumAutomated test มีไฟล์เดียว — RN template ที่ยัง renderer.create(<App />)__tests__/App-test.js:1-14เริ่ม unit จาก buildQuery / isSuccess / pure utils แล้วขยาย reducer tests
QUAL-MOBILE-03Mediumชื่อ package/app rn_redcross ไม่ตรง branding / repo namepackage.json:2, app.json:1-4, ios/Podfile:6rename ระวัง native module + store listing
QUAL-MOBILE-04MediumApp.js 1071 บรรทัด รวม import + Scene flat ทั้งแอป — ทุกการเพิ่มหน้าแก้ไฟล์เดียวApp.jsแยก scene map ตาม cluster; ลด circular import risk
QUAL-MOBILE-05Mediumactions/*Slice.js ชื่อลงท้าย Slice แต่ไม่ใช่ createSlice; leave อยู่ใน withdrawSlicesrc/actions/ssoSlice.js, src/features/withdrawSlice.jsเปลี่ยนชื่อไฟล์/slice ให้สื่อความหมาย
QUAL-MOBILE-06MediumScript ชื่อ apk-ios / aab-ios รัน Gradle Android — สับสน operationalpackage.json:14-15เปลี่ยนชื่อ script ให้ตรงงาน
OBS-MOBILE-01Mediumไม่มี crash reporting SDK (ไม่มี Sentry/Crashlytics ใน dependencies) ทั้งที่มี @react-native-firebase/apppackage.json:29-30เพิ่ม @react-native-firebase/crashlytics; ส่ง error สำคัญผ่าน LOG_ERROR mutation ที่มีอยู่ (authSlice.js:1064+, gql/mutation/auth.js:147)
OBS-MOBILE-02Mediumcatch ใน login/refresh/logout หลายจุดว่างหรือแค่ console.log — ไม่มี telemetryauthSlice.js:423-425, 613-622, 452-455, meetingService.js:335-337รวม error boundary + remote log

SEC-MOBILE-01 — รายละเอียด RSA ใน bundle

ของที่อยู่ใน client

ไฟล์ src/utils/login.js มีทั้ง public และ private PEM ใน object RSA_KEY (login.js:6-43)

การใช้จริงของ client:

javascript
crypto.publicEncrypt(RSA_KEY.public, Buffer.from(JSON.stringify(data)))

เพื่อสร้าง csId (login.js:61-67)

Private key มี comment ทดสอบ decrypt ที่ปิดไว้ (login.js:68-71) — ยืนยันว่าทีมเคยถอดด้วย private ฝั่ง client ได้จริง

ทำไม Critical

  1. Private key ใน JS ถูก pack เข้า Hermes/JSC bundle → reverse ได้ด้วยเครื่องมือมาตรฐาน
  2. ถ้า server ใช้คู่กุญแจนี้ถอด csId เพื่อเชื่อว่า device ถูกต้อง การถือ private ที่ client ทำให้ผู้โจมตีสร้าง csId ที่ถูกต้องเองได้หลังถอด apiKey จากจุดอื่นใน bundle
  3. คีย์เดียวกันปรากฏใน new-vtrc-mobile (ดู scorecard sibling) → blast radius ขยายข้าม repo

สิ่งที่เอกสารนี้ไม่ทำ

ไม่ paste PEM เต็มในคู่มือ — อ้างเฉพาะ file:line และอธิบายผลกระทบ


SEC-MOBILE-02 — keystore ใน git

release {
    storeFile file('/Users/bosspj/Desktop/Project/react-native/vtrc-mobile/android/I3Keystore')
    storePassword '…'   // plain text ใน repo
    keyAlias 'VTRC'
    keyPassword '…'
}

หลักฐาน: android/app/build.gradle:174-179

ผลกระทบ:

  • ใครได้สิทธิ์อ่าน repo สร้าง APK ที่เซ็นด้วย signing key เดียวกับ production ได้ (ถ้ายังมีไฟล์ keystore อยู่ที่ path นั้นหรือถูก commit/แชร์แยก)
  • path เครื่องบุคคลทำให้ CI/เครื่องคนอื่น build release ไม่ผ่านจนกว่าจะแก้ไฟล์ — operational fragility

เอกสารนี้ไม่ทำซ้ำรหัสผ่านในตาราง — อ่านจากไฟล์ต้นทางเมื่อต้อง rotate


SEC-MOBILE-03 / 05 — device keys

Secretที่เก็บใช้ทำอะไร
apiKey prod 346155c3-…fetch.js, Authen.js, login.jsGraphQL/REST header + hash ใน csId
apiKey UAT 9d464b27-…comment คู่กันสลับด้วย uncomment
RECRUITMENT_KEYPrivateKey.jsrecruitment REST

Device key ฝั่ง vtrc-api ผูก APP_TYPE (ดู Vol 3 / canonical truths) — การหลุดของ production key จาก mobile bundle = ลูกค้าปลอมสามารถยิง GraphQL ในฐานะแอป MOBILE ได้ถ้าไม่มีชั้นป้องกันอื่น


SEC-MOBILE-04 — biometric private key ใน AsyncStorage

ลำดับ:

  1. User เปิด biometric ใน Setting.js
  2. RSA.generateKeys(2048) → base64 private → setEncode → AsyncStorage Encode
  3. Public ส่งขึ้น server ผ่าน createAuthSession
  4. ตอน login authenBiometric อ่าน getEncode() แล้ว crypto.createSign('RSA-SHA256') (authSlice.js:960-967)

AsyncStorage บน Android โดยทั่วไปไม่เท่ากับ EncryptedSharedPreferences / iOS Keychain ที่ redux-persist ใช้สำหรับ JWT — ชั้นป้องกันต่ำกว่า token session ทั้งที่ private key นี้ใช้ bypass รหัสผ่านได้


CORR-MOBILE-01 — Meeting ค้าง UAT

หลักฐานเดียวที่ชัด:

javascript
MEETING_API: 'https://uat-vtrcapi.redcross.or.th/meeting',

ไม่มีบรรทัด prod ที่ comment คู่แบบ API (config/index.js:3-12)

ผลกระทบที่พิสูจน์ได้จากโค้ด:

  • meetingAPI ทุก POST ใต้ /secure ไป UAT (axios.js:4-5)
  • หลัง login/refresh เรียก redirectByPass ไป UAT (meetingService.js:327-338, authSlice.js:409,591)
  • ไฟล์แนบ meeting ใช้ ${Config.MEETING_API}/file/…

UNVERIFIED โดยไม่ยิง network: ว่า production user เห็นข้อมูลประชุม UAT จริงในสนามหรือถูก firewall กัน — แต่ configuration ในซอร์สชี้ผิด env ชัดเจน


LEG-MOBILE-02 — ตาราง drift แบบ side-by-side

ความต้องการconfig/index.jsconstants/HostApi.jsutils/hostUrl.js
GraphQL URL เต็ม/api/graphql— (ต้องต่อ /graphql เองจาก /api)ไม่มี
Origin อย่างเดียวHOSTHOST
MeetingUAT เท่านั้น
RecruitmentHOSTRECRUITMENT
สลับ UATcomment ที่ APIcomment block ทั้งก้อนมี UAT_HOST แต่ Conicle ใช้ HOST prod

Consumer ที่จะพังถ้าสลับไม่ครบ: fetch.js + Conicle.js + ssoSlice + recruitment.js + withdraw upload


OBS / Test — ช่องว่าง

ช่องหลักฐาน
ไม่มี Sentry/Crashlytics dependencypackage.json dependencies จบที่ messaging
มี LOG_ERROR mutation แต่ไม่ถูกเรียกจากทุก catchgql/mutation/auth.js:147, authSlice.js:1064
Jest template อย่างเดียว__tests__/App-test.js:1-14
redux-logger เฉพาะ non-productionstore.js:106-108

ลำดับแก้ที่แนะนำ (ไม่ใช่ roadmap บังคับ)

  1. ทันที (secret): SEC-MOBILE-01 / 02 / 03 / 05 — rotate + เลิก commit secret
  2. สั้น: SEC-MOBILE-04 ย้าย Encode เข้า sensitive storage; CORR-MOBILE-01 ตั้ง meeting prod; CORR-MOBILE-02 แก้ import
  3. กลาง: LEG-MOBILE-02 รวม host config + flavors; OBS-MOBILE-01 crash reporting
  4. ยาว: LEG-MOBILE-01 / 03 upgrade RN + navigation ควบคู่ new-vtrc-mobile

เทียบ sibling ใน Volume 8

หัวข้อvtrc-mobilenew-vtrc-mobile
RSA private ใน clientมี (login.js)มี (copy) — ดู scorecard sibling
Feature coverage121+ scenesน้อยมาก
Navigationrouter-flux(ตรวจในเอกสาร sibling)
Meeting UAT hardcodeมี

LOGIN_MUTATION — comment archaeology (apiKey ใน body vs header)

src/gql/mutation/auth.js มีหลายเวอร์ชันของ LOGIN_MUTATION ซ้อนกัน:

บล็อกสถานะapiKey ใน mutation body?หลักฐาน
บรรทัด 3-18comment — ป้าย "API KEY Production"มี inline apiKey ใน login(…, apiKey: "346155c3-…")gql/mutation/auth.js:3-18
บรรทัด 21-36active exportไม่มี — พึ่ง header จาก fetch.jsgql/mutation/auth.js:21-36
บรรทัด 38+comment — ป้าย "API KEY UAT"มี UAT apiKey ใน bodygql/mutation/auth.js:38-50

สรุปที่พิสูจน์ได้: path ปัจจุบันส่ง apiKey ผ่าน HTTP header (fetch.js:43) ไม่ผ่าน GraphQL argument ของ mutation ที่ active — แต่ประวัติใน comment แสดงว่าเคยส่งใน body ด้วย และยังเหลือ string ของทั้ง prod/UAT key ในไฟล์ mutation เอง (เพิ่มจุดหลุดของ secret นอกเหนือจากสามจุดใน SEC-MOBILE-03)

ถ้า backend ยังรองรับทั้งสองแบบ การมี key ใน comment ของ mutation file เป็น attack surface เพิ่มเมื่อแตก bundle (comment ใน JS ยังอยู่ใน source map / บางครั้งใน bundle ถ้าไม่ถูก strip)


Biometric vs csId — ตารางเปรียบเทียบ cryptography

csId (makeCsID)Biometric bypass
Key materialPEM คู่ hardcode ใน login.jsRSA.generateKeys(2048) ตอนเปิด Setting
Private อยู่ที่JS bundle (ทุกเครื่องเหมือนกัน)AsyncStorage Encode ต่อเครื่อง
Public อยู่ที่bundle + (น่าจะ) serverส่งขึ้น server ผ่าน createAuthSession
Algorithm ฝั่ง clientcrypto.publicEncryptcrypto.createSign('RSA-SHA256')
Server mutationรวมใน login(…) ผ่าน field csIdloginBypass / LOGIN_BYPASS_MUTATION
Prompt UIไม่มีTouchID / FaceID / Biometrics (authSlice.js:968-1044)
Debt IDSEC-MOBILE-01SEC-MOBILE-04

หลักฐาน Setting: src/containers/Setting.js:34-44
หลักฐาน sign: src/features/authSlice.js:953-1062
หลักฐาน csId: src/utils/login.js:45-74


Cross-repo RSA copy (หลักฐาน sibling)

new-vtrc-mobile (branch ตาม docs sibling) มี:

  • new-vtrc-mobile/src/constants/RSAKey.ts:11-38RSA_KEY.private PEM
  • ใช้ใน EncryptCrypto.ts ทั้ง public และ private (EncryptCrypto.ts:7,18)
  • CSID.ts ใช้ public สำหรับ csId แบบเดียวกับ mobile เก่า

แปลว่าหนี้ SEC-MOBILE-01 ไม่จบด้วยการแก้ vtrc-mobile อย่างเดียว — ต้อง rotate และลบจากทั้งสอง repo พร้อมกัน ไม่งั้นคีย์ที่หลุดยังใช้ได้ผ่านแอปใหม่


Persist / purge — failure modes

Eventพฤติกรรมหลักฐาน
ติดตั้งแล้วเปิดครั้งแรก (firstRun ว่าง)persist/PURGE แล้วตั้ง firstRun=trueApp.js:195-199
TOKEN_INVALID จาก GraphQLPURGE + Actions.mainmenufetch.js:83-86
Logout สำเร็จ/ล้มเหลวPURGE + mainmenuauthSlice.js:441-454
Jailbreak (non-dev)บังคับปิดแอป — ไม่ PURGE แยกApp.js:202-208
Refresh ล้มบน cold startไป intro/decideenter — token เก่าอาจยังอยู่ใน persist จนกว่า login ใหม่จะทับauthSlice.js:601-610

PURGE handler ใน store.js:84-102 รีเซ็ตทุก slice เป็น initialState ด้วยมือ — ถ้าเพิ่ม reducer ใหม่แล้วลืมใส่ในก้อน PURGE state เก่าจะรั่วข้าม logout (QUAL แฝง — ตรวจเมื่อเพิ่ม slice)


Build / release hygiene checklist (จากซอร์ส)

ก่อนตัด release จาก branch master ควรตรวจใน repo:

  1. Config.API ชี้ env ที่ต้องการ (config/index.js:3-10)
  2. HostApi.js uncomment บล็อกเดียวกับ Config (HostApi.js:2-36)
  3. hostUrl.js HOST ตรงกับ origin เดียวกัน (hostUrl.js:5) — ไม่งั้น Conicle พังเงียบ
  4. MEETING_API — ปัจจุบันบังคับ UAT; ต้องแก้โค้ดก่อนหวัง prod meeting (config/index.js:12)
  5. apiKey ใน fetch.js / Authen.js / login.js ตรง env
  6. signingConfigs.release — path/password ไม่ควรเป็นของเครื่องบุคคล (build.gradle:174-179)
  7. SSL cert digicert2025.cer ยังไม่หมดอายุ (asset อยู่ใต้ android/app/src/main/assets/)
  8. versionName / versionCode (build.gradle:154-155)

Checklist นี้สะท้อน debt LEG-MOBILE-02 โดยตรง — ขั้นตอนที่ต้องทำมือหลายไฟล์คืออาการของ design ที่ผิด


รายการที่ตรวจสอบแล้วว่าไม่พบ (เชิงลบ)

ตรวจหาผล
.env / react-native-config สำหรับ API hostไม่พบใน path config หลัก
Sentry dependencyไม่พบใน package.json
@react-native-firebase/crashlyticsไม่พบ
Detox / Appium / Maestroไม่พบ
TypeScript source ใต้ src/ สำหรับ screen หลักไฟล์หลักเป็น .js
checkLinkURL หรือเทียบเท่าไม่พบ


Verification stamp

รายการค่า
Repo pathvtrc-mobile/
Branch ที่อ่านmaster
Tracked files1055
Docs pathdocs/volume-08-mobile/vtrc-mobile/
วันที่เขียนเนื้อหา V1-depth รอบนี้2026-07-14

ทุก claim ในชุดนี้ผูก file:line จากซอร์สบน master — ไม่ invent endpoint หรือ secret ที่ไม่มีในโค้ด


ดูเพิ่ม