aboutsummaryrefslogtreecommitdiff
path: root/format-common/src/test/kotlin
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2021-04-18 02:48:59 +0530
committerHarsh Shandilya <me@msfjarvis.dev>2021-04-18 04:03:17 +0530
commit77abe7ee2c906747d80813fef8d786b3e8d94c0a (patch)
tree8146a514df53fc6e51e5288e7cf7e46673247740 /format-common/src/test/kotlin
parent931cc052a8f22b96e11d4bd8f9d069ef8ff92e21 (diff)
format-common: initial API for PasswordEntry
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'format-common/src/test/kotlin')
-rw-r--r--format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt183
-rw-r--r--format-common/src/test/kotlin/dev/msfjarvis/aps/util/time/TestClocks.kt27
-rw-r--r--format-common/src/test/kotlin/dev/msfjarvis/aps/util/totp/OtpTest.kt73
3 files changed, 283 insertions, 0 deletions
diff --git a/format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt b/format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt
new file mode 100644
index 00000000..2520853c
--- /dev/null
+++ b/format-common/src/test/kotlin/dev/msfjarvis/aps/data/passfile/PasswordEntryTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.data.passfile
+
+import dev.msfjarvis.aps.util.time.TestUserClock
+import dev.msfjarvis.aps.util.totp.TotpFinder
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import kotlin.time.ExperimentalTime
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScope
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class, ExperimentalTime::class)
+internal class PasswordEntryTest {
+
+ private fun makeEntry(content: String) = PasswordEntry(fakeClock, testFinder, testScope, content.encodeToByteArray())
+
+ @Test
+ fun testGetPassword() {
+ assertEquals("fooooo", makeEntry("fooooo\nbla\n").password)
+ assertEquals("fooooo", makeEntry("fooooo\nbla").password)
+ assertEquals("fooooo", makeEntry("fooooo\n").password)
+ assertEquals("fooooo", makeEntry("fooooo").password)
+ assertEquals("", makeEntry("\nblubb\n").password)
+ assertEquals("", makeEntry("\nblubb").password)
+ assertEquals("", makeEntry("\n").password)
+ assertEquals("", makeEntry("").password)
+ for (field in PasswordEntry.PASSWORD_FIELDS) {
+ assertEquals("fooooo", makeEntry("\n$field fooooo").password)
+ assertEquals("fooooo", makeEntry("\n${field.toUpperCase()} fooooo").password)
+ assertEquals("fooooo", makeEntry("GOPASS-SECRET-1.0\n$field fooooo").password)
+ assertEquals("fooooo", makeEntry("someFirstLine\nUsername: bar\n$field fooooo").password)
+ }
+ }
+
+ @Test
+ fun testGetExtraContent() {
+ assertEquals("bla\n", makeEntry("fooooo\nbla\n").extraContentString)
+ assertEquals("bla", makeEntry("fooooo\nbla").extraContentString)
+ assertEquals("", makeEntry("fooooo\n").extraContentString)
+ assertEquals("", makeEntry("fooooo").extraContentString)
+ assertEquals("blubb\n", makeEntry("\nblubb\n").extraContentString)
+ assertEquals("blubb", makeEntry("\nblubb").extraContentString)
+ assertEquals("blubb", makeEntry("blubb\npassword: foo").extraContentString)
+ assertEquals("blubb", makeEntry("password: foo\nblubb").extraContentString)
+ assertEquals("blubb\nusername: bar", makeEntry("blubb\npassword: foo\nusername: bar").extraContentString)
+ assertEquals("", makeEntry("\n").extraContentString)
+ assertEquals("", makeEntry("").extraContentString)
+ }
+
+ @Test
+ fun parseExtraContentWithoutAuth() {
+ var entry = makeEntry("username: abc\npassword: abc\ntest: abcdef")
+ assertEquals(1, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("test"))
+ assertEquals("abcdef", entry.extraContent["test"])
+
+ entry = makeEntry("username: abc\npassword: abc\ntest: :abcdef:")
+ assertEquals(1, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("test"))
+ assertEquals(":abcdef:", entry.extraContent["test"])
+
+ entry = makeEntry("username: abc\npassword: abc\ntest : ::abc:def::")
+ assertEquals(1, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("test"))
+ assertEquals("::abc:def::", entry.extraContent["test"])
+
+ entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\ntest2: ghijkl")
+ assertEquals(2, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("test2"))
+ assertEquals("ghijkl", entry.extraContent["test2"])
+
+ entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\n: ghijkl\n mnopqr:")
+ assertEquals(2, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("Extra Content"))
+ assertEquals(": ghijkl\n mnopqr:", entry.extraContent["Extra Content"])
+
+ entry = makeEntry("username: abc\npassword: abc\n:\n\n")
+ assertEquals(1, entry.extraContent.size)
+ assertTrue(entry.extraContent.containsKey("Extra Content"))
+ assertEquals(":", entry.extraContent["Extra Content"])
+ }
+
+ @Test
+ fun testGetUsername() {
+ for (field in PasswordEntry.USERNAME_FIELDS) {
+ assertEquals("username", makeEntry("\n$field username").username)
+ assertEquals("username", makeEntry("\n${field.toUpperCase()} username").username)
+ }
+ assertEquals("username", makeEntry("secret\nextra\nlogin: username\ncontent\n").username)
+ assertEquals("username", makeEntry("\nextra\nusername: username\ncontent\n").username)
+ assertEquals("username", makeEntry("\nUSERNaMe: username\ncontent\n").username)
+ assertEquals("username", makeEntry("\nlogin: username").username)
+ assertEquals("foo@example.com", makeEntry("\nemail: foo@example.com").username)
+ assertEquals("username", makeEntry("\nidentity: username\nlogin: another_username").username)
+ assertEquals("username", makeEntry("\nLOGiN:username").username)
+ assertNull(makeEntry("secret\nextra\ncontent\n").username)
+ }
+
+ @Test
+ fun testHasUsername() {
+ assertNotNull(makeEntry("secret\nextra\nlogin: username\ncontent\n").username)
+ assertNull(makeEntry("secret\nextra\ncontent\n").username)
+ assertNull(makeEntry("secret\nlogin failed\n").username)
+ assertNull(makeEntry("\n").username)
+ assertNull(makeEntry("").username)
+ }
+
+ @Test
+ fun testGeneratesOtpFromTotpUri() {
+ val entry = makeEntry("secret\nextra\n$TOTP_URI")
+ assertTrue(entry.hasTotp())
+ val code = entry.totp.value
+ assertNotNull(code) { "Generated OTP cannot be null" }
+ assertEquals("818800", code)
+ }
+
+ @Test
+ fun testGeneratesOtpWithOnlyUriInFile() {
+ val entry = makeEntry(TOTP_URI)
+ assertNull(entry.password)
+ assertTrue(entry.hasTotp())
+ val code = entry.totp.value
+ assertNotNull(code) { "Generated OTP cannot be null" }
+ assertEquals("818800", code)
+ }
+
+ @Test
+ fun testOnlyLooksForUriInFirstLine() {
+ val entry = makeEntry("id:\n$TOTP_URI")
+ assertNotNull(entry.password)
+ assertTrue(entry.hasTotp())
+ assertNull(entry.username)
+ }
+
+ // https://github.com/android-password-store/Android-Password-Store/issues/1190
+ @Test
+ fun extraContentWithMultipleUsernameFields() {
+ val entry = makeEntry("pass\nuser: user\nid: id\n$TOTP_URI")
+ assertTrue(entry.extraContent.isNotEmpty())
+ assertTrue(entry.hasTotp())
+ assertNotNull(entry.username)
+ assertEquals("pass", entry.password)
+ assertEquals("user", entry.username)
+ assertEquals(mapOf("id" to "id"), entry.extraContent)
+ }
+
+ companion object {
+
+ const val TOTP_URI =
+ "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30"
+
+ val testScope = TestCoroutineScope()
+
+ val fakeClock = TestUserClock()
+
+ // This implementation is hardcoded for the URI above.
+ val testFinder =
+ object : TotpFinder {
+ override fun findSecret(content: String): String {
+ return "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"
+ }
+
+ override fun findDigits(content: String): String {
+ return "6"
+ }
+
+ override fun findPeriod(content: String): Long {
+ return 30
+ }
+
+ override fun findAlgorithm(content: String): String {
+ return "SHA1"
+ }
+ }
+ }
+}
diff --git a/format-common/src/test/kotlin/dev/msfjarvis/aps/util/time/TestClocks.kt b/format-common/src/test/kotlin/dev/msfjarvis/aps/util/time/TestClocks.kt
new file mode 100644
index 00000000..d75cc900
--- /dev/null
+++ b/format-common/src/test/kotlin/dev/msfjarvis/aps/util/time/TestClocks.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.util.time
+
+import java.time.Clock
+import java.time.Instant
+import java.time.ZoneId
+import java.time.ZoneOffset.UTC
+
+/**
+ * Implementation of [UserClock] that is fixed to [Instant.EPOCH] for deterministic time-based tests
+ */
+internal class TestUserClock(instant: Instant) : UserClock() {
+
+ constructor() : this(Instant.EPOCH)
+
+ private var clock = fixed(instant, UTC)
+
+ override fun withZone(zone: ZoneId): Clock = clock.withZone(zone)
+
+ override fun getZone(): ZoneId = UTC
+
+ override fun instant(): Instant = clock.instant()
+}
diff --git a/format-common/src/test/kotlin/dev/msfjarvis/aps/util/totp/OtpTest.kt b/format-common/src/test/kotlin/dev/msfjarvis/aps/util/totp/OtpTest.kt
new file mode 100644
index 00000000..e6dc372d
--- /dev/null
+++ b/format-common/src/test/kotlin/dev/msfjarvis/aps/util/totp/OtpTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.util.totp
+
+import com.github.michaelbull.result.get
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import org.junit.Test
+
+internal class OtpTest {
+
+ @Test
+ fun testOtpGeneration6Digits() {
+ assertEquals("953550", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333298159 / (1000 * 30), "SHA1", "6").get())
+ assertEquals("275379", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333571918 / (1000 * 30), "SHA1", "6").get())
+ assertEquals("867507", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333600517 / (1000 * 57), "SHA1", "6").get())
+ }
+
+ @Test
+ fun testOtpGeneration10Digits() {
+ assertEquals("0740900914", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333655044 / (1000 * 30), "SHA1", "10").get())
+ assertEquals("0070632029", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333691405 / (1000 * 30), "SHA1", "10").get())
+ assertEquals("1017265882", Otp.calculateCode("JBSWY3DPEHPK3PXP", 1593333728893 / (1000 * 83), "SHA1", "10").get())
+ }
+
+ @Test
+ fun testOtpGenerationIllegalInput() {
+ assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXP", 10000, "SHA0", "10").get())
+ assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXP", 10000, "SHA1", "a").get())
+ assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXP", 10000, "SHA1", "5").get())
+ assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXP", 10000, "SHA1", "11").get())
+ assertNull(Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAB", 10000, "SHA1", "6").get())
+ }
+
+ @Test
+ fun testOtpGenerationUnusualSecrets() {
+ assertEquals(
+ "127764",
+ Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAAAAA", 1593367111963 / (1000 * 30), "SHA1", "6").get()
+ )
+ assertEquals("047515", Otp.calculateCode("JBSWY3DPEHPK3PXPAAAAA", 1593367171420 / (1000 * 30), "SHA1", "6").get())
+ }
+
+ @Test
+ fun testOtpGenerationUnpaddedSecrets() {
+ // Secret was generated with `echo 'string with some padding needed' | base32`
+ // We don't care for the resultant OTP's actual value, we just want both the padded and
+ // unpadded variant to generate the same one.
+ val unpaddedOtp =
+ Otp.calculateCode(
+ "ON2HE2LOM4QHO2LUNAQHG33NMUQHAYLEMRUW4ZZANZSWKZDFMQFA",
+ 1593367171420 / (1000 * 30),
+ "SHA1",
+ "6"
+ )
+ .get()
+ val paddedOtp =
+ Otp.calculateCode(
+ "ON2HE2LOM4QHO2LUNAQHG33NMUQHAYLEMRUW4ZZANZSWKZDFMQFA====",
+ 1593367171420 / (1000 * 30),
+ "SHA1",
+ "6"
+ )
+ .get()
+ assertNotNull(unpaddedOtp)
+ assertNotNull(paddedOtp)
+ assertEquals(unpaddedOtp, paddedOtp)
+ }
+}