The $encode identifier allows you to encode literal text, or text in %vars or &binvars. The $encode identifier uses either Uuencode or MIME or Base32 to encode. Additionally, $encode is capable of utilizing Blowfish to encrypt the target before encoding using u/m/a encoding.
Synopsis
; encoding only $encode(text/%var/&binvar [, btuma] [, N] ) ; encryption $encode(text/%var/&binvar, celsirznpbtuma, key[, salt/iv if s|i used] [,N] )
Parameters
Encoding
text/%var/&binvar The target to be encoded
b Target is a &binvar
t Target is text (this is default target type)
u Target should be encoded using Uuencode (this is default encode type when 'tuma' not used)
m Target should be encoded using Mime (base64) (favored; has shortest output)
a Target should be encoded using Base32
N Integer Reference index for the Nth chunk (can't use without at least 1 switch)
- 'b' recognizes target is a binary variable instead of text beginning with '&'.
- 'b' returns encoded output back to the same binary variable, then the $encode identifier 'returns' N where N is the number of encoded characters returned to the binary variable. i.e. updated $bvar(&binvar,0). Most common usage would be to $encode as an argument to noop or conditional checking if non-zero to detect failure.
- When target is text or %variable, content is UTF-8 encoded before encoding. To avoid this, you must load text into a binary variable using 'bset -ta' then use $encode's 'b' switch to allow using that binary variable as the target. (Assuming string does not contain codepoints 256+)
- 'm' and 'u' encode 3 bytes into 4 printable characters, with u also prepending a length byte before each chunk of up to 60 encoded characters. 'a' encodes 5 bytes into 8 printable characters.
- If N is present, N=0 returns the number of chunks in the output, N >= 1 returns the Nth encoded chunk of the output or $null if N is greater than the N=0 value. N allows handling encoded output output of &binary strings too long to fit within %variables or mIRC's maximum line length.
- Chunks are the length which encodes 45 input bytes. For 'm' and 'u', the 4:3 ratio causes the chunk to contain 60 encoding characters per chunk, with the default 'u' also prepending with a character indicating the number of bytes encoded by that chunk. Because base32 encodes 5 input bytes as 8 output characters, encoded 'a' chunks are length 72. Total output length is padded to be multiples of those groups of 4 or 8 printable characters plus the 'u' chunk length byte(s), with 'u' also padding strings that were already a length multiple of 4. $decode is currently able to decode encoded strings without the "`" or "=" padding added by $encode, but don't count on pre-v7.53 or other applications accepting them for all cases for all encoding types.
- This version of Base32 is different than the base32 used in $totp and $hotp. It uses a 32-item alphabet consisting of upper-case A-Z and the numbers 2-7. However, in addition to accepting base32 as case-insensitive, $decode also accepts these invalid numbers the same as if valid characters were used: Number 1 as if the letter L, Number 8 as if the letter B.
//var -s %a $encode(PWETA,a) | echo -a $decode(%a,a) vs $decode($replace(%a,8,b,1,l),a) vs $decode($lower(%a),a)
Encryption
c CBC encryption mode (c and e should not be used together) uses mirc-generated 'random' salt. Adds 16-byte header inside u/m/a encoding: Salted__ $+ 8-byte-random-salt.
e ECB encryption mode, 'sir' switches become invalid.
l Literal key parameter used as key, instead of using hash of the key parameter. Has no effect with 'e' except 'el' limits key length to 56 bytes while avoiding 'l' allows literal ECB key's length limit to be 72 instead of 56 bytes.
s user defined 8-byte salt - . (valid only when 'c' also used) When shorter than 8 bytes, is padded to 8 using 0x00 bytes. Same 16-byte header as non-s, except the 8-byte salt is the 1st 8 bytes of Parameter#4.
i user defined 8-byte (IV) initialization vector (valid only when 'c' also used, does not prepend the header to the encrypted data) When shorter than 8 bytes, is padded to 8 using 0x00 bytes. 's' and 'i' should not be used together, as 'i' causes 's' to be ignored.
r random IV (valid only when 'c' also used. i+r places the user-defined IV into the header as if random) Adds 16-byte header inside u/m/a encoding: RandomIV $+ X. Where X is Parameter#4 when 'i' also used, or X is the random 8-byte value when 'r' used without 'i'.
z 1-8 0x00's padding ( no more than 1 of 'z n p' should be used at the same time)
n padding 0x80 plus 0-7 0x00's
p 1-8 0x20 spaces padding
Notes:
- 'N' parameter uses the same rules as non-encryption. It's an optional last parameter after those required by the presence of other switches ('ec' require <key>, 'c(s|i)' also require <salt|iv>)
- 'e' ECB mode is invalid syntax with switches 's|i|r'. In ECB mode, same key always encrypts identical groups of 8 plaintext bytes into the same 8 ciphertext bytes. Key is always literal UTF-8 'key' parameter of 0-72 bytes, except 'l' switch limits literal ECB key to 56.
- 'c' default random salt allows same key to create different session-key:IV each time salt differs. If 8-byte random salt is randomly distributed across all 2^64 values, would require more than 2^32 messages before probability of any 2 matching 'salt' values reaches 50%.
- Salt/IV parameter is silently truncated to 8 bytes, and is padded to length 8 with 0x00 bytes if shorter. ASCII 128-255 are not UTF-8 encoded into byte-pairs. Beginning v7.56, codepoints 256+ are invalid syntax instead of being replaced by the $chr(63) question mark. $encode does not disallow invalid syntax of $null key parameter#3.
- Blowfish 'c' encryption by default has 16-byte header beneath the encoding: The 8-byte string Salted__ followed by 8 'random' bytes. If using the 's' switch, the 4th 'salt' parameter is placed in the header instead.
- any of s|i|r disables default random salt, and s|i require presence of 4th Salt/IV parameter
- <without s|i|r> Header is: Salted__ $+ 8-random-bytes
- 's' changes header to: 'Salted__' $+ Parameter-#4
- 'r' changes header to: 'RandomIV' $+ 8-random-bytes
- 'ir' changes header to: 'RandomIV' $+ Parameter-#4
- 'i' without 'r' means Parameter-#4 user defined IV is required, but is kept a secret without being revealed in the header.
Note: 'i' is the only option that does not "blow your cover," as any of the other (s|r|ir|<none>) options above will positively identify that the message is Blowfish encrypted by placing a plain-text header ("Salted__" or "RandomIV") at the beginning of the encoded digest, along with the Salt/IV that was used. Using 'i' will be the most popular choice for this reason. You must provide a #4 parameter, but you can leave it empty for all 0x00's IV, or use a predictable changing IV like the current time, an incriminating nonce, or use it as a second 8-character secret password.
maroon alternate note: 'ci' cannot be the popular choice without a way for the decryptor to know the IV used when encrypting. Blowing your cover can include sending mime strings which always decode to a multiple of length 8. Solutions can be, as described in examples: Stripping the 1st 8 bytes of the header but leaving the random salt/iv and having the decryptor insert the text header before feeding to $decode. Also, 'blow your cover' can include using a salt parm left in the encoded string which never contain the 0x00 byte, or worse if they are always simple alphanumeric text. You can minimize this issue by using the $randsalt alias for creating random Salt/IV parms, or using the examples below which avoid storing the "Salted__" or "RandomIV" string in the header, but leave the other (hopefully) random parm. The purpose of a Salt/IV is to be unique, and using $ctime results in different nicks using matching Salt/IV often because everyone is using the same narrow group of salt/IV values which slowly shifts across time. i.e. $encode ignores the 9th-and-later characters of the Salt/IV parameter, and the 1st 8 digits of $ctime changes only every 100 seconds.
maroon conclusion: Popular choice should be:
1. Avoid using Blowfish function prior to v7.56, as it fixes several weaknesses and security vulnerabilities mentioned here.
2. Use either no-padding-switch or 'n' to avoid false matching the original message as if padding.
3. If using v7.56+, use 'c' without the 's' switch to have a random salt. If worried about the 'Salted__" header, you can script the removal/reinstatement of the header as in example below.
4. If using v7.55+earlier, use 'c' WITH the 's' switch, but use the $randsalt alias to define a random salt string which cannot contain embedded 0x00's. Be sure to avoid keys longer than 56 bytes in v7.52-7.55, as bytes 57+ are silently ignored.
5. Always avoid using 'r' and/or 'i' switches without the 'l' switch, because the method used to hash the key parameter lowers the strength of the key parameter down to 'only' 128 bits. Even a literal key of 56 hex digits with the 'l' switch would've had 2^224 possible combos.
Padding of 8-byte blocks with 1-8 bytes of padding ensures encryption sees binary message length as exact multiple of 8. Padding is added by $encode depending which of the 'n|p|z|none' padding switches are used.
- default: if 'npz' not specified, PKCS#7 padding = appends 1-8 of $chr(N) where N is the number of bytes to be padded. ie: padding length 1 = $chr(1), 2 = 2x $chr(2)'s, ..., 8x $chr(8)'s
- 'n' Bit Padding = Appends $chr(128) character followed by 0-7 0x00's
- 'p' Appends 1-8 $chr(32) spaces
- 'z' Appends 1-8 0x00 nulls
- Due to the broken by-design behavior of $decode, the 'z' padding switch should NOT be used, even for text messages which cannot contain the 0x00's appended by 'z' padding. Instead of removing the indicated type of padding, $decode ignores any padding switches used, and instead searches for all 4 supported types of padding. 'z' padding has a false match with 'n' padding if the plaintext ends with the 0x80 byte, as happens for ALL unicode codepoints 128+ which are a multiple of 64. You cannot reliably detect use of bad keys, because decrypting with the wrong password has a 4/256 chance of false matching padding due to matching at minimum final byte values being any of the 4: 0x00 0x20 0x01 0x80.
'p' padding is OK to use with text messages where removing trailing spaces is harmless. But due to the broken $decode handling of padding, you should avoid using 'p' or 'z' padding, and should use only the 'n' switch or PKCS#7 from no-padding-switch used.
If possible, use mIRC version v7.56+ which fixes most of the security issues with $encode. Starting with v7.56:
1. 'c' used without 'l' switch for non-literal keys no longer silently chops hashed key parameter to 56 bytes
2. 'i' and 's' switches no longer permit strings containing codepoint 256+ which were each silently replaced with the same codepoint 63.
3. Salts are now treated as 8-byte binary strings instead of being truncated at the first 0x00 byte. This formerly resulted in a significant portion of unique random salts being treated as if identical. i.e. 1/256 of all random salts began with 0x00, and were all treated as identical $null salt.
4. No longer accepts invalid literal key parameter longer than 56 bytes then silently chops to 56 bytes.
Remaining security issues your script needs to avoid:
1. $encode/$decode accept invalid $null key parameter
2. $encode accepts invalid 's|i' parameters longer than 8 bytes, then silently chops them to the valid length 8 bytes.
3. $decode's by-design bug of ignoring padding switches and instead searching for match with 4 types of padding means you should avoid using 'z' padding when there's a possibility that the final UTF8-encoded byte of the message is 0x80.
Incompatibilities between v7.56+ and earlier versions due to fixing bugs. Possible workarounds are explained in more detail at https://forums.mirc.com/ubbthreads.php/topics/265396/re-invalid-key-lengths-in-encode-data-lt-e-l-cl-gt-key#Post265396
1. For the approx 3.1% of random 64-bit salt strings which contain at least 1 0x00 byte, the incompatible salt string handling can make it difficult to manipulate old messages so they can be decoded in new mIRC, but is generally impossible for older mIRC's to decode those 3.1% of messages created in newer mIRC.
2. For older messages created where byte values 57+ of the key parameter are ignored, v7.56+ can use $left(key parameter,56) to decode them, as long as the 56-byte string does not end with a partial UTF-8 character encoding.
3. For older messages created using 's|i' parameters containing codepoints 256+, these were encrypted using the '?' character instead, so v7.56+ can simply use the '?' in all salt|IV parameters which formerly included those characters, which are now invalid.
The strongest attack against Blowfish is the SWEET32 attack against the 64-bit block size. You can mitigate this problem by avoiding the encryption of 100's of megabytes of data using the same encryption key. A random+unique salt is enough to defend against this attack, since each message then uses a different encryption key. Note this attack works against identical encryption key using the identical IV, and cannot be defended against by using a longer encryption key.
Examples
Echo to the active screen the following encode text, using Mime (base64) encoding:
//echo -a $encode(Hello there! This will be encoded using Mime.,m)
Mime encodes 3 input bytes as 4 output text characters using a 64-item alphabet, padded with '=' to a length that's a multiple of 4.
The 'N' parameter is ignored by $decode, but is used to encode individual chunks of long binary variables, potentially chopping UTF8 encoding characters across separate chunks. When 'N' is 0, returns the number of chunks in the input string, otherwise for N >= 1 it encodes the Nth group of 45 bytes in the input string. Note how this example splits the encoding, resulting in chunk 1 ending with the alt+195 byte and the chunk 2 begins with the alt+169 byte: //var -s %a $str($chr(233),100)) , %b $encode(%a,m,1) , %c $encode(%a,m,2) , %d $decode(%b,m) vs $decode(%c,m)
$encode encrypts the file then applies u/m/a coding to change binary encrypted data to text. Decoding with matching u/m/a without using e|c displays the header and cipher binary hidden beneath (some strings can be truncated when 0x00 are encountered in decoded mime string): //var %a $encode(text,csm,key,ParmSalt) | echo -a %a -> $decode(%a,m)
/* These are 2 methods of encrypting channel messages where everyone in a channel uses the same shared password. They intentionally does not support '/me action' or /query windows, and handle only 1-word messages of length 25+. To defend against different users unknowingly using the same salt, the messages include the sender's nick as part of the key, which causes the same salt to NOT generate the same encryption key. Message is encrypted using a random salt, but the 'Salted__' header is removed from the mimed string before sent to channel, then added to message before decryption. Add 'g' switch to echoes to avoid them being logged. */ ON *:TEXT:=*:##maroon,#channelname: { if (($2 != $null) || ($len($1) < 25)) return | fake_secret_chat $1 } ON &*:INPUT:##maroon,#channelname:{ if ((/* iswm $1) || ($ctrlenter) || ($inpaste)) return | fake_secret_chat $1- } ON *:CONNECT:{ if (!$hget(secret_chat)) hload secret_chat secret_chat.txt } ON *:EXIT:{ if ( $hget(secret_chat)) hsave secret_chat secret_chat.txt } ; remove the number from either alias name ending with chat1 or chat2 alias fake_secret_chat1 { if (!$chan) return ; //hadd -m secret_chat $network $+ ##maroon Change this Shared Secret var %main_key $hget(secret_chat,$network $+ $chan) if (%main_key == $null) { echo -g $chan Note: Secret_Chat halted due to missing password! add password: /hadd -m secret_chat $network $+ $chan Password goes here | return } if ($event == input) { bset -tc &secret_chat_msg 1 $encode($1-,mc,$me %main_key) | noop $decode(&secret_chat_msg,bm) bcopy -c &secret_chat_msg 1 &secret_chat_msg 9 -1 | noop $encode(&secret_chat_msg,bm) msg # = $+ $bvar(&secret_chat_msg,1-).text | echo 3 -tc own # Channel sees Encryption of: $+(<,$nick($chan,$me).pnick,>) $1- halt } else { bset -tc &secret_chat_msg 1 $mid($1,2) | noop $decode(&secret_chat_msg,bm) bcopy &secret_chat_msg 9 &secret_chat_msg 1 -1 bset -t &secret_chat_msg 1 Salted__ | noop $encode(&secret_chat_msg,bm) $decode(&secret_chat_msg,bmc,$nick %main_key) echo -tcl normal # Decoded $+(<,$nick,>,:) $bvar(&secret_chat_msg,1-).text } } /* This is a more complicated channel encryption, which includes a shared salt contained in the #channel topic, allowing the same shared password to behave similar to an unrelated password each time the #topic salt changes. This uses a superior method to hash the key parameter without using MD5. Each message generates a new 9-byte string where each byte can be values 0-255, and the 12-byte mime-encoding of this string is included in the message sent to the channel, instead of the actual IV or Salt used to encrypt the message. This mime-string + sender's nick + topic salt + main password are hashed with SHA-512 to create a 64-byte secret string which is split into a 56-byte literal key parameter and an 8-byte salt parameter. Since text salt parameters are truncated if they contain embedded 0x00 bytes, the 9-byte message salt is changed to avoid strings where the 1st 7 bytes contain the 0x00 byte. The attacker cannot know either the key parameter or the IV without also knowing the %main_key for that channel. Assuming the 9-byte string is generated randomly, the only way to identify which of the 2 aliases was used to encrypt the message is whether the mimed string's length is 8N or 8N+1. */ alias fake_secret_chat2 { if (!$chan) return ; //hadd -m secret_chat $network $+ ##maroon Change this Shared Secret noop $regex(foo,$chan($chan).topic,salt:(\S+) ) | var %chan.salt $regml(foo,1) var %main_key $hget(secret_chat,$network $+ $chan) if (%main_key == $null) { echo -g $chan Note: Secret_Chat halted due to missing password! add password: /hadd -m secret_chat $network $+ $chan Password goes here | return } if ($event == input) { :make_another_salt bset -c &secret_chat_salt 1 $regsubex(foo,$str(x,9),/x/g,$rand(0,255) $chr(32)) noop $encode(&secret_chat_salt,bm) | var %msg.salt $bvar(&secret_chat_salt,1-).text bset -c &secret_chat_digest 1 $regsubex(foo,$sha512(%msg.salt $me %chan.salt %main_key %chan.salt),/(..)/g,$base(\t,16,10) $chr(32)) if ($istok($bvar(&secret_chat_digest,1-7),0,32)) goto make_another_salt var -p %iv $regsubex(foo,$bvar(&secret_chat_digest,1-8),/(\d+)\s?/g,$chr(\t)) noop $encode(&secret_chat_digest,bm) var %session_key $bvar(&secret_chat_digest,12,56).text , %text $encode($1-,mcli,%session_key,%iv) var %msg $+(=,%msg.salt,%text) | msg # %msg | echo 3 -tc own # Channel sees Encryption of: $+(<,$nick($chan,$me).pnick,>) $1- halt } else { bset -c &secret_chat_digest 1 $regsubex(foo,$sha512($mid($1,2,12) $nick %chan.salt %main_key %chan.salt),/(..)/g,$base(\t,16,10) $chr(32)) var -p %iv $regsubex(foo,$bvar(&secret_chat_digest,1-8),/(\d+)\s?/g,$chr(\t)) noop $encode(&secret_chat_digest,bm) var %session_key $bvar(&secret_chat_digest,12,56).text , %text $decode($mid($1,14),mcli,%session_key,%iv) echo -tc normal # Decoded $+(<,$nick,>,:) %text } }
If using pre-v7.56 random salt, or if using 'ir|s' switches to create user-defined Salt's or IV's, you SHOULD use the $randsalt alias to salt strings from the entire valid text range of codepoints 1-255. This increases the possible combinations to as high as 255^8, which is the 96.9% of valid 8-byte strings which don't contain the 0x00 byte. This makes it much less likely where the birthday paradox produces messages with identical salt/iv's.
alias randsalt returnex $regsubex($str(x,8),/x/g,$chr($rands(1,255))) alias randsalt returnex $regsubex($str(x,8),/x/g,$chr($rand( 1,255))) alias randsalt { :retry | var %a $regsubex(foo,$str(x,8),/x/g,$rands(0,255) $chr(32)) , %i 1 , %j 2 while (%i isnum 1-7) { if (($gettok(%a,%i,32) == 0) && ($gettok(%a,%j,32) > 0)) goto retry | inc %i | inc %j } returnex $regsubex(foo,%a,/(\d+)\s*/g,$chr(\t)) } (1st variant uses $rands, 2nd uses $rand, 3rd allows salt to contain 0x00 if not preceding byte values 1-255)
- Note: Because 'r' switch is CBC mode without authentication, the decrypted message is vulnerable to trace-less bit-flipping of the 1st 8 bytes of the message into anything the attacker wishes by simply manipulating the IV and knowing the exact contents of the 1st 8 bytes of the plaintext message. The attacker does not need to know the key, nor even need to see how the message has been encrypted. This is not a Blowfish weakness, as the same thing would happen with AES where the larger blocksize would expose 16 bytes instead of 8. As an example, use any message where the 1st 8 bytes are all alpha characters and which do NOT contain a space character. Then encrypt with any lower-case text salt, and decrypt with the upper-case equivalent. This results in the decrypted message having the upper/lower case of each text character flipped, but the remainder of the message is NOT affected:
//var -s %iv $regsubex($str(x,8),/x/g,$rand(a,z)) , %msg $encode(BitFlipping Example,mci,key,$upper(%iv)) | echo -a $decode(%msg,mci,key,$lower(%iv)) result: bITfLIPPing Example
Solution: If worried about this, you should avoid using the 'i' and/or 'r' switches, and always use either a random or user-defined salt. Using a 'salt' would then hash key_parameter+salt into binary_56_byte_hashed_key+hash_derived_IV, which shields the IV from someone who doesn't know the key. If they tried to bit-flip the salt, the generated encryption key and IV would be completely different, and the message would decrypt to garbage. Another way would be to include 8 garbage bytes preceding the actual message. The garbage bytes would be thrown away and only bytes 9+ of the decoded message are used.
Note: If using v7.52-7.55, beware of key parameter silently chopped to 56 bytes. The following examples always produce identical output in those versions each time the command is repeated, demonstrating silent ignore of key beyond byte 56.
CBC hashed-key with salt: //echo -a $version $encode(testtest,mcs,$str(a,55) $+ $chr($rand(192,255)) ,saltsalt) CBC literal key with IV: //echo -a $version $encode(testtest,mcirl,$str(a,55) $+ $chr($rand(192,255)) , (8)bytes) ECB literal key: //echo -a $version $encode(testtest,me,$str(a,55) $+ $chr($rand(192,255)) ) CBC hashed-key with IV: //echo -a $version $encode(testtest,mcir,$str(a,55) $+ $chr($rand(192,255)) , (8)bytes)
Note: 'mc' or 'mcr' or 'mcrl' switch combos also silently ignored the 57th-and-later bytes, as demonstrated by being able to decode those messages without the full key:
//var -s %msg $encode(testtest,mc,$str(a,56) $+ key) | echo -a $version : $decode(%msg,mc,$str(a,56) )
Security flaws remaining with v7.56:
1. Allows encrypting where the key parameter is $null: //echo -a $encode(message,mc,$null) Your script must check to verify that %key is not null. 2. Silently ignores salt and IV parameter beyond 8th character, producing identical outputs, such as when using $ctime as the IV during a 100-seconds interval: //echo -a $encode(message,mcs,$null,12345678 $+ $rand(a,z)) //echo -a $encode(message,mcir,$null,12345678 $+ $rand(a,z)) //echo -a $encode(message,mci,$null,12345678 $+ $rand(a,z)) (Only difference between 'i' and 'ir' is that 'ir' stuffs the IV parm into the header as if it's random. The extra 16 byte header lengthens the encrypted string. Your script must verify that $len(%salt) is not greater than 8. 3. Padding is broken because $decode does not let you recover the original message by specifying the 1 of 4 types of padding to remove. For every message encoded using 'z' padding that's not a multiple of 8 bytes and which ends with a codepoint that's a multiple of 64 greater than 64 ends with the byte value 128: $decode makes a false match with 'n' padding, causing the final byte of the last character's encoding to be stripped, resulting in the final character being displayed incorrectly. Your script should encode use the 'n' switch or using none of the n|p|z switches. //bset -t &v 8 0 | while ($bvar(&v,0) == 8) { bset -tc &v 1 $str(.,$rand(1,7)) $+ $chr($calc(64*$rand(3,1023))) } | echo -a original: = $bvar(&v,1-) $bvar(&v,1-).text | noop $encode(&v,bmcz,key) $decode(&v,bmcz,key) | echo 4 -a decrypted = $bvar(&v,1-) $bvar(&v,1-).text
- NOTE: Your key should be long enough to make it difficult for someone to brute-force guess it. You can't count on the guesser using a mIRC script which contains overhead which slows down the guessing. Beginning v7.56, you can use a key parameter as long as possible, and the entire string will be hashed to generate the key, instead of the hashing looking at only the 1st 56 bytes of the key parameter. When using 'r' without 'l', the key parameter is hashed in a way which does not generate more than 2^128 possible keys. The hash is derived using the $md5 function, so $encode(string,mcir,%key,iv) generates identical encryption passwords from all %key which have matching MD5 hash.
When using a salted key ('c' without 'r' or 'i' switches) the hash method has some 448-bit keys which can never be generated from every salt string, but a long enough key parameter can generate well over 2^400 possible encryption keys from each salt string. If avoiding use of a 'salt' by instead specifying the IV, using the switch combos 'mcrl' or 'mcril' or 'mcil', a simplistic literal hex string of length 56 can have 2^224 possible combinations.
Compatibility
Added: mIRC v5.8
Added on: 05 Sep 2000
Note: Unless otherwise stated, this was the date of original functionality.
Further enhancements may have been made in later versions.