Browse Source

Add account classes and URI parser

master
Chris Smith 5 years ago
parent
commit
25f973ef10

+ 8
- 0
app/src/main/java/com/chameth/yaotp/accounts/Accounts.kt View File

@@ -0,0 +1,8 @@
1
+package com.chameth.yaotp.accounts
2
+
3
+import com.chameth.yaotp.algos.HotpParams
4
+import com.chameth.yaotp.algos.TotpParams
5
+
6
+sealed class Account(val label: String, val issuer: String? = null)
7
+class HotpAccount(label: String, issuer: String?, val counter: Long, val params: HotpParams) : Account(label, issuer)
8
+class TotpAccount(label: String, issuer: String?, val params: TotpParams) : Account(label, issuer)

+ 55
- 0
app/src/main/java/com/chameth/yaotp/accounts/UriParser.kt View File

@@ -0,0 +1,55 @@
1
+package com.chameth.yaotp.accounts
2
+
3
+import com.chameth.yaotp.algos.HotpParams
4
+import com.chameth.yaotp.algos.TotpParams
5
+import com.chameth.yaotp.algos.getHmacFunc
6
+import com.chameth.yaotp.util.base32Decode
7
+import java.net.URI
8
+
9
+fun parseUri(uri: String) : Account? {
10
+    return try {
11
+        toAccount(URI.create(uri))
12
+    } catch (_ : Exception) {
13
+        null
14
+    }
15
+}
16
+
17
+@Throws(IllegalArgumentException::class)
18
+internal fun toAccount(uri: URI): Account? {
19
+    require(uri.scheme == "otpauth") { "Scheme must be 'otpauth', got '${uri.scheme}'" }
20
+    require(uri.path.length > 1) { "No label specified" }
21
+    requireNotNull(uri.query) { "No query string supplied" }
22
+
23
+    val label = uri.path.substring(1)
24
+    val params = uri.queryParameters()
25
+    require("secret" in params) { "No 'secret' parameter specified, got ${params.keys}"}
26
+
27
+    return when(uri.host) {
28
+        "hotp" -> getHotpAccount(label, params)
29
+        "totp" -> getTotpAccount(label, params)
30
+        else -> throw IllegalArgumentException("Unrecognised type: ${uri.host}")
31
+    }
32
+}
33
+
34
+internal fun getHotpAccount(label: String, uriParams: Map<String, String>): HotpAccount {
35
+    val counter = uriParams["counter"]?.toLongOrNull()
36
+    requireNotNull(counter) { "No 'counter' parameter specified for HOTP" }
37
+    return HotpAccount(label, uriParams["issuer"], counter!!, getHotpParameters(uriParams))
38
+}
39
+
40
+fun getTotpAccount(label: String, uriParams: Map<String, String>): TotpAccount {
41
+    val step = uriParams["step"]?.toIntOrNull() ?: 30
42
+    return TotpAccount(label, uriParams["issuer"], TotpParams(getHotpParameters(uriParams), step = step))
43
+}
44
+
45
+internal fun getHotpParameters(uriParams : Map<String, String>) : HotpParams {
46
+    return HotpParams(
47
+            base32Decode(uriParams["secret"] ?: ""),
48
+            uriParams["digits"]?.toIntOrNull() ?: 6,
49
+            getHmacFunc(uriParams["algorithm"])
50
+    )
51
+}
52
+
53
+internal fun URI.queryParameters(): Map<String, String> {
54
+    return query.split('&').map { with(it.split('=', limit = 2)) { this[0] to this[1] } }.toMap()
55
+}

+ 9
- 0
app/src/main/java/com/chameth/yaotp/algos/Hmac.kt View File

@@ -5,6 +5,15 @@ import javax.crypto.spec.SecretKeySpec
5 5
 
6 6
 typealias HmacFunc = (ByteArray, ByteArray) -> ByteArray
7 7
 
8
+fun getHmacFunc(name: String?): HmacFunc {
9
+    return when (name?.toLowerCase()) {
10
+        "sha1" -> ::hmacSha1
11
+        "sha256" -> ::hmacSha256
12
+        "sha512" -> ::hmacSha512
13
+        else -> ::hmacSha1
14
+    }
15
+}
16
+
8 17
 fun hmacSha1(keyMaterial: ByteArray, input: ByteArray) = hmac(keyMaterial, input, "HmacSHA1")
9 18
 fun hmacSha256(keyMaterial: ByteArray, input: ByteArray) = hmac(keyMaterial, input, "HmacSHA256")
