क्या आपको Base64 फ़ॉर्मेट के साथ काम करना होता है? तो यह साइट आपके लिए एकदम सही है! अपने डेटा को एन्कोड या डिकोड करने के लिए, हमारे बेहद आसान ऑनलाइन टूल का इस्तेमाल करें।

Java में Base64 डिकोडिंग: एक सम्पूर्ण गाइड

यह एक सपोर्ट टिकट में आता है, एक API रिस्पॉन्स में, एक Kubernetes सीक्रेट में, या URL के बीच-बीच में दबा हुआ: अक्षरों और अंकों का लंबा सिलसिला, बीच-बीच में कभी-कभी +, /, - या _, और शायद आख़िर में लटके एक-दो = चर। कोई कहता है कि यह Base64 है और इसमें वह छिपा है जो आपको चाहिए: एक पासवर्ड, एक JSON पेलोड, एक सर्टिफ़िकेट, एक फोटो। यह गाइड इसे वापस लाने की Java रेसिपी है। एक तेज़ ओरिएंटेशन, क्योंकि होम पेज पर फ़ॉर्मैट का पूरा दौर होता है: Base64 हर तीन बाइट्स डेटा को 64-चिह्नों की वर्णमाला से चार चरों में रीवाइट करता है, और आख़िरी चंक छोटा हो तो अंत में एक-दो = पैड जोड़ देता है। डिकोडिंग इस सौदे की सिकुड़ती दिशा है: चार चर अंदर जाते हैं, तीन बाइट्स बाहर आते हैं, इसलिए रिज़ल्ट इनपुट से लगभग एक-चौथाई कम जगह लेता है।

यहाँ हेडलाइन है, और वह अच्छी है। 18 मार्च 2014 से हर JDK स्टैंडर्ड लाइब्रेरी में एक पूरा Base64 टूलकिट साथ ला रहा है: java.util.Base64। कोई डाउनलोड नहीं, कोई Maven कोऑर्डिनेट नहीं, कोई नेटिव लाइब्रेरी नहीं। एक इम्पोर्ट, सात फ़ैक्टरी मेटोड्स, तीन वर्णमालाएँ, और Java 8 से आज के Java 26 तक वही व्यवहार। इस लेख की हर चीज़ उसी एक क्लास पर बनी है।

शुरू करने से पहले एक ईमानदार सीमा: यह कहानी का डिकोडर वाला हिस्सा है। आप सीखेंगे कि जिस वर्णमाला से मुक़ाबला हो रहा है उसके लिए सही डिकोडर कैसे चुनें, JDK के एरर मैसेज डॉक्टर की स्कैन पढ़ने की तरह कैसे पढ़ें, बाइट्स को मोजिबैक के बिना टेक्स्ट में कैसे बदलें, PEM अर्मर को कैसे खोलें, मल्टी-गीगाबाइट पेलोड्स को कैसे स्ट्रीम करें, और वे सुरक्षा फँसाव कैसे पहचानें जो यह फ़ॉर्मैट चुपचाप रास्ते में बिखेर जाता है। दूसरी दिशा, बाइट्स को स्ट्रिंग में पैक करना, अपने ही गाइड में मिलती है, और इस लेख के आख़िर में उसका लिंक है।

आपके पास पहले से क्या है

Java में Base64 इंस्टॉल करना वही एक-लाइन जवाब है जो आप व्हाइटबोर्ड पर देते हैं: "वह JDK में है।" क्लास java.util.Base64 1.8 से java.base मॉड्यूल का हिस्सा है, और 2026 में भी उसका javadoc Since: 1.8 ही लिखता है। आप जो इंस्टॉल करते हैं, सिर्फ़ एक JDK है - किसी भी वेंडर (Oracle, Eclipse Temurin, Amazon Corretto, Zulu) की Java 8 या नई चलेगी, और Debian-बेस्ड बॉक्स पर वह एक ही कमांड है:

sudo apt install openjdk-17-jdk-headless

API एक फ़ैक्टरी है: आप कभी डिकोडर नहीं बनाते; आप क्लास से एक माँगते हैं। सात फ़ैक्टरी मेटोड्स हर दिशा में तीन-तीन शख्सियतें बाँटते हैं, और डिकोडर की तरफ़ ऐसे दिखती है:

फ़ैक्टरी मेटोड वर्णमाला मूड इसे कब हाथ में लें
getDecoder() A-Z a-z 0-9 + / सख़्त: वर्णमाला से बाहर का कोई भी चर ठुकराता है जो डेटा आप खुद बनाते हैं या कंट्रोल करते हैं
getUrlDecoder() A-Z a-z 0-9 - _ सख़्त, URL-सेफ़ वर्णमाला JWTs, टोकन, IDs, URL में जन्म लेने वाली कोई भी चीज़
getMimeDecoder() A-Z a-z 0-9 + / लचिला: हर गैर-वर्णमाला चर को छुड़ा देता है ईमेल, सच में रैप्ड इनपुट, PEM बॉडीज़
getEncoder(), getUrlEncoder(), getMimeEncoder() ऊपर जैसी ही एन्कोडिंग, बहन गाइड का इलाक़ा जब भी आप Base64 पढ़ने की बजाय खुद बना रहे हों

वापस मिले इंस्टेंस की तीन प्रॉपर्टीयाँ याद रखने लायक़ हैं। पहली, वे थ्रेड-सेफ़ हैं: javadoc कहता है कि इंस्टेंस "अनेक एक साथ चलने वाले थ्रेड्स के इस्तेमाल के लिए सुरक्षित" हैं, और सोर्स कोड दिखाता है कि फ़ैक्टरी मेटोड्स हर कॉल पर वही साझा इंस्टेंस वापस करते हैं, इसलिए Base64.getDecoder() == Base64.getDecoder() सच है। एक स्टैटिक फ़ील्ड में एक डिकोडर बनाइए और पूरे सर्विस में साझा कीजिए; आप कुछ कॉपी भी नहीं कर रहे। दूसरी, कॉल के बीच वे स्टेटलेस हैं, इसलिए न कुछ रीसेट करने को है और न किसी चीज़ के चारों ओर सिंक्रनाइज़ करने को। तीसरी, जहाँ बाइट अरेय़ या स्ट्रिंग की उम्मीद हो, null देना कोई कोमल no-op नहीं है: वह NullPointerException है, बिल्कुल वही जो क्लास का javadoc वादा करता है।

पुरानी लाइब्रेरियाँ कोडबेस में अब भी मिलती रहेंगी, इसलिए इस पूरे इलाक़े का एक तेज़ नक्शा। Apache Commons Codec (वर्तमान 1.22.1) 1.0 से अपनी खुद की org.apache.commons.codec.binary.Base64 साथ ला रहा है, जिसका Builder API सख़्त-या-लचिला पॉलिसी, लाइन लेंथ और सेपरेटर को डायल की तरह खोलता है; वह सही टूल सिर्फ़ तब है जब आपको pre-Java-8 JVMs का सपोर्ट करना पड़े या उसके shape-जाँच वाले हेल्पर्स चाहिए हों। Guava com.google.common.io.BaseEncoding देता है, एक समान क्षमता का वीटेरन, जो बिग डेटा स्टैक्स में अभी भी लोकप्रिय है। किसी भी आधुनिक JVM पर चलने वाली चीज़ के लिए java.util.Base64 डिफ़ॉल्ट पसंद है: ज़ीरो डिपेंडेंसी, और कम्युनिटी बेंचमार्क्स बार-बार उसे इस गिरोह का सबसे तेज़ पाते हैं (विस्तार से वह बात परफ़ॉर्मन्स सेक्शन में)।

आपकी पहली स्ट्रिंग डिकोड कीजिए

डिकोडिंग की ज़िंदगी का नब्बे फ़ीसदी हिस्सा एक मुट्ठी भर लाइनों में समा जाता है। यहाँ पूरी रस्म है, उसी सबसे छोटे उदाहरण से जो RFC खुद वर्णमाला समझाने के लिए इस्तेमाल करता है:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class FirstDecode {
  public static void main(String[] args) {
    byte[] bytes = Base64.getDecoder().decode("TWFu");
    String text = new String(bytes, StandardCharsets.UTF_8);
    System.out.println(text); // Man
  }
}

अब जो हुआ, उसके बारे में चार वाक्य। पहला, एंट्री पॉइंट क्लास नहीं, इंस्टेंस है: decode() उस Base64.Decoder ऑबजैक्ट पर बसता है जो आपको फ़ैक्टरी से मिला। दूसरा, और यह पूरे API का सबसे ज़रूरी डिज़ाइन फैसला है, रिज़ल्ट बाइट अरेय़ है, कभी String नहीं। पेलोड एक वाक्य हो सकता है, एक JPEG, या एक हैश, और यह पता चलने से पहले कि आपके पास क्या आया है, इनमें से किसी एक को भी दूसरे जैसा नहीं माना जाना चाहिए, इसलिए JDK जान-बूझ कर बाइट्स पर रुक जाता है। तीसरा, बाइट्स से टेक्स्ट की छलांग एक अलग, जान-बूझ कर किया गया कदम है जिसमें एक्सप्लिसिट कैरेक्टर सेट होता है, और उसी कदम पर अदब न रखने से "café" मोजिबैक बन जाता है; नीचे का कैरेक्टर सेट सेक्शन बस इसी के लिए है। चौथा, ख़ाली स्ट्रिंग एक फर्स्ट-क्लास वैल्यू है: Base64.getDecoder().decode("") आपको ज़ीरो-लेंथ अरेय़ देता है, न कोई एक्ससेप्शन, न कोई शोर।

दिमाग़ के लिए टेस्ट डेटा याद रखिए: TWFu स्टैंडर्ड का अपना स्मोक टेस्ट है: अगर आपका डिकोड कोड उसे Man में बदल दे, तो मशीन ईमानदार है। दूसरी दिशा का राउंड ट्रिप वही API की दो लाइन हैं, और उन्हें पूरा इलाज उस एन्कोडिंग गाइड में मिलेगा जो इस लेख के आख़िर में लिंक है।

डिकोडर की कतार

Java आपको एक ही डिकोडर नहीं देता; तीन देता है, और इन तीनों के बीच का फ़र्क़ एक पॉलिसी फैसला है - कौन-सी वर्णमाला स्वीकारेंगे और उतनी गंदगी सहेंगे। तीनों वही एक नेस्टेड क्लास Base64.Decoder के इंस्टेंस हैं। क्लास का javadoc हर मूड का फ़र्क़ एक-एक वाक्य में पढ़ कर सुनाता है। बेसिक और URL-safe डिकोडर के लिए: डिकोडर "base64 वर्णमाला से बाहर के चरों वाले डेटा को ठुकरा देता है"। MIME डिकोडर के लिए: "डिकोडिंग ऑपरेशन में लाइन सेपरेटर्स और base64 वर्णमाला टेबल में न मिलने वाले बाकी सभी चरों को अनदेखा किया जाता है"। वह दूसरा वाक्य पूरी MIME कहानी एक लाइन में है, और उसके दांत हैं, क्योंकि "अनदेखा" का मतलब है वर्णमाला-चर न होने वाली हर चीज़, सिर्फ़ लाइन ब्रेक्स नहीं।

चुनने का नियम छोटा है। डिफ़ॉल्ट में getDecoder() पर रहिए। अगर वैल्यू URL, टोकन, या ऐसे API से आई है जो "URL-safe" वादा करता था, तो getUrlDecoder() पर चले जाएँ। सिर्फ़ तब जब आपको सच में MIME-शक्ल का इनपुट उम्मीद हो (हर 76 चरों पर लाइन ब्रेक, सीधे मेल सिस्टम से आया) तब getMimeDecoder() हाथ में लें। शक हो तो सख़्त चुनिए: सख़्त डिकोडर का काम है कि सरप्राइज़ फेल हों, और ट्रास्ट बाउंड्री पर बस वही चाहिए। दूसरी तरफ़, लचिला डिकोडर कोरप्शन की लेंस है: बिखरे चरों वाली स्ट्रिंग ऐसे कुछ में डिकोड हो जाएगी जो सही-सही दिखे पर ग़लत हो, और कोई एरर भी नहीं मिलेगा।

डिकोडर की शिकायतें पढ़ना

सख़्त डिकोडर जोर से फेल होते हैं, और फेल होते हैं सटीक रूप से। हर ख़राब इनपुट IllegalArgumentException थ्रो करता है जिसका मैसेज बिल्कुल बताता है कि क्या ग़लत हुआ, इसलिए पहली बार जब किसी प्रोडक्शन स्ट्रिंग का ख़ाका उड़ जाए, तब आप यही टेबल पढ़ते हैं। नीचे के मैसेज वर्तमान JDK के बिल्कुल उसी शब्दों वाले हैं:

इनपुट (जहाँ नोट न किया हो, getDecoder को) क्या ग़लत है सटीक मैसेज
"SGVs bG8s" एक स्पेस घुस आया Illegal base64 character 20
"SGVs\nbG8s" एक लाइन ब्रेक घुस आया Illegal base64 character a
"SGVs$bG8s" डॉलर चिह्न वर्णमाला में नहीं है Illegal base64 character 24
"SGVsbG8-" स्टैंडर्ड डिकोडर में URL-safe डैश Illegal base64 character 2d
"ab+c" को getUrlDecoder() URL-safe डिकोडर में प्लस चिह्न Illegal base64 character 2b
"S" एक सिंबल अकेले बाइट नहीं बना पाता Input byte[] should at least have 2 bytes for base64 bytes
"SG=VsbG8s" डेटा के बीच में पैडिंग Input byte array has wrong 4-byte ending unit
"Zm8==" एक पैड की जगह दो पैड Input byte array has incorrect ending byte at 4
"Z=" एक चर के बाद पैड Last unit does not have enough valid bits
"SGVsbG8sIHdvcmxkIQ==xx" पैड के बाद कचरा Input byte array has incorrect ending byte at 20

मैसेज में वह hex नंबर दोषी चर का बाइट वैल्यू है, जो Integer.toString(byte, 16) से प्रिंट होता है: 20 स्पेस है, a लाइन फीड, d कैरियाज रिटर्न, 24 डॉलर चिह्न, 2d URL-safe डैश, 2b प्लस, 2f स्लैश, 5f अंडरस्कोर। दो अजीबियाँ जेब में रख लीजिए। पहली, मैसेज नेगेटिव भी हो सकता है: डिकोडर को é वाली स्ट्रिंग खिलाने पर वह Illegal base64 character -17 की शिकायत करता है, क्योंकि वह चर सबसे पहले Latin-1 बाइट 0xE9 में मैप होता है, जो साइनड Java बाइट के रूप में माइनस 23 है, और माइनस 23 का hex है माइनस 17। आपका एरर लॉगर, थोड़ी देर के लिए, साइनड अंकगणित कर रहा है। दूसरी, पोज़िशन: incorrect ending byte at N परिवार में N उस पहले बाइट का ज़ीरो-बेस्ड इंडेक्स है जिसका अर्थ डिकोडर न निकाल पाया, और यह तोहफ़ा है जब आप कोरप्टेड पेलोड को आधा-आधा कर के जाँच रहे हों।

एक कॉस्ट्यूम बदलाव जान लेना: जब डिकोडिंग रैप्ड स्ट्रीम के रास्ते होती है (wrap(InputStream) वैरिएंट, जो नीचे कवर है), तो वही मसले 0x प्रीफिक्स के साथ IOException की शक्ल में सामने आते हैं: Illegal base64 character 0x20 (वर्तमान JDKs; JDK 8 का स्ट्रीम डिकोडर बाइट की जगह लुकअप वैल्यू, -1, प्रिंट करता है)। वही मुसीबत, अलग एक्ससेप्शन, हल्की अलग स्पेलिंग। और लचिला MIME डिकोडर, बेशक, इनमें से किसी की शिकायत नहीं करता: वह बस छुड़ा देता है। यह ही लचिले मूड की कीमत है।

पैडिंग के नियम

बाहर की दुनिया में हर Base64 स्ट्रिंग पैडिंग के बारे में एक चुपचाप वादा करती है, और Java का वादा असामान्य रूप से दोस्ताना है। डिकोडर का javadoc बिल्कुल यही कहता है: पैडिंग चर = "स्वीकार किया जाता है और एन्कोडेड बाइट डेटा के अंत के रूप में पढ़ा जाता है, लेकिन वह ज़रूरी नहीं है"। दो या तीन चरों वाला आख़िरी यूनिट ऐसे डिकोड होता है जैसे पैड हो चुका हो, और जब पैड मौजूद हों तो उनकी संख्या ठीक सही जितनी होनी चाहिए। वर्तमान JDK का व्यवहार क्लासिक उदाहरणों के आसपास:

