Base64 یا Base64URL: چه چیزی تغییر می‌کند و کی از کدام استفاده کنیم

Base64URL همان Base64 استاندارد است که دو نویسه‌اش عوض شده، - به‌جای + و _ به‌جای /، و معمولاً padding با = ندارد.

همین حالا امتحان کنید: کدگذار و کدگشای Base64 رمزگشای JWT

هر دو کدگذاری در RFC 4648 تعریف شده‌اند: Base64 استاندارد در بخش ۴ و نسخه «امن برای URL و نام فایل»، یعنی base64url، در بخش ۵. هر دو داده را دقیقاً به یک شکل به گروه‌های ۶ بیتی تقسیم می‌کنند و ۶۲ نویسه از ۶۴ نویسه‌شان مشترک است. فقط نویسه‌های مقدارهای ۶۲ و ۶۳ متفاوت‌اند، به‌علاوه قرارداد مربوط به padding. به همین دلیل تبدیل آن‌ها به یکدیگر فقط جایگزینی چند نویسه است و نیازی به کدگذاری دوباره داده نیست؛ و به همین دلیل هم اشتباه گرفتنشان باگی است که به‌راحتی رخ می‌دهد و اغلب دیده نمی‌شود.

دو الفبا کنار هم

Base64 (RFC 4648، بخش ۴)Base64URL (RFC 4648، بخش ۵)
مقدارهای ۰ تا ۶۱A-Z a-z 0-9A-Z a-z 0-9
مقدار ۶۲+-
مقدار ۶۳/_
padding با =الزامی، مگر اینکه قالبی که از آن استفاده می‌کند چیز دیگری بگویدمعمولاً حذف می‌شود
امن در URL و نام فایل بدون تغییرخیربله

بایت‌های یکسان در هر دو شکل:

Bytes       fb ff bf
Base64      +/+/
Base64URL   -_-_

Text        <<???>>
Base64      PDw/Pz8+Pg==
Base64URL   PDw_Pz8-Pg

داده‌های باینری کدگذاری‌شده، مثل هش‌ها، کلیدها و توکن‌های تصادفی، خیلی وقت‌ها جایی یک + یا / دارند؛ پس این تفاوت یک حالت نادر و حاشیه‌ای نیست. خود RFC 4648 می‌گوید base64url نباید همان کدگذاری base64 در نظر گرفته شود و کد باید نام نسخه‌ای را که انتظار دارد صریحاً مشخص کند.

چرا Base64 استاندارد در URL و نام فایل خراب می‌شود

هرکدام از سه نویسه اضافه Base64 استاندارد، جای دیگری معنای خاصی دارد:

  • + وقتی query string به شکل داده فرم رمزگشایی شود، فاصله خوانده می‌شود؛ و این همان کاری است که URLSearchParams، $_GET در PHP و بیشتر فریم‌ورک‌های سمت سرور انجام می‌دهند. توکنی مثل ab+c به شکل ab c می‌رسد.
  • / بخش‌های مسیر را از هم جدا می‌کند، پس یک مقدار Base64 در مسیر، بخش‌های اضافه می‌سازد، و بعضی وب‌سرورها شکل کدگذاری‌شده %2F را در مسیر نمی‌پذیرند. این نویسه در نام فایل هم در ویندوز، macOS و لینوکس مجاز نیست.
  • = نام یک پارامتر را از مقدارش جدا می‌کند.

کدگذاری درصدی مقدار (%2B، %2F، %3D) وقتی جواب می‌دهد که همه بخش‌های کد درگیر، این کار را دقیقاً یک بار انجام دهند. در عمل یکی از لایه‌ها فراموش می‌کند یا دو بار انجامش می‌دهد؛ به همین دلیل قالب‌هایی که برای URL طراحی شده‌اند از Base64URL استفاده می‌کنند و کلاً از escape کردن پرهیز می‌کنند.

قاعده‌های padding

Base64 هر ۳ بایت را به ۴ نویسه تبدیل می‌کند. وقتی طول داده مضربی از ۳ نباشد، گروه آخر کوتاه‌تر است و Base64 استاندارد آن را با = تا ۴ نویسه پر می‌کند:

