From 248a4003185b971a14813fba1741e631c1dba2ad Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Fri, 22 Dec 2017 22:19:44 +0800 Subject: [PATCH 001/178] first commit --- README.md | 23 +- _locales/en/messages.json | 174 ++++ _locales/zh_CN/messages.json | 174 ++++ css/DroidSansMono.woff2 | Bin 0 -> 7568 bytes css/content.css | 25 + css/font-awesome.css | 1800 +++++++++++++++++++++++++++++++++ css/fontawesome-webfont.woff2 | Bin 0 -> 56780 bytes css/popup.css | 700 +++++++++++++ images/icon128.png | Bin 0 -> 68456 bytes images/icon16.png | Bin 0 -> 3827 bytes images/icon19.png | Bin 0 -> 4250 bytes images/icon38.png | Bin 0 -> 8601 bytes images/icon48.png | Bin 0 -> 12051 bytes images/scan.gif | Bin 0 -> 32144 bytes manifest.json | 67 ++ package-lock.json | 1438 ++++++++++++++++++++++++++ package.json | 38 + src/.gitignore | 2 + src/interface.ts | 19 + src/opt.ts | 25 + tsconfig.json | 16 + 21 files changed, 4500 insertions(+), 1 deletion(-) create mode 100644 _locales/en/messages.json create mode 100644 _locales/zh_CN/messages.json create mode 100644 css/DroidSansMono.woff2 create mode 100644 css/content.css create mode 100644 css/font-awesome.css create mode 100644 css/fontawesome-webfont.woff2 create mode 100644 css/popup.css create mode 100644 images/icon128.png create mode 100644 images/icon16.png create mode 100644 images/icon19.png create mode 100644 images/icon38.png create mode 100644 images/icon48.png create mode 100644 images/scan.gif create mode 100644 manifest.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/.gitignore create mode 100644 src/interface.ts create mode 100644 src/opt.ts create mode 100644 tsconfig.json diff --git a/README.md b/README.md index abfd92652..7fcb00ebd 100644 --- a/README.md +++ b/README.md @@ -1 +1,22 @@ -# Authenticator2 \ No newline at end of file +# authenticator + +> For Google Authenticator and Battle.net Authenticator. + +## Build Setup + +``` bash +# install dependencies +npm install + +# serve with hot reload at localhost:8080 +npm run dev + +# build for production with minification +npm run build + +# lint all *.js and *.vue files +npm run lint + +# run unit tests +npm test +``` \ No newline at end of file diff --git a/_locales/en/messages.json b/_locales/en/messages.json new file mode 100644 index 000000000..a8af031a3 --- /dev/null +++ b/_locales/en/messages.json @@ -0,0 +1,174 @@ +{ + "extName": { + "message": "Authenticator", + "description": "Extension Name." + }, + "extShortName": { + "message": "Authenticator", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "For Google Authenticator and Battle.net Authenticator.", + "description": "Extension Description." + }, + "added": { + "message": " has been added.", + "description": "Added Account." + }, + "errorqr": { + "message": "Unrecognized QR code.", + "description": "QR Error." + }, + "errorsecret": { + "message": "Secret Error. Only Base32(A-Z, 2-7 and =) and HEX(0-9 and A-F) are supported. However, your secret is: ", + "description": "Secret Error." + }, + "info": { + "message": "

Authenticator for Google™ Authenticator,
© 2014 Sneezry. Released under the Apache License 2.0.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", + "description": "Information." + }, + "add_qr": { + "message": "Scan QR Code", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "Manual Entry", + "description": "Manual Entry." + }, + "close": { + "message": "Close", + "description": "Close." + }, + "ok": { + "message": "OK", + "description": "OK." + }, + "err_acc_sec": { + "message": "Please input Account and Secret.", + "description": "Input Account and Secret." + }, + "account": { + "message": "Account", + "description": "Account." + }, + "secret": { + "message": "Secret", + "description": "Secret." + }, + "updateSuccess": { + "message": "Success.", + "description": "Update Success." + }, + "updateFailure": { + "message": "Failure.", + "description": "Update Failure." + }, + "about": { + "message": "About", + "description": "About." + }, + "export_import": { + "message": "Export / Import", + "description": "Export and Import." + }, + "settings": { + "message": "Settings", + "description": "Settings." + }, + "security": { + "message": "Security", + "description": "Security." + }, + "current_phrase": { + "message": "Current Passphrase", + "description": "Current Passphrase." + }, + "new_phrase": { + "message": "New Passphrase", + "description": "New Passphrase." + }, + "phrase": { + "message": "Passphrase", + "description": "Passphrase." + }, + "confirm_phrase": { + "message": "Confirm Passphrase", + "description": "Confirmm Passphrase." + }, + "security_warning": { + "message": "This passphrase will be used to encrypt your secrets. No one can help you if you forget the passphrase.", + "description": "Passphrase Warning." + }, + "update": { + "message": "Update", + "description": "Update." + }, + "phrase_incorrect": { + "message": "Some accounts and passphrase do not match.", + "description": "Passphrase Incorrect." + }, + "phrase_not_match": { + "message": "Two passphrases do not match.", + "description": "Passphrase Not Match." + }, + "encrypted": { + "message": "Encrypted", + "description": "Encrypted." + }, + "copied": { + "message": "Copied", + "description": "Copied." + }, + "feedback": { + "message": "Feedback", + "description": "Feedback." + }, + "source": { + "message": "Source Code", + "description": "Source Code." + }, + "passphrase_info": { + "message": "Input passphrase to decrypt account data.", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "Sync Clock with Google", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "Remember Passphrase", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "Caution! Your local clock is too far off, please fix it before continuing.", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "Do you have a backup for your secrets? Please note that no one can help you with getting back locked account, don't wait until it's too late. We will remind you to make a backup again after 30 days.", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "Capture failed, please reload the page you are veiwing and try again.", + "description": "Capture Failed" + }, + "unencrypted_secret_warning": { + "message": "This secret is not encrypted! Click here to set a passphrase to fix this issue.", + "description": "Unencrypted Secret Warning" + }, + "based_on_time": { + "message": "Time Based", + "description": "Time Based" + }, + "based_on_counter": { + "message": "Counter Based", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "Resize Popup Page", + "description": "Resize Popup Page" + }, + "scale": { + "message": "Scale", + "description": "Scale" + } +} diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json new file mode 100644 index 000000000..94a2be19f --- /dev/null +++ b/_locales/zh_CN/messages.json @@ -0,0 +1,174 @@ +{ + "extName": { + "message": "身份验证器", + "description": "Extension Name." + }, + "extShortName": { + "message": "身份验证器", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "适用于Google身份验证器及战网安全令。", + "description": "Extension Description." + }, + "added": { + "message": "已添加。", + "description": "Added Account." + }, + "errorqr": { + "message": "无法识别的QR码。", + "description": "QR Error." + }, + "errorsecret": { + "message": "密钥错误,仅支持Base32(A-Z,2-7及=)和HEX(0-9及A-F)格式,然而您的密钥是:", + "description": "Secret Error." + }, + "info": { + "message": "

Authenticator for Google™ Authenticator,
© 2014 Sneezry. Released under the Apache License 2.0.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", + "description": "Information." + }, + "add_qr": { + "message": "扫描QR码", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "手动输入", + "description": "Manual Entry." + }, + "close": { + "message": "关闭", + "description": "Close." + }, + "ok": { + "message": "确定", + "description": "OK." + }, + "err_acc_sec": { + "message": "请输入账户和密钥。", + "description": "Input Account and Secret." + }, + "account": { + "message": "账户", + "description": "Account." + }, + "secret": { + "message": "密钥", + "description": "Secret." + }, + "updateSuccess": { + "message": "成功。", + "description": "Update Success." + }, + "updateFailure": { + "message": "失败。", + "description": "Update Failure." + }, + "about": { + "message": "关于", + "description": "About." + }, + "export_import": { + "message": "导出 / 导入", + "description": "Export and Import." + }, + "settings": { + "message": "设置", + "description": "Settings." + }, + "security": { + "message": "安全", + "description": "Security." + }, + "current_phrase": { + "message": "当前密码", + "description": "Current Phrase." + }, + "new_phrase": { + "message": "新密码", + "description": "New Phrase." + }, + "phrase": { + "message": "密码", + "description": "Phrase." + }, + "confirm_phrase": { + "message": "确认密码", + "description": "Confirmm Phrase." + }, + "security_warning": { + "message": "您的密钥将使用此密码进行加密。如果您忘记了密码没有人能够提供帮助。", + "description": "Phrase Warning." + }, + "update": { + "message": "更新", + "description": "Update." + }, + "phrase_incorrect": { + "message": "部分账户与密码不匹配。", + "description": "Phrase Incorrect." + }, + "phrase_not_match": { + "message": "两次密码不一致。", + "description": "Phrase Not Match." + }, + "encrypted": { + "message": "已加密", + "description": "Encrypted." + }, + "copied": { + "message": "已复制", + "description": "Copied." + }, + "feedback": { + "message": "问题反馈", + "description": "Feedback." + }, + "source": { + "message": "源代码", + "description": "Source Code." + }, + "passphrase_info": { + "message": "输入密码以解码账户数据。", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "通过Google校准时间", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "记住密码", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "注意!您的本地时钟时间差过大,请修正后再进行操作。", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "捕捉失败,请重载您正在浏览的页面后重试。", + "description": "Capture Failed" + }, + "unencrypted_secret_warning": { + "message": "此密钥未被加密!点击此处来设置一个密码以解决此问题。", + "description": "Unencrypted Secret Warning" + }, + "based_on_time": { + "message": "基于时间", + "description": "Time Based" + }, + "based_on_counter": { + "message": "基于计数器", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "调整弹出页面尺寸", + "description": "Resize Popup Page" + }, + "scale": { + "message": "比例", + "description": "Scale" + } +} diff --git a/css/DroidSansMono.woff2 b/css/DroidSansMono.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..ca4b22690ab798a5e7ed414e8ac17fd0c0d8f5d0 GIT binary patch literal 7568 zcmV;B9dF`yPew8T0RR9103DD33jhEB06HK503AL60RR9100000000000000000000 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 li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .3em; +} +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -ms-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -ms-transform: scale(1, -1); + transform: scale(1, -1); +} +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + filter: none; +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook-f:before, +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before, +.fa-gratipay:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-genderless:before, +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: "\f1e3"; +} +.fa-tty:before { + content: "\f1e4"; +} +.fa-binoculars:before { + content: "\f1e5"; +} +.fa-plug:before { + content: "\f1e6"; +} +.fa-slideshare:before { + content: "\f1e7"; +} +.fa-twitch:before { + content: "\f1e8"; +} +.fa-yelp:before { + content: "\f1e9"; +} +.fa-newspaper-o:before { + content: "\f1ea"; +} +.fa-wifi:before { + content: "\f1eb"; +} +.fa-calculator:before { + content: "\f1ec"; +} +.fa-paypal:before { + content: "\f1ed"; +} +.fa-google-wallet:before { + content: "\f1ee"; +} +.fa-cc-visa:before { + content: "\f1f0"; +} +.fa-cc-mastercard:before { + content: "\f1f1"; +} +.fa-cc-discover:before { + content: "\f1f2"; +} +.fa-cc-amex:before { + content: "\f1f3"; +} +.fa-cc-paypal:before { + content: "\f1f4"; +} +.fa-cc-stripe:before { + content: "\f1f5"; +} +.fa-bell-slash:before { + content: "\f1f6"; +} +.fa-bell-slash-o:before { + content: "\f1f7"; +} +.fa-trash:before { + content: "\f1f8"; +} +.fa-copyright:before { + content: "\f1f9"; +} +.fa-at:before { + content: "\f1fa"; +} +.fa-eyedropper:before { + content: "\f1fb"; +} +.fa-paint-brush:before { + content: "\f1fc"; +} +.fa-birthday-cake:before { + content: "\f1fd"; +} +.fa-area-chart:before { + content: "\f1fe"; +} +.fa-pie-chart:before { + content: "\f200"; +} +.fa-line-chart:before { + content: "\f201"; +} +.fa-lastfm:before { + content: "\f202"; +} +.fa-lastfm-square:before { + content: "\f203"; +} +.fa-toggle-off:before { + content: "\f204"; +} +.fa-toggle-on:before { + content: "\f205"; +} +.fa-bicycle:before { + content: "\f206"; +} +.fa-bus:before { + content: "\f207"; +} +.fa-ioxhost:before { + content: "\f208"; +} +.fa-angellist:before { + content: "\f209"; +} +.fa-cc:before { + content: "\f20a"; +} +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: "\f20b"; +} +.fa-meanpath:before { + content: "\f20c"; +} +.fa-buysellads:before { + content: "\f20d"; +} +.fa-connectdevelop:before { + content: "\f20e"; +} +.fa-dashcube:before { + content: "\f210"; +} +.fa-forumbee:before { + content: "\f211"; +} +.fa-leanpub:before { + content: "\f212"; +} +.fa-sellsy:before { + content: "\f213"; +} +.fa-shirtsinbulk:before { + content: "\f214"; +} +.fa-simplybuilt:before { + content: "\f215"; +} +.fa-skyatlas:before { + content: "\f216"; +} +.fa-cart-plus:before { + content: "\f217"; +} +.fa-cart-arrow-down:before { + content: "\f218"; +} +.fa-diamond:before { + content: "\f219"; +} +.fa-ship:before { + content: "\f21a"; +} +.fa-user-secret:before { + content: "\f21b"; +} +.fa-motorcycle:before { + content: "\f21c"; +} +.fa-street-view:before { + content: "\f21d"; +} +.fa-heartbeat:before { + content: "\f21e"; +} +.fa-venus:before { + content: "\f221"; +} +.fa-mars:before { + content: "\f222"; +} +.fa-mercury:before { + content: "\f223"; +} +.fa-transgender:before { + content: "\f224"; +} +.fa-transgender-alt:before { + content: "\f225"; +} +.fa-venus-double:before { + content: "\f226"; +} +.fa-mars-double:before { + content: "\f227"; +} +.fa-venus-mars:before { + content: "\f228"; +} +.fa-mars-stroke:before { + content: "\f229"; +} +.fa-mars-stroke-v:before { + content: "\f22a"; +} +.fa-mars-stroke-h:before { + content: "\f22b"; +} +.fa-neuter:before { + content: "\f22c"; +} +.fa-facebook-official:before { + content: "\f230"; +} +.fa-pinterest-p:before { + content: "\f231"; +} +.fa-whatsapp:before { + content: "\f232"; +} +.fa-server:before { + content: "\f233"; +} +.fa-user-plus:before { + content: "\f234"; +} +.fa-user-times:before { + content: "\f235"; +} +.fa-hotel:before, +.fa-bed:before { + content: "\f236"; +} +.fa-viacoin:before { + content: "\f237"; +} +.fa-train:before { + content: "\f238"; +} +.fa-subway:before { + content: "\f239"; +} +.fa-medium:before { + content: "\f23a"; +} diff --git a/css/fontawesome-webfont.woff2 b/css/fontawesome-webfont.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..3311d585145b1cc1b9581e914acbb32d8542b4f5 GIT binary patch literal 56780 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 literal 0 HcmV?d00001 diff --git a/css/popup.css b/css/popup.css new file mode 100644 index 000000000..440ad0199 --- /dev/null +++ b/css/popup.css @@ -0,0 +1,700 @@ +@font-face { + font-family: 'Droid Sans Mono'; + font-style: normal; + font-weight: 400; + src: url(DroidSansMono.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; +} + +@-webkit-keyframes twinkling{ + 0%{ + color:#DD4B39; + } + 100%{ + color:#EEA59C; + } +} + +@-webkit-keyframes fadeshow{ + 0%{ + opacity:0; + } + 100%{ + opacity:1; + } +} + +@-webkit-keyframes fadehide{ + 0%{ + opacity:1; + } + 100%{ + opacity:0; + } +} + +@-webkit-keyframes fadein{ + 0%{ + opacity:0; + top:110px; + } + 100%{ + opacity:1; + top:10px; + } +} + +@-webkit-keyframes fadeout{ + 0%{ + opacity:1; + top:10px; + } + 100%{ + opacity:0; + top:110px; + } +} + +@-webkit-keyframes slidein{ + 0%{ + opacity:0; + left:-55px; + } + 100%{ + opacity:1; + left:0; + } +} + +@-webkit-keyframes slideout{ + 0%{ + opacity:1; + left:0; + } + 100%{ + opacity:0; + left:-55px; + } +} + +@-webkit-keyframes qrfadein{ + 0%{ + opacity:0; + } + 100%{ + opacity:1; + } +} + +@-webkit-keyframes qrfadeout{ + 0%{ + opacity:1; + } + 100%{ + opacity:0; + } +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + width: 320px; + height: 480px; + overflow: hidden; + font-family: arial, 'Microsoft YaHei'; + cursor: default; + -webkit-user-select: none; + transform-origin: left top; +} + +#header, +#menuHead { + height: 38px; + line-height: 38px; + position: relative; + text-align: center; + font-size: 16px; + border-bottom: #CCC 1px solid; +} + +#notification { + position: absolute; + left: 100px; + top: -1000px; + width: 120px; + height: 60px; + line-height: 60px; + text-align: center; + background: rgba(0,0,0,0.5); + color: #FFF; + font-size: 20px; + border-radius: 2px; +} + +#notification.fadein { + top: 190px; + -webkit-animation: fadeshow 0.2s 1 ease-out; +} + +#notification.fadeout { + top: 190px; + -webkit-animation: fadehide 0.2s 1 ease-in; +} + +#codes { + height: 442px; + overflow-x: hidden; + overflow-y: hidden; + background: #EEE; + padding-right:10px; +} + +#codes:hover { + padding-right: 0; + overflow-y: scroll; +} + +#codes::-webkit-scrollbar { + width: 10px; + background: #EEE; +} + +#codes::-webkit-scrollbar-thumb { + background-color: #AAA; + border: 2px solid #EEE; + border-radius: 5px; +} + +#codeClipboard { + position: absolute; + top: -1000px; +} + +.codeBox { + margin: 10px; + margin-right: 0; + padding: 10px; + border: #CCC 1px solid; + background: white; + border-radius: 2px; + position: relative; +} + +.codeBox[unencrypted="true"] .warning { + position: absolute; + height: 0; + line-height: 12px; + font-size: 12px; + padding: 0 10px; + margin: 0 4px; + width: 250px; + bottom: 4px; + left: 0; + background: #EC6959; + color: #FFF; + cursor: pointer; + overflow: hidden; + border-radius: 2px; + -webkit-transition: height 0.2s; +} + +#codes:not(.edit) .codeBox[unencrypted="true"]:hover .warning { + height: 24px; +} + +.codeBox[dropOver="true"] { + border: gray 1px dashed; +} + +.issuer { + font-size: 12px; + color: black; + width: 80%; + text-overflow: ellipsis; + overflow: hidden; +} + +.code { + font-size: 36px; + color: #08C; + width: 80%; + -webkit-user-select: text; + font-family: 'Droid Sans Mono'; + cursor: pointer; +} + +#codes.edit .code { + color: #CCC!important; + -webkit-user-select: none; + cursor: default; +} + +#codes.edit .account, +#codes.edit .issuer { + display: none; +} + +.accountEdit, +.issuerEdit { + display: none; +} + +.accountEdit input, +.issuerEdit input { + border: none; + height: 14px; + width: 70%; + font-size: 12px; + font-family: arial, 'Microsoft YaHei'; + outline: none; + background: #eee; +} + +#codes.edit .accountEdit, +#codes.edit .issuerEdit { + display: block; +} + +#codes.timeout .code:not(.hotp) { + -webkit-animation: twinkling 1s infinite ease-in-out; +} + +.hotp { + color: #555; + cursor: default; +} + +.hotp[hasCode="true"] { + color: #08C; + cursor: pointer; +} + +.movehandle { + height: 98px; + line-height: 98px; + right: 10px; + top: 0; + position: absolute; + font-size: 24px; + color: #CCC; + cursor: move; + display: none; +} + +#codes.edit .movehandle { + display: block; +} + +.showqr { + right: 10px; + top: 10px; + position: absolute; + font-size: 20px; + color: #CCC; + cursor: pointer; + opacity: 0; +} + +.showqr:hover { + opacity: 1; +} + +#codes.edit .showqr, +.showqr.hidden { + display: none; +} + +.account { + font-size: 12px; + color: gray; + width: 80%; + text-overflow: ellipsis; + overflow: hidden; +} + +#add, +#add_qr, +#add_secret, +#add_button, +#security_save, +#passphrase_ok, +#message_close, +#exportButton, +#resize_save { + margin: 10px; + padding: 20px; + border: #CCC 1px solid; + background: white; + border-radius: 2px; + position: relative; + text-align: center; + font-size: 16px; + color: gray; + cursor: pointer; +} + +#add { + margin-right: 0; +} + +#message_close, +#add_button, +#exportButton, +#security_save, +#passphrase_ok, +#resize_save { + font-size: 12px; + margin: 20px 100px; + padding: 10px; + cursor: pointer; +} + +#codes #add { + font-size: 16px; + line-height: 56px; + display: none; +} + +#codes.edit #add { + display: block; +} + +#codes .deleteAction { + font-size: 20px; + color: #DD4B39; + position: absolute; + top: -10px; + left: -10px; + z-index: 10; + display: none; +} + +#codes.edit .deleteAction { + display: block; + cursor: pointer; +} + +#infoAction { + position: absolute; + left: 20px; + bottom: 0; + height: 38px; + line-height: 38px; + font-size: 16px; + color: gray; + cursor: pointer; +} + +#infoAction.hidden { + display: none; +} + +#editAction { + position: absolute; + right: 20px; + bottom: 0; + height: 38px; + line-height: 38px; + font-size: 16px; + color: gray; + cursor: pointer; +} + +.counter { + color: #888; + font-size: 18px; + text-align: center; + cursor: pointer; +} + +.counter:not([disabled="true"]):hover { + color: #000; +} + +.counter[disabled="true"] { + color: #CCC; + cursor: default; +} + +.sector, +.counter { + width: 20px; + height: 20px; + position: absolute; + right: 10px; + bottom: 10px; +} + +#codes.edit .sector, +#codes.edit .counter { + display: none; +} + +#menu { + width: 320px; + height: 480px; + position: absolute; + left: -1000px; + background: #EEE; + top: 0; +} + +#menu.slidein { + left: 0; + -webkit-animation: slidein 0.2s 1 ease-out; +} + +#menu.slideout { + left: -55px; + -webkit-animation: slideout 0.2s 1 ease-in; +} + +#menuHead { + background: #FFF; +} + +#menu .menuList { + margin: 10px; + border: #CCC 1px solid; + border-radius: 2px; + background: #FFF; +} + +#menu .menuList p { + position: relative; + border-bottom: #CCC 1px solid; + padding: 10px; + font-size: 16px; + color: gray; + cursor: pointer; +} + +#menu .menuList p:hover { + background: #F4FCFF; + color: black; +} + +#menu .menuList p:hover:after { + color: black; +} + +#menu .menuList p:last-child { + border-bottom: none; +} + +#menu .menuList p a { + color: gray; + text-decoration: none; + display: line-block; +} + +#menu .menuList p i.fa { + font-size: 14px; + display: line-block; + width: 30px; +} + +#version { + text-align: center; + color: gray; + margin: 10px; +} + +#info, +#addAccount, +#security, +#passphrase, +#export, +#resize { + position: absolute; + height: 460px; + width: 300px; + padding: 10px; + border: gray; + background: white; + left: 10px; + top: -1000px; + box-shadow: 1px 1px 3px gray; + z-index: 100; +} + +#info.fadein, +#addAccount.fadein, +#security.fadein, +#passphrase.fadein, +#export.fadein, +#resize.fadein { + top: 10px; + -webkit-animation: fadein 0.2s 1 ease-out; +} + +#info.fadeout, +#addAccount.fadeout, +#security.fadeout, +#passphrase.fadeout, +#export.fadeout, +#resize.fadeout { + top: 110px; + -webkit-animation: fadeout 0.2s 1 ease-in; +} + +#infoClose, +#addAccountClose, +#securityClose, +#passphraseClose, +#exportClose, +#resizeClose { + height: 20px; + width: 20px; + font-size: 14px; + color: gray; + cursor: pointer; +} + +#menuClose { + position: absolute; + height: 38px; + line-height: 38px; + left: 20px; + font-size: 16px; + color: gray; + bottom: 0; + cursor: pointer; +} + +#menuClose:hover, +#exportButton:hover, +#message_close:hover, +#add_button:hover, +#add_secret:hover, +#add_qr:hover, +#editAction:hover, +#infoAction:hover, +#codes #add:hover, +#infoClose:hover, +#addAccountClose:hover, +#securityClose:hover, +#passphraseClose:hover, +#security_save:hover, +#passphrase_ok:hover, +#export:hover, +#resizeClose:hover, +#resize_save:hover { + color: black; +} + +#infoContent, +#addAccountContent, +#exportContent { + height: 420px; + overflow-y: auto; + overflow-x: hidden; +} + +#exportData { + height: 330px; + width: 100%; + word-break: break-all; + resize: none; + outline: none; +} + +#infoContent p { + font-size: 12px; + margin-bottom: 20px; +} + +#infoContent a { + color: #4183c4; +} + +#qr { + width: 100%; + height: 100%; + top: -1000px; + left: 0; + position: absolute; + z-index: 10; + background-color: rgba(0, 0, 0, 0.5); + background-repeat: no-repeat; + background-position: center; +} + +#qr canvas { + display: none; +} + +#qr.qrfadein { + top: 0; + -webkit-animation: qrfadein 0.2s 1 ease-out; +} + +#qr.qrfadeout { + top: 0; + -webkit-animation: qrfadeout 0.2s 1 ease-in; +} + +#secret_box { + display: none; +} + +#secret_box input, +#security input, +#passphrase input { + display: block; + margin: 0 10px 10px 10px; + padding: 10px; + width: 260px; + border: #CCC 1px solid; + background: white; + outline: none; +} + +.checkbox_group input[type="checkbox"], +.radio_group input[type="radio"] { + display: inline-block !important; + width: auto !important; +} + +.checkbox_group label, +.radio_group label { + display: inline-block !important; + margin-left: 0 !important; +} + +#secret_box label, +#security label, +#passphrase label, +#security_warning, +#passphrase_info { + display: block; + margin: 10px 0 0 10px; +} + +#security_warning, +#passphrase_info { + color: gray; +} + +#resize_list_label, +#resize_list { + margin: 20px; + font-size: 16px; +} + +#message { + position: absolute; + width: 300px; + padding: 10px; + border: gray; + background: white; + left: 10px; + top: 150px; + box-shadow: 1px 1px 3px gray; + display: none; + z-index: 1000; +} \ No newline at end of file diff --git a/images/icon128.png b/images/icon128.png new file mode 100644 index 0000000000000000000000000000000000000000..6a4a18e36642677740fd8232c11b2924031a07ff GIT binary patch literal 68456 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 Ls`2uMufF$xwX823 literal 0 HcmV?d00001 diff --git a/images/icon16.png b/images/icon16.png new file mode 100644 index 0000000000000000000000000000000000000000..fe84e4249e58ed6f3d59d791b7fed0feefc24e54 GIT binary patch literal 3827 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

I5byi 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 H=FYzW^|)OI literal 0 HcmV?d00001 diff --git a/images/icon38.png b/images/icon38.png new file mode 100644 index 0000000000000000000000000000000000000000..7d5174b4b3a329173758253db66a14b67240f117 GIT binary patch literal 8601 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 literal 0 HcmV?d00001 diff --git a/images/icon48.png b/images/icon48.png new file mode 100644 index 0000000000000000000000000000000000000000..059a7ccc2bf22368bfeea6fd0dfb3c600a131539 GIT binary patch literal 12051 zcmdTq2|Sfq`{!P}>}#lOF{!jDeT^b%p;APac2ZP`DTx+oZ)yrjvL-5&nrbQ%(IP?& zMY2SQWVx2?+jpLG@5^PF<@!Bw?b+_+tgvj+A_eE& zPPRK7wjtzJ7-G85(zjhor?h>Wu(@7A<2`?eBS=X(hBr-QZMet5>LzoC+dU8PDywQV?q7aCh@?xx1i;_OlbBBj>zn z6U9Ci_Xzdz$E>NbloS&%It{ARC@Ar6c(eaG>tR)4$X7zBMp7JXCfDJ zJ#XnPtZRil1N5e5B89nhv}l6(8x93Sj<6ZINoCQSolnFW&`@%Q-y8oGxw9nV{C<_) z*W6fEu2}xu;hvtgw3>)5U!_v8e~_}cK{bbAmH_IQ}tX>Q%N}O`K72R;Pc~C#SNr9&LEEuV{ZG`XNv^;C6DvvECo7 zce&m%IbWT@sW)?1d$n?3>WRa>=hbq`XW!c}Rq{-e<&Sa3N-I;{ADj3rR($POE>?3` zqe=c!FQ4x|K@@Y+D0$T*o`)F@$sP?f`dJ~%Tvw#)(W3s7>=+*;VmDjEy&WOnq)sp0 zDKzxP|5^n?317x;h`K0k_=*>y#bHM#-(AQf@Ju2mSt$6~_=IF>x;6J=U4ehO*qNNxiF1a z+*(oVwCI-e1xi`G(gEi=m8=bBU+}!9P;**hwNgsj?rZdDy`4TA4V0VJcT0SKZi8-@ zcTt*x#+jZo3HmcH9=X4o;~jmk?&BzV*)y-g_T;$C7T)N2Dcn9s_Q3>g&qv{rIV-;G zbwP*DoVW4JnJHxZ(+BGfKb=})^`oV%YO0jYXG=|UFHM{Amj7~a#Nv4^_>LW)`TRR2-zcL8D*MGcU%GA4y60`VG)A>S zae`u~%BJsx#5emIOcZ*3p>h?!qKdc1vN6l-4T=oZubE!c$rafVY^Yl6?fCj*CGUOD z7zKZySZ`r(qYK@yUsT3QXr-JkH{=VSXmahM*mBKn3qEsRzoMZR5O#C*o9NJYyf;s4 zU0JuxG0fF?y6LHC@!5L=Ro(60$zr{!nv=iyg@6j$B!TTpqEDbwp5V6K4_La;zMX6UB+nOx(@^_na)K0a(ymHI> z`<-=$u3X)6uG0?~nl9RN(>BJ|$2O$uhc`5#SNF;qzyA=tJWDZ4FzbLihx!8bd-*$W z+uruL{jb9Pg-;8&)bFmBs;@0%>~#*&4VfC!s^zHFzLzh4PW*}QYSWgKSCwQ0=sw)tXzc}HAM&}0l7i=GD{q#dZQO~&(*OvsREI&KvZ0e=LO9wN!F0H)O9&_1T z$K2h#)%<ezt<`i_;mo?8 zI{v!AI_~=K+pODE+8o=m$7hZc8=pVVW!}NvE_*Xw9h?(tuYH>RBKl?Yiy8|%IlHTS zLRvJXZ>;dn5c7BSmo{~bJQKO=%Ki+$&$12ixewpAMg%DA(26m&s)5!r*CrIw02W>;sm2#f>lHFXE>%6yg<0(^pN<#+o!B}+HUb=u4-wwoH5Bbm+{ zBDqekYeuH3;e>qA^Cz=;OGP}KZgd*fWIp0OU8e7FI!=CvOn`LfAqlm~8aJnXUH!n+ z<>SDQ2+j7@WtQ=6Tuyq9!z;ZBX&}(k?oWzzJitRRgUR$ zzNgaLE0sIg z?;6M5{iPtzZEaJTQqqzqQp#sdrT0ynxzQwktJ{vo+(Q}LGwxsNH7>U_F>BuGUfg`% zx-rwwV3S}eere1t~=k>J+_gw_SGib+wbyEQ#k8+Y`gwWc&V_luo@^AnE!TAJv)pFK_6o>2EkRtu^b{XFiH4YQ8!rqHTZ3;f)#Qy|-`0KK-Ctn4eU#??8*tvGJ1gJ~A%W zB|MzsW?`SW$NcBhMzL-Ef^N%#32 zP3D}NV4&svImpwgFsMFo-$UnhlZBoy{QT^@(i;a4R2YA`n)Rai#hcQA!?CTrwFd4} zaRS=HH8M10 z)dKBz0mbg^hCKoH8&_{kv|E%NxiCuA9`;NB0 zhjZ^IgP+$s)oyFgsJEDZ$K~6~+IcyQw}-oSCg<&W-|>Fh#w+fLj8}DXvAa}D#>J_` zw|2O;)l2W=Z!j$0xZlg|=bop9yLrh<)u$x4Lwt03fg457|`{H{T0FD^^JS|Ox$86jz>3!77x zA~aEV+2Vz3+-PXV#Oxwke?Lb39|lg~Ju522F)?vj#{#F&_v;I_85PRt_$xOmT5>d{DpulBlYxKKiFK54>`5aS2#iSv{AQmLA{G(7+TxOiavDSXfvS z5dIf{4}+$rrVBSWHzF~BO|f_+#=m^|LSh5E*GR*o_CYL3Nl77IUVNnZ6VTk;j6j=M zyF;*vkJD&q?AWp3;zPzJOqj3&$7=);081h^WR&%B=+GflP*8xdg+>}4u@9IBW_*2p z(X3gJApaQd-@lK}ojZpxQ$vv3+uIRVHVO?5Md0u3fR!VT`IaOCun(}eX;^xY9GLq0 z^(z^IzL5ZMU=dhwIDjVO;^NS^Z{HBkCI|;2TC!vb5)~CCsL)?gQGviE0Du-`nc-R| zgRGyvzCKL0zyEgy0D{1T9GiMHIw%EP#l@(yvJ&BRPSzbDHrRj-?Dqu#+?JLWB7ld7 z$KcdaTHfB?=-s<_h@T&>c!q+ktSrfPFa{b2#wgI!(?jsE!^x8;QBF<{S)09oczaMy z!$d%61tq|6;v;=ij8lS)gadt$G$5uRwjiiSgTE|*kdP3PmzPI!a&jcN zz!HB4n3|d*T-uNn4uL-!{AB?e8yitkQ4xa70v3R*^LK!bjt+w7kKiv00O|Db;lm_5 zKsuZ>Y0@99@rUD(G9NyCh;TVWvL3kSiWMvVa2_E1WdUZ)n1SFK3I52hN16kDA3l6Q zr%s(Bu31%8h4l6H|2_dmW8(kPKW5Arq^zt=;tJ{~TxtGkax4olb1tM_8iF5Yxd0hM zP*4yt7z_k;5oma{ofrx+hzVR^q1M(`ylD;c0CYgJFi!2nU7dqxC44|3yEM?QuC9jC zo^3KT`%yn$o5dCW9xP{w0E3HRM7Ci_k#JJ0udgRX9WX-2ga~gG4UiYl>0zsdV5jZv z?ZLfFpws9GvI;1Ihnoj=bao&yw(O;-sEF+B>_}+=aWzB$U>?ZewaFVdS^6c!e>Vmo zsKi9Ykcg--l8|JkOQs%x6cz-Sr{SM|+$L{CEY#Q6J5U;f&@?&?ZQZ(+EJ*!8KRf_x zlhn}gQiU3$@JD-(DgY2Te*8FL=&uH6e_uc1%@qK}z6ySTL3|1M^G)SPT;(FgEo$*kBirkYfojfJr)m z8ORJ!8WVtF%#XUdIytbg3j(+h=x=l1;y~^pfxLS zZ9`#UVMJgG7brsp1A&3|jqBG@dPW9HPfsU1`=(8shO9rp?4b7ybU}lnJE8>ufsT)o zVsP(*7X@Tkc(fxPjKR4X_5!FL`FQ!rsS4;&z=2aKWTRKFUXhGbS67GNkORfwNCzNl z9-H?JbWsh^HxYo6hI#`+A9jNloRcX(1tzJo7W&|u0{DiL8{}{}X@Ny3T#6=*gNEQN zVQ+6wHan;^0SAgh3MgFWH@oWD+Ry~R%;RN84q_MwZ7M4(;TCfSFbro^09d%Ehq+Nv z^}3>>B4HeGIyyRr92udnxw!?MKK&!H6lk3ikPsa4XV9J5qIs}M*uv*$m`kBD-~EI+ zxQhu43>r$Bg_xL_h5dfGb z4hO%ib)hdgDG8UFi1Z8h8SPdgE(FSM| z=F4yHOQF1m1I_E#uLqI@Sp@&Ux)6(wj=M+~D2J*pD4YQc0OL$Ss3C-;@WBG^E2)D! z6ltAZo#Yz`QvmZiaeWUOhK-CJAQt%f_>qjP3@Z3gfZ$SqeH|DF1;E1&;L4yBxVC{Y z5`VxW3yGq^vH(-_%s^qPk3s-^%JlH2Zk&O5cXt;KT#Sz$6Z-3MMtX^JJ>bE7cx!8G zle4a_t}c4~`0)_t*%=<*hWVhB1-c8!P;U?OYGp8(nKM`x0J$jfC>fauzywGSnFrqB zG7wb2FzlerT8BBO2H)j)dU}#Q1O$XlEjl`y2(WbNQuO4>6O@pUKrRAcd^CVy2De(c za3M0#Hz1Z6&NwxfDFE<}B?dO|-<-%8H30$@j)QPwq5v*kH*DBI;sOGXvKFLWxG#ct zFBTF~C&X?!{ZfXY))Pyhu41R%HshU<1X zvO-KiTJG-Y#-&ROu`C=vp^yaMhj~mlr4&1KRbl}^AQoV0pO_c7A$>vs55E~xQBgt4 z6DJZ%?DN2_X5paYU^U8xRyKUjW!=z?G;u6fNov!hZbtvEk99N9%|MU^9ZhX~A^r z=m(O*MuMqO^9J5P7_0f84-e1*VK}kiP&YvzlOB@-v|)9Y zgR_$btI_aUG$H`L25Ez>9!^GJPN+R#VmBuE`#=fD@=v@VXzn*ap$!`V1_6J>IV{}a z%!Bs9@;Em8umYHWp}~KjN)ZqgSPvQmOMqLjYl3}UAUFy?>}db>HmnJ2Ls9{c!KF+I zcF&K*0yyVhRZvhk+}+hJ4VO;>IQq#1;vWBXUj8G3;BF94@IbuaRDKmFgf5Rk5Dv%^nQ&Vr_bM*zNhKT@>CUM6zyi-oZa2DUlEy8ZZ zhlPM3819CD18cue5^En)9Ub4BR^sgAhyQwW9(X260H%V!!={0&zY_dES4?nQ9Sb0h u`~NWlU|#z1S^ERdRMnt)U>^9!uHN4-_bf0`Ju023VcC*Zi|^=J9sM_0Wap0n literal 0 HcmV?d00001 diff --git a/images/scan.gif b/images/scan.gif new file mode 100644 index 0000000000000000000000000000000000000000..ae5698b01831ef0977561039833367d4f072103c GIT binary patch literal 32144 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"], + "css": ["css/content.css"], + "js": ["javascript/content.js"] + } + ], + "permissions": [ + "activeTab", + "storage" + ], + "optional_permissions": [ + "clipboardWrite", + "https://www.google.com/" + ], + "offline_enabled": true, + "web_accessible_resources": [ + "qr.html", + "images/scan.gif" + ], + "content_security_policy": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/; default-src 'none'" +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..143fdf42b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1438 @@ +{ + "name": "authenticator", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@types/chrome": { + "version": "0.0.56", + "resolved": "http://registry.npm.taobao.org/@types/chrome/download/@types/chrome-0.0.56.tgz", + "integrity": "sha1-Dch9wkf3uens0B8GoIBCLTXjQyI=", + "dev": true, + "requires": { + "@types/filesystem": "0.0.28" + } + }, + "@types/crypto-js": { + "version": "3.1.38", + "resolved": "http://registry.npm.taobao.org/@types/crypto-js/download/@types/crypto-js-3.1.38.tgz", + "integrity": "sha1-4TZ/dz7d4phrqa6+478c3z6Bil0=", + "dev": true + }, + "@types/filesystem": { + "version": "0.0.28", + "resolved": "http://registry.npm.taobao.org/@types/filesystem/download/@types/filesystem-0.0.28.tgz", + "integrity": "sha1-P9dzWDDyx0E8taxFeAvEWQRpew4=", + "dev": true, + "requires": { + "@types/filewriter": "0.0.28" + } + }, + "@types/filewriter": { + "version": "0.0.28", + "resolved": "http://registry.npm.taobao.org/@types/filewriter/download/@types/filewriter-0.0.28.tgz", + "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=", + "dev": true + }, + "@types/otplib": { + "version": "7.0.0", + "resolved": "http://registry.npm.taobao.org/@types/otplib/download/@types/otplib-7.0.0.tgz", + "integrity": "sha1-vGCMh3HLoPRBdHju957z95xH6fY=", + "dev": true + }, + "@types/vue": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/@types/vue/download/@types/vue-2.0.0.tgz", + "integrity": "sha1-7Hez2JWR3rnKXLBSNoqpwyvgiOc=", + "dev": true, + "requires": { + "vue": "2.5.13" + } + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/ansi-align/download/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "2.1.1" + } + }, + "ansi-escapes": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/ansi-escapes/download/ansi-escapes-3.0.0.tgz", + "integrity": "sha1-7D6LTp+AZPwCw6ybZfHCdb2o75I=", + "dev": true + }, + "ansi-regex": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "ansi-styles": { + "version": "3.2.0", + "resolved": "http://registry.npm.taobao.org/ansi-styles/download/ansi-styles-3.2.0.tgz", + "integrity": "sha1-wVm41b4PnlpvNG2rlPFs4CIWG4g=", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/array-find-index/download/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/arrify/download/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "http://registry.npm.taobao.org/async/download/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", + "dev": true + }, + "babel-code-frame": { + "version": "6.26.0", + "resolved": "http://registry.npm.taobao.org/babel-code-frame/download/babel-code-frame-6.26.0.tgz", + "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=", + "dev": true, + "requires": { + "chalk": "1.1.3", + "esutils": "2.0.2", + "js-tokens": "3.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "http://registry.npm.taobao.org/ansi-styles/download/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "http://registry.npm.taobao.org/chalk/download/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "boxen": { + "version": "1.3.0", + "resolved": "http://registry.npm.taobao.org/boxen/download/boxen-1.3.0.tgz", + "integrity": "sha1-VcbDmouljZxhrSLNh3Uy3rZlogs=", + "dev": true, + "requires": { + "ansi-align": "2.0.0", + "camelcase": "4.1.0", + "chalk": "2.3.0", + "cli-boxes": "1.0.0", + "string-width": "2.1.1", + "term-size": "1.2.0", + "widest-line": "2.0.0" + } + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "http://registry.npm.taobao.org/brace-expansion/download/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "http://registry.npm.taobao.org/builtin-modules/download/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "http://registry.npm.taobao.org/camelcase/download/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "http://registry.npm.taobao.org/camelcase-keys/download/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "dev": true, + "requires": { + "camelcase": "4.1.0", + "map-obj": "2.0.0", + "quick-lru": "1.1.0" + } + }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/capture-stack-trace/download/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "dev": true + }, + "chalk": { + "version": "2.3.0", + "resolved": "http://registry.npm.taobao.org/chalk/download/chalk-2.3.0.tgz", + "integrity": "sha1-tepI78nBeT3MybR2fJORTT8tUro=", + "dev": true, + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "http://registry.npm.taobao.org/chardet/download/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true + }, + "clang-format": { + "version": "1.2.1", + "resolved": "http://registry.npm.taobao.org/clang-format/download/clang-format-1.2.1.tgz", + "integrity": "sha1-UYVk1fC28Fdm57JB4CAbMgnijqw=", + "dev": true, + "requires": { + "async": "1.5.2", + "glob": "7.1.2", + "resolve": "1.5.0" + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/cli-boxes/download/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/cli-cursor/download/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "http://registry.npm.taobao.org/cli-width/download/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "color-convert": { + "version": "1.9.1", + "resolved": "http://registry.npm.taobao.org/color-convert/download/color-convert-1.9.1.tgz", + "integrity": "sha1-wSYRB66y8pTr/+ye2eytUppgl+0=", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "http://registry.npm.taobao.org/color-name/download/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.12.2", + "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.12.2.tgz", + "integrity": "sha1-D1lGxCftnsDZGka7ne9T5UZQ5VU=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "http://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "configstore": { + "version": "3.1.1", + "resolved": "http://registry.npm.taobao.org/configstore/download/configstore-3.1.1.tgz", + "integrity": "sha1-CU7mYquD+tmRdnjeEU+q6o/NypA=", + "dev": true, + "requires": { + "dot-prop": "4.2.0", + "graceful-fs": "4.1.11", + "make-dir": "1.1.0", + "unique-string": "1.0.0", + "write-file-atomic": "2.3.0", + "xdg-basedir": "3.0.0" + } + }, + "create-error-class": { + "version": "3.0.2", + "resolved": "http://registry.npm.taobao.org/create-error-class/download/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "1.0.0" + } + }, + "cross-spawn": { + "version": "5.1.0", + "resolved": "http://registry.npm.taobao.org/cross-spawn/download/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "dev": true, + "requires": { + "lru-cache": "4.1.1", + "shebang-command": "1.2.0", + "which": "1.3.0" + } + }, + "crypto-js": { + "version": "3.1.9-1", + "resolved": "http://registry.npm.taobao.org/crypto-js/download/crypto-js-3.1.9-1.tgz", + "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" + }, + "crypto-random-string": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/crypto-random-string/download/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "http://registry.npm.taobao.org/currently-unhandled/download/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "1.0.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/decamelize/download/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/decamelize-keys/download/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "dev": true, + "requires": { + "decamelize": "1.2.0", + "map-obj": "1.0.1" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/map-obj/download/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + } + } + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "http://registry.npm.taobao.org/deep-extend/download/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true + }, + "diff": { + "version": "3.4.0", + "resolved": "http://registry.npm.taobao.org/diff/download/diff-3.4.0.tgz", + "integrity": "sha1-sdhVB9rzlkgo3lSzfQ1zumfdpWw=", + "dev": true + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "http://registry.npm.taobao.org/dot-prop/download/dot-prop-4.2.0.tgz", + "integrity": "sha1-HxngwuGqDjJ5fEl5nyg3rGr2nFc=", + "dev": true, + "requires": { + "is-obj": "1.0.1" + } + }, + "duplexer3": { + "version": "0.1.4", + "resolved": "http://registry.npm.taobao.org/duplexer3/download/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, + "error-ex": { + "version": "1.3.1", + "resolved": "http://registry.npm.taobao.org/error-ex/download/error-ex-1.3.1.tgz", + "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", + "dev": true, + "requires": { + "is-arrayish": "0.2.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "http://registry.npm.taobao.org/escape-string-regexp/download/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "http://registry.npm.taobao.org/esutils/download/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "execa": { + "version": "0.7.0", + "resolved": "http://registry.npm.taobao.org/execa/download/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "dev": true, + "requires": { + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" + } + }, + "external-editor": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/external-editor/download/external-editor-2.1.0.tgz", + "integrity": "sha1-PQJqIbf5W1cmOH1CAKwWDTcsO0g=", + "dev": true, + "requires": { + "chardet": "0.4.2", + "iconv-lite": "0.4.19", + "tmp": "0.0.33" + } + }, + "figures": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/figures/download/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "dev": true, + "requires": { + "escape-string-regexp": "1.0.5" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/find-up/download/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/fs.realpath/download/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/get-stream/download/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "http://registry.npm.taobao.org/glob/download/glob-7.1.2.tgz", + "integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "global-dirs": { + "version": "0.1.1", + "resolved": "http://registry.npm.taobao.org/global-dirs/download/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", + "dev": true, + "requires": { + "ini": "1.3.5" + } + }, + "got": { + "version": "6.7.1", + "resolved": "http://registry.npm.taobao.org/got/download/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "3.0.2", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "lowercase-keys": "1.0.0", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "unzip-response": "2.0.1", + "url-parse-lax": "1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "http://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "gts": { + "version": "0.5.2", + "resolved": "http://registry.npm.taobao.org/gts/download/gts-0.5.2.tgz", + "integrity": "sha1-h2FxIasZDXK6SbFww7utF6T89S8=", + "dev": true, + "requires": { + "chalk": "2.3.0", + "clang-format": "1.2.1", + "inquirer": "3.3.0", + "meow": "4.0.0", + "pify": "3.0.0", + "rimraf": "2.6.2", + "tslint": "5.8.0", + "update-notifier": "2.3.0", + "write-file-atomic": "2.3.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/has-ansi/download/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/ansi-regex/download/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + } + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/has-flag/download/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "hosted-git-info": { + "version": "2.5.0", + "resolved": "http://registry.npm.taobao.org/hosted-git-info/download/hosted-git-info-2.5.0.tgz", + "integrity": "sha1-bWDjSzq7yDEwYsO3mO+NkBoHrzw=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.19", + "resolved": "http://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.19.tgz", + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=", + "dev": true + }, + "import-lazy": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/import-lazy/download/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "http://registry.npm.taobao.org/imurmurhash/download/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "3.2.0", + "resolved": "http://registry.npm.taobao.org/indent-string/download/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "http://registry.npm.taobao.org/inflight/download/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "http://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "http://registry.npm.taobao.org/ini/download/ini-1.3.5.tgz", + "integrity": "sha1-7uJfVtscnsYIXgwid4CD9Zar+Sc=", + "dev": true + }, + "inquirer": { + "version": "3.3.0", + "resolved": "http://registry.npm.taobao.org/inquirer/download/inquirer-3.3.0.tgz", + "integrity": "sha1-ndLyrXZdyrH/BEO0kUQqILoifck=", + "dev": true, + "requires": { + "ansi-escapes": "3.0.0", + "chalk": "2.3.0", + "cli-cursor": "2.1.0", + "cli-width": "2.2.0", + "external-editor": "2.1.0", + "figures": "2.0.0", + "lodash": "4.17.4", + "mute-stream": "0.0.7", + "run-async": "2.3.0", + "rx-lite": "4.0.8", + "rx-lite-aggregates": "4.0.8", + "string-width": "2.1.1", + "strip-ansi": "4.0.0", + "through": "2.3.8" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "http://registry.npm.taobao.org/is-arrayish/download/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/is-builtin-module/download/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "dev": true, + "requires": { + "builtin-modules": "1.1.1" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/is-fullwidth-code-point/download/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "http://registry.npm.taobao.org/is-installed-globally/download/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", + "dev": true, + "requires": { + "global-dirs": "0.1.1", + "is-path-inside": "1.0.1" + } + }, + "is-npm": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/is-npm/download/is-npm-1.0.0.tgz", + "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ=", + "dev": true + }, + "is-obj": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/is-obj/download/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/is-path-inside/download/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "dev": true, + "requires": { + "path-is-inside": "1.0.2" + } + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/is-plain-obj/download/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", + "dev": true + }, + "is-promise": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/is-promise/download/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, + "is-redirect": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/is-redirect/download/is-redirect-1.0.0.tgz", + "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=", + "dev": true + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/is-retry-allowed/download/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/is-stream/download/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/isexe/download/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "http://registry.npm.taobao.org/js-tokens/download/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/json-parse-better-errors/download/json-parse-better-errors-1.0.1.tgz", + "integrity": "sha1-UBg80bLSUnXeBp6ecbRnrJ6rlzo=", + "dev": true + }, + "latest-version": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/latest-version/download/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", + "dev": true, + "requires": { + "package-json": "4.0.1" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/load-json-file/download/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "parse-json": "4.0.0", + "pify": "3.0.0", + "strip-bom": "3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/locate-path/download/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "2.0.0", + "path-exists": "3.0.0" + } + }, + "lodash": { + "version": "4.17.4", + "resolved": "http://registry.npm.taobao.org/lodash/download/lodash-4.17.4.tgz", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "dev": true + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "http://registry.npm.taobao.org/loud-rejection/download/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "0.4.1", + "signal-exit": "3.0.2" + } + }, + "lowercase-keys": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/lowercase-keys/download/lowercase-keys-1.0.0.tgz", + "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", + "dev": true + }, + "lru-cache": { + "version": "4.1.1", + "resolved": "http://registry.npm.taobao.org/lru-cache/download/lru-cache-4.1.1.tgz", + "integrity": "sha1-Yi4y6CSItJJ5EUpPns9F581rulU=", + "dev": true, + "requires": { + "pseudomap": "1.0.2", + "yallist": "2.1.2" + } + }, + "make-dir": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/make-dir/download/make-dir-1.1.0.tgz", + "integrity": "sha1-GbQ2n+SMEW9Twq+VrRAsDjnoXVE=", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "map-obj": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/map-obj/download/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=", + "dev": true + }, + "meow": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/meow/download/meow-4.0.0.tgz", + "integrity": "sha1-/VhV3QCNtbksVSCC2xwwfLogsp0=", + "dev": true, + "requires": { + "camelcase-keys": "4.2.0", + "decamelize-keys": "1.1.0", + "loud-rejection": "1.6.0", + "minimist": "1.2.0", + "minimist-options": "3.0.2", + "normalize-package-data": "2.4.0", + "read-pkg-up": "3.0.0", + "redent": "2.0.0", + "trim-newlines": "2.0.0" + } + }, + "mimic-fn": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/mimic-fn/download/mimic-fn-1.1.0.tgz", + "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "http://registry.npm.taobao.org/minimatch/download/minimatch-3.0.4.tgz", + "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/minimist/download/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "http://registry.npm.taobao.org/minimist-options/download/minimist-options-3.0.2.tgz", + "integrity": "sha1-+6TIGRM54T7PTWG+sD8HAQPz2VQ=", + "dev": true, + "requires": { + "arrify": "1.0.1", + "is-plain-obj": "1.1.0" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "http://registry.npm.taobao.org/normalize-package-data/download/normalize-package-data-2.4.0.tgz", + "integrity": "sha1-EvlaMH1YNSB1oEkHuErIvpisAS8=", + "dev": true, + "requires": { + "hosted-git-info": "2.5.0", + "is-builtin-module": "1.0.0", + "semver": "5.4.1", + "validate-npm-package-license": "3.0.1" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "http://registry.npm.taobao.org/npm-run-path/download/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "http://registry.npm.taobao.org/once/download/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "http://registry.npm.taobao.org/onetime/download/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "dev": true, + "requires": { + "mimic-fn": "1.1.0" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/os-tmpdir/download/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, + "otplib": { + "version": "7.0.0", + "resolved": "http://registry.npm.taobao.org/otplib/download/otplib-7.0.0.tgz", + "integrity": "sha1-xIrTn6//SkUux1OtnCNp+wszgOo=", + "requires": { + "thirty-two": "1.0.2" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/p-finally/download/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true + }, + "p-limit": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/p-limit/download/p-limit-1.1.0.tgz", + "integrity": "sha1-sH/y2aXYi+yAYDWJWiurZqJ5iLw=", + "dev": true + }, + "p-locate": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/p-locate/download/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "1.1.0" + } + }, + "package-json": { + "version": "4.0.1", + "resolved": "http://registry.npm.taobao.org/package-json/download/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", + "dev": true, + "requires": { + "got": "6.7.1", + "registry-auth-token": "3.3.1", + "registry-url": "3.1.0", + "semver": "5.4.1" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/parse-json/download/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "dev": true, + "requires": { + "error-ex": "1.3.1", + "json-parse-better-errors": "1.0.1" + } + }, + "path-exists": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/path-exists/download/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "http://registry.npm.taobao.org/path-is-absolute/download/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/path-is-inside/download/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "dev": true + }, + "path-key": { + "version": "2.0.1", + "resolved": "http://registry.npm.taobao.org/path-key/download/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "http://registry.npm.taobao.org/path-parse/download/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-type": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/path-type/download/path-type-3.0.0.tgz", + "integrity": "sha1-zvMdyOCho7sNEFwM2Xzzv0f0428=", + "dev": true, + "requires": { + "pify": "3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/pify/download/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "http://registry.npm.taobao.org/prepend-http/download/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/pseudomap/download/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "http://registry.npm.taobao.org/quick-lru/download/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=", + "dev": true + }, + "rc": { + "version": "1.2.2", + "resolved": "http://registry.npm.taobao.org/rc/download/rc-1.2.2.tgz", + "integrity": "sha1-2M6ctX6NZNnHut2YdsfDTL48cHc=", + "dev": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.5", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/read-pkg/download/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "dev": true, + "requires": { + "load-json-file": "4.0.0", + "normalize-package-data": "2.4.0", + "path-type": "3.0.0" + } + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/read-pkg-up/download/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "dev": true, + "requires": { + "find-up": "2.1.0", + "read-pkg": "3.0.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/redent/download/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "dev": true, + "requires": { + "indent-string": "3.2.0", + "strip-indent": "2.0.0" + } + }, + "registry-auth-token": { + "version": "3.3.1", + "resolved": "http://registry.npm.taobao.org/registry-auth-token/download/registry-auth-token-3.3.1.tgz", + "integrity": "sha1-+w0yie4Nmtosu1KvXf5mywcNMAY=", + "dev": true, + "requires": { + "rc": "1.2.2", + "safe-buffer": "5.1.1" + } + }, + "registry-url": { + "version": "3.1.0", + "resolved": "http://registry.npm.taobao.org/registry-url/download/registry-url-3.1.0.tgz", + "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", + "dev": true, + "requires": { + "rc": "1.2.2" + } + }, + "resolve": { + "version": "1.5.0", + "resolved": "http://registry.npm.taobao.org/resolve/download/resolve-1.5.0.tgz", + "integrity": "sha1-HwmsznlsmnYlefMbLBzEw83fnzY=", + "dev": true, + "requires": { + "path-parse": "1.0.5" + } + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/restore-cursor/download/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "dev": true, + "requires": { + "onetime": "2.0.1", + "signal-exit": "3.0.2" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "http://registry.npm.taobao.org/rimraf/download/rimraf-2.6.2.tgz", + "integrity": "sha1-LtgVDSShbqhlHm1u8PR8QVjOejY=", + "dev": true, + "requires": { + "glob": "7.1.2" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "http://registry.npm.taobao.org/run-async/download/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "2.1.0" + } + }, + "rx-lite": { + "version": "4.0.8", + "resolved": "http://registry.npm.taobao.org/rx-lite/download/rx-lite-4.0.8.tgz", + "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", + "dev": true + }, + "rx-lite-aggregates": { + "version": "4.0.8", + "resolved": "http://registry.npm.taobao.org/rx-lite-aggregates/download/rx-lite-aggregates-4.0.8.tgz", + "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", + "dev": true, + "requires": { + "rx-lite": "4.0.8" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "http://registry.npm.taobao.org/safe-buffer/download/safe-buffer-5.1.1.tgz", + "integrity": "sha1-iTMSr2myEj3vcfV4iQAWce6yyFM=", + "dev": true + }, + "semver": { + "version": "5.4.1", + "resolved": "http://registry.npm.taobao.org/semver/download/semver-5.4.1.tgz", + "integrity": "sha1-4FnAnYVx8FQII3M0M1BdOi8AsY4=", + "dev": true + }, + "semver-diff": { + "version": "2.1.0", + "resolved": "http://registry.npm.taobao.org/semver-diff/download/semver-diff-2.1.0.tgz", + "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", + "dev": true, + "requires": { + "semver": "5.4.1" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/shebang-command/download/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/shebang-regex/download/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", + "dev": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "http://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true + }, + "spdx-correct": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/spdx-correct/download/spdx-correct-1.0.2.tgz", + "integrity": "sha1-SzBz2TP/UfORLwOsVRlJikFQ20A=", + "dev": true, + "requires": { + "spdx-license-ids": "1.2.2" + } + }, + "spdx-expression-parse": { + "version": "1.0.4", + "resolved": "http://registry.npm.taobao.org/spdx-expression-parse/download/spdx-expression-parse-1.0.4.tgz", + "integrity": "sha1-m98vIOH0DtRH++JzJmGR/O1RYmw=", + "dev": true + }, + "spdx-license-ids": { + "version": "1.2.2", + "resolved": "http://registry.npm.taobao.org/spdx-license-ids/download/spdx-license-ids-1.2.2.tgz", + "integrity": "sha1-yd96NCRZSt5r0RkA1ZZpbcBrrFc=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "http://registry.npm.taobao.org/string-width/download/string-width-2.1.1.tgz", + "integrity": "sha1-q5Pyeo3BPSjKyBXEYhQ6bZASrp4=", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "http://registry.npm.taobao.org/strip-ansi/download/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/strip-bom/download/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/strip-eof/download/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/strip-indent/download/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=", + "dev": true + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "http://registry.npm.taobao.org/strip-json-comments/download/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true + }, + "supports-color": { + "version": "4.5.0", + "resolved": "http://registry.npm.taobao.org/supports-color/download/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "term-size": { + "version": "1.2.0", + "resolved": "http://registry.npm.taobao.org/term-size/download/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "0.7.0" + } + }, + "thirty-two": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/thirty-two/download/thirty-two-1.0.2.tgz", + "integrity": "sha1-TKL//AKlEpDSdEueP1V2k8prYno=" + }, + "through": { + "version": "2.3.8", + "resolved": "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "timed-out": { + "version": "4.0.1", + "resolved": "http://registry.npm.taobao.org/timed-out/download/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "http://registry.npm.taobao.org/tmp/download/tmp-0.0.33.tgz", + "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", + "dev": true, + "requires": { + "os-tmpdir": "1.0.2" + } + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/trim-newlines/download/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=", + "dev": true + }, + "tslib": { + "version": "1.8.1", + "resolved": "http://registry.npm.taobao.org/tslib/download/tslib-1.8.1.tgz", + "integrity": "sha1-aUavLR1lGnsYY7Ux1uWvpBqkTqw=", + "dev": true + }, + "tslint": { + "version": "5.8.0", + "resolved": "http://registry.npm.taobao.org/tslint/download/tslint-5.8.0.tgz", + "integrity": "sha1-H0mtWy53x2w69N3K5VKuTjYS6xM=", + "dev": true, + "requires": { + "babel-code-frame": "6.26.0", + "builtin-modules": "1.1.1", + "chalk": "2.3.0", + "commander": "2.12.2", + "diff": "3.4.0", + "glob": "7.1.2", + "minimatch": "3.0.4", + "resolve": "1.5.0", + "semver": "5.4.1", + "tslib": "1.8.1", + "tsutils": "2.13.1" + } + }, + "tsutils": { + "version": "2.13.1", + "resolved": "http://registry.npm.taobao.org/tsutils/download/tsutils-2.13.1.tgz", + "integrity": "sha1-1tHMD3wEz5+zsoopKXPP+5z74Jo=", + "dev": true, + "requires": { + "tslib": "1.8.1" + } + }, + "typescript": { + "version": "2.6.2", + "resolved": "http://registry.npm.taobao.org/typescript/download/typescript-2.6.2.tgz", + "integrity": "sha1-PFtv1/beCRQmkCfwPAlGdY92c6Q=", + "dev": true + }, + "unique-string": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/unique-string/download/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "1.0.0" + } + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "http://registry.npm.taobao.org/unzip-response/download/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true + }, + "update-notifier": { + "version": "2.3.0", + "resolved": "http://registry.npm.taobao.org/update-notifier/download/update-notifier-2.3.0.tgz", + "integrity": "sha1-TognpruRUUCrCTVZ1wFOPruDdFE=", + "dev": true, + "requires": { + "boxen": "1.3.0", + "chalk": "2.3.0", + "configstore": "3.1.1", + "import-lazy": "2.1.0", + "is-installed-globally": "0.1.0", + "is-npm": "1.0.0", + "latest-version": "3.1.0", + "semver-diff": "2.1.0", + "xdg-basedir": "3.0.0" + } + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "http://registry.npm.taobao.org/url-parse-lax/download/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "1.0.4" + } + }, + "validate-npm-package-license": { + "version": "3.0.1", + "resolved": "http://registry.npm.taobao.org/validate-npm-package-license/download/validate-npm-package-license-3.0.1.tgz", + "integrity": "sha1-KAS6vnEq0zeUWaz74kdGqywwP7w=", + "dev": true, + "requires": { + "spdx-correct": "1.0.2", + "spdx-expression-parse": "1.0.4" + } + }, + "vue": { + "version": "2.5.13", + "resolved": "http://registry.npm.taobao.org/vue/download/vue-2.5.13.tgz", + "integrity": "sha1-lb0x4g7896fzkjnJqmeHzoz1eOE=" + }, + "which": { + "version": "1.3.0", + "resolved": "http://registry.npm.taobao.org/which/download/which-1.3.0.tgz", + "integrity": "sha1-/wS9/AEO5UfXgL7DjhrBwnd9JTo=", + "dev": true, + "requires": { + "isexe": "2.0.0" + } + }, + "widest-line": { + "version": "2.0.0", + "resolved": "http://registry.npm.taobao.org/widest-line/download/widest-line-2.0.0.tgz", + "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", + "dev": true, + "requires": { + "string-width": "2.1.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "http://registry.npm.taobao.org/wrappy/download/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "http://registry.npm.taobao.org/write-file-atomic/download/write-file-atomic-2.3.0.tgz", + "integrity": "sha1-H/YVdcLipOjlENb6TiQ8zhg5mas=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "imurmurhash": "0.1.4", + "signal-exit": "3.0.2" + } + }, + "xdg-basedir": { + "version": "3.0.0", + "resolved": "http://registry.npm.taobao.org/xdg-basedir/download/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "http://registry.npm.taobao.org/yallist/download/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..d38aa7168 --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "authenticator", + "version": "0.1.0", + "description": "For Google Authenticator and Battle.net Authenticator.", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "check": "gts check", + "clean": "gts clean", + "compile": "tsc -p .", + "fix": "gts fix", + "prepare": "npm run compile", + "pretest": "npm run compile", + "posttest": "npm run check" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Sneezry/Authenticator2.git" + }, + "author": "Sneezry", + "license": "MIT", + "bugs": { + "url": "https://github.com/Sneezry/Authenticator2/issues" + }, + "homepage": "https://github.com/Sneezry/Authenticator2#readme", + "devDependencies": { + "@types/chrome": "0.0.56", + "@types/crypto-js": "^3.1.38", + "@types/otplib": "^7.0.0", + "@types/vue": "^2.0.0", + "gts": "^0.5.2", + "typescript": "^2.6.1" + }, + "dependencies": { + "crypto-js": "^3.1.9-1", + "otplib": "^7.0.0", + "vue": "^2.5.13" + } +} diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 000000000..b7dab5e9c --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,2 @@ +node_modules +build \ No newline at end of file diff --git a/src/interface.ts b/src/interface.ts new file mode 100644 index 000000000..ee26ebdf7 --- /dev/null +++ b/src/interface.ts @@ -0,0 +1,19 @@ +export enum OPTType { + topt = 1, + hopt, + battle, + steam +} + +export interface OPT { + type: OPTType; + index: number; + issuer: string; + secret: string; + account: string; + hash: string; + encrypted: boolean; + create(type: OPTType, issuer: string, secret: string, account: string): void; + update(): void; + delete(): void; +} \ No newline at end of file diff --git a/src/opt.ts b/src/opt.ts new file mode 100644 index 000000000..b5fe44927 --- /dev/null +++ b/src/opt.ts @@ -0,0 +1,25 @@ +import * as CryptoJS from 'crypto-js'; +import {authenticator} from 'otplib'; + +import {OPT, OPTType} from './interface'; + +export class OPTEntry implements OPT { + type: OPTType; + index: number; + issuer: string; + secret: string; + account: string; + hash: string; + encrypted: boolean; + + create(type: OPTType, issuer: string, secret: string, account: string) { + this.type = type; + this.issuer = issuer; + this.secret = secret; + this.account = account; + this.hash = CryptoJS.MD5(secret).toString(); + } + + update() {} + delete() {} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..8a23a6525 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "./../../../../usr/local/lib/node_modules/gts/tsconfig-google.json", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/*.ts", + "src/**/*.ts", + "test/*.ts", + "test/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} From 83c4e23da83a08b9803c75129648164a9da21674 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 3 Feb 2018 07:13:49 +0800 Subject: [PATCH 002/178] save --- src/.gitignore => .gitignore | 0 .vscode/settings.json | 3 + package-lock.json | 32 ++++----- package.json | 6 +- src/interface.ts | 19 ----- src/models/encryption.ts | 32 +++++++++ src/models/interface.ts | 34 +++++++++ src/models/key-utilities.ts | 122 ++++++++++++++++++++++++++++++++ src/models/opt.ts | 63 +++++++++++++++++ src/models/storage.ts | 133 +++++++++++++++++++++++++++++++++++ src/opt.ts | 25 ------- tsconfig.json | 2 + 12 files changed, 404 insertions(+), 67 deletions(-) rename src/.gitignore => .gitignore (100%) create mode 100644 .vscode/settings.json delete mode 100644 src/interface.ts create mode 100644 src/models/encryption.ts create mode 100644 src/models/interface.ts create mode 100644 src/models/key-utilities.ts create mode 100644 src/models/opt.ts create mode 100644 src/models/storage.ts delete mode 100644 src/opt.ts diff --git a/src/.gitignore b/.gitignore similarity index 100% rename from src/.gitignore rename to .gitignore diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..68ebc15d9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "editor.tabSize": 4 +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 143fdf42b..356d89b1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@types/chrome": { - "version": "0.0.56", - "resolved": "http://registry.npm.taobao.org/@types/chrome/download/@types/chrome-0.0.56.tgz", - "integrity": "sha1-Dch9wkf3uens0B8GoIBCLTXjQyI=", + "version": "0.0.59", + "resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.59.tgz", + "integrity": "sha512-wbzlTyfybnPgKDgc6krdbZW+8QPTYedWptjB4szZU4mWmhm9+Gn8yjDJQe2rM+HiYp3iTdP6yOa0kBDURStjEg==", "dev": true, "requires": { "@types/filesystem": "0.0.28" @@ -34,10 +34,10 @@ "integrity": "sha1-wFTor02d11205jq8dviFFocU1LM=", "dev": true }, - "@types/otplib": { - "version": "7.0.0", - "resolved": "http://registry.npm.taobao.org/@types/otplib/download/@types/otplib-7.0.0.tgz", - "integrity": "sha1-vGCMh3HLoPRBdHju957z95xH6fY=", + "@types/jssha": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/jssha/-/jssha-0.0.29.tgz", + "integrity": "sha1-leg9uph4f/eW0tXzehklq/Qbycs=", "dev": true }, "@types/vue": { @@ -730,6 +730,11 @@ "integrity": "sha1-UBg80bLSUnXeBp6ecbRnrJ6rlzo=", "dev": true }, + "jssha": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz", + "integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po=" + }, "latest-version": { "version": "3.1.0", "resolved": "http://registry.npm.taobao.org/latest-version/download/latest-version-3.1.0.tgz", @@ -907,14 +912,6 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, - "otplib": { - "version": "7.0.0", - "resolved": "http://registry.npm.taobao.org/otplib/download/otplib-7.0.0.tgz", - "integrity": "sha1-xIrTn6//SkUux1OtnCNp+wszgOo=", - "requires": { - "thirty-two": "1.0.2" - } - }, "p-finally": { "version": "1.0.0", "resolved": "http://registry.npm.taobao.org/p-finally/download/p-finally-1.0.0.tgz", @@ -1259,11 +1256,6 @@ "execa": "0.7.0" } }, - "thirty-two": { - "version": "1.0.2", - "resolved": "http://registry.npm.taobao.org/thirty-two/download/thirty-two-1.0.2.tgz", - "integrity": "sha1-TKL//AKlEpDSdEueP1V2k8prYno=" - }, "through": { "version": "2.3.8", "resolved": "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz", diff --git a/package.json b/package.json index d38aa7168..b9b270b96 100644 --- a/package.json +++ b/package.json @@ -23,16 +23,16 @@ }, "homepage": "https://github.com/Sneezry/Authenticator2#readme", "devDependencies": { - "@types/chrome": "0.0.56", + "@types/chrome": "^0.0.59", "@types/crypto-js": "^3.1.38", - "@types/otplib": "^7.0.0", + "@types/jssha": "0.0.29", "@types/vue": "^2.0.0", "gts": "^0.5.2", "typescript": "^2.6.1" }, "dependencies": { "crypto-js": "^3.1.9-1", - "otplib": "^7.0.0", + "jssha": "^2.3.1", "vue": "^2.5.13" } } diff --git a/src/interface.ts b/src/interface.ts deleted file mode 100644 index ee26ebdf7..000000000 --- a/src/interface.ts +++ /dev/null @@ -1,19 +0,0 @@ -export enum OPTType { - topt = 1, - hopt, - battle, - steam -} - -export interface OPT { - type: OPTType; - index: number; - issuer: string; - secret: string; - account: string; - hash: string; - encrypted: boolean; - create(type: OPTType, issuer: string, secret: string, account: string): void; - update(): void; - delete(): void; -} \ No newline at end of file diff --git a/src/models/encryption.ts b/src/models/encryption.ts new file mode 100644 index 000000000..df81368f5 --- /dev/null +++ b/src/models/encryption.ts @@ -0,0 +1,32 @@ +import * as CryptoJS from 'crypto-js'; + +export class Encription { + private password: string; + + constructor(password: string) { + this.password = password; + } + + getEncryptedSecret(secret: string): string { + if (!this.password) { + return secret; + } + return CryptoJS.AES.encrypt(secret, this.password).toString(); + } + + getDecryptedSecret(secret: string): string { + if (!this.password) { + return secret; + } + return CryptoJS.AES.decrypt(secret, this.password) + .toString(CryptoJS.enc.Utf8); + } + + getEncryptionStatus(): boolean { + return this.password ? true : false; + } + + updateEncryptionPassword(password: string) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/models/interface.ts b/src/models/interface.ts new file mode 100644 index 000000000..fe24bbc13 --- /dev/null +++ b/src/models/interface.ts @@ -0,0 +1,34 @@ +import {Encription} from './encryption'; + +export enum OPTType { + totp = 1, + hotp, + battle, + steam +} + +export interface OPT { + type: OPTType; + index: number; + issuer: string; + secret: string; + account: string; + hash: string; + create(encryption: Encription): Promise; + update( + encryption: Encription, issuer: string, account: string, index: number, + counter: number): Promise; + next(encryption: Encription): Promise; + delete(): Promise; + generate(): string; +} + +export interface OPTStorage { + account: string; + encrypted: boolean; + hash: string; + index: number; + issuer: string; + secret: string; + type: string; +} \ No newline at end of file diff --git a/src/models/key-utilities.ts b/src/models/key-utilities.ts new file mode 100644 index 000000000..37f49dd52 --- /dev/null +++ b/src/models/key-utilities.ts @@ -0,0 +1,122 @@ +// Originally based on the JavaScript implementation as provided by Russell +// Sayers on his Tin Isles blog: +// http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/ + +// Rewrite with TypeScript by Sneezry https://github.com/Sneezry +import jsSHA = require('jssha'); + +export class KeyUtilities { + private static dec2hex(s: number): string { + return (s < 15.5 ? '0' : '') + Math.round(s).toString(16); + } + + private static hex2dec(s: string): number { + return Number(`0x${s}`); + } + + private static hex2str(hex: string) { + let str = ''; + for (let i = 0; i < hex.length; i += 2) { + str += String.fromCharCode(this.hex2dec(hex.substr(i, 2))); + } + return str; + } + + private static leftpad(str: string, len: number, pad: string): string { + if (len + 1 >= str.length) { + str = new Array(len + 1 - str.length).join(pad) + str; + } + return str; + } + + private static base32tohex(base32: string): string { + const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + let bits = ''; + let hex = ''; + + for (let i = 0; i < base32.length; i++) { + 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) { + const chunk = bits.substr(i, 4); + hex = hex + Number(`0b${chunk}`).toString(16); + } + + if (hex.length % 2 && hex[hex.length - 1] === '0') { + hex = hex.substr(0, hex.length - 1); + } + + return hex; + } + + private static base26(num: number) { + const chars = '23456789BCDFGHJKMNPQRTVWXY'; + let output = ''; + const len = 5; + for (let i = 0; i < len; i++) { + output += chars[num % chars.length]; + num = Math.floor(num / chars.length); + } + if (output.length < len) { + output = new Array(len - output.length + 1).join(chars[0]) + output; + } + return output; + } + + static generate(secret: string, counter?: number) { + secret = secret.replace(/\s/g, ''); + let len = 6; + let b26 = false; + let key = ''; + if (/^[a-z2-7]+=*$/.test(secret.toLowerCase())) { + key = this.base32tohex(secret); + } else if (/^[0-9a-f]+$/.test(secret.toLowerCase())) { + key = secret; + } else if (/^bliz\-/.test(secret.toLowerCase())) { + key = this.base32tohex(secret.substr(5)); + len = 8; + } else if (/^blz\-/.test(secret.toLowerCase())) { + key = this.base32tohex(secret.substr(4)); + len = 8; + } else if (/^stm\-/.test(secret.toLowerCase())) { + key = this.base32tohex(secret.substr(4)); + len = 10; + b26 = true; + } + if (counter === undefined) { + let epoch = Math.round(new Date().getTime() / 1000.0); + if (localStorage.offset) { + epoch = epoch + Number(localStorage.offset); + } + counter = Math.floor(epoch / 30); + } + + const time = this.leftpad(this.dec2hex(counter), 16, '0'); + + // external library for SHA functionality + const hmacObj = new jsSHA('SHA-1', 'HEX'); + hmacObj.setHMACKey(key, 'HEX'); + hmacObj.update(this.hex2str(time)); + const hmac = hmacObj.getHMAC('HEX'); + + let offset = 0; + if (hmac !== 'KEY MUST BE IN BYTE INCREMENTS') { + offset = this.hex2dec(hmac.substring(hmac.length - 1)); + } + + let otp = + (this.hex2dec(hmac.substr(offset * 2, 8)) & this.hex2dec('7fffffff')) + + ''; + + if (b26) { + return this.base26(Number(otp)); + } + + if (otp.length < len) { + otp = new Array(len - otp.length + 1).join('0') + otp; + } + return (otp).substr(otp.length - len, len).toString(); + } +} diff --git a/src/models/opt.ts b/src/models/opt.ts new file mode 100644 index 000000000..96be5da58 --- /dev/null +++ b/src/models/opt.ts @@ -0,0 +1,63 @@ +import * as CryptoJS from 'crypto-js'; + +import {Encription} from './encryption'; +import {OPT, OPTType} from './interface'; +import {KeyUtilities} from './key-utilities'; +import {Storage} from './storage'; + +export class OPTEntry implements OPT { + type: OPTType; + index: number; + issuer: string; + secret: string; + account: string; + hash: string; + counter: number; + + constructor( + type: OPTType, issuer: string, secret: string, account: string, + index: number) { + this.type = type; + this.index = index; + this.issuer = issuer; + this.secret = secret; + this.account = account; + this.hash = CryptoJS.MD5(secret).toString(); + this.counter = 0; + } + + async create(encryption: Encription) { + await Storage.add(encryption, this); + return; + } + + async update( + encryption: Encription, issuer: string, account: string, index: number, + counter: number) { + this.issuer = issuer; + this.account = account; + this.index = index; + this.counter = counter; + Storage.update(encryption, this); + return; + } + + async delete() { + Storage.delete(this); + return; + } + + async next(encryption: Encription) { + if (this.type !== OPTType.hotp) { + return; + } + this.counter++; + await this.update( + encryption, this.issuer, this.secret, this.index, this.counter); + return; + } + + generate() { + return KeyUtilities.generate(this.secret); + } +} \ No newline at end of file diff --git a/src/models/storage.ts b/src/models/storage.ts new file mode 100644 index 000000000..7450073bc --- /dev/null +++ b/src/models/storage.ts @@ -0,0 +1,133 @@ +import {Encription} from './encryption'; +import {OPTStorage, OPTType} from './interface'; +import {OPTEntry} from './opt'; + +export class Storage { + private static getOPTStorageFromEntry( + encryption: Encription, entry: OPTEntry): OPTStorage { + const storageItem: OPTStorage = { + account: entry.account, + hash: entry.hash, + index: entry.index, + issuer: entry.issuer, + type: OPTType[entry.type], + secret: encryption.getEncryptedSecret(entry.secret), + encrypted: encryption.getEncryptionStatus() + }; + return storageItem; + } + + private static ensureUniqueIndex(_data: {[hash: string]: OPTStorage}) { + const tempEntryArray: OPTStorage[] = []; + + for (const hash of Object.keys(_data)) { + tempEntryArray.push(_data[hash]); + } + + tempEntryArray.sort((a, b) => { + return a.index - b.index; + }); + + const newData: {[hash: string]: OPTStorage} = {}; + for (let i = 0; i < tempEntryArray.length; i++) { + tempEntryArray[i].index = i; + newData[tempEntryArray[i].hash] = tempEntryArray[i]; + } + + return newData; + } + + static async add(encryption: Encription, entry: OPTEntry) { + return new Promise( + (resolve: () => void, reject: (reason: Error) => void) => { + try { + chrome.storage.sync.get((_data: {[hash: string]: OPTStorage}) => { + if (_data.hasOwnProperty(entry.hash)) { + throw new Error('The specific entry has already existed.'); + } + const storageItem = + this.getOPTStorageFromEntry(encryption, entry); + _data[entry.hash] = storageItem; + _data = this.ensureUniqueIndex(_data); + chrome.storage.sync.set(_data, Promise.resolve); + }); + return; + } catch (error) { + return Promise.reject(error); + } + }); + } + + static async update(encryption: Encription, entry: OPTEntry) { + return new Promise( + (resolve: () => void, reject: (reason: Error) => void) => { + try { + chrome.storage.sync.get((_data: {[hash: string]: OPTStorage}) => { + if (!_data.hasOwnProperty(entry.hash)) { + throw new Error('The specific entry is not existing.'); + } + const storageItem = + this.getOPTStorageFromEntry(encryption, entry); + _data[entry.hash] = storageItem; + _data = this.ensureUniqueIndex(_data); + chrome.storage.sync.set(_data, Promise.resolve); + }); + return; + } catch (error) { + return Promise.reject(error); + } + }); + } + + static async get() { + return new Promise( + (resolve: (value: OPTEntry[]) => void, + reject: (reason: Error) => void) => { + try { + chrome.storage.sync.get((_data: {[hash: string]: OPTStorage}) => { + const data: OPTEntry[] = []; + for (const hash of Object.keys(_data)) { + const entryData = _data[hash]; + let type: OPTType; + switch (entryData.type) { + case 'totp': + case 'hotp': + case 'battle': + case 'steam': + type = OPTType[entryData.type]; + break; + default: + type = OPTType.totp; + } + const entry = new OPTEntry( + type, entryData.issuer, entryData.secret, entryData.account, + entryData.index); + data.push(entry); + } + Promise.resolve(data); + }); + return; + } catch (error) { + return Promise.reject(error); + } + }); + } + + static async delete(entry: OPTEntry) { + return new Promise( + (resolve: () => void, reject: (reason: Error) => void) => { + try { + chrome.storage.sync.get((_data: {[hash: string]: OPTStorage}) => { + if (_data.hasOwnProperty(entry.hash)) { + delete _data[entry.hash]; + } + _data = this.ensureUniqueIndex(_data); + return Promise.resolve(); + }); + return; + } catch (error) { + return Promise.reject(error); + } + }); + } +} \ No newline at end of file diff --git a/src/opt.ts b/src/opt.ts deleted file mode 100644 index b5fe44927..000000000 --- a/src/opt.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as CryptoJS from 'crypto-js'; -import {authenticator} from 'otplib'; - -import {OPT, OPTType} from './interface'; - -export class OPTEntry implements OPT { - type: OPTType; - index: number; - issuer: string; - secret: string; - account: string; - hash: string; - encrypted: boolean; - - create(type: OPTType, issuer: string, secret: string, account: string) { - this.type = type; - this.issuer = issuer; - this.secret = secret; - this.account = account; - this.hash = CryptoJS.MD5(secret).toString(); - } - - update() {} - delete() {} -} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 8a23a6525..6165fd7d3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "./../../../../usr/local/lib/node_modules/gts/tsconfig-google.json", "compilerOptions": { + "module": "commonjs", + "lib": ["es6", "dom"], "rootDir": ".", "outDir": "build" }, From 3b6228b1358427322e8ef2f866a40bef96d1719b Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 3 Feb 2018 07:18:13 +0800 Subject: [PATCH 003/178] fix typo --- src/models/interface.ts | 8 +++--- src/models/{opt.ts => otp.ts} | 10 ++++---- src/models/storage.ts | 48 +++++++++++++++++------------------ 3 files changed, 33 insertions(+), 33 deletions(-) rename src/models/{opt.ts => otp.ts} (85%) diff --git a/src/models/interface.ts b/src/models/interface.ts index fe24bbc13..a53394e04 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -1,14 +1,14 @@ import {Encription} from './encryption'; -export enum OPTType { +export enum OTPType { totp = 1, hotp, battle, steam } -export interface OPT { - type: OPTType; +export interface OTP { + type: OTPType; index: number; issuer: string; secret: string; @@ -23,7 +23,7 @@ export interface OPT { generate(): string; } -export interface OPTStorage { +export interface OTPStorage { account: string; encrypted: boolean; hash: string; diff --git a/src/models/opt.ts b/src/models/otp.ts similarity index 85% rename from src/models/opt.ts rename to src/models/otp.ts index 96be5da58..7332c5f2a 100644 --- a/src/models/opt.ts +++ b/src/models/otp.ts @@ -1,12 +1,12 @@ import * as CryptoJS from 'crypto-js'; import {Encription} from './encryption'; -import {OPT, OPTType} from './interface'; +import {OTP, OTPType} from './interface'; import {KeyUtilities} from './key-utilities'; import {Storage} from './storage'; -export class OPTEntry implements OPT { - type: OPTType; +export class OTPEntry implements OTP { + type: OTPType; index: number; issuer: string; secret: string; @@ -15,7 +15,7 @@ export class OPTEntry implements OPT { counter: number; constructor( - type: OPTType, issuer: string, secret: string, account: string, + type: OTPType, issuer: string, secret: string, account: string, index: number) { this.type = type; this.index = index; @@ -48,7 +48,7 @@ export class OPTEntry implements OPT { } async next(encryption: Encription) { - if (this.type !== OPTType.hotp) { + if (this.type !== OTPType.hotp) { return; } this.counter++; diff --git a/src/models/storage.ts b/src/models/storage.ts index 7450073bc..aeb91d7c3 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -1,24 +1,24 @@ import {Encription} from './encryption'; -import {OPTStorage, OPTType} from './interface'; -import {OPTEntry} from './opt'; +import {OTPStorage, OTPType} from './interface'; +import {OTPEntry} from './otp'; export class Storage { - private static getOPTStorageFromEntry( - encryption: Encription, entry: OPTEntry): OPTStorage { - const storageItem: OPTStorage = { + private static getOTPStorageFromEntry( + encryption: Encription, entry: OTPEntry): OTPStorage { + const storageItem: OTPStorage = { account: entry.account, hash: entry.hash, index: entry.index, issuer: entry.issuer, - type: OPTType[entry.type], + type: OTPType[entry.type], secret: encryption.getEncryptedSecret(entry.secret), encrypted: encryption.getEncryptionStatus() }; return storageItem; } - private static ensureUniqueIndex(_data: {[hash: string]: OPTStorage}) { - const tempEntryArray: OPTStorage[] = []; + private static ensureUniqueIndex(_data: {[hash: string]: OTPStorage}) { + const tempEntryArray: OTPStorage[] = []; for (const hash of Object.keys(_data)) { tempEntryArray.push(_data[hash]); @@ -28,7 +28,7 @@ export class Storage { return a.index - b.index; }); - const newData: {[hash: string]: OPTStorage} = {}; + const newData: {[hash: string]: OTPStorage} = {}; for (let i = 0; i < tempEntryArray.length; i++) { tempEntryArray[i].index = i; newData[tempEntryArray[i].hash] = tempEntryArray[i]; @@ -37,16 +37,16 @@ export class Storage { return newData; } - static async add(encryption: Encription, entry: OPTEntry) { + static async add(encryption: Encription, entry: OTPEntry) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { try { - chrome.storage.sync.get((_data: {[hash: string]: OPTStorage}) => { + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { if (_data.hasOwnProperty(entry.hash)) { throw new Error('The specific entry has already existed.'); } const storageItem = - this.getOPTStorageFromEntry(encryption, entry); + this.getOTPStorageFromEntry(encryption, entry); _data[entry.hash] = storageItem; _data = this.ensureUniqueIndex(_data); chrome.storage.sync.set(_data, Promise.resolve); @@ -58,16 +58,16 @@ export class Storage { }); } - static async update(encryption: Encription, entry: OPTEntry) { + static async update(encryption: Encription, entry: OTPEntry) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { try { - chrome.storage.sync.get((_data: {[hash: string]: OPTStorage}) => { + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { if (!_data.hasOwnProperty(entry.hash)) { throw new Error('The specific entry is not existing.'); } const storageItem = - this.getOPTStorageFromEntry(encryption, entry); + this.getOTPStorageFromEntry(encryption, entry); _data[entry.hash] = storageItem; _data = this.ensureUniqueIndex(_data); chrome.storage.sync.set(_data, Promise.resolve); @@ -81,25 +81,25 @@ export class Storage { static async get() { return new Promise( - (resolve: (value: OPTEntry[]) => void, + (resolve: (value: OTPEntry[]) => void, reject: (reason: Error) => void) => { try { - chrome.storage.sync.get((_data: {[hash: string]: OPTStorage}) => { - const data: OPTEntry[] = []; + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { + const data: OTPEntry[] = []; for (const hash of Object.keys(_data)) { const entryData = _data[hash]; - let type: OPTType; + let type: OTPType; switch (entryData.type) { case 'totp': case 'hotp': case 'battle': case 'steam': - type = OPTType[entryData.type]; + type = OTPType[entryData.type]; break; default: - type = OPTType.totp; + type = OTPType.totp; } - const entry = new OPTEntry( + const entry = new OTPEntry( type, entryData.issuer, entryData.secret, entryData.account, entryData.index); data.push(entry); @@ -113,11 +113,11 @@ export class Storage { }); } - static async delete(entry: OPTEntry) { + static async delete(entry: OTPEntry) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { try { - chrome.storage.sync.get((_data: {[hash: string]: OPTStorage}) => { + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { if (_data.hasOwnProperty(entry.hash)) { delete _data[entry.hash]; } From cda1034d155714883fe0878a0e24d702ba31c209 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 3 Feb 2018 07:30:41 +0800 Subject: [PATCH 004/178] change root folder --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 6165fd7d3..3152f3670 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "module": "commonjs", "lib": ["es6", "dom"], - "rootDir": ".", + "rootDir": "src", "outDir": "build" }, "include": [ From e38686ae46c5e9685fe429343cd30d1b7c9a4923 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 3 Feb 2018 15:49:10 +0800 Subject: [PATCH 005/178] remove modules --- src/models/encryption.ts | 4 ++-- src/models/interface.ts | 9 +++++---- src/models/key-utilities.ts | 10 ++++++++-- src/models/otp.ts | 18 ++++++++---------- src/models/storage.ts | 8 ++++---- tsconfig.json | 1 - 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/src/models/encryption.ts b/src/models/encryption.ts index df81368f5..70ecec698 100644 --- a/src/models/encryption.ts +++ b/src/models/encryption.ts @@ -1,6 +1,6 @@ -import * as CryptoJS from 'crypto-js'; +/// -export class Encription { +class Encription { private password: string; constructor(password: string) { diff --git a/src/models/interface.ts b/src/models/interface.ts index a53394e04..2ad6955df 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -1,19 +1,20 @@ -import {Encription} from './encryption'; +/// -export enum OTPType { +enum OTPType { totp = 1, hotp, battle, steam } -export interface OTP { +interface OTP { type: OTPType; index: number; issuer: string; secret: string; account: string; hash: string; + counter: number; create(encryption: Encription): Promise; update( encryption: Encription, issuer: string, account: string, index: number, @@ -23,7 +24,7 @@ export interface OTP { generate(): string; } -export interface OTPStorage { +interface OTPStorage { account: string; encrypted: boolean; hash: string; diff --git a/src/models/key-utilities.ts b/src/models/key-utilities.ts index 37f49dd52..c7b2a1605 100644 --- a/src/models/key-utilities.ts +++ b/src/models/key-utilities.ts @@ -3,9 +3,10 @@ // http://blog.tinisles.com/2011/10/google-authenticator-one-time-password-algorithm-in-javascript/ // Rewrite with TypeScript by Sneezry https://github.com/Sneezry -import jsSHA = require('jssha'); -export class KeyUtilities { +/// + +class KeyUtilities { private static dec2hex(s: number): string { return (s < 15.5 ? '0' : '') + Math.round(s).toString(16); } @@ -85,6 +86,11 @@ export class KeyUtilities { len = 10; b26 = true; } + + if (!key) { + throw new Error('Invalid secret key'); + } + if (counter === undefined) { let epoch = Math.round(new Date().getTime() / 1000.0); if (localStorage.offset) { diff --git a/src/models/otp.ts b/src/models/otp.ts index 7332c5f2a..544b631d3 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -1,11 +1,9 @@ -import * as CryptoJS from 'crypto-js'; +/// +/// +/// +/// -import {Encription} from './encryption'; -import {OTP, OTPType} from './interface'; -import {KeyUtilities} from './key-utilities'; -import {Storage} from './storage'; - -export class OTPEntry implements OTP { +class OTPEntry implements OTP { type: OTPType; index: number; issuer: string; @@ -27,7 +25,7 @@ export class OTPEntry implements OTP { } async create(encryption: Encription) { - await Storage.add(encryption, this); + await entryStorage.add(encryption, this); return; } @@ -38,12 +36,12 @@ export class OTPEntry implements OTP { this.account = account; this.index = index; this.counter = counter; - Storage.update(encryption, this); + entryStorage.update(encryption, this); return; } async delete() { - Storage.delete(this); + entryStorage.delete(this); return; } diff --git a/src/models/storage.ts b/src/models/storage.ts index aeb91d7c3..b42a279ae 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -1,8 +1,8 @@ -import {Encription} from './encryption'; -import {OTPStorage, OTPType} from './interface'; -import {OTPEntry} from './otp'; +/// +/// +/// -export class Storage { +class entryStorage { private static getOTPStorageFromEntry( encryption: Encription, entry: OTPEntry): OTPStorage { const storageItem: OTPStorage = { diff --git a/tsconfig.json b/tsconfig.json index 3152f3670..41fb4c57c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "extends": "./../../../../usr/local/lib/node_modules/gts/tsconfig-google.json", "compilerOptions": { - "module": "commonjs", "lib": ["es6", "dom"], "rootDir": "src", "outDir": "build" From c8475f9f41b6728538f4e86170b00d63851fd872 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 3 Feb 2018 16:03:15 +0800 Subject: [PATCH 006/178] save --- src/models/interface.ts | 3 ++- src/models/key-utilities.ts | 40 +++++++++++++++++++++---------------- src/models/otp.ts | 2 +- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/models/interface.ts b/src/models/interface.ts index 2ad6955df..c88ee3b31 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -4,7 +4,8 @@ enum OTPType { totp = 1, hotp, battle, - steam + steam, + hex } interface OTP { diff --git a/src/models/key-utilities.ts b/src/models/key-utilities.ts index c7b2a1605..df12af23d 100644 --- a/src/models/key-utilities.ts +++ b/src/models/key-utilities.ts @@ -5,6 +5,7 @@ // Rewrite with TypeScript by Sneezry https://github.com/Sneezry /// +/// class KeyUtilities { private static dec2hex(s: number): string { @@ -66,32 +67,37 @@ class KeyUtilities { return output; } - static generate(secret: string, counter?: number) { + static generate(type: OTPType, secret: string, counter: number) { secret = secret.replace(/\s/g, ''); let len = 6; let b26 = false; - let key = ''; - if (/^[a-z2-7]+=*$/.test(secret.toLowerCase())) { - key = this.base32tohex(secret); - } else if (/^[0-9a-f]+$/.test(secret.toLowerCase())) { - key = secret; - } else if (/^bliz\-/.test(secret.toLowerCase())) { - key = this.base32tohex(secret.substr(5)); - len = 8; - } else if (/^blz\-/.test(secret.toLowerCase())) { - key = this.base32tohex(secret.substr(4)); - len = 8; - } else if (/^stm\-/.test(secret.toLowerCase())) { - key = this.base32tohex(secret.substr(4)); - len = 10; - b26 = true; + let key: string; + switch(type) { + case OTPType.totp: + case OTPType.hotp: + key = this.base32tohex(secret); + break; + case OTPType.hex: + key = secret; + break; + case OTPType.battle: + key = this.base32tohex(secret.substr(5)); + len = 8; + break; + case OTPType.steam: + key = this.base32tohex(secret.substr(4)); + len = 10; + b26 = true; + break; + default: + key = this.base32tohex(secret); } if (!key) { throw new Error('Invalid secret key'); } - if (counter === undefined) { + if (type !== OTPType.hotp) { 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 544b631d3..1067529a4 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -56,6 +56,6 @@ class OTPEntry implements OTP { } generate() { - return KeyUtilities.generate(this.secret); + return KeyUtilities.generate(this.type, this.secret, this.counter); } } \ No newline at end of file From 6a79a81d064684bc057ac0eb16ac71acaeb147a6 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 3 Feb 2018 16:06:24 +0800 Subject: [PATCH 007/178] fix tslint style --- src/models/encryption.ts | 1 + src/models/interface.ts | 1 + src/models/key-utilities.ts | 3 ++- src/models/otp.ts | 7 ++++--- src/models/storage.ts | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/models/encryption.ts b/src/models/encryption.ts index 70ecec698..1dcc097cc 100644 --- a/src/models/encryption.ts +++ b/src/models/encryption.ts @@ -1,3 +1,4 @@ +/* tslint:disable:no-reference */ /// class Encription { diff --git a/src/models/interface.ts b/src/models/interface.ts index c88ee3b31..6cfb8a777 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -1,3 +1,4 @@ +/* tslint:disable:no-reference */ /// enum OTPType { diff --git a/src/models/key-utilities.ts b/src/models/key-utilities.ts index df12af23d..59b360f54 100644 --- a/src/models/key-utilities.ts +++ b/src/models/key-utilities.ts @@ -4,6 +4,7 @@ // Rewrite with TypeScript by Sneezry https://github.com/Sneezry +/* tslint:disable:no-reference */ /// /// @@ -72,7 +73,7 @@ class KeyUtilities { let len = 6; let b26 = false; let key: string; - switch(type) { + switch (type) { case OTPType.totp: case OTPType.hotp: key = this.base32tohex(secret); diff --git a/src/models/otp.ts b/src/models/otp.ts index 1067529a4..39ee0a284 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -1,3 +1,4 @@ +/* tslint:disable:no-reference */ /// /// /// @@ -25,7 +26,7 @@ class OTPEntry implements OTP { } async create(encryption: Encription) { - await entryStorage.add(encryption, this); + await EntryStorage.add(encryption, this); return; } @@ -36,12 +37,12 @@ class OTPEntry implements OTP { this.account = account; this.index = index; this.counter = counter; - entryStorage.update(encryption, this); + EntryStorage.update(encryption, this); return; } async delete() { - entryStorage.delete(this); + EntryStorage.delete(this); return; } diff --git a/src/models/storage.ts b/src/models/storage.ts index b42a279ae..4e2f29da3 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -1,8 +1,9 @@ +/* tslint:disable:no-reference */ /// /// /// -class entryStorage { +class EntryStorage { private static getOTPStorageFromEntry( encryption: Encription, entry: OTPEntry): OTPStorage { const storageItem: OTPStorage = { From bbb4cc2203adfb79cf05417c9a6bc1ab23778c6d Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sun, 4 Feb 2018 05:34:16 +0800 Subject: [PATCH 008/178] save --- .vscode/settings.json | 2 +- aes.js | 35 ++++++ css/popup.css | 10 +- manifest.json | 33 ++---- md5.js | 19 ++++ package-lock.json | 206 ++++++++++++++++++++++++++++-------- package.json | 5 +- popup.bak.html | 130 +++++++++++++++++++++++ popup.html | 38 +++++++ sha.js | 45 ++++++++ src/background.ts | 29 +++++ src/content.ts | 0 src/models/encryption.ts | 10 +- src/models/interface.ts | 8 +- src/models/key-utilities.ts | 6 +- src/models/otp.ts | 8 +- src/models/storage.ts | 84 +++++++++------ src/popup.ts | 62 +++++++++++ tsconfig.json | 2 + vue.min.js | 11 ++ 20 files changed, 632 insertions(+), 111 deletions(-) create mode 100644 aes.js create mode 100644 md5.js create mode 100644 popup.bak.html create mode 100644 popup.html create mode 100755 sha.js create mode 100644 src/background.ts create mode 100644 src/content.ts create mode 100644 src/popup.ts create mode 100644 vue.min.js diff --git a/.vscode/settings.json b/.vscode/settings.json index 68ebc15d9..ff30c4464 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "editor.tabSize": 4 + "editor.tabSize": 2 } \ No newline at end of file diff --git a/aes.js b/aes.js new file mode 100644 index 000000000..827503cbd --- /dev/null +++ b/aes.js @@ -0,0 +1,35 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +var CryptoJS=CryptoJS||function(u,p){var d={},l=d.lib={},s=function(){},t=l.Base={extend:function(a){s.prototype=this;var c=new s;a&&c.mixIn(a);c.hasOwnProperty("init")||(c.init=function(){c.$super.init.apply(this,arguments)});c.init.prototype=c;c.$super=this;return c},create:function(){var a=this.extend();a.init.apply(a,arguments);return a},init:function(){},mixIn:function(a){for(var c in a)a.hasOwnProperty(c)&&(this[c]=a[c]);a.hasOwnProperty("toString")&&(this.toString=a.toString)},clone:function(){return this.init.prototype.extend(this)}}, +r=l.WordArray=t.extend({init:function(a,c){a=this.words=a||[];this.sigBytes=c!=p?c:4*a.length},toString:function(a){return(a||v).stringify(this)},concat:function(a){var c=this.words,e=a.words,j=this.sigBytes;a=a.sigBytes;this.clamp();if(j%4)for(var k=0;k>>2]|=(e[k>>>2]>>>24-8*(k%4)&255)<<24-8*((j+k)%4);else if(65535>>2]=e[k>>>2];else c.push.apply(c,e);this.sigBytes+=a;return this},clamp:function(){var a=this.words,c=this.sigBytes;a[c>>>2]&=4294967295<< +32-8*(c%4);a.length=u.ceil(c/4)},clone:function(){var a=t.clone.call(this);a.words=this.words.slice(0);return a},random:function(a){for(var c=[],e=0;e>>2]>>>24-8*(j%4)&255;e.push((k>>>4).toString(16));e.push((k&15).toString(16))}return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>3]|=parseInt(a.substr(j, +2),16)<<24-4*(j%8);return new r.init(e,c/2)}},b=w.Latin1={stringify:function(a){var c=a.words;a=a.sigBytes;for(var e=[],j=0;j>>2]>>>24-8*(j%4)&255));return e.join("")},parse:function(a){for(var c=a.length,e=[],j=0;j>>2]|=(a.charCodeAt(j)&255)<<24-8*(j%4);return new r.init(e,c)}},x=w.Utf8={stringify:function(a){try{return decodeURIComponent(escape(b.stringify(a)))}catch(c){throw Error("Malformed UTF-8 data");}},parse:function(a){return b.parse(unescape(encodeURIComponent(a)))}}, +q=l.BufferedBlockAlgorithm=t.extend({reset:function(){this._data=new r.init;this._nDataBytes=0},_append:function(a){"string"==typeof a&&(a=x.parse(a));this._data.concat(a);this._nDataBytes+=a.sigBytes},_process:function(a){var c=this._data,e=c.words,j=c.sigBytes,k=this.blockSize,b=j/(4*k),b=a?u.ceil(b):u.max((b|0)-this._minBufferSize,0);a=b*k;j=u.min(4*a,j);if(a){for(var q=0;q>>2]>>>24-8*(r%4)&255)<<16|(l[r+1>>>2]>>>24-8*((r+1)%4)&255)<<8|l[r+2>>>2]>>>24-8*((r+2)%4)&255,v=0;4>v&&r+0.75*v>>6*(3-v)&63));if(l=t.charAt(64))for(;d.length%4;)d.push(l);return d.join("")},parse:function(d){var l=d.length,s=this._map,t=s.charAt(64);t&&(t=d.indexOf(t),-1!=t&&(l=t));for(var t=[],r=0,w=0;w< +l;w++)if(w%4){var v=s.indexOf(d.charAt(w-1))<<2*(w%4),b=s.indexOf(d.charAt(w))>>>6-2*(w%4);t[r>>>2]|=(v|b)<<24-8*(r%4);r++}return p.create(t,r)},_map:"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="}})(); +(function(u){function p(b,n,a,c,e,j,k){b=b+(n&a|~n&c)+e+k;return(b<>>32-j)+n}function d(b,n,a,c,e,j,k){b=b+(n&c|a&~c)+e+k;return(b<>>32-j)+n}function l(b,n,a,c,e,j,k){b=b+(n^a^c)+e+k;return(b<>>32-j)+n}function s(b,n,a,c,e,j,k){b=b+(a^(n|~c))+e+k;return(b<>>32-j)+n}for(var t=CryptoJS,r=t.lib,w=r.WordArray,v=r.Hasher,r=t.algo,b=[],x=0;64>x;x++)b[x]=4294967296*u.abs(u.sin(x+1))|0;r=r.MD5=v.extend({_doReset:function(){this._hash=new w.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(q,n){for(var a=0;16>a;a++){var c=n+a,e=q[c];q[c]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360}var a=this._hash.words,c=q[n+0],e=q[n+1],j=q[n+2],k=q[n+3],z=q[n+4],r=q[n+5],t=q[n+6],w=q[n+7],v=q[n+8],A=q[n+9],B=q[n+10],C=q[n+11],u=q[n+12],D=q[n+13],E=q[n+14],x=q[n+15],f=a[0],m=a[1],g=a[2],h=a[3],f=p(f,m,g,h,c,7,b[0]),h=p(h,f,m,g,e,12,b[1]),g=p(g,h,f,m,j,17,b[2]),m=p(m,g,h,f,k,22,b[3]),f=p(f,m,g,h,z,7,b[4]),h=p(h,f,m,g,r,12,b[5]),g=p(g,h,f,m,t,17,b[6]),m=p(m,g,h,f,w,22,b[7]), +f=p(f,m,g,h,v,7,b[8]),h=p(h,f,m,g,A,12,b[9]),g=p(g,h,f,m,B,17,b[10]),m=p(m,g,h,f,C,22,b[11]),f=p(f,m,g,h,u,7,b[12]),h=p(h,f,m,g,D,12,b[13]),g=p(g,h,f,m,E,17,b[14]),m=p(m,g,h,f,x,22,b[15]),f=d(f,m,g,h,e,5,b[16]),h=d(h,f,m,g,t,9,b[17]),g=d(g,h,f,m,C,14,b[18]),m=d(m,g,h,f,c,20,b[19]),f=d(f,m,g,h,r,5,b[20]),h=d(h,f,m,g,B,9,b[21]),g=d(g,h,f,m,x,14,b[22]),m=d(m,g,h,f,z,20,b[23]),f=d(f,m,g,h,A,5,b[24]),h=d(h,f,m,g,E,9,b[25]),g=d(g,h,f,m,k,14,b[26]),m=d(m,g,h,f,v,20,b[27]),f=d(f,m,g,h,D,5,b[28]),h=d(h,f, +m,g,j,9,b[29]),g=d(g,h,f,m,w,14,b[30]),m=d(m,g,h,f,u,20,b[31]),f=l(f,m,g,h,r,4,b[32]),h=l(h,f,m,g,v,11,b[33]),g=l(g,h,f,m,C,16,b[34]),m=l(m,g,h,f,E,23,b[35]),f=l(f,m,g,h,e,4,b[36]),h=l(h,f,m,g,z,11,b[37]),g=l(g,h,f,m,w,16,b[38]),m=l(m,g,h,f,B,23,b[39]),f=l(f,m,g,h,D,4,b[40]),h=l(h,f,m,g,c,11,b[41]),g=l(g,h,f,m,k,16,b[42]),m=l(m,g,h,f,t,23,b[43]),f=l(f,m,g,h,A,4,b[44]),h=l(h,f,m,g,u,11,b[45]),g=l(g,h,f,m,x,16,b[46]),m=l(m,g,h,f,j,23,b[47]),f=s(f,m,g,h,c,6,b[48]),h=s(h,f,m,g,w,10,b[49]),g=s(g,h,f,m, +E,15,b[50]),m=s(m,g,h,f,r,21,b[51]),f=s(f,m,g,h,u,6,b[52]),h=s(h,f,m,g,k,10,b[53]),g=s(g,h,f,m,B,15,b[54]),m=s(m,g,h,f,e,21,b[55]),f=s(f,m,g,h,v,6,b[56]),h=s(h,f,m,g,x,10,b[57]),g=s(g,h,f,m,t,15,b[58]),m=s(m,g,h,f,D,21,b[59]),f=s(f,m,g,h,z,6,b[60]),h=s(h,f,m,g,C,10,b[61]),g=s(g,h,f,m,j,15,b[62]),m=s(m,g,h,f,A,21,b[63]);a[0]=a[0]+f|0;a[1]=a[1]+m|0;a[2]=a[2]+g|0;a[3]=a[3]+h|0},_doFinalize:function(){var b=this._data,n=b.words,a=8*this._nDataBytes,c=8*b.sigBytes;n[c>>>5]|=128<<24-c%32;var e=u.floor(a/ +4294967296);n[(c+64>>>9<<4)+15]=(e<<8|e>>>24)&16711935|(e<<24|e>>>8)&4278255360;n[(c+64>>>9<<4)+14]=(a<<8|a>>>24)&16711935|(a<<24|a>>>8)&4278255360;b.sigBytes=4*(n.length+1);this._process();b=this._hash;n=b.words;for(a=0;4>a;a++)c=n[a],n[a]=(c<<8|c>>>24)&16711935|(c<<24|c>>>8)&4278255360;return b},clone:function(){var b=v.clone.call(this);b._hash=this._hash.clone();return b}});t.MD5=v._createHelper(r);t.HmacMD5=v._createHmacHelper(r)})(Math); +(function(){var u=CryptoJS,p=u.lib,d=p.Base,l=p.WordArray,p=u.algo,s=p.EvpKDF=d.extend({cfg:d.extend({keySize:4,hasher:p.MD5,iterations:1}),init:function(d){this.cfg=this.cfg.extend(d)},compute:function(d,r){for(var p=this.cfg,s=p.hasher.create(),b=l.create(),u=b.words,q=p.keySize,p=p.iterations;u.length>>2]&255}};d.BlockCipher=v.extend({cfg:v.cfg.extend({mode:b,padding:q}),reset:function(){v.reset.call(this);var a=this.cfg,b=a.iv,a=a.mode;if(this._xformMode==this._ENC_XFORM_MODE)var c=a.createEncryptor;else c=a.createDecryptor,this._minBufferSize=1;this._mode=c.call(a, +this,b&&b.words)},_doProcessBlock:function(a,b){this._mode.processBlock(a,b)},_doFinalize:function(){var a=this.cfg.padding;if(this._xformMode==this._ENC_XFORM_MODE){a.pad(this._data,this.blockSize);var b=this._process(!0)}else b=this._process(!0),a.unpad(b);return b},blockSize:4});var n=d.CipherParams=l.extend({init:function(a){this.mixIn(a)},toString:function(a){return(a||this.formatter).stringify(this)}}),b=(p.format={}).OpenSSL={stringify:function(a){var b=a.ciphertext;a=a.salt;return(a?s.create([1398893684, +1701076831]).concat(a).concat(b):b).toString(r)},parse:function(a){a=r.parse(a);var b=a.words;if(1398893684==b[0]&&1701076831==b[1]){var c=s.create(b.slice(2,4));b.splice(0,4);a.sigBytes-=16}return n.create({ciphertext:a,salt:c})}},a=d.SerializableCipher=l.extend({cfg:l.extend({format:b}),encrypt:function(a,b,c,d){d=this.cfg.extend(d);var l=a.createEncryptor(c,d);b=l.finalize(b);l=l.cfg;return n.create({ciphertext:b,key:c,iv:l.iv,algorithm:a,mode:l.mode,padding:l.padding,blockSize:a.blockSize,formatter:d.format})}, +decrypt:function(a,b,c,d){d=this.cfg.extend(d);b=this._parse(b,d.format);return a.createDecryptor(c,d).finalize(b.ciphertext)},_parse:function(a,b){return"string"==typeof a?b.parse(a,this):a}}),p=(p.kdf={}).OpenSSL={execute:function(a,b,c,d){d||(d=s.random(8));a=w.create({keySize:b+c}).compute(a,d);c=s.create(a.words.slice(b),4*c);a.sigBytes=4*b;return n.create({key:a,iv:c,salt:d})}},c=d.PasswordBasedCipher=a.extend({cfg:a.cfg.extend({kdf:p}),encrypt:function(b,c,d,l){l=this.cfg.extend(l);d=l.kdf.execute(d, +b.keySize,b.ivSize);l.iv=d.iv;b=a.encrypt.call(this,b,c,d.key,l);b.mixIn(d);return b},decrypt:function(b,c,d,l){l=this.cfg.extend(l);c=this._parse(c,l.format);d=l.kdf.execute(d,b.keySize,b.ivSize,c.salt);l.iv=d.iv;return a.decrypt.call(this,b,c,d.key,l)}})}(); +(function(){for(var u=CryptoJS,p=u.lib.BlockCipher,d=u.algo,l=[],s=[],t=[],r=[],w=[],v=[],b=[],x=[],q=[],n=[],a=[],c=0;256>c;c++)a[c]=128>c?c<<1:c<<1^283;for(var e=0,j=0,c=0;256>c;c++){var k=j^j<<1^j<<2^j<<3^j<<4,k=k>>>8^k&255^99;l[e]=k;s[k]=e;var z=a[e],F=a[z],G=a[F],y=257*a[k]^16843008*k;t[e]=y<<24|y>>>8;r[e]=y<<16|y>>>16;w[e]=y<<8|y>>>24;v[e]=y;y=16843009*G^65537*F^257*z^16843008*e;b[k]=y<<24|y>>>8;x[k]=y<<16|y>>>16;q[k]=y<<8|y>>>24;n[k]=y;e?(e=z^a[a[a[G^z]]],j^=a[a[j]]):e=j=1}var H=[0,1,2,4,8, +16,32,64,128,27,54],d=d.AES=p.extend({_doReset:function(){for(var a=this._key,c=a.words,d=a.sigBytes/4,a=4*((this._nRounds=d+6)+1),e=this._keySchedule=[],j=0;j>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255]):(k=k<<8|k>>>24,k=l[k>>>24]<<24|l[k>>>16&255]<<16|l[k>>>8&255]<<8|l[k&255],k^=H[j/d|0]<<24);e[j]=e[j-d]^k}c=this._invKeySchedule=[];for(d=0;dd||4>=j?k:b[l[k>>>24]]^x[l[k>>>16&255]]^q[l[k>>> +8&255]]^n[l[k&255]]},encryptBlock:function(a,b){this._doCryptBlock(a,b,this._keySchedule,t,r,w,v,l)},decryptBlock:function(a,c){var d=a[c+1];a[c+1]=a[c+3];a[c+3]=d;this._doCryptBlock(a,c,this._invKeySchedule,b,x,q,n,s);d=a[c+1];a[c+1]=a[c+3];a[c+3]=d},_doCryptBlock:function(a,b,c,d,e,j,l,f){for(var m=this._nRounds,g=a[b]^c[0],h=a[b+1]^c[1],k=a[b+2]^c[2],n=a[b+3]^c[3],p=4,r=1;r>>24]^e[h>>>16&255]^j[k>>>8&255]^l[n&255]^c[p++],s=d[h>>>24]^e[k>>>16&255]^j[n>>>8&255]^l[g&255]^c[p++],t= +d[k>>>24]^e[n>>>16&255]^j[g>>>8&255]^l[h&255]^c[p++],n=d[n>>>24]^e[g>>>16&255]^j[h>>>8&255]^l[k&255]^c[p++],g=q,h=s,k=t;q=(f[g>>>24]<<24|f[h>>>16&255]<<16|f[k>>>8&255]<<8|f[n&255])^c[p++];s=(f[h>>>24]<<24|f[k>>>16&255]<<16|f[n>>>8&255]<<8|f[g&255])^c[p++];t=(f[k>>>24]<<24|f[n>>>16&255]<<16|f[g>>>8&255]<<8|f[h&255])^c[p++];n=(f[n>>>24]<<24|f[g>>>16&255]<<16|f[h>>>8&255]<<8|f[k&255])^c[p++];a[b]=q;a[b+1]=s;a[b+2]=t;a[b+3]=n},keySize:8});u.AES=p._createHelper(d)})(); diff --git a/css/popup.css b/css/popup.css index 440ad0199..f3e5e62a2 100644 --- a/css/popup.css +++ b/css/popup.css @@ -95,6 +95,8 @@ } } +[v-cloak] { display: none } + * { margin: 0; padding: 0; @@ -174,7 +176,7 @@ body { top: -1000px; } -.codeBox { +.entry { margin: 10px; margin-right: 0; padding: 10px; @@ -184,7 +186,7 @@ body { position: relative; } -.codeBox[unencrypted="true"] .warning { +.entry[unencrypted="true"] .warning { position: absolute; height: 0; line-height: 12px; @@ -202,11 +204,11 @@ body { -webkit-transition: height 0.2s; } -#codes:not(.edit) .codeBox[unencrypted="true"]:hover .warning { +#codes:not(.edit) .entry[unencrypted="true"]:hover .warning { height: 24px; } -.codeBox[dropOver="true"] { +.entry[dropOver="true"] { border: gray 1px dashed; } diff --git a/manifest.json b/manifest.json index 4fb4d9462..70c5436eb 100644 --- a/manifest.json +++ b/manifest.json @@ -20,34 +20,23 @@ }, "background": { "scripts": [ - "assist/aes.js", - "assist/md5.js", - "jsqrcode/grid.js", - "jsqrcode/version.js", - "jsqrcode/detector.js", - "jsqrcode/formatinf.js", - "jsqrcode/errorlevel.js", - "jsqrcode/bitmat.js", - "jsqrcode/datablock.js", - "jsqrcode/bmparser.js", - "jsqrcode/datamask.js", - "jsqrcode/rsdecoder.js", - "jsqrcode/gf256poly.js", - "jsqrcode/gf256.js", - "jsqrcode/decoder.js", - "jsqrcode/qrcode.js", - "jsqrcode/findpat.js", - "jsqrcode/alignpat.js", - "jsqrcode/databr.js", - "javascript/background.js" + "aes.js", + "md5.js", + "sha.js", + "build/models/encryption.js", + "build/models/interface.js", + "build/models/key-utilities.js", + "build/models/otp.js", + "build/models/storage.js", + "build/background.js" ], "persistent": false }, "content_scripts": [ { "matches": [""], - "css": ["css/content.css"], - "js": ["javascript/content.js"] + "css": [], + "js": ["build/content.js"] } ], "permissions": [ diff --git a/md5.js b/md5.js new file mode 100644 index 000000000..0fae5ca1d --- /dev/null +++ b/md5.js @@ -0,0 +1,19 @@ +/* +CryptoJS v3.1.2 +code.google.com/p/crypto-js +(c) 2009-2013 by Jeff Mott. All rights reserved. +code.google.com/p/crypto-js/wiki/License +*/ +var CryptoJS=CryptoJS||function(s,p){var m={},l=m.lib={},n=function(){},r=l.Base={extend:function(b){n.prototype=this;var h=new n;b&&h.mixIn(b);h.hasOwnProperty("init")||(h.init=function(){h.$super.init.apply(this,arguments)});h.init.prototype=h;h.$super=this;return h},create:function(){var b=this.extend();b.init.apply(b,arguments);return b},init:function(){},mixIn:function(b){for(var h in b)b.hasOwnProperty(h)&&(this[h]=b[h]);b.hasOwnProperty("toString")&&(this.toString=b.toString)},clone:function(){return this.init.prototype.extend(this)}}, +q=l.WordArray=r.extend({init:function(b,h){b=this.words=b||[];this.sigBytes=h!=p?h:4*b.length},toString:function(b){return(b||t).stringify(this)},concat:function(b){var h=this.words,a=b.words,j=this.sigBytes;b=b.sigBytes;this.clamp();if(j%4)for(var g=0;g>>2]|=(a[g>>>2]>>>24-8*(g%4)&255)<<24-8*((j+g)%4);else if(65535>>2]=a[g>>>2];else h.push.apply(h,a);this.sigBytes+=b;return this},clamp:function(){var b=this.words,h=this.sigBytes;b[h>>>2]&=4294967295<< +32-8*(h%4);b.length=s.ceil(h/4)},clone:function(){var b=r.clone.call(this);b.words=this.words.slice(0);return b},random:function(b){for(var h=[],a=0;a>>2]>>>24-8*(j%4)&255;g.push((k>>>4).toString(16));g.push((k&15).toString(16))}return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j>>3]|=parseInt(b.substr(j, +2),16)<<24-4*(j%8);return new q.init(g,a/2)}},a=v.Latin1={stringify:function(b){var a=b.words;b=b.sigBytes;for(var g=[],j=0;j>>2]>>>24-8*(j%4)&255));return g.join("")},parse:function(b){for(var a=b.length,g=[],j=0;j>>2]|=(b.charCodeAt(j)&255)<<24-8*(j%4);return new q.init(g,a)}},u=v.Utf8={stringify:function(b){try{return decodeURIComponent(escape(a.stringify(b)))}catch(g){throw Error("Malformed UTF-8 data");}},parse:function(b){return a.parse(unescape(encodeURIComponent(b)))}}, +g=l.BufferedBlockAlgorithm=r.extend({reset:function(){this._data=new q.init;this._nDataBytes=0},_append:function(b){"string"==typeof b&&(b=u.parse(b));this._data.concat(b);this._nDataBytes+=b.sigBytes},_process:function(b){var a=this._data,g=a.words,j=a.sigBytes,k=this.blockSize,m=j/(4*k),m=b?s.ceil(m):s.max((m|0)-this._minBufferSize,0);b=m*k;j=s.min(4*b,j);if(b){for(var l=0;l>>32-j)+k}function m(a,k,b,h,l,j,m){a=a+(k&h|b&~h)+l+m;return(a<>>32-j)+k}function l(a,k,b,h,l,j,m){a=a+(k^b^h)+l+m;return(a<>>32-j)+k}function n(a,k,b,h,l,j,m){a=a+(b^(k|~h))+l+m;return(a<>>32-j)+k}for(var r=CryptoJS,q=r.lib,v=q.WordArray,t=q.Hasher,q=r.algo,a=[],u=0;64>u;u++)a[u]=4294967296*s.abs(s.sin(u+1))|0;q=q.MD5=t.extend({_doReset:function(){this._hash=new v.init([1732584193,4023233417,2562383102,271733878])}, +_doProcessBlock:function(g,k){for(var b=0;16>b;b++){var h=k+b,w=g[h];g[h]=(w<<8|w>>>24)&16711935|(w<<24|w>>>8)&4278255360}var b=this._hash.words,h=g[k+0],w=g[k+1],j=g[k+2],q=g[k+3],r=g[k+4],s=g[k+5],t=g[k+6],u=g[k+7],v=g[k+8],x=g[k+9],y=g[k+10],z=g[k+11],A=g[k+12],B=g[k+13],C=g[k+14],D=g[k+15],c=b[0],d=b[1],e=b[2],f=b[3],c=p(c,d,e,f,h,7,a[0]),f=p(f,c,d,e,w,12,a[1]),e=p(e,f,c,d,j,17,a[2]),d=p(d,e,f,c,q,22,a[3]),c=p(c,d,e,f,r,7,a[4]),f=p(f,c,d,e,s,12,a[5]),e=p(e,f,c,d,t,17,a[6]),d=p(d,e,f,c,u,22,a[7]), +c=p(c,d,e,f,v,7,a[8]),f=p(f,c,d,e,x,12,a[9]),e=p(e,f,c,d,y,17,a[10]),d=p(d,e,f,c,z,22,a[11]),c=p(c,d,e,f,A,7,a[12]),f=p(f,c,d,e,B,12,a[13]),e=p(e,f,c,d,C,17,a[14]),d=p(d,e,f,c,D,22,a[15]),c=m(c,d,e,f,w,5,a[16]),f=m(f,c,d,e,t,9,a[17]),e=m(e,f,c,d,z,14,a[18]),d=m(d,e,f,c,h,20,a[19]),c=m(c,d,e,f,s,5,a[20]),f=m(f,c,d,e,y,9,a[21]),e=m(e,f,c,d,D,14,a[22]),d=m(d,e,f,c,r,20,a[23]),c=m(c,d,e,f,x,5,a[24]),f=m(f,c,d,e,C,9,a[25]),e=m(e,f,c,d,q,14,a[26]),d=m(d,e,f,c,v,20,a[27]),c=m(c,d,e,f,B,5,a[28]),f=m(f,c, +d,e,j,9,a[29]),e=m(e,f,c,d,u,14,a[30]),d=m(d,e,f,c,A,20,a[31]),c=l(c,d,e,f,s,4,a[32]),f=l(f,c,d,e,v,11,a[33]),e=l(e,f,c,d,z,16,a[34]),d=l(d,e,f,c,C,23,a[35]),c=l(c,d,e,f,w,4,a[36]),f=l(f,c,d,e,r,11,a[37]),e=l(e,f,c,d,u,16,a[38]),d=l(d,e,f,c,y,23,a[39]),c=l(c,d,e,f,B,4,a[40]),f=l(f,c,d,e,h,11,a[41]),e=l(e,f,c,d,q,16,a[42]),d=l(d,e,f,c,t,23,a[43]),c=l(c,d,e,f,x,4,a[44]),f=l(f,c,d,e,A,11,a[45]),e=l(e,f,c,d,D,16,a[46]),d=l(d,e,f,c,j,23,a[47]),c=n(c,d,e,f,h,6,a[48]),f=n(f,c,d,e,u,10,a[49]),e=n(e,f,c,d, +C,15,a[50]),d=n(d,e,f,c,s,21,a[51]),c=n(c,d,e,f,A,6,a[52]),f=n(f,c,d,e,q,10,a[53]),e=n(e,f,c,d,y,15,a[54]),d=n(d,e,f,c,w,21,a[55]),c=n(c,d,e,f,v,6,a[56]),f=n(f,c,d,e,D,10,a[57]),e=n(e,f,c,d,t,15,a[58]),d=n(d,e,f,c,B,21,a[59]),c=n(c,d,e,f,r,6,a[60]),f=n(f,c,d,e,z,10,a[61]),e=n(e,f,c,d,j,15,a[62]),d=n(d,e,f,c,x,21,a[63]);b[0]=b[0]+c|0;b[1]=b[1]+d|0;b[2]=b[2]+e|0;b[3]=b[3]+f|0},_doFinalize:function(){var a=this._data,k=a.words,b=8*this._nDataBytes,h=8*a.sigBytes;k[h>>>5]|=128<<24-h%32;var l=s.floor(b/ +4294967296);k[(h+64>>>9<<4)+15]=(l<<8|l>>>24)&16711935|(l<<24|l>>>8)&4278255360;k[(h+64>>>9<<4)+14]=(b<<8|b>>>24)&16711935|(b<<24|b>>>8)&4278255360;a.sigBytes=4*(k.length+1);this._process();a=this._hash;k=a.words;for(b=0;4>b;b++)h=k[b],k[b]=(h<<8|h>>>24)&16711935|(h<<24|h>>>8)&4278255360;return a},clone:function(){var a=t.clone.call(this);a._hash=this._hash.clone();return a}});r.MD5=t._createHelper(q);r.HmacMD5=t._createHmacHelper(q)})(Math); diff --git a/package-lock.json b/package-lock.json index 356d89b1e..fc87f7bc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,14 +40,15 @@ "integrity": "sha1-leg9uph4f/eW0tXzehklq/Qbycs=", "dev": true }, - "@types/vue": { - "version": "2.0.0", - "resolved": "http://registry.npm.taobao.org/@types/vue/download/@types/vue-2.0.0.tgz", - "integrity": "sha1-7Hez2JWR3rnKXLBSNoqpwyvgiOc=", - "dev": true, - "requires": { - "vue": "2.5.13" - } + "acorn": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==" + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, "ansi-align": { "version": "2.0.0", @@ -91,6 +92,11 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, + "ast-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", + "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=" + }, "async": { "version": "1.5.2", "resolved": "http://registry.npm.taobao.org/async/download/async-1.5.2.tgz", @@ -153,8 +159,12 @@ "balanced-match": { "version": "1.0.0", "resolved": "http://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base62": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/base62/-/base62-1.2.1.tgz", + "integrity": "sha512-xVtfFHNPUzpCNHygpXFGMlDk3saxXLQcOOQzAAk6ibvlAHgT6WKXLv9rMFhcyEK1n9LuDmp/LxyGW/Fm9L8++g==" }, "boxen": { "version": "1.3.0", @@ -175,7 +185,6 @@ "version": "1.1.8", "resolved": "http://registry.npm.taobao.org/brace-expansion/download/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", - "dev": true, "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -277,14 +286,42 @@ "commander": { "version": "2.12.2", "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.12.2.tgz", - "integrity": "sha1-D1lGxCftnsDZGka7ne9T5UZQ5VU=", - "dev": true + "integrity": "sha1-D1lGxCftnsDZGka7ne9T5UZQ5VU=" + }, + "commoner": { + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/commoner/-/commoner-0.10.8.tgz", + "integrity": "sha1-NPw2cs0kOT6LtH5wyqApOBH08sU=", + "requires": { + "commander": "2.12.2", + "detective": "4.7.1", + "glob": "5.0.15", + "graceful-fs": "4.1.11", + "iconv-lite": "0.4.19", + "mkdirp": "0.5.1", + "private": "0.1.8", + "q": "1.5.1", + "recast": "0.11.23" + }, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "requires": { + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + } + } }, "concat-map": { "version": "0.0.1", "resolved": "http://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "configstore": { "version": "3.1.1", @@ -320,11 +357,6 @@ "which": "1.3.0" } }, - "crypto-js": { - "version": "3.1.9-1", - "resolved": "http://registry.npm.taobao.org/crypto-js/download/crypto-js-3.1.9-1.tgz", - "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" - }, "crypto-random-string": { "version": "1.0.0", "resolved": "http://registry.npm.taobao.org/crypto-random-string/download/crypto-random-string-1.0.0.tgz", @@ -370,6 +402,20 @@ "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", "dev": true }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "detective": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", + "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", + "requires": { + "acorn": "5.4.1", + "defined": "1.0.0" + } + }, "diff": { "version": "3.4.0", "resolved": "http://registry.npm.taobao.org/diff/download/diff-3.4.0.tgz", @@ -391,6 +437,15 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, + "envify": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/envify/-/envify-3.4.1.tgz", + "integrity": "sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg=", + "requires": { + "jstransform": "11.0.3", + "through": "2.3.8" + } + }, "error-ex": { "version": "1.3.1", "resolved": "http://registry.npm.taobao.org/error-ex/download/error-ex-1.3.1.tgz", @@ -406,6 +461,11 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "esprima-fb": { + "version": "15001.1.0-dev-harmony-fb", + "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz", + "integrity": "sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE=" + }, "esutils": { "version": "2.0.2", "resolved": "http://registry.npm.taobao.org/esutils/download/esutils-2.0.2.tgz", @@ -513,8 +573,7 @@ "graceful-fs": { "version": "4.1.11", "resolved": "http://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", - "dev": true + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, "gts": { "version": "0.5.2", @@ -565,8 +624,7 @@ "iconv-lite": { "version": "0.4.19", "resolved": "http://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.19.tgz", - "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=", - "dev": true + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=" }, "import-lazy": { "version": "2.1.0", @@ -590,7 +648,6 @@ "version": "1.0.6", "resolved": "http://registry.npm.taobao.org/inflight/download/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -599,8 +656,7 @@ "inherits": { "version": "2.0.3", "resolved": "http://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -730,10 +786,17 @@ "integrity": "sha1-UBg80bLSUnXeBp6ecbRnrJ6rlzo=", "dev": true }, - "jssha": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/jssha/-/jssha-2.3.1.tgz", - "integrity": "sha1-FHshJTaQNcpLL30hDcU58Amz3po=" + "jstransform": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/jstransform/-/jstransform-11.0.3.tgz", + "integrity": "sha1-CaeJk+CuTU70SH9hVakfYZDLQiM=", + "requires": { + "base62": "1.2.1", + "commoner": "0.10.8", + "esprima-fb": "15001.1.0-dev-harmony-fb", + "object-assign": "2.1.1", + "source-map": "0.4.4" + } }, "latest-version": { "version": "3.1.0", @@ -840,7 +903,6 @@ "version": "3.0.4", "resolved": "http://registry.npm.taobao.org/minimatch/download/minimatch-3.0.4.tgz", "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", - "dev": true, "requires": { "brace-expansion": "1.1.8" } @@ -861,6 +923,21 @@ "is-plain-obj": "1.1.0" } }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, "mute-stream": { "version": "0.0.7", "resolved": "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.7.tgz", @@ -888,11 +965,15 @@ "path-key": "2.0.1" } }, + "object-assign": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", + "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=" + }, "once": { "version": "1.4.0", "resolved": "http://registry.npm.taobao.org/once/download/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1.0.2" } @@ -964,8 +1045,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "http://registry.npm.taobao.org/path-is-absolute/download/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -1006,12 +1086,22 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, + "private": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", + "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" + }, "pseudomap": { "version": "1.0.2", "resolved": "http://registry.npm.taobao.org/pseudomap/download/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, "quick-lru": { "version": "1.1.0", "resolved": "http://registry.npm.taobao.org/quick-lru/download/quick-lru-1.1.0.tgz", @@ -1051,6 +1141,29 @@ "read-pkg": "3.0.0" } }, + "recast": { + "version": "0.11.23", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", + "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", + "requires": { + "ast-types": "0.9.6", + "esprima": "3.1.3", + "private": "0.1.8", + "source-map": "0.5.7" + }, + "dependencies": { + "esprima": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", + "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + } + } + }, "redent": { "version": "2.0.0", "resolved": "http://registry.npm.taobao.org/redent/download/redent-2.0.0.tgz", @@ -1174,6 +1287,14 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": "1.0.1" + } + }, "spdx-correct": { "version": "1.0.2", "resolved": "http://registry.npm.taobao.org/spdx-correct/download/spdx-correct-1.0.2.tgz", @@ -1259,8 +1380,7 @@ "through": { "version": "2.3.8", "resolved": "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "timed-out": { "version": "4.0.1", @@ -1375,9 +1495,12 @@ } }, "vue": { - "version": "2.5.13", - "resolved": "http://registry.npm.taobao.org/vue/download/vue-2.5.13.tgz", - "integrity": "sha1-lb0x4g7896fzkjnJqmeHzoz1eOE=" + "version": "1.0.28-csp", + "resolved": "https://registry.npmjs.org/vue/-/vue-1.0.28-csp.tgz", + "integrity": "sha1-AoFNUC7/Pk77ahK4gvvztV8eLx4=", + "requires": { + "envify": "3.4.1" + } }, "which": { "version": "1.3.0", @@ -1400,8 +1523,7 @@ "wrappy": { "version": "1.0.2", "resolved": "http://registry.npm.taobao.org/wrappy/download/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write-file-atomic": { "version": "2.3.0", diff --git a/package.json b/package.json index b9b270b96..6e052be71 100644 --- a/package.json +++ b/package.json @@ -26,13 +26,10 @@ "@types/chrome": "^0.0.59", "@types/crypto-js": "^3.1.38", "@types/jssha": "0.0.29", - "@types/vue": "^2.0.0", "gts": "^0.5.2", "typescript": "^2.6.1" }, "dependencies": { - "crypto-js": "^3.1.9-1", - "jssha": "^2.3.1", - "vue": "^2.5.13" + "vue": "^1.0.28-csp" } } diff --git a/popup.bak.html b/popup.bak.html new file mode 100644 index 000000000..1ed902c92 --- /dev/null +++ b/popup.bak.html @@ -0,0 +1,130 @@ + + + + + + + + + +

+ +
+
Battlenet
68667399
DigitalOcean
897222
Slack
924170
GitLab
500089
GitHub
815823
www.lakebtc.com
352408
yunbi.com
976894
huobi
391281
Bitstamp
049350
GateHub
793891
V2EX
644612
BigONE
692926
+
+
+
+ +
+
+
+
+
+
+
+
Scan QR Code
+
Manual Entry
+
+ + + + +
+ + +
+
+ + +
+
OK
+
+
+
+
+
+
This passphrase will be used to encrypt your secrets. No one can help you if you forget the passphrase.
+ + + + +
+ + +
+
OK
+
+
+
+
Input passphrase to decrypt account data.
+ + +
+ + +
+
OK
+
+
+
+
+ +
Update
+
+
+ +
+
+ + +
OK
+
+ +
+
+
OK
+
+
+ + + + + + + + \ No newline at end of file diff --git a/popup.html b/popup.html new file mode 100644 index 000000000..f53a15113 --- /dev/null +++ b/popup.html @@ -0,0 +1,38 @@ + + + + + + + + + +
+ +
+
+
+
+
{{ entry.issuer }}
+
+ +
+
{{ entry.code }}
+ +
+ +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/sha.js b/sha.js new file mode 100755 index 000000000..af212fba0 --- /dev/null +++ b/sha.js @@ -0,0 +1,45 @@ +/* + A JavaScript implementation of the SHA family of hashes, as + defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding + HMAC implementation as defined in FIPS PUB 198a + + Copyright Brian Turek 2008-2017 + Distributed under the BSD License + See http://caligatio.github.com/jsSHA/ for more information + + Several functions taken from Paul Johnston +*/ +'use strict';(function(Y){function C(c,a,b){var e=0,h=[],n=0,g,l,d,f,m,q,u,r,I=!1,v=[],w=[],t,y=!1,z=!1,x=-1;b=b||{};g=b.encoding||"UTF8";t=b.numRounds||1;if(t!==parseInt(t,10)||1>t)throw Error("numRounds must a integer >= 1");if("SHA-1"===c)m=512,q=K,u=Z,f=160,r=function(a){return a.slice()};else if(0===c.lastIndexOf("SHA-",0))if(q=function(a,b){return L(a,b,c)},u=function(a,b,h,e){var k,f;if("SHA-224"===c||"SHA-256"===c)k=(b+65>>>9<<4)+15,f=16;else if("SHA-384"===c||"SHA-512"===c)k=(b+129>>>10<< +5)+31,f=32;else throw Error("Unexpected error in SHA-2 implementation");for(;a.length<=k;)a.push(0);a[b>>>5]|=128<<24-b%32;b=b+h;a[k]=b&4294967295;a[k-1]=b/4294967296|0;h=a.length;for(b=0;be;e+=1)c[e]=a[e].slice();return c};x=1;if("SHA3-224"=== +c)m=1152,f=224;else if("SHA3-256"===c)m=1088,f=256;else if("SHA3-384"===c)m=832,f=384;else if("SHA3-512"===c)m=576,f=512;else if("SHAKE128"===c)m=1344,f=-1,F=31,z=!0;else if("SHAKE256"===c)m=1088,f=-1,F=31,z=!0;else throw Error("Chosen SHA variant is not supported");u=function(a,c,e,b,h){e=m;var k=F,f,g=[],n=e>>>5,l=0,d=c>>>5;for(f=0;f=e;f+=n)b=D(a.slice(f,f+n),b),c-=e;a=a.slice(f);for(c%=e;a.length>>3;a[f>>2]^=k<=h)break;g.push(a.a);l+=1;0===64*l%e&&D(null,b)}return g}}else throw Error("Chosen SHA variant is not supported");d=M(a,g,x);l=A(c);this.setHMACKey=function(a,b,h){var k;if(!0===I)throw Error("HMAC key already set");if(!0===y)throw Error("Cannot set HMAC key after calling update");if(!0===z)throw Error("SHAKE is not supported for HMAC");g=(h||{}).encoding||"UTF8";b=M(b,g,x)(a);a=b.binLen;b=b.value;k=m>>>3;h=k/4-1;if(ka/8){for(;b.length<=h;)b.push(0);b[h]&=4294967040}for(a=0;a<=h;a+=1)v[a]=b[a]^909522486,w[a]=b[a]^1549556828;l=q(v,l);e=m;I=!0};this.update=function(a){var c,b,k,f=0,g=m>>>5;c=d(a,h,n);a=c.binLen;b=c.value;c=a>>>5;for(k=0;k>>5);n=a%m;y=!0};this.getHash=function(a,b){var k,g,d,m;if(!0===I)throw Error("Cannot call getHash after setting HMAC key");d=N(b);if(!0===z){if(-1===d.shakeLen)throw Error("shakeLen must be specified in options"); +f=d.shakeLen}switch(a){case "HEX":k=function(a){return O(a,f,x,d)};break;case "B64":k=function(a){return P(a,f,x,d)};break;case "BYTES":k=function(a){return Q(a,f,x)};break;case "ARRAYBUFFER":try{g=new ArrayBuffer(0)}catch(p){throw Error("ARRAYBUFFER not supported by this environment");}k=function(a){return R(a,f,x)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER");}m=u(h.slice(),n,e,r(l),f);for(g=1;g>>24-f%32),m=u(m,f, +0,A(c),f);return k(m)};this.getHMAC=function(a,b){var k,g,d,p;if(!1===I)throw Error("Cannot call getHMAC without first setting HMAC key");d=N(b);switch(a){case "HEX":k=function(a){return O(a,f,x,d)};break;case "B64":k=function(a){return P(a,f,x,d)};break;case "BYTES":k=function(a){return Q(a,f,x)};break;case "ARRAYBUFFER":try{k=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}k=function(a){return R(a,f,x)};break;default:throw Error("outputFormat must be HEX, B64, BYTES, or ARRAYBUFFER"); +}g=u(h.slice(),n,e,r(l),f);p=q(w,A(c));p=u(g,f,m,p,f);return k(p)}}function b(c,a){this.a=c;this.b=a}function O(c,a,b,e){var h="";a/=8;var n,g,d;d=-1===b?3:0;for(n=0;n>>2]>>>8*(d+n%4*b),h+="0123456789abcdef".charAt(g>>>4&15)+"0123456789abcdef".charAt(g&15);return e.outputUpper?h.toUpperCase():h}function P(c,a,b,e){var h="",n=a/8,g,d,p,f;f=-1===b?3:0;for(g=0;g>>2]:0,p=g+2>>2]:0,p=(c[g>>>2]>>>8*(f+g%4*b)&255)<<16|(d>>>8*(f+(g+1)%4*b)&255)<<8|p>>>8*(f+ +(g+2)%4*b)&255,d=0;4>d;d+=1)8*g+6*d<=a?h+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(p>>>6*(3-d)&63):h+=e.b64Pad;return h}function Q(c,a,b){var e="";a/=8;var h,d,g;g=-1===b?3:0;for(h=0;h>>2]>>>8*(g+h%4*b)&255,e+=String.fromCharCode(d);return e}function R(c,a,b){a/=8;var e,h=new ArrayBuffer(a),d,g;g=new Uint8Array(h);d=-1===b?3:0;for(e=0;e>>2]>>>8*(d+e%4*b)&255;return h}function N(c){var a={outputUpper:!1,b64Pad:"=",shakeLen:-1};c=c||{}; +a.outputUpper=c.outputUpper||!1;!0===c.hasOwnProperty("b64Pad")&&(a.b64Pad=c.b64Pad);if(!0===c.hasOwnProperty("shakeLen")){if(0!==c.shakeLen%8)throw Error("shakeLen must be a multiple of 8");a.shakeLen=c.shakeLen}if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option");if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function M(c,a,b){switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE"); +}switch(c){case "HEX":c=function(a,c,d){var g=a.length,l,p,f,m,q,u;if(0!==g%2)throw Error("String of HEX type must be in byte increments");c=c||[0];d=d||0;q=d>>>3;u=-1===b?3:0;for(l=0;l>>1)+q;for(f=m>>>2;c.length<=f;)c.push(0);c[f]|=p<<8*(u+m%4*b)}return{value:c,binLen:4*g+d}};break;case "TEXT":c=function(c,h,d){var g,l,p=0,f,m,q,u,r,t;h=h||[0];d=d||0;q=d>>>3;if("UTF8"===a)for(t=-1=== +b?3:0,f=0;fg?l.push(g):2048>g?(l.push(192|g>>>6),l.push(128|g&63)):55296>g||57344<=g?l.push(224|g>>>12,128|g>>>6&63,128|g&63):(f+=1,g=65536+((g&1023)<<10|c.charCodeAt(f)&1023),l.push(240|g>>>18,128|g>>>12&63,128|g>>>6&63,128|g&63)),m=0;m>>2;h.length<=u;)h.push(0);h[u]|=l[m]<<8*(t+r%4*b);p+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(t=-1===b?2:0,l="UTF16LE"===a&&1!==b||"UTF16LE"!==a&&1===b,f=0;f>>8);r=p+q;for(u=r>>>2;h.length<=u;)h.push(0);h[u]|=g<<8*(t+r%4*b);p+=2}return{value:h,binLen:8*p+d}};break;case "B64":c=function(a,c,d){var g=0,l,p,f,m,q,u,r,t;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");p=a.indexOf("=");a=a.replace(/\=/g,"");if(-1!==p&&p { + switch (request.action) { + case 'GET_ENTRIES': + _getEntries().then(cb); + break; + default: + break; + } + // return true is must, + // so that chrome knows the response is async. + // Or callback value will be undefined + return true; +}); \ No newline at end of file diff --git a/src/content.ts b/src/content.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/models/encryption.ts b/src/models/encryption.ts index 1dcc097cc..6e084bee8 100644 --- a/src/models/encryption.ts +++ b/src/models/encryption.ts @@ -19,8 +19,14 @@ class Encription { if (!this.password) { return secret; } - return CryptoJS.AES.decrypt(secret, this.password) - .toString(CryptoJS.enc.Utf8); + + try { + const decryptedSecret = CryptoJS.AES.decrypt(secret, this.password) + .toString(CryptoJS.enc.Utf8); + return decryptedSecret || 'Encrypted'; + } catch (error) { + return 'Encrypted'; + } } getEncryptionStatus(): boolean { diff --git a/src/models/interface.ts b/src/models/interface.ts index 6cfb8a777..96c9229a9 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -17,13 +17,14 @@ interface OTP { account: string; hash: string; counter: number; + code: string; create(encryption: Encription): Promise; update( encryption: Encription, issuer: string, account: string, index: number, counter: number): Promise; next(encryption: Encription): Promise; delete(): Promise; - generate(): string; + generate(): void; } interface OTPStorage { @@ -34,4 +35,9 @@ interface OTPStorage { issuer: string; secret: string; type: string; +} + +/* tslint:disable-next-line:interface-name */ +interface I18nMessage { + [key: string]: {message: string, description: string}; } \ No newline at end of file diff --git a/src/models/key-utilities.ts b/src/models/key-utilities.ts index 59b360f54..9d66a3443 100644 --- a/src/models/key-utilities.ts +++ b/src/models/key-utilities.ts @@ -82,11 +82,11 @@ class KeyUtilities { key = secret; break; case OTPType.battle: - key = this.base32tohex(secret.substr(5)); + key = this.base32tohex(secret); len = 8; break; case OTPType.steam: - key = this.base32tohex(secret.substr(4)); + key = this.base32tohex(secret); len = 10; b26 = true; break; @@ -111,7 +111,7 @@ class KeyUtilities { // external library for SHA functionality const hmacObj = new jsSHA('SHA-1', 'HEX'); hmacObj.setHMACKey(key, 'HEX'); - hmacObj.update(this.hex2str(time)); + hmacObj.update(time); const hmac = hmacObj.getHMAC('HEX'); let offset = 0; diff --git a/src/models/otp.ts b/src/models/otp.ts index 39ee0a284..e6ec2390b 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -12,6 +12,7 @@ class OTPEntry implements OTP { account: string; hash: string; counter: number; + code: string; constructor( type: OTPType, issuer: string, secret: string, account: string, @@ -23,6 +24,7 @@ class OTPEntry implements OTP { this.account = account; this.hash = CryptoJS.MD5(secret).toString(); this.counter = 0; + this.generate(); } async create(encryption: Encription) { @@ -57,6 +59,10 @@ class OTPEntry implements OTP { } generate() { - return KeyUtilities.generate(this.type, this.secret, this.counter); + if (this.secret === 'Encrypted') { + this.code = 'Encrypted'; + } else { + this.code = KeyUtilities.generate(this.type, this.secret, this.counter); + } } } \ No newline at end of file diff --git a/src/models/storage.ts b/src/models/storage.ts index 4e2f29da3..d7f37a1ef 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -50,11 +50,11 @@ class EntryStorage { this.getOTPStorageFromEntry(encryption, entry); _data[entry.hash] = storageItem; _data = this.ensureUniqueIndex(_data); - chrome.storage.sync.set(_data, Promise.resolve); + chrome.storage.sync.set(_data, resolve); }); return; } catch (error) { - return Promise.reject(error); + return reject(error); } }); } @@ -71,49 +71,71 @@ class EntryStorage { this.getOTPStorageFromEntry(encryption, entry); _data[entry.hash] = storageItem; _data = this.ensureUniqueIndex(_data); - chrome.storage.sync.set(_data, Promise.resolve); + chrome.storage.sync.set(_data, resolve); }); return; } catch (error) { - return Promise.reject(error); + return reject(error); } }); } - static async get() { + static async get(encryption: Encription) { return new Promise( (resolve: (value: OTPEntry[]) => void, reject: (reason: Error) => void) => { try { - chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { - const data: OTPEntry[] = []; - for (const hash of Object.keys(_data)) { - const entryData = _data[hash]; - let type: OTPType; - switch (entryData.type) { - case 'totp': - case 'hotp': - case 'battle': - case 'steam': - type = OTPType[entryData.type]; - break; - default: - type = OTPType.totp; - } - const entry = new OTPEntry( - type, entryData.issuer, entryData.secret, entryData.account, - entryData.index); - data.push(entry); - } - Promise.resolve(data); - }); - return; + const data: OTPEntry[] = []; + data.push(new OTPEntry( + OTPType.totp, 'test issuer', 'abcd2345', 'sneezry', 0)); + data.push(new OTPEntry( + OTPType.totp, 'test issuer1', 'bbcd2345', 'sneezry1', 1)); + data.push(new OTPEntry( + OTPType.totp, 'test issuer2', 'abcc2345', 'sneezry2', 2)); + return resolve(data); } catch (error) { - return Promise.reject(error); + return reject(error); } }); } + // static async get(encryption: Encription) { + // return new Promise( + // (resolve: (value: OTPEntry[]) => void, + // reject: (reason: Error) => void) => { + // try { + // chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => + // { + // const data: OTPEntry[] = []; + // for (const hash of Object.keys(_data)) { + // const entryData = _data[hash]; + // let type: OTPType; + // switch (entryData.type) { + // case 'totp': + // case 'hotp': + // case 'battle': + // case 'steam': + // type = OTPType[entryData.type]; + // break; + // default: + // type = OTPType.totp; + // } + // entryData.secret = + // encryption.getDecryptedSecret(entryData.secret); + // const entry = new OTPEntry( + // type, entryData.issuer, entryData.secret, + // entryData.account, entryData.index); + // data.push(entry); + // } + // return resolve(data); + // }); + // return; + // } catch (error) { + // return reject(error); + // } + // }); + // } + static async delete(entry: OTPEntry) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { @@ -123,11 +145,11 @@ class EntryStorage { delete _data[entry.hash]; } _data = this.ensureUniqueIndex(_data); - return Promise.resolve(); + return resolve(); }); return; } catch (error) { - return Promise.reject(error); + return reject(error); } }); } diff --git a/src/popup.ts b/src/popup.ts new file mode 100644 index 000000000..9b49f6f5a --- /dev/null +++ b/src/popup.ts @@ -0,0 +1,62 @@ +/* tslint:disable:no-reference */ +/// + +// need to find a better way to handle Vue types without modules +/* tslint:disable-next-line:no-any */ +declare var Vue: any; + +async function loadI18nMessages() { + return new Promise( + (resolve: (value: {[key: string]: string}) => void, + reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); + const i18nData: {[key: string]: string} = {}; + for (const key of Object.keys(i18nMessage)) { + i18nData[key] = chrome.i18n.getMessage(key); + } + return resolve(i18nData); + } + return; + }; + xhr.open( + 'GET', chrome.extension.getURL('/_locales/en/messages.json')); + xhr.send(); + } catch (error) { + return reject(error); + } + }); +} + +async function getDataFromBackground(command: {}) { + return new Promise( + (resolve: (value: T) => void, reject: (reason: Error) => void) => { + chrome.runtime.sendMessage(command, (response: T) => { + return resolve(response); + }); + }); +} + +async function getEntries() { + const entries: OTP[] = + await getDataFromBackground({action: 'GET_ENTRIES'}); + return entries; +} + +async function init() { + const i18n = await loadI18nMessages(); + const entries = await getEntries(); + + const authenticator = + new Vue({el: '#authenticator', data: {i18n, entries}, methods: {}}); + + // setInterval(async () => { + // authenticator.entries = await getEntries(); + // }, 1000); + return; +} + +init(); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 41fb4c57c..9450bb83e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,8 @@ "extends": "./../../../../usr/local/lib/node_modules/gts/tsconfig-google.json", "compilerOptions": { "lib": ["es6", "dom"], + "target": "es6", + "strict": true, "rootDir": "src", "outDir": "build" }, diff --git a/vue.min.js b/vue.min.js new file mode 100644 index 000000000..fccc40b0f --- /dev/null +++ b/vue.min.js @@ -0,0 +1,11 @@ +/*! + * Vue.js v1.0.28-csp + * (c) 2016 Evan You + * Released under the MIT License. + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Vue=t()}(this,function(){"use strict";function e(t,r,i){if(n(t,r))return void(t[r]=i);if(t._isVue)return void e(t._data,r,i);var a=t.__ob__;if(!a)return void(t[r]=i);if(a.convert(r,i),a.dep.notify(),a.vms)for(var o=a.vms.length;o--;){var s=a.vms[o];s._proxy(r),s._digest()}return i}function t(e,t){if(n(e,t)){delete e[t];var r=e.__ob__;if(!r)return void(e._isVue&&(delete e._data[t],e._digest()));if(r.dep.notify(),r.vms)for(var i=r.vms.length;i--;){var a=r.vms[i];a._unproxy(t),a._digest()}}}function n(e,t){return Mn.call(e,t)}function r(e){return Vn.test(e)}function i(e){var t=(e+"").charCodeAt(0);return 36===t||95===t}function a(e){return null==e?"":e.toString()}function o(e){if("string"!=typeof e)return e;var t=Number(e);return isNaN(t)?e:t}function s(e){return"true"===e||"false"!==e&&e}function l(e){var t=e.charCodeAt(0),n=e.charCodeAt(e.length-1);return t!==n||34!==t&&39!==t?e:e.slice(1,-1)}function u(e){return e.replace(Hn,c)}function c(e,t){return t?t.toUpperCase():""}function p(e){return e.replace(Wn,"$1-$2").replace(Wn,"$1-$2").toLowerCase()}function h(e){return e.replace(zn,c)}function f(e,t){return function(n){var r=arguments.length;return r?r>1?e.apply(t,arguments):e.call(t,n):e.call(t)}}function d(e,t){t=t||0;for(var n=e.length-t,r=new Array(n);n--;)r[n]=e[n+t];return r}function v(e,t){for(var n=Object.keys(t),r=n.length;r--;)e[n[r]]=t[n[r]];return e}function m(e){return null!==e&&"object"==typeof e}function g(e){return qn.call(e)===Gn}function y(e,t,n,r){Object.defineProperty(e,t,{value:n,enumerable:!!r,writable:!0,configurable:!0})}function b(e,t){var n,r,i,a,o,s=function s(){var l=Date.now()-a;l=0?n=setTimeout(s,t-l):(n=null,o=e.apply(i,r),n||(i=r=null))};return function(){return i=this,r=arguments,a=Date.now(),n||(n=setTimeout(s,t)),o}}function _(e,t){for(var n=e.length;n--;)if(e[n]===t)return n;return-1}function x(e){var t=function t(){if(!t.cancelled)return e.apply(this,arguments)};return t.cancel=function(){t.cancelled=!0},t}function w(e,t){return e==t||!(!m(e)||!m(t))&&JSON.stringify(e)===JSON.stringify(t)}function k(e){return/native code/.test(e.toString())}function S(e){this.size=0,this.limit=e,this.head=this.tail=void 0,this._keymap=Object.create(null)}function C(){return hr.charCodeAt(vr+1)}function E(){return hr.charCodeAt(++vr)}function $(){return vr>=dr}function O(){for(;C()===Or;)E()}function A(e){return e===Sr||e===Cr}function N(e){return Ar[e]}function L(e,t){return Nr[e]===t}function P(){for(var e,t=E();!$();)if(e=E(),e===$r)E();else if(e===t)break}function I(e){for(var t=0,n=e;!$();)if(e=C(),A(e))P();else if(n===e&&t++,L(n,e)&&t--,E(),0===t)break}function j(){for(var e=vr;!$();)if(mr=C(),A(mr))P();else if(N(mr))I(mr);else if(mr===Er){if(E(),mr=C(),mr!==Er){gr!==_r&&gr!==kr||(gr=xr);break}E()}else{if(mr===Or&&(gr===wr||gr===kr)){O();break}gr===xr&&(gr=wr),E()}return hr.slice(e+1,vr)||null}function T(){for(var e=[];!$();)e.push(F());return e}function F(){var e,t={};return gr=xr,t.name=j().trim(),gr=kr,e=D(),e.length&&(t.args=e),t}function D(){for(var e=[];!$()&&gr!==xr;){var t=j();if(!t)break;e.push(R(t))}return e}function R(e){if(br.test(e))return{value:o(e),dynamic:!1};var t=l(e),n=t===e;return{value:n?e:t,dynamic:n}}function B(e){var t=yr.get(e);if(t)return t;hr=e,fr={},dr=hr.length,vr=-1,mr="",gr=_r;var n;return hr.indexOf("|")<0?fr.expression=hr.trim():(fr.expression=j().trim(),n=T(),n.length&&(fr.filters=n)),yr.put(e,fr),fr}function U(e){return e.replace(Pr,"\\$&")}function M(){var e=U(Ur.delimiters[0]),t=U(Ur.delimiters[1]),n=U(Ur.unsafeDelimiters[0]),r=U(Ur.unsafeDelimiters[1]);jr=new RegExp(n+"((?:.|\\n)+?)"+r+"|"+e+"((?:.|\\n)+?)"+t,"g"),Tr=new RegExp("^"+n+"((?:.|\\n)+?)"+r+"$"),Ir=new S(1e3)}function V(e){Ir||M();var t=Ir.get(e);if(t)return t;if(!jr.test(e))return null;for(var n,r,i,a,o,s,l=[],u=jr.lastIndex=0;n=jr.exec(e);)r=n.index,r>u&&l.push({value:e.slice(u,r)}),i=Tr.test(n[0]),a=i?n[1]:n[2],o=a.charCodeAt(0),s=42===o,a=s?a.slice(1):a,l.push({tag:!0,value:a.trim(),html:i,oneTime:s}),u=r+n[0].length;return u1?e.map(function(e){return W(e,t)}).join("+"):W(e[0],t,!0)}function W(e,t,n){return e.tag?e.oneTime&&t?'"'+t.$eval(e.value)+'"':z(e.value,n):'"'+e.value+'"'}function z(e,t){if(Fr.test(e)){var n=B(e);return n.filters?"this._applyFilters("+n.expression+",null,"+JSON.stringify(n.filters)+",false)":"("+e+")"}return t?e:"("+e+")"}function q(e,t,n,r){K(e,1,function(){t.appendChild(e)},n,r)}function G(e,t,n,r){K(e,1,function(){te(e,t)},n,r)}function J(e,t,n){K(e,-1,function(){re(e)},t,n)}function K(e,t,n,r,i){var a=e.__v_trans;if(!a||!a.hooks&&!ir||!r._isCompiled||r.$parent&&!r.$parent._isCompiled)return n(),void(i&&i());var o=t>0?"enter":"leave";a[o](n,i)}function Q(e){if("string"==typeof e){e=document.querySelector(e)}return e}function X(e){if(!e)return!1;var t=e.ownerDocument.documentElement,n=e.parentNode;return t===e||t===n||!(!n||1!==n.nodeType||!t.contains(n))}function Z(e,t){var n=e.getAttribute(t);return null!==n&&e.removeAttribute(t),n}function Y(e,t){var n=Z(e,":"+t);return null===n&&(n=Z(e,"v-bind:"+t)),n}function ee(e,t){return e.hasAttribute(t)||e.hasAttribute(":"+t)||e.hasAttribute("v-bind:"+t)}function te(e,t){t.parentNode.insertBefore(e,t)}function ne(e,t){t.nextSibling?te(e,t.nextSibling):t.parentNode.appendChild(e)}function re(e){e.parentNode.removeChild(e)}function ie(e,t){t.firstChild?te(e,t.firstChild):t.appendChild(e)}function ae(e,t){var n=e.parentNode;n&&n.replaceChild(t,e)}function oe(e,t,n,r){e.addEventListener(t,n,r)}function se(e,t,n){e.removeEventListener(t,n)}function le(e){var t=e.className;return"object"==typeof t&&(t=t.baseVal||""),t}function ue(e,t){er&&!/svg$/.test(e.namespaceURI)?e.className=t:e.setAttribute("class",t)}function ce(e,t){if(e.classList)e.classList.add(t);else{var n=" "+le(e)+" ";n.indexOf(" "+t+" ")<0&&ue(e,(n+t).trim())}}function pe(e,t){if(e.classList)e.classList.remove(t);else{for(var n=" "+le(e)+" ",r=" "+t+" ";n.indexOf(r)>=0;)n=n.replace(r," ");ue(e,n.trim())}e.className||e.removeAttribute("class")}function he(e,t){var n,r;if(ve(e)&&_e(e.content)&&(e=e.content),e.hasChildNodes())for(fe(e),r=t?document.createDocumentFragment():document.createElement("div");n=e.firstChild;)r.appendChild(n);return r}function fe(e){for(var t;t=e.firstChild,de(t);)e.removeChild(t);for(;t=e.lastChild,de(t);)e.removeChild(t)}function de(e){return e&&(3===e.nodeType&&!e.data.trim()||8===e.nodeType)}function ve(e){return e.tagName&&"template"===e.tagName.toLowerCase()}function me(e,t){var n=Ur.debug?document.createComment(e):document.createTextNode(t?" ":"");return n.__v_anchor=!0,n}function ge(e){if(e.hasAttributes())for(var t=e.attributes,n=0,r=t.length;n=l.length){for(var e=0;e=97&&t<=122||t>=65&&t<=90?"ident":t>=49&&t<=57?"number":"else"}function Ue(e){var t=e.trim();return("0"!==e.charAt(0)||!isNaN(e))&&(r(t)?l(t):"*"+t)}function Me(e){function t(){var t=e[c+1];if(p===yi&&"'"===t||p===bi&&'"'===t)return c++,r="\\"+t,f[ui](),!0}var n,r,i,a,o,s,l,u=[],c=-1,p=fi,h=0,f=[];for(f[ci]=function(){void 0!==i&&(u.push(i),i=void 0)},f[ui]=function(){void 0===i?i=r:i+=r},f[pi]=function(){f[ui](),h++},f[hi]=function(){if(h>0)h--,p=gi,f[ui]();else{if(h=0,i=Ue(i),i===!1)return!1;f[ci]()}};null!=p;)if(c++,n=e[c],"\\"!==n||!t()){if(a=Be(n),l=wi[p],o=l[a]||l.else||xi,o===xi)return;if(p=o[0],s=f[o[1]],s&&(r=o[2],r=void 0===r?n:r,s()===!1))return;if(p===_i)return u.raw=e,u}}function Ve(e){var t=li.get(e);return t||(t=Me(e),t&&li.put(e,t)),t}function He(e,t){return Ze(t).get(e)}function We(t,n,r){var i=t;if("string"==typeof n&&(n=Me(n)),!n||!m(t))return!1;for(var a,o,s=0,l=n.length;s-1?n.replace(Pi,Je):n,t+"scope."+n)}function Je(e,t){return Fi[t]}function Ke(e){Oi.test(e),Fi.length=0;var t=e.replace(Li,qe).replace(Ai,"");return t=(" "+t).replace(ji,Ge).replace(Pi,Je),Qe(t)}function Qe(e){try{var t=oi.Function("scope","Math","return "+e);return function(e){return t.call(this,e,Math)}}catch(e){return ze}}function Xe(e){var t=Ve(e);if(t)return function(e,n){We(e,t,n)}}function Ze(e,t){e=e.trim();var n=Si.get(e);if(n)return t&&!n.set&&(n.set=Xe(n.exp)),n;var r={exp:e};return r.get=Ye(e)&&e.indexOf("[")<0?Qe("scope."+e):Ke(e),t&&(r.set=Xe(e)),Si.put(e,r),r}function Ye(e){return Ii.test(e)&&!Ti.test(e)&&"Math."!==e.slice(0,5)}function et(){Ri.length=0,Bi.length=0,Ui={},Mi={},Vi=!1}function tt(){for(var e=!0;e;)e=!1,nt(Ri),nt(Bi),Ri.length?e=!0:(Xn&&Ur.devtools&&Xn.emit("flush"),et())}function nt(e){for(var t=0;t0){var o=a+(r?t:xe(t));i=na.get(o),i||(i=Gt(n,e.$options,!0),na.put(o,i))}else i=Gt(n,e.$options,!0);this.linker=i}function bt(e,t,n){var r=e.node.previousSibling;if(r){for(e=r.__v_frag;!(e&&e.forId===n&&e.inserted||r===t);){if(r=r.previousSibling,!r)return;e=r.__v_frag}return e}}function _t(e){for(var t=-1,n=new Array(Math.floor(e));++t47&&t<58?parseInt(e,10):1===e.length&&(t=e.toUpperCase().charCodeAt(0),t>64&&t<91)?t:ka[e]});return n=[].concat.apply([],n),function(t){if(n.indexOf(t.keyCode)>-1)return e.call(this,t)}}function Et(e){return function(t){return t.stopPropagation(),e.call(this,t)}}function $t(e){return function(t){return t.preventDefault(),e.call(this,t)}}function Ot(e){return function(t){if(t.target===t.currentTarget)return e.call(this,t)}}function At(e){if(Oa[e])return Oa[e];var t=Nt(e);return Oa[e]=Oa[t]=t,t}function Nt(e){e=p(e);var t=u(e),n=t.charAt(0).toUpperCase()+t.slice(1);Aa||(Aa=document.createElement("div"));var r,i=Ca.length;if("filter"!==t&&t in Aa.style)return{kebab:e,camel:t};for(;i--;)if(r=Ea[i]+n,r in Aa.style)return{kebab:Ca[i]+e,camel:r}}function Lt(e){var t=[];if(Jn(e))for(var n=0,r=e.length;n=i?n():e[a].call(t,r)}var i=e.length,a=0;e[0].call(t,r)}function jt(e,t,n){for(var i,a,o,s,l,c,h,f=[],d=n.$options.propsData,v=Object.keys(t),m=v.length;m--;)a=v[m],i=t[a]||za,l=u(a),qa.test(l)&&(h={name:a,path:l,options:i,mode:Wa.ONE_WAY,raw:null},o=p(a),null===(s=Y(e,o))&&(null!==(s=Y(e,o+".sync"))?h.mode=Wa.TWO_WAY:null!==(s=Y(e,o+".once"))&&(h.mode=Wa.ONE_TIME)),null!==s?(h.raw=s,c=B(s),s=c.expression,h.filters=c.filters,r(s)&&!c.filters?h.optimizedLiteral=!0:h.dynamic=!0,h.parentPath=s):null!==(s=Z(e,o))?h.raw=s:d&&null!==(s=d[a]||d[l])&&(h.raw=s),f.push(h));return Tt(f)}function Tt(e){return function(t,r){t._props={};for(var i,a,u,c,h,f=t.$options.propsData,d=e.length;d--;)if(i=e[d],h=i.raw,a=i.path,u=i.options,t._props[a]=i,f&&n(f,a)&&Dt(t,i,f[a]),null===h)Dt(t,i,void 0);else if(i.dynamic)i.mode===Wa.ONE_TIME?(c=(r||t._context||t).$get(i.parentPath),Dt(t,i,c)):t._context?t._bindDir({name:"prop",def:Ja,prop:i},null,null,r):Dt(t,i,t.$get(i.parentPath));else if(i.optimizedLiteral){var v=l(h);c=v===h?s(o(h)):v,Dt(t,i,c)}else c=u.type===Boolean&&(""===h||h===p(i.name))||h,Dt(t,i,c)}}function Ft(e,t,n,r){var i=t.dynamic&&Ye(t.parentPath),a=n;void 0===a&&(a=Bt(e,t)),a=Mt(t,a,e);var o=a!==n;Ut(t,a,e)||(a=void 0),i&&!o?Pe(function(){r(a)}):r(a)}function Dt(e,t,n){Ft(e,t,n,function(n){De(e,t.path,n)})}function Rt(e,t,n){Ft(e,t,n,function(n){e[t.path]=n})}function Bt(e,t){var r=t.options;if(!n(r,"default"))return r.type!==Boolean&&void 0;var i=r.default;return m(i),"function"==typeof i&&r.type!==Function?i.call(e):i}function Ut(e,t,n){if(!e.options.required&&(null===e.raw||null==t))return!0;var r=e.options,i=r.type,a=!i,o=[];if(i){Jn(i)||(i=[i]);for(var s=0;st?-1:e===t?0:1}),t=0,n=s.length;tf.priority)&&(f=h,c=i.name,s=vn(i.name),o=i.value,u=l[1],p=l[2]));return f?fn(e,u,o,n,f,c,p,s):void 0}function hn(){}function fn(e,t,n,r,i,a,o,s){var l=B(n),u={name:t,arg:o,expression:l.expression,filters:l.filters,raw:n,attr:a,modifiers:s,def:i};"for"!==t&&"router-view"!==t||(u.ref=ge(e));var c=function(e,t,n,r,i){u.ref&&De((r||e).$refs,u.ref,null),e._bindDir(u,t,n,r,i)};return c.terminal=!0,c}function dn(e,t){function n(e,t,n){var r=n&&gn(n),i=!r&&B(a);v.push({name:e,attr:o,raw:s,def:t,arg:u,modifiers:c,expression:i&&i.expression,filters:i&&i.filters,interp:n,hasOneTime:r})}for(var r,i,a,o,s,l,u,c,p,h,f,d=e.length,v=[];d--;)if(r=e[d],i=o=r.name,a=s=r.value,h=V(a),u=null,c=vn(i),i=i.replace(uo,""),h)a=H(h),u=i,n("bind",Ma.bind,h);else if(co.test(i))c.literal=!oo.test(i),n("transition",ao.transition);else if(so.test(i))u=i.replace(so,""),n("on",Ma.on);else if(oo.test(i))l=i.replace(oo,""),"style"===l||"class"===l?n(l,ao[l]):(u=l,n("bind",Ma.bind));else if(f=i.match(lo)){if(l=f[1],u=f[2],"else"===l)continue;p=Ne(t,"directives",l,!0),p&&n(l,p)}if(v.length)return mn(v)}function vn(e){var t=Object.create(null),n=e.match(uo);if(n)for(var r=n.length;r--;)t[n[r].slice(1)]=!0;return t}function mn(e){return function(t,n,r,i,a){for(var o=e.length;o--;)t._bindDir(e[o],n,r,i,a)}}function gn(e){for(var t=e.length;t--;)if(e[t].oneTime)return!0}function yn(e){return"SCRIPT"===e.tagName&&(!e.hasAttribute("type")||"text/javascript"===e.getAttribute("type"))}function bn(e,t){return t&&(t._containerAttrs=xn(e)),ve(e)&&(e=ct(e)),t&&(t._asComponent&&!t.template&&(t.template=""),t.template&&(t._content=he(e),e=_n(e,t))),_e(e)&&(ie(me("v-start",!0),e),e.appendChild(me("v-end",!0))),e}function _n(e,t){var n=t.template,r=ct(n,!0);if(r){var i=r.firstChild;if(!i)return r;var a=i.tagName&&i.tagName.toLowerCase();return t.replace?(e===document.body,r.childNodes.length>1||1!==i.nodeType||"component"===a||Ne(t,"components",a)||ee(i,"is")||Ne(t,"elementDirectives",a)||i.hasAttribute("v-for")||i.hasAttribute("v-if")?r:(t._replacerAttrs=xn(i),wn(e,i),i)):(e.appendChild(r),e)}}function xn(e){if(1===e.nodeType&&e.hasAttributes())return d(e.attributes)}function wn(e,t){for(var n,r,i=e.attributes,a=i.length;a--;)n=i[a].name,r=i[a].value,t.hasAttribute(n)||fo.test(n)?"class"===n&&!V(r)&&(r=r.trim())&&r.split(/\s+/).forEach(function(e){ce(t,e)}):t.setAttribute(n,r)}function kn(e,t){if(t){for(var n,r,i=e._slotContents=Object.create(null),a=0,o=t.children.length;a1?d(n):n;var i=t&&n.some(function(e){return e._fromParent});i&&(r=!1);for(var a=d(arguments,1),o=0,s=n.length;ot?a:-a}var n=null,r=void 0;e=xo(e);var i=d(arguments,1),a=i[i.length-1];"number"==typeof a?(a=a<0?-1:1,i=i.length>1?i.slice(0,-1):i):a=1;var o=i[0];return o?("function"==typeof o?n=function(e,t){return o(e,t)*a}:(r=Array.prototype.concat.apply([],i),n=function(e,i,a){return a=a||0,a>=r.length-1?t(e,i,a):t(e,i,a)||n(e,i,a+1)}),e.slice().sort(n)):e}function Bn(e,t){var n;if(g(e)){var r=Object.keys(e);for(n=r.length;n--;)if(Bn(e[r[n]],t))return!0}else if(Jn(e)){for(n=e.length;n--;)if(Bn(e[n],t))return!0}else if(null!=e)return e.toString().toLowerCase().indexOf(t)>-1}function Un(n){n.options={directives:Ma,elementDirectives:_o,filters:ko,transitions:{},components:{},partials:{},replace:!0},n.util=Yr,n.config=Ur,n.set=e,n.delete=t,n.nextTick=ur,n.compiler=vo,n.FragmentFactory=yt,n.internalDirectives=ao,n.parsers={path:ki,text:Dr,template:ea,directive:Lr,expression:Di},n.cid=0;var r=1;n.extend=function(e){e=e||{};var t=this,i=0===t.cid;if(i&&e._Ctor)return e._Ctor;var a=e.name||t.options.name,o=function(e){n.call(this,e)};return o.prototype=Object.create(t.prototype),o.prototype.constructor=o,o.cid=r++,o.options=Ae(t.options,e),o.super=t,o.extend=t.extend,Ur._assetTypes.forEach(function(e){o[e]=t[e]}),a&&(o.options.components[a]=o),i&&(e._Ctor=o),o},n.use=function(e){if(!e.installed){var t=d(arguments,1);return t.unshift(this),"function"==typeof e.install?e.install.apply(e,t):e.apply(null,t),e.installed=!0,this}},n.mixin=function(e){n.options=Ae(n.options,e)},Ur._assetTypes.forEach(function(e){n[e]=function(t,r){return r?("component"===e&&g(r)&&(r.name||(r.name=t),r=n.extend(r)),this.options[e+"s"][t]=r,r):this.options[e+"s"][t]}}),v(n.transition,Vr)}var Mn=Object.prototype.hasOwnProperty,Vn=/^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/,Hn=/-(\w)/g,Wn=/([^-])([A-Z])/g,zn=/(?:^|[-_\/])(\w)/g,qn=Object.prototype.toString,Gn="[object Object]",Jn=Array.isArray,Kn="__proto__"in{},Qn="undefined"!=typeof window&&"[object Object]"!==Object.prototype.toString.call(window),Xn=Qn&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__,Zn=Qn&&window.navigator.userAgent.toLowerCase(),Yn=Zn&&Zn.indexOf("trident")>0,er=Zn&&Zn.indexOf("msie 9.0")>0,tr=Zn&&Zn.indexOf("android")>0,nr=Zn&&/iphone|ipad|ipod|ios/.test(Zn),rr=void 0,ir=void 0,ar=void 0,or=void 0;if(Qn&&!er){var sr=void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend,lr=void 0===window.onanimationend&&void 0!==window.onwebkitanimationend;rr=sr?"WebkitTransition":"transition",ir=sr?"webkitTransitionEnd":"transitionend",ar=lr?"WebkitAnimation":"animation",or=lr?"webkitAnimationEnd":"animationend"}var ur=function(){function e(){n=!1;var e=t.slice(0);t.length=0;for(var r=0;r=this.length&&(this.length=Number(e)+1),this.splice(e,1,t)[0]}),y(Kr,"$remove",function(e){if(this.length){var t=_(this,e);return t>-1?this.splice(t,1):void 0}});var Xr=Object.getOwnPropertyNames(Qr),Zr=!0;Ie.prototype.walk=function(e){for(var t=Object.keys(e),n=0,r=t.length;nthis.maxIterations)throw new Error("Infinite loop detected - reached max iterations")},e.exports}({exports:{}}),ii=function(e){function t(e){function i(e){for(var t=null,n=0;n=0}function i(e){return"0123456789abcdefABCDEF".indexOf(e)>=0}function a(e){return"01234567".indexOf(e)>=0}function o(e){return" "===e||"\t"===e||"\v"===e||"\f"===e||"\xa0"===e||e.charCodeAt(0)>=5760&&"\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\ufeff".indexOf(e)>=0}function s(e){return"\n"===e||"\r"===e||"\u2028"===e||"\u2029"===e}function l(e){return"$"===e||"_"===e||"\\"===e||e>="a"&&e<="z"||e>="A"&&e<="Z"||e.charCodeAt(0)>=128&&pt.NonAsciiIdentifierStart.test(e)}function u(e){return"$"===e||"_"===e||"\\"===e||e>="a"&&e<="z"||e>="A"&&e<="Z"||e>="0"&&e<="9"||e.charCodeAt(0)>=128&&pt.NonAsciiIdentifierPart.test(e)}function c(e){switch(e){case"class":case"enum":case"export":case"extends":case"import":case"super":return!0}return!1}function p(e){switch(e){case"implements":case"interface":case"package":case"private":case"protected":case"public":case"static":case"yield":case"let":return!0}return!1}function h(e){return"eval"===e||"arguments"===e}function f(e){var t=!1;switch(e.length){case 2:t="if"===e||"in"===e||"do"===e;break;case 3:t="var"===e||"for"===e||"new"===e||"try"===e;break;case 4:t="this"===e||"else"===e||"case"===e||"void"===e||"with"===e;break;case 5:t="while"===e||"break"===e||"catch"===e||"throw"===e;break;case 6:t="return"===e||"typeof"===e||"delete"===e||"switch"===e;break;case 7:t="default"===e||"finally"===e;break;case 8:t="function"===e||"continue"===e||"debugger"===e;break;case 10:t="instanceof"===e}if(t)return!0;switch(e){case"const":return!0;case"yield":case"let":return!0}return!(!ft||!p(e))||c(e)}function d(){var e,t,n;for(t=!1,n=!1;dt=gt&&E({},ct.UnexpectedToken,"ILLEGAL")):(e=ht[dt++],dt>=gt&&E({},ct.UnexpectedToken,"ILLEGAL"),"*"===e&&(e=ht[dt],"/"===e&&(++dt,t=!1)));else if("/"===e)if(e=ht[dt+1],"/"===e)dt+=2,n=!0;else{if("*"!==e)break;dt+=2,t=!0,dt>=gt&&E({},ct.UnexpectedToken,"ILLEGAL")}else if(o(e))++dt;else{if(!s(e))break;++dt,"\r"===e&&"\n"===ht[dt]&&++dt,++vt,mt=dt}}function v(e){var t,n,r,a=0;for(n="u"===e?4:2,t=0;t"===a&&">"===e&&">"===t&&"="===n?(dt+=4,{type:ot.Punctuator,value:">>>=",lineNumber:vt,lineStart:mt,range:[i,dt]}):"="===a&&"="===e&&"="===t?(dt+=3,{type:ot.Punctuator,value:"===",lineNumber:vt,lineStart:mt,range:[i,dt]}):"!"===a&&"="===e&&"="===t?(dt+=3,{type:ot.Punctuator,value:"!==",lineNumber:vt,lineStart:mt,range:[i,dt]}):">"===a&&">"===e&&">"===t?(dt+=3,{type:ot.Punctuator,value:">>>",lineNumber:vt,lineStart:mt,range:[i,dt]}):"<"===a&&"<"===e&&"="===t?(dt+=3,{type:ot.Punctuator,value:"<<=",lineNumber:vt,lineStart:mt,range:[i,dt]}):">"===a&&">"===e&&"="===t?(dt+=3,{type:ot.Punctuator,value:">>=",lineNumber:vt,lineStart:mt,range:[i,dt]}):"="===e&&"<>=!+-*%&|^/".indexOf(a)>=0?(dt+=2,{type:ot.Punctuator,value:a+e,lineNumber:vt,lineStart:mt,range:[i,dt]}):a===e&&"+-<>&|".indexOf(a)>=0&&"+-<>&|".indexOf(e)>=0?(dt+=2,{type:ot.Punctuator,value:a+e,lineNumber:vt,lineStart:mt,range:[i,dt]}):"[]<>+-*%&|^!~?:=/".indexOf(a)>=0?{type:ot.Punctuator,value:ht[dt++],lineNumber:vt,lineStart:mt,range:[i,dt]}:void 0):{type:ot.Punctuator,value:ht[dt++],lineNumber:vt,lineStart:mt,range:[i,dt]})}function y(){var e,n,o;if(o=ht[dt],t(r(o)||"."===o,"Numeric literal must start with a decimal digit or a decimal point"),n=dt,e="","."!==o){if(e=ht[dt++],o=ht[dt],"0"===e){if("x"===o||"X"===o){for(e+=ht[dt++];dt=gt&&(o=""),E({},ct.UnexpectedToken,"ILLEGAL");return dt=0&&dt=gt?{type:ot.EOF,lineNumber:vt,lineStart:mt,range:[dt,dt]}:(t=g(),"undefined"!=typeof t?t:(e=ht[dt],"'"===e||'"'===e?b():"."===e||r(e)?y():(t=m(),"undefined"!=typeof t?t:void E({},ct.UnexpectedToken,"ILLEGAL"))))}function k(){var e;return yt?(dt=yt.range[1],vt=yt.lineNumber,mt=yt.lineStart,e=yt,yt=null,e):(yt=null,w())}function S(){var e,t,n;return null!==yt?yt:(e=dt,t=vt,n=mt,yt=w(),dt=e,vt=t,mt=n,yt)}function C(){var e,t,n,r;return e=dt,t=vt,n=mt,d(),r=vt!==t,dt=e,vt=t,mt=n,r}function E(e,t){var n,r=Array.prototype.slice.call(arguments,2),i=t.replace(/%(\d)/g,function(e,t){return r[t]||""});throw"number"==typeof e.lineNumber?(n=new Error("Line "+e.lineNumber+": "+i),n.index=e.range[0],n.lineNumber=e.lineNumber,n.column=e.range[0]-mt+1):(n=new Error("Line "+vt+": "+i),n.index=dt,n.lineNumber=vt,n.column=dt-mt+1),n}function $(){try{E.apply(null,arguments)}catch(e){if(!_t.errors)throw e;_t.errors.push(e)}}function O(e){if(e.type===ot.EOF&&E(e,ct.UnexpectedEOS),e.type===ot.NumericLiteral&&E(e,ct.UnexpectedNumber),e.type===ot.StringLiteral&&E(e,ct.UnexpectedString),e.type===ot.Identifier&&E(e,ct.UnexpectedIdentifier),e.type===ot.Keyword){if(c(e.value))E(e,ct.UnexpectedReserved);else if(ft&&p(e.value))return void $(e,ct.StrictReservedWord);E(e,ct.UnexpectedToken,e.value)}E(e,ct.UnexpectedToken,e.value)}function A(e){var t=k();t.type===ot.Punctuator&&t.value===e||O(t)}function N(e){var t=k();t.type===ot.Keyword&&t.value===e||O(t)}function L(e){var t=S();return t.type===ot.Punctuator&&t.value===e}function P(e){var t=S();return t.type===ot.Keyword&&t.value===e}function I(){var e=S(),t=e.value;return e.type===ot.Punctuator&&("="===t||"*="===t||"/="===t||"%="===t||"+="===t||"-="===t||"<<="===t||">>="===t||">>>="===t||"&="===t||"^="===t||"|="===t)}function j(){var e,t;if(";"===ht[dt])return void k();if(t=vt,d(),vt===t){if(L(";"))return void k();e=S(),e.type===ot.EOF||L("}")||O(e)}}function T(e){return e.type===lt.Identifier||e.type===lt.MemberExpression}function F(){var e=[];for(A("[");!L("]");)L(",")?(k(),e.push(null)):(e.push(ue()),L("]")||A(","));return A("]"),{type:lt.ArrayExpression,elements:e}}function D(e,t){var n,r;return n=ft,r=Fe(),t&&ft&&h(e[0].name)&&$(t,ct.StrictParamName),ft=n,{type:lt.FunctionExpression,id:null,params:e,defaults:[],body:r,rest:null,generator:!1,expression:!1}}function R(){var e=k();return e.type===ot.StringLiteral||e.type===ot.NumericLiteral?(ft&&e.octal&&$(e,ct.StrictOctalLiteral),Je(e)):{type:lt.Identifier,name:e.value}}function B(){var e,t,n,r;return e=S(),e.type===ot.Identifier?(n=R(),"get"!==e.value||L(":")?"set"!==e.value||L(":")?(A(":"),{type:lt.Property,key:n,value:ue(),kind:"init"}):(t=R(),A("("),e=S(),e.type!==ot.Identifier?(A(")"),$(e,ct.UnexpectedToken,e.value),{type:lt.Property,key:t,value:D([]),kind:"set"}):(r=[fe()],A(")"),{type:lt.Property,key:t,value:D(r,e),kind:"set"})):(t=R(),A("("),A(")"),{type:lt.Property,key:t,value:D([]),kind:"get"})):e.type!==ot.EOF&&e.type!==ot.Punctuator?(t=R(),A(":"),{type:lt.Property,key:t,value:ue(),kind:"init"}):void O(e)}function U(){var e,t,n,r=[],i={},a=String;for(A("{");!L("}");)e=B(),t=e.key.type===lt.Identifier?e.key.name:a(e.key.value),n="init"===e.kind?ut.Data:"get"===e.kind?ut.Get:ut.Set,Object.prototype.hasOwnProperty.call(i,t)?(i[t]===ut.Data?ft&&n===ut.Data?$({},ct.StrictDuplicateProperty):n!==ut.Data&&$({},ct.AccessorDataProperty):n===ut.Data?$({},ct.AccessorDataProperty):i[t]&n&&$({},ct.AccessorGetSet),i[t]|=n):i[t]=n,r.push(e),L("}")||A(",");return A("}"),{type:lt.ObjectExpression,properties:r}}function M(){var e;return A("("),e=ce(),A(")"),e}function V(){var e=S(),t=e.type;if(t===ot.Identifier)return{type:lt.Identifier,name:k().value};if(t===ot.StringLiteral||t===ot.NumericLiteral)return ft&&e.octal&&$(e,ct.StrictOctalLiteral),Je(k());if(t===ot.Keyword){if(P("this"))return k(),{type:lt.ThisExpression};if(P("function"))return Re()}return t===ot.BooleanLiteral?(k(),e.value="true"===e.value,Je(e)):t===ot.NullLiteral?(k(),e.value=null,Je(e)):L("[")?F():L("{")?U():L("(")?M():L("/")||L("/=")?Je(_()):O(k())}function H(){var e=[];if(A("("),!L(")"))for(;dt>")||L(">>>");)e={type:lt.BinaryExpression, +operator:k().value,left:e,right:Y()};return e}function te(){var e,t;for(t=bt.allowIn,bt.allowIn=!0,e=ee();L("<")||L(">")||L("<=")||L(">=")||t&&P("in")||P("instanceof");)e={type:lt.BinaryExpression,operator:k().value,left:e,right:ee()};return bt.allowIn=t,e}function ne(){for(var e=te();L("==")||L("!=")||L("===")||L("!==");)e={type:lt.BinaryExpression,operator:k().value,left:e,right:te()};return e}function re(){for(var e=ne();L("&");)k(),e={type:lt.BinaryExpression,operator:"&",left:e,right:ne()};return e}function ie(){for(var e=re();L("^");)k(),e={type:lt.BinaryExpression,operator:"^",left:e,right:re()};return e}function ae(){for(var e=ie();L("|");)k(),e={type:lt.BinaryExpression,operator:"|",left:e,right:ie()};return e}function oe(){for(var e=ae();L("&&");)k(),e={type:lt.LogicalExpression,operator:"&&",left:e,right:ae()};return e}function se(){for(var e=oe();L("||");)k(),e={type:lt.LogicalExpression,operator:"||",left:e,right:oe()};return e}function le(){var e,t,n;return e=se(),L("?")&&(k(),t=bt.allowIn,bt.allowIn=!0,n=ue(),bt.allowIn=t,A(":"),e={type:lt.ConditionalExpression,test:e,consequent:n,alternate:ue()}),e}function ue(){var e,t;return e=S(),t=le(),I()&&(T(t)||$({},ct.InvalidLHSInAssignment),ft&&t.type===lt.Identifier&&h(t.name)&&$(e,ct.StrictLHSAssignment),t={type:lt.AssignmentExpression,operator:k().value,left:t,right:ue()}),t}function ce(){var e=ue();if(L(","))for(e={type:lt.SequenceExpression,expressions:[e]};dt0&&_t.comments[_t.comments.length-1].range[1]>r||_t.comments.push({type:e,value:n,range:[r,i],loc:a})}function He(){var e,t,n,r,i,a;for(e="",i=!1,a=!1;dt=gt?(a=!1,e+=t,n.end={line:vt,column:gt-mt},Ve("Line",e,r,gt,n)):e+=t;else if(i)s(t)?("\r"===t&&"\n"===ht[dt+1]?(++dt,e+="\r\n"):e+=t,++vt,++dt,mt=dt,dt>=gt&&E({},ct.UnexpectedToken,"ILLEGAL")):(t=ht[dt++],dt>=gt&&E({},ct.UnexpectedToken,"ILLEGAL"),e+=t,"*"===t&&(t=ht[dt],"/"===t&&(e=e.substr(0,e.length-1),i=!1,++dt,n.end={line:vt,column:dt-mt},Ve("Block",e,r,dt,n),e="")));else if("/"===t)if(t=ht[dt+1],"/"===t)n={start:{line:vt,column:dt-mt}},r=dt,dt+=2,a=!0,dt>=gt&&(n.end={line:vt,column:dt-mt},a=!1,Ve("Line",e,r,dt,n));else{if("*"!==t)break;r=dt,dt+=2,i=!0,n={start:{line:vt,column:dt-mt-2}},dt>=gt&&E({},ct.UnexpectedToken,"ILLEGAL")}else if(o(t))++dt;else{if(!s(t))break;++dt,"\r"===t&&"\n"===ht[dt]&&++dt,++vt,mt=dt}}function We(){var e,t,n,r=[];for(e=0;e<_t.comments.length;++e)t=_t.comments[e],n={type:t.type,value:t.value},_t.range&&(n.range=t.range),_t.loc&&(n.loc=t.loc),r.push(n);_t.comments=r}function ze(){var e,t,r,i,a;return d(),e=dt,t={start:{line:vt,column:dt-mt}},r=_t.advance(),t.end={line:vt,column:dt-mt},r.type!==ot.EOF&&(i=[r.range[0],r.range[1]],a=n(r.range[0],r.range[1]),_t.tokens.push({type:st[r.type],value:a,range:i,loc:t})),r}function qe(){var e,t,n,r;return d(),e=dt,t={start:{line:vt,column:dt-mt}},n=_t.scanRegExp(),t.end={line:vt,column:dt-mt},_t.tokens.length>0&&(r=_t.tokens[_t.tokens.length-1],r.range[0]===e&&"Punctuator"===r.type&&("/"!==r.value&&"/="!==r.value||_t.tokens.pop())),_t.tokens.push({type:"RegularExpression",value:n.literal,range:[e,dt],loc:t}),n}function Ge(){var e,t,n,r=[];for(e=0;e<_t.tokens.length;++e)t=_t.tokens[e],n={type:t.type,value:t.value},_t.range&&(n.range=t.range),_t.loc&&(n.loc=t.loc),r.push(n);_t.tokens=r}function Je(e){return{type:lt.Literal,value:e.value}}function Ke(e){return{type:lt.Literal,value:e.value,raw:n(e.range[0],e.range[1])}}function Qe(){var e={};return e.range=[dt,dt],e.loc={start:{line:vt,column:dt-mt},end:{line:vt,column:dt-mt}},e.end=function(){this.range[1]=dt,this.loc.end.line=vt,this.loc.end.column=dt-mt},e.applyGroup=function(e){_t.range&&(e.groupRange=[this.range[0],this.range[1]]),_t.loc&&(e.groupLoc={start:{line:this.loc.start.line,column:this.loc.start.column},end:{line:this.loc.end.line,column:this.loc.end.column}})},e.apply=function(e){_t.range&&(e.range=[this.range[0],this.range[1]]),_t.loc&&(e.loc={start:{line:this.loc.start.line,column:this.loc.start.column},end:{line:this.loc.end.line,column:this.loc.end.column}})},e}function Xe(){var e,t;return d(),e=Qe(),A("("),t=ce(),A(")"),e.end(),e.applyGroup(t),t}function Ze(){var e,t;for(d(),e=Qe(),t=P("new")?G():V();L(".")||L("[");)L("[")?(t={type:lt.MemberExpression,computed:!0,object:t,property:q()},e.end(),e.apply(t)):(t={type:lt.MemberExpression,computed:!1,object:t,property:z()},e.end(),e.apply(t));return t}function Ye(){var e,t;for(d(),e=Qe(),t=P("new")?G():V();L(".")||L("[")||L("(");)L("(")?(t={type:lt.CallExpression,callee:t,arguments:H()},e.end(),e.apply(t)):L("[")?(t={type:lt.MemberExpression,computed:!0,object:t,property:q()},e.end(),e.apply(t)):(t={type:lt.MemberExpression,computed:!1,object:t,property:z()},e.end(),e.apply(t));return t}function et(e){var t,n,r;t="[object Array]"===Object.prototype.toString.apply(e)?[]:{};for(n in e)e.hasOwnProperty(n)&&"groupRange"!==n&&"groupLoc"!==n&&(r=e[n],null===r||"object"!=typeof r||r instanceof RegExp?t[n]=r:t[n]=et(r));return t}function tt(e,t){return function(n){function r(e){return e.type===lt.LogicalExpression||e.type===lt.BinaryExpression}function i(n){var a,o;r(n.left)&&i(n.left),r(n.right)&&i(n.right),e&&(n.left.groupRange||n.right.groupRange?(a=n.left.groupRange?n.left.groupRange[0]:n.left.range[0],o=n.right.groupRange?n.right.groupRange[1]:n.right.range[1],n.range=[a,o]):"undefined"==typeof n.range&&(a=n.left.range[0],o=n.right.range[1],n.range=[a,o])),t&&(n.left.groupLoc||n.right.groupLoc?(a=n.left.groupLoc?n.left.groupLoc.start:n.left.loc.start,o=n.right.groupLoc?n.right.groupLoc.end:n.right.loc.end,n.loc={start:a,end:o}):"undefined"==typeof n.loc&&(n.loc={start:n.left.loc.start,end:n.right.loc.end}))}return function(){var a,o;return d(),a=Qe(),o=n.apply(null,arguments),a.end(),e&&"undefined"==typeof o.range&&a.apply(o),t&&"undefined"==typeof o.loc&&a.apply(o),r(o)&&i(o),o}}}function nt(){var e;_t.comments&&(_t.skipComment=d,d=He),_t.raw&&(_t.createLiteral=Je,Je=Ke),(_t.range||_t.loc)&&(_t.parseGroupExpression=M,_t.parseLeftHandSideExpression=K,_t.parseLeftHandSideExpressionAllowCall=J,M=Xe,K=Ze,J=Ye,e=tt(_t.range,_t.loc),_t.parseAdditiveExpression=Y,_t.parseAssignmentExpression=ue,_t.parseBitwiseANDExpression=re,_t.parseBitwiseORExpression=ae,_t.parseBitwiseXORExpression=ie,_t.parseBlock=he,_t.parseFunctionSourceElements=Fe,_t.parseCatchClause=Pe,_t.parseComputedMember=q,_t.parseConditionalExpression=le,_t.parseConstLetDeclaration=ge,_t.parseEqualityExpression=ne,_t.parseExpression=ce,_t.parseForVariableDeclaration=ke,_t.parseFunctionDeclaration=De,_t.parseFunctionExpression=Re,_t.parseLogicalANDExpression=oe,_t.parseLogicalORExpression=se,_t.parseMultiplicativeExpression=Z,_t.parseNewExpression=G,_t.parseNonComputedProperty=W,_t.parseObjectProperty=B,_t.parseObjectPropertyKey=R,_t.parsePostfixExpression=Q,_t.parsePrimaryExpression=V,_t.parseProgram=Me,_t.parsePropertyFunction=D,_t.parseRelationalExpression=te,_t.parseStatement=Te,_t.parseShiftExpression=ee,_t.parseSwitchCase=Ae,_t.parseUnaryExpression=X,_t.parseVariableDeclaration=de,_t.parseVariableIdentifier=fe,Y=e(_t.parseAdditiveExpression),ue=e(_t.parseAssignmentExpression),re=e(_t.parseBitwiseANDExpression),ae=e(_t.parseBitwiseORExpression),ie=e(_t.parseBitwiseXORExpression),he=e(_t.parseBlock),Fe=e(_t.parseFunctionSourceElements),Pe=e(_t.parseCatchClause),q=e(_t.parseComputedMember),le=e(_t.parseConditionalExpression),ge=e(_t.parseConstLetDeclaration),ne=e(_t.parseEqualityExpression),ce=e(_t.parseExpression),ke=e(_t.parseForVariableDeclaration),De=e(_t.parseFunctionDeclaration),Re=e(_t.parseFunctionExpression),K=e(K),oe=e(_t.parseLogicalANDExpression),se=e(_t.parseLogicalORExpression),Z=e(_t.parseMultiplicativeExpression),G=e(_t.parseNewExpression),W=e(_t.parseNonComputedProperty),B=e(_t.parseObjectProperty),R=e(_t.parseObjectPropertyKey),Q=e(_t.parsePostfixExpression),V=e(_t.parsePrimaryExpression),Me=e(_t.parseProgram),D=e(_t.parsePropertyFunction),te=e(_t.parseRelationalExpression),Te=e(_t.parseStatement),ee=e(_t.parseShiftExpression),Ae=e(_t.parseSwitchCase),X=e(_t.parseUnaryExpression),de=e(_t.parseVariableDeclaration),fe=e(_t.parseVariableIdentifier)),"undefined"!=typeof _t.tokens&&(_t.advance=w,_t.scanRegExp=_,w=ze,_=qe)}function rt(){"function"==typeof _t.skipComment&&(d=_t.skipComment),_t.raw&&(Je=_t.createLiteral),(_t.range||_t.loc)&&(Y=_t.parseAdditiveExpression,ue=_t.parseAssignmentExpression,re=_t.parseBitwiseANDExpression,ae=_t.parseBitwiseORExpression,ie=_t.parseBitwiseXORExpression,he=_t.parseBlock,Fe=_t.parseFunctionSourceElements,Pe=_t.parseCatchClause,q=_t.parseComputedMember,le=_t.parseConditionalExpression,ge=_t.parseConstLetDeclaration,ne=_t.parseEqualityExpression,ce=_t.parseExpression,ke=_t.parseForVariableDeclaration,De=_t.parseFunctionDeclaration,Re=_t.parseFunctionExpression,M=_t.parseGroupExpression,K=_t.parseLeftHandSideExpression,J=_t.parseLeftHandSideExpressionAllowCall,oe=_t.parseLogicalANDExpression,se=_t.parseLogicalORExpression,Z=_t.parseMultiplicativeExpression,G=_t.parseNewExpression,W=_t.parseNonComputedProperty,B=_t.parseObjectProperty,R=_t.parseObjectPropertyKey,V=_t.parsePrimaryExpression,Q=_t.parsePostfixExpression,Me=_t.parseProgram,D=_t.parsePropertyFunction,te=_t.parseRelationalExpression,Te=_t.parseStatement,ee=_t.parseShiftExpression,Ae=_t.parseSwitchCase,X=_t.parseUnaryExpression,de=_t.parseVariableDeclaration,fe=_t.parseVariableIdentifier),"function"==typeof _t.scanRegExp&&(w=_t.advance,_=_t.scanRegExp)}function it(e){var t,n=e.length,r=[];for(t=0;t0?1:0,mt=0,gt=ht.length,yt=null,bt={allowIn:!0,labelSet:{},inFunctionBody:!1,inIteration:!1,inSwitch:!1},_t={},"undefined"!=typeof t&&(_t.range="boolean"==typeof t.range&&t.range,_t.loc="boolean"==typeof t.loc&&t.loc,_t.raw="boolean"==typeof t.raw&&t.raw,"boolean"==typeof t.tokens&&t.tokens&&(_t.tokens=[]),"boolean"==typeof t.comment&&t.comment&&(_t.comments=[]),"boolean"==typeof t.tolerant&&t.tolerant&&(_t.errors=[])),gt>0&&"undefined"==typeof ht[0]&&(e instanceof String&&(ht=e.valueOf()),"undefined"==typeof ht[0]&&(ht=it(e))),nt();try{n=Me(),"undefined"!=typeof _t.comments&&(We(),n.comments=_t.comments),"undefined"!=typeof _t.tokens&&(Ge(),n.tokens=_t.tokens),"undefined"!=typeof _t.errors&&(n.errors=_t.errors),(_t.range||_t.loc)&&(n.body=et(n.body))}catch(e){throw e}finally{rt(),_t={}}return n}var ot,st,lt,ut,ct,pt,ht,ft,dt,vt,mt,gt,yt,bt,_t;ot={BooleanLiteral:1,EOF:2,Identifier:3,Keyword:4,NullLiteral:5,NumericLiteral:6,Punctuator:7,StringLiteral:8},st={},st[ot.BooleanLiteral]="Boolean",st[ot.EOF]="",st[ot.Identifier]="Identifier",st[ot.Keyword]="Keyword",st[ot.NullLiteral]="Null",st[ot.NumericLiteral]="Numeric",st[ot.Punctuator]="Punctuator",st[ot.StringLiteral]="String",lt={AssignmentExpression:"AssignmentExpression",ArrayExpression:"ArrayExpression",BlockStatement:"BlockStatement",BinaryExpression:"BinaryExpression",BreakStatement:"BreakStatement",CallExpression:"CallExpression",CatchClause:"CatchClause",ConditionalExpression:"ConditionalExpression",ContinueStatement:"ContinueStatement",DoWhileStatement:"DoWhileStatement",DebuggerStatement:"DebuggerStatement",EmptyStatement:"EmptyStatement",ExpressionStatement:"ExpressionStatement",ForStatement:"ForStatement",ForInStatement:"ForInStatement",FunctionDeclaration:"FunctionDeclaration",FunctionExpression:"FunctionExpression",Identifier:"Identifier",IfStatement:"IfStatement",Literal:"Literal",LabeledStatement:"LabeledStatement",LogicalExpression:"LogicalExpression",MemberExpression:"MemberExpression",NewExpression:"NewExpression",ObjectExpression:"ObjectExpression",Program:"Program",Property:"Property",ReturnStatement:"ReturnStatement",SequenceExpression:"SequenceExpression",SwitchStatement:"SwitchStatement",SwitchCase:"SwitchCase",ThisExpression:"ThisExpression",ThrowStatement:"ThrowStatement",TryStatement:"TryStatement",UnaryExpression:"UnaryExpression",UpdateExpression:"UpdateExpression",VariableDeclaration:"VariableDeclaration",VariableDeclarator:"VariableDeclarator",WhileStatement:"WhileStatement",WithStatement:"WithStatement"},ut={Data:1,Get:2,Set:4},ct={UnexpectedToken:"Unexpected token %0",UnexpectedNumber:"Unexpected number",UnexpectedString:"Unexpected string",UnexpectedIdentifier:"Unexpected identifier",UnexpectedReserved:"Unexpected reserved word",UnexpectedEOS:"Unexpected end of input",NewlineAfterThrow:"Illegal newline after throw",InvalidRegExp:"Invalid regular expression",UnterminatedRegExp:"Invalid regular expression: missing /",InvalidLHSInAssignment:"Invalid left-hand side in assignment",InvalidLHSInForIn:"Invalid left-hand side in for-in",MultipleDefaultsInSwitch:"More than one default clause in switch statement",NoCatchOrFinally:"Missing catch or finally after try",UnknownLabel:"Undefined label '%0'",Redeclaration:"%0 '%1' has already been declared",IllegalContinue:"Illegal continue statement",IllegalBreak:"Illegal break statement",IllegalReturn:"Illegal return statement",StrictModeWith:"Strict mode code may not include a with statement",StrictCatchVariable:"Catch variable may not be eval or arguments in strict mode",StrictVarName:"Variable name may not be eval or arguments in strict mode",StrictParamName:"Parameter name eval or arguments is not allowed in strict mode",StrictParamDupe:"Strict mode function may not have duplicate parameter names",StrictFunctionName:"Function name may not be eval or arguments in strict mode",StrictOctalLiteral:"Octal literals are not allowed in strict mode.",StrictDelete:"Delete of an unqualified identifier in strict mode.",StrictDuplicateProperty:"Duplicate data property in object literal not allowed in strict mode",AccessorDataProperty:"Object literal may not have data and accessor property with the same name",AccessorGetSet:"Object literal may not have multiple get/set accessors with the same name",StrictLHSAssignment:"Assignment to eval or arguments is not allowed in strict mode",StrictLHSPostfix:"Postfix increment/decrement may not have eval or arguments operand in strict mode",StrictLHSPrefix:"Prefix increment/decrement may not have eval or arguments operand in strict mode",StrictReservedWord:"Use of future reserved word in strict mode"},pt={NonAsciiIdentifierStart:new RegExp("[\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f2\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u08a0\u08a2-\u08ac\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0e01-\u0e30\u0e32\u0e33\u0e40-\u0e46\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb0\u0eb2\u0eb3\u0ebd\u0ec0-\u0ec4\u0ec6\u0edc-\u0edf\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u1000-\u102a\u103f\u1050-\u1055\u105a-\u105d\u1061\u1065\u1066\u106e-\u1070\u1075-\u1081\u108e\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1780-\u17b3\u17d7\u17dc\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1950-\u196d\u1970-\u1974\u1980-\u19ab\u19c1-\u19c7\u1a00-\u1a16\u1a20-\u1a54\u1aa7\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bba-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1cf5\u1cf6\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2cf2\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303c\u3041-\u3096\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uaa60-\uaa76\uaa7a\uaa80-\uaaaf\uaab1\uaab5\uaab6\uaab9-\uaabd\uaac0\uaac2\uaadb-\uaadd\uaae0-\uaaea\uaaf2-\uaaf4\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]"),NonAsciiIdentifierPart:new RegExp("[\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0300-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u0483-\u0487\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u05d0-\u05ea\u05f0-\u05f2\u0610-\u061a\u0620-\u0669\u066e-\u06d3\u06d5-\u06dc\u06df-\u06e8\u06ea-\u06fc\u06ff\u0710-\u074a\u074d-\u07b1\u07c0-\u07f5\u07fa\u0800-\u082d\u0840-\u085b\u08a0\u08a2-\u08ac\u08e4-\u08fe\u0900-\u0963\u0966-\u096f\u0971-\u0977\u0979-\u097f\u0981-\u0983\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bc-\u09c4\u09c7\u09c8\u09cb-\u09ce\u09d7\u09dc\u09dd\u09df-\u09e3\u09e6-\u09f1\u0a01-\u0a03\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a59-\u0a5c\u0a5e\u0a66-\u0a75\u0a81-\u0a83\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abc-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ad0\u0ae0-\u0ae3\u0ae6-\u0aef\u0b01-\u0b03\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3c-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b5c\u0b5d\u0b5f-\u0b63\u0b66-\u0b6f\u0b71\u0b82\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd0\u0bd7\u0be6-\u0bef\u0c01-\u0c03\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c58\u0c59\u0c60-\u0c63\u0c66-\u0c6f\u0c82\u0c83\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbc-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0cde\u0ce0-\u0ce3\u0ce6-\u0cef\u0cf1\u0cf2\u0d02\u0d03\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d-\u0d44\u0d46-\u0d48\u0d4a-\u0d4e\u0d57\u0d60-\u0d63\u0d66-\u0d6f\u0d7a-\u0d7f\u0d82\u0d83\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e01-\u0e3a\u0e40-\u0e4e\u0e50-\u0e59\u0e81\u0e82\u0e84\u0e87\u0e88\u0e8a\u0e8d\u0e94-\u0e97\u0e99-\u0e9f\u0ea1-\u0ea3\u0ea5\u0ea7\u0eaa\u0eab\u0ead-\u0eb9\u0ebb-\u0ebd\u0ec0-\u0ec4\u0ec6\u0ec8-\u0ecd\u0ed0-\u0ed9\u0edc-\u0edf\u0f00\u0f18\u0f19\u0f20-\u0f29\u0f35\u0f37\u0f39\u0f3e-\u0f47\u0f49-\u0f6c\u0f71-\u0f84\u0f86-\u0f97\u0f99-\u0fbc\u0fc6\u1000-\u1049\u1050-\u109d\u10a0-\u10c5\u10c7\u10cd\u10d0-\u10fa\u10fc-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u135d-\u135f\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176c\u176e-\u1770\u1772\u1773\u1780-\u17d3\u17d7\u17dc\u17dd\u17e0-\u17e9\u180b-\u180d\u1810-\u1819\u1820-\u1877\u1880-\u18aa\u18b0-\u18f5\u1900-\u191c\u1920-\u192b\u1930-\u193b\u1946-\u196d\u1970-\u1974\u1980-\u19ab\u19b0-\u19c9\u19d0-\u19d9\u1a00-\u1a1b\u1a20-\u1a5e\u1a60-\u1a7c\u1a7f-\u1a89\u1a90-\u1a99\u1aa7\u1b00-\u1b4b\u1b50-\u1b59\u1b6b-\u1b73\u1b80-\u1bf3\u1c00-\u1c37\u1c40-\u1c49\u1c4d-\u1c7d\u1cd0-\u1cd2\u1cd4-\u1cf6\u1d00-\u1de6\u1dfc-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u200c\u200d\u203f\u2040\u2054\u2071\u207f\u2090-\u209c\u20d0-\u20dc\u20e1\u20e5-\u20f0\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cf3\u2d00-\u2d25\u2d27\u2d2d\u2d30-\u2d67\u2d6f\u2d7f-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2de0-\u2dff\u2e2f\u3005-\u3007\u3021-\u302f\u3031-\u3035\u3038-\u303c\u3041-\u3096\u3099\u309a\u309d-\u309f\u30a1-\u30fa\u30fc-\u30ff\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\u31f0-\u31ff\u3400-\u4db5\u4e00-\u9fcc\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua62b\ua640-\ua66f\ua674-\ua67d\ua67f-\ua697\ua69f-\ua6f1\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790-\ua793\ua7a0-\ua7aa\ua7f8-\ua827\ua840-\ua873\ua880-\ua8c4\ua8d0-\ua8d9\ua8e0-\ua8f7\ua8fb\ua900-\ua92d\ua930-\ua953\ua960-\ua97c\ua980-\ua9c0\ua9cf-\ua9d9\uaa00-\uaa36\uaa40-\uaa4d\uaa50-\uaa59\uaa60-\uaa76\uaa7a\uaa7b\uaa80-\uaac2\uaadb-\uaadd\uaae0-\uaaef\uaaf2-\uaaf6\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabea\uabec\uabed\uabf0-\uabf9\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\uf900-\ufa6d\ufa70-\ufad9\ufb00-\ufb06\ufb13-\ufb17\ufb1d-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe00-\ufe0f\ufe20-\ufe26\ufe33\ufe34\ufe4d-\ufe4f\ufe70-\ufe74\ufe76-\ufefc\uff10-\uff19\uff21-\uff3a\uff3f\uff41-\uff5a\uff66-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]")},"undefined"==typeof"esprima"[0]&&(n=function(e,t){return ht.slice(e,t).join("")}),e.version="1.0.4",e.parse=at,e.Syntax=function(){var e,t={};"function"==typeof Object.create&&(t=Object.create(null));for(e in lt)lt.hasOwnProperty(e)&&(t[e]=lt[e]); +return"function"==typeof Object.freeze&&Object.freeze(t),t}()}),e.exports}({exports:{}}),oi=function(e,t){function n(e,t){var n=i(e),r=Object.create(t||{});return f(a(n,r))}function r(e){var t=Object.create(e||{});return function(){var e=Array.prototype.slice.call(arguments),n=e.slice(-1)[0];e=e.slice(0,-1),"string"==typeof n&&(n=m("function a(){"+n+"}").body[0].body);var r=i(n);return h(r,e,t)}}function i(e){var t="string"==typeof e?m(e):e;return g(t)}function a(e,t){function n(e){for(var t=void 0,n=0;n":return j>S;case">=":return j>=S;case"|":return j|S;case"&":return j&S;case"^":return j^S;case"instanceof":return j instanceof S;default:return o(e)}case"LogicalExpression":switch(e.operator){case"&&":return i(e.left)&&i(e.right);case"||":return i(e.left)||i(e.right);default:return o(e)}case"ThisExpression":return w.this;case"Identifier":if("undefined"===e.name)return;if(l(w,e.name,x))return f(w[e.name]);throw new ReferenceError(e.name+" is not defined");case"CallExpression":var I=e.arguments.map(function(e){return i(e)}),T=null,$=i(e.callee);return"MemberExpression"===e.callee.type&&(T=i(e.callee.object)),$.apply(T,I);case"MemberExpression":var L=i(e.object);if(e.computed)var P=i(e.property);else var P=e.property.name;return L=x.getPropertyObject(L,P),a(L[P]);case"ConditionalExpression":var N=i(e.test);return i(N?e.consequent:e.alternate);case"EmptyStatement":return;default:return o(e)}}function a(e){return e===si&&(e=g),f(e)}function u(){w=Object.create(w)}function c(){w=Object.getPrototypeOf(w)}function m(e,t,n,r){var a=null;if("Identifier"===t.type?(a=t.name,e=s(e,a,x)):"MemberExpression"===t.type&&(a=t.computed?i(t.property):t.property.name,e=i(t.object)),p(e,a,x))switch(r){case void 0:return e[a]=i(n);case"=":return e[a]=i(n);case"+=":return e[a]+=i(n);case"-=":return e[a]-=i(n);case"++":return e[a]++;case"--":return e[a]--}}var g=r(t),x=b(t),w=t;return i(e)}function o(e){console.error(e);var t=new Error("Unsupported expression: "+e.type);throw t.node=e,t}function s(e,t,n){var r=n.getPrototypeOf(e);return!r||u(e,t)?e:s(r,t,n)}function l(e,t,n){var r=n.getPrototypeOf(e),i=u(e,t);return void 0!==e[t]||(!r||i?i:l(r,t,n))}function u(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function c(e,t){return Object.prototype.propertyIsEnumerable.call(e,t)}function p(e,t,n){return"__proto__"!==t&&!n.isPrimitive(e)&&(null==e||(u(e,t)?!!c(e,t):p(n.getPrototypeOf(e),t,n)))}function h(e,n,r){return function(){var i=Object.create(r);this==t?i.this=null:i.this=this;var o=Array.prototype.slice.call(arguments);i.arguments=arguments,o.forEach(function(e,t){var r=n[t];r&&(i[r]=e)});var s=a(e,i);if(s instanceof v)return s.value}}function f(e){return e instanceof v?e.value:e}function d(e){return e.name}function v(e,t){this.type=e,this.value=t}var m=(e.exports,ai.parse),g=ii,y=ri,b=ni;e.exports=n,e.exports.FunctionFactory=r,e.exports.Function=r();var _=1e6;return e.exports}({exports:{}},ti),si=oi.Function,li=new S(1e3),ui=0,ci=1,pi=2,hi=3,fi=0,di=1,vi=2,mi=3,gi=4,yi=5,bi=6,_i=7,xi=8,wi=[];wi[fi]={ws:[fi],ident:[mi,ui],"[":[gi],eof:[_i]},wi[di]={ws:[di],".":[vi],"[":[gi],eof:[_i]},wi[vi]={ws:[vi],ident:[mi,ui]},wi[mi]={ident:[mi,ui],0:[mi,ui],number:[mi,ui],ws:[di,ci],".":[vi,ci],"[":[gi,ci],eof:[_i,ci]},wi[gi]={"'":[yi,ui],'"':[bi,ui],"[":[gi,pi],"]":[di,hi],eof:xi,else:[gi,ui]},wi[yi]={"'":[gi,ui],eof:xi,else:[yi,ui]},wi[bi]={'"':[gi,ui],eof:xi,else:[bi,ui]};var ki=Object.freeze({parsePath:Ve,getPath:He,setPath:We}),Si=new S(1e3),Ci="Math,Date,this,true,false,null,undefined,Infinity,NaN,isNaN,isFinite,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,parseInt,parseFloat",Ei=new RegExp("^("+Ci.replace(/,/g,"\\b|")+"\\b)"),$i="break,case,class,catch,const,continue,debugger,default,delete,do,else,export,extends,finally,for,function,if,import,in,instanceof,let,return,super,switch,throw,try,var,while,with,yield,enum,await,implements,package,protected,static,interface,private,public",Oi=new RegExp("^("+$i.replace(/,/g,"\\b|")+"\\b)"),Ai=/\s/g,Ni=/\n/g,Li=/[\{,]\s*[\w\$_]+\s*:|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\"']|\\.)*`|`(?:[^`\\]|\\.)*`)|new |typeof |void /g,Pi=/"(\d+)"/g,Ii=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['.*?'\]|\[".*?"\]|\[\d+\]|\[[A-Za-z_$][\w$]*\])*$/,ji=/[^\w$\.](?:[A-Za-z_$][\w$]*)/g,Ti=/^(?:true|false|null|undefined|Infinity|NaN)$/,Fi=[],Di=Object.freeze({parseExpression:Ze,isSimplePath:Ye}),Ri=[],Bi=[],Ui={},Mi={},Vi=!1,Hi=0;it.prototype.get=function(){this.beforeGet();var e,t=this.scope||this.vm;try{e=this.getter.call(t,t)}catch(e){}return this.deep&&at(e),this.preProcess&&(e=this.preProcess(e)),this.filters&&(e=t._applyFilters(e,null,this.filters,!1)),this.postProcess&&(e=this.postProcess(e)),this.afterGet(),e},it.prototype.set=function(e){var t=this.scope||this.vm;this.filters&&(e=t._applyFilters(e,this.value,this.filters,!0));try{this.setter.call(t,t,e)}catch(e){}var n=t.$forContext;if(n&&n.alias===this.expression){if(n.filters)return;n._withLock(function(){t.$key?n.rawValue[t.$key]=e:n.rawValue.$set(t.$index,e)})}},it.prototype.beforeGet=function(){Le.target=this},it.prototype.addDep=function(e){var t=e.id;this.newDepIds.has(t)||(this.newDepIds.add(t),this.newDeps.push(e),this.depIds.has(t)||e.addSub(this))},it.prototype.afterGet=function(){Le.target=null;for(var e=this.deps.length;e--;){var t=this.deps[e];this.newDepIds.has(t.id)||t.removeSub(this)}var n=this.depIds;this.depIds=this.newDepIds,this.newDepIds=n,this.newDepIds.clear(),n=this.deps,this.deps=this.newDeps,this.newDeps=n,this.newDeps.length=0},it.prototype.update=function(e){this.lazy?this.dirty=!0:this.sync||!Ur.async?this.run():(this.shallow=this.queued?!!e&&this.shallow:!!e,this.queued=!0,rt(this))},it.prototype.run=function(){if(this.active){var e=this.get();if(e!==this.value||(m(e)||this.deep)&&!this.shallow){var t=this.value;this.value=e;this.prevError;this.cb.call(this.vm,e,t)}this.queued=this.shallow=!1}},it.prototype.evaluate=function(){var e=Le.target;this.value=this.get(),this.dirty=!1,Le.target=e},it.prototype.depend=function(){for(var e=this.deps.length;e--;)this.deps[e].depend()},it.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||this.vm._vForRemoving||this.vm._watchers.$remove(this);for(var e=this.deps.length;e--;)this.deps[e].removeSub(this);this.active=!1,this.vm=this.cb=this.value=null}};var Wi=new cr,zi={bind:function(){this.attr=3===this.el.nodeType?"data":"textContent"},update:function(e){this.el[this.attr]=a(e)}},qi=new S(1e3),Gi=new S(1e3),Ji={efault:[0,"",""],legend:[1,"
","
"],tr:[2,"","
"],col:[2,"","
"]};Ji.td=Ji.th=[3,"","
"],Ji.option=Ji.optgroup=[1,'"],Ji.thead=Ji.tbody=Ji.colgroup=Ji.caption=Ji.tfoot=[1,"","
"],Ji.g=Ji.defs=Ji.symbol=Ji.use=Ji.image=Ji.text=Ji.circle=Ji.ellipse=Ji.line=Ji.path=Ji.polygon=Ji.polyline=Ji.rect=[1,'',""];var Ki=/<([\w:-]+)/,Qi=/&#?\w+?;/,Xi=/ diff --git a/src/popup.ts b/src/popup.ts index 9b49f6f5a..13110b6d3 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -2,6 +2,7 @@ /// // need to find a better way to handle Vue types without modules +// we use vue 1.0 here to solve csp issues /* tslint:disable-next-line:no-any */ declare var Vue: any; From cbe3e6bf8c7deeaaddcd1e50bb054ea678ad39df Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sun, 4 Feb 2018 14:06:26 +0800 Subject: [PATCH 010/178] save --- css/popup.css | 44 ++++++++++++++++++++--------------------- popup.html | 8 ++++---- src/popup.ts | 55 ++++++++++++++++++++++++++++++++++++++++++++++----- 3 files changed, 76 insertions(+), 31 deletions(-) diff --git a/css/popup.css b/css/popup.css index f3e5e62a2..4f6c7cc43 100644 --- a/css/popup.css +++ b/css/popup.css @@ -6,7 +6,7 @@ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; } -@-webkit-keyframes twinkling{ +@keyframes twinkling{ 0%{ color:#DD4B39; } @@ -15,7 +15,7 @@ } } -@-webkit-keyframes fadeshow{ +@keyframes fadeshow{ 0%{ opacity:0; } @@ -24,7 +24,7 @@ } } -@-webkit-keyframes fadehide{ +@keyframes fadehide{ 0%{ opacity:1; } @@ -33,7 +33,7 @@ } } -@-webkit-keyframes fadein{ +@keyframes fadein{ 0%{ opacity:0; top:110px; @@ -44,7 +44,7 @@ } } -@-webkit-keyframes fadeout{ +@keyframes fadeout{ 0%{ opacity:1; top:10px; @@ -55,7 +55,7 @@ } } -@-webkit-keyframes slidein{ +@keyframes slidein{ 0%{ opacity:0; left:-55px; @@ -66,7 +66,7 @@ } } -@-webkit-keyframes slideout{ +@keyframes slideout{ 0%{ opacity:1; left:0; @@ -77,7 +77,7 @@ } } -@-webkit-keyframes qrfadein{ +@keyframes qrfadein{ 0%{ opacity:0; } @@ -86,7 +86,7 @@ } } -@-webkit-keyframes qrfadeout{ +@keyframes qrfadeout{ 0%{ opacity:1; } @@ -139,12 +139,12 @@ body { #notification.fadein { top: 190px; - -webkit-animation: fadeshow 0.2s 1 ease-out; + animation: fadeshow 0.2s 1 ease-out; } #notification.fadeout { top: 190px; - -webkit-animation: fadehide 0.2s 1 ease-in; + animation: fadehide 0.2s 1 ease-in; } #codes { @@ -201,7 +201,7 @@ body { cursor: pointer; overflow: hidden; border-radius: 2px; - -webkit-transition: height 0.2s; + transition: height 0.2s; } #codes:not(.edit) .entry[unencrypted="true"]:hover .warning { @@ -224,14 +224,14 @@ body { font-size: 36px; color: #08C; width: 80%; - -webkit-user-select: text; + user-select: text; font-family: 'Droid Sans Mono'; cursor: pointer; } #codes.edit .code { color: #CCC!important; - -webkit-user-select: none; + user-select: none; cursor: default; } @@ -262,7 +262,7 @@ body { } #codes.timeout .code:not(.hotp) { - -webkit-animation: twinkling 1s infinite ease-in-out; + animation: twinkling 1s infinite ease-in-out; } .hotp { @@ -301,7 +301,7 @@ body { opacity: 0; } -.showqr:hover { +.entry:hover .showqr { opacity: 1; } @@ -447,12 +447,12 @@ body { #menu.slidein { left: 0; - -webkit-animation: slidein 0.2s 1 ease-out; + animation: slidein 0.2s 1 ease-out; } #menu.slideout { left: -55px; - -webkit-animation: slideout 0.2s 1 ease-in; + animation: slideout 0.2s 1 ease-in; } #menuHead { @@ -531,7 +531,7 @@ body { #export.fadein, #resize.fadein { top: 10px; - -webkit-animation: fadein 0.2s 1 ease-out; + animation: fadein 0.2s 1 ease-out; } #info.fadeout, @@ -541,7 +541,7 @@ body { #export.fadeout, #resize.fadeout { top: 110px; - -webkit-animation: fadeout 0.2s 1 ease-in; + animation: fadeout 0.2s 1 ease-in; } #infoClose, @@ -632,12 +632,12 @@ body { #qr.qrfadein { top: 0; - -webkit-animation: qrfadein 0.2s 1 ease-out; + animation: qrfadein 0.2s 1 ease-out; } #qr.qrfadeout { top: 0; - -webkit-animation: qrfadeout 0.2s 1 ease-in; + animation: qrfadeout 0.2s 1 ease-in; } #secret_box { diff --git a/popup.html b/popup.html index b213d9f05..5bb990a69 100644 --- a/popup.html +++ b/popup.html @@ -13,17 +13,17 @@ -
+
-
+
{{ entry.issuer }}
-
{{ entry.code }}
+
diff --git a/src/popup.ts b/src/popup.ts index 13110b6d3..193df47db 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -47,16 +47,61 @@ async function getEntries() { return entries; } +/* tslint:disable-next-line:no-any */ +async function updateCode(app: any) { + let second = new Date().getSeconds(); + if (localStorage.offset) { + second += Number(localStorage.offset) + 30; + } + second = second % 30; + app.sector = getSector(second); + if (second > 25) { + app.class.timeout = true; + } else { + app.class.timeout = false; + } + if (second < 1) { + app.entries = await getEntries(); + } +} + +function getSector(second: number) { + const canvas = document.createElement('canvas'); + canvas.width = 40; + canvas.height = 40; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + ctx.fillStyle = '#888'; + ctx.beginPath(); + ctx.moveTo(20, 20); + ctx.arc( + 20, 20, 16, second / 30 * Math.PI * 2 - Math.PI / 2, Math.PI * 3 / 2, + false); + ctx.fill(); + const url = canvas.toDataURL(); + return `url(${url}) center / 20px 20px`; +} + async function init() { const i18n = await loadI18nMessages(); const entries = await getEntries(); - const authenticator = - new Vue({el: '#authenticator', data: {i18n, entries}, methods: {}}); + const authenticator = new Vue({ + el: '#authenticator', + data: {i18n, entries, class: {timeout: false, edit: false}, sector: ''}, + methods: { + showBulls: (code: string) => { + return new Array(code.length).fill('•').join(''); + } + } + }); - // setInterval(async () => { - // authenticator.entries = await getEntries(); - // }, 1000); + updateCode(authenticator); + setInterval(async () => { + await updateCode(authenticator); + }, 1000); return; } From 7ffddf6db830d4020eb7ac3d7815160b7549416e Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sun, 4 Feb 2018 23:37:26 +0800 Subject: [PATCH 011/178] save --- popup.html | 8 ++++++-- src/background.ts | 14 -------------- src/models/storage.ts | 3 ++- src/popup.ts | 24 +++++++++++++++++------- 4 files changed, 25 insertions(+), 24 deletions(-) diff --git a/popup.html b/popup.html index 5bb990a69..04e0836bd 100644 --- a/popup.html +++ b/popup.html @@ -4,9 +4,13 @@ - + + + +
diff --git a/src/background.ts b/src/background.ts index 4ba837420..36963e0b9 100644 --- a/src/background.ts +++ b/src/background.ts @@ -3,22 +3,8 @@ /// /// -let encryption = new Encription(''); - -function _updateEncription(password: string) { - encryption = new Encription(password); -} - -async function _getEntries() { - const optEntries: OTP[] = await EntryStorage.get(encryption); - return optEntries; -} - chrome.runtime.onMessage.addListener((request, sender, cb) => { switch (request.action) { - case 'GET_ENTRIES': - _getEntries().then(cb); - break; default: break; } diff --git a/src/models/storage.ts b/src/models/storage.ts index d7f37a1ef..cc8616d8e 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -145,7 +145,8 @@ class EntryStorage { delete _data[entry.hash]; } _data = this.ensureUniqueIndex(_data); - return resolve(); + chrome.storage.sync.set(_data, resolve); + return; }); return; } catch (error) { diff --git a/src/popup.ts b/src/popup.ts index 193df47db..66ace558e 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -1,11 +1,24 @@ /* tslint:disable:no-reference */ +/// /// +/// // need to find a better way to handle Vue types without modules // we use vue 1.0 here to solve csp issues /* tslint:disable-next-line:no-any */ declare var Vue: any; +let encryption = new Encription(''); + +function updateEncription(password: string) { + encryption = new Encription(password); +} + +async function getEntries() { + const optEntries: OTP[] = await EntryStorage.get(encryption); + return optEntries; +} + async function loadI18nMessages() { return new Promise( (resolve: (value: {[key: string]: string}) => void, @@ -41,12 +54,6 @@ async function getDataFromBackground(command: {}) { }); } -async function getEntries() { - const entries: OTP[] = - await getDataFromBackground({action: 'GET_ENTRIES'}); - return entries; -} - /* tslint:disable-next-line:no-any */ async function updateCode(app: any) { let second = new Date().getSeconds(); @@ -61,7 +68,10 @@ async function updateCode(app: any) { app.class.timeout = false; } if (second < 1) { - app.entries = await getEntries(); + const entries = app.entries as OTP[]; + for (let i = 0; i < entries.length; i++) { + entries[i].generate(); + } } } From a485d6d98835f2010ecdcccaca6944c55e77eca3 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Mon, 5 Feb 2018 00:16:26 +0800 Subject: [PATCH 012/178] save --- _locales/en/messages.json | 10 +++++- _locales/zh_CN/messages.json | 12 +++++-- css/popup.css | 2 ++ popup.html | 33 ++++++++++++++--- src/models/interface.ts | 4 +-- src/models/otp.ts | 11 ++---- src/models/storage.ts | 70 +++++++++++++----------------------- src/popup.ts | 55 +++++++++++++++++++++++----- 8 files changed, 123 insertions(+), 74 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a8af031a3..9470e288d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -123,6 +123,10 @@ "message": "Feedback", "description": "Feedback." }, + "translate": { + "message": "Translate", + "description": "Translate." + }, "source": { "message": "Source Code", "description": "Source Code." @@ -144,7 +148,7 @@ "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "Do you have a backup for your secrets? Please note that no one can help you with getting back locked account, don't wait until it's too late. We will remind you to make a backup again after 30 days.", + "message": "NEVER REINSTALL THE EXTENSION TO TRY TO FIX ANY ISSUE, OR YOU WILL LOSE ALL YOUR DATA! Do you have a backup for your secrets? Please note that no one can help you with getting back locked account, don't wait until it's too late. We will remind you to make a backup again after 30 days.", "description": "Remind Backup" }, "capture_failed": { @@ -170,5 +174,9 @@ "scale": { "message": "Scale", "description": "Scale" + }, + "export_info": { + "message": "Copy this text and save it somewhere else to backup your secrets. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", + "description": "Export menu info text" } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 94a2be19f..241bbff56 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -123,6 +123,10 @@ "message": "问题反馈", "description": "Feedback." }, + "translate": { + "message": "参与翻译", + "description": "Translate." + }, "source": { "message": "源代码", "description": "Source Code." @@ -144,7 +148,7 @@ "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", + "message": "永远不要通过重装扩展来尝试解决问题,否则您将丢失全部数据!您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", "description": "Remind Backup" }, "capture_failed": { @@ -170,5 +174,9 @@ "scale": { "message": "比例", "description": "Scale" - } + }, + "export_info": { + "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", + "description": "Export menu info text" + } } diff --git a/css/popup.css b/css/popup.css index 4f6c7cc43..686bd3d6e 100644 --- a/css/popup.css +++ b/css/popup.css @@ -448,11 +448,13 @@ body { #menu.slidein { left: 0; animation: slidein 0.2s 1 ease-out; + opacity: 1; } #menu.slideout { left: -55px; animation: slideout 0.2s 1 ease-in; + opacity: 0; } #menuHead { diff --git a/popup.html b/popup.html index 04e0836bd..319c3aeb0 100644 --- a/popup.html +++ b/popup.html @@ -16,25 +16,48 @@
- diff --git a/src/models/interface.ts b/src/models/interface.ts index 96c9229a9..959360793 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -19,9 +19,7 @@ interface OTP { counter: number; code: string; create(encryption: Encription): Promise; - update( - encryption: Encription, issuer: string, account: string, index: number, - counter: number): Promise; + update(encryption: Encription): Promise; next(encryption: Encription): Promise; delete(): Promise; generate(): void; diff --git a/src/models/otp.ts b/src/models/otp.ts index e6ec2390b..2066c2b77 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -32,13 +32,7 @@ class OTPEntry implements OTP { return; } - async update( - encryption: Encription, issuer: string, account: string, index: number, - counter: number) { - this.issuer = issuer; - this.account = account; - this.index = index; - this.counter = counter; + async update(encryption: Encription) { EntryStorage.update(encryption, this); return; } @@ -53,8 +47,7 @@ class OTPEntry implements OTP { return; } this.counter++; - await this.update( - encryption, this.issuer, this.secret, this.index, this.counter); + await this.update(encryption); return; } diff --git a/src/models/storage.ts b/src/models/storage.ts index cc8616d8e..e2af66d15 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -85,57 +85,37 @@ class EntryStorage { (resolve: (value: OTPEntry[]) => void, reject: (reason: Error) => void) => { try { - const data: OTPEntry[] = []; - data.push(new OTPEntry( - OTPType.totp, 'test issuer', 'abcd2345', 'sneezry', 0)); - data.push(new OTPEntry( - OTPType.totp, 'test issuer1', 'bbcd2345', 'sneezry1', 1)); - data.push(new OTPEntry( - OTPType.totp, 'test issuer2', 'abcc2345', 'sneezry2', 2)); - return resolve(data); + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { + const data: OTPEntry[] = []; + for (const hash of Object.keys(_data)) { + const entryData = _data[hash]; + let type: OTPType; + switch (entryData.type) { + case 'totp': + case 'hotp': + case 'battle': + case 'steam': + type = OTPType[entryData.type]; + break; + default: + type = OTPType.totp; + } + entryData.secret = + encryption.getDecryptedSecret(entryData.secret); + const entry = new OTPEntry( + type, entryData.issuer, entryData.secret, entryData.account, + entryData.index); + data.push(entry); + } + return resolve(data); + }); + return; } catch (error) { return reject(error); } }); } - // static async get(encryption: Encription) { - // return new Promise( - // (resolve: (value: OTPEntry[]) => void, - // reject: (reason: Error) => void) => { - // try { - // chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => - // { - // const data: OTPEntry[] = []; - // for (const hash of Object.keys(_data)) { - // const entryData = _data[hash]; - // let type: OTPType; - // switch (entryData.type) { - // case 'totp': - // case 'hotp': - // case 'battle': - // case 'steam': - // type = OTPType[entryData.type]; - // break; - // default: - // type = OTPType.totp; - // } - // entryData.secret = - // encryption.getDecryptedSecret(entryData.secret); - // const entry = new OTPEntry( - // type, entryData.issuer, entryData.secret, - // entryData.account, entryData.index); - // data.push(entry); - // } - // return resolve(data); - // }); - // return; - // } catch (error) { - // return reject(error); - // } - // }); - // } - static async delete(entry: OTPEntry) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { diff --git a/src/popup.ts b/src/popup.ts index 66ace558e..0d0c3bdbe 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -8,17 +8,31 @@ /* tslint:disable-next-line:no-any */ declare var Vue: any; -let encryption = new Encription(''); - -function updateEncription(password: string) { - encryption = new Encription(password); -} - -async function getEntries() { +async function getEntries(encryption: Encription) { const optEntries: OTP[] = await EntryStorage.get(encryption); return optEntries; } +async function getVersion() { + return new Promise( + (resolve: (value: string) => void, reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + const manifest: {version: string} = JSON.parse(xhr.responseText); + return resolve(manifest.version); + } + return; + }; + xhr.open('GET', chrome.extension.getURL('/manifest.json')); + xhr.send(); + } catch (error) { + return reject(error); + } + }); +} + async function loadI18nMessages() { return new Promise( (resolve: (value: {[key: string]: string}) => void, @@ -95,15 +109,38 @@ function getSector(second: number) { } async function init() { + const version = await getVersion(); const i18n = await loadI18nMessages(); - const entries = await getEntries(); + const encryption = new Encription(''); + const entries = await getEntries(encryption); const authenticator = new Vue({ el: '#authenticator', - data: {i18n, entries, class: {timeout: false, edit: false}, sector: ''}, + data: { + version, + i18n, + entries, + encryption, + class: {timeout: false, edit: false, slidein: false, slideout: false}, + sector: '' + }, methods: { showBulls: (code: string) => { return new Array(code.length).fill('•').join(''); + }, + updateEncription: (password: string) => { + authenticator.encryption = new Encription(password); + }, + showMenu: () => { + authenticator.class.slidein = true; + authenticator.class.slideout = false; + }, + closeMenu: () => { + authenticator.class.slidein = false; + authenticator.class.slideout = true; + setTimeout(() => { + authenticator.class.slideout = false; + }, 200); } } }); From 2e35861fa09b94dd2282f8b92d6f9c44d22d14ff Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Mon, 5 Feb 2018 00:53:59 +0800 Subject: [PATCH 013/178] save --- popup.html | 87 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/popup.ts | 25 +++++++++++++-- 2 files changed, 104 insertions(+), 8 deletions(-) diff --git a/popup.html b/popup.html index 319c3aeb0..98d1d4f0b 100644 --- a/popup.html +++ b/popup.html @@ -16,7 +16,7 @@
@@ -35,8 +35,9 @@
-
+
+ + +
+
+
+ +
+ +
+
{{ i18n.add_qr }}
+
{{ i18n.add_secret }}
+
+ + + + +
+ + +
+
+ + +
+
{{ i18n.ok }}
+
+
+ +
+
{{ i18n.security_warning }}
+ + + + +
+ + +
+
{{ i18n.ok }}
+
+ +
+
{{ i18n.passphrase_info }}
+ + +
+ + +
+
{{ i18n.ok }}
+
+ +
+ +
{{ i18n.update }}
+
+ +
+ + +
{{ i18n.ok }}
+
+
+
diff --git a/src/popup.ts b/src/popup.ts index 0d0c3bdbe..dd780b6f3 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -121,8 +121,16 @@ async function init() { i18n, entries, encryption, - class: {timeout: false, edit: false, slidein: false, slideout: false}, - sector: '' + class: { + timeout: false, + edit: false, + slidein: false, + slideout: false, + fadein: false, + fadeout: false + }, + sector: '', + info: '' }, methods: { showBulls: (code: string) => { @@ -141,6 +149,19 @@ async function init() { setTimeout(() => { authenticator.class.slideout = false; }, 200); + }, + showInfo: (tab: string) => { + authenticator.class.fadein = true; + authenticator.class.fadeout = false; + authenticator.info = tab; + }, + closeInfo: () => { + authenticator.class.fadein = false; + authenticator.class.fadeout = true; + setTimeout(() => { + authenticator.class.fadeout = false; + authenticator.info = ''; + }, 200); } } }); From 5ba0611c11ee1889846fc5f2c00dd5feadbc09df Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 5 Feb 2018 12:53:33 +0800 Subject: [PATCH 014/178] fix tslink path --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 9450bb83e..dd0840a2f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "./../../../../usr/local/lib/node_modules/gts/tsconfig-google.json", + "extends": "./node_modules/gts/tsconfig-google.json", "compilerOptions": { "lib": ["es6", "dom"], "target": "es6", From 928e6d212bfba07bbb8393d98d0fb47c35766129 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 5 Feb 2018 16:39:53 +0800 Subject: [PATCH 015/178] save --- css/popup.css | 1 - popup.html | 24 ++++--- src/models/interface.ts | 1 + src/models/otp.ts | 2 +- src/models/storage.ts | 138 +++++++++++++++++++++++++++++++++++++++- src/popup.ts | 102 +++++++++++++++++++++++++++-- 6 files changed, 250 insertions(+), 18 deletions(-) diff --git a/css/popup.css b/css/popup.css index 686bd3d6e..7319581d8 100644 --- a/css/popup.css +++ b/css/popup.css @@ -699,6 +699,5 @@ body { left: 10px; top: 150px; box-shadow: 1px 1px 3px gray; - display: none; z-index: 1000; } \ No newline at end of file diff --git a/popup.html b/popup.html index 98d1d4f0b..aa626f1b7 100644 --- a/popup.html +++ b/popup.html @@ -17,11 +17,12 @@
+
-
+
{{ entry.issuer }}
@@ -50,7 +51,7 @@ +
@@ -112,14 +114,14 @@
- -
{{ i18n.update }}
+ +
{{ i18n.update }}
- + @@ -130,10 +132,16 @@ -
{{ i18n.ok }}
+
{{ i18n.ok }}
+ + +
+
{{ message }}
+
{{ i18n.ok }}
+
diff --git a/src/models/interface.ts b/src/models/interface.ts index 959360793..cbaa19285 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -33,6 +33,7 @@ interface OTPStorage { issuer: string; secret: string; type: string; + counter: number; } /* tslint:disable-next-line:interface-name */ diff --git a/src/models/otp.ts b/src/models/otp.ts index 2066c2b77..c4d5eea08 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -38,7 +38,7 @@ class OTPEntry implements OTP { } async delete() { - EntryStorage.delete(this); + await EntryStorage.delete(this); return; } diff --git a/src/models/storage.ts b/src/models/storage.ts index e2af66d15..630fd6149 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -12,6 +12,7 @@ class EntryStorage { index: entry.index, issuer: entry.issuer, type: OTPType[entry.type], + counter: entry.counter, secret: encryption.getEncryptedSecret(entry.secret), encrypted: encryption.getEncryptionStatus() }; @@ -38,6 +39,82 @@ class EntryStorage { return newData; } + static async getExport() { + return new Promise( + (resolve: (value: string) => void, reject: (reason: Error) => void) => { + try { + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { + for (const hash of Object.keys(_data)) { + // we need correct hash + if (hash !== _data[hash].hash) { + _data[_data[hash].hash] = _data[hash]; + delete _data[hash]; + } + } + return resolve(JSON.stringify(_data, null, 2)); + }); + return; + } catch (error) { + return reject(error); + } + }); + } + + static async import(data: {[hash: string]: OTPStorage}) { + return new Promise( + (resolve: () => void, reject: (reason: Error) => void) => { + try { + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { + for (const hash of Object.keys(data)) { + // never trust data import from user + if (!data[hash].secret) { + continue; + } + + data[hash].hash = data[hash].hash || hash; + data[hash].account = data[hash].account || ''; + data[hash].encrypted = !!data[hash].encrypted; + data[hash].index = data[hash].index || 0; + data[hash].issuer = data[hash].issuer || ''; + data[hash].type = data[hash].type || OTPType[OTPType.totp]; + data[hash].counter = data[hash].counter || 0; + + const _hash = CryptoJS.MD5(data[hash].secret).toString(); + if (_hash !== hash) { + data[_hash] = data[hash]; + data[_hash].hash = _hash; + delete data[hash]; + } + + if (/^(blz\-|bliz\-)/.test(data[hash].secret)) { + const secretMatches = + data[hash].secret.match(/^(blz\-|bliz\-)(.*)/); + if (secretMatches && secretMatches.length >= 3) { + data[hash].secret = secretMatches[2]; + data[hash].type = OTPType[OTPType.battle]; + } + } + + if (/^stm\-/.test(data[hash].secret)) { + const secretMatches = data[hash].secret.match(/^stm\-(.*)/); + if (secretMatches && secretMatches.length >= 2) { + data[hash].secret = secretMatches[1]; + data[hash].type = OTPType[OTPType.steam]; + } + } + + _data[hash] = data[hash]; + } + _data = this.ensureUniqueIndex(_data); + chrome.storage.sync.set(_data, resolve); + }); + return; + } catch (error) { + return reject(error); + } + }); + } + static async add(encryption: Encription, entry: OTPEntry) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { @@ -87,8 +164,15 @@ class EntryStorage { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { const data: OTPEntry[] = []; - for (const hash of Object.keys(_data)) { + for (let hash of Object.keys(_data)) { const entryData = _data[hash]; + let needMigrate = false; + + if (!entryData.type) { + entryData.type = OTPType[OTPType.totp]; + needMigrate = true; + } + let type: OTPType; switch (entryData.type) { case 'totp': @@ -98,15 +182,63 @@ class EntryStorage { type = OTPType[entryData.type]; break; default: + // we need correct the type here + // and save it type = OTPType.totp; + entryData.type = OTPType[OTPType.totp]; + needMigrate = true; } entryData.secret = encryption.getDecryptedSecret(entryData.secret); + const entry = new OTPEntry( type, entryData.issuer, entryData.secret, entryData.account, entryData.index); data.push(entry); + + // we need migrate secret in old format here + if (/^(blz\-|bliz\-)/.test(entryData.secret)) { + const secretMatches = + entryData.secret.match(/^(blz\-|bliz\-)(.*)/); + if (secretMatches && secretMatches.length >= 3) { + entryData.secret = entryData.encrypted ? + secretMatches[2] : + encryption.getEncryptedSecret(entry.secret); + entryData.type = OTPType[OTPType.battle]; + needMigrate = true; + } + } + + if (/^stm\-/.test(entryData.secret)) { + const secretMatches = entryData.secret.match(/^stm\-(.*)/); + if (secretMatches && secretMatches.length >= 2) { + entryData.secret = entryData.encrypted ? + secretMatches[2] : + encryption.getEncryptedSecret(entry.secret); + entryData.type = OTPType[OTPType.steam]; + needMigrate = true; + } + } + + // we need correct the hash + const _hash = CryptoJS.MD5(entry.secret).toString(); + if (hash !== _hash) { + chrome.storage.sync.remove(hash); + hash = _hash; + entryData.hash = hash; + needMigrate = true; + } + + if (needMigrate) { + const _entry: {[hash: string]: OTPStorage} = {}; + _entry[hash] = entryData; + this.import(_entry); + } } + + data.sort((a, b) => { + return a.index - b.index; + }); return resolve(data); }); return; @@ -125,7 +257,9 @@ class EntryStorage { delete _data[entry.hash]; } _data = this.ensureUniqueIndex(_data); - chrome.storage.sync.set(_data, resolve); + chrome.storage.sync.remove(entry.hash, () => { + chrome.storage.sync.set(_data, resolve); + }); return; }); return; diff --git a/src/popup.ts b/src/popup.ts index dd780b6f3..7935c6619 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -59,12 +59,40 @@ async function loadI18nMessages() { }); } -async function getDataFromBackground(command: {}) { +async function syncTimeWithGoogle() { return new Promise( - (resolve: (value: T) => void, reject: (reason: Error) => void) => { - chrome.runtime.sendMessage(command, (response: T) => { - return resolve(response); - }); + (resolve: (value: string) => void, reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.open('HEAD', 'https://www.google.com/generate_204'); + const xhrAbort = setTimeout(() => { + xhr.abort(); + return resolve('updateFailure'); + }, 5000); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + clearTimeout(xhrAbort); + const date = xhr.getResponseHeader('date'); + if (!date) { + return resolve('updateFailure'); + } + const serverTime = new Date(date).getTime(); + const clientTime = new Date().getTime(); + const offset = Math.round((serverTime - clientTime) / 1000); + + if (Math.abs(offset) <= 300) { // within 5 minutes + localStorage.offset = + Math.round((serverTime - clientTime) / 1000); + return resolve('updateSuccess'); + } else { + return resolve('clock_too_far_off'); + } + } + }; + xhr.send(); + } catch (error) { + return reject(error); + } }); } @@ -108,11 +136,23 @@ function getSector(second: number) { return `url(${url}) center / 20px 20px`; } +function resize(zoom: number) { + if (zoom !== 100) { + document.body.style.marginBottom = 480 * (zoom / 100 - 1) + 'px'; + document.body.style.marginRight = 320 * (zoom / 100 - 1) + 'px'; + document.body.style.transform = 'scale(' + (zoom / 100) + ')'; + } +} + async function init() { + const zoom = Number(localStorage.zoom) || 100; + resize(zoom); + const version = await getVersion(); const i18n = await loadI18nMessages(); const encryption = new Encription(''); const entries = await getEntries(encryption); + const exportData = await EntryStorage.getExport(); const authenticator = new Vue({ el: '#authenticator', @@ -121,6 +161,8 @@ async function init() { i18n, entries, encryption, + exportData, + zoom, class: { timeout: false, edit: false, @@ -130,7 +172,8 @@ async function init() { fadeout: false }, sector: '', - info: '' + info: '', + message: '' }, methods: { showBulls: (code: string) => { @@ -162,6 +205,46 @@ async function init() { authenticator.class.fadeout = false; authenticator.info = ''; }, 200); + }, + updateEntries: async () => { + await EntryStorage.import(JSON.parse(authenticator.exportData)); + authenticator.entries = await getEntries(authenticator.encryption); + updateCode(authenticator); + authenticator.message = authenticator.i18n.updateSuccess; + }, + saveZoom: () => { + localStorage.zoom = authenticator.zoom; + resize(authenticator.zoom); + }, + removeEntry: async (entry: OTPEntry) => { + if (confirm('Remove?')) { + await entry.delete(); + authenticator.exportData = await EntryStorage.getExport(); + authenticator.entries = await getEntries(authenticator.encryption); + updateCode(authenticator); + } + return; + }, + syncClock: async () => { + chrome.permissions.request( + {origins: ['https://www.google.com/']}, async (granted) => { + if (granted) { + const message = await syncTimeWithGoogle(); + authenticator.message = authenticator.i18n[message]; + } + return; + }); + return; + }, + editEntry: () => { + authenticator.class.edit = !authenticator.class.edit; + const codes = document.getElementById('codes'); + if (codes) { + // wait vue apply changes to dom + setTimeout(() => { + codes.scrollTop = authenticator.class.edit ? codes.scrollHeight : 0; + }, 0); + } } } }); @@ -173,4 +256,11 @@ async function init() { return; } +chrome.permissions.contains( + {origins: ['https://www.google.com/']}, (hasPermission) => { + if (hasPermission) { + syncTimeWithGoogle(); + } + }); + init(); \ No newline at end of file From fd471cc3f8796e8f5ede2e6cbfc59ae25e425e46 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 5 Feb 2018 17:41:55 +0800 Subject: [PATCH 016/178] save --- js/qrcode.js | 618 +++++++++++++++++++++++++++++++++++++++++++++++++++ popup.html | 18 +- src/popup.ts | 87 +++++++- 3 files changed, 717 insertions(+), 6 deletions(-) create mode 100644 js/qrcode.js diff --git a/js/qrcode.js b/js/qrcode.js new file mode 100644 index 000000000..83566a8e1 --- /dev/null +++ b/js/qrcode.js @@ -0,0 +1,618 @@ +/** + * @fileoverview + * - Using the 'QRCode for Javascript library' + * - Fixed dataset of 'QRCode for Javascript library' for support full-spec. + * - this library has no dependencies. + * + * @author davidshimjs + * @see http://www.d-project.com/ + * @see http://jeromeetienne.github.com/jquery-qrcode/ + */ +var QRCode; + +(function () { + //--------------------------------------------------------------------- + // QRCode for JavaScript + // + // Copyright (c) 2009 Kazuhiko Arase + // + // URL: http://www.d-project.com/ + // + // Licensed under the MIT license: + // http://www.opensource.org/licenses/mit-license.php + // + // The word "QR Code" is registered trademark of + // DENSO WAVE INCORPORATED + // http://www.denso-wave.com/qrcode/faqpatent-e.html + // + //--------------------------------------------------------------------- + function QR8bitByte(data) { + this.mode = QRMode.MODE_8BIT_BYTE; + this.data = data; + this.parsedData = []; + + // Added to support UTF-8 Characters + for (var i = 0, l = this.data.length; i < l; i++) { + var byteArray = []; + var code = this.data.charCodeAt(i); + + if (code > 0x10000) { + byteArray[0] = 0xF0 | ((code & 0x1C0000) >>> 18); + byteArray[1] = 0x80 | ((code & 0x3F000) >>> 12); + byteArray[2] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[3] = 0x80 | (code & 0x3F); + } else if (code > 0x800) { + byteArray[0] = 0xE0 | ((code & 0xF000) >>> 12); + byteArray[1] = 0x80 | ((code & 0xFC0) >>> 6); + byteArray[2] = 0x80 | (code & 0x3F); + } else if (code > 0x80) { + byteArray[0] = 0xC0 | ((code & 0x7C0) >>> 6); + byteArray[1] = 0x80 | (code & 0x3F); + } else { + byteArray[0] = code; + } + + this.parsedData.push(byteArray); + } + + this.parsedData = Array.prototype.concat.apply([], this.parsedData); + + if (this.parsedData.length != this.data.length) { + this.parsedData.unshift(191); + this.parsedData.unshift(187); + this.parsedData.unshift(239); + } + } + + QR8bitByte.prototype = { + getLength: function (buffer) { + return this.parsedData.length; + }, + write: function (buffer) { + for (var i = 0, l = this.parsedData.length; i < l; i++) { + buffer.put(this.parsedData[i], 8); + } + } + }; + + function QRCodeModel(typeNumber, errorCorrectLevel) { + this.typeNumber = typeNumber; + this.errorCorrectLevel = errorCorrectLevel; + this.modules = null; + this.moduleCount = 0; + this.dataCache = null; + this.dataList = []; + } + + QRCodeModel.prototype={addData:function(data){var newData=new QR8bitByte(data);this.dataList.push(newData);this.dataCache=null;},isDark:function(row,col){if(row<0||this.moduleCount<=row||col<0||this.moduleCount<=col){throw new Error(row+","+col);} + return this.modules[row][col];},getModuleCount:function(){return this.moduleCount;},make:function(){this.makeImpl(false,this.getBestMaskPattern());},makeImpl:function(test,maskPattern){this.moduleCount=this.typeNumber*4+17;this.modules=new Array(this.moduleCount);for(var row=0;row=7){this.setupTypeNumber(test);} + if(this.dataCache==null){this.dataCache=QRCodeModel.createData(this.typeNumber,this.errorCorrectLevel,this.dataList);} + this.mapData(this.dataCache,maskPattern);},setupPositionProbePattern:function(row,col){for(var r=-1;r<=7;r++){if(row+r<=-1||this.moduleCount<=row+r)continue;for(var c=-1;c<=7;c++){if(col+c<=-1||this.moduleCount<=col+c)continue;if((0<=r&&r<=6&&(c==0||c==6))||(0<=c&&c<=6&&(r==0||r==6))||(2<=r&&r<=4&&2<=c&&c<=4)){this.modules[row+r][col+c]=true;}else{this.modules[row+r][col+c]=false;}}}},getBestMaskPattern:function(){var minLostPoint=0;var pattern=0;for(var i=0;i<8;i++){this.makeImpl(true,i);var lostPoint=QRUtil.getLostPoint(this);if(i==0||minLostPoint>lostPoint){minLostPoint=lostPoint;pattern=i;}} + return pattern;},createMovieClip:function(target_mc,instance_name,depth){var qr_mc=target_mc.createEmptyMovieClip(instance_name,depth);var cs=1;this.make();for(var row=0;row>i)&1)==1);this.modules[Math.floor(i/3)][i%3+this.moduleCount-8-3]=mod;} + for(var i=0;i<18;i++){var mod=(!test&&((bits>>i)&1)==1);this.modules[i%3+this.moduleCount-8-3][Math.floor(i/3)]=mod;}},setupTypeInfo:function(test,maskPattern){var data=(this.errorCorrectLevel<<3)|maskPattern;var bits=QRUtil.getBCHTypeInfo(data);for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<6){this.modules[i][8]=mod;}else if(i<8){this.modules[i+1][8]=mod;}else{this.modules[this.moduleCount-15+i][8]=mod;}} + for(var i=0;i<15;i++){var mod=(!test&&((bits>>i)&1)==1);if(i<8){this.modules[8][this.moduleCount-i-1]=mod;}else if(i<9){this.modules[8][15-i-1+1]=mod;}else{this.modules[8][15-i-1]=mod;}} + this.modules[this.moduleCount-8][8]=(!test);},mapData:function(data,maskPattern){var inc=-1;var row=this.moduleCount-1;var bitIndex=7;var byteIndex=0;for(var col=this.moduleCount-1;col>0;col-=2){if(col==6)col--;while(true){for(var c=0;c<2;c++){if(this.modules[row][col-c]==null){var dark=false;if(byteIndex>>bitIndex)&1)==1);} + var mask=QRUtil.getMask(maskPattern,row,col-c);if(mask){dark=!dark;} + this.modules[row][col-c]=dark;bitIndex--;if(bitIndex==-1){byteIndex++;bitIndex=7;}}} + row+=inc;if(row<0||this.moduleCount<=row){row-=inc;inc=-inc;break;}}}}};QRCodeModel.PAD0=0xEC;QRCodeModel.PAD1=0x11;QRCodeModel.createData=function(typeNumber,errorCorrectLevel,dataList){var rsBlocks=QRRSBlock.getRSBlocks(typeNumber,errorCorrectLevel);var buffer=new QRBitBuffer();for(var i=0;itotalDataCount*8){throw new Error("code length overflow. (" + +buffer.getLengthInBits() + +">" + +totalDataCount*8 + +")");} + if(buffer.getLengthInBits()+4<=totalDataCount*8){buffer.put(0,4);} + while(buffer.getLengthInBits()%8!=0){buffer.putBit(false);} + while(true){if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD0,8);if(buffer.getLengthInBits()>=totalDataCount*8){break;} + buffer.put(QRCodeModel.PAD1,8);} + return QRCodeModel.createBytes(buffer,rsBlocks);};QRCodeModel.createBytes=function(buffer,rsBlocks){var offset=0;var maxDcCount=0;var maxEcCount=0;var dcdata=new Array(rsBlocks.length);var ecdata=new Array(rsBlocks.length);for(var r=0;r=0)?modPoly.get(modIndex):0;}} + var totalCodeCount=0;for(var i=0;i=0){d^=(QRUtil.G15<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G15)));} + return((data<<10)|d)^QRUtil.G15_MASK;},getBCHTypeNumber:function(data){var d=data<<12;while(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)>=0){d^=(QRUtil.G18<<(QRUtil.getBCHDigit(d)-QRUtil.getBCHDigit(QRUtil.G18)));} + return(data<<12)|d;},getBCHDigit:function(data){var digit=0;while(data!=0){digit++;data>>>=1;} + return digit;},getPatternPosition:function(typeNumber){return QRUtil.PATTERN_POSITION_TABLE[typeNumber-1];},getMask:function(maskPattern,i,j){switch(maskPattern){case QRMaskPattern.PATTERN000:return(i+j)%2==0;case QRMaskPattern.PATTERN001:return i%2==0;case QRMaskPattern.PATTERN010:return j%3==0;case QRMaskPattern.PATTERN011:return(i+j)%3==0;case QRMaskPattern.PATTERN100:return(Math.floor(i/2)+Math.floor(j/3))%2==0;case QRMaskPattern.PATTERN101:return(i*j)%2+(i*j)%3==0;case QRMaskPattern.PATTERN110:return((i*j)%2+(i*j)%3)%2==0;case QRMaskPattern.PATTERN111:return((i*j)%3+(i+j)%2)%2==0;default:throw new Error("bad maskPattern:"+maskPattern);}},getErrorCorrectPolynomial:function(errorCorrectLength){var a=new QRPolynomial([1],0);for(var i=0;i5){lostPoint+=(3+sameCount-5);}}} + for(var row=0;row=256){n-=255;} + return QRMath.EXP_TABLE[n];},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)};for(var i=0;i<8;i++){QRMath.EXP_TABLE[i]=1<>>(7-index%8))&1)==1;},put:function(num,length){for(var i=0;i>>(length-i-1))&1)==1);}},getLengthInBits:function(){return this.length;},putBit:function(bit){var bufIndex=Math.floor(this.length/8);if(this.buffer.length<=bufIndex){this.buffer.push(0);} + if(bit){this.buffer[bufIndex]|=(0x80>>>(this.length%8));} + this.length++;}};var QRCodeLimitLength=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]]; + + function _isSupportCanvas() { + return typeof CanvasRenderingContext2D != "undefined"; + } + + // android 2.x doesn't support Data-URI spec + function _getAndroid() { + var android = false; + var sAgent = navigator.userAgent; + + if (/android/i.test(sAgent)) { // android + android = true; + aMat = sAgent.toString().match(/android ([0-9]\.[0-9])/i); + + if (aMat && aMat[1]) { + android = parseFloat(aMat[1]); + } + } + + return android; + } + + var svgDrawer = (function() { + + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + + this.clear(); + + function makeSVG(tag, attrs) { + var el = document.createElementNS('http://www.w3.org/2000/svg', tag); + for (var k in attrs) + if (attrs.hasOwnProperty(k)) el.setAttribute(k, attrs[k]); + return el; + } + + var svg = makeSVG("svg" , {'viewBox': '0 0 ' + String(nCount) + " " + String(nCount), 'width': '100%', 'height': '100%', 'fill': _htOption.colorLight}); + svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink"); + _el.appendChild(svg); + + svg.appendChild(makeSVG("rect", {"fill": _htOption.colorDark, "width": "1", "height": "1", "id": "template"})); + + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + if (oQRCode.isDark(row, col)) { + var child = makeSVG("use", {"x": String(row), "y": String(col)}); + child.setAttributeNS("http://www.w3.org/1999/xlink", "href", "#template") + svg.appendChild(child); + } + } + } + }; + Drawing.prototype.clear = function () { + while (this._el.hasChildNodes()) + this._el.removeChild(this._el.lastChild); + }; + return Drawing; + })(); + + var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; + + // Drawing in DOM by using Table tag + var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { + var Drawing = function (el, htOption) { + this._el = el; + this._htOption = htOption; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _htOption = this._htOption; + var _el = this._el; + var nCount = oQRCode.getModuleCount(); + var nWidth = Math.floor(_htOption.width / nCount); + var nHeight = Math.floor(_htOption.height / nCount); + var aHTML = ['']; + + for (var row = 0; row < nCount; row++) { + aHTML.push(''); + + for (var col = 0; col < nCount; col++) { + aHTML.push(''); + } + + aHTML.push(''); + } + + aHTML.push('
'); + _el.innerHTML = aHTML.join(''); + + // Fix the margin values as real size. + var elTable = _el.childNodes[0]; + var nLeftMarginTable = (_htOption.width - elTable.offsetWidth) / 2; + var nTopMarginTable = (_htOption.height - elTable.offsetHeight) / 2; + + if (nLeftMarginTable > 0 && nTopMarginTable > 0) { + elTable.style.margin = nTopMarginTable + "px " + nLeftMarginTable + "px"; + } + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._el.innerHTML = ''; + }; + + return Drawing; + })() : (function () { // Drawing in Canvas + function _onMakeImage() { + if(this._callback){ + this._elCanvas.style.display = "none"; + this._callback(this._elCanvas.toDataURL("image/png")); + } + else{ + this._elImage.src = this._elCanvas.toDataURL("image/png"); + this._elImage.style.display = "block"; + this._elCanvas.style.display = "none"; + } + } + + // Android 2.1 bug workaround + // http://code.google.com/p/android/issues/detail?id=5141 + if (this._android && this._android <= 2.1) { + var factor = 1 / window.devicePixelRatio; + var drawImage = CanvasRenderingContext2D.prototype.drawImage; + CanvasRenderingContext2D.prototype.drawImage = function (image, sx, sy, sw, sh, dx, dy, dw, dh) { + if (("nodeName" in image) && /img/i.test(image.nodeName)) { + for (var i = arguments.length - 1; i >= 1; i--) { + arguments[i] = arguments[i] * factor; + } + } else if (typeof dw == "undefined") { + arguments[1] *= factor; + arguments[2] *= factor; + arguments[3] *= factor; + arguments[4] *= factor; + } + + drawImage.apply(this, arguments); + }; + } + + /** + * Check whether the user's browser supports Data URI or not + * + * @private + * @param {Function} fSuccess Occurs if it supports Data URI + * @param {Function} fFail Occurs if it doesn't support Data URI + */ + function _safeSetDataURI(fSuccess, fFail) { + var self = this; + self._fFail = fFail; + self._fSuccess = fSuccess; + + // Check it just once + if (self._bSupportDataURI === null) { + var el = document.createElement("img"); + var fOnError = function() { + self._bSupportDataURI = false; + + if (self._fFail) { + _fFail.call(self); + } + }; + var fOnSuccess = function() { + self._bSupportDataURI = true; + + if (self._fSuccess) { + self._fSuccess.call(self); + } + }; + + el.onabort = fOnError; + el.onerror = fOnError; + el.onload = fOnSuccess; + el.src = "data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=="; // the Image contains 1px data. + return; + } else if (self._bSupportDataURI === true && self._fSuccess) { + self._fSuccess.call(self); + } else if (self._bSupportDataURI === false && self._fFail) { + self._fFail.call(self); + } + }; + + /** + * Drawing QRCode by using canvas + * + * @constructor + * @param {HTMLElement} el + * @param {Object} htOption QRCode Options + */ + var Drawing = function (el, htOption, callback) { + this._bIsPainted = false; + this._android = _getAndroid(); + + this._htOption = htOption; + this._elCanvas = document.createElement("canvas"); + this._elCanvas.width = htOption.width+10; + this._elCanvas.height = htOption.height+10; + el.appendChild(this._elCanvas); + this._el = el; + this._callback = callback; + this._oContext = this._elCanvas.getContext("2d"); + this._bIsPainted = false; + this._elImage = document.createElement("img"); + this._elImage.alt = "Scan me!"; + this._elImage.style.display = "none"; + this._el.appendChild(this._elImage); + this._bSupportDataURI = null; + }; + + /** + * Draw the QRCode + * + * @param {QRCode} oQRCode + */ + Drawing.prototype.draw = function (oQRCode) { + var _elImage = this._elImage; + var _oContext = this._oContext; + var _htOption = this._htOption; + + var nCount = oQRCode.getModuleCount(); + var nWidth = _htOption.width / nCount; + var nHeight = _htOption.height / nCount; + var nRoundedWidth = Math.round(nWidth); + var nRoundedHeight = Math.round(nHeight); + + _elImage.style.display = "none"; + this.clear(); + + _oContext.fillStyle = _htOption.colorLight; + _oContext.fillRect(0, 0, _htOption.width+10, _htOption.height+10); + for (var row = 0; row < nCount; row++) { + for (var col = 0; col < nCount; col++) { + var bIsDark = oQRCode.isDark(row, col); + var nLeft = col * nWidth + 5; + var nTop = row * nHeight + 5; + _oContext.strokeStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.lineWidth = 1; + _oContext.fillStyle = bIsDark ? _htOption.colorDark : _htOption.colorLight; + _oContext.fillRect(nLeft, nTop, nWidth, nHeight); + + _oContext.strokeRect( + Math.floor(nLeft) + 0.5, + Math.floor(nTop) + 0.5, + nRoundedWidth, + nRoundedHeight + ); + + _oContext.strokeRect( + Math.ceil(nLeft) - 0.5, + Math.ceil(nTop) - 0.5, + nRoundedWidth, + nRoundedHeight + ); + } + } + + this._bIsPainted = true; + }; + + /** + * Make the image from Canvas if the browser supports Data URI. + */ + Drawing.prototype.makeImage = function () { + if (this._bIsPainted) { + _safeSetDataURI.call(this, _onMakeImage); + } + }; + + /** + * Return whether the QRCode is painted or not + * + * @return {Boolean} + */ + Drawing.prototype.isPainted = function () { + return this._bIsPainted; + }; + + /** + * Clear the QRCode + */ + Drawing.prototype.clear = function () { + this._oContext.clearRect(0, 0, this._elCanvas.width, this._elCanvas.height); + this._bIsPainted = false; + }; + + /** + * @private + * @param {Number} nNumber + */ + Drawing.prototype.round = function (nNumber) { + if (!nNumber) { + return nNumber; + } + + return Math.floor(nNumber * 1000) / 1000; + }; + + return Drawing; + })(); + + /** + * Get the type by string length + * + * @private + * @param {String} sText + * @param {Number} nCorrectLevel + * @return {Number} type + */ + function _getTypeNumber(sText, nCorrectLevel) { + var nType = 1; + var length = _getUTF8Length(sText); + + for (var i = 0, len = QRCodeLimitLength.length; i <= len; i++) { + var nLimit = 0; + + switch (nCorrectLevel) { + case QRErrorCorrectLevel.L : + nLimit = QRCodeLimitLength[i][0]; + break; + case QRErrorCorrectLevel.M : + nLimit = QRCodeLimitLength[i][1]; + break; + case QRErrorCorrectLevel.Q : + nLimit = QRCodeLimitLength[i][2]; + break; + case QRErrorCorrectLevel.H : + nLimit = QRCodeLimitLength[i][3]; + break; + } + + if (length <= nLimit) { + break; + } else { + nType++; + } + } + + if (nType > QRCodeLimitLength.length) { + throw new Error("Too long data"); + } + + return nType; + } + + function _getUTF8Length(sText) { + var replacedText = encodeURI(sText).toString().replace(/\%[0-9a-fA-F]{2}/g, 'a'); + return replacedText.length + (replacedText.length != sText ? 3 : 0); + } + + /** + * @class QRCode + * @constructor + * @example + * new QRCode(document.getElementById("test"), "http://jindo.dev.naver.com/collie"); + * + * @example + * var oQRCode = new QRCode("test", { + * text : "http://naver.com", + * width : 128, + * height : 128 + * }); + * + * oQRCode.clear(); // Clear the QRCode. + * oQRCode.makeCode("http://map.naver.com"); // Re-create the QRCode. + * + * @param {HTMLElement|String} el target element or 'id' attribute of element. + * @param {Object|String} vOption + * @param {String} vOption.text QRCode link data + * @param {Number} [vOption.width=256] + * @param {Number} [vOption.height=256] + * @param {String} [vOption.colorDark="#000000"] + * @param {String} [vOption.colorLight="#ffffff"] + * @param {QRCode.CorrectLevel} [vOption.correctLevel=QRCode.CorrectLevel.H] [L|M|Q|H] + */ + QRCode = function (el, vOption, callback) { + this._htOption = { + width : 256, + height : 256, + typeNumber : 4, + colorDark : "#000000", + colorLight : "#ffffff", + correctLevel : QRErrorCorrectLevel.H + }; + + if (typeof vOption === 'string') { + vOption = { + text : vOption + }; + } + + // Overwrites options + if (vOption) { + for (var i in vOption) { + this._htOption[i] = vOption[i]; + } + } + + if (typeof el == "string") { + el = document.getElementById(el); + } + + this._callback = callback; + this._android = _getAndroid(); + this._el = el; + this._oQRCode = null; + this._oDrawing = new Drawing(this._el, this._htOption, this._callback); + + if (this._htOption.text) { + this.makeCode(this._htOption.text); + } + }; + + /** + * Make the QRCode + * + * @param {String} sText link data + */ + QRCode.prototype.makeCode = function (sText) { + this._oQRCode = new QRCodeModel(_getTypeNumber(sText, this._htOption.correctLevel), this._htOption.correctLevel); + this._oQRCode.addData(sText); + this._oQRCode.make(); + //this._el.title = sText; + this._oDrawing.draw(this._oQRCode); + this.makeImage(); + }; + + /** + * Make the Image from Canvas element + * - It occurs automatically + * - Android below 3 doesn't support Data-URI spec. + * + * @private + */ + QRCode.prototype.makeImage = function () { + if (typeof this._oDrawing.makeImage == "function" && (!this._android || this._android >= 3)) { + this._oDrawing.makeImage(); + } + }; + + /** + * Clear the QRCode + */ + QRCode.prototype.clear = function () { + this._oDrawing.clear(); + }; + + /** + * @name QRCode.CorrectLevel + */ + QRCode.CorrectLevel = QRErrorCorrectLevel; +})(); diff --git a/popup.html b/popup.html index aa626f1b7..0a3773873 100644 --- a/popup.html +++ b/popup.html @@ -6,6 +6,7 @@ + @@ -24,16 +25,16 @@
-
{{ entry.issuer }}
+
{{ entry.issuer.split('::')[0] }}
-
+
-
+
@@ -137,11 +138,20 @@
- +
{{ message }}
{{ i18n.ok }}
+ + +
{{ notification }}
+ + +
+ + +
diff --git a/src/popup.ts b/src/popup.ts index 7935c6619..ce0c92282 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -8,6 +8,9 @@ /* tslint:disable-next-line:no-any */ declare var Vue: any; +/* tslint:disable-next-line:no-any */ +declare var QRCode: any; + async function getEntries(encryption: Encription) { const optEntries: OTP[] = await EntryStorage.get(encryption); return optEntries; @@ -144,6 +147,33 @@ function resize(zoom: number) { } } +async function getQrUrl(entry: OTPEntry) { + return new Promise( + (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 otpauth = 'otpauth://' + type + '/' + label + + '?secret=' + entry.secret + + (entry.issuer ? ('&issuer=' + entry.issuer.split('::')[0]) : '') + + ((entry.type === OTPType.hotp) ? ('&counter=' + entry.counter) : + ''); + /* tslint:disable-next-line:no-unused-expression */ + new QRCode( + 'qr', { + text: otpauth, + width: 128, + height: 128, + colorDark: '#000000', + colorLight: '#ffffff', + correctLevel: QRCode.CorrectLevel.L + }, + resolve); + return; + }); +} + async function init() { const zoom = Number(localStorage.zoom) || 100; resize(zoom); @@ -169,11 +199,18 @@ async function init() { slidein: false, slideout: false, fadein: false, - fadeout: false + fadeout: false, + qrfadein: false, + qrfadeout: false, + notificationFadein: false, + notificationFadeout: false }, sector: '', info: '', - message: '' + message: '', + qr: '', + notification: '', + notificationTimeout: 0 }, methods: { showBulls: (code: string) => { @@ -245,6 +282,52 @@ async function init() { codes.scrollTop = authenticator.class.edit ? codes.scrollHeight : 0; }, 0); } + }, + shouldShowQrIcon: (entry: OTPEntry) => { + return entry.type !== OTPType.battle && entry.type !== OTPType.steam; + }, + showQr: async (entry: OTPEntry) => { + const qrUrl = await getQrUrl(entry); + authenticator.qr = `url(${qrUrl})`; + authenticator.class.qrfadein = true; + authenticator.class.qrfadeout = false; + }, + hideQr: () => { + authenticator.class.qrfadein = false; + authenticator.class.qrfadeout = true; + setTimeout(() => { + authenticator.class.qrfadeout = false; + }, 200); + }, + copyCode: (entry: OTPEntry) => { + if (authenticator.class.edit) { + return; + } + chrome.permissions.request( + {permissions: ['clipboardWrite']}, (granted) => { + if (granted) { + const codeClipboard = document.getElementById( + 'codeClipboard') as HTMLInputElement; + if (!codeClipboard) { + return; + } + codeClipboard.value = entry.code; + codeClipboard.focus(); + codeClipboard.select(); + document.execCommand('Copy'); + authenticator.notification = authenticator.i18n.copied; + clearTimeout(authenticator.notificationTimeout); + authenticator.class.notificationFadein = true; + authenticator.class.notificationFadeout = false; + authenticator.notificationTimeout = setTimeout(() => { + authenticator.class.notificationFadein = false; + authenticator.class.notificationFadeout = true; + setTimeout(() => { + authenticator.class.notificationFadeout = false; + }, 200); + }, 1000); + } + }); } } }); From 4fe0c104de6c91602a99845bfdb61faae7ce5999 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Tue, 6 Feb 2018 00:14:56 +0800 Subject: [PATCH 017/178] save --- css/popup.css | 33 +++++++++++++++++++++++------ popup.html | 37 ++++++++++++++++++++------------- src/popup.ts | 57 +++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 105 insertions(+), 22 deletions(-) diff --git a/css/popup.css b/css/popup.css index 7319581d8..b5c301aa1 100644 --- a/css/popup.css +++ b/css/popup.css @@ -109,7 +109,7 @@ body { overflow: hidden; font-family: arial, 'Microsoft YaHei'; cursor: default; - -webkit-user-select: none; + user-select: none; transform-origin: left top; } @@ -339,6 +339,25 @@ body { cursor: pointer; } +.buttons { + text-align: center; +} + +#confirm_ok, +#confirm_cancel { + display: inline-block; + margin: 10px; + padding: 5px 20px; + border: #CCC 1px solid; + background: white; + border-radius: 2px; + position: relative; + text-align: center; + font-size: 16px; + color: gray; + cursor: pointer; +} + #add { margin-right: 0; } @@ -642,10 +661,6 @@ body { animation: qrfadeout 0.2s 1 ease-in; } -#secret_box { - display: none; -} - #secret_box input, #security input, #passphrase input { @@ -670,6 +685,11 @@ body { margin-left: 0 !important; } +#secret_box select { + margin: 20px; + font-size: 16px; +} + #secret_box label, #security label, #passphrase label, @@ -690,7 +710,8 @@ body { font-size: 16px; } -#message { +#message, +#confirm { position: absolute; width: 300px; padding: 10px; diff --git a/popup.html b/popup.html index 0a3773873..22be77a62 100644 --- a/popup.html +++ b/popup.html @@ -71,22 +71,22 @@
-
{{ i18n.add_qr }}
-
{{ i18n.add_secret }}
-
+
+
{{ i18n.add_qr }}
+
{{ i18n.add_secret }}
+
+
- + - -
- - -
-
- - -
-
{{ i18n.ok }}
+ + +
{{ i18n.ok }}
@@ -144,6 +144,15 @@
{{ i18n.ok }}
+ +
+
{{ confirmMessage }}
+
+
{{ i18n.ok }}
+
Cancel
+
+
+
{{ notification }}
diff --git a/src/popup.ts b/src/popup.ts index ce0c92282..9ab3623c2 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -174,6 +174,10 @@ async function getQrUrl(entry: OTPEntry) { }); } +function isCustomEvent(event: Event): event is CustomEvent { + return 'detail' in event; +} + async function init() { const zoom = Number(localStorage.zoom) || 100; resize(zoom); @@ -193,6 +197,7 @@ async function init() { encryption, exportData, zoom, + OTPType, class: { timeout: false, edit: false, @@ -208,9 +213,11 @@ async function init() { sector: '', info: '', message: '', + confirmMessage: '', qr: '', notification: '', - notificationTimeout: 0 + notificationTimeout: 0, + newAccount: {show: false, account: '', secret: '', type: OTPType.totp} }, methods: { showBulls: (code: string) => { @@ -254,7 +261,7 @@ async function init() { resize(authenticator.zoom); }, removeEntry: async (entry: OTPEntry) => { - if (confirm('Remove?')) { + if (await authenticator.confirm('Remove?')) { await entry.delete(); authenticator.exportData = await EntryStorage.getExport(); authenticator.entries = await getEntries(authenticator.encryption); @@ -328,6 +335,51 @@ async function init() { }, 1000); } }); + }, + addNewAccount: async () => { + const entry = new OTPEntry( + authenticator.newAccount.type, '', authenticator.newAccount.secret, + authenticator.newAccount.account, 0); + await entry.create(authenticator.encryption); + authenticator.exportData = await EntryStorage.getExport(); + authenticator.entries = await getEntries(authenticator.encryption); + authenticator.newAccount.type = OTPType.totp; + authenticator.account = ''; + authenticator.secret = ''; + authenticator.newAccount.show = false; + authenticator.closeInfo(); + authenticator.class.edit = false; + + const codes = document.getElementById('codes'); + if (codes) { + // wait vue apply changes to dom + setTimeout(() => { + codes.scrollTop = 0; + }, 0); + } + }, + confirm: async (message: string) => { + return new Promise( + (resolve: (value: boolean) => void, + reject: (reason: Error) => void) => { + authenticator.confirmMessage = message; + window.addEventListener('confirm', (event) => { + authenticator.confirmMessage = ''; + if (!isCustomEvent(event)) { + return resolve(false); + } + return resolve(event.detail); + }); + return; + }); + }, + confirmOK: () => { + const confirmEvent = new CustomEvent('confirm', {detail: true}); + window.dispatchEvent(confirmEvent); + }, + confirmCancel: () => { + const confirmEvent = new CustomEvent('confirm', {detail: false}); + window.dispatchEvent(confirmEvent); } } }); @@ -336,6 +388,7 @@ async function init() { setInterval(async () => { await updateCode(authenticator); }, 1000); + return; } From c61780c1fe46f2b0f31d9f7c3034dd7d063fc838 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Tue, 6 Feb 2018 02:24:52 +0800 Subject: [PATCH 018/178] save --- css/popup.css | 4 ++-- popup.html | 5 +++-- src/models/otp.ts | 13 +++++++++---- src/models/storage.ts | 2 +- src/popup.ts | 30 ++++++++++++++++++++++++++---- 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/css/popup.css b/css/popup.css index b5c301aa1..9a74968db 100644 --- a/css/popup.css +++ b/css/popup.css @@ -432,11 +432,11 @@ body { cursor: pointer; } -.counter:not([disabled="true"]):hover { +.counter:not(.disabled):hover { color: #000; } -.counter[disabled="true"] { +.counter.disabled { color: #CCC; cursor: default; } diff --git a/popup.html b/popup.html index 22be77a62..80a0aecc4 100644 --- a/popup.html +++ b/popup.html @@ -24,12 +24,13 @@
-
+
+
{{ entry.issuer.split('::')[0] }}
-
+
diff --git a/src/models/otp.ts b/src/models/otp.ts index c4d5eea08..1de164537 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -16,15 +16,19 @@ class OTPEntry implements OTP { constructor( type: OTPType, issuer: string, secret: string, account: string, - index: number) { + index: number, counter: number) { this.type = type; this.index = index; this.issuer = issuer; this.secret = secret; this.account = account; this.hash = CryptoJS.MD5(secret).toString(); - this.counter = 0; - this.generate(); + this.counter = counter; + if (this.type !== OTPType.hotp) { + this.generate(); + } else { + this.code = '••••••'; + } } async create(encryption: Encription) { @@ -33,7 +37,7 @@ class OTPEntry implements OTP { } async update(encryption: Encription) { - EntryStorage.update(encryption, this); + await EntryStorage.update(encryption, this); return; } @@ -46,6 +50,7 @@ class OTPEntry implements OTP { if (this.type !== OTPType.hotp) { return; } + this.generate(); this.counter++; await this.update(encryption); return; diff --git a/src/models/storage.ts b/src/models/storage.ts index 630fd6149..95711cbf5 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -193,7 +193,7 @@ class EntryStorage { const entry = new OTPEntry( type, entryData.issuer, entryData.secret, entryData.account, - entryData.index); + entryData.index, entryData.counter); data.push(entry); // we need migrate secret in old format here diff --git a/src/popup.ts b/src/popup.ts index 9ab3623c2..aa87459a5 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -115,7 +115,9 @@ async function updateCode(app: any) { if (second < 1) { const entries = app.entries as OTP[]; for (let i = 0; i < entries.length; i++) { - entries[i].generate(); + if (entries[i].type !== OTPType.hotp) { + entries[i].generate(); + } } } } @@ -208,7 +210,8 @@ async function init() { qrfadein: false, qrfadeout: false, notificationFadein: false, - notificationFadeout: false + notificationFadeout: false, + hotpDiabled: false }, sector: '', info: '', @@ -337,9 +340,17 @@ async function init() { }); }, addNewAccount: async () => { + let type: OTPType; + if (!/^[a-z2-7]+=*$/i.test(authenticator.newAccount.secret) && + /^[0-9a-f]+$/i.test(authenticator.newAccount.secret)) { + type = OTPType.hex; + } else { + type = authenticator.newAccount.type; + } + const entry = new OTPEntry( - authenticator.newAccount.type, '', authenticator.newAccount.secret, - authenticator.newAccount.account, 0); + type, '', authenticator.newAccount.secret, + authenticator.newAccount.account, 0, 0); await entry.create(authenticator.encryption); authenticator.exportData = await EntryStorage.getExport(); authenticator.entries = await getEntries(authenticator.encryption); @@ -380,6 +391,17 @@ async function init() { confirmCancel: () => { const confirmEvent = new CustomEvent('confirm', {detail: false}); window.dispatchEvent(confirmEvent); + }, + nextCode: async (entry: OTPEntry) => { + if (authenticator.class.hotpDiabled) { + return; + } + authenticator.class.hotpDiabled = true; + await entry.next(authenticator.encryption); + authenticator.exportData = await EntryStorage.getExport(); + setTimeout(() => { + authenticator.class.hotpDiabled = false; + }, 3000); } } }); From da88fa3aca77e981e6d016a35d602fbc064ce311 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Tue, 6 Feb 2018 02:57:02 +0800 Subject: [PATCH 019/178] save --- popup.html | 4 ++-- src/models/encryption.ts | 2 +- src/models/interface.ts | 6 +++--- src/models/otp.ts | 6 +++--- src/models/storage.ts | 13 +++++++------ src/popup.ts | 42 +++++++++++++++++++++++++++++----------- 6 files changed, 47 insertions(+), 26 deletions(-) diff --git a/popup.html b/popup.html index 80a0aecc4..54870fab9 100644 --- a/popup.html +++ b/popup.html @@ -107,12 +107,12 @@
{{ i18n.passphrase_info }}
- +
-
{{ i18n.ok }}
+
{{ i18n.ok }}
diff --git a/src/models/encryption.ts b/src/models/encryption.ts index 6e084bee8..c6ba0a8fb 100644 --- a/src/models/encryption.ts +++ b/src/models/encryption.ts @@ -1,7 +1,7 @@ /* tslint:disable:no-reference */ /// -class Encription { +class Encryption { private password: string; constructor(password: string) { diff --git a/src/models/interface.ts b/src/models/interface.ts index cbaa19285..6c8c013f3 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -18,9 +18,9 @@ interface OTP { hash: string; counter: number; code: string; - create(encryption: Encription): Promise; - update(encryption: Encription): Promise; - next(encryption: Encription): Promise; + create(encryption: Encryption): Promise; + update(encryption: Encryption): Promise; + next(encryption: Encryption): Promise; delete(): Promise; generate(): void; } diff --git a/src/models/otp.ts b/src/models/otp.ts index 1de164537..bc5ba8935 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -31,12 +31,12 @@ class OTPEntry implements OTP { } } - async create(encryption: Encription) { + async create(encryption: Encryption) { await EntryStorage.add(encryption, this); return; } - async update(encryption: Encription) { + async update(encryption: Encryption) { await EntryStorage.update(encryption, this); return; } @@ -46,7 +46,7 @@ class OTPEntry implements OTP { return; } - async next(encryption: Encription) { + async next(encryption: Encryption) { if (this.type !== OTPType.hotp) { return; } diff --git a/src/models/storage.ts b/src/models/storage.ts index 95711cbf5..7d26ebcbc 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -5,7 +5,7 @@ class EntryStorage { private static getOTPStorageFromEntry( - encryption: Encription, entry: OTPEntry): OTPStorage { + encryption: Encryption, entry: OTPEntry): OTPStorage { const storageItem: OTPStorage = { account: entry.account, hash: entry.hash, @@ -41,7 +41,8 @@ class EntryStorage { static async getExport() { return new Promise( - (resolve: (value: string) => void, reject: (reason: Error) => void) => { + (resolve: (value: {[hash: string]: OTPStorage}) => void, + reject: (reason: Error) => void) => { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(_data)) { @@ -51,7 +52,7 @@ class EntryStorage { delete _data[hash]; } } - return resolve(JSON.stringify(_data, null, 2)); + return resolve(_data); }); return; } catch (error) { @@ -115,7 +116,7 @@ class EntryStorage { }); } - static async add(encryption: Encription, entry: OTPEntry) { + static async add(encryption: Encryption, entry: OTPEntry) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { try { @@ -136,7 +137,7 @@ class EntryStorage { }); } - static async update(encryption: Encription, entry: OTPEntry) { + static async update(encryption: Encryption, entry: OTPEntry) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { try { @@ -157,7 +158,7 @@ class EntryStorage { }); } - static async get(encryption: Encription) { + static async get(encryption: Encryption) { return new Promise( (resolve: (value: OTPEntry[]) => void, reject: (reason: Error) => void) => { diff --git a/src/popup.ts b/src/popup.ts index aa87459a5..0f780fa19 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -11,8 +11,8 @@ declare var Vue: any; /* tslint:disable-next-line:no-any */ declare var QRCode: any; -async function getEntries(encryption: Encription) { - const optEntries: OTP[] = await EntryStorage.get(encryption); +async function getEntries(encryption: Encryption) { + const optEntries: OTPEntry[] = await EntryStorage.get(encryption); return optEntries; } @@ -186,9 +186,17 @@ async function init() { const version = await getVersion(); const i18n = await loadI18nMessages(); - const encryption = new Encription(''); - const entries = await getEntries(encryption); const exportData = await EntryStorage.getExport(); + const encryption: Encryption = new Encryption(''); + let entries: OTPEntry[] = []; + let shouldShowPassphrase = false; + for (const hash in Object.keys(exportData)) { + if (exportData[hash].encrypted) { + shouldShowPassphrase = true; + } else { + entries = await getEntries(encryption); + } + } const authenticator = new Vue({ el: '#authenticator', @@ -197,9 +205,9 @@ async function init() { i18n, entries, encryption, - exportData, zoom, OTPType, + exportData: JSON.stringify(exportData, null, 2), class: { timeout: false, edit: false, @@ -214,11 +222,12 @@ async function init() { hotpDiabled: false }, sector: '', - info: '', + info: shouldShowPassphrase ? 'passphrase' : '', message: '', confirmMessage: '', qr: '', notification: '', + passphrase: '', notificationTimeout: 0, newAccount: {show: false, account: '', secret: '', type: OTPType.totp} }, @@ -226,8 +235,8 @@ async function init() { showBulls: (code: string) => { return new Array(code.length).fill('•').join(''); }, - updateEncription: (password: string) => { - authenticator.encryption = new Encription(password); + updateEncryption: (password: string) => { + authenticator.encryption = new Encryption(password); }, showMenu: () => { authenticator.class.slidein = true; @@ -266,7 +275,8 @@ async function init() { removeEntry: async (entry: OTPEntry) => { if (await authenticator.confirm('Remove?')) { await entry.delete(); - authenticator.exportData = await EntryStorage.getExport(); + const exportData = await EntryStorage.getExport(); + authenticator.exportData = JSON.stringify(exportData, null, 2); authenticator.entries = await getEntries(authenticator.encryption); updateCode(authenticator); } @@ -352,7 +362,8 @@ async function init() { type, '', authenticator.newAccount.secret, authenticator.newAccount.account, 0, 0); await entry.create(authenticator.encryption); - authenticator.exportData = await EntryStorage.getExport(); + const exportData = await EntryStorage.getExport(); + authenticator.exportData = JSON.stringify(exportData, null, 2); authenticator.entries = await getEntries(authenticator.encryption); authenticator.newAccount.type = OTPType.totp; authenticator.account = ''; @@ -398,10 +409,19 @@ async function init() { } authenticator.class.hotpDiabled = true; await entry.next(authenticator.encryption); - authenticator.exportData = await EntryStorage.getExport(); + const exportData = await EntryStorage.getExport(); + authenticator.exportData = JSON.stringify(exportData, null, 2); setTimeout(() => { authenticator.class.hotpDiabled = false; }, 3000); + }, + applyPassphrase: async () => { + authenticator.encryption.updateEncryptionPassword( + authenticator.passphrase); + const exportData = await EntryStorage.getExport(); + authenticator.exportData = JSON.stringify(exportData, null, 2); + authenticator.entries = await getEntries(authenticator.encryption); + updateCode(authenticator); } } }); From e2522ef3839b1283bdc450e9c25beb37f994f5e0 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Tue, 6 Feb 2018 02:59:12 +0800 Subject: [PATCH 020/178] save --- src/popup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/popup.ts b/src/popup.ts index 0f780fa19..b840b55bc 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -190,7 +190,7 @@ async function init() { const encryption: Encryption = new Encryption(''); let entries: OTPEntry[] = []; let shouldShowPassphrase = false; - for (const hash in Object.keys(exportData)) { + for (const hash of Object.keys(exportData)) { if (exportData[hash].encrypted) { shouldShowPassphrase = true; } else { From e490011de52ddb44ac807480d9a6d50b155d80c8 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 6 Feb 2018 15:45:47 +0800 Subject: [PATCH 021/178] save --- src/models/storage.ts | 40 ++++++++++++++++++++++++++++++++++------ src/popup.ts | 42 +++++++++++++++++------------------------- 2 files changed, 51 insertions(+), 31 deletions(-) diff --git a/src/models/storage.ts b/src/models/storage.ts index 7d26ebcbc..0517bac95 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -39,13 +39,34 @@ class EntryStorage { return newData; } - static async getExport() { + static async hasEncryptedEntrie() { + return new Promise( + (resolve: (value: boolean) => void, + reject: (reason: Error) => void) => { + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { + for (const hash of Object.keys(_data)) { + if (_data[hash].encrypted) { + return resolve(true); + } + } + return resolve(false); + }); + return; + }); + } + + static async getExport(encryption: Encryption) { return new Promise( (resolve: (value: {[hash: string]: OTPStorage}) => void, reject: (reason: Error) => void) => { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(_data)) { + // decrypt the data to export + _data[hash].secret = _data[hash].encrypted ? + encryption.getDecryptedSecret(_data[hash].secret) : + _data[hash].secret; + _data[hash].encrypted = false; // we need correct hash if (hash !== _data[hash].hash) { _data[_data[hash].hash] = _data[hash]; @@ -61,20 +82,23 @@ class EntryStorage { }); } - static async import(data: {[hash: string]: OTPStorage}) { + static async import( + encryption: Encryption, data: {[hash: string]: OTPStorage}) { return new Promise( (resolve: () => void, reject: (reason: Error) => void) => { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(data)) { // never trust data import from user - if (!data[hash].secret) { + // we do not support encrypted data import any longer + if (!data[hash].secret || data[hash].encrypted) { + // we need give a failed warning continue; } data[hash].hash = data[hash].hash || hash; data[hash].account = data[hash].account || ''; - data[hash].encrypted = !!data[hash].encrypted; + data[hash].encrypted = encryption.getEncryptionStatus(); data[hash].index = data[hash].index || 0; data[hash].issuer = data[hash].issuer || ''; data[hash].type = data[hash].type || OTPType[OTPType.totp]; @@ -104,6 +128,9 @@ class EntryStorage { } } + data[hash].secret = + encryption.getEncryptedSecret(data[hash].secret); + _data[hash] = data[hash]; } _data = this.ensureUniqueIndex(_data); @@ -189,8 +216,9 @@ class EntryStorage { entryData.type = OTPType[OTPType.totp]; needMigrate = true; } - entryData.secret = - encryption.getDecryptedSecret(entryData.secret); + entryData.secret = entryData.encrypted ? + encryption.getDecryptedSecret(entryData.secret) : + entryData.secret; const entry = new OTPEntry( type, entryData.issuer, entryData.secret, entryData.account, diff --git a/src/popup.ts b/src/popup.ts index b840b55bc..40b76f3d0 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -186,17 +186,11 @@ async function init() { const version = await getVersion(); const i18n = await loadI18nMessages(); - const exportData = await EntryStorage.getExport(); const encryption: Encryption = new Encryption(''); - let entries: OTPEntry[] = []; - let shouldShowPassphrase = false; - for (const hash of Object.keys(exportData)) { - if (exportData[hash].encrypted) { - shouldShowPassphrase = true; - } else { - entries = await getEntries(encryption); - } - } + const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); + const exportData = + shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); + const entries = shouldShowPassphrase ? [] : await getEntries(encryption); const authenticator = new Vue({ el: '#authenticator', @@ -262,11 +256,18 @@ async function init() { authenticator.info = ''; }, 200); }, + importEnties: async () => { + await EntryStorage.import( + authenticator.encryption, JSON.parse(authenticator.exportData)); + await authenticator.updateEntries(); + authenticator.message = authenticator.i18n.updateSuccess; + }, updateEntries: async () => { - await EntryStorage.import(JSON.parse(authenticator.exportData)); + const exportData = + await EntryStorage.getExport(authenticator.encryption); + authenticator.exportData = JSON.stringify(exportData, null, 2); authenticator.entries = await getEntries(authenticator.encryption); updateCode(authenticator); - authenticator.message = authenticator.i18n.updateSuccess; }, saveZoom: () => { localStorage.zoom = authenticator.zoom; @@ -275,10 +276,7 @@ async function init() { removeEntry: async (entry: OTPEntry) => { if (await authenticator.confirm('Remove?')) { await entry.delete(); - const exportData = await EntryStorage.getExport(); - authenticator.exportData = JSON.stringify(exportData, null, 2); - authenticator.entries = await getEntries(authenticator.encryption); - updateCode(authenticator); + await authenticator.updateEntries(); } return; }, @@ -362,9 +360,7 @@ async function init() { type, '', authenticator.newAccount.secret, authenticator.newAccount.account, 0, 0); await entry.create(authenticator.encryption); - const exportData = await EntryStorage.getExport(); - authenticator.exportData = JSON.stringify(exportData, null, 2); - authenticator.entries = await getEntries(authenticator.encryption); + await authenticator.updateEntries(); authenticator.newAccount.type = OTPType.totp; authenticator.account = ''; authenticator.secret = ''; @@ -409,8 +405,7 @@ async function init() { } authenticator.class.hotpDiabled = true; await entry.next(authenticator.encryption); - const exportData = await EntryStorage.getExport(); - authenticator.exportData = JSON.stringify(exportData, null, 2); + await authenticator.updateEntries(); setTimeout(() => { authenticator.class.hotpDiabled = false; }, 3000); @@ -418,10 +413,7 @@ async function init() { applyPassphrase: async () => { authenticator.encryption.updateEncryptionPassword( authenticator.passphrase); - const exportData = await EntryStorage.getExport(); - authenticator.exportData = JSON.stringify(exportData, null, 2); - authenticator.entries = await getEntries(authenticator.encryption); - updateCode(authenticator); + await authenticator.updateEntries(); } } }); From dde462726115579160d3c90266344b449b0ac86b Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 6 Feb 2018 16:46:24 +0800 Subject: [PATCH 022/178] save --- README.md | 24 +++--- css/popup.css | 28 +------ package-lock.json | 190 +++++------------------------------------- package.json | 3 - popup.html | 21 ++--- src/models/storage.ts | 2 +- src/popup.ts | 48 +++++++++-- 7 files changed, 89 insertions(+), 227 deletions(-) diff --git a/README.md b/README.md index 7fcb00ebd..6b05fb70f 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,24 @@ -# authenticator +# Authenticator > For Google Authenticator and Battle.net Authenticator. ## Build Setup ``` bash +# install typescript +npm install -g typescript # install dependencies npm install +# check typescript style +gts check +# try to auto fix style issue +gts fix +# compile +npm run compile +``` -# serve with hot reload at localhost:8080 -npm run dev +## FAQ -# build for production with minification -npm run build +### gts is found -# lint all *.js and *.vue files -npm run lint - -# run unit tests -npm test -``` \ No newline at end of file +gts (Google TypeScript style) is installed locally by default, see to add local node modules into path, or run `npm install -g gts` to install gts global. \ No newline at end of file diff --git a/css/popup.css b/css/popup.css index 9a74968db..8cf9b62b9 100644 --- a/css/popup.css +++ b/css/popup.css @@ -527,12 +527,7 @@ body { margin: 10px; } -#info, -#addAccount, -#security, -#passphrase, -#export, -#resize { +#info { position: absolute; height: 460px; width: 300px; @@ -545,32 +540,17 @@ body { z-index: 100; } -#info.fadein, -#addAccount.fadein, -#security.fadein, -#passphrase.fadein, -#export.fadein, -#resize.fadein { +#info.fadein { top: 10px; animation: fadein 0.2s 1 ease-out; } -#info.fadeout, -#addAccount.fadeout, -#security.fadeout, -#passphrase.fadeout, -#export.fadeout, -#resize.fadeout { +#info.fadeout { top: 110px; animation: fadeout 0.2s 1 ease-in; } -#infoClose, -#addAccountClose, -#securityClose, -#passphraseClose, -#exportClose, -#resizeClose { +#infoClose { height: 20px; width: 20px; font-size: 14px; diff --git a/package-lock.json b/package-lock.json index fc87f7bc3..f4ddf795d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,16 +40,6 @@ "integrity": "sha1-leg9uph4f/eW0tXzehklq/Qbycs=", "dev": true }, - "acorn": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", - "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==" - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, "ansi-align": { "version": "2.0.0", "resolved": "http://registry.npm.taobao.org/ansi-align/download/ansi-align-2.0.0.tgz", @@ -92,11 +82,6 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, - "ast-types": { - "version": "0.9.6", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.9.6.tgz", - "integrity": "sha1-ECyenpAF0+fjgpvwxPok7oYu6bk=" - }, "async": { "version": "1.5.2", "resolved": "http://registry.npm.taobao.org/async/download/async-1.5.2.tgz", @@ -159,12 +144,8 @@ "balanced-match": { "version": "1.0.0", "resolved": "http://registry.npm.taobao.org/balanced-match/download/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base62": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/base62/-/base62-1.2.1.tgz", - "integrity": "sha512-xVtfFHNPUzpCNHygpXFGMlDk3saxXLQcOOQzAAk6ibvlAHgT6WKXLv9rMFhcyEK1n9LuDmp/LxyGW/Fm9L8++g==" + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true }, "boxen": { "version": "1.3.0", @@ -185,6 +166,7 @@ "version": "1.1.8", "resolved": "http://registry.npm.taobao.org/brace-expansion/download/brace-expansion-1.1.8.tgz", "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true, "requires": { "balanced-match": "1.0.0", "concat-map": "0.0.1" @@ -286,42 +268,14 @@ "commander": { "version": "2.12.2", "resolved": "http://registry.npm.taobao.org/commander/download/commander-2.12.2.tgz", - "integrity": "sha1-D1lGxCftnsDZGka7ne9T5UZQ5VU=" - }, - "commoner": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/commoner/-/commoner-0.10.8.tgz", - "integrity": "sha1-NPw2cs0kOT6LtH5wyqApOBH08sU=", - "requires": { - "commander": "2.12.2", - "detective": "4.7.1", - "glob": "5.0.15", - "graceful-fs": "4.1.11", - "iconv-lite": "0.4.19", - "mkdirp": "0.5.1", - "private": "0.1.8", - "q": "1.5.1", - "recast": "0.11.23" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "requires": { - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" - } - } - } + "integrity": "sha1-D1lGxCftnsDZGka7ne9T5UZQ5VU=", + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "http://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true }, "configstore": { "version": "3.1.1", @@ -402,20 +356,6 @@ "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", "dev": true }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "detective": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz", - "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==", - "requires": { - "acorn": "5.4.1", - "defined": "1.0.0" - } - }, "diff": { "version": "3.4.0", "resolved": "http://registry.npm.taobao.org/diff/download/diff-3.4.0.tgz", @@ -437,15 +377,6 @@ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", "dev": true }, - "envify": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/envify/-/envify-3.4.1.tgz", - "integrity": "sha1-1xIjKejfFoi6dxsSUBkXyc5cvOg=", - "requires": { - "jstransform": "11.0.3", - "through": "2.3.8" - } - }, "error-ex": { "version": "1.3.1", "resolved": "http://registry.npm.taobao.org/error-ex/download/error-ex-1.3.1.tgz", @@ -461,11 +392,6 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, - "esprima-fb": { - "version": "15001.1.0-dev-harmony-fb", - "resolved": "https://registry.npmjs.org/esprima-fb/-/esprima-fb-15001.1.0-dev-harmony-fb.tgz", - "integrity": "sha1-MKlHMDxrjV6VW+4rmbHSMyBqaQE=" - }, "esutils": { "version": "2.0.2", "resolved": "http://registry.npm.taobao.org/esutils/download/esutils-2.0.2.tgz", @@ -573,7 +499,8 @@ "graceful-fs": { "version": "4.1.11", "resolved": "http://registry.npm.taobao.org/graceful-fs/download/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true }, "gts": { "version": "0.5.2", @@ -624,7 +551,8 @@ "iconv-lite": { "version": "0.4.19", "resolved": "http://registry.npm.taobao.org/iconv-lite/download/iconv-lite-0.4.19.tgz", - "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=" + "integrity": "sha1-90aPYBNfXl2tM5nAqBvpoWA6CCs=", + "dev": true }, "import-lazy": { "version": "2.1.0", @@ -648,6 +576,7 @@ "version": "1.0.6", "resolved": "http://registry.npm.taobao.org/inflight/download/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, "requires": { "once": "1.4.0", "wrappy": "1.0.2" @@ -656,7 +585,8 @@ "inherits": { "version": "2.0.3", "resolved": "http://registry.npm.taobao.org/inherits/download/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true }, "ini": { "version": "1.3.5", @@ -786,18 +716,6 @@ "integrity": "sha1-UBg80bLSUnXeBp6ecbRnrJ6rlzo=", "dev": true }, - "jstransform": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/jstransform/-/jstransform-11.0.3.tgz", - "integrity": "sha1-CaeJk+CuTU70SH9hVakfYZDLQiM=", - "requires": { - "base62": "1.2.1", - "commoner": "0.10.8", - "esprima-fb": "15001.1.0-dev-harmony-fb", - "object-assign": "2.1.1", - "source-map": "0.4.4" - } - }, "latest-version": { "version": "3.1.0", "resolved": "http://registry.npm.taobao.org/latest-version/download/latest-version-3.1.0.tgz", @@ -903,6 +821,7 @@ "version": "3.0.4", "resolved": "http://registry.npm.taobao.org/minimatch/download/minimatch-3.0.4.tgz", "integrity": "sha1-UWbihkV/AzBgZL5Ul+jbsMPTIIM=", + "dev": true, "requires": { "brace-expansion": "1.1.8" } @@ -923,21 +842,6 @@ "is-plain-obj": "1.1.0" } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - } - } - }, "mute-stream": { "version": "0.0.7", "resolved": "http://registry.npm.taobao.org/mute-stream/download/mute-stream-0.0.7.tgz", @@ -965,15 +869,11 @@ "path-key": "2.0.1" } }, - "object-assign": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-2.1.1.tgz", - "integrity": "sha1-Q8NuXVaf+OSBbE76i+AtJpZ8GKo=" - }, "once": { "version": "1.4.0", "resolved": "http://registry.npm.taobao.org/once/download/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, "requires": { "wrappy": "1.0.2" } @@ -1045,7 +945,8 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "http://registry.npm.taobao.org/path-is-absolute/download/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true }, "path-is-inside": { "version": "1.0.2", @@ -1086,22 +987,12 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" - }, "pseudomap": { "version": "1.0.2", "resolved": "http://registry.npm.taobao.org/pseudomap/download/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, "quick-lru": { "version": "1.1.0", "resolved": "http://registry.npm.taobao.org/quick-lru/download/quick-lru-1.1.0.tgz", @@ -1141,29 +1032,6 @@ "read-pkg": "3.0.0" } }, - "recast": { - "version": "0.11.23", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.11.23.tgz", - "integrity": "sha1-RR/TAEqx5N+bTktmN2sqIZEkYtM=", - "requires": { - "ast-types": "0.9.6", - "esprima": "3.1.3", - "private": "0.1.8", - "source-map": "0.5.7" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } - }, "redent": { "version": "2.0.0", "resolved": "http://registry.npm.taobao.org/redent/download/redent-2.0.0.tgz", @@ -1287,14 +1155,6 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "requires": { - "amdefine": "1.0.1" - } - }, "spdx-correct": { "version": "1.0.2", "resolved": "http://registry.npm.taobao.org/spdx-correct/download/spdx-correct-1.0.2.tgz", @@ -1380,7 +1240,8 @@ "through": { "version": "2.3.8", "resolved": "http://registry.npm.taobao.org/through/download/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true }, "timed-out": { "version": "4.0.1", @@ -1494,14 +1355,6 @@ "spdx-expression-parse": "1.0.4" } }, - "vue": { - "version": "1.0.28-csp", - "resolved": "https://registry.npmjs.org/vue/-/vue-1.0.28-csp.tgz", - "integrity": "sha1-AoFNUC7/Pk77ahK4gvvztV8eLx4=", - "requires": { - "envify": "3.4.1" - } - }, "which": { "version": "1.3.0", "resolved": "http://registry.npm.taobao.org/which/download/which-1.3.0.tgz", @@ -1523,7 +1376,8 @@ "wrappy": { "version": "1.0.2", "resolved": "http://registry.npm.taobao.org/wrappy/download/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, "write-file-atomic": { "version": "2.3.0", diff --git a/package.json b/package.json index 6e052be71..2e54af799 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,5 @@ "@types/jssha": "0.0.29", "gts": "^0.5.2", "typescript": "^2.6.1" - }, - "dependencies": { - "vue": "^1.0.28-csp" } } diff --git a/popup.html b/popup.html index 54870fab9..a6dc62d05 100644 --- a/popup.html +++ b/popup.html @@ -5,6 +5,7 @@ + @@ -66,7 +67,7 @@
-
+
@@ -91,27 +92,19 @@
-
+
{{ i18n.security_warning }}
- + - -
- - -
-
{{ i18n.ok }}
+ +
{{ i18n.ok }}
-
+
{{ i18n.passphrase_info }}
-
- - -
{{ i18n.ok }}
diff --git a/src/models/storage.ts b/src/models/storage.ts index 0517bac95..b2886056f 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -261,7 +261,7 @@ class EntryStorage { if (needMigrate) { const _entry: {[hash: string]: OTPStorage} = {}; _entry[hash] = entryData; - this.import(_entry); + this.import(encryption, _entry); } } diff --git a/src/popup.ts b/src/popup.ts index 40b76f3d0..94ae39813 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -216,25 +216,24 @@ async function init() { hotpDiabled: false }, sector: '', - info: shouldShowPassphrase ? 'passphrase' : '', + info: '', message: '', confirmMessage: '', qr: '', notification: '', passphrase: '', notificationTimeout: 0, - newAccount: {show: false, account: '', secret: '', type: OTPType.totp} + newAccount: {show: false, account: '', secret: '', type: OTPType.totp}, + newPassphrase: {phrase: '', confirm: ''} }, methods: { showBulls: (code: string) => { return new Array(code.length).fill('•').join(''); }, - updateEncryption: (password: string) => { - authenticator.encryption = new Encryption(password); - }, showMenu: () => { authenticator.class.slidein = true; authenticator.class.slideout = false; + return; }, closeMenu: () => { authenticator.class.slidein = false; @@ -242,11 +241,13 @@ async function init() { setTimeout(() => { authenticator.class.slideout = false; }, 200); + return; }, showInfo: (tab: string) => { authenticator.class.fadein = true; authenticator.class.fadeout = false; authenticator.info = tab; + return; }, closeInfo: () => { authenticator.class.fadein = false; @@ -255,12 +256,14 @@ async function init() { authenticator.class.fadeout = false; authenticator.info = ''; }, 200); + return; }, importEnties: async () => { await EntryStorage.import( authenticator.encryption, JSON.parse(authenticator.exportData)); await authenticator.updateEntries(); authenticator.message = authenticator.i18n.updateSuccess; + return; }, updateEntries: async () => { const exportData = @@ -268,10 +271,12 @@ async function init() { authenticator.exportData = JSON.stringify(exportData, null, 2); authenticator.entries = await getEntries(authenticator.encryption); updateCode(authenticator); + return; }, saveZoom: () => { localStorage.zoom = authenticator.zoom; resize(authenticator.zoom); + return; }, removeEntry: async (entry: OTPEntry) => { if (await authenticator.confirm('Remove?')) { @@ -300,15 +305,18 @@ async function init() { codes.scrollTop = authenticator.class.edit ? codes.scrollHeight : 0; }, 0); } + return; }, shouldShowQrIcon: (entry: OTPEntry) => { - return entry.type !== OTPType.battle && entry.type !== OTPType.steam; + return entry.secret !== 'Encrypted' && entry.type !== OTPType.battle && + entry.type !== OTPType.steam; }, showQr: async (entry: OTPEntry) => { const qrUrl = await getQrUrl(entry); authenticator.qr = `url(${qrUrl})`; authenticator.class.qrfadein = true; authenticator.class.qrfadeout = false; + return; }, hideQr: () => { authenticator.class.qrfadein = false; @@ -316,11 +324,17 @@ async function init() { setTimeout(() => { authenticator.class.qrfadeout = false; }, 200); + return; }, copyCode: (entry: OTPEntry) => { if (authenticator.class.edit) { return; } + + if (entry.code === 'Encrypted') { + authenticator.showInfo('passphrase'); + return; + } chrome.permissions.request( {permissions: ['clipboardWrite']}, (granted) => { if (granted) { @@ -346,6 +360,7 @@ async function init() { }, 1000); } }); + return; }, addNewAccount: async () => { let type: OTPType; @@ -375,6 +390,7 @@ async function init() { codes.scrollTop = 0; }, 0); } + return; }, confirm: async (message: string) => { return new Promise( @@ -394,10 +410,12 @@ async function init() { confirmOK: () => { const confirmEvent = new CustomEvent('confirm', {detail: true}); window.dispatchEvent(confirmEvent); + return; }, confirmCancel: () => { const confirmEvent = new CustomEvent('confirm', {detail: false}); window.dispatchEvent(confirmEvent); + return; }, nextCode: async (entry: OTPEntry) => { if (authenticator.class.hotpDiabled) { @@ -409,15 +427,33 @@ async function init() { setTimeout(() => { authenticator.class.hotpDiabled = false; }, 3000); + return; }, applyPassphrase: async () => { authenticator.encryption.updateEncryptionPassword( authenticator.passphrase); await authenticator.updateEntries(); + authenticator.closeInfo(); + return; + }, + changePassphrase: async () => { + if (authenticator.newPassphrase.phrase !== + authenticator.newPassphrase.confirm) { + authenticator.message = authenticator.i18n.phrase_not_match; + return; + } + authenticator.encryption.updateEncryptionPassword( + authenticator.newPassphrase.phrase); + await authenticator.importEnties(); + return; } } }); + if (shouldShowPassphrase) { + authenticator.showInfo('passphrase'); + } + updateCode(authenticator); setInterval(async () => { await updateCode(authenticator); From bced1a36340a56e54f1a9ac43b3d9aa889e46fcb Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 6 Feb 2018 16:49:11 +0800 Subject: [PATCH 023/178] remove popup bak and background --- manifest.json | 16 +----- popup.bak.html | 130 ---------------------------------------------- src/background.ts | 15 ------ 3 files changed, 1 insertion(+), 160 deletions(-) delete mode 100644 popup.bak.html delete mode 100644 src/background.ts diff --git a/manifest.json b/manifest.json index 1ff9f9e57..c928e471a 100644 --- a/manifest.json +++ b/manifest.json @@ -18,24 +18,10 @@ "default_title": "__MSG_extShortName__", "default_popup": "popup.html" }, - "background": { - "scripts": [ - "js/aes.js", - "js/md5.js", - "js/sha.js", - "build/models/encryption.js", - "build/models/interface.js", - "build/models/key-utilities.js", - "build/models/otp.js", - "build/models/storage.js", - "build/background.js" - ], - "persistent": false - }, "content_scripts": [ { "matches": [""], - "css": [], + "css": ["css/content.css"], "js": ["build/content.js"] } ], diff --git a/popup.bak.html b/popup.bak.html deleted file mode 100644 index 1ed902c92..000000000 --- a/popup.bak.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - -
-
Battlenet
68667399
DigitalOcean
897222
Slack
924170
GitLab
500089
GitHub
815823
www.lakebtc.com
352408
yunbi.com
976894
huobi
391281
Bitstamp
049350
GateHub
793891
V2EX
644612
BigONE
692926
-
-
-
- -
-
-
-
-
-
-
-
Scan QR Code
-
Manual Entry
-
- - - - -
- - -
-
- - -
-
OK
-
-
-
-
-
-
This passphrase will be used to encrypt your secrets. No one can help you if you forget the passphrase.
- - - - -
- - -
-
OK
-
-
-
-
Input passphrase to decrypt account data.
- - -
- - -
-
OK
-
-
-
-
- -
Update
-
-
- -
-
- - -
OK
-
- -
-
-
OK
-
-
- - - - - - - - \ No newline at end of file diff --git a/src/background.ts b/src/background.ts deleted file mode 100644 index 36963e0b9..000000000 --- a/src/background.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// -/// - -chrome.runtime.onMessage.addListener((request, sender, cb) => { - switch (request.action) { - default: - break; - } - // return true is must, - // so that chrome knows the response is async. - // Or callback value will be undefined - return true; -}); \ No newline at end of file From 1b6ff2e0061240500adb19e378b5eb4a8f8e11f6 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Tue, 6 Feb 2018 17:53:52 +0800 Subject: [PATCH 024/178] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b05fb70f..62a0d58a5 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,6 @@ npm run compile ## FAQ -### gts is found +### gts is not found gts (Google TypeScript style) is installed locally by default, see to add local node modules into path, or run `npm install -g gts` to install gts global. \ No newline at end of file From 1ea8fe460410a298779d27cff932237352c75121 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Tue, 6 Feb 2018 18:03:49 +0800 Subject: [PATCH 025/178] add .gitattributes --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..0a9b14eae --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +*.js linguist-vendored +*.css linguist-vendored +*.html linguist-vendored \ No newline at end of file From fb89963631b727ff3840bd5419dec992b9d3859b Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Wed, 7 Feb 2018 02:10:05 +0800 Subject: [PATCH 026/178] add scan qr --- js/jsqrcode/COPYING | 201 ++++++++++++ js/jsqrcode/alignpat.js | 279 ++++++++++++++++ js/jsqrcode/bitmat.js | 111 +++++++ js/jsqrcode/bmparser.js | 203 ++++++++++++ js/jsqrcode/datablock.js | 117 +++++++ js/jsqrcode/databr.js | 325 +++++++++++++++++++ js/jsqrcode/datamask.js | 207 ++++++++++++ js/jsqrcode/decoder.js | 95 ++++++ js/jsqrcode/detector.js | 413 ++++++++++++++++++++++++ js/jsqrcode/errorlevel.js | 58 ++++ js/jsqrcode/findpat.js | 649 ++++++++++++++++++++++++++++++++++++++ js/jsqrcode/formatinf.js | 104 ++++++ js/jsqrcode/gf256.js | 117 +++++++ js/jsqrcode/gf256poly.js | 230 ++++++++++++++ js/jsqrcode/grid.js | 152 +++++++++ js/jsqrcode/index.d.ts | 500 +++++++++++++++++++++++++++++ js/jsqrcode/qrcode.js | 319 +++++++++++++++++++ js/jsqrcode/rsdecoder.js | 178 +++++++++++ js/jsqrcode/version.js | 261 +++++++++++++++ js/qr.js | 4 + manifest.json | 31 ++ popup.html | 2 +- qr.html | 8 + src/background.ts | 116 +++++++ src/content.ts | 139 ++++++++ src/popup.ts | 38 +-- 26 files changed, 4837 insertions(+), 20 deletions(-) create mode 100644 js/jsqrcode/COPYING create mode 100644 js/jsqrcode/alignpat.js create mode 100644 js/jsqrcode/bitmat.js create mode 100644 js/jsqrcode/bmparser.js create mode 100644 js/jsqrcode/datablock.js create mode 100644 js/jsqrcode/databr.js create mode 100644 js/jsqrcode/datamask.js create mode 100644 js/jsqrcode/decoder.js create mode 100644 js/jsqrcode/detector.js create mode 100644 js/jsqrcode/errorlevel.js create mode 100644 js/jsqrcode/findpat.js create mode 100644 js/jsqrcode/formatinf.js create mode 100644 js/jsqrcode/gf256.js create mode 100644 js/jsqrcode/gf256poly.js create mode 100644 js/jsqrcode/grid.js create mode 100644 js/jsqrcode/index.d.ts create mode 100644 js/jsqrcode/qrcode.js create mode 100644 js/jsqrcode/rsdecoder.js create mode 100644 js/jsqrcode/version.js create mode 100644 js/qr.js create mode 100644 qr.html create mode 100644 src/background.ts diff --git a/js/jsqrcode/COPYING b/js/jsqrcode/COPYING new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/js/jsqrcode/COPYING @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/js/jsqrcode/alignpat.js b/js/jsqrcode/alignpat.js new file mode 100644 index 000000000..967473f39 --- /dev/null +++ b/js/jsqrcode/alignpat.js @@ -0,0 +1,279 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function AlignmentPattern(posX, posY, estimatedModuleSize) +{ + this.x=posX; + this.y=posY; + this.count = 1; + this.estimatedModuleSize = estimatedModuleSize; + + this.__defineGetter__("EstimatedModuleSize", function() + { + return this.estimatedModuleSize; + }); + this.__defineGetter__("Count", function() + { + return this.count; + }); + this.__defineGetter__("X", function() + { + return Math.floor(this.x); + }); + this.__defineGetter__("Y", function() + { + return Math.floor(this.y); + }); + this.incrementCount = function() + { + this.count++; + } + this.aboutEquals=function( moduleSize, i, j) + { + if (Math.abs(i - this.y) <= moduleSize && Math.abs(j - this.x) <= moduleSize) + { + var moduleSizeDiff = Math.abs(moduleSize - this.estimatedModuleSize); + return moduleSizeDiff <= 1.0 || moduleSizeDiff / this.estimatedModuleSize <= 1.0; + } + return false; + } + +} + +function AlignmentPatternFinder( image, startX, startY, width, height, moduleSize, resultPointCallback) +{ + this.image = image; + this.possibleCenters = new Array(); + this.startX = startX; + this.startY = startY; + this.width = width; + this.height = height; + this.moduleSize = moduleSize; + this.crossCheckStateCount = new Array(0,0,0); + this.resultPointCallback = resultPointCallback; + + this.centerFromEnd=function(stateCount, end) + { + return (end - stateCount[2]) - stateCount[1] / 2.0; + } + this.foundPatternCross = function(stateCount) + { + var moduleSize = this.moduleSize; + var maxVariance = moduleSize / 2.0; + for (var i = 0; i < 3; i++) + { + if (Math.abs(moduleSize - stateCount[i]) >= maxVariance) + { + return false; + } + } + return true; + } + + this.crossCheckVertical=function( startI, centerJ, maxCount, originalStateCountTotal) + { + var image = this.image; + + var maxI = qrcode.height; + var stateCount = this.crossCheckStateCount; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + + // Start counting up from center + var i = startI; + while (i >= 0 && image[centerJ + i*qrcode.width] && stateCount[1] <= maxCount) + { + stateCount[1]++; + i--; + } + // If already too many modules in this state or ran off the edge: + if (i < 0 || stateCount[1] > maxCount) + { + return NaN; + } + while (i >= 0 && !image[centerJ + i*qrcode.width] && stateCount[0] <= maxCount) + { + stateCount[0]++; + i--; + } + if (stateCount[0] > maxCount) + { + return NaN; + } + + // Now also count down from center + i = startI + 1; + while (i < maxI && image[centerJ + i*qrcode.width] && stateCount[1] <= maxCount) + { + stateCount[1]++; + i++; + } + if (i == maxI || stateCount[1] > maxCount) + { + return NaN; + } + while (i < maxI && !image[centerJ + i*qrcode.width] && stateCount[2] <= maxCount) + { + stateCount[2]++; + i++; + } + if (stateCount[2] > maxCount) + { + return NaN; + } + + var stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) + { + return NaN; + } + + return this.foundPatternCross(stateCount)?this.centerFromEnd(stateCount, i):NaN; + } + + this.handlePossibleCenter=function( stateCount, i, j) + { + var stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2]; + var centerJ = this.centerFromEnd(stateCount, j); + var centerI = this.crossCheckVertical(i, Math.floor (centerJ), 2 * stateCount[1], stateCountTotal); + if (!isNaN(centerI)) + { + var estimatedModuleSize = (stateCount[0] + stateCount[1] + stateCount[2]) / 3.0; + var max = this.possibleCenters.length; + for (var index = 0; index < max; index++) + { + var center = this.possibleCenters[index]; + // Look for about the same center and module size: + if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) + { + return new AlignmentPattern(centerJ, centerI, estimatedModuleSize); + } + } + // Hadn't found this before; save it + var point = new AlignmentPattern(centerJ, centerI, estimatedModuleSize); + this.possibleCenters.push(point); + if (this.resultPointCallback != null) + { + this.resultPointCallback.foundPossibleResultPoint(point); + } + } + return null; + } + + this.find = function() + { + var startX = this.startX; + var height = this.height; + var maxJ = startX + width; + var middleI = startY + (height >> 1); + // We are looking for black/white/black modules in 1:1:1 ratio; + // this tracks the number of black/white/black modules seen so far + var stateCount = new Array(0,0,0); + for (var iGen = 0; iGen < height; iGen++) + { + // Search from middle outwards + var i = middleI + ((iGen & 0x01) == 0?((iGen + 1) >> 1):- ((iGen + 1) >> 1)); + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + var j = startX; + // Burn off leading white pixels before anything else; if we start in the middle of + // a white run, it doesn't make sense to count its length, since we don't know if the + // white run continued to the left of the start point + while (j < maxJ && !image[j + qrcode.width* i]) + { + j++; + } + var currentState = 0; + while (j < maxJ) + { + if (image[j + i*qrcode.width]) + { + // Black pixel + if (currentState == 1) + { + // Counting black pixels + stateCount[currentState]++; + } + else + { + // Counting white pixels + if (currentState == 2) + { + // A winner? + if (this.foundPatternCross(stateCount)) + { + // Yes + var confirmed = this.handlePossibleCenter(stateCount, i, j); + if (confirmed != null) + { + return confirmed; + } + } + stateCount[0] = stateCount[2]; + stateCount[1] = 1; + stateCount[2] = 0; + currentState = 1; + } + else + { + stateCount[++currentState]++; + } + } + } + else + { + // White pixel + if (currentState == 1) + { + // Counting black pixels + currentState++; + } + stateCount[currentState]++; + } + j++; + } + if (this.foundPatternCross(stateCount)) + { + var confirmed = this.handlePossibleCenter(stateCount, i, maxJ); + if (confirmed != null) + { + return confirmed; + } + } + } + + // Hmm, nothing we saw was observed and confirmed twice. If we had + // any guess at all, return it. + if (!(this.possibleCenters.length == 0)) + { + return this.possibleCenters[0]; + } + + throw "Couldn't find enough alignment patterns"; + } + +} \ No newline at end of file diff --git a/js/jsqrcode/bitmat.js b/js/jsqrcode/bitmat.js new file mode 100644 index 000000000..5b0078429 --- /dev/null +++ b/js/jsqrcode/bitmat.js @@ -0,0 +1,111 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function BitMatrix( width, height) +{ + if(!height) + height=width; + if (width < 1 || height < 1) + { + throw "Both dimensions must be greater than 0"; + } + this.width = width; + this.height = height; + var rowSize = width >> 5; + if ((width & 0x1f) != 0) + { + rowSize++; + } + this.rowSize = rowSize; + this.bits = new Array(rowSize * height); + for(var i=0;i> 5); + return ((URShift(this.bits[offset], (x & 0x1f))) & 1) != 0; + } + this.set_Renamed=function( x, y) + { + var offset = y * this.rowSize + (x >> 5); + this.bits[offset] |= 1 << (x & 0x1f); + } + this.flip=function( x, y) + { + var offset = y * this.rowSize + (x >> 5); + this.bits[offset] ^= 1 << (x & 0x1f); + } + this.clear=function() + { + var max = this.bits.length; + for (var i = 0; i < max; i++) + { + this.bits[i] = 0; + } + } + this.setRegion=function( left, top, width, height) + { + if (top < 0 || left < 0) + { + throw "Left and top must be nonnegative"; + } + if (height < 1 || width < 1) + { + throw "Height and width must be at least 1"; + } + var right = left + width; + var bottom = top + height; + if (bottom > this.height || right > this.width) + { + throw "The region must fit inside the matrix"; + } + for (var y = top; y < bottom; y++) + { + var offset = y * this.rowSize; + for (var x = left; x < right; x++) + { + this.bits[offset + (x >> 5)] |= 1 << (x & 0x1f); + } + } + } +} \ No newline at end of file diff --git a/js/jsqrcode/bmparser.js b/js/jsqrcode/bmparser.js new file mode 100644 index 000000000..51c6e85dd --- /dev/null +++ b/js/jsqrcode/bmparser.js @@ -0,0 +1,203 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function BitMatrixParser(bitMatrix) +{ + var dimension = bitMatrix.Dimension; + if (dimension < 21 || (dimension & 0x03) != 1) + { + throw "Error BitMatrixParser"; + } + this.bitMatrix = bitMatrix; + this.parsedVersion = null; + this.parsedFormatInfo = null; + + this.copyBit=function( i, j, versionBits) + { + return this.bitMatrix.get_Renamed(i, j)?(versionBits << 1) | 0x1:versionBits << 1; + } + + this.readFormatInformation=function() + { + if (this.parsedFormatInfo != null) + { + return this.parsedFormatInfo; + } + + // Read top-left format info bits + var formatInfoBits = 0; + for (var i = 0; i < 6; i++) + { + formatInfoBits = this.copyBit(i, 8, formatInfoBits); + } + // .. and skip a bit in the timing pattern ... + formatInfoBits = this.copyBit(7, 8, formatInfoBits); + formatInfoBits = this.copyBit(8, 8, formatInfoBits); + formatInfoBits = this.copyBit(8, 7, formatInfoBits); + // .. and skip a bit in the timing pattern ... + for (var j = 5; j >= 0; j--) + { + formatInfoBits = this.copyBit(8, j, formatInfoBits); + } + + this.parsedFormatInfo = FormatInformation.decodeFormatInformation(formatInfoBits); + if (this.parsedFormatInfo != null) + { + return this.parsedFormatInfo; + } + + // Hmm, failed. Try the top-right/bottom-left pattern + var dimension = this.bitMatrix.Dimension; + formatInfoBits = 0; + var iMin = dimension - 8; + for (var i = dimension - 1; i >= iMin; i--) + { + formatInfoBits = this.copyBit(i, 8, formatInfoBits); + } + for (var j = dimension - 7; j < dimension; j++) + { + formatInfoBits = this.copyBit(8, j, formatInfoBits); + } + + this.parsedFormatInfo = FormatInformation.decodeFormatInformation(formatInfoBits); + if (this.parsedFormatInfo != null) + { + return this.parsedFormatInfo; + } + throw "Error readFormatInformation"; + } + this.readVersion=function() + { + + if (this.parsedVersion != null) + { + return this.parsedVersion; + } + + var dimension = this.bitMatrix.Dimension; + + var provisionalVersion = (dimension - 17) >> 2; + if (provisionalVersion <= 6) + { + return Version.getVersionForNumber(provisionalVersion); + } + + // Read top-right version info: 3 wide by 6 tall + var versionBits = 0; + var ijMin = dimension - 11; + for (var j = 5; j >= 0; j--) + { + for (var i = dimension - 9; i >= ijMin; i--) + { + versionBits = this.copyBit(i, j, versionBits); + } + } + + this.parsedVersion = Version.decodeVersionInformation(versionBits); + if (this.parsedVersion != null && this.parsedVersion.DimensionForVersion == dimension) + { + return this.parsedVersion; + } + + // Hmm, failed. Try bottom left: 6 wide by 3 tall + versionBits = 0; + for (var i = 5; i >= 0; i--) + { + for (var j = dimension - 9; j >= ijMin; j--) + { + versionBits = this.copyBit(i, j, versionBits); + } + } + + this.parsedVersion = Version.decodeVersionInformation(versionBits); + if (this.parsedVersion != null && this.parsedVersion.DimensionForVersion == dimension) + { + return this.parsedVersion; + } + throw "Error readVersion"; + } + this.readCodewords=function() + { + + var formatInfo = this.readFormatInformation(); + var version = this.readVersion(); + + // Get the data mask for the format used in this QR Code. This will exclude + // some bits from reading as we wind through the bit matrix. + var dataMask = DataMask.forReference( formatInfo.DataMask); + var dimension = this.bitMatrix.Dimension; + dataMask.unmaskBitMatrix(this.bitMatrix, dimension); + + var functionPattern = version.buildFunctionPattern(); + + var readingUp = true; + var result = new Array(version.TotalCodewords); + var resultOffset = 0; + var currentByte = 0; + var bitsRead = 0; + // Read columns in pairs, from right to left + for (var j = dimension - 1; j > 0; j -= 2) + { + if (j == 6) + { + // Skip whole column with vertical alignment pattern; + // saves time and makes the other code proceed more cleanly + j--; + } + // Read alternatingly from bottom to top then top to bottom + for (var count = 0; count < dimension; count++) + { + var i = readingUp?dimension - 1 - count:count; + for (var col = 0; col < 2; col++) + { + // Ignore bits covered by the function pattern + if (!functionPattern.get_Renamed(j - col, i)) + { + // Read a bit + bitsRead++; + currentByte <<= 1; + if (this.bitMatrix.get_Renamed(j - col, i)) + { + currentByte |= 1; + } + // If we've made a whole byte, save it off + if (bitsRead == 8) + { + result[resultOffset++] = currentByte; + bitsRead = 0; + currentByte = 0; + } + } + } + } + readingUp ^= true; // readingUp = !readingUp; // switch directions + } + if (resultOffset != version.TotalCodewords) + { + throw "Error readCodewords"; + } + return result; + } +} \ No newline at end of file diff --git a/js/jsqrcode/datablock.js b/js/jsqrcode/datablock.js new file mode 100644 index 000000000..3cb277a89 --- /dev/null +++ b/js/jsqrcode/datablock.js @@ -0,0 +1,117 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function DataBlock(numDataCodewords, codewords) +{ + this.numDataCodewords = numDataCodewords; + this.codewords = codewords; + + this.__defineGetter__("NumDataCodewords", function() + { + return this.numDataCodewords; + }); + this.__defineGetter__("Codewords", function() + { + return this.codewords; + }); +} + +DataBlock.getDataBlocks=function(rawCodewords, version, ecLevel) +{ + + if (rawCodewords.length != version.TotalCodewords) + { + throw "ArgumentException"; + } + + // Figure out the number and size of data blocks used by this version and + // error correction level + var ecBlocks = version.getECBlocksForLevel(ecLevel); + + // First count the total number of data blocks + var totalBlocks = 0; + var ecBlockArray = ecBlocks.getECBlocks(); + for (var i = 0; i < ecBlockArray.length; i++) + { + totalBlocks += ecBlockArray[i].Count; + } + + // Now establish DataBlocks of the appropriate size and number of data codewords + var result = new Array(totalBlocks); + var numResultBlocks = 0; + for (var j = 0; j < ecBlockArray.length; j++) + { + var ecBlock = ecBlockArray[j]; + for (var i = 0; i < ecBlock.Count; i++) + { + var numDataCodewords = ecBlock.DataCodewords; + var numBlockCodewords = ecBlocks.ECCodewordsPerBlock + numDataCodewords; + result[numResultBlocks++] = new DataBlock(numDataCodewords, new Array(numBlockCodewords)); + } + } + + // All blocks have the same amount of data, except that the last n + // (where n may be 0) have 1 more byte. Figure out where these start. + var shorterBlocksTotalCodewords = result[0].codewords.length; + var longerBlocksStartAt = result.length - 1; + while (longerBlocksStartAt >= 0) + { + var numCodewords = result[longerBlocksStartAt].codewords.length; + if (numCodewords == shorterBlocksTotalCodewords) + { + break; + } + longerBlocksStartAt--; + } + longerBlocksStartAt++; + + var shorterBlocksNumDataCodewords = shorterBlocksTotalCodewords - ecBlocks.ECCodewordsPerBlock; + // The last elements of result may be 1 element longer; + // first fill out as many elements as all of them have + var rawCodewordsOffset = 0; + for (var i = 0; i < shorterBlocksNumDataCodewords; i++) + { + for (var j = 0; j < numResultBlocks; j++) + { + result[j].codewords[i] = rawCodewords[rawCodewordsOffset++]; + } + } + // Fill out the last data block in the longer ones + for (var j = longerBlocksStartAt; j < numResultBlocks; j++) + { + result[j].codewords[shorterBlocksNumDataCodewords] = rawCodewords[rawCodewordsOffset++]; + } + // Now add in error correction blocks + var max = result[0].codewords.length; + for (var i = shorterBlocksNumDataCodewords; i < max; i++) + { + for (var j = 0; j < numResultBlocks; j++) + { + var iOffset = j < longerBlocksStartAt?i:i + 1; + result[j].codewords[iOffset] = rawCodewords[rawCodewordsOffset++]; + } + } + return result; +} diff --git a/js/jsqrcode/databr.js b/js/jsqrcode/databr.js new file mode 100644 index 000000000..66279c41d --- /dev/null +++ b/js/jsqrcode/databr.js @@ -0,0 +1,325 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function QRCodeDataBlockReader(blocks, version, numErrorCorrectionCode) +{ + this.blockPointer = 0; + this.bitPointer = 7; + this.dataLength = 0; + this.blocks = blocks; + this.numErrorCorrectionCode = numErrorCorrectionCode; + if (version <= 9) + this.dataLengthMode = 0; + else if (version >= 10 && version <= 26) + this.dataLengthMode = 1; + else if (version >= 27 && version <= 40) + this.dataLengthMode = 2; + + this.getNextBits = function( numBits) + { + var bits = 0; + if (numBits < this.bitPointer + 1) + { + // next word fits into current data block + var mask = 0; + for (var i = 0; i < numBits; i++) + { + mask += (1 << i); + } + mask <<= (this.bitPointer - numBits + 1); + + bits = (this.blocks[this.blockPointer] & mask) >> (this.bitPointer - numBits + 1); + this.bitPointer -= numBits; + return bits; + } + else if (numBits < this.bitPointer + 1 + 8) + { + // next word crosses 2 data blocks + var mask1 = 0; + for (var i = 0; i < this.bitPointer + 1; i++) + { + mask1 += (1 << i); + } + bits = (this.blocks[this.blockPointer] & mask1) << (numBits - (this.bitPointer + 1)); + this.blockPointer++; + bits += ((this.blocks[this.blockPointer]) >> (8 - (numBits - (this.bitPointer + 1)))); + + this.bitPointer = this.bitPointer - numBits % 8; + if (this.bitPointer < 0) + { + this.bitPointer = 8 + this.bitPointer; + } + return bits; + } + else if (numBits < this.bitPointer + 1 + 16) + { + // next word crosses 3 data blocks + var mask1 = 0; // mask of first block + var mask3 = 0; // mask of 3rd block + //bitPointer + 1 : number of bits of the 1st block + //8 : number of the 2nd block (note that use already 8bits because next word uses 3 data blocks) + //numBits - (bitPointer + 1 + 8) : number of bits of the 3rd block + for (var i = 0; i < this.bitPointer + 1; i++) + { + mask1 += (1 << i); + } + var bitsFirstBlock = (this.blocks[this.blockPointer] & mask1) << (numBits - (this.bitPointer + 1)); + this.blockPointer++; + + var bitsSecondBlock = this.blocks[this.blockPointer] << (numBits - (this.bitPointer + 1 + 8)); + this.blockPointer++; + + for (var i = 0; i < numBits - (this.bitPointer + 1 + 8); i++) + { + mask3 += (1 << i); + } + mask3 <<= 8 - (numBits - (this.bitPointer + 1 + 8)); + var bitsThirdBlock = (this.blocks[this.blockPointer] & mask3) >> (8 - (numBits - (this.bitPointer + 1 + 8))); + + bits = bitsFirstBlock + bitsSecondBlock + bitsThirdBlock; + this.bitPointer = this.bitPointer - (numBits - 8) % 8; + if (this.bitPointer < 0) + { + this.bitPointer = 8 + this.bitPointer; + } + return bits; + } + else + { + return 0; + } + } + this.NextMode=function() + { + if ((this.blockPointer > this.blocks.length - this.numErrorCorrectionCode - 2)) + return 0; + else + return this.getNextBits(4); + } + this.getDataLength=function( modeIndicator) + { + var index = 0; + while (true) + { + if ((modeIndicator >> index) == 1) + break; + index++; + } + + return this.getNextBits(qrcode.sizeOfDataLengthInfo[this.dataLengthMode][index]); + } + this.getRomanAndFigureString=function( dataLength) + { + var length = dataLength; + var intData = 0; + var strData = ""; + var tableRomanAndFigure = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ' ', '$', '%', '*', '+', '-', '.', '/', ':'); + do + { + if (length > 1) + { + intData = this.getNextBits(11); + var firstLetter = Math.floor(intData / 45); + var secondLetter = intData % 45; + strData += tableRomanAndFigure[firstLetter]; + strData += tableRomanAndFigure[secondLetter]; + length -= 2; + } + else if (length == 1) + { + intData = this.getNextBits(6); + strData += tableRomanAndFigure[intData]; + length -= 1; + } + } + while (length > 0); + + return strData; + } + this.getFigureString=function( dataLength) + { + var length = dataLength; + var intData = 0; + var strData = ""; + do + { + if (length >= 3) + { + intData = this.getNextBits(10); + if (intData < 100) + strData += "0"; + if (intData < 10) + strData += "0"; + length -= 3; + } + else if (length == 2) + { + intData = this.getNextBits(7); + if (intData < 10) + strData += "0"; + length -= 2; + } + else if (length == 1) + { + intData = this.getNextBits(4); + length -= 1; + } + strData += intData; + } + while (length > 0); + + return strData; + } + this.get8bitByteArray=function( dataLength) + { + var length = dataLength; + var intData = 0; + var output = new Array(); + + do + { + intData = this.getNextBits(8); + output.push( intData); + length--; + } + while (length > 0); + return output; + } + this.getKanjiString=function( dataLength) + { + var length = dataLength; + var intData = 0; + var unicodeString = ""; + do + { + intData = getNextBits(13); + var lowerByte = intData % 0xC0; + var higherByte = intData / 0xC0; + + var tempWord = (higherByte << 8) + lowerByte; + var shiftjisWord = 0; + if (tempWord + 0x8140 <= 0x9FFC) + { + // between 8140 - 9FFC on Shift_JIS character set + shiftjisWord = tempWord + 0x8140; + } + else + { + // between E040 - EBBF on Shift_JIS character set + shiftjisWord = tempWord + 0xC140; + } + + //var tempByte = new Array(0,0); + //tempByte[0] = (sbyte) (shiftjisWord >> 8); + //tempByte[1] = (sbyte) (shiftjisWord & 0xFF); + //unicodeString += new String(SystemUtils.ToCharArray(SystemUtils.ToByteArray(tempByte))); + unicodeString += String.fromCharCode(shiftjisWord); + length--; + } + while (length > 0); + + + return unicodeString; + } + + this.__defineGetter__("DataByte", function() + { + var output = new Array(); + var MODE_NUMBER = 1; + var MODE_ROMAN_AND_NUMBER = 2; + var MODE_8BIT_BYTE = 4; + var MODE_KANJI = 8; + do + { + var mode = this.NextMode(); + //canvas.println("mode: " + mode); + if (mode == 0) + { + if (output.length > 0) + break; + else + throw "Empty data block"; + } + //if (mode != 1 && mode != 2 && mode != 4 && mode != 8) + // break; + //} + if (mode != MODE_NUMBER && mode != MODE_ROMAN_AND_NUMBER && mode != MODE_8BIT_BYTE && mode != MODE_KANJI) + { + /* canvas.println("Invalid mode: " + mode); + mode = guessMode(mode); + canvas.println("Guessed mode: " + mode); */ + throw "Invalid mode: " + mode + " in (block:" + this.blockPointer + " bit:" + this.bitPointer + ")"; + } + dataLength = this.getDataLength(mode); + if (dataLength < 1) + throw "Invalid data length: " + dataLength; + //canvas.println("length: " + dataLength); + switch (mode) + { + + case MODE_NUMBER: + //canvas.println("Mode: Figure"); + var temp_str = this.getFigureString(dataLength); + var ta = new Array(temp_str.length); + for(var j=0;j 7) + { + throw "System.ArgumentException"; + } + return DataMask.DATA_MASKS[reference]; +} + +function DataMask000() +{ + this.unmaskBitMatrix=function(bits, dimension) + { + for (var i = 0; i < dimension; i++) + { + for (var j = 0; j < dimension; j++) + { + if (this.isMasked(i, j)) + { + bits.flip(j, i); + } + } + } + } + this.isMasked=function( i, j) + { + return ((i + j) & 0x01) == 0; + } +} + +function DataMask001() +{ + this.unmaskBitMatrix=function(bits, dimension) + { + for (var i = 0; i < dimension; i++) + { + for (var j = 0; j < dimension; j++) + { + if (this.isMasked(i, j)) + { + bits.flip(j, i); + } + } + } + } + this.isMasked=function( i, j) + { + return (i & 0x01) == 0; + } +} + +function DataMask010() +{ + this.unmaskBitMatrix=function(bits, dimension) + { + for (var i = 0; i < dimension; i++) + { + for (var j = 0; j < dimension; j++) + { + if (this.isMasked(i, j)) + { + bits.flip(j, i); + } + } + } + } + this.isMasked=function( i, j) + { + return j % 3 == 0; + } +} + +function DataMask011() +{ + this.unmaskBitMatrix=function(bits, dimension) + { + for (var i = 0; i < dimension; i++) + { + for (var j = 0; j < dimension; j++) + { + if (this.isMasked(i, j)) + { + bits.flip(j, i); + } + } + } + } + this.isMasked=function( i, j) + { + return (i + j) % 3 == 0; + } +} + +function DataMask100() +{ + this.unmaskBitMatrix=function(bits, dimension) + { + for (var i = 0; i < dimension; i++) + { + for (var j = 0; j < dimension; j++) + { + if (this.isMasked(i, j)) + { + bits.flip(j, i); + } + } + } + } + this.isMasked=function( i, j) + { + return (((URShift(i, 1)) + (j / 3)) & 0x01) == 0; + } +} + +function DataMask101() +{ + this.unmaskBitMatrix=function(bits, dimension) + { + for (var i = 0; i < dimension; i++) + { + for (var j = 0; j < dimension; j++) + { + if (this.isMasked(i, j)) + { + bits.flip(j, i); + } + } + } + } + this.isMasked=function( i, j) + { + var temp = i * j; + return (temp & 0x01) + (temp % 3) == 0; + } +} + +function DataMask110() +{ + this.unmaskBitMatrix=function(bits, dimension) + { + for (var i = 0; i < dimension; i++) + { + for (var j = 0; j < dimension; j++) + { + if (this.isMasked(i, j)) + { + bits.flip(j, i); + } + } + } + } + this.isMasked=function( i, j) + { + var temp = i * j; + return (((temp & 0x01) + (temp % 3)) & 0x01) == 0; + } +} +function DataMask111() +{ + this.unmaskBitMatrix=function(bits, dimension) + { + for (var i = 0; i < dimension; i++) + { + for (var j = 0; j < dimension; j++) + { + if (this.isMasked(i, j)) + { + bits.flip(j, i); + } + } + } + } + this.isMasked=function( i, j) + { + return ((((i + j) & 0x01) + ((i * j) % 3)) & 0x01) == 0; + } +} + +DataMask.DATA_MASKS = new Array(new DataMask000(), new DataMask001(), new DataMask010(), new DataMask011(), new DataMask100(), new DataMask101(), new DataMask110(), new DataMask111()); + diff --git a/js/jsqrcode/decoder.js b/js/jsqrcode/decoder.js new file mode 100644 index 000000000..58467c3d5 --- /dev/null +++ b/js/jsqrcode/decoder.js @@ -0,0 +1,95 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +Decoder={}; +Decoder.rsDecoder = new ReedSolomonDecoder(GF256.QR_CODE_FIELD); + +Decoder.correctErrors=function( codewordBytes, numDataCodewords) +{ + var numCodewords = codewordBytes.length; + // First read into an array of ints + var codewordsInts = new Array(numCodewords); + for (var i = 0; i < numCodewords; i++) + { + codewordsInts[i] = codewordBytes[i] & 0xFF; + } + var numECCodewords = codewordBytes.length - numDataCodewords; + try + { + Decoder.rsDecoder.decode(codewordsInts, numECCodewords); + //var corrector = new ReedSolomon(codewordsInts, numECCodewords); + //corrector.correct(); + } + catch ( rse) + { + throw rse; + } + // Copy back into array of bytes -- only need to worry about the bytes that were data + // We don't care about errors in the error-correction codewords + for (var i = 0; i < numDataCodewords; i++) + { + codewordBytes[i] = codewordsInts[i]; + } +} + +Decoder.decode=function(bits) +{ + var parser = new BitMatrixParser(bits); + var version = parser.readVersion(); + var ecLevel = parser.readFormatInformation().ErrorCorrectionLevel; + + // Read codewords + var codewords = parser.readCodewords(); + + // Separate into data blocks + var dataBlocks = DataBlock.getDataBlocks(codewords, version, ecLevel); + + // Count total number of data bytes + var totalBytes = 0; + for (var i = 0; i < dataBlocks.length; i++) + { + totalBytes += dataBlocks[i].NumDataCodewords; + } + var resultBytes = new Array(totalBytes); + var resultOffset = 0; + + // Error-correct and copy data blocks together into a stream of bytes + for (var j = 0; j < dataBlocks.length; j++) + { + var dataBlock = dataBlocks[j]; + var codewordBytes = dataBlock.Codewords; + var numDataCodewords = dataBlock.NumDataCodewords; + Decoder.correctErrors(codewordBytes, numDataCodewords); + for (var i = 0; i < numDataCodewords; i++) + { + resultBytes[resultOffset++] = codewordBytes[i]; + } + } + + // Decode the contents of that stream of bytes + var reader = new QRCodeDataBlockReader(resultBytes, version.VersionNumber, ecLevel.Bits); + return reader; + //return DecodedBitStreamParser.decode(resultBytes, version, ecLevel); +} diff --git a/js/jsqrcode/detector.js b/js/jsqrcode/detector.js new file mode 100644 index 000000000..012f8c5d1 --- /dev/null +++ b/js/jsqrcode/detector.js @@ -0,0 +1,413 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function PerspectiveTransform( a11, a21, a31, a12, a22, a32, a13, a23, a33) +{ + this.a11 = a11; + this.a12 = a12; + this.a13 = a13; + this.a21 = a21; + this.a22 = a22; + this.a23 = a23; + this.a31 = a31; + this.a32 = a32; + this.a33 = a33; + this.transformPoints1=function( points) + { + var max = points.length; + var a11 = this.a11; + var a12 = this.a12; + var a13 = this.a13; + var a21 = this.a21; + var a22 = this.a22; + var a23 = this.a23; + var a31 = this.a31; + var a32 = this.a32; + var a33 = this.a33; + for (var i = 0; i < max; i += 2) + { + var x = points[i]; + var y = points[i + 1]; + var denominator = a13 * x + a23 * y + a33; + points[i] = (a11 * x + a21 * y + a31) / denominator; + points[i + 1] = (a12 * x + a22 * y + a32) / denominator; + } + } + this. transformPoints2=function(xValues, yValues) + { + var n = xValues.length; + for (var i = 0; i < n; i++) + { + var x = xValues[i]; + var y = yValues[i]; + var denominator = this.a13 * x + this.a23 * y + this.a33; + xValues[i] = (this.a11 * x + this.a21 * y + this.a31) / denominator; + yValues[i] = (this.a12 * x + this.a22 * y + this.a32) / denominator; + } + } + + this.buildAdjoint=function() + { + // Adjoint is the transpose of the cofactor matrix: + return new PerspectiveTransform(this.a22 * this.a33 - this.a23 * this.a32, this.a23 * this.a31 - this.a21 * this.a33, this.a21 * this.a32 - this.a22 * this.a31, this.a13 * this.a32 - this.a12 * this.a33, this.a11 * this.a33 - this.a13 * this.a31, this.a12 * this.a31 - this.a11 * this.a32, this.a12 * this.a23 - this.a13 * this.a22, this.a13 * this.a21 - this.a11 * this.a23, this.a11 * this.a22 - this.a12 * this.a21); + } + this.times=function( other) + { + return new PerspectiveTransform(this.a11 * other.a11 + this.a21 * other.a12 + this.a31 * other.a13, this.a11 * other.a21 + this.a21 * other.a22 + this.a31 * other.a23, this.a11 * other.a31 + this.a21 * other.a32 + this.a31 * other.a33, this.a12 * other.a11 + this.a22 * other.a12 + this.a32 * other.a13, this.a12 * other.a21 + this.a22 * other.a22 + this.a32 * other.a23, this.a12 * other.a31 + this.a22 * other.a32 + this.a32 * other.a33, this.a13 * other.a11 + this.a23 * other.a12 +this.a33 * other.a13, this.a13 * other.a21 + this.a23 * other.a22 + this.a33 * other.a23, this.a13 * other.a31 + this.a23 * other.a32 + this.a33 * other.a33); + } + +} + +PerspectiveTransform.quadrilateralToQuadrilateral=function( x0, y0, x1, y1, x2, y2, x3, y3, x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p) +{ + + var qToS = this.quadrilateralToSquare(x0, y0, x1, y1, x2, y2, x3, y3); + var sToQ = this.squareToQuadrilateral(x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p); + return sToQ.times(qToS); +} + +PerspectiveTransform.squareToQuadrilateral=function( x0, y0, x1, y1, x2, y2, x3, y3) +{ + dy2 = y3 - y2; + dy3 = y0 - y1 + y2 - y3; + if (dy2 == 0.0 && dy3 == 0.0) + { + return new PerspectiveTransform(x1 - x0, x2 - x1, x0, y1 - y0, y2 - y1, y0, 0.0, 0.0, 1.0); + } + else + { + dx1 = x1 - x2; + dx2 = x3 - x2; + dx3 = x0 - x1 + x2 - x3; + dy1 = y1 - y2; + denominator = dx1 * dy2 - dx2 * dy1; + a13 = (dx3 * dy2 - dx2 * dy3) / denominator; + a23 = (dx1 * dy3 - dx3 * dy1) / denominator; + return new PerspectiveTransform(x1 - x0 + a13 * x1, x3 - x0 + a23 * x3, x0, y1 - y0 + a13 * y1, y3 - y0 + a23 * y3, y0, a13, a23, 1.0); + } +} + +PerspectiveTransform.quadrilateralToSquare=function( x0, y0, x1, y1, x2, y2, x3, y3) +{ + // Here, the adjoint serves as the inverse: + return this.squareToQuadrilateral(x0, y0, x1, y1, x2, y2, x3, y3).buildAdjoint(); +} + +function DetectorResult(bits, points) +{ + this.bits = bits; + this.points = points; +} + + +function Detector(image) +{ + this.image=image; + this.resultPointCallback = null; + + this.sizeOfBlackWhiteBlackRun=function( fromX, fromY, toX, toY) + { + // Mild variant of Bresenham's algorithm; + // see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm + var steep = Math.abs(toY - fromY) > Math.abs(toX - fromX); + if (steep) + { + var temp = fromX; + fromX = fromY; + fromY = temp; + temp = toX; + toX = toY; + toY = temp; + } + + var dx = Math.abs(toX - fromX); + var dy = Math.abs(toY - fromY); + var error = - dx >> 1; + var ystep = fromY < toY?1:- 1; + var xstep = fromX < toX?1:- 1; + var state = 0; // In black pixels, looking for white, first or second time + for (var x = fromX, y = fromY; x != toX; x += xstep) + { + + var realX = steep?y:x; + var realY = steep?x:y; + if (state == 1) + { + // In white pixels, looking for black + if (this.image[realX + realY*qrcode.width]) + { + state++; + } + } + else + { + if (!this.image[realX + realY*qrcode.width]) + { + state++; + } + } + + if (state == 3) + { + // Found black, white, black, and stumbled back onto white; done + var diffX = x - fromX; + var diffY = y - fromY; + return Math.sqrt( (diffX * diffX + diffY * diffY)); + } + error += dy; + if (error > 0) + { + if (y == toY) + { + break; + } + y += ystep; + error -= dx; + } + } + var diffX2 = toX - fromX; + var diffY2 = toY - fromY; + return Math.sqrt( (diffX2 * diffX2 + diffY2 * diffY2)); + } + + + this.sizeOfBlackWhiteBlackRunBothWays=function( fromX, fromY, toX, toY) + { + + var result = this.sizeOfBlackWhiteBlackRun(fromX, fromY, toX, toY); + + // Now count other way -- don't run off image though of course + var scale = 1.0; + var otherToX = fromX - (toX - fromX); + if (otherToX < 0) + { + scale = fromX / (fromX - otherToX); + otherToX = 0; + } + else if (otherToX >= qrcode.width) + { + scale = (qrcode.width - 1 - fromX) / (otherToX - fromX); + otherToX = qrcode.width - 1; + } + var otherToY = Math.floor (fromY - (toY - fromY) * scale); + + scale = 1.0; + if (otherToY < 0) + { + scale = fromY / (fromY - otherToY); + otherToY = 0; + } + else if (otherToY >= qrcode.height) + { + scale = (qrcode.height - 1 - fromY) / (otherToY - fromY); + otherToY = qrcode.height - 1; + } + otherToX = Math.floor (fromX + (otherToX - fromX) * scale); + + result += this.sizeOfBlackWhiteBlackRun(fromX, fromY, otherToX, otherToY); + return result - 1.0; // -1 because we counted the middle pixel twice + } + + + + this.calculateModuleSizeOneWay=function( pattern, otherPattern) + { + var moduleSizeEst1 = this.sizeOfBlackWhiteBlackRunBothWays(Math.floor( pattern.X), Math.floor( pattern.Y), Math.floor( otherPattern.X), Math.floor(otherPattern.Y)); + var moduleSizeEst2 = this.sizeOfBlackWhiteBlackRunBothWays(Math.floor(otherPattern.X), Math.floor(otherPattern.Y), Math.floor( pattern.X), Math.floor(pattern.Y)); + if (isNaN(moduleSizeEst1)) + { + return moduleSizeEst2 / 7.0; + } + if (isNaN(moduleSizeEst2)) + { + return moduleSizeEst1 / 7.0; + } + // Average them, and divide by 7 since we've counted the width of 3 black modules, + // and 1 white and 1 black module on either side. Ergo, divide sum by 14. + return (moduleSizeEst1 + moduleSizeEst2) / 14.0; + } + + + this.calculateModuleSize=function( topLeft, topRight, bottomLeft) + { + // Take the average + return (this.calculateModuleSizeOneWay(topLeft, topRight) + this.calculateModuleSizeOneWay(topLeft, bottomLeft)) / 2.0; + } + + this.distance=function( pattern1, pattern2) + { + xDiff = pattern1.X - pattern2.X; + yDiff = pattern1.Y - pattern2.Y; + return Math.sqrt( (xDiff * xDiff + yDiff * yDiff)); + } + this.computeDimension=function( topLeft, topRight, bottomLeft, moduleSize) + { + + var tltrCentersDimension = Math.round(this.distance(topLeft, topRight) / moduleSize); + var tlblCentersDimension = Math.round(this.distance(topLeft, bottomLeft) / moduleSize); + var dimension = ((tltrCentersDimension + tlblCentersDimension) >> 1) + 7; + switch (dimension & 0x03) + { + + // mod 4 + case 0: + dimension++; + break; + // 1? do nothing + + case 2: + dimension--; + break; + + case 3: + throw "Error"; + } + return dimension; + } + + this.findAlignmentInRegion=function( overallEstModuleSize, estAlignmentX, estAlignmentY, allowanceFactor) + { + // Look for an alignment pattern (3 modules in size) around where it + // should be + var allowance = Math.floor (allowanceFactor * overallEstModuleSize); + var alignmentAreaLeftX = Math.max(0, estAlignmentX - allowance); + var alignmentAreaRightX = Math.min(qrcode.width - 1, estAlignmentX + allowance); + if (alignmentAreaRightX - alignmentAreaLeftX < overallEstModuleSize * 3) + { + throw "Error"; + } + + var alignmentAreaTopY = Math.max(0, estAlignmentY - allowance); + var alignmentAreaBottomY = Math.min(qrcode.height - 1, estAlignmentY + allowance); + + var alignmentFinder = new AlignmentPatternFinder(this.image, alignmentAreaLeftX, alignmentAreaTopY, alignmentAreaRightX - alignmentAreaLeftX, alignmentAreaBottomY - alignmentAreaTopY, overallEstModuleSize, this.resultPointCallback); + return alignmentFinder.find(); + } + + this.createTransform=function( topLeft, topRight, bottomLeft, alignmentPattern, dimension) + { + var dimMinusThree = dimension - 3.5; + var bottomRightX; + var bottomRightY; + var sourceBottomRightX; + var sourceBottomRightY; + if (alignmentPattern != null) + { + bottomRightX = alignmentPattern.X; + bottomRightY = alignmentPattern.Y; + sourceBottomRightX = sourceBottomRightY = dimMinusThree - 3.0; + } + else + { + // Don't have an alignment pattern, just make up the bottom-right point + bottomRightX = (topRight.X - topLeft.X) + bottomLeft.X; + bottomRightY = (topRight.Y - topLeft.Y) + bottomLeft.Y; + sourceBottomRightX = sourceBottomRightY = dimMinusThree; + } + + var transform = PerspectiveTransform.quadrilateralToQuadrilateral(3.5, 3.5, dimMinusThree, 3.5, sourceBottomRightX, sourceBottomRightY, 3.5, dimMinusThree, topLeft.X, topLeft.Y, topRight.X, topRight.Y, bottomRightX, bottomRightY, bottomLeft.X, bottomLeft.Y); + + return transform; + } + + this.sampleGrid=function( image, transform, dimension) + { + + var sampler = GridSampler; + return sampler.sampleGrid3(image, dimension, transform); + } + + this.processFinderPatternInfo = function( info) + { + + var topLeft = info.TopLeft; + var topRight = info.TopRight; + var bottomLeft = info.BottomLeft; + + var moduleSize = this.calculateModuleSize(topLeft, topRight, bottomLeft); + if (moduleSize < 1.0) + { + throw "Error"; + } + var dimension = this.computeDimension(topLeft, topRight, bottomLeft, moduleSize); + var provisionalVersion = Version.getProvisionalVersionForDimension(dimension); + var modulesBetweenFPCenters = provisionalVersion.DimensionForVersion - 7; + + var alignmentPattern = null; + // Anything above version 1 has an alignment pattern + if (provisionalVersion.AlignmentPatternCenters.length > 0) + { + + // Guess where a "bottom right" finder pattern would have been + var bottomRightX = topRight.X - topLeft.X + bottomLeft.X; + var bottomRightY = topRight.Y - topLeft.Y + bottomLeft.Y; + + // Estimate that alignment pattern is closer by 3 modules + // from "bottom right" to known top left location + var correctionToTopLeft = 1.0 - 3.0 / modulesBetweenFPCenters; + var estAlignmentX = Math.floor (topLeft.X + correctionToTopLeft * (bottomRightX - topLeft.X)); + var estAlignmentY = Math.floor (topLeft.Y + correctionToTopLeft * (bottomRightY - topLeft.Y)); + + // Kind of arbitrary -- expand search radius before giving up + for (var i = 4; i <= 16; i <<= 1) + { + //try + //{ + alignmentPattern = this.findAlignmentInRegion(moduleSize, estAlignmentX, estAlignmentY, i); + break; + //} + //catch (re) + //{ + // try next round + //} + } + // If we didn't find alignment pattern... well try anyway without it + } + + var transform = this.createTransform(topLeft, topRight, bottomLeft, alignmentPattern, dimension); + + var bits = this.sampleGrid(this.image, transform, dimension); + + var points; + if (alignmentPattern == null) + { + points = new Array(bottomLeft, topLeft, topRight); + } + else + { + points = new Array(bottomLeft, topLeft, topRight, alignmentPattern); + } + return new DetectorResult(bits, points); + } + + + + this.detect=function() + { + var info = new FinderPatternFinder().findFinderPattern(this.image); + + return this.processFinderPatternInfo(info); + } +} \ No newline at end of file diff --git a/js/jsqrcode/errorlevel.js b/js/jsqrcode/errorlevel.js new file mode 100644 index 000000000..222398ecd --- /dev/null +++ b/js/jsqrcode/errorlevel.js @@ -0,0 +1,58 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function ErrorCorrectionLevel(ordinal, bits, name) +{ + this.ordinal_Renamed_Field = ordinal; + this.bits = bits; + this.name = name; + this.__defineGetter__("Bits", function() + { + return this.bits; + }); + this.__defineGetter__("Name", function() + { + return this.name; + }); + this.ordinal=function() + { + return this.ordinal_Renamed_Field; + } +} + +ErrorCorrectionLevel.forBits=function( bits) +{ + if (bits < 0 || bits >= FOR_BITS.length) + { + throw "ArgumentException"; + } + return FOR_BITS[bits]; +} + +var L = new ErrorCorrectionLevel(0, 0x01, "L"); +var M = new ErrorCorrectionLevel(1, 0x00, "M"); +var Q = new ErrorCorrectionLevel(2, 0x03, "Q"); +var H = new ErrorCorrectionLevel(3, 0x02, "H"); +var FOR_BITS = new Array( M, L, H, Q); diff --git a/js/jsqrcode/findpat.js b/js/jsqrcode/findpat.js new file mode 100644 index 000000000..a2e4b8363 --- /dev/null +++ b/js/jsqrcode/findpat.js @@ -0,0 +1,649 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +var MIN_SKIP = 3; +var MAX_MODULES = 57; +var INTEGER_MATH_SHIFT = 8; +var CENTER_QUORUM = 2; + +qrcode.orderBestPatterns=function(patterns) + { + + function distance( pattern1, pattern2) + { + xDiff = pattern1.X - pattern2.X; + yDiff = pattern1.Y - pattern2.Y; + return Math.sqrt( (xDiff * xDiff + yDiff * yDiff)); + } + + /// Returns the z component of the cross product between vectors BC and BA. + function crossProductZ( pointA, pointB, pointC) + { + var bX = pointB.x; + var bY = pointB.y; + return ((pointC.x - bX) * (pointA.y - bY)) - ((pointC.y - bY) * (pointA.x - bX)); + } + + + // Find distances between pattern centers + var zeroOneDistance = distance(patterns[0], patterns[1]); + var oneTwoDistance = distance(patterns[1], patterns[2]); + var zeroTwoDistance = distance(patterns[0], patterns[2]); + + var pointA, pointB, pointC; + // Assume one closest to other two is B; A and C will just be guesses at first + if (oneTwoDistance >= zeroOneDistance && oneTwoDistance >= zeroTwoDistance) + { + pointB = patterns[0]; + pointA = patterns[1]; + pointC = patterns[2]; + } + else if (zeroTwoDistance >= oneTwoDistance && zeroTwoDistance >= zeroOneDistance) + { + pointB = patterns[1]; + pointA = patterns[0]; + pointC = patterns[2]; + } + else + { + pointB = patterns[2]; + pointA = patterns[0]; + pointC = patterns[1]; + } + + // Use cross product to figure out whether A and C are correct or flipped. + // This asks whether BC x BA has a positive z component, which is the arrangement + // we want for A, B, C. If it's negative, then we've got it flipped around and + // should swap A and C. + if (crossProductZ(pointA, pointB, pointC) < 0.0) + { + var temp = pointA; + pointA = pointC; + pointC = temp; + } + + patterns[0] = pointA; + patterns[1] = pointB; + patterns[2] = pointC; + } + + +function FinderPattern(posX, posY, estimatedModuleSize) +{ + this.x=posX; + this.y=posY; + this.count = 1; + this.estimatedModuleSize = estimatedModuleSize; + + this.__defineGetter__("EstimatedModuleSize", function() + { + return this.estimatedModuleSize; + }); + this.__defineGetter__("Count", function() + { + return this.count; + }); + this.__defineGetter__("X", function() + { + return this.x; + }); + this.__defineGetter__("Y", function() + { + return this.y; + }); + this.incrementCount = function() + { + this.count++; + } + this.aboutEquals=function( moduleSize, i, j) + { + if (Math.abs(i - this.y) <= moduleSize && Math.abs(j - this.x) <= moduleSize) + { + var moduleSizeDiff = Math.abs(moduleSize - this.estimatedModuleSize); + return moduleSizeDiff <= 1.0 || moduleSizeDiff / this.estimatedModuleSize <= 1.0; + } + return false; + } + +} + +function FinderPatternInfo(patternCenters) +{ + this.bottomLeft = patternCenters[0]; + this.topLeft = patternCenters[1]; + this.topRight = patternCenters[2]; + this.__defineGetter__("BottomLeft", function() + { + return this.bottomLeft; + }); + this.__defineGetter__("TopLeft", function() + { + return this.topLeft; + }); + this.__defineGetter__("TopRight", function() + { + return this.topRight; + }); +} + +function FinderPatternFinder() +{ + this.image=null; + this.possibleCenters = []; + this.hasSkipped = false; + this.crossCheckStateCount = new Array(0,0,0,0,0); + this.resultPointCallback = null; + + this.__defineGetter__("CrossCheckStateCount", function() + { + this.crossCheckStateCount[0] = 0; + this.crossCheckStateCount[1] = 0; + this.crossCheckStateCount[2] = 0; + this.crossCheckStateCount[3] = 0; + this.crossCheckStateCount[4] = 0; + return this.crossCheckStateCount; + }); + + this.foundPatternCross=function( stateCount) + { + var totalModuleSize = 0; + for (var i = 0; i < 5; i++) + { + var count = stateCount[i]; + if (count == 0) + { + return false; + } + totalModuleSize += count; + } + if (totalModuleSize < 7) + { + return false; + } + var moduleSize = Math.floor((totalModuleSize << INTEGER_MATH_SHIFT) / 7); + var maxVariance = Math.floor(moduleSize / 2); + // Allow less than 50% variance from 1-1-3-1-1 proportions + return Math.abs(moduleSize - (stateCount[0] << INTEGER_MATH_SHIFT)) < maxVariance && Math.abs(moduleSize - (stateCount[1] << INTEGER_MATH_SHIFT)) < maxVariance && Math.abs(3 * moduleSize - (stateCount[2] << INTEGER_MATH_SHIFT)) < 3 * maxVariance && Math.abs(moduleSize - (stateCount[3] << INTEGER_MATH_SHIFT)) < maxVariance && Math.abs(moduleSize - (stateCount[4] << INTEGER_MATH_SHIFT)) < maxVariance; + } + this.centerFromEnd=function( stateCount, end) + { + return (end - stateCount[4] - stateCount[3]) - stateCount[2] / 2.0; + } + this.crossCheckVertical=function( startI, centerJ, maxCount, originalStateCountTotal) + { + var image = this.image; + + var maxI = qrcode.height; + var stateCount = this.CrossCheckStateCount; + + // Start counting up from center + var i = startI; + while (i >= 0 && image[centerJ + i*qrcode.width]) + { + stateCount[2]++; + i--; + } + if (i < 0) + { + return NaN; + } + while (i >= 0 && !image[centerJ +i*qrcode.width] && stateCount[1] <= maxCount) + { + stateCount[1]++; + i--; + } + // If already too many modules in this state or ran off the edge: + if (i < 0 || stateCount[1] > maxCount) + { + return NaN; + } + while (i >= 0 && image[centerJ + i*qrcode.width] && stateCount[0] <= maxCount) + { + stateCount[0]++; + i--; + } + if (stateCount[0] > maxCount) + { + return NaN; + } + + // Now also count down from center + i = startI + 1; + while (i < maxI && image[centerJ +i*qrcode.width]) + { + stateCount[2]++; + i++; + } + if (i == maxI) + { + return NaN; + } + while (i < maxI && !image[centerJ + i*qrcode.width] && stateCount[3] < maxCount) + { + stateCount[3]++; + i++; + } + if (i == maxI || stateCount[3] >= maxCount) + { + return NaN; + } + while (i < maxI && image[centerJ + i*qrcode.width] && stateCount[4] < maxCount) + { + stateCount[4]++; + i++; + } + if (stateCount[4] >= maxCount) + { + return NaN; + } + + // If we found a finder-pattern-like section, but its size is more than 40% different than + // the original, assume it's a false positive + var stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) + { + return NaN; + } + + return this.foundPatternCross(stateCount)?this.centerFromEnd(stateCount, i):NaN; + } + this.crossCheckHorizontal=function( startJ, centerI, maxCount, originalStateCountTotal) + { + var image = this.image; + + var maxJ = qrcode.width; + var stateCount = this.CrossCheckStateCount; + + var j = startJ; + while (j >= 0 && image[j+ centerI*qrcode.width]) + { + stateCount[2]++; + j--; + } + if (j < 0) + { + return NaN; + } + while (j >= 0 && !image[j+ centerI*qrcode.width] && stateCount[1] <= maxCount) + { + stateCount[1]++; + j--; + } + if (j < 0 || stateCount[1] > maxCount) + { + return NaN; + } + while (j >= 0 && image[j+ centerI*qrcode.width] && stateCount[0] <= maxCount) + { + stateCount[0]++; + j--; + } + if (stateCount[0] > maxCount) + { + return NaN; + } + + j = startJ + 1; + while (j < maxJ && image[j+ centerI*qrcode.width]) + { + stateCount[2]++; + j++; + } + if (j == maxJ) + { + return NaN; + } + while (j < maxJ && !image[j+ centerI*qrcode.width] && stateCount[3] < maxCount) + { + stateCount[3]++; + j++; + } + if (j == maxJ || stateCount[3] >= maxCount) + { + return NaN; + } + while (j < maxJ && image[j+ centerI*qrcode.width] && stateCount[4] < maxCount) + { + stateCount[4]++; + j++; + } + if (stateCount[4] >= maxCount) + { + return NaN; + } + + // If we found a finder-pattern-like section, but its size is significantly different than + // the original, assume it's a false positive + var stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= originalStateCountTotal) + { + return NaN; + } + + return this.foundPatternCross(stateCount)?this.centerFromEnd(stateCount, j):NaN; + } + this.handlePossibleCenter=function( stateCount, i, j) + { + var stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4]; + var centerJ = this.centerFromEnd(stateCount, j); //float + var centerI = this.crossCheckVertical(i, Math.floor( centerJ), stateCount[2], stateCountTotal); //float + if (!isNaN(centerI)) + { + // Re-cross check + centerJ = this.crossCheckHorizontal(Math.floor( centerJ), Math.floor( centerI), stateCount[2], stateCountTotal); + if (!isNaN(centerJ)) + { + var estimatedModuleSize = stateCountTotal / 7.0; + var found = false; + var max = this.possibleCenters.length; + for (var index = 0; index < max; index++) + { + var center = this.possibleCenters[index]; + // Look for about the same center and module size: + if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) + { + center.incrementCount(); + found = true; + break; + } + } + if (!found) + { + var point = new FinderPattern(centerJ, centerI, estimatedModuleSize); + this.possibleCenters.push(point); + if (this.resultPointCallback != null) + { + this.resultPointCallback.foundPossibleResultPoint(point); + } + } + return true; + } + } + return false; + } + + this.selectBestPatterns=function() + { + + var startSize = this.possibleCenters.length; + if (startSize < 3) + { + // Couldn't find enough finder patterns + throw "Couldn't find enough finder patterns"; + } + + // Filter outlier possibilities whose module size is too different + if (startSize > 3) + { + // But we can only afford to do so if we have at least 4 possibilities to choose from + var totalModuleSize = 0.0; + var square = 0.0; + for (var i = 0; i < startSize; i++) + { + //totalModuleSize += this.possibleCenters[i].EstimatedModuleSize; + var centerValue=this.possibleCenters[i].EstimatedModuleSize; + totalModuleSize += centerValue; + square += (centerValue * centerValue); + } + var average = totalModuleSize / startSize; + this.possibleCenters.sort(function(center1,center2) { + var dA=Math.abs(center2.EstimatedModuleSize - average); + var dB=Math.abs(center1.EstimatedModuleSize - average); + if (dA < dB) { + return (-1); + } else if (dA == dB) { + return 0; + } else { + return 1; + } + }); + + var stdDev = Math.sqrt(square / startSize - average * average); + var limit = Math.max(0.1 * average, stdDev); + for (var i = 0; i < this.possibleCenters.length && this.possibleCenters.length > 3; i++) + { + var pattern = this.possibleCenters[i]; + //if (Math.abs(pattern.EstimatedModuleSize - average) > 0.2 * average) + if (Math.abs(pattern.EstimatedModuleSize - average) > limit) + { + this.possibleCenters.remove(i); + i--; + } + } + } + + if (this.possibleCenters.length > 3) + { + // Throw away all but those first size candidate points we found. + this.possibleCenters.sort(function(a, b){ + if (a.count > b.count){return -1;} + if (a.count < b.count){return 1;} + return 0; + }); + } + + return new Array( this.possibleCenters[0], this.possibleCenters[1], this.possibleCenters[2]); + } + + this.findRowSkip=function() + { + var max = this.possibleCenters.length; + if (max <= 1) + { + return 0; + } + var firstConfirmedCenter = null; + for (var i = 0; i < max; i++) + { + var center = this.possibleCenters[i]; + if (center.Count >= CENTER_QUORUM) + { + if (firstConfirmedCenter == null) + { + firstConfirmedCenter = center; + } + else + { + // We have two confirmed centers + // How far down can we skip before resuming looking for the next + // pattern? In the worst case, only the difference between the + // difference in the x / y coordinates of the two centers. + // This is the case where you find top left last. + this.hasSkipped = true; + return Math.floor ((Math.abs(firstConfirmedCenter.X - center.X) - Math.abs(firstConfirmedCenter.Y - center.Y)) / 2); + } + } + } + return 0; + } + + this.haveMultiplyConfirmedCenters=function() + { + var confirmedCount = 0; + var totalModuleSize = 0.0; + var max = this.possibleCenters.length; + for (var i = 0; i < max; i++) + { + var pattern = this.possibleCenters[i]; + if (pattern.Count >= CENTER_QUORUM) + { + confirmedCount++; + totalModuleSize += pattern.EstimatedModuleSize; + } + } + if (confirmedCount < 3) + { + return false; + } + // OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive" + // and that we need to keep looking. We detect this by asking if the estimated module sizes + // vary too much. We arbitrarily say that when the total deviation from average exceeds + // 5% of the total module size estimates, it's too much. + var average = totalModuleSize / max; + var totalDeviation = 0.0; + for (var i = 0; i < max; i++) + { + pattern = this.possibleCenters[i]; + totalDeviation += Math.abs(pattern.EstimatedModuleSize - average); + } + return totalDeviation <= 0.05 * totalModuleSize; + } + + this.findFinderPattern = function(image){ + var tryHarder = false; + this.image=image; + var maxI = qrcode.height; + var maxJ = qrcode.width; + var iSkip = Math.floor((3 * maxI) / (4 * MAX_MODULES)); + if (iSkip < MIN_SKIP || tryHarder) + { + iSkip = MIN_SKIP; + } + + var done = false; + var stateCount = new Array(5); + for (var i = iSkip - 1; i < maxI && !done; i += iSkip) + { + // Get a row of black/white values + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + var currentState = 0; + for (var j = 0; j < maxJ; j++) + { + if (image[j+i*qrcode.width] ) + { + // Black pixel + if ((currentState & 1) == 1) + { + // Counting white pixels + currentState++; + } + stateCount[currentState]++; + } + else + { + // White pixel + if ((currentState & 1) == 0) + { + // Counting black pixels + if (currentState == 4) + { + // A winner? + if (this.foundPatternCross(stateCount)) + { + // Yes + var confirmed = this.handlePossibleCenter(stateCount, i, j); + if (confirmed) + { + // Start examining every other line. Checking each line turned out to be too + // expensive and didn't improve performance. + iSkip = 2; + if (this.hasSkipped) + { + done = this.haveMultiplyConfirmedCenters(); + } + else + { + var rowSkip = this.findRowSkip(); + if (rowSkip > stateCount[2]) + { + // Skip rows between row of lower confirmed center + // and top of presumed third confirmed center + // but back up a bit to get a full chance of detecting + // it, entire width of center of finder pattern + + // Skip by rowSkip, but back off by stateCount[2] (size of last center + // of pattern we saw) to be conservative, and also back off by iSkip which + // is about to be re-added + i += rowSkip - stateCount[2] - iSkip; + j = maxJ - 1; + } + } + } + else + { + // Advance to next black pixel + do + { + j++; + } + while (j < maxJ && !image[j + i*qrcode.width]); + j--; // back up to that last white pixel + } + // Clear state to start looking again + currentState = 0; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + } + else + { + // No, shift counts back by two + stateCount[0] = stateCount[2]; + stateCount[1] = stateCount[3]; + stateCount[2] = stateCount[4]; + stateCount[3] = 1; + stateCount[4] = 0; + currentState = 3; + } + } + else + { + stateCount[++currentState]++; + } + } + else + { + // Counting white pixels + stateCount[currentState]++; + } + } + } + if (this.foundPatternCross(stateCount)) + { + var confirmed = this.handlePossibleCenter(stateCount, i, maxJ); + if (confirmed) + { + iSkip = stateCount[0]; + if (this.hasSkipped) + { + // Found a third one + done = haveMultiplyConfirmedCenters(); + } + } + } + } + + var patternInfo = this.selectBestPatterns(); + qrcode.orderBestPatterns(patternInfo); + + return new FinderPatternInfo(patternInfo); + }; +} diff --git a/js/jsqrcode/formatinf.js b/js/jsqrcode/formatinf.js new file mode 100644 index 000000000..7e4b59268 --- /dev/null +++ b/js/jsqrcode/formatinf.js @@ -0,0 +1,104 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +var FORMAT_INFO_MASK_QR = 0x5412; +var FORMAT_INFO_DECODE_LOOKUP = new Array(new Array(0x5412, 0x00), new Array(0x5125, 0x01), new Array(0x5E7C, 0x02), new Array(0x5B4B, 0x03), new Array(0x45F9, 0x04), new Array(0x40CE, 0x05), new Array(0x4F97, 0x06), new Array(0x4AA0, 0x07), new Array(0x77C4, 0x08), new Array(0x72F3, 0x09), new Array(0x7DAA, 0x0A), new Array(0x789D, 0x0B), new Array(0x662F, 0x0C), new Array(0x6318, 0x0D), new Array(0x6C41, 0x0E), new Array(0x6976, 0x0F), new Array(0x1689, 0x10), new Array(0x13BE, 0x11), new Array(0x1CE7, 0x12), new Array(0x19D0, 0x13), new Array(0x0762, 0x14), new Array(0x0255, 0x15), new Array(0x0D0C, 0x16), new Array(0x083B, 0x17), new Array(0x355F, 0x18), new Array(0x3068, 0x19), new Array(0x3F31, 0x1A), new Array(0x3A06, 0x1B), new Array(0x24B4, 0x1C), new Array(0x2183, 0x1D), new Array(0x2EDA, 0x1E), new Array(0x2BED, 0x1F)); +var BITS_SET_IN_HALF_BYTE = new Array(0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4); + + +function FormatInformation(formatInfo) +{ + this.errorCorrectionLevel = ErrorCorrectionLevel.forBits((formatInfo >> 3) & 0x03); + this.dataMask = (formatInfo & 0x07); + + this.__defineGetter__("ErrorCorrectionLevel", function() + { + return this.errorCorrectionLevel; + }); + this.__defineGetter__("DataMask", function() + { + return this.dataMask; + }); + this.GetHashCode=function() + { + return (this.errorCorrectionLevel.ordinal() << 3) | dataMask; + } + this.Equals=function( o) + { + var other = o; + return this.errorCorrectionLevel == other.errorCorrectionLevel && this.dataMask == other.dataMask; + } +} + +FormatInformation.numBitsDiffering=function( a, b) +{ + a ^= b; // a now has a 1 bit exactly where its bit differs with b's + // Count bits set quickly with a series of lookups: + return BITS_SET_IN_HALF_BYTE[a & 0x0F] + BITS_SET_IN_HALF_BYTE[(URShift(a, 4) & 0x0F)] + BITS_SET_IN_HALF_BYTE[(URShift(a, 8) & 0x0F)] + BITS_SET_IN_HALF_BYTE[(URShift(a, 12) & 0x0F)] + BITS_SET_IN_HALF_BYTE[(URShift(a, 16) & 0x0F)] + BITS_SET_IN_HALF_BYTE[(URShift(a, 20) & 0x0F)] + BITS_SET_IN_HALF_BYTE[(URShift(a, 24) & 0x0F)] + BITS_SET_IN_HALF_BYTE[(URShift(a, 28) & 0x0F)]; +} + +FormatInformation.decodeFormatInformation=function( maskedFormatInfo) +{ + var formatInfo = FormatInformation.doDecodeFormatInformation(maskedFormatInfo); + if (formatInfo != null) + { + return formatInfo; + } + // Should return null, but, some QR codes apparently + // do not mask this info. Try again by actually masking the pattern + // first + return FormatInformation.doDecodeFormatInformation(maskedFormatInfo ^ FORMAT_INFO_MASK_QR); +} +FormatInformation.doDecodeFormatInformation=function( maskedFormatInfo) +{ + // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing + var bestDifference = 0xffffffff; + var bestFormatInfo = 0; + for (var i = 0; i < FORMAT_INFO_DECODE_LOOKUP.length; i++) + { + var decodeInfo = FORMAT_INFO_DECODE_LOOKUP[i]; + var targetInfo = decodeInfo[0]; + if (targetInfo == maskedFormatInfo) + { + // Found an exact match + return new FormatInformation(decodeInfo[1]); + } + var bitsDifference = this.numBitsDiffering(maskedFormatInfo, targetInfo); + if (bitsDifference < bestDifference) + { + bestFormatInfo = decodeInfo[1]; + bestDifference = bitsDifference; + } + } + // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits + // differing means we found a match + if (bestDifference <= 3) + { + return new FormatInformation(bestFormatInfo); + } + return null; +} + + \ No newline at end of file diff --git a/js/jsqrcode/gf256.js b/js/jsqrcode/gf256.js new file mode 100644 index 000000000..97658627d --- /dev/null +++ b/js/jsqrcode/gf256.js @@ -0,0 +1,117 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function GF256( primitive) +{ + this.expTable = new Array(256); + this.logTable = new Array(256); + var x = 1; + for (var i = 0; i < 256; i++) + { + this.expTable[i] = x; + x <<= 1; // x = x * 2; we're assuming the generator alpha is 2 + if (x >= 0x100) + { + x ^= primitive; + } + } + for (var i = 0; i < 255; i++) + { + this.logTable[this.expTable[i]] = i; + } + // logTable[0] == 0 but this should never be used + var at0=new Array(1);at0[0]=0; + this.zero = new GF256Poly(this, new Array(at0)); + var at1=new Array(1);at1[0]=1; + this.one = new GF256Poly(this, new Array(at1)); + + this.__defineGetter__("Zero", function() + { + return this.zero; + }); + this.__defineGetter__("One", function() + { + return this.one; + }); + this.buildMonomial=function( degree, coefficient) + { + if (degree < 0) + { + throw "System.ArgumentException"; + } + if (coefficient == 0) + { + return zero; + } + var coefficients = new Array(degree + 1); + for(var i=0;i 1 && coefficients[0] == 0) + { + // Leading term must be non-zero for anything except the constant polynomial "0" + var firstNonZero = 1; + while (firstNonZero < coefficientsLength && coefficients[firstNonZero] == 0) + { + firstNonZero++; + } + if (firstNonZero == coefficientsLength) + { + this.coefficients = field.Zero.coefficients; + } + else + { + this.coefficients = new Array(coefficientsLength - firstNonZero); + for(var i=0;i largerCoefficients.length) + { + var temp = smallerCoefficients; + smallerCoefficients = largerCoefficients; + largerCoefficients = temp; + } + var sumDiff = new Array(largerCoefficients.length); + var lengthDiff = largerCoefficients.length - smallerCoefficients.length; + // Copy high-order terms only found in higher-degree polynomial's coefficients + //Array.Copy(largerCoefficients, 0, sumDiff, 0, lengthDiff); + for(var ci=0;ci= other.Degree && !remainder.Zero) + { + var degreeDifference = remainder.Degree - other.Degree; + var scale = this.field.multiply(remainder.getCoefficient(remainder.Degree), inverseDenominatorLeadingTerm); + var term = other.multiplyByMonomial(degreeDifference, scale); + var iterationQuotient = this.field.buildMonomial(degreeDifference, scale); + quotient = quotient.addOrSubtract(iterationQuotient); + remainder = remainder.addOrSubtract(term); + } + + return new Array(quotient, remainder); + } +} \ No newline at end of file diff --git a/js/jsqrcode/grid.js b/js/jsqrcode/grid.js new file mode 100644 index 000000000..cf9150ea5 --- /dev/null +++ b/js/jsqrcode/grid.js @@ -0,0 +1,152 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +GridSampler = {}; + +GridSampler.checkAndNudgePoints=function( image, points) + { + var width = qrcode.width; + var height = qrcode.height; + // Check and nudge points from start until we see some that are OK: + var nudged = true; + for (var offset = 0; offset < points.length && nudged; offset += 2) + { + var x = Math.floor (points[offset]); + var y = Math.floor( points[offset + 1]); + if (x < - 1 || x > width || y < - 1 || y > height) + { + throw "Error.checkAndNudgePoints "; + } + nudged = false; + if (x == - 1) + { + points[offset] = 0.0; + nudged = true; + } + else if (x == width) + { + points[offset] = width - 1; + nudged = true; + } + if (y == - 1) + { + points[offset + 1] = 0.0; + nudged = true; + } + else if (y == height) + { + points[offset + 1] = height - 1; + nudged = true; + } + } + // Check and nudge points from end: + nudged = true; + for (var offset = points.length - 2; offset >= 0 && nudged; offset -= 2) + { + var x = Math.floor( points[offset]); + var y = Math.floor( points[offset + 1]); + if (x < - 1 || x > width || y < - 1 || y > height) + { + throw "Error.checkAndNudgePoints "; + } + nudged = false; + if (x == - 1) + { + points[offset] = 0.0; + nudged = true; + } + else if (x == width) + { + points[offset] = width - 1; + nudged = true; + } + if (y == - 1) + { + points[offset + 1] = 0.0; + nudged = true; + } + else if (y == height) + { + points[offset + 1] = height - 1; + nudged = true; + } + } + } + + + +GridSampler.sampleGrid3=function( image, dimension, transform) + { + var bits = new BitMatrix(dimension); + var points = new Array(dimension << 1); + for (var y = 0; y < dimension; y++) + { + var max = points.length; + var iValue = y + 0.5; + for (var x = 0; x < max; x += 2) + { + points[x] = (x >> 1) + 0.5; + points[x + 1] = iValue; + } + transform.transformPoints1(points); + // Quick check to see if points transformed to something inside the image; + // sufficient to check the endpoints + GridSampler.checkAndNudgePoints(image, points); + try + { + for (var x = 0; x < max; x += 2) + { + var xpoint = (Math.floor( points[x]) * 4) + (Math.floor( points[x + 1]) * qrcode.width * 4); + var bit = image[Math.floor( points[x])+ qrcode.width* Math.floor( points[x + 1])]; + qrcode.imagedata.data[xpoint] = bit?255:0; + qrcode.imagedata.data[xpoint+1] = bit?255:0; + qrcode.imagedata.data[xpoint+2] = 0; + qrcode.imagedata.data[xpoint+3] = 255; + //bits[x >> 1][ y]=bit; + if(bit) + bits.set_Renamed(x >> 1, y); + } + } + catch ( aioobe) + { + // This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting + // transform gets "twisted" such that it maps a straight line of points to a set of points + // whose endpoints are in bounds, but others are not. There is probably some mathematical + // way to detect this about the transformation that I don't know yet. + // This results in an ugly runtime exception despite our clever checks above -- can't have + // that. We could check each point's coordinates but that feels duplicative. We settle for + // catching and wrapping ArrayIndexOutOfBoundsException. + throw "Error.checkAndNudgePoints"; + } + } + return bits; + } + +GridSampler.sampleGridx=function( image, dimension, p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY, p1FromX, p1FromY, p2FromX, p2FromY, p3FromX, p3FromY, p4FromX, p4FromY) +{ + var transform = PerspectiveTransform.quadrilateralToQuadrilateral(p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY, p1FromX, p1FromY, p2FromX, p2FromY, p3FromX, p3FromY, p4FromX, p4FromY); + + return GridSampler.sampleGrid3(image, dimension, transform); +} diff --git a/js/jsqrcode/index.d.ts b/js/jsqrcode/index.d.ts new file mode 100644 index 000000000..e7768d593 --- /dev/null +++ b/js/jsqrcode/index.d.ts @@ -0,0 +1,500 @@ +// Type definitions for jsqrcode 1.0 +// Project: https://github.com/LazarSoft/jsqrcode +// Definitions by: Ricardo Azzi Silva +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare function URShift(number: number, bits: number): number; + +interface ResultPointCallback { + foundPossibleResultPoint(point: FinderPattern): void; +} + +declare class AlignmentPattern { + private x: number; + private y: number; + private count: number; + private estimatedModuleSize: number; + + readonly X: number; + readonly Y: number; + readonly EstimatedModuleSize: number; + readonly Count: number; + + constructor(posX: number, posY: number, estimatedModuleSize: number); + + incrementCount(): void; + + aboutEquals(moduleSize: number, i: number, j: number): boolean; +} + +declare class AlignmentPatternFinder { + possibleCenters: AlignmentPattern[]; + crossCheckStateCount: [number, number, number]; + image: Uint8Array; + startX: number; + startY: number; + width: number; + height: number; + moduleSize: number; + resultPointCallback: ResultPointCallback; + + constructor( + image: Uint8Array, startX: number, startY: number, + width: number, height: number, + moduleSize: number, resultPointCallback: ResultPointCallback + ); + + private centerFromEnd(stateCount: number, end: number): number; + + private foundPatternCross(stateCount: number): boolean; + + private handlePossibleCenter(stateCount: number, i: number, j: number): AlignmentPattern; + + find(): AlignmentPattern; +} + +declare class BitMatrix { + private width: number; + private height: number; + + readonly Width: number; + readonly Height: number; + readonly Dimension: number; + + rowSize: number; + bits: number[]; + + constructor(width: number, height?: number); + + get_Renamed(x: number, y: number): boolean; + set_Renamed(x: number, y: number): void; + flip(x: number, y: number): void; + clear(): void; + setRegion(left: number, top: number, width: number, height: number): void; +} + +declare class BitMatrixParser { + bitMatrix: BitMatrix; + parsedVersion: Version; + parsedFormatInfo: FormatInformation; + + constructor(bitMatrix: BitMatrix); + + copyBit(i: number, j: number, versionBits: number): number; + readFormatInformation(): FormatInformation; + readVersion(): Version; + readCodewords(): number[]; +} + +declare class DataBlock { + static getDataBlocks( + rawCodewords: number[], version: Version, ecLevel: ErrorCorrectionLevel + ): DataBlock[]; + + private numDataCodewords: number; + private codewords: number[]; + + readonly NumDataCodewords: number; + readonly Codewords: number[]; + + constructor(numDataCodewords: number, codewords: number[]); +} + +declare class QRCodeDataBlockReader { + blockPointer: number; + bitPointer: number; + dataLength: number; + dataLengthMode: number; + blocks: number[]; + numErrorCorrectionCode: number; + + readonly DataByte: Array; + + constructor(blocks: number[], version: number, numErrorCorrectionCode: number); + + getNextBits(numBits: number): number; + NextMode(): number; + getDataLength(modeIndicator: number): number; + getRomanAndFigureString(dataLength: number): string; + getFigureString(dataLength: number): string; + get8bitByteArray(dataLength: number): number[]; + getKanjiString(dataLength: number): string; + parseECIValue(): number; +} + +declare abstract class DataMask { + private static DATA_MASKS: DataMask[]; + + static forReference(reference: number): DataMask; + + abstract unmaskBitMatrix(bits: number[], dimension: number): void; + + abstract isMasked(i: number, j: number): boolean; +} + +declare class DataMask000 extends DataMask { + unmaskBitMatrix(bits: number[], dimension: number): void; + + isMasked(i: number, j: number): boolean; +} + +declare class DataMask001 extends DataMask { + unmaskBitMatrix(bits: number[], dimension: number): void; + + isMasked(i: number, j: number): boolean; +} + +declare class DataMask010 extends DataMask { + unmaskBitMatrix(bits: number[], dimension: number): void; + + isMasked(i: number, j: number): boolean; +} + +declare class DataMask011 extends DataMask { + unmaskBitMatrix(bits: number[], dimension: number): void; + + isMasked(i: number, j: number): boolean; +} + +declare class DataMask100 extends DataMask { + unmaskBitMatrix(bits: number[], dimension: number): void; + + isMasked(i: number, j: number): boolean; +} + +declare class DataMask101 extends DataMask { + unmaskBitMatrix(bits: number[], dimension: number): void; + + isMasked(i: number, j: number): boolean; +} + +declare class DataMask110 extends DataMask { + unmaskBitMatrix(bits: number[], dimension: number): void; + + isMasked(i: number, j: number): boolean; +} + +declare class DataMask111 extends DataMask { + unmaskBitMatrix(bits: number[], dimension: number): void; + + isMasked(i: number, j: number): boolean; +} + +declare const Decoder: { + rsDecoder: ReedSolomonDecoder, + + correctErrors(codewordBytes: number[], numDataCodewords: number): void; + + decode(bits: BitMatrix): QRCodeDataBlockReader; +}; + +declare class PerspectiveTransform { + a11: number; + a12: number; + a13: number; + a21: number; + a22: number; + a23: number; + a31: number; + a32: number; + a33: number; + + static quadrilateralToQuadrilateral( + x0: number, y0: number, x1: number, y1: number, + x2: number, y2: number, x3: number, y3: number, + x0p: number, y0p: number, x1p: number, y1p: number, + x2p: number, y2p: number, x3p: number, y3p: number + ): PerspectiveTransform; + static squareToQuadrilateral( + x0: number, y0: number, x1: number, y1: number, + x2: number, y2: number, x3: number, y3: number + ): PerspectiveTransform; + static quadrilateralToSquare( + x0: number, y0: number, x1: number, y1: number, + x2: number, y2: number, x3: number, y3: number + ): PerspectiveTransform; + + constructor( + a11: number, a21: number, a31: number, + a12: number, a22: number, a32: number, + a13: number, a23: number, a33: number + ); + + transformPoints1(points: number[]): void; + transformPoints2(xValues: number[], yValues: number[]): void; + buildAdjoint(): PerspectiveTransform; + times(other: PerspectiveTransform): PerspectiveTransform; +} + +declare class DetectorResult { + bits: BitMatrix; + points: [DetectorResult, DetectorResult, DetectorResult] | + [DetectorResult, DetectorResult, DetectorResult, DetectorResult]; + + constructor( + bits: BitMatrix, + points: [DetectorResult, DetectorResult, DetectorResult] | + [DetectorResult, DetectorResult, DetectorResult, DetectorResult] + ); +} + +declare class Detector { + image: Uint8Array; + resultPointCallback: ResultPointCallback; + + constructor(image: Uint8Array); + + sizeOfBlackWhiteBlackRun(fromX: number, fromY: number, toX: number, toY: number): number; + sizeOfBlackWhiteBlackRunBothWays(fromX: number, fromY: number, toX: number, toY: number): number; + calculateModuleSizeOneWay(pattern: AlignmentPattern, otherPattern: AlignmentPattern): number; + calculateModuleSize(topLeft: AlignmentPattern, topRight: AlignmentPattern, bottomLeft: AlignmentPattern): number; + distance(pattern1: AlignmentPattern, pattern2: AlignmentPattern): number; + computeDimension(topLeft: AlignmentPattern, topRight: AlignmentPattern, bottomLeft: AlignmentPattern, moduleSize: number): number; + findAlignmentInRegion(overallEstModuleSize: number, estAlignmentX: number, estAlignmentY: number, allowanceFactor: number): AlignmentPattern; + createTransform(topLeft: AlignmentPattern, topRight: AlignmentPattern, bottomLeft: AlignmentPattern, alignmentPattern: AlignmentPattern, dimension: number): PerspectiveTransform; + sampleGrid(image: Uint8Array, transform: PerspectiveTransform, dimension: number): BitMatrix; + processFinderPatternInfo(info: FinderPatternInfo): DetectorResult; + detect(): DetectorResult; +} + +declare const L: ErrorCorrectionLevel; +declare const M: ErrorCorrectionLevel; +declare const Q: ErrorCorrectionLevel; +declare const H: ErrorCorrectionLevel; +declare const FOR_BITS: ErrorCorrectionLevel[]; + +declare class ErrorCorrectionLevel { + private ordinal_Renamed_Field: number; + private bits: number; + private name: string; + + readonly Bits: number; + readonly Name: string; + + static forBits(bits: number): ErrorCorrectionLevel; + + constructor(ordinal: number, bits: number, name: string); +} +declare const MIN_SKIP: number; +declare const MAX_MODULES: number; +declare const INTEGER_MATH_SHIFT: number; +declare const CENTER_QUORUM: number; + +declare class FinderPattern { + private x: number; + private y: number; + private count: number; + private estimatedModuleSize: number; + + readonly X: number; + readonly Y: number; + readonly Count: number; + readonly EstimatedModuleSize: number; + + constructor(posX: number, posY: number, estimatedModuleSize: number); + + incrementCount(): void; + aboutEquals(moduleSize: number, i: number, j: number): boolean; +} + +declare class FinderPatternInfo { + readonly BottomLeft: AlignmentPattern; + readonly TopLeft: AlignmentPattern; + readonly TopRight: AlignmentPattern; + + constructor(patternCenters: [AlignmentPattern, AlignmentPattern, AlignmentPattern]); +} + +declare class FinderPatternFinder { + image: Uint8Array; + possibleCenters: FinderPattern[]; + hasSkipped: boolean; + resultPointCallback: ResultPointCallback; + + private crossCheckStateCount: [number, number, number, number, number]; + readonly CrossCheckStateCount: [number, number, number, number, number]; + + foundPatternCross(stateCount: [number, number, number, number, number]): boolean; + centerFromEnd(stateCount: [number, number, number, number, number], end: number): number; + crossCheckVertical(startI: number, centerJ: number, maxCount: number, originalStateCountTotal: number): number; + crossCheckHorizontal(startJ: number, centerI: number, maxCount: number, originalStateCountTotal: number): number; + handlePossibleCenter(stateCount: [number, number, number, number, number], i: number, j: number): boolean; + selectBestPatterns(): number; + findRowSkip(): number; + haveMultiplyConfirmedCenters(): boolean; + findFinderPattern(image: Uint8Array): FinderPatternInfo; +} + +declare const FORMAT_INFO_MASK_QR: 0x5412; +declare const FORMAT_INFO_DECODE_LOOKUP: [ + [0x5412, 0x00], [0x5125, 0x01], [0x5E7C, 0x02], [0x5B4B, 0x03], [0x45F9, 0x04], [0x40CE, 0x05], [0x4F97, 0x06], + [0x4AA0, 0x07], [0x77C4, 0x08], [0x72F3, 0x09], [0x7DAA, 0x0A], [0x789D, 0x0B], [0x662F, 0x0C], [0x6318, 0x0D], + [0x6C41, 0x0E], [0x6976, 0x0F], [0x1689, 0x10], [0x13BE, 0x11], [0x1CE7, 0x12], [0x19D0, 0x13], [0x0762, 0x14], + [0x0255, 0x15], [0x0D0C, 0x16], [0x083B, 0x17], [0x355F, 0x18], [0x3068, 0x19], [0x3F31, 0x1A], [0x3A06, 0x1B], + [0x24B4, 0x1C], [0x2183, 0x1D], [0x2EDA, 0x1E], [0x2BED, 0x1F] +]; +declare const BITS_SET_IN_HALF_BYTE: [0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4]; + +declare class FormatInformation { + static numBitsDiffering(a: number, b: [number, number]): number; + static decodeFormatInformation(maskedFormatInfo: number): FormatInformation; + private static doDecodeFormatInformation(maskedFormatInfo: number): FormatInformation; + + private errorCorrectionLevel: ErrorCorrectionLevel; + private dataMask: number; + + readonly ErrorCorrectionLevel: ErrorCorrectionLevel; + readonly DataMask: number; + + GetHashCode(): number; + Equals(other: FormatInformation): boolean; +} + +declare class GF256 { + static readonly QR_CODE_FIELD: GF256; + static readonly DATA_MATRIX_FIELD: GF256; + + private zero: GF256Poly; + private one: GF256Poly; + + expTable: number[]; + logTable: number[]; + readonly Zero: GF256Poly; + readonly One: GF256Poly; + + static addOrSubtract(a: number, b: number): number; + + constructor(primitive: number); + + buildMonomial(degree: number, coefficient: number): GF256Poly; + exp(a: number): number; + log(a: number): number; + inverse(a: number): number; + multiply(a: number, b: number): number; +} + +declare class GF256Poly { + field: GF256; + private coefficients: number[]; + + readonly Zero: boolean; + readonly Degree: number; + readonly Coefficients: number[]; + + constructor(field: GF256, coefficients: number[]); + + getCoefficient(degree: number): number; + evaluateAt(a: number): number; + addOrSubtract(other: GF256Poly): GF256Poly; + multiply1(other: GF256Poly): GF256Poly; + multiply2(scalar: number): GF256Poly; + multiplyByMonomial(degree: number, coefficient: number): GF256Poly; + divide(other: GF256Poly): [GF256Poly, GF256Poly]; +} + +declare const GridSampler: { + checkAndNudgePoints(image: Uint8Array, points: number[]): void; + sampleGrid3(image: Uint8Array, dimension: number, transform: PerspectiveTransform): BitMatrix; + sampleGridx( + image: Uint8Array, dimension: number, p1ToX: number, + p1ToY: number, p2ToX: number, p2ToY: number, p3ToX: number, + p3ToY: number, p4ToX: number, p4ToY: number, p1FromX: number, + p1FromY: number, p2FromX: number, p2FromY: number, p3FromX: number, + p3FromY: number, p4FromX: number, p4FromY: number + ): BitMatrix; +}; + +declare class ReedSolomonDecoder { + field: GF256; + + constructor(field: GF256); + + decode(received: GF256, twoS: number[]): void; + runEuclideanAlgorithm(a: GF256Poly, b: GF256Poly, R: number): [GF256Poly, GF256Poly]; + findErrorLocations(errorLocator: GF256Poly): number[]; + findErrorMagnitudes(errorEvaluator: GF256Poly, errorLocations: number[], dataMatrix: boolean): number[]; +} +declare function buildVersions(): Version[]; + +declare class ECB { + private count: number; + private dataCodewords: number; + + readonly Count: number; + readonly DataCodewords: number; + + constructor(count: number, dataCodewords: number); +} + +declare class ECBlocks { + private ecCodewordsPerBlock: number; + private ecBlocks: [ECB] | [ECB, ECB]; + + readonly ECCodewordsPerBlock: number; + readonly TotalECCodewords: number; + readonly NumBlocks: number; + + constructor(ecCodewordsPerBlock: number, ecBlocks1: ECB, ecBlocks2?: ECB); + + getECBlocks(): [ECB] | [ECB, ECB]; +} + +declare class Version { + static readonly VERSION_DECODE_INFO: [ + 0x07C94, 0x085BC, 0x09A99, 0x0A4D3, 0x0BBF6, 0x0C762, 0x0D847, 0x0E60D, 0x0F928, + 0x10B78, 0x1145D, 0x12A17, 0x13532, 0x149A6, 0x15683, 0x168C9, 0x177EC, 0x18EC4, + 0x191E1, 0x1AFAB, 0x1B08E, 0x1CC1A, 0x1D33F, 0x1ED75, 0x1F250, 0x209D5, 0x216F0, + 0x228BA, 0x2379F, 0x24B0B, 0x2542E, 0x26A64, 0x27541, 0x28C69 + ]; + static readonly VERSIONS: Version[]; + + static getVersionForNumber(versionNumber: number): Version; + static getProvisionalVersionForDimension(dimension: number): Version; + static decodeVersionInformation(versionBits: number): Version; + + versionNumber: number; + alignmentPatternCenters: number[]; + ecBlocks: ECBlocks[]; + + readonly VersionNumber: number; + readonly AlignmentPatternCenters: number[]; + readonly TotalCodewords: number; + readonly DimensionForVersion: number; + + constructor(versionNumber: number, alignmentPatternCenters: number[], + ecBlocks1: ECBlocks, ecBlocks2: ECBlocks, + ecBlocks3: ECBlocks, ecBlocks4: ECBlocks); + + buildFunctionPattern(): BitMatrix; + + getECBlocksForLevel(ecLevel: ErrorCorrectionLevel): ECBlocks; +} + +declare const qrcode: { + imagedata: ImageData, + width: number, + height: number, + qrCodeSymbol: any, + debug: boolean, + maxImgSize: number, + readonly sizeOfDataLengthInfo: [[10, 9, 8, 8], [12, 11, 16, 10], [14, 13, 16, 12]], + + // tslint:disable-next-line:prefer-method-signature + callback: (result: string) => void, + + orderBestPatterns(patterns: AlignmentPattern[]): void, + + vidError(error?: any): void, + captureToCanvas(): void, + setWebcam(videoId: string): void, + decode(src?: string): void, + isUrl(s: string): boolean, + decode_url(s: string): string, + decode_utf8(s: string): string, + process(ctx: CanvasRenderingContext2D): string, + getPixel(x: number, y: number): number, + binarize(th: number): boolean[], + getMiddleBrightnessPerArea(image: number[]): number[][], + grayScaleToBitmap(grayScale: number[]): Uint8Array, + grayscale(): Uint8Array +}; diff --git a/js/jsqrcode/qrcode.js b/js/jsqrcode/qrcode.js new file mode 100644 index 000000000..8644c66c5 --- /dev/null +++ b/js/jsqrcode/qrcode.js @@ -0,0 +1,319 @@ +/* + Copyright 2011 Lazar Laszlo (lazarsoft@gmail.com, www.lazarsoft.info) + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + + +qrcode = {}; +qrcode.imagedata = null; +qrcode.width = 0; +qrcode.height = 0; +qrcode.qrCodeSymbol = null; +qrcode.debug = false; +qrcode.maxImgSize = 1024*1024; + +qrcode.sizeOfDataLengthInfo = [ [ 10, 9, 8, 8 ], [ 12, 11, 16, 10 ], [ 14, 13, 16, 12 ] ]; + +qrcode.callback = null; + +qrcode.decode = function(src){ + + if(arguments.length==0) + { + var canvas_qr = document.getElementById("qr-canvas"); + var context = canvas_qr.getContext('2d'); + qrcode.width = canvas_qr.width; + qrcode.height = canvas_qr.height; + qrcode.imagedata = context.getImageData(0, 0, qrcode.width, qrcode.height); + qrcode.result = qrcode.process(context); + if(qrcode.callback!=null) + qrcode.callback(qrcode.result); + return qrcode.result; + } + else + { + var image = new Image(); + image.onload=function(){ + //var canvas_qr = document.getElementById("qr-canvas"); + var canvas_qr = document.createElement('canvas'); + var context = canvas_qr.getContext('2d'); + var nheight = image.height; + var nwidth = image.width; + if(image.width*image.height>qrcode.maxImgSize) + { + var ir = image.width / image.height; + nheight = Math.sqrt(qrcode.maxImgSize/ir); + nwidth=ir*nheight; + } + + canvas_qr.width = nwidth; + canvas_qr.height = nheight; + + context.drawImage(image, 0, 0, canvas_qr.width, canvas_qr.height ); + qrcode.width = canvas_qr.width; + qrcode.height = canvas_qr.height; + try{ + qrcode.imagedata = context.getImageData(0, 0, canvas_qr.width, canvas_qr.height); + }catch(e){ + qrcode.result = "Cross domain image reading not supported in your browser! Save it to your computer then drag and drop the file!"; + if(qrcode.callback!=null) + qrcode.callback(qrcode.result); + return; + } + + try + { + qrcode.result = qrcode.process(context); + } + catch(e) + { + console.log(e); + qrcode.result = "error decoding QR Code"; + } + if(qrcode.callback!=null) + qrcode.callback(qrcode.result); + } + image.src = src; + } +} + +qrcode.isUrl = function(s) +{ + var regexp = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; + return regexp.test(s); +} + +qrcode.decode_url = function (s) +{ + var escaped = ""; + try{ + escaped = escape( s ); + } + catch(e) + { + console.log(e); + escaped = s; + } + var ret = ""; + try{ + ret = decodeURIComponent( escaped ); + } + catch(e) + { + console.log(e); + ret = escaped; + } + return ret; +} + +qrcode.decode_utf8 = function ( s ) +{ + if(qrcode.isUrl(s)) + return qrcode.decode_url(s); + else + return s; +} + +qrcode.process = function(ctx){ + + var start = new Date().getTime(); + + var image = qrcode.grayScaleToBitmap(qrcode.grayscale()); + //var image = qrcode.binarize(128); + + if(qrcode.debug) + { + for (var y = 0; y < qrcode.height; y++) + { + for (var x = 0; x < qrcode.width; x++) + { + var point = (x * 4) + (y * qrcode.width * 4); + qrcode.imagedata.data[point] = image[x+y*qrcode.width]?0:0; + qrcode.imagedata.data[point+1] = image[x+y*qrcode.width]?0:0; + qrcode.imagedata.data[point+2] = image[x+y*qrcode.width]?255:0; + } + } + ctx.putImageData(qrcode.imagedata, 0, 0); + } + + //var finderPatternInfo = new FinderPatternFinder().findFinderPattern(image); + + var detector = new Detector(image); + + var qRCodeMatrix = detector.detect(); + + /*for (var y = 0; y < qRCodeMatrix.bits.Height; y++) + { + for (var x = 0; x < qRCodeMatrix.bits.Width; x++) + { + var point = (x * 4*2) + (y*2 * qrcode.width * 4); + qrcode.imagedata.data[point] = qRCodeMatrix.bits.get_Renamed(x,y)?0:0; + qrcode.imagedata.data[point+1] = qRCodeMatrix.bits.get_Renamed(x,y)?0:0; + qrcode.imagedata.data[point+2] = qRCodeMatrix.bits.get_Renamed(x,y)?255:0; + } + }*/ + if(qrcode.debug) + ctx.putImageData(qrcode.imagedata, 0, 0); + + var reader = Decoder.decode(qRCodeMatrix.bits); + var data = reader.DataByte; + var str=""; + for(var i=0;i minmax[ax][ay][1]) + minmax[ax][ay][1] = target; + } + } + //minmax[ax][ay][0] = (minmax[ax][ay][0] + minmax[ax][ay][1]) / 2; + } + } + var middle = new Array(numSqrtArea); + for (var i3 = 0; i3 < numSqrtArea; i3++) + { + middle[i3] = new Array(numSqrtArea); + } + for (var ay = 0; ay < numSqrtArea; ay++) + { + for (var ax = 0; ax < numSqrtArea; ax++) + { + middle[ax][ay] = Math.floor((minmax[ax][ay][0] + minmax[ax][ay][1]) / 2); + //Console.out.print(middle[ax][ay] + ","); + } + //Console.out.println(""); + } + //Console.out.println(""); + + return middle; +} + +qrcode.grayScaleToBitmap=function(grayScale) +{ + var middle = qrcode.getMiddleBrightnessPerArea(grayScale); + var sqrtNumArea = middle.length; + var areaWidth = Math.floor(qrcode.width / sqrtNumArea); + var areaHeight = Math.floor(qrcode.height / sqrtNumArea); + var bitmap = new Array(qrcode.height*qrcode.width); + + for (var ay = 0; ay < sqrtNumArea; ay++) + { + for (var ax = 0; ax < sqrtNumArea; ax++) + { + for (var dy = 0; dy < areaHeight; dy++) + { + for (var dx = 0; dx < areaWidth; dx++) + { + bitmap[areaWidth * ax + dx+ (areaHeight * ay + dy)*qrcode.width] = (grayScale[areaWidth * ax + dx+ (areaHeight * ay + dy)*qrcode.width] < middle[ax][ay])?true:false; + } + } + } + } + return bitmap; +} + +qrcode.grayscale = function(){ + var ret = new Array(qrcode.width*qrcode.height); + for (var y = 0; y < qrcode.height; y++) + { + for (var x = 0; x < qrcode.width; x++) + { + var gray = qrcode.getPixel(x, y); + + ret[x+y*qrcode.width] = gray; + } + } + return ret; +} + + + + +function URShift( number, bits) +{ + if (number >= 0) + return number >> bits; + else + return (number >> bits) + (2 << ~bits); +} + + +Array.prototype.remove = function(from, to) { + var rest = this.slice((to || from) + 1 || this.length); + this.length = from < 0 ? this.length + from : from; + return this.push.apply(this, rest); +}; diff --git a/js/jsqrcode/rsdecoder.js b/js/jsqrcode/rsdecoder.js new file mode 100644 index 000000000..640769a5d --- /dev/null +++ b/js/jsqrcode/rsdecoder.js @@ -0,0 +1,178 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + +function ReedSolomonDecoder(field) +{ + this.field = field; + this.decode=function(received, twoS) + { + var poly = new GF256Poly(this.field, received); + var syndromeCoefficients = new Array(twoS); + for(var i=0;i= b's + if (a.Degree < b.Degree) + { + var temp = a; + a = b; + b = temp; + } + + var rLast = a; + var r = b; + var sLast = this.field.One; + var s = this.field.Zero; + var tLast = this.field.Zero; + var t = this.field.One; + + // Run Euclidean algorithm until r's degree is less than R/2 + while (r.Degree >= Math.floor(R / 2)) + { + var rLastLast = rLast; + var sLastLast = sLast; + var tLastLast = tLast; + rLast = r; + sLast = s; + tLast = t; + + // Divide rLastLast by rLast, with quotient in q and remainder in r + if (rLast.Zero) + { + // Oops, Euclidean algorithm already terminated? + throw "r_{i-1} was zero"; + } + r = rLastLast; + var q = this.field.Zero; + var denominatorLeadingTerm = rLast.getCoefficient(rLast.Degree); + var dltInverse = this.field.inverse(denominatorLeadingTerm); + while (r.Degree >= rLast.Degree && !r.Zero) + { + var degreeDiff = r.Degree - rLast.Degree; + var scale = this.field.multiply(r.getCoefficient(r.Degree), dltInverse); + q = q.addOrSubtract(this.field.buildMonomial(degreeDiff, scale)); + r = r.addOrSubtract(rLast.multiplyByMonomial(degreeDiff, scale)); + //r.EXE(); + } + + s = q.multiply1(sLast).addOrSubtract(sLastLast); + t = q.multiply1(tLast).addOrSubtract(tLastLast); + } + + var sigmaTildeAtZero = t.getCoefficient(0); + if (sigmaTildeAtZero == 0) + { + throw "ReedSolomonException sigmaTilde(0) was zero"; + } + + var inverse = this.field.inverse(sigmaTildeAtZero); + var sigma = t.multiply2(inverse); + var omega = r.multiply2(inverse); + return new Array(sigma, omega); + } + this.findErrorLocations=function( errorLocator) + { + // This is a direct application of Chien's search + var numErrors = errorLocator.Degree; + if (numErrors == 1) + { + // shortcut + return new Array(errorLocator.getCoefficient(1)); + } + var result = new Array(numErrors); + var e = 0; + for (var i = 1; i < 256 && e < numErrors; i++) + { + if (errorLocator.evaluateAt(i) == 0) + { + result[e] = this.field.inverse(i); + e++; + } + } + if (e != numErrors) + { + throw "Error locator degree does not match number of roots"; + } + return result; + } + this.findErrorMagnitudes=function( errorEvaluator, errorLocations, dataMatrix) + { + // This is directly applying Forney's Formula + var s = errorLocations.length; + var result = new Array(s); + for (var i = 0; i < s; i++) + { + var xiInverse = this.field.inverse(errorLocations[i]); + var denominator = 1; + for (var j = 0; j < s; j++) + { + if (i != j) + { + denominator = this.field.multiply(denominator, GF256.addOrSubtract(1, this.field.multiply(errorLocations[j], xiInverse))); + } + } + result[i] = this.field.multiply(errorEvaluator.evaluateAt(xiInverse), this.field.inverse(denominator)); + // Thanks to sanfordsquires for this fix: + if (dataMatrix) + { + result[i] = this.field.multiply(result[i], xiInverse); + } + } + return result; + } +} \ No newline at end of file diff --git a/js/jsqrcode/version.js b/js/jsqrcode/version.js new file mode 100644 index 000000000..4e19c7f1a --- /dev/null +++ b/js/jsqrcode/version.js @@ -0,0 +1,261 @@ +/* + Ported to JavaScript by Lazar Laszlo 2011 + + lazarsoft@gmail.com, www.lazarsoft.info + +*/ + +/* +* +* Copyright 2007 ZXing authors +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + + + +function ECB(count, dataCodewords) +{ + this.count = count; + this.dataCodewords = dataCodewords; + + this.__defineGetter__("Count", function() + { + return this.count; + }); + this.__defineGetter__("DataCodewords", function() + { + return this.dataCodewords; + }); +} + +function ECBlocks( ecCodewordsPerBlock, ecBlocks1, ecBlocks2) +{ + this.ecCodewordsPerBlock = ecCodewordsPerBlock; + if(ecBlocks2) + this.ecBlocks = new Array(ecBlocks1, ecBlocks2); + else + this.ecBlocks = new Array(ecBlocks1); + + this.__defineGetter__("ECCodewordsPerBlock", function() + { + return this.ecCodewordsPerBlock; + }); + + this.__defineGetter__("TotalECCodewords", function() + { + return this.ecCodewordsPerBlock * this.NumBlocks; + }); + + this.__defineGetter__("NumBlocks", function() + { + var total = 0; + for (var i = 0; i < this.ecBlocks.length; i++) + { + total += this.ecBlocks[i].length; + } + return total; + }); + + this.getECBlocks=function() + { + return this.ecBlocks; + } +} + +function Version( versionNumber, alignmentPatternCenters, ecBlocks1, ecBlocks2, ecBlocks3, ecBlocks4) +{ + this.versionNumber = versionNumber; + this.alignmentPatternCenters = alignmentPatternCenters; + this.ecBlocks = new Array(ecBlocks1, ecBlocks2, ecBlocks3, ecBlocks4); + + var total = 0; + var ecCodewords = ecBlocks1.ECCodewordsPerBlock; + var ecbArray = ecBlocks1.getECBlocks(); + for (var i = 0; i < ecbArray.length; i++) + { + var ecBlock = ecbArray[i]; + total += ecBlock.Count * (ecBlock.DataCodewords + ecCodewords); + } + this.totalCodewords = total; + + this.__defineGetter__("VersionNumber", function() + { + return this.versionNumber; + }); + + this.__defineGetter__("AlignmentPatternCenters", function() + { + return this.alignmentPatternCenters; + }); + this.__defineGetter__("TotalCodewords", function() + { + return this.totalCodewords; + }); + this.__defineGetter__("DimensionForVersion", function() + { + return 17 + 4 * this.versionNumber; + }); + + this.buildFunctionPattern=function() + { + var dimension = this.DimensionForVersion; + var bitMatrix = new BitMatrix(dimension); + + // Top left finder pattern + separator + format + bitMatrix.setRegion(0, 0, 9, 9); + // Top right finder pattern + separator + format + bitMatrix.setRegion(dimension - 8, 0, 8, 9); + // Bottom left finder pattern + separator + format + bitMatrix.setRegion(0, dimension - 8, 9, 8); + + // Alignment patterns + var max = this.alignmentPatternCenters.length; + for (var x = 0; x < max; x++) + { + var i = this.alignmentPatternCenters[x] - 2; + for (var y = 0; y < max; y++) + { + if ((x == 0 && (y == 0 || y == max - 1)) || (x == max - 1 && y == 0)) + { + // No alignment patterns near the three finder paterns + continue; + } + bitMatrix.setRegion(this.alignmentPatternCenters[y] - 2, i, 5, 5); + } + } + + // Vertical timing pattern + bitMatrix.setRegion(6, 9, 1, dimension - 17); + // Horizontal timing pattern + bitMatrix.setRegion(9, 6, dimension - 17, 1); + + if (this.versionNumber > 6) + { + // Version info, top right + bitMatrix.setRegion(dimension - 11, 0, 3, 6); + // Version info, bottom left + bitMatrix.setRegion(0, dimension - 11, 6, 3); + } + + return bitMatrix; + } + this.getECBlocksForLevel=function( ecLevel) + { + return this.ecBlocks[ecLevel.ordinal()]; + } +} + +Version.VERSION_DECODE_INFO = new Array(0x07C94, 0x085BC, 0x09A99, 0x0A4D3, 0x0BBF6, 0x0C762, 0x0D847, 0x0E60D, 0x0F928, 0x10B78, 0x1145D, 0x12A17, 0x13532, 0x149A6, 0x15683, 0x168C9, 0x177EC, 0x18EC4, 0x191E1, 0x1AFAB, 0x1B08E, 0x1CC1A, 0x1D33F, 0x1ED75, 0x1F250, 0x209D5, 0x216F0, 0x228BA, 0x2379F, 0x24B0B, 0x2542E, 0x26A64, 0x27541, 0x28C69); + +Version.VERSIONS = buildVersions(); + +Version.getVersionForNumber=function( versionNumber) +{ + if (versionNumber < 1 || versionNumber > 40) + { + throw "ArgumentException"; + } + return Version.VERSIONS[versionNumber - 1]; +} + +Version.getProvisionalVersionForDimension=function(dimension) +{ + if (dimension % 4 != 1) + { + throw "Error getProvisionalVersionForDimension"; + } + try + { + return Version.getVersionForNumber((dimension - 17) >> 2); + } + catch ( iae) + { + throw "Error getVersionForNumber"; + } +} + +Version.decodeVersionInformation=function( versionBits) +{ + var bestDifference = 0xffffffff; + var bestVersion = 0; + for (var i = 0; i < Version.VERSION_DECODE_INFO.length; i++) + { + var targetVersion = Version.VERSION_DECODE_INFO[i]; + // Do the version info bits match exactly? done. + if (targetVersion == versionBits) + { + return this.getVersionForNumber(i + 7); + } + // Otherwise see if this is the closest to a real version info bit string + // we have seen so far + var bitsDifference = FormatInformation.numBitsDiffering(versionBits, targetVersion); + if (bitsDifference < bestDifference) + { + bestVersion = i + 7; + bestDifference = bitsDifference; + } + } + // We can tolerate up to 3 bits of error since no two version info codewords will + // differ in less than 4 bits. + if (bestDifference <= 3) + { + return this.getVersionForNumber(bestVersion); + } + // If we didn't find a close enough match, fail + return null; +} + +function buildVersions() +{ + return new Array(new Version(1, new Array(), new ECBlocks(7, new ECB(1, 19)), new ECBlocks(10, new ECB(1, 16)), new ECBlocks(13, new ECB(1, 13)), new ECBlocks(17, new ECB(1, 9))), + new Version(2, new Array(6, 18), new ECBlocks(10, new ECB(1, 34)), new ECBlocks(16, new ECB(1, 28)), new ECBlocks(22, new ECB(1, 22)), new ECBlocks(28, new ECB(1, 16))), + new Version(3, new Array(6, 22), new ECBlocks(15, new ECB(1, 55)), new ECBlocks(26, new ECB(1, 44)), new ECBlocks(18, new ECB(2, 17)), new ECBlocks(22, new ECB(2, 13))), + new Version(4, new Array(6, 26), new ECBlocks(20, new ECB(1, 80)), new ECBlocks(18, new ECB(2, 32)), new ECBlocks(26, new ECB(2, 24)), new ECBlocks(16, new ECB(4, 9))), + new Version(5, new Array(6, 30), new ECBlocks(26, new ECB(1, 108)), new ECBlocks(24, new ECB(2, 43)), new ECBlocks(18, new ECB(2, 15), new ECB(2, 16)), new ECBlocks(22, new ECB(2, 11), new ECB(2, 12))), + new Version(6, new Array(6, 34), new ECBlocks(18, new ECB(2, 68)), new ECBlocks(16, new ECB(4, 27)), new ECBlocks(24, new ECB(4, 19)), new ECBlocks(28, new ECB(4, 15))), + new Version(7, new Array(6, 22, 38), new ECBlocks(20, new ECB(2, 78)), new ECBlocks(18, new ECB(4, 31)), new ECBlocks(18, new ECB(2, 14), new ECB(4, 15)), new ECBlocks(26, new ECB(4, 13), new ECB(1, 14))), + new Version(8, new Array(6, 24, 42), new ECBlocks(24, new ECB(2, 97)), new ECBlocks(22, new ECB(2, 38), new ECB(2, 39)), new ECBlocks(22, new ECB(4, 18), new ECB(2, 19)), new ECBlocks(26, new ECB(4, 14), new ECB(2, 15))), + new Version(9, new Array(6, 26, 46), new ECBlocks(30, new ECB(2, 116)), new ECBlocks(22, new ECB(3, 36), new ECB(2, 37)), new ECBlocks(20, new ECB(4, 16), new ECB(4, 17)), new ECBlocks(24, new ECB(4, 12), new ECB(4, 13))), + new Version(10, new Array(6, 28, 50), new ECBlocks(18, new ECB(2, 68), new ECB(2, 69)), new ECBlocks(26, new ECB(4, 43), new ECB(1, 44)), new ECBlocks(24, new ECB(6, 19), new ECB(2, 20)), new ECBlocks(28, new ECB(6, 15), new ECB(2, 16))), + new Version(11, new Array(6, 30, 54), new ECBlocks(20, new ECB(4, 81)), new ECBlocks(30, new ECB(1, 50), new ECB(4, 51)), new ECBlocks(28, new ECB(4, 22), new ECB(4, 23)), new ECBlocks(24, new ECB(3, 12), new ECB(8, 13))), + new Version(12, new Array(6, 32, 58), new ECBlocks(24, new ECB(2, 92), new ECB(2, 93)), new ECBlocks(22, new ECB(6, 36), new ECB(2, 37)), new ECBlocks(26, new ECB(4, 20), new ECB(6, 21)), new ECBlocks(28, new ECB(7, 14), new ECB(4, 15))), + new Version(13, new Array(6, 34, 62), new ECBlocks(26, new ECB(4, 107)), new ECBlocks(22, new ECB(8, 37), new ECB(1, 38)), new ECBlocks(24, new ECB(8, 20), new ECB(4, 21)), new ECBlocks(22, new ECB(12, 11), new ECB(4, 12))), + new Version(14, new Array(6, 26, 46, 66), new ECBlocks(30, new ECB(3, 115), new ECB(1, 116)), new ECBlocks(24, new ECB(4, 40), new ECB(5, 41)), new ECBlocks(20, new ECB(11, 16), new ECB(5, 17)), new ECBlocks(24, new ECB(11, 12), new ECB(5, 13))), + new Version(15, new Array(6, 26, 48, 70), new ECBlocks(22, new ECB(5, 87), new ECB(1, 88)), new ECBlocks(24, new ECB(5, 41), new ECB(5, 42)), new ECBlocks(30, new ECB(5, 24), new ECB(7, 25)), new ECBlocks(24, new ECB(11, 12), new ECB(7, 13))), + new Version(16, new Array(6, 26, 50, 74), new ECBlocks(24, new ECB(5, 98), new ECB(1, 99)), new ECBlocks(28, new ECB(7, 45), new ECB(3, 46)), new ECBlocks(24, new ECB(15, 19), new ECB(2, 20)), new ECBlocks(30, new ECB(3, 15), new ECB(13, 16))), + new Version(17, new Array(6, 30, 54, 78), new ECBlocks(28, new ECB(1, 107), new ECB(5, 108)), new ECBlocks(28, new ECB(10, 46), new ECB(1, 47)), new ECBlocks(28, new ECB(1, 22), new ECB(15, 23)), new ECBlocks(28, new ECB(2, 14), new ECB(17, 15))), + new Version(18, new Array(6, 30, 56, 82), new ECBlocks(30, new ECB(5, 120), new ECB(1, 121)), new ECBlocks(26, new ECB(9, 43), new ECB(4, 44)), new ECBlocks(28, new ECB(17, 22), new ECB(1, 23)), new ECBlocks(28, new ECB(2, 14), new ECB(19, 15))), + new Version(19, new Array(6, 30, 58, 86), new ECBlocks(28, new ECB(3, 113), new ECB(4, 114)), new ECBlocks(26, new ECB(3, 44), new ECB(11, 45)), new ECBlocks(26, new ECB(17, 21), new ECB(4, 22)), new ECBlocks(26, new ECB(9, 13), new ECB(16, 14))), + new Version(20, new Array(6, 34, 62, 90), new ECBlocks(28, new ECB(3, 107), new ECB(5, 108)), new ECBlocks(26, new ECB(3, 41), new ECB(13, 42)), new ECBlocks(30, new ECB(15, 24), new ECB(5, 25)), new ECBlocks(28, new ECB(15, 15), new ECB(10, 16))), + new Version(21, new Array(6, 28, 50, 72, 94), new ECBlocks(28, new ECB(4, 116), new ECB(4, 117)), new ECBlocks(26, new ECB(17, 42)), new ECBlocks(28, new ECB(17, 22), new ECB(6, 23)), new ECBlocks(30, new ECB(19, 16), new ECB(6, 17))), + new Version(22, new Array(6, 26, 50, 74, 98), new ECBlocks(28, new ECB(2, 111), new ECB(7, 112)), new ECBlocks(28, new ECB(17, 46)), new ECBlocks(30, new ECB(7, 24), new ECB(16, 25)), new ECBlocks(24, new ECB(34, 13))), + new Version(23, new Array(6, 30, 54, 74, 102), new ECBlocks(30, new ECB(4, 121), new ECB(5, 122)), new ECBlocks(28, new ECB(4, 47), new ECB(14, 48)), new ECBlocks(30, new ECB(11, 24), new ECB(14, 25)), new ECBlocks(30, new ECB(16, 15), new ECB(14, 16))), + new Version(24, new Array(6, 28, 54, 80, 106), new ECBlocks(30, new ECB(6, 117), new ECB(4, 118)), new ECBlocks(28, new ECB(6, 45), new ECB(14, 46)), new ECBlocks(30, new ECB(11, 24), new ECB(16, 25)), new ECBlocks(30, new ECB(30, 16), new ECB(2, 17))), + new Version(25, new Array(6, 32, 58, 84, 110), new ECBlocks(26, new ECB(8, 106), new ECB(4, 107)), new ECBlocks(28, new ECB(8, 47), new ECB(13, 48)), new ECBlocks(30, new ECB(7, 24), new ECB(22, 25)), new ECBlocks(30, new ECB(22, 15), new ECB(13, 16))), + new Version(26, new Array(6, 30, 58, 86, 114), new ECBlocks(28, new ECB(10, 114), new ECB(2, 115)), new ECBlocks(28, new ECB(19, 46), new ECB(4, 47)), new ECBlocks(28, new ECB(28, 22), new ECB(6, 23)), new ECBlocks(30, new ECB(33, 16), new ECB(4, 17))), + new Version(27, new Array(6, 34, 62, 90, 118), new ECBlocks(30, new ECB(8, 122), new ECB(4, 123)), new ECBlocks(28, new ECB(22, 45), new ECB(3, 46)), new ECBlocks(30, new ECB(8, 23), new ECB(26, 24)), new ECBlocks(30, new ECB(12, 15), new ECB(28, 16))), + new Version(28, new Array(6, 26, 50, 74, 98, 122), new ECBlocks(30, new ECB(3, 117), new ECB(10, 118)), new ECBlocks(28, new ECB(3, 45), new ECB(23, 46)), new ECBlocks(30, new ECB(4, 24), new ECB(31, 25)), new ECBlocks(30, new ECB(11, 15), new ECB(31, 16))), + new Version(29, new Array(6, 30, 54, 78, 102, 126), new ECBlocks(30, new ECB(7, 116), new ECB(7, 117)), new ECBlocks(28, new ECB(21, 45), new ECB(7, 46)), new ECBlocks(30, new ECB(1, 23), new ECB(37, 24)), new ECBlocks(30, new ECB(19, 15), new ECB(26, 16))), + new Version(30, new Array(6, 26, 52, 78, 104, 130), new ECBlocks(30, new ECB(5, 115), new ECB(10, 116)), new ECBlocks(28, new ECB(19, 47), new ECB(10, 48)), new ECBlocks(30, new ECB(15, 24), new ECB(25, 25)), new ECBlocks(30, new ECB(23, 15), new ECB(25, 16))), + new Version(31, new Array(6, 30, 56, 82, 108, 134), new ECBlocks(30, new ECB(13, 115), new ECB(3, 116)), new ECBlocks(28, new ECB(2, 46), new ECB(29, 47)), new ECBlocks(30, new ECB(42, 24), new ECB(1, 25)), new ECBlocks(30, new ECB(23, 15), new ECB(28, 16))), + new Version(32, new Array(6, 34, 60, 86, 112, 138), new ECBlocks(30, new ECB(17, 115)), new ECBlocks(28, new ECB(10, 46), new ECB(23, 47)), new ECBlocks(30, new ECB(10, 24), new ECB(35, 25)), new ECBlocks(30, new ECB(19, 15), new ECB(35, 16))), + new Version(33, new Array(6, 30, 58, 86, 114, 142), new ECBlocks(30, new ECB(17, 115), new ECB(1, 116)), new ECBlocks(28, new ECB(14, 46), new ECB(21, 47)), new ECBlocks(30, new ECB(29, 24), new ECB(19, 25)), new ECBlocks(30, new ECB(11, 15), new ECB(46, 16))), + new Version(34, new Array(6, 34, 62, 90, 118, 146), new ECBlocks(30, new ECB(13, 115), new ECB(6, 116)), new ECBlocks(28, new ECB(14, 46), new ECB(23, 47)), new ECBlocks(30, new ECB(44, 24), new ECB(7, 25)), new ECBlocks(30, new ECB(59, 16), new ECB(1, 17))), + new Version(35, new Array(6, 30, 54, 78, 102, 126, 150), new ECBlocks(30, new ECB(12, 121), new ECB(7, 122)), new ECBlocks(28, new ECB(12, 47), new ECB(26, 48)), new ECBlocks(30, new ECB(39, 24), new ECB(14, 25)),new ECBlocks(30, new ECB(22, 15), new ECB(41, 16))), + new Version(36, new Array(6, 24, 50, 76, 102, 128, 154), new ECBlocks(30, new ECB(6, 121), new ECB(14, 122)), new ECBlocks(28, new ECB(6, 47), new ECB(34, 48)), new ECBlocks(30, new ECB(46, 24), new ECB(10, 25)), new ECBlocks(30, new ECB(2, 15), new ECB(64, 16))), + new Version(37, new Array(6, 28, 54, 80, 106, 132, 158), new ECBlocks(30, new ECB(17, 122), new ECB(4, 123)), new ECBlocks(28, new ECB(29, 46), new ECB(14, 47)), new ECBlocks(30, new ECB(49, 24), new ECB(10, 25)), new ECBlocks(30, new ECB(24, 15), new ECB(46, 16))), + new Version(38, new Array(6, 32, 58, 84, 110, 136, 162), new ECBlocks(30, new ECB(4, 122), new ECB(18, 123)), new ECBlocks(28, new ECB(13, 46), new ECB(32, 47)), new ECBlocks(30, new ECB(48, 24), new ECB(14, 25)), new ECBlocks(30, new ECB(42, 15), new ECB(32, 16))), + new Version(39, new Array(6, 26, 54, 82, 110, 138, 166), new ECBlocks(30, new ECB(20, 117), new ECB(4, 118)), new ECBlocks(28, new ECB(40, 47), new ECB(7, 48)), new ECBlocks(30, new ECB(43, 24), new ECB(22, 25)), new ECBlocks(30, new ECB(10, 15), new ECB(67, 16))), + new Version(40, new Array(6, 30, 58, 86, 114, 142, 170), new ECBlocks(30, new ECB(19, 118), new ECB(6, 119)), new ECBlocks(28, new ECB(18, 47), new ECB(31, 48)), new ECBlocks(30, new ECB(34, 24), new ECB(34, 25)), new ECBlocks(30, new ECB(20, 15), new ECB(61, 16)))); +} \ No newline at end of file diff --git a/js/qr.js b/js/qr.js new file mode 100644 index 000000000..bb8167c96 --- /dev/null +++ b/js/qr.js @@ -0,0 +1,4 @@ +var text = location.search.substr(1); +text = decodeURIComponent(text); +document.title = chrome.i18n.getMessage('extName'); +document.body.innerText = text; \ No newline at end of file diff --git a/manifest.json b/manifest.json index c928e471a..8d25e6571 100644 --- a/manifest.json +++ b/manifest.json @@ -18,6 +18,37 @@ "default_title": "__MSG_extShortName__", "default_popup": "popup.html" }, + "background": { + "scripts": [ + "js/jsqrcode/grid.js", + "js/jsqrcode/version.js", + "js/jsqrcode/detector.js", + "js/jsqrcode/formatinf.js", + "js/jsqrcode/errorlevel.js", + "js/jsqrcode/bitmat.js", + "js/jsqrcode/datablock.js", + "js/jsqrcode/bmparser.js", + "js/jsqrcode/datamask.js", + "js/jsqrcode/rsdecoder.js", + "js/jsqrcode/gf256poly.js", + "js/jsqrcode/gf256.js", + "js/jsqrcode/decoder.js", + "js/jsqrcode/qrcode.js", + "js/jsqrcode/findpat.js", + "js/jsqrcode/alignpat.js", + "js/jsqrcode/databr.js", + "js/md5.js", + "js/aes.js", + "js/sha.js", + "js/qrcode.js", + "build/models/encryption.js", + "build/models/interface.js", + "build/models/otp.js", + "build/models/storage.js", + "build/background.js" + ], + "persistent": false + }, "content_scripts": [ { "matches": [""], diff --git a/popup.html b/popup.html index a6dc62d05..cac4c71fd 100644 --- a/popup.html +++ b/popup.html @@ -74,7 +74,7 @@
-
{{ i18n.add_qr }}
+
{{ i18n.add_qr }}
{{ i18n.add_secret }}
diff --git a/qr.html b/qr.html new file mode 100644 index 000000000..950956940 --- /dev/null +++ b/qr.html @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/background.ts b/src/background.ts new file mode 100644 index 000000000..829a293a0 --- /dev/null +++ b/src/background.ts @@ -0,0 +1,116 @@ +/* tslint:disable:no-reference */ +/// +/// +/// +/// + +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'position') { + if (!sender.tab) { + return; + } + getQr( + sender.tab, message.info.left, message.info.top, message.info.width, + message.info.height, message.info.windowWidth); + } +}); + +let contentTab: chrome.tabs.Tab; + +function getQr( + tab: chrome.tabs.Tab, left: number, top: number, width: number, + height: number, windowWidth: number) { + chrome.tabs.captureVisibleTab(tab.windowId, {format: 'png'}, (dataUrl) => { + contentTab = tab; + 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(); + qrcode.callback = getTotp; + qrcode.decode(url); + }; + }); +} + +async function getTotp(text: string) { + const id = contentTab.id; + if (!id) { + return; + } + + if (text.indexOf('otpauth://') !== 0) { + if (text === 'error decoding QR Code') { + chrome.tabs.sendMessage(id, {action: 'errorqr'}); + } else { + chrome.tabs.sendMessage(id, {action: 'text', text}); + } + } else { + let uri = text.split('otpauth://')[1]; + let type = uri.substr(0, 4).toLowerCase(); + uri = uri.substr(5); + let label = uri.split('?')[0]; + const parameterPart = uri.split('?')[1]; + if (!label || !parameterPart) { + chrome.tabs.sendMessage(id, {action: 'errorqr'}); + } else { + let account = ''; + let secret = ''; + let issuer = ''; + + label = decodeURIComponent(label); + if (label.indexOf(':') !== -1) { + issuer = label.split(':')[0]; + account = label.split(':')[1]; + } else { + account = label; + } + const parameters = parameterPart.split('&'); + parameters.forEach((item) => { + const parameter = item.split('='); + if (parameter[0].toLowerCase() === 'secret') { + secret = parameter[1]; + } else if (parameter[0].toLowerCase() === 'issuer') { + issuer = parameter[1]; + } else if (parameter[0].toLowerCase() === 'counter') { + let counter = Number(parameter[1]); + counter = (isNaN(counter) || counter < 0) ? 0 : counter; + } + }); + + if (!secret) { + chrome.tabs.sendMessage(id, {action: 'errorqr'}); + } else if ( + !/^[0-9a-f]+$/i.test(secret) && !/^[2-7a-z]+=*$/i.test(secret)) { + chrome.tabs.sendMessage(id, {action: 'secretqr', secret}); + } else { + const encryption = new Encryption(''); + const hash = CryptoJS.MD5(secret).toString(); + if (!/^[2-7a-z]+=*$/i.test(secret) && /^[0-9a-f]+$/i.test(secret)) { + type = 'hex'; + } + const entryData: {[hash: string]: OTPStorage} = {}; + entryData[hash] = { + account, + hash, + issuer, + secret, + type, + encrypted: false, + index: 0, + counter: 0 + }; + await EntryStorage.import(encryption, entryData); + chrome.tabs.sendMessage(id, {action: 'added', account}); + } + } + } + return; +} \ No newline at end of file diff --git a/src/content.ts b/src/content.ts index e69de29bb..951f64f62 100644 --- a/src/content.ts +++ b/src/content.ts @@ -0,0 +1,139 @@ +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.action === 'capture') { + sendResponse('beginCapture'); + showGrayLayout(); + } else if (message.action === 'errorsecret') { + alert(chrome.i18n.getMessage('errorsecret') + message.secret); + } else if (message.action === 'errorqr') { + alert(chrome.i18n.getMessage('errorqr')); + } else if (message.action === 'added') { + alert(message.account + chrome.i18n.getMessage('added')); + } else if (message.action === 'text') { + showQrCode(message.text); + } +}); + +interface CaptureBoxPosition { + left: number; + top: number; +} + +let captureBoxPosition: CaptureBoxPosition = {left: 0, top: 0}; + +function showGrayLayout() { + let grayLayout = document.getElementById('__ga_grayLayout__'); + if (!grayLayout) { + grayLayout = document.createElement('div'); + grayLayout.id = '__ga_grayLayout__'; + document.body.appendChild(grayLayout); + const scan = document.createElement('div'); + scan.className = 'scan'; + scan.style.background = 'url(' + + chrome.extension.getURL('images/scan.gif') + ') no-repeat center'; + grayLayout.appendChild(scan); + const captureBox = document.createElement('div'); + captureBox.id = '__ga_captureBox__'; + grayLayout.appendChild(captureBox); + grayLayout.onmousedown = grayLayoutDown; + grayLayout.onmousemove = grayLayoutMove; + grayLayout.onmouseup = grayLayoutUp; + grayLayout.oncontextmenu = (event) => { + event.preventDefault(); + return; + }; + } + grayLayout.style.display = 'block'; +} + +function grayLayoutDown(event: MouseEvent) { + if (event.button === 1 || event.button === 2) { + event.preventDefault(); + return; + } + const captureBox = document.getElementById('__ga_captureBox__'); + if (!captureBox) { + return; + } + + captureBoxPosition.left = event.clientX; + captureBoxPosition.top = event.clientY; + captureBox.style.left = event.clientX + 'px'; + captureBox.style.top = event.clientY + 'px'; + captureBox.style.width = '1px'; + captureBox.style.height = '1px'; + captureBox.style.display = 'block'; + return; +} + +function grayLayoutMove(event: MouseEvent) { + if (event.button === 1 || event.button === 2) { + event.preventDefault(); + return; + } + const captureBox = document.getElementById('__ga_captureBox__'); + if (!captureBox) { + return; + } + + const captureBoxLeft = Math.min(captureBoxPosition.left, event.clientX); + const captureBoxTop = Math.min(captureBoxPosition.top, event.clientY); + const captureBoxWidth = Math.abs(captureBoxPosition.left - event.clientX) - 1; + const captureBoxHeight = Math.abs(captureBoxPosition.top - event.clientY) - 1; + captureBox.style.left = captureBoxLeft + 'px'; + captureBox.style.top = captureBoxTop + 'px'; + captureBox.style.width = captureBoxWidth + 'px'; + captureBox.style.height = captureBoxHeight + 'px'; + return; +} + +function grayLayoutUp(event: MouseEvent) { + const grayLayout = document.getElementById('__ga_grayLayout__'); + const captureBox = document.getElementById('__ga_captureBox__'); + if (!captureBox || !grayLayout) { + return; + } + + let captureBoxLeft = Math.min(captureBoxPosition.left, event.clientX) + 1; + let captureBoxTop = Math.min(captureBoxPosition.top, event.clientY) + 1; + let captureBoxWidth = Math.abs(captureBoxPosition.left - event.clientX) - 1; + let captureBoxHeight = Math.abs(captureBoxPosition.top - event.clientY) - 1; + captureBoxLeft *= window.devicePixelRatio; + captureBoxTop *= window.devicePixelRatio; + captureBoxWidth *= window.devicePixelRatio; + captureBoxHeight *= window.devicePixelRatio; + + if (event.button === 1 || event.button === 2) { + event.preventDefault(); + return; + } + + setTimeout(() => { + captureBox.style.display = 'none'; + grayLayout.style.display = 'none'; + }, 100); + // make sure captureBox and grayLayout is hidden + setTimeout(() => { + sendPosition( + captureBoxLeft, captureBoxTop, captureBoxWidth, captureBoxHeight); + }, 200); + return false; +} + +function sendPosition( + left: number, top: number, width: number, height: number) { + chrome.runtime.sendMessage({ + action: 'position', + info: {left, top, width, height, windowWidth: window.innerWidth} + }); +} + +function showQrCode(msg: string) { + const left = (screen.width / 2) - 200; + const top = (screen.height / 2) - 100; + const url = + chrome.extension.getURL('qr.html') + '?' + encodeURIComponent(msg); + window.open( + url, '_blank', + 'toolbar=no, location=no, status=no, menubar=no, scrollbars=yes, copyhistory=no, width=400, height=200, left=' + + left + ',top=' + top); +} \ No newline at end of file diff --git a/src/popup.ts b/src/popup.ts index 94ae39813..e8e6a5d45 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -16,24 +16,8 @@ async function getEntries(encryption: Encryption) { return optEntries; } -async function getVersion() { - return new Promise( - (resolve: (value: string) => void, reject: (reason: Error) => void) => { - try { - const xhr = new XMLHttpRequest(); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - const manifest: {version: string} = JSON.parse(xhr.responseText); - return resolve(manifest.version); - } - return; - }; - xhr.open('GET', chrome.extension.getURL('/manifest.json')); - xhr.send(); - } catch (error) { - return reject(error); - } - }); +function getVersion() { + return chrome.runtime.getManifest().version; } async function loadI18nMessages() { @@ -184,7 +168,7 @@ async function init() { const zoom = Number(localStorage.zoom) || 100; resize(zoom); - const version = await getVersion(); + const version = getVersion(); const i18n = await loadI18nMessages(); const encryption: Encryption = new Encryption(''); const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); @@ -446,6 +430,22 @@ async function init() { authenticator.newPassphrase.phrase); await authenticator.importEnties(); return; + }, + beginCapture: () => { + chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { + const tab = tabs[0]; + if (!tab || !tab.id) { + return; + } + chrome.tabs.sendMessage(tab.id, {action: 'capture'}, (result) => { + if (result !== 'beginCapture') { + authenticator.message = authenticator.i18n.capture_failed; + } else { + window.close(); + } + }); + }); + return; } } }); From 64dc6d8fb9cc24d219d0c6c2b0ef318db8636cf0 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Wed, 7 Feb 2018 02:16:05 +0800 Subject: [PATCH 027/178] save --- _locales/en/messages.json | 364 +++++++++++++++++++------------------- _locales/fr/messages.json | 182 +++++++++++++++++++ src/popup.ts | 11 ++ 3 files changed, 375 insertions(+), 182 deletions(-) create mode 100644 _locales/fr/messages.json diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9470e288d..b418ebc79 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1,182 +1,182 @@ -{ - "extName": { - "message": "Authenticator", - "description": "Extension Name." - }, - "extShortName": { - "message": "Authenticator", - "description": "Extension Short Name." - }, - "extDesc": { - "message": "For Google Authenticator and Battle.net Authenticator.", - "description": "Extension Description." - }, - "added": { - "message": " has been added.", - "description": "Added Account." - }, - "errorqr": { - "message": "Unrecognized QR code.", - "description": "QR Error." - }, - "errorsecret": { - "message": "Secret Error. Only Base32(A-Z, 2-7 and =) and HEX(0-9 and A-F) are supported. However, your secret is: ", - "description": "Secret Error." - }, - "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2014 Sneezry. Released under the Apache License 2.0.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", - "description": "Information." - }, - "add_qr": { - "message": "Scan QR Code", - "description": "Scan QR Code." - }, - "add_secret": { - "message": "Manual Entry", - "description": "Manual Entry." - }, - "close": { - "message": "Close", - "description": "Close." - }, - "ok": { - "message": "OK", - "description": "OK." - }, - "err_acc_sec": { - "message": "Please input Account and Secret.", - "description": "Input Account and Secret." - }, - "account": { - "message": "Account", - "description": "Account." - }, - "secret": { - "message": "Secret", - "description": "Secret." - }, - "updateSuccess": { - "message": "Success.", - "description": "Update Success." - }, - "updateFailure": { - "message": "Failure.", - "description": "Update Failure." - }, - "about": { - "message": "About", - "description": "About." - }, - "export_import": { - "message": "Export / Import", - "description": "Export and Import." - }, - "settings": { - "message": "Settings", - "description": "Settings." - }, - "security": { - "message": "Security", - "description": "Security." - }, - "current_phrase": { - "message": "Current Passphrase", - "description": "Current Passphrase." - }, - "new_phrase": { - "message": "New Passphrase", - "description": "New Passphrase." - }, - "phrase": { - "message": "Passphrase", - "description": "Passphrase." - }, - "confirm_phrase": { - "message": "Confirm Passphrase", - "description": "Confirmm Passphrase." - }, - "security_warning": { - "message": "This passphrase will be used to encrypt your secrets. No one can help you if you forget the passphrase.", - "description": "Passphrase Warning." - }, - "update": { - "message": "Update", - "description": "Update." - }, - "phrase_incorrect": { - "message": "Some accounts and passphrase do not match.", - "description": "Passphrase Incorrect." - }, - "phrase_not_match": { - "message": "Two passphrases do not match.", - "description": "Passphrase Not Match." - }, - "encrypted": { - "message": "Encrypted", - "description": "Encrypted." - }, - "copied": { - "message": "Copied", - "description": "Copied." - }, - "feedback": { - "message": "Feedback", - "description": "Feedback." - }, - "translate": { - "message": "Translate", - "description": "Translate." - }, - "source": { - "message": "Source Code", - "description": "Source Code." - }, - "passphrase_info": { - "message": "Input passphrase to decrypt account data.", - "description": "Passphrase Info" - }, - "sync_clock": { - "message": "Sync Clock with Google", - "description": "Sync Clock" - }, - "remember_phrase": { - "message": "Remember Passphrase", - "description": "Remember Passphrase" - }, - "clock_too_far_off": { - "message": "Caution! Your local clock is too far off, please fix it before continuing.", - "description": "Local Time is Too Far Off" - }, - "remind_backup": { - "message": "NEVER REINSTALL THE EXTENSION TO TRY TO FIX ANY ISSUE, OR YOU WILL LOSE ALL YOUR DATA! Do you have a backup for your secrets? Please note that no one can help you with getting back locked account, don't wait until it's too late. We will remind you to make a backup again after 30 days.", - "description": "Remind Backup" - }, - "capture_failed": { - "message": "Capture failed, please reload the page you are veiwing and try again.", - "description": "Capture Failed" - }, - "unencrypted_secret_warning": { - "message": "This secret is not encrypted! Click here to set a passphrase to fix this issue.", - "description": "Unencrypted Secret Warning" - }, - "based_on_time": { - "message": "Time Based", - "description": "Time Based" - }, - "based_on_counter": { - "message": "Counter Based", - "description": "Counter Based" - }, - "resize_popup_page": { - "message": "Resize Popup Page", - "description": "Resize Popup Page" - }, - "scale": { - "message": "Scale", - "description": "Scale" - }, - "export_info": { - "message": "Copy this text and save it somewhere else to backup your secrets. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", - "description": "Export menu info text" - } -} +{ + "extName": { + "message": "Authenticator", + "description": "Extension Name." + }, + "extShortName": { + "message": "Authenticator", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "For Google Authenticator and Battle.net Authenticator.", + "description": "Extension Description." + }, + "added": { + "message": " has been added.", + "description": "Added Account." + }, + "errorqr": { + "message": "Unrecognized QR code.", + "description": "QR Error." + }, + "errorsecret": { + "message": "Secret Error. Only Base32(A-Z, 2-7 and =) and HEX(0-9 and A-F) are supported. However, your secret is: ", + "description": "Secret Error." + }, + "info": { + "message": "

Authenticator for Google™ Authenticator,
© 2014 Sneezry. Released under the Apache License 2.0.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", + "description": "Information." + }, + "add_qr": { + "message": "Scan QR Code", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "Manual Entry", + "description": "Manual Entry." + }, + "close": { + "message": "Close", + "description": "Close." + }, + "ok": { + "message": "OK", + "description": "OK." + }, + "err_acc_sec": { + "message": "Please input Account and Secret.", + "description": "Input Account and Secret." + }, + "account": { + "message": "Account", + "description": "Account." + }, + "secret": { + "message": "Secret", + "description": "Secret." + }, + "updateSuccess": { + "message": "Success.", + "description": "Update Success." + }, + "updateFailure": { + "message": "Failure.", + "description": "Update Failure." + }, + "about": { + "message": "About", + "description": "About." + }, + "export_import": { + "message": "Export / Import", + "description": "Export and Import." + }, + "settings": { + "message": "Settings", + "description": "Settings." + }, + "security": { + "message": "Security", + "description": "Security." + }, + "current_phrase": { + "message": "Current Passphrase", + "description": "Current Passphrase." + }, + "new_phrase": { + "message": "New Passphrase", + "description": "New Passphrase." + }, + "phrase": { + "message": "Passphrase", + "description": "Passphrase." + }, + "confirm_phrase": { + "message": "Confirm Passphrase", + "description": "Confirmm Passphrase." + }, + "security_warning": { + "message": "This passphrase will be used to encrypt your secrets. No one can help you if you forget the passphrase.", + "description": "Passphrase Warning." + }, + "update": { + "message": "Update", + "description": "Update." + }, + "phrase_incorrect": { + "message": "Some accounts and passphrase do not match.", + "description": "Passphrase Incorrect." + }, + "phrase_not_match": { + "message": "Two passphrases do not match.", + "description": "Passphrase Not Match." + }, + "encrypted": { + "message": "Encrypted", + "description": "Encrypted." + }, + "copied": { + "message": "Copied", + "description": "Copied." + }, + "feedback": { + "message": "Feedback", + "description": "Feedback." + }, + "translate": { + "message": "Translate", + "description": "Translate." + }, + "source": { + "message": "Source Code", + "description": "Source Code." + }, + "passphrase_info": { + "message": "Input passphrase to decrypt account data.", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "Sync Clock with Google", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "Remember Passphrase", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "Caution! Your local clock is too far off, please fix it before continuing.", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "NEVER REINSTALL THE EXTENSION TO TRY TO FIX ANY ISSUE, OR YOU WILL LOSE ALL YOUR DATA! Do you have a backup for your secrets? Please note that no one can help you with getting back locked account, don't wait until it's too late. We will remind you to make a backup again after 30 days.", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "Capture failed, please reload the page you are veiwing and try again.", + "description": "Capture Failed" + }, + "unencrypted_secret_warning": { + "message": "This secret is not encrypted! Click here to set a passphrase to fix this issue.", + "description": "Unencrypted Secret Warning" + }, + "based_on_time": { + "message": "Time Based", + "description": "Time Based" + }, + "based_on_counter": { + "message": "Counter Based", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "Resize Popup Page", + "description": "Resize Popup Page" + }, + "scale": { + "message": "Scale", + "description": "Scale" + }, + "export_info": { + "message": "Copy this text and save it somewhere else to backup your secrets. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", + "description": "Export menu info text" + } +} diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json new file mode 100644 index 000000000..3dfb0e2dd --- /dev/null +++ b/_locales/fr/messages.json @@ -0,0 +1,182 @@ +{ + "extName": { + "message": "S’authentifier", + "description": "Extension Name." + }, + "extShortName": { + "message": "S’authentifier", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "Pour Google Authenticator et Battle.net Authenticator.", + "description": "Extension Description." + }, + "added": { + "message": " a bien été ajouté.", + "description": "Added Account." + }, + "errorqr": { + "message": "Code QR non reconnu.", + "description": "QR Error." + }, + "errorsecret": { + "message": "Erreur de secret. Seul Base32 (A-Z, 2-7 =) et HEX (0-9 et A-F) sont pris en charge. Toutefois, votre secret est : ", + "description": "Secret Error." + }, + "info": { + "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2014 Sneezry<\/a>. Released under the Apache License 2.0.<\/p>

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.<\/p>

jsqrcode<\/a> Copyright jsqrcode authors. Licensed under the Apache License.<\/p>

ZXing<\/a> Copyright ZXing authors. Licensed under the Apache License.<\/p>

totp.js<\/a> Copyright its author. Licensed under the MIT License.<\/p>

jsSHA<\/a> Copyright jsSHA authors. Licensed under the BSD License.<\/p>

qrcode.js<\/a> Copyright qrcode.js author. Licensed under the MIT License.<\/p>

crypto-js<\/a> Copyright crypto-js author. Licensed under the BSD License.<\/p>

Droid Sans Mono<\/a> Copyright Steve Matteson. Licensed under the Apache License.<\/p>

Font Awesome<\/a> Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.<\/p>

Thanks to Mike Robinson <3<\/p>", + "description": "Information." + }, + "add_qr": { + "message": "Scanner le QR Code", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "Saisie Manuelle", + "description": "Manual Entry." + }, + "close": { + "message": "Fermer", + "description": "Close." + }, + "ok": { + "message": "OK", + "description": "OK." + }, + "err_acc_sec": { + "message": "Entrez le Compte ainsi que le Secret.", + "description": "Input Account and Secret." + }, + "account": { + "message": "Compte", + "description": "Account." + }, + "secret": { + "message": "Secret", + "description": "Secret." + }, + "updateSuccess": { + "message": "Succès.", + "description": "Update Success." + }, + "updateFailure": { + "message": "Échec.", + "description": "Update Failure." + }, + "about": { + "message": "A propos", + "description": "About." + }, + "export_import": { + "message": "Exporter \/ Importer", + "description": "Export and Import." + }, + "settings": { + "message": "Paramètres", + "description": "Settings." + }, + "security": { + "message": "Sécurité", + "description": "Security." + }, + "current_phrase": { + "message": "Mot de passe actuel", + "description": "Current Passphrase." + }, + "new_phrase": { + "message": "Nouveau mot de passe", + "description": "New Passphrase." + }, + "phrase": { + "message": "Mot de passe", + "description": "Passphrase." + }, + "confirm_phrase": { + "message": "Confirmer le mot de passe", + "description": "Confirmm Passphrase." + }, + "security_warning": { + "message": "Ajouter un mot de passe pour crypter vos secrets. Personne ne peut vous aider si vous avez oublié votre mot de passe.", + "description": "Passphrase Warning." + }, + "update": { + "message": "Mettre à jour", + "description": "Update." + }, + "phrase_incorrect": { + "message": "Certains comptes et mots de passe ne correspondent pas.", + "description": "Passphrase Incorrect." + }, + "phrase_not_match": { + "message": "Mots de passe sont différents.", + "description": "Passphrase Not Match." + }, + "encrypted": { + "message": "Crypté", + "description": "Encrypted." + }, + "copied": { + "message": "Copié", + "description": "Copied." + }, + "feedback": { + "message": "Retours", + "description": "Feedback." + }, + "translate": { + "message": "Traduire", + "description": "Translate." + }, + "source": { + "message": "Code source", + "description": "Source Code." + }, + "passphrase_info": { + "message": "Entrez le Mot de passe pour déchiffrer les données du compte.", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "Synchroniser l'horloge avec Google", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "Mémoriser le mot de passe", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "Attention ! Votre horloge locale est trop décalée, veuillez rectifier cela avant de continuer.", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "JAMAIS RÉINSTALLER L’EXTENSION POUR TENTER DE RÉSOUDRE TOUT PROBLÈME, OU VOUS PERDREZ TOUTES VOS DONNÉES ! Vous avez une sauvegarde de vos secrets ? Veuillez noter que personne ne peut vous aider à retrouver un compte verrouillé, n’attendez pas qu’il ne soit trop tard. Nous allons rappeler vous de faire une sauvegarde après 30 jours.", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "La capture a échoué, veuillez recharger la page que vous regardez et réessayez.", + "description": "Capture Failed" + }, + "unencrypted_secret_warning": { + "message": "Ce secret n’est pas crypté ! Cliquez ici pour définir un mot de passe pour corriger cette erreur.", + "description": "Unencrypted Secret Warning" + }, + "based_on_time": { + "message": "Basé sur le temps", + "description": "Time Based" + }, + "based_on_counter": { + "message": "Basée sur un compteur", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "Redimensionner la pop-up", + "description": "Resize Popup Page" + }, + "scale": { + "message": "Mise à l'échelle", + "description": "Scale" + }, + "export_info": { + "message": "Copiez ce texte et enregistrez-le quelque part ailleurs pour sauvegarder vos secrets. Vous souhaitez ajouter un compte à une autre application ? Placez le curseur sur la partie supérieure droite de n’importe quel compte et cliquez sur le bouton caché.", + "description": "Export menu info text" + } +} \ No newline at end of file diff --git a/src/popup.ts b/src/popup.ts index e8e6a5d45..e24b930d7 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -459,6 +459,17 @@ async function init() { await updateCode(authenticator); }, 1000); + // Remind backup + const clientTime = Math.floor(new Date().getTime() / 1000 / 3600 / 24); + if (!localStorage.lastRemindingBackupTime) { + localStorage.lastRemindingBackupTime = clientTime; + } else if ( + clientTime - localStorage.lastRemindingBackupTime >= 30 || + clientTime - localStorage.lastRemindingBackupTime < 0) { + authenticator.message = authenticator.i18n.remind_backup; + localStorage.lastRemindingBackupTime = clientTime; + } + return; } From 5783beaee69ef073ba07fa7dac0e295ea60fadbc Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Wed, 7 Feb 2018 03:06:58 +0800 Subject: [PATCH 028/178] update about --- _locales/en/messages.json | 2 +- _locales/fr/messages.json | 2 +- _locales/zh_CN/messages.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b418ebc79..05c582af1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2014
Sneezry. Released under the Apache License 2.0.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", + "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", "description": "Information." }, "add_qr": { diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 3dfb0e2dd..978c905d3 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2014 Sneezry<\/a>. Released under the Apache License 2.0.<\/p>

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.<\/p>

jsqrcode<\/a> Copyright jsqrcode authors. Licensed under the Apache License.<\/p>

ZXing<\/a> Copyright ZXing authors. Licensed under the Apache License.<\/p>

totp.js<\/a> Copyright its author. Licensed under the MIT License.<\/p>

jsSHA<\/a> Copyright jsSHA authors. Licensed under the BSD License.<\/p>

qrcode.js<\/a> Copyright qrcode.js author. Licensed under the MIT License.<\/p>

crypto-js<\/a> Copyright crypto-js author. Licensed under the BSD License.<\/p>

Droid Sans Mono<\/a> Copyright Steve Matteson. Licensed under the Apache License.<\/p>

Font Awesome<\/a> Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.<\/p>

Thanks to Mike Robinson <3<\/p>", + "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2018 Sneezry<\/a>. Released under the MIT License.<\/p>

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.<\/p>

jsqrcode<\/a> Copyright jsqrcode authors. Licensed under the Apache License.<\/p>

ZXing<\/a> Copyright ZXing authors. Licensed under the Apache License.<\/p>

totp.js<\/a> Copyright its author. Licensed under the MIT License.<\/p>

jsSHA<\/a> Copyright jsSHA authors. Licensed under the BSD License.<\/p>

qrcode.js<\/a> Copyright qrcode.js author. Licensed under the MIT License.<\/p>

crypto-js<\/a> Copyright crypto-js author. Licensed under the BSD License.<\/p>

Droid Sans Mono<\/a> Copyright Steve Matteson. Licensed under the Apache License.<\/p>

Font Awesome<\/a> Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.<\/p>

Thanks to Mike Robinson <3<\/p>", "description": "Information." }, "add_qr": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 241bbff56..88a76b431 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2014
Sneezry. Released under the Apache License 2.0.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", + "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", "description": "Information." }, "add_qr": { From cd7d6c36224b9177f9662e005ba71b271ccf0dac Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Wed, 7 Feb 2018 03:11:24 +0800 Subject: [PATCH 029/178] add link to Mike Robinson --- _locales/en/messages.json | 2 +- _locales/fr/messages.json | 2 +- _locales/zh_CN/messages.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 05c582af1..c3fb0284e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", + "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", "description": "Information." }, "add_qr": { diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 978c905d3..d292bb12d 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2018 Sneezry<\/a>. Released under the MIT License.<\/p>

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.<\/p>

jsqrcode<\/a> Copyright jsqrcode authors. Licensed under the Apache License.<\/p>

ZXing<\/a> Copyright ZXing authors. Licensed under the Apache License.<\/p>

totp.js<\/a> Copyright its author. Licensed under the MIT License.<\/p>

jsSHA<\/a> Copyright jsSHA authors. Licensed under the BSD License.<\/p>

qrcode.js<\/a> Copyright qrcode.js author. Licensed under the MIT License.<\/p>

crypto-js<\/a> Copyright crypto-js author. Licensed under the BSD License.<\/p>

Droid Sans Mono<\/a> Copyright Steve Matteson. Licensed under the Apache License.<\/p>

Font Awesome<\/a> Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.<\/p>

Thanks to Mike Robinson <3<\/p>", + "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2018 Sneezry<\/a>. Released under the MIT License.<\/p>

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.<\/p>

jsqrcode<\/a> Copyright jsqrcode authors. Licensed under the Apache License.<\/p>

ZXing<\/a> Copyright ZXing authors. Licensed under the Apache License.<\/p>

totp.js<\/a> Copyright its author. Licensed under the MIT License.<\/p>

jsSHA<\/a> Copyright jsSHA authors. Licensed under the BSD License.<\/p>

qrcode.js<\/a> Copyright qrcode.js author. Licensed under the MIT License.<\/p>

crypto-js<\/a> Copyright crypto-js author. Licensed under the BSD License.<\/p>

Droid Sans Mono<\/a> Copyright Steve Matteson. Licensed under the Apache License.<\/p>

Font Awesome<\/a> Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.<\/p>

Thanks to Mike Robinson <3<\/p>", "description": "Information." }, "add_qr": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 88a76b431..55827f1cb 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", + "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", "description": "Information." }, "add_qr": { From 9630f11272cf123cdddb37ca171ca2e451a2e921 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Wed, 7 Feb 2018 03:12:19 +0800 Subject: [PATCH 030/178] add link to Mike Robinson --- _locales/en/messages.json | 2 +- _locales/fr/messages.json | 2 +- _locales/zh_CN/messages.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c3fb0284e..fd08a642b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", + "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", "description": "Information." }, "add_qr": { diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index d292bb12d..81ebc51b1 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2018 Sneezry<\/a>. Released under the MIT License.<\/p>

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.<\/p>

jsqrcode<\/a> Copyright jsqrcode authors. Licensed under the Apache License.<\/p>

ZXing<\/a> Copyright ZXing authors. Licensed under the Apache License.<\/p>

totp.js<\/a> Copyright its author. Licensed under the MIT License.<\/p>

jsSHA<\/a> Copyright jsSHA authors. Licensed under the BSD License.<\/p>

qrcode.js<\/a> Copyright qrcode.js author. Licensed under the MIT License.<\/p>

crypto-js<\/a> Copyright crypto-js author. Licensed under the BSD License.<\/p>

Droid Sans Mono<\/a> Copyright Steve Matteson. Licensed under the Apache License.<\/p>

Font Awesome<\/a> Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.<\/p>

Thanks to Mike Robinson <3<\/p>", + "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2018 Sneezry<\/a>. Released under the MIT License.<\/p>

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.<\/p>

jsqrcode<\/a> Copyright jsqrcode authors. Licensed under the Apache License.<\/p>

ZXing<\/a> Copyright ZXing authors. Licensed under the Apache License.<\/p>

totp.js<\/a> Copyright its author. Licensed under the MIT License.<\/p>

jsSHA<\/a> Copyright jsSHA authors. Licensed under the BSD License.<\/p>

qrcode.js<\/a> Copyright qrcode.js author. Licensed under the MIT License.<\/p>

crypto-js<\/a> Copyright crypto-js author. Licensed under the BSD License.<\/p>

Droid Sans Mono<\/a> Copyright Steve Matteson. Licensed under the Apache License.<\/p>

Font Awesome<\/a> Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.<\/p>

Thanks to Mike Robinson <3<\/p>", "description": "Information." }, "add_qr": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 55827f1cb..5532db0e6 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", + "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", "description": "Information." }, "add_qr": { From 7588fc33050c044d1ddf981ebaef4d5fdd9da9d4 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Wed, 7 Feb 2018 03:24:03 +0800 Subject: [PATCH 031/178] add delete confirm --- _locales/en/messages.json | 4 ++++ _locales/zh_CN/messages.json | 12 ++++++++---- src/popup.ts | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index fd08a642b..46d258ddd 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -178,5 +178,9 @@ "export_info": { "message": "Copy this text and save it somewhere else to backup your secrets. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", "description": "Export menu info text" + }, + "confirm_remove_entry" : { + "message": "Are you sure you want to delete this item? You cannot retrieve deleted secret. This action cannot be undone.", + "description": "Remove entry confirmation" } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 5532db0e6..90cfb750f 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -175,8 +175,12 @@ "message": "比例", "description": "Scale" }, - "export_info": { - "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", - "description": "Export menu info text" - } + "export_info": { + "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", + "description": "Export menu info text" + }, + "confirm_remove_entry" : { + "message": "您确定要删除此项吗?您无法找回已删除的密钥。此操作无法撤销。", + "description": "Remove entry confirmation" + } } diff --git a/src/popup.ts b/src/popup.ts index e24b930d7..428e91bfd 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -263,7 +263,7 @@ async function init() { return; }, removeEntry: async (entry: OTPEntry) => { - if (await authenticator.confirm('Remove?')) { + if (await authenticator.confirm(authenticator.i18n.confirm_remove_entry)) { await entry.delete(); await authenticator.updateEntries(); } From 801276ac967afe42d4058681b1c2e3d7e8040877 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 6 Feb 2018 17:04:34 -0600 Subject: [PATCH 032/178] Bring delete confirm dialog in line with crowdin --- _locales/en/messages.json | 16 +- _locales/fr/messages.json | 14 +- _locales/zh_CN/messages.json | 380 ++++++++++++++++++----------------- popup.html | 6 +- src/popup.ts | 4 +- 5 files changed, 224 insertions(+), 196 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 46d258ddd..32c85eae0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -43,6 +43,14 @@ "message": "OK", "description": "OK." }, + "yes": { + "message": "Yes", + "description": "Yes." + }, + "no": { + "message": "No", + "description": "No." + }, "err_acc_sec": { "message": "Please input Account and Secret.", "description": "Input Account and Secret." @@ -95,6 +103,10 @@ "message": "Confirm Passphrase", "description": "Confirmm Passphrase." }, + "confirm_delete" : { + "message": "Are you sure you want to delete this item? This action cannot be undone.", + "description": "Remove entry confirmation" + }, "security_warning": { "message": "This passphrase will be used to encrypt your secrets. No one can help you if you forget the passphrase.", "description": "Passphrase Warning." @@ -178,9 +190,5 @@ "export_info": { "message": "Copy this text and save it somewhere else to backup your secrets. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", "description": "Export menu info text" - }, - "confirm_remove_entry" : { - "message": "Are you sure you want to delete this item? You cannot retrieve deleted secret. This action cannot be undone.", - "description": "Remove entry confirmation" } } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 81ebc51b1..5147d467d 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -43,6 +43,14 @@ "message": "OK", "description": "OK." }, + "yes": { + "message": "Oui", + "description": "Yes." + }, + "no": { + "message": "Non", + "description": "No." + }, "err_acc_sec": { "message": "Entrez le Compte ainsi que le Secret.", "description": "Input Account and Secret." @@ -95,6 +103,10 @@ "message": "Confirmer le mot de passe", "description": "Confirmm Passphrase." }, + "confirm_delete": { + "message": "Souhaitez-vous vraiment supprimer ce secret?", + "description": "Confirm Delete." + }, "security_warning": { "message": "Ajouter un mot de passe pour crypter vos secrets. Personne ne peut vous aider si vous avez oublié votre mot de passe.", "description": "Passphrase Warning." @@ -179,4 +191,4 @@ "message": "Copiez ce texte et enregistrez-le quelque part ailleurs pour sauvegarder vos secrets. Vous souhaitez ajouter un compte à une autre application ? Placez le curseur sur la partie supérieure droite de n’importe quel compte et cliquez sur le bouton caché.", "description": "Export menu info text" } -} \ No newline at end of file +} diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 90cfb750f..2a5e0d0bb 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -1,186 +1,194 @@ -{ - "extName": { - "message": "身份验证器", - "description": "Extension Name." - }, - "extShortName": { - "message": "身份验证器", - "description": "Extension Short Name." - }, - "extDesc": { - "message": "适用于Google身份验证器及战网安全令。", - "description": "Extension Description." - }, - "added": { - "message": "已添加。", - "description": "Added Account." - }, - "errorqr": { - "message": "无法识别的QR码。", - "description": "QR Error." - }, - "errorsecret": { - "message": "密钥错误,仅支持Base32(A-Z,2-7及=)和HEX(0-9及A-F)格式,然而您的密钥是:", - "description": "Secret Error." - }, - "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", - "description": "Information." - }, - "add_qr": { - "message": "扫描QR码", - "description": "Scan QR Code." - }, - "add_secret": { - "message": "手动输入", - "description": "Manual Entry." - }, - "close": { - "message": "关闭", - "description": "Close." - }, - "ok": { - "message": "确定", - "description": "OK." - }, - "err_acc_sec": { - "message": "请输入账户和密钥。", - "description": "Input Account and Secret." - }, - "account": { - "message": "账户", - "description": "Account." - }, - "secret": { - "message": "密钥", - "description": "Secret." - }, - "updateSuccess": { - "message": "成功。", - "description": "Update Success." - }, - "updateFailure": { - "message": "失败。", - "description": "Update Failure." - }, - "about": { - "message": "关于", - "description": "About." - }, - "export_import": { - "message": "导出 / 导入", - "description": "Export and Import." - }, - "settings": { - "message": "设置", - "description": "Settings." - }, - "security": { - "message": "安全", - "description": "Security." - }, - "current_phrase": { - "message": "当前密码", - "description": "Current Phrase." - }, - "new_phrase": { - "message": "新密码", - "description": "New Phrase." - }, - "phrase": { - "message": "密码", - "description": "Phrase." - }, - "confirm_phrase": { - "message": "确认密码", - "description": "Confirmm Phrase." - }, - "security_warning": { - "message": "您的密钥将使用此密码进行加密。如果您忘记了密码没有人能够提供帮助。", - "description": "Phrase Warning." - }, - "update": { - "message": "更新", - "description": "Update." - }, - "phrase_incorrect": { - "message": "部分账户与密码不匹配。", - "description": "Phrase Incorrect." - }, - "phrase_not_match": { - "message": "两次密码不一致。", - "description": "Phrase Not Match." - }, - "encrypted": { - "message": "已加密", - "description": "Encrypted." - }, - "copied": { - "message": "已复制", - "description": "Copied." - }, - "feedback": { - "message": "问题反馈", - "description": "Feedback." - }, - "translate": { - "message": "参与翻译", - "description": "Translate." - }, - "source": { - "message": "源代码", - "description": "Source Code." - }, - "passphrase_info": { - "message": "输入密码以解码账户数据。", - "description": "Passphrase Info" - }, - "sync_clock": { - "message": "通过Google校准时间", - "description": "Sync Clock" - }, - "remember_phrase": { - "message": "记住密码", - "description": "Remember Passphrase" - }, - "clock_too_far_off": { - "message": "注意!您的本地时钟时间差过大,请修正后再进行操作。", - "description": "Local Time is Too Far Off" - }, - "remind_backup": { - "message": "永远不要通过重装扩展来尝试解决问题,否则您将丢失全部数据!您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", - "description": "Remind Backup" - }, - "capture_failed": { - "message": "捕捉失败,请重载您正在浏览的页面后重试。", - "description": "Capture Failed" - }, - "unencrypted_secret_warning": { - "message": "此密钥未被加密!点击此处来设置一个密码以解决此问题。", - "description": "Unencrypted Secret Warning" - }, - "based_on_time": { - "message": "基于时间", - "description": "Time Based" - }, - "based_on_counter": { - "message": "基于计数器", - "description": "Counter Based" - }, - "resize_popup_page": { - "message": "调整弹出页面尺寸", - "description": "Resize Popup Page" - }, - "scale": { - "message": "比例", - "description": "Scale" - }, - "export_info": { - "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", - "description": "Export menu info text" - }, - "confirm_remove_entry" : { - "message": "您确定要删除此项吗?您无法找回已删除的密钥。此操作无法撤销。", - "description": "Remove entry confirmation" - } -} +{ + "extName": { + "message": "身份验证器", + "description": "Extension Name." + }, + "extShortName": { + "message": "身份验证器", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "适用于Google身份验证器及战网安全令。", + "description": "Extension Description." + }, + "added": { + "message": "已添加。", + "description": "Added Account." + }, + "errorqr": { + "message": "无法识别的QR码。", + "description": "QR Error." + }, + "errorsecret": { + "message": "密钥错误,仅支持Base32(A-Z,2-7及=)和HEX(0-9及A-F)格式,然而您的密钥是:", + "description": "Secret Error." + }, + "info": { + "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", + "description": "Information." + }, + "add_qr": { + "message": "扫描QR码", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "手动输入", + "description": "Manual Entry." + }, + "close": { + "message": "关闭", + "description": "Close." + }, + "ok": { + "message": "确定", + "description": "OK." + }, + "yes": { + "message": "Yes", + "description": "Yes." + }, + "no": { + "message": "No", + "description": "No." + }, + "err_acc_sec": { + "message": "请输入账户和密钥。", + "description": "Input Account and Secret." + }, + "account": { + "message": "账户", + "description": "Account." + }, + "secret": { + "message": "密钥", + "description": "Secret." + }, + "updateSuccess": { + "message": "成功。", + "description": "Update Success." + }, + "updateFailure": { + "message": "失败。", + "description": "Update Failure." + }, + "about": { + "message": "关于", + "description": "About." + }, + "export_import": { + "message": "导出 / 导入", + "description": "Export and Import." + }, + "settings": { + "message": "设置", + "description": "Settings." + }, + "security": { + "message": "安全", + "description": "Security." + }, + "current_phrase": { + "message": "当前密码", + "description": "Current Phrase." + }, + "new_phrase": { + "message": "新密码", + "description": "New Phrase." + }, + "phrase": { + "message": "密码", + "description": "Phrase." + }, + "confirm_phrase": { + "message": "确认密码", + "description": "Confirmm Phrase." + }, + "confirm_delete" : { + "message": "您确定要删除此项吗?您无法找回已删除的密钥。此操作无法撤销。", + "description": "Remove entry confirmation" + }, + "security_warning": { + "message": "您的密钥将使用此密码进行加密。如果您忘记了密码没有人能够提供帮助。", + "description": "Phrase Warning." + }, + "update": { + "message": "更新", + "description": "Update." + }, + "phrase_incorrect": { + "message": "部分账户与密码不匹配。", + "description": "Phrase Incorrect." + }, + "phrase_not_match": { + "message": "两次密码不一致。", + "description": "Phrase Not Match." + }, + "encrypted": { + "message": "已加密", + "description": "Encrypted." + }, + "copied": { + "message": "已复制", + "description": "Copied." + }, + "feedback": { + "message": "问题反馈", + "description": "Feedback." + }, + "translate": { + "message": "参与翻译", + "description": "Translate." + }, + "source": { + "message": "源代码", + "description": "Source Code." + }, + "passphrase_info": { + "message": "输入密码以解码账户数据。", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "通过Google校准时间", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "记住密码", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "注意!您的本地时钟时间差过大,请修正后再进行操作。", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "永远不要通过重装扩展来尝试解决问题,否则您将丢失全部数据!您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "捕捉失败,请重载您正在浏览的页面后重试。", + "description": "Capture Failed" + }, + "unencrypted_secret_warning": { + "message": "此密钥未被加密!点击此处来设置一个密码以解决此问题。", + "description": "Unencrypted Secret Warning" + }, + "based_on_time": { + "message": "基于时间", + "description": "Time Based" + }, + "based_on_counter": { + "message": "基于计数器", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "调整弹出页面尺寸", + "description": "Resize Popup Page" + }, + "scale": { + "message": "比例", + "description": "Scale" + }, + "export_info": { + "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", + "description": "Export menu info text" + } +} diff --git a/popup.html b/popup.html index cac4c71fd..3702f7eff 100644 --- a/popup.html +++ b/popup.html @@ -142,8 +142,8 @@
{{ confirmMessage }}
-
{{ i18n.ok }}
-
Cancel
+
{{ i18n.yes }}
+
{{ i18n.no }}
@@ -158,4 +158,4 @@
- \ No newline at end of file + diff --git a/src/popup.ts b/src/popup.ts index 428e91bfd..919b1a4df 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -263,7 +263,7 @@ async function init() { return; }, removeEntry: async (entry: OTPEntry) => { - if (await authenticator.confirm(authenticator.i18n.confirm_remove_entry)) { + if (await authenticator.confirm(authenticator.i18n.confirm_delete)) { await entry.delete(); await authenticator.updateEntries(); } @@ -480,4 +480,4 @@ chrome.permissions.contains( } }); -init(); \ No newline at end of file +init(); From 555b57fb861fb352103450cd3d62e8be61faf9d4 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 6 Feb 2018 18:42:19 -0600 Subject: [PATCH 033/178] Submit password on enter key --- popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/popup.html b/popup.html index 3702f7eff..1d94b3c48 100644 --- a/popup.html +++ b/popup.html @@ -104,7 +104,7 @@
{{ i18n.passphrase_info }}
- +
{{ i18n.ok }}
From 3e4457a60e16c2a6522e14440c0beba6a3d85f57 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 7 Feb 2018 12:04:36 +0800 Subject: [PATCH 034/178] fix #4 --- src/popup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/popup.ts b/src/popup.ts index 919b1a4df..d4c7a9898 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -239,6 +239,7 @@ async function init() { setTimeout(() => { authenticator.class.fadeout = false; authenticator.info = ''; + authenticator.newAccount.show = false; }, 200); return; }, From 165ca8e5fff4417b92dcaf333912274b1795f1bd Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 6 Feb 2018 23:08:34 -0600 Subject: [PATCH 035/178] Add .vscode to gitignore --- .gitignore | 3 ++- .vscode/settings.json | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore index b7dab5e9c..16d2e37b3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -build \ No newline at end of file +build +.vscode diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index ff30c4464..000000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "editor.tabSize": 2 -} \ No newline at end of file From c78949d04a7492d5ef37de78b937e33f1e21c0c3 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 7 Feb 2018 14:10:36 +0800 Subject: [PATCH 036/178] fix #3 and #5 --- _locales/en/messages.json | 2 +- _locales/zh_CN/messages.json | 2 +- popup.html | 2 +- src/background.ts | 12 +++++----- src/content.ts | 25 +++++++++++++++------ src/models/storage.ts | 14 +++++++----- src/popup.ts | 43 ++++++++++++++++++++++++++++++------ 7 files changed, 72 insertions(+), 28 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 32c85eae0..81e177952 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -116,7 +116,7 @@ "description": "Update." }, "phrase_incorrect": { - "message": "Some accounts and passphrase do not match.", + "message": "Some accounts and passphrase do not match, you cannot add new account. Please enter correct passphrase and try again.", "description": "Passphrase Incorrect." }, "phrase_not_match": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 2a5e0d0bb..34be6821e 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -116,7 +116,7 @@ "description": "Update." }, "phrase_incorrect": { - "message": "部分账户与密码不匹配。", + "message": "部分账户与密码不匹配,您无法添加新账户。请提供正确的密码后重试。", "description": "Phrase Incorrect." }, "phrase_not_match": { diff --git a/popup.html b/popup.html index 1d94b3c48..355d31855 100644 --- a/popup.html +++ b/popup.html @@ -75,7 +75,7 @@
{{ i18n.add_qr }}
-
{{ i18n.add_secret }}
+
{{ i18n.add_secret }}
diff --git a/src/background.ts b/src/background.ts index 829a293a0..a339a1e92 100644 --- a/src/background.ts +++ b/src/background.ts @@ -11,7 +11,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } getQr( sender.tab, message.info.left, message.info.top, message.info.width, - message.info.height, message.info.windowWidth); + message.info.height, message.info.windowWidth, message.info.passphrase); } }); @@ -19,7 +19,7 @@ let contentTab: chrome.tabs.Tab; function getQr( tab: chrome.tabs.Tab, left: number, top: number, width: number, - height: number, windowWidth: number) { + height: number, windowWidth: number, passphrase: string) { chrome.tabs.captureVisibleTab(tab.windowId, {format: 'png'}, (dataUrl) => { contentTab = tab; const qr = new Image(); @@ -34,13 +34,15 @@ function getQr( } ctx.drawImage(qr, left, top, width, height, 0, 0, width, height); const url = captureCanvas.toDataURL(); - qrcode.callback = getTotp; + qrcode.callback = (text) => { + getTotp(text, passphrase); + }; qrcode.decode(url); }; }); } -async function getTotp(text: string) { +async function getTotp(text: string, passphrase: string) { const id = contentTab.id; if (!id) { return; @@ -91,7 +93,7 @@ async function getTotp(text: string) { !/^[0-9a-f]+$/i.test(secret) && !/^[2-7a-z]+=*$/i.test(secret)) { chrome.tabs.sendMessage(id, {action: 'secretqr', secret}); } else { - const encryption = new Encryption(''); + const encryption = new Encryption(passphrase); const hash = CryptoJS.MD5(secret).toString(); if (!/^[2-7a-z]+=*$/i.test(secret) && /^[0-9a-f]+$/i.test(secret)) { type = 'hex'; diff --git a/src/content.ts b/src/content.ts index 951f64f62..01c2b9b97 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,7 +1,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'capture') { sendResponse('beginCapture'); - showGrayLayout(); + showGrayLayout(message.passphrase); } else if (message.action === 'errorsecret') { alert(chrome.i18n.getMessage('errorsecret') + message.secret); } else if (message.action === 'errorqr') { @@ -20,7 +20,7 @@ interface CaptureBoxPosition { let captureBoxPosition: CaptureBoxPosition = {left: 0, top: 0}; -function showGrayLayout() { +function showGrayLayout(passphrase: string) { let grayLayout = document.getElementById('__ga_grayLayout__'); if (!grayLayout) { grayLayout = document.createElement('div'); @@ -36,7 +36,9 @@ function showGrayLayout() { grayLayout.appendChild(captureBox); grayLayout.onmousedown = grayLayoutDown; grayLayout.onmousemove = grayLayoutMove; - grayLayout.onmouseup = grayLayoutUp; + grayLayout.onmouseup = (event) => { + grayLayoutUp(event, passphrase); + }; grayLayout.oncontextmenu = (event) => { event.preventDefault(); return; @@ -86,7 +88,7 @@ function grayLayoutMove(event: MouseEvent) { return; } -function grayLayoutUp(event: MouseEvent) { +function grayLayoutUp(event: MouseEvent, passphrase: string) { const grayLayout = document.getElementById('__ga_grayLayout__'); const captureBox = document.getElementById('__ga_captureBox__'); if (!captureBox || !grayLayout) { @@ -114,16 +116,25 @@ function grayLayoutUp(event: MouseEvent) { // make sure captureBox and grayLayout is hidden setTimeout(() => { sendPosition( - captureBoxLeft, captureBoxTop, captureBoxWidth, captureBoxHeight); + captureBoxLeft, captureBoxTop, captureBoxWidth, captureBoxHeight, + passphrase); }, 200); return false; } function sendPosition( - left: number, top: number, width: number, height: number) { + left: number, top: number, width: number, height: number, + passphrase: string) { chrome.runtime.sendMessage({ action: 'position', - info: {left, top, width, height, windowWidth: window.innerWidth} + info: { + left, + top, + width, + height, + windowWidth: window.innerWidth, + passphrase + } }); } diff --git a/src/models/storage.ts b/src/models/storage.ts index b2886056f..0d0ca46e7 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -250,12 +250,14 @@ class EntryStorage { } // we need correct the hash - const _hash = CryptoJS.MD5(entry.secret).toString(); - if (hash !== _hash) { - chrome.storage.sync.remove(hash); - hash = _hash; - entryData.hash = hash; - needMigrate = true; + if (entry.secret !== 'Encrypted') { + const _hash = CryptoJS.MD5(entry.secret).toString(); + if (hash !== _hash) { + chrome.storage.sync.remove(hash); + hash = _hash; + entryData.hash = hash; + needMigrate = true; + } } if (needMigrate) { diff --git a/src/popup.ts b/src/popup.ts index d4c7a9898..06b9705dd 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -347,6 +347,21 @@ async function init() { }); return; }, + addAccountManually: () => { + const entries = authenticator.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // shouldn't add new account with + // the current passphrass + if (entries[i].code === 'Encrypted') { + authenticator.message = authenticator.i18n.phrase_incorrect; + return; + } + } + + authenticator.newAccount.show = true; + }, addNewAccount: async () => { let type: OTPType; if (!/^[a-z2-7]+=*$/i.test(authenticator.newAccount.secret) && @@ -433,18 +448,32 @@ async function init() { return; }, beginCapture: () => { + const entries = authenticator.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // shouldn't add new account with + // the current passphrass + if (entries[i].code === 'Encrypted') { + authenticator.message = authenticator.i18n.phrase_incorrect; + return; + } + } + chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { const tab = tabs[0]; if (!tab || !tab.id) { return; } - chrome.tabs.sendMessage(tab.id, {action: 'capture'}, (result) => { - if (result !== 'beginCapture') { - authenticator.message = authenticator.i18n.capture_failed; - } else { - window.close(); - } - }); + chrome.tabs.sendMessage( + tab.id, {action: 'capture', passphrase: authenticator.passphrase}, + (result) => { + if (result !== 'beginCapture') { + authenticator.message = authenticator.i18n.capture_failed; + } else { + window.close(); + } + }); }); return; } From 5c3d982df5d3e2b86c8dd38fe6c377ec6428f243 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 7 Feb 2018 14:30:29 +0800 Subject: [PATCH 037/178] use switch instead of if else --- src/content.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/content.ts b/src/content.ts index 01c2b9b97..a9c0757e2 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,15 +1,24 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - if (message.action === 'capture') { - sendResponse('beginCapture'); - showGrayLayout(message.passphrase); - } else if (message.action === 'errorsecret') { - alert(chrome.i18n.getMessage('errorsecret') + message.secret); - } else if (message.action === 'errorqr') { - alert(chrome.i18n.getMessage('errorqr')); - } else if (message.action === 'added') { - alert(message.account + chrome.i18n.getMessage('added')); - } else if (message.action === 'text') { - showQrCode(message.text); + switch (message.action) { + case 'capture': + sendResponse('beginCapture'); + showGrayLayout(message.passphrase); + break; + case 'errorsecret': + alert(chrome.i18n.getMessage('errorsecret') + message.secret); + break; + case 'errorqr': + alert(chrome.i18n.getMessage('errorqr')); + break; + case 'added': + alert(message.account + chrome.i18n.getMessage('added')); + break; + case 'text': + showQrCode(message.text); + break; + default: + // invalid command, ignore it + break; } }); From 5cd551d4c654d956ed852a549e8c17a0287794f8 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 7 Feb 2018 14:57:55 +0800 Subject: [PATCH 038/178] download backup file --- _locales/en/messages.json | 4 ++++ _locales/zh_CN/messages.json | 4 ++++ css/popup.css | 6 +++++- popup.html | 3 +++ src/popup.ts | 11 +++++++++++ 5 files changed, 27 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 81e177952..09f6eaab5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -190,5 +190,9 @@ "export_info": { "message": "Copy this text and save it somewhere else to backup your secrets. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", "description": "Export menu info text" + }, + "download_backup": { + "message": "Download backup file", + "description": "Download backup file." } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 34be6821e..94e2aa319 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -190,5 +190,9 @@ "export_info": { "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", "description": "Export menu info text" + }, + "download_backup": { + "message": "下载备份文件", + "description": "Download backup file." } } diff --git a/css/popup.css b/css/popup.css index 8cf9b62b9..82ecf38aa 100644 --- a/css/popup.css +++ b/css/popup.css @@ -599,7 +599,7 @@ body { } #exportData { - height: 330px; + height: 320px; width: 100%; word-break: break-all; resize: none; @@ -701,4 +701,8 @@ body { top: 150px; box-shadow: 1px 1px 3px gray; z-index: 1000; +} + +#downloadBackup { + text-align: right; } \ No newline at end of file diff --git a/popup.html b/popup.html index 355d31855..dc0ec8aa2 100644 --- a/popup.html +++ b/popup.html @@ -110,6 +110,9 @@
+
{{ i18n.update }}
diff --git a/src/popup.ts b/src/popup.ts index 06b9705dd..929784b92 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -164,6 +164,14 @@ function isCustomEvent(event: Event): event is CustomEvent { return 'detail' in event; } +function getBackupFile(entryData: {[hash: string]: OTPStorage}) { + let json = JSON.stringify(entryData, null, 2); + // for windows notepad + json = json.replace(/\n/g, '\r\n'); + const base64Data = btoa(json); + return `data:application/octet-stream;base64,${base64Data}`; +} + async function init() { const zoom = Number(localStorage.zoom) || 100; resize(zoom); @@ -175,6 +183,7 @@ async function init() { const exportData = shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); const entries = shouldShowPassphrase ? [] : await getEntries(encryption); + const exportFile = getBackupFile(exportData); const authenticator = new Vue({ el: '#authenticator', @@ -186,6 +195,7 @@ async function init() { zoom, OTPType, exportData: JSON.stringify(exportData, null, 2), + exportFile, class: { timeout: false, edit: false, @@ -255,6 +265,7 @@ async function init() { await EntryStorage.getExport(authenticator.encryption); authenticator.exportData = JSON.stringify(exportData, null, 2); authenticator.entries = await getEntries(authenticator.encryption); + authenticator.exportFile = getBackupFile(exportData); updateCode(authenticator); return; }, From e0a15520bd67aeb60512f9b4888948f85d9bc584 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 7 Feb 2018 15:18:07 +0800 Subject: [PATCH 039/178] disable export, change passphrass when passphrass is incorrect --- _locales/en/messages.json | 2 +- _locales/zh_CN/messages.json | 8 ++++---- src/popup.ts | 13 +++++++++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 09f6eaab5..5611a0ff0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -116,7 +116,7 @@ "description": "Update." }, "phrase_incorrect": { - "message": "Some accounts and passphrase do not match, you cannot add new account. Please enter correct passphrase and try again.", + "message": "Some accounts and passphrase do not match, you cannot add new account, export accounts or change passphrass. Please enter correct passphrase and try again.", "description": "Passphrase Incorrect." }, "phrase_not_match": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 94e2aa319..293c3fc2d 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -116,7 +116,7 @@ "description": "Update." }, "phrase_incorrect": { - "message": "部分账户与密码不匹配,您无法添加新账户。请提供正确的密码后重试。", + "message": "部分账户与密码不匹配,您无法添加新账户、导出账户数据或者更改密码。请提供正确的密码后重试。", "description": "Phrase Incorrect." }, "phrase_not_match": { @@ -136,9 +136,9 @@ "description": "Feedback." }, "translate": { - "message": "参与翻译", - "description": "Translate." - }, + "message": "参与翻译", + "description": "Translate." + }, "source": { "message": "源代码", "description": "Source Code." diff --git a/src/popup.ts b/src/popup.ts index 929784b92..0cb0eda3a 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -238,6 +238,19 @@ async function init() { return; }, showInfo: (tab: string) => { + if (tab === 'export' || tab === 'security') { + const entries = authenticator.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // cannot export account data + // or change passphrase + if (entries[i].code === 'Encrypted') { + authenticator.message = authenticator.i18n.phrase_incorrect; + return; + } + } + } authenticator.class.fadein = true; authenticator.class.fadeout = false; authenticator.info = tab; From 8c828063b947245381d44a6ee19048085ad5d269 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Wed, 7 Feb 2018 15:24:41 -0600 Subject: [PATCH 040/178] Upload backup file frontend work --- popup.html | 5 ++++- src/popup.ts | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/popup.html b/popup.html index dc0ec8aa2..b8f2a7709 100644 --- a/popup.html +++ b/popup.html @@ -1,4 +1,4 @@ - + @@ -113,6 +113,9 @@ +
+ +
{{ i18n.update }}
diff --git a/src/popup.ts b/src/popup.ts index 0cb0eda3a..914280aaa 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -282,6 +282,20 @@ async function init() { updateCode(authenticator); return; }, + //TODO: Figure out what event & data are supposed to be typed as + importFile: (event: any) => { + if (event.target.files[0] && + event.target.files[0].type.startsWith('text/')) { + const reader = new FileReader(); + reader.onload = (data: any) => { + const importData = JSON.parse(data.target.result); + //Replace data with import data + // if current data has codes insert and check for duplicates + }; + reader.readAsText(event.target.files[0]); + } + return; + }, saveZoom: () => { localStorage.zoom = authenticator.zoom; resize(authenticator.zoom); From 75b529972b415cb0e29a5a0a06e7ee9e899c6060 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 8 Feb 2018 12:10:04 +0800 Subject: [PATCH 041/178] fix type issue --- src/popup.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/popup.ts b/src/popup.ts index 914280aaa..1f5ea8efc 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -282,17 +282,21 @@ async function init() { updateCode(authenticator); return; }, - //TODO: Figure out what event & data are supposed to be typed as - importFile: (event: any) => { - if (event.target.files[0] && - event.target.files[0].type.startsWith('text/')) { + // TODO: Figure out what event & data are supposed to be typed as + importFile: (event: Event) => { + const target = event.target as HTMLInputElement; + if (!target || !target.files) { + return; + } + if (target.files[0] && target.files[0].type.startsWith('text/')) { const reader = new FileReader(); - reader.onload = (data: any) => { - const importData = JSON.parse(data.target.result); - //Replace data with import data + reader.onload = () => { + const importData = JSON.parse(reader.result); + console.log(importData); + // Replace data with import data // if current data has codes insert and check for duplicates }; - reader.readAsText(event.target.files[0]); + reader.readAsText(target.files[0]); } return; }, From 99014d667b9fda1354636c2fb24fd27e0036ef77 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 10 Feb 2018 04:36:41 +0800 Subject: [PATCH 042/178] split popup --- popup.html | 10 + src/models/interface.ts | 12 + src/popup.ts | 543 ++-------------------------------------- src/ui/add-account.ts | 62 +++++ src/ui/class.ts | 25 ++ src/ui/entry.ts | 180 +++++++++++++ src/ui/i18n.ts | 37 +++ src/ui/info.ts | 28 +++ src/ui/menu.ts | 95 +++++++ src/ui/message.ts | 42 ++++ src/ui/passphrase.ts | 31 +++ src/ui/qr.ts | 62 +++++ src/ui/ui.ts | 39 +++ 13 files changed, 646 insertions(+), 520 deletions(-) create mode 100644 src/ui/add-account.ts create mode 100644 src/ui/class.ts create mode 100644 src/ui/entry.ts create mode 100644 src/ui/i18n.ts create mode 100644 src/ui/info.ts create mode 100644 src/ui/menu.ts create mode 100644 src/ui/message.ts create mode 100644 src/ui/passphrase.ts create mode 100644 src/ui/qr.ts create mode 100644 src/ui/ui.ts diff --git a/popup.html b/popup.html index b8f2a7709..d77ec5175 100644 --- a/popup.html +++ b/popup.html @@ -162,6 +162,16 @@
+ + + + + + + + + + diff --git a/src/models/interface.ts b/src/models/interface.ts index 6c8c013f3..97efcd7a4 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -39,4 +39,16 @@ interface OTPStorage { /* tslint:disable-next-line:interface-name */ interface I18nMessage { [key: string]: {message: string, description: string}; +} + +interface UIConfig { + el?: string; + data?: { + /* tslint:disable-next-line:no-any */ + [name: string]: any + }; + methods?: { + /* tslint:disable-next-line:no-any */ + [name: string]: (...arg: any[]) => any + }; } \ No newline at end of file diff --git a/src/popup.ts b/src/popup.ts index 1f5ea8efc..690e87fa4 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -1,528 +1,31 @@ /* tslint:disable:no-reference */ -/// -/// -/// - -// need to find a better way to handle Vue types without modules -// we use vue 1.0 here to solve csp issues -/* tslint:disable-next-line:no-any */ -declare var Vue: any; - -/* tslint:disable-next-line:no-any */ -declare var QRCode: any; - -async function getEntries(encryption: Encryption) { - const optEntries: OTPEntry[] = await EntryStorage.get(encryption); - return optEntries; -} - -function getVersion() { - return chrome.runtime.getManifest().version; -} - -async function loadI18nMessages() { - return new Promise( - (resolve: (value: {[key: string]: string}) => void, - reject: (reason: Error) => void) => { - try { - const xhr = new XMLHttpRequest(); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); - const i18nData: {[key: string]: string} = {}; - for (const key of Object.keys(i18nMessage)) { - i18nData[key] = chrome.i18n.getMessage(key); - } - return resolve(i18nData); - } - return; - }; - xhr.open( - 'GET', chrome.extension.getURL('/_locales/en/messages.json')); - xhr.send(); - } catch (error) { - return reject(error); - } - }); -} - -async function syncTimeWithGoogle() { - return new Promise( - (resolve: (value: string) => void, reject: (reason: Error) => void) => { - try { - const xhr = new XMLHttpRequest(); - xhr.open('HEAD', 'https://www.google.com/generate_204'); - const xhrAbort = setTimeout(() => { - xhr.abort(); - return resolve('updateFailure'); - }, 5000); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - clearTimeout(xhrAbort); - const date = xhr.getResponseHeader('date'); - if (!date) { - return resolve('updateFailure'); - } - const serverTime = new Date(date).getTime(); - const clientTime = new Date().getTime(); - const offset = Math.round((serverTime - clientTime) / 1000); - - if (Math.abs(offset) <= 300) { // within 5 minutes - localStorage.offset = - Math.round((serverTime - clientTime) / 1000); - return resolve('updateSuccess'); - } else { - return resolve('clock_too_far_off'); - } - } - }; - xhr.send(); - } catch (error) { - return reject(error); - } - }); -} - -/* tslint:disable-next-line:no-any */ -async function updateCode(app: any) { - let second = new Date().getSeconds(); - if (localStorage.offset) { - second += Number(localStorage.offset) + 30; - } - second = second % 30; - app.sector = getSector(second); - if (second > 25) { - app.class.timeout = true; - } else { - app.class.timeout = false; - } - if (second < 1) { - const entries = app.entries as OTP[]; - for (let i = 0; i < entries.length; i++) { - if (entries[i].type !== OTPType.hotp) { - entries[i].generate(); - } - } - } -} - -function getSector(second: number) { - const canvas = document.createElement('canvas'); - canvas.width = 40; - canvas.height = 40; - const ctx = canvas.getContext('2d'); - if (!ctx) { - return; - } - ctx.fillStyle = '#888'; - ctx.beginPath(); - ctx.moveTo(20, 20); - ctx.arc( - 20, 20, 16, second / 30 * Math.PI * 2 - Math.PI / 2, Math.PI * 3 / 2, - false); - ctx.fill(); - const url = canvas.toDataURL(); - return `url(${url}) center / 20px 20px`; -} - -function resize(zoom: number) { - if (zoom !== 100) { - document.body.style.marginBottom = 480 * (zoom / 100 - 1) + 'px'; - document.body.style.marginRight = 320 * (zoom / 100 - 1) + 'px'; - document.body.style.transform = 'scale(' + (zoom / 100) + ')'; - } -} - -async function getQrUrl(entry: OTPEntry) { - return new Promise( - (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 otpauth = 'otpauth://' + type + '/' + label + - '?secret=' + entry.secret + - (entry.issuer ? ('&issuer=' + entry.issuer.split('::')[0]) : '') + - ((entry.type === OTPType.hotp) ? ('&counter=' + entry.counter) : - ''); - /* tslint:disable-next-line:no-unused-expression */ - new QRCode( - 'qr', { - text: otpauth, - width: 128, - height: 128, - colorDark: '#000000', - colorLight: '#ffffff', - correctLevel: QRCode.CorrectLevel.L - }, - resolve); - return; - }); -} - -function isCustomEvent(event: Event): event is CustomEvent { - return 'detail' in event; -} - -function getBackupFile(entryData: {[hash: string]: OTPStorage}) { - let json = JSON.stringify(entryData, null, 2); - // for windows notepad - json = json.replace(/\n/g, '\r\n'); - const base64Data = btoa(json); - return `data:application/octet-stream;base64,${base64Data}`; -} +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// async function init() { - const zoom = Number(localStorage.zoom) || 100; - resize(zoom); - - const version = getVersion(); - const i18n = await loadI18nMessages(); - const encryption: Encryption = new Encryption(''); - const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); - const exportData = - shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); - const entries = shouldShowPassphrase ? [] : await getEntries(encryption); - const exportFile = getBackupFile(exportData); - - const authenticator = new Vue({ - el: '#authenticator', - data: { - version, - i18n, - entries, - encryption, - zoom, - OTPType, - exportData: JSON.stringify(exportData, null, 2), - exportFile, - class: { - timeout: false, - edit: false, - slidein: false, - slideout: false, - fadein: false, - fadeout: false, - qrfadein: false, - qrfadeout: false, - notificationFadein: false, - notificationFadeout: false, - hotpDiabled: false - }, - sector: '', - info: '', - message: '', - confirmMessage: '', - qr: '', - notification: '', - passphrase: '', - notificationTimeout: 0, - newAccount: {show: false, account: '', secret: '', type: OTPType.totp}, - newPassphrase: {phrase: '', confirm: ''} - }, - methods: { - showBulls: (code: string) => { - return new Array(code.length).fill('•').join(''); - }, - showMenu: () => { - authenticator.class.slidein = true; - authenticator.class.slideout = false; - return; - }, - closeMenu: () => { - authenticator.class.slidein = false; - authenticator.class.slideout = true; - setTimeout(() => { - authenticator.class.slideout = false; - }, 200); - return; - }, - showInfo: (tab: string) => { - if (tab === 'export' || tab === 'security') { - const entries = authenticator.entries as OTPEntry[]; - for (let i = 0; i < entries.length; i++) { - // we have encrypted entry - // the current passphrass is incorrect - // cannot export account data - // or change passphrase - if (entries[i].code === 'Encrypted') { - authenticator.message = authenticator.i18n.phrase_incorrect; - return; - } - } - } - authenticator.class.fadein = true; - authenticator.class.fadeout = false; - authenticator.info = tab; - return; - }, - closeInfo: () => { - authenticator.class.fadein = false; - authenticator.class.fadeout = true; - setTimeout(() => { - authenticator.class.fadeout = false; - authenticator.info = ''; - authenticator.newAccount.show = false; - }, 200); - return; - }, - importEnties: async () => { - await EntryStorage.import( - authenticator.encryption, JSON.parse(authenticator.exportData)); - await authenticator.updateEntries(); - authenticator.message = authenticator.i18n.updateSuccess; - return; - }, - updateEntries: async () => { - const exportData = - await EntryStorage.getExport(authenticator.encryption); - authenticator.exportData = JSON.stringify(exportData, null, 2); - authenticator.entries = await getEntries(authenticator.encryption); - authenticator.exportFile = getBackupFile(exportData); - updateCode(authenticator); - return; - }, - // TODO: Figure out what event & data are supposed to be typed as - importFile: (event: Event) => { - const target = event.target as HTMLInputElement; - if (!target || !target.files) { - return; - } - if (target.files[0] && target.files[0].type.startsWith('text/')) { - const reader = new FileReader(); - reader.onload = () => { - const importData = JSON.parse(reader.result); - console.log(importData); - // Replace data with import data - // if current data has codes insert and check for duplicates - }; - reader.readAsText(target.files[0]); - } - return; - }, - saveZoom: () => { - localStorage.zoom = authenticator.zoom; - resize(authenticator.zoom); - return; - }, - removeEntry: async (entry: OTPEntry) => { - if (await authenticator.confirm(authenticator.i18n.confirm_delete)) { - await entry.delete(); - await authenticator.updateEntries(); - } - return; - }, - syncClock: async () => { - chrome.permissions.request( - {origins: ['https://www.google.com/']}, async (granted) => { - if (granted) { - const message = await syncTimeWithGoogle(); - authenticator.message = authenticator.i18n[message]; - } - return; - }); - return; - }, - editEntry: () => { - authenticator.class.edit = !authenticator.class.edit; - const codes = document.getElementById('codes'); - if (codes) { - // wait vue apply changes to dom - setTimeout(() => { - codes.scrollTop = authenticator.class.edit ? codes.scrollHeight : 0; - }, 0); - } - return; - }, - shouldShowQrIcon: (entry: OTPEntry) => { - return entry.secret !== 'Encrypted' && entry.type !== OTPType.battle && - entry.type !== OTPType.steam; - }, - showQr: async (entry: OTPEntry) => { - const qrUrl = await getQrUrl(entry); - authenticator.qr = `url(${qrUrl})`; - authenticator.class.qrfadein = true; - authenticator.class.qrfadeout = false; - return; - }, - hideQr: () => { - authenticator.class.qrfadein = false; - authenticator.class.qrfadeout = true; - setTimeout(() => { - authenticator.class.qrfadeout = false; - }, 200); - return; - }, - copyCode: (entry: OTPEntry) => { - if (authenticator.class.edit) { - return; - } + const ui = new UI({el: '#authenticator'}); - if (entry.code === 'Encrypted') { - authenticator.showInfo('passphrase'); - return; - } - chrome.permissions.request( - {permissions: ['clipboardWrite']}, (granted) => { - if (granted) { - const codeClipboard = document.getElementById( - 'codeClipboard') as HTMLInputElement; - if (!codeClipboard) { - return; - } - codeClipboard.value = entry.code; - codeClipboard.focus(); - codeClipboard.select(); - document.execCommand('Copy'); - authenticator.notification = authenticator.i18n.copied; - clearTimeout(authenticator.notificationTimeout); - authenticator.class.notificationFadein = true; - authenticator.class.notificationFadeout = false; - authenticator.notificationTimeout = setTimeout(() => { - authenticator.class.notificationFadein = false; - authenticator.class.notificationFadeout = true; - setTimeout(() => { - authenticator.class.notificationFadeout = false; - }, 200); - }, 1000); - } - }); - return; - }, - addAccountManually: () => { - const entries = authenticator.entries as OTPEntry[]; - for (let i = 0; i < entries.length; i++) { - // we have encrypted entry - // the current passphrass is incorrect - // shouldn't add new account with - // the current passphrass - if (entries[i].code === 'Encrypted') { - authenticator.message = authenticator.i18n.phrase_incorrect; - return; - } - } + await className(ui); + await i18n(ui); + await menu(ui); + await info(ui); + await passphrase(ui); + await entry(ui); + await qr(ui); + await message(ui); + await addAccount(ui); - authenticator.newAccount.show = true; - }, - addNewAccount: async () => { - let type: OTPType; - if (!/^[a-z2-7]+=*$/i.test(authenticator.newAccount.secret) && - /^[0-9a-f]+$/i.test(authenticator.newAccount.secret)) { - type = OTPType.hex; - } else { - type = authenticator.newAccount.type; - } - - const entry = new OTPEntry( - type, '', authenticator.newAccount.secret, - authenticator.newAccount.account, 0, 0); - await entry.create(authenticator.encryption); - await authenticator.updateEntries(); - authenticator.newAccount.type = OTPType.totp; - authenticator.account = ''; - authenticator.secret = ''; - authenticator.newAccount.show = false; - authenticator.closeInfo(); - authenticator.class.edit = false; - - const codes = document.getElementById('codes'); - if (codes) { - // wait vue apply changes to dom - setTimeout(() => { - codes.scrollTop = 0; - }, 0); - } - return; - }, - confirm: async (message: string) => { - return new Promise( - (resolve: (value: boolean) => void, - reject: (reason: Error) => void) => { - authenticator.confirmMessage = message; - window.addEventListener('confirm', (event) => { - authenticator.confirmMessage = ''; - if (!isCustomEvent(event)) { - return resolve(false); - } - return resolve(event.detail); - }); - return; - }); - }, - confirmOK: () => { - const confirmEvent = new CustomEvent('confirm', {detail: true}); - window.dispatchEvent(confirmEvent); - return; - }, - confirmCancel: () => { - const confirmEvent = new CustomEvent('confirm', {detail: false}); - window.dispatchEvent(confirmEvent); - return; - }, - nextCode: async (entry: OTPEntry) => { - if (authenticator.class.hotpDiabled) { - return; - } - authenticator.class.hotpDiabled = true; - await entry.next(authenticator.encryption); - await authenticator.updateEntries(); - setTimeout(() => { - authenticator.class.hotpDiabled = false; - }, 3000); - return; - }, - applyPassphrase: async () => { - authenticator.encryption.updateEncryptionPassword( - authenticator.passphrase); - await authenticator.updateEntries(); - authenticator.closeInfo(); - return; - }, - changePassphrase: async () => { - if (authenticator.newPassphrase.phrase !== - authenticator.newPassphrase.confirm) { - authenticator.message = authenticator.i18n.phrase_not_match; - return; - } - authenticator.encryption.updateEncryptionPassword( - authenticator.newPassphrase.phrase); - await authenticator.importEnties(); - return; - }, - beginCapture: () => { - const entries = authenticator.entries as OTPEntry[]; - for (let i = 0; i < entries.length; i++) { - // we have encrypted entry - // the current passphrass is incorrect - // shouldn't add new account with - // the current passphrass - if (entries[i].code === 'Encrypted') { - authenticator.message = authenticator.i18n.phrase_incorrect; - return; - } - } - - chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { - const tab = tabs[0]; - if (!tab || !tab.id) { - return; - } - chrome.tabs.sendMessage( - tab.id, {action: 'capture', passphrase: authenticator.passphrase}, - (result) => { - if (result !== 'beginCapture') { - authenticator.message = authenticator.i18n.capture_failed; - } else { - window.close(); - } - }); - }); - return; - } - } - }); + const authenticator = ui.generate(); - if (shouldShowPassphrase) { + if (authenticator.shouldShowPassphrase) { authenticator.showInfo('passphrase'); } @@ -552,4 +55,4 @@ chrome.permissions.contains( } }); -init(); +init(); \ No newline at end of file diff --git a/src/ui/add-account.ts b/src/ui/add-account.ts new file mode 100644 index 000000000..4a663ff53 --- /dev/null +++ b/src/ui/add-account.ts @@ -0,0 +1,62 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function addAccount(_ui: UI) { + const ui: UIConfig = { + data: { + newAccount: {show: false, account: '', secret: '', type: OTPType.totp}, + newPassphrase: {phrase: '', confirm: ''} + }, + methods: { + addNewAccount: async () => { + let type: OTPType; + if (!/^[a-z2-7]+=*$/i.test(_ui.instance.newAccount.secret) && + /^[0-9a-f]+$/i.test(_ui.instance.newAccount.secret)) { + type = OTPType.hex; + } else { + type = _ui.instance.newAccount.type; + } + + const entry = new OTPEntry( + type, '', _ui.instance.newAccount.secret, + _ui.instance.newAccount.account, 0, 0); + await entry.create(_ui.instance.encryption); + await _ui.instance.updateEntries(); + _ui.instance.newAccount.type = OTPType.totp; + _ui.instance.account = ''; + _ui.instance.secret = ''; + _ui.instance.newAccount.show = false; + _ui.instance.closeInfo(); + _ui.instance.class.edit = false; + + const codes = document.getElementById('codes'); + if (codes) { + // wait vue apply changes to dom + setTimeout(() => { + codes.scrollTop = 0; + }, 0); + } + return; + }, + beginCapture: () => { + chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { + const tab = tabs[0]; + if (!tab || !tab.id) { + return; + } + chrome.tabs.sendMessage(tab.id, {action: 'capture'}, (result) => { + if (result !== 'beginCapture') { + _ui.instance.message = _ui.instance.i18n.capture_failed; + } else { + window.close(); + } + }); + }); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/class.ts b/src/ui/class.ts new file mode 100644 index 000000000..de34d8545 --- /dev/null +++ b/src/ui/class.ts @@ -0,0 +1,25 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function className(_ui: UI) { + const ui: UIConfig = { + data: { + class: { + timeout: false, + edit: false, + slidein: false, + slideout: false, + fadein: false, + fadeout: false, + qrfadein: false, + qrfadeout: false, + notificationFadein: false, + notificationFadeout: false, + hotpDiabled: false + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/entry.ts b/src/ui/entry.ts new file mode 100644 index 000000000..d5acf3b1b --- /dev/null +++ b/src/ui/entry.ts @@ -0,0 +1,180 @@ +/* tslint:disable:no-reference */ +/// +/// +/// +/// + +async function getEntries(encryption: Encryption) { + const optEntries: OTPEntry[] = await EntryStorage.get(encryption); + return optEntries; +} + +/* tslint:disable-next-line:no-any */ +async function updateCode(app: any) { + let second = new Date().getSeconds(); + if (localStorage.offset) { + second += Number(localStorage.offset) + 30; + } + second = second % 30; + app.sector = getSector(second); + if (second > 25) { + app.class.timeout = true; + } else { + app.class.timeout = false; + } + if (second < 1) { + const entries = app.entries as OTP[]; + for (let i = 0; i < entries.length; i++) { + if (entries[i].type !== OTPType.hotp) { + entries[i].generate(); + } + } + } +} + +function getSector(second: number) { + const canvas = document.createElement('canvas'); + canvas.width = 40; + canvas.height = 40; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + ctx.fillStyle = '#888'; + ctx.beginPath(); + ctx.moveTo(20, 20); + ctx.arc( + 20, 20, 16, second / 30 * Math.PI * 2 - Math.PI / 2, Math.PI * 3 / 2, + false); + ctx.fill(); + const url = canvas.toDataURL(); + return `url(${url}) center / 20px 20px`; +} + +async function entry(_ui: UI) { + const encryption: Encryption = new Encryption(''); + const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); + const exportData = + shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); + const entries = shouldShowPassphrase ? [] : await getEntries(encryption); + + const ui: UIConfig = { + data: { + entries, + encryption, + OTPType, + shouldShowPassphrase, + exportData: JSON.stringify(exportData, null, 2), + sector: '', + notification: '', + notificationTimeout: 0 + }, + methods: { + showBulls: (code: string) => { + return new Array(code.length).fill('•').join(''); + }, + importEnties: async () => { + await EntryStorage.import( + _ui.instance.encryption, JSON.parse(_ui.instance.exportData)); + await _ui.instance.updateEntries(); + _ui.instance.message = _ui.instance.i18n.updateSuccess; + return; + }, + updateEntries: async () => { + const exportData = + await EntryStorage.getExport(_ui.instance.encryption); + _ui.instance.exportData = JSON.stringify(exportData, null, 2); + _ui.instance.entries = await getEntries(_ui.instance.encryption); + updateCode(_ui.instance); + return; + }, + // TODO: Figure out what event & data are supposed to be typed as + importFile: (event: Event) => { + const target = event.target as HTMLInputElement; + if (!target || !target.files) { + return; + } + if (target.files[0] && target.files[0].type.startsWith('text/')) { + const reader = new FileReader(); + reader.onload = () => { + const importData = JSON.parse(reader.result); + console.log(importData); + // Replace data with import data + // if current data has codes insert and check for duplicates + }; + reader.readAsText(target.files[0]); + } + return; + }, + removeEntry: async (entry: OTPEntry) => { + if (await _ui.instance.confirm( + _ui.instance.i18n.confirm_remove_entry)) { + await entry.delete(); + await _ui.instance.updateEntries(); + } + return; + }, + editEntry: () => { + _ui.instance.class.edit = !_ui.instance.class.edit; + const codes = document.getElementById('codes'); + if (codes) { + // wait vue apply changes to dom + setTimeout(() => { + codes.scrollTop = _ui.instance.class.edit ? codes.scrollHeight : 0; + }, 0); + } + return; + }, + nextCode: async (entry: OTPEntry) => { + if (_ui.instance.class.hotpDiabled) { + return; + } + _ui.instance.class.hotpDiabled = true; + await entry.next(_ui.instance.encryption); + await _ui.instance.updateEntries(); + setTimeout(() => { + _ui.instance.class.hotpDiabled = false; + }, 3000); + return; + }, + copyCode: (entry: OTPEntry) => { + if (_ui.instance.class.edit) { + return; + } + + if (entry.code === 'Encrypted') { + _ui.instance.showInfo('passphrase'); + return; + } + chrome.permissions.request( + {permissions: ['clipboardWrite']}, (granted) => { + if (granted) { + const codeClipboard = document.getElementById( + 'codeClipboard') as HTMLInputElement; + if (!codeClipboard) { + return; + } + codeClipboard.value = entry.code; + codeClipboard.focus(); + codeClipboard.select(); + document.execCommand('Copy'); + _ui.instance.notification = _ui.instance.i18n.copied; + clearTimeout(_ui.instance.notificationTimeout); + _ui.instance.class.notificationFadein = true; + _ui.instance.class.notificationFadeout = false; + _ui.instance.notificationTimeout = setTimeout(() => { + _ui.instance.class.notificationFadein = false; + _ui.instance.class.notificationFadeout = true; + setTimeout(() => { + _ui.instance.class.notificationFadeout = false; + }, 200); + }, 1000); + } + }); + return; + }, + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/i18n.ts b/src/ui/i18n.ts new file mode 100644 index 000000000..8b6086c79 --- /dev/null +++ b/src/ui/i18n.ts @@ -0,0 +1,37 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function loadI18nMessages() { + return new Promise( + (resolve: (value: {[key: string]: string}) => void, + reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); + const i18nData: {[key: string]: string} = {}; + for (const key of Object.keys(i18nMessage)) { + i18nData[key] = chrome.i18n.getMessage(key); + } + return resolve(i18nData); + } + return; + }; + xhr.open( + 'GET', chrome.extension.getURL('/_locales/en/messages.json')); + xhr.send(); + } catch (error) { + return reject(error); + } + }); +} + +async function i18n(_ui: UI) { + const i18n = await loadI18nMessages(); + + const ui: UIConfig = {data: {i18n}}; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/info.ts b/src/ui/info.ts new file mode 100644 index 000000000..588fb04b0 --- /dev/null +++ b/src/ui/info.ts @@ -0,0 +1,28 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function info(_ui: UI) { + const ui: UIConfig = { + data: {info: ''}, + methods: { + showInfo: (tab: string) => { + _ui.instance.class.fadein = true; + _ui.instance.class.fadeout = false; + _ui.instance.info = tab; + return; + }, + closeInfo: () => { + _ui.instance.class.fadein = false; + _ui.instance.class.fadeout = true; + setTimeout(() => { + _ui.instance.class.fadeout = false; + _ui.instance.info = ''; + }, 200); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/menu.ts b/src/ui/menu.ts new file mode 100644 index 000000000..6ab101d41 --- /dev/null +++ b/src/ui/menu.ts @@ -0,0 +1,95 @@ +/* tslint:disable:no-reference */ +/// +/// + +function getVersion() { + return chrome.runtime.getManifest().version; +} + +async function syncTimeWithGoogle() { + return new Promise( + (resolve: (value: string) => void, reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.open('HEAD', 'https://www.google.com/generate_204'); + const xhrAbort = setTimeout(() => { + xhr.abort(); + return resolve('updateFailure'); + }, 5000); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + clearTimeout(xhrAbort); + const date = xhr.getResponseHeader('date'); + if (!date) { + return resolve('updateFailure'); + } + const serverTime = new Date(date).getTime(); + const clientTime = new Date().getTime(); + const offset = Math.round((serverTime - clientTime) / 1000); + + if (Math.abs(offset) <= 300) { // within 5 minutes + localStorage.offset = + Math.round((serverTime - clientTime) / 1000); + return resolve('updateSuccess'); + } else { + return resolve('clock_too_far_off'); + } + } + }; + xhr.send(); + } catch (error) { + return reject(error); + } + }); +} + +function resize(zoom: number) { + if (zoom !== 100) { + document.body.style.marginBottom = 480 * (zoom / 100 - 1) + 'px'; + document.body.style.marginRight = 320 * (zoom / 100 - 1) + 'px'; + document.body.style.transform = 'scale(' + (zoom / 100) + ')'; + } +} + +async function menu(_ui: UI) { + const version = getVersion(); + const zoom = Number(localStorage.zoom) || 100; + resize(zoom); + + const ui: UIConfig = { + data: {version, zoom}, + methods: { + showMenu: () => { + _ui.instance.class.slidein = true; + _ui.instance.class.slideout = false; + return; + }, + closeMenu: () => { + _ui.instance.class.slidein = false; + _ui.instance.class.slideout = true; + setTimeout(() => { + _ui.instance.class.slideout = false; + }, 200); + return; + }, + saveZoom: () => { + localStorage.zoom = _ui.instance.zoom; + resize(_ui.instance.zoom); + return; + }, + syncClock: async () => { + chrome.permissions.request( + {origins: ['https://www.google.com/']}, async (granted) => { + if (granted) { + const message = await syncTimeWithGoogle(); + _ui.instance.message = _ui.instance.i18n[message]; + } + return; + }); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/message.ts b/src/ui/message.ts new file mode 100644 index 000000000..e4e03ddb7 --- /dev/null +++ b/src/ui/message.ts @@ -0,0 +1,42 @@ +/* tslint:disable:no-reference */ +/// +/// + +function isCustomEvent(event: Event): event is CustomEvent { + return 'detail' in event; +} + +async function message(_ui: UI) { + const ui: UIConfig = { + data: {message: '', confirmMessage: ''}, + methods: { + confirm: async (message: string) => { + return new Promise( + (resolve: (value: boolean) => void, + reject: (reason: Error) => void) => { + _ui.instance.confirmMessage = message; + window.addEventListener('confirm', (event) => { + _ui.instance.confirmMessage = ''; + if (!isCustomEvent(event)) { + return resolve(false); + } + return resolve(event.detail); + }); + return; + }); + }, + confirmOK: () => { + const confirmEvent = new CustomEvent('confirm', {detail: true}); + window.dispatchEvent(confirmEvent); + return; + }, + confirmCancel: () => { + const confirmEvent = new CustomEvent('confirm', {detail: false}); + window.dispatchEvent(confirmEvent); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts new file mode 100644 index 000000000..11cc61214 --- /dev/null +++ b/src/ui/passphrase.ts @@ -0,0 +1,31 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function passphrase(_ui: UI) { + const ui: UIConfig = { + data: {passphrase: ''}, + methods: { + applyPassphrase: async () => { + _ui.instance.encryption.updateEncryptionPassword( + _ui.instance.passphrase); + await _ui.instance.updateEntries(); + _ui.instance.closeInfo(); + return; + }, + changePassphrase: async () => { + if (_ui.instance.newPassphrase.phrase !== + _ui.instance.newPassphrase.confirm) { + _ui.instance.message = _ui.instance.i18n.phrase_not_match; + return; + } + _ui.instance.encryption.updateEncryptionPassword( + _ui.instance.newPassphrase.phrase); + await _ui.instance.importEnties(); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/qr.ts b/src/ui/qr.ts new file mode 100644 index 000000000..a45570963 --- /dev/null +++ b/src/ui/qr.ts @@ -0,0 +1,62 @@ +/* tslint:disable:no-reference */ +/// +/// + +/* tslint:disable-next-line:no-any */ +declare var QRCode: any; + +async function getQrUrl(entry: OTPEntry) { + return new Promise( + (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 otpauth = 'otpauth://' + type + '/' + label + + '?secret=' + entry.secret + + (entry.issuer ? ('&issuer=' + entry.issuer.split('::')[0]) : '') + + ((entry.type === OTPType.hotp) ? ('&counter=' + entry.counter) : + ''); + /* tslint:disable-next-line:no-unused-expression */ + new QRCode( + 'qr', { + text: otpauth, + width: 128, + height: 128, + colorDark: '#000000', + colorLight: '#ffffff', + correctLevel: QRCode.CorrectLevel.L + }, + resolve); + return; + }); +} + +async function qr(_ui: UI) { + const ui: UIConfig = { + data: {qr: ''}, + methods: { + shouldShowQrIcon: (entry: OTPEntry) => { + return entry.secret !== 'Encrypted' && entry.type !== OTPType.battle && + entry.type !== OTPType.steam; + }, + showQr: async (entry: OTPEntry) => { + const qrUrl = await getQrUrl(entry); + _ui.instance.qr = `url(${qrUrl})`; + _ui.instance.class.qrfadein = true; + _ui.instance.class.qrfadeout = false; + return; + }, + hideQr: () => { + _ui.instance.class.qrfadein = false; + _ui.instance.class.qrfadeout = true; + setTimeout(() => { + _ui.instance.class.qrfadeout = false; + }, 200); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/ui.ts b/src/ui/ui.ts new file mode 100644 index 000000000..5c4b530d4 --- /dev/null +++ b/src/ui/ui.ts @@ -0,0 +1,39 @@ +/* tslint:disable:no-reference */ +/// + +// need to find a better way to handle Vue types without modules +// we use vue 1.0 here to solve csp issues +/* tslint:disable-next-line:no-any */ +declare var Vue: any; + +class UI { + private ui: UIConfig; + // Vue instance + /* tslint:disable-next-line:no-any */ + instance: any; + + constructor(ui: UIConfig) { + this.ui = ui; + } + + update(ui: UIConfig) { + if (ui.data) { + this.ui.data = this.ui.data || {}; + for (const key of Object.keys(ui.data)) { + this.ui.data[key] = ui.data[key]; + } + } + + if (ui.methods) { + this.ui.methods = this.ui.methods || {}; + for (const key of Object.keys(ui.methods)) { + this.ui.methods[key] = ui.methods[key]; + } + } + } + + generate() { + this.instance = new Vue(this.ui); + return this.instance; + } +} \ No newline at end of file From f67a3123dc38393b234982fd4becf65aefbbd735 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 10 Feb 2018 05:20:12 +0800 Subject: [PATCH 043/178] Revert "split popup" This reverts commit 99014d667b9fda1354636c2fb24fd27e0036ef77. --- popup.html | 10 - src/models/interface.ts | 12 - src/popup.ts | 543 ++++++++++++++++++++++++++++++++++++++-- src/ui/add-account.ts | 62 ----- src/ui/class.ts | 25 -- src/ui/entry.ts | 180 ------------- src/ui/i18n.ts | 37 --- src/ui/info.ts | 28 --- src/ui/menu.ts | 95 ------- src/ui/message.ts | 42 ---- src/ui/passphrase.ts | 31 --- src/ui/qr.ts | 62 ----- src/ui/ui.ts | 39 --- 13 files changed, 520 insertions(+), 646 deletions(-) delete mode 100644 src/ui/add-account.ts delete mode 100644 src/ui/class.ts delete mode 100644 src/ui/entry.ts delete mode 100644 src/ui/i18n.ts delete mode 100644 src/ui/info.ts delete mode 100644 src/ui/menu.ts delete mode 100644 src/ui/message.ts delete mode 100644 src/ui/passphrase.ts delete mode 100644 src/ui/qr.ts delete mode 100644 src/ui/ui.ts diff --git a/popup.html b/popup.html index d77ec5175..b8f2a7709 100644 --- a/popup.html +++ b/popup.html @@ -162,16 +162,6 @@
- - - - - - - - - - diff --git a/src/models/interface.ts b/src/models/interface.ts index 97efcd7a4..6c8c013f3 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -39,16 +39,4 @@ interface OTPStorage { /* tslint:disable-next-line:interface-name */ interface I18nMessage { [key: string]: {message: string, description: string}; -} - -interface UIConfig { - el?: string; - data?: { - /* tslint:disable-next-line:no-any */ - [name: string]: any - }; - methods?: { - /* tslint:disable-next-line:no-any */ - [name: string]: (...arg: any[]) => any - }; } \ No newline at end of file diff --git a/src/popup.ts b/src/popup.ts index 690e87fa4..1f5ea8efc 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -1,31 +1,528 @@ /* tslint:disable:no-reference */ -/// -/// -/// -/// -/// -/// -/// -/// -/// -/// +/// +/// +/// + +// need to find a better way to handle Vue types without modules +// we use vue 1.0 here to solve csp issues +/* tslint:disable-next-line:no-any */ +declare var Vue: any; + +/* tslint:disable-next-line:no-any */ +declare var QRCode: any; + +async function getEntries(encryption: Encryption) { + const optEntries: OTPEntry[] = await EntryStorage.get(encryption); + return optEntries; +} + +function getVersion() { + return chrome.runtime.getManifest().version; +} + +async function loadI18nMessages() { + return new Promise( + (resolve: (value: {[key: string]: string}) => void, + reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); + const i18nData: {[key: string]: string} = {}; + for (const key of Object.keys(i18nMessage)) { + i18nData[key] = chrome.i18n.getMessage(key); + } + return resolve(i18nData); + } + return; + }; + xhr.open( + 'GET', chrome.extension.getURL('/_locales/en/messages.json')); + xhr.send(); + } catch (error) { + return reject(error); + } + }); +} + +async function syncTimeWithGoogle() { + return new Promise( + (resolve: (value: string) => void, reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.open('HEAD', 'https://www.google.com/generate_204'); + const xhrAbort = setTimeout(() => { + xhr.abort(); + return resolve('updateFailure'); + }, 5000); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + clearTimeout(xhrAbort); + const date = xhr.getResponseHeader('date'); + if (!date) { + return resolve('updateFailure'); + } + const serverTime = new Date(date).getTime(); + const clientTime = new Date().getTime(); + const offset = Math.round((serverTime - clientTime) / 1000); + + if (Math.abs(offset) <= 300) { // within 5 minutes + localStorage.offset = + Math.round((serverTime - clientTime) / 1000); + return resolve('updateSuccess'); + } else { + return resolve('clock_too_far_off'); + } + } + }; + xhr.send(); + } catch (error) { + return reject(error); + } + }); +} + +/* tslint:disable-next-line:no-any */ +async function updateCode(app: any) { + let second = new Date().getSeconds(); + if (localStorage.offset) { + second += Number(localStorage.offset) + 30; + } + second = second % 30; + app.sector = getSector(second); + if (second > 25) { + app.class.timeout = true; + } else { + app.class.timeout = false; + } + if (second < 1) { + const entries = app.entries as OTP[]; + for (let i = 0; i < entries.length; i++) { + if (entries[i].type !== OTPType.hotp) { + entries[i].generate(); + } + } + } +} + +function getSector(second: number) { + const canvas = document.createElement('canvas'); + canvas.width = 40; + canvas.height = 40; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + ctx.fillStyle = '#888'; + ctx.beginPath(); + ctx.moveTo(20, 20); + ctx.arc( + 20, 20, 16, second / 30 * Math.PI * 2 - Math.PI / 2, Math.PI * 3 / 2, + false); + ctx.fill(); + const url = canvas.toDataURL(); + return `url(${url}) center / 20px 20px`; +} + +function resize(zoom: number) { + if (zoom !== 100) { + document.body.style.marginBottom = 480 * (zoom / 100 - 1) + 'px'; + document.body.style.marginRight = 320 * (zoom / 100 - 1) + 'px'; + document.body.style.transform = 'scale(' + (zoom / 100) + ')'; + } +} + +async function getQrUrl(entry: OTPEntry) { + return new Promise( + (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 otpauth = 'otpauth://' + type + '/' + label + + '?secret=' + entry.secret + + (entry.issuer ? ('&issuer=' + entry.issuer.split('::')[0]) : '') + + ((entry.type === OTPType.hotp) ? ('&counter=' + entry.counter) : + ''); + /* tslint:disable-next-line:no-unused-expression */ + new QRCode( + 'qr', { + text: otpauth, + width: 128, + height: 128, + colorDark: '#000000', + colorLight: '#ffffff', + correctLevel: QRCode.CorrectLevel.L + }, + resolve); + return; + }); +} + +function isCustomEvent(event: Event): event is CustomEvent { + return 'detail' in event; +} + +function getBackupFile(entryData: {[hash: string]: OTPStorage}) { + let json = JSON.stringify(entryData, null, 2); + // for windows notepad + json = json.replace(/\n/g, '\r\n'); + const base64Data = btoa(json); + return `data:application/octet-stream;base64,${base64Data}`; +} async function init() { - const ui = new UI({el: '#authenticator'}); + const zoom = Number(localStorage.zoom) || 100; + resize(zoom); + + const version = getVersion(); + const i18n = await loadI18nMessages(); + const encryption: Encryption = new Encryption(''); + const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); + const exportData = + shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); + const entries = shouldShowPassphrase ? [] : await getEntries(encryption); + const exportFile = getBackupFile(exportData); + + const authenticator = new Vue({ + el: '#authenticator', + data: { + version, + i18n, + entries, + encryption, + zoom, + OTPType, + exportData: JSON.stringify(exportData, null, 2), + exportFile, + class: { + timeout: false, + edit: false, + slidein: false, + slideout: false, + fadein: false, + fadeout: false, + qrfadein: false, + qrfadeout: false, + notificationFadein: false, + notificationFadeout: false, + hotpDiabled: false + }, + sector: '', + info: '', + message: '', + confirmMessage: '', + qr: '', + notification: '', + passphrase: '', + notificationTimeout: 0, + newAccount: {show: false, account: '', secret: '', type: OTPType.totp}, + newPassphrase: {phrase: '', confirm: ''} + }, + methods: { + showBulls: (code: string) => { + return new Array(code.length).fill('•').join(''); + }, + showMenu: () => { + authenticator.class.slidein = true; + authenticator.class.slideout = false; + return; + }, + closeMenu: () => { + authenticator.class.slidein = false; + authenticator.class.slideout = true; + setTimeout(() => { + authenticator.class.slideout = false; + }, 200); + return; + }, + showInfo: (tab: string) => { + if (tab === 'export' || tab === 'security') { + const entries = authenticator.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // cannot export account data + // or change passphrase + if (entries[i].code === 'Encrypted') { + authenticator.message = authenticator.i18n.phrase_incorrect; + return; + } + } + } + authenticator.class.fadein = true; + authenticator.class.fadeout = false; + authenticator.info = tab; + return; + }, + closeInfo: () => { + authenticator.class.fadein = false; + authenticator.class.fadeout = true; + setTimeout(() => { + authenticator.class.fadeout = false; + authenticator.info = ''; + authenticator.newAccount.show = false; + }, 200); + return; + }, + importEnties: async () => { + await EntryStorage.import( + authenticator.encryption, JSON.parse(authenticator.exportData)); + await authenticator.updateEntries(); + authenticator.message = authenticator.i18n.updateSuccess; + return; + }, + updateEntries: async () => { + const exportData = + await EntryStorage.getExport(authenticator.encryption); + authenticator.exportData = JSON.stringify(exportData, null, 2); + authenticator.entries = await getEntries(authenticator.encryption); + authenticator.exportFile = getBackupFile(exportData); + updateCode(authenticator); + return; + }, + // TODO: Figure out what event & data are supposed to be typed as + importFile: (event: Event) => { + const target = event.target as HTMLInputElement; + if (!target || !target.files) { + return; + } + if (target.files[0] && target.files[0].type.startsWith('text/')) { + const reader = new FileReader(); + reader.onload = () => { + const importData = JSON.parse(reader.result); + console.log(importData); + // Replace data with import data + // if current data has codes insert and check for duplicates + }; + reader.readAsText(target.files[0]); + } + return; + }, + saveZoom: () => { + localStorage.zoom = authenticator.zoom; + resize(authenticator.zoom); + return; + }, + removeEntry: async (entry: OTPEntry) => { + if (await authenticator.confirm(authenticator.i18n.confirm_delete)) { + await entry.delete(); + await authenticator.updateEntries(); + } + return; + }, + syncClock: async () => { + chrome.permissions.request( + {origins: ['https://www.google.com/']}, async (granted) => { + if (granted) { + const message = await syncTimeWithGoogle(); + authenticator.message = authenticator.i18n[message]; + } + return; + }); + return; + }, + editEntry: () => { + authenticator.class.edit = !authenticator.class.edit; + const codes = document.getElementById('codes'); + if (codes) { + // wait vue apply changes to dom + setTimeout(() => { + codes.scrollTop = authenticator.class.edit ? codes.scrollHeight : 0; + }, 0); + } + return; + }, + shouldShowQrIcon: (entry: OTPEntry) => { + return entry.secret !== 'Encrypted' && entry.type !== OTPType.battle && + entry.type !== OTPType.steam; + }, + showQr: async (entry: OTPEntry) => { + const qrUrl = await getQrUrl(entry); + authenticator.qr = `url(${qrUrl})`; + authenticator.class.qrfadein = true; + authenticator.class.qrfadeout = false; + return; + }, + hideQr: () => { + authenticator.class.qrfadein = false; + authenticator.class.qrfadeout = true; + setTimeout(() => { + authenticator.class.qrfadeout = false; + }, 200); + return; + }, + copyCode: (entry: OTPEntry) => { + if (authenticator.class.edit) { + return; + } - await className(ui); - await i18n(ui); - await menu(ui); - await info(ui); - await passphrase(ui); - await entry(ui); - await qr(ui); - await message(ui); - await addAccount(ui); + if (entry.code === 'Encrypted') { + authenticator.showInfo('passphrase'); + return; + } + chrome.permissions.request( + {permissions: ['clipboardWrite']}, (granted) => { + if (granted) { + const codeClipboard = document.getElementById( + 'codeClipboard') as HTMLInputElement; + if (!codeClipboard) { + return; + } + codeClipboard.value = entry.code; + codeClipboard.focus(); + codeClipboard.select(); + document.execCommand('Copy'); + authenticator.notification = authenticator.i18n.copied; + clearTimeout(authenticator.notificationTimeout); + authenticator.class.notificationFadein = true; + authenticator.class.notificationFadeout = false; + authenticator.notificationTimeout = setTimeout(() => { + authenticator.class.notificationFadein = false; + authenticator.class.notificationFadeout = true; + setTimeout(() => { + authenticator.class.notificationFadeout = false; + }, 200); + }, 1000); + } + }); + return; + }, + addAccountManually: () => { + const entries = authenticator.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // shouldn't add new account with + // the current passphrass + if (entries[i].code === 'Encrypted') { + authenticator.message = authenticator.i18n.phrase_incorrect; + return; + } + } - const authenticator = ui.generate(); + authenticator.newAccount.show = true; + }, + addNewAccount: async () => { + let type: OTPType; + if (!/^[a-z2-7]+=*$/i.test(authenticator.newAccount.secret) && + /^[0-9a-f]+$/i.test(authenticator.newAccount.secret)) { + type = OTPType.hex; + } else { + type = authenticator.newAccount.type; + } + + const entry = new OTPEntry( + type, '', authenticator.newAccount.secret, + authenticator.newAccount.account, 0, 0); + await entry.create(authenticator.encryption); + await authenticator.updateEntries(); + authenticator.newAccount.type = OTPType.totp; + authenticator.account = ''; + authenticator.secret = ''; + authenticator.newAccount.show = false; + authenticator.closeInfo(); + authenticator.class.edit = false; + + const codes = document.getElementById('codes'); + if (codes) { + // wait vue apply changes to dom + setTimeout(() => { + codes.scrollTop = 0; + }, 0); + } + return; + }, + confirm: async (message: string) => { + return new Promise( + (resolve: (value: boolean) => void, + reject: (reason: Error) => void) => { + authenticator.confirmMessage = message; + window.addEventListener('confirm', (event) => { + authenticator.confirmMessage = ''; + if (!isCustomEvent(event)) { + return resolve(false); + } + return resolve(event.detail); + }); + return; + }); + }, + confirmOK: () => { + const confirmEvent = new CustomEvent('confirm', {detail: true}); + window.dispatchEvent(confirmEvent); + return; + }, + confirmCancel: () => { + const confirmEvent = new CustomEvent('confirm', {detail: false}); + window.dispatchEvent(confirmEvent); + return; + }, + nextCode: async (entry: OTPEntry) => { + if (authenticator.class.hotpDiabled) { + return; + } + authenticator.class.hotpDiabled = true; + await entry.next(authenticator.encryption); + await authenticator.updateEntries(); + setTimeout(() => { + authenticator.class.hotpDiabled = false; + }, 3000); + return; + }, + applyPassphrase: async () => { + authenticator.encryption.updateEncryptionPassword( + authenticator.passphrase); + await authenticator.updateEntries(); + authenticator.closeInfo(); + return; + }, + changePassphrase: async () => { + if (authenticator.newPassphrase.phrase !== + authenticator.newPassphrase.confirm) { + authenticator.message = authenticator.i18n.phrase_not_match; + return; + } + authenticator.encryption.updateEncryptionPassword( + authenticator.newPassphrase.phrase); + await authenticator.importEnties(); + return; + }, + beginCapture: () => { + const entries = authenticator.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // shouldn't add new account with + // the current passphrass + if (entries[i].code === 'Encrypted') { + authenticator.message = authenticator.i18n.phrase_incorrect; + return; + } + } + + chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { + const tab = tabs[0]; + if (!tab || !tab.id) { + return; + } + chrome.tabs.sendMessage( + tab.id, {action: 'capture', passphrase: authenticator.passphrase}, + (result) => { + if (result !== 'beginCapture') { + authenticator.message = authenticator.i18n.capture_failed; + } else { + window.close(); + } + }); + }); + return; + } + } + }); - if (authenticator.shouldShowPassphrase) { + if (shouldShowPassphrase) { authenticator.showInfo('passphrase'); } @@ -55,4 +552,4 @@ chrome.permissions.contains( } }); -init(); \ No newline at end of file +init(); diff --git a/src/ui/add-account.ts b/src/ui/add-account.ts deleted file mode 100644 index 4a663ff53..000000000 --- a/src/ui/add-account.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// - -async function addAccount(_ui: UI) { - const ui: UIConfig = { - data: { - newAccount: {show: false, account: '', secret: '', type: OTPType.totp}, - newPassphrase: {phrase: '', confirm: ''} - }, - methods: { - addNewAccount: async () => { - let type: OTPType; - if (!/^[a-z2-7]+=*$/i.test(_ui.instance.newAccount.secret) && - /^[0-9a-f]+$/i.test(_ui.instance.newAccount.secret)) { - type = OTPType.hex; - } else { - type = _ui.instance.newAccount.type; - } - - const entry = new OTPEntry( - type, '', _ui.instance.newAccount.secret, - _ui.instance.newAccount.account, 0, 0); - await entry.create(_ui.instance.encryption); - await _ui.instance.updateEntries(); - _ui.instance.newAccount.type = OTPType.totp; - _ui.instance.account = ''; - _ui.instance.secret = ''; - _ui.instance.newAccount.show = false; - _ui.instance.closeInfo(); - _ui.instance.class.edit = false; - - const codes = document.getElementById('codes'); - if (codes) { - // wait vue apply changes to dom - setTimeout(() => { - codes.scrollTop = 0; - }, 0); - } - return; - }, - beginCapture: () => { - chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { - const tab = tabs[0]; - if (!tab || !tab.id) { - return; - } - chrome.tabs.sendMessage(tab.id, {action: 'capture'}, (result) => { - if (result !== 'beginCapture') { - _ui.instance.message = _ui.instance.i18n.capture_failed; - } else { - window.close(); - } - }); - }); - return; - } - } - }; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/class.ts b/src/ui/class.ts deleted file mode 100644 index de34d8545..000000000 --- a/src/ui/class.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// - -async function className(_ui: UI) { - const ui: UIConfig = { - data: { - class: { - timeout: false, - edit: false, - slidein: false, - slideout: false, - fadein: false, - fadeout: false, - qrfadein: false, - qrfadeout: false, - notificationFadein: false, - notificationFadeout: false, - hotpDiabled: false - } - } - }; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/entry.ts b/src/ui/entry.ts deleted file mode 100644 index d5acf3b1b..000000000 --- a/src/ui/entry.ts +++ /dev/null @@ -1,180 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// -/// -/// - -async function getEntries(encryption: Encryption) { - const optEntries: OTPEntry[] = await EntryStorage.get(encryption); - return optEntries; -} - -/* tslint:disable-next-line:no-any */ -async function updateCode(app: any) { - let second = new Date().getSeconds(); - if (localStorage.offset) { - second += Number(localStorage.offset) + 30; - } - second = second % 30; - app.sector = getSector(second); - if (second > 25) { - app.class.timeout = true; - } else { - app.class.timeout = false; - } - if (second < 1) { - const entries = app.entries as OTP[]; - for (let i = 0; i < entries.length; i++) { - if (entries[i].type !== OTPType.hotp) { - entries[i].generate(); - } - } - } -} - -function getSector(second: number) { - const canvas = document.createElement('canvas'); - canvas.width = 40; - canvas.height = 40; - const ctx = canvas.getContext('2d'); - if (!ctx) { - return; - } - ctx.fillStyle = '#888'; - ctx.beginPath(); - ctx.moveTo(20, 20); - ctx.arc( - 20, 20, 16, second / 30 * Math.PI * 2 - Math.PI / 2, Math.PI * 3 / 2, - false); - ctx.fill(); - const url = canvas.toDataURL(); - return `url(${url}) center / 20px 20px`; -} - -async function entry(_ui: UI) { - const encryption: Encryption = new Encryption(''); - const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); - const exportData = - shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); - const entries = shouldShowPassphrase ? [] : await getEntries(encryption); - - const ui: UIConfig = { - data: { - entries, - encryption, - OTPType, - shouldShowPassphrase, - exportData: JSON.stringify(exportData, null, 2), - sector: '', - notification: '', - notificationTimeout: 0 - }, - methods: { - showBulls: (code: string) => { - return new Array(code.length).fill('•').join(''); - }, - importEnties: async () => { - await EntryStorage.import( - _ui.instance.encryption, JSON.parse(_ui.instance.exportData)); - await _ui.instance.updateEntries(); - _ui.instance.message = _ui.instance.i18n.updateSuccess; - return; - }, - updateEntries: async () => { - const exportData = - await EntryStorage.getExport(_ui.instance.encryption); - _ui.instance.exportData = JSON.stringify(exportData, null, 2); - _ui.instance.entries = await getEntries(_ui.instance.encryption); - updateCode(_ui.instance); - return; - }, - // TODO: Figure out what event & data are supposed to be typed as - importFile: (event: Event) => { - const target = event.target as HTMLInputElement; - if (!target || !target.files) { - return; - } - if (target.files[0] && target.files[0].type.startsWith('text/')) { - const reader = new FileReader(); - reader.onload = () => { - const importData = JSON.parse(reader.result); - console.log(importData); - // Replace data with import data - // if current data has codes insert and check for duplicates - }; - reader.readAsText(target.files[0]); - } - return; - }, - removeEntry: async (entry: OTPEntry) => { - if (await _ui.instance.confirm( - _ui.instance.i18n.confirm_remove_entry)) { - await entry.delete(); - await _ui.instance.updateEntries(); - } - return; - }, - editEntry: () => { - _ui.instance.class.edit = !_ui.instance.class.edit; - const codes = document.getElementById('codes'); - if (codes) { - // wait vue apply changes to dom - setTimeout(() => { - codes.scrollTop = _ui.instance.class.edit ? codes.scrollHeight : 0; - }, 0); - } - return; - }, - nextCode: async (entry: OTPEntry) => { - if (_ui.instance.class.hotpDiabled) { - return; - } - _ui.instance.class.hotpDiabled = true; - await entry.next(_ui.instance.encryption); - await _ui.instance.updateEntries(); - setTimeout(() => { - _ui.instance.class.hotpDiabled = false; - }, 3000); - return; - }, - copyCode: (entry: OTPEntry) => { - if (_ui.instance.class.edit) { - return; - } - - if (entry.code === 'Encrypted') { - _ui.instance.showInfo('passphrase'); - return; - } - chrome.permissions.request( - {permissions: ['clipboardWrite']}, (granted) => { - if (granted) { - const codeClipboard = document.getElementById( - 'codeClipboard') as HTMLInputElement; - if (!codeClipboard) { - return; - } - codeClipboard.value = entry.code; - codeClipboard.focus(); - codeClipboard.select(); - document.execCommand('Copy'); - _ui.instance.notification = _ui.instance.i18n.copied; - clearTimeout(_ui.instance.notificationTimeout); - _ui.instance.class.notificationFadein = true; - _ui.instance.class.notificationFadeout = false; - _ui.instance.notificationTimeout = setTimeout(() => { - _ui.instance.class.notificationFadein = false; - _ui.instance.class.notificationFadeout = true; - setTimeout(() => { - _ui.instance.class.notificationFadeout = false; - }, 200); - }, 1000); - } - }); - return; - }, - } - }; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/i18n.ts b/src/ui/i18n.ts deleted file mode 100644 index 8b6086c79..000000000 --- a/src/ui/i18n.ts +++ /dev/null @@ -1,37 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// - -async function loadI18nMessages() { - return new Promise( - (resolve: (value: {[key: string]: string}) => void, - reject: (reason: Error) => void) => { - try { - const xhr = new XMLHttpRequest(); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); - const i18nData: {[key: string]: string} = {}; - for (const key of Object.keys(i18nMessage)) { - i18nData[key] = chrome.i18n.getMessage(key); - } - return resolve(i18nData); - } - return; - }; - xhr.open( - 'GET', chrome.extension.getURL('/_locales/en/messages.json')); - xhr.send(); - } catch (error) { - return reject(error); - } - }); -} - -async function i18n(_ui: UI) { - const i18n = await loadI18nMessages(); - - const ui: UIConfig = {data: {i18n}}; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/info.ts b/src/ui/info.ts deleted file mode 100644 index 588fb04b0..000000000 --- a/src/ui/info.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// - -async function info(_ui: UI) { - const ui: UIConfig = { - data: {info: ''}, - methods: { - showInfo: (tab: string) => { - _ui.instance.class.fadein = true; - _ui.instance.class.fadeout = false; - _ui.instance.info = tab; - return; - }, - closeInfo: () => { - _ui.instance.class.fadein = false; - _ui.instance.class.fadeout = true; - setTimeout(() => { - _ui.instance.class.fadeout = false; - _ui.instance.info = ''; - }, 200); - return; - } - } - }; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/menu.ts b/src/ui/menu.ts deleted file mode 100644 index 6ab101d41..000000000 --- a/src/ui/menu.ts +++ /dev/null @@ -1,95 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// - -function getVersion() { - return chrome.runtime.getManifest().version; -} - -async function syncTimeWithGoogle() { - return new Promise( - (resolve: (value: string) => void, reject: (reason: Error) => void) => { - try { - const xhr = new XMLHttpRequest(); - xhr.open('HEAD', 'https://www.google.com/generate_204'); - const xhrAbort = setTimeout(() => { - xhr.abort(); - return resolve('updateFailure'); - }, 5000); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - clearTimeout(xhrAbort); - const date = xhr.getResponseHeader('date'); - if (!date) { - return resolve('updateFailure'); - } - const serverTime = new Date(date).getTime(); - const clientTime = new Date().getTime(); - const offset = Math.round((serverTime - clientTime) / 1000); - - if (Math.abs(offset) <= 300) { // within 5 minutes - localStorage.offset = - Math.round((serverTime - clientTime) / 1000); - return resolve('updateSuccess'); - } else { - return resolve('clock_too_far_off'); - } - } - }; - xhr.send(); - } catch (error) { - return reject(error); - } - }); -} - -function resize(zoom: number) { - if (zoom !== 100) { - document.body.style.marginBottom = 480 * (zoom / 100 - 1) + 'px'; - document.body.style.marginRight = 320 * (zoom / 100 - 1) + 'px'; - document.body.style.transform = 'scale(' + (zoom / 100) + ')'; - } -} - -async function menu(_ui: UI) { - const version = getVersion(); - const zoom = Number(localStorage.zoom) || 100; - resize(zoom); - - const ui: UIConfig = { - data: {version, zoom}, - methods: { - showMenu: () => { - _ui.instance.class.slidein = true; - _ui.instance.class.slideout = false; - return; - }, - closeMenu: () => { - _ui.instance.class.slidein = false; - _ui.instance.class.slideout = true; - setTimeout(() => { - _ui.instance.class.slideout = false; - }, 200); - return; - }, - saveZoom: () => { - localStorage.zoom = _ui.instance.zoom; - resize(_ui.instance.zoom); - return; - }, - syncClock: async () => { - chrome.permissions.request( - {origins: ['https://www.google.com/']}, async (granted) => { - if (granted) { - const message = await syncTimeWithGoogle(); - _ui.instance.message = _ui.instance.i18n[message]; - } - return; - }); - return; - } - } - }; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/message.ts b/src/ui/message.ts deleted file mode 100644 index e4e03ddb7..000000000 --- a/src/ui/message.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// - -function isCustomEvent(event: Event): event is CustomEvent { - return 'detail' in event; -} - -async function message(_ui: UI) { - const ui: UIConfig = { - data: {message: '', confirmMessage: ''}, - methods: { - confirm: async (message: string) => { - return new Promise( - (resolve: (value: boolean) => void, - reject: (reason: Error) => void) => { - _ui.instance.confirmMessage = message; - window.addEventListener('confirm', (event) => { - _ui.instance.confirmMessage = ''; - if (!isCustomEvent(event)) { - return resolve(false); - } - return resolve(event.detail); - }); - return; - }); - }, - confirmOK: () => { - const confirmEvent = new CustomEvent('confirm', {detail: true}); - window.dispatchEvent(confirmEvent); - return; - }, - confirmCancel: () => { - const confirmEvent = new CustomEvent('confirm', {detail: false}); - window.dispatchEvent(confirmEvent); - return; - } - } - }; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts deleted file mode 100644 index 11cc61214..000000000 --- a/src/ui/passphrase.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// - -async function passphrase(_ui: UI) { - const ui: UIConfig = { - data: {passphrase: ''}, - methods: { - applyPassphrase: async () => { - _ui.instance.encryption.updateEncryptionPassword( - _ui.instance.passphrase); - await _ui.instance.updateEntries(); - _ui.instance.closeInfo(); - return; - }, - changePassphrase: async () => { - if (_ui.instance.newPassphrase.phrase !== - _ui.instance.newPassphrase.confirm) { - _ui.instance.message = _ui.instance.i18n.phrase_not_match; - return; - } - _ui.instance.encryption.updateEncryptionPassword( - _ui.instance.newPassphrase.phrase); - await _ui.instance.importEnties(); - return; - } - } - }; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/qr.ts b/src/ui/qr.ts deleted file mode 100644 index a45570963..000000000 --- a/src/ui/qr.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* tslint:disable:no-reference */ -/// -/// - -/* tslint:disable-next-line:no-any */ -declare var QRCode: any; - -async function getQrUrl(entry: OTPEntry) { - return new Promise( - (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 otpauth = 'otpauth://' + type + '/' + label + - '?secret=' + entry.secret + - (entry.issuer ? ('&issuer=' + entry.issuer.split('::')[0]) : '') + - ((entry.type === OTPType.hotp) ? ('&counter=' + entry.counter) : - ''); - /* tslint:disable-next-line:no-unused-expression */ - new QRCode( - 'qr', { - text: otpauth, - width: 128, - height: 128, - colorDark: '#000000', - colorLight: '#ffffff', - correctLevel: QRCode.CorrectLevel.L - }, - resolve); - return; - }); -} - -async function qr(_ui: UI) { - const ui: UIConfig = { - data: {qr: ''}, - methods: { - shouldShowQrIcon: (entry: OTPEntry) => { - return entry.secret !== 'Encrypted' && entry.type !== OTPType.battle && - entry.type !== OTPType.steam; - }, - showQr: async (entry: OTPEntry) => { - const qrUrl = await getQrUrl(entry); - _ui.instance.qr = `url(${qrUrl})`; - _ui.instance.class.qrfadein = true; - _ui.instance.class.qrfadeout = false; - return; - }, - hideQr: () => { - _ui.instance.class.qrfadein = false; - _ui.instance.class.qrfadeout = true; - setTimeout(() => { - _ui.instance.class.qrfadeout = false; - }, 200); - return; - } - } - }; - - _ui.update(ui); -} \ No newline at end of file diff --git a/src/ui/ui.ts b/src/ui/ui.ts deleted file mode 100644 index 5c4b530d4..000000000 --- a/src/ui/ui.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* tslint:disable:no-reference */ -/// - -// need to find a better way to handle Vue types without modules -// we use vue 1.0 here to solve csp issues -/* tslint:disable-next-line:no-any */ -declare var Vue: any; - -class UI { - private ui: UIConfig; - // Vue instance - /* tslint:disable-next-line:no-any */ - instance: any; - - constructor(ui: UIConfig) { - this.ui = ui; - } - - update(ui: UIConfig) { - if (ui.data) { - this.ui.data = this.ui.data || {}; - for (const key of Object.keys(ui.data)) { - this.ui.data[key] = ui.data[key]; - } - } - - if (ui.methods) { - this.ui.methods = this.ui.methods || {}; - for (const key of Object.keys(ui.methods)) { - this.ui.methods[key] = ui.methods[key]; - } - } - } - - generate() { - this.instance = new Vue(this.ui); - return this.instance; - } -} \ No newline at end of file From afe42e748339cbdde507ea30e9aecd049bfce1dc Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Sat, 10 Feb 2018 05:47:53 +0800 Subject: [PATCH 044/178] split popup --- popup.html | 18 +- src/models/interface.ts | 12 + src/popup.ts | 543 ++-------------------------------------- src/ui/add-account.ts | 91 +++++++ src/ui/class.ts | 25 ++ src/ui/entry.ts | 190 ++++++++++++++ src/ui/i18n.ts | 37 +++ src/ui/info.ts | 41 +++ src/ui/menu.ts | 95 +++++++ src/ui/message.ts | 42 ++++ src/ui/passphrase.ts | 31 +++ src/ui/qr.ts | 62 +++++ src/ui/ui.ts | 39 +++ 13 files changed, 702 insertions(+), 524 deletions(-) create mode 100644 src/ui/add-account.ts create mode 100644 src/ui/class.ts create mode 100644 src/ui/entry.ts create mode 100644 src/ui/i18n.ts create mode 100644 src/ui/info.ts create mode 100644 src/ui/menu.ts create mode 100644 src/ui/message.ts create mode 100644 src/ui/passphrase.ts create mode 100644 src/ui/qr.ts create mode 100644 src/ui/ui.ts diff --git a/popup.html b/popup.html index b8f2a7709..97837f918 100644 --- a/popup.html +++ b/popup.html @@ -113,10 +113,10 @@ -
- -
-
{{ i18n.update }}
+
+ +
+
{{ i18n.update }}
@@ -162,6 +162,16 @@
+ + + + + + + + + + diff --git a/src/models/interface.ts b/src/models/interface.ts index 6c8c013f3..97efcd7a4 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -39,4 +39,16 @@ interface OTPStorage { /* tslint:disable-next-line:interface-name */ interface I18nMessage { [key: string]: {message: string, description: string}; +} + +interface UIConfig { + el?: string; + data?: { + /* tslint:disable-next-line:no-any */ + [name: string]: any + }; + methods?: { + /* tslint:disable-next-line:no-any */ + [name: string]: (...arg: any[]) => any + }; } \ No newline at end of file diff --git a/src/popup.ts b/src/popup.ts index 1f5ea8efc..690e87fa4 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -1,528 +1,31 @@ /* tslint:disable:no-reference */ -/// -/// -/// - -// need to find a better way to handle Vue types without modules -// we use vue 1.0 here to solve csp issues -/* tslint:disable-next-line:no-any */ -declare var Vue: any; - -/* tslint:disable-next-line:no-any */ -declare var QRCode: any; - -async function getEntries(encryption: Encryption) { - const optEntries: OTPEntry[] = await EntryStorage.get(encryption); - return optEntries; -} - -function getVersion() { - return chrome.runtime.getManifest().version; -} - -async function loadI18nMessages() { - return new Promise( - (resolve: (value: {[key: string]: string}) => void, - reject: (reason: Error) => void) => { - try { - const xhr = new XMLHttpRequest(); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); - const i18nData: {[key: string]: string} = {}; - for (const key of Object.keys(i18nMessage)) { - i18nData[key] = chrome.i18n.getMessage(key); - } - return resolve(i18nData); - } - return; - }; - xhr.open( - 'GET', chrome.extension.getURL('/_locales/en/messages.json')); - xhr.send(); - } catch (error) { - return reject(error); - } - }); -} - -async function syncTimeWithGoogle() { - return new Promise( - (resolve: (value: string) => void, reject: (reason: Error) => void) => { - try { - const xhr = new XMLHttpRequest(); - xhr.open('HEAD', 'https://www.google.com/generate_204'); - const xhrAbort = setTimeout(() => { - xhr.abort(); - return resolve('updateFailure'); - }, 5000); - xhr.onreadystatechange = () => { - if (xhr.readyState === 4) { - clearTimeout(xhrAbort); - const date = xhr.getResponseHeader('date'); - if (!date) { - return resolve('updateFailure'); - } - const serverTime = new Date(date).getTime(); - const clientTime = new Date().getTime(); - const offset = Math.round((serverTime - clientTime) / 1000); - - if (Math.abs(offset) <= 300) { // within 5 minutes - localStorage.offset = - Math.round((serverTime - clientTime) / 1000); - return resolve('updateSuccess'); - } else { - return resolve('clock_too_far_off'); - } - } - }; - xhr.send(); - } catch (error) { - return reject(error); - } - }); -} - -/* tslint:disable-next-line:no-any */ -async function updateCode(app: any) { - let second = new Date().getSeconds(); - if (localStorage.offset) { - second += Number(localStorage.offset) + 30; - } - second = second % 30; - app.sector = getSector(second); - if (second > 25) { - app.class.timeout = true; - } else { - app.class.timeout = false; - } - if (second < 1) { - const entries = app.entries as OTP[]; - for (let i = 0; i < entries.length; i++) { - if (entries[i].type !== OTPType.hotp) { - entries[i].generate(); - } - } - } -} - -function getSector(second: number) { - const canvas = document.createElement('canvas'); - canvas.width = 40; - canvas.height = 40; - const ctx = canvas.getContext('2d'); - if (!ctx) { - return; - } - ctx.fillStyle = '#888'; - ctx.beginPath(); - ctx.moveTo(20, 20); - ctx.arc( - 20, 20, 16, second / 30 * Math.PI * 2 - Math.PI / 2, Math.PI * 3 / 2, - false); - ctx.fill(); - const url = canvas.toDataURL(); - return `url(${url}) center / 20px 20px`; -} - -function resize(zoom: number) { - if (zoom !== 100) { - document.body.style.marginBottom = 480 * (zoom / 100 - 1) + 'px'; - document.body.style.marginRight = 320 * (zoom / 100 - 1) + 'px'; - document.body.style.transform = 'scale(' + (zoom / 100) + ')'; - } -} - -async function getQrUrl(entry: OTPEntry) { - return new Promise( - (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 otpauth = 'otpauth://' + type + '/' + label + - '?secret=' + entry.secret + - (entry.issuer ? ('&issuer=' + entry.issuer.split('::')[0]) : '') + - ((entry.type === OTPType.hotp) ? ('&counter=' + entry.counter) : - ''); - /* tslint:disable-next-line:no-unused-expression */ - new QRCode( - 'qr', { - text: otpauth, - width: 128, - height: 128, - colorDark: '#000000', - colorLight: '#ffffff', - correctLevel: QRCode.CorrectLevel.L - }, - resolve); - return; - }); -} - -function isCustomEvent(event: Event): event is CustomEvent { - return 'detail' in event; -} - -function getBackupFile(entryData: {[hash: string]: OTPStorage}) { - let json = JSON.stringify(entryData, null, 2); - // for windows notepad - json = json.replace(/\n/g, '\r\n'); - const base64Data = btoa(json); - return `data:application/octet-stream;base64,${base64Data}`; -} +/// +/// +/// +/// +/// +/// +/// +/// +/// +/// async function init() { - const zoom = Number(localStorage.zoom) || 100; - resize(zoom); - - const version = getVersion(); - const i18n = await loadI18nMessages(); - const encryption: Encryption = new Encryption(''); - const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); - const exportData = - shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); - const entries = shouldShowPassphrase ? [] : await getEntries(encryption); - const exportFile = getBackupFile(exportData); - - const authenticator = new Vue({ - el: '#authenticator', - data: { - version, - i18n, - entries, - encryption, - zoom, - OTPType, - exportData: JSON.stringify(exportData, null, 2), - exportFile, - class: { - timeout: false, - edit: false, - slidein: false, - slideout: false, - fadein: false, - fadeout: false, - qrfadein: false, - qrfadeout: false, - notificationFadein: false, - notificationFadeout: false, - hotpDiabled: false - }, - sector: '', - info: '', - message: '', - confirmMessage: '', - qr: '', - notification: '', - passphrase: '', - notificationTimeout: 0, - newAccount: {show: false, account: '', secret: '', type: OTPType.totp}, - newPassphrase: {phrase: '', confirm: ''} - }, - methods: { - showBulls: (code: string) => { - return new Array(code.length).fill('•').join(''); - }, - showMenu: () => { - authenticator.class.slidein = true; - authenticator.class.slideout = false; - return; - }, - closeMenu: () => { - authenticator.class.slidein = false; - authenticator.class.slideout = true; - setTimeout(() => { - authenticator.class.slideout = false; - }, 200); - return; - }, - showInfo: (tab: string) => { - if (tab === 'export' || tab === 'security') { - const entries = authenticator.entries as OTPEntry[]; - for (let i = 0; i < entries.length; i++) { - // we have encrypted entry - // the current passphrass is incorrect - // cannot export account data - // or change passphrase - if (entries[i].code === 'Encrypted') { - authenticator.message = authenticator.i18n.phrase_incorrect; - return; - } - } - } - authenticator.class.fadein = true; - authenticator.class.fadeout = false; - authenticator.info = tab; - return; - }, - closeInfo: () => { - authenticator.class.fadein = false; - authenticator.class.fadeout = true; - setTimeout(() => { - authenticator.class.fadeout = false; - authenticator.info = ''; - authenticator.newAccount.show = false; - }, 200); - return; - }, - importEnties: async () => { - await EntryStorage.import( - authenticator.encryption, JSON.parse(authenticator.exportData)); - await authenticator.updateEntries(); - authenticator.message = authenticator.i18n.updateSuccess; - return; - }, - updateEntries: async () => { - const exportData = - await EntryStorage.getExport(authenticator.encryption); - authenticator.exportData = JSON.stringify(exportData, null, 2); - authenticator.entries = await getEntries(authenticator.encryption); - authenticator.exportFile = getBackupFile(exportData); - updateCode(authenticator); - return; - }, - // TODO: Figure out what event & data are supposed to be typed as - importFile: (event: Event) => { - const target = event.target as HTMLInputElement; - if (!target || !target.files) { - return; - } - if (target.files[0] && target.files[0].type.startsWith('text/')) { - const reader = new FileReader(); - reader.onload = () => { - const importData = JSON.parse(reader.result); - console.log(importData); - // Replace data with import data - // if current data has codes insert and check for duplicates - }; - reader.readAsText(target.files[0]); - } - return; - }, - saveZoom: () => { - localStorage.zoom = authenticator.zoom; - resize(authenticator.zoom); - return; - }, - removeEntry: async (entry: OTPEntry) => { - if (await authenticator.confirm(authenticator.i18n.confirm_delete)) { - await entry.delete(); - await authenticator.updateEntries(); - } - return; - }, - syncClock: async () => { - chrome.permissions.request( - {origins: ['https://www.google.com/']}, async (granted) => { - if (granted) { - const message = await syncTimeWithGoogle(); - authenticator.message = authenticator.i18n[message]; - } - return; - }); - return; - }, - editEntry: () => { - authenticator.class.edit = !authenticator.class.edit; - const codes = document.getElementById('codes'); - if (codes) { - // wait vue apply changes to dom - setTimeout(() => { - codes.scrollTop = authenticator.class.edit ? codes.scrollHeight : 0; - }, 0); - } - return; - }, - shouldShowQrIcon: (entry: OTPEntry) => { - return entry.secret !== 'Encrypted' && entry.type !== OTPType.battle && - entry.type !== OTPType.steam; - }, - showQr: async (entry: OTPEntry) => { - const qrUrl = await getQrUrl(entry); - authenticator.qr = `url(${qrUrl})`; - authenticator.class.qrfadein = true; - authenticator.class.qrfadeout = false; - return; - }, - hideQr: () => { - authenticator.class.qrfadein = false; - authenticator.class.qrfadeout = true; - setTimeout(() => { - authenticator.class.qrfadeout = false; - }, 200); - return; - }, - copyCode: (entry: OTPEntry) => { - if (authenticator.class.edit) { - return; - } + const ui = new UI({el: '#authenticator'}); - if (entry.code === 'Encrypted') { - authenticator.showInfo('passphrase'); - return; - } - chrome.permissions.request( - {permissions: ['clipboardWrite']}, (granted) => { - if (granted) { - const codeClipboard = document.getElementById( - 'codeClipboard') as HTMLInputElement; - if (!codeClipboard) { - return; - } - codeClipboard.value = entry.code; - codeClipboard.focus(); - codeClipboard.select(); - document.execCommand('Copy'); - authenticator.notification = authenticator.i18n.copied; - clearTimeout(authenticator.notificationTimeout); - authenticator.class.notificationFadein = true; - authenticator.class.notificationFadeout = false; - authenticator.notificationTimeout = setTimeout(() => { - authenticator.class.notificationFadein = false; - authenticator.class.notificationFadeout = true; - setTimeout(() => { - authenticator.class.notificationFadeout = false; - }, 200); - }, 1000); - } - }); - return; - }, - addAccountManually: () => { - const entries = authenticator.entries as OTPEntry[]; - for (let i = 0; i < entries.length; i++) { - // we have encrypted entry - // the current passphrass is incorrect - // shouldn't add new account with - // the current passphrass - if (entries[i].code === 'Encrypted') { - authenticator.message = authenticator.i18n.phrase_incorrect; - return; - } - } + await className(ui); + await i18n(ui); + await menu(ui); + await info(ui); + await passphrase(ui); + await entry(ui); + await qr(ui); + await message(ui); + await addAccount(ui); - authenticator.newAccount.show = true; - }, - addNewAccount: async () => { - let type: OTPType; - if (!/^[a-z2-7]+=*$/i.test(authenticator.newAccount.secret) && - /^[0-9a-f]+$/i.test(authenticator.newAccount.secret)) { - type = OTPType.hex; - } else { - type = authenticator.newAccount.type; - } - - const entry = new OTPEntry( - type, '', authenticator.newAccount.secret, - authenticator.newAccount.account, 0, 0); - await entry.create(authenticator.encryption); - await authenticator.updateEntries(); - authenticator.newAccount.type = OTPType.totp; - authenticator.account = ''; - authenticator.secret = ''; - authenticator.newAccount.show = false; - authenticator.closeInfo(); - authenticator.class.edit = false; - - const codes = document.getElementById('codes'); - if (codes) { - // wait vue apply changes to dom - setTimeout(() => { - codes.scrollTop = 0; - }, 0); - } - return; - }, - confirm: async (message: string) => { - return new Promise( - (resolve: (value: boolean) => void, - reject: (reason: Error) => void) => { - authenticator.confirmMessage = message; - window.addEventListener('confirm', (event) => { - authenticator.confirmMessage = ''; - if (!isCustomEvent(event)) { - return resolve(false); - } - return resolve(event.detail); - }); - return; - }); - }, - confirmOK: () => { - const confirmEvent = new CustomEvent('confirm', {detail: true}); - window.dispatchEvent(confirmEvent); - return; - }, - confirmCancel: () => { - const confirmEvent = new CustomEvent('confirm', {detail: false}); - window.dispatchEvent(confirmEvent); - return; - }, - nextCode: async (entry: OTPEntry) => { - if (authenticator.class.hotpDiabled) { - return; - } - authenticator.class.hotpDiabled = true; - await entry.next(authenticator.encryption); - await authenticator.updateEntries(); - setTimeout(() => { - authenticator.class.hotpDiabled = false; - }, 3000); - return; - }, - applyPassphrase: async () => { - authenticator.encryption.updateEncryptionPassword( - authenticator.passphrase); - await authenticator.updateEntries(); - authenticator.closeInfo(); - return; - }, - changePassphrase: async () => { - if (authenticator.newPassphrase.phrase !== - authenticator.newPassphrase.confirm) { - authenticator.message = authenticator.i18n.phrase_not_match; - return; - } - authenticator.encryption.updateEncryptionPassword( - authenticator.newPassphrase.phrase); - await authenticator.importEnties(); - return; - }, - beginCapture: () => { - const entries = authenticator.entries as OTPEntry[]; - for (let i = 0; i < entries.length; i++) { - // we have encrypted entry - // the current passphrass is incorrect - // shouldn't add new account with - // the current passphrass - if (entries[i].code === 'Encrypted') { - authenticator.message = authenticator.i18n.phrase_incorrect; - return; - } - } - - chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { - const tab = tabs[0]; - if (!tab || !tab.id) { - return; - } - chrome.tabs.sendMessage( - tab.id, {action: 'capture', passphrase: authenticator.passphrase}, - (result) => { - if (result !== 'beginCapture') { - authenticator.message = authenticator.i18n.capture_failed; - } else { - window.close(); - } - }); - }); - return; - } - } - }); + const authenticator = ui.generate(); - if (shouldShowPassphrase) { + if (authenticator.shouldShowPassphrase) { authenticator.showInfo('passphrase'); } @@ -552,4 +55,4 @@ chrome.permissions.contains( } }); -init(); +init(); \ No newline at end of file diff --git a/src/ui/add-account.ts b/src/ui/add-account.ts new file mode 100644 index 000000000..ebcc64ed1 --- /dev/null +++ b/src/ui/add-account.ts @@ -0,0 +1,91 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function addAccount(_ui: UI) { + const ui: UIConfig = { + data: { + newAccount: {show: false, account: '', secret: '', type: OTPType.totp}, + newPassphrase: {phrase: '', confirm: ''} + }, + methods: { + addNewAccount: async () => { + let type: OTPType; + if (!/^[a-z2-7]+=*$/i.test(_ui.instance.newAccount.secret) && + /^[0-9a-f]+$/i.test(_ui.instance.newAccount.secret)) { + type = OTPType.hex; + } else { + type = _ui.instance.newAccount.type; + } + + const entry = new OTPEntry( + type, '', _ui.instance.newAccount.secret, + _ui.instance.newAccount.account, 0, 0); + await entry.create(_ui.instance.encryption); + await _ui.instance.updateEntries(); + _ui.instance.newAccount.type = OTPType.totp; + _ui.instance.account = ''; + _ui.instance.secret = ''; + _ui.instance.newAccount.show = false; + _ui.instance.closeInfo(); + _ui.instance.class.edit = false; + + const codes = document.getElementById('codes'); + if (codes) { + // wait vue apply changes to dom + setTimeout(() => { + codes.scrollTop = 0; + }, 0); + } + return; + }, + beginCapture: () => { + const entries = _ui.instance.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // shouldn't add new account with + // the current passphrass + if (entries[i].code === 'Encrypted') { + _ui.instance.message = _ui.instance.i18n.phrase_incorrect; + return; + } + } + + chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { + const tab = tabs[0]; + if (!tab || !tab.id) { + return; + } + chrome.tabs.sendMessage( + tab.id, {action: 'capture', passphrase: _ui.instance.passphrase}, + (result) => { + if (result !== 'beginCapture') { + _ui.instance.message = _ui.instance.i18n.capture_failed; + } else { + window.close(); + } + }); + }); + return; + }, + addAccountManually: () => { + const entries = _ui.instance.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // shouldn't add new account with + // the current passphrass + if (entries[i].code === 'Encrypted') { + _ui.instance.message = _ui.instance.i18n.phrase_incorrect; + return; + } + } + + _ui.instance.newAccount.show = true; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/class.ts b/src/ui/class.ts new file mode 100644 index 000000000..de34d8545 --- /dev/null +++ b/src/ui/class.ts @@ -0,0 +1,25 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function className(_ui: UI) { + const ui: UIConfig = { + data: { + class: { + timeout: false, + edit: false, + slidein: false, + slideout: false, + fadein: false, + fadeout: false, + qrfadein: false, + qrfadeout: false, + notificationFadein: false, + notificationFadeout: false, + hotpDiabled: false + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/entry.ts b/src/ui/entry.ts new file mode 100644 index 000000000..dc3b686ab --- /dev/null +++ b/src/ui/entry.ts @@ -0,0 +1,190 @@ +/* tslint:disable:no-reference */ +/// +/// +/// +/// + +async function getEntries(encryption: Encryption) { + const optEntries: OTPEntry[] = await EntryStorage.get(encryption); + return optEntries; +} + +/* tslint:disable-next-line:no-any */ +async function updateCode(app: any) { + let second = new Date().getSeconds(); + if (localStorage.offset) { + second += Number(localStorage.offset) + 30; + } + second = second % 30; + app.sector = getSector(second); + if (second > 25) { + app.class.timeout = true; + } else { + app.class.timeout = false; + } + if (second < 1) { + const entries = app.entries as OTP[]; + for (let i = 0; i < entries.length; i++) { + if (entries[i].type !== OTPType.hotp) { + entries[i].generate(); + } + } + } +} + +function getSector(second: number) { + const canvas = document.createElement('canvas'); + canvas.width = 40; + canvas.height = 40; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return; + } + ctx.fillStyle = '#888'; + ctx.beginPath(); + ctx.moveTo(20, 20); + ctx.arc( + 20, 20, 16, second / 30 * Math.PI * 2 - Math.PI / 2, Math.PI * 3 / 2, + false); + ctx.fill(); + const url = canvas.toDataURL(); + return `url(${url}) center / 20px 20px`; +} + +function getBackupFile(entryData: {[hash: string]: OTPStorage}) { + let json = JSON.stringify(entryData, null, 2); + // for windows notepad + json = json.replace(/\n/g, '\r\n'); + const base64Data = btoa(json); + return `data:application/octet-stream;base64,${base64Data}`; +} + +async function entry(_ui: UI) { + const encryption: Encryption = new Encryption(''); + const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); + const exportData = + shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); + const entries = shouldShowPassphrase ? [] : await getEntries(encryption); + const exportFile = getBackupFile(exportData); + + const ui: UIConfig = { + data: { + entries, + encryption, + OTPType, + shouldShowPassphrase, + exportData: JSON.stringify(exportData, null, 2), + exportFile, + sector: '', + notification: '', + notificationTimeout: 0 + }, + methods: { + showBulls: (code: string) => { + return new Array(code.length).fill('•').join(''); + }, + importEnties: async () => { + await EntryStorage.import( + _ui.instance.encryption, JSON.parse(_ui.instance.exportData)); + await _ui.instance.updateEntries(); + _ui.instance.message = _ui.instance.i18n.updateSuccess; + return; + }, + updateEntries: async () => { + const exportData = + await EntryStorage.getExport(_ui.instance.encryption); + _ui.instance.exportData = JSON.stringify(exportData, null, 2); + _ui.instance.entries = await getEntries(_ui.instance.encryption); + _ui.instance.exportFile = getBackupFile(exportData); + updateCode(_ui.instance); + return; + }, + // TODO: Figure out what event & data are supposed to be typed as + importFile: (event: Event) => { + const target = event.target as HTMLInputElement; + if (!target || !target.files) { + return; + } + if (target.files[0] && target.files[0].type.startsWith('text/')) { + const reader = new FileReader(); + reader.onload = () => { + const importData = JSON.parse(reader.result); + console.log(importData); + // Replace data with import data + // if current data has codes insert and check for duplicates + }; + reader.readAsText(target.files[0]); + } + return; + }, + removeEntry: async (entry: OTPEntry) => { + if (await _ui.instance.confirm(_ui.instance.i18n.confirm_delete)) { + await entry.delete(); + await _ui.instance.updateEntries(); + } + return; + }, + editEntry: () => { + _ui.instance.class.edit = !_ui.instance.class.edit; + const codes = document.getElementById('codes'); + if (codes) { + // wait vue apply changes to dom + setTimeout(() => { + codes.scrollTop = _ui.instance.class.edit ? codes.scrollHeight : 0; + }, 0); + } + return; + }, + nextCode: async (entry: OTPEntry) => { + if (_ui.instance.class.hotpDiabled) { + return; + } + _ui.instance.class.hotpDiabled = true; + await entry.next(_ui.instance.encryption); + await _ui.instance.updateEntries(); + setTimeout(() => { + _ui.instance.class.hotpDiabled = false; + }, 3000); + return; + }, + copyCode: (entry: OTPEntry) => { + if (_ui.instance.class.edit) { + return; + } + + if (entry.code === 'Encrypted') { + _ui.instance.showInfo('passphrase'); + return; + } + chrome.permissions.request( + {permissions: ['clipboardWrite']}, (granted) => { + if (granted) { + const codeClipboard = document.getElementById( + 'codeClipboard') as HTMLInputElement; + if (!codeClipboard) { + return; + } + codeClipboard.value = entry.code; + codeClipboard.focus(); + codeClipboard.select(); + document.execCommand('Copy'); + _ui.instance.notification = _ui.instance.i18n.copied; + clearTimeout(_ui.instance.notificationTimeout); + _ui.instance.class.notificationFadein = true; + _ui.instance.class.notificationFadeout = false; + _ui.instance.notificationTimeout = setTimeout(() => { + _ui.instance.class.notificationFadein = false; + _ui.instance.class.notificationFadeout = true; + setTimeout(() => { + _ui.instance.class.notificationFadeout = false; + }, 200); + }, 1000); + } + }); + return; + }, + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/i18n.ts b/src/ui/i18n.ts new file mode 100644 index 000000000..8b6086c79 --- /dev/null +++ b/src/ui/i18n.ts @@ -0,0 +1,37 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function loadI18nMessages() { + return new Promise( + (resolve: (value: {[key: string]: string}) => void, + reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); + const i18nData: {[key: string]: string} = {}; + for (const key of Object.keys(i18nMessage)) { + i18nData[key] = chrome.i18n.getMessage(key); + } + return resolve(i18nData); + } + return; + }; + xhr.open( + 'GET', chrome.extension.getURL('/_locales/en/messages.json')); + xhr.send(); + } catch (error) { + return reject(error); + } + }); +} + +async function i18n(_ui: UI) { + const i18n = await loadI18nMessages(); + + const ui: UIConfig = {data: {i18n}}; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/info.ts b/src/ui/info.ts new file mode 100644 index 000000000..40a5741fd --- /dev/null +++ b/src/ui/info.ts @@ -0,0 +1,41 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function info(_ui: UI) { + const ui: UIConfig = { + data: {info: ''}, + methods: { + showInfo: (tab: string) => { + if (tab === 'export' || tab === 'security') { + const entries = _ui.instance.entries as OTPEntry[]; + for (let i = 0; i < entries.length; i++) { + // we have encrypted entry + // the current passphrass is incorrect + // cannot export account data + // or change passphrase + if (entries[i].code === 'Encrypted') { + _ui.instance.message = _ui.instance.i18n.phrase_incorrect; + return; + } + } + } + _ui.instance.class.fadein = true; + _ui.instance.class.fadeout = false; + _ui.instance.info = tab; + return; + }, + closeInfo: () => { + _ui.instance.class.fadein = false; + _ui.instance.class.fadeout = true; + setTimeout(() => { + _ui.instance.class.fadeout = false; + _ui.instance.info = ''; + }, 200); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/menu.ts b/src/ui/menu.ts new file mode 100644 index 000000000..6ab101d41 --- /dev/null +++ b/src/ui/menu.ts @@ -0,0 +1,95 @@ +/* tslint:disable:no-reference */ +/// +/// + +function getVersion() { + return chrome.runtime.getManifest().version; +} + +async function syncTimeWithGoogle() { + return new Promise( + (resolve: (value: string) => void, reject: (reason: Error) => void) => { + try { + const xhr = new XMLHttpRequest(); + xhr.open('HEAD', 'https://www.google.com/generate_204'); + const xhrAbort = setTimeout(() => { + xhr.abort(); + return resolve('updateFailure'); + }, 5000); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + clearTimeout(xhrAbort); + const date = xhr.getResponseHeader('date'); + if (!date) { + return resolve('updateFailure'); + } + const serverTime = new Date(date).getTime(); + const clientTime = new Date().getTime(); + const offset = Math.round((serverTime - clientTime) / 1000); + + if (Math.abs(offset) <= 300) { // within 5 minutes + localStorage.offset = + Math.round((serverTime - clientTime) / 1000); + return resolve('updateSuccess'); + } else { + return resolve('clock_too_far_off'); + } + } + }; + xhr.send(); + } catch (error) { + return reject(error); + } + }); +} + +function resize(zoom: number) { + if (zoom !== 100) { + document.body.style.marginBottom = 480 * (zoom / 100 - 1) + 'px'; + document.body.style.marginRight = 320 * (zoom / 100 - 1) + 'px'; + document.body.style.transform = 'scale(' + (zoom / 100) + ')'; + } +} + +async function menu(_ui: UI) { + const version = getVersion(); + const zoom = Number(localStorage.zoom) || 100; + resize(zoom); + + const ui: UIConfig = { + data: {version, zoom}, + methods: { + showMenu: () => { + _ui.instance.class.slidein = true; + _ui.instance.class.slideout = false; + return; + }, + closeMenu: () => { + _ui.instance.class.slidein = false; + _ui.instance.class.slideout = true; + setTimeout(() => { + _ui.instance.class.slideout = false; + }, 200); + return; + }, + saveZoom: () => { + localStorage.zoom = _ui.instance.zoom; + resize(_ui.instance.zoom); + return; + }, + syncClock: async () => { + chrome.permissions.request( + {origins: ['https://www.google.com/']}, async (granted) => { + if (granted) { + const message = await syncTimeWithGoogle(); + _ui.instance.message = _ui.instance.i18n[message]; + } + return; + }); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/message.ts b/src/ui/message.ts new file mode 100644 index 000000000..e4e03ddb7 --- /dev/null +++ b/src/ui/message.ts @@ -0,0 +1,42 @@ +/* tslint:disable:no-reference */ +/// +/// + +function isCustomEvent(event: Event): event is CustomEvent { + return 'detail' in event; +} + +async function message(_ui: UI) { + const ui: UIConfig = { + data: {message: '', confirmMessage: ''}, + methods: { + confirm: async (message: string) => { + return new Promise( + (resolve: (value: boolean) => void, + reject: (reason: Error) => void) => { + _ui.instance.confirmMessage = message; + window.addEventListener('confirm', (event) => { + _ui.instance.confirmMessage = ''; + if (!isCustomEvent(event)) { + return resolve(false); + } + return resolve(event.detail); + }); + return; + }); + }, + confirmOK: () => { + const confirmEvent = new CustomEvent('confirm', {detail: true}); + window.dispatchEvent(confirmEvent); + return; + }, + confirmCancel: () => { + const confirmEvent = new CustomEvent('confirm', {detail: false}); + window.dispatchEvent(confirmEvent); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts new file mode 100644 index 000000000..11cc61214 --- /dev/null +++ b/src/ui/passphrase.ts @@ -0,0 +1,31 @@ +/* tslint:disable:no-reference */ +/// +/// + +async function passphrase(_ui: UI) { + const ui: UIConfig = { + data: {passphrase: ''}, + methods: { + applyPassphrase: async () => { + _ui.instance.encryption.updateEncryptionPassword( + _ui.instance.passphrase); + await _ui.instance.updateEntries(); + _ui.instance.closeInfo(); + return; + }, + changePassphrase: async () => { + if (_ui.instance.newPassphrase.phrase !== + _ui.instance.newPassphrase.confirm) { + _ui.instance.message = _ui.instance.i18n.phrase_not_match; + return; + } + _ui.instance.encryption.updateEncryptionPassword( + _ui.instance.newPassphrase.phrase); + await _ui.instance.importEnties(); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/qr.ts b/src/ui/qr.ts new file mode 100644 index 000000000..a45570963 --- /dev/null +++ b/src/ui/qr.ts @@ -0,0 +1,62 @@ +/* tslint:disable:no-reference */ +/// +/// + +/* tslint:disable-next-line:no-any */ +declare var QRCode: any; + +async function getQrUrl(entry: OTPEntry) { + return new Promise( + (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 otpauth = 'otpauth://' + type + '/' + label + + '?secret=' + entry.secret + + (entry.issuer ? ('&issuer=' + entry.issuer.split('::')[0]) : '') + + ((entry.type === OTPType.hotp) ? ('&counter=' + entry.counter) : + ''); + /* tslint:disable-next-line:no-unused-expression */ + new QRCode( + 'qr', { + text: otpauth, + width: 128, + height: 128, + colorDark: '#000000', + colorLight: '#ffffff', + correctLevel: QRCode.CorrectLevel.L + }, + resolve); + return; + }); +} + +async function qr(_ui: UI) { + const ui: UIConfig = { + data: {qr: ''}, + methods: { + shouldShowQrIcon: (entry: OTPEntry) => { + return entry.secret !== 'Encrypted' && entry.type !== OTPType.battle && + entry.type !== OTPType.steam; + }, + showQr: async (entry: OTPEntry) => { + const qrUrl = await getQrUrl(entry); + _ui.instance.qr = `url(${qrUrl})`; + _ui.instance.class.qrfadein = true; + _ui.instance.class.qrfadeout = false; + return; + }, + hideQr: () => { + _ui.instance.class.qrfadein = false; + _ui.instance.class.qrfadeout = true; + setTimeout(() => { + _ui.instance.class.qrfadeout = false; + }, 200); + return; + } + } + }; + + _ui.update(ui); +} \ No newline at end of file diff --git a/src/ui/ui.ts b/src/ui/ui.ts new file mode 100644 index 000000000..5c4b530d4 --- /dev/null +++ b/src/ui/ui.ts @@ -0,0 +1,39 @@ +/* tslint:disable:no-reference */ +/// + +// need to find a better way to handle Vue types without modules +// we use vue 1.0 here to solve csp issues +/* tslint:disable-next-line:no-any */ +declare var Vue: any; + +class UI { + private ui: UIConfig; + // Vue instance + /* tslint:disable-next-line:no-any */ + instance: any; + + constructor(ui: UIConfig) { + this.ui = ui; + } + + update(ui: UIConfig) { + if (ui.data) { + this.ui.data = this.ui.data || {}; + for (const key of Object.keys(ui.data)) { + this.ui.data[key] = ui.data[key]; + } + } + + if (ui.methods) { + this.ui.methods = this.ui.methods || {}; + for (const key of Object.keys(ui.methods)) { + this.ui.methods[key] = ui.methods[key]; + } + } + } + + generate() { + this.instance = new Vue(this.ui); + return this.instance; + } +} \ No newline at end of file From c1f97c6297039436eb7e5d49410013c3d8c54558 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 9 Feb 2018 16:02:28 -0600 Subject: [PATCH 045/178] Fix typo hasEncryptedEntrie -> hasEncryptedEntry --- src/models/storage.ts | 4 ++-- src/ui/entry.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/models/storage.ts b/src/models/storage.ts index 0d0ca46e7..c541add97 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -39,7 +39,7 @@ class EntryStorage { return newData; } - static async hasEncryptedEntrie() { + static async hasEncryptedEntry() { return new Promise( (resolve: (value: boolean) => void, reject: (reason: Error) => void) => { @@ -299,4 +299,4 @@ class EntryStorage { } }); } -} \ No newline at end of file +} diff --git a/src/ui/entry.ts b/src/ui/entry.ts index dc3b686ab..4c4c41070 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -61,7 +61,7 @@ function getBackupFile(entryData: {[hash: string]: OTPStorage}) { async function entry(_ui: UI) { const encryption: Encryption = new Encryption(''); - const shouldShowPassphrase = await EntryStorage.hasEncryptedEntrie(); + const shouldShowPassphrase = await EntryStorage.hasEncryptedEntry(); const exportData = shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); const entries = shouldShowPassphrase ? [] : await getEntries(encryption); @@ -99,7 +99,6 @@ async function entry(_ui: UI) { updateCode(_ui.instance); return; }, - // TODO: Figure out what event & data are supposed to be typed as importFile: (event: Event) => { const target = event.target as HTMLInputElement; if (!target || !target.files) { @@ -187,4 +186,4 @@ async function entry(_ui: UI) { }; _ui.update(ui); -} \ No newline at end of file +} From 2912dd5f41b76498dd8b234b4271d6ad666b5a5e Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 9 Feb 2018 16:18:38 -0600 Subject: [PATCH 046/178] Finish file import code --- src/ui/entry.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 4c4c41070..9aad80f51 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -106,11 +106,11 @@ async function entry(_ui: UI) { } if (target.files[0] && target.files[0].type.startsWith('text/')) { const reader = new FileReader(); - reader.onload = () => { + reader.onload = async () => { const importData = JSON.parse(reader.result); - console.log(importData); - // Replace data with import data - // if current data has codes insert and check for duplicates + await EntryStorage.import(_ui.instance.encryption, importData); + await _ui.instance.updateEntries(); + _ui.instance.message = _ui.instance.i18n.updateSuccess; }; reader.readAsText(target.files[0]); } From 23b73130f4549d1daaf5cdc38ebb3dbc066d314d Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 9 Feb 2018 16:26:38 -0600 Subject: [PATCH 047/178] Fix typo importEnties -> importEntries --- popup.html | 2 +- src/ui/entry.ts | 2 +- src/ui/passphrase.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/popup.html b/popup.html index 97837f918..4c71d3d46 100644 --- a/popup.html +++ b/popup.html @@ -116,7 +116,7 @@
-
{{ i18n.update }}
+
{{ i18n.update }}
diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 9aad80f51..ffbbf85cc 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -83,7 +83,7 @@ async function entry(_ui: UI) { showBulls: (code: string) => { return new Array(code.length).fill('•').join(''); }, - importEnties: async () => { + importEntries: async () => { await EntryStorage.import( _ui.instance.encryption, JSON.parse(_ui.instance.exportData)); await _ui.instance.updateEntries(); diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts index 11cc61214..19189169a 100644 --- a/src/ui/passphrase.ts +++ b/src/ui/passphrase.ts @@ -21,11 +21,11 @@ async function passphrase(_ui: UI) { } _ui.instance.encryption.updateEncryptionPassword( _ui.instance.newPassphrase.phrase); - await _ui.instance.importEnties(); + await _ui.instance.importEntries(); return; } } }; _ui.update(ui); -} \ No newline at end of file +} From aadc3247f162d8bdf2ccda07ceb3bad525179be8 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 9 Feb 2018 16:38:46 -0600 Subject: [PATCH 048/178] Error on bad file --- src/ui/entry.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/entry.ts b/src/ui/entry.ts index ffbbf85cc..8f06d261d 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -113,6 +113,8 @@ async function entry(_ui: UI) { _ui.instance.message = _ui.instance.i18n.updateSuccess; }; reader.readAsText(target.files[0]); + } else { + _ui.instance.message = _ui.instance.i18n.updateFailure; } return; }, From de4c8336426ea3fa33a5a20b0be9916237e81406 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 9 Feb 2018 19:35:34 -0600 Subject: [PATCH 049/178] Add build scripts --- .gitignore | 2 + README.md | 17 +++++- manifest.json => manifest-chrome.json | 0 manifest-firefox.json | 78 +++++++++++++++++++++++++++ package.json | 6 ++- 5 files changed, 101 insertions(+), 2 deletions(-) rename manifest.json => manifest-chrome.json (100%) create mode 100644 manifest-firefox.json diff --git a/.gitignore b/.gitignore index 16d2e37b3..e287602b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules build +chrome +firefox .vscode diff --git a/README.md b/README.md index 62a0d58a5..7eab98f90 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,21 @@ ## Build Setup +Compile for chrome: + +```bash +npm install +npm run chrome +``` +Compile for firefox: + +```bash +npm install +npm run firefox +``` + +Compile for development: + ``` bash # install typescript npm install -g typescript @@ -21,4 +36,4 @@ npm run compile ### gts is not found -gts (Google TypeScript style) is installed locally by default, see to add local node modules into path, or run `npm install -g gts` to install gts global. \ No newline at end of file +gts (Google TypeScript style) is installed locally by default, see to add local node modules into path, or run `npm install -g gts` to install gts global. diff --git a/manifest.json b/manifest-chrome.json similarity index 100% rename from manifest.json rename to manifest-chrome.json diff --git a/manifest-firefox.json b/manifest-firefox.json new file mode 100644 index 000000000..b8a0936ad --- /dev/null +++ b/manifest-firefox.json @@ -0,0 +1,78 @@ +{ + "manifest_version": 2, + "name": "__MSG_extName__", + "short_name": "__MSG_extShortName__", + "version": "5.0", + "default_locale": "en", + "description": "__MSG_extDesc__", + "applications": { + "gecko": { + "id": "authenticator@mymindstorm", + "strict_min_version": "53.0" + } + }, + "icons": { + "16": "images/icon16.png", + "48": "images/icon48.png", + "128": "images/icon128.png" + }, + "browser_action": { + "default_icon": { + "19": "images/icon19.png", + "38": "images/icon38.png" + }, + "default_title": "__MSG_extShortName__", + "default_popup": "popup.html" + }, + "background": { + "scripts": [ + "js/jsqrcode/grid.js", + "js/jsqrcode/version.js", + "js/jsqrcode/detector.js", + "js/jsqrcode/formatinf.js", + "js/jsqrcode/errorlevel.js", + "js/jsqrcode/bitmat.js", + "js/jsqrcode/datablock.js", + "js/jsqrcode/bmparser.js", + "js/jsqrcode/datamask.js", + "js/jsqrcode/rsdecoder.js", + "js/jsqrcode/gf256poly.js", + "js/jsqrcode/gf256.js", + "js/jsqrcode/decoder.js", + "js/jsqrcode/qrcode.js", + "js/jsqrcode/findpat.js", + "js/jsqrcode/alignpat.js", + "js/jsqrcode/databr.js", + "js/md5.js", + "js/aes.js", + "js/sha.js", + "js/qrcode.js", + "build/models/encryption.js", + "build/models/interface.js", + "build/models/otp.js", + "build/models/storage.js", + "build/background.js" + ] + }, + "content_scripts": [ + { + "matches": [""], + "css": ["css/content.css"], + "js": ["build/content.js"] + } + ], + "permissions": [ + "activeTab", + "", + "clipboardWrite", + "storage" + ], + "optional_permissions": [ + "https://www.google.com/" + ], + "web_accessible_resources": [ + "qr.html", + "images/scan.gif" + ], + "content_security_policy": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/; default-src 'none'" +} diff --git a/package.json b/package.json index 2e54af799..fbcc84215 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,15 @@ "test": "echo \"Error: no test specified\" && exit 1", "check": "gts check", "clean": "gts clean", + "copyChrome": "cp -r build css images js _locales LICENSE *.html chrome", + "copyFirefox": "cp -r build css images js _locales LICENSE *.html firefox", "compile": "tsc -p .", "fix": "gts fix", "prepare": "npm run compile", "pretest": "npm run compile", - "posttest": "npm run check" + "posttest": "npm run check", + "chrome": "mkdir -p chrome && npm run compile && npm run copyChrome && cp manifest-chrome.json chrome/manifest.json", + "firefox": "mkdir -p firefox && npm run compile && npm run copyFirefox && cp manifest-firefox.json firefox/manifest.json" }, "repository": { "type": "git", From bbd2e1d25800fca6f546052deb0ba26b4d73f748 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 9 Feb 2018 19:45:58 -0600 Subject: [PATCH 050/178] Fix the clipboard again --- manifest-firefox.json | 1 + 1 file changed, 1 insertion(+) diff --git a/manifest-firefox.json b/manifest-firefox.json index b8a0936ad..5ad09034b 100644 --- a/manifest-firefox.json +++ b/manifest-firefox.json @@ -68,6 +68,7 @@ "storage" ], "optional_permissions": [ + "clipboardWrite", "https://www.google.com/" ], "web_accessible_resources": [ From caead8737becca4ed2a2cad684da55fe51efa500 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 10 Feb 2018 16:14:04 -0600 Subject: [PATCH 051/178] Declare charset as utf-8 --- popup.html | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/popup.html b/popup.html index 4c71d3d46..df647d51a 100644 --- a/popup.html +++ b/popup.html @@ -1,18 +1,19 @@ - - - - - - - - - - - - + + + + + + + + + + + + +
From 541c121f3233098dcd188e30c365239b33254db8 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 10 Feb 2018 17:45:06 -0600 Subject: [PATCH 052/178] Remove old backup method --- css/popup.css | 1418 +++++++++++++++++++++++++------------------------ popup.html | 12 +- 2 files changed, 715 insertions(+), 715 deletions(-) diff --git a/css/popup.css b/css/popup.css index 82ecf38aa..e542e0528 100644 --- a/css/popup.css +++ b/css/popup.css @@ -1,708 +1,710 @@ -@font-face { - font-family: 'Droid Sans Mono'; - font-style: normal; - font-weight: 400; - src: url(DroidSansMono.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; -} - -@keyframes twinkling{ - 0%{ - color:#DD4B39; - } - 100%{ - color:#EEA59C; - } -} - -@keyframes fadeshow{ - 0%{ - opacity:0; - } - 100%{ - opacity:1; - } -} - -@keyframes fadehide{ - 0%{ - opacity:1; - } - 100%{ - opacity:0; - } -} - -@keyframes fadein{ - 0%{ - opacity:0; - top:110px; - } - 100%{ - opacity:1; - top:10px; - } -} - -@keyframes fadeout{ - 0%{ - opacity:1; - top:10px; - } - 100%{ - opacity:0; - top:110px; - } -} - -@keyframes slidein{ - 0%{ - opacity:0; - left:-55px; - } - 100%{ - opacity:1; - left:0; - } -} - -@keyframes slideout{ - 0%{ - opacity:1; - left:0; - } - 100%{ - opacity:0; - left:-55px; - } -} - -@keyframes qrfadein{ - 0%{ - opacity:0; - } - 100%{ - opacity:1; - } -} - -@keyframes qrfadeout{ - 0%{ - opacity:1; - } - 100%{ - opacity:0; - } -} - -[v-cloak] { display: none } - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - width: 320px; - height: 480px; - overflow: hidden; - font-family: arial, 'Microsoft YaHei'; - cursor: default; - user-select: none; - transform-origin: left top; -} - -#header, -#menuHead { - height: 38px; - line-height: 38px; - position: relative; - text-align: center; - font-size: 16px; - border-bottom: #CCC 1px solid; -} - -#notification { - position: absolute; - left: 100px; - top: -1000px; - width: 120px; - height: 60px; - line-height: 60px; - text-align: center; - background: rgba(0,0,0,0.5); - color: #FFF; - font-size: 20px; - border-radius: 2px; -} - -#notification.fadein { - top: 190px; - animation: fadeshow 0.2s 1 ease-out; -} - -#notification.fadeout { - top: 190px; - animation: fadehide 0.2s 1 ease-in; -} - -#codes { - height: 442px; - overflow-x: hidden; - overflow-y: hidden; - background: #EEE; - padding-right:10px; -} - -#codes:hover { - padding-right: 0; - overflow-y: scroll; -} - -#codes::-webkit-scrollbar { - width: 10px; - background: #EEE; -} - -#codes::-webkit-scrollbar-thumb { - background-color: #AAA; - border: 2px solid #EEE; - border-radius: 5px; -} - -#codeClipboard { - position: absolute; - top: -1000px; -} - -.entry { - margin: 10px; - margin-right: 0; - padding: 10px; - border: #CCC 1px solid; - background: white; - border-radius: 2px; - position: relative; -} - -.entry[unencrypted="true"] .warning { - position: absolute; - height: 0; - line-height: 12px; - font-size: 12px; - padding: 0 10px; - margin: 0 4px; - width: 250px; - bottom: 4px; - left: 0; - background: #EC6959; - color: #FFF; - cursor: pointer; - overflow: hidden; - border-radius: 2px; - transition: height 0.2s; -} - -#codes:not(.edit) .entry[unencrypted="true"]:hover .warning { - height: 24px; -} - -.entry[dropOver="true"] { - border: gray 1px dashed; -} - -.issuer { - font-size: 12px; - color: black; - width: 80%; - text-overflow: ellipsis; - overflow: hidden; -} - -.code { - font-size: 36px; - color: #08C; - width: 80%; - user-select: text; - font-family: 'Droid Sans Mono'; - cursor: pointer; -} - -#codes.edit .code { - color: #CCC!important; - user-select: none; - cursor: default; -} - -#codes.edit .account, -#codes.edit .issuer { - display: none; -} - -.accountEdit, -.issuerEdit { - display: none; -} - -.accountEdit input, -.issuerEdit input { - border: none; - height: 14px; - width: 70%; - font-size: 12px; - font-family: arial, 'Microsoft YaHei'; - outline: none; - background: #eee; -} - -#codes.edit .accountEdit, -#codes.edit .issuerEdit { - display: block; -} - -#codes.timeout .code:not(.hotp) { - animation: twinkling 1s infinite ease-in-out; -} - -.hotp { - color: #555; - cursor: default; -} - -.hotp[hasCode="true"] { - color: #08C; - cursor: pointer; -} - -.movehandle { - height: 98px; - line-height: 98px; - right: 10px; - top: 0; - position: absolute; - font-size: 24px; - color: #CCC; - cursor: move; - display: none; -} - -#codes.edit .movehandle { - display: block; -} - -.showqr { - right: 10px; - top: 10px; - position: absolute; - font-size: 20px; - color: #CCC; - cursor: pointer; - opacity: 0; -} - -.entry:hover .showqr { - opacity: 1; -} - -#codes.edit .showqr, -.showqr.hidden { - display: none; -} - -.account { - font-size: 12px; - color: gray; - width: 80%; - text-overflow: ellipsis; - overflow: hidden; -} - -#add, -#add_qr, -#add_secret, -#add_button, -#security_save, -#passphrase_ok, -#message_close, -#exportButton, -#resize_save { - margin: 10px; - padding: 20px; - border: #CCC 1px solid; - background: white; - border-radius: 2px; - position: relative; - text-align: center; - font-size: 16px; - color: gray; - cursor: pointer; -} - -.buttons { - text-align: center; -} - -#confirm_ok, -#confirm_cancel { - display: inline-block; - margin: 10px; - padding: 5px 20px; - border: #CCC 1px solid; - background: white; - border-radius: 2px; - position: relative; - text-align: center; - font-size: 16px; - color: gray; - cursor: pointer; -} - -#add { - margin-right: 0; -} - -#message_close, -#add_button, -#exportButton, -#security_save, -#passphrase_ok, -#resize_save { - font-size: 12px; - margin: 20px 100px; - padding: 10px; - cursor: pointer; -} - -#codes #add { - font-size: 16px; - line-height: 56px; - display: none; -} - -#codes.edit #add { - display: block; -} - -#codes .deleteAction { - font-size: 20px; - color: #DD4B39; - position: absolute; - top: -10px; - left: -10px; - z-index: 10; - display: none; -} - -#codes.edit .deleteAction { - display: block; - cursor: pointer; -} - -#infoAction { - position: absolute; - left: 20px; - bottom: 0; - height: 38px; - line-height: 38px; - font-size: 16px; - color: gray; - cursor: pointer; -} - -#infoAction.hidden { - display: none; -} - -#editAction { - position: absolute; - right: 20px; - bottom: 0; - height: 38px; - line-height: 38px; - font-size: 16px; - color: gray; - cursor: pointer; -} - -.counter { - color: #888; - font-size: 18px; - text-align: center; - cursor: pointer; -} - -.counter:not(.disabled):hover { - color: #000; -} - -.counter.disabled { - color: #CCC; - cursor: default; -} - -.sector, -.counter { - width: 20px; - height: 20px; - position: absolute; - right: 10px; - bottom: 10px; -} - -#codes.edit .sector, -#codes.edit .counter { - display: none; -} - -#menu { - width: 320px; - height: 480px; - position: absolute; - left: -1000px; - background: #EEE; - top: 0; -} - -#menu.slidein { - left: 0; - animation: slidein 0.2s 1 ease-out; - opacity: 1; -} - -#menu.slideout { - left: -55px; - animation: slideout 0.2s 1 ease-in; - opacity: 0; -} - -#menuHead { - background: #FFF; -} - -#menu .menuList { - margin: 10px; - border: #CCC 1px solid; - border-radius: 2px; - background: #FFF; -} - -#menu .menuList p { - position: relative; - border-bottom: #CCC 1px solid; - padding: 10px; - font-size: 16px; - color: gray; - cursor: pointer; -} - -#menu .menuList p:hover { - background: #F4FCFF; - color: black; -} - -#menu .menuList p:hover:after { - color: black; -} - -#menu .menuList p:last-child { - border-bottom: none; -} - -#menu .menuList p a { - color: gray; - text-decoration: none; - display: line-block; -} - -#menu .menuList p i.fa { - font-size: 14px; - display: line-block; - width: 30px; -} - -#version { - text-align: center; - color: gray; - margin: 10px; -} - -#info { - position: absolute; - height: 460px; - width: 300px; - padding: 10px; - border: gray; - background: white; - left: 10px; - top: -1000px; - box-shadow: 1px 1px 3px gray; - z-index: 100; -} - -#info.fadein { - top: 10px; - animation: fadein 0.2s 1 ease-out; -} - -#info.fadeout { - top: 110px; - animation: fadeout 0.2s 1 ease-in; -} - -#infoClose { - height: 20px; - width: 20px; - font-size: 14px; - color: gray; - cursor: pointer; -} - -#menuClose { - position: absolute; - height: 38px; - line-height: 38px; - left: 20px; - font-size: 16px; - color: gray; - bottom: 0; - cursor: pointer; -} - -#menuClose:hover, -#exportButton:hover, -#message_close:hover, -#add_button:hover, -#add_secret:hover, -#add_qr:hover, -#editAction:hover, -#infoAction:hover, -#codes #add:hover, -#infoClose:hover, -#addAccountClose:hover, -#securityClose:hover, -#passphraseClose:hover, -#security_save:hover, -#passphrase_ok:hover, -#export:hover, -#resizeClose:hover, -#resize_save:hover { - color: black; -} - -#infoContent, -#addAccountContent, -#exportContent { - height: 420px; - overflow-y: auto; - overflow-x: hidden; -} - -#exportData { - height: 320px; - width: 100%; - word-break: break-all; - resize: none; - outline: none; -} - -#infoContent p { - font-size: 12px; - margin-bottom: 20px; -} - -#infoContent a { - color: #4183c4; -} - -#qr { - width: 100%; - height: 100%; - top: -1000px; - left: 0; - position: absolute; - z-index: 10; - background-color: rgba(0, 0, 0, 0.5); - background-repeat: no-repeat; - background-position: center; -} - -#qr canvas { - display: none; -} - -#qr.qrfadein { - top: 0; - animation: qrfadein 0.2s 1 ease-out; -} - -#qr.qrfadeout { - top: 0; - animation: qrfadeout 0.2s 1 ease-in; -} - -#secret_box input, -#security input, -#passphrase input { - display: block; - margin: 0 10px 10px 10px; - padding: 10px; - width: 260px; - border: #CCC 1px solid; - background: white; - outline: none; -} - -.checkbox_group input[type="checkbox"], -.radio_group input[type="radio"] { - display: inline-block !important; - width: auto !important; -} - -.checkbox_group label, -.radio_group label { - display: inline-block !important; - margin-left: 0 !important; -} - -#secret_box select { - margin: 20px; - font-size: 16px; -} - -#secret_box label, -#security label, -#passphrase label, -#security_warning, -#passphrase_info { - display: block; - margin: 10px 0 0 10px; -} - -#security_warning, -#passphrase_info { - color: gray; -} - -#resize_list_label, -#resize_list { - margin: 20px; - font-size: 16px; -} - -#message, -#confirm { - position: absolute; - width: 300px; - padding: 10px; - border: gray; - background: white; - left: 10px; - top: 150px; - box-shadow: 1px 1px 3px gray; - z-index: 1000; -} - -#downloadBackup { - text-align: right; -} \ No newline at end of file +@font-face { + font-family: 'Droid Sans Mono'; + font-style: normal; + font-weight: 400; + src: url(DroidSansMono.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000; +} + +@keyframes twinkling{ + 0%{ + color:#DD4B39; + } + 100%{ + color:#EEA59C; + } +} + +@keyframes fadeshow{ + 0%{ + opacity:0; + } + 100%{ + opacity:1; + } +} + +@keyframes fadehide{ + 0%{ + opacity:1; + } + 100%{ + opacity:0; + } +} + +@keyframes fadein{ + 0%{ + opacity:0; + top:110px; + } + 100%{ + opacity:1; + top:10px; + } +} + +@keyframes fadeout{ + 0%{ + opacity:1; + top:10px; + } + 100%{ + opacity:0; + top:110px; + } +} + +@keyframes slidein{ + 0%{ + opacity:0; + left:-55px; + } + 100%{ + opacity:1; + left:0; + } +} + +@keyframes slideout{ + 0%{ + opacity:1; + left:0; + } + 100%{ + opacity:0; + left:-55px; + } +} + +@keyframes qrfadein{ + 0%{ + opacity:0; + } + 100%{ + opacity:1; + } +} + +@keyframes qrfadeout{ + 0%{ + opacity:1; + } + 100%{ + opacity:0; + } +} + +[v-cloak] { display: none } + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + width: 320px; + height: 480px; + overflow: hidden; + font-family: arial, 'Microsoft YaHei'; + cursor: default; + user-select: none; + transform-origin: left top; +} + +#header, +#menuHead { + height: 38px; + line-height: 38px; + position: relative; + text-align: center; + font-size: 16px; + border-bottom: #CCC 1px solid; +} + +#notification { + position: absolute; + left: 100px; + top: -1000px; + width: 120px; + height: 60px; + line-height: 60px; + text-align: center; + background: rgba(0,0,0,0.5); + color: #FFF; + font-size: 20px; + border-radius: 2px; +} + +#notification.fadein { + top: 190px; + animation: fadeshow 0.2s 1 ease-out; +} + +#notification.fadeout { + top: 190px; + animation: fadehide 0.2s 1 ease-in; +} + +#codes { + height: 442px; + overflow-x: hidden; + overflow-y: hidden; + background: #EEE; + padding-right:10px; +} + +#codes:hover { + padding-right: 0; + overflow-y: scroll; +} + +#codes::-webkit-scrollbar { + width: 10px; + background: #EEE; +} + +#codes::-webkit-scrollbar-thumb { + background-color: #AAA; + border: 2px solid #EEE; + border-radius: 5px; +} + +#codeClipboard { + position: absolute; + top: -1000px; +} + +.entry { + margin: 10px; + margin-right: 0; + padding: 10px; + border: #CCC 1px solid; + background: white; + border-radius: 2px; + position: relative; +} + +.entry[unencrypted="true"] .warning { + position: absolute; + height: 0; + line-height: 12px; + font-size: 12px; + padding: 0 10px; + margin: 0 4px; + width: 250px; + bottom: 4px; + left: 0; + background: #EC6959; + color: #FFF; + cursor: pointer; + overflow: hidden; + border-radius: 2px; + transition: height 0.2s; +} + +#codes:not(.edit) .entry[unencrypted="true"]:hover .warning { + height: 24px; +} + +.entry[dropOver="true"] { + border: gray 1px dashed; +} + +.issuer { + font-size: 12px; + color: black; + width: 80%; + text-overflow: ellipsis; + overflow: hidden; +} + +.code { + font-size: 36px; + color: #08C; + width: 80%; + user-select: text; + font-family: 'Droid Sans Mono'; + cursor: pointer; +} + +#codes.edit .code { + color: #CCC!important; + user-select: none; + cursor: default; +} + +#codes.edit .account, +#codes.edit .issuer { + display: none; +} + +.accountEdit, +.issuerEdit { + display: none; +} + +.accountEdit input, +.issuerEdit input { + border: none; + height: 14px; + width: 70%; + font-size: 12px; + font-family: arial, 'Microsoft YaHei'; + outline: none; + background: #eee; +} + +#codes.edit .accountEdit, +#codes.edit .issuerEdit { + display: block; +} + +#codes.timeout .code:not(.hotp) { + animation: twinkling 1s infinite ease-in-out; +} + +.hotp { + color: #555; + cursor: default; +} + +.hotp[hasCode="true"] { + color: #08C; + cursor: pointer; +} + +.movehandle { + height: 98px; + line-height: 98px; + right: 10px; + top: 0; + position: absolute; + font-size: 24px; + color: #CCC; + cursor: move; + display: none; +} + +#codes.edit .movehandle { + display: block; +} + +.showqr { + right: 10px; + top: 10px; + position: absolute; + font-size: 20px; + color: #CCC; + cursor: pointer; + opacity: 0; +} + +.entry:hover .showqr { + opacity: 1; +} + +#codes.edit .showqr, +.showqr.hidden { + display: none; +} + +.account { + font-size: 12px; + color: gray; + width: 80%; + text-overflow: ellipsis; + overflow: hidden; +} + +#add, +#add_qr, +#add_secret, +#add_button, +#download_backup, +#upload_backup, +#security_save, +#passphrase_ok, +#message_close, +#exportButton, +#resize_save { + margin: 10px; + padding: 20px; + border: #CCC 1px solid; + background: white; + border-radius: 2px; + position: relative; + text-align: center; + font-size: 16px; + color: gray; + cursor: pointer; +} + +.buttons { + text-align: center; +} + +#confirm_ok, +#confirm_cancel { + display: inline-block; + margin: 10px; + padding: 5px 20px; + border: #CCC 1px solid; + background: white; + border-radius: 2px; + position: relative; + text-align: center; + font-size: 16px; + color: gray; + cursor: pointer; +} + +#add { + margin-right: 0; +} + +#message_close, +#add_button, +#exportButton, +#security_save, +#passphrase_ok, +#resize_save { + font-size: 12px; + margin: 20px 100px; + padding: 10px; + cursor: pointer; +} + +#codes #add { + font-size: 16px; + line-height: 56px; + display: none; +} + +#codes.edit #add { + display: block; +} + +#codes .deleteAction { + font-size: 20px; + color: #DD4B39; + position: absolute; + top: -10px; + left: -10px; + z-index: 10; + display: none; +} + +#codes.edit .deleteAction { + display: block; + cursor: pointer; +} + +#infoAction { + position: absolute; + left: 20px; + bottom: 0; + height: 38px; + line-height: 38px; + font-size: 16px; + color: gray; + cursor: pointer; +} + +#infoAction.hidden { + display: none; +} + +#editAction { + position: absolute; + right: 20px; + bottom: 0; + height: 38px; + line-height: 38px; + font-size: 16px; + color: gray; + cursor: pointer; +} + +.counter { + color: #888; + font-size: 18px; + text-align: center; + cursor: pointer; +} + +.counter:not(.disabled):hover { + color: #000; +} + +.counter.disabled { + color: #CCC; + cursor: default; +} + +.sector, +.counter { + width: 20px; + height: 20px; + position: absolute; + right: 10px; + bottom: 10px; +} + +#codes.edit .sector, +#codes.edit .counter { + display: none; +} + +#menu { + width: 320px; + height: 480px; + position: absolute; + left: -1000px; + background: #EEE; + top: 0; +} + +#menu.slidein { + left: 0; + animation: slidein 0.2s 1 ease-out; + opacity: 1; +} + +#menu.slideout { + left: -55px; + animation: slideout 0.2s 1 ease-in; + opacity: 0; +} + +#menuHead { + background: #FFF; +} + +#menu .menuList { + margin: 10px; + border: #CCC 1px solid; + border-radius: 2px; + background: #FFF; +} + +#menu .menuList p { + position: relative; + border-bottom: #CCC 1px solid; + padding: 10px; + font-size: 16px; + color: gray; + cursor: pointer; +} + +#menu .menuList p:hover { + background: #F4FCFF; + color: black; +} + +#menu .menuList p:hover:after { + color: black; +} + +#menu .menuList p:last-child { + border-bottom: none; +} + +#menu .menuList p a { + color: gray; + text-decoration: none; + display: line-block; +} + +#menu .menuList p i.fa { + font-size: 14px; + display: line-block; + width: 30px; +} + +#version { + text-align: center; + color: gray; + margin: 10px; +} + +#info { + position: absolute; + height: 460px; + width: 300px; + padding: 10px; + border: gray; + background: white; + left: 10px; + top: -1000px; + box-shadow: 1px 1px 3px gray; + z-index: 100; +} + +#info.fadein { + top: 10px; + animation: fadein 0.2s 1 ease-out; +} + +#info.fadeout { + top: 110px; + animation: fadeout 0.2s 1 ease-in; +} + +#infoClose { + height: 20px; + width: 20px; + font-size: 14px; + color: gray; + cursor: pointer; +} + +#menuClose { + position: absolute; + height: 38px; + line-height: 38px; + left: 20px; + font-size: 16px; + color: gray; + bottom: 0; + cursor: pointer; +} + +#menuClose:hover, +#exportButton:hover, +#message_close:hover, +#add_button:hover, +#add_secret:hover, +#add_qr:hover, +#upload_backup:hover, +#download_backup:hover, +#editAction:hover, +#infoAction:hover, +#codes #add:hover, +#infoClose:hover, +#addAccountClose:hover, +#securityClose:hover, +#passphraseClose:hover, +#security_save:hover, +#passphrase_ok:hover, +#export:hover, +#resizeClose:hover, +#resize_save:hover { + color: black; +} + +#infoContent, +#addAccountContent, +#exportContent { + height: 420px; + overflow-y: auto; + overflow-x: hidden; +} + +#exportData { + height: 320px; + width: 100%; + word-break: break-all; + resize: none; + outline: none; +} + +#infoContent p { + font-size: 12px; + margin-bottom: 20px; +} + +#qr { + width: 100%; + height: 100%; + top: -1000px; + left: 0; + position: absolute; + z-index: 10; + background-color: rgba(0, 0, 0, 0.5); + background-repeat: no-repeat; + background-position: center; +} + +#qr canvas { + display: none; +} + +#qr.qrfadein { + top: 0; + animation: qrfadein 0.2s 1 ease-out; +} + +#qr.qrfadeout { + top: 0; + animation: qrfadeout 0.2s 1 ease-in; +} + +#secret_box input, +#security input, +#passphrase input { + display: block; + margin: 0 10px 10px 10px; + padding: 10px; + width: 260px; + border: #CCC 1px solid; + background: white; + outline: none; +} + +.checkbox_group input[type="checkbox"], +.radio_group input[type="radio"] { + display: inline-block !important; + width: auto !important; +} + +.checkbox_group label, +.radio_group label { + display: inline-block !important; + margin-left: 0 !important; +} + +#secret_box select { + margin: 20px; + font-size: 16px; +} + +#secret_box label, +#security label, +#passphrase label, +#security_warning, +#passphrase_info { + display: block; + margin: 10px 0 0 10px; +} + +#security_warning, +#passphrase_info { + color: gray; +} + +#resize_list_label, +#resize_list { + margin: 20px; + font-size: 16px; +} + +#message, +#confirm { + position: absolute; + width: 300px; + padding: 10px; + border: gray; + background: white; + left: 10px; + top: 150px; + box-shadow: 1px 1px 3px gray; + z-index: 1000; +} + +#download_backup { + text-decoration: none; + color: gray; + display: block; +} diff --git a/popup.html b/popup.html index df647d51a..94a9a07df 100644 --- a/popup.html +++ b/popup.html @@ -109,15 +109,13 @@
{{ i18n.ok }}
-
- - -
+
+ + {{ i18n.download_backup }} +
-
{{ i18n.update }}
+
From 78e7726ed015666567136049af2cad9849724704 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 10 Feb 2018 17:49:19 -0600 Subject: [PATCH 053/178] Add encryption warning --- _locales/en/messages.json | 2 +- popup.html | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5611a0ff0..ad72384ad 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -188,7 +188,7 @@ "description": "Scale" }, "export_info": { - "message": "Copy this text and save it somewhere else to backup your secrets. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", + "message": "Warning: all backups are unencrypted. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", "description": "Export menu info text" }, "download_backup": { diff --git a/popup.html b/popup.html index 94a9a07df..5f99608a2 100644 --- a/popup.html +++ b/popup.html @@ -1,4 +1,4 @@ - +/ @@ -110,6 +110,7 @@
+
{{ i18n.export_info }}
{{ i18n.download_backup }}
From 0ffbbe21d53e59687ff8eded74e8c002f08726a1 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 10 Feb 2018 17:51:54 -0600 Subject: [PATCH 054/178] Update popup.html --- popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/popup.html b/popup.html index 5f99608a2..5e89bcef3 100644 --- a/popup.html +++ b/popup.html @@ -1,4 +1,4 @@ -/ + From 8ccca0f83c7810a84d5e78e3f44fc5fd60df6712 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 10 Feb 2018 18:02:52 -0600 Subject: [PATCH 055/178] Remove old html --- popup.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/popup.html b/popup.html index 5e89bcef3..7200a1a65 100644 --- a/popup.html +++ b/popup.html @@ -111,12 +111,10 @@
{{ i18n.export_info }}
- {{ i18n.download_backup }}
-
From 10a85283b9c1c2cf2907cef0eee93e947d6cc6ef Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 10 Feb 2018 18:29:44 -0600 Subject: [PATCH 056/178] Grammar changes --- _locales/en/messages.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5611a0ff0..7afee2c83 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -8,7 +8,7 @@ "description": "Extension Short Name." }, "extDesc": { - "message": "For Google Authenticator and Battle.net Authenticator.", + "message": "Authenticator generates 2-Step Verification codes in your browser.", "description": "Extension Description." }, "added": { @@ -40,7 +40,7 @@ "description": "Close." }, "ok": { - "message": "OK", + "message": "Ok", "description": "OK." }, "yes": { @@ -52,7 +52,7 @@ "description": "No." }, "err_acc_sec": { - "message": "Please input Account and Secret.", + "message": "Please enter account and secret.", "description": "Input Account and Secret." }, "account": { @@ -88,27 +88,27 @@ "description": "Security." }, "current_phrase": { - "message": "Current Passphrase", + "message": "Current Password", "description": "Current Passphrase." }, "new_phrase": { - "message": "New Passphrase", + "message": "New Password", "description": "New Passphrase." }, "phrase": { - "message": "Passphrase", + "message": "Password", "description": "Passphrase." }, "confirm_phrase": { - "message": "Confirm Passphrase", + "message": "Confirm Password", "description": "Confirmm Passphrase." }, "confirm_delete" : { - "message": "Are you sure you want to delete this item? This action cannot be undone.", + "message": "Are you sure you want to delete this code? This action cannot be undone.", "description": "Remove entry confirmation" }, "security_warning": { - "message": "This passphrase will be used to encrypt your secrets. No one can help you if you forget the passphrase.", + "message": "This password will be used to encrypt your secrets. No one can help you if you forget the password.", "description": "Passphrase Warning." }, "update": { @@ -116,7 +116,7 @@ "description": "Update." }, "phrase_incorrect": { - "message": "Some accounts and passphrase do not match, you cannot add new account, export accounts or change passphrass. Please enter correct passphrase and try again.", + "message": "You cannot add a new account or export data until all accounts are decrypted. Please enter the correct password before continuing.", "description": "Passphrase Incorrect." }, "phrase_not_match": { @@ -144,7 +144,7 @@ "description": "Source Code." }, "passphrase_info": { - "message": "Input passphrase to decrypt account data.", + "message": "Enter password to decrypt account data.", "description": "Passphrase Info" }, "sync_clock": { @@ -152,7 +152,7 @@ "description": "Sync Clock" }, "remember_phrase": { - "message": "Remember Passphrase", + "message": "Remember Password", "description": "Remember Passphrase" }, "clock_too_far_off": { @@ -160,15 +160,15 @@ "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "NEVER REINSTALL THE EXTENSION TO TRY TO FIX ANY ISSUE, OR YOU WILL LOSE ALL YOUR DATA! Do you have a backup for your secrets? Please note that no one can help you with getting back locked account, don't wait until it's too late. We will remind you to make a backup again after 30 days.", + "message": "Do you have a backup for your secrets? Don't wait until it's too late!", "description": "Remind Backup" }, "capture_failed": { - "message": "Capture failed, please reload the page you are veiwing and try again.", + "message": "Capture failed, please reload the page and try again.", "description": "Capture Failed" }, "unencrypted_secret_warning": { - "message": "This secret is not encrypted! Click here to set a passphrase to fix this issue.", + "message": "This secret is not encrypted! Click here to set a passphrase.", "description": "Unencrypted Secret Warning" }, "based_on_time": { @@ -192,7 +192,7 @@ "description": "Export menu info text" }, "download_backup": { - "message": "Download backup file", + "message": "Download Backup File", "description": "Download backup file." } } From 5799039a6a5df825eecc5b49bcae6c18b2e6fbb8 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 10 Feb 2018 16:59:05 -0600 Subject: [PATCH 057/178] add ensureObject --- src/models/storage.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/models/storage.ts b/src/models/storage.ts index c541add97..28c21c7cb 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -39,6 +39,14 @@ class EntryStorage { return newData; } + private static ensureObject(_data: {[hash: string]: OTPStorage}) { + for (const hash of Object.keys(_data)) { + if (typeof _data[hash] !== 'object') { + // Drop invalid data? + } + } + } + static async hasEncryptedEntry() { return new Promise( (resolve: (value: boolean) => void, @@ -61,6 +69,7 @@ class EntryStorage { reject: (reason: Error) => void) => { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { + this.ensureObject(_data); for (const hash of Object.keys(_data)) { // decrypt the data to export _data[hash].secret = _data[hash].encrypted ? From b20b6db8f3c10710c87d51682dd7477808152eb6 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 11 Feb 2018 22:20:31 +0800 Subject: [PATCH 058/178] change import backup UI, change all tab to 4 spaces in code --- _locales/en/messages.json | 14 +- _locales/zh_CN/messages.json | 396 ++++++++++++++++++----------------- css/content.css | 34 +-- css/popup.css | 8 + manifest-firefox.json | 4 +- popup.html | 9 +- 6 files changed, 240 insertions(+), 225 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 34fd2db5c..402e37931 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -44,12 +44,12 @@ "description": "OK." }, "yes": { - "message": "Yes", - "description": "Yes." + "message": "Yes", + "description": "Yes." }, "no": { - "message": "No", - "description": "No." + "message": "No", + "description": "No." }, "err_acc_sec": { "message": "Please enter account and secret.", @@ -116,7 +116,7 @@ "description": "Update." }, "phrase_incorrect": { - "message": "You cannot add a new account or export data until all accounts are decrypted. Please enter the correct password before continuing.", + "message": "You cannot add a new account or export data until all accounts are decrypted. Please enter the correct password before continuing.", "description": "Passphrase Incorrect." }, "phrase_not_match": { @@ -194,5 +194,9 @@ "download_backup": { "message": "Download Backup File", "description": "Download backup file." + }, + "import_backup": { + "message": "Import Backup File", + "description": "Import backup file." } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 293c3fc2d..19b185810 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -1,198 +1,202 @@ { - "extName": { - "message": "身份验证器", - "description": "Extension Name." - }, - "extShortName": { - "message": "身份验证器", - "description": "Extension Short Name." - }, - "extDesc": { - "message": "适用于Google身份验证器及战网安全令。", - "description": "Extension Description." - }, - "added": { - "message": "已添加。", - "description": "Added Account." - }, - "errorqr": { - "message": "无法识别的QR码。", - "description": "QR Error." - }, - "errorsecret": { - "message": "密钥错误,仅支持Base32(A-Z,2-7及=)和HEX(0-9及A-F)格式,然而您的密钥是:", - "description": "Secret Error." - }, - "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", - "description": "Information." - }, - "add_qr": { - "message": "扫描QR码", - "description": "Scan QR Code." - }, - "add_secret": { - "message": "手动输入", - "description": "Manual Entry." - }, - "close": { - "message": "关闭", - "description": "Close." - }, - "ok": { - "message": "确定", - "description": "OK." - }, - "yes": { - "message": "Yes", - "description": "Yes." - }, - "no": { - "message": "No", - "description": "No." - }, - "err_acc_sec": { - "message": "请输入账户和密钥。", - "description": "Input Account and Secret." - }, - "account": { - "message": "账户", - "description": "Account." - }, - "secret": { - "message": "密钥", - "description": "Secret." - }, - "updateSuccess": { - "message": "成功。", - "description": "Update Success." - }, - "updateFailure": { - "message": "失败。", - "description": "Update Failure." - }, - "about": { - "message": "关于", - "description": "About." - }, - "export_import": { - "message": "导出 / 导入", - "description": "Export and Import." - }, - "settings": { - "message": "设置", - "description": "Settings." - }, - "security": { - "message": "安全", - "description": "Security." - }, - "current_phrase": { - "message": "当前密码", - "description": "Current Phrase." - }, - "new_phrase": { - "message": "新密码", - "description": "New Phrase." - }, - "phrase": { - "message": "密码", - "description": "Phrase." - }, - "confirm_phrase": { - "message": "确认密码", - "description": "Confirmm Phrase." - }, - "confirm_delete" : { - "message": "您确定要删除此项吗?您无法找回已删除的密钥。此操作无法撤销。", - "description": "Remove entry confirmation" - }, - "security_warning": { - "message": "您的密钥将使用此密码进行加密。如果您忘记了密码没有人能够提供帮助。", - "description": "Phrase Warning." - }, - "update": { - "message": "更新", - "description": "Update." - }, - "phrase_incorrect": { - "message": "部分账户与密码不匹配,您无法添加新账户、导出账户数据或者更改密码。请提供正确的密码后重试。", - "description": "Phrase Incorrect." - }, - "phrase_not_match": { - "message": "两次密码不一致。", - "description": "Phrase Not Match." - }, - "encrypted": { - "message": "已加密", - "description": "Encrypted." - }, - "copied": { - "message": "已复制", - "description": "Copied." - }, - "feedback": { - "message": "问题反馈", - "description": "Feedback." - }, - "translate": { - "message": "参与翻译", - "description": "Translate." - }, - "source": { - "message": "源代码", - "description": "Source Code." - }, - "passphrase_info": { - "message": "输入密码以解码账户数据。", - "description": "Passphrase Info" - }, - "sync_clock": { - "message": "通过Google校准时间", - "description": "Sync Clock" - }, - "remember_phrase": { - "message": "记住密码", - "description": "Remember Passphrase" - }, - "clock_too_far_off": { - "message": "注意!您的本地时钟时间差过大,请修正后再进行操作。", - "description": "Local Time is Too Far Off" - }, - "remind_backup": { - "message": "永远不要通过重装扩展来尝试解决问题,否则您将丢失全部数据!您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", - "description": "Remind Backup" - }, - "capture_failed": { - "message": "捕捉失败,请重载您正在浏览的页面后重试。", - "description": "Capture Failed" - }, - "unencrypted_secret_warning": { - "message": "此密钥未被加密!点击此处来设置一个密码以解决此问题。", - "description": "Unencrypted Secret Warning" - }, - "based_on_time": { - "message": "基于时间", - "description": "Time Based" - }, - "based_on_counter": { - "message": "基于计数器", - "description": "Counter Based" - }, - "resize_popup_page": { - "message": "调整弹出页面尺寸", - "description": "Resize Popup Page" - }, - "scale": { - "message": "比例", - "description": "Scale" - }, - "export_info": { - "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", - "description": "Export menu info text" - }, - "download_backup": { - "message": "下载备份文件", - "description": "Download backup file." - } + "extName": { + "message": "身份验证器", + "description": "Extension Name." + }, + "extShortName": { + "message": "身份验证器", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "适用于Google身份验证器及战网安全令。", + "description": "Extension Description." + }, + "added": { + "message": "已添加。", + "description": "Added Account." + }, + "errorqr": { + "message": "无法识别的QR码。", + "description": "QR Error." + }, + "errorsecret": { + "message": "密钥错误,仅支持Base32(A-Z,2-7及=)和HEX(0-9及A-F)格式,然而您的密钥是:", + "description": "Secret Error." + }, + "info": { + "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", + "description": "Information." + }, + "add_qr": { + "message": "扫描QR码", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "手动输入", + "description": "Manual Entry." + }, + "close": { + "message": "关闭", + "description": "Close." + }, + "ok": { + "message": "确定", + "description": "OK." + }, + "yes": { + "message": "Yes", + "description": "Yes." + }, + "no": { + "message": "No", + "description": "No." + }, + "err_acc_sec": { + "message": "请输入账户和密钥。", + "description": "Input Account and Secret." + }, + "account": { + "message": "账户", + "description": "Account." + }, + "secret": { + "message": "密钥", + "description": "Secret." + }, + "updateSuccess": { + "message": "成功。", + "description": "Update Success." + }, + "updateFailure": { + "message": "失败。", + "description": "Update Failure." + }, + "about": { + "message": "关于", + "description": "About." + }, + "export_import": { + "message": "导出 / 导入", + "description": "Export and Import." + }, + "settings": { + "message": "设置", + "description": "Settings." + }, + "security": { + "message": "安全", + "description": "Security." + }, + "current_phrase": { + "message": "当前密码", + "description": "Current Phrase." + }, + "new_phrase": { + "message": "新密码", + "description": "New Phrase." + }, + "phrase": { + "message": "密码", + "description": "Phrase." + }, + "confirm_phrase": { + "message": "确认密码", + "description": "Confirmm Phrase." + }, + "confirm_delete" : { + "message": "您确定要删除此项吗?您无法找回已删除的密钥。此操作无法撤销。", + "description": "Remove entry confirmation" + }, + "security_warning": { + "message": "您的密钥将使用此密码进行加密。如果您忘记了密码没有人能够提供帮助。", + "description": "Phrase Warning." + }, + "update": { + "message": "更新", + "description": "Update." + }, + "phrase_incorrect": { + "message": "部分账户与密码不匹配,您无法添加新账户、导出账户数据或者更改密码。请提供正确的密码后重试。", + "description": "Phrase Incorrect." + }, + "phrase_not_match": { + "message": "两次密码不一致。", + "description": "Phrase Not Match." + }, + "encrypted": { + "message": "已加密", + "description": "Encrypted." + }, + "copied": { + "message": "已复制", + "description": "Copied." + }, + "feedback": { + "message": "问题反馈", + "description": "Feedback." + }, + "translate": { + "message": "参与翻译", + "description": "Translate." + }, + "source": { + "message": "源代码", + "description": "Source Code." + }, + "passphrase_info": { + "message": "输入密码以解码账户数据。", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "通过Google校准时间", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "记住密码", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "注意!您的本地时钟时间差过大,请修正后再进行操作。", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "永远不要通过重装扩展来尝试解决问题,否则您将丢失全部数据!您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "捕捉失败,请重载您正在浏览的页面后重试。", + "description": "Capture Failed" + }, + "unencrypted_secret_warning": { + "message": "此密钥未被加密!点击此处来设置一个密码以解决此问题。", + "description": "Unencrypted Secret Warning" + }, + "based_on_time": { + "message": "基于时间", + "description": "Time Based" + }, + "based_on_counter": { + "message": "基于计数器", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "调整弹出页面尺寸", + "description": "Resize Popup Page" + }, + "scale": { + "message": "比例", + "description": "Scale" + }, + "export_info": { + "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", + "description": "Export menu info text" + }, + "download_backup": { + "message": "下载备份文件", + "description": "Download backup file." + }, + "import_backup": { + "message": "导入备份文件", + "description": "Import backup file." + } } diff --git a/css/content.css b/css/content.css index e5c420d4e..cb30a6aaa 100644 --- a/css/content.css +++ b/css/content.css @@ -1,25 +1,25 @@ #__ga_grayLayout__ { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.6); - z-index: 1000000; - display: none; - cursor: crosshair; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.6); + z-index: 1000000; + display: none; + cursor: crosshair; } #__ga_grayLayout__ .scan { - width: 100%; - height: 100%; - position: absolute; - top: 0; - opacity: 0.5; + width: 100%; + height: 100%; + position: absolute; + top: 0; + opacity: 0.5; } #__ga_captureBox__ { - position: absolute; - border: white 1px dashed; - display: none; + position: absolute; + border: white 1px dashed; + display: none; } \ No newline at end of file diff --git a/css/popup.css b/css/popup.css index e542e0528..071720958 100644 --- a/css/popup.css +++ b/css/popup.css @@ -708,3 +708,11 @@ body { color: gray; display: block; } + +#import_file { + display: none; +} + +#upload_backup { + display: block; +} \ No newline at end of file diff --git a/manifest-firefox.json b/manifest-firefox.json index 5ad09034b..47dd73648 100644 --- a/manifest-firefox.json +++ b/manifest-firefox.json @@ -63,8 +63,8 @@ ], "permissions": [ "activeTab", - "", - "clipboardWrite", + "", + "clipboardWrite", "storage" ], "optional_permissions": [ diff --git a/popup.html b/popup.html index 7200a1a65..26879f6bb 100644 --- a/popup.html +++ b/popup.html @@ -110,11 +110,10 @@
-
{{ i18n.export_info }}
- {{ i18n.download_backup }} -
- -
+
{{ i18n.export_info }}
+ {{ i18n.download_backup }} + +
From cb39d5371769bcb34cf685c018d1c1ecd0a8a770 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 11 Feb 2018 22:22:45 +0800 Subject: [PATCH 059/178] fix style issue --- _locales/en/messages.json | 2 +- manifest-firefox.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 402e37931..a0f84a948 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -116,7 +116,7 @@ "description": "Update." }, "phrase_incorrect": { - "message": "You cannot add a new account or export data until all accounts are decrypted. Please enter the correct password before continuing.", + "message": "You cannot add a new account or export data until all accounts are decrypted. Please enter the correct password before continuing.", "description": "Passphrase Incorrect." }, "phrase_not_match": { diff --git a/manifest-firefox.json b/manifest-firefox.json index 47dd73648..3ff5f82a5 100644 --- a/manifest-firefox.json +++ b/manifest-firefox.json @@ -63,8 +63,8 @@ ], "permissions": [ "activeTab", - "", - "clipboardWrite", + "", + "clipboardWrite", "storage" ], "optional_permissions": [ From b05f676bf78d86043b6f44d38dde8dfe32644a8e Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 11 Feb 2018 22:24:21 +0800 Subject: [PATCH 060/178] fix style issue --- popup.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/popup.html b/popup.html index 26879f6bb..835fee09f 100644 --- a/popup.html +++ b/popup.html @@ -110,10 +110,10 @@
-
{{ i18n.export_info }}
- {{ i18n.download_backup }} - - +
{{ i18n.export_info }}
+ {{ i18n.download_backup }} + +
From 4bbfa8ffe12641e2eed0090a3a81faf8e9413590 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 17 Feb 2018 23:29:02 +0800 Subject: [PATCH 061/178] fix #15, #22 --- popup.html | 2 +- src/models/otp.ts | 4 +--- src/ui/entry.ts | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/popup.html b/popup.html index 835fee09f..41c34306c 100644 --- a/popup.html +++ b/popup.html @@ -111,7 +111,7 @@
{{ i18n.export_info }}
- {{ i18n.download_backup }} + {{ i18n.download_backup }}
diff --git a/src/models/otp.ts b/src/models/otp.ts index bc5ba8935..ee60775a0 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -12,7 +12,7 @@ class OTPEntry implements OTP { account: string; hash: string; counter: number; - code: string; + code = '••••••'; constructor( type: OTPType, issuer: string, secret: string, account: string, @@ -26,8 +26,6 @@ class OTPEntry implements OTP { this.counter = counter; if (this.type !== OTPType.hotp) { this.generate(); - } else { - this.code = '••••••'; } } diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 8f06d261d..76be39c35 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -142,7 +142,6 @@ async function entry(_ui: UI) { } _ui.instance.class.hotpDiabled = true; await entry.next(_ui.instance.encryption); - await _ui.instance.updateEntries(); setTimeout(() => { _ui.instance.class.hotpDiabled = false; }, 3000); From 5bd77742a4af4b9052b8b5e1d7c3ad0cecb031d7 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 17 Feb 2018 23:46:00 +0800 Subject: [PATCH 062/178] fix #18 --- src/models/otp.ts | 6 +++++- src/ui/entry.ts | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/models/otp.ts b/src/models/otp.ts index ee60775a0..bd9b418e2 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -58,7 +58,11 @@ class OTPEntry implements OTP { if (this.secret === 'Encrypted') { this.code = 'Encrypted'; } else { - this.code = KeyUtilities.generate(this.type, this.secret, this.counter); + try { + this.code = KeyUtilities.generate(this.type, this.secret, this.counter); + } catch(error) { + this.code = 'Invalid'; + } } } } \ No newline at end of file diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 76be39c35..18baaa886 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -81,6 +81,9 @@ async function entry(_ui: UI) { }, methods: { showBulls: (code: string) => { + if (code.startsWith('•')) { + return code; + } return new Array(code.length).fill('•').join(''); }, importEntries: async () => { @@ -104,7 +107,7 @@ async function entry(_ui: UI) { if (!target || !target.files) { return; } - if (target.files[0] && target.files[0].type.startsWith('text/')) { + if (target.files[0]) { const reader = new FileReader(); reader.onload = async () => { const importData = JSON.parse(reader.result); From 35d69e93aae10cb49cef6b63cafc36c0575d6478 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 17 Feb 2018 23:49:54 +0800 Subject: [PATCH 063/178] fix #4 --- src/ui/info.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/info.ts b/src/ui/info.ts index 40a5741fd..3b83fd480 100644 --- a/src/ui/info.ts +++ b/src/ui/info.ts @@ -31,6 +31,7 @@ async function info(_ui: UI) { setTimeout(() => { _ui.instance.class.fadeout = false; _ui.instance.info = ''; + _ui.instance.newAccount.show = false; }, 200); return; } From 67a42c902fe8c2673f9e0b1d8945da21c2a43779 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 18 Feb 2018 01:04:38 +0800 Subject: [PATCH 064/178] add sort --- css/popup.css | 4 + js/vue-dragula.js | 1387 +++++++++++++++++++++++++++++++++++++++ popup.html | 3 +- src/models/interface.ts | 2 + src/models/otp.ts | 2 +- src/models/storage.ts | 20 + src/ui/entry.ts | 4 + src/ui/ui.ts | 25 + 8 files changed, 1445 insertions(+), 2 deletions(-) create mode 100644 js/vue-dragula.js diff --git a/css/popup.css b/css/popup.css index 071720958..7b9b87994 100644 --- a/css/popup.css +++ b/css/popup.css @@ -715,4 +715,8 @@ body { #upload_backup { display: block; +} + +.gu-mirror { + display: none; } \ No newline at end of file diff --git a/js/vue-dragula.js b/js/vue-dragula.js new file mode 100644 index 000000000..3a701ba8f --- /dev/null +++ b/js/vue-dragula.js @@ -0,0 +1,1387 @@ +/*! + * vue-dragula v1.3.1 + * (c) 2017 Yichang Liu + * Released under the MIT License. + */ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.vueDragula = factory()); +}(this, (function () { 'use strict'; + +var babelHelpers = {}; +babelHelpers.typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { + return typeof obj; +} : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; +}; + +babelHelpers.classCallCheck = function (instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } +}; + +babelHelpers.createClass = function () { + function defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + return function (Constructor, protoProps, staticProps) { + if (protoProps) defineProperties(Constructor.prototype, protoProps); + if (staticProps) defineProperties(Constructor, staticProps); + return Constructor; + }; +}(); + +babelHelpers; + +var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + +function interopDefault(ex) { + return ex && (typeof ex === 'undefined' ? 'undefined' : babelHelpers.typeof(ex)) === 'object' && 'default' in ex ? ex['default'] : ex; +} + +function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; +} + +var atoa = createCommonjsModule(function (module) { + module.exports = function atoa(a, n) { + return Array.prototype.slice.call(a, n); + }; +}); + +var atoa$1 = interopDefault(atoa); + +var require$$1 = Object.freeze({ + default: atoa$1 +}); + +var ticky = createCommonjsModule(function (module) { + var si = typeof setImmediate === 'function', + tick; + if (si) { + tick = function tick(fn) { + setImmediate(fn); + }; + } else if (typeof process !== 'undefined' && process.nextTick) { + tick = process.nextTick; + } else { + tick = function tick(fn) { + setTimeout(fn, 0); + }; + } + + module.exports = tick; +}); + +var ticky$1 = interopDefault(ticky); + +var require$$0$1 = Object.freeze({ + default: ticky$1 +}); + +var debounce = createCommonjsModule(function (module) { + 'use strict'; + + var ticky = interopDefault(require$$0$1); + + module.exports = function debounce(fn, args, ctx) { + if (!fn) { + return; + } + ticky(function run() { + fn.apply(ctx || null, args || []); + }); + }; +}); + +var debounce$1 = interopDefault(debounce); + +var require$$0 = Object.freeze({ + default: debounce$1 +}); + +var emitter = createCommonjsModule(function (module) { + 'use strict'; + + var atoa = interopDefault(require$$1); + var debounce = interopDefault(require$$0); + + module.exports = function emitter(thing, options) { + var opts = options || {}; + var evt = {}; + if (thing === undefined) { + thing = {}; + } + thing.on = function (type, fn) { + if (!evt[type]) { + evt[type] = [fn]; + } else { + evt[type].push(fn); + } + return thing; + }; + thing.once = function (type, fn) { + fn._once = true; // thing.off(fn) still works! + thing.on(type, fn); + return thing; + }; + thing.off = function (type, fn) { + var c = arguments.length; + if (c === 1) { + delete evt[type]; + } else if (c === 0) { + evt = {}; + } else { + var et = evt[type]; + if (!et) { + return thing; + } + et.splice(et.indexOf(fn), 1); + } + return thing; + }; + thing.emit = function () { + var args = atoa(arguments); + return thing.emitterSnapshot(args.shift()).apply(this, args); + }; + thing.emitterSnapshot = function (type) { + var et = (evt[type] || []).slice(0); + return function () { + var args = atoa(arguments); + var ctx = this || thing; + if (type === 'error' && opts.throws !== false && !et.length) { + throw args.length === 1 ? args[0] : args; + } + et.forEach(function emitter(listen) { + if (opts.async) { + debounce(listen, args, ctx); + } else { + listen.apply(ctx, args); + } + if (listen._once) { + thing.off(type, listen); + } + }); + return thing; + }; + }; + return thing; + }; +}); + +var emitter$1 = interopDefault(emitter); + +var require$$2 = Object.freeze({ + default: emitter$1 +}); + +var index = createCommonjsModule(function (module) { + var NativeCustomEvent = commonjsGlobal.CustomEvent; + + function useNative() { + try { + var p = new NativeCustomEvent('cat', { detail: { foo: 'bar' } }); + return 'cat' === p.type && 'bar' === p.detail.foo; + } catch (e) {} + return false; + } + + /** + * Cross-browser `CustomEvent` constructor. + * + * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent.CustomEvent + * + * @public + */ + + module.exports = useNative() ? NativeCustomEvent : + + // IE >= 9 + 'function' === typeof document.createEvent ? function CustomEvent(type, params) { + var e = document.createEvent('CustomEvent'); + if (params) { + e.initCustomEvent(type, params.bubbles, params.cancelable, params.detail); + } else { + e.initCustomEvent(type, false, false, void 0); + } + return e; + } : + + // IE <= 8 + function CustomEvent(type, params) { + var e = document.createEventObject(); + e.type = type; + if (params) { + e.bubbles = Boolean(params.bubbles); + e.cancelable = Boolean(params.cancelable); + e.detail = params.detail; + } else { + e.bubbles = false; + e.cancelable = false; + e.detail = void 0; + } + return e; + }; +}); + +var index$1 = interopDefault(index); + +var require$$1$2 = Object.freeze({ + default: index$1 +}); + +var eventmap = createCommonjsModule(function (module) { + 'use strict'; + + var eventmap = []; + var eventname = ''; + var ron = /^on/; + + for (eventname in commonjsGlobal) { + if (ron.test(eventname)) { + eventmap.push(eventname.slice(2)); + } + } + + module.exports = eventmap; +}); + +var eventmap$1 = interopDefault(eventmap); + +var require$$0$2 = Object.freeze({ + default: eventmap$1 +}); + +var crossvent = createCommonjsModule(function (module) { + 'use strict'; + + var customEvent = interopDefault(require$$1$2); + var eventmap = interopDefault(require$$0$2); + var doc = commonjsGlobal.document; + var addEvent = addEventEasy; + var removeEvent = removeEventEasy; + var hardCache = []; + + if (!commonjsGlobal.addEventListener) { + addEvent = addEventHard; + removeEvent = removeEventHard; + } + + module.exports = { + add: addEvent, + remove: removeEvent, + fabricate: fabricateEvent + }; + + function addEventEasy(el, type, fn, capturing) { + return el.addEventListener(type, fn, capturing); + } + + function addEventHard(el, type, fn) { + return el.attachEvent('on' + type, wrap(el, type, fn)); + } + + function removeEventEasy(el, type, fn, capturing) { + return el.removeEventListener(type, fn, capturing); + } + + function removeEventHard(el, type, fn) { + var listener = unwrap(el, type, fn); + if (listener) { + return el.detachEvent('on' + type, listener); + } + } + + function fabricateEvent(el, type, model) { + var e = eventmap.indexOf(type) === -1 ? makeCustomEvent() : makeClassicEvent(); + if (el.dispatchEvent) { + el.dispatchEvent(e); + } else { + el.fireEvent('on' + type, e); + } + function makeClassicEvent() { + var e; + if (doc.createEvent) { + e = doc.createEvent('Event'); + e.initEvent(type, true, true); + } else if (doc.createEventObject) { + e = doc.createEventObject(); + } + return e; + } + function makeCustomEvent() { + return new customEvent(type, { detail: model }); + } + } + + function wrapperFactory(el, type, fn) { + return function wrapper(originalEvent) { + var e = originalEvent || commonjsGlobal.event; + e.target = e.target || e.srcElement; + e.preventDefault = e.preventDefault || function preventDefault() { + e.returnValue = false; + }; + e.stopPropagation = e.stopPropagation || function stopPropagation() { + e.cancelBubble = true; + }; + e.which = e.which || e.keyCode; + fn.call(el, e); + }; + } + + function wrap(el, type, fn) { + var wrapper = unwrap(el, type, fn) || wrapperFactory(el, type, fn); + hardCache.push({ + wrapper: wrapper, + element: el, + type: type, + fn: fn + }); + return wrapper; + } + + function unwrap(el, type, fn) { + var i = find(el, type, fn); + if (i) { + var wrapper = hardCache[i].wrapper; + hardCache.splice(i, 1); // free up a tad of memory + return wrapper; + } + } + + function find(el, type, fn) { + var i, item; + for (i = 0; i < hardCache.length; i++) { + item = hardCache[i]; + if (item.element === el && item.type === type && item.fn === fn) { + return i; + } + } + } +}); + +var crossvent$1 = interopDefault(crossvent); +var add = crossvent.add; +var remove = crossvent.remove; +var fabricate = crossvent.fabricate; + +var require$$1$1 = Object.freeze({ + default: crossvent$1, + add: add, + remove: remove, + fabricate: fabricate +}); + +var classes = createCommonjsModule(function (module) { + 'use strict'; + + var cache = {}; + var start = '(?:^|\\s)'; + var end = '(?:\\s|$)'; + + function lookupClass(className) { + var cached = cache[className]; + if (cached) { + cached.lastIndex = 0; + } else { + cache[className] = cached = new RegExp(start + className + end, 'g'); + } + return cached; + } + + function addClass(el, className) { + var current = el.className; + if (!current.length) { + el.className = className; + } else if (!lookupClass(className).test(current)) { + el.className += ' ' + className; + } + } + + function rmClass(el, className) { + el.className = el.className.replace(lookupClass(className), ' ').trim(); + } + + module.exports = { + add: addClass, + rm: rmClass + }; +}); + +var classes$1 = interopDefault(classes); +var add$1 = classes.add; +var rm = classes.rm; + +var require$$0$3 = Object.freeze({ + default: classes$1, + add: add$1, + rm: rm +}); + +var dragula = createCommonjsModule(function (module) { + 'use strict'; + + var emitter = interopDefault(require$$2); + var crossvent = interopDefault(require$$1$1); + var classes = interopDefault(require$$0$3); + var doc = document; + var documentElement = doc.documentElement; + + function dragula(initialContainers, options) { + var len = arguments.length; + if (len === 1 && Array.isArray(initialContainers) === false) { + options = initialContainers; + initialContainers = []; + } + var _mirror; // mirror image + var _source; // source container + var _item; // item being dragged + var _offsetX; // reference x + var _offsetY; // reference y + var _moveX; // reference move x + var _moveY; // reference move y + var _initialSibling; // reference sibling when grabbed + var _currentSibling; // reference sibling now + var _copy; // item used for copying + var _renderTimer; // timer for setTimeout renderMirrorImage + var _lastDropTarget = null; // last container item was over + var _grabbed; // holds mousedown context until first mousemove + + var o = options || {}; + if (o.moves === void 0) { + o.moves = always; + } + if (o.accepts === void 0) { + o.accepts = always; + } + if (o.invalid === void 0) { + o.invalid = invalidTarget; + } + if (o.containers === void 0) { + o.containers = initialContainers || []; + } + if (o.isContainer === void 0) { + o.isContainer = never; + } + if (o.copy === void 0) { + o.copy = false; + } + if (o.copySortSource === void 0) { + o.copySortSource = false; + } + if (o.revertOnSpill === void 0) { + o.revertOnSpill = false; + } + if (o.removeOnSpill === void 0) { + o.removeOnSpill = false; + } + if (o.direction === void 0) { + o.direction = 'vertical'; + } + if (o.ignoreInputTextSelection === void 0) { + o.ignoreInputTextSelection = true; + } + if (o.mirrorContainer === void 0) { + o.mirrorContainer = doc.body; + } + + var drake = emitter({ + containers: o.containers, + start: manualStart, + end: end, + cancel: cancel, + remove: remove, + destroy: destroy, + canMove: canMove, + dragging: false + }); + + if (o.removeOnSpill === true) { + drake.on('over', spillOver).on('out', spillOut); + } + + events(); + + return drake; + + function isContainer(el) { + return drake.containers.indexOf(el) !== -1 || o.isContainer(el); + } + + function events(remove) { + var op = remove ? 'remove' : 'add'; + touchy(documentElement, op, 'mousedown', grab); + touchy(documentElement, op, 'mouseup', release); + } + + function eventualMovements(remove) { + var op = remove ? 'remove' : 'add'; + touchy(documentElement, op, 'mousemove', startBecauseMouseMoved); + } + + function movements(remove) { + var op = remove ? 'remove' : 'add'; + crossvent[op](documentElement, 'selectstart', preventGrabbed); // IE8 + crossvent[op](documentElement, 'click', preventGrabbed); + } + + function destroy() { + events(true); + release({}); + } + + function preventGrabbed(e) { + if (_grabbed) { + e.preventDefault(); + } + } + + function grab(e) { + _moveX = e.clientX; + _moveY = e.clientY; + + var ignore = whichMouseButton(e) !== 1 || e.metaKey || e.ctrlKey; + if (ignore) { + return; // we only care about honest-to-god left clicks and touch events + } + var item = e.target; + var context = canStart(item); + if (!context) { + return; + } + _grabbed = context; + eventualMovements(); + if (e.type === 'mousedown') { + if (isInput(item)) { + // see also: https://github.com/bevacqua/dragula/issues/208 + item.focus(); // fixes https://github.com/bevacqua/dragula/issues/176 + } else { + e.preventDefault(); // fixes https://github.com/bevacqua/dragula/issues/155 + } + } + } + + function startBecauseMouseMoved(e) { + if (!_grabbed) { + return; + } + if (whichMouseButton(e) === 0) { + release({}); + return; // when text is selected on an input and then dragged, mouseup doesn't fire. this is our only hope + } + // truthy check fixes #239, equality fixes #207 + if (e.clientX !== void 0 && e.clientX === _moveX && e.clientY !== void 0 && e.clientY === _moveY) { + return; + } + if (o.ignoreInputTextSelection) { + var clientX = getCoord('clientX', e); + var clientY = getCoord('clientY', e); + var elementBehindCursor = doc.elementFromPoint(clientX, clientY); + if (isInput(elementBehindCursor)) { + return; + } + } + + var grabbed = _grabbed; // call to end() unsets _grabbed + eventualMovements(true); + movements(); + end(); + start(grabbed); + + var offset = getOffset(_item); + _offsetX = getCoord('pageX', e) - offset.left; + _offsetY = getCoord('pageY', e) - offset.top; + + classes.add(_copy || _item, 'gu-transit'); + renderMirrorImage(); + drag(e); + } + + function canStart(item) { + if (drake.dragging && _mirror) { + return; + } + if (isContainer(item)) { + return; // don't drag container itself + } + var handle = item; + while (getParent(item) && isContainer(getParent(item)) === false) { + if (o.invalid(item, handle)) { + return; + } + item = getParent(item); // drag target should be a top element + if (!item) { + return; + } + } + var source = getParent(item); + if (!source) { + return; + } + if (o.invalid(item, handle)) { + return; + } + + var movable = o.moves(item, source, handle, nextEl(item)); + if (!movable) { + return; + } + + return { + item: item, + source: source + }; + } + + function canMove(item) { + return !!canStart(item); + } + + function manualStart(item) { + var context = canStart(item); + if (context) { + start(context); + } + } + + function start(context) { + if (isCopy(context.item, context.source)) { + _copy = context.item.cloneNode(true); + drake.emit('cloned', _copy, context.item, 'copy'); + } + + _source = context.source; + _item = context.item; + _initialSibling = _currentSibling = nextEl(context.item); + + drake.dragging = true; + drake.emit('drag', _item, _source); + } + + function invalidTarget() { + return false; + } + + function end() { + if (!drake.dragging) { + return; + } + var item = _copy || _item; + drop(item, getParent(item)); + } + + function ungrab() { + _grabbed = false; + eventualMovements(true); + movements(true); + } + + function release(e) { + ungrab(); + + if (!drake.dragging) { + return; + } + var item = _copy || _item; + var clientX = getCoord('clientX', e); + var clientY = getCoord('clientY', e); + var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY); + var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY); + if (dropTarget && (_copy && o.copySortSource || !_copy || dropTarget !== _source)) { + drop(item, dropTarget); + } else if (o.removeOnSpill) { + remove(); + } else { + cancel(); + } + } + + function drop(item, target) { + var parent = getParent(item); + if (_copy && o.copySortSource && target === _source) { + parent.removeChild(_item); + } + if (isInitialPlacement(target)) { + drake.emit('cancel', item, _source, _source); + } else { + drake.emit('drop', item, target, _source, _currentSibling); + } + cleanup(); + } + + function remove() { + if (!drake.dragging) { + return; + } + var item = _copy || _item; + var parent = getParent(item); + if (parent) { + parent.removeChild(item); + } + drake.emit(_copy ? 'cancel' : 'remove', item, parent, _source); + cleanup(); + } + + function cancel(revert) { + if (!drake.dragging) { + return; + } + var reverts = arguments.length > 0 ? revert : o.revertOnSpill; + var item = _copy || _item; + var parent = getParent(item); + var initial = isInitialPlacement(parent); + if (initial === false && reverts) { + if (_copy) { + if (parent) { + parent.removeChild(_copy); + } + } else { + _source.insertBefore(item, _initialSibling); + } + } + if (initial || reverts) { + drake.emit('cancel', item, _source, _source); + } else { + drake.emit('drop', item, parent, _source, _currentSibling); + } + cleanup(); + } + + function cleanup() { + var item = _copy || _item; + ungrab(); + removeMirrorImage(); + if (item) { + classes.rm(item, 'gu-transit'); + } + if (_renderTimer) { + clearTimeout(_renderTimer); + } + drake.dragging = false; + if (_lastDropTarget) { + drake.emit('out', item, _lastDropTarget, _source); + } + drake.emit('dragend', item); + _source = _item = _copy = _initialSibling = _currentSibling = _renderTimer = _lastDropTarget = null; + } + + function isInitialPlacement(target, s) { + var sibling; + if (s !== void 0) { + sibling = s; + } else if (_mirror) { + sibling = _currentSibling; + } else { + sibling = nextEl(_copy || _item); + } + return target === _source && sibling === _initialSibling; + } + + function findDropTarget(elementBehindCursor, clientX, clientY) { + var target = elementBehindCursor; + while (target && !accepted()) { + target = getParent(target); + } + return target; + + function accepted() { + var droppable = isContainer(target); + if (droppable === false) { + return false; + } + + var immediate = getImmediateChild(target, elementBehindCursor); + var reference = getReference(target, immediate, clientX, clientY); + var initial = isInitialPlacement(target, reference); + if (initial) { + return true; // should always be able to drop it right back where it was + } + return o.accepts(_item, target, _source, reference); + } + } + + function drag(e) { + if (!_mirror) { + return; + } + e.preventDefault(); + + var clientX = getCoord('clientX', e); + var clientY = getCoord('clientY', e); + var x = clientX - _offsetX; + var y = clientY - _offsetY; + + _mirror.style.left = x + 'px'; + _mirror.style.top = y + 'px'; + + var item = _copy || _item; + var elementBehindCursor = getElementBehindPoint(_mirror, clientX, clientY); + var dropTarget = findDropTarget(elementBehindCursor, clientX, clientY); + var changed = dropTarget !== null && dropTarget !== _lastDropTarget; + if (changed || dropTarget === null) { + out(); + _lastDropTarget = dropTarget; + over(); + } + var parent = getParent(item); + if (dropTarget === _source && _copy && !o.copySortSource) { + if (parent) { + parent.removeChild(item); + } + return; + } + var reference; + var immediate = getImmediateChild(dropTarget, elementBehindCursor); + if (immediate !== null) { + reference = getReference(dropTarget, immediate, clientX, clientY); + } else if (o.revertOnSpill === true && !_copy) { + reference = _initialSibling; + dropTarget = _source; + } else { + if (_copy && parent) { + parent.removeChild(item); + } + return; + } + if (reference === null && changed || reference !== item && reference !== nextEl(item)) { + _currentSibling = reference; + dropTarget.insertBefore(item, reference); + drake.emit('shadow', item, dropTarget, _source); + } + function moved(type) { + drake.emit(type, item, _lastDropTarget, _source); + } + function over() { + if (changed) { + moved('over'); + } + } + function out() { + if (_lastDropTarget) { + moved('out'); + } + } + } + + function spillOver(el) { + classes.rm(el, 'gu-hide'); + } + + function spillOut(el) { + if (drake.dragging) { + classes.add(el, 'gu-hide'); + } + } + + function renderMirrorImage() { + if (_mirror) { + return; + } + var rect = _item.getBoundingClientRect(); + _mirror = _item.cloneNode(true); + _mirror.style.width = getRectWidth(rect) + 'px'; + _mirror.style.height = getRectHeight(rect) + 'px'; + classes.rm(_mirror, 'gu-transit'); + classes.add(_mirror, 'gu-mirror'); + o.mirrorContainer.appendChild(_mirror); + touchy(documentElement, 'add', 'mousemove', drag); + classes.add(o.mirrorContainer, 'gu-unselectable'); + drake.emit('cloned', _mirror, _item, 'mirror'); + } + + function removeMirrorImage() { + if (_mirror) { + classes.rm(o.mirrorContainer, 'gu-unselectable'); + touchy(documentElement, 'remove', 'mousemove', drag); + getParent(_mirror).removeChild(_mirror); + _mirror = null; + } + } + + function getImmediateChild(dropTarget, target) { + var immediate = target; + while (immediate !== dropTarget && getParent(immediate) !== dropTarget) { + immediate = getParent(immediate); + } + if (immediate === documentElement) { + return null; + } + return immediate; + } + + function getReference(dropTarget, target, x, y) { + var horizontal = o.direction === 'horizontal'; + var reference = target !== dropTarget ? inside() : outside(); + return reference; + + function outside() { + // slower, but able to figure out any position + var len = dropTarget.children.length; + var i; + var el; + var rect; + for (i = 0; i < len; i++) { + el = dropTarget.children[i]; + rect = el.getBoundingClientRect(); + if (horizontal && rect.left + rect.width / 2 > x) { + return el; + } + if (!horizontal && rect.top + rect.height / 2 > y) { + return el; + } + } + return null; + } + + function inside() { + // faster, but only available if dropped inside a child element + var rect = target.getBoundingClientRect(); + if (horizontal) { + return resolve(x > rect.left + getRectWidth(rect) / 2); + } + return resolve(y > rect.top + getRectHeight(rect) / 2); + } + + function resolve(after) { + return after ? nextEl(target) : target; + } + } + + function isCopy(item, container) { + return typeof o.copy === 'boolean' ? o.copy : o.copy(item, container); + } + } + + function touchy(el, op, type, fn) { + var touch = { + mouseup: 'touchend', + mousedown: 'touchstart', + mousemove: 'touchmove' + }; + var pointers = { + mouseup: 'pointerup', + mousedown: 'pointerdown', + mousemove: 'pointermove' + }; + var microsoft = { + mouseup: 'MSPointerUp', + mousedown: 'MSPointerDown', + mousemove: 'MSPointerMove' + }; + if (commonjsGlobal.navigator.pointerEnabled) { + crossvent[op](el, pointers[type], fn); + } else if (commonjsGlobal.navigator.msPointerEnabled) { + crossvent[op](el, microsoft[type], fn); + } else { + crossvent[op](el, touch[type], fn); + crossvent[op](el, type, fn); + } + } + + function whichMouseButton(e) { + if (e.touches !== void 0) { + return e.touches.length; + } + if (e.which !== void 0 && e.which !== 0) { + return e.which; + } // see https://github.com/bevacqua/dragula/issues/261 + if (e.buttons !== void 0) { + return e.buttons; + } + var button = e.button; + if (button !== void 0) { + // see https://github.com/jquery/jquery/blob/99e8ff1baa7ae341e94bb89c3e84570c7c3ad9ea/src/event.js#L573-L575 + return button & 1 ? 1 : button & 2 ? 3 : button & 4 ? 2 : 0; + } + } + + function getOffset(el) { + var rect = el.getBoundingClientRect(); + return { + left: rect.left + getScroll('scrollLeft', 'pageXOffset'), + top: rect.top + getScroll('scrollTop', 'pageYOffset') + }; + } + + function getScroll(scrollProp, offsetProp) { + if (typeof commonjsGlobal[offsetProp] !== 'undefined') { + return commonjsGlobal[offsetProp]; + } + if (documentElement.clientHeight) { + return documentElement[scrollProp]; + } + return doc.body[scrollProp]; + } + + function getElementBehindPoint(point, x, y) { + var p = point || {}; + var state = p.className; + var el; + p.className += ' gu-hide'; + el = doc.elementFromPoint(x, y); + p.className = state; + return el; + } + + function never() { + return false; + } + function always() { + return true; + } + function getRectWidth(rect) { + return rect.width || rect.right - rect.left; + } + function getRectHeight(rect) { + return rect.height || rect.bottom - rect.top; + } + function getParent(el) { + return el.parentNode === doc ? null : el.parentNode; + } + function isInput(el) { + return el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT' || isEditable(el); + } + function isEditable(el) { + if (!el) { + return false; + } // no parents were editable + if (el.contentEditable === 'false') { + return false; + } // stop the lookup + if (el.contentEditable === 'true') { + return true; + } // found a contentEditable element in the chain + return isEditable(getParent(el)); // contentEditable is set to 'inherit' + } + + function nextEl(el) { + return el.nextElementSibling || manually(); + function manually() { + var sibling = el; + do { + sibling = sibling.nextSibling; + } while (sibling && sibling.nodeType !== 1); + return sibling; + } + } + + function getEventHost(e) { + // on touchend event, we have to use `e.changedTouches` + // see http://stackoverflow.com/questions/7192563/touchend-event-properties + // see https://github.com/bevacqua/dragula/issues/34 + if (e.targetTouches && e.targetTouches.length) { + return e.targetTouches[0]; + } + if (e.changedTouches && e.changedTouches.length) { + return e.changedTouches[0]; + } + return e; + } + + function getCoord(coord, e) { + var host = getEventHost(e); + var missMap = { + pageX: 'clientX', // IE8 + pageY: 'clientY' // IE8 + }; + if (coord in missMap && !(coord in host) && missMap[coord] in host) { + coord = missMap[coord]; + } + return host[coord]; + } + + module.exports = dragula; +}); + +var dragula$1 = interopDefault(dragula); + +if (!dragula$1) { + throw new Error('[vue-dragula] cannot locate dragula.'); +} + +var raf = window.requestAnimationFrame; +var waitForTransition = raf ? function (fn) { + raf(function () { + raf(fn); + }); +} : function (fn) { + window.setTimeout(fn, 50); +}; + +var DragulaService = function () { + function DragulaService(Vue) { + babelHelpers.classCallCheck(this, DragulaService); + + this.bags = []; // bag store + this.eventBus = new Vue(); + this.events = ['cancel', 'cloned', 'drag', 'dragend', 'drop', 'out', 'over', 'remove', 'shadow', 'dropModel', 'removeModel']; + } + + babelHelpers.createClass(DragulaService, [{ + key: 'add', + value: function add(name, drake) { + var bag = this.find(name); + if (bag) { + throw new Error('Bag named: "' + name + '" already exists.'); + } + bag = { + name: name, + drake: drake + }; + this.bags.push(bag); + if (drake.models) { + this.handleModels(name, drake); + } + if (!bag.initEvents) { + this.setupEvents(bag); + } + return bag; + } + }, { + key: 'find', + value: function find(name) { + var bags = this.bags; + for (var i = 0; i < bags.length; i++) { + if (bags[i].name === name) { + return bags[i]; + } + } + } + }, { + key: 'handleModels', + value: function handleModels(name, drake) { + var _this2 = this; + + if (drake.registered) { + // do not register events twice + return; + } + var dragElm = void 0; + var dragIndex = void 0; + var dropIndex = void 0; + var sourceModel = void 0; + drake.on('remove', function (el, container, source) { + if (!drake.models) { + return; + } + sourceModel = _this2.findModelForContainer(source, drake); + sourceModel.splice(dragIndex, 1); + drake.cancel(true); + _this2.eventBus.$emit('removeModel', [name, el, source, dragIndex]); + }); + drake.on('drag', function (el, source) { + dragElm = el; + dragIndex = _this2.domIndexOf(el, source); + }); + drake.on('drop', function (dropElm, target, source) { + if (!drake.models || !target) { + return; + } + dropIndex = _this2.domIndexOf(dropElm, target); + sourceModel = _this2.findModelForContainer(source, drake); + + if (target === source) { + sourceModel.splice(dropIndex, 0, sourceModel.splice(dragIndex, 1)[0]); + } else { + var notCopy = dragElm === dropElm; + var targetModel = _this2.findModelForContainer(target, drake); + var dropElmModel = notCopy ? sourceModel[dragIndex] : JSON.parse(JSON.stringify(sourceModel[dragIndex])); + + if (notCopy) { + waitForTransition(function () { + sourceModel.splice(dragIndex, 1); + }); + } + targetModel.splice(dropIndex, 0, dropElmModel); + drake.cancel(true); + } + _this2.eventBus.$emit('dropModel', [name, dropElm, target, source, dropIndex]); + }); + drake.registered = true; + } + }, { + key: 'destroy', + value: function destroy(name) { + var bag = this.find(name); + if (!bag) { + return; + } + var bagIndex = this.bags.indexOf(bag); + this.bags.splice(bagIndex, 1); + bag.drake.destroy(); + } + }, { + key: 'setOptions', + value: function setOptions(name, options) { + var bag = this.add(name, dragula$1(options)); + this.handleModels(name, bag.drake); + } + }, { + key: 'setupEvents', + value: function setupEvents(bag) { + bag.initEvents = true; + var _this = this; + var emitter = function emitter(type) { + function replicate() { + var args = Array.prototype.slice.call(arguments); + _this.eventBus.$emit(type, [bag.name].concat(args)); + } + bag.drake.on(type, replicate); + }; + this.events.forEach(emitter); + } + }, { + key: 'domIndexOf', + value: function domIndexOf(child, parent) { + return Array.prototype.indexOf.call(parent.children, child); + } + }, { + key: 'findModelForContainer', + value: function findModelForContainer(container, drake) { + return (this.findModelContainerByContainer(container, drake) || {}).model; + } + }, { + key: 'findModelContainerByContainer', + value: function findModelContainerByContainer(container, drake) { + if (!drake.models) { + return; + } + return drake.models.find(function (model) { + return model.container === container; + }); + } + }]); + return DragulaService; +}(); + +if (!dragula$1) { + throw new Error('[vue-dragula] cannot locate dragula.'); +} + +function VueDragula (Vue) { + var service = new DragulaService(Vue); + + var name = 'globalBag'; + var drake = void 0; + + Vue.vueDragula = { + options: service.setOptions.bind(service), + find: service.find.bind(service), + eventBus: service.eventBus + }; + + Vue.directive('dragula', { + params: ['bag'], + + bind: function bind(container, binding, vnode) { + var bagName = vnode ? vnode.data.attrs.bag // Vue 2 + : this.params.bag; // Vue 1 + if (!vnode) { + container = this.el; // Vue 1 + } + if (bagName !== undefined && bagName.length !== 0) { + name = bagName; + } + var bag = service.find(name); + if (bag) { + drake = bag.drake; + drake.containers.push(container); + return; + } + drake = dragula$1({ + containers: [container] + }); + service.add(name, drake); + + service.handleModels(name, drake); + }, + update: function update(container, binding, vnode, oldVnode) { + var newValue = vnode ? binding.value // Vue 2 + : container; // Vue 1 + if (!newValue) { + return; + } + + var bagName = vnode ? vnode.data.attrs.bag // Vue 2 + : this.params.bag; // Vue 1 + if (bagName !== undefined && bagName.length !== 0) { + name = bagName; + } + var bag = service.find(name); + drake = bag.drake; + if (!drake.models) { + drake.models = []; + } + + if (!vnode) { + container = this.el; // Vue 1 + } + var modelContainer = service.findModelContainerByContainer(container, drake); + + if (modelContainer) { + modelContainer.model = newValue; + } else { + drake.models.push({ + model: newValue, + container: container + }); + } + }, + unbind: function unbind(container, binding, vnode) { + var unbindBagName = 'globalBag'; + var bagName = vnode ? vnode.data.attrs.bag // Vue 2 + : this.params.bag; // Vue 1 + if (!vnode) { + container = this.el; // Vue 1 + } + if (bagName !== undefined && bagName.length !== 0) { + unbindBagName = bagName; + } + var drake = service.find(unbindBagName).drake; + if (!drake) { + return; + } + var containerIndex = drake.containers.indexOf(container); + if (containerIndex > -1) { + drake.containers.splice(containerIndex, 1); + } + if (drake.containers.length === 0) { + service.destroy(unbindBagName); + } + } + }); +} + +function plugin(Vue) { + var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; + + if (plugin.installed) { + console.warn('[vue-dragula] already installed.'); + } + + VueDragula(Vue); +} + +plugin.version = '1.0.0'; + +if (typeof define === 'function' && define.amd) { + // eslint-disable-line + define([], function () { + plugin; + }); // eslint-disable-line +} else if (window.Vue) { + window.Vue.use(plugin); + } + +return plugin; + +}))); \ No newline at end of file diff --git a/popup.html b/popup.html index 41c34306c..fb68efe36 100644 --- a/popup.html +++ b/popup.html @@ -5,6 +5,7 @@ + @@ -22,7 +23,7 @@
-
+
diff --git a/src/models/interface.ts b/src/models/interface.ts index 97efcd7a4..eca3a77dc 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -51,4 +51,6 @@ interface UIConfig { /* tslint:disable-next-line:no-any */ [name: string]: (...arg: any[]) => any }; + /* tslint:disable-next-line:no-any */ + ready?: (...arg: any[]) => any; } \ No newline at end of file diff --git a/src/models/otp.ts b/src/models/otp.ts index bd9b418e2..806939894 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -60,7 +60,7 @@ class OTPEntry implements OTP { } else { try { this.code = KeyUtilities.generate(this.type, this.secret, this.counter); - } catch(error) { + } catch (error) { this.code = 'Invalid'; } } diff --git a/src/models/storage.ts b/src/models/storage.ts index c541add97..2796453a7 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -185,6 +185,26 @@ class EntryStorage { }); } + static async set(encryption: Encryption, entries: OTPEntry[]) { + return new Promise( + (resolve: () => void, reject: (reason: Error) => void) => { + try { + chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { + entries.forEach(entry => { + const storageItem = + this.getOTPStorageFromEntry(encryption, entry); + _data[entry.hash] = storageItem; + }); + _data = this.ensureUniqueIndex(_data); + chrome.storage.sync.set(_data, resolve); + }); + return; + } catch (error) { + reject(error); + } + }); + } + static async get(encryption: Encryption) { return new Promise( (resolve: (value: OTPEntry[]) => void, diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 18baaa886..1531f1495 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -80,6 +80,10 @@ async function entry(_ui: UI) { notificationTimeout: 0 }, methods: { + updateStorage: async () => { + await EntryStorage.set(_ui.instance.encryption, _ui.instance.entries); + return; + }, showBulls: (code: string) => { if (code.startsWith('•')) { return code; diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 5c4b530d4..7e4c46ad9 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -6,6 +6,9 @@ /* tslint:disable-next-line:no-any */ declare var Vue: any; +/* tslint:disable-next-line:no-any */ +declare var vueDragula: any; + class UI { private ui: UIConfig; // Vue instance @@ -33,6 +36,28 @@ class UI { } generate() { + Vue.use(vueDragula); + this.ui.ready = () => { + Vue.vueDragula.eventBus.$on('drop', async () => { + // wait for this.instance.entries sync from dom + setTimeout(async () => { + let needUpdate = false; + for (let i = 0; i < this.instance.entries.length; i++) { + const entry: OTPEntry = this.instance.entries[i]; + if (entry.index !== i) { + needUpdate = true; + entry.index = i; + } + } + + if (needUpdate) { + await this.instance.updateStorage(); + } + return; + }, 0); + return; + }); + }; this.instance = new Vue(this.ui); return this.instance; } From 5ebedc5a0c570850e1197ea93c526632b17394dc Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 18 Feb 2018 01:21:09 +0800 Subject: [PATCH 065/178] fix copy issue --- css/popup.css | 6 +++++- popup.html | 2 +- src/ui/entry.ts | 8 +++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/css/popup.css b/css/popup.css index 7b9b87994..de485320d 100644 --- a/css/popup.css +++ b/css/popup.css @@ -270,7 +270,7 @@ body { cursor: default; } -.hotp[hasCode="true"] { +.hotp:not(.no-copy) { color: #08C; cursor: pointer; } @@ -719,4 +719,8 @@ body { .gu-mirror { display: none; +} + +.no-copy { + cursor: default; } \ No newline at end of file diff --git a/popup.html b/popup.html index fb68efe36..4724bc1b8 100644 --- a/popup.html +++ b/popup.html @@ -33,7 +33,7 @@
-
+
diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 1531f1495..0ed83cd15 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -80,6 +80,10 @@ async function entry(_ui: UI) { notificationTimeout: 0 }, methods: { + noCopy: (code: string) => { + return code === 'Encrypted' || code === 'Invalid' || + code.startsWith('•'); + }, updateStorage: async () => { await EntryStorage.set(_ui.instance.encryption, _ui.instance.entries); return; @@ -155,7 +159,8 @@ async function entry(_ui: UI) { return; }, copyCode: (entry: OTPEntry) => { - if (_ui.instance.class.edit) { + if (_ui.instance.class.edit || entry.code === 'Invalid' || + entry.code.startsWith('•')) { return; } @@ -163,6 +168,7 @@ async function entry(_ui: UI) { _ui.instance.showInfo('passphrase'); return; } + chrome.permissions.request( {permissions: ['clipboardWrite']}, (granted) => { if (granted) { From db97c7f8e5b72c411f5bc255f1b653e4dc0013f6 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 18 Feb 2018 01:42:41 +0800 Subject: [PATCH 066/178] update url --- popup.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/popup.html b/popup.html index 4724bc1b8..7b9027f0a 100644 --- a/popup.html +++ b/popup.html @@ -60,9 +60,9 @@
Version {{ version }}
From fd27b19b87c5398c7a26350cd050f0eed3e19dff Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 18 Feb 2018 03:21:26 +0800 Subject: [PATCH 067/178] add scan shortcut --- css/popup.css | 11 +++++++++++ popup.html | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index de485320d..60d758b9f 100644 --- a/css/popup.css +++ b/css/popup.css @@ -427,6 +427,17 @@ body { cursor: pointer; } +#scan { + position: absolute; + right: 50px; + bottom: 0; + height: 38px; + line-height: 41px; + font-size: 16px; + color: gray; + cursor: pointer; +} + .counter { color: #888; font-size: 18px; diff --git a/popup.html b/popup.html index 7b9027f0a..1d801c95b 100644 --- a/popup.html +++ b/popup.html @@ -20,7 +20,8 @@
From 12f01ec3edd2e29a8522a89812e27ea3180e033f Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 18 Feb 2018 03:59:42 +0800 Subject: [PATCH 068/178] move code generate interval to ui init --- src/popup.ts | 5 ----- src/ui/entry.ts | 5 ++++- src/ui/ui.ts | 6 +++++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/popup.ts b/src/popup.ts index 690e87fa4..96034d193 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -29,11 +29,6 @@ async function init() { authenticator.showInfo('passphrase'); } - updateCode(authenticator); - setInterval(async () => { - await updateCode(authenticator); - }, 1000); - // Remind backup const clientTime = Math.floor(new Date().getTime() / 1000 / 3600 / 24); if (!localStorage.lastRemindingBackupTime) { diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 0ed83cd15..10cfd4908 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -80,6 +80,9 @@ async function entry(_ui: UI) { notificationTimeout: 0 }, methods: { + updateCode: async () => { + return await updateCode(_ui.instance); + }, noCopy: (code: string) => { return code === 'Encrypted' || code === 'Invalid' || code.startsWith('•'); @@ -107,7 +110,7 @@ async function entry(_ui: UI) { _ui.instance.exportData = JSON.stringify(exportData, null, 2); _ui.instance.entries = await getEntries(_ui.instance.encryption); _ui.instance.exportFile = getBackupFile(exportData); - updateCode(_ui.instance); + await _ui.instance.updateCode(); return; }, importFile: (event: Event) => { diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 7e4c46ad9..25d0bd4c5 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -37,7 +37,7 @@ class UI { generate() { Vue.use(vueDragula); - this.ui.ready = () => { + this.ui.ready = async () => { Vue.vueDragula.eventBus.$on('drop', async () => { // wait for this.instance.entries sync from dom setTimeout(async () => { @@ -59,6 +59,10 @@ class UI { }); }; this.instance = new Vue(this.ui); + this.instance.updateCode(); + setInterval(async () => { + await this.instance.updateCode(); + }, 1000); return this.instance; } } \ No newline at end of file From b8bef4d2783c86a032fa8153f07cd765fb8c6bf7 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 18 Feb 2018 04:02:28 +0800 Subject: [PATCH 069/178] remove async from ready method --- src/ui/ui.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 25d0bd4c5..806267d64 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -37,7 +37,7 @@ class UI { generate() { Vue.use(vueDragula); - this.ui.ready = async () => { + this.ui.ready = () => { Vue.vueDragula.eventBus.$on('drop', async () => { // wait for this.instance.entries sync from dom setTimeout(async () => { From 5d95851c4bca72a2fe44724abddb94d3975821e3 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 01:34:05 +0800 Subject: [PATCH 070/178] add dropbox backup --- _locales/en/messages.json | 16 +++ _locales/fr/messages.json | 194 ----------------------------------- _locales/zh_CN/messages.json | 16 +++ css/popup.css | 25 +++++ manifest-chrome.json | 5 +- manifest-firefox.json | 5 +- package.json | 6 +- popup.html | 16 ++- src/models/dropbox.ts | 97 ++++++++++++++++++ src/popup.ts | 53 ++++++++-- src/ui/info.ts | 25 ++++- 11 files changed, 246 insertions(+), 212 deletions(-) delete mode 100644 _locales/fr/messages.json create mode 100644 src/models/dropbox.ts diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a0f84a948..3c1bf0207 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -198,5 +198,21 @@ "import_backup": { "message": "Import Backup File", "description": "Import backup file." + }, + "dropbox_backup": { + "message": "Auto Backup to Dropbox", + "description": "Auto backup to Dropbox." + }, + "dropbox_code": { + "message": "Dropbox Code", + "description": "Dropbox code." + }, + "dropbox_token": { + "message": "Dropbox Token", + "description": "Dropbox token." + }, + "dropbox_authorization": { + "message": "Dropbox Authorization", + "description": "Dropbox authorization." } } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json deleted file mode 100644 index 5147d467d..000000000 --- a/_locales/fr/messages.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "extName": { - "message": "S’authentifier", - "description": "Extension Name." - }, - "extShortName": { - "message": "S’authentifier", - "description": "Extension Short Name." - }, - "extDesc": { - "message": "Pour Google Authenticator et Battle.net Authenticator.", - "description": "Extension Description." - }, - "added": { - "message": " a bien été ajouté.", - "description": "Added Account." - }, - "errorqr": { - "message": "Code QR non reconnu.", - "description": "QR Error." - }, - "errorsecret": { - "message": "Erreur de secret. Seul Base32 (A-Z, 2-7 =) et HEX (0-9 et A-F) sont pris en charge. Toutefois, votre secret est : ", - "description": "Secret Error." - }, - "info": { - "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2018 Sneezry<\/a>. Released under the MIT License.<\/p>

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.<\/p>

jsqrcode<\/a> Copyright jsqrcode authors. Licensed under the Apache License.<\/p>

ZXing<\/a> Copyright ZXing authors. Licensed under the Apache License.<\/p>

totp.js<\/a> Copyright its author. Licensed under the MIT License.<\/p>

jsSHA<\/a> Copyright jsSHA authors. Licensed under the BSD License.<\/p>

qrcode.js<\/a> Copyright qrcode.js author. Licensed under the MIT License.<\/p>

crypto-js<\/a> Copyright crypto-js author. Licensed under the BSD License.<\/p>

Droid Sans Mono<\/a> Copyright Steve Matteson. Licensed under the Apache License.<\/p>

Font Awesome<\/a> Copyright Dave Gandy. Licensed under the SIL OFL License 1.1.<\/p>

Thanks to Mike Robinson <3<\/p>", - "description": "Information." - }, - "add_qr": { - "message": "Scanner le 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." - }, - "err_acc_sec": { - "message": "Entrez le Compte ainsi que le Secret.", - "description": "Input Account and Secret." - }, - "account": { - "message": "Compte", - "description": "Account." - }, - "secret": { - "message": "Secret", - "description": "Secret." - }, - "updateSuccess": { - "message": "Succès.", - "description": "Update Success." - }, - "updateFailure": { - "message": "Échec.", - "description": "Update Failure." - }, - "about": { - "message": "A propos", - "description": "About." - }, - "export_import": { - "message": "Exporter \/ Importer", - "description": "Export and Import." - }, - "settings": { - "message": "Paramètres", - "description": "Settings." - }, - "security": { - "message": "Sécurité", - "description": "Security." - }, - "current_phrase": { - "message": "Mot de passe actuel", - "description": "Current Passphrase." - }, - "new_phrase": { - "message": "Nouveau mot de passe", - "description": "New Passphrase." - }, - "phrase": { - "message": "Mot de passe", - "description": "Passphrase." - }, - "confirm_phrase": { - "message": "Confirmer le mot de passe", - "description": "Confirmm Passphrase." - }, - "confirm_delete": { - "message": "Souhaitez-vous vraiment supprimer ce secret?", - "description": "Confirm Delete." - }, - "security_warning": { - "message": "Ajouter un mot de passe pour crypter vos secrets. Personne ne peut vous aider si vous avez oublié votre mot de passe.", - "description": "Passphrase Warning." - }, - "update": { - "message": "Mettre à jour", - "description": "Update." - }, - "phrase_incorrect": { - "message": "Certains comptes et mots de passe ne correspondent pas.", - "description": "Passphrase Incorrect." - }, - "phrase_not_match": { - "message": "Mots de passe sont différents.", - "description": "Passphrase Not Match." - }, - "encrypted": { - "message": "Crypté", - "description": "Encrypted." - }, - "copied": { - "message": "Copié", - "description": "Copied." - }, - "feedback": { - "message": "Retours", - "description": "Feedback." - }, - "translate": { - "message": "Traduire", - "description": "Translate." - }, - "source": { - "message": "Code source", - "description": "Source Code." - }, - "passphrase_info": { - "message": "Entrez le Mot de passe pour déchiffrer les données du compte.", - "description": "Passphrase Info" - }, - "sync_clock": { - "message": "Synchroniser l'horloge avec Google", - "description": "Sync Clock" - }, - "remember_phrase": { - "message": "Mémoriser le mot de passe", - "description": "Remember Passphrase" - }, - "clock_too_far_off": { - "message": "Attention ! Votre horloge locale est trop décalée, veuillez rectifier cela avant de continuer.", - "description": "Local Time is Too Far Off" - }, - "remind_backup": { - "message": "JAMAIS RÉINSTALLER L’EXTENSION POUR TENTER DE RÉSOUDRE TOUT PROBLÈME, OU VOUS PERDREZ TOUTES VOS DONNÉES ! Vous avez une sauvegarde de vos secrets ? Veuillez noter que personne ne peut vous aider à retrouver un compte verrouillé, n’attendez pas qu’il ne soit trop tard. Nous allons rappeler vous de faire une sauvegarde après 30 jours.", - "description": "Remind Backup" - }, - "capture_failed": { - "message": "La capture a échoué, veuillez recharger la page que vous regardez et réessayez.", - "description": "Capture Failed" - }, - "unencrypted_secret_warning": { - "message": "Ce secret n’est pas crypté ! Cliquez ici pour définir un mot de passe pour corriger cette erreur.", - "description": "Unencrypted Secret Warning" - }, - "based_on_time": { - "message": "Basé sur le temps", - "description": "Time Based" - }, - "based_on_counter": { - "message": "Basée sur un compteur", - "description": "Counter Based" - }, - "resize_popup_page": { - "message": "Redimensionner la pop-up", - "description": "Resize Popup Page" - }, - "scale": { - "message": "Mise à l'échelle", - "description": "Scale" - }, - "export_info": { - "message": "Copiez ce texte et enregistrez-le quelque part ailleurs pour sauvegarder vos secrets. Vous souhaitez ajouter un compte à une autre application ? Placez le curseur sur la partie supérieure droite de n’importe quel compte et cliquez sur le bouton caché.", - "description": "Export menu info text" - } -} diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 19b185810..eae0b6afb 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -198,5 +198,21 @@ "import_backup": { "message": "导入备份文件", "description": "Import backup file." + }, + "dropbox_backup": { + "message": "自动备份至Dropbox", + "description": "Auto backup to Dropbox." + }, + "dropbox_code": { + "message": "Dropbox授权码", + "description": "Dropbox code." + }, + "dropbox_token": { + "message": "Dropbox Token", + "description": "Dropbox token." + }, + "dropbox_authorization": { + "message": "Dropbox授权", + "description": "Dropbox authorization." } } diff --git a/css/popup.css b/css/popup.css index 60d758b9f..47bf2498e 100644 --- a/css/popup.css +++ b/css/popup.css @@ -326,6 +326,7 @@ body { #upload_backup, #security_save, #passphrase_ok, +#dropbox_ok, #message_close, #exportButton, #resize_save { @@ -369,6 +370,7 @@ body { #exportButton, #security_save, #passphrase_ok, +#dropbox_ok, #resize_save { font-size: 12px; margin: 20px 100px; @@ -416,6 +418,16 @@ body { display: none; } +#dropbox { + position: absolute; + left: 50px; + bottom: 0; + height: 38px; + line-height: 38px; + font-size: 16px; + color: #ccc; +} + #editAction { position: absolute; right: 20px; @@ -599,6 +611,7 @@ body { #passphraseClose:hover, #security_save:hover, #passphrase_ok:hover, +#dropbox_ok:hover, #export:hover, #resizeClose:hover, #resize_save:hover { @@ -652,6 +665,7 @@ body { animation: qrfadeout 0.2s 1 ease-in; } +#dropbox_box input, #secret_box input, #security input, #passphrase input { @@ -664,6 +678,11 @@ body { outline: none; } +#dropbox_box input:read-only { + background: #eee; + cursor: not-allowed; +} + .checkbox_group input[type="checkbox"], .radio_group input[type="radio"] { display: inline-block !important; @@ -681,6 +700,7 @@ body { font-size: 16px; } +#dropbox_box label, #secret_box label, #security label, #passphrase label, @@ -734,4 +754,9 @@ body { .no-copy { cursor: default; +} + +#dropbox_authorization { + text-align: right; + padding: 0 10px; } \ No newline at end of file diff --git a/manifest-chrome.json b/manifest-chrome.json index 8d25e6571..9b1847a61 100644 --- a/manifest-chrome.json +++ b/manifest-chrome.json @@ -62,12 +62,13 @@ ], "optional_permissions": [ "clipboardWrite", - "https://www.google.com/" + "https://www.google.com/", + "https://*.dropboxapi.com/*" ], "offline_enabled": true, "web_accessible_resources": [ "qr.html", "images/scan.gif" ], - "content_security_policy": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/; default-src 'none'" + "content_security_policy": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com; default-src 'none'" } \ No newline at end of file diff --git a/manifest-firefox.json b/manifest-firefox.json index 3ff5f82a5..c78958cc7 100644 --- a/manifest-firefox.json +++ b/manifest-firefox.json @@ -69,11 +69,12 @@ ], "optional_permissions": [ "clipboardWrite", - "https://www.google.com/" + "https://www.google.com/", + "https://*.dropboxapi.com/*" ], "web_accessible_resources": [ "qr.html", "images/scan.gif" ], - "content_security_policy": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/; default-src 'none'" + "content_security_policy": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com; default-src 'none'" } diff --git a/package.json b/package.json index fbcc84215..a9f168ee2 100644 --- a/package.json +++ b/package.json @@ -18,14 +18,14 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/Sneezry/Authenticator2.git" + "url": "git+https://github.com/Authenticator-Extension/Authenticator.git" }, "author": "Sneezry", "license": "MIT", "bugs": { - "url": "https://github.com/Sneezry/Authenticator2/issues" + "url": "https://github.com/Authenticator-Extension/Authenticator/issues" }, - "homepage": "https://github.com/Sneezry/Authenticator2#readme", + "homepage": "https://github.com/Authenticator-Extension/Authenticator#readme", "devDependencies": { "@types/chrome": "^0.0.59", "@types/crypto-js": "^3.1.38", diff --git a/popup.html b/popup.html index 1d801c95b..7cd612d04 100644 --- a/popup.html +++ b/popup.html @@ -15,12 +15,14 @@ +

@@ -56,6 +58,7 @@
-
+
{{ i18n.export_info }}
{{ i18n.download_backup }}
+ +
+
+ + + + + +
{{ i18n.ok }}
+
+
diff --git a/src/models/dropbox.ts b/src/models/dropbox.ts new file mode 100644 index 000000000..330430fc9 --- /dev/null +++ b/src/models/dropbox.ts @@ -0,0 +1,97 @@ +/* tslint:disable:no-reference */ +/// +/// +/// + +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 upload(encryption: Encryption) { + const exportData = await EntryStorage.getExport(encryption); + for (const hash of Object.keys(exportData)) { + if (exportData[hash].encrypted) { + throw new Error('Error passphrass.'); + } + } + const backup = JSON.stringify(exportData, null, 2); + + const url = 'https://content.dropboxapi.com/2/files/upload'; + const token = await this.getToken(); + return new Promise( + (resolve: (value: boolean) => void, + reject: (reason: Error) => void) => { + if (!token) { + resolve(false); + } + try { + const xhr = new XMLHttpRequest(); + const now = + (new Date()).toISOString().slice(0, 10).replace(/-/g, ''); + const apiArg = { + path: `/Authenticator Backup/${now}.json`, + mode: 'add', + autorename: true + }; + xhr.open('POST', url); + xhr.setRequestHeader('Authorization', 'Bearer ' + token); + xhr.setRequestHeader('Content-type', 'application/octet-stream'); + xhr.setRequestHeader('Dropbox-API-Arg', JSON.stringify(apiArg)); + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 401) { + localStorage.removeItem('dropboxToken'); + resolve(false); + } + try { + const res = JSON.parse(xhr.responseText); + if (res.name) { + resolve(true); + } else { + resolve(false); + } + } catch (error) { + reject(error); + } + } + return; + }; + xhr.send(backup); + } catch (error) { + return reject(error); + } + }); + } +} diff --git a/src/popup.ts b/src/popup.ts index 96034d193..548348b67 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -9,6 +9,7 @@ /// /// /// +/// async function init() { const ui = new UI({el: '#authenticator'}); @@ -30,15 +31,49 @@ async function init() { } // Remind backup - const clientTime = Math.floor(new Date().getTime() / 1000 / 3600 / 24); - if (!localStorage.lastRemindingBackupTime) { - localStorage.lastRemindingBackupTime = clientTime; - } else if ( - clientTime - localStorage.lastRemindingBackupTime >= 30 || - clientTime - localStorage.lastRemindingBackupTime < 0) { - authenticator.message = authenticator.i18n.remind_backup; - localStorage.lastRemindingBackupTime = clientTime; - } + const backupReminder = setInterval(() => { + for (let i = 0; i < authenticator.entries.length; i++) { + if (authenticator.entries[i].secret === 'Encrypted') { + return; + } + } + + clearInterval(backupReminder); + const clientTime = Math.floor(new Date().getTime() / 1000 / 3600 / 24); + if (!localStorage.lastRemindingBackupTime) { + localStorage.lastRemindingBackupTime = clientTime; + } else if ( + clientTime - localStorage.lastRemindingBackupTime >= 30 || + clientTime - localStorage.lastRemindingBackupTime < 0) { + // backup to Dropbox + if (authenticator.dropboxToken) { + chrome.permissions.contains( + {origins: ['https://*.dropboxapi.com/*']}, + async (hasPermission) => { + if (hasPermission) { + try { + const dropbox = new Dropbox(); + const res = await dropbox.upload(authenticator.encryption); + if (res) { + // we have uploaded backup to Dropbox + // no need to remind + localStorage.lastRemindingBackupTime = clientTime; + return; + } + } catch (error) { + // ignore + } + } + authenticator.message = authenticator.i18n.remind_backup; + localStorage.lastRemindingBackupTime = clientTime; + }); + } else { + authenticator.message = authenticator.i18n.remind_backup; + localStorage.lastRemindingBackupTime = clientTime; + } + } + return; + }, 1000); return; } diff --git a/src/ui/info.ts b/src/ui/info.ts index 3b83fd480..b5629fcb4 100644 --- a/src/ui/info.ts +++ b/src/ui/info.ts @@ -4,8 +4,19 @@ async function info(_ui: UI) { const ui: UIConfig = { - data: {info: ''}, + data: { + info: '', + dropboxCode: '', + dropboxToken: localStorage.dropboxToken || '' + }, 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[]; @@ -19,7 +30,19 @@ async function info(_ui: UI) { return; } } + } else if (tab === 'dropbox') { + chrome.permissions.request( + {origins: ['https://*.dropboxapi.com/*']}, async (granted) => { + if (granted) { + _ui.instance.class.fadein = true; + _ui.instance.class.fadeout = false; + _ui.instance.info = tab; + } + return; + }); + return; } + _ui.instance.class.fadein = true; _ui.instance.class.fadeout = false; _ui.instance.info = tab; From cdf2400e326152e651b2f9292f0e1494d39e66ba Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 01:44:28 +0800 Subject: [PATCH 071/178] fix #5 --- src/content.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/content.ts b/src/content.ts index a9c0757e2..8da4ec5bd 100644 --- a/src/content.ts +++ b/src/content.ts @@ -104,6 +104,16 @@ function grayLayoutUp(event: MouseEvent, passphrase: string) { return; } + setTimeout(() => { + captureBox.style.display = 'none'; + grayLayout.style.display = 'none'; + }, 100); + + if (event.button === 1 || event.button === 2) { + event.preventDefault(); + return; + } + let captureBoxLeft = Math.min(captureBoxPosition.left, event.clientX) + 1; let captureBoxTop = Math.min(captureBoxPosition.top, event.clientY) + 1; let captureBoxWidth = Math.abs(captureBoxPosition.left - event.clientX) - 1; @@ -113,15 +123,6 @@ function grayLayoutUp(event: MouseEvent, passphrase: string) { captureBoxWidth *= window.devicePixelRatio; captureBoxHeight *= window.devicePixelRatio; - if (event.button === 1 || event.button === 2) { - event.preventDefault(); - return; - } - - setTimeout(() => { - captureBox.style.display = 'none'; - grayLayout.style.display = 'none'; - }, 100); // make sure captureBox and grayLayout is hidden setTimeout(() => { sendPosition( From 0ff920602922d337a5506241d2e0b64a9f778622 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 01:51:41 +0800 Subject: [PATCH 072/178] include src in package for debug --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a9f168ee2..f7646ebdf 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,8 @@ "test": "echo \"Error: no test specified\" && exit 1", "check": "gts check", "clean": "gts clean", - "copyChrome": "cp -r build css images js _locales LICENSE *.html chrome", - "copyFirefox": "cp -r build css images js _locales LICENSE *.html firefox", + "copyChrome": "cp -r src build css images js _locales LICENSE *.html chrome", + "copyFirefox": "cp -r src build css images js _locales LICENSE *.html firefox", "compile": "tsc -p .", "fix": "gts fix", "prepare": "npm run compile", From 1b5575836ca70ac2c0d357d73b36e49bcc51a212 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 02:21:34 +0800 Subject: [PATCH 073/178] fix encrypted data dropbox backup issue --- src/models/storage.ts | 13 +++++++++---- src/popup.ts | 5 +++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/models/storage.ts b/src/models/storage.ts index 2796453a7..b302c1a9e 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -63,10 +63,15 @@ class EntryStorage { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(_data)) { // decrypt the data to export - _data[hash].secret = _data[hash].encrypted ? - encryption.getDecryptedSecret(_data[hash].secret) : - _data[hash].secret; - _data[hash].encrypted = false; + if (_data[hash].encrypted) { + const decryptedSecret = + encryption.getDecryptedSecret(_data[hash].secret); + if (decryptedSecret !== _data[hash].secret && + decryptedSecret !== 'Encrypted') { + _data[hash].secret = decryptedSecret; + _data[hash].encrypted = false; + } + } // we need correct hash if (hash !== _data[hash].hash) { _data[_data[hash].hash] = _data[hash]; diff --git a/src/popup.ts b/src/popup.ts index 548348b67..09e65526b 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -32,6 +32,10 @@ async function init() { // Remind backup const backupReminder = setInterval(() => { + if (authenticator.entries.length === 0) { + return; + } + for (let i = 0; i < authenticator.entries.length; i++) { if (authenticator.entries[i].secret === 'Encrypted') { return; @@ -39,6 +43,7 @@ async function init() { } clearInterval(backupReminder); + const clientTime = Math.floor(new Date().getTime() / 1000 / 3600 / 24); if (!localStorage.lastRemindingBackupTime) { localStorage.lastRemindingBackupTime = clientTime; From ebc36eb7fea8f05af2c3c1f8259eadce9573aafc Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 02:24:38 +0800 Subject: [PATCH 074/178] typo --- src/models/dropbox.ts | 2 +- src/ui/add-account.ts | 8 ++++---- src/ui/info.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/models/dropbox.ts b/src/models/dropbox.ts index 330430fc9..0d08637ae 100644 --- a/src/models/dropbox.ts +++ b/src/models/dropbox.ts @@ -43,7 +43,7 @@ class Dropbox { const exportData = await EntryStorage.getExport(encryption); for (const hash of Object.keys(exportData)) { if (exportData[hash].encrypted) { - throw new Error('Error passphrass.'); + throw new Error('Error passphrase.'); } } const backup = JSON.stringify(exportData, null, 2); diff --git a/src/ui/add-account.ts b/src/ui/add-account.ts index ebcc64ed1..95b1ad548 100644 --- a/src/ui/add-account.ts +++ b/src/ui/add-account.ts @@ -43,9 +43,9 @@ async function addAccount(_ui: UI) { const entries = _ui.instance.entries as OTPEntry[]; for (let i = 0; i < entries.length; i++) { // we have encrypted entry - // the current passphrass is incorrect + // the current passphrase is incorrect // shouldn't add new account with - // the current passphrass + // the current passphrase if (entries[i].code === 'Encrypted') { _ui.instance.message = _ui.instance.i18n.phrase_incorrect; return; @@ -73,9 +73,9 @@ async function addAccount(_ui: UI) { const entries = _ui.instance.entries as OTPEntry[]; for (let i = 0; i < entries.length; i++) { // we have encrypted entry - // the current passphrass is incorrect + // the current passphrase is incorrect // shouldn't add new account with - // the current passphrass + // the current passphrase if (entries[i].code === 'Encrypted') { _ui.instance.message = _ui.instance.i18n.phrase_incorrect; return; diff --git a/src/ui/info.ts b/src/ui/info.ts index b5629fcb4..cfd1131ca 100644 --- a/src/ui/info.ts +++ b/src/ui/info.ts @@ -22,7 +22,7 @@ async function info(_ui: UI) { const entries = _ui.instance.entries as OTPEntry[]; for (let i = 0; i < entries.length; i++) { // we have encrypted entry - // the current passphrass is incorrect + // the current passphrase is incorrect // cannot export account data // or change passphrase if (entries[i].code === 'Encrypted') { From cca9470321ed5d0dfe7ed11f68cd492f497f0c60 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 02:28:06 +0800 Subject: [PATCH 075/178] hide dropbox status icon in edit mode --- popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/popup.html b/popup.html index 7cd612d04..e73a6a7fd 100644 --- a/popup.html +++ b/popup.html @@ -22,7 +22,7 @@ From 23e50abd7657add17a5225af2c64835853cacf8f Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 03:01:36 +0800 Subject: [PATCH 076/178] update i18n --- _locales/en/messages.json | 4 ++-- _locales/zh_CN/messages.json | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 3c1bf0207..d7013bd67 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -104,7 +104,7 @@ "description": "Confirmm Passphrase." }, "confirm_delete" : { - "message": "Are you sure you want to delete this code? This action cannot be undone.", + "message": "Are you sure you want to delete this item? This action cannot be undone.", "description": "Remove entry confirmation" }, "security_warning": { @@ -160,7 +160,7 @@ "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "Do you have a backup for your secrets? Don't wait until it's too late!", + "message": "NEVER REINSTALL THE EXTENSION TO TRY TO FIX ANY ISSUE, OR YOU WILL LOSE ALL YOUR DATA! Do you have a backup for your secrets? Don't wait until it's too late!", "description": "Remind Backup" }, "capture_failed": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index eae0b6afb..178872c87 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -8,7 +8,7 @@ "description": "Extension Short Name." }, "extDesc": { - "message": "适用于Google身份验证器及战网安全令。", + "message": "身份验证器用以在浏览器中生成二步认证代码。", "description": "Extension Description." }, "added": { @@ -44,11 +44,11 @@ "description": "OK." }, "yes": { - "message": "Yes", + "message": "是", "description": "Yes." }, "no": { - "message": "No", + "message": "否", "description": "No." }, "err_acc_sec": { @@ -188,7 +188,7 @@ "description": "Scale" }, "export_info": { - "message": "将此文本保存至安全的地方来备份你的密钥。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", + "message": "警告:所有备份均未加密。想要将账号添加至其他应用?请点击账号右上角隐藏的图标。", "description": "Export menu info text" }, "download_backup": { From 16ea49d14e11616658c6b2100e75ddf11f0c5be9 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 03:11:32 +0800 Subject: [PATCH 077/178] add hover style for scan icon --- css/popup.css | 1 + 1 file changed, 1 insertion(+) diff --git a/css/popup.css b/css/popup.css index 47bf2498e..fb6edd952 100644 --- a/css/popup.css +++ b/css/popup.css @@ -604,6 +604,7 @@ body { #download_backup:hover, #editAction:hover, #infoAction:hover, +#scan:hover, #codes #add:hover, #infoClose:hover, #addAccountClose:hover, From 840da2528cbbcacc1381ffa78aa2b54326f4d621 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Mon, 19 Feb 2018 12:43:33 +0800 Subject: [PATCH 078/178] fix #6 --- src/ui/entry.ts | 9 +++++++-- src/ui/passphrase.ts | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 10cfd4908..c15b44c9f 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -60,8 +60,13 @@ function getBackupFile(entryData: {[hash: string]: OTPStorage}) { } async function entry(_ui: UI) { - const encryption: Encryption = new Encryption(''); - const shouldShowPassphrase = await EntryStorage.hasEncryptedEntry(); + const cookie = document.cookie; + const cookieMatch = cookie ? document.cookie.split('passphrase=') : null; + const cachedPassphrase = + cookieMatch && cookieMatch.length > 1 ? cookieMatch[1] : null; + const encryption: Encryption = new Encryption(cachedPassphrase || ''); + const shouldShowPassphrase = + cachedPassphrase ? false : await EntryStorage.hasEncryptedEntry(); const exportData = shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); const entries = shouldShowPassphrase ? [] : await getEntries(encryption); diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts index 19189169a..b35cf8325 100644 --- a/src/ui/passphrase.ts +++ b/src/ui/passphrase.ts @@ -11,6 +11,7 @@ async function passphrase(_ui: UI) { _ui.instance.passphrase); await _ui.instance.updateEntries(); _ui.instance.closeInfo(); + document.cookie = 'passphrase=' + _ui.instance.passphrase; return; }, changePassphrase: async () => { From df5e02c6a9b0cf19be6e1977baf74857b3a5e6ea Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Mon, 19 Feb 2018 10:29:22 -0600 Subject: [PATCH 079/178] add checks --- src/models/storage.ts | 161 ++++++++++++++++++++++-------------------- 1 file changed, 84 insertions(+), 77 deletions(-) diff --git a/src/models/storage.ts b/src/models/storage.ts index 28c21c7cb..dac54aa7d 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -39,11 +39,13 @@ class EntryStorage { return newData; } - private static ensureObject(_data: {[hash: string]: OTPStorage}) { - for (const hash of Object.keys(_data)) { - if (typeof _data[hash] !== 'object') { - // Drop invalid data? - } + private static ensureObject( + _data: {[hash: string]: OTPStorage}, hash: string) { + if (typeof _data[hash] !== 'object') { + console.log('Key ' + hash + 'is not an object'); + return false; + } else { + return true; } } @@ -53,8 +55,10 @@ class EntryStorage { reject: (reason: Error) => void) => { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(_data)) { - if (_data[hash].encrypted) { - return resolve(true); + if (this.ensureObject(_data, hash)) { + if (_data[hash].encrypted) { + return resolve(true); + } } } return resolve(false); @@ -69,17 +73,18 @@ class EntryStorage { reject: (reason: Error) => void) => { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { - this.ensureObject(_data); for (const hash of Object.keys(_data)) { - // decrypt the data to export - _data[hash].secret = _data[hash].encrypted ? - encryption.getDecryptedSecret(_data[hash].secret) : - _data[hash].secret; - _data[hash].encrypted = false; - // we need correct hash - if (hash !== _data[hash].hash) { - _data[_data[hash].hash] = _data[hash]; - delete _data[hash]; + if (this.ensureObject(_data, hash)) { + // decrypt the data to export + _data[hash].secret = _data[hash].encrypted ? + encryption.getDecryptedSecret(_data[hash].secret) : + _data[hash].secret; + _data[hash].encrypted = false; + // we need correct hash + if (hash !== _data[hash].hash) { + _data[_data[hash].hash] = _data[hash]; + delete _data[hash]; + } } } return resolve(_data); @@ -202,77 +207,79 @@ class EntryStorage { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { const data: OTPEntry[] = []; for (let hash of Object.keys(_data)) { - const entryData = _data[hash]; - let needMigrate = false; - - if (!entryData.type) { - entryData.type = OTPType[OTPType.totp]; - needMigrate = true; - } + if (this.ensureObject(_data, hash)) { + const entryData = _data[hash]; + let needMigrate = false; - let type: OTPType; - switch (entryData.type) { - case 'totp': - case 'hotp': - case 'battle': - case 'steam': - type = OTPType[entryData.type]; - break; - default: - // we need correct the type here - // and save it - type = OTPType.totp; + if (!entryData.type) { entryData.type = OTPType[OTPType.totp]; needMigrate = true; - } - entryData.secret = entryData.encrypted ? - encryption.getDecryptedSecret(entryData.secret) : - entryData.secret; + } - const entry = new OTPEntry( - type, entryData.issuer, entryData.secret, entryData.account, - entryData.index, entryData.counter); - data.push(entry); + let type: OTPType; + switch (entryData.type) { + case 'totp': + case 'hotp': + case 'battle': + case 'steam': + type = OTPType[entryData.type]; + break; + default: + // we need correct the type here + // and save it + type = OTPType.totp; + entryData.type = OTPType[OTPType.totp]; + needMigrate = true; + } + entryData.secret = entryData.encrypted ? + encryption.getDecryptedSecret(entryData.secret) : + entryData.secret; - // we need migrate secret in old format here - if (/^(blz\-|bliz\-)/.test(entryData.secret)) { - const secretMatches = - entryData.secret.match(/^(blz\-|bliz\-)(.*)/); - if (secretMatches && secretMatches.length >= 3) { - entryData.secret = entryData.encrypted ? - secretMatches[2] : - encryption.getEncryptedSecret(entry.secret); - entryData.type = OTPType[OTPType.battle]; - needMigrate = true; + const entry = new OTPEntry( + type, entryData.issuer, entryData.secret, + entryData.account, entryData.index, entryData.counter); + data.push(entry); + + // we need migrate secret in old format here + if (/^(blz\-|bliz\-)/.test(entryData.secret)) { + const secretMatches = + entryData.secret.match(/^(blz\-|bliz\-)(.*)/); + if (secretMatches && secretMatches.length >= 3) { + entryData.secret = entryData.encrypted ? + secretMatches[2] : + encryption.getEncryptedSecret(entry.secret); + entryData.type = OTPType[OTPType.battle]; + needMigrate = true; + } } - } - if (/^stm\-/.test(entryData.secret)) { - const secretMatches = entryData.secret.match(/^stm\-(.*)/); - if (secretMatches && secretMatches.length >= 2) { - entryData.secret = entryData.encrypted ? - secretMatches[2] : - encryption.getEncryptedSecret(entry.secret); - entryData.type = OTPType[OTPType.steam]; - needMigrate = true; + if (/^stm\-/.test(entryData.secret)) { + const secretMatches = entryData.secret.match(/^stm\-(.*)/); + if (secretMatches && secretMatches.length >= 2) { + entryData.secret = entryData.encrypted ? + secretMatches[2] : + encryption.getEncryptedSecret(entry.secret); + entryData.type = OTPType[OTPType.steam]; + needMigrate = true; + } } - } - // we need correct the hash - if (entry.secret !== 'Encrypted') { - const _hash = CryptoJS.MD5(entry.secret).toString(); - if (hash !== _hash) { - chrome.storage.sync.remove(hash); - hash = _hash; - entryData.hash = hash; - needMigrate = true; + // we need correct the hash + if (entry.secret !== 'Encrypted') { + const _hash = CryptoJS.MD5(entry.secret).toString(); + if (hash !== _hash) { + chrome.storage.sync.remove(hash); + hash = _hash; + entryData.hash = hash; + needMigrate = true; + } } - } - if (needMigrate) { - const _entry: {[hash: string]: OTPStorage} = {}; - _entry[hash] = entryData; - this.import(encryption, _entry); + if (needMigrate) { + const _entry: {[hash: string]: OTPStorage} = {}; + _entry[hash] = entryData; + this.import(encryption, _entry); + } } } From b785b2074738d6c5ab6f99a75dea72f9a652fddf Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Mon, 19 Feb 2018 10:31:55 -0600 Subject: [PATCH 080/178] fix console message --- src/models/storage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/storage.ts b/src/models/storage.ts index dac54aa7d..d2f84ee4f 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -42,7 +42,7 @@ class EntryStorage { private static ensureObject( _data: {[hash: string]: OTPStorage}, hash: string) { if (typeof _data[hash] !== 'object') { - console.log('Key ' + hash + 'is not an object'); + console.log('Key "' + hash + '" is not an object'); return false; } else { return true; From 8ab1624ca649039f0f2f2478a7eceb3dd88e4c0b Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Mon, 19 Feb 2018 10:57:52 -0600 Subject: [PATCH 081/178] add crowdin config --- crowdin.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 crowdin.yml diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..987c85f93 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,8 @@ +files: + - source: /_locales/en/messages.json + translation: /_locales/%two_letters_code%/messages.json + languages_mapping: + two_letters_code: + zh-CN: zh_CN + zh-TW: zh_TW + pt-BR: pt_BR From 1a578319b268dec4caaf04c5fe1dfc6edfdf6c56 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 01:33:53 +0800 Subject: [PATCH 082/178] fix #7 --- _locales/en/messages.json | 4 ++++ _locales/zh_CN/messages.json | 4 ++++ css/popup.css | 23 +++++++++++++++++++++++ popup.html | 5 +++-- src/ui/entry.ts | 33 ++++++++++++++++++++++++++++++++- 5 files changed, 66 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d7013bd67..3e68f86e2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -214,5 +214,9 @@ "dropbox_authorization": { "message": "Dropbox Authorization", "description": "Dropbox authorization." + }, + "show_all_entries": { + "message": "Show all entries", + "description": "Show all entries." } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 178872c87..760f60951 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -214,5 +214,9 @@ "dropbox_authorization": { "message": "Dropbox授权", "description": "Dropbox authorization." + }, + "show_all_entries": { + "message": "显示全部条目", + "description": "Show all entries." } } diff --git a/css/popup.css b/css/popup.css index fb6edd952..088e08a61 100644 --- a/css/popup.css +++ b/css/popup.css @@ -204,6 +204,29 @@ body { transition: height 0.2s; } +#codes.filter .entry[filtered] { + display: none; +} + +#filter { + background: #fff4cc; + padding: 0 10px; + margin-left: 10px; + font-size: 12px; + height: 24px; + line-height: 24px; + cursor: pointer; + display: none; +} + +#filter:hover { + background: #fff1ba; +} + +#codes.filter #filter { + display: block; +} + #codes:not(.edit) .entry[unencrypted="true"]:hover .warning { height: 24px; } diff --git a/popup.html b/popup.html index e73a6a7fd..8ad300c5b 100644 --- a/popup.html +++ b/popup.html @@ -26,9 +26,10 @@
-
+
+
{{ i18n.show_all_entries }}
-
+
diff --git a/src/ui/entry.ts b/src/ui/entry.ts index c15b44c9f..6e278fd1a 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -59,6 +59,32 @@ function getBackupFile(entryData: {[hash: string]: OTPStorage}) { return `data:application/octet-stream;base64,${base64Data}`; } +async function getCurrentHostname() { + return new Promise( + (resolve: (value: string|null) => void, + reject: (reason: Error) => void) => { + chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { + const tab = tabs[0]; + if (!tab || !tab.url) { + return resolve(null); + } + const urlParser = document.createElement('a'); + urlParser.href = tab.url; + const hostname = urlParser.hostname; + return resolve(hostname); + }); + }); +} + +function hasMatchedEntry(currentHost: string, entries: OTPEntry[]) { + for (let i = 0; i < entries.length; i++) { + if (entries[i].issuer.indexOf(currentHost) !== -1) { + return true; + } + } + return false; +} + async function entry(_ui: UI) { const cookie = document.cookie; const cookieMatch = cookie ? document.cookie.split('passphrase=') : null; @@ -71,6 +97,8 @@ async function entry(_ui: UI) { shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); const entries = shouldShowPassphrase ? [] : await getEntries(encryption); const exportFile = getBackupFile(exportData); + const currentHost = await getCurrentHostname(); + const shouldFilter = currentHost ? hasMatchedEntry(currentHost, entries) : false; const ui: UIConfig = { data: { @@ -82,7 +110,10 @@ async function entry(_ui: UI) { exportFile, sector: '', notification: '', - notificationTimeout: 0 + notificationTimeout: 0, + filter: true, + currentHost, + shouldFilter }, methods: { updateCode: async () => { From b988dca43e42d83e542a5dbdd980d1266a81d638 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 02:11:38 +0800 Subject: [PATCH 083/178] midify i18n message --- _locales/en/messages.json | 4 ++-- _locales/zh_CN/messages.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 3e68f86e2..ade9b099c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -104,7 +104,7 @@ "description": "Confirmm Passphrase." }, "confirm_delete" : { - "message": "Are you sure you want to delete this item? This action cannot be undone.", + "message": "Are you sure you want to delete this secret? This action cannot be undone.", "description": "Remove entry confirmation" }, "security_warning": { @@ -160,7 +160,7 @@ "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "NEVER REINSTALL THE EXTENSION TO TRY TO FIX ANY ISSUE, OR YOU WILL LOSE ALL YOUR DATA! Do you have a backup for your secrets? Don't wait until it's too late!", + "message": "Do you have a backup for your secrets? Don't wait until it's too late!", "description": "Remind Backup" }, "capture_failed": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 760f60951..ea8316e19 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -104,7 +104,7 @@ "description": "Confirmm Phrase." }, "confirm_delete" : { - "message": "您确定要删除此项吗?您无法找回已删除的密钥。此操作无法撤销。", + "message": "您确定要删除此密钥吗?您无法找回已删除的密钥。此操作无法撤销。", "description": "Remove entry confirmation" }, "security_warning": { @@ -160,7 +160,7 @@ "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "永远不要通过重装扩展来尝试解决问题,否则您将丢失全部数据!您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", + "message": "您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", "description": "Remind Backup" }, "capture_failed": { From b8a216f64d76c799cf9643a0fcca46d5d073df95 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 03:09:49 +0800 Subject: [PATCH 084/178] fix 12 --- src/background.ts | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/background.ts b/src/background.ts index a339a1e92..b31a2c86f 100644 --- a/src/background.ts +++ b/src/background.ts @@ -115,4 +115,23 @@ async function getTotp(text: string, passphrase: string) { } } return; -} \ No newline at end of file +} + +// Show issue page after first install +chrome.runtime.onInstalled.addListener((details) => { + if (details.reason !== 'install') { + return; + } + + let url: string|null = null; + + if (navigator.userAgent.indexOf('Chrome') !== -1) { + url = 'https://github.com/Authenticator-Extension/Authenticator/wiki/Chrome-Issues' + } else if (navigator.userAgent.indexOf('Firefox') !== -1) { + url = 'https://github.com/Authenticator-Extension/Authenticator/wiki/Firefox-Issues'; + } + + if (url) { + window.open(url, '_blank'); + } +}); \ No newline at end of file From 9fede279808027be8e36041521907bf9456d9d70 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 03:11:20 +0800 Subject: [PATCH 085/178] gts fix --- src/background.ts | 6 ++++-- src/ui/entry.ts | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/background.ts b/src/background.ts index b31a2c86f..d07094973 100644 --- a/src/background.ts +++ b/src/background.ts @@ -126,9 +126,11 @@ chrome.runtime.onInstalled.addListener((details) => { let url: string|null = null; if (navigator.userAgent.indexOf('Chrome') !== -1) { - url = 'https://github.com/Authenticator-Extension/Authenticator/wiki/Chrome-Issues' + url = + 'https://github.com/Authenticator-Extension/Authenticator/wiki/Chrome-Issues'; } else if (navigator.userAgent.indexOf('Firefox') !== -1) { - url = 'https://github.com/Authenticator-Extension/Authenticator/wiki/Firefox-Issues'; + url = + 'https://github.com/Authenticator-Extension/Authenticator/wiki/Firefox-Issues'; } if (url) { diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 6e278fd1a..cf641235f 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -98,7 +98,8 @@ async function entry(_ui: UI) { const entries = shouldShowPassphrase ? [] : await getEntries(encryption); const exportFile = getBackupFile(exportData); const currentHost = await getCurrentHostname(); - const shouldFilter = currentHost ? hasMatchedEntry(currentHost, entries) : false; + const shouldFilter = + currentHost ? hasMatchedEntry(currentHost, entries) : false; const ui: UIConfig = { data: { From 894a52337268f367ccf58b609447b30bec37eaae Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Mon, 19 Feb 2018 12:59:11 -0600 Subject: [PATCH 086/178] update according to review --- src/models/storage.ts | 186 ++++++++++++++++++++++++------------------ 1 file changed, 108 insertions(+), 78 deletions(-) diff --git a/src/models/storage.ts b/src/models/storage.ts index d2f84ee4f..6b5741182 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -23,6 +23,9 @@ class EntryStorage { const tempEntryArray: OTPStorage[] = []; for (const hash of Object.keys(_data)) { + if (!this.isValidEntry(_data, hash)) { + continue; + } tempEntryArray.push(_data[hash]); } @@ -39,12 +42,33 @@ class EntryStorage { return newData; } - private static ensureObject( + private static isOTPStorage(entry: object) { + const properties = [ + 'account', 'hash', 'index', 'issuer', 'type', 'counter', 'secret', + 'encrypted' + ]; + for (let i = 0; i < properties.length; i++) { + if (!entry.hasOwnProperty(properties[i])) { + return false; + } + } + return true; + } + + private static isValidEntry( _data: {[hash: string]: OTPStorage}, hash: string) { + if (!_data.hasOwnProperty(hash)) { + console.log('Key "' + hash + '" does not exist'); + return false; + } if (typeof _data[hash] !== 'object') { console.log('Key "' + hash + '" is not an object'); return false; } else { + if (!this.isOTPStorage(_data[hash])) { + console.log('Key "' + hash + '" is not OTPStorage'); + return false; + } return true; } } @@ -55,10 +79,11 @@ class EntryStorage { reject: (reason: Error) => void) => { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(_data)) { - if (this.ensureObject(_data, hash)) { - if (_data[hash].encrypted) { - return resolve(true); - } + if (!this.isValidEntry(_data, hash)) { + continue; + } + if (_data[hash].encrypted) { + return resolve(true); } } return resolve(false); @@ -74,17 +99,18 @@ class EntryStorage { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(_data)) { - if (this.ensureObject(_data, hash)) { - // decrypt the data to export - _data[hash].secret = _data[hash].encrypted ? - encryption.getDecryptedSecret(_data[hash].secret) : - _data[hash].secret; - _data[hash].encrypted = false; - // we need correct hash - if (hash !== _data[hash].hash) { - _data[_data[hash].hash] = _data[hash]; - delete _data[hash]; - } + if (!this.isValidEntry(_data, hash)) { + continue; + } + // decrypt the data to export + _data[hash].secret = _data[hash].encrypted ? + encryption.getDecryptedSecret(_data[hash].secret) : + _data[hash].secret; + _data[hash].encrypted = false; + // we need correct hash + if (hash !== _data[hash].hash) { + _data[_data[hash].hash] = _data[hash]; + delete _data[hash]; } } return resolve(_data); @@ -103,6 +129,9 @@ class EntryStorage { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(data)) { + if (!this.isValidEntry(_data, hash)) { + continue; + } // never trust data import from user // we do not support encrypted data import any longer if (!data[hash].secret || data[hash].encrypted) { @@ -207,79 +236,80 @@ class EntryStorage { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { const data: OTPEntry[] = []; for (let hash of Object.keys(_data)) { - if (this.ensureObject(_data, hash)) { - const entryData = _data[hash]; - let needMigrate = false; + if (!this.isValidEntry(_data, hash)) { + continue; + } + const entryData = _data[hash]; + let needMigrate = false; + + if (!entryData.type) { + entryData.type = OTPType[OTPType.totp]; + needMigrate = true; + } - if (!entryData.type) { + let type: OTPType; + switch (entryData.type) { + case 'totp': + case 'hotp': + case 'battle': + case 'steam': + type = OTPType[entryData.type]; + break; + default: + // we need correct the type here + // and save it + type = OTPType.totp; entryData.type = OTPType[OTPType.totp]; needMigrate = true; - } - - let type: OTPType; - switch (entryData.type) { - case 'totp': - case 'hotp': - case 'battle': - case 'steam': - type = OTPType[entryData.type]; - break; - default: - // we need correct the type here - // and save it - type = OTPType.totp; - entryData.type = OTPType[OTPType.totp]; - needMigrate = true; - } - entryData.secret = entryData.encrypted ? - encryption.getDecryptedSecret(entryData.secret) : - entryData.secret; + } + entryData.secret = entryData.encrypted ? + encryption.getDecryptedSecret(entryData.secret) : + entryData.secret; - const entry = new OTPEntry( - type, entryData.issuer, entryData.secret, - entryData.account, entryData.index, entryData.counter); - data.push(entry); + const entry = new OTPEntry( + type, entryData.issuer, entryData.secret, entryData.account, + entryData.index, entryData.counter); + data.push(entry); - // we need migrate secret in old format here - if (/^(blz\-|bliz\-)/.test(entryData.secret)) { - const secretMatches = - entryData.secret.match(/^(blz\-|bliz\-)(.*)/); - if (secretMatches && secretMatches.length >= 3) { - entryData.secret = entryData.encrypted ? - secretMatches[2] : - encryption.getEncryptedSecret(entry.secret); - entryData.type = OTPType[OTPType.battle]; - needMigrate = true; - } + // we need migrate secret in old format here + if (/^(blz\-|bliz\-)/.test(entryData.secret)) { + const secretMatches = + entryData.secret.match(/^(blz\-|bliz\-)(.*)/); + if (secretMatches && secretMatches.length >= 3) { + entryData.secret = entryData.encrypted ? + secretMatches[2] : + encryption.getEncryptedSecret(entry.secret); + entryData.type = OTPType[OTPType.battle]; + needMigrate = true; } + } - if (/^stm\-/.test(entryData.secret)) { - const secretMatches = entryData.secret.match(/^stm\-(.*)/); - if (secretMatches && secretMatches.length >= 2) { - entryData.secret = entryData.encrypted ? - secretMatches[2] : - encryption.getEncryptedSecret(entry.secret); - entryData.type = OTPType[OTPType.steam]; - needMigrate = true; - } + if (/^stm\-/.test(entryData.secret)) { + const secretMatches = entryData.secret.match(/^stm\-(.*)/); + if (secretMatches && secretMatches.length >= 2) { + entryData.secret = entryData.encrypted ? + secretMatches[2] : + encryption.getEncryptedSecret(entry.secret); + entryData.type = OTPType[OTPType.steam]; + needMigrate = true; } + } - // we need correct the hash - if (entry.secret !== 'Encrypted') { - const _hash = CryptoJS.MD5(entry.secret).toString(); - if (hash !== _hash) { - chrome.storage.sync.remove(hash); - hash = _hash; - entryData.hash = hash; - needMigrate = true; - } + // we need correct the hash + if (entry.secret !== 'Encrypted') { + const _hash = CryptoJS.MD5(entry.secret).toString(); + if (hash !== _hash) { + chrome.storage.sync.remove(hash); + hash = _hash; + entryData.hash = hash; + needMigrate = true; } + } - if (needMigrate) { - const _entry: {[hash: string]: OTPStorage} = {}; - _entry[hash] = entryData; - this.import(encryption, _entry); - } + if (needMigrate) { + const _entry: {[hash: string]: OTPStorage} = {}; + _entry[hash] = entryData; + this.import(encryption, _entry); } } From 8bf94f0110acac04bc6f81c8cfb6b78c987a3cb3 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Mon, 19 Feb 2018 13:17:05 -0600 Subject: [PATCH 087/178] Remove if exists check --- src/models/storage.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/models/storage.ts b/src/models/storage.ts index 6b5741182..608fc211e 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -57,10 +57,6 @@ class EntryStorage { private static isValidEntry( _data: {[hash: string]: OTPStorage}, hash: string) { - if (!_data.hasOwnProperty(hash)) { - console.log('Key "' + hash + '" does not exist'); - return false; - } if (typeof _data[hash] !== 'object') { console.log('Key "' + hash + '" is not an object'); return false; From 0a22d59575046b64318f3f52a0b6ac4cee6ad7c0 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Mon, 19 Feb 2018 14:01:49 -0600 Subject: [PATCH 088/178] Open version specific help page --- popup.html | 2 +- src/ui/menu.ts | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/popup.html b/popup.html index 8ad300c5b..9b280190b 100644 --- a/popup.html +++ b/popup.html @@ -65,7 +65,7 @@
diff --git a/src/ui/menu.ts b/src/ui/menu.ts index 6ab101d41..87047d88a 100644 --- a/src/ui/menu.ts +++ b/src/ui/menu.ts @@ -51,6 +51,22 @@ function resize(zoom: number) { } } +function openHelp() { + console.log('help'); + let url = + 'https://github.com/Authenticator-Extension/Authenticator/wiki/Chrome-Issues'; + + if (navigator.userAgent.indexOf('Chrome') !== -1) { + url = + 'https://github.com/Authenticator-Extension/Authenticator/wiki/Chrome-Issues'; + } else if (navigator.userAgent.indexOf('Firefox') !== -1) { + url = + 'https://github.com/Authenticator-Extension/Authenticator/wiki/Firefox-Issues'; + } + + window.open(url, '_blank'); +} + async function menu(_ui: UI) { const version = getVersion(); const zoom = Number(localStorage.zoom) || 100; @@ -72,6 +88,10 @@ async function menu(_ui: UI) { }, 200); return; }, + openHelp: () => { + openHelp(); + return; + }, saveZoom: () => { localStorage.zoom = _ui.instance.zoom; resize(_ui.instance.zoom); @@ -92,4 +112,4 @@ async function menu(_ui: UI) { }; _ui.update(ui); -} \ No newline at end of file +} From 773640705985174677decb537fcee9b7ee3179c1 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Mon, 19 Feb 2018 18:32:52 -0600 Subject: [PATCH 089/178] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7eab98f90..3c1bf2286 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Authenticator +# Authenticator [![Crowdin](https://d322cqt584bo4o.cloudfront.net/authenticator-firefox/localized.svg)](https://crowdin.com/project/authenticator-firefox) -> For Google Authenticator and Battle.net Authenticator. +> Authenticator generates 2-Step Verification codes in your browser. ## Build Setup From e73a35aa6ce14a1fe5f00c94df890a7d8094934c Mon Sep 17 00:00:00 2001 From: Brendan Date: Mon, 19 Feb 2018 19:47:25 -0600 Subject: [PATCH 090/178] Create ISSUE_TEMPLATE.md --- ISSUE_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 ISSUE_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 000000000..62dd9c458 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,3 @@ + + + From 35b999856a52cdcaeebec1f5475744d319db47a3 Mon Sep 17 00:00:00 2001 From: Brendan Date: Mon, 19 Feb 2018 20:00:38 -0600 Subject: [PATCH 091/178] Update ISSUE_TEMPLATE.md --- ISSUE_TEMPLATE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 62dd9c458..3def4e572 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,3 +1,3 @@ - - + + From c5b1256cffd92720ec25a3de13982bf9bbfa7e71 Mon Sep 17 00:00:00 2001 From: Brendan Date: Mon, 19 Feb 2018 20:00:59 -0600 Subject: [PATCH 092/178] Update ISSUE_TEMPLATE.md --- ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 3def4e572..dd6efb364 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,3 +1,3 @@ - + From 9298d38282a024dcffa335aae1b3ff9c204ea88b Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 22:33:10 +0800 Subject: [PATCH 093/178] disable dropbox token input --- css/popup.css | 2 +- popup.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/css/popup.css b/css/popup.css index 088e08a61..38891736d 100644 --- a/css/popup.css +++ b/css/popup.css @@ -702,7 +702,7 @@ body { outline: none; } -#dropbox_box input:read-only { +#dropbox_box input:disabled { background: #eee; cursor: not-allowed; } diff --git a/popup.html b/popup.html index 9b280190b..0c091bcca 100644 --- a/popup.html +++ b/popup.html @@ -127,7 +127,7 @@ - +
{{ i18n.ok }}
From 849d0ad3db280782e8ca22c853fc4610ea7375ce Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 09:01:14 -0600 Subject: [PATCH 094/178] bugfix --- src/models/storage.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/models/storage.ts b/src/models/storage.ts index b7bcc9b5a..fc77203c3 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -130,9 +130,6 @@ class EntryStorage { try { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(data)) { - if (!this.isValidEntry(_data, hash)) { - continue; - } // never trust data import from user // we do not support encrypted data import any longer if (!data[hash].secret || data[hash].encrypted) { From 641564c307805b19dd2c315cbe20889f6a031d52 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 23:06:22 +0800 Subject: [PATCH 095/178] make build task run correct in Windows --- ensureDir.js | 18 ++++++++++++++++++ package-lock.json | 26 ++++++++++++++++++++++++++ package.json | 7 ++++--- 3 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 ensureDir.js diff --git a/ensureDir.js b/ensureDir.js new file mode 100644 index 000000000..1fc436de4 --- /dev/null +++ b/ensureDir.js @@ -0,0 +1,18 @@ +const fs = require('fs-extra'); +const path = require('path'); +const argv = process.argv; +if (argv.length < 2) { + console.log('No path specific.'); + process.exit(); +} + +const pathName = argv[2]; + +try { + const absolutePath = path.join(__dirname, pathName); + fs.emptyDirSync(absolutePath); + process.exit(); +} catch(error) { + console.error(error); + process.exit(); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f4ddf795d..12f2f1b6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -442,6 +442,17 @@ "locate-path": "2.0.0" } }, + "fs-extra": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-5.0.0.tgz", + "integrity": "sha512-66Pm4RYbjzdyeuqudYqhFiNBbCIuI9kgRqLPSHIlXHidW8NIQtVdkM1yeZ4lXwuhbTETv3EUGMNHAAw6hiundQ==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "http://registry.npm.taobao.org/fs.realpath/download/fs.realpath-1.0.0.tgz", @@ -716,6 +727,15 @@ "integrity": "sha1-UBg80bLSUnXeBp6ecbRnrJ6rlzo=", "dev": true }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + } + }, "latest-version": { "version": "3.1.0", "resolved": "http://registry.npm.taobao.org/latest-version/download/latest-version-3.1.0.tgz", @@ -1313,6 +1333,12 @@ "crypto-random-string": "1.0.0" } }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, "unzip-response": { "version": "2.0.1", "resolved": "http://registry.npm.taobao.org/unzip-response/download/unzip-response-2.0.1.tgz", diff --git a/package.json b/package.json index f7646ebdf..187da26b5 100644 --- a/package.json +++ b/package.json @@ -8,13 +8,13 @@ "clean": "gts clean", "copyChrome": "cp -r src build css images js _locales LICENSE *.html chrome", "copyFirefox": "cp -r src build css images js _locales LICENSE *.html firefox", - "compile": "tsc -p .", + "compile": "gts clean && tsc -p .", "fix": "gts fix", "prepare": "npm run compile", "pretest": "npm run compile", "posttest": "npm run check", - "chrome": "mkdir -p chrome && npm run compile && npm run copyChrome && cp manifest-chrome.json chrome/manifest.json", - "firefox": "mkdir -p firefox && npm run compile && npm run copyFirefox && cp manifest-firefox.json firefox/manifest.json" + "chrome": "node ensureDir.js chrome && npm run compile && npm run copyChrome && cp manifest-chrome.json chrome/manifest.json", + "firefox": "node ensureDir.js && npm run compile && npm run copyFirefox && cp manifest-firefox.json firefox/manifest.json" }, "repository": { "type": "git", @@ -30,6 +30,7 @@ "@types/chrome": "^0.0.59", "@types/crypto-js": "^3.1.38", "@types/jssha": "0.0.29", + "fs-extra": "^5.0.0", "gts": "^0.5.2", "typescript": "^2.6.1" } From 2bac438c3d8ff12ee0a00b0d91acc15e3a6bfab8 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 23:19:04 +0800 Subject: [PATCH 096/178] fix #17 --- popup.html | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/popup.html b/popup.html index 0c091bcca..a5b13fa53 100644 --- a/popup.html +++ b/popup.html @@ -26,24 +26,26 @@
-
+
{{ i18n.show_all_entries }}
- -
-
-
-
-
{{ entry.issuer.split('::')[0] }}
-
- -
-
- -
- +
+ +
+
+
+
+
{{ entry.issuer.split('::')[0] }}
+
+ +
+
+ +
+ +
+
+
-
-
From 7be05382404a0cf8e70b91fbf5b56a99351e5012 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 23:27:57 +0800 Subject: [PATCH 097/178] update info --- _locales/en/messages.json | 2 +- _locales/zh_CN/messages.json | 2 +- package.json | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ade9b099c..bb3802071 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", + "message": "

Authenticator for Google™ Authenticator,
© 2018 Authenticator Extension. Released under the MIT License.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", "description": "Information." }, "add_qr": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index ea8316e19..4dbfb9d24 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Sneezry. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", + "message": "

Authenticator for Google™ Authenticator,
© 2018 Authenticator Extension. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", "description": "Information." }, "add_qr": { diff --git a/package.json b/package.json index 187da26b5..617774147 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "authenticator", + "name": "authenticator-extension", "version": "0.1.0", - "description": "For Google Authenticator and Battle.net Authenticator.", + "description": "Authenticator generates 2-Step Verification codes in your browser.", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "check": "gts check", @@ -20,7 +20,7 @@ "type": "git", "url": "git+https://github.com/Authenticator-Extension/Authenticator.git" }, - "author": "Sneezry", + "author": "Authenticator Extension", "license": "MIT", "bugs": { "url": "https://github.com/Authenticator-Extension/Authenticator/issues" From 21f4a0ce47c3f0d274bf21f3b63a24cd5a267fd8 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 20 Feb 2018 23:30:58 +0800 Subject: [PATCH 098/178] fix firefox build task bug --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 617774147..5ef5fad02 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "pretest": "npm run compile", "posttest": "npm run check", "chrome": "node ensureDir.js chrome && npm run compile && npm run copyChrome && cp manifest-chrome.json chrome/manifest.json", - "firefox": "node ensureDir.js && npm run compile && npm run copyFirefox && cp manifest-firefox.json firefox/manifest.json" + "firefox": "node ensureDir.js firefox && npm run compile && npm run copyFirefox && cp manifest-firefox.json firefox/manifest.json" }, "repository": { "type": "git", From b72658634bf53cb9f5f6ef6767995874198a913f Mon Sep 17 00:00:00 2001 From: Brendan Date: Tue, 20 Feb 2018 09:52:11 -0600 Subject: [PATCH 099/178] Add .travis.yml (#18) * Add .travis.yml * Add chrome build test * Update README.md --- .travis.yml | 12 ++++++++++++ README.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..028ecc7c9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: node_js +node_js: + - "node" +before_script: + - "npm install -g typescript" + - "npm install -g gts" + - "npm install -g addons-linter" +script: + - "gts check" + - "npm run firefox" + - "npm run chrome" + - "addons-linter firefox" diff --git a/README.md b/README.md index 3c1bf2286..81b49c22b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Authenticator [![Crowdin](https://d322cqt584bo4o.cloudfront.net/authenticator-firefox/localized.svg)](https://crowdin.com/project/authenticator-firefox) +# Authenticator [![Build Status](https://travis-ci.org/Authenticator-Extension/Authenticator.svg?branch=dev)](https://travis-ci.org/Authenticator-Extension/Authenticator) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/authenticator-firefox/localized.svg)](https://crowdin.com/project/authenticator-firefox) > Authenticator generates 2-Step Verification codes in your browser. From ab2cfd2bcc8589e49d00817f4fab298bc92d417e Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 10:15:25 -0600 Subject: [PATCH 100/178] Add mime type --- src/ui/i18n.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/i18n.ts b/src/ui/i18n.ts index 8b6086c79..40a30686b 100644 --- a/src/ui/i18n.ts +++ b/src/ui/i18n.ts @@ -8,6 +8,7 @@ async function loadI18nMessages() { reject: (reason: Error) => void) => { try { const xhr = new XMLHttpRequest(); + xhr.overrideMimeType("application/json"); xhr.onreadystatechange = () => { if (xhr.readyState === 4) { const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); @@ -34,4 +35,4 @@ async function i18n(_ui: UI) { const ui: UIConfig = {data: {i18n}}; _ui.update(ui); -} \ No newline at end of file +} From 171cc486a44e46dd42172ab1ba84ab65596b2999 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 10:20:31 -0600 Subject: [PATCH 101/178] gts fix I have the feeling that this is going to start getting annoying. --- src/ui/i18n.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/i18n.ts b/src/ui/i18n.ts index 40a30686b..86628c0ea 100644 --- a/src/ui/i18n.ts +++ b/src/ui/i18n.ts @@ -8,7 +8,7 @@ async function loadI18nMessages() { reject: (reason: Error) => void) => { try { const xhr = new XMLHttpRequest(); - xhr.overrideMimeType("application/json"); + xhr.overrideMimeType('application/json'); xhr.onreadystatechange = () => { if (xhr.readyState === 4) { const i18nMessage: I18nMessage = JSON.parse(xhr.responseText); From 08c6e0ba9cca2f34fd1b878f0b4741a5c881c530 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 00:35:25 +0800 Subject: [PATCH 102/178] add dropbox risk warning --- _locales/en/messages.json | 4 ++++ _locales/zh_CN/messages.json | 4 ++++ css/popup.css | 4 ++++ popup.html | 3 ++- 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index bb3802071..565e97d4d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -218,5 +218,9 @@ "show_all_entries": { "message": "Show all entries", "description": "Show all entries." + }, + "dropbox_risk": { + "message": "Backups saved in Dropbox are unencrypted (even if you have set a passphrse), use it at your own risk.", + "description": "Dropbox backup risk warning." } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 4dbfb9d24..c7e40fcb6 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -218,5 +218,9 @@ "show_all_entries": { "message": "显示全部条目", "description": "Show all entries." + }, + "dropbox_risk": { + "message": "保存至Dropbox的备份没有加密(即使您已设置了密码),使用此功能需要您自担风险。", + "description": "Dropbox backup risk warning." } } diff --git a/css/popup.css b/css/popup.css index 38891736d..be920028c 100644 --- a/css/popup.css +++ b/css/popup.css @@ -729,12 +729,16 @@ body { #security label, #passphrase label, #security_warning, +#dropbox_risk, +#export_info, #passphrase_info { display: block; margin: 10px 0 0 10px; } #security_warning, +#export_info, +#dropbox_risk, #passphrase_info { color: gray; } diff --git a/popup.html b/popup.html index a5b13fa53..aacb6a7b9 100644 --- a/popup.html +++ b/popup.html @@ -118,7 +118,7 @@
-
{{ i18n.export_info }}
+
{{ i18n.export_info }}
{{ i18n.download_backup }} @@ -126,6 +126,7 @@
+
{{ i18n.dropbox_risk }}
From 61e1a2a1b4ed1d9b5c4b2ebc2735024bc2865f00 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 00:43:02 +0800 Subject: [PATCH 103/178] scrollbar style in chrome --- css/popup.css | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/css/popup.css b/css/popup.css index be920028c..dc7cc83e6 100644 --- a/css/popup.css +++ b/css/popup.css @@ -95,6 +95,17 @@ } } +::-webkit-scrollbar { + width: 10px; + background: #EEE; +} + +::-webkit-scrollbar-thumb { + background-color: #AAA; + border: 2px solid #EEE; + border-radius: 5px; +} + [v-cloak] { display: none } * { @@ -160,17 +171,6 @@ body { overflow-y: scroll; } -#codes::-webkit-scrollbar { - width: 10px; - background: #EEE; -} - -#codes::-webkit-scrollbar-thumb { - background-color: #AAA; - border: 2px solid #EEE; - border-radius: 5px; -} - #codeClipboard { position: absolute; top: -1000px; From 32814adb7134010942c32f3297a88a65e12b1b01 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 10:55:14 -0600 Subject: [PATCH 104/178] Import file bug workaround base work --- import.html | 35 +++++++++++++++++++++++++++++++++++ popup.html | 4 +++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 import.html diff --git a/import.html b/import.html new file mode 100644 index 000000000..d8649fff7 --- /dev/null +++ b/import.html @@ -0,0 +1,35 @@ + + + + Import Backup + + + + + + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + diff --git a/popup.html b/popup.html index a5b13fa53..50be0950a 100644 --- a/popup.html +++ b/popup.html @@ -1,4 +1,4 @@ - +/ @@ -122,6 +122,8 @@ {{ i18n.download_backup }} + + {{ i18n.import_backup }}
From 5756398f1e3e5f2c5a685e831c1fb18f581c2c6c Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 00:59:46 +0800 Subject: [PATCH 105/178] change dropbox backup path --- src/models/dropbox.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/dropbox.ts b/src/models/dropbox.ts index 0d08637ae..2003b3c9c 100644 --- a/src/models/dropbox.ts +++ b/src/models/dropbox.ts @@ -61,7 +61,7 @@ class Dropbox { const now = (new Date()).toISOString().slice(0, 10).replace(/-/g, ''); const apiArg = { - path: `/Authenticator Backup/${now}.json`, + path: `/${now}.json`, mode: 'add', autorename: true }; From bc60401b3bc172036fb810c398cbe4118e07ebf7 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 01:04:31 +0800 Subject: [PATCH 106/178] typo fix --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 565e97d4d..1340964ad 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -220,7 +220,7 @@ "description": "Show all entries." }, "dropbox_risk": { - "message": "Backups saved in Dropbox are unencrypted (even if you have set a passphrse), use it at your own risk.", + "message": "Backups saved in Dropbox are unencrypted (even if you have set a passphrase), use it at your own risk.", "description": "Dropbox backup risk warning." } } From b3525174280a0c0bab155ac2c3cf9ebe37b67f63 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 11:56:51 -0600 Subject: [PATCH 107/178] Show right button depending on useragent --- popup.html | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/popup.html b/popup.html index 50be0950a..e944491a3 100644 --- a/popup.html +++ b/popup.html @@ -1,4 +1,4 @@ -/ + @@ -120,10 +120,11 @@
{{ i18n.export_info }}
{{ i18n.download_backup }} - - - - {{ i18n.import_backup }} +
+ + +
+ {{ i18n.import_backup }}
From f89c1786e5de5ed7a719c4207128b67f85f24df8 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 12:06:33 -0600 Subject: [PATCH 108/178] Close window on import --- import.html | 2 +- src/ui/entry.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/import.html b/import.html index d8649fff7..845026ad1 100644 --- a/import.html +++ b/import.html @@ -17,7 +17,7 @@
- +
diff --git a/src/ui/entry.ts b/src/ui/entry.ts index cf641235f..1a8347708 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -150,7 +150,7 @@ async function entry(_ui: UI) { await _ui.instance.updateCode(); return; }, - importFile: (event: Event) => { + importFile: (event: Event, closeWindow: boolean) => { const target = event.target as HTMLInputElement; if (!target || !target.files) { return; @@ -162,10 +162,17 @@ async function entry(_ui: UI) { await EntryStorage.import(_ui.instance.encryption, importData); await _ui.instance.updateEntries(); _ui.instance.message = _ui.instance.i18n.updateSuccess; + if (closeWindow) { + window.close(); + } }; reader.readAsText(target.files[0]); } else { _ui.instance.message = _ui.instance.i18n.updateFailure; + if (closeWindow) { + window.alert(_ui.instance.i18n.updateFailure); + window.close(); + } } return; }, From 2f861c0c343430cb4274f9c3aea3f413cb619a38 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 02:12:11 +0800 Subject: [PATCH 109/178] fix #19 --- src/ui/entry.ts | 12 ++++++++++-- src/ui/passphrase.ts | 2 ++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ui/entry.ts b/src/ui/entry.ts index cf641235f..3533718ab 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -1,4 +1,5 @@ /* tslint:disable:no-reference */ +/// /// /// /// @@ -90,9 +91,16 @@ async function entry(_ui: UI) { const cookieMatch = cookie ? document.cookie.split('passphrase=') : null; const cachedPassphrase = cookieMatch && cookieMatch.length > 1 ? cookieMatch[1] : null; - const encryption: Encryption = new Encryption(cachedPassphrase || ''); + const cachedPassphraseLocalStorage = localStorage.encodedPhrase ? + CryptoJS.AES.decrypt(localStorage.encodedPhrase, '') + .toString(CryptoJS.enc.Utf8) : + ''; + const encryption: Encryption = + new Encryption(cachedPassphrase || cachedPassphraseLocalStorage || ''); const shouldShowPassphrase = - cachedPassphrase ? false : await EntryStorage.hasEncryptedEntry(); + (cachedPassphrase || cachedPassphraseLocalStorage) ? + false : + await EntryStorage.hasEncryptedEntry(); const exportData = shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); const entries = shouldShowPassphrase ? [] : await getEntries(encryption); diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts index b35cf8325..c7dc92e65 100644 --- a/src/ui/passphrase.ts +++ b/src/ui/passphrase.ts @@ -23,6 +23,8 @@ async function passphrase(_ui: UI) { _ui.instance.encryption.updateEncryptionPassword( _ui.instance.newPassphrase.phrase); await _ui.instance.importEntries(); + // remove cached passphrase in old version + localStorage.removeItem('encodedPhrase'); return; } } From 043557a14d09c653f7594361f81923039b5c1322 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 12:14:46 -0600 Subject: [PATCH 110/178] Style fixes --- import.html | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/import.html b/import.html index 845026ad1..fb8e28362 100644 --- a/import.html +++ b/import.html @@ -1,24 +1,23 @@ - - Import Backup - - - - - - - - - - - - + + Import Backup + + + + + + + + + + + +
- - +
From a0f10dfcc21c7e1ea283d6c260c7c05b0ed3fbbc Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 12:15:41 -0600 Subject: [PATCH 111/178] scripts to body --- import.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/import.html b/import.html index fb8e28362..36754afae 100644 --- a/import.html +++ b/import.html @@ -19,7 +19,6 @@
- @@ -31,4 +30,5 @@ + From b9ae341865837d371dfa55fb0f3ea45aa074d963 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 12:40:55 -0600 Subject: [PATCH 112/178] Fix chrome issue --- popup.html | 4 ++-- src/ui/menu.ts | 8 +++++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/popup.html b/popup.html index e944491a3..279efd87f 100644 --- a/popup.html +++ b/popup.html @@ -120,9 +120,9 @@
{{ i18n.export_info }}
{{ i18n.download_backup }} -
+
- +
{{ i18n.import_backup }}
diff --git a/src/ui/menu.ts b/src/ui/menu.ts index 87047d88a..65d7a02f9 100644 --- a/src/ui/menu.ts +++ b/src/ui/menu.ts @@ -52,7 +52,6 @@ function resize(zoom: number) { } function openHelp() { - console.log('help'); let url = 'https://github.com/Authenticator-Extension/Authenticator/wiki/Chrome-Issues'; @@ -92,6 +91,13 @@ async function menu(_ui: UI) { openHelp(); return; }, + isChrome: () => { + if (navigator.userAgent.indexOf('Chrome') !== -1) { + return true; + } else { + return false; + } + }, saveZoom: () => { localStorage.zoom = _ui.instance.zoom; resize(_ui.instance.zoom); From a5523ff76eae301f646a1a7b34e1f08ff27cba4f Mon Sep 17 00:00:00 2001 From: Brendan Date: Tue, 20 Feb 2018 12:48:00 -0600 Subject: [PATCH 113/178] Update popup.html --- popup.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/popup.html b/popup.html index 279efd87f..33a681cd9 100644 --- a/popup.html +++ b/popup.html @@ -120,7 +120,7 @@
{{ i18n.export_info }}
{{ i18n.download_backup }} -
+
From 3143a8287cb673e9cb7e79bf83d88eb0a657c345 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 12:50:52 -0600 Subject: [PATCH 114/178] Tabs to spaces --- popup.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/popup.html b/popup.html index 33a681cd9..9f993a702 100644 --- a/popup.html +++ b/popup.html @@ -123,8 +123,8 @@
-
- {{ i18n.import_backup }} +
+ {{ i18n.import_backup }}
From 4908d7965e3f9a930ffc1b69a4c4ee03c0eeacae Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 13:02:02 -0600 Subject: [PATCH 115/178] Add lintspaces --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 028ecc7c9..66c832bcf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,10 @@ before_script: - "npm install -g typescript" - "npm install -g gts" - "npm install -g addons-linter" + - "npm install -g lintspaces-cli" script: - "gts check" + - "lintspaces -nt -d 'spaces' -i 'js-comments' src/* *.html manifest-*.json css/popup.css" - "npm run firefox" - "npm run chrome" - "addons-linter firefox" From 0a7aba98dd3fbe9ecb4e2e47ffff2de539b0d6fc Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 03:04:53 +0800 Subject: [PATCH 116/178] update cached passphrase after changed --- src/ui/passphrase.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts index c7dc92e65..d0ead3a5a 100644 --- a/src/ui/passphrase.ts +++ b/src/ui/passphrase.ts @@ -22,6 +22,7 @@ async function passphrase(_ui: UI) { } _ui.instance.encryption.updateEncryptionPassword( _ui.instance.newPassphrase.phrase); + document.cookie = 'passphrase=' + _ui.instance.newPassphrase.phrase; await _ui.instance.importEntries(); // remove cached passphrase in old version localStorage.removeItem('encodedPhrase'); From 730bd48e44fbb91fd1a01372d93e3878fc5bfb1b Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 13:05:57 -0600 Subject: [PATCH 117/178] Make the lint happy --- css/popup.css | 4 +- manifest-chrome.json | 148 +++++++++++++++++++++---------------------- qr.html | 2 +- src/background.ts | 2 +- src/content.ts | 2 +- src/popup.ts | 2 +- 6 files changed, 80 insertions(+), 80 deletions(-) diff --git a/css/popup.css b/css/popup.css index dc7cc83e6..58b56666b 100644 --- a/css/popup.css +++ b/css/popup.css @@ -106,7 +106,7 @@ border-radius: 5px; } -[v-cloak] { display: none } +[v-cloak] { display: none } * { margin: 0; @@ -787,4 +787,4 @@ body { #dropbox_authorization { text-align: right; padding: 0 10px; -} \ No newline at end of file +} diff --git a/manifest-chrome.json b/manifest-chrome.json index 9b1847a61..b329c6cd8 100644 --- a/manifest-chrome.json +++ b/manifest-chrome.json @@ -1,74 +1,74 @@ -{ - "manifest_version": 2, - "name": "__MSG_extName__", - "short_name": "__MSG_extShortName__", - "version": "5.0", - "default_locale": "en", - "description": "__MSG_extDesc__", - "icons": { - "16": "images/icon16.png", - "48": "images/icon48.png", - "128": "images/icon128.png" - }, - "browser_action": { - "default_icon": { - "19": "images/icon19.png", - "38": "images/icon38.png" - }, - "default_title": "__MSG_extShortName__", - "default_popup": "popup.html" - }, - "background": { - "scripts": [ - "js/jsqrcode/grid.js", - "js/jsqrcode/version.js", - "js/jsqrcode/detector.js", - "js/jsqrcode/formatinf.js", - "js/jsqrcode/errorlevel.js", - "js/jsqrcode/bitmat.js", - "js/jsqrcode/datablock.js", - "js/jsqrcode/bmparser.js", - "js/jsqrcode/datamask.js", - "js/jsqrcode/rsdecoder.js", - "js/jsqrcode/gf256poly.js", - "js/jsqrcode/gf256.js", - "js/jsqrcode/decoder.js", - "js/jsqrcode/qrcode.js", - "js/jsqrcode/findpat.js", - "js/jsqrcode/alignpat.js", - "js/jsqrcode/databr.js", - "js/md5.js", - "js/aes.js", - "js/sha.js", - "js/qrcode.js", - "build/models/encryption.js", - "build/models/interface.js", - "build/models/otp.js", - "build/models/storage.js", - "build/background.js" - ], - "persistent": false - }, - "content_scripts": [ - { - "matches": [""], - "css": ["css/content.css"], - "js": ["build/content.js"] - } - ], - "permissions": [ - "activeTab", - "storage" - ], - "optional_permissions": [ - "clipboardWrite", - "https://www.google.com/", - "https://*.dropboxapi.com/*" - ], - "offline_enabled": true, - "web_accessible_resources": [ - "qr.html", - "images/scan.gif" - ], - "content_security_policy": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com; default-src 'none'" -} \ No newline at end of file +{ + "manifest_version": 2, + "name": "__MSG_extName__", + "short_name": "__MSG_extShortName__", + "version": "5.0", + "default_locale": "en", + "description": "__MSG_extDesc__", + "icons": { + "16": "images/icon16.png", + "48": "images/icon48.png", + "128": "images/icon128.png" + }, + "browser_action": { + "default_icon": { + "19": "images/icon19.png", + "38": "images/icon38.png" + }, + "default_title": "__MSG_extShortName__", + "default_popup": "popup.html" + }, + "background": { + "scripts": [ + "js/jsqrcode/grid.js", + "js/jsqrcode/version.js", + "js/jsqrcode/detector.js", + "js/jsqrcode/formatinf.js", + "js/jsqrcode/errorlevel.js", + "js/jsqrcode/bitmat.js", + "js/jsqrcode/datablock.js", + "js/jsqrcode/bmparser.js", + "js/jsqrcode/datamask.js", + "js/jsqrcode/rsdecoder.js", + "js/jsqrcode/gf256poly.js", + "js/jsqrcode/gf256.js", + "js/jsqrcode/decoder.js", + "js/jsqrcode/qrcode.js", + "js/jsqrcode/findpat.js", + "js/jsqrcode/alignpat.js", + "js/jsqrcode/databr.js", + "js/md5.js", + "js/aes.js", + "js/sha.js", + "js/qrcode.js", + "build/models/encryption.js", + "build/models/interface.js", + "build/models/otp.js", + "build/models/storage.js", + "build/background.js" + ], + "persistent": false + }, + "content_scripts": [ + { + "matches": [""], + "css": ["css/content.css"], + "js": ["build/content.js"] + } + ], + "permissions": [ + "activeTab", + "storage" + ], + "optional_permissions": [ + "clipboardWrite", + "https://www.google.com/", + "https://*.dropboxapi.com/*" + ], + "offline_enabled": true, + "web_accessible_resources": [ + "qr.html", + "images/scan.gif" + ], + "content_security_policy": "script-src 'self'; font-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; connect-src https://www.google.com/ https://*.dropboxapi.com; default-src 'none'" +} diff --git a/qr.html b/qr.html index 950956940..4111933e1 100644 --- a/qr.html +++ b/qr.html @@ -5,4 +5,4 @@ - \ No newline at end of file + diff --git a/src/background.ts b/src/background.ts index d07094973..8e385e7ef 100644 --- a/src/background.ts +++ b/src/background.ts @@ -136,4 +136,4 @@ chrome.runtime.onInstalled.addListener((details) => { if (url) { window.open(url, '_blank'); } -}); \ No newline at end of file +}); diff --git a/src/content.ts b/src/content.ts index 8da4ec5bd..e7edb8412 100644 --- a/src/content.ts +++ b/src/content.ts @@ -157,4 +157,4 @@ function showQrCode(msg: string) { url, '_blank', 'toolbar=no, location=no, status=no, menubar=no, scrollbars=yes, copyhistory=no, width=400, height=200, left=' + left + ',top=' + top); -} \ No newline at end of file +} diff --git a/src/popup.ts b/src/popup.ts index 09e65526b..5714116d9 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -90,4 +90,4 @@ chrome.permissions.contains( } }); -init(); \ No newline at end of file +init(); From 50f7e23556a1667f2080598eeeedce05887aa2d0 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 03:56:03 +0800 Subject: [PATCH 118/178] update info --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 0fe67bfb8..ebf4a3833 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017 Zhe Li +Copyright (c) 2017 Authenticator Extension Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 12ee8e82574cbc3d995a093410f33c633e09a7eb Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 14:34:47 -0600 Subject: [PATCH 119/178] Make header text non-selectable --- css/popup.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/css/popup.css b/css/popup.css index 58b56666b..f180667f6 100644 --- a/css/popup.css +++ b/css/popup.css @@ -132,6 +132,8 @@ body { text-align: center; font-size: 16px; border-bottom: #CCC 1px solid; + user-select: none; + -moz-user-select: none; } #notification { From 91d5825a0cdfe25e4cc2286cb2faf356935c4af1 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 14:46:35 -0600 Subject: [PATCH 120/178] Add helper text --- _locales/en/messages.json | 6 +++++- popup.html | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 1340964ad..c67d38af0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -59,6 +59,10 @@ "message": "Account", "description": "Account." }, + "username": { + "message": "Username", + "description": "Username." + }, "secret": { "message": "Secret", "description": "Secret." @@ -220,7 +224,7 @@ "description": "Show all entries." }, "dropbox_risk": { - "message": "Backups saved in Dropbox are unencrypted (even if you have set a passphrase), use it at your own risk.", + "message": "Warning: backups saved in Dropbox are unencrypted. Use at your own risk.", "description": "Dropbox backup risk warning." } } diff --git a/popup.html b/popup.html index b5a532add..d5dccede1 100644 --- a/popup.html +++ b/popup.html @@ -36,12 +36,12 @@
{{ entry.issuer.split('::')[0] }}
- +
- +
From cd2507a8047130c308d941089e5f94e07b3e4582 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 20 Feb 2018 15:04:43 -0600 Subject: [PATCH 121/178] CSS tweaks --- _locales/en/messages.json | 4 ++++ css/popup.css | 10 ++++++---- popup.html | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c67d38af0..7f42af41f 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -59,6 +59,10 @@ "message": "Account", "description": "Account." }, + "accountName": { + "message": "Account name", + "description": "Account Name." + }, "username": { "message": "Username", "description": "Username." diff --git a/css/popup.css b/css/popup.css index f180667f6..45539c5f8 100644 --- a/css/popup.css +++ b/css/popup.css @@ -551,9 +551,6 @@ body { color: black; } -#menu .menuList p:hover:after { - color: black; -} #menu .menuList p:last-child { border-bottom: none; @@ -640,7 +637,12 @@ body { #dropbox_ok:hover, #export:hover, #resizeClose:hover, -#resize_save:hover { +#resize_save:hover, +#confirm_cancel:hover, +#confirm_ok:hover, +#menu .menuList p:hover:after, +.entry:hover .movehandle, +.showqr:hover { color: black; } diff --git a/popup.html b/popup.html index d5dccede1..0724ab7d2 100644 --- a/popup.html +++ b/popup.html @@ -41,7 +41,7 @@
- +
From 154a186a7c6ec02f0794a07769e9f9fe94f3946c Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 13:09:26 +0800 Subject: [PATCH 122/178] add build version --- manifest-chrome.json | 2 +- manifest-firefox.json | 2 +- package-lock.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/manifest-chrome.json b/manifest-chrome.json index b329c6cd8..ca0581345 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", + "version": "5.0.1", "default_locale": "en", "description": "__MSG_extDesc__", "icons": { diff --git a/manifest-firefox.json b/manifest-firefox.json index c78958cc7..c6adf2a6a 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", + "version": "5.0.1", "default_locale": "en", "description": "__MSG_extDesc__", "applications": { diff --git a/package-lock.json b/package-lock.json index 12f2f1b6f..fdeb30c9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "authenticator", + "name": "authenticator-extension", "version": "0.1.0", "lockfileVersion": 1, "requires": true, From d9a754e3815dd79eba2c147e56d9438f4afbfce9 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 23:11:49 +0800 Subject: [PATCH 123/178] fix #1 --- _locales/en/messages.json | 8 ++++ css/import.css | 94 +++++++++++++++++++++++++++++++++++++++ css/popup.css | 5 ++- import.html | 31 +++++++++++-- js/import.js | 1 + popup.html | 6 +-- src/models/storage.ts | 20 ++++----- src/ui/entry.ts | 55 ++++++++++++++++++++++- 8 files changed, 200 insertions(+), 20 deletions(-) create mode 100644 css/import.css create mode 100644 js/import.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7f42af41f..d5a242513 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -204,9 +204,17 @@ "description": "Download backup file." }, "import_backup": { + "message": "Import Backup", + "description": "Import backup." + }, + "import_backup_file": { "message": "Import Backup File", "description": "Import backup file." }, + "import_backup_code": { + "message": "Import Backup Code", + "description": "Import backup code." + }, "dropbox_backup": { "message": "Auto Backup to Dropbox", "description": "Auto backup to Dropbox." diff --git a/css/import.css b/css/import.css new file mode 100644 index 000000000..47098bde6 --- /dev/null +++ b/css/import.css @@ -0,0 +1,94 @@ +[v-cloak] { display: none } + +#authenticator { + width: 600px; + position: relative; + margin: 0 auto; +} + +.import_tab { + text-align: center; + font-size: 0; +} + +.import_tab input { + display: none; +} + +.import_tab label { + width: 250px; + height: 50px; + line-height: 50px; + font-size: 18px; + text-align: center; + display: inline-block; + margin: 20px; + cursor: pointer; + border-radius: 2px; +} + +.import_tab input:checked + label, +.import_tab label:hover { + background: #eee; +} + +.import_file { + text-align: center; +} + +.import_file input { + display: none; +} + +button, +.import_file label { + display: inline-block; + width: 260px; + height: 60px; + line-height: 60px; + border: #CCC 1px solid; + background: white; + border-radius: 2px; + position: relative; + text-align: center; + font-size: 16px; + color: gray; + cursor: pointer; + outline: none; +} + +.import_file label:hover { + color: black; +} + +.import_encrypted { + margin-bottom: 20px; +} + +.import_encrypted input { + margin-left: 0; +} + +.import_code { + float: left; + margin-left: 30px; + margin-right: 40px; +} + +.import_code textarea { + width: 250px; + height: 400px; + padding: 10px; + outline: none; + resize: none; + box-sizing: border-box; +} + +.import_passphrase input { + padding: 10px; + margin-bottom: 20px; + width: 250px; + border: #CCC 1px solid; + background: white; + outline: none; +} \ No newline at end of file diff --git a/css/popup.css b/css/popup.css index 45539c5f8..d1bedd2b7 100644 --- a/css/popup.css +++ b/css/popup.css @@ -348,6 +348,7 @@ body { #add_secret, #add_button, #download_backup, +#import_backup, #upload_backup, #security_save, #passphrase_ok, @@ -624,6 +625,7 @@ body { #add_qr:hover, #upload_backup:hover, #download_backup:hover, +#import_backup:hover, #editAction:hover, #infoAction:hover, #scan:hover, @@ -766,7 +768,8 @@ body { z-index: 1000; } -#download_backup { +#download_backup, +#import_backup { text-decoration: none; color: gray; display: block; diff --git a/import.html b/import.html index 36754afae..1da79050e 100644 --- a/import.html +++ b/import.html @@ -1,7 +1,9 @@ - Import Backup + + + @@ -16,8 +18,31 @@ -
- +
+
+ + + + +
+
+ + +
+
+
+ +
+
+ + +
+
+ + +
+ +
diff --git a/js/import.js b/js/import.js new file mode 100644 index 000000000..8beb28690 --- /dev/null +++ b/js/import.js @@ -0,0 +1 @@ +document.title = chrome.i18n.getMessage('import_backup'); \ No newline at end of file diff --git a/popup.html b/popup.html index 0724ab7d2..13dd4636e 100644 --- a/popup.html +++ b/popup.html @@ -120,11 +120,7 @@
{{ i18n.export_info }}
{{ i18n.download_backup }} -
- - -
- {{ i18n.import_backup }} + {{ i18n.import_backup }}
diff --git a/src/models/storage.ts b/src/models/storage.ts index fc77203c3..5d80dcac6 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -145,13 +145,6 @@ class EntryStorage { data[hash].type = data[hash].type || OTPType[OTPType.totp]; data[hash].counter = data[hash].counter || 0; - const _hash = CryptoJS.MD5(data[hash].secret).toString(); - if (_hash !== hash) { - data[_hash] = data[hash]; - data[_hash].hash = _hash; - delete data[hash]; - } - if (/^(blz\-|bliz\-)/.test(data[hash].secret)) { const secretMatches = data[hash].secret.match(/^(blz\-|bliz\-)(.*)/); @@ -169,10 +162,17 @@ class EntryStorage { } } - data[hash].secret = - encryption.getEncryptedSecret(data[hash].secret); + const _hash = CryptoJS.MD5(data[hash].secret).toString(); + if (_hash !== hash) { + data[_hash] = data[hash]; + data[_hash].hash = _hash; + delete data[hash]; + } + + data[_hash].secret = + encryption.getEncryptedSecret(data[_hash].secret); - _data[hash] = data[hash]; + _data[_hash] = data[_hash]; } _data = this.ensureUniqueIndex(_data); chrome.storage.sync.set(_data, resolve); diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 5eca388f0..e2acc3d0e 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -122,12 +122,65 @@ async function entry(_ui: UI) { notificationTimeout: 0, filter: true, currentHost, - shouldFilter + shouldFilter, + importType: 'import_file', + importCode: '', + importEncrypted: false, + importPassphrase: '' }, methods: { updateCode: async () => { return await updateCode(_ui.instance); }, + importBackupCode: async () => { + try { + const exportData: {[hash: string]: OTPStorage} = + JSON.parse(_ui.instance.importCode); + const passphrase = + _ui.instance.importEncrypted && _ui.instance.importPassphrase ? + _ui.instance.importPassphrase : + null; + const decryptedBackup: {[hash: string]: OTPStorage} = {}; + for (const hash of Object.keys(exportData)) { + if (typeof exportData[hash] !== 'object') { + continue; + } + if (!exportData[hash].secret) { + continue; + } + if (exportData[hash].encrypted && !passphrase) { + continue; + } + if (exportData[hash].encrypted) { + try { + exportData[hash].secret = + CryptoJS.AES.decrypt(exportData[hash].secret, passphrase) + .toString(CryptoJS.enc.Utf8); + exportData[hash].encrypted = false; + } catch (error) { + continue; + } + } + // exportData[hash].secret may be empty after decrypt with wrong + // passphrase + if (!exportData[hash].secret) { + continue; + } + decryptedBackup[hash] = exportData[hash]; + } + if (Object.keys(decryptedBackup).length) { + await EntryStorage.import(_ui.instance.encryption, decryptedBackup); + await _ui.instance.updateEntries(); + alert(_ui.instance.i18n.updateSuccess); + window.close(); + } else { + alert(_ui.instance.i18n.updateFailure); + } + return; + } catch (error) { + throw error; + } + }, noCopy: (code: string) => { return code === 'Encrypted' || code === 'Invalid' || code.startsWith('•'); From 0577598fbd5daf4686727c8e7cc607571c8e3e0d Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 23:25:44 +0800 Subject: [PATCH 124/178] hover button style --- css/import.css | 1 + 1 file changed, 1 insertion(+) diff --git a/css/import.css b/css/import.css index 47098bde6..895ce9694 100644 --- a/css/import.css +++ b/css/import.css @@ -57,6 +57,7 @@ button, outline: none; } +button:hover, .import_file label:hover { color: black; } From 6eef3ee5b3eadada0561a6238452cc54792abe24 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 21 Feb 2018 23:45:28 +0800 Subject: [PATCH 125/178] chinese i18n --- _locales/zh_CN/messages.json | 50 ++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index c7e40fcb6..83e0c411c 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -1,4 +1,4 @@ -{ +{ "extName": { "message": "身份验证器", "description": "Extension Name." @@ -24,7 +24,7 @@ "description": "Secret Error." }, "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018 Authenticator Extension. Released under the MIT License.

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。

jsqrcode的所有权归jsqrcode的作者所有。以Apache协议授权。

ZXing的所有权归ZXing的作者所有。以Apache协议授权。

totp.js的所有权归其作者所有。以MIT协议授权。

jsSHA的所有权归jsSHA的作者所有。以BSD协议授权。

qrcode.js的所有权归qrcode.js作者所有。以MIT协议授权。

crypto-js的所有权归crypto-js作者所有。以BSD协议授权。

Droid Sans Mono的所有权归Steve Matteson所有。以Apache协议授权。

Font Awesome的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。

感谢 Mike Robinson <3

", + "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2018 Authenticator Extension<\/a>. Released under the MIT License.<\/p>

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。<\/p>

jsqrcode<\/a>的所有权归jsqrcode的作者所有。以Apache协议授权。<\/p>

ZXing<\/a>的所有权归ZXing的作者所有。以Apache协议授权。<\/p>

totp.js<\/a>的所有权归其作者所有。以MIT协议授权。<\/p>

jsSHA<\/a>的所有权归jsSHA的作者所有。以BSD协议授权。<\/p>

qrcode.js<\/a>的所有权归qrcode.js作者所有。以MIT协议授权。<\/p>

crypto-js<\/a>的所有权归crypto-js作者所有。以BSD协议授权。<\/p>

Droid Sans Mono<\/a>的所有权归Steve Matteson所有。以Apache协议授权。<\/p>

Font Awesome<\/a>的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。<\/p>

感谢 Mike Robinson<\/a> <3<\/p>", "description": "Information." }, "add_qr": { @@ -44,8 +44,8 @@ "description": "OK." }, "yes": { - "message": "是", - "description": "Yes." + "message": "是", + "description": "Yes." }, "no": { "message": "否", @@ -59,6 +59,14 @@ "message": "账户", "description": "Account." }, + "accountName": { + "message": "账户名称", + "description": "Account Name." + }, + "username": { + "message": "用户名", + "description": "Username." + }, "secret": { "message": "密钥", "description": "Secret." @@ -76,7 +84,7 @@ "description": "About." }, "export_import": { - "message": "导出 / 导入", + "message": "导出 \/ 导入", "description": "Export and Import." }, "settings": { @@ -89,27 +97,27 @@ }, "current_phrase": { "message": "当前密码", - "description": "Current Phrase." + "description": "Current Passphrase." }, "new_phrase": { "message": "新密码", - "description": "New Phrase." + "description": "New Passphrase." }, "phrase": { "message": "密码", - "description": "Phrase." + "description": "Passphrase." }, "confirm_phrase": { "message": "确认密码", - "description": "Confirmm Phrase." + "description": "Confirmm Passphrase." }, - "confirm_delete" : { - "message": "您确定要删除此密钥吗?您无法找回已删除的密钥。此操作无法撤销。", + "confirm_delete": { + "message": "您确定要删除此密钥吗?此操作无法撤销。", "description": "Remove entry confirmation" }, "security_warning": { "message": "您的密钥将使用此密码进行加密。如果您忘记了密码没有人能够提供帮助。", - "description": "Phrase Warning." + "description": "Passphrase Warning." }, "update": { "message": "更新", @@ -117,11 +125,11 @@ }, "phrase_incorrect": { "message": "部分账户与密码不匹配,您无法添加新账户、导出账户数据或者更改密码。请提供正确的密码后重试。", - "description": "Phrase Incorrect." + "description": "Passphrase Incorrect." }, "phrase_not_match": { "message": "两次密码不一致。", - "description": "Phrase Not Match." + "description": "Passphrase Not Match." }, "encrypted": { "message": "已加密", @@ -160,7 +168,7 @@ "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "您是否为密钥创建了备份?请注意,没有人可以帮助您找回被锁定的账户,不要等到为时已晚。我们将在30天后再次提醒您进行备份。", + "message": "您是否为密钥创建了备份?不要等到为时已晚。", "description": "Remind Backup" }, "capture_failed": { @@ -196,9 +204,17 @@ "description": "Download backup file." }, "import_backup": { + "message": "导入备份", + "description": "Import backup." + }, + "import_backup_file": { "message": "导入备份文件", "description": "Import backup file." }, + "import_backup_code": { + "message": "导入备份代码", + "description": "Import backup code." + }, "dropbox_backup": { "message": "自动备份至Dropbox", "description": "Auto backup to Dropbox." @@ -220,7 +236,7 @@ "description": "Show all entries." }, "dropbox_risk": { - "message": "保存至Dropbox的备份没有加密(即使您已设置了密码),使用此功能需要您自担风险。", + "message": "警告:保存至Dropbox的备份均未备份,您需自担风险。", "description": "Dropbox backup risk warning." } -} +} \ No newline at end of file From 687bc0296f100827ceb3a9419d2f6b7fe293b835 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Wed, 21 Feb 2018 10:16:04 -0600 Subject: [PATCH 126/178] travis update --- .travis.yml | 2 +- _locales/en/messages.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 66c832bcf..063bcd179 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ before_script: - "npm install -g lintspaces-cli" script: - "gts check" - - "lintspaces -nt -d 'spaces' -i 'js-comments' src/* *.html manifest-*.json css/popup.css" + - "lintspaces -nt -d 'spaces' -i 'js-comments' src/* *.html manifest-*.json css/popup.css css/import.css" - "npm run firefox" - "npm run chrome" - "addons-linter firefox" diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d5a242513..158024211 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -212,7 +212,7 @@ "description": "Import backup file." }, "import_backup_code": { - "message": "Import Backup Code", + "message": "Import Text Backup", "description": "Import backup code." }, "dropbox_backup": { From 94ad3c2f7c0fcd89963c04511573e88de1180959 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Wed, 21 Feb 2018 10:26:40 -0600 Subject: [PATCH 127/178] String cleanup --- _locales/en/messages.json | 14 +-- _locales/zh_CN/messages.json | 10 +- css/import.css | 190 +++++++++++++++++------------------ 3 files changed, 99 insertions(+), 115 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 158024211..f1bb6dc22 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -51,16 +51,12 @@ "message": "No", "description": "No." }, - "err_acc_sec": { - "message": "Please enter account and secret.", - "description": "Input Account and Secret." - }, "account": { "message": "Account", "description": "Account." }, "accountName": { - "message": "Account name", + "message": "Account Name", "description": "Account Name." }, "username": { @@ -109,7 +105,7 @@ }, "confirm_phrase": { "message": "Confirm Password", - "description": "Confirmm Passphrase." + "description": "Confirm Passphrase." }, "confirm_delete" : { "message": "Are you sure you want to delete this secret? This action cannot be undone.", @@ -128,7 +124,7 @@ "description": "Passphrase Incorrect." }, "phrase_not_match": { - "message": "Two passphrases do not match.", + "message": "Password does not match.", "description": "Passphrase Not Match." }, "encrypted": { @@ -175,10 +171,6 @@ "message": "Capture failed, please reload the page and try again.", "description": "Capture Failed" }, - "unencrypted_secret_warning": { - "message": "This secret is not encrypted! Click here to set a passphrase.", - "description": "Unencrypted Secret Warning" - }, "based_on_time": { "message": "Time Based", "description": "Time Based" diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 83e0c411c..1bb4cde71 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -51,10 +51,6 @@ "message": "否", "description": "No." }, - "err_acc_sec": { - "message": "请输入账户和密钥。", - "description": "Input Account and Secret." - }, "account": { "message": "账户", "description": "Account." @@ -175,10 +171,6 @@ "message": "捕捉失败,请重载您正在浏览的页面后重试。", "description": "Capture Failed" }, - "unencrypted_secret_warning": { - "message": "此密钥未被加密!点击此处来设置一个密码以解决此问题。", - "description": "Unencrypted Secret Warning" - }, "based_on_time": { "message": "基于时间", "description": "Time Based" @@ -239,4 +231,4 @@ "message": "警告:保存至Dropbox的备份均未备份,您需自担风险。", "description": "Dropbox backup risk warning." } -} \ No newline at end of file +} diff --git a/css/import.css b/css/import.css index 895ce9694..7cab0ad46 100644 --- a/css/import.css +++ b/css/import.css @@ -1,95 +1,95 @@ -[v-cloak] { display: none } - -#authenticator { - width: 600px; - position: relative; - margin: 0 auto; -} - -.import_tab { - text-align: center; - font-size: 0; -} - -.import_tab input { - display: none; -} - -.import_tab label { - width: 250px; - height: 50px; - line-height: 50px; - font-size: 18px; - text-align: center; - display: inline-block; - margin: 20px; - cursor: pointer; - border-radius: 2px; -} - -.import_tab input:checked + label, -.import_tab label:hover { - background: #eee; -} - -.import_file { - text-align: center; -} - -.import_file input { - display: none; -} - -button, -.import_file label { - display: inline-block; - width: 260px; - height: 60px; - line-height: 60px; - border: #CCC 1px solid; - background: white; - border-radius: 2px; - position: relative; - text-align: center; - font-size: 16px; - color: gray; - cursor: pointer; - outline: none; -} - -button:hover, -.import_file label:hover { - color: black; -} - -.import_encrypted { - margin-bottom: 20px; -} - -.import_encrypted input { - margin-left: 0; -} - -.import_code { - float: left; - margin-left: 30px; - margin-right: 40px; -} - -.import_code textarea { - width: 250px; - height: 400px; - padding: 10px; - outline: none; - resize: none; - box-sizing: border-box; -} - -.import_passphrase input { - padding: 10px; - margin-bottom: 20px; - width: 250px; - border: #CCC 1px solid; - background: white; - outline: none; -} \ No newline at end of file +[v-cloak] { display: none } + +#authenticator { + width: 600px; + position: relative; + margin: 0 auto; +} + +.import_tab { + text-align: center; + font-size: 0; +} + +.import_tab input { + display: none; +} + +.import_tab label { + width: 250px; + height: 50px; + line-height: 50px; + font-size: 18px; + text-align: center; + display: inline-block; + margin: 20px; + cursor: pointer; + border-radius: 2px; +} + +.import_tab input:checked + label, +.import_tab label:hover { + background: #eee; +} + +.import_file { + text-align: center; +} + +.import_file input { + display: none; +} + +button, +.import_file label { + display: inline-block; + width: 260px; + height: 60px; + line-height: 60px; + border: #CCC 1px solid; + background: white; + border-radius: 2px; + position: relative; + text-align: center; + font-size: 16px; + color: gray; + cursor: pointer; + outline: none; +} + +button:hover, +.import_file label:hover { + color: black; +} + +.import_encrypted { + margin-bottom: 20px; +} + +.import_encrypted input { + margin-left: 0; +} + +.import_code { + float: left; + margin-left: 30px; + margin-right: 40px; +} + +.import_code textarea { + width: 250px; + height: 400px; + padding: 10px; + outline: none; + resize: none; + box-sizing: border-box; +} + +.import_passphrase input { + padding: 10px; + margin-bottom: 20px; + width: 250px; + border: #CCC 1px solid; + background: white; + outline: none; +} From f29c81c2893e8f28e42b6a09181d28b29f6a5dfe Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 22 Feb 2018 00:32:54 +0800 Subject: [PATCH 128/178] remove about from i18n --- _locales/en/messages.json | 4 ---- _locales/zh_CN/messages.json | 6 +----- popup.html | 14 +++++++++++++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f1bb6dc22..e9ae4f5c7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -23,10 +23,6 @@ "message": "Secret Error. Only Base32(A-Z, 2-7 and =) and HEX(0-9 and A-F) are supported. However, your secret is: ", "description": "Secret Error." }, - "info": { - "message": "

Authenticator for Google™ Authenticator,
© 2018
Authenticator Extension. Released under the MIT License.

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

ZXing Copyright ZXing authors. Licensed under the Apache License.

totp.js Copyright its author. Licensed under the MIT License.

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

crypto-js Copyright crypto-js author. Licensed under the BSD License.

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

", - "description": "Information." - }, "add_qr": { "message": "Scan QR Code", "description": "Scan QR Code." diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 1bb4cde71..1d1eaaf15 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -23,10 +23,6 @@ "message": "密钥错误,仅支持Base32(A-Z,2-7及=)和HEX(0-9及A-F)格式,然而您的密钥是:", "description": "Secret Error." }, - "info": { - "message": "

Authenticator<\/strong> for Google™ Authenticator,© 2018 Authenticator Extension<\/a>. Released under the MIT License.<\/p>

Google™是Google的商标,Google Authenticator是Google的商品名,以上所有权归Google公司所有。<\/p>

jsqrcode<\/a>的所有权归jsqrcode的作者所有。以Apache协议授权。<\/p>

ZXing<\/a>的所有权归ZXing的作者所有。以Apache协议授权。<\/p>

totp.js<\/a>的所有权归其作者所有。以MIT协议授权。<\/p>

jsSHA<\/a>的所有权归jsSHA的作者所有。以BSD协议授权。<\/p>

qrcode.js<\/a>的所有权归qrcode.js作者所有。以MIT协议授权。<\/p>

crypto-js<\/a>的所有权归crypto-js作者所有。以BSD协议授权。<\/p>

Droid Sans Mono<\/a>的所有权归Steve Matteson所有。以Apache协议授权。<\/p>

Font Awesome<\/a>的所有权归Dave Gandy所有。以SIL OFL协议1.1授权。<\/p>

感谢 Mike Robinson<\/a> <3<\/p>", - "description": "Information." - }, "add_qr": { "message": "扫描QR码", "description": "Scan QR Code." @@ -204,7 +200,7 @@ "description": "Import backup file." }, "import_backup_code": { - "message": "导入备份代码", + "message": "导入备份文本", "description": "Import backup code." }, "dropbox_backup": { diff --git a/popup.html b/popup.html index 13dd4636e..785c64dd6 100644 --- a/popup.html +++ b/popup.html @@ -79,7 +79,19 @@

-
+
+

Authenticator for Google™ Authenticator,
© 2018
Authenticator Extension. Released under the MIT License.

+

Google™ is Google's trademark, Google Authenticator is Google’s trade name, those rights of ownership belong with Google Inc.

+

jsqrcode Copyright jsqrcode authors. Licensed under the Apache License.

+

ZXing Copyright ZXing authors. Licensed under the Apache License.

+

totp.js Copyright its author. Licensed under the MIT License.

+

jsSHA Copyright jsSHA authors. Licensed under the BSD License.

+

qrcode.js Copyright qrcode.js author. Licensed under the MIT License.

+

crypto-js Copyright crypto-js author. Licensed under the BSD License.

+

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

+
From 0e4bfab389f5238fe1a6b4740ef2f83f2a429f7b Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 22 Feb 2018 01:24:04 +0800 Subject: [PATCH 129/178] fix cookie parse bug --- src/ui/entry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ui/entry.ts b/src/ui/entry.ts index e2acc3d0e..44721d0af 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -88,7 +88,8 @@ function hasMatchedEntry(currentHost: string, entries: OTPEntry[]) { async function entry(_ui: UI) { const cookie = document.cookie; - const cookieMatch = cookie ? document.cookie.split('passphrase=') : null; + const cookieMatch = + cookie ? document.cookie.match(/passphrase=([^;]*)/) : null; const cachedPassphrase = cookieMatch && cookieMatch.length > 1 ? cookieMatch[1] : null; const cachedPassphraseLocalStorage = localStorage.encodedPhrase ? From 98d51091ead65d6b245759ad45b05e7e4558a4d9 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 22 Feb 2018 01:39:29 +0800 Subject: [PATCH 130/178] check password when import backuos --- _locales/en/messages.json | 4 ++++ _locales/zh_CN/messages.json | 4 ++++ css/import.css | 6 +++++ import.html | 43 +++++++++++++++++++----------------- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index e9ae4f5c7..7e5bfa50e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -226,5 +226,9 @@ "dropbox_risk": { "message": "Warning: backups saved in Dropbox are unencrypted. Use at your own risk.", "description": "Dropbox backup risk warning." + }, + "import_error_password": { + "message": "You must provide correct password to import backups.", + "description": "Error password warning when import backups." } } diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 1d1eaaf15..21975ca8d 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -226,5 +226,9 @@ "dropbox_risk": { "message": "警告:保存至Dropbox的备份均未备份,您需自担风险。", "description": "Dropbox backup risk warning." + }, + "import_error_password": { + "message": "您必须提供正确的密码才能导入备份。", + "description": "Error password warning when import backups." } } diff --git a/css/import.css b/css/import.css index 7cab0ad46..09141e489 100644 --- a/css/import.css +++ b/css/import.css @@ -93,3 +93,9 @@ button:hover, background: white; outline: none; } + +.error_password { + font-size: 18px; + color: gray; + text-align: center; +} \ No newline at end of file diff --git a/import.html b/import.html index 1da79050e..1b16c2d4c 100644 --- a/import.html +++ b/import.html @@ -19,30 +19,33 @@
-
- - - - -
-
- - -
-
-
- +
+
+ + + +
-
- - +
+ +
-
- - +
+
+ +
+
+ + +
+
+ + +
+
-
+
From 0cb04f028aed47be70fd9e57499ce699210d5056 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 22 Feb 2018 01:44:22 +0800 Subject: [PATCH 131/178] check password when import backups --- import.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/import.html b/import.html index 1b16c2d4c..7a9d76d55 100644 --- a/import.html +++ b/import.html @@ -45,7 +45,7 @@
-
+
{{ i18n.import_error_password }}
From 1d736122422b1b12bd5f13e26c606f5b89b46ed5 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Wed, 21 Feb 2018 12:08:07 -0600 Subject: [PATCH 132/178] Add newline at end of file --- css/import.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/import.css b/css/import.css index 09141e489..12b582b93 100644 --- a/css/import.css +++ b/css/import.css @@ -98,4 +98,4 @@ button:hover, font-size: 18px; color: gray; text-align: center; -} \ No newline at end of file +} From 869ad1bd7a554b44e191e8763a48c94cb522b01d Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 22 Feb 2018 02:36:54 +0800 Subject: [PATCH 133/178] cache password in background --- css/import.css | 11 +++++++++++ src/background.ts | 6 ++++++ src/ui/entry.ts | 40 ++++++++++++++++++++++++++-------------- src/ui/passphrase.ts | 9 +++++++-- 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/css/import.css b/css/import.css index 12b582b93..057d60793 100644 --- a/css/import.css +++ b/css/import.css @@ -1,3 +1,14 @@ +::-webkit-scrollbar { + width: 10px; + background: #EEE; +} + +::-webkit-scrollbar-thumb { + background-color: #AAA; + border: 2px solid #EEE; + border-radius: 5px; +} + [v-cloak] { display: none } #authenticator { diff --git a/src/background.ts b/src/background.ts index 8e385e7ef..2aba094bc 100644 --- a/src/background.ts +++ b/src/background.ts @@ -4,6 +4,8 @@ /// /// +let cachedPassphrase = ''; + chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (message.action === 'position') { if (!sender.tab) { @@ -12,6 +14,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { getQr( sender.tab, message.info.left, message.info.top, message.info.width, message.info.height, message.info.windowWidth, message.info.passphrase); + } else if (message.action === 'cachePassphrase') { + cachedPassphrase = message.value; + } else if (message.action === 'passphrase') { + sendResponse(cachedPassphrase); } }); diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 44721d0af..a2d74122e 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -86,22 +86,34 @@ function hasMatchedEntry(currentHost: string, entries: OTPEntry[]) { return false; } +async function getCachedPassphrase() { + return new Promise( + (resolve: (value: string) => void, reject: (reason: Error) => void) => { + const cookie = document.cookie; + const cookieMatch = + cookie ? document.cookie.match(/passphrase=([^;]*)/) : null; + const cachedPassphrase = + cookieMatch && cookieMatch.length > 1 ? cookieMatch[1] : null; + const cachedPassphraseLocalStorage = localStorage.encodedPhrase ? + CryptoJS.AES.decrypt(localStorage.encodedPhrase, '') + .toString(CryptoJS.enc.Utf8) : + ''; + if (cachedPassphrase || cachedPassphraseLocalStorage) { + return resolve(cachedPassphrase || cachedPassphraseLocalStorage); + } + + chrome.runtime.sendMessage( + {action: 'passphrase'}, (passphrase: string) => { + return resolve(passphrase); + }); + }); +} + async function entry(_ui: UI) { - const cookie = document.cookie; - const cookieMatch = - cookie ? document.cookie.match(/passphrase=([^;]*)/) : null; - const cachedPassphrase = - cookieMatch && cookieMatch.length > 1 ? cookieMatch[1] : null; - const cachedPassphraseLocalStorage = localStorage.encodedPhrase ? - CryptoJS.AES.decrypt(localStorage.encodedPhrase, '') - .toString(CryptoJS.enc.Utf8) : - ''; - const encryption: Encryption = - new Encryption(cachedPassphrase || cachedPassphraseLocalStorage || ''); + const cachedPassphrase = await getCachedPassphrase(); + const encryption: Encryption = new Encryption(cachedPassphrase); const shouldShowPassphrase = - (cachedPassphrase || cachedPassphraseLocalStorage) ? - false : - await EntryStorage.hasEncryptedEntry(); + cachedPassphrase ? false : await EntryStorage.hasEncryptedEntry(); const exportData = shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); const entries = shouldShowPassphrase ? [] : await getEntries(encryption); diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts index d0ead3a5a..e031da03a 100644 --- a/src/ui/passphrase.ts +++ b/src/ui/passphrase.ts @@ -2,6 +2,11 @@ /// /// +function cachePassword(password: string) { + document.cookie = 'passphrase=' + password; + chrome.runtime.sendMessage({action: 'cachePassphrase', value: password}); +} + async function passphrase(_ui: UI) { const ui: UIConfig = { data: {passphrase: ''}, @@ -11,7 +16,7 @@ async function passphrase(_ui: UI) { _ui.instance.passphrase); await _ui.instance.updateEntries(); _ui.instance.closeInfo(); - document.cookie = 'passphrase=' + _ui.instance.passphrase; + cachePassword(_ui.instance.passphrase); return; }, changePassphrase: async () => { @@ -22,7 +27,7 @@ async function passphrase(_ui: UI) { } _ui.instance.encryption.updateEncryptionPassword( _ui.instance.newPassphrase.phrase); - document.cookie = 'passphrase=' + _ui.instance.newPassphrase.phrase; + cachePassword(_ui.instance.newPassphrase.phrase); await _ui.instance.importEntries(); // remove cached passphrase in old version localStorage.removeItem('encodedPhrase'); From cdd972caf54dbf52ed1bcf6bacfedc1914515c80 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 22 Feb 2018 02:43:31 +0800 Subject: [PATCH 134/178] show password box for incorrect password --- src/ui/entry.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ui/entry.ts b/src/ui/entry.ts index a2d74122e..907dd1684 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -112,11 +112,19 @@ async function getCachedPassphrase() { async function entry(_ui: UI) { const cachedPassphrase = await getCachedPassphrase(); const encryption: Encryption = new Encryption(cachedPassphrase); - const shouldShowPassphrase = + let shouldShowPassphrase = cachedPassphrase ? false : await EntryStorage.hasEncryptedEntry(); const exportData = shouldShowPassphrase ? {} : await EntryStorage.getExport(encryption); const entries = shouldShowPassphrase ? [] : await getEntries(encryption); + + for (let i = 0; i < entries.length; i++) { + if (entries[i].code === 'Encrypted') { + shouldShowPassphrase = true; + break; + } + } + const exportFile = getBackupFile(exportData); const currentHost = await getCurrentHostname(); const shouldFilter = From 1d285ff470f5521109f288f0233d6fe704bbf76b Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 22 Feb 2018 02:49:46 +0800 Subject: [PATCH 135/178] username -> issuer --- _locales/en/messages.json | 6 +++--- _locales/zh_CN/messages.json | 6 +++--- popup.html | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7e5bfa50e..611f9474e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -55,9 +55,9 @@ "message": "Account Name", "description": "Account Name." }, - "username": { - "message": "Username", - "description": "Username." + "issuer": { + "message": "Issuer", + "description": "Issuer." }, "secret": { "message": "Secret", diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 21975ca8d..444167022 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -55,9 +55,9 @@ "message": "账户名称", "description": "Account Name." }, - "username": { - "message": "用户名", - "description": "Username." + "issuer": { + "message": "签发方", + "description": "Issuer." }, "secret": { "message": "密钥", diff --git a/popup.html b/popup.html index 785c64dd6..1a5899733 100644 --- a/popup.html +++ b/popup.html @@ -36,7 +36,7 @@
{{ entry.issuer.split('::')[0] }}
- +
From b97399e2b4cfc4c80f80a534dc2115f3fa30bd0e Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Wed, 21 Feb 2018 12:51:22 -0600 Subject: [PATCH 136/178] Style dropbox auth link --- css/popup.css | 8 +++----- popup.html | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/css/popup.css b/css/popup.css index d1bedd2b7..72b78ffee 100644 --- a/css/popup.css +++ b/css/popup.css @@ -348,6 +348,7 @@ body { #add_secret, #add_button, #download_backup, +#dropbox_authorization, #import_backup, #upload_backup, #security_save, @@ -625,6 +626,7 @@ body { #add_qr:hover, #upload_backup:hover, #download_backup:hover, +#dropbox_authorization:hover, #import_backup:hover, #editAction:hover, #infoAction:hover, @@ -769,6 +771,7 @@ body { } #download_backup, +#dropbox_authorization, #import_backup { text-decoration: none; color: gray; @@ -790,8 +793,3 @@ body { .no-copy { cursor: default; } - -#dropbox_authorization { - text-align: right; - padding: 0 10px; -} diff --git a/popup.html b/popup.html index 785c64dd6..079ef90ff 100644 --- a/popup.html +++ b/popup.html @@ -142,7 +142,7 @@ - + {{ i18n.dropbox_authorization }}
{{ i18n.ok }}
From 4f603e3657ed8d85e2d5e116c11d5536ccab41ae Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 22 Feb 2018 02:56:07 +0800 Subject: [PATCH 137/178] 5.0.2 --- manifest-chrome.json | 2 +- manifest-firefox.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-chrome.json b/manifest-chrome.json index ca0581345..e83fc7b36 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.1", + "version": "5.0.2", "default_locale": "en", "description": "__MSG_extDesc__", "icons": { diff --git a/manifest-firefox.json b/manifest-firefox.json index c6adf2a6a..28401c5d9 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.1", + "version": "5.0.2", "default_locale": "en", "description": "__MSG_extDesc__", "applications": { From c6f21a5000dc5246024379df98c97662b4d02701 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 24 Feb 2018 01:32:13 +0800 Subject: [PATCH 138/178] fix #34 #35 --- _locales/en/messages.json | 2 +- _locales/zh_CN/messages.json | 2 +- css/popup.css | 48 +++++++++++++++++++++++++++++++++--- popup.html | 9 +++++-- 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 611f9474e..66aec7944 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -216,7 +216,7 @@ "description": "Dropbox token." }, "dropbox_authorization": { - "message": "Dropbox Authorization", + "message": "Get Code", "description": "Dropbox authorization." }, "show_all_entries": { diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 444167022..b664e1e24 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -216,7 +216,7 @@ "description": "Dropbox token." }, "dropbox_authorization": { - "message": "Dropbox授权", + "message": "获取授权码", "description": "Dropbox authorization." }, "show_all_entries": { diff --git a/css/popup.css b/css/popup.css index 72b78ffee..66971c23f 100644 --- a/css/popup.css +++ b/css/popup.css @@ -114,6 +114,10 @@ box-sizing: border-box; } +a { + color: #08C; +} + body { width: 320px; height: 480px; @@ -348,7 +352,6 @@ body { #add_secret, #add_button, #download_backup, -#dropbox_authorization, #import_backup, #upload_backup, #security_save, @@ -626,7 +629,6 @@ body { #add_qr:hover, #upload_backup:hover, #download_backup:hover, -#dropbox_authorization:hover, #import_backup:hover, #editAction:hover, #infoAction:hover, @@ -771,7 +773,6 @@ body { } #download_backup, -#dropbox_authorization, #import_backup { text-decoration: none; color: gray; @@ -793,3 +794,44 @@ body { .no-copy { cursor: default; } + +#dropbox_box .dropbox_code_input { + width: 260px; + margin: 0 10px 10px 10px; + border: #CCC 1px solid; + background: white; + font-size: 0; +} + +#dropbox_box .dropbox_code_input input { + border: none; + margin: 0; + font-size: 12px; + width: 188px; + display: inline-block; +} + +#dropbox_authorization { + display: inline-block; + font-size: 12px; + width: 70px; + border-left: 1px solid #ccc; + padding: 3px; + text-decoration: none; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; + margin: 7px 0; +} + +#overlay { + position: absolute; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + top: 0; + left: 0; + z-index: 900; +} \ No newline at end of file diff --git a/popup.html b/popup.html index 83071721e..4017d009f 100644 --- a/popup.html +++ b/popup.html @@ -139,10 +139,12 @@
{{ i18n.dropbox_risk }}
- + - {{ i18n.dropbox_authorization }}
{{ i18n.ok }}
@@ -187,6 +189,9 @@
+ +
+
From b3a64172b8d9558aa106f40caf7b25c4a70e078d Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 24 Feb 2018 01:35:50 +0800 Subject: [PATCH 139/178] fix #33 --- popup.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/popup.html b/popup.html index 4017d009f..fc3f40d58 100644 --- a/popup.html +++ b/popup.html @@ -118,14 +118,14 @@ - +
{{ i18n.ok }}
{{ i18n.passphrase_info }}
- +
{{ i18n.ok }}
From 1b6ee8aea35d6125c4f5884b9929fd48a604752b Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 24 Feb 2018 01:45:51 +0800 Subject: [PATCH 140/178] make translate and source code menu clickable in entire area --- popup.html | 6 +++--- src/ui/menu.ts | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/popup.html b/popup.html index fc3f40d58..04137433d 100644 --- a/popup.html +++ b/popup.html @@ -67,9 +67,9 @@
Version {{ version }}
diff --git a/src/ui/menu.ts b/src/ui/menu.ts index 65d7a02f9..2bde53982 100644 --- a/src/ui/menu.ts +++ b/src/ui/menu.ts @@ -74,6 +74,10 @@ async function menu(_ui: UI) { const ui: UIConfig = { data: {version, zoom}, methods: { + openLink: (url: string) => { + window.open(url, '_blank'); + return; + }, showMenu: () => { _ui.instance.class.slidein = true; _ui.instance.class.slideout = false; From 908fa3f7f55bb8682a5d95650d88f637d654a88c Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 24 Feb 2018 01:54:58 +0800 Subject: [PATCH 141/178] add newline in popup.css --- css/popup.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index 66971c23f..f2e72d827 100644 --- a/css/popup.css +++ b/css/popup.css @@ -834,4 +834,4 @@ body { top: 0; left: 0; z-index: 900; -} \ No newline at end of file +} From 1e70c0961a63765e81b273087d776a8ea8cda61e Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 24 Feb 2018 02:21:04 +0800 Subject: [PATCH 142/178] handle menu text overflow --- css/popup.css | 7 +++++-- popup.html | 18 +++++++++--------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/css/popup.css b/css/popup.css index f2e72d827..01c8ea7f6 100644 --- a/css/popup.css +++ b/css/popup.css @@ -142,9 +142,9 @@ body { #notification { position: absolute; - left: 100px; + left: 60px; top: -1000px; - width: 120px; + width: 200px; height: 60px; line-height: 60px; text-align: center; @@ -549,6 +549,9 @@ body { font-size: 16px; color: gray; cursor: pointer; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } #menu .menuList p:hover { diff --git a/popup.html b/popup.html index 04137433d..0b89d2276 100644 --- a/popup.html +++ b/popup.html @@ -57,19 +57,19 @@
Version {{ version }}
From 3f83319af5d7014f4635922d3bb157d9f977a3da Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sat, 24 Feb 2018 02:24:51 +0800 Subject: [PATCH 143/178] add type select box padding --- css/popup.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index 01c8ea7f6..6350f0745 100644 --- a/css/popup.css +++ b/css/popup.css @@ -734,7 +734,8 @@ body { #secret_box select { margin: 20px; - font-size: 16px; + font-size: 12px; + padding: 5px; } #dropbox_box label, From 71c6e58cf4788cac230960ca797358fc255e35ac Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 15:07:20 -0600 Subject: [PATCH 144/178] Add ru translations --- _locales/ru/messages.json | 234 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 _locales/ru/messages.json diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json new file mode 100644 index 000000000..279320276 --- /dev/null +++ b/_locales/ru/messages.json @@ -0,0 +1,234 @@ +{ + "extName": { + "message": "Авторизация", + "description": "Extension Name." + }, + "extShortName": { + "message": "Авторизация", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "Аутентификатор генерирует коды подтверждения (Totp) 2-шаг в вашем браузере.", + "description": "Extension Description." + }, + "added": { + "message": " добавлено.", + "description": "Added Account." + }, + "errorqr": { + "message": "Unrecognized QR code.", + "description": "QR Error." + }, + "errorsecret": { + "message": "Секретный ошибка. Только Base32 (A-Z, 2-7 и =) и ШЕСТНАДЦАТЕРИЧНЫЙ (0-9 и A-F) поддерживаются. Однако ваш секрет: ", + "description": "Secret Error." + }, + "add_qr": { + "message": "Отсканировать QR-код", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "Ручной ввод", + "description": "Manual Entry." + }, + "close": { + "message": "Закрыть", + "description": "Close." + }, + "ok": { + "message": "Готово", + "description": "OK." + }, + "yes": { + "message": "Да", + "description": "Yes." + }, + "no": { + "message": "Нет", + "description": "No." + }, + "account": { + "message": "Account", + "description": "Account." + }, + "accountName": { + "message": "Имя учётной записи", + "description": "Account Name." + }, + "issuer": { + "message": "Издатель", + "description": "Issuer." + }, + "secret": { + "message": "Секрет", + "description": "Secret." + }, + "updateSuccess": { + "message": "Успех.", + "description": "Update Success." + }, + "updateFailure": { + "message": "Неудача.", + "description": "Update Failure." + }, + "about": { + "message": "О расширении", + "description": "About." + }, + "export_import": { + "message": "Экспортировать и импортировать", + "description": "Export and Import." + }, + "settings": { + "message": "Настройки", + "description": "Settings." + }, + "security": { + "message": "Настройки Безопасности", + "description": "Security." + }, + "current_phrase": { + "message": "Текущий пароль", + "description": "Current Passphrase." + }, + "new_phrase": { + "message": "Новый Пароль", + "description": "New Passphrase." + }, + "phrase": { + "message": "Пароль", + "description": "Passphrase." + }, + "confirm_phrase": { + "message": "Подтвердить Пароль", + "description": "Confirm Passphrase." + }, + "confirm_delete": { + "message": "Вы правда хотите удалить этот секрет? Это действие безвозвратное.", + "description": "Remove entry confirmation" + }, + "security_warning": { + "message": "Этот пароль будет использоваться для вашего секрета. Ни кто вам не поможет если вы его забудете.", + "description": "Passphrase Warning." + }, + "update": { + "message": "Обновить", + "description": "Update." + }, + "phrase_incorrect": { + "message": "Вы не можете добавить новую учетную запись или вывести данные, пока не будет выполнен вход во все учётные записи. Пожалуйста, введите правильный пароль, прежде чем продолжить.", + "description": "Passphrase Incorrect." + }, + "phrase_not_match": { + "message": "Пороли не совпадают.", + "description": "Passphrase Not Match." + }, + "encrypted": { + "message": "Encrypted", + "description": "Encrypted." + }, + "copied": { + "message": "Скопировано", + "description": "Copied." + }, + "feedback": { + "message": "Отзыв", + "description": "Feedback." + }, + "translate": { + "message": "Перевод", + "description": "Translate." + }, + "source": { + "message": "Исходный код", + "description": "Source Code." + }, + "passphrase_info": { + "message": "Введите пароль для доступа к данным учётной записи.", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "Синхронизировать время с Google", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "Запомнить пароль", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "Внимание! Ваши местные часы слишком далеко, пожалуйста, исправить перед продолжением.", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "Не ждите если у вас есть копия ваших секретов или иначе будет поздно!", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "Фокусировка не удалась, перезагрузите страницу и повторите попытку.", + "description": "Capture Failed" + }, + "based_on_time": { + "message": "Time Based", + "description": "Time Based" + }, + "based_on_counter": { + "message": "Counter Based", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "Изменение размера всплывающего окна", + "description": "Resize Popup Page" + }, + "scale": { + "message": "Масштаб", + "description": "Scale" + }, + "export_info": { + "message": "Внимание: все резервные копии не зашифрованы. Хотите добавить учетную запись в другое приложение? Наведите курсор на верхнюю правую часть любой учетной записи и нажмите скрытую кнопку.", + "description": "Export menu info text" + }, + "download_backup": { + "message": "Скачать резервную копию", + "description": "Download backup file." + }, + "import_backup": { + "message": "Импорт резервной копии", + "description": "Import backup." + }, + "import_backup_file": { + "message": "Импортировать резервный файл", + "description": "Import backup file." + }, + "import_backup_code": { + "message": "Импорт текста резервной копии", + "description": "Import backup code." + }, + "dropbox_backup": { + "message": "Автосохранение резервной копии в Dropbox", + "description": "Auto backup to Dropbox." + }, + "dropbox_code": { + "message": "Dropbox код", + "description": "Dropbox code." + }, + "dropbox_token": { + "message": "Dropbox маркер", + "description": "Dropbox token." + }, + "dropbox_authorization": { + "message": "Получить код", + "description": "Dropbox authorization." + }, + "show_all_entries": { + "message": "Показать все записи", + "description": "Show all entries." + }, + "dropbox_risk": { + "message": "Внимание: резервные копии, сохраненные в Dropbox, не зашифрованы. Используйте на свой страх и риск.", + "description": "Dropbox backup risk warning." + }, + "import_error_password": { + "message": "Для импорта резервных копий необходимо указать правильный пароль.", + "description": "Error password warning when import backups." + } +} \ No newline at end of file From d2f4c6061f9696730fe2d807453cf7582ad421c2 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 15:29:44 -0600 Subject: [PATCH 145/178] Add overflow scrolling to menu --- css/popup.css | 12 +++++++----- popup.html | 32 +++++++++++++++++--------------- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/css/popup.css b/css/popup.css index 6350f0745..14abac5ea 100644 --- a/css/popup.css +++ b/css/popup.css @@ -515,10 +515,16 @@ body { height: 480px; position: absolute; left: -1000px; - background: #EEE; top: 0; } +#menuBody { + overflow-y: scroll; + height: 442px; + background: #EEE; + position: absolute; +} + #menu.slidein { left: 0; animation: slidein 0.2s 1 ease-out; @@ -550,8 +556,6 @@ body { color: gray; cursor: pointer; text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; } #menu .menuList p:hover { @@ -824,8 +828,6 @@ body { text-decoration: none; text-align: center; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; vertical-align: top; margin: 7px 0; } diff --git a/popup.html b/popup.html index 0b89d2276..f3164d331 100644 --- a/popup.html +++ b/popup.html @@ -56,22 +56,24 @@ {{ i18n.settings }}
- - - From 739acc5bf70b1da5220e89e25c8120985975dd5a Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 16:22:09 -0600 Subject: [PATCH 146/178] CSS Grid the menu --- css/popup.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index 14abac5ea..a3e9e2557 100644 --- a/css/popup.css +++ b/css/popup.css @@ -555,7 +555,8 @@ body { font-size: 16px; color: gray; cursor: pointer; - text-overflow: ellipsis; + display: grid; + grid-template-columns: 30px auto; } #menu .menuList p:hover { From a2a5aa9d4a925c0dfacde4c5ccd48640f27be412 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 16:24:28 -0600 Subject: [PATCH 147/178] Center the icons --- css/popup.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/css/popup.css b/css/popup.css index a3e9e2557..6ac97a36d 100644 --- a/css/popup.css +++ b/css/popup.css @@ -579,6 +579,8 @@ body { font-size: 14px; display: line-block; width: 30px; + display: flex; + align-items: center; } #version { From ec8db920ffdf333e0bdb84ff3966cc811ec0544c Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 16:41:46 -0600 Subject: [PATCH 148/178] Fix Dropbox menu It really shouldn't be this hard to center text. --- css/popup.css | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/css/popup.css b/css/popup.css index 6ac97a36d..174c015b3 100644 --- a/css/popup.css +++ b/css/popup.css @@ -812,6 +812,7 @@ body { border: #CCC 1px solid; background: white; font-size: 0; + height: 42px; } #dropbox_box .dropbox_code_input input { @@ -820,10 +821,13 @@ body { font-size: 12px; width: 188px; display: inline-block; + height: 40px; + padding: 0px 0px 0px 10px; + vertical-align: top; } #dropbox_authorization { - display: inline-block; + display: inline-flex; font-size: 12px; width: 70px; border-left: 1px solid #ccc; @@ -832,7 +836,8 @@ body { text-align: center; overflow: hidden; vertical-align: top; - margin: 7px 0; + height: 40px; + align-items: center; } #overlay { From 17817407a9df43361d6cb007b6bc1f1688b67e20 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 16:50:27 -0600 Subject: [PATCH 149/178] Fix menu width --- css/popup.css | 1 + 1 file changed, 1 insertion(+) diff --git a/css/popup.css b/css/popup.css index 174c015b3..3c8fc43f3 100644 --- a/css/popup.css +++ b/css/popup.css @@ -521,6 +521,7 @@ body { #menuBody { overflow-y: scroll; height: 442px; + width: inherit; background: #EEE; position: absolute; } From 3f687733d8bb15c7cb0788500bb43b463f408664 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 17:09:44 -0600 Subject: [PATCH 150/178] Fix import.html alignment --- css/import.css | 8 ++++---- import.html | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/css/import.css b/css/import.css index 057d60793..3629a2deb 100644 --- a/css/import.css +++ b/css/import.css @@ -29,10 +29,10 @@ .import_tab label { width: 250px; height: 50px; - line-height: 50px; font-size: 18px; text-align: center; - display: inline-block; + display: inline-grid; + align-items: center; margin: 20px; cursor: pointer; border-radius: 2px; @@ -53,15 +53,15 @@ button, .import_file label { - display: inline-block; + display: inline-grid; width: 260px; height: 60px; - line-height: 60px; border: #CCC 1px solid; background: white; border-radius: 2px; position: relative; text-align: center; + align-items: center; font-size: 16px; color: gray; cursor: pointer; diff --git a/import.html b/import.html index 7a9d76d55..41b132853 100644 --- a/import.html +++ b/import.html @@ -1,3 +1,4 @@ + From 31a28cfb45abb4d5c9b352b5eb4df7f4b018a1f7 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 18:30:28 -0600 Subject: [PATCH 151/178] Update README.md --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 81b49c22b..5e55fc0e8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ + + # Authenticator [![Build Status](https://travis-ci.org/Authenticator-Extension/Authenticator.svg?branch=dev)](https://travis-ci.org/Authenticator-Extension/Authenticator) [![Crowdin](https://d322cqt584bo4o.cloudfront.net/authenticator-firefox/localized.svg)](https://crowdin.com/project/authenticator-firefox) > Authenticator generates 2-Step Verification codes in your browser. @@ -22,6 +24,8 @@ Compile for development: ``` bash # install typescript npm install -g typescript +#install gts +npm install -g gts # install dependencies npm install # check typescript style @@ -31,9 +35,3 @@ gts fix # compile npm run compile ``` - -## FAQ - -### gts is not found - -gts (Google TypeScript style) is installed locally by default, see to add local node modules into path, or run `npm install -g gts` to install gts global. From aebe94935be6a6a2039e4f32fcf5fd3e835156a9 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 23 Feb 2018 18:36:00 -0600 Subject: [PATCH 152/178] Capitalization --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5e55fc0e8..c679b6706 100644 --- a/README.md +++ b/README.md @@ -6,13 +6,13 @@ ## Build Setup -Compile for chrome: +Compile for Chrome: ```bash npm install npm run chrome ``` -Compile for firefox: +Compile for Firefox: ```bash npm install From b34de552647ac1e880fb57877db47fc032fa6454 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 24 Feb 2018 12:50:38 -0600 Subject: [PATCH 153/178] I don't know how or why but I did it. --- css/popup.css | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/css/popup.css b/css/popup.css index 3c8fc43f3..f3aa80079 100644 --- a/css/popup.css +++ b/css/popup.css @@ -519,7 +519,7 @@ body { } #menuBody { - overflow-y: scroll; + overflow-y: auto; height: 442px; width: inherit; background: #EEE; @@ -814,6 +814,8 @@ body { background: white; font-size: 0; height: 42px; + display: grid; + grid-template-columns: auto auto; } #dropbox_box .dropbox_code_input input { @@ -830,15 +832,15 @@ body { #dropbox_authorization { display: inline-flex; font-size: 12px; - width: 70px; border-left: 1px solid #ccc; padding: 3px; text-decoration: none; text-align: center; overflow: hidden; vertical-align: top; - height: 40px; + margin: 7px 0; align-items: center; + padding-left: 5px; } #overlay { From a69eee125ccf52e1031d74507083ec02299165b6 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 24 Feb 2018 13:06:00 -0600 Subject: [PATCH 154/178] Hover colors --- css/popup.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/css/popup.css b/css/popup.css index f3aa80079..24c41b8a7 100644 --- a/css/popup.css +++ b/css/popup.css @@ -841,6 +841,11 @@ body { margin: 7px 0; align-items: center; padding-left: 5px; + color: gray; +} + +#dropbox_authorization:hover { + color: black; } #overlay { From 5abd4377593e367b65eddff27133fef4c842f482 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 24 Feb 2018 18:59:24 -0600 Subject: [PATCH 155/178] Add Japanese translations --- _locales/ja/messages.json | 234 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 _locales/ja/messages.json diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json new file mode 100644 index 000000000..5c9f93b65 --- /dev/null +++ b/_locales/ja/messages.json @@ -0,0 +1,234 @@ +{ + "extName": { + "message": "Authenticator", + "description": "Extension Name." + }, + "extShortName": { + "message": "Authenticator", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "Authenticator generates 2-Step Verification codes in your browser.", + "description": "Extension Description." + }, + "added": { + "message": " 追加されました。", + "description": "Added Account." + }, + "errorqr": { + "message": "QR コードが認識できません。", + "description": "QR Error." + }, + "errorsecret": { + "message": "Secret Error. Only Base32(A-Z, 2-7 and =) and HEX(0-9 and A-F) are supported. However, your secret is: ", + "description": "Secret Error." + }, + "add_qr": { + "message": "QRコードをスキャン", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "手動入力", + "description": "Manual Entry." + }, + "close": { + "message": "閉じる", + "description": "Close." + }, + "ok": { + "message": "OK", + "description": "OK." + }, + "yes": { + "message": "はい", + "description": "Yes." + }, + "no": { + "message": "いいえ", + "description": "No." + }, + "account": { + "message": "アカウント", + "description": "Account." + }, + "accountName": { + "message": "アカウント名", + "description": "Account Name." + }, + "issuer": { + "message": "発行者", + "description": "Issuer." + }, + "secret": { + "message": "シークレット", + "description": "Secret." + }, + "updateSuccess": { + "message": "成功しました。", + "description": "Update Success." + }, + "updateFailure": { + "message": "失敗しました。", + "description": "Update Failure." + }, + "about": { + "message": "このアプリついて", + "description": "About." + }, + "export_import": { + "message": "エクスポート\/インポート", + "description": "Export and Import." + }, + "settings": { + "message": "設定", + "description": "Settings." + }, + "security": { + "message": "セキュリティ", + "description": "Security." + }, + "current_phrase": { + "message": "現在のパスワード", + "description": "Current Passphrase." + }, + "new_phrase": { + "message": "新しいパスワード", + "description": "New Passphrase." + }, + "phrase": { + "message": "パスワード", + "description": "Passphrase." + }, + "confirm_phrase": { + "message": "パスワードの再入力", + "description": "Confirm Passphrase." + }, + "confirm_delete": { + "message": "Are you sure you want to delete this secret? This action cannot be undone.", + "description": "Remove entry confirmation" + }, + "security_warning": { + "message": "This password will be used to encrypt your secrets. No one can help you if you forget the password.", + "description": "Passphrase Warning." + }, + "update": { + "message": "更新", + "description": "Update." + }, + "phrase_incorrect": { + "message": "You cannot add a new account or export data until all accounts are decrypted. Please enter the correct password before continuing.", + "description": "Passphrase Incorrect." + }, + "phrase_not_match": { + "message": "パスワードが一致しません。", + "description": "Passphrase Not Match." + }, + "encrypted": { + "message": "暗号化", + "description": "Encrypted." + }, + "copied": { + "message": "コピーしました。", + "description": "Copied." + }, + "feedback": { + "message": "ご意見", + "description": "Feedback." + }, + "translate": { + "message": "翻訳", + "description": "Translate." + }, + "source": { + "message": "ソース コード", + "description": "Source Code." + }, + "passphrase_info": { + "message": "Enter password to decrypt account data.", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "Sync Clock with Google", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "パスワードを記憶します。", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "Caution! Your local clock is too far off, please fix it before continuing.", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "Do you have a backup for your secrets? Don't wait until it's too late!", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "Capture failed, please reload the page and try again.", + "description": "Capture Failed" + }, + "based_on_time": { + "message": "タイム ベース", + "description": "Time Based" + }, + "based_on_counter": { + "message": "カウンター ベース", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "Resize Popup Page", + "description": "Resize Popup Page" + }, + "scale": { + "message": "Scale", + "description": "Scale" + }, + "export_info": { + "message": "Warning: all backups are unencrypted. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", + "description": "Export menu info text" + }, + "download_backup": { + "message": "バックアップファイルのダウンロード", + "description": "Download backup file." + }, + "import_backup": { + "message": "Import Backup", + "description": "Import backup." + }, + "import_backup_file": { + "message": "Import Backup File", + "description": "Import backup file." + }, + "import_backup_code": { + "message": "Import Text Backup", + "description": "Import backup code." + }, + "dropbox_backup": { + "message": "Dropbox へ自動バックアップ", + "description": "Auto backup to Dropbox." + }, + "dropbox_code": { + "message": "Dropbox Code", + "description": "Dropbox code." + }, + "dropbox_token": { + "message": "Dropbox Token", + "description": "Dropbox token." + }, + "dropbox_authorization": { + "message": "Get Code", + "description": "Dropbox authorization." + }, + "show_all_entries": { + "message": "Show all entries", + "description": "Show all entries." + }, + "dropbox_risk": { + "message": "Warning: backups saved in Dropbox are unencrypted. Use at your own risk.", + "description": "Dropbox backup risk warning." + }, + "import_error_password": { + "message": "You must provide correct password to import backups.", + "description": "Error password warning when import backups." + } +} \ No newline at end of file From bbec4eea4237103d01fb24b3d60d9d6e560809d5 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 24 Feb 2018 19:05:24 -0600 Subject: [PATCH 156/178] Bring up to date w/ crowdin --- _locales/ja/messages.json | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 5c9f93b65..47044f5d4 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -8,7 +8,7 @@ "description": "Extension Short Name." }, "extDesc": { - "message": "Authenticator generates 2-Step Verification codes in your browser.", + "message": "Authenticator はお使いのブラウザーで2段階認証コードを生成します。", "description": "Extension Description." }, "added": { @@ -20,7 +20,7 @@ "description": "QR Error." }, "errorsecret": { - "message": "Secret Error. Only Base32(A-Z, 2-7 and =) and HEX(0-9 and A-F) are supported. However, your secret is: ", + "message": "認証コードが間違っています。Base32 (A-Z, 2-7, =) と HEX (0-9, A-F) のみ使用できます。認証コード: ", "description": "Secret Error." }, "add_qr": { @@ -104,11 +104,11 @@ "description": "Confirm Passphrase." }, "confirm_delete": { - "message": "Are you sure you want to delete this secret? This action cannot be undone.", + "message": "この認証を削除しても宜しいですか? この操作を元に戻すことはできません。", "description": "Remove entry confirmation" }, "security_warning": { - "message": "This password will be used to encrypt your secrets. No one can help you if you forget the password.", + "message": "このパスワードは認証を暗号化するために使用されます。パスワードを紛失した場合には復旧できませんのでお気をつけください。", "description": "Passphrase Warning." }, "update": { @@ -116,7 +116,7 @@ "description": "Update." }, "phrase_incorrect": { - "message": "You cannot add a new account or export data until all accounts are decrypted. Please enter the correct password before continuing.", + "message": "すべてのアカウントが解読されるまで、新しいアカウントを追加したり、データをエクスポートすることはできません。続行する前に正しいパスワードを入力してください。", "description": "Passphrase Incorrect." }, "phrase_not_match": { @@ -144,11 +144,11 @@ "description": "Source Code." }, "passphrase_info": { - "message": "Enter password to decrypt account data.", + "message": "アカウントデータを復号化するパスワードを入力してください。", "description": "Passphrase Info" }, "sync_clock": { - "message": "Sync Clock with Google", + "message": "Google と時刻を同期", "description": "Sync Clock" }, "remember_phrase": { @@ -156,15 +156,15 @@ "description": "Remember Passphrase" }, "clock_too_far_off": { - "message": "Caution! Your local clock is too far off, please fix it before continuing.", + "message": "危険!お使いの環境の日時が実際の日時と離れすぎています。続ける前にまずは修正してください。", "description": "Local Time is Too Far Off" }, "remind_backup": { - "message": "Do you have a backup for your secrets? Don't wait until it's too late!", + "message": "認証のバックアップは取っていますか?忘れないうちにぜひ!", "description": "Remind Backup" }, "capture_failed": { - "message": "Capture failed, please reload the page and try again.", + "message": "キャプチャーできませんでした。再読込してもう一度お試しください。", "description": "Capture Failed" }, "based_on_time": { @@ -176,15 +176,15 @@ "description": "Counter Based" }, "resize_popup_page": { - "message": "Resize Popup Page", + "message": "ポップアップのサイズを変更", "description": "Resize Popup Page" }, "scale": { - "message": "Scale", + "message": "大きさ", "description": "Scale" }, "export_info": { - "message": "Warning: all backups are unencrypted. Want to add an account to another app? Hover over the top right part of any account and hit the hidden button.", + "message": "警告: バックアップはすべて暗号化されていません。別のアプリにアカウントを追加したい場合、アカウントの右上にカーソルを合わせ、非表示のボタンを押してください。", "description": "Export menu info text" }, "download_backup": { @@ -192,15 +192,15 @@ "description": "Download backup file." }, "import_backup": { - "message": "Import Backup", + "message": "バックアップのインポート", "description": "Import backup." }, "import_backup_file": { - "message": "Import Backup File", + "message": "バックアップファイルのインポート", "description": "Import backup file." }, "import_backup_code": { - "message": "Import Text Backup", + "message": "テキストのバックアップのインポート", "description": "Import backup code." }, "dropbox_backup": { @@ -208,27 +208,27 @@ "description": "Auto backup to Dropbox." }, "dropbox_code": { - "message": "Dropbox Code", + "message": "Dropboxのコード", "description": "Dropbox code." }, "dropbox_token": { - "message": "Dropbox Token", + "message": "Dropboxのトークン", "description": "Dropbox token." }, "dropbox_authorization": { - "message": "Get Code", + "message": "コードを取得", "description": "Dropbox authorization." }, "show_all_entries": { - "message": "Show all entries", + "message": "すべての登録を表示", "description": "Show all entries." }, "dropbox_risk": { - "message": "Warning: backups saved in Dropbox are unencrypted. Use at your own risk.", + "message": "警告: Dropbox に保存したバックアップは暗号化されていません。お気をつけください。", "description": "Dropbox backup risk warning." }, "import_error_password": { - "message": "You must provide correct password to import backups.", + "message": "バックアップをインポートするために正しいパスワードを入力してください。", "description": "Error password warning when import backups." } } \ No newline at end of file From ea03d1de6f760728cc79bca7b8b38c064c0ed7a9 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 24 Feb 2018 19:14:41 -0600 Subject: [PATCH 157/178] Add font to import.html Closes #40 --- css/import.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/css/import.css b/css/import.css index 057d60793..a99d3f4f4 100644 --- a/css/import.css +++ b/css/import.css @@ -11,6 +11,10 @@ [v-cloak] { display: none } +* { + font-family: arial, 'Microsoft YaHei'; +} + #authenticator { width: 600px; position: relative; From 6c70824307debe2e37f6235e7b8cf2b1533eac21 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 25 Feb 2018 12:12:33 +0800 Subject: [PATCH 158/178] add alert method --- ensureDir.js | 2 +- popup.html | 8 ++++---- src/models/encryption.ts | 2 +- src/models/interface.ts | 2 +- src/models/otp.ts | 2 +- src/popup.ts | 4 ++-- src/ui/add-account.ts | 8 ++++---- src/ui/class.ts | 2 +- src/ui/entry.ts | 6 +++--- src/ui/info.ts | 4 ++-- src/ui/menu.ts | 2 +- src/ui/message.ts | 14 ++++++++++++-- src/ui/passphrase.ts | 2 +- src/ui/qr.ts | 2 +- src/ui/ui.ts | 2 +- 15 files changed, 36 insertions(+), 26 deletions(-) diff --git a/ensureDir.js b/ensureDir.js index 1fc436de4..42fa5e182 100644 --- a/ensureDir.js +++ b/ensureDir.js @@ -15,4 +15,4 @@ try { } catch(error) { console.error(error); process.exit(); -} \ No newline at end of file +} diff --git a/popup.html b/popup.html index f3164d331..948628a92 100644 --- a/popup.html +++ b/popup.html @@ -171,9 +171,9 @@
-
-
{{ message }}
-
{{ i18n.ok }}
+
+
{{ message.length ? message[0] : '' }}
+
{{ i18n.ok }}
@@ -192,7 +192,7 @@
-
+
diff --git a/src/models/encryption.ts b/src/models/encryption.ts index c6ba0a8fb..73a6f0d95 100644 --- a/src/models/encryption.ts +++ b/src/models/encryption.ts @@ -36,4 +36,4 @@ class Encryption { updateEncryptionPassword(password: string) { this.password = password; } -} \ No newline at end of file +} diff --git a/src/models/interface.ts b/src/models/interface.ts index eca3a77dc..f108cbae9 100644 --- a/src/models/interface.ts +++ b/src/models/interface.ts @@ -53,4 +53,4 @@ interface UIConfig { }; /* tslint:disable-next-line:no-any */ ready?: (...arg: any[]) => any; -} \ No newline at end of file +} diff --git a/src/models/otp.ts b/src/models/otp.ts index 806939894..bb3ed3507 100644 --- a/src/models/otp.ts +++ b/src/models/otp.ts @@ -65,4 +65,4 @@ class OTPEntry implements OTP { } } } -} \ No newline at end of file +} diff --git a/src/popup.ts b/src/popup.ts index 5714116d9..64847a1e8 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -69,11 +69,11 @@ async function init() { // ignore } } - authenticator.message = authenticator.i18n.remind_backup; + authenticator.alert(authenticator.i18n.remind_backup); localStorage.lastRemindingBackupTime = clientTime; }); } else { - authenticator.message = authenticator.i18n.remind_backup; + authenticator.alert(authenticator.i18n.remind_backup); localStorage.lastRemindingBackupTime = clientTime; } } diff --git a/src/ui/add-account.ts b/src/ui/add-account.ts index 95b1ad548..fc3b9ab53 100644 --- a/src/ui/add-account.ts +++ b/src/ui/add-account.ts @@ -47,7 +47,7 @@ async function addAccount(_ui: UI) { // shouldn't add new account with // the current passphrase if (entries[i].code === 'Encrypted') { - _ui.instance.message = _ui.instance.i18n.phrase_incorrect; + _ui.instance.alert(_ui.instance.i18n.phrase_incorrect); return; } } @@ -61,7 +61,7 @@ async function addAccount(_ui: UI) { tab.id, {action: 'capture', passphrase: _ui.instance.passphrase}, (result) => { if (result !== 'beginCapture') { - _ui.instance.message = _ui.instance.i18n.capture_failed; + _ui.instance.alert(_ui.instance.i18n.capture_failed); } else { window.close(); } @@ -77,7 +77,7 @@ async function addAccount(_ui: UI) { // shouldn't add new account with // the current passphrase if (entries[i].code === 'Encrypted') { - _ui.instance.message = _ui.instance.i18n.phrase_incorrect; + _ui.instance.alert(_ui.instance.i18n.phrase_incorrect); return; } } @@ -88,4 +88,4 @@ async function addAccount(_ui: UI) { }; _ui.update(ui); -} \ No newline at end of file +} diff --git a/src/ui/class.ts b/src/ui/class.ts index de34d8545..41a38dd3a 100644 --- a/src/ui/class.ts +++ b/src/ui/class.ts @@ -22,4 +22,4 @@ async function className(_ui: UI) { }; _ui.update(ui); -} \ No newline at end of file +} diff --git a/src/ui/entry.ts b/src/ui/entry.ts index 907dd1684..aa795bebb 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -220,7 +220,7 @@ async function entry(_ui: UI) { await EntryStorage.import( _ui.instance.encryption, JSON.parse(_ui.instance.exportData)); await _ui.instance.updateEntries(); - _ui.instance.message = _ui.instance.i18n.updateSuccess; + _ui.instance.alert(_ui.instance.i18n.updateSuccess); return; }, updateEntries: async () => { @@ -243,14 +243,14 @@ async function entry(_ui: UI) { const importData = JSON.parse(reader.result); await EntryStorage.import(_ui.instance.encryption, importData); await _ui.instance.updateEntries(); - _ui.instance.message = _ui.instance.i18n.updateSuccess; + _ui.instance.alert(_ui.instance.i18n.updateSuccess); if (closeWindow) { window.close(); } }; reader.readAsText(target.files[0]); } else { - _ui.instance.message = _ui.instance.i18n.updateFailure; + _ui.instance.alert(_ui.instance.i18n.updateFailure); if (closeWindow) { window.alert(_ui.instance.i18n.updateFailure); window.close(); diff --git a/src/ui/info.ts b/src/ui/info.ts index cfd1131ca..9eab6d32b 100644 --- a/src/ui/info.ts +++ b/src/ui/info.ts @@ -26,7 +26,7 @@ async function info(_ui: UI) { // cannot export account data // or change passphrase if (entries[i].code === 'Encrypted') { - _ui.instance.message = _ui.instance.i18n.phrase_incorrect; + _ui.instance.alert(_ui.instance.i18n.phrase_incorrect); return; } } @@ -62,4 +62,4 @@ async function info(_ui: UI) { }; _ui.update(ui); -} \ No newline at end of file +} diff --git a/src/ui/menu.ts b/src/ui/menu.ts index 2bde53982..8f444b95f 100644 --- a/src/ui/menu.ts +++ b/src/ui/menu.ts @@ -112,7 +112,7 @@ async function menu(_ui: UI) { {origins: ['https://www.google.com/']}, async (granted) => { if (granted) { const message = await syncTimeWithGoogle(); - _ui.instance.message = _ui.instance.i18n[message]; + _ui.instance.alert(_ui.instance.i18n[message]); } return; }); diff --git a/src/ui/message.ts b/src/ui/message.ts index e4e03ddb7..330870b3e 100644 --- a/src/ui/message.ts +++ b/src/ui/message.ts @@ -8,8 +8,18 @@ function isCustomEvent(event: Event): event is CustomEvent { async function message(_ui: UI) { const ui: UIConfig = { - data: {message: '', confirmMessage: ''}, + data: {message: [], messageIdle: true, confirmMessage: ''}, methods: { + alert: (message: string) => { + _ui.instance.message.unshift(message); + }, + closeAlert: () => { + _ui.instance.messageIdle = false; + _ui.instance.message.shift(); + setTimeout(() => { + _ui.instance.messageIdle = true; + }, 200); + }, confirm: async (message: string) => { return new Promise( (resolve: (value: boolean) => void, @@ -39,4 +49,4 @@ async function message(_ui: UI) { }; _ui.update(ui); -} \ No newline at end of file +} diff --git a/src/ui/passphrase.ts b/src/ui/passphrase.ts index e031da03a..1d2cf1dfc 100644 --- a/src/ui/passphrase.ts +++ b/src/ui/passphrase.ts @@ -22,7 +22,7 @@ async function passphrase(_ui: UI) { changePassphrase: async () => { if (_ui.instance.newPassphrase.phrase !== _ui.instance.newPassphrase.confirm) { - _ui.instance.message = _ui.instance.i18n.phrase_not_match; + _ui.instance.alert(_ui.instance.i18n.phrase_not_match); return; } _ui.instance.encryption.updateEncryptionPassword( diff --git a/src/ui/qr.ts b/src/ui/qr.ts index a45570963..ff7542320 100644 --- a/src/ui/qr.ts +++ b/src/ui/qr.ts @@ -59,4 +59,4 @@ async function qr(_ui: UI) { }; _ui.update(ui); -} \ No newline at end of file +} diff --git a/src/ui/ui.ts b/src/ui/ui.ts index 806267d64..9d45a5e0d 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -65,4 +65,4 @@ class UI { }, 1000); return this.instance; } -} \ No newline at end of file +} From 61a842db836013b41620e60dff30e5cd8b4072f3 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 24 Feb 2018 23:40:45 -0600 Subject: [PATCH 159/178] Add localStorage password warning (#38) Add localStorage password warning --- _locales/en/messages.json | 4 ++++ src/popup.ts | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 66aec7944..269eb466d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -230,5 +230,9 @@ "import_error_password": { "message": "You must provide correct password to import backups.", "description": "Error password warning when import backups." + }, + "local_passphrase_warning": { + "message": "Your password is stored locally, please change it in the security menu immediately.", + "description": "localStorage password warning." } } diff --git a/src/popup.ts b/src/popup.ts index 64847a1e8..b17c2b04b 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -30,6 +30,11 @@ async function init() { authenticator.showInfo('passphrase'); } + // localStorage passphrase warning + if (localStorage.encodedPhrase) { + authenticator.alert(authenticator.i18n.local_passphrase_warning); + } + // Remind backup const backupReminder = setInterval(() => { if (authenticator.entries.length === 0) { From 840efd1daf09bed2e7b9c080d9ad806841bbf0d3 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 24 Feb 2018 23:56:36 -0600 Subject: [PATCH 160/178] Add border --- css/popup.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index 24c41b8a7..8ff0df326 100644 --- a/css/popup.css +++ b/css/popup.css @@ -776,7 +776,8 @@ body { position: absolute; width: 300px; padding: 10px; - border: gray; + border: gray 1px solid; + border-radius: 2px; background: white; left: 10px; top: 150px; From 575e977559644e7aa36aae0a032617b81b064e8a Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 24 Feb 2018 23:57:34 -0600 Subject: [PATCH 161/178] Make overlay transparent --- css/popup.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index 8ff0df326..a5eee60cc 100644 --- a/css/popup.css +++ b/css/popup.css @@ -853,7 +853,7 @@ body { position: absolute; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0); top: 0; left: 0; z-index: 900; From 0a7dc190e62ae45881e9482103d58d57c2e8b9a9 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Sun, 25 Feb 2018 14:08:12 +0800 Subject: [PATCH 162/178] add local_passphrase_warning for chinese --- _locales/zh_CN/messages.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index b664e1e24..143bb51cf 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -230,5 +230,9 @@ "import_error_password": { "message": "您必须提供正确的密码才能导入备份。", "description": "Error password warning when import backups." + }, + "local_passphrase_warning": { + "message": "您的密码保存在了本地,请立即通过安全菜单更改密码。", + "description": "localStorage password warning." } } From 15f2f41304ac57d46fca47940f119b9b425e3f8c Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sun, 25 Feb 2018 11:10:36 -0600 Subject: [PATCH 163/178] Remove background --- css/popup.css | 1 - 1 file changed, 1 deletion(-) diff --git a/css/popup.css b/css/popup.css index a5eee60cc..7dd3e018e 100644 --- a/css/popup.css +++ b/css/popup.css @@ -853,7 +853,6 @@ body { position: absolute; width: 100%; height: 100%; - background: rgba(0, 0, 0, 0); top: 0; left: 0; z-index: 900; From 60ef59bcec691b07471c320fba5823d2a7567fff Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sun, 25 Feb 2018 11:35:41 -0600 Subject: [PATCH 164/178] Add zh_TW --- _locales/zh_TW/messages.json | 238 +++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 _locales/zh_TW/messages.json diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json new file mode 100644 index 000000000..a2c4b925b --- /dev/null +++ b/_locales/zh_TW/messages.json @@ -0,0 +1,238 @@ +{ + "extName": { + "message": "Authenticator", + "description": "Extension Name." + }, + "extShortName": { + "message": "Authenticator", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "Authenticator用以在瀏覽器中生成二步認證程式碼。", + "description": "Extension Description." + }, + "added": { + "message": "已新增。", + "description": "Added Account." + }, + "errorqr": { + "message": "無法識別的QR碼。", + "description": "QR Error." + }, + "errorsecret": { + "message": "金鑰錯誤,僅支援Base32(A-Z,2-7及=)和HEX(0-9及A-F)格式,然而您的金鑰是:", + "description": "Secret Error." + }, + "add_qr": { + "message": "掃描QR碼", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "手動輸入", + "description": "Manual Entry." + }, + "close": { + "message": "關閉", + "description": "Close." + }, + "ok": { + "message": "確定", + "description": "OK." + }, + "yes": { + "message": "是", + "description": "Yes." + }, + "no": { + "message": "否", + "description": "No." + }, + "account": { + "message": "賬戶", + "description": "Account." + }, + "accountName": { + "message": "賬戶名稱", + "description": "Account Name." + }, + "issuer": { + "message": "簽發方", + "description": "Issuer." + }, + "secret": { + "message": "金鑰", + "description": "Secret." + }, + "updateSuccess": { + "message": "成功。", + "description": "Update Success." + }, + "updateFailure": { + "message": "失敗。", + "description": "Update Failure." + }, + "about": { + "message": "關於", + "description": "About." + }, + "export_import": { + "message": "匯出 \/ 匯入", + "description": "Export and Import." + }, + "settings": { + "message": "設定", + "description": "Settings." + }, + "security": { + "message": "安全", + "description": "Security." + }, + "current_phrase": { + "message": "當前密碼", + "description": "Current Passphrase." + }, + "new_phrase": { + "message": "新密碼", + "description": "New Passphrase." + }, + "phrase": { + "message": "密碼", + "description": "Passphrase." + }, + "confirm_phrase": { + "message": "確認密碼", + "description": "Confirm Passphrase." + }, + "confirm_delete": { + "message": "您確定要刪除此金鑰嗎?此操作無法撤銷。", + "description": "Remove entry confirmation" + }, + "security_warning": { + "message": "您的金鑰將使用此密碼進行加密。如果您忘記了密碼沒有人能夠提供幫助。", + "description": "Passphrase Warning." + }, + "update": { + "message": "更新", + "description": "Update." + }, + "phrase_incorrect": { + "message": "部分賬戶與密碼不匹配,您無法新增新賬戶、匯出賬戶資料或者更改密碼。請提供正確的密碼後重試。", + "description": "Passphrase Incorrect." + }, + "phrase_not_match": { + "message": "兩次密碼不一致。", + "description": "Passphrase Not Match." + }, + "encrypted": { + "message": "已加密", + "description": "Encrypted." + }, + "copied": { + "message": "已複製", + "description": "Copied." + }, + "feedback": { + "message": "問題反饋", + "description": "Feedback." + }, + "translate": { + "message": "參與翻譯", + "description": "Translate." + }, + "source": { + "message": "原始碼", + "description": "Source Code." + }, + "passphrase_info": { + "message": "輸入密碼以解碼賬戶資料。", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "通過Google校準時間", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "記住密碼", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "注意!您的本地時鐘時間差過大,請修正後再進行操作。", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "您是否為金鑰建立了備份?不要等到為時已晚。", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "捕捉失敗,請過載您正在瀏覽的頁面後重試。", + "description": "Capture Failed" + }, + "based_on_time": { + "message": "基於時間", + "description": "Time Based" + }, + "based_on_counter": { + "message": "基於計數器", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "調整彈出頁面尺寸", + "description": "Resize Popup Page" + }, + "scale": { + "message": "比例", + "description": "Scale" + }, + "export_info": { + "message": "警告:所有備份均未加密。想要將賬號新增至其他應用?請點選賬號右上角隱藏的圖示。", + "description": "Export menu info text" + }, + "download_backup": { + "message": "下載備份檔案", + "description": "Download backup file." + }, + "import_backup": { + "message": "匯入備份", + "description": "Import backup." + }, + "import_backup_file": { + "message": "匯入備份檔案", + "description": "Import backup file." + }, + "import_backup_code": { + "message": "匯入備份文字", + "description": "Import backup code." + }, + "dropbox_backup": { + "message": "自動備份至Dropbox", + "description": "Auto backup to Dropbox." + }, + "dropbox_code": { + "message": "Dropbox授權碼", + "description": "Dropbox code." + }, + "dropbox_token": { + "message": "Dropbox Token", + "description": "Dropbox token." + }, + "dropbox_authorization": { + "message": "獲取授權碼", + "description": "Dropbox authorization." + }, + "show_all_entries": { + "message": "顯示全部條目", + "description": "Show all entries." + }, + "dropbox_risk": { + "message": "警告:儲存至Dropbox的備份均未備份,您需自擔風險。", + "description": "Dropbox backup risk warning." + }, + "import_error_password": { + "message": "您必須提供正確的密碼才能匯入備份。", + "description": "Error password warning when import backups." + }, + "local_passphrase_warning": { + "message": "您的密碼儲存在了本地,請立即通過安全選單更改密碼。", + "description": "localStorage password warning." + } +} \ No newline at end of file From b515103012ddc16ccd25b850bb8b55361bed10af Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Tue, 27 Feb 2018 02:01:14 +0800 Subject: [PATCH 165/178] remove chrome content script (#48) * remove chrome content script --- manifest-chrome.json | 7 ------- src/content.ts | 36 ++++++++++++++++++++---------------- src/ui/add-account.ts | 18 +++++++++++++++++- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/manifest-chrome.json b/manifest-chrome.json index e83fc7b36..aff64f018 100644 --- a/manifest-chrome.json +++ b/manifest-chrome.json @@ -49,13 +49,6 @@ ], "persistent": false }, - "content_scripts": [ - { - "matches": [""], - "css": ["css/content.css"], - "js": ["build/content.js"] - } - ], "permissions": [ "activeTab", "storage" diff --git a/src/content.ts b/src/content.ts index e7edb8412..c6dc7b1ad 100644 --- a/src/content.ts +++ b/src/content.ts @@ -22,12 +22,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { } }); -interface CaptureBoxPosition { - left: number; - top: number; -} - -let captureBoxPosition: CaptureBoxPosition = {left: 0, top: 0}; +sessionStorage.captureBoxPositionLeft = 0; +sessionStorage.captureBoxPositionTop = 0; function showGrayLayout(passphrase: string) { let grayLayout = document.getElementById('__ga_grayLayout__'); @@ -66,8 +62,8 @@ function grayLayoutDown(event: MouseEvent) { return; } - captureBoxPosition.left = event.clientX; - captureBoxPosition.top = event.clientY; + sessionStorage.captureBoxPositionLeft = event.clientX; + sessionStorage.captureBoxPositionTop = event.clientY; captureBox.style.left = event.clientX + 'px'; captureBox.style.top = event.clientY + 'px'; captureBox.style.width = '1px'; @@ -86,10 +82,14 @@ function grayLayoutMove(event: MouseEvent) { return; } - const captureBoxLeft = Math.min(captureBoxPosition.left, event.clientX); - const captureBoxTop = Math.min(captureBoxPosition.top, event.clientY); - const captureBoxWidth = Math.abs(captureBoxPosition.left - event.clientX) - 1; - const captureBoxHeight = Math.abs(captureBoxPosition.top - event.clientY) - 1; + const captureBoxLeft = + Math.min(sessionStorage.captureBoxPositionLeft, event.clientX); + const captureBoxTop = + Math.min(sessionStorage.captureBoxPositionTop, event.clientY); + const captureBoxWidth = + Math.abs(sessionStorage.captureBoxPositionLeft - event.clientX) - 1; + const captureBoxHeight = + Math.abs(sessionStorage.captureBoxPositionTop - event.clientY) - 1; captureBox.style.left = captureBoxLeft + 'px'; captureBox.style.top = captureBoxTop + 'px'; captureBox.style.width = captureBoxWidth + 'px'; @@ -114,10 +114,14 @@ function grayLayoutUp(event: MouseEvent, passphrase: string) { return; } - let captureBoxLeft = Math.min(captureBoxPosition.left, event.clientX) + 1; - let captureBoxTop = Math.min(captureBoxPosition.top, event.clientY) + 1; - let captureBoxWidth = Math.abs(captureBoxPosition.left - event.clientX) - 1; - let captureBoxHeight = Math.abs(captureBoxPosition.top - event.clientY) - 1; + let captureBoxLeft = + Math.min(sessionStorage.captureBoxPositionLeft, event.clientX) + 1; + let captureBoxTop = + Math.min(sessionStorage.captureBoxPositionTop, event.clientY) + 1; + let captureBoxWidth = + Math.abs(sessionStorage.captureBoxPositionLeft - event.clientX) - 1; + let captureBoxHeight = + Math.abs(sessionStorage.captureBoxPositionTop - event.clientY) - 1; captureBoxLeft *= window.devicePixelRatio; captureBoxTop *= window.devicePixelRatio; captureBoxWidth *= window.devicePixelRatio; diff --git a/src/ui/add-account.ts b/src/ui/add-account.ts index fc3b9ab53..b057ded1a 100644 --- a/src/ui/add-account.ts +++ b/src/ui/add-account.ts @@ -2,6 +2,18 @@ /// /// +async function insertContentScript() { + return new Promise((resolve: () => void, reject: (reason: Error) => void) => { + try { + return chrome.tabs.executeScript({file: 'build/content.js'}, () => { + chrome.tabs.insertCSS({file: 'css/content.css'}, resolve); + }); + } catch (error) { + return reject(error); + } + }); +} + async function addAccount(_ui: UI) { const ui: UIConfig = { data: { @@ -39,7 +51,11 @@ async function addAccount(_ui: UI) { } return; }, - beginCapture: () => { + beginCapture: async () => { + if (navigator.userAgent.indexOf('Chrome') !== -1) { + await insertContentScript(); + } + const entries = _ui.instance.entries as OTPEntry[]; for (let i = 0; i < entries.length; i++) { // we have encrypted entry From 8c195e6d4f691e1ace1a3fb874dcb2a9a3244b47 Mon Sep 17 00:00:00 2001 From: Li Zhe Date: Tue, 27 Feb 2018 02:14:01 +0800 Subject: [PATCH 166/178] fix #47 --- _locales/ja/messages.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 47044f5d4..eec5d6907 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -230,5 +230,9 @@ "import_error_password": { "message": "バックアップをインポートするために正しいパスワードを入力してください。", "description": "Error password warning when import backups." + }, + "local_passphrase_warning": { + "message": "パスワードをローカル環境に保存しました。すぐにメニューのセキュリティから変更してください。", + "description": "localStorage password warning." } } \ No newline at end of file From a2dfcf04c19432438bb4aba6ea4aef369d2dcce8 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Mon, 26 Feb 2018 15:26:08 -0600 Subject: [PATCH 167/178] Remove firefox content scripts #48 --- manifest-firefox.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/manifest-firefox.json b/manifest-firefox.json index 28401c5d9..6e92947fa 100644 --- a/manifest-firefox.json +++ b/manifest-firefox.json @@ -54,13 +54,6 @@ "build/background.js" ] }, - "content_scripts": [ - { - "matches": [""], - "css": ["css/content.css"], - "js": ["build/content.js"] - } - ], "permissions": [ "activeTab", "", From dbf69d3f933f3bf1b1b830814d6b9ab12990faf6 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 27 Feb 2018 18:10:04 -0600 Subject: [PATCH 168/178] Add local_passphrase_warning --- _locales/ru/messages.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 279320276..2b867506e 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -230,5 +230,9 @@ "import_error_password": { "message": "Для импорта резервных копий необходимо указать правильный пароль.", "description": "Error password warning when import backups." + }, + "local_passphrase_warning": { + "message": "Ваш пароль хранится локально, пожалуйста немедленно изменить его в меню безопасность.", + "description": "localStorage password warning." } } \ No newline at end of file From 24ac9bdeee19be0d801625e5f7c4fcf921edcaee Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Tue, 27 Feb 2018 18:11:29 -0600 Subject: [PATCH 169/178] Fix content script issue (#49) --- src/ui/add-account.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/ui/add-account.ts b/src/ui/add-account.ts index b057ded1a..9442406ae 100644 --- a/src/ui/add-account.ts +++ b/src/ui/add-account.ts @@ -5,8 +5,8 @@ async function insertContentScript() { return new Promise((resolve: () => void, reject: (reason: Error) => void) => { try { - return chrome.tabs.executeScript({file: 'build/content.js'}, () => { - chrome.tabs.insertCSS({file: 'css/content.css'}, resolve); + return chrome.tabs.executeScript({file: '/build/content.js'}, () => { + chrome.tabs.insertCSS({file: '/css/content.css'}, resolve); }); } catch (error) { return reject(error); @@ -52,9 +52,7 @@ async function addAccount(_ui: UI) { return; }, beginCapture: async () => { - if (navigator.userAgent.indexOf('Chrome') !== -1) { - await insertContentScript(); - } + await insertContentScript(); const entries = _ui.instance.entries as OTPEntry[]; for (let i = 0; i < entries.length; i++) { From 6e66d4c05d7206c0bf1823f1daf687cf25a51576 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Wed, 28 Feb 2018 15:58:47 +0800 Subject: [PATCH 170/178] 5.0.3 --- manifest-chrome.json | 2 +- manifest-firefox.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-chrome.json b/manifest-chrome.json index aff64f018..2a01988f9 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.2", + "version": "5.0.3", "default_locale": "en", "description": "__MSG_extDesc__", "icons": { diff --git a/manifest-firefox.json b/manifest-firefox.json index 6e92947fa..1c72eae36 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.2", + "version": "5.0.3", "default_locale": "en", "description": "__MSG_extDesc__", "applications": { From 292bbd53da15545dde65fb58873e059ff27a892a Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Thu, 1 Mar 2018 21:26:57 +0800 Subject: [PATCH 171/178] filter entry better (#53) --- popup.html | 2 +- src/ui/entry.ts | 104 ++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/popup.html b/popup.html index 948628a92..4b32a56f3 100644 --- a/popup.html +++ b/popup.html @@ -30,7 +30,7 @@
{{ i18n.show_all_entries }}
-
+
diff --git a/src/ui/entry.ts b/src/ui/entry.ts index aa795bebb..bd2a695a0 100644 --- a/src/ui/entry.ts +++ b/src/ui/entry.ts @@ -60,32 +60,111 @@ function getBackupFile(entryData: {[hash: string]: OTPStorage}) { return `data:application/octet-stream;base64,${base64Data}`; } -async function getCurrentHostname() { +async function getSiteName() { return new Promise( - (resolve: (value: string|null) => void, + (resolve: (value: Array) => void, reject: (reason: Error) => void) => { chrome.tabs.query({active: true, lastFocusedWindow: true}, (tabs) => { const tab = tabs[0]; - if (!tab || !tab.url) { - return resolve(null); + if (!tab) { + return resolve([null, null]); } + + const title = tab.title ? + tab.title.replace(/[^a-z0-9]/ig, '').toLowerCase() : + null; + + if (!tab.url) { + return resolve([title, null]); + } + const urlParser = document.createElement('a'); urlParser.href = tab.url; - const hostname = urlParser.hostname; - return resolve(hostname); + const hostname = urlParser.hostname.toLowerCase(); + + // try to parse name from hostname + // i.e. hostname is www.example.com + // name should be example + let nameFromDomain = ''; + + // ip address + if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { + nameFromDomain = hostname; + } + + // local network + if (hostname.indexOf('.') === -1) { + nameFromDomain = hostname; + } + + const hostLevelUnits = hostname.split('.'); + + if (hostLevelUnits.length === 2) { + nameFromDomain = hostLevelUnits[0]; + } + + // www.example.com + // example.com.cn + if (hostLevelUnits.length > 2) { + // example.com.cn + if (['com', 'net', 'org', 'edu', 'gov', 'co'].indexOf( + hostLevelUnits[hostLevelUnits.length - 2]) !== -1) { + nameFromDomain = hostLevelUnits[hostLevelUnits.length - 3]; + } else { // www.example.com + nameFromDomain = hostLevelUnits[hostLevelUnits.length - 2]; + } + } + + nameFromDomain = nameFromDomain.replace(/-/g, '').toLowerCase(); + + return resolve([title, nameFromDomain, hostname]); }); }); } -function hasMatchedEntry(currentHost: string, entries: OTPEntry[]) { +function hasMatchedEntry(siteName: Array, entries: OTPEntry[]) { + if (siteName.length < 2) { + return false; + } + for (let i = 0; i < entries.length; i++) { - if (entries[i].issuer.indexOf(currentHost) !== -1) { + if (isMatchedEntry(siteName, entries[i])) { return true; } } return false; } +function isMatchedEntry(siteName: Array, entry: OTPEntry) { + const issuerHostMatches = entry.issuer.split('::'); + const issuer = issuerHostMatches[0].replace(/[^0-9a-z]/ig, '').toLowerCase(); + + if (!issuer) { + return false; + } + + const siteTitle = siteName[0] || ''; + const siteNameFromHost = siteName[1] || ''; + const siteHost = siteName[2] || ''; + + if (issuerHostMatches.length > 1) { + if (siteHost && siteHost.indexOf(issuerHostMatches[1]) !== -1) { + return true; + } + } + // site title should be more detailed + // so we use siteTitle.indexOf(issuer) + if (siteTitle && siteTitle.indexOf(issuer) !== -1) { + return true; + } + + if (siteNameFromHost && issuer.indexOf(siteNameFromHost) !== -1) { + return true; + } + + return false; +} + async function getCachedPassphrase() { return new Promise( (resolve: (value: string) => void, reject: (reason: Error) => void) => { @@ -126,9 +205,8 @@ async function entry(_ui: UI) { } const exportFile = getBackupFile(exportData); - const currentHost = await getCurrentHostname(); - const shouldFilter = - currentHost ? hasMatchedEntry(currentHost, entries) : false; + const siteName = await getSiteName(); + const shouldFilter = hasMatchedEntry(siteName, entries); const ui: UIConfig = { data: { @@ -142,7 +220,6 @@ async function entry(_ui: UI) { notification: '', notificationTimeout: 0, filter: true, - currentHost, shouldFilter, importType: 'import_file', importCode: '', @@ -150,6 +227,9 @@ async function entry(_ui: UI) { importPassphrase: '' }, methods: { + isMatchedEntry: (entry: OTPEntry) => { + return isMatchedEntry(siteName, entry); + }, updateCode: async () => { return await updateCode(_ui.instance); }, From a87b544de5f2fa8116357f329de51b13408700e0 Mon Sep 17 00:00:00 2001 From: Zhe Li Date: Fri, 2 Mar 2018 14:41:08 +0800 Subject: [PATCH 172/178] remove invalid data in export --- src/models/storage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/storage.ts b/src/models/storage.ts index 5d80dcac6..3cb7d9c4a 100644 --- a/src/models/storage.ts +++ b/src/models/storage.ts @@ -96,6 +96,7 @@ class EntryStorage { chrome.storage.sync.get((_data: {[hash: string]: OTPStorage}) => { for (const hash of Object.keys(_data)) { if (!this.isValidEntry(_data, hash)) { + delete _data[hash]; continue; } // decrypt the data to export From d3391a4d44b3139ff00ad4d86780d7363c0b2c05 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 2 Mar 2018 19:10:45 -0600 Subject: [PATCH 173/178] Add Spanish translations --- _locales/es/messages.json | 238 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 _locales/es/messages.json diff --git a/_locales/es/messages.json b/_locales/es/messages.json new file mode 100644 index 000000000..3a8296d82 --- /dev/null +++ b/_locales/es/messages.json @@ -0,0 +1,238 @@ +{ + "extName": { + "message": "Autenticador", + "description": "Extension Name." + }, + "extShortName": { + "message": "Autenticador", + "description": "Extension Short Name." + }, + "extDesc": { + "message": "Autenticador genera vereficación de dos pasos para tu navegador.", + "description": "Extension Description." + }, + "added": { + "message": " ha sido añadido.", + "description": "Added Account." + }, + "errorqr": { + "message": "Código de QR no es reconocido.", + "description": "QR Error." + }, + "errorsecret": { + "message": "Error al secreto. Sólo Base32 (A-Z, 2-7 y =) y hexadecimal (0-9 y A-f) son compatibles. Sin embargo, su secreto es: ", + "description": "Secret Error." + }, + "add_qr": { + "message": "Escanear código QR", + "description": "Scan QR Code." + }, + "add_secret": { + "message": "Entrada Manual", + "description": "Manual Entry." + }, + "close": { + "message": "Cierra", + "description": "Close." + }, + "ok": { + "message": "Ok", + "description": "OK." + }, + "yes": { + "message": "Si", + "description": "Yes." + }, + "no": { + "message": "No", + "description": "No." + }, + "account": { + "message": "Cuenta", + "description": "Account." + }, + "accountName": { + "message": "Nombre de Cuenta", + "description": "Account Name." + }, + "issuer": { + "message": "Emisor", + "description": "Issuer." + }, + "secret": { + "message": "Secreto", + "description": "Secret." + }, + "updateSuccess": { + "message": "Éxito.", + "description": "Update Success." + }, + "updateFailure": { + "message": "Fracaso.", + "description": "Update Failure." + }, + "about": { + "message": "Acerca de", + "description": "About." + }, + "export_import": { + "message": "Exportación \/ importación", + "description": "Export and Import." + }, + "settings": { + "message": "Configuración -", + "description": "Settings." + }, + "security": { + "message": "Seguridad", + "description": "Security." + }, + "current_phrase": { + "message": "Contraseña actual", + "description": "Current Passphrase." + }, + "new_phrase": { + "message": "Nueva contraseña", + "description": "New Passphrase." + }, + "phrase": { + "message": "Contraseña", + "description": "Passphrase." + }, + "confirm_phrase": { + "message": "Confirme su contraseña", + "description": "Confirm Passphrase." + }, + "confirm_delete": { + "message": "¿Está seguro que desea eliminar esta secreto? ¡Esto no se puede deshacer.", + "description": "Remove entry confirmation" + }, + "security_warning": { + "message": "Esta contraseña se utilizará para cifrar tus secretos. Nadie le puede ayudar Si olvidas la contraseña.", + "description": "Passphrase Warning." + }, + "update": { + "message": "Actualizar", + "description": "Update." + }, + "phrase_incorrect": { + "message": "No puede Agregar una nueva cuenta o exportar los datos hasta que todas las cuentas se descifran. Introduce la contraseña correcta antes de continuar.", + "description": "Passphrase Incorrect." + }, + "phrase_not_match": { + "message": "Las contraseñas no coinciden.", + "description": "Passphrase Not Match." + }, + "encrypted": { + "message": "Cifrados", + "description": "Encrypted." + }, + "copied": { + "message": "Copiado", + "description": "Copied." + }, + "feedback": { + "message": "Comentarios", + "description": "Feedback." + }, + "translate": { + "message": "Traducir", + "description": "Translate." + }, + "source": { + "message": "Código fuente", + "description": "Source Code." + }, + "passphrase_info": { + "message": "Introduzca la contraseña para descifrar los datos de la cuenta.", + "description": "Passphrase Info" + }, + "sync_clock": { + "message": "Sincroniza el reloj con Google", + "description": "Sync Clock" + }, + "remember_phrase": { + "message": "Recuerda Contraseña", + "description": "Remember Passphrase" + }, + "clock_too_far_off": { + "message": "¡Precaución! Su reloj local está demasiado lejos, por favor arreglarlo antes de continuar.", + "description": "Local Time is Too Far Off" + }, + "remind_backup": { + "message": "¿Tienes una copia de seguridad de tus secretos? ¡No esperes hasta que sea demasiado tarde!", + "description": "Remind Backup" + }, + "capture_failed": { + "message": "Error en Captura, por favor recarga la página e inténtelo de nuevo.", + "description": "Capture Failed" + }, + "based_on_time": { + "message": "Tiempo en función", + "description": "Time Based" + }, + "based_on_counter": { + "message": "Contador basado en", + "description": "Counter Based" + }, + "resize_popup_page": { + "message": "Cambiar el tamaño de página", + "description": "Resize Popup Page" + }, + "scale": { + "message": "Escala", + "description": "Scale" + }, + "export_info": { + "message": "ADVERTENCIA: todos los backups son unencrypted. ¿Desea agregar una cuenta a otra aplicación? Pasa el cursor sobre la parte superior derecha de cualquier cuenta y presione el botón oculto.", + "description": "Export menu info text" + }, + "download_backup": { + "message": "Descargar archivo de respaldo", + "description": "Download backup file." + }, + "import_backup": { + "message": "Copia de seguridad de importación", + "description": "Import backup." + }, + "import_backup_file": { + "message": "Importar archivo de copia de seguridad", + "description": "Import backup file." + }, + "import_backup_code": { + "message": "Copia de seguridad de importación", + "description": "Import backup code." + }, + "dropbox_backup": { + "message": "Copia de seguridad automática a Dropbox", + "description": "Auto backup to Dropbox." + }, + "dropbox_code": { + "message": "Código de Dropbox", + "description": "Dropbox code." + }, + "dropbox_token": { + "message": "Token de Dropbox", + "description": "Dropbox token." + }, + "dropbox_authorization": { + "message": "Obtener el código de", + "description": "Dropbox authorization." + }, + "show_all_entries": { + "message": "Mostrar todas las entradas", + "description": "Show all entries." + }, + "dropbox_risk": { + "message": "ADVERTENCIA: backups guardados en Dropbox están sin cifrar. Usar bajo su propio riesgo.", + "description": "Dropbox backup risk warning." + }, + "import_error_password": { + "message": "Debe proporcionar la contraseña correcta para la importación de copias de seguridad.", + "description": "Error password warning when import backups." + }, + "local_passphrase_warning": { + "message": "La contraseña se almacena localmente, por favor cambiarlo en el menú de seguridad inmediatamente.", + "description": "localStorage password warning." + } +} \ No newline at end of file From 43bb21f6396fbe7c027179d6aa052716d267b377 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 2 Mar 2018 19:39:04 -0600 Subject: [PATCH 174/178] 5.0.4 --- manifest-chrome.json | 2 +- manifest-firefox.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/manifest-chrome.json b/manifest-chrome.json index 2a01988f9..045afe68c 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.3", + "version": "5.0.4", "default_locale": "en", "description": "__MSG_extDesc__", "icons": { diff --git a/manifest-firefox.json b/manifest-firefox.json index 1c72eae36..0946636a5 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.3", + "version": "5.0.4", "default_locale": "en", "description": "__MSG_extDesc__", "applications": { From 5849ff3aa6dbfbc3224f9a0f9e45fc96ec8140e6 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 2 Mar 2018 20:00:40 -0600 Subject: [PATCH 175/178] Add download links --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index c679b6706..2f941ec4c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ > Authenticator generates 2-Step Verification codes in your browser. +Get Authenticator at the [Chrome Web Store](https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai) or [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/auth-helper/) + ## Build Setup Compile for Chrome: From 7f8771de4294425c0b955208b7d2198cae5eea03 Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Fri, 2 Mar 2018 20:00:55 -0600 Subject: [PATCH 176/178] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f941ec4c..f0ea907dd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > Authenticator generates 2-Step Verification codes in your browser. -Get Authenticator at the [Chrome Web Store](https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai) or [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/auth-helper/) +Get Authenticator at the [Chrome Web Store](https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai) or on [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/auth-helper/) ## Build Setup From 8c14a782f4451f3515614c5a1b54595af5c57a6e Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 3 Mar 2018 02:09:30 -0600 Subject: [PATCH 177/178] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0ea907dd..d9f849b88 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > Authenticator generates 2-Step Verification codes in your browser. -Get Authenticator at the [Chrome Web Store](https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai) or on [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/auth-helper/) +You can install Authenticator from the [Chrome Web Store](https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai) or [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/auth-helper/). ## Build Setup From adb961551a5a3e4c4e6e40c722ad6c3333ecf1bc Mon Sep 17 00:00:00 2001 From: mymindstorm Date: Sat, 3 Mar 2018 02:09:55 -0600 Subject: [PATCH 178/178] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d9f849b88..e8d951a3a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ > Authenticator generates 2-Step Verification codes in your browser. -You can install Authenticator from the [Chrome Web Store](https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai) or [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/auth-helper/). +You can install Authenticator from the [Chrome Web Store](https://chrome.google.com/webstore/detail/authenticator/bhghoamapcdpbohphigoooaddinpkbai) or from [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/auth-helper/). ## Build Setup