इनपुट रिज़ल्ट
"" ख़ाली बाइट अरेय़, कोई एरर नहीं
"Zm8" "fo", पैडिंग बस मौजूद ही नहीं
"Zm8=" "fo", कैनोनिकल स्पेलिंग
"Zm8==" IllegalArgumentException: incorrect ending byte at 4
"Zm9v=" IllegalArgumentException: wrong 4-byte ending unit
"Zg==" "f", एक बाइट
"Z=" IllegalArgumentException: last unit does not have enough valid bits
"AA==" बिल्कुल एक बाइट, NUL बाइट 0x00
"AAAA" तीन NUL बाइट्स

वह टेबल दो बार पढ़िए। ख़ाली स्ट्रिंग में कुछ नहींं डिकोड होता, जबकि AA== एक ही NUL बाइट में: Base64 में "कुछ नहीं" और "ज़ीरो" दो अलग जानवर हैं, और दोनों बिल्कुल वैध इनपुट हैं। और पैडिंग, जब मौजूद हो, सटीक होनी चाहिए: Zm8= सही है, Zm8== ग़लत है, Zm9v= ग़लत है, और स्ट्रिंग के बीच में कोई पैड भी ग़लत है। अपने प्रोटोकॉल के लिए प्रैक्टिकल नतीजा: एक स्पेलिंग चुनिए (पैडेड या बिना पैडिंग) और दोनों सिरों पर उसी का पालन करवाइए, क्योंकि दो स्पेलिंग्स में आकर पहुँचने वाली वैल्यू वही वैल्यू है जो कहीं नीचे किसी बेबुझ समानता जाँच को तोड़ सकती है।

base64url: URL के लिए बनी वर्णमाला

स्टैंडर्ड Base64 अपनी वर्णमाला के अंत में + और / रखता है, और वही दो चर हैं जो URL में सही नहीं बैठते: क्वरी स्ट्रिंग में + तो Java तक पहुँचने से पहले ही स्पेस बन चुका होता है, / पाथ सेपरेटर है, और लटका हुआ = तीन-चर के राक्षस में परसेंट-एन्कोडिंग की तलाश करता है। RFC 4648 सेक्शन 5 हल खींचता है: URL- और फ़ाइल-नाम-सेफ़ वर्णमाला, जहाँ + बनता है -, / बनता है _, और आख़िर वाला = पैडिंग आमतौर पर तब गिरा दिया जाता है जब लेंथ बिना बताए ही पता हो जाए। RFC नाम के लिए कठोर है: इस एन्कोडिंग को "base64 एन्कोडिंग से एक जैसा नहीं माना जाना चाहिए"। आप इसे base64url के रूप में मिलेंगे, और यही वह जगह है जहाँ JSON Web Tokens, OAuth state पैरामीटर, API सेशन IDs, और ग्यारह-चर वाले वीडियो IDs सब बसे हैं।

वेब पर सबसे मशहूर base64url पेलोड JWT है, और एक के अंदर झाँकना तीन लाइन का काम है। टोकन के हिस्से रिवाज़ से बिना पैडिंग के होते हैं, और URL डिकोडर उससे खुश रहता है, क्योंकि पैडिंग स्वीकार होती है लेकिन ज़रूरी नहीं - याद रखिए:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class JwtPeek {
  public static void main(String[] args) {
    String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
      + ".eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
      + ".SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
    String[] parts = token.split("\\.");
    byte[] header = Base64.getUrlDecoder().decode(parts[0]);
    byte[] payload = Base64.getUrlDecoder().decode(parts[1]);
    System.out.println(new String(header, StandardCharsets.UTF_8));
    // {"alg":"HS256","typ":"JWT"}
    System.out.println(new String(payload, StandardCharsets.UTF_8));
    // {"sub":"1234567890","name":"John Doe","iat":1516239022}
  }
}

यहाँ दो ईमानदार चेतावनियाँ बसी हैं। पहली, JWT का डिकोड करना झाँकना है, भरोसा नहीं: तीसरा हिस्सा एक सिग्नेचर है, और जो दो हिस्से आपने अभी पढ़े वे न गुप्त हैं और न ऑथेंटिकेटेड। सिग्नेचर सत्यापित करने से पहले पेलोड पर भरोसा करना क्लासिक JWT बग है, और सुधार है कि सत्यापन का काम JJWT (0.13.0) या nimbus-jose-jwt (10.9.1) जैसी JOSE लाइब्रेरी को सौंपें, न कि खुद की क्राइप्टोग्राफी हाथ से गढ़ें। दूसरी, एरर दिशा बताते हैं: स्टैंडर्ड-वर्णमाला वाली स्ट्रिंग getUrlDecoder() को खिलाने पर Illegal base64 character 2b या 2f मिलता है, और उल्टे में 2d या 5f। वर्णमाला का मिल न पाना बाहर की दुनिया में Base64 डिकोड फेल होने का सबसे आम एक कारण है, और एरर मैसेज पलक झपकने में उसी की तरफ़ इशारा करता है। अगर क्वरी स्ट्रिंग में कोई टोकन होना चाहिए था स्टैंडर्ड Base64, तो उसके + और / शायद ट्रांसपोर्ट ने तोड़-फोड़ कर दिए पहले आप तक पहुँचने से, और डिकोड एरर आपको बता रहा है कि बग ऊपर की तरफ़ है, आपके डिकोडर में नहीं।

बाइट्स से शब्दों तक

इस लेख की हर डिकोड कॉल जान-बूझ कर बाइट्स पर रुक जाती है, क्योंकि Base64 एक बाइट फ़ॉर्मैट है, ख़त्म। "वह टेक्स्ट क्या था?" का जवाब देने वाला आप हैं, और आधुनिक डिफ़ॉल्ट जवाब है UTF-8। हालाँकि API की डिकोडिंग तरफ़ एक कैरेक्टर सेट की बारीकियाँ हैं जो लोगों को चौंकाती हैं, इसलिए यहाँ वह है। decode(String) ओवरलोड आपकी स्ट्रिंग को UTF-8 के रूप में नहीं समझता। javadoc बिल्कुल यही कहता है: एक कॉल "decode(src.getBytes(StandardCharsets.ISO_8859_1)) को कॉल करने से बिल्कुल उसी असर वाली है"। यह बग नहीं है - यह एक ट्रिक है: Base64 वर्णमाला साफ़ ASCII है, इसलिए स्ट्रिंग को Latin-1 के रास्ते मैप करने से डिकोडर को बिल्कुल वही बाइट्स ज़ीरो कन्वर्ज़न लागत में मिल जाते हैं, और इनपुट में कोई भी गैर-ASCII चर बस एक ग़ैर-वैध सिंबल बन जाता है जो सख़्त डिकोडर ठुकरा देता है (एरर मैसेजों में वे नेगेटिव hex नंबर यहीं से आते हैं)।

पेलोड का कैरेक्टर सेट एक बिल्कुल अलग फैसला है, new String(bytes, charset) वाले कदम पर लिया जाने वाला। यहाँ क्लासिक केस है: "café" UTF-8 में पाँच बाइट्स 63 61 66 C3 A9 है, जो एन्कोड होने पर Y2Fmw6k= बनता है:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class CharsetDecode {
  public static void main(String[] args) {
    byte[] packed = Base64.getDecoder().decode("Y2Fmw6k=");
    System.out.println(new String(packed, StandardCharsets.UTF_8));
    // café, एक्सेंट बच गया
    System.out.println(new String(packed, StandardCharsets.ISO_8859_1));
    // caf के बाद मोजिबैक, UTF-8 बाइट्स को Latin-1 समझ लिया गया
  }
}

