Base64 یا Base64URL: چه چیزی تغییر میکند و کی از کدام استفاده کنیم
Base64URL همان Base64 استاندارد است که دو نویسهاش عوض شده، - بهجای + و _ بهجای /، و معمولاً padding با = ندارد.
هر دو کدگذاری در RFC 4648 تعریف شدهاند: Base64 استاندارد در بخش ۴ و نسخه «امن برای URL و نام فایل»، یعنی base64url، در بخش ۵. هر دو داده را دقیقاً به یک شکل به گروههای ۶ بیتی تقسیم میکنند و ۶۲ نویسه از ۶۴ نویسهشان مشترک است. فقط نویسههای مقدارهای ۶۲ و ۶۳ متفاوتاند، بهعلاوه قرارداد مربوط به padding. به همین دلیل تبدیل آنها به یکدیگر فقط جایگزینی چند نویسه است و نیازی به کدگذاری دوباره داده نیست؛ و به همین دلیل هم اشتباه گرفتنشان باگی است که بهراحتی رخ میدهد و اغلب دیده نمیشود.
دو الفبا کنار هم
| Base64 (RFC 4648، بخش ۴) | Base64URL (RFC 4648، بخش ۵) | |
|---|---|---|
| مقدارهای ۰ تا ۶۱ | A-Z a-z 0-9 | A-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 استاندارد آن را با = تا ۴ نویسه پر میکند:
| تعداد بایت در گروه آخر | Base64 | Base64URL بدون 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 و امضای JWT | Base64URL بدون padding |
مقدارهای JSON Web Key مثل n و e | Base64URL بدون padding |
code_challenge در OAuth PKCE | Base64URL بدون padding |
Data URIها، data:image/png;base64,... | Base64 استاندارد (RFC 2397) |
| پیوستهای ایمیل (MIME) | Base64 استاندارد، با خطهایی حداکثر ۷۶ نویسهای (RFC 2045) |
| گواهیها و کلیدهای PEM | Base64 استاندارد، با خطهای ۶۴ نویسهای (RFC 7468) |
| هدر احراز هویت HTTP Basic | Base64 استاندارد (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 از قلمافتاده را خودش اضافه میکند، پس میتوانید هر دو شکل را بچسبانید.