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
สรุปคะแนนภาพรวม
| มิติ | ระดับ | เหตุผลสั้น |
|---|---|---|
| Security | Critical | RSA private PEM + keystore passwords + apiKey + biometric private key ใน AsyncStorage |
| Correctness | High | Meeting API ค้าง UAT; HostApi path drift; HOSTAPIDEV import พัง |
| Maintainability | High | RN 0.63 / React 16 / router-flux EOL; App.js 1071 บรรทัด flat scenes |
| Observability | Medium | ไม่มี Crashlytics/Sentry; catch หลายจุดเงียบ |
| Test | Medium | มีแค่ template __tests__/App-test.js |
Debt table
| ID | Severity | คำอธิบาย | หลักฐาน | การแก้ไขที่เสนอ |
|---|---|---|---|---|
| SEC-MOBILE-01 | Critical | RSA private key (PEM เต็ม) hardcode ใน JS bundle ที่ใช้สร้าง csId — แตก APK/IPA ถอดกุญแจได้; คีย์ชุดเดียวถูก copy ไป new-vtrc-mobile | vtrc-mobile/src/utils/login.js:17-42 | ย้าย keypair ไป server เท่านั้น; client ถือแค่ public ที่ rotate ได้ผ่าน API; rotate คีย์ที่หลุดแล้วทั้งหมด |
| SEC-MOBILE-02 | Critical | Android release signingConfigs ใส่ path เครื่อง dev (/Users/bosspj/…/I3Keystore) + storePassword / keyPassword / keyAlias เป็น plain text ใน git | vtrc-mobile/android/app/build.gradle:174-179 | ย้ายไป CI secret / keystore.properties ที่ gitignore; วางแผน rotate signing key ร่วมทีม release (กระทบ update path ของแอปที่ publish แล้ว) |
| SEC-MOBILE-03 | High | Production device apiKey hardcode ซ้ำ 3 จุด พร้อม UAT key ใน comment คู่กัน — ใครแตก bundle เห็นทั้งสอง env | src/utils/fetch.js:43-45, src/constants/Authen.js:1-5, src/utils/login.js:49-52 | inject จาก native BuildConfig / XCConfig ตอน CI; อย่าใส่ JS string; rotate key ที่หลุด |
| SEC-MOBILE-04 | High | Biometric RSA private key เก็บใน AsyncStorage key Encode (ไม่ใช่ keychain) หลัง RSA.generateKeys ใน Setting | src/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-05 | High | RECRUITMENT_KEY hardcode ในซอร์ส | src/constants/PrivateKey.js:1 | ย้ายไป build-time secret; rotate |
| SEC-MOBILE-06 | Medium | Dev mode ปิด SSL pinning ทั้งก้อน (disableAllSecurity: Config.isDevMode) — ถ้าระวัง NODE_ENV ผิดตอน bundle จะปิด pin ใน build ที่คิดว่าเป็น prod | src/utils/fetch.js:33, src/config/index.js:14 | ผูก pinning กับ build flavor ชัดเจน ไม่ใช่แค่ NODE_ENV; ตรวจ release bundle ว่า pin ยังทำงาน |
| LEG-MOBILE-01 | High | React Native 0.63.2 + React 16.13.1 — เก่ากว่าหลายปี ไม่ได้ security patch จาก upstream | package.json:63-64 | phase upgrade (0.63 → 0.71 LTS ก่อน) สอดคล้อง roadmap new-vtrc-mobile |
| LEG-MOBILE-02 | High | ไม่มี 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-03 | Medium | react-native-router-flux + imperative Actions.* — library ไม่มีทิศทางใหม่; typed routes/deep-link ทดสอบยาก; ผูก scene ทั้งแอปในไฟล์เดียว | package.json:99, App.js:227-1062 | migrate ทีละ cluster ไป @react-navigation (แบบที่ new-vtrc-mobile ใช้) |
| LEG-MOBILE-04 | Medium | Hermes ปิด; เสียโอกาส perf/bytecode ของ RN รุ่นใหม่ | android/app/build.gradle:81-82 | เปิดหลัง upgrade RN ที่รองรับ Hermes นิ่ง |
| CORR-MOBILE-01 | High | Config.MEETING_API hardcode UAT ไม่มี branch prod — meeting-management ทั้งก้อน + redirectByPass หลัง login ในแอป production ยิง UAT | src/config/index.js:12, src/utils/axios.js:4-5, src/services/meetingService.js:327-338 | เพิ่มค่า prod และสลับคู่กับ Config.API; ตรวจว่า UAT meeting มีข้อมูล/สิทธิ์รั่วข้าม env หรือไม่ |
| CORR-MOBILE-02 | High | fetchRestFull.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-03 | Medium | Scene 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-04 | Medium | queryProfileData dispatch bankAccount สองครั้งติดกัน | src/features/authSlice.js:777,780 | ลบ duplicate เพื่อลดโหลดหลัง login |
| CORR-MOBILE-05 | Medium | createAuthSession เรียก ReactNativeBiometrics.createKeys แล้วไม่ใช้ publicKey จากผลลัพธ์ — keypair จริงถูกสร้างด้วย RSA.generateKeys ใน Setting ส่งขึ้น server คนละชุดกับ biometrics keys | authSlice.js:854-857, Setting.js:36-43 | รวมเป็นหนึ่งโมเดลกุญแจ; เอกสาร/ลบ dead call |
| QUAL-MOBILE-01 | Medium | Typo ฝังชื่อไฟล์/scene: Merdical, witchdraw, Cancle, delegete, DEATIL ใน query name | containers Merdical*, components/witchdraw/, App.js scene keys, gql/query/withdraw.js study leave DEATIL | ยอมรับสั้น ๆ หรือ rename พร้อม notification payload + deep-link matrix |
| QUAL-MOBILE-02 | Medium | Automated test มีไฟล์เดียว — RN template ที่ยัง renderer.create(<App />) | __tests__/App-test.js:1-14 | เริ่ม unit จาก buildQuery / isSuccess / pure utils แล้วขยาย reducer tests |
| QUAL-MOBILE-03 | Medium | ชื่อ package/app rn_redcross ไม่ตรง branding / repo name | package.json:2, app.json:1-4, ios/Podfile:6 | rename ระวัง native module + store listing |
| QUAL-MOBILE-04 | Medium | App.js 1071 บรรทัด รวม import + Scene flat ทั้งแอป — ทุกการเพิ่มหน้าแก้ไฟล์เดียว | App.js | แยก scene map ตาม cluster; ลด circular import risk |
| QUAL-MOBILE-05 | Medium | actions/*Slice.js ชื่อลงท้าย Slice แต่ไม่ใช่ createSlice; leave อยู่ใน withdrawSlice | src/actions/ssoSlice.js, src/features/withdrawSlice.js | เปลี่ยนชื่อไฟล์/slice ให้สื่อความหมาย |
| QUAL-MOBILE-06 | Medium | Script ชื่อ apk-ios / aab-ios รัน Gradle Android — สับสน operational | package.json:14-15 | เปลี่ยนชื่อ script ให้ตรงงาน |
| OBS-MOBILE-01 | Medium | ไม่มี crash reporting SDK (ไม่มี Sentry/Crashlytics ใน dependencies) ทั้งที่มี @react-native-firebase/app | package.json:29-30 | เพิ่ม @react-native-firebase/crashlytics; ส่ง error สำคัญผ่าน LOG_ERROR mutation ที่มีอยู่ (authSlice.js:1064+, gql/mutation/auth.js:147) |
| OBS-MOBILE-02 | Medium | catch ใน login/refresh/logout หลายจุดว่างหรือแค่ console.log — ไม่มี telemetry | authSlice.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:
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
- Private key ใน JS ถูก pack เข้า Hermes/JSC bundle → reverse ได้ด้วยเครื่องมือมาตรฐาน
- ถ้า server ใช้คู่กุญแจนี้ถอด
csIdเพื่อเชื่อว่า device ถูกต้อง การถือ private ที่ client ทำให้ผู้โจมตีสร้างcsIdที่ถูกต้องเองได้หลังถอด apiKey จากจุดอื่นใน bundle - คีย์เดียวกันปรากฏใน
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.js | GraphQL/REST header + hash ใน csId |
apiKey UAT 9d464b27-… | comment คู่กัน | สลับด้วย uncomment |
RECRUITMENT_KEY | PrivateKey.js | recruitment REST |
Device key ฝั่ง vtrc-api ผูก APP_TYPE (ดู Vol 3 / canonical truths) — การหลุดของ production key จาก mobile bundle = ลูกค้าปลอมสามารถยิง GraphQL ในฐานะแอป MOBILE ได้ถ้าไม่มีชั้นป้องกันอื่น
SEC-MOBILE-04 — biometric private key ใน AsyncStorage
ลำดับ:
- User เปิด biometric ใน
Setting.js RSA.generateKeys(2048)→ base64 private →setEncode→ AsyncStorageEncode- Public ส่งขึ้น server ผ่าน
createAuthSession - ตอน 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
หลักฐานเดียวที่ชัด:
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.js | constants/HostApi.js | utils/hostUrl.js |
|---|---|---|---|
| GraphQL URL เต็ม | /api/graphql | — (ต้องต่อ /graphql เองจาก /api) | ไม่มี |
| Origin อย่างเดียว | — | HOST | HOST |
| Meeting | UAT เท่านั้น | — | — |
| Recruitment | — | HOSTRECRUITMENT | — |
| สลับ UAT | comment ที่ API | comment block ทั้งก้อน | มี UAT_HOST แต่ Conicle ใช้ HOST prod |
Consumer ที่จะพังถ้าสลับไม่ครบ: fetch.js + Conicle.js + ssoSlice + recruitment.js + withdraw upload
OBS / Test — ช่องว่าง
| ช่อง | หลักฐาน |
|---|---|
| ไม่มี Sentry/Crashlytics dependency | package.json dependencies จบที่ messaging |
มี LOG_ERROR mutation แต่ไม่ถูกเรียกจากทุก catch | gql/mutation/auth.js:147, authSlice.js:1064 |
| Jest template อย่างเดียว | __tests__/App-test.js:1-14 |
redux-logger เฉพาะ non-production | store.js:106-108 |
ลำดับแก้ที่แนะนำ (ไม่ใช่ roadmap บังคับ)
- ทันที (secret): SEC-MOBILE-01 / 02 / 03 / 05 — rotate + เลิก commit secret
- สั้น: SEC-MOBILE-04 ย้าย Encode เข้า sensitive storage; CORR-MOBILE-01 ตั้ง meeting prod; CORR-MOBILE-02 แก้ import
- กลาง: LEG-MOBILE-02 รวม host config + flavors; OBS-MOBILE-01 crash reporting
- ยาว: LEG-MOBILE-01 / 03 upgrade RN + navigation ควบคู่
new-vtrc-mobile
เทียบ sibling ใน Volume 8
| หัวข้อ | vtrc-mobile | new-vtrc-mobile |
|---|---|---|
| RSA private ใน client | มี (login.js) | มี (copy) — ดู scorecard sibling |
| Feature coverage | 121+ scenes | น้อยมาก |
| Navigation | router-flux | (ตรวจในเอกสาร sibling) |
| Meeting UAT hardcode | มี | — |
LOGIN_MUTATION — comment archaeology (apiKey ใน body vs header)
src/gql/mutation/auth.js มีหลายเวอร์ชันของ LOGIN_MUTATION ซ้อนกัน:
| บล็อก | สถานะ | apiKey ใน mutation body? | หลักฐาน |
|---|---|---|---|
| บรรทัด 3-18 | comment — ป้าย "API KEY Production" | มี inline apiKey ใน login(…, apiKey: "346155c3-…") | gql/mutation/auth.js:3-18 |
| บรรทัด 21-36 | active export | ไม่มี — พึ่ง header จาก fetch.js | gql/mutation/auth.js:21-36 |
| บรรทัด 38+ | comment — ป้าย "API KEY UAT" | มี UAT apiKey ใน body | gql/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 material | PEM คู่ hardcode ใน login.js | RSA.generateKeys(2048) ตอนเปิด Setting |
| Private อยู่ที่ | JS bundle (ทุกเครื่องเหมือนกัน) | AsyncStorage Encode ต่อเครื่อง |
| Public อยู่ที่ | bundle + (น่าจะ) server | ส่งขึ้น server ผ่าน createAuthSession |
| Algorithm ฝั่ง client | crypto.publicEncrypt | crypto.createSign('RSA-SHA256') |
| Server mutation | รวมใน login(…) ผ่าน field csId | loginBypass / LOGIN_BYPASS_MUTATION |
| Prompt UI | ไม่มี | TouchID / FaceID / Biometrics (authSlice.js:968-1044) |
| Debt ID | SEC-MOBILE-01 | SEC-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-38—RSA_KEY.privatePEM- ใช้ใน
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=true | App.js:195-199 |
TOKEN_INVALID จาก GraphQL | PURGE + Actions.mainmenu | fetch.js:83-86 |
| Logout สำเร็จ/ล้มเหลว | PURGE + mainmenu | authSlice.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:
Config.APIชี้ env ที่ต้องการ (config/index.js:3-10)HostApi.jsuncomment บล็อกเดียวกับ Config (HostApi.js:2-36)hostUrl.jsHOSTตรงกับ origin เดียวกัน (hostUrl.js:5) — ไม่งั้น Conicle พังเงียบMEETING_API— ปัจจุบันบังคับ UAT; ต้องแก้โค้ดก่อนหวัง prod meeting (config/index.js:12)apiKeyในfetch.js/Authen.js/login.jsตรง envsigningConfigs.release— path/password ไม่ควรเป็นของเครื่องบุคคล (build.gradle:174-179)- SSL cert
digicert2025.cerยังไม่หมดอายุ (asset อยู่ใต้android/app/src/main/assets/) 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 path | vtrc-mobile/ |
| Branch ที่อ่าน | master |
| Tracked files | 1055 |
| Docs path | docs/volume-08-mobile/vtrc-mobile/ |
| วันที่เขียนเนื้อหา V1-depth รอบนี้ | 2026-07-14 |
ทุก claim ในชุดนี้ผูก file:line จากซอร์สบน master — ไม่ invent endpoint หรือ secret ที่ไม่มีในโค้ด
ดูเพิ่ม
- 02-core-pipeline — auth / RSA / host drift เชิงลึก
- 03-features-map — แผนที่ฟีเจอร์
- new-vtrc-mobile — rewrite + RSA copy
- Volume 8 index — กลุ่ม Mobile ทั้งชุด