वह दूसरी लाइन वह फेल मोड है जो तुरंत पहचानना चाहिए: UTF-8 पेलोड को Latin-1 से पढ़ा जा रहा है, जिससे बनी स्ट्रिंग ठीक एक चर ज़्यादा लंबी और एक बाइट कम है। इलाज हमेशा वही है: प्रोड्यूसर के साथ एक कैरेक्टर सेट तय करिए और उसे एक्सप्लिसिट पास कीजिए। और एक्सप्लिसिट पास कीजिए कोड में, सिर्फ़ दिमाग़ में नहीं: बिना-एरगुमेंट new String(bytes) कन्स्ट्रक्टर प्लेटफ़ॉर्म डिफ़ॉल्ट कैरेक्टर सेट इस्तेमाल करता है, जो Windows सर्वर पर Cp1252 हो सकता है और पुराने Linux पर जो मशीन को मज़ा आए। JDK 18 (JEP 400, "डिफ़ॉल्ट रूप में UTF-8") से डिफ़ॉल्ट हर प्लेटफ़ॉर्म पर UTF-8 है, इसलिए आधुनिक JVM पर बिना-एरगुमेंट फ़ॉर्म बस सही होता है, लेकिन आपका कोड फिर भी वह कहना चाहिए, क्योंकि जो अगला व्यक्ति इसे पढ़ेगा उसे डिफ़ॉल्ट क्या है यह पता होने की ज़रूरत नहीं रहनी चाहिए। और जब पेलोड टेक्स्ट ही न हो, तो वही कोड बस अलग अंत लेकर चले: बाइट्स अंदर, बाइट्स बाहर, बिल्कुल आख़िरी कदम तक।

जब पेलोड एक फ़ाइल हो

फ़ाइलों की सबसे आम नौकरी किसी एक्सपोर्ट प्रक्रिया का उल्टा है: एक .b64 टेक्स्ट फ़ाइल आती है, और आपको मूल फ़ाइल वापस चाहिए। सख़्त डिकोडिंग के साथ यह पहले से प्रोडक्शन-शक्ल का है:

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class DecodeFile {
  public static void main(String[] args) throws Exception {
    byte[] packed = Files.readAllBytes(Paths.get("payload.bin.b64"));
    byte[] raw = Base64.getDecoder().decode(packed);
    Files.write(Paths.get("payload.bin"), raw);
  }
}

इस रास्ते में कहीं भी फ़र्क़ नहीं पड़ता कि पेलोड टेक्स्ट फ़ाइल है, ZIP आर्काइव, या वीडियो: byte[] बस बाइट्स हैं। साइज़ की गणना भी आपके पक्ष में काम करती है: डिकोडेड आउटपुट एन्कोडेड इनपुट की लंबाई का तीन-चौथाई होता है, इसलिए डिकोडिंग कभी मेमोरी को बदतर नहीं करती, और कई-सौ मेगाबाइट की एन्कोडेड फ़ाइल दोनों में छोटी वह है। अच्छी आदत यह है कि किसी भी लेबल पर भरोसा करने से पहले बाइट्स को खुद की पहचान करने दीजिए। PNG के पहले आठ बाइट्स हमेशा मैजिक नंबर 89 50 4E 47 0D 0A 1A 0A होते हैं, यानी आप जिस हर Base64-एन्कोडेड PNG से कभी मिलेंगे वह वही प्रीफिक्स, iVBORw0K, से शुरू होगी: अगर कोई पेलोड "दावा" करता है कि वह इमेज है और ऐसे नहीं शुरू होता, तो कुछ तो पहले से ग़लत है।

अगर डिस्टिनेशन बफ़र आपका ही है, तो दो-अरेय़ वाला ओवरलोड सीधे उसमें लिखता है और बिल्कुल सटीक बताता है कि कितनी बाइट्स उतरीं, बिना किसी बीच के अलॉकेशन के:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class DecodeInto {
  public static void main(String[] args) {
    byte[] src = "SGVsbG8sIHdvcmxkIQ==".getBytes(StandardCharsets.ISO_8859_1);
    byte[] dst = new byte[16];
    int written = Base64.getDecoder().decode(src, dst);
    System.out.println(written); // 13
    System.out.println(new String(dst, 0, written, StandardCharsets.UTF_8));
    // Hello, world!
  }
}

उस ओवरलोड पर एक तेज़ कोना है, जो javadoc में दस्तावेज़ीकृत है: अगर डिस्टिनेशन छोटा पड़ जाए, तो एक बाइट तक नहीं लिखी जाती और आपको IllegalArgumentException: Output byte array is too small for decoding all input bytes मिलता है। बफ़र का साइज़ उसी आसान गणना से तय कीजिए, लगभग 3 * n / 4 माइनस पैडिंग, और एक्ससेप्शन कभी अपना चेहरा नहीं दिखाएगा। साथ में एक ByteBuffer ओवरलोड भी है जो एक नया बफ़र वापस करता है जिसका limit डिकोडेड लेंथ पर सेट होता है, जब आपका पाइपलाइन NIO में बसा है तो काम आता है।

वायर से: हेडर्स, JSON और डेटा URIs

Base64 Java से सबसे ज़्यादा नेटवर्क की किनारे पर मिलता है। तीन शक्लों को एक-एक काम किया हुआ उदाहरण मिलने लायक हैं।

शक्ल एक: HTTP Basic ऑथेंटिकेशन हेडर। वेब का सबसे पुराना ऑथेंटिकेशन हेडर अभी भी Base64 पर सफ़र करता है। RFC 7617 के मुताबिक, Basic रिक्वेस्ट Authorization: Basic भेजता है और उसके बाद username:password का Base64 एन्कोडिंग, और RFC स्पष्ट है कि यह एन्कोडिंग है, रक्षा नहीं: पैकेट कैप्चर वाले किसी भी हाथ में दोनों आधे एक ही कीस्ट्रोक में पढ़ लेने लायक होते हैं। RFC का अपना उदाहरण, QWxhZGRpbjpvcGVuIHNlc2FtZQ==, Aladdin:open sesame में डिकोड होता है। सर्वर साइड पर हेडर पार्स करना कुछ लाइन का काम है:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class BasicAuth {
  public static String[] credentials(String header) {
    if (header == null || !header.startsWith("Basic ")) {
      return null;
    }
    byte[] packed = header.substring(6).getBytes(StandardCharsets.ISO_8859_1);
    byte[] raw = Base64.getDecoder().decode(packed);
    String userPass = new String(raw, StandardCharsets.UTF_8);
    int colon = userPass.indexOf(':');
    if (colon < 0) {
      return null;
    }
    return new String[] {userPass.substring(0, colon), userPass.substring(colon + 1)};
  }
}

दो बारीकियाँ इसे सुरक्षित रखती हैं। पहले कॉलन पर बंटना ज़रूरी है, क्योंकि पासवर्ड में कानूनी तौर पर अपने कॉलन भी हो सकती हैं। और डिकोडेड पासवर्ड की तुलना आपके संरक्षित वैल्यू से कॉन्स्टेंट-टाइम होनी चाहिए: दोनों वैल्यूज़ को SHA-256 से हैश कीजिए और डायजेस्ट्स की तुलना MessageDigest.isEqual से कीजिए, कभी सादा equals नहीं जिसके समय मापने से हमलावर यूज़र-लिस्ट बना ले। इसे सिर्फ़ HTTPS के ऊपर सर्व कीजिए; सादे कनेक्शन पर Base64 परत सिर्फ़ सजावट है।

शक्ल दो: JSON के अंदर बायनेरी। आधुनिक APIs का बड़ा हिस्सा JSON के अंदर बायनेरी को Base64 टेक्स्ट की शक्ल में एम्बेड करता है: फ़ाइल अपलोड एंडपॉइंट्स, कंटेंट APIs, सिक्रेट स्टोर, वेबहुक्स - सब यही करते हैं, क्योंकि रॉ बाइट्स JSON स्ट्रिंग के एस्केपिंग नियमों को तोड़ देते। पैटर्न हमेशा वही है: फ़ील्ड एक सादी स्ट्रिंग की शक्ल में आती है, और आप उसे बाउंड्री पर डिकोड करते हैं, अपने डोमेन ऑबजैक्ट्स के अंदर नहीं:

import java.util.Base64;
public class ApiField {
  public static void main(String[] args) {
    // Parsed JSON इस साथ लाया था:  "content" : "iVBORw0KGgoAAA..."
    String field = "iVBORw0KGgo=";
    byte[] image = Base64.getUrlDecoder().decode(field);
    // कुछ APIs स्टैंडर्ड Base64 की ही बात करती हैं। स्पेक पढ़िए,
    // फिर उसी के मुताबिक getDecoder() या getUrlDecoder() चुनिए।
    System.out.println(image.length); // 8
  }
}

यहाँ फँसाव डिकोडिंग नहीं है; स्पेक पढ़ना है। कुछ APIs को पैडेड स्टैंडर्ड Base64 चाहिए, कुछ को बिना पैडिंग का base64url, और कुछ दोनों पर लचिले हैं। जब स्पेक चुप हो, तो सबसे सस्ता ठीक है दूसरी तरफ़ का एक उदाहरण वैल्यू देखना: वैल्यू में कहीं भी - या _ मिल जाए तो वर्णमाला तय हो जाती है, और आख़िर में = पैडिंग तय करता है।

शक्ल तीन: डेटा URI। कोई फ़ॉर्म में इमेज पेस्ट करता है और फ़्रंट एंड आपको पूरा डेटा URI सौंप देता है: data:image/png;base64,iVBORw0KGgo...। RFC 2397 शक्ल तय करता है: data:, वैकल्पिक मीडिया टाइप, वैकल्पिक ;base64 फ़्लैग, एक कॉमा, और फिर डेटा। फ़्लैग मौजूद हो तो पेलोड Base64 है; गायब हो तो पेलोड परसेंट-एन्कोडेड सादा टेक्स्ट है, कम मिलता है पर क़ानूनी है। मीडिया टाइप छोड़ा गया हो तो डिफ़ॉल्ट है text/plain;charset=US-ASCII। एक को बंटाना सीधा-सादा है:

import java.util.Base64;
public class DataUri {
  public static void main(String[] args) {
    String uri = "data:image/png;base64,iVBORw0KGgo=";
    int comma = uri.indexOf(',');
    String meta = uri.substring(5, comma);
    String payload = uri.substring(comma + 1);
    boolean isBase64 = meta.endsWith(";base64");
    String mime = isBase64 ? meta.substring(0, meta.length() - 7) : meta;
    byte[] raw = Base64.getDecoder().decode(payload);
    System.out.println(mime + " -> " + raw.length + " bytes");
    // image/png -> 8 bytes
  }
}

इस फ़ॉर्मैट में दो फँसाव बसे हैं। पहला: ;base64 फ़्लैग का गायब होना: फ़्लैग के बिना वैध डेटा URI परसेंट-एन्कोडेड पेलोड ढोता है, और उसे Base64.getDecoder() से गुज़ारने पर थ्रो हो जाता है। दूसरा: दावा किया हुआ मीडिया टाइप: वह भेजने वाले की हिंट है, तथ्य नहीं, इसलिए "png" की शक्ल में रखने से पहले अपने डिकोडेड हुए बाइट्स के मैजिक बाइट्स जाँचिए। और RFC की अपनी सलाह याद रखिए कि data URIs छोटी वैल्यूज़ के लिए हैं; URL के अंदर मल्टी-मेगाबाइट इमेज गंध है, पैटर्न नहीं।

ईमेल, MIME और PEM अर्मर

Base64 ईमेल के लिए जन्मा था, और ईमेल-शक्ल का Base64 अब भी Java प्रोग्राम्स में बार-बार आता है। MIME स्टैंडर्ड (RFC 2045) ने Base64 को बायनेरी ट्रांसफर एन्कोडिंग्स में से एक बनाया और दो घर के नियम जोड़े: एन्कोडेड लाइन 76 चरों से बड़ी नहीं हो सकती, और डिकोडर को वर्णमाला से बाहर के हर चर को अनदेखा करना होगा, लाइन ब्रेक्स समेत। सख़्त डिकोडर पहली लाइन ब्रेक तक ठुकरा देते हैं; getMimeDecoder() बस इसी इनपुट के लिए बना है:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class MimeDecode {
  public static void main(String[] args) {
    String wrapped = "SGVs\nbG8s\r\nIHN0\nYW5kYXJk";
    byte[] bytes = Base64.getMimeDecoder().decode(wrapped);
    System.out.println(new String(bytes, StandardCharsets.UTF_8));
    // Hello, standard
  }
}

यह काम करता है, और आपको उस फँसाव का पता भी होना चाहिए, क्योंकि उस फँसाव के दांत हैं। लचिला डिकोडर "लाइन ब्रेक्स अनदेखा" नहीं करता; वह अपनी वर्णमाला में न होने वाला हर चीज़ अनदेखा करता है। अगर किसी स्टैंडर्ड Base64 स्ट्रिंग में बिखरे चरों की कोरप्शन घुस जाए, तो कचरा गायब हो जाता है और बाकी कुछ ऐसे में डिकोड होता है जो सही-सही दिखे, इसलिए getMimeDecoder() हाथ में लें सिर्फ़ तब जब आपको सच में MIME-शक्ल का इनपुट उम्मीद हो।

MIME का बाहर-दुनिया वाला भाई है PEM अर्मर, वही -----BEGIN CERTIFICATE----- का कारोबार जो सर्टिफ़िकेट्स और कुंजियों को रैप करता है। यहाँ फँसाव है: अर्मर की लाइनें साधारण वर्णमाला-चरों से भरी हैं। "BEGIN CERTIFICATE" के अक्षर बस Base64 के अक्षर हैं, इसलिए पूरा PEM ब्लॉक, अर्मर समेत, खिलाने पर अर्मर भी डेटा की तरह डिकोड हो जाता है। अर्मर खुद छील लीजिए, फिर नंगी बॉडी को डिकोडर को सौंपिए:

import java.util.Base64;
public class PemDecode {
  public static void main(String[] args) {
    String pem = "-----BEGIN CERTIFICATE-----\n"
      + "TUlJQm96Q0NBVWlnQXdJQkFnSUpBSXBhVDJUaVFvZU1BMEdDU3FHU0liM0RRRUE9\n"
      + "-----END CERTIFICATE-----\n";
    String body = pem.replaceAll("(?m)^-----.*$", "").replaceAll("\\s", "");
    byte[] der = Base64.getDecoder().decode(body);
    System.out.println(der.length); // DER बॉडी, अर्मर छोड़कर
  }
}

PEM रिवाज़ से हर लाइन 64 चरों पर रैप होता है (MIME 76 पर), और एक बार व्हाइटस्पेस चला जाए तो सख़्त डिकोडर और MIME डिकोडर रिज़ल्ट पर सहमत हो जाते हैं। सख़्त वाला इस्तेमाल कीजिए: सरप्राइज़ कम-से-कम इतनी सभ्यता रखता है कि थ्रो कर दे। गंदे पर स्टैंडर्ड केस के लिए क्लासिक रेसिपी है: ज्ञात व्हाइटस्पेस छीलकर सख़्त इंस्टेंस से डिकोड कीजिए, ताकि कोई बचा-खुचा कचरा कोरप्टेड सर्टिफ़िकेट की जगह IllegalArgumentException कमाए।

कॉन्फ़िग, एनवायरनमेंट और डेटाबेस वैल्यूज़

Base64 एक टेक्स्ट कंटेनर है, इसलिए वह जगहों पर दिखता है जहाँ आपकी उम्मीद ही नहीं। डेटाबेस में, बायनेरी ब्लॉब (फ़ाइल, आइकॉन, सेरियलाइज़्ड संरचना) TEXT कॉलम में Base64 की शक्ल में बस सकता है, हर उस टूल के हाथ बचकर जो टेक्स्ट मान लेता है; संरक्षित वैल्यू मूल से करीब एक-तिहाई बड़ी उम्मीद कीजिए और कॉलम का साइज़ उसी के हिसाब से तय कीजिए। कॉन्फ़िग फ़ाइलों और एनवायरनमेंट वेरिएबल्स में, Base64 वह ट्रिक है जिससे ऐसे वैल्यूज़ गुप्त ढंग से ढोले जाते हैं जो वरना फ़ॉर्मैट को तोड़ देते: सेमीकॉलन वाला DSN, कोट्स वाला पासवर्ड, मल्टी-लाइन सर्टिफ़िकेट। बूट पर डिकोड करना ही सारा काम है:

import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class ConfigDecode {
  public static void main(String[] args) {
    String value = System.getenv("DB_DSN_B64");
    if (value == null) {
      return;
    }
    byte[] raw = Base64.getDecoder().decode(value);
    String dsn = new String(raw, StandardCharsets.UTF_8);
    // dsn कुछ ऐसा हो सकता है: pg:host=db;password=qu"ote
  }
}