10 19
 fun hmacSha512(keyMaterial: ByteArray, input: ByteArray) = hmac(keyMaterial, input, "HmacSHA512")

+ 139
- 0
app/src/test/java/com/chameth/yaotp/accounts/UriParserTest.kt View File

@@ -0,0 +1,139 @@
1
+package com.chameth.yaotp.accounts
2
+
3
+import com.chameth.yaotp.algos.hmacSha1
4
+import com.chameth.yaotp.algos.hmacSha256
5
+import com.chameth.yaotp.algos.hmacSha512
6
+import com.chameth.yaotp.toHexString
7
+import org.junit.Assert
8
+import org.junit.Rule
9
+import org.junit.Test
10
+import org.junit.rules.ExpectedException
11
+import java.net.URI
12
+
13
+class UriParserTest {
14
+
15
+    @get:Rule
16
+    val expectedExceptionRule = ExpectedException.none()
17
+
18
+    @Test
19
+    fun testToAccount_throwsForInvalidScheme() {
20
+        expectedExceptionRule.expect(IllegalArgumentException::class.java)
21
+        expectedExceptionRule.expectMessage("Scheme must be 'otpauth', got 'foo'")
22
+
23
+        toAccount(URI.create("foo://hotp/baz?secret=23"))
24
+    }
25
+
26
+    @Test
27
+    fun testToAccount_throwsForInvalidType() {
28
+        expectedExceptionRule.expect(IllegalArgumentException::class.java)
29
+        expectedExceptionRule.expectMessage("Unrecognised type: bar")
30
+
31
+        toAccount(URI.create("otpauth://bar/baz?secret=23"))
32
+    }
33
+
34
+    @Test
35
+    fun testToAccount_throwsForMissingLabel() {
36
+        expectedExceptionRule.expect(IllegalArgumentException::class.java)
37
+        expectedExceptionRule.expectMessage("No label specified")
38
+
39
+        toAccount(URI.create("otpauth://hotp/?secret=23"))
40
+    }
41
+
42
+    @Test
43
+    fun testToAccount_throwsForNoQueryString() {
44
+        expectedExceptionRule.expect(IllegalArgumentException::class.java)
45
+        expectedExceptionRule.expectMessage("No query string supplied")
46
+
47
+        toAccount(URI.create("otpauth://totp/foo"))
48
+    }
49
+
50
+    @Test
51
+    fun testToAccount_throwsForNoSecret() {
52
+        expectedExceptionRule.expect(IllegalArgumentException::class.java)
53
+        expectedExceptionRule.expectMessage("No 'secret' parameter specified, got [secrat]")
54
+
55
+        toAccount(URI.create("otpauth://totp/foo?secrat=23"))
56
+    }
57
+
58
+    @Test
59
+    fun testToAccount_throwsForNoCounter() {
60
+        expectedExceptionRule.expect(IllegalArgumentException::class.java)
61
+        expectedExceptionRule.expectMessage("No 'counter' parameter specified for HOTP")
62
+
63
+        toAccount(URI.create("otpauth://hotp/foo?secret=23"))
64
+    }
65
+
66
+    @Test
67
+    fun testToAccount_returnsHotpAccount_withDefaultArgs() {
68
+        val account = toAccount(URI.create("otpauth://hotp/foo?secret=ABC234&counter=123")) as HotpAccount
69
+        Assert.assertNull(account.issuer)
70
+        Assert.assertEquals("foo", account.label)
71
+        Assert.assertEquals(::hmacSha1, account.params.hmacFunc)
72
+        Assert.assertEquals(6, account.params.length)
73
+        Assert.assertEquals("0045adf4", account.params.key.toHexString())
74
+        Assert.assertEquals(123L, account.counter)
75
+    }
76
+
77
+    @Test
78
+    fun testToAccount_returnsHotpAccount_withIssuer() {
79
+        val account = toAccount(URI.create("otpauth://hotp/foo?secret=ABC234&issuer=BigCorp%20Inc&counter=0")) as HotpAccount
80
+        Assert.assertEquals("BigCorp Inc", account.issuer)
81
+    }
82
+
83
+    @Test
84
+    fun testToAccount_returnsHotpAccount_withCustomDigits() {
85
+        val account = toAccount(URI.create("otpauth://hotp/foo?secret=ABC234&digits=8&counter=0")) as HotpAccount
86
+        Assert.assertEquals(8, account.params.length)
87
+    }
88
+
89
+    @Test
90
+    fun testToAccount_returnsHotpAccount_withInvalidDigits() {
91
+        val account = toAccount(URI.create("otpauth://hotp/foo?secret=ABC234&digits=HALLO&counter=0")) as HotpAccount
92
+        Assert.assertEquals(6, account.params.length)
93
+    }
94
+
95
+    @Test
96
+    fun testToAccount_returnsHotpAccount_withSha256() {
97
+        val account = toAccount(URI.create("otpauth://hotp/foo?secret=ABC234&algorithm=Sha256&counter=0")) as HotpAccount
98
+        Assert.assertEquals(::hmacSha256, account.params.hmacFunc)
99
+    }
100
+
101
+    @Test
102
+    fun testToAccount_returnsHotpAccount_withSha512() {
103
+        val account = toAccount(URI.create("otpauth://hotp/foo?secret=ABC234&algorithm=SHA512&counter=0")) as HotpAccount
104
+        Assert.assertEquals(::hmacSha512, account.params.hmacFunc)
105
+    }
106
+
107
+    @Test
108
+    fun testToAccount_returnsTotpAccount_withDefaultArgs() {
109
+        val account = toAccount(URI.create("otpauth://totp/foo?secret=ABC234")) as TotpAccount
110
+        Assert.assertNull(account.issuer)
111
+        Assert.assertEquals("foo", account.label)
112
+        Assert.assertEquals(::hmacSha1, account.params.hotpParams.hmacFunc)
113
+        Assert.assertEquals(6, account.params.hotpParams.length)
114
+        Assert.assertEquals("0045adf4", account.params.hotpParams.key.toHexString())
115
+        Assert.assertEquals(0, account.params.startTime)
116
+        Assert.assertEquals(30, account.params.step)
117
+    }
118
+
119
+    @Test
120
+    fun testToAccount_returnsTotpAccount_withCustomStep() {
121
+        val account = toAccount(URI.create("otpauth://totp/foo?secret=ABC234&step=60")) as TotpAccount
122
+        Assert.assertEquals(60, account.params.step)
123
+    }
124
+
125
+    @Test
126
+    fun testParseUri_returnsAccountIfValid() {
127
+        Assert.assertNotNull(parseUri("otpauth://totp/foo?secret=ABC234&step=60"))
128
+        Assert.assertNotNull(parseUri("otpauth://hotp/foo?secret=ABC234&counter=60"))
129
+    }
130
+
131
+    @Test
132
+    fun testParseUri_returnsNullIfInvalid() {
133
+        Assert.assertNull(parseUri(""))
134
+        Assert.assertNull(parseUri("NOT A URI"))
135
+        Assert.assertNull(parseUri("foo://hotp/baz?secret=23"))
136
+        Assert.assertNull(parseUri("otpauth://hotp/baz?secret=23"))
137
+    }
138
+
139
+}