تعداد بایت در گروه آخرBase64Base64URL بدون padding
۱، مثلاً ff/w==_w
۲، مثلاً fb ff+/8=-_8
۳، مثلاً fb ff bf+/+/-_-_

بخش ۳٫۲ از RFC 4648 padding را الزامی می‌داند، مگر اینکه مشخصاتی که به آن ارجاع می‌دهد صریحاً چیز دیگری بگوید. بخش ۵ اضافه می‌کند که = در URL معمولاً باید کدگذاری درصدی شود و وقتی طول داده به هر حال معلوم است، می‌توان padding را کنار گذاشت. قالب‌هایی که بر پایه Base64URL ساخته شده‌اند از همین امکان استفاده می‌کنند: JSON Web Signature (RFC 7515)، و در نتیجه JWTها، همه =های انتهایی را حذف می‌کنند، و code challengeهای PKCE (RFC 7636) هم همین‌طور.

بدون padding، n بایت به ⌈4n/3⌉ نویسه تبدیل می‌شود (چهار سوم n، گرد شده به بالا): ۱۶ بایت به‌جای ۲۴ نویسه، ۲۲ نویسه می‌شود و ۳۲ بایت به‌جای ۴۴ نویسه، ۴۳ نویسه. طولی که باقی‌مانده تقسیمش بر ۴ برابر ۱ باشد، در هیچ‌کدام از دو نسخه معتبر نیست.

هرکدام کجا استفاده می‌شود

کاربردنسخه
هدر، payload و امضای JWTBase64URL بدون padding
مقدارهای JSON Web Key مثل n و eBase64URL بدون padding
code_challenge در OAuth PKCEBase64URL بدون padding
Data URIها، data:image/png;base64,...Base64 استاندارد (RFC 2397)
پیوست‌های ایمیل (MIME)Base64 استاندارد، با خط‌هایی حداکثر ۷۶ نویسه‌ای (RFC 2045)
گواهی‌ها و کلیدهای PEMBase64 استاندارد، با خط‌های ۶۴ نویسه‌ای (RFC 7468)
هدر احراز هویت HTTP BasicBase64 استاندارد (RFC 7617)
توکن در URL، نام فایل و کلید کشBase64URL انتخاب امن است

پیش‌فرض کتابخانه‌ها Base64 استاندارد است: btoa، b64encode در Python، base64_encode در PHP و Base64.getEncoder() در Java همگی +، / و padding تولید می‌کنند. Base64URL را باید صریحاً درخواست کنید.

تبدیل این دو به یکدیگر

atob و btoa فقط Base64 استاندارد را می‌شناسند، پس اگر از آن‌ها استفاده می‌کنید، باید خودتان نویسه‌ها را تبدیل کنید و padding را درست کنید:

// Base64 -> Base64URL
const url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');

// Base64URL -> Base64
let std = url.replace(/-/g, '+').replace(/_/g, '/');
std += '='.repeat((4 - std.length % 4) % 4);

مرورگرهای جدید متدهای Uint8Array.prototype.toBase64({ alphabet: 'base64url', omitPadding: true }) و Uint8Array.fromBase64(s, { alphabet: 'base64url' }) را هم دارند که مستقیماً روی بایت‌ها کار می‌کنند؛ پیش از تکیه بر آن‌ها، پشتیبانی مرورگرهای هدفتان را بررسی کنید. پلتفرم‌های دیگر یا پشتیبانی مستقیم دارند یا یک روش کوتاه و رایج:

// Node.js
Buffer.from(data).toString('base64url');     // no padding
Buffer.from(s, 'base64url');

# Python
base64.urlsafe_b64encode(data).rstrip(b'=')
base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))

// PHP
rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
base64_decode(strtr($s, '-_', '+/'), true);

// Java
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
Base64.getUrlDecoder().decode(s);