यहाँ वही सावधानी दो बार लगती है। पहली, यह फ़ॉर्मैट-सुरक्षा है, रहस्यता नहीं: जैसे ही कोई डेवलपर कॉन्फ़िग फ़ाइल पढ़ता है, वह एक कॉल में वैल्यू डिकोड कर सकता है, इसलिए कभी सिक्रेट को Base64 में रखकर उसे एन्क्रिप्टेड मत कहिए; नीचे का सुरक्षा सेक्शन विस्तार से जाता है। दूसरी, बूट पर वैलिडेट कीजिए: कोरप्टेड या आधा-पेस्ट एनवायरनमेंट वैल्यू सख़्त कॉल से IllegalArgumentException है, और दो-लाइन की जाँच किसी रहस्यमय रनटाइम एरर को काम-आने लायक स्टार्टअप मैसेज में बदल देती है। डेटाबेस वालों के लिए एक Java-खास नोट: डिकोडेड बायनेरी को byte[] की शक्ल में रखिए (अपने JDBC कोड में byte[] पैरामीटर), और बायनेरी को कभी String के रास्ते राउंड-ट्रिप न कीजिए, क्योंकि स्ट्रिंग कन्स्ट्रक्टर वही जगह है जहाँ बायनेरी पेलोड मर जाते हैं।

बड़ी चीज़ों को स्ट्रीम करना

ऐसे पेलोड्स के लिए जो बड़े हैं पर फिर भी आपके संभाले हुए बफ़र में समा जाते हैं, अरेय़ APIs काफ़ी हैं। ऐसे पेलोड्स के लिए जो मेमोरी में ही फिट नहीं होने चाहिए, स्ट्रीम एडैप्टर ही हाल है: wrap(InputStream) एक इनपुट स्ट्रीम वापस करता है जो पढ़ते-पढ़ते डिकोड करती है, इसलिए मल्टी-गीगाबाइट की एन्कोडेड फ़ाइल कभी बाइट अरेय़ में बैठी नहीं रहनी पड़ती:

import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
public class StreamDecode {
  public static void main(String[] args) throws Exception {
    InputStream packed = Base64.getDecoder().wrap(Files.newInputStream(Paths.get("bigfile.b64")));
    OutputStream raw = Files.newOutputStream(Paths.get("bigfile.bin"));
    byte[] buf = new byte[8192];
    int n;
    while ((n = packed.read(buf)) != -1) {
      raw.write(buf, 0, n);
    }
    raw.close();
    packed.close();
  }
}

दो बारीकियाँ जानने लायक हैं। रैप्ड स्ट्रीम के read मेटोड तब IOException थ्रो करते हैं जब वे ऐसे बाइट्स पर मिलते हैं जो डिकोड न हों, इसलिए कोरप्टेड फ़ाइल IllegalArgumentException की जगह स्ट्रीम एक्ससेप्शन से फेल होती है। और रैप्ड स्ट्रीम बंद करने पर नीचे की स्ट्रीम भी बंद हो जाती है, इसलिए उदाहरण packed को आख़िर में, कॉपी लूप के बाद, बंद करता है, और प्रोडक्शन में आप दोनों को try-with-resources ब्लॉक में रखेंगे। (8192 बफ़र बस एक चौड़ा read बफ़र है; रैप्ड स्ट्रीम अंदर ही डिकोड करती है, इसलिए पढ़ने का साइज़ आपकी परफ़ॉर्मन्स पसंद है, प्रोटोकॉल की ज़रूरत नहीं।)

अब एक लेगासी चेतावनी, क्योंकि पूरी कहानी में यही एक असली बग है, और उसके पास बग नंबर भी है। 16 से पहले हर JDK पर (बग रिपोर्ट इसे 8, 10 और 11 पर दोहराती है), wrapped decoder से कुछ ख़ास बफ़र साइज़्स के साथ पढ़ने पर डिकोडेड डेटा के अंत में दो घुसपैश ज़ीरो बाइट्स जुड़ जाते हैं: JDK 8222187, जिसका क्लासिक रिप्रोडक्शन सात-बाइट के read बफ़र को सादे आठ-बाइट इनपुट के साथ जोड़ता है, और यह JDK 16 में ठीक हो गया। अगर आपको पुराने JDK 8 पर स्ट्रीम करना ही है, तो कॉपी के बाद डिकोडेड लेंथ दोबारा जाँच लीजिए, क्योंकि बग ख़ास इनपुट-और-बफ़र जोड़ों पर फायर होता है, और 4096-बाइट बफ़र का रिपोर्ट बाहर की दुनिया में भी मिला है; या बेहतर, JDK अपग्रेड कीजिए, जो वैसे भी करीब सौ और चीज़ें ठीक कर देगा।

पैकिंग टेप, ताला नहीं

अब वह सेक्शन जो सावधानों को जल चुके हुए से अलग करता है। Base64 एन्क्रिप्शन नहीं है, और स्टैंडर्ड खुद इसे दो-चार बार कहता है। RFC 4648 सेक्शन 12: Base एन्कोडिंग "देखने-समझने में आसानी से पहचाने जाने वाले तथ्यों, जैसे पासवर्ड, को दृश्य रूप से छिपा देती है, पर किसी गणनात्मक गोपनीयता नहीं देती", और आगे यह नोट भी करता है कि जब कोई प्रोटोकॉल एक्सचेंज को टिकट में पेस्ट करके ग़लती से पासवर्ड उगल दे, तो इससे "सुरक्षा घटनाएँ हो चुकी हैं"। RFC की इम्प्लीमेंटरों के लिए सलाह भी फ्रेम में लटकने लायक है: "डिकोडर ग़ैर-वैध इनपुट, जैसे अंदर दबे NUL चर समेत, पर टूटे नहीं चाहिए"।

सूक्ष्म फँसाव है मैलेबिलिटी। याद रखिए कि हर सिंबल छह बिट्स ढोता है, और छोटा आख़िरी यूनिट ख़ाली बिट्स छोड़ता है जो अच्छी-बनी एन्कोडिंग में शून्य होने चाहिए। लापरवाह या दुश्मन एन्कोडर उन ख़ाली बिट्स में कचरा भर सकता है, और रिज़ल्ट बिल्कुल वैध दिखता रहता है: MQ== और MT== दोनों अंक 1 के एक ही बाइट में डिकोड होते हैं। Java इसका माफ़क पक्ष लेता है: Base64.getDecoder().decode("MT==") अनमहत्वपूर्ण बिट्स की जाँच नहीं करता और खुशी-खुशी वही बाइट सौंप देता है। क्यों परवाह करनी है? क्योंकि दो अलग स्ट्रिंग्स जो एक ही डेटा में डिकोड हों, "एकमात्र स्पेलिंग" वाले मान को तोड़ देती हैं जिस पर hash जाँचें, ड्यूप्लिकेशन-हटाना, और सिग्नेचर तुलनाएँ चुपचाप भरोसा करती हैं, और हमलावर जो एन्कोडेड वैल्यू को सफ़र के दौरान छेड़ सकता है, वह एक स्पेलिंग को दूसरी से बदल सकता है। Chatzigiannis और Chalkias की 2022 की पेपर "अमल में Base64 की लचिली" (ACM ASIA CCS 2022) असली दुनिया के इम्प्लीमेंटेशन में बस यही असंगतियाँ गिनती है। ख़ाली बिट्स पर RFC के अपने शब्द: वे "तथ्य रिसाने के लिए, या स्ट्रिंग equality तुलनाओं को चकमा देने के लिए, या इम्प्लीमेंटेशन समस्याएँ भड़काने के लिए ग़लत इस्तेमाल हो सकते हैं"। प्रैक्टिकल नियम "कभी डिकोड न करें" नहीं है; "अपनी बाउंड्री जान लो" है: अपने ही सिस्टमों के बीच के डेटा के लिए JDK की उदारता काफ़ी है, पर ट्रास्ट बाउंड्री पार करने वाले डेटा के लिए, डिकोड की गई किसी चीज़ पर भरोसा करने से पहले कैनोनिकल फ़ॉर्म (सही लेंथ, शून्य ख़ाली बिट्स, पैडिंग की एक स्पेलिंग) ज़रूर करवाइए।

परफ़ॉर्मन्स नोट्स

एक वाक्य में अच्छी ख़बर: आधुनिक JVM पर बिल्ट-इन डिकोडर इतना तेज़ है कि Base64 लगभग कभी आपका बॉटलनेक नहीं बनेगा, और बस वह हीबेंचमार्क है जिसके ख़िलाफ़ बाकी पूरा इकोसिस्टम खुद को मापता है। एक मामला: 2025 में gRPC-java प्रोजेक्ट ने अपने Guava-आधारित Base64 हैंडलिंग का बेंचमार्क java.util.Base64 के ख़िलाफ़ पब्लिक किया (issue 11857), और JDK 17 और 21 पर JDK वाला इम्प्लीमेंटेशन एन्कोडिंग में करीब 2.5 से 3.8 गुना और डिकोडिंग में 1.3 से 2.1 गुना तेज़ निकला, सबसे बड़ा फ़ासला x86 पर। यह एक मज़बूत इशारा है कि JDK का इम्प्लीमेंटेशन-काम कहाँ गया है, और यही वह निष्कर्ष है जो आप Base64 बेंचमार्क्स में बार-बार पाते रहेंगे: अब तेज़ वाला स्टैंडर्ड लाइब्रेरी वाला है, legacy वाला नहीं।

