guest@websec ~ % man 16-cryptography
16 · การเข้ารหัสและการปกป้องข้อมูล
บั๊กด้านการเข้ารหัสแทบไม่เคยเป็นเรื่องคณิตศาสตร์พัง แต่เป็นความสับสน: คิดว่า encoding คือการเข้ารหัส ใช้ hash แบบเร็วกับรหัสผ่าน ตัวเลขสุ่มที่เดาได้ คีย์ที่วางไว้ข้างข้อมูล
16.1 เครื่องมือสี่แบบที่ต่างกัน
| เครื่องมือ | ย้อนกลับได้? | ต้องใช้คีย์? | ใช้สำหรับ |
|---|---|---|---|
| Encoding (Base64, hex, URL-encoding) | ได้ ใครก็ได้ | ไม่ | ทำให้ไบต์ส่งต่อได้อย่างปลอดภัย ไม่ใช่การรักษาความลับ |
| Hashing (SHA-256; bcrypt/Argon2 for passwords) | ไม่ | ไม่ (มี salt สำหรับรหัสผ่าน) | ลายนิ้วมือข้อมูล ตรวจความถูกต้อง เก็บรหัสผ่าน (เฉพาะ hash แบบช้า) |
| Encryption (AES-256-GCM, ChaCha20-Poly1305) | ได้ ถ้ามีคีย์ | ใช่ | รักษาความลับของข้อมูลขณะจัดเก็บหรือรับส่ง |
| Signing / MAC (HMAC-SHA256, Ed25519) | ข้อมูลยังอ่านได้ | ใช่ | พิสูจน์ว่าข้อมูลไม่ถูกแก้และมาจากคุณ (token, คุกกี้, webhook) |
"การเข้ารหัส" แบบ Base64
hash SHA-256
การเข้ารหัส AES-256-GCM
ลายเซ็น HMAC-SHA256
16.2 การปกป้องข้อมูลขณะจัดเก็บ
ข้อแรก อย่าเก็บสิ่งที่ไม่จำเป็น ส่วนที่เหลือ การเข้ารหัสดิสก์กันดิสก์ที่ถูกขโมยได้ แต่ใครที่ query ฐานข้อมูลได้ยังเห็นข้อความเปล่า การเข้ารหัสรายฟิลด์ ทำให้ dump, backup และ query ที่สอดรู้สอดเห็นได้แต่ตัวอักษรมั่วๆ
bin/rails db:encryption:init แล้ว encrypts :ssn ใช้ deterministic: true เฉพาะคอลัมน์ที่ต้องค้นหาclass Patient < ApplicationRecord encrypts :ssn # non-deterministic (default, strongest) encrypts :email, deterministic: true, downcase: true # queryable: where(email: …) end # bin/rails db:encryption:init → add the output to credentials: # active_record_encryption: # primary_key: … deterministic_key: … key_derivation_salt: …
16.3 คีย์
- เก็บคีย์ แยกจากข้อมูล: ใน credentials หรือ secret manager
- คีย์หนึ่งตัวต่อหนึ่งงาน รั่วตัวเดียวจะได้ไม่เปิดทุกอย่าง
- วางแผน การหมุนเวียนคีย์ ก่อนจะต้องใช้
16.4 ความสุ่ม
token ต้องเดาไม่ได้ rand กับ Math.random() มีไว้ทำเกม ถ้าได้ตัวอย่างมากพอ ผู้โจมตีทำนายตัวถัดไปได้ ให้ใช้ตัวสุ่มทางคริปโต
| เดาได้ (ห้ามใช้) | ทางคริปโต (ใช้) | |
|---|---|---|
| Ruby | rand, Random.new, Array#sample | SecureRandom.urlsafe_base64(32), SecureRandom.uuid |
| JavaScript | Math.random() | crypto.getRandomValues(), crypto.randomUUID() |
| Python | random | secrets.token_urlsafe(32) |
ทำ token ให้ยาว (128 บิตขึ้นไป) รหัส 6 หลักต้องจำกัดอัตราอย่างเข้มงวด
16.5 Timing attack และการเปรียบเทียบแบบใช้เวลาคงที่
การเทียบสตริงปกติหยุดที่ตัวแรกที่ผิด การเดาที่ใกล้กว่าจึงช้ากว่านิดหน่อย วัดมากพอก็กู้ความลับได้ทีละตัว ให้ใช้ การเทียบแบบเวลาคงที่ กับความลับ
SecureRandom, secure_compare, has_secure_token, generates_token_for, MessageVerifier ใช้สิ่งเหล่านี้แทน OpenSSL ดิบtoken = SecureRandom.urlsafe_base64(32) # 256 bits class ApiKey < ApplicationRecord has_secure_token :token, length: 36 # random, unique end ActiveSupport::SecurityUtils.secure_compare(provided, expected) # constant time provided == expected # leaks timing # verify a webhook signature expected = OpenSSL::HMAC.hexdigest("SHA256", webhook_secret, request.raw_post) head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, request.headers["X-Signature"].to_s) # sign/encrypt your own data instead of rolling crypto verifier = Rails.application.message_verifier(:unsubscribe) link_token = verifier.generate(user.id, expires_in: 7.days, purpose: :unsubscribe)
import crypto from "node:crypto";
const token = crypto.randomBytes(32).toString("base64url");
const a = Buffer.from(provided), b = Buffer.from(expected);
const ok = a.length === b.length && crypto.timingSafeEqual(a, b); // constant time
16.6 อย่าคิดค้นเอง
- ใช้ AES-GCM ผ่านไลบรารีที่ดี ห้ามใช้ ECB ห้ามใช้ IV ซ้ำ
- MD5 และ SHA-1 ใช้เป็น cache key ได้ แต่ไม่ใช่สำหรับความปลอดภัย
- อย่าคิดรูปแบบ token เอง ถ้าใช้ JWT ให้ปฏิเสธ
alg: none