urlsafe_b64decode در Python ورودی بدون padding را رد می‌کند و به همین دلیل علامت‌های = اضافه شده‌اند. base64_decode در PHP، حتی در حالت strict، نبودن padding را می‌پذیرد.

دام‌ها

  • باگ‌هایی که از تست‌ها رد می‌شوند. یک رمزگشای استاندارد فقط روی ورودی Base64URL که - یا _ دارد شکست می‌خورد. برای داده تصادفی، این یعنی حدود نیمی از مقدارهای ۱۶ بایتی و حدود سه‌چهارم مقدارهای ۳۲ بایتی، مثل چکیده‌های SHA-256 (یک منهای ۶۲/۶۴ به توان ۴۲، یعنی تقریباً ۷۴ درصد). تستی با یکی دو مقدار نمونه به‌راحتی ممکن است پاس شود. اینکه چرا رمزگشاها این رشته‌ها را رد می‌کنند و هر زبان چقدر متفاوت واکنش نشان می‌دهد، در راهنمای خطای Invalid Base64 string آمده است.
  • رمزگشاهای سهل‌انگار این اشتباه را پنهان می‌کنند. Buffer.from(s, 'base64') در Node.js هر دو الفبا را می‌پذیرد؛ این راحت است، اما یعنی باگ فقط در یک سرویس دیگر خودش را نشان می‌دهد. b64decode پیش‌فرض Python بی‌صدا - و _ را دور می‌ریزد و بعد یا بایت‌های اشتباه برمی‌گرداند یا خطای گمراه‌کننده‌ای درباره تعداد نویسه‌ها می‌دهد.
  • Base64URL همان کدگذاری URL نیست. کدگذاری درصدی یک رشته Base64 استاندارد، %2B، %2F و %3D می‌دهد؛ آن را به Base64URL تبدیل نمی‌کند، و رمزگشایی که یکی را انتظار دارد دیگری را نمی‌پذیرد.
  • هیچ‌کدام چیزی را پنهان نمی‌کند. payload یک JWT فقط JSON کدگذاری‌شده با Base64URL است و هر کسی که توکن را داشته باشد می‌تواند آن را بخواند. یک توکن را در رمزگشای JWT بچسبانید تا هدر و payload آن را ببینید.

کدگذار Base64 این سایت Base64 استاندارد همراه با padding تولید می‌کند؛ اگر Base64URL لازم دارید، جایگزینی بالا را اعمال کنید. برای رمزگشایی، کدگشای Base64 هر دو الفبا را می‌پذیرد و padding از قلم‌افتاده را خودش اضافه می‌کند، پس می‌توانید هر دو شکل را بچسبانید.

سوالات متداول

تفاوت Base64 و Base64URL چیست؟

Base64URL نویسه + را با - و / را با _ جایگزین می‌کند تا نتیجه در URL و نام فایل امن باشد، و معمولاً padding با = را حذف می‌کند. ۶۲ نویسه دیگر و روش تقسیم داده به گروه‌های ۶ بیتی کاملاً یکسان است.

آیا یک رمزگشای Base64 معمولی می‌تواند Base64URL را بخواند؟

فقط بعد از تبدیل: - را با + و _ را با / جایگزین کنید، بعد تا وقتی طول مضربی از ۴ شود = اضافه کنید. بعضی رمزگشاها، مثل Buffer در Node.js، هر دو الفبا را مستقیماً می‌پذیرند.

چرا JWTها از Base64URL بدون padding استفاده می‌کنند؟

توکن‌ها در URL، هدرهای HTTP و کوکی‌ها جابه‌جا می‌شوند، جایی که +، / و = مشکل‌ساز هستند. RFC 7515 کدگذاری JSON Web Signature را base64url با حذف همه نویسه‌های = انتهایی تعریف می‌کند.

آیا Base64URL از Base64 طولانی‌تر است؟

نه. هر دو برای هر بایت همان تعداد نویسه تولید می‌کنند؛ Base64URL بدون padding تا دو نویسه کوتاه‌تر است.
همین حالا امتحان کنید: کدگذار و کدگشای Base64 رمزگشای JWT