├── README.md └── decoder.php /README.md: -------------------------------------------------------------------------------- 1 | FreeOTP Decoder 2 | = 3 | 4 | Decodes the tokens preference file from [FreeOTP](https://fedorahosted.org/freeotp/) for [Android](https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp). 5 | 6 | Outputs tokens as "Name: URI". 7 | 8 | URIs are designed to support the Google Authenticator format: 9 | https://github.com/google/google-authenticator/wiki/Key-Uri-Format 10 | 11 | Warning 12 | == 13 | Using this script is a terrible idea. It will expose your one-time-password secrets, 14 | which can be used to generate codes to pass two-factor authentication checks. 15 | 16 | This whole process should only be attempted on a secure machine with an encoded disk. 17 | Care should be taken to redirect output and/or clear scrollback. 18 | 19 | Requirements 20 | == 21 | * PHP 5.4+ 22 | * PHP SimpleXML extension (enabled by default) 23 | 24 | Preparation 25 | == 26 | Before running the decoder you must get and extract a backup file of your FreeOTP data. 27 | The most direct way is to use the Android Debug Bridge (`adb`). 28 | 29 | The general command for backup is `adb backup -f ~/freeotp.ab -noapk org.fedorahosted.freeotp` 30 | 31 | The commands to extract are `dd if=freeotp.ab bs=1 skip=24 | openssl zlib -d | tar -xvf -` 32 | or `dd if=freeotp.ab bs=1 skip=24 | python -c "import zlib,sys;sys.stdout.write(zlib.decompress(sys.stdin.read()))" | tar -xvf -` 33 | 34 | The files will be extracted into the subdirectory `apps/org.fedorahosted.freeotp` 35 | 36 | Detailed instructions are available at http://blog.shvetsov.com/2013/02/access-android-app-data-without-root.html 37 | 38 | Usage 39 | == 40 | `php decoder.php /path/to/apps/org.fedorahosted.freeotp/sp/tokens.xml` 41 | 42 | License 43 | == 44 | This script is released under the same Apache License, Version 2.0, as FreeOTP and 45 | Google Authenticator 46 | -------------------------------------------------------------------------------- /decoder.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2015 Philip Sharp 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | 21 | if ($argc != 2) { 22 | help(); 23 | exit(1); 24 | } 25 | 26 | $file = $argv[1]; 27 | 28 | if (!is_readable($file)) { 29 | echo "Cannot read '$file'." .PHP_EOL; 30 | exit(1); 31 | } 32 | 33 | $xml = simplexml_load_file($file); 34 | 35 | if (!$xml) { 36 | echo "Cannot load XML token file." . PHP_EOL; 37 | exit(1); 38 | } 39 | 40 | if (count($xml->string) == 0) { 41 | echo "Invalid XML token file." . PHP_EOL; 42 | exit(1); 43 | } 44 | 45 | foreach($xml->string as $s) { 46 | $name = (string)$s['name']; 47 | $json = (string)$s; 48 | $data = json_decode($json); 49 | if (!$data) { // bad JSON 50 | echo "$name: invalid data" . PHP_EOL; 51 | continue; 52 | } 53 | if (!isset($data->secret)) { // not a token 54 | continue; 55 | } 56 | echo $name . ': '; 57 | echo makeUri($data); 58 | echo PHP_EOL; 59 | } 60 | 61 | function help() { 62 | global $argv; 63 | echo "Usage: php {$argv[0]} tokens-xml-file" . PHP_EOL; 64 | echo PHP_EOL; 65 | } 66 | 67 | /** 68 | * Convert FreeOTP token data into standard otpauth URI 69 | * 70 | * Based on the Token::toURI() method from FreeOTP 71 | * 72 | * @param stdObject $token FreeOTP token data structure 73 | * @return string otpauth URI 74 | */ 75 | function makeUri($token) { 76 | $label = isset($token->issuerExt) ? $token->issuerExt . ':' . $token->label: $token->label; 77 | $baseUri = 'otpauth://' . urlencode(strtolower($token->type)) . '/' . urlencode($label); 78 | $params = [ 79 | 'secret' => encodeSecretBytes($token->secret), 80 | 'issuer' => isset($token->issuerInt) ? $token->issuerInt : $token->issuerExt, 81 | 'algorithm' => $token->algo, 82 | 'digits' => $token->digits, 83 | 'period' => $token->period, 84 | ]; 85 | return $baseUri . '?' . http_build_query($params); 86 | } 87 | 88 | /** 89 | * Convert stored secret into otpauth URI parameter 90 | * 91 | * Port of encodeInternal() method from Google Authenticator via FreeOTP 92 | * @link https://fedorahosted.org/freeotp/browser/android/app/src/main/java/com/google/android/apps/authenticator/Base32String.java 93 | * 94 | * @param array $data Internal representation of the secret as byte array 95 | * @return string Secret as Base32 encode string 96 | */ 97 | function encodeSecretBytes($data) { 98 | if (empty($data)) { 99 | return ''; 100 | } 101 | 102 | $DIGITS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // 32 chars 103 | $SHIFT = 5; // trailing zeros in binary representation of the size of the alphabet 104 | $MASK = 31; // one less than size of alphabet 105 | 106 | // SHIFT is the number of bits per output character, so the length of the 107 | // output is the length of the input multiplied by 8/SHIFT, rounded up. 108 | if (count($data) >= (1 << 28)) { 109 | // The computation below will fail, so don't do it. 110 | throw new UnexpectedValueException('Bad Secret'); 111 | } 112 | 113 | $outputLength = (count($data) * 8 + $SHIFT - 1) / $SHIFT; 114 | $result = ''; 115 | 116 | $buffer = $data[0]; 117 | $next = 1; 118 | $bitsLeft = 8; 119 | while ($bitsLeft > 0 || $next < count($data)) { 120 | if ($bitsLeft < $SHIFT) { 121 | if ($next < count($data)) { 122 | $buffer <<= 8; 123 | $buffer |= ($data[$next++] & 0xff); 124 | $bitsLeft += 8; 125 | } else { 126 | $pad = $SHIFT - $bitsLeft; 127 | $buffer <<= $pad; 128 | $bitsLeft += $pad; 129 | } 130 | } 131 | $index = $MASK & ($buffer >> ($bitsLeft - $SHIFT)); 132 | $bitsLeft -= $SHIFT; 133 | $result .= $DIGITS[$index]; 134 | } 135 | return $result; 136 | } 137 | --------------------------------------------------------------------------------