001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.codec.net;
019
020import java.io.UnsupportedEncodingException;
021import java.nio.charset.Charset;
022import java.nio.charset.StandardCharsets;
023import java.nio.charset.UnsupportedCharsetException;
024
025import org.apache.commons.codec.CodecPolicy;
026import org.apache.commons.codec.DecoderException;
027import org.apache.commons.codec.EncoderException;
028import org.apache.commons.codec.StringDecoder;
029import org.apache.commons.codec.StringEncoder;
030import org.apache.commons.codec.binary.Base64;
031import org.apache.commons.codec.binary.BaseNCodec;
032
033/**
034 * Identical to the Base64 encoding defined by <a href="https://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a>
035 * and allows a character set to be specified.
036 * <p>
037 * <a href="https://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII
038 * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message
039 * handling software.
040 * </p>
041 * <p>
042 * This class is immutable and thread-safe.
043 * </p>
044 *
045 * <p>
046 * Decoding is lenient by default: the Base64 payload can contain ignored characters, noncanonical padding or trailing bits, and data after padding.
047 * Different encoded words can therefore decode to the same text. To require a canonical Base64 payload, select {@link CodecPolicy#STRICT}:
048 * </p>
049 *
050 * <pre>
051 * BCodec codec = new BCodec(StandardCharsets.UTF_8, CodecPolicy.STRICT);
052 * </pre>
053 *
054 * <p>
055 * Strict decoding requires the standard Base64 alphabet, padding for partial blocks, and no whitespace within the payload. Invalid payloads cause a
056 * {@link DecoderException}. This validates the Base64 payload only; it does not establish a unique representation of the complete encoded word or message
057 * header, including its charset label. Applications comparing header values for security decisions must use a consistent representation, and signature
058 * verification must follow the signing protocol.
059 * </p>
060 *
061 * @see <a href="https://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message
062 *          Header Extensions for Non-ASCII Text</a>
063 *
064 * @since 1.3
065 */
066public class BCodec extends RFC1522Codec implements StringEncoder, StringDecoder {
067
068    /**
069     * The default decoding policy is lenient.
070     */
071    private static final CodecPolicy DECODING_POLICY_DEFAULT = CodecPolicy.LENIENT;
072
073    /**
074     * Decoding policy for the Base64 payload. The default is lenient; strict decoding requires a canonical payload.
075     */
076    private final CodecPolicy decodingPolicy;
077
078    /**
079     * Constructs a new instance.
080     */
081    public BCodec() {
082        this(StandardCharsets.UTF_8);
083    }
084
085    /**
086     * Constructs a new instance for the selection of a default Charset.
087     *
088     * @param charset
089     *            the default string Charset to use.
090     *
091     * @see Charset
092     * @since 1.7
093     */
094    public BCodec(final Charset charset) {
095        this(charset, DECODING_POLICY_DEFAULT);
096    }
097
098    /**
099     * 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}