2021-12-27 16:36:10 +01:00
export type Base64 = string
export type Base64Ext = string
export type Base64Url = string
export type Hex = string
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
// TODO rename methods according to their JAVA counterparts (e.g. Uint8Array == bytes, Utf8Uint8Array == bytes...)
2025-12-04 18:00:11 +01:00
export function uint8ArrayToArrayBuffer ( uint8Array : Uint8Array ) : ArrayBufferLike {
2022-12-27 15:37:40 +01:00
if ( uint8Array . byteLength === uint8Array . buffer . byteLength ) {
return uint8Array . buffer
} else {
return new Uint8Array ( uint8Array ) . buffer // create a new instance with the correct length, if uint8Array is only a DataView on a longer Array.buffer
}
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts a hex coded string into a base64 coded string .
*
* @param hex A hex encoded string .
* @return A base64 encoded string .
* /
export function hexToBase64 ( hex : Hex ) : Base64 {
2022-12-27 15:37:40 +01:00
return uint8ArrayToBase64 ( hexToUint8Array ( hex ) )
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts a base64 coded string into a hex coded string .
*
* @param base64 A base64 encoded string .
* @return A hex encoded string .
* /
export function base64ToHex ( base64 : Base64 ) : Hex {
2022-12-27 15:37:40 +01:00
return uint8ArrayToHex ( base64ToUint8Array ( base64 ) )
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts a base64 string to a url - conform base64 string . This is used for
* base64 coded url parameters .
*
* @param base64 The base64 string .
* @return The base64url string .
* /
export function base64ToBase64Url ( base64 : Base64 ) : Base64Url {
2022-12-27 15:37:40 +01:00
let base64url = base64 . replace ( /\+/g , "-" )
base64url = base64url . replace ( /\//g , "_" )
base64url = base64url . replace ( /=/g , "" )
return base64url
2021-12-27 16:36:10 +01:00
}
function makeLookup ( str : string ) : Record < string , number > {
2022-12-27 15:37:40 +01:00
const lookup : Record < string , number > = { }
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < str . length ; i ++ ) {
lookup [ str . charAt ( i ) ] = i
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return lookup
2021-12-27 16:36:10 +01:00
}
const base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
const base64Lookup = makeLookup ( base64Alphabet )
const base64extAlphabet = "-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"
const base64ExtLookup = makeLookup ( base64extAlphabet )
/ * *
* Converts a base64 string to a base64ext string . Base64ext uses another character set than base64 in order to make it sortable .
*
*
* @param base64 The base64 string .
* @return The base64Ext string .
* /
export function base64ToBase64Ext ( base64 : Base64 ) : Base64Ext {
2022-12-27 15:37:40 +01:00
base64 = base64 . replace ( /=/g , "" )
let base64ext = ""
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < base64 . length ; i ++ ) {
2024-08-23 13:00:37 +02:00
const index = base64Lookup [ base64 . charAt ( i ) ]
2022-12-27 15:37:40 +01:00
base64ext += base64extAlphabet [ index ]
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return base64ext
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts a Base64Ext string to a Base64 string and appends the padding if needed .
* @param base64ext The base64Ext string
* @returns The base64 string
* /
export function base64ExtToBase64 ( base64ext : Base64Ext ) : Base64 {
2022-12-27 15:37:40 +01:00
let base64 = ""
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < base64ext . length ; i ++ ) {
const index = base64ExtLookup [ base64ext . charAt ( i ) ]
base64 += base64Alphabet [ index ]
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
let padding
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
if ( base64 . length % 4 === 2 ) {
padding = "=="
} else if ( base64 . length % 4 === 3 ) {
padding = "="
} else {
padding = ""
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return base64 + padding
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts a base64 url string to a "normal" base64 string . This is used for
* base64 coded url parameters .
*
* @param base64url The base64 url string .
* @return The base64 string .
* /
export function base64UrlToBase64 ( base64url : Base64Url ) : Base64 {
2022-12-27 15:37:40 +01:00
let base64 = base64url . replace ( /-/g , "+" )
base64 = base64 . replace ( /_/g , "/" )
let nbrOfRemainingChars = base64 . length % 4
if ( nbrOfRemainingChars === 0 ) {
return base64
} else if ( nbrOfRemainingChars === 2 ) {
return base64 + "=="
} else if ( nbrOfRemainingChars === 3 ) {
return base64 + "="
}
throw new Error ( "Illegal base64 string." )
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
// just for edge, as it does not support TextEncoder yet
export function _stringToUtf8Uint8ArrayLegacy ( string : string ) : Uint8Array {
2022-12-27 15:37:40 +01:00
let fixedString
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
try {
fixedString = encodeURIComponent ( string )
} catch ( e ) {
fixedString = encodeURIComponent ( _replaceLoneSurrogates ( string ) ) // we filter lone surrogates as trigger URIErrors, otherwise (see https://github.com/tutao/tutanota/issues/618)
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
let utf8 = unescape ( fixedString )
let uint8Array = new Uint8Array ( utf8 . length )
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < utf8 . length ; i ++ ) {
uint8Array [ i ] = utf8 . charCodeAt ( i )
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return uint8Array
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
const REPLACEMENT_CHAR = "\uFFFD"
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
export function _replaceLoneSurrogates ( s : string | null | undefined ) : string {
2022-12-27 15:37:40 +01:00
if ( s == null ) {
return ""
}
let result : string [ ] = [ ]
for ( let i = 0 ; i < s . length ; i ++ ) {
let code = s . charCodeAt ( i )
let char = s . charAt ( i )
if ( 0xd800 <= code && code <= 0xdbff ) {
if ( s . length === i ) {
// replace high surrogate without following low surrogate
result . push ( REPLACEMENT_CHAR )
} else {
let next = s . charCodeAt ( i + 1 )
if ( 0xdc00 <= next && next <= 0xdfff ) {
result . push ( char )
result . push ( s . charAt ( i + 1 ) )
i ++ // valid high and low surrogate, skip next low surrogate check
} else {
result . push ( REPLACEMENT_CHAR )
}
}
} else if ( 0xdc00 <= code && code <= 0xdfff ) {
// replace low surrogate without preceding high surrogate
result . push ( REPLACEMENT_CHAR )
} else {
result . push ( char )
}
}
return result . join ( "" )
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
const encoder =
2025-07-18 14:55:36 +02:00
typeof TextEncoder === "function"
2022-12-27 15:37:40 +01:00
? new TextEncoder ( )
: {
encode : _stringToUtf8Uint8ArrayLegacy ,
2025-07-07 11:51:45 +02:00
}
2021-12-27 16:36:10 +01:00
const decoder =
2025-07-18 14:55:36 +02:00
typeof TextDecoder === "function"
2022-12-27 15:37:40 +01:00
? new TextDecoder ( )
: {
decode : _utf8Uint8ArrayToStringLegacy ,
2025-07-07 11:51:45 +02:00
}
2021-12-27 16:36:10 +01:00
/ * *
* Converts a string to a Uint8Array containing a UTF - 8 string data .
*
* @param string The string to convert .
* @return The array .
* /
export function stringToUtf8Uint8Array ( string : string ) : Uint8Array {
2022-12-27 15:37:40 +01:00
return encoder . encode ( string )
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
// just for edge, as it does not support TextDecoder yet
export function _utf8Uint8ArrayToStringLegacy ( uint8Array : Uint8Array ) : string {
2022-12-27 15:37:40 +01:00
let stringArray : string [ ] = [ ]
stringArray . length = uint8Array . length
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < uint8Array . length ; i ++ ) {
stringArray [ i ] = String . fromCharCode ( uint8Array [ i ] )
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return decodeURIComponent ( escape ( stringArray . join ( "" ) ) )
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts an Uint8Array containing UTF - 8 string data into a string .
*
* @param uint8Array The Uint8Array .
* @return The string .
* /
export function utf8Uint8ArrayToString ( uint8Array : Uint8Array ) : string {
2022-12-27 15:37:40 +01:00
return decoder . decode ( uint8Array )
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
export function hexToUint8Array ( hex : Hex ) : Uint8Array {
2022-12-27 15:37:40 +01:00
let bufView = new Uint8Array ( hex . length / 2 )
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < bufView . byteLength ; i ++ ) {
bufView [ i ] = parseInt ( hex . substring ( i * 2 , i * 2 + 2 ) , 16 )
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return bufView
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
const hexDigits = "0123456789abcdef"
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
export function uint8ArrayToHex ( uint8Array : Uint8Array ) : Hex {
2022-12-27 15:37:40 +01:00
let hex = ""
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < uint8Array . byteLength ; i ++ ) {
let value = uint8Array [ i ]
hex += hexDigits [ value >> 4 ] + hexDigits [ value & 15 ]
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return hex
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts an Uint8Array to a Base64 encoded string .
*
* @param bytes The bytes to convert .
* @return The Base64 encoded string .
* /
export function uint8ArrayToBase64 ( bytes : Uint8Array ) : Base64 {
2022-12-27 15:37:40 +01:00
if ( bytes . length < 512 ) {
// Apply fails on big arrays fairly often. We tried it with 60000 but if you're already
// deep in the stack than we cannot allocate such a big argument array.
2021-12-27 16:36:10 +01:00
return btoa ( String . fromCharCode ( . . . bytes ) )
2022-12-27 15:37:40 +01:00
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
let binary = ""
const len = bytes . byteLength
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < len ; i ++ ) {
binary += String . fromCharCode ( bytes [ i ] )
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return btoa ( binary )
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
export function int8ArrayToBase64 ( bytes : Int8Array ) : Base64 {
2022-12-27 15:37:40 +01:00
// Values 0 to 127 are the same for signed and unsigned bytes
// and -128 to -1 are mapped to the same chars as 128 to 255.
let converted = new Uint8Array ( bytes )
return uint8ArrayToBase64 ( converted )
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts a base64 encoded string to a Uint8Array .
*
* @param base64 The Base64 encoded string .
* @return The bytes .
* /
export function base64ToUint8Array ( base64 : Base64 ) : Uint8Array {
2022-12-27 15:37:40 +01:00
if ( base64 . length % 4 !== 0 ) {
throw new Error ( ` invalid base64 length: ${ base64 } ( ${ base64 . length } ) ` )
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
const binaryString = atob ( base64 )
const result = new Uint8Array ( binaryString . length )
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
for ( let i = 0 ; i < binaryString . length ; i ++ ) {
result [ i ] = binaryString . charCodeAt ( i )
}
2021-12-27 16:36:10 +01:00
2022-12-27 15:37:40 +01:00
return result
2021-12-27 16:36:10 +01:00
}
/ * *
* Converts a Uint8Array containing string data into a string , given the charset the data is in .
* @param charset The charset . Must be supported by TextDecoder .
* @param bytes The string data
* @trhows RangeError if the charset is not supported
* @return The string
* /
export function uint8ArrayToString ( charset : string , bytes : Uint8Array ) : string {
2022-12-27 15:37:40 +01:00
const decoder = new TextDecoder ( charset )
return decoder . decode ( bytes )
2021-12-27 16:36:10 +01:00
}
/ * *
* Decodes a quoted - printable piece of text in a given charset .
* This was copied and modified from https : //github.com/mathiasbynens/quoted-printable/blob/master/src/quoted-printable.js (MIT licensed)
*
* @param charset Must be supported by TextEncoder
* @param input The encoded text
* @throws RangeError if the charset is not supported
* @returns The text as a JavaScript string
* /
export function decodeQuotedPrintable ( charset : string , input : string ) : string {
2022-12-27 15:37:40 +01:00
return (
input // https://tools.ietf.org/html/rfc2045#section-6.7, rule 3:
// “Therefore, when decoding a `Quoted-Printable` body, any trailing white
// space on a line must be deleted, as it will necessarily have been added
// by intermediate transport agents.”
. replace ( /[\t\x20]$/gm , "" ) // Remove hard line breaks preceded by `=`. Proper `Quoted-Printable`-
// encoded data only contains CRLF line endings, but for compatibility
// reasons we support separate CR and LF too.
. replace ( /=(?:\r\n?|\n|$)/g , "" ) // Decode escape sequences of the form `=XX` where `XX` is any
// combination of two hexidecimal digits. For optimal compatibility,
// lowercase hexadecimal digits are supported as well. See
// https://tools.ietf.org/html/rfc2045#section-6.7, note 1.
. replace ( /(=([a-fA-F0-9]{2}))+/g , ( match ) = > {
const hexValues = match . split ( /=/ )
// splitting on '=' is convenient, but adds an empty string at the start due to the first byte
hexValues . shift ( )
const intArray = hexValues . map ( ( char ) = > parseInt ( char , 16 ) )
const bytes = Uint8Array . from ( intArray )
return uint8ArrayToString ( charset , bytes )
} )
)
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
export function decodeBase64 ( charset : string , input : string ) : string {
2022-12-27 15:37:40 +01:00
return uint8ArrayToString ( charset , base64ToUint8Array ( input ) )
2021-12-27 16:36:10 +01:00
}
2023-12-18 11:36:38 +01:00
2021-12-27 16:36:10 +01:00
export function stringToBase64 ( str : string ) : string {
2022-12-27 15:37:40 +01:00
return uint8ArrayToBase64 ( stringToUtf8Uint8Array ( str ) )
}
2023-12-18 11:36:38 +01:00
/ * *
* Encodes a variable number of byte arrays as a single byte array . Format :
* short ( length of byteArray [ 0 ] ) | byteArray [ 0 ] | . . . | short ( length of byteArray [ n ] ) | byteArray [ n ]
*
* @return encoded byte array
* /
export function byteArraysToBytes ( byteArrays : Array < Uint8Array > ) : Uint8Array {
const totalBytesLength = byteArrays . reduce ( ( acc , element ) = > acc + element . length , 0 )
const encodingOverhead = byteArrays . length * 2 // two byte length overhead for each byte array
const encodedByteArrays = new Uint8Array ( encodingOverhead + totalBytesLength )
let index = 0
2025-01-03 10:16:07 +01:00
for ( const byteArray of byteArrays ) {
2023-12-18 11:36:38 +01:00
if ( byteArray . length > MAX_ENCODED_BYTES_LENGTH ) {
throw new Error ( "byte array is to long for encoding" )
}
index = writeByteArray ( encodedByteArrays , byteArray , index )
}
return encodedByteArrays
}
/ * *
* Decodes a byte array encoded by # byteArraysToBytes .
*
* @return list of byte arrays
* /
2025-01-03 10:16:07 +01:00
export function bytesToByteArrays ( encodedByteArrays : Uint8Array , expectedByteArrays : number ) : Array < Uint8Array > {
2023-12-18 11:36:38 +01:00
const byteArrays = new Array < Uint8Array > ( )
let index = 0
while ( index < encodedByteArrays . length ) {
const readResult = readByteArray ( encodedByteArrays , index )
byteArrays . push ( readResult . byteArray )
index = readResult . index
}
2025-07-18 14:55:36 +02:00
if ( byteArrays . length !== expectedByteArrays ) {
2023-12-18 11:36:38 +01:00
throw new Error ( "invalid amount of key parameters. Expected: " + expectedByteArrays + " actual:" + byteArrays . length )
}
return byteArrays
}
// Size of the length field for encoded byte arrays
const BYTE_ARRAY_LENGTH_FIELD_SIZE = 2
const MAX_ENCODED_BYTES_LENGTH = 65535
function writeByteArray ( result : Uint8Array , byteArray : Uint8Array , index : number ) : number {
writeShort ( result , byteArray . length , index )
index += BYTE_ARRAY_LENGTH_FIELD_SIZE
result . set ( byteArray , index )
index += byteArray . length
return index
}
function readByteArray ( encoded : Uint8Array , index : number ) : { index : number ; byteArray : Uint8Array } {
const length = readShort ( encoded , index )
index += BYTE_ARRAY_LENGTH_FIELD_SIZE
const byteArray = encoded . slice ( index , length + index )
index += length
2025-07-18 14:55:36 +02:00
if ( byteArray . length !== length ) {
2023-12-18 11:36:38 +01:00
throw new Error ( "cannot read encoded byte array at pos:" + index + " expected bytes:" + length + " read bytes:" + byteArray . length )
}
return { index , byteArray }
}
function writeShort ( array : Uint8Array , value : number , index : number ) {
array [ index ] = ( value & 0x0000ff00 ) >> 8
array [ index + 1 ] = ( value & 0x000000ff ) >> 0
}
function readShort ( array : Uint8Array , index : number ) : number {
const bytes = array . subarray ( index , index + BYTE_ARRAY_LENGTH_FIELD_SIZE )
let n = 0
for ( const byte of bytes . values ( ) ) {
n = ( n << 8 ) | byte
}
return n
}