Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit 02d07e9

Browse files
committed
wip(crypto-pgpainless): add PGPKeyPair and PGPKeyManager
Signed-off-by: Aditya Wasan <adityawasan55@gmail.com>
1 parent 9a1d95c commit 02d07e9

File tree

6 files changed

+204
-0
lines changed

6 files changed

+204
-0
lines changed

crypto-pgpainless/build.gradle.kts

+3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ plugins {
1010

1111
dependencies {
1212
api(projects.cryptoCommon)
13+
implementation(libs.androidx.annotation)
1314
implementation(libs.dagger.hilt.core)
1415
implementation(libs.kotlin.coroutines.core)
1516
implementation(libs.thirdparty.kotlinResult)
1617
implementation(libs.thirdparty.pgpainless)
18+
testImplementation(libs.bundles.testDependencies)
19+
testImplementation(libs.kotlin.coroutines.test)
1720
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
*/
5+
6+
package dev.msfjarvis.aps.crypto
7+
8+
import androidx.annotation.VisibleForTesting
9+
import com.github.michaelbull.result.Result
10+
import com.github.michaelbull.result.runCatching
11+
import java.io.File
12+
import kotlinx.coroutines.CoroutineDispatcher
13+
import kotlinx.coroutines.withContext
14+
import org.pgpainless.PGPainless
15+
16+
public class PGPKeyManager(filesDir: String, private val dispatcher: CoroutineDispatcher) : KeyManager<PGPKeyPair> {
17+
18+
private val keyDir = File(filesDir, KEY_DIR_NAME)
19+
20+
override suspend fun addKey(key: PGPKeyPair, replace: Boolean): Result<PGPKeyPair, Throwable> =
21+
withContext(dispatcher) {
22+
runCatching {
23+
if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
24+
val keyFile = File(keyDir, "${key.getKeyId()}.$KEY_EXTENSION")
25+
if (keyFile.exists()) {
26+
// Check for replace flag first and if it is false, throw an error
27+
if (!replace) throw KeyManagerException.KeyAlreadyExistsException(key.getKeyId())
28+
if (!keyFile.delete()) throw KeyManagerException.KeyDeletionFailedException
29+
}
30+
31+
keyFile.writeBytes(key.getPrivateKey())
32+
33+
key
34+
}
35+
}
36+
37+
override suspend fun removeKey(key: PGPKeyPair): Result<PGPKeyPair, Throwable> =
38+
withContext(dispatcher) {
39+
runCatching {
40+
if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
41+
val keyFile = File(keyDir, "${key.getKeyId()}.$KEY_EXTENSION")
42+
if (keyFile.exists()) {
43+
if (!keyFile.delete()) throw KeyManagerException.KeyDeletionFailedException
44+
}
45+
46+
key
47+
}
48+
}
49+
50+
override suspend fun getKeyById(id: String): Result<PGPKeyPair, Throwable> =
51+
withContext(dispatcher) {
52+
runCatching {
53+
if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
54+
val keys = keyDir.listFiles()
55+
if (keys.isNullOrEmpty()) throw KeyManagerException.NoKeysAvailableException
56+
57+
for (keyFile in keys) {
58+
val secretKeyRing = PGPainless.readKeyRing().secretKeyRing(keyFile.inputStream())
59+
val secretKey = secretKeyRing.secretKey
60+
val keyPair = PGPKeyPair(secretKey)
61+
if (keyPair.getKeyId() == id) return@runCatching keyPair
62+
}
63+
64+
throw KeyManagerException.KeyNotFoundException(id)
65+
}
66+
}
67+
68+
override suspend fun getAllKeys(): Result<List<PGPKeyPair>, Throwable> =
69+
withContext(dispatcher) {
70+
runCatching {
71+
if (!keyDirExists()) throw KeyManagerException.KeyDirectoryUnavailableException
72+
val keys = keyDir.listFiles()
73+
if (keys.isNullOrEmpty()) return@runCatching listOf()
74+
75+
keys.map { keyFile ->
76+
val secretKeyRing = PGPainless.readKeyRing().secretKeyRing(keyFile.inputStream())
77+
val secretKey = secretKeyRing.secretKey
78+
79+
PGPKeyPair(secretKey)
80+
}.toList()
81+
}
82+
}
83+
84+
override fun canHandle(fileName: String): Boolean {
85+
// TODO: This is a temp hack for now and in future it should check that the GPGKeyManager can
86+
// decrypt the file
87+
return fileName.endsWith(KEY_EXTENSION)
88+
}
89+
90+
private fun keyDirExists(): Boolean {
91+
return keyDir.exists() || keyDir.mkdirs()
92+
}
93+
94+
internal companion object {
95+
96+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
97+
internal const val KEY_DIR_NAME: String = "keys"
98+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
99+
internal const val KEY_EXTENSION: String = "key"
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
*/
5+
6+
package dev.msfjarvis.aps.crypto
7+
8+
import org.bouncycastle.openpgp.PGPSecretKey
9+
10+
public class PGPKeyPair(private val secretKey: PGPSecretKey): KeyPair {
11+
12+
init { if (secretKey.isPrivateKeyEmpty) throw KeyPairException.PrivateKeyUnavailableException }
13+
14+
override fun getPrivateKey(): ByteArray {
15+
return secretKey.encoded
16+
}
17+
override fun getPublicKey(): ByteArray {
18+
return secretKey.publicKey.encoded
19+
}
20+
override fun getKeyId(): String {
21+
return secretKey.keyID.toString()
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
*/
5+
6+
package dev.msfjarvis.aps.crypto.utils
7+
8+
internal object CryptoConstants {
9+
internal const val KEY_PASSPHRASE = "hunter2"
10+
internal const val PLAIN_TEXT = "encryption worthy content"
11+
internal const val KEY_NAME = "John Doe"
12+
internal const val KEY_EMAIL = "john.doe@example.com"
13+
internal const val KEY_ID = "04ace699d5b15b7e"
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
3+
* SPDX-License-Identifier: GPL-3.0-only
4+
*/
5+
6+
package dev.msfjarvis.aps.crypto
7+
8+
import dev.msfjarvis.aps.crypto.utils.CryptoConstants
9+
import java.io.File
10+
import kotlin.test.assertEquals
11+
import kotlin.test.assertFailsWith
12+
import org.junit.Test
13+
import org.pgpainless.PGPainless
14+
15+
public class GPGKeyPairTest {
16+
17+
@Test
18+
public fun testIfKeyIdIsCorrect() {
19+
val secretKey = PGPainless.readKeyRing().secretKeyRing(getKey()).secretKey
20+
val keyPair = PGPKeyPair(secretKey)
21+
22+
assertEquals(CryptoConstants.KEY_ID, keyPair.getKeyId())
23+
}
24+
25+
@Test
26+
public fun testBuildingKeyPairWithoutPrivateKey() {
27+
assertFailsWith<KeyPairException.PrivateKeyUnavailableException> {
28+
// Get public key object from private key
29+
val publicKey = PGPainless.readKeyRing().secretKeyRing(getKey()).publicKey
30+
31+
// Create secret key ring from public key
32+
val secretKeyRing = PGPainless.readKeyRing().secretKeyRing(publicKey.encoded)
33+
34+
// Get secret key from key ring
35+
val publicSecretKey = secretKeyRing.secretKey
36+
37+
// Try creating a KeyPair from public key
38+
val keyPair = PGPKeyPair(publicSecretKey)
39+
40+
keyPair.getPrivateKey()
41+
}
42+
}
43+
44+
private fun getKey(): String = this::class.java.classLoader.getResource("src/test/private_key").readText()
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-----BEGIN PGP PRIVATE KEY BLOCK-----
2+
Version: GopenPGP 2.1.9
3+
Comment: https://gopenpgp.org
4+
5+
xYYEYN+EThYJKwYBBAHaRw8BAQdAh0d9GdVyJV6KbFynPz3sHkdi5RDnKYs+l0x0
6+
rEOEthX+CQMIfg7BTvTTe7pgvNERA1vLXRjSxXyi7tfSV13JRnrapp7YtNUSHLVS
7+
PqbaLBd6+EXx7dJ9mUSUSWVga5mdtLZ/k6e+6dsygeHiJuwxfGbHnc0fSm9obiBE
8+
b2UgPGpvaG4uZG9lQGV4YW1wbGUuY29tPsKIBBMWCAA6BQJg34ROCRAErOaZ1bFb
9+
fhYhBJQ0DPsSHC5XfslyQwSs5pnVsVt+AhsDAh4BAhkBAwsJBwIVCAIiAQAAtgwB
10+
AOa3rnipQPsxgxvOP1V+2kD6ssiwt6BZRWwPcyfeX1h4AP9ozBYr+PSmNbam9bnq
11+
wgXwuQhPJeWTSgILMaiasugGCMeLBGDfhE4SCisGAQQBl1UBBQEBB0ClFQJX/L2G
12+
EX9ucC5mvwj3X/7aDXDFAmIpQeWYSS1negMBCgn+CQMIF1uko+Ym3thgoDWUgM5e
13+
MNmDG3rYkTa7h6mlhhrsYtE/GN78EJHP1ygFzOczU/YdbxSRTZCu697uPCZLWURV
14+
1+b66KLTMNHNaAkoFb2JC8J4BBgWCAAqBQJg34ROCRAErOaZ1bFbfhYhBJQ0DPsS
15+
HC5XfslyQwSs5pnVsVt+AhsMAAB1CgEApNcEivCSp0f8CnV4UCoSRRRekIbP1Ub2
16+
GJx6iRJR8xwA/jicDxdnl/Umfd3mWjGk04R47whiDOXdwjBmC1KVBaMH
17+
=Sfsa
18+
-----END PGP PRIVATE KEY BLOCK-----

0 commit comments

Comments
 (0)