दो प्रैक्टिकल नोट्स। पहला, बहुत बड़ी फ़ाइलों के लिए आप जो संभालते हैं वह स्पीड नहीं, मेमोरी प्रोफ़ाइल है, इसीलिए तो स्ट्रीमिंग सेक्शन मौजूद है: wrap(InputStream) वर्किंग सेट को आपके read बफ़र तक ही सीमित रखता है। दूसरा, अगर आप असल में ऐसे हॉट पाथ पर हैं जहाँ मिलियनो छोटी वैल्यूज़ की डिकोडिंग होती है, तो एक ही डिकोडर इंस्टेंस साझा कीजिए (फ़ैक्टरी पहले से वही साझा इंस्टेंस वापस करती है, ऊपर नोट किया गया), जब बाइट्स पहले से हों तो decode(String) ओवरलोड छोड़ दीजिए (वह पहले स्ट्रिंग को Latin-1 के रास्ते कॉपी करता है), और decode(byte[], byte[]) ओवरलोड को पहले-से-साइज़ किए गए डिस्टिनेशन अरेय़ में लिखने दीजिए ताकि अलॉकेशन की नाच-धूम छूटे।

Java-अक्सेंट वाले फँसाव

सारे फँसाव एक जगह इकट्ठे, सब Java-खास:

  • वर्णमाला के लिए ग़लत डिकोडर। base64url स्ट्रिंग को getDecoder() में डालना (या उल्टा) क्लासिक Illegal base64 character क्रैश है, आमतौर पर मैसेज में 2d, 5f, 2b या 2f के साथ। हर बार डिकोडर को प्रोटोकॉल से मिलाइए।
  • बाहर की दुनिया का आख़िरी व्हाइटस्पेस। टर्मिनल, एनवायरनमेंट वेरिएबल, या कॉन्फ़िग फ़ाइल से कॉपी की गई वैल्यूज़ अक्सर लाइन ब्रेक के साथ आती हैं, और सख़्त डिकोडर उसे Illegal base64 character a में बदल देता है। इनपुट को strip() कीजिए, या MIME डिकोडर सिर्फ़ तब इस्तेमाल कीजिए जब डेटा सच में रैप्ड हो।
  • अर्मर का फँसाव। getMimeDecoder() PEM हेडर्स को समझता नहीं, और BEGIN और CERTIFICATE के अक्षर डेटा की तरह डिकोड हो जाते हैं। अर्मर की लाइनें खुद छील लीजिए, हमेशा।
  • MIME लचिलपन शॉर्टकट की तरह। बस "सुरक्षित रहने" के लिए MIME डिकोडर से डिकोड करना किसी भी घुसे हुए गैर-वर्णमाला चर को चुपचाप छुड़ा देता है, इसलिए कोरप्टेड पेलोड सही-सही दिखते हुए ग़लत निकल सकता है। उसे सिर्फ़ असली MIME इनपुट के लिए इस्तेमाल कीजिए।
  • कैरेक्टर सेट किस्मत पर छोड़ा हुआ। बिना-एरगुमेंट new String(bytes) प्लेटफ़ॉर्म डिफ़ॉल्ट इस्तेमाल करता है। JDK 18+ पर वह UTF-8 है, पर आपका कोड StandardCharsets.UTF_8 एक्सप्लिसिट पास करना चाहिए, वरना अगली सर्वर माइग्रेशन के बाद मोजिबैक का मज़ा लीजिए।
  • बायनेरी को स्ट्रिंग में बदलना। new String(decodedPng) और फिर वापस, यह डेटा का विनाश है: आपकी कैरेक्टर सेट में वैध न होने वाली हर बाइट सिलसिला रिप्लेसमेंट चर बन जाती है, और राउंड ट्रिप एक-तरफ़ा है। बाइट्स अंदर, बाइट्स बाहर, बिल्कुल आख़िरी कदम तक।
  • ख़ाली बिट्स पर भरोसा। MT== MQ== जैसा ही डिकोड होता है, इसलिए अनमहत्वपूर्ण बिट्स में कचरा छुपाए हुए पेलोड JDK की चलाई हर जाँच से निकल जाता है। अगर प्रोटोकॉल मायने रखता है, तो कैनोनिकल फ़ॉर्म ज़रूर करवाइए।
  • JDK 8, 11 और 12 के स्ट्रीम्स। उन वर्ज़न्स पर wrapped decoder कुछ बफ़र साइज़्स के लिए दो घुसपैश ज़ीरो बाइट्स जोड़ सकता है (JDK 8222187, 16 में ठीक)। 16 और बाद में यह मुसीबत नहीं है; पुरानों पर है।
  • null ख़ाली नहीं है। decode() में null देने पर NullPointerException आता है, ख़ाली अरेय़ नहीं। अगर किसी वैरिएबल में null हो सकता है, तो कॉल से पहले उसे कोई स्थिर वैल्यू दे दीजिए।
  • Android एक अलग ज़ुर्नाख़ाना है। Android पर java.util.Base64 बस API स्तर 26 से मौजूद है; उससे नीचे फ़्रेमवर्क क्लास है android.util.Base64 अपने फ़्लैग कॉन्स्टेंट्स के साथ। जो कोड बिना जाँचे एक या दूसरे को हार्ड-कोड करता है, बस वही उन डेवाइस पर टूटता है जिन्हें आपने कभी टेस्ट नहीं किया।
  • यह सुरक्षा नहीं है, भूल जाना। Base64 पासवर्ड को एक नज़र से छुपाता है, बाकि दुनिया से नहीं। अगर डेटा संवेदनशील है, तो पहले एन्क्रिप्ट कीजिए, और सिर्फ़ तब पैक कीजिए जब चैनल टेक्स्ट ही माँगता हो।

java.util.Base64 तक की लंबी राह

फ़ॉर्मैट की कहानी Java से पुरानी है। 1980 के दशक में इंटरनेट की मेल इंफ्रास्ट्रक्चर सिर्फ़ 7-बिट ASCII ढो पाती थी, और जो लोग बायनेरी भेजना चाहते थे उन्होंने लोकल बोलियाँ गढ़ लीं: UNIX के लिए uuencode (जिसकी वर्णमाला लगातार ASCII कोड्स पर चलती है, इसलिए एन्कोडिंग बस 32 जोड़ने का काम था, कोई लुकअप टेबल नहीं) और Apple मशीनों के लिए BinHex (जो ने अपनी वर्णमाला इतना क्यूरेट की कि 7, O, g और o जैसे देखने में भ्रामक चर निकाल दिए)। 1987 में Privacy-Enhanced Mail प्रोटोकॉल (RFC 989) ने 64-चर लाइनों के साथ सर्टिफ़िकेट्स ढोने के लिए 64-चर स्कीम को मानक बनाया, और 1993 की RFC 1421 ने वह वर्णमाला और पैडिंग नियम संभाले रखे। 1996 में MIME (RFC 2045, 1993 की RFC 1521 का अपडेट) उसी स्कीम को अपने साथ लाया जो उसके 64-चर वर्णमाला पर नाम ले चुकी थी - "base64" - 76-चर लाइन लेंथ तय की जो आज भी आपके ईमेल अटैचमेंट्स को रैप करती है, और वह लचिला डिकोडर नियम लिखा जो getMimeDecoder() आज तक इम्प्लीमेंट करता है। 2003 में RFC 3548 ने पूरे परिवार को साफ़-सुथरा करने की कोशिश की और घोषणा की कि डिकोडरों को वर्णमाला-बाहर चरों को ठुकराना चाहिए, और 2006 में RFC 4648 वह मानक बन गया जो हर कोई उद्धृत करता है, वर्णमाला टेबल्स, सेक्शन 5 का base64url वैरिएंट, और उस सुरक्षा सेक्शन के साथ जो इस लेख के आख़िरी सेक्शन को ईमानदार रखता है।