+ 26
- 0
app/src/test/java/com/chameth/yaotp/algos/HmacTest.kt View File

@@ -3,6 +3,7 @@ package com.chameth.yaotp.algos
3 3
 import com.chameth.yaotp.toHexString
4 4
 import com.natpryce.hamkrest.assertion.assert
5 5
 import com.natpryce.hamkrest.equalTo
6
+import org.junit.Assert
6 7
 import org.junit.Test
7 8
 
8 9
 class HmacTest {
@@ -34,4 +35,29 @@ class HmacTest {
34 35
         assert.that(hmacSha512(key, input).toHexString(), equalTo(expected))
35 36
     }
36 37
 
38
+    @Test
39
+    fun testGetHmacFunc_defaultsToSha1() {
40
+        Assert.assertEquals(::hmacSha1, getHmacFunc(null))
41
+        Assert.assertEquals(::hmacSha1, getHmacFunc("foo"))
42
+        Assert.assertEquals(::hmacSha1, getHmacFunc("sha25618"))
43
+    }
44
+
45
+    @Test
46
+    fun testGetHmacFunc_withSha1() {
47
+        Assert.assertEquals(::hmacSha1, getHmacFunc("sha1"))
48
+        Assert.assertEquals(::hmacSha1, getHmacFunc("SHA1"))
49
+    }
50
+
51
+    @Test
52
+    fun testGetHmacFunc_withSha256() {
53
+        Assert.assertEquals(::hmacSha256, getHmacFunc("sha256"))
54
+        Assert.assertEquals(::hmacSha256, getHmacFunc("SHA256"))
55
+    }
56
+
57
+    @Test
58
+    fun testGetHmacFunc_withSha512() {
59
+        Assert.assertEquals(::hmacSha512, getHmacFunc("sha512"))
60
+        Assert.assertEquals(::hmacSha512, getHmacFunc("SHA512"))
61
+    }
62
+
37 63
 }

Loading…
Cancel
Save