├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ └── ic_launcher_round.png
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── values-night
│ │ │ │ └── themes.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ ├── layout
│ │ │ │ └── activity_main.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ ├── com
│ │ │ │ └── fossil
│ │ │ │ │ └── crypto
│ │ │ │ │ └── EllipticCurveKeyPair$CppProxy.java
│ │ │ └── d
│ │ │ │ └── d
│ │ │ │ └── watchauthenticator
│ │ │ │ └── MainActivity.java
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── d
│ │ │ └── d
│ │ │ └── watchauthenticator
│ │ │ └── ExampleUnitTest.java
│ └── androidTest
│ │ └── java
│ │ └── d
│ │ └── d
│ │ └── watchauthenticator
│ │ └── ExampleInstrumentedTest.java
├── proguard-rules.pro
└── build.gradle
├── settings.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── gradle.properties
├── README.md
├── gradlew.bat
└── gradlew
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 | rootProject.name = "WatchAuthenticator"
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | WatchAuthenticator
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dakhnod/WatchAuthenticator/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | .idea/
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 | jniLibs
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Jul 02 00:21:16 CEST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/test/java/d/d/watchauthenticator/ExampleUnitTest.java:
--------------------------------------------------------------------------------
1 | package d.d.watchauthenticator;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.junit.Assert.*;
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * @see Testing documentation
11 | */
12 | public class ExampleUnitTest {
13 | @Test
14 | public void addition_isCorrect() {
15 | assertEquals(4, 2 + 2);
16 | }
17 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/app/src/androidTest/java/d/d/watchauthenticator/ExampleInstrumentedTest.java:
--------------------------------------------------------------------------------
1 | package d.d.watchauthenticator;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.test.platform.app.InstrumentationRegistry;
6 | import androidx.test.ext.junit.runners.AndroidJUnit4;
7 |
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 |
11 | import static org.junit.Assert.*;
12 |
13 | /**
14 | * Instrumented test, which will execute on an Android device.
15 | *
16 | * @see Testing documentation
17 | */
18 | @RunWith(AndroidJUnit4.class)
19 | public class ExampleInstrumentedTest {
20 | @Test
21 | public void useAppContext() {
22 | // Context of the app under test.
23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
24 | assertEquals("d.d.watchauthenticator", appContext.getPackageName());
25 | }
26 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/fossil/crypto/EllipticCurveKeyPair$CppProxy.java:
--------------------------------------------------------------------------------
1 | package com.fossil.crypto;
2 |
3 | public class EllipticCurveKeyPair$CppProxy {
4 | private long nativeRef;
5 |
6 | public EllipticCurveKeyPair$CppProxy(long nativeRef) {
7 | this.nativeRef = nativeRef;
8 | }
9 |
10 | static {
11 | System.loadLibrary("EllipticCurveCrypto");
12 | }
13 |
14 | public byte[] getPrivateKey(){
15 | return this.native_privateKey(this.nativeRef);
16 | }
17 |
18 | public byte[] getPublicKey(){
19 | return this.native_publicKey(this.nativeRef);
20 | }
21 |
22 | public byte[] calculateSecretKey(byte[] otherPublic){
23 | return this.native_calculateSecretKey(this.nativeRef, otherPublic);
24 | }
25 |
26 | public native static EllipticCurveKeyPair$CppProxy create();
27 | private native byte[] native_privateKey(long ref);
28 | private native byte[] native_publicKey(long ref);
29 | private native byte[] native_calculateSecretKey(long ref, byte[] otherPublic);
30 | }
31 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | }
4 |
5 | android {
6 | compileSdkVersion 29
7 | buildToolsVersion "29.0.3"
8 |
9 | defaultConfig {
10 | applicationId "d.d.watchauthenticator"
11 | minSdkVersion 26
12 | targetSdkVersion 29
13 | versionCode 2
14 | versionName "1.1"
15 |
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 | }
30 |
31 | dependencies {
32 |
33 | implementation 'androidx.appcompat:appcompat:1.2.0'
34 | implementation 'com.google.android.material:material:1.2.1'
35 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
36 | testImplementation 'junit:junit:4.+'
37 | androidTestImplementation 'androidx.test.ext:junit:1.1.1'
38 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
39 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
18 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FossilHRAuthenticator
2 | Create a shared secret for Fossil HR watches and more.
3 |
4 | ## how to use
5 | This app allows authenticating against all endpoints that support the same protocol.
6 | I, for instance, use the authenticator to authenticate my Fossil HR against the Fossil servers.
7 |
8 | First, the proper crypto lib needs to be injected into the project.
9 | The proper lib can be acquired from already existing Apks.
10 | For educational purposes, you can extract the libs from an app like "regualr.
11 | Just unzip the .apk and extract the "libs/" content, and pack it into the "jniLibs" folder from this project.
12 |
13 |
14 |
15 | The, after compiling and installing, enter the proper endpoints and credentials into the app.
16 | Once again, for educational purposes, I used Fossils servers.
17 | If authentication succeeds, the app saves the refresh token and reuses it on next open.
18 | (In the screenshot, all the urls start with "https://c.fossil.com", although it is cut off in the screenshot.
19 |
20 |
21 |
22 | My latest attempt worked with:
23 | Auth Endpoint: https://api.fossil.linkplatforms.com/v2/rpc/auth/login/
24 | Handshake Endpoint: https://api.fossil.linkplatforms.com/v2/rpc/device/
25 |
26 | also, other people reported that those endpoints work, only for educational purposes, though:
27 | - https://api.skagen.linkplatforms.com/v2.1/rpc/auth/
28 | - https://api.skagen.linkplatforms.com/v2.1/rpc/device/
29 | - https://api.skagen.linkplatforms.com/v2/users/me/devices
30 | - https://api.skagen.linkplatforms.com/v2/users/me/devices/%s/secret-key
31 |
32 | ### Negotiating new key with watch
33 | Clicking on a scan result should result in a successfull Key negotiaion and the key in the clipboard,
34 | ready to be pasted in apps like GB.
35 | For this, only the first two endpoints need to be configured.
36 |
37 | ### Retrieving key from Server
38 | If the device is paired with the manufacturer you can use the "Fetch key from server" button to download the key from the server.
39 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
19 |
20 |
26 |
27 |
33 |
34 |
40 |
41 |
47 |
48 |
54 |
55 |
61 |
62 |
67 |
68 |
74 |
75 |
81 |
82 |
86 |
87 |
88 |
89 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/app/src/main/java/d/d/watchauthenticator/MainActivity.java:
--------------------------------------------------------------------------------
1 | package d.d.watchauthenticator;
2 |
3 | import androidx.appcompat.app.AppCompatActivity;
4 |
5 | import android.bluetooth.BluetoothAdapter;
6 | import android.bluetooth.BluetoothDevice;
7 | import android.bluetooth.BluetoothGatt;
8 | import android.bluetooth.BluetoothGattCallback;
9 | import android.bluetooth.BluetoothGattCharacteristic;
10 | import android.bluetooth.BluetoothGattDescriptor;
11 | import android.bluetooth.BluetoothGattService;
12 | import android.bluetooth.BluetoothManager;
13 | import android.bluetooth.le.BluetoothLeScanner;
14 | import android.bluetooth.le.ScanCallback;
15 | import android.bluetooth.le.ScanFilter;
16 | import android.bluetooth.le.ScanResult;
17 | import android.bluetooth.le.ScanSettings;
18 | import android.content.ClipData;
19 | import android.content.ClipboardManager;
20 | import android.content.SharedPreferences;
21 | import android.os.Bundle;
22 | import android.os.ParcelUuid;
23 | import android.text.method.ScrollingMovementMethod;
24 | import android.util.Base64;
25 | import android.util.Log;
26 | import android.view.View;
27 | import android.widget.AdapterView;
28 | import android.widget.ArrayAdapter;
29 | import android.widget.Button;
30 | import android.widget.EditText;
31 | import android.widget.ListView;
32 | import android.widget.TextView;
33 | import android.widget.Toast;
34 |
35 | import com.fossil.crypto.EllipticCurveKeyPair$CppProxy;
36 |
37 | import org.json.JSONArray;
38 | import org.json.JSONException;
39 | import org.json.JSONObject;
40 |
41 | import java.io.IOException;
42 | import java.io.InputStream;
43 | import java.net.HttpURLConnection;
44 | import java.net.URL;
45 | import java.nio.ByteBuffer;
46 | import java.nio.ByteOrder;
47 | import java.security.KeyManagementException;
48 | import java.security.NoSuchAlgorithmException;
49 | import java.security.cert.CertificateException;
50 | import java.security.cert.X509Certificate;
51 | import java.util.ArrayList;
52 | import java.util.Arrays;
53 | import java.util.List;
54 | import java.util.Random;
55 | import java.util.Timer;
56 | import java.util.TimerTask;
57 | import java.util.UUID;
58 |
59 | import javax.crypto.Cipher;
60 | import javax.crypto.spec.IvParameterSpec;
61 | import javax.crypto.spec.SecretKeySpec;
62 | import javax.net.ssl.HttpsURLConnection;
63 | import javax.net.ssl.SSLContext;
64 | import javax.net.ssl.TrustManager;
65 | import javax.net.ssl.X509TrustManager;
66 |
67 | public class MainActivity extends AppCompatActivity {
68 | private static final String TAG = "MainActiviry";
69 | SharedPreferences prefs;
70 | EditText textUriAuthentication, textUriHandshake, textUriDevicesList, textUriSecretKeys, textAccountEmail, textAccountPassword;
71 | ListView scanResultList;
72 |
73 | String accessToken;
74 |
75 | String handshakeEndpoint;
76 | String serialNumber;
77 |
78 | boolean scanStarted = false;
79 |
80 | ArrayList scanResults = new ArrayList<>();
81 |
82 | ArrayAdapter resultAdapter;
83 |
84 | BluetoothAdapter adapter;
85 | BluetoothLeScanner scanner;
86 | BluetoothGattCharacteristic authCharacteristic;
87 |
88 | EllipticCurveKeyPair$CppProxy keyPair = EllipticCurveKeyPair$CppProxy.create();
89 | byte[] sharedSecret = new byte[16];
90 | byte[] randomNumbers = new byte[8];
91 |
92 | boolean authenticatedViaServer = false;
93 |
94 | UUID deviceInfoServiceUuid = UUID.fromString("0000180A-0000-1000-8000-00805f9b34fb");
95 | UUID serialNumberUuid = UUID.fromString("00002A25-0000-1000-8000-00805f9b34fb");
96 | UUID authServiceUuid = UUID.fromString("3dda0001-957f-7d4a-34a6-74696673696d");
97 | UUID authCharacteristicUuid = UUID.fromString("3dda0005-957f-7d4a-34a6-74696673696d");
98 |
99 | Timer requestTimeout = new Timer();
100 |
101 | TextView logTextView;
102 |
103 | @Override
104 | protected void onCreate(Bundle savedInstanceState) {
105 | super.onCreate(savedInstanceState);
106 | setContentView(R.layout.activity_main);
107 |
108 | prefs = getPreferences(MODE_PRIVATE);
109 |
110 | initBt();
111 |
112 | initViews();
113 |
114 | disableSSLCertificateChecking();
115 |
116 | autoLogin();
117 |
118 | EllipticCurveKeyPair$CppProxy result = EllipticCurveKeyPair$CppProxy.create();
119 |
120 | Log.d("", "");
121 | }
122 |
123 | private void disableSSLCertificateChecking() {
124 | TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() {
125 | public X509Certificate[] getAcceptedIssuers() {
126 | return null;
127 | }
128 |
129 | @Override
130 | public void checkClientTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
131 | // Not implemented
132 | }
133 |
134 | @Override
135 | public void checkServerTrusted(X509Certificate[] arg0, String arg1) throws CertificateException {
136 | // Not implemented
137 | }
138 | } };
139 |
140 | try {
141 | SSLContext sc = SSLContext.getInstance("TLS");
142 |
143 | sc.init(null, trustAllCerts, new java.security.SecureRandom());
144 |
145 | HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
146 | } catch (KeyManagementException | NoSuchAlgorithmException e) {
147 | e.printStackTrace();
148 | }
149 | }
150 |
151 | private void screenLog(String data){
152 | logTextView.append(data + "\n");
153 | }
154 |
155 | private void initBt() {
156 | BluetoothManager manager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
157 | adapter = manager.getAdapter();
158 | scanner = adapter.getBluetoothLeScanner();
159 | }
160 |
161 | private void autoLogin() {
162 | String authUril = prefs.getString("uri_authentication", null);
163 | if (authUril == null || authUril.isEmpty()) return;
164 | String refreshToken = prefs.getString("refresh_token", null);
165 | if (refreshToken == null || refreshToken.isEmpty()) return;
166 |
167 | new Thread(() -> {
168 | try {
169 | this.performAuth(authUril + "token/refresh", refreshToken);
170 | toast("auth successfull");
171 | screenLog("login success");
172 | runOnUiThread(() -> toggleScan(true));
173 | } catch (IOException | JSONException | RuntimeException | InterruptedException e) {
174 | e.printStackTrace();
175 | toast("cannot auto login");
176 | screenLog("login fail");
177 | prefs.edit().remove("refresh_token").apply();
178 | }
179 | }).start();
180 | }
181 |
182 | private void performAuth(String authUrl, String accountEmail, String accountPassword) throws JSONException, IOException, InterruptedException {
183 | Log.d(TAG, String.format("performAuth: %s %s %s", authUrl, accountEmail, accountPassword));
184 | performAuth(authUrl, new JSONObject()
185 | .put("clientId", "unknown")
186 | .put("email", accountEmail)
187 | .put("password", accountPassword)
188 | );
189 | }
190 |
191 | private void performAuth(String authUrl, String refreshToken) throws JSONException, IOException, InterruptedException {
192 | performAuth(authUrl, new JSONObject()
193 | .put("refreshToken", refreshToken)
194 | );
195 | }
196 |
197 | private void performAuth(String endpoint, JSONObject authObject) throws IOException, JSONException, InterruptedException {
198 | JSONObject resultObject = this.sendPostRequest(endpoint, authObject);
199 | runOnUiThread(() -> {
200 | try {
201 | handleAuthSuccess(resultObject.getString("accessToken"), resultObject.getString("refreshToken"));
202 | } catch (JSONException e) {
203 | e.printStackTrace();
204 | }
205 | });
206 | }
207 |
208 | private JSONObject sendPostRequest(String endpoint, JSONObject postData) throws IOException, JSONException, InterruptedException {
209 | byte[] authData = postData.toString().getBytes();
210 |
211 | URL url = new URL(endpoint);
212 | HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
213 | connection.setConnectTimeout(10000);
214 | connection.setReadTimeout(10000);
215 | connection.setRequestProperty("Content-Type", "application/json");
216 | if (this.accessToken != null && !this.accessToken.isEmpty()) {
217 | connection.setRequestProperty("Authorization", "Bearer " + this.accessToken);
218 | }
219 | connection.setRequestProperty("x-ratelimit-value", "dakhnod@gmail.com");
220 |
221 | connection.setDoOutput(true);
222 | connection.setDoInput(true);
223 | connection.setFixedLengthStreamingMode(authData.length);
224 | connection.getOutputStream().write(authData);
225 | int response = connection.getResponseCode();
226 | if (response != 200) {
227 | throw new RuntimeException("response code is not 200: " + response);
228 | }
229 | InputStream is = connection.getInputStream();
230 |
231 | byte[] data = new byte[2048];
232 |
233 | int readResult = is.read(data);
234 | String result = new String(data);
235 | return new JSONObject(result);
236 | }
237 |
238 | private JSONObject sendGetRequest(String endpoint) throws IOException, JSONException, InterruptedException {
239 | URL url = new URL(endpoint);
240 | HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
241 | connection.setConnectTimeout(10000);
242 | connection.setReadTimeout(10000);
243 | if (this.accessToken != null && !this.accessToken.isEmpty()) {
244 | connection.setRequestProperty("Authorization", "Bearer " + this.accessToken);
245 | }
246 | connection.setDoInput(true);
247 | int response = connection.getResponseCode();
248 | if (response != 200) {
249 | toast("Response code is:"+response+" Perhaps check the URLs for typos");
250 | return null;
251 |
252 | }
253 | InputStream is = connection.getInputStream();
254 |
255 | byte[] data = new byte[2048];
256 |
257 | int readResult = is.read(data);
258 | String result = new String(data);
259 | return new JSONObject(result);
260 | }
261 |
262 | private void stopScan() {
263 | BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
264 | scanner.stopScan(scanCallback);
265 | }
266 |
267 | private void startScan() {
268 | BluetoothLeScanner scanner = adapter.getBluetoothLeScanner();
269 | scanResults.clear();
270 | resultAdapter.notifyDataSetChanged();
271 | List filers = Arrays.asList(
272 | new ScanFilter.Builder()
273 | .setServiceUuid(ParcelUuid.fromString("3dda0001-957f-7d4a-34a6-74696673696d"))
274 | .build()
275 | );
276 | scanner.startScan(
277 | filers,
278 | new ScanSettings.Builder()
279 | .setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH)
280 | .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
281 | .build(),
282 | scanCallback
283 | );
284 | }
285 |
286 | ScanCallback scanCallback = new ScanCallback() {
287 | @Override
288 | public void onScanResult(int callbackType, ScanResult result) {
289 | super.onScanResult(callbackType, result);
290 | Log.d("Main", "found device " + result.toString());
291 | scanResults.add(result.getDevice().getAddress());
292 | runOnUiThread(() -> resultAdapter.notifyDataSetChanged());
293 | }
294 |
295 | @Override
296 | public void onScanFailed(int errorCode) {
297 | super.onScanFailed(errorCode);
298 | toast("LE scan failed");
299 | }
300 | };
301 |
302 | private void startAuthentication(BluetoothGatt gatt, String serialNumber){
303 | try {
304 | authenticatedViaServer = false;
305 | JSONObject requestData = new JSONObject()
306 | .put("serialNumber", serialNumber);
307 | JSONObject response = sendPostRequest(handshakeEndpoint + "generate-pairing-key", requestData);
308 | byte[] randomNumber = Base64.decode(response.getString("randomKey"), 0);
309 |
310 | ByteBuffer dataBuffer = ByteBuffer.allocate(11);
311 | dataBuffer.order(ByteOrder.LITTLE_ENDIAN);
312 |
313 | dataBuffer.put(new byte[]{0x02, 0x01, 0x00});
314 | dataBuffer.put(randomNumber);
315 |
316 | screenLog("sending random to watch...");
317 | authCharacteristic.setValue(dataBuffer.array());
318 | boolean success = gatt.writeCharacteristic(authCharacteristic);
319 | if (!success) {
320 | requestTimeout.cancel();
321 | requestTimeout = null;
322 | screenLog("failed writing random number to watch");
323 | throw new RuntimeException("failed writing random number to watch");
324 | }
325 | } catch (JSONException | InterruptedException | IOException e) {
326 | e.printStackTrace();
327 | toast("failed getting random number from server");
328 | screenLog("failed getting random number from server");
329 | requestTimeout.cancel();
330 | requestTimeout = null;
331 | }
332 | }
333 |
334 | BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
335 | @Override
336 | public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
337 | super.onCharacteristicRead(gatt, characteristic, status);
338 | if (characteristic.getUuid().equals(serialNumberUuid)) {
339 | serialNumber = characteristic.getStringValue(0);
340 | // toast("serial: " + serialNumber);
341 | screenLog("read serial number " + serialNumber + ". Requesting random from server...");
342 | try {
343 | startAuthentication(gatt, serialNumber);
344 | }catch (Exception e){
345 | e.printStackTrace();
346 | screenLog(e.getMessage());
347 | }
348 | // gatt.disconnect();
349 | }
350 | }
351 |
352 | @Override
353 | public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
354 | super.onCharacteristicChanged(gatt, characteristic);
355 | if (characteristic.getUuid().equals(authCharacteristicUuid)) {
356 | ByteBuffer buffer = ByteBuffer.wrap(characteristic.getValue());
357 | buffer.order(ByteOrder.LITTLE_ENDIAN);
358 |
359 | if(requestTimeout != null) {
360 | requestTimeout.cancel();
361 | }
362 | requestTimeout = new Timer();
363 | requestTimeout.schedule(new TimerTask() {
364 | @Override
365 | public void run() {
366 | screenLog("request timeout, restarting...");
367 | startAuthentication(gatt, serialNumber);
368 | }
369 | }, 5000);
370 |
371 | if (buffer.get(1) == 0x01) {
372 | byte[] encryptedNumbers = new byte[16];
373 | buffer.position(4);
374 | buffer.get(encryptedNumbers);
375 | if(buffer.get(3) == 0x00) { // server challenge
376 | try {
377 | JSONObject responseData = sendPostRequest(
378 | handshakeEndpoint + "swap-pairing-keys",
379 | new JSONObject()
380 | .put("serialNumber", serialNumber)
381 | .put("encryptedData", Base64.encodeToString(encryptedNumbers, Base64.NO_WRAP))
382 | );
383 |
384 | screenLog("exchanging data with server...");
385 | byte[] responseEncryptedNumbers = Base64.decode(responseData.getString("encryptedData"), 0);
386 | ByteBuffer responseBuffer = ByteBuffer.allocate(19);
387 | responseBuffer.order(ByteOrder.LITTLE_ENDIAN);
388 | responseBuffer.put(new byte[]{0x02, 0x02, 0x00});
389 | responseBuffer.put(responseEncryptedNumbers);
390 |
391 | screenLog("sending server challenge response to watch...");
392 | authCharacteristic.setValue(responseBuffer.array());
393 | gatt.writeCharacteristic(authCharacteristic);
394 | } catch (IOException | JSONException | InterruptedException e) {
395 | e.printStackTrace();
396 | toast("failed sending encrypted numbers to server");
397 | requestTimeout.cancel();
398 | requestTimeout = null;
399 | }
400 | }else{ // local challenge
401 | try{
402 | byte[] encrypted = new byte[16];
403 |
404 | buffer.position(4);
405 | buffer.get(encrypted);
406 |
407 | SecretKeySpec keySpec = new SecretKeySpec(sharedSecret, "AES");
408 | Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
409 | cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
410 | byte[] decrypted = cipher.doFinal(encrypted);
411 |
412 | screenLog("re-encrypting challenge from watch...");
413 |
414 | byte[] swapped = new byte[16];
415 | System.arraycopy(decrypted, 8, swapped, 0, 8);
416 | System.arraycopy(decrypted, 0, swapped, 8, 8);
417 |
418 | cipher = Cipher.getInstance("AES/CBC/NoPadding");
419 | cipher.init(Cipher.ENCRYPT_MODE, keySpec, new IvParameterSpec(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}));
420 | byte[] encryptedSwapped = cipher.doFinal(swapped);
421 |
422 | authCharacteristic.setValue(
423 | ByteBuffer
424 | .allocate(19)
425 | .put(new byte[]{0x02, 0x02, 0x01})
426 | .put(encryptedSwapped)
427 | .array()
428 | );
429 | gatt.writeCharacteristic(authCharacteristic);
430 | }catch (Exception e){
431 | e.printStackTrace();
432 | toast("error with encryption");
433 | }
434 | }
435 | } else if (buffer.get(1) == 0x02) {
436 | if (buffer.get(2) != 0x00) {
437 | toast(authenticatedViaServer ? "local random number challenge failed" : "server random number challenge failed");
438 | screenLog(authenticatedViaServer ? "local random number challenge failed" : "server random number challenge failed");
439 | requestTimeout.cancel();
440 | requestTimeout = null;
441 | gatt.disconnect();
442 | return;
443 | }
444 | screenLog("watch challenge success.");
445 | if (authenticatedViaServer) {
446 | String secretHex = "0x" + bytesToHex(MainActivity.this.sharedSecret);
447 |
448 | screenLog(String.format("got key: %s", secretHex));
449 |
450 | ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
451 | clipboardManager.setPrimaryClip(ClipData.newPlainText("fossil key", secretHex));
452 |
453 | toast("secret copied to clipboard");
454 | screenLog("secret copied to clipboard.");
455 |
456 | requestTimeout.cancel();
457 | requestTimeout = null;
458 | gatt.disconnect();
459 | } else {
460 | byte[] publicKey = MainActivity.this.keyPair.getPublicKey();
461 |
462 | ByteBuffer publicKeyBuffer = ByteBuffer.allocate(34);
463 | publicKeyBuffer.put(new byte[]{0x02, 0x03});
464 | publicKeyBuffer.put(publicKey);
465 |
466 | screenLog("sending public key to watch...");
467 |
468 | authCharacteristic.setValue(publicKeyBuffer.array());
469 | gatt.writeCharacteristic(authCharacteristic);
470 | authenticatedViaServer = true;
471 | }
472 | } else if (buffer.get(1) == 0x03) {
473 | byte[] watchPublicKey = new byte[32];
474 | buffer.position(3);
475 | buffer.get(watchPublicKey);
476 |
477 | screenLog("revived public key from watch, calculating shared secret...");
478 |
479 | byte[] sharedSecret = MainActivity.this.keyPair.calculateSecretKey(watchPublicKey);
480 | System.arraycopy(sharedSecret, 0, MainActivity.this.sharedSecret, 0, 16);
481 |
482 | screenLog("sending local challenge random number...");
483 |
484 | new Random().nextBytes(MainActivity.this.randomNumbers);
485 | authCharacteristic.setValue(
486 | ByteBuffer
487 | .allocate(11)
488 | .put(new byte[]{0x02, 0x01, 0x01})
489 | .put(randomNumbers)
490 | .array()
491 | );
492 | Log.d("bt", "written random number");
493 | gatt.writeCharacteristic(authCharacteristic);
494 | }
495 |
496 | }
497 | }
498 |
499 |
500 | @Override
501 | public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
502 | super.onDescriptorWrite(gatt, descriptor, status);
503 | Log.d("bt", "descriptor written");
504 | screenLog("subscribed, reading serial...");
505 | BluetoothGattService infoService = gatt.getService(deviceInfoServiceUuid);
506 | BluetoothGattCharacteristic serialCharacteristic = infoService.getCharacteristic(serialNumberUuid);
507 | gatt.readCharacteristic(serialCharacteristic);
508 |
509 | }
510 |
511 | @Override
512 | public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
513 | super.onCharacteristicWrite(gatt, characteristic, status);
514 | Log.d("bt", "characteristic written");
515 | // toast("characteristic written");
516 | }
517 |
518 | @Override
519 | public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
520 | super.onConnectionStateChange(gatt, status, newState);
521 | if (newState == BluetoothGatt.STATE_CONNECTED) {
522 | // toast("device connected");
523 | screenLog("connected to device, discovering services...");
524 | Log.d("bt", "device connected");
525 | gatt.discoverServices();
526 | }
527 | }
528 |
529 | @Override
530 | public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
531 | super.onMtuChanged(gatt, mtu, status);
532 | screenLog("MTU changed, enabling notification...");
533 |
534 | BluetoothGattService authService = gatt.getService(authServiceUuid);
535 | authCharacteristic = authService.getCharacteristic(authCharacteristicUuid);
536 | gatt.setCharacteristicNotification(authCharacteristic, true);
537 | BluetoothGattDescriptor notification = authCharacteristic.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
538 | notification.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
539 | gatt.writeDescriptor(notification);
540 | }
541 |
542 | @Override
543 | public void onServicesDiscovered(BluetoothGatt gatt, int status) {
544 | super.onServicesDiscovered(gatt, status);
545 | screenLog("discovered services, requesting MTU...");
546 | gatt.requestMtu(512);
547 | }
548 | };
549 |
550 | private void connectDevice(String address) {
551 | screenLog("connecting to device " + address);
552 | authenticatedViaServer = false;
553 | BluetoothDevice device = adapter.getRemoteDevice(address);
554 | BluetoothGatt gatt = device.connectGatt(this, false, gattCallback);
555 | }
556 |
557 | private void handleAuthSuccess(String accessToken, String refreshToken) {
558 | Log.d("", "auth success");
559 | this.accessToken = accessToken;
560 | prefs.edit().putString("refresh_token", refreshToken).apply();
561 | findViewById(R.id.button_scan).setVisibility(View.VISIBLE);
562 | findViewById(R.id.button_fetch_key).setVisibility(View.VISIBLE);
563 | }
564 |
565 | private void handleAuthenticate(View v) {
566 | String authenticationUri = textUriAuthentication.getText().toString();
567 | handshakeEndpoint = textUriHandshake.getText().toString();
568 | String accountEmail = textAccountEmail.getText().toString();
569 | String accountPassword = textAccountPassword.getText().toString();
570 |
571 | prefs.edit()
572 | .putString("uri_authentication", authenticationUri)
573 | .putString("uri_handshake", handshakeEndpoint)
574 | .putString("account_email", accountEmail)
575 | .putString("account_password", accountPassword)
576 | .apply();
577 |
578 | new Thread(() -> {
579 | try {
580 | this.performAuth(authenticationUri + "login", accountEmail, accountPassword);
581 | toast("auth successfull");
582 | } catch (IOException | JSONException | RuntimeException | InterruptedException e) {
583 | e.printStackTrace();
584 | toast("auth error: " + e.getMessage());
585 | }
586 | }).start();
587 | }
588 |
589 | private void toast(String data) {
590 | runOnUiThread(() -> Toast.makeText(MainActivity.this, data, Toast.LENGTH_SHORT).show());
591 | }
592 |
593 | @Override
594 | protected void onPause() {
595 | super.onPause();
596 | Log.d("Main", "finishing");
597 | finish();
598 | System.exit(0);
599 | }
600 |
601 | private void toggleScan() {
602 | toggleScan(!scanStarted);
603 | }
604 |
605 | private void toggleScan(boolean start) {
606 | if (start == scanStarted) return;
607 | if (scanStarted) {
608 | stopScan();
609 | ((Button) findViewById(R.id.button_scan)).setText("start scan");
610 | } else {
611 | startScan();
612 | ((Button) findViewById(R.id.button_scan)).setText("stop scan");
613 | }
614 | scanStarted = start;
615 | }
616 |
617 | private void handleFetchKeys(View view){
618 | String deviceListUri = textUriDevicesList.getText().toString();
619 | String secretKeysUri = textUriSecretKeys.getText().toString();
620 |
621 | prefs.edit()
622 | .putString("uri_device_list", deviceListUri)
623 | .putString("uri_secret_keys", secretKeysUri)
624 | .apply();
625 |
626 | if(deviceListUri.isEmpty()){
627 | toast("Please enter URI for device list");
628 | return;
629 | }
630 | new Thread(new Runnable() {
631 | @Override
632 | public void run() {
633 | try {
634 | JSONObject result = sendGetRequest(deviceListUri);
635 | if(result == null){
636 | return;
637 | }
638 | JSONArray devices = result.getJSONArray("_items");
639 | screenLog("got device list");
640 | if(devices.length() < 1){
641 | toast("no devices configured with account");
642 | screenLog("no devices found");
643 | return;
644 | }
645 | JSONObject firstDevice = devices.getJSONObject(0);
646 | String serial = firstDevice.getString("id");
647 | screenLog(String.format("device serial: %s", serial));
648 |
649 | String secretKeysUriFilled = String.format(secretKeysUri, serial);
650 | JSONObject secretKeyResult = sendGetRequest(secretKeysUriFilled);
651 | String secretKey = secretKeyResult.getString("secretKey");
652 | byte[] fullKey = Base64.decode(secretKey, Base64.DEFAULT);
653 | System.arraycopy(fullKey, 0, MainActivity.this.sharedSecret, 0, 16);
654 | Log.d(TAG, "run: " + secretKey);
655 |
656 | String secretHex = "0x" + bytesToHex(MainActivity.this.sharedSecret);
657 |
658 | screenLog(String.format("got key: %s", secretHex));
659 |
660 | ClipboardManager clipboardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
661 | clipboardManager.setPrimaryClip(ClipData.newPlainText("fossil key", secretHex));
662 |
663 | toast("secret copied to clipboard");
664 | screenLog("secret copied to clipboard.");
665 | } catch (IOException | JSONException | InterruptedException e) {
666 | e.printStackTrace();
667 | toast("Error sending GET request: " + e.getMessage());
668 | }
669 | }
670 | }).start();
671 | }
672 |
673 | private void initViews() {
674 | textUriAuthentication = findViewById(R.id.uri_authentication);
675 | textUriHandshake = findViewById(R.id.uri_handshake);
676 | textAccountEmail = findViewById(R.id.account_email);
677 | textAccountPassword = findViewById(R.id.account_password);
678 | textUriDevicesList = findViewById(R.id.uri_devices_list);
679 | textUriSecretKeys = findViewById(R.id.uri_secret_keys);
680 | scanResultList = findViewById(R.id.scan_results);
681 | logTextView = findViewById(R.id.log_text);
682 | logTextView.setMovementMethod(new ScrollingMovementMethod());
683 |
684 |
685 |
686 | resultAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, scanResults);
687 | scanResultList.setAdapter(resultAdapter);
688 |
689 | scanResultList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
690 | @Override
691 | public void onItemClick(AdapterView> parent, View view, int position, long id) {
692 | toggleScan(false);
693 | logTextView.setText("");
694 | new Thread(() -> connectDevice(scanResults.get(position))).start();
695 | }
696 | });
697 |
698 | handshakeEndpoint = prefs.getString("uri_handshake", "");
699 |
700 | textUriAuthentication.setText(prefs.getString("uri_authentication", ""));
701 | textUriDevicesList.setText(prefs.getString("uri_device_list", ""));
702 | textUriSecretKeys.setText(prefs.getString("uri_secret_keys", ""));
703 | textUriAuthentication.setText(prefs.getString("uri_authentication", ""));
704 | textUriHandshake.setText(handshakeEndpoint);
705 | textAccountEmail.setText(prefs.getString("account_email", ""));
706 | textAccountPassword.setText(prefs.getString("account_password", ""));
707 |
708 | findViewById(R.id.button_fetch_key).setOnClickListener(this::handleFetchKeys);
709 | findViewById(R.id.button_authenticate).setOnClickListener(this::handleAuthenticate);
710 | findViewById(R.id.button_scan).setOnClickListener((v) -> {
711 | toggleScan();
712 | });
713 | }
714 |
715 | private String bytesToHex(byte[] bytes) {
716 | final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
717 | String hex = "";
718 | for (int j = 0; j < bytes.length; j++) {
719 | int v = bytes[j] & 0xFF;
720 | hex += HEX_ARRAY[v >>> 4];
721 | hex += HEX_ARRAY[v & 0x0F];
722 | }
723 | return hex;
724 | }
725 | }
--------------------------------------------------------------------------------