View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.codec.net;
19  
20  import java.io.UnsupportedEncodingException;
21  import java.nio.charset.Charset;
22  import java.nio.charset.StandardCharsets;
23  import java.nio.charset.UnsupportedCharsetException;
24  
25  import org.apache.commons.codec.CodecPolicy;
26  import org.apache.commons.codec.DecoderException;
27  import org.apache.commons.codec.EncoderException;
28  import org.apache.commons.codec.StringDecoder;
29  import org.apache.commons.codec.StringEncoder;
30  import org.apache.commons.codec.binary.Base64;
31  import org.apache.commons.codec.binary.BaseNCodec;
32  
33  /**
34   * Identical to the Base64 encoding defined by <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a>
35   * and allows a character set to be specified.
36   * <p>
37   * <a href="https://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII
38   * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message
39   * handling software.
40   * </p>
41   * <p>
42   * This class is immutable and thread-safe.
43   * </p>
44   *
45   * <p>
46   * Decoding is lenient by default: the Base64 payload can contain ignored characters, noncanonical padding or trailing bits, and data after padding.
47   * Different encoded words can therefore decode to the same text. To require a canonical Base64 payload, select {@link CodecPolicy#STRICT}:
48   * </p>
49   *
50   * <pre>
51   * BCodec codec = new BCodec(StandardCharsets.UTF_8, CodecPolicy.STRICT);
52   * </pre>
53   *
54   * <p>
55   * Strict decoding requires the standard Base64 alphabet, padding for partial blocks, and no whitespace within the payload. Invalid payloads cause a
56   * {@link DecoderException}. This validates the Base64 payload only; it does not establish a unique representation of the complete encoded word or message
57   * header, including its charset label. Applications comparing header values for security decisions must use a consistent representation, and signature
58   * verification must follow the signing protocol.
59   * </p>
60   *
61   * @see <a href="https://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message
62   *          Header Extensions for Non-ASCII Text</a>
63   *
64   * @since 1.3
65   */
66  public class BCodec extends RFC1522Codec implements StringEncoder, StringDecoder {
67  
68      /**
69       * The default decoding policy is lenient.
70       */
71      private static final CodecPolicy DECODING_POLICY_DEFAULT = CodecPolicy.LENIENT;
72  
73      /**
74       * Decoding policy for the Base64 payload. The default is lenient; strict decoding requires a canonical payload.
75       */
76      private final CodecPolicy decodingPolicy;
77  
78      /**
79       * Constructs a new instance.
80       */
81      public BCodec() {
82          this(StandardCharsets.UTF_8);
83      }
84  
85      /**
86       * Constructs a new instance for the selection of a default Charset.
87       *
88       * @param charset
89       *            the default string Charset to use.
90       *
91       * @see Charset
92       * @since 1.7
93       */
94      public BCodec(final Charset charset) {
95          this(charset, DECODING_POLICY_DEFAULT);
96      }
97  
98      /**
99       * Constructs a new instance for the selection of a default Charset.
100      *
101      * <p>
102      * Use {@link CodecPolicy#STRICT} to require canonical standard Base64 payloads. The other constructors use {@link CodecPolicy#LENIENT}.
103      * This policy applies to the Base64 payload, not the complete encoded word; see the class documentation.
104      * </p>
105      *
106      * @param charset
107      *            the default string Charset to use.
108      * @param decodingPolicy The decoding policy.
109      * @see Charset
110      * @since 1.15
111      */
112     public BCodec(final Charset charset, final CodecPolicy decodingPolicy) {
113         super(charset);
114         this.decodingPolicy = decodingPolicy;
115     }
116 
117     /**
118      * Constructs a new instance for the selection of a default Charset.
119      *
120      * @param charsetName
121      *            the default Charset to use.
122      * @throws java.nio.charset.UnsupportedCharsetException
123      *             Thrown if the named Charset is unavailable.
124      * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable
125      * @see Charset
126      */
127     public BCodec(final String charsetName) {
128         this(Charset.forName(charsetName));
129     }
130 
131     /**
132      * Decodes a Base64 object into its original form. Escaped characters are converted back to their original
133      * representation.
134      *
135      * <p>
136      * Uses the decoding policy selected at construction. The default is lenient and does not require a canonical Base64 payload. Use
137      * {@link #BCodec(Charset, CodecPolicy)} with {@link CodecPolicy#STRICT} for canonical payload validation.
138      * </p>
139      *
140      * @param value
141      *            Base64 object to convert into its original form.
142      * @return original object.
143      * @throws DecoderException
144      *             Thrown if the argument is not a {@code String}. Thrown if a failure condition is encountered
145      *             during the decode process.
146      */
147     @Override
148     public Object decode(final Object value) throws DecoderException {
149         if (value == null) {
150             return null;
151         }
152         if (value instanceof String) {
153             return decode((String) value);
154         }
155         throw new DecoderException("Objects of type " + value.getClass().getName() + " cannot be decoded using BCodec");
156     }
157 
158     /**
159      * Decodes a Base64 string into its original form. Escaped characters are converted back to their original
160      * representation.
161      *
162      * <p>
163      * Uses the decoding policy selected at construction. The default is lenient and does not require a canonical Base64 payload. Use
164      * {@link #BCodec(Charset, CodecPolicy)} with {@link CodecPolicy#STRICT} for canonical payload validation.
165      * </p>
166      *
167      * @param value
168      *            Base64 string to convert into its original form.
169      * @return original string.
170      * @throws DecoderException
171      *             Thrown if a failure condition is encountered during the decoding process.
172      */
173     @Override
174     public String decode(final String value) throws DecoderException {
175         try {
176             return decodeText(value);
177         } catch (final UnsupportedEncodingException | IllegalArgumentException e) {
178             throw new DecoderException(e.getMessage(), e);
179         }
180     }
181 
182     /**
183      * {@inheritDoc}
184      *
185      * @throws IllegalArgumentException Thrown when a problem is detected processing data.
186      */
187     @Override
188     protected byte[] doDecoding(final byte[] bytes) throws DecoderException {
189         if (bytes == null) {
190             return null;
191         }
192         // @formatter:off
193         try {
194             return Base64.builder()
195                     .setLineLength(0)
196                     .setLineSeparator(BaseNCodec.getChunkSeparator())
197                     .setUrlSafe(false)
198                     .setDecodingPolicy(decodingPolicy)
199                     .get()
200                     .decode(bytes);
201         } catch (final IllegalArgumentException e) {
202             throw new DecoderException(e.getMessage(), e);
203         }
204         // @formatter:on
205     }
206 
207     @Override
208     protected byte[] doEncoding(final byte[] bytes) {
209         if (bytes == null) {
210             return null;
211         }
212         return Base64.encodeBase64(bytes);
213     }
214 
215     /**
216      * Encodes an object into its Base64 form using the default Charset. Unsafe characters are escaped.
217      *
218      * @param value
219      *            object to convert to Base64 form.
220      * @return Base64 object.
221      * @throws EncoderException
222      *             Thrown if a failure condition is encountered during the encoding process.
223      */
224     @Override
225     public Object encode(final Object value) throws EncoderException {
226         if (value == null) {
227             return null;
228         }
229         if (value instanceof String) {
230             return encode((String) value);
231         }
232         throw new EncoderException("Objects of type " + value.getClass().getName() + " cannot be encoded using BCodec");
233     }
234 
235     /**
236      * Encodes a string into its Base64 form using the default Charset. Unsafe characters are escaped.
237      *
238      * @param strSource
239      *            string to convert to Base64 form.
240      * @return Base64 string.
241      * @throws EncoderException
242      *             Thrown if a failure condition is encountered during the encoding process.
243      */
244     @Override
245     public String encode(final String strSource) throws EncoderException {
246         return encode(strSource, getCharset());
247     }
248 
249     /**
250      * Encodes a string into its Base64 form using the specified Charset. Unsafe characters are escaped.
251      *
252      * @param strSource
253      *            string to convert to Base64 form.
254      * @param sourceCharset
255      *            the Charset for {@code value}.
256      * @return Base64 string.
257      * @throws EncoderException
258      *             Thrown if a failure condition is encountered during the encoding process.
259      * @since 1.7
260      */
261     public String encode(final String strSource, final Charset sourceCharset) throws EncoderException {
262         return encodeText(strSource, sourceCharset);
263     }
264 
265     /**
266      * Encodes a string into its Base64 form using the specified Charset. Unsafe characters are escaped.
267      *
268      * @param strSource
269      *            string to convert to Base64 form.
270      * @param sourceCharset
271      *            the Charset for {@code value}.
272      * @return Base64 string.
273      * @throws EncoderException
274      *             Thrown if a failure condition is encountered during the encoding process.
275      */
276     public String encode(final String strSource, final String sourceCharset) throws EncoderException {
277         try {
278             return encodeText(strSource, sourceCharset);
279         } catch (final UnsupportedCharsetException e) {
280             throw new EncoderException(e.getMessage(), e);
281         }
282     }
283 
284     @Override
285     protected String getEncoding() {
286         return "B";
287     }
288 
289     /**
290      * Tests whether decoding requires a canonical Base64 payload.
291      *
292      * <p>
293      * Strict decoding raises {@link DecoderException} for a noncanonical Base64 payload, including invalid alphabet characters, padding, or trailing bits.
294      * The default is lenient. This policy does not establish a canonical representation of the complete encoded word.
295      * </p>
296      *
297      * @return true if using strict decoding.
298      * @since 1.15
299      */
300     public boolean isStrictDecoding() {
301         return decodingPolicy == CodecPolicy.STRICT;
302     }
303 }