diff --git a/.gitattributes b/.gitattributes index 0a9b14ea..2e391662 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,2 @@ *.js linguist-vendored -*.css linguist-vendored -*.html linguist-vendored \ No newline at end of file +css/font-awesome.css linguist-vendored diff --git a/.travis.yml b/.travis.yml index 063bcd17..a907aa1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,3 +12,9 @@ script: - "npm run firefox" - "npm run chrome" - "addons-linter firefox" +deploy: + # release tagging + - provider: script + script: bash ci/tag.sh + on: + branch: release diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json new file mode 100644 index 00000000..414f5ac8 --- /dev/null +++ b/_locales/fr/messages.json @@ -0,0 +1,238 @@ +{ + "extName": { + "message": "Authentificateur", + "description": "Extension Name." + }, + "extShortName": { + "message": "Authentificateur", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "Authenticator génère, dans votre navigateur, des codes d'authentification (TOTP) en deux étapes.", + "description": "Extension Description." + }, + "added": { + "message": " à été ajouté.", + "description": "Added Account." + }, + "errorqr": { + "message": "Le QR code n'a pas été reconnu.", + "description": "QR Error." + }, + "errorsecret": { + "message": "Erreur sur la clef secrète. Seul les caractères Base32(A-Z, 2-7 et =) et HEX(0-9 et A-F) sont supportés. Néanmoins, votre clef secrète est: ", + "description": "Secret Error." + }, + "add_qr": { + "message": "Acquisition du QR code", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "Saisie manuelle", + "description": "Manual Entry." + }, + "close": { + "message": "Fermer", + "description": "Close." + }, + "ok": { + "message": "Ok", + "description": "OK." + }, + "yes": { + "message": "Oui", + "description": "Yes." + }, + "no": { + "message": "Non", + "description": "No." + }, + "account": { + "message": "Compte", + "description": "Account." + }, + "accountName": { + "message": "Nom du Compte", + "description": "Account Name." + }, + "issuer": { + "message": "Emetteur", + "description": "Issuer." + }, + "secret": { + "message": "Secret", + "description": "Secret." + }, + "updateSuccess": { + "message": "Succés.", + "description": "Update Success." + }, + "updateFailure": { + "message": "Echec.", + "description": "Update Failure." + }, + "about": { + "message": "A Propos", + "description": "About." + }, + "export_import": { + "message": "Export \/ Import", + "description": "Export and Import." + }, + "settings": { + "message": "Paramètres", + "description": "Settings." + }, + "security": { + "message": "Securité", + "description": "Security." + }, + "current_phrase": { + "message": "Phrase secrète Actuelle", + "description": "Current Passphrase." + }, + "new_phrase": { + "message": "Nouvelle Phrase Secrète", + "description": "New Passphrase." + }, + "phrase": { + "message": "Phrase Secrète", + "description": "Passphrase." + }, + "confirm_phrase": { + "message": "Confirmation de la phrase Secrète", + "description": "Confirm Passphrase." + }, + "confirm_delete": { + "message": "Etes-vous sûr de vouloir détruire ce secret ? Cette action ne peut pas être annulé.", + "description": "Remove entry confirmation" + }, + "security_warning": { + "message": "Ce mot de passe sera utilisé pour chiffrer vos clefs secrètes. Personne ne sera en mesure de vous aider à les retrouver, si vous les perdez.", + "description": "Passphrase Warning." + }, + "update": { + "message": "Mise à Jour", + "description": "Update." + }, + "phrase_incorrect": { + "message": "Vous ne pouvez pas ajouter de nouveaux comptes ou exporter les données avant que le déchiffrement de tous les comptes ne soit terminé. Veuillez saisir le mot de passe avant de pouvoir continuer.", + "description": "Passphrase Incorrect." + }, + "phrase_not_match": { + "message": "Le Mot de Passe ne correspond pas.", + "description": "Passphrase Not Match." + }, + "encrypted": { + "message": "Chiffré", + "description": "Encrypted." + }, + "copied": { + "message": "Copié", + "description": "Copied." + }, + "feedback": { + "message": "Feedback", + "description": "Feedback." + }, + "translate": { + "message": "Traduire", + "description": "Translate." + }, + "source": { + "message": "Code Source", + "description": "Source Code." + }, + "passphrase_info": { + "message": "Saisir le Mot de Passe pour Déchiffrer les Données du Compte.", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "Synchroniser l'horloge avec celle de Google", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "Mémoriser le Mot de Passe", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "Attention ! Un décalage trop important existe avec votre horloge locale, réglez ce problème avant de continuer.", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "Avez vous sauvegardé vos clefs secrètes ? N'attendez pas qu'il soit trop tard !", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "La Capture à échoué, veuillez recharger la page et essayer à nouveau.", + "description": "Capture Failed" + }, + "based_on_time": { + "message": "Basé sur le temps", + "description": "Time Based" + }, + "based_on_counter": { + "message": "Basé sur un compteur", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "Retailler la page du popup", + "description": "Resize Popup Page" + }, + "scale": { + "message": "Dimensionner", + "description": "Scale" + }, + "export_info": { + "message": "Attention: les backup ne sont pas protégés par chiffrement. Vous voulez ajouté un compte à une autre application ? Survollez la partie supérieur droite d'un des comptes et cliquez sur le bouton caché qui apparaît.", + "description": "Export menu info text" + }, + "download_backup": { + "message": "Télécharger un Fichier de Sauvegarde", + "description": "Download backup file." + }, + "import_backup": { + "message": "Import d'une sauvegarde", + "description": "Import backup." + }, + "import_backup_file": { + "message": "Import d'un fichier de sauvegarde", + "description": "Import backup file." + }, + "import_backup_code": { + "message": "Importation au format texte", + "description": "Import backup code." + }, + "dropbox_backup": { + "message": "Sauvegarde Automatique dans Dropbox", + "description": "Auto backup to Dropbox." + }, + "dropbox_code": { + "message": "Code Dropbox", + "description": "Dropbox code." + }, + "dropbox_token": { + "message": "Jeton Dropbox", + "description": "Dropbox token." + }, + "dropbox_authorization": { + "message": "Obtenir le Code", + "description": "Dropbox authorization." + }, + "show_all_entries": { + "message": "Afficher Toutes les Entrées", + "description": "Show all entries." + }, + "dropbox_risk": { + "message": "Attention: les sauvegardes Dropbox ne sont pas protégées par chiffrement. Utilisez les à vos risques et périls.", + "description": "Dropbox backup risk warning." + }, + "import_error_password": { + "message": "Vous devez fournir le mot de passe pour importer une sauvegarde.", + "description": "Error password warning when import backups." + }, + "local_passphrase_warning": { + "message": "Votre mot de passe est stocké localement, veuillez le changer immédiatement par l'intermédiaire du menu sécurité.", + "description": "localStorage password warning." + } +} diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index a2c4b925..6595483c 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -8,7 +8,7 @@ "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator用以在瀏覽器中生成二步認證程式碼。", + "message": "Authenticator用以在瀏覽器中生成兩步驟驗證碼。", "description": "Extension Description." }, "added": { @@ -48,11 +48,11 @@ "description": "No." }, "account": { - "message": "賬戶", + "message": "帳戶", "description": "Account." }, "accountName": { - "message": "賬戶名稱", + "message": "帳戶名稱", "description": "Account Name." }, "issuer": { @@ -84,7 +84,7 @@ "description": "Settings." }, "security": { - "message": "安全", + "message": "安全性", "description": "Security." }, "current_phrase": { @@ -104,11 +104,11 @@ "description": "Confirm Passphrase." }, "confirm_delete": { - "message": "您確定要刪除此金鑰嗎?此操作無法撤銷。", + "message": "您確定要刪除此金鑰嗎?此操作無法復原。", "description": "Remove entry confirmation" }, "security_warning": { - "message": "您的金鑰將使用此密碼進行加密。如果您忘記了密碼沒有人能夠提供幫助。", + "message": "您的金鑰將使用此密碼進行加密。請妥善保管,將無法取回遺失的密碼。", "description": "Passphrase Warning." }, "update": { @@ -116,19 +116,19 @@ "description": "Update." }, "phrase_incorrect": { - "message": "部分賬戶與密碼不匹配,您無法新增新賬戶、匯出賬戶資料或者更改密碼。請提供正確的密碼後重試。", + "message": "部分帳戶密碼輸入錯誤,您無法新增帳戶、匯出帳戶資料或更改密碼。請在輸入正確的密碼後重試。", "description": "Passphrase Incorrect." }, "phrase_not_match": { - "message": "兩次密碼不一致。", + "message": "密碼錯誤。", "description": "Passphrase Not Match." }, "encrypted": { - "message": "已加密", + "message": "加密成功!", "description": "Encrypted." }, "copied": { - "message": "已複製", + "message": "複製成功!", "description": "Copied." }, "feedback": { @@ -144,7 +144,7 @@ "description": "Source Code." }, "passphrase_info": { - "message": "輸入密碼以解碼賬戶資料。", + "message": "輸入密碼以解碼帳戶資料。", "description": "Passphrase Info" }, "sync_clock": { @@ -156,27 +156,27 @@ "description": "Remember Passphrase" }, "clock_too_far_off": { - "message": "注意!您的本地時鐘時間差過大,請修正後再進行操作。", + "message": "注意!您的本地時鐘時間差異過大,請於效準時間後再進行操作。", "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "您是否為金鑰建立了備份?不要等到為時已晚。", + "message": "您是否為金鑰建立了備份?別等到為時已晚才建立!", "description": "Remind Backup" }, "capture_failed": { - "message": "捕捉失敗,請過載您正在瀏覽的頁面後重試。", + "message": "讀取失敗,請於重新載入頁面後重試。", "description": "Capture Failed" }, "based_on_time": { - "message": "基於時間", + "message": "驗證碼", "description": "Time Based" }, "based_on_counter": { - "message": "基於計數器", + "message": "一次性驗證碼", "description": "Counter Based" }, "resize_popup_page": { - "message": "調整彈出頁面尺寸", + "message": "調整彈出視窗大小", "description": "Resize Popup Page" }, "scale": { @@ -196,11 +196,11 @@ "description": "Import backup." }, "import_backup_file": { - "message": "匯入備份檔案", + "message": "以檔案匯入", "description": "Import backup file." }, "import_backup_code": { - "message": "匯入備份文字", + "message": "以文字匯入", "description": "Import backup code." }, "dropbox_backup": { @@ -224,7 +224,7 @@ "description": "Show all entries." }, "dropbox_risk": { - "message": "警告:儲存至Dropbox的備份均未備份,您需自擔風險。", + "message": "警告:儲存至Dropbox的備份檔案均未加密,需自行承擔風險。", "description": "Dropbox backup risk warning." }, "import_error_password": { diff --git a/ci/authenticator-build-key.enc b/ci/authenticator-build-key.enc new file mode 100644 index 00000000..351832cc Binary files /dev/null and b/ci/authenticator-build-key.enc differ diff --git a/ci/tag.sh b/ci/tag.sh new file mode 100644 index 00000000..dca9a025 --- /dev/null +++ b/ci/tag.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# This script is used by travis to auto tag our releases + +# Configure git +git config --global user.email "builds@travis-ci.com" +git config --global user.name "Travis CI" +git remote set-url origin git@github.com:Authenticator-Extension/Authenticator.git +openssl aes-256-cbc -K $encrypted_2b3e3bd93233_key -iv $encrypted_2b3e3bd93233_iv -in $TRAVIS_BUILD_DIR/ci/authenticator-build-key.enc -out $TRAVIS_BUILD_DIR/ci/authenticator-build-key -d +chmod 600 $TRAVIS_BUILD_DIR/ci/authenticator-build-key +eval `ssh-agent -s` +ssh-add $TRAVIS_BUILD_DIR/ci/authenticator-build-key + +# Create and push tag +export GIT_TAG=v$(grep -m 1 "\"version\"" $TRAVIS_BUILD_DIR/manifest-chrome.json | sed -r 's/^ *//;s/.*: *"//;s/",?//') +git checkout $TRAVIS_BRANCH +git tag $GIT_TAG -a -m "Automatic tag from TravisCI build $TRAVIS_BUILD_NUMBER" +git push origin $GIT_TAG diff --git a/css/popup.css b/css/popup.css index 7dd3e018..048cfa0e 100644 --- a/css/popup.css +++ b/css/popup.css @@ -356,7 +356,7 @@ body { #upload_backup, #security_save, #passphrase_ok, -#dropbox_ok, +#dropbox_get_code, #message_close, #exportButton, #resize_save { @@ -400,7 +400,7 @@ body { #exportButton, #security_save, #passphrase_ok, -#dropbox_ok, +#dropbox_get_code, #resize_save { font-size: 12px; margin: 20px 100px; @@ -651,7 +651,7 @@ body { #passphraseClose:hover, #security_save:hover, #passphrase_ok:hover, -#dropbox_ok:hover, +#dropbox_get_code:hover, #export:hover, #resizeClose:hover, #resize_save:hover, diff --git a/dropboxtoken.html b/dropboxtoken.html new file mode 100644 index 00000000..6c9beb81 --- /dev/null +++ b/dropboxtoken.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/js/dropboxtoken.js b/js/dropboxtoken.js new file mode 100644 index 00000000..dddb013b --- /dev/null +++ b/js/dropboxtoken.js @@ -0,0 +1,30 @@ +function getToken() { + let hash = document.location.hash; + + if (!hash) { + return; + } + + hash = hash.substr(1); + + let resData = hash.split('&'); + for (let i = 0; i < resData.length; i++) { + let kv = resData[i]; + console.log(kv, /^(.*?)=(.*?)$/.test(kv)) + if (/^(.*?)=(.*?)$/.test(kv)) { + let kvMatches = kv.match(/^(.*?)=(.*?)$/); + let key = kvMatches[1]; + let value = kvMatches[2]; + console.log(key, value) + if (key === 'access_token') { + localStorage.dropboxToken = value; + break; + } + } + } + + window.close(); + return; +} + +window.onload = getToken; \ No newline at end of file diff --git a/manifest-chrome.json b/manifest-chrome.json index ab7955fa..ed7d47a8 100644 --- a/manifest-chrome.json +++ b/manifest-chrome.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_extShortName__", - "version": "5.0.6", + "version": "5.0.7", "default_locale": "en", "description": "__MSG_extDesc__", "icons": { diff --git a/manifest-firefox.json b/manifest-firefox.json index beeb4e75..3ef1d738 100644 --- a/manifest-firefox.json +++ b/manifest-firefox.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "__MSG_extName__", "short_name": "__MSG_extShortName__", - "version": "5.0.6", + "version": "5.0.7", "default_locale": "en", "description": "__MSG_extDesc__", "applications": { diff --git a/popup.html b/popup.html index 4b32a56f..1269a0ef 100644 --- a/popup.html +++ b/popup.html @@ -32,13 +32,13 @@
-
-
+
+
{{ entry.issuer.split('::')[0] }}
-
+
{{ entry.account }}
@@ -93,6 +93,7 @@

