-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
329 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
194 changes: 194 additions & 0 deletions
194
shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/backup/OneDriveService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
package ml.dev.kotlin.openotp.backup | ||
|
||
import io.ktor.client.* | ||
import io.ktor.client.request.* | ||
import io.ktor.client.request.forms.* | ||
import io.ktor.client.statement.* | ||
import io.ktor.http.* | ||
import io.ktor.http.content.* | ||
import io.ktor.util.* | ||
import kotlinx.datetime.Clock | ||
import kotlinx.datetime.Instant | ||
import kotlinx.serialization.SerialName | ||
import kotlinx.serialization.Serializable | ||
import ml.dev.kotlin.openotp.component.UserLinkedAccountsModel | ||
import ml.dev.kotlin.openotp.util.createJsonHttpClient | ||
import ml.dev.kotlin.openotp.util.randomBytesChallenge | ||
import ml.dev.kotlin.openotp.util.safeHttpRequest | ||
import ml.dev.kotlin.openotp.util.safeRequest | ||
import org.kotlincrypto.hash.sha2.SHA256 | ||
import kotlin.time.Duration.Companion.seconds | ||
|
||
sealed class OneDriveService : OAuth2AccountService { | ||
|
||
protected val client: HttpClient by lazy(::createJsonHttpClient) | ||
|
||
data object Initialized : OneDriveService(), OAuth2AccountService.Initialized { | ||
override fun requestPermissions(): RequestedPermissions? { | ||
val bytes = randomBytesChallenge(count = 32) ?: return null | ||
val codeVerifier = bytes.oneDriveEncodeBase64() | ||
val codeChallenge = SHA256().digest(codeVerifier.encodeToByteArray()).oneDriveEncodeBase64() | ||
val accessData = OneDriveAccessData(codeVerifier, codeChallenge) | ||
return RequestedPermissions(accessData) | ||
} | ||
} | ||
|
||
class RequestedPermissions( | ||
private val accessData: OneDriveAccessData, | ||
) : OneDriveService(), OAuth2AccountService.RequestedPermissions { | ||
|
||
override fun generateVerifyUri(): String = | ||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize" + | ||
"?client_id=$CLIENT_ID" + | ||
"&response_type=code" + | ||
"&redirect_uri=https%3A%2F%2Fopen-otp.procyk.in" + | ||
"&response_mode=fragment" + | ||
"&scope=offline_access+files.readwrite.all" + | ||
"&code_challenge=${accessData.codeChallenge}" + | ||
"&code_challenge_method=S256" | ||
|
||
override suspend fun authenticateUser(userCode: String): Result<Authenticated> = | ||
client.safeRequest<OneDriveOAuth2RefreshableTokenResponse> { | ||
method = HttpMethod.Post | ||
url("https://login.microsoftonline.com/common/oauth2/v2.0/token") | ||
setBody(FormDataContent(parameters { | ||
append("code", userCode) | ||
append("grant_type", "authorization_code") | ||
append("code_verifier", accessData.codeVerifier) | ||
append("client_id", CLIENT_ID) | ||
append("redirect_uri", "https://open-otp.procyk.in") | ||
append("scope", "files.readwrite.all") | ||
})) | ||
}.map { response -> | ||
val refreshableAccessData = response.toOneDriveRefreshableAccessData() | ||
Authenticated(refreshableAccessData) | ||
} | ||
|
||
} | ||
|
||
class Authenticated( | ||
private val refreshableAccessData: OneDriveRefreshableAccessData, | ||
) : OneDriveService(), OAuth2AccountService.Authenticated { | ||
|
||
override val isExpired: Boolean | ||
get() = Clock.System.now() >= refreshableAccessData.expiresAt | ||
|
||
override suspend fun refreshUserAccessToken(): Result<Authenticated> = | ||
client.safeRequest<OneDriveOAuth2TokenResponse> { | ||
method = HttpMethod.Post | ||
url("https://login.microsoftonline.com/common/oauth2/v2.0/token") | ||
setBody(FormDataContent(parameters { | ||
append("grant_type", "refresh_token") | ||
append("refresh_token", refreshableAccessData.refreshToken) | ||
append("client_id", CLIENT_ID) | ||
append("scope", "files.readwrite.all") | ||
})) | ||
}.map { response -> | ||
val refreshableAccessData = response.toOneDriveRefreshableAccessData(refreshableAccessData.refreshToken) | ||
Authenticated(refreshableAccessData) | ||
} | ||
|
||
override suspend fun uploadBackupData(data: ByteArray): Result<Boolean> = | ||
client.safeRequest<OneDriveUploadResponse> { | ||
method = HttpMethod.Put | ||
url("https://graph.microsoft.com/v1.0/me/drive/root:/$BACKUP_PATH:/content") | ||
header(HttpHeaders.ContentType, ContentType.Application.OctetStream) | ||
header(HttpHeaders.Authorization, "Bearer ${refreshableAccessData.accessToken}") | ||
setBody(ByteArrayContent(data)) | ||
}.map { response -> | ||
response.file.hashes.sha256Hash == data.oneDriveContentHash() | ||
} | ||
|
||
override suspend fun downloadBackupData(): ByteArray? = | ||
client.safeHttpRequest { | ||
method = HttpMethod.Get | ||
url("https://graph.microsoft.com/v1.0/me/drive/root:/$BACKUP_PATH:/content") | ||
header(HttpHeaders.Authorization, "Bearer ${refreshableAccessData.accessToken}") | ||
}.map { response -> | ||
val downloadLocation = response.headers[HttpHeaders.ContentLocation] ?: return@map null | ||
client.safeHttpRequest { | ||
method = HttpMethod.Get | ||
url(downloadLocation) | ||
} | ||
.map { it.readBytes() } | ||
.getOrNull() | ||
}.getOrNull() | ||
|
||
override fun updateUserLinkedAccounts(linkedAccounts: UserLinkedAccountsModel): UserLinkedAccountsModel = | ||
linkedAccounts.copy(onedrive = refreshableAccessData) | ||
} | ||
} | ||
|
||
data class OneDriveAccessData( | ||
val codeVerifier: String, | ||
val codeChallenge: String, | ||
) | ||
|
||
@Serializable | ||
data class OneDriveRefreshableAccessData( | ||
val expiresAt: Instant, | ||
val accessToken: String, | ||
val refreshToken: String, | ||
) | ||
|
||
@Serializable | ||
private data class OneDriveOAuth2RefreshableTokenResponse( | ||
@SerialName("access_token") val accessToken: String, | ||
@SerialName("expires_in") val expiresInSeconds: Int, | ||
@SerialName("token_type") val tokenType: TokenType, | ||
@SerialName("refresh_token") val refreshToken: String, | ||
) { | ||
@Serializable | ||
enum class TokenType { Bearer } | ||
} | ||
|
||
@Serializable | ||
private data class OneDriveOAuth2TokenResponse( | ||
@SerialName("access_token") val accessToken: String, | ||
@SerialName("expires_in") val expiresInSeconds: Int, | ||
@SerialName("token_type") val tokenType: TokenType, | ||
) { | ||
@Serializable | ||
enum class TokenType { Bearer } | ||
} | ||
|
||
@Serializable | ||
private data class OneDriveUploadResponse( | ||
@SerialName("file") val file: File, | ||
) { | ||
@Serializable | ||
data class File( | ||
@SerialName("hashes") val hashes: Hashes, | ||
) { | ||
@Serializable | ||
data class Hashes( | ||
@SerialName("sha256Hash") val sha256Hash: String, | ||
) | ||
} | ||
} | ||
|
||
private fun OneDriveOAuth2RefreshableTokenResponse.toOneDriveRefreshableAccessData(): OneDriveRefreshableAccessData = | ||
OneDriveRefreshableAccessData( | ||
expiresAt = Clock.System.now() + expiresInSeconds.seconds / 2, | ||
accessToken = accessToken, | ||
refreshToken = refreshToken, | ||
) | ||
|
||
private fun OneDriveOAuth2TokenResponse.toOneDriveRefreshableAccessData(refreshToken: String): OneDriveRefreshableAccessData = | ||
OneDriveRefreshableAccessData( | ||
expiresAt = Clock.System.now() + expiresInSeconds.seconds / 2, | ||
accessToken = accessToken, | ||
refreshToken = refreshToken, | ||
) | ||
|
||
private const val CLIENT_ID: String = "8612f175-11d3-4dea-960a-d2cb89867a33" | ||
|
||
private const val BACKUP_PATH: String = "OpenOTP/OpenOTP.backup" | ||
|
||
private fun ByteArray.oneDriveEncodeBase64(): String = encodeBase64() | ||
.replace('+', '-') | ||
.replace('/', '_') | ||
.replace("=", "") | ||
|
||
private fun ByteArray.oneDriveContentHash(): String = | ||
SHA256().digest(this).toHexString(format = HexFormat.UpperCase) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
shared/src/commonMain/kotlin/ml/dev/kotlin/openotp/ui/icons/OneDrive.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package ml.dev.kotlin.openotp.ui.icons | ||
|
||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero | ||
import androidx.compose.ui.graphics.SolidColor | ||
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt | ||
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter | ||
import androidx.compose.ui.graphics.vector.ImageVector | ||
import androidx.compose.ui.graphics.vector.ImageVector.Builder | ||
import androidx.compose.ui.graphics.vector.path | ||
import androidx.compose.ui.unit.dp | ||
import ml.dev.kotlin.openotp.ui.OtpIcons | ||
|
||
val OtpIcons.OneDrive: ImageVector | ||
get() { | ||
if (_onedrive != null) { | ||
return _onedrive!! | ||
} | ||
_onedrive = Builder( | ||
name = "OneDrive", defaultWidth = 24.0.dp, | ||
defaultHeight = 24.0.dp, viewportWidth = 24.0f, viewportHeight = 24.0f | ||
).apply { | ||
path( | ||
fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f, | ||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f, | ||
pathFillType = NonZero | ||
) { | ||
moveTo(19.453f, 9.95f) | ||
quadToRelative(0.961f, 0.058f, 1.787f, 0.468f) | ||
quadToRelative(0.826f, 0.41f, 1.442f, 1.066f) | ||
quadToRelative(0.615f, 0.657f, 0.966f, 1.512f) | ||
quadToRelative(0.352f, 0.856f, 0.352f, 1.816f) | ||
quadToRelative(0.0f, 1.008f, -0.387f, 1.893f) | ||
quadToRelative(-0.386f, 0.885f, -1.049f, 1.547f) | ||
quadToRelative(-0.662f, 0.662f, -1.546f, 1.049f) | ||
quadToRelative(-0.885f, 0.387f, -1.893f, 0.387f) | ||
lineTo(6.0f, 19.688f) | ||
quadToRelative(-1.242f, 0.0f, -2.332f, -0.475f) | ||
quadToRelative(-1.09f, -0.475f, -1.904f, -1.29f) | ||
quadToRelative(-0.815f, -0.814f, -1.29f, -1.903f) | ||
quadTo(0.0f, 14.93f, 0.0f, 13.688f) | ||
quadToRelative(0.0f, -0.985f, 0.31f, -1.887f) | ||
quadToRelative(0.311f, -0.903f, 0.862f, -1.658f) | ||
quadToRelative(0.55f, -0.756f, 1.324f, -1.325f) | ||
quadToRelative(0.774f, -0.568f, 1.711f, -0.861f) | ||
quadToRelative(0.434f, -0.129f, 0.85f, -0.187f) | ||
quadToRelative(0.416f, -0.06f, 0.861f, -0.082f) | ||
horizontalLineToRelative(0.012f) | ||
quadToRelative(0.515f, -0.786f, 1.207f, -1.413f) | ||
quadToRelative(0.691f, -0.627f, 1.5f, -1.066f) | ||
quadToRelative(0.808f, -0.44f, 1.705f, -0.668f) | ||
quadToRelative(0.896f, -0.229f, 1.845f, -0.229f) | ||
quadToRelative(1.278f, 0.0f, 2.456f, 0.417f) | ||
quadToRelative(1.177f, 0.416f, 2.144f, 1.16f) | ||
quadToRelative(0.967f, 0.744f, 1.658f, 1.78f) | ||
quadToRelative(0.692f, 1.038f, 1.008f, 2.28f) | ||
close() | ||
moveTo(12.188f, 5.813f) | ||
quadToRelative(-1.325f, 0.0f, -2.52f, 0.544f) | ||
quadToRelative(-1.195f, 0.545f, -2.04f, 1.565f) | ||
quadToRelative(0.446f, 0.117f, 0.85f, 0.299f) | ||
quadToRelative(0.405f, 0.181f, 0.792f, 0.416f) | ||
lineToRelative(4.78f, 2.86f) | ||
lineToRelative(2.731f, -1.15f) | ||
quadToRelative(0.27f, -0.117f, 0.545f, -0.204f) | ||
quadToRelative(0.276f, -0.088f, 0.58f, -0.147f) | ||
quadToRelative(-0.293f, -0.937f, -0.855f, -1.705f) | ||
quadToRelative(-0.563f, -0.768f, -1.319f, -1.318f) | ||
quadToRelative(-0.755f, -0.551f, -1.658f, -0.856f) | ||
quadToRelative(-0.902f, -0.304f, -1.886f, -0.304f) | ||
close() | ||
moveTo(2.414f, 16.395f) | ||
lineToRelative(9.914f, -4.184f) | ||
lineToRelative(-3.832f, -2.297f) | ||
quadToRelative(-0.586f, -0.351f, -1.23f, -0.539f) | ||
quadToRelative(-0.645f, -0.188f, -1.325f, -0.188f) | ||
quadToRelative(-0.914f, 0.0f, -1.722f, 0.364f) | ||
quadToRelative(-0.809f, 0.363f, -1.412f, 0.978f) | ||
quadToRelative(-0.604f, 0.616f, -0.955f, 1.436f) | ||
quadToRelative(-0.352f, 0.82f, -0.352f, 1.723f) | ||
quadToRelative(0.0f, 0.703f, 0.234f, 1.423f) | ||
quadToRelative(0.235f, 0.721f, 0.68f, 1.284f) | ||
close() | ||
moveTo(19.125f, 18.188f) | ||
quadToRelative(0.563f, 0.0f, 1.078f, -0.176f) | ||
quadToRelative(0.516f, -0.176f, 0.961f, -0.516f) | ||
lineToRelative(-7.23f, -4.324f) | ||
lineToRelative(-10.301f, 4.336f) | ||
quadToRelative(0.527f, 0.328f, 1.13f, 0.504f) | ||
quadToRelative(0.604f, 0.175f, 1.237f, 0.175f) | ||
close() | ||
moveTo(22.137f, 16.336f) | ||
quadToRelative(0.363f, -0.727f, 0.363f, -1.523f) | ||
quadToRelative(0.0f, -0.774f, -0.293f, -1.407f) | ||
reflectiveQuadToRelative(-0.791f, -1.072f) | ||
quadToRelative(-0.498f, -0.44f, -1.166f, -0.68f) | ||
quadToRelative(-0.668f, -0.24f, -1.406f, -0.24f) | ||
quadToRelative(-0.422f, 0.0f, -0.838f, 0.1f) | ||
reflectiveQuadToRelative(-0.815f, 0.252f) | ||
quadToRelative(-0.398f, 0.152f, -0.785f, 0.334f) | ||
quadToRelative(-0.386f, 0.181f, -0.761f, 0.345f) | ||
close() | ||
} | ||
} | ||
.build() | ||
return _onedrive!! | ||
} | ||
|
||
private var _onedrive: ImageVector? = null |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters