# Copyright 2013 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Python provides the base64 module as a core module but this is mostly limited to encoding and decoding base64 and it's variants. It is often useful to be able to perform other operations on base64 text. This module is meant to be used in conjunction with the core base64 module. Standarized base64 is defined in RFC-4648 "The Base16, Base32, and Base64 Data Encodings". This module provides the following base64 utility functionality: * tests if text is valid base64 * filter formatting from base64 * convert base64 between different alphabets * Handle padding issues - test if base64 is padded - removes padding - restores padding * wraps base64 text into formatted blocks - via iterator - return formatted string """ import re import string import six from six.moves import urllib class InvalidBase64Error(ValueError): pass base64_alphabet_re = re.compile(r'^[^A-Za-z0-9+/=]+$') base64url_alphabet_re = re.compile(r'^[^A-Za-z0-9---_=]+$') base64_non_alphabet_re = re.compile(r'[^A-Za-z0-9+/=]+') base64url_non_alphabet_re = re.compile(r'[^A-Za-z0-9---_=]+') _strip_formatting_re = re.compile(r'\s+') _base64_to_base64url_trans = string.maketrans('+/', '-_') _base64url_to_base64_trans = string.maketrans('-_', '+/') def is_valid_base64(text): """Test if input text can be base64 decoded. :param text: input base64 text :type text: string :returns: bool -- True if text can be decoded as base64, False otherwise """ text = filter_formatting(text) if base64_non_alphabet_re.search(text): return False try: return base64_is_padded(text) except InvalidBase64Error: return False def is_valid_base64url(text): """Test if input text can be base64url decoded. :param text: input base64 text :type text: string :returns: bool -- True if text can be decoded as base64url, False otherwise """ text = filter_formatting(text) if base64url_non_alphabet_re.search(text): return False try: return base64_is_padded(text) except InvalidBase64Error: return False def filter_formatting(text): """Return base64 text without any formatting, just the base64. Base64 text is often formatted with whitespace, line endings, etc. This function strips out any formatting, the result will contain only base64 characters. Note, this function does not filter out all non-base64 alphabet characters, it only removes characters used for formatting. :param text: input text to filter :type text: string :returns: string -- filtered text without formatting """ return _strip_formatting_re.sub('', text) def base64_to_base64url(text): """Convert base64 text to base64url text. base64url text is designed to be safe for use in filenames and URL's. It is defined in RFC-4648 Section 5. base64url differs from base64 in the last two alphabet characters at index 62 and 63, these are sometimes referred as the altchars. The '+' character at index 62 is replaced by '-' (hyphen) and the '/' character at index 63 is replaced by '_' (underscore). This function only translates the altchars, non-alphabet characters are not filtered out. WARNING:: base64url continues to use the '=' pad character which is NOT URL safe. RFC-4648 suggests two alternate methods to deal with this: percent-encode percent-encode the pad character (e.g. '=' becomes '%3D'). This makes the base64url text fully safe. But percent-enconding has the downside of requiring percent-decoding prior to feeding the base64url text into a base64url decoder since most base64url decoders do not recognize %3D as a pad character and most decoders require correct padding. no-padding padding is not strictly necessary to decode base64 or base64url text, the pad can be computed from the input text length. However many decoders demand padding and will consider non-padded text to be malformed. If one wants to omit the trailing pad character(s) for use in URL's it can be added back using the base64_assure_padding() function. This function makes no decisions about which padding methodolgy to use. One can either call base64_strip_padding() to remove any pad characters (restoring later with base64_assure_padding()) or call base64url_percent_encode() to percent-encode the pad characters. :param text: input base64 text :type text: string :returns: string -- base64url text """ return text.translate(_base64_to_base64url_trans) def base64url_to_base64(text): """Convert base64url text to base64 text. See base64_to_base64url() for a description of base64url text and it's issues. This function does NOT handle percent-encoded pad characters, they will be left intact. If the input base64url text is percent-encoded you should call :param text: text in base64url alphabet :type text: string :returns: string -- text in base64 alphabet """ return text.translate(_base64url_to_base64_trans) def base64_is_padded(text, pad='='): """Test if the text is base64 padded. The input text must be in a base64 alphabet. The pad must be a single character. If the text has been percent-encoded (e.g. pad is the string '%3D') you must convert the text back to a base64 alphabet (e.g. if percent-encoded use the function base64url_percent_decode()). :param text: text containing ONLY characters in a base64 alphabet :type text: string :param pad: pad character (must be single character) (default: '=') :type pad: string :returns: bool -- True if padded, False otherwise :raises: ValueError, InvalidBase64Error """ if len(pad) != 1: raise ValueError(_('pad must be single character')) text_len = len(text) if text_len > 0 and text_len % 4 == 0: pad_index = text.find(pad) if pad_index >= 0 and pad_index < text_len - 2: raise InvalidBase64Error(_('text is multiple of 4, ' 'but pad "%s" occurs before ' '2nd to last char') % pad) if pad_index == text_len - 2 and text[-1] != pad: raise InvalidBase64Error(_('text is multiple of 4, ' 'but pad "%s" occurs before ' 'non-pad last char') % pad) return True if text.find(pad) >= 0: raise InvalidBase64Error(_('text is not a multiple of 4, ' 'but contains pad "%s"') % pad) return False def base64url_percent_encode(text): """Percent-encode base64url padding. The input text should only contain base64url alphabet characters. Any non-base64url alphabet characters will also be subject to percent-encoding. :param text: text containing ONLY characters in the base64url alphabet :type text: string :returns: string -- percent-encoded base64url text :raises: InvalidBase64Error """ if len(text) % 4 != 0: raise InvalidBase64Error(_('padded base64url text must be ' 'multiple of 4 characters')) return urllib.parse.quote(text) def base64url_percent_decode(text): """Percent-decode base64url padding. The input text should only contain base64url alphabet characters and the percent-encoded pad character. Any other percent-encoded characters will be subject to percent-decoding. :param text: base64url alphabet text :type text: string :returns: string -- percent-decoded base64url text """ decoded_text = urllib.parse.unquote(text) if len(decoded_text) % 4 != 0: raise InvalidBase64Error(_('padded base64url text must be ' 'multiple of 4 characters')) return decoded_text def base64_strip_padding(text, pad='='): """Remove padding from input base64 text. :param text: text containing ONLY characters in a base64 alphabet :type text: string :param pad: pad character (must be single character) (default: '=') :type pad: string :returns: string -- base64 text without padding :raises: ValueError """ if len(pad) != 1: raise ValueError(_('pad must be single character')) # Can't be padded if text is less than 4 characters. if len(text) < 4: return text if text[-1] == pad: if text[-2] == pad: return text[0:-2] else: return text[0:-1] else: return text def base64_assure_padding(text, pad='='): """Assure the input text ends with padding. Base64 text is normally expected to be a multple of 4 characters. Each 4 character base64 sequence produces 3 octets of binary data. If the binary data is not a multiple of 3 the base64 text is padded at the end with a pad character such that is is always a multple of 4. Padding is ignored and does not alter the binary data nor it's length. In some circumstances is is desirable to omit the padding character due to transport encoding conflicts. Base64 text can still be correctly decoded if the length of the base64 text (consisting only of characters in the desired base64 alphabet) is known, padding is not absolutely necessary. Some base64 decoders demand correct padding or one may wish to format RFC compliant base64, this function performs this action. Input is assumed to consist only of members of a base64 alphabet (i.e no whitepace). Iteration yields a sequence of lines. The line does NOT terminate with a line ending. Use the filter_formatting() function to assure the input text contains only the members of the alphabet. If the text ends with the pad it is assumed to already be padded. Otherwise the binary length is computed from the input text length and correct number of pad characters are appended. :param text: text containing ONLY characters in a base64 alphabet :type text: string :param pad: pad character (must be single character) (default: '=') :type pad: string :returns: string -- input base64 text with padding :raises: ValueError """ if len(pad) != 1: raise ValueError(_('pad must be single character')) if text.endswith(pad): return text n = len(text) % 4 if n == 0: return text n = 4 - n padding = pad * n return text + padding def base64_wrap_iter(text, width=64): """Fold text into lines of text with max line length. Input is assumed to consist only of members of a base64 alphabet (i.e no whitepace). Iteration yields a sequence of lines. The line does NOT terminate with a line ending. Use the filter_formatting() function to assure the input text contains only the members of the alphabet. :param text: text containing ONLY characters in a base64 alphabet :type text: string :param width: number of characters in each wrapped line (default: 64) :type width: int :returns: generator -- sequence of lines of base64 text. """ text = six.text_type(text) for x in six.moves.range(0, len(text), width): yield text[x:x + width] def base64_wrap(text, width=64): """Fold text into lines of text with max line length. Input is assumed to consist only of members of a base64 alphabet (i.e no whitepace). Fold the text into lines whose line length is width chars long, terminate each line with line ending (default is '\\n'). Return the wrapped text as a single string. Use the filter_formatting() function to assure the input text contains only the members of the alphabet. :param text: text containing ONLY characters in a base64 alphabet :type text: string :param width: number of characters in each wrapped line (default: 64) :type width: int :returns: string -- wrapped text. """ buf = six.StringIO() for line in base64_wrap_iter(text, width): buf.write(line) buf.write(u'\n') text = buf.getvalue() buf.close() return text