Java का अपना अध्याय थोड़ा ज़्यादा नाटक-भरा है। सालों तक JDK के अंदर एकमात्र Base64 वह आंतरिक जोड़ी sun.misc.BASE64Encoder और sun.misc.BASE64Decoder थी - वही जाति का API जो आज कंपाइल होता है और बिना किसी देप्रीकेशन वॉर्निंग के गायब हो जाता है - और अगर XML दुनिया में Base64 चाहिए था तो JAXB का javax.xml.bind.DatatypeConverter भी मौजूद था। बाकी सबने Apache Commons Codec या Guava इस्तेमाल की। फिर 18 मार्च 2014 आया: Java 8 ने java.util.Base64 शिप किया, RFC 4648 और RFC 2045 को एक क्लास में, वही फ़ैक्टरी-मेटोड पैटर्न जिसका आप इस्तेमाल कर रहे हैं। तीन आधा साल बाद, Java 9 (21 सितंबर 2017) ने sun.misc जोड़ी को हमेशा के लिए हटाया, और आधिकारिक माइग्रेशन गाइड इस बारे में सीधा है: "खास तौर पर, sun.misc.BASE64Encoder और sun.misc.BASE64Decoder को हटा दिया गया। बजाय इसके, supported java.util.Base64 क्लास इस्तेमाल करें, जो JDK 8 में जोड़ी गई थी"। पुराने कोड पर jdeps चलाइए जो अभी भी उनका उल्लेख करता है, और टूल उस निर्भरता को "JDK removed internal API" चिह्नित करता है। Java 11 ने JAXB मॉड्यूल और उसके DatatypeConverter को भी साथ-साथ हटा दिया (JEP 320)। 1.8 से सार्वजनिक API का एक भी मेटोड नहीं बदला, और javadoc आज भी उसी मूल Since: 1.8 टैग के साथ चलता है। जो बदला है वह नीचे का इंजन है: बग फिक्स (JDK 8222187 वाला स्ट्रीम बग, JDK 16 में ठीक) और परफ़ॉर्मन्स का काम, इसीलिए कम्युनिटी बेंचमार्क्स बार-बार वही निष्कर्ष देते रहे। बारह साल, एक API, और यह अभी भी वह सबसे तेज़ Base64 है जिसके लिए आपको कुछ देना नहीं पड़ता।

मज़ेदार तथ्य, Java एडिशन

क्योंकि एक पूरा गाइड मुस्कान पर ख़त्म होना चाहिए, यहाँ कुछ Java-खास तथ्य हैं जो बस मज़े के हैं:

  • decode(byte[] src, byte[] dst) का Oracle javadoc वादा करता है कि "IllegalargumentException थ्रो होने से पहले आउटपुट बाइट अरेय़ में कुछ बाइट्स लिखी जा चुकी हो सकती हैं"। IllegalArgumentException नहीं, IllegalargumentException, छोटे a के साथ। टाइपो असली JDK सोर्स में है, और 2014 से वहीं बैठा है। टाइपो के इस हद तक समर्पित दस्तावेज़ जितना होना चाहिए उससे कम आम है।
  • é वाली स्ट्रिंग डिकोड कीजिए और एरर मैसेज होगा Illegal base64 character -17: एक नेगेटिव hex नंबर, क्योंकि वह चर Latin-1 बाइट 0xE9 बनता है, जो साइनड Java बाइट के रूप में माइनस 23 है, और JDK उसे बेस 16 में प्रिंट करता है। आपका एरर लॉगर, थोड़ी देर के लिए, साइनड अंकगणित कर रहा है।
  • Base64.getDecoder() == Base64.getDecoder() सच है। सोर्स कोड हर कॉल पर एक साझा स्टैटिक इंस्टेंस वापस करता है, इसलिए "नया लीजिए" वाला API सिर्फ़ singleton का कॉस्ट्यूम है, और थ्रेड-सेफ्टी का वादा बस उस चीज़ का वर्णन है जो JVM पहले से कर ही रहा है।
  • URL डिकोडर को चार अंडरस्कोरों की स्ट्रिंग, "____", खिलाइए और वह तीन बाइट्स की साफ़ 0xFF वापस देता है। अंडरस्कोर की वर्णमाला वैल्यू 63 है, चार मिलकर 24 बिट्स बनाते हैं, और एकों के 24 बिट्स हैं बाइट टुपल FF FF FF। इसमें कुछ भी ग़लत नहीं है, और यही सबसे मज़ेदार हिस्सा है।
  • AA== एक ही NUL बाइट में डिकोड होता है जबकि ख़ाली स्ट्रिंग में कुछ नहींं डिकोड होता। Base64 में "कुछ नहीं" और "ज़ीरो" दो अलग जानवर हैं, और दोनों बिल्कुल वैध इनपुट हैं।
  • वह छोटी स्ट्रिंग TWFu जो Man में डिकोड होती है, इकोसिस्टम की पसंदीदा स्मोक टेस्ट बन गई है: वह RFC में, Wikipedia में, संदर्भ मैन्युअल्स में और धरती के ज़्यादातर Base64 ट्यूटोरियल्स में मिलती है, इसलिए 2014 के बाद लिखा गया हर डिकोडर उसी छोटी स्ट्रिंग को सलाम करता आया।
  • आप जिस हर Base64-एन्कोडेड PNG को कभी भी डिकोड करेंगे, वह iVBORw0K से शुरू होगी। यह PNG का मैजिक नंबर है बदले हुए शक्ल में, और यह इंटरनेट की सबसे पहचाने जाने वाली आठ-चर प्रीफिक्स में से एक है।
  • RFC 4648 का URL-safe सेक्शन वही जगह है जहाँ नाम "base64url" जन्मा: स्पेक कहता है कि एन्कोडिंग को "base64url कहा जा सकता है" और चेतावनी देता है कि इसे "base64 एन्कोडिंग से एक जैसा नहीं माना जाना चाहिए"। URL-safe वर्णमाला की उत्पत्ति के फ़ुटनोट में 2001 के P2P-hackers मेलिंग लिस्ट पोस्ट की तरफ़ इशारा है, इसलिए जो नाम आप हर URL में पेस्ट करते हैं, उसकी परवरिश मेलिंग लिस्ट में हुई है।
  • YouTube वीडियो IDs base64url बिना पैडिंग हैं, वही परिचित ग्यारह-चर स्ट्रिंग जो आप URL के कहीं भी पेस्ट कर सकते हैं। जो फ़ॉर्मैट ईमेल अटैचमेंट्स के लिए बना था वह अब एक वीडियो प्लेटफ़ॉर्म चला रहा है, और getUrlDecoder() आपके JDK का वह हिस्सा है जो इसे संभव बनाता है।
  • YmFzZTY0 को डिकोड कीजिए और आपको वापस शब्द base64 मिलेगा, पैडिंग के बिना, क्योंकि छह, तीन का गुणज है। फ़ॉर्मैट खुद का वर्णन करता हुआ, Morse में बोलने वाले आईने का तकनीकी बराबरी है।

दूसरी दिशा

यह कहानी का डिकोडर वाला हिस्सा था, और यहीं ज़्यादातर दर्द बसता है, क्योंकि डिकोडिंग वही जगह है जहाँ आप दूसरों का डेटा मिलते हैं: उनकी पैडिंग की पसंद, उनके लाइन ब्रेक्स, उनके कैरेक्टर सेट्स, उनके टोकन, उनका अर्मर। दूसरी दिशा, java.util.Base64 के एन्कोडर्स से बाइट्स को Base64 स्ट्रिंग में बदलना, एक शांत जानवर है: वह ग़ैर-वैध इनपुट पर कभी थ्रो नहीं करता (एन्कोड करने के लिए कोई ग़ैर-वैध इनपुट ही नहीं होता), एरर मैसेज पढ़ने की जगह साइज़ की बिल भरनी पड़ती है, और उसके अपने फँसाव (कैरेक्टर सेट का कदम, MIME के डायल्स, टोकन के लिए पैडिंग का फैसला) को अपना गाइड मिलता है। Java में Base64 एन्कोडिंग, जो इसी पेज से लिंक है, एन्कोडर को उसी गहराई से कवर करता है, और दोनों एक जोड़े की तरह आराम से पढ़े जाते हैं।

अंतिम अपडेट: 2026-09-08

संबंधित लेख: Java में Base64 एन्कोडिंग: एक सम्पूर्ण गाइड