├── LICENSE ├── README.md ├── apple ├── README.md └── iphone-local-store-availability.js └── smart ├── README.md ├── modules ├── LICENSE └── hashes.js ├── smart-lockscreen-info.js └── smart-one-info-small.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 marco79cgn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ios-scriptable-widgets 2 | A collection of my iOS widgets for the Scriptable app 3 | 4 | ## Requirements 5 | 6 | - iOS 16 (or later) 7 | - [Scriptable](https://apps.apple.com/us/app/scriptable/id1405459188) app 1.7 (or later) 8 | 9 | ## Disclaimer 10 | I have no affiliation with Apple or any other brand and build these widgets just for fun on my free time. Those are not official products. 11 | -------------------------------------------------------------------------------- /apple/README.md: -------------------------------------------------------------------------------- 1 | ## iPhone Local Store Availability 2 | Shows whether your desired iPhone is available for pickup at your nearest Apple Store. In this example, the left one is an iPhone SE (2020) and the right one is an iPhone 14 Pro. Tapping on the icon will open the Apple Store app with your chosen product to assure a fast order process. 3 | 4 | 5 | 6 | You can configure both the part number (e.g. MQ1F3ZD/A) and zip code (50670) as widget parameter, separated with semicolon. 7 | 8 | 9 | -------------------------------------------------------------------------------- /apple/iphone-local-store-availability.js: -------------------------------------------------------------------------------- 1 | // Insert your zip code or the one of your desired Apple Store 2 | let zip = '50670' 3 | // Insert the part number of your desired iPhone 4 | // for example iPhone 14 Pro 256GB Deep Purple 5 | let partNo = "MQ1F3ZD/A" 6 | 7 | let params = getParams(args.widgetParameter) 8 | const shopAvailability = await getShopAvailability() 9 | 10 | let widget = new ListWidget() 11 | let symbol 12 | if (shopAvailability.message.toLowerCase().indexOf('nicht') >= 0 || shopAvailability.message.toLowerCase().indexOf('ab') >= 0) { 13 | symbol = widget.addImage(SFSymbol.named("iphone.slash.circle").image) 14 | } else { 15 | symbol = widget.addImage(SFSymbol.named("iphone.circle").image) 16 | } 17 | symbol.imageSize = new Size(60, 60) 18 | symbol.centerAlignImage() 19 | 20 | widget.url = "https://store.apple.com/de/xc/product/" + params.partNo 21 | 22 | if (config.runsInApp) { 23 | widget.presentSmall() 24 | } 25 | Script.setWidget(widget) 26 | Script.complete() 27 | 28 | // fetches the local shop availability 29 | async function getShopAvailability() { 30 | let availabilityMessage 31 | let productName 32 | let storeName 33 | let city 34 | const url = "https://www.apple.com/de/shop/retail/pickup-message?pl=true&parts.0=" 35 | + params.partNo + "&location=" + params.zip 36 | let req = new Request(url) 37 | try { 38 | let result = await req.loadJSON() 39 | const store = result.body.stores[0] 40 | const item = result.body.stores[0].partsAvailability[params.partNo] 41 | 42 | availabilityMessage = item.pickupSearchQuote 43 | productName = item.messageTypes.regular.storePickupProductTitle 44 | storeName = store.storeName 45 | city = store.city 46 | 47 | } catch (exception) { 48 | console.log("Exception Occured.") 49 | availabilityMessage = "N/A" 50 | productName = "Keine Internetverbindung" 51 | storeName = "Oder ungültige" 52 | city = "PartNo" 53 | } 54 | return { "message": availabilityMessage, "product": productName, "storeName": storeName, "city": city } 55 | } 56 | 57 | // parses the widget params to overwrite part number and zip code 58 | function getParams(widgetParams) { 59 | if (widgetParams) { 60 | let split = widgetParams.split(';') 61 | partNo = split[0] 62 | if (split.length > 1) { 63 | zip = split[1] 64 | } 65 | } 66 | return { "partNo": partNo, "zip": zip } 67 | } 68 | -------------------------------------------------------------------------------- /smart/README.md: -------------------------------------------------------------------------------- 1 | ## Smart #1 Info Widget 2 | 3 | 4 | 5 |
iOS Scriptable widget that shows basic information of your Smart #1 car like the current position and battery state. Tapping on the widget will open the official [Hello Smart app](https://apps.apple.com/de/app/hello-smart/id6443878915). 6 | 7 | ### Setup 8 | - Create your own (free) [HERE account](https://platform.here.com) 9 | - Create a HERE app to obtain your api key 10 | - Create a folder named `modules` inside your iCloud Drive Scriptable folder. 11 | - Download the file [hashes.js](https://raw.githubusercontent.com/marco79cgn/ios-scriptable-widgets/main/smart/modules/hashes.js) into the modules folder you just created 12 | - Open the Scriptable app on your iPhone, click on the "+" sign on the upper right, copy the [source code](https://raw.githubusercontent.com/marco79cgn/ios-scriptable-widgets/main/smart/smart-one-info-small.js) above and paste it inside. 13 | - Name the script (at the top of the App) and save it by pressing "Done" in the upper left. 14 | - Go to your homescreen, long press anywhere and configure a new Scriptable widget with small size. Assign the created widget. 15 | - Insert your real username, password, vin and api-key as widget parameter, separated by semicolon: `username;password;vin;apikey` 16 | 17 | 18 | 19 | - optional: you could also edit your credentials inside the script (lines 20-22), but never give this script to anyone with your hardcoded credentials! 20 | - Tip: use another user for this script who has access to your car through the digital key 21 | 22 | ## Smart #1 Lockscreen Widget 23 | 24 | 25 | 26 |
iOS Scriptable widget that shows basic information of your Smart #1 car like the current temperature and battery state on your iPhone Lockscreen. Tapping on the widget will open the official [Hello Smart app](https://apps.apple.com/de/app/hello-smart/id6443878915). 27 | 28 | ### Setup 29 | - Create a folder named `modules` inside your iCloud Drive Scriptable folder. 30 | - Download the file [hashes.js](https://raw.githubusercontent.com/marco79cgn/ios-scriptable-widgets/main/smart/modules/hashes.js) into the modules folder you just created 31 | - Open the Scriptable app on your iPhone, click on the "+" sign on the upper right, copy the [source code](https://raw.githubusercontent.com/marco79cgn/ios-scriptable-widgets/main/smart/smart-lockscreen-info.js) above and paste it inside. 32 | - Name the script (at the top of the App) and save it by pressing "Done" in the upper left. 33 | - Go to your iPhone homescreen, swipe down from the upper middle position to see the lock screen, then long press anywhere and press "customize" to configure a new Scriptable widget with bigger size. Assign the created widget. 34 | 35 | 36 | 37 | - Insert your real username, password, and vin as widget parameter, separated by semicolon: `username;password;vin` 38 | 39 | 40 | 41 | - Press the "Done" button in the upper right to finish the setup. 42 | 43 | ### Icons 44 | Temperatur Icons erstellt von Good Ware - Flaticon 45 | 46 | E-mobilität Icons erstellt von Danteee82 - Flaticon 47 | 48 | Charging icons created by redempticon - Flaticon 49 | 50 | 51 | ### Disclaimer 52 | Es handelt sich um ein von mir selbst entwickeltes Spaßprojekt, **kein** offizielles Produkt der Marke smart. Ich stehe in keinerlei Beziehung zu smart und bekomme weder Provision noch sonstiges. 53 | -------------------------------------------------------------------------------- /smart/modules/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2017, Tomas Aparicio 2 | Copyright (c) 1999-2012, Paul Johnston, Angel Marin, Jeremy Lin 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /smart/modules/hashes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jshashes - https://github.com/h2non/jshashes 3 | * Released under the "New BSD" license 4 | * 5 | * Algorithms specification: 6 | * 7 | * MD5 - http://www.ietf.org/rfc/rfc1321.txt 8 | * RIPEMD-160 - http://homes.esat.kuleuven.be/~bosselae/ripemd160.html 9 | * SHA1 - http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf 10 | * SHA256 - http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf 11 | * SHA512 - http://csrc.nist.gov/publications/fips/fips180-4/fips-180-4.pdf 12 | * HMAC - http://www.ietf.org/rfc/rfc2104.txt 13 | */ 14 | (function() { 15 | var Hashes; 16 | 17 | function utf8Encode(str) { 18 | var x, y, output = '', 19 | i = -1, 20 | l; 21 | 22 | if (str && str.length) { 23 | l = str.length; 24 | while ((i += 1) < l) { 25 | /* Decode utf-16 surrogate pairs */ 26 | x = str.charCodeAt(i); 27 | y = i + 1 < l ? str.charCodeAt(i + 1) : 0; 28 | if (0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) { 29 | x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); 30 | i += 1; 31 | } 32 | /* Encode output as utf-8 */ 33 | if (x <= 0x7F) { 34 | output += String.fromCharCode(x); 35 | } else if (x <= 0x7FF) { 36 | output += String.fromCharCode(0xC0 | ((x >>> 6) & 0x1F), 37 | 0x80 | (x & 0x3F)); 38 | } else if (x <= 0xFFFF) { 39 | output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), 40 | 0x80 | ((x >>> 6) & 0x3F), 41 | 0x80 | (x & 0x3F)); 42 | } else if (x <= 0x1FFFFF) { 43 | output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), 44 | 0x80 | ((x >>> 12) & 0x3F), 45 | 0x80 | ((x >>> 6) & 0x3F), 46 | 0x80 | (x & 0x3F)); 47 | } 48 | } 49 | } 50 | return output; 51 | } 52 | 53 | function utf8Decode(str) { 54 | var i, ac, c1, c2, c3, arr = [], 55 | l; 56 | i = ac = c1 = c2 = c3 = 0; 57 | 58 | if (str && str.length) { 59 | l = str.length; 60 | str += ''; 61 | 62 | while (i < l) { 63 | c1 = str.charCodeAt(i); 64 | ac += 1; 65 | if (c1 < 128) { 66 | arr[ac] = String.fromCharCode(c1); 67 | i += 1; 68 | } else if (c1 > 191 && c1 < 224) { 69 | c2 = str.charCodeAt(i + 1); 70 | arr[ac] = String.fromCharCode(((c1 & 31) << 6) | (c2 & 63)); 71 | i += 2; 72 | } else { 73 | c2 = str.charCodeAt(i + 1); 74 | c3 = str.charCodeAt(i + 2); 75 | arr[ac] = String.fromCharCode(((c1 & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); 76 | i += 3; 77 | } 78 | } 79 | } 80 | return arr.join(''); 81 | } 82 | 83 | /** 84 | * Add integers, wrapping at 2^32. This uses 16-bit operations internally 85 | * to work around bugs in some JS interpreters. 86 | */ 87 | 88 | function safe_add(x, y) { 89 | var lsw = (x & 0xFFFF) + (y & 0xFFFF), 90 | msw = (x >> 16) + (y >> 16) + (lsw >> 16); 91 | return (msw << 16) | (lsw & 0xFFFF); 92 | } 93 | 94 | /** 95 | * Bitwise rotate a 32-bit number to the left. 96 | */ 97 | 98 | function bit_rol(num, cnt) { 99 | return (num << cnt) | (num >>> (32 - cnt)); 100 | } 101 | 102 | /** 103 | * Convert a raw string to a hex string 104 | */ 105 | 106 | function rstr2hex(input, hexcase) { 107 | var hex_tab = hexcase ? '0123456789ABCDEF' : '0123456789abcdef', 108 | output = '', 109 | x, i = 0, 110 | l = input.length; 111 | for (; i < l; i += 1) { 112 | x = input.charCodeAt(i); 113 | output += hex_tab.charAt((x >>> 4) & 0x0F) + hex_tab.charAt(x & 0x0F); 114 | } 115 | return output; 116 | } 117 | 118 | /** 119 | * Encode a string as utf-16 120 | */ 121 | 122 | function str2rstr_utf16le(input) { 123 | var i, l = input.length, 124 | output = ''; 125 | for (i = 0; i < l; i += 1) { 126 | output += String.fromCharCode(input.charCodeAt(i) & 0xFF, (input.charCodeAt(i) >>> 8) & 0xFF); 127 | } 128 | return output; 129 | } 130 | 131 | function str2rstr_utf16be(input) { 132 | var i, l = input.length, 133 | output = ''; 134 | for (i = 0; i < l; i += 1) { 135 | output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, input.charCodeAt(i) & 0xFF); 136 | } 137 | return output; 138 | } 139 | 140 | /** 141 | * Convert an array of big-endian words to a string 142 | */ 143 | 144 | function binb2rstr(input) { 145 | var i, l = input.length * 32, 146 | output = ''; 147 | for (i = 0; i < l; i += 8) { 148 | output += String.fromCharCode((input[i >> 5] >>> (24 - i % 32)) & 0xFF); 149 | } 150 | return output; 151 | } 152 | 153 | /** 154 | * Convert an array of little-endian words to a string 155 | */ 156 | 157 | function binl2rstr(input) { 158 | var i, l = input.length * 32, 159 | output = ''; 160 | for (i = 0; i < l; i += 8) { 161 | output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); 162 | } 163 | return output; 164 | } 165 | 166 | /** 167 | * Convert a raw string to an array of little-endian words 168 | * Characters >255 have their high-byte silently ignored. 169 | */ 170 | 171 | function rstr2binl(input) { 172 | var i, l = input.length * 8, 173 | output = Array(input.length >> 2), 174 | lo = output.length; 175 | for (i = 0; i < lo; i += 1) { 176 | output[i] = 0; 177 | } 178 | for (i = 0; i < l; i += 8) { 179 | output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (i % 32); 180 | } 181 | return output; 182 | } 183 | 184 | /** 185 | * Convert a raw string to an array of big-endian words 186 | * Characters >255 have their high-byte silently ignored. 187 | */ 188 | 189 | function rstr2binb(input) { 190 | var i, l = input.length * 8, 191 | output = Array(input.length >> 2), 192 | lo = output.length; 193 | for (i = 0; i < lo; i += 1) { 194 | output[i] = 0; 195 | } 196 | for (i = 0; i < l; i += 8) { 197 | output[i >> 5] |= (input.charCodeAt(i / 8) & 0xFF) << (24 - i % 32); 198 | } 199 | return output; 200 | } 201 | 202 | /** 203 | * Convert a raw string to an arbitrary string encoding 204 | */ 205 | 206 | function rstr2any(input, encoding) { 207 | var divisor = encoding.length, 208 | remainders = Array(), 209 | i, q, x, ld, quotient, dividend, output, full_length; 210 | 211 | /* Convert to an array of 16-bit big-endian values, forming the dividend */ 212 | dividend = Array(Math.ceil(input.length / 2)); 213 | ld = dividend.length; 214 | for (i = 0; i < ld; i += 1) { 215 | dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); 216 | } 217 | 218 | /** 219 | * Repeatedly perform a long division. The binary array forms the dividend, 220 | * the length of the encoding is the divisor. Once computed, the quotient 221 | * forms the dividend for the next step. We stop when the dividend is zerHashes. 222 | * All remainders are stored for later use. 223 | */ 224 | while (dividend.length > 0) { 225 | quotient = Array(); 226 | x = 0; 227 | for (i = 0; i < dividend.length; i += 1) { 228 | x = (x << 16) + dividend[i]; 229 | q = Math.floor(x / divisor); 230 | x -= q * divisor; 231 | if (quotient.length > 0 || q > 0) { 232 | quotient[quotient.length] = q; 233 | } 234 | } 235 | remainders[remainders.length] = x; 236 | dividend = quotient; 237 | } 238 | 239 | /* Convert the remainders to the output string */ 240 | output = ''; 241 | for (i = remainders.length - 1; i >= 0; i--) { 242 | output += encoding.charAt(remainders[i]); 243 | } 244 | 245 | /* Append leading zero equivalents */ 246 | full_length = Math.ceil(input.length * 8 / (Math.log(encoding.length) / Math.log(2))); 247 | for (i = output.length; i < full_length; i += 1) { 248 | output = encoding[0] + output; 249 | } 250 | return output; 251 | } 252 | 253 | /** 254 | * Convert a raw string to a base-64 string 255 | */ 256 | 257 | function rstr2b64(input, b64pad) { 258 | var tab = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 259 | output = '', 260 | len = input.length, 261 | i, j, triplet; 262 | b64pad = b64pad || '='; 263 | for (i = 0; i < len; i += 3) { 264 | triplet = (input.charCodeAt(i) << 16) | (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) | (i + 2 < len ? input.charCodeAt(i + 2) : 0); 265 | for (j = 0; j < 4; j += 1) { 266 | if (i * 8 + j * 6 > input.length * 8) { 267 | output += b64pad; 268 | } else { 269 | output += tab.charAt((triplet >>> 6 * (3 - j)) & 0x3F); 270 | } 271 | } 272 | } 273 | return output; 274 | } 275 | 276 | Hashes = { 277 | /** 278 | * @property {String} version 279 | * @readonly 280 | */ 281 | VERSION: '1.0.6', 282 | /** 283 | * @member Hashes 284 | * @class Base64 285 | * @constructor 286 | */ 287 | Base64: function() { 288 | // private properties 289 | var tab = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 290 | pad = '=', // default pad according with the RFC standard 291 | url = false, // URL encoding support @todo 292 | utf8 = true; // by default enable UTF-8 support encoding 293 | 294 | // public method for encoding 295 | this.encode = function(input) { 296 | var i, j, triplet, 297 | output = ''; 298 | 299 | pad = pad || '='; 300 | input = (utf8) ? utf8Encode(input) : input; 301 | len = input.length; 302 | 303 | for (i = 0; i < len; i += 3) { 304 | triplet = (input.charCodeAt(i) << 16) | (i + 1 < len ? input.charCodeAt(i + 1) << 8 : 0) | (i + 2 < len ? input.charCodeAt(i + 2) : 0); 305 | for (j = 0; j < 4; j += 1) { 306 | if (i * 8 + j * 6 > len * 8) { 307 | output += pad; 308 | } else { 309 | output += tab.charAt((triplet >>> 6 * (3 - j)) & 0x3F); 310 | } 311 | } 312 | } 313 | return output; 314 | }; 315 | 316 | // public method for decoding 317 | this.decode = function(input) { 318 | // var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; 319 | var i, o1, o2, o3, h1, h2, h3, h4, bits, ac, 320 | dec = '', 321 | arr = []; 322 | if (!input) { 323 | return input; 324 | } 325 | 326 | i = ac = 0; 327 | input = input.replace(new RegExp('\\' + pad, 'gi'), ''); // use '=' 328 | //input += ''; 329 | 330 | do { // unpack four hexets into three octets using index points in b64 331 | h1 = tab.indexOf(input.charAt(i++)); 332 | h2 = tab.indexOf(input.charAt(i++)); 333 | h3 = tab.indexOf(input.charAt(i++)); 334 | h4 = tab.indexOf(input.charAt(i++)); 335 | 336 | bits = h1 << 18 | h2 << 12 | h3 << 6 | h4; 337 | 338 | o1 = bits >> 16 & 0xff; 339 | o2 = bits >> 8 & 0xff; 340 | o3 = bits & 0xff; 341 | ac += 1; 342 | 343 | if (h3 === 64) { 344 | arr[ac] = String.fromCharCode(o1); 345 | } else if (h4 === 64) { 346 | arr[ac] = String.fromCharCode(o1, o2); 347 | } else { 348 | arr[ac] = String.fromCharCode(o1, o2, o3); 349 | } 350 | } while (i < input.length); 351 | 352 | dec = arr.join(''); 353 | dec = (utf8) ? utf8Decode(dec) : dec; 354 | 355 | return dec; 356 | }; 357 | 358 | // set custom pad string 359 | this.setPad = function(str) { 360 | pad = str || pad; 361 | return this; 362 | }; 363 | // set custom tab string characters 364 | this.setTab = function(str) { 365 | tab = str || tab; 366 | return this; 367 | }; 368 | this.setUTF8 = function(bool) { 369 | if (typeof bool === 'boolean') { 370 | utf8 = bool; 371 | } 372 | return this; 373 | }; 374 | }, 375 | 376 | /** 377 | * CRC-32 calculation 378 | * @member Hashes 379 | * @method CRC32 380 | * @static 381 | * @param {String} str Input String 382 | * @return {String} 383 | */ 384 | CRC32: function(str) { 385 | var crc = 0, 386 | x = 0, 387 | y = 0, 388 | table, i, iTop; 389 | str = utf8Encode(str); 390 | 391 | table = [ 392 | '00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 0EDB8832 ', 393 | '79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 6AB020F2 F3B97148 ', 394 | '84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 FD62F97A 8A65C9EC 14015C4F ', 395 | '63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 A2677172 3C03E4D1 4B04D447 D20D85FD ', 396 | 'A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC ', 397 | '51DE003A C8D75180 BFD06116 21B4F4B5 56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 ', 398 | 'B10BE924 2F6F7C87 58684C11 C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 ', 399 | '06B6B51F 9FBFE4A5 E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 ', 400 | 'E6635C01 6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 ', 401 | '12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE A3BC0074 ', 402 | 'D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 DA60B8D0 44042D73 ', 403 | '33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 5768B525 206F85B3 B966D409 ', 404 | 'CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 2EB40D81 B7BD5C3B C0BA6CAD EDB88320 ', 405 | '9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF 04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E ', 406 | '7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D ', 407 | '806567CB 196C3671 6E6B06E7 FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 ', 408 | '60B08ED5 D6D6A3E8 A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA ', 409 | 'AF0A1B4C 36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 ', 410 | '5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 C2D7FFA7 ', 411 | 'B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 EB0E363F 72076785 ', 412 | '05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D 7CDCEFB7 0BDBDF21 86D3D2D4 ', 413 | 'F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 18B74777 88085AE6 FF0F6A70 66063BCA ', 414 | '11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 ', 415 | 'D06016F7 4969474D 3E6E77DB AED16A4A D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F ', 416 | '30B5FFE9 BDBDF21C CABAC28A 53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E ', 417 | 'C4614AB8 5D681B02 2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D' 418 | ].join(''); 419 | 420 | crc = crc ^ (-1); 421 | for (i = 0, iTop = str.length; i < iTop; i += 1) { 422 | y = (crc ^ str.charCodeAt(i)) & 0xFF; 423 | x = '0x' + table.substr(y * 9, 8); 424 | crc = (crc >>> 8) ^ x; 425 | } 426 | // always return a positive number (that's what >>> 0 does) 427 | return (crc ^ (-1)) >>> 0; 428 | }, 429 | /** 430 | * @member Hashes 431 | * @class MD5 432 | * @constructor 433 | * @param {Object} [config] 434 | * 435 | * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message 436 | * Digest Algorithm, as defined in RFC 1321. 437 | * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009 438 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 439 | * See for more infHashes. 440 | */ 441 | MD5: function(options) { 442 | /** 443 | * Private config properties. You may need to tweak these to be compatible with 444 | * the server-side, but the defaults work in most cases. 445 | * See {@link Hashes.MD5#method-setUpperCase} and {@link Hashes.SHA1#method-setUpperCase} 446 | */ 447 | var hexcase = (options && typeof options.uppercase === 'boolean') ? options.uppercase : false, // hexadecimal output case format. false - lowercase; true - uppercase 448 | b64pad = (options && typeof options.pad === 'string') ? options.pad : '=', // base-64 pad character. Defaults to '=' for strict RFC compliance 449 | utf8 = (options && typeof options.utf8 === 'boolean') ? options.utf8 : true; // enable/disable utf8 encoding 450 | 451 | // privileged (public) methods 452 | this.hex = function(s) { 453 | return rstr2hex(rstr(s, utf8), hexcase); 454 | }; 455 | this.b64 = function(s) { 456 | return rstr2b64(rstr(s), b64pad); 457 | }; 458 | this.any = function(s, e) { 459 | return rstr2any(rstr(s, utf8), e); 460 | }; 461 | this.raw = function(s) { 462 | return rstr(s, utf8); 463 | }; 464 | this.hex_hmac = function(k, d) { 465 | return rstr2hex(rstr_hmac(k, d), hexcase); 466 | }; 467 | this.b64_hmac = function(k, d) { 468 | return rstr2b64(rstr_hmac(k, d), b64pad); 469 | }; 470 | this.any_hmac = function(k, d, e) { 471 | return rstr2any(rstr_hmac(k, d), e); 472 | }; 473 | /** 474 | * Perform a simple self-test to see if the VM is working 475 | * @return {String} Hexadecimal hash sample 476 | */ 477 | this.vm_test = function() { 478 | return hex('abc').toLowerCase() === '900150983cd24fb0d6963f7d28e17f72'; 479 | }; 480 | /** 481 | * Enable/disable uppercase hexadecimal returned string 482 | * @param {Boolean} 483 | * @return {Object} this 484 | */ 485 | this.setUpperCase = function(a) { 486 | if (typeof a === 'boolean') { 487 | hexcase = a; 488 | } 489 | return this; 490 | }; 491 | /** 492 | * Defines a base64 pad string 493 | * @param {String} Pad 494 | * @return {Object} this 495 | */ 496 | this.setPad = function(a) { 497 | b64pad = a || b64pad; 498 | return this; 499 | }; 500 | /** 501 | * Defines a base64 pad string 502 | * @param {Boolean} 503 | * @return {Object} [this] 504 | */ 505 | this.setUTF8 = function(a) { 506 | if (typeof a === 'boolean') { 507 | utf8 = a; 508 | } 509 | return this; 510 | }; 511 | 512 | // private methods 513 | 514 | /** 515 | * Calculate the MD5 of a raw string 516 | */ 517 | 518 | function rstr(s) { 519 | s = (utf8) ? utf8Encode(s) : s; 520 | return binl2rstr(binl(rstr2binl(s), s.length * 8)); 521 | } 522 | 523 | /** 524 | * Calculate the HMAC-MD5, of a key and some data (raw strings) 525 | */ 526 | 527 | function rstr_hmac(key, data) { 528 | var bkey, ipad, opad, hash, i; 529 | 530 | key = (utf8) ? utf8Encode(key) : key; 531 | data = (utf8) ? utf8Encode(data) : data; 532 | bkey = rstr2binl(key); 533 | if (bkey.length > 16) { 534 | bkey = binl(bkey, key.length * 8); 535 | } 536 | 537 | ipad = Array(16), opad = Array(16); 538 | for (i = 0; i < 16; i += 1) { 539 | ipad[i] = bkey[i] ^ 0x36363636; 540 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 541 | } 542 | hash = binl(ipad.concat(rstr2binl(data)), 512 + data.length * 8); 543 | return binl2rstr(binl(opad.concat(hash), 512 + 128)); 544 | } 545 | 546 | /** 547 | * Calculate the MD5 of an array of little-endian words, and a bit length. 548 | */ 549 | 550 | function binl(x, len) { 551 | var i, olda, oldb, oldc, oldd, 552 | a = 1732584193, 553 | b = -271733879, 554 | c = -1732584194, 555 | d = 271733878; 556 | 557 | /* append padding */ 558 | x[len >> 5] |= 0x80 << ((len) % 32); 559 | x[(((len + 64) >>> 9) << 4) + 14] = len; 560 | 561 | for (i = 0; i < x.length; i += 16) { 562 | olda = a; 563 | oldb = b; 564 | oldc = c; 565 | oldd = d; 566 | 567 | a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936); 568 | d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586); 569 | c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819); 570 | b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330); 571 | a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897); 572 | d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426); 573 | c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341); 574 | b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983); 575 | a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416); 576 | d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417); 577 | c = md5_ff(c, d, a, b, x[i + 10], 17, -42063); 578 | b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162); 579 | a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682); 580 | d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101); 581 | c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290); 582 | b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329); 583 | 584 | a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510); 585 | d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632); 586 | c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713); 587 | b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302); 588 | a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691); 589 | d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083); 590 | c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335); 591 | b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848); 592 | a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438); 593 | d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690); 594 | c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961); 595 | b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501); 596 | a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467); 597 | d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784); 598 | c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473); 599 | b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734); 600 | 601 | a = md5_hh(a, b, c, d, x[i + 5], 4, -378558); 602 | d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463); 603 | c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562); 604 | b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556); 605 | a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060); 606 | d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353); 607 | c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632); 608 | b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640); 609 | a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174); 610 | d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222); 611 | c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979); 612 | b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189); 613 | a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487); 614 | d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835); 615 | c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520); 616 | b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651); 617 | 618 | a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844); 619 | d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415); 620 | c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905); 621 | b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055); 622 | a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571); 623 | d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606); 624 | c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523); 625 | b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799); 626 | a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359); 627 | d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744); 628 | c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380); 629 | b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649); 630 | a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070); 631 | d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379); 632 | c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259); 633 | b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551); 634 | 635 | a = safe_add(a, olda); 636 | b = safe_add(b, oldb); 637 | c = safe_add(c, oldc); 638 | d = safe_add(d, oldd); 639 | } 640 | return Array(a, b, c, d); 641 | } 642 | 643 | /** 644 | * These functions implement the four basic operations the algorithm uses. 645 | */ 646 | 647 | function md5_cmn(q, a, b, x, s, t) { 648 | return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b); 649 | } 650 | 651 | function md5_ff(a, b, c, d, x, s, t) { 652 | return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); 653 | } 654 | 655 | function md5_gg(a, b, c, d, x, s, t) { 656 | return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); 657 | } 658 | 659 | function md5_hh(a, b, c, d, x, s, t) { 660 | return md5_cmn(b ^ c ^ d, a, b, x, s, t); 661 | } 662 | 663 | function md5_ii(a, b, c, d, x, s, t) { 664 | return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); 665 | } 666 | }, 667 | /** 668 | * @member Hashes 669 | * @class Hashes.SHA1 670 | * @param {Object} [config] 671 | * @constructor 672 | * 673 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined in FIPS 180-1 674 | * Version 2.2 Copyright Paul Johnston 2000 - 2009. 675 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 676 | * See http://pajhome.org.uk/crypt/md5 for details. 677 | */ 678 | SHA1: function(options) { 679 | /** 680 | * Private config properties. You may need to tweak these to be compatible with 681 | * the server-side, but the defaults work in most cases. 682 | * See {@link Hashes.MD5#method-setUpperCase} and {@link Hashes.SHA1#method-setUpperCase} 683 | */ 684 | var hexcase = (options && typeof options.uppercase === 'boolean') ? options.uppercase : false, // hexadecimal output case format. false - lowercase; true - uppercase 685 | b64pad = (options && typeof options.pad === 'string') ? options.pad : '=', // base-64 pad character. Defaults to '=' for strict RFC compliance 686 | utf8 = (options && typeof options.utf8 === 'boolean') ? options.utf8 : true; // enable/disable utf8 encoding 687 | 688 | // public methods 689 | this.hex = function(s) { 690 | return rstr2hex(rstr(s, utf8), hexcase); 691 | }; 692 | this.b64 = function(s) { 693 | return rstr2b64(rstr(s, utf8), b64pad); 694 | }; 695 | this.any = function(s, e) { 696 | return rstr2any(rstr(s, utf8), e); 697 | }; 698 | this.raw = function(s) { 699 | return rstr(s, utf8); 700 | }; 701 | this.hex_hmac = function(k, d) { 702 | return rstr2hex(rstr_hmac(k, d)); 703 | }; 704 | this.b64_hmac = function(k, d) { 705 | return rstr2b64(rstr_hmac(k, d), b64pad); 706 | }; 707 | this.any_hmac = function(k, d, e) { 708 | return rstr2any(rstr_hmac(k, d), e); 709 | }; 710 | /** 711 | * Perform a simple self-test to see if the VM is working 712 | * @return {String} Hexadecimal hash sample 713 | * @public 714 | */ 715 | this.vm_test = function() { 716 | return hex('abc').toLowerCase() === '900150983cd24fb0d6963f7d28e17f72'; 717 | }; 718 | /** 719 | * @description Enable/disable uppercase hexadecimal returned string 720 | * @param {boolean} 721 | * @return {Object} this 722 | * @public 723 | */ 724 | this.setUpperCase = function(a) { 725 | if (typeof a === 'boolean') { 726 | hexcase = a; 727 | } 728 | return this; 729 | }; 730 | /** 731 | * @description Defines a base64 pad string 732 | * @param {string} Pad 733 | * @return {Object} this 734 | * @public 735 | */ 736 | this.setPad = function(a) { 737 | b64pad = a || b64pad; 738 | return this; 739 | }; 740 | /** 741 | * @description Defines a base64 pad string 742 | * @param {boolean} 743 | * @return {Object} this 744 | * @public 745 | */ 746 | this.setUTF8 = function(a) { 747 | if (typeof a === 'boolean') { 748 | utf8 = a; 749 | } 750 | return this; 751 | }; 752 | 753 | // private methods 754 | 755 | /** 756 | * Calculate the SHA-512 of a raw string 757 | */ 758 | 759 | function rstr(s) { 760 | s = (utf8) ? utf8Encode(s) : s; 761 | return binb2rstr(binb(rstr2binb(s), s.length * 8)); 762 | } 763 | 764 | /** 765 | * Calculate the HMAC-SHA1 of a key and some data (raw strings) 766 | */ 767 | 768 | function rstr_hmac(key, data) { 769 | var bkey, ipad, opad, i, hash; 770 | key = (utf8) ? utf8Encode(key) : key; 771 | data = (utf8) ? utf8Encode(data) : data; 772 | bkey = rstr2binb(key); 773 | 774 | if (bkey.length > 16) { 775 | bkey = binb(bkey, key.length * 8); 776 | } 777 | ipad = Array(16), opad = Array(16); 778 | for (i = 0; i < 16; i += 1) { 779 | ipad[i] = bkey[i] ^ 0x36363636; 780 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 781 | } 782 | hash = binb(ipad.concat(rstr2binb(data)), 512 + data.length * 8); 783 | return binb2rstr(binb(opad.concat(hash), 512 + 160)); 784 | } 785 | 786 | /** 787 | * Calculate the SHA-1 of an array of big-endian words, and a bit length 788 | */ 789 | 790 | function binb(x, len) { 791 | var i, j, t, olda, oldb, oldc, oldd, olde, 792 | w = Array(80), 793 | a = 1732584193, 794 | b = -271733879, 795 | c = -1732584194, 796 | d = 271733878, 797 | e = -1009589776; 798 | 799 | /* append padding */ 800 | x[len >> 5] |= 0x80 << (24 - len % 32); 801 | x[((len + 64 >> 9) << 4) + 15] = len; 802 | 803 | for (i = 0; i < x.length; i += 16) { 804 | olda = a; 805 | oldb = b; 806 | oldc = c; 807 | oldd = d; 808 | olde = e; 809 | 810 | for (j = 0; j < 80; j += 1) { 811 | if (j < 16) { 812 | w[j] = x[i + j]; 813 | } else { 814 | w[j] = bit_rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1); 815 | } 816 | t = safe_add(safe_add(bit_rol(a, 5), sha1_ft(j, b, c, d)), 817 | safe_add(safe_add(e, w[j]), sha1_kt(j))); 818 | e = d; 819 | d = c; 820 | c = bit_rol(b, 30); 821 | b = a; 822 | a = t; 823 | } 824 | 825 | a = safe_add(a, olda); 826 | b = safe_add(b, oldb); 827 | c = safe_add(c, oldc); 828 | d = safe_add(d, oldd); 829 | e = safe_add(e, olde); 830 | } 831 | return Array(a, b, c, d, e); 832 | } 833 | 834 | /** 835 | * Perform the appropriate triplet combination function for the current 836 | * iteration 837 | */ 838 | 839 | function sha1_ft(t, b, c, d) { 840 | if (t < 20) { 841 | return (b & c) | ((~b) & d); 842 | } 843 | if (t < 40) { 844 | return b ^ c ^ d; 845 | } 846 | if (t < 60) { 847 | return (b & c) | (b & d) | (c & d); 848 | } 849 | return b ^ c ^ d; 850 | } 851 | 852 | /** 853 | * Determine the appropriate additive constant for the current iteration 854 | */ 855 | 856 | function sha1_kt(t) { 857 | return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : 858 | (t < 60) ? -1894007588 : -899497514; 859 | } 860 | }, 861 | /** 862 | * @class Hashes.SHA256 863 | * @param {config} 864 | * 865 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-256, as defined in FIPS 180-2 866 | * Version 2.2 Copyright Angel Marin, Paul Johnston 2000 - 2009. 867 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 868 | * See http://pajhome.org.uk/crypt/md5 for details. 869 | * Also http://anmar.eu.org/projects/jssha2/ 870 | */ 871 | SHA256: function(options) { 872 | /** 873 | * Private properties configuration variables. You may need to tweak these to be compatible with 874 | * the server-side, but the defaults work in most cases. 875 | * @see this.setUpperCase() method 876 | * @see this.setPad() method 877 | */ 878 | var hexcase = (options && typeof options.uppercase === 'boolean') ? options.uppercase : false, // hexadecimal output case format. false - lowercase; true - uppercase */ 879 | b64pad = (options && typeof options.pad === 'string') ? options.pad : '=', 880 | /* base-64 pad character. Default '=' for strict RFC compliance */ 881 | utf8 = (options && typeof options.utf8 === 'boolean') ? options.utf8 : true, 882 | /* enable/disable utf8 encoding */ 883 | sha256_K; 884 | 885 | /* privileged (public) methods */ 886 | this.hex = function(s) { 887 | return rstr2hex(rstr(s, utf8)); 888 | }; 889 | this.b64 = function(s) { 890 | return rstr2b64(rstr(s, utf8), b64pad); 891 | }; 892 | this.any = function(s, e) { 893 | return rstr2any(rstr(s, utf8), e); 894 | }; 895 | this.raw = function(s) { 896 | return rstr(s, utf8); 897 | }; 898 | this.hex_hmac = function(k, d) { 899 | return rstr2hex(rstr_hmac(k, d)); 900 | }; 901 | this.b64_hmac = function(k, d) { 902 | return rstr2b64(rstr_hmac(k, d), b64pad); 903 | }; 904 | this.any_hmac = function(k, d, e) { 905 | return rstr2any(rstr_hmac(k, d), e); 906 | }; 907 | /** 908 | * Perform a simple self-test to see if the VM is working 909 | * @return {String} Hexadecimal hash sample 910 | * @public 911 | */ 912 | this.vm_test = function() { 913 | return hex('abc').toLowerCase() === '900150983cd24fb0d6963f7d28e17f72'; 914 | }; 915 | /** 916 | * Enable/disable uppercase hexadecimal returned string 917 | * @param {boolean} 918 | * @return {Object} this 919 | * @public 920 | */ 921 | this.setUpperCase = function(a) { 922 | if (typeof a === 'boolean') { 923 | hexcase = a; 924 | } 925 | return this; 926 | }; 927 | /** 928 | * @description Defines a base64 pad string 929 | * @param {string} Pad 930 | * @return {Object} this 931 | * @public 932 | */ 933 | this.setPad = function(a) { 934 | b64pad = a || b64pad; 935 | return this; 936 | }; 937 | /** 938 | * Defines a base64 pad string 939 | * @param {boolean} 940 | * @return {Object} this 941 | * @public 942 | */ 943 | this.setUTF8 = function(a) { 944 | if (typeof a === 'boolean') { 945 | utf8 = a; 946 | } 947 | return this; 948 | }; 949 | 950 | // private methods 951 | 952 | /** 953 | * Calculate the SHA-512 of a raw string 954 | */ 955 | 956 | function rstr(s, utf8) { 957 | s = (utf8) ? utf8Encode(s) : s; 958 | return binb2rstr(binb(rstr2binb(s), s.length * 8)); 959 | } 960 | 961 | /** 962 | * Calculate the HMAC-sha256 of a key and some data (raw strings) 963 | */ 964 | 965 | function rstr_hmac(key, data) { 966 | key = (utf8) ? utf8Encode(key) : key; 967 | data = (utf8) ? utf8Encode(data) : data; 968 | var hash, i = 0, 969 | bkey = rstr2binb(key), 970 | ipad = Array(16), 971 | opad = Array(16); 972 | 973 | if (bkey.length > 16) { 974 | bkey = binb(bkey, key.length * 8); 975 | } 976 | 977 | for (; i < 16; i += 1) { 978 | ipad[i] = bkey[i] ^ 0x36363636; 979 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 980 | } 981 | 982 | hash = binb(ipad.concat(rstr2binb(data)), 512 + data.length * 8); 983 | return binb2rstr(binb(opad.concat(hash), 512 + 256)); 984 | } 985 | 986 | /* 987 | * Main sha256 function, with its support functions 988 | */ 989 | 990 | function sha256_S(X, n) { 991 | return (X >>> n) | (X << (32 - n)); 992 | } 993 | 994 | function sha256_R(X, n) { 995 | return (X >>> n); 996 | } 997 | 998 | function sha256_Ch(x, y, z) { 999 | return ((x & y) ^ ((~x) & z)); 1000 | } 1001 | 1002 | function sha256_Maj(x, y, z) { 1003 | return ((x & y) ^ (x & z) ^ (y & z)); 1004 | } 1005 | 1006 | function sha256_Sigma0256(x) { 1007 | return (sha256_S(x, 2) ^ sha256_S(x, 13) ^ sha256_S(x, 22)); 1008 | } 1009 | 1010 | function sha256_Sigma1256(x) { 1011 | return (sha256_S(x, 6) ^ sha256_S(x, 11) ^ sha256_S(x, 25)); 1012 | } 1013 | 1014 | function sha256_Gamma0256(x) { 1015 | return (sha256_S(x, 7) ^ sha256_S(x, 18) ^ sha256_R(x, 3)); 1016 | } 1017 | 1018 | function sha256_Gamma1256(x) { 1019 | return (sha256_S(x, 17) ^ sha256_S(x, 19) ^ sha256_R(x, 10)); 1020 | } 1021 | 1022 | function sha256_Sigma0512(x) { 1023 | return (sha256_S(x, 28) ^ sha256_S(x, 34) ^ sha256_S(x, 39)); 1024 | } 1025 | 1026 | function sha256_Sigma1512(x) { 1027 | return (sha256_S(x, 14) ^ sha256_S(x, 18) ^ sha256_S(x, 41)); 1028 | } 1029 | 1030 | function sha256_Gamma0512(x) { 1031 | return (sha256_S(x, 1) ^ sha256_S(x, 8) ^ sha256_R(x, 7)); 1032 | } 1033 | 1034 | function sha256_Gamma1512(x) { 1035 | return (sha256_S(x, 19) ^ sha256_S(x, 61) ^ sha256_R(x, 6)); 1036 | } 1037 | 1038 | sha256_K = [ 1039 | 1116352408, 1899447441, -1245643825, -373957723, 961987163, 1508970993, -1841331548, -1424204075, -670586216, 310598401, 607225278, 1426881987, 1040 | 1925078388, -2132889090, -1680079193, -1046744716, -459576895, -272742522, 1041 | 264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986, -1740746414, -1473132947, -1341970488, -1084653625, -958395405, -710438585, 1042 | 113926993, 338241895, 666307205, 773529912, 1294757372, 1396182291, 1043 | 1695183700, 1986661051, -2117940946, -1838011259, -1564481375, -1474664885, -1035236496, -949202525, -778901479, -694614492, -200395387, 275423344, 1044 | 430227734, 506948616, 659060556, 883997877, 958139571, 1322822218, 1045 | 1537002063, 1747873779, 1955562222, 2024104815, -2067236844, -1933114872, -1866530822, -1538233109, -1090935817, -965641998 1046 | ]; 1047 | 1048 | function binb(m, l) { 1049 | var HASH = [1779033703, -1150833019, 1013904242, -1521486534, 1050 | 1359893119, -1694144372, 528734635, 1541459225 1051 | ]; 1052 | var W = new Array(64); 1053 | var a, b, c, d, e, f, g, h; 1054 | var i, j, T1, T2; 1055 | 1056 | /* append padding */ 1057 | m[l >> 5] |= 0x80 << (24 - l % 32); 1058 | m[((l + 64 >> 9) << 4) + 15] = l; 1059 | 1060 | for (i = 0; i < m.length; i += 16) { 1061 | a = HASH[0]; 1062 | b = HASH[1]; 1063 | c = HASH[2]; 1064 | d = HASH[3]; 1065 | e = HASH[4]; 1066 | f = HASH[5]; 1067 | g = HASH[6]; 1068 | h = HASH[7]; 1069 | 1070 | for (j = 0; j < 64; j += 1) { 1071 | if (j < 16) { 1072 | W[j] = m[j + i]; 1073 | } else { 1074 | W[j] = safe_add(safe_add(safe_add(sha256_Gamma1256(W[j - 2]), W[j - 7]), 1075 | sha256_Gamma0256(W[j - 15])), W[j - 16]); 1076 | } 1077 | 1078 | T1 = safe_add(safe_add(safe_add(safe_add(h, sha256_Sigma1256(e)), sha256_Ch(e, f, g)), 1079 | sha256_K[j]), W[j]); 1080 | T2 = safe_add(sha256_Sigma0256(a), sha256_Maj(a, b, c)); 1081 | h = g; 1082 | g = f; 1083 | f = e; 1084 | e = safe_add(d, T1); 1085 | d = c; 1086 | c = b; 1087 | b = a; 1088 | a = safe_add(T1, T2); 1089 | } 1090 | 1091 | HASH[0] = safe_add(a, HASH[0]); 1092 | HASH[1] = safe_add(b, HASH[1]); 1093 | HASH[2] = safe_add(c, HASH[2]); 1094 | HASH[3] = safe_add(d, HASH[3]); 1095 | HASH[4] = safe_add(e, HASH[4]); 1096 | HASH[5] = safe_add(f, HASH[5]); 1097 | HASH[6] = safe_add(g, HASH[6]); 1098 | HASH[7] = safe_add(h, HASH[7]); 1099 | } 1100 | return HASH; 1101 | } 1102 | 1103 | }, 1104 | 1105 | /** 1106 | * @class Hashes.SHA512 1107 | * @param {config} 1108 | * 1109 | * A JavaScript implementation of the Secure Hash Algorithm, SHA-512, as defined in FIPS 180-2 1110 | * Version 2.2 Copyright Anonymous Contributor, Paul Johnston 2000 - 2009. 1111 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 1112 | * See http://pajhome.org.uk/crypt/md5 for details. 1113 | */ 1114 | SHA512: function(options) { 1115 | /** 1116 | * Private properties configuration variables. You may need to tweak these to be compatible with 1117 | * the server-side, but the defaults work in most cases. 1118 | * @see this.setUpperCase() method 1119 | * @see this.setPad() method 1120 | */ 1121 | var hexcase = (options && typeof options.uppercase === 'boolean') ? options.uppercase : false, 1122 | /* hexadecimal output case format. false - lowercase; true - uppercase */ 1123 | b64pad = (options && typeof options.pad === 'string') ? options.pad : '=', 1124 | /* base-64 pad character. Default '=' for strict RFC compliance */ 1125 | utf8 = (options && typeof options.utf8 === 'boolean') ? options.utf8 : true, 1126 | /* enable/disable utf8 encoding */ 1127 | sha512_k; 1128 | 1129 | /* privileged (public) methods */ 1130 | this.hex = function(s) { 1131 | return rstr2hex(rstr(s)); 1132 | }; 1133 | this.b64 = function(s) { 1134 | return rstr2b64(rstr(s), b64pad); 1135 | }; 1136 | this.any = function(s, e) { 1137 | return rstr2any(rstr(s), e); 1138 | }; 1139 | this.raw = function(s) { 1140 | return rstr(s, utf8); 1141 | }; 1142 | this.hex_hmac = function(k, d) { 1143 | return rstr2hex(rstr_hmac(k, d)); 1144 | }; 1145 | this.b64_hmac = function(k, d) { 1146 | return rstr2b64(rstr_hmac(k, d), b64pad); 1147 | }; 1148 | this.any_hmac = function(k, d, e) { 1149 | return rstr2any(rstr_hmac(k, d), e); 1150 | }; 1151 | /** 1152 | * Perform a simple self-test to see if the VM is working 1153 | * @return {String} Hexadecimal hash sample 1154 | * @public 1155 | */ 1156 | this.vm_test = function() { 1157 | return hex('abc').toLowerCase() === '900150983cd24fb0d6963f7d28e17f72'; 1158 | }; 1159 | /** 1160 | * @description Enable/disable uppercase hexadecimal returned string 1161 | * @param {boolean} 1162 | * @return {Object} this 1163 | * @public 1164 | */ 1165 | this.setUpperCase = function(a) { 1166 | if (typeof a === 'boolean') { 1167 | hexcase = a; 1168 | } 1169 | return this; 1170 | }; 1171 | /** 1172 | * @description Defines a base64 pad string 1173 | * @param {string} Pad 1174 | * @return {Object} this 1175 | * @public 1176 | */ 1177 | this.setPad = function(a) { 1178 | b64pad = a || b64pad; 1179 | return this; 1180 | }; 1181 | /** 1182 | * @description Defines a base64 pad string 1183 | * @param {boolean} 1184 | * @return {Object} this 1185 | * @public 1186 | */ 1187 | this.setUTF8 = function(a) { 1188 | if (typeof a === 'boolean') { 1189 | utf8 = a; 1190 | } 1191 | return this; 1192 | }; 1193 | 1194 | /* private methods */ 1195 | 1196 | /** 1197 | * Calculate the SHA-512 of a raw string 1198 | */ 1199 | 1200 | function rstr(s) { 1201 | s = (utf8) ? utf8Encode(s) : s; 1202 | return binb2rstr(binb(rstr2binb(s), s.length * 8)); 1203 | } 1204 | /* 1205 | * Calculate the HMAC-SHA-512 of a key and some data (raw strings) 1206 | */ 1207 | 1208 | function rstr_hmac(key, data) { 1209 | key = (utf8) ? utf8Encode(key) : key; 1210 | data = (utf8) ? utf8Encode(data) : data; 1211 | 1212 | var hash, i = 0, 1213 | bkey = rstr2binb(key), 1214 | ipad = Array(32), 1215 | opad = Array(32); 1216 | 1217 | if (bkey.length > 32) { 1218 | bkey = binb(bkey, key.length * 8); 1219 | } 1220 | 1221 | for (; i < 32; i += 1) { 1222 | ipad[i] = bkey[i] ^ 0x36363636; 1223 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 1224 | } 1225 | 1226 | hash = binb(ipad.concat(rstr2binb(data)), 1024 + data.length * 8); 1227 | return binb2rstr(binb(opad.concat(hash), 1024 + 512)); 1228 | } 1229 | 1230 | /** 1231 | * Calculate the SHA-512 of an array of big-endian dwords, and a bit length 1232 | */ 1233 | 1234 | function binb(x, len) { 1235 | var j, i, l, 1236 | W = new Array(80), 1237 | hash = new Array(16), 1238 | //Initial hash values 1239 | H = [ 1240 | new int64(0x6a09e667, -205731576), 1241 | new int64(-1150833019, -2067093701), 1242 | new int64(0x3c6ef372, -23791573), 1243 | new int64(-1521486534, 0x5f1d36f1), 1244 | new int64(0x510e527f, -1377402159), 1245 | new int64(-1694144372, 0x2b3e6c1f), 1246 | new int64(0x1f83d9ab, -79577749), 1247 | new int64(0x5be0cd19, 0x137e2179) 1248 | ], 1249 | T1 = new int64(0, 0), 1250 | T2 = new int64(0, 0), 1251 | a = new int64(0, 0), 1252 | b = new int64(0, 0), 1253 | c = new int64(0, 0), 1254 | d = new int64(0, 0), 1255 | e = new int64(0, 0), 1256 | f = new int64(0, 0), 1257 | g = new int64(0, 0), 1258 | h = new int64(0, 0), 1259 | //Temporary variables not specified by the document 1260 | s0 = new int64(0, 0), 1261 | s1 = new int64(0, 0), 1262 | Ch = new int64(0, 0), 1263 | Maj = new int64(0, 0), 1264 | r1 = new int64(0, 0), 1265 | r2 = new int64(0, 0), 1266 | r3 = new int64(0, 0); 1267 | 1268 | if (sha512_k === undefined) { 1269 | //SHA512 constants 1270 | sha512_k = [ 1271 | new int64(0x428a2f98, -685199838), new int64(0x71374491, 0x23ef65cd), 1272 | new int64(-1245643825, -330482897), new int64(-373957723, -2121671748), 1273 | new int64(0x3956c25b, -213338824), new int64(0x59f111f1, -1241133031), 1274 | new int64(-1841331548, -1357295717), new int64(-1424204075, -630357736), 1275 | new int64(-670586216, -1560083902), new int64(0x12835b01, 0x45706fbe), 1276 | new int64(0x243185be, 0x4ee4b28c), new int64(0x550c7dc3, -704662302), 1277 | new int64(0x72be5d74, -226784913), new int64(-2132889090, 0x3b1696b1), 1278 | new int64(-1680079193, 0x25c71235), new int64(-1046744716, -815192428), 1279 | new int64(-459576895, -1628353838), new int64(-272742522, 0x384f25e3), 1280 | new int64(0xfc19dc6, -1953704523), new int64(0x240ca1cc, 0x77ac9c65), 1281 | new int64(0x2de92c6f, 0x592b0275), new int64(0x4a7484aa, 0x6ea6e483), 1282 | new int64(0x5cb0a9dc, -1119749164), new int64(0x76f988da, -2096016459), 1283 | new int64(-1740746414, -295247957), new int64(-1473132947, 0x2db43210), 1284 | new int64(-1341970488, -1728372417), new int64(-1084653625, -1091629340), 1285 | new int64(-958395405, 0x3da88fc2), new int64(-710438585, -1828018395), 1286 | new int64(0x6ca6351, -536640913), new int64(0x14292967, 0xa0e6e70), 1287 | new int64(0x27b70a85, 0x46d22ffc), new int64(0x2e1b2138, 0x5c26c926), 1288 | new int64(0x4d2c6dfc, 0x5ac42aed), new int64(0x53380d13, -1651133473), 1289 | new int64(0x650a7354, -1951439906), new int64(0x766a0abb, 0x3c77b2a8), 1290 | new int64(-2117940946, 0x47edaee6), new int64(-1838011259, 0x1482353b), 1291 | new int64(-1564481375, 0x4cf10364), new int64(-1474664885, -1136513023), 1292 | new int64(-1035236496, -789014639), new int64(-949202525, 0x654be30), 1293 | new int64(-778901479, -688958952), new int64(-694614492, 0x5565a910), 1294 | new int64(-200395387, 0x5771202a), new int64(0x106aa070, 0x32bbd1b8), 1295 | new int64(0x19a4c116, -1194143544), new int64(0x1e376c08, 0x5141ab53), 1296 | new int64(0x2748774c, -544281703), new int64(0x34b0bcb5, -509917016), 1297 | new int64(0x391c0cb3, -976659869), new int64(0x4ed8aa4a, -482243893), 1298 | new int64(0x5b9cca4f, 0x7763e373), new int64(0x682e6ff3, -692930397), 1299 | new int64(0x748f82ee, 0x5defb2fc), new int64(0x78a5636f, 0x43172f60), 1300 | new int64(-2067236844, -1578062990), new int64(-1933114872, 0x1a6439ec), 1301 | new int64(-1866530822, 0x23631e28), new int64(-1538233109, -561857047), 1302 | new int64(-1090935817, -1295615723), new int64(-965641998, -479046869), 1303 | new int64(-903397682, -366583396), new int64(-779700025, 0x21c0c207), 1304 | new int64(-354779690, -840897762), new int64(-176337025, -294727304), 1305 | new int64(0x6f067aa, 0x72176fba), new int64(0xa637dc5, -1563912026), 1306 | new int64(0x113f9804, -1090974290), new int64(0x1b710b35, 0x131c471b), 1307 | new int64(0x28db77f5, 0x23047d84), new int64(0x32caab7b, 0x40c72493), 1308 | new int64(0x3c9ebe0a, 0x15c9bebc), new int64(0x431d67c4, -1676669620), 1309 | new int64(0x4cc5d4be, -885112138), new int64(0x597f299c, -60457430), 1310 | new int64(0x5fcb6fab, 0x3ad6faec), new int64(0x6c44198c, 0x4a475817) 1311 | ]; 1312 | } 1313 | 1314 | for (i = 0; i < 80; i += 1) { 1315 | W[i] = new int64(0, 0); 1316 | } 1317 | 1318 | // append padding to the source string. The format is described in the FIPS. 1319 | x[len >> 5] |= 0x80 << (24 - (len & 0x1f)); 1320 | x[((len + 128 >> 10) << 5) + 31] = len; 1321 | l = x.length; 1322 | for (i = 0; i < l; i += 32) { //32 dwords is the block size 1323 | int64copy(a, H[0]); 1324 | int64copy(b, H[1]); 1325 | int64copy(c, H[2]); 1326 | int64copy(d, H[3]); 1327 | int64copy(e, H[4]); 1328 | int64copy(f, H[5]); 1329 | int64copy(g, H[6]); 1330 | int64copy(h, H[7]); 1331 | 1332 | for (j = 0; j < 16; j += 1) { 1333 | W[j].h = x[i + 2 * j]; 1334 | W[j].l = x[i + 2 * j + 1]; 1335 | } 1336 | 1337 | for (j = 16; j < 80; j += 1) { 1338 | //sigma1 1339 | int64rrot(r1, W[j - 2], 19); 1340 | int64revrrot(r2, W[j - 2], 29); 1341 | int64shr(r3, W[j - 2], 6); 1342 | s1.l = r1.l ^ r2.l ^ r3.l; 1343 | s1.h = r1.h ^ r2.h ^ r3.h; 1344 | //sigma0 1345 | int64rrot(r1, W[j - 15], 1); 1346 | int64rrot(r2, W[j - 15], 8); 1347 | int64shr(r3, W[j - 15], 7); 1348 | s0.l = r1.l ^ r2.l ^ r3.l; 1349 | s0.h = r1.h ^ r2.h ^ r3.h; 1350 | 1351 | int64add4(W[j], s1, W[j - 7], s0, W[j - 16]); 1352 | } 1353 | 1354 | for (j = 0; j < 80; j += 1) { 1355 | //Ch 1356 | Ch.l = (e.l & f.l) ^ (~e.l & g.l); 1357 | Ch.h = (e.h & f.h) ^ (~e.h & g.h); 1358 | 1359 | //Sigma1 1360 | int64rrot(r1, e, 14); 1361 | int64rrot(r2, e, 18); 1362 | int64revrrot(r3, e, 9); 1363 | s1.l = r1.l ^ r2.l ^ r3.l; 1364 | s1.h = r1.h ^ r2.h ^ r3.h; 1365 | 1366 | //Sigma0 1367 | int64rrot(r1, a, 28); 1368 | int64revrrot(r2, a, 2); 1369 | int64revrrot(r3, a, 7); 1370 | s0.l = r1.l ^ r2.l ^ r3.l; 1371 | s0.h = r1.h ^ r2.h ^ r3.h; 1372 | 1373 | //Maj 1374 | Maj.l = (a.l & b.l) ^ (a.l & c.l) ^ (b.l & c.l); 1375 | Maj.h = (a.h & b.h) ^ (a.h & c.h) ^ (b.h & c.h); 1376 | 1377 | int64add5(T1, h, s1, Ch, sha512_k[j], W[j]); 1378 | int64add(T2, s0, Maj); 1379 | 1380 | int64copy(h, g); 1381 | int64copy(g, f); 1382 | int64copy(f, e); 1383 | int64add(e, d, T1); 1384 | int64copy(d, c); 1385 | int64copy(c, b); 1386 | int64copy(b, a); 1387 | int64add(a, T1, T2); 1388 | } 1389 | int64add(H[0], H[0], a); 1390 | int64add(H[1], H[1], b); 1391 | int64add(H[2], H[2], c); 1392 | int64add(H[3], H[3], d); 1393 | int64add(H[4], H[4], e); 1394 | int64add(H[5], H[5], f); 1395 | int64add(H[6], H[6], g); 1396 | int64add(H[7], H[7], h); 1397 | } 1398 | 1399 | //represent the hash as an array of 32-bit dwords 1400 | for (i = 0; i < 8; i += 1) { 1401 | hash[2 * i] = H[i].h; 1402 | hash[2 * i + 1] = H[i].l; 1403 | } 1404 | return hash; 1405 | } 1406 | 1407 | //A constructor for 64-bit numbers 1408 | 1409 | function int64(h, l) { 1410 | this.h = h; 1411 | this.l = l; 1412 | //this.toString = int64toString; 1413 | } 1414 | 1415 | //Copies src into dst, assuming both are 64-bit numbers 1416 | 1417 | function int64copy(dst, src) { 1418 | dst.h = src.h; 1419 | dst.l = src.l; 1420 | } 1421 | 1422 | //Right-rotates a 64-bit number by shift 1423 | //Won't handle cases of shift>=32 1424 | //The function revrrot() is for that 1425 | 1426 | function int64rrot(dst, x, shift) { 1427 | dst.l = (x.l >>> shift) | (x.h << (32 - shift)); 1428 | dst.h = (x.h >>> shift) | (x.l << (32 - shift)); 1429 | } 1430 | 1431 | //Reverses the dwords of the source and then rotates right by shift. 1432 | //This is equivalent to rotation by 32+shift 1433 | 1434 | function int64revrrot(dst, x, shift) { 1435 | dst.l = (x.h >>> shift) | (x.l << (32 - shift)); 1436 | dst.h = (x.l >>> shift) | (x.h << (32 - shift)); 1437 | } 1438 | 1439 | //Bitwise-shifts right a 64-bit number by shift 1440 | //Won't handle shift>=32, but it's never needed in SHA512 1441 | 1442 | function int64shr(dst, x, shift) { 1443 | dst.l = (x.l >>> shift) | (x.h << (32 - shift)); 1444 | dst.h = (x.h >>> shift); 1445 | } 1446 | 1447 | //Adds two 64-bit numbers 1448 | //Like the original implementation, does not rely on 32-bit operations 1449 | 1450 | function int64add(dst, x, y) { 1451 | var w0 = (x.l & 0xffff) + (y.l & 0xffff); 1452 | var w1 = (x.l >>> 16) + (y.l >>> 16) + (w0 >>> 16); 1453 | var w2 = (x.h & 0xffff) + (y.h & 0xffff) + (w1 >>> 16); 1454 | var w3 = (x.h >>> 16) + (y.h >>> 16) + (w2 >>> 16); 1455 | dst.l = (w0 & 0xffff) | (w1 << 16); 1456 | dst.h = (w2 & 0xffff) | (w3 << 16); 1457 | } 1458 | 1459 | //Same, except with 4 addends. Works faster than adding them one by one. 1460 | 1461 | function int64add4(dst, a, b, c, d) { 1462 | var w0 = (a.l & 0xffff) + (b.l & 0xffff) + (c.l & 0xffff) + (d.l & 0xffff); 1463 | var w1 = (a.l >>> 16) + (b.l >>> 16) + (c.l >>> 16) + (d.l >>> 16) + (w0 >>> 16); 1464 | var w2 = (a.h & 0xffff) + (b.h & 0xffff) + (c.h & 0xffff) + (d.h & 0xffff) + (w1 >>> 16); 1465 | var w3 = (a.h >>> 16) + (b.h >>> 16) + (c.h >>> 16) + (d.h >>> 16) + (w2 >>> 16); 1466 | dst.l = (w0 & 0xffff) | (w1 << 16); 1467 | dst.h = (w2 & 0xffff) | (w3 << 16); 1468 | } 1469 | 1470 | //Same, except with 5 addends 1471 | 1472 | function int64add5(dst, a, b, c, d, e) { 1473 | var w0 = (a.l & 0xffff) + (b.l & 0xffff) + (c.l & 0xffff) + (d.l & 0xffff) + (e.l & 0xffff), 1474 | w1 = (a.l >>> 16) + (b.l >>> 16) + (c.l >>> 16) + (d.l >>> 16) + (e.l >>> 16) + (w0 >>> 16), 1475 | w2 = (a.h & 0xffff) + (b.h & 0xffff) + (c.h & 0xffff) + (d.h & 0xffff) + (e.h & 0xffff) + (w1 >>> 16), 1476 | w3 = (a.h >>> 16) + (b.h >>> 16) + (c.h >>> 16) + (d.h >>> 16) + (e.h >>> 16) + (w2 >>> 16); 1477 | dst.l = (w0 & 0xffff) | (w1 << 16); 1478 | dst.h = (w2 & 0xffff) | (w3 << 16); 1479 | } 1480 | }, 1481 | /** 1482 | * @class Hashes.RMD160 1483 | * @constructor 1484 | * @param {Object} [config] 1485 | * 1486 | * A JavaScript implementation of the RIPEMD-160 Algorithm 1487 | * Version 2.2 Copyright Jeremy Lin, Paul Johnston 2000 - 2009. 1488 | * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet 1489 | * See http://pajhome.org.uk/crypt/md5 for details. 1490 | * Also http://www.ocf.berkeley.edu/~jjlin/jsotp/ 1491 | */ 1492 | RMD160: function(options) { 1493 | /** 1494 | * Private properties configuration variables. You may need to tweak these to be compatible with 1495 | * the server-side, but the defaults work in most cases. 1496 | * @see this.setUpperCase() method 1497 | * @see this.setPad() method 1498 | */ 1499 | var hexcase = (options && typeof options.uppercase === 'boolean') ? options.uppercase : false, 1500 | /* hexadecimal output case format. false - lowercase; true - uppercase */ 1501 | b64pad = (options && typeof options.pad === 'string') ? options.pa : '=', 1502 | /* base-64 pad character. Default '=' for strict RFC compliance */ 1503 | utf8 = (options && typeof options.utf8 === 'boolean') ? options.utf8 : true, 1504 | /* enable/disable utf8 encoding */ 1505 | rmd160_r1 = [ 1506 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 1507 | 7, 4, 13, 1, 10, 6, 15, 3, 12, 0, 9, 5, 2, 14, 11, 8, 1508 | 3, 10, 14, 4, 9, 15, 8, 1, 2, 7, 0, 6, 13, 11, 5, 12, 1509 | 1, 9, 11, 10, 0, 8, 12, 4, 13, 3, 7, 15, 14, 5, 6, 2, 1510 | 4, 0, 5, 9, 7, 12, 2, 10, 14, 1, 3, 8, 11, 6, 15, 13 1511 | ], 1512 | rmd160_r2 = [ 1513 | 5, 14, 7, 0, 9, 2, 11, 4, 13, 6, 15, 8, 1, 10, 3, 12, 1514 | 6, 11, 3, 7, 0, 13, 5, 10, 14, 15, 8, 12, 4, 9, 1, 2, 1515 | 15, 5, 1, 3, 7, 14, 6, 9, 11, 8, 12, 2, 10, 0, 4, 13, 1516 | 8, 6, 4, 1, 3, 11, 15, 0, 5, 12, 2, 13, 9, 7, 10, 14, 1517 | 12, 15, 10, 4, 1, 5, 8, 7, 6, 2, 13, 14, 0, 3, 9, 11 1518 | ], 1519 | rmd160_s1 = [ 1520 | 11, 14, 15, 12, 5, 8, 7, 9, 11, 13, 14, 15, 6, 7, 9, 8, 1521 | 7, 6, 8, 13, 11, 9, 7, 15, 7, 12, 15, 9, 11, 7, 13, 12, 1522 | 11, 13, 6, 7, 14, 9, 13, 15, 14, 8, 13, 6, 5, 12, 7, 5, 1523 | 11, 12, 14, 15, 14, 15, 9, 8, 9, 14, 5, 6, 8, 6, 5, 12, 1524 | 9, 15, 5, 11, 6, 8, 13, 12, 5, 12, 13, 14, 11, 8, 5, 6 1525 | ], 1526 | rmd160_s2 = [ 1527 | 8, 9, 9, 11, 13, 15, 15, 5, 7, 7, 8, 11, 14, 14, 12, 6, 1528 | 9, 13, 15, 7, 12, 8, 9, 11, 7, 7, 12, 7, 6, 15, 13, 11, 1529 | 9, 7, 15, 11, 8, 6, 6, 14, 12, 13, 5, 14, 13, 13, 7, 5, 1530 | 15, 5, 8, 11, 14, 14, 6, 14, 6, 9, 12, 9, 12, 5, 15, 8, 1531 | 8, 5, 12, 9, 12, 5, 14, 6, 8, 13, 6, 5, 15, 13, 11, 11 1532 | ]; 1533 | 1534 | /* privileged (public) methods */ 1535 | this.hex = function(s) { 1536 | return rstr2hex(rstr(s, utf8)); 1537 | }; 1538 | this.b64 = function(s) { 1539 | return rstr2b64(rstr(s, utf8), b64pad); 1540 | }; 1541 | this.any = function(s, e) { 1542 | return rstr2any(rstr(s, utf8), e); 1543 | }; 1544 | this.raw = function(s) { 1545 | return rstr(s, utf8); 1546 | }; 1547 | this.hex_hmac = function(k, d) { 1548 | return rstr2hex(rstr_hmac(k, d)); 1549 | }; 1550 | this.b64_hmac = function(k, d) { 1551 | return rstr2b64(rstr_hmac(k, d), b64pad); 1552 | }; 1553 | this.any_hmac = function(k, d, e) { 1554 | return rstr2any(rstr_hmac(k, d), e); 1555 | }; 1556 | /** 1557 | * Perform a simple self-test to see if the VM is working 1558 | * @return {String} Hexadecimal hash sample 1559 | * @public 1560 | */ 1561 | this.vm_test = function() { 1562 | return hex('abc').toLowerCase() === '900150983cd24fb0d6963f7d28e17f72'; 1563 | }; 1564 | /** 1565 | * @description Enable/disable uppercase hexadecimal returned string 1566 | * @param {boolean} 1567 | * @return {Object} this 1568 | * @public 1569 | */ 1570 | this.setUpperCase = function(a) { 1571 | if (typeof a === 'boolean') { 1572 | hexcase = a; 1573 | } 1574 | return this; 1575 | }; 1576 | /** 1577 | * @description Defines a base64 pad string 1578 | * @param {string} Pad 1579 | * @return {Object} this 1580 | * @public 1581 | */ 1582 | this.setPad = function(a) { 1583 | if (typeof a !== 'undefined') { 1584 | b64pad = a; 1585 | } 1586 | return this; 1587 | }; 1588 | /** 1589 | * @description Defines a base64 pad string 1590 | * @param {boolean} 1591 | * @return {Object} this 1592 | * @public 1593 | */ 1594 | this.setUTF8 = function(a) { 1595 | if (typeof a === 'boolean') { 1596 | utf8 = a; 1597 | } 1598 | return this; 1599 | }; 1600 | 1601 | /* private methods */ 1602 | 1603 | /** 1604 | * Calculate the rmd160 of a raw string 1605 | */ 1606 | 1607 | function rstr(s) { 1608 | s = (utf8) ? utf8Encode(s) : s; 1609 | return binl2rstr(binl(rstr2binl(s), s.length * 8)); 1610 | } 1611 | 1612 | /** 1613 | * Calculate the HMAC-rmd160 of a key and some data (raw strings) 1614 | */ 1615 | 1616 | function rstr_hmac(key, data) { 1617 | key = (utf8) ? utf8Encode(key) : key; 1618 | data = (utf8) ? utf8Encode(data) : data; 1619 | var i, hash, 1620 | bkey = rstr2binl(key), 1621 | ipad = Array(16), 1622 | opad = Array(16); 1623 | 1624 | if (bkey.length > 16) { 1625 | bkey = binl(bkey, key.length * 8); 1626 | } 1627 | 1628 | for (i = 0; i < 16; i += 1) { 1629 | ipad[i] = bkey[i] ^ 0x36363636; 1630 | opad[i] = bkey[i] ^ 0x5C5C5C5C; 1631 | } 1632 | hash = binl(ipad.concat(rstr2binl(data)), 512 + data.length * 8); 1633 | return binl2rstr(binl(opad.concat(hash), 512 + 160)); 1634 | } 1635 | 1636 | /** 1637 | * Convert an array of little-endian words to a string 1638 | */ 1639 | 1640 | function binl2rstr(input) { 1641 | var i, output = '', 1642 | l = input.length * 32; 1643 | for (i = 0; i < l; i += 8) { 1644 | output += String.fromCharCode((input[i >> 5] >>> (i % 32)) & 0xFF); 1645 | } 1646 | return output; 1647 | } 1648 | 1649 | /** 1650 | * Calculate the RIPE-MD160 of an array of little-endian words, and a bit length. 1651 | */ 1652 | 1653 | function binl(x, len) { 1654 | var T, j, i, l, 1655 | h0 = 0x67452301, 1656 | h1 = 0xefcdab89, 1657 | h2 = 0x98badcfe, 1658 | h3 = 0x10325476, 1659 | h4 = 0xc3d2e1f0, 1660 | A1, B1, C1, D1, E1, 1661 | A2, B2, C2, D2, E2; 1662 | 1663 | /* append padding */ 1664 | x[len >> 5] |= 0x80 << (len % 32); 1665 | x[(((len + 64) >>> 9) << 4) + 14] = len; 1666 | l = x.length; 1667 | 1668 | for (i = 0; i < l; i += 16) { 1669 | A1 = A2 = h0; 1670 | B1 = B2 = h1; 1671 | C1 = C2 = h2; 1672 | D1 = D2 = h3; 1673 | E1 = E2 = h4; 1674 | for (j = 0; j <= 79; j += 1) { 1675 | T = safe_add(A1, rmd160_f(j, B1, C1, D1)); 1676 | T = safe_add(T, x[i + rmd160_r1[j]]); 1677 | T = safe_add(T, rmd160_K1(j)); 1678 | T = safe_add(bit_rol(T, rmd160_s1[j]), E1); 1679 | A1 = E1; 1680 | E1 = D1; 1681 | D1 = bit_rol(C1, 10); 1682 | C1 = B1; 1683 | B1 = T; 1684 | T = safe_add(A2, rmd160_f(79 - j, B2, C2, D2)); 1685 | T = safe_add(T, x[i + rmd160_r2[j]]); 1686 | T = safe_add(T, rmd160_K2(j)); 1687 | T = safe_add(bit_rol(T, rmd160_s2[j]), E2); 1688 | A2 = E2; 1689 | E2 = D2; 1690 | D2 = bit_rol(C2, 10); 1691 | C2 = B2; 1692 | B2 = T; 1693 | } 1694 | 1695 | T = safe_add(h1, safe_add(C1, D2)); 1696 | h1 = safe_add(h2, safe_add(D1, E2)); 1697 | h2 = safe_add(h3, safe_add(E1, A2)); 1698 | h3 = safe_add(h4, safe_add(A1, B2)); 1699 | h4 = safe_add(h0, safe_add(B1, C2)); 1700 | h0 = T; 1701 | } 1702 | return [h0, h1, h2, h3, h4]; 1703 | } 1704 | 1705 | // specific algorithm methods 1706 | 1707 | function rmd160_f(j, x, y, z) { 1708 | return (0 <= j && j <= 15) ? (x ^ y ^ z) : 1709 | (16 <= j && j <= 31) ? (x & y) | (~x & z) : 1710 | (32 <= j && j <= 47) ? (x | ~y) ^ z : 1711 | (48 <= j && j <= 63) ? (x & z) | (y & ~z) : 1712 | (64 <= j && j <= 79) ? x ^ (y | ~z) : 1713 | 'rmd160_f: j out of range'; 1714 | } 1715 | 1716 | function rmd160_K1(j) { 1717 | return (0 <= j && j <= 15) ? 0x00000000 : 1718 | (16 <= j && j <= 31) ? 0x5a827999 : 1719 | (32 <= j && j <= 47) ? 0x6ed9eba1 : 1720 | (48 <= j && j <= 63) ? 0x8f1bbcdc : 1721 | (64 <= j && j <= 79) ? 0xa953fd4e : 1722 | 'rmd160_K1: j out of range'; 1723 | } 1724 | 1725 | function rmd160_K2(j) { 1726 | return (0 <= j && j <= 15) ? 0x50a28be6 : 1727 | (16 <= j && j <= 31) ? 0x5c4dd124 : 1728 | (32 <= j && j <= 47) ? 0x6d703ef3 : 1729 | (48 <= j && j <= 63) ? 0x7a6d76e9 : 1730 | (64 <= j && j <= 79) ? 0x00000000 : 1731 | 'rmd160_K2: j out of range'; 1732 | } 1733 | } 1734 | }; 1735 | 1736 | // exposes Hashes 1737 | (function(window, undefined) { 1738 | var freeExports = false; 1739 | if (typeof exports === 'object') { 1740 | freeExports = exports; 1741 | if (exports && typeof global === 'object' && global && global === global.global) { 1742 | window = global; 1743 | } 1744 | } 1745 | 1746 | if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { 1747 | // define as an anonymous module, so, through path mapping, it can be aliased 1748 | define(function() { 1749 | return Hashes; 1750 | }); 1751 | } else if (freeExports) { 1752 | // in Node.js or RingoJS v0.8.0+ 1753 | if (typeof module === 'object' && module && module.exports === freeExports) { 1754 | module.exports = Hashes; 1755 | } 1756 | // in Narwhal or RingoJS v0.7.0- 1757 | else { 1758 | freeExports.Hashes = Hashes; 1759 | } 1760 | } else { 1761 | // in a browser or Rhino 1762 | window.Hashes = Hashes; 1763 | } 1764 | }(this)); 1765 | }()); // IIFE 1766 | -------------------------------------------------------------------------------- /smart/smart-lockscreen-info.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: blue; icon-glyph: car; 4 | const widgetHeadline = "Smart #1" 5 | 6 | let hashes 7 | try { 8 | hashes = importModule('modules/hashes') 9 | new hashes.MD5() 10 | new hashes.SHA1() 11 | } catch (exception) { 12 | await showError('Hashes file unavailable or unreadable!') 13 | return 14 | } 15 | 16 | let userName 17 | let password 18 | let vin 19 | const param = args.widgetParameter 20 | if (param != null && param.length > 0) { 21 | const paramArray = param.split(';') 22 | if (paramArray.length >= 3) { 23 | userName = paramArray[0].trim() 24 | password = paramArray[1].trim() 25 | vin = paramArray[2].trim() 26 | } else { 27 | console.log('Error reading user credentials.') 28 | } 29 | } else { 30 | // insert credentials for testing inside the app 31 | // never share this code with your sensible data! 32 | userName = '***' 33 | password = '***' 34 | vin = '***' 35 | } 36 | 37 | const deviceId = randomHexString(16) 38 | let apiTokenRefreshed = false 39 | let credentials = {} 40 | 41 | let cachedCredentials = await loadCachedCredentials() 42 | const isEmpty = Object.entries(cachedCredentials).length === 0 43 | if (isEmpty) { 44 | console.log('Cached credentials file is empty. Creating...') 45 | cachedCredentials = await initialLogin() 46 | credentials = cachedCredentials 47 | } else { 48 | credentials = cachedCredentials 49 | } 50 | if (credentials.hasOwnProperty('apiAccessToken')) { 51 | console.log('Found apiAccessToken.') 52 | } else { 53 | console.log('apiAccessToken unavailable. Refreshing...') 54 | await refreshApiAccessToken() 55 | } 56 | 57 | let updateSession = await updateSessionForCar(credentials.apiAccessToken, vin) 58 | if (updateSession.code == 1402) { 59 | if (apiTokenRefreshed) { 60 | console.log('Api Token expired and could not be refreshed!') 61 | } else { 62 | console.log('Api token expired and neees to be refreshed!') 63 | let refreshedApiAccessToken = await refreshApiAccessToken() 64 | if (!refreshedApiAccessToken) { 65 | cachedCredentials = await initialLogin() 66 | credentials = cachedCredentials 67 | refreshedApiAccessToken = await refreshApiAccessToken() 68 | } 69 | apiTokenRefreshed = true 70 | updateSession = await updateSessionForCar(credentials.apiAccessToken, vin) 71 | } 72 | } 73 | 74 | let carData = await getCarInfo(credentials.apiAccessToken) 75 | //console.log(carData) 76 | const batteryState = carData.data.vehicleStatus.additionalVehicleStatus.electricVehicleStatus.chargeLevel 77 | const temperatureInt = carData.data.vehicleStatus.additionalVehicleStatus.climateStatus.interiorTemp 78 | const temperatureExt = carData.data.vehicleStatus.additionalVehicleStatus.climateStatus.exteriorTemp 79 | const temperature = Number.parseFloat(temperatureInt).toFixed(1) + ' °C | ' + Number.parseFloat(temperatureExt).toFixed(1) + ' °C' 80 | const remainingKilometer = carData.data.vehicleStatus.additionalVehicleStatus.electricVehicleStatus.distanceToEmptyOnBatteryOnly 81 | console.log('Battery State: ' + batteryState) 82 | console.log('TemperatureInt: ' + temperatureInt) 83 | console.log('TemperatureExt: ' + temperatureExt) 84 | console.log('RemainingKilometer: ' + remainingKilometer) 85 | 86 | let widget = new ListWidget() 87 | await getData() 88 | widget.presentAccessoryRectangular() 89 | Script.setWidget(widget) 90 | Script.complete() 91 | 92 | // loads a random track from recent shazams 93 | async function getData() { 94 | 95 | widget.addSpacer(2) 96 | widget.url = "hellosmart://" 97 | 98 | let logoStack = widget.addStack() 99 | logoStack.layoutHorizontally() 100 | let smartLogo = await getImage("smart-lockscreen.png") 101 | logoStack.addSpacer(3) 102 | let image = logoStack.addImage(smartLogo) 103 | image.imageSize = new Size(16,16) 104 | logoStack.addSpacer(6) 105 | let headerStack = logoStack.addStack() 106 | headerStack.layoutVertically() 107 | headerStack.addSpacer(1) 108 | let headerText = headerStack.addText(widgetHeadline) 109 | headerText.font = Font.mediumRoundedSystemFont(12) 110 | 111 | widget.addSpacer(5) 112 | 113 | let temperatureStack = widget.addStack() 114 | temperatureStack.layoutHorizontally() 115 | let tempLogo = await getImage("temperature-icon.png") 116 | temperatureStack.addSpacer(2) 117 | let tempImage = temperatureStack.addImage(tempLogo) 118 | tempImage.imageSize = new Size(16, 16) 119 | temperatureStack.addSpacer(7) 120 | let tempTextStack = temperatureStack.addStack() 121 | tempTextStack.layoutVertically() 122 | tempTextStack.addSpacer(1) 123 | let tempTxt = tempTextStack.addText(temperature) 124 | tempTxt.font = Font.lightRoundedSystemFont(13) 125 | 126 | widget.addSpacer(5) 127 | 128 | let reachStack = widget.addStack() 129 | reachStack.layoutHorizontally() 130 | let reachLogo = await getImage('reachability.png') 131 | let reachImage = reachStack.addImage(reachLogo) 132 | reachImage.imageSize = new Size(18,18) 133 | reachStack.addSpacer(7) 134 | let reachTextStack = reachStack.addStack() 135 | reachTextStack.layoutVertically() 136 | reachTextStack.addSpacer(1) 137 | let reachTxt = reachTextStack.addText(remainingKilometer + ' km') 138 | reachTxt.font = Font.lightRoundedSystemFont(13) 139 | reachStack.addSpacer(10) 140 | let batteryLogo = await getImage('battery.png') 141 | let batteryImage = reachStack.addImage(batteryLogo) 142 | batteryImage.imageSize = new Size(16,16) 143 | reachStack.addSpacer(7) 144 | let batteryTextStack = reachStack.addStack() 145 | batteryTextStack.layoutVertically() 146 | batteryTextStack.addSpacer(1) 147 | let batteryTxt = batteryTextStack.addText(batteryState + ' %') 148 | batteryTxt.font = Font.lightRoundedSystemFont(13) 149 | 150 | } 151 | 152 | // random number, min and max included 153 | function getRandomNumber(min, max) { 154 | return Math.floor(Math.random() * (max - min + 1) + min) 155 | } 156 | 157 | // download an image from a given url 158 | async function loadImage(imgUrl) { 159 | let req = new Request(imgUrl) 160 | req.allowInsecureRequest = true 161 | let image = await req.loadImage() 162 | return image 163 | } 164 | 165 | // get image from local filestore or download it only once 166 | async function getImage(image) { 167 | let fm = FileManager.local() 168 | let dir = fm.documentsDirectory() 169 | let path = fm.joinPath(dir, image) 170 | if (fm.fileExists(path)) { 171 | return fm.readImage(path) 172 | } else { 173 | // download once 174 | let imageUrl 175 | switch (image) { 176 | case 'smart-lockscreen.png': 177 | imageUrl = "https://i.imgur.com/1tJMoG1.png"; 178 | break; 179 | case 'temperature-icon.png': 180 | imageUrl = "https://i.imgur.com/U8jq3QB.png"; 181 | break; 182 | case 'reachability.png': 183 | imageUrl = "https://i.imgur.com/WCx3VWK.png"; 184 | break; 185 | case 'battery.png': 186 | imageUrl = "https://i.imgur.com/xz6v8os.png"; 187 | break; 188 | default: 189 | console.log(`Sorry, couldn't find ${image}.`); 190 | } 191 | let iconImage = await loadImage(imageUrl) 192 | fm.writeImage(path, iconImage) 193 | return iconImage 194 | } 195 | } 196 | 197 | // returns a pseudo random value as hex 198 | function randomHexString (len) { 199 | charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 200 | var randomString = '' 201 | for (var i = 0; i < len; i++) { 202 | var randomPoz = Math.floor(Math.random() * charSet.length) 203 | randomString += charSet.substring(randomPoz, randomPoz + 1) 204 | } 205 | 206 | var result = '' 207 | for (var i = 0; i < randomString.length; i++) { 208 | result += randomString.charCodeAt(i).toString(16) 209 | } 210 | return result 211 | } 212 | 213 | // returns url query params as json 214 | function getUrlParams (url) { 215 | var regex = /[?&]([^=#]+)=([^&#]*)/g, 216 | params = {}, 217 | match 218 | while ((match = regex.exec(url))) { 219 | params[match[1]] = match[2] 220 | } 221 | return params 222 | } 223 | 224 | // load credentials from iCloud Drive 225 | async function loadCachedCredentials () { 226 | // load existing credentials from iCloud Drive 227 | const fm = FileManager.iCloud() 228 | const dir = fm.documentsDirectory() 229 | const path = fm.joinPath(dir, 'smart-credentials.json') 230 | const credentials = Data.fromFile(path) 231 | if (credentials != null) { 232 | return JSON.parse(credentials.toRawString()) 233 | } else { 234 | return new Object() 235 | } 236 | } 237 | 238 | // save smart api credentials to iCloud Drive 239 | async function saveCredentials (credentials) { 240 | const fm = FileManager.iCloud() 241 | const dir = fm.documentsDirectory() 242 | const path = fm.joinPath(dir, 'smart-credentials.json') 243 | fm.writeString(path, JSON.stringify(credentials)) 244 | } 245 | 246 | // initial login to get credentials for the first time or after login expiration 247 | async function initialLogin () { 248 | console.log('Starting complete login process from scratch.') 249 | const url = 250 | 'https://awsapi.future.smart.com/login-app/api/v1/authorize?uiLocales=de-DE&uiLocales=de-DE' 251 | let req = new Request(url) 252 | req.headers = { 253 | 'x-app-id': 'SmartAPPEU', 254 | accept: 'application/json;responseformat=3', 255 | 'x-requested-with': 'com.smart.hellosmart', 256 | 'upgrade-insecure-requests': '1', 257 | 'user-agent': 258 | 'Mozilla/5.0 (Linux; Android 9; ANE-LX1 Build/HUAWEIANE-L21; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/118.0.0.0 Mobile Safari/537.36', 259 | 'content-type': 'application/json; charset=utf-8', 260 | 'sec-fetch-site': 'none', 261 | 'sec-fetch-mode': 'navigate', 262 | 'sec-fetch-user': '?1', 263 | 'sec-fetch-dest': 'document', 264 | 'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-US;q=0.7,en;q=0.6' 265 | } 266 | let contextResult = await req.load() 267 | const urlParams = getUrlParams(req.response.url) 268 | const context = urlParams.context 269 | 270 | const loginUrl = 'https://auth.smart.com/accounts.login' 271 | req = new Request(loginUrl) 272 | req.method = 'POST' 273 | req.headers = { 274 | accept: '*/*', 275 | 'accept-language': 'de', 276 | 'content-type': 'application/x-www-form-urlencoded', 277 | 'x-requested-with': 'com.smart.hellosmart', 278 | cookie: 279 | 'gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4', 280 | origin: 'https://app.id.smart.com', 281 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)' 282 | } 283 | req.body = 284 | 'loginID=' + 285 | encodeURIComponent(userName) + 286 | '&password=' + 287 | encodeURIComponent(password) + 288 | '&sessionExpiration=2592000&targetEnv=jssdk&include=profile%2Cdata%2Cemails%2Csubscriptions%2Cpreferences%2C&includeUserInfo=true&loginMode=standard&lang=de&APIKey=3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a&source=showScreenSet&sdk=js_latest&authMode=cookie&pageURL=https%3A%2F%2Fapp.id.smart.com%2Flogin%3Fgig_ui_locales%3Dde-DE&sdkBuild=15482&format=json&riskContext=%7B%22b0%22%3A41187%2C%22b1%22%3A%5B0%2C2%2C3%2C1%5D%2C%22b2%22%3A4%2C%22b3%22%3A%5B%22-23%7C0.383%22%2C%22-81.33333587646484%7C0.236%22%5D%2C%22b4%22%3A3%2C%22b5%22%3A1%2C%22b6%22%3A%22Mozilla%2F5.0%20%28Linux%3B%20Android%209%3B%20ANE-LX1%20Build%2FHUAWEIANE-L21%3B%20wv%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Version%2F4.0%20Chrome%2F118.0.0.0%20Mobile%20Safari%2F537.36%22%2C%22b7%22%3A%5B%5D%2C%22b8%22%3A%2216%3A33%3A26%22%2C%22b9%22%3A-60%2C%22b10%22%3Anull%2C%22b11%22%3Afalse%2C%22b12%22%3A%7B%22charging%22%3Atrue%2C%22chargingTime%22%3Anull%2C%22dischargingTime%22%3Anull%2C%22level%22%3A0.58%7D%2C%22b13%22%3A%5B5%2C%22360%7C760%7C24%22%2Cfalse%2Ctrue%5D%7D' 289 | let loginResult = await req.loadJSON() 290 | console.log('Login token result http status code: ' + req.response.statusCode) 291 | const loginToken = loginResult.sessionInfo.login_token 292 | 293 | const authUrl = 294 | 'https://auth.smart.com/oidc/op/v1.0/3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a/authorize/continue?context=' + 295 | context + 296 | '&login_token=' + 297 | loginToken 298 | const cookieValue = 299 | 'gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4; glt_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=' + 300 | loginToken 301 | req = new Request(authUrl) 302 | req.headers = { 303 | accept: '*/*', 304 | cookie: cookieValue, 305 | 'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-US;q=0.7,en;q=0.6', 306 | 'x-requested-with': 'com.smart.hellosmart', 307 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)' 308 | } 309 | let authResult = await req.load() 310 | req = new Request(req.response.url) 311 | let finalAuthResult = await req.load() 312 | const tokens = getUrlParams(req.response.url) 313 | // console.log('Login finished. Saving credentials to iCloud: ' + JSON.stringify(tokens)) 314 | await saveCredentials(tokens) 315 | return tokens 316 | } 317 | 318 | // get credentials for configured car/vin 319 | async function updateSessionForCar (access_token, vin) { 320 | const timestamp = Date.now().toString() 321 | const nonce = randomHexString(16) 322 | const params = {} 323 | let url = '/device-platform/user/session/update' 324 | const payload = { 325 | vin: vin, 326 | sessionToken: access_token, 327 | language: '' 328 | } 329 | const sign = createSignature(nonce, params, timestamp, 'POST', url, payload) 330 | url = 'https://api.ecloudeu.com' + url 331 | let req = new Request(url) 332 | req.method = 'POST' 333 | req.headers = { 334 | 'x-app-id': 'SmartAPPEU', 335 | accept: 'application/json;responseformat=3', 336 | 'x-agent-type': 'iOS', 337 | 'x-device-type': 'mobile', 338 | 'x-operator-code': 'SMART', 339 | 'x-device-identifier': deviceId, 340 | 'x-env-type': 'production', 341 | 'x-version': 'smartNew', 342 | 'accept-language': 'en_US', 343 | 'x-api-signature-version': '1.0', 344 | 'x-api-signature-nonce': nonce, 345 | 'x-device-manufacture': 'Apple', 346 | 'x-device-brand': 'Apple', 347 | 'x-device-model': 'iPhone', 348 | 'x-agent-version': '17.1', 349 | authorization: access_token, 350 | 'content-type': 'application/json; charset=utf-8', 351 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)', 352 | 'x-signature': sign, 353 | 'x-timestamp': timestamp 354 | } 355 | req.body = JSON.stringify(payload) 356 | const carSessionResult = await req.loadJSON() 357 | const statusCode = req.response.statusCode 358 | console.log('CarSession http status code: ' + statusCode) 359 | console.log('CarSession api status code: ' + carSessionResult.code) 360 | //console.log(carSessionResult); 361 | return carSessionResult 362 | } 363 | 364 | // refreshes the api access token (valid for a few hours) 365 | async function refreshApiAccessToken () { 366 | const timestamp = Date.now().toString() 367 | const nonce = randomHexString(16) 368 | const params = { identity_type: 'smart' } 369 | let url = '/auth/account/session/secure' 370 | let data = { accessToken: credentials.access_token } 371 | 372 | const sign = createSignature(nonce, params, timestamp, 'POST', url, data) 373 | url = 374 | 'https://api.ecloudeu.com/auth/account/session/secure?identity_type=smart' 375 | let req = new Request(url) 376 | req.method = 'POST' 377 | req.headers = { 378 | 'x-app-id': 'SmartAPPEU', 379 | accept: 'application/json;responseformat=3', 380 | 'x-agent-type': 'iOS', 381 | 'x-device-type': 'mobile', 382 | 'x-operator-code': 'SMART', 383 | 'x-device-identifier': deviceId, 384 | 'x-env-type': 'production', 385 | 'x-version': 'smartNew', 386 | 'accept-language': 'en_US', 387 | 'x-api-signature-version': '1.0', 388 | 'x-api-signature-nonce': nonce, 389 | 'x-device-manufacture': 'Apple', 390 | 'x-device-brand': 'Apple', 391 | 'x-device-model': 'iPhone', 392 | 'x-agent-version': '17.1', 393 | 'Content-Type': 'application/json', 394 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)', 395 | 'x-signature': sign, 396 | 'x-timestamp': timestamp 397 | } 398 | req.body = JSON.stringify({ accessToken: credentials.access_token }) 399 | let result = await req.loadJSON() 400 | if (result.code == 1501) { 401 | console.log( 402 | 'Both access and login token expired. Logging in from the beginning.' 403 | ) 404 | return null 405 | } 406 | console.log('currentToken http status code: ' + req.response.statusCode) 407 | console.log('currentToken api status code: ' + result.code) 408 | credentials.apiAccessToken = result.data.accessToken 409 | credentials.userId = result.data.userId 410 | console.log('Saving new credentials with updated api access token.') 411 | await saveCredentials(credentials) 412 | return result.data.accessToken 413 | } 414 | 415 | async function getCarInfo (access_token) { 416 | const timestamp = Date.now().toString() 417 | const nonce = randomHexString(16) 418 | let url = '/remote-control/vehicle/status/' + vin 419 | const params = { 420 | latest: true, 421 | target: 'basic%2Cmore', 422 | userId: credentials.userId 423 | } 424 | const sign = createSignature(nonce, params, timestamp, 'GET', url) 425 | url = 426 | 'https://api.ecloudeu.com' + 427 | url + 428 | '?latest=true&target=basic%2Cmore&userId=' + 429 | credentials.userId 430 | let req = new Request(url) 431 | req.method = 'GET' 432 | req.headers = { 433 | 'x-app-id': 'SmartAPPEU', 434 | accept: 'application/json;responseformat=3', 435 | 'x-agent-type': 'iOS', 436 | 'x-device-type': 'mobile', 437 | 'x-operator-code': 'SMART', 438 | 'x-device-identifier': deviceId, 439 | 'x-env-type': 'production', 440 | 'x-version': 'smartNew', 441 | 'accept-language': 'en_US', 442 | 'x-api-signature-version': '1.0', 443 | 'x-api-signature-nonce': nonce, 444 | 'x-device-manufacture': 'Apple', 445 | 'x-device-brand': 'Apple', 446 | 'x-device-model': 'iPhone', 447 | 'x-agent-version': '17.1', 448 | authorization: access_token, 449 | 'content-type': 'application/json; charset=utf-8', 450 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)', 451 | 'x-signature': sign, 452 | 'x-timestamp': timestamp 453 | } 454 | 455 | let carData = await req.loadJSON() 456 | const statusCode = req.response.statusCode 457 | console.log('carInfo http status code: ' + statusCode) 458 | console.log('carInfo api status code: ' + carData.code) 459 | // console.log(carData); 460 | return carData 461 | } 462 | 463 | // sign http requests for smart api 464 | function createSignature (nonce, urlParams, timestamp, method, url, body) { 465 | var MD5 = new hashes.MD5() 466 | const md5Hash = body 467 | ? MD5.b64(JSON.stringify(body)) 468 | : '1B2M2Y8AsgTpgAmY7PhCfg==' 469 | let urlParameters = Object.entries(urlParams) 470 | .map(e => e.join('=')) 471 | .join('&') 472 | const payload = `application/json;responseformat=3 473 | x-api-signature-nonce:${nonce} 474 | x-api-signature-version:1.0 475 | 476 | ${urlParameters} 477 | ${md5Hash} 478 | ${timestamp} 479 | ${method} 480 | ${url}` 481 | const secret = atob('NzRlNzQ2OWFmZjUwNDJiYmJlZDdiYmIxYjM2YzE1ZTk=') 482 | const result = new hashes.SHA1().b64_hmac(secret, payload) 483 | return result 484 | } 485 | 486 | 487 | // 488 | // make sure to copy this script until the end! 489 | // 490 | -------------------------------------------------------------------------------- /smart/smart-one-info-small.js: -------------------------------------------------------------------------------- 1 | // Variables used by Scriptable. 2 | // These must be at the very top of the file. Do not edit. 3 | // icon-color: teal; icon-glyph: car; 4 | let hashes 5 | try { 6 | hashes = importModule('modules/hashes') 7 | new hashes.MD5() 8 | new hashes.SHA1() 9 | } catch (exception) { 10 | await showError('Hashes file unavailable or unreadable!') 11 | return 12 | } 13 | 14 | let userName 15 | let password 16 | let vin 17 | let model 18 | const param = args.widgetParameter 19 | if (param != null && param.length > 0) { 20 | const paramArray = param.split(';') 21 | if (paramArray.length >= 4) { 22 | userName = paramArray[0].trim() 23 | password = paramArray[1].trim() 24 | vin = paramArray[2].trim() 25 | apiKey = paramArray[3].trim() 26 | } else { 27 | console.log('Error reading user credentials.') 28 | } 29 | } else { 30 | // insert credentials for testing inside the app 31 | // never share this code with your sensible data! 32 | vin = '***' 33 | userName = '***' 34 | password = '***' 35 | apiKey = '***' 36 | } 37 | 38 | const deviceId = randomHexString(16) 39 | let apiTokenRefreshed = false 40 | let credentials = {} 41 | // widget colors 42 | const labelColor = new Color('#000080') // widget label color 43 | const labelTextColor = Color.white() // widget label text color 44 | const textColor = new Color('#34443c') // widget text color 45 | 46 | // LANGUAGES 47 | const languageMap = { 48 | en: { 49 | temperature: 'Temperature', 50 | range: 'Range', 51 | location: 'Location', 52 | unavailable: 'unavailable' 53 | }, 54 | 55 | de: { 56 | temperature: 'Temperatur', 57 | range: 'Reichweite', 58 | location: 'Standort', 59 | unavailable: 'Nicht verfügbar' 60 | }, 61 | 62 | fr: { 63 | temperature: 'Température', 64 | range: 'Autonomie', 65 | location: 'Position', 66 | unavailable: 'indisponible' 67 | } 68 | } 69 | 70 | // detects the user's language needed for translation 71 | function detectLanguage () { 72 | let selected_language = 'en' 73 | let userLanguages = Device.preferredLanguages() 74 | userLanguages = userLanguages.map(l => l.toLowerCase()) 75 | let availableLanguages = Object.keys(languageMap) 76 | 77 | // Try to find a match based on the first part of the available languages (i.e., match "es" for "es-ES") 78 | chunkLoop: for (let language of userLanguages) { 79 | for (let availableLanguage of availableLanguages) { 80 | if (language.startsWith(availableLanguage)) { 81 | selected_language = availableLanguage 82 | break chunkLoop 83 | } 84 | } 85 | } 86 | return selected_language 87 | } 88 | const detectedLanguage = detectLanguage() 89 | 90 | // create an object to be use for languages 91 | // example: lang.range 92 | function getLanguage () { 93 | return languageMap[detectedLanguage] 94 | } 95 | 96 | // 97 | // main workflow 98 | // 99 | let lang = getLanguage() 100 | let cachedCredentials = await loadCachedCredentials() 101 | const isEmpty = Object.entries(cachedCredentials).length === 0 102 | if (isEmpty) { 103 | cachedCredentials = await initialLogin() 104 | credentials = cachedCredentials 105 | } else { 106 | credentials = cachedCredentials 107 | } 108 | if (credentials.hasOwnProperty('apiAccessToken')) { 109 | console.log('Found apiAccessToken.') 110 | } else { 111 | console.log('apiAccessToken unavailable. Refreshing...') 112 | await refreshApiAccessToken() 113 | } 114 | let geoData = '' 115 | 116 | let updateSession = await updateSessionForCar(credentials.apiAccessToken, vin) 117 | if (updateSession.code == 1402) { 118 | if (apiTokenRefreshed) { 119 | console.log('Api Token expired and could not be refreshed!') 120 | } else { 121 | console.log('Api token expired and neees to be refreshed!') 122 | let refreshedApiAccessToken = await refreshApiAccessToken() 123 | if (!refreshedApiAccessToken) { 124 | cachedCredentials = await initialLogin() 125 | credentials = cachedCredentials 126 | refreshedApiAccessToken = await refreshApiAccessToken() 127 | } 128 | apiTokenRefreshed = true 129 | updateSession = await updateSessionForCar(credentials.apiAccessToken, vin) 130 | } 131 | } 132 | 133 | const allCars = await getAllCars(credentials.apiAccessToken) 134 | const car = allCars.data.list.find(o => o.vin === vin) 135 | 136 | model = car.matCode 137 | let carData = await getCarInfo(credentials.apiAccessToken) 138 | 139 | if (carData.code == 1000) { 140 | geoData = await getGeoData() 141 | } else { 142 | carData = '' 143 | } 144 | 145 | // battery circle graphics 146 | const canvSize = 200 147 | const canvTextSize = 50 148 | const canvas = new DrawContext() 149 | canvas.opaque = false 150 | const battCircleRemainColor = new Color('#32CD33') // battery remaining color 151 | const battCircleDepletedColor = new Color('#E3e3dc') // battery depleted color 152 | const battCircleBGColor = new Color('#fff') // battery circle background color 153 | const canvWidth = 16 // battery circle thickness 154 | const canvRadius = 80 // battery circle radius 155 | canvas.size = new Size(canvSize, canvSize) 156 | canvas.respectScreenScale = true 157 | 158 | let widget = new ListWidget() 159 | widget.url = 'hellosmart://' 160 | widget.backgroundColor = Color.white() 161 | widget = await createWidget() 162 | Script.setWidget(widget) 163 | Script.complete() 164 | 165 | widget.presentSmall() 166 | 167 | async function createWidget () { 168 | let smartIcon = await getImage('smart-logo.png') 169 | // widget.backgroundColor = new Color("#768178") 170 | 171 | if (carData) { 172 | const batteryLevel = 173 | carData.data.vehicleStatus.additionalVehicleStatus.electricVehicleStatus 174 | .chargeLevel 175 | widget.setPadding(4, 13, 2, 13) 176 | 177 | let iconStack = widget.addStack() 178 | iconStack.layoutHorizontally() 179 | iconStack.addSpacer(50) 180 | 181 | let smartIconImage = iconStack.addImage(smartIcon) 182 | smartIconImage.imageSize = new Size(42, 10) 183 | smartIconImage.centerAlignImage() 184 | 185 | let carStack = widget.addStack() 186 | 187 | drawArc( 188 | Math.floor(batteryLevel * 3.6), 189 | battCircleRemainColor, 190 | battCircleDepletedColor, 191 | batteryLevel.toString() + '%' 192 | ) 193 | 194 | const carIcon = await getImage(model + '.png') 195 | let carIconImage = carStack.addImage(carIcon) 196 | carIconImage.imageSize = new Size(75, 57) 197 | carIconImage.centerAlignImage() 198 | 199 | carStack.addSpacer(9) 200 | 201 | let batteryStack = carStack.addStack() 202 | batteryStack.layoutVertically() 203 | const batteryImage = batteryStack.addImage(canvas.getImage()) 204 | batteryImage.imageSize = new Size(55, 55) 205 | 206 | let detailsStack = widget.addStack() 207 | detailsStack.layoutHorizontally() 208 | 209 | let distanceStack = detailsStack.addStack() 210 | distanceStack.layoutVertically() 211 | 212 | let temperatureLabelStack = distanceStack.addStack() 213 | temperatureLabelStack.layoutHorizontally() 214 | temperatureLabelStack.backgroundColor = labelColor 215 | temperatureLabelStack.cornerRadius = 4 216 | temperatureLabelStack.size = new Size(68, 14) 217 | 218 | let temperatureLabel = temperatureLabelStack.addText(lang.temperature) 219 | temperatureLabel.font = Font.semiboldSystemFont(10) 220 | temperatureLabel.textColor = labelTextColor 221 | 222 | distanceStack.addSpacer(1) 223 | let temperature = 224 | carData.data.vehicleStatus.additionalVehicleStatus.climateStatus 225 | .interiorTemp 226 | let temperatureNo = distanceStack.addText( 227 | Number.parseFloat(temperature).toFixed(1) + '°C' 228 | ) 229 | temperatureNo.font = Font.semiboldSystemFont(11) 230 | temperatureNo.textColor = textColor 231 | 232 | detailsStack.addSpacer() 233 | 234 | let mainBatteryStack = detailsStack.addStack() 235 | mainBatteryStack.layoutVertically() 236 | 237 | let remainingKilometerLabelStack = mainBatteryStack.addStack() 238 | remainingKilometerLabelStack.layoutHorizontally() 239 | remainingKilometerLabelStack.backgroundColor = labelColor 240 | remainingKilometerLabelStack.cornerRadius = 4 241 | remainingKilometerLabelStack.size = new Size(68, 14) 242 | 243 | let remainingKilometerLabel = remainingKilometerLabelStack.addText( 244 | lang.range 245 | ) 246 | remainingKilometerLabel.font = Font.semiboldSystemFont(10) 247 | remainingKilometerLabel.textColor = labelTextColor 248 | 249 | mainBatteryStack.addSpacer(1) 250 | 251 | let remainingKilometer = 252 | carData.data.vehicleStatus.additionalVehicleStatus.electricVehicleStatus 253 | .distanceToEmptyOnBatteryOnly 254 | 255 | let remainingKilometerNo = mainBatteryStack.addText( 256 | Math.round(remainingKilometer) + ' KM' 257 | ) 258 | remainingKilometerNo.font = Font.semiboldSystemFont(11) 259 | remainingKilometerNo.textColor = textColor 260 | 261 | widget.addSpacer(2) 262 | 263 | let locationTextStack = widget.addStack() 264 | locationTextStack.layoutHorizontally() 265 | locationTextStack.backgroundColor = labelColor 266 | locationTextStack.cornerRadius = 4 267 | locationTextStack.size = new Size(56, 14) 268 | 269 | let locationText = locationTextStack.addText(lang.location) 270 | locationText.font = Font.semiboldSystemFont(10) 271 | locationText.textColor = labelTextColor 272 | 273 | widget.addSpacer(1) 274 | 275 | // car location 276 | const road = geoData.items[0].address.street || '' 277 | const houseNumber = geoData.items[0].address.houseNumber || 'Unbekannt' 278 | let street = road + ' ' + houseNumber 279 | let geoPositionStreetTxt = widget.addText(street) 280 | geoPositionStreetTxt.font = Font.semiboldSystemFont(11) 281 | geoPositionStreetTxt.textColor = textColor 282 | geoPositionStreetTxt.lineLimit = 1 283 | geoPositionStreetTxt.minimumScaleFactor = 0.8 284 | const zip = geoData.items[0].address.postalCode || '' 285 | const city = geoData.items[0].address.city || 'Unbekannt' 286 | let cityFormatted = zip + ' ' + city 287 | let geoPositionCityTxt = widget.addText(cityFormatted) 288 | geoPositionCityTxt.font = Font.lightSystemFont(10) 289 | geoPositionCityTxt.textColor = textColor 290 | geoPositionCityTxt.lineLimit = 1 291 | } else { 292 | let smartIconImage = widget.addImage(smartIcon) 293 | smartIconImage.imageSize = new Size(70, 14) 294 | smartIconImage.centerAlignImage() 295 | 296 | widget.addSpacer() 297 | 298 | let warningTxt = widget.addText( 299 | 'Error getting data, please double check your credentials.' 300 | ) 301 | warningTxt.font = Font.semiboldSystemFont(12) 302 | warningTxt.textColor = textColor 303 | warningTxt.centerAlignText() 304 | } 305 | return widget 306 | } 307 | 308 | // refreshes the api access token (valid for a few hours) 309 | async function refreshApiAccessToken () { 310 | const timestamp = Date.now().toString() 311 | const nonce = randomHexString(16) 312 | const params = { identity_type: 'smart' } 313 | let url = '/auth/account/session/secure' 314 | let data = { accessToken: credentials.access_token } 315 | const sign = createSignature(nonce, params, timestamp, 'POST', url, data) 316 | url = 317 | 'https://api.ecloudeu.com/auth/account/session/secure?identity_type=smart' 318 | let req = new Request(url) 319 | req.method = 'POST' 320 | req.headers = { 321 | 'x-app-id': 'SmartAPPEU', 322 | accept: 'application/json;responseformat=3', 323 | 'x-agent-type': 'iOS', 324 | 'x-device-type': 'mobile', 325 | 'x-operator-code': 'SMART', 326 | 'x-device-identifier': deviceId, 327 | 'x-env-type': 'production', 328 | 'x-version': 'smartNew', 329 | 'accept-language': 'en_US', 330 | 'x-api-signature-version': '1.0', 331 | 'x-api-signature-nonce': nonce, 332 | 'x-device-manufacture': 'Apple', 333 | 'x-device-brand': 'Apple', 334 | 'x-device-model': 'iPhone', 335 | 'x-agent-version': '17.1', 336 | 'Content-Type': 'application/json', 337 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)', 338 | 'x-signature': sign, 339 | 'x-timestamp': timestamp 340 | } 341 | req.body = JSON.stringify({ accessToken: credentials.access_token }) 342 | let result = await req.loadJSON() 343 | if (result.code == 1501) { 344 | console.log( 345 | 'Both access and login token expired. Logging in from the beginning.' 346 | ) 347 | return null 348 | } 349 | credentials.apiAccessToken = result.data.accessToken 350 | credentials.userId = result.data.userId 351 | await saveCredentials(credentials) 352 | return result.data.accessToken 353 | } 354 | 355 | // returns all cars of the user 356 | async function getAllCars (access_token) { 357 | const timestamp = Date.now().toString() 358 | const nonce = randomHexString(16) 359 | const params = { needSharedCar: 1, userId: credentials.userId } 360 | let url = '/device-platform/user/vehicle/secure' 361 | const sign = createSignature(nonce, params, timestamp, 'GET', url) 362 | url = 363 | 'https://api.ecloudeu.com' + 364 | url + 365 | '?needSharedCar=1&userId=' + 366 | credentials.userId 367 | let req = new Request(url) 368 | req.method = 'GET' 369 | req.headers = { 370 | 'x-app-id': 'SmartAPPEU', 371 | accept: 'application/json;responseformat=3', 372 | 'x-agent-type': 'iOS', 373 | 'x-device-type': 'mobile', 374 | 'x-operator-code': 'SMART', 375 | 'x-device-identifier': deviceId, 376 | 'x-env-type': 'production', 377 | 'x-version': 'smartNew', 378 | 'accept-language': 'en_US', 379 | 'x-api-signature-version': '1.0', 380 | 'x-api-signature-nonce': nonce, 381 | 'x-device-manufacture': 'Apple', 382 | 'x-device-brand': 'Apple', 383 | 'x-device-model': 'iPhone', 384 | 'x-agent-version': '17.1', 385 | authorization: access_token, 386 | 'content-type': 'application/json; charset=utf-8', 387 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)', 388 | 'x-signature': sign, 389 | 'x-timestamp': timestamp 390 | } 391 | const carsResult = await req.loadJSON() 392 | const statusCode = req.response.statusCode 393 | return carsResult 394 | } 395 | 396 | // get credentials for configured car/vin 397 | async function updateSessionForCar (access_token, vin) { 398 | const timestamp = Date.now().toString() 399 | const nonce = randomHexString(16) 400 | const params = {} 401 | let url = '/device-platform/user/session/update' 402 | const payload = { 403 | vin: vin, 404 | sessionToken: access_token, 405 | language: '' 406 | } 407 | const sign = createSignature(nonce, params, timestamp, 'POST', url, payload) 408 | url = 'https://api.ecloudeu.com' + url 409 | let req = new Request(url) 410 | req.method = 'POST' 411 | req.headers = { 412 | 'x-app-id': 'SmartAPPEU', 413 | accept: 'application/json;responseformat=3', 414 | 'x-agent-type': 'iOS', 415 | 'x-device-type': 'mobile', 416 | 'x-operator-code': 'SMART', 417 | 'x-device-identifier': deviceId, 418 | 'x-env-type': 'production', 419 | 'x-version': 'smartNew', 420 | 'accept-language': 'en_US', 421 | 'x-api-signature-version': '1.0', 422 | 'x-api-signature-nonce': nonce, 423 | 'x-device-manufacture': 'Apple', 424 | 'x-device-brand': 'Apple', 425 | 'x-device-model': 'iPhone', 426 | 'x-agent-version': '17.1', 427 | authorization: access_token, 428 | 'content-type': 'application/json; charset=utf-8', 429 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)', 430 | 'x-signature': sign, 431 | 'x-timestamp': timestamp 432 | } 433 | req.body = JSON.stringify(payload) 434 | return await req.loadJSON() 435 | } 436 | 437 | async function getCarInfo (access_token) { 438 | const timestamp = Date.now().toString() 439 | const nonce = randomHexString(16) 440 | let url = '/remote-control/vehicle/status/' + vin 441 | const params = { 442 | latest: true, 443 | target: 'basic%2Cmore', 444 | userId: credentials.userId 445 | } 446 | const sign = createSignature(nonce, params, timestamp, 'GET', url) 447 | url = 448 | 'https://api.ecloudeu.com' + 449 | url + 450 | '?latest=true&target=basic%2Cmore&userId=' + 451 | credentials.userId 452 | let req = new Request(url) 453 | req.method = 'GET' 454 | req.headers = { 455 | 'x-app-id': 'SmartAPPEU', 456 | accept: 'application/json;responseformat=3', 457 | 'x-agent-type': 'iOS', 458 | 'x-device-type': 'mobile', 459 | 'x-operator-code': 'SMART', 460 | 'x-device-identifier': deviceId, 461 | 'x-env-type': 'production', 462 | 'x-version': 'smartNew', 463 | 'accept-language': 'en_US', 464 | 'x-api-signature-version': '1.0', 465 | 'x-api-signature-nonce': nonce, 466 | 'x-device-manufacture': 'Apple', 467 | 'x-device-brand': 'Apple', 468 | 'x-device-model': 'iPhone', 469 | 'x-agent-version': '17.1', 470 | authorization: access_token, 471 | 'content-type': 'application/json; charset=utf-8', 472 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)', 473 | 'x-signature': sign, 474 | 'x-timestamp': timestamp 475 | } 476 | return await req.loadJSON() 477 | } 478 | 479 | // sign http requests for smart api 480 | function createSignature (nonce, urlParams, timestamp, method, url, body) { 481 | var MD5 = new hashes.MD5() 482 | const md5Hash = body 483 | ? MD5.b64(JSON.stringify(body)) 484 | : '1B2M2Y8AsgTpgAmY7PhCfg==' 485 | let urlParameters = Object.entries(urlParams) 486 | .map(e => e.join('=')) 487 | .join('&') 488 | const payload = `application/json;responseformat=3 489 | x-api-signature-nonce:${nonce} 490 | x-api-signature-version:1.0 491 | 492 | ${urlParameters} 493 | ${md5Hash} 494 | ${timestamp} 495 | ${method} 496 | ${url}` 497 | const secret = atob('NzRlNzQ2OWFmZjUwNDJiYmJlZDdiYmIxYjM2YzE1ZTk=') 498 | return new hashes.SHA1().b64_hmac(secret, payload) 499 | } 500 | 501 | // initial login to get credentials for the first time or after login expiration 502 | async function initialLogin () { 503 | console.log('Starting complete login process from scratch.') 504 | const url = 505 | 'https://awsapi.future.smart.com/login-app/api/v1/authorize?uiLocales=de-DE&uiLocales=de-DE' 506 | let req = new Request(url) 507 | req.headers = { 508 | 'x-app-id': 'SmartAPPEU', 509 | accept: 'application/json;responseformat=3', 510 | 'x-requested-with': 'com.smart.hellosmart', 511 | 'user-agent': 512 | 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', 513 | 'content-type': 'application/json; charset=utf-8' 514 | } 515 | await req.load() 516 | const urlParams = getUrlParams(req.response.url) 517 | const context = urlParams.context 518 | 519 | const loginUrl = 'https://auth.smart.com/accounts.login' 520 | req = new Request(loginUrl) 521 | req.method = 'POST' 522 | req.headers = { 523 | accept: '*/*', 524 | 'accept-language': 'de', 525 | 'content-type': 'application/x-www-form-urlencoded', 526 | 'x-requested-with': 'com.smart.hellosmart', 527 | cookie: 528 | 'gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4', 529 | origin: 'https://app.id.smart.com', 530 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)' 531 | } 532 | req.body = 533 | 'loginID=' + 534 | encodeURIComponent(userName) + 535 | '&password=' + 536 | encodeURIComponent(password) + 537 | '&sessionExpiration=2592000&targetEnv=jssdk&include=profile%2Cdata%2Cemails%2Csubscriptions%2Cpreferences%2C&includeUserInfo=true&loginMode=standard&lang=de&APIKey=3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a&source=showScreenSet&sdk=js_latest&authMode=cookie&pageURL=https%3A%2F%2Fapp.id.smart.com%2Flogin%3Fgig_ui_locales%3Dde-DE&sdkBuild=15482&format=json&riskContext=%7B%22b0%22%3A41187%2C%22b1%22%3A%5B0%2C2%2C3%2C1%5D%2C%22b2%22%3A4%2C%22b3%22%3A%5B%22-23%7C0.383%22%2C%22-81.33333587646484%7C0.236%22%5D%2C%22b4%22%3A3%2C%22b5%22%3A1%2C%22b6%22%3A%22Hello%20smart%2F1.4.0%20%28iPhone%3B%20iOS%2017.1%3B%20Scale%2F3.00%29%22%2C%22b7%22%3A%5B%5D%2C%22b8%22%3A%2216%3A33%3A26%22%2C%22b9%22%3A-60%2C%22b10%22%3Anull%2C%22b11%22%3Afalse%2C%22b12%22%3A%7B%22charging%22%3Afalse%2C%22chargingTime%22%3Anull%2C%22dischargingTime%22%3Anull%2C%22level%22%3A0.58%7D%2C%22b13%22%3A%5B5%2C%22360%7C760%7C24%22%2Cfalse%2Ctrue%5D%7D' 538 | let loginResult = await req.loadJSON() 539 | const loginToken = loginResult.sessionInfo.login_token 540 | 541 | const authUrl = 542 | 'https://auth.smart.com/oidc/op/v1.0/3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a/authorize/continue?context=' + 543 | context + 544 | '&login_token=' + 545 | loginToken 546 | const cookieValue = 547 | 'gmid=gmid.ver4.AcbHPqUK5Q.xOaWPhRTb7gy-6-GUW6cxQVf_t7LhbmeabBNXqqqsT6dpLJLOWCGWZM07EkmfM4j.u2AMsCQ9ZsKc6ugOIoVwCgryB2KJNCnbBrlY6pq0W2Ww7sxSkUa9_WTPBIwAufhCQYkb7gA2eUbb6EIZjrl5mQ.sc3; ucid=hPzasmkDyTeHN0DinLRGvw; hasGmid=ver4; gig_bootstrap_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=auth_ver4; glt_3_L94eyQ-wvJhWm7Afp1oBhfTGXZArUfSHHW9p9Pncg513hZELXsxCfMWHrF8f5P5a=' + 548 | loginToken 549 | req = new Request(authUrl) 550 | req.headers = { 551 | accept: '*/*', 552 | cookie: cookieValue, 553 | 'accept-language': 'de-DE,de;q=0.9,en-DE;q=0.8,en-US;q=0.7,en;q=0.6', 554 | 'x-requested-with': 'com.smart.hellosmart', 555 | 'user-agent': 'Hello smart/1.4.0 (iPhone; iOS 17.1; Scale/3.00)' 556 | } 557 | const authResult = await req.load() 558 | req = new Request(req.response.url) 559 | // follow redirect 560 | const finalAuthResult = await req.load() 561 | const tokens = getUrlParams(req.response.url) 562 | await saveCredentials(tokens) 563 | return tokens 564 | } 565 | 566 | // save smart api credentials to iCloud Drive 567 | async function saveCredentials (credentials) { 568 | const fm = FileManager.iCloud() 569 | const dir = fm.documentsDirectory() 570 | const path = fm.joinPath(dir, 'smart-credentials.json') 571 | fm.writeString(path, JSON.stringify(credentials)) 572 | } 573 | 574 | // load credentials from iCloud Drive 575 | async function loadCachedCredentials () { 576 | // load existing credentials from iCloud Drive 577 | const fm = FileManager.iCloud() 578 | const dir = fm.documentsDirectory() 579 | const path = fm.joinPath(dir, 'smart-credentials.json') 580 | const credentials = Data.fromFile(path) 581 | if (credentials != null) { 582 | return JSON.parse(credentials.toRawString()) 583 | } else { 584 | return new Object() 585 | } 586 | } 587 | 588 | // returns a pseudo random value as hex 589 | function randomHexString (len) { 590 | charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' 591 | var randomString = '' 592 | for (var i = 0; i < len; i++) { 593 | var randomPoz = Math.floor(Math.random() * charSet.length) 594 | randomString += charSet.substring(randomPoz, randomPoz + 1) 595 | } 596 | 597 | var result = '' 598 | for (var i = 0; i < randomString.length; i++) { 599 | result += randomString.charCodeAt(i).toString(16) 600 | } 601 | return result 602 | } 603 | 604 | // returns url query params as json 605 | function getUrlParams (url) { 606 | var regex = /[?&]([^=#]+)=([^&#]*)/g, 607 | params = {}, 608 | match 609 | while ((match = regex.exec(url))) { 610 | params[match[1]] = match[2] 611 | } 612 | return params 613 | } 614 | 615 | // resolve geo location 616 | async function getGeoData () { 617 | let latitude = carData.data.vehicleStatus.basicVehicleStatus.position.latitude 618 | latitude = latitude / 3600000 619 | let longitude = 620 | carData.data.vehicleStatus.basicVehicleStatus.position.longitude 621 | longitude = longitude / 3600000 622 | let geoData 623 | if (longitude == 0 && latitude == 0) { 624 | console.log('Geo data unavailable!') 625 | geoData = { 626 | address: { 627 | road: lang.unavailable, 628 | house_number: '', 629 | postcode: '', 630 | city: '', 631 | city_district: '' 632 | } 633 | } 634 | } else { 635 | const url = 636 | 'https://revgeocode.search.hereapi.com/v1/revgeocode?xnlp=CL_JSMv3.1.49.1&apikey=' 637 | + apiKey + '&at=' + latitude + '%2C' + longitude + '&limit=1' 638 | const req = new Request(url) 639 | geoData = await req.loadJSON() 640 | } 641 | return geoData 642 | } 643 | 644 | // get images from local filestore or download them once 645 | async function getImage (image) { 646 | let fm = FileManager.local() 647 | let dir = fm.documentsDirectory() 648 | let path = fm.joinPath(dir, image) 649 | if (fm.fileExists(path)) { 650 | return fm.readImage(path) 651 | } else { 652 | // download once 653 | let imageUrl 654 | if (image === 'smart-logo.png') { 655 | imageUrl = 'https://i.imgur.com/MM0UI31.png' 656 | } else { 657 | imageUrl = 658 | 'https://s7.future.smart.com/is/image/smarteurope/ppo_' + 659 | model + 660 | ':16-9?$smartResponsiveHiDPI$&wid=305&hei=232' 661 | } 662 | let iconImage = await loadImage(imageUrl) 663 | fm.writeImage(path, iconImage) 664 | return iconImage 665 | } 666 | } 667 | 668 | // helper function to download an image from a given url 669 | async function loadImage (imgUrl) { 670 | const req = new Request(imgUrl) 671 | return await req.loadImage() 672 | } 673 | 674 | // draws the battery circle 675 | function drawArc (deg, fillColor, strokeColor, label) { 676 | let ctr = new Point(canvSize / 2, canvSize / 2), 677 | bgx = ctr.x - canvRadius 678 | bgy = ctr.y - canvRadius 679 | bgd = 2 * canvRadius 680 | bgr = new Rect(bgx, bgy, bgd, bgd) 681 | 682 | canvas.opaque = false 683 | 684 | canvas.setFillColor(fillColor) 685 | canvas.setStrokeColor(strokeColor) 686 | canvas.setLineWidth(canvWidth) 687 | canvas.strokeEllipse(bgr) 688 | 689 | for (t = 0; t < deg; t++) { 690 | rect_x = ctr.x + canvRadius * sinDeg(t) - canvWidth / 2 691 | rect_y = ctr.y - canvRadius * cosDeg(t) - canvWidth / 2 692 | rect_r = new Rect(rect_x, rect_y, canvWidth, canvWidth) 693 | canvas.fillEllipse(rect_r) 694 | } 695 | // attempt to draw label/icon 696 | const canvLabelRect = new Rect(0, 100 - canvTextSize / 2 - 8, canvSize, 200) 697 | canvas.setTextAlignedCenter() 698 | canvas.setFont(Font.boldSystemFont(canvTextSize)) 699 | canvas.setTextColor(new Color('#000080')) 700 | canvas.drawTextInRect(label, canvLabelRect) 701 | // return canvas.getImage() 702 | } 703 | 704 | // helper function for the battery circle 705 | function sinDeg (deg) { 706 | return Math.sin((deg * Math.PI) / 180) 707 | } 708 | 709 | // helper function for the battery circle 710 | function cosDeg (deg) { 711 | return Math.cos((deg * Math.PI) / 180) 712 | } 713 | 714 | async function showError (errorMessage) { 715 | let widget = new ListWidget() 716 | widget.url = 'hellosmart://' 717 | widget.backgroundColor = Color.white() 718 | 719 | let smartIcon = await getImage('smart-logo.png') 720 | let smartIconImage = widget.addImage(smartIcon) 721 | smartIconImage.imageSize = new Size(70, 14) 722 | smartIconImage.centerAlignImage() 723 | widget.addSpacer() 724 | let warningTxt = widget.addText(errorMessage) 725 | warningTxt.font = Font.semiboldSystemFont(12) 726 | warningTxt.textColor = new Color('#34443c') 727 | warningTxt.centerAlignText() 728 | Script.setWidget(widget) 729 | Script.complete() 730 | widget.presentSmall() 731 | } 732 | 733 | // 734 | // ATTENTION: Make sure to copy the script until the end! 735 | // 736 | --------------------------------------------------------------------------------