diff --git a/.gitignore b/.gitignore
index aa724b7..f7415cf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,4 @@
/captures
.externalNativeBuild
.cxx
-local.properties
+local.properties
\ No newline at end of file
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
new file mode 100644
index 0000000..b0eeacf
--- /dev/null
+++ b/.idea/deploymentTargetDropDown.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index af020f6..7b46144 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -4,17 +4,16 @@
diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml
index a5f05cd..4dcbce1 100644
--- a/.idea/jarRepositories.xml
+++ b/.idea/jarRepositories.xml
@@ -21,5 +21,15 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml
new file mode 100644
index 0000000..8ec256a
--- /dev/null
+++ b/.idea/render.experimental.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 3a065d8..430e8ad 100644
--- a/README.md
+++ b/README.md
@@ -8,42 +8,46 @@
# Wristkey
-
+
-Need 2FA codes quickly, right on your Wear watch without needing a phone? Wristkey is an open-source 2FA client for Wear OS watches that does just that! Supports Android Wear 7.1 (Nougat) and above. [Get the APK here.](app/release/app-release.apk)
+Have an old smartwatch lying around that you'd like to use for 2FA? Or do you just need a minimal and open-source 2FA app you can trust? Wristkey is an open-source 2FA client for Wear OS watches, but it works on any Android-based smartwatch! [Get the APK here.](app/release/app-release.apk)
-
+Now with a fresh new look for Wear OS 3.0!
+
+You can now run Wristkey on your phone too! Just download the APK and install it on your phone.
+
+
## FEATURES
-- Works without being paired to phone (standalone) and without internet connection.
+- Use without pairing to a phone (standalone) or being connected to Wi-Fi.
-- Direct import from Aegis Authenticator, andOTP, Bitwarden and Google Authenticator.
+- Import via Aegis Authenticator, andOTP, Bitwarden and Google Authenticator using ADB.
-- Secure app by locking via PIN, pattern and password.
+- Export to other devices using QR code or export file.
-- Safe data storage using 256-bit encryption.
+- Lock via PIN, pattern and password.
-- Time and counter mode OTPs, upto SHA-512 and 4-8 digits.
+- Data stored using 256-bit AES-GCM.
-- Supports round and square devices and customization via theming.
+- Time and counter mode OTPs, up to SHA512 and 4-8 digits.
-- Backup options via QR code and JSON.
+- Supports round and square devices.
## USAGE
### Adding items
-Wristkey supports importing data from multiple sources for ease-of-use, though the procedures differ slightly for each of them. For example, for Bitwarden, a JSON file is parsed and the ```totp``` field is extracted from each account. For website QR codes, the QR Code is scanned and the resulting `otpauth://` URL is parsed.
+Wristkey supports importing data from multiple sources, though the procedures differ slightly for each of them. For example, for Bitwarden, a JSON file is parsed and the ```totp``` field is extracted from each account. For website QR codes, the QR Code is scanned and the resulting `otpauth://` URL is parsed.
-
+
#### Transferring data
-##### Via phone
-
If your watch is paired to an Android phone, you can use a third-party Wear OS file manager like [myWear File Explorer](https://play.google.com/store/apps/details?id=com.mrs.wear_file_explorer) or [Nav Explorer](https://play.google.com/store/apps/details?id=com.turndapage.navexplorer) to transfer PNG / JSON files from your phone's storage to your watch.
+If your device has a camera, you can just use that to scan for QR codes.
+
##### Via ADB
1. Enable ADB Debugging on your watch by going to Settings → System → About and tapping 'Build Number' 7 times.
@@ -84,25 +88,17 @@ If your watch is paired to an Android phone, you can use a third-party Wear OS f
#### Google Authenticator and normal QR Code imports
-1. If using a QR Code from a website, save it as a screenshot and make sure it is clearly visible with no pixelation. If importing from Google Authenticator, tap the three dots on the top right corner, then tap on 'Export accounts'. Then select the accounts you\'d like to export and tap the export button 'Export'. Take a picture or screenshot of the QR code that is displayed and **make sure it is a PNG or JPG file** and is clear with no blurring, glare or pixelation.
+1. If using a QR Code from a website, save it as a screenshot and make sure it is clearly visible with no pixelation. If importing from Google Authenticator, tap the three dots on the top right corner, then tap on 'Export accounts'. Then select the accounts you\'d like to export and tap the export button 'Export'. Take a picture or screenshot of the QR code that is displayed, **make sure it is a PNG or JPG file** and that it is clear with no blurring, glare or pixelation.
2. Open a terminal on your computer and place this PNG or JPG file on the main directory of your watch (/sdcard/) via the following command
```
- adb push .png /sdcard/
+ adb push .png /data/local/tmp
+ adb shell run-as app.wristkey cp /data/local/tmp/.json files
```
3. On your watch, open Wristkey, scroll down and tap the add icon '+', then select your import option.
-4. After your accounts are imported, delete the PNG or JPG file from your watch via the following commands
-
- ```
- adb shell
- cd /sdcard/
- rm .png
- exit
- ```
-
#### Aegis Authenticator, andOTP, Bitwarden and Wristkey backup imports
1. Export your data in an unencrypted JSON format. Make sure you don't rename the file.
@@ -110,35 +106,29 @@ If your watch is paired to an Android phone, you can use a third-party Wear OS f
2. Open a terminal on your computer and place this JSON file on the main directory of your watch (/sdcard/). If using a Wristkey backup file, do **not** place it in the /Wristkey folder. Do this via the following command
```
- adb push /sdcard/
+ adb push .json /data/local/tmp
+ adb shell run-as app.wristkey cp /data/local/tmp/.json files
```
3. On your watch, open Wristkey, scroll down and tap the add icon '+', then select your import option.
-4. After your accounts are imported, delete the JSON file from your watch via the following commands
-
- ```
- adb shell
- cd /sdcard/
- rm
- exit
- ```
-
#### Manual entry
-1. On your watch, open Wristkey, scroll down and tap the add icon '+', then tap *Manual Entry*. The default settings are for Google Authenticator codes (SHA-1, 6 digits, time-based).
+1. On your watch, open Wristkey, scroll down and tap the add icon '+', then tap *Manual Entry*. The default settings are for Google Authenticator codes (SHA1, 6 digits, time-based).
-2. Scroll down and tap the tick button '✓' at the at the bottom when done.
+***Note:** Steam codes aren't supported yet.*
-
+2. Scroll down and tap the done button '✓' at the at the bottom when done.
+
+
### Editing and Deleting items
-To edit or delete an item, tap and hold on its name. This was made difficult on purpose so that accounts aren't accidentally edited or deleted. To delete an item, scroll all the way to the bottom of the edit screen and tap the trash icon.
+To edit or delete an item, tap and hold on its name. To delete an item, scroll all the way to the bottom of the edit screen and tap the trash icon.
### Exporting
-
+
Since watches are tiny devices that can be misplaced, backing up and exporting your secrets and storing them in a safe place is always a a good idea.
@@ -146,46 +136,53 @@ Since watches are tiny devices that can be misplaced, backing up and exporting y
To transfer a code from your watch to an Authenticator app on your phone, just press and hold the 2FA code number on your watch. You can then scan the QR code that is displayed on your watch screen in any 2FA application.
-***Tip:** Tap the QR Code to dim it for better scanning.*
-
-#### All accounts
-
-To backup all content, open Wristkey, tap the settings icon '⚙️', then scroll down and tap *Backup all data*.
-
##### Via QR code
-Tap 'QR code' to get a (not compatible with Authenticator) QR Code data. **This QR code cannot be scanned in any 2FA application and is purely for extraction purposes.**
+To export a single account via a QR code, press and hold it, then scroll down and select the 'Show QR code' option.
+
+To export all your accounts via QR codes, tap the settings icon '⚙️', then scroll down and select *Export vault*. Then select the QR code option. All your account QR codes will be displayed at 5 second intervals. You can scan these in your preferred Authenticator app.
##### Via file
-1. Tap 'File' to get your backups in the form of a file. The data will be placed in ```/sdcard/wristkey/```.
+1. Tap the settings icon '⚙️', then scroll down and select *Export vault*. Then select the file option. An export file will be generated on your device.
2. Open a terminal on your computer and extract this file via the following command
```
- adb pull /sdcard/wristkey/
+ adb shell
+ run-as app.wristkey
+ cat files/.wfs > /sdcard/.wfs
+ exit
+ exit
+ adb pull /sdcard/.wfs /file/location/on/computer/
```
+
3. To delete the directory, type
```
- adb shell rm /sdcard/wristkey/
+ adb shell rm /sdcard/.wfs
```
-***Note:** The exported data is unencrypted and must be handled with care. Delete it when not in use.*
+***Note:** Exported text files are unencrypted and must be handled with care. Delete them when not in use.*
## TROUBLESHOOTING
-#### Wrong TOTP codes are shown
+#### App displays wrong TOTP codes
-Make sure you set your secret key, digit length and algorithm correctly. If the displayed codes are still wrong, your watch may have the time set incorrectly. Please set the time by pairing it to a phone or connecting to WiFi.
+Make sure you set your secret key, digit length and algorithm correctly. If the 2FA codes are still wrong, your watch may have the time set incorrectly. Please set the time by pairing it to a phone or connecting to Wi-Fi.
#### File import not working
-Make sure Wristkey has storage permissions in your watch's Settings app. If importing from JSON, make sure the file you export is an **Unencrypted** file in **JSON** format. If importing from Authenticator, make sure the screenshot or picture is in **PNG or JPG** format and is clear. If using a Wristkey backup file, make sure it has the _.backup_ extension.
+Make sure that
+
+1. Wristkey has storage permissions in your watch's Settings app.
+2. If importing from JSON, make sure the file you export is an **Unencrypted** file in **JSON** format and that you don't rename it.
+3. If importing from Google Authenticator or a QR code, make sure the screenshot or picture is in **PNG or JPG** format and is clear.
+4. If using a Wristkey backup file, make sure it has the _.wfs_ extension.
#### File export not working
-Make sure Wristkey has storage permissions in your watch's Settings app. If already enabled, disable and enable storage permissions again.
+Make sure that Wristkey has storage permissions in your watch's Settings app. If already enabled, disable and enable storage permissions again.
## SECURITY
@@ -193,17 +190,15 @@ _Further reading: [Security Policy](https://github.com/4f77616973/Wristkey/secur
### Importing files
-To prevent data extraction, snooping and theft, make sure you delete the JSON, PNG or JPG files from your watch's storage once you're done importing them. You can confirm the existence of items by connecting your watch via ADB and running the ```adb shell ls /sdcard/``` command.
+To prevent data extraction, make sure you delete the JSON, PNG or JPG files from your watch's storage once you're done importing them. Check for any lingering files via ADB by running the ```adb shell ls /sdcard/``` command.
### In-app storage
-All sensitive data within Wristkey (including secrets to generate OTPs) is stored encrypted [using 256-bit AES encryption](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences), with the decryption key [stored locally](https://developer.android.com/training/articles/keystore) on your watch. No backdoor on my end. ;)
+All sensitive data within Wristkey (including secrets to generate OTPs) is stored encrypted [using 256-bit AES-GCM encryption](https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences), with the decryption key [stored locally](https://developer.android.com/training/articles/keystore) on your watch. No backdoor on my end. ;)
### Privacy
-Wristkey can be set to unlock after entering your watch's password / PIN / pattern. To enable screen locking for the app, go to your watch's Settings → Personalization → Screen Lock and set a PIN / pattern / password. To override this setting, open Wristkey, tap the settings icon '⚙️', then scroll down and disable *Screen locking*
-
-Wristkey doesn't use Wear OS's Ambient Mode by default to prevent bystanders from peeking at your 2FA codes. To enable Ambient Mode, open Wristkey, tap the settings icon '⚙️', then scroll down and enable *Ambient mode*.
+Wristkey can be set to unlock after entering your watch's password / PIN / pattern. To enable screen locking for the app, go to your watch's Settings → Personalization → Screen Lock and set a PIN / pattern / password. To override this setting, open Wristkey, tap the settings icon '⚙️', then scroll down and disable *Screen lock*
## CHANGELOG
@@ -213,7 +208,7 @@ A detailed changelog is available on the [releases](https://github.com/4f7761697
### Contributing
-I made this app for myself because the LG G Watch W100 I use doesn't support internet access when paired with iOS and Google scrapped their Authenticator app from the Wear OS Play Store. However, anyone can contribute to this project. [Click here to read the rules](CONTRIBUTING.md) if you'd like to.
+I made this app for myself because the ancient LG G Watch W100 I use barely works when paired with iOS. But you can contribute too if you want. [Click here](CONTRIBUTING.md) to read the rules before doing so.
### Code of Conduct
diff --git a/app/build.gradle b/app/build.gradle
index c90c546..b311452 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,6 @@
plugins {
- id 'com.android.application'
id 'kotlin-android'
+ id 'com.android.application'
id 'com.chaquo.python'
}
@@ -8,23 +8,22 @@ apply plugin: 'com.android.application'
apply plugin: 'com.chaquo.python'
android {
- compileSdkVersion 30
- buildToolsVersion "30.0.3"
+ compileSdk 33
defaultConfig {
- applicationId 'com.wristkey'
- minSdkVersion 23
- targetSdkVersion 30 // Play Store requires API29+
- versionCode 1
- versionName "1.3.1"
+ applicationId 'app.wristkey'
+ minSdk 21
+ targetSdkVersion 33 // Play Store requires API29+
+ versionCode 2
+ versionName "2.0"
ndk {
abiFilters "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
}
+ // A pip requirement specifier, with or without a version number
python {
pip {
- // A pip requirement specifier, with or without a version number:
install "protobuf"
}
}
@@ -47,34 +46,39 @@ android {
buildFeatures {
viewBinding true
}
- lintOptions {
- checkReleaseBuilds false
- // Or, if you prefer, you can continue to check for errors in release builds,
- // but continue the build even when errors are found:
+ ndkVersion '21.4.7075529'
+ lint {
abortOnError false
+ checkReleaseBuilds false
}
+ namespace 'wristkey'
+ buildToolsVersion '32.0.0'
}
dependencies {
- implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.31"
- implementation 'com.google.code.gson:gson:2.8.6'
- implementation 'androidx.core:core-ktx:1.5.0'
- implementation 'com.google.android.support:wearable:2.8.1'
- implementation 'com.google.android.gms:play-services-wearable:17.1.0'
+ // implementation "androidx.wear:wear-input:1.0.0"
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:1.7.20"
+ implementation 'com.google.code.gson:gson:2.10'
+ implementation 'androidx.core:core-ktx:1.9.0'
+ implementation 'com.google.android.material:material:1.7.0'
+ implementation 'com.google.android.support:wearable:2.9.0'
+ implementation 'com.google.android.gms:play-services-wearable:18.0.0'
implementation 'androidx.percentlayout:percentlayout:1.0.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
- implementation 'androidx.recyclerview:recyclerview:1.2.0'
- implementation 'androidx.wear:wear:1.1.0'
- // implementation "androidx.wear:wear-input:1.0.0"
- implementation 'dev.turingcomplete:kotlin-onetimepassword:2.0.1'
+ implementation 'androidx.recyclerview:recyclerview:1.2.1'
+ implementation 'androidx.wear:wear:1.2.0'
+ implementation 'dev.turingcomplete:kotlin-onetimepassword:2.4.0'
implementation 'androidx.cardview:cardview:1.0.0'
- implementation 'com.google.android.material:material:1.3.0'
- implementation 'androidx.appcompat:appcompat:1.3.0'
- implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
- implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
- implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'
- implementation 'androidmads.library.qrgenearator:QRGenearator:1.0.3'
- implementation 'com.google.zxing:core:3.3.3'
- compileOnly 'com.google.android.wearable:wearable:2.8.1'
- implementation "androidx.security:security-crypto:1.1.0-alpha03"
+ implementation 'com.google.android.material:material:1.7.0'
+ implementation 'androidx.appcompat:appcompat:1.5.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
+ implementation 'com.github.androidmads:QRGenerator:1.0.1'
+ implementation 'com.google.zxing:core:3.5.1'
+ implementation 'androidx.core:core-ktx:1.9.0'
+ compileOnly 'com.google.android.wearable:wearable:2.9.0'
+ implementation "androidx.security:security-crypto:1.1.0-alpha04"
+ implementation 'com.github.yuriy-budiyev:code-scanner:2.3.2'
+ implementation 'androidx.biometric:biometric:1.2.0-alpha05'
}
\ No newline at end of file
diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json
index c4eccc5..3b5fa6f 100644
--- a/app/release/output-metadata.json
+++ b/app/release/output-metadata.json
@@ -4,7 +4,7 @@
"type": "APK",
"kind": "Directory"
},
- "applicationId": "com.wristkey",
+ "applicationId": "app.wristkey",
"variantName": "release",
"elements": [
{
diff --git a/app/src/debug/ic_launcher-playstore.png b/app/src/debug/ic_launcher-playstore.png
new file mode 100644
index 0000000..26a691e
Binary files /dev/null and b/app/src/debug/ic_launcher-playstore.png differ
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
similarity index 74%
rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
index 036d09b..f3cf475 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml
similarity index 100%
rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml
diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher.png b/app/src/debug/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..0f27f77
Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..927c6c4
Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..7f48477
Binary files /dev/null and b/app/src/debug/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher.png b/app/src/debug/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..707ed81
Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8e92a50
Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..8f3260f
Binary files /dev/null and b/app/src/debug/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..c3d627c
Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..5e0cb5e
Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..762f733
Binary files /dev/null and b/app/src/debug/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..76fcebd
Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..8e17b53
Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..da88344
Binary files /dev/null and b/app/src/debug/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..7d41663
Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..10d0801
Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..961b8e7
Binary files /dev/null and b/app/src/debug/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/debug/res/values/ic_launcher_background.xml b/app/src/debug/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..beab31f
--- /dev/null
+++ b/app/src/debug/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #000000
+
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 61e2295..d3d9df8 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,89 +1,84 @@
-
+
+
+
+
-
-
-
-
-
-
-
+ android:theme="@style/Theme.AppCompat.NoActionBar">
+
+
+
+ android:name="app.wristkey.MainActivity"
+ android:label="@string/app_name"
+ android:exported="true">
-
+ android:name="app.wristkey.QRCodeActivity"
+ android:label="QR code" />
+
diff --git a/app/src/main/java/app/wristkey/AboutActivity.kt b/app/src/main/java/app/wristkey/AboutActivity.kt
new file mode 100644
index 0000000..bab860e
--- /dev/null
+++ b/app/src/main/java/app/wristkey/AboutActivity.kt
@@ -0,0 +1,132 @@
+package app.wristkey
+
+
+import android.annotation.SuppressLint
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.animation.AnimationUtils
+import android.widget.ImageView
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
+import com.google.android.wearable.intent.RemoteIntent
+import wristkey.BuildConfig
+import wristkey.R
+import java.text.SimpleDateFormat
+import java.util.*
+
+class AboutActivity : AppCompatActivity() {
+
+ lateinit var mfaCodesTimer: Timer
+ lateinit var utilities: Utilities
+
+ private lateinit var clock: TextView
+
+ private lateinit var backButton: CardView
+ private lateinit var appNameText: TextView
+ private lateinit var heart: TextView
+ private lateinit var versionText: TextView
+ private lateinit var bitcoinDonateQrCode: ImageView
+ private lateinit var urlLink: TextView
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ @SuppressLint("SetTextI18n")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_about)
+
+ utilities = Utilities(applicationContext)
+ mfaCodesTimer = Timer()
+ initializeUI()
+ startClock()
+
+ }
+
+ override fun onStop() {
+ super.onStop()
+ mfaCodesTimer.cancel()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mfaCodesTimer.cancel()
+ finish()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ mfaCodesTimer = Timer()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun startClock () {
+
+ if (!utilities.vault.getBoolean(utilities.SETTINGS_CLOCK_ENABLED, true)) {
+ findViewById(R.id.clockBackground).visibility = View.GONE
+ }
+
+ try {
+ mfaCodesTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ val currentHour24 = SimpleDateFormat("HH", Locale.getDefault()).format(Date())
+ val currentHour = SimpleDateFormat("hh", Locale.getDefault()).format(Date())
+ val currentMinute = SimpleDateFormat("mm", Locale.getDefault()).format(Date())
+ val currentSecond = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt()
+ val currentAmPm = SimpleDateFormat("a", Locale.getDefault()).format(Date())
+ runOnUiThread {
+ try {
+
+ if (utilities.vault.getBoolean(utilities.SETTINGS_24H_CLOCK_ENABLED, false)) {
+ clock.text = "$currentHour24:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour24 $currentMinute"
+ } else {
+ clock.text = "$currentHour:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour $currentMinute"
+ }
+
+ } catch (_: Exception) { }
+ }
+ }
+ }, 0, 1000) // 1000 milliseconds = 1 second
+ } catch (_: IllegalStateException) { }
+ }
+
+ fun initializeUI () {
+
+ clock = findViewById(R.id.clock)
+
+ backButton = findViewById(R.id.backButton)
+ appNameText = findViewById(R.id.AppName)
+ heart = findViewById(R.id.heart)
+ versionText = findViewById(R.id.Version)
+ bitcoinDonateQrCode = findViewById(R.id.bitcoinDonateQrCode)
+ urlLink = findViewById(R.id.SourceCode)
+
+ versionText.text = "v${BuildConfig.VERSION_NAME}"
+ val uri: String = getString(R.string.about_url)
+
+ heart.startAnimation(AnimationUtils.loadAnimation(this, R.anim.heartbeat))
+
+ urlLink.setOnClickListener {
+ val intent = Intent(Intent.ACTION_VIEW).addCategory(Intent.CATEGORY_BROWSABLE).setData(Uri.parse(uri))
+ RemoteIntent.startRemoteActivity(this, intent, null)
+ Toast.makeText(this, "URL opened\non phone", Toast.LENGTH_SHORT).show()
+ try {
+ val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
+ startActivity(browserIntent)
+ val toast2 = Toast.makeText(this, "URL opened\nin browser", Toast.LENGTH_SHORT)
+ toast2.show()
+ } catch (ex: Exception) { }
+ }
+
+ backButton.setOnClickListener {
+ finish()
+ }
+ }
+
+}
diff --git a/app/src/main/java/app/wristkey/AddActivity.kt b/app/src/main/java/app/wristkey/AddActivity.kt
new file mode 100644
index 0000000..8826f16
--- /dev/null
+++ b/app/src/main/java/app/wristkey/AddActivity.kt
@@ -0,0 +1,147 @@
+package app.wristkey
+import android.content.Intent
+import android.media.audiofx.HapticGenerator
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.widget.TextView
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
+import wristkey.R
+import java.text.SimpleDateFormat
+import java.util.*
+
+class AddActivity : AppCompatActivity() {
+
+ lateinit var mfaCodesTimer: Timer
+ lateinit var utilities: Utilities
+
+ private lateinit var clock: TextView
+
+ private lateinit var manualEntry: CardView
+ private lateinit var aegisImportButton: CardView
+ private lateinit var googleAuthenticatorImport: CardView
+ private lateinit var bitwardenImport: CardView
+ private lateinit var andOtpImport: CardView
+ private lateinit var backupFileButton: CardView
+ private lateinit var scanQRCode: CardView
+
+ private lateinit var backButton: CardView
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_add)
+
+ utilities = Utilities(applicationContext)
+ mfaCodesTimer = Timer()
+ initializeUI()
+ startClock()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun startClock () {
+
+ if (!utilities.vault.getBoolean(utilities.SETTINGS_CLOCK_ENABLED, true)) {
+ findViewById(R.id.clockBackground).visibility = View.GONE
+ }
+
+ try {
+ mfaCodesTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ val currentHour24 = SimpleDateFormat("HH", Locale.getDefault()).format(Date())
+ val currentHour = SimpleDateFormat("hh", Locale.getDefault()).format(Date())
+ val currentMinute = SimpleDateFormat("mm", Locale.getDefault()).format(Date())
+ val currentSecond = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt()
+ val currentAmPm = SimpleDateFormat("a", Locale.getDefault()).format(Date())
+ runOnUiThread {
+ try {
+
+ if (utilities.vault.getBoolean(utilities.SETTINGS_24H_CLOCK_ENABLED, false)) {
+ clock.text = "$currentHour24:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour24 $currentMinute"
+ } else {
+ clock.text = "$currentHour:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour $currentMinute"
+ }
+
+ } catch (_: Exception) { }
+ }
+ }
+ }, 0, 1000) // 1000 milliseconds = 1 second
+ } catch (_: IllegalStateException) { }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ mfaCodesTimer.cancel()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mfaCodesTimer.cancel()
+ finish()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ mfaCodesTimer = Timer()
+ }
+
+ private fun initializeUI () {
+
+ clock = findViewById(R.id.clock)
+
+ manualEntry = findViewById (R.id.manualEntry)
+ aegisImportButton = findViewById (R.id.aegisImportButton)
+ googleAuthenticatorImport = findViewById (R.id.googleAuthenticatorImport)
+ andOtpImport = findViewById (R.id.andOtpImportButton)
+ bitwardenImport = findViewById (R.id.bitwardenImport)
+ backupFileButton = findViewById (R.id.backupFileButton)
+ scanQRCode = findViewById (R.id.scanQRCode)
+
+ backButton = findViewById (R.id.backButton)
+
+ manualEntry.setOnClickListener {
+ startActivity(Intent(applicationContext, ManualEntryActivity::class.java))
+ manualEntry.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ backupFileButton.setOnClickListener {
+ startActivity(Intent(applicationContext, WristkeyImport::class.java))
+ aegisImportButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ scanQRCode.setOnClickListener {
+ startActivity(Intent(applicationContext, OtpAuthImport::class.java))
+ aegisImportButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ aegisImportButton.setOnClickListener {
+ startActivity(Intent(applicationContext, AegisJSONImport::class.java))
+ aegisImportButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ googleAuthenticatorImport.setOnClickListener {
+ startActivity(Intent(applicationContext, AuthenticatorQRImport::class.java))
+ aegisImportButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ bitwardenImport.setOnClickListener {
+ startActivity(Intent(applicationContext, BitwardenJSONImport::class.java))
+ aegisImportButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ andOtpImport.setOnClickListener {
+ startActivity(Intent(applicationContext, AndOtpJSONImport::class.java))
+ aegisImportButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ backButton.setOnClickListener {
+ finish()
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/AegisJSONImport.kt b/app/src/main/java/app/wristkey/AegisJSONImport.kt
new file mode 100644
index 0000000..27879b2
--- /dev/null
+++ b/app/src/main/java/app/wristkey/AegisJSONImport.kt
@@ -0,0 +1,146 @@
+package app.wristkey
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.media.audiofx.HapticGenerator
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.widget.ImageButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import wristkey.R
+import java.io.File
+import java.io.FileReader
+import java.util.*
+
+
+class AegisJSONImport : Activity() {
+
+ lateinit var utilities: Utilities
+
+ lateinit var backButton: ImageButton
+ lateinit var doneButton: ImageButton
+ lateinit var importLabel: TextView
+ lateinit var description: TextView
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_aegis_jsonimport)
+
+ utilities = Utilities (applicationContext)
+
+ initializeUI()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ setContentView(R.layout.activity_aegis_jsonimport)
+ backButton = findViewById (R.id.backButton)
+ doneButton = findViewById (R.id.doneButton)
+ importLabel = findViewById (R.id.label)
+ description = findViewById (R.id.description)
+
+ description.text = getString (R.string.wristkey_import_blurb) + " " + applicationContext.filesDir.toString() + "\n\n" + getString (R.string.use_adb_blurb)
+
+ backButton.setOnClickListener {
+ backButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ finish()
+ }
+
+ doneButton.setOnClickListener {
+ doneButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, utilities.FILES_REQUEST_CODE)
+ }
+
+ }
+
+ // Function to check and request permission.
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun checkPermission(permission: String, requestCode: Int) {
+ if (ContextCompat.checkSelfPermission(this@AegisJSONImport, permission) == PackageManager.PERMISSION_DENIED) {
+ ActivityCompat.requestPermissions(this@AegisJSONImport, arrayOf(permission), requestCode)
+ } else {
+ initializeScanUI()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == utilities.FILES_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initializeScanUI()
+ } else {
+ Toast.makeText(this@AegisJSONImport, "Please grant Wristkey storage permissions in settings", Toast.LENGTH_LONG).show()
+ val intent = Intent (android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeScanUI () {
+ setContentView(R.layout.import_loading_screen)
+ val importingDescription = findViewById(R.id.ImportingDescription)
+
+ var logins = mutableListOf()
+
+ try {
+ val directory = File (applicationContext.filesDir.toString())
+ Log.d ("Wristkey", "Looking for files in: " + applicationContext.filesDir.toString())
+ importingDescription.text = "Looking for files in: \n${directory}"
+
+ for (file in directory.listFiles()!!) {
+
+ try {
+ val reader = FileReader(file.path)
+ val jsonData = reader.readText()
+
+ if (file.name.contains("aegis") && file.name.endsWith(".json")) {
+ logins = utilities.aegisToWristkey (jsonData)
+ }
+
+ } catch (_: Exception) {
+ Log.d ("Wristkey", "${file.name} is invalid")
+ }
+
+ importingDescription.text = "Found file: \n${file.name}"
+
+ Toast.makeText(applicationContext, "Imported ${logins.size} accounts", Toast.LENGTH_SHORT).show()
+ importingDescription.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ file.delete()
+
+ for (login in logins) {
+ importingDescription.text = "${login.issuer}"
+ utilities.writeToVault(login, UUID.randomUUID().toString())
+ }
+ }
+
+ if (logins.isEmpty()) {
+ Toast.makeText(this, "No files found.", Toast.LENGTH_LONG).show()
+ finish()
+ } else {
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+
+ } catch (noDirectory: NullPointerException) {
+ initializeUI()
+ Toast.makeText(this, "Couldn't access storage. Please raise an issue on Wristkey's GitHub repo.", Toast.LENGTH_LONG).show()
+ noDirectory.printStackTrace()
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/AndOtpJSONImport.kt b/app/src/main/java/app/wristkey/AndOtpJSONImport.kt
new file mode 100644
index 0000000..0631b1d
--- /dev/null
+++ b/app/src/main/java/app/wristkey/AndOtpJSONImport.kt
@@ -0,0 +1,147 @@
+package app.wristkey
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.media.audiofx.HapticGenerator
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.widget.ImageButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import org.json.JSONArray
+import wristkey.R
+import java.io.File
+import java.io.FileReader
+import java.util.*
+
+
+class AndOtpJSONImport : Activity() {
+
+ lateinit var utilities: Utilities
+
+ lateinit var backButton: ImageButton
+ lateinit var doneButton: ImageButton
+ lateinit var importLabel: TextView
+ lateinit var description: TextView
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_andotp_jsonimport)
+
+ utilities = Utilities (applicationContext)
+
+ initializeUI()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ setContentView(R.layout.activity_andotp_jsonimport)
+ backButton = findViewById (R.id.backButton)
+ doneButton = findViewById (R.id.doneButton)
+ importLabel = findViewById (R.id.label)
+ description = findViewById (R.id.description)
+
+ description.text = getString (R.string.wristkey_import_blurb) + " " + applicationContext.filesDir.toString() + "\n\n" + getString (R.string.use_adb_blurb)
+
+ backButton.setOnClickListener {
+ backButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ finish()
+ }
+
+ doneButton.setOnClickListener {
+ doneButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, utilities.FILES_REQUEST_CODE)
+ }
+
+ }
+
+ // Function to check and request permission.
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun checkPermission(permission: String, requestCode: Int) {
+ if (ContextCompat.checkSelfPermission(this@AndOtpJSONImport, permission) == PackageManager.PERMISSION_DENIED) {
+ ActivityCompat.requestPermissions(this@AndOtpJSONImport, arrayOf(permission), requestCode)
+ } else {
+ initializeScanUI()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == utilities.FILES_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initializeScanUI()
+ } else {
+ Toast.makeText(this@AndOtpJSONImport, "Please grant Wristkey storage permissions in settings", Toast.LENGTH_LONG).show()
+ val intent = Intent (android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeScanUI () {
+ setContentView(R.layout.import_loading_screen)
+ val importingDescription = findViewById(R.id.ImportingDescription)
+
+ var logins = mutableListOf()
+
+ try {
+ val directory = File (applicationContext.filesDir.toString())
+ Log.d ("Wristkey", "Looking for files in: " + applicationContext.filesDir.toString())
+ importingDescription.text = "Looking for files in: \n${directory}"
+
+ for (file in directory.listFiles()!!) {
+
+ try {
+ val reader = FileReader(file.path)
+ val jsonData = reader.readText()
+
+ if (file.name.contains("otp_accounts") && file.name.endsWith(".json")) {
+ logins = utilities.andOtpToWristkey (JSONArray(jsonData))
+ }
+
+ } catch (_: java.io.FileNotFoundException) {
+ Log.d ("Wristkey", "${file.name} is invalid")
+ }
+
+ importingDescription.text = "Found file: \n${file.name}"
+
+ importingDescription.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ file.delete()
+
+ for (login in logins) {
+ importingDescription.text = "${login.issuer}"
+ utilities.writeToVault(login, UUID.randomUUID().toString())
+ }
+ }
+
+ if (logins.isEmpty()) {
+ Toast.makeText(this, "No files found.", Toast.LENGTH_LONG).show()
+ finish()
+ } else {
+ Toast.makeText(applicationContext, "Imported ${logins.size} accounts", Toast.LENGTH_SHORT).show()
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+
+ } catch (noDirectory: NullPointerException) {
+ initializeUI()
+ Toast.makeText(this, "Couldn't access storage. Please raise an issue on Wristkey's GitHub repo.", Toast.LENGTH_LONG).show()
+ noDirectory.printStackTrace()
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/AuthenticatorQRImport.kt b/app/src/main/java/app/wristkey/AuthenticatorQRImport.kt
new file mode 100644
index 0000000..490ef06
--- /dev/null
+++ b/app/src/main/java/app/wristkey/AuthenticatorQRImport.kt
@@ -0,0 +1,194 @@
+package app.wristkey
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.BitmapFactory
+import android.media.audiofx.HapticGenerator
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.view.View
+import android.widget.ImageButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.cardview.widget.CardView
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import wristkey.R
+import java.io.BufferedInputStream
+import java.io.File
+import java.io.FileInputStream
+import java.io.InputStream
+import java.util.*
+
+class AuthenticatorQRImport : Activity() {
+
+ lateinit var utilities: Utilities
+
+ lateinit var backButton: ImageButton
+ lateinit var doneButton: ImageButton
+ lateinit var importLabel: TextView
+ lateinit var description: TextView
+ lateinit var scanViaCameraButton: CardView
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_authenticator_qrimport)
+
+ utilities = Utilities (applicationContext)
+
+ initializeUI()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ setContentView(R.layout.activity_authenticator_qrimport)
+ backButton = findViewById (R.id.backButton)
+ doneButton = findViewById (R.id.doneButton)
+ importLabel = findViewById (R.id.label)
+ description = findViewById (R.id.description)
+ scanViaCameraButton = findViewById (R.id.scanViaCameraButton)
+
+ description.text = getString (R.string.authenticator_import_blurb) + " " + applicationContext.filesDir.toString() + "\n\n" + getString (
+ R.string.use_adb_blurb)
+
+ backButton.setOnClickListener {
+ backButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ finish()
+ }
+
+ doneButton.setOnClickListener {
+ doneButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, utilities.FILES_REQUEST_CODE)
+ }
+
+ scanViaCameraButton.setOnClickListener {
+ scanViaCameraButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ checkPermission(Manifest.permission.CAMERA, utilities.CAMERA_REQUEST_CODE)
+ }
+
+ if (applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY))
+ scanViaCameraButton.visibility = View.VISIBLE
+ else scanViaCameraButton.visibility = View.GONE
+
+ }
+
+ // Function to check and request permission.
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun checkPermission(permission: String, requestCode: Int) {
+ if (ContextCompat.checkSelfPermission(this@AuthenticatorQRImport, permission) == PackageManager.PERMISSION_DENIED) {
+ ActivityCompat.requestPermissions(this@AuthenticatorQRImport, arrayOf(permission), requestCode)
+ } else {
+ when (requestCode) {
+ utilities.FILES_REQUEST_CODE -> {
+ initializeScanUI()
+ }
+
+ utilities.CAMERA_REQUEST_CODE -> {
+ startScannerUI()
+ }
+
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == utilities.FILES_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initializeScanUI()
+ } else {
+ Toast.makeText(this@AuthenticatorQRImport, "Please grant Wristkey storage permissions in settings", Toast.LENGTH_LONG).show()
+ val intent = Intent (Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ } else if (requestCode == utilities.CAMERA_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ startScannerUI()
+ } else {
+ Toast.makeText(this@AuthenticatorQRImport, "Please grant Wristkey camera permissions in settings", Toast.LENGTH_LONG).show()
+ val intent = Intent (Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun startScannerUI () {
+ val intent = Intent (applicationContext, QRScannerActivity::class.java)
+ intent.putExtra (utilities.QR_CODE_SCAN_REQUEST, utilities.AUTHENTICATOR_EXPORT_SCAN_CODE)
+ startActivity(intent)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeScanUI () {
+ setContentView(R.layout.import_loading_screen)
+ val importingDescription = findViewById(R.id.ImportingDescription)
+
+ var logins = mutableListOf()
+
+ try {
+ val directory = File (applicationContext.filesDir.toString())
+ Log.d ("Wristkey", "Looking for files in: " + applicationContext.filesDir.toString())
+ importingDescription.text = "Looking for files in: \n${directory}"
+
+ for (file in directory.listFiles()!!) {
+
+ try {
+ if (
+ file.name.endsWith(".png", ignoreCase = true)
+ || file.name.endsWith(".jpg", ignoreCase = true)
+ || file.name.endsWith(".jpeg", ignoreCase = true)
+ ) {
+
+ val reader: InputStream = BufferedInputStream(FileInputStream(file.path))
+ val imageBitmap = BitmapFactory.decodeStream(reader)
+ val decodedQRCodeData: String = utilities.scanQRImage(imageBitmap)
+
+ logins = utilities.authenticatorToWristkey (decodedQRCodeData)
+
+ }
+ } catch (_: Exception) {
+ Log.d ("Wristkey", "${file.name} is invalid")
+ }
+
+ importingDescription.text = "Found file: \n${file.name}"
+
+ importingDescription.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ file.delete()
+ }
+
+ if (logins.isEmpty()) {
+ Toast.makeText(this, "No files found.", Toast.LENGTH_LONG).show()
+ finish()
+
+ } else {
+ for (login in logins) {
+ utilities.writeToVault(login, UUID.randomUUID().toString())
+ }
+ Toast.makeText(applicationContext, "Imported ${logins.size} accounts", Toast.LENGTH_SHORT).show()
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+
+ } catch (noDirectory: NullPointerException) {
+ initializeUI()
+ Toast.makeText(this, "Couldn't access storage. Please raise an issue on Wristkey's GitHub repo.", Toast.LENGTH_LONG).show()
+ noDirectory.printStackTrace()
+
+ } catch (_: java.io.FileNotFoundException) { }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/BitwardenJSONImport.kt b/app/src/main/java/app/wristkey/BitwardenJSONImport.kt
new file mode 100644
index 0000000..1fe0ab6
--- /dev/null
+++ b/app/src/main/java/app/wristkey/BitwardenJSONImport.kt
@@ -0,0 +1,147 @@
+package app.wristkey
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.media.audiofx.HapticGenerator
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.widget.ImageButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import org.json.JSONObject
+import wristkey.R
+import java.io.File
+import java.io.FileReader
+import java.util.*
+
+
+class BitwardenJSONImport : Activity() {
+
+ lateinit var utilities: Utilities
+
+ lateinit var backButton: ImageButton
+ lateinit var doneButton: ImageButton
+ lateinit var importLabel: TextView
+ lateinit var description: TextView
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_bitwarden_jsonimport)
+
+ utilities = Utilities (applicationContext)
+
+ initializeUI()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ setContentView(R.layout.activity_bitwarden_jsonimport)
+ backButton = findViewById (R.id.backButton)
+ doneButton = findViewById (R.id.doneButton)
+ importLabel = findViewById (R.id.label)
+ description = findViewById (R.id.description)
+
+ description.text = getString (R.string.wristkey_import_blurb) + " " + applicationContext.filesDir.toString() + "\n\n" + getString (R.string.use_adb_blurb)
+
+ backButton.setOnClickListener {
+ backButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ finish()
+ }
+
+ doneButton.setOnClickListener {
+ doneButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, utilities.FILES_REQUEST_CODE)
+ }
+
+ }
+
+ // Function to check and request permission.
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun checkPermission(permission: String, requestCode: Int) {
+ if (ContextCompat.checkSelfPermission(this@BitwardenJSONImport, permission) == PackageManager.PERMISSION_DENIED) {
+ ActivityCompat.requestPermissions(this@BitwardenJSONImport, arrayOf(permission), requestCode)
+ } else {
+ initializeScanUI()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == utilities.FILES_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initializeScanUI()
+ } else {
+ Toast.makeText(this@BitwardenJSONImport, "Please grant Wristkey storage permissions in settings", Toast.LENGTH_LONG).show()
+ val intent = Intent (android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeScanUI () {
+ setContentView(R.layout.import_loading_screen)
+ val importingDescription = findViewById(R.id.ImportingDescription)
+
+ var logins = mutableListOf()
+
+ try {
+ val directory = File (applicationContext.filesDir.toString())
+ Log.d ("Wristkey", "Looking for files in: " + applicationContext.filesDir.toString())
+ importingDescription.text = "Looking for files in: \n${directory}"
+
+ for (file in directory.listFiles()!!) {
+
+ try {
+ val reader = FileReader(file.path)
+ val jsonData = reader.readText()
+
+ if (file.name.contains("bitwarden") && file.name.endsWith(".json")) {
+ logins = utilities.bitwardenToWristkey (JSONObject(jsonData))
+ }
+
+ } catch (_: java.io.FileNotFoundException) {
+ Log.d ("Wristkey", "${file.name} is invalid")
+ }
+
+ importingDescription.text = "Found file: \n${file.name}"
+
+ importingDescription.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ file.delete()
+
+ for (login in logins) {
+ importingDescription.text = "${login.issuer}"
+ utilities.writeToVault(login, UUID.randomUUID().toString())
+ }
+ }
+
+ if (logins.isEmpty()) {
+ Toast.makeText(this, "No files found.", Toast.LENGTH_LONG).show()
+ finish()
+ } else {
+ Toast.makeText(applicationContext, "Imported ${logins.size} accounts", Toast.LENGTH_SHORT).show()
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+
+ } catch (noDirectory: NullPointerException) {
+ initializeUI()
+ Toast.makeText(this, "Couldn't access storage. Please raise an issue on Wristkey's GitHub repo.", Toast.LENGTH_LONG).show()
+ noDirectory.printStackTrace()
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/DeleteActivity.kt b/app/src/main/java/app/wristkey/DeleteActivity.kt
new file mode 100644
index 0000000..cc859e6
--- /dev/null
+++ b/app/src/main/java/app/wristkey/DeleteActivity.kt
@@ -0,0 +1,101 @@
+package app.wristkey
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import wristkey.R
+
+class DeleteActivity : AppCompatActivity() {
+
+ lateinit var utilities: Utilities
+
+ private lateinit var deleteLabel: TextView
+ private lateinit var deleteButton: ImageButton
+
+ private lateinit var backButton: ImageButton
+
+ private lateinit var uuid: String
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun onCreate (savedInstanceState: Bundle?) {
+ super.onCreate (savedInstanceState)
+ setContentView (R.layout.activity_delete)
+
+ utilities = Utilities (applicationContext)
+
+ initializeUI()
+
+ if (intent.getStringExtra(utilities.INTENT_DELETE_MODE) == utilities.INTENT_WIPE) {
+ initializeForWipe()
+ }
+
+ if (intent.hasExtra(utilities.INTENT_UUID)) {
+ uuid = intent.getStringExtra(utilities.INTENT_UUID)!!
+ initializeForDeletingLogin ()
+ }
+
+ }
+
+ private fun initializeUI () {
+ deleteLabel = findViewById (R.id.deleteLabel)
+
+ deleteButton = findViewById (R.id.deleteButton)
+
+ backButton = findViewById (R.id.backButton)
+
+ backButton.setOnClickListener {
+ finish()
+ }
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeForDeletingLogin () {
+ var login = utilities.getLogin(uuid)
+ var itemName = login?.issuer
+ if (!login?.account.isNullOrEmpty()) itemName += " (${login?.account})"
+
+ deleteLabel.text = "Would you like to delete \"$itemName\"?"
+
+ deleteButton.setOnClickListener {
+ utilities.deleteFromVault (uuid)
+ finish()
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeForWipe () {
+
+ val logins = utilities.getLogins().size
+ val settings = utilities.vault.all.size - utilities.getLogins().size
+
+ var deleteLabelString =
+ if (logins == 1) "Wipe $logins login"
+ else if (logins > 1) "Wipe $logins logins"
+ else "Reset Wristkey"
+
+ deleteLabelString +=
+ if (settings == 1 && logins != 0) " and $settings setting"
+ else if (settings > 1 && logins != 0) " and $settings settings"
+ else if (settings > 1) if (!deleteLabelString.contains("Reset")) "Reset Wristkey" else ""
+ else ""
+
+ deleteLabelString += "?"
+
+ deleteLabel.text = deleteLabelString
+ deleteButton.setOnClickListener {
+ utilities.vault.edit().clear().apply()
+ finish()
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/ExportActivity.kt b/app/src/main/java/app/wristkey/ExportActivity.kt
new file mode 100644
index 0000000..fe3108a
--- /dev/null
+++ b/app/src/main/java/app/wristkey/ExportActivity.kt
@@ -0,0 +1,187 @@
+package app.wristkey
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.view.View
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
+import org.json.JSONObject
+import wristkey.R
+import java.io.File
+import java.io.FileWriter
+import java.text.SimpleDateFormat
+import java.util.*
+
+class ExportActivity : AppCompatActivity() {
+
+ lateinit var mfaCodesTimer: Timer
+
+ lateinit var utilities: Utilities
+
+ private lateinit var clock: TextView
+
+ private lateinit var qrExportButton: CardView
+ private lateinit var fileExportButton: CardView
+
+ private lateinit var backButton: CardView
+
+ private lateinit var logins: List
+ var loginNumber = 0
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_export)
+
+ utilities = Utilities (applicationContext)
+ mfaCodesTimer = Timer()
+
+ initializeUI()
+ startClock()
+
+ }
+
+ override fun onStop() {
+ super.onStop()
+ mfaCodesTimer.cancel()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mfaCodesTimer.cancel()
+ finish()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ mfaCodesTimer = Timer()
+ }
+
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+
+ clock = findViewById(R.id.clock)
+
+ qrExportButton = findViewById (R.id.qrExportButton)
+ fileExportButton = findViewById (R.id.fileExportButton)
+
+ backButton = findViewById (R.id.backButton)
+
+ logins = utilities.getLogins()
+
+ qrExportButton.setOnClickListener {
+ exportViaQrCodes()
+ }
+
+ fileExportButton.setOnClickListener {
+ exportViaFile()
+ }
+
+ backButton.setOnClickListener {
+ finish()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun exportViaFile () {
+
+ if (logins.isEmpty()) {
+ Toast.makeText(this, "Your vault is empty!", Toast.LENGTH_LONG).show()
+ finish()
+ return
+ }
+
+ val directory = File (applicationContext.filesDir.toString())
+
+ val rfc3339Timestamp = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault()).format(Date())
+
+ val filename = directory.absolutePath + '/' + rfc3339Timestamp + ".wfs"
+
+ Log.d ("Wristkey", "Writing export file to: " + applicationContext.filesDir.toString())
+
+ val writer = FileWriter(filename)
+ writer.write(JSONObject(utilities.getVaultLoginsOnly()).toString(4))
+ writer.flush()
+ writer.close()
+
+ Toast.makeText(this, "Exported Wristkey vault to ${directory.absolutePath}", Toast.LENGTH_LONG).show()
+ finish()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun exportViaQrCodes() {
+
+ if (logins.isEmpty()) {
+ Toast.makeText(this, "Your vault is empty!", Toast.LENGTH_LONG).show()
+ finish()
+ return
+ }
+
+ val intent = Intent (applicationContext, QRCodeActivity::class.java)
+ intent.putExtra (utilities.INTENT_UUID, utilities.getUuid(logins[loginNumber]))
+ loginNumber += 1
+ startActivityForResult (intent, utilities.EXPORT_RESPONSE_CODE)
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ when (requestCode) {
+ utilities.EXPORT_RESPONSE_CODE -> {
+ if (logins.size > 2) {
+ if (loginNumber < logins.size) {
+ val intent = Intent (applicationContext, QRCodeActivity::class.java)
+ intent.putExtra (utilities.INTENT_UUID, utilities.getUuid(logins[loginNumber]))
+ loginNumber += 1
+ startActivityForResult (intent, utilities.EXPORT_RESPONSE_CODE)
+ } else {
+ Toast.makeText(applicationContext, "Done!", Toast.LENGTH_SHORT).show()
+ qrExportButton.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ }
+ }
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun startClock () {
+
+ if (!utilities.vault.getBoolean(utilities.SETTINGS_CLOCK_ENABLED, true)) {
+ findViewById(R.id.clockBackground).visibility = View.GONE
+ }
+
+ try {
+ mfaCodesTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ val currentHour24 = SimpleDateFormat("HH", Locale.getDefault()).format(Date())
+ val currentHour = SimpleDateFormat("hh", Locale.getDefault()).format(Date())
+ val currentMinute = SimpleDateFormat("mm", Locale.getDefault()).format(Date())
+ val currentSecond = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt()
+ val currentAmPm = SimpleDateFormat("a", Locale.getDefault()).format(Date())
+ runOnUiThread {
+ try {
+
+ if (utilities.vault.getBoolean(utilities.SETTINGS_24H_CLOCK_ENABLED, false)) {
+ clock.text = "$currentHour24:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour24 $currentMinute"
+ } else {
+ clock.text = "$currentHour:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour $currentMinute"
+ }
+
+ } catch (_: Exception) { }
+ }
+ }
+ }, 0, 1000) // 1000 milliseconds = 1 second
+ } catch (_: IllegalStateException) { }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/MainActivity.kt b/app/src/main/java/app/wristkey/MainActivity.kt
new file mode 100644
index 0000000..df37567
--- /dev/null
+++ b/app/src/main/java/app/wristkey/MainActivity.kt
@@ -0,0 +1,525 @@
+package app.wristkey
+
+import android.annotation.SuppressLint
+import android.app.KeyguardManager
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Intent
+import android.media.audiofx.HapticGenerator
+import android.os.Build
+import android.os.Bundle
+import android.view.HapticFeedbackConstants
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.view.animation.AlphaAnimation
+import android.view.animation.AnimationUtils
+import android.widget.*
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import dev.turingcomplete.kotlinonetimepassword.*
+import wristkey.R
+import java.text.SimpleDateFormat
+import java.util.*
+import java.util.concurrent.TimeUnit
+import kotlin.concurrent.thread
+import kotlin.math.abs
+import kotlin.properties.Delegates
+
+
+const val CODE_AUTHENTICATION_VERIFICATION = 241
+
+class MainActivity : AppCompatActivity() {
+
+ lateinit var mfaCodesTimer: Timer
+ lateinit var utilities: Utilities
+
+ private lateinit var clock: TextView
+ private lateinit var roundTimeLeft: ProgressBar
+ private lateinit var squareTimeLeft: ProgressBar
+ private lateinit var loginsRecycler: RecyclerView
+ private lateinit var addAccountButton: CardView
+ private lateinit var settingsButton: CardView
+ private lateinit var aboutButton: CardView
+
+ private lateinit var vault: kotlin.collections.List
+ private lateinit var keys: kotlin.collections.List
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ @SuppressLint("WrongConstant")
+ override fun onCreate(savedInstanceState: Bundle?) {
+
+ super.onCreate(savedInstanceState)
+
+ utilities = Utilities (applicationContext)
+ mfaCodesTimer = Timer()
+
+ lockScreen()
+ }
+
+ override fun onStop() {
+ super.onStop()
+ mfaCodesTimer.cancel()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mfaCodesTimer.cancel()
+ finish()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ mfaCodesTimer = Timer()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mfaCodesTimer = Timer()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun setShape () {
+ if (
+ utilities.vault.getBoolean (
+ utilities.CONFIG_SCREEN_ROUND,
+ resources.configuration.isScreenRound
+ )
+ ) {
+ roundTimeLeft.visibility = View.VISIBLE
+ squareTimeLeft.visibility = View.GONE
+ } else {
+ roundTimeLeft.visibility = View.GONE
+ squareTimeLeft.visibility = View.VISIBLE
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ setContentView(R.layout.activity_main)
+
+ vault = utilities.getVault()
+ keys = utilities.vault.all.keys.toList()
+
+ clock = findViewById(R.id.clock)
+
+ loginsRecycler = findViewById(R.id.loginsRecycler)
+
+ roundTimeLeft = findViewById(R.id.RoundTimeLeft)
+ squareTimeLeft = findViewById(R.id.SquareTimeLeftTop)
+
+ addAccountButton = findViewById(R.id.AddAccountButton)
+ settingsButton = findViewById(R.id.SettingsButton)
+ aboutButton = findViewById(R.id.AboutButton)
+
+ val logins = utilities.getLogins().toMutableList()
+
+ val adapter = LoginsAdapter(logins)
+
+ loginsRecycler.layoutManager = LinearLayoutManager(this@MainActivity)
+ loginsRecycler.adapter = adapter
+ loginsRecycler.invalidate()
+ loginsRecycler.refreshDrawableState()
+ loginsRecycler.scheduleLayoutAnimation()
+ loginsRecycler.setItemViewCacheSize(vault.size)
+
+ addAccountButton.animation = AnimationUtils.loadAnimation(applicationContext, R.anim.slide_right)
+ settingsButton.animation = AnimationUtils.loadAnimation(applicationContext, R.anim.slide_right)
+ aboutButton.animation = AnimationUtils.loadAnimation(applicationContext, R.anim.slide_right)
+
+ addAccountButton.setOnClickListener {
+ startActivity(Intent(applicationContext, AddActivity::class.java))
+ aboutButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ aboutButton.setOnClickListener {
+ startActivity(Intent(applicationContext, AboutActivity::class.java))
+ aboutButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ settingsButton.setOnClickListener {
+ startActivity(Intent(applicationContext, SettingsActivity::class.java))
+ aboutButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ }
+
+ private fun start2faTimer () {
+
+ thread {
+
+ mfaCodesTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ val currentSecond = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt()
+ var halfMinuteElapsed = abs((60-currentSecond))
+ if (halfMinuteElapsed >= 30) halfMinuteElapsed -= 30
+ try {
+ roundTimeLeft.progress = halfMinuteElapsed
+ squareTimeLeft.progress = halfMinuteElapsed
+ } catch (_: Exception) {
+ runOnUiThread {
+ roundTimeLeft.progress = halfMinuteElapsed
+ squareTimeLeft.progress = halfMinuteElapsed
+ }
+ }
+ }
+ }, 0, 1000) // 1000 milliseconds = 1 second
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun startClock () {
+
+ if (!utilities.vault.getBoolean(utilities.SETTINGS_CLOCK_ENABLED, true)) {
+ findViewById(R.id.clockBackground).visibility = View.GONE
+ }
+
+ try {
+ mfaCodesTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ val currentHour24 = SimpleDateFormat("HH", Locale.getDefault()).format(Date())
+ val currentHour = SimpleDateFormat("hh", Locale.getDefault()).format(Date())
+ val currentMinute = SimpleDateFormat("mm", Locale.getDefault()).format(Date())
+ val currentSecond = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt()
+ val currentAmPm = SimpleDateFormat("a", Locale.getDefault()).format(Date())
+ runOnUiThread {
+ try {
+
+ if (utilities.vault.getBoolean(utilities.SETTINGS_24H_CLOCK_ENABLED, false)) {
+ clock.text = "$currentHour24:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour24 $currentMinute"
+ } else {
+ clock.text = "$currentHour:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour $currentMinute"
+ }
+
+ } catch (_: Exception) { }
+ }
+ }
+ }, 0, 1000) // 1000 milliseconds = 1 second
+ } catch (_: IllegalStateException) { }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun lockScreen () {
+ if (utilities.vault.getBoolean(utilities.SETTINGS_LOCK_ENABLED, false)) {
+ val lockscreen = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
+ if (lockscreen.isKeyguardSecure) {
+ val i = lockscreen.createConfirmDeviceCredentialIntent ("Wristkey", "App locked")
+ startActivityForResult(i, CODE_AUTHENTICATION_VERIFICATION)
+ }
+ } else {
+ initializeUI()
+ setShape()
+ startClock()
+ start2faTimer()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if (!(resultCode == RESULT_OK && requestCode == CODE_AUTHENTICATION_VERIFICATION)) {
+ finish()
+ } else {
+ initializeUI()
+ setShape()
+ startClock()
+ start2faTimer()
+ }
+ }
+
+ inner class LoginsAdapter (private val logins: MutableList) : RecyclerView.Adapter() {
+
+ lateinit var blinkAnimation: AlphaAnimation
+ lateinit var singleBlinkAnimation: AlphaAnimation
+ var beepEnabled by Delegates.notNull()
+ var hapticsEnabled by Delegates.notNull()
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) : ViewHolder { // create new views
+ val loginCard: View = LayoutInflater.from(parent.context).inflate(R.layout.login_card, parent, false)
+ loginCard.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)
+ return ViewHolder(loginCard)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onBindViewHolder(loginCard: ViewHolder, position: Int) { // binds the list items to a view
+
+ loginCard.setIsRecyclable(false)
+
+ val login = logins[loginCard.adapterPosition]
+
+ lateinit var algorithm: HmacAlgorithm
+ if (login.algorithm!!.contains(utilities.ALGO_SHA1)) algorithm = HmacAlgorithm.SHA1
+ else if (login.algorithm.contains(utilities.ALGO_SHA256)) algorithm = HmacAlgorithm.SHA256
+ else if (login.algorithm.contains(utilities.ALGO_SHA512)) algorithm = HmacAlgorithm.SHA512
+
+ loginCard.name.text = login.issuer
+
+ if (!login.account.isNullOrBlank()) loginCard.label.text = login.account
+ else if (!login.label.isNullOrBlank()) loginCard.label.text = login.label
+ else loginCard.label.visibility = View.GONE
+
+ var otp: String?
+
+ when (login.mode) {
+ utilities.MFA_TIME_MODE -> {
+
+ when (login.period) {
+ 15, 60 -> {
+ loginCard.counterControls.visibility = View.VISIBLE
+ loginCard.incrementCounter.visibility = View.GONE
+ loginCard.decrementCounter.visibility = View.GONE
+ loginCard.counter.visibility = View.VISIBLE
+ loginCard.counter.text = "${login.period}s"
+ }
+
+ 30 -> loginCard.counterControls.visibility = View.GONE
+
+ }
+
+ val config = TimeBasedOneTimePasswordConfig (
+ timeStep = login.period!!.toLong(),
+ timeStepUnit = TimeUnit.SECONDS,
+ codeDigits = login.digits!!,
+ hmacAlgorithm = algorithm
+ )
+
+ otp = TimeBasedOneTimePasswordGenerator(login.secret!!.toByteArray(), config).generate()
+ if (login.algorithm == utilities.ALGO_SHA1 && login.period == 30 && login.digits == 6)
+ otp = GoogleAuthenticator(login.secret.toByteArray()).generate()
+
+ loginCard.code.text =
+ if (otp.length == 6) otp.replace("...".toRegex(), "$0 ") else otp.replace("....".toRegex(), "$0 ")
+
+ var timerElapsed: Int
+ try {
+ mfaCodesTimer.scheduleAtFixedRate (object : TimerTask() {
+ override fun run() {
+ timerElapsed = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt()
+ timerElapsed = 60 - timerElapsed
+
+ when (login.period) {
+ 15 -> when(timerElapsed) {
+ 15, 30, 45, 60 -> {
+ try { loginCard.code.startAnimation(blinkAnimation) } catch (_: Exception) { }
+ if (beepEnabled) utilities.beep()
+ if (hapticsEnabled) loginCard.name.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ runOnUiThread {
+ loginCard.code.text =
+ if (otp!!.length == 6) otp!!.replace("...".toRegex(), "$0 ")
+ else otp!!.replace("....".toRegex(), "$0 ")
+ }
+ }
+ }
+
+ 30 -> when(timerElapsed) {
+ 30, 60 -> {
+ try { loginCard.code.startAnimation(blinkAnimation) } catch (_: Exception) { }
+ if (beepEnabled) utilities.beep()
+ if (hapticsEnabled) loginCard.name.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ runOnUiThread {
+ loginCard.code.text =
+ if (otp!!.length == 6) otp!!.replace("...".toRegex(), "$0 ")
+ else otp!!.replace("....".toRegex(), "$0 ")
+ }
+ }
+ }
+
+ 60 -> when(timerElapsed) {
+ 60 -> {
+ try { loginCard.code.startAnimation(blinkAnimation) } catch (_: Exception) { }
+ if (beepEnabled) utilities.beep()
+ if (hapticsEnabled) loginCard.name.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ runOnUiThread {
+ loginCard.code.text =
+ if (otp!!.length == 6) otp!!.replace("...".toRegex(), "$0 ")
+ else otp!!.replace("....".toRegex(), "$0 ")
+ }
+ }
+ }
+ }
+
+ otp = TimeBasedOneTimePasswordGenerator(login.secret!!.toByteArray(), config).generate()
+ if (login.algorithm == utilities.ALGO_SHA1 && login.period == 30 && login.digits == 6)
+ otp = GoogleAuthenticator(login.secret.toByteArray()).generate()
+ }
+ }, 0, 1000) // 1000 milliseconds = 1 second
+ } catch (_: Exception) { }
+ }
+
+ utilities.MFA_COUNTER_MODE -> {
+
+ loginCard.counterControls.visibility = View.VISIBLE
+
+ val config = HmacOneTimePasswordConfig (
+ codeDigits = login.digits!!,
+ hmacAlgorithm = algorithm
+ )
+
+ var currentCount: Long
+
+ currentCount = login.counter!!
+ loginCard.counter.text = currentCount.toString()
+
+ try {
+
+ loginCard.incrementCounter.setOnClickListener {
+ currentCount = loginCard.counter.text.toString().toLong()
+ loginCard.counter.text = (currentCount + 1).toString()
+
+ otp = HmacOneTimePasswordGenerator(login.secret!!.toByteArray(), config)
+ .generate(loginCard.counter.text.toString().toLong())
+
+ loginCard.code.text =
+ if (otp!!.length == 6) otp!!.replace("...".toRegex(), "$0 ") else otp!!.replace("....".toRegex(), "$0 ")
+
+ try {
+ loginCard.code.startAnimation(singleBlinkAnimation)
+ loginCard.incrementCounter.startAnimation(singleBlinkAnimation)
+ } catch (_: Exception) { }
+
+ loginCard.incrementCounter.performHapticFeedback(HapticGenerator.ALREADY_EXISTS)
+ if (beepEnabled) utilities.beep()
+ if (hapticsEnabled) loginCard.incrementCounter.performHapticFeedback(HapticFeedbackConstants.REJECT)
+
+ if (currentCount+1 == 69L) Toast.makeText(applicationContext, "Nice ;)", Toast.LENGTH_SHORT).show()
+
+ val loginData = Utilities.MfaCode (
+ type = utilities.DEFAULT_TYPE,
+ mode = login.mode,
+ issuer = login.issuer,
+ account = login.account,
+ secret = login.secret,
+ algorithm = login.algorithm,
+ digits = login.digits,
+ period = login.period,
+ lock = false,
+ counter = loginCard.counter.text.toString().toLong(),
+ label = login.label
+ )
+
+ utilities.overwriteLogin(oldLogin = login, newLogin = loginData)
+
+ }
+
+ loginCard.decrementCounter.setOnClickListener {
+ currentCount = loginCard.counter.text.toString().toLong()
+ if (currentCount != 0L) {
+ loginCard.counter.text = (currentCount - 1).toString()
+ }
+
+ otp = HmacOneTimePasswordGenerator(login.secret!!.toByteArray(), config)
+ .generate(loginCard.counter.text.toString().toLong())
+
+ loginCard.code.text =
+ if (otp!!.length == 6) otp!!.replace("...".toRegex(), "$0 ") else otp!!.replace("....".toRegex(), "$0 ")
+
+ try {
+ loginCard.code.startAnimation(singleBlinkAnimation)
+ loginCard.incrementCounter.startAnimation(singleBlinkAnimation)
+ } catch (_: Exception) { }
+
+ loginCard.decrementCounter.performHapticFeedback(HapticGenerator.ALREADY_EXISTS)
+ if (beepEnabled) utilities.beep()
+ if (hapticsEnabled) loginCard.incrementCounter.performHapticFeedback(HapticFeedbackConstants.REJECT)
+
+ if (currentCount-1 == 69L) Toast.makeText(applicationContext, "Nice ;)", Toast.LENGTH_SHORT).show()
+
+ val loginData = Utilities.MfaCode (
+ type = utilities.DEFAULT_TYPE,
+ mode = login.mode,
+ issuer = login.issuer,
+ account = login.account,
+ secret = login.secret,
+ algorithm = login.algorithm,
+ digits = login.digits,
+ period = login.period,
+ lock = false,
+ counter = loginCard.counter.text.toString().toLong(),
+ label = login.label
+ )
+
+ utilities.overwriteLogin(oldLogin = login, newLogin = loginData)
+
+ }
+
+ } catch (_: IllegalStateException) { }
+
+ otp = HmacOneTimePasswordGenerator(login.secret!!.toByteArray(), config)
+ .generate(loginCard.counter.text.toString().toLong())
+
+ loginCard.code.text =
+ if (otp!!.length == 6) otp!!.replace("...".toRegex(), "$0 ") else otp!!.replace("....".toRegex(), "$0 ")
+
+ }
+ }
+
+ // tap on totp / mfa / 2fa
+ loginCard.code.setOnClickListener {
+ val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("Wristkey", loginCard.code.text.toString())
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(applicationContext, "Code copied!", Toast.LENGTH_LONG).show()
+ }
+
+ loginCard.loginCard.setOnTouchListener(object : OnSwipeTouchListener(this@MainActivity) {
+
+ override fun onLongClick() {
+ val intent = Intent(applicationContext, ManualEntryActivity::class.java)
+ intent.putExtra(utilities.INTENT_UUID, utilities.getUuid(login))
+ startActivity(intent)
+ loginCard.loginCard.performHapticFeedback(HapticGenerator.SUCCESS)
+ super.onSwipeRight()
+ }
+
+ })
+
+ }
+
+ override fun getItemCount(): Int { // return the number of the items in the list
+ return logins.size
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ inner class ViewHolder(ItemView: View) : RecyclerView.ViewHolder(ItemView) { // Holds the views for adding it to image and text
+ val loginCard: CardView = itemView.findViewById(R.id.loginCard)
+
+ val name: TextView = itemView.findViewById(R.id.name)
+ val label: TextView = itemView.findViewById(R.id.label)
+ val code: TextView = itemView.findViewById(R.id.code)
+
+ val counterControls: LinearLayout = itemView.findViewById(R.id.counterControls)
+ val incrementCounter: ImageView = itemView.findViewById(R.id.increment_counter)
+ val counter: TextView = itemView.findViewById(R.id.counter)
+ val decrementCounter: ImageView = itemView.findViewById(R.id.decrement_counter)
+
+ init {
+ name.isSelected = true
+ label.isSelected = true
+
+ blinkAnimation = AlphaAnimation (0.25f, 1f)
+ blinkAnimation.duration = 500
+ blinkAnimation.startOffset = 20
+ blinkAnimation.repeatCount = 1
+
+ singleBlinkAnimation = AlphaAnimation (0.25f, 1f)
+ singleBlinkAnimation.duration = 500
+ singleBlinkAnimation.startOffset = 20
+ singleBlinkAnimation.repeatCount = 0
+
+ beepEnabled = utilities.vault.getBoolean(utilities.SETTINGS_BEEP_ENABLED, false)
+ hapticsEnabled = utilities.vault.getBoolean(utilities.SETTINGS_HAPTICS_ENABLED, true)
+
+ }
+
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/ManualEntryActivity.kt b/app/src/main/java/app/wristkey/ManualEntryActivity.kt
new file mode 100644
index 0000000..4c706dc
--- /dev/null
+++ b/app/src/main/java/app/wristkey/ManualEntryActivity.kt
@@ -0,0 +1,251 @@
+package app.wristkey
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.text.method.PasswordTransformationMethod
+import android.view.HapticFeedbackConstants
+import android.view.View
+import android.view.WindowManager
+import android.widget.SeekBar
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
+import com.google.android.material.textfield.TextInputEditText
+import wristkey.R
+import java.util.*
+import kotlin.properties.Delegates
+
+
+class ManualEntryActivity : AppCompatActivity() {
+
+ lateinit var utilities: Utilities
+
+ private lateinit var issuerInput: TextInputEditText
+ private lateinit var labelInput: TextInputEditText
+ private lateinit var secretInput: TextInputEditText
+
+ private lateinit var typeLabel: TextView
+ private lateinit var periodLabel: TextView
+ private lateinit var hashLabel: TextView
+ private lateinit var lengthLabel: TextView
+
+ private lateinit var modeSeekbar: SeekBar
+ private lateinit var periodSeekbar: SeekBar
+ private lateinit var hashSeekbar: SeekBar
+ private lateinit var lengthSeekbar: SeekBar
+
+ private lateinit var showQrCodeButton: CardView
+ private lateinit var doneButton: CardView
+ private lateinit var deleteButton: CardView
+ private lateinit var backButton: CardView
+
+ private lateinit var mode: String
+ private lateinit var hashingAlgorithm: String
+ private var length by Delegates.notNull()
+ private var period by Delegates.notNull()
+
+ private lateinit var uuid: String
+
+ private lateinit var loginData: Utilities.MfaCode
+
+ @RequiresApi(Build.VERSION_CODES.N)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_manual_entry)
+
+ window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
+
+ utilities = Utilities(applicationContext)
+ uuid = UUID.randomUUID().toString()
+
+ initializeUI()
+
+ if (intent.hasExtra(utilities.INTENT_UUID)) {
+ uuid = intent.getStringExtra(utilities.INTENT_UUID)!!
+ loadLogin ()
+ }
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ issuerInput = findViewById (R.id.issuerInput)
+ labelInput = findViewById (R.id.labelInput)
+ secretInput = findViewById (R.id.secretInput)
+
+ modeSeekbar = findViewById (R.id.modeSeekbar)
+ hashSeekbar = findViewById (R.id.hashSeekbar)
+ lengthSeekbar = findViewById (R.id.lengthSeekbar)
+ periodSeekbar = findViewById (R.id.periodSeekbar)
+
+ typeLabel = findViewById (R.id.typeLabel)
+ hashLabel = findViewById (R.id.hashLabel)
+ lengthLabel = findViewById (R.id.lengthLabel)
+ periodLabel = findViewById (R.id.periodLabel)
+
+ showQrCodeButton = findViewById (R.id.showQrCodeButton)
+ doneButton = findViewById (R.id.doneButton)
+ deleteButton = findViewById (R.id.deleteButton)
+ backButton = findViewById (R.id.backButton)
+
+ secretInput.transformationMethod = PasswordTransformationMethod()
+
+ mode = "totp"
+ modeSeekbar.setOnSeekBarChangeListener (object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged (p0: SeekBar?, p1: Int, p2: Boolean) {
+ when (p0?.progress) {
+ 0 -> {
+ typeLabel.text = "Time"
+ mode = "totp"
+ periodSeekbar.visibility = View.VISIBLE
+ periodLabel.visibility = View.VISIBLE
+ }
+
+ 1 -> {
+ typeLabel.text = "Counter"
+ mode = "hotp"
+ periodSeekbar.visibility = View.GONE
+ periodLabel.visibility = View.GONE
+ }
+ }
+ }
+ override fun onStartTrackingTouch (seekBar: SeekBar?) { }
+ override fun onStopTrackingTouch (seekBar: SeekBar?) { }
+ })
+
+ hashingAlgorithm = "SHA1"
+ hashSeekbar.setOnSeekBarChangeListener (object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged (p0: SeekBar?, p1: Int, p2: Boolean) {
+ when (p0?.progress) {
+ 0 -> hashingAlgorithm = utilities.ALGO_SHA1
+ 1 -> hashingAlgorithm = utilities.ALGO_SHA256
+ 2 -> hashingAlgorithm = utilities.ALGO_SHA512
+ }
+ hashLabel.text = hashingAlgorithm
+ }
+ override fun onStartTrackingTouch (seekBar: SeekBar?) { }
+ override fun onStopTrackingTouch (seekBar: SeekBar?) { }
+ })
+
+ length = 6
+ lengthSeekbar.setOnSeekBarChangeListener (object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged (p0: SeekBar?, p1: Int, p2: Boolean) {
+ when (p0?.progress) {
+ 0 -> length = 6
+ 1 -> length = 8
+ }
+ lengthLabel.text = "$length digits"
+ }
+ override fun onStartTrackingTouch (seekBar: SeekBar?) { }
+ override fun onStopTrackingTouch (seekBar: SeekBar?) { }
+ })
+
+ period = 30
+ periodSeekbar.setOnSeekBarChangeListener (object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged (p0: SeekBar?, p1: Int, p2: Boolean) {
+ when (p0?.progress) {
+ 0 -> period = 15
+ 1 -> period = 30
+ 2 -> period = 60
+ }
+ periodLabel.text = "$period seconds"
+ }
+ override fun onStartTrackingTouch (seekBar: SeekBar?) { }
+ override fun onStopTrackingTouch (seekBar: SeekBar?) { }
+ })
+
+ doneButton.setOnClickListener {
+
+ if (issuerInput.length() < 2) {
+ Toast.makeText(applicationContext, "Enter issuer name", Toast.LENGTH_SHORT).show()
+ return@setOnClickListener
+ }
+
+ if (secretInput.length() <= 7) {
+ Toast.makeText(applicationContext, "Enter a valid secret", Toast.LENGTH_SHORT).show()
+ return@setOnClickListener
+ }
+
+ loginData = Utilities.MfaCode (
+ type = utilities.DEFAULT_TYPE,
+ mode = mode,
+ issuer = issuerInput.text.toString(),
+ account = if (!labelInput.text.isNullOrEmpty()) labelInput.text.toString() else "",
+ secret = secretInput.text.toString(),
+ algorithm = hashingAlgorithm,
+ digits = length,
+ period = period,
+ lock = false,
+ counter = 0,
+ label = labelInput.text.toString()
+ )
+
+ utilities.writeToVault(loginData, uuid)
+
+ finish()
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+
+ }
+
+ showQrCodeButton.visibility = View.GONE
+ deleteButton.visibility = View.GONE
+
+ backButton.setOnClickListener {
+ finish()
+ }
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun loadLogin () {
+ val login = utilities.getLogin(uuid)
+
+ issuerInput.setText (login?.issuer.toString())
+
+ val label = if (login?.account.isNullOrEmpty()) login?.label.toString() else login?.account
+ labelInput.setText (label)
+ secretInput.setText (login?.secret.toString())
+
+ if (login?.mode == utilities.MFA_TIME_MODE) modeSeekbar.progress = 0 else modeSeekbar.progress = 1
+
+ when (login?.algorithm) {
+ utilities.ALGO_SHA1 -> hashSeekbar.progress = 0
+ utilities.ALGO_SHA256 -> hashSeekbar.progress = 1
+ utilities.ALGO_SHA512 -> hashSeekbar.progress = 2
+ }
+
+ when (login?.digits) {
+ 6 -> lengthSeekbar.progress = 0
+ 8 -> lengthSeekbar.progress = 1
+ }
+
+ when (login?.period) {
+ 15 -> periodSeekbar.progress = 0
+ 30 -> periodSeekbar.progress = 1
+ 60 -> periodSeekbar.progress = 2
+ }
+
+ showQrCodeButton.visibility = View.VISIBLE
+
+ showQrCodeButton.setOnClickListener {
+ val intent = Intent(applicationContext, QRCodeActivity::class.java)
+ intent.putExtra(utilities.INTENT_UUID, uuid)
+ startActivity(intent)
+ deleteButton.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ }
+
+ deleteButton.visibility = View.VISIBLE
+
+ deleteButton.setOnClickListener {
+ val intent = Intent(applicationContext, DeleteActivity::class.java)
+ intent.putExtra(utilities.INTENT_UUID, uuid)
+ startActivity(intent)
+ deleteButton.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/OtpAuthImport.kt b/app/src/main/java/app/wristkey/OtpAuthImport.kt
new file mode 100644
index 0000000..72ecd14
--- /dev/null
+++ b/app/src/main/java/app/wristkey/OtpAuthImport.kt
@@ -0,0 +1,200 @@
+package app.wristkey
+
+import android.Manifest
+import android.app.Activity
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.BitmapFactory
+import android.media.audiofx.HapticGenerator
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.provider.Settings
+import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.view.View
+import android.widget.ImageButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.cardview.widget.CardView
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import wristkey.R
+import java.io.*
+import java.util.*
+
+class OtpAuthImport : Activity() {
+
+ lateinit var utilities: Utilities
+
+ lateinit var backButton: ImageButton
+ lateinit var doneButton: ImageButton
+ lateinit var scanViaCameraButton: CardView
+ lateinit var importLabel: TextView
+ lateinit var description: TextView
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_otpauth_import)
+
+ utilities = Utilities (applicationContext)
+
+ initializeUI()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ setContentView(R.layout.activity_otpauth_import)
+ importLabel = findViewById (R.id.label)
+ description = findViewById (R.id.description)
+
+ scanViaCameraButton = findViewById (R.id.scanViaCameraButton)
+ backButton = findViewById (R.id.backButton)
+ doneButton = findViewById (R.id.doneButton)
+
+ description.text = getString (R.string.otpauth_import_blurb) + " " + applicationContext.filesDir.toString() + "\n\n" + getString (
+ R.string.use_adb_blurb)
+
+ backButton.setOnClickListener {
+ backButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ finish()
+ }
+
+ doneButton.setOnClickListener {
+ doneButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, utilities.FILES_REQUEST_CODE)
+ }
+
+ scanViaCameraButton.setOnClickListener {
+ scanViaCameraButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ checkPermission(Manifest.permission.CAMERA, utilities.CAMERA_REQUEST_CODE)
+ }
+
+ if (applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY))
+ scanViaCameraButton.visibility = View.VISIBLE
+ else scanViaCameraButton.visibility = View.GONE
+
+ }
+
+ // Function to check and request permission.
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun checkPermission(permission: String, requestCode: Int) {
+ if (ContextCompat.checkSelfPermission(this@OtpAuthImport, permission) == PackageManager.PERMISSION_DENIED) {
+ ActivityCompat.requestPermissions(this@OtpAuthImport, arrayOf(permission), requestCode)
+ } else {
+ when (requestCode) {
+ utilities.FILES_REQUEST_CODE -> {
+ initializeScanUI()
+ }
+
+ utilities.CAMERA_REQUEST_CODE -> {
+ startScannerUI()
+ }
+
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == utilities.FILES_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initializeScanUI()
+ } else {
+ Toast.makeText(this@OtpAuthImport, "Please grant Wristkey storage permissions in settings", Toast.LENGTH_LONG).show()
+ val intent = Intent (Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ } else if (requestCode == utilities.CAMERA_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ startScannerUI()
+ } else {
+ Toast.makeText(this@OtpAuthImport, "Please grant Wristkey camera permissions in settings", Toast.LENGTH_LONG).show()
+ val intent = Intent (Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun startScannerUI () {
+ val intent = Intent (applicationContext, QRScannerActivity::class.java)
+ intent.putExtra (utilities.QR_CODE_SCAN_REQUEST, utilities.OTPAUTH_SCAN_CODE)
+ startActivity(intent)
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeScanUI () {
+ setContentView(R.layout.import_loading_screen)
+ val importingDescription = findViewById(R.id.ImportingDescription)
+
+ val logins = mutableListOf()
+
+ try {
+ val directory = File (applicationContext.filesDir.toString())
+ Log.d ("Wristkey", "Looking for files in: " + applicationContext.filesDir.toString())
+ importingDescription.text = "Looking for files in: \n${directory}"
+
+ for (file in directory.listFiles()!!) {
+
+ try {
+ val fileData = FileReader(file.path).readText()
+
+ if (
+ file.name.endsWith(".png", ignoreCase = true)
+ || file.name.endsWith(".jpg", ignoreCase = true)
+ || file.name.endsWith(".jpeg", ignoreCase = true)
+ ) {
+
+ val reader: InputStream = BufferedInputStream(FileInputStream(file.path))
+ val imageBitmap = BitmapFactory.decodeStream(reader)
+ val decodedQRCodeData: String = utilities.scanQRImage(imageBitmap)
+
+ if (decodedQRCodeData.contains("otpauth://") && !decodedQRCodeData.contains("otpauth-migration://"))
+ logins. add(utilities.decodeOTPAuthURL (decodedQRCodeData)!!)
+ else if (decodedQRCodeData.contains("otpauth-migration://")) {
+ Toast.makeText(this, "This appears to be a Google Authenticator export. Please choose that option instead to proceed.", Toast.LENGTH_LONG).show()
+ break
+ }
+
+ }
+
+ } catch (_: Exception) {
+ Log.d ("Wristkey", "${file.name} is invalid")
+ }
+
+ importingDescription.text = "Found file: \n${file.name}"
+ importingDescription.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ file.delete()
+
+ }
+
+ if (logins.isEmpty()) {
+ Toast.makeText(this, "No files found.", Toast.LENGTH_LONG).show()
+ finish()
+
+ } else {
+ for (login in logins) {
+ utilities.writeToVault(login, UUID.randomUUID().toString())
+ }
+ Toast.makeText(applicationContext, "Imported ${logins.size} accounts", Toast.LENGTH_SHORT).show()
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+
+ } catch (noDirectory: NullPointerException) {
+ initializeUI()
+ Toast.makeText(this, "Couldn't access storage. Please raise an issue on Wristkey's GitHub repo.", Toast.LENGTH_LONG).show()
+ noDirectory.printStackTrace()
+
+ } catch (_: java.io.FileNotFoundException) { }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/QRCodeActivity.kt b/app/src/main/java/app/wristkey/QRCodeActivity.kt
new file mode 100644
index 0000000..96f69b1
--- /dev/null
+++ b/app/src/main/java/app/wristkey/QRCodeActivity.kt
@@ -0,0 +1,191 @@
+package app.wristkey
+
+import android.app.Activity
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.Point
+import android.graphics.drawable.BitmapDrawable
+import android.media.audiofx.HapticGenerator
+import android.os.Build
+import android.os.Bundle
+import android.view.View
+import android.view.WindowManager
+import android.widget.*
+import androidmads.library.qrgenearator.QRGContents
+import androidmads.library.qrgenearator.QRGEncoder
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.google.zxing.WriterException
+import wristkey.R
+import java.util.*
+import kotlin.concurrent.thread
+
+class QRCodeActivity : AppCompatActivity() {
+
+ lateinit var mfaCodesTimer: Timer
+
+ lateinit var utilities: Utilities
+
+ lateinit var mfaCode: Utilities.MfaCode
+
+ private lateinit var roundTimeLeft: ProgressBar
+ private lateinit var squareTimeLeft: ProgressBar
+
+ lateinit var qrCodeRoot: ConstraintLayout
+ lateinit var qrCode: ImageView
+ lateinit var qrCodeIssuer: TextView
+ lateinit var qrCodeAccount: TextView
+
+ lateinit var doneButton: ImageButton
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_qrcode)
+
+ utilities = Utilities(applicationContext)
+ mfaCodesTimer = Timer()
+
+ mfaCode = utilities.getLogin (
+ intent.getStringExtra(utilities.INTENT_UUID)!!
+ )!!
+
+ initializeUI()
+ setShape()
+ start2faTimer()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun setShape () {
+ if (
+ utilities.vault.getBoolean (
+ utilities.CONFIG_SCREEN_ROUND,
+ resources.configuration.isScreenRound
+ )
+ ) {
+ roundTimeLeft.visibility = View.VISIBLE
+ squareTimeLeft.visibility = View.GONE
+ } else {
+ roundTimeLeft.visibility = View.GONE
+ squareTimeLeft.visibility = View.VISIBLE
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ mfaCodesTimer.cancel()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mfaCodesTimer.cancel()
+ finish()
+ }
+
+ override fun onStart() {
+ super.onStart()
+ mfaCodesTimer = Timer()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ mfaCodesTimer = Timer()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun start2faTimer () {
+ try {
+ thread {
+
+ // round timer
+ var timerDuration = utilities.QR_TIMER_DURATION
+ mfaCodesTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ if (timerDuration == 0) {
+ val resultIntent = Intent()
+ setResult(Activity.RESULT_OK, resultIntent)
+ finish()
+ }
+ else timerDuration -= 1
+ try {
+ roundTimeLeft.progress = timerDuration
+ squareTimeLeft.progress = timerDuration
+ } catch (_: Exception) {
+ runOnUiThread {
+ roundTimeLeft.progress = timerDuration
+ squareTimeLeft.progress = timerDuration
+ }
+ }
+ }
+ }, 0, 1000) // 1000 milliseconds = 1 second
+
+ }
+ } catch (_: IllegalStateException) {}
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ qrCodeRoot = findViewById(R.id.qrCodeRoot)
+ qrCode = findViewById(R.id.qrCode)
+ qrCodeIssuer = findViewById(R.id.qrCodeIssuer)
+ qrCodeAccount = findViewById(R.id.qrCodeAccount)
+
+ roundTimeLeft = findViewById(R.id.RoundTimeLeft)
+ squareTimeLeft = findViewById(R.id.SquareTimeLeftTop)
+
+ doneButton = findViewById(R.id.doneButton)
+
+ qrCodeIssuer.text = mfaCode.issuer
+ qrCodeAccount.text = mfaCode.account
+
+ val qrData = utilities.encodeOTPAuthURL(mfaCode)
+
+ try {
+ qrCode.setImageDrawable(BitmapDrawable(generateQrCode(qrData!!)))
+ } catch (_: WriterException) { }
+
+ var state = 0
+ qrCode.setOnClickListener {
+ when (state) {
+
+ 0 -> {
+ state += 1
+ qrCode.imageTintList = ColorStateList.valueOf(Color.parseColor("#818181"))
+ Toast.makeText(this, "Dimmed", Toast.LENGTH_SHORT).show()
+ }
+
+ 1 -> {
+ state = 0
+ qrCode.imageTintList = ColorStateList.valueOf(Color.WHITE)
+ Toast.makeText(this, "Normal", Toast.LENGTH_SHORT).show()
+ }
+
+ }
+
+ }
+
+ doneButton.setOnClickListener {
+ doneButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ finish()
+ }
+
+ }
+
+ private fun generateQrCode (qrData: String): Bitmap? {
+ val manager = getSystemService(WINDOW_SERVICE) as WindowManager
+ val display = manager.defaultDisplay
+ val point = Point()
+ display.getSize(point)
+ val width: Int = point.x
+ val height: Int = point.y
+ val dimensions = if (width < height) width else height
+
+ val qrEncoder = QRGEncoder(qrData, null, QRGContents.Type.TEXT, dimensions)
+ return qrEncoder.bitmap
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/QRScannerActivity.kt b/app/src/main/java/app/wristkey/QRScannerActivity.kt
new file mode 100644
index 0000000..3f7e8f7
--- /dev/null
+++ b/app/src/main/java/app/wristkey/QRScannerActivity.kt
@@ -0,0 +1,105 @@
+package app.wristkey
+
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.view.HapticFeedbackConstants
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import com.budiyev.android.codescanner.*
+import wristkey.R
+import java.util.*
+
+class QRScannerActivity : AppCompatActivity() {
+ private lateinit var codeScanner: CodeScanner
+
+ private lateinit var scannerView: CodeScannerView
+
+ private lateinit var scanType: String
+
+ lateinit var utilities: Utilities
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_qr_scanner)
+
+ utilities = Utilities(applicationContext)
+ scanType = intent.getStringExtra(utilities.QR_CODE_SCAN_REQUEST)!!
+
+ scannerView = findViewById(R.id.scanner_view)
+ codeScanner = CodeScanner(this, scannerView)
+ codeScanner.camera = CodeScanner.CAMERA_BACK
+ codeScanner.formats = CodeScanner.ALL_FORMATS
+ codeScanner.autoFocusMode = AutoFocusMode.CONTINUOUS
+ codeScanner.scanMode = ScanMode.SINGLE
+ codeScanner.isAutoFocusEnabled = true
+ codeScanner.isFlashEnabled = false
+
+ codeScanner.decodeCallback = DecodeCallback {
+ runOnUiThread {
+ if (scanType == utilities.OTPAUTH_SCAN_CODE) _2faScan(it.text)
+ else if (scanType == utilities.AUTHENTICATOR_EXPORT_SCAN_CODE) authenticatorExportScan(it.text)
+ finish()
+ }
+ }
+
+ codeScanner.errorCallback = ErrorCallback {
+ runOnUiThread {
+ Toast.makeText(this, "Error initializing camera", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ scannerView.setOnClickListener {
+ codeScanner.startPreview()
+ }
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun _2faScan (scanData: String) {
+ if (
+ scanData.contains("otpauth://")
+ && !scanData.contains("otpauth-migration://")
+ && scanData.contains("secret=")
+ ) {
+ Toast.makeText(this, "Saving \"${utilities.decodeOTPAuthURL(scanData)?.issuer}\"", Toast.LENGTH_LONG).show()
+ utilities.writeToVault(utilities.decodeOTPAuthURL(scanData)!!, UUID.randomUUID().toString())
+ scannerView.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ } else {
+ Toast.makeText(this, "This QR code is invalid", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun authenticatorExportScan (scanData: String) {
+ if (
+ !scanData.contains("otpauth://")
+ && scanData.contains("otpauth-migration://")
+ ) {
+ val logins = utilities.authenticatorToWristkey(scanData)
+ Toast.makeText(this, "Saving login(s)", Toast.LENGTH_LONG).show()
+ for (login in logins) {
+ utilities.writeToVault(login, UUID.randomUUID().toString())
+ }
+ scannerView.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ } else {
+ Toast.makeText(this, "This QR code is invalid", Toast.LENGTH_LONG).show()
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+ codeScanner.startPreview()
+ }
+
+ override fun onPause() {
+ codeScanner.releaseResources()
+ super.onPause()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/SettingsActivity.kt b/app/src/main/java/app/wristkey/SettingsActivity.kt
new file mode 100644
index 0000000..2efc7ef
--- /dev/null
+++ b/app/src/main/java/app/wristkey/SettingsActivity.kt
@@ -0,0 +1,204 @@
+package app.wristkey
+
+import android.app.KeyguardManager
+import android.content.Intent
+import android.media.audiofx.HapticGenerator
+import android.os.Build
+import android.os.Bundle
+import android.view.HapticFeedbackConstants
+import android.view.View
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.cardview.widget.CardView
+import com.google.android.material.switchmaterial.SwitchMaterial
+import wristkey.R
+import java.text.SimpleDateFormat
+import java.util.*
+
+class SettingsActivity : AppCompatActivity() {
+
+ lateinit var mfaCodesTimer: Timer
+ lateinit var utilities: Utilities
+
+ private lateinit var clock: TextView
+
+ lateinit var beepButton: SwitchMaterial
+ lateinit var vibrateButton: SwitchMaterial
+ lateinit var lockButton: SwitchMaterial
+ lateinit var clockButton: SwitchMaterial
+ lateinit var twentyFourHourClockButton: SwitchMaterial
+ lateinit var roundButton: SwitchMaterial
+ lateinit var deleteButton: CardView
+ lateinit var exportButton: CardView
+ lateinit var backButton: CardView
+
+ var settingsChanged = false
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_settings)
+
+ utilities = Utilities(applicationContext)
+ mfaCodesTimer = Timer()
+
+ initializeUI()
+ startClock()
+
+ }
+
+ override fun onStop() {
+ super.onStop()
+ mfaCodesTimer.cancel()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ mfaCodesTimer.cancel()
+ finish()
+
+ if (settingsChanged) {
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+
+ }
+
+ override fun onStart() {
+ super.onStart()
+ mfaCodesTimer = Timer()
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+
+ clock = findViewById(R.id.clock)
+
+ beepButton = findViewById (R.id.beepButton)
+ vibrateButton = findViewById (R.id.vibrateButton)
+ lockButton = findViewById (R.id.lockButton)
+ clockButton = findViewById (R.id.clockButton)
+ twentyFourHourClockButton = findViewById (R.id.twentyFourHourClockButton)
+ roundButton = findViewById (R.id.roundButton)
+ deleteButton = findViewById (R.id.deleteButton)
+ exportButton = findViewById (R.id.exportButton)
+ backButton = findViewById (R.id.backButton)
+
+ val clockButton: SwitchMaterial = findViewById(R.id.clockButton)
+ clockButton.isChecked = utilities.vault.getBoolean(utilities.SETTINGS_CLOCK_ENABLED, true)
+ clockButton.setOnCheckedChangeListener { _, isChecked ->
+ settingsChanged = true
+ utilities.vault.edit().remove(utilities.SETTINGS_CLOCK_ENABLED).apply()
+ utilities.vault.edit().putBoolean(utilities.SETTINGS_CLOCK_ENABLED, isChecked).apply()
+ if (!isChecked)
+ findViewById(R.id.clockBackground).visibility = View.GONE
+ else
+ findViewById(R.id.clockBackground).visibility = View.VISIBLE
+ }
+
+
+ val twentyFourHourClockButton: SwitchMaterial = findViewById(R.id.twentyFourHourClockButton)
+ twentyFourHourClockButton.isChecked = utilities.vault.getBoolean(utilities.SETTINGS_24H_CLOCK_ENABLED, true)
+ twentyFourHourClockButton.setOnCheckedChangeListener { _, isChecked ->
+ settingsChanged = true
+ utilities.vault.edit().remove(utilities.SETTINGS_24H_CLOCK_ENABLED).apply()
+ utilities.vault.edit().putBoolean(utilities.SETTINGS_24H_CLOCK_ENABLED, isChecked).apply()
+ }
+
+ val vibrateButton: SwitchMaterial = findViewById(R.id.vibrateButton)
+ vibrateButton.isChecked = utilities.vault.getBoolean(utilities.SETTINGS_HAPTICS_ENABLED, true)
+ vibrateButton.setOnCheckedChangeListener { _, isChecked ->
+ settingsChanged = true
+ utilities.vault.edit().remove(utilities.SETTINGS_HAPTICS_ENABLED).apply()
+ utilities.vault.edit().putBoolean(utilities.SETTINGS_HAPTICS_ENABLED, isChecked).apply()
+ if (isChecked) deleteButton.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ }
+
+ val beepButton: SwitchMaterial = findViewById(R.id.beepButton)
+ beepButton.isChecked = utilities.vault.getBoolean(utilities.SETTINGS_BEEP_ENABLED, false)
+ beepButton.setOnCheckedChangeListener { _, isChecked ->
+ settingsChanged = true
+ utilities.vault.edit().remove(utilities.SETTINGS_BEEP_ENABLED).apply()
+ utilities.vault.edit().putBoolean(utilities.SETTINGS_BEEP_ENABLED, isChecked).apply()
+ if (isChecked) utilities.beep()
+ }
+
+ val lockscreen = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
+ if (!lockscreen.isKeyguardSecure) utilities.vault.edit().putBoolean(utilities.SETTINGS_LOCK_ENABLED, false).apply()
+ val lockButton: SwitchMaterial = findViewById(R.id.lockButton)
+ lockButton.isChecked = utilities.vault.getBoolean(utilities.SETTINGS_LOCK_ENABLED, false)
+ lockButton.setOnCheckedChangeListener { _, isChecked ->
+ if (!lockscreen.isKeyguardSecure) {
+ lockButton.isChecked = false
+ Toast.makeText(this, "Enable screen lock in device settings first!", Toast.LENGTH_LONG).show()
+ utilities.vault.edit().remove(utilities.SETTINGS_LOCK_ENABLED).apply()
+ utilities.vault.edit().putBoolean(utilities.SETTINGS_LOCK_ENABLED, false).apply()
+ } else {
+ settingsChanged = true
+ utilities.vault.edit().remove(utilities.SETTINGS_LOCK_ENABLED).apply()
+ utilities.vault.edit().putBoolean(utilities.SETTINGS_LOCK_ENABLED, isChecked).apply()
+ }
+ }
+
+ val roundButton: SwitchMaterial = findViewById(R.id.roundButton)
+ roundButton.isChecked = utilities.vault.getBoolean(utilities.CONFIG_SCREEN_ROUND, resources.configuration.isScreenRound)
+ roundButton.setOnCheckedChangeListener { _, isChecked ->
+ settingsChanged = true
+ utilities.vault.edit().remove(utilities.CONFIG_SCREEN_ROUND).apply()
+ utilities.vault.edit().putBoolean(utilities.CONFIG_SCREEN_ROUND, isChecked).apply()
+ }
+
+ exportButton.setOnClickListener {
+ startActivity(Intent(applicationContext, ExportActivity::class.java))
+ exportButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ }
+
+ backButton.setOnClickListener {
+ finish()
+ }
+
+ deleteButton.setOnClickListener {
+ val intent = Intent(applicationContext, DeleteActivity::class.java)
+ intent.putExtra(utilities.INTENT_DELETE_MODE, utilities.INTENT_WIPE)
+ startActivity(intent)
+ deleteButton.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ }
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun startClock () {
+
+ if (!utilities.vault.getBoolean(utilities.SETTINGS_CLOCK_ENABLED, true)) {
+ findViewById(R.id.clockBackground).visibility = View.GONE
+ }
+
+ try {
+ mfaCodesTimer.scheduleAtFixedRate(object : TimerTask() {
+ override fun run() {
+ val currentHour24 = SimpleDateFormat("HH", Locale.getDefault()).format(Date())
+ val currentHour = SimpleDateFormat("hh", Locale.getDefault()).format(Date())
+ val currentMinute = SimpleDateFormat("mm", Locale.getDefault()).format(Date())
+ val currentSecond = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt()
+ val currentAmPm = SimpleDateFormat("a", Locale.getDefault()).format(Date())
+ runOnUiThread {
+ try {
+
+ if (utilities.vault.getBoolean(utilities.SETTINGS_24H_CLOCK_ENABLED, false)) {
+ clock.text = "$currentHour24:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour24 $currentMinute"
+ } else {
+ clock.text = "$currentHour:$currentMinute"
+ if ((currentSecond % 2) == 0) clock.text = "$currentHour $currentMinute"
+ }
+
+ } catch (_: Exception) { }
+ }
+ }
+ }, 0, 1000) // 1000 milliseconds = 1 second
+ } catch (_: IllegalStateException) { }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/Utilities.kt b/app/src/main/java/app/wristkey/Utilities.kt
new file mode 100644
index 0000000..9a25a00
--- /dev/null
+++ b/app/src/main/java/app/wristkey/Utilities.kt
@@ -0,0 +1,637 @@
+package app.wristkey
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.graphics.Bitmap
+import android.media.AudioManager
+import android.media.ToneGenerator
+import android.os.Build
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+import androidx.annotation.RequiresApi
+import androidx.security.crypto.EncryptedSharedPreferences
+import androidx.security.crypto.MasterKeys
+import com.chaquo.python.Python
+import com.chaquo.python.android.AndroidPlatform
+import com.google.zxing.*
+import com.google.zxing.common.HybridBinarizer
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.net.URLDecoder
+import java.text.SimpleDateFormat
+import java.util.*
+
+
+@RequiresApi(Build.VERSION_CODES.M)
+class Utilities (context: Context) {
+
+ val FILES_REQUEST_CODE = 69
+ val CAMERA_REQUEST_CODE = 420
+ val EXPORT_RESPONSE_CODE = 69420
+
+
+ val OTPAUTH_SCAN_CODE = "OTPAUTH_SCAN_CODE"
+ val AUTHENTICATOR_EXPORT_SCAN_CODE = "AUTHENTICATOR_EXPORT_SCAN_CODE"
+ val QR_CODE_SCAN_REQUEST = "QR_CODE_SCAN_REQUEST"
+
+ val context = context
+
+ val QR_TIMER_DURATION = 5
+
+ val DEFAULT_TYPE = "otpauth"
+ val INTENT_UUID = "INTENT_UUID"
+ val INTENT_WIPE = "INTENT_WIPE"
+ val INTENT_DELETE = "INTENT_DELETE"
+ val INTENT_DELETE_MODE = "INTENT_DELETE_MODE"
+
+ val SETTINGS_BACKGROUND_COLOR = "SETTINGS_BACKGROUND_COLOR"
+ val SETTINGS_ACCENT_COLOR = "SETTINGS_ACCENT_COLOR"
+
+ val SETTINGS_CLOCK_ENABLED = "SETTINGS_CLOCK_ENABLED"
+ val SETTINGS_24H_CLOCK_ENABLED = "SETTINGS_24H_CLOCK_ENABLED"
+ val SETTINGS_HAPTICS_ENABLED = "SETTINGS_HAPTICS_ENABLED"
+ val SETTINGS_BEEP_ENABLED = "SETTINGS_BEEP_ENABLED"
+ val CONFIG_SCREEN_ROUND = "CONFIG_SCREEN_ROUND"
+ val SETTINGS_LOCK_ENABLED = "SETTINGS_LOCK_ENABLED"
+
+ val MFA_TIME_MODE = "totp"
+ val MFA_COUNTER_MODE = "hotp"
+
+ val ALGO_SHA1 = "SHA1"
+ val ALGO_SHA256 = "SHA256"
+ val ALGO_SHA512 = "SHA512"
+
+ var masterKeyAlias: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
+ private val accountsFilename: String = "vault.wfs" // WristkeyFS
+ var vault: SharedPreferences
+
+ init {
+ vault = EncryptedSharedPreferences.create (
+ accountsFilename,
+ masterKeyAlias,
+ context,
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
+ )
+ }
+
+ data class MfaCode (
+ val type: String?,
+ val mode: String?,
+ val issuer: String?,
+ val account: String?,
+ val secret: String?,
+ val algorithm: String?,
+ val digits: Int?,
+ val period: Int?,
+ val lock: Boolean?,
+ val counter: Long?,
+ val label: String?,
+ )
+
+ fun scanQRImage(bMap: Bitmap): String {
+ var contents: String
+ val intArray = IntArray(bMap.width * bMap.height)
+ //copy pixel data from the Bitmap into the 'intArray' array
+ bMap.getPixels(intArray, 0, bMap.width, 0, 0, bMap.width, bMap.height)
+ val source: LuminanceSource = RGBLuminanceSource(bMap.width, bMap.height, intArray)
+ val bitmap = BinaryBitmap(HybridBinarizer(source))
+ val reader: Reader = MultiFormatReader()
+
+ contents = try {
+ val result = reader.decode(bitmap)
+ result.text
+ } catch (e: Exception) {
+ "No data found"
+ }
+
+ return contents
+ }
+
+ fun authenticatorToWristkey (decodedQRCodeData: String): MutableList {
+
+ fun decodeAuthenticatorData (authenticatorJsonString: String): MutableList {
+ val logins = mutableListOf()
+
+ val items = JSONObject(authenticatorJsonString)
+ for (key in items.keys()) {
+ val itemData = JSONObject(items[key].toString())
+
+ var mode: String = if (itemData["type"].toString() == "1") "hotp" else "totp"
+
+ var issuer: String = key
+
+ var account: String = if (itemData["username"].toString().isNotEmpty()
+ && itemData["username"].toString() != issuer)
+ itemData["username"].toString()
+ else ""
+
+ var secret: String = itemData["secret"].toString()
+
+ logins.add(
+ MfaCode (
+ type = "otpauth",
+ mode = mode,
+ issuer = issuer,
+ account = account,
+ secret = secret,
+ algorithm = ALGO_SHA1,
+ digits = 6,
+ period = 30,
+ lock = false,
+ counter = 0,
+ label = ""
+ )
+ )
+ }
+
+ return logins
+ }
+
+ fun scanAndDecodeQrCode(decodedQRCodeData: String): String {
+ if (!Python.isStarted()) {
+ Python.start(AndroidPlatform(context))
+ }
+
+ val pythonRuntime = Python
+ .getInstance()
+ .getModule("extract_otp_secret_keys")
+ .callAttr("decode", decodedQRCodeData)
+
+ val timeStamp: String = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())
+
+ val logcat: Process
+ val log = StringBuilder()
+ try {
+ logcat = Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
+ val br =
+ BufferedReader(InputStreamReader(logcat.inputStream), 4 * 1024)
+ var line: String?
+ val separator = System.getProperty("line.separator")
+ while (br.readLine().also { line = it } != null) {
+ log.append(line)
+ log.append(separator)
+ }
+ Runtime.getRuntime().exec("clear")
+ } catch (e: java.lang.Exception) {
+ e.printStackTrace()
+ }
+
+ // pythonRuntime.close()
+
+ return log.toString()
+ .substringAfter(timeStamp) // get most recent occurrence of data
+ .substringAfter("python.stdout")
+ .substringAfter("")
+ .substringBefore("<\\wristkey>")
+ }
+
+ // put data in Python script and extract Authenticator data
+ var logins: MutableList = mutableListOf()
+ if (decodedQRCodeData.contains("otpauth-migration://")) {
+ val logExtractedString = scanAndDecodeQrCode(decodedQRCodeData)
+ logins = decodeAuthenticatorData(logExtractedString)
+ }
+
+ return logins
+
+ }
+
+ fun wfsToHashmap(jsonObject: JSONObject): Map {
+ val map: MutableMap = HashMap()
+ for (key in jsonObject.keys()) {
+ map[key] = jsonObject[key] as String
+ }
+ return map
+ }
+
+ fun bitwardenToWristkey (jsonObject: JSONObject): MutableList {
+
+ val logins = mutableListOf()
+
+ val items = jsonObject["items"].toString()
+ val itemsArray = JSONArray(items)
+
+ val accounts = JSONArray(itemsArray.toString())
+
+ val accountList = mutableListOf()
+
+ for (index in 0 until accounts.length()) {
+ accountList.add(JSONObject(accounts[index].toString()))
+ }
+
+ for (account in accountList) {
+
+ try {
+ val issuer = account["name"].toString().trim()
+ val totp = JSONObject(account["login"].toString())["totp"].toString().trim()
+ val username = JSONObject(account["login"].toString())["username"].toString().trim()
+
+ if (!totp.contains("null") && !totp.isNullOrBlank()) {
+
+ if (totp.startsWith("otpauth://")) {
+ val type: String = if (totp.substringAfter("://").substringBefore("/").contains("totp")) "totp" else "hotp"
+ val issuer: String = totp.substringAfterLast("otp/").substringBefore(":")
+ val account: String = totp.substringAfterLast(":").substringBefore("?")
+ val secret: String? = if (totp.contains("secret")) totp.substringAfter("secret=").substringBefore("&") else null
+ val algorithm: String = if (totp.contains("algorithm")) totp.substringAfter("algorithm=").substringBefore("&") else ALGO_SHA1
+ val digits: Int = if (totp.contains("digits")) totp.substringAfter("digits=").substringBefore("&").toInt() else 6
+ val period: Int = if (totp.contains("period")) totp.substringAfter("period=").substringBefore("&").toInt() else 30
+ val lock: Boolean = if (totp.contains("lock")) totp.substringAfter("lock=").substringBefore("&").toBoolean() else false
+ val counter: Long = if (totp.contains("counter")) totp.substringAfter("counter=").substringBefore("&").toLong() else 0
+ val label: String = if (totp.contains("label")) totp.substringAfter("label=").substringBefore("&") else account
+ logins.add (
+ MfaCode(
+ type = "otpauth",
+ mode = type,
+ issuer = issuer,
+ account = account,
+ secret = secret,
+ algorithm = algorithm,
+ digits = digits,
+ period = period,
+ lock = false,
+ counter = counter,
+ label = label
+ )
+ )
+
+ } else if (!totp.startsWith("otpauth://")) { // Google Authenticator
+ logins.add (
+ MfaCode(
+ type = "otpauth",
+ mode = "totp",
+ issuer = issuer.ifBlank { "Unknown issuer" },
+ account = username.ifBlank { "" },
+ secret = totp,
+ algorithm = ALGO_SHA1,
+ digits = 6,
+ period = 30,
+ lock = false,
+ counter = 0,
+ label = ""
+ )
+ )
+ }
+ }
+
+ } catch (_: JSONException) { }
+
+ }
+
+ return logins
+
+ }
+
+ fun andOtpToWristkey (jsonArray: JSONArray): MutableList {
+
+ val logins = mutableListOf()
+
+ for (itemIndex in 0 until jsonArray.length()) {
+
+ val login = jsonArray[itemIndex].toString()
+ val secret = JSONObject(login)["secret"].toString().replace("=", "")
+ val issuer = JSONObject(login)["issuer"].toString()
+ val counter = try { JSONObject(login)["used_frequency"].toString().toLong() } catch (_: JSONException) { 0 }
+ val algorithm = JSONObject(login)["algorithm"].toString()
+ val digits = JSONObject(login)["digits"].toString().toInt()
+ val period = JSONObject(login)["period"].toString().toInt()
+ var type = JSONObject(login)["type"].toString().lowercase()
+ if (type == "STEAM") type = "totp"
+ val label = try { JSONObject(login)["label"].toString() } catch (_: JSONException) { "" }
+
+ logins.add (
+ Utilities.MfaCode(
+ type = "otpauth",
+ mode = type,
+ issuer = issuer,
+ account = label,
+ secret = secret,
+ algorithm = algorithm,
+ digits = digits,
+ period = period,
+ lock = false,
+ counter = counter,
+ label = label
+ )
+ )
+
+ }
+
+ return logins
+
+ }
+
+ fun aegisToWristkey (unencryptedAegisJsonString: String): MutableList {
+
+ val logins = mutableListOf()
+
+ val db = JSONObject(unencryptedAegisJsonString)["db"].toString()
+ val entries = JSONObject(db)["entries"].toString()
+
+ val itemsArray = JSONArray(entries)
+
+ for (itemIndex in 0 until itemsArray.length()) {
+ try {
+
+ val accountData = JSONObject(itemsArray[itemIndex].toString())
+ var type = accountData["type"]
+ val uuid = accountData["uuid"].toString()
+ val issuer = accountData["issuer"].toString()
+ var username = accountData["name"].toString()
+
+ if (username == issuer || username == "null" || username.isNullOrEmpty()) {
+ username = ""
+ }
+
+ var totpSecret = JSONObject(accountData["info"].toString())["secret"].toString()
+ val digits = JSONObject(accountData["info"].toString())["digits"].toString().toInt()
+ var algorithm = JSONObject(accountData["info"].toString())["algo"].toString()
+ var period = JSONObject(accountData["info"].toString())["period"].toString().toInt()
+ var counter = try { JSONObject(accountData["info"].toString())["counter"].toString().toLong() } catch (_: JSONException) { 0L }
+
+ type = if (type.equals("totp")) "totp" else "hotp"
+
+ if (totpSecret.isNotEmpty() && totpSecret != "null") {
+ logins.add (
+ MfaCode(
+ type = "otpauth",
+ mode = type,
+ issuer = issuer,
+ account = username,
+ secret = totpSecret,
+ algorithm = algorithm,
+ digits = digits,
+ period = period,
+ lock = false,
+ counter = counter,
+ label = ""
+ )
+ )
+ }
+
+ } catch (noData: JSONException) {
+ noData.printStackTrace()
+ }
+ }
+
+ return logins
+
+ }
+
+ fun decodeOTPAuthURL (OTPAuthURL: String): MfaCode? {
+ val url = URLDecoder.decode(OTPAuthURL, "UTF-8")
+ if (url.contains("otpauth://")) {
+ val type: String =
+ if (url.substringBefore("://").contains("migration")) "Google Authenticator Backup"
+ else "OTP"
+ val mode: String =
+ if (url.substringAfter("://").substringBefore("/").contains("totp"))
+ "totp"
+ else "hotp"
+ val issuer: String = url.substringAfterLast("otp/").substringBefore(":")
+ val account: String = url.substringAfterLast(":").substringBefore("?")
+ val secret: String? = if (url.contains("secret")) url.substringAfter("secret=").substringBefore("&") else null
+ val algorithm: String? = if (url.contains("algorithm")) url.substringAfter("algorithm=").substringBefore("&") else ALGO_SHA1
+ val digits: Int? = if (url.contains("digits")) url.substringAfter("digits=").substringBefore("&").toInt() else 6
+ val period: Int? = if (url.contains("period")) url.substringAfter("period=").substringBefore("&").toInt() else 30
+ val lock: Boolean? = if (url.contains("lock")) url.substringAfter("lock=").substringBefore("&").toBoolean() else false
+ val counter: Long? = if (url.contains("counter")) url.substringAfter("counter=").substringBefore("&").toLong() else 0
+ val label: String? = if (url.contains("label")) url.substringAfter("label=").substringBefore("&") else account
+
+ return MfaCode(
+ type = type,
+ mode = mode,
+ issuer = issuer,
+ account = account,
+ secret = secret,
+ algorithm = algorithm,
+ digits = digits,
+ period = period,
+ lock = lock,
+ counter = counter,
+ label = label,
+ )
+ } else {
+ return null
+ }
+ }
+
+ fun encodeOTPAuthURL (mfaCodeObject: MfaCode): String? {
+ lateinit var type: String
+ lateinit var issuer: String
+ lateinit var account: String
+ lateinit var secret: String
+
+ if (mfaCodeObject.type.toString().isNotEmpty())
+ type = if (mfaCodeObject.type.toString().lowercase().contains("backup") || mfaCodeObject.type.toString().lowercase().contains("migration")) "otpauth-migration" else "otpauth"
+ else return null
+
+ val mode: String = if (mfaCodeObject.mode.toString().isNotEmpty())
+ if (mfaCodeObject.mode.toString().lowercase().contains("time") || mfaCodeObject.mode.toString().contains("totp")) "totp" else "hotp"
+ else "totp"
+
+ account = mfaCodeObject.account.toString().ifEmpty { "" }
+
+ issuer = mfaCodeObject.issuer.toString().ifEmpty { account }
+
+ if (mfaCodeObject.secret.toString().isNotEmpty())
+ secret = mfaCodeObject.secret.toString().trim().trim().replace(" ", "")
+ else return null
+
+ val algorithm: String = mfaCodeObject.algorithm.toString().ifEmpty { "SHA1" }
+
+ val digits: String = mfaCodeObject.digits.toString().ifEmpty { "6" }
+
+ val period: String = mfaCodeObject.period.toString().ifEmpty { "30" }
+
+ val lock: String = mfaCodeObject.lock.toString().ifEmpty { "false" }
+
+ val counter: String = mfaCodeObject.counter.toString().ifEmpty { "0" }
+
+ val label: String = mfaCodeObject.label.toString().ifEmpty { "" }
+
+ return "$type://$mode/$issuer:$account?secret=$secret&algorithm=$algorithm&digits=$digits&period=$period&lock=$lock&counter=$counter&label=$label"
+ }
+
+ fun writeToVault (mfaCodeObject: MfaCode, uuid4: String): Boolean {
+ val mfaCodeObjectAsString = encodeOTPAuthURL (mfaCodeObject = mfaCodeObject)
+ val data = getLogins()
+
+ vault.edit().remove(uuid4).commit()
+ vault.edit().putString(uuid4, mfaCodeObjectAsString).commit()
+
+ return true
+ }
+
+ fun deleteFromVault (uuid4: String): Boolean {
+ val items = vault.all
+
+ for (item in items) {
+ if (item.key.contains(uuid4)) {
+ vault.edit().remove(item.key).apply()
+ }
+ }
+
+ return true
+ }
+
+ fun getUuid (login: MfaCode): String? {
+ val items = getVaultLoginsOnly()
+ for ((key, value) in items) if (value.contains(login.secret.toString())) return key
+ return null
+ }
+
+ fun getLogin (uuid: String): MfaCode? {
+ val items = vault.all
+ var value: MfaCode? = null
+
+ for (item in items) {
+ try {
+ value = decodeOTPAuthURL(item.value as String) as MfaCode
+ if (item.key == uuid) return value
+ } catch (_: Exception) { }
+ }
+
+ return value
+ }
+
+ fun overwriteLogin (oldLogin: MfaCode, newLogin: MfaCode): Boolean {
+
+ val items = vault.all
+ var key: String? = null
+
+ for (item in items) {
+ key = item.key
+ if (item.value == oldLogin) {
+ vault.edit().remove(key).apply()
+ break
+ }
+ }
+
+ vault.edit().putString(key, encodeOTPAuthURL(newLogin)).apply()
+
+ return true
+ }
+
+ fun getVault (): List {
+ var vault = vault.all.values.toList()
+ if (vault.isEmpty()) vault = mutableListOf()
+ return vault as MutableList
+ }
+
+ fun getVaultLoginsOnly (): Map {
+ val vault = vault.all
+ val finalVault = mutableMapOf()
+ for ((key, value) in vault) {
+ try {
+ UUID.fromString(key)
+ finalVault[key] = value.toString()
+ } catch (_: IllegalArgumentException) { }
+ }
+ return finalVault
+ }
+
+ fun getLogins (): List {
+ val items = vault.all
+ val logins = mutableListOf()
+ var key: String? = null
+
+ for (item in items) {
+ key = item.key
+ try {
+ val uuid = UUID.fromString(item.key as String)
+ logins.add(decodeOTPAuthURL(item.value as String)!!)
+ } catch (_: IllegalArgumentException) { }
+ }
+
+ return logins
+ }
+
+ fun beep () {
+ try {
+ val toneGen1 = ToneGenerator(AudioManager.STREAM_MUSIC, 100)
+ toneGen1.startTone(ToneGenerator.TONE_SUP_INTERCEPT, 150)
+ } catch (_: Exception) { }
+ }
+
+}
+
+// UI stuff
+open class OnSwipeTouchListener(c: Context?) : View.OnTouchListener {
+ private val gestureDetector: GestureDetector
+ override fun onTouch(view: View?, motionEvent: MotionEvent?): Boolean {
+ return gestureDetector.onTouchEvent(motionEvent!!)
+ }
+
+ private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
+
+ private val SWIPE_THRESHOLD = 100
+ private val SWIPE_VELOCITY_THRESHOLD = 100
+
+ override fun onDown(e: MotionEvent): Boolean {
+ return true
+ }
+
+ override fun onSingleTapUp(e: MotionEvent): Boolean {
+ onClick()
+ return super.onSingleTapUp(e)
+ }
+
+ override fun onDoubleTap(e: MotionEvent): Boolean {
+ onDoubleClick()
+ return super.onDoubleTap(e)
+ }
+
+ override fun onLongPress(e: MotionEvent) {
+ onLongClick()
+ super.onLongPress(e)
+ }
+
+ // Determines the fling velocity and then fires the appropriate swipe event accordingly
+ override fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
+ val result = false
+ try {
+ val diffY = e2.y - e1.y
+ val diffX = e2.x - e1.x
+ if (Math.abs(diffX) > Math.abs(diffY)) {
+ if (Math.abs(diffX) > SWIPE_THRESHOLD && Math.abs(velocityX) > SWIPE_VELOCITY_THRESHOLD) {
+ if (diffX > 0) {
+ onSwipeRight()
+ } else {
+ onSwipeLeft()
+ }
+ }
+ } else {
+ if (Math.abs(diffY) > SWIPE_THRESHOLD && Math.abs(velocityY) > SWIPE_VELOCITY_THRESHOLD) {
+ if (diffY > 0) {
+ onSwipeDown()
+ } else {
+ onSwipeUp()
+ }
+ }
+ }
+ } catch (exception: java.lang.Exception) {
+ exception.printStackTrace()
+ }
+ return result
+ }
+
+ }
+
+ open fun onSwipeRight() {}
+ open fun onSwipeLeft() {}
+ open fun onSwipeUp() {}
+ open fun onSwipeDown() {}
+ open fun onClick() {}
+ fun onDoubleClick() {}
+ open fun onLongClick() {}
+
+ init {
+ gestureDetector = GestureDetector(c, GestureListener())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/wristkey/WristkeyImport.kt b/app/src/main/java/app/wristkey/WristkeyImport.kt
new file mode 100644
index 0000000..61d3ee6
--- /dev/null
+++ b/app/src/main/java/app/wristkey/WristkeyImport.kt
@@ -0,0 +1,148 @@
+package app.wristkey
+
+import android.Manifest
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.media.audiofx.HapticGenerator
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.HapticFeedbackConstants
+import android.widget.ImageButton
+import android.widget.TextView
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import org.json.JSONObject
+import wristkey.R
+import java.io.File
+import java.io.FileReader
+
+
+class WristkeyImport : AppCompatActivity() {
+
+ lateinit var utilities: Utilities
+
+ lateinit var backButton: ImageButton
+ lateinit var doneButton: ImageButton
+ lateinit var importLabel: TextView
+ lateinit var description: TextView
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_wristkey_import)
+
+ utilities = Utilities (applicationContext)
+
+ initializeUI()
+
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeUI () {
+ backButton = findViewById (R.id.backButton)
+ doneButton = findViewById (R.id.doneButton)
+ importLabel = findViewById (R.id.label)
+ description = findViewById (R.id.description)
+
+ description.text = getString (R.string.wristkey_import_blurb) + " " + applicationContext.filesDir.toString() + "\n\n" + getString (R.string.use_adb_blurb)
+
+ backButton.setOnClickListener {
+ backButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ finish()
+ }
+
+ doneButton.setOnClickListener {
+ doneButton.performHapticFeedback(HapticGenerator.SUCCESS)
+ checkPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, utilities.FILES_REQUEST_CODE)
+ }
+
+ }
+
+ // Function to check and request permission.
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun checkPermission(permission: String, requestCode: Int) {
+ if (ContextCompat.checkSelfPermission(this@WristkeyImport, permission) == PackageManager.PERMISSION_DENIED) {
+ ActivityCompat.requestPermissions(this@WristkeyImport, arrayOf(permission), requestCode)
+ } else {
+ initializeScanUI()
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+ if (requestCode == utilities.FILES_REQUEST_CODE) {
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ initializeScanUI()
+ } else {
+ Toast.makeText(this@WristkeyImport, "Please grant Wristkey storage permissions in settings", Toast.LENGTH_LONG).show()
+ val intent = Intent (android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ intent.data = Uri.parse("package:$packageName")
+ startActivity(intent)
+ }
+ }
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun initializeScanUI () {
+ setContentView(R.layout.import_loading_screen)
+ val importingDescription = findViewById(R.id.ImportingDescription)
+
+ var logins = HashMap()
+
+ try {
+ val directory = File (applicationContext.filesDir.toString())
+ Log.d ("Wristkey", "Looking for files in: " + applicationContext.filesDir.toString())
+ importingDescription.text = "Looking for files in: \n${directory}"
+
+ for (file in directory.listFiles()!!) {
+ try {
+ val fileData = FileReader(file.path).readText()
+
+ if (file.name.endsWith(".wfs")) {
+ logins = utilities.wfsToHashmap (JSONObject(fileData)) as HashMap
+ }
+ } catch (_: Exception) {
+ Log.d ("Wristkey", "${file.name} is invalid")
+ }
+
+ importingDescription.text = "Found file: \n${file.name}"
+
+ importingDescription.performHapticFeedback(HapticFeedbackConstants.REJECT)
+ file.delete()
+
+ for (login in logins) {
+ importingDescription.text = "${utilities.decodeOTPAuthURL(login.value)?.issuer}"
+ utilities.writeToVault(utilities.decodeOTPAuthURL(login.value)!!, login.key)
+ }
+ }
+
+ if (logins.isEmpty()) {
+ Toast.makeText(this, "No files found.", Toast.LENGTH_LONG).show()
+ finish()
+ } else {
+ Toast.makeText(applicationContext, "Imported ${logins.size} accounts", Toast.LENGTH_SHORT).show()
+ finishAffinity()
+ startActivity(Intent(applicationContext, MainActivity::class.java))
+ }
+
+ } catch (noDirectory: NullPointerException) {
+ finish()
+ Toast.makeText(this, "Couldn't access storage. Please raise an issue on Wristkey's GitHub repo.", Toast.LENGTH_LONG).show()
+ noDirectory.printStackTrace()
+
+ } catch (invalidJson: NullPointerException) {
+ finish()
+ Toast.makeText(this, "The file you provided is invalid.", Toast.LENGTH_LONG).show()
+ invalidJson.printStackTrace()
+
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/AboutActivity.kt b/app/src/main/java/com/wristkey/AboutActivity.kt
deleted file mode 100644
index a36f50f..0000000
--- a/app/src/main/java/com/wristkey/AboutActivity.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.wristkey
-
-
-import android.annotation.SuppressLint
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.net.Uri
-import android.os.Bundle
-import android.os.Vibrator
-import android.support.wearable.activity.WearableActivity
-import android.widget.ImageView
-import android.widget.TextView
-import android.widget.Toast
-import androidx.wear.widget.BoxInsetLayout
-import com.google.android.wearable.intent.RemoteIntent
-import java.text.SimpleDateFormat
-import java.util.*
-
-
-class AboutActivity : WearableActivity() {
- @SuppressLint("SetTextI18n")
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_about)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val doneButton = findViewById(R.id.DoneButton)
- val appNameText = findViewById(R.id.AppName)
- val copyrightText = findViewById(R.id.Copyright)
- val versionText = findViewById(R.id.Version)
- val descriptionText = findViewById(R.id.AuthenticatorDescription)
- val currentTheme = appData.getString("theme", "000000")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#$currentTheme"))
- val currentAccent = appData.getString("accent", "4285F4")
- doneButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#$currentAccent"))
- val urlLink = findViewById(R.id.SourceCode)
- urlLink.setTextColor(ColorStateList.valueOf(Color.parseColor("#$currentAccent")))
- if (currentTheme == "F7F7F7") {
- appNameText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- copyrightText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- versionText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- descriptionText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- urlLink.compoundDrawableTintList = ColorStateList.valueOf(Color.parseColor("#000000"))
- } else {
- appNameText.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- copyrightText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- versionText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- descriptionText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- urlLink.compoundDrawableTintList = ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
- }
-
- var year = Calendar.getInstance().get(Calendar.YEAR).toString()
- if (year == "1970") year = "2021"
- copyrightText.text = "${copyrightText.text} $year"
- versionText.text = "v${BuildConfig.VERSION_NAME}"
- val uri: String = getString(R.string.about_url)
-
- urlLink.setOnClickListener {
- val intent = Intent(Intent.ACTION_VIEW).addCategory(Intent.CATEGORY_BROWSABLE).setData(Uri.parse(uri))
- RemoteIntent.startRemoteActivity(this, intent, null)
- val toast = Toast.makeText(this, "URL opened\non phone", Toast.LENGTH_SHORT)
- toast.show()
- try {
- val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(uri))
- startActivity(browserIntent)
- val toast2 = Toast.makeText(this, "URL opened\nin browser", Toast.LENGTH_SHORT)
- toast2.show()
- } catch (ex: Exception) { }
- }
-
- doneButton.setOnClickListener {
- finish()
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
- }
-}
diff --git a/app/src/main/java/com/wristkey/AddActivity.kt b/app/src/main/java/com/wristkey/AddActivity.kt
deleted file mode 100644
index d5e315f..0000000
--- a/app/src/main/java/com/wristkey/AddActivity.kt
+++ /dev/null
@@ -1,145 +0,0 @@
-package com.wristkey
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.os.Bundle
-import android.os.Vibrator
-import android.support.wearable.activity.WearableActivity
-import android.widget.*
-import androidx.wear.widget.BoxInsetLayout
-
-class AddActivity : WearableActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_add)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
-
- val importBitwardenButtonText = findViewById(R.id.BitwardenImportLabel)
- val importBitwardenButton = findViewById(R.id.BitwardenImportButton)
- val importBitwarden = findViewById(R.id.BitwardenImport)
-
- val manualEntry = findViewById(R.id.ManualEntry)
- val manualEntryButton = findViewById(R.id.ManualEntryButton)
- val manualEntryButtonText = findViewById(R.id.ManualEntryButtonLabel)
-
- val importAuthenticatorButtonText = findViewById(R.id.AuthenticatorImportLabel)
- val importAuthenticator = findViewById(R.id.AuthenticatorImport)
- val importAuthenticatorButton = findViewById(R.id.AuthenticatorImportButton)
-
- val aegisImportButton = findViewById(R.id.AegisImportButton)
- val aegisImportLabel = findViewById(R.id.AegisImportLabel)
- val aegisImport = findViewById(R.id.AegisImport)
-
- val scanQRCodeButton = findViewById(R.id.ScanQRCodeButton)
- val scanQRCodeLabel = findViewById(R.id.ScanQRCodeLabel)
- val scanQRCode = findViewById(R.id.ScanQRCode)
-
- val andOTPImportButton = findViewById(R.id.AndOTPImportButton)
- val andOTPImportImportLabel = findViewById(R.id.AndOTPImportImportLabel)
- val andOTPImport = findViewById(R.id.AndOTPImport)
-
- val wristkeyImportButton = findViewById(R.id.WristkeyImportButton)
- val wristkeyImportLabel = findViewById(R.id.WristkeyImportLabel)
- val wristkeyImport = findViewById(R.id.WristkeyImport)
-
- val backButton = findViewById(R.id.BackButton)
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
-
- boxinsetlayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- manualEntryButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- importBitwardenButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- importAuthenticatorButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- aegisImportButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- wristkeyImportButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- andOTPImportButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- backButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- scanQRCodeButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
-
- if (currentTheme == "F7F7F7") {
- manualEntryButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importBitwardenButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importAuthenticatorButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- aegisImportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- andOTPImportImportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- wristkeyImportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- scanQRCodeLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- manualEntryButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- importBitwardenButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- importAuthenticatorButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- aegisImportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- andOTPImportImportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- wristkeyImportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- scanQRCodeLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- manualEntry.setOnClickListener {
- val intent = Intent(applicationContext, ManualEntryActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- scanQRCode.setOnClickListener {
- val intent = Intent(applicationContext, OtpAuthImport::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- importBitwarden.setOnClickListener {
- val intent = Intent(applicationContext, BitwardenJSONImport::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- importAuthenticator.setOnClickListener {
- val intent = Intent(applicationContext, AuthenticatorQRImport::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- andOTPImport.setOnClickListener {
- val intent = Intent(applicationContext, AndOtpJSONImport::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- aegisImport.setOnClickListener {
- val intent = Intent(applicationContext, AegisJSONImport::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- wristkeyImport.setOnClickListener {
- val intent = Intent(applicationContext, WristkeyImport::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- backButton.setOnClickListener {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/AegisJSONImport.kt b/app/src/main/java/com/wristkey/AegisJSONImport.kt
deleted file mode 100644
index 4d09a19..0000000
--- a/app/src/main/java/com/wristkey/AegisJSONImport.kt
+++ /dev/null
@@ -1,186 +0,0 @@
-package com.wristkey
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment.getExternalStorageDirectory
-import android.os.Vibrator
-import android.provider.Settings
-import android.util.Log
-import android.widget.*
-import androidx.wear.widget.BoxInsetLayout
-import com.google.gson.Gson
-import com.wristkey.databinding.ActivityAegisJsonimportBinding
-import org.json.JSONArray
-import org.json.JSONException
-import org.json.JSONObject
-import java.io.File
-import java.io.FileReader
-import java.util.*
-
-class AegisJSONImport : Activity() {
-
- private lateinit var binding: ActivityAegisJsonimportBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityAegisJsonimportBinding.inflate(layoutInflater)
- setContentView(binding.root)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val backButton = findViewById(R.id.AuthenticatorBackButton)
- val confirmButton = findViewById(R.id.AuthenticatorConfirmButton)
- val importLabel = findViewById(R.id.AuthenticatorImportLabel)
- val description = findViewById(R.id.AuthenticatorDescription)
- val importUsernames = findViewById(R.id.AuthenticatorImportUsernames)
- var theme = "Dark"
- var accent = "Blue"
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- confirmButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- importUsernames.buttonTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- if (currentTheme == "F7F7F7") {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importUsernames.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- importUsernames.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- backButton.setOnClickListener {
- val intent = Intent(applicationContext, AddActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- confirmButton.setOnClickListener {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
-
- val files: Array = getExternalStorageDirectory().listFiles()
- // start import
- try {
- for (file in files) {
- if (file.name.startsWith("aegis") && file.name.endsWith(".json")) {
- val reader = FileReader(file.path)
- val jsonData = reader.readText()
- val db = JSONObject(jsonData)["db"].toString()
- val entries = JSONObject(db)["entries"].toString()
-
-
-
- var itemsArray = JSONArray(entries)
-
- setContentView(R.layout.import_loading_screen)
- val loadingLayout = findViewById(R.id.LoadingLayout)
- val loadingIcon = findViewById(R.id.LoadingIcon)
- val importingLabel = findViewById(R.id.ImportingLabel)
- val importingDescription = findViewById(R.id.ImportingDescription)
- loadingLayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- loadingIcon.progressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- loadingIcon.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentTheme))
- if (currentTheme == "F7F7F7") {
- importingLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importingDescription.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importingLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- importingDescription.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- importingDescription.text = "Found ${itemsArray.length()} items"
-
- for (itemIndex in 0 until itemsArray.length()) {
- try {
- val accountData = JSONObject(itemsArray[itemIndex].toString())
- var type = accountData["type"]
- val uuid = accountData["uuid"].toString()
- val sitename = accountData["issuer"]
- val username = accountData["name"]
- val totpSecret = JSONObject(accountData["info"].toString())["secret"]
- val digits = JSONObject(accountData["info"].toString())["digits"].toString()
- var algorithm = JSONObject(accountData["info"].toString())["algo"].toString()
-
- type = if (type.equals("totp")) "Time" else "Counter"
-
- if (algorithm == "SHA1") {
- algorithm = "HmacAlgorithm.SHA1"
- } else if (algorithm == "SHA256") {
- algorithm = "HmacAlgorithm.SHA256"
- } else if (algorithm == "SHA512") {
- algorithm = "HmacAlgorithm.SHA512"
- }
-
- val accountName: String = if (username.toString() == "null") {
- sitename.toString()
- } else {
- if (importUsernames.isChecked)
- "$sitename ($username)"
- else
- sitename.toString()
- }
-
- var totp = ""
- if (totpSecret.toString() != "null") {
- totp = totpSecret.toString()
- }
-
- if (totp.isNotEmpty()) {
- // begin storing data
- importingDescription.text = "Adding $sitename account"
- val accountData = ArrayList()
-
- val id = uuid
-
- accountData.add(accountName)
- accountData.add(totp)
-
- accountData.add(type)
- accountData.add(digits)
- accountData.add(algorithm)
- accountData.add("0") // If counter mode is selected, initial value must be 0.
- val json = Gson().toJson(accountData)
- accounts.edit().putString(id, json).apply()
- } else {
- importingDescription.text = "No TOTP secret for $sitename account"
- }
- } catch (noData: JSONException) { }
- }
- importingDescription.text = "Saving data"
- val toast = Toast.makeText(this, "Imported accounts successfully!", Toast.LENGTH_SHORT)
- toast.show()
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(100)
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
- }
-
- } catch (noFileFound: IllegalStateException) {
- val toast = Toast.makeText(this, "Couldn't find file. Check if the file exists and if Wristkey is granted storage permission.", Toast.LENGTH_LONG)
- toast.show()
-
- val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- val uri: Uri = Uri.fromParts("package", packageName, null)
- settingsIntent.data = uri
- startActivity(settingsIntent)
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
- // stop import
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/AndOtpJSONImport.kt b/app/src/main/java/com/wristkey/AndOtpJSONImport.kt
deleted file mode 100644
index f982e5a..0000000
--- a/app/src/main/java/com/wristkey/AndOtpJSONImport.kt
+++ /dev/null
@@ -1,165 +0,0 @@
-package com.wristkey
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment.getExternalStorageDirectory
-import android.os.Vibrator
-import android.provider.Settings
-import android.util.Log
-import android.widget.*
-import androidx.wear.widget.BoxInsetLayout
-import com.google.gson.Gson
-import com.wristkey.databinding.ActivityAndotpJsonimportBinding
-import org.json.JSONArray
-import org.json.JSONException
-import org.json.JSONObject
-import java.io.File
-import java.io.FileReader
-import java.util.*
-
-class AndOtpJSONImport : Activity() {
-
- private lateinit var binding: ActivityAndotpJsonimportBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityAndotpJsonimportBinding.inflate(layoutInflater)
- setContentView(binding.root)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val backButton = findViewById(R.id.AuthenticatorBackButton)
- val confirmButton = findViewById(R.id.AuthenticatorConfirmButton)
- val importLabel = findViewById(R.id.AuthenticatorImportLabel)
- val description = findViewById(R.id.AuthenticatorDescription)
- val importUsernames = findViewById(R.id.AuthenticatorImportUsernames)
- var theme = "Dark"
- var accent = "Blue"
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- confirmButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- importUsernames.buttonTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- if (currentTheme == "F7F7F7") {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importUsernames.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- importUsernames.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- backButton.setOnClickListener {
- val intent = Intent(applicationContext, AddActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- confirmButton.setOnClickListener {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
-
- val files: Array = getExternalStorageDirectory().listFiles()
- // start import
- try {
- for (file in files) {
- if (file.name.startsWith("otp_accounts") && file.name.endsWith(".json")) {
- val reader = FileReader(file.path)
- val jsonData = reader.readText()
- val itemsArray = JSONArray(jsonData)
-
- setContentView(R.layout.import_loading_screen)
- val loadingLayout = findViewById(R.id.LoadingLayout)
- val loadingIcon = findViewById(R.id.LoadingIcon)
- val importingLabel = findViewById(R.id.ImportingLabel)
- val importingDescription = findViewById(R.id.ImportingDescription)
- loadingLayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- loadingIcon.progressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- loadingIcon.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentTheme))
- if (currentTheme == "F7F7F7") {
- importingLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importingDescription.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importingLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- importingDescription.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- importingDescription.text = "Found ${itemsArray.length()} items"
-
- for (itemIndex in 0 until itemsArray.length()) {
- Log.d ("rtyu", itemsArray[itemIndex].toString())
-
- val account = itemsArray[itemIndex].toString()
- val secret = JSONObject(account)["secret"].toString().replace("=", "")
- val issuer = JSONObject(account)["issuer"].toString()
- val algorithm = "HmacAlgorithm."+JSONObject(account)["algorithm"].toString()
-
- val label = JSONObject(account)["label"].toString()
- val digits = JSONObject(account)["digits"].toString()
- var type = JSONObject(account)["type"].toString()
-
- val accountData = ArrayList()
-
- if (label.isNullOrEmpty()) {
- accountData.add(issuer)
- } else {
- if (importUsernames.isChecked) accountData.add ("$issuer ($label)")
- else accountData.add(issuer)
- }
-
- accountData.add(secret)
- if (type == "TOTP") accountData.add("Time")
- else if (type == "HOTP") accountData.add("Counter")
- else if (type == "STEAM") accountData.add("Time")
- accountData.add(digits)
- accountData.add(algorithm)
-
- try { // If counter mode is selected, initial value must be 0.
- accountData.add(JSONObject(account)["counter"].toString())
- } catch (noCounter: JSONException) {
- accountData.add("0")
- }
-
- val json = Gson().toJson(accountData)
-
- val id = UUID.randomUUID().toString()
- accounts.edit().putString(id, json).apply()
-
- }
- importingDescription.text = "Saving data"
- val toast = Toast.makeText(this, "Imported accounts successfully!", Toast.LENGTH_SHORT)
- toast.show()
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(100)
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
- }
-
- } catch (noFileFound: IllegalStateException) {
- val toast = Toast.makeText(this, "Couldn't find file. Check if the file exists and if Wristkey is granted storage permission.", Toast.LENGTH_LONG)
- toast.show()
-
- val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- val uri: Uri = Uri.fromParts("package", packageName, null)
- settingsIntent.data = uri
- startActivity(settingsIntent)
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
- // stop import
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/AuthenticatorQRImport.kt b/app/src/main/java/com/wristkey/AuthenticatorQRImport.kt
deleted file mode 100644
index 6c40691..0000000
--- a/app/src/main/java/com/wristkey/AuthenticatorQRImport.kt
+++ /dev/null
@@ -1,258 +0,0 @@
-package com.wristkey
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.graphics.Color
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment.getExternalStorageDirectory
-import android.os.Vibrator
-import android.provider.Settings
-import android.widget.*
-import androidx.wear.widget.BoxInsetLayout
-import com.chaquo.python.Python
-import com.chaquo.python.android.AndroidPlatform
-import com.google.gson.Gson
-import com.google.zxing.*
-import com.google.zxing.Reader
-import com.google.zxing.common.HybridBinarizer
-import com.wristkey.databinding.ActivityAuthenticatorQrimportBinding
-import org.json.JSONObject
-import java.io.*
-import java.text.SimpleDateFormat
-import java.util.*
-
-class AuthenticatorQRImport : Activity() {
-
- private lateinit var binding: ActivityAuthenticatorQrimportBinding
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityAuthenticatorQrimportBinding.inflate(layoutInflater)
- setContentView(binding.root)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val backButton = findViewById(R.id.AuthenticatorBackButton)
- val confirmButton = findViewById(R.id.AuthenticatorConfirmButton)
- val importLabel = findViewById(R.id.AuthenticatorImportLabel)
- val description = findViewById(R.id.AuthenticatorDescription)
- val importUsernames = findViewById(R.id.AuthenticatorImportUsernames)
- var theme = "Dark"
- var accent = "Blue"
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#" + currentTheme))
- confirmButton.backgroundTintList =
- ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- importUsernames.buttonTintList =
- ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- if (currentTheme == "F7F7F7") {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importUsernames.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- importUsernames.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- backButton.setOnClickListener {
- val intent = Intent(applicationContext, AddActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- confirmButton.setOnClickListener {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
-
- val files: Array = getExternalStorageDirectory().listFiles()
- // start import
- try {
- for (file in files) {
- if (file.name.endsWith(".png", ignoreCase = true) || file.name.endsWith(".jpg", ignoreCase = true) || file.name.endsWith(".jpeg", ignoreCase = true)) {
- val reader: InputStream = BufferedInputStream(FileInputStream(file.path))
- val imageBitmap = BitmapFactory.decodeStream(reader)
- val decodedQRCodeData: String = scanQRImage(imageBitmap)
-
- if (decodedQRCodeData.contains("otpauth-migration://")) {
- setContentView(R.layout.import_loading_screen)
- val loadingLayout = findViewById(R.id.LoadingLayout)
- val loadingIcon = findViewById(R.id.LoadingIcon)
- val importingLabel = findViewById(R.id.ImportingLabel)
- val importingDescription =
- findViewById(R.id.ImportingDescription)
- loadingLayout.setBackgroundColor(Color.parseColor("#" + currentTheme))
- loadingIcon.progressTintList =
- ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- loadingIcon.backgroundTintList =
- ColorStateList.valueOf(Color.parseColor("#" + currentTheme))
- if (currentTheme == "F7F7F7") {
- importingLabel.setTextColor(
- ColorStateList.valueOf(
- Color.parseColor(
- "#000000"
- )
- )
- )
- importingDescription.setTextColor(
- ColorStateList.valueOf(
- Color.parseColor(
- "#000000"
- )
- )
- )
- } else {
- importingLabel.setTextColor(
- ColorStateList.valueOf(
- Color.parseColor(
- "#BDBDBD"
- )
- )
- )
- importingDescription.setTextColor(
- ColorStateList.valueOf(
- Color.parseColor(
- "#FFFFFF"
- )
- )
- )
- }
-
- //found QR Code
-
- importingDescription.text = "Found QR Code"
-
- // put data in Python script and extract Authenticator data
- if (!Python.isStarted()) {
- Python.start(AndroidPlatform(this))
- }
-
- Python.getInstance().getModule("extract_otp_secret_keys").callAttr("decode", decodedQRCodeData)
- val timeStamp: String = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())
-
- var logcat: Process
- val log = StringBuilder()
- try {
- logcat = Runtime.getRuntime().exec(arrayOf("logcat", "-d"))
- val br =
- BufferedReader(InputStreamReader(logcat.inputStream), 4 * 1024)
- var line: String?
- val separator = System.getProperty("line.separator")
- while (br.readLine().also { line = it } != null) {
- log.append(line)
- log.append(separator)
- }
- Runtime.getRuntime().exec("clear")
- } catch (e: java.lang.Exception) {
- e.printStackTrace()
- }
-
- val logExtractedString = log.toString()
- .substringAfter(timeStamp) // get most recent occurence of data
- .substringAfter("python.stdout")
- .substringAfter("<\$beginwristkeygoogleauthenticatorimport\$>")
- .substringBefore("<\$endwristkeygoogleauthenticatorimport\$>")
-
- // convert json data and store in sharedprefs
- val items = JSONObject(logExtractedString)
- for (key in items.keys()) {
- val accountData = ArrayList()
- if (!importUsernames.isChecked) {
- accountData.add(key.toString().replaceAfter("(", "").replace("(", "")) // name without username
- } else {
- accountData.add(key) // name with username
- }
- val itemData = JSONObject(items[key].toString())
- accountData.add(itemData["secret"].toString()) //secret
- if (itemData["type"] == "2") accountData.add("Time") else accountData.add("Counter") // mode
- accountData.add("6") // length
- accountData.add("HmacAlgorithm.SHA1") // algorithm
- accountData.add("0") // If counter mode is selected, initial value must be 0.
-
- val id = UUID.randomUUID().toString()
- val json = Gson().toJson(accountData)
- accounts.edit().putString(id, json).apply()
- }
-
- importingDescription.text = "Saving data"
- Toast.makeText(
- this,
- "Imported accounts successfully!",
- Toast.LENGTH_SHORT
- ).show()
-
- val vibratorService =
- getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
-
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- } else {
- Toast.makeText(
- this,
- "Couldn't find Google Authenticator data in image!",
- Toast.LENGTH_SHORT
- ).show()
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
-
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
- }
- }
-
- } catch (noFileFound: FileNotFoundException) {
- Toast.makeText(
- this,
- "Couldn't find file. Check if the file exists and if Wristkey is granted storage permission.",
- Toast.LENGTH_LONG
- ).show()
-
- val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- val uri: Uri = Uri.fromParts("package", packageName, null)
- settingsIntent.data = uri
- startActivity(settingsIntent)
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
-
- } catch (badData: IllegalArgumentException) {
- Toast.makeText(this, "QR code data corrupt.", Toast.LENGTH_SHORT).show()
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
-
- }
- // stop import
- }
- }
-
- fun scanQRImage(bMap: Bitmap): String {
- var contents: String
- val intArray = IntArray(bMap.width * bMap.height)
- //copy pixel data from the Bitmap into the 'intArray' array
- bMap.getPixels(intArray, 0, bMap.width, 0, 0, bMap.width, bMap.height)
- val source: LuminanceSource = RGBLuminanceSource(bMap.width, bMap.height, intArray)
- val bitmap = BinaryBitmap(HybridBinarizer(source))
- val reader: Reader = MultiFormatReader()
- try {
- val result = reader.decode(bitmap)
- contents = result.text
- } catch (e: Exception) {
- contents = "No data found"
- }
- return contents
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/BitwardenJSONImport.kt b/app/src/main/java/com/wristkey/BitwardenJSONImport.kt
deleted file mode 100644
index 7dcb872..0000000
--- a/app/src/main/java/com/wristkey/BitwardenJSONImport.kt
+++ /dev/null
@@ -1,202 +0,0 @@
-package com.wristkey
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment.getExternalStorageDirectory
-import android.os.Vibrator
-import android.provider.Settings
-import android.widget.*
-import androidx.wear.widget.BoxInsetLayout
-import com.google.gson.Gson
-import com.wristkey.databinding.ActivityBitwardenJsonimportBinding
-import org.json.JSONArray
-import org.json.JSONException
-import org.json.JSONObject
-import java.io.File
-import java.io.FileReader
-import java.util.*
-
-class BitwardenJSONImport : Activity() {
-
- private lateinit var binding: ActivityBitwardenJsonimportBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityBitwardenJsonimportBinding.inflate(layoutInflater)
- setContentView(binding.root)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val backButton = findViewById(R.id.AuthenticatorBackButton)
- val confirmButton = findViewById(R.id.AuthenticatorConfirmButton)
- val importLabel = findViewById(R.id.AuthenticatorImportLabel)
- val description = findViewById(R.id.AuthenticatorDescription)
- val importUsernames = findViewById(R.id.AuthenticatorImportUsernames)
- var theme = "Dark"
- var accent = "Blue"
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- confirmButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- importUsernames.buttonTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- if (currentTheme == "F7F7F7") {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importUsernames.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- importUsernames.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- backButton.setOnClickListener {
- val intent = Intent(applicationContext, AddActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- confirmButton.setOnClickListener {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
-
- val files: Array = getExternalStorageDirectory().listFiles()
- // start import
- try {
- for (file in files) {
- if (file.name.startsWith("bitwarden") && file.name.endsWith(".json")) {
- val reader = FileReader(file.path)
- val jsonData = reader.readText()
- val items = JSONObject(jsonData)["items"].toString()
- val itemsArray = JSONArray(items)
-
- setContentView(R.layout.import_loading_screen)
- val loadingLayout = findViewById(R.id.LoadingLayout)
- val loadingIcon = findViewById(R.id.LoadingIcon)
- val importingLabel = findViewById(R.id.ImportingLabel)
- val importingDescription = findViewById(R.id.ImportingDescription)
- loadingLayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- loadingIcon.progressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- loadingIcon.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentTheme))
- if (currentTheme == "F7F7F7") {
- importingLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importingDescription.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importingLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- importingDescription.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- //found x number of items
-
- importingDescription.text = "Found ${itemsArray.length()} items"
-
- for (itemIndex in 0 until itemsArray.length()) {
- val itemData = JSONObject(itemsArray[itemIndex].toString())
-
- try {
- val accountData = JSONObject(itemData["login"].toString())
- val totpSecret = accountData["totp"]
- val username = accountData["username"]
- val sitename = itemData["name"]
- val uuid = itemData["id"].toString()
-
- val accountName: String = if (username.toString() == "null") {
- sitename.toString()
- } else {
- if (importUsernames.isChecked)
- "$sitename ($username)"
- else
- sitename.toString()
- }
-
- var totp = ""
- if (totpSecret.toString() != "null") {
- totp = totpSecret.toString()
- }
-
- if (totp.isNotEmpty()) { // begin storing data
- importingDescription.text = "Adding $sitename account"
- if (totp.startsWith("otpauth://")) {
- var type = totp.substringAfter("otpauth://").substringBefore("/")
- var secret = totp.substringAfter("secret=").substringBefore("&")
- var algorithm = totp.substringAfter("algorithm=").substringBefore("&")
- var digits = totp.substringAfter("digits=").substringBefore("&")
- var label = totp.substringAfterLast("/").substringBefore("?")
- var issuer = totp.substringAfterLast("issuer=").substringBefore("&")
-
- type = if (type.equals("totp")) "Time" else "Counter"
-
- if (algorithm == "SHA1") {
- algorithm = "HmacAlgorithm.SHA1"
- } else if (algorithm == "SHA256") {
- algorithm = "HmacAlgorithm.SHA256"
- } else if (algorithm == "SHA512") {
- algorithm = "HmacAlgorithm.SHA512"
- }
-
- val accountData = ArrayList()
-
- accountData.add(accountName)
- accountData.add(secret)
-
- accountData.add(type)
- accountData.add(digits)
- accountData.add(algorithm)
- accountData.add("0") // If counter mode is selected, initial value must be 0.
- val json = Gson().toJson(accountData)
- accounts.edit().putString(uuid, json).apply()
-
- } else { // Google Authenticator
-
- val accountData = ArrayList()
-
- accountData.add(accountName)
- accountData.add(totp)
-
- accountData.add("Time")
- accountData.add("6")
- accountData.add("HmacAlgorithm.SHA1")
- accountData.add("0") // If counter mode is selected, initial value must be 0.
- val json = Gson().toJson(accountData)
- accounts.edit().putString(uuid, json).apply()
- }
- } else {
- importingDescription.text = "No TOTP secret for $sitename account"
- }
- } catch (noData: JSONException) { }
- }
- importingDescription.text = "Saving data"
- val toast = Toast.makeText(this, "Imported accounts successfully!", Toast.LENGTH_SHORT)
- toast.show()
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(100)
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
- }
-
- } catch (noFileFound: IllegalStateException) {
- val toast = Toast.makeText(this, "Couldn't find file. Check if the file exists and if Wristkey is granted storage permission.", Toast.LENGTH_LONG)
- toast.show()
-
- val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- val uri: Uri = Uri.fromParts("package", packageName, null)
- settingsIntent.data = uri
- startActivity(settingsIntent)
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
- // stop import
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/DeleteActivity.kt b/app/src/main/java/com/wristkey/DeleteActivity.kt
deleted file mode 100644
index 85513af..0000000
--- a/app/src/main/java/com/wristkey/DeleteActivity.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-package com.wristkey
-
-import android.app.KeyguardManager
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.os.Build
-import android.os.Bundle
-import android.os.Vibrator
-import android.support.wearable.activity.WearableActivity
-import android.widget.ImageButton
-import android.widget.TextView
-import android.widget.Toast
-import androidx.annotation.RequiresApi
-import androidx.wear.widget.BoxInsetLayout
-
-class DeleteActivity : WearableActivity() {
-
-
-
- @RequiresApi(Build.VERSION_CODES.N)
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_delete)
- val confirmationText = findViewById(R.id.ConfirmationText)
- val confirmButton = findViewById(R.id.AuthenticatorConfirmButton)
- val cancelButton = findViewById(R.id.CancelButton)
- val boxInsetLayout = findViewById(R.id.BoxInsetLayout)
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
- boxInsetLayout.setBackgroundColor(Color.parseColor("#" + currentTheme))
- confirmButton.backgroundTintList =
- ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- if (currentTheme == "F7F7F7") {
- confirmationText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- confirmationText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- if (appData.getBoolean("screen_lock", true)) {
- val lockscreen = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
- if (lockscreen.isKeyguardSecure) {
- val i = lockscreen.createConfirmDeviceCredentialIntent("Wristkey", "App locked")
- startActivityForResult(i, CODE_AUTHENTICATION_VERIFICATION)
- }
- }
-
- cancelButton.setOnClickListener {
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- val isDeleteAll = intent.getBooleanExtra("delete_all", false)
- val accountIDToDelete = intent.getStringExtra("account_id")
-
- if (isDeleteAll) {
- confirmationText.text = "Delete all accounts and app settings?"
- confirmButton.setOnClickListener {
- appData.edit().clear().apply()
- accounts.edit().clear().apply()
- finish()
- Toast.makeText(this, "All items deleted!", Toast.LENGTH_SHORT).show()
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(500)
- }
- } else {
- confirmButton.setOnClickListener {
- accounts.edit().remove(accountIDToDelete).apply()
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- Toast.makeText(this, "Account deleted", Toast.LENGTH_SHORT).show()
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(500)
- }
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (!(resultCode == RESULT_OK && requestCode == CODE_AUTHENTICATION_VERIFICATION)) {
- finish()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/ExportActivity.kt b/app/src/main/java/com/wristkey/ExportActivity.kt
deleted file mode 100644
index 7337ce2..0000000
--- a/app/src/main/java/com/wristkey/ExportActivity.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-package com.wristkey
-import android.app.KeyguardManager
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment
-import android.os.Vibrator
-import android.provider.Settings
-import android.support.wearable.activity.WearableActivity
-import android.widget.ImageView
-import android.widget.LinearLayout
-import android.widget.TextView
-import android.widget.Toast
-import androidx.security.crypto.EncryptedSharedPreferences
-import androidx.security.crypto.MasterKeys
-import androidx.wear.widget.BoxInsetLayout
-import java.io.File
-import java.io.FileWriter
-import java.io.IOException
-import java.text.SimpleDateFormat
-import java.util.*
-
-
-class ExportActivity : WearableActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_export)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val jsonFileExportLabel = findViewById(R.id.BitwardenImportLabel)
- val jsonFileExportButton = findViewById(R.id.BitwardenImportButton)
- val fileExport = findViewById(R.id.BitwardenImport)
- val jsonQrCodeExportLabel = findViewById(R.id.AuthenticatorImportLabel)
- val qrCodeExport = findViewById(R.id.AuthenticatorImport)
- val jsonQrCodeExportButton = findViewById(R.id.AuthenticatorImportButton)
- val backButton = findViewById(R.id.BackButton)
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
-
- boxinsetlayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- jsonFileExportButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- jsonQrCodeExportButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- backButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
-
- if (currentTheme == "F7F7F7") {
- jsonFileExportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- jsonQrCodeExportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- jsonFileExportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- jsonQrCodeExportLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- if (appData.getBoolean("screen_lock", true)) {
- val lockscreen = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
- if (lockscreen.isKeyguardSecure) {
- val i = lockscreen.createConfirmDeviceCredentialIntent("Wristkey", "App locked")
- startActivityForResult(i, CODE_AUTHENTICATION_VERIFICATION)
- }
- }
-
- // Begin unpacking data
-
- val exportData: String = accounts.all.values.toString()
-
- fileExport.setOnClickListener {
-
- val sdf = SimpleDateFormat("yyyy-MM-dd'@'HH:mm:ss")
- val currentDateandTime: String = sdf.format(Date())
- val fileName = "$currentDateandTime.backup"
-
- try {
- val root = File(Environment.getExternalStorageDirectory(), "wristkey")
- if (!root.exists()) {
- root.mkdirs()
- }
- val file = File(root, fileName)
- val writer = FileWriter(file)
- writer.append(exportData)
- writer.flush()
- writer.close()
- Toast.makeText(this, "Exported successfully. Make sure to delete after use.", Toast.LENGTH_SHORT).show()
- } catch (e: IOException) {
- e.printStackTrace()
- val toast = Toast.makeText(this, "Couldn't write to file. Disable and re-enable storage permission.", Toast.LENGTH_LONG)
- toast.show()
-
- val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- val uri: Uri = Uri.fromParts("package", packageName, null)
- settingsIntent.data = uri
- startActivity(settingsIntent)
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- qrCodeExport.setOnClickListener {
- val intent = Intent(applicationContext, QRCodeActivity::class.java)
- intent.putExtra("qr_data", exportData)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- backButton.setOnClickListener {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
-
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (!(resultCode == RESULT_OK && requestCode == CODE_AUTHENTICATION_VERIFICATION)) {
- finish()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/MainActivity.kt b/app/src/main/java/com/wristkey/MainActivity.kt
deleted file mode 100644
index 6d6e252..0000000
--- a/app/src/main/java/com/wristkey/MainActivity.kt
+++ /dev/null
@@ -1,359 +0,0 @@
-package com.wristkey
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.app.KeyguardManager
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.media.AudioManager
-import android.media.ToneGenerator
-import android.os.Build
-import android.os.Bundle
-import android.os.Vibrator
-import android.support.wearable.activity.WearableActivity
-import android.util.Log
-import android.view.View
-import android.widget.*
-import androidx.core.app.ActivityCompat
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import androidx.security.crypto.EncryptedSharedPreferences
-import androidx.security.crypto.MasterKeys
-import androidx.wear.widget.BoxInsetLayout
-import dev.turingcomplete.kotlinonetimepassword.*
-import org.json.JSONArray
-import java.text.SimpleDateFormat
-import java.util.*
-import java.util.concurrent.TimeUnit
-import kotlin.collections.ArrayList
-
-lateinit var masterKeyAlias: String
-public const val accountsFile: String = "accounts"
-public const val appDataFile: String = "app_data"
-public lateinit var accounts: SharedPreferences
-public lateinit var appData: SharedPreferences
-public const val CODE_AUTHENTICATION_VERIFICATION = 241
-
-class MainActivity : WearableActivity() {
- var appExited: Boolean = false
- @SuppressLint("WrongConstant")
- override fun onCreate(savedInstanceState: Bundle?) {
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- val code = 0x3
- try {
- ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), code)
- } catch (e: Exception) {
- e.printStackTrace()
- throw e
- }
- }
-
- masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
-
- appData = EncryptedSharedPreferences.create(
- appDataFile,
- masterKeyAlias,
- applicationContext,
- EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
- EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
- )
-
- if (appData.getBoolean("screen_lock", true)) {
- val lockscreen = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
- if (lockscreen.isKeyguardSecure) {
- val i = lockscreen.createConfirmDeviceCredentialIntent("Wristkey", "App locked")
- startActivityForResult(i, CODE_AUTHENTICATION_VERIFICATION)
- }
- }
-
- accounts = EncryptedSharedPreferences.create(
- accountsFile,
- masterKeyAlias,
- applicationContext,
- EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
- EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
- )
-
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_main)
-
- if (appData.getBoolean("ambient_mode", false)) {
- setAmbientEnabled() // disabled by default because app contains sensitive information
- }
-
- var timeLeft: ProgressBar
-
- if (applicationContext.resources.configuration.isScreenRound) {
- timeLeft = findViewById(R.id.RoundTimeLeft)
- timeLeft.visibility = View.GONE
- } else {
- timeLeft = findViewById(R.id.SquareTimeLeftTop)
- timeLeft.visibility = View.GONE
- }
-
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val addAccountButton = findViewById(R.id.AddAccountButton)
- val settingsButton = findViewById(R.id.SettingsButton)
- val aboutButton = findViewById(R.id.AboutButton)
-
- val currentTheme = appData.getString("theme", "000000")
- val currentAccent = appData.getString("accent", "4285F4")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#" + currentTheme))
- timeLeft.progressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- timeLeft.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentTheme))
- if (currentTheme == "F7F7F7") {
- addAccountButton.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- addAccountButton.compoundDrawableTintList = ColorStateList.valueOf(Color.parseColor("#000000"))
- aboutButton.imageTintList = ColorStateList.valueOf(Color.parseColor("#000000"))
- settingsButton.imageTintList = ColorStateList.valueOf(Color.parseColor("#000000"))
- } else {
- addAccountButton.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- addAccountButton.compoundDrawableTintList = ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
- aboutButton.imageTintList = ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
- settingsButton.imageTintList = ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
- }
-
- fun getData(){
- val timeRecyclerView = findViewById(R.id.TimeaccountList)
- timeRecyclerView.layoutManager = LinearLayoutManager(applicationContext, LinearLayout.VERTICAL, false) // adding a LayoutManager
- val counterRecyclerView = findViewById(R.id.CounteraccountList)
- counterRecyclerView.layoutManager = LinearLayoutManager(applicationContext, LinearLayout.VERTICAL, false) // adding a LayoutManager
- val timeBasedAccounts = ArrayList() // creating an arrayList to store users using the data class
- val counterBasedAccounts = ArrayList() // creating an arrayList to store users using the data class
-
- val keys: Map = accounts.all
-
- for ((key, _) in keys) {
- val accountData = accounts.getString(key, null)
- val accountList = ArrayList()
-
- try {
- val jArray = JSONArray(accountData)
- for (i in 0 until jArray.length()) {
- accountList.add(jArray.getString(i))
- }
-
- val accountName = accountList[0]
- val secret = accountList[1]
- val mode = accountList[2]
- val digits = accountList[3]
- val algorithm = accountList[4]
- val counter = accountList[5]
- if (mode == "Time") {
- if(algorithm=="HmacAlgorithm.SHA1" && digits == "6"){
- // Google Authenticator
- val googleAuthenticator = GoogleAuthenticator(base32secret = secret) // "OurSharedSecret" Base32-encoded
- val totp = googleAuthenticator.generate()
- timeBasedAccounts.add(Account(key, accountName, totp, counter))
- }else if(algorithm=="HmacAlgorithm.SHA1" && digits != "6"){
- val config = TimeBasedOneTimePasswordConfig(
- codeDigits = digits.toInt(),
- hmacAlgorithm = HmacAlgorithm.SHA1,
- timeStep = 30,
- timeStepUnit = TimeUnit.SECONDS
- )
- val totp = TimeBasedOneTimePasswordGenerator(secret.toByteArray(), config)
- timeBasedAccounts.add(
- Account(
- key,
- accountName,
- totp.generate(),
- counter
- )
- )
- }else if(algorithm=="HmacAlgorithm.SHA256"){
- val config = TimeBasedOneTimePasswordConfig(
- codeDigits = digits.toInt(),
- hmacAlgorithm = HmacAlgorithm.SHA256,
- timeStep = 30,
- timeStepUnit = TimeUnit.SECONDS
- )
- val totp = TimeBasedOneTimePasswordGenerator(secret.toByteArray(), config)
- timeBasedAccounts.add(
- Account(
- key,
- accountName,
- totp.generate().toString(),
- counter
- )
- )
- }else if(algorithm=="HmacAlgorithm.SHA512"){
- val config = TimeBasedOneTimePasswordConfig(
- codeDigits = digits.toInt(),
- hmacAlgorithm = HmacAlgorithm.SHA512,
- timeStep = 30,
- timeStepUnit = TimeUnit.SECONDS
- )
- val totp = TimeBasedOneTimePasswordGenerator(secret.toByteArray(), config)
- timeBasedAccounts.add(
- Account(
- key,
- accountName,
- totp.generate(),
- counter
- )
- )
- }
- } else if (mode == "Counter") {
- if (algorithm=="HmacAlgorithm.SHA1"){
- val config = HmacOneTimePasswordConfig(
- codeDigits = digits.toInt(),
- hmacAlgorithm = HmacAlgorithm.SHA1
- )
- val cotp = HmacOneTimePasswordGenerator(secret.toByteArray(), config)
- counterBasedAccounts.add(
- Account(
- key,
- accountName,
- cotp.generate(counter = counter.toLong()),
- counter
- )
- )
- }else if(algorithm=="HmacAlgorithm.SHA256"){
- val config = HmacOneTimePasswordConfig(
- codeDigits = digits.toInt(),
- hmacAlgorithm = HmacAlgorithm.SHA256
- )
- val cotp = HmacOneTimePasswordGenerator(secret.toByteArray(), config)
- counterBasedAccounts.add(
- Account(
- key,
- accountName,
- cotp.generate(counter = counter.toLong()),
- counter
- )
- )
- }else if(algorithm=="HmacAlgorithm.SHA512"){
- val config = HmacOneTimePasswordConfig(
- codeDigits = digits.toInt(),
- hmacAlgorithm = HmacAlgorithm.SHA512
- )
- val cotp = HmacOneTimePasswordGenerator(secret.toByteArray(), config)
- counterBasedAccounts.add(
- Account(
- key,
- accountName,
- cotp.generate(counter = counter.toLong()),
- counter
- )
- )
- }
- }
- } catch (nullPointer: NullPointerException) {
-
- } catch (noData: ArrayIndexOutOfBoundsException) {
-
- }
-
- }
-
- val timeAdapter = TimeCardAdapter(applicationContext, timeBasedAccounts){} //creating adapter
- timeRecyclerView.adapter = timeAdapter //now adding adapter to recyclerview
-
- val counterAdapter = CounterCardAdapter(applicationContext, counterBasedAccounts){} //creating adapter
- counterRecyclerView.adapter = counterAdapter //now adding adapter to recyclerview
- }
-
- getData()
-
- object : Thread() {
- override fun run() {
- try {
- while (!appExited) {
- sleep(1000)
- runOnUiThread {
- getData()
- }
- }
- } catch (e: InterruptedException) { }
- }
- }.start()
-
- fun getTimerUI() {
- val toneG = ToneGenerator(AudioManager.STREAM_ALARM, 100)
- val tone = ToneGenerator.TONE_CDMA_CALL_SIGNAL_ISDN_PING_RING
- val toneLength = 50
- var currentSecondsValue = SimpleDateFormat("s", Locale.getDefault()).format(Date()).toInt()
- if (currentSecondsValue in 30..59) {
- if (currentSecondsValue == 59) {
- if (appData.getBoolean("beep", false)) toneG.startTone(tone, toneLength)
-
- if (appData.getBoolean("vibrate", false)) {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(25)
- }
- }
- timeLeft.progress = 59-currentSecondsValue
- } else {
- if (currentSecondsValue == 29) {
- if (appData.getBoolean("beep", false)) toneG.startTone(tone, toneLength)
-
- if (appData.getBoolean("vibrate", false)) {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(25)
- }
- }
- timeLeft.progress = 29-currentSecondsValue
- }
- }
-
- getTimerUI()
-
- if (accounts.all.isNotEmpty()) {
- timeLeft.visibility = View.VISIBLE
- object : Thread() {
- override fun run() {
- try {
- while (!appExited) {
- sleep(1000)
- runOnUiThread {
- getTimerUI()
- }
- }
- } catch (e: InterruptedException) { }
- }
- }.start()
- }
-
- addAccountButton.setOnClickListener {
- val intent = Intent(applicationContext, AddActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(100)
- finish()
- }
-
- aboutButton.setOnClickListener {
- val intent = Intent(applicationContext, AboutActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- }
-
- settingsButton.setOnClickListener {
- val intent = Intent(applicationContext, SettingsActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
- }
-
- override fun onStop() {
- super.onStop()
- appExited=true
- finish()
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (!(resultCode == RESULT_OK && requestCode == CODE_AUTHENTICATION_VERIFICATION)) {
- finish()
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/ManualEntryActivity.kt b/app/src/main/java/com/wristkey/ManualEntryActivity.kt
deleted file mode 100644
index 6a35c54..0000000
--- a/app/src/main/java/com/wristkey/ManualEntryActivity.kt
+++ /dev/null
@@ -1,287 +0,0 @@
-package com.wristkey
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.os.Build
-import android.os.Bundle
-import android.support.wearable.activity.WearableActivity
-import android.text.method.PasswordTransformationMethod
-import android.util.Log
-import android.view.View
-import android.widget.*
-import androidx.annotation.RequiresApi
-import androidx.wear.widget.BoxInsetLayout
-import com.google.gson.Gson
-import org.json.JSONArray
-import java.util.*
-
-
-class ManualEntryActivity : WearableActivity() {
- @RequiresApi(Build.VERSION_CODES.N)
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_manual_entry)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val addAccountLabel = findViewById(R.id.ManualEntryLabel)
- val confirmButton = findViewById(R.id.AuthenticatorConfirmButton)
- val deleteButton = findViewById(R.id.DeleteButton)
- val cancelButton = findViewById(R.id.CancelButton)
- val other = findViewById(R.id.Other)
- val account = findViewById(R.id.AccountField)
- val sharedSecret = findViewById(R.id.SharedSecretField)
- sharedSecret.transformationMethod = PasswordTransformationMethod.getInstance()
- val modeGroup = findViewById(R.id.GeneratorMode)
- val timeMode = findViewById(R.id.TimeMode)
- val counterMode = findViewById(R.id.CounterMode)
- modeGroup.check(R.id.TimeMode)
- var mode = "Time"
- val digitLength = findViewById(R.id.DigitLengthSeekbar)
- digitLength.progress = 1
- var selectedDigitLength = "6"
- val digitLengthLabel = findViewById(R.id.DigitLength)
- val algorithm = findViewById(R.id.AlgorithmKeylengthSeekbar)
- algorithm.progress = 0
- var selectedAlgorithm = "HmacAlgorithm.SHA1"
- val algorithmLabel = findViewById(R.id.AlgorithmLength)
-
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#" + currentTheme))
- account.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- account.foregroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- account.compoundDrawableTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- sharedSecret.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- sharedSecret.foregroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- sharedSecret.compoundDrawableTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- timeMode.buttonTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- counterMode.buttonTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- digitLength.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- digitLength.foregroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- digitLength.progressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- digitLength.progressBackgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- digitLength.secondaryProgressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- digitLength.indeterminateTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- digitLength.thumbTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- digitLength.tickMarkTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- algorithm.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- algorithm.foregroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- algorithm.progressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- algorithm.progressBackgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- algorithm.secondaryProgressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- algorithm.indeterminateTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- algorithm.thumbTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- algorithm.tickMarkTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- confirmButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- if (currentTheme == "F7F7F7") {
- addAccountLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- account.setHintTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- account.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- sharedSecret.setHintTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- sharedSecret.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- timeMode.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- counterMode.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- digitLengthLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- algorithmLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- addAccountLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- account.setHintTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- account.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- sharedSecret.setHintTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- sharedSecret.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- timeMode.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- counterMode.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- digitLengthLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- algorithmLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- var accountID = intent.getStringExtra("account_id")
-
- if (accountID != null) {
- addAccountLabel.text = "Edit Account"
- other.visibility = View.VISIBLE
-
- val data = JSONArray(accounts.getString(accountID, null))
-
- account.setText(data[0].toString())
- sharedSecret.setText(data[1].toString())
-
- if (data[2].toString() == "Time") {
- modeGroup.check(R.id.TimeMode)
- } else {
- modeGroup.check(R.id.CounterMode)
- }
-
- when(data[3].toString()) {
- "4" -> digitLength.progress = 0
- "5" -> digitLength.progress = 1
- "6" -> digitLength.progress = 2
- "7" -> digitLength.progress = 3
- "8" -> digitLength.progress = 4
- else -> digitLength.progress = 2
- }
-
- when(data[4].toString()) {
- "HmacAlgorithm.SHA1" -> algorithm.progress = 0
- "HmacAlgorithm.SHA256" -> algorithm.progress = 1
- "HmacAlgorithm.SHA512" -> algorithm.progress = 2
- else -> algorithm.progress = 1
- }
-
- deleteButton.setOnClickListener {
- val intent = Intent(applicationContext, DeleteActivity::class.java)
- intent.putExtra("account_id", accountID)
- startActivity(intent)
- finish()
- }
- }
-
- confirmButton.setOnClickListener {
- val errorToast: Toast?
- val accountData = ArrayList()
- if (account.text.toString() == ""){
- errorToast = Toast.makeText(this, "Enter account name", Toast.LENGTH_SHORT)
- errorToast.show()
- }else if (sharedSecret.text.toString() == ""){
- errorToast = Toast.makeText(this, "Enter shared secret", Toast.LENGTH_SHORT)
- errorToast.show()
- }else if((sharedSecret.text.toString()).length < 8 && selectedDigitLength == "6" && selectedAlgorithm == "HmacAlgorithm.SHA1" && mode == "Time"){
- errorToast = Toast.makeText(this, "Invalid shared secret", Toast.LENGTH_SHORT)
- errorToast.show()
- }else{
- accountData.add(account.text.toString())
- accountData.add(sharedSecret.text.toString())
- accountData.add(mode)
- accountData.add(selectedDigitLength)
- accountData.add(selectedAlgorithm)
- accountData.add("0") // If counter mode is selected, initial value must be 0.
- val json = Gson().toJson(accountData)
-
- if (accountID.isNullOrEmpty()) accountID = UUID.randomUUID().toString()
-
- accounts.edit().putString(accountID, json).apply()
- val addedToast = Toast.makeText(this, "Added account", Toast.LENGTH_SHORT)
- addedToast.show()
-
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
- }
-
- cancelButton.setOnClickListener {
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
-
- modeGroup.setOnCheckedChangeListener { _, checkedId ->
- mode = if (checkedId != -1) {
- (findViewById(checkedId) as RadioButton).text.toString()
- } else {
- ""
- }
- }
-
- digitLength.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
- override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
- if (digitLength.progress == 0) {
- digitLengthLabel.text = "4 digits"
- selectedDigitLength = "4"
- } else if (digitLength.progress == 1) {
- selectedDigitLength = "5"
- digitLengthLabel.text = "5 digits"
- } else if (digitLength.progress == 2) {
- selectedDigitLength = "6"
- digitLengthLabel.text = "6 digits"
- } else if (digitLength.progress == 3) {
- selectedDigitLength = "7"
- digitLengthLabel.text = "7 digits"
- } else if (digitLength.progress == 4) {
- selectedDigitLength = "8"
- digitLengthLabel.text = "8 digits"
- }
- }
-
- override fun onStartTrackingTouch(seekBar: SeekBar?) {
- if (digitLength.progress == 0) {
- digitLengthLabel.text = "4 digits"
- selectedDigitLength = "4"
- } else if (digitLength.progress == 1) {
- selectedDigitLength = "5"
- digitLengthLabel.text = "5 digits"
- } else if (digitLength.progress == 2) {
- selectedDigitLength = "6"
- digitLengthLabel.text = "6 digits"
- } else if (digitLength.progress == 3) {
- selectedDigitLength = "7"
- digitLengthLabel.text = "7 digits"
- } else if (digitLength.progress == 4) {
- selectedDigitLength = "8"
- digitLengthLabel.text = "8 digits"
- }
- }
-
- override fun onStopTrackingTouch(seekBar: SeekBar?) {
- if (digitLength.progress == 0) {
- digitLengthLabel.text = "4 digits"
- selectedDigitLength = "4"
- } else if (digitLength.progress == 1) {
- selectedDigitLength = "5"
- digitLengthLabel.text = "5 digits"
- } else if (digitLength.progress == 2) {
- selectedDigitLength = "6"
- digitLengthLabel.text = "6 digits"
- } else if (digitLength.progress == 3) {
- selectedDigitLength = "7"
- digitLengthLabel.text = "7 digits"
- } else if (digitLength.progress == 4) {
- selectedDigitLength = "8"
- digitLengthLabel.text = "8 digits"
- }
- }
- })
-
- algorithm.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
- override fun onProgressChanged(p0: SeekBar?, p1: Int, p2: Boolean) {
- if (algorithm.progress == 0) {
- algorithmLabel.text = "SHA-1"
- selectedAlgorithm = "HmacAlgorithm.SHA1"
- } else if (algorithm.progress == 1) {
- algorithmLabel.text = "SHA-256"
- selectedAlgorithm = "HmacAlgorithm.SHA256"
- } else if (algorithm.progress == 2) {
- algorithmLabel.text = "SHA-512"
- selectedAlgorithm = "HmacAlgorithm.SHA512"
- }
- }
-
- override fun onStartTrackingTouch(seekBar: SeekBar?) {
- if (algorithm.progress == 0) {
- algorithmLabel.text = "SHA-1"
- selectedAlgorithm = "HmacAlgorithm.SHA1"
- } else if (algorithm.progress == 1) {
- algorithmLabel.text = "SHA-256"
- selectedAlgorithm = "HmacAlgorithm.SHA256"
- } else if (algorithm.progress == 2) {
- algorithmLabel.text = "SHA-512"
- selectedAlgorithm = "HmacAlgorithm.SHA512"
- }
- }
-
- override fun onStopTrackingTouch(seekBar: SeekBar?) {
- if (algorithm.progress == 0) {
- algorithmLabel.text = "SHA-1"
- selectedAlgorithm = "HmacAlgorithm.SHA1"
- } else if (algorithm.progress == 1) {
- algorithmLabel.text = "SHA-256"
- selectedAlgorithm = "HmacAlgorithm.SHA256"
- } else if (algorithm.progress == 2) {
- algorithmLabel.text = "SHA-512"
- selectedAlgorithm = "HmacAlgorithm.SHA512"
- }
- }
- })
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/OtpAuthImport.kt b/app/src/main/java/com/wristkey/OtpAuthImport.kt
deleted file mode 100644
index 5647bf9..0000000
--- a/app/src/main/java/com/wristkey/OtpAuthImport.kt
+++ /dev/null
@@ -1,261 +0,0 @@
-package com.wristkey
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.res.ColorStateList
-import android.graphics.Bitmap
-import android.graphics.BitmapFactory
-import android.graphics.Color
-import android.graphics.Point
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment.getExternalStorageDirectory
-import android.os.Vibrator
-import android.provider.Settings
-import android.text.Editable
-import android.text.TextWatcher
-import android.util.Log
-import android.view.View
-import android.view.WindowManager
-import android.widget.*
-import androidmads.library.qrgenearator.QRGContents
-import androidmads.library.qrgenearator.QRGEncoder
-import androidx.wear.widget.BoxInsetLayout
-import com.google.gson.Gson
-import com.google.zxing.*
-import com.google.zxing.Reader
-import com.google.zxing.common.HybridBinarizer
-import com.wristkey.databinding.ActivityOtpauthImportBinding
-import java.io.*
-import java.util.*
-
-class OtpAuthImport : Activity() {
-
- private lateinit var binding: ActivityOtpauthImportBinding
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityOtpauthImportBinding.inflate(layoutInflater)
- setContentView(binding.root)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
-
- val qrCodesLayout = findViewById(R.id.QRCodesLayout)
- val qrPreview = findViewById(R.id.QRPreview)
- val previous = findViewById(R.id.Previous)
- val next = findViewById(R.id.Next)
-
- val backButton = findViewById(R.id.AuthenticatorBackButton)
- val confirmButton = findViewById(R.id.AuthenticatorConfirmButton)
- val importLabel = findViewById(R.id.AuthenticatorImportLabel)
-
- val otpAuth = findViewById(R.id.OtpAuth)
-
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#" + currentTheme))
-
- otpAuth.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- otpAuth.foregroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- otpAuth.compoundDrawableTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
-
- confirmButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
-
- if (currentTheme == "F7F7F7") {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- otpAuth.setHintTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- otpAuth.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- previous.imageTintList = ColorStateList.valueOf(Color.parseColor("#000000"))
- next.imageTintList = ColorStateList.valueOf(Color.parseColor("#000000"))
- } else {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- otpAuth.setHintTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- otpAuth.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- previous.imageTintList = ColorStateList.valueOf(Color.parseColor("#BDBDBD"))
- next.imageTintList = ColorStateList.valueOf(Color.parseColor("#BDBDBD"))
- }
-
- val files: Array = getExternalStorageDirectory().listFiles()
-
- val filenameList = ArrayList()
- var index = 0
-
- try {
- for (file in files) {
-
- if (file.name.endsWith(".png", ignoreCase = true) || file.name.endsWith(".jpg", ignoreCase = true) || file.name.endsWith(".jpeg", ignoreCase = true)) {
- val reader: InputStream = BufferedInputStream(FileInputStream(file.absoluteFile))
- val imageBitmap = BitmapFactory.decodeStream(reader)
- val decodedQRCodeData: String = scanQRImage(imageBitmap)
-
- if (decodedQRCodeData.contains("otpauth://")) {
- filenameList.add(file.absolutePath.toString())
- }
- }
- }
-
-
- fun getData () {
- val manager = getSystemService(WINDOW_SERVICE) as WindowManager
- val display = manager.defaultDisplay
- val point = Point()
- display.getSize(point)
- val width: Int = point.x
- val height: Int = point.y
- val dimensions = if (width < height) width else height
-
- val reader: InputStream = BufferedInputStream(FileInputStream(filenameList[index]))
- val imageBitmap = BitmapFactory.decodeStream(reader)
- val decodedQRCodeData: String = scanQRImage(imageBitmap)
-
- var qrEncoder = QRGEncoder(decodedQRCodeData, null, QRGContents.Type.TEXT, dimensions)
- qrPreview.setImageBitmap(qrEncoder.encodeAsBitmap())
- otpAuth.setText(decodedQRCodeData)
- }
-
- getData()
-
- if (filenameList.size == 1) {
- previous.visibility = View.GONE
- next.visibility = View.GONE
- } else if (filenameList.size > 1) {
- previous.visibility = View.VISIBLE
- next.visibility = View.VISIBLE
-
- previous.setOnClickListener {
- if (index > 0) index-- else index = 0
- getData()
- }
-
- next.setOnClickListener {
- if (index == filenameList.size-1) index = filenameList.size-1 else index++
- getData()
- }
-
- }
-
- otpAuth.addTextChangedListener(object : TextWatcher {
- override fun afterTextChanged(s: Editable?) { }
-
- override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
-
- override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
- })
-
- } catch (noFileFound: FileNotFoundException) {
- Toast.makeText(this, "Couldn't find file. Check if the file exists and if Wristkey is granted storage permission.", Toast.LENGTH_LONG).show()
-
- val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- val uri: Uri = Uri.fromParts("package", packageName, null)
- settingsIntent.data = uri
- startActivity(settingsIntent)
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
-
- } catch (badData: IllegalArgumentException) {
- Toast.makeText(this, "QR code data corrupt.", Toast.LENGTH_SHORT).show()
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
-
- } catch (noData: IndexOutOfBoundsException) {
- qrCodesLayout.visibility = View.GONE
- importLabel.text = "No QR Codes Found.\nEnter otpauth URL manually."
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- }
-
- backButton.setOnClickListener {
- val intent = Intent(applicationContext, AddActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- confirmButton.setOnClickListener {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- // decode otpauth
-
- val otpAuthUrl = otpAuth.text.toString()
- if (!otpAuthUrl.contains("otpauth://") ||
- !otpAuthUrl.contains("secret") ||
- !otpAuthUrl.contains("otp")) {
- Toast.makeText(this, "Invalid otpauth url", Toast.LENGTH_LONG).show()
- } else {
- val accountData = ArrayList()
- if (otpAuthUrl.substringAfter("otp/").substringBefore("?").isNotEmpty()) { // name
- accountData.add(otpAuthUrl.substringAfter("otp/").substringBefore("?"))
- } else {
- accountData.add(otpAuthUrl.substringAfter("&issuer=").substringBefore("&"))
- }
-
- if (otpAuthUrl.contains("?secret=")) { // secret
- accountData.add(otpAuthUrl.substringAfter("?secret=").substringBefore("&"))
- } else {
- Toast.makeText(this, "Couldn't find secret", Toast.LENGTH_LONG).show()
- finish()
- }
-
- if (otpAuthUrl.contains("totp")) { // type
- accountData.add("Time")
- } else if (otpAuthUrl.contains("hotp")) {
- accountData.add("Counter")
- }
-
- if (otpAuthUrl.contains("&digits=")) { // digits
- accountData.add(otpAuthUrl.substringAfter("&digits=").substringBefore("&"))
- } else {
- accountData.add("6")
- }
-
- if (otpAuthUrl.contains("&algorithm=")) { // algorithm
- accountData.add("HmacAlgorithm."+otpAuthUrl.substringAfter("&algorithm=").substringBefore("&"))
- } else {
- accountData.add("HmacAlgorithm.SHA1")
- }
-
- if (otpAuthUrl.contains("&counter=")) { // counter
- accountData.add(otpAuthUrl.substringAfter("&counter=").substringBefore("&"))
- } else {
- accountData.add("0")
- }
-
- val id = UUID.randomUUID().toString()
- val json = Gson().toJson(accountData)
- if (accounts.all.values.toString().contains(accountData[1])) {
- Toast.makeText(this, "This account already exists.", Toast.LENGTH_LONG).show()
- } else {
- accounts.edit().putString(id, json).apply()
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
-
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
-
- }
- }
- }
-
- fun scanQRImage(bMap: Bitmap): String {
- var contents: String
- val intArray = IntArray(bMap.width * bMap.height)
- //copy pixel data from the Bitmap into the 'intArray' array
- bMap.getPixels(intArray, 0, bMap.width, 0, 0, bMap.width, bMap.height)
- val source: LuminanceSource = RGBLuminanceSource(bMap.width, bMap.height, intArray)
- val bitmap = BinaryBitmap(HybridBinarizer(source))
- val reader: Reader = MultiFormatReader()
- try {
- val result = reader.decode(bitmap)
- contents = result.text
- } catch (e: Exception) {
- contents = "No data found"
- }
- return contents
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/QRCodeActivity.kt b/app/src/main/java/com/wristkey/QRCodeActivity.kt
deleted file mode 100644
index 1b47416..0000000
--- a/app/src/main/java/com/wristkey/QRCodeActivity.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package com.wristkey
-
-import android.app.KeyguardManager
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.graphics.Point
-import android.os.Bundle
-import android.support.wearable.activity.WearableActivity
-import android.view.WindowManager
-import android.widget.ImageView
-import android.widget.Toast
-import androidmads.library.qrgenearator.QRGContents
-import androidmads.library.qrgenearator.QRGEncoder
-import androidx.wear.widget.BoxInsetLayout
-import com.google.zxing.WriterException
-
-class QRCodeActivity : WearableActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_qrcode)
-
- if (appData.getBoolean("screen_lock", true)) {
- val lockscreen = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
- if (lockscreen.isKeyguardSecure) {
- val i = lockscreen.createConfirmDeviceCredentialIntent("Wristkey", "App locked")
- startActivityForResult(i, CODE_AUTHENTICATION_VERIFICATION)
- }
- }
-
- val manager = getSystemService(WINDOW_SERVICE) as WindowManager
- val display = manager.defaultDisplay
- val point = Point()
- display.getSize(point)
- val width: Int = point.x
- val height: Int = point.y
- val dimensions = if (width < height) width else height
- val qrCode = findViewById(R.id.qrCode)
- val boxInsetLayout = findViewById(R.id.BoxInsetLayout)
-
- val qrData = intent.getStringExtra("qr_data")
-
- val qrEncoder = QRGEncoder(qrData, null, QRGContents.Type.TEXT, dimensions)
-
- try {
- qrCode.setImageBitmap(qrEncoder.encodeAsBitmap())
- } catch (e: WriterException) { }
-
- qrCode.setOnClickListener {
- if (qrCode.imageTintList == ColorStateList.valueOf(Color.parseColor("#BF000000"))) {
- qrCode.imageTintList = ColorStateList.valueOf(Color.parseColor("#00000000"))
- boxInsetLayout.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#FFFFFFFF"))
- Toast.makeText(applicationContext, "Normal", Toast.LENGTH_SHORT).show()
- } else {
- qrCode.imageTintList = ColorStateList.valueOf(Color.parseColor("#BF000000"))
- boxInsetLayout.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#40FFFFFF"))
- Toast.makeText(applicationContext, "Dimmed", Toast.LENGTH_SHORT).show()
- }
- }
- }
-
- override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- super.onActivityResult(requestCode, resultCode, data)
- if (!(resultCode == RESULT_OK && requestCode == CODE_AUTHENTICATION_VERIFICATION)) {
- finish()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/RecyclerViewAdapter.kt b/app/src/main/java/com/wristkey/RecyclerViewAdapter.kt
deleted file mode 100644
index b332a6b..0000000
--- a/app/src/main/java/com/wristkey/RecyclerViewAdapter.kt
+++ /dev/null
@@ -1,192 +0,0 @@
-package com.wristkey
-
-import android.content.Context
-import android.content.Intent
-import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.os.Handler
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import android.widget.TextView
-import androidx.cardview.widget.CardView
-import androidx.core.content.ContextCompat.startActivity
-import android.util.Log
-import androidx.recyclerview.widget.RecyclerView
-import org.json.JSONArray
-
-
-data class Account(val id: String, val accountName: String, val code: String, val counter: String)
-
-class TimeCardAdapter(context: Context, private val accountList: ArrayList, val clickListener: (Account) -> Unit) : RecyclerView.Adapter() {
- //this method is returning the view for each item in the list
- val context = context
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val v = LayoutInflater.from(parent.context).inflate(R.layout.time_card, parent, false)
- return ViewHolder(context, v)
- }
- //this method is binding the data on the list
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bindItems(accountList[position])
- val item : Account = accountList[position]
- holder.itemView.setOnClickListener {
- clickListener(item)
- }
- }
- //this method is giving the size of the list
- override fun getItemCount(): Int {
- return accountList.size
- }
- //the class is holding the list view
- class ViewHolder(context: Context, itemView: View) : RecyclerView.ViewHolder(itemView) {
- val context = context
- fun bindItems(account: Account) {
- var currentTheme = appData.getString("theme", "000000")
- val timeCard=itemView.findViewById(R.id.TimeCard)
- val accountName = itemView.findViewById(R.id.ServiceName)
- val code = itemView.findViewById(R.id.Code)
- if (currentTheme == "F7F7F7") {
- timeCard.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
- accountName.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- code.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- accountName.text = "white"
- } else if (currentTheme == "192835") {
- timeCard.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#3A4149"))
- accountName.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- code.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- } else if (currentTheme == "000000") {
- timeCard.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#0E1013"))
- accountName.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- code.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
- accountName.text = account.accountName
- accountName.isSelected = true
- code.text = account.code.replace("...".toRegex(), "$0 ")
- val accountID=account.id
- accountName.setOnLongClickListener {
- val intent = Intent(context, ManualEntryActivity::class.java)
- intent.putExtra("account_id", accountID)
- intent.flags = FLAG_ACTIVITY_NEW_TASK
- context.startActivity(intent)
- true
- }
-
- code.setOnLongClickListener {
- val qrcodeData: String
- val sitename: String
- val accountNameForQRCode: String
- if (account.accountName.contains("(") && account.accountName.contains(")")) {
- accountNameForQRCode = account.accountName.substringAfter("(").substringBefore(")")
- sitename = account.accountName.substringBefore("(")
- qrcodeData = "otpauth://totp/${accountNameForQRCode}?secret=${JSONArray(accounts.getString(accountID, null))[1]}&issuer=${sitename}" // where 1 is the array index for the secret
- } else {
- sitename = account.accountName.substringBefore("(")
- qrcodeData = "otpauth://totp/?secret=${JSONArray(accounts.getString(accountID, null))[1]}&issuer=${sitename}" // where 1 is the array index for the secret
- }
-
- val intent = Intent(context, QRCodeActivity::class.java)
- intent.putExtra("qr_data", qrcodeData)
- intent.flags = FLAG_ACTIVITY_NEW_TASK
- context.startActivity(intent)
- true
- }
-
- }
- }
-}
-
-
-class CounterCardAdapter(context: Context, private val accountList: ArrayList, val clickListener: (Account) -> Unit) : RecyclerView.Adapter() {
- //this method is returning the view for each item in the list
- val context = context
- override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
- val v = LayoutInflater.from(parent.context).inflate(R.layout.counter_card, parent, false)
- return ViewHolder(context, v)
- }
- //this method is binding the data on the list
- override fun onBindViewHolder(holder: ViewHolder, position: Int) {
- holder.bindItems(accountList[position])
- val item : Account = accountList[position]
- holder.itemView.setOnClickListener {
- clickListener(item)
- }
- }
- //this method is giving the size of the list
- override fun getItemCount(): Int {
- return accountList.size
- }
- //the class is holding the list view
- class ViewHolder(context: Context, itemView: View) : RecyclerView.ViewHolder(itemView) {
- val context = context
- fun bindItems(account: Account) {
- var currentTheme = appData.getString("theme", "000000")
- val counterCard=itemView.findViewById(R.id.CounterCard)
- val accountName = itemView.findViewById(R.id.ServiceName)
- val code = itemView.findViewById(R.id.Code)
- val counter = itemView.findViewById(R.id.Counter)
- if (currentTheme == "F7F7F7") {
- counterCard.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#FFFFFF"))
- accountName.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- code.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- counter.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else if (currentTheme == "192835") {
- counterCard.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#3A4149"))
- accountName.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- code.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- counter.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- } else if (currentTheme == "000000") {
- counterCard.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#0E1013"))
- accountName.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- code.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- counter.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
- accountName.text = account.accountName
- accountName.isSelected = true
- code.text = account.code.replace("...".toRegex(), "$0 ")
- counter.text = "#"+account.counter
- val currentCounterValue=account.counter.toInt()
- val accountID=account.id
- code.setOnClickListener{
- var newCounterValue = currentCounterValue+1
- val data =
- accounts
- .getString(accountID, null).toString()
- .removeSuffix("\"]")
- .replaceAfterLast("\"", "")
- val newData = "$data$newCounterValue\"]"
- accounts.edit().putString(accountID, newData).apply()
- code.text = "Code used"
- }
-
- accountName.setOnLongClickListener{
- val intent = Intent(context, ManualEntryActivity::class.java)
- intent.putExtra("account_id", accountID)
- intent.flags = FLAG_ACTIVITY_NEW_TASK
- context.startActivity(intent)
- true
- }
-
- code.setOnLongClickListener {
- val qrcodeData: String
- val sitename: String
- val accountNameForQRCode: String
- if (account.accountName.contains("(") && account.accountName.contains(")")) {
- accountNameForQRCode = account.accountName.substringAfter("(").substringBefore(")")
- sitename = account.accountName.substringBefore("(")
- qrcodeData = "otpauth://hotp/${accountNameForQRCode}?secret=${JSONArray(appData.getString(accountID, null))[2]}&issuer=${sitename}" // where 2 is the array index for the secret
- } else {
- sitename = account.accountName.substringBefore("(")
- qrcodeData = "otpauth://hotp/?secret=${JSONArray(appData.getString(accountID, null))[2]}&issuer=${sitename}" // where 2 is the array index for the secret
- }
-
- val intent = Intent(context, QRCodeActivity::class.java)
- intent.putExtra("qr_data", qrcodeData)
- intent.flags = FLAG_ACTIVITY_NEW_TASK
- context.startActivity(intent)
- true
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/SettingsActivity.kt b/app/src/main/java/com/wristkey/SettingsActivity.kt
deleted file mode 100644
index 803ef84..0000000
--- a/app/src/main/java/com/wristkey/SettingsActivity.kt
+++ /dev/null
@@ -1,194 +0,0 @@
-package com.wristkey
-
-import android.app.KeyguardManager
-import android.content.Context
-import android.content.Intent
-import android.content.SharedPreferences
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.os.Bundle
-import android.os.Vibrator
-import android.support.wearable.activity.WearableActivity
-import android.view.View
-import android.widget.*
-import androidx.wear.widget.BoxInsetLayout
-
-class SettingsActivity : WearableActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_settings)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val settingsLabelText = findViewById(R.id.SettingsLabel)
- val themeLabelText = findViewById(R.id.ThemeLabel)
- val alertsLabelText = findViewById(R.id.AlertsLabel)
- val privacyLabelText = findViewById(R.id.privacyLabel)
- val beep = findViewById(R.id.Beep)
- val vibrate = findViewById(R.id.Vibrate)
- val ambientMode = findViewById(R.id.AmbientMode)
- val screenLock = findViewById(R.id.ScreenLock)
- val accentLabelText = findViewById(R.id.AccentLabel)
- val numberOfItemsText = findViewById(R.id.NumberOfItems)
- val deleteButtonText = findViewById(R.id.DeleteAllAccountsButtonLabel)
- val deleteButton = findViewById(R.id.DeleteButton)
- val exportButtonText = findViewById(R.id.ExportAllAccountsButtonLabel)
- val exportButton = findViewById(R.id.ExportButton)
- val backButton = findViewById(R.id.AuthenticatorBackButton)
- val accentGroup = findViewById(R.id.AccentRadioGroup)
- val themeGroup = findViewById(R.id.ThemeRadioGroup)
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
-
- boxinsetlayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- backButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- deleteButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- ambientMode.buttonTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- screenLock.buttonTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- beep.buttonTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- vibrate.buttonTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- exportButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
-
- beep.isChecked = appData.getBoolean("beep", false)
- vibrate.isChecked = appData.getBoolean("vibrate", false)
- ambientMode.isChecked = appData.getBoolean("ambient_mode", false)
- screenLock.isChecked = appData.getBoolean("screen_lock", true)
-
- if (currentTheme == "F7F7F7") {
- settingsLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- alertsLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- privacyLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- beep.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- vibrate.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- ambientMode.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- screenLock.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- themeLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- accentLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- deleteButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- exportButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- settingsLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- alertsLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- privacyLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- beep.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- vibrate.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- ambientMode.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- screenLock.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- themeLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- accentLabelText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- deleteButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- exportButtonText.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- if (currentAccent == "FF4141") {
- accentGroup.check(R.id.Red)
- } else if (currentAccent == "FF6D00") {
- accentGroup.check(R.id.Saffron)
- } else if (currentAccent == "FFBB00") {
- accentGroup.check(R.id.Yellow)
- } else if (currentAccent == "4285F4") {
- accentGroup.check(R.id.Blue)
- } else if (currentAccent == "009688") {
- accentGroup.check(R.id.Teal)
- } else if (currentAccent == "434343") {
- accentGroup.check(R.id.Dark)
- }
-
- if (currentTheme == "F7F7F7") {
- themeGroup.check(R.id.LightTheme)
- } else if (currentTheme == "192835") {
- themeGroup.check(R.id.GrayTheme)
- } else if (currentTheme == "000000") {
- themeGroup.check(R.id.DarkTheme)
- }
-
- numberOfItemsText.text = "${accounts.all.size} accounts"
-
- deleteButton.setOnClickListener {
- val intent = Intent(applicationContext, DeleteActivity::class.java)
- intent.putExtra("delete_all", true)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(500)
- true
- finish()
- }
-
- if (accounts.all.isNotEmpty()) {
- exportButtonText.setOnClickListener {
- val intent = Intent(applicationContext, ExportActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- true
- finish()
- }
- } else {
- exportButton.visibility = View.GONE
- exportButtonText.visibility = View.GONE
- val divider2 = findViewById(R.id.divider2)
- divider2.visibility = View.GONE
- }
-
- accentGroup.setOnCheckedChangeListener { _, _ ->
- val selectedIndex =
- accentGroup.indexOfChild(findViewById(accentGroup.checkedRadioButtonId)).toString()
- if (selectedIndex == "0") {
- appData.edit().putString("accent", "FF4141").apply()
- } else if (selectedIndex == "1") {
- appData.edit().putString("accent", "FF6D00").apply()
- } else if (selectedIndex == "2") {
- appData.edit().putString("accent", "FFBB00").apply()
- } else if (selectedIndex == "3") {
- appData.edit().putString("accent", "4285F4").apply()
- } else if (selectedIndex == "4") {
- appData.edit().putString("accent", "009688").apply()
- } else if (selectedIndex == "5") {
- appData.edit().putString("accent", "434343").apply()
- }
- }
-
- themeGroup.setOnCheckedChangeListener { _, _ ->
- val selectedIndex =
- themeGroup.indexOfChild(findViewById(themeGroup.checkedRadioButtonId)).toString()
- if (selectedIndex == "0") {
- appData.edit().putString("theme", "F7F7F7").apply()
- } else if (selectedIndex == "1") {
- appData.edit().putString("theme", "192835").apply()
- } else if (selectedIndex == "2") {
- appData.edit().putString("theme", "000000").apply()
- }
- appData.edit().apply()
- }
-
- beep.setOnCheckedChangeListener { _, b ->
- appData.edit().putBoolean("beep", b).apply()
- }
-
- vibrate.setOnCheckedChangeListener { _, b ->
- appData.edit().putBoolean("vibrate", b).apply()
- }
-
- val lockscreen = getSystemService(KEYGUARD_SERVICE) as KeyguardManager
- if (!lockscreen.isKeyguardSecure) {
- screenLock.visibility = View.GONE
- } else {
- screenLock.visibility = View.VISIBLE
- }
-
- ambientMode.setOnCheckedChangeListener { _, b ->
- appData.edit().putBoolean("ambient_mode", b).apply()
- }
-
- screenLock.setOnCheckedChangeListener { _, b ->
- appData.edit().putBoolean("screen_lock", b).apply()
- }
-
- backButton.setOnClickListener {
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/wristkey/WristkeyImport.kt b/app/src/main/java/com/wristkey/WristkeyImport.kt
deleted file mode 100644
index e7c099a..0000000
--- a/app/src/main/java/com/wristkey/WristkeyImport.kt
+++ /dev/null
@@ -1,150 +0,0 @@
-package com.wristkey
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.content.res.ColorStateList
-import android.graphics.Color
-import android.net.Uri
-import android.os.Bundle
-import android.os.Environment.getExternalStorageDirectory
-import android.os.Vibrator
-import android.provider.Settings
-import android.widget.ImageButton
-import android.widget.ProgressBar
-import android.widget.TextView
-import android.widget.Toast
-import androidx.wear.widget.BoxInsetLayout
-import com.google.gson.Gson
-import com.wristkey.databinding.ActivityWristkeyImportBinding
-import org.json.JSONArray
-import org.json.JSONException
-import java.io.File
-import java.io.FileReader
-import java.util.*
-
-class WristkeyImport : Activity() {
-
- private lateinit var binding: ActivityWristkeyImportBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- binding = ActivityWristkeyImportBinding.inflate(layoutInflater)
- setContentView(binding.root)
- val boxinsetlayout = findViewById(R.id.BoxInsetLayout)
- val backButton = findViewById(R.id.BackButton)
- val confirmButton = findViewById(R.id.ConfirmButton)
- val importLabel = findViewById(R.id.ImportLabel)
- val description = findViewById(R.id.Description)
- var theme = "Dark"
- var accent = "Blue"
- var currentAccent = appData.getString("accent", "4285F4")
- var currentTheme = appData.getString("theme", "000000")
- boxinsetlayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- confirmButton.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#"+currentAccent))
- if (currentTheme == "F7F7F7") {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- description.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- backButton.setOnClickListener {
- val intent = Intent(applicationContext, AddActivity::class.java)
- startActivity(intent)
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
-
- confirmButton.setOnClickListener {
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
-
- val files: Array = getExternalStorageDirectory().listFiles()
- // start import
- try {
- for (file in files) {
- if (file.name.endsWith(".backup")) {
- val reader = FileReader(file.path)
- val jsonData = reader.readText()
- val itemsArray = JSONArray(jsonData)
-
- setContentView(R.layout.import_loading_screen)
- val loadingLayout = findViewById(R.id.LoadingLayout)
- val loadingIcon = findViewById(R.id.LoadingIcon)
- val importingLabel = findViewById(R.id.ImportingLabel)
- val importingDescription = findViewById(R.id.ImportingDescription)
- loadingLayout.setBackgroundColor(Color.parseColor("#"+currentTheme))
- loadingIcon.progressTintList = ColorStateList.valueOf(Color.parseColor("#" + currentAccent))
- loadingIcon.backgroundTintList = ColorStateList.valueOf(Color.parseColor("#" + currentTheme))
- if (currentTheme == "F7F7F7") {
- importingLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- importingDescription.setTextColor(ColorStateList.valueOf(Color.parseColor("#000000")))
- } else {
- importingLabel.setTextColor(ColorStateList.valueOf(Color.parseColor("#BDBDBD")))
- importingDescription.setTextColor(ColorStateList.valueOf(Color.parseColor("#FFFFFF")))
- }
-
- //found x number of items
-
- importingDescription.text = "Found ${itemsArray.length()} items"
-
- for (itemIndex in 0 until itemsArray.length()) {
- try {
- val item = itemsArray[itemIndex].toString()
- val name = JSONArray(item)[0].toString()
- val totpSecret = JSONArray(item)[1].toString()
- val mode = JSONArray(item)[2].toString()
- val digits = JSONArray(item)[3].toString()
- val algorithm = JSONArray(item)[4].toString()
- val counter = JSONArray(item)[5].toString()
-
- if (totpSecret.isNotEmpty()) { // begin storing data
- importingDescription.text = "Adding $name"
- val accountData = ArrayList()
- accountData.add(name)
- accountData.add(totpSecret)
- accountData.add(mode)
- accountData.add(digits)
- accountData.add(algorithm)
- accountData.add(counter)
- val json = Gson().toJson(accountData)
- val id = UUID.randomUUID().toString()
- accounts.edit().putString(id, json).apply()
- } else {
- importingDescription.text = "No TOTP secret for $name"
- }
- } catch (noData: JSONException) { }
- }
- importingDescription.text = "Saving data"
- val toast = Toast.makeText(this, "Imported accounts successfully!", Toast.LENGTH_SHORT)
- toast.show()
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(100)
- val intent = Intent(applicationContext, MainActivity::class.java)
- startActivity(intent)
- finish()
- }
- }
-
- } catch (noFileFound: IllegalStateException) {
- val toast = Toast.makeText(this, "Couldn't find file. Check if the file exists and if Wristkey is granted storage permission.", Toast.LENGTH_LONG)
- toast.show()
-
- val settingsIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
- val uri: Uri = Uri.fromParts("package", packageName, null)
- settingsIntent.data = uri
- startActivity(settingsIntent)
-
- val vibratorService = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
- vibratorService.vibrate(50)
- finish()
- }
- // stop import
- }
-
- }
-}
\ No newline at end of file
diff --git a/app/src/main/python/extract_otp_secret_keys.py b/app/src/main/python/extract_otp_secret_keys.py
index 88cec01..8a49dba 100644
--- a/app/src/main/python/extract_otp_secret_keys.py
+++ b/app/src/main/python/extract_otp_secret_keys.py
@@ -74,7 +74,7 @@ def convert_secret_from_bytes_to_base32_str(bytes):
i += 1
# pylint: disable=no-member
json = ""
- json+= "<$beginwristkeygoogleauthenticatorimport$>"
+ json+= ""
json+=("{")
for otp in payload.otp_parameters:
@@ -96,5 +96,5 @@ def convert_secret_from_bytes_to_base32_str(bytes):
json+=("},")
json=json[:-1]
json+=("}")
- json+= "<$endwristkeygoogleauthenticatorimport$>"
+ json+= "<\wristkey>"
print(json)
\ No newline at end of file
diff --git a/app/src/main/res/anim/heartbeat.xml b/app/src/main/res/anim/heartbeat.xml
new file mode 100644
index 0000000..4a7a062
--- /dev/null
+++ b/app/src/main/res/anim/heartbeat.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_right.xml b/app/src/main/res/anim/slide_right.xml
new file mode 100644
index 0000000..36c1392
--- /dev/null
+++ b/app/src/main/res/anim/slide_right.xml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_right_controller.xml b/app/src/main/res/anim/slide_right_controller.xml
new file mode 100644
index 0000000..fe5cdf6
--- /dev/null
+++ b/app/src/main/res/anim/slide_right_controller.xml
@@ -0,0 +1,4 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-mdpi/owais_bitcoin_wear.png b/app/src/main/res/drawable-mdpi/owais_bitcoin_wear.png
new file mode 100644
index 0000000..df5d4b8
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/owais_bitcoin_wear.png differ
diff --git a/app/src/main/res/drawable/gradient_background.xml b/app/src/main/res/drawable/gradient_background.xml
new file mode 100644
index 0000000..b49448a
--- /dev/null
+++ b/app/src/main/res/drawable/gradient_background.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml b/app/src/main/res/drawable/ic_baseline_add_24.xml
similarity index 64%
rename from app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml
rename to app/src/main/res/drawable/ic_baseline_add_24.xml
index 045d385..89633bb 100644
--- a/app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml
+++ b/app/src/main/res/drawable/ic_baseline_add_24.xml
@@ -1,5 +1,5 @@
-
-
+
diff --git a/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml b/app/src/main/res/drawable/ic_baseline_add_circle_24.xml
similarity index 54%
rename from app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml
rename to app/src/main/res/drawable/ic_baseline_add_circle_24.xml
index 2a02e12..4fcaa07 100644
--- a/app/src/main/res/drawable/ic_baseline_insert_drive_file_24.xml
+++ b/app/src/main/res/drawable/ic_baseline_add_circle_24.xml
@@ -1,5 +1,5 @@
-
-
+
diff --git a/app/src/main/res/drawable/ic_baseline_send_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml
similarity index 67%
rename from app/src/main/res/drawable/ic_baseline_send_24.xml
rename to app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml
index 5b10648..14180d9 100644
--- a/app/src/main/res/drawable/ic_baseline_send_24.xml
+++ b/app/src/main/res/drawable/ic_baseline_arrow_back_ios_24.xml
@@ -1,5 +1,5 @@
-
+
diff --git a/app/src/main/res/drawable/ic_baseline_more_time_24.xml b/app/src/main/res/drawable/ic_baseline_more_time_24.xml
new file mode 100644
index 0000000..1dd1d1c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_more_time_24.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_remove_circle_24.xml
similarity index 59%
rename from app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
rename to app/src/main/res/drawable/ic_baseline_remove_circle_24.xml
index fa122e1..86d8c4b 100644
--- a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml
+++ b/app/src/main/res/drawable/ic_baseline_remove_circle_24.xml
@@ -1,5 +1,5 @@
-
-
+
diff --git a/app/src/main/res/drawable/ic_baseline_vibration_24.xml b/app/src/main/res/drawable/ic_baseline_vibration_24.xml
new file mode 100644
index 0000000..ba58866
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_vibration_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_watch_24.xml b/app/src/main/res/drawable/ic_baseline_watch_24.xml
new file mode 100644
index 0000000..85ffbe2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_watch_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_access_time_24.xml b/app/src/main/res/drawable/ic_outline_access_time_24.xml
new file mode 100644
index 0000000..50cde10
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_access_time_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_delete_24.xml b/app/src/main/res/drawable/ic_outline_delete_24.xml
new file mode 100644
index 0000000..62d04a2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_delete_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_delete_forever_24.xml b/app/src/main/res/drawable/ic_outline_delete_forever_24.xml
new file mode 100644
index 0000000..f2e260b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_delete_forever_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_info_24.xml b/app/src/main/res/drawable/ic_outline_info_24.xml
new file mode 100644
index 0000000..dca49ae
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_info_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_insert_drive_file_24.xml b/app/src/main/res/drawable/ic_outline_insert_drive_file_24.xml
new file mode 100644
index 0000000..655fca8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_insert_drive_file_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_lock_24.xml b/app/src/main/res/drawable/ic_outline_lock_24.xml
new file mode 100644
index 0000000..0157735
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_lock_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_palette_24.xml b/app/src/main/res/drawable/ic_outline_palette_24.xml
new file mode 100644
index 0000000..159e106
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_palette_24.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_photo_camera_24.xml b/app/src/main/res/drawable/ic_outline_photo_camera_24.xml
new file mode 100644
index 0000000..b32f450
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_photo_camera_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_qr_code_2_24.xml b/app/src/main/res/drawable/ic_outline_qr_code_2_24.xml
new file mode 100644
index 0000000..3ecf085
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_qr_code_2_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_send_24.xml b/app/src/main/res/drawable/ic_outline_send_24.xml
new file mode 100644
index 0000000..c2b847c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_send_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_settings_24.xml b/app/src/main/res/drawable/ic_outline_settings_24.xml
new file mode 100644
index 0000000..c624d19
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_settings_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_volume_up_24.xml b/app/src/main/res/drawable/ic_outline_volume_up_24.xml
new file mode 100644
index 0000000..429aea9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_volume_up_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_twotone_insert_drive_file_24.xml b/app/src/main/res/drawable/ic_twotone_insert_drive_file_24.xml
deleted file mode 100644
index cce8e71..0000000
--- a/app/src/main/res/drawable/ic_twotone_insert_drive_file_24.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_twotone_qr_code_24.xml b/app/src/main/res/drawable/ic_twotone_qr_code_24.xml
deleted file mode 100644
index 81d2a29..0000000
--- a/app/src/main/res/drawable/ic_twotone_qr_code_24.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_twotone_send_24.xml b/app/src/main/res/drawable/ic_twotone_send_24.xml
deleted file mode 100644
index 897d15b..0000000
--- a/app/src/main/res/drawable/ic_twotone_send_24.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/wristkey.xml b/app/src/main/res/drawable/wristkey.xml
index 1141df5..9b30ca3 100644
--- a/app/src/main/res/drawable/wristkey.xml
+++ b/app/src/main/res/drawable/wristkey.xml
@@ -1,8 +1,8 @@
+ android:width="42dp"
+ android:height="42dp">
diff --git a/app/src/main/res/font/productsans.ttf b/app/src/main/res/font/productsans.ttf
new file mode 100644
index 0000000..c0442ee
Binary files /dev/null and b/app/src/main/res/font/productsans.ttf differ
diff --git a/app/src/main/res/font/sfmono.ttf b/app/src/main/res/font/sfmono.ttf
new file mode 100644
index 0000000..18395b3
Binary files /dev/null and b/app/src/main/res/font/sfmono.ttf differ
diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml
index 5db9679..26023f4 100644
--- a/app/src/main/res/layout/activity_about.xml
+++ b/app/src/main/res/layout/activity_about.xml
@@ -1,13 +1,13 @@
-
-
-