Droid Sans Mono Copyright Steve Matteson. Licensed under the Apache License.

Font Awesome Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.

Thanks to Mike Robinson <3

+

QR Debugging

@@ -101,7 +102,7 @@
{{ i18n.add_secret }}
- + @@ -140,14 +141,9 @@
{{ i18n.dropbox_risk }}
- - -
{{ i18n.ok }}
+
{{ i18n.dropbox_authorization }}
diff --git a/qrdebug.html b/qrdebug.html new file mode 100644 index 00000000..bbdf7d11 --- /dev/null +++ b/qrdebug.html @@ -0,0 +1,16 @@ + + + + QR Debugging + + + +

QR Scan Debugging Page

+ +
+
+ + + + diff --git a/src/background.ts b/src/background.ts index 2aba094b..755c855d 100644 --- a/src/background.ts +++ b/src/background.ts @@ -101,8 +101,13 @@ async function getTotp(text: string, passphrase: string) { } else { const encryption = new Encryption(passphrase); const hash = CryptoJS.MD5(secret).toString(); - if (!/^[2-7a-z]+=*$/i.test(secret) && /^[0-9a-f]+$/i.test(secret)) { + if (!/^[2-7a-z]+=*$/i.test(secret) && /^[0-9a-f]+$/i.test(secret) && + type === 'totp') { type = 'hex'; + } else if ( + !/^[2-7a-z]+=*$/i.test(secret) && /^[0-9a-f]+$/i.test(secret) && + type === 'hotp') { + type = 'hhex'; } const entryData: {[hash: string]: OTPStorage} = {}; entryData[hash] = { diff --git a/src/models/dropbox.ts b/src/models/dropbox.ts index 2003b3c9..c240106d 100644 --- a/src/models/dropbox.ts +++ b/src/models/dropbox.ts @@ -4,39 +4,8 @@ /// class Dropbox { - async getToken(code?: string) { - if (localStorage.dropboxToken) { - return localStorage.dropboxToken; - } - - if (!code) { - return ''; - } - - const url = 'https://api.dropboxapi.com/oauth2/token'; - return new Promise( - (resolve: (value: string) => void, reject: (reason: Error) => void) => { - try { - const xhr = new XMLHttpRequest(); - xhr.open('POST', url); - xhr.setRequestHeader( - 'Content-type', 'application/x-www-form-urlencoded'); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - const res: {[key: string]: string} = - JSON.parse(xhr.responseText); - localStorage.dropboxToken = res.access_token; - return resolve(res.access_token); - } - return; - }; - xhr.send( - `client_id=013qun2m82h9jim&client_secret=pk5tt1jrxuwq240&grant_type=authorization_code&code=${ - code}`); - } catch (error) { - return reject(error); - } - }); + async getToken() { + return localStorage.dropboxToken || ''; } async upload(encryption: Encryption) { diff --git a/src/models/interface.ts b/src/models/interface.ts index f108cbae..b7e48e99 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -6,7 +6,8 @@ enum OTPType { hotp, battle, steam, - hex + hex, + hhex } interface OTP { diff --git a/src/models/key-utilities.ts b/src/models/key-utilities.ts index 9d66a344..5f3fd5bf 100644 --- a/src/models/key-utilities.ts +++ b/src/models/key-utilities.ts @@ -36,10 +36,16 @@ class KeyUtilities { const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let bits = ''; let hex = ''; + let padding = 0; for (let i = 0; i < base32.length; i++) { - const val = base32chars.indexOf(base32.charAt(i).toUpperCase()); - bits += this.leftpad(val.toString(2), 5, '0'); + if (base32.charAt(i) === '=') { + bits += '00000'; + padding++; + } else { + const val = base32chars.indexOf(base32.charAt(i).toUpperCase()); + bits += this.leftpad(val.toString(2), 5, '0'); + } } for (let i = 0; i + 4 <= bits.length; i += 4) { @@ -47,8 +53,26 @@ class KeyUtilities { hex = hex + Number(`0b${chunk}`).toString(16); } - if (hex.length % 2 && hex[hex.length - 1] === '0') { - hex = hex.substr(0, hex.length - 1); + // if (hex.length % 2 && hex[hex.length - 1] === '0') { + // hex = hex.substr(0, hex.length - 1); + // } + switch (padding) { + case 0: + break; + case 6: + hex = hex.substr(0, hex.length - 8); + break; + case 4: + hex = hex.substr(0, hex.length - 6); + break; + case 3: + hex = hex.substr(0, hex.length - 4); + break; + case 1: + hex = hex.substr(0, hex.length - 2); + break; + default: + throw new Error('Invalid Base32 string'); } return hex; @@ -79,6 +103,7 @@ class KeyUtilities { key = this.base32tohex(secret); break; case OTPType.hex: + case OTPType.hhex: key = secret; break; case OTPType.battle: @@ -98,7 +123,7 @@ class KeyUtilities { throw new Error('Invalid secret key'); } - if (type !== OTPType.hotp) { + if (type !== OTPType.hotp && type !== OTPType.hhex) { let epoch = Math.round(new Date().getTime() / 1000.0); if (localStorage.offset) { epoch = epoch + Number(localStorage.offset); diff --git a/src/models/otp.ts b/src/models/otp.ts index bb3ed350..4f8b4ad2 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -16,15 +16,17 @@ class OTPEntry implements OTP { constructor( type: OTPType, issuer: string, secret: string, account: string, - index: number, counter: number) { + index: number, counter: number, hash?: string) { this.type = type; this.index = index; this.issuer = issuer; this.secret = secret; this.account = account; - this.hash = CryptoJS.MD5(secret).toString(); + this.hash = hash && /^[0-9a-f]{32}$/.test(hash) ? + hash : + CryptoJS.MD5(secret).toString(); this.counter = counter; - if (this.type !== OTPType.hotp) { + if (this.type !== OTPType.hotp && this.type !== OTPType.hhex) { this.generate(); } } @@ -45,7 +47,7 @@ class OTPEntry implements OTP { } async next(encryption: Encryption) { - if (this.type !== OTPType.hotp) { + if (this.type !== OTPType.hotp && this.type !== OTPType.hhex) { return; } this.generate(); @@ -62,6 +64,9 @@ class OTPEntry implements OTP { this.code = KeyUtilities.generate(this.type, this.secret, this.counter); } catch (error) { this.code = 'Invalid'; + if (parent) { + parent.postMessage(`Invalid secret: [${this.secret}]`, '*'); + } } } } diff --git a/src/models/storage.ts b/src/models/storage.ts index f638ed67..d97a8652 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -159,8 +159,21 @@ class EntryStorage { } } + if (!/^[a-z2-7]+=*$/i.test(data[hash].secret) && + /^[0-9a-f]+$/i.test(data[hash].secret) && + data[hash].type === OTPType[OTPType.totp]) { + data[hash].type = OTPType[OTPType.hex]; + } + + if (!/^[a-z2-7]+=*$/i.test(data[hash].secret) && + /^[0-9a-f]+$/i.test(data[hash].secret) && + data[hash].type === OTPType[OTPType.hotp]) { + data[hash].type = OTPType[OTPType.hhex]; + } + const _hash = CryptoJS.MD5(data[hash].secret).toString(); - if (_hash !== hash) { + // not a valid hash + if (!/^[0-9a-f]{32}$/.test(hash)) { data[_hash] = data[hash]; data[_hash].hash = _hash; delete data[hash]; @@ -251,7 +264,7 @@ class EntryStorage { chrome.storage.sync.get( async (_data: {[hash: string]: OTPStorage}) => { const data: OTPEntry[] = []; - for (let hash of Object.keys(_data)) { + for (const hash of Object.keys(_data)) { if (!this.isValidEntry(_data, hash)) { continue; } @@ -274,6 +287,8 @@ class EntryStorage { case 'hotp': case 'battle': case 'steam': + case 'hex': + case 'hhex': type = OTPType[entryData.type]; break; default: @@ -283,6 +298,7 @@ class EntryStorage { entryData.type = OTPType[OTPType.totp]; needMigrate = true; } + entryData.secret = entryData.encrypted ? encryption.getDecryptedSecret(entryData.secret) : entryData.secret; @@ -294,6 +310,9 @@ class EntryStorage { if (secretMatches && secretMatches.length >= 3) { entryData.secret = secretMatches[2]; entryData.type = OTPType[OTPType.battle]; + entryData.hash = + CryptoJS.MD5(entryData.secret).toString(); + await this.remove(hash); needMigrate = true; } } @@ -304,31 +323,57 @@ class EntryStorage { if (secretMatches && secretMatches.length >= 2) { entryData.secret = secretMatches[1]; entryData.type = OTPType[OTPType.steam]; + entryData.hash = + CryptoJS.MD5(entryData.secret).toString(); + await this.remove(hash); needMigrate = true; } } + if (!/^[a-z2-7]+=*$/i.test(entryData.secret) && + /^[0-9a-f]+$/i.test(entryData.secret) && + entryData.type === OTPType[OTPType.totp]) { + entryData.type = OTPType[OTPType.hex]; + needMigrate = true; + } + + if (!/^[a-z2-7]+=*$/i.test(entryData.secret) && + /^[0-9a-f]+$/i.test(entryData.secret) && + entryData.type === OTPType[OTPType.hotp]) { + entryData.type = OTPType[OTPType.hhex]; + needMigrate = true; + } + const entry = new OTPEntry( type, entryData.issuer, entryData.secret, - entryData.account, entryData.index, entryData.counter); - + entryData.account, entryData.index, entryData.counter, + entryData.hash); data.push(entry); - // we need correct the hash - if (entry.secret !== 'Encrypted') { + // we need correct the hash + + // Do not correct hash, wrong password + // may not only 'Encrypted', but also + // other wrong secret. We cannot know + // if the hash doesn't match the correct + // secret + + // Only correct invalid hash here + + if (entry.secret !== 'Encrypted' && + !/^[0-9a-f]{32}$/.test(hash)) { const _hash = CryptoJS.MD5(entryData.secret).toString(); if (hash !== _hash) { await this.remove(hash); - hash = _hash; - entryData.hash = hash; + entryData.hash = _hash; needMigrate = true; } } if (needMigrate) { const _entry: {[hash: string]: OTPStorage} = {}; - _entry[hash] = entryData; - _entry[hash].encrypted = false; + _entry[entryData.hash] = entryData; + _entry[entryData.hash].encrypted = false; this.import(encryption, _entry); } } @@ -336,6 +381,15 @@ class EntryStorage { data.sort((a, b) => { return a.index - b.index; }); + + for (let i = 0; i < data.length; i++) { + if (data[i].index !== i) { + const exportData = await this.getExport(encryption); + await this.import(encryption, exportData); + break; + } + } + return resolve(data); }); return; diff --git a/src/qrdebug.ts b/src/qrdebug.ts new file mode 100644 index 00000000..b1ea2bf3 --- /dev/null +++ b/src/qrdebug.ts @@ -0,0 +1,53 @@ +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'position') { + if (!sender.tab) { + return; + } + getQrDebug( + sender.tab, message.info.left, message.info.top, message.info.width, + message.info.height, message.info.windowWidth, message.info.passphrase); + } +}); + +function getQrDebug( + tab: chrome.tabs.Tab, left: number, top: number, width: number, + height: number, windowWidth: number, passphrase: string) { + chrome.tabs.captureVisibleTab(tab.windowId, {format: 'png'}, (dataUrl) => { + const qr = new Image(); + qr.src = dataUrl; + qr.onload = () => { + const captureCanvas = document.createElement('canvas'); + captureCanvas.width = width; + captureCanvas.height = height; + const ctx = captureCanvas.getContext('2d'); + if (!ctx) { + return; + } + ctx.drawImage(qr, left, top, width, height, 0, 0, width, height); + const url = captureCanvas.toDataURL(); + const infoDom = document.getElementById('info'); + if (infoDom) { + infoDom.innerHTML = 'Scan Data:
' + + `
` + + `Window Inner Width: ${windowWidth}
` + + `Width: ${width}
` + + `Height: ${height}
` + + `Left: ${left}
` + + `Top: ${top}
` + + `Screen Width: ${window.screen.width}
` + + `Screen Height: ${window.screen.height}
` + + `Capture Width: ${qr.width}
` + + `Capture Height: ${qr.height}
` + + `Device Pixel Ratio:${window.devicePixelRatio}
` + + `Tab ID: ${tab.id}
` + + '
' + + 'Captured Screenshot:'; + } + + const qrDom = document.getElementById('qr') as HTMLImageElement; + if (qrDom) { + qrDom.src = url; + } + }; + }); +} diff --git a/src/test/test.ts b/src/test/test.ts new file mode 100644 index 00000000..6270d2c9 --- /dev/null +++ b/src/test/test.ts @@ -0,0 +1,359 @@ +interface TestCase { + name: string; + data: { + /* tslint:disable-next-line:no-any */ + [hash: string]: { + /* tslint:disable-next-line:no-any */ + [key: string]: any + }; + }; +} + +const cases: TestCase[] = [ + { + name: 'Missing fields', + data: {'7733be61632fa6af88d31218e6c4afb2': {'secret': 'abcd2345'}} + }, + { + name: 'Bad hash in key', + data: { + 'badhash': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': '7733be61632fa6af88d31218e6c4afb2', + 'index': 0, + 'issuer': '', + 'secret': 'abcd2345', + 'type': 'totp' + } + } + }, + { + name: 'Bad hash', + data: { + 'badhash': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': 'badhash', + 'index': 0, + 'issuer': '', + 'secret': 'abcd2345', + 'type': 'totp' + } + } + }, + { + name: 'Bad type for HEX', + data: { + 'e19d5cd5af0378da05f63f891c7467af': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': 'e19d5cd5af0378da05f63f891c7467af', + 'index': 0, + 'issuer': '', + 'secret': 'abcd1234', + 'type': 'totp' + } + } + }, + { + name: 'Unicode in issuer', + data: { + '7733be61632fa6af88d31218e6c4afb2': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': '7733be61632fa6af88d31218e6c4afb2', + 'index': 0, + 'issuer': '✓ à la mode', + 'secret': 'abcd2345', + 'type': 'totp' + } + } + }, + { + name: 'Battle migrate', + data: { + '95c869de1221960c7f7e6892f78d7062': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': '95c869de1221960c7f7e6892f78d7062', + 'index': 0, + 'issuer': '', + 'secret': 'blz-abcd2345', + 'type': 'totp' + } + } + }, + { + name: 'Steam migrate', + data: { + '95c869de1221960c7f7e6892f78d7062': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': '95c869de1221960c7f7e6892f78d7062', + 'index': 0, + 'issuer': '', + 'secret': 'stm-abcd2345', + 'type': 'totp' + } + } + }, + { + name: 'Missing field with HEX secret', + data: {'e19d5cd5af0378da05f63f891c7467af': {'secret': 'abcd1234'}} + }, + { + name: 'Mess index', + data: { + '7733be61632fa6af88d31218e6c4afb2': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': '7733be61632fa6af88d31218e6c4afb2', + 'index': 6, + 'issuer': '', + 'secret': 'abcd2345', + 'type': 'totp' + }, + '770f51f23603ddae810e446630c2f673': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': '770f51f23603ddae810e446630c2f673', + 'index': 6, + 'issuer': '', + 'secret': 'abcd2346', + 'type': 'totp' + } + } + }, + { + name: 'Base32 with padding', + data: { + 'b905232a977347a0a113a7d1c924fb8d': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': 'b905232a977347a0a113a7d1c924fb8d', + 'index': 0, + 'issuer': '', + 'secret': 'DKCE3SQPHJRJQGBGI322QA7Z5E======', + 'type': 'totp' + } + } + }, + { + name: 'Incorrect but valid hash', + data: { + 'ffffffffffffffffffffffffffffffff': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': 'ffffffffffffffffffffffffffffffff', + 'index': 0, + 'issuer': '', + 'secret': 'abcd2345', + 'type': 'totp' + } + } + }, + { + name: 'HOTP with HEX secret', + data: { + '7c117a118e015b6232ff359958b9e270': { + 'account': 'test', + 'counter': 0, + 'encrypted': false, + 'hash': '7c117a118e015b6232ff359958b9e270', + 'index': 0, + 'issuer': '', + 'secret': '2c52e8fcfac34091da63ef7b118f1cc50b925a42', + 'type': 'hhex' + } + } + } +]; + +let testCaseIndex = 0; +let testRes: Array<{pass: boolean, error: string}> = []; +let testResData: string[] = []; + +function testStart() { + if (document.getElementById('lock')) { + const checkbox = document.getElementById('lock') as HTMLInputElement; + if (!checkbox.checked) { + return; + } + } + const startBtn = document.getElementById('start'); + if (startBtn) { + startBtn.setAttribute('disabled', 'true'); + } + testCaseIndex = 0; + testRes = []; + test(); +} + +function testFinished() { + clear(); + console.log('Test finished.'); + for (const res of testRes) { + if (!res.pass) { + alert('Test failed!'); + return; + } + } + alert('Test passed!'); + return; +} + +async function clear() { + return new Promise((resolve: () => void, reject: (reason: Error) => void) => { + try { + chrome.storage.sync.clear(resolve); + } catch (error) { + reject(error); + } + }); +} + +async function get() { + return new Promise( + (resolve: (items: {[key: string]: T}) => void, + reject: (reason: Error) => void) => { + try { + chrome.storage.sync.get(resolve); + } catch (error) { + reject(error); + } + }); +} + +async function set(items: {[key: string]: {}}) { + /* tslint:disable-next-line:no-any */ + return new Promise((resolve: () => void, reject: (reason: Error) => void) => { + try { + chrome.storage.sync.set(items, resolve); + } catch (error) { + reject(error); + } + }); +} + +async function test() { + if (testCaseIndex === cases.length * 2) { + testFinished(); + return; + } + + console.log( + cases[Math.floor(testCaseIndex / 2)].name, + testCaseIndex % 2 ? 'Reopen' : ''); + + if (testCaseIndex % 2 === 0) { + clear(); + await set(cases[Math.floor(testCaseIndex / 2)].data); + } + + if (document.getElementsByTagName('iframe') && + document.getElementsByTagName('iframe')[0]) { + testRes[testCaseIndex] = {pass: true, error: ''}; + + document.getElementsByTagName('iframe')[0].src = 'popup.html'; + document.getElementsByTagName('iframe')[0].onload = () => { + document.getElementsByTagName('iframe')[0].contentWindow.addEventListener( + 'unhandledrejection', event => { + const rejectionEvent = event as PromiseRejectionEvent; + testRes[testCaseIndex] = { + pass: false, + error: rejectionEvent.reason + }; + }); + + document.getElementsByTagName('iframe')[0].contentWindow.onerror = + error => { + testRes[testCaseIndex] = {pass: false, error}; + }; + }; + } + + setTimeout(async () => { + const data = await get<{ + /* tslint:disable-next-line:no-any */ + [key: string]: any + }>(); + + testResData[testCaseIndex] = JSON.stringify(data, null, 2); + + if (testRes[testCaseIndex].pass) { + if (Object.keys(data).length !== + Object.keys(cases[Math.floor(testCaseIndex / 2)].data).length) { + testRes[testCaseIndex] = {pass: false, error: `Missing data`}; + } else { + for (const hash of Object.keys(data)) { + const item = data[hash]; + const keys = [ + 'issuer', 'account', 'secret', 'hash', 'index', 'type', 'counter', + 'encrypted' + ]; + for (const key of keys) { + if (item[key] === undefined) { + testRes[testCaseIndex] = { + pass: false, + error: `Missing key<${key}>: ${JSON.stringify(item)}` + }; + break; + } + } + } + } + } + + showTestResult(); + testCaseIndex++; + + if (document.getElementsByTagName('iframe') && + document.getElementsByTagName('iframe')[0]) { + document.getElementsByTagName('iframe')[0].src = 'about:blank'; + } + + await test(); + }, 1000); +} + +function showTestResult() { + const testResultContainer = document.getElementById('test'); + if (!testResultContainer) { + return; + } + + testResultContainer.innerHTML = ''; + for (let i = 0; i < testRes.length; i++) { + const el = document.createElement('tr'); + el.innerHTML = `[${testRes[i].pass ? 'Pass' : 'Fail'}]`; + el.innerHTML += + `

${cases[Math.floor(i / 2)].name}${ + i % 2 === 1 ? ' (Reopen)' : + ''}

${testRes[i].error}
${
+            testResData[i]}

`; + + testResultContainer.appendChild(el); + } +} + +const startBtn = document.getElementById('start'); +if (startBtn) { + startBtn.onclick = testStart; +} + +window.addEventListener('message', (event) => { + testRes[testCaseIndex] = {pass: false, error: event.data}; +}, false); diff --git a/src/ui/add-account.ts b/src/ui/add-account.ts index 9442406a..1681a9d8 100644 --- a/src/ui/add-account.ts +++ b/src/ui/add-account.ts @@ -24,8 +24,14 @@ async function addAccount(_ui: UI) { addNewAccount: async () => { let type: OTPType; if (!/^[a-z2-7]+=*$/i.test(_ui.instance.newAccount.secret) && - /^[0-9a-f]+$/i.test(_ui.instance.newAccount.secret)) { + /^[0-9a-f]+$/i.test(_ui.instance.newAccount.secret) && + _ui.instance.newAccount.type === 'totp') { type = OTPType.hex; + } else if ( + !/^[a-z2-7]+=*$/i.test(_ui.instance.newAccount.secret) && + /^[0-9a-f]+$/i.test(_ui.instance.newAccount.secret) && + _ui.instance.newAccount.type === 'hotp') { + type = OTPType.hhex; } else { type = _ui.instance.newAccount.type; } diff --git a/src/ui/entry.ts b/src/ui/entry.ts index a26b93d6..0309b4fc 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -26,7 +26,8 @@ async function updateCode(app: any) { if (second < 1) { const entries = app.entries as OTP[]; for (let i = 0; i < entries.length; i++) { - if (entries[i].type !== OTPType.hotp) { + if (entries[i].type !== OTPType.hotp && + entries[i].type !== OTPType.hhex) { entries[i].generate(); } } @@ -137,6 +138,10 @@ function hasMatchedEntry(siteName: Array, entries: OTPEntry[]) { } function isMatchedEntry(siteName: Array, entry: OTPEntry) { + if (!entry.issuer) { + return false; + } + const issuerHostMatches = entry.issuer.split('::'); const issuer = issuerHostMatches[0].replace(/[^0-9a-z]/ig, '').toLowerCase(); @@ -358,7 +363,7 @@ async function entry(_ui: UI) { return; }, nextCode: async (entry: OTPEntry) => { - if (_ui.instance.class.hotpDiabled) { + if (_ui.instance.class.Diabled) { return; } _ui.instance.class.hotpDiabled = true; diff --git a/src/ui/info.ts b/src/ui/info.ts index 9eab6d32..ee454a43 100644 --- a/src/ui/info.ts +++ b/src/ui/info.ts @@ -6,17 +6,12 @@ async function info(_ui: UI) { const ui: UIConfig = { data: { info: '', - dropboxCode: '', - dropboxToken: localStorage.dropboxToken || '' + dropboxToken: localStorage.dropboxToken || '', + dropboxTokenUrl: + 'https://www.dropbox.com/oauth2/authorize?response_type=token&client_id=013qun2m82h9jim&redirect_uri=chrome-extension%3A%2F%2F' + + chrome.runtime.id + '%2Fdropboxtoken.html' }, methods: { - saveDropboxCode: async () => { - const dropbox = new Dropbox(); - _ui.instance.dropboxToken = - await dropbox.getToken(_ui.instance.dropboxCode); - _ui.instance.closeInfo(); - return; - }, showInfo: (tab: string) => { if (tab === 'export' || tab === 'security') { const entries = _ui.instance.entries as OTPEntry[]; diff --git a/src/ui/qr.ts b/src/ui/qr.ts index ff754232..ad21bddf 100644 --- a/src/ui/qr.ts +++ b/src/ui/qr.ts @@ -10,13 +10,16 @@ async function getQrUrl(entry: OTPEntry) { (resolve: (value: string) => void, reject: (reason: Error) => void) => { const label = entry.issuer ? (entry.issuer + ':' + entry.account) : entry.account; - const type = entry.type === OTPType.hex ? OTPType[OTPType.totp] : - OTPType[entry.type]; + const type = entry.type === OTPType.hex ? + OTPType[OTPType.totp] : + (entry.type === OTPType.hhex ? OTPType[OTPType.hotp] : + OTPType[entry.type]); const otpauth = 'otpauth://' + type + '/' + label + '?secret=' + entry.secret + (entry.issuer ? ('&issuer=' + entry.issuer.split('::')[0]) : '') + - ((entry.type === OTPType.hotp) ? ('&counter=' + entry.counter) : - ''); + ((entry.type === OTPType.hotp || entry.type === OTPType.hhex) ? + ('&counter=' + entry.counter) : + ''); /* tslint:disable-next-line:no-unused-expression */ new QRCode( 'qr', { diff --git a/test.html b/test.html new file mode 100644 index 00000000..32ee585b --- /dev/null +++ b/test.html @@ -0,0 +1,28 @@ + + + + + Authenticator Tests + + + +

This page is dangerous, if you do not know what this is close this immediately!

+ + All data will be destroyed!!! Check box to continue +

+ +
+
+
+
+ + + diff --git a/tsconfig.json b/tsconfig.json index dd0840a2..15280d2e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,7 @@ }, "include": [ "src/*.ts", - "src/**/*.ts", - "test/*.ts", - "test/**/*.ts" + "src/**/*.ts" ], "exclude": [ "node_modules"