C में Base64 डिकोडिंग: एक सम्पूर्ण गाइड
वह बैठती रहती है एक API रेस्पॉन्स में, एक कॉन्फ़िग फ़ाइल में, एक ईमेल अटैचमेंट में, या किसी URL के बीच-बीच में: अक्षरों और अंकों की एक लंबी स्ट्रिंग, कभी-कभार एक + या /, और शायद आख़िर में एक-दो =। आप उसे तुरंत पहचान लेते हैं, और अब आपको वापस असली बाइट्स चाहिए - C में। Base64 डिकोडिंग का पूरा काम बस यही है: चार वर्णमाला के चर अंदर जाते हैं, तीन कच्चे बाइट्स बाहर आते हैं, बार-बार, तब तक जब तक = निशान आपसे न कह दें कि असली डेटा कहाँ तक था। इस साइट का होम पेज फॉर्मेट को चरण-दर-चरण समझाता है, इसलिए यह आर्टिकल अपनी ताक़त वहीं लगाता है जहाँ असली काम है: बफ़र्स, लाइब्रेरियाँ, और उनके बीच बसे फंदे।
पहली malloc से पहले दो बातें ज़रूर जान लें। पहली, डिकोडिंग वह दिशा है जो सिकोड़ती है: आउटपुट इनपुट का तीन चوتथाई साइज़ होता है, इसलिए किसी डिकोडर को कभी उतनी मेमोरी की ज़रूरत नहीं पड़ती जितना पेयलॉड वह पहले से ही पकड़े हुए है। दूसरी - और यही सबसे बड़ी ख़बर है - C के साथ कोई Base64 डिकोडर शिप नहीं होता। भाषा की स्टैंडर्ड लाइब्रेरी उससे कहीं पहले फ्रीज़ हो चुकी थी जब Base64 का अस्तित्व ही नहीं था, और आज तक कोई भी स्टैंडर्ड उस खाली जगह को भरने आया नहीं। इसलिए हर C प्रोग्राम जो Base64 डिकोड करता है, किसी न किसी लाइब्रेरी पर टिकता है, और जिन चार का अमली ज़िन्दगी में फ़र्क़ पड़ता है वे हैं OpenSSL, Mbed TLS, APR-Util, और GLib। हर एक का अपना अलग व्यक्तित्व है: वह क्या माफ़ कर देता है, एरर कैसे रिपोर्ट करता है, और आपके आउटपुट के साथ चुपचाप क्या करता है। जैसे ही आपको अपने डिकोडर का व्यक्तित्व पता हो जाता है, C में Base64 डिकोडिंग रहस्यमयी बगों का स्रोत बंद होकर एक रूटीन बन जाता है जिसे आप आँखें बंद करके भी लिख सकते हैं।
टूलबॉक्स: चार रास्ते, अपना बाइट वापस पाने के
यह है एक नज़र में पूरा नज़ारा। चारों स्टैंडर्ड वर्णमाला को कवर करती हैं; फ़र्क़ किनारों में है, और बग यहीं किनारों से निकलते हैं।
| लाइब्रेरी | हेडर | एरर मॉडल | आउटपुट की वह अजीब बात जो याद रखनी है |
|---|---|---|---|
| OpenSSL (libcrypto) | <openssl/evp.h> |
बुरे इनपुट पर -1 रिटर्न करता है |
वन-शॉट डिकोडर आख़िरी हिस्से को ज़ीरो-पैड करता है |
| Mbed TLS | <mbedtls/base64.h> |
रिटर्न कोड (-0x002C, -0x002A) |
चारों में सबसे सख़्त इनपुट नियम |
| APR-Util | <apr-1.0/apr_base64.h> |
कुछ नहीं: पहले ही अजीब चर पर रुक जाता है | इंट-लंबाई वाली API, इसलिए 2 GB ही सीमा है |
| GLib | <glib.h> |
NULL सिर्फ़ कठोर फेलियर पर रिटर्न होता है |
बेख़बर कचरा चुपचाप नज़रअंदाज़ कर देता है |
इंस्टॉलेशन में हर डिस्ट्रो के लिए बस एक पैकेज का नाम। OpenSSL के लिए: Debian और Ubuntu पर libssl-dev, Fedora और RHEL पर openssl-devel, Arch पर openssl, और macOS पर brew install openssl। Mbed TLS के लिए: libmbedtls-dev (या mbedtls)। APR-Util के लिए: libaprutil1-dev और साथ में libapr1-dev। GLib के लिए: glib2.0-dev। उसके बाद लिंकिंग के लिए क्रमशः -lcrypto, -lmbedcrypto, -laprutil-1, या -lglib-2.0। कौन-सी चुनोगे? अगर आप पहले से ही TLS या हैशिंग के लिए OpenSSL लिंक करते हैं (ज़्यादातर सर्वर ऐसा करते हैं), तो OpenSSL ही इस्तेमाल करें। एम्बेडेड और संसाधन-सीमित बिल्ड्स के लिए Mbed TLS वह छोटी, सख़्त नागरिक है। अगर आप Apache एकोसिस्टम के अंदर हैं, तो APR-Util पहले से वहाँ खड़ा है। अगर आपका कोडबेस GNOME या GTK पर टिका है, तो GLib सब कुछ एक ही रनटाइम में सजाता है।
OpenSSL: वह डिकोडर जो खाली जगह ज़ीरो से भर देता है
OpenSSL में Base64 दो रूपों में रहता है। वन-शॉट फ़ंक्शन ज़्यादातर कोड का सितारा है:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
unsigned char out[16];
const char *payload = "TWFuZQ==";
int n = EVP_DecodeBlock(out, (const unsigned char *)payload,
(int)strlen(payload));
if (n < 0) {
printf("not base64\n");
return 1;
}
printf("%d bytes\n", n);
return 0;
}
इसे Base64 चरों का बफ़र और एक लंबाई दें, तो वह डिकोड किए बाइट्स out में लिखता है और वापस बताता है कि कितने। वह शुरू के व्हिटस्पेस काटता है, आख़िर के व्हिटस्पेस और न्यूलाइन काटता है, और वह इनपुट मना कर देता है जो ट्रिम करने के बाद चार चरों का गुणज न हो, या जिसमें वर्णमाला से बाहर का कोई चर हो। अब तक सब बिल्कुल बूझदारी भरा करार। सिवाय एक बात की, जिसने चुपचाप से एक से ज़्यादा डेटाबेस इम्पोर्ट ख़राब कर डाला है: रिटर्न वैल्यू असली डेटा लंबाई नहीं होती।
उस प्रोग्राम को चलाइए और आपको 4 bytes मिलेंगी... नहीं, रुकिए। TWFuZQ== दो चार-चर समूह हैं, इसलिए फ़ंक्शन 6 रिटर्न करता है, और बफ़र में 4d 61 6e 65 00 00 है: "Mane" शब्द और दो ज़ीरो बाइट्स। OpenSSL का वन-शॉट डिकोडर फिक्स्ड क्वांटा में काम करता है - हर चार इनपुट चर हमेशा बिल्कुल तीन आउटपुट बाइट्स देते हैं - और जब आख़िरी समूह में सिर्फ़ एक ही असली बाइट थी, तो बची हुई दो स्लॉट्स ज़ीरो से भर जाती हैं। मैनुअल में इसका ज़िक्र एक ही शांत वाक्य में होता है ("आवश्यकता हो तो आउटपुट को 0 बिट्स से पैड कर दिया जाएगा"), और वही एक वाक्य इस फ़ंक्शन की पूरी मैन पेज का सबसे ज़रूरी वाक्य है।
असली लंबाई पैडिंग से वापस निकाली जाती है, और यह बस दो-लाइन की गणित है:
size_t real_length(const char *b64) {
size_t len = strlen(b64);
while (len > 0 && b64[len - 1] == '=') len--;
return len * 3 / 4;
}
वर्णमाला के चर गिनें, आख़िरी पैड हटाएँ, तीन से गुणा करें, चार से भाग दें। TQ== के लिए (M अक्षर, एन्कोडेड) यह (2 * 3) / 4 = 1 असली बाइट देता है - जबकि EVP_DecodeBlock तीन रिपोर्ट करेगा। (पॉइंटर, लंबाई) की जोड़ी हमेशा साथ रखें, और डिकोड किए डेटा पर कभी strlen न चलाएँ, क्योंकि आपके पास लौटे बाइट्स JPEG भी हो सकते हैं, और उनमें से पहला NUL भी हो सकता है।
स्ट्रीमिंग डिकोडर: वह जो कब रुकना है, पता है
बाकी सबके लिए OpenSSL स्ट्रीमिंग जोड़ी देता है: EVP_DecodeUpdate और EVP_DecodeFinal। जो कॉल के बीच स्टेट को आगे बढ़ाता है, वही कॉन्टेक्स्ट ऑब्जेक्ट है: वह एक अधूरे समूह के एक से तीन चर पकड़े रहता है, ताकि आप पेयलॉड चंक्स में ढाल सकें। ज़रूरी व्यवहार यह है: व्हिटस्पेस (स्पेस, टैब, कैरिएज रीटर्न, लाइन फ़ीड) स्ट्रीम में कहीं भी छलांग खाने को मिलता है, वर्णमाला से बाहर कोई भी चर या डेटा के बीच में आया = तुरंत -1 दे देता है, और अपडेट से 0 का मतलब है "पैडिंग देख ली गई, अब कुछ और आने की उम्मीद नहीं"। अगर आधा-अधूरा समूह अभी भी लटका हुआ है, तो EVP_DecodeFinal -1 के साथ मना कर देता है, क्योंकि व्हिटस्पेस हटाने के बाद जो लंबाई चार का गुणज न हो, वह वैध पेयलॉड नहीं।
कोड से पहले एक वर्ज़न का नोट, क्योंकि पुराने ट्यूटोरियल आपको उठाएँगे: OpenSSL 3.x में कॉन्टेक्स्ट टाइप EVP_ENCODE_CTX ऑपेक है, इसलिए इंटरनेट के कोड में ज़्यादातर दिखने वाला स्टैक पैटर्न EVP_ENCODE_CTX ctx; अब कंपाइल ही नहीं होता। खुद अलोकेट करें और खुद फ्री करें:
static int decode_b64(const unsigned char *in, int in_len,
unsigned char *out, int *out_len) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
if (ctx == NULL) {
return -1;
}
*out_len = 0;
EVP_DecodeInit(ctx);
int r = EVP_DecodeUpdate(ctx, out, out_len, in, in_len);
if (r < 0) {
EVP_ENCODE_CTX_free(ctx);
return -1;
}
int tail = 0;
r = EVP_DecodeFinal(ctx, out + *out_len, &tail);
EVP_ENCODE_CTX_free(ctx);
if (r < 0) {
return -1;
}
*out_len += tail;
return 0;
}
आउटपुट बफ़र का साइज़ in_len * 3 / 4 + 3 रखें और कॉल हर इनपुट के लिए सुरक्षित है। इसे एक MIME-रैप पेयलॉड पर काम करते देखिए, जहाँ लाइन ब्रेक समूह के बीच में गिरता है:
const char *wrapped = "TWFu\nZQ==";
unsigned char out[16];
int out_len = 0;
if (decode_b64((const unsigned char *)wrapped,
(int)strlen(wrapped), out, &out_len) != 0) {
printf("invalid base64\n");
return 1;
}
printf("%.*s\n", out_len, out); /* Mane */
ब्रेक गायब हो जाता है, चार बाइट्स बाहर आ जाते हैं, और किसी को इनपुट को प्री-क्लिन करने की ज़रूरत ही नहीं पड़ी। वन-शॉट फ़ंक्शन से एक बोनस फ़र्क़ भी है: स्ट्रीमिंग पथ बाइट्स को ईमानदारी से गिनता है। इसे TQ== दें, तो वह बिल्कुल एक बाइट (4d) रिटर्न करता है, बिना किसी ज़ीरो पैडिंग के, क्योंकि उसे समझ है कि दो पैड का मतलब है कि तीन आउटपुट स्लॉट्स में से दो कभी भरे ही नहीं। अगर आपके पेयलॉड को OpenSSL से भरोसेमंद लंबाई चाहिए, तो यही वह रास्ता है।
Mbed TLS: वह सख़्त
Mbed TLS (वह क्रिप्टो लाइब्रेरी जिसे PolarSSL के रूप में जन्म मिला और जो अब ARM के एम्बेडेड स्टैक्स के अंदर शिप होती है) आपको दो फ़ंक्शन देता है, जिनका करार बड़ा साफ़ है:
int mbedtls_base64_encode(unsigned char *dst, size_t dlen, size_t *olen,
const unsigned char *src, size_t slen);
int mbedtls_base64_decode(unsigned char *dst, size_t dlen, size_t *olen,
const unsigned char *src, size_t slen);
डिकोडिंग ऐसे कीजिए जैसे एक सावधान इंसान करता है। इसे dst को NULL (या dlen को शून्य) रखकर कॉल करें, तो वह बिना किसी काम किए *olen में ज़रूरी साइज़ बता देता है; असली काम के लिए कॉल करें, तो सफलता पर 0, इनपुट में कुछ भी ग़लत हो तो MBEDTLS_ERR_BASE64_INVALID_CHARACTER (यानी -0x002C), और डेस्टिनेशन छोटा हो तो MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL (यानी -0x002A) मिलता है। डिकोड की गई लंबाई *olen में उतरती है, और OpenSSL के वन-शॉट फ़ंक्शन के बिल्कुल उल्टा, यह हमेशा ईमानदार संख्या होती है: TQ== डिकोड करने पर आपको एक बाइट मिलती है, 4d, और बस।
इनपुट के नियम चारों लाइब्रेरियों में सबसे सख़्त हैं, और याद रखने लायक़, क्योंकि इनसे तय होता है कि Mbed TLS के लिए "वैध" का मतलब क्या है:
- समूहों के बीच CRLF और LF लाइन ब्रेक आ सकते हैं - ईमेल पेयलॉड जैसा है वैसा ही काम करते हैं।
- स्पेस लाइन ब्रेक के ठीक पहले और बफ़र के बिल्कुल आख़िर में जायज़ हैं, लेकिन लाइन ब्रेक के बाद या समूह के बीच में स्पेस एक एरर है।
=चर ज़्यादा से ज़्यादा दो, और सिर्फ़ आख़िर में; पैड के बाद कोई भी डेटा एक एरर है।- 127 से ऊपर कोई भी बाइट (एक्सेंट, UTF-8 टुकड़े, बाइनरी कचरा) एक एरर है।
वह आख़िरी नियम ही वार करती है: अगर पेयलॉड किसी ऐसे स्रोत से आए जिसने चर एन्कोडिंग ख़राब कर दी हो, तो Mbed TLS उसे रद्द कर देगा, जहाँ कोई आलसी डिकोडर कंधे उछालकर डिकोड कर लेता। जिस भी काम में अनट्रस्टेड इनपुट छूता है, सख़्ती एक फ़ीचर है। पूरा डिकोड ऐसा दिखता है:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/base64.h>
int main(void) {
const char *payload = "TWFuZQ==";
size_t need = 0;
int rc = mbedtls_base64_decode(NULL, 0, &need,
(const unsigned char *)payload,
strlen(payload));
if (rc != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) {
printf("size query failed: %d\n", rc);
return 1;
}
unsigned char *out = malloc(need);
size_t olen = 0;
rc = mbedtls_base64_decode(out, need, &olen,
(const unsigned char *)payload,
strlen(payload));
if (rc != 0) {
printf("decode failed: %d\n", rc);
free(out);
return 1;
}
printf("%.*s\n", (int)olen, out);
free(out);
return 0;
}
(साइज़ क्वेरी का "too small" कोड लौटाना इरादे से है: यही वह तरीका है जिससे फ़ंक्शन बताता है कि वह लिखने वाला था। ऊपर दिए दोनों रिटर्न कोड <mbedtls/base64.h> से आते हैं, यानी वही हेडर जिसमें यह फ़ंक्शन घोषित है।)
APR-Util और GLib: दो और कुर्सियाँ
APR-Util - Apache Portable Runtime की यूटिलिटी लाइब्रेरी, उस नींव पर जो Apache HTTP Server खड़ा है - ने Base64 उतने लंबे समय से संभाला है जितने से सर्वर को Basic ऑथेंटिकेशन हेडर डिकोड करने की ज़रूरत है। API int-आधारित फ़ंक्शनों का एक छोटा सा परिवार है:
#include <apr-1.0/apr_base64.h>
int apr_base64_encode_len(int len);
int apr_base64_encode(char *coded_dst, const char *plain_src,
int len_plain_src);
int apr_base64_decode_len(const char *coded_src);
int apr_base64_decode(char *plain_dst, const char *coded_src);
इसे हाथ में लेने से पहले दो बातें जान लें। पहली, लंबाई int हैं: 32-बिट, इसलिए अमली सीमा हर कॉल के लिए 2 GB है, जो हेडर और कॉन्फ़िग वैल्यू के लिए ठीक है, मगर 4 GB फ़ाइल डिकोड करने के लिए बिल्कुल नहीं। दूसरी - और यह बड़ी है - डिकोड फ़ंक्शन का कोई एरर रिटर्न ही नहीं है। यह व्यवहार सिर्फ़ इम्प्लिमेंटेशन में दिखता है, हेडर में नहीं: डिकोडर कोई भी अवैध चर, व्हिटस्पेस और NUL समेत, एक टर्मिनल मान लेता है। वह तब तक डिकोड करता है जब तक उसे पहली ऐसी चीज़ न मिले जो वह पहचाने, कितनी दूर तक गया यह रिटर्न करता है, और कुछ नहीं कहता। कटा हुआ पेयलॉड, आख़िर में टिप्पणी वाला पेस्ट, बीच में ख़राब बाइट - सब कुछ चुपचाप छोटे आउटपुट की तरफ़ जाता है। अगर आप APR के डिकोडर का इस्तेमाल करते हैं, तो आपको रिटर्न की गई लंबाई को इससे तुलना करनी होगी कि पेयलॉड का वादा क्या था; फ़ंक्शन यह आपके लिए नहीं करेगा। कोई पूल-अलोकेटिंग रैपर नहीं है - डेस्टिनेशन बफ़र आप खुद देते हैं, इसलिए पूल-चालित कोड में आप खुद plain_dst को पूल से अलोकेट करते हैं। इस आर्टिकल में कहीं और न मिलने वाली एक EBCDIC बात भी है: EBCDIC मशीनों पर फ़ंक्शन एन्कोडिंग से पहले इनपुट को ASCII में बदलते हैं और डिकोडिंग के बाद वापस, इसलिए वही कोड उन मेन्फ़्रेम्स पर चलता है जहाँ आज भी httpd चलता है।
GLib, जो GTK और ज़्यादातर GNOME ऐप्लिकेशन के पीछे का रनटाइम है, बिल्कुल उल्टा व्यक्तित्व लेता है। इसका डिकोडर एक स्ट्रिंग लेता है और हमेशा ताज़ा अलोकेट किया हुआ बफ़र वापस सौंपता है (NULL सिर्फ़ तभी जब आप NULL पॉइंटर दें), जो कुछ डिकोड कर सके वही करता है और बाक़ी को चुपचाप छोड़ देता है:
#include <glib.h>
gsize out_len = 0;
guchar *bytes = g_base64_decode(payload, &out_len);
if (bytes == NULL) {
printf("not base64\n");
} else {
printf("%u bytes\n", (unsigned)out_len);
g_free(bytes);
}
फंदा शब्द "हमेशा" में छिपा है। GLib का डिकोडर सहिष्णु स्कूल का है: वर्णमाला से बाहर के चर छलांग खाने की चीज़ हैं, घातक नहीं। इसे TWFuZ@== दें, तो वह उँगली उठाए बिना "Man" के तीन बाइट्स वापस सौंप देगा। एक हौज़ी इन-प्लेस वैरिएंट भी है, g_base64_decode_inplace(), जो इनपुट बफ़र के ऊपर से डिकोड करता है (सुरक्षित, क्योंकि आउटपुट इनपुट से छोटा है) और वही पॉइंटर रिटर्न करता है, ताकि नतीजा बफ़र की शुरूआत से ही शुरू हो - मेमोरी-तंग कोड के लिए एक बढ़िया ट्रिक, और यह CRLF-रैप इनपुट भी खुशी-खुशी चबा लेता है। C डेवलपर्स के लिए सीख यह है: अगर आपका डेटा अनट्रस्टेड है, तो ख़राब पेयलॉड से GLib आपकी बचाव नहीं करेगा। _step वैरिएंट्स (g_base64_decode_step, स्टेट इंटीजर के साथ) तब उपलब्ध हैं जब आपको इनक्रिमेंटल डिकोडिंग चाहिए, और मेल कराने वाली g_base64_encode_step/g_base64_encode_close जोड़ी एन्कोडिंग की तरफ़ रहती है।
URL-सेफ़ Base64: दूसरी वर्णमाला
स्टैंडर्ड वर्णमाला और आपके URLs के बीच कहीं, किसी का नुकसान हुआ। स्टैंडर्ड Base64 अपने दो सबसे ऊँचे चिह्नों के रूप में + और / इस्तेमाल करता है, और दोनों ही URLs में मुसीबत हैं: क्वेरी स्ट्रिंग में + अक्सर आपके सर्वर तक पहुँचने से पहले ही स्पेस मान लिया जाता है, और / पथ सेपरेटर है। RFC 4648, सेक्शन 5, ठीक इसका इलाज परिभाषित करता है, जिसे base64url कहते हैं: वही एन्कोडिंग, जिसमें + की जगह -, / की जगह _, और जब लंबाई किसी दूसरे तरीके से मालूम हो तो आख़िरी = पैडिंग बिल्कुल छोड़ दी जाती है। JSON Web Tokens, OAuth state पैरामीटर, और अनेक API सेशन IDs इसी डायलक्ट में रहते हैं।
चारों C लाइब्रेरियों में से कोई भी base64url को नेटिव रूप से डिकोड नहीं करता, इसलिए यह कन्वर्ज़न एक छोटा सा हेल्पर है जिसे आप एक बार लिखते हैं और बार-बार रीयूज़ करते हैं: दो ख़ास चरों को वापस मैप करें, ग़ायब पैडिंग जोड़ दें, फिर नतीजा अपने स्टैंडर्ड डिकोडर को सौंप दें। पहले लंबाई की जाँच, क्योंकि चार के गुणज से एक ज़्यादा वाली लंबाई किसी भी Base64 डायलक्ट में असंभव है:
int base64url_decode(const char *url_safe, unsigned char *out,
size_t out_cap, size_t *out_len) {
size_t len = strlen(url_safe);
if (len % 4 == 1) {
return -1;
}
size_t needed = (len * 3) / 4;
if (needed > out_cap) {
return -2;
}
char *std = malloc(len + 4);
if (std == NULL) {
return -3;
}
for (size_t i = 0; i < len; i++) {
char c = url_safe[i];
if (c == '-') c = '+';
if (c == '_') c = '/';
std[i] = c;
}
size_t pad = (4 - len % 4) % 4;
for (size_t i = 0; i < pad; i++) {
std[len + i] = '=';
}
int n = EVP_DecodeBlock(out, (const unsigned char *)std,
(int)(len + pad));
free(std);
if (n < 0) {
return -1;
}
*out_len = needed;
return 0;
}
इस राह को दो गड्ढे चौकाए हुए हैं। पहला दिशा का है: अगर आप URL-सेफ़ पेयलॉड को चर-बदलवां के बिना स्टैंडर्ड डिकोडर में डालें, तो OpenSSL और Mbed TLS उसे मना कर देंगे (वह चर उनकी वर्णमाला में नहीं हैं), जबकि GLib - और _ को चुपचाप छोड़कर ऐसी स्ट्रिंग वापस सौंपेगा जो जितनी होनी चाहिए उससे छोटी है - बिना किसी एरर के। हमेशा हेल्पर के रास्ते से जाएँ। दूसरा RFC की अपनी ही चेतावनी है, जिसे ग़ंभीरता से लेने लायक़ है: base64url को "base64 एन्कोडिंग के बराबर नहीं माना जाना चाहिए"। अगर किसी पेयलॉड में - या _ चर बिल्कुल न हों, तो वह डेटा दोनों डायलक्ट्स के लिए बाइट-दर-बाइट एक जैसा होता है, और भ्रम दिखाई नहीं देता - यही वजह है कि भ्रम तब तक जीवित रहता है जब तक वह ऐसे पेयलॉड से न टकराए जिसमें वह चर हो।
फ़ाइलें: मूल को वापस पाना
फ़ाइल-रूप के कामों में सबसे आम काम किसी एक्सपोर्ट रूटीन के किए काम का उल्टा है: एक .b64 टेक्स्ट फ़ाइल आती है, और आपको मूल फ़ाइल वापस चाहिए। पूरा टेक्स्ट पढ़ लीजिए, डिकोड कर लीजिए, और किसी भी लेबल पर भरोसा करने से पहले बाइट्स को खुद बयान करने दीजिए। C में finfo नहीं है, इसलिए अमली जाँच शुरू के कुछ बाइट्स पर मैजिक-नंबर स्निफ़ है:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int main(void) {
FILE *f = fopen("upload.b64", "rb");
if (f == NULL) {
return 1;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *text = malloc((size_t)size + 1);
size_t got = fread(text, 1, (size_t)size, f);
fclose(f);
text[got] = '\0';
unsigned char *out = malloc((got * 3) / 4 + 3);
int out_len = 0;
if (decode_b64((const unsigned char *)text, (int)got,
out, &out_len) != 0) {
printf("not valid base64\n");
free(text);
free(out);
return 1;
}
free(text);
const char *kind = "unknown binary";
if (out_len >= 4 && memcmp(out, "\x89PNG", 4) == 0) kind = "png";
else if (out_len >= 5 && memcmp(out, "%PDF-", 5) == 0) kind = "pdf";
else if (out_len >= 4 && memcmp(out, "PK\x03\x04", 4) == 0) kind = "zip";
else if (out_len >= 3 && memcmp(out, "\xff\xd8\xff", 3) == 0) kind = "jpeg";
printf("looks like a %s, %d real bytes\n", kind, out_len);
free(out);
return 0;
}
किनारों के नोट्स: टेक्स्ट वाले हिस्से के लिए भी फ़ाइल को बाइनरी मोड (rb/wb) में खोलें, क्योंकि कुछ प्लेटफ़ॉर्म्स पर टेक्स्ट मोड लाइन एंडिंग्स बदल देता है और आपके चरों की गिनती ख़राब कर देता है; और डिकोड किए बफ़र को "देखने के लिए कि यह क्या है" कभी printf("%s") न करें। उस सवाल के लिए सच्चाई से पूछने का रास्ता मैजिक स्निफ़ है, और अगर आप बाद में ब्राउज़र को पुनर्प्राप्त फ़ाइल सर्व करते हैं, तो Content-Type उसी स्निफ़ से आए, फ़ाइल नाम से नहीं।
Data URIs: URL के अंदर की इमेज
वेब दुनिया का पसंदीदा अतिथि: किसी ने फ़ॉर्म में एक इमेज पेस्ट कर दी, और फ्रंट-एंड आपके सर्वर को एक पूरा data URI सौंपता है, जैसे data:image/png;base64,iVBORw0KGgo...। RFC 2397 आकार परिभाषित करता है: data:, वैकल्पिक मीडिया टाइप, वैकल्पिक ;base64 फ्लैग, एक कॉमा, और उसके बाद पेयलॉड। जब फ्लैग मौजूद हो, पेयलॉड Base64 होता है; जब न हो, पेयलॉड पर्सेंट-एन्कोडेड सादा टेक्स्ट होता है - दुर्लभ, पर क़ानूनी। अगर मीडिया टाइप छोड़ दिया गया हो, तो डिफ़ॉल्ट text/plain;charset=US-ASCII है। C में इसे पार्स करना बस इतना है: कॉमा खोजें, और उसके ठीक पहले बैठे को देखें:
int split_data_uri(const char *uri, char *mime, size_t mime_cap,
int *is_b64, const char **payload) {
if (strncmp(uri, "data:", 5) != 0) {
return -1;
}
const char *comma = strchr(uri, ',');
if (comma == NULL) {
return -1;
}
*is_b64 = 0;
const char *meta = uri + 5;
size_t meta_len = (size_t)(comma - meta);
if (meta_len >= 7 && strncmp(comma - 7, ";base64", 7) == 0) {
*is_b64 = 1;
meta_len -= 7;
}
if (meta_len == 0) {
snprintf(mime, mime_cap, "text/plain;charset=US-ASCII");
} else {
snprintf(mime, mime_cap, "%.*s", (int)meta_len, meta);
}
*payload = comma + 1;
return 0;
}
और कॉलर पढ़ने में एक वाक्य जैसा है:
char mime[256];
int is_b64 = 0;
const char *payload = NULL;
const char *uri = "data:image/png;base64,iVBORw0KGgo...";
if (split_data_uri(uri, mime, sizeof(mime), &is_b64, &payload) == 0) {
printf("mime=%s base64=%d\n", mime, is_b64);
/* अब अपनी पसंद की लाइब्रेरी से payload डिकोड करें */
}
इस फॉर्मेट में तीन गड्ढे बसे हैं। पहला ग़ायब ;base64 फ्लैग है: बिना उसके एक क़ानूनी data URI पर्सेंट-एन्कोडेड पेयलॉड ले जाता है, और उसे Base64 डिकोडर से गुज़ारने पर कचरा निकलता है - पहले फ्लैग देखें, फिर अपना डिकोडर चुनें। दूसरा दावा किया गया मीडिया टाइप है: यह भेजने वाले की तरफ़ से एक संकेत है, सत्य नहीं; फ़ाइल वाले सेक्शन का मैजिक-नंबर स्निफ़ आपका सत्य है। तीसरा साइज़ है: RFC की अपनी सलाह यह है कि data URIs छोटे मानों के लिए हैं, इसलिए कई मेगाबाइट की इमेज जो URL के अंदर सवार हो, वह आपकी आर्किटेक्चर में एक बुरी बू है, जश्न की घटना नहीं।
JWTs: ग़ायब-न-सिक्क्रेट हिस्से पढ़ना
वेब पर सबसे मशहूर Base64 पेयलॉड JSON Web Token है, और जैसे ही आपको उसके आकार से पहचान हो जाती है, वह सबसे कम डरावना भी। RFC 7519 के अनुसार, एक कम्पैक्ट JWT तीन base64url हिस्सों से बनता है, जो डॉट से जुड़े होते हैं: हेडर, पेयलॉड, और सिग्नेचर - हर एक बिना पैडिंग और बिना लाइन ब्रेक एन्कोड होता है। पहले दो हिस्से सादा JSON हैं, यही वजह है कि हर कोई उन्हें पढ़ सकता है, और यही वजह है कि टोकन छूने से पहले हर किसी को आगे पढ़ना चाहिए।
पहले दो हिस्से पढ़ना ऊपर वाले base64url हेल्पर के साथ कुछ ही लाइन का काम है, और टोकन का रहस्योद्घाटन करने का सबसे तेज़ रास्ता भी:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int base64url_decode(const char *url_safe, unsigned char *out,
size_t out_cap, size_t *out_len);
int main(void) {
const char *token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0."
"TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
const char *dot1 = strchr(token, '.');
if (dot1 == NULL) {
return 1;
}
const char *part2 = dot1 + 1;
const char *dot2 = strchr(part2, '.');
if (dot2 == NULL) {
return 1;
}
const char *part3 = dot2 + 1;
char seg[512];
char buf[1024];
size_t n = 0;
size_t hlen = (size_t)(dot1 - token);
memcpy(seg, token, hlen);
seg[hlen] = '\0';
if (base64url_decode(seg, (unsigned char *)buf,
sizeof(buf), &n) == 0) {
printf("header: %.*s\n", (int)n, buf);
}
size_t plen = (size_t)(dot2 - part2);
memcpy(seg, part2, plen);
seg[plen] = '\0';
if (base64url_decode(seg, (unsigned char *)buf,
sizeof(buf), &n) == 0) {
printf("payload: %.*s\n", (int)n, buf);
}
printf("signature: %s (encoded, verify before trusting!)\n", part3);
return 0;
}
प्रिंट होने पर हेडर {"alg":"HS256","typ":"JWT"} होता है और पेयलॉड {"sub":"1234567890","name":"John Doe"}। अब वह हिस्सा जो मायने रखता है: तीसरा हिस्सा सिग्नेचर है, और जो दो हिस्से आपने अभी डिकोड किए, वे न गुप्त हैं, न सत्यापित। किसी के पास पैकेट कैप्चर हो तो वह उन्हें पढ़ सकता है, और किसी के पास टेक्स्ट एडिटर हो तो वह उन्हें फिर से लिख सकता है। C में सिग्नेचर सत्यापित करने से पहले JWT पेयलॉड पर भरोसा करना ही क्लासिक ऑथेंटिकेशन बग है, और Base64 इसे नोटिस न करने में आसान बना देता है - टोकन अटूट ब्लॉब जैसा दिखता है, जबकि वह पोस्टकार्ड है। HS256 टोकन सत्यापित करने के लिए आप <openssl/hmac.h> के HMAC() से अपने सिक्रिट के साथ header.part पर HMAC-SHA256 दोबारा गणित करते हैं, और CRYPTO_memcmp() से कॉन्स्टेंट टाइम में तुलना करते हैं; अगर डायजेस्ट्स मेल न खाएँ, तो टोकन रद्द है, चाहे उसकी दावेदनें कुछ भी हों। C में JWT लाइब्रेरी का कोई दे-फैक्टो स्टैंडर्ड नहीं है, इसलिए प्रोडक्शन के लिए आप या तो वह छोटा सा वेरिफिकेशन स्टेप खुद बनाएँगे या किसी कम्युनिटी लाइब्रेरी को अपनाएँगे - पर इस काम का Base64 हिस्सा ऊपर वाली तोड़ने-और-डिकोड-करने की प्रक्रिया है, और उसे आपको पूरा समझना चाहिए।
Basic Auth: वह हेडर जिसने कभी गोपनीयता सीखी नहीं
वेब पर सबसे पुराना ऑथेंटिकेशन हेडर आज भी Base64 पर सवार है: Authorization: Basic, और उसके बाद username:password की स्टैंडर्ड-वर्णमाला एन्कोडिंग (RFC 7617, जिसे RFC 9110 Basic स्कीम के लिए रेफ़रेंस करता है)। RFC स्पष्ट है कि यह एन्कोडिंग है, संरक्षण नहीं - किसी के पास पैकेट कैप्चर हो तो वह दोनों हिस्से एक ही कमांड में डिकोड कर सकता है - इसलिए C में डिकोड-साइड का काम यह है: हेडर पार्स करें, सख़ती से डिकोड करें, पहले कॉलन पर तोड़ें (पासवर्ड में क़ानूनी तौर पर कॉलन हो सकते हैं), और टाइमिंग-सेफ़ फ़ंक्शन से तुलना करें:
#include <string.h>
#include <openssl/evp.h>
#include <openssl/crypto.h>
static size_t real_length(const char *b64);
int basic_auth_ok(const char *header, const char *expected_user,
const char *expected_pass) {
if (strncmp(header, "Basic ", 6) != 0) {
return 0;
}
const char *b64 = header + 6;
unsigned char out[256];
int n = EVP_DecodeBlock(out, (const unsigned char *)b64,
(int)strlen(b64));
if (n < 0) {
return 0;
}
size_t real = real_length(b64);
size_t u_len = strlen(expected_user);
size_t p_len = strlen(expected_pass);
if (real != u_len + 1 + p_len) {
return 0;
}
if (memcmp(out, expected_user, u_len) != 0) {
return 0;
}
if (out[u_len] != ':') {
return 0;
}
return CRYPTO_memcmp(out + u_len + 1, expected_pass, p_len) == 0;
}
लंबाई की जाँच असली काम करती है: यह उस पेयलॉड को रोकती है जो "alice:secret" में डिकोड होकर आख़िर में कचरा छोड़ता है, या "alice:secre" में कट जाता है, कि वह मैच न कर ले। और CRYPTO_memcmp (या memcmp बस तभी जब आप टाइमिंग के असर को समझते हैं) ही वही चीज़ है जो अटैकर को टाइमिंग नापकर आपकी यूज़र लिस्ट पार करने से रोकती है। यह हेडर HTTPS पर सर्व करें, वरना बिल्कुल न करें - साधारण कनेक्शन पर Base64 की परत बस खिड़की की सजावट है।
ईमेल और PEM: मूल घर
Base64 का जन्म एक बिल्कुल ख़ास समस्या के लिए हुआ था: मेल ट्रांसपोर्ट सिर्फ़ 7-बिट ASCII ले जाता था, और लोग बाइनरी भेजना चाहते थे। MIME (RFC 2045) ने Base64 को स्टैंडर्ड ट्रांसफ़र एन्कोडिंग्स में से एक बनाया और दो घर के नियम जोड़े: एन्कोडेड लाइनें 76 चरों से ज़्यादा लंबी नहीं हो सकतीं, और डिकोडिंग सॉफ्टवेयर को वर्णमाला से बाहर के हर चर को नज़रअंदाज़ करना होगा - लाइन ब्रेक समेत। वही दूसरा नियम यही वजह है कि ऊपर के स्ट्रीमिंग डिकोडर एक रैप अटैचमेंट को बिल्कुल प्री-प्रोसेसिंग के बिना चबा डालते हैं, और यही वजह है कि 76-चर की आदत आज भी पृथ्वी की हर मेल लाइब्रेरी में बसी हुई है। पूर्वज था PEM (Privacy Enhanced Mail, RFC 1421), जो बदले में 64-चर लाइनें इस्तेमाल करता था - जो 64/76 बंटवारा आप टूल्स में देखते हैं, वही उसका इतिहास है, और दोनों सीमाएँ अंततः SMTP ने ही लगाईं।
PEM कवच - वह फॉर्मेट जिसमें कुंजियाँ और सर्टिफ़िकेट यात्रा करते हैं - बस लेबल किया हुआ Base64 है: एक -----BEGIN ... ----- लाइन, 64-चर लाइनों में बॉडी, और मेल कराने वाली END लाइन। C में कवच उतारना बस लाइन-स्कैन है, और फिर बाक़ी काम डिकोडर कर लेता है:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
FILE *f = fopen("server.key", "r");
if (f == NULL) {
return 1;
}
char line[256];
char b64[8192];
size_t pos = 0;
int in_body = 0;
while (fgets(line, sizeof(line), f) != NULL) {
if (strncmp(line, "-----BEGIN", 10) == 0) {
in_body = 1;
continue;
}
if (strncmp(line, "-----END", 8) == 0) {
in_body = 0;
break;
}
if (in_body) {
size_t l = strlen(line);
while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r')) {
l--;
}
memcpy(b64 + pos, line, l);
pos += l;
}
}
fclose(f);
unsigned char der[8192];
int out_len = 0;
if (decode_b64((const unsigned char *)b64, (int)pos,
der, &out_len) != 0) {
printf("armor contained no valid base64\n");
return 1;
}
printf("DER payload decoded\n");
return 0;
}
डिकोड किए बाइट्स DER हैं, एक कॉम्पैक्ट बाइनरी सिरियलाइज़ेशन, और वही चीज़ है जो OpenSSL के सर्टिफ़िकेट और कुंजी फ़ंक्शन अंततः खाते हैं। दो नोट्स: बॉडी को लाइन ब्रेक के बिना इकट्ठा करें (जैसे लूप करता है), ताकि आपकी लंबाई चार का गुणज हो, और अगर फ़ाइल में कई ब्लॉक्स हों, तो END लेबल को उस BEGIN लेबल से मिलाएँ जिसे आपने खोला - अगर आपको बस पहला ब्लॉक चाहिए, जैसे यहाँ, तो एक साधारण फ्लैग काम करता है।
सिक्रिट, कॉन्फ़िग और डेटाबेस कॉलम्स
Base64 एक टेक्स्ट कंटेनर है, और यही वजह है कि यह बार-बार ऐसी जगहों पर दिखाई देता है जहाँ आपकी उम्मीद नहीं होगी। कॉन्फ़िग फ़ाइलों और एनवायरनमेंट वेरिएबल्स में, यह वह ट्रिक है जिससे आप उन मानों को घुसा लेते हैं जो वरना फॉर्मेट को तोड़ देते: सेमीकोलॉन्स वाला डेटाबेस DSN, कोट्स वाला पासवर्ड, न्यूलाइन वाला मान। डेटाबेस में, बाइनरी ब्लॉब Base64 के रूप में TEXT कॉलम में रह सकता है, और हर उस टूल से बच जाता है जो टेक्स्ट की धारणा करता है - हालाँकि करीब एक-तिहाई अतिरिक्त साइज़ की कीमत पर, इसलिए कॉलम्स की साइज़ उसी के हिसाब से तय करें (और यह तो पूछिए ही कि मान BLOB कॉलम में ही क्यों नहीं है)।
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
static size_t real_length(const char *b64) {
size_t len = strlen(b64);
while (len > 0 && b64[len - 1] == '=') len--;
return len * 3 / 4;
}
int main(void) {
const char *b64 = getenv("API_KEY_B64");
if (b64 == NULL) {
printf("API_KEY_B64 is not set\n");
return 1;
}
size_t cap = strlen(b64);
unsigned char *out = malloc(cap);
int n = EVP_DecodeBlock(out, (const unsigned char *)b64, (int)cap);
if (n < 0) {
printf("API_KEY_B64 is not valid base64\n");
free(out);
return 1;
}
size_t real = real_length(b64);
printf("key is %zu bytes\n", real);
free(out);
return 0;
}
सावधानी दो बार लागू होती है। पहली, यह फॉर्मेट-सुरक्षा है, गोपनीयता नहीं: जैसे ही कोई डेवलपर कॉन्फ़िग फ़ाइल पढ़ सकता है, वह एक ही कॉल में मान डिकोड कर सकता है, और RFC के सिक्योरिटी सेक्शन में असली घटनाएँ दर्ज हैं जहाँ लोगों ने सपोर्ट को प्रोटोकॉल एक्सचेंज की शिकायत की और "ग़लती से पासवर्ड सार्वजनिक कर दिया" - क्योंकि Base64 नज़रों में छुपाता है, कम्प्यूटेशनली संरक्षण नहीं करता। कभी सिक्रिट को Base64 बनाकर स्टोर करके उसे एन्क्रिप्टेड मत कहें। दूसरी, स्टार्टअप पर वैलिडेट करें: आधा-पेस्ट एनवायरनमेंट मान सख़्त कॉल से -1 है, और एक-लाइन की जाँच तीन घंटे बाद की रहस्यमयी फेलियर को बूट के समय के एक काम करने लायक़ मैसेज में बदल देती है।
Shell से डिकोडिंग
सारी डिकोडिंग आपके प्रोग्राम के अंदर नहीं होती। CLI स्क्रिप््ट्स, cron जॉब्स, और वन-लाइनर्स बार-बार Base64 डिकोड करते हैं, और C डेवलपर्स को वह दो टूल जानने चाहिए जो हर Linux बॉक्स पर पहले से मौजूद हैं। coreutils का टूल सामान्य-उपयोग वाला है: base64 -d डिकोड करता है, -i उसे कचरे जैसे चरों को फेल होने के बजाय नज़रअंदाज़ करने देता है, और -w रैपिंग कॉलम सेट करता है (जो सिर्फ़ एन्कोडिंग को प्रभावित करता है, डिकोडिंग को नहीं):
base64 -d < blob.b64 > blob.bin
base64 -d -i < messy.b64 > blob.bin
OpenSSL अपना भी शिप करता है, जो openssl base64 के रूप में पहुँचता है (openssl enc -base64 का एक ज़्यादा दोस्ताना एलियस):
openssl base64 -d < blob.b64 > blob.bin
openssl base64 -d -A < blob.b64 > blob.bin
-A फ्लैग का मतलब है "एक ही लाइन": 64-चर रैपिंग के बिना एन्कोड करें, और इनपुट को भी एक ही लाइन की उम्मीद करें। और यहाँ एक CLI फंदा है, जो पढ़े बिना आपका एक संध्या खर्च करवा सकता है: OpenSSL का base64 डिकोड लाइन-ओरिएंटेड है, और ऐसा पेयलॉड जो बिल्कुल लाइन ब्रेक के बिना आए, वह चुपचाप बिल्कुल कुछ नहीं डिकोड होता:
printf 'TQ==' | openssl base64 -d | wc -c # 0
printf 'TQ==\n' | openssl base64 -d | wc -c # 1
coreutils के डिकोडर में वह उम्मीद नहीं है, और यही एक वजह है कि ग्ल्यू काम के लिए यह सुरक्षित डिफ़ॉल्ट है। डायलक्ट पर एक और नोट: BSD-उत्पन्न सिस्टम (खासकर पुराने macOS) पर इतिहास में डिकोड फ्लैग को -D लिखा जाता था; आधुनिक रिलीज़ GNU की -d वाली रस्म का पालन करती हैं, इसलिए उसी मशीन की मैन पेज देख लीजिए जहाँ आप असल में काम कर रहे हैं।
बड़े पेयलॉड, छोटी मेमोरी
डिकोडिंग वह दिशा है जो आपकी मदद करती है: आउटपुट इनपुट का तीन चोटथाई साइज़ होता है, इसलिए Base64 से मेमोरी दबाव दुर्लभ है। फिर भी, जब सैकड़ों मेगाबाइट की .b64 फ़ाइल डिस्क पर उतरती है, तो आगे वाला स्ट्रीमिंग पथ आपके हाथ का औज़ार है, और वह दिखने से आसान है। एन्कोडेड फ़ाइल को चंक्स में पढ़ें, हर चंक को EVP_DecodeUpdate को दें, और डिकोड किए बाइट्स को जैसे-जैसे आएँ लिखें। कॉन्टेक्स्ट किसी भी अधूरे समूह के एक से तीन चर्स को कॉल्स के बीच पकड़े रहता है, इसलिए चंक बाउंड्रीज़ कहीं भी गिर सकती हैं - आपको उन्हें अलाइन करने की ज़रूरत नहीं:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_DecodeInit(ctx);
FILE *in = fopen("huge.b64", "rb");
FILE *outf = fopen("huge.bin", "wb");
char inbuf[65536];
unsigned char outbuf[49152 + 4];
size_t got;
int ok = 1;
while (ok && (got = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
int outl = 0;
int r = EVP_DecodeUpdate(ctx, outbuf, &outl,
(const unsigned char *)inbuf, (int)got);
if (r < 0) {
ok = 0;
} else if (outl > 0) {
fwrite(outbuf, 1, (size_t)outl, outf);
}
}
int tail = 0;
if (ok && EVP_DecodeFinal(ctx, outbuf, &tail) == 1 && tail > 0) {
fwrite(outbuf, 1, (size_t)tail, outf);
}
EVP_ENCODE_CTX_free(ctx);
fclose(in);
fclose(outf);
return ok ? 0 : 1;
}
पीक मेमोरी दो बफ़र्स की क़रीब कुछ दसियों किलोबाइट्स होती है, फ़ाइल साइज़ चाहे जो हो, और ख़राब फ़ाइल तेज़ी से फेल करती है - EVP_DecodeUpdate उसी चंक पर -1 रिटर्न करता है जहाँ नुकसान है, इसलिए आप ऑफ़सेट रिपोर्ट कर सकते हैं, कंधे उछालने के बजाय। इस पथ के लिए एक लाइब्रेरी चेतावनी: APR-Util का डिकोडर NUL-टेर्मिनेटेड स्ट्रिंग्स पर काम करता है, int-साइज़ हिसाब-किताब के साथ (इसका रिटर्न वैल्यू int है, और इनपुट को एक इंटरनल कॉन्स्टेंट से थोड़ा 3 GB से नीचे तक कैप किया गया है), इसलिए मल्टी-गिगाबाइट फ़ाइलों के लिए वह रेस से बाहर है। अगर आपको प्रोग्रेस रिपोर्टिंग चाहिए, तो लिखे गए बाइट्स की गिनती रखें - वह आपकी आउटपुट में पोज़िशन है, और इनपुट पोज़िशन उसका लगभग 4/3 गुना होता है।
फंदे, सब C-ख़ास
एक जगह इकट्ठे, वही फंदे जो ऐसा करने के C-ख़ास हैं:
- ज़ीरो-पैडेड वन-शॉट।
EVP_DecodeBlockक्वांटम लंबाई रिटर्न करता है, डेटा लंबाई नहीं।TQ==तीन बाइट्स रिपोर्ट करता है मगर एक ही उठाता है। आख़िरी पैड्स से असली लंबाई हमेशा दोबारा गणित करें, या स्ट्रीमिंग जोड़ी इस्तेमाल करें। - डिकोड किए बाइट्स स्ट्रिंग नहीं हैं। नतीजे में NUL बाइट्स हो सकते हैं, और वह UTF-8 भी न हो। न
strlen, नprintf("%s"), न उसे ऐसे फ़ंक्शनों को देना जो टेक्स्ट की धारणा करते हैं। (पॉइंटर, लंबाई) हर जगह साथ उठाएँ। - बफ़र साइज़ आपकी ज़िम्मेदारी है। C आपका आउटपुट बफ़र बढ़ाएगा नहीं, और न ही डिकोडर - OpenSSL का अपडेट जो डिकोड करता है वही उतनी जगह में लिखता है जितनी आपने दी है। साइज़
in_len * 3 / 4 + 3रखें (रैप ओवरहेड भी, अगर इनपुट रैप्ड है और आप ऐसे हेल्पर से डिकोड कर रहे हैं जो स्ट्रिप नहीं करता), और हर रैपर में कैप चेक रखें। - साइंड char लुकअप्स। अगर आप कभी खुद डिकोडर लिखें, तो क्लासिक बग है: ऐसी प्लेटफ़ॉर्म पर जहाँ char साइंड है, वहाँ साधारण
charसे इनपुट बाइट को 256-एंट्री टेबल में इंडेक्स के रूप में इस्तेमाल करना: बाइट0xFFबनकर-1हो जाती है और आप मेमोरी में पीछे की तरफ़ इंडेक्स कर बैठते हैं। हमेशाunsigned charयाunsignedवैल्यूज़ से इंडेक्स करें। - खामोश वही ख़तरनाक हैं। APR-Util पहले ही अवैध चर पर रुक जाता है और कुछ नहीं कहता; GLib कचरा छलांग खाना और कुछ नहीं कहता। OpenSSL और Mbed TLS ज़ोर से फेल करते हैं। अगर आपका इनपुट अनट्रस्टेड है, तो लाइब्रेरी की ख़ामोशी आपकी प्रोग्राम की बग है, लाइब्रेरी की नहीं।
- कमांड लाइन न्यूलाइन्स खा जाती है।
openssl base64 -dजीरो बाइट्स डिकोड करता है अगर इनपुट में लाइन ब्रेक न हो। Shell पाइपलाइन्स जो आख़िरी न्यूलाइन्स हटाते हैं (tr -d '\n',xargs, अंतिम न्यूलाइन के बिना एडिटर सेव) ख़ाली आउटपुट देंगे, बिना किसी एरर के। - int बनाम size_t। OpenSSL के वन-शॉट API को
intलंबाई मिलती है, APR-Util हर जगहintइस्तेमाल करता है, और Mbed TLS तथा GLib के APIssize_tइस्तेमाल करते हैं। उनके बीच मिक्स्ड-लंबाई गणित वही जगह है जहाँ साइंड/अनसाइंड वार्निंग्स असली बग छुपाए बैठे हैं - और जहाँ APR की 2 GB की सीमा बसती है। - व्हिटस्पेस एक-जैसा नहीं। OpenSSL कहीं भी सारा व्हिटस्पेस छोड़ देता है; Mbed TLS समूहों के बीच CRLF/LF और ब्रेक के ठीक पहले स्पेस देता है, मगर ब्रेक के बाद या लाइन के बीच में नहीं; CLI टूल्स अलग-अलग हैं। ऐसा पेयलॉड जो एक डिकोडर के लिए वैध है, दूसरे के लिए अवैध हो सकता है, और "मेरी मशीन पर तो चल रहा था" आमतौर पर "मेरा डिकोडर और आलसी था" का मतलब होता है।
अच्छी आदतें, एक जगह
भरोसा करने से पहले वैलिडेट करें: आकार-चेक (वर्णमाला के चर, ज़्यादा से ज़्यादा दो आख़िरी पैड्स) किसी भी डिकोड से पहले उघाड़े कचरे को पकड़ लेता है, मगर सिर्फ़ असली डिकोड ही Base64 के सेमेंटिक्स समझता है, इसलिए अंतिम बोल सख़्त डिकोडर के पास है। ज़ब आपको ईमानदार लंबाई या चंक्ड इनपुट चाहिए, तब OpenSSL की स्ट्रीमिंग जोड़ी इस्तेमाल करें, और जब पेयलॉड छोटा हो और आप तुरंत उसकी लंबाई ठीक कर लेते हैं, तब वन-शॉट। (पॉइंटर, लंबाई) जोड़ियाँ साथ रखें और कभी डिकोड किए बफ़र को स्ट्रिंग फ़ंक्शन से मत मिलने दें। ऑथेंटिकेशन मटेरियल की तुलना CRYPTO_memcmp से करें। फ़ाइल नाम या दावा किए गए MIME टाइप पर भरोसा करने से पहले मैजिक बाइट्स का स्निफ़ करें। और Base64 को वही मानें जो वह है - एक पैकेजिंग फॉर्मेट, बाइट्स के लिए एक छोटा सा बक्सा - लॉक नहीं: इन 64 अक्षरों में से एक भी चीज़ आपका डेटा गोपनीय नहीं बनाती।
C में Base64 का छोटा सा इतिहास
कहानी ईमेल से शुरू होती है। 1990 और 1991 में क्रिप्टोग्राफर्स का एक समूह Privacy Enhanced Mail का ख़ाका तैयार कर बैठा, साइन और एन्क्रिप्टेड ईमेल का सिस्टम, और उन्हें बाइनरी को 7-बिट नेटवर्क से पार ले जाने का तरीका चाहिए था। उनका जवाब, जो 1993 में RFC 1421 के रूप में स्टैंडर्ड बना, डेटा को हर चर में छह बिट्स - "base 64" - में 64-चर लाइनों में एन्कोड करता था, और इम्प्लिमेंटेशन, ज़रूर, C थी। उसी आसपास वेब अपने खुद के MIME के साथ पहुँचा, RFC 1521 (1993) और फिर RFC 2045 (1996), जिसने वही वर्णमाला रखी, लाइन लंबाई को 76 पर ढीला किया, और Base64 को नई इंटरनेट के अटैचमेंट फॉर्मेट बना दिया।
C की स्टैंडर्ड लाइब्रेरी ने पूरा नाव छूट जाने दिया। C89 स्टैंडर्ड 1990 में प्रकाशित हुआ, MIME से तीन साल पहले, और भाषा की कॉमिटी ने उससे आगे कोई Base64 फ़ंक्शन जोड़ा ही नहीं - न C99 में, न C11 में, न C23 (2024 की रिविज़न) में। इसलिए इकोसिस्टम लाइब्रेरियों के चारों ओर बढ़ा: OpenSSL तब से libcrypto में EVP एन्कोड/डिकोड रूटीन्स संभाल रही है जब तक कोई भी TLS के लिए OpenSSL लिंक कर रहा है, Mbed TLS (2015 में PolarSSL से नाम बदला) ने एम्बेडेड सिस्टम्स के लिए एक छोटी सख़्त जोड़ी रखी, APR-Util तब Apache के साथ शिप किया जब सर्वर को अपने खुद के ऑथेंटिकेशन हेडर डिकोड करने पड़े, और GLib ने डेसक्टॉप के लिए अपनी त्रय जोड़ी। स्टैंडर्ड्स ने इम्पलिमेनटेशन्स का पीछा किया: 2003 का RFC 3548 ने पुरानी परिभाषाओं को साफ़-सुथरा किया, और 2006 का RFC 4648 (Base-N Encodings) ने वर्णमालाओं, URL-safe वैरिएंट, और उन सिक्योरिटी नियमों को औपचारिकता दी जिन पर यह आर्टिकल टिका है। उचित ही, उसी RFC के सेक्शन 11 एक ISO C99 रेफ़रेंस इम्प्लिमेंटेशन की तरफ़ इशारा करता है - स्टैंडर्ड का अपना एग्ज़ेमपल डिकोडर C में लिखा है, और यह बात आपको इस फॉर्मेट की बसाहट के बारे में सब कुछ बता देती है।
मज़ेदार बातें, C एडिशन
कुछ C-रस वाली बातें जो बस जानकर मज़ा आता है:
- नाम गणित है, मार्केटिंग नहीं: हर आउटपुट चर बिल्कुल छह बिट्स उठाता है, और 2 की 6 है 64। "Base64" रेडिक्स है, ज़ोर-से पढ़ा हुआ।
- वर्णमाला 65 चरों की है, 64 की नहीं: 64 सिम्बल्स और
=, जिसे RFC 4648 "अतिरिक्त 65वाँ चर" कहता है, एक ख़ास प्रोसेसिंग फ़ंक्शन के लिए इस्तेमाल। पैड मज़दूर है, अक्षर नहीं। - OpenSSL एन्कोडेड आउटपुट को 64 चरों पर रैप करता है (PEM की आदत), जबकि coreutils 76 पर (MIME की आदत)। 12 चरों का फ़र्क़ दो दशकों का मेल इतिहास है, जो आपकी मशीन पर दो कमांड्स के आउटपुट में नज़र आ जाता है।
- GNU coreutils के
base64कमांड के लेखक Simon Josefsson हैं - वही इंसान जिसने RFC 4648 लिखा। स्टैंडर्ड और उसके सबसे ज़्यादा इस्तेमाल होने वाले इम्पलिमेनटेशन्स में से एक का लेखक साझा है, और यही वजह है कि दोनों ने हर एज केस पर एक ही राय रच ली। - Mbed TLS अपनी टेबल लुकअप्स कॉन्स्टेंट-टाइम हेल्पर्स के ज़रिए करती है (
mbedtls_ct_base64_*), इसलिए डिकोड स्पीड यह नहीं रिसाती कि उसने कौन-से चर देखे। एक डिटेल जो आप कभी नोटिस न करें, और जिसके मौजूद होने पर राहत मिले। TQ==सबसे छोटा नॉन्ट्रिवियल पेयलॉड है: एक असली बाइट, दो पैड्स। यह परफ़ेक्ट टेस्ट वेक्टर है - OpenSSL का वन-शॉट डिकोडर इसके लिए तीन बाइट्स रिटर्न करता है, उसका स्ट्रीमिंग डिकोडर एक, Mbed TLS एक, और GLib एक। चार लाइब्रेरियाँ, दो जवाब, और फ़र्क़ ज़ीरो पैडिंग है।- APR के base64 फ़ंक्शन इस आर्टिकल के ही वे हैं जो EBCDIC की परवाह करते हैं, क्योंकि httpd आज भी ऐसी मशीन पर चलता है जहाँ अक्षर ASCII नहीं हैं। C की स्टैंडर्ड लाइब्रेरी ने कभी मेनफ़्रेम से मिलन नहीं किया; APR ने किया।
- ख़ाली पेयलॉड सार्वभौमिक आईडेंटिटी है: हर लाइब्रेरी ज़ीरो-लेंथ इनपुट को ज़ीरो-लेंथ आउटपुट में एन्कोड और डिकोड करती है, बिना किसी एरर के। अगर आपका डिकोडर ख़ाली स्ट्रिंग पर छूट जाए, तो फॉर्मेट की नहीं, बग की बात है।
एन्कोडर की तरफ़ उलटते हुए
वह था डिकोडर वाला हिस्सा, और ज़्यादातर दर्द वहीं रहता है, क्योंकि डिकोडिंग वही जगह है जहाँ आप दूसरों का डेटा मिलते हैं: उनकी पैडिंग की पसंदें, उनकी लाइन ब्रेक्स, उनके ख़राब बाइट्स, उनके टोकन। उल्टी दिशा - बाइट्स को Base64 स्ट्रिंग में बदलना - एक शांत जानवर है, जिसका अपना फंदों का क़िस्सा है: एग्ज़ाक्ट बफ़र गणित, लाइन रैपिंग का सवाल, और वह साइज़ बिल जो हर भेजने वाले को मिलती है। C में Base64 एन्कोडिंग इस पेज से जुड़े आर्टिकल में गहराई से कवर है, और यह इससे वैसे ही जोड़ी बनाता है जैसे डिकोडर एन्कोडर के साथ जोड़ता है: दोनों पढ़ लीजिए और आप कभी किसी भी दिशा से चौंके नहीं।
अंतिम अपडेट: 2026-09-08
संबंधित लेख: C में Base64 एन्कोडिंग: एक सम्पूर्